Stacks (Blockchain Platform)
Table of Contents
1. 简介
Stacks 是一个 Bitcoin 二层网络,采用 PoX (Proof of Transfer) 共识机制,Stacks 支持智能合约。
Stacks 的名称来源于它的特性的简写:
- S Secured by the entire hash power of Bitcoin (Bitcoin finality).
- T Trust-minimized Bitcoin peg mechanism; write to Bitcoin.
- A Atomic BTC swaps and assets owned by BTC addresses.
- C Clarity language for safe, decidable smart contracts.
- K Knowledge of full Bitcoin state; read from Bitcoin.
- S Scalable, fast transactions that settle on Bitcoin
1.1. 使用 OP_RETURN 锚定
Stacks 会把它的区块信息写入到 Bitcoin 网络上。
比如,Stacks 的区块 146238 的信息会锚定到 Bitcoin 的交易 01db6ca33b6d6003f1ce6c6592a3bf6f8f69acbc234647b497399bc453809ab4 中。具体来说,就是通过 OP_RETURN 往 Bitcoin 中写入 Stacks 的区块信息,所写入数据的详细的定义可以参考 SIP-001 。
1.2. 原生币 STX
Stacks 的原生币是 STX,最小单位的原生币为 micro-STX,它们的换算关系为:1 STX = 1000000 micro-STX
8 字节的整数足够保存 STX 数量了。在 Stacks 的交易结构中,转帐数量就是占用 8 字节。
1.2.1. 奖励减半机制
Stacks 对 miner 的奖励实行下面减半机制:
- 1000 STX per block are released in the first 4 years of mining
- 500 STX per block are released during the following 4 years
- 250 STX per block are released during the following 4 years
- 125 STX per block are released from then on indefinitely.
从上可知, Stacks 中没有限制 STX 的发行总量。
1.3. 签名算法
Stacks 和 Bitcoin 一样都是采用 ECDSA 签名,背后的椭圆曲线都是 secp256k1。
Stacks 的 JavaScript SDK stacks.js 中返回的 stxPrivateKey 是 compressed 私钥。 compressed 私钥和普通私钥的区别在于 compressed 私钥最后多了一个字节 0x01。compressed 私钥表示在推导地址时使用 compressed 的公钥:
53e804fec042f8fa21c6ab9e8c00d2c879aed9c8737601a8888602848471869a01 # 这是 compressed private key 例子,表示推导地址时使用 compressed 的公钥 53e804fec042f8fa21c6ab9e8c00d2c879aed9c8737601a8888602848471869a # 这是 uncompressed private key 例子,表示推导地址时使用 uncompressed 的公钥
在通过参数 senderKey 指定私钥时,如果您的 sender 地址时根据 compressed 公钥生成的,那么对应的 senderKey 也要指定为 compressed 私钥(即最后多一个字节 0x01)。
2. PoX (Proof of Transfer)
Stacks 采用了 PoX (Proof of Transfer) 共识机制。可以认为 PoX 是 PoB (Proof of Burn) 机制的一种扩展。
所谓 Proof of Burn 就是:为了获得一种新的加密货币,用户必须销毁另一种锚定的加密货币。比如在 CounterParty 区块链中,用户需要通过销毁 BTC 来获得 XCP。这里销毁 BTC 的意思是把 BTC 发送到一个没有知道对应私钥的地址中,比如地址 1CounterpartyXXXXXXXXXXXXXXXUWLpVr。另一个区块链项目 Slimcoin 也采用了 Proof of Burn,不过这个项目已经不再活跃了。
而 Stacks 的 PoX 和 PoB 的不同之处在于: Stacks 的 miner 并不会直接把锚定货币(如 BTC)销毁,而是把 BTC 发送给其它的参与节点。miner 获得出块权时得到 STX 奖励。 如图 1 所示。
Figure 1: Stacks PoX
3. P2PKH 地址生成
下面介绍一下 Stacks 的 P2PKH 地址的推导过程:
第一步,从助记词推导出 compressed 公钥的 Hash:
mnemonic: face ankle save vote kiwi still salmon private physical tent impulse clown blind initial addict feel outdoor during viable close gas frown sure unveil | | path: m/44'/5757'/0'/0/0 | v private key: 53e804fec042f8fa21c6ab9e8c00d2c879aed9c8737601a8888602848471869a | v compressed public key: 03a1328ef6068af52aea4c09f1a31627017acd2ea15a3e23df2760ff1457f77165 | v sha256: 6faf60f850af648334dd882109d162771648718e724d299097d04bce351e1c32 | v ripemd160_hash: 89480e8142160c42ad05b5aea9388abcba56e51c
第二步,计算 checksum。规则是对上面结果前面加上地址版本号(主网为 22,即 0x16;测试网为 26,即 0x1a),然后计算两次 SHA256:
89480e8142160c42ad05b5aea9388abcba56e51c | 最前面加上地址版本号(主网为 22,即 0x16;测试网为 26,即 0x1a),这里使用测试网的 0x1a v 1a89480e8142160c42ad05b5aea9388abcba56e51c | 第 1 次 SHA256 v e652964c3665a4fb1b7d5ff1a402d189a7f093bff4e11dc65f94868f877681f5 | 第 2 次 SHA256 v 1c6e62ea3a1de4756ec3e69346f642818e6e0a3ecac328cef626a8bf69209f9d | 取前 4 字节 v 1c6e62ea
第三步,用格式 [ripemd160_hash][checksum] 构造出下面结果:
89480e8142160c42ad05b5aea9388abcba56e51c1c6e62ea
第四步,对上面结果进行 c32encode 编码(它是 Base32 编码的一种变种)得到:
24MG3M188B0RGND0PTTXA9RHAYBMNQ53GE6WRQA
第五步,对上面结果前面增加两个字符: 如果是 Stacks 主网 P2PKH 地址,则前面加上 SP
,如果是 Stacks 测试网 P2PKH 地址,则前面加上 ST
。 这个例子中,由于计算 checksum 时选择的版本号是 0x1a,所以是测试网地址,所以前面需要增加 ST
,即最终得到的 Stacks 测试网 P2PKH 地址为:
ST24MG3M188B0RGND0PTTXA9RHAYBMNQ53GE6WRQA
注:根据前面介绍的 P2PKH 地址产生过程,我们可知:同一个助记词,推导出来的 Stacks 测试网地址和主网地址中间大部分内容是相同的,只有两处不同:
- 第 2 个字符不同。主网 P2PKH 地址第 2 个字符是 P(它其实就是 Crockford's Base32 字符集的第 22 个字符),而测试网 P2PKH 地址第 2 个字符是 T(它其实就是 Crockford's Base32 字符集的第 26 个字符);
- 最后几个字符不同,这是由于不同的 checksum 导致的,而不同的 checksum 是由于使用不同的地址版本号(主网是 22,测试网是 26)导致。
对于前面的助记词,如果同时推导测试网和主网 P2PKH 地址的话,分别为:
ST24MG3M188B0RGND0PTTXA9RHAYBMNQ53GE6WRQA # Stacks 测试网 P2PKH 地址 SP24MG3M188B0RGND0PTTXA9RHAYBMNQ53H0A0YY5 # Stacks 主网 P2PKH 地址
4. 交易结构
Stacks 的交易结构如表 1 所示,参考 Transaction Encoding。
Fields | Description |
---|---|
Version number | Network version. 0x80 for testnet, 0x0 for mainnet |
Chain ID | Chain instance ID. 0x80000000 for testnet, 0x00000001 for mainnet |
Authorization | 包含认证类型(Authorization Type),地址 nonce 值,手续费,ECDSA 签名等信息 |
Anchor Mode/Block Type | 有 3 个选择:"onChainOnly" (Anchor Blocks), "offChainOnly" (Microblocks), "any" |
Post-conditions | List of post-conditions,后面会介绍 |
Payload | Transaction type and variable-length payload,如转移金额,目标地址,转帐备注等 |
4.1. 认证类型(自己付 Gas、别人帮忙付 Gas)
Stacks 支持两种 Authorization Type:
- Standard authorization,表示自己付 Gas;
- Sponsored authorizations,表示别人帮忙付 Gas。
4.2. 5 种交易类型(5 种 Payload)
Stacks 有 5 种交易类型,即 5 种不同的 Transaction Payloads:
- Type-0: Transferring an Asset
- Type-1: Instantiating a Smart Contract
- Type-2: Calling an Existing Smart Contract
- Type-3: Punishing an Equivocating Stacks Leader
- Type-4: Coinbase
4.3. 转账交易实例(stacks.js)
下面是使用 stacks.js 进行原生币转账的例子(参考:https://docs.hiro.so/stacks-blockchain-api/feature-guides/transactions#stacks-token-transfer ):
import { makeSTXTokenTransfer } from '@stacks/transactions'; import { StacksTestnet, StacksMainnet } from '@stacks/network'; const BigNum = require('bn.js'); const txOptions = { recipient: 'SP3FGQ8Z7JY9BWYZ5WM53E0M9NK7WHJF0691NZ159', amount: new BigNum(12345), senderKey: 'b244296d5907de9864c0b0d51f98a13c52890be0404e83f273144cd5b9960eed01', network: new StacksMainnet(), // for testnet, use `StacksTestnet()` memo: 'test memo', nonce: new BigNum(0), // set a nonce manually if you don't want builder to fetch from a Stacks node fee: new BigNum(200), // set a tx fee if you don't want the builder to estimate }; const transaction = await makeSTXTokenTransfer(txOptions);
使用 RPC v2/accounts 可以获得某地址的余额和 nonce 值,比如:
$ curl 'https://api.mainnet.hiro.so/v2/accounts/SP3FGQ8Z7JY9BWYZ5WM53E0M9NK7WHJF0691NZ159?proof=0' # 获得地址的余额和 nonce 值 { "balance": "0x00000000000000000000000000016d0b", "locked": "0x00000000000000000000000000000000", "unlock_height": 0, "nonce": 0 }
4.4. 手续费
Stacks 中的手续费是用户估计出一个值后设置是在交易的 Authorization 结构中。关于手续费估计可以参考 https://docs.stacks.co/stacks-101/network#fees 。
5. Post Conditions
Post Conditions 是 Stacks 中比较特别的地方。
使用 Post Conditions,可以避免合约产生预期外的行为,比如用户可以确保自己的资产不会由于合约代码 Bug,或者恶意合约代码而遭受损失。
Post Conditions 的例子(这个例子来源于 Understanding Stacks Post Conditions):假设用户想花 20 STX 在 NFT 交易市场中购买一个 NFT。那么用户可以指定下面的 Post Conditions 来保障自己的权益:
- 不超过 20 STX;
- 收到一个指定类型的 NFT。
如果合约执行完后,这两个 Post Conditions 没有满足,则整个交易会失败,用户仅仅损失一些 Gas 费。
6. 智能合约
Stacks 中采用 Clarity 作为智能合约的开发语言,这是一种 Lisp 语法风格的语言。 Clarity 没有编译器,部署合约时直接把源码部署到链上,任何人都可以查看合约源码,更加透明。 这是 Stacks 上的 USDT,链上可以直接看到它的源码。
Stacks 中智能合约采用 [contractAddress].[contractName]
来唯一标记,其中 contractAddress 是部署者的地址,而 contractName 是部署时指定的名称。 比如 Stacks 上的 USDT 为 SP3K8BC0PPEVCV7NZ6QSRWPQ2JE9E5B6N3PA0KBR9.token-susdt,其中 SP3K8BC0PPEVCV7NZ6QSRWPQ2JE9E5B6N3PA0KBR9 就是 USDT 部署者的地址,token-susdt 是部署时指定的名称。
6.1. 部署合约(stacks.js)
使用 stacks.js 部署合约很简单。首先准备好合约源码文件(如 /path/to/contract.clar),再:
import { makeContractDeploy } from '@stacks/transactions'; import { StacksTestnet, StacksMainnet } from '@stacks/network'; const BigNum = require('bn.js'); const txOptions = { contractName: 'contract_name', codeBody: fs.readFileSync('/path/to/contract.clar').toString(), senderKey: 'b244296d5907de9864c0b0d51f98a13c52890be0404e83f273144cd5b9960eed01', network: new StacksMainnet(), // for testnet, use `StacksTestnet()` }; const transaction = await makeContractDeploy(txOptions);
参考:https://docs.hiro.so/stacks-blockchain-api/feature-guides/transactions#smart-contract-deployment
6.2. 调用合约(stacks.js)
下面是使用 stacks.js 调用合约方法的例子:
import { makeContractCall, BufferCV } from '@stacks/transactions'; import { StacksTestnet, StacksMainnet } from '@stacks/network'; const BigNum = require('bn.js'); const txOptions = { contractAddress: 'SPBMRFRPPGCDE3F384WCJPK8PQJGZ8K9QKK7F59X', contractName: 'contract_name', functionName: 'contract_function', functionArgs: [bufferCVFromString('foo')], senderKey: 'b244296d5907de9864c0b0d51f98a13c52890be0404e83f273144cd5b9960eed01', // attempt to fetch this contracts interface and validate the provided functionArgs validateWithAbi: true, network: new StacksMainnet(), // for testnet, use `StacksTestnet()` }; const transaction = await makeContractCall(txOptions);
参考:https://docs.hiro.so/stacks-blockchain-api/feature-guides/transactions#smart-contract-function-call
7. 代币标准(SIP10, STX20)
SIP10 和 STX20 都是 Stacks 上的代币标准。 SIP10 类似于 Ethereum 的 ERC20,而 STX20 类似于 Bitcoin 的 BRC20。
比如,Stacks 上的 USDT 采用的是 SIP10 标准。
7.1. SIP10(类似于 ERC20)
SIP10 规定了代币合约需要实现下面方法:
(define-trait sip-010-trait ( ;; Transfer from the caller to a new principal (transfer (uint principal principal (optional (buff 34))) (response bool uint)) ;; the human readable name of the token (get-name () (response (string-ascii 32) uint)) ;; the ticker symbol, or empty if none (get-symbol () (response (string-ascii 32) uint)) ;; the number of decimals used, e.g. 6 would mean 1_000_000 represents 1 token (get-decimals () (response uint uint)) ;; the balance of the passed principal (get-balance (principal) (response uint uint)) ;; the current total supply (which does not need to be a constant) (get-total-supply () (response uint uint)) ;; an optional URI that represents metadata of this token (get-token-uri () (response (optional (string-utf8 256)) uint)) ) )
这里是 SIP10 合约的一个例子:https://github.com/hstove/stacks-fungible-token/blob/main/contracts/example-token.clar