本文详细解析了 Stacks 区块链中的交易结构,包括原生币转账和 SIP10 代币转账,深入探讨了交易的构造、签名计算以及关键字段的含义,并提供了相关的 JS 和 Python 代码示例,以便开发者理解和构建 Stacks 交易。
Created: <2024-04-14 Sun>
Last updated: <2024-04-22 Mon>
在 Stacks 区块链中,通过 RPC /v2/transactions 可以把签名后的 Tx 提交到链上。本文介绍签名打包 Stacks 交易的细节。
测试网上交易 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 |
上一节的例子中,ECDSA 的签名 rs 值(即 fac7b6f6a97d511d89d72e1fa496621c6d95ee77d212a057a23669adc76ff25d69f93d90c57d16c3608f7a603f3f55880ddb25603472ca8476f2cc98aaccb6da)是如何计算的呢?
首先,要计算“待签名数据”,其过程如下:
8080000000040089480e8142160c42ad05b5aea9388abcba56e51c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003020000000000051a2a300cabe02389be4e1b8958d44831f5fa98754f000000000000007b74657374206d656d6f00000000000000000000000000000000000000000000000000
把上面结果计算 SHA512_256 哈希,即得到:855f926c9893082213fd2d8bdd100dbbe98422b918959579a29a09dc290d6dec
上面数据的后面再加上 [auth type][fee][nonce],即得到“待签名数据”:855f926c9893082213fd2d8bdd100dbbe98422b918959579a29a09dc290d6dec0400000000000008b90000000000000001
然后,进行 ECDSA 签名,对“待签名数据”所使用的哈希函数为 SHA512_256(它是 SHA512 的截断版本),这一步的细节可以参考节 4.2,最终可以得到结果:
r=fac7b6f6a97d511d89d72e1fa496621c6d95ee77d212a057a23669adc76ff25d
s=69f93d90c57d16c3608f7a603f3f55880ddb25603472ca8476f2cc98aaccb6da
合约 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
下面 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!');
});
Stacks 中,TxId 就是签名后交易的 SHA-512/256 哈希(它是 SHA512 的截断版本)。 在上面例子中,TxId 就是 605c958a9ddcf38ac36f0f6f44c02d3d3cf5602ef3d3ad517e4756bc2d1c0883。
下面是计算节 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)
下面 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 会发现变化。
https://github.com/stacksgov/sips/blob/main/sips/sip-005/sip-005-blocks-and-transactions.md
https://docs.hiro.so/stacks-blockchain-api/feature-guides/transactions
- 本文转载自: aandds.com/blog/stacks-t... , 如有侵权请联系管理员删除。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!