本文档详细介绍了Starknet区块链平台,它是一个以太坊上的ZK rollup扩容方案。文章涵盖了Starknet的简介、命令行工具starkli的安装、账户创建与部署(包括使用Argent提供商的示例)、以及ETH转账的实现,包括手续费和签名计算。此外,还提供了关于查询合约余额(starknet_call),以及推导Starknet地址、交易哈希和签名的Rust代码示例。
Created: <2024-06-06 Thu> Last updated: <2024-06-10 Mon>
Starknet 是以太坊上的 ZK rollup 扩容方案,它的签名算法采用 ECDSA,底层的曲线是 STARK curve。Starknet 的智能合约编程语言是 Cairo 。
starkli 是 Starknet 的命令行工具,可以通过它进行发送交易等操作。下面是 starkli 的安装步骤:
$ curl https://get.starkli.sh | sh # 安装 starkliup
$ . ${HOME}/.starkli/env
$ starkliup # 通过 starkliup 安装 starkli
$ starkli --version # 安装成功后,检查一下安装的 starkli 的版本
0.3.0 (765251d)
我们知道以太坊上有两种帐户:EOA 和智能合约帐户。不过 Starknet 和以太坊不同, 在 Starknet 中,所有的账户都是智能合约。
Starknet 的帐户合约有不同的提供商,starkli 工具中集成了表 所示的三家流行提供商。参考: https://book.starkli.rs/accounts
Vendor | Identifier | Link |
---|---|---|
Argent | argent | Link |
Braavos | braavos | Link |
OpenZeppelin | oz | Link |
Table 1: Starknet 帐户合约的提供商
使用 starkli 创建帐户合约时,会涉及到两个文件:
keystore.json:保存加密的私钥
account.json:保存部署帐户合约时需要的公钥,class_hash,salt,帐户合约地址等
下面以帐户合约提供商 argent 为例,介绍一下 Starknet 帐户合约的创建过程。
$ mkdir -p ~/.starkli-wallets/wallet1/
$ starkli signer keystore new ~/.starkli-wallets/wallet1/keystore.json
Enter password: # 这里输入了密码 12345678,注:主网环境中不要用这样的简单密码!
Created new encrypted keystore file: /Users/user/.starkli-wallets/wallet1/keystore.json
Public key: 0x00e8cc2755e908706534f9d9656d2607d22f4ccd4e10f74ec894365aeb293bc2
$ cat ~/.starkli-wallets/wallet1/keystore.json | python -m json.tool
{
"crypto": {
"cipher": "aes-128-ctr",
"cipherparams": {
"iv": "356e71ab8a2aab561c3179f945fc3884"
},
"ciphertext": "9942eb21032c37b4faccfcb2ea63a28d448ea2b6f80bd15bfc4787ee122fc6ea",
"kdf": "scrypt",
"kdfparams": {
"dklen": 32,
"n": 8192,
"p": 1,
"r": 8,
"salt": "726c68dce77fee277a50b5b468222c8b24e922b61b4dfd75ee5d341dabf2b812"
},
"mac": "39b768d44689ee5864dad79dedf3ca7820614f2aebd4196f8609ff65a2ce573d"
},
"id": "2e1126d1-e1a3-481e-83ee-c0a49434798e",
"version": 3
}
$ starkli account argent init --keystore ~/.starkli-wallets/wallet1/keystore.json ~/.starkli-wallets/wallet1/account.json
Enter keystore password:
Created new account config file: /Users/user/.starkli-wallets/wallet1/account.json
Once deployed, this account will be available at:
0x074627055cda2c0f1a986e8a7e778c06fbd5e622942d37c39955f8786d60f0c6
Deploy this account by running:
starkli account deploy /Users/user/.starkli-wallets/wallet1/account.json
$ cat /Users/user/.starkli-wallets/wallet1/account.json
{
"version": 1,
"variant": {
"type": "argent",
"version": 1,
"owner": "0xe8cc2755e908706534f9d9656d2607d22f4ccd4e10f74ec894365aeb293bc2",
"guardian": "0x0"
},
"deployment": {
"status": "undeployed",
"class_hash": "0x29927c8af6bccf3f6fda035981e765a7bdbf18a2dc0d630494f8758aa908e2b",
"salt": "0x601b5937784f99a86bced889f7a8d8937ab832301ee68760d15dc376947ce90"
}
}
从上面的输出中可知,帐户合约地址为 0x074627055cda2c0f1a986e8a7e778c06fbd5e622942d37c39955f8786d60f0c6,它由公钥 0xe8cc2755e908706534f9d9656d2607d22f4ccd4e10f74ec894365aeb293bc2 背后的私钥所控制。
在部署之前,先从 Starknet Faucet Sepolia 中领取一些 ETH 测试币到地址 0x074627055cda2c0f1a986e8a7e778c06fbd5e622942d37c39955f8786d60f0c6 中。
执行 starkli account deploy
可以部署合约,比如:
$ starkli account deploy --network=sepolia --keystore ~/.starkli-wallets/wallet1/keystore.json ~/.starkli-wallets/wallet1/account.json
Enter keystore password:
The estimated account deployment fee is 0.000061258282597512 ETH. However, to avoid failure, fund at least:
0.000091887423896268 ETH
to the following address:
0x074627055cda2c0f1a986e8a7e778c06fbd5e622942d37c39955f8786d60f0c6
Press [ENTER] once you've funded the address.
Account deployment transaction: 0x05ed078f6f240974d201d094fc60248cfb856766365b2080022268058c68c1d0
Waiting for transaction 0x05ed078f6f240974d201d094fc60248cfb856766365b2080022268058c68c1d0 to confirm. If this process is interrupted, you will need to run `starkli account fetch` to update the account file.
Transaction not confirmed yet...
Transaction 0x05ed078f6f240974d201d094fc60248cfb856766365b2080022268058c68c1d0 confirmed
可以看到 Tx 0x05ed078f6f240974d201d094fc60248cfb856766365b2080022268058c68c1d0 是一个类型为 DEPLOY_ACCOUNT 的交易。
部署执行完成后,上面脚本会修改 ~/.starkli-wallets/wallet1/account.json 的内容:
部署状态从 undeployed 改为 deployed;
salt 不再需要了,会删除
增加了部署的合约 address。
比如下面是部署帐户合约后 account.json 的内容:
$ cat ~/.starkli-wallets/wallet1/account.json
{
"version": 1,
"variant": {
"type": "argent",
"version": 1,
"owner": "0xe8cc2755e908706534f9d9656d2607d22f4ccd4e10f74ec894365aeb293bc2",
"guardian": "0x0"
},
"deployment": {
"status": "deployed",
"class_hash": "0x29927c8af6bccf3f6fda035981e765a7bdbf18a2dc0d630494f8758aa908e2b",
"address": "0x74627055cda2c0f1a986e8a7e778c06fbd5e622942d37c39955f8786d60f0c6"
}
}
前面例子中,帐户合约的部署实际上是通过 RPC starknet_addDeployAccountTransaction 完成的。对于上面例子,它的具体参数为:
{
"id": 1,
"jsonrpc": "2.0",
"method": "starknet_addDeployAccountTransaction",
"params": [\
{\
"type": "DEPLOY_ACCOUNT",\
"max_fee": "0x53923542BACC",\
"version": "0x1",\
"signature": [\
"0x312c46b41c33cea4b71ece943228054bceb5297e90151a88fb0b1a3ed56bda7",\
"0x316bcac811c8eb4b8ee4077a8da3492e492b550495513d7a97211b4d2d8f22c"\
],\
"nonce": "0x0",\
"contract_address_salt": "0x601b5937784f99a86bced889f7a8d8937ab832301ee68760d15dc376947ce90",\
"constructor_calldata": [\
"0xe8cc2755e908706534f9d9656d2607d22f4ccd4e10f74ec894365aeb293bc2",\
"0x0"\
],\
"class_hash": "0x29927c8af6bccf3f6fda035981e765a7bdbf18a2dc0d630494f8758aa908e2b"\
}\
]
}
关于 tx_hash 0x05ed078f6f240974d201d094fc60248cfb856766365b2080022268058c68c1d0 及签名的计算可以参考节 3.2。
ETH 在 Starknet 中是以 ERC20 合约存在的,它的地址为 0x049D36570D4e46f48e99674bd3fcc84644DdD6b96F7C741B1562B82f9e004dC7(Starknet 主网和 Starknet Sepolia 测试网都是这个地址)。所以, 在 Starknet 中转帐 ETH 实质上就是调用合约 0x049D36570D4e46f48e99674bd3fcc84644DdD6b96F7C741B1562B82f9e004dC7 中的 transfer 方法。
通过 starkli invoke
可以调用合约方法,如下面命令可以向地址 0x04f24fe313f5970ad24d83f26e10bfc8e7a9c5d97997a99b71b90b311e66e33e 转帐 0.000123456789012345 ETH:
$ starkli invoke 0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7 transfer 0x04f24fe313f5970ad24d83f26e10bfc8e7a9c5d97997a99b71b90b311e66e33e u256:123456789012345 --log-traffic --network=sepolia --keystore ~/.starkli-wallets/wallet1/keystore.json --account ~/.starkli-wallets/wallet1/account.json
Enter keystore password:
[2024-06-06T14:48:40Z TRACE starknet_providers::jsonrpc::transports::http] Sending request via JSON-RPC: {"id":1,"jsonrpc":"2.0","method":"starknet_chainId","params":[]}
[2024-06-06T14:48:40Z TRACE starknet_providers::jsonrpc::transports::http] Response from JSON-RPC: {"jsonrpc":"2.0","result":"0x534e5f5345504f4c4941","id":1}
[2024-06-06T14:48:40Z TRACE starknet_providers::jsonrpc::transports::http] Sending request via JSON-RPC: {"id":1,"jsonrpc":"2.0","method":"starknet_getNonce","params":["pending","0x74627055cda2c0f1a986e8a7e778c06fbd5e622942d37c39955f8786d60f0c6"]}
[2024-06-06T14:48:41Z TRACE starknet_providers::jsonrpc::transports::http] Response from JSON-RPC: {"jsonrpc":"2.0","result":"0x2","id":1}
[2024-06-06T14:48:41Z TRACE starknet_providers::jsonrpc::transports::http] Sending request via JSON-RPC: {"id":1,"jsonrpc":"2.0","method":"starknet_estimateFee","params":[[{"type":"INVOKE","sender_address":"0x74627055cda2c0f1a986e8a7e778c06fbd5e622942d37c39955f8786d60f0c6","calldata":["0x1","0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7","0x83afd3f4caedc6eebf44246fe54e38c95e3179a5ec9ea81740eca5b482d12e","0x3","0x4f24fe313f5970ad24d83f26e10bfc8e7a9c5d97997a99b71b90b311e66e33e","0x7048860ddf79","0x0"],"max_fee":"0x0","version":"0x100000000000000000000000000000001","signature":["0x64bafdcfa3dcbc6a78857b55fb7deeb0325fed1702f9797036be9f7e24affeb","0x703f6533089fe84d3271d6d216d37aae1dd0222c5735ba2f4fac9f6ed55bdaf"],"nonce":"0x2"}],[],"pending"]}
[2024-06-06T14:48:41Z TRACE starknet_providers::jsonrpc::transports::http] Response from JSON-RPC: {"jsonrpc":"2.0","result":[{"gas_consumed":"0xadb","gas_price":"0x6b8a23d4d","data_gas_consumed":"0x0","data_gas_price":"0x186a0","overall_fee":"0x48f6492f72df","unit":"WEI"}],"id":1}
[2024-06-06T14:48:41Z TRACE starknet_providers::jsonrpc::transports::http] Sending request via JSON-RPC: {"id":1,"jsonrpc":"2.0","method":"starknet_addInvokeTransaction","params":[{"type":"INVOKE","sender_address":"0x74627055cda2c0f1a986e8a7e778c06fbd5e622942d37c39955f8786d60f0c6","calldata":["0x1","0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7","0x83afd3f4caedc6eebf44246fe54e38c95e3179a5ec9ea81740eca5b482d12e","0x3","0x4f24fe313f5970ad24d83f26e10bfc8e7a9c5d97997a99b71b90b311e66e33e","0x7048860ddf79","0x0"],"max_fee":"0x6d716dc72c4e","version":"0x1","signature":["0x1e1e751caefcdd7a72a12b6472b7a83dbe353dc0d65829929822a1aa829aea0","0x16668e1026e1990fb7d73cb2b36a62f4deefa489297effef24cd7f5f6a8b280"],"nonce":"0x2"}]}
[2024-06-06T14:48:42Z TRACE starknet_providers::jsonrpc::transports::http] Response from JSON-RPC: {"jsonrpc":"2.0","result":{"transaction_hash":"0x6541740a3af874ba7c52906b7743af175baaf313c92b21e161bd6cfcd631ef8"},"id":1}
Invoke transaction: 0x06541740a3af874ba7c52906b7743af175baaf313c92b21e161bd6cfcd631ef8
从上面例子的日志中可以知道,一共有下面 4 次 RPC 调用:
starknet_chainId
starknet_getNonce
starknet_estimateFee
starknet_addInvokeTransaction
通过 RPC starknet_addInvokeTransaction 可以提交签名 Tx 给节点进行打包。对于上面的例子,它的数据及说明如下:
{
"id":1,
"jsonrpc":"2.0",
"method":"starknet_addInvokeTransaction",
"params":[\
{\
"type":"INVOKE",\
"sender_address":"0x74627055cda2c0f1a986e8a7e778c06fbd5e622942d37c39955f8786d60f0c6",\
"calldata":[\
"0x1", // call 的个数,这里是 1。starknet 中指定多个 call,就可以实现在一个 Tx 中往多个地址转帐了。\
"0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7", // ETH 合约地址\
"0x83afd3f4caedc6eebf44246fe54e38c95e3179a5ec9ea81740eca5b482d12e", // selector,它是字符串 transfer 的 keccak 哈希,再清除前 6 个 bit 位\
"0x3", // transfer 参数的个数。明明只有目标地址和转帐金额两个参数,为什么这里是 3 呢?因为转帐金额是 u256 类型,需要用两个 Stark fields 元素来表达\
"0x4f24fe313f5970ad24d83f26e10bfc8e7a9c5d97997a99b71b90b311e66e33e", // 转帐目标地址\
"0x7048860ddf79", // 十进制为 123456789012345,它是转帐金额的低 128 bits\
"0x0" // 转帐金额的高 128 bits\
],\
"max_fee":"0x6d716dc72c4e", // RPC starknet_estimateFee 返回值中字段 overall_fee 的值乘以 1.5(当然也可以不乘这么大的因子)\
"version":"0x1", // 交易版本号,如果用 ETH 支付手续费,可以使用 v1 交易,如果用 STRK 支持手续费,则要使用 v3 交易\
"signature":[\
"0x1e1e751caefcdd7a72a12b6472b7a83dbe353dc0d65829929822a1aa829aea0",\
"0x16668e1026e1990fb7d73cb2b36a62f4deefa489297effef24cd7f5f6a8b280"\
],\
"nonce":"0x2" // 通过 RPC starknet_getNonce 可以获得\
}\
]
}
关于 Starknet 中手续费计算可以参考: https://docs.starknet.io/documentation/architecture_and_concepts/Network_Architecture/fee-mechanism/
对于 v1 的交易来说,只需要设置 max_fee。工具 starkli 设置 max_fee 的策略比较简单: RPC starknet_estimateFee 返回值中字段 overall_fee 的值乘以 1.5, 对于上面的例子中,就是:
max_fee (i.e. 0x6d716dc72c4e) = overall_fee (i.e. 0x48f6492f72df) * 1.5
Starknet 的签名是 ECDSA,不过它底层的曲线是 STARK curve。 关于上面例子中的签名的计算可以参考节 3.3 。
节 2.3 中提到过,transfer 函数转帐金额(u256 类型)参数需要 Stark 域的两个元素来表达。这里介绍更多的一些细节。
由于 Stark 域中元素最大值是 2251+17∗2192+1 ,所以一个 Stark 域元素无法表达出 u256 类型数据的完整范围。 在 Starknet 中,用两个 Stark 域元素来表示一个 u256 数据,分别对应 u256 数据的低 128 bits 和高 128 bits。 显然,当 u256 类型的数据没有超过 128 bits 时,第二个 Stark 域元素为 0。
参考: https://book.starkli.rs/argument-resolution#u256
如何查询某帐户的 ETH 余额呢?其实就是调用 ETH 相关 ERC20 合约的只读方法 balanceOf,这是通过向 RPC 发起 starknet_call
请求来实现的,比如:
$ curl -X POST -H "Content-Type: application/json" https://starknet-sepolia.public.blastapi.io/rpc/v0_7 -d '{
"id": 1,
"jsonrpc": "2.0",
"method": "starknet_call",
"params": [\
{\
"contract_address": "0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7",\
"entry_point_selector": "0x2e4263afad30923c891518314c3c95dbe830a16874e8abc5777a9a20b54c76e",\
"calldata": [\
"0x74627055cda2c0f1a986e8a7e778c06fbd5e622942d37c39955f8786d60f0c6"\
]\
},\
"pending"\
]
}' # 查询 0x74627055cda2c0f1a986e8a7e778c06fbd5e622942d37c39955f8786d60f0c6 的余额
{"jsonrpc":"2.0","result":["0x57a5936e90b4d7","0x0"],"id":1}
其中 entry_point_selector 就是合约方法 balanceOf
的 keccak 哈希,然后清除前 6 个 bits 得到的。
注意,查询得到的结果是 u256 类型,所以返回了 Stark 域两个元素(参考节 2.3.4)。
下面 Rust 程序演示了 Starknet 中帐户合约地址是如何推导的:
// Add starknet = "0.10.0" as dependencies into file Cargo.toml
use starknet::core::crypto::compute_hash_on_elements;
use starknet::core::types::FieldElement;
use starknet::core::utils::{cairo_short_string_to_felt, normalize_address};
use starknet::macros::felt;
use starknet::signers::SigningKey;
fn main() -> () {
// Starknet contract 地址的生成规则可参考:
// https://docs.starknet.io/documentation/architecture_and_concepts/Smart_Contracts/contract-address/
// 这个例子中,我们选择了 Argent 合约作为帐户合约,当然也可以选择 Braavos/OpenZeppelin 合约作为帐户合约
// 如果要使用 Braavos/OpenZeppelin 合约,需要:
// 1. 修改 CLASS_HASH 为 Braavos/OpenZeppelin 合约的 CLASS_HASH
// 2. 修改后面的 constructor_calldata 为 Braavos/OpenZeppelin 合约的 constructor_calldata(只需要 owner_public_key)
//
// Argent X official account (as of 5.13.1)
// 来源:https://github.com/xJonathanLEI/starkli/blob/765251df21b30c2cd4eee914e6a3b2f7912b13e8/src/account.rs#L60
// 合约源码:https://voyager.online/class/0x029927c8af6bccf3f6fda035981e765a7bdbf18a2dc0d630494f8758aa908e2b
const CLASS_HASH: FieldElement = felt!("0x029927c8af6bccf3f6fda035981e765a7bdbf18a2dc0d630494f8758aa908e2b");
// 合约通过 DEPLOY_ACCOUNT Tx 来部署,所以 deployer_address 为 0
// 参考:https://docs.starknet.io/documentation/architecture_and_concepts/Smart_Contracts/contract-address/
let deployer_address = FieldElement::ZERO;
// 随机 salt(不能超过 2^{251}+17⋅2^{192}+1)
let salt = felt!("0x0601b5937784f99a86bced889f7a8d8937ab832301ee68760d15dc376947ce90");
// 私钥(不能超过 2^{251}+17⋅2^{192}+1)。也可以用 SigningKey::from_random() 随机生成
let key = SigningKey::from_secret_scalar(FieldElement::from_hex_be(
"0x071f28eb14ab13f7c79397e299375128c926d946d8bc7ea5acb9c9272c6160fe",
).unwrap());
// 公钥
let owner_public_key = key.verifying_key().scalar();
print!("Owner private key: {}\n", format!("{:#064x}", key.secret_scalar()));
print!("Owner public key: {}\n", format!("{:#064x}", owner_public_key)); // 0x00e8cc2755e908706534f9d9656d2607d22f4ccd4e10f74ec894365aeb293bc2
print!("Salt: {}\n", format!("{:#064x}", salt));
let argent_guardian = FieldElement::ZERO;
// 注:对于 Argent 合约来说,constructor_calldata 中除 owner 公钥外,还需要提供一个 guardian 参数
// 对于 Braavos/OpenZeppelin 合约来说,constructor_calldata 中只需要提供 owner 公钥
let constructor_calldata = [owner_public_key, argent_guardian];
// Starknet contract 地址是 pedersen hash
let starknet_addr = normalize_address(compute_hash_on_elements(&[\
cairo_short_string_to_felt("STARKNET_CONTRACT_ADDRESS").unwrap(),\
deployer_address,\
salt,\
CLASS_HASH,\
compute_hash_on_elements(&constructor_calldata),\
]));
print!("Starknet address: {}\n", format!("{:#064x}", starknet_addr)); // 0x074627055cda2c0f1a986e8a7e778c06fbd5e622942d37c39955f8786d60f0c6
}
下面 Rust 程序演示了节 2.2.1 中部署帐户合约的 tx_hash 和 ECDSA 签名数据的具体计算过程:
use starknet::core::crypto::compute_hash_on_elements;
use starknet::core::types::FieldElement;
use starknet::core::utils::cairo_short_string_to_felt;
use starknet::signers::SigningKey;
// 这是一个部署帐户合约的 Tx
// tx_hash 是 Tx 数据的 pedersen hash
// https://docs.starknet.io/documentation/architecture_and_concepts/Cryptography/hash-functions/#array_hashing
fn get_tx_hash() -> FieldElement {
let encoded_calls = compute_hash_on_elements(&[\
FieldElement::from_hex_be("0x29927c8af6bccf3f6fda035981e765a7bdbf18a2dc0d630494f8758aa908e2b").unwrap(), // class_hash\
FieldElement::from_hex_be("0x601b5937784f99a86bced889f7a8d8937ab832301ee68760d15dc376947ce90").unwrap(), // salt\
FieldElement::from_hex_be("0xe8cc2755e908706534f9d9656d2607d22f4ccd4e10f74ec894365aeb293bc2").unwrap(), // constructor_calldata\
FieldElement::from_hex_be("0x0").unwrap(), // constructor_calldata\
]);
let tx_hash = compute_hash_on_elements(&[\
cairo_short_string_to_felt("deploy_account").unwrap(), // transaction type\
FieldElement::ONE, // version\
FieldElement::from_hex_be("0x74627055cda2c0f1a986e8a7e778c06fbd5e622942d37c39955f8786d60f0c6").unwrap(), // sender address\
FieldElement::ZERO, // entry_point_selector\
encoded_calls, // compute_hash_on_elements(&encoder.encode_calls(&self.calls)),\
FieldElement::from_hex_be("0x53923542BACC").unwrap(), // max_fee\
FieldElement::from_hex_be("0x534e5f5345504f4c4941").unwrap(), // chain_id, can get from RPC starknet_chainId\
FieldElement::from_hex_be("0x0").unwrap() // nonce, can get from RPC starknet_getNonce\
]);
return tx_hash
}
fn main() -> () {
// 私钥
let key = SigningKey::from_secret_scalar(FieldElement::from_hex_be(
"0x071f28eb14ab13f7c79397e299375128c926d946d8bc7ea5acb9c9272c6160fe",
).unwrap());
// 公钥
let owner_public_key = key.verifying_key().scalar();
println!("Owner private key: {}", format!("{:#064x}", key.secret_scalar()));
println!("Owner public key: {}", format!("{:#064x}", owner_public_key));
// tx_hash 就是 ECDSA 签名中的 msg_hash
let tx_hash = get_tx_hash();
println!("tx_hash: {}", format!("{:#064x}", tx_hash)); // 0x05ed078f6f240974d201d094fc60248cfb856766365b2080022268058c68c1d0
// 计算 ECDSA 签名(Stark Curve)
let signature = key.sign(&tx_hash).unwrap();
println!("signature r: {}", format!("{:#064x}", signature.r)); // 0x0312c46b41c33cea4b71ece943228054bceb5297e90151a88fb0b1a3ed56bda7
println!("signature s: {}", format!("{:#064x}", signature.s)); // 0x0316bcac811c8eb4b8ee4077a8da3492e492b550495513d7a97211b4d2d8f22c
}
下面 Rust 程序演示了节 2.3 中 ETH 转移例子中的 tx_hash 和 ECDSA 签名数据的具体计算过程:
use starknet::core::crypto::compute_hash_on_elements;
use starknet::core::types::FieldElement;
use starknet::core::utils::{cairo_short_string_to_felt, starknet_keccak};
use starknet::signers::SigningKey;
// 这是一个合约调用的 Tx
// tx_hash 是 Tx 数据的 pedersen hash
// https://docs.starknet.io/documentation/architecture_and_concepts/Cryptography/hash-functions/#array_hashing
fn get_tx_hash() -> FieldElement {
let selector = starknet_keccak("transfer".as_bytes()); // 就是 keccak 哈希后,清除前 6 个 bit 位(即设置为 0)
let encoded_calls = compute_hash_on_elements(&[\
FieldElement::from_hex_be("0x1").unwrap(), // number of calls\
FieldElement::from_hex_be("0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7").unwrap(), // contract address\
selector, // selector, "0x83afd3f4caedc6eebf44246fe54e38c95e3179a5ec9ea81740eca5b482d12e"\
FieldElement::from_hex_be("0x3").unwrap(), // length of calldata\
FieldElement::from_hex_be("0x4f24fe313f5970ad24d83f26e10bfc8e7a9c5d97997a99b71b90b311e66e33e").unwrap(), // target_address\
FieldElement::from_hex_be("0x7048860ddf79").unwrap(), // amount\
FieldElement::from_hex_be("0x0").unwrap(),\
]);
let tx_hash = compute_hash_on_elements(&[\
cairo_short_string_to_felt("invoke").unwrap(), // transaction type\
FieldElement::ONE, // version\
FieldElement::from_hex_be("0x74627055cda2c0f1a986e8a7e778c06fbd5e622942d37c39955f8786d60f0c6").unwrap(), // sender address\
FieldElement::ZERO, // entry_point_selector\
encoded_calls, // compute_hash_on_elements(&encoder.encode_calls(&self.calls)),\
FieldElement::from_hex_be("0x6d716dc72c4e").unwrap(), // max_fee\
FieldElement::from_hex_be("0x534e5f5345504f4c4941").unwrap(), // chain_id, can get from RPC starknet_chainId\
FieldElement::from_hex_be("0x2").unwrap() // nonce, can get from RPC starknet_getNonce\
]);
return tx_hash
}
fn main() -> () {
// 私钥
let key = SigningKey::from_secret_scalar(FieldElement::from_hex_be(
"0x071f28eb14ab13f7c79397e299375128c926d946d8bc7ea5acb9c9272c6160fe",
).unwrap());
// 公钥
let owner_public_key = key.verifying_key().scalar();
println!("Owner private key: {}", format!("{:#064x}", key.secret_scalar()));
println!("Owner public key: {}", format!("{:#064x}", owner_public_key));
// tx_hash 就是 ECDSA 签名中的 msg_hash
let tx_hash = get_tx_hash();
println!("tx_hash: {}", format!("{:#064x}", tx_hash)); // 0x06541740a3af874ba7c52906b7743af175baaf313c92b21e161bd6cfcd631ef8
// 计算 ECDSA 签名(Stark Curve)
let signature = key.sign(&tx_hash).unwrap();
println!("signature r: {}", format!("{:#064x}", signature.r)); // 0x01e1e751caefcdd7a72a12b6472b7a83dbe353dc0d65829929822a1aa829aea0
println!("signature s: {}", format!("{:#064x}", signature.s)); // 0x016668e1026e1990fb7d73cb2b36a62f4deefa489297effef24cd7f5f6a8b280
}
- 本文转载自: aandds.com/blog/starknet... , 如有侵权请联系管理员删除。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!