Skip to main content

Signing transactions

Advanced
Ethereum
Tutorial

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 using threshold ECDSA chain-key signatures. 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 hashed and signed using the Keccak256 algorithm. Below is an example written in Rust demonstrating how to format a raw ETH transaction and hash it using Keccak256. 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;

let mut unsigned_tx_bytes = tx.rlp().to_vec();
unsigned_tx_bytes.insert(0, EIP1559_TX_ID);

let txhash = keccak256(&unsigned_tx_bytes);

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]),
};

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 can be found in the threshold ECDSA documentation.

Rebuild the raw transaction

Submit transaction

Now that your transaction is signed, it can be submitted to Ethereum to be executed.

Learn how to submit Ethereum transactions.