以太坊 - Stacks Raw Tx Breakdown

  • cig01
  • 发布于 2024-04-15 17:48
  • 阅读 6

本文详细解析了 Stacks 区块链中的交易结构,包括原生币转账和 SIP10 代币转账,深入探讨了交易的构造、签名计算以及关键字段的含义,并提供了相关的 JS 和 Python 代码示例,以便开发者理解和构建 Stacks 交易。

Created: <2024-04-14 Sun>

Last updated: <2024-04-22 Mon>

Stacks Raw Tx Breakdown

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)是如何计算的呢?

首先,要计算“待签名数据”,其过程如下:

  1. 把 nonce 值、fee 以及签名数据全部设置为 0,即得到:
    8080000000040089480e8142160c42ad05b5aea9388abcba56e51c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003020000000000051a2a300cabe02389be4e1b8958d44831f5fa98754f000000000000007b74657374206d656d6f00000000000000000000000000000000000000000000000000
  1. 把上面结果计算 SHA512_256 哈希,即得到:855f926c9893082213fd2d8bdd100dbbe98422b918959579a29a09dc290d6dec

  2. 上面数据的后面再加上 [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) &lt; 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 会发现变化。

5. 参考

  1. https://github.com/stacksgov/sips/blob/main/sips/sip-005/sip-005-blocks-and-transactions.md

  2. https://docs.hiro.so/stacks-blockchain-api/feature-guides/transactions

点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
cig01
cig01
https://aandds.com/