Stacks Raw Tx Breakdown
Table of Contents
1. Stacks 交易
在 Stacks 区块链中,通过 RPC /v2/transactions 可以把签名后的 Tx 提交到链上。本文介绍签名打包 Stacks 交易的细节。
2. Tx 解析实例:原生币转帐
测试网上交易 0x605c958a9ddcf38ac36f0f6f44c02d3d3cf5602ef3d3ad517e4756bc2d1c0883 的功能是原生币 STX 转帐:
ST24MG3M188B0RGND0PTTXA9RHAYBMNQ53GE6WRQA ---- 0.000123 STX ---> STN3035BW0HRKFJE3E4NHN2867TZN63N9XCGSM1Z
这个交易是通过下面方式提交到 Stacks 测试网的(注:要在 JS 代码中提交交易上链可直接使用 stacks.js 库中的 broadcastTransaction 函数,这里仅仅为了演示 RPC 的使用):
$ xxd raw_tx.bin # 查看交易原始文件 raw_tx.bin 的内容 00000000: 8080 0000 0004 0089 480e 8142 160c 42ad ........H..B..B. 00000010: 05b5 aea9 388a bcba 56e5 1c00 0000 0000 ....8...V....... 00000020: 0000 0100 0000 0000 0008 b900 00fa c7b6 ................ 00000030: f6a9 7d51 1d89 d72e 1fa4 9662 1c6d 95ee ..}Q.......b.m.. 00000040: 77d2 12a0 57a2 3669 adc7 6ff2 5d69 f93d w...W.6i..o.]i.= 00000050: 90c5 7d16 c360 8f7a 603f 3f55 880d db25 ..}..`.z`??U...% 00000060: 6034 72ca 8476 f2cc 98aa ccb6 da03 0200 `4r..v.......... 00000070: 0000 0000 051a 2a30 0cab e023 89be 4e1b ......*0...#..N. 00000080: 8958 d448 31f5 fa98 754f 0000 0000 0000 .X.H1...uO...... 00000090: 007b 7465 7374 206d 656d 6f00 0000 0000 .{test memo..... 000000a0: 0000 0000 0000 0000 0000 0000 0000 0000 ................ 000000b0: 0000 0000 .... $ curl --verbose -L \ -X POST \ -H 'Content-Type: application/octet-stream' \ --data-binary @raw_tx.bin \ 'https://api.testnet.hiro.so/v2/transactions' # 提交交易到链上 "605c958a9ddcf38ac36f0f6f44c02d3d3cf5602ef3d3ad517e4756bc2d1c0883"
其中交易原始文件 raw_tx.bin 是通过节 4.1 中的代码生成的,我们把它的内容转换为 Hex String 形式:
8080000000040089480e8142160c42ad05b5aea9388abcba56e51c000000000000000100000000000008b90000fac7b6f6a97d511d89d72e1fa496621c6d95ee77d212a057a23669adc76ff25d69f93d90c57d16c3608f7a603f3f55880ddb25603472ca8476f2cc98aaccb6da03020000000000051a2a300cabe02389be4e1b8958d44831f5fa98754f000000000000007b74657374206d656d6f00000000000000000000000000000000000000000000000000
上面的数据可以分解为下面这种更清晰的表达方式:
version number | 80 | 主网为 00,测试网为 80 | ||
chain id | 80000000 | 主网为 00000001,测试网为 80000000 | ||
authorization |
auth type | 04 | 04 表示 standard,05 表示 sponsored | |
hash mode | 00 | 00/01/02/03 分别表示 P2PKH/P2SH/P2WPKH-P2SH/P2WSH-P2SH | ||
public key hash | 89480e8142160c42ad05b5aea9388abcba56e51c | 发送者公钥的哈希 sha256(ripemd160(pubkey)) | ||
nonce | 0000000000000001 | 发送者地址关联的 nonce 值 | ||
fee | 00000000000008b9 | 手续费。这里是 2233 micro-STX | ||
pub key encoding | 00 | 00 表示 compressed 公钥,01 表示 uncompressed 公钥 | ||
ECDSA sign |
v | 00 | recovery ID | |
r | fac7b6f6a97d511d89d72e1fa496621c6d95ee77d212a057a23669adc76ff25d | 签名 r 值 | ||
s | 69f93d90c57d16c3608f7a603f3f55880ddb25603472ca8476f2cc98aaccb6da | 签名 s 值 | ||
anchor mode | 03 | 01/02/03 分别表示 anchored block/microblock/leader can choose | ||
post-condition |
post-condition mode | 02 | 可选值有 01/02,这里不介绍 | |
post-condition length | 00000000 | post-condition 长度 | ||
tx payload |
payload type ID | 00 | 可选值有 00/01/02/03/04/05,其中 00 表示 token-transfer | |
recipient type | 05 | 接收者类型。05 表示 recipient address,06 表示 contract recipient | ||
address version | 1a | 地址版本。1a 表示测试网 P2PKH,16 表示主网 P2PKH | ||
20-byte hash | 2a300cabe02389be4e1b8958d44831f5fa98754f | 接收者公钥哈希,从地址 STN3035BW0HRKFJE3E4NHN2867TZN63N9XCGSM1Z 解码得到 | ||
transfer amount | 000000000000007b | 转帐金额。这里是 123 micro-STX | ||
memo (34 bytes) | 74657374206d656d6f00000000000000000000000000000000000000000000000000 | 转帐备注。这里是 test memo 的 hexadecimal 编码。如 74 编码字符 t |
2.1. 签名计算
上一节的例子中,ECDSA 的签名 rs 值(即 fac7b6f6a97d511d89d72e1fa496621c6d95ee77d212a057a23669adc76ff25d69f93d90c57d16c3608f7a603f3f55880ddb25603472ca8476f2cc98aaccb6da)是如何计算的呢?
首先,要计算“待签名数据”,其过程如下:
把 nonce 值、fee 以及签名数据全部设置为 0,即得到:
8080000000040089480e8142160c42ad05b5aea9388abcba56e51c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003020000000000051a2a300cabe02389be4e1b8958d44831f5fa98754f000000000000007b74657374206d656d6f00000000000000000000000000000000000000000000000000
- 把上面结果计算 SHA512_256 哈希,即得到:855f926c9893082213fd2d8bdd100dbbe98422b918959579a29a09dc290d6dec
- 上面数据的后面再加上 [auth type][fee][nonce],即得到“待签名数据”:855f926c9893082213fd2d8bdd100dbbe98422b918959579a29a09dc290d6dec0400000000000008b90000000000000001
然后,进行 ECDSA 签名,对“待签名数据”所使用的哈希函数为 SHA512_256(它是 SHA512 的截断版本),这一步的细节可以参考节 4.2,最终可以得到结果:
r=fac7b6f6a97d511d89d72e1fa496621c6d95ee77d212a057a23669adc76ff25d s=69f93d90c57d16c3608f7a603f3f55880ddb25603472ca8476f2cc98aaccb6da
3. Tx 解析实例:SIP10 代币转帐
合约 STN3035BW0HRKFJE3E4NHN2867TZN63N9XCGSM1Z.ambitious-olive-capybara 是 Stacks 测试网上的 SIP10 代币,交易 0x3bd122ed6e2c8db4da436a9ae3a36ce465337ccd383a9a546193eaa81f6682a5 的功能就是对这个 SIP10 代币进行转账。
交易 0x3bd122ed6e2c8db4da436a9ae3a36ce465337ccd383a9a546193eaa81f6682a5 的原始 Tx 数据(它由节 4.3 的代码产生)的 Hex String 形式为:
8080000000040089480e8142160c42ad05b5aea9388abcba56e51c000000000000000200000000000001050000d58550986a2040495ca86b2ca7c0fd58e9b62f53f27848bf6a07c1568d769bdb6e9b4ad52cb2f5e390ad8d7d0b1f08a03fbb94520eaf98c9736960c080849b4d030200000000021a2a300cabe02389be4e1b8958d44831f5fa98754f18616d626974696f75732d6f6c6976652d6361707962617261087472616e73666572000000040100000000000000000000000000000078051a89480e8142160c42ad05b5aea9388abcba56e51c051a2a300cabe02389be4e1b8958d44831f5fa98754f0a02000000137369703130207472616e736665722074657374
上面的数据可以分解为下面这种更清晰的表达方式:
version number | 80 | 主网为 00,测试网为 80 | |||
chain id | 80000000 | 主网为 00000001,测试网为 80000000 | |||
authorization |
auth type | 04 | 04 表示 standard,05 表示 sponsored | ||
hash mode | 00 | 00/01/02/03 分别表示 P2PKH/P2SH/P2WPKH-P2SH/P2WSH-P2SH | |||
public key hash | 89480e8142160c42ad05b5aea9388abcba56e51c | 发送者公钥的哈希 sha256(ripemd160(pubkey)) | |||
nonce | 0000000000000002 | 发送者地址关联的 nonce 值 | |||
fee | 0000000000000105 | 手续费。这里是 261 micro-STX | |||
pub key encoding | 00 | 00 表示 compressed 公钥,01 表示 uncompressed 公钥 | |||
ECDSA sign |
v | 00 | recovery ID | ||
r | d58550986a2040495ca86b2ca7c0fd58e9b62f53f27848bf6a07c1568d769bdb | 签名 r 值 | |||
s | 6e9b4ad52cb2f5e390ad8d7d0b1f08a03fbb94520eaf98c9736960c080849b4d | 签名 s 值 | |||
anchor mode | 03 | 01/02/03 分别表示 anchored block/microblock/leader can choose | |||
post-condition |
post-condition mode | 02 | 可选值有 01/02,这里不介绍 | ||
post-condition length | 00000000 | post-condition 长度 | |||
tx payload |
payload type ID | 02 | 可选值有 00/01/02/03/04/05,其中 02 表示调用合约函数 | ||
address version | 1a | 地址版本。1a 表示测试网 P2PKH,16 表示主网 P2PKH | |||
20-byte hash | 2a300cabe02389be4e1b8958d44831f5fa98754f | 合约部署者的公钥哈希,从合约部署者地址中可以解码得到 | |||
contract name length | 18 | 合约名称的长度,这里是 0x18(24)字节 | |||
contract name | 616d626974696f75732d6f6c6976652d6361707962617261 | 合约名称 ambitious-olive-capybara 的编码 | |||
function name length | 08 | 合约函数名称的长度 | |||
function name | 7472616e73666572 | 合约函数名称 transfer 的编码 | |||
arguments length | 00000004 | 合约函数的参数个数,这里是 4 个参数 | |||
arg 0 |
type | 01 | 01 表示 Clarity type: 128-bit unsigned integer | ||
value | 00000000000000000000000000000078 | 表示转帐数量 120 | |||
arg 1 |
type | 05 | 05 表示 Clarity type: standard principal | ||
addr version | 1a | 1a 表示测试网 P2PKH,16 表示主网 P2PKH | |||
20-byte hash | 89480e8142160c42ad05b5aea9388abcba56e51c | 代币发出方的公钥哈希 | |||
arg 2 |
type | 05 | 05 表示 Clarity type: standard principal | ||
addr version | 1a | 1a 表示测试网 P2PKH,16 表示主网 P2PKH | |||
20-byte hash | 2a300cabe02389be4e1b8958d44831f5fa98754f | 代币接收方的公钥哈希 | |||
arg 3 |
type | 0a | 0a 表示 Clarity type: Some option | ||
type | 02 | 02 表示 Clarity type: buffer | |||
buffer length | 00000013 | 转帐备注长度 | |||
value | 7369703130207472616e736665722074657374 | 转帐备注 sip10 transfer test 的编码 |
关于函数参数的 Clarity type,可以参考:https://github.com/stacksgov/sips/blob/main/sips/sip-005/sip-005-blocks-and-transactions.md#clarity-value-representation
4. 附录
4.1. 构造转帐交易(JS)
下面 JS 代码可以构造节 2 中所演示的交易:
import {bytesToHex, hexToBytes, intToBigInt, intToBytes} from '@stacks/common'; import {StacksTestnet, StacksMainnet} from "@stacks/network"; import { AddressHashMode, AddressVersion, AnchorMode, AuthType, createMemoString, createSingleSigSpendingCondition, createStacksPrivateKey, estimateTransactionFeeWithFallback, getNonce, getPublicKey, PayloadType, principalCV, publicKeyToString, signWithKey, StacksMessageType, StacksTransaction, TransactionVersion, broadcastTransaction, makeSTXTokenTransfer } from "@stacks/transactions"; import {c32address} from "c32check"; import {sha512_256} from "@noble/hashes/sha512"; import {writeFile} from 'node:fs'; // 没有必要自己实现 buildTx 函数,因为 stacks.js 中已经有了 makeSTXTokenTransfer 可以转帐 STX // 这里仅仅是为了演示交易的具体构造过程才自己实现 async function buildTx(network, senderKey, recipient, amount, memo) { const publicKey = publicKeyToString(getPublicKey(createStacksPrivateKey(senderKey))) console.log("publicKey", publicKey) // 03a1328ef6068af52aea4c09f1a31627017acd2ea15a3e23df2760ff1457f77165 const spendingCondition = createSingleSigSpendingCondition( AddressHashMode.SerializeP2PKH, publicKey, 0, 0 ); const authorization = { authType: AuthType.Standard, spendingCondition }; const payload = { type: StacksMessageType.Payload, payloadType: PayloadType.TokenTransfer, recipient: principalCV(recipient), amount: intToBigInt(amount, false), memo: createMemoString(memo), }; const transaction = new StacksTransaction( network.version, authorization, payload, undefined, // no post conditions on STX transfers (see SIP-005) undefined, // no post conditions on STX transfers (see SIP-005) AnchorMode.Any, network.chainId ); // Estimate the fee // const fee = await estimateTransactionFeeWithFallback(transaction, network); const fee = 2233; // 应该使用上一行代码从链上获得,这里为了演示方便直接写死 transaction.setFee(fee); // Set the nonce const addressVersion = network.version === TransactionVersion.Mainnet ? AddressVersion.MainnetSingleSig : AddressVersion.TestnetSingleSig; const senderAddress = c32address(addressVersion, spendingCondition.signer); console.log("senderAddress", senderAddress); // ST24MG3M188B0RGND0PTTXA9RHAYBMNQ53GE6WRQA // const nonce = await getNonce(senderAddress, network); const nonce = 1; // 应该使用上一行代码从链上获得,这里为了演示方便直接写死 transaction.setNonce(nonce); // Sign the transaction, set the signature on the spending condition const privateKey = createStacksPrivateKey(senderKey); const msg = transaction.signBegin() + bytesToHex(new Uint8Array([transaction.auth.authType])) + bytesToHex(intToBytes(fee, false, 8)) + bytesToHex(intToBytes(nonce, false, 8)); console.log("msg", msg); // 855f926c9893082213fd2d8bdd100dbbe98422b918959579a29a09dc290d6dec0400000000000008b90000000000000001 const msgHash = sha512_256(hexToBytes(msg)); console.log("msgHash", bytesToHex(msgHash)) // 05733e861a2a10b4b389dd9b9cb82a348ebd442e7015f19026efa0e2d48db5ab const signature = signWithKey(privateKey, bytesToHex(msgHash)); // VRS signature console.log("signature", signature.data); // 00fac7b6f6a97d511d89d72e1fa496621c6d95ee77d212a057a23669adc76ff25d69f93d90c57d16c3608f7a603f3f55880ddb25603472ca8476f2cc98aaccb6da transaction.auth.spendingCondition.signature = signature; return transaction } // 构造 tx const tx = await buildTx( new StacksTestnet(), // 如果是主网交易,则改为 StacksMainnet() '53e804fec042f8fa21c6ab9e8c00d2c879aed9c8737601a8888602848471869a01', // 发送方私钥(注:最后多一个字节 01 表示使用对应 compressed 公钥推导发送方地址) 'STN3035BW0HRKFJE3E4NHN2867TZN63N9XCGSM1Z', // 接收方地址 123, // 单位为 micro-STX 'test memo') const rawTx = tx.serialize(); console.log("txHex", bytesToHex(rawTx)) // 8080000000040089480e8142160c42ad05b5aea9388abcba56e51c000000000000000100000000000008b90000fac7b6f6a97d511d89d72e1fa496621c6d95ee77d212a057a23669adc76ff25d69f93d90c57d16c3608f7a603f3f55880ddb25603472ca8476f2cc98aaccb6da03020000000000051a2a300cabe02389be4e1b8958d44831f5fa98754f000000000000007b74657374206d656d6f00000000000000000000000000000000000000000000000000 console.log("txId", bytesToHex(sha512_256(rawTx))); // 605c958a9ddcf38ac36f0f6f44c02d3d3cf5602ef3d3ad517e4756bc2d1c0883 writeFile('raw_tx.bin', rawTx, (err) => { if (err) throw err; console.log('The raw tx has been saved!'); });
4.1.1. TxId 计算
Stacks 中,TxId 就是签名后交易的 SHA-512/256 哈希(它是 SHA512 的截断版本)。 在上面例子中,TxId 就是 605c958a9ddcf38ac36f0f6f44c02d3d3cf5602ef3d3ad517e4756bc2d1c0883。
4.2. 计算签名(Python)
下面是计算节 2 中所演示的交易的 ECDSA 签名数据:
#!/usr/bin/env python3 import hashlib import ecdsa # pip install ecdsa from Crypto.Hash import SHA512 # pip install pycryptodome # SHA512/256(https://eprint.iacr.org/2010/548.pdf )是 SHA512 的截断版本,截断后的哈希值长度为 256 位 class SHA512_256(SHA512.SHA512Hash): def __init__(self, data=None): super().__init__(data, truncate='256') priv_key_hex = '53e804fec042f8fa21c6ab9e8c00d2c879aed9c8737601a8888602848471869a' priv_key = int(priv_key_hex, 16) msg = bytes.fromhex('855f926c9893082213fd2d8bdd100dbbe98422b918959579a29a09dc290d6dec0400000000000008b90000000000000001') # 前面有介绍它是怎么计算的 msgHash = SHA512_256(msg).digest() # Stacks 区块链要求待签名消息的哈希值必须是 SHA512/256 的结果 print("msgHash: ", msgHash.hex()) # 05733e861a2a10b4b389dd9b9cb82a348ebd442e7015f19026efa0e2d48db5ab sk: ecdsa.SigningKey = ecdsa.SigningKey.from_secret_exponent(priv_key, curve=ecdsa.SECP256k1) # 下面生成确定性签名(RFC6979),设置计算 k 时所用的 HMAC 中的哈希函数为 sha256 # 当然我们设置计算 k 时所用的 HMAC 中的哈希函数和对 msg 采用的哈希函数一致(即也设置为 SHA512_256)也可以正常产生签名 # 这里之所以采用 sha256,是为了和 stacks.js 中的 makeSTXTokenTransfer 的设置一样 deterministic_signature: bytes = sk.sign_digest_deterministic(msgHash, hashfunc=hashlib.sha256) r = deterministic_signature.hex()[0:64] s = deterministic_signature.hex()[64:] print("RFC6979 signature (r): ", r) # fac7b6f6a97d511d89d72e1fa496621c6d95ee77d212a057a23669adc76ff25d print("RFC6979 signature (s): ", s) # 9606c26f3a82e93c9f70859fc0c0aa76acd3b7867ad5d5b748df91f425698a67 # bip62: enforce low S values if int(s, 16) > ecdsa.SECP256k1.order // 2: s = hex(ecdsa.SECP256k1.order - int(s, 16))[2:] assert len(s) % 2 == 0 if len(s) < 64: s = '0' * (64 - len(s)) + s assert len(s) == 64 print("Low S signature (s): ", s) # 69f93d90c57d16c3608f7a603f3f55880ddb25603472ca8476f2cc98aaccb6da vk: ecdsa.VerifyingKey = sk.verifying_key print("Public key (compressed):", vk.to_string('compressed').hex()) # 03a1328ef6068af52aea4c09f1a31627017acd2ea15a3e23df2760ff1457f77165 assert vk.verify_digest(deterministic_signature, msgHash) assert vk.verify_digest(bytes.fromhex(r+s), msgHash)
4.3. 构造 SIP10 转帐交易(JS)
下面 JS 代码可以构造节 3 中所演示的交易:
import {broadcastTransaction, makeContractCall, uintCV, standardPrincipalCV, bufferCVFromString, optionalCVOf} from '@stacks/transactions'; import {StacksTestnet, StacksMainnet} from '@stacks/network'; import {bytesToHex} from "@stacks/common"; const txOptions = { contractAddress: 'STN3035BW0HRKFJE3E4NHN2867TZN63N9XCGSM1Z', contractName: 'ambitious-olive-capybara', functionName: 'transfer', functionArgs: [uintCV(120), standardPrincipalCV('ST24MG3M188B0RGND0PTTXA9RHAYBMNQ53GE6WRQA'), standardPrincipalCV('STN3035BW0HRKFJE3E4NHN2867TZN63N9XCGSM1Z'), optionalCVOf(bufferCVFromString('sip10 transfer test'))], senderKey: '53e804fec042f8fa21c6ab9e8c00d2c879aed9c8737601a8888602848471869a01', // attempt to fetch this contracts interface and validate the provided functionArgs validateWithAbi: true, network: new StacksTestnet(), // for mainnet, use `StacksMainnet()` }; const tx = await makeContractCall(txOptions); console.log("txHex", bytesToHex(tx.serialize())) // 8080000000040089480e8142160c42ad05b5aea9388abcba56e51c000000000000000200000000000001050000d58550986a2040495ca86b2ca7c0fd58e9b62f53f27848bf6a07c1568d769bdb6e9b4ad52cb2f5e390ad8d7d0b1f08a03fbb94520eaf98c9736960c080849b4d030200000000021a2a300cabe02389be4e1b8958d44831f5fa98754f18616d626974696f75732d6f6c6976652d6361707962617261087472616e73666572000000040100000000000000000000000000000078051a89480e8142160c42ad05b5aea9388abcba56e51c051a2a300cabe02389be4e1b8958d44831f5fa98754f0a02000000137369703130207472616e736665722074657374 console.log("txId", tx.txid()); // 3bd122ed6e2c8db4da436a9ae3a36ce465337ccd383a9a546193eaa81f6682a5 const broadcastResponse = await broadcastTransaction(tx); console.log("broadcastResponse", broadcastResponse)
注:重复运行上面代码时,输出的信息会不一样,这是因为 nonce 值和 fee 会发现变化。