本文介绍了 Stacks 区块链平台,它是一个 Bitcoin 的二层网络,采用 PoX 共识机制,支持智能合约。文章详细描述了 Stacks 的特性,包括使用 OP_RETURN 锚定 Bitcoin、原生币 STX 的奖励减半机制、签名算法、PoX 共识机制、P2PKH 地址生成、交易结构、Post Conditions、智能合约以及代币标准 SIP10 和 STX20。
Created: <2024-04-13 Sat>
Last updated: <2024-04-20 Sat>
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
Stacks 会把它的区块信息写入到 Bitcoin 网络上。
比如,Stacks 的区块 146238 的信息会锚定到 Bitcoin 的交易 01db6ca33b6d6003f1ce6c6592a3bf6f8f69acbc234647b497399bc453809ab4 中。具体来说,就是通过 OP_RETURN 往 Bitcoin 中写入 Stacks 的区块信息,所写入数据的详细的定义可以参考 SIP-001 。
Stacks 的原生币是 STX,最小单位的原生币为 micro-STX,它们的换算关系为:1 STX = 1000000 micro-STX
8 字节的整数足够保存 STX 数量了。在 Stacks 的交易结构中,转帐数量就是占用 8 字节。
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 的发行总量。
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)。
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
下面介绍一下 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 地址
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,如转移金额,目标地址,转帐备注等 |
Table 1: Stacks 的交易结构
Stacks 支持两种 Authorization Type:
Standard authorization,表示自己付 Gas;
Sponsored authorizations,表示别人帮忙付 Gas。
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
下面是使用 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
}
Stacks 中的手续费是用户估计出一个值后设置是在交易的 Authorization 结构中。关于手续费估计可以参考 https://docs.stacks.co/stacks-101/network#fees 。
Post Conditions 是 Stacks 中比较特别的地方。
使用 Post Conditions,可以避免合约产生预期外的行为,比如用户可以确保自己的资产不会由于合约代码 Bug,或者恶意合约代码而遭受损失。
Post Conditions 的例子(这个例子来源于 Understanding Stacks Post Conditions):假设用户想花 20 STX 在 NFT 交易市场中购买一个 NFT。那么用户可以指定下面的 Post Conditions 来保障自己的权益:
不超过 20 STX;
收到一个指定类型的 NFT。
如果合约执行完后,这两个 Post Conditions 没有满足,则整个交易会失败,用户仅仅损失一些 Gas 费。
Stacks 中采用 Clarity 作为智能合约的开发语言,这是一种 Lisp 语法风格的语言。 Clarity 没有编译器,部署合约时直接把源码部署到链上,任何人都可以查看合约源码,更加透明。 这是 Stacks 上的 USDT,链上可以直接看到它的源码。
Stacks 中智能合约采用 [contractAddress].[contractName]
来唯一标记,其中 contractAddress 是部署者的地址,而 contractName 是部署时指定的名称。 比如 Stacks 上的 USDT 为 SP3K8BC0PPEVCV7NZ6QSRWPQ2JE9E5B6N3PA0KBR9.token-susdt,其中 SP3K8BC0PPEVCV7NZ6QSRWPQ2JE9E5B6N3PA0KBR9 就是 USDT 部署者的地址,token-susdt 是部署时指定的名称。
使用 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
下面是使用 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
SIP10 和 STX20 都是 Stacks 上的代币标准。 SIP10 类似于 Ethereum 的 ERC20,而 STX20 类似于 Bitcoin 的 BRC20。
比如,Stacks 上的 USDT 采用的是 SIP10 标准。
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
- 本文转载自: aandds.com/blog/stacks.h... , 如有侵权请联系管理员删除。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!