Starknet (Blockchain Platform)
1. 简介
Starknet 是以太坊上的 ZK rollup 扩容方案,它的签名算法采用 ECDSA,底层的曲线是 STARK curve。Starknet 的智能合约编程语言是 Cairo 。
1.1. 安装 Command Line(starkli)
starkli 是 Starknet 的命令行工具,可以通过它进行发送交易等操作。下面是 starkli 的安装步骤:
$ curl https://get.starkli.sh | sh # 安装 starkliup $ . ${HOME}/.starkli/env $ starkliup # 通过 starkliup 安装 starkli $ starkli --version # 安装成功后,检查一下安装的 starkli 的版本 0.3.0 (765251d)
2. 帐户
我们知道以太坊上有两种帐户:EOA 和智能合约帐户。不过 Starknet 和以太坊不同, 在 Starknet 中,所有的账户都是智能合约。
Starknet 的帐户合约有不同的提供商,starkli 工具中集成了表 所示的三家流行提供商。参考:https://book.starkli.rs/accounts
Vendor | Identifier | Link |
---|---|---|
Argent | argent | Link |
Braavos | braavos | Link |
OpenZeppelin | oz | Link |
2.1. 创建帐户合约
使用 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 背后的私钥所控制。
2.2. 部署帐户合约
在部署之前,先从 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" } }
2.2.1. 提交部署帐户合约的 Tx(starknet_addDeployAccountTransaction)
前面例子中,帐户合约的部署实际上是通过 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。
2.3. 测试 ETH 转账
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
2.3.1. 提交合约调用的 Tx(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 可以获得 } ] }
2.3.2. 手续费计算
关于 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
2.3.3. 签名计算
Starknet 的签名是 ECDSA,不过它底层的曲线是 STARK curve。 关于上面例子中的签名的计算可以参考节 3.3 。
2.4. 查询合约(starknet_call)
如何查询某帐户的 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)。
3. 附录
3.1. 推导 Starknet 地址
下面 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 }
3.2. 部署合约 Tx Hash 及签名的计算
下面 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 }
3.3. 合约调用 Tx Hash 及签名的计算
下面 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 }