Signing transactions
Overview
Before a transaction can be sent to the Ethereum network, it must be signed and formatted into a raw ETH transaction. Transactions are signed with threshold ECDSA. For this example, the transaction standard EIP1559 will be used.
Build a transaction
First, an raw EIP1559 ETH transaction must be built containing the transaction's metadata, such as gas fee, sender, receiver, and transaction data. Below is a programmatic example of how to build a transaction using Rust:
pub async fn transfer_eth(
transfer_args: TransferArgs,
rpc_services: RpcServices,
key_id: EcdsaKeyId,
derivation_path: Vec<Vec<u8>>,
nonce: U256,
evm_rpc: EvmRpcCanister,
) -> SendRawTransactionStatus {
// use the user provided gas_limit or fallback to default 210000
let gas = transfer_args.gas.unwrap_or(U256::from(21000));
// estimate the transaction fees by calling eth_feeHistory
let FeeEstimates {
max_fee_per_gas,
max_priority_fee_per_gas,
} = estimate_transaction_fees(9, rpc_services.clone(), evm_rpc.clone()).await;
// assemble the EIP 1559 transaction to be signed with t-ECDSA
let tx = Eip1559TransactionRequest {
from: None,
to: transfer_args.to,
value: Some(transfer_args.value),
max_fee_per_gas: Some(max_fee_per_gas),
max_priority_fee_per_gas: Some(max_priority_fee_per_gas),
gas: Some(gas),
nonce: Some(nonce),
chain_id: Some(rpc_services.chain_id()),
data: Default::default(),
access_list: Default::default(),
};
let tx = sign_eip1559_transaction(tx, key_id, derivation_path).await;
send_raw_transaction(tx.clone(), rpc_services, evm_rpc).await
}
View the full code example on GitHub.
Format, hash, and sign a transaction
Ethereum EIP1559 transactions are first hashed with the Keccak256 algorithm and then signed using the private key. Below is an example written in Rust demonstrating how to format a raw ETH transaction, hash it using Keccak256 and sign the hash using threshold ECDSA. This code snippet accomplishes the following:
Formats the transaction.
Hashes the transaction using Keccak256.
Signs the Keccak hash.
Rebuilds the transaction using the VRS signature.
pub async fn sign_eip1559_transaction(
tx: Eip1559TransactionRequest,
key_id: EcdsaKeyId,
derivation_path: Vec<Vec<u8>>,
) -> SignedTransaction {
const EIP1559_TX_ID: u8 = 2;
let ecdsa_pub_key =
get_canister_public_key(key_id.clone(), None, derivation_path.clone()).await;
// Formats the transaction using RLP encoding
let mut unsigned_tx_bytes = tx.rlp().to_vec();
unsigned_tx_bytes.insert(0, EIP1559_TX_ID);
// Hashes the transaction using Keccak256
let txhash = keccak256(&unsigned_tx_bytes);
// Sign the transaction hash using Threshold Signature
let signature = sign_with_ecdsa(SignWithEcdsaArgument {
message_hash: txhash.to_vec(),
derivation_path,
key_id,
})
.await
.expect("failed to sign the transaction")
.0
.signature;
let signature = Signature {
v: y_parity(&txhash, &signature, &ecdsa_pub_key),
r: U256::from_big_endian(&signature[0..32]),
s: U256::from_big_endian(&signature[32..64]),
};
// Rebuild the raw transaction which includes the VRS signatures
let mut signed_tx_bytes = tx.rlp_signed(&signature).to_vec();
signed_tx_bytes.insert(0, EIP1559_TX_ID);
format!("0x{}", hex::encode(&signed_tx_bytes))
}
View the full code example on GitHub.
Additional examples of signing transactions with threshold ECDSA can be found in the threshold ECDSA documentation.
Submit transaction
Now that your transaction is signed, it can be submitted to Ethereum to be executed.