diff --git a/Cargo.lock b/Cargo.lock index b9ed0c2..370d83a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -885,9 +885,11 @@ dependencies = [ name = "cuprate-types" version = "0.0.0" dependencies = [ + "bytemuck", "bytes", "cuprate-epee-encoding", "cuprate-fixed-bytes", + "cuprate-helper", "curve25519-dalek", "hex", "monero-serai", diff --git a/types/Cargo.toml b/types/Cargo.toml index 0b8b513..65fb2bd 100644 --- a/types/Cargo.toml +++ b/types/Cargo.toml @@ -13,14 +13,17 @@ default = ["blockchain", "epee", "serde", "json", "hex"] blockchain = [] epee = ["dep:cuprate-epee-encoding"] serde = ["dep:serde"] +bytemuck = ["dep:bytemuck"] proptest = ["dep:proptest", "dep:proptest-derive"] -json = ["hex"] +json = ["hex", "bytemuck", "dep:cuprate-helper"] hex = ["dep:hex", "dep:paste"] [dependencies] cuprate-epee-encoding = { path = "../net/epee-encoding", optional = true } +cuprate-helper = { path = "../helper", optional = true, features = ["cast"] } cuprate-fixed-bytes = { path = "../net/fixed-bytes" } +bytemuck = { workspace = true, features = ["derive"], optional = true } bytes = { workspace = true } curve25519-dalek = { workspace = true } monero-serai = { workspace = true } diff --git a/types/src/hex.rs b/types/src/hex.rs index 621ee03..032acfb 100644 --- a/types/src/hex.rs +++ b/types/src/hex.rs @@ -3,6 +3,16 @@ //! This module provides transparent wrapper types for //! arrays that (de)serialize from hexadecimal input/output. +#![cfg_attr( + feature = "bytemuck", + expect( + clippy::allow_attributes, + reason = "bytemuck uses allow in its derive macros" + ) +)] + +#[cfg(feature = "bytemuck")] +use bytemuck::{Pod, TransparentWrapper, Zeroable}; #[cfg(feature = "epee")] use cuprate_epee_encoding::{error, macros::bytes, EpeeValue, Marker}; #[cfg(feature = "serde")] @@ -19,6 +29,7 @@ macro_rules! generate_hex_array { $( #[doc = concat!("Wrapper type that (de)serializes from/to a ", stringify!($array_len), "-byte array.")] #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] + #[cfg_attr(feature = "bytemuck", derive(Pod, Zeroable, TransparentWrapper))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "serde", serde(transparent))] #[repr(transparent)] diff --git a/types/src/json/block.rs b/types/src/json/block.rs index a363afb..7692102 100644 --- a/types/src/json/block.rs +++ b/types/src/json/block.rs @@ -3,7 +3,14 @@ #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; -use crate::{hex::HexBytes32, json::output::Output}; +use monero_serai::{block, transaction}; + +use cuprate_helper::cast::usize_to_u64; + +use crate::{ + hex::{HexBytes1, HexBytes32}, + json::output::{Output, TaggedKey, Target}, +}; /// JSON representation of a block. /// @@ -21,19 +28,25 @@ pub struct Block { pub tx_hashes: Vec, } -// impl From for Block { -// fn from(b: monero_serai::block::Block) -> Self { -// Self { -// major_version: todo!(), -// minor_version: todo!(), -// timestamp: todo!(), -// prev_id: todo!(), -// nonce: todo!(), -// miner_tx: todo!(), -// tx_hashes: todo!(), -// } -// } -// } +impl From for Block { + fn from(b: block::Block) -> Self { + let Ok(miner_tx) = MinerTransaction::try_from(b.miner_transaction) else { + unreachable!("input is a miner tx, this should never fail"); + }; + + let tx_hashes = b.transactions.into_iter().map(HexBytes32).collect(); + + Self { + major_version: b.header.hardfork_version, + minor_version: b.header.hardfork_signal, + timestamp: b.header.timestamp, + prev_id: HexBytes32(b.header.previous), + nonce: b.header.nonce, + miner_tx, + tx_hashes, + } + } +} /// [`Block::miner_tx`]. #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] @@ -54,6 +67,98 @@ pub enum MinerTransaction { }, } +impl TryFrom for MinerTransaction { + type Error = transaction::Transaction; + + /// # Errors + /// This function errors if the input is not a miner transaction. + fn try_from(tx: transaction::Transaction) -> Result { + fn map_prefix( + prefix: transaction::TransactionPrefix, + version: u8, + ) -> Result { + let Some(input) = prefix.inputs.first() else { + return Err(prefix); + }; + + let height = match input { + transaction::Input::Gen(height) => usize_to_u64(*height), + transaction::Input::ToKey { .. } => return Err(prefix), + }; + + let vin = { + let r#gen = Gen { height }; + let input = Input { r#gen }; + [input] + }; + + let vout = prefix + .outputs + .into_iter() + .map(|o| { + let amount = o.amount.unwrap_or(0); + + let target = match o.view_tag { + Some(view_tag) => { + let tagged_key = TaggedKey { + key: HexBytes32(o.key.0), + view_tag: HexBytes1([view_tag]), + }; + + Target::TaggedKey { tagged_key } + } + None => Target::Key { + key: HexBytes32(o.key.0), + }, + }; + + Output { amount, target } + }) + .collect(); + + // TODO: use cuprate_constants target time + let unlock_time = match prefix.additional_timelock { + transaction::Timelock::None => height, + transaction::Timelock::Block(height_lock) => height + usize_to_u64(height_lock), + transaction::Timelock::Time(seconds) => height + (seconds * 120), + } + 60; + + Ok(MinerTransactionPrefix { + version, + unlock_time, + vin, + vout, + extra: prefix.extra, + }) + } + + Ok(match tx { + transaction::Transaction::V1 { prefix, signatures } => { + let prefix = match map_prefix(prefix, 1) { + Ok(p) => p, + Err(prefix) => return Err(transaction::Transaction::V1 { prefix, signatures }), + }; + + Self::V1 { + prefix, + signatures: [(); 0], + } + } + transaction::Transaction::V2 { prefix, proofs } => { + let prefix = match map_prefix(prefix, 2) { + Ok(p) => p, + Err(prefix) => return Err(transaction::Transaction::V2 { prefix, proofs }), + }; + + Self::V2 { + prefix, + rct_signatures: MinerTransactionRctSignatures { r#type: 0 }, + } + } + }) + } +} + impl Default for MinerTransaction { fn default() -> Self { Self::V1 { @@ -69,7 +174,7 @@ impl Default for MinerTransaction { pub struct MinerTransactionPrefix { pub version: u8, pub unlock_time: u64, - pub vin: Vec, + pub vin: [Input; 1], pub vout: Vec, pub extra: Vec, } diff --git a/types/src/json/tx.rs b/types/src/json/tx.rs index de2d507..b6f8520 100644 --- a/types/src/json/tx.rs +++ b/types/src/json/tx.rs @@ -8,9 +8,13 @@ #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; +use cuprate_helper::cast::usize_to_u64; + +use monero_serai::transaction; + use crate::{ - hex::{HexBytes32, HexBytes64, HexBytes8}, - json::output::Output, + hex::{HexBytes1, HexBytes32, HexBytes64, HexBytes8}, + json::output::{Output, TaggedKey, Target}, }; /// JSON representation of a non-miner transaction. @@ -48,6 +52,89 @@ pub struct TransactionPrefix { pub extra: Vec, } +impl From for Transaction { + fn from(tx: transaction::Transaction) -> Self { + fn map_prefix(prefix: transaction::TransactionPrefix, version: u8) -> TransactionPrefix { + let mut height = 0; + + let vin = prefix + .inputs + .into_iter() + .filter_map(|input| match input { + transaction::Input::ToKey { + amount, + key_offsets, + key_image, + } => { + let key = Key { + amount: amount.unwrap_or(0), + key_offsets, + k_image: HexBytes32(key_image.compress().0), + }; + + Some(Input { key }) + } + transaction::Input::Gen(h) => { + height = usize_to_u64(h); + None + } + }) + .collect(); + + let vout = prefix + .outputs + .into_iter() + .map(|o| { + let amount = o.amount.unwrap_or(0); + + let target = match o.view_tag { + Some(view_tag) => { + let tagged_key = TaggedKey { + key: HexBytes32(o.key.0), + view_tag: HexBytes1([view_tag]), + }; + + Target::TaggedKey { tagged_key } + } + None => Target::Key { + key: HexBytes32(o.key.0), + }, + }; + + Output { amount, target } + }) + .collect(); + + // TODO: use cuprate_constants target time + let unlock_time = match prefix.additional_timelock { + transaction::Timelock::None => height, + transaction::Timelock::Block(height_lock) => height + usize_to_u64(height_lock), + transaction::Timelock::Time(seconds) => height + (seconds * 120), + } + 60; + + TransactionPrefix { + version, + unlock_time, + vin, + vout, + extra: prefix.extra, + } + } + + match tx { + transaction::Transaction::V1 { prefix, signatures } => Self::V1 { + prefix: map_prefix(prefix, 1), + signatures: todo!(), + }, + transaction::Transaction::V2 { prefix, proofs } => Self::V2 { + prefix: map_prefix(prefix, 2), + rct_signatures: todo!(), + rctsig_prunable: todo!(), + }, + } + } +} + /// [`Transaction::V2::rct_signatures`]. #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]