Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,7 @@ class TestBarz {
}.build()
}.build()

eip7702Authority = Ethereum.Authority.newBuilder().apply {
eip7702Authorization = Ethereum.Authorization.newBuilder().apply {
address = "0x117BC8454756456A0f83dbd130Bb94D793D3F3F7"
}.build()
}
Expand Down
128 changes: 92 additions & 36 deletions rust/tw_evm/src/modules/tx_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ use crate::address::{Address, EvmAddress};
use crate::evm_context::EvmContext;
use crate::modules::authorization_signer::AuthorizationSigner;
use crate::transaction::access_list::{Access, AccessList};
use crate::transaction::authorization_list::{Authorization, AuthorizationList};
use crate::transaction::authorization_list::{
Authorization, AuthorizationList, SignedAuthorization,
};
use crate::transaction::transaction_eip1559::TransactionEip1559;
use crate::transaction::transaction_eip7702::TransactionEip7702;
use crate::transaction::transaction_non_typed::TransactionNonTyped;
Expand All @@ -23,8 +25,11 @@ use crate::transaction::UnsignedTransactionBox;
use std::marker::PhantomData;
use std::str::FromStr;
use tw_coin_entry::error::prelude::*;
use tw_encoding::hex::DecodeHex;
use tw_hash::H256;
use tw_keypair::ecdsa::secp256k1;
use tw_keypair::ecdsa::secp256k1::Signature;
use tw_keypair::KeyPairError;
use tw_memory::Data;
use tw_number::U256;
use tw_proto::Common::Proto::SigningError as CommonError;
Expand Down Expand Up @@ -363,16 +368,6 @@ impl<Context: EvmContext> TxBuilder<Context> {
payload: Data,
to_address: Option<Address>,
) -> SigningResult<Box<dyn UnsignedTransactionBox>> {
let signer_key = secp256k1::PrivateKey::try_from(input.private_key.as_ref())
.into_tw()
.context("Sender's private key must be provided to generate an EIP-7702 transaction")?;
let signer = Address::with_secp256k1_pubkey(&signer_key.public());
if to_address != Some(signer) {
return SigningError::err(SigningErrorType::Error_invalid_params).context(
"Unexpected 'accountAddress'. Expected to be the same as the signer address",
);
}

let nonce = U256::from_big_endian_slice(&input.nonce)
.into_tw()
.context("Invalid nonce")?;
Expand All @@ -381,6 +376,10 @@ impl<Context: EvmContext> TxBuilder<Context> {
.into_tw()
.context("Invalid gas limit")?;

let to = to_address
.or_tw_err(SigningErrorType::Error_invalid_params)
.context("'to' must be provided for `SetCode` transaction")?;

let max_inclusion_fee_per_gas =
U256::from_big_endian_slice(&input.max_inclusion_fee_per_gas)
.into_tw()
Expand All @@ -393,37 +392,14 @@ impl<Context: EvmContext> TxBuilder<Context> {
let access_list =
Self::parse_access_list(&input.access_list).context("Invalid access list")?;

let authority: Address = input
.eip7702_authority
.as_ref()
.or_tw_err(SigningErrorType::Error_invalid_params)
.context("'eip7702Authority' must be provided for `SetCode` transaction")?
.address
// Parse `Address`
.parse()
.into_tw()
.context("Invalid authority address")?;

let chain_id = U256::from_big_endian_slice(&input.chain_id)
.into_tw()
.context("Invalid chain ID")?;

let authorization = Authorization {
chain_id,
address: authority,
// `authorization.nonce` must be incremented by 1 over `transaction.nonce`.
nonce: nonce + 1,
};
let signed_authorization = AuthorizationSigner::sign(&signer_key, authorization)?;
let authorization_list = AuthorizationList::from(vec![signed_authorization]);
let authorization_list = Self::build_authorization_list(input, to)?;

Ok(TransactionEip7702 {
nonce,
max_inclusion_fee_per_gas,
max_fee_per_gas,
gas_limit,
// EIP-7702 transaction calls a smart contract function of the authorized address.
to: Some(signer),
to,
amount: eth_amount,
payload,
access_list,
Expand Down Expand Up @@ -664,4 +640,84 @@ impl<Context: EvmContext> TxBuilder<Context> {
let signer = Address::with_secp256k1_pubkey(&signer_key.public());
Ok(signer)
}

fn build_authorization_list(
input: &Proto::SigningInput,
destination: Address, // Field `destination` is only used for sanity check
) -> SigningResult<AuthorizationList> {
let eip7702_authorization = input
.eip7702_authorization
.as_ref()
.or_tw_err(SigningErrorType::Error_invalid_params)
.context("'eip7702Authorization' must be provided for `SetCode` transaction")?;

let address = eip7702_authorization
.address
.parse()
.into_tw()
.context("Invalid authority address")?;

let signed_authorization =
if let Some(other_auth_fields) = &eip7702_authorization.custom_signature {
// If field `custom_signature` is provided, it means that the authorization is already signed.
let chain_id = U256::from_big_endian_slice(&other_auth_fields.chain_id)
.into_tw()
.context("Invalid chain ID")?;
let nonce = U256::from_big_endian_slice(&other_auth_fields.nonce)
.into_tw()
.context("Invalid nonce")?;
let signature = Signature::try_from(
other_auth_fields
.signature
.decode_hex()
.map_err(|_| KeyPairError::InvalidSignature)?
.as_slice(),
)
.tw_err(SigningErrorType::Error_invalid_params)
.context("Invalid signature")?;

SignedAuthorization {
authorization: Authorization {
chain_id,
address,
nonce,
},
y_parity: signature.v(),
r: U256::from_big_endian(signature.r()),
s: U256::from_big_endian(signature.s()),
}
} else {
// If field `custom_signature` is not provided, the authorization will be signed with the provided private key, nonce and chainId
let signer_key = secp256k1::PrivateKey::try_from(input.private_key.as_ref())
.into_tw()
.context(
"Sender's private key must be provided to generate an EIP-7702 transaction",
)?;
let signer = Address::with_secp256k1_pubkey(&signer_key.public());
if destination != signer {
return SigningError::err(SigningErrorType::Error_invalid_params).context(
"Unexpected 'destination'. Expected to be the same as the signer address",
);
}

let chain_id = U256::from_big_endian_slice(&input.chain_id)
.into_tw()
.context("Invalid chain ID")?;
let nonce = U256::from_big_endian_slice(&input.nonce)
.into_tw()
.context("Invalid nonce")?;

AuthorizationSigner::sign(
&signer_key,
Authorization {
chain_id,
address,
// `authorization.nonce` must be incremented by 1 over `transaction.nonce`.
nonce: nonce + 1,
},
)?
};

Ok(AuthorizationList::from(vec![signed_authorization]))
}
}
4 changes: 2 additions & 2 deletions rust/tw_evm/src/transaction/transaction_eip7702.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ pub struct TransactionEip7702 {
pub max_inclusion_fee_per_gas: U256,
pub max_fee_per_gas: U256,
pub gas_limit: U256,
pub to: Option<Address>,
pub to: Address,
pub amount: U256,
pub payload: Data,
pub access_list: AccessList,
Expand Down Expand Up @@ -143,7 +143,7 @@ mod tests {
max_inclusion_fee_per_gas: U256::from(2_u32),
max_fee_per_gas: U256::from(3_u32),
gas_limit: U256::from(4_u32),
to: Some(Address::from_str("0x0101010101010101010101010101010101010101").unwrap()),
to: Address::from_str("0x0101010101010101010101010101010101010101").unwrap(),
amount: U256::from(5_u32),
payload: "0x1234".decode_hex().unwrap(),
access_list: AccessList::default(),
Expand Down
6 changes: 4 additions & 2 deletions rust/tw_evm/tests/barz.rs
Original file line number Diff line number Diff line change
Expand Up @@ -437,8 +437,9 @@ fn test_biz_eip7702_transfer() {
)),
// TWT token.
to_address: "0x4B0F1812e5Df2A09796481Ff14017e6005508003".into(),
eip7702_authority: Some(Proto::Authority {
eip7702_authorization: Some(Proto::Authorization {
address: "0x117BC8454756456A0f83dbd130Bb94D793D3F3F7".into(),
custom_signature: None,
}),
..Proto::SigningInput::default()
};
Expand Down Expand Up @@ -518,8 +519,9 @@ fn test_biz_eip7702_transfer_batch() {
wallet_type: Proto::SCWalletType::Biz,
}),
}),
eip7702_authority: Some(Proto::Authority {
eip7702_authorization: Some(Proto::Authorization {
address: "0x117BC8454756456A0f83dbd130Bb94D793D3F3F7".into(),
custom_signature: None,
}),
..Proto::SigningInput::default()
};
Expand Down
78 changes: 78 additions & 0 deletions rust/tw_tests/tests/chains/ethereum/ethereum_compile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ use tw_memory::test_utils::tw_data_helper::TWDataHelper;
use tw_memory::test_utils::tw_data_vector_helper::TWDataVectorHelper;
use tw_number::U256;
use tw_proto::Ethereum::Proto;
use tw_proto::Ethereum::Proto::{Authorization, AuthorizationCustomSignature, TransactionMode};
use tw_proto::TxCompiler::Proto as CompilerProto;
use tw_proto::{deserialize, serialize};

Expand Down Expand Up @@ -83,6 +84,83 @@ fn test_transaction_compiler_eth() {
assert_eq!(output.encoded.to_hex(), expected_encoded);
}

#[test]
fn test_transaction_compiler_eip7702() {
let transfer = Proto::mod_Transaction::Transfer {
amount: U256::encode_be_compact(0),
data: Cow::default(),
};
let input = Proto::SigningInput {
tx_mode: TransactionMode::SetCode,
nonce: U256::encode_be_compact(0),
chain_id: U256::encode_be_compact(17000), // Holesky Testnet
max_inclusion_fee_per_gas: U256::encode_be_compact(20_000_000_000),
max_fee_per_gas: U256::encode_be_compact(20_000_000_000),
gas_limit: U256::encode_be_compact(50_000),
to_address: "0x18356de2Bc664e45dD22266A674906574087Cf54".into(),
transaction: Some(Proto::Transaction {
transaction_oneof: Proto::mod_Transaction::OneOftransaction_oneof::transfer(transfer),
}),
eip7702_authorization: Some(Authorization {
address: "0x3535353535353535353535353535353535353535".into(),
custom_signature: Some(AuthorizationCustomSignature {
nonce: U256::encode_be_compact(1),
chain_id: U256::encode_be_compact(0), // chain id 0 means any chain
signature: "08b7bfc6bcaca1dfd7a295c3a6908fea545a62958cf2c048639224a8bede8d1f56dce327574529c56f7f3db308a34d44e2312a11c89db8af99371d4fe490e55f00".into(),
}),
}),
..Proto::SigningInput::default()
};

// Step 2: Obtain preimage hash
let input_data = TWDataHelper::create(serialize(&input).unwrap());
let preimage_data = TWDataHelper::wrap(unsafe {
tw_transaction_compiler_pre_image_hashes(CoinType::Ethereum as u32, input_data.ptr())
})
.to_vec()
.expect("!tw_transaction_compiler_pre_image_hashes returned nullptr");

let preimage: CompilerProto::PreSigningOutput =
deserialize(&preimage_data).expect("Coin entry returned an invalid output");

assert_eq!(preimage.error, SigningErrorType::OK);
assert!(preimage.error_message.is_empty());
assert_eq!(
preimage.data_hash.to_hex(),
"d5f3f94ff2c70686623c71ef9e367f341a3ef6691d02e9e9db671c9281d56432"
);

// Step 3: Compile transaction info

// Simulate signature, normally obtained from signature server
let signature = "601148c0af0108fe9f051ca5e9bd5c0b9e7c4cc7385b1625eeae0ec4bbe5537960d8c76d98279329d1ef9d79002ccddd53e522c0e821003590e74eba51a1c95601".decode_hex().unwrap();
let public_key = "04ec6632291fbfe6b47826a1c4b195f8b112a7e147e8a5a15fb0f7d7de022652e7f65b97a57011c09527688e23c07ae9c83a2cae2e49edba226e7c43f0baa7296d".decode_hex().unwrap();

let signatures = TWDataVectorHelper::create([signature]);
let public_keys = TWDataVectorHelper::create([public_key]);

let input_data = TWDataHelper::create(serialize(&input).unwrap());
let output_data = TWDataHelper::wrap(unsafe {
tw_transaction_compiler_compile(
CoinType::Ethereum as u32,
input_data.ptr(),
signatures.ptr(),
public_keys.ptr(),
)
})
.to_vec()
.expect("!tw_transaction_compiler_compile returned nullptr");

let output: Proto::SigningOutput =
deserialize(&output_data).expect("Coin entry returned an invalid output");

assert_eq!(output.error, SigningErrorType::OK);
assert!(output.error_message.is_empty());
let expected_encoded = "04f8cc824268808504a817c8008504a817c80082c3509418356de2bc664e45dd22266a674906574087cf548080c0f85cf85a809435353535353535353535353535353535353535350180a008b7bfc6bcaca1dfd7a295c3a6908fea545a62958cf2c048639224a8bede8d1fa056dce327574529c56f7f3db308a34d44e2312a11c89db8af99371d4fe490e55f01a0601148c0af0108fe9f051ca5e9bd5c0b9e7c4cc7385b1625eeae0ec4bbe55379a060d8c76d98279329d1ef9d79002ccddd53e522c0e821003590e74eba51a1c956";
// Successfully broadcasted: https://holesky.etherscan.io/tx/0x1c349e81dd135bfe104c8051fa9e668d6f8c0323abe852b1d1522b772932c0ec
assert_eq!(output.encoded.to_hex(), expected_encoded);
}

#[test]
fn test_transaction_compiler_plan_not_supported() {
let transfer = Proto::mod_Transaction::Transfer {
Expand Down
20 changes: 16 additions & 4 deletions src/proto/Ethereum.proto
Original file line number Diff line number Diff line change
Expand Up @@ -194,10 +194,22 @@ message Access {
repeated bytes stored_keys = 2;
}

// [EIP-7702](https://eips.ethereum.org/EIPS/eip-7702) authority.
message Authority {
// [EIP-7702](https://eips.ethereum.org/EIPS/eip-7702) authorization.
message Authorization {
// Address to be authorized, a smart contract address.
string address = 2;
// If custom_signature isn't provided, the authorization will be signed with the provided private key, nonce and chainId
AuthorizationCustomSignature custom_signature = 3;
}

// [EIP-7702](https://eips.ethereum.org/EIPS/eip-7702) authorization.
message AuthorizationCustomSignature {
// Chain id (uint256, serialized big endian).
bytes chain_id = 1;
// Nonce, the nonce of authority.
bytes nonce = 2;
// The signature, Hex-encoded.
string signature = 3;
}

// Smart Contract Wallet type.
Expand Down Expand Up @@ -261,10 +273,10 @@ message SigningInput {
// Used in `TransactionMode::Enveloped` only.
repeated Access access_list = 12;

// A smart contract to which we’re delegating to.
// EIP7702 authorization.
// Used in `TransactionMode::SetOp` only.
// Currently, we support delegation to only one authority at a time.
Authority eip7702_authority = 15;
Authorization eip7702_authorization = 15;
}

// Result containing the signed and encoded transaction.
Expand Down
Loading