From dc4e46137942eeac5335f0d51bb93dfe3637c064 Mon Sep 17 00:00:00 2001 From: parazyd Date: Wed, 21 May 2025 20:48:06 +0200 Subject: [PATCH] validator/xmr: Add aux chain checks --- Cargo.lock | 102 +++++++++++++++++++++++++++++++++- Cargo.toml | 4 ++ src/validator/xmr/helpers.rs | 103 ++++++++++++++++++++++++++++++++++- 3 files changed, 207 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 978465b15..7d90d9f33 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -907,6 +907,12 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "64fa3c856b712db6612c019f14756e64e4bcea13337a6b33b696333a9eaa2d06" +[[package]] +name = "byte-slice-cast" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7575182f7272186991736b70173b0ea045398f984bf5ebbb3804736ce1330c9d" + [[package]] name = "bytecheck" version = "0.6.12" @@ -1257,6 +1263,26 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[package]] +name = "const_format" +version = "0.2.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "126f97965c8ad46d6d9163268ff28432e8f6a1196a55578867832e3049df63dd" +dependencies = [ + "const_format_proc_macros", +] + +[[package]] +name = "const_format_proc_macros" +version = "0.2.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d57c2eccfb16dbac1f4e61e206105db5820c9d26c3c472bc17c774259ef7744" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + [[package]] name = "constant_time_eq" version = "0.3.1" @@ -1798,6 +1824,7 @@ dependencies = [ "pin-project-lite", "plotters", "prettytable-rs", + "primitive-types", "rand 0.8.5", "randomx", "rcgen", @@ -1805,6 +1832,8 @@ dependencies = [ "rustls-pemfile", "semver", "serde", + "sha2", + "simplelog", "sled-overlay", "smol", "socket2", @@ -3420,7 +3449,7 @@ dependencies = [ "rand 0.8.5", "sinsemilla", "subtle", - "uint", + "uint 0.9.5", ] [[package]] @@ -3772,6 +3801,26 @@ dependencies = [ "png", ] +[[package]] +name = "impl-codec" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d40b9d5e17727407e55028eafc22b2dc68781786e6d7eb8a21103f5058e3a14" +dependencies = [ + "parity-scale-codec", +] + +[[package]] +name = "impl-trait-for-tuples" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0eb5a3343abf848c0984fe4604b2b105da9539376e24fc0a3b0007411ae4fd9" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] + [[package]] name = "incrementalmerkletree" version = "0.8.2" @@ -4780,6 +4829,34 @@ dependencies = [ "sha2", ] +[[package]] +name = "parity-scale-codec" +version = "3.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799781ae679d79a948e13d4824a40970bfa500058d245760dd857301059810fa" +dependencies = [ + "arrayvec", + "bitvec", + "byte-slice-cast", + "const_format", + "impl-trait-for-tuples", + "parity-scale-codec-derive", + "rustversion", + "serde", +] + +[[package]] +name = "parity-scale-codec-derive" +version = "3.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34b4653168b563151153c9e4c08ebed57fb8262bebfa79711552fa983c623e7a" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.101", +] + [[package]] name = "parking" version = "2.2.1" @@ -5173,6 +5250,17 @@ dependencies = [ "elliptic-curve", ] +[[package]] +name = "primitive-types" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d15600a7d856470b7d278b3fe0e311fe28c2526348549f8ef2ff7db3299c87f5" +dependencies = [ + "fixed-hash", + "impl-codec", + "uint 0.10.0", +] + [[package]] name = "priority-queue" version = "2.5.0" @@ -8154,6 +8242,18 @@ dependencies = [ "static_assertions", ] +[[package]] +name = "uint" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "909988d098b2f738727b161a106cfc7cab00c539c2687a8836f8e565976fb53e" +dependencies = [ + "byteorder", + "crunchy", + "hex", + "static_assertions", +] + [[package]] name = "unarray" version = "0.1.4" diff --git a/Cargo.toml b/Cargo.toml index 88e94554d..2bd150e87 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -88,6 +88,7 @@ hex = {version = "0.4.3", optional = true} serde = {version = "1.0.219", features = ["derive"], optional = true} tinyjson = {version = "2.5.1", optional = true} httparse = {version = "1.10.1", optional = true} +primitive-types = {version = "0.13.1", optional = true} semver = {version = "1.0.26", optional = true} structopt = {version= "0.3.26", optional = true} structopt-toml = {version= "0.5.1", optional = true} @@ -118,6 +119,7 @@ blake3 = {version = "1.8.2", features = ["rayon"], optional = true} crypto_api_chachapoly = {version = "0.5.0", optional = true} halo2_proofs = {version = "0.3.1", features = ["circuit-params"], optional = true} halo2_gadgets = {version = "0.3.1", features = ["circuit-params"], optional = true} +sha2 = {version = "0.10.9", optional = true} # Smart contract runtime darkfi-sdk = {path = "src/sdk", optional = true} @@ -172,8 +174,10 @@ validator = [ "hex", "lazy_static", "monero", + "primitive-types", "randomx", "smol", + "sha2", "blockchain", "system", diff --git a/src/validator/xmr/helpers.rs b/src/validator/xmr/helpers.rs index 3892a34b2..8727b277c 100644 --- a/src/validator/xmr/helpers.rs +++ b/src/validator/xmr/helpers.rs @@ -19,9 +19,18 @@ use std::{io, iter}; -use monero::{consensus::Encodable as XmrEncodable, cryptonote::hash::Hashable, VarInt}; +use log::warn; +use monero::{ + blockdata::transaction::{ExtraField, RawExtraField, SubField}, + consensus::Encodable as XmrEncodable, + cryptonote::hash::Hashable, + VarInt, +}; +use primitive_types::U256; +use sha2::{Digest, Sha256}; use tiny_keccak::{Hasher, Keccak}; +use super::merkle_tree_parameters::MerkleTreeParameters; use crate::{ blockchain::{ header_store::HeaderHash, @@ -31,6 +40,7 @@ use crate::{ MoneroPowData, }, }, + Error, Error::MoneroMergeMineError, Result, }; @@ -136,3 +146,94 @@ pub fn construct_monero_data( aux_chain_merkle_proof, }) } + +fn check_aux_chains( + monero_data: &MoneroPowData, + merge_mining_params: VarInt, + aux_chain_merkle_root: &monero::Hash, + darkfi_hash: HeaderHash, + darkfi_genesis_hash: HeaderHash, +) -> bool { + let df_hash = monero::Hash::from_slice(darkfi_hash.as_slice()); + + if merge_mining_params == VarInt(0) { + // Interpret 0 as only 1 chain + if df_hash == *aux_chain_merkle_root { + return true + } + } + + let merkle_tree_params = MerkleTreeParameters::from_varint(merge_mining_params); + if merkle_tree_params.number_of_chains() == 0 { + return false + } + + let hash_position = U256::from_little_endian( + &Sha256::new() + .chain_update(darkfi_genesis_hash.as_slice()) + .chain_update(merkle_tree_params.aux_nonce().to_le_bytes()) + .chain_update((109_u8).to_le_bytes()) + .finalize(), + ) + .low_u32() % + u32::from(merkle_tree_params.number_of_chains()); + + let (merkle_root, pos) = monero_data + .aux_chain_merkle_proof + .calculate_root_with_pos(&df_hash, merkle_tree_params.number_of_chains()); + + if hash_position != pos { + return false + } + + merkle_root == *aux_chain_merkle_root +} + +// Parsing an extra field from bytes will always return an extra field with sub-fields +// that could be read, even if it does not represent the original extra field. As per +// Monero consensus rules, an error here will not represent a failure to deserialize a +// block, so no need to error here. +fn parse_extra_field_truncate_on_error(raw_extra_field: &RawExtraField) -> ExtraField { + match ExtraField::try_parse(raw_extra_field) { + Ok(val) => val, + Err(val) => { + warn!( + target: "validator::xmr::helpers", + "[MERGEMINING] Some sub-fields could not be parsed from the Monero coinbase", + ); + val + } + } +} + +/// Extracts the Monero block hash from the coinbase transaction's extra field +pub fn extract_aux_merkle_root_from_block(monero: &monero::Block) -> Result> { + // When we extract the merge mining hash, we do not care if + // the extra field can be parsed without error. + let extra_field = parse_extra_field_truncate_on_error(&monero.miner_tx.prefix.extra); + + // Only one merge mining tag is allowed + let merge_mining_hashes: Vec = extra_field + .0 + .iter() + .filter_map(|item| { + if let SubField::MergeMining(_depth, merge_mining_hash) = item { + Some(*merge_mining_hash) + } else { + None + } + }) + .collect(); + + if merge_mining_hashes.len() > 1 { + return Err(Error::MoneroMergeMineError( + "More than one merge mining tag found in coinbase".to_string(), + )) + } + + if let Some(merge_mining_hash) = merge_mining_hashes.into_iter().next() { + Ok(Some(merge_mining_hash)) + } else { + Ok(None) + } +}