跳转到帖子
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论坛

管理员
  • 注册日期

  • 上次访问

  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

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.