Starknet (Blockchain Platform)

Table of Contents

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

Table 1: Starknet 帐户合约的提供商
Vendor Identifier Link
Argent argent Link
Braavos braavos Link
OpenZeppelin oz Link

2.1. 创建帐户合约

使用 starkli 创建帐户合约时,会涉及到两个文件:

  1. keystore.json:保存加密的私钥
  2. 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 的内容:

  1. 部署状态从 undeployed 改为 deployed;
  2. salt 不再需要了,会删除
  3. 增加了部署的合约 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.3.4. u256 对应 Stark 域两个元素

2.3 中提到过,transfer 函数转帐金额(u256 类型)参数需要 Stark 域的两个元素来表达。这里介绍更多的一些细节。

由于 Stark 域中元素最大值是 2251+172192+1 ,所以一个 Stark 域元素无法表达出 u256 类型数据的完整范围。 在 Starknet 中,用两个 Stark 域元素来表示一个 u256 数据,分别对应 u256 数据的低 128 bits 和高 128 bits。 显然,当 u256 类型的数据没有超过 128 bits 时,第二个 Stark 域元素为 0。

参考:https://book.starkli.rs/argument-resolution#u256

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
}

4. 参考

Author: cig01

Created: <2024-06-06 Thu>

Last updated: <2024-06-10 Mon>

Creator: Emacs 27.1 (Org mode 9.4)