diff --git a/Cargo.lock b/Cargo.lock index b8cdc63f..0e566e76 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9455,6 +9455,7 @@ version = "0.1.0" dependencies = [ "alloy-consensus", "alloy-primitives", + "borsh", "group", "k256", ] @@ -9475,6 +9476,7 @@ dependencies = [ "alloy-sol-macro-input", "alloy-sol-types", "alloy-transport", + "borsh", "build-solidity-contracts", "ethereum-schnorr-contract", "group", diff --git a/processor/ethereum/primitives/Cargo.toml b/processor/ethereum/primitives/Cargo.toml index 05b23189..89869cb8 100644 --- a/processor/ethereum/primitives/Cargo.toml +++ b/processor/ethereum/primitives/Cargo.toml @@ -17,6 +17,8 @@ rustdoc-args = ["--cfg", "docsrs"] workspace = true [dependencies] +borsh = { version = "1", default-features = false, features = ["std", "derive", "de_strict_order"] } + group = { version = "0.13", default-features = false } k256 = { version = "^0.13.1", default-features = false, features = ["std", "arithmetic"] } diff --git a/processor/ethereum/primitives/src/borsh.rs b/processor/ethereum/primitives/src/borsh.rs new file mode 100644 index 00000000..d7f30dbf --- /dev/null +++ b/processor/ethereum/primitives/src/borsh.rs @@ -0,0 +1,24 @@ +use ::borsh::{io, BorshSerialize, BorshDeserialize}; + +use alloy_primitives::{U256, Address}; + +/// Serialize a U256 with a borsh-compatible API. +pub fn serialize_u256(value: &U256, writer: &mut impl io::Write) -> io::Result<()> { + let value: [u8; 32] = value.to_be_bytes(); + value.serialize(writer) +} + +/// Deserialize an address with a borsh-compatible API. +pub fn deserialize_u256(reader: &mut impl io::Read) -> io::Result { + <[u8; 32]>::deserialize_reader(reader).map(|value| U256::from_be_bytes(value)) +} + +/// Serialize an address with a borsh-compatible API. +pub fn serialize_address(address: &Address, writer: &mut impl io::Write) -> io::Result<()> { + <[u8; 20]>::from(address.0).serialize(writer) +} + +/// Deserialize an address with a borsh-compatible API. +pub fn deserialize_address(reader: &mut impl io::Read) -> io::Result
{ + <[u8; 20]>::deserialize_reader(reader).map(|address| Address(address.into())) +} diff --git a/processor/ethereum/primitives/src/lib.rs b/processor/ethereum/primitives/src/lib.rs index dadc5424..44d08e5a 100644 --- a/processor/ethereum/primitives/src/lib.rs +++ b/processor/ethereum/primitives/src/lib.rs @@ -2,12 +2,27 @@ #![doc = include_str!("../README.md")] #![deny(missing_docs)] +use ::borsh::{BorshSerialize, BorshDeserialize}; + use group::ff::PrimeField; use k256::Scalar; use alloy_primitives::PrimitiveSignature; use alloy_consensus::{SignableTransaction, Signed, TxLegacy}; +mod borsh; +pub use borsh::*; + +/// An index of a log within a block. +#[derive(Clone, Copy, PartialEq, Eq, Debug, BorshSerialize, BorshDeserialize)] +#[borsh(crate = "::borsh")] +pub struct LogIndex { + /// The hash of the block which produced this log. + pub block_hash: [u8; 32], + /// The index of this log within the execution of the block. + pub index_within_block: u64, +} + /// The Keccak256 hash function. pub fn keccak256(data: impl AsRef<[u8]>) -> [u8; 32] { alloy_primitives::keccak256(data.as_ref()).into() diff --git a/processor/ethereum/router/Cargo.toml b/processor/ethereum/router/Cargo.toml index 46b1f203..4b737a00 100644 --- a/processor/ethereum/router/Cargo.toml +++ b/processor/ethereum/router/Cargo.toml @@ -17,6 +17,8 @@ rustdoc-args = ["--cfg", "docsrs"] workspace = true [dependencies] +borsh = { version = "1", default-features = false, features = ["std", "derive", "de_strict_order"] } + group = { version = "0.13", default-features = false } alloy-core = { version = "0.8", default-features = false } diff --git a/processor/ethereum/router/src/lib.rs b/processor/ethereum/router/src/lib.rs index e13e9eba..d8cac48a 100644 --- a/processor/ethereum/router/src/lib.rs +++ b/processor/ethereum/router/src/lib.rs @@ -2,7 +2,9 @@ #![doc = include_str!("../README.md")] #![deny(missing_docs)] -use std::{sync::Arc, io, collections::HashSet}; +use std::{sync::Arc, collections::HashSet}; + +use borsh::{BorshSerialize, BorshDeserialize}; use group::ff::PrimeField; @@ -16,6 +18,7 @@ use alloy_transport::{TransportErrorKind, RpcError}; use alloy_simple_request_transport::SimpleRequest; use alloy_provider::{Provider, RootProvider}; +use ethereum_primitives::LogIndex; use ethereum_schnorr::{PublicKey, Signature}; use ethereum_deployer::Deployer; use erc20::{Transfer, Erc20}; @@ -65,12 +68,18 @@ impl From<&Signature> for abi::Signature { } /// A coin on Ethereum. -#[derive(Clone, Copy, PartialEq, Eq, Debug)] +#[derive(Clone, Copy, PartialEq, Eq, Debug, BorshSerialize, BorshDeserialize)] pub enum Coin { /// Ether, the native coin of Ethereum. Ether, /// An ERC20 token. - Erc20(Address), + Erc20( + #[borsh( + serialize_with = "ethereum_primitives::serialize_address", + deserialize_with = "ethereum_primitives::deserialize_address" + )] + Address, + ), } impl Coin { @@ -80,100 +89,31 @@ impl Coin { Coin::Erc20(address) => *address, } } - - /// Read a `Coin`. - pub fn read(reader: &mut R) -> io::Result { - let mut kind = [0xff]; - reader.read_exact(&mut kind)?; - Ok(match kind[0] { - 0 => Coin::Ether, - 1 => { - let mut address = [0; 20]; - reader.read_exact(&mut address)?; - Coin::Erc20(address.into()) - } - _ => Err(io::Error::other("unrecognized Coin type"))?, - }) - } - - /// Write the `Coin`. - pub fn write(&self, writer: &mut W) -> io::Result<()> { - match self { - Coin::Ether => writer.write_all(&[0]), - Coin::Erc20(token) => { - writer.write_all(&[1])?; - writer.write_all(token.as_ref()) - } - } - } } /// An InInstruction from the Router. -#[derive(Clone, PartialEq, Eq, Debug)] +#[derive(Clone, PartialEq, Eq, Debug, BorshSerialize, BorshDeserialize)] pub struct InInstruction { /// The ID for this `InInstruction`. - pub id: ([u8; 32], u64), + pub id: LogIndex, /// The address which transferred these coins to Serai. - pub from: [u8; 20], + #[borsh( + serialize_with = "ethereum_primitives::serialize_address", + deserialize_with = "ethereum_primitives::deserialize_address" + )] + pub from: Address, /// The coin transferred. pub coin: Coin, /// The amount transferred. + #[borsh( + serialize_with = "ethereum_primitives::serialize_u256", + deserialize_with = "ethereum_primitives::deserialize_u256" + )] pub amount: U256, /// The data associated with the transfer. pub data: Vec, } -impl InInstruction { - /// Read an `InInstruction`. - pub fn read(reader: &mut R) -> io::Result { - let id = { - let mut id_hash = [0; 32]; - reader.read_exact(&mut id_hash)?; - let mut id_pos = [0; 8]; - reader.read_exact(&mut id_pos)?; - let id_pos = u64::from_le_bytes(id_pos); - (id_hash, id_pos) - }; - - let mut from = [0; 20]; - reader.read_exact(&mut from)?; - - let coin = Coin::read(reader)?; - let mut amount = [0; 32]; - reader.read_exact(&mut amount)?; - let amount = U256::from_le_slice(&amount); - - let mut data_len = [0; 4]; - reader.read_exact(&mut data_len)?; - let data_len = usize::try_from(u32::from_le_bytes(data_len)) - .map_err(|_| io::Error::other("InInstruction data exceeded 2**32 in length"))?; - let mut data = vec![0; data_len]; - reader.read_exact(&mut data)?; - - Ok(InInstruction { id, from, coin, amount, data }) - } - - /// Write the `InInstruction`. - pub fn write(&self, writer: &mut W) -> io::Result<()> { - writer.write_all(&self.id.0)?; - writer.write_all(&self.id.1.to_le_bytes())?; - - writer.write_all(&self.from)?; - - self.coin.write(writer)?; - writer.write_all(&self.amount.as_le_bytes())?; - - writer.write_all( - &u32::try_from(self.data.len()) - .map_err(|_| { - io::Error::other("InInstruction being written had data exceeding 2**32 in length") - })? - .to_le_bytes(), - )?; - writer.write_all(&self.data) - } -} - /// A list of `OutInstruction`s. #[derive(Clone)] pub struct OutInstructions(Vec); @@ -205,7 +145,7 @@ impl From<&[(SeraiAddress, U256)]> for OutInstructions { } /// An action which was executed by the Router. -#[derive(Clone, PartialEq, Eq, Debug)] +#[derive(Clone, PartialEq, Eq, Debug, BorshSerialize, BorshDeserialize)] pub enum Executed { /// New key was set. SetKey { @@ -230,44 +170,6 @@ impl Executed { Executed::SetKey { nonce, .. } | Executed::Batch { nonce, .. } => *nonce, } } - - /// Write the Executed. - pub fn write(&self, writer: &mut impl io::Write) -> io::Result<()> { - match self { - Self::SetKey { nonce, key } => { - writer.write_all(&[0])?; - writer.write_all(&nonce.to_le_bytes())?; - writer.write_all(key) - } - Self::Batch { nonce, message_hash } => { - writer.write_all(&[1])?; - writer.write_all(&nonce.to_le_bytes())?; - writer.write_all(message_hash) - } - } - } - - /// Read an Executed. - pub fn read(reader: &mut impl io::Read) -> io::Result { - let mut kind = [0xff]; - reader.read_exact(&mut kind)?; - if kind[0] >= 2 { - Err(io::Error::other("unrecognized type of Executed"))?; - } - - let mut nonce = [0; 8]; - reader.read_exact(&mut nonce)?; - let nonce = u64::from_le_bytes(nonce); - - let mut payload = [0; 32]; - reader.read_exact(&mut payload)?; - - Ok(match kind[0] { - 0 => Self::SetKey { nonce, key: payload }, - 1 => Self::Batch { nonce, message_hash: payload }, - _ => unreachable!(), - }) - } } /// A view of the Router for Serai. @@ -452,17 +354,17 @@ impl Router { ))?; } - let id = ( - log + let id = LogIndex { + block_hash: log .block_hash .ok_or_else(|| { TransportErrorKind::Custom("log didn't have its block hash set".to_string().into()) })? .into(), - log.log_index.ok_or_else(|| { + index_within_block: log.log_index.ok_or_else(|| { TransportErrorKind::Custom("log didn't have its index set".to_string().into()) })?, - ); + }; let tx_hash = log.transaction_hash.ok_or_else(|| { TransportErrorKind::Custom("log didn't have its transaction hash set".to_string().into()) @@ -551,7 +453,7 @@ impl Router { in_instructions.push(InInstruction { id, - from: *log.from.0, + from: log.from, coin, amount: log.amount, data: log.instruction.as_ref().to_vec(), diff --git a/processor/ethereum/router/src/tests/mod.rs b/processor/ethereum/router/src/tests/mod.rs index bb0da393..41363daf 100644 --- a/processor/ethereum/router/src/tests/mod.rs +++ b/processor/ethereum/router/src/tests/mod.rs @@ -17,13 +17,12 @@ use alloy_provider::RootProvider; use alloy_node_bindings::{Anvil, AnvilInstance}; +use ethereum_primitives::LogIndex; use ethereum_schnorr::{PublicKey, Signature}; use ethereum_deployer::Deployer; use crate::{Coin, OutInstructions, Router}; -mod read_write; - #[test] fn execute_reentrancy_guard() { let hash = alloy_core::primitives::keccak256(b"ReentrancyGuard Router.execute"); @@ -217,7 +216,10 @@ async fn test_eth_in_instruction() { assert_eq!(parsed_in_instructions.len(), 1); assert_eq!( parsed_in_instructions[0].id, - (<[u8; 32]>::from(receipt.block_hash.unwrap()), receipt.inner.logs()[0].log_index.unwrap()) + LogIndex { + block_hash: *receipt.block_hash.unwrap(), + index_within_block: receipt.inner.logs()[0].log_index.unwrap(), + }, ); assert_eq!(parsed_in_instructions[0].from, signer); assert_eq!(parsed_in_instructions[0].coin, Coin::Ether); diff --git a/processor/ethereum/router/src/tests/read_write.rs b/processor/ethereum/router/src/tests/read_write.rs deleted file mode 100644 index 3b6e6b73..00000000 --- a/processor/ethereum/router/src/tests/read_write.rs +++ /dev/null @@ -1,85 +0,0 @@ -use rand_core::{RngCore, OsRng}; - -use alloy_core::primitives::U256; - -use crate::{Coin, InInstruction, Executed}; - -fn coins() -> [Coin; 2] { - [Coin::Ether, { - let mut erc20 = [0; 20]; - OsRng.fill_bytes(&mut erc20); - Coin::Erc20(erc20.into()) - }] -} - -#[test] -fn test_coin_read_write() { - for coin in coins() { - let mut res = vec![]; - coin.write(&mut res).unwrap(); - assert_eq!(coin, Coin::read(&mut res.as_slice()).unwrap()); - } -} - -#[test] -fn test_in_instruction_read_write() { - for coin in coins() { - let instruction = InInstruction { - id: ( - { - let mut tx_id = [0; 32]; - OsRng.fill_bytes(&mut tx_id); - tx_id - }, - OsRng.next_u64(), - ), - from: { - let mut from = [0; 20]; - OsRng.fill_bytes(&mut from); - from - }, - coin, - amount: U256::from_le_bytes({ - let mut amount = [0; 32]; - OsRng.fill_bytes(&mut amount); - amount - }), - data: { - let len = usize::try_from(OsRng.next_u64() % 65536).unwrap(); - let mut data = vec![0; len]; - OsRng.fill_bytes(&mut data); - data - }, - }; - - let mut buf = vec![]; - instruction.write(&mut buf).unwrap(); - assert_eq!(InInstruction::read(&mut buf.as_slice()).unwrap(), instruction); - } -} - -#[test] -fn test_executed_read_write() { - for executed in [ - Executed::SetKey { - nonce: OsRng.next_u64(), - key: { - let mut key = [0; 32]; - OsRng.fill_bytes(&mut key); - key - }, - }, - Executed::Batch { - nonce: OsRng.next_u64(), - message_hash: { - let mut message_hash = [0; 32]; - OsRng.fill_bytes(&mut message_hash); - message_hash - }, - }, - ] { - let mut res = vec![]; - executed.write(&mut res).unwrap(); - assert_eq!(executed, Executed::read(&mut res.as_slice()).unwrap()); - } -} diff --git a/processor/ethereum/src/primitives/output.rs b/processor/ethereum/src/primitives/output.rs index 2215c29d..f7aaa1f8 100644 --- a/processor/ethereum/src/primitives/output.rs +++ b/processor/ethereum/src/primitives/output.rs @@ -145,7 +145,7 @@ impl ReceivedOutput<::G, Address> for Output { Output::Output { key, instruction } => { writer.write_all(&[0])?; writer.write_all(key.to_bytes().as_ref())?; - instruction.write(writer) + instruction.serialize(writer) } Output::Eventuality { key, nonce } => { writer.write_all(&[1])?; @@ -164,7 +164,7 @@ impl ReceivedOutput<::G, Address> for Output { Ok(match kind[0] { 0 => { let key = Secp256k1::read_G(reader)?; - let instruction = EthereumInInstruction::read(reader)?; + let instruction = EthereumInInstruction::deserialize_reader(reader)?; Self::Output { key, instruction } } 1 => {