Skip to content

Commit

Permalink
Merge pull request #3446 from dfinity/add-eth-transaction-details
Browse files Browse the repository at this point in the history
Add: ETH raw transaction details
  • Loading branch information
letmejustputthishere authored Sep 10, 2024
2 parents de3de90 + 29be9e0 commit 36e84de
Show file tree
Hide file tree
Showing 2 changed files with 112 additions and 30 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -12,48 +12,90 @@ import { MarkdownChipRow } from "/src/components/Chip/MarkdownChipRow";

## 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](/docs/current/developer-docs/smart-contracts/signatures/t-ecdsa).
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](/docs/current/developer-docs/smart-contracts/signatures/t-ecdsa). For this example, the transaction standard EIP1559 will be used.

A code example using the [`ethers-core`](https://github.com/gakonst/ethers-rs) Rust library to sign and format an ETH [EIP-1559](https://eips.ethereum.org/EIPS/eip-1559) transaction can be found below:
## Build a transaction

```rust
#[update(guard = "caller_is_not_anonymous")]
async fn sign_transaction(req: SignRequest) -> String {
use ethers_core::types::transaction::eip1559::Eip1559TransactionRequest;
use ethers_core::types::Signature;

const EIP1559_TX_ID: u8 = 2;

let caller = ic_cdk::caller();

let data = req.data.as_ref().map(|s| decode_hex(s));
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:

```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 {
chain_id: Some(nat_to_u64(&req.chain_id)),
from: None,
to: Some(
Address::from_str(&req.to)
.expect("failed to parse the destination address")
.into(),
),
gas: Some(nat_to_u256(&req.gas)),
value: Some(nat_to_u256(&req.value)),
nonce: Some(nat_to_u256(&req.nonce)),
data,
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(),
max_priority_fee_per_gas: Some(nat_to_u256(&req.max_priority_fee_per_gas)),
max_fee_per_gas: Some(nat_to_u256(&req.max_fee_per_gas)),
};

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](https://github.com/letmejustputthishere/chain-fusion-starter/blob/5b97edabc8d5dacac44795c3db005805804fdb46/packages/ic-evm-utils/src/eth_send_raw_transaction.rs#L43).

## 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.

```rust
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 (pubkey, signature) = pubkey_and_signature(&caller, txhash.to_vec()).await;
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, &pubkey),
v: y_parity(&txhash, &signature, &ecdsa_pub_key),
r: U256::from_big_endian(&signature[0..32]),
s: U256::from_big_endian(&signature[32..64]),
};
Expand All @@ -63,13 +105,15 @@ async fn sign_transaction(req: SignRequest) -> String {

format!("0x{}", hex::encode(&signed_tx_bytes))
}

```

[View the full code example on GitHub](https://github.com/letmejustputthishere/chain-fusion-starter/blob/5b97edabc8d5dacac44795c3db005805804fdb46/packages/ic-evm-utils/src/evm_signer.rs#L52).

Additional examples of signing transactions can be found in the [threshold ECDSA documentation](/docs/current/developer-docs/smart-contracts/signatures/signing-messages-t-ecdsa).

## Rebuild the raw transaction

## Next steps
## Submit transaction

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,45 @@ import { MarkdownChipRow } from "/src/components/Chip/MarkdownChipRow";

To submit transactions to the Ethereum network, first the transaction must be signed and formatted as an ETH transaction. [Learn more about signing transactions](signing-transactions.mdx).

Once you have your raw signed transaction, you can use the EVM RPC canister's RPC method `eth_sendRawTransaction`:
Once you have your raw signed transaction, you can either programmatically submit the transaction, or use the EVM RPC canister's RPC method `eth_sendRawTransaction`.

### Submitting transactions programmatically

Below is an example demonstrating how to submit a raw ETH transaction programmatically using Rust:

```Rust
pub async fn send_raw_transaction(
tx: String,
rpc_services: RpcServices,
evm_rpc: EvmRpcCanister,
) -> SendRawTransactionStatus {
let cycles = 10_000_000_000;

match evm_rpc
.eth_send_raw_transaction(rpc_services, None, tx, cycles)
.await
{
Ok((res,)) => match res {
MultiSendRawTransactionResult::Consistent(status) => match status {
SendRawTransactionResult::Ok(status) => status,
SendRawTransactionResult::Err(e) => {
ic_cdk::trap(format!("Error: {:?}", e).as_str());
}
},
MultiSendRawTransactionResult::Inconsistent(_) => {
ic_cdk::trap("Status is inconsistent");
}
},
Err(e) => ic_cdk::trap(format!("Error: {:?}", e).as_str()),
}
}
```

[View the full code example on GitHub](https://github.com/letmejustputthishere/chain-fusion-starter/blob/5b97edabc8d5dacac44795c3db005805804fdb46/packages/ic-evm-utils/src/eth_send_raw_transaction.rs#L211).

### Using the EVM RPC canister

To submit a raw transaction with the EVM RPC canister, make a call to the `eth_sendRawTransaction` method:

```
# Configuration
Expand Down

0 comments on commit 36e84de

Please sign in to comment.