所有动态
- 前几天
-
SEVEN SONG注册了
-
in...注册了
-
heess注册了
- 之前的
-
-
Good afternoon! I would appreciate your advice.
Hello! I sent a request but haven't received a response yet. Please contact me via WhatsApp or by phone. wa.me/+380951034806
-
JuliaTek注册了
-
-
Aviator Pin Up игра онлайн в Таджикистане
Авиатор простой, но эмоций хватает ловил хороший множитель, ощущения мощные. Я начинал отсюда https://aviator-tj.com/ рега быстрая.
-
AviaTog注册了
-
xsgdeofqeu注册了
-
ydvikgpjml注册了
-
kgfifznzsy注册了
-
dgzjnvykfs注册了
-
150 亿美元 BTC 易主:美司法部剿灭柬埔寨太子集团,摇身一变全球最大 BTC 巨鲸
作者:Ethan;编辑:秦晓峰 出品 | Odaily星球日报 美国纽约东区联邦法院的一纸诉状,在加密世界掀起巨浪。 10 月 14 日,美国司法部宣布对柬埔寨太子集团创始人陈志提起刑事指控,并申请没收其控制的 127,271 枚 BTC,市值约 150 亿美元,使之成为全球规模最大的比特币司法没收案。 “历史上最重大的一次虚拟资产没收行动。”司法部在公告中给出了极具警示性的措辞。并且,官方特别强调,这批 BTC 并非存放在交易平台,而是长期由陈志本人通过未托管的私人钱包保管。这似乎动摇了加密社区的核心信条:“掌握私钥,资产即不可剥夺。” 实际上,即便不破解加密算法,美国政府依然可以通过法律程序完成资产的“司法转移”。通过链上追踪与国际协作,执法部门锁定了分散于多个地址、但均属陈志控制的比特币。法院随后发布查封令,将这些资产合法转移至美国政府控制的地址,进入司法托管程序,等待最终的民事没收判决。 与此同时,美国财政部海外资产控制办公室将“太子集团”列为跨国犯罪组织,并对 146 个相关个人及实体实施制裁;美国金融犯罪执法网络则依据《爱国者法案》,将 Huione Group 认定为“主要洗钱关注对象”,禁止其接入美元清算系统。英国也同步对陈志及其家族成员执行了资产冻结与旅行禁令。 在加密市场的语境中,这一刻极具象征意义。它不仅是针对一个犯罪集团的执法,更是一次国家机关对链上资产直接行使控制权的公开展示。127,271 枚 BTC——这个足以改变市场情绪与监管走向的数字,已被写入比特币监管史,成为一个关键标记。 从福建商人到诈骗帝国:陈志的资本布局与产业化罪行 美国司法部的起诉书,揭开了陈志及其太子集团的另一副面孔。 在东南亚媒体的报道中,陈志曾是“柬埔寨新贵”,其掌控的太子集团也被宣传为业务遍及房地产、金融等领域的跨国财团。然而,美司法部指控其背后存在一套“双层运行逻辑”:对外是合法的商业帝国,对内则是一个为诈骗收益服务的资金控制与清算系统。 陈志原籍福建,早年在柬埔寨凭借博彩与地产行业发迹。2014 年获得柬埔寨国籍后,他通过政商关系迅速获取了多项开发许可与金融牌照。此后,他并未止步于本地业务,而是通过设立英属维尔京群岛公司、新加坡控股架构等方式,构建了复杂的跨国资产配置,并疑似持有英国身份,从而在不同司法管辖区之间制造壁垒。2024 年 4 月,柬埔寨国王更签发王令,任命陈志为参议院主席洪森的顾问,这显示了其在当地深厚的政商根基。 Prince Group 的两层业务结构梳理 资金溯源:从黑客劫掠到诈骗洗白 本案中 127,271 枚 BTC 的来历尤为复杂。根据 Elliptic 与 Arkham Intelligence 等链上分析机构的报告,这批比特币与 2020 年一家名为“LuBian”的大型矿企被盗事件高度重合。 记录显示,2020 年 12 月,LuBian 的核心钱包发生异常转移,约 127,426 枚 BTC 被窃。链上甚至留下了 LuBian 向黑客地址发出的带信息小额交易:“Please return our funds, we'll pay a reward”。此后这批巨额资金长期沉寂,直到 2024 年中才开始活跃,其移动路径与太子集团控制的钱包集群出现重叠。(最新动态:10 月 15 日,LuBian 相关钱包沉寂三年后转移全部 9757 枚 BTC,价值 11 亿美元(https://www.odaily.news/zh-CN/newsflash/452472)) 这意味着,调查揭示的并非简单的“诈骗-洗钱”链条,而是一条更复杂的路径:“黑客劫掠矿场→长期潜伏→被犯罪组织纳入资金池→试图通过矿业与场外交易洗白”。这一发现将本案提升至一个新的复杂度:它既涉及黑客攻击与矿业安全漏洞,也揭示了灰色兑换网络如何吸纳并隐藏来源异常的巨大资金。 比特币是如何被查封的? 对于加密货币行业而言,本案的深远影响远不止于扳倒一个诈骗头目,更在于司法与情报机构完整演示了一套针对链上资产的处置流程:链上定位 → 金融封锁 → 司法接管。这是一次将“链上追踪能力”与“传统司法权力”无缝衔接的实战闭环。 第一步:链上追踪——锁定“资金容器” 比特币的匿名性常被误解。事实上,其区块链是公开账本,每一笔转账都留有痕迹。陈志集团试图通过经典的“喷洒-漏斗”模式来洗钱:将主钱包的资金像喷壶洒水般分散至海量中间地址,经过短暂停留后,再如溪流汇入大江般重新聚合至少数核心地址。 这种操作看似复杂,但在链上分析视角下,频繁的“分散-汇聚”行为反而会形成独特的图谱特征。调查机构(如 TRM Labs、Chainalysis)利用聚类算法,精准绘制出“资金回流地图”,最终证实:这些看似分散的地址,均指向同一个控制实体——太子集团。 第二步:金融制裁——切断“变现通道” 在锁定了链上资产后,美国当局随之启动双重金融制裁: 财政部(OFAC)制裁:将陈志及相关实体列入名单,任何受美国管辖的机构不得与其交易。 金融犯罪执法局(FinCEN)§311 条款:将关键实体列为“主要洗钱关注对象”,彻底切断其接入美元清算体系的路径。 至此,这些比特币虽然在链上仍可被私钥控制,但其最重要的价值属性——“兑换成美元的能力”已被冻结。 第三步:司法接管——完成“权属转移” 最终的没收并非依靠暴力破解私钥,而是通过法律程序直接接管“签名权”。执法人员依据搜查令,获取助记词、硬件钱包或交易权限,随后像资产的原主人一样,发起一笔合法的转账交易,将比特币转入政府控制的托管地址。 当这笔交易被区块链网络确认的那一刻,“法律上的所有权”与“链上的控制权”便实现了统一。这 127,271 枚 BTC 的归属权,在技术和法律双重意义上,正式从陈志转移至美国政府手中。这套组合拳清晰地昭示:在国家权力面前,“链上资产不可剥夺”并非绝对。 没收之后,比特币会去哪? 当 127,271 枚 BTC 从诈骗帝国的钱包转入“U.S. Government Controlled Wallet”后,一个更具战略意义的问题浮现:这批巨量资产的最终去向,将揭示美国政府如何定位比特币——是亟待变现的“赃物”,还是可纳入囊中的“战略资产”? 历史上,美国政府处理没收数字资产的方式大致分为几类。Silk Road 案件中的比特币是在完成司法程序后通过公开拍卖的方式转让给私人机构投资者,例如 Tim Draper 就是其中一批拍卖买家。Colonial Pipeline 勒索赎金中的 BTC 则在追回后被司法部暂时留在政府账户,用作案件证据和财政部记录用途。至于 FTX,目前的状态还停留在司法托管阶段,官方未正式确认将没收资产归为政府所有,其中大部分资产理论上应在债权清算程序内用于赔偿用户,而不是直接列入国库储备。 与以上通过公开拍卖处理没收比特币(如 Silk Road 案)的方式不同的是,本案正面临一个关键变量:2025 年 3 月,美国白宫已签署行政令,建立了“战略比特币储备”机制。 这意味着,陈志案中这批 BTC 很可能不再被简单拍卖,而是直接转为由国家持有的储备资产。 由此,美国正在构建一个前所未有的“链上资产监管闭环”:通过链上追踪锁定目标——利用制裁手段切断其法币出口——通过司法程序完成法律上的所有权剥夺——最终将资产转入政府控制。这套流程的核心,并非限制市场流通,而是对“密钥控制权”的合法归属进行重新定义。 一旦司法程序确认资产为犯罪所得,其属性便从“个人控制的加密货币”,转变为“国家管辖下的数字资产凭证”。 随着 127,271 枚 BTC 的转移,美国已然成为全球持有比特币最多的主权实体。 这不仅是一次史无前例的没收行动,更预示着国家力量对链上资产进行系统性管控的时代,已经开启。 原文链接
-
赵长鹏被特赦前的180天
2025 年 10 月 23 日半夜,华人加密首富赵长鹏,被宣布得到美国特朗普的总统特赦。 根据公开信息,特朗普总统此前向顾问表示,他对赵长鹏面临「美国政治迫害」的说法表示同情。 然而就在一年之前,2024 年 4 月 30 日,赵长鹏正经历人生至暗时刻:他被脱光衣服,接受羞辱性的搜身,展示臀部,然后被关进一间冰冷的牢房。狱友们是满脸刺青、头上也刻着图案的肌肉壮汉 [1]。 在西雅图的联邦法院里,赵长鹏穿着囚服,承认了自己的罪名。这个被称作「华人首富」的男人,主动选择交出 43 亿美元罚金,并在媒体面前说了一句:「我是选择主动交政治罚金。」 倘若那时有人告诉他:一年后你不但能被美国特赦,还能够重新踏上中国,而此刻西雅图牢房里的冷眼与拳脚,连同那 43 亿的政治罚金,都会清零。他大概心里会骂一句,这算哪门子黑色幽默。 关于赵长鹏特赦的信号,是从 2025 年 9 月 17 日开始释放。当天,CZ 突然更新自己推特简介,将「ex-@binance」改回「@binance」。某种程度,表明自己能够回归币安的尘埃落定。 同时,就在 10 月,美国两大「合规交易入口」几乎前后脚给出信号:加密上市平台 Coinbase 与受 SEC 监管的主流券商 Robinhood,先后开放了 BNB 交易,这枚来自赵长鹏创立的 Binance 生态的平台币,首次在美国主流金融体系中获得了正式入口。 白宫新闻秘书卡罗琳·莱维特(Karoline Leavitt)宣布特赦生效,同时强调:「拜登政府对加密货币的战争,至此画上句号。」 我们把时间拨回到特赦前的 180 天。当华人首富、币安创始人赵长鹏距离「政治特赦」一步之遥时,他在做什么? 01. 2025 年的春天,维多利亚港的空气里弥漫着一丝久违的热闹,一张合影迅速刷屏。 合影 C 位里站着四个人:火币创始人李林、赵长鹏 CZ、孙宇晨,以及孔剑平。 合影 C 位左九 Huobi 创始人李林,左十 币安创始人赵长鹏 CZ,左六 孔剑平 对旁人来说,这只是币圈几位大佬的合照,但在懂行的人眼里,这一幕本身就是信号。 八年前,中国全面叫停 ICO 和交易平台,币安匆忙出海,赵长鹏成了「最不可能再回国的人」。八年后,他却在这张合影里重新出现。这是他是与本地资本和制度重新建立链接的开场白。 这场聚会的东道主李林,曾经世界排名前三的交易平台之一,Huobi 的创始人,3 年前,他把这家自己亲手创立的企业,卖给了同在饭局上的孙宇晨。而这场饭局结束后,和 CZ 产生最多联系的,则是站在他们身边的孔剑平。 孔剑平曾经是知名矿机厂商嘉楠科技的董事会联席主席,2020 年,他创立了 Nano Labs 并担任董事长,同时也是香港数码港董事、香港「推动 Web3 发展专责小组」成员,甚至还获香港特区政府财经事务及库务局局长委任,出任该审裁处委员 [2]。 这场聚会结束两个月后,孔剑平高调宣布要做一个 10 亿美元的 BNB 财库,目标囤下 5%–10% 的流通量,把币安平台币 BNB 包装进「美股上市公司」。 CZ 的推特亲自转发,市场情绪立马点燃,股价一路飙升,盘中涨幅甚至高达 107% 。赵长鹏强调,他和他关联的实体「没有参与此轮融资」。不过,他们「仍然非常支持」。 此后,赵长鹏在香港的多数公开演讲活动背后,都能看到孔建平的身影。 四个月后,当赵长鹏第二次回到香港,他已经不再只是币安活动中的「神秘嘉宾」,而是带着明确的议程而来:一方面,在活动前官宣与华兴资本的合作;另一方面,在活动结束后敲定了与 OSL 的对接。前后呼应,标志着他在香港的落地路径逐渐清晰。 华兴资本的故事,与币安有很多共通点,光鲜与游离并存。创始人包凡,本是投行圈里最能搅动风云的人,撮合过滴滴与快的、美团与大众点评等世纪合并,投资过稳定币第一大上市公司 Circle。 可 2023 年 2 月,包凡突然「失踪」,于是,华兴在资本市场成了一个敏感名字。投行业务还在运转,却因为创始人「在押」,长期处在游离位置:传统金融不敢完全信任它,新兴互联网资本又觉得它气数已尽。 2025 年八月末,币安官宣了与华兴资本的合作。 而在华兴资本与 BNB 的合作落地之前,发生了微妙的巧合。2025 年 8 月 8 日,财新披露华兴创始人包凡「获释」,结束了长达两年半的失踪调查;仅仅三周后,华兴就宣布投资 1 亿美元 BNB,并携手赵长鹏家族基金 YZi Labs 启动合规基金。包凡的太太许彦清,同时也是华兴董事会现任主席,作为演讲嘉宾,参加了 BNB 生态五周年活动。 除此之外,赵长鹏与华兴还落地了一项看似不起眼的举措:将推动 BNB 在香港证监会持牌的虚拟资产交易平台上合规上市。 短短 12 天后,作为香港首家持牌交易平台的 OSL,便上线公告:币安平台币 BNB,成为香港第五个获批在持牌交易平台交易的加密资产。 作为香港最早拿到牌照的交易平台,OSL 背后站的是母公司 BC Technology,一家在港交所上市的持牌金融科技集团。OSL 本身拿下了香港首批虚拟资产交易平台牌照,同时兼具托管和经纪业务,拥有直接打通本地券商、ETF 托管人和机构分销的网络。 这家公司之所以在行业里特别,被认为与其早期管理层的「金融化背景」有关。它的第一大股东,占股 25.43%,本来就是传统券商出身,后来切入加密,同样也是交易平台 Bitget 创始人,把合规与资本市场结合得最彻底。 赵长鹏的归国,写在了这些巧合与运作之间,靠一块块资本和政治完成拼图。 获得特赦前的 180 天,赵长鹏这些看似松散的小动作,其实都是为同一个目标服务:首先重建 CZ 回中国的合法性。 02. 赵长鹏在香港大学活动时候说了这样一句话,「四年前离开大陆的时候,我以为再也回不到华语圈的核心舞台了。但今天站在香港,我清楚地知道之前的漂泊只是铺垫,真正的故事才刚刚开始。」 有些人觉得这只是场面话,可一旦知道背后的故事,就会发现,这可能是肺腑之言。 2017 年 7 月,币安在上海起步。两个月后,国内全面叫停 ICO 和交易平台,CZ 被迫带着三十多人的团队撤离。六周里,他们把数据从阿里云搬到 AWS,又给没出过国的工程师们办签证,像一支临时拼凑的远征队,来到了东京。 当时的日本,看上去是理想的避风港,政府已经承认虚拟货币合法。于是币安租了办公室,十来号人一坐,就是「全球总部」。 2017 年的牛市汹涌,比特币从 3000 美元冲到 1.9 万美元。币安在短短五个月内登顶全球交易量第一。那段时间,他们几乎不眠不休,注册量暴涨到一度关停开户。 可风向很快逆转。2018 年初,骗子用伪造的谷歌广告钓鱼,骗走了投资者的币安账号和资金。日本金融厅骤然收紧政策,3 月直接警告币安在日本的无证营业活动。监管的冷脸比黑客更可怕,CZ 再次收拾行李,撤离东京。 从东京撤退后,CZ 把赌注押在了地中海的马耳他。2018 年,总理穆斯卡特喊出「区块链岛」的口号。于是赵长鹏和当地政府合作落地,宣布币安全球总部就在这里,三个月之内,团队就扩展到来自 39 个国家的员工。可两年后,马耳他金融管理局一句冷冰冰的声明:币安从未注册。 这一来一回,日本冷眼,马耳他反悔,逼得 CZ 干脆宣布:币安将不再寻找总部。 2021 年 9 月,这种「无总部模式」开始在监管中发挥特殊作用。2021 年,有交易平台对手在美国起诉币安,一份集体诉讼将币安、CoinMarketCap 和赵长鹏一同告上法庭。 封神的地方来了:传票还能找到公司,但「无总部」找不到地址,最后只能去追创始人。于是原告律师雇了一名退役海军陆战队出身的私家侦探,追查 CZ 的下落。调查范围横跨亚洲、欧洲、中东,翻遍了航班数据、商业登记和社交媒体。几个月过去,还是一无所获。 直到最后侦探在报告里留下了一句话:「我们付出了极大的努力去追踪赵长鹏,但赵长鹏的行踪几乎无法被发现。」 甚至律师最后建议直接通过 Twitter 送传票,毕竟 CZ 每天都在上面发声。当然,这被法官拒绝了。 漂泊只是铺垫,总部可以消失,护照可以更换,只是很快,赵长鹏就要面对另一道更棘手的身份考题——当美国人把矛头指向他的华人血统时,赵长鹏能给出怎样的答案? 03. 在世界权力游戏的牌桌上,先亮的是出身,再摆的是护照,至于能力,往往只是最后的谈资。 从美国监狱走到美国总统特赦,这一路,赵长鹏对所谓「合规」的付出,从来都「不止是」合规。 2022 年底,美国第二大交易平台 FTX 崩盘倒闭,亏损带来巨额资金缺口。紧接着七个月后,美国 SEC 便开始起诉币安 CZ 非法经营,年底便开出 43 亿天价美元罚单。 法庭外,华盛顿的权力游戏从未停歇。民主党主导的监管风暴,将 CZ 塑造成完美靶子:一个华人企业家,掌控着加密世界的半壁江山,却涉嫌违反反洗钱法和制裁规定。检察官的指控书里,币安被指「服务于非法活动」,而 CZ 的背景,中国出生、早期在上海生活的华人身份,成了最廉价、最高效的攻击入口。 2023 年 11 月 24 日,海外社媒 Reddit 上的一则热帖冲上加密板块热搜,讨论币安是否真的能承担 43 亿美元罚款,并拿它与 FTX 的 68 亿美元窟窿相比较。不少美国网友甚至暗示:政府这是在向币安「榨血」,借此填补美国加密行业的亏空。 但钱能解决的,也只有账面问题,出身的质疑始终如影随形。 美国议员 Stacey Plaskett 在一次听证会上,直截了当地说:「虽然他是加拿大公民,但他是中国人 [3]。」 Congresswoman Stacey Plaskett,图源 Federal Newswire Report 在《福布斯》的文章中,赵长鹏说道:「我的华裔身份又被提起了,好像这很重要。」赵过去曾因华裔身份而成为歧视的目标,尤其是在一些人试图把他和亚洲的相关政府联系在一起时。 2024 年春天,赵长鹏交完 43 亿美元罚款,并穿上囚服进到美国西雅图监狱伏法。这段被他称为「人生最难熬的时刻」的牢狱生活,并未完全带来政治的身份重置。 真正的转折点,是共和党的特朗普重返白宫,给加密行业来了一场「大赦」。 赵长鹏交给民主党的 43 亿罚金,成了政治献祭的沉没成本。他不得不重新开始押注。 2025 年 3 月,币安宣布获得阿布扎比主权基金 MGX 20 亿美元投资。比例、治理权、资金用途未公开,但真正引人注目的是结算方式。不是美元现金,而是 USD1 稳定币——背后站着与特朗普家族关系密切的 World Liberty。 不久后,赵长鹏在社媒发布与扎克·维特科夫(Zach Witkoff)的合影。Zach 既是 USD1 的联合创始人,也是特朗普阵营的盟友。他的父亲史蒂夫·维特科夫(Steve Witkoff)正担任着特朗普政府担任中东事务特使。 这让一桩金融投资被赋予了更多政治意味,中东资本入场,特朗普家族的稳定币登台,CZ 则借此换来一层新的庇护。 短短两周后,特朗普家族的稳定币 USD1,宣布正式登陆币安生态 BNB Chain。 USD1 的口号很简单:「美国人的数字美元」。而 CZ 做的第一件事,就是把它接入自己的基本盘。BNB Chain,本来就是个热闹的菜市场,借贷、DEX、Meme,应有尽有。USD1 一上架,借贷池上线,跨链工具接入,特朗普家族基金甚至带飞币安嫡系 Four.meme 项目的 Meme 币。 事实上,USD1 稳定币的总发行量中,已有近九成都在 BNB 链上流通。 表面上,这是产品合作;实际上,所有人都羡慕这张挤破头都难获得的政治加持。即便如此,赵长鹏今年 4 月已正式向特朗普申请总统赦免,过去整整五个月,才获得官方文件「已批准特赦」。 鲍勃迪伦曾成名作《Blowin' in the wind》中唱到: How many roads must a man walkdown 一个人要走过多少路, Before you can call him a man 才能称为真正的男人。 对币安而言,这个问题同样坎坷:「要走过多少路,跨过多少关,才真正站上合规的舞台?」 04. 对赵长鹏而言,这是个人劫数;对华人企业家而言,却是群体性的难题。护照的国籍页可以换,但在政治叙事中,华人身份却成了博弈中一道无法消解的标签。 这种标签,带来的是结构性的脆弱。商业竞争是一套合法框架下的较量,但战争则不一样,对手几乎不会考虑任何的规则和限制,会不择手段地来完成自己的战略目的。 赵长鹏曾经说:「如果有听众,我或许愿意私下里担任少数几位新兴企业家的导师。就算不为别的,我至少可以告诉他们什么不该做。」 毕竟所以对于华人企业家而言,「合规」并不只是合规,它常常还意味着一种更高门槛的「身份赎身」。 表面上,这是商业竞争背后的制度摩擦,深层次上,却是身份政治在全球市场的投射。一个德国企业家、一个日本企业家、一个韩国企业家,即便面对监管,也很少被无限放大其「国籍背景」。但当主体是华人时,身份便天然带上了地缘政治的隐喻色彩,仿佛企业的每一次扩张,都暗含着国家的意志。 Shein 的 CEO 许仰天,拿到新加坡护照,却没能换来 Shein 的顺利上市;TikTok 换上了新加坡籍的 CEO 周受资,却挡不住国会对「华人身份」的持续质疑;Temu 把总部迁去爱尔兰,却迁不走华盛顿对其「强迫劳动」的指控。 正因如此,华人企业家的「护照」与「身份」之间,总存在一种割裂。护照可以一换再换:加拿大、新加坡、格林纳达……但「身份」却是一种更深层的烙印,写在脸上,刻在经历里,难以抹去。它让这些企业家在跨境扩张的道路上,总是要付出额外的代价,更多的解释、更多的审查,甚至更多的妥协。 有人说,这是全球化走到深水区的必然:资本可以自由流动,但人的身份却无法轻易跨越政治的藩篱。华人企业家的成功与困境,正是这一矛盾的集中体现。 他们一方面证明了华人群体的勤劳努力,另一方面,也不断被提醒,无论市场多大、资本多雄厚,他们始终处在一个需要额外证明「自己无害」的位置。 这或许是赵长鹏们共同的隐痛:他们可以改变公司结构,可以拥抱不同的市场,但他们必须学会在美国、在欧洲、在中东不同的权力格局里寻找庇护;必须接受护照可以成为工具,身份是另一种难以摆脱的宿命。 赵长鹏在获特赦后第一时间发推表示:感谢特朗普总统的特赦,将尽一切努力帮助美国成为加密货币之都。 或许对于华人企业家而言,这场关于「身份」的博弈,远未结束。 - END - 参考资料: [1].Rug Radio 的 Farokh Sarmad 和 CZ 的深度专访,谈狱中经历部分 [2]. 香港特区政府公报 [3].Plaskett uses 『anti-Asian discrimination』 against Canadian crypto CEO in congressional hearing, Federal Newswire Report
- TSSHOCK:针对门限签名方案(TSS)的新型密钥提取攻击
-
Thanos 具有长期存储功能的高可用性 Prometheus 设置
web: https://thanos.io/ github: https://github.com/thanos-io/thanos
-
Roxy-WI 负载平衡变得更容易
Roxy-WI 是为那些想要拥有容错基础设施,但又不想深入了解基于 HAProxy、NGINX、Apache 和 Keepalived 的集群的设置和创建细节的人创建的。 https://roxy-wi.org/ https://roxy-wi.org/installation#deb
- 欢迎大家加入百谷区块链论坛社区
-
DeFi 治理指南
本着去中心化的精神,DeFi(去中心化金融)协议一直在赋能其社区,通过链上治理来引领这些协议的未来发展。然而,DeFi 治理和代币委托的生态系统往往难以驾驭。在伯克利区块链(B@B),我们在过去一年中为四个协议( Uniswap、Compound、Aave、Fei )运行委托项目,并从中获益良多。本指南旨在将其中一些经验提炼成易于理解的格式,以帮助其他委托人和未来的社区成员快速上手。 DeFi治理概述 协议利用 DeFi 治理赋能社区成员,使其能够共同做出有关协议变更、启动新计划以及转变协议长期愿景的关键决策。在最常见的基于代币的治理模型中,1 个代币对应协议中的 1 票,这使得协议的每个代币持有者都能参与治理。通过利用治理,DeFi 协议团队能够将决策权分散到整个社区。 治理流程与 DeFi 协议本身一样,由智能合约控制。在大多数协议中,一旦治理提案及其协议变更获得批准,提案就会进入时间锁,冻结一段时间(例如两天),然后再进行整合。时间锁为社区提供了一个缓冲,以便在潜在的恶意或误导性活动被纳入协议之前进行抵消。 例如,Governor Bravo是由 Compound Labs 开发的一套流行的治理合约。Governor Bravo 合约中的任何提案都由一系列操作组成:调用合约函数、通过重新部署和设置新版本的代理地址来升级合约等。这些变更随后将进行投票,投票结果要么被取消,要么在执行前被发送到时间锁。 提案如何融入协议?(Open Zeppelin提供) 使用 Bravo 治理机制和类似的治理合约,协议指定的某些智能合约无需更改代理地址即可升级。然而,未部署在这些框架下的智能合约是不可变的,因此许多 DeFi 协议使用代理合约模式来连接客户端和实际的合约逻辑。用户和客户端应用程序使用代理合约的地址并与其函数进行交互。代理合约随后指向协议的合约逻辑。这使得客户端无需更改与之交互的地址即可升级协议参数并将合约重新部署到链上;客户端只需更改代理合约指向的协议逻辑版本即可。 代币委托 为什么授权很重要? 早期投资者和协议构建者通常持有大量治理代币,而这些代币的委托可以作为鼓励社区多元化参与的载体。例如,a16z 最近开源了他们的代币委托计划,该计划旨在通过将代币委托给在治理方面展现早期领导力的社区成员来提高协议治理机构的质量。 开源我们的代币委托计划 - a16z crypto 过去一年,我们与数十位代表就多项领先的礼仪方案展开了合作。以下是一些最佳实践…… a16z.com 投票权是通过治理代币智能合约中的实用函数进行委托的。这些功能使代币持有者能够将其链上治理权委托给他们认为是熟练的“协议治理者”。 如何成为代表? 在启动 DeFi 治理计划时,首先要积极参与论坛和社区活动,展现对协议的深度了解。在熟悉协议并了解您的组织能够带来的增值之后,为协议做出贡献并成为协议的早期社区领导者是获得首批委托的最佳途径。与风险投资公司和协议团队/创始人建立联系也是潜在的途径,但最有效的影响力方法仍然是积极推动协议发展并发出自己的声音。根据我们的经验,我们发现,通过成为社区中活跃的协议治理者,投资者和创始人会开始直接联系您并提出委托提案。然而,作为委托人,您有必要也有责任对协议进行尽职调查,并且只以委托人的身份参与合法的协议。保持透明、负责和独立是赢得社区信任的关键,您必须积极寻求所有途径,才能成为一名有影响力的社区成员。 与委托相关的法律合约可能有所不同,但通常包含一笔 Gas 津贴,用于支付链上投票的 Gas 费用,以及委托的代币数量和合约期限。标准做法是,合约中应包含一项条款,允许在您的组织在社区中不再活跃时撤销委托的代币。我们强烈建议签订一份法律合约,以确保各方在委托细节和期望方面达成一致。 DeFi 治理委托书示例(由 a16z 提供) crypto-governance/delegation/delegation_letter.md 在 main · a16z/crypto-governance 通过在 GitHub 上创建帐户来为 a16z/crypto-governance 开发做出贡献。 github.com 我们的 DeFi 治理方法 使命 我们非常重视积极参与整个治理流程,从论坛讨论、进度核查、提案讨论到中期投票和最终投票。我们不仅通过论坛与社区互动,还在推特上解释我们的投票决定,并与各协议的社区负责人会面讨论协议变更。作为活跃的治理者和社区成员,我们希望对 DeFi 治理产生持久影响,并助力塑造 DeFi 的未来。 委员会结构 尽管我们力求将尽可能多的信息纳入治理决策,但我们发现,精心构建的核心治理团队有助于确保我们始终掌握协议发展动态,并成为治理中知情的投票者和贡献者。核心团队确保我们组织在 DeFi 治理方面做出的决策是民主且合理的。 最初,我们从一个委员会开始,每个成员都会活跃于我们作为代表的每个协议的社区中,并作为一个小组聚集在一起讨论提案以达成共识。虽然这种模式在 1-2 个协议中作为代表时效果很好,但我们很快意识到,当我们成为新协议的代表时,它无法扩展。目前,我们指定核心成员担任特定协议和协议内 DAO 的负责人(例如 B@B 参与了Aave Risk DAO)。我们的核心治理团队定期与治理委员会开会,讨论治理提案的优点。在会议上,我们的协议负责人会向委员会传达协议的具体细节和细微差别,使我们能够作为一个团队做出更明智的投票。 案例研究:暂时禁用 COMP 奖励(Compound 提案 63) 为了更深入地了解我们的治理流程,我们将解释我们最近投票反对 COMP 提案 63 的评估流程,该提案旨在暂时禁用 COMP 奖励,以应对COMP 提案 62 中被忽视的错误导致的审计官漏洞。 Compound 提案 63:COMP 分发漏洞临时补丁(2021 年 9 月 29 日) 发起一个讨论此提案的帖子,稍后会从提案正文链接到该提案,并在下方开始讨论。…… www.comp.xyz 我们团队的首要目标是在协议负责人的带领下,了解该提案的背景、目标、缺点和实施情况。提案 63 需要快速响应,以应对漏洞利用带来的超过 1 亿美元的损失风险,因此提出了一个初步的解决方案:赋予社区暂停所有 COMP 申领的权力,以防止更多资金面临风险。然而,该提案也存在一个重大缺陷——所有涉及claimComp函数的协议集成(例如,将 Compound 作为投资策略一部分的协议)都将被阻止。两天后发布的提案 64 以更细致的解决方案解决了同样的问题:只有未与被漏洞利用的市场互动的用户才能提取他们的 COMP。经过进一步研究,我们认识到这个解决方案不仅更加优雅,而且能够更好地维护更广泛的 DeFi 社区对 Compound 协议的信任。因此,初步研究导致 Compound 协议负责人建议投票反对该提案。 在完成评估研究阶段后,Compound 协议负责人向委员会提交了他们的初步发现和初步研究,以供核心团队进行全面投票。鉴于投票的潜在影响以及找到漏洞解决方案的重要性,我们的团队投入了大量时间,分别研究了该提案的优缺点。之后,委员会召开投票会,多数成员决定反对第 63 号提案。 由于提案 63/64/65 相互关联,我们将这三项提案的论证都整合到一条推文中,并发布给社区。我们希望通过推文阐述这些论证,引导社区成员做出明智的治理投票决策。 未来步骤 作为 DeFi 治理领域的积极代表和贡献者,我们希望能够帮助更广泛的加密社区,并肩负起推动协议发展的重任。我们计划在未来的文章中发布我们对 B@B DeFi 治理影响的具体观点和目标。先简单介绍一下:随着 DeFi 逐渐成为主流,重新思考社区参与和治理渠道的激励机制是实现大规模采用的必要前提。一如既往,我们很高兴能够继续以透明公开的方式为社区做出贡献! 一般资源 随着 DeFi 的发展,治理工具生态系统也随之快速发展。我们发现,建立一个资源中心对于提升 DeFi 治理意识和效率至关重要。以下是我们认为对 DeFi 治理生态系统的代表和监督者最有帮助的资源汇总: 仪表板 Tally:投票仪表板、治理工具和维基百科,提供有关治理系统的信息 Sybil:通过协议跟踪代表的投票权和提案投票情况 Boardroom:DAO管理平台,供数百个社区成员参与治理 协议:以美元追踪投票代表的投票权
-
区块链相关术语(中英对照)
区块链相关术语(中英对照)说明:阅读英文文档是编程开发过程中最常做的一件事,英文阅读也是一个程序员的基本能力。区块链刚刚起步,每天各种新概念层出不穷,为方便大家学习和使用,这里收录了巴比特论坛上的一个帖子内容。该帖子仍在持续更新,更多新内容请点击下面的地址阅读原帖。 原文标题:《数字货币翻译术语(中英对照)》 原文地址:http://8btc.com/thread-17286-16-1.html 版权归巴比特论坛,也感谢社区小伙伴们的参与和贡献。 English 中文 account level(multiaccountstructure) 账户等级(多账户结构) accounts 账户 adding blocks to 增加区块至 addition operator 加法操作符 addr message 地址消息 Advanced Encryption Standard(AES) 高级加密标准(AES) aggregating 聚合 aggregating into blocks 聚集至区块 alert messages 警告信息 altchains 竞争币区块链 altcoins 竞争币 AML 反洗钱 anonymity focused 匿名的 antshares 小蚁 appcoins 应用币 API 应用程序接口 App Coins 应用币 architecture 架构 assembling 集合 attacks 攻击 attack vectors 攻击向量 Autonomous Decentralized Peer-to-Peer Telemetry 去中心化的 p2p 自动遥测系统 auxiliary blockchain 辅链 authentication path 认证路径 B backing up 备份 balanced trees 平衡树 balances 余额 bandwidth 带宽 Base58 Check encoding Base58Check编码 Base58 encoding Base58编码 Base-64 representation Base-64表示 BFT(Byzantine Fault Tolerance) 拜占庭容错 binary hash tree 二叉哈希树 BIP0038 encryption BIP0038加密标准 bitcoin addressesvs. 比特币地址与 bitcoin core engine 比特币核心引擎或网络 bitcoin ledger 比特币账目 bitcoin network 比特币网络 Bitcoin Network Deficit 比特币网络赤字 Bitcoin Miners 比特币矿工 Bitcoin mixing services 混币服务 Bitcoin source code 比特币源码 BitLicense 数字货币许可 BIP152 比特币改进提议 Bitmain 比特大陆 Bitmessage 比特信 BITNET 币联网 Bitshares 比特股 BitTorrent 文件分享 Blake algorithm Blake算法 block chain apps 区块链应用 block generation rate 出块速度 block hash 区块散列值 block header hash 区块头散列值 block headers 区块头 block height 区块高度 blockmeta 区块元 block templates 区块模板 blockchains 区块链 bloom filtersand 布鲁姆过滤器(bloom过滤器) BOINC open grid computing BOINC开放式网格计算 brain wallet 脑钱包 broad casting to network 全网广播 broad casting transactions to 广播交易到 bytes 字节 Byzantine fault-tolerant 拜占庭容错 C call 调用 CCVM(Cross Chain Virtual Machine) 跨链交易的虚拟机 centralized control 中心化控制 chaining transactions 交易链条 chainwork 区块链上工作量总值 Check Block function(Bitcoin Coreclient) 区块检查功能(BitcoinCore客户端) CHECKMULTISIG implementation CHECKMULTISIG实现 CheckSequenceVerify (CSV) 检查序列验证/CSV checksum 校验和 child key derivation(CKD) function 子密钥导出(CKD)函数 child private keys 子私钥 Child Pays For Parent,CPFP 父子支付方案 coinbase reward calculating coinbase奖励计算 coinbase rewards coinbase奖励 coinbase transaction coinbase交易 cold-storage wallets 冷钱包 Compact block 致密区块 Compact block relay 致密区块中继 colored coins 彩色币 compressed keys 压缩钥 compressed private keys 压缩格式私钥 compressed public keys 压缩格式公钥 computing power 算力 connections 连接 consensus 共识 Consensus Ledger 共识账本 consensus attacks 一致性功能攻击 consensus innovation 一致性的创新 consensus plugin 共识算法 Confidential Transactions 保密交易 constant 常数 constructing 建造 constructing block headers with 通过...构造区块头部 converting compressed keys to 将压缩地址转换为 converting to bitcoin addresses 转换为比特币地址 conversion fee 兑换费用 consortium blockchains 共同体区块链 counterparty protocol 合约方协议 Counterparty 合约币 creating full blockchains on 建立全节点于 creating on nodes 在节点上新建 crypto community 加密社区 crypto 2.0 ecosystem 加密2.0生态系统 cryptocurrency 加密货币 Cunning hamprime chains 坎宁安素数链 currency creation 货币创造 D Darkcoin 暗黑币(译者注:现已更名为达世币Dash) data structure 数据结构 DAO(Decentralized Autonomous Organization) 去中心化自治组织 Debt Token 债权代币 decentralized 去中心化 decentralized consensus 去中心化共识 decentralised applications 去中心化应用 decentralised platform 去中心化平台 decoding Base58Check to/from hex Base58Check编码与16进制的相互转换 decoding to hex 解码为16进制 deep web 深网 Decode Raw Transaction 解码原始交易 deflationary money 通缩货币 delegated proof of stake 授权股权证明机制 demurrage currency 滞期费 denial of service attack 拒绝服务攻击 detached block 分离块 deterministic wallets 确定性钱包 DEX :distributed exchange 去中心化交易所 difficulty bits 难度位 difficulty retargeting 难度调整 difficulty targets 难度目标 digital notary services 数字公正服务 digital currency 数字货币 distributed hash table 分布式哈希表 Distributed Autonomous Corporations Runtime System(DACRS) 自治系统运行环境 Distributed Ledger Technology(DLT) 分布式账簿技术 domain name service(DNS) 域名服务(DNS) double-spend attack 双重支付攻击 double spend 双花 Dogecoin 狗狗币 DoS(denial of service) attack 拒绝服务攻击 DPOS 权益代表证明机制/DPOS算法(POS基础上的改良) dual-purpose 双重目标 dual-purpose mining 双重目的挖矿 dust rule 尘额规则(极其小的余额) E eavesdroppers 窃听者 ecommerce servers keys for… 电子商务服务器...的密钥 ECDSA 椭圆曲线数字签名算法保障 Eigentrust++ for nodes 用于节点的Eigentrust++技术 electricity cost 电力成本 electricity cost and target difficulty 电力消耗与目标难度 Electrum wallet Electrum 钱包 ellipticcurve multiplication 椭圆曲线乘法 Emercoin(EMC) 崛起币 encoding/decoding from Base58Check 依据Base58Check编码/解码 encrypted 加密 encrypted private keys 加密私钥 Equity Token 权益代币 Ethereum 以太坊 External owned account(EOA) 外有账户 ether 以太币 extended key 扩展密钥 extra nonce solutions 添加额外nonce的方式 extraBalance 附加余额 F Factom 公证通 fault tolerance 外加容错 Feathercoin 羽毛币 fees 手续费 FRN 快速中继网络 FBRP 快速区块中继协议 FEC 向前纠错 field programma blegatearray(FPGA) 现场可编程门阵列(FPGA) Financial disintermediation 金融脱媒 fintech 金融技术 fork attack 分叉攻击 forks 分叉 fraud proofs 欺诈证明 full nodes 完整节点;全节点 G generating 生成 generation transaction 区块创始交易 generator point 生成点 genesis block 创始区块 GetBlock Template(GBT)mining protocol GetBlockTemplate(GBT)挖矿协议 gettingon SPV nodes 获取SPV节点 GetWork(GWK) mining protocol GetWork(GWK)挖矿协议 graphical processing units(GPUs) 图形处理单元(GPUs) GUID 全域唯一识别元 H hackers 黑客 halving 减半 hardware wallets 硬件钱包 hard fork 硬分叉 hard limit 硬限制 hash 哈希值 Hardware Security Modules(HSM) 硬件安全模块 hashing powerand 哈希算力 hashcash 现金算法 HD wallet system 分层确定性钱包系统 header hash 头部散列值 heavyweight wallet 重量级钱包 Hierarchy deterministic 分层确定的 honesty 诚信算力 hyperledger 超级账本 human readable format 人类可读模式 I identifiers 标识符 immutability of blockchai 区块链不可更改性 implementing in Python 由Python实现 in block header 在区块的头部 independent verificatio 独立验证 innovation 创新 inputs 输入 Internet of Things 物联网 instamine 偷挖 Invertible Bloom Lookup Table(IBLT) 可逆式布鲁姆查找表 Invalid Numerical Value 无效数值 IPDB 星际数据库 K key formats 密钥格式 key-value 键值 KYC 了解你的客户 L Level DB database(Google) LevelDB数据库(Google) light weight 轻量级 linking blocks to… 将区块连接至… linking to blockchain 连接至区块链 Lightning network 闪电网络 linear scale 线性尺度 Litecoin 莱特币 lock time 锁定时间 locking scripts 锁定脚本 log scale 对数单位 M mainnet 主网 managed pools 托管池 mastercoin protocol 万事达币协议 masternode 主节点 memorypool(mempool) 内存池 Merkle tree(Merkle Hash tree) 二进制的哈希树或者二叉哈希树 Merkle root 二进制哈希树根 metachains 附生块链 mining 挖矿 mining blocks successfully 成功产(挖)出区块 mining pools 矿池 mining rigs 矿机 micropayment 小额支付 microblocks 微区块 modifying private key formats 修改密钥格式 monetary parameter alternatives 货币参数替代物 Moore’s Law 摩尔定律 Moonpledge 月球之誓 MPC 多方计算 multi account structure 多重账户结构 multi-hop network 多跳网络 multi-signature 多重签名 multi-signature addresse 多重签名地址 multi-signature addresses 多重签名地址 multi-signature scripts 多重签名脚本 multi-signatureaccount 多重签名账户 N Namecoin 域名币 native token 原生代币 navigating 导航 Network Propagation 网络传播算法 Network of marketplaces 市场网络 Nextcoin(NXT) 未来币 Neoscrypt N算法 nested subchains 嵌套子链 NFC(Near Field Communication) 非接触式 NIST5 NIST5是一种新算法,由TalkCoin首创 nodes 节点 nonce 随机数 noncurrency 非货币 nondeterministic wallets 非确定性的 O off-chain 链下 on full nodes 在全节点上 on new nodes 在新节点上 on SPV nodes 在SPV节点 on the bitcoin network 在比特币网络中 one-hop network 单跳网络 OP_RETURN operator OP_RETURN操作符 OpenSSL cryptographiclibrary OpenSSL密码库 open source of bitcoin 比特币的开源性 open transaction(OT) 开放交易 orphan block 孤儿块 Oracles 价值中介 OWAS 单向聚合签名 OTC(over the counter) 场外交易 outputs 输出 P P2P Pool P2Pool(一种点对点方式的矿池) parent blocks 父区块 parent blockchain 主链 paths for 路径 Pay to script hash (P2SH) P2SH代码;脚本哈希支付方式 payment channel 支付通道 P2SH address P2SH地址;脚本哈希支付地址 peer-to-peer networks P2P网络 physical bitcoin storage 比特币物理存储 PIN-verification 芯片密码 plot/chunks of data 完整数据块 pool operator of mining pools 矿池运营方 post-trade 交易后 post-trade processing 交易后处理 POI: proof of importance 重要性证明( NEM提出来的一种共识算法) Ppcoin 点点币 Premine 预挖 priority of transactions 交易优先级 Primecoin 素数币 proof of stake 权益证明 proof of work 工作量证明 proof-of-work algorithm 工作量证明算法 proof-of-work chain 工作量证明链 propagating transactions on 交易广播 protein folding algorithms 蛋白质折叠算法 public child key derivation 公钥子钥派生 public key derivation 公钥推导 publickeys 公钥 public blockchain/permissionless blockchain 公链 private blockchain/permissioned blockchain 私链 pump and bump 拉升出货 purpose level(multiaccount structure) 目标层(多帐户结构) Python ECDSA library PythonECDSA库 R random 随机 random wallets 随机钱包 raw value 原始价格 reentrancy 可重入性 regtech 监管技术 replay attacks 重放攻击 RBF:Replace By Fee 费用替代方案 retargeting 切换目标 recursive call 递归调用 RIPEMD160 RIPEMD160一种算法 Ripple 瑞波币 risk balancing 适度安保 risk diversifying 分散风险 root of trust 可信根 root seeds 根种子 S sandbox 沙箱 satoshis 中本聪 scoops/4096 portions 子数据块 scriptcons truction 脚本构建 scriptl anguage for 脚本语言 Scriptlanguage 脚本语言 scripts 脚本 scrypt algorithm scrypt算法 scrypt-N algorithm scrypt-N算法 Secure Hash Algorithm(SHA) 安全散列算法 security 安全 security thresholds 安全阈值 seed nodes 种子节点 seeded 种子 seeded wallets 种子钱包 selecting 选择 soft limit 软限制 Segregated Witness(SegWit) 隔离见证 SHA256 SHA256 SHA3 algorithm SHA3算法 Shared Permission Blockchain 共享认证型区块链 shibes 狗狗币粉丝 shopping carts public keys 购物车公钥 simplified payment verification (SPV) nodes 简易支付验证(SPV)节点 simplified payment verification(SPV)wallet 轻钱包 sidechain 侧链 signature operations(sigops) 处理签名操作 signature aggregation 签名集合 Skein algorithm Skein算法 smart pool 机枪池 smart contracts 智能合约 solo mining 单机挖矿 solo miners 独立矿工 soft fork 软分叉 spilt 分割 Stellar 恒星币 stateless verification of transactions 交易状态验证 statelessness 无状态 state machine replication 状态机原理 storage 存储 Stratum(STM)mining protocol Stratum挖矿协议 structure of 的结构 sx tools sx工具 syncing the blockchain 同步区块链 system security 系统安全 Subchains 子链 T taking off blockchain 从区块链中删除 tainted address 被污染的地址 taint Analysis 污点分析 TeleHash p2p信息发送系统 timeline 时间轴 timestamping blocks 带时间戳的区块 txids 缩短交易标识符 token 代币 token system 代币系统 token-less blockchain 无代币区块链=私链 transaction fees 交易费;矿工费 transaction pools 交易池 transaction processing 交易处理 transaction validation 交易验证 transactions independent verification 独立验证交易 transaction malleability 交易延展性 tree structure 树结构 Trezor wallet Trezor钱包 Turing Complete 图灵完备 two-factor authentication 双因素认证 tx messages tx消息 Type-0 nondeterministic wallet 原始随机钱包 U uncompressed keys 解密钥 unconfirmed transactions 未确认交易 unspent outputs 未花费输出 user security 用户安全性 User Token 用户代币 UTXO pool UTXO池 UTXO set UTXO集合 UTXOs 未交易输出 V validating new blocks 验证新区块 validation 验证条件 validation(transaction) 校验(交易) vanity 靓号 vanity addresses 靓号地址 vanity-miners 靓号挖掘程序 verification 验证 verification criteria 验证条件 version message 版本信息 Visualise Transaction 可视化交易 W Wallet Import Format(WIF) 钱包导入格 wallets 钱包 white hat attack 白帽攻击 weak blocks 弱区块 whitelist 白名单 wildcard 通配符 X Xthin 极瘦区块 XRP (Ripple) 瑞波币 Z zero knowledge proof 零知识证明 zero codehash 零代码哈希 Zerocoin protocol 零币协议
-
区块链基础知识与关键技术
前言 #最近对在上 HKU 的<COMP7408 Distributed Ledger and Blockchain Technology>课程,对区块链的基础概念有了更系统的认知,结合之前上过的北京大学肖臻老师《区块链技术与应用》公开课,深知区块链知识体系之庞大,打算更新系列文章对区块链、比特币、以太坊等进行系统的知识梳理,如有错漏,欢迎交流指正。 区块链中的密码学原理 #区块链和密码学紧密相关,如比特币采用的核心的公私钥加密技术、数字签名、哈希等,包括很多共识算法也是基于复杂的密码学概念,因此,在开始学习区块链之前,要先了解几个核心的密码学概念,从而能够更深入理解其在区块链体系中的应用。 哈希函数 #哈希函数是把一个任意长度的源数据经过一系列算法变成一个固定长度输出值的方法,概念很简单,但其具备的几个特性使它被各个领域广泛应用。 可以访问这个 Demo 体验一下哈希函数的工作原理(以SHA256为例)! 第一个特性是单向不可逆性。将一个输入 x 进行哈希运算得到值 H(x),这一过程很容易,但是如果给定一个值 H(x),几乎不可能逆推得到 x 的取值,这一特性很好地保护了源数据。 第二个特性是抗碰撞性。给定一个值 x 和另一个值 y,如果 x 不等于 y,那 H(x) 几乎不可能等于 H(y),并非完全不可能,但是几率非常低,因此,一个数据的 Hash 值几乎是唯一的,这可以很好地用于身份验证等场景。 第三个特性是哈希计算不可预测。很难根据现有条件推导出哈希值,但是很容易检验是否正确,这一机制主要应用于PoW挖矿机制中。 加密/解密 #加密机制主要分为对称加密和非对称加密两类。 对称加密机制是两方用同一个密钥来进行信息的加密和解密,很方便,效率也很高,但是密钥的分发存在很大的风险,如果通过网络等方式进行分发,很容易会出现密钥泄漏,从而导致信息泄漏。 非对称加密机制主要指的是公私钥加密机制,每个人通过算法生成一对密钥,称为公钥和私钥,如果 A 想发送一个信息给 B,可以用 B 的公钥对文件进行加密,将加密后的信息发给 B,这个过程中,即使信息被截获或出现泄漏,也不会暴露源文件,所以可以用任何方式进行传播,当 B 收到加密文件后,用自己的私钥进行解密,从而获取文件内容。B 的私钥没有经过任何渠道进行传播,仅自己知道,所以具备极高的安全性。 在现实应用中,对很大的文件进行非对称加密效率较低,所以一般采用一种组合机制:假设 A 想发送一个大文件 D 给 B,则先将文件 D 用一个密钥 K 进行对称加密,再用 B 的公钥对密钥 K 进行非对称加密。A 将加密后的密钥 K 和文件 D 发送给 B,期间即使被截获或泄漏,因为没有 B 的私钥,所以无法得到密钥 K,也就无法访问文件 D。B 收到加密后的文件和密钥后,则先用自己的私钥解密得到密钥 K,再用密钥 K 对文件 D 进行解密,从而获取文件内容。 数字签名 #数字签名是非对称加密机制的另一种用法,上文讲到每个人拥有一对生成的公钥和私钥,在加密/解密应用中,是用公钥进行加密,用私钥进行解密,而数字签名机制刚好相反,假设一个文件持有者用自己的私钥对文件进行加密,其他人可以用他的公钥进行解密,如果得到结果则可以证明文件的归属权。 数字签名机制最典型的应用就是比特币区块链网络中,用私钥证明自己对比特币的归属权,对交易进行签名,其他人则可以用公钥来验证交易是否合法,整个过程无需暴露自己的私钥,保障了资产的安全。 区块链基本概念 #随着历史的发展,人们的记账方式从单式记账,发展到复式记账、数字记账,最后到分布式记账,因为传统的中心化数字记账则往往依赖于某个或某些组织的可信度,存在一些信任风险,而区块链技术本质上就是一种分布式账本技术,一群人共同维护着一个去中心化的数据库,通过共识机制来共同记账。区块链很容易追溯历史记录,而因为去中心化信任机制的存在,也几乎不可篡改(或者是篡改的成本远远大于收益)。 相比于传统的数据库,区块链只有增加和查询两种操作,所有的操作历史记录都会准确地保存在账本中且不可变,具备很高的透明度和安全性,当然,代价就是所有节点必须通过一些机制达成共识(因此效率较低,不适合实时性的操作),而且因为每个节点都要永久保存历史记录,会占据很大的存储空间。 应用场景 # 是否需要数据库? 是否需要共享写入 是否需要多方达成信任? 是否能够脱离第三方机构运作? 是否能够脱离权限机制运作? 区块链作为一个分布式数据库,主要做的还是信息存储的工作,只是通过其各类机制,在不需要第三方机构介入的前提下让有共同需求但并不互相信任的实体之间也能以相对较低的代价达成一致,从而满足需求,除此之外,系统还有加密认证、高透明度等特性,能够满足一些业务需求。而如果所涉及到的数据不能公开/数据量非常大/需要外部服务来存储数据,或者是业务规则经常发生变化,那区块链就并不适合作为其解决方案。 需要建立一个共享的数据库,且有多方参与 参与业务的各方没有建立信任 现有业务信任一个或者多个信任机构 现有业务有加密认证的业务需求 数据需要集成到不同的数据库且业务数字化和一致性的需求迫切 对于系统参与者有统一的规则 多方决策是透明的 需要客观的、不可改变的记录 非实时性处理业务 但其实在很多应用场景里,企业需要在去中心化和效率之间做一些权衡,且有时候很多复杂的业务对透明度、规则都有不同的需求,因此,基于复杂的商业化需求,也有“联盟链”这样的解决方案,能够更好地与现有的系统结合,以满足业务需求。 区块链类型 #区块链也有不同的类型,主要有私有链、公有链、联盟链三种。 私有链主要是应用于某一个特定领域或者只是在某一个企业运行的区块链,主要是用于解决信任问题,如跨部门协作等场景,一般不需要外部机构来访问数据。 公有链则是公开的交易,往往用于一些需要交易/数据公开的业务,如认证、溯源、金融等场景,比如比特币、以太坊和EOS等。 联盟链最大的特征是节点需要验证权限才能参与到区块链网络中,而认证一般都是与其现实角色所关联的,因此,联盟链也具有中心化的属性,但效率、拓展性和交易隐私则大大提升了,满足了企业级应用的需求,其中最广泛使用的就是Hyperledger Fabric了。值得一提的是,联盟链往往不需要代币来作为激励,而是将参与的各个节点作为记账节点,通过区块链机制实现跨部门之间的业务协同所带来的经济效益作为内部激励,是一种更健康、更符合企业应用的方式。 长期来看的话,公有链和联盟链在技术上也会逐渐趋于融合,即使是同一个业务,可以将需要信任的数据放在共有链上,而一些行业数据和私有的数据则可以放在联盟链上,通过权限管理来保障交易隐私。 区块链基本框架 # 区块 区块链 P2P 网络 共识机制 … 区块 #区块链就是由一个个区块组成的生态系统,每一个区块中包含了前一个区块链的哈希值、时间戳、Merkle Root、Nonce以及区块数据几个部分,比特币的区块大小为 1 MB。可以访问这个 Demo 来体验一下一个区块的生成过程。 因为每个区块都包含前一个区块的哈希值,根据前文所述的哈希性质,哪怕是极其微小的改变哈希值也会截然不同,因此很容易检测某个区块是否被篡改;Nonce 值则主要是用于调整挖矿难度,可以把时间控制在 10 分钟左右,以保障安全性。 区块链 #所有的区块串联起来就形成了区块链,是一个存储着网络中所有交易历史记录的账本,因为每一个区块都包含着上一个区块的哈希信息(比如比特币系统是将上一个区块的块头取两次哈希),因此如果有交易发生变化则会造成区块链断裂,有一个小 Demo 很好地演示了这一过程,大家可以体验一下! P2P 网络 #P2P 网络是用于不同用户之间共享信息和资源的一种分布式网络,是一种分布式网络,网络中的每个人都能够得到一份信息备份,而且都有访问权限;而中心化网络是所有人都连接至一个(或一组)中心化网络;去中心化网络是有多个这样的中心网络,但没有一个单点网络可以拥有所有的信息。下图很好地解释了它们之间的区别: 共识机制 #区块链网络是由多个网络节点组成的,其中每个节点都存有一份信息备份,那它们是如何对交易达成一致的呢?也就是说,它们作为独立的节点,需要有一种机制来保障互相信任,这就是共识机制。 常用的共识机制有PoW(Proof of Work)工作量证明,PoS(Proof of Stake)权益证明,DPoS(Delegated Proof of Stake委任权益证明,DBFT(Delegated Byzantine Fault Tolerance)等。 比特币/以太坊主要采用的是工作量证明机制,通过算力比拼来增加恶意节点的作恶成本。通过动态调整挖矿的难度来让一笔交易时间控制在 10 分钟左右(6 个确认),但随着比特币挖矿越来越火热,消耗资源越来越多,对环境造成破坏;有些矿池拥有大量资源,也会造成一些中心化的风险。 权益证明机制则是通过权益(一般是代币)持有者进行投票来达成共识。这种机制不需要像工作量证明一样进行大量的算力比拼,但是也有一些风险,称为Nothing at Stake问题,很多权益持有者会在所有区块都投注并从中获利。为了解决这个问题,系统设置了一些规则,如对同时在多个链创建区块的用户/在错误链上创建区块的用户设置一些惩罚机制。目前以太坊正在向这种共识机制转变。 EOS则采用了委任权益证明,选出一些代表性的节点来进行投票,这种方式目的是优化社区投票的效率和结果,但带来了一些中心化的风险。 DBFT共识机制则是通过对节点分配不同的角色来达成共识,这样可以很大程度降低开销和避免分叉,但是也有核心角色作恶的风险。 区块链安全与隐私 #安全 #区块链作为一个较新的技术,也存在很多安全隐患,如对数字货币交易所的攻击、智能合约漏洞、对共识协议的攻击、对网络流量(互联网 ISP)的攻击以及上传恶意数据等。比较著名的案例有 Mt.Gox 事件、以太坊 DAO 事件等,因此,对区块链的安全风险也是区块链的重要研究方向。 可以从协议、加密方案、应用、程序开发和系统等角度进行风险分析,提高区块链应用的安全性。例如在以太坊区块链中,可以对Solidity编程语言、EVM和区块链本身进行一些分析。 如智能合约中的一种叫低成本攻击的方式,就是通过识别以太坊网络中较低Gas费用的操作,重复执行以破坏整个网络。 对于安全问题,构建一个通用的代码检测器来检查恶意代码将会是一个更通用的解决方案。 隐私 #在讲区块链概念的时候,提到了它很重要的一个特征,隐私性。也就是说,所有人都能看到链上的交易细节和历史记录,这一特性主要应用在食品、药物等供应链环节,但是对于一些金融场景,如个人账户余额、交易信息,则容易造成一些隐私风险。 硬件层面,可以采用可信的执行环境,采用一些安全硬件,如Intel SGX,很大程度保障了隐私;网络可以采用多路径转发以避免从节点的 ip 地址推算出真实身份。 在技术层面,混币技术可以把很多交易进行一些混合,这样不容易找出对应的交易发送方和接收方;盲签技术可以保障第三方机构不能将参与交易的双方联系起来;环签用于保障交易签名的匿名性;零知识证明则可以应用于一方(证明者)向另一方(验证者)证明一个陈述是正确的,而无需透露除该陈述是正确的以外的人和信息;同态加密可以保护原数据,给定 E(x)和 E(y),可以很容易计算出某些关于 x, y 的加密函数值(同态运算);基于属性的加密(Attribute-based Encryption, ABE)则为各个节点添加一些属性/角色,实现权限控制,从而保护隐私。 值得注意的是,即使一笔交易生成多个 inputs 和 outputs,这些 inputs 和 outputs 的地址也可能被人关联;除此之外,地址账户和现实世界中的真实身份也可能产生关联。 总结 #以上就是对区块链基础知识的一些梳理,主要从概念和原理层面进行了一些学习,后续还会更新对比特币、以太坊、Hyperledger Fabric等典型应用的分析与思考,并对 IPFS、跨链、NFT 等热门技术进行一些探究,敬请期待! 参考资料 #
- 1111111111111111111111
-
推送RPC
推送RPC推送RPC,其实是RPC节点允许连接的一个WebSocket长连接。通过在该长连接上发送订阅请求, RPC节点会将相关事件在长连接上推送过来。当前订阅主要分为: accountSubscribe : 订阅Account的变化,比如lamports logsSubscribe : 订阅交易的日志 programSubscribe : 订阅合约Account的变化 signatureSubscribe : 订阅签名状态变化 slotSubscribe : 订阅slot的变化 每个事件,还有对应的Unsubscribe动作,取消订阅。将上面的Subscribe替换成Unsubscribe即可。 这里我们通过wscat命令行工具来模拟wss客户端。首先安装工具: Copynpm install -g ws wscat然后建立连接: Copywscat -c wss://api.devnet.solana.com下面举例说明: 订阅Account变化这里的Account就是每个地址的Account元数据。主要变化的就是data部分和lamports部分。 比如我们要订阅我们的账号余额的变化。 Copy{ "jsonrpc": "2.0", "id": 1, "method": "accountSubscribe", "params": [ "CnjrCefFBHmWnKcwH5T8DFUQuVEmUJwfBL3Goqj6YhKw", { "encoding": "jsonParsed", "commitment": "finalized" } ] }这里订阅对账号的变化的事件,我们通过wscat来模拟: Copywscat -c wss://api.devnet.solana.com Connected (press CTRL+C to quit) > {"jsonrpc":"2.0","id":1,"method":"accountSubscribe","params":["EZhhUANUMKsRhRMArczio1kLc9axefTUAh5xofGX35AK",{"encoding":"jsonParsed","commitment":"finalized"}]} < {"jsonrpc":"2.0","result":3283925,"id":1}然后我们在另外一个终端里面进行转账: Copysolana transfer --allow-unfunded-recipient CZmVK1DymrSVWHiQCGXx6VG5zgHVrh5J1P514jHKRDxA 0.01接着我们注意观察上面的wscat: CopyConnected (press CTRL+C to quit) > {"jsonrpc":"2.0","id":1,"method":"accountSubscribe","params":["CnjrCefFBHmWnKcwH5T8DFUQuVEmUJwfBL3Goqj6YhKw",{"encoding":"jsonParsed","commitment":"finalized"}]} < {"jsonrpc":"2.0","result":3283925,"id":1} < {"jsonrpc":"2.0","method":"accountNotification","params":{"result":{"context":{"slot":209127027},"value":{"lamports":989995000,"data":["","base64"],"owner":"11111111111111111111111111111111","executable":false,"rentEpoch":0,"space":0}},"subscription":3283925}}会发现,一段时间后,也就是到达了 “finalized”状态后,就会将修改过后的Account信息推送过来: Copy{ "lamports": 989995000, "data": [ "", "base64" ], "owner": "11111111111111111111111111111111", "executable": false, "rentEpoch": 0, "space": 0 }可以看到这里余额发生了变化 订阅日志订阅日志可能是做应用最常见到的,任何在log里面打印了相关事件的交易都会被通知 Copy{ "jsonrpc": "2.0", "id": 1, "method": "logsSubscribe", "params": [ { "mentions": [ "CdJp6W7S8muM85UXq7u2P42ryytDacqEo8JgoHENSiUi" ] }, { "commitment": "finalized" } ] }这里mentions来指定,通知了哪个程序或者账号的地址。 比如这里我们订阅我们的一个ATA的账号: Copywscat -c wss://api.devnet.solana.com Connected (press CTRL+C to quit) > {"jsonrpc":"2.0","id":1,"method":"logsSubscribe","params":[{"mentions":["CdJp6W7S8muM85UXq7u2P42ryytDacqEo8JgoHENSiUi"]},{"commitment":"finalized"}]} < {"jsonrpc":"2.0","result":610540,"id":1}然后我们给这个地址做mint增加他的余额: Copyspl-token mint 7dyTPp6Jd1nWWyz3y7CXqdSG86yFpVF7u45ARKnqDhRF 1000000000 Minting 1000000000 tokens Token: 7dyTPp6Jd1nWWyz3y7CXqdSG86yFpVF7u45ARKnqDhRF Recipient: CdJp6W7S8muM85UXq7u2P42ryytDacqEo8JgoHENSiUi Signature: 5NVHNccPo4ADxnHZjVSYZzxk3fuZfZvuLP6MwkhSNBbQRNcGfC2gwScz24XYictZuqaMKFEcmsXuHV4WZDiFUD3r可以在事件通知中看到: Copywscat -c wss://api.devnet.solana.com Connected (press CTRL+C to quit) > {"jsonrpc":"2.0","id":1,"method":"logsSubscribe","params":[{"mentions":["CdJp6W7S8muM85UXq7u2P42ryytDacqEo8JgoHENSiUi"]},{"commitment":"finalized"}]} < {"jsonrpc":"2.0","result":610540,"id":1} < {"jsonrpc":"2.0","method":"logsNotification","params":{"result":{"context":{"slot":209131722},"value":{"signature":"5NVHNccPo4ADxnHZjVSYZzxk3fuZfZvuLP6MwkhSNBbQRNcGfC2gwScz24XYictZuqaMKFEcmsXuHV4WZDiFUD3r","err":null,"logs":["Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [1]","Program log: Instruction: MintToChecked","Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 4498 of 200000 compute units","Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success"]}},"subscription":610540}}这里有个"MintToChecked"指令。 订阅合约所属于Account事件比如我们希望知道所有Token合约管理的账号的余额变化是,我们可以通过订阅合约管理的账号事件来发现: Copy{ "jsonrpc": "2.0", "id": 1, "method": "programSubscribe", "params": [ "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", { "encoding": "jsonParsed" } ] }对应的命令 Copywscat -c wss://api.devnet.solana.com Connected (press CTRL+C to quit) > {"jsonrpc":"2.0","id":1,"method":"programSubscribe","params":["TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",{"encoding":"jsonParsed"}]} < {"jsonrpc":"2.0","result":142408,"id":1} < {"jsonrpc":"2.0","method":"programNotification","params":{"result":{"context":{"slot":209131042},"value":{"pubkey":"GGUY45VyYy9j7vFdHRP3ecyMYhFCfrCBpVQaUxoEtHfv","account":{"lamports":2039280,"data":{"program":"spl-token","parsed":{"info":{"isNative":false,"mint":"GCBnu9k28isstJjCcYoZZcyTkMh5cXTsk7abpgWJesQT","owner":"AV1JYHgShqNdbza84sLi7Hgbtfgd1hn9mNMgez4twBuG","state":"initialized","tokenAmount":{"amount":"0","decimals":9,"uiAmount":0.0,"uiAmountString":"0"}},"type":"account"},"space":165},"owner":"TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA","executable":false,"rentEpoch":0,"space":165}}},"subscription":142408}} < {"jsonrpc":"2.0","method":"programNotification","params":{"result":{"context":{"slot":209131042},"value":{"pubkey":"GGUXUncym8riA1izYZnBWspYL1k4rVBnLuZ3KbUnc6WG","account":{"lamports":2039280,"data":{"program":"spl-token","parsed":{"info":{"isNative":false,"mint":"GCBnu9k28isstJjCcYoZZcyTkMh5cXTsk7abpgWJesQT","owner":"2E7BD9ibbHinwohM4pLFsjdFYq1S2o4wqKmfaQXXg8Dr","state":"initialized","tokenAmount":{"amount":"0","decimals":9,"uiAmount":0.0,"uiAmountString":"0"}},"type":"account"},"space":165},"owner":"TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA","executable":false,"rentEpoch":0,"space":165}}},"subscription":142408}} < {"jsonrpc":"2.0","method":"programNotification","params":{"result":{"context":{"slot":209131042},"value":{"pubkey":"GGUZyCzhCKEFZMdf8mDfUU4L1tr4q2xh3FHRWpRM8cPB","account":{"lamports":2039280,"data":{"program":"spl-token","parsed":{"info":{"isNative":false,"mint":"GCBnu9k28isstJjCcYoZZcyTkMh5cXTsk7abpgWJesQT","owner":"7PydWu5QtMcbdj7qgdgn42Rwp247GFf3e2pQ5fQ8LRGY","state":"initialized","tokenAmount":{"amount":"0","decimals":9,"uiAmount":0.0,"uiAmountString":"0"}},"type":"account"},"space":165},"owner":"TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA","executable":false,"rentEpoch":0,"space":165}}},"subscription":142408}} < {"jsonrpc":"2.0","method":"programNotification","params":{"result":{"context":{"slot":209131042},"value":{"pubkey":"GGUZzsex1ybU4V1duGetLHqFc7zz74jPbLp6rvNoScrR","account":{"lamports":2039280,"data":{"program":"spl-token","parsed":{"info":{"isNative":false,"mint":"GCBnu9k28isstJjCcYoZZcyTkMh5cXTsk7abpgWJesQT","owner":"2E7BD9ibbHinwohM4pLFsjdFYq1S2o4wqKmfaQXXg8Dr","state":"initialized","tokenAmount":{"amount":"0","decimals":9,"uiAmount":0.0,"uiAmountString":"0"}},"type":"account"},"space":165},"owner":"TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA","executable":false,"rentEpoch":0,"space":165}}},"subscription":142408}} < {"jsonrpc":"2.0","method":"programNotification","params":{"result":{"context":{"slot":209131042},"value":{"pubkey":"GGUV4mWenyQGyVVCNV3xPjmioJoMCCYSPFxFzFB3AmBt","account":{"lamports":2039280,"data":{"program":"spl-token","parsed":{"info":{"isNative":false,"mint":"GCBnu9k28isstJjCcYoZZcyTkMh5cXTsk7abpgWJesQT","owner":"2E7BD9ibbHinwohM4pLFsjdFYq1S2o4wqKmfaQXXg8Dr","state":"initialized","tokenAmount":{"amount":"0","decimals":9,"uiAmount":0.0,"uiAmountString":"0"}},"type":"account"},"space":165},"owner":"TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA","executable":false,"rentEpoch":0,"space":165}}},"subscription":142408}}这里里面就可以看到。有很多的SPL Token账号都在变化。并且因为我们加了"jsonParsed",所以这里SPL Token的内容也展示出来了。 订阅交易状态比如我们希望在我们发起交易后,第一时间知道交易的确定状态,我们可以通过订阅该事件来实现: Copy{ "jsonrpc": "2.0", "id": 1, "method": "signatureSubscribe", "params": [ "BfQAbgqQZMfsxFHwh6Hve8yGb843QfZcYtD2j2nN3K1hLHZrQjzdwG9uWgNkGXs4tBNVLE3JAzvNLtwJBt3zDsN", { "commitment": "finalized", "enableReceivedNotification": false } ] }这里。我们再次发起一笔转账交易: Copysolana transfer --allow-unfunded-recipient CZmVK1DymrSVWHiQCGXx6VG5zgHVrh5J1P514jHKRDxA 0.01 Signature: BfQAbgqQZMfsxFHwh6Hve8yGb843QfZcYtD2j2nN3K1hLHZrQjzdwG9uWgNkGXs4tBNVLE3JAzvNLtwJBt3zDsN然后在另外一个终端,迅速建立wscat连接,并订阅该事件: Copywscat -c wss://api.devnet.solana.com Connected (press CTRL+C to quit) > {"jsonrpc":"2.0","id":1,"method":"signatureSubscribe","params":["BfQAbgqQZMfsxFHwh6Hve8yGb843QfZcYtD2j2nN3K1hLHZrQjzdwG9uWgNkGXs4tBNVLE3JAzvNLtwJBt3zDsN",{"commitment":"finalized","enableReceivedNotification":false}]} < {"jsonrpc":"2.0","result":3285176,"id":1} < {"jsonrpc":"2.0","method":"signatureNotification","params":{"result":{"context":{"slot":209127740},"value":{"err":null}},"subscription":3285176}}可以看到。当到达"finalized"状态时,通知我们,该交易已经成功,没有出错。
-
Solana零基础中文开发详细教程全篇
为什么选择 Solana?Solana 作为新一代高性能公链,以其每秒处理 65,000 笔交易的速度和低至 $0.00025 的手续费,正在重新定义区块链开发的可能性。对于中文开发者而言,掌握 Solana 开发技术意味着: 进入 Web3 高薪领域:Solana 生态项目平均薪资比传统开发高 50% 低门槛创业机会:DeFi、NFT、GameFi 应用层出不穷 技术前瞻性:并行处理、POH 共识机制等创新技术 教程目录(点击链接进入相关教程)Week 1 Solana 基础知识 Solana 介绍 Solana 核心概念 SPL 代币 命令行工具 钱包使用 课后练习 Week 2 通过 RPC 与 Solana 交互 Solana 的 RPC 介绍 接口 RPC 推送 RPC 课后练习 Week 3 与 Solana 合约交互 Solana 的 Web3.js 与钱包交互 合约调用 课后练习 Week 4 Rust 基本知识 Hello World Rust 基本语法 通过 Cargo 管理工程 Rustaceans 的理解 课后练习 Week 5 Solana 合约开发 Part.1 Hello World Solana 合约基础概念 Solana 合约处理逻辑 Solana 合约错误定义 课后练习 Week 6 Solana 合约开发 Part.2 使用 VS Code 开发合约 PDA 账号 合约间调用 CPI 系统变量 课后练习 Week 7 Solana 合约开发进阶 ALTs 交易 Solana 序列化标准 Anchor 协议 Anchor 开发框架 Anchor 实践 Week 8 Solana DApp 开发实践 DeFi & NFT TokenSwap 合约走读 Solana 的 NFT 事实标准 Metaplex Week 9 Solana 合约安全 Cashio 攻击事件分析 黑客入侵手段和网络安全 合约开发安全注意点 学习收获完成本教程后,你将: 技术能力 独立开发完整 DApp(前端+合约+运维) 掌握 Solana 独特编程模型 具备合约安全审计能力 项目经验 拥有 3 个可展示的主网项目 GitHub 获得 100+ star 的开源代码 完整的技术文档写作经验 职业发展 获得官方认证开发者证书 推荐至头部 Web3 企业实习 优秀项目获得生态基金投资
-
合约开发安全注意点
合约开发安全注意点签名安全我们需要对敏感的数据修改做权限校验,比如如下程序 Copypub fn process_create( program_id: &Pubkey, accounts: &[AccountInfo], msg: String, ) -> ProgramResult { let accounts_iter = &mut accounts.iter(); let greeting_account = next_account_info(accounts_iter)?; // Increment and store the number of times the account has been greeted let mut greeting_info = GreetingInfo { message: "".to_string(), }; greeting_info.message = msg; greeting_info.serialize(&mut *greeting_account.data.borrow_mut())?; msg!("set note to {} !", greeting_info.message); Ok(()) }这里在修改 自己账号 GreetingInfo 的时候,并没有Check操作者是否有权限修改。如果这个结构里面存储的是资金, 那么在转移资金的时候,如果不确保相应的账号有签名校验,就有可能在这里被钻空子。 Owner 检查这个可以认为是Solana/Sui/Aptos这种将存储视为资源的链的通病。Solana里面的每个Account都有Owner属性, 只有Owner为本合约,这个合约才有权限操作这个Account。当然了,这就带来了,如果我们不检查这个Owner的话, 就有可能操作一个本不该这个合约可以操作的Account,比如读取某个状态变量。 Copy#[derive(Clone)] pub struct AccountInfo<'a> { /// Public key of the account pub key: &'a Pubkey, /// Was the transaction signed by this account's public key? pub is_signer: bool, /// Is the account writable? pub is_writable: bool, /// The lamports in the account. Modifiable by programs. pub lamports: Rc<RefCell<&'a mut u64>>, /// The data held in this account. Modifiable by programs. pub data: Rc<RefCell<&'a mut [u8]>>, /// Program that owns this account pub owner: &'a Pubkey, /// This account's data contains a loaded program (and is now read-only) pub executable: bool, /// The epoch at which this account will next owe rent pub rent_epoch: Epoch, }这个是SPL-Token的 Token Account。这个Account的Owner为 "SPL-Token", 如果在我们的合约中 依赖读取某个 TokenAccount的余额信息,但是又没有做Owner检查的时候,科学家就可以按照上面的 这个Account的数据格式,构造出来一个这样的Account,然后将这个Account传递给我们的合约,从而 修改状态数据。 PDA 错乱PDA是我们常用的用来存储特定数据的方式,通过给定Seed,我们可以推到出需要的Account的地址。因此 这里必须设计好PDA的Seed。如果Seed的规则设计的有问题,就有可能导致不同的逻辑,可以互相操作对方 的数据。 比如我们有这样的Seed设计。为每个用户生成一个Vault Account,该Account的生成Seed规则是: Copy使用者的Address+"vault"然后再另外一个Play的逻辑里面也定义了一些PDA,比如: Copy使用者的Address+"profile" 使用者的Address+"level" ... 使用者的Address+ "vault"这里因为不知道之前已经用了这个规则,因此在此对这个账号进行了写入操作。就会导致之前写入的关键金融信息丢失。 另外一种场景就是CPI之间调用的时候,我们是要根据 Seed生成Bump,如果Seed的规则过于简单,可以被科学家 推导出来,那么就可以进行相应的构造,从而使得这个 CPI的调用PDA签名成立,对相应的数据进行修改。 Type Cosplay因为Solana在操作的时候,需要客户端将相应的Account的地址都传递给合约进行操作。那么这里在传递的时候, 有时候可以被科学家构造一个相同的数据结果的Account给到合约去用。这样因为这个数据是伪造的,就有可能导致 逻辑出错问题。 比如这里我们定义了用户的Config信息: Copypub struct UserConfig { x: u8, y: u16, z: u32, }在我们操作用户的动作的时候,需要传递这个信息: Copyfn process_play(cfg: UserConfig, accounts: &[AccountInfo] ) { let accounts_iter = &mut accounts.iter(); let user_account = next_account_info(accounts_iter)?; ... user.score = cfg.x*1+cfg.y*1+cfg.z*1 ... }这里计算用户得分的时候,需要传递一个配置文件。假设这里我们没有check这个Account的其他信息。直接 使用的话。因为这里数据结构是一样的,因此合约 是可以正常执行的。但是我们使用的配置信息就不一样了, 就给了科学家可操作的空间。 Account Close在前面的课程中,我们有介绍rents的作用。当Account里面的lamports低于需要的rents的时候,该Account 将会被系统回收,也就是会销毁这个Account。 在我们的合约中,有时候需要主动的关闭或者说消耗我们Account资源,就好比我们普通的程序中可能会删除某个 文件一样。比如有个玩家注销的时候,需要删除这个用户的用户数据,由PDA生成的一个Account资源。 正常的操作里面,一般是有个Instruction来处理这个事情具体的处理逻辑如下: Copypub fn close(ctx: Context<Close>) -> ProgramResult { let dest_starting_lamports = ctx.accounts.destination.lamports(); **ctx.accounts.destination.lamports.borrow_mut() = dest_starting_lamports .checked_add(ctx.accounts.account_to_close.to_account_info().lamports()) .unwrap(); **ctx.accounts.account_to_close.to_account_info().lamports.borrow_mut() = 0; Ok(()) }先把目标Account的lamports设置为0,然后再将其lamports转移到需要存储的Account中。 正常情况下是没有问题的,在这个instruciton执行后,这个Account的owner会变成System,然后并不是立马被 销毁,而是在系统做GC的时候,进行销毁。 此时科学家有可能构造一个tx,里面在这个instruction之后,再跟一个给这个Account传入sol的动作。这样这个 Account就会成为一个脏数据,或者历史数据遗留下来。 比如我们在这个Account里面记录用户可以提取的奖励,提取一次后,就关闭了。因为这里没法关闭,就可能导致用户 可以一直提取。 多Bumps待选在使用PDA的时候,我们在合约中是这样操作的 Copylet (gen_ext_mint_key, bump) = Pubkey::find_program_address( &[ &spl_token_program_account.key.to_bytes(), &mint_account.key.to_bytes(), ], program_id, );这里"find_program_address"是从[0-255]找到第一个可以用的bump跟seed凑成可以推到出这里的PDA 地址。但是[0-255]并不是只有一个值可以用。 所以我们需要做好这个bump的校验。 假设我们在这里没有校验bump,科学家在传入的account里面,用另外一个bump构造了这样的一个Account, 那么合约也是可以正常执行的,但是这个时候操作的数据却是另外一个账号,假设我们将是否可以提前某个奖励 的信息存放在这个Account中。当判断是否提前的时候,科学家就用这个 bump2 的Account,提取的时候, 就用正确的bump,这样就又构造了一个无限提取的条件。
-
Solana经典黑客入侵手段防范(开发篇)
经典科学家手段假Token前面介绍的cashio,本质上就是个假Token的案例。 假Token最出名的,是从EOS时代。ERC20规范导致,大家都会去检查ERC20的地址,或者说写合约的时候 判断条件就是地址。而EOS包括现在的Solana,地址条件不是那么明显,比如EOS的name,Solana 需要 用户传递进来Mint Account。 因此如果没有做相关check就有可能出现假Token 闪电贷闪电贷本身是没有什么问题,但是闪电贷可能被科学家用于辅助。比如正常情况下,用户是没有那么多的 某个Token的,但是他可以去闪电贷服务里面,临时借入大量这种Token,然后操作我们的逻辑,影响这里的 数据,典型的是DeFi里面,通过外借Token,进行砸盘。然后同一个Tx里面再跟上科学家自己的Token 低价买入。 CPI普通合约都是针对普通玩家来执行动作的,也就是发起交易的是一个owner为system的Account。这个Account 最多就是将多个指令串在一个交易里面执行,但是他没法做到其他更多的功能。 通过CPI,我们可以让发起动作的是一个合约,在这个合约里面,我们就可以编程了,Sui 提出了一个叫做 "Programmable Transaction"的概念,其实就类似这个。或者ETH里面的MultiCall也是类似的概念。 在这里我们就可以借助合约的执行,来做一些逻辑,比如判断当前执行的时候,Blocke Height,Slot信息。 权限检查权限检查是个老生常谈的问题,在任何链上都是存在的。但是在Solana上,可能会更明显。 为什么呢?因为Account除了记录的信息外,其本身还有个owner的属性,来判断所属。所以除了我们应用逻辑里面 检查这里Account里面记录的信息外,还需要检查这个Account的onwer 重入、重新初始化在Eth中,重入是个必检查项目。在Solana里面,重入和重新初始化问题不是那么严重,我们可以简单的在 Account里面增加一个字段,表示是否已经初始化了。 另外,可以根据这个Account是否存在来判断是否已经初始化了。 但是如果没有检查,就有可能出现这样的问题。
-
Solana合约安全 Cashio 攻击事件分析
Cashio 攻击事件分析Cashio是一个去中心化的稳定币平台,完全由计息的Saber美元流动性提供者代币支持。允许用户做两件事:一是通过存入相应价值的稳定对 LP 代币作为抵押品,打印与美元挂钩的稳定币 - $CASH;二是燃烧$CASH 以赎回基础 LP 代币。 Cashio 提供了一种无需信任、去中心化的稳定币,由于通过 Sunny、Sabre 和 COW 上的 Tribeca Protocol Gauges 战略性地针对 CASH LP 农场的奖励代币激励措施,提高了 CASH LP 对的收益。 Cashio 应用程序提供了一个简单的界面来增加稳定币对的收益: 将稳定的货币流动性存入 Sabre 以换取 LP 代币。 将 LP 代币存入 Cashio 以打印 $CASH 将 $CASH 与其他稳定币存入 Sabre 以获得 $CASH LP 代币 在北京时间2022年3月23日,Cashio 称遭到黑客攻击,合约存在铸造故障,声明用户不要铸造任何资金,并督促用户提取池中的资产。 攻击分析攻击交易为4fgL8D6QXKH1q3Gt9GPzeRDpTgq4cE5hxf1hNDUWrJVUe4qDJ1xmUZE7KJWDANT99jD8UvwNeBb1imvujz3Pz2K5 攻击者的基本逻辑是 S1: 铸造Token,作为LP token S2: 用这个LP Token去抵押得到$CASH S3: 将$CASH套现为$USDT/$USDC 这里的基本逻辑就是用了一个fack的LP Token,可以正常兑换出来$CASH。 攻击者调用 BRRRot6ig147TBU6EGp7TMesmQrwu729CbG6qu2ZUHWm的 "2efcde74f717f00300008d49fd1a0700" 指令的时候,用 GCnK63zpqfGwpmikGBWRSMJLGLW8dsW97N4VAXKaUSSC充当了LP,触发Brr 在1.3里面Mint了 2000000000000000 个CASHVDm2wsJXfhj6VWxb7GiMdoLc17Du7paH4bNr5woT给科学家。 问题原因通过查看出问题的合约BRRRot6ig147TBU6EGp7TMesmQrwu729CbG6qu2ZUHWm 的代码print_cash.rs Copyimpl<'info> Validate<'info> for PrintCash<'info> { fn validate(&self) -> Result<()> { self.common.validate()?; assert_keys_eq!(self.depositor, self.depositor_source.owner); assert_keys_eq!(self.depositor_source.mint, self.common.collateral.mint); assert_keys_eq!(self.mint_destination.mint, self.common.crate_token.mint); assert_keys_eq!(self.issue_authority, ISSUE_AUTHORITY_ADDRESS); Ok(()) } }这里没有检查时没有对arrow account 的 mint 字段进行校验。从而使得上面科学家的逻辑可以执行。
-
Solana的NFT事实标准Metaplex
Solana 的 NFT 事实标准 Metaplex我来看下当前 Solana 官方对于 NFT 的定义Non-Fungible tokens: 这段话出自 SPL Token 的官方文档。也就是说,Solana 上的 NFT 标准,也是由"SPL Token"来实现的,但是他是一个特殊的 Token,这个 Token 的 supply 一定为 1 精度为 0 mint Authority 为空,也就是不能再 mint 新的 token 根据我们前面的介绍,大家已经知道了 "SPL Token" 的几个基本属性,但是这里作为 NFT,他最典型的小图片地址在哪里呢?总的供应量在哪里呢? 我们来看 Solana 域名的下的 NFT 在这个页面,随便点击,我们会发现,官方站点将我们引导到了 Metaplex。这个 metaplex 是什么呢? 简而言之,就是 Metaplex 是一套 NFT 系统,他包含了一套 NFT 标准,一个发布 NFT 的工具和一套 NFT 交易市场协议。 从这里我们可以看到,Solana 官方基本是认可这里定义的这套 NFT 标准了。那么我们就来介绍下这个标准是怎样的。 NFT 标准首先 Metaplex 也一样要遵循前面 Solana 官方的"SPL Token"里面说的,一个 NFT 就是一个特殊的"SPL Token" 这个基础原则。然后 Metaplex 在这个基础之上做了一些扩展,为这个 supply 为 1 的 token 增加了如下属性: 用 JSON 表示就是: Copy{ "name": "SolanaArtProject #1", "description": "Generative art on Solana.", "image": "https://arweave.net/26YdhY_eAzv26YdhY1uu9uiA3nmDZYwP8MwZAultcE?ext=jpeg", "animation_url": "https://arweave.net/ZAultcE_eAzv26YdhY1uu9uiA3nmDZYwP8MwuiA3nm?ext=glb", "external_url": "https://example.com", "attributes": [ { "trait_type": "trait1", "value": "value1" }, { "trait_type": "trait2", "value": "value2" } ], "properties": { "files": [ { "uri": "https://www.arweave.net/abcd5678?ext=png", "type": "image/png" }, { "uri": "https://watch.videodelivery.net/9876jkl", "type": "unknown", "cdn": true }, { "uri": "https://www.arweave.net/efgh1234?ext=mp4", "type": "video/mp4" } ], "category": "video", } }这里有了 name/image/attributes 是不是就比较像一个正常的 NFT 了。 这里只要有个地方记录上面的这个 meta 信息和我们的"SPL Token"的对应关系就可以了。在 Solana 中,我们很容易就想到了 PDA: 由合约和 seeds 管理的单向 Hash 图中的信息可以和上面标准中定义的字段意义匹配上,这里将基本信息,放入到 Solana 的 Account 的进行链上存储,而内容比较多的,复杂的信息,则以一个 JSON 格式存储在链下,这里可以是一个 s3 静态文件,也可以是 ipfs 的存储。而用来存储扩展 Meta 信息的 Account,我们叫做 "Metadata Account",其地址是经由 这个管理合约,以及"SPL Token" 的 Mint Account,推导出来的 PDA 唯一地址。因此知道 "SPL Token"也就知道了这里的 Metadata。 上面的逻辑定义了单个 NFT,到这里,我们还只是实现了我们前面作业里面的相关逻辑,那这样就够了么? 我们知道 Mint Account 还可以通过 Authority 和 Freezen 来控制增发和冻结,那么这个时候,这个权限还在创建者手里,我们怎么来保证其不会被随意的触发呢? 为此 Metaplex 引入了一个"Master Edition Account"来作为某 NFT 的管理者: 这里,同样,在此以 PDA 为基础,推导出一个"Master Edition Account"地址出来,然后在这个 Account 里面来记录 Supply 等信息。同时将"SPL Token"的"mint" 和 "freeze" 都设置成这个 Account,这样就可以使得没人在此修改这个 NFT 对应的“SPL Token”。 但是在现实中,我们还会对 NFT 做集合归类,比如游戏里面 的宝物集合,装备结合,药水集合等,ERC1155 定义了这样的逻辑。而 Metaplex 定义了 Certified Collections。 为了将 NFT(或任何代币)分组在一起,Metaplex 首先创建一个 "Collection" NFT,其目的是存储与该集合相关的任何元数据。 也就是"Collection "本身就是 NFT。 它与任何其他 NFT 具有相同的链上数据布局,也就是类似的 PDA 推导还有其配套的“SPL Token” 账号。 Collection NFT 和常规 NFT 之间的区别在于,前者提供的信息将用于定义其包含的 NFT 组,而后者将用于定义 NFT 本身。 在上面的 PDA 生成的 “Metadata Account”里面有一个 Collection 属性,如果其有值,那么这个 NFT 就是从属于这个组的 NFT,如果这个属性没有值,那么这个 NFT 可能就是普通 NFT 或者是“Collection” NFT。如果其"CollectionDetails"还有值,那么就是一个“Collection” 了。 下面图描述了 NFT 组和组内普通 NFT 的关系: NFT 实例来看个 NFT 的例子,Solana 手机Saga。 点开其中一个 NFT,比如 1927: 我们可以在浏览器中看到对应的这里的 "MINT Account" BBDajxrF4KJdmXredbz8BtCBF5b5HFrAPxX5xqtysAJC 而对应的真实"SPL Token Account"为 BVhF7uWD4LYKZmwWMk7KwohZbC7vUNzQPD953h5atjb8 对应的 Collection 为 1yPMtWU5aqcF72RdyRD5yipmcMRC8NGNK59NvYubLkZ 这个 Collection 的 Mint Account: 其对应的 MasterEdition 为 5WFe712HEfptjd6EyhwojxbMiA5NVsGsr9vVj5Rm4Urs 而相关的 NFT 数据存储在 Metadata 2VVEUmmkVUg1SCtszhjnn2WNudJE5ESJKF8Q1WsfJqMy中。 Token Metadata ProgramMetaplex 的 NFT 标准实现,在一个叫做Token Metadata Program 合约中。也就是上面的用于生成 PDA 的合约。 当我们需要创建一个 NFT 的时候,我们通过 Token Metadata Program 的"Create a Metadata account" 指令来创建 一个 NFT,他会依次创建"Metadata Account"以及 "SPL Token"的"Mint Account"。 具体 Accounts 参数为: 参数为: 直接来看,太复杂了。 创建完单个 NFT 后,我们可以创建"Master Edition Account"来进行管理。我们使用"CreateMasterEditionV3"指令 Account 参数为: 参数为: 这里还是以 Saga 的 NFT 来举例,来看一个 NFT 的创建过程:首先创建 MasterEdition : 321ytzCAk2JAWwBEKKSCnk4w717UA6PWMtEfLxQQ5Pz4gS3xZA9AMUbXShU7s4ekLCkqC8s5WLhkHhtid5VCF5hD I1-I2 创建了 MintAccount I3-I4 创建 ATA 作为 TokenAccount 并 Mint I5 创建 Metadata Account I6 创建 Master Edition Account 其次创建具体的 NFT: 3P8MgszvGDmzVV3yByQnPLQ1t7p7jC9ZvtQeRs4nZxmSoWvDV2MBx9A1sCMbrZosRdXBfRZTXj6YjXimVLQuW5Rf 这里主要关注 I5,因为这里通过 Metaplex 的 Canndy 来进行 Mint 也就是 5.2 中,并且这里传递了"1yPMtWU5aqcF72RdyRD5yipmcMRC8NGNK59NvYubLkZ"作为 Collection。而 5.3 创建了 Metadata。并在 5.10 和 5.11 设置了 mint 和 frezzen 的权限。在 5.12 中完成对这个 NFT 的 Mint 逻辑。最后在 5.19 中,将这个 NFT 和这个 Collection 做 Verify,从而确保他是归属于这个 NFT 集合的。
-
Solana DApp开发实践 DeFi & NFT TokenSwap合约走读
TokenSwap合约走读Solana官方在SPL里面给了一个AMM的参考实现,其代码在 Token Swap 相应的文档在 Token Swap Program。 这个Swap合约允许在没有集中限价订单簿的情况下进行代币对的简单交易。该程序使用称为“curve”的数学公式来计算所有交易的价格。曲线旨在模仿正常的市场动态:例如,当交易者大量购买一种代币类型时,另一种代币类型的价值就会上涨。 Pool中的存款人为代币对提供流动性。这种流动性使得交易能够以现货价格执行。作为流动性的交换,储户收到矿池代币,代表他们在矿池中的部分所有权。在每次交易期间,程序都会扣留一部分输入代币作为费用。该费用通过存储在池中而增加了池代币的价值。 基本操作创建新的代币PairPool的创建展示了 Solana 上的帐户、指令和授权模型,这与其他区块链相比可能有很大不同。 两种代币类型之间的池的初始化(为简单起见,我们将其称为“A”和“B”)需要以下帐户: empty pool state account pool authority token A account token B account pool token mint pool token fee account pool token recipient account token program 只需使用 system_instruction::create_account正确的大小和足够的 lamport 来创建池状态帐户即可免租金。 Pool权限是一个 PDA地址 ,可以“签署”针对其他程序的指令。这是令牌交换计划铸造池令牌并从其令牌 A 和 B 账户转移令牌所必需的。 代币 A / B 账户、矿池代币铸造和矿池代币账户都必须创建(使用system_instruction::create_account)并初始化(使用 spl_token::instruction::initialize_mint或 spl_token::instruction::initialize_account)。代币 A 和 B 账户必须由代币资助,其所有者设置为交换机构,并且铸币厂也必须由交换机构拥有。 创建所有这些帐户后,代币交换initialize指令将正确设置所有内容并允许立即交易。请注意,池状态帐户不需要是 的签名者,因此在与其 相同的交易中initialize执行指令非常重要。initializesystem_instruction::create_account Swapping创建池后,用户可以立即使用swap指令开始对其进行交易。交换指令将代币从用户的源账户转移到交换的源代币账户,然后将代币从其目标代币账户转移到用户的目标代币账户。 由于 Solana 程序要求在指令中声明所有账户,因此用户需要从池状态账户收集所有账户信息:代币 A 和 B 账户、池代币铸造和费用账户。 此外,用户必须允许从其源代币账户转移代币。最佳实践是向spl_token::instruction::approve新的一次性密钥对输入精确的金额,然后让该新密钥对签署交换交易。这限制了程序可以从用户帐户中获取的代币数量。 Depositing liquidity为了允许任何交易,矿池需要外部提供的流动性。使用deposit_all_token_types或 deposit_single_token_type_exact_amount_in指令,任何人都可以为其他人提供流动性进行交易,作为交换,储户收到代表池中所有 A 和 B 代币部分所有权的池代币。 此外,用户需要批准委托人从其 A 和 B 代币账户转移代币。这限制了程序可以从用户帐户中获取的代币数量。 Withdrawing liquidity池代币持有者可以随时赎回其池代币以换取代币 A 和 B,并按曲线确定的当前“公平”利率返回。在withdraw_all_token_types和 withdraw_single_token_type_exact_amount_out指令中,池代币被销毁,代币 A 和 B 被转入用户的账户。 此外,用户需要批准委托人从其代币池账户转移代币。这限制了程序可以从用户帐户中获取的代币数量。 前端部分官方的实现中,同时为这个合约程序提供了一个参考的前端实现。因为后续Serum/Raydium等项目的原因,这个参考 实现后续没有再更新了。但是通过这个参考的前端已经包含了对这个Swap合约的全部交互。我们将代码拉下来: Copygit clone https://github.com/solana-labs/oyster-swap.git因为年久失修的原因,目前这个前端不一定能运行,但是我们可以从其代码中,了解以上的操作。 在"src/models/tokenSwap.ts"文件中,定义了上面说的几个功能的指令: CopycreateInitSwapInstruction, TokenSwapLayout, depositInstruction, withdrawInstruction, TokenSwapLayoutLegacyV0, swapInstruction,其主要是对 "Token Swap"提供的 JS SDK的封装,在SDK 比如创建pair的指令: Copystatic createInitSwapInstruction( tokenSwapAccount: Account, authority: PublicKey, tokenAccountA: PublicKey, tokenAccountB: PublicKey, tokenPool: PublicKey, feeAccount: PublicKey, tokenAccountPool: PublicKey, tokenProgramId: PublicKey, swapProgramId: PublicKey, nonce: number, tradeFeeNumerator: number, tradeFeeDenominator: number, ownerTradeFeeNumerator: number, ownerTradeFeeDenominator: number, ownerWithdrawFeeNumerator: number, ownerWithdrawFeeDenominator: number, hostFeeNumerator: number, hostFeeDenominator: number, curveType: number, ): TransactionInstruction { const keys = [ {pubkey: tokenSwapAccount.publicKey, isSigner: false, isWritable: true}, {pubkey: authority, isSigner: false, isWritable: false}, {pubkey: tokenAccountA, isSigner: false, isWritable: false}, {pubkey: tokenAccountB, isSigner: false, isWritable: false}, {pubkey: tokenPool, isSigner: false, isWritable: true}, {pubkey: feeAccount, isSigner: false, isWritable: false}, {pubkey: tokenAccountPool, isSigner: false, isWritable: true}, {pubkey: tokenProgramId, isSigner: false, isWritable: false}, ]; const commandDataLayout = BufferLayout.struct([ BufferLayout.u8('instruction'), BufferLayout.u8('nonce'), BufferLayout.nu64('tradeFeeNumerator'), BufferLayout.nu64('tradeFeeDenominator'), BufferLayout.nu64('ownerTradeFeeNumerator'), BufferLayout.nu64('ownerTradeFeeDenominator'), BufferLayout.nu64('ownerWithdrawFeeNumerator'), BufferLayout.nu64('ownerWithdrawFeeDenominator'), BufferLayout.nu64('hostFeeNumerator'), BufferLayout.nu64('hostFeeDenominator'), BufferLayout.u8('curveType'), BufferLayout.blob(32, 'curveParameters'), ]); let data = Buffer.alloc(1024); { const encodeLength = commandDataLayout.encode( { instruction: 0, // InitializeSwap instruction nonce, tradeFeeNumerator, tradeFeeDenominator, ownerTradeFeeNumerator, ownerTradeFeeDenominator, ownerWithdrawFeeNumerator, ownerWithdrawFeeDenominator, hostFeeNumerator, hostFeeDenominator, curveType, }, data, ); data = data.slice(0, encodeLength); } return new TransactionInstruction({ keys, programId: swapProgramId, data, }); }这里因为之前的系统中,没有Borsh也没有Anchor,通过手动的方式,排列了这里要用到的keys和各个参数。 具体参数的意义,我们在下面的合约部分会做详细介绍。 合约部分我们按照上面的代码地址,找到TokenSwap的代码,然后这里我们checkout到上面的前端对应的合约的版本,也就是 2020年11月17日的提交: Copygit checkout d46f010195c461108030e25f1808126baf1ae810首先看到的是,这个Swap合约,跟我们之前介绍的非anchor项目是类似的: Copy. ├── Cargo.toml ├── Xargo.toml ├── cbindgen.toml ├── fuzz │ ├── Cargo.toml │ └── src ├── inc │ └── token-swap.h ├── sim │ ├── Cargo.lock │ ├── Cargo.toml │ ├── simulation.py │ └── src └── src ├── constraints.rs ├── curve ├── entrypoint.rs ├── error.rs ├── instruction.rs ├── lib.rs ├── processor.rs └── state.rs主要看src目录。这里有"entrypoint"/"error"/"instruction"/"processor"以及"state"。 这里我们能看到: Copy169 constraints.rs 21 entrypoint.rs 104 error.rs 721 instruction.rs 18 lib.rs 6658 processor.rs 228 state.rs 7919 total那是不是这个合约有7k代码量的复杂呢?其实不是的,我们前面学习过rust的单元测试,其实在processor里面 其实只有一千多一点的核心代码,其余部分都是测试代码。 我们从指令开始看起来,总共定义了: Copypub enum SwapInstruction { /// Initializes a new SwapInfo. /// /// 0. `[writable, signer]` New Token-swap to create. /// 1. `[]` $authority derived from `create_program_address(&[Token-swap account])` /// 2. `[]` token_a Account. Must be non zero, owned by $authority. /// 3. `[]` token_b Account. Must be non zero, owned by $authority. /// 4. `[writable]` Pool Token Mint. Must be empty, owned by $authority. /// 5. `[]` Pool Token Account to deposit trading and withdraw fees. /// Must be empty, not owned by $authority /// 6. `[writable]` Pool Token Account to deposit the initial pool token /// supply. Must be empty, not owned by $authority. /// 7. '[]` Token program id Initialize(Initialize), /// Swap the tokens in the pool. /// /// 0. `[]` Token-swap /// 1. `[]` $authority /// 2. `[writable]` token_(A|B) SOURCE Account, amount is transferable by $authority, /// 3. `[writable]` token_(A|B) Base Account to swap INTO. Must be the SOURCE token. /// 4. `[writable]` token_(A|B) Base Account to swap FROM. Must be the DESTINATION token. /// 5. `[writable]` token_(A|B) DESTINATION Account assigned to USER as the owner. /// 6. `[writable]` Pool token mint, to generate trading fees /// 7. `[writable]` Fee account, to receive trading fees /// 8. '[]` Token program id /// 9. `[optional, writable]` Host fee account to receive additional trading fees Swap(Swap), /// Deposit both types of tokens into the pool. The output is a "pool" /// token representing ownership in the pool. Inputs are converted to /// the current ratio. /// /// 0. `[]` Token-swap /// 1. `[]` $authority /// 2. `[writable]` token_a $authority can transfer amount, /// 3. `[writable]` token_b $authority can transfer amount, /// 4. `[writable]` token_a Base Account to deposit into. /// 5. `[writable]` token_b Base Account to deposit into. /// 6. `[writable]` Pool MINT account, $authority is the owner. /// 7. `[writable]` Pool Account to deposit the generated tokens, user is the owner. /// 8. '[]` Token program id DepositAllTokenTypes(DepositAllTokenTypes), /// Withdraw both types of tokens from the pool at the current ratio, given /// pool tokens. The pool tokens are burned in exchange for an equivalent /// amount of token A and B. /// /// 0. `[]` Token-swap /// 1. `[]` $authority /// 2. `[writable]` Pool mint account, $authority is the owner /// 3. `[writable]` SOURCE Pool account, amount is transferable by $authority. /// 4. `[writable]` token_a Swap Account to withdraw FROM. /// 5. `[writable]` token_b Swap Account to withdraw FROM. /// 6. `[writable]` token_a user Account to credit. /// 7. `[writable]` token_b user Account to credit. /// 8. `[writable]` Fee account, to receive withdrawal fees /// 9. '[]` Token program id WithdrawAllTokenTypes(WithdrawAllTokenTypes), /// Deposit one type of tokens into the pool. The output is a "pool" token /// representing ownership into the pool. Input token is converted as if /// a swap and deposit all token types were performed. /// /// 0. `[]` Token-swap /// 1. `[]` $authority /// 2. `[writable]` token_(A|B) SOURCE Account, amount is transferable by $authority, /// 3. `[writable]` token_a Swap Account, may deposit INTO. /// 4. `[writable]` token_b Swap Account, may deposit INTO. /// 5. `[writable]` Pool MINT account, $authority is the owner. /// 6. `[writable]` Pool Account to deposit the generated tokens, user is the owner. /// 7. '[]` Token program id DepositSingleTokenTypeExactAmountIn(DepositSingleTokenTypeExactAmountIn), /// Withdraw one token type from the pool at the current ratio given the /// exact amount out expected. /// /// 0. `[]` Token-swap /// 1. `[]` $authority /// 2. `[writable]` Pool mint account, $authority is the owner /// 3. `[writable]` SOURCE Pool account, amount is transferable by $authority. /// 4. `[writable]` token_a Swap Account to potentially withdraw from. /// 5. `[writable]` token_b Swap Account to potentially withdraw from. /// 6. `[writable]` token_(A|B) User Account to credit /// 7. `[writable]` Fee account, to receive withdrawal fees /// 8. '[]` Token program id WithdrawSingleTokenTypeExactAmountOut(WithdrawSingleTokenTypeExactAmountOut), }总共有6个指令。分别是创建pair,swap,抵押和提取pair以及抵押和提取单个token。我们主要来看前四种。 在state里面定义了一个存储: Copypub struct SwapInfo { /// Initialized state. pub is_initialized: bool, /// Nonce used in program address. /// The program address is created deterministically with the nonce, /// swap program id, and swap account pubkey. This program address has /// authority over the swap's token A account, token B account, and pool /// token mint. pub nonce: u8, /// Program ID of the tokens being exchanged. pub token_program_id: Pubkey, /// Token A pub token_a: Pubkey, /// Token B pub token_b: Pubkey, /// Pool tokens are issued when A or B tokens are deposited. /// Pool tokens can be withdrawn back to the original A or B token. pub pool_mint: Pubkey, /// Mint information for token A pub token_a_mint: Pubkey, /// Mint information for token B pub token_b_mint: Pubkey, /// Pool token account to receive trading and / or withdrawal fees pub pool_fee_account: Pubkey, /// All fee information pub fees: Fees, /// Swap curve parameters, to be unpacked and used by the SwapCurve, which /// calculates swaps, deposits, and withdrawals pub swap_curve: SwapCurve, }这个就类似univ2里面的pair。其表示token_a_mint和token_b_mint的一个pair。pool中相应的token存放在 token account中也就是上面的token_a和token_b,对应的LP Token为pool_mint。swap_curve用于记录curve 数据信息。其定义为: Copypub struct SwapCurve { /// The type of curve contained in the calculator, helpful for outside /// queries pub curve_type: CurveType, /// The actual calculator, represented as a trait object to allow for many /// different types of curves pub calculator: Box<dyn CurveCalculator>, }这里CurveCalculator主要有四种实现: impl CurveCalculator for ConstantPriceCurve /// Constant price curve always returns 1:1 impl CurveCalculator for ConstantProductCurve /// Constant product swap ensures x * y = constant impl CurveCalculator for StableCurve /// Stable curve impl CurveCalculator for OffsetCurve /// Constant product swap ensures token a (token b + offset) = constant /// This is guaranteed to work for all values such that: /// - 1 <= source_amount <= u64::MAX /// - 1 <= (swap_source_amount (swap_destination_amount + token_b_offset)) <= u128::MAX /// If the offset and token B are both close to u64::MAX, there can be /// overflow errors with the invariant. 类比到最容易理解的UniV2,我们这里主要来看"ConstantProductCurve"。 所以整体结构就是这样的: 以process为入口,然后读取swapinfo中的token信息以及数量等,最后通过选定的curve做计算。 process_initialize首先解析各个key: Copylet account_info_iter = &mut accounts.iter(); let swap_info = next_account_info(account_info_iter)?; let authority_info = next_account_info(account_info_iter)?; let token_a_info = next_account_info(account_info_iter)?; let token_b_info = next_account_info(account_info_iter)?; let pool_mint_info = next_account_info(account_info_iter)?; let fee_account_info = next_account_info(account_info_iter)?; let destination_info = next_account_info(account_info_iter)?; let token_program_info = next_account_info(account_info_iter)?; let token_program_id = *token_program_info.key; let token_swap = SwapInfo::unpack_unchecked(&swap_info.data.borrow())?;然后用Self::unpack_token_account 将需要读取data部分的account做解析。 接着是一对的安全值check。如 Copyif *authority_info.key != token_a.owner { return Err(SwapError::InvalidOwner.into()); }检查完了以后,初始化curve: Copyswap_curve .calculator .validate_supply(token_a.amount, token_b.amount)?; ... swap_curve.calculator.validate()?; ....最后将pair的信息记录下来: Copylet obj = SwapInfo { is_initialized: true, nonce, token_program_id, token_a: *token_a_info.key, token_b: *token_b_info.key, pool_mint: *pool_mint_info.key, token_a_mint: token_a.mint, token_b_mint: token_b.mint, pool_fee_account: *fee_account_info.key, fees, swap_curve, }; SwapInfo::pack(obj, &mut swap_info.data.borrow_mut())?;将SwapInfo信息写入。 process_deposit_all_token_types首先也是解析key部分的Account和相应的data部分的unpack,这里就不截代码了。 然后将check部分封装在 check_accounts 函数中,做检查。 然后就是真正的逻辑部分,通过curve计算可以的输出数量: Copylet calculator = token_swap.swap_curve.calculator; let results = calculator .pool_tokens_to_trading_tokens( pool_token_amount, new_pool_mint_supply, to_u128(token_a.amount)?, to_u128(token_b.amount)?, ) .ok_or(SwapError::ZeroTradingTokens)?;算好了相关的输出,收取token A/B,并给用户发送LP: CopySelf::token_transfer( swap_info.key, token_program_info.clone(), source_a_info.clone(), token_a_info.clone(), authority_info.clone(), token_swap.nonce, token_a_amount, )?; Self::token_transfer( swap_info.key, token_program_info.clone(), source_b_info.clone(), token_b_info.clone(), authority_info.clone(), token_swap.nonce, token_b_amount, )?; Self::token_mint_to( swap_info.key, token_program_info.clone(), pool_mint_info.clone(), dest_info.clone(), authority_info.clone(), token_swap.nonce, to_u64(pool_token_amount)?, )?;process_swap首先也是对Key和data部分做解析。然后做相应的信息检查。检查完后,通过SwapInfo中的Curve进行计算: Copylet result = token_swap .swap_curve .swap( to_u128(amount_in)?, to_u128(source_account.amount)?, to_u128(dest_account.amount)?, trade_direction, &token_swap.fees, ) .ok_or(SwapError::ZeroTradingTokens)?;计算完成后,做Token A/B的交换 CopySelf::token_transfer( swap_info.key, token_program_info.clone(), source_info.clone(), swap_source_info.clone(), authority_info.clone(), token_swap.nonce, to_u64(result.source_amount_swapped)?, )?; Self::token_transfer( swap_info.key, token_program_info.clone(), swap_destination_info.clone(), destination_info.clone(), authority_info.clone(), token_swap.nonce, to_u64(result.destination_amount_swapped)?, )?;最后是做fee的计算和分配。 另外三个指令的操作,基本类似。整个逻辑可以类比UniV2和Banlancer的算法
-
Anchor实践
Anchor实践我们将之前的我们的记事本合约改成Anchor工程。同时为了模拟PDA,我们将记事本所在地,按照用户 改成其PDA地址。 首先创建工程: Copyanchor init note设计指令定义指令Account: Copy#[derive(Accounts)] pub struct Create<'info> { #[account( init, payer=user, space = 128, seeds = [user.key().as_ref()], bump )] pub note: Account<'info, Note>, #[account(mut)] pub user: Signer<'info>, pub system_program: Program<'info, System>, }其中State定义为: Copy#[account] pub struct Note { pub message: String }存储消息。 这里 Copy#[account( init, payer=user, space = 128, seeds = [user.key().as_ref()], bump )]会新创建一个Account,该account的地址为 seeds确定的PDA地址,空间大小为128字节,由user来支付lamports费用。 执行逻辑Copy#[program] pub mod note { use super::*; pub fn create(ctx: Context<Create>, msg: String) -> Result<()> { let note = &mut ctx.accounts.note; note.message = msg; Ok(()) } }这里整个逻辑就非常简单。直接获取相应的Account对象,然后将该state对象的message赋值即可。
-
Anchor开发框架
Anchor开发框架Anchor作为一款开发框架,提供了合约开发的基本结构,区别于我们之前介绍"instruction/stat/process" 基本程序结构,同时Anchor还提供了客户端相关的Typescript相关类库,以及"anchor"命令工具。 Anchor程序结构一个Anchor工程主要包含了 "declare_id"宏声明的合约地址,用于创建对象的owner #[derive(Accounts)] 修饰的Account对象,用于表示存储和指令 "program" 模块,这里面写主要的合约处理逻辑 对应到我们之前的HelloWorld,就是要将state和instruction部分用 #[derive(Accounts)] 修饰,将process逻辑放到program模块中,并增加一个合约地址的修饰。 #[program] 修饰的Module即为指令处理模块。其中有一个Context类型,来存放所有的指令参数。比如 ctx.accounts 所有的请求keys,也就是AccountMeta数组 ctx.program_id 指令中的program_id ctx.remaining_accounts 指令中,没有被下面说的"Accounts"修饰的成员的AccountMeta 处理指令对于指令,我们要通过#[derive(Accounts)]来修饰我们定义的指令部分的定义: Copy#[account] #[derive(Default)] pub struct MyAccount { data: u64 } #[derive(Accounts)] pub struct SetData<'info> { #[account(mut)] pub my_account: Account<'info, MyAccount> }这里定义了指令结构 "SetData" , 那么在处理里面我们就要定义相应的处理函数: Copy#[program] mod hello_anchor { use super::*; pub fn set_data(ctx: Context<SetData>, data: u64) -> Result<()> { ctx.accounts.my_account.data = data; Ok(()) } }函数名固定为结构体名的小写snake风格的命名,对应"SetData"也就是"set_data"。类似process 的函数,这个函数的原型也是固定的 Copypub fun xxx_yyy_zzz(ctx: Context<IxData>, data:Data) -> Result<()> {}第一个参数为Context 其为泛型类型,类型为要处理的指令结构,后续data部分的结构定义。 返回值为一个Result。 同时我们可以给指令增加一些校验,类似我们在process里面的相关校验。 Copy#[account] #[derive(Default)] pub struct MyAccount { data: u64, mint: Pubkey } #[derive(Accounts)] pub struct SetData<'info> { #[account(mut)] pub my_account: Account<'info, MyAccount>, #[account( constraint = my_account.mint == token_account.mint, has_one = owner )] pub token_account: Account<'info, TokenAccount>, pub owner: Signer<'info> }在需要增加校验信息的account上面增加 #[account()] 修饰,比如这里用 "mut"表示 "my_account"为"writeable", "has_one" 表示token_account的owner为这里的owner成员 "constraint" 指定限制条件,类似一个条件表达式,这里意思是 if my_account.mint == token_account.mint "init" account是否创建了 "payer" 为这个账号创建付费的账号 "space" 这个账号的data部分大小 错误处理在我们之前的结构中,我们专门用了error.rs来枚举错误,在Anchor中提供了两类错误 Anchor自身错误 自定义错误 Anchor自身错误,可以参考具体的错误码 自定义错误通过"err!"和"error_code!"宏来抛出和定义: Copy#[program] mod hello_anchor { use super::*; pub fn set_data(ctx: Context<SetData>, data: MyAccount) -> Result<()> { if data.data >= 100 { return err!(MyError::DataTooLarge); } ctx.accounts.my_account.set_inner(data); Ok(()) } } #[error_code] pub enum MyError { #[msg("MyAccount may only hold data below 100")] DataTooLarge }Anchor提供了一个类似assert的 requre!宏,用于判断条件,并打印错误码,返回错误: Copyrequire!(data.data < 100, MyError::DataTooLarge);如果条件不满足,则返回后面的错误。 合约间调用在前面介绍的CPI,我们主要是通过 invoke 和 invoke_signed来实现。在Anchor中,也可以 用这两个函数,同时如果两个合约都是anchor工程,anchor还提供了一个cpi模块来实现更方便的操作。 此时在主调项目中引入被调用项目的代码,并添加特性 features = ["cpi"]: Copypuppet = { path = "../puppet", features = ["cpi"]}这样在主调用合约工程里面,anchor会自动生成 "puppet::cpi" 模块,该模块下的accounts既可以访问到 被调用合约工程的accounts定义。而"cpi"模块下,有别调用合约的 #[program] 修饰的模块的方法 当调用时,通过 Copy被调用合约::cpi::被调用指令方法(CpiContext类型ctx, data)来进行调用,比如: Copylet cpi_program = self.puppet_program.to_account_info(); let cpi_accounts = SetData { puppet: self.puppet.to_account_info() }; let ctx = CpiContext::new(cpi_program, cpi_accounts) puppet::cpi::set_data(ctx, data)在主调合约中,先通过传递过来的被调用合约地址构造"cpi_program",然后再构造需要调用的指令结构, 用这个地址和指令结构构造CpiContext。 接着使用cpi调用即可。 当进行调用完成后,我们也可以像"invoke"一样来调用"get_return_data"获取返回值,而在Anchor中, 通过上面的介绍,我们知道,可以直接在指令函数的返回结果中从Result中获得: Copypuppet: pub fn set_data(ctx: Context<SetData>, data: u64) -> Result<u64> { puppet master: let cpi_ctx = CpiContext::new(cpi_program, cpi_accounts); let result = puppet::cpi::set_data(cpi_ctx, data)?;这里既可以获得结果值。 PDA生成在前面,我们介绍的PDA生成,是通过 Pubkey::find_program_address方法,该方法,返回一个 key和一个bump,Anchor将这个过程封装了一下,但是这里好像不是那么丝滑。 Copylet pda = hash(seeds, bump, program_id);需要自己来提供这个bump,为了寻找bump就得进行循环查找: Copyfn find_pda(seeds, program_id) { for bump in 0..256 { let potential_pda = hash(seeds, bump, program_id); if is_pubkey(potential_pda) { continue; } return (potential_pda, bump); } panic!("Could not find pda after 256 tries."); }或者由用户提供。但是实际上在Anchor中使用的时候,是不需要显式的去调用的,Anchor通过 在#[account(中添加 seeds = [b"user-stats", user.key().as_ref()], bump = user_stats.bump) 来指定seeds和bump。 这样结合本合约的地址,就可以推导出这个account的Pubkey了。 在合约里面通过 ctx.bumps.get("user_stats")既可以获得对应#[account] 修饰的指令成员Account的 如果bump不赋值,比如: Copyseeds = [b"user-stats", user.key().as_ref()], bump]在调用ctx.bumps.get("user_stats")则由合约去用上面的循环来找到第一个可用的bump。 那如果需要做签名的PDA要怎么调用,也就是在CPI中如何使用PDA签名。 这个时候我们需要将 CopyCpiContext::new(cpi_program, cpi_accounts) 修改成 CpiContext::new_with_signer(cpi_program, cpi_accounts, seeds)这里"seeds"即为生成PDA时候的Seeds。此时调用的时候,会检查 所有的cpi_accounts 都符合: Copyhash(seeds, current_program_id) == account address除非该成员Account是"UncheckedAccount"类型。 参考示例Anchor官方提供了 一些例子
-
Solana序列化标准Anchor协议
Solana序列化标准Anchor协议Anchor是什么? Anchor现在是Solana合约开发的一套框架,但是Anchor在建立之初,其实只是一个序列化协议。 Long Long Ago,我们来看之前的Token代码,关于Mint的对象的序列化存储是这样的: Copyfn pack_into_slice(&self, dst: &mut [u8]) { let dst = array_mut_ref![dst, 0, 82]; let ( mint_authority_dst, supply_dst, decimals_dst, is_initialized_dst, freeze_authority_dst, ) = mut_array_refs![dst, 36, 8, 1, 1, 36]; let &Mint { ref mint_authority, supply, decimals, is_initialized, ref freeze_authority, } = self; pack_coption_key(mint_authority, mint_authority_dst); *supply_dst = supply.to_le_bytes(); decimals_dst[0] = decimals; is_initialized_dst[0] = is_initialized as u8; pack_coption_key(freeze_authority, freeze_authority_dst);}而Instruction的打包是这样的: Copy/// Packs a [TokenInstruction](enum.TokenInstruction.html) into a byte buffer.pub fn pack(&self) -> Vec<u8> { let mut buf = Vec::with_capacity(size_of::<Self>()); match self { &Self::InitializeMint { ref mint_authority, ref freeze_authority, decimals, } => { buf.push(0); buf.push(decimals); buf.extend_from_slice(mint_authority.as_ref()); Self::pack_pubkey_option(freeze_authority, &mut buf); } ...这里都是自定义的,比如上面这些,用第一个字节作为类型判断,后续需要代码进行手动大解包。在传统的 Web2领域,这个是非常low且不工程化的实践,Web2领域有成熟的Protobuf/Thift等方案。但是这些方案 一来性能较差,二来序列化的结果较大,并不适用区块链的场景。因此在区块链领域就有了各种各样的实现, 比如Ethereum的rlp编码。 在Rust实现的公链领域中,主要有两种方案,一种是BCS,他出生 与Libra/Diem,现在主要用于Aptos和Sui。而另外一种则是Borsh 他是Near 开发团队的一大力作,在今年的Rust China上,他们也做了比较详细的一个分享,当前Borsh已经在性能,支持的语言, 压缩率上有一个比较好的表现。因此Solana官方实现也很多都采用了Borsh的序列方式。 anchor的官网域名是"https://www.anchor-lang.com/" 是不是跟一个框架有点不搭,对的。其实 anchor最开始的命题就是序列化方案,或者说一种IDL语言。 后面Anchor也改变了他的命题,将其定义为一套开发框架。那么Anchor是不是必须的?当然不是,我们前面介绍 的代码组织形式,加上Borsh的能力,其实已经很好的覆盖了Anchor的功能。但是Anchor除了这些功能外, 还通过IDL对instruction交互协议进行描述,更方便非Rust得语言的接入,比如在钱包测显示交互的内容。 同时还提供了项目管理如构建,发布等工具,以及合约逻辑结构的框架,方便做客户端接入以及rust客户端和测试。 可以类比以太生态里面的hardhat/truffle 安装现在的Anchor定位是一整套开发工具,其大部分是用rust实现的,因此我们可以通过cargo来进行安装, 前提条件是你已经按照我们前面的步骤按照好了Rust和Solana命令行。 Copycargo install --git https://github.com/coral-xyz/anchor avm --locked --force安装完成后,通过Anchor的工具安装最新的avm,avm是"Anchor Version Manager",类似rustup 管理rustc的版本,又或者pyenv管理python版本。 Copyavm install latest avm use latest然后查看anchor的版本 Copyanchor --version创建工程通过anchor的命令创建工程 Copyanchor init helloworld这将创建一个您可以移入的新锚定工作区。 Copy.├── Anchor.toml ├── Cargo.toml ├── app ├── migrations ├── node_modules ├── package.json ├── programs ├── tests ├── tsconfig.json └── yarn.lock以下是该文件夹中的一些重要文件: .anchor文件夹:它包含最新的程序日志和用于测试的本地分类帐 app目录:如果您使用 monorepo,则可以使用它来保存前端的空文件夹 programs目录:此文件夹包含您的程序。 它可以包含多个,但最初只包含一个与 <new-workspace-name> 同名的程序。 该程序已经包含一个 lib.rs 文件和一些示例代码。 tests目录:包含 E2E 测试的文件夹。 它已经包含一个用于测试programs/<new-workspace-name>中的示例代码的文件。 migrations目录:在此文件夹中,您可以保存程序的部署和迁移脚本。 Anchor.toml 文件:此文件为您的程序配置工作区范围的设置。 最初,它配置 合约在本地网络上的地址 ([programs.localnet]) 合约可以推送到的注册表 ([registry]) 测试中使用的provider ([provider]) 通过Anchor 执行的脚本 ([scripts])。 测试脚本在运行锚点测试时运行。 可以使用anchor run <script_name>运行自己的脚本。 构建工程执行build命令便可以完成对合约的构建: Copyanchor build warning: unused variable: `ctx` --> programs/hellowolrd/src/lib.rs:9:23 |9 | pub fn initialize(ctx: Context<Initialize>) -> Result<()> { | ^^^ help: if this is intentional, prefix it with an underscore: `_ctx` | = note: `#[warn(unused_variables)]` on by defaultwarning: `hellowolrd` (lib) generated 1 warning (run `cargo fix --lib -p hellowolrd` to apply 1 suggestion) Finished release [optimized] target(s) in 13m 08s这里warning可以忽略。 测试工程执行anchor的测试命令: Copy' hellowolrd Your transaction signature 5ne8MSmBpWFBnQr5LhuB87Ma2Snz4CvwuMjx4P8pSUCzJtBa5QUrsJkhnfrbaUJFcXJoPn8bx6HS2LLS11SvPurx Is initialized! (350ms) 1 passing (357ms)Done in 2.44s.可以看到这里提示测试通过。 这里测试执行的是哪里的代码呢?又是怎么运行的呢? 其实测试代码在 "tests/hellowolrd.ts"中,他就是一个类似我们自己的前端访问代码。来测试这里的合约: Copyimport * as anchor from "@coral-xyz/anchor";import { Program } from "@coral-xyz/anchor";import { Hellowolrd } from "../target/types/hellowolrd";describe("hellowolrd", () => { // Configure the client to use the local cluster. anchor.setProvider(anchor.AnchorProvider.env()); const program = anchor.workspace.Hellowolrd as Program<Hellowolrd>; it("Is initialized!", async () => { // Add your test here. const tx = await program.methods.initialize().rpc(); console.log("Your transaction signature", tx); });});这里通过anchor的sdk可以直接导入合约。 那么合约又是怎么来运行的呢?这里其实anchor拉起了一个solana的本地节点,并通过".anchor/test-ledger"下的genesis.json文件作为初始节点信息。 这里我们可以看到在运行test的时候,会同时启动一个solana进程。 发布合约Anchor在build的时候,和 solana build-sbf 一样会生成一个私钥,在位置"target/deploy/xxx.json"中, 后续我们在发布的时候,都是使用的这个私钥对应的地址,作为合约地址。因此我们可以在"lib.rs"中声明我们的 合约地址 Copydeclare_id!("8gDUQtUK65Aaq6gWHTvGJqfjoUW4Nt7GX3LfXnVhnsu8");为了在开发网发布,我们修改Anchor.toml中的provider: Copy[provider]cluster = "devnet"然后执行发布命令即可。 Copyanchor deploy Deploying cluster: https://api.devnet.solana.com Upgrade authority: /home/ubuntu/.config/solana/id.json Deploying program "hellowolrd"...Program path: ./Solana-Asia-Summer-2023/s101/Expert-Solana-Program/demo/hellowolrd/target/deploy/hellowolrd.so...Program Id: 8gDUQtUK65Aaq6gWHTvGJqfjoUW4Nt7GX3LfXnVhnsu8 Deploy success在浏览器中可以看到合约 对应的数据在 合约为422KB大小。
-
Solana合约开发进阶ALTs 交易
ALTs 交易传输到 Solana 验证器的消息不得超过 IPv6 MTU 大小,以确保通过 UDP 快速可靠地进行集群信息网络传输。Solana 的网络堆栈使用 1280 字节的保守 MTU 大小,在考虑标头后,为数据包数据(如序列化事务)留下 1232 字节。 在 Solana 上构建应用程序的开发人员必须在上述交易大小限制约束内设计其链上程序接口。一种常见的解决方法是将状态临时存储在链上并在以后的交易中使用该状态。这是 BPF 加载程序用于部署 Solana 程序的方法。 然而,当开发人员在单个原子事务中编写许多链上程序时,这种解决方法效果不佳。组合越多,帐户输入就越多,每个帐户输入占用 32 个字节。目前没有可用的解决方法来增加单个事务中使用的帐户数量,因为每个事务必须列出正确锁定帐户以进行并行执行所需的所有帐户。因此,在考虑签名和其他交易元数据后,当前上限约为 35 个账户。 地址查找表通常简称为“查找表”或简称“ ALT ”,允许开发人员创建相关地址的集合,以便在单个事务中有效地加载更多地址。 由于 Solana 区块链上的每笔交易都需要列出作为交易一部分进行交互的每个地址,因此该列表实际上将限制每笔交易的 32 个地址。在地址查找表的帮助下,一笔交易现在可以将该限制提高到每笔交易 256 个地址。 ALT在这里,我们描述了一个基于程序的解决方案,协议开发人员或最终用户可以在链上创建相关地址的集合,以便在交易的帐户输入中简洁使用。 地址存储在链上地址查找表账户中后,可以使用 1 字节 u8 索引而不是完整的 32 字节地址在交易中简洁地引用它们。这将需要一种新的交易格式来利用这些简洁的引用以及运行时处理来从链上查找表中查找和加载地址。 地址查找表在初始化时以及每次添加新地址后都必须免租。查找表可以从链上缓冲的地址列表扩展,也可以直接通过指令数据附加地址来扩展。新添加的地址需要一个槽位进行预热,然后才能供交易进行查找。 由于事务使用u8索引来查找地址,因此每个地址表最多可以存储 256 个地址。除了存储的地址之外,地址表帐户还跟踪下面解释的各种元数据。 Copy/// The maximum number of addresses that a lookup table can holdpub const LOOKUP_TABLE_MAX_ADDRESSES: usize = 256;/// The serialized size of lookup table metadatapub const LOOKUP_TABLE_META_SIZE: usize = 56;pub struct LookupTableMeta { /// Lookup tables cannot be closed until the deactivation slot is /// no longer "recent" (not accessible in the `SlotHashes` sysvar). pub deactivation_slot: Slot, /// The slot that the table was last extended. Address tables may /// only be used to lookup addresses that were extended before /// the current bank's slot. pub last_extended_slot: Slot, /// The start index where the table was last extended from during /// the `last_extended_slot`. pub last_extended_slot_start_index: u8, /// Authority address which must sign for each modification. pub authority: Option<Pubkey>, // Raw list of addresses follows this serialized structure in // the account's data, starting from `LOOKUP_TABLE_META_SIZE`.}一旦不再需要地址查找表,就可以将其停用并关闭以回收其租金余额。地址查找表不能在同一地址重新创建,因为每个新的查找表必须在从最近的槽派生的地址处初始化。 地址查找表可以随时停用,但可以继续被事务使用,直到停用槽不再出现在槽哈希 sysvar 中。此冷却期可确保正在进行的事务无法被审查,并且地址查找表无法关闭并为同一槽重新创建。 版本化交易为了支持上述的ALT,我们需要对交易数据内容做修改,因此区别于原始的交易(legacy),新交易使用了 VersionedTransaction: Copy#[derive(Serialize, Deserialize)]pub struct VersionedTransaction { /// List of signatures #[serde(with = "short_vec")] pub signatures: Vec<Signature>, /// Message to sign. pub message: VersionedMessage,}// Uses custom serialization. If the first bit is set, the remaining bits// in the first byte will encode a version number. If the first bit is not// set, the first byte will be treated as the first byte of an encoded// legacy message.pub enum VersionedMessage { Legacy(LegacyMessage), V0(v0::Message),}// The structure of the new v0 Message#[derive(Serialize, Deserialize)]pub struct Message { // unchanged pub header: MessageHeader, // unchanged #[serde(with = "short_vec")] pub account_keys: Vec<Pubkey>, // unchanged pub recent_blockhash: Hash, // unchanged // // # Notes // // Account and program indexes will index into the list of addresses // constructed from the concatenation of three key lists: // 1) message `account_keys` // 2) ordered list of keys loaded from address table `writable_indexes` // 3) ordered list of keys loaded from address table `readonly_indexes` #[serde(with = "short_vec")] pub instructions: Vec<CompiledInstruction>, /// List of address table lookups used to load additional accounts /// for this transaction. #[serde(with = "short_vec")] pub address_table_lookups: Vec<MessageAddressTableLookup>,}/// Address table lookups describe an on-chain address lookup table to use/// for loading more readonly and writable accounts in a single tx.#[derive(Serialize, Deserialize)]pub struct MessageAddressTableLookup { /// Address lookup table account key pub account_key: Pubkey, /// List of indexes used to load writable account addresses #[serde(with = "short_vec")] pub writable_indexes: Vec<u8>, /// List of indexes used to load readonly account addresses #[serde(with = "short_vec")] pub readonly_indexes: Vec<u8>,}新的VersionedTransaction需要用VersionedMessage来构造,而VersionedMessage使用的是 v0::Message 其中包含了"address_table_lookups"他是"MessageAddressTableLookup" 数组,每个Table包含了Table存储的内容Account,以及其实读和写的Index。 这样最终在序列化的交易中,只需要Table中的index和Table的地址,既可以实现对256个Account的 追踪。 ALT使用要发起ALT交易,首先要通过createLookupTable创建ALT的Table账户: Copyconst web3 = require("@solana/web3.js");// connect to a cluster and get the current `slot`const connection = new web3.Connection(web3.clusterApiUrl("devnet"));const slot = await connection.getSlot();// Assumption:// `payer` is a valid `Keypair` with enough SOL to pay for the executionconst [lookupTableInst, lookupTableAddress] =web3.AddressLookupTableProgram.createLookupTable({ authority: payer.publicKey, payer: payer.publicKey, recentSlot: slot,});console.log("lookup table address:", lookupTableAddress.toBase58());// To create the Address Lookup Table on chain:// send the `lookupTableInst` instruction in a transaction然后将要用到的Account的地址,存入这个账号: Copy// add addresses to the `lookupTableAddress` table via an `extend` instructionconst extendInstruction = web3.AddressLookupTableProgram.extendLookupTable({payer: payer.publicKey,authority: payer.publicKey,lookupTable: lookupTableAddress,addresses: [ payer.publicKey, web3.SystemProgram.programId, // list more `publicKey` addresses here],});// Send this `extendInstruction` in a transaction to the cluster// to insert the listing of `addresses` into your lookup table with address `lookupTableAddress`然后发起交易: Copy// Assumptions:// - `arrayOfInstructions` has been created as an `array` of `TransactionInstruction`// - we are using the `lookupTableAccount` obtained above// construct a v0 compatible transaction `Message`const messageV0 = new web3.TransactionMessage({payerKey: payer.publicKey,recentBlockhash: blockhash,instructions: arrayOfInstructions, // note this is an array of instructions}).compileToV0Message([lookupTableAccount]);// create a v0 transaction from the v0 messageconst transactionV0 = new web3.VersionedTransaction(messageV0);// sign the v0 transaction using the file system wallet we created named `payer`transactionV0.sign([payer]);// send and confirm the transaction// (NOTE: There is NOT an array of Signers here; see the note below...)const txid = await web3.sendAndConfirmTransaction(connection, transactionV0);console.log(`Transaction: https://explorer.solana.com/tx/${txidV0}?cluster=devnet`,);实例以ALT的方式,来组合实现Mint Token的创建 Copyconst slot = await connection.getSlot(); const [lookupTableIx, lookupTableAddress] = await AddressLookupTableProgram.createLookupTable({ authority: publicKey, payer: publicKey, recentSlot: slot, }); const extendIx = await AddressLookupTableProgram.extendLookupTable({ payer: publicKey, authority: publicKey, lookupTable: lookupTableAddress, addresses: [ publicKey, SystemProgram.programId, mintKeypair.publicKey, TOKEN_PROGRAM_ID ], }); const txInstructions = [ lookupTableIx, extendIx, SystemProgram.createAccount({ fromPubkey: publicKey, newAccountPubkey: mintKeypair.publicKey, space: MINT_SIZE, lamports:lamports, programId: TOKEN_PROGRAM_ID, }), createInitializeMint2Instruction(mintKeypair.publicKey, 9, publicKey, publicKey, TOKEN_PROGRAM_ID) ]; console.log("txi : ", txInstructions); const { context: { slot: minContextSlot }, value: { blockhash, lastValidBlockHeight }, } = await connection.getLatestBlockhashAndContext(); //let latestBlockhash = await connection.getLatestBlockhash("finalized"); enqueueSnackbar( ` - Fetched latest blockhash. Last Valid Height: ${lastValidBlockHeight}` ); console.log("slot:", minContextSlot); console.log("latestBlockhash:", blockhash); const messageV0 = new TransactionMessage({ payerKey: publicKey, recentBlockhash: blockhash, instructions: txInstructions, }).compileToV0Message(); const trx = new VersionedTransaction(messageV0); const signature = await sendTransaction(trx, connection, { minContextSlot, signers:[mintKeypair], }); console.log("signature:", signature);运行后,我们创建Token,并得到交易记录 https://explorer.solana.com/tx/4DFETLv7bExTESy4cGtJ1A7Vd4G8WK2f48hCAhB33i2bc9Kuofbw9y5KeLqBW4gbFHFMA4RnUgDuzAkcsbrszQRp?cluster=devnet
-
Solana合约开发 Part.2课后练习
课后练习扩充 Token 合约,为 Token 合约增加 Meta 信息,如 icon: 代币图标 name: 代币名称 symbol: 代币符号缩写 home: 代币主页 参考答案我们实现一个合约,这个合约输入为一个 Mint 的 token 地址,然后我们在这个合约中用 SPL Token 地址这个 Mint 的地址为 seed 生成一个 PDA: Copylet (gen_ext_mint_key, bump) = Pubkey::find_program_address( &[ &spl_token_program_account.key.to_bytes(), &mint_account.key.to_bytes(), ], program_id, );以这个推导出来的地址作为 Token 的 Meta 信息,然后定义其中格式为: Copy#[derive(BorshSerialize, BorshDeserialize, Debug)] pub struct ExtMint { /// number of greetings pub mint: Pubkey, pub name: String, pub symbol: String, pub icon: String, }为这个合约定义一个 mint 的 instruction: Copy/// Instructions supported by the generic Name Registry program #[derive(Clone, Debug, BorshSerialize, BorshDeserialize, PartialEq, BorshSchema)] pub enum ExtSplInstruction { Mint{ name: String, symbol: String, icon: String, } }在处理里面,首先创建这个 Meta 信息的 Account,然后将这些内容序列化进去: Copylet ext_mint = ExtMint{ mint: *mint_account.key, name: name, symbol: symbol, icon: icon, }; let ext_mint_data_len = ext_mint.try_to_vec().unwrap().len(); let rent = Rent::get()?; let invoke_seed: &[&[_]] = &[ &spl_token_program_account.key.to_bytes(), &mint_account.key.to_bytes(), &[bump], ]; invoke_signed( &system_instruction::create_account( auth_account.key, ext_mint_account.key, rent.minimum_balance(ext_mint_data_len).max(1), ext_mint_data_len as u64, program_id, ), &[ auth_account.clone(), ext_mint_account.clone(), system_program_account.clone(), ], &[invoke_seed], )?; ext_mint.serialize(&mut *ext_mint_account.data.borrow_mut())?;在客户端访问的时候,只需要知道是那个 Token 的 Mint 地址。就可以构造出 Meta 信息的 Account,然后请求 Account 并做解析: Copy#[derive(BorshSerialize, BorshDeserialize, Debug)] pub struct ExtMint { /// number of greetings pub mint: Pubkey, pub name: String, pub symbol: String, pub icon: String, } let state = client.get_account(&ext_mint).unwrap(); let extmint_info= ExtMint::try_from_slice(&state.data).unwrap(); println!("extmint_info:{:#?}", extmint_info);参考代码w6-exerciese
-
系统变量
系统变量Solana作为一个24h小时运行的系统,其中一些系统变量可以通过接口直接获取,而另外一些变量 则要需要将特定的Account通过指令传递给合约。 Clock EpochSchedule Fees Rent 这几个变量,可以在合约里面直接通过get()方法得到。比如: Copylet clock = Clock::get()即可得到Clock对象。而其他的变量,则需要在指令中传入该变量的地址,然后再合约里面解析: Copylet clock_sysvar_info = next_account_info(account_info_iter)?; let clock = Clock::from_account_info(&clock_sysvar_info)?;ClockClock的内容为: Copy#[repr(C)] pub struct Clock { pub slot: Slot, pub epoch_start_timestamp: UnixTimestamp, pub epoch: Epoch, pub leader_schedule_epoch: Epoch, pub unix_timestamp: UnixTimestamp, }其意义有: Slot:当前槽位 epoch_start_timestamp:该纪元中第一个槽的 Unix 时间戳。 在纪元的第一个时隙中,此时间戳与 unix_timestamp(如下)相同。 epoch:当前纪元 leader_schedule_epoch:已生成领导者调度的最新纪元 unix_timestamp:该槽的 Unix 时间戳。 每个时段都有基于历史证明的估计持续时间。 但实际上,时隙的流逝速度可能比这个估计更快或更慢。 因此,槽的 Unix 时间戳是根据投票验证器的预言机输入生成的。 该时间戳计算为投票提供的时间戳估计的权益加权中位数,以自纪元开始以来经过的预期时间为界限。 更明确地说:对于每个槽,每个验证器提供的最新投票时间戳用于生成当前槽的时间戳估计(假设自投票时间戳以来经过的槽为 Bank::ns_per_slot)。 每个时间戳估计都与委托给该投票账户的权益相关联,以按权益创建时间戳分布。 时间戳中位数用作 unix_timestamp,除非自 epoch_start_timestamp 以来的经过时间与预期经过时间的偏差超过 25%。 EpochSchedule包含在创世纪中设置的纪元调度常量,并允许计算给定纪元中的时隙数、给定时隙的纪元等 Copy#[repr(C)] pub struct EpochSchedule { pub slots_per_epoch: u64, pub leader_schedule_slot_offset: u64, pub warmup: bool, pub first_normal_epoch: Epoch, pub first_normal_slot: Slot, }Fees当前系统的fee设置,结构为: Copy#[repr(C)] pub struct Fees { pub fee_calculator: FeeCalculator, } pub struct FeeCalculator { pub lamports_per_signature: u64, }Instructions包含正在处理消息时消息中的序列化指令。这允许程序指令引用同一事务中的其他指令 Copypub struct Instructions();Rent租金系统变量包含租金率。目前,该比率是静态的并在创世时设定。租金消耗百分比通过手动功能激活进行修改。 Copy#[repr(C)] pub struct Rent { pub lamports_per_byte_year: u64, pub exemption_threshold: f64, pub burn_percent: u8, }SlotHashes包含插槽父银行的最新哈希值。每个插槽都会更新。 SlotHistory包含上一个纪元中存在的插槽的位向量。每个插槽都会更新。 StakeHistory包含每个时期集群范围内权益激活和停用的历史记录。它在每个纪元开始时更新。 系统变量地址系统变量 地址 Clock SysvarC1ock11111111111111111111111111111111 EpochSchedule SysvarEpochSchedu1e111111111111111111111111 Fees SysvarFees111111111111111111111111111111111 Instructions Sysvar1nstructions1111111111111111111111111 RecentBlockhashes SysvarRecentB1ockHashes11111111111111111111 Rent SysvarRent111111111111111111111111111111111 SlotHashes SysvarS1otHistory11111111111111111111111111 SlotHistory SysvarS1otHistory11111111111111111111111111 StakeHistory SysvarStakeHistory1111111111111111111111111 实例在我们创建PDA账号的时候,需要给其传递多少lamports来维持其的生存呢? Copy/// Creates associated token account using Program Derived Address for the given seeds pub fn create_pda_account<'a>( payer: &AccountInfo<'a>, rent: &Rent, space: usize, owner: &Pubkey, system_program: &AccountInfo<'a>, new_pda_account: &AccountInfo<'a>, new_pda_signer_seeds: &[&[u8]], ) -> ProgramResult { if new_pda_account.lamports() > 0 { let required_lamports = rent .minimum_balance(space) .max(1) .saturating_sub(new_pda_account.lamports()); if required_lamports > 0 { invoke( &system_instruction::transfer(payer.key, new_pda_account.key, required_lamports), &[ payer.clone(), new_pda_account.clone(), system_program.clone(), ], )?; } invoke_signed( &system_instruction::allocate(new_pda_account.key, space as u64), &[new_pda_account.clone(), system_program.clone()], &[new_pda_signer_seeds], )?; invoke_signed( &system_instruction::assign(new_pda_account.key, owner), &[new_pda_account.clone(), system_program.clone()], &[new_pda_signer_seeds], ) } else { invoke_signed( &system_instruction::create_account( payer.key, new_pda_account.key, rent.minimum_balance(space).max(1), space as u64, owner, ), &[ payer.clone(), new_pda_account.clone(), system_program.clone(), ], &[new_pda_signer_seeds], ) } }这里我们通过: Copyrent.minimum_balance(space).max(1),来计算,因为rent的值可以通过: CopyRent::get()获取,所以这里,我们传递它即可。