ethereum: support payment requests for eip1559
What changed, and why it matters
This commit adds support for EIP-1559 Ethereum transactions in the BitBox02's payment-request (swap) feature. It lets users verify and sign Ethereum and ERC-20 token swaps that include a merchant payment request, similar to what already existed for Bitcoin. The change is a feature addition, not a fix for a known vulnerability.
Review the payment_request::validate_eth implementation and the new signing paths to ensure the source-side amount normalization, address derivation checks, and signature verification are correct. Run the new unit tests and perform manual QA on EIP-1559 swap flows for both native ETH and ERC-20 tokens.
Security signals we found
New user-facing signing path for EIP-1559 payment requests
Payment-request validation now covers both BTC and ETH/ERC-20 source assets
Plain ETH swaps require empty transaction data
ERC-20 swaps decode transfer(...) recipient and amount for validation
Validation failures show an 'Invalid payment request' status and abort signing
Refactored common validation logic into validate_common()
Evidence from the diff
The patch extends the existing payment-request validation framework to Ethereum. It exposes the ethereum and ethereum::params modules, adds a slip44() helper, and introduces validate_eth() in payment_request.rs. In ethereum/sign.rs, EIP-1559 transactions can now carry an optional BtcPaymentRequestRequest; for plain ETH and standard ERC-20 transfers the device verifies the recipient/value against the signed payment request before signing. Legacy transactions do not support payment requests. The commit includes unit tests for plain ETH and known ERC-20 swap flows, plus a negative test for non-standard contract shapes.
Changed components
src/rust/bitbox02-rust/src/hww/api.rssrc/rust/bitbox02-rust/src/hww/api/ethereum.rssrc/rust/bitbox02-rust/src/hww/api/ethereum/params.rssrc/rust/bitbox02-rust/src/hww/api/ethereum/sign.rssrc/rust/bitbox02-rust/src/hww/api/payment_request.rsInspect captured patch +504 / −28
diff --git a/src/rust/bitbox02-rust/src/hww/api.rs b/src/rust/bitbox02-rust/src/hww/api.rs
index e3f5375..624ec21 100644
--- a/src/rust/bitbox02-rust/src/hww/api.rs
+++ b/src/rust/bitbox02-rust/src/hww/api.rs
@@ -5,7 +5,7 @@ use crate::pb;
pub(super) mod error;
#[cfg(feature = "app-ethereum")]
-mod ethereum;
+pub mod ethereum;
#[cfg(any(feature = "app-bitcoin", feature = "app-litecoin"))]
pub mod bitcoin;
diff --git a/src/rust/bitbox02-rust/src/hww/api/ethereum.rs b/src/rust/bitbox02-rust/src/hww/api/ethereum.rs
index 17723f2..7768144 100644
--- a/src/rust/bitbox02-rust/src/hww/api/ethereum.rs
+++ b/src/rust/bitbox02-rust/src/hww/api/ethereum.rs
@@ -8,7 +8,7 @@ compile_error!(
mod address;
mod amount;
mod keypath;
-mod params;
+pub mod params;
mod pubrequest;
mod sighash;
mod sign;
diff --git a/src/rust/bitbox02-rust/src/hww/api/ethereum/params.rs b/src/rust/bitbox02-rust/src/hww/api/ethereum/params.rs
index 702a7e9..1636426 100644
--- a/src/rust/bitbox02-rust/src/hww/api/ethereum/params.rs
+++ b/src/rust/bitbox02-rust/src/hww/api/ethereum/params.rs
@@ -21,6 +21,14 @@ pub struct Params {
pub unit: &'static str,
}
+impl Params {
+ /// Returns the SLIP44 coin type:
+ /// https://github.com/satoshilabs/slips/blob/master/slip-0044.md
+ pub fn slip44(&self) -> u32 {
+ self.bip44_coin - HARDENED
+ }
+}
+
// If there should ever be two networks with the same chain ID, the `get()` function should prompt
// the user to choose the network they want to interact with.
const PARAMS: &[Params] = &[
diff --git a/src/rust/bitbox02-rust/src/hww/api/ethereum/sign.rs b/src/rust/bitbox02-rust/src/hww/api/ethereum/sign.rs
index 31242cc..0cf43af 100644
--- a/src/rust/bitbox02-rust/src/hww/api/ethereum/sign.rs
+++ b/src/rust/bitbox02-rust/src/hww/api/ethereum/sign.rs
@@ -1,5 +1,6 @@
// SPDX-License-Identifier: Apache-2.0
+use super::super::payment_request;
use super::Error;
use super::amount::{Amount, calculate_percentage};
use super::params::Params;
@@ -164,6 +165,27 @@ async fn verify_standard_total_fee(
Ok(())
}
+/// Show the shared payment-request recipient UI and validate it against the
+/// source-side facts parsed from the ETH-like transaction.
+async fn verify_payment_request_recipient(
+ hal: &mut impl crate::hal::Hal,
+ params: &Params,
+ payment_request: &pb::BtcPaymentRequestRequest,
+ displayed_source_amount: &str,
+ output_value: &BigUint,
+ output_address: &str,
+) -> Result<(), Error> {
+ payment_request::user_verify(hal, payment_request, displayed_source_amount).await?;
+ match payment_request::validate_eth(hal, params, payment_request, output_value, output_address)
+ {
+ Ok(()) => Ok(()),
+ Err(_) => {
+ hal.ui().status("Invalid\npayment request", true).await;
+ Err(Error::InvalidInput)
+ }
+ }
+}
+
// For legacy transactions: `fee = gas limit * gas price`
// For 1559 transactions: `fee = gas limit * max fee per gas` where max fee per gas is composed of the base fee + priority fee
// In both instances we show the user the max possible fee, but the actual fee paid at execution might be lower
@@ -247,9 +269,34 @@ async fn verify_erc20_transaction(
params: &Params,
erc20_recipient: [u8; 20],
erc20_value: BigUint,
+ payment_request: Option<&pb::BtcPaymentRequestRequest>,
) -> Result<(), Error> {
let erc20_params = erc20_params::get(params.chain_id, parse_recipient(request.recipient())?);
let recipient_address = super::address::from_pubkey_hash(&erc20_recipient, request.case()?);
+ if let Some(payment_request) = payment_request {
+ let token_params = erc20_params.ok_or(Error::InvalidInput)?;
+ let displayed_source_amount = Amount {
+ unit: token_params.unit,
+ decimals: token_params.decimals as _,
+ value: erc20_value.clone(),
+ }
+ .format();
+
+ // For ERC20 transfers, the tx recipient is the token contract. The
+ // actual swap deposit address is the decoded `transfer(...)` recipient.
+ verify_payment_request_recipient(
+ hal,
+ params,
+ payment_request,
+ &displayed_source_amount,
+ &erc20_value,
+ &recipient_address,
+ )
+ .await?;
+ verify_erc20_total_fee(hal, request, params, &displayed_source_amount).await?;
+ return Ok(());
+ }
+
let recipient_address_display = super::address::format_display_address(&recipient_address);
let (formatted_value, formatted_total) = match erc20_params {
Some(erc20_params) => {
@@ -283,11 +330,41 @@ async fn verify_standard_transaction(
hal: &mut impl crate::hal::Hal,
request: &Transaction<'_>,
params: &Params,
+ payment_request: Option<&pb::BtcPaymentRequestRequest>,
) -> Result<(), Error> {
let recipient = parse_recipient(request.recipient())?;
let data_length = request.data_length();
+ if let Some(payment_request) = payment_request {
+ if !request.data().is_empty() || data_length > 0 {
+ return Err(Error::InvalidInput);
+ }
+
+ let address = super::address::from_pubkey_hash(&recipient, request.case()?);
+ let amount_value = BigUint::from_bytes_be(request.value());
+ let displayed_source_amount = Amount {
+ unit: params.unit,
+ decimals: WEI_DECIMALS,
+ value: amount_value.clone(),
+ }
+ .format();
+
+ // Native ETH transfers encode the real deposit address directly in the
+ // tx recipient field.
+ verify_payment_request_recipient(
+ hal,
+ params,
+ payment_request,
+ &displayed_source_amount,
+ &amount_value,
+ &address,
+ )
+ .await?;
+ verify_standard_total_fee(hal, request, params, &amount_value).await?;
+ return Ok(());
+ }
+
if !request.data().is_empty() || data_length > 0 {
hal.ui()
.confirm(&ConfirmParams {
@@ -434,10 +511,24 @@ pub async fn _process(
return Err(Error::InvalidInput);
}
+ // A payment request only changes how the recipient/source
+ // asset is verified within the existing ETH/ERC20 flows.
+ let payment_request = match request {
+ Transaction::Eip1559(eip1559) => eip1559.payment_request.as_ref(),
+ Transaction::Legacy(_) => None,
+ };
if let Some((erc20_recipient, erc20_value)) = parse_erc20(request) {
- verify_erc20_transaction(hal, request, ¶ms, erc20_recipient, erc20_value).await?;
+ verify_erc20_transaction(
+ hal,
+ request,
+ ¶ms,
+ erc20_recipient,
+ erc20_value,
+ payment_request,
+ )
+ .await?;
} else {
- verify_standard_transaction(hal, request, ¶ms).await?;
+ verify_standard_transaction(hal, request, ¶ms, payment_request).await?;
}
hal.ui().status("Transaction\nconfirmed", true).await;
@@ -505,8 +596,42 @@ mod tests {
use util::bb02_async::block_on;
use util::bip32::HARDENED;
+ use super::super::super::payment_request;
+ use super::super::address;
use super::super::sighash::tests::{clear_chunk_responder, setup_chunk_responder};
+ // Base payment request fixture for ETH-side swap tests.
+ fn make_eth_swap_payment_request() -> pb::BtcPaymentRequestRequest {
+ pb::BtcPaymentRequestRequest {
+ recipient_name: "Test Merchant".into(),
+ memos: vec![pb::btc_payment_request_request::Memo {
+ memo: Some(pb::btc_payment_request_request::memo::Memo::CoinPurchaseMemo(
+ pb::btc_payment_request_request::memo::CoinPurchaseMemo {
+ coin_type: 60,
+ amount: "0.25 ETH".into(),
+ address: "0x773A77b9D32589be03f9132AF759e294f7851be9".into(),
+ address_derivation: Some(
+ pb::btc_payment_request_request::memo::coin_purchase_memo::AddressDerivation::Eth(
+ pb::btc_payment_request_request::memo::coin_purchase_memo::EthAddressDerivation {
+ keypath: vec![
+ 44 + HARDENED,
+ 60 + HARDENED,
+ 0 + HARDENED,
+ 0,
+ 0,
+ ],
+ },
+ ),
+ ),
+ },
+ )),
+ }],
+ nonce: vec![],
+ total_amount: 0,
+ signature: vec![],
+ }
+ }
+
#[test]
pub fn test_parse_recipient() {
assert_eq!(
@@ -972,6 +1097,204 @@ mod tests {
);
}
+ #[test]
+ fn test_process_eip1559_payment_request_invalid_contract_shape() {
+ // Payment requests only support plain ETH transfers or standard ERC20 transfers.
+ const KEYPATH: &[u32] = &[44 + HARDENED, 60 + HARDENED, 0 + HARDENED, 0, 0];
+
+ let mut mock_hal = TestingHal::new();
+ assert_eq!(
+ block_on(process(
+ &mut mock_hal,
+ &Transaction::Eip1559(&pb::EthSignEip1559Request {
+ keypath: KEYPATH.to_vec(),
+ nonce: hex!("1fdc").to_vec(),
+ max_priority_fee_per_gas: hex!("3b9aca00").to_vec(),
+ max_fee_per_gas: hex!("0165a0bc00").to_vec(),
+ gas_limit: hex!("5208").to_vec(),
+ recipient: hex!("04f264cf34440313b4a0192a352814fbe927b885").to_vec(),
+ value: hex!("075cf1259e9c4000").to_vec(),
+ data: b"foo bar".to_vec(),
+ host_nonce_commitment: None,
+ chain_id: 1,
+ address_case: pb::EthAddressCase::Mixed as _,
+ data_length: 0,
+ payment_request: Some(Default::default()),
+ }),
+ )),
+ Err(Error::InvalidInput)
+ );
+ assert_eq!(
+ mock_hal.ui.screens,
+ vec![Screen::Confirm {
+ title: "".into(),
+ body: "Sign transaction on\n\nEthereum".into(),
+ longtouch: false,
+ }]
+ );
+ }
+
+ #[test]
+ fn test_process_eip1559_payment_request_plain_eth() {
+ // Native ETH swaps use the tx recipient/value as the signed source-side output.
+ const KEYPATH: &[u32] = &[44 + HARDENED, 60 + HARDENED, 0 + HARDENED, 0, 0];
+
+ // 0.530564 ETH in wei
+ let value = hex!("075cf1259e9c4000");
+ // value left padded to 32 bytes
+ let output_value = hex!("000000000000000000000000000000000000000000000000075cf1259e9c4000");
+ // recipient address
+ let output_address = address::from_pubkey_hash(
+ &hex!("04f264cf34440313b4a0192a352814fbe927b885"),
+ pb::EthAddressCase::Mixed,
+ );
+ let mut payment_request = make_eth_swap_payment_request();
+ payment_request::tst_sign_payment_request_eth(
+ 60,
+ &mut payment_request,
+ &output_value,
+ &output_address,
+ );
+
+ let expected_screens = vec![
+ Screen::Confirm {
+ title: "".into(),
+ body: "Sign transaction on\n\nEthereum".into(),
+ longtouch: false,
+ },
+ Screen::Recipient {
+ recipient: "Test Merchant".into(),
+ amount: "0.530564 ETH".into(),
+ },
+ Screen::Confirm {
+ title: "SWAP".into(),
+ body: "0.530564 ETH\nto\n0.25 ETH".into(),
+ longtouch: false,
+ },
+ Screen::Confirm {
+ title: "Receive to".into(),
+ body: "ETH account #1".into(),
+ longtouch: false,
+ },
+ Screen::TotalFee {
+ total: "0.53069 ETH".into(),
+ fee: "0.000126 ETH".into(),
+ longtouch: true,
+ },
+ Screen::Status {
+ title: "Transaction\nconfirmed".into(),
+ success: true,
+ },
+ ];
+
+ let mut mock_hal = TestingHal::new();
+ assert_eq!(
+ block_on(process(
+ &mut mock_hal,
+ &Transaction::Eip1559(&pb::EthSignEip1559Request {
+ keypath: KEYPATH.to_vec(),
+ nonce: hex!("1fdc").to_vec(),
+ max_priority_fee_per_gas: b"".to_vec(),
+ max_fee_per_gas: hex!("0165a0bc00").to_vec(),
+ gas_limit: hex!("5208").to_vec(),
+ recipient: hex!("04f264cf34440313b4a0192a352814fbe927b885").to_vec(),
+ value: value.to_vec(),
+ data: b"".to_vec(),
+ host_nonce_commitment: None,
+ chain_id: 1,
+ address_case: pb::EthAddressCase::Mixed as _,
+ data_length: 0,
+ payment_request: Some(payment_request),
+ }),
+ )),
+ Ok(Response::Sign(pb::EthSignResponse {
+ signature: hex!("289111770dc067895780de3e9b30454e331ba6661f046e9e26431576d7f08a496ffe6deffb07dd8d4713d8c523b6c33b53dd6ef2dc9c394d6e21f64307d2bcf001")
+ .to_vec()
+ }))
+ );
+ assert_eq!(mock_hal.ui.screens, expected_screens);
+ }
+
+ #[test]
+ fn test_process_eip1559_payment_request_known_erc20() {
+ // Known ERC20 swaps decode the transfer recipient/amount and show the token unit.
+ const KEYPATH: &[u32] = &[44 + HARDENED, 60 + HARDENED, 0 + HARDENED, 0, 0];
+ // function selector + recipient address (left padded) + token amount
+ let data = hex!(
+ "a9059cbb000000000000000000000000e6ce0a092a99700cd4ccccbb1fedc39cf53e6330000000000000000000000000000000000000000000000000000000000365c040"
+ );
+ let output_value: [u8; 32] = data[36..68].try_into().unwrap();
+ let output_address = address::from_pubkey_hash(
+ &hex!("e6ce0a092a99700cd4ccccbb1fedc39cf53e6330"),
+ pb::EthAddressCase::Mixed,
+ );
+ let mut payment_request = make_eth_swap_payment_request();
+ payment_request::tst_sign_payment_request_eth(
+ 60,
+ &mut payment_request,
+ &output_value,
+ &output_address,
+ );
+
+ let expected_screens = vec![
+ Screen::Confirm {
+ title: "".into(),
+ body: "Sign transaction on\n\nEthereum".into(),
+ longtouch: false,
+ },
+ Screen::Recipient {
+ recipient: "Test Merchant".into(),
+ amount: "57 USDT".into(),
+ },
+ Screen::Confirm {
+ title: "SWAP".into(),
+ body: "57 USDT\nto\n0.25 ETH".into(),
+ longtouch: false,
+ },
+ Screen::Confirm {
+ title: "Receive to".into(),
+ body: "ETH account #1".into(),
+ longtouch: false,
+ },
+ Screen::TotalFee {
+ total: "57 USDT".into(),
+ fee: "0.0012658164 ETH".into(),
+ longtouch: true,
+ },
+ Screen::Status {
+ title: "Transaction\nconfirmed".into(),
+ success: true,
+ },
+ ];
+
+ let mut mock_hal = TestingHal::new();
+ assert_eq!(
+ block_on(process(
+ &mut mock_hal,
+ &Transaction::Eip1559(&pb::EthSignEip1559Request {
+ keypath: KEYPATH.to_vec(),
+ nonce: hex!("2367").to_vec(),
+ max_priority_fee_per_gas: hex!("3b9aca00").to_vec(),
+ max_fee_per_gas: hex!("027aca1a80").to_vec(),
+ gas_limit: hex!("01d048").to_vec(),
+ recipient: hex!("dac17f958d2ee523a2206206994597c13d831ec7").to_vec(),
+ value: b"".to_vec(),
+ data: data.to_vec(),
+ host_nonce_commitment: None,
+ chain_id: 1,
+ address_case: pb::EthAddressCase::Mixed as _,
+ data_length: 0,
+ payment_request: Some(payment_request),
+ }),
+ )),
+ Ok(Response::Sign(pb::EthSignResponse {
+ signature: hex!("3162487880abdea1f352d9a4e3d56066f122f04ff112117c8ca3cd220f1666302dacd5e5e8da4cd39704e33443a9a7f32602d332bb52567c2e34aafe9ed48feb01")
+ .to_vec()
+ }))
+ );
+ assert_eq!(mock_hal.ui.screens, expected_screens);
+ }
+
/// ERC20 transaction: recipient is an ERC20 contract address, and
/// the data field contains an ERC20 transfer method invocation.
#[test]
diff --git a/src/rust/bitbox02-rust/src/hww/api/payment_request.rs b/src/rust/bitbox02-rust/src/hww/api/payment_request.rs
index fc841f2..3551140 100644
--- a/src/rust/bitbox02-rust/src/hww/api/payment_request.rs
+++ b/src/rust/bitbox02-rust/src/hww/api/payment_request.rs
@@ -5,6 +5,8 @@ use crate::hal::ui::ConfirmParams;
use crate::pb;
use alloc::vec::Vec;
+#[cfg(feature = "app-ethereum")]
+use num_bigint::BigUint;
use pb::btc_payment_request_request::{Memo, memo};
@@ -294,6 +296,22 @@ pub fn tst_sign_payment_request_btc(
);
}
+#[cfg(feature = "testing")]
+#[allow(dead_code)]
+pub fn tst_sign_payment_request_eth(
+ source_coin_type: u32,
+ payment_request: &mut pb::BtcPaymentRequestRequest,
+ total_value_bytes: &[u8; 32],
+ output_address: &str,
+) {
+ tst_sign_payment_request(
+ source_coin_type,
+ payment_request,
+ total_value_bytes,
+ output_address,
+ );
+}
+
#[cfg(feature = "testing")]
#[allow(dead_code)]
fn tst_sign_payment_request(
@@ -336,7 +354,10 @@ pub fn validate_btc(
output_address: &str,
) -> Result<(), ValidationError> {
let total_value_bytes = total_value.to_le_bytes();
- validate(
+ if total_value_bytes.as_slice() != payment_request.total_amount.to_le_bytes().as_slice() {
+ return Err(ValidationError::Other);
+ }
+ validate_common(
hal,
coin_params.slip44(),
payment_request,
@@ -345,6 +366,30 @@ pub fn validate_btc(
)
}
+/// Validate an ETH/EVM payment request against the parsed source-side transaction.
+#[cfg(feature = "app-ethereum")]
+pub fn validate_eth(
+ hal: &mut impl crate::hal::Hal,
+ coin_params: &super::ethereum::params::Params,
+ payment_request: &pb::BtcPaymentRequestRequest,
+ output_value: &BigUint,
+ output_address: &str,
+) -> Result<(), ValidationError> {
+ let output_value = output_value.to_bytes_be();
+ if output_value.len() > 32 {
+ return Err(ValidationError::Other);
+ }
+ let mut output_value_padded = [0u8; 32];
+ output_value_padded[32 - output_value.len()..].copy_from_slice(&output_value);
+ validate_common(
+ hal,
+ coin_params.slip44(),
+ payment_request,
+ &output_value_padded,
+ output_address,
+ )
+}
+
/// Validate that the parsed source-side transaction matches the signed payment request.
///
/// The caller provides the normalized source-side facts that are actually being
@@ -354,7 +399,7 @@ pub fn validate_btc(
/// Destination ownership checks for `CoinPurchaseMemo.address` still happen
/// here, because they are derived from the memo's keypath metadata rather than
/// from the source transaction itself.
-fn validate(
+fn validate_common(
#[cfg_attr(not(feature = "app-ethereum"), allow(unused_variables))] hal: &mut impl crate::hal::Hal,
source_coin_type: u32,
payment_request: &pb::BtcPaymentRequestRequest,
@@ -363,9 +408,6 @@ fn validate(
) -> Result<(), ValidationError> {
let identity =
find_identity(&payment_request.recipient_name).ok_or(ValidationError::UnknownRecipient)?;
- if output_value != payment_request.total_amount.to_le_bytes().as_slice() {
- return Err(ValidationError::Other);
- }
if !payment_request.nonce.is_empty() {
// No support for nonces yet.
return Err(ValidationError::Other);
@@ -453,6 +495,8 @@ mod tests {
use crate::hal::testing::TestingHal;
use crate::hal::testing::ui::Screen;
use crate::hww::api::bitcoin::params;
+ #[cfg(feature = "app-ethereum")]
+ use crate::hww::api::ethereum::params as eth_params;
use util::bb02_async::block_on;
fn make_text_memo(note: &str) -> Memo {
@@ -677,7 +721,7 @@ mod tests {
);
assert!(
- validate(
+ validate_common(
&mut mock_hal,
source_coin_type,
&payment_request,
@@ -713,7 +757,7 @@ mod tests {
address,
);
assert!(
- validate(
+ validate_common(
&mut mock_hal,
source_coin_type,
&payment_request,
@@ -783,7 +827,7 @@ mod tests {
);
assert!(
- validate(
+ validate_common(
&mut mock_hal,
source_coin_type,
&payment_request,
@@ -853,7 +897,7 @@ mod tests {
);
assert!(
- validate(
+ validate_common(
&mut mock_hal,
source_coin_type,
&payment_request,
@@ -889,7 +933,7 @@ mod tests {
address,
);
assert!(matches!(
- validate(
+ validate_common(
&mut mock_hal,
source_coin_type,
&payment_request,
@@ -922,7 +966,7 @@ mod tests {
address,
);
assert!(matches!(
- validate(
+ validate_common(
&mut TestingHal::new(),
source_coin_type,
&payment_request,
@@ -955,7 +999,7 @@ mod tests {
address,
);
assert!(matches!(
- validate(
+ validate_common(
&mut mock_hal,
source_coin_type,
&payment_request,
@@ -1013,7 +1057,7 @@ mod tests {
address,
);
assert!(matches!(
- validate(
+ validate_common(
&mut mock_hal,
source_coin_type,
&payment_request,
@@ -1056,7 +1100,7 @@ mod tests {
address,
);
assert!(matches!(
- validate(
+ validate_common(
&mut mock_hal,
source_coin_type,
&payment_request,
@@ -1098,7 +1142,7 @@ mod tests {
address,
);
assert!(matches!(
- validate(
+ validate_common(
&mut mock_hal,
source_coin_type,
&payment_request,
@@ -1140,7 +1184,7 @@ mod tests {
address,
);
assert!(matches!(
- validate(
+ validate_common(
&mut mock_hal,
source_coin_type,
&payment_request,
@@ -1182,7 +1226,7 @@ mod tests {
address,
);
assert!(matches!(
- validate(
+ validate_common(
&mut mock_hal,
source_coin_type,
&payment_request,
@@ -1240,7 +1284,7 @@ mod tests {
address,
);
assert!(matches!(
- validate(
+ validate_common(
&mut mock_hal,
source_coin_type,
&payment_request,
@@ -1260,7 +1304,7 @@ mod tests {
signature: vec![],
};
assert!(matches!(
- validate(
+ validate_common(
&mut mock_hal,
source_coin_type,
&payment_request,
@@ -1279,11 +1323,11 @@ mod tests {
signature: vec![],
};
assert!(matches!(
- validate(
+ validate_btc(
&mut mock_hal,
- source_coin_type,
+ params::get(pb::BtcCoin::Tbtc),
&payment_request,
- (value + 1).to_le_bytes().as_ref(),
+ value + 1,
address
),
Err(ValidationError::Other)
@@ -1298,7 +1342,7 @@ mod tests {
signature: vec![],
};
assert!(matches!(
- validate(
+ validate_common(
&mut mock_hal,
source_coin_type,
&payment_request,
@@ -1317,7 +1361,7 @@ mod tests {
signature: vec![],
};
assert!(matches!(
- validate(
+ validate_common(
&mut mock_hal,
source_coin_type,
&payment_request,
@@ -1463,6 +1507,107 @@ mod tests {
);
}
+ #[cfg(feature = "app-ethereum")]
+ #[test]
+ fn test_validate_eth() {
+ let mut mock_hal = TestingHal::new();
+ let params = eth_params::Params {
+ coin: Some(pb::EthCoin::Eth),
+ bip44_coin: 60 + util::bip32::HARDENED,
+ chain_id: 1,
+ name: "Ethereum",
+ unit: "ETH",
+ };
+ let output_value = hex!("000000000000000000000000000000000000000000000000075cf1259e9c4000");
+ let output_address = "0x04F264Cf34440313B4A0192A352814FBe927b885";
+ let mut payment_request = pb::BtcPaymentRequestRequest {
+ recipient_name: "Test Merchant".into(),
+ memos: vec![make_coin_purchase_memo(
+ 60,
+ "0.25 ETH",
+ "0x773A77b9D32589be03f9132AF759e294f7851be9",
+ Some(dummy_eth_address_derivation(/*valid=*/ true)),
+ )],
+ nonce: vec![],
+ total_amount: 0,
+ signature: vec![],
+ };
+ tst_sign_payment_request_eth(
+ params.slip44(),
+ &mut payment_request,
+ &output_value,
+ output_address,
+ );
+
+ // Fully normalized 32-byte EVM amount bytes validate as-is.
+ assert!(
+ validate_eth(
+ &mut mock_hal,
+ ¶ms,
+ &payment_request,
+ &BigUint::from_bytes_be(&output_value),
+ output_address,
+ )
+ .is_ok()
+ );
+
+ // Native ETH values may be shorter than 32 bytes.
+ assert!(matches!(
+ validate_eth(
+ &mut TestingHal::new(),
+ ¶ms,
+ &payment_request,
+ &BigUint::from_bytes_be(&output_value[24..]),
+ output_address,
+ ),
+ Ok(())
+ ));
+
+ let wrong_output_value =
+ hex!("000000000000000000000000000000000000000000000000075cf1259e9c4001");
+ // Wrong source amount must produce wrong signature.
+ assert!(matches!(
+ validate_eth(
+ &mut TestingHal::new(),
+ ¶ms,
+ &payment_request,
+ &BigUint::from_bytes_be(&wrong_output_value),
+ output_address,
+ ),
+ Err(ValidationError::InvalidSignature)
+ ));
+
+ //Wrong source-side deposit address must produce wrong signature.
+ assert!(matches!(
+ validate_eth(
+ &mut TestingHal::new(),
+ ¶ms,
+ &payment_request,
+ &BigUint::from_bytes_be(&output_value),
+ "0x1111111111111111111111111111111111111111",
+ ),
+ Err(ValidationError::InvalidSignature)
+ ));
+
+ // Wrong source coin type must produce wrong signature.
+ assert!(matches!(
+ validate_eth(
+ &mut TestingHal::new(),
+ ð_params::Params {
+ coin: Some(pb::EthCoin::Eth),
+ bip44_coin: 1 + util::bip32::HARDENED,
+ chain_id: 1,
+ name: "Ethereum",
+ unit: "ETH",
+ },
+ &payment_request,
+ &BigUint::from_bytes_be(&output_value),
+ output_address,
+ ),
+ Err(ValidationError::InvalidSignature)
+ ));
+ }
+
#[cfg(all(feature = "app-litecoin", feature = "app-ethereum"))]
#[test]
fn test_user_verify_swap_invalid() {
Why this scored 28/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.