From 2f82a7d55a90e3aa607f358af5fe539bd4c93a0b Mon Sep 17 00:00:00 2001 From: sterliakov <50529348+sterliakov@users.noreply.github.com> Date: Sat, 23 Dec 2023 06:25:17 +0400 Subject: [PATCH] Add public RLP and HTTP API support (#15) * Add public RLP * Add HDNode docs * Clean up address namespace * Add network interface for blocks and transactions --- .github/workflows/test.yml | 2 +- CHANGELOG.md | 12 +- Cargo.toml | 22 +- README.md | 4 +- src/address.rs | 38 ++- src/hdnode.rs | 49 +++- src/lib.rs | 14 +- src/network.rs | 501 +++++++++++++++++++++++++++++++++++++ src/rlp.rs | 436 ++++++++++++++++++++++++++++++++ src/transactions.rs | 310 ++++++----------------- src/utils.rs | 430 +++++++++++++++++++++++++++++-- tests/test_address.rs | 2 +- tests/test_hdnode.rs | 16 +- tests/test_network.rs | 479 +++++++++++++++++++++++++++++++++++ tests/test_transactions.rs | 83 +++--- 15 files changed, 2039 insertions(+), 359 deletions(-) create mode 100644 src/network.rs create mode 100644 src/rlp.rs create mode 100644 tests/test_network.rs diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f8c9e1a..0206483 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -29,7 +29,7 @@ jobs: run: cargo rdme --check - name: Run tests with coverage - run: RUST_BACKTRACE=1 cargo tarpaulin --out Xml + run: RUST_BACKTRACE=1 cargo tarpaulin --out Xml --all-features - name: Upload coverage reports to Codecov uses: codecov/codecov-action@v3 diff --git a/CHANGELOG.md b/CHANGELOG.md index cada410..45dc4b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,15 +10,13 @@ Possible header types: - `Breaking Changes` for any backwards-incompatible changes. ## [Unreleased] - +- Made RLP encoding components public and rewrite it with more conscious syntax. +- Added network support for Transaction and Block retrieval. -## v0.0.1 (2023-10-01) +## v0.0.1-alpha.1 (2023-10-01) - Initial Release on [crates.io] :tada: -[crates.io]: https://crates.io/crates/thor-devkit.rs +[crates.io]: https://crates.io/crates/thor-devkit diff --git a/Cargo.toml b/Cargo.toml index 9cca6fd..f6453bf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,7 +20,6 @@ targets = ["x86_64-unknown-linux-gnu"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -alloy-rlp = { version = "0.3.3", features = ["derive"] } bip32 = { version = "0.5.1", default-features=false, features = [ "alloc", "secp256k1-ffi"] } blake2 = "0.10.6" ethereum-types = "0.14.0" @@ -28,20 +27,21 @@ itertools = "0.12.0" tiny-keccak = { version = "2.0.0", features = ["keccak"] } secp256k1 = { version = "0.27.0", features = [ "recovery" ] } tiny-bip39 = "1.0.0" -ethabi = "18.0.0" rustc-hex = "2.1.0" +open-fastrlp = { version = "0.1.4", features = ["std", "ethereum-types"] } +bytes = "1.5.0" +reqwest = { version = "0.11", features = ["json"], optional = true } +serde = { version = "^1.0", features=["derive"], optional = true } +serde_json = { version = "^1.0", optional = true } +serde_with = { version = "^3.4", features = ["hex"], optional = true } [dev-dependencies] # version_sync: to ensure versions in `Cargo.toml` and `README.md` are in sync version-sync = "0.9.4" - -# cargo-bump: to bump package version and tag a commit at the same time. -# actually, the docs recommend installing this globally: -# $ git clone https://github.com/rnag/cargo-bump && cd cargo-bump && cargo install --path . && cd .. && rm -rf cargo-bump -# logging utilities -# log = "^0.4" -# sensible-env-logger = "0.1.0" -# tokio: for `async` support -# tokio = { version = "^1.0", features = ["macros", "rt-multi-thread"] } +rand = { version = "0.8.5", features = ["getrandom"] } +tokio = { version = "1", features = ["full"] } [features] +default = ['http'] +serde = ["dep:serde", "dep:serde_json", "dep:serde_with"] +http = ["dep:reqwest", "serde"] diff --git a/README.md b/README.md index 5ecd0b1..2874c8d 100644 --- a/README.md +++ b/README.md @@ -83,9 +83,9 @@ Check out the [Contributing][] section in the docs for more info. ### License -This project is proudly licensed under the GNU General Public License v3 ([LICENSE](LICENSE)). +This project is proudly licensed under the Lesser GNU General Public License v3 ([LICENSE](https://github.com/sterliakov/thor-devkit.rs/blob/master/LICENSE)). -`thor-devkit` can be distributed according to the GNU General Public License v3. Contributions +`thor-devkit` can be distributed according to the Lesser GNU General Public License v3. Contributions will be accepted under the same license. diff --git a/src/address.rs b/src/address.rs index 87b15ac..4c07c6f 100644 --- a/src/address.rs +++ b/src/address.rs @@ -1,22 +1,29 @@ //! VeChain address operations and verifications. +use crate::rlp::{Decodable, Encodable, RLPError}; use crate::utils::keccak; -use alloy_rlp::{Decodable, Encodable}; use ethereum_types::Address as WrappedAddress; pub use secp256k1::{PublicKey, SecretKey as PrivateKey}; +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; use std::{ ops::{Deref, DerefMut}, result::Result, str::FromStr, }; +#[cfg_attr(feature = "serde", serde_with::serde_as)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde", serde(remote = "ethereum_types::H160"))] +struct _Address(#[cfg_attr(feature = "serde", serde_as(as = "crate::utils::unhex::Hex"))] [u8; 20]); + /// VeChain address. +#[cfg_attr(feature = "serde", serde_with::serde_as)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Copy, Clone, Debug, PartialEq, Eq)] -pub struct Address(WrappedAddress); +pub struct Address(#[cfg_attr(feature = "serde", serde_as(as = "_Address"))] WrappedAddress); impl DerefMut for Address { - // type Target = WrappedAddress; - fn deref_mut(&mut self) -> &mut WrappedAddress { &mut self.0 } @@ -29,18 +36,18 @@ impl Deref for Address { } } impl Encodable for Address { - fn encode(&self, out: &mut dyn alloy_rlp::BufMut) { - use crate::transactions::lstrip; - alloy_rlp::Bytes::copy_from_slice(&lstrip(self.0)).encode(out) + fn encode(&self, out: &mut dyn open_fastrlp::BufMut) { + use crate::rlp::lstrip; + bytes::Bytes::copy_from_slice(&lstrip(self.0)).encode(out) } } impl Decodable for Address { - fn decode(buf: &mut &[u8]) -> Result { - use crate::transactions::static_left_pad; - let bytes = alloy_rlp::Bytes::decode(buf)?; + fn decode(buf: &mut &[u8]) -> Result { + use crate::rlp::static_left_pad; + let bytes = bytes::Bytes::decode(buf)?; Ok(Self(WrappedAddress::from_slice( &static_left_pad::<20>(&bytes).map_err(|e| match e { - alloy_rlp::Error::Overflow => alloy_rlp::Error::ListLengthMismatch { + RLPError::Overflow => RLPError::ListLengthMismatch { expected: Self::WIDTH, got: bytes.len(), }, @@ -102,12 +109,3 @@ impl AddressConvertible for secp256k1::PublicKey { Address(WrappedAddress::from_slice(&suffix)) } } - -/// Invalid public key format reasons -#[derive(Clone, Debug, Eq, PartialEq)] -pub enum AddressValidationError { - /// Not of length 20 - InvalidLength, - /// Not a hex string - InvalidHex, -} diff --git a/src/hdnode.rs b/src/hdnode.rs index 9cd4790..7d30869 100644 --- a/src/hdnode.rs +++ b/src/hdnode.rs @@ -1,6 +1,10 @@ -//! VeChain-tailored Hierarchically deterministic nodes support +//! VeChain-tailored hierarchically deterministic nodes support //! -//! `Reference ` +//! [In-deep explanation](https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki) +//! +//! This module glues together several important components involved in key derivation +//! from different sources. You can construct an [`HDNode`] in multiple ways, allowing, +//! for example, generating a private key from mnemonic or generating a random key. use bip32::{ ChainCode, ChildNumber, DerivationPath, ExtendedKey, ExtendedKeyAttrs, ExtendedPrivateKey, @@ -22,6 +26,9 @@ enum HDNodeVariant { use HDNodeVariant::{Full, Restricted}; /// Hierarchically deterministic node. +/// +/// To construct a wallet, use the [`HDNode::build`] method. It exposes access to the builder +/// that supports multiple construction methods and validates the arguments. #[derive(Clone, Debug, Eq, PartialEq)] pub struct HDNode(HDNodeVariant); @@ -41,14 +48,14 @@ impl HDNode { } pub fn public_key(&self) -> ExtendedPublicKey { - //! Get underlying public key. + //! Get underlying extended public key. match &self.0 { Full(privkey) => privkey.public_key(), Restricted(pubkey) => pubkey.clone(), } } pub fn private_key(&self) -> Result, HDNodeError> { - //! Get underlying private key. + //! Get underlying extended private key. match &self.0 { Full(privkey) => Ok(privkey.clone()), Restricted(_) => Err(HDNodeError::Crypto), @@ -140,6 +147,36 @@ impl From for HDNodeError { } /// Builder for HDNode: use this to construct a node from different sources. +/// +/// The following sources are supported: +/// - Binary seed. 64 bytes of raw entropy to use for key generation. +/// - [BIP39](https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki) mnemonic +/// with optional password. This method is compatible with derivation in Sync2 wallet. +/// - Master private key bytes and chain code +/// - Extended private key +/// - Master public key bytes and chain code +/// - Extended public key +/// +/// First two methods accept a derivation path to use (defaults to VeChain path). +/// +/// For example, here's what you could do: +/// +/// ```rust +/// use thor_devkit::hdnode::{Mnemonic, Language, HDNode}; +/// use rand::RngCore; +/// use rand::rngs::OsRng; +/// +/// let mnemonic = Mnemonic::from_phrase( +/// "ignore empty bird silly journey junior ripple have guard waste between tenant", +/// Language::English, +/// ) +/// .expect("Should be constructible"); +/// let wallet = HDNode::build().mnemonic(mnemonic).build().expect("Must be buildable"); +/// // OR +/// let mut entropy = [0u8; 64]; +/// OsRng.fill_bytes(&mut entropy); +/// let other_wallet = HDNode::build().seed(entropy).build().expect("Must be buildable"); +/// ``` #[derive(Clone, Default)] pub struct HDNodeBuilder<'a> { path: Option, @@ -211,7 +248,9 @@ impl<'a> HDNodeBuilder<'a> { ) -> Self { //! Create an HDNode from private key bytes and chain code. //! + //!
//! Beware that this node cannot be used to derive new private keys. + //!
self.ext_pubkey = Some(ExtendedKey { prefix: Prefix::XPUB, attrs: ExtendedKeyAttrs { @@ -227,7 +266,9 @@ impl<'a> HDNodeBuilder<'a> { pub fn public_key(mut self, ext_key: ExtendedKey) -> Self { //! Create an HDNode from extended public key structure. //! + //!
//! Beware that this node cannot be used to derive new private keys. + //!
self.ext_pubkey = Some(ext_key); self } diff --git a/src/lib.rs b/src/lib.rs index 9315552..7a7462a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -77,15 +77,19 @@ //! //! ## License //! -//! This project is proudly licensed under the GNU General Public License v3 ([LICENSE](LICENSE)). +//! This project is proudly licensed under the Lesser GNU General Public License v3 ([LICENSE](https://github.com/sterliakov/thor-devkit.rs/blob/master/LICENSE)). //! -//! `thor-devkit` can be distributed according to the GNU General Public License v3. Contributions +//! `thor-devkit` can be distributed according to the Lesser GNU General Public License v3. Contributions //! will be accepted under the same license. -pub mod address; +mod address; +pub use address::{Address, AddressConvertible, PrivateKey, PublicKey}; pub mod hdnode; +#[cfg(feature = "http")] +pub mod network; +pub mod rlp; pub mod transactions; mod utils; -pub use ethabi; pub use ethereum_types::U256; -pub use utils::{blake2_256, decode_hex, keccak}; +pub use rustc_hex::FromHexError as AddressValidationError; +pub use utils::{blake2_256, keccak}; diff --git a/src/network.rs b/src/network.rs new file mode 100644 index 0000000..371a48d --- /dev/null +++ b/src/network.rs @@ -0,0 +1,501 @@ +//! Module for interacting with node HTTP APIs. + +use crate::rlp::{Bytes, Decodable}; +use crate::transactions::Reserved; +use crate::utils::unhex; +use crate::U256; +use crate::{ + transactions::{Clause, Transaction}, + Address, +}; +use reqwest::{Client, Url}; +use rustc_hex::ToHex; +use serde::Deserialize; + +/// Generic result of all asynchronous calls in this module. +pub type AResult = std::result::Result>; + +/// 256-byte binary sequence (usually a hash of something) +pub type Hash256 = [u8; 32]; + +/// A simple HTTP REST client for a VeChain node. +pub struct ThorNode { + base_url: Url, + #[allow(dead_code)] + chain_tag: u8, +} + +#[serde_with::serde_as] +#[derive(Deserialize)] +struct RawTxResponse { + #[serde_as(as = "unhex::Hex")] + raw: Bytes, + meta: Option, +} + +/// Extended transaction data +#[serde_with::serde_as] +#[derive(Clone, Debug, PartialEq, Deserialize)] +pub struct ExtendedTransaction { + /// Identifier of the transaction + #[serde_as(as = "unhex::Hex")] + pub id: Hash256, + /// The one who signed the transaction + pub origin: Address, + /// The delegator who paid the gas fee + pub delegator: Option
, + /// Byte size of the transaction that is RLP encoded + pub size: u32, + /// Last byte of genesis block ID + #[serde(rename = "chainTag")] + pub chain_tag: u8, + /// 8 bytes prefix of some block ID + #[serde(rename = "blockRef")] + #[serde_as(as = "unhex::HexNum<8, u64>")] + pub block_ref: u64, + /// Expiration relative to blockRef, in unit block + pub expiration: u32, + /// Transaction clauses + pub clauses: Vec, + /// Coefficient used to calculate the final gas price + #[serde(rename = "gasPriceCoef")] + pub gas_price_coef: u8, + /// Max amount of gas can be consumed to execute this transaction + pub gas: u64, + /// ID of the transaction on which the current transaction depends on. can be null. + #[serde(rename = "dependsOn")] + #[serde_as(as = "Option>")] + pub depends_on: Option, + /// Transaction nonce + #[serde_as(as = "unhex::HexNum<8, u64>")] + pub nonce: u64, +} + +impl ExtendedTransaction { + pub fn as_transaction(self) -> Transaction { + //! Convert to package-compatible [`Transaction`] + let Self { + chain_tag, + block_ref, + expiration, + clauses, + gas_price_coef, + gas, + depends_on, + nonce, + delegator, + .. + } = self; + Transaction { + chain_tag, + block_ref, + expiration, + clauses, + gas_price_coef, + gas, + depends_on, + nonce, + reserved: if delegator.is_some() { + Some(Reserved::new_delegated()) + } else { + None + }, + signature: None, + } + } +} + +#[serde_with::serde_as] +#[derive(Deserialize)] +struct ExtendedTransactionResponse { + #[serde(flatten)] + transaction: ExtendedTransaction, + meta: Option, +} + +/// Transaction metadata +#[serde_with::serde_as] +#[derive(Clone, Debug, Eq, PartialEq, Deserialize)] +pub struct TransactionMeta { + /// Block identifier + #[serde(rename = "blockID")] + #[serde_as(as = "unhex::Hex")] + pub block_id: Hash256, + /// Block number (height) + #[serde(rename = "blockNumber")] + pub block_number: u32, + /// Block unix timestamp + #[serde(rename = "blockTimestamp")] + pub block_timestamp: u32, +} + +/// Transaction receipt +#[derive(Clone, Debug, PartialEq, Deserialize)] +pub struct Receipt { + /// Amount of gas consumed by this transaction + #[serde(rename = "gasUsed")] + pub gas_used: u32, + /// Address of account who paid used gas + #[serde(rename = "gasPayer")] + pub gas_payer: Address, + /// Hex form of amount of paid energy + pub paid: U256, + /// Hex form of amount of reward + pub reward: U256, + /// true means the transaction was reverted + pub reverted: bool, + /// Outputs (if this transaction was a contract call) + pub outputs: Vec, +} + +#[derive(Clone, Debug, PartialEq, Deserialize)] +struct ReceiptResponse { + #[serde(flatten)] + body: Receipt, + meta: ReceiptMeta, +} + +/// Single output in the transaction receipt +#[derive(Clone, Debug, PartialEq, Deserialize)] +pub struct ReceiptOutput { + /// Deployed contract address, if the corresponding clause is a contract deployment clause + #[serde(rename = "contractAddress")] + pub contract_address: Option
, + /// Emitted contract events + pub events: Vec, + /// Transfers executed during the contract call + pub transfers: Vec, +} + +/// Transaction receipt metadata +#[serde_with::serde_as] +#[derive(Clone, Debug, PartialEq, Deserialize)] +pub struct ReceiptMeta { + /// Block identifier + #[serde(rename = "blockID")] + #[serde_as(as = "unhex::Hex")] + pub block_id: Hash256, + /// Block number (height) + #[serde(rename = "blockNumber")] + pub block_number: u32, + /// Block unix timestamp + #[serde(rename = "blockTimestamp")] + pub block_timestamp: u32, + /// Transaction identifier + #[serde(rename = "txID")] + #[serde_as(as = "unhex::Hex")] + pub tx_id: Hash256, + /// Transaction origin (signer) + #[serde(rename = "txOrigin")] + pub tx_origin: Address, +} + +/// Emitted contract event +#[serde_with::serde_as] +#[derive(Clone, Debug, PartialEq, Deserialize)] +pub struct Event { + /// The address of contract which produces the event + pub address: Address, + /// Event topics + #[serde_as(as = "Vec")] + pub topics: Vec, + /// Event data + #[serde_as(as = "unhex::Hex")] + pub data: Bytes, +} + +/// Single transfer during the contract call +#[derive(Clone, Debug, PartialEq, Deserialize)] +pub struct Transfer { + /// Address that sends tokens + pub sender: Address, + /// Address that receives tokens + pub recipient: Address, + /// Amount of tokens + pub amount: U256, +} + +/// A blockchain block. +#[serde_with::serde_as] +#[derive(Clone, Debug, PartialEq, Deserialize)] +pub struct BlockInfo { + /// Block number (height) + pub number: u32, + /// Block identifier + #[serde_as(as = "unhex::Hex")] + pub id: Hash256, + /// RLP encoded block size in bytes + pub size: u32, + /// Parent block ID + #[serde_as(as = "unhex::Hex")] + #[serde(rename = "parentID")] + pub parent_id: Hash256, + /// Block unix timestamp + pub timestamp: u32, + /// Block gas limit (max allowed accumulative gas usage of transactions) + #[serde(rename = "gasLimit")] + pub gas_limit: u32, + /// Address of account to receive block reward + pub beneficiary: Address, + /// Accumulative gas usage of transactions + #[serde(rename = "gasUsed")] + pub gas_used: u32, + /// Sum of all ancestral blocks' score + #[serde(rename = "totalScore")] + pub total_score: u32, + /// Root hash of transactions in the block + #[serde_as(as = "unhex::Hex")] + #[serde(rename = "txsRoot")] + pub txs_root: Hash256, + /// Supported txs features bitset + #[serde(rename = "txsFeatures")] + pub txs_features: u32, + /// Root hash of accounts state + #[serde_as(as = "unhex::Hex")] + #[serde(rename = "stateRoot")] + pub state_root: Hash256, + /// Root hash of transaction receipts + #[serde_as(as = "unhex::Hex")] + #[serde(rename = "receiptsRoot")] + pub receipts_root: Hash256, + /// Is in trunk? + #[serde(rename = "isTrunk")] + pub is_trunk: bool, + /// Is finalized? + #[serde(rename = "isFinalized")] + pub is_finalized: bool, + /// Whether the block signer voted COM(Commit) in BFT + pub com: bool, + /// The one who signed this block + pub signer: Address, +} + +/// Transaction data included in the block extended details. +/// +/// Combines [`ExtendedTransaction`] and [`Receipt`]. +#[derive(Clone, Debug, PartialEq, Deserialize)] +pub struct BlockTransaction { + /// Transaction details + #[serde(flatten)] + pub transaction: ExtendedTransaction, + /// Transaction receipt + #[serde(flatten)] + pub receipt: Receipt, +} + +#[serde_with::serde_as] +#[derive(Clone, Debug, PartialEq, Deserialize)] +struct BlockResponse { + #[serde(flatten)] + base: BlockInfo, + #[serde_as(as = "Vec")] + transactions: Vec, +} + +#[serde_with::serde_as] +#[derive(Clone, Debug, PartialEq, Deserialize)] +struct BlockExtendedResponse { + #[serde(flatten)] + base: BlockInfo, + transactions: Vec, +} + +/// Block reference: a way to identify the block on the chain. +#[derive(Clone, Debug)] +pub enum BlockReference { + /// Latest: already approved by some node, but not finalized yet. + Best, + /// Finalized: block is frozen on chain. + Finalized, + /// Block ordinal number (1..) + Number(u64), + /// Block ID + ID(Hash256), +} + +impl BlockReference { + fn as_query_param(&self) -> String { + match self { + BlockReference::Best => "best".to_string(), + BlockReference::Finalized => "finalized".to_string(), + BlockReference::Number(num) => format!("0x{:02x}", num), + BlockReference::ID(id) => { + let hex: String = id.to_hex(); + format!("0x{}", hex) + } + } + } +} + +impl ThorNode { + /// Chain tag for mainnet + pub const MAINNET_CHAIN_TAG: u8 = 0x4A; + /// REST API URL for mainnet (one possible) + pub const MAINNET_BASE_URL: &'static str = "https://mainnet.vecha.in/"; + /// Chain tag for testnet + pub const TESTNET_CHAIN_TAG: u8 = 0x27; + /// REST API URL for testnet (one possible) + pub const TESTNET_BASE_URL: &'static str = "https://testnet.vecha.in/"; + + pub fn mainnet() -> Self { + //! Mainnet parameters + Self { + base_url: Self::MAINNET_BASE_URL.parse().unwrap(), + chain_tag: Self::MAINNET_CHAIN_TAG, + } + } + + pub fn testnet() -> Self { + //! Testnet parameters + Self { + base_url: Self::TESTNET_BASE_URL.parse().unwrap(), + chain_tag: Self::TESTNET_CHAIN_TAG, + } + } + + pub async fn fetch_transaction( + &self, + transaction_id: Hash256, + ) -> AResult)>> { + //! Retrieve a [`Transaction`] from node by its ID. + //! + //! Returns [`None`] for nonexistent transactions. + //! + //! Meta can be [`None`] if a transaction was broadcasted, but + //! not yet included into a block. + //! + //! This method exists for interoperability with [`Transaction`] + //! from other parts of library. You can get more info from node + //! with [`ThorNode::fetch_extended_transaction`]. + let client = Client::new(); + let hex_id: String = transaction_id.to_hex(); + let path = format!("/transactions/0x{}", hex_id); + let response = client + .get(self.base_url.join(&path)?) + .query(&[("raw", "true")]) + .send() + .await? + .text() + .await?; + if response.strip_suffix('\n').unwrap_or(&response) == "null" { + Ok(None) + } else { + let decoded: RawTxResponse = serde_json::from_str(&response)?; + let tx = Transaction::decode(&mut &decoded.raw[..])?; + Ok(Some((tx, decoded.meta))) + } + } + + pub async fn fetch_extended_transaction( + &self, + transaction_id: Hash256, + ) -> AResult)>> { + //! Retrieve a [`Transaction`] from node by its ID. + //! + //! Returns [`None`] for nonexistent transactions. + //! + //! Meta can be [`None`] if a transaction was broadcasted, but + //! not yet included into a block. + //! + //! This method returns more data than [`ThorNode::fetch_transaction`], + //! but is not interoperable with [`Transaction`]. + let client = Client::new(); + let hex_id: String = transaction_id.to_hex(); + let path = format!("/transactions/0x{}", hex_id); + let response = client + .get(self.base_url.join(&path)?) + .send() + .await? + .text() + .await?; + if response.strip_suffix('\n').unwrap_or(&response) == "null" { + Ok(None) + } else { + let decoded: ExtendedTransactionResponse = serde_json::from_str(&response)?; + Ok(Some((decoded.transaction, decoded.meta))) + } + } + + pub async fn fetch_transaction_receipt( + &self, + transaction_id: Hash256, + ) -> AResult> { + //! Retrieve a transaction receipt from node given a transaction ID. + //! + //! Returns [`None`] for nonexistent or not mined transactions. + let client = Client::new(); + let hex_id: String = transaction_id.to_hex(); + let path = format!("/transactions/0x{}/receipt", hex_id); + let response = client + .get(self.base_url.join(&path)?) + .send() + .await? + .text() + .await?; + if response.strip_suffix('\n').unwrap_or(&response) == "null" { + Ok(None) + } else { + let decoded: ReceiptResponse = serde_json::from_str(&response)?; + Ok(Some((decoded.body, decoded.meta))) + } + } + + pub async fn fetch_block( + &self, + block_ref: BlockReference, + ) -> AResult)>> { + //! Retrieve a block from node by given identifier. + //! + //! Returns [`None`] for nonexistent blocks. + let client = Client::new(); + let path = format!("/blocks/{}", block_ref.as_query_param()); + let response = client + .get(self.base_url.join(&path)?) + .send() + .await? + .text() + .await?; + if response.strip_suffix('\n').unwrap_or(&response) == "null" { + Ok(None) + } else { + let decoded: BlockResponse = serde_json::from_str(&response)?; + Ok(Some((decoded.base, decoded.transactions))) + } + } + + pub async fn fetch_block_expanded( + &self, + block_ref: BlockReference, + ) -> AResult)>> { + //! Retrieve a block from node by given identifier together with extended + //! transaction details. + //! + //! Returns [`None`] for nonexistent blocks. + let client = Client::new(); + let path = format!("/blocks/{}", block_ref.as_query_param()); + let response = client + .get(self.base_url.join(&path)?) + .query(&[("expanded", "true")]) + .send() + .await? + .text() + .await?; + if response.strip_suffix('\n').unwrap_or(&response) == "null" { + Ok(None) + } else { + let decoded: BlockExtendedResponse = serde_json::from_str(&response)?; + Ok(Some((decoded.base, decoded.transactions))) + } + } + + pub async fn broadcast_transaction(&self, transaction: &Transaction) -> AResult<()> { + //! Broadcast a new [`Transaction`] to the node. + let client = Client::new(); + client + .post(self.base_url.join("/transactions/")?) + .body(transaction.to_broadcastable_bytes()?) + .send() + .await?; + Ok(()) + } +} diff --git a/src/rlp.rs b/src/rlp.rs new file mode 100644 index 0000000..fadb10c --- /dev/null +++ b/src/rlp.rs @@ -0,0 +1,436 @@ +//! This module enables RLP encoding of high-level objects. +//! +//! RLP (recursive length prefix) is a common algorithm for encoding +//! of variable length binary data. RLP encodes data before storing on disk +//! or transmitting via network. +//! +//! Theory +//! ------ +//! +//! Encoding +//! ******** +//! +//! Primary RLP can only deal with "item" type, which is defined as: +//! +//! - Byte string ([`Bytes`]) or +//! - Sequence of items ([`Vec`], fixed array or slice). +//! +//! Some examples are: +//! +//! * ``b'\x00\xff'`` +//! * empty list ``vec![]`` +//! * list of bytes ``vec![vec![0u8], vec![1u8, 3u8]]`` +//! * list of combinations ``vec![vec![], vec![0u8], vec![vec![0]]]`` +//! +//! The encoded result is always a byte string (sequence of [`u8`]). +//! +//! Encoding algorithm +//! ****************** +//! +//! Given `x` item as input, we define `rlp_encode` as the following algorithm: +//! +//! Let `concat` be a function that joins given bytes into single byte sequence. +//! 1. If `x` is a single byte and `0x00 <= x <= 0x7F`, `rlp_encode(x) = x`. +//! 1. Otherwise, if `x` is a byte string, let `len(x)` be length of `x` in bytes +//! and define encoding as follows: +//! * If `0 < len(x) < 0x38` (note that empty byte string fulfills this requirement), then +//! ```txt +//! rlp_encode(x) = concat(0x80 + len(x), x) +//! ``` +//! In this case first byte is in range `[0x80; 0xB7]`. +//! * If `0x38 <= len(x) <= 0xFFFFFFFF`, then +//! ```txt +//! rlp_encode(x) = concat(0xB7 + len(len(x)), len(x), x) +//! ``` +//! In this case first byte is in range `[0xB8; 0xBF]`. +//! * For longer strings encoding is undefined. +//! 1. Otherwise, if `x` is a list, let `s = concat(map(rlp_encode, x))` +//! be concatenation of RLP encodings of all its items. +//! * If `0 < len(s) < 0x38` (note that empty list matches), then +//! ```txt +//! rlp_encode(x) = concat(0xC0 + len(s), s) +//! ``` +//! In this case first byte is in range `[0xC0; 0xF7]`. +//! * If `0x38 <= len(s) <= 0xFFFFFFFF`, then +//! ```txt +//! rlp_encode(x) = concat(0xF7 + len(len(s)), len(s), x) +//! ``` +//! In this case first byte is in range `[0xF8; 0xFF]`. +//! * For longer lists encoding is undefined. +//! +//! See more in [Ethereum wiki](https://eth.wiki/fundamentals/rlp). +//! +//! Encoding examples +//! ***************** +//! +//! | ``x`` | ``rlp_encode(x)`` | +//! |-------------------|--------------------------------| +//! | ``b''`` | ``0x80`` | +//! | ``b'\x00'`` | ``0x00`` | +//! | ``b'\x0F'`` | ``0x0F`` | +//! | ``b'\x79'`` | ``0x79`` | +//! | ``b'\x80'`` | ``0x81 0x80`` | +//! | ``b'\xFF'`` | ``0x81 0xFF`` | +//! | ``b'foo'`` | ``0x83 0x66 0x6F 0x6F`` | +//! | ``[]`` | ``0xC0`` | +//! | ``[b'\x0F']`` | ``0xC1 0x0F`` | +//! | ``[b'\xEF']`` | ``0xC1 0x81 0xEF`` | +//! | ``[[], [[]]]`` | ``0xC3 0xC0 0xC1 0xC0`` | +//! +//! +//! Serialization +//! ************* +//! +//! However, in the real world, the inputs are not pure bytes nor lists. +//! We need a way to encode numbers (like [`u64`]), custom structs, enums and other +//! more complex machinery that exists in the surrounding code. +//! +//! This library wraps [`open_fastrlp`](https://docs.rs/open-fastrlp/0.1.4/open_fastrlp/) +//! crate, so everything mentioned there about [`Encodable`] and [`Decodable`] traits still +//! applies. You can implement those for any object to make it RLP-serializable. +//! +//! However, following this approach directly results in cluttered code: your `struct`s +//! now have to use field types that match serialization, which may be very inconvenient. +//! +//! To avoid this pitfall, this RLP implementation allows "extended" struct definition +//! via a macro. Let's have a look at `Transaction` definition: +//! +//! ```rust +//! use thor_devkit::rlp::{AsBytes, AsVec, Maybe, Bytes}; +//! use thor_devkit::{rlp_encodable, U256}; +//! use thor_devkit::transactions::{Clause, Reserved}; +//! +//! rlp_encodable! { +//! /// Represents a single VeChain transaction. +//! #[derive(Clone, Debug, Eq, PartialEq)] +//! pub struct Transaction { +//! /// Chain tag +//! pub chain_tag: u8, +//! pub block_ref: u64, +//! pub expiration: u32, +//! pub clauses: Vec, +//! pub gas_price_coef: u8, +//! pub gas: u64, +//! pub depends_on: Option => AsBytes, +//! pub nonce: u64, +//! pub reserved: Option => AsVec, +//! pub signature: Option => Maybe, +//! } +//! } +//! ``` +//! +//! What's going on here? First, some fields are encoded "as usual": unsigned integers +//! are encoded just fine and you likely won't need any different encoding. However, +//! some fields work in a different way. `depends_on` is a number that may be present +//! or absent, and it should be encoded as a byte sting. `U256` is already encoded this +//! way, but `None` is not ([`Option`] is not RLP-serializable on itself). So we wrap it +//! in a special wrapper: [`AsBytes`]. [`AsBytes`] will serialize `Some(T)` as `T` and +//! [`None`] as an empty byte string. +//! +//! `reserved` is a truly special struct that has custom encoding implemented for it. +//! That implementation serializes `Reserved` into a [`Vec`], and then serializes +//! this [`Vec`] to the output stream. If it is empty, an empty vector should be +//! written instead. This is achieved via [`AsVec`] annotation. +//! +//! [`Maybe`] is a third special wrapper. Fields annotated with [`Maybe`] may only be placed +//! last (otherwise encoding is ambiguous), and with [`Maybe`] `Some(T)` is serialized +//! as `T` and [`None`] --- as nothing (zero bytes added). +//! +//! Fields comments are omitted here for brevity, they are preserved as well. +//! +//! This macro adds both decoding and encoding capabilities. See examples folder +//! for more examples of usage, including custom types and machinery. +//! +//! Note that this syntax is not restricted to these three wrappers, you can use +//! any types with proper [`From`] implementation: +//! +//! ```rust +//! use thor_devkit::rlp_encodable; +//! +//! #[derive(Clone)] +//! struct MySeries { +//! left: [u8; 2], +//! right: [u8; 2], +//! } +//! +//! impl From for u32 { +//! fn from(value: MySeries) -> Self { +//! Self::from_be_bytes(value.left.into_iter().chain(value.right).collect::>().try_into().unwrap()) +//! } +//! } +//! impl From for MySeries { +//! fn from(value: u32) -> Self { +//! let [a, b, c, d] = value.to_be_bytes(); +//! Self{ left: [a, b], right: [c, d] } +//! } +//! } +//! +//! rlp_encodable! { +//! pub struct Foo { +//! pub foo: MySeries => u32, +//! } +//! } +//! ``` +//! + +pub use bytes::{Buf, BufMut, Bytes, BytesMut}; +pub use open_fastrlp::{Decodable, DecodeError as RLPError, Encodable, Header}; + +#[doc(hidden)] +#[macro_export] +macro_rules! __encode_as { + ($out:expr, $field:expr) => {{ + use $crate::rlp::Encodable; + $field.encode($out) + }}; + ($out:expr, $field:expr => $cast:ty) => {{ + use $crate::rlp::Encodable; + // TODO: this clone bugs me, we should be able to do better + <$cast>::from($field.clone()).encode($out) + }}; + + ($out:expr, $field:expr $(=> $cast:ty)?, $($fields:expr $(=> $casts:ty)?),+) => {{ + $crate::__encode_as! { $out, $field $(=> $cast)? } + $crate::__encode_as! { $out, $($fields $(=> $casts)?),+ } + }}; +} + +#[doc(hidden)] +#[macro_export] +macro_rules! __decode_as { + ($buf:expr, $field:ty) => {{ + #[allow(unused_imports)] + use $crate::rlp::Decodable; + <$field>::decode($buf)? + }}; + ($buf:expr, $field:ty => $cast:ty) => {{ + #[allow(unused_imports)] + use $crate::rlp::Decodable; + <$field>::from(<$cast>::decode($buf)?) + }}; + + ($buf:expr, $field:ty $(=> $cast:ty)?, $($fields:ty $(=> $casts:ty)?),+) => {{ + $crate::__decode_as! { $buf, $field $(=> $cast)? } + $crate::__decode_as! { $buf, $($fields $(=> $casts)?),+ } + }}; +} + +/// Create an RLP-encodable struct by specifying types to cast to. +#[macro_export] +macro_rules! rlp_encodable { + ( + $(#[$attr:meta])* + $vis:vis struct $name:ident { + $( + $(#[$field_attr:meta])* + $field_vis:vis $field_name:ident: $field_type:ty $(=> $cast:ty)?, + )* + } + ) => { + $(#[$attr])* + $vis struct $name { + $( + $(#[$field_attr])* + $field_vis $field_name: $field_type, + )* + } + + impl $name { + fn encode_internal(&self, out: &mut dyn $crate::rlp::BufMut) { + $crate::__encode_as!(out, $(self.$field_name $(=> $cast)?),+) + } + } + + impl $crate::rlp::Encodable for $name { + fn encode(&self, out: &mut dyn $crate::rlp::BufMut) { + let mut buf = $crate::rlp::BytesMut::new(); + self.encode_internal(&mut buf); + $crate::rlp::Header { + list: true, + payload_length: buf.len() + }.encode(out); + out.put_slice(&buf) + } + } + + impl $crate::rlp::Decodable for $name { + fn decode(buf: &mut &[u8]) -> Result { + $crate::rlp::Header::decode(buf)?; + Ok(Self { + $($field_name: $crate::__decode_as!(buf, $field_type $(=> $cast)? )),* + }) + } + } + } +} + +/// Serialization wrapper for `Option` to serialize `None` as empty `Bytes`. +/// +///
+/// Do not use it directly: it is only intended for use with `rlp_encodable!` macro. +///
+pub enum AsBytes { + #[doc(hidden)] + Just(T), + #[doc(hidden)] + Nothing, +} +impl Encodable for AsBytes { + fn encode(&self, out: &mut dyn BufMut) { + match self { + Self::Just(value) => value.encode(out), + Self::Nothing => Bytes::new().encode(out), + } + } +} +impl> From> for AsBytes { + fn from(value: Option) -> Self { + match value { + Some(v) => Self::Just(v.into()), + None => Self::Nothing, + } + } +} +impl From> for Option { + fn from(value: AsBytes) -> Self { + match value { + AsBytes::Just(v) => Self::Some(v), + AsBytes::Nothing => Self::None, + } + } +} +impl Decodable for AsBytes { + fn decode(buf: &mut &[u8]) -> Result { + if buf[0] == open_fastrlp::EMPTY_STRING_CODE { + Bytes::decode(buf)?; + Ok(Self::Nothing) + } else { + Ok(Self::Just(T::decode(buf)?)) + } + } +} + +/// Serialization wrapper for `Option` to serialize `None` as empty `Vec`. +/// +///
+/// Do not use it directly: it is only intended for use with `rlp_encodable!` macro. +///
+pub enum AsVec { + #[doc(hidden)] + Just(T), + #[doc(hidden)] + Nothing, +} +impl Encodable for AsVec { + fn encode(&self, out: &mut dyn BufMut) { + match self { + Self::Just(value) => value.encode(out), + Self::Nothing => Vec::::new().encode(out), + } + } +} +impl> From> for AsVec { + fn from(value: Option) -> Self { + match value { + Some(v) => Self::Just(v.into()), + None => Self::Nothing, + } + } +} +impl From> for Option { + fn from(value: AsVec) -> Self { + match value { + AsVec::Just(v) => Self::Some(v), + AsVec::Nothing => Self::None, + } + } +} +impl Decodable for AsVec { + fn decode(buf: &mut &[u8]) -> Result { + if buf[0] == open_fastrlp::EMPTY_LIST_CODE { + Vec::::decode(buf)?; + Ok(Self::Nothing) + } else { + Ok(Self::Just(T::decode(buf)?)) + } + } +} + +/// Serialization wrapper for `Option` to serialize `None` as nothing (do not modify +/// output stream). +/// +///
+/// Do not use it directly: it is only intended for use with `rlp_encodable!` macro. +///
+pub enum Maybe { + #[doc(hidden)] + Just(T), + #[doc(hidden)] + Nothing, +} +impl Encodable for Maybe { + fn encode(&self, out: &mut dyn BufMut) { + match self { + Self::Just(value) => value.encode(out), + Self::Nothing => (), + } + } +} +impl Decodable for Maybe { + fn decode(buf: &mut &[u8]) -> Result { + if buf.remaining() == 0 { + Ok(Self::Nothing) + } else { + Ok(Self::Just(T::decode(buf)?)) + } + } +} +impl> From> for Maybe { + fn from(value: Option) -> Self { + match value { + Some(v) => Self::Just(v.into()), + None => Self::Nothing, + } + } +} +impl From> for Option { + fn from(value: Maybe) -> Self { + match value { + Maybe::Just(v) => Self::Some(v), + Maybe::Nothing => Self::None, + } + } +} + +#[inline] +pub(crate) fn lstrip>(bytes: S) -> Vec { + bytes + .as_ref() + .iter() + .skip_while(|&&x| x == 0) + .copied() + .collect() +} + +#[inline] +pub(crate) fn static_left_pad( + data: &[u8], +) -> Result<[u8; N], open_fastrlp::DecodeError> { + if data.len() > N { + return Err(open_fastrlp::DecodeError::Overflow); + } + + let mut v = [0; N]; + + if data.is_empty() { + return Ok(v); + } + + if data[0] == 0 { + return Err(open_fastrlp::DecodeError::LeadingZero); + } + + // SAFETY: length checked above + unsafe { v.get_unchecked_mut(N - data.len()..) }.copy_from_slice(data); + Ok(v) +} diff --git a/src/transactions.rs b/src/transactions.rs index 4c15239..bed5f23 100644 --- a/src/transactions.rs +++ b/src/transactions.rs @@ -1,160 +1,61 @@ //! VeChain transactions support. use crate::address::{Address, AddressConvertible, PrivateKey}; +use crate::rlp::{ + lstrip, static_left_pad, AsBytes, AsVec, BufMut, Bytes, BytesMut, Decodable, Encodable, Maybe, + RLPError, +}; use crate::utils::blake2_256; -use alloy_rlp::{Buf, BufMut, RlpDecodable, RlpEncodable}; -pub use alloy_rlp::{Bytes, Decodable, Encodable}; -use ethereum_types::U256; +use crate::{rlp_encodable, U256}; use secp256k1::ecdsa::{RecoverableSignature, RecoveryId}; use secp256k1::{Message, PublicKey, Secp256k1}; - -pub(crate) fn lstrip>(bytes: S) -> Vec { - bytes - .as_ref() - .iter() - .skip_while(|&&x| x == 0) - .copied() - .collect() -} - -/// Represents a single VeChain transaction. -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct Transaction { - /// Chain tag - pub chain_tag: u8, - /// Previous block reference - /// - /// First 4 bytes (BE) are block height, the rest is part of referred block ID. - pub block_ref: u64, - /// Expiration (in blocks) - pub expiration: u32, - /// Vector of clauses - pub clauses: Vec, - /// Coefficient to calculate the gas price. - pub gas_price_coef: u8, - /// Maximal amount of gas to spend for transaction. - pub gas: u64, - /// Hash of transaction on which current transaction depends. - /// - /// May be left unspecified if this functionality is not necessary. - pub depends_on: Option, - /// Transaction nonce - pub nonce: u64, - /// Reserved fields. - pub reserved: Option, - /// Signature. 65 bytes for regular transactions, 130 - for VIP-191. - /// - /// Ignored when making a signing hash. - /// - /// For VIP-191 transactions, this would be a simple concatenation - /// of two signatures. - pub signature: Option, -} - -#[derive(Clone, Debug, Eq, PartialEq, RlpEncodable, RlpDecodable)] -struct WrappedInternalTransaction { - body: InternalTransaction, -} - -#[derive(Clone, Debug, Eq, PartialEq)] -struct InternalTransaction(Transaction); - -// TODO: add serde optional support -impl Encodable for InternalTransaction { - fn encode(&self, out: &mut dyn BufMut) { - self.0.chain_tag.encode(out); - self.0.block_ref.encode(out); - self.0.expiration.encode(out); - self.0.clauses.encode(out); - self.0.gas_price_coef.encode(out); - self.0.gas.encode(out); - if let Some(a) = self.0.depends_on.as_ref() { - let mut buf = [0; 32]; - a.to_big_endian(&mut buf); - Bytes::copy_from_slice(&buf).encode(out) - } else { - Bytes::new().encode(out); - } - self.0.nonce.encode(out); - if let Some(r) = self.0.reserved.as_ref() { - r.encode(out) - } else { - b"".to_vec().encode(out); - } - if let Some(s) = self.0.signature.as_ref() { - s.encode(out); - } - } -} -impl Decodable for InternalTransaction { - fn decode(buf: &mut &[u8]) -> Result { - alloy_rlp::Header::decode(buf)?; - let tx = Self(Transaction { - chain_tag: u8::decode(buf)?, - block_ref: u64::decode(buf)?, - expiration: u32::decode(buf)?, - clauses: Vec::::decode(buf)?, - gas_price_coef: u8::decode(buf)?, - gas: u64::decode(buf)?, - depends_on: { - let binary = Bytes::decode(buf)?; - if binary.is_empty() { - None - } else { - Some(U256::from_big_endian( - &static_left_pad::<32>(&binary).map_err(|_| { - alloy_rlp::Error::ListLengthMismatch { - expected: 32, - got: binary.len(), - } - })?, - )) - } - }, - nonce: u64::decode(buf)?, - reserved: { - let reserved = Reserved::decode(buf)?; - if reserved.is_empty() { - None - } else { - Some(reserved) - } - }, - signature: { - if buf.remaining() == 0 { - None - } else { - Some(Bytes::decode(buf)?) - } - }, - }); - if tx.0.signature_length_valid() { - Ok(tx) - } else { - Err(alloy_rlp::Error::ListLengthMismatch { - expected: if tx.0.is_delegated() { 130 } else { 65 }, - got: tx.0.signature.expect("Already checked to be present").len(), - }) - } +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +rlp_encodable! { + /// Represents a single VeChain transaction. + #[cfg_attr(feature="serde", serde_with::serde_as)] + #[cfg_attr(feature="serde", derive(Deserialize, Serialize))] + #[derive(Clone, Debug, Eq, PartialEq)] + pub struct Transaction { + /// Chain tag + #[serde(rename="chainTag")] + pub chain_tag: u8, + /// Previous block reference + /// + /// First 4 bytes (BE) are block height, the rest is part of referred block ID. + #[serde(rename="blockRef")] + pub block_ref: u64, + /// Expiration (in blocks) + pub expiration: u32, + /// Vector of clauses + pub clauses: Vec, + /// Coefficient to calculate the gas price. + #[serde(rename="gasPriceCoef")] + pub gas_price_coef: u8, + /// Maximal amount of gas to spend for transaction. + pub gas: u64, + /// Hash of transaction on which current transaction depends. + /// + /// May be left unspecified if this functionality is not necessary. + #[serde(rename="dependsOn")] + pub depends_on: Option => AsBytes, + /// Transaction nonceserde_as + pub nonce: u64, + /// Reserved fields. + pub reserved: Option => AsVec, + /// Signature. 65 bytes for regular transactions, 130 - for VIP-191. + /// + /// Ignored when making a signing hash. + /// + /// For VIP-191 transactions, this would be a simple concatenation + /// of two signatures. + #[cfg_attr(feature="serde", serde(with = "serde_with::As::>"))] + pub signature: Option => Maybe, } } -impl Encodable for Transaction { - fn encode(&self, out: &mut dyn BufMut) { - WrappedInternalTransaction { - body: InternalTransaction(self.clone()), - } - .encode(out) - } -} -impl Decodable for Transaction { - fn decode(buf: &mut &[u8]) -> Result { - let WrappedInternalTransaction { - body: InternalTransaction(clause), - } = WrappedInternalTransaction::decode(buf)?; - Ok(clause) - } -} +// TODO: add serde optional support impl Transaction { /// Gas cost for whole transaction execution. @@ -173,11 +74,7 @@ impl Transaction { //! Get a signing hash for this transaction with fee delegation. //! //! `VIP-191 ` - let mut encoded = Vec::with_capacity(1024); - let mut without_signature = self.clone(); - without_signature.signature = None; - without_signature.encode(&mut encoded); - let main_hash = blake2_256(&[encoded]); + let main_hash = self.get_signing_hash(); blake2_256(&[&main_hash[..], &delegate_for.to_fixed_bytes()[..]]) } @@ -349,7 +246,7 @@ impl Transaction { //! //! Returns `Err(secp256k1::Error::IncorrectSignature)` if signature is not set. if self.signature.is_some() { - let mut buf = alloy_rlp::BytesMut::new(); + let mut buf = BytesMut::new(); self.encode(&mut buf); Ok(buf.into()) } else { @@ -358,69 +255,19 @@ impl Transaction { } } -/// Represents a single transaction clause (recipient, value and data). -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct Clause { - /// Recipient - pub to: Option
, - /// Amount of funds to spend. - pub value: U256, - /// Contract code or other data. - pub data: Bytes, -} - -#[derive(Clone)] -struct InternalClause(Clause); -#[derive(Clone, RlpEncodable, RlpDecodable)] -struct WrappedInternalClause(InternalClause); - -impl Encodable for Clause { - fn encode(&self, out: &mut dyn BufMut) { - WrappedInternalClause(InternalClause(self.clone())).encode(out) - } -} -impl Decodable for Clause { - fn decode(buf: &mut &[u8]) -> Result { - let WrappedInternalClause(InternalClause(clause)) = WrappedInternalClause::decode(buf)?; - Ok(clause) - } -} - -impl Encodable for InternalClause { - fn encode(&self, out: &mut dyn BufMut) { - if let Some(a) = self.0.to { - a.encode(out) - } else { - Bytes::new().encode(out); - } - - let value = { - let mut buf = [0; 32]; - self.0.value.to_big_endian(&mut buf); - lstrip(buf) - }; - Bytes::from(value).encode(out); - - self.0.data.encode(out); - } -} -impl Decodable for InternalClause { - fn decode(buf: &mut &[u8]) -> Result { - Ok(Self(Clause { - to: { - let address = Address::decode(buf)?; - if address.to_fixed_bytes() == [0; 20] { - // None is zero address (contract creation). It is not - // distinguishable from real [0; 20] address by design: - // null address is a really existing one, parent of contracts. - None - } else { - Some(address) - } - }, - value: U256::from_big_endian(&static_left_pad::<32>(&Bytes::decode(buf)?)?), - data: Bytes::decode(buf)?, - })) +rlp_encodable! { + /// Represents a single transaction clause (recipient, value and data). + #[serde_with::serde_as] + #[cfg_attr(feature="serde", derive(Deserialize, Serialize))] + #[cfg_attr(feature="serde", derive(Clone, Debug, Eq, PartialEq))] + pub struct Clause { + /// Recipient + pub to: Option
=> AsBytes
, + /// Amount of funds to spend. + pub value: U256, + /// Contract code or other data. + #[cfg_attr(feature="serde", serde(with = "serde_with::As::"))] + pub data: Bytes, } } @@ -460,11 +307,17 @@ impl Clause { } /// Represents a transaction's ``reserved`` field. +#[cfg_attr(feature = "serde", serde_with::serde_as)] +#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))] #[derive(Clone, Debug, Eq, PartialEq)] pub struct Reserved { /// Features to enable (bitmask). pub features: u32, /// Currently unused field. + #[cfg_attr( + feature = "serde", + serde(with = "serde_with::As::>") + )] pub unused: Vec, } @@ -485,7 +338,7 @@ impl Encodable for Reserved { } impl Decodable for Reserved { - fn decode(buf: &mut &[u8]) -> Result { + fn decode(buf: &mut &[u8]) -> Result { if let Some((feature_bytes, unused)) = Vec::::decode(buf)?.split_first() { Ok(Self { features: u32::from_be_bytes(static_left_pad(feature_bytes)?), @@ -524,24 +377,3 @@ impl Reserved { self.features == 0 && self.unused.is_empty() } } - -#[inline] -pub(crate) fn static_left_pad(data: &[u8]) -> Result<[u8; N], alloy_rlp::Error> { - if data.len() > N { - return Err(alloy_rlp::Error::Overflow); - } - - let mut v = [0; N]; - - if data.is_empty() { - return Ok(v); - } - - if data[0] == 0 { - return Err(alloy_rlp::Error::LeadingZero); - } - - // SAFETY: length checked above - unsafe { v.get_unchecked_mut(N - data.len()..) }.copy_from_slice(data); - Ok(v) -} diff --git a/src/utils.rs b/src/utils.rs index 8568dcf..92e2676 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,4 +1,3 @@ -use crate::address::AddressValidationError; use blake2::{digest::consts::U32, Blake2b, Digest}; use tiny_keccak::{Hasher, Keccak}; @@ -23,20 +22,419 @@ pub fn keccak>(bytes: S) -> [u8; 32] { hash } -#[cfg(not(tarpaulin_include))] -pub fn decode_hex(s: &str) -> Result, AddressValidationError> { - //! Convert a hex string (with or without 0x prefix) to binary. - let prefix = if s.starts_with("0x") { 2 } else { 0 }; - (0..s.len()) - .skip(prefix) - .step_by(2) - .map(|i| { - u8::from_str_radix( - s.get(i..i + 2) - .ok_or(AddressValidationError::InvalidLength)?, - 16, +#[cfg(feature = "serde")] +pub(crate) mod unhex { + use crate::U256; + use rustc_hex::{FromHex, ToHex}; + use serde::de::Error; + use serde::{Deserialize, Deserializer, Serializer}; + use serde_with::de::DeserializeAs; + use serde_with::formats::{Format, Lowercase, Uppercase}; + use serde_with::ser::SerializeAs; + use std::any::type_name; + use std::borrow::Cow; + use std::convert::{TryFrom, TryInto}; + use std::marker::PhantomData; + + #[derive(Copy, Clone, Debug, Default)] + pub struct Hex(PhantomData); + + impl> SerializeAs for Hex { + fn serialize_as(source: &T, serializer: S) -> Result { + serializer.serialize_str(&("0x".to_string() + &source.as_ref().to_hex::())) + } + } + + impl> SerializeAs for Hex { + fn serialize_as(source: &T, serializer: S) -> Result { + serializer.serialize_str( + &("0x".to_string() + &source.as_ref().to_hex::().to_uppercase()), ) - .map_err(|_| AddressValidationError::InvalidHex) - }) - .collect() + } + } + + impl<'de, T, FORMAT> DeserializeAs<'de, T> for Hex + where + T: TryFrom>, + FORMAT: Format, + { + fn deserialize_as>(deserializer: D) -> Result { + as Deserialize<'de>>::deserialize(deserializer) + .and_then(|s| { + s.strip_prefix("0x") + .unwrap_or(&s) + .from_hex() + .map_err(Error::custom) + }) + .and_then(|vec: Vec| { + let length = vec.len(); + vec.try_into().map_err(|_e: T::Error| { + Error::custom(format!( + "Can't convert a Byte Vector of length {} to {}", + length, + type_name::(), + )) + }) + }) + } + } + + pub trait BeBytesConvertible + where + Self: Sized, + { + fn from_be_bytes(src: [u8; N]) -> Self; + fn to_be_bytes_(self) -> [u8; N]; + } + + macro_rules! impl_from_be_bytes { + ($t:ty) => { + impl BeBytesConvertible<{ <$t>::BITS as usize / 8 }> for $t { + fn from_be_bytes(src: [u8; <$t>::BITS as usize / 8]) -> Self { + Self::from_be_bytes(src) + } + fn to_be_bytes_(self) -> [u8; <$t>::BITS as usize / 8] { + self.to_be_bytes() + } + } + }; + } + impl_from_be_bytes!(u64); + impl_from_be_bytes!(u32); + impl_from_be_bytes!(u16); + impl BeBytesConvertible<32> for U256 { + fn from_be_bytes(src: [u8; 32]) -> Self { + Self::from_big_endian(&src) + } + fn to_be_bytes_(self) -> [u8; 32] { + let mut buf = [0; 32]; + self.to_big_endian(&mut buf); + buf + } + } + + impl BeBytesConvertible<1> for u8 { + fn from_be_bytes(src: [u8; 1]) -> Self { + src[0] + } + fn to_be_bytes_(self) -> [u8; 1] { + [self] + } + } + + #[derive(Copy, Clone, Debug, Default)] + pub struct HexNum, FORMAT: Format = Lowercase>( + PhantomData, + PhantomData, + ); + + impl> SerializeAs for HexNum { + fn serialize_as(source: &T, serializer: S) -> Result { + serializer + .serialize_str(&("0x".to_string() + &source.to_be_bytes_().to_hex::())) + } + } + + impl> SerializeAs for HexNum { + fn serialize_as(source: &T, serializer: S) -> Result { + serializer.serialize_str( + &("0x".to_string() + &source.to_be_bytes_().to_hex::().to_uppercase()), + ) + } + } + + impl<'de, const N: usize, Type: BeBytesConvertible, T, FORMAT> DeserializeAs<'de, T> + for HexNum + where + T: From, + FORMAT: Format, + { + fn deserialize_as>(deserializer: D) -> Result { + as Deserialize<'de>>::deserialize(deserializer) + .and_then(|s| { + let stripped = s.strip_prefix("0x").unwrap_or(&s); + let padded = if stripped.len() % 2 == 0 { + stripped.to_string() + } else { + "0".to_string() + stripped + }; + padded.from_hex().map_err(Error::custom) + }) + .and_then(|vec: Vec| { + let length = vec.len(); + Ok(Type::from_be_bytes(static_left_pad::(&vec).map_err(|_| { + Error::custom(format!( + "Can't convert a Byte Vector of length {} to {}", + length, + type_name::(), + )) + })?) + .into()) + }) + } + } + + #[inline] + #[cfg(not(tarpaulin_include))] + fn static_left_pad(data: &[u8]) -> Result<[u8; N], open_fastrlp::DecodeError> { + // Similar to RLP padding, but allows leading zero. Tested there. + if data.len() > N { + return Err(open_fastrlp::DecodeError::Overflow); + } + + let mut v = [0; N]; + + if data.is_empty() { + return Ok(v); + } + + // SAFETY: length checked above + unsafe { v.get_unchecked_mut(N - data.len()..) }.copy_from_slice(data); + Ok(v) + } +} + +#[cfg(feature = "serde")] +#[cfg(test)] +mod test { + use super::super::rlp::Bytes; + use super::super::U256; + use super::unhex::*; + use serde::{Deserialize, Serialize}; + use serde_json::{from_str, json, to_value}; + use serde_with::formats::{Lowercase, Uppercase}; + + #[test] + fn test_numbers() { + #[serde_with::serde_as] + #[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)] + struct Test { + #[serde_as(as = "HexNum<1, u8>")] + a: u8, + #[serde_as(as = "HexNum<2, u16>")] + b: u16, + #[serde_as(as = "HexNum<4, u32>")] + c: u32, + #[serde_as(as = "HexNum<8, u64>")] + d: u64, + #[serde_as(as = "HexNum<32, U256>")] + e: U256, + } + assert_eq!( + to_value(Test { + a: 0, + b: 0, + c: 0, + d: 0, + e: 0.into() + }) + .expect("Works"), + json! {{ + "a": "0x00", + "b": "0x0000", + "c": "0x00000000", + "d": "0x0000000000000000", + "e": "0x0000000000000000000000000000000000000000000000000000000000000000", + }} + ); + assert_eq!( + from_str::( + r#"{ + "a": "0x00", + "b": "0x0000", + "c": "0x00000000", + "d": "0x0000000000000000", + "e": "0x0000000000000000000000000000000000000000000000000000000000000000" + }"# + ) + .expect("Must parse"), + Test { + a: 0, + b: 0, + c: 0, + d: 0, + e: 0.into() + }, + ); + } + + #[test] + fn test_numbers_padding() { + #[serde_with::serde_as] + #[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)] + struct Test { + #[serde_as(as = "HexNum<2, u16>")] + a: u16, + } + assert_eq!( + from_str::(r#"{"a": "0x1"}"#).expect("Must parse"), + Test { a: 1_u16 }, + ); + } + + #[test] + fn test_numbers_fail_too_long() { + #[serde_with::serde_as] + #[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)] + struct Test { + #[serde_as(as = "HexNum<2, u16>")] + a: u16, + } + assert_eq!( + from_str::(r#"{"a": "0x01010101"}"#) + .expect_err("Must not parse") + .to_string(), + "Can't convert a Byte Vector of length 4 to u16 at line 1 column 19" + ); + } + + #[test] + fn test_wrapped_numbers() { + #[serde_with::serde_as] + #[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)] + struct Test { + #[serde_as(as = "Option>")] + a: Option, + #[serde_as(as = "Option>")] + b: Option, + #[serde_as(as = "Vec>")] + c: Vec, + } + assert_eq!( + to_value(Test { + a: Some(0x0F), + b: None, + c: vec![1, 0x0E], + }) + .expect("Works"), + json! {{ + "a": "0x0F", + "b": Option::::None, + "c": vec!["0x0001", "0x000e"], + }} + ); + assert_eq!( + from_str::( + r#"{ + "a": "0x0f", + "b": null, + "c": ["0x0001", "0x000E"] + }"# + ) + .expect("Must parse"), + Test { + a: Some(0x0F), + b: None, + c: vec![1, 0x0E], + } + ); + } + + #[test] + fn test_hex_strings() { + #[serde_with::serde_as] + #[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)] + struct Test { + #[serde_as(as = "Option")] + a: Option>, + #[serde_as(as = "Option>")] + b: Option, + #[serde_as(as = "Option>")] + c: Option, + #[serde_as(as = "Vec>")] + d: Vec, + #[serde_as(as = "Vec>")] + e: Vec, + #[serde_as(as = "Vec>")] + f: Vec, + } + assert_eq!( + to_value(Test { + a: Some(vec![]), + b: None, + c: Some(Bytes::copy_from_slice(&b"\x01\x0F"[..])), + d: vec![Bytes::copy_from_slice(&b"\x01\x0F"[..])], + e: vec![], + f: vec![Bytes::copy_from_slice(&b"\x01\x0F"[..])], + }) + .expect("Works"), + json! {{ + "a": "0x", + "b": Option::::None, + "c": "0x010f", + "d": vec!["0x010F"], + "e": Vec::::new(), + "f": vec!["0x010f"] + }} + ); + assert_eq!( + from_str::( + r#"{ + "a": "0x", + "b": null, + "c": "0x010f", + "d": ["0x010F"], + "e": [], + "f": ["0x010f"] + }"# + ) + .expect("Must parse"), + Test { + a: Some(vec![]), + b: None, + c: Some(Bytes::copy_from_slice(&b"\x01\x0F"[..])), + d: vec![Bytes::copy_from_slice(&b"\x01\x0F"[..])], + e: vec![], + f: vec![Bytes::copy_from_slice(&b"\x01\x0F"[..])], + } + ); + } + + #[test] + fn test_string_fail_too_long() { + #[serde_with::serde_as] + #[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)] + struct Test { + #[serde_as(as = "Hex")] + a: [u8; 3], + } + assert_eq!( + from_str::(r#"{"a": "0x01010101"}"#) + .expect_err("Must not parse") + .to_string(), + "Can't convert a Byte Vector of length 4 to [u8; 3] at line 1 column 19" + ); + } + + #[test] + fn test_string_fail_too_short() { + #[serde_with::serde_as] + #[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)] + struct Test { + #[serde_as(as = "Hex")] + a: [u8; 5], + } + assert_eq!( + from_str::(r#"{"a": "0x01010101"}"#) + .expect_err("Must not parse") + .to_string(), + "Can't convert a Byte Vector of length 4 to [u8; 5] at line 1 column 19" + ); + } + + #[test] + fn test_string_fail_bad_hex() { + #[serde_with::serde_as] + #[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)] + struct Test { + #[serde_as(as = "Hex")] + a: [u8; 5], + } + assert_eq!( + from_str::(r#"{"a": "0x0101010G"}"#) + .expect_err("Must not parse") + .to_string(), + "Invalid character 'G' at position 7 at line 1 column 19" + ); + } } diff --git a/tests/test_address.rs b/tests/test_address.rs index d728e34..c465ec2 100644 --- a/tests/test_address.rs +++ b/tests/test_address.rs @@ -1,4 +1,4 @@ -use thor_devkit::address::*; +use thor_devkit::{Address, AddressConvertible, PublicKey}; #[test] fn test_upubkey_to_address() { diff --git a/tests/test_hdnode.rs b/tests/test_hdnode.rs index 72bbfa4..4cc68b2 100644 --- a/tests/test_hdnode.rs +++ b/tests/test_hdnode.rs @@ -1,11 +1,15 @@ use bip32::{ExtendedKey, ExtendedKeyAttrs, Prefix}; -use thor_devkit::decode_hex; +use rustc_hex::FromHex; use thor_devkit::hdnode::*; +fn decode_hex(hex: &str) -> Vec { + hex.from_hex().unwrap() +} + #[test] fn test_from_seed() { //! Test vectors from https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki - let seed = decode_hex("fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9c6c3c0bdbab7b4b1aeaba8a5a29f9c999693908d8a8784817e7b7875726f6c696663605d5a5754514e4b484542").unwrap(); + let seed = decode_hex("fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9c6c3c0bdbab7b4b1aeaba8a5a29f9c999693908d8a8784817e7b7875726f6c696663605d5a5754514e4b484542"); let node = HDNode::build() .seed(seed.clone().try_into().unwrap()) .path("m".parse().unwrap()) @@ -56,12 +60,12 @@ fn test_from_mnemonic_vet() { .public_key() .serialize_uncompressed() .to_vec(), - decode_hex(public).unwrap(), + decode_hex(public), "Public key differs" ); assert_eq!( node.chain_code().to_vec(), - decode_hex(chain_code).unwrap(), + decode_hex(chain_code), "Chain code differs" ); let same_node = HDNode::build() @@ -118,7 +122,7 @@ fn test_from_mnemonic_vet() { #[test] fn test_derive() { //! Test vectors from https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki - let seed = decode_hex("fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9c6c3c0bdbab7b4b1aeaba8a5a29f9c999693908d8a8784817e7b7875726f6c696663605d5a5754514e4b484542").unwrap(); + let seed = decode_hex("fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9c6c3c0bdbab7b4b1aeaba8a5a29f9c999693908d8a8784817e7b7875726f6c696663605d5a5754514e4b484542"); // Not hardened let node = HDNode::build() .seed(seed.clone().try_into().unwrap()) @@ -271,7 +275,6 @@ fn test_build() { HDNode::build() .master_private_key_bytes( decode_hex("00e4a2687ec443f4d23b6ba9e7d904a31acdda90032b34aa0e642e6dd3fd36f682") - .unwrap() .try_into() .unwrap(), [0; 32], @@ -281,7 +284,6 @@ fn test_build() { HDNode::build() .master_public_key_bytes( decode_hex("035A784662A4A20A65BF6AAB9AE98A6C068A81C52E4B032C0FB5400C706CFCCC56") - .unwrap() .try_into() .unwrap(), [0; 32], diff --git a/tests/test_network.rs b/tests/test_network.rs new file mode 100644 index 0000000..64e70dc --- /dev/null +++ b/tests/test_network.rs @@ -0,0 +1,479 @@ +use rustc_hex::FromHex; + +fn decode_hex(hex: &str) -> Vec { + hex.from_hex().unwrap() +} + +#[cfg(feature = "http")] +#[cfg(test)] +mod test_network { + use super::*; + use thor_devkit::network::*; + use thor_devkit::rlp::Bytes; + use thor_devkit::transactions::*; + + fn existing_tx_id() -> [u8; 32] { + decode_hex("ea4c3d8b830f777ae55052bd92f2c65ae9f6c36eb391ac52e8e77d5d2bf5f308") + .try_into() + .unwrap() + } + fn existing_block_id() -> [u8; 32] { + decode_hex("0107b6875c70deb02eda7a6724891e7774b34b8aecc57d2898f36384c6a6868c") + .try_into() + .unwrap() + } + + fn transaction_details() -> (Transaction, TransactionMeta) { + let clause_data = b"\xb3\x91\xc7\xd3vtho-usd\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x84\x17\x19\x1a\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0e\x7f\x8eJ"; + let signature = b"\xbf3\xd0\0\xd9\xa8\x93\x10\xd4\xda\xddy@\x03\xd5\x1e.\x86\x80\x8b#HQ)\xa4|\xcaE\xf5>Ib_\xd3q>?i\x99\x17X\xc5u\xf7\x12\xc7\xd23\x15\xf6\xe6Z+D\xd3\x19\x0b\xf7\x0c\r=\x85{\xc6\0\x8d5\xb2\xf1\xff\xe2b4\x8d\x17yj\x9a\xc1%\xf8\x9e\xe2b\x83\xa4\xf9F\xb79H\xe6\x80\x11\x1eWTm\x08\x8b\xec\t1\xe3\xae\\7\xae\xe2e\xc9\xaa|\x11L\xfc\x87h\xabi\xe1L\xeez\xdc\x90\xdb\r\xfc\x01"; + + let tx = Transaction { + chain_tag: 39, + block_ref: 74228606445726694, + expiration: 32, + clauses: vec![Clause { + to: Some( + "0x12e3582d7ca22234f39d2a7be12c98ea9c077e25" + .parse() + .unwrap(), + ), + value: 0.into(), + data: Bytes::copy_from_slice(&clause_data[..]), + }], + gas_price_coef: 128, + gas: 79481, + depends_on: None, + nonce: 1702858315418, + reserved: Some(Reserved { + features: 1, + unused: vec![], + }), + signature: Some(Bytes::copy_from_slice(&signature[..])), + }; + let meta = TransactionMeta { + block_id: "0107b6875c70deb02eda7a6724891e7774b34b8aecc57d2898f36384c6a6868c" + .from_hex::>() + .unwrap() + .try_into() + .unwrap(), + block_number: 17282695, + block_timestamp: 1702858320, + }; + (tx, meta) + } + fn block_details() -> (BlockInfo, Vec) { + let info = BlockInfo { + number: 17282695, + id: existing_block_id(), + size: 655, + parent_id: decode_hex( + "0107b686375eabe6821225b0218ef5d51d0933756ca95d558c0a6b010f45f503", + ) + .try_into() + .unwrap(), + timestamp: 1702858320, + gas_limit: 30000000, + beneficiary: "0xb4094c25f86d628fdd571afc4077f0d0196afb48" + .parse() + .unwrap(), + gas_used: 37918, + total_score: 136509202, + txs_root: decode_hex( + "0e3d83681601227e22c2eaa3dd5ef1c3301fe23dd7db21ac15984d7b6e2c6552", + ) + .try_into() + .unwrap(), + txs_features: 1, + state_root: decode_hex( + "dc94484d4f0d01b068dc1d66c5731dd36b32ba3b08bcfad977d599e5c9342dd1", + ) + .try_into() + .unwrap(), + receipts_root: decode_hex( + "df5066746c62904390f2312e81fb7a98811098627a2ae8474457e69cd60b846a", + ) + .try_into() + .unwrap(), + com: true, + signer: "0x0771bc0fe8b6dcf72372440c79a309e92ffb93e8" + .parse() + .unwrap(), + is_trunk: true, + is_finalized: true, + }; + let clause_data= b"\xb3\x91\xc7\xd3vtho-usd\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x84\x17\x19\x1a\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0e\x7f\x8eJ"; + let exec_data= b"vtho-usd\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x84\x17\x19\x1a\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0e\x7f\x8eJ"; + let transaction = BlockTransaction { + transaction: ExtendedTransaction { + id: [ + 234, 76, 61, 139, 131, 15, 119, 122, 229, 80, 82, 189, 146, 242, 198, 90, 233, + 246, 195, 110, 179, 145, 172, 82, 232, 231, 125, 93, 43, 245, 243, 8, + ], + origin: "0x56cb0e0276ad689cc68954d47460cd70f46244dc" + .parse() + .unwrap(), + delegator: Some( + "0xeedfd966da350803ba7fc8f40f6a3151164e2058" + .parse() + .unwrap(), + ), + size: 290, + chain_tag: 39, + block_ref: 74228606445726694, + expiration: 32, + clauses: vec![Clause { + to: Some( + "0x12e3582d7ca22234f39d2a7be12c98ea9c077e25" + .parse() + .unwrap(), + ), + value: 0.into(), + data: Bytes::copy_from_slice(clause_data), + }], + gas_price_coef: 128, + gas: 79481, + depends_on: None, + nonce: 1702858315418, + }, + receipt: Receipt { + gas_used: 37918, + gas_payer: "0xeedfd966da350803ba7fc8f40f6a3151164e2058" + .parse() + .unwrap(), + paid: 569513490196068766_u128.into(), + reward: 170854047058820629_u128.into(), + reverted: false, + outputs: vec![ReceiptOutput { + contract_address: None, + events: vec![Event { + address: "0x12e3582d7ca22234f39d2a7be12c98ea9c077e25" + .parse() + .unwrap(), + topics: vec![[ + 239, 200, 244, 4, 28, 11, 164, 208, 151, 229, 75, 206, 126, 91, 196, + 126, 197, 180, 92, 2, 112, 196, 35, 121, 196, 182, 145, 200, 89, 67, + 237, 240, + ]], + data: Bytes::copy_from_slice(exec_data), + }], + transfers: vec![], + }], + }, + }; + (info, vec![transaction]) + } + + #[tokio::test] + async fn test_fetch_existing() { + let client = ThorNode::testnet(); + let (tx, meta) = client + .fetch_transaction(existing_tx_id()) + .await + .unwrap() + .expect("Must be found"); + let (tx_expected, meta_expected) = transaction_details(); + assert_eq!(tx, tx_expected); + assert_eq!(meta.expect("Meta should be found"), meta_expected); + } + + #[tokio::test] + async fn test_fetch_existing_ext() { + let client = ThorNode::testnet(); + let (tx, meta) = client + .fetch_extended_transaction(existing_tx_id()) + .await + .unwrap() + .expect("Must be found"); + let (tx_expected, meta_expected) = transaction_details(); + let Transaction { + chain_tag, + block_ref, + expiration, + clauses, + gas_price_coef, + gas, + depends_on, + nonce, + .. + } = tx_expected.clone(); + assert_eq!( + tx, + ExtendedTransaction { + chain_tag, + block_ref, + expiration, + clauses, + gas_price_coef, + gas, + depends_on, + nonce, + size: 290, + delegator: Some( + "0xeedfd966da350803ba7fc8f40f6a3151164e2058" + .parse() + .unwrap() + ), + id: existing_tx_id(), + origin: "0x56cb0e0276ad689cc68954d47460cd70f46244dc" + .parse() + .unwrap() + } + ); + assert_eq!( + tx.as_transaction(), + Transaction { + signature: None, + ..tx_expected + } + ); + assert_eq!(meta.expect("Meta should be found"), meta_expected); + } + + #[tokio::test] + async fn test_fetch_existing_receipt() { + let client = ThorNode::testnet(); + let (receipt, meta) = client + .fetch_transaction_receipt(existing_tx_id()) + .await + .expect("Must not fail") + .expect("Must have been found"); + let clause_data = b"vtho-usd\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x84\x17\x19\x1a\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0e\x7f\x8eJ"; + let expected = Receipt { + gas_used: 37918, + gas_payer: "0xeedfd966da350803ba7fc8f40f6a3151164e2058" + .parse() + .unwrap(), + paid: 569513490196068766_u64.into(), + reward: 170854047058820629_u64.into(), + reverted: false, + outputs: vec![ReceiptOutput { + contract_address: None, + events: vec![Event { + address: "0x12e3582d7ca22234f39d2a7be12c98ea9c077e25" + .parse() + .unwrap(), + topics: vec![[ + 239, 200, 244, 4, 28, 11, 164, 208, 151, 229, 75, 206, 126, 91, 196, 126, + 197, 180, 92, 2, 112, 196, 35, 121, 196, 182, 145, 200, 89, 67, 237, 240, + ]], + data: Bytes::copy_from_slice(&clause_data[..]), + }], + transfers: vec![], + }], + }; + let expected_meta = ReceiptMeta { + block_id: [ + 1, 7, 182, 135, 92, 112, 222, 176, 46, 218, 122, 103, 36, 137, 30, 119, 116, 179, + 75, 138, 236, 197, 125, 40, 152, 243, 99, 132, 198, 166, 134, 140, + ], + block_number: 17282695, + block_timestamp: 1702858320, + tx_id: [ + 234, 76, 61, 139, 131, 15, 119, 122, 229, 80, 82, 189, 146, 242, 198, 90, 233, 246, + 195, 110, 179, 145, 172, 82, 232, 231, 125, 93, 43, 245, 243, 8, + ], + tx_origin: "0x56cb0e0276ad689cc68954d47460cd70f46244dc" + .parse() + .unwrap(), + }; + assert_eq!(receipt, expected); + assert_eq!(meta, expected_meta); + } + + #[tokio::test] + async fn test_fetch_existing_receipt_with_transfers() { + let client = ThorNode::mainnet(); + let (receipt, meta) = client + .fetch_transaction_receipt( + decode_hex("1755319a898a52fbceb4c58f51d63e6fa53592678512dd786de953d55895946a") + .try_into() + .unwrap(), + ) + .await + .expect("Must not fail") + .expect("Must have been found"); + let expected = Receipt { + gas_used: 21000, + gas_payer: "0xb0c224a96655ba8d51f35f98068f5fc12f930946" + .parse() + .unwrap(), + paid: 210000000000000000_u64.into(), + reward: 63000000000000000_u64.into(), + reverted: false, + outputs: vec![ReceiptOutput { + contract_address: None, + events: vec![], + transfers: vec![Transfer { + sender: "0xb0c224a96655ba8d51f35f98068f5fc12f930946" + .parse() + .unwrap(), + recipient: "0xfe33b406664f7cc03ede326e695c06c211a45a48" + .parse() + .unwrap(), + amount: 5733123118820000000000_u128.into(), + }], + }], + }; + let expected_meta = ReceiptMeta { + block_id: [ + 1, 7, 123, 88, 83, 194, 165, 215, 242, 232, 190, 239, 4, 90, 10, 32, 219, 20, 208, + 49, 108, 178, 89, 39, 71, 129, 5, 138, 112, 103, 10, 247, + ], + block_number: 17267544, + block_timestamp: 1703201660, + tx_id: [ + 23, 85, 49, 154, 137, 138, 82, 251, 206, 180, 197, 143, 81, 214, 62, 111, 165, 53, + 146, 103, 133, 18, 221, 120, 109, 233, 83, 213, 88, 149, 148, 106, + ], + tx_origin: "0xb0c224a96655ba8d51f35f98068f5fc12f930946" + .parse() + .unwrap(), + }; + assert_eq!(receipt, expected); + assert_eq!(meta, expected_meta); + } + + #[tokio::test] + async fn test_fetch_block() { + let client = ThorNode::testnet(); + let (blockinfo, transactions) = client + .fetch_block(BlockReference::ID( + decode_hex("0107b6875c70deb02eda7a6724891e7774b34b8aecc57d2898f36384c6a6868c") + .try_into() + .unwrap(), + )) + .await + .expect("Must not fail") + .expect("Must have been found"); + + let (expected_info, expected_transactions) = block_details(); + let expected_transactions: Vec<_> = expected_transactions + .into_iter() + .map(|t| t.transaction.id) + .collect(); + assert_eq!(blockinfo, expected_info); + assert_eq!(transactions, expected_transactions); + } + + #[tokio::test] + async fn test_fetch_block_by_number() { + let client = ThorNode::testnet(); + let (blockinfo, transactions) = client + .fetch_block(BlockReference::Number(0x0107b687)) + .await + .expect("Must not fail") + .expect("Must have been found"); + + let (expected_info, expected_transactions) = block_details(); + let expected_transactions: Vec<_> = expected_transactions + .into_iter() + .map(|t| t.transaction.id) + .collect(); + assert_eq!(blockinfo, expected_info); + assert_eq!(transactions, expected_transactions); + } + + #[tokio::test] + async fn test_fetch_block_latest() { + let client = ThorNode::testnet(); + let res = client + .fetch_block(BlockReference::Best) + .await + .expect("Must not fail"); + assert!(res.is_some()); + } + + #[tokio::test] + async fn test_fetch_block_finalized() { + let client = ThorNode::testnet(); + let res = client + .fetch_block(BlockReference::Finalized) + .await + .expect("Must not fail"); + assert!(res.is_some()); + } + + #[tokio::test] + async fn test_fetch_block_expanded() { + let client = ThorNode::testnet(); + let (blockinfo, transactions) = client + .fetch_block_expanded(BlockReference::ID(existing_block_id())) + .await + .expect("Must not fail") + .expect("Must have been found"); + + let (expected_info, expected_transactions) = block_details(); + assert_eq!(blockinfo, expected_info); + assert_eq!(transactions, expected_transactions); + } + + #[tokio::test] + async fn test_fetch_nonexisting() { + let client = ThorNode::testnet(); + let result = client + .fetch_transaction( + decode_hex("ea0c3d8b830f777ae55052bd92f2c65ae9f6c36eb391ac52e8e77d5d2bf5f308") + .try_into() + .unwrap(), + ) + .await + .expect("Must not fail"); + assert!(result.is_none()); + } + + #[tokio::test] + async fn test_fetch_nonexisting_ext() { + let client = ThorNode::testnet(); + let result = client + .fetch_extended_transaction( + decode_hex("ea0c3d8b830f777ae55052bd92f2c65ae9f6c36eb391ac52e8e77d5d2bf5f308") + .try_into() + .unwrap(), + ) + .await + .expect("Must not fail"); + assert!(result.is_none()); + } + + #[tokio::test] + async fn test_fetch_nonexisting_receipt() { + let client = ThorNode::testnet(); + let result = client + .fetch_transaction_receipt( + decode_hex("ea0c3d8b830f777ae55052bd92f2c65ae9f6c36eb391ac52e8e77d5d2bf5f308") + .try_into() + .unwrap(), + ) + .await + .expect("Must not fail"); + assert!(result.is_none()); + } + + #[tokio::test] + async fn test_fetch_nonexisting_block() { + let client = ThorNode::testnet(); + let result = client + .fetch_block(BlockReference::ID( + decode_hex("0107b6875c70deb02eda7a6724891e7774b34b8aecc57d2898f36384c6a6868d") + .try_into() + .unwrap(), + )) + .await + .expect("Must not fail"); + assert!(result.is_none()); + } + + #[tokio::test] + async fn test_fetch_nonexisting_block_ext() { + let client = ThorNode::testnet(); + let result = client + .fetch_block_expanded(BlockReference::ID( + decode_hex("0107b6875c70deb02eda7a6724891e7774b34b8aecc57d2898f36384c6a6868d") + .try_into() + .unwrap(), + )) + .await + .expect("Must not fail"); + assert!(result.is_none()); + } +} diff --git a/tests/test_transactions.rs b/tests/test_transactions.rs index 28556f1..4b94434 100644 --- a/tests/test_transactions.rs +++ b/tests/test_transactions.rs @@ -1,15 +1,21 @@ +use rustc_hex::FromHex; use secp256k1::Secp256k1; -use thor_devkit::address::{AddressConvertible, PrivateKey}; +use thor_devkit::rlp::{Bytes, Decodable, Encodable, RLPError}; use thor_devkit::transactions::*; -use thor_devkit::{decode_hex, U256}; +use thor_devkit::U256; +use thor_devkit::{AddressConvertible, PrivateKey}; + +fn decode_hex(hex: &str) -> Vec { + hex.from_hex().unwrap() +} const PK_STRING: &str = "7582be841ca040aa940fff6c05773129e135623e41acce3e0b8ba520dc1ae26a"; macro_rules! make_pk { () => { - PrivateKey::from_slice(&decode_hex(PK_STRING).unwrap()).unwrap() + PrivateKey::from_slice(&decode_hex(PK_STRING)).unwrap() }; ($hex:expr) => { - PrivateKey::from_slice(&decode_hex($hex).unwrap()).unwrap() + PrivateKey::from_slice(&decode_hex($hex)).unwrap() }; } @@ -66,7 +72,7 @@ fn test_rlp_encode_basic() { let tx = undelegated_tx!(); let expected = decode_hex( "f8540184aabbccdd20f840df947567d83b7b8d80addcb281a71d54fc7b3364ffed82271086000000606060df947567d83b7b8d80addcb281a71d54fc7b3364ffed824e208600000060606081808252088083bc614ec0" - ).unwrap(); + ); let mut buf = vec![]; tx.encode(&mut buf); assert_eq!(buf, expected); @@ -91,7 +97,7 @@ fn test_rlp_encode_basic_contract() { }], ..undelegated_tx!() }; - let expected = decode_hex("d90184aabbccdd20c6c5808082123481808252088083bc614ec0").unwrap(); + let expected = decode_hex("d90184aabbccdd20c6c5808082123481808252088083bc614ec0"); let mut buf = vec![]; tx.encode(&mut buf); buf.iter().for_each(|c| print!("{:02x?}", c)); @@ -108,7 +114,7 @@ fn test_rlp_encode_delegated() { let tx = delegated_tx!(); let expected = decode_hex( "f85a0184aabbccdd20f840df947567d83b7b8d80addcb281a71d54fc7b3364ffed82271086000000606060df947567d83b7b8d80addcb281a71d54fc7b3364ffed824e208600000060606081808252088083bc614ec6018431323334" - ).unwrap(); + ); let mut buf = vec![]; tx.encode(&mut buf); assert_eq!(buf, expected); @@ -130,7 +136,7 @@ fn test_rlp_encode_reserved_unused_untrimmed() { }; let expected = decode_hex( "f8540184aabbccdd20f840df947567d83b7b8d80addcb281a71d54fc7b3364ffed82271086000000606060df947567d83b7b8d80addcb281a71d54fc7b3364ffed824e208600000060606081808252088083bc614ec0" - ).unwrap(); + ); let mut buf = vec![]; tx.encode(&mut buf); assert_eq!(buf, expected); @@ -172,15 +178,14 @@ fn test_rlp_encode_reserved_can_be_omitted() { fn test_rlp_encode_depends_on() { // Verified on-chain after signing. let tx = Transaction { - depends_on: Some(U256::from_big_endian( - &decode_hex("360341090d2c4a01fa7da816c57d51c0b2fa3fcf1f99141806efc99f568c0b2a") - .unwrap(), - )), + depends_on: Some(U256::from_big_endian(&decode_hex( + "360341090d2c4a01fa7da816c57d51c0b2fa3fcf1f99141806efc99f568c0b2a", + ))), ..undelegated_tx!() }; let mut buf = vec![]; tx.encode(&mut buf); - let expected = decode_hex("f8740184aabbccdd20f840df947567d83b7b8d80addcb281a71d54fc7b3364ffed82271086000000606060df947567d83b7b8d80addcb281a71d54fc7b3364ffed824e20860000006060608180825208a0360341090d2c4a01fa7da816c57d51c0b2fa3fcf1f99141806efc99f568c0b2a83bc614ec0").unwrap(); + let expected = decode_hex("f8740184aabbccdd20f840df947567d83b7b8d80addcb281a71d54fc7b3364ffed82271086000000606060df947567d83b7b8d80addcb281a71d54fc7b3364ffed824e20860000006060608180825208a0360341090d2c4a01fa7da816c57d51c0b2fa3fcf1f99141806efc99f568c0b2a83bc614ec0"); assert_eq!(buf, expected); assert_eq!( @@ -192,13 +197,10 @@ fn test_rlp_encode_depends_on() { #[test] fn test_rlp_encode_depends_on_malformed() { // Manually crafted: here depends_on is 33 bytes long. - let malformed = decode_hex("f8750184aabbccdd20f840df947567d83b7b8d80addcb281a71d54fc7b3364ffed82271086000000606060df947567d83b7b8d80addcb281a71d54fc7b3364ffed824e20860000006060608180825208a136034141090d2c4a01fa7da816c57d51c0b2fa3fcf1f99141806efc99f568c0b2a83bc614ec0").unwrap(); + let malformed = decode_hex("f8750184aabbccdd20f840df947567d83b7b8d80addcb281a71d54fc7b3364ffed82271086000000606060df947567d83b7b8d80addcb281a71d54fc7b3364ffed824e20860000006060608180825208a136034141090d2c4a01fa7da816c57d51c0b2fa3fcf1f99141806efc99f568c0b2a83bc614ec0"); assert_eq!( Transaction::decode(&mut &malformed[..]).unwrap_err(), - alloy_rlp::Error::ListLengthMismatch { - expected: 32, - got: 33 - } + RLPError::Overflow ); } @@ -249,12 +251,12 @@ fn test_sign_undelegated() { let hash = tx.get_signing_hash(); assert_eq!( hash.to_vec(), - decode_hex("2a1c25ce0d66f45276a5f308b99bf410e2fc7d5b6ea37a49f2ab9f1da9446478").unwrap() + decode_hex("2a1c25ce0d66f45276a5f308b99bf410e2fc7d5b6ea37a49f2ab9f1da9446478") ); let signature = Transaction::sign_hash(hash, &pk); assert_eq!( signature.to_vec(), - decode_hex("f76f3c91a834165872aa9464fc55b03a13f46ea8d3b858e528fcceaf371ad6884193c3f313ff8effbb57fe4d1adc13dceb933bedbf9dbb528d2936203d5511df00").unwrap() + decode_hex("f76f3c91a834165872aa9464fc55b03a13f46ea8d3b858e528fcceaf371ad6884193c3f313ff8effbb57fe4d1adc13dceb933bedbf9dbb528d2936203d5511df00") ); let signed = tx.sign(&pk); @@ -306,17 +308,17 @@ fn test_undelegated_signed_properties() { let tx = undelegated_tx!().sign(&pk); assert_eq!( tx.signature.clone().unwrap(), - decode_hex("f76f3c91a834165872aa9464fc55b03a13f46ea8d3b858e528fcceaf371ad6884193c3f313ff8effbb57fe4d1adc13dceb933bedbf9dbb528d2936203d5511df00").unwrap(), + decode_hex("f76f3c91a834165872aa9464fc55b03a13f46ea8d3b858e528fcceaf371ad6884193c3f313ff8effbb57fe4d1adc13dceb933bedbf9dbb528d2936203d5511df00"), ); let pubkey = pk.public_key(&Secp256k1::signing_only()); assert_eq!(tx.origin().unwrap().unwrap(), pubkey); assert_eq!( tx.id().unwrap().unwrap().to_vec(), - decode_hex("da90eaea52980bc4bb8d40cb2ff84d78433b3b4a6e7d50b75736c5e3e77b71ec").unwrap() + decode_hex("da90eaea52980bc4bb8d40cb2ff84d78433b3b4a6e7d50b75736c5e3e77b71ec") ); assert_eq!( &tx.get_delegate_signing_hash(&pubkey.address())[..], - decode_hex("da90eaea52980bc4bb8d40cb2ff84d78433b3b4a6e7d50b75736c5e3e77b71ec").unwrap() + decode_hex("da90eaea52980bc4bb8d40cb2ff84d78433b3b4a6e7d50b75736c5e3e77b71ec") ); } @@ -351,7 +353,7 @@ fn test_with_signature_validated() { #[test] fn test_decode_real() { - let src = decode_hex("f8804a880106f4db1482fd5a81b4e1e09477845a52acad7fe6a346f5b09e5e89e7caec8e3b890391c64cd2bc206c008080828ca08088a63565b632b9b7c3c0b841d76de99625a1a8795e467d509818701ec5961a8a4cf7cc2d75cee95f9ad70891013aaa4088919cc46df4f1e3f87b4ea44d002033fa3f7bd69485cb807aa2985100").unwrap(); + let src = decode_hex("f8804a880106f4db1482fd5a81b4e1e09477845a52acad7fe6a346f5b09e5e89e7caec8e3b890391c64cd2bc206c008080828ca08088a63565b632b9b7c3c0b841d76de99625a1a8795e467d509818701ec5961a8a4cf7cc2d75cee95f9ad70891013aaa4088919cc46df4f1e3f87b4ea44d002033fa3f7bd69485cb807aa2985100"); let tx = Transaction::decode(&mut &src[..]).unwrap(); let buf = tx.to_broadcastable_bytes().expect("Was signed"); assert_eq!(buf, src); @@ -359,7 +361,7 @@ fn test_decode_real() { #[test] fn test_decode_real_delegated() { - let src = decode_hex("f9011f27880107b55a710b022420f87ef87c9412e3582d7ca22234f39d2a7be12c98ea9c077e2580b864b391c7d37674686f2d7573640000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000085bb373400000000000000000000000000000000000000000000000000000000657f828f8180830136798086018c7a1602b1c101b882abd35e0d57fd07462b8517109797bd2608f97a4961d0bb1fbc09d4a2f4983c2230d8a6cb4f3136e49f58eb6d32cf5edad2b0f69af6f0bf767d502a8f5510824101d87ae764add6cddff325122bf5658364fa2a04ad538621bfeb40c56c7185cf28031d9b945e7a124f171daa232499038312de60b3db4cdd6beecde6c8c0c967a100").unwrap(); + let src = decode_hex("f9011f27880107b55a710b022420f87ef87c9412e3582d7ca22234f39d2a7be12c98ea9c077e2580b864b391c7d37674686f2d7573640000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000085bb373400000000000000000000000000000000000000000000000000000000657f828f8180830136798086018c7a1602b1c101b882abd35e0d57fd07462b8517109797bd2608f97a4961d0bb1fbc09d4a2f4983c2230d8a6cb4f3136e49f58eb6d32cf5edad2b0f69af6f0bf767d502a8f5510824101d87ae764add6cddff325122bf5658364fa2a04ad538621bfeb40c56c7185cf28031d9b945e7a124f171daa232499038312de60b3db4cdd6beecde6c8c0c967a100"); let tx = Transaction::decode(&mut &src[..]).unwrap(); let mut buf = vec![]; tx.encode(&mut buf); @@ -368,36 +370,26 @@ fn test_decode_real_delegated() { #[test] fn test_decode_delegated_signature_too_short() { - let src = decode_hex("f9011e27880107b55a710b022420f87ef87c9412e3582d7ca22234f39d2a7be12c98ea9c077e2580b864b391c7d37674686f2d7573640000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000085bb373400000000000000000000000000000000000000000000000000000000657f828f8180830136798086018c7a1602b1c101b881abd35e0d57fd07462b8517109797bd2608f97a4961d0bb1fbc09d4a2f4983c2230d8a6cb4f3136e49f58eb6d32cf5edad2b0f69af6f0bf767d502a8f5510824101d87ae764add6cddff325122bf5658364fa2a04ad538621bfeb40c56c7185cf28031d9b945e7a124f171daa232499038312de60b3db4cdd6beecde6c8c0c967a1").unwrap(); - assert_eq!( - Transaction::decode(&mut &src[..]).unwrap_err(), - alloy_rlp::Error::ListLengthMismatch { - expected: 130, - got: 129 - } - ) + let src = decode_hex("f9011e27880107b55a710b022420f87ef87c9412e3582d7ca22234f39d2a7be12c98ea9c077e2580b864b391c7d37674686f2d7573640000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000085bb373400000000000000000000000000000000000000000000000000000000657f828f8180830136798086018c7a1602b1c101b881abd35e0d57fd07462b8517109797bd2608f97a4961d0bb1fbc09d4a2f4983c2230d8a6cb4f3136e49f58eb6d32cf5edad2b0f69af6f0bf767d502a8f5510824101d87ae764add6cddff325122bf5658364fa2a04ad538621bfeb40c56c7185cf28031d9b945e7a124f171daa232499038312de60b3db4cdd6beecde6c8c0c967a1"); + let tx = Transaction::decode(&mut &src[..]).expect("Should be decodable"); + assert!(!tx.has_valid_signature()) } #[test] fn test_decode_delegated_signature_too_long() { - let src = decode_hex("f9012027880107b55a710b022420f87ef87c9412e3582d7ca22234f39d2a7be12c98ea9c077e2580b864b391c7d37674686f2d7573640000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000085bb373400000000000000000000000000000000000000000000000000000000657f828f8180830136798086018c7a1602b1c101b883abd35e0d57fd07462b8517109797bd2608f97a4961d0bb1fbc09d4a2f4983c2230d8a6cb4f3136e49f58eb6d32cf5edad2b0f69af6f0bf767d502a8f5510824101d87ae764add6cddff325122bf5658364fa2a04ad538621bfeb40c56c7185cf28031d9b945e7a124f171daa232499038312de60b3db4cdd6beecde6c8c0c967a10101").unwrap(); - assert_eq!( - Transaction::decode(&mut &src[..]).unwrap_err(), - alloy_rlp::Error::ListLengthMismatch { - expected: 130, - got: 131 - } - ) + let src = decode_hex("f9012027880107b55a710b022420f87ef87c9412e3582d7ca22234f39d2a7be12c98ea9c077e2580b864b391c7d37674686f2d7573640000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000085bb373400000000000000000000000000000000000000000000000000000000657f828f8180830136798086018c7a1602b1c101b883abd35e0d57fd07462b8517109797bd2608f97a4961d0bb1fbc09d4a2f4983c2230d8a6cb4f3136e49f58eb6d32cf5edad2b0f69af6f0bf767d502a8f5510824101d87ae764add6cddff325122bf5658364fa2a04ad538621bfeb40c56c7185cf28031d9b945e7a124f171daa232499038312de60b3db4cdd6beecde6c8c0c967a10101"); + let tx = Transaction::decode(&mut &src[..]).expect("Should be decodable"); + assert!(!tx.has_valid_signature()) } #[test] fn test_rlp_decode_address_too_long() { let malformed = decode_hex( "ec0184aabbccdd20d8d795515167d83b7b8d80addcb281a71d54fc7b3364ffed808081808252088083bc614ec0" - ).unwrap(); + ); assert_eq!( Transaction::decode(&mut &malformed[..]).unwrap_err(), - alloy_rlp::Error::ListLengthMismatch { + RLPError::ListLengthMismatch { expected: 20, got: 21 } @@ -408,11 +400,10 @@ fn test_rlp_decode_address_too_long() { fn test_rlp_decode_address_startswith_zero_misencoded() { let malformed = decode_hex( "eb0184aabbccdd20d8d7940067d83b7b8d80addcb281a71d54fc7b3364ffed808081808252088083bc614ec0", - ) - .unwrap(); + ); assert_eq!( Transaction::decode(&mut &malformed[..]).unwrap_err(), - alloy_rlp::Error::LeadingZero + RLPError::LeadingZero ); }