diff --git a/CHANGELOG-npm.md b/CHANGELOG-npm.md index 3dcc41e..39bfe1f 100644 --- a/CHANGELOG-npm.md +++ b/CHANGELOG-npm.md @@ -1,5 +1,8 @@ # Changelog +## [Unreleased] +- eth: add support for streaming transactions with large data + ## 0.12.0 - btc: add support for OP_RETURN outputs - add `changePassword()` to change the device password (firmware >=9.25.0) diff --git a/CHANGELOG-rust.md b/CHANGELOG-rust.md index 53ce750..0a3b156 100644 --- a/CHANGELOG-rust.md +++ b/CHANGELOG-rust.md @@ -1,5 +1,8 @@ # Changelog +## [Unreleased] +- eth: add support for streaming transactions with large data + ## 0.11.0 - btc: add support for OP_RETURN outputs - add `change_password()` to change the device password (firmware >=9.25.0) diff --git a/Cargo.lock b/Cargo.lock index f5d0cff..475f82f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -162,7 +162,7 @@ checksum = "d965446196e3b7decd44aa7ee49e31d630118f90ef12f97900f262eb915c951d" [[package]] name = "bitbox-api" -version = "0.11.0" +version = "0.12.0" dependencies = [ "async-trait", "base32", @@ -188,6 +188,7 @@ dependencies = [ "serde-wasm-bindgen", "serde_json", "thiserror", + "tiny-keccak", "tokio", "url", "wasm-bindgen", @@ -403,6 +404,12 @@ dependencies = [ "libc", ] +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + [[package]] name = "crypto-common" version = "0.1.6" @@ -1729,6 +1736,15 @@ dependencies = [ "syn 2.0.68", ] +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + [[package]] name = "tinyvec" version = "1.6.1" diff --git a/Cargo.toml b/Cargo.toml index 0025066..dc3b281 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "bitbox-api" authors = ["Marko Bencun "] -version = "0.11.0" +version = "0.12.0" homepage = "https://bitbox.swiss/" repository = "https://github.com/BitBoxSwiss/bitbox-api-rs/" readme = "README-rust.md" @@ -50,6 +50,8 @@ wasm-bindgen-test = "0.3.42" tokio = { version = "1", features = ["time", "macros", "rt", "fs"] } reqwest = "0.12" url = "2.5" +tiny-keccak = { version = "2.0", features = ["keccak"] } +rlp = "0.5" # Enable this to be able to get coverage using `cargo tarpaulin --features=simulator,tokio --out=Html` without compilation error. # See https://github.com/rust-bitcoin/rust-bitcoinconsensus/pull/94 # bitcoinconsensus = { git = "https://github.com/rust-bitcoin/rust-bitcoinconsensus.git", rev = "788ce4d210f7fe6fae4414f5be80968216ba0fd8", default-features = false } diff --git a/NPM_VERSION b/NPM_VERSION index ac454c6..54d1a4f 100644 --- a/NPM_VERSION +++ b/NPM_VERSION @@ -1 +1 @@ -0.12.0 +0.13.0 diff --git a/messages/eth.proto b/messages/eth.proto index 65c8f90..ebff5c1 100644 --- a/messages/eth.proto +++ b/messages/eth.proto @@ -1,16 +1,4 @@ -// Copyright 2019 Shift Cryptosecurity AG -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. +// SPDX-License-Identifier: Apache-2.0 syntax = "proto3"; package shiftcrypto.bitbox02; @@ -63,6 +51,8 @@ message ETHSignRequest { // If non-zero, `coin` is ignored and `chain_id` is used to identify the network. uint64 chain_id = 10; ETHAddressCase address_case = 11; + // For streaming: if non-zero, data field should be empty and data will be requested in chunks + uint32 data_length = 12; } // TX payload for an EIP-1559 (type 2) transaction: https://eips.ethereum.org/EIPS/eip-1559 @@ -78,6 +68,17 @@ message ETHSignEIP1559Request { bytes data = 9; AntiKleptoHostNonceCommitment host_nonce_commitment = 10; ETHAddressCase address_case = 11; + // For streaming: if non-zero, data field should be empty and data will be requested in chunks + uint32 data_length = 12; +} + +message ETHSignDataRequestChunkResponse { + uint32 offset = 1; + uint32 length = 2; +} + +message ETHSignDataResponseChunkRequest { + bytes chunk = 1; } message ETHSignMessageRequest { @@ -154,6 +155,7 @@ message ETHRequest { ETHSignTypedMessageRequest sign_typed_msg = 5; ETHTypedMessageValueRequest typed_msg_value = 6; ETHSignEIP1559Request sign_eip1559 = 7; + ETHSignDataResponseChunkRequest data_response_chunk = 8; } } @@ -163,5 +165,6 @@ message ETHResponse { ETHSignResponse sign = 2; AntiKleptoSignerCommitment antiklepto_signer_commitment = 3; ETHTypedMessageValueResponse typed_msg_value = 4; + ETHSignDataRequestChunkResponse data_request_chunk = 5; } } diff --git a/src/eth.rs b/src/eth.rs index 596eb6a..939947b 100644 --- a/src/eth.rs +++ b/src/eth.rs @@ -20,6 +20,10 @@ use num_bigint::{BigInt, BigUint}; //use num_traits::ToPrimitive; use serde_json::Value; +/// Threshold above which transaction data is streamed in chunks. +/// Transactions with data larger than this use streaming mode. +const STREAMING_THRESHOLD: usize = 6144; + impl PairedBitBox { async fn query_proto_eth( &self, @@ -478,6 +482,31 @@ impl PairedBitBox { } } + /// Handles streaming of transaction data when in streaming mode. + /// The device requests data chunks, and this method responds with the requested chunks. + async fn handle_eth_data_streaming( + &self, + data: &[u8], + mut response: pb::eth_response::Response, + ) -> Result { + while let pb::eth_response::Response::DataRequestChunk(chunk_req) = &response { + let offset = chunk_req.offset as usize; + let length = chunk_req.length as usize; + + if offset + length > data.len() { + return Err(Error::UnexpectedResponse); + } + + let chunk = data[offset..offset + length].to_vec(); + response = self + .query_proto_eth(pb::eth_request::Request::DataResponseChunk( + pb::EthSignDataResponseChunkRequest { chunk }, + )) + .await?; + } + Ok(response) + } + /// Signs an Ethereum transaction. It returns a 65 byte signature (R, S, and 1 byte recID). The /// `tx` param can be constructed manually or parsed from a raw transaction using /// `raw_tx_slice.try_into()` (`rlp` feature required). @@ -491,6 +520,11 @@ impl PairedBitBox { // passing chainID instead of coin only since v9.10.0 self.validate_version(">=9.10.0")?; + let use_streaming = tx.data.len() > STREAMING_THRESHOLD; + if use_streaming { + self.validate_version(">=9.26.0")?; + } + let host_nonce = crate::antiklepto::gen_host_nonce()?; let request = pb::eth_request::Request::Sign(pb::EthSignRequest { coin: 0, @@ -500,14 +534,27 @@ impl PairedBitBox { gas_limit: crate::util::remove_leading_zeroes(&tx.gas_limit), recipient: tx.recipient.to_vec(), value: crate::util::remove_leading_zeroes(&tx.value), - data: tx.data.clone(), + data: if use_streaming { + vec![] + } else { + tx.data.clone() + }, host_nonce_commitment: Some(pb::AntiKleptoHostNonceCommitment { commitment: crate::antiklepto::host_commit(&host_nonce).to_vec(), }), chain_id, address_case: address_case.unwrap_or(pb::EthAddressCase::Mixed).into(), + data_length: if use_streaming { + tx.data.len() as u32 + } else { + 0 + }, }); - let response = self.query_proto_eth(request).await?; + + let mut response = self.query_proto_eth(request).await?; + if use_streaming { + response = self.handle_eth_data_streaming(&tx.data, response).await?; + } self.handle_antiklepto(&response, host_nonce).await } @@ -523,6 +570,11 @@ impl PairedBitBox { // EIP1559 is suported from v9.16.0 self.validate_version(">=9.16.0")?; + let use_streaming = tx.data.len() > STREAMING_THRESHOLD; + if use_streaming { + self.validate_version(">=9.26.0")?; + } + let host_nonce = crate::antiklepto::gen_host_nonce()?; let request = pb::eth_request::Request::SignEip1559(pb::EthSignEip1559Request { chain_id: tx.chain_id, @@ -535,13 +587,26 @@ impl PairedBitBox { gas_limit: crate::util::remove_leading_zeroes(&tx.gas_limit), recipient: tx.recipient.to_vec(), value: crate::util::remove_leading_zeroes(&tx.value), - data: tx.data.clone(), + data: if use_streaming { + vec![] + } else { + tx.data.clone() + }, host_nonce_commitment: Some(pb::AntiKleptoHostNonceCommitment { commitment: crate::antiklepto::host_commit(&host_nonce).to_vec(), }), address_case: address_case.unwrap_or(pb::EthAddressCase::Mixed).into(), + data_length: if use_streaming { + tx.data.len() as u32 + } else { + 0 + }, }); - let response = self.query_proto_eth(request).await?; + + let mut response = self.query_proto_eth(request).await?; + if use_streaming { + response = self.handle_eth_data_streaming(&tx.data, response).await?; + } self.handle_antiklepto(&response, host_nonce).await } diff --git a/src/shiftcrypto.bitbox02.rs b/src/shiftcrypto.bitbox02.rs index ee80be9..ce62944 100644 --- a/src/shiftcrypto.bitbox02.rs +++ b/src/shiftcrypto.bitbox02.rs @@ -157,12 +157,16 @@ pub struct DeviceInfoResponse { pub mnemonic_passphrase_enabled: bool, #[prost(uint32, tag = "5")] pub monotonic_increments_remaining: u32, - /// From v9.6.0: "ATECC608A" or "ATECC608B". + /// From v9.6.0: "ATECC608A" or "ATECC608B" or "OPTIGA_TRUST_M_V3". #[prost(string, tag = "6")] pub securechip_model: ::prost::alloc::string::String, /// Only present in Bluetooth-enabled devices. #[prost(message, optional, tag = "7")] pub bluetooth: ::core::option::Option, + /// From v9.25.0. This together with `securechip_model` determines the password stretching + /// algorithm. + #[prost(string, tag = "8")] + pub password_stretching_algo: ::prost::alloc::string::String, } /// Nested message and enum types in `DeviceInfoResponse`. pub mod device_info_response { @@ -1706,6 +1710,9 @@ pub struct EthSignRequest { pub chain_id: u64, #[prost(enumeration = "EthAddressCase", tag = "11")] pub address_case: i32, + /// For streaming: if non-zero, data field should be empty and data will be requested in chunks + #[prost(uint32, tag = "12")] + pub data_length: u32, } /// TX payload for an EIP-1559 (type 2) transaction: #[cfg_attr(feature = "wasm", derive(serde::Serialize, serde::Deserialize))] @@ -1744,6 +1751,25 @@ pub struct EthSignEip1559Request { pub host_nonce_commitment: ::core::option::Option, #[prost(enumeration = "EthAddressCase", tag = "11")] pub address_case: i32, + /// For streaming: if non-zero, data field should be empty and data will be requested in chunks + #[prost(uint32, tag = "12")] + pub data_length: u32, +} +#[cfg_attr(feature = "wasm", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "wasm", serde(rename_all = "camelCase"))] +#[derive(Clone, Copy, PartialEq, ::prost::Message)] +pub struct EthSignDataRequestChunkResponse { + #[prost(uint32, tag = "1")] + pub offset: u32, + #[prost(uint32, tag = "2")] + pub length: u32, +} +#[cfg_attr(feature = "wasm", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "wasm", serde(rename_all = "camelCase"))] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct EthSignDataResponseChunkRequest { + #[prost(bytes = "vec", tag = "1")] + pub chunk: ::prost::alloc::vec::Vec, } #[cfg_attr(feature = "wasm", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "wasm", serde(rename_all = "camelCase"))] @@ -1952,7 +1978,7 @@ pub struct EthTypedMessageValueRequest { #[cfg_attr(feature = "wasm", serde(rename_all = "camelCase"))] #[derive(Clone, PartialEq, ::prost::Message)] pub struct EthRequest { - #[prost(oneof = "eth_request::Request", tags = "1, 2, 3, 4, 5, 6, 7")] + #[prost(oneof = "eth_request::Request", tags = "1, 2, 3, 4, 5, 6, 7, 8")] pub request: ::core::option::Option, } /// Nested message and enum types in `ETHRequest`. @@ -1975,13 +2001,15 @@ pub mod eth_request { TypedMsgValue(super::EthTypedMessageValueRequest), #[prost(message, tag = "7")] SignEip1559(super::EthSignEip1559Request), + #[prost(message, tag = "8")] + DataResponseChunk(super::EthSignDataResponseChunkRequest), } } #[cfg_attr(feature = "wasm", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "wasm", serde(rename_all = "camelCase"))] #[derive(Clone, PartialEq, ::prost::Message)] pub struct EthResponse { - #[prost(oneof = "eth_response::Response", tags = "1, 2, 3, 4")] + #[prost(oneof = "eth_response::Response", tags = "1, 2, 3, 4, 5")] pub response: ::core::option::Option, } /// Nested message and enum types in `ETHResponse`. @@ -1998,6 +2026,8 @@ pub mod eth_response { AntikleptoSignerCommitment(super::AntiKleptoSignerCommitment), #[prost(message, tag = "4")] TypedMsgValue(super::EthTypedMessageValueResponse), + #[prost(message, tag = "5")] + DataRequestChunk(super::EthSignDataRequestChunkResponse), } } /// Kept for backwards compatibility. Use chain_id instead, introduced in v9.10.0. diff --git a/src/wasm/connect.rs b/src/wasm/connect.rs index e6232e8..870377e 100644 --- a/src/wasm/connect.rs +++ b/src/wasm/connect.rs @@ -11,7 +11,7 @@ struct JsReadWrite { impl crate::util::Threading for JsReadWrite {} -#[wasm_bindgen(raw_module = "./webhid")] +#[wasm_bindgen(raw_module = "./webhid.js")] extern "C" { #[wasm_bindgen(catch)] async fn getWebHIDDevice( diff --git a/src/wasm/mod.rs b/src/wasm/mod.rs index 8b45c4a..57d0fdd 100644 --- a/src/wasm/mod.rs +++ b/src/wasm/mod.rs @@ -107,7 +107,7 @@ pub fn eth_identify_case(recipient_address: &str) -> types::TsEthAddressCase { crate::eth::eth_identify_case(recipient_address).into() } -#[wasm_bindgen(raw_module = "./webhid")] +#[wasm_bindgen(raw_module = "./webhid.js")] extern "C" { async fn jsSleep(millis: f64); } diff --git a/tests/test_btc.rs b/tests/test_btc.rs index bc17347..1b3bda0 100644 --- a/tests/test_btc.rs +++ b/tests/test_btc.rs @@ -1,6 +1,6 @@ -#![cfg(feature = "simulator")] // SPDX-License-Identifier: Apache-2.0 +#![cfg(feature = "simulator")] // Simulators only run on linux/amd64. #![cfg(all(target_os = "linux", target_arch = "x86_64"))] diff --git a/tests/test_btc_psbt.rs b/tests/test_btc_psbt.rs index fa3a507..29aaf38 100644 --- a/tests/test_btc_psbt.rs +++ b/tests/test_btc_psbt.rs @@ -1,6 +1,6 @@ -#![cfg(feature = "simulator")] // SPDX-License-Identifier: Apache-2.0 +#![cfg(feature = "simulator")] // Simulators only run on linux/amd64. #![cfg(all(target_os = "linux", target_arch = "x86_64"))] diff --git a/tests/test_cardano.rs b/tests/test_cardano.rs index e48c95b..eac36e6 100644 --- a/tests/test_cardano.rs +++ b/tests/test_cardano.rs @@ -1,6 +1,6 @@ -#![cfg(feature = "simulator")] // SPDX-License-Identifier: Apache-2.0 +#![cfg(feature = "simulator")] // Simulators only run on linux/amd64. #![cfg(all(target_os = "linux", target_arch = "x86_64"))] diff --git a/tests/test_device.rs b/tests/test_device.rs index f6ed716..7d75e5f 100644 --- a/tests/test_device.rs +++ b/tests/test_device.rs @@ -1,6 +1,6 @@ -#![cfg(feature = "simulator")] // SPDX-License-Identifier: Apache-2.0 +#![cfg(feature = "simulator")] // Simulators only run on linux/amd64. #![cfg(all(target_os = "linux", target_arch = "x86_64"))] diff --git a/tests/test_eth.rs b/tests/test_eth.rs new file mode 100644 index 0000000..fe7d72a --- /dev/null +++ b/tests/test_eth.rs @@ -0,0 +1,203 @@ +// SPDX-License-Identifier: Apache-2.0 + +#![cfg(feature = "simulator")] +// Simulators only run on linux/amd64. +#![cfg(all(target_os = "linux", target_arch = "x86_64"))] + +#[cfg(not(feature = "tokio"))] +compile_error!("Enable the tokio feature to run simulator tests"); + +mod util; + +use bitbox_api::eth::{EIP1559Transaction, Transaction}; +use bitcoin::secp256k1; +use tiny_keccak::{Hasher, Keccak}; +use util::test_initialized_simulators; + +fn keccak256(data: &[u8]) -> [u8; 32] { + let mut hasher = Keccak::v256(); + hasher.update(data); + let mut output = [0u8; 32]; + hasher.finalize(&mut output); + output +} + +fn legacy_sighash(chain_id: u64, tx: &Transaction) -> [u8; 32] { + let mut stream = rlp::RlpStream::new_list(9); + stream.append(&tx.nonce); + stream.append(&tx.gas_price); + stream.append(&tx.gas_limit); + let recipient = tx.recipient.to_vec(); + stream.append(&recipient); + stream.append(&tx.value); + stream.append(&tx.data); + stream.append(&chain_id); + stream.append(&0u64); + stream.append(&0u64); + keccak256(&stream.out()) +} + +fn eip1559_sighash(tx: &EIP1559Transaction) -> [u8; 32] { + let mut stream = rlp::RlpStream::new_list(9); + stream.append(&tx.chain_id); + stream.append(&tx.nonce); + stream.append(&tx.max_priority_fee_per_gas); + stream.append(&tx.max_fee_per_gas); + stream.append(&tx.gas_limit); + let recipient = tx.recipient.to_vec(); + stream.append(&recipient); + stream.append(&tx.value); + stream.append(&tx.data); + stream.begin_list(0); + let rlp_bytes = stream.out(); + let mut prefixed = vec![0x02]; + prefixed.extend_from_slice(&rlp_bytes); + keccak256(&prefixed) +} + +fn verify_eth_signature(sighash: &[u8; 32], signature: &[u8; 65]) { + let secp = secp256k1::Secp256k1::new(); + let path: bitcoin::bip32::DerivationPath = "m/44'/60'/0'/0/0".parse().unwrap(); + let child_xprv = util::simulator_xprv().derive_priv(&secp, &path).unwrap(); + let expected_pubkey = bitcoin::bip32::Xpub::from_priv(&secp, &child_xprv).public_key; + + let sig = secp256k1::ecdsa::Signature::from_compact(&signature[..64]).unwrap(); + let msg = secp256k1::Message::from_digest(*sighash); + + secp.verify_ecdsa(&msg, &sig, &expected_pubkey).unwrap(); +} + +#[tokio::test] +async fn test_eth_address() { + test_initialized_simulators(async |paired_bitbox| { + let address = paired_bitbox + .eth_address(1, &"m/44'/60'/0'/0/0".try_into().unwrap(), false) + .await + .unwrap(); + assert_eq!(address, "0x416E88840Eb6353E49252Da2a2c140eA1f969D1a"); + }) + .await +} + +#[tokio::test] +async fn test_eth_sign_transaction_nonstreaming() { + test_initialized_simulators(async |paired_bitbox| { + assert!(paired_bitbox.eth_supported()); + + let tx = Transaction { + nonce: vec![0x01], + gas_price: vec![0x01], + gas_limit: vec![0x52, 0x08], + recipient: [ + 0x04, 0xf2, 0x64, 0xcf, 0x34, 0x44, 0x03, 0x13, 0xb4, 0xa0, 0x19, 0x2a, 0x35, 0x28, + 0x14, 0xfb, 0xe9, 0x27, 0xb8, 0x85, + ], + value: vec![0x01], + data: vec![0xAB; 100], + }; + + let signature = paired_bitbox + .eth_sign_transaction(1, &"m/44'/60'/0'/0/0".try_into().unwrap(), &tx, None) + .await + .unwrap(); + assert_eq!(signature.len(), 65); + verify_eth_signature(&legacy_sighash(1, &tx), &signature); + }) + .await +} + +#[tokio::test] +async fn test_eth_sign_transaction_streaming() { + test_initialized_simulators(async |paired_bitbox| { + if !semver::VersionReq::parse(">=9.26.0") + .unwrap() + .matches(paired_bitbox.version()) + { + return; + } + + // Large data (over threshold) - streaming mode + let tx = Transaction { + nonce: vec![0x01], + gas_price: vec![0x01], + gas_limit: vec![0x52, 0x08], + recipient: [ + 0x04, 0xf2, 0x64, 0xcf, 0x34, 0x44, 0x03, 0x13, 0xb4, 0xa0, 0x19, 0x2a, 0x35, 0x28, + 0x14, 0xfb, 0xe9, 0x27, 0xb8, 0x85, + ], + value: vec![0x01], + data: vec![0xAB; 10000], + }; + + let signature = paired_bitbox + .eth_sign_transaction(1, &"m/44'/60'/0'/0/0".try_into().unwrap(), &tx, None) + .await + .unwrap(); + assert_eq!(signature.len(), 65); + verify_eth_signature(&legacy_sighash(1, &tx), &signature); + }) + .await +} + +#[tokio::test] +async fn test_eth_sign_1559_transaction_nonstreaming() { + test_initialized_simulators(async |paired_bitbox| { + assert!(paired_bitbox.eth_supported()); + + let tx = EIP1559Transaction { + chain_id: 1, + nonce: vec![0x01], + max_priority_fee_per_gas: vec![0x01], + max_fee_per_gas: vec![0x01], + gas_limit: vec![0x52, 0x08], + recipient: [ + 0x04, 0xf2, 0x64, 0xcf, 0x34, 0x44, 0x03, 0x13, 0xb4, 0xa0, 0x19, 0x2a, 0x35, 0x28, + 0x14, 0xfb, 0xe9, 0x27, 0xb8, 0x85, + ], + value: vec![0x01], + data: vec![0xAB; 100], + }; + + let signature = paired_bitbox + .eth_sign_1559_transaction(&"m/44'/60'/0'/0/0".try_into().unwrap(), &tx, None) + .await + .unwrap(); + assert_eq!(signature.len(), 65); + verify_eth_signature(&eip1559_sighash(&tx), &signature); + }) + .await +} + +#[tokio::test] +async fn test_eth_sign_1559_transaction_streaming() { + test_initialized_simulators(async |paired_bitbox| { + if !semver::VersionReq::parse(">=9.26.0") + .unwrap() + .matches(paired_bitbox.version()) + { + return; + } + + let tx = EIP1559Transaction { + chain_id: 1, + nonce: vec![0x01], + max_priority_fee_per_gas: vec![0x01], + max_fee_per_gas: vec![0x01], + gas_limit: vec![0x52, 0x08], + recipient: [ + 0x04, 0xf2, 0x64, 0xcf, 0x34, 0x44, 0x03, 0x13, 0xb4, 0xa0, 0x19, 0x2a, 0x35, 0x28, + 0x14, 0xfb, 0xe9, 0x27, 0xb8, 0x85, + ], + value: vec![0x01], + data: vec![0xCD; 8000], + }; + + let signature = paired_bitbox + .eth_sign_1559_transaction(&"m/44'/60'/0'/0/0".try_into().unwrap(), &tx, None) + .await + .unwrap(); + assert_eq!(signature.len(), 65); + verify_eth_signature(&eip1559_sighash(&tx), &signature); + }) + .await +}