|
@@ -0,0 +1,605 @@
|
|
|
|
|
+/* This file is part of DarkFi (https://dark.fi)
|
|
|
|
|
+ *
|
|
|
|
|
+ * Copyright (C) 2020-2026 Dyne.org foundation
|
|
|
|
|
+ *
|
|
|
|
|
+ * This program is free software: you can redistribute it and/or modify
|
|
|
|
|
+ * it under the terms of the GNU Affero General Public License as
|
|
|
|
|
+ * published by the Free Software Foundation, either version 3 of the
|
|
|
|
|
+ * License, or (at your option) any later version.
|
|
|
|
|
+ *
|
|
|
|
|
+ * This program is distributed in the hope that it will be useful,
|
|
|
|
|
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
|
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
|
+ * GNU Affero General Public License for more details.
|
|
|
|
|
+ *
|
|
|
|
|
+ * You should have received a copy of the GNU Affero General Public License
|
|
|
|
|
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
|
+ */
|
|
|
|
|
+
|
|
|
|
|
+use std::ops::Range;
|
|
|
|
|
+
|
|
|
|
|
+use monero::blockdata::transaction::{KeyImage, TxIn, TxOut, TxOutTarget};
|
|
|
|
|
+
|
|
|
|
|
+use super::*;
|
|
|
|
|
+use crate::blockchain::{header_store::PowData, Header, HeaderHash};
|
|
|
|
|
+
|
|
|
|
|
+// Blob from Monero testnet, height 2912484, mergemined DarkFi.
|
|
|
|
|
+const XMR_BLOCK: &str = "1010f881efca0644a1185eeccb2629b316ec0d41659111299ad1b736a3b0d8eac8bbc6384dc5c84bb6010002a0e2b10101ffe4e1b1010180e0a596bb1103f1d23951bd28ce2bfad791f2350e2ac348e4620e19af3418653a1839cc5c8f2be14a010b204d874ed5087b649c711dd4479434a85dbf7e9bdfae26f5bc785964d4b45c0204751b43e10321082d5f403be836d45d026fbaa2a8e4b4a9d0d821f29d709321f8d764f32d446fa80000";
|
|
|
|
|
+const SEED_HASH: &str = "f1d23951bd28ce2bfad791f2350e2ac348e4620e19af3418653a1839cc5c8f2b";
|
|
|
|
|
+
|
|
|
|
|
+// Hand-written wire vector, including nonempty branches and a nonzero aux path.
|
|
|
|
|
+// Its 128-byte E uses CompactSize 80 on the wire, but Monero 8001 when hashed.
|
|
|
|
|
+const GOLDEN_WIRE: &str = concat!(
|
|
|
|
|
+ "44464d4d00000001",
|
|
|
|
|
+ "27010203",
|
|
|
|
|
+ "1111111111111111111111111111111111111111111111111111111111111111",
|
|
|
|
|
+ "04050607",
|
|
|
|
|
+ "02aabb",
|
|
|
|
|
+ "0201",
|
|
|
|
|
+ "202222222222222222222222222222222222222222222222222222222222222222",
|
|
|
|
|
+ "01333333333333333333333333333333333333333333333333333333333333333300000000",
|
|
|
|
|
+ "28020001ff00010002",
|
|
|
|
|
+ "4444444444444444444444444444444444444444444444444444444444444444",
|
|
|
|
|
+ "80",
|
|
|
|
|
+ "5555555555555555555555555555555555555555555555555555555555555555",
|
|
|
|
|
+ "5555555555555555555555555555555555555555555555555555555555555555",
|
|
|
|
|
+ "5555555555555555555555555555555555555555555555555555555555555555",
|
|
|
|
|
+ "5555555555555555555555555555555555555555555555555555555555555555",
|
|
|
|
|
+ "01666666666666666666666666666666666666666666666666666666666666666601000000",
|
|
|
|
|
+);
|
|
|
|
|
+
|
|
|
|
|
+// Framed field ranges in GOLDEN_WIRE. Every vector in this fixture has a
|
|
|
|
|
+// one-byte CompactSize length; these offsets are independent of the decoder.
|
|
|
|
|
+const HEADER: Range<usize> = 8..48;
|
|
|
|
|
+const KEY: Range<usize> = 48..51;
|
|
|
|
|
+const COUNT: Range<usize> = 51..53;
|
|
|
|
|
+const ROOT: Range<usize> = 53..86;
|
|
|
|
|
+const COINBASE_PROOF: Range<usize> = 86..123;
|
|
|
|
|
+const PREFIX: Range<usize> = 123..164;
|
|
|
|
|
+const EXTRA: Range<usize> = 164..293;
|
|
|
|
|
+const AUX_PROOF: Range<usize> = 293..330;
|
|
|
|
|
+
|
|
|
|
|
+fn replace_component(wire: &[u8], field: Range<usize>, body: &[u8]) -> Vec<u8> {
|
|
|
|
|
+ let mut replaced = wire.to_vec();
|
|
|
|
|
+ replaced.splice(field, darkfi_serial::serialize(&body));
|
|
|
|
|
+ replaced
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+fn empty_proof() -> MerkleProof {
|
|
|
|
|
+ MerkleProof::try_construct(vec![], 0).unwrap()
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+fn from_block(block: monero::Block) -> Result<MoneroPowData> {
|
|
|
|
|
+ MoneroPowData::new(
|
|
|
|
|
+ block,
|
|
|
|
|
+ FixedByteArray::from_bytes(&hex::decode(SEED_HASH).unwrap()).unwrap(),
|
|
|
|
|
+ empty_proof(),
|
|
|
|
|
+ )
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+fn outputs(tagged: bool, count: usize) -> Vec<TxOut> {
|
|
|
|
|
+ let key = [0x42; 32];
|
|
|
|
|
+ let target = if tagged {
|
|
|
|
|
+ TxOutTarget::ToTaggedKey { key, view_tag: 0xff }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ TxOutTarget::ToKey { key }
|
|
|
|
|
+ };
|
|
|
|
|
+ vec![TxOut { amount: monero::VarInt(u64::MAX), target }; count]
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+fn golden_powdata() -> MoneroPowData {
|
|
|
|
|
+ let mut prefix = vec![2, 0, 1, 0xff, 0, 1, 0, 2];
|
|
|
|
|
+ prefix.extend([0x44; 32]);
|
|
|
|
|
+ MoneroPowData {
|
|
|
|
|
+ header: BlockHeader {
|
|
|
|
|
+ major_version: monero::VarInt(1),
|
|
|
|
|
+ minor_version: monero::VarInt(2),
|
|
|
|
|
+ timestamp: monero::VarInt(3),
|
|
|
|
|
+ prev_id: monero::Hash::from([0x11; 32]),
|
|
|
|
|
+ nonce: 0x07060504,
|
|
|
|
|
+ },
|
|
|
|
|
+ randomx_key: FixedByteArray::from_bytes(&[0xaa, 0xbb]).unwrap(),
|
|
|
|
|
+ transaction_count: 258,
|
|
|
|
|
+ merkle_root: monero::Hash::from([0x22; 32]),
|
|
|
|
|
+ coinbase_merkle_proof: MerkleProof::try_construct(vec![monero::Hash::from([0x33; 32])], 0)
|
|
|
|
|
+ .unwrap(),
|
|
|
|
|
+ coinbase_tx_prefix: CoinbasePrefix::new(prefix).unwrap(),
|
|
|
|
|
+ coinbase_tx_extra: RawExtraField(vec![0x55; 128]),
|
|
|
|
|
+ aux_chain_merkle_proof: MerkleProof::try_construct(vec![monero::Hash::from([0x66; 32])], 1)
|
|
|
|
|
+ .unwrap(),
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+fn assert_fields(actual: &MoneroPowData, expected: &MoneroPowData) {
|
|
|
|
|
+ assert_eq!(actual.header, expected.header);
|
|
|
|
|
+ assert_eq!(actual.randomx_key(), expected.randomx_key());
|
|
|
|
|
+ assert_eq!(actual.transaction_count(), expected.transaction_count());
|
|
|
|
|
+ assert_eq!(actual.merkle_root, expected.merkle_root);
|
|
|
|
|
+ assert_eq!(actual.coinbase_merkle_proof.branch(), expected.coinbase_merkle_proof.branch());
|
|
|
|
|
+ assert_eq!(actual.coinbase_merkle_proof.path(), expected.coinbase_merkle_proof.path());
|
|
|
|
|
+ assert_eq!(actual.coinbase_tx_prefix(), expected.coinbase_tx_prefix());
|
|
|
|
|
+ assert_eq!(actual.coinbase_tx_extra(), expected.coinbase_tx_extra());
|
|
|
|
|
+ assert_eq!(actual.aux_chain_merkle_proof.branch(), expected.aux_chain_merkle_proof.branch());
|
|
|
|
|
+ assert_eq!(actual.aux_chain_merkle_proof.path(), expected.aux_chain_merkle_proof.path());
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+fn assert_roundtrip(powdata: &MoneroPowData) -> MoneroPowData {
|
|
|
|
|
+ let wire = darkfi_serial::serialize(powdata);
|
|
|
|
|
+ // Cross-decode each encoder's bytes once; independent hash correctness
|
|
|
|
|
+ // belongs in the full-block fixture test rather than every roundtrip.
|
|
|
|
|
+ #[cfg(feature = "async-serial")]
|
|
|
|
|
+ let decode_wire = smol::future::block_on(async {
|
|
|
|
|
+ let async_wire = darkfi_serial::serialize_async(powdata).await;
|
|
|
|
|
+ assert_eq!(async_wire, wire);
|
|
|
|
|
+ let async_decoded: MoneroPowData = darkfi_serial::deserialize_async(&wire).await.unwrap();
|
|
|
|
|
+ assert_fields(&async_decoded, powdata);
|
|
|
|
|
+ async_wire
|
|
|
|
|
+ });
|
|
|
|
|
+ #[cfg(not(feature = "async-serial"))]
|
|
|
|
|
+ let decode_wire = &wire;
|
|
|
|
|
+ let decoded: MoneroPowData = darkfi_serial::deserialize(&decode_wire).unwrap();
|
|
|
|
|
+ assert_fields(&decoded, powdata);
|
|
|
|
|
+ assert_eq!(darkfi_serial::serialize(&decoded), wire);
|
|
|
|
|
+ decoded
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+fn assert_rejected(wire: &[u8]) {
|
|
|
|
|
+ assert!(darkfi_serial::deserialize::<MoneroPowData>(wire).is_err());
|
|
|
|
|
+ #[cfg(feature = "async-serial")]
|
|
|
|
|
+ smol::future::block_on(async {
|
|
|
|
|
+ assert!(darkfi_serial::deserialize_async::<MoneroPowData>(wire).await.is_err());
|
|
|
|
|
+ });
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+#[test]
|
|
|
|
|
+fn fixture_hashes_roots_blob_and_serde_match_monero() {
|
|
|
|
|
+ let block = monero_block_deserialize(XMR_BLOCK).unwrap();
|
|
|
|
|
+ assert_eq!(monero::consensus::serialize(&block), hex::decode(XMR_BLOCK).unwrap());
|
|
|
|
|
+ let powdata = from_block(block.clone()).unwrap();
|
|
|
|
|
+ assert_eq!(powdata.coinbase_prefix_hash().unwrap(), block.miner_tx.prefix.hash());
|
|
|
|
|
+ assert_eq!(powdata.coinbase_hash().unwrap(), block.miner_tx.hash());
|
|
|
|
|
+ assert_eq!(powdata.merkle_root, block.tx_root());
|
|
|
|
|
+ assert_eq!(powdata.to_block_hashing_blob(), block.serialize_hashable());
|
|
|
|
|
+ assert_eq!(powdata.transaction_count(), 1);
|
|
|
|
|
+ assert!(powdata.is_coinbase_valid_merkle_root());
|
|
|
|
|
+
|
|
|
|
|
+ let mut prefix = powdata.coinbase_tx_prefix().to_vec();
|
|
|
|
|
+ prefix.extend(monero::consensus::serialize(powdata.coinbase_tx_extra()));
|
|
|
|
|
+ assert_eq!(prefix, monero::consensus::serialize(&block.miner_tx.prefix));
|
|
|
|
|
+ assert_eq!(monero::Hash::new(&prefix), powdata.coinbase_prefix_hash().unwrap());
|
|
|
|
|
+ prefix.push(0); // Present null RingCT base is outside the prefix hash.
|
|
|
|
|
+ assert_eq!(prefix, monero::consensus::serialize(&block.miner_tx));
|
|
|
|
|
+ let mut hash_input = block.miner_tx.prefix.hash().to_bytes().to_vec();
|
|
|
|
|
+ hash_input.extend(monero::Hash::new([0]).to_bytes());
|
|
|
|
|
+ hash_input.extend([0; 32]);
|
|
|
|
|
+ assert_eq!(monero::Hash::new(&hash_input), block.miner_tx.hash());
|
|
|
|
|
+ for wrong_base in [monero::Hash::null(), monero::Hash::new([])] {
|
|
|
|
|
+ hash_input[32..64].copy_from_slice(wrong_base.as_bytes());
|
|
|
|
|
+ assert_ne!(monero::Hash::new(&hash_input), powdata.coinbase_hash().unwrap());
|
|
|
|
|
+ }
|
|
|
|
|
+ let decoded = assert_roundtrip(&powdata);
|
|
|
|
|
+ assert_eq!(decoded.coinbase_prefix_hash().unwrap(), block.miner_tx.prefix.hash());
|
|
|
|
|
+ assert_eq!(decoded.coinbase_hash().unwrap(), block.miner_tx.hash());
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+#[test]
|
|
|
|
|
+fn fixed_golden_wire_and_distinct_extra_length_encodings() {
|
|
|
|
|
+ let powdata = golden_powdata();
|
|
|
|
|
+ let wire = hex::decode(GOLDEN_WIRE).unwrap();
|
|
|
|
|
+ assert_eq!(darkfi_serial::serialize(&powdata), wire);
|
|
|
|
|
+ assert_roundtrip(&powdata);
|
|
|
|
|
+
|
|
|
|
|
+ let mut hash_input = powdata.coinbase_tx_prefix().to_vec();
|
|
|
|
|
+ hash_input.extend([0x80, 0x01]);
|
|
|
|
|
+ hash_input.extend([0x55; 128]);
|
|
|
|
|
+ assert_eq!(powdata.coinbase_prefix_hash().unwrap(), monero::Hash::new(&hash_input));
|
|
|
|
|
+ let wrong_input = [powdata.coinbase_tx_prefix(), &wire[EXTRA]].concat();
|
|
|
|
|
+ assert_ne!(powdata.coinbase_prefix_hash().unwrap(), monero::Hash::new(wrong_input));
|
|
|
|
|
+ let mut blob = monero::consensus::serialize(&powdata.header);
|
|
|
|
|
+ blob.extend([0x22; 32]);
|
|
|
|
|
+ blob.extend([0x82, 0x02]); // V(258), not transport u16 0201.
|
|
|
|
|
+ assert_eq!(powdata.to_block_hashing_blob(), blob);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+#[test]
|
|
|
|
|
+fn marker_legacy_shape_every_truncation_and_trailing_bytes() {
|
|
|
|
|
+ let wire = hex::decode(GOLDEN_WIRE).unwrap();
|
|
|
|
|
+ for end in 0..wire.len() {
|
|
|
|
|
+ assert_rejected(&wire[..end]);
|
|
|
|
|
+ }
|
|
|
|
|
+ for index in 0..POW_DATA_FORMAT.len() {
|
|
|
|
|
+ let mut wrong_marker = wire.clone();
|
|
|
|
|
+ wrong_marker[index] ^= 1;
|
|
|
|
|
+ assert_rejected(&wrong_marker);
|
|
|
|
|
+ }
|
|
|
|
|
+ assert_rejected(&[wire.as_slice(), &[0]].concat());
|
|
|
|
|
+ assert_rejected(&wire[POW_DATA_FORMAT.len()..]);
|
|
|
|
|
+ // Old layout: no marker, 217-byte sponge field (200 buffer, two u64s,
|
|
|
|
|
+ // delimiter) in place of P. Never restore or execute this historical state.
|
|
|
|
|
+ let mut legacy = wire[POW_DATA_FORMAT.len()..PREFIX.start].to_vec();
|
|
|
|
|
+ legacy.push(217);
|
|
|
|
|
+ legacy.extend([0; 200]);
|
|
|
|
|
+ legacy.extend(0u64.to_le_bytes());
|
|
|
|
|
+ legacy.extend(136u64.to_le_bytes());
|
|
|
|
|
+ legacy.push(1);
|
|
|
|
|
+ legacy.extend_from_slice(&wire[EXTRA.start..]);
|
|
|
|
|
+ assert_rejected(&legacy);
|
|
|
|
|
+ // Merely adding the new marker cannot turn a sponge into a valid prefix.
|
|
|
|
|
+ assert_rejected(&[POW_DATA_FORMAT.as_slice(), legacy.as_slice()].concat());
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// A bounds failure must occur before even attempting to read its absent body.
|
|
|
|
|
+struct NoBodyReader<'a>(&'a [u8]);
|
|
|
|
|
+
|
|
|
|
|
+impl Read for NoBodyReader<'_> {
|
|
|
|
|
+ fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
|
|
|
|
+ assert!(!self.0.is_empty(), "decoder attempted to read an oversized component body");
|
|
|
|
|
+ Read::read(&mut self.0, buf)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+#[cfg(feature = "async-serial")]
|
|
|
|
|
+impl AsyncRead for NoBodyReader<'_> {
|
|
|
|
|
+ fn poll_read(
|
|
|
|
|
+ mut self: std::pin::Pin<&mut Self>,
|
|
|
|
|
+ _: &mut std::task::Context<'_>,
|
|
|
|
|
+ buf: &mut [u8],
|
|
|
|
|
+ ) -> std::task::Poll<io::Result<usize>> {
|
|
|
|
|
+ std::task::Poll::Ready(Read::read(&mut *self, buf))
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+fn assert_early_rejection(wire: &[u8]) {
|
|
|
|
|
+ let mut reader = NoBodyReader(wire);
|
|
|
|
|
+ assert!(MoneroPowData::decode(&mut reader).is_err());
|
|
|
|
|
+ assert!(reader.0.is_empty());
|
|
|
|
|
+ #[cfg(feature = "async-serial")]
|
|
|
|
|
+ smol::future::block_on(async {
|
|
|
|
|
+ let mut reader = NoBodyReader(wire);
|
|
|
|
|
+ assert!(MoneroPowData::decode_async(&mut reader).await.is_err());
|
|
|
|
|
+ assert!(reader.0.is_empty());
|
|
|
|
|
+ });
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+#[test]
|
|
|
|
|
+fn vector_bounds_are_checked_before_body_reads_in_both_codecs() {
|
|
|
|
|
+ let wire = hex::decode(GOLDEN_WIRE).unwrap();
|
|
|
|
|
+ for (field, max) in [
|
|
|
|
|
+ (HEADER, MAX_MONERO_HEADER_SIZE),
|
|
|
|
|
+ (ROOT, 32),
|
|
|
|
|
+ (PREFIX, MAX_COINBASE_PREFIX_SIZE),
|
|
|
|
|
+ (EXTRA, MAX_COINBASE_EXTRA_SIZE),
|
|
|
|
|
+ ] {
|
|
|
|
|
+ for length in [max as u64 + 1, u32::MAX as u64, u64::MAX] {
|
|
|
|
|
+ let framing = darkfi_serial::serialize(&darkfi_serial::VarInt(length));
|
|
|
|
|
+ assert_early_rejection(&[&wire[..field.start], framing.as_slice()].concat());
|
|
|
|
|
+ }
|
|
|
|
|
+ for framing in [
|
|
|
|
|
+ vec![0xfd, wire[field.start], 0],
|
|
|
|
|
+ vec![0xfe, wire[field.start], 0, 0, 0],
|
|
|
|
|
+ vec![0xff, wire[field.start], 0, 0, 0, 0, 0, 0, 0],
|
|
|
|
|
+ ] {
|
|
|
|
|
+ assert_early_rejection(&[&wire[..field.start], framing.as_slice()].concat());
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ for (field, bytes) in [
|
|
|
|
|
+ (KEY, vec![61]),
|
|
|
|
|
+ (COUNT, vec![0, 0]),
|
|
|
|
|
+ (ROOT, vec![0]),
|
|
|
|
|
+ (ROOT, vec![31]),
|
|
|
|
|
+ (COINBASE_PROOF, vec![32]),
|
|
|
|
|
+ (AUX_PROOF, vec![255]),
|
|
|
|
|
+ ] {
|
|
|
|
|
+ assert_early_rejection(&[&wire[..field.start], bytes.as_slice()].concat());
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+#[test]
|
|
|
|
|
+fn framed_components_require_complete_header_and_prefix() {
|
|
|
|
|
+ let wire = hex::decode(GOLDEN_WIRE).unwrap();
|
|
|
|
|
+ for field in [HEADER, PREFIX] {
|
|
|
|
|
+ let body = &wire[field.start + 1..field.end];
|
|
|
|
|
+ // Complete outer framing containing an incomplete component.
|
|
|
|
|
+ for length in 0..body.len() {
|
|
|
|
|
+ assert_rejected(&replace_component(&wire, field.clone(), &body[..length]));
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+#[test]
|
|
|
|
|
+fn malformed_header_and_prefix_inside_valid_framing() {
|
|
|
|
|
+ let wire = hex::decode(GOLDEN_WIRE).unwrap();
|
|
|
|
|
+ // Prefix-field/scalar combinations are tested at the parser layer. Here
|
|
|
|
|
+ // check that both transport decoders actually enforce canonical parsing.
|
|
|
|
|
+ for (field, scalars) in [(HEADER, vec![0, 1, 2]), (PREFIX, vec![1])] {
|
|
|
|
|
+ let body = &wire[field.start + 1..field.end];
|
|
|
|
|
+ for offset in scalars {
|
|
|
|
|
+ for replacement in [vec![body[offset] | 0x80, 0], vec![0xff; 10], vec![0x80; 11]] {
|
|
|
|
|
+ let mut malformed_body = body.to_vec();
|
|
|
|
|
+ malformed_body.splice(offset..offset + 1, replacement);
|
|
|
|
|
+ assert_rejected(&replace_component(&wire, field.clone(), &malformed_body));
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ for tail in [vec![0], vec![1, 0x42], vec![0x80, 1]] {
|
|
|
|
|
+ let malformed_body = [body, tail.as_slice()].concat();
|
|
|
|
|
+ assert_rejected(&replace_component(&wire, field.clone(), &malformed_body));
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ // Move one byte across the P/E boundary, keeping their concatenation fixed.
|
|
|
|
|
+ let prefix = &wire[PREFIX.start + 1..PREFIX.end];
|
|
|
|
|
+ let extra = &wire[EXTRA.start + 1..EXTRA.end];
|
|
|
|
|
+ let joined = [prefix, extra].concat();
|
|
|
|
|
+ for split in [prefix.len() - 1, prefix.len() + 1] {
|
|
|
|
|
+ let mut malformed = wire[..PREFIX.start].to_vec();
|
|
|
|
|
+ malformed.extend(darkfi_serial::serialize(&&joined[..split]));
|
|
|
|
|
+ malformed.extend(darkfi_serial::serialize(&&joined[split..]));
|
|
|
|
|
+ malformed.extend_from_slice(&wire[EXTRA.end..]);
|
|
|
|
|
+ assert_rejected(&malformed);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+#[test]
|
|
|
|
|
+fn constructor_supported_targets_output_extra_and_count_boundaries() {
|
|
|
|
|
+ let fixture = monero_block_deserialize(XMR_BLOCK).unwrap();
|
|
|
|
|
+ // The tagged maximum is exercised by the combined maximum-component test.
|
|
|
|
|
+ for (tagged, count) in [(false, 1), (true, 1), (false, MAX_COINBASE_OUTPUTS)] {
|
|
|
|
|
+ let mut block = fixture.clone();
|
|
|
|
|
+ block.miner_tx.prefix.outputs = outputs(tagged, count);
|
|
|
|
|
+ let powdata = from_block(block.clone()).unwrap();
|
|
|
|
|
+ assert_eq!(powdata.coinbase_hash().unwrap(), block.miner_tx.hash());
|
|
|
|
|
+ assert!(powdata.is_coinbase_valid_merkle_root());
|
|
|
|
|
+ }
|
|
|
|
|
+ for count in [0, MAX_COINBASE_OUTPUTS + 1] {
|
|
|
|
|
+ let mut block = fixture.clone();
|
|
|
|
|
+ block.miner_tx.prefix.outputs = vec![fixture.miner_tx.prefix.outputs[0].clone(); count];
|
|
|
|
|
+ assert!(from_block(block).is_err());
|
|
|
|
|
+ }
|
|
|
|
|
+ for length in [0, 127, 128, 252, 253, 65535, MAX_COINBASE_EXTRA_SIZE] {
|
|
|
|
|
+ let mut block = fixture.clone();
|
|
|
|
|
+ block.miner_tx.prefix.extra = RawExtraField(vec![0x42; length]);
|
|
|
|
|
+ let powdata = from_block(block.clone()).unwrap();
|
|
|
|
|
+ assert_eq!(powdata.coinbase_prefix_hash().unwrap(), block.miner_tx.prefix.hash());
|
|
|
|
|
+ assert_roundtrip(&powdata);
|
|
|
|
|
+ }
|
|
|
|
|
+ let mut block = fixture.clone();
|
|
|
|
|
+ block.miner_tx.prefix.extra = RawExtraField(vec![0; MAX_COINBASE_EXTRA_SIZE + 1]);
|
|
|
|
|
+ assert!(from_block(block).is_err());
|
|
|
|
|
+ for count in [1, 128, u16::MAX as usize] {
|
|
|
|
|
+ let mut block = fixture.clone();
|
|
|
|
|
+ // Distinct hashes make the independent root/blob comparisons detect
|
|
|
|
|
+ // reordering, which a vector of identical hashes would conceal.
|
|
|
|
|
+ block.tx_hashes =
|
|
|
|
|
+ (1..count).map(|index| monero::Hash::new((index as u64).to_le_bytes())).collect();
|
|
|
|
|
+ let powdata = from_block(block.clone()).unwrap();
|
|
|
|
|
+ assert_eq!(powdata.transaction_count() as usize, count);
|
|
|
|
|
+ assert_eq!(powdata.merkle_root, block.tx_root());
|
|
|
|
|
+ assert_eq!(powdata.to_block_hashing_blob(), block.serialize_hashable());
|
|
|
|
|
+ assert!(powdata.is_coinbase_valid_merkle_root());
|
|
|
|
|
+ assert_roundtrip(&powdata);
|
|
|
|
|
+ }
|
|
|
|
|
+ let mut block = fixture;
|
|
|
|
|
+ block.tx_hashes = vec![monero::Hash::null(); u16::MAX as usize];
|
|
|
|
|
+ assert!(from_block(block).is_err());
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+#[test]
|
|
|
|
|
+fn constructor_rejects_unsupported_transactions_before_hashing() {
|
|
|
|
|
+ let fixture = monero_block_deserialize(XMR_BLOCK).unwrap();
|
|
|
|
|
+ for version in [0, 1, 3, u64::MAX] {
|
|
|
|
|
+ let mut block = fixture.clone();
|
|
|
|
|
+ block.miner_tx.prefix.version = monero::VarInt(version);
|
|
|
|
|
+ assert!(from_block(block).is_err());
|
|
|
|
|
+ }
|
|
|
|
|
+ for count in [0, 2] {
|
|
|
|
|
+ let mut block = fixture.clone();
|
|
|
|
|
+ block.miner_tx.prefix.inputs = vec![fixture.miner_tx.prefix.inputs[0].clone(); count];
|
|
|
|
|
+ assert!(from_block(block).is_err());
|
|
|
|
|
+ }
|
|
|
|
|
+ let mut block = fixture.clone();
|
|
|
|
|
+ block.miner_tx.prefix.inputs = vec![TxIn::ToKey {
|
|
|
|
|
+ amount: monero::VarInt(0),
|
|
|
|
|
+ key_offsets: vec![],
|
|
|
|
|
+ k_image: KeyImage { image: monero::Hash::null() },
|
|
|
|
|
+ }];
|
|
|
|
|
+ assert!(from_block(block).is_err());
|
|
|
|
|
+ let mut block = fixture.clone();
|
|
|
|
|
+ block.miner_tx.signatures.push(vec![]);
|
|
|
|
|
+ assert!(from_block(block).is_err());
|
|
|
|
|
+ let mut block = fixture.clone();
|
|
|
|
|
+ block.miner_tx.rct_signatures.sig = None;
|
|
|
|
|
+ assert_ne!(block.miner_tx.hash(), fixture.miner_tx.hash());
|
|
|
|
|
+ assert!(from_block(block).is_err());
|
|
|
|
|
+ for rct_type in [
|
|
|
|
|
+ RctType::Full,
|
|
|
|
|
+ RctType::Simple,
|
|
|
|
|
+ RctType::Bulletproof,
|
|
|
|
|
+ RctType::Bulletproof2,
|
|
|
|
|
+ RctType::Clsag,
|
|
|
|
|
+ RctType::BulletproofPlus,
|
|
|
|
|
+ ] {
|
|
|
|
|
+ let mut block = fixture.clone();
|
|
|
|
|
+ block.miner_tx.rct_signatures.sig.as_mut().unwrap().rct_type = rct_type;
|
|
|
|
|
+ assert!(from_block(block).is_err());
|
|
|
|
|
+ }
|
|
|
|
|
+ let mut block = fixture;
|
|
|
|
|
+ block.miner_tx.rct_signatures.p = Some(monero::util::ringct::RctSigPrunable {
|
|
|
|
|
+ range_sigs: vec![],
|
|
|
|
|
+ bulletproofs: vec![],
|
|
|
|
|
+ bulletproofplus: vec![],
|
|
|
|
|
+ MGs: vec![],
|
|
|
|
|
+ Clsags: vec![],
|
|
|
|
|
+ pseudo_outs: vec![],
|
|
|
|
|
+ });
|
|
|
|
|
+ assert!(from_block(block).is_err());
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+#[test]
|
|
|
|
|
+fn largest_supported_components_roundtrip_without_extra_inspection() {
|
|
|
|
|
+ let mut block = monero_block_deserialize(XMR_BLOCK).unwrap();
|
|
|
|
|
+ block.header.major_version = monero::VarInt(u64::MAX);
|
|
|
|
|
+ block.header.minor_version = monero::VarInt(u64::MAX);
|
|
|
|
|
+ block.header.timestamp = monero::VarInt(u64::MAX);
|
|
|
|
|
+ block.miner_tx.prefix.unlock_time = monero::VarInt(u64::MAX);
|
|
|
|
|
+ block.miner_tx.prefix.inputs = vec![TxIn::Gen { height: monero::VarInt(u64::MAX) }];
|
|
|
|
|
+ block.miner_tx.prefix.outputs = outputs(true, MAX_COINBASE_OUTPUTS);
|
|
|
|
|
+ // Opaque bytes only: never invoke the existing partial extra parser here.
|
|
|
|
|
+ block.miner_tx.prefix.extra = RawExtraField(vec![0xff; MAX_COINBASE_EXTRA_SIZE]);
|
|
|
|
|
+ let mut powdata = MoneroPowData::new(
|
|
|
|
|
+ block.clone(),
|
|
|
|
|
+ FixedByteArray::from_bytes(&[0x42; 60]).unwrap(),
|
|
|
|
|
+ MerkleProof::try_construct(vec![monero::Hash::from([0x33; 32]); 31], u32::MAX).unwrap(),
|
|
|
|
|
+ )
|
|
|
|
|
+ .unwrap();
|
|
|
|
|
+ assert_eq!(powdata.coinbase_hash().unwrap(), block.miner_tx.hash());
|
|
|
|
|
+ assert_eq!(powdata.coinbase_tx_prefix().len(), 44025);
|
|
|
|
|
+ assert_eq!(monero::consensus::serialize(&powdata.header).len(), 66);
|
|
|
|
|
+ assert_eq!(powdata.randomx_key().len(), 60);
|
|
|
|
|
+ powdata.coinbase_merkle_proof =
|
|
|
|
|
+ MerkleProof::try_construct(vec![monero::Hash::from([0x22; 32]); 31], 0).unwrap();
|
|
|
|
|
+ powdata.merkle_root = powdata.coinbase_merkle_proof.calculate_root(&block.miner_tx.hash());
|
|
|
|
|
+ assert!(powdata.is_coinbase_valid_merkle_root());
|
|
|
|
|
+ let wire = darkfi_serial::serialize(&powdata);
|
|
|
|
|
+ // Actual grammar maxima are tighter than the conservative 133309-byte cap.
|
|
|
|
|
+ assert_eq!(wire.len(), 8 + 67 + 61 + 2 + 33 + 2 * 997 + 3 + 44025 + 5 + 65536);
|
|
|
|
|
+ assert_roundtrip(&powdata);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+#[test]
|
|
|
|
|
+fn complete_raw_extra_including_unknown_tail_is_hashed() {
|
|
|
|
|
+ let mut block = monero_block_deserialize(XMR_BLOCK).unwrap();
|
|
|
|
|
+ let original = from_block(block.clone()).unwrap();
|
|
|
|
|
+ // An unknown subfield cannot be discarded before hashing. No tag parser
|
|
|
|
|
+ // is needed to establish equality with the complete Monero transaction.
|
|
|
|
|
+ block.miner_tx.prefix.extra.0.extend([0xff, 0x42]);
|
|
|
|
|
+ let powdata = from_block(block.clone()).unwrap();
|
|
|
|
|
+ assert_eq!(powdata.coinbase_tx_prefix(), original.coinbase_tx_prefix());
|
|
|
|
|
+ assert_eq!(powdata.coinbase_prefix_hash().unwrap(), block.miner_tx.prefix.hash());
|
|
|
|
|
+ assert_eq!(powdata.coinbase_hash().unwrap(), block.miner_tx.hash());
|
|
|
|
|
+ assert_ne!(powdata.coinbase_hash().unwrap(), original.coinbase_hash().unwrap());
|
|
|
|
|
+ assert_roundtrip(&powdata);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+fn bound_header() -> Header {
|
|
|
|
|
+ let mut header = Header::new(HeaderHash::new([0x42; 32]), 1, 7, 1_700_000_000u64.into());
|
|
|
|
|
+ let mut block = monero_block_deserialize(XMR_BLOCK).unwrap();
|
|
|
|
|
+ block.miner_tx.prefix.extra = ExtraField(vec![SubField::MergeMining(
|
|
|
|
|
+ monero::VarInt(0),
|
|
|
|
|
+ monero::Hash::from(header.template_hash().inner()),
|
|
|
|
|
+ )])
|
|
|
|
|
+ .into();
|
|
|
|
|
+ header.pow_data = PowData::Monero(from_block(block).unwrap());
|
|
|
|
|
+ assert!(header.validate_powdata());
|
|
|
|
|
+ header
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+#[test]
|
|
|
|
|
+fn template_extra_and_valid_prefix_cannot_rebind_existing_work() {
|
|
|
|
|
+ let honest = bound_header();
|
|
|
|
|
+ let PowData::Monero(original) = &honest.pow_data else { unreachable!() };
|
|
|
|
|
+ let decoded: Header = darkfi_serial::deserialize(&darkfi_serial::serialize(&honest)).unwrap();
|
|
|
|
|
+ assert!(decoded.validate_powdata());
|
|
|
|
|
+ assert_eq!(decoded.hash(), honest.hash());
|
|
|
|
|
+ #[cfg(feature = "async-serial")]
|
|
|
|
|
+ smol::future::block_on(async {
|
|
|
|
|
+ let wire = darkfi_serial::serialize_async(&honest).await;
|
|
|
|
|
+ assert_eq!(wire, darkfi_serial::serialize(&honest));
|
|
|
|
|
+ let decoded: Header = darkfi_serial::deserialize_async(&wire).await.unwrap();
|
|
|
|
|
+ assert!(decoded.validate_powdata());
|
|
|
|
|
+ assert_eq!(decoded.hash(), honest.hash());
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ let mut changed = honest.clone();
|
|
|
|
|
+ changed.nonce += 1;
|
|
|
|
|
+ assert_ne!(changed.template_hash(), honest.template_hash());
|
|
|
|
|
+ assert!(!changed.validate_powdata());
|
|
|
|
|
+ let PowData::Monero(unchanged) = &changed.pow_data else { unreachable!() };
|
|
|
|
|
+ assert!(unchanged.is_coinbase_valid_merkle_root());
|
|
|
|
|
+
|
|
|
|
|
+ let replacement: RawExtraField = ExtraField(vec![SubField::MergeMining(
|
|
|
|
|
+ monero::VarInt(0),
|
|
|
|
|
+ monero::Hash::from(changed.template_hash().inner()),
|
|
|
|
|
+ )])
|
|
|
|
|
+ .into();
|
|
|
|
|
+ assert!(ExtraField::try_parse(&replacement).is_ok());
|
|
|
|
|
+ let mut rebound = original.clone();
|
|
|
|
|
+ rebound.coinbase_tx_extra = replacement;
|
|
|
|
|
+ assert_eq!(
|
|
|
|
|
+ rebound
|
|
|
|
|
+ .aux_chain_merkle_proof
|
|
|
|
|
+ .calculate_root(&monero::Hash::from(changed.template_hash().inner())),
|
|
|
|
|
+ extract_aux_merkle_root(rebound.coinbase_tx_extra()).unwrap().unwrap()
|
|
|
|
|
+ );
|
|
|
|
|
+ assert_ne!(rebound.coinbase_hash().unwrap(), original.coinbase_hash().unwrap());
|
|
|
|
|
+ assert!(!rebound.is_coinbase_valid_merkle_root());
|
|
|
|
|
+ assert_eq!(rebound.to_block_hashing_blob(), original.to_block_hashing_blob());
|
|
|
|
|
+ assert_eq!(rebound.randomx_key(), original.randomx_key());
|
|
|
|
|
+ changed.pow_data = PowData::Monero(assert_roundtrip(&rebound));
|
|
|
|
|
+ assert!(!changed.validate_powdata());
|
|
|
|
|
+
|
|
|
|
|
+ let mut altered_block = monero_block_deserialize(XMR_BLOCK).unwrap();
|
|
|
|
|
+ altered_block.miner_tx.prefix.unlock_time.0 += 1;
|
|
|
|
|
+ let altered = from_block(altered_block).unwrap();
|
|
|
|
|
+ let mut rebound = original.clone();
|
|
|
|
|
+ rebound.coinbase_tx_prefix =
|
|
|
|
|
+ CoinbasePrefix::new(altered.coinbase_tx_prefix().to_vec()).unwrap();
|
|
|
|
|
+ assert_ne!(rebound.coinbase_tx_prefix(), original.coinbase_tx_prefix());
|
|
|
|
|
+ assert_ne!(rebound.coinbase_hash().unwrap(), original.coinbase_hash().unwrap());
|
|
|
|
|
+ assert!(!rebound.is_coinbase_valid_merkle_root());
|
|
|
|
|
+ assert_eq!(rebound.to_block_hashing_blob(), original.to_block_hashing_blob());
|
|
|
|
|
+ let mut changed = honest.clone();
|
|
|
|
|
+ changed.pow_data = PowData::Monero(assert_roundtrip(&rebound));
|
|
|
|
|
+ assert!(!changed.validate_powdata());
|
|
|
|
|
+
|
|
|
|
|
+ let mut non_coinbase_path = original.clone();
|
|
|
|
|
+ non_coinbase_path.coinbase_merkle_proof = MerkleProof::try_construct(vec![], 1).unwrap();
|
|
|
|
|
+ assert_eq!(non_coinbase_path.coinbase_hash().unwrap(), non_coinbase_path.merkle_root);
|
|
|
|
|
+ assert!(!non_coinbase_path.is_coinbase_valid_merkle_root());
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+#[cfg(feature = "validator")]
|
|
|
|
|
+#[test]
|
|
|
|
|
+fn consistent_commitments_still_require_randomx_below_local_target() -> Result<()> {
|
|
|
|
|
+ use darkfi_sdk::num_traits::{One, Zero};
|
|
|
|
|
+ use kvdb_overlay::Database;
|
|
|
|
|
+ use num_bigint::BigUint;
|
|
|
|
|
+
|
|
|
|
|
+ use crate::{
|
|
|
|
|
+ blockchain::{BlockInfo, Blockchain},
|
|
|
|
|
+ validator::pow::PoWModule,
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ let header = bound_header();
|
|
|
|
|
+ let PowData::Monero(powdata) = &header.pow_data else { unreachable!() };
|
|
|
|
|
+ let (database, _folder) = Database::open_temp()?;
|
|
|
|
|
+ let blockchain = Blockchain::new(&database)?;
|
|
|
|
|
+ let mut genesis = BlockInfo::default();
|
|
|
|
|
+ genesis.header.timestamp = 0u64.into();
|
|
|
|
|
+ blockchain.add_block(&genesis)?;
|
|
|
|
|
+ let mut module = PoWModule::new(blockchain, 120, Some(BigUint::one()), None)?;
|
|
|
|
|
+ // Fixed difficulty only applies after two local timestamps exist.
|
|
|
|
|
+ module.append(&header, &BigUint::one())?;
|
|
|
|
|
+ module.append(&header, &BigUint::one())?;
|
|
|
|
|
+
|
|
|
|
|
+ let vm = module.monero_rx_factory.create(powdata.randomx_key())?;
|
|
|
|
|
+ let digest_bytes = vm.calculate_hash(&powdata.to_block_hashing_blob())?;
|
|
|
|
|
+ assert_eq!(digest_bytes.len(), 32);
|
|
|
|
|
+ let digest = BigUint::from_bytes_le(&digest_bytes);
|
|
|
|
|
+ assert!(!digest.is_zero());
|
|
|
|
|
+ assert_eq!(module.calculate_hash(&header)?, digest);
|
|
|
|
|
+ assert_eq!(module.verify_block_target(&header, &digest)?, digest);
|
|
|
|
|
+ assert!(matches!(
|
|
|
|
|
+ module.verify_block_target(&header, &(&digest - BigUint::one())),
|
|
|
|
|
+ Err(crate::Error::PoWInvalidOutHash)
|
|
|
|
|
+ ));
|
|
|
|
|
+ let maximum = BigUint::from_bytes_le(&[0xff; 32]);
|
|
|
|
|
+ assert_eq!(module.next_mine_target()?, maximum);
|
|
|
|
|
+ module.verify_block_hash(&header)?;
|
|
|
|
|
+
|
|
|
|
|
+ let strict_difficulty = &maximum / &digest + BigUint::one();
|
|
|
|
|
+ module.fixed_difficulty = Some(strict_difficulty.clone());
|
|
|
|
|
+ assert_eq!(module.next_difficulty()?, strict_difficulty);
|
|
|
|
|
+ assert!(module.next_mine_target()? < digest);
|
|
|
|
|
+ assert!(header.validate_powdata());
|
|
|
|
|
+ assert!(matches!(module.verify_block_hash(&header), Err(crate::Error::PoWInvalidOutHash)));
|
|
|
|
|
+ Ok(())
|
|
|
|
|
+}
|