跳转到帖子
View in the app

A better way to browse. Learn more.

WEB3论坛社区

A full-screen app on your home screen with push notifications, badges and more.

To install this app on iOS and iPadOS
  1. Tap the Share icon in Safari
  2. Scroll the menu and tap Add to Home Screen.
  3. Tap Add in the top-right corner.
To install this app on Android
  1. Tap the 3-dot menu (⋮) in the top-right corner of the browser.
  2. Tap Add to Home screen or Install app.
  3. Confirm by tapping Install.

WEB3论坛

管理员
  • 注册日期

  • 上次访问

WEB3论坛 发布的所有帖子

  1. 欢迎大家加入百谷区块链论坛社区
  2. 本着去中心化的精神,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管理平台,供数百个社区成员参与治理 协议:以美元追踪投票代表的投票权
  3. 区块链相关术语(中英对照)说明:阅读英文文档是编程开发过程中最常做的一件事,英文阅读也是一个程序员的基本能力。区块链刚刚起步,每天各种新概念层出不穷,为方便大家学习和使用,这里收录了巴比特论坛上的一个帖子内容。该帖子仍在持续更新,更多新内容请点击下面的地址阅读原帖。 原文标题:《数字货币翻译术语(中英对照)》 原文地址: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 零币协议
  4. 前言 #最近对在上 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 等热门技术进行一些探究,敬请期待! 参考资料 #
  5. 推送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"状态时,通知我们,该交易已经成功,没有出错。
  6. 为什么选择 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 企业实习 优秀项目获得生态基金投资
  7. 合约开发安全注意点签名安全我们需要对敏感的数据修改做权限校验,比如如下程序 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,这样就又构造了一个无限提取的条件。
  8. 经典科学家手段假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是否存在来判断是否已经初始化了。 但是如果没有检查,就有可能出现这样的问题。
  9. 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 字段进行校验。从而使得上面科学家的逻辑可以执行。
  10. 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 集合的。
  11. 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的算法
  12. 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赋值即可。
  13. 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官方提供了 一些例子
  14. 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大小。
  15. 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
  16. 课后练习扩充 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
  17. 系统变量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()获取,所以这里,我们传递它即可。
  18. 合约间调用Solana 运行时允许程序通过称为跨程序调用的机制相互调用。 程序之间的调用是通过一个程序调用另一个程序的指令来实现的。 调用程序将暂停,直到被调用程序完成对指令的处理。 还是以 ATA 账号来举例子。比如我们创建了代币 TokenA,这个时候我们要给某个人发放空投。那么根据我们前面的经验知道,处理方式是这样的,首先我们要为这个用户创建其对应的 TokenA 的 ATA 账号,比如叫 TokenA_ATA_Account。然后再给这个地址做 mint 操作。 那么我们首先按照前面文章的步骤,创建一个 TokenA_ATA_Account 地址,并将其 ower_pubkey 为该用户的交易。然后还要再创建一个给 TokenA_ATA_Account mint TokenA 代币的交易。 这里的两个交易有先后顺序关系,且第二个交易需要等待第一个交易执行完成,同时两次交易的 gas 费用也会更高。那么有没什么办法将两次交易进行合并呢? 答案就是用一个合约来实现这两个步骤,然后发送一个交易即可。在这个合约里面,实现对账号的创建,以及对 SPL-Token 的 mint 两个动作。这个合约需要跟 ATA 合约交互,调用 ATA 的创建账号指令,之后再和 Token 合约交互,执行其 mint 的指令。 合约之间调用分成两类,一类是不需要校验签名的invoke,另一类是目标合约需要校验操作权限的invoke_signed 方法,前者类似 router 路由到另外一个合约执行,后者则目标程序对相关的账号有操作权限,也就是我们前面说的 PDA 账号。 invoke 路由合约如果目标合约对输入的所有 Account 无需做签名校验,这种场景我们可以使用 invoke 来直接调用目标合约。比如我们在最前面的"hello wolrd"程序中,只是打印了一段日志,并没有需要对输入的 Account 签名。 这里我们再实现一个 invoke 合约: Copyuse solana_program::{ account_info::{AccountInfo, next_account_info}, entrypoint, entrypoint::ProgramResult, pubkey::Pubkey, instruction, msg, program::invoke, }; // Declare and export the program's entrypoint entrypoint!(process_instruction); // Program entrypoint's implementation pub fn process_instruction( _program_id: &Pubkey, // Public key of the account the hello world program was loaded into accounts: &[AccountInfo], // The account to say hello to _instruction_data: &[u8], // Ignored, all helloworld instructions are hellos ) -> ProgramResult { // Iterating accounts is safer than indexing let accounts_iter = &mut accounts.iter(); // Get the account to say hello to let account = next_account_info(accounts_iter)?; let helloworld = next_account_info(accounts_iter)?; msg!("invoke program entrypoint from {}", account.key); let account_metas = vec![ instruction::AccountMeta::new(*account.key, true), ]; let instruction = instruction::Instruction::new_with_bytes( *helloworld.key, "hello".as_bytes(), account_metas, ); let account_infos = [ account.clone(), ]; invoke(&instruction, &account_infos[..]) }这里,从输入中得到两个 Account,第一个是调用第二个的参数,第二个是 helloworld 的合约的地址。 然后构造一个 instruction::Instruction。最后再调用上面说的invoke,这里指令中的数组和 invoke 里面的数组是不一样的。指令中是: Copy#[repr(C)] #[derive(Debug, Default, PartialEq, Eq, Clone, Serialize, Deserialize)] pub struct AccountMeta { /// An account's public key. pub pubkey: Pubkey, /// True if an `Instruction` requires a `Transaction` signature matching `pubkey`. pub is_signer: bool, /// True if the account data or metadata may be mutated during program execution. pub is_writable: bool, }而 invoke 里面是系统的 Account: Copy/// Account information #[derive(Clone)] #[repr(C)] pub struct AccountInfo<'a> { /// Public key of the account pub key: &'a Pubkey, /// 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, /// The epoch at which this account will next owe rent pub rent_epoch: Epoch, /// Was the transaction signed by this account's public key? pub is_signer: bool, /// Is the account writable? pub is_writable: bool, /// This account's data contains a loaded program (and is now read-only) pub executable: bool, }然后我们,继续实现一个客户端的调用: Copyuse std::str::FromStr; use solana_sdk::signature::Signer; use solana_rpc_client::rpc_client; use solana_sdk::signer::keypair; use solana_sdk::transaction; use solana_program::instruction; use solana_program::pubkey; const RPC_ADDR: &str = "https://api.devnet.solana.com"; fn main() { let helloworld = pubkey::Pubkey::from_str("FbLTBNZmc77xJpf4whkr4t7vdctjsk8DBkfuksqtQ7g8").unwrap(); let invoke= pubkey::Pubkey::from_str("CjN2fRAzLxJnYrNM8X3cbJLiVR57EzXNrK1HBNR2QXPa").unwrap(); let me = keypair::Keypair::from_base58_string("VtqQi7BBnvnkkBefXigNdSwjywsTj2JNSFT82sNtQ3qcmcQ96SnLqtsmK44eCsVqgEX2YBMTmcvov3YfA2z7xs8"); println!("me is {}", me.pubkey()); let client = rpc_client::RpcClient::new(RPC_ADDR); let account_metas = vec![ instruction::AccountMeta::new(me.pubkey(), true), instruction::AccountMeta::new_readonly(helloworld, false), ]; let instruction = instruction::Instruction::new_with_bytes( invoke, "hello".as_bytes(), account_metas, ); let ixs = vec![instruction]; let latest_blockhash = client.get_latest_blockhash().unwrap(); let sig = client.send_and_confirm_transaction(&transaction::Transaction::new_signed_with_payer( &ixs, Some(&me.pubkey()), &[&me], latest_blockhash, )).unwrap(); println!("tx:{}", sig); }和前面的调用一样,主要传递了两个 AccountMeta。构建执行: Copycargo run Compiling cli v0.1.0 (./Solana-Asia-Summer-2023/s101/Solana-Program-Part2/demo/invoke/cli) Finished dev [unoptimized + debuginfo] target(s) in 2.48s Running `target/debug/cli` me is 5pWae6RxD3zrYzBmPTMYo1LZ5vef3vfWH6iV3s8n6ZRG tx:3neCUu9cQHLuZvbwiDzneFPztos3PsmNuvytSdzFUz5ix4NpK8GmgDcGvsqSPzAdmnMhTpmzhFfgNZGeRYQn3zfh产看这个交易的浏览器: 这里可以很明显的看到 "inner instruction"。以及对应的日志打印,先打印了 invoke 合约,然后到 helloworld 合约。 invoke_signed 调用invoke_signed 和 invoke 类似,都是在合约中调用其他合约的方法。但是有一个特殊的功能,就是可以进行签名校验。具体是什么样的签名呢? 在 PDA 的内容介绍中,我们有介绍到合约内生成 PDA 账号的方式,其有两个部分组成: 一个地址 一个 Seed 通过 find_program_address 可以得到 PDA 地址,以及一个 Bump 种子。 这里传入的地址,将有权限校验该 PDA 地址的签名,就可以认为这个合约地址,相当于 PDA 账号的私钥。 最常见的使用就是管理资金账号,比如: Copy# use borsh::{BorshSerialize, BorshDeserialize}; # use solana_program::{ # pubkey::Pubkey, # entrypoint::ProgramResult, # program::invoke_signed, # system_instruction, # account_info::{ # AccountInfo, # next_account_info, # }, # }; // The custom instruction processed by our program. It includes the // PDA's bump seed, which is derived by the client program. This // definition is also imported into the off-chain client program. // The computed address of the PDA will be passed to this program via // the `accounts` vector of the `Instruction` type. #[derive(BorshSerialize, BorshDeserialize, Debug)] pub struct InstructionData { pub vault_bump_seed: u8, pub lamports: u64, } // The size in bytes of a vault account. The client program needs // this information to calculate the quantity of lamports necessary // to pay for the account's rent. pub static VAULT_ACCOUNT_SIZE: u64 = 1024; / // The entrypoint of the on-chain program, as provided to the // `entrypoint!` macro. fn process_instruction( program_id: &Pubkey, accounts: &[AccountInfo], instruction_data: &[u8], ) -> ProgramResult { let account_info_iter = &mut accounts.iter(); let payer = next_account_info(account_info_iter)?; // The vault PDA, derived from the payer's address let vault = next_account_info(account_info_iter)?; let mut instruction_data = instruction_data; let instr = InstructionData::deserialize(&mut instruction_data)?; let vault_bump_seed = instr.vault_bump_seed; let lamports = instr.lamports; let vault_size = VAULT_ACCOUNT_SIZE; // Invoke the system program to create an account while virtually // signing with the vault PDA, which is owned by this caller program. invoke_signed( &system_instruction::create_account( &payer.key, &vault.key, lamports, vault_size, &program_id, ), &[ payer.clone(), vault.clone(), ], // A slice of seed slices, each seed slice being the set // of seeds used to generate one of the PDAs required by the // callee program, the final seed being a single-element slice // containing the `u8` bump seed. &[ &[ b"vault", payer.key.as_ref(), &[vault_bump_seed], ], ] )?; Ok(()) }这个程序,接收客户端输入的一个 seed 作为金库的钥匙。金库账号,由客户端请求的时候进行创建 PDA 账号,这个账号的地址参数用的就是这个合约,因此这个合约拥有对于该 PDA 账号的签名权。 所以这里在创建账号的时候,system_instruction::create_account可以鉴权通过。 在此之后,只要客户端传入正确的 seed,既可以出发这个合约对该 PDA 程序进行相应的签名动作,比如和 SPL Token 交互,做转账动作等。
  19. PDA账号程序派生地址 (PDA:Program derived addresses) 是只有程序 program_id 有权签名的帐户密钥。 该地址与 Solana Pubkey 的形式相同,但确保它们不在 ed25519 曲线上,因此没有关联的私钥。 程序派生地址允许在程序之间调用时使用以编程方式生成的签名。 使用程序派生地址,程序可以被授予对帐户的权限,然后将该权限转移给另一个程序。 这是可能的,因为程序可以充当授予权限的交易中的签名者。 例如,如果两个用户想要对 Solana 中的游戏结果进行下注,他们必须将其下注资产转移给某个遵守协议的中介机构。 目前,无法在 Solana 中将此中介程序实施为程序,因为中介程序无法将资产转移给获胜者。 此功能对于许多 DeFi 应用程序来说是必要的,因为它们需要将资产转移到托管代理,直到发生确定新所有者的事件为止。 在匹配的买价和卖价订单之间转移资产的去中心化交易所。 将资产转移给获胜者的拍卖。 收集奖品并将其重新分配给获胜者的游戏或预测市场。 程序派生地址: 允许程序控制特定地址(称为程序地址),这样外部用户就无法生成带有这些地址签名的有效交易。 允许程序以编程方式对通过跨程序调用调用的指令中存在的程序地址进行签名。 满足这两个条件,用户可以安全地将链上资产的权限转移或分配给程序地址,然后程序可以自行将该权限分配到其他地方。 生成方式由于程序地址不得位于 ed25519 曲线上,因此可能存在无效的种子和程序 ID 组合。 因此,会计算一个额外的种子(凹凸种子),从而产生偏离曲线的点。找到有效程序地址的过程是通过反复试验来实现的,即使在给定一组输入的情况下它是确定性的,但在不同的输入上成功需要不同的时间。 这意味着当从链上程序调用时,可能会产生可变数量的程序计算预算。 想要高性能的程序可能不想使用此函数,因为它可能需要相当长的时间。 已经面临超出计算预算风险的程序应谨慎调用此方法,因为程序的预算有可能偶尔且不可预测地超出。 由于链上 Solana 程序访问的所有账户地址都必须显式传递给该程序,因此 PDA 通常在链外客户端程序中派生,从而避免了生成链上地址的计算成本。 一般的使用分成两种形式: 在链外创建PDA账号,然后给到合约去用,如ATA账号 合约内部创建PDA账号,只有这个合约可以使用,如存管NFT账号 对于链外账号,可以通过SDK提供的接口来创建,如web3.js提供的: Copy/** * Async version of findProgramAddressSync * For backwards compatibility * * @deprecated Use {@link findProgramAddressSync} instead */ static async findProgramAddress( seeds: Array<Buffer | Uint8Array>, programId: PublicKey, ): Promise<[PublicKey, number]> { return this.findProgramAddressSync(seeds, programId); }来创建。 而合约里面,则可以通过: Copypub fn find_program_address(seeds: &[&[u8]], program_id: &Pubkey) -> (Pubkey, u8) { Self::try_find_program_address(seeds, program_id) .unwrap_or_else(|| panic!("Unable to find a viable program address bump seed")) }Rust SDK中的方法创建。 由于种子的散列方式,同一程序 ID 可能会发生程序地址冲突。 种子按顺序进行散列,这意味着种子 {“abcdef”}、{“abc”、“def”} 和 {“ab”、“cd”、“ef”} 在给定相同程序 ID 的情况下都将产生相同的程序地址。 由于冲突的机会对于给定的程序 ID 而言是局部的,因此该程序的开发人员必须注意选择不会相互冲突的种子。 对于容易受到此类哈希冲突影响的种子方案,常见的补救措施是在种子之间插入分隔符,例如 将 {“abc”, “def”} 转换为 {“abc”, “-”, “def”}。 使用场景PDA的初衷是设计在执行跨程序调用时,程序可以通过调用 invoke_signed 并传递用于生成地址的相同种子以及计算出的凹凸种子(该函数将其作为第二个元组元素返回)来“签名”密钥。 运行时将验证与该地址关联的程序是否是调用者,从而被授权为签名者。从而实现程序管理特定账号的需求。比如用特定账号作为资管账号,或者swap信息存储账号等。 所以在swap程序中,会使用到PDA账号作为pool的信息账号。 另外一种场景,如果给定合约程序和seed,就可以得到唯一的PDA账号,这样类似一个单向hash的特性,使得我们可以针对 特定合约,给定参数对输出账号做管理。比如SPL-Token中的ATA账号。通过已知的固定SPL-Token合约地址+用户的SOL账号地址,以及代币Mint的地址。就可以唯一确定一个地址用来存储该用户的该Mint Token的SPL-Token 账号。从而实现 只需要SOL账号地址,就可以进行SPL-Token的转账。类似的NameService,通过SOL账号地址,得到唯一存储Name的账号 ,然后将名字相关信息存储在该账号中,解析的时候,根据该逻辑即可得到SOL账号地址。 实例创建ATA账号,我们在命令行中,创建一个代币: Copyspl-token create-token Creating token EUGfLUCBAMFvEDk1MZ2SbcZQ54mdczFyFkWVYvVVUcdF under program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA Address: EUGfLUCBAMFvEDk1MZ2SbcZQ54mdczFyFkWVYvVVUcdF Decimals: 9 Signature: V32Eo2ZQttHRYzUqPgcvnGZGTzsPUDfJCJwxArCtvE372BSMtZQB7UqdJN3HUiJKg1oeQ8EYGuj5oMnFkUmcPcq然后创建我们自己账号在这个mint下的ATA账号: Copyspl-token create-account EUGfLUCBAMFvEDk1MZ2SbcZQ54mdczFyFkWVYvVVUcdF Creating account 7X8RKbXhxGATEHwXPVWvZFDL5yZwgf9YyActE93wyhku Signature: 67JaFzPF6umPu54Sh7xd8V78ECVd8iEtiR7GDLmbXEerXoprAR7vNPcekaahnxgYprbWKnhsyR98omhfjZ8qnZ1b这里的7X8RKbXhxGATEHwXPVWvZFDL5yZwgf9YyActE93wyhku账号就是我们的地址子在EUGfLUCBAMFvEDk1MZ2SbcZQ54mdczFyFkWVYvVVUcdF token下的ATA账号。他是怎么计算得来的呢? ATA地址是用 SOL钱包地址,目标代币地址以及SPL-Token地址作为Seed,在SPL-Token合约下生成的。 所以对于Rust的调用是: CopyPubkey::find_program_address( &[ &wallet_address.to_bytes(), &token_program_id.to_bytes(), &token_mint_address.to_bytes(), ], program_id, )我们写一个简单的rust程序来验证。 Copyuse std::str::FromStr; use solana_program::pubkey::Pubkey; fn main() { let spl_token_addr : Pubkey= Pubkey::from_str("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA").unwrap(); let token_addr: Pubkey = Pubkey::from_str("EUGfLUCBAMFvEDk1MZ2SbcZQ54mdczFyFkWVYvVVUcdF").unwrap(); let sol_addr: Pubkey = Pubkey::from_str("5pWae6RxD3zrYzBmPTMYo1LZ5vef3vfWH6iV3s8n6ZRG").unwrap(); let ata_program_addr: Pubkey = Pubkey::from_str("ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL").unwrap(); let seeds = [ &sol_addr.to_bytes()[..], &spl_token_addr.to_bytes()[..], &token_addr.to_bytes()[..], ]; let (ata_addr ,_seed )= Pubkey::find_program_address(&seeds[..], &ata_program_addr); println!("ata_addr is {}", ata_addr); }运行一下: Copycargo run Compiling ata v0.1.0 (./Solana-Asia-Summer-2023/s101/Solana-Program-Part2/demo/ata) Finished dev [unoptimized + debuginfo] target(s) in 0.65s Running `target/debug/ata` ata_addr is 7X8RKbXhxGATEHwXPVWvZFDL5yZwgf9YyActE93wyhku可以看到这里的ata地址和我们的命令行得出来的是一致的。
  20. 使用VS Code开发合约在前面的基础中,已经安装好了rust和solana cli。首先确定rust版本: Copyrustc --version rustc 1.70.0 (90c541806 2023-05-31)如果没有安装,可以通过: Copycurl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh Current installation options: default host triple: x86_64-apple-darwin default toolchain: stable (default) profile: default modify PATH variable: yes 1) Proceed with installation (default) 2) Customize installation 3) Cancel installation 直接Enter采用默认方式 Rust is installed now. Great! To get started you may need to restart your current shell. This would reload your PATH environment variable to include Cargo's bin directory ($HOME/.cargo/bin). To configure your current shell, run: source "$HOME/.cargo/env"这里执行下环境导入 Copysource "$HOME/.cargo/env"在此确定下solana的版本: Copysolana --version solana-cli 1.16.4 (src:4ce8fe76; feat:2891131721, client:SolanaLabs)如果这里的cli是1.14.x 为当前的主网版本,我们的操作都是在测试环境,所以这里我们切换要切换到测试 环境的1.16版本: Copysolana-install init 1.16.4安装VS Code打开VS Code的页面,根据自己的系统选择下载对应的版本。 要先安装好rust,然后再安装VC Code 的插件。VS Code安装好后,打开VS Code,然后再插件的地方 安装rust-analyzer: 创建工程在自己的工作目录下,用cargo创建一个工程: Copycargo new --lib helloworld然后进入到helloworld目录下,添加solana SDK的依赖: Copycargo add solana-program Updating `tuna` index Adding solana-program v1.16.5 to dependencies.构建与发布在 src/lib.rs文件中,填入如下合约代码: Copyuse solana_program::{ account_info::{AccountInfo, next_account_info}, entrypoint, entrypoint::ProgramResult, pubkey::Pubkey, msg, }; // Declare and export the program's entrypoint entrypoint!(process_instruction); // Program entrypoint's implementation pub fn process_instruction( _program_id: &Pubkey, // Public key of the account the hello world program was loaded into accounts: &[AccountInfo], // The account to say hello to _instruction_data: &[u8], // Ignored, all helloworld instructions are hellos ) -> ProgramResult { // Iterating accounts is safer than indexing let accounts_iter = &mut accounts.iter(); // Get the account to say hello to let account = next_account_info(accounts_iter)?; msg!("Hello World Rust program entrypoint from {}", account.key); Ok(()) }并给Cargo.toml增加: Copy[features] no-entrypoint = [] [lib] crate-type = ["cdylib", "lib"]然后执行: Copycargo build-sbf Finished release [optimized] target(s) in 0.23s表示运行成功。这个时候我们可以用solana cli来进行发布: Copysolana program deploy target/deploy/helloworld.so Program Id: FbLTBNZmc77xJpf4whkr4t7vdctjsk8DBkfuksqtQ7g8Rust客户端测试我们在新建一个cli的bin工程: Copycargo new --bin cli Created binary (application) `cli` package然后再main.rs里面添加: Copyuse std::str::FromStr; use solana_sdk::signature::Signer; use solana_rpc_client::rpc_client; use solana_sdk::signer::keypair; use solana_sdk::transaction; use solana_program::instruction; use solana_program::pubkey; const RPC_ADDR: &str = "https://api.devnet.solana.com"; fn main() { let helloworld = pubkey::Pubkey::from_str("FbLTBNZmc77xJpf4whkr4t7vdctjsk8DBkfuksqtQ7g8").unwrap(); let me = keypair::Keypair::from_base58_string("VtqQ...xs8"); println!("me is {}", me.pubkey()); let client = rpc_client::RpcClient::new(RPC_ADDR); let account_metas = vec![ instruction::AccountMeta::new(me.pubkey(), true), ]; let instruction = instruction::Instruction::new_with_bytes( helloworld, "hello".as_bytes(), account_metas, ); let ixs = vec![instruction]; let latest_blockhash = client.get_latest_blockhash().unwrap(); let sig = client.send_and_confirm_transaction(&transaction::Transaction::new_signed_with_payer( &ixs, Some(&me.pubkey()), &[&me], latest_blockhash, )).unwrap(); println!("tx:{}", sig); }执行后可以得到: Copycargo run Compiling cli v0.1.0 (/Users/cz/repos/github.com/Gametaverse/Solana-Asia-Summer-2023/s101/Solana-Program-Part2/demo/helloworld/cli) Finished dev [unoptimized + debuginfo] target(s) in 6.13s Running `target/debug/cli` me is 5pWae6RxD3zrYzBmPTMYo1LZ5vef3vfWH6iV3s8n6ZRG tx:xosYkqqhHfD2xmcrqYXUQhkdTZiVUCMSc6QWazK25Li5Y4xebuG974vSvfVQCo9A7A7MZ6KQoNaKuTEb8YxXhYY通过查看浏览器: 可以看到跟之前的Playgroud能够得到一样的效果。
  21. 课后练习实现一个备忘录合约程序。可以将一段内容存入到链上,并可以对其进行修改、删除。 参考答案w5-answer
  22. Solana合约错误定义在前面的指令处理函数中,我们需要返回ProgramResult类型,其定义为: Copyuse { std::{ result::Result as ResultGeneric, }, }; pub type ProgramResult = ResultGeneric<(), ProgramError>;其实就是一个使用了ProgramError作为Err部分的Result类型枚举。 系统错误上面提到的ProgramError的定义为: Copy/// Reasons the program may fail #[derive(Clone, Debug, Deserialize, Eq, Error, PartialEq, Serialize)] pub enum ProgramError { /// Allows on-chain programs to implement program-specific error types and see them returned /// by the Solana runtime. A program-specific error may be any type that is represented as /// or serialized to a u32 integer. #[error("Custom program error: {0:#x}")] Custom(u32), #[error("The arguments provided to a program instruction were invalid")] InvalidArgument, #[error("An instruction's data contents was invalid")] InvalidInstructionData, #[error("An account's data contents was invalid")] InvalidAccountData, #[error("An account's data was too small")] AccountDataTooSmall, #[error("An account's balance was too small to complete the instruction")] InsufficientFunds, #[error("The account did not have the expected program id")] IncorrectProgramId, #[error("A signature was required but not found")] MissingRequiredSignature, #[error("An initialize instruction was sent to an account that has already been initialized")] AccountAlreadyInitialized, #[error("An attempt to operate on an account that hasn't been initialized")] UninitializedAccount, #[error("The instruction expected additional account keys")] NotEnoughAccountKeys, #[error("Failed to borrow a reference to account data, already borrowed")] AccountBorrowFailed, #[error("Length of the seed is too long for address generation")] MaxSeedLengthExceeded, #[error("Provided seeds do not result in a valid address")] InvalidSeeds, #[error("IO Error: {0}")] BorshIoError(String), #[error("An account does not have enough lamports to be rent-exempt")] AccountNotRentExempt, #[error("Unsupported sysvar")] UnsupportedSysvar, #[error("Provided owner is not allowed")] IllegalOwner, #[error("Accounts data allocations exceeded the maximum allowed per transaction")] MaxAccountsDataAllocationsExceeded, #[error("Account data reallocation was invalid")] InvalidRealloc, #[error("Instruction trace length exceeded the maximum allowed per transaction")] MaxInstructionTraceLengthExceeded, #[error("Builtin programs must consume compute units")] BuiltinProgramsMustConsumeComputeUnits, }因此我们的process函数里面必须要返回这里的一个值枚举值。 上面的错误枚举,基本意义还是比较明确的。比如参数不对,Gas不够。相当于系统已经把错误进行了分类。 但是我们合约中的错误其实是每个逻辑不一样的,那么要如何去定义他呢? 这里上面的分类中有一类 CopyCustom(u32)这个就是专门给应用合约定义其错误用的,每个错误用一个u32来表示 合约错误定义既然是用u32类区别合约错误,自然我们想到了Rust里面的enum来定义错误: Copy#[derive(Clone, Debug, Eq, Error, FromPrimitive, PartialEq)] pub enum HelloWorldError { #[error("Not owned by HelloWolrd Program")] NotOwnedByHelloWrold, }但是这是我们自定义的枚举,要如何让他变成上面的Custom(u32) 呢?因此需要定义转换函数: Copyimpl From<HelloWorldError> for ProgramError { fn from(e: HelloWorldError) -> Self { ProgramError::Custom(e as u32) } }直接将枚举的值,转换成了Custom里面的错误码。 错误码有了,但是错误码对应的意义是什么呢? Copyimpl PrintProgramError for HelloWorldError { fn print<E>(&self) where E: 'static + std::error::Error + DecodeError<E> + PrintProgramError + FromPrimitive, { match self { HelloWorldError::NotOwnedByHelloWrold => msg!("Error: Greeted account does not have the correct program id!"), } } }这里通过PrintProgramError trate的实现,来定义其错误消息。 这样在出错的时候,返回相应错误。 Copyif greeting_account.owner != program_id { msg!("Greeted account does not have the correct program id"); return Err(HelloWorldError::NotOwnedByHelloWrold.into()); }通过into直接转换。
  23. Solana合约处理逻辑我们这里说的处理逻辑,并不是Runtime是如何去处理逻辑的。而是在我们的合约里面,要 怎么安排逻辑。 在前面的文章中,我们已经规划了一个合约的代码结构,其中在"process.rs"文件中来放我们的 处理逻辑,每个处理逻辑处理对应的Instruction。并且Instruction的data部分是通过Borsh 来进行序列化的。 那么我们一个合约中怎么根据用户的输入来执行不同的逻辑呢?在前面我们知道一个合约只有通过 "entrypoint"宏定义的一个入口。那么我们是怎么区分不同的Instruction的呢? 本质上来说,只有一个Instruction,在客户端的不同的Instruction其实只是data部分存放的 数据不同而已。这里如果有应用开发经验的同学,立马就会联想到Protobuf里面定义的各种消息。 其实这里我们也是用类似的方法,在data部分的最开始,我们用来放cmd,cmd来表示后面的内容要按照 什么结构来解析。 结构化工程上面我们在同一个文件中,安排了一个合约的各个部分。当合约逻辑复杂的时候,我们可以将其一一拆分, 在书写的时候更清晰。来看token 合约的结构: Copy├── src │ ├── entrypoint.rs │ ├── error.rs │ ├── instruction.rs │ ├── lib.rs │ ├── processor.rs │ └── state.rs其中"entrypoint" 专门用来定义合约入口函数。 在entrypoint中,最终会调用"processor"里面定义的具体逻辑。对不同的命令进行处理。 在链上要存储的结构数据,如Token账号pub struct Account, Token信息 pub struct Mint放在 "state"中,有点类似MVC结构里面的model。 相关错误,定义在"error"里面,在"processor"中处理出错的是,直接进行返回。 "lib"作为rust工程的基本结构而存在,里面也可以定义一些脚手架工具函数。 定义指令在Rust中,我们天然的可以用enum来模拟Protobuf中的Message,或者Oneof。 这里我们专门用一个"instruction.rs"文件来定义各个指令,类似pb文件定义消息: Copy/// Instructions supported by the generic Name Registry program #[derive(Clone, Debug, BorshSerialize, BorshDeserialize, PartialEq)] pub enum HelloWorldInstruction { /// Greeting to a account /// /// Accounts expected by this instruction: /// 0. `[writeable]` the account to greet /// Greeting { /// greet count counter: u32, }, }比如这里定义了"HelloWorldInstruction"指令集,其中有一个 "Greeting" 指令。 这个指令的data部分为包含“counter”成员的struct。 因为这里通过derive来默认实现了Borsh的序列化和反序列化逻辑。当我们调用 "HelloWorldInstruction"的 try_to_vec的方法的时候,就可以得到序列化后的结果。 同时对于我们这个指令,还有配套使用的Account,要将他们放入到accounts数组中。 因此如果要在Rust里面构造这样的一个指令(通常在用Rust写合约调用的时候): Copy#[allow(clippy::too_many_arguments)] pub fn greeting( helloworld_program_id: Pubkey, instruction_data: HelloWorldInstruction, name_greeting: Pubkey, ) -> Result<Instruction, ProgramError> { let data = instruction_data.try_to_vec().unwrap(); let mut accounts = vec![ AccountMeta::new(name_greeting, false), ]; Ok(Instruction { program_id: helloworld_program_id, accounts, data, }) }可以这样来实现,将相关的AccountMeta push到accounts部分。然后就构建了一个Instruction对象。 这个一般用于在Rust调用合约,或者写单元测试的时候。 指令解析上面我们构建了指令,当前端调用合约的时候,RPC会讲相应的值转换成这里的Instruction,那么我们要怎么去 理解他呢?按照上面的逻辑,我们执行反逻辑就可以了: Copypub fn process_instruction( program_id: &Pubkey, accounts: &[AccountInfo], instruction_data: &[u8], ) -> ProgramResult { msg!("Beginning processing"); let instruction = HelloWorldInstruction::try_from_slice(instruction_data) .map_err(|_| ProgramError::InvalidInstructionData)?; msg!("Instruction unpacked"); match instruction { HelloWorldInstruction::Greeting { counter, } => { msg!("Instruction: Greeting"); Processor::process_greeting(program_id, accounts, counter)?; } } Ok(()) }这里在合约入口里面,首先通过Borsh的 try_from_slice 既可以将其转换成对应的指令枚举结构。对应到这里就是 我们上面定义的HelloWorldInstruction。 然后通过match语法,将其一一匹配。 Copymatch instruction { HelloWorldInstruction::Greeting { counter, }是一个解包语法糖。意思就是将enum中定义的无名struct的成员一一借用到这里的和成员同名的变量。方便后面使用。 执行指令在上面的解析中,我们已经得到了accounts的AccountMeta数组,以及要处理的Create的指令的三个成员变量。 这样我们就可以把他们传递给我们要处理的逻辑,所以我们定义处理函数: Copypub fn process_greeting( program_id: &Pubkey, accounts: &[AccountInfo], counter: u32, ) -> ProgramResult这里传入即为上面解析出来的内容。意义相对明确。并且该函数返回了 ProgramResult类型。 在实现中,我们通过: Copylet accounts_iter = &mut accounts.iter(); let greeting_account = next_account_info(accounts_iter)?;用next_account_info 来依次取出AccountInfo对象。 Copypub struct AccountInfo<'a> { /// Public key of the account pub key: &'a Pubkey, /// 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, /// The epoch at which this account will next owe rent pub rent_epoch: Epoch, /// Was the transaction signed by this account's public key? pub is_signer: bool, /// Is the account writable? pub is_writable: bool, /// This account's data contains a loaded program (and is now read-only) pub executable: bool, }可以对这个对象的is_signer/is_writeable/owner等做一些校验。 这里我们的逻辑中要修改data部分的内容。 Model层因为要修改data部分,而data部分本质上是一段二进制内容。因此我们这里类同指令部分。借助Borsh做序列化。 将结构化数据序列化成一段二进制数据,在存入这里的data。 Copy/// Define the type of state stored in accounts #[derive(BorshSerialize, BorshDeserialize, Debug)] pub struct GreetingInfo{ /// number of greetings pub counter: u32, }定义的struct加上BorshSerialize, BorshDeserialize 的derive即可。 这样便可以跟指令部分一样,调用try_from_slice 做反序列化,得到结构体的内容。 在通过 serialize将其序列化到&mut [u8]这样的buffer中去。通过对data部分做borrow_mut 可以得到这样的类型。因此,我们在process里面增加: Copy// Increment and store the number of times the account has been greeted let mut greeting_info= GreetingInfo::try_from_slice(&greeting_account.data.borrow())?; greeting_info.counter += 1; greeting_info.serialize(&mut *greeting_account.data.borrow_mut())?;先将data部分反序列化出来,修改后,在序列化存回去。 客户端访问这里贴上客户端示例代码: Copy// No imports needed: web3, borsh, pg and more are globally available import { serialize, deserialize, deserializeUnchecked } from "borsh"; import { Buffer } from "buffer"; /** * The state of a greeting account managed by the hello world program */ class GreetingAccount { counter = 0; constructor(fields: { counter: number } | undefined = undefined) { if (fields) { this.counter = fields.counter; } } } /** * Borsh schema definition for greeting accounts */ const GreetingSchema = new Map([ [GreetingAccount, { kind: "struct", fields: [["counter", "u32"]] }], ]); class Assignable { constructor(properties) { Object.keys(properties).map((key) => { return (this[key] = properties[key]); }); } } // Our instruction payload vocabulary class HelloWorldInstruction extends Assignable {} // Borsh needs a schema describing the payload const helloWorldInstructionSchema = new Map([ [ HelloWorldInstruction, { kind: "struct", fields: [ ["id", "u8"], ["counter", "u32"], ], }, ], ]); // Instruction variant indexes enum InstructionVariant { Greeting = 0, } /** * The expected size of each greeting account. */ const GREETING_SIZE = borsh.serialize( GreetingSchema, new GreetingAccount() ).length; // Create greetings account instruction const greetingAccountKp = new web3.Keypair(); const lamports = await pg.connection.getMinimumBalanceForRentExemption( GREETING_SIZE ); const createGreetingAccountIx = web3.SystemProgram.createAccount({ fromPubkey: pg.wallet.publicKey, lamports, newAccountPubkey: greetingAccountKp.publicKey, programId: pg.PROGRAM_ID, space: GREETING_SIZE, }); const helloIx = new HelloWorldInstruction({ id: InstructionVariant.Greeting, counter: 2, }); // Serialize the payload const helloSerBuf = Buffer.from( serialize(helloWorldInstructionSchema, helloIx) ); // Create greet instruction const greetIx = new web3.TransactionInstruction({ data: helloSerBuf, keys: [ { pubkey: greetingAccountKp.publicKey, isSigner: false, isWritable: true, }, ], programId: pg.PROGRAM_ID, }); // Create transaction and add the instructions const tx = new web3.Transaction(); tx.add(createGreetingAccountIx, greetIx); // Send and confirm the transaction const txHash = await web3.sendAndConfirmTransaction(pg.connection, tx, [ pg.wallet.keypair, greetingAccountKp, ]); console.log(`Use 'solana confirm -v ${txHash}' to see the logs`); // Fetch the greetings account const greetingAccount = await pg.connection.getAccountInfo( greetingAccountKp.publicKey ); // Deserialize the account data const deserializedAccountData = borsh.deserialize( GreetingSchema, GreetingAccount, greetingAccount.data ); console.log( `deserializedAccountData.counter ${deserializedAccountData.counter}` );
  24. Solana合约结构回到我们之前在Playground,这次我们直接用其给的模版创建项目。 里面看到合约代码: Copyuse borsh::{BorshDeserialize, BorshSerialize}; use solana_program::{ account_info::{next_account_info, AccountInfo}, entrypoint, entrypoint::ProgramResult, msg, program_error::ProgramError, pubkey::Pubkey, }; /// Define the type of state stored in accounts #[derive(BorshSerialize, BorshDeserialize, Debug)] pub struct GreetingAccount { /// number of greetings pub counter: u32, } // Declare and export the program's entrypoint entrypoint!(process_instruction); // Program entrypoint's implementation pub fn process_instruction( program_id: &Pubkey, // Public key of the account the hello world program was loaded into accounts: &[AccountInfo], // The account to say hello to _instruction_data: &[u8], // Ignored, all helloworld instructions are hellos ) -> ProgramResult { msg!("Hello World Rust program entrypoint"); // Iterating accounts is safer than indexing let accounts_iter = &mut accounts.iter(); // Get the account to say hello to let account = next_account_info(accounts_iter)?; // The account must be owned by the program in order to modify its data if account.owner != program_id { msg!("Greeted account does not have the correct program id"); return Err(ProgramError::IncorrectProgramId); } // Increment and store the number of times the account has been greeted let mut greeting_account = GreetingAccount::try_from_slice(&account.data.borrow())?; greeting_account.counter += 1; greeting_account.serialize(&mut *account.data.borrow_mut())?; msg!("Greeted {} time(s)!", greeting_account.counter); Ok(()) }合约的整体结构是: Copyuse xxx; entrypoint!(process_instruction); pub fn process_instruction( program_id: &Pubkey, accounts: &[AccountInfo], instruction_data: &[u8], ) -> ProgramResult { ... Ok(()) }最前面的 use 是rust的基本语法,导入相关定义。 这里通过 entrypoint 宏声明了一个函数"process_instruction"为整个合约的入口函数。 在前面的调用我们知道,调用的基本单元是instruction,其定义为: Copy/** * Transaction Instruction class */ export class TransactionInstruction { /** * Public keys to include in this transaction * Boolean represents whether this pubkey needs to sign the transaction */ keys: Array<AccountMeta>; /** * Program Id to execute */ programId: PublicKey; /** * Program input */ data: Buffer; constructor(opts: TransactionInstructionCtorFields); }这里的 programId 指定了和哪个合约交互。而具体执行这个合约的哪个方法呢?就是这里的 entrypoint 来指定的。其原型必须为: Copypub fn process_instruction( program_id: &Pubkey, accounts: &[AccountInfo], instruction_data: &[u8], ) -> ProgramResult { ... Ok(()) }program_id对应了调用Instruction里面的 programId, accounts则对应调用里面的 keys。 instruction_data则为调用指令里面的 data。这样solana在处理的时候,就可以将调用与 合约逻辑一一对上了。 因为函数返回的是一个Result: Copyuse { std::{ result::Result as ResultGeneric, }, }; pub type ProgramResult = ResultGeneric<(), ProgramError>;所以最后返回结果,成功的时候返回 Ok(()).如果错误,需要返回"solana::program_error::ProgramError" 获取Account对象客户端通过RPC调用传递过来的Account对象,在合约里面要怎么去获取呢? Copy// Iterating accounts is safer than indexing let accounts_iter = &mut accounts.iter(); // Get the account to say hello to let account = next_account_info(accounts_iter)?;因为这里,accounts 是一个 &[AccountInfo] AccountInfo的数组,因此我们可以通过 iter()来得到其迭代器,并通过 solana_program::account_info::next_account_info 解析出 solana_program::AccountInfo对象。 Copypub fn next_account_info<'a, 'b, I: Iterator<Item = &'a AccountInfo<'b>>>( iter: &mut I, ) -> Result<I::Item, ProgramError> { iter.next().ok_or(ProgramError::NotEnoughAccountKeys) } pub struct AccountInfo<'a> { /// Public key of the account pub key: &'a Pubkey, /// 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, /// The epoch at which this account will next owe rent pub rent_epoch: Epoch, /// Was the transaction signed by this account's public key? pub is_signer: bool, /// Is the account writable? pub is_writable: bool, /// This account's data contains a loaded program (and is now read-only) pub executable: bool, }这样就可以得到传递过来的最原始的Account对象了。 比如这里我们传递了一个owner为这个合约的account对象,并在其data部分存储了: Copypub struct GreetingAccount { /// number of greetings pub counter: u32, }这个结构体的数据,作为计数器来使用。 所以在获得该对象后,可以进行Account信息的相关检查: Copyif account.owner != program_id { msg!("Greeted account does not have the correct program id"); return Err(ProgramError::IncorrectProgramId); }如果Account里面的owner不是我们的合约,直接返回出错。并且指定了错误码。 Account数据存储上面我们读取了传递的存储单元Account的基础信息,那么具体存储的值是如何操作的呢? Copylet mut greeting_account = GreetingAccount::try_from_slice(&account.data.borrow())?;这里因为我们在定义GreetingAccount的使用了 Copy#[derive(BorshSerialize, BorshDeserialize, Debug)] pub struct GreetingAccount {Borsh的默认实现,所以可以直接通过try_from_slice方法,将Account中的data借用出来做解析。 Borsh 是一个序列化标准,其有多个语言的实现,比如我们这里的Rust和客户端可以用 的Javascript。具体逻辑类似我们在前端web3.js访问时候的buffer的定义。我们可以忽略其具体实现,直接进行使用。 当然这里我们还可以使用其他序列化工具,比如Anchor。 反序列化这里的data部分后,就可以得到 GreetingAccount对象了。 Copygreeting_account.counter += 1; greeting_account.serialize(&mut *account.data.borrow_mut())?;这里先修改 GreetingAccount对象,然后再将其序列化回Account的data部分中。实际就是将 "greeting_account" 序列化成二进制数据,然后再填入account.data部分的。 因为我们在前端传递的时候,给这个Account的isWritable是true,所以我们合约中修改了Account.data 部分在合约执行结束时,就会修改链上的相关数据。 客户端访问这里贴上课上客户端访问代码: Copy// No imports needed: web3, borsh, pg and more are globally available /** * The state of a greeting account managed by the hello world program */ class GreetingAccount { counter = 0; constructor(fields: { counter: number } | undefined = undefined) { if (fields) { this.counter = fields.counter; } } } /** * Borsh schema definition for greeting accounts */ const GreetingSchema = new Map([ [GreetingAccount, { kind: "struct", fields: [["counter", "u32"]] }], ]); /** * The expected size of each greeting account. */ const GREETING_SIZE = borsh.serialize( GreetingSchema, new GreetingAccount() ).length; // Create greetings account instruction const greetingAccountKp = new web3.Keypair(); const lamports = await pg.connection.getMinimumBalanceForRentExemption( GREETING_SIZE ); const createGreetingAccountIx = web3.SystemProgram.createAccount({ fromPubkey: pg.wallet.publicKey, lamports, newAccountPubkey: greetingAccountKp.publicKey, programId: pg.PROGRAM_ID, space: GREETING_SIZE, }); // Create greet instruction const greetIx = new web3.TransactionInstruction({ keys: [ { pubkey: greetingAccountKp.publicKey, isSigner: false, isWritable: true, }, ], programId: pg.PROGRAM_ID, }); // Create transaction and add the instructions const tx = new web3.Transaction(); tx.add(createGreetingAccountIx, greetIx); // Send and confirm the transaction const txHash = await web3.sendAndConfirmTransaction(pg.connection, tx, [ pg.wallet.keypair, greetingAccountKp, ]); console.log(`Use 'solana confirm -v ${txHash}' to see the logs`); // Fetch the greetings account const greetingAccount = await pg.connection.getAccountInfo( greetingAccountKp.publicKey ); // Deserialize the account data const deserializedAccountData = borsh.deserialize( GreetingSchema, GreetingAccount, greetingAccount.data ); console.log( `deserializedAccountData.counter :${deserializedAccountData.counter}` );
  25. HelloWorldSolana为了初学者可以快速入门,提供了一个 Playground 服务。 可以在不需要本地环境的情况下,进行DApp的开发。 创建项目在界面中,我们点击 "Create a New Project" 创建完项目后。在Explorer里可以看到文件列表,主要有: Copysrc/ lib.rs client/ clinet.ts test/ native.test.ts 对应的位置有 "Build":构建合约 "Run" : 运行客户端端,调试合约。 "Test": 执行测试代码 链接钱包在界面的左下角有个"unconnect"的提示,点击后,弹出: 在这里选择我们之前生成的"~/.config/solana/id.json"秘钥文件。导入后,可以看到钱包详情 开发合约打开这里的lib.rs,在里面贴上: Copyuse solana_program::{ account_info::AccountInfo, entrypoint, entrypoint::ProgramResult, pubkey::Pubkey, msg, }; // Declare and export the program's entrypoint entrypoint!(process_instruction); // Program entrypoint's implementation pub fn process_instruction( _program_id: &Pubkey, // Public key of the account the hello world program was loaded into _accounts: &[AccountInfo], // The account to say hello to _instruction_data: &[u8], // Ignored, all helloworld instructions are hellos ) -> ProgramResult { msg!("Hello World Rust program entrypoint"); Ok(()) }然后点击"Build",在下面的界面可以看到build的结果: CopyBuilding... Build successful. Completed in 0.60s.然后点击左侧的 锤子+扳手的图标,点击"Deploy": 发布成功,可以在界面上看到发布后的合约地址。在发布过一次后,这里的"Deploy"就会变成"Update" 如果我们的合约有修改,只要在这里构建后点击"Update"就可以进行更新了。 客户端开发回到Explorer界面,打开client.ts,在里面贴上: Copy// Client console.log("My address:", pg.wallet.publicKey.toString()); const balance = await pg.connection.getBalance(pg.wallet.publicKey); console.log(`My balance: ${balance / web3.LAMPORTS_PER_SOL} SOL`); // create an empty transaction const transaction = new web3.Transaction(); // add a hello world program instruction to the transaction transaction.add( new web3.TransactionInstruction({ keys: [], programId: new web3.PublicKey(pg.PROGRAM_ID), }), ); console.log("Sending transaction..."); const txHash = await web3.sendAndConfirmTransaction( pg.connection, transaction, [pg.wallet.keypair], ); console.log("Transaction sent with hash:", txHash);这里,不需要过多的import,IDE已经帮忙们做了import。可以直接使用web3。其中"pg.wallet" 就是我们的钱包,其publicKey属性就是钱包的地址。而pg.connection就如同我们用web3创建 的connection对象,这里共用playground的设置里面的RPC地址。 pb.PROGRAM_ID 就是我们要刚刚构建并发布的合约的地址。 点击"Run"我们可以在日志里面看到: CopyRunning client... client.ts: My address: 5pWae6RxD3zrYzBmPTMYo1LZ5vef3vfWH6iV3s8n6ZRG My balance: 4.27388232 SOL Sending transaction... Transaction sent with hash: 3XcaF6zpXthBQ2mih7DdVBuvwXi7L6Wy4rCHzgwHEaNsqMDNRGC5yppG9xKP9g9hYfT6wPEw127mxgYWBTouS5gz打开solana浏览器,贴上这里的hash地址。可以看到。在日志部分,执行了我们前面合约代码里面的"HelloWorld"。

Configure browser push notifications

Chrome (Android)
  1. Tap the lock icon next to the address bar.
  2. Tap Permissions → Notifications.
  3. Adjust your preference.
Chrome (Desktop)
  1. Click the padlock icon in the address bar.
  2. Select Site settings.
  3. Find Notifications and adjust your preference.