diff --git a/.github/package-filters/rs-packages-direct.yml b/.github/package-filters/rs-packages-direct.yml index b74139a2523..25201a06fe1 100644 --- a/.github/package-filters/rs-packages-direct.yml +++ b/.github/package-filters/rs-packages-direct.yml @@ -102,6 +102,9 @@ drive-proof-verifier: dash-async: - packages/rs-dash-async/** +dash-core-proof: + - packages/rs-core-proof/** + dash-context-provider: - packages/rs-context-provider/** diff --git a/.github/package-filters/rs-packages-no-workflows.yml b/.github/package-filters/rs-packages-no-workflows.yml index 5c59193d383..934d478c05a 100644 --- a/.github/package-filters/rs-packages-no-workflows.yml +++ b/.github/package-filters/rs-packages-no-workflows.yml @@ -107,6 +107,9 @@ drive-abci: dash-async: &dash_async - packages/rs-dash-async/** +dash-core-proof: &core_proof + - packages/rs-core-proof/** + dash-context-provider: &context_provider - packages/rs-context-provider/** - *dash_async diff --git a/.github/package-filters/rs-packages.yml b/.github/package-filters/rs-packages.yml index 91dd8c681ba..854e8c8bfdc 100644 --- a/.github/package-filters/rs-packages.yml +++ b/.github/package-filters/rs-packages.yml @@ -127,6 +127,10 @@ dash-async: &dash_async - .github/workflows/tests* - packages/rs-dash-async/** +dash-core-proof: &core_proof + - .github/workflows/tests* + - packages/rs-core-proof/** + dash-context-provider: &context_provider - .github/workflows/tests* - packages/rs-context-provider/** diff --git a/.github/workflows/tests-rs-workspace.yml b/.github/workflows/tests-rs-workspace.yml index fe3988d872c..eeaac8ef7cf 100644 --- a/.github/workflows/tests-rs-workspace.yml +++ b/.github/workflows/tests-rs-workspace.yml @@ -246,13 +246,14 @@ jobs: cargo check -p dapi-grpc --no-default-features --features core,platform,client --locked cargo check -p drive-proof-verifier --locked cargo check -p dash-platform-queries --locked + cargo check -p dash-core-proof --locked # Native graphs: assert the networking transport stack stays out. # `tonic` itself is present (dapi-grpc's generated client types) but # without its transport feature — which is exactly what the absence # of hyper/rustls/tower proves. tokio is deliberately NOT asserted # absent: dash-context-provider depends on dash-async, which uses it # on native targets, and that edge predates the queries-crate split. - for native_package in drive-proof-verifier dash-platform-queries; do + for native_package in drive-proof-verifier dash-platform-queries dash-core-proof; do for banned in hyper rustls tower; do if cargo tree -p "$native_package" -e normal -i "$banned" 2>/dev/null | grep -q .; then echo "::error::$banned leaked into $native_package's dependency tree" @@ -456,6 +457,7 @@ jobs: --package drive-abci \ --package dash-sdk \ --package dash-async \ + --package dash-core-proof \ --package platform-value \ --package platform-encryption \ --package rs-dapi \ diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 70c7841cf20..d8de57042fe 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -180,6 +180,16 @@ jobs: exit 0 fi + # A crate that compiles its README as doctests (`include_str!("../README.md")`) + # runs them whenever that README changes. + for readme in $(git diff --name-only "$BASE_SHA"...HEAD -- 'packages/*/README.md' 2>/dev/null); do + if grep -qsF 'include_str!("../README.md")' "$(dirname "$readme")/src/lib.rs"; then + echo "doctests-changed=true" >> "$GITHUB_OUTPUT" + echo "$readme is compiled as doctests and changed — doctests will run" + exit 0 + fi + done + echo "doctests-changed=false" >> "$GITHUB_OUTPUT" echo "No doctest changes — skipping doctests" diff --git a/Cargo.lock b/Cargo.lock index 1a5f036177b..605e2d5e6dd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1822,6 +1822,18 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "dash-core-proof" +version = "5.0.0-rc.4" +dependencies = [ + "blst", + "hex", + "rs-x11-hash", + "serde", + "serde_json", + "sha2 0.10.9", +] + [[package]] name = "dash-network" version = "0.45.0" diff --git a/Cargo.toml b/Cargo.toml index c5de8df04ee..ac5b27c9eef 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,7 @@ members = [ "packages/data-contracts", "packages/rs-drive-proof-verifier", "packages/rs-context-provider", + "packages/rs-core-proof", "packages/rs-sdk-trusted-context-provider", "packages/wasm-dpp", "packages/wasm-dpp2", diff --git a/Dockerfile b/Dockerfile index 699d538122c..4868fea42d9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -412,6 +412,7 @@ COPY --parents \ # TODO: We don't need those. Maybe dynamically remove them from workspace or move outside of monorepo? packages/rs-drive-proof-verifier \ packages/rs-context-provider \ + packages/rs-core-proof \ packages/rs-sdk-trusted-context-provider \ packages/rs-platform-wallet \ packages/rs-platform-wallet-storage \ @@ -541,6 +542,7 @@ COPY --parents \ # TODO: We don't need those. Maybe dynamically remove them from workspace or move outside of monorepo? packages/rs-drive-proof-verifier \ packages/rs-context-provider \ + packages/rs-core-proof \ packages/rs-sdk-trusted-context-provider \ packages/rs-platform-wallet \ packages/rs-platform-wallet-storage \ @@ -694,6 +696,7 @@ COPY --parents \ packages/rs-json-schema-compatibility-validator \ # rs-sdk stack (needed to build wasm-sdk / evo-sdk for the test suite) packages/rs-context-provider \ + packages/rs-core-proof \ packages/rs-dapi-client \ packages/rs-dash-platform-macros \ packages/rs-drive \ @@ -975,6 +978,7 @@ COPY --parents \ packages/rs-json-schema-compatibility-validator \ packages/rs-drive-proof-verifier \ packages/rs-context-provider \ + packages/rs-core-proof \ packages/rs-sdk-trusted-context-provider \ packages/wasm-dpp \ packages/wasm-dpp2 \ diff --git a/packages/rs-core-proof/Cargo.toml b/packages/rs-core-proof/Cargo.toml new file mode 100644 index 00000000000..60c0d6a7d97 --- /dev/null +++ b/packages/rs-core-proof/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "dash-core-proof" +version.workspace = true +rust-version.workspace = true +edition = "2021" +license = "MIT" +description = "Verifies Dash Core quorum proof chains (DIP 175) from a trusted checkpoint" + +[dependencies] +blst = "0.3.17" +hex = "0.4" +rs-x11-hash = "0.1.8" +serde = { version = "1.0", features = ["derive"] } +sha2 = "0.10" + +[dev-dependencies] +serde_json = "1.0" diff --git a/packages/rs-core-proof/README.md b/packages/rs-core-proof/README.md new file mode 100644 index 00000000000..b9fde671d74 --- /dev/null +++ b/packages/rs-core-proof/README.md @@ -0,0 +1,76 @@ +# dash-core-proof + +Verifies Dash Core quorum proof chains (`DASHNC02`, [DIP 175](https://github.com/dashpay/dips/pull/175)), +the format Dash Core's `getquorumproofchain` RPC returns. + +A client that trusts a checkpoint (a block's height and hash and the masternode and quorum +Merkle roots its coinbase commits to) can check a later block's roots, and any quorum +commitment or masternode entry under them, without trusting whoever served the proof: + +```rust +use dash_core_proof::bootstrap::{self, RecordKind}; +use dash_core_proof::{parse_commitment, Hash, State}; + +/// The key of quorum (`quorum_type`, `quorum_hash`, in wire order), if the +/// envelope proves it from `checkpoint` at `min_height` or later. +fn quorum_key( + envelope: &[u8], + checkpoint: &State, + min_height: u32, + quorum_type: u8, + quorum_hash: &Hash, +) -> Option<[u8; 48]> { + let verified = bootstrap::verify(envelope, checkpoint, min_height).ok()?; + // Every record is authentic, but the relay chose which ones to open: + // use only the quorum that was asked for. + verified + .records() + .iter() + .filter(|record| record.kind == RecordKind::Quorum) + .filter_map(|record| parse_commitment(record.leaf).ok()) + .find(|c| c.quorum_type == quorum_type && c.quorum_hash == *quorum_hash) + .map(|c| c.public_key) +} +``` + +A `State` deserializes from the same JSON Core's RPC uses for `target`, so a block's +`getquorumproofchain` target can serve as a checkpoint file. + +## What it checks + +- The chain starts at exactly the checkpoint the caller supplies. +- Each ChainLock certificate is a valid Basic-scheme BLS signature by the current ChainLock + quorum, at a strictly increasing height. +- Each next ChainLock quorum's commitment is mined in the certified block, or in an ancestor + linked to it header by header. +- The target block's coinbase (payload version 3, or 4 from Core v24) commits to the returned + roots, and every record opens under them. +- Nothing Core's verifier rejects is accepted: CompactSizes, commitments and BLS points must be + canonical, Merkle paths free of duplicate-leaf ambiguity, leaves and transactions other than + 64 bytes, with no trailing bytes. The one place it is stricter than Core is legacy-scheme BLS + signature bytes Core would read as null; in a coinbase's best ChainLock signature those are + valid only before any ChainLock, and a proof ending at such a block is refused. Every length is + bounded before allocation. + +## Trust model + +The checkpoint, and that none of these ChainLock quorums ever signs dishonestly, including +quorums long since retired: + +- every ChainLock quorum committed under the checkpoint's quorum root, however long ago it was + mined, because any of them can seed a proof; +- every ChainLock quorum mined less than 4,096 blocks before the checkpoint, or at any time after + it, because a link's ancestor headers (at most 4,096 per proof) can reach back that far. + +The relay picks the path, and like Core's verifier this one does not check that a signer was the +quorum selected for that height. +It does not replay DKG or re-derive which members signed. An older checkpoint therefore trusts +more quorums for longer; refresh checkpoints with releases. + +A valid proof does not show that its target is the newest ChainLock; how fresh a target must +be is the caller's policy. + +## Tests + +`cargo test -p dash-core-proof`. The fixtures in `tests/data` are unmodified +`getquorumproofchain` results from mainnet and testnet nodes. diff --git a/packages/rs-core-proof/src/bls.rs b/packages/rs-core-proof/src/bls.rs new file mode 100644 index 00000000000..bc9e79f5b71 --- /dev/null +++ b/packages/rs-core-proof/src/bls.rs @@ -0,0 +1,113 @@ +//! Basic-scheme BLS checks with the `blst` backend the SDK already ships. +use crate::wire::{Hash, Result}; +use crate::Error; +use blst::min_pk::{PublicKey, Signature}; +use blst::BLST_ERROR; + +const BLS_DST: &[u8] = b"BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_NUL_"; + +/// Decode a public key that is canonical, in the prime-order subgroup and not +/// the point at infinity. +pub(crate) fn public_key(key: &[u8; 48]) -> Result { + let pk = PublicKey::key_validate(key).map_err(|_| Error("BLS public key"))?; + if pk.compress() != *key { + return Err(Error("BLS noncanonical public key")); + } + Ok(pk) +} + +/// Accept the signature encodings Core deserializes: all zeros (a null +/// signature), or a canonical compressed point in the subgroup other than the +/// point at infinity, which Core resets and then rejects as malleable. +/// +/// Core also accepts, as null, bytes that are invalid in the Basic scheme but +/// valid in the legacy one; they are rejected here. In a mined commitment they +/// cannot occur, because Core verifies its signatures. In a coinbase's best +/// ChainLock signature they are consensus-valid only while no earlier coinbase +/// carries a ChainLock; a proof ending at such a block is refused, which costs +/// availability for that one target but never accepts anything Core rejects. +pub(crate) fn check_signature_encoding(signature: &[u8; 96]) -> Result<()> { + if signature.iter().all(|byte| *byte == 0) { + return Ok(()); + } + let sig = + Signature::sig_validate(signature, true).map_err(|_| Error("BLS signature encoding"))?; + if sig.compress() != *signature { + return Err(Error("BLS signature encoding")); + } + Ok(()) +} + +/// Verify a Basic-scheme signature over a 32-byte message hash. +pub(crate) fn verify(key: &[u8; 48], signature: &[u8; 96], message: &Hash) -> Result<()> { + let pk = public_key(key)?; + let sig = Signature::sig_validate(signature, true).map_err(|_| Error("BLS signature"))?; + if sig.compress() != *signature { + return Err(Error("BLS noncanonical signature")); + } + // Both inputs were validated above, so verification need not repeat it. + if sig.verify(false, message, BLS_DST, &[], &pk, false) != BLST_ERROR::BLST_SUCCESS { + return Err(Error("BLS verification")); + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use blst::min_pk::SecretKey; + + #[test] + fn should_reject_infinity_noncanonical_and_torsion_encodings() { + let secret = SecretKey::key_gen(&[42u8; 32], &[]).unwrap(); + let key = secret.sk_to_pk().compress(); + let message = [0u8; 32]; + let signature = secret.sign(&message, BLS_DST, &[]).compress(); + assert_eq!(verify(&key, &signature, &message), Ok(())); + assert!(verify(&key, &signature, &[1u8; 32]).is_err()); + + let mut infinity_key = [0u8; 48]; + infinity_key[0] = 0xc0; + let mut infinity_signature = [0u8; 96]; + infinity_signature[0] = 0xc0; + assert!(verify(&infinity_key, &infinity_signature, &message).is_err()); + assert!(verify(&key, &infinity_signature, &message).is_err()); + assert!(verify(&infinity_key, &signature, &message).is_err()); + + // (0, 2) lies on G1 but has order 3, outside the prime-order subgroup. + let mut torsion_key = [0u8; 48]; + torsion_key[0] = 0x80; + assert!(verify(&torsion_key, &signature, &message).is_err()); + + let mut field_modulus: [u8; 48] = hex::decode( + "1a0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaab", + ) + .unwrap() + .try_into() + .unwrap(); + field_modulus[0] |= 0x80; + assert!(verify(&field_modulus, &signature, &message).is_err()); + assert!(verify(&[0xff; 48], &signature, &message).is_err()); + assert!(verify(&key, &[0xff; 96], &message).is_err()); + } + + #[test] + fn should_accept_only_signature_encodings_core_deserializes() { + let secret = SecretKey::key_gen(&[7u8; 32], &[]).unwrap(); + let signature = secret.sign(&[0u8; 32], BLS_DST, &[]).compress(); + assert_eq!(check_signature_encoding(&signature), Ok(())); + assert_eq!(check_signature_encoding(&[0; 96]), Ok(())); + // Core resets the identity element and then rejects it as malleable. + let mut infinity = [0u8; 96]; + infinity[0] = 0xc0; + assert!(check_signature_encoding(&infinity).is_err()); + // Uncompressed flag, garbage, and a nonzero infinity payload. + let mut uncompressed = signature; + uncompressed[0] &= 0x7f; + assert!(check_signature_encoding(&uncompressed).is_err()); + assert!(check_signature_encoding(&[0xff; 96]).is_err()); + let mut dirty_infinity = infinity; + dirty_infinity[95] = 1; + assert!(check_signature_encoding(&dirty_infinity).is_err()); + } +} diff --git a/packages/rs-core-proof/src/bootstrap.rs b/packages/rs-core-proof/src/bootstrap.rs new file mode 100644 index 00000000000..ae6c5ebb7eb --- /dev/null +++ b/packages/rs-core-proof/src/bootstrap.rs @@ -0,0 +1,94 @@ +//! The bootstrap envelope: a proof chain plus Merkle openings of records under +//! the target's roots, as `getquorumproofchain` returns in `bootstrap_hex`. +use crate::chain::{QuorumProofChain, MAX_PROOF_BYTES}; +use crate::merkle::MerklePath; +use crate::state::State; +use crate::wire::{sha256d, Reader, Result}; +use crate::Error; + +/// Most records one envelope may open, as in Core. +const MAX_RECORDS: u8 = 16; +/// Largest record leaf, as in Core. +const MAX_LEAF_BYTES: usize = 4096; + +/// Which target root a record is opened under. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum RecordKind { + /// A serialized quorum commitment under the quorum root. + Quorum, + /// A simplified masternode list entry, without its version, under the + /// masternode root. + Masternode, +} + +/// A record the envelope opened under the verified target state. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct VerifiedRecord<'a> { + /// The root it is opened under. + pub kind: RecordKind, + /// The leaf's preimage. + pub leaf: &'a [u8], +} + +/// The result of verifying an envelope. Only [`verify`] constructs it. +#[derive(Debug)] +pub struct VerifiedBootstrap<'a> { + state: State, + records: Vec>, +} + +impl<'a> VerifiedBootstrap<'a> { + /// The authenticated target state. + pub fn state(&self) -> &State { + &self.state + } + + /// The records, in envelope order. + pub fn records(&self) -> &[VerifiedRecord<'a>] { + &self.records + } +} + +/// Verify an envelope from the `trusted` checkpoint, requiring a target at or +/// above `minimum_height`. +/// +/// Records are returned only after the chain and every opening verify. The +/// caller still has to check that the records are the ones it asked for and +/// decode their contents. +pub fn verify<'a>( + bytes: &'a [u8], + trusted: &State, + minimum_height: u32, +) -> Result> { + if bytes.len() > MAX_PROOF_BYTES { + return Err(Error("bootstrap size limit")); + } + let mut r = Reader::new(bytes); + let chain = QuorumProofChain::decode(r.blob(MAX_PROOF_BYTES)?)?; + // Decided before any signature work: the height alone makes it useless. + if chain.target.height < minimum_height { + return Err(Error::STALE_TARGET); + } + let state = chain.verify(trusted)?; + let count = r.u8()?; + if !(1..=MAX_RECORDS).contains(&count) { + return Err(Error("projection count")); + } + let mut records = Vec::with_capacity(count as usize); + for _ in 0..count { + let (kind, root) = match r.u8()? { + 0 => (RecordKind::Quorum, &state.quorum_root), + 1 => (RecordKind::Masternode, &state.masternode_root), + _ => return Err(Error("record kind")), + }; + let leaf = r.blob(MAX_LEAF_BYTES)?; + // A 64-byte leaf could pose as an inner Merkle node. + if leaf.is_empty() || leaf.len() == 64 { + return Err(Error("record leaf size")); + } + MerklePath::decode(&mut r)?.verify(sha256d(leaf), root)?; + records.push(VerifiedRecord { kind, leaf }); + } + r.finish()?; + Ok(VerifiedBootstrap { state, records }) +} diff --git a/packages/rs-core-proof/src/chain.rs b/packages/rs-core-proof/src/chain.rs new file mode 100644 index 00000000000..3b7ed0618a7 --- /dev/null +++ b/packages/rs-core-proof/src/chain.rs @@ -0,0 +1,314 @@ +//! The `DASHNC02` quorum proof chain (Core's `QuorumProofChain`). +//! +//! Starting from a checkpoint's quorum root, the chain opens one ChainLock +//! quorum's commitment, then follows ChainLock certificates: each one signed +//! by the current ChainLock quorum authenticates a block that mined the next +//! ChainLock quorum. The last certificate authenticates a target block whose +//! coinbase commits to new masternode and quorum roots. +use crate::bls; +use crate::merkle::MerklePath; +use crate::state::State; +use crate::transaction::{ + coinbase_roots, mined_commitment, parse_commitment, Commitment, MAX_COMMITMENT_BYTES, + MAX_TRANSACTION_BYTES, +}; +use crate::wire::{sha256d, write_blob, Hash, Reader, Result}; +use crate::Error; + +/// Eight-byte format identifier: DASH, Network Compact, format 02. +pub const PROOF_MAGIC: &[u8; 8] = b"DASHNC02"; +/// Largest encoded proof chain or bootstrap envelope, as in Core. +pub const MAX_PROOF_BYTES: usize = 1024 * 1024; +/// Bound on the number of certificates in a chain, as in Core. +pub const MAX_PROOF_CERTIFICATES: usize = 4096; +/// Bound on the ancestor headers across a whole chain, as in Core. +pub const MAX_PROOF_HEADERS: usize = 4096; + +/// A block header's X11 hash, its Dash block hash. +pub fn header_hash(header: &[u8; 80]) -> Hash { + // The hash's C routine reads exactly 80 bytes whatever the slice length; + // the array type guarantees that. + rs_x11_hash::get_x11_hash(header) +} + +/// The hash a ChainLock quorum signs for a block at `height`. +fn chainlock_sign_hash(signer: &Commitment, height: u32, block: &Hash) -> Hash { + let mut request = b"\x05clsig".to_vec(); + request.extend(height.to_le_bytes()); + let mut sign = vec![signer.quorum_type]; + sign.extend(signer.quorum_hash); + sign.extend(sha256d(&request)); + sign.extend(block); + sha256d(&sign) +} + +/// A ChainLock: a block header at a height, signed by a ChainLock quorum. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Certificate { + /// Height of the block. + pub height: u32, + /// The block's 80-byte header. + pub header: [u8; 80], + /// The quorum's Basic-scheme signature. + pub signature: [u8; 96], +} + +impl Certificate { + /// Check the signature by `signer` at a height above `minimum`, and return + /// the certified block's hash. + fn verify(&self, signer: &Commitment, minimum: u32) -> Result { + if self.height <= minimum || self.height > i32::MAX as u32 { + return Err(Error("non-increasing certificate")); + } + let hash = header_hash(&self.header); + let message = chainlock_sign_hash(signer, self.height, &hash); + bls::verify(&signer.public_key, &self.signature, &message)?; + Ok(hash) + } + + fn encode(&self, out: &mut Vec) { + out.extend(self.height.to_le_bytes()); + out.extend(self.header); + out.extend(self.signature); + } + + fn decode(r: &mut Reader<'_>) -> Result { + Ok(Self { + height: r.u32()?, + header: r.array()?, + signature: r.array()?, + }) + } +} + +/// A complete transaction and its path to a block's transaction Merkle root. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ProofTransaction { + /// The serialized transaction. + pub transaction: Vec, + /// Its position in the block. + pub path: MerklePath, +} + +impl ProofTransaction { + fn verify(&self, header: &[u8; 80]) -> Result<()> { + // A 64-byte transaction could pose as an inner Merkle node. + if self.transaction.len() == 64 || self.transaction.len() > MAX_TRANSACTION_BYTES { + return Err(Error("transaction size")); + } + let mut root = [0; 32]; + root.copy_from_slice(&header[36..68]); + self.path.verify(sha256d(&self.transaction), &root) + } + + fn encode(&self, out: &mut Vec) -> Result<()> { + if self.transaction.len() > MAX_TRANSACTION_BYTES { + return Err(Error("proof transaction limit")); + } + write_blob(out, &self.transaction)?; + self.path.encode(out) + } + + fn decode(r: &mut Reader<'_>) -> Result { + Ok(Self { + transaction: r.blob(MAX_TRANSACTION_BYTES)?.to_vec(), + path: MerklePath::decode(r)?, + }) + } +} + +/// One step of the chain: a certificate by the current ChainLock quorum and +/// the transaction that mined the next one. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Link { + /// The ChainLock that authenticates this step. + pub certificate: Certificate, + /// The next ChainLock quorum's commitment transaction. + pub mining: ProofTransaction, + /// Headers from the mining block up to the certified block's parent, + /// oldest first; empty when the mining block itself is certified. + pub ancestors: Vec<[u8; 80]>, +} + +impl Link { + /// Check that the mining transaction sits in the certified block or in an + /// ancestor linked to it, and return the commitment it mines. + fn mined_commitment(&self) -> Result { + let ancestors = u32::try_from(self.ancestors.len()).map_err(|_| Error("ancestor limit"))?; + let height = self + .certificate + .height + .checked_sub(ancestors) + .filter(|height| *height > 0) + .ok_or(Error("ancestor budget/height"))?; + let mut descendant = &self.certificate.header; + for ancestor in self.ancestors.iter().rev() { + if descendant[4..36] != header_hash(ancestor) { + return Err(Error("ancestor continuity")); + } + descendant = ancestor; + } + if self.mining.path.index == 0 { + return Err(Error("mining inclusion")); + } + self.mining.verify(descendant)?; + mined_commitment(&self.mining.transaction, height) + } +} + +/// A `DASHNC02` proof chain, as `getquorumproofchain` returns in `proof_hex`. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct QuorumProofChain { + /// The checkpoint the chain starts from. + pub anchor: State, + /// The first ChainLock quorum's serialized commitment. + pub seed: Vec, + /// Its path to the checkpoint's quorum root. + pub seed_path: MerklePath, + /// Steps through later ChainLock quorums. + pub links: Vec, + /// The ChainLock of the target block. + pub target: Certificate, + /// The target block's coinbase. + pub coinbase: ProofTransaction, +} + +impl QuorumProofChain { + /// Verify the chain from `trusted` and return the target block's state. + /// + /// The chain must start from exactly `trusted`; a relay cannot choose the + /// checkpoint. + pub fn verify(&self, trusted: &State) -> Result { + if &self.anchor != trusted { + return Err(Error::UNTRUSTED_CHECKPOINT); + } + trusted.validate()?; + if self.links.len() >= MAX_PROOF_CERTIFICATES { + return Err(Error("certificate limit")); + } + let headers: usize = self.links.iter().map(|link| link.ancestors.len()).sum(); + if headers > MAX_PROOF_HEADERS { + return Err(Error("ancestor budget/height")); + } + let chainlock_type = trusted.network.chainlock_quorum_type(); + let ensure_chainlock = |commitment: Commitment| { + if commitment.quorum_type == chainlock_type { + Ok(commitment) + } else { + Err(Error("unexpected quorum type")) + } + }; + + let mut signer = ensure_chainlock(parse_commitment(&self.seed)?)?; + self.seed_path + .verify(sha256d(&self.seed), &trusted.quorum_root)?; + let mut height = trusted.height; + for link in &self.links { + link.certificate.verify(&signer, height)?; + let next = ensure_chainlock(link.mined_commitment()?)?; + if next.quorum_hash == signer.quorum_hash { + return Err(Error("redundant signer")); + } + signer = next; + height = link.certificate.height; + } + let block_hash = self.target.verify(&signer, height)?; + if self.coinbase.path.index != 0 { + return Err(Error("coinbase inclusion")); + } + self.coinbase.verify(&self.target.header)?; + let (masternode_root, quorum_root) = + coinbase_roots(&self.coinbase.transaction, self.target.height)?; + Ok(State { + network: trusted.network, + height: self.target.height, + block_hash, + masternode_root, + quorum_root, + }) + } + + /// Decode a chain, bounding every allocation by the input's length. + pub fn decode(bytes: &[u8]) -> Result { + if bytes.len() > MAX_PROOF_BYTES { + return Err(Error("proof size limit")); + } + let mut r = Reader::new(bytes); + if r.take(PROOF_MAGIC.len())? != PROOF_MAGIC { + return Err(Error("proof version")); + } + let anchor = State::decode(&mut r)?; + let seed = r.blob(MAX_COMMITMENT_BYTES)?.to_vec(); + let seed_path = MerklePath::decode(&mut r)?; + let count = r.u16()? as usize; + if count >= MAX_PROOF_CERTIFICATES { + return Err(Error("certificate limit")); + } + let mut remaining = MAX_PROOF_HEADERS; + let mut links = Vec::new(); + for _ in 0..count { + let certificate = Certificate::decode(&mut r)?; + let mining = ProofTransaction::decode(&mut r)?; + let headers = r.u16()? as usize; + remaining = remaining + .checked_sub(headers) + .ok_or(Error("ancestor length/budget"))?; + // Take every header's bytes before allocating for them. + let raw = r.take(headers * 80)?; + let ancestors = raw.as_chunks::<80>().0.to_vec(); + links.push(Link { + certificate, + mining, + ancestors, + }); + } + let target = Certificate::decode(&mut r)?; + let coinbase = ProofTransaction::decode(&mut r)?; + r.finish()?; + Ok(Self { + anchor, + seed, + seed_path, + links, + target, + coinbase, + }) + } + + /// Encode the chain as Core does. + pub fn encode(&self) -> Result> { + if self.links.len() >= MAX_PROOF_CERTIFICATES { + return Err(Error("certificate limit")); + } + if self.seed.len() > MAX_COMMITMENT_BYTES { + return Err(Error("proof blob length")); + } + let mut out = PROOF_MAGIC.to_vec(); + self.anchor.encode(&mut out); + write_blob(&mut out, &self.seed)?; + self.seed_path.encode(&mut out)?; + out.extend((self.links.len() as u16).to_le_bytes()); + let mut remaining = MAX_PROOF_HEADERS; + for link in &self.links { + remaining = remaining + .checked_sub(link.ancestors.len()) + .ok_or(Error("total ancestor limit"))?; + link.certificate.encode(&mut out); + link.mining.encode(&mut out)?; + out.extend((link.ancestors.len() as u16).to_le_bytes()); + for header in &link.ancestors { + out.extend(header); + } + // Checked per link, as in Core, so a huge chain fails early. + if out.len() > MAX_PROOF_BYTES { + return Err(Error("proof size limit")); + } + } + self.target.encode(&mut out); + self.coinbase.encode(&mut out)?; + if out.len() > MAX_PROOF_BYTES { + return Err(Error("proof size limit")); + } + Ok(out) + } +} diff --git a/packages/rs-core-proof/src/lib.rs b/packages/rs-core-proof/src/lib.rs new file mode 100644 index 00000000000..dea80637e15 --- /dev/null +++ b/packages/rs-core-proof/src/lib.rs @@ -0,0 +1,99 @@ +//! Verification of Dash Core quorum proof chains (`DASHNC02`, [DIP 175]). +//! +//! An application fixes a trusted checkpoint, a [`State`]: a block's height +//! and hash and the masternode and quorum Merkle roots its coinbase commits +//! to. Core's `getquorumproofchain` RPC proves later state from it. Its +//! `proof_hex` is a [`QuorumProofChain`] ending in a ChainLocked block whose +//! coinbase commits to new roots; its `bootstrap_hex` adds Merkle openings of +//! quorum commitments and masternode entries under those roots, which +//! [`bootstrap::verify`] checks as a whole. +//! +//! The trust model is the checkpoint, and that none of these ChainLock quorums +//! ever signs dishonestly, retired ones included: every ChainLock quorum +//! committed under the checkpoint's quorum root, however long ago it was +//! mined (any of them can seed a proof), and every ChainLock quorum mined less +//! than [`MAX_PROOF_HEADERS`] blocks before the checkpoint or at any time after +//! it (a link's ancestor headers can reach back that far). The relay chooses +//! the path, and, as in Core, nothing checks that a signer was the quorum +//! selected for its height. The verifier checks every +//! byte, signature and Merkle path the way Core's verifier does, but it does +//! not replay DKG or re-derive which members signed, and a valid proof does +//! not show that its target is the newest ChainLock: freshness is the +//! caller's policy. +//! +//! Every length is bounded before allocation, and decoding is canonical: +//! re-encoding a decoded chain gives back the same bytes. +//! +//! [DIP 175]: https://github.com/dashpay/dips/pull/175 +#![forbid(unsafe_code)] +#![deny(missing_docs)] + +mod bls; +pub mod bootstrap; +mod chain; +mod merkle; +mod state; +mod transaction; +mod wire; + +pub use chain::{ + header_hash, Certificate, Link, ProofTransaction, QuorumProofChain, MAX_PROOF_BYTES, + MAX_PROOF_CERTIFICATES, MAX_PROOF_HEADERS, PROOF_MAGIC, +}; +pub use merkle::MerklePath; +pub use state::{from_rpc_hex, rpc_hex, ProofNetwork, State}; +pub use transaction::{coinbase_roots, parse_commitment, Commitment, MAX_TRANSACTION_BYTES}; +pub use wire::{sha256d, Hash, Result}; + +/// Why a proof, or a part of one, was rejected. +/// +/// Match on [`Error::kind`]; the message is for people and may change. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Error(&'static str); + +/// The categories of [`Error`] a caller may act on differently. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[non_exhaustive] +pub enum ErrorKind { + /// The proof is valid as far as checked, but its target is below the + /// minimum height asked for: the relay is behind, or the height was wrong. + StaleTarget, + /// The proof starts from a checkpoint other than the trusted one. + UntrustedCheckpoint, + /// The input is malformed, inconsistent or wrongly signed. + Invalid, +} + +const STALE_TARGET: &str = "stale target"; +const UNTRUSTED_CHECKPOINT: &str = "untrusted snapshot"; + +impl Error { + pub(crate) const STALE_TARGET: Error = Error(STALE_TARGET); + pub(crate) const UNTRUSTED_CHECKPOINT: Error = Error(UNTRUSTED_CHECKPOINT); + + /// The category of the rejection. + pub fn kind(&self) -> ErrorKind { + match self.0 { + STALE_TARGET => ErrorKind::StaleTarget, + UNTRUSTED_CHECKPOINT => ErrorKind::UntrustedCheckpoint, + _ => ErrorKind::Invalid, + } + } + + /// A short description of what failed. + pub fn message(&self) -> &'static str { + self.0 + } +} + +impl core::fmt::Display for Error { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.write_str(self.0) + } +} + +impl std::error::Error for Error {} + +#[cfg(doctest)] +#[doc = include_str!("../README.md")] +struct ReadmeDoctests; diff --git a/packages/rs-core-proof/src/merkle.rs b/packages/rs-core-proof/src/merkle.rs new file mode 100644 index 00000000000..d9fc1efece9 --- /dev/null +++ b/packages/rs-core-proof/src/merkle.rs @@ -0,0 +1,154 @@ +//! Core's positional Merkle paths (`ProofMerklePath`). +use crate::wire::{sha256d, Hash, Reader, Result}; +use crate::Error; + +/// Most leaves a path may claim; Core uses the same bound. +const MAX_LEAVES: u32 = 100_000; +/// Most siblings a path may carry, enough for [`MAX_LEAVES`]. +const MAX_DEPTH: usize = 17; + +/// The parent of two sibling nodes. +fn node(left: &Hash, right: &Hash) -> Hash { + let mut pair = [0; 64]; + pair[..32].copy_from_slice(left); + pair[32..].copy_from_slice(right); + sha256d(&pair) +} + +/// The siblings from a leaf up to a Merkle root, one per level. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct MerklePath { + /// Position of the leaf. + pub index: u32, + /// Number of leaves in the tree. + pub count: u32, + /// One sibling per level. Where a level has an odd width and the leaf is + /// last, the sibling is the node itself, as in Bitcoin's Merkle trees. + pub siblings: Vec, +} + +impl MerklePath { + /// Check that `leaf` sits at `index` of a `count`-leaf tree with `root`. + /// + /// A sibling equal to the node is accepted only where the tree duplicates + /// that node, so a path cannot use Bitcoin's duplicate-leaf ambiguity to + /// prove a leaf at a second position. + pub fn verify(&self, mut leaf: Hash, root: &Hash) -> Result<()> { + if self.count == 0 || self.count > MAX_LEAVES || self.index >= self.count { + return Err(Error("Merkle position")); + } + let mut width = self.count; + let mut index = self.index; + let mut siblings = self.siblings.iter(); + while width > 1 { + let sibling = siblings.next().ok_or(Error("short Merkle path"))?; + let duplicated = index ^ 1 >= width; + if duplicated != (*sibling == leaf) { + return Err(Error("mutated Merkle path")); + } + leaf = if index & 1 == 0 { + node(&leaf, sibling) + } else { + node(sibling, &leaf) + }; + index /= 2; + width = width.div_ceil(2); + } + if siblings.next().is_some() || leaf != *root { + return Err(Error("Merkle root/path")); + } + Ok(()) + } + + pub(crate) fn decode(r: &mut Reader<'_>) -> Result { + let index = r.u32()?; + let count = r.u32()?; + let depth = r.u8()? as usize; + if depth > MAX_DEPTH { + return Err(Error("Merkle depth limit")); + } + let siblings = (0..depth).map(|_| r.array()).collect::>()?; + Ok(Self { + index, + count, + siblings, + }) + } + + pub(crate) fn encode(&self, out: &mut Vec) -> Result<()> { + if self.siblings.len() > MAX_DEPTH { + return Err(Error("Merkle depth limit")); + } + out.extend(self.index.to_le_bytes()); + out.extend(self.count.to_le_bytes()); + out.push(self.siblings.len() as u8); + for sibling in &self.siblings { + out.extend(sibling); + } + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn should_accept_duplicated_last_leaf_only_at_its_own_position() { + let (a, b, c) = ([1; 32], [2; 32], [3; 32]); + let root = node(&node(&a, &b), &node(&c, &c)); + let last = MerklePath { + index: 2, + count: 3, + siblings: vec![c, node(&a, &b)], + }; + assert_eq!(last.verify(c, &root), Ok(())); + // The same leaf cannot be proven at the phantom fourth position. + let phantom = MerklePath { + index: 3, + count: 3, + siblings: vec![c, node(&a, &b)], + }; + assert!(phantom.verify(c, &root).is_err()); + // A four-leaf claim over the same root is rejected because the + // sibling equals the node where the tree does not duplicate. + let four = MerklePath { + index: 2, + count: 4, + siblings: vec![c, node(&a, &b)], + }; + assert!(four.verify(c, &root).is_err()); + } + + #[test] + fn should_reject_paths_with_extra_or_missing_siblings() { + let (a, b) = ([1; 32], [2; 32]); + let root = node(&a, &b); + let path = |siblings| MerklePath { + index: 0, + count: 2, + siblings, + }; + assert_eq!(path(vec![b]).verify(a, &root), Ok(())); + assert!(path(vec![]).verify(a, &root).is_err()); + assert!(path(vec![b, b]).verify(a, &root).is_err()); + let single = MerklePath { + index: 0, + count: 1, + siblings: vec![], + }; + assert_eq!(single.verify(a, &a), Ok(())); + } + + #[test] + fn should_reject_out_of_range_positions() { + let path = |index, count| MerklePath { + index, + count, + siblings: vec![], + }; + assert!(path(0, 0).verify([0; 32], &[0; 32]).is_err()); + assert!(path(1, 1).verify([0; 32], &[0; 32]).is_err()); + assert!(path(0, MAX_LEAVES + 1).verify([0; 32], &[0; 32]).is_err()); + } +} diff --git a/packages/rs-core-proof/src/state.rs b/packages/rs-core-proof/src/state.rs new file mode 100644 index 00000000000..af92b434ace --- /dev/null +++ b/packages/rs-core-proof/src/state.rs @@ -0,0 +1,203 @@ +//! The authenticated state a proof starts from and ends at (Core's `ProofState`). +use crate::wire::{Hash, Reader, Result}; +use crate::Error; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; + +/// Core network a [`State`] belongs to. Only these two networks have the +/// fixed ChainLock quorum types the proof format relies on. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum ProofNetwork { + /// Mainnet, ChainLocks signed by `LLMQ_400_60`. + Mainnet, + /// Testnet, ChainLocks signed by `LLMQ_50_60`. + Testnet, +} + +impl ProofNetwork { + fn from_wire(network: u8) -> Result { + match network { + 0 => Ok(Self::Mainnet), + 1 => Ok(Self::Testnet), + _ => Err(Error("unsupported proof network")), + } + } + + fn to_wire(self) -> u8 { + match self { + Self::Mainnet => 0, + Self::Testnet => 1, + } + } + + /// The LLMQ type that signs this network's ChainLocks. + pub fn chainlock_quorum_type(self) -> u8 { + match self { + Self::Mainnet => 2, + Self::Testnet => 1, + } + } + + /// First height of the era the format covers: v20 (Basic BLS, coinbase + /// ChainLocks) is buried at this height. Core rejects earlier checkpoints. + fn first_height(self) -> u32 { + match self { + Self::Mainnet => 1_987_776, + Self::Testnet => 905_100, + } + } +} + +/// A block's height and hash and the masternode and quorum Merkle roots its +/// coinbase commits to. +/// +/// As a checkpoint, it is trust configuration the application fixes; a relay +/// never chooses it. The JSON form matches Core's `ProofState::ToJson`, so the +/// `target` that `getquorumproofchain` returns is a valid checkpoint file: +/// `network` is 0 for mainnet and 1 for testnet, and hashes are hex in RPC +/// display order. Unlike Core's `FromJson`, deserialization rejects unknown +/// fields and other networks. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct State { + /// Network the block belongs to. + pub network: ProofNetwork, + /// Block height. + pub height: u32, + /// Block hash, wire order. + pub block_hash: Hash, + /// The coinbase's `merkleRootMNList`, wire order. + pub masternode_root: Hash, + /// The coinbase's `merkleRootQuorums`, wire order. + pub quorum_root: Hash, +} + +impl State { + /// Check that the state is one a proof can start from. + pub fn validate(&self) -> Result<()> { + if self.height < self.network.first_height() || self.height > i32::MAX as u32 { + return Err(Error("height outside supported era")); + } + if self.block_hash == [0; 32] || self.quorum_root == [0; 32] { + return Err(Error("empty authenticated state")); + } + Ok(()) + } + + pub(crate) fn encode(&self, out: &mut Vec) { + out.push(self.network.to_wire()); + out.extend(self.height.to_le_bytes()); + out.extend(self.block_hash); + out.extend(self.masternode_root); + out.extend(self.quorum_root); + } + + pub(crate) fn decode(r: &mut Reader<'_>) -> Result { + Ok(Self { + network: ProofNetwork::from_wire(r.u8()?)?, + height: r.u32()?, + block_hash: r.array()?, + masternode_root: r.array()?, + quorum_root: r.array()?, + }) + } +} + +/// A hash as Core's RPC displays it: hex of the reversed wire bytes. +pub fn rpc_hex(hash: &Hash) -> String { + let mut reversed = *hash; + reversed.reverse(); + hex::encode(reversed) +} + +/// Parse a hash in Core's RPC display order into wire order. +pub fn from_rpc_hex(text: &str) -> Result { + // Decoding into the array rejects any other length before allocating. + let mut hash: Hash = [0; 32]; + hex::decode_to_slice(text, &mut hash).map_err(|_| Error("hash must be 64 hex characters"))?; + hash.reverse(); + Ok(hash) +} + +#[derive(Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +struct StateJson { + network: u8, + height: u32, + block_hash: String, + masternode_root: String, + quorum_root: String, +} + +impl Serialize for State { + fn serialize(&self, serializer: S) -> core::result::Result { + StateJson { + network: self.network.to_wire(), + height: self.height, + block_hash: rpc_hex(&self.block_hash), + masternode_root: rpc_hex(&self.masternode_root), + quorum_root: rpc_hex(&self.quorum_root), + } + .serialize(serializer) + } +} + +impl<'de> Deserialize<'de> for State { + fn deserialize>(deserializer: D) -> core::result::Result { + let json = StateJson::deserialize(deserializer)?; + let hash = |text: &str| from_rpc_hex(text).map_err(serde::de::Error::custom); + Ok(Self { + network: ProofNetwork::from_wire(json.network).map_err(serde::de::Error::custom)?, + height: json.height, + block_hash: hash(&json.block_hash)?, + masternode_root: hash(&json.masternode_root)?, + quorum_root: hash(&json.quorum_root)?, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn state(network: ProofNetwork, height: u32) -> State { + State { + network, + height, + block_hash: [1; 32], + masternode_root: [2; 32], + quorum_root: [3; 32], + } + } + + #[test] + fn should_accept_checkpoints_from_the_v20_height_as_core_does() { + assert!(state(ProofNetwork::Mainnet, 1_987_776).validate().is_ok()); + assert!(state(ProofNetwork::Mainnet, 1_987_775).validate().is_err()); + assert!(state(ProofNetwork::Testnet, 905_100).validate().is_ok()); + assert!(state(ProofNetwork::Testnet, 905_099).validate().is_err()); + assert!(state(ProofNetwork::Testnet, i32::MAX as u32 + 1) + .validate() + .is_err()); + let mut empty = state(ProofNetwork::Testnet, 905_100); + empty.quorum_root = [0; 32]; + assert!(empty.validate().is_err()); + } + + #[test] + fn should_round_trip_json_in_rpc_byte_order() { + let mut original = state(ProofNetwork::Testnet, 1_000_000); + original.block_hash[0] = 0xab; + let json = serde_json::to_value(&original).unwrap(); + assert_eq!(json["network"], 1); + assert!(json["block_hash"].as_str().unwrap().ends_with("ab")); + assert_eq!(serde_json::from_value::(json).unwrap(), original); + let unknown = serde_json::json!({ + "network": 2, "height": 1_000_000, + "block_hash": "00".repeat(32), "masternode_root": "00".repeat(32), + "quorum_root": "00".repeat(32), + }); + assert!(serde_json::from_value::(unknown).is_err()); + assert!(from_rpc_hex(&"00".repeat(31)).is_err()); + assert!(from_rpc_hex(&"00".repeat(33)).is_err()); + assert!(from_rpc_hex(&"zz".repeat(32)).is_err()); + } +} diff --git a/packages/rs-core-proof/src/transaction.rs b/packages/rs-core-proof/src/transaction.rs new file mode 100644 index 00000000000..248332c8263 --- /dev/null +++ b/packages/rs-core-proof/src/transaction.rs @@ -0,0 +1,452 @@ +//! The transactions and payloads a proof carries: quorum commitments, the +//! special transactions that mine them, and coinbases. Each parser rejects +//! everything Core's proof verifier rejects, and trailing data. It is stricter +//! than Core only for legacy-scheme BLS signature bytes Core would read as +//! null (see `check_signature_encoding`). +use crate::bls::{check_signature_encoding, public_key}; +use crate::wire::{Hash, Reader, Result}; +use crate::Error; + +/// Largest transaction a proof may carry, as in Core. +pub const MAX_TRANSACTION_BYTES: usize = 100_000; +/// Largest serialized quorum commitment, as in Core. +pub(crate) const MAX_COMMITMENT_BYTES: usize = 1024; +/// Core's `MAX_SIZE`, the bound its CompactSize reader enforces. +const MAX_COMPACT_SIZE: u64 = 0x0200_0000; + +const TRANSACTION_COINBASE: u16 = 5; +const TRANSACTION_QUORUM_COMMITMENT: u16 = 6; +/// First coinbase payload version with ChainLock and credit-pool fields. +const CBTX_CLSIG_AND_BALANCE: u16 = 3; +/// First coinbase payload version with `merkleRootAssetUnlocks` (Core v24). +const CBTX_MERKLE_ROOT_ASSET_UNLOCKS: u16 = 4; + +/// The fields of a final quorum commitment a proof needs. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Commitment { + /// LLMQ type. + pub quorum_type: u8, + /// Quorum hash, wire order (the reverse of Platform's and Core RPC's order). + pub quorum_hash: Hash, + /// Aggregated quorum public key, a validated Basic-scheme key. + pub public_key: [u8; 48], +} + +/// Size and signing threshold of each LLMQ type a commitment may have, and +/// whether the type rotates (and so carries a quorum index). +fn quorum_parameters(quorum_type: u8) -> Result<(usize, u32, bool)> { + Ok(match quorum_type { + 1 => (50, 30, false), // LLMQ_50_60 + 2 => (400, 240, false), // LLMQ_400_60 + 3 => (400, 340, false), // LLMQ_400_85 + 4 => (100, 67, false), // LLMQ_100_67 + 5 => (60, 45, true), // LLMQ_60_75 + 6 => (25, 17, false), // LLMQ_25_67 + _ => return Err(Error("unsupported quorum type")), + }) +} + +/// Parse a serialized `CFinalCommitment` of the Basic BLS era (version 3, or +/// 4 for rotated quorums) that is canonical, non-null and signed by at least +/// the quorum's threshold. +pub fn parse_commitment(bytes: &[u8]) -> Result { + if bytes.len() > MAX_COMMITMENT_BYTES { + return Err(Error("commitment size")); + } + let mut r = Reader::new(bytes); + let version = r.u16()?; + if version != 3 && version != 4 { + return Err(Error("unsupported commitment version")); + } + let quorum_type = r.u8()?; + let (size, threshold, rotated) = quorum_parameters(quorum_type)?; + if (version == 4) != rotated { + return Err(Error("commitment rotation version")); + } + let quorum_hash = r.array()?; + // Core reads the index as int16; negative values fail this check too. + if rotated && r.u16()? >= 32 { + return Err(Error("quorum index")); + } + // Signers, then valid members: bit count, then packed bits. + for _ in 0..2 { + if r.compact(MAX_COMPACT_SIZE)? != size { + return Err(Error("quorum bit count")); + } + let packed = r.take(size.div_ceil(8))?; + if size % 8 != 0 && packed[packed.len() - 1] >> (size % 8) != 0 { + return Err(Error("bitset padding")); + } + if packed.iter().map(|b| b.count_ones()).sum::() < threshold { + return Err(Error("null/undersized quorum")); + } + } + let public_key_bytes = r.array()?; + public_key(&public_key_bytes)?; + if quorum_hash == [0; 32] { + return Err(Error("null quorum")); + } + r.take(32)?; // verification vector hash + check_signature_encoding(&r.array()?)?; // quorum signature + check_signature_encoding(&r.array()?)?; // members' signature + r.finish()?; + Ok(Commitment { + quorum_type, + quorum_hash, + public_key: public_key_bytes, + }) +} + +/// Read a special transaction's version and type. Core's `nVersion` is an +/// int16, so 0x8000..=0xFFFF are negative and never special. +fn special_type(r: &mut Reader<'_>) -> Result> { + let version = r.u16()? as i16; + let kind = r.u16()?; + Ok((version >= 3).then_some(kind)) +} + +/// A transaction's inputs, outputs and lock time, as far as the parsers need. +struct Body<'a> { + inputs: usize, + outputs: usize, + /// The first input's prevout and scriptSig. + first_input: Option<(&'a [u8], &'a [u8])>, +} + +/// Read a CompactSize-prefixed byte string, at most a transaction long. +fn var_bytes<'a>(r: &mut Reader<'a>) -> Result<&'a [u8]> { + let len = r.compact(MAX_TRANSACTION_BYTES as u64)?; + r.take(len) +} + +/// Read past a transaction's inputs, outputs and lock time. +fn read_body<'a>(r: &mut Reader<'a>) -> Result> { + let max = MAX_TRANSACTION_BYTES as u64; + let inputs = r.compact(max)?; + let mut first_input = None; + for _ in 0..inputs { + let prevout = r.take(36)?; + let script = var_bytes(r)?; + r.u32()?; // sequence + first_input.get_or_insert((prevout, script)); + } + let outputs = r.compact(max)?; + for _ in 0..outputs { + r.u64()?; // value + var_bytes(r)?; // scriptPubKey + } + r.u32()?; // lock time + Ok(Body { + inputs, + outputs, + first_input, + }) +} + +/// Parse a mined quorum commitment transaction, mined at `height`. +/// +/// Consensus requires only a special version, type 6, and a version 1 +/// payload carrying the mining height and the commitment; its inputs, outputs +/// and lock time are unconstrained, so any shape is accepted. +pub(crate) fn mined_commitment(bytes: &[u8], height: u32) -> Result { + let mut r = Reader::new(bytes); + if special_type(&mut r)? != Some(TRANSACTION_QUORUM_COMMITMENT) { + return Err(Error("quorum transaction envelope")); + } + read_body(&mut r)?; + let payload = var_bytes(&mut r)?; + r.finish()?; + let mut header = Reader::new(payload); + if header.u16()? != 1 || header.u32()? != height { + return Err(Error("quorum payload height/version")); + } + parse_commitment(&payload[6..]) +} + +/// Parse the coinbase of the block at `height` and return its masternode and +/// quorum Merkle roots. +/// +/// Mirrors Core's proof verifier: any special version, type 5, one null-prevout +/// input with a 1..=100 byte scriptSig, at least one output, and a payload of +/// version 3 or later for `height` with a non-null quorum root and a ChainLock +/// height difference below `height`. Version 4 payloads (Core v24) also carry +/// `merkleRootAssetUnlocks`. +pub fn coinbase_roots(bytes: &[u8], height: u32) -> Result<(Hash, Hash)> { + if bytes.len() > MAX_TRANSACTION_BYTES { + return Err(Error("coinbase size")); + } + // Core compares the payload's int32 height. + if height > i32::MAX as u32 { + return Err(Error("coinbase payload version/height")); + } + let mut r = Reader::new(bytes); + if special_type(&mut r)? != Some(TRANSACTION_COINBASE) { + return Err(Error("coinbase transaction type/version")); + } + let body = read_body(&mut r)?; + let null_prevout = |prevout: &[u8]| prevout[..32] == [0; 32] && prevout[32..] == [0xff; 4]; + match body.first_input { + Some((prevout, script)) + if body.inputs == 1 && null_prevout(prevout) && (1..=100).contains(&script.len()) => {} + _ => return Err(Error("coinbase input")), + } + if body.outputs == 0 { + return Err(Error("coinbase outputs")); + } + let mut payload = Reader::new(var_bytes(&mut r)?); + r.finish()?; + + let version = payload.u16()?; + if version < CBTX_CLSIG_AND_BALANCE || payload.u32()? != height { + return Err(Error("coinbase payload version/height")); + } + let masternode_root = payload.array()?; + let quorum_root: Hash = payload.array()?; + if quorum_root == [0; 32] { + return Err(Error("coinbase quorum root")); + } + if payload.compact(MAX_COMPACT_SIZE)? >= height as usize { + return Err(Error("coinbase ChainLock height")); + } + check_signature_encoding(&payload.array()?)?; // best ChainLock signature + payload.u64()?; // credit pool balance + if version >= CBTX_MERKLE_ROOT_ASSET_UNLOCKS { + payload.take(32)?; + } + payload.finish()?; + Ok((masternode_root, quorum_root)) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::chain::QuorumProofChain; + + /// The testnet fixture's chain, from a real `getquorumproofchain` call. + fn chain() -> QuorumProofChain { + let value: serde_json::Value = + serde_json::from_str(include_str!("../tests/data/testnet.json")).unwrap(); + let proof = hex::decode(value["proof_hex"].as_str().unwrap()).unwrap(); + QuorumProofChain::decode(&proof).unwrap() + } + + fn compact(out: &mut Vec, n: usize) { + match n { + 0..=252 => out.push(n as u8), + 253..=0xffff => { + out.push(253); + out.extend((n as u16).to_le_bytes()); + } + _ => unreachable!("test helper encodes at most u16::MAX"), + } + } + + /// A transaction's parts, to re-serialize it with changes. + struct Parts { + input: Vec, + script: Vec, + sequence: u32, + lock_time: u32, + payload: Vec, + } + + /// Split a transaction with exactly one input (any number of outputs). + fn split(tx: &[u8]) -> Parts { + let mut r = Reader::new(tx); + r.take(4).unwrap(); + assert_eq!(r.compact(1).unwrap(), 1); + let input = r.take(36).unwrap().to_vec(); + let n = r.compact(100).unwrap(); + let script = r.take(n).unwrap().to_vec(); + let sequence = r.u32().unwrap(); + for _ in 0..r.compact(100_000).unwrap() { + r.u64().unwrap(); + let n = r.compact(100_000).unwrap(); + r.take(n).unwrap(); + } + let lock_time = r.u32().unwrap(); + let n = r.compact(100_000).unwrap(); + let payload = r.take(n).unwrap().to_vec(); + r.finish().unwrap(); + Parts { + input, + script, + sequence, + lock_time, + payload, + } + } + + fn coinbase(parts: &Parts, version: u16, outputs: usize, payload: &[u8]) -> Vec { + let mut tx = Vec::new(); + tx.extend(version.to_le_bytes()); + tx.extend(TRANSACTION_COINBASE.to_le_bytes()); + compact(&mut tx, 1); + tx.extend(&parts.input); + compact(&mut tx, parts.script.len()); + tx.extend(&parts.script); + tx.extend(parts.sequence.to_le_bytes()); + compact(&mut tx, outputs); + for _ in 0..outputs { + tx.extend(0u64.to_le_bytes()); + compact(&mut tx, 0); + } + tx.extend(parts.lock_time.to_le_bytes()); + compact(&mut tx, payload.len()); + tx.extend(payload); + tx + } + + #[test] + fn should_read_v24_coinbase_payloads_with_the_asset_unlock_root() { + let chain = chain(); + let height = chain.target.height; + let expected = coinbase_roots(&chain.coinbase.transaction, height).unwrap(); + let parts = split(&chain.coinbase.transaction); + assert_eq!(parts.payload[..2], CBTX_CLSIG_AND_BALANCE.to_le_bytes()); + + let mut v4 = parts.payload.clone(); + v4[..2].copy_from_slice(&CBTX_MERKLE_ROOT_ASSET_UNLOCKS.to_le_bytes()); + v4.extend([0x5a; 32]); + assert_eq!( + coinbase_roots(&coinbase(&parts, 3, 1, &v4), height), + Ok(expected) + ); + + // A v3 payload followed by 32 bytes, and a v4 payload without them, + // are not what Core deserializes. + let mut v3_long = parts.payload.clone(); + v3_long.extend([0x5a; 32]); + assert_eq!( + coinbase_roots(&coinbase(&parts, 3, 1, &v3_long), height), + Err(Error("trailing bytes")) + ); + let mut v4_short = parts.payload.clone(); + v4_short[..2].copy_from_slice(&CBTX_MERKLE_ROOT_ASSET_UNLOCKS.to_le_bytes()); + assert_eq!( + coinbase_roots(&coinbase(&parts, 3, 1, &v4_short), height), + Err(Error("truncated input")) + ); + } + + #[test] + fn should_accept_every_coinbase_shape_consensus_allows() { + let chain = chain(); + let height = chain.target.height; + let expected = coinbase_roots(&chain.coinbase.transaction, height).unwrap(); + let parts = split(&chain.coinbase.transaction); + // Any special version, and more than 4,096 outputs. + let wide = coinbase(&parts, 4, 4097, &parts.payload); + assert_eq!(coinbase_roots(&wide, height), Ok(expected)); + + let rejected = |tx: &[u8]| coinbase_roots(tx, height).unwrap_err(); + assert_eq!( + rejected(&coinbase(&parts, 2, 1, &parts.payload)), + Error("coinbase transaction type/version") + ); + assert_eq!( + rejected(&coinbase(&parts, 0xfffe, 1, &parts.payload)), + Error("coinbase transaction type/version") + ); + assert_eq!( + rejected(&coinbase(&parts, 3, 0, &parts.payload)), + Error("coinbase outputs") + ); + assert_eq!( + coinbase_roots(&coinbase(&parts, 3, 1, &parts.payload), height + 1).unwrap_err(), + Error("coinbase payload version/height") + ); + let mut empty_script = Parts { + script: Vec::new(), + ..split(&chain.coinbase.transaction) + }; + assert_eq!( + rejected(&coinbase(&empty_script, 3, 1, &parts.payload)), + Error("coinbase input") + ); + empty_script.script = vec![0; 101]; + assert_eq!( + rejected(&coinbase(&empty_script, 3, 1, &parts.payload)), + Error("coinbase input") + ); + } + + #[test] + fn should_accept_every_commitment_transaction_shape_consensus_allows() { + let chain = chain(); + let link = &chain.links[0]; + let height = link.certificate.height - link.ancestors.len() as u32; + let tx = &link.mining.transaction; + let expected = mined_commitment(tx, height).unwrap(); + // Core mines commitments with no inputs or outputs. + assert_eq!(tx[4..6], [0, 0]); + let payload = { + let mut r = Reader::new(&tx[10..]); + let n = r.compact(100_000).unwrap(); + r.take(n).unwrap().to_vec() + }; + + let build = |version: u16, kind: u16| { + let mut tx = Vec::new(); + tx.extend(version.to_le_bytes()); + tx.extend(kind.to_le_bytes()); + compact(&mut tx, 1); + tx.extend([0xab; 32]); + tx.extend(1u32.to_le_bytes()); + compact(&mut tx, 3); + tx.extend([0x51, 0x52, 0x53]); + tx.extend(0xffff_fffeu32.to_le_bytes()); + compact(&mut tx, 2); + for _ in 0..2 { + tx.extend(1_000u64.to_le_bytes()); + compact(&mut tx, 1); + tx.push(0x6a); + } + tx.extend(7u32.to_le_bytes()); + compact(&mut tx, payload.len()); + tx.extend(&payload); + tx + }; + // Inputs, outputs and lock time are free; any special version works. + assert_eq!(mined_commitment(&build(4, 6), height), Ok(expected)); + assert_eq!( + mined_commitment(&build(2, 6), height), + Err(Error("quorum transaction envelope")) + ); + assert_eq!( + mined_commitment(&build(3, 5), height), + Err(Error("quorum transaction envelope")) + ); + assert_eq!( + mined_commitment(&build(3, 6), height + 1), + Err(Error("quorum payload height/version")) + ); + } + + #[test] + fn should_reject_noncanonical_commitments() { + let chain = chain(); + let seed = &chain.seed; + assert!(parse_commitment(seed).is_ok()); + let mut trailing = seed.clone(); + trailing.push(0); + assert_eq!(parse_commitment(&trailing), Err(Error("trailing bytes"))); + // The quorum signature is the second-to-last 96 bytes. + let mut bad_signature = seed.clone(); + let at = bad_signature.len() - 192; + bad_signature[at..at + 96].fill(0xff); + assert_eq!( + parse_commitment(&bad_signature), + Err(Error("BLS signature encoding")) + ); + let mut null_signature = seed.clone(); + null_signature[at..at + 96].fill(0); + assert!(parse_commitment(&null_signature).is_ok()); + let mut wrong_version = seed.clone(); + wrong_version[0] = 2; + assert_eq!( + parse_commitment(&wrong_version), + Err(Error("unsupported commitment version")) + ); + } +} diff --git a/packages/rs-core-proof/src/wire.rs b/packages/rs-core-proof/src/wire.rs new file mode 100644 index 00000000000..0b8c913feb4 --- /dev/null +++ b/packages/rs-core-proof/src/wire.rs @@ -0,0 +1,142 @@ +//! Bounded reading of Core's wire encoding. Hashes are 32 bytes in wire order, +//! the reverse of the order Core's RPC displays them in. +use crate::Error; +use sha2::{Digest, Sha256}; + +/// A double-SHA256 hash in wire byte order. +pub type Hash = [u8; 32]; + +/// Result of every check in this crate. +pub type Result = core::result::Result; + +/// Core's double SHA-256. +pub fn sha256d(bytes: &[u8]) -> Hash { + Sha256::digest(Sha256::digest(bytes)).into() +} + +pub(crate) struct Reader<'a> { + bytes: &'a [u8], + pos: usize, +} + +impl<'a> Reader<'a> { + pub(crate) fn new(bytes: &'a [u8]) -> Self { + Self { bytes, pos: 0 } + } + + pub(crate) fn take(&mut self, len: usize) -> Result<&'a [u8]> { + let end = self.pos.checked_add(len).ok_or(Error("length overflow"))?; + let bytes = self + .bytes + .get(self.pos..end) + .ok_or(Error("truncated input"))?; + self.pos = end; + Ok(bytes) + } + + pub(crate) fn array(&mut self) -> Result<[u8; N]> { + self.take(N)? + .try_into() + .map_err(|_| Error("truncated input")) + } + + pub(crate) fn u8(&mut self) -> Result { + Ok(self.array::<1>()?[0]) + } + + pub(crate) fn u16(&mut self) -> Result { + Ok(u16::from_le_bytes(self.array()?)) + } + + pub(crate) fn u32(&mut self) -> Result { + Ok(u32::from_le_bytes(self.array()?)) + } + + pub(crate) fn u64(&mut self) -> Result { + Ok(u64::from_le_bytes(self.array()?)) + } + + pub(crate) fn finish(self) -> Result<()> { + if self.pos == self.bytes.len() { + Ok(()) + } else { + Err(Error("trailing bytes")) + } + } + + /// Core's CompactSize: canonical encoding only, and at most `max`. + pub(crate) fn compact(&mut self, max: u64) -> Result { + let n = match self.u8()? { + 253 => { + let n = u64::from(self.u16()?); + if n < 253 { + return Err(Error("noncanonical CompactSize")); + } + n + } + 254 => { + let n = u64::from(self.u32()?); + if n <= 0xffff { + return Err(Error("noncanonical CompactSize")); + } + n + } + 255 => { + let n = self.u64()?; + if n <= 0xffff_ffff { + return Err(Error("noncanonical CompactSize")); + } + n + } + n => u64::from(n), + }; + if n > max { + return Err(Error("CompactSize limit")); + } + usize::try_from(n).map_err(|_| Error("length overflow")) + } + + /// A blob with a 32-bit little-endian length prefix, at most `max` bytes. + pub(crate) fn blob(&mut self, max: usize) -> Result<&'a [u8]> { + let n = self.u32()? as usize; + if n > max { + return Err(Error("blob limit")); + } + self.take(n) + } +} + +pub(crate) fn write_blob(out: &mut Vec, bytes: &[u8]) -> Result<()> { + let n = u32::try_from(bytes.len()).map_err(|_| Error("blob limit"))?; + out.extend(n.to_le_bytes()); + out.extend(bytes); + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn should_reject_noncanonical_and_oversized_compact_sizes() { + assert_eq!(Reader::new(&[252]).compact(252), Ok(252)); + assert_eq!(Reader::new(&[253, 253, 0]).compact(253), Ok(253)); + assert!(Reader::new(&[253, 252, 0]).compact(u64::MAX).is_err()); + assert!(Reader::new(&[254, 0xff, 0xff, 0, 0]) + .compact(u64::MAX) + .is_err()); + assert!(Reader::new(&[255, 0xff, 0xff, 0xff, 0xff, 0, 0, 0, 0]) + .compact(u64::MAX) + .is_err()); + assert!(Reader::new(&[253, 253, 0]).compact(252).is_err()); + } + + #[test] + fn should_reject_truncated_blobs_and_trailing_bytes() { + assert!(Reader::new(&[2, 0, 0, 0, 1]).blob(16).is_err()); + assert!(Reader::new(&[2, 0, 0, 0, 1, 2]).blob(1).is_err()); + let mut reader = Reader::new(&[1, 0, 0, 0, 7, 8]); + assert_eq!(reader.blob(16), Ok(&[7u8][..])); + assert!(reader.finish().is_err()); + } +} diff --git a/packages/rs-core-proof/tests/data/mainnet.json b/packages/rs-core-proof/tests/data/mainnet.json new file mode 100644 index 00000000000..781fa108839 --- /dev/null +++ b/packages/rs-core-proof/tests/data/mainnet.json @@ -0,0 +1,18 @@ +{ + "checkpoint": { + "network": 0, + "height": 2557564, + "block_hash": "0000000000000016cab4d83ebe0b97aa4c185a54e3e3b1b71d81089b42a25084", + "masternode_root": "7408f665b08384f080708f1f1d63f619856525ec2e774a582a25a3de87ee1b59", + "quorum_root": "1961ef5b35e9f918419758ede42f7ecca013b30777741b185b35a3519b7c65ee" + }, + "proof_hex": "444153484e433032007c0627008450a2429b08811db7b1e3e3545a184caa970bbe3ed8b4ca1600000000000000591bee87dea3252a584a772eec25658519f6631d1f8f7080f08483b065f60874ee657c9b51a3355b181b747707b313a0cc7e2fe4ed58974118f9e9355bef61199d010000030002d923a53e111baa0c675393690cf8fadaadf9c10e990f1f6f0400000000000000fd9001ff7ffffffffeff7fffdffffffefffffffffffffffffffffffffefffbf6defffffffffffffffffffffffffffffffffdfffffffd9001fffffffffffffffffffffffffffffffffffffffffffffffffffffffffeffffffffffffffffffffffffffffffffffffffffff880f756820607f3f6dbf0921045583eb6db631842f834b17880242f4013b46c3fd6ce7a9f72b2f790a07c23306d0452fd489e53ae3bdce8ac6092a028b039e8efbb647b503297383a4f284ff9d578968915fddc3f719edb0f5bd23a6954084d335f2f98a1de3574ffb8aad38be4ccd2f4e36af901b366a177ca5b32cb0c506ee04affca3b38371e3fd48d7fe965d238e3bdfba27ae7abd9133e3762f559eddd02ebaf29f43ac34a38f84a07346ba9840a320309e24d4c488e4d976fa551d081018b283a288e4a64854ef715b343bfe5b9399625c33e6f35b61967bf676341e4b0f66db9c247f89df6b15c2a07b51627ffa7787c55fed99891d723c2236c243c5d8fa3a94f2a772b9073c16b9c36633b4450000005800000007d2c7a47f66b8a11f462c52ee54df1a43b79721023fd435600946f20b4fd45efa0bc20a5ac8942f2d479c17f169e0f0effa1bb5f14c19339a9f43d3fd2f143b27629b8ef72a511f1c28c5aba1266c90312d034850dae60ae54ce540b8c8709c85141dc7ff8c020361437e9f251c5d13796c9d69cd87d3144a8bdce4d14371621439fa2c65c4142bdf0d20f11d02e95837591ef8a7f2618d2cef6919a6d6e2f7b4d5e8cd8a477a166fcfd1e348a13f15204e668acfbd3f925ece8f25da284e4f9ac54f580e5a7f86fa55318850e3927b9bd9d693046506bbc4bc49e6bb0d4acb9702003507270000000020b0bc2fe6eaa443d6b12462cef9b23696a0f645a77e34bb3a260000000000000044e937cdae9e2372cdaf59ad090f376c3325ae5e3fa99a37365f1ccad950cfd09ab9d56a0d5d3519ea1c6d1ea19bd1937caa70ec8b51f40909e16efa31d19562cbaa15632488b59436e4a6b495c24e2d9453d0ff044a43921809143d0273c4d4799d0463e47b652b8e16a7ef13c306c61b2d738ad59b22372952475b32145e99dd00da1f9e9474bea29d5e01b001000003000600000000000000fda301010035072700030002d84784d6a005526733639c02104a316f4397df6a2124f8803100000000000000fd9001ffffbfffffe7ffffffffffefeffffffffff7fffffbffffffffffffffffffffff79fff9ffffffffffffffbffffffdffffffdffd9001ffffffffffefffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff91f425e7e7465f13504d6257068fddb843eba17ffe4502f1d318668b086ba4227ec3b19d6cdd8fa0dce22a6103f4236be787023398d5dcbd4bac6822f2bf35828067c53f6b90bcb7f01f7280c882d8fa92d7c87c5d40c7c5047d6e6aa64ef24f24f90d48774b79f300bc39a2cd2d4a97ca2926720fd3c903008d1c88b5ca4d4a160a8e8afd9eafac5766481d1ebbeb0773d0f8b7ce7c1bdeb2dc799023d50f877cae8115bbc98342ab8b7d18f7d4e47e966c2b340d71bebec4e2903f762510a47afc333ca47183bf136eca5da05f047f487c54da0818b5a7dbe71df7de798758101912801d1a3fe485645cee567e15dcf8ade6e9bc0bc108555160b3356d8c05ed349ec3d7bcda8390d82a89b468e91e010000000f000000045100c6cefdfad63ae0381043d71b226fc5dac30ba73f9c308c562ac59ee6023a2c5021731cd0ea6f077e31f3d40a6fb5e105432c0935300d40fdce2c5e5bc666b183bcac2cf430e7b808f8cf9901f7cdd458285daf10f3e96179437c5a753315eebb053ae09bb4013e2d126cc9e030203060f504e97dff6feccd609a388e74fd0000b50b2700000000204977537a3c2aab47dc5f765b38aa2eab4ebe419009f77f8f0300000000000000a95ababc8c49b5ce030bfb6e8e0fe063f2beaeacc84d3d0a1f95da1eb793a4115279d86a54951219083b259b8013a228dc9d9971874680967afc828d25a92d82e7a06559d1d0bcf888cced6af995c54e52eec0cf3f5e55ec50b9e1190ea400999cd874ae0547c54e9b09e4787537ab255a913dca120db300139868558f0bfdba1ea5779d79153b83c8f055cdb001000003000600000000000000fda3010100b50b27000300024f53faafb819a9d6b8348b3ff4c7b025bc9f4d02ea4ba1ab2900000000000000fd9001effffffbff7fffffffffffedff7ffffffffffffff7fffffffffffffffffffffffffffffffffffffffdfdffffffffffdffffffd9001ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffa9fcfab24895d15f57819734dfa331fbd22ea19cb3017ee7a47a576e7ae2ea1b57777846d72e6f1a20baba62701342231effc0d73e1850da0c3bd605856000f85fac323a5b3afc70131e5a06a9137523aa18b388492cc61a1b340e0711175562fbcceeb2196ee535bec72f683928b918f285c6e70716beb6820bd287d167c5750498eaddd323a33c0ece707d3a0bce8d6552d90c570867a51625cf9eb6e63a083914ea026cd30d4562460ef3f838d9fc94ef40870fb35c37e78384effadf1f59544c8d73fc698c637a7ececa9aa3c94e913ce51dcc952a1c0809f3ae06bdfd6d02b879455a7b053a341a5a1d22c925a37e68d7775ee67954f6a723e22f2b0f43215dd011d9a4f2857531b9cf20cb9cf6010000001e0000000585729fb23c80a52d14980febf8ff6c7764b10bdf023e80701f9b6550159d0f116ed5765aa56d1071f844485533f8707ed166893a9f663f0e26c0abd64659c6b77b625a7f0f4fb1ac9c809b10bc3d7105b914b391ef9094f01547a927bb3c5f6405f107e6f0e8489f4236aa94ffb01e43495ca7a810700958caec3c06c177cd2f2dd10fe47d7f78d9c1be81c7c4fd3be60b9bcc6d44aeaffd2361bcfda52e8a580000580c27000000002090fe54ef03175d17f065d081fc32060331b4eec2402e3a1c090000000000000012a0a0bbbb0d4a7644cfbda0231ca084261bf23d4997f08f6120a13a6d3d2446f0e0d86a0d161c19461a068dad236cbb9d9d85ba09013e44a62395a51ffe01b4af2726d4d6fcf250c153a7723fe78bb3a6085b9e1647502e464b68b109440b7e041439a039e73848c31b8fb4e717006598fe07ea9a7dc844cb01c322f2d820697ab21ba38220d0e80f38b2b45801000003000500010000000000000000000000000000000000000000000000000000000000000000ffffffff2703580c2704f0e0d86a0800004e00815108000fe4b883e5bda9e7a59ee4bb99e9b1bc04f09f909f004e81b80300ad7302000000001976a91464d4d4127bbce4b888bb4af080fa4de863b02b3f88ac1b6fc10200000000016ae4979904000000001976a914df08e5b0877bc3bf686e92c5fb21efd03b52f30288acb0ecb825af0300580c27006a583e3f51328edb8d225ba017800e67eb90023a172d809f2c50ffa45232d4ea5062daf169d830c39ec0e0187f517acb62d2031aae9639e4ce5f6e76e6fcb35200b7a957a2e6b00349712f203621a86f49fdcbae576eaf477c0d014c936bdb2cbced25a351a74ff27ea4b81626475b1505072c26cc4aff5f80a1701bb0b5b71474c8acdef833400843302923349dcd9798ffcd800665c0dc52ae74b28ac644b927774c828f57030000000000004600000007a4667324472ddc99d9416ad6ce53ea409c8b3a585a2edd4a3a33a5288d4b7d247b811bdc6ac8aed651f4d23b69ddb60696050ba50d65b523093bc170501cc69f0d702252a228be032e60fb25a498c97c0dda6556cc4ceb996a67ae50bd529737743469aeafd5665857625f280cd00227dde497e137da0a68d578cf2ab7133f36a365afbbb2a7deb9837044a09f420444d488c600faa289e7c78b72f2f58359b22e47e02e1b8313f02f587469ca73e63e32780004dd709eaf3c5b361a3538def1721ceac9e3a5fedd45622d2cadfef2eca71acee5d1d81e44fc9d52619df82fd3", + "bootstrap_hex": "f80b0000444153484e433032007c0627008450a2429b08811db7b1e3e3545a184caa970bbe3ed8b4ca1600000000000000591bee87dea3252a584a772eec25658519f6631d1f8f7080f08483b065f60874ee657c9b51a3355b181b747707b313a0cc7e2fe4ed58974118f9e9355bef61199d010000030002d923a53e111baa0c675393690cf8fadaadf9c10e990f1f6f0400000000000000fd9001ff7ffffffffeff7fffdffffffefffffffffffffffffffffffffefffbf6defffffffffffffffffffffffffffffffffdfffffffd9001fffffffffffffffffffffffffffffffffffffffffffffffffffffffffeffffffffffffffffffffffffffffffffffffffffff880f756820607f3f6dbf0921045583eb6db631842f834b17880242f4013b46c3fd6ce7a9f72b2f790a07c23306d0452fd489e53ae3bdce8ac6092a028b039e8efbb647b503297383a4f284ff9d578968915fddc3f719edb0f5bd23a6954084d335f2f98a1de3574ffb8aad38be4ccd2f4e36af901b366a177ca5b32cb0c506ee04affca3b38371e3fd48d7fe965d238e3bdfba27ae7abd9133e3762f559eddd02ebaf29f43ac34a38f84a07346ba9840a320309e24d4c488e4d976fa551d081018b283a288e4a64854ef715b343bfe5b9399625c33e6f35b61967bf676341e4b0f66db9c247f89df6b15c2a07b51627ffa7787c55fed99891d723c2236c243c5d8fa3a94f2a772b9073c16b9c36633b4450000005800000007d2c7a47f66b8a11f462c52ee54df1a43b79721023fd435600946f20b4fd45efa0bc20a5ac8942f2d479c17f169e0f0effa1bb5f14c19339a9f43d3fd2f143b27629b8ef72a511f1c28c5aba1266c90312d034850dae60ae54ce540b8c8709c85141dc7ff8c020361437e9f251c5d13796c9d69cd87d3144a8bdce4d14371621439fa2c65c4142bdf0d20f11d02e95837591ef8a7f2618d2cef6919a6d6e2f7b4d5e8cd8a477a166fcfd1e348a13f15204e668acfbd3f925ece8f25da284e4f9ac54f580e5a7f86fa55318850e3927b9bd9d693046506bbc4bc49e6bb0d4acb9702003507270000000020b0bc2fe6eaa443d6b12462cef9b23696a0f645a77e34bb3a260000000000000044e937cdae9e2372cdaf59ad090f376c3325ae5e3fa99a37365f1ccad950cfd09ab9d56a0d5d3519ea1c6d1ea19bd1937caa70ec8b51f40909e16efa31d19562cbaa15632488b59436e4a6b495c24e2d9453d0ff044a43921809143d0273c4d4799d0463e47b652b8e16a7ef13c306c61b2d738ad59b22372952475b32145e99dd00da1f9e9474bea29d5e01b001000003000600000000000000fda301010035072700030002d84784d6a005526733639c02104a316f4397df6a2124f8803100000000000000fd9001ffffbfffffe7ffffffffffefeffffffffff7fffffbffffffffffffffffffffff79fff9ffffffffffffffbffffffdffffffdffd9001ffffffffffefffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff91f425e7e7465f13504d6257068fddb843eba17ffe4502f1d318668b086ba4227ec3b19d6cdd8fa0dce22a6103f4236be787023398d5dcbd4bac6822f2bf35828067c53f6b90bcb7f01f7280c882d8fa92d7c87c5d40c7c5047d6e6aa64ef24f24f90d48774b79f300bc39a2cd2d4a97ca2926720fd3c903008d1c88b5ca4d4a160a8e8afd9eafac5766481d1ebbeb0773d0f8b7ce7c1bdeb2dc799023d50f877cae8115bbc98342ab8b7d18f7d4e47e966c2b340d71bebec4e2903f762510a47afc333ca47183bf136eca5da05f047f487c54da0818b5a7dbe71df7de798758101912801d1a3fe485645cee567e15dcf8ade6e9bc0bc108555160b3356d8c05ed349ec3d7bcda8390d82a89b468e91e010000000f000000045100c6cefdfad63ae0381043d71b226fc5dac30ba73f9c308c562ac59ee6023a2c5021731cd0ea6f077e31f3d40a6fb5e105432c0935300d40fdce2c5e5bc666b183bcac2cf430e7b808f8cf9901f7cdd458285daf10f3e96179437c5a753315eebb053ae09bb4013e2d126cc9e030203060f504e97dff6feccd609a388e74fd0000b50b2700000000204977537a3c2aab47dc5f765b38aa2eab4ebe419009f77f8f0300000000000000a95ababc8c49b5ce030bfb6e8e0fe063f2beaeacc84d3d0a1f95da1eb793a4115279d86a54951219083b259b8013a228dc9d9971874680967afc828d25a92d82e7a06559d1d0bcf888cced6af995c54e52eec0cf3f5e55ec50b9e1190ea400999cd874ae0547c54e9b09e4787537ab255a913dca120db300139868558f0bfdba1ea5779d79153b83c8f055cdb001000003000600000000000000fda3010100b50b27000300024f53faafb819a9d6b8348b3ff4c7b025bc9f4d02ea4ba1ab2900000000000000fd9001effffffbff7fffffffffffedff7ffffffffffffff7fffffffffffffffffffffffffffffffffffffffdfdffffffffffdffffffd9001ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffa9fcfab24895d15f57819734dfa331fbd22ea19cb3017ee7a47a576e7ae2ea1b57777846d72e6f1a20baba62701342231effc0d73e1850da0c3bd605856000f85fac323a5b3afc70131e5a06a9137523aa18b388492cc61a1b340e0711175562fbcceeb2196ee535bec72f683928b918f285c6e70716beb6820bd287d167c5750498eaddd323a33c0ece707d3a0bce8d6552d90c570867a51625cf9eb6e63a083914ea026cd30d4562460ef3f838d9fc94ef40870fb35c37e78384effadf1f59544c8d73fc698c637a7ececa9aa3c94e913ce51dcc952a1c0809f3ae06bdfd6d02b879455a7b053a341a5a1d22c925a37e68d7775ee67954f6a723e22f2b0f43215dd011d9a4f2857531b9cf20cb9cf6010000001e0000000585729fb23c80a52d14980febf8ff6c7764b10bdf023e80701f9b6550159d0f116ed5765aa56d1071f844485533f8707ed166893a9f663f0e26c0abd64659c6b77b625a7f0f4fb1ac9c809b10bc3d7105b914b391ef9094f01547a927bb3c5f6405f107e6f0e8489f4236aa94ffb01e43495ca7a810700958caec3c06c177cd2f2dd10fe47d7f78d9c1be81c7c4fd3be60b9bcc6d44aeaffd2361bcfda52e8a580000580c27000000002090fe54ef03175d17f065d081fc32060331b4eec2402e3a1c090000000000000012a0a0bbbb0d4a7644cfbda0231ca084261bf23d4997f08f6120a13a6d3d2446f0e0d86a0d161c19461a068dad236cbb9d9d85ba09013e44a62395a51ffe01b4af2726d4d6fcf250c153a7723fe78bb3a6085b9e1647502e464b68b109440b7e041439a039e73848c31b8fb4e717006598fe07ea9a7dc844cb01c322f2d820697ab21ba38220d0e80f38b2b45801000003000500010000000000000000000000000000000000000000000000000000000000000000ffffffff2703580c2704f0e0d86a0800004e00815108000fe4b883e5bda9e7a59ee4bb99e9b1bc04f09f909f004e81b80300ad7302000000001976a91464d4d4127bbce4b888bb4af080fa4de863b02b3f88ac1b6fc10200000000016ae4979904000000001976a914df08e5b0877bc3bf686e92c5fb21efd03b52f30288acb0ecb825af0300580c27006a583e3f51328edb8d225ba017800e67eb90023a172d809f2c50ffa45232d4ea5062daf169d830c39ec0e0187f517acb62d2031aae9639e4ce5f6e76e6fcb35200b7a957a2e6b00349712f203621a86f49fdcbae576eaf477c0d014c936bdb2cbced25a351a74ff27ea4b81626475b1505072c26cc4aff5f80a1701bb0b5b71474c8acdef833400843302923349dcd9798ffcd800665c0dc52ae74b28ac644b927774c828f57030000000000004600000007a4667324472ddc99d9416ad6ce53ea409c8b3a585a2edd4a3a33a5288d4b7d247b811bdc6ac8aed651f4d23b69ddb60696050ba50d65b523093bc170501cc69f0d702252a228be032e60fb25a498c97c0dda6556cc4ceb996a67ae50bd529737743469aeafd5665857625f280cd00227dde497e137da0a68d578cf2ab7133f36a365afbbb2a7deb9837044a09f420444d488c600faa289e7c78b72f2f58359b22e47e02e1b8313f02f587469ca73e63e32780004dd709eaf3c5b361a3538def1721ceac9e3a5fedd45622d2cadfef2eca71acee5d1d81e44fc9d52619df82fd303004f01000003000479b9eae40aed5dd7e67e50d19bc510afd5fd45765b1534321e0000000000000064ffffffffffffffffffffffff0f64ffffffffffffffffffffffff0f8445e70c9d22d2a3f174091ece2e7a9bfadab486562c7ae5266c4e3266e3d0da0c875b089558d239662b6cdde1ca4415c432b35c68ffe88a6238529dddabba0f9f4c749db4570623688160b6a0b6facc87eea4095b32faecafcd7da68b9195ea213933aa976febb4cd5286ee20faacfe412f350d9d7b398f120f03a0784d439415fb2afb1e086da59bacb04daf9256fbf02b59b9f59280f9942c323b89eed607af9a0775be153e169381c9dbbb8072978392e1d283ba41fa42ee5e7499517323d5b9bbfc816d7b836d3a54d1826e68a121c9958d81d242a3d5fd54c87ba4ab1c19f6be9e8627468c53732fbec137a9c872821efb4ca144cfe4c1a154ac9e653d12f6b382fd23cf3863b9e11ea73a2e2d0c000000580000000740617cb0095a2b6a6f2db5c14ddf4850244f905d25093567732404c38a84349ec5d83501f757cfa9eb70ca5ef33655ce297f03bae755df956421f18e58b6589823de39ac37c6377bacb54fb4a7cd27eff5ac1841e7ba401dcb861ac268c4a5ecf254c70de0e22b565b147e783817c0f12551b6daa9e3552a7c2154a27a4ac621bb1adfc61baa5d03ad5792462ed456675f72a128f733ac6e5da9d85003eb2b1ef55e3c13e0d72d72efa6419987a9548c4f3e66da0d9eaa69e5c979d37fb59ccc71ce9268f2c3458a9192d3c1faa1ef261eafd5a25a1c3dfadbe7f31d11f4abee01af00000001b2dde1b42a20d00d0f860f34e76e7484c703fcc941d8efac00b4c034fb79ea742f4688978c2f4721388e39a5fd476d59ef506ac0e8ec6f200000000000000000000000000000000000ffffadd4e85a270fa2ea825c2b13c6c1ed98941e701627a260ffa92fc5528e0f16427d73a2b129668172ce387fb38af9b5b3f6947cab6cef9eb8167d9cb1b6a6cf3eefebea75a38c8ac951a6010100bb01d9ac437d0b83c5251b4b096ae6fd97d81efff74a11000000c60b00000c07c171fa22a893014fa5d3fc4cf0c507affb40353d1c71bd563bfdeb8ab99867cc441bf36b87028e48ac7f0f15538384eea71d115bf7678efcd0ab078307988d855282369662ed7a3c6b85a7e889d0f13a48aac5b57a54e6b8574745977ba98db2e7325b617c833a0ef6ffd5687ff1fa028779668656c7a70316de6de3d8c7fe866b1be43fdf41dc7c2ce3ce6930391464dfc174f9795dab87db3cc18bd9344b0eff61c696da314aa7707453a2edf5189794c1a5775867975beac0e26fe4579437dec36ea6863e42103cee0a10d8fee182104d374ab915d3a9d6a504e060150a6aa5cec14ee36470051a58066ac081236e4d169b6236997301721fcffa11446ead3a3e4f39550e8131438dfe55f614893f61609813205860f09dea4531636400da00c5fcd658962006ea9ea7dacfd94690fadad9d233b9321344b944f73f871448feed702bc4daa1d08f18ecafaef078716029fcbd3afb004a07ce45013878550aea84f9dcdde6bb7617549ea2fb5782ab60c6da0552bea95ded3f70c8c7168401af000000021fb986e0ee3134ed19cba704f04b2479c0aa3bc05e09a994c667d1d2df5dcbe58f25eab055b62e1a39b86073db371d37de0adc1790c3700f0000000000000000000000000000000000ffff22d2ee43270f875cb8c7bcaabebdc4ee34cff249808761d95a3f8a0298df56c35324d8a07f44b163158093d9d005ef3680ac352524b7403227f73fd804b7e4d657912f7bcc496c79fa54010100bb018cb97997a418f4814a63d3564b9574a39343796816000000c60b00000c25d718d7ffa7622cc0d1185fe7ddf3c665096ec7b26bcc8aae32094b4cb39cdd2f5c6af3f104e93f1dded2e6dd9aee5a270862e42578846053c42da3525307a2fe39ea427e0a39abc3e434f4bb9f0a3a1abd7d2c373d476b4b3ac79f06152c44b2e7325b617c833a0ef6ffd5687ff1fa028779668656c7a70316de6de3d8c7fe866b1be43fdf41dc7c2ce3ce6930391464dfc174f9795dab87db3cc18bd9344b0eff61c696da314aa7707453a2edf5189794c1a5775867975beac0e26fe4579437dec36ea6863e42103cee0a10d8fee182104d374ab915d3a9d6a504e060150a6aa5cec14ee36470051a58066ac081236e4d169b6236997301721fcffa11446ead3a3e4f39550e8131438dfe55f614893f61609813205860f09dea4531636400da00c5fcd658962006ea9ea7dacfd94690fadad9d233b9321344b944f73f871448feed702bc4daa1d08f18ecafaef078716029fcbd3afb004a07ce45013878550aea84f9dcdde6bb7617549ea2fb5782ab60c6da0552bea95ded3f70c8c71684", + "target": { + "network": 0, + "height": 2559064, + "block_hash": "0000000000000010f0455b6c18e43218b6a5af379f6b5c25e84970943d774d23", + "masternode_root": "ead43252a4ff502c9f802d173a0290eb670e8017a05b228ddb8e32513f3e586a", + "quorum_root": "52b3fce6766e5fcee43996ae1a03d262cb7a517f18e0c09ec330d869f1da6250" + } +} diff --git a/packages/rs-core-proof/tests/data/testnet-no-links.json b/packages/rs-core-proof/tests/data/testnet-no-links.json new file mode 100644 index 00000000000..ea28d635005 --- /dev/null +++ b/packages/rs-core-proof/tests/data/testnet-no-links.json @@ -0,0 +1,18 @@ +{ + "checkpoint": { + "network": 1, + "height": 1576616, + "block_hash": "0000000d3730f7d79f266391a3404636ae7d53e9ac1535ee629dac1fd01eec6d", + "masternode_root": "28736666198c9a8d0564edea185ac0e35fd4e315e384aea716b7fa2e51ea4ddf", + "quorum_root": "e5cf38b3f4aa4fdc1ce08bb90ae297f78f2659c4d043d0042b309c6d05d5fd38" + }, + "proof_hex": "444153484e43303201a80e18006dec1ed01fac9d62ee3515ace9537dae364640a39163269fd7f730370d000000df4dea512efab716a7ae84e315e3d45fe3c05a18eaed64058d9a8c196666732838fdd5056d9c302b04d043d0c459268ff797e20ab98be01cdc4faaf4b338cfe543010000030001f94e8097514b57020d7d908d4966051535dd12a0a2dae7a9905a011a3f00000032ffffffffffff0332ffffffffffff0394be2be0233ebc598fc641982e613dbcfc5877aa602f159cfcdd29be1d0a4a750813459b63f0c405ab34014781357ad94259bb4abbda8f3466fbc42967c84bd5ecc4f796b75e34f47b54b72af075dcb6989a99e1b1da140f77edbb04e036598b52ef4ccec10d99cb31ec779d4d850868537b8ce4494d98ca6a5b679069b106e90c5e1ef210b68295a3995975c2361e0b3bc86948068e2c8e51a879f9bbcba402835698407cbaafb9f020706a4fa29c48a1b285d4b64cb837366d873e0437fd8ec7a401ae35920c29967f38c1ace4239be38ed28d308311ff6ff3b96e9b5150b904574da93dd7c4b210ce1654c695bc2ceeb7d5fd275813decf9a56e972ab7db74aea3bccb739de7b6e9e273b171516a32b0000006d000000074c097f54a9994db8255d509566490589f0277a0d42afaf64c7ff8f096797bc3c2ef0779342efb9820f53577e9fc79b59c393f204a9c2b1745e48392b1eeefd09cd4b4a67dd4c74bb8021815dd277c5a2a1974f699ad28c2e44072874c9a120c4e48a1ecf7705668ce329a18c7f5bba9f14f494830288571b82b7138534c6929f089b14beaa068ac85c1443a97aa6009c8ac175de5461cf2034a094f3ebb1d4c4d3e23116ae003d38d61f14af6299de389397805ca4ecbd65a2aab7c9c5f99be77f818b449cef8aab960ee6848b4968a0c24f7b45e0f1c105622379b1cb8c083300003e0f180000000020373732036aa2b418bda3ad8f22a62152d4ef9d550b49c0ebf261604f34000000ec6c85972aba478981a19233b3514ef541976a4e8a6d0450dcd8d7277a685538e4e0d86ad4e7001ebff50500aa08742e5f391c309351c0f45dd708fa7bac12a5d3b47f6d8e3d84547dab096f39268c2e64c7cca868a503f908bf0c7d08f18e05dc3658522ae25124561bce8ae0e5d7f85456d6ebdc7b9817a0c73ed26055550e0a2ede3482e990e227fcf1fb3201000003000500010000000000000000000000000000000000000000000000000000000000000000ffffffff016affffffff03b84b8c03000000001976a914b489115851ca07a26a5ad8bac3cec3c7dbebd83188ac2ed5fd0300000000016af80da706000000001976a914c69a0bda7daaae481be8def95e5f347a1d00a4b488ac00000000af03003e0f1800df4dea512efab716a7ae84e315e3d45fe3c05a18eaed64058d9a8c1966667328e1f2b2b43f667274b3e8f488f24dc9f7fa611149dc36e2324826b951d5be8950008539584f69b9a56090c811d246a1571fdf8edda332425d51db17f9d421c77b1d7f7d610a9314032a18b73989ff8b82f70a06172ae5b1daae0b858225b6c85fabaee79909e71158e102f7207eee5ca3783b7a4cabe981fb525e072d1751e0ba789802289a2d220000000000000100000000", + "bootstrap_hex": "92040000444153484e43303201a80e18006dec1ed01fac9d62ee3515ace9537dae364640a39163269fd7f730370d000000df4dea512efab716a7ae84e315e3d45fe3c05a18eaed64058d9a8c196666732838fdd5056d9c302b04d043d0c459268ff797e20ab98be01cdc4faaf4b338cfe543010000030001f94e8097514b57020d7d908d4966051535dd12a0a2dae7a9905a011a3f00000032ffffffffffff0332ffffffffffff0394be2be0233ebc598fc641982e613dbcfc5877aa602f159cfcdd29be1d0a4a750813459b63f0c405ab34014781357ad94259bb4abbda8f3466fbc42967c84bd5ecc4f796b75e34f47b54b72af075dcb6989a99e1b1da140f77edbb04e036598b52ef4ccec10d99cb31ec779d4d850868537b8ce4494d98ca6a5b679069b106e90c5e1ef210b68295a3995975c2361e0b3bc86948068e2c8e51a879f9bbcba402835698407cbaafb9f020706a4fa29c48a1b285d4b64cb837366d873e0437fd8ec7a401ae35920c29967f38c1ace4239be38ed28d308311ff6ff3b96e9b5150b904574da93dd7c4b210ce1654c695bc2ceeb7d5fd275813decf9a56e972ab7db74aea3bccb739de7b6e9e273b171516a32b0000006d000000074c097f54a9994db8255d509566490589f0277a0d42afaf64c7ff8f096797bc3c2ef0779342efb9820f53577e9fc79b59c393f204a9c2b1745e48392b1eeefd09cd4b4a67dd4c74bb8021815dd277c5a2a1974f699ad28c2e44072874c9a120c4e48a1ecf7705668ce329a18c7f5bba9f14f494830288571b82b7138534c6929f089b14beaa068ac85c1443a97aa6009c8ac175de5461cf2034a094f3ebb1d4c4d3e23116ae003d38d61f14af6299de389397805ca4ecbd65a2aab7c9c5f99be77f818b449cef8aab960ee6848b4968a0c24f7b45e0f1c105622379b1cb8c083300003e0f180000000020373732036aa2b418bda3ad8f22a62152d4ef9d550b49c0ebf261604f34000000ec6c85972aba478981a19233b3514ef541976a4e8a6d0450dcd8d7277a685538e4e0d86ad4e7001ebff50500aa08742e5f391c309351c0f45dd708fa7bac12a5d3b47f6d8e3d84547dab096f39268c2e64c7cca868a503f908bf0c7d08f18e05dc3658522ae25124561bce8ae0e5d7f85456d6ebdc7b9817a0c73ed26055550e0a2ede3482e990e227fcf1fb3201000003000500010000000000000000000000000000000000000000000000000000000000000000ffffffff016affffffff03b84b8c03000000001976a914b489115851ca07a26a5ad8bac3cec3c7dbebd83188ac2ed5fd0300000000016af80da706000000001976a914c69a0bda7daaae481be8def95e5f347a1d00a4b488ac00000000af03003e0f1800df4dea512efab716a7ae84e315e3d45fe3c05a18eaed64058d9a8c1966667328e1f2b2b43f667274b3e8f488f24dc9f7fa611149dc36e2324826b951d5be8950008539584f69b9a56090c811d246a1571fdf8edda332425d51db17f9d421c77b1d7f7d610a9314032a18b73989ff8b82f70a06172ae5b1daae0b858225b6c85fabaee79909e71158e102f7207eee5ca3783b7a4cabe981fb525e072d1751e0ba789802289a2d22000000000000010000000003003d010000030006f64aa6f2cf18889e8ff4704f6bbd50a7ab80f3f1fc6eab984e691f381b00000019ffffff0119ffffff01b813d560b9a73db7b01cfe80ee7ab20a4af7ccda4b94a0cd8ceda8f5cb0fa604760430ef8c644e0f823e089aa7839f9393e0215b2e2b69a88310ffe95ac90099be1d2ece9704bd99a3fd0a3ba8d948e387351b53935ce873abf06103066bcd2f6c1f10f343ce5baf4ab97db6dd483b914ca27ef04636641bcba6c5a5c7aa053903bfacfe8a3317e4fd2be691d6301e814b0c563b9baff75ee551967e41c8bfc8ea07d468ad3123eeb5bd52fb397ae8c788da739f2edf77707516ad03022a0abb3fb74623fe36a82f3036c7cfe6a8d40f426fceded4783cd87b43bc45160e9dfe0d6f09ba2f353ee0797334ea74ce21e967c0efc0ac3984ec0a314650ed1dc4f746f146742eb342b95e7466426d5b532f0a0000006d000000071b4109128f361787885d43e827477232ca77d1253201f14b6d9bfcbb36b5ca29296f2a6e10f6b5d257bdf26f29fd098fbefcf317d43ac45bf9b9a912d6990f973e1af01f8e5eae6165af402ae54256702b3ef04e7bd38397bb341c593a62bcad03ca107596c89ccecb5e5cefdb493a11d62c7f7c171d9b0d9a4f223b5237c9112862024cffad39318b66bbcef991aaa9c5b6862e41310b0410c050a61bda1be2b1fea91d5e2ee314113d4bfcf58b80aa6d0f7a5fa959582db28ee87945740044ef7f65c97d261116cdd5dfa7a5203904d3c6a4a6a625a412384925657abdc78b01af0000000a5b7f1478902ecab28b3404f5e79e9020acb9abd31a6e13928c2450d42684b6adf56996e04e36430af272b8c0b1ed1dd57b6146775532c53634ecb5b600000000000000000000000000ffff44437a064e1fb8a150e6472bc281b51537dda3d077264786f207d05c4ec8d314b5883e32b14b15077bf39ccf5d92543a6fd0ee23897802e543987843fbd195e84ff3a28637ab2231d98b010100a30559b537a5cf3ed95e48d8c006d53de9e049dbf9a1120000008a01000009de4fceb4b2b1ab89bc498f4caca25247813558f17e26eb6c6054ef28688906fe592122fffeb92cbd77956c86811e91b0ed60d5a072dbb3d402c90b4361af3f81d70256b9757f6707488b92e518d616a9cf223aa571cd66e73e7c1236f85dc66ff3a1f817422025f9894edeabb8639ebb57d968a3f5fd3bc5ceb7c661cc34273033306f00e1e3878289f50ae34e3d72691aff06fe77a52080f7a479fcdb79263e8c1ed9f00b2c64da2453cd9bc9de4c250c6fd40a0fc9b18b9fe62408dc14f29ce668b31b1e605ea286913109d87371338c9d545551119f821a939657e6f2491632c3ff1a6584536b4196c735f8331a47844337e0cb68c59388a1ea289e724443d00c1109ec72111ce417de8c49fea97279ee87434bc1f85707010cdbda71b22301af00000012c8d756322b07e0cf2f73ad46f5f25bf91c8b7749419d02a6ccca8da60dc488f5364ad36ce7990bab58abc9f2d480903668268322b7e5099d089ccb3d00000000000000000000000000ffff55d1f3064e1f89d3ff18d870c0b6c9605d986c2de92f44c48032408e183a1d268bad77570e30a6d057a026119d9d044c1ce29b518e91d69372e5829f2cc1c3a9882e466ae9761dfc40e5010100a3053fab9f8566e701ad9bdc75f6f0fe7aaba6008f061d0000008a01000009b1f4342c3ae24aaaae59a75c67fc9ad1e62d520ae3f9a11459aa73f7e8fb753170ff09a84bf2b4e29d54d9c346bee0a9498548bdfb78fef626caa32545a6a4d4bbca76f29274761340c446e9e7d3c8940d296a7abb8f4d4d315a19fe11dd5bdd23a95704aa8cbd08f66e671b27e3782a5f5d8c5cc4f625ae1654b06e3888daaa33306f00e1e3878289f50ae34e3d72691aff06fe77a52080f7a479fcdb79263e8c1ed9f00b2c64da2453cd9bc9de4c250c6fd40a0fc9b18b9fe62408dc14f29ce668b31b1e605ea286913109d87371338c9d545551119f821a939657e6f2491632c3ff1a6584536b4196c735f8331a47844337e0cb68c59388a1ea289e724443d00c1109ec72111ce417de8c49fea97279ee87434bc1f85707010cdbda71b223", + "target": { + "network": 1, + "height": 1576766, + "block_hash": "0000004d3052033c5aafbbecf1c247137c7b0879845c0f13a97bd704339885fb", + "masternode_root": "28736666198c9a8d0564edea185ac0e35fd4e315e384aea716b7fa2e51ea4ddf", + "quorum_root": "5089bed551b9264832e236dc491161faf7c94df288f4e8b37472663fb4b2f2e1" + } +} diff --git a/packages/rs-core-proof/tests/data/testnet.json b/packages/rs-core-proof/tests/data/testnet.json new file mode 100644 index 00000000000..7b22d55764c --- /dev/null +++ b/packages/rs-core-proof/tests/data/testnet.json @@ -0,0 +1,18 @@ +{ + "checkpoint": { + "network": 1, + "height": 1574768, + "block_hash": "0000006648a17e1ab1c4c5745d80fdb6d69bff32fd93ad4fc60baad624e1bb62", + "masternode_root": "d19783ddd57d2cdcf8cc2552a82859f062faab7c4b675b870762bd0848b5af8c", + "quorum_root": "5e6becb125aed06017530637a1423d87a27b468bef6865405d619b20e8dcf2c5" + }, + "proof_hex": "444153484e433032017007180062bbe124d6aa0bc64fad93fd32ff9bd6b6fd805d74c5c4b11a7ea148660000008cafb54808bd6207875b674b7cabfa62f05928a85225ccf8dc2c7dd5dd8397d1c5f2dce8209b615d406568ef8b467ba2873d42a13706531760d0ae25b1ec6b5e430100000300010d95087124bef7460a981cd5900accd968de3ef62bcc4beb15e12a057000000032ffffffffffff0332ffffffffffff03b8ec962444fda60eb92aa98921ea8f30beae27e4df2b29b0e7034f3ec07d707930f7b4270bbdb1555945c95b5c83e83cc5a2061f4a1264f4ffe8dc96a3225fb0d5801a573c664faf8185832c13949665a1c29c6782e2327382da0477368180966d8edd7fad10524186dc4237d6a113cd999dfece70f7ca7ef7e2044780caa5ca02185d175ad1a7a8c95e816cb007f087c5af457c50c17e5e9387c15374077844f22a6b82364d41867ab09a809d11bb3f8a089a200c80aafa2249ea7258b787ceeea7038e99e264c0eec5821c758efc840f0c5299ffe06a6e609b9be24abe91b60e3c5cc7193d299126852deb9db67519a302979b611418e1f3c3644dcf60c2c1b3c1509c7586fc36ce1ed1ec5029a855590000006d00000007cf9f697dec52933af5dfe5b6384677f03483ee40b86548fee10cfbaa39e645704ebfb6ebef37d76f9e165b2ed9fdf7d836c48b048ed3afc645d102ad7f5c7ceba0de3ff4a1134f4d584a2b4ec04ebb269d9341798d7a42c7f153d6d2e70432a5239d4140cada4d980599d8ad36d5c4cde999553e552644ce3473579fabbd22b0b573feb9ee12066cc7d5c58d452f3c93348ad4566c4a3bf5334761ef683ff287e32c60f601702009cb148cf8540cd3895c1a749af84146ae3235aadbeb1ee86f7e06521e6a8fde85c191e6c1a959afa67f3584f614896e58d41d9b289c27dfd804004c08180000000020ed36421d595231d47ca154e473b7fe295c8887ee8ef2792832fbe1217600000042b8a8e30a7ceffbbf223fc7996e4bc500f1466cf4acd2bf35ffa300cdb7552aa11cd56a3671081e4bf40700b25838566ff7148e704894657f8a57245c1948c64b2331343c7bd17586a93721a08395bf340bb6f946537a4785c7391614ef0ac4c98af0e4c5354305906a8fac3a39158d240e42711affe2b2effc50238b45e2ddb8b9fb307681fb3ee79b08f35601000003000600000000000000fd490101004c081800030001002ac52efade4141ddad6eace8848d62bd0e9eab08d78d318c9a67b54100000032ffffffffffff0332ffffffffffff038c2b695848a83578c91f93abe2a82cb49b87c3808331a4b71af0e64aa067bb12cf4f7b39b6360b3c6f761f87fed867747e4abfc1e7516f79fc57529bda60a0a9672b3fe391d72343f6e436e997d420cd900ebe0078752340e408f4675f8be88dfafcc4b7b6bc1d8bc42d67f5d77a22c388c43365475d36af3f82a80489cae366122921c6979d0a9f5240f39fd735ddfcca1cac6e2a57a89999166a1ed53ee3cb941dcf293630fb4c0d87a93db32dcf19aad2b734dc5f0e2ce1cd98fb5c5ba260c57e209e6dcbdf94b9d3204257b72be796b8b06cd6d1e0e881b4ec89476741c816ca5ef8ce05ffd3fb68a99e56dbb1d5857b35bc1ad38a0b0abb56d2fb5713be9bad91fd723bfd43c844f0ad2960c3fc01000000090000000407ad1c43e625e57a92eb8bfc3405b96a2d1d191bd769965d25247496d76c483c01f48e3d0bd279c582a9e4e7e19025f947e9c9837b63b4bc31a8c9a0dcf487e4ae91136e6f21ab70c073ac0903175496d030aff12c8b3eb3c89821664618cbff9c979ccd160f37b0e8f98f37a8544e6f52f430347d6b4a3c10fcbe0e1e35b2cb00002d0a180000000020af0df340c2ca8fa70d16a30c0ac2a816d3b889c94b8a8df305885f99810000003effd237bb0f2c93a6eb426841d028c84d930a76d90e68b44649f09de4b4f059a524d66abaf2001ea0790300b37d1b0eb7b82b866de51573f7052f27b7b47227a05aae6135a033a680e91e9fca7ac0ee63788e711d3f58f2022b8f00081b1d2a0e5282a2aba993bf0c8e260d06bd3cf3b2246446b50255b920ad0aff470544a796308c5edcd3190a488dfd205601000003000600000000000000fd490101002b0a1800030001d63e1a2e552b5e587be5b601a46db1d658507be5b5f335c6f94ccbd58700000032ffffffffffff0332ffffffffffff03a89c1a59186276c2e8d0bbe16c7b1cc1ca33e1e01999fcb03743a8b379d40096102006a9033a9968d4446f9276fe0e6493af4d30b5915f3eb55180859c827e0f0fb1410a7bc21b1ec33680b86b09be48a714ce03012396db7fdf8657a203457617321bde81d6cffdaa2632a67f678cf2f4d1c379efe48137594aa2634398901f0799500ab92d564dc535cc0a5218a04ea27607721d1d8d5f904899e07f6b10319aad0818f476797a77108a6b3b19e69cae8b8a7a39ae74cbb8caa70deb45cc1dbd3146b9c7028fdbedfb31b81ab137b8767660a7156a25f5793f1335a1fde3650807563eb4a466936a9fbb5afdb7ed7faab2d455e9703ed88a949f24b5c240001938d0d9e2f38891c39e53731293d54e0100000004000000021fdb2c1f0572f6b450d3248ab684916e34ddf253cb2f227c3ed6afd59614116a0fe0ff86eabf34e96a60ca95062c93fc92e5063a89bd82cb679580cec2011788020000000020c8d425146fb9576a709eba4d5b8e687d60a3fb1145097802f21e53391a00000058b2a810fb0e2cc6529886b841746408274af7835e8c167097526379444c60f43223d66a49fd001e9fda020000000020e0817029310a6efdfd2dfaf4c748690b55084c4c6276c0513fedc5c339000000cc50ab9c888f36a173ca0d35ca143aa15d8e5a6bbb89ff4b56b260bd41025bab0624d66a75fc001e6cba0200ab0b180000000020a317d9052d2b8fc82951e4d6a2f8ab5ecc9bff910db7a0b394921971bf000000afbcfc3b9e8ef8f75ef65bb655608adf3e759f741e1e6b70a6f3bd24f90170a901f2d66afc5d011ed73a050090269ef2eefee16cc69dd6ac4b847ffb515804e55a5b721623ada9ebf1f5760508f521a8800f323ae6bdf3470e5ea7de06320d3f4fcd86fa3c519e10abf8a2f9b3f398fcbbb3e0cbbb4212210d8b8ec0fa1db2c04318de595a992ae6ca2e39c15601000003000600000000000000fd49010100ab0b18000300019e3221848c14752808d3be14a37d3bf6186efe8b04137bd3cf75d9b22d00000032ffffffffffff0332ffffffffffff0397b1d1bb8e9bf6ece8600d3da9d2e0cf185f0e1b0e0176d6a2f1734049e8ae0280a79c58e2ad2f941a3b372471ba19479472988c79245e291b1cb7112518da65a2501f40cb006acee652b3af9804bf1db99794fab1d74869619491e4aba703cd18361d4561b3ae6703131bfb49fbbbc555c2b75ed40471109af490a80b3f14bb045c6516800ab685fa00db1c8bbee6b0fd705a56d90114b0498d4f89ec5c9599361ea17cccc72509f5680807f80448e4ad5809608497ddb3ee9d199e006bfb648b7d4c10bc208ba3022560e05c364a0ead7be8e15ca51cabaaa6ef3bed8d15ce0681b262d52374b93432b48058012a2044c2b17a5e3816bc96f53f0e99a8ea7d16ce5c1fbfe21ac3e12456d4d1db2adf010000000600000003fb24f952450bb40a953581cea905bd97df4dc187e180a4ba45085148b99b3983f58e336499890b90ec37aea130a0f75478bd0270788d63e1961f47a8b644aff12f2cd56720a13a04b8e3936892a980c7ab70619d9c6e6d6c34ab58e9288165c60000be0d180000000020752292f6f953563fa944234551f944106e5c217827253dba0000231ef0000000a1dd2c9ccd046790ebe38555e289154a967f7aaabea8f0a20fa596a11ceb67ab4f0ed86a1b75011ecf740400808e90bd2060e079b20f865feea40131f378333eecca5918c67817f96ef2fa8490785c003bce0b6b0d3ef4b49b93a8b90ab2027affd42e74f674d18701531a923e3c566c6ff732e4c1dd22a3c68b16e6903474512863f22172313336a46d205e5601000003000600000000000000fd49010100bb0d18000300013524c22745ca56b62cc5956e49bc9bcfcd725b821b2dbec797a807ea4100000032ffffffffffff0332ffffffffffff03a4854d5b1c3ff8e6012c3a3a1d34d12c77d542a07e893ef582c48ea8d2e9f0412b53f01ff29bc64cfdd7ee9a0bcaacdc5e0a6603d6aaf5141c612e16066d94e4cad096560fddcc90d1353044ebe8a1fd85406b3e75cd6b3debab754f4f8552b1f37062c988edef4931960b9469ca3fac0efbb1fde3f5e45801243e752134c6270ea886e5676fa46941f29f35a87942ff1b93f4f5cc5092df754142a1605965a56bab1050884c9e77570dabc5cc2fd5f9b1f94b81435bdb39d3bbc66034cede162febd77e1e1105cb4cc2a879e0baac4b94f2ec7ae06cc889e9fcbe6cdf43fb4f144fe18ac2f0e9f85e4f2fb232b000377744acc40147db2a098b7dbc55319c45ce6cdee01ca275c54c64d9a11665cf1801000000040000000276de27fa656a35dbb32e7cd9f1f7a15479eb3d3800380528fba0e07f0071442e98bf09a6736601b1a05c3f8416b5c2cedd6383652975d4c4f263ee3e2cba915b030000000020085663f228b88ea38af5180fd36e13e489c9373c646cc4825a1b9b4c5f00000061d1b9bae9061efc4d1d0526b56f3dfeba5257fa227326d0bdbbf1ee9a723f632c0dd86a1879011e96e70100000000208b487ce47e6a317bad93be933847b822553f8d240e8d74601684b4c5b3000000ad0101428b9020a7aa726db48e6e50fb1d4f4b85daf807b0c1fc6a52ca8454cf390dd86a3673011ef0c50100000000209dc63ad77910d9fabcdf23be8126b5c6a3a51ec2acc526eb57d7766a230000006a281b8c67908ebd83d47341673c93f27fa198ab66d55dc3034536d72c508b9b4b0dd86a0a7b011e0cba0300400f1800000000203e6023289fa4ebf4338f76a2c9da0741e8084a6d02e7efface6cff60020000005615dc62efe815c5b0fa2bcf8af7594c1b02124f6c53759a39b2e050ad4e467417e1d86a5ee1001efcac0e00b46a4edf1b6b8eecc2f0245d2f22c51c739c6ca3aadb3a58ff186bb7ec7f55d96545fb3e531c8d5240f668e3f312827603674df95713354777f9729b451637a141489a1e87c34b1059abee83cc04bcba976127b15dd3512c558c3f2b5d0c55e43201000003000500010000000000000000000000000000000000000000000000000000000000000000ffffffff016affffffff03b84b8c03000000001976a914b489115851ca07a26a5ad8bac3cec3c7dbebd83188ac2ed5fd0300000000016af80da706000000001976a914c69a0bda7daaae481be8def95e5f347a1d00a4b488ac00000000af0300400f1800df4dea512efab716a7ae84e315e3d45fe3c05a18eaed64058d9a8c1966667328e1f2b2b43f667274b3e8f488f24dc9f7fa611149dc36e2324826b951d5be895001aa08742e5f391c309351c0f45dd708fa7bac12a5d3b47f6d8e3d84547dab096f39268c2e64c7cca868a503f908bf0c7d08f18e05dc3658522ae25124561bce8ae0e5d7f85456d6ebdc7b9817a0c73ed26055550e0a2ede3482e990e227fcf1fbf4ac23a22d220000000000000100000000", + "bootstrap_hex": "e60f0000444153484e433032017007180062bbe124d6aa0bc64fad93fd32ff9bd6b6fd805d74c5c4b11a7ea148660000008cafb54808bd6207875b674b7cabfa62f05928a85225ccf8dc2c7dd5dd8397d1c5f2dce8209b615d406568ef8b467ba2873d42a13706531760d0ae25b1ec6b5e430100000300010d95087124bef7460a981cd5900accd968de3ef62bcc4beb15e12a057000000032ffffffffffff0332ffffffffffff03b8ec962444fda60eb92aa98921ea8f30beae27e4df2b29b0e7034f3ec07d707930f7b4270bbdb1555945c95b5c83e83cc5a2061f4a1264f4ffe8dc96a3225fb0d5801a573c664faf8185832c13949665a1c29c6782e2327382da0477368180966d8edd7fad10524186dc4237d6a113cd999dfece70f7ca7ef7e2044780caa5ca02185d175ad1a7a8c95e816cb007f087c5af457c50c17e5e9387c15374077844f22a6b82364d41867ab09a809d11bb3f8a089a200c80aafa2249ea7258b787ceeea7038e99e264c0eec5821c758efc840f0c5299ffe06a6e609b9be24abe91b60e3c5cc7193d299126852deb9db67519a302979b611418e1f3c3644dcf60c2c1b3c1509c7586fc36ce1ed1ec5029a855590000006d00000007cf9f697dec52933af5dfe5b6384677f03483ee40b86548fee10cfbaa39e645704ebfb6ebef37d76f9e165b2ed9fdf7d836c48b048ed3afc645d102ad7f5c7ceba0de3ff4a1134f4d584a2b4ec04ebb269d9341798d7a42c7f153d6d2e70432a5239d4140cada4d980599d8ad36d5c4cde999553e552644ce3473579fabbd22b0b573feb9ee12066cc7d5c58d452f3c93348ad4566c4a3bf5334761ef683ff287e32c60f601702009cb148cf8540cd3895c1a749af84146ae3235aadbeb1ee86f7e06521e6a8fde85c191e6c1a959afa67f3584f614896e58d41d9b289c27dfd804004c08180000000020ed36421d595231d47ca154e473b7fe295c8887ee8ef2792832fbe1217600000042b8a8e30a7ceffbbf223fc7996e4bc500f1466cf4acd2bf35ffa300cdb7552aa11cd56a3671081e4bf40700b25838566ff7148e704894657f8a57245c1948c64b2331343c7bd17586a93721a08395bf340bb6f946537a4785c7391614ef0ac4c98af0e4c5354305906a8fac3a39158d240e42711affe2b2effc50238b45e2ddb8b9fb307681fb3ee79b08f35601000003000600000000000000fd490101004c081800030001002ac52efade4141ddad6eace8848d62bd0e9eab08d78d318c9a67b54100000032ffffffffffff0332ffffffffffff038c2b695848a83578c91f93abe2a82cb49b87c3808331a4b71af0e64aa067bb12cf4f7b39b6360b3c6f761f87fed867747e4abfc1e7516f79fc57529bda60a0a9672b3fe391d72343f6e436e997d420cd900ebe0078752340e408f4675f8be88dfafcc4b7b6bc1d8bc42d67f5d77a22c388c43365475d36af3f82a80489cae366122921c6979d0a9f5240f39fd735ddfcca1cac6e2a57a89999166a1ed53ee3cb941dcf293630fb4c0d87a93db32dcf19aad2b734dc5f0e2ce1cd98fb5c5ba260c57e209e6dcbdf94b9d3204257b72be796b8b06cd6d1e0e881b4ec89476741c816ca5ef8ce05ffd3fb68a99e56dbb1d5857b35bc1ad38a0b0abb56d2fb5713be9bad91fd723bfd43c844f0ad2960c3fc01000000090000000407ad1c43e625e57a92eb8bfc3405b96a2d1d191bd769965d25247496d76c483c01f48e3d0bd279c582a9e4e7e19025f947e9c9837b63b4bc31a8c9a0dcf487e4ae91136e6f21ab70c073ac0903175496d030aff12c8b3eb3c89821664618cbff9c979ccd160f37b0e8f98f37a8544e6f52f430347d6b4a3c10fcbe0e1e35b2cb00002d0a180000000020af0df340c2ca8fa70d16a30c0ac2a816d3b889c94b8a8df305885f99810000003effd237bb0f2c93a6eb426841d028c84d930a76d90e68b44649f09de4b4f059a524d66abaf2001ea0790300b37d1b0eb7b82b866de51573f7052f27b7b47227a05aae6135a033a680e91e9fca7ac0ee63788e711d3f58f2022b8f00081b1d2a0e5282a2aba993bf0c8e260d06bd3cf3b2246446b50255b920ad0aff470544a796308c5edcd3190a488dfd205601000003000600000000000000fd490101002b0a1800030001d63e1a2e552b5e587be5b601a46db1d658507be5b5f335c6f94ccbd58700000032ffffffffffff0332ffffffffffff03a89c1a59186276c2e8d0bbe16c7b1cc1ca33e1e01999fcb03743a8b379d40096102006a9033a9968d4446f9276fe0e6493af4d30b5915f3eb55180859c827e0f0fb1410a7bc21b1ec33680b86b09be48a714ce03012396db7fdf8657a203457617321bde81d6cffdaa2632a67f678cf2f4d1c379efe48137594aa2634398901f0799500ab92d564dc535cc0a5218a04ea27607721d1d8d5f904899e07f6b10319aad0818f476797a77108a6b3b19e69cae8b8a7a39ae74cbb8caa70deb45cc1dbd3146b9c7028fdbedfb31b81ab137b8767660a7156a25f5793f1335a1fde3650807563eb4a466936a9fbb5afdb7ed7faab2d455e9703ed88a949f24b5c240001938d0d9e2f38891c39e53731293d54e0100000004000000021fdb2c1f0572f6b450d3248ab684916e34ddf253cb2f227c3ed6afd59614116a0fe0ff86eabf34e96a60ca95062c93fc92e5063a89bd82cb679580cec2011788020000000020c8d425146fb9576a709eba4d5b8e687d60a3fb1145097802f21e53391a00000058b2a810fb0e2cc6529886b841746408274af7835e8c167097526379444c60f43223d66a49fd001e9fda020000000020e0817029310a6efdfd2dfaf4c748690b55084c4c6276c0513fedc5c339000000cc50ab9c888f36a173ca0d35ca143aa15d8e5a6bbb89ff4b56b260bd41025bab0624d66a75fc001e6cba0200ab0b180000000020a317d9052d2b8fc82951e4d6a2f8ab5ecc9bff910db7a0b394921971bf000000afbcfc3b9e8ef8f75ef65bb655608adf3e759f741e1e6b70a6f3bd24f90170a901f2d66afc5d011ed73a050090269ef2eefee16cc69dd6ac4b847ffb515804e55a5b721623ada9ebf1f5760508f521a8800f323ae6bdf3470e5ea7de06320d3f4fcd86fa3c519e10abf8a2f9b3f398fcbbb3e0cbbb4212210d8b8ec0fa1db2c04318de595a992ae6ca2e39c15601000003000600000000000000fd49010100ab0b18000300019e3221848c14752808d3be14a37d3bf6186efe8b04137bd3cf75d9b22d00000032ffffffffffff0332ffffffffffff0397b1d1bb8e9bf6ece8600d3da9d2e0cf185f0e1b0e0176d6a2f1734049e8ae0280a79c58e2ad2f941a3b372471ba19479472988c79245e291b1cb7112518da65a2501f40cb006acee652b3af9804bf1db99794fab1d74869619491e4aba703cd18361d4561b3ae6703131bfb49fbbbc555c2b75ed40471109af490a80b3f14bb045c6516800ab685fa00db1c8bbee6b0fd705a56d90114b0498d4f89ec5c9599361ea17cccc72509f5680807f80448e4ad5809608497ddb3ee9d199e006bfb648b7d4c10bc208ba3022560e05c364a0ead7be8e15ca51cabaaa6ef3bed8d15ce0681b262d52374b93432b48058012a2044c2b17a5e3816bc96f53f0e99a8ea7d16ce5c1fbfe21ac3e12456d4d1db2adf010000000600000003fb24f952450bb40a953581cea905bd97df4dc187e180a4ba45085148b99b3983f58e336499890b90ec37aea130a0f75478bd0270788d63e1961f47a8b644aff12f2cd56720a13a04b8e3936892a980c7ab70619d9c6e6d6c34ab58e9288165c60000be0d180000000020752292f6f953563fa944234551f944106e5c217827253dba0000231ef0000000a1dd2c9ccd046790ebe38555e289154a967f7aaabea8f0a20fa596a11ceb67ab4f0ed86a1b75011ecf740400808e90bd2060e079b20f865feea40131f378333eecca5918c67817f96ef2fa8490785c003bce0b6b0d3ef4b49b93a8b90ab2027affd42e74f674d18701531a923e3c566c6ff732e4c1dd22a3c68b16e6903474512863f22172313336a46d205e5601000003000600000000000000fd49010100bb0d18000300013524c22745ca56b62cc5956e49bc9bcfcd725b821b2dbec797a807ea4100000032ffffffffffff0332ffffffffffff03a4854d5b1c3ff8e6012c3a3a1d34d12c77d542a07e893ef582c48ea8d2e9f0412b53f01ff29bc64cfdd7ee9a0bcaacdc5e0a6603d6aaf5141c612e16066d94e4cad096560fddcc90d1353044ebe8a1fd85406b3e75cd6b3debab754f4f8552b1f37062c988edef4931960b9469ca3fac0efbb1fde3f5e45801243e752134c6270ea886e5676fa46941f29f35a87942ff1b93f4f5cc5092df754142a1605965a56bab1050884c9e77570dabc5cc2fd5f9b1f94b81435bdb39d3bbc66034cede162febd77e1e1105cb4cc2a879e0baac4b94f2ec7ae06cc889e9fcbe6cdf43fb4f144fe18ac2f0e9f85e4f2fb232b000377744acc40147db2a098b7dbc55319c45ce6cdee01ca275c54c64d9a11665cf1801000000040000000276de27fa656a35dbb32e7cd9f1f7a15479eb3d3800380528fba0e07f0071442e98bf09a6736601b1a05c3f8416b5c2cedd6383652975d4c4f263ee3e2cba915b030000000020085663f228b88ea38af5180fd36e13e489c9373c646cc4825a1b9b4c5f00000061d1b9bae9061efc4d1d0526b56f3dfeba5257fa227326d0bdbbf1ee9a723f632c0dd86a1879011e96e70100000000208b487ce47e6a317bad93be933847b822553f8d240e8d74601684b4c5b3000000ad0101428b9020a7aa726db48e6e50fb1d4f4b85daf807b0c1fc6a52ca8454cf390dd86a3673011ef0c50100000000209dc63ad77910d9fabcdf23be8126b5c6a3a51ec2acc526eb57d7766a230000006a281b8c67908ebd83d47341673c93f27fa198ab66d55dc3034536d72c508b9b4b0dd86a0a7b011e0cba0300400f1800000000203e6023289fa4ebf4338f76a2c9da0741e8084a6d02e7efface6cff60020000005615dc62efe815c5b0fa2bcf8af7594c1b02124f6c53759a39b2e050ad4e467417e1d86a5ee1001efcac0e00b46a4edf1b6b8eecc2f0245d2f22c51c739c6ca3aadb3a58ff186bb7ec7f55d96545fb3e531c8d5240f668e3f312827603674df95713354777f9729b451637a141489a1e87c34b1059abee83cc04bcba976127b15dd3512c558c3f2b5d0c55e43201000003000500010000000000000000000000000000000000000000000000000000000000000000ffffffff016affffffff03b84b8c03000000001976a914b489115851ca07a26a5ad8bac3cec3c7dbebd83188ac2ed5fd0300000000016af80da706000000001976a914c69a0bda7daaae481be8def95e5f347a1d00a4b488ac00000000af0300400f1800df4dea512efab716a7ae84e315e3d45fe3c05a18eaed64058d9a8c1966667328e1f2b2b43f667274b3e8f488f24dc9f7fa611149dc36e2324826b951d5be895001aa08742e5f391c309351c0f45dd708fa7bac12a5d3b47f6d8e3d84547dab096f39268c2e64c7cca868a503f908bf0c7d08f18e05dc3658522ae25124561bce8ae0e5d7f85456d6ebdc7b9817a0c73ed26055550e0a2ede3482e990e227fcf1fbf4ac23a22d22000000000000010000000003003d010000030006f64aa6f2cf18889e8ff4704f6bbd50a7ab80f3f1fc6eab984e691f381b00000019ffffff0119ffffff01b813d560b9a73db7b01cfe80ee7ab20a4af7ccda4b94a0cd8ceda8f5cb0fa604760430ef8c644e0f823e089aa7839f9393e0215b2e2b69a88310ffe95ac90099be1d2ece9704bd99a3fd0a3ba8d948e387351b53935ce873abf06103066bcd2f6c1f10f343ce5baf4ab97db6dd483b914ca27ef04636641bcba6c5a5c7aa053903bfacfe8a3317e4fd2be691d6301e814b0c563b9baff75ee551967e41c8bfc8ea07d468ad3123eeb5bd52fb397ae8c788da739f2edf77707516ad03022a0abb3fb74623fe36a82f3036c7cfe6a8d40f426fceded4783cd87b43bc45160e9dfe0d6f09ba2f353ee0797334ea74ce21e967c0efc0ac3984ec0a314650ed1dc4f746f146742eb342b95e7466426d5b532f0a0000006d000000071b4109128f361787885d43e827477232ca77d1253201f14b6d9bfcbb36b5ca29296f2a6e10f6b5d257bdf26f29fd098fbefcf317d43ac45bf9b9a912d6990f973e1af01f8e5eae6165af402ae54256702b3ef04e7bd38397bb341c593a62bcad03ca107596c89ccecb5e5cefdb493a11d62c7f7c171d9b0d9a4f223b5237c9112862024cffad39318b66bbcef991aaa9c5b6862e41310b0410c050a61bda1be2b1fea91d5e2ee314113d4bfcf58b80aa6d0f7a5fa959582db28ee87945740044ef7f65c97d261116cdd5dfa7a5203904d3c6a4a6a625a412384925657abdc78b01af0000000a5b7f1478902ecab28b3404f5e79e9020acb9abd31a6e13928c2450d42684b6adf56996e04e36430af272b8c0b1ed1dd57b6146775532c53634ecb5b600000000000000000000000000ffff44437a064e1fb8a150e6472bc281b51537dda3d077264786f207d05c4ec8d314b5883e32b14b15077bf39ccf5d92543a6fd0ee23897802e543987843fbd195e84ff3a28637ab2231d98b010100a30559b537a5cf3ed95e48d8c006d53de9e049dbf9a1120000008a01000009de4fceb4b2b1ab89bc498f4caca25247813558f17e26eb6c6054ef28688906fe592122fffeb92cbd77956c86811e91b0ed60d5a072dbb3d402c90b4361af3f81d70256b9757f6707488b92e518d616a9cf223aa571cd66e73e7c1236f85dc66ff3a1f817422025f9894edeabb8639ebb57d968a3f5fd3bc5ceb7c661cc34273033306f00e1e3878289f50ae34e3d72691aff06fe77a52080f7a479fcdb79263e8c1ed9f00b2c64da2453cd9bc9de4c250c6fd40a0fc9b18b9fe62408dc14f29ce668b31b1e605ea286913109d87371338c9d545551119f821a939657e6f2491632c3ff1a6584536b4196c735f8331a47844337e0cb68c59388a1ea289e724443d00c1109ec72111ce417de8c49fea97279ee87434bc1f85707010cdbda71b22301af00000012c8d756322b07e0cf2f73ad46f5f25bf91c8b7749419d02a6ccca8da60dc488f5364ad36ce7990bab58abc9f2d480903668268322b7e5099d089ccb3d00000000000000000000000000ffff55d1f3064e1f89d3ff18d870c0b6c9605d986c2de92f44c48032408e183a1d268bad77570e30a6d057a026119d9d044c1ce29b518e91d69372e5829f2cc1c3a9882e466ae9761dfc40e5010100a3053fab9f8566e701ad9bdc75f6f0fe7aaba6008f061d0000008a01000009b1f4342c3ae24aaaae59a75c67fc9ad1e62d520ae3f9a11459aa73f7e8fb753170ff09a84bf2b4e29d54d9c346bee0a9498548bdfb78fef626caa32545a6a4d4bbca76f29274761340c446e9e7d3c8940d296a7abb8f4d4d315a19fe11dd5bdd23a95704aa8cbd08f66e671b27e3782a5f5d8c5cc4f625ae1654b06e3888daaa33306f00e1e3878289f50ae34e3d72691aff06fe77a52080f7a479fcdb79263e8c1ed9f00b2c64da2453cd9bc9de4c250c6fd40a0fc9b18b9fe62408dc14f29ce668b31b1e605ea286913109d87371338c9d545551119f821a939657e6f2491632c3ff1a6584536b4196c735f8331a47844337e0cb68c59388a1ea289e724443d00c1109ec72111ce417de8c49fea97279ee87434bc1f85707010cdbda71b223", + "target": { + "network": 1, + "height": 1576768, + "block_hash": "00000021a51acdce0ba4c5f1bff3fd121143e7ab958f9f30bd1de4856b449285", + "masternode_root": "28736666198c9a8d0564edea185ac0e35fd4e315e384aea716b7fa2e51ea4ddf", + "quorum_root": "5089bed551b9264832e236dc491161faf7c94df288f4e8b37472663fb4b2f2e1" + } +} diff --git a/packages/rs-core-proof/tests/synthetic.rs b/packages/rs-core-proof/tests/synthetic.rs new file mode 100644 index 00000000000..c65d7d657dd --- /dev/null +++ b/packages/rs-core-proof/tests/synthetic.rs @@ -0,0 +1,556 @@ +//! Chains built from scratch with known BLS keys, to reach rejections no +//! valid chain data can: a repeated signer, a non-ChainLock quorum, a v4 +//! coinbase from end to end, and each envelope and commitment limit. +use blst::min_pk::SecretKey; +use dash_core_proof::{ + bootstrap, coinbase_roots, header_hash, parse_commitment, sha256d, Certificate, ErrorKind, + Hash, Link, MerklePath, ProofNetwork, ProofTransaction, QuorumProofChain, State, +}; + +const DST: &[u8] = b"BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_NUL_"; +/// Testnet's ChainLock quorum type, LLMQ_50_60. +const CHAINLOCK: u8 = 1; +const CHECKPOINT_HEIGHT: u32 = 1_000_000; + +fn compact(out: &mut Vec, n: usize) { + match n { + 0..=252 => out.push(n as u8), + 253..=0xffff => { + out.push(253); + out.extend((n as u16).to_le_bytes()); + } + _ => unreachable!("test helper encodes at most u16::MAX"), + } +} + +fn node(left: &Hash, right: &Hash) -> Hash { + let mut pair = [0; 64]; + pair[..32].copy_from_slice(left); + pair[32..].copy_from_slice(right); + sha256d(&pair) +} + +/// A commitment's parts, so tests can break one rule at a time. +#[derive(Clone)] +struct CommitmentSpec { + version: u16, + quorum_type: u8, + quorum_hash: Hash, + quorum_index: Option, + size: usize, + signers: usize, + padding: bool, + public_key: [u8; 48], +} + +impl CommitmentSpec { + fn new(quorum_type: u8, quorum_hash: Hash, public_key: [u8; 48]) -> Self { + let (size, threshold, rotated) = match quorum_type { + 1 => (50, 30, false), + 4 => (100, 67, false), + 5 => (60, 45, true), + 6 => (25, 17, false), + _ => unreachable!("test helper covers the types it uses"), + }; + Self { + version: if rotated { 4 } else { 3 }, + quorum_type, + quorum_hash, + quorum_index: rotated.then_some(0), + size, + signers: threshold, + padding: false, + public_key, + } + } + + fn encode(&self) -> Vec { + let mut out = Vec::new(); + out.extend(self.version.to_le_bytes()); + out.push(self.quorum_type); + out.extend(self.quorum_hash); + if let Some(index) = self.quorum_index { + out.extend(index.to_le_bytes()); + } + for _ in 0..2 { + compact(&mut out, self.size); + let mut bits = vec![0u8; self.size.div_ceil(8)]; + for i in 0..self.signers { + bits[i / 8] |= 1 << (i % 8); + } + if self.padding { + *bits.last_mut().unwrap() |= 0x80; + } + out.extend(bits); + } + out.extend(self.public_key); + out.extend([7; 32]); // verification vector hash + out.extend([0; 192]); // null quorum and members' signatures + out + } +} + +struct Quorum { + secret: SecretKey, + quorum_type: u8, + quorum_hash: Hash, +} + +impl Quorum { + fn new(seed: u8, quorum_type: u8) -> Self { + Self { + secret: SecretKey::key_gen(&[seed; 32], &[]).unwrap(), + quorum_type, + quorum_hash: [seed; 32], + } + } + + fn commitment(&self) -> Vec { + let public_key = self.secret.sk_to_pk().compress(); + CommitmentSpec::new(self.quorum_type, self.quorum_hash, public_key).encode() + } + + /// A ChainLock for `header` at `height`, computed independently of the crate. + fn certify(&self, height: u32, header: [u8; 80]) -> Certificate { + let mut request = b"\x05clsig".to_vec(); + request.extend(height.to_le_bytes()); + let mut message = vec![self.quorum_type]; + message.extend(self.quorum_hash); + message.extend(sha256d(&request)); + message.extend(header_hash(&header)); + let signature = self.secret.sign(&sha256d(&message), DST, &[]).compress(); + Certificate { + height, + header, + signature, + } + } +} + +fn header(prev: Hash, merkle_root: Hash) -> [u8; 80] { + let mut header = [0; 80]; + header[..4].copy_from_slice(&4u32.to_le_bytes()); + header[4..36].copy_from_slice(&prev); + header[36..68].copy_from_slice(&merkle_root); + header +} + +fn mining_transaction(height: u32, commitment: &[u8]) -> Vec { + let mut payload = 1u16.to_le_bytes().to_vec(); + payload.extend(height.to_le_bytes()); + payload.extend(commitment); + let mut tx = Vec::new(); + tx.extend(3u16.to_le_bytes()); + tx.extend(6u16.to_le_bytes()); + tx.extend([0, 0]); // no inputs or outputs + tx.extend(0u32.to_le_bytes()); + compact(&mut tx, payload.len()); + tx.extend(payload); + tx +} + +fn coinbase(version: u16, height: u32, roots: (Hash, Hash), chainlock_diff: u8) -> Vec { + let mut payload = version.to_le_bytes().to_vec(); + payload.extend(height.to_le_bytes()); + payload.extend(roots.0); + payload.extend(roots.1); + payload.push(chainlock_diff); + payload.extend([0; 96]); // null best ChainLock signature + payload.extend(0u64.to_le_bytes()); + if version >= 4 { + payload.extend([9; 32]); // merkleRootAssetUnlocks + } + let mut tx = Vec::new(); + tx.extend(3u16.to_le_bytes()); + tx.extend(5u16.to_le_bytes()); + compact(&mut tx, 1); + tx.extend([0; 32]); + tx.extend(u32::MAX.to_le_bytes()); + compact(&mut tx, 2); + tx.extend([1, 2]); + tx.extend(u32::MAX.to_le_bytes()); + compact(&mut tx, 1); + tx.extend(0u64.to_le_bytes()); + compact(&mut tx, 0); + tx.extend(0u32.to_le_bytes()); + compact(&mut tx, payload.len()); + tx.extend(payload); + tx +} + +/// A link certifying the block that mines `next`, a block with a dummy +/// coinbase at index 0 and the commitment at index 1. +fn link(signer: &Quorum, next_commitment: &[u8], height: u32) -> Link { + let dummy = [0x42; 100]; + let mining = mining_transaction(height, next_commitment); + let root = node(&sha256d(&dummy), &sha256d(&mining)); + Link { + certificate: signer.certify(height, header([height as u8; 32], root)), + mining: ProofTransaction { + transaction: mining, + path: MerklePath { + index: 1, + count: 2, + siblings: vec![sha256d(&dummy)], + }, + }, + ancestors: Vec::new(), + } +} + +struct Built { + checkpoint: State, + chain: QuorumProofChain, + record: Vec, +} + +/// A testnet chain: the checkpoint's ChainLock quorum `seed`, one link per +/// further quorum, and a target whose quorum root is the single `record`. +fn build(seed: &Quorum, next: &[&Quorum], coinbase_version: u16) -> Built { + let seed_commitment = seed.commitment(); + let checkpoint = State { + network: ProofNetwork::Testnet, + height: CHECKPOINT_HEIGHT, + block_hash: [1; 32], + masternode_root: [2; 32], + quorum_root: sha256d(&seed_commitment), + }; + let mut signer = seed; + let mut links = Vec::new(); + let mut height = CHECKPOINT_HEIGHT; + for quorum in next { + height += 24; + links.push(link(signer, &quorum.commitment(), height)); + signer = quorum; + } + let record = Quorum::new(0xee, 6).commitment(); + let target_height = height + 10; + let cb = coinbase( + coinbase_version, + target_height, + ([3; 32], sha256d(&record)), + 0, + ); + let target = signer.certify(target_height, header([0xaa; 32], sha256d(&cb))); + let chain = QuorumProofChain { + anchor: checkpoint.clone(), + seed: seed_commitment, + seed_path: MerklePath { + index: 0, + count: 1, + siblings: Vec::new(), + }, + links, + target, + coinbase: ProofTransaction { + transaction: cb, + path: MerklePath { + index: 0, + count: 1, + siblings: Vec::new(), + }, + }, + }; + Built { + checkpoint, + chain, + record, + } +} + +/// A bootstrap envelope around `chain` with raw `(kind, leaf, path)` records. +fn envelope(chain: &QuorumProofChain, count: u8, records: &[(u8, &[u8])]) -> Vec { + let proof = chain.encode().unwrap(); + let mut out = (proof.len() as u32).to_le_bytes().to_vec(); + out.extend(proof); + out.push(count); + for (kind, leaf) in records { + out.push(*kind); + out.extend((leaf.len() as u32).to_le_bytes()); + out.extend(*leaf); + out.extend(0u32.to_le_bytes()); // index + out.extend(1u32.to_le_bytes()); // count + out.push(0); // no siblings + } + out +} + +#[test] +fn should_verify_chains_ending_in_v3_and_v4_coinbases() { + let (a, b, c) = ( + Quorum::new(1, CHAINLOCK), + Quorum::new(2, CHAINLOCK), + Quorum::new(3, CHAINLOCK), + ); + for (next, version) in [(vec![], 3), (vec![&b, &c], 3), (vec![&b, &c], 4)] { + let built = build(&a, &next, version); + let state = built.chain.verify(&built.checkpoint).unwrap(); + assert_eq!(state.quorum_root, sha256d(&built.record)); + assert_eq!(state.height, built.chain.target.height); + + let bytes = envelope(&built.chain, 1, &[(0, &built.record)]); + let verified = bootstrap::verify(&bytes, &built.checkpoint, state.height).unwrap(); + assert_eq!(verified.state(), &state); + assert_eq!( + parse_commitment(verified.records()[0].leaf) + .unwrap() + .quorum_type, + 6 + ); + } +} + +#[test] +fn should_reject_a_link_that_mines_its_own_signer() { + let (a, b) = (Quorum::new(1, CHAINLOCK), Quorum::new(2, CHAINLOCK)); + let mut built = build(&a, &[&b], 3); + built.chain.links[0] = link(&a, &a.commitment(), CHECKPOINT_HEIGHT + 24); + assert_eq!( + built.chain.verify(&built.checkpoint).unwrap_err().message(), + "redundant signer" + ); +} + +#[test] +fn should_reject_signers_that_are_not_chainlock_quorums() { + let platform = Quorum::new(9, 6); + let built = build(&platform, &[], 3); + assert_eq!( + built.chain.verify(&built.checkpoint).unwrap_err().message(), + "unexpected quorum type" + ); + + let a = Quorum::new(1, CHAINLOCK); + let built = build(&a, &[&platform], 3); + assert_eq!( + built.chain.verify(&built.checkpoint).unwrap_err().message(), + "unexpected quorum type" + ); + + // Mainnet ChainLocks are LLMQ_400_60, so a testnet type is wrong there. + let mut built = build(&a, &[], 3); + built.checkpoint.network = ProofNetwork::Mainnet; + built.checkpoint.height = 2_000_000; + built.chain.anchor = built.checkpoint.clone(); + assert_eq!( + built.chain.verify(&built.checkpoint).unwrap_err().message(), + "unexpected quorum type" + ); +} + +#[test] +fn should_reject_envelope_record_limits() { + let a = Quorum::new(1, CHAINLOCK); + let built = build(&a, &[], 3); + let ok = |count, records: &[(u8, &[u8])]| { + bootstrap::verify( + &envelope(&built.chain, count, records), + &built.checkpoint, + 0, + ) + .map(|_| ()) + .map_err(|e| e.message()) + }; + assert_eq!(ok(1, &[(0, &built.record)]), Ok(())); + assert_eq!(ok(0, &[]), Err("projection count")); + assert_eq!(ok(17, &[]), Err("projection count")); + assert_eq!(ok(1, &[(2, &built.record)]), Err("record kind")); + assert_eq!(ok(1, &[(0, &[])]), Err("record leaf size")); + assert_eq!(ok(1, &[(0, &[5; 64])]), Err("record leaf size")); + assert_eq!(ok(1, &[(0, &[5; 4097])]), Err("blob limit")); + // The record must open under the right root. + assert_eq!(ok(1, &[(1, &built.record)]), Err("Merkle root/path")); + // A stale target is refused before any signature is checked: with a + // broken signature it is still reported stale, and only a reachable + // minimum height gets as far as the signature. + let mut broken = built.chain.clone(); + broken.target.signature = [0; 96]; + let bytes = envelope(&broken, 1, &[(0, &built.record)]); + let height = broken.target.height; + let stale = bootstrap::verify(&bytes, &built.checkpoint, height + 1).unwrap_err(); + assert_eq!(stale.message(), "stale target"); + assert_eq!(stale.kind(), ErrorKind::StaleTarget); + let reached = bootstrap::verify(&bytes, &built.checkpoint, height).unwrap_err(); + assert_eq!(reached.message(), "BLS signature"); + assert_eq!(reached.kind(), ErrorKind::Invalid); +} + +/// A link whose certified block is `ancestors` blocks above the block that +/// mined `next`; the ancestor headers are linked by their hashes. +fn link_through_ancestors( + signer: &Quorum, + next_commitment: &[u8], + height: u32, + ancestors: u32, +) -> Link { + let mined = link(signer, next_commitment, height - ancestors); + let mut headers = vec![mined.certificate.header]; + for _ in 1..ancestors { + let parent = header_hash(headers.last().unwrap()); + headers.push(header(parent, [0x33; 32])); + } + let certified = header(header_hash(headers.last().unwrap()), [0x44; 32]); + Link { + certificate: signer.certify(height, certified), + mining: mined.mining, + ancestors: headers, + } +} + +#[test] +fn should_accept_a_signer_mined_before_the_checkpoint_through_ancestors() { + // The trust model covers this: an honest checkpoint quorum certifies a + // block just after the checkpoint, and ancestor headers reach a quorum + // mined 10 blocks before it, which then signs the target. Its key is + // trusted although it was never in the checkpoint's quorum root. + let (a, old) = (Quorum::new(1, CHAINLOCK), Quorum::new(7, CHAINLOCK)); + let mut built = build(&a, &[&old], 3); + built.chain.links[0] = link_through_ancestors(&a, &old.commitment(), CHECKPOINT_HEIGHT + 1, 11); + assert_eq!( + built.chain.links[0].certificate.height - 11, + CHECKPOINT_HEIGHT - 10 + ); + // The target was signed by `old` for a height above the link, so it + // still verifies. + built.chain.target = old.certify(built.chain.target.height, built.chain.target.header); + assert!(built.chain.verify(&built.checkpoint).is_ok()); + + // Breaking one ancestor link breaks the handoff. + built.chain.links[0].ancestors[3][0] ^= 1; + assert_eq!( + built.chain.verify(&built.checkpoint).unwrap_err().message(), + "ancestor continuity" + ); +} + +#[test] +fn should_refuse_to_encode_chains_core_would_not_decode() { + let a = Quorum::new(1, CHAINLOCK); + let mut built = build(&a, &[], 3); + built.chain.seed.resize(1025, 0); + assert_eq!( + built.chain.encode().unwrap_err().message(), + "proof blob length" + ); + + // Full-size links fail at the first one past the 1 MiB budget. + let mut built = build(&a, &[], 3); + let mut big = link( + &a, + &Quorum::new(2, CHAINLOCK).commitment(), + CHECKPOINT_HEIGHT + 1, + ); + big.mining.transaction.resize(100_000, 0); + built.chain.links = vec![big; 12]; + assert_eq!( + built.chain.encode().unwrap_err().message(), + "proof size limit" + ); +} + +#[test] +fn should_classify_an_untrusted_checkpoint() { + let a = Quorum::new(1, CHAINLOCK); + let built = build(&a, &[], 3); + let mut other = built.checkpoint.clone(); + other.height += 1; + assert_eq!( + built.chain.verify(&other).unwrap_err().kind(), + ErrorKind::UntrustedCheckpoint + ); +} + +#[test] +fn should_reject_coinbase_chainlock_diff_reaching_the_block() { + let roots = ([3; 32], [4; 32]); + assert_eq!( + coinbase_roots(&coinbase(3, 200, roots, 199), 200), + Ok(roots) + ); + assert_eq!( + coinbase_roots(&coinbase(3, 200, roots, 200), 200) + .unwrap_err() + .message(), + "coinbase ChainLock height" + ); + assert_eq!( + coinbase_roots(&coinbase(3, 200, ([3; 32], [0; 32]), 0), 200) + .unwrap_err() + .message(), + "coinbase quorum root" + ); + assert_eq!( + coinbase_roots(&coinbase(2, 200, roots, 0), 200) + .unwrap_err() + .message(), + "coinbase payload version/height" + ); +} + +#[test] +fn should_reject_commitments_core_rejects() { + let key = SecretKey::key_gen(&[1; 32], &[]) + .unwrap() + .sk_to_pk() + .compress(); + let rejects = |spec: CommitmentSpec| parse_commitment(&spec.encode()).unwrap_err().message(); + let platform = CommitmentSpec::new(6, [5; 32], key); + let rotated = CommitmentSpec::new(5, [5; 32], key); + assert!(parse_commitment(&platform.encode()).is_ok()); + assert!(parse_commitment(&rotated.encode()).is_ok()); + + assert_eq!( + rejects(CommitmentSpec { + signers: platform.signers - 1, + ..platform.clone() + }), + "null/undersized quorum" + ); + assert_eq!( + rejects(CommitmentSpec { + padding: true, + ..platform.clone() + }), + "bitset padding" + ); + assert_eq!( + rejects(CommitmentSpec { + size: 24, + signers: 17, + ..platform.clone() + }), + "quorum bit count" + ); + assert_eq!( + rejects(CommitmentSpec { + quorum_hash: [0; 32], + ..platform.clone() + }), + "null quorum" + ); + assert_eq!( + rejects(CommitmentSpec { + version: 4, + quorum_index: Some(0), + ..platform.clone() + }), + "commitment rotation version" + ); + assert_eq!( + rejects(CommitmentSpec { + version: 3, + quorum_index: None, + ..rotated.clone() + }), + "commitment rotation version" + ); + for index in [32, 0x8000] { + assert_eq!( + rejects(CommitmentSpec { + quorum_index: Some(index), + ..rotated.clone() + }), + "quorum index" + ); + } +} diff --git a/packages/rs-core-proof/tests/verification.rs b/packages/rs-core-proof/tests/verification.rs new file mode 100644 index 00000000000..4c12a129954 --- /dev/null +++ b/packages/rs-core-proof/tests/verification.rs @@ -0,0 +1,216 @@ +//! Real proofs from Dash Core 23.1.8 nodes' `getquorumproofchain`. +//! +//! `tests/data/.json` holds the checkpoint (from `getblock`), the RPC's +//! `proof_hex`, `bootstrap_hex` and `target`. Each request opened one Platform +//! quorum (LLMQ_100_67 on mainnet, LLMQ_25_67 on testnet) and two EvoNodes. +//! The mainnet chain has 2 links and the testnet one 4, two of which were +//! mined in blocks below the certified ones (2 and 3 ancestor headers). +//! `testnet-no-links.json` is a proof from a recent checkpoint, signed by the +//! checkpoint's own ChainLock quorum. +use dash_core_proof::bootstrap::{self, RecordKind}; +use dash_core_proof::{parse_commitment, rpc_hex, QuorumProofChain, State, MAX_PROOF_HEADERS}; + +struct Fixture { + checkpoint: State, + proof: Vec, + bootstrap: Vec, + target: State, +} + +fn fixture(json: &str) -> Fixture { + let value: serde_json::Value = serde_json::from_str(json).unwrap(); + let hex = |key: &str| hex::decode(value[key].as_str().unwrap()).unwrap(); + Fixture { + checkpoint: serde_json::from_value(value["checkpoint"].clone()).unwrap(), + proof: hex("proof_hex"), + bootstrap: hex("bootstrap_hex"), + target: serde_json::from_value(value["target"].clone()).unwrap(), + } +} + +fn mainnet() -> Fixture { + fixture(include_str!("data/mainnet.json")) +} + +fn testnet() -> Fixture { + fixture(include_str!("data/testnet.json")) +} + +#[test] +fn should_verify_core_proofs_to_the_target_core_reported() { + for (f, links, platform_quorum_type) in [(mainnet(), 2, 4), (testnet(), 4, 6)] { + let chain = QuorumProofChain::decode(&f.proof).unwrap(); + assert_eq!(chain.links.len(), links); + assert_eq!(chain.encode().unwrap(), f.proof, "decoding is canonical"); + assert_eq!(chain.verify(&f.checkpoint).unwrap(), f.target); + + let verified = bootstrap::verify(&f.bootstrap, &f.checkpoint, f.target.height).unwrap(); + assert_eq!(verified.state(), &f.target); + let kinds: Vec<_> = verified.records().iter().map(|r| r.kind).collect(); + assert_eq!( + kinds, + [ + RecordKind::Quorum, + RecordKind::Masternode, + RecordKind::Masternode + ] + ); + let quorum = parse_commitment(verified.records()[0].leaf).unwrap(); + assert_eq!(quorum.quorum_type, platform_quorum_type); + } +} + +#[test] +fn should_open_the_requested_mainnet_quorum() { + let f = mainnet(); + let verified = bootstrap::verify(&f.bootstrap, &f.checkpoint, 0).unwrap(); + let quorum = parse_commitment(verified.records()[0].leaf).unwrap(); + assert_eq!( + rpc_hex(&quorum.quorum_hash), + "000000000000001e3234155b7645fdd5af10c59bd1507ee6d75ded0ae4eab979" + ); +} + +#[test] +fn should_reject_a_stale_target_or_another_checkpoint() { + let f = testnet(); + let stale = bootstrap::verify(&f.bootstrap, &f.checkpoint, f.target.height + 1); + assert_eq!(stale.unwrap_err().message(), "stale target"); + + let mut other = f.checkpoint.clone(); + other.quorum_root[0] ^= 1; + assert_eq!( + bootstrap::verify(&f.bootstrap, &other, 0) + .unwrap_err() + .message(), + "untrusted snapshot" + ); + // A checkpoint on another network never matches either. + let mainnet = mainnet(); + assert!(bootstrap::verify(&f.bootstrap, &mainnet.checkpoint, 0).is_err()); +} + +#[test] +fn should_reject_every_truncation() { + for f in [mainnet(), testnet()] { + for n in 0..f.proof.len() { + assert!( + QuorumProofChain::decode(&f.proof[..n]).is_err(), + "proof prefix {n}" + ); + } + // Past the chain, each prefix costs a full verification; a stride + // still cuts inside every record field. + for n in (0..f.bootstrap.len()).step_by(7) { + assert!( + bootstrap::verify(&f.bootstrap[..n], &f.checkpoint, 0).is_err(), + "bootstrap prefix {n}" + ); + } + } +} + +#[test] +fn should_reject_byte_flips_across_the_envelope() { + let f = testnet(); + // Every 61st byte reaches each section of the envelope; each flip is checked + // through full verification. + for i in (0..f.bootstrap.len()).step_by(61) { + let mut corrupt = f.bootstrap.clone(); + corrupt[i] ^= 0x01; + assert!( + bootstrap::verify(&corrupt, &f.checkpoint, 0).is_err(), + "flip at {i}" + ); + } +} + +#[test] +fn should_reject_tampered_links() { + let f = testnet(); + let chain = QuorumProofChain::decode(&f.proof).unwrap(); + let with_ancestors = chain + .links + .iter() + .position(|link| link.ancestors.len() > 1) + .unwrap(); + let rejects = |mutate: &dyn Fn(&mut QuorumProofChain)| { + let mut bad = chain.clone(); + mutate(&mut bad); + bad.verify(&f.checkpoint).unwrap_err().message() + }; + + assert_eq!(rejects(&|c| c.target.signature[5] ^= 1), "BLS signature"); + assert_eq!( + rejects(&|c| c.links[with_ancestors].ancestors.clear()), + "Merkle root/path" + ); + assert_eq!( + rejects(&|c| c.links[with_ancestors].ancestors[0][0] ^= 1), + "ancestor continuity" + ); + assert_eq!( + rejects(&|c| { + c.links[with_ancestors].ancestors.remove(0); + }), + "Merkle root/path" + ); + assert_eq!( + rejects(&|c| { + c.links.remove(0); + }), + "BLS verification" + ); + assert_eq!( + rejects(&|c| c.target.height = c.links.last().unwrap().certificate.height), + "non-increasing certificate" + ); + // The seed's verification vector hash: still a well-formed commitment, + // but no longer the one under the checkpoint's quorum root. + assert_eq!( + rejects(&|c| { + let at = c.seed.len() - 224; + c.seed[at] ^= 1; + }), + "Merkle root/path" + ); + assert_eq!( + rejects(&|c| c.coinbase.path.index = 1), + "coinbase inclusion" + ); + assert_eq!( + rejects(&|c| c.links[0].mining.path.index = 0), + "mining inclusion" + ); + // Replaying a link is caught by the certificate heights. + assert_eq!( + rejects(&|c| { + let first = c.links[0].clone(); + c.links.insert(0, first); + }), + "non-increasing certificate" + ); +} + +#[test] +fn should_reject_chains_over_the_header_budget() { + let f = testnet(); + let mut chain = QuorumProofChain::decode(&f.proof).unwrap(); + let header = chain.links[0].certificate.header; + chain.links[0].ancestors = vec![header; MAX_PROOF_HEADERS + 1]; + assert!(chain.encode().is_err()); + assert_eq!( + chain.verify(&f.checkpoint).unwrap_err().message(), + "ancestor budget/height" + ); +} + +#[test] +fn should_verify_a_proof_signed_by_the_checkpoint_quorum_itself() { + let f = fixture(include_str!("data/testnet-no-links.json")); + let chain = QuorumProofChain::decode(&f.proof).unwrap(); + assert!(chain.links.is_empty()); + assert_eq!(chain.verify(&f.checkpoint).unwrap(), f.target); + let verified = bootstrap::verify(&f.bootstrap, &f.checkpoint, f.target.height).unwrap(); + assert_eq!(verified.state(), &f.target); +}