From 068c3f24e96f2571630dbe44db445084d60a6cb2 Mon Sep 17 00:00:00 2001 From: Liu-Cheng Xu Date: Tue, 25 Feb 2025 07:40:46 +0800 Subject: [PATCH 01/53] Fix `is_invalid_use_of_sighash_single()` incompatibility with Bitcoin Core --- bitcoin/src/crypto/sighash.rs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/bitcoin/src/crypto/sighash.rs b/bitcoin/src/crypto/sighash.rs index f27dbb27b..17cc0a37c 100644 --- a/bitcoin/src/crypto/sighash.rs +++ b/bitcoin/src/crypto/sighash.rs @@ -400,6 +400,16 @@ impl EcdsaSighashType { } } + /// Checks if the sighash type is [`Self::Single`] or [`Self::SinglePlusAnyoneCanPay`]. + /// + /// This matches Bitcoin Core's behavior where SIGHASH_SINGLE bug check is based on the base + /// type (after masking with 0x1f), regardless of the ANYONECANPAY flag. + /// + /// See: + pub fn is_single(&self) -> bool { + matches!(self, Self::Single | Self::SinglePlusAnyoneCanPay) + } + /// Creates a [`EcdsaSighashType`] from a raw `u32`. /// /// **Note**: this replicates consensus behaviour, for current standardness rules correctness @@ -1316,7 +1326,7 @@ impl std::error::Error for AnnexError { fn is_invalid_use_of_sighash_single(sighash: u32, input_index: usize, outputs_len: usize) -> bool { let ty = EcdsaSighashType::from_consensus(sighash); - ty == EcdsaSighashType::Single && input_index >= outputs_len + ty.is_single() && input_index >= outputs_len } /// Result of [`SighashCache::legacy_encode_signing_data_to`]. From 18c2cad578fd4383411f1b958caa90217d78d590 Mon Sep 17 00:00:00 2001 From: Liu-Cheng Xu Date: Tue, 25 Feb 2025 07:38:36 +0800 Subject: [PATCH 02/53] Add test for sighash_single_bug incompatility fix --- bitcoin/src/crypto/sighash.rs | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/bitcoin/src/crypto/sighash.rs b/bitcoin/src/crypto/sighash.rs index 17cc0a37c..294dac70e 100644 --- a/bitcoin/src/crypto/sighash.rs +++ b/bitcoin/src/crypto/sighash.rs @@ -1463,8 +1463,6 @@ mod tests { #[test] fn sighash_single_bug() { - const SIGHASH_SINGLE: u32 = 3; - // We need a tx with more inputs than outputs. let tx = Transaction { version: transaction::Version::ONE, @@ -1475,10 +1473,16 @@ mod tests { let script = ScriptBuf::new(); let cache = SighashCache::new(&tx); - let got = cache.legacy_signature_hash(1, &script, SIGHASH_SINGLE).expect("sighash"); - let want = LegacySighash::from_slice(&UINT256_ONE).unwrap(); + let sighash_single = 3; + let got = cache.legacy_signature_hash(1, &script, sighash_single).expect("sighash"); + let want = LegacySighash::from_byte_array(UINT256_ONE); + assert_eq!(got, want); - assert_eq!(got, want) + // https://github.com/rust-bitcoin/rust-bitcoin/issues/4112 + let sighash_single = 131; + let got = cache.legacy_signature_hash(1, &script, sighash_single).expect("sighash"); + let want = LegacySighash::from_byte_array(UINT256_ONE); + assert_eq!(got, want); } #[test] From 39e280a2b61fb10900c483485dbceb5296b87020 Mon Sep 17 00:00:00 2001 From: Martin Habovstiak Date: Fri, 21 Feb 2025 14:59:55 +0100 Subject: [PATCH 03/53] Fix key/script spend detection in `Witness` The `taproot_control_block` did not properly detect whether it deals with script spend or key spend. As a result, if key spend with annex was used it'd return the first element (the signature) as if it was a control block. Further, the conditions identifying which kind of spend it was were repeated multiple times but behaved subtly differently making only `taproot_control_block` buggy but the other places confusing. To resolve these issues this change adds a `P2TrSpend` enum that represents a parsed witness and has a single method doing all the parsing. The other methods can then be trivially implemented by matching on that type. This way only one place needs to be verified and the parsing code is more readable since it uses one big `match` to handle all possibilities. The downside of this is a potential perf impact if the parsing code doesn't get inlined since the common parsing code has to shuffle around data that the caller is not intersted in. I don't think this will be a problem but if it will I suppose it will be solvable (e.g. by using `#[inline(always)]`). The enum also looks somewhat nice and perhaps downstream consumers could make use of it. This change does not expose it yet but is written such that after exposing it the API would be (mostly) idiomatic. Closes #4097 --- bitcoin/src/blockdata/witness.rs | 141 ++++++++++++++++++++++--------- 1 file changed, 103 insertions(+), 38 deletions(-) diff --git a/bitcoin/src/blockdata/witness.rs b/bitcoin/src/blockdata/witness.rs index 0737a678a..024418a0e 100644 --- a/bitcoin/src/blockdata/witness.rs +++ b/bitcoin/src/blockdata/witness.rs @@ -399,57 +399,40 @@ impl Witness { /// /// This does not guarantee that this represents a P2TR [`Witness`]. It /// merely gets the second to last or third to last element depending on - /// the first byte of the last element being equal to 0x50. See - /// [Script::is_p2tr](crate::blockdata::script::Script::is_p2tr) to - /// check whether this is actually a Taproot witness. + /// the first byte of the last element being equal to 0x50. + /// + /// See [`Script::is_p2tr`] to check whether this is actually a Taproot witness. pub fn tapscript(&self) -> Option<&Script> { - if self.is_empty() { - return None; - } - - if self.taproot_annex().is_some() { - self.third_to_last().map(Script::from_bytes) - } else { - self.second_to_last().map(Script::from_bytes) - } + match P2TrSpend::from_witness(self) { + // Note: the method is named "tapscript" but historically it was actually returning + // leaf script. This is broken but we now keep the behavior the same to not subtly + // break someone. + Some(P2TrSpend::Script { leaf_script, .. }) => Some(leaf_script), + _ => None, + } } /// Get the taproot control block following BIP341 rules. /// /// This does not guarantee that this represents a P2TR [`Witness`]. It /// merely gets the last or second to last element depending on the first - /// byte of the last element being equal to 0x50. See - /// [Script::is_p2tr](crate::blockdata::script::Script::is_p2tr) to - /// check whether this is actually a Taproot witness. + /// byte of the last element being equal to 0x50. + /// + /// See [`Script::is_p2tr`] to check whether this is actually a Taproot witness. pub fn taproot_control_block(&self) -> Option<&[u8]> { - if self.is_empty() { - return None; - } - - if self.taproot_annex().is_some() { - self.second_to_last() - } else { - self.last() - } + match P2TrSpend::from_witness(self) { + Some(P2TrSpend::Script { control_block, .. }) => Some(control_block), + _ => None, + } } /// Get the taproot annex following BIP341 rules. /// - /// This does not guarantee that this represents a P2TR [`Witness`]. See - /// [Script::is_p2tr](crate::blockdata::script::Script::is_p2tr) to - /// check whether this is actually a Taproot witness. + /// This does not guarantee that this represents a P2TR [`Witness`]. + /// + /// See [`Script::is_p2tr`] to check whether this is actually a Taproot witness. pub fn taproot_annex(&self) -> Option<&[u8]> { - self.last().and_then(|last| { - // From BIP341: - // If there are at least two witness elements, and the first byte of - // the last element is 0x50, this last element is called annex a - // and is removed from the witness stack. - if self.len() >= 2 && last.first() == Some(&TAPROOT_ANNEX_PREFIX) { - Some(last) - } else { - None - } - }) + P2TrSpend::from_witness(self)?.annex() } /// Get the p2wsh witness script following BIP141 rules. @@ -468,6 +451,88 @@ impl Index for Witness { fn index(&self, index: usize) -> &Self::Output { self.nth(index).expect("Out of Bounds") } } +/// Represents a possible Taproot spend. +/// +/// Taproot can be spent as key spend or script spend and, depending on which it is, different data +/// is in the witness. This type helps representing that data more cleanly when parsing the witness +/// because there are a lot of conditions that make reasoning hard. It's better to parse it at one +/// place and pass it along. +/// +/// This type is so far private but it could be published eventually. The design is geared towards +/// it but it's not fully finished. +enum P2TrSpend<'a> { + Key { + // This field is technically present in witness in case of key spend but none of our code + // uses it yet. Rather than deleting it, it's kept here commented as documentation and as + // an easy way to add it if anything needs it - by just uncommenting. + // signature: &'a [u8], + annex: Option<&'a [u8]>, + }, + Script { + leaf_script: &'a Script, + control_block: &'a [u8], + annex: Option<&'a [u8]>, + }, +} + +impl<'a> P2TrSpend<'a> { + /// Parses `Witness` to determine what kind of taproot spend this is. + /// + /// Note: this assumes `witness` is a taproot spend. The function cannot figure it out for sure + /// (without knowing the output), so it doesn't attempt to check anything other than what is + /// required for the program to not crash. + /// + /// In other words, if the caller is certain that the witness is a valid p2tr spend (e.g. + /// obtained from Bitcoin Core) then it's OK to unwrap this but not vice versa - `Some` does + /// not imply correctness. + fn from_witness(witness: &'a Witness) -> Option { + // BIP341 says: + // If there are at least two witness elements, and the first byte of + // the last element is 0x50, this last element is called annex a + // and is removed from the witness stack. + // + // However here we're not removing anything, so we have to adjust the numbers to account + // for the fact that annex is still there. + match witness.len() { + 0 => None, + 1 => Some(P2TrSpend::Key { /* signature: witness.last().expect("len > 0") ,*/ annex: None }), + 2 if witness.last().expect("len > 0").starts_with(&[TAPROOT_ANNEX_PREFIX]) => { + let spend = P2TrSpend::Key { + // signature: witness.second_to_last().expect("len > 1"), + annex: witness.last(), + }; + Some(spend) + }, + // 2 => this is script spend without annex - same as when there are 3+ elements and the + // last one does NOT start with TAPROOT_ANNEX_PREFIX. This is handled in the catchall + // arm. + 3.. if witness.last().expect("len > 0").starts_with(&[TAPROOT_ANNEX_PREFIX]) => { + let spend = P2TrSpend::Script { + leaf_script: Script::from_bytes(witness.third_to_last().expect("len > 2")), + control_block: witness.second_to_last().expect("len > 1"), + annex: witness.last(), + }; + Some(spend) + }, + _ => { + let spend = P2TrSpend::Script { + leaf_script: Script::from_bytes(witness.second_to_last().expect("len > 1")), + control_block: witness.last().expect("len > 0"), + annex: None, + }; + Some(spend) + }, + } + } + + fn annex(&self) -> Option<&'a [u8]> { + match self { + P2TrSpend::Key { annex, .. } => *annex, + P2TrSpend::Script { annex, .. } => *annex, + } + } +} + impl<'a> Iterator for Iter<'a> { type Item = &'a [u8]; From 74138d56b1a8bdeb0c9ce0084c630f4193a03dd3 Mon Sep 17 00:00:00 2001 From: Martin Habovstiak Date: Fri, 21 Feb 2025 15:34:39 +0100 Subject: [PATCH 04/53] Add a test case checking `taproot_control_block` The previous commit fixed a bug when `taproot_control_block` returned `Some` on key-spends. This adds a test case for it which succeeds when applied after the previous commit and fails if applied before it. --- bitcoin/src/blockdata/witness.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/bitcoin/src/blockdata/witness.rs b/bitcoin/src/blockdata/witness.rs index 024418a0e..de36c15eb 100644 --- a/bitcoin/src/blockdata/witness.rs +++ b/bitcoin/src/blockdata/witness.rs @@ -848,19 +848,24 @@ mod test { let control_block = hex!("02"); // annex starting with 0x50 causes the branching logic. let annex = hex!("50"); + let signature = vec![0xff; 64]; let witness_vec = vec![tapscript.clone(), control_block.clone()]; - let witness_vec_annex = vec![tapscript.clone(), control_block.clone(), annex]; + let witness_vec_annex = vec![tapscript.clone(), control_block.clone(), annex.clone()]; + let witness_vec_key_spend_annex = vec![signature, annex]; let witness_serialized: Vec = serialize(&witness_vec); let witness_serialized_annex: Vec = serialize(&witness_vec_annex); + let witness_serialized_key_spend_annex: Vec = serialize(&witness_vec_key_spend_annex); let witness = deserialize::(&witness_serialized[..]).unwrap(); let witness_annex = deserialize::(&witness_serialized_annex[..]).unwrap(); + let witness_key_spend_annex = deserialize::(&witness_serialized_key_spend_annex[..]).unwrap(); // With or without annex, the tapscript should be returned. assert_eq!(witness.taproot_control_block(), Some(&control_block[..])); assert_eq!(witness_annex.taproot_control_block(), Some(&control_block[..])); + assert!(witness_key_spend_annex.taproot_control_block().is_none()) } #[test] From 730baeb4e82a75b3e542124b0b34bfc325734f8b Mon Sep 17 00:00:00 2001 From: Martin Habovstiak Date: Fri, 21 Feb 2025 16:33:00 +0100 Subject: [PATCH 05/53] Add `taproot_leaf_script` methood to `Witness` We already have `tapscript` method on `Witness` which is broken because it doesn't check that the leaf script is a tapscript, however that behavior might have been intended by some consumers who want to inspect the script independent of the version. To resolve the confusion, we're going to add a new method that returns both the leaf script and, to avoid forgetting version check, also the leaf version. This doesn't touch the `tapscript` method yet to make backporting of this commit easier. It's also worth noting that leaf script is often used together with version. To make passing them around easier it'd be helpful to use a separate type. Thus this also adds a public POD type containing the script and the version. In anticipation of if being usable in different APIs it's also generic over the script type. Similarly to the `tapscript` method, this also only adds the type and doesn't change other functions to use it yet. Only the newly added `taproot_leaf_script` method uses it now. This is a part of #4073 --- bitcoin/src/blockdata/witness.rs | 44 +++++++++++++++++++++++++++++++- bitcoin/src/taproot/mod.rs | 11 +++++++- 2 files changed, 53 insertions(+), 2 deletions(-) diff --git a/bitcoin/src/blockdata/witness.rs b/bitcoin/src/blockdata/witness.rs index de36c15eb..6d159e0ae 100644 --- a/bitcoin/src/blockdata/witness.rs +++ b/bitcoin/src/blockdata/witness.rs @@ -14,7 +14,7 @@ use crate::consensus::encode::{Error, MAX_VEC_SIZE}; use crate::consensus::{Decodable, Encodable, WriteExt}; use crate::crypto::ecdsa; use crate::prelude::*; -use crate::taproot::{self, TAPROOT_ANNEX_PREFIX}; +use crate::taproot::{self, LeafScript, LeafVersion, TAPROOT_ANNEX_PREFIX, TAPROOT_CONTROL_BASE_SIZE, TAPROOT_LEAF_MASK}; use crate::{Script, VarInt}; /// The Witness is the data used to unlock bitcoin since the [segwit upgrade]. @@ -412,6 +412,22 @@ impl Witness { } } + /// Returns the leaf script with its version but without the merkle proof. + /// + /// This does not guarantee that this represents a P2TR [`Witness`]. It + /// merely gets the second to last or third to last element depending on + /// the first byte of the last element being equal to 0x50 and the associated + /// version. + pub fn taproot_leaf_script(&self) -> Option> { + match P2TrSpend::from_witness(self) { + Some(P2TrSpend::Script { leaf_script, control_block, .. }) if control_block.len() >= TAPROOT_CONTROL_BASE_SIZE => { + let version = LeafVersion::from_consensus(control_block[0] & TAPROOT_LEAF_MASK).ok()?; + Some(LeafScript { version, script: leaf_script, }) + }, + _ => None, + } + } + /// Get the taproot control block following BIP341 rules. /// /// This does not guarantee that this represents a P2TR [`Witness`]. It @@ -842,6 +858,32 @@ mod test { assert_eq!(witness_annex.tapscript(), None); } + #[test] + fn get_taproot_leaf_script() { + let tapscript = hex!("deadbeef"); + let control_block = hex!("c0ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"); + // annex starting with 0x50 causes the branching logic. + let annex = hex!("50"); + + let witness_vec = vec![tapscript.clone(), control_block.clone()]; + let witness_vec_annex = vec![tapscript.clone(), control_block, annex]; + + let witness_serialized: Vec = serialize(&witness_vec); + let witness_serialized_annex: Vec = serialize(&witness_vec_annex); + + let witness = deserialize::(&witness_serialized[..]).unwrap(); + let witness_annex = deserialize::(&witness_serialized_annex[..]).unwrap(); + + let expected_leaf_script = LeafScript { + version: LeafVersion::TapScript, + script: Script::from_bytes(&tapscript), + }; + + // With or without annex, the tapscript should be returned. + assert_eq!(witness.taproot_leaf_script().unwrap(), expected_leaf_script); + assert_eq!(witness_annex.taproot_leaf_script().unwrap(), expected_leaf_script); + } + #[test] fn test_get_control_block() { let tapscript = hex!("deadbeef"); diff --git a/bitcoin/src/taproot/mod.rs b/bitcoin/src/taproot/mod.rs index 60ce28ee0..656a3ccbf 100644 --- a/bitcoin/src/taproot/mod.rs +++ b/bitcoin/src/taproot/mod.rs @@ -158,7 +158,16 @@ pub const TAPROOT_CONTROL_BASE_SIZE: usize = 33; pub const TAPROOT_CONTROL_MAX_SIZE: usize = TAPROOT_CONTROL_BASE_SIZE + TAPROOT_CONTROL_NODE_SIZE * TAPROOT_CONTROL_MAX_NODE_COUNT; -// type alias for versioned tap script corresponding merkle proof +/// The leaf script with its version. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub struct LeafScript { + /// The version of the script. + pub version: LeafVersion, + /// The script, usually `ScriptBuf` or `&Script`. + pub script: S, +} + +// type alias for versioned tap script corresponding Merkle proof type ScriptMerkleProofMap = BTreeMap<(ScriptBuf, LeafVersion), BTreeSet>; /// Represents taproot spending information. From 9e87bc5b2cb39b6b7079507abfc0c1603a13d381 Mon Sep 17 00:00:00 2001 From: Martin Habovstiak Date: Fri, 21 Feb 2025 18:09:29 +0100 Subject: [PATCH 06/53] Deprecate the `Witness::tapscript` method Now that an alternative exists we can deprecate the method with an expalantion of what's going on. --- bitcoin/src/blockdata/witness.rs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/bitcoin/src/blockdata/witness.rs b/bitcoin/src/blockdata/witness.rs index 6d159e0ae..77f426d65 100644 --- a/bitcoin/src/blockdata/witness.rs +++ b/bitcoin/src/blockdata/witness.rs @@ -395,13 +395,22 @@ impl Witness { self.element_at(pos) } - /// Get Tapscript following BIP341 rules regarding accounting for an annex. + /// Get leaf script following BIP341 rules regarding accounting for an annex. + /// + /// This method is broken: it's called `tapscript` but it's actually returning a leaf script. + /// We're not going to fix it because someone might be relying on it thinking leaf script and + /// tapscript are the same thing (they are not). Instead, this is deprecated and will be + /// removed in the next breaking release. You need to use `taproot_leaf_script` and if you + /// intended to use it as leaf script, just access the `script` field of the returned type. If + /// you intended tapscript specifically you have to check the version first and bail if it's not + /// `LeafVersion::TapScript`. /// /// This does not guarantee that this represents a P2TR [`Witness`]. It /// merely gets the second to last or third to last element depending on /// the first byte of the last element being equal to 0x50. /// /// See [`Script::is_p2tr`] to check whether this is actually a Taproot witness. + #[deprecated = "use `taproot_leaf_script` and check leaf version, if applicable"] pub fn tapscript(&self) -> Option<&Script> { match P2TrSpend::from_witness(self) { // Note: the method is named "tapscript" but historically it was actually returning From 315750deb3a74f82f28c9b388f0f57a38ebd3e62 Mon Sep 17 00:00:00 2001 From: Andrew Poelstra Date: Sat, 3 May 2025 13:08:15 +0000 Subject: [PATCH 07/53] bip32: return error when attempting to derive past maximum depth The corresponding PR on master is #4387, but this doesn't really resemble that PR. Rather than changing all the error enums, this just adds a new variant to the #[non_exhaustive] bip32::Error enum and returns that when adding 1 to 255 when deriving child keys. This is therefore not an API break and can be released in a minor version. Although it does change the error return on private derivation from a "succeeds except with negligible probability" to "there is an error path you may need to check for". Fixes #4308 --- bitcoin/src/bip32.rs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/bitcoin/src/bip32.rs b/bitcoin/src/bip32.rs index f15d121bf..ebc0dde2c 100644 --- a/bitcoin/src/bip32.rs +++ b/bitcoin/src/bip32.rs @@ -178,6 +178,9 @@ impl ChildNumber { /// Returns the child number that is a single increment from this one. pub fn increment(self) -> Result { + // Bare addition in this function is okay, because we have an invariant that + // `index` is always within [0, 2^31 - 1]. FIXME this is not actually an + // invariant because the fields are public. match self { ChildNumber::Normal { index: idx } => ChildNumber::from_normal_idx(idx + 1), ChildNumber::Hardened { index: idx } => ChildNumber::from_hardened_idx(idx + 1), @@ -481,6 +484,10 @@ pub type KeySource = (Fingerprint, DerivationPath); pub enum Error { /// A pk->pk derivation was attempted on a hardened key CannotDeriveFromHardenedKey, + /// Attempted to derive a child of depth 256 or higher. + /// + /// There is no way to encode such xkeys. + MaximumDepthExceeded, /// A secp256k1 error occurred Secp256k1(secp256k1::Error), /// A child number was provided that was out of range @@ -512,6 +519,7 @@ impl fmt::Display for Error { match *self { CannotDeriveFromHardenedKey => f.write_str("cannot derive hardened key from public key"), + MaximumDepthExceeded => f.write_str("cannot derive child of depth 256 or higher"), Secp256k1(ref e) => write_err!(f, "secp256k1 error"; e), InvalidChildNumber(ref n) => write!(f, "child number {} is invalid (not within [0, 2^31 - 1])", n), @@ -540,6 +548,7 @@ impl std::error::Error for Error { Hex(ref e) => Some(e), InvalidBase58PayloadLength(ref e) => Some(e), CannotDeriveFromHardenedKey + | MaximumDepthExceeded | InvalidChildNumber(_) | InvalidChildNumberFormat | InvalidDerivationPathFormat @@ -636,7 +645,7 @@ impl Xpriv { Ok(Xpriv { network: self.network, - depth: self.depth + 1, + depth: self.depth.checked_add(1).ok_or(Error::MaximumDepthExceeded)?, parent_fingerprint: self.fingerprint(secp), child_number: i, private_key: tweaked, @@ -768,7 +777,7 @@ impl Xpub { Ok(Xpub { network: self.network, - depth: self.depth + 1, + depth: self.depth.checked_add(1).ok_or(Error::MaximumDepthExceeded)?, parent_fingerprint: self.fingerprint(), child_number: i, public_key: tweaked, From b75b2e36496674d2dd9964c726dbdd50b3d1ed01 Mon Sep 17 00:00:00 2001 From: Nadav Ivgi Date: Fri, 13 Sep 2024 11:28:17 +0300 Subject: [PATCH 08/53] Fix GetKey for sets to properly compare the fingerprint --- bitcoin/src/psbt/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bitcoin/src/psbt/mod.rs b/bitcoin/src/psbt/mod.rs index 3d428fdac..7c42d6dd3 100644 --- a/bitcoin/src/psbt/mod.rs +++ b/bitcoin/src/psbt/mod.rs @@ -807,7 +807,7 @@ impl GetKey for $set { KeyRequest::Pubkey(_) => Err(GetKeyError::NotSupported), KeyRequest::Bip32((fingerprint, path)) => { for xpriv in self.iter() { - if xpriv.parent_fingerprint == fingerprint { + if xpriv.fingerprint(secp) == fingerprint { let k = xpriv.derive_priv(secp, &path)?; return Ok(Some(k.to_priv())); } From d005ddd5249909bd8919a169abb5c7d6d2386ec1 Mon Sep 17 00:00:00 2001 From: Nadav Ivgi Date: Fri, 13 Sep 2024 11:35:32 +0300 Subject: [PATCH 09/53] Refactor GetKey for sets to internally use Xpriv::get_key() --- bitcoin/src/psbt/mod.rs | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/bitcoin/src/psbt/mod.rs b/bitcoin/src/psbt/mod.rs index 7c42d6dd3..7fd497b9d 100644 --- a/bitcoin/src/psbt/mod.rs +++ b/bitcoin/src/psbt/mod.rs @@ -803,18 +803,11 @@ impl GetKey for $set { key_request: KeyRequest, secp: &Secp256k1 ) -> Result, Self::Error> { - match key_request { - KeyRequest::Pubkey(_) => Err(GetKeyError::NotSupported), - KeyRequest::Bip32((fingerprint, path)) => { - for xpriv in self.iter() { - if xpriv.fingerprint(secp) == fingerprint { - let k = xpriv.derive_priv(secp, &path)?; - return Ok(Some(k.to_priv())); - } - } - Ok(None) - } - } + // OK to stop at the first error because Xpriv::get_key() can only fail + // if this isn't a KeyRequest::Bip32, which would fail for all Xprivs. + self.iter() + .find_map(|xpriv| xpriv.get_key(key_request.clone(), secp).transpose()) + .transpose() } }}} impl_get_key_for_set!(BTreeSet); From 2858b6cf801def1800a2cfd3287a38633d194a49 Mon Sep 17 00:00:00 2001 From: Nadav Ivgi Date: Fri, 13 Sep 2024 11:46:07 +0300 Subject: [PATCH 10/53] Support GetKey where the Xpriv is a direct child of the looked up KeySource --- bitcoin/src/psbt/mod.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/bitcoin/src/psbt/mod.rs b/bitcoin/src/psbt/mod.rs index 7fd497b9d..69a0cf1cb 100644 --- a/bitcoin/src/psbt/mod.rs +++ b/bitcoin/src/psbt/mod.rs @@ -21,7 +21,7 @@ use std::collections::{HashMap, HashSet}; use internals::write_err; use secp256k1::{Keypair, Message, Secp256k1, Signing, Verification}; -use crate::bip32::{self, KeySource, Xpriv, Xpub}; +use crate::bip32::{self, DerivationPath, KeySource, Xpriv, Xpub}; use crate::blockdata::transaction::{self, Transaction, TxOut}; use crate::crypto::key::{PrivateKey, PublicKey}; use crate::crypto::{ecdsa, taproot}; @@ -764,6 +764,13 @@ impl GetKey for Xpriv { let key = if self.fingerprint(secp) == fingerprint { let k = self.derive_priv(secp, &path)?; Some(k.to_priv()) + } else if self.parent_fingerprint == fingerprint + && !path.is_empty() + && path[0] == self.child_number + { + let path = DerivationPath::from_iter(path.into_iter().skip(1).copied()); + let k = self.derive_priv(secp, &path)?; + Some(k.to_priv()) } else { None }; From 95eb2556b93da25a402e82eab1e032a0ececb67e Mon Sep 17 00:00:00 2001 From: Erick Cestari Date: Fri, 14 Mar 2025 10:25:28 -0300 Subject: [PATCH 11/53] Add XOnlyPublicKey support for PSBT key retrieval and improve Taproot signing This commit enhances PSBT signing functionality by: 1. Added new KeyRequest::XOnlyPubkey variant to support direct retrieval using XOnly public keys 2. Implemented GetKey for HashMap for more efficient Taproot key management 3. Modified HashMap implementation to handle XOnlyPublicKey requests by checking both even and odd parity variants These changes allow for more flexible key management in Taproot transactions. Specifically, wallet implementations can now store keys indexed by either PublicKey or XOnlyPublicKey and successfully sign PSBTs with Taproot inputs. Added tests for both implementations to verify correct behavior. Added test for odd parity key retrieval. Closes #4150 --- bitcoin/src/crypto/key.rs | 14 +++ bitcoin/src/psbt/mod.rs | 182 ++++++++++++++++++++++++++++++++++++-- 2 files changed, 187 insertions(+), 9 deletions(-) diff --git a/bitcoin/src/crypto/key.rs b/bitcoin/src/crypto/key.rs index f6a5a4735..c65b432bf 100644 --- a/bitcoin/src/crypto/key.rs +++ b/bitcoin/src/crypto/key.rs @@ -496,6 +496,20 @@ impl PrivateKey { inner: secp256k1::SecretKey::from_slice(&data[1..33])?, }) } + + /// Returns a new private key with the negated secret value. + /// + /// The resulting key corresponds to the same x-only public key (identical x-coordinate) + /// but with the opposite y-coordinate parity. This is useful for ensuring compatibility + /// with specific public key formats and BIP-340 requirements. + #[inline] + pub fn negate(&self) -> Self { + PrivateKey { + compressed: self.compressed, + network: self.network, + inner: self.inner.negate(), + } + } } impl fmt::Display for PrivateKey { diff --git a/bitcoin/src/psbt/mod.rs b/bitcoin/src/psbt/mod.rs index 69a0cf1cb..278d11ddf 100644 --- a/bitcoin/src/psbt/mod.rs +++ b/bitcoin/src/psbt/mod.rs @@ -423,6 +423,8 @@ impl Psbt { k.get_key(KeyRequest::Bip32(key_source.clone()), secp) { secret_key + } else if let Ok(Some(sk)) = k.get_key(KeyRequest::XOnlyPubkey(xonly), secp) { + sk } else { continue; }; @@ -730,6 +732,8 @@ pub enum KeyRequest { Pubkey(PublicKey), /// Request a private key using BIP-32 fingerprint and derivation path. Bip32(KeySource), + /// Request a private key using the associated x-only public key. + XOnlyPubkey(XOnlyPublicKey), } /// Trait to get a private key from a key request, key is then used to sign an input. @@ -760,6 +764,7 @@ impl GetKey for Xpriv { ) -> Result, Self::Error> { match key_request { KeyRequest::Pubkey(_) => Err(GetKeyError::NotSupported), + KeyRequest::XOnlyPubkey(_) => Err(GetKeyError::NotSupported), KeyRequest::Bip32((fingerprint, path)) => { let key = if self.fingerprint(secp) == fingerprint { let k = self.derive_priv(secp, &path)?; @@ -822,7 +827,7 @@ impl_get_key_for_set!(BTreeSet); impl_get_key_for_set!(HashSet); #[rustfmt::skip] -macro_rules! impl_get_key_for_map { +macro_rules! impl_get_key_for_pubkey_map { ($map:ident) => { impl GetKey for $map { @@ -835,13 +840,67 @@ impl GetKey for $map { ) -> Result, Self::Error> { match key_request { KeyRequest::Pubkey(pk) => Ok(self.get(&pk).cloned()), + KeyRequest::XOnlyPubkey(xonly) => { + let pubkey_even = PublicKey::new(xonly.public_key(secp256k1::Parity::Even)); + let key = self.get(&pubkey_even).cloned(); + + if key.is_some() { + return Ok(key); + } + + let pubkey_odd = PublicKey::new(xonly.public_key(secp256k1::Parity::Odd)); + if let Some(priv_key) = self.get(&pubkey_odd).copied() { + let negated_priv_key = priv_key.negate(); + return Ok(Some(negated_priv_key)); + } + + Ok(None) + }, + KeyRequest::Bip32(_) => Err(GetKeyError::NotSupported), + } + } +}}} +impl_get_key_for_pubkey_map!(BTreeMap); +#[cfg(feature = "std")] +impl_get_key_for_pubkey_map!(HashMap); + +#[rustfmt::skip] +macro_rules! impl_get_key_for_xonly_map { + ($map:ident) => { + +impl GetKey for $map { + type Error = GetKeyError; + + fn get_key( + &self, + key_request: KeyRequest, + secp: &Secp256k1, + ) -> Result, Self::Error> { + match key_request { + KeyRequest::XOnlyPubkey(xonly) => Ok(self.get(&xonly).cloned()), + KeyRequest::Pubkey(pk) => { + let (xonly, parity) = pk.inner.x_only_public_key(); + + if let Some(mut priv_key) = self.get(&XOnlyPublicKey::from(xonly)).cloned() { + let computed_pk = priv_key.public_key(&secp); + let (_, computed_parity) = computed_pk.inner.x_only_public_key(); + + if computed_parity != parity { + priv_key = priv_key.negate(); + } + + return Ok(Some(priv_key)); + } + + Ok(None) + }, KeyRequest::Bip32(_) => Err(GetKeyError::NotSupported), } } }}} -impl_get_key_for_map!(BTreeMap); +impl_get_key_for_xonly_map!(BTreeMap); #[cfg(feature = "std")] -impl_get_key_for_map!(HashMap); +impl_get_key_for_xonly_map!(HashMap); /// Errors when getting a key. #[derive(Debug, Clone, PartialEq, Eq)] @@ -1208,7 +1267,14 @@ mod tests { use hashes::{hash160, ripemd160, sha256, Hash}; use hex::{test_hex_unwrap as hex, FromHex}; #[cfg(feature = "rand-std")] - use secp256k1::{All, SecretKey}; + use { + crate::bip32::{DerivationPath, Fingerprint}, + crate::key::WPubkeyHash, + crate::locktime, + crate::witness_version::WitnessVersion, + crate::WitnessProgram, + secp256k1::{All, SecretKey}, + }; use super::*; use crate::bip32::ChildNumber; @@ -2138,7 +2204,43 @@ mod tests { } #[test] - fn test_fee() { + #[cfg(feature = "rand-std")] + fn pubkey_map_get_key_negates_odd_parity_keys() { + use crate::psbt::{GetKey, KeyRequest}; + + let (mut priv_key, mut pk, secp) = gen_keys(); + let (xonly, parity) = pk.inner.x_only_public_key(); + + let mut pubkey_map: HashMap = HashMap::new(); + + if parity == secp256k1::Parity::Even { + priv_key = PrivateKey { + compressed: priv_key.compressed, + network: priv_key.network, + inner: priv_key.inner.negate(), + }; + pk = priv_key.public_key(&secp); + } + + pubkey_map.insert(pk, priv_key); + + let req_result = pubkey_map.get_key(KeyRequest::XOnlyPubkey(xonly), &secp).unwrap(); + + let retrieved_key = req_result.unwrap(); + + let retrieved_pub_key = retrieved_key.public_key(&secp); + let (retrieved_xonly, retrieved_parity) = retrieved_pub_key.inner.x_only_public_key(); + + assert_eq!(xonly, retrieved_xonly); + assert_eq!( + retrieved_parity, + secp256k1::Parity::Even, + "Key should be normalized to have even parity, even when original had odd parity" + ); + } + + #[test] + fn fee() { let output_0_val = Amount::from_sat(99_999_699); let output_1_val = Amount::from_sat(100_000_000); let prev_output_val = Amount::from_sat(200_000_000); @@ -2248,11 +2350,73 @@ mod tests { #[test] #[cfg(feature = "rand-std")] - fn sign_psbt() { - use crate::bip32::{DerivationPath, Fingerprint}; - use crate::witness_version::WitnessVersion; - use crate::{WPubkeyHash, WitnessProgram}; + fn hashmap_can_sign_taproot() { + let (priv_key, pk, secp) = gen_keys(); + let internal_key: XOnlyPublicKey = pk.inner.into(); + + let tx = Transaction { + version: transaction::Version::TWO, + lock_time: locktime::absolute::LockTime::ZERO, + input: vec![TxIn::default()], + output: vec![TxOut { value: Amount::ZERO, script_pubkey: ScriptBuf::new() }], + }; + + let mut psbt = Psbt::from_unsigned_tx(tx).unwrap(); + psbt.inputs[0].tap_internal_key = Some(internal_key); + psbt.inputs[0].witness_utxo = Some(transaction::TxOut { + value: Amount::from_sat(10), + script_pubkey: ScriptBuf::new_p2tr(&secp, internal_key, None), + }); + let mut key_map: HashMap = HashMap::new(); + key_map.insert(pk, priv_key); + + let key_source = (Fingerprint::default(), DerivationPath::default()); + let mut tap_key_origins = std::collections::BTreeMap::new(); + tap_key_origins.insert(internal_key, (vec![], key_source)); + psbt.inputs[0].tap_key_origins = tap_key_origins; + + let signing_keys = psbt.sign(&key_map, &secp).unwrap(); + assert_eq!(signing_keys.len(), 1); + assert_eq!(signing_keys[&0], SigningKeys::Schnorr(vec![internal_key])); + } + + #[test] + #[cfg(feature = "rand-std")] + fn xonly_hashmap_can_sign_taproot() { + let (priv_key, pk, secp) = gen_keys(); + let internal_key: XOnlyPublicKey = pk.inner.into(); + + let tx = Transaction { + version: transaction::Version::TWO, + lock_time: locktime::absolute::LockTime::ZERO, + input: vec![TxIn::default()], + output: vec![TxOut { value: Amount::ZERO, script_pubkey: ScriptBuf::new() }], + }; + + let mut psbt = Psbt::from_unsigned_tx(tx).unwrap(); + psbt.inputs[0].tap_internal_key = Some(internal_key); + psbt.inputs[0].witness_utxo = Some(transaction::TxOut { + value: Amount::from_sat(10), + script_pubkey: ScriptBuf::new_p2tr(&secp, internal_key, None), + }); + + let mut xonly_key_map: HashMap = HashMap::new(); + xonly_key_map.insert(internal_key, priv_key); + + let key_source = (Fingerprint::default(), DerivationPath::default()); + let mut tap_key_origins = std::collections::BTreeMap::new(); + tap_key_origins.insert(internal_key, (vec![], key_source)); + psbt.inputs[0].tap_key_origins = tap_key_origins; + + let signing_keys = psbt.sign(&xonly_key_map, &secp).unwrap(); + assert_eq!(signing_keys.len(), 1); + assert_eq!(signing_keys[&0], SigningKeys::Schnorr(vec![internal_key])); + } + + #[test] + #[cfg(feature = "rand-std")] + fn sign_psbt() { let unsigned_tx = Transaction { version: transaction::Version::TWO, lock_time: absolute::LockTime::ZERO, From c67adcd64e9b425f2787629d50ecbcce465e1f5e Mon Sep 17 00:00:00 2001 From: Shing Him Ng Date: Sun, 20 Apr 2025 13:19:15 -0500 Subject: [PATCH 12/53] backport: Add methods to retrieve inner types Backport #4373, authored by Shing Him Ng. Original gitlog: For TweakedKeypair, `to_inner` is also renamed to `to_keypair` to maintain consistency. Similarly, `to_inner` is renamed to `to_x_only_pubkey` for TweakedPublicKey Co-authored-by: Shing Him Ng --- bitcoin/examples/sign-tx-taproot.rs | 2 +- bitcoin/examples/taproot-psbt.rs | 2 +- .../src/blockdata/script/witness_program.rs | 4 ++-- bitcoin/src/crypto/key.rs | 21 +++++++++++++++++-- bitcoin/src/psbt/mod.rs | 2 +- bitcoin/src/taproot/mod.rs | 8 +++---- 6 files changed, 28 insertions(+), 11 deletions(-) diff --git a/bitcoin/examples/sign-tx-taproot.rs b/bitcoin/examples/sign-tx-taproot.rs index 3221c560c..9dae0b2e0 100644 --- a/bitcoin/examples/sign-tx-taproot.rs +++ b/bitcoin/examples/sign-tx-taproot.rs @@ -72,7 +72,7 @@ fn main() { // Sign the sighash using the secp256k1 library (exported by rust-bitcoin). let tweaked: TweakedKeypair = keypair.tap_tweak(&secp, None); let msg = Message::from(sighash); - let signature = secp.sign_schnorr(&msg, &tweaked.to_inner()); + let signature = secp.sign_schnorr(&msg, tweaked.as_keypair()); // Update the witness stack. let signature = bitcoin::taproot::Signature { signature, sighash_type }; diff --git a/bitcoin/examples/taproot-psbt.rs b/bitcoin/examples/taproot-psbt.rs index b5db05ab8..62f943196 100644 --- a/bitcoin/examples/taproot-psbt.rs +++ b/bitcoin/examples/taproot-psbt.rs @@ -734,7 +734,7 @@ fn sign_psbt_taproot( ) { let keypair = secp256k1::Keypair::from_seckey_slice(secp, secret_key.as_ref()).unwrap(); let keypair = match leaf_hash { - None => keypair.tap_tweak(secp, psbt_input.tap_merkle_root).to_inner(), + None => keypair.tap_tweak(secp, psbt_input.tap_merkle_root).to_keypair(), Some(_) => keypair, // no tweak for script spend }; diff --git a/bitcoin/src/blockdata/script/witness_program.rs b/bitcoin/src/blockdata/script/witness_program.rs index 9b76becc8..7a661f21c 100644 --- a/bitcoin/src/blockdata/script/witness_program.rs +++ b/bitcoin/src/blockdata/script/witness_program.rs @@ -90,13 +90,13 @@ impl WitnessProgram { merkle_root: Option, ) -> Self { let (output_key, _parity) = internal_key.tap_tweak(secp, merkle_root); - let pubkey = output_key.to_inner().serialize(); + let pubkey = output_key.as_x_only_public_key().serialize(); WitnessProgram::new_p2tr(pubkey) } /// Creates a pay to taproot address from a pre-tweaked output key. pub fn p2tr_tweaked(output_key: TweakedPublicKey) -> Self { - let pubkey = output_key.to_inner().serialize(); + let pubkey = output_key.as_x_only_public_key().serialize(); WitnessProgram::new_p2tr(pubkey) } diff --git a/bitcoin/src/crypto/key.rs b/bitcoin/src/crypto/key.rs index f6a5a4735..a44a21b2e 100644 --- a/bitcoin/src/crypto/key.rs +++ b/bitcoin/src/crypto/key.rs @@ -841,9 +841,18 @@ impl TweakedPublicKey { TweakedPublicKey(key) } - /// Returns the underlying public key. + #[doc(hidden)] + #[deprecated(since="0.32.6", note="use to_x_only_public_key() instead")] pub fn to_inner(self) -> XOnlyPublicKey { self.0 } + /// Returns the underlying x-only public key. + #[inline] + pub fn to_x_only_public_key(self) -> XOnlyPublicKey { self.0 } + + /// Returns a reference to the underlying x-only public key. + #[inline] + pub fn as_x_only_public_key(&self) -> &XOnlyPublicKey { &self.0 } + /// Serialize the key as a byte-encoded pair of values. In compressed form /// the y-coordinate is represented by only a single bit, as x determines /// it up to one bit. @@ -860,9 +869,17 @@ impl TweakedKeypair { #[inline] pub fn dangerous_assume_tweaked(pair: Keypair) -> TweakedKeypair { TweakedKeypair(pair) } + #[doc(hidden)] + #[deprecated(since="0.32.6", note="use to_keypair() instead")] + pub fn to_inner(self) -> Keypair { self.0 } + /// Returns the underlying key pair. #[inline] - pub fn to_inner(self) -> Keypair { self.0 } + pub fn to_keypair(self) -> Keypair { self.0 } + + /// Returns a reference to the underlying key pair. + #[inline] + pub fn as_keypair(&self) -> &Keypair { &self.0 } /// Returns the [`TweakedPublicKey`] and its [`Parity`] for this [`TweakedKeypair`]. #[inline] diff --git a/bitcoin/src/psbt/mod.rs b/bitcoin/src/psbt/mod.rs index 3d428fdac..695af0011 100644 --- a/bitcoin/src/psbt/mod.rs +++ b/bitcoin/src/psbt/mod.rs @@ -442,7 +442,7 @@ impl Psbt { let (msg, sighash_type) = self.sighash_taproot(input_index, cache, None)?; let key_pair = Keypair::from_secret_key(secp, &sk.inner) .tap_tweak(secp, input.tap_merkle_root) - .to_inner(); + .to_keypair(); #[cfg(feature = "rand-std")] let signature = secp.sign_schnorr(&msg, &key_pair); diff --git a/bitcoin/src/taproot/mod.rs b/bitcoin/src/taproot/mod.rs index 656a3ccbf..54576c578 100644 --- a/bitcoin/src/taproot/mod.rs +++ b/bitcoin/src/taproot/mod.rs @@ -1556,7 +1556,7 @@ mod test { let control_block = ControlBlock::decode(&Vec::::from_hex(control_block_hex).unwrap()).unwrap(); assert_eq!(control_block_hex, control_block.serialize().to_lower_hex_string()); - assert!(control_block.verify_taproot_commitment(secp, out_pk.to_inner(), &script)); + assert!(control_block.verify_taproot_commitment(secp, out_pk.to_x_only_public_key(), &script)); } #[test] @@ -1663,7 +1663,7 @@ mod test { let ctrl_block = tree_info.control_block(&ver_script).unwrap(); assert!(ctrl_block.verify_taproot_commitment( &secp, - output_key.to_inner(), + output_key.to_x_only_public_key(), &ver_script.0 )) } @@ -1738,7 +1738,7 @@ mod test { let ctrl_block = tree_info.control_block(&ver_script).unwrap(); assert!(ctrl_block.verify_taproot_commitment( &secp, - output_key.to_inner(), + output_key.to_x_only_public_key(), &ver_script.0 )) } @@ -1854,7 +1854,7 @@ mod test { let addr = Address::p2tr(secp, internal_key, merkle_root, KnownHrp::Mainnet); let spk = addr.script_pubkey(); - assert_eq!(expected_output_key, output_key.to_inner()); + assert_eq!(expected_output_key, output_key.to_x_only_public_key()); assert_eq!(expected_tweak, tweak); assert_eq!(expected_addr, addr); assert_eq!(expected_spk, spk); From 916982a025d12ab98c9c878d33aae60cfb98102b Mon Sep 17 00:00:00 2001 From: "Tobin C. Harding" Date: Tue, 6 May 2025 09:21:12 +1000 Subject: [PATCH 13/53] bitcoin: Bump version to 0.32.6 In preparation for release bump the version, add a changelog entry, and update the lock files. --- Cargo-minimal.lock | 2 +- Cargo-recent.lock | 2 +- bitcoin/CHANGELOG.md | 8 ++++++++ bitcoin/Cargo.toml | 2 +- 4 files changed, 11 insertions(+), 3 deletions(-) diff --git a/Cargo-minimal.lock b/Cargo-minimal.lock index f28cbe79f..a335ef716 100644 --- a/Cargo-minimal.lock +++ b/Cargo-minimal.lock @@ -47,7 +47,7 @@ dependencies = [ [[package]] name = "bitcoin" -version = "0.32.5" +version = "0.32.6" dependencies = [ "base58ck", "base64", diff --git a/Cargo-recent.lock b/Cargo-recent.lock index dde2e6b31..65a7f1f37 100644 --- a/Cargo-recent.lock +++ b/Cargo-recent.lock @@ -46,7 +46,7 @@ dependencies = [ [[package]] name = "bitcoin" -version = "0.32.5" +version = "0.32.6" dependencies = [ "base58ck", "base64", diff --git a/bitcoin/CHANGELOG.md b/bitcoin/CHANGELOG.md index 1c5b03aae..c02599a3a 100644 --- a/bitcoin/CHANGELOG.md +++ b/bitcoin/CHANGELOG.md @@ -1,3 +1,11 @@ +# 0.32.6 - 2025-05-06 + +- Backport - Fix `is_invalid_use_of_sighash_single()` incompatibility with Bitcoin Core [#4122](https://github.com/rust-bitcoin/rust-bitcoin/pull/4122) +- Backport - Backport witness fixes [#4101](https://github.com/rust-bitcoin/rust-bitcoin/pull/4101) +- Backport - bip32: Return error when attempting to derive past maximum depth [#4434](https://github.com/rust-bitcoin/rust-bitcoin/pull/4434) +- Backport - Add `XOnlyPublicKey` support for PSBT key retrieval and improve Taproot signing [#4443](https://github.com/rust-bitcoin/rust-bitcoin/pull/4443) +- Backport - Add methods to retrieve inner types [#4450](https://github.com/rust-bitcoin/rust-bitcoin/pull/4450) + # 0.32.5 - 2024-11-27 - Backport - Re-export `bech32` crate [#3662](https://github.com/rust-bitcoin/rust-bitcoin/pull/3662) diff --git a/bitcoin/Cargo.toml b/bitcoin/Cargo.toml index 70594e093..7cc56d124 100644 --- a/bitcoin/Cargo.toml +++ b/bitcoin/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bitcoin" -version = "0.32.5" +version = "0.32.6" authors = ["Andrew Poelstra "] license = "CC0-1.0" repository = "https://github.com/rust-bitcoin/rust-bitcoin/" From c7b20f411ad03232a9b68455f7856638fe0bd413 Mon Sep 17 00:00:00 2001 From: "Tobin C. Harding" Date: Sat, 24 May 2025 11:54:09 +1000 Subject: [PATCH 14/53] backport: Use _u32 in FeeRate constructor instead of _unchecked Manually backport #4538. If we use a `u32` then the constructor no longer panics. 32 bits is plenty for an sane usage. --- units/src/fee_rate.rs | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/units/src/fee_rate.rs b/units/src/fee_rate.rs index 9dcb88ddb..da686fdb2 100644 --- a/units/src/fee_rate.rs +++ b/units/src/fee_rate.rs @@ -37,10 +37,10 @@ impl FeeRate { /// Minimum fee rate required to broadcast a transaction. /// /// The value matches the default Bitcoin Core policy at the time of library release. - pub const BROADCAST_MIN: FeeRate = FeeRate::from_sat_per_vb_unchecked(1); + pub const BROADCAST_MIN: FeeRate = FeeRate::from_sat_per_vb_u32(1); /// Fee rate used to compute dust amount. - pub const DUST: FeeRate = FeeRate::from_sat_per_vb_unchecked(3); + pub const DUST: FeeRate = FeeRate::from_sat_per_vb_u32(3); /// Constructs `FeeRate` from satoshis per 1000 weight units. pub const fn from_sat_per_kwu(sat_kwu: u64) -> Self { FeeRate(sat_kwu) } @@ -57,7 +57,14 @@ impl FeeRate { Some(FeeRate(sat_vb.checked_mul(1000 / 4)?)) } + /// Constructs a new [`FeeRate`] from satoshis per virtual bytes. + pub const fn from_sat_per_vb_u32(sat_vb: u32) -> Self { + let sat_vb = sat_vb as u64; // No `Into` in const context. + FeeRate(sat_vb * (1000 / 4)) + } + /// Constructs `FeeRate` from satoshis per virtual bytes without overflow check. + #[deprecated(since = "0.32.7", note = "use from_sat_per_vb_u32 instead")] pub const fn from_sat_per_vb_unchecked(sat_vb: u64) -> Self { FeeRate(sat_vb * (1000 / 4)) } /// Returns raw fee rate. @@ -168,16 +175,17 @@ mod tests { fn fee_rate_from_sat_per_vb_overflow_test() { let fee_rate = FeeRate::from_sat_per_vb(u64::MAX); assert!(fee_rate.is_none()); - } + } #[test] - fn from_sat_per_vb_unchecked_test() { - let fee_rate = FeeRate::from_sat_per_vb_unchecked(10); + fn from_sat_per_vb_u32() { + let fee_rate = FeeRate::from_sat_per_vb_u32(10); assert_eq!(FeeRate(2500), fee_rate); } #[test] #[cfg(debug_assertions)] + #[allow(deprecated)] // Keep test until we remove the function. #[should_panic] fn from_sat_per_vb_unchecked_panic_test() { FeeRate::from_sat_per_vb_unchecked(u64::MAX); } From c2481e4e824fddd86cef50d73f97ad590be2c55f Mon Sep 17 00:00:00 2001 From: "Tobin C. Harding" Date: Tue, 8 Jul 2025 15:38:57 +1000 Subject: [PATCH 15/53] backport: Add support for pay to anchor outputs Manually backport PR #4111. Of note, here we put `new_p2a` on `ScriptBuf` instead of on the `script::Builder` because that seems to be where all the other `new_foo` methods are in this release. Note the `WitnessProgram::p2a` is conditionally const on Rust `v1.61` because MSRV is only `v1.56.1`. From the original patch: Add support for the newly created Pay2Anchor output-type. See https://github.com/bitcoin/bitcoin/pull/30352 --- bitcoin/Cargo.toml | 2 +- bitcoin/src/address/mod.rs | 24 +++++++++++++++++++ bitcoin/src/blockdata/script/owned.rs | 12 +++++++--- .../src/blockdata/script/witness_program.rs | 15 ++++++++++++ 4 files changed, 49 insertions(+), 4 deletions(-) diff --git a/bitcoin/Cargo.toml b/bitcoin/Cargo.toml index 7cc56d124..dcfd8959e 100644 --- a/bitcoin/Cargo.toml +++ b/bitcoin/Cargo.toml @@ -81,4 +81,4 @@ required-features = ["std", "rand-std", "bitcoinconsensus"] name = "sighash" [lints.rust] -unexpected_cfgs = { level = "deny", check-cfg = ['cfg(bench)', 'cfg(fuzzing)', 'cfg(kani)', 'cfg(mutate)', 'cfg(rust_v_1_60)'] } +unexpected_cfgs = { level = "deny", check-cfg = ['cfg(bench)', 'cfg(fuzzing)', 'cfg(kani)', 'cfg(mutate)', 'cfg(rust_v_1_60)', 'cfg(rust_v_1_61)'] } diff --git a/bitcoin/src/address/mod.rs b/bitcoin/src/address/mod.rs index 7c546ae78..c05f32ac8 100644 --- a/bitcoin/src/address/mod.rs +++ b/bitcoin/src/address/mod.rs @@ -74,6 +74,8 @@ pub enum AddressType { P2wsh, /// Pay to taproot. P2tr, + /// Pay to anchor. + P2a } impl fmt::Display for AddressType { @@ -84,6 +86,7 @@ impl fmt::Display for AddressType { AddressType::P2wpkh => "p2wpkh", AddressType::P2wsh => "p2wsh", AddressType::P2tr => "p2tr", + AddressType::P2a => "p2a", }) } } @@ -97,6 +100,7 @@ impl FromStr for AddressType { "p2wpkh" => Ok(AddressType::P2wpkh), "p2wsh" => Ok(AddressType::P2wsh), "p2tr" => Ok(AddressType::P2tr), + "p2a" => Ok(AddressType::P2a), _ => Err(UnknownAddressTypeError(s.to_owned())), } } @@ -496,6 +500,8 @@ impl Address { Some(AddressType::P2wsh) } else if program.is_p2tr() { Some(AddressType::P2tr) + } else if program.is_p2a() { + Some(AddressType::P2a) } else { None }, @@ -1367,4 +1373,22 @@ mod tests { } } } + + #[test] + fn pay_to_anchor_address_regtest() { + // Verify that p2a uses the expected address for regtest. + // This test-vector is borrowed from the bitcoin source code. + let address_str = "bcrt1pfeesnyr2tx"; + + let script = ScriptBuf::new_p2a(); + let address_unchecked = address_str.parse().unwrap(); + let address = Address::from_script(&script, Network::Regtest).unwrap(); + assert_eq!(address.as_unchecked(), &address_unchecked); + assert_eq!(address.to_string(), address_str); + + // Verify that the address is considered standard + // and that the output type is P2a + assert!(address.is_spend_standard()); + assert_eq!(address.address_type(), Some(AddressType::P2a)); + } } diff --git a/bitcoin/src/blockdata/script/owned.rs b/bitcoin/src/blockdata/script/owned.rs index d7189488c..829ead845 100644 --- a/bitcoin/src/blockdata/script/owned.rs +++ b/bitcoin/src/blockdata/script/owned.rs @@ -8,7 +8,7 @@ use secp256k1::{Secp256k1, Verification}; use crate::blockdata::opcodes::all::*; use crate::blockdata::opcodes::{self, Opcode}; -use crate::blockdata::script::witness_program::WitnessProgram; +use crate::blockdata::script::witness_program::{WitnessProgram, P2A_PROGRAM}; use crate::blockdata::script::witness_version::WitnessVersion; use crate::blockdata::script::{ opcode_to_verify, Builder, Instruction, PushBytes, Script, ScriptHash, WScriptHash, @@ -130,6 +130,11 @@ impl ScriptBuf { ScriptBuf::new_witness_program_unchecked(WitnessVersion::V1, output_key.serialize()) } + /// Generates pay to anchor output. + pub fn new_p2a() -> Self { + ScriptBuf::new_witness_program_unchecked(WitnessVersion::V1, P2A_PROGRAM) + } + /// Generates P2WSH-type of scriptPubkey with a given [`WitnessProgram`]. pub fn new_witness_program(witness_program: &WitnessProgram) -> Self { Builder::new() @@ -141,14 +146,15 @@ impl ScriptBuf { /// Generates P2WSH-type of scriptPubkey with a given [`WitnessVersion`] and the program bytes. /// Does not do any checks on version or program length. /// - /// Convenience method used by `new_p2wpkh`, `new_p2wsh`, `new_p2tr`, and `new_p2tr_tweaked`. + /// Convenience method used by `new_p2wpkh`, `new_p2wsh`, `new_p2tr`, and `new_p2tr_tweaked`, + /// and `new_p2a`. pub(crate) fn new_witness_program_unchecked>( version: WitnessVersion, program: T, ) -> Self { let program = program.as_ref(); debug_assert!(program.len() >= 2 && program.len() <= 40); - // In segwit v0, the program must be 20 or 32 bytes long. + // In SegWit v0, the program must be either 20 (P2WPKH) bytes or 32 (P2WSH) bytes long debug_assert!(version != WitnessVersion::V0 || program.len() == 20 || program.len() == 32); Builder::new().push_opcode(version.into()).push_slice(program).into_script() } diff --git a/bitcoin/src/blockdata/script/witness_program.rs b/bitcoin/src/blockdata/script/witness_program.rs index 7a661f21c..2cf7101f2 100644 --- a/bitcoin/src/blockdata/script/witness_program.rs +++ b/bitcoin/src/blockdata/script/witness_program.rs @@ -24,6 +24,9 @@ pub const MIN_SIZE: usize = 2; /// The maximum byte size of a segregated witness program. pub const MAX_SIZE: usize = 40; +/// The P2A program which is given by 0x4e73. +pub(crate) const P2A_PROGRAM: [u8;2] = [78, 115]; + /// The segregated witness program. /// /// The segregated witness program is technically only the program bytes _excluding_ the witness @@ -100,6 +103,13 @@ impl WitnessProgram { WitnessProgram::new_p2tr(pubkey) } + internals::const_tools::cond_const! { + /// Constructs a new pay to anchor address + pub const(in rust_v_1_61 = "1.61") fn p2a() -> Self { + WitnessProgram { version: WitnessVersion::V1, program: ArrayVec::from_slice(&P2A_PROGRAM)} + } + } + /// Returns the witness program version. pub fn version(&self) -> WitnessVersion { self.version } @@ -123,6 +133,11 @@ impl WitnessProgram { /// Returns true if this witness program is for a P2TR output. pub fn is_p2tr(&self) -> bool { self.version == WitnessVersion::V1 && self.program.len() == 32 } + + /// Returns true if this is a pay to anchor output. + pub fn is_p2a(&self) -> bool { + self.version == WitnessVersion::V1 && self.program == P2A_PROGRAM + } } /// Witness program error. From 3cf4a916e3599f2ede002d9358c7776571dcc70c Mon Sep 17 00:00:00 2001 From: "Tobin C. Harding" Date: Wed, 2 Jul 2025 09:36:47 +1000 Subject: [PATCH 16/53] Remove non_exhausive from Network Issue #2225 is long and has many valid opposing opinions. The main argument for having non_exhaustive is that it helps future proof the ecosystem at the cost of pain now. The main argument against having non_exhaustive is why have pain now when adding a network is so rare that having pain then is ok. At the end of the thread Andrew posts: > I continue to think we should have an exhaustive enum, with a bunch of > documentation about how to use it properly. I am warming up to the > "don't have an enum, just have rules for defining your own" but I think > this would be needless work for people who just want to grab an > off-the-shelf set of networks or people who want to make their own enum > but want to see an example of how to do it first. In order to make some forward progress lets remove the `non_exhaustive` now and backport this change to 0.32, 0.31, an 0.30. Later we can add, and release in 0.33, whatever forward protection / libapocalyse protection we want to add. This removes the pain now and gives us a path to prevent future pain - that should keep all parties happy. --- bitcoin/src/network.rs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/bitcoin/src/network.rs b/bitcoin/src/network.rs index f16f2d5c7..8df48b0be 100644 --- a/bitcoin/src/network.rs +++ b/bitcoin/src/network.rs @@ -59,11 +59,19 @@ impl From for NetworkKind { } /// The cryptocurrency network to act on. +/// +/// This is an exhaustive enum, meaning that we cannot add any future networks without defining a +/// new, incompatible version of this type. If you are using this type directly and wish to support the +/// new network, this will be a breaking change to your APIs and likely require changes in your code. +/// +/// If you are concerned about forward compatibility, consider using `T: Into` instead of +/// this type as a parameter to functions in your public API, or directly using the `Params` type. +// For extensive discussion on the usage of `non_exhaustive` please see: +// https://github.com/rust-bitcoin/rust-bitcoin/issues/2225 #[derive(Copy, PartialEq, Eq, PartialOrd, Ord, Clone, Hash, Debug)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "serde", serde(crate = "actual_serde"))] #[cfg_attr(feature = "serde", serde(rename_all = "lowercase"))] -#[non_exhaustive] pub enum Network { /// Mainnet Bitcoin. Bitcoin, From 571cd7f33ec82e7febd708d0e5984b6367f7a361 Mon Sep 17 00:00:00 2001 From: "Tobin C. Harding" Date: Wed, 30 Jul 2025 11:10:31 +1000 Subject: [PATCH 17/53] bitcoin: Bump version to 0.32.7 In preparation for release bump the version, add a changelog entry, and update the lock files. --- Cargo-minimal.lock | 2 +- Cargo-recent.lock | 2 +- bitcoin/CHANGELOG.md | 6 ++++++ bitcoin/Cargo.toml | 2 +- 4 files changed, 9 insertions(+), 3 deletions(-) diff --git a/Cargo-minimal.lock b/Cargo-minimal.lock index a335ef716..8b47f1e59 100644 --- a/Cargo-minimal.lock +++ b/Cargo-minimal.lock @@ -47,7 +47,7 @@ dependencies = [ [[package]] name = "bitcoin" -version = "0.32.6" +version = "0.32.7" dependencies = [ "base58ck", "base64", diff --git a/Cargo-recent.lock b/Cargo-recent.lock index 65a7f1f37..9f49d4c0c 100644 --- a/Cargo-recent.lock +++ b/Cargo-recent.lock @@ -46,7 +46,7 @@ dependencies = [ [[package]] name = "bitcoin" -version = "0.32.6" +version = "0.32.7" dependencies = [ "base58ck", "base64", diff --git a/bitcoin/CHANGELOG.md b/bitcoin/CHANGELOG.md index c02599a3a..caa0de726 100644 --- a/bitcoin/CHANGELOG.md +++ b/bitcoin/CHANGELOG.md @@ -1,3 +1,9 @@ +# 0.32.7 - 2025-07-30 + +- Backport - Use `_u32` in `FeeRate` constructor instead of `_unchecked` [#4552](https://github.com/rust-bitcoin/rust-bitcoin/pull/4552) +- Backport - Add support for pay to anchor outputs [#4691](https://github.com/rust-bitcoin/rust-bitcoin/pull/4691) +- Backport - Remove `non_exhaustive` from `Network` [#4658](https://github.com/rust-bitcoin/rust-bitcoin/pull/4658) + # 0.32.6 - 2025-05-06 - Backport - Fix `is_invalid_use_of_sighash_single()` incompatibility with Bitcoin Core [#4122](https://github.com/rust-bitcoin/rust-bitcoin/pull/4122) diff --git a/bitcoin/Cargo.toml b/bitcoin/Cargo.toml index dcfd8959e..1dc142e0a 100644 --- a/bitcoin/Cargo.toml +++ b/bitcoin/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bitcoin" -version = "0.32.6" +version = "0.32.7" authors = ["Andrew Poelstra "] license = "CC0-1.0" repository = "https://github.com/rust-bitcoin/rust-bitcoin/" From 17a424173c83200b23dda0b4677ebd50def5ef5a Mon Sep 17 00:00:00 2001 From: Mathieu Ducroux Date: Mon, 26 May 2025 11:53:34 +0200 Subject: [PATCH 18/53] chore(workflow): update labeler workflow to use doge-master instead of master branch --- .github/workflows/manage-pr.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/manage-pr.yml b/.github/workflows/manage-pr.yml index 1ae250017..3ba1dcc55 100644 --- a/.github/workflows/manage-pr.yml +++ b/.github/workflows/manage-pr.yml @@ -9,18 +9,18 @@ jobs: pull-requests: write runs-on: ubuntu-latest steps: - - name: Checkout master + - name: Checkout doge-master uses: actions/checkout@v4 with: - path: master + path: doge-master - name: Checkout merge commit uses: actions/checkout@v4 with: path: merge ref: "refs/pull/${{ github.event.number }}/merge" - name: Generate label config - run: cd master && SCAN_DIR=../merge ./contrib/gen_label_config.sh + run: cd doge-master && SCAN_DIR=../merge ./contrib/gen_label_config.sh - name: Update labels uses: actions/labeler@v5 with: - configuration-path: master/.github/labeler.yml + configuration-path: doge-master/.github/labeler.yml From 2c3f453de903e5c3c00001e0edd07f5ffb730155 Mon Sep 17 00:00:00 2001 From: Mathieu Ducroux Date: Mon, 26 May 2025 12:54:49 +0200 Subject: [PATCH 19/53] chore(workflow): update labeler permissions to allow writing issues --- .github/workflows/manage-pr.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/manage-pr.yml b/.github/workflows/manage-pr.yml index 3ba1dcc55..885382208 100644 --- a/.github/workflows/manage-pr.yml +++ b/.github/workflows/manage-pr.yml @@ -6,6 +6,7 @@ jobs: labeler: permissions: contents: read + issues: write pull-requests: write runs-on: ubuntu-latest steps: From 22dc185b872186a642451696e1ce8da2200d57f2 Mon Sep 17 00:00:00 2001 From: Mathieu Ducroux Date: Fri, 23 May 2025 16:37:02 +0200 Subject: [PATCH 20/53] chore(dogecoin): set up crate --- .gitignore | 3 +++ bitcoin/Cargo.toml | 10 ++++------ 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/.gitignore b/.gitignore index 6055dd863..358b1850c 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,6 @@ bitcoin/dep_test # Fuzz artifacts hfuzz_target hfuzz_workspace + +# IDEA IDE files +.idea/ \ No newline at end of file diff --git a/bitcoin/Cargo.toml b/bitcoin/Cargo.toml index 1dc142e0a..8c9f6ae8a 100644 --- a/bitcoin/Cargo.toml +++ b/bitcoin/Cargo.toml @@ -1,13 +1,11 @@ [package] name = "bitcoin" -version = "0.32.7" -authors = ["Andrew Poelstra "] +version = "0.32.5-doge.0" license = "CC0-1.0" -repository = "https://github.com/rust-bitcoin/rust-bitcoin/" -documentation = "https://docs.rs/bitcoin/" -description = "General purpose library for using and interoperating with Bitcoin." +repository = "https://github.com/rust-dogecoin/rust-dogecoin/" +description = "General purpose library for using and interoperating with Bitcoin and Dogecoin." categories = ["cryptography::cryptocurrencies"] -keywords = [ "crypto", "bitcoin" ] +keywords = [ "crypto", "bitcoin", "dogecoin"] readme = "../README.md" edition = "2021" rust-version = "1.56.1" From d812d42c333e075ee491ad957c89fde8d2414520 Mon Sep 17 00:00:00 2001 From: Mathieu Ducroux Date: Fri, 23 May 2025 16:37:17 +0200 Subject: [PATCH 21/53] feat(dogecoin): add dogecoin AuxPow and Block --- bitcoin/src/dogecoin/mod.rs | 107 ++++++++++++++++++++++++++++++++++++ bitcoin/src/lib.rs | 1 + 2 files changed, 108 insertions(+) create mode 100644 bitcoin/src/dogecoin/mod.rs diff --git a/bitcoin/src/dogecoin/mod.rs b/bitcoin/src/dogecoin/mod.rs new file mode 100644 index 000000000..46e9fedbb --- /dev/null +++ b/bitcoin/src/dogecoin/mod.rs @@ -0,0 +1,107 @@ +use crate::block::Header; +use crate::{io, BlockHash, Transaction}; +use crate::consensus::{encode, Decodable, Encodable}; +use crate::io::{Read, Write}; +use crate::block::TxMerkleNode; +use crate::internal_macros::impl_consensus_encoding; + +/// AuxPow version bit, see https://github.com/dogecoin/dogecoin/blob/d7cc7f8bbb5f790942d0ed0617f62447e7675233/src/primitives/pureheader.h#L23 +pub const VERSION_AUXPOW: i32 = 1 << 8; + +fn is_auxpow(header: Header) -> bool { + (header.version.to_consensus() & VERSION_AUXPOW) != 0 +} + +/// Data for merge-mining AuxPoW. +/// +/// It contains the parent block's coinbase tx that can be verified to be in the parent block. +/// The transaction's input contains the hash to the actual merge-mined block. +#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Hash)] +pub struct AuxPow { + /// The parent block's coinbase tx. + pub coinbase_tx: Transaction, + /// The parent block's hash. + pub parent_hash: BlockHash, + /// The Merkle branch linking the coinbase tx to the parent block's Merkle root. + pub coinbase_branch: Vec, + /// The index of the coinbase tx in the Merkle tree. + pub coinbase_index: i32, + /// The Merkle branch linking the merge-mined block to the coinbase tx. + pub blockchain_branch: Vec, + /// The index of the merged-mined block in the Merkle tree. + pub blockchain_index: i32, + /// Parent block header (on which the PoW is done). + pub parent_block: Header, +} + +impl_consensus_encoding!( + AuxPow, + coinbase_tx, + parent_hash, + coinbase_branch, + coinbase_index, + blockchain_branch, + blockchain_index, + parent_block +); + +/// Dogecoin block. +/// +/// A collection of transactions with an attached proof of work. +/// The AuxPoW is present if the block was mined using merge-mining. +/// +/// See [Bitcoin Wiki: Block][wiki-block] and [Bitcoin Wiki: Merged_mining_specification][merge-mining] +/// for more information. +/// +/// [wiki-block]: https://en.bitcoin.it/wiki/Block +/// [merge-mining]: https://en.bitcoin.it/wiki/Merged_mining_specification +/// +/// ### Dogecoin Core References +/// +/// * [CBlock definition](https://github.com/dogecoin/dogecoin/blob/d7cc7f8bbb5f790942d0ed0617f62447e7675233/src/primitives/block.h#L65) +#[derive(PartialEq, Eq, Clone, Debug)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde", serde(crate = "actual_serde"))] +pub struct Block { + /// The block header. + pub header: Header, + /// AuxPoW structure, present if merged mining was used to mine this block. + pub auxpow: Option, + /// List of transactions contained in the block. + pub txdata: Vec, +} + +impl Block { + /// Returns the block hash computed as SHA256d(header). + pub fn block_hash(&self) -> BlockHash { self.header.block_hash() } +} + +impl Decodable for Block { + #[inline] + fn consensus_decode_from_finite_reader( + r: &mut R, + ) -> Result { + let header: Header = Decodable::consensus_decode_from_finite_reader(r)?; + let auxpow = if is_auxpow(header) { + Some(Decodable::consensus_decode_from_finite_reader(r)?) + } else { + None + }; + let txdata = Decodable::consensus_decode_from_finite_reader(r)?; + + Ok(Self { header, auxpow, txdata }) + } +} + +impl Encodable for Block { + #[inline] + fn consensus_encode(&self, w: &mut W) -> Result { + let mut len = 0; + len += self.header.consensus_encode(w)?; + if let Some(ref auxpow) = self.auxpow { + len += auxpow.consensus_encode(w)?; + } + len += self.txdata.consensus_encode(w)?; + Ok(len) + } +} \ No newline at end of file diff --git a/bitcoin/src/lib.rs b/bitcoin/src/lib.rs index d6d99c292..c335874e9 100644 --- a/bitcoin/src/lib.rs +++ b/bitcoin/src/lib.rs @@ -105,6 +105,7 @@ pub mod blockdata; pub mod consensus; // Private until we either make this a crate or flatten it - still to be decided. pub(crate) mod crypto; +pub mod dogecoin; pub mod error; pub mod hash_types; pub mod merkle_tree; From c463c5b636d2d534538ba8bacd32c87f226a5212 Mon Sep 17 00:00:00 2001 From: Mathieu Ducroux Date: Fri, 23 May 2025 16:55:49 +0200 Subject: [PATCH 22/53] feat(dogecoin): add module documentation and update AuxPow struct --- bitcoin/src/dogecoin/mod.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/bitcoin/src/dogecoin/mod.rs b/bitcoin/src/dogecoin/mod.rs index 46e9fedbb..31c031627 100644 --- a/bitcoin/src/dogecoin/mod.rs +++ b/bitcoin/src/dogecoin/mod.rs @@ -1,3 +1,8 @@ +//! Dogecoin module. +//! +//! This module provides support for de/serialization, parsing and execution on data structures and +//! network messages related to Dogecoin. + use crate::block::Header; use crate::{io, BlockHash, Transaction}; use crate::consensus::{encode, Decodable, Encodable}; @@ -16,7 +21,9 @@ fn is_auxpow(header: Header) -> bool { /// /// It contains the parent block's coinbase tx that can be verified to be in the parent block. /// The transaction's input contains the hash to the actual merge-mined block. -#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Hash)] +#[derive(PartialEq, Eq, Clone, Debug)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde", serde(crate = "actual_serde"))] pub struct AuxPow { /// The parent block's coinbase tx. pub coinbase_tx: Transaction, From 9a1c94fcf816abb6fb489fbef9eaee12da8615ae Mon Sep 17 00:00:00 2001 From: Mathieu Ducroux Date: Fri, 23 May 2025 17:13:08 +0200 Subject: [PATCH 23/53] add prelude import --- bitcoin/src/dogecoin/mod.rs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/bitcoin/src/dogecoin/mod.rs b/bitcoin/src/dogecoin/mod.rs index 31c031627..ba84d1da7 100644 --- a/bitcoin/src/dogecoin/mod.rs +++ b/bitcoin/src/dogecoin/mod.rs @@ -4,11 +4,12 @@ //! network messages related to Dogecoin. use crate::block::Header; -use crate::{io, BlockHash, Transaction}; -use crate::consensus::{encode, Decodable, Encodable}; -use crate::io::{Read, Write}; use crate::block::TxMerkleNode; +use crate::consensus::{encode, Decodable, Encodable}; use crate::internal_macros::impl_consensus_encoding; +use crate::io::{Read, Write}; +use crate::prelude::*; +use crate::{io, BlockHash, Transaction}; /// AuxPow version bit, see https://github.com/dogecoin/dogecoin/blob/d7cc7f8bbb5f790942d0ed0617f62447e7675233/src/primitives/pureheader.h#L23 pub const VERSION_AUXPOW: i32 = 1 << 8; @@ -80,7 +81,9 @@ pub struct Block { impl Block { /// Returns the block hash computed as SHA256d(header). - pub fn block_hash(&self) -> BlockHash { self.header.block_hash() } + pub fn block_hash(&self) -> BlockHash { + self.header.block_hash() + } } impl Decodable for Block { @@ -111,4 +114,4 @@ impl Encodable for Block { len += self.txdata.consensus_encode(w)?; Ok(len) } -} \ No newline at end of file +} From 91b5c2b022ad5694dcc00307169b2a803b7fd6b1 Mon Sep 17 00:00:00 2001 From: Mathieu Ducroux Date: Fri, 23 May 2025 17:21:37 +0200 Subject: [PATCH 24/53] update lock file --- Cargo.lock | 492 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 492 insertions(+) create mode 100644 Cargo.lock diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 000000000..28a4b973c --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,492 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "anyhow" +version = "1.0.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "base58ck" +version = "0.1.0" +dependencies = [ + "bitcoin-internals", + "bitcoin_hashes", + "hex-conservative", +] + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "bech32" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d965446196e3b7decd44aa7ee49e31d630118f90ef12f97900f262eb915c951d" + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + +[[package]] +name = "bitcoin" +version = "0.32.5-doge.0" +dependencies = [ + "base58ck", + "base64", + "bech32", + "bincode", + "bitcoin-internals", + "bitcoin-io", + "bitcoin-units", + "bitcoin_hashes", + "bitcoinconsensus", + "hex-conservative", + "hex_lit", + "mutagen", + "ordered", + "secp256k1", + "serde", + "serde_json", + "serde_test", +] + +[[package]] +name = "bitcoin-fuzz" +version = "0.0.1" +dependencies = [ + "bitcoin", + "honggfuzz", + "serde", + "serde_cbor", + "serde_json", +] + +[[package]] +name = "bitcoin-internals" +version = "0.3.0" +dependencies = [ + "serde", +] + +[[package]] +name = "bitcoin-io" +version = "0.1.2" + +[[package]] +name = "bitcoin-units" +version = "0.1.1" +dependencies = [ + "bitcoin-internals", + "serde", + "serde_json", + "serde_test", +] + +[[package]] +name = "bitcoin_hashes" +version = "0.14.0" +dependencies = [ + "bitcoin-io", + "hex-conservative", + "schemars", + "serde", + "serde_json", + "serde_test", +] + +[[package]] +name = "bitcoinconsensus" +version = "0.105.0+25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f260ac8fb2c621329013fc0ed371c940fcc512552dcbcb9095ed0179098c9e18" +dependencies = [ + "cc", +] + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "cc" +version = "1.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16595d3be041c03b09d08d0858631facccee9221e579704070e6e9e4915d3bc7" +dependencies = [ + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "dyn-clone" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c7a8fb8a9fbf66c1f703fe16184d10ca0ee9d23be5b4436400408ba54a95005" + +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "half" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b43ede17f21864e81be2fa654110bf1e793774238d86ef8555c37e6519c0403" + +[[package]] +name = "hex-conservative" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5313b072ce3c597065a808dbf612c4c8e8590bdbf8b579508bf7a762c5eae6cd" +dependencies = [ + "arrayvec", +] + +[[package]] +name = "hex_lit" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3011d1213f159867b13cfd6ac92d2cd5f1345762c63be3554e84092d85a50bbd" + +[[package]] +name = "honggfuzz" +version = "0.5.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc563d4f41b17364d5c48ded509f2bcf1c3f6ae9c7f203055b4a5c325072d57e" +dependencies = [ + "lazy_static", + "memmap2", + "rustc_version", + "semver", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "json" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "078e285eafdfb6c4b434e0d31e8cfcb5115b651496faca5749b88fafd4f23bfd" + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.172" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "memmap2" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd3f7eed9d3848f8b98834af67102b720745c4ec028fcd0aa0239277e7de374f" +dependencies = [ + "libc", +] + +[[package]] +name = "mutagen" +version = "0.2.0" +source = "git+https://github.com/llogiq/mutagen#a6377c4c3f360afeb7a287c1c17e4b69456d5f53" +dependencies = [ + "mutagen-core", + "mutagen-transform", +] + +[[package]] +name = "mutagen-core" +version = "0.2.0" +source = "git+https://github.com/llogiq/mutagen#a6377c4c3f360afeb7a287c1c17e4b69456d5f53" +dependencies = [ + "anyhow", + "json", + "lazy_static", + "proc-macro2", + "quote", + "serde", + "serde_json", + "syn 1.0.109", +] + +[[package]] +name = "mutagen-transform" +version = "0.2.0" +source = "git+https://github.com/llogiq/mutagen#a6377c4c3f360afeb7a287c1c17e4b69456d5f53" +dependencies = [ + "mutagen-core", + "proc-macro2", +] + +[[package]] +name = "ordered" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f0642533dea0bb58bd5cae31bafc1872429f0f12ac8c61fe2b4ba44f80b959b" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "schemars" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" +dependencies = [ + "dyn-clone", + "serde", + "serde_json", +] + +[[package]] +name = "secp256k1" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9465315bc9d4566e1724f0fffcbcc446268cb522e60f9a27bcded6b19c108113" +dependencies = [ + "bitcoin_hashes", + "rand", + "secp256k1-sys", + "serde", +] + +[[package]] +name = "secp256k1-sys" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4387882333d3aa8cb20530a17c69a3752e97837832f34f6dccc760e715001d9" +dependencies = [ + "cc", +] + +[[package]] +name = "semver" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" + +[[package]] +name = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_cbor" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45cd6d95391b16cd57e88b68be41d504183b7faae22030c0cc3b3f73dd57b2fd" +dependencies = [ + "byteorder", + "half", + "serde", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] + +[[package]] +name = "serde_json" +version = "1.0.140" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_test" +version = "1.0.177" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f901ee573cab6b3060453d2d5f0bae4e6d628c23c0a962ff9b5f1d7c8d4f1ed" +dependencies = [ + "serde", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "zerocopy" +version = "0.8.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] From e44902c8d89160de1a9f0439e969801fd0c2f3a2 Mon Sep 17 00:00:00 2001 From: Mathieu Ducroux Date: Fri, 23 May 2025 17:24:17 +0200 Subject: [PATCH 25/53] Revert "update lock file" This reverts commit bcfcca930c6a4af684a0faa6d05f8abbb491f5ae. --- Cargo.lock | 492 ----------------------------------------------------- 1 file changed, 492 deletions(-) delete mode 100644 Cargo.lock diff --git a/Cargo.lock b/Cargo.lock deleted file mode 100644 index 28a4b973c..000000000 --- a/Cargo.lock +++ /dev/null @@ -1,492 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 3 - -[[package]] -name = "anyhow" -version = "1.0.98" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" - -[[package]] -name = "arrayvec" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" - -[[package]] -name = "base58ck" -version = "0.1.0" -dependencies = [ - "bitcoin-internals", - "bitcoin_hashes", - "hex-conservative", -] - -[[package]] -name = "base64" -version = "0.21.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" - -[[package]] -name = "bech32" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d965446196e3b7decd44aa7ee49e31d630118f90ef12f97900f262eb915c951d" - -[[package]] -name = "bincode" -version = "1.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" -dependencies = [ - "serde", -] - -[[package]] -name = "bitcoin" -version = "0.32.5-doge.0" -dependencies = [ - "base58ck", - "base64", - "bech32", - "bincode", - "bitcoin-internals", - "bitcoin-io", - "bitcoin-units", - "bitcoin_hashes", - "bitcoinconsensus", - "hex-conservative", - "hex_lit", - "mutagen", - "ordered", - "secp256k1", - "serde", - "serde_json", - "serde_test", -] - -[[package]] -name = "bitcoin-fuzz" -version = "0.0.1" -dependencies = [ - "bitcoin", - "honggfuzz", - "serde", - "serde_cbor", - "serde_json", -] - -[[package]] -name = "bitcoin-internals" -version = "0.3.0" -dependencies = [ - "serde", -] - -[[package]] -name = "bitcoin-io" -version = "0.1.2" - -[[package]] -name = "bitcoin-units" -version = "0.1.1" -dependencies = [ - "bitcoin-internals", - "serde", - "serde_json", - "serde_test", -] - -[[package]] -name = "bitcoin_hashes" -version = "0.14.0" -dependencies = [ - "bitcoin-io", - "hex-conservative", - "schemars", - "serde", - "serde_json", - "serde_test", -] - -[[package]] -name = "bitcoinconsensus" -version = "0.105.0+25.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f260ac8fb2c621329013fc0ed371c940fcc512552dcbcb9095ed0179098c9e18" -dependencies = [ - "cc", -] - -[[package]] -name = "byteorder" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" - -[[package]] -name = "cc" -version = "1.2.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16595d3be041c03b09d08d0858631facccee9221e579704070e6e9e4915d3bc7" -dependencies = [ - "shlex", -] - -[[package]] -name = "cfg-if" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" - -[[package]] -name = "dyn-clone" -version = "1.0.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c7a8fb8a9fbf66c1f703fe16184d10ca0ee9d23be5b4436400408ba54a95005" - -[[package]] -name = "getrandom" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" -dependencies = [ - "cfg-if", - "libc", - "wasi", -] - -[[package]] -name = "half" -version = "1.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b43ede17f21864e81be2fa654110bf1e793774238d86ef8555c37e6519c0403" - -[[package]] -name = "hex-conservative" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5313b072ce3c597065a808dbf612c4c8e8590bdbf8b579508bf7a762c5eae6cd" -dependencies = [ - "arrayvec", -] - -[[package]] -name = "hex_lit" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3011d1213f159867b13cfd6ac92d2cd5f1345762c63be3554e84092d85a50bbd" - -[[package]] -name = "honggfuzz" -version = "0.5.57" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc563d4f41b17364d5c48ded509f2bcf1c3f6ae9c7f203055b4a5c325072d57e" -dependencies = [ - "lazy_static", - "memmap2", - "rustc_version", - "semver", -] - -[[package]] -name = "itoa" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" - -[[package]] -name = "json" -version = "0.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "078e285eafdfb6c4b434e0d31e8cfcb5115b651496faca5749b88fafd4f23bfd" - -[[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" - -[[package]] -name = "libc" -version = "0.2.172" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" - -[[package]] -name = "memchr" -version = "2.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" - -[[package]] -name = "memmap2" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd3f7eed9d3848f8b98834af67102b720745c4ec028fcd0aa0239277e7de374f" -dependencies = [ - "libc", -] - -[[package]] -name = "mutagen" -version = "0.2.0" -source = "git+https://github.com/llogiq/mutagen#a6377c4c3f360afeb7a287c1c17e4b69456d5f53" -dependencies = [ - "mutagen-core", - "mutagen-transform", -] - -[[package]] -name = "mutagen-core" -version = "0.2.0" -source = "git+https://github.com/llogiq/mutagen#a6377c4c3f360afeb7a287c1c17e4b69456d5f53" -dependencies = [ - "anyhow", - "json", - "lazy_static", - "proc-macro2", - "quote", - "serde", - "serde_json", - "syn 1.0.109", -] - -[[package]] -name = "mutagen-transform" -version = "0.2.0" -source = "git+https://github.com/llogiq/mutagen#a6377c4c3f360afeb7a287c1c17e4b69456d5f53" -dependencies = [ - "mutagen-core", - "proc-macro2", -] - -[[package]] -name = "ordered" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f0642533dea0bb58bd5cae31bafc1872429f0f12ac8c61fe2b4ba44f80b959b" - -[[package]] -name = "ppv-lite86" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" -dependencies = [ - "zerocopy", -] - -[[package]] -name = "proc-macro2" -version = "1.0.95" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quote" -version = "1.0.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "rand" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" -dependencies = [ - "libc", - "rand_chacha", - "rand_core", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom", -] - -[[package]] -name = "rustc_version" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" -dependencies = [ - "semver", -] - -[[package]] -name = "ryu" -version = "1.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" - -[[package]] -name = "schemars" -version = "0.8.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" -dependencies = [ - "dyn-clone", - "serde", - "serde_json", -] - -[[package]] -name = "secp256k1" -version = "0.29.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9465315bc9d4566e1724f0fffcbcc446268cb522e60f9a27bcded6b19c108113" -dependencies = [ - "bitcoin_hashes", - "rand", - "secp256k1-sys", - "serde", -] - -[[package]] -name = "secp256k1-sys" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4387882333d3aa8cb20530a17c69a3752e97837832f34f6dccc760e715001d9" -dependencies = [ - "cc", -] - -[[package]] -name = "semver" -version = "1.0.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" - -[[package]] -name = "serde" -version = "1.0.219" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_cbor" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45cd6d95391b16cd57e88b68be41d504183b7faae22030c0cc3b3f73dd57b2fd" -dependencies = [ - "byteorder", - "half", - "serde", -] - -[[package]] -name = "serde_derive" -version = "1.0.219" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.101", -] - -[[package]] -name = "serde_json" -version = "1.0.140" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" -dependencies = [ - "itoa", - "memchr", - "ryu", - "serde", -] - -[[package]] -name = "serde_test" -version = "1.0.177" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f901ee573cab6b3060453d2d5f0bae4e6d628c23c0a962ff9b5f1d7c8d4f1ed" -dependencies = [ - "serde", -] - -[[package]] -name = "shlex" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" - -[[package]] -name = "syn" -version = "1.0.109" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "syn" -version = "2.0.101" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "unicode-ident" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" - -[[package]] -name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" - -[[package]] -name = "zerocopy" -version = "0.8.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb" -dependencies = [ - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.8.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.101", -] From 790dcf0c08b16b87a485cfc706e22238c484fce2 Mon Sep 17 00:00:00 2001 From: Mathieu Ducroux Date: Mon, 26 May 2025 08:52:58 +0200 Subject: [PATCH 26/53] run formatter in nightly mode --- bitcoin/src/address/mod.rs | 11 +++--- bitcoin/src/blockdata/constants.rs | 2 +- bitcoin/src/blockdata/locktime/absolute.rs | 12 ++----- bitcoin/src/blockdata/transaction.rs | 14 +++----- bitcoin/src/consensus/encode.rs | 8 ++--- bitcoin/src/consensus/params.rs | 2 +- bitcoin/src/dogecoin/mod.rs | 11 ++---- bitcoin/src/network.rs | 20 +++-------- bitcoin/src/p2p/message.rs | 4 +-- bitcoin/src/p2p/mod.rs | 6 ++-- bitcoin/src/pow.rs | 41 +++++++++++++--------- bitcoin/tests/psbt-sign-taproot.rs | 5 ++- io/src/lib.rs | 3 +- units/src/lib.rs | 7 ++-- 14 files changed, 57 insertions(+), 89 deletions(-) diff --git a/bitcoin/src/address/mod.rs b/bitcoin/src/address/mod.rs index c05f32ac8..a7e02b78b 100644 --- a/bitcoin/src/address/mod.rs +++ b/bitcoin/src/address/mod.rs @@ -249,17 +249,17 @@ pub enum AddressData { /// Data encoded by a P2PKH address. P2pkh { /// The pubkey hash used to encumber outputs to this address. - pubkey_hash: PubkeyHash + pubkey_hash: PubkeyHash, }, /// Data encoded by a P2SH address. P2sh { /// The script hash used to encumber outputs to this address. - script_hash: ScriptHash + script_hash: ScriptHash, }, /// Data encoded by a Segwit address. Segwit { /// The witness program used to encumber outputs to this address. - witness_program: WitnessProgram + witness_program: WitnessProgram, }, } @@ -565,7 +565,10 @@ impl Address { pub fn is_spend_standard(&self) -> bool { self.address_type().is_some() } /// Constructs an [`Address`] from an output script (`scriptPubkey`). - pub fn from_script(script: &Script, params: impl AsRef) -> Result { + pub fn from_script( + script: &Script, + params: impl AsRef, + ) -> Result { let network = params.as_ref().network; if script.is_p2pkh() { let bytes = script.as_bytes()[3..23].try_into().expect("statically 20B long"); diff --git a/bitcoin/src/blockdata/constants.rs b/bitcoin/src/blockdata/constants.rs index 4d98ba907..34667881f 100644 --- a/bitcoin/src/blockdata/constants.rs +++ b/bitcoin/src/blockdata/constants.rs @@ -261,8 +261,8 @@ mod test { use hex::test_hex_unwrap as hex; use super::*; - use crate::consensus::params; use crate::consensus::encode::serialize; + use crate::consensus::params; #[test] fn bitcoin_genesis_first_transaction() { diff --git a/bitcoin/src/blockdata/locktime/absolute.rs b/bitcoin/src/blockdata/locktime/absolute.rs index 074ca26a7..77728b16f 100644 --- a/bitcoin/src/blockdata/locktime/absolute.rs +++ b/bitcoin/src/blockdata/locktime/absolute.rs @@ -297,25 +297,19 @@ impl FromStr for LockTime { impl TryFrom<&str> for LockTime { type Error = ParseIntError; - fn try_from(s: &str) -> Result { - LockTime::from_str(s) - } + fn try_from(s: &str) -> Result { LockTime::from_str(s) } } impl TryFrom for LockTime { type Error = ParseIntError; - fn try_from(s: String) -> Result { - LockTime::from_str(&s) - } + fn try_from(s: String) -> Result { LockTime::from_str(&s) } } impl TryFrom> for LockTime { type Error = ParseIntError; - fn try_from(s: Box) -> Result { - LockTime::from_str(&s) - } + fn try_from(s: Box) -> Result { LockTime::from_str(&s) } } impl From for LockTime { diff --git a/bitcoin/src/blockdata/transaction.rs b/bitcoin/src/blockdata/transaction.rs index 1e11ac66b..446b96898 100644 --- a/bitcoin/src/blockdata/transaction.rs +++ b/bitcoin/src/blockdata/transaction.rs @@ -11,8 +11,8 @@ //! This module provides the structures and functions needed to support transactions. //! -use core::{cmp, fmt}; use core::str::FromStr; +use core::{cmp, fmt}; use hashes::{sha256d, Hash}; use internals::write_err; @@ -539,25 +539,19 @@ impl FromStr for Sequence { impl TryFrom<&str> for Sequence { type Error = ParseIntError; - fn try_from(s: &str) -> Result { - Sequence::from_str(s) - } + fn try_from(s: &str) -> Result { Sequence::from_str(s) } } impl TryFrom for Sequence { type Error = ParseIntError; - fn try_from(s: String) -> Result { - Sequence::from_str(&s) - } + fn try_from(s: String) -> Result { Sequence::from_str(&s) } } impl TryFrom> for Sequence { type Error = ParseIntError; - fn try_from(s: Box) -> Result { - Sequence::from_str(&s) - } + fn try_from(s: Box) -> Result { Sequence::from_str(&s) } } /// Bitcoin transaction output. diff --git a/bitcoin/src/consensus/encode.rs b/bitcoin/src/consensus/encode.rs index b885e9318..d0bb754a3 100644 --- a/bitcoin/src/consensus/encode.rs +++ b/bitcoin/src/consensus/encode.rs @@ -406,9 +406,7 @@ macro_rules! impl_int_encodable { ($ty:ident, $meth_dec:ident, $meth_enc:ident) => { impl Decodable for $ty { #[inline] - fn consensus_decode( - r: &mut R, - ) -> core::result::Result { + fn consensus_decode(r: &mut R) -> core::result::Result { ReadExt::$meth_dec(r) } } @@ -593,9 +591,7 @@ macro_rules! impl_array { impl Decodable for [u8; $size] { #[inline] - fn consensus_decode( - r: &mut R, - ) -> core::result::Result { + fn consensus_decode(r: &mut R) -> core::result::Result { let mut ret = [0; $size]; r.read_slice(&mut ret)?; Ok(ret) diff --git a/bitcoin/src/consensus/params.rs b/bitcoin/src/consensus/params.rs index 93a36c220..325a6c7ee 100644 --- a/bitcoin/src/consensus/params.rs +++ b/bitcoin/src/consensus/params.rs @@ -76,7 +76,7 @@ pub static SIGNET: Params = Params::SIGNET; /// The regtest parameters. pub static REGTEST: Params = Params::REGTEST; -#[allow(deprecated)] // For `pow_limit`. +#[allow(deprecated)] // For `pow_limit`. impl Params { /// The mainnet parameters (alias for `Params::MAINNET`). pub const BITCOIN: Params = Params::MAINNET; diff --git a/bitcoin/src/dogecoin/mod.rs b/bitcoin/src/dogecoin/mod.rs index ba84d1da7..f45e10a6b 100644 --- a/bitcoin/src/dogecoin/mod.rs +++ b/bitcoin/src/dogecoin/mod.rs @@ -3,8 +3,7 @@ //! This module provides support for de/serialization, parsing and execution on data structures and //! network messages related to Dogecoin. -use crate::block::Header; -use crate::block::TxMerkleNode; +use crate::block::{Header, TxMerkleNode}; use crate::consensus::{encode, Decodable, Encodable}; use crate::internal_macros::impl_consensus_encoding; use crate::io::{Read, Write}; @@ -14,9 +13,7 @@ use crate::{io, BlockHash, Transaction}; /// AuxPow version bit, see https://github.com/dogecoin/dogecoin/blob/d7cc7f8bbb5f790942d0ed0617f62447e7675233/src/primitives/pureheader.h#L23 pub const VERSION_AUXPOW: i32 = 1 << 8; -fn is_auxpow(header: Header) -> bool { - (header.version.to_consensus() & VERSION_AUXPOW) != 0 -} +fn is_auxpow(header: Header) -> bool { (header.version.to_consensus() & VERSION_AUXPOW) != 0 } /// Data for merge-mining AuxPoW. /// @@ -81,9 +78,7 @@ pub struct Block { impl Block { /// Returns the block hash computed as SHA256d(header). - pub fn block_hash(&self) -> BlockHash { - self.header.block_hash() - } + pub fn block_hash(&self) -> BlockHash { self.header.block_hash() } } impl Decodable for Block { diff --git a/bitcoin/src/network.rs b/bitcoin/src/network.rs index 8df48b0be..9bea93b9d 100644 --- a/bitcoin/src/network.rs +++ b/bitcoin/src/network.rs @@ -333,26 +333,14 @@ mod tests { #[test] fn serialize_test() { assert_eq!(serialize(&Network::Bitcoin.magic()), &[0xf9, 0xbe, 0xb4, 0xd9]); - assert_eq!( - serialize(&Network::Testnet.magic()), - &[0x0b, 0x11, 0x09, 0x07] - ); - assert_eq!( - serialize(&Network::Testnet4.magic()), - &[0x1c, 0x16, 0x3f, 0x28] - ); + assert_eq!(serialize(&Network::Testnet.magic()), &[0x0b, 0x11, 0x09, 0x07]); + assert_eq!(serialize(&Network::Testnet4.magic()), &[0x1c, 0x16, 0x3f, 0x28]); assert_eq!(serialize(&Network::Signet.magic()), &[0x0a, 0x03, 0xcf, 0x40]); assert_eq!(serialize(&Network::Regtest.magic()), &[0xfa, 0xbf, 0xb5, 0xda]); assert_eq!(deserialize(&[0xf9, 0xbe, 0xb4, 0xd9]).ok(), Some(Network::Bitcoin.magic())); - assert_eq!( - deserialize(&[0x0b, 0x11, 0x09, 0x07]).ok(), - Some(Network::Testnet.magic()) - ); - assert_eq!( - deserialize(&[0x1c, 0x16, 0x3f, 0x28]).ok(), - Some(Network::Testnet4.magic()) - ); + assert_eq!(deserialize(&[0x0b, 0x11, 0x09, 0x07]).ok(), Some(Network::Testnet.magic())); + assert_eq!(deserialize(&[0x1c, 0x16, 0x3f, 0x28]).ok(), Some(Network::Testnet4.magic())); assert_eq!(deserialize(&[0x0a, 0x03, 0xcf, 0x40]).ok(), Some(Network::Signet.magic())); assert_eq!(deserialize(&[0xfa, 0xbf, 0xb5, 0xda]).ok(), Some(Network::Regtest.magic())); } diff --git a/bitcoin/src/p2p/message.rs b/bitcoin/src/p2p/message.rs index 11e10cafd..cf1d52f90 100644 --- a/bitcoin/src/p2p/message.rs +++ b/bitcoin/src/p2p/message.rs @@ -312,9 +312,7 @@ impl RawNetworkMessage { } /// Consumes the [RawNetworkMessage] instance and returns the inner payload. - pub fn into_payload(self) -> NetworkMessage { - self.payload - } + pub fn into_payload(self) -> NetworkMessage { self.payload } /// The actual message data pub fn payload(&self) -> &NetworkMessage { &self.payload } diff --git a/bitcoin/src/p2p/mod.rs b/bitcoin/src/p2p/mod.rs index 988dc0d05..372bf1188 100644 --- a/bitcoin/src/p2p/mod.rs +++ b/bitcoin/src/p2p/mod.rs @@ -29,8 +29,8 @@ use io::{Read, Write}; use crate::consensus::encode::{self, Decodable, Encodable}; use crate::consensus::Params; -use crate::prelude::*; use crate::network::Network; +use crate::prelude::*; #[rustfmt::skip] #[doc(inline)] @@ -234,9 +234,7 @@ impl Magic { pub fn to_bytes(self) -> [u8; 4] { self.0 } /// Returns the magic bytes for the network defined by `params`. - pub fn from_params(params: impl AsRef) -> Self { - params.as_ref().network.into() - } + pub fn from_params(params: impl AsRef) -> Self { params.as_ref().network.into() } } impl FromStr for Magic { diff --git a/bitcoin/src/pow.rs b/bitcoin/src/pow.rs index d4cb696fc..0cc589611 100644 --- a/bitcoin/src/pow.rs +++ b/bitcoin/src/pow.rs @@ -19,7 +19,9 @@ use crate::block::Header; use crate::blockdata::block::BlockHash; use crate::consensus::encode::{self, Decodable, Encodable}; use crate::consensus::Params; -use crate::error::{ContainsPrefixError, MissingPrefixError, ParseIntError, PrefixedHexError, UnprefixedHexError}; +use crate::error::{ + ContainsPrefixError, MissingPrefixError, ParseIntError, PrefixedHexError, UnprefixedHexError, +}; /// Implement traits and methods shared by `Target` and `Work`. macro_rules! do_impl { @@ -1798,8 +1800,11 @@ mod tests { #[test] fn compact_target_from_upwards_difficulty_adjustment_using_headers() { - use crate::{block::Version, constants::genesis_block, TxMerkleNode}; use hashes::Hash; + + use crate::block::Version; + use crate::constants::genesis_block; + use crate::TxMerkleNode; let params = Params::new(crate::Network::Signet); let epoch_start = genesis_block(¶ms).header; // Block 2015, the only information used are `bits` and `time` @@ -1809,27 +1814,30 @@ mod tests { merkle_root: TxMerkleNode::all_zeros(), time: 1599332177, bits: epoch_start.bits, - nonce: epoch_start.nonce + nonce: epoch_start.nonce, }; - let adjustment = CompactTarget::from_header_difficulty_adjustment(epoch_start, current, params); + let adjustment = + CompactTarget::from_header_difficulty_adjustment(epoch_start, current, params); let adjustment_bits = CompactTarget::from_consensus(503394215); // Block 2016 compact target assert_eq!(adjustment, adjustment_bits); } #[test] fn compact_target_from_downwards_difficulty_adjustment_using_headers() { - use crate::{block::Version, TxMerkleNode}; use hashes::Hash; + + use crate::block::Version; + use crate::TxMerkleNode; let params = Params::new(crate::Network::Signet); let starting_bits = CompactTarget::from_consensus(503394215); // Block 2016 compact target - // Block 2016, the only information used is `time` + // Block 2016, the only information used is `time` let epoch_start = Header { version: Version::ONE, prev_blockhash: BlockHash::all_zeros(), merkle_root: TxMerkleNode::all_zeros(), time: 1599332844, bits: starting_bits, - nonce: 0 + nonce: 0, }; // Block 4031, the only information used are `bits` and `time` let current = Header { @@ -1838,9 +1846,10 @@ mod tests { merkle_root: TxMerkleNode::all_zeros(), time: 1600591200, bits: starting_bits, - nonce: 0 + nonce: 0, }; - let adjustment = CompactTarget::from_header_difficulty_adjustment(epoch_start, current, params); + let adjustment = + CompactTarget::from_header_difficulty_adjustment(epoch_start, current, params); let adjustment_bits = CompactTarget::from_consensus(503397348); // Block 4032 compact target assert_eq!(adjustment, adjustment_bits); } @@ -1851,9 +1860,8 @@ mod tests { let starting_bits = CompactTarget::from_consensus(503403001); let timespan = (0.2 * params.pow_target_timespan as f64) as u64; let got = CompactTarget::from_next_work_required(starting_bits, timespan, params); - let want = Target::from_compact(starting_bits) - .min_transition_threshold() - .to_compact_lossy(); + let want = + Target::from_compact(starting_bits).min_transition_threshold().to_compact_lossy(); assert_eq!(got, want); } @@ -1861,11 +1869,10 @@ mod tests { fn compact_target_from_minimum_downward_difficulty_adjustment() { let params = Params::new(crate::Network::Signet); let starting_bits = CompactTarget::from_consensus(403403001); // High difficulty for Signet - let timespan = 5 * params.pow_target_timespan; // Really slow. + let timespan = 5 * params.pow_target_timespan; // Really slow. let got = CompactTarget::from_next_work_required(starting_bits, timespan, ¶ms); - let want = Target::from_compact(starting_bits) - .max_transition_threshold(params) - .to_compact_lossy(); + let want = + Target::from_compact(starting_bits).max_transition_threshold(params).to_compact_lossy(); assert_eq!(got, want); } @@ -1873,7 +1880,7 @@ mod tests { fn compact_target_from_adjustment_is_max_target() { let params = Params::new(crate::Network::Signet); let starting_bits = CompactTarget::from_consensus(503543726); // Genesis compact target on Signet - let timespan = 5 * params.pow_target_timespan; // Really slow. + let timespan = 5 * params.pow_target_timespan; // Really slow. let got = CompactTarget::from_next_work_required(starting_bits, timespan, ¶ms); let want = params.max_attainable_target.to_compact_lossy(); assert_eq!(got, want); diff --git a/bitcoin/tests/psbt-sign-taproot.rs b/bitcoin/tests/psbt-sign-taproot.rs index 4cf7616b7..78fbcdd60 100644 --- a/bitcoin/tests/psbt-sign-taproot.rs +++ b/bitcoin/tests/psbt-sign-taproot.rs @@ -31,13 +31,12 @@ fn psbt_sign_taproot() { _secp: &Secp256k1, ) -> Result, Self::Error> { match key_request { - KeyRequest::Bip32((mfp, _)) => { + KeyRequest::Bip32((mfp, _)) => if mfp == self.mfp { Ok(Some(self.sk)) } else { Err(SignError::KeyNotFound) - } - } + }, _ => Err(SignError::KeyNotFound), } } diff --git a/io/src/lib.rs b/io/src/lib.rs index 82d51acc7..e7878625e 100644 --- a/io/src/lib.rs +++ b/io/src/lib.rs @@ -211,8 +211,7 @@ impl> Read for Cursor { let start_pos = self.pos.try_into().unwrap_or(inner.len()); let read = core::cmp::min(inner.len().saturating_sub(start_pos), buf.len()); buf[..read].copy_from_slice(&inner[start_pos..start_pos + read]); - self.pos = - self.pos.saturating_add(read.try_into().unwrap_or(u64::MAX /* unreachable */)); + self.pos = self.pos.saturating_add(read.try_into().unwrap_or(u64::MAX /* unreachable */)); Ok(read) } } diff --git a/units/src/lib.rs b/units/src/lib.rs index b2c278795..393b5ced1 100644 --- a/units/src/lib.rs +++ b/units/src/lib.rs @@ -45,17 +45,14 @@ pub mod parse; #[cfg(feature = "alloc")] pub mod weight; +pub use self::amount::ParseAmountError; #[doc(inline)] pub use self::amount::{Amount, SignedAmount}; -pub use self::amount::ParseAmountError; #[cfg(feature = "alloc")] pub use self::parse::ParseIntError; #[cfg(feature = "alloc")] #[doc(inline)] -pub use self::{ - fee_rate::FeeRate, - weight::Weight, -}; +pub use self::{fee_rate::FeeRate, weight::Weight}; #[rustfmt::skip] #[allow(unused_imports)] From 8f6dcdb8c0c68d6a4b11d3ac17dce2949c0cc36a Mon Sep 17 00:00:00 2001 From: Mathieu Ducroux Date: Mon, 26 May 2025 09:05:57 +0200 Subject: [PATCH 27/53] update lock files --- contrib/update-lock-files.sh | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/contrib/update-lock-files.sh b/contrib/update-lock-files.sh index 02ba1f7c8..483ee10c7 100755 --- a/contrib/update-lock-files.sh +++ b/contrib/update-lock-files.sh @@ -1,11 +1,16 @@ #!/usr/bin/env bash # # Update the minimal/recent lock file - set -euo pipefail +if [[ "$(uname)" == "Darwin" ]]; then + CP="cp -f" +else + CP="cp --force" +fi + for file in Cargo-minimal.lock Cargo-recent.lock; do - cp --force "$file" Cargo.lock + $CP "$file" Cargo.lock cargo check - cp --force Cargo.lock "$file" + $CP Cargo.lock "$file" done From 8af5eafdd2ed5b025ec7ccd48c2545f08d9c0374 Mon Sep 17 00:00:00 2001 From: Mathieu Ducroux Date: Mon, 26 May 2025 09:21:53 +0200 Subject: [PATCH 28/53] update doc --- bitcoin/src/dogecoin/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bitcoin/src/dogecoin/mod.rs b/bitcoin/src/dogecoin/mod.rs index f45e10a6b..3c97ce288 100644 --- a/bitcoin/src/dogecoin/mod.rs +++ b/bitcoin/src/dogecoin/mod.rs @@ -10,7 +10,7 @@ use crate::io::{Read, Write}; use crate::prelude::*; use crate::{io, BlockHash, Transaction}; -/// AuxPow version bit, see https://github.com/dogecoin/dogecoin/blob/d7cc7f8bbb5f790942d0ed0617f62447e7675233/src/primitives/pureheader.h#L23 +/// AuxPow version bit, see pub const VERSION_AUXPOW: i32 = 1 << 8; fn is_auxpow(header: Header) -> bool { (header.version.to_consensus() & VERSION_AUXPOW) != 0 } From 0549e83b22019f294bd2a4d842c3fbf376f16452 Mon Sep 17 00:00:00 2001 From: Mathieu Ducroux Date: Mon, 26 May 2025 09:55:12 +0200 Subject: [PATCH 29/53] Revert "add write permission to labeller for issues" This reverts commit 67bbf4c7da85ec8ac7051786c2af99656373ab98. --- .github/workflows/manage-pr.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/manage-pr.yml b/.github/workflows/manage-pr.yml index 885382208..3ba1dcc55 100644 --- a/.github/workflows/manage-pr.yml +++ b/.github/workflows/manage-pr.yml @@ -6,7 +6,6 @@ jobs: labeler: permissions: contents: read - issues: write pull-requests: write runs-on: ubuntu-latest steps: From 6d91df4443f6655f52c7eb16ef3324c6e815ebb8 Mon Sep 17 00:00:00 2001 From: Mathieu Ducroux Date: Mon, 26 May 2025 10:58:43 +0200 Subject: [PATCH 30/53] Revert "run formatter in nightly mode" This reverts commit 4ef012980f83797afd3741f945457b0396ceb656. --- bitcoin/src/address/mod.rs | 11 +++--- bitcoin/src/blockdata/constants.rs | 2 +- bitcoin/src/blockdata/locktime/absolute.rs | 12 +++++-- bitcoin/src/blockdata/transaction.rs | 14 +++++--- bitcoin/src/consensus/encode.rs | 8 +++-- bitcoin/src/consensus/params.rs | 2 +- bitcoin/src/dogecoin/mod.rs | 11 ++++-- bitcoin/src/network.rs | 20 ++++++++--- bitcoin/src/p2p/message.rs | 4 ++- bitcoin/src/p2p/mod.rs | 6 ++-- bitcoin/src/pow.rs | 41 +++++++++------------- bitcoin/tests/psbt-sign-taproot.rs | 5 +-- io/src/lib.rs | 3 +- units/src/lib.rs | 7 ++-- 14 files changed, 89 insertions(+), 57 deletions(-) diff --git a/bitcoin/src/address/mod.rs b/bitcoin/src/address/mod.rs index a7e02b78b..c05f32ac8 100644 --- a/bitcoin/src/address/mod.rs +++ b/bitcoin/src/address/mod.rs @@ -249,17 +249,17 @@ pub enum AddressData { /// Data encoded by a P2PKH address. P2pkh { /// The pubkey hash used to encumber outputs to this address. - pubkey_hash: PubkeyHash, + pubkey_hash: PubkeyHash }, /// Data encoded by a P2SH address. P2sh { /// The script hash used to encumber outputs to this address. - script_hash: ScriptHash, + script_hash: ScriptHash }, /// Data encoded by a Segwit address. Segwit { /// The witness program used to encumber outputs to this address. - witness_program: WitnessProgram, + witness_program: WitnessProgram }, } @@ -565,10 +565,7 @@ impl Address { pub fn is_spend_standard(&self) -> bool { self.address_type().is_some() } /// Constructs an [`Address`] from an output script (`scriptPubkey`). - pub fn from_script( - script: &Script, - params: impl AsRef, - ) -> Result { + pub fn from_script(script: &Script, params: impl AsRef) -> Result { let network = params.as_ref().network; if script.is_p2pkh() { let bytes = script.as_bytes()[3..23].try_into().expect("statically 20B long"); diff --git a/bitcoin/src/blockdata/constants.rs b/bitcoin/src/blockdata/constants.rs index 34667881f..4d98ba907 100644 --- a/bitcoin/src/blockdata/constants.rs +++ b/bitcoin/src/blockdata/constants.rs @@ -261,8 +261,8 @@ mod test { use hex::test_hex_unwrap as hex; use super::*; - use crate::consensus::encode::serialize; use crate::consensus::params; + use crate::consensus::encode::serialize; #[test] fn bitcoin_genesis_first_transaction() { diff --git a/bitcoin/src/blockdata/locktime/absolute.rs b/bitcoin/src/blockdata/locktime/absolute.rs index 77728b16f..074ca26a7 100644 --- a/bitcoin/src/blockdata/locktime/absolute.rs +++ b/bitcoin/src/blockdata/locktime/absolute.rs @@ -297,19 +297,25 @@ impl FromStr for LockTime { impl TryFrom<&str> for LockTime { type Error = ParseIntError; - fn try_from(s: &str) -> Result { LockTime::from_str(s) } + fn try_from(s: &str) -> Result { + LockTime::from_str(s) + } } impl TryFrom for LockTime { type Error = ParseIntError; - fn try_from(s: String) -> Result { LockTime::from_str(&s) } + fn try_from(s: String) -> Result { + LockTime::from_str(&s) + } } impl TryFrom> for LockTime { type Error = ParseIntError; - fn try_from(s: Box) -> Result { LockTime::from_str(&s) } + fn try_from(s: Box) -> Result { + LockTime::from_str(&s) + } } impl From for LockTime { diff --git a/bitcoin/src/blockdata/transaction.rs b/bitcoin/src/blockdata/transaction.rs index 446b96898..1e11ac66b 100644 --- a/bitcoin/src/blockdata/transaction.rs +++ b/bitcoin/src/blockdata/transaction.rs @@ -11,8 +11,8 @@ //! This module provides the structures and functions needed to support transactions. //! -use core::str::FromStr; use core::{cmp, fmt}; +use core::str::FromStr; use hashes::{sha256d, Hash}; use internals::write_err; @@ -539,19 +539,25 @@ impl FromStr for Sequence { impl TryFrom<&str> for Sequence { type Error = ParseIntError; - fn try_from(s: &str) -> Result { Sequence::from_str(s) } + fn try_from(s: &str) -> Result { + Sequence::from_str(s) + } } impl TryFrom for Sequence { type Error = ParseIntError; - fn try_from(s: String) -> Result { Sequence::from_str(&s) } + fn try_from(s: String) -> Result { + Sequence::from_str(&s) + } } impl TryFrom> for Sequence { type Error = ParseIntError; - fn try_from(s: Box) -> Result { Sequence::from_str(&s) } + fn try_from(s: Box) -> Result { + Sequence::from_str(&s) + } } /// Bitcoin transaction output. diff --git a/bitcoin/src/consensus/encode.rs b/bitcoin/src/consensus/encode.rs index d0bb754a3..b885e9318 100644 --- a/bitcoin/src/consensus/encode.rs +++ b/bitcoin/src/consensus/encode.rs @@ -406,7 +406,9 @@ macro_rules! impl_int_encodable { ($ty:ident, $meth_dec:ident, $meth_enc:ident) => { impl Decodable for $ty { #[inline] - fn consensus_decode(r: &mut R) -> core::result::Result { + fn consensus_decode( + r: &mut R, + ) -> core::result::Result { ReadExt::$meth_dec(r) } } @@ -591,7 +593,9 @@ macro_rules! impl_array { impl Decodable for [u8; $size] { #[inline] - fn consensus_decode(r: &mut R) -> core::result::Result { + fn consensus_decode( + r: &mut R, + ) -> core::result::Result { let mut ret = [0; $size]; r.read_slice(&mut ret)?; Ok(ret) diff --git a/bitcoin/src/consensus/params.rs b/bitcoin/src/consensus/params.rs index 325a6c7ee..93a36c220 100644 --- a/bitcoin/src/consensus/params.rs +++ b/bitcoin/src/consensus/params.rs @@ -76,7 +76,7 @@ pub static SIGNET: Params = Params::SIGNET; /// The regtest parameters. pub static REGTEST: Params = Params::REGTEST; -#[allow(deprecated)] // For `pow_limit`. +#[allow(deprecated)] // For `pow_limit`. impl Params { /// The mainnet parameters (alias for `Params::MAINNET`). pub const BITCOIN: Params = Params::MAINNET; diff --git a/bitcoin/src/dogecoin/mod.rs b/bitcoin/src/dogecoin/mod.rs index 3c97ce288..e26b72068 100644 --- a/bitcoin/src/dogecoin/mod.rs +++ b/bitcoin/src/dogecoin/mod.rs @@ -3,7 +3,8 @@ //! This module provides support for de/serialization, parsing and execution on data structures and //! network messages related to Dogecoin. -use crate::block::{Header, TxMerkleNode}; +use crate::block::Header; +use crate::block::TxMerkleNode; use crate::consensus::{encode, Decodable, Encodable}; use crate::internal_macros::impl_consensus_encoding; use crate::io::{Read, Write}; @@ -13,7 +14,9 @@ use crate::{io, BlockHash, Transaction}; /// AuxPow version bit, see pub const VERSION_AUXPOW: i32 = 1 << 8; -fn is_auxpow(header: Header) -> bool { (header.version.to_consensus() & VERSION_AUXPOW) != 0 } +fn is_auxpow(header: Header) -> bool { + (header.version.to_consensus() & VERSION_AUXPOW) != 0 +} /// Data for merge-mining AuxPoW. /// @@ -78,7 +81,9 @@ pub struct Block { impl Block { /// Returns the block hash computed as SHA256d(header). - pub fn block_hash(&self) -> BlockHash { self.header.block_hash() } + pub fn block_hash(&self) -> BlockHash { + self.header.block_hash() + } } impl Decodable for Block { diff --git a/bitcoin/src/network.rs b/bitcoin/src/network.rs index 9bea93b9d..8df48b0be 100644 --- a/bitcoin/src/network.rs +++ b/bitcoin/src/network.rs @@ -333,14 +333,26 @@ mod tests { #[test] fn serialize_test() { assert_eq!(serialize(&Network::Bitcoin.magic()), &[0xf9, 0xbe, 0xb4, 0xd9]); - assert_eq!(serialize(&Network::Testnet.magic()), &[0x0b, 0x11, 0x09, 0x07]); - assert_eq!(serialize(&Network::Testnet4.magic()), &[0x1c, 0x16, 0x3f, 0x28]); + assert_eq!( + serialize(&Network::Testnet.magic()), + &[0x0b, 0x11, 0x09, 0x07] + ); + assert_eq!( + serialize(&Network::Testnet4.magic()), + &[0x1c, 0x16, 0x3f, 0x28] + ); assert_eq!(serialize(&Network::Signet.magic()), &[0x0a, 0x03, 0xcf, 0x40]); assert_eq!(serialize(&Network::Regtest.magic()), &[0xfa, 0xbf, 0xb5, 0xda]); assert_eq!(deserialize(&[0xf9, 0xbe, 0xb4, 0xd9]).ok(), Some(Network::Bitcoin.magic())); - assert_eq!(deserialize(&[0x0b, 0x11, 0x09, 0x07]).ok(), Some(Network::Testnet.magic())); - assert_eq!(deserialize(&[0x1c, 0x16, 0x3f, 0x28]).ok(), Some(Network::Testnet4.magic())); + assert_eq!( + deserialize(&[0x0b, 0x11, 0x09, 0x07]).ok(), + Some(Network::Testnet.magic()) + ); + assert_eq!( + deserialize(&[0x1c, 0x16, 0x3f, 0x28]).ok(), + Some(Network::Testnet4.magic()) + ); assert_eq!(deserialize(&[0x0a, 0x03, 0xcf, 0x40]).ok(), Some(Network::Signet.magic())); assert_eq!(deserialize(&[0xfa, 0xbf, 0xb5, 0xda]).ok(), Some(Network::Regtest.magic())); } diff --git a/bitcoin/src/p2p/message.rs b/bitcoin/src/p2p/message.rs index cf1d52f90..11e10cafd 100644 --- a/bitcoin/src/p2p/message.rs +++ b/bitcoin/src/p2p/message.rs @@ -312,7 +312,9 @@ impl RawNetworkMessage { } /// Consumes the [RawNetworkMessage] instance and returns the inner payload. - pub fn into_payload(self) -> NetworkMessage { self.payload } + pub fn into_payload(self) -> NetworkMessage { + self.payload + } /// The actual message data pub fn payload(&self) -> &NetworkMessage { &self.payload } diff --git a/bitcoin/src/p2p/mod.rs b/bitcoin/src/p2p/mod.rs index 372bf1188..988dc0d05 100644 --- a/bitcoin/src/p2p/mod.rs +++ b/bitcoin/src/p2p/mod.rs @@ -29,8 +29,8 @@ use io::{Read, Write}; use crate::consensus::encode::{self, Decodable, Encodable}; use crate::consensus::Params; -use crate::network::Network; use crate::prelude::*; +use crate::network::Network; #[rustfmt::skip] #[doc(inline)] @@ -234,7 +234,9 @@ impl Magic { pub fn to_bytes(self) -> [u8; 4] { self.0 } /// Returns the magic bytes for the network defined by `params`. - pub fn from_params(params: impl AsRef) -> Self { params.as_ref().network.into() } + pub fn from_params(params: impl AsRef) -> Self { + params.as_ref().network.into() + } } impl FromStr for Magic { diff --git a/bitcoin/src/pow.rs b/bitcoin/src/pow.rs index 0cc589611..d4cb696fc 100644 --- a/bitcoin/src/pow.rs +++ b/bitcoin/src/pow.rs @@ -19,9 +19,7 @@ use crate::block::Header; use crate::blockdata::block::BlockHash; use crate::consensus::encode::{self, Decodable, Encodable}; use crate::consensus::Params; -use crate::error::{ - ContainsPrefixError, MissingPrefixError, ParseIntError, PrefixedHexError, UnprefixedHexError, -}; +use crate::error::{ContainsPrefixError, MissingPrefixError, ParseIntError, PrefixedHexError, UnprefixedHexError}; /// Implement traits and methods shared by `Target` and `Work`. macro_rules! do_impl { @@ -1800,11 +1798,8 @@ mod tests { #[test] fn compact_target_from_upwards_difficulty_adjustment_using_headers() { + use crate::{block::Version, constants::genesis_block, TxMerkleNode}; use hashes::Hash; - - use crate::block::Version; - use crate::constants::genesis_block; - use crate::TxMerkleNode; let params = Params::new(crate::Network::Signet); let epoch_start = genesis_block(¶ms).header; // Block 2015, the only information used are `bits` and `time` @@ -1814,30 +1809,27 @@ mod tests { merkle_root: TxMerkleNode::all_zeros(), time: 1599332177, bits: epoch_start.bits, - nonce: epoch_start.nonce, + nonce: epoch_start.nonce }; - let adjustment = - CompactTarget::from_header_difficulty_adjustment(epoch_start, current, params); + let adjustment = CompactTarget::from_header_difficulty_adjustment(epoch_start, current, params); let adjustment_bits = CompactTarget::from_consensus(503394215); // Block 2016 compact target assert_eq!(adjustment, adjustment_bits); } #[test] fn compact_target_from_downwards_difficulty_adjustment_using_headers() { + use crate::{block::Version, TxMerkleNode}; use hashes::Hash; - - use crate::block::Version; - use crate::TxMerkleNode; let params = Params::new(crate::Network::Signet); let starting_bits = CompactTarget::from_consensus(503394215); // Block 2016 compact target - // Block 2016, the only information used is `time` + // Block 2016, the only information used is `time` let epoch_start = Header { version: Version::ONE, prev_blockhash: BlockHash::all_zeros(), merkle_root: TxMerkleNode::all_zeros(), time: 1599332844, bits: starting_bits, - nonce: 0, + nonce: 0 }; // Block 4031, the only information used are `bits` and `time` let current = Header { @@ -1846,10 +1838,9 @@ mod tests { merkle_root: TxMerkleNode::all_zeros(), time: 1600591200, bits: starting_bits, - nonce: 0, + nonce: 0 }; - let adjustment = - CompactTarget::from_header_difficulty_adjustment(epoch_start, current, params); + let adjustment = CompactTarget::from_header_difficulty_adjustment(epoch_start, current, params); let adjustment_bits = CompactTarget::from_consensus(503397348); // Block 4032 compact target assert_eq!(adjustment, adjustment_bits); } @@ -1860,8 +1851,9 @@ mod tests { let starting_bits = CompactTarget::from_consensus(503403001); let timespan = (0.2 * params.pow_target_timespan as f64) as u64; let got = CompactTarget::from_next_work_required(starting_bits, timespan, params); - let want = - Target::from_compact(starting_bits).min_transition_threshold().to_compact_lossy(); + let want = Target::from_compact(starting_bits) + .min_transition_threshold() + .to_compact_lossy(); assert_eq!(got, want); } @@ -1869,10 +1861,11 @@ mod tests { fn compact_target_from_minimum_downward_difficulty_adjustment() { let params = Params::new(crate::Network::Signet); let starting_bits = CompactTarget::from_consensus(403403001); // High difficulty for Signet - let timespan = 5 * params.pow_target_timespan; // Really slow. + let timespan = 5 * params.pow_target_timespan; // Really slow. let got = CompactTarget::from_next_work_required(starting_bits, timespan, ¶ms); - let want = - Target::from_compact(starting_bits).max_transition_threshold(params).to_compact_lossy(); + let want = Target::from_compact(starting_bits) + .max_transition_threshold(params) + .to_compact_lossy(); assert_eq!(got, want); } @@ -1880,7 +1873,7 @@ mod tests { fn compact_target_from_adjustment_is_max_target() { let params = Params::new(crate::Network::Signet); let starting_bits = CompactTarget::from_consensus(503543726); // Genesis compact target on Signet - let timespan = 5 * params.pow_target_timespan; // Really slow. + let timespan = 5 * params.pow_target_timespan; // Really slow. let got = CompactTarget::from_next_work_required(starting_bits, timespan, ¶ms); let want = params.max_attainable_target.to_compact_lossy(); assert_eq!(got, want); diff --git a/bitcoin/tests/psbt-sign-taproot.rs b/bitcoin/tests/psbt-sign-taproot.rs index 78fbcdd60..4cf7616b7 100644 --- a/bitcoin/tests/psbt-sign-taproot.rs +++ b/bitcoin/tests/psbt-sign-taproot.rs @@ -31,12 +31,13 @@ fn psbt_sign_taproot() { _secp: &Secp256k1, ) -> Result, Self::Error> { match key_request { - KeyRequest::Bip32((mfp, _)) => + KeyRequest::Bip32((mfp, _)) => { if mfp == self.mfp { Ok(Some(self.sk)) } else { Err(SignError::KeyNotFound) - }, + } + } _ => Err(SignError::KeyNotFound), } } diff --git a/io/src/lib.rs b/io/src/lib.rs index e7878625e..82d51acc7 100644 --- a/io/src/lib.rs +++ b/io/src/lib.rs @@ -211,7 +211,8 @@ impl> Read for Cursor { let start_pos = self.pos.try_into().unwrap_or(inner.len()); let read = core::cmp::min(inner.len().saturating_sub(start_pos), buf.len()); buf[..read].copy_from_slice(&inner[start_pos..start_pos + read]); - self.pos = self.pos.saturating_add(read.try_into().unwrap_or(u64::MAX /* unreachable */)); + self.pos = + self.pos.saturating_add(read.try_into().unwrap_or(u64::MAX /* unreachable */)); Ok(read) } } diff --git a/units/src/lib.rs b/units/src/lib.rs index 393b5ced1..b2c278795 100644 --- a/units/src/lib.rs +++ b/units/src/lib.rs @@ -45,14 +45,17 @@ pub mod parse; #[cfg(feature = "alloc")] pub mod weight; -pub use self::amount::ParseAmountError; #[doc(inline)] pub use self::amount::{Amount, SignedAmount}; +pub use self::amount::ParseAmountError; #[cfg(feature = "alloc")] pub use self::parse::ParseIntError; #[cfg(feature = "alloc")] #[doc(inline)] -pub use self::{fee_rate::FeeRate, weight::Weight}; +pub use self::{ + fee_rate::FeeRate, + weight::Weight, +}; #[rustfmt::skip] #[allow(unused_imports)] From 081989f258208e4ae809138581adc10bcd23c579 Mon Sep 17 00:00:00 2001 From: Mathieu Ducroux Date: Mon, 26 May 2025 11:12:17 +0200 Subject: [PATCH 31/53] update workflows to include doge-master branch --- .github/workflows/release.yml | 1 + .github/workflows/rust.yml | 29 +++++++++++++++-------------- fuzz/generate-files.sh | 1 + 3 files changed, 17 insertions(+), 14 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index cd2a87255..7e74972ac 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -2,6 +2,7 @@ on: push: branches: - master + - doge-master - 0.28.x - 0.29.x - 'test-ci/**' diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 5907f8491..bbb2a6f00 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -1,8 +1,9 @@ --- # rust-bitcoin CI: If you edit this file please update README.md -on: # yamllint disable-line rule:truthy +on: # yamllint disable-line rule:truthy push: branches: - master + - doge-master - 'test-ci/**' pull_request: @@ -20,13 +21,13 @@ jobs: id: read_toolchain run: echo "nightly_version=$(cat nightly-version)" >> $GITHUB_OUTPUT - Stable: # 2 jobs, one per manifest. + Stable: # 2 jobs, one per manifest. name: Test - stable toolchain runs-on: ubuntu-latest strategy: fail-fast: false matrix: - dep: [minimal, recent] + dep: [ minimal, recent ] steps: - name: "Checkout repo" uses: actions/checkout@v4 @@ -43,14 +44,14 @@ jobs: - name: "Run test script" run: ./maintainer-tools/ci/run_task.sh stable - Nightly: # 2 jobs, one per manifest. + Nightly: # 2 jobs, one per manifest. name: Test - nightly toolchain needs: Prepare runs-on: ubuntu-latest strategy: fail-fast: false matrix: - dep: [minimal, recent] + dep: [ minimal, recent ] steps: - name: "Checkout repo" uses: actions/checkout@v4 @@ -69,13 +70,13 @@ jobs: - name: "Run test script" run: ./maintainer-tools/ci/run_task.sh nightly - MSRV: # 2 jobs, one per manifest. + MSRV: # 2 jobs, one per manifest. name: Test - MSRV toolchain runs-on: ubuntu-latest strategy: fail-fast: false matrix: - dep: [minimal, recent] + dep: [ minimal, recent ] steps: - name: "Checkout repo" uses: actions/checkout@v4 @@ -101,7 +102,7 @@ jobs: strategy: fail-fast: false matrix: - dep: [recent] + dep: [ recent ] steps: - name: "Checkout repo" uses: actions/checkout@v4 @@ -128,7 +129,7 @@ jobs: strategy: fail-fast: false matrix: - dep: [recent] + dep: [ recent ] steps: - name: "Checkout repo" uses: actions/checkout@v4 @@ -152,7 +153,7 @@ jobs: strategy: fail-fast: false matrix: - dep: [recent] + dep: [ recent ] steps: - name: "Checkout repo" uses: actions/checkout@v4 @@ -178,7 +179,7 @@ jobs: strategy: fail-fast: false matrix: - dep: [recent] + dep: [ recent ] steps: - name: "Checkout repo" uses: actions/checkout@v4 @@ -256,14 +257,14 @@ jobs: - name: "Run hashes/embedded with alloc" run: cd hashes/embedded && cargo run --target thumbv7m-none-eabi --features=alloc - ASAN: # hashes crate only. + ASAN: # hashes crate only. name: ASAN - nightly toolchain needs: Prepare runs-on: ubuntu-latest strategy: fail-fast: false matrix: - dep: [recent] + dep: [ recent ] steps: - name: "Checkout repo" uses: actions/checkout@v4 @@ -278,7 +279,7 @@ jobs: - name: "Run sanitizer script" run: cd ./hashes && ./contrib/sanitizer.sh - WASM: # hashes crate only. + WASM: # hashes crate only. name: WASM - stable toolchain runs-on: ubuntu-latest strategy: diff --git a/fuzz/generate-files.sh b/fuzz/generate-files.sh index bbb36ea46..337d321f5 100755 --- a/fuzz/generate-files.sh +++ b/fuzz/generate-files.sh @@ -48,6 +48,7 @@ on: push: branches: - master + - doge-master - 'test-ci/**' pull_request: From a6163fb9d57cdc7516fe52d55a360e1a58c65184 Mon Sep 17 00:00:00 2001 From: Mathieu Ducroux Date: Mon, 26 May 2025 11:45:12 +0200 Subject: [PATCH 32/53] update labeler configuration to include base-branch for doge-master --- .github/labeler.yml | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/.github/labeler.yml b/.github/labeler.yml index 884b5961a..2046e47e4 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -1,15 +1,18 @@ ci: - changed-files: - - any-glob-to-any-file: .github/** + - any-glob-to-any-file: .github/** + - base-branch: [ 'doge-master' ] test: - changed-files: - - any-glob-to-any-file: fuzz/** - - any-glob-to-any-file: '*/tests/**' - - any-glob-to-any-file: 'dep_test' - - any-glob-to-any-file: 'contrib/run_task.sh' - - any-glob-to-any-file: 'contrib/test_vars.sh' - - any-glob-to-any-file: '*/contrib/extra_tests.sh' - - any-glob-to-any-file: '*/contrib/test_vars.sh' + - any-glob-to-any-file: fuzz/** + - any-glob-to-any-file: '*/tests/**' + - any-glob-to-any-file: 'dep_test' + - any-glob-to-any-file: 'contrib/run_task.sh' + - any-glob-to-any-file: 'contrib/test_vars.sh' + - any-glob-to-any-file: '*/contrib/extra_tests.sh' + - any-glob-to-any-file: '*/contrib/test_vars.sh' + - base-branch: [ 'doge-master' ] doc: - changed-files: - - any-glob-to-any-file: '**/*.md' + - any-glob-to-any-file: '**/*.md' + - base-branch: [ 'doge-master' ] From 8b2e5924062c9be9981f6653c7aa55fe6c5cd63f Mon Sep 17 00:00:00 2001 From: Mathieu Ducroux Date: Mon, 26 May 2025 11:57:07 +0200 Subject: [PATCH 33/53] Revert "update labeler configuration to include base-branch for doge-master" This reverts commit aa11fe61d1bf9c76c1f4d16ceac9d06fc6abe4f5. --- .github/labeler.yml | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/.github/labeler.yml b/.github/labeler.yml index 2046e47e4..884b5961a 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -1,18 +1,15 @@ ci: - changed-files: - - any-glob-to-any-file: .github/** - - base-branch: [ 'doge-master' ] + - any-glob-to-any-file: .github/** test: - changed-files: - - any-glob-to-any-file: fuzz/** - - any-glob-to-any-file: '*/tests/**' - - any-glob-to-any-file: 'dep_test' - - any-glob-to-any-file: 'contrib/run_task.sh' - - any-glob-to-any-file: 'contrib/test_vars.sh' - - any-glob-to-any-file: '*/contrib/extra_tests.sh' - - any-glob-to-any-file: '*/contrib/test_vars.sh' - - base-branch: [ 'doge-master' ] + - any-glob-to-any-file: fuzz/** + - any-glob-to-any-file: '*/tests/**' + - any-glob-to-any-file: 'dep_test' + - any-glob-to-any-file: 'contrib/run_task.sh' + - any-glob-to-any-file: 'contrib/test_vars.sh' + - any-glob-to-any-file: '*/contrib/extra_tests.sh' + - any-glob-to-any-file: '*/contrib/test_vars.sh' doc: - changed-files: - - any-glob-to-any-file: '**/*.md' - - base-branch: [ 'doge-master' ] + - any-glob-to-any-file: '**/*.md' From 96dd33ae0c17f01fc28d505884355d6b199640d4 Mon Sep 17 00:00:00 2001 From: Mathieu Ducroux Date: Mon, 26 May 2025 12:57:43 +0200 Subject: [PATCH 34/53] Reapply "add write permission to labeller for issues" This reverts commit 7adbf2966df2ec463bc998e6bffc327cbd581925. --- .github/workflows/manage-pr.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/manage-pr.yml b/.github/workflows/manage-pr.yml index 3ba1dcc55..885382208 100644 --- a/.github/workflows/manage-pr.yml +++ b/.github/workflows/manage-pr.yml @@ -6,6 +6,7 @@ jobs: labeler: permissions: contents: read + issues: write pull-requests: write runs-on: ubuntu-latest steps: From 28571b3ea325a857a86ed54c02a1d1b76d876459 Mon Sep 17 00:00:00 2001 From: Mathieu Ducroux Date: Wed, 28 May 2025 10:25:42 +0200 Subject: [PATCH 35/53] feature(dogecoin): add scrypt PoW validation --- bitcoin/Cargo.toml | 1 + bitcoin/src/blockdata/block.rs | 32 ++++++++ bitcoin/src/dogecoin/mod.rs | 140 +++++++++++++++++++++++++++++++-- 3 files changed, 165 insertions(+), 8 deletions(-) diff --git a/bitcoin/Cargo.toml b/bitcoin/Cargo.toml index 8c9f6ae8a..5897ab9ef 100644 --- a/bitcoin/Cargo.toml +++ b/bitcoin/Cargo.toml @@ -34,6 +34,7 @@ hex_lit = "0.1.1" internals = { package = "bitcoin-internals", version = "0.3.0", features = ["alloc"] } io = { package = "bitcoin-io", version = "0.1.1", default-features = false, features = ["alloc"] } secp256k1 = { version = "0.29.0", default-features = false, features = ["hashes", "alloc"] } +scrypt = { version = "0.11.0", default-features = false } units = { package = "bitcoin-units", version = "0.1.0", default-features = false, features = ["alloc"] } base64 = { version = "0.21.3", optional = true } diff --git a/bitcoin/src/blockdata/block.rs b/bitcoin/src/blockdata/block.rs index ec24abb42..1bc234e50 100644 --- a/bitcoin/src/blockdata/block.rs +++ b/bitcoin/src/blockdata/block.rs @@ -12,6 +12,7 @@ use core::fmt; use hashes::{sha256d, Hash, HashEngine}; use io::{Read, Write}; +use scrypt::{scrypt, Params as ScryptParams}; use super::Weight; use crate::blockdata::script; @@ -86,6 +87,20 @@ impl Header { BlockHash::from_engine(engine) } + /// Returns the block hash using the scrypt hash function. + pub fn block_hash_with_scrypt(&self) -> BlockHash { + let params = ScryptParams::new(10, 1, 1, 32).expect("invalid scrypt params"); + + let mut output = [0u8; 32]; + + let mut buf = Vec::new(); + self.consensus_encode(&mut buf).expect("write to vec failed"); + + scrypt(buf.as_slice(), buf.as_slice(), ¶ms, &mut output).unwrap(); + + BlockHash::from_slice(&output).unwrap() + } + /// Computes the target (range [0, T] inclusive) that a blockhash must land in to be valid. pub fn target(&self) -> Target { self.bits.into() } @@ -114,6 +129,23 @@ impl Header { } } + /// Checks that the proof-of-work using scrypt is valid, returning the scrypt block hash. + pub fn validate_pow_with_scrypt( + &self, + required_target: Target, + ) -> Result { + let target = self.target(); + if target != required_target { + return Err(ValidationError::BadTarget); + } + let block_hash = self.block_hash_with_scrypt(); + if target.is_met_by(block_hash) { + Ok(block_hash) + } else { + Err(ValidationError::BadProofOfWork) + } + } + /// Returns the total work of the block. pub fn work(&self) -> Work { self.target().to_work() } } diff --git a/bitcoin/src/dogecoin/mod.rs b/bitcoin/src/dogecoin/mod.rs index e26b72068..31033cca2 100644 --- a/bitcoin/src/dogecoin/mod.rs +++ b/bitcoin/src/dogecoin/mod.rs @@ -3,8 +3,7 @@ //! This module provides support for de/serialization, parsing and execution on data structures and //! network messages related to Dogecoin. -use crate::block::Header; -use crate::block::TxMerkleNode; +use crate::block::{Header, TxMerkleNode}; use crate::consensus::{encode, Decodable, Encodable}; use crate::internal_macros::impl_consensus_encoding; use crate::io::{Read, Write}; @@ -14,9 +13,7 @@ use crate::{io, BlockHash, Transaction}; /// AuxPow version bit, see pub const VERSION_AUXPOW: i32 = 1 << 8; -fn is_auxpow(header: Header) -> bool { - (header.version.to_consensus() & VERSION_AUXPOW) != 0 -} +fn is_auxpow(header: Header) -> bool { (header.version.to_consensus() & VERSION_AUXPOW) != 0 } /// Data for merge-mining AuxPoW. /// @@ -81,9 +78,10 @@ pub struct Block { impl Block { /// Returns the block hash computed as SHA256d(header). - pub fn block_hash(&self) -> BlockHash { - self.header.block_hash() - } + pub fn block_hash(&self) -> BlockHash { self.header.block_hash() } + + /// Returns the block hash using the scrypt hash function. + pub fn block_hash_with_scrypt(&self) -> BlockHash { self.header.block_hash_with_scrypt() } } impl Decodable for Block { @@ -115,3 +113,129 @@ impl Encodable for Block { Ok(len) } } + +#[cfg(test)] +mod tests { + use hex::test_hex_unwrap as hex; + + use super::*; + use crate::block::{ValidationError, Version}; + use crate::consensus::encode::{deserialize, serialize}; + use crate::{CompactTarget, Network, Target, Work}; + + #[test] + fn dogecoin_block_test() { + let params = crate::consensus::Params::new(Network::Bitcoin); + // Mainnet Dogecoin block 5794c80b80d9c33e0737a5353cd52b1f097f61d8d2b9f471e1702345080e0002 + let some_block = hex!("01000000c76fe7f8ec09989d32b7907966fbd347134f80a7b71efce55fec502aa126ba3894b3065289ff8ba1ab4e8391771174d47cf2c974ebd24a1bdafd6c107d5a7a207d78bb52de8f001c00da8c3c0201000000010000000000000000000000000000000000000000000000000000000000000000ffffffff2602bc6d062f503253482f047178bb5208f8042975030000000d2f7374726174756d506f6f6c2f000000000100629b29c45500001976a91450e9fe87c705dcd4b7523b47e3314c2115f5d5df88ac0000000001000000015f48fabf4425324df2b5e58f4e9c771297f76f5fa37db7556f6fc1d22742da1f010000006a473044022062d29d2d26f7d826e7b72257486e294d284832743c7803a2901eb07e326b25a002207efc391b0f4e724c9d518075c0e056cc425540f845b0fd419ba8a9d49d69288301210297a2568525760a98454d84f5e5adba9fd0a41726a6fb774ddc407279e41e2061ffffffff0240bab598200000001976a91401348a2b83aeb6b1ba2a174a1a40b7c75fbeb12088ac0040be40250000001976a914025407d928ef333979d064ae233353d80e29d58c88ac00000000"); + let cutoff_block = hex!("01000000c76fe7f8ec09989d32b7907966fbd347134f80a7b71efce55fec502aa126ba3894b3065289ff8ba1ab4e8391771174d47cf2c974ebd24a1bdafd6c107d5a7a207d78bb52de8f001c00da8c3c0201000000010000000000000000000000000000000000000000000000000000000000000000ffffffff2602bc6d062f503253482f047178bb5208f8042975030000000d2f7374726174756d506f6f6c2f000000000100629b29c45500001976a91450e9fe87c705dcd4b7523b47e3314c2115f5d5df88ac0000000001000000015f48fabf4425324df2b5e58f4e9c771297f76f5fa37db7556f6fc1d22742da1f010000006a473044022062d29d2d26f7d826e7b72257486e294d284832743c7803a2901eb07e326b25a002207efc391b0f4e724c9d518075c0e056cc425540f845b0fd419ba8a9d49d69288301210297a2568525760a98454d84f5e5adba9fd0a41726a6fb774ddc407279e41e2061ffffffff0240bab598200000001976a91401348a2b83aeb6b1ba2a174a1a40b7c75fbeb12088ac0040be40250000001976a914025407d928ef333979d064ae233353d80e29d58c88ac"); + + let currhash = hex!("02000e08452370e171f4b9d2d8617f091f2bd53c35a537073ec3d9800bc89457"); + let prevhash = hex!("c76fe7f8ec09989d32b7907966fbd347134f80a7b71efce55fec502aa126ba38"); + let merkle = hex!("94b3065289ff8ba1ab4e8391771174d47cf2c974ebd24a1bdafd6c107d5a7a20"); + let work = Work::from(0x1c788001c78_u128); + + let decode: Result = deserialize(&some_block); + let bad_decode: Result = deserialize(&cutoff_block); + + assert!(decode.is_ok()); + assert!(bad_decode.is_err()); + let real_decode = decode.unwrap(); + assert_eq!(serialize(&real_decode.header.block_hash()), currhash); + assert_eq!(real_decode.header.version, Version::ONE); + assert_eq!(serialize(&real_decode.header.prev_blockhash), prevhash); + // assert_eq!(real_decode.header.merkle_root, real_decode.compute_merkle_root().unwrap()); + assert_eq!(serialize(&real_decode.header.merkle_root), merkle); + assert_eq!(real_decode.header.time, 1388017789); + assert_eq!(real_decode.header.bits, CompactTarget::from_consensus(469798878)); + assert_eq!(real_decode.header.nonce, 1015863808); + assert_eq!(real_decode.header.work(), work); + assert_eq!( + real_decode.header.validate_pow_with_scrypt(real_decode.header.target()).unwrap(), + real_decode.block_hash_with_scrypt() + ); + assert_eq!(real_decode.header.difficulty(¶ms), 455); + assert_eq!(real_decode.header.difficulty_float(), 455.52430084170516); + + assert_eq!(serialize(&real_decode), some_block); + } + + #[test] + fn validate_pow_with_scrypt_test() { + let some_header = hex!("01000000c76fe7f8ec09989d32b7907966fbd347134f80a7b71efce55fec502aa126ba3894b3065289ff8ba1ab4e8391771174d47cf2c974ebd24a1bdafd6c107d5a7a207d78bb52de8f001c00da8c3c"); + let some_header: Header = + deserialize(&some_header).expect("Can't deserialize correct block header"); + assert_eq!( + some_header.validate_pow_with_scrypt(some_header.target()).unwrap(), + some_header.block_hash_with_scrypt() + ); + + // test with zero target + match some_header.validate_pow_with_scrypt(Target::ZERO) { + Err(ValidationError::BadTarget) => (), + _ => panic!("unexpected result from validate_pow_with_scrypt"), + } + + // test with modified header + let mut invalid_header: Header = some_header; + invalid_header.nonce += 1; + match invalid_header.validate_pow_with_scrypt(invalid_header.target()) { + Err(ValidationError::BadProofOfWork) => (), + _ => panic!("unexpected result from validate_pow_with_scrypt"), + } + } + + #[test] + fn block_hash_with_scrypt_test() { + struct Test { + input: Vec, + output: Vec, + output_str: &'static str, + } + + let tests = vec![ + // Example from + Test { + input: hex!("01000000f615f7ce3b4fc6b8f61e8f89aedb1d0852507650533a9e3b10b9bbcc30639f279fcaa86746e1ef52d3edb3c4ad8259920d509bd073605c9bf1d59983752a6b06b817bb4ea78e011d012d59d4"), + output: vec![217, 235, 134, 99, 255, 236, 36, 28, 47, 177, 24, 173, 183, 222, 151, 168, 44, 128, 59, 111, 244, 109, 87, 102, 121, 53, 200, 16, 1, 0, 0, 0], + output_str: "0000000110c8357966576df46f3b802ca897deb7ad18b12f1c24ecff6386ebd9" + }, + // Examples from + Test { + input: hex!("020000004c1271c211717198227392b029a64a7971931d351b387bb80db027f270411e398a07046f7d4a08dd815412a8712f874a7ebf0507e3878bd24e20a3b73fd750a667d2f451eac7471b00de6659"), + output: vec![6, 88, 152, 215, 171, 45, 170, 130, 53, 205, 218, 149, 17, 210, 72, 243, 1, 11, 94, 17, 246, 130, 248, 7, 65, 239, 43, 0, 0, 0, 0, 0], + output_str: "00000000002bef4107f882f6115e0b01f348d21195dacd3582aa2dabd7985806", + }, + Test { + input: hex!("0200000011503ee6a855e900c00cfdd98f5f55fffeaee9b6bf55bea9b852d9de2ce35828e204eef76acfd36949ae56d1fbe81c1ac9c0209e6331ad56414f9072506a77f8c6faf551eac7471b00389d01"), + output: vec![148, 252, 136, 28, 159, 241, 218, 80, 210, 53, 237, 40, 242, 187, 207, 221, 254, 183, 8, 78, 99, 235, 213, 189, 17, 13, 58, 0, 0, 0, 0, 0], + output_str: "00000000003a0d11bdd5eb634e08b7feddcfbbf228ed35d250daf19f1c88fc94", + }, + Test { + input: hex!("02000000a72c8a177f523946f42f22c3e86b8023221b4105e8007e59e81f6beb013e29aaf635295cb9ac966213fb56e046dc71df5b3f7f67ceaeab24038e743f883aff1aaafaf551eac7471b0166249b"), + output: vec![129, 202, 168, 20, 81, 221, 248, 101, 156, 242, 175, 216, 89, 157, 45, 108, 138, 114, 68, 50, 225, 136, 242, 149, 248, 64, 11, 0, 0, 0, 0, 0], + output_str: "00000000000b40f895f288e13244728a6c2d9d59d8aff29c65f8dd5114a8ca81", + }, + Test { + input: hex!("010000007824bc3a8a1b4628485eee3024abd8626721f7f870f8ad4d2f33a27155167f6a4009d1285049603888fe85a84b6c803a53305a8d497965a5e896e1a00568359589faf551eac7471b0065434e"), + output: vec![254, 5, 225, 151, 24, 24, 134, 106, 220, 126, 142, 110, 47, 215, 232, 216, 153, 30, 3, 35, 73, 205, 145, 88, 0, 7, 48, 0, 0, 0, 0, 0], + output_str: "00000000003007005891cd4923031e99d8e8d72f6e8e7edc6a86181897e105fe", + }, + Test { + input: hex!("0200000050bfd4e4a307a8cb6ef4aef69abc5c0f2d579648bd80d7733e1ccc3fbc90ed664a7f74006cb11bde87785f229ecd366c2d4e44432832580e0608c579e4cb76f383f7f551eac7471b00c36982"), + output: vec![140, 236, 0, 56, 77, 114, 199, 231, 79, 91, 52, 13, 115, 175, 2, 250, 71, 203, 12, 19, 199, 175, 164, 38, 180, 240, 24, 0, 0, 0, 0, 0], + output_str: "000000000018f0b426a4afc7130ccb47fa02af730d345b4fe7c7724d3800ec8c", + }, + ]; + + for test in tests { + let header: Header = + deserialize(&test.input).expect("Can't deserialize correct block header"); + assert_eq!(header.block_hash_with_scrypt().to_string(), test.output_str); + assert_eq!(serialize(&header.block_hash_with_scrypt()), test.output); + } + } + + #[test] + fn target_exceeds_max_allowed_test() {} +} From 8190978f8526c7ec4e3a44e7c78ff3f9a0745f8b Mon Sep 17 00:00:00 2001 From: Mathieu Ducroux Date: Wed, 28 May 2025 10:28:41 +0200 Subject: [PATCH 36/53] remove leftover test --- bitcoin/src/dogecoin/mod.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/bitcoin/src/dogecoin/mod.rs b/bitcoin/src/dogecoin/mod.rs index 31033cca2..55f682c9c 100644 --- a/bitcoin/src/dogecoin/mod.rs +++ b/bitcoin/src/dogecoin/mod.rs @@ -235,7 +235,4 @@ mod tests { assert_eq!(serialize(&header.block_hash_with_scrypt()), test.output); } } - - #[test] - fn target_exceeds_max_allowed_test() {} } From 33cf462550771643162e9aade4f49cdcbdb838dc Mon Sep 17 00:00:00 2001 From: Mathieu Ducroux Date: Wed, 28 May 2025 10:32:21 +0200 Subject: [PATCH 37/53] update lock files --- Cargo-minimal.lock | 149 ++++++++++++++++++++++++++++++++++++++++++++- Cargo-recent.lock | 141 +++++++++++++++++++++++++++++++++++++++++- 2 files changed, 285 insertions(+), 5 deletions(-) diff --git a/Cargo-minimal.lock b/Cargo-minimal.lock index 8b47f1e59..ae1a28845 100644 --- a/Cargo-minimal.lock +++ b/Cargo-minimal.lock @@ -62,6 +62,7 @@ dependencies = [ "hex_lit", "mutagen", "ordered", + "scrypt", "secp256k1", "serde", "serde_json", @@ -121,6 +122,15 @@ dependencies = [ "cc", ] +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + [[package]] name = "byteorder" version = "1.3.0" @@ -139,19 +149,75 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4c819a1287eb618df47cc647173c5c4c66ba19d888a6e50d605672aed3140de" +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + [[package]] name = "dyn-clone" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0da518043f6481364cd454be81dfe096cfd3f82daa1466f4946d24ea325b0941" +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "getrandom" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee8025cf36f917e6a52cce185b7c7177689b838b7ec138364e50cc2277a56cf4" dependencies = [ - "cfg-if", + "cfg-if 0.1.2", "libc", "wasi", ] @@ -177,6 +243,15 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3011d1213f159867b13cfd6ac92d2cd5f1345762c63be3554e84092d85a50bbd" +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + [[package]] name = "honggfuzz" version = "0.5.55" @@ -188,6 +263,15 @@ dependencies = [ "rustc_version", ] +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + [[package]] name = "itoa" version = "0.4.3" @@ -208,9 +292,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.64" +version = "0.2.172" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74dfca3d9957906e8d1e6a0b641dc9a59848e793f1da2165889fd4f62d10d79c" +checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" [[package]] name = "memmap2" @@ -260,6 +344,16 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f0642533dea0bb58bd5cae31bafc1872429f0f12ac8c61fe2b4ba44f80b959b" +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest", + "hmac", +] + [[package]] name = "ppv-lite86" version = "0.2.8" @@ -339,6 +433,15 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c92464b447c0ee8c4fb3824ecc8383b81717b9f1e74ba2e72540aef7b9f82997" +[[package]] +name = "salsa20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213" +dependencies = [ + "cipher", +] + [[package]] name = "schemars" version = "0.8.3" @@ -350,6 +453,17 @@ dependencies = [ "serde_json", ] +[[package]] +name = "scrypt" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0516a385866c09368f0b5bcd1caff3366aace790fcd46e2bb032697bb172fd1f" +dependencies = [ + "pbkdf2", + "salsa20", + "sha2", +] + [[package]] name = "secp256k1" version = "0.29.0" @@ -428,6 +542,23 @@ dependencies = [ "serde", ] +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if 1.0.0", + "cpufeatures", + "digest", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "syn" version = "1.0.109" @@ -439,12 +570,24 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "typenum" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" + [[package]] name = "unicode-ident" version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + [[package]] name = "wasi" version = "0.9.0+wasi-snapshot-preview1" diff --git a/Cargo-recent.lock b/Cargo-recent.lock index 9f49d4c0c..a9b178ed5 100644 --- a/Cargo-recent.lock +++ b/Cargo-recent.lock @@ -61,6 +61,7 @@ dependencies = [ "hex_lit", "mutagen", "ordered", + "scrypt", "secp256k1", "serde", "serde_json", @@ -120,6 +121,15 @@ dependencies = [ "cc", ] +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + [[package]] name = "byteorder" version = "1.4.3" @@ -138,12 +148,62 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + [[package]] name = "dyn-clone" version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68b0cf012f1230e43cd00ebb729c6bb58707ecfa8ad08b52ef3a4ccd2697fc30" +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "getrandom" version = "0.2.9" @@ -176,6 +236,15 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3011d1213f159867b13cfd6ac92d2cd5f1345762c63be3554e84092d85a50bbd" +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + [[package]] name = "honggfuzz" version = "0.5.55" @@ -187,6 +256,15 @@ dependencies = [ "rustc_version", ] +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + [[package]] name = "itoa" version = "1.0.6" @@ -207,9 +285,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.142" +version = "0.2.172" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a987beff54b60ffa6d51982e1aa1146bc42f19bd26be28b0586f252fccf5317" +checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" [[package]] name = "memmap2" @@ -259,6 +337,16 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f0642533dea0bb58bd5cae31bafc1872429f0f12ac8c61fe2b4ba44f80b959b" +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest", + "hmac", +] + [[package]] name = "ppv-lite86" version = "0.2.17" @@ -328,6 +416,15 @@ version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" +[[package]] +name = "salsa20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213" +dependencies = [ + "cipher", +] + [[package]] name = "schemars" version = "0.8.12" @@ -339,6 +436,17 @@ dependencies = [ "serde_json", ] +[[package]] +name = "scrypt" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0516a385866c09368f0b5bcd1caff3366aace790fcd46e2bb032697bb172fd1f" +dependencies = [ + "pbkdf2", + "salsa20", + "sha2", +] + [[package]] name = "secp256k1" version = "0.29.0" @@ -417,6 +525,23 @@ dependencies = [ "serde", ] +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "syn" version = "1.0.109" @@ -428,12 +553,24 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "typenum" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" + [[package]] name = "unicode-ident" version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" From 9cf9d935d4b34494ba0b2f4b71de9cc3dfb3be96 Mon Sep 17 00:00:00 2001 From: mducroux Date: Tue, 10 Jun 2025 08:21:15 +0200 Subject: [PATCH 38/53] chore(license): add Apache-2.0 license (#4) [XC-374](https://dfinity.atlassian.net/browse/XC-374?atlOrigin=eyJpIjoiM2VjNTk4OWNmNzY1NDUzZmEwODE1ODI0NWVkMTJkMmMiLCJwIjoiaiJ9): this PR adds the Apache-2.0 license to the dogecoin module while preserving the CC0-1.0 license for code inherited from upstream rust-bitcoin. [XC-374]: https://dfinity.atlassian.net/browse/XC-374?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ --- LICENSE | 311 ++++++++++++++++++++------------- README.md | 5 +- bitcoin/src/blockdata/block.rs | 2 +- bitcoin/src/dogecoin/mod.rs | 2 + 4 files changed, 197 insertions(+), 123 deletions(-) diff --git a/LICENSE b/LICENSE index 6ca207ef0..71b381e56 100644 --- a/LICENSE +++ b/LICENSE @@ -1,122 +1,191 @@ -Creative Commons Legal Code - -CC0 1.0 Universal - - CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE - LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN - ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS - INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES - REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS - PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM - THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED - HEREUNDER. - -Statement of Purpose - -The laws of most jurisdictions throughout the world automatically confer -exclusive Copyright and Related Rights (defined below) upon the creator -and subsequent owner(s) (each and all, an "owner") of an original work of -authorship and/or a database (each, a "Work"). - -Certain owners wish to permanently relinquish those rights to a Work for -the purpose of contributing to a commons of creative, cultural and -scientific works ("Commons") that the public can reliably and without fear -of later claims of infringement build upon, modify, incorporate in other -works, reuse and redistribute as freely as possible in any form whatsoever -and for any purposes, including without limitation commercial purposes. -These owners may contribute to the Commons to promote the ideal of a free -culture and the further production of creative, cultural and scientific -works, or to gain reputation or greater distribution for their Work in -part through the use and efforts of others. - -For these and/or other purposes and motivations, and without any -expectation of additional consideration or compensation, the person -associating CC0 with a Work (the "Affirmer"), to the extent that he or she -is an owner of Copyright and Related Rights in the Work, voluntarily -elects to apply CC0 to the Work and publicly distribute the Work under its -terms, with knowledge of his or her Copyright and Related Rights in the -Work and the meaning and intended legal effect of CC0 on those rights. - -1. Copyright and Related Rights. A Work made available under CC0 may be -protected by copyright and related or neighboring rights ("Copyright and -Related Rights"). Copyright and Related Rights include, but are not -limited to, the following: - - i. the right to reproduce, adapt, distribute, perform, display, - communicate, and translate a Work; - ii. moral rights retained by the original author(s) and/or performer(s); -iii. publicity and privacy rights pertaining to a person's image or - likeness depicted in a Work; - iv. rights protecting against unfair competition in regards to a Work, - subject to the limitations in paragraph 4(a), below; - v. rights protecting the extraction, dissemination, use and reuse of data - in a Work; - vi. database rights (such as those arising under Directive 96/9/EC of the - European Parliament and of the Council of 11 March 1996 on the legal - protection of databases, and under any national implementation - thereof, including any amended or successor version of such - directive); and -vii. other similar, equivalent or corresponding rights throughout the - world based on applicable law or treaty, and any national - implementations thereof. - -2. Waiver. To the greatest extent permitted by, but not in contravention -of, applicable law, Affirmer hereby overtly, fully, permanently, -irrevocably and unconditionally waives, abandons, and surrenders all of -Affirmer's Copyright and Related Rights and associated claims and causes -of action, whether now known or unknown (including existing as well as -future claims and causes of action), in the Work (i) in all territories -worldwide, (ii) for the maximum duration provided by applicable law or -treaty (including future time extensions), (iii) in any current or future -medium and for any number of copies, and (iv) for any purpose whatsoever, -including without limitation commercial, advertising or promotional -purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each -member of the public at large and to the detriment of Affirmer's heirs and -successors, fully intending that such Waiver shall not be subject to -revocation, rescission, cancellation, termination, or any other legal or -equitable action to disrupt the quiet enjoyment of the Work by the public -as contemplated by Affirmer's express Statement of Purpose. - -3. Public License Fallback. Should any part of the Waiver for any reason -be judged legally invalid or ineffective under applicable law, then the -Waiver shall be preserved to the maximum extent permitted taking into -account Affirmer's express Statement of Purpose. In addition, to the -extent the Waiver is so judged Affirmer hereby grants to each affected -person a royalty-free, non transferable, non sublicensable, non exclusive, -irrevocable and unconditional license to exercise Affirmer's Copyright and -Related Rights in the Work (i) in all territories worldwide, (ii) for the -maximum duration provided by applicable law or treaty (including future -time extensions), (iii) in any current or future medium and for any number -of copies, and (iv) for any purpose whatsoever, including without -limitation commercial, advertising or promotional purposes (the -"License"). The License shall be deemed effective as of the date CC0 was -applied by Affirmer to the Work. Should any part of the License for any -reason be judged legally invalid or ineffective under applicable law, such -partial invalidity or ineffectiveness shall not invalidate the remainder -of the License, and in such case Affirmer hereby affirms that he or she -will not (i) exercise any of his or her remaining Copyright and Related -Rights in the Work or (ii) assert any associated claims and causes of -action with respect to the Work, in either case contrary to Affirmer's -express Statement of Purpose. - -4. Limitations and Disclaimers. - - a. No trademark or patent rights held by Affirmer are waived, abandoned, - surrendered, licensed or otherwise affected by this document. - b. Affirmer offers the Work as-is and makes no representations or - warranties of any kind concerning the Work, express, implied, - statutory or otherwise, including without limitation warranties of - title, merchantability, fitness for a particular purpose, non - infringement, or the absence of latent or other defects, accuracy, or - the present or absence of errors, whether or not discoverable, all to - the greatest extent permissible under applicable law. - c. Affirmer disclaims responsibility for clearing rights of other persons - that may apply to the Work or any use thereof, including without - limitation any person's Copyright and Related Rights in the Work. - Further, Affirmer disclaims responsibility for obtaining any necessary - consents, permissions or other rights required for any use of the - Work. - d. Affirmer understands and acknowledges that Creative Commons is not a - party to this document and has no duty or obligation with respect to - this CC0 or use of the Work. +Copyright © 2025 DFINITY Foundation +This project includes derivative work from the upstream rust-bitcoin +project, originally licensed under Creative Commons Zero v1.0 Universal +(SPDX: CC0-1.0). + +All original code introduced in this repository is licensed under Apache +License, Version 2.0 (SPDX: Apache-2.0) (the "License"); you may not use +these files except in compliance with the License. You may obtain a copy +of the License at + + http://www.apache.org/licenses/LICENSE-2.0. + +The Apache-2.0 license is also copied below: + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS \ No newline at end of file diff --git a/README.md b/README.md index e30924cb8..664950da2 100644 --- a/README.md +++ b/README.md @@ -216,5 +216,8 @@ Release notes are done per crate, see: ## Licensing -The code in this project is licensed under the [Creative Commons CC0 1.0 Universal license](LICENSE). +This project is a fork of [rust-bitcoin](https://github.com/rust-bitcoin/rust-bitcoin/tree/master), originally licensed under CC0 v1.0 Universal. + +This fork is licensed under the Apache License, Version 2.0, except where otherwise noted. + We use the [SPDX license list](https://spdx.org/licenses/) and [SPDX IDs](https://spdx.dev/ids/). diff --git a/bitcoin/src/blockdata/block.rs b/bitcoin/src/blockdata/block.rs index 1bc234e50..e90401700 100644 --- a/bitcoin/src/blockdata/block.rs +++ b/bitcoin/src/blockdata/block.rs @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: CC0-1.0 +// SPDX-License-Identifier: CC0-1.0 OR Apache-2.0 //! Bitcoin blocks. //! diff --git a/bitcoin/src/dogecoin/mod.rs b/bitcoin/src/dogecoin/mod.rs index 55f682c9c..b1aa18e2e 100644 --- a/bitcoin/src/dogecoin/mod.rs +++ b/bitcoin/src/dogecoin/mod.rs @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + //! Dogecoin module. //! //! This module provides support for de/serialization, parsing and execution on data structures and From afd98dca4378dda086108be1243800ef76c080bc Mon Sep 17 00:00:00 2001 From: mducroux Date: Wed, 11 Jun 2025 09:27:58 +0200 Subject: [PATCH 39/53] feat(dogecoin): add dogecoin consensus and genesis parameters (#3) [XC-383](https://dfinity.atlassian.net/browse/XC-383?atlOrigin=eyJpIjoiZGIzYzllMDQ4M2U0NDc2MjllMWJmYzFmOTYwYmYwZWMiLCJwIjoiaiJ9): This PR adds 1) Dogecoin consensus parameters used in block validation and 2) genesis transaction/block parameters which will be used for testing purposes. [XC-383]: https://dfinity.atlassian.net/browse/XC-383?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ --- bitcoin/src/blockdata/script/builder.rs | 2 +- bitcoin/src/dogecoin/constants.rs | 204 ++++++++++++++++++++++++ bitcoin/src/dogecoin/mod.rs | 46 ++++++ bitcoin/src/dogecoin/params.rs | 124 ++++++++++++++ bitcoin/src/pow.rs | 18 ++- 5 files changed, 392 insertions(+), 2 deletions(-) create mode 100644 bitcoin/src/dogecoin/constants.rs create mode 100644 bitcoin/src/dogecoin/params.rs diff --git a/bitcoin/src/blockdata/script/builder.rs b/bitcoin/src/blockdata/script/builder.rs index 8d0381529..bc166a3a8 100644 --- a/bitcoin/src/blockdata/script/builder.rs +++ b/bitcoin/src/blockdata/script/builder.rs @@ -50,7 +50,7 @@ impl Builder { /// Adds instructions to push an integer onto the stack without optimization. /// /// This uses the explicit encoding regardless of the availability of dedicated opcodes. - pub(in crate::blockdata) fn push_int_non_minimal(self, data: i64) -> Builder { + pub fn push_int_non_minimal(self, data: i64) -> Builder { let mut buf = [0u8; 8]; let len = write_scriptint(&mut buf, data); self.push_slice(&<&PushBytes>::from(&buf)[..len]) diff --git a/bitcoin/src/dogecoin/constants.rs b/bitcoin/src/dogecoin/constants.rs new file mode 100644 index 000000000..a9a1e8c1a --- /dev/null +++ b/bitcoin/src/dogecoin/constants.rs @@ -0,0 +1,204 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Blockdata constants. +//! +//! This module provides various constants relating to the blockchain and +//! consensus code. In particular, it defines the genesis block and its +//! single transaction. +//! + +use hashes::{sha256d, Hash}; +use units::Amount; + +use crate::dogecoin::params::Params; +use crate::dogecoin::{Block, Network}; +use crate::opcodes::all::OP_CHECKSIG; +use crate::{ + absolute, block, script, transaction, CompactTarget, OutPoint, Sequence, Transaction, TxIn, + TxMerkleNode, TxOut, Witness, +}; + +// This is the 65 byte (uncompressed) pubkey used as the one-and-only output of the genesis transaction. +// +// ref: https://github.com/dogecoin/dogecoin/blob/7237da74b8c356568644cbe4fba19d994704355b/src/chainparams.cpp#L55 +// Note output script includes a leading 0x41 and trailing 0xac (added below using the `script::Builder`). +#[rustfmt::skip] +const GENESIS_OUTPUT_PK: [u8; 65] = [ + 0x04, + 0x01, 0x84, 0x71, 0x0f, 0xa6, 0x89, 0xad, 0x50, + 0x23, 0x69, 0x0c, 0x80, 0xf3, 0xa4, 0x9c, 0x8f, + 0x13, 0xf8, 0xd4, 0x5b, 0x8c, 0x85, 0x7f, 0xbc, + 0xbc, 0x8b, 0xc4, 0xa8, 0xe4, 0xd3, 0xeb, 0x4b, + 0x10, 0xf4, 0xd4, 0x60, 0x4f, 0xa0, 0x8d, 0xce, + 0x60, 0x1a, 0xaf, 0x0f, 0x47, 0x02, 0x16, 0xfe, + 0x1b, 0x51, 0x85, 0x0b, 0x4a, 0xcf, 0x21, 0xb1, + 0x79, 0xc4, 0x50, 0x70, 0xac, 0x7b, 0x03, 0xa9 +]; + +/// Constructs and returns the coinbase (and only) transaction of the Dogecoin genesis block. +pub fn dogecoin_genesis_tx() -> Transaction { + // Base + let mut ret = Transaction { + version: transaction::Version::ONE, + lock_time: absolute::LockTime::ZERO, + input: vec![], + output: vec![], + }; + + let (in_script, out_script) = ( + script::Builder::new() + .push_int(486604799) + .push_int_non_minimal(4) + .push_slice(b"Nintondo") + .into_script(), + script::Builder::new().push_slice(GENESIS_OUTPUT_PK).push_opcode(OP_CHECKSIG).into_script(), + ); + + ret.input.push(TxIn { + previous_output: OutPoint::null(), + script_sig: in_script, + sequence: Sequence::MAX, + witness: Witness::default(), + }); + ret.output.push(TxOut { value: Amount::from_sat(88 * 100_000_000), script_pubkey: out_script }); + + // end + ret +} + +/// Constructs and returns the genesis block. +#[allow(dead_code)] +pub fn genesis_block(params: impl AsRef) -> Block { + let params = params.as_ref(); + let txdata = vec![dogecoin_genesis_tx()]; + let hash: sha256d::Hash = txdata[0].compute_txid().into(); + let merkle_root: TxMerkleNode = hash.into(); + + match params.dogecoin_params.network { + Network::Dogecoin => Block { + header: block::Header { + version: block::Version::ONE, + prev_blockhash: Hash::all_zeros(), + merkle_root, + time: 1386325540, + bits: CompactTarget::from_consensus(0x1e0ffff0), + nonce: 99943, + }, + auxpow: None, + txdata, + }, + Network::Testnet => Block { + header: block::Header { + version: block::Version::ONE, + prev_blockhash: Hash::all_zeros(), + merkle_root, + time: 1391503289, + bits: CompactTarget::from_consensus(0x1e0ffff0), + nonce: 997879, + }, + auxpow: None, + txdata, + }, + Network::Regtest => Block { + header: block::Header { + version: block::Version::ONE, + prev_blockhash: Hash::all_zeros(), + merkle_root, + time: 1296688602, + bits: CompactTarget::from_consensus(0x207fffff), + nonce: 2, + }, + auxpow: None, + txdata, + }, + } +} + +#[cfg(test)] +mod test { + use core::str::FromStr; + + use hex::test_hex_unwrap as hex; + + use super::*; + use crate::consensus::encode::serialize; + + #[test] + fn genesis_first_transaction() { + let gen = dogecoin_genesis_tx(); + + assert_eq!(gen.version, transaction::Version::ONE); + assert_eq!(gen.input.len(), 1); + assert_eq!(gen.input[0].previous_output.txid, Hash::all_zeros()); + assert_eq!(gen.input[0].previous_output.vout, 0xFFFFFFFF); + assert_eq!(serialize(&gen.input[0].script_sig), hex!("1004ffff001d0104084e696e746f6e646f")); + + assert_eq!(gen.input[0].sequence, Sequence::MAX); + assert_eq!(gen.output.len(), 1); + assert_eq!(serialize(&gen.output[0].script_pubkey), + hex!("4341040184710fa689ad5023690c80f3a49c8f13f8d45b8c857fbcbc8bc4a8e4d3eb4b10f4d4604fa08dce601aaf0f470216fe1b51850b4acf21b179c45070ac7b03a9ac")); + assert_eq!(gen.output[0].value, Amount::from_str("88 BTC").unwrap()); + assert_eq!(gen.lock_time, absolute::LockTime::ZERO); + + assert_eq!( + gen.compute_wtxid().to_string(), + "5b2a3f53f605d62c53e62932dac6925e3d74afa5a4b459745c36d42d0ed26a69" + ); + } + + #[test] + fn genesis_full_block() { + let gen = genesis_block(&Params::DOGECOIN); + + assert_eq!(gen.header.version, block::Version::ONE); + assert_eq!(gen.header.prev_blockhash, Hash::all_zeros()); + assert_eq!( + gen.header.merkle_root.to_string(), + "5b2a3f53f605d62c53e62932dac6925e3d74afa5a4b459745c36d42d0ed26a69" + ); + + assert_eq!(gen.header.time, 1386325540); + assert_eq!(gen.header.bits, CompactTarget::from_consensus(0x1e0ffff0)); + assert_eq!(gen.header.nonce, 99943); + assert_eq!( + gen.header.block_hash().to_string(), + "1a91e3dace36e2be3bf030a65679fe821aa1d6ef92e7c9902eb318182c355691" + ); + } + + #[test] + fn testnet_genesis_full_block() { + let gen = genesis_block(&Params::TESTNET); + assert_eq!(gen.header.version, block::Version::ONE); + assert_eq!(gen.header.prev_blockhash, Hash::all_zeros()); + assert_eq!( + gen.header.merkle_root.to_string(), + "5b2a3f53f605d62c53e62932dac6925e3d74afa5a4b459745c36d42d0ed26a69" + ); + assert_eq!(gen.header.time, 1391503289); + assert_eq!(gen.header.bits, CompactTarget::from_consensus(0x1e0ffff0)); + assert_eq!(gen.header.nonce, 997879); + assert_eq!( + gen.header.block_hash().to_string(), + "bb0a78264637406b6360aad926284d544d7049f45189db5664f3c4d07350559e" + ); + } + + #[test] + fn regtest_genesis_full_block() { + let gen = genesis_block(&Params::REGTEST); + assert_eq!(gen.header.version, block::Version::ONE); + assert_eq!(gen.header.prev_blockhash, Hash::all_zeros()); + assert_eq!( + gen.header.merkle_root.to_string(), + "5b2a3f53f605d62c53e62932dac6925e3d74afa5a4b459745c36d42d0ed26a69" + ); + assert_eq!(gen.header.time, 1296688602); + assert_eq!(gen.header.bits, CompactTarget::from_consensus(0x207fffff)); + assert_eq!(gen.header.nonce, 2); + assert_eq!( + gen.header.block_hash().to_string(), + "3d2160a3b5dc4a9d62e7e66a295f70313ac808440ef7400d6c0772171ce973a5" + ); + } +} diff --git a/bitcoin/src/dogecoin/mod.rs b/bitcoin/src/dogecoin/mod.rs index b1aa18e2e..b8114c502 100644 --- a/bitcoin/src/dogecoin/mod.rs +++ b/bitcoin/src/dogecoin/mod.rs @@ -5,8 +5,13 @@ //! This module provides support for de/serialization, parsing and execution on data structures and //! network messages related to Dogecoin. +pub mod constants; +pub mod params; + use crate::block::{Header, TxMerkleNode}; use crate::consensus::{encode, Decodable, Encodable}; +use crate::params::Params as BitcoinParams; +use crate::dogecoin::params::Params; use crate::internal_macros::impl_consensus_encoding; use crate::io::{Read, Write}; use crate::prelude::*; @@ -116,6 +121,38 @@ impl Encodable for Block { } } +/// The cryptocurrency network to act on. +#[derive(Copy, PartialEq, Eq, PartialOrd, Ord, Clone, Hash, Debug)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde", serde(crate = "actual_serde"))] +#[cfg_attr(feature = "serde", serde(rename_all = "lowercase"))] +#[non_exhaustive] +pub enum Network { + /// Mainnet Dogecoin. + Dogecoin, + /// Dogecoin's testnet network. + Testnet, + /// Dogecoin's regtest network. + Regtest, +} + +impl Network { + /// Returns the associated network parameters. + pub const fn params(self) -> &'static Params { + match self { + Network::Dogecoin => &Params::DOGECOIN, + Network::Testnet => &Params::TESTNET, + Network::Regtest => &Params::REGTEST, + } + } +} + +impl AsRef for Network { + fn as_ref(&self) -> &BitcoinParams { + &Self::params(*self).bitcoin_params + } +} + #[cfg(test)] mod tests { use hex::test_hex_unwrap as hex; @@ -237,4 +274,13 @@ mod tests { assert_eq!(serialize(&header.block_hash_with_scrypt()), test.output); } } + + #[test] + fn max_target_from_compact() { + // The highest possible target in Dogecoin is defined as 0x1e0fffff + let bits = 0x1e0fffff_u32; + let want = Target::MAX_ATTAINABLE_MAINNET_DOGE; + let got = Target::from_compact(CompactTarget::from_consensus(bits)); + assert_eq!(got, want) + } } diff --git a/bitcoin/src/dogecoin/params.rs b/bitcoin/src/dogecoin/params.rs new file mode 100644 index 000000000..78e9ab044 --- /dev/null +++ b/bitcoin/src/dogecoin/params.rs @@ -0,0 +1,124 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Dogecoin consensus parameters. +//! +//! This module provides a predefined set of parameters for different Dogecoin +//! chains (such as mainnet, testnet, regtest). +//! + +use crate::dogecoin::Network; +use crate::network::Network as BitcoinNetwork; +use crate::params::Params as BitcoinParams; +use crate::Target; + +/// Parameters that influence chain consensus. +#[derive(Debug, Clone)] +pub struct Params { + /// Parameters inherited from Bitcoin, reused for Dogecoin consensus. + pub bitcoin_params: BitcoinParams, + /// Parameters that are not inherited from Bitcoin. + pub dogecoin_params: DogecoinParams, +} + +/// Dogecoin-specific consensus parameters. +#[non_exhaustive] +#[derive(Debug, Clone)] +pub struct DogecoinParams { + /// Network for which parameters are valid. + pub network: Network, +} + +/// The mainnet parameters. +/// +/// Use this for a static reference e.g., `¶ms::MAINNET`. +/// +/// For more on static vs const see The Rust Reference [using-statics-or-consts] section. +/// +/// [using-statics-or-consts]: +pub static MAINNET: Params = Params::MAINNET; +/// The dogecoin testnet parameters. +pub static TESTNET: Params = Params::TESTNET; +/// The dogecoin regtest parameters. +pub static REGTEST: Params = Params::REGTEST; + +impl Params { + /// The mainnet parameters (alias for `Params::MAINNET`). + pub const DOGECOIN: Params = Params::MAINNET; + + /// The mainnet parameters. + pub const MAINNET: Params = Params { + bitcoin_params: BitcoinParams { + network: BitcoinNetwork::Bitcoin, + bip16_time: 1333238400, // Apr 1 2012 + bip34_height: 1034383, // 80d1364201e5df97e696c03bdd24dc885e8617b9de51e453c10a4f629b1e797a + bip65_height: 3464751, // 34cd2cbba4ba366f47e5aa0db5f02c19eba2adf679ceb6653ac003bdc9a0ef1f + bip66_height: 1034383, // 80d1364201e5df97e696c03bdd24dc885e8617b9de51e453c10a4f629b1e797a + rule_change_activation_threshold: 9576, // 95% of 10,080 + miner_confirmation_window: 10080, // 60 * 24 * 7 = 10,080 blocks, or one week + pow_limit: Target::MAX_ATTAINABLE_MAINNET_DOGE, + max_attainable_target: Target::MAX_ATTAINABLE_MAINNET_DOGE, + pow_target_spacing: 60, // 1 minute + pow_target_timespan: 4 * 60 * 60, // pre-digishield: 4 hours + allow_min_difficulty_blocks: false, + no_pow_retargeting: false, + }, + dogecoin_params: DogecoinParams { network: Network::Dogecoin }, + }; + + /// The Dogecoin testnet parameters. + pub const TESTNET: Params = Params { + bitcoin_params: BitcoinParams { + network: BitcoinNetwork::Testnet, + bip16_time: 1333238400, // Apr 1 2012 + bip34_height: 708658, // 21b8b97dcdb94caa67c7f8f6dbf22e61e0cfe0e46e1fff3528b22864659e9b38 + bip65_height: 1854705, // 955bd496d23790aba1ecfacb722b089a6ae7ddabaedf7d8fb0878f48308a71f9 + bip66_height: 708658, // 21b8b97dcdb94caa67c7f8f6dbf22e61e0cfe0e46e1fff3528b22864659e9b38 + rule_change_activation_threshold: 2880, // 2 days (note this is significantly lower than Bitcoin standard) + miner_confirmation_window: 10080, // 60 * 24 * 7 = 10,080 blocks, or one week + pow_limit: Target::MAX_ATTAINABLE_TESTNET_DOGE, + max_attainable_target: Target::MAX_ATTAINABLE_TESTNET_DOGE, + pow_target_spacing: 60, // 1 minute + pow_target_timespan: 4 * 60 * 60, // pre-digishield: 4 hours + allow_min_difficulty_blocks: true, + no_pow_retargeting: false, + }, + dogecoin_params: DogecoinParams { network: Network::Testnet }, + }; + + /// The Dogecoin regtest parameters. + pub const REGTEST: Params = Params { + bitcoin_params: BitcoinParams { + network: BitcoinNetwork::Regtest, + bip16_time: 1333238400, // Apr 1 2012 + bip34_height: 100000000, // not activated on regtest + bip65_height: 1351, + bip66_height: 1251, // used only in rpc tests + rule_change_activation_threshold: 540, // 75% + miner_confirmation_window: 720, + pow_limit: Target::MAX_ATTAINABLE_REGTEST_DOGE, + max_attainable_target: Target::MAX_ATTAINABLE_REGTEST_DOGE, + pow_target_spacing: 1, // regtest: 1 second blocks + pow_target_timespan: 4 * 60 * 60, // pre-digishield: 4 hours + allow_min_difficulty_blocks: true, + no_pow_retargeting: true, + }, + dogecoin_params: DogecoinParams { network: Network::Regtest }, + }; + + /// Creates parameters set for the given network. + pub const fn new(network: Network) -> Self { + match network { + Network::Dogecoin => Params::MAINNET, + Network::Testnet => Params::TESTNET, + Network::Regtest => Params::REGTEST, + } + } +} + +impl AsRef for Params { + fn as_ref(&self) -> &BitcoinParams { &self.bitcoin_params } +} + +impl AsRef for Params { + fn as_ref(&self) -> &Params { self } +} diff --git a/bitcoin/src/pow.rs b/bitcoin/src/pow.rs index d4cb696fc..aa2db499c 100644 --- a/bitcoin/src/pow.rs +++ b/bitcoin/src/pow.rs @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: CC0-1.0 +// SPDX-License-Identifier: CC0-1.0 OR Apache-2.0 //! Proof-of-work related integer types. //! @@ -166,6 +166,22 @@ impl Target { // https://github.com/bitcoin/bitcoin/blob/8105bce5b384c72cf08b25b7c5343622754e7337/src/kernel/chainparams.cpp#L348 pub const MAX_ATTAINABLE_SIGNET: Self = Target(U256(0x0377_ae00 << 80, 0)); + /// The maximum **attainable** target value on Dogecoin mainnet. + /// + /// Not all target values are attainable because consensus code uses the compact format to + /// represent targets (see [`CompactTarget`]). + pub const MAX_ATTAINABLE_MAINNET_DOGE: Self = Target(U256(0xFFFF_F000u128 << (204 - 128), 0)); + + /// The proof of work limit on Dogecoin testnet. + // Taken from Dogecoin Core but had lossy conversion to/from compact form. + // https://github.com/dogecoin/dogecoin/blob/d7cc7f8bbb5f790942d0ed0617f62447e7675233/src/chainparams.cpp#L248 + pub const MAX_ATTAINABLE_TESTNET_DOGE: Self = Target(U256(0xFFFF_F000u128 << (204 - 128), 0)); + + /// The proof of work limit on Dogecoin regtest. + // Taken from Dogecoin Core but had lossy conversion to/from compact form. + // https://github.com/dogecoin/dogecoin/blob/d7cc7f8bbb5f790942d0ed0617f62447e7675233/src/chainparams.cpp#L392 + pub const MAX_ATTAINABLE_REGTEST_DOGE: Self = Target(U256(0x7FFF_FF00u128 << 96, 0)); + /// Computes the [`Target`] value from a compact representation. /// /// ref: From 0b182fba26dade92aa3d64e19e85d88a54ad66dd Mon Sep 17 00:00:00 2001 From: Paul Liu Date: Wed, 11 Jun 2025 17:22:30 +0800 Subject: [PATCH 40/53] feat: Add more methods to the dogecoin network type (#7) --- bitcoin/src/dogecoin/constants.rs | 4 ++- bitcoin/src/dogecoin/mod.rs | 57 +++++++++++++++++++++++++++++++ bitcoin/src/network.rs | 2 +- 3 files changed, 61 insertions(+), 2 deletions(-) diff --git a/bitcoin/src/dogecoin/constants.rs b/bitcoin/src/dogecoin/constants.rs index a9a1e8c1a..0897189dc 100644 --- a/bitcoin/src/dogecoin/constants.rs +++ b/bitcoin/src/dogecoin/constants.rs @@ -149,7 +149,7 @@ mod test { #[test] fn genesis_full_block() { let gen = genesis_block(&Params::DOGECOIN); - + assert!(gen.check_merkle_root()); assert_eq!(gen.header.version, block::Version::ONE); assert_eq!(gen.header.prev_blockhash, Hash::all_zeros()); assert_eq!( @@ -169,6 +169,7 @@ mod test { #[test] fn testnet_genesis_full_block() { let gen = genesis_block(&Params::TESTNET); + assert!(gen.check_merkle_root()); assert_eq!(gen.header.version, block::Version::ONE); assert_eq!(gen.header.prev_blockhash, Hash::all_zeros()); assert_eq!( @@ -187,6 +188,7 @@ mod test { #[test] fn regtest_genesis_full_block() { let gen = genesis_block(&Params::REGTEST); + assert!(gen.check_merkle_root()); assert_eq!(gen.header.version, block::Version::ONE); assert_eq!(gen.header.prev_blockhash, Hash::all_zeros()); assert_eq!( diff --git a/bitcoin/src/dogecoin/mod.rs b/bitcoin/src/dogecoin/mod.rs index b8114c502..91f9982e5 100644 --- a/bitcoin/src/dogecoin/mod.rs +++ b/bitcoin/src/dogecoin/mod.rs @@ -14,8 +14,10 @@ use crate::params::Params as BitcoinParams; use crate::dogecoin::params::Params; use crate::internal_macros::impl_consensus_encoding; use crate::io::{Read, Write}; +use crate::p2p::Magic; use crate::prelude::*; use crate::{io, BlockHash, Transaction}; +use core::fmt; /// AuxPow version bit, see pub const VERSION_AUXPOW: i32 = 1 << 8; @@ -89,6 +91,23 @@ impl Block { /// Returns the block hash using the scrypt hash function. pub fn block_hash_with_scrypt(&self) -> BlockHash { self.header.block_hash_with_scrypt() } + + /// Checks if merkle root of header matches merkle root of the transaction list. + pub fn check_merkle_root(&self) -> bool { + match self.compute_merkle_root() { + Some(merkle_root) => self.header.merkle_root == merkle_root, + None => false, + } + } + + /// Compute merkle root of the transaction list in this block. + pub fn compute_merkle_root(&self) -> Option { + let hashes = self + .txdata + .iter() + .map(|obj| obj.compute_txid().to_raw_hash()); + crate::merkle_tree::calculate_root(hashes).map(|h| h.into()) + } } impl Decodable for Block { @@ -145,6 +164,15 @@ impl Network { Network::Regtest => &Params::REGTEST, } } + + /// Return the magic bytes for the given network. + pub fn magic(self) -> Magic { + match self { + Network::Dogecoin => Magic::from_bytes([0xC0, 0xC0, 0xC0, 0xC0]), + Network::Testnet => Magic::from_bytes([0xFC, 0xC1, 0xB7, 0xDC]), + Network::Regtest => Magic::from_bytes([0xFA, 0xBF, 0xB5, 0xDA]), + } + } } impl AsRef for Network { @@ -153,6 +181,35 @@ impl AsRef for Network { } } +impl AsRef for Network { + fn as_ref(&self) -> &Params { + self.params() + } +} + +impl fmt::Display for Network { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Network::Dogecoin => write!(f, "dogecoin"), + Network::Testnet => write!(f, "testnet"), + Network::Regtest => write!(f, "regtest"), + } + } +} + +impl core::str::FromStr for Network { + type Err = crate::network::ParseNetworkError; + + fn from_str(s: &str) -> Result { + match s { + "dogecoin" => Ok(Network::Dogecoin), + "testnet" => Ok(Network::Testnet), + "regtest" => Ok(Network::Regtest), + _ => Err(crate::network::ParseNetworkError(s.to_owned())), + } + } +} + #[cfg(test)] mod tests { use hex::test_hex_unwrap as hex; diff --git a/bitcoin/src/network.rs b/bitcoin/src/network.rs index 8df48b0be..3b60fd98d 100644 --- a/bitcoin/src/network.rs +++ b/bitcoin/src/network.rs @@ -256,7 +256,7 @@ pub mod as_core_arg { /// An error in parsing network string. #[derive(Debug, Clone, PartialEq, Eq)] #[non_exhaustive] -pub struct ParseNetworkError(String); +pub struct ParseNetworkError(pub(crate) String); impl fmt::Display for ParseNetworkError { fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { From 762f8af1401806a0104eb4d191a203d94d875312 Mon Sep 17 00:00:00 2001 From: mducroux Date: Thu, 12 Jun 2025 14:04:20 +0200 Subject: [PATCH 41/53] feat(dogecoin): add difficulty adjustment algorithm for blocks 0-5,000 (#8) This PR adds methods required for implementing Dogecoin's difficulty adjustment algorithm (DAA). In particular it adds the method `from_next_work_required_dogecoin()` which computes the target value for the next difficulty adjustment period, based on the `timespan` between start and end of period and the `last` target value. Note: this PR only adds the required logic to compute targets in block period 0-5,000 (see https://github.com/dogecoin/dogecoin/blob/51cbc1fd5d0d045dda2ad84f53572bbf524c6a8e/src/dogecoin.cpp#L41 for complete algorithm). A following PR will add the logic for further periods and in particular for the Digishield algorithm used in later periods. --- bitcoin/src/dogecoin/mod.rs | 129 +++++++++++++++++++++++++++++++++++- bitcoin/src/pow.rs | 93 ++++++++++++++++++++++++++ 2 files changed, 219 insertions(+), 3 deletions(-) diff --git a/bitcoin/src/dogecoin/mod.rs b/bitcoin/src/dogecoin/mod.rs index 91f9982e5..aab03be93 100644 --- a/bitcoin/src/dogecoin/mod.rs +++ b/bitcoin/src/dogecoin/mod.rs @@ -217,11 +217,12 @@ mod tests { use super::*; use crate::block::{ValidationError, Version}; use crate::consensus::encode::{deserialize, serialize}; - use crate::{CompactTarget, Network, Target, Work}; + use crate::{CompactTarget, Target, Work}; + use crate::{Network as BitcoinNetwork}; + #[test] fn dogecoin_block_test() { - let params = crate::consensus::Params::new(Network::Bitcoin); // Mainnet Dogecoin block 5794c80b80d9c33e0737a5353cd52b1f097f61d8d2b9f471e1702345080e0002 let some_block = hex!("01000000c76fe7f8ec09989d32b7907966fbd347134f80a7b71efce55fec502aa126ba3894b3065289ff8ba1ab4e8391771174d47cf2c974ebd24a1bdafd6c107d5a7a207d78bb52de8f001c00da8c3c0201000000010000000000000000000000000000000000000000000000000000000000000000ffffffff2602bc6d062f503253482f047178bb5208f8042975030000000d2f7374726174756d506f6f6c2f000000000100629b29c45500001976a91450e9fe87c705dcd4b7523b47e3314c2115f5d5df88ac0000000001000000015f48fabf4425324df2b5e58f4e9c771297f76f5fa37db7556f6fc1d22742da1f010000006a473044022062d29d2d26f7d826e7b72257486e294d284832743c7803a2901eb07e326b25a002207efc391b0f4e724c9d518075c0e056cc425540f845b0fd419ba8a9d49d69288301210297a2568525760a98454d84f5e5adba9fd0a41726a6fb774ddc407279e41e2061ffffffff0240bab598200000001976a91401348a2b83aeb6b1ba2a174a1a40b7c75fbeb12088ac0040be40250000001976a914025407d928ef333979d064ae233353d80e29d58c88ac00000000"); let cutoff_block = hex!("01000000c76fe7f8ec09989d32b7907966fbd347134f80a7b71efce55fec502aa126ba3894b3065289ff8ba1ab4e8391771174d47cf2c974ebd24a1bdafd6c107d5a7a207d78bb52de8f001c00da8c3c0201000000010000000000000000000000000000000000000000000000000000000000000000ffffffff2602bc6d062f503253482f047178bb5208f8042975030000000d2f7374726174756d506f6f6c2f000000000100629b29c45500001976a91450e9fe87c705dcd4b7523b47e3314c2115f5d5df88ac0000000001000000015f48fabf4425324df2b5e58f4e9c771297f76f5fa37db7556f6fc1d22742da1f010000006a473044022062d29d2d26f7d826e7b72257486e294d284832743c7803a2901eb07e326b25a002207efc391b0f4e724c9d518075c0e056cc425540f845b0fd419ba8a9d49d69288301210297a2568525760a98454d84f5e5adba9fd0a41726a6fb774ddc407279e41e2061ffffffff0240bab598200000001976a91401348a2b83aeb6b1ba2a174a1a40b7c75fbeb12088ac0040be40250000001976a914025407d928ef333979d064ae233353d80e29d58c88ac"); @@ -250,7 +251,9 @@ mod tests { real_decode.header.validate_pow_with_scrypt(real_decode.header.target()).unwrap(), real_decode.block_hash_with_scrypt() ); - assert_eq!(real_decode.header.difficulty(¶ms), 455); + // Bitcoin network is used because Dogecoin's difficulty calculation is based on Bitcoin's, + // which uses Bitcoin's `max_attainable_target` value + assert_eq!(real_decode.header.difficulty(BitcoinNetwork::Bitcoin), 455); assert_eq!(real_decode.header.difficulty_float(), 455.52430084170516); assert_eq!(serialize(&real_decode), some_block); @@ -340,4 +343,124 @@ mod tests { let got = Target::from_compact(CompactTarget::from_consensus(bits)); assert_eq!(got, want) } + + #[test] + fn compact_target_from_upwards_difficulty_adjustment() { + let params = Params::new(Network::Dogecoin); + let starting_bits = CompactTarget::from_consensus(0x1e0ffff0); // Genesis compact target on Mainnet + let start_time: u64 = 1386325540; // Genesis block unix time + let end_time: u64 = 1386475638; // Block 239 unix time + let timespan = end_time - start_time; // Slower than expected (150,098 seconds diff) + let adjustment = CompactTarget::from_next_work_required_dogecoin(starting_bits, timespan, ¶ms); + let adjustment_bits = CompactTarget::from_consensus(0x1e0fffff); // Block 240 compact target + assert_eq!(adjustment, adjustment_bits); + } + + #[test] + fn compact_target_from_downwards_difficulty_adjustment() { + let params = Params::new(Network::Dogecoin); + let starting_bits = CompactTarget::from_consensus(0x1e0fffff); // Block 240 compact target + let start_time: u64 = 1386475638; // Block 239 unix time + let end_time: u64 = 1386475840; // Block 479 unix time + let timespan = end_time - start_time; // Faster than expected (202 seconds diff) + let adjustment = CompactTarget::from_next_work_required_dogecoin(starting_bits, timespan, ¶ms); + let adjustment_bits = CompactTarget::from_consensus(0x1e00ffff); // Block 480 compact target + assert_eq!(adjustment, adjustment_bits); + } + + #[test] + fn compact_target_from_downwards_difficulty_adjustment_using_headers() { + use crate::{block::Version, dogecoin::constants::genesis_block, TxMerkleNode}; + use hashes::Hash; + let params = Params::new(Network::Dogecoin); + let epoch_start = genesis_block(¶ms).header; + // Block 239, the only information used are `bits` and `time` + let current = Header { + version: Version::ONE, + prev_blockhash: BlockHash::all_zeros(), + merkle_root: TxMerkleNode::all_zeros(), + time: 1386475638, + bits: epoch_start.bits, + nonce: epoch_start.nonce + }; + let adjustment = CompactTarget::from_header_difficulty_adjustment_dogecoin(epoch_start, current, params); + let adjustment_bits = CompactTarget::from_consensus(0x1e0fffff); // Block 240 compact target + assert_eq!(adjustment, adjustment_bits); + } + + #[test] + fn compact_target_from_upwards_difficulty_adjustment_using_headers() { + use crate::{block::Version, TxMerkleNode}; + use hashes::Hash; + let params = Params::new(Network::Dogecoin); + let starting_bits = CompactTarget::from_consensus(0x1e0fffff); // Block 479 compact target + // Block 239, the only information used is `time` + let epoch_start = Header { + version: Version::ONE, + prev_blockhash: BlockHash::all_zeros(), + merkle_root: TxMerkleNode::all_zeros(), + time: 1386475638, + bits: starting_bits, + nonce: 0 + }; + // Block 479, the only information used are `bits` and `time` + let current = Header { + version: Version::ONE, + prev_blockhash: BlockHash::all_zeros(), + merkle_root: TxMerkleNode::all_zeros(), + time: 1386475840, + bits: starting_bits, + nonce: 0 + }; + let adjustment = CompactTarget::from_header_difficulty_adjustment_dogecoin(epoch_start, current, params); + let adjustment_bits = CompactTarget::from_consensus(0x1e00ffff); // Block 480 compact target + assert_eq!(adjustment, adjustment_bits); + } + + #[test] + fn compact_target_from_maximum_upward_difficulty_adjustment() { + let params = Params::new(Network::Dogecoin); + let starting_bits = CompactTarget::from_consensus(21403001); // Arbitrary difficulty + let timespan = (0.06 * params.bitcoin_params.pow_target_timespan as f64) as u64; // > 16x Faster than expected + let got = CompactTarget::from_next_work_required_dogecoin(starting_bits, timespan, params); + let want = Target::from_compact(starting_bits) + .min_transition_threshold_dogecoin(5000) + .to_compact_lossy(); + assert_eq!(got, want); + } + + #[test] + fn compact_target_from_minimum_downward_difficulty_adjustment() { + let params = Params::new(Network::Dogecoin); + let starting_bits = CompactTarget::from_consensus(21403001); // Arbitrary difficulty + let timespan = 5 * params.bitcoin_params.pow_target_timespan; // > 4x Slower than expected + let got = CompactTarget::from_next_work_required_dogecoin(starting_bits, timespan, ¶ms); + let want = Target::from_compact(starting_bits) + .max_transition_threshold(params) + .to_compact_lossy(); + assert_eq!(got, want); + } + + #[test] + fn compact_target_from_adjustment_is_max_target() { + let params = Params::new(Network::Dogecoin); + let starting_bits = CompactTarget::from_consensus(0x1e0fffff); // Block 240 compact target (max target) + let timespan = 5 * params.bitcoin_params.pow_target_timespan; // > 4x Slower than expected + let got = CompactTarget::from_next_work_required(starting_bits, timespan, ¶ms); + let want = params.bitcoin_params.max_attainable_target.to_compact_lossy(); + assert_eq!(got, want); + } + + #[test] + fn roundtrip_compact_target() { + let consensus = 0x1e0f_ffff; + let compact = CompactTarget::from_consensus(consensus); + let t = Target::from_compact(CompactTarget::from_consensus(consensus)); + assert_eq!(t, Target::from(compact)); // From/Into sanity check. + + let back = t.to_compact_lossy(); + assert_eq!(back, compact); // From/Into sanity check. + + assert_eq!(back.to_consensus(), consensus); + } } diff --git a/bitcoin/src/pow.rs b/bitcoin/src/pow.rs index aa2db499c..7cc1cd4f8 100644 --- a/bitcoin/src/pow.rs +++ b/bitcoin/src/pow.rs @@ -316,6 +316,27 @@ impl Target { /// In line with Bitcoin Core this function may return a target value of zero. pub fn min_transition_threshold(&self) -> Self { Self(self.0 >> 2) } + /// Computes the minimum valid [`Target`] threshold allowed for a block in which a difficulty + /// adjustment occurs. + /// + /// The difficulty can only decrease by a factor of 4, 8, or 16 max on each difficulty + /// adjustment period, depending on the height. + /// + /// ref: + /// + /// # Returns + /// + /// In line with Dogecoin Core this function may return a target value of zero. + pub fn min_transition_threshold_dogecoin(&self, height: u32) -> Self { + if height > 10000 { + Self(self.0 >> 2) + } else if height > 5000 { + Self(self.0 >> 3) + } else { + Self(self.0 >> 4) + } + } + /// Computes the maximum valid [`Target`] threshold allowed for a block in which a difficulty /// adjustment occurs. /// @@ -429,6 +450,44 @@ impl CompactTarget { retarget.to_compact_lossy() } + /// Computes the [`CompactTarget`] from a difficulty adjustment in Dogecoin. + /// + /// ref: + /// + /// Given the previous Target, represented as a [`CompactTarget`], the difficulty is adjusted + /// by taking the timespan between them, and multipling the current [`CompactTarget`] by a factor + /// of the net timespan and expected timespan. The [`CompactTarget`] may not increase by more than + /// a factor of 4, adjust beyond the maximum threshold for the network, or decrease... TODO + /// + /// # Returns + /// + /// The expected [`CompactTarget`] recalculation. + pub fn from_next_work_required_dogecoin( + last: CompactTarget, + timespan: u64, + params: impl AsRef + ) -> CompactTarget { + let params = params.as_ref(); + if params.no_pow_retargeting { + return last; + } + // Comments relate to the `pow.cpp` file from Core. + // ref: + let min_timespan = params.pow_target_timespan >> 4; // Lines 64 + let max_timespan = params.pow_target_timespan << 2; // Lines 65 + let actual_timespan = timespan.clamp(min_timespan, max_timespan); // Lines 69-72 nModulatedTimespan + let prev_target: Target = last.into(); + let maximum_retarget = prev_target.max_transition_threshold(params); // bnPowLimit + let retarget = prev_target.0; // bnNew + let retarget = retarget.mul(actual_timespan.into()); + let retarget = retarget.div(params.pow_target_timespan.into()); + let retarget = Target(retarget); + if retarget.ge(&maximum_retarget) { + return maximum_retarget.to_compact_lossy(); + } + retarget.to_compact_lossy() + } + /// Computes the [`CompactTarget`] from a difficulty adjustment, /// assuming these are the relevant block headers. /// @@ -457,6 +516,40 @@ impl CompactTarget { CompactTarget::from_next_work_required(bits, timespan.into(), params) } + /// Computes the [`CompactTarget`] from a difficulty adjustment, + /// assuming these are the relevant block headers, in the Dogecoin network. + /// + /// Given two headers, representing the start and end of a difficulty adjustment epoch, + /// compute the [`CompactTarget`] based on the net time between them and the current + /// [`CompactTarget`]. + /// + /// # Note + /// + /// See [`CompactTarget::from_next_work_required_dogecoin`]. + /// + /// Unlike Bitcoin, Dogecoin uses overlapping intervals (see Time Wrap Attack bug fix + /// introduced by Litecoin). + /// + /// For example, to successfully compute the first difficulty adjustments on the Dogecoin network, + /// one would pass the following headers: + /// - `current`: Block 239, `last_epoch_boundary`: Block 0 + /// - `current`: Block 479, `last_epoch_boundary`: Block 239 + /// - `current`: Block 719, `last_epoch_boundary`: Block 479 + /// - `current`: Block 959, `last_epoch_boundary`: Block 719 + /// + /// # Returns + /// + /// The expected [`CompactTarget`] recalculation. + pub fn from_header_difficulty_adjustment_dogecoin( + last_epoch_boundary: Header, + current: Header, + params: impl AsRef, + ) -> CompactTarget { + let timespan = current.time - last_epoch_boundary.time; + let bits = current.bits; + CompactTarget::from_next_work_required_dogecoin(bits, timespan.into(), params) + } + /// Creates a [`CompactTarget`] from a consensus encoded `u32`. pub fn from_consensus(bits: u32) -> Self { Self(bits) } From ee2493a8af081474de21c318de59fdbfc40e362a Mon Sep 17 00:00:00 2001 From: Paul Liu Date: Thu, 12 Jun 2025 23:33:15 +0800 Subject: [PATCH 42/53] feat: Dogecoin specific address type (#5) Since dogecoin uses different prefixes and only supports P2PKH and P2SH address types, this PR introduces separate implementations for Address related types, which are adapted from the bitcoin ones. --- bitcoin/src/dogecoin/address/error.rs | 112 ++++ bitcoin/src/dogecoin/address/mod.rs | 776 ++++++++++++++++++++++++++ bitcoin/src/dogecoin/constants.rs | 7 + bitcoin/src/dogecoin/mod.rs | 5 +- 4 files changed, 899 insertions(+), 1 deletion(-) create mode 100644 bitcoin/src/dogecoin/address/error.rs create mode 100644 bitcoin/src/dogecoin/address/mod.rs diff --git a/bitcoin/src/dogecoin/address/error.rs b/bitcoin/src/dogecoin/address/error.rs new file mode 100644 index 000000000..521b19e7f --- /dev/null +++ b/bitcoin/src/dogecoin/address/error.rs @@ -0,0 +1,112 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Error code for the address module. + +use core::fmt; + +use internals::write_err; + +use crate::dogecoin::address::{Address, NetworkUnchecked}; +use crate::dogecoin::Network; + +pub use crate::address::error::{ + FromScriptError, InvalidBase58PayloadLengthError, InvalidLegacyPrefixError, + LegacyAddressTooLongError, P2shError, +}; + +/// Address parsing error. +#[derive(Debug, Clone, PartialEq, Eq)] +#[non_exhaustive] +pub enum ParseError { + /// Base58 error. + Base58(base58::Error), + /// Legacy address is too long. + LegacyAddressTooLong(LegacyAddressTooLongError), + /// Invalid base58 payload data length for legacy address. + InvalidBase58PayloadLength(InvalidBase58PayloadLengthError), + /// Invalid legacy address prefix in base58 data payload. + InvalidLegacyPrefix(InvalidLegacyPrefixError), + /// Address's network differs from required one. + NetworkValidation(NetworkValidationError), +} + +internals::impl_from_infallible!(ParseError); + +impl fmt::Display for ParseError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + use ParseError::*; + + match *self { + Base58(ref e) => write_err!(f, "base58 error"; e), + LegacyAddressTooLong(ref e) => write_err!(f, "legacy address base58 string"; e), + InvalidBase58PayloadLength(ref e) => write_err!(f, "legacy address base58 data"; e), + InvalidLegacyPrefix(ref e) => write_err!(f, "legacy address base58 prefix"; e), + NetworkValidation(ref e) => write_err!(f, "validation error"; e), + } + } +} + +#[cfg(feature = "std")] +impl std::error::Error for ParseError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + use ParseError::*; + + match *self { + Base58(ref e) => Some(e), + LegacyAddressTooLong(ref e) => Some(e), + InvalidBase58PayloadLength(ref e) => Some(e), + InvalidLegacyPrefix(ref e) => Some(e), + NetworkValidation(ref e) => Some(e), + } + } +} + +impl From for ParseError { + fn from(e: base58::Error) -> Self { + Self::Base58(e) + } +} + +impl From for ParseError { + fn from(e: LegacyAddressTooLongError) -> Self { + Self::LegacyAddressTooLong(e) + } +} + +impl From for ParseError { + fn from(e: InvalidBase58PayloadLengthError) -> Self { + Self::InvalidBase58PayloadLength(e) + } +} + +impl From for ParseError { + fn from(e: InvalidLegacyPrefixError) -> Self { + Self::InvalidLegacyPrefix(e) + } +} + +impl From for ParseError { + fn from(e: NetworkValidationError) -> Self { + Self::NetworkValidation(e) + } +} + +/// Address's network differs from required one. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct NetworkValidationError { + /// Network that was required. + pub(crate) required: Network, + /// The address itself. + pub(crate) address: Address, +} + +impl fmt::Display for NetworkValidationError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "address ")?; + fmt::Display::fmt(&self.address.0, f)?; + write!(f, " is not valid on {}", self.required) + } +} + +#[cfg(feature = "std")] +impl std::error::Error for NetworkValidationError {} diff --git a/bitcoin/src/dogecoin/address/mod.rs b/bitcoin/src/dogecoin/address/mod.rs new file mode 100644 index 000000000..f23a9a991 --- /dev/null +++ b/bitcoin/src/dogecoin/address/mod.rs @@ -0,0 +1,776 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Dogecoin addresses. +//! +//! Support for ordinary base58 Dogecoin addresses and private keys. +//! +//! # Example: creating a new address from a randomly-generated key pair +//! +//! ```rust +//! # #[cfg(feature = "rand-std")] { +//! use bitcoin::dogecoin::{Address, Network}; +//! use bitcoin::{PublicKey, secp256k1::{rand, Secp256k1}}; +//! +//! // Generate random key pair. +//! let s = Secp256k1::new(); +//! let public_key = PublicKey::new(s.generate_keypair(&mut rand::thread_rng()).1); +//! +//! // Generate pay-to-pubkey-hash address. +//! let address = Address::p2pkh(&public_key, Network::Dogecoin); +//! # } +//! ``` +//! +//! # Note: creating a new address requires the rand-std feature flag +//! +//! ```toml +//! bitcoin = { version = "...", features = ["rand-std"] } +//! ``` + +pub mod error; + +use core::fmt; +use core::marker::PhantomData; +use core::str::FromStr; + +use hashes::Hash; +use secp256k1::XOnlyPublicKey; + +use crate::blockdata::constants::MAX_SCRIPT_ELEMENT_SIZE; +use crate::blockdata::script::{self, Script, ScriptBuf, ScriptHash}; +use crate::crypto::key::{PubkeyHash, PublicKey}; +use crate::dogecoin::constants::{ + PUBKEY_ADDRESS_PREFIX_MAINNET, PUBKEY_ADDRESS_PREFIX_REGTEST, PUBKEY_ADDRESS_PREFIX_TESTNET, + SCRIPT_ADDRESS_PREFIX_MAINNET, SCRIPT_ADDRESS_PREFIX_REGTEST, SCRIPT_ADDRESS_PREFIX_TESTNET, +}; +use crate::dogecoin::Network; + +#[rustfmt::skip] // Keep public re-exports separate. +#[doc(inline)] +pub use self::{ + error::{ + FromScriptError, InvalidBase58PayloadLengthError, InvalidLegacyPrefixError, LegacyAddressTooLongError, + NetworkValidationError, ParseError, P2shError, + }, +}; + +// Re-export shared types from bitcoin::address. +pub use crate::address::{AddressType, NetworkChecked, NetworkUnchecked, NetworkValidation}; + +/// The inner representation of an address, without the network validation tag. +/// +/// This struct represents the inner representation of an address without the network validation +/// tag, which is used to ensure that addresses are used only on the appropriate network. +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +enum AddressInner { + P2pkh { hash: PubkeyHash, network: Network }, + P2sh { hash: ScriptHash, network: Network }, +} + +/// Formats bech32 as upper case if alternate formatting is chosen (`{:#}`). +impl fmt::Display for AddressInner { + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + use AddressInner::*; + match self { + P2pkh { hash, network } => { + let mut prefixed = [0; 21]; + prefixed[0] = match network { + Network::Dogecoin => PUBKEY_ADDRESS_PREFIX_MAINNET, + Network::Testnet => PUBKEY_ADDRESS_PREFIX_TESTNET, + Network::Regtest => PUBKEY_ADDRESS_PREFIX_REGTEST, + }; + prefixed[1..].copy_from_slice(&hash[..]); + base58::encode_check_to_fmt(fmt, &prefixed[..]) + } + P2sh { hash, network } => { + let mut prefixed = [0; 21]; + prefixed[0] = match network { + Network::Dogecoin => SCRIPT_ADDRESS_PREFIX_MAINNET, + Network::Testnet => SCRIPT_ADDRESS_PREFIX_TESTNET, + Network::Regtest => SCRIPT_ADDRESS_PREFIX_REGTEST, + }; + prefixed[1..].copy_from_slice(&hash[..]); + base58::encode_check_to_fmt(fmt, &prefixed[..]) + } + } + } +} + +/// The data encoded by an `Address`. +/// +/// This is the data used to encumber an output that pays to this address i.e., it is the address +/// excluding the network information. +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[non_exhaustive] +pub enum AddressData { + /// Data encoded by a P2PKH address. + P2pkh { + /// The pubkey hash used to encumber outputs to this address. + pubkey_hash: PubkeyHash, + }, + /// Data encoded by a P2SH address. + P2sh { + /// The script hash used to encumber outputs to this address. + script_hash: ScriptHash, + }, +} + +/// A Dogecoin address. +/// +/// ### Parsing addresses +/// +/// When parsing string as an address, one has to pay attention to the network, on which the parsed +/// address is supposed to be valid. For the purpose of this validation, `Address` has +/// [`is_valid_for_network`](Address::is_valid_for_network) method. In order to provide more safety, +/// enforced by compiler, `Address` also contains a special marker type, which indicates whether network of the parsed +/// address has been checked. This marker type will prevent from calling certain functions unless the network +/// verification has been successfully completed. +/// +/// The result of parsing an address is `Address` suggesting that network of the parsed address +/// has not yet been verified. To perform this verification, method [`require_network`](Address::require_network) +/// can be called, providing network on which the address is supposed to be valid. If the verification succeeds, +/// `Address` is returned. +/// +/// The types `Address` and `Address` are synonymous, i. e. they can be used interchangeably. +/// +/// ```rust +/// use std::str::FromStr; +/// use bitcoin::dogecoin::{Address, Network}; +/// use bitcoin::address::{NetworkUnchecked, NetworkChecked}; +/// +/// // variant 1 +/// let address: Address = "DUSamFaUtRQ78DVidoeY3J8keYkQXdinrt".parse().unwrap(); +/// let address: Address = address.require_network(Network::Dogecoin).unwrap(); +/// +/// // variant 2 +/// let address: Address = Address::from_str("DUSamFaUtRQ78DVidoeY3J8keYkQXdinrt").unwrap() +/// .require_network(Network::Dogecoin).unwrap(); +/// +/// // variant 3 +/// let address: Address = "DUSamFaUtRQ78DVidoeY3J8keYkQXdinrt".parse::>() +/// .unwrap().require_network(Network::Dogecoin).unwrap(); +/// ``` +/// +/// ### Formatting addresses +/// +/// To format address into its textual representation, both `Debug` (for usage in programmer-facing, +/// debugging context) and `Display` (for user-facing output) can be used, with the following caveats: +/// +/// 1. `Display` is implemented only for `Address`: +/// +/// ``` +/// # use std::str::FromStr; +/// # use bitcoin::dogecoin::address::{Address, NetworkChecked}; +/// let address: Address = Address::from_str("n48pquU8ieq7gidgJJ4vWD2jbsErmZvrwe") +/// .unwrap().assume_checked(); +/// assert_eq!(address.to_string(), "n48pquU8ieq7gidgJJ4vWD2jbsErmZvrwe"); +/// ``` +/// +/// ```ignore +/// # use std::str::FromStr; +/// # use bitcoin::dogecoin::address::{Address, NetworkChecked}; +/// let address: Address = Address::from_str("n48pquU8ieq7gidgJJ4vWD2jbsErmZvrwe") +/// .unwrap(); +/// let s = address.to_string(); // does not compile +/// ``` +/// +/// 2. `Debug` on `Address` does not produce clean address but address wrapped by +/// an indicator that its network has not been checked. This is to encourage programmer to properly +/// check the network and use `Display` in user-facing context. +/// +/// ``` +/// # use std::str::FromStr; +/// # use bitcoin::dogecoin::address::{Address, NetworkUnchecked}; +/// let address: Address = Address::from_str("n48pquU8ieq7gidgJJ4vWD2jbsErmZvrwe") +/// .unwrap(); +/// assert_eq!(format!("{:?}", address), "Address(n48pquU8ieq7gidgJJ4vWD2jbsErmZvrwe)"); +/// ``` +/// +/// ``` +/// # use std::str::FromStr; +/// # use bitcoin::dogecoin::address::{Address, NetworkChecked}; +/// let address: Address = Address::from_str("n48pquU8ieq7gidgJJ4vWD2jbsErmZvrwe") +/// .unwrap().assume_checked(); +/// assert_eq!(format!("{:?}", address), "n48pquU8ieq7gidgJJ4vWD2jbsErmZvrwe"); +/// ``` +#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +// The `#[repr(transparent)]` attribute is used to guarantee the layout of the `Address` struct. It +// is an implementation detail and users should not rely on it in their code. +#[repr(transparent)] +pub struct Address(AddressInner, PhantomData) +where + V: NetworkValidation; + +#[cfg(feature = "serde")] +struct DisplayUnchecked<'a, N: NetworkValidation>(&'a Address); + +#[cfg(feature = "serde")] +impl fmt::Display for DisplayUnchecked<'_, N> { + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + fmt::Display::fmt(&self.0 .0, fmt) + } +} + +#[cfg(feature = "serde")] +crate::serde_utils::serde_string_deserialize_impl!(Address, "a Dogecoin address"); + +#[cfg(feature = "serde")] +impl serde::Serialize for Address { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.collect_str(&DisplayUnchecked(self)) + } +} + +/// Methods on [`Address`] that can be called on both `Address` and +/// `Address`. +impl Address { + /// Returns a reference to the address as if it was unchecked. + pub fn as_unchecked(&self) -> &Address { + unsafe { &*(self as *const Address as *const Address) } + } + + /// Marks the network of this address as unchecked. + pub fn into_unchecked(self) -> Address { + Address(self.0, PhantomData) + } +} + +/// Methods and functions that can be called only on `Address`. +impl Address { + /// Creates a pay to (compressed) public key hash address from a public key. + /// + /// This is the preferred non-witness type address. + #[inline] + pub fn p2pkh(pk: impl Into, network: impl Into) -> Address { + let hash = pk.into(); + Self(AddressInner::P2pkh { hash, network: network.into() }, PhantomData) + } + + /// Creates a pay to script hash P2SH address from a script. + /// + /// This address type was introduced with BIP16 and is the popular type to implement multi-sig + /// these days. + #[inline] + pub fn p2sh(script: &Script, network: impl Into) -> Result { + if script.len() > MAX_SCRIPT_ELEMENT_SIZE { + return Err(P2shError::ExcessiveScriptSize); + } + let hash = script.script_hash(); + Ok(Address::p2sh_from_hash(hash, network)) + } + + /// Creates a pay to script hash P2SH address from a script hash. + /// + /// # Warning + /// + /// The `hash` pre-image (redeem script) must not exceed 520 bytes in length + /// otherwise outputs created from the returned address will be un-spendable. + pub fn p2sh_from_hash(hash: ScriptHash, network: impl Into) -> Address { + Self(AddressInner::P2sh { hash, network: network.into() }, PhantomData) + } + + /// Gets the address type of the address. + /// + /// # Returns + /// + /// None if unknown, non-standard or related to the future witness version. + #[inline] + pub fn address_type(&self) -> Option { + match self.0 { + AddressInner::P2pkh { .. } => Some(AddressType::P2pkh), + AddressInner::P2sh { .. } => Some(AddressType::P2sh), + } + } + + /// Gets the address data from this address. + pub fn to_address_data(&self) -> AddressData { + use AddressData::*; + + match self.0 { + AddressInner::P2pkh { hash, network: _ } => P2pkh { pubkey_hash: hash }, + AddressInner::P2sh { hash, network: _ } => P2sh { script_hash: hash }, + } + } + + /// Gets the pubkey hash for this address if this is a P2PKH address. + pub fn pubkey_hash(&self) -> Option { + use AddressInner::*; + + match self.0 { + P2pkh { ref hash, network: _ } => Some(*hash), + _ => None, + } + } + + /// Gets the script hash for this address if this is a P2SH address. + pub fn script_hash(&self) -> Option { + use AddressInner::*; + + match self.0 { + P2sh { ref hash, network: _ } => Some(*hash), + _ => None, + } + } + + /// Constructs an [`Address`] from an output script (`scriptPubkey`). + pub fn from_script( + script: &Script, + network: impl Into, + ) -> Result { + let network = network.into(); + if script.is_p2pkh() { + let bytes = script.as_bytes()[3..23].try_into().expect("statically 20B long"); + let hash = PubkeyHash::from_byte_array(bytes); + Ok(Address::p2pkh(hash, network)) + } else if script.is_p2sh() { + let bytes = script.as_bytes()[2..22].try_into().expect("statically 20B long"); + let hash = ScriptHash::from_byte_array(bytes); + Ok(Address::p2sh_from_hash(hash, network)) + } else { + Err(FromScriptError::UnrecognizedScript) + } + } + + /// Generates a script pubkey spending to this address. + pub fn script_pubkey(&self) -> ScriptBuf { + use AddressInner::*; + match self.0 { + P2pkh { ref hash, network: _ } => ScriptBuf::new_p2pkh(hash), + P2sh { ref hash, network: _ } => ScriptBuf::new_p2sh(hash), + } + } + + /// Returns true if the given pubkey is directly related to the address payload. + /// + /// This is determined by directly comparing the address payload with either the + /// hash of the given public key or the segwit redeem hash generated from the + /// given key. For taproot addresses, the supplied key is assumed to be tweaked + pub fn is_related_to_pubkey(&self, pubkey: &PublicKey) -> bool { + let pubkey_hash = pubkey.pubkey_hash(); + let payload = self.payload_as_bytes(); + let xonly_pubkey = XOnlyPublicKey::from(pubkey.inner); + + (*pubkey_hash.as_byte_array() == *payload) || (xonly_pubkey.serialize() == *payload) + } + + /// Returns true if the address creates a particular script + /// This function doesn't make any allocations. + pub fn matches_script_pubkey(&self, script: &Script) -> bool { + use AddressInner::*; + match self.0 { + P2pkh { ref hash, network: _ } if script.is_p2pkh() => { + &script.as_bytes()[3..23] == >::as_ref(hash) + } + P2sh { ref hash, network: _ } if script.is_p2sh() => { + &script.as_bytes()[2..22] == >::as_ref(hash) + } + P2pkh { .. } | P2sh { .. } => false, + } + } + + /// Returns the "payload" for this address. + /// + /// The "payload" is the useful stuff excluding serialization prefix, the exact payload is + /// dependent on the inner address: + /// + /// - For p2sh, the payload is the script hash. + /// - For p2pkh, the payload is the pubkey hash. + fn payload_as_bytes(&self) -> &[u8] { + use AddressInner::*; + match self.0 { + P2sh { ref hash, network: _ } => hash.as_ref(), + P2pkh { ref hash, network: _ } => hash.as_ref(), + } + } +} + +/// Methods that can be called only on `Address`. +impl Address { + /// Returns a reference to the checked address. + /// + /// This function is dangerous in case the address is not a valid checked address. + pub fn assume_checked_ref(&self) -> &Address { + unsafe { &*(self as *const Address as *const Address) } + } + + /// Parsed addresses do not always have *one* network. The problem is that testnet, + /// regtest p2pkh addresses use different prefixes, but testnet and regtest p2sh + /// addresses use the same prefix. + /// + /// So if one wants to check if an address belongs to a certain network a simple + /// comparison is not enough anymore. Instead this function can be used. + /// + /// ```rust + /// use bitcoin::dogecoin::{Address, Network}; + /// use bitcoin::address::NetworkUnchecked; + /// + /// let address: Address = "no2dRNaFqxNjWZLeTRu4XyCuzeGdE3VY2S".parse().unwrap(); + /// assert!(address.is_valid_for_network(Network::Testnet)); + /// assert_eq!(address.is_valid_for_network(Network::Regtest), false); + /// + /// let address: Address = "n48pquU8ieq7gidgJJ4vWD2jbsErmZvrwe".parse().unwrap(); + /// assert!(address.is_valid_for_network(Network::Regtest)); + /// assert_eq!(address.is_valid_for_network(Network::Testnet), false); + /// + /// let address: Address = "DUSamFaUtRQ78DVidoeY3J8keYkQXdinrt".parse().unwrap(); + /// assert!(address.is_valid_for_network(Network::Dogecoin)); + /// assert_eq!(address.is_valid_for_network(Network::Testnet), false); + /// + /// let address: Address = "2N3zXjbwdTcPsJiy8sUK9FhWJhqQCxA8Jjr".parse().unwrap(); + /// assert!(address.is_valid_for_network(Network::Testnet)); + /// assert!(address.is_valid_for_network(Network::Regtest)); + /// assert_eq!(address.is_valid_for_network(Network::Dogecoin), false); + /// ``` + pub fn is_valid_for_network(&self, n: Network) -> bool { + use AddressInner::*; + match self.0 { + P2pkh { hash: _, ref network } => *network == n, + P2sh { hash: _, network: Network::Dogecoin } => n == Network::Dogecoin, + P2sh { hash: _, network: Network::Testnet } => { + n == Network::Testnet || n == Network::Regtest + } + P2sh { hash: _, network: Network::Regtest } => { + n == Network::Testnet || n == Network::Regtest + } + } + } + + /// Checks whether network of this address is as required. + /// + /// For details about this mechanism, see section [*Parsing addresses*](Address#parsing-addresses) + /// on [`Address`]. + /// + /// # Errors + /// + /// This function only ever returns the [`ParseError::NetworkValidation`] variant of + /// `ParseError`. This is not how we normally implement errors in this library but + /// `require_network` is not a typical function, it is conceptually part of string parsing. + /// + /// # Examples + /// + /// ``` + /// use bitcoin::address::{NetworkChecked, NetworkUnchecked}; + /// use bitcoin::dogecoin::{Address, Network, ParseError}; + /// + /// const ADDR: &str = "DUSamFaUtRQ78DVidoeY3J8keYkQXdinrt"; + /// + /// fn parse_and_validate_address(network: Network) -> Result { + /// let address = ADDR.parse::>()? + /// .require_network(network)?; + /// Ok(address) + /// } + /// + /// fn parse_and_validate_address_combinator(network: Network) -> Result { + /// let address = ADDR.parse::>() + /// .and_then(|a| a.require_network(network))?; + /// Ok(address) + /// } + /// + /// fn parse_and_validate_address_show_types(network: Network) -> Result { + /// let address: Address = ADDR.parse::>()? + /// .require_network(network)?; + /// Ok(address) + /// } + /// + /// let network = Network::Dogecoin; // Don't hard code network in applications. + /// let _ = parse_and_validate_address(network).unwrap(); + /// let _ = parse_and_validate_address_combinator(network).unwrap(); + /// let _ = parse_and_validate_address_show_types(network).unwrap(); + /// ``` + #[inline] + pub fn require_network(self, required: Network) -> Result { + if self.is_valid_for_network(required) { + Ok(self.assume_checked()) + } else { + Err(NetworkValidationError { required, address: self }.into()) + } + } + + /// Marks, without any additional checks, network of this address as checked. + /// + /// Improper use of this method may lead to loss of funds. Reader will most likely prefer + /// [`require_network`](Address::require_network) as a safe variant. + /// For details about this mechanism, see section [*Parsing addresses*](Address#parsing-addresses) + /// on [`Address`]. + #[inline] + pub fn assume_checked(self) -> Address { + Address(self.0, PhantomData) + } +} + +impl From
for script::ScriptBuf { + fn from(a: Address) -> Self { + a.script_pubkey() + } +} + +// Alternate formatting `{:#}` is used to return uppercase version of bech32 addresses which should +// be used in QR codes, see [`Address::to_qr_uri`]. +impl fmt::Display for Address { + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + fmt::Display::fmt(&self.0, fmt) + } +} + +impl fmt::Debug for Address { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + if V::IS_CHECKED { + fmt::Display::fmt(&self.0, f) + } else { + write!(f, "Address(")?; + fmt::Display::fmt(&self.0, f)?; + write!(f, ")") + } + } +} + +/// Address can be parsed only with `NetworkUnchecked`. +impl FromStr for Address { + type Err = ParseError; + + fn from_str(s: &str) -> Result, ParseError> { + if s.len() > 50 { + return Err(LegacyAddressTooLongError { length: s.len() }.into()); + } + let data = base58::decode_check(s)?; + if data.len() != 21 { + return Err(InvalidBase58PayloadLengthError { length: s.len() }.into()); + } + + let (prefix, data) = data.split_first().expect("length checked above"); + let data: [u8; 20] = data.try_into().expect("length checked above"); + + let inner = match *prefix { + PUBKEY_ADDRESS_PREFIX_MAINNET => { + let hash = PubkeyHash::from_byte_array(data); + AddressInner::P2pkh { hash, network: Network::Dogecoin } + } + PUBKEY_ADDRESS_PREFIX_TESTNET => { + let hash = PubkeyHash::from_byte_array(data); + AddressInner::P2pkh { hash, network: Network::Testnet } + } + PUBKEY_ADDRESS_PREFIX_REGTEST => { + let hash = PubkeyHash::from_byte_array(data); + AddressInner::P2pkh { hash, network: Network::Regtest } + } + SCRIPT_ADDRESS_PREFIX_MAINNET => { + let hash = ScriptHash::from_byte_array(data); + AddressInner::P2sh { hash, network: Network::Dogecoin } + } + // Because SCRIPT_ADDRESS_PREFIX_TESTNET and SCRIPT_ADDRESS_PREFIX_REGTTEST are + // the same, we do not differentiate between Testnet and Regtest only when it is P2sh. + // This is handled in function is_valid_for_network (its docstring has more + // explanation). + SCRIPT_ADDRESS_PREFIX_TESTNET => { + let hash = ScriptHash::from_byte_array(data); + AddressInner::P2sh { hash, network: Network::Testnet } + } + invalid => return Err(InvalidLegacyPrefixError { invalid }.into()), + }; + + Ok(Address(inner, PhantomData)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::dogecoin::Network::{Dogecoin, Testnet}; + + fn roundtrips(addr: &Address, network: Network) { + assert_eq!( + Address::from_str(&addr.to_string()).unwrap().assume_checked(), + *addr, + "string round-trip failed for {}", + addr, + ); + assert_eq!( + Address::from_script(&addr.script_pubkey(), network) + .expect("failed to create inner address from script_pubkey"), + *addr, + "script round-trip failed for {}", + addr, + ); + + #[cfg(feature = "serde")] + { + let ser = serde_json::to_string(addr).expect("failed to serialize address"); + let back: Address = + serde_json::from_str(&ser).expect("failed to deserialize address"); + assert_eq!(back.assume_checked(), *addr, "serde round-trip failed for {}", addr) + } + } + + #[test] + fn test_p2pkh_address_58() { + let hash = "162c5ea71c0b23f5b9022ef047c4a86470a5b070".parse::().unwrap(); + let addr = Address::p2pkh(hash, Dogecoin); + + assert_eq!( + addr.script_pubkey(), + ScriptBuf::from_hex("76a914162c5ea71c0b23f5b9022ef047c4a86470a5b07088ac").unwrap() + ); + assert_eq!(&addr.to_string(), "D7ALZLo7BL5vM9Vb4vAqvqwX9fpQ4wKRiy"); + assert_eq!(addr.address_type(), Some(AddressType::P2pkh)); + roundtrips(&addr, Dogecoin); + } + + #[test] + fn test_p2pkh_from_key() { + let key = "048d5141948c1702e8c95f438815794b87f706a8d4cd2bffad1dc1570971032c9b6042a0431ded2478b5c9cf2d81c124a5e57347a3c63ef0e7716cf54d613ba183".parse::().unwrap(); + let addr = Address::p2pkh(key, Dogecoin); + assert_eq!(&addr.to_string(), "DUSamFaUtRQ78DVidoeY3J8keYkQXdinrt"); + + let key = "03df154ebfcf29d29cc10d5c2565018bce2d9edbab267c31d2caf44a63056cf99f" + .parse::() + .unwrap(); + let addr = Address::p2pkh(key, Testnet); + assert_eq!(&addr.to_string(), "neRuCZsfnZaJN8FmxxVZDSZbcGATnzGByf"); + assert_eq!(addr.address_type(), Some(AddressType::P2pkh)); + roundtrips(&addr, Testnet); + } + + #[test] + fn test_p2sh_address_58() { + let hash = "162c5ea71c0b23f5b9022ef047c4a86470a5b070".parse::().unwrap(); + let addr = Address::p2sh_from_hash(hash, Dogecoin); + + assert_eq!( + addr.script_pubkey(), + ScriptBuf::from_hex("a914162c5ea71c0b23f5b9022ef047c4a86470a5b07087").unwrap(), + ); + assert_eq!(&addr.to_string(), "9tTWgUQoVtNuogNtsZWJ3qmE7dkrPTrVAj"); + assert_eq!(addr.address_type(), Some(AddressType::P2sh)); + roundtrips(&addr, Dogecoin); + } + + #[test] + fn test_p2sh_parse() { + let script = ScriptBuf::from_hex("552103a765fc35b3f210b95223846b36ef62a4e53e34e2925270c2c7906b92c9f718eb2103c327511374246759ec8d0b89fa6c6b23b33e11f92c5bc155409d86de0c79180121038cae7406af1f12f4786d820a1466eec7bc5785a1b5e4a387eca6d797753ef6db2103252bfb9dcaab0cd00353f2ac328954d791270203d66c2be8b430f115f451b8a12103e79412d42372c55dd336f2eb6eb639ef9d74a22041ba79382c74da2338fe58ad21035049459a4ebc00e876a9eef02e72a3e70202d3d1f591fc0dd542f93f642021f82102016f682920d9723c61b27f562eb530c926c00106004798b6471e8c52c60ee02057ae").unwrap(); + let addr = Address::p2sh(&script, Testnet).unwrap(); + assert_eq!(&addr.to_string(), "2N3zXjbwdTcPsJiy8sUK9FhWJhqQCxA8Jjr"); + assert_eq!(addr.address_type(), Some(AddressType::P2sh)); + roundtrips(&addr, Testnet); + } + + #[test] + fn test_p2sh_parse_for_large_script() { + let script = ScriptBuf::from_hex("552103a765fc35b3f210b95223846b36ef62a4e53e34e2925270c2c7906b92c9f718eb2103c327511374246759ec8d0b89fa6c6b23b33e11f92c5bc155409d86de0c79180121038cae7406af1f12f4786d820a1466eec7bc5785a1b5e4a387eca6d797753ef6db2103252bfb9dcaab0cd00353f2ac328954d791270203d66c2be8b430f115f451b8a12103e79412d42372c55dd336f2eb6eb639ef9d74a22041ba79382c74da2338fe58ad21035049459a4ebc00e876a9eef02e72a3e70202d3d1f591fc0dd542f93f642021f82102016f682920d9723c61b27f562eb530c926c00106004798b6471e8c52c60ee02057ae12123122313123123ac1231231231231313123131231231231313212313213123123552103a765fc35b3f210b95223846b36ef62a4e53e34e2925270c2c7906b92c9f718eb2103c327511374246759ec8d0b89fa6c6b23b33e11f92c5bc155409d86de0c79180121038cae7406af1f12f4786d820a1466eec7bc5785a1b5e4a387eca6d797753ef6db2103252bfb9dcaab0cd00353f2ac328954d791270203d66c2be8b430f115f451b8a12103e79412d42372c55dd336f2eb6eb639ef9d74a22041ba79382c74da2338fe58ad21035049459a4ebc00e876a9eef02e72a3e70202d3d1f591fc0dd542f93f642021f82102016f682920d9723c61b27f562eb530c926c00106004798b6471e8c52c60ee02057ae12123122313123123ac1231231231231313123131231231231313212313213123123552103a765fc35b3f210b95223846b36ef62a4e53e34e2925270c2c7906b92c9f718eb2103c327511374246759ec8d0b89fa6c6b23b33e11f92c5bc155409d86de0c79180121038cae7406af1f12f4786d820a1466eec7bc5785a1b5e4a387eca6d797753ef6db2103252bfb9dcaab0cd00353f2ac328954d791270203d66c2be8b430f115f451b8a12103e79412d42372c55dd336f2eb6eb639ef9d74a22041ba79382c74da2338fe58ad21035049459a4ebc00e876a9eef02e72a3e70202d3d1f591fc0dd542f93f642021f82102016f682920d9723c61b27f562eb530c926c00106004798b6471e8c52c60ee02057ae12123122313123123ac1231231231231313123131231231231313212313213123123").unwrap(); + assert_eq!(Address::p2sh(&script, Testnet), Err(P2shError::ExcessiveScriptSize)); + } + + #[test] + fn test_address_debug() { + // This is not really testing output of Debug but the ability and proper functioning + // of Debug derivation on structs generic in NetworkValidation. + #[derive(Debug)] + #[allow(unused)] + struct Test { + address: Address, + } + + let addr_str = "n48pquU8ieq7gidgJJ4vWD2jbsErmZvrwe"; + let unchecked = Address::from_str(addr_str).unwrap(); + + assert_eq!( + format!("{:?}", Test { address: unchecked.clone() }), + format!("Test {{ address: Address({}) }}", addr_str) + ); + + assert_eq!( + format!("{:?}", Test { address: unchecked.assume_checked() }), + format!("Test {{ address: {} }}", addr_str) + ); + } + + #[test] + fn test_address_type() { + let addresses = [ + ("DMKhUaRmnxJXfDxyFguMnMjVdgvnNipFzt", Some(AddressType::P2pkh)), + ("A1yb6viUzAcUWftRHT6GpnCwvhXHg4CV1x", Some(AddressType::P2sh)), + ]; + for (address, expected_type) in &addresses { + let addr = + Address::from_str(address).unwrap().require_network(Dogecoin).expect("mainnet"); + assert_eq!(&addr.address_type(), expected_type); + } + } + + #[test] + #[cfg(feature = "serde")] + fn test_json_serialize() { + use serde_json; + + let addr = + Address::from_str("D7ALZLo7BL5vM9Vb4vAqvqwX9fpQ4wKRiy").unwrap().assume_checked(); + let json = serde_json::to_value(&addr).unwrap(); + assert_eq!( + json, + serde_json::Value::String("D7ALZLo7BL5vM9Vb4vAqvqwX9fpQ4wKRiy".to_owned()) + ); + let into: Address = serde_json::from_value::>(json).unwrap().assume_checked(); + assert_eq!(addr.to_string(), into.to_string()); + assert_eq!( + into.script_pubkey(), + ScriptBuf::from_hex("76a914162c5ea71c0b23f5b9022ef047c4a86470a5b07088ac").unwrap() + ); + + let addr = + Address::from_str("9tTWgUQoVtNuogNtsZWJ3qmE7dkrPTrVAj").unwrap().assume_checked(); + let json = serde_json::to_value(&addr).unwrap(); + assert_eq!( + json, + serde_json::Value::String("9tTWgUQoVtNuogNtsZWJ3qmE7dkrPTrVAj".to_owned()) + ); + let into: Address = serde_json::from_value::>(json).unwrap().assume_checked(); + assert_eq!(addr.to_string(), into.to_string()); + assert_eq!( + into.script_pubkey(), + ScriptBuf::from_hex("a914162c5ea71c0b23f5b9022ef047c4a86470a5b07087").unwrap() + ); + } + + #[test] + fn test_is_related_to_pubkey_p2pkh() { + let address_string = "neRuCZsfnZaJN8FmxxVZDSZbcGATnzGByf"; + let address = Address::from_str(address_string) + .expect("address") + .require_network(Testnet) + .expect("testnet"); + + let pubkey_string = "03df154ebfcf29d29cc10d5c2565018bce2d9edbab267c31d2caf44a63056cf99f"; + let pubkey = PublicKey::from_str(pubkey_string).expect("pubkey"); + + let result = address.is_related_to_pubkey(&pubkey); + assert!(result); + + let unused_pubkey = PublicKey::from_str( + "02ba604e6ad9d3864eda8dc41c62668514ef7d5417d3b6db46e45cc4533bff001c", + ) + .expect("pubkey"); + assert!(!address.is_related_to_pubkey(&unused_pubkey)) + } + + #[test] + fn test_is_related_to_pubkey_p2pkh_uncompressed_key() { + let address_string = "DUSamFaUtRQ78DVidoeY3J8keYkQXdinrt"; + let address = Address::from_str(address_string) + .expect("address") + .require_network(Dogecoin) + .expect("mainnet"); + + let pubkey_string = "048d5141948c1702e8c95f438815794b87f706a8d4cd2bffad1dc1570971032c9b6042a0431ded2478b5c9cf2d81c124a5e57347a3c63ef0e7716cf54d613ba183"; + let pubkey = PublicKey::from_str(pubkey_string).expect("pubkey"); + + let result = address.is_related_to_pubkey(&pubkey); + assert!(result); + + let unused_pubkey = PublicKey::from_str( + "02ba604e6ad9d3864eda8dc41c62668514ef7d5417d3b6db46e45cc4533bff001c", + ) + .expect("pubkey"); + assert!(!address.is_related_to_pubkey(&unused_pubkey)) + } +} diff --git a/bitcoin/src/dogecoin/constants.rs b/bitcoin/src/dogecoin/constants.rs index 0897189dc..17649be4e 100644 --- a/bitcoin/src/dogecoin/constants.rs +++ b/bitcoin/src/dogecoin/constants.rs @@ -18,6 +18,13 @@ use crate::{ TxMerkleNode, TxOut, Witness, }; +pub(crate) const PUBKEY_ADDRESS_PREFIX_MAINNET : u8 = 0x1e; +pub(crate) const PUBKEY_ADDRESS_PREFIX_TESTNET : u8 = 0x71; +pub(crate) const PUBKEY_ADDRESS_PREFIX_REGTEST : u8 = 0x6f; +pub(crate) const SCRIPT_ADDRESS_PREFIX_MAINNET : u8 = 0x16; +pub(crate) const SCRIPT_ADDRESS_PREFIX_TESTNET : u8 = 0xc4; +pub(crate) const SCRIPT_ADDRESS_PREFIX_REGTEST : u8 = 0xc4; + // This is the 65 byte (uncompressed) pubkey used as the one-and-only output of the genesis transaction. // // ref: https://github.com/dogecoin/dogecoin/blob/7237da74b8c356568644cbe4fba19d994704355b/src/chainparams.cpp#L55 diff --git a/bitcoin/src/dogecoin/mod.rs b/bitcoin/src/dogecoin/mod.rs index aab03be93..03f46eede 100644 --- a/bitcoin/src/dogecoin/mod.rs +++ b/bitcoin/src/dogecoin/mod.rs @@ -5,16 +5,19 @@ //! This module provides support for de/serialization, parsing and execution on data structures and //! network messages related to Dogecoin. +pub mod address; pub mod constants; pub mod params; +pub use address::*; + use crate::block::{Header, TxMerkleNode}; use crate::consensus::{encode, Decodable, Encodable}; -use crate::params::Params as BitcoinParams; use crate::dogecoin::params::Params; use crate::internal_macros::impl_consensus_encoding; use crate::io::{Read, Write}; use crate::p2p::Magic; +use crate::params::Params as BitcoinParams; use crate::prelude::*; use crate::{io, BlockHash, Transaction}; use core::fmt; From 8cd380f8a3806fc7f0fa3c30883d77f0546a60a5 Mon Sep 17 00:00:00 2001 From: Paul Liu Date: Fri, 13 Jun 2025 15:39:01 +0800 Subject: [PATCH 43/53] feat: Support dogecoin in p2p messages (#6) Support dogecoin in p2p messages by making NetworkMessage type parameterized by Block type. Also add `with_version()` methods to a few types to allow changing protocol version number. --- bitcoin/examples/handshake.rs | 19 +++++++----- bitcoin/src/p2p/message.rs | 35 +++++++++++++--------- bitcoin/src/p2p/message_blockdata.rs | 8 +++++ bitcoin/src/p2p/message_network.rs | 4 +++ fuzz/fuzz_targets/bitcoin/deser_net_msg.rs | 2 +- 5 files changed, 45 insertions(+), 23 deletions(-) diff --git a/bitcoin/examples/handshake.rs b/bitcoin/examples/handshake.rs index d77828bb3..4106cbf16 100644 --- a/bitcoin/examples/handshake.rs +++ b/bitcoin/examples/handshake.rs @@ -9,6 +9,9 @@ use bitcoin::consensus::{encode, Decodable}; use bitcoin::p2p::{self, address, message, message_network}; use bitcoin::secp256k1::rand::Rng; +type NetworkMessage = message::NetworkMessage; +type RawNetworkMessage = message::RawNetworkMessage; + fn main() { // This example establishes a connection to a Bitcoin node, sends the initial // "version" message, waits for the reply, and finally closes the connection. @@ -28,7 +31,7 @@ fn main() { let version_message = build_version_message(address); let first_message = - message::RawNetworkMessage::new(bitcoin::Network::Bitcoin.magic(), version_message); + RawNetworkMessage::new(bitcoin::Network::Bitcoin.magic(), version_message); if let Ok(mut stream) = TcpStream::connect(address) { // Send the message @@ -40,20 +43,20 @@ fn main() { let mut stream_reader = BufReader::new(read_stream); loop { // Loop an retrieve new messages - let reply = message::RawNetworkMessage::consensus_decode(&mut stream_reader).unwrap(); + let reply = RawNetworkMessage::consensus_decode(&mut stream_reader).unwrap(); match reply.payload() { - message::NetworkMessage::Version(_) => { + NetworkMessage::Version(_) => { println!("Received version message: {:?}", reply.payload()); - let second_message = message::RawNetworkMessage::new( + let second_message = RawNetworkMessage::new( bitcoin::Network::Bitcoin.magic(), - message::NetworkMessage::Verack, + NetworkMessage::Verack, ); let _ = stream.write_all(encode::serialize(&second_message).as_slice()); println!("Sent verack message"); } - message::NetworkMessage::Verack => { + NetworkMessage::Verack => { println!("Received verack message: {:?}", reply.payload()); break; } @@ -69,7 +72,7 @@ fn main() { } } -fn build_version_message(address: SocketAddr) -> message::NetworkMessage { +fn build_version_message(address: SocketAddr) -> NetworkMessage { // Building version message, see https://en.bitcoin.it/wiki/Protocol_documentation#version let my_address = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 0); @@ -95,7 +98,7 @@ fn build_version_message(address: SocketAddr) -> message::NetworkMessage { let start_height: i32 = 0; // Construct the message - message::NetworkMessage::Version(message_network::VersionMessage::new( + NetworkMessage::Version(message_network::VersionMessage::new( services, timestamp as i64, addr_recv, diff --git a/bitcoin/src/p2p/message.rs b/bitcoin/src/p2p/message.rs index 11e10cafd..80d655921 100644 --- a/bitcoin/src/p2p/message.rs +++ b/bitcoin/src/p2p/message.rs @@ -150,9 +150,9 @@ impl std::error::Error for CommandStringError { /// A Network message #[derive(Clone, Debug, PartialEq, Eq)] -pub struct RawNetworkMessage { +pub struct RawNetworkMessage { magic: Magic, - payload: NetworkMessage, + payload: NetworkMessage, payload_len: u32, checksum: [u8; 4], } @@ -160,7 +160,7 @@ pub struct RawNetworkMessage { /// A Network message payload. Proper documentation is available on at /// [Bitcoin Wiki: Protocol Specification](https://en.bitcoin.it/wiki/Protocol_specification) #[derive(Clone, PartialEq, Eq, Debug)] -pub enum NetworkMessage { +pub enum NetworkMessage { /// `version` Version(message_network::VersionMessage), /// `verack` @@ -182,7 +182,7 @@ pub enum NetworkMessage { /// tx Tx(transaction::Transaction), /// `block` - Block(block::Block), + Block(Block), /// `headers` Headers(Vec), /// `sendheaders` @@ -243,7 +243,7 @@ pub enum NetworkMessage { }, } -impl NetworkMessage { +impl NetworkMessage { /// Return the message command as a static string reference. /// /// This returns `"unknown"` for [NetworkMessage::Unknown], @@ -300,9 +300,9 @@ impl NetworkMessage { } } -impl RawNetworkMessage { +impl RawNetworkMessage { /// Creates a [RawNetworkMessage] - pub fn new(magic: Magic, payload: NetworkMessage) -> Self { + pub fn new(magic: Magic, payload: NetworkMessage) -> Self { let mut engine = sha256d::Hash::engine(); let payload_len = payload.consensus_encode(&mut engine).expect("engine doesn't error"); let payload_len = u32::try_from(payload_len).expect("network message use u32 as length"); @@ -312,12 +312,14 @@ impl RawNetworkMessage { } /// Consumes the [RawNetworkMessage] instance and returns the inner payload. - pub fn into_payload(self) -> NetworkMessage { + pub fn into_payload(self) -> NetworkMessage { self.payload } /// The actual message data - pub fn payload(&self) -> &NetworkMessage { &self.payload } + pub fn payload(&self) -> &NetworkMessage { + &self.payload + } /// Magic bytes to identify the network these messages are meant for pub fn magic(&self) -> &Magic { &self.magic } @@ -348,7 +350,7 @@ impl<'a> Encodable for HeaderSerializationWrapper<'a> { } } -impl Encodable for NetworkMessage { +impl Encodable for NetworkMessage { fn consensus_encode(&self, writer: &mut W) -> Result { match self { NetworkMessage::Version(ref dat) => dat.consensus_encode(writer), @@ -393,7 +395,7 @@ impl Encodable for NetworkMessage { } } -impl Encodable for RawNetworkMessage { +impl Encodable for RawNetworkMessage { fn consensus_encode(&self, w: &mut W) -> Result { let mut len = 0; len += self.magic.consensus_encode(w)?; @@ -433,7 +435,7 @@ impl Decodable for HeaderDeserializationWrapper { } } -impl Decodable for RawNetworkMessage { +impl Decodable for RawNetworkMessage { fn consensus_decode_from_finite_reader( r: &mut R, ) -> Result { @@ -547,7 +549,7 @@ mod test { use hex::test_hex_unwrap as hex; use super::message_network::{Reject, RejectReason, VersionMessage}; - use super::*; + use super::{block, AddrV2Message, Address, CommandString, Magic, MerkleBlock}; use crate::bip152::BlockTransactionsRequest; use crate::blockdata::block::Block; use crate::blockdata::script::ScriptBuf; @@ -562,7 +564,12 @@ mod test { }; use crate::p2p::ServiceFlags; - fn hash(slice: [u8; 32]) -> Hash { Hash::from_slice(&slice).unwrap() } + type NetworkMessage = super::NetworkMessage; + type RawNetworkMessage = super::RawNetworkMessage; + + fn hash(slice: [u8; 32]) -> Hash { + Hash::from_slice(&slice).unwrap() + } #[test] fn full_round_ser_der_raw_network_message_test() { diff --git a/bitcoin/src/p2p/message_blockdata.rs b/bitcoin/src/p2p/message_blockdata.rs index 83c1d153c..db74f397f 100644 --- a/bitcoin/src/p2p/message_blockdata.rs +++ b/bitcoin/src/p2p/message_blockdata.rs @@ -130,6 +130,10 @@ impl GetBlocksMessage { pub fn new(locator_hashes: Vec, stop_hash: BlockHash) -> GetBlocksMessage { GetBlocksMessage { version: p2p::PROTOCOL_VERSION, locator_hashes, stop_hash } } + /// Set the version number. + pub fn with_version(self, version: u32) -> Self { + Self { version, ..self } + } } impl_consensus_encoding!(GetBlocksMessage, version, locator_hashes, stop_hash); @@ -139,6 +143,10 @@ impl GetHeadersMessage { pub fn new(locator_hashes: Vec, stop_hash: BlockHash) -> GetHeadersMessage { GetHeadersMessage { version: p2p::PROTOCOL_VERSION, locator_hashes, stop_hash } } + /// Set the version number. + pub fn with_version(self, version: u32) -> Self { + Self { version, ..self } + } } impl_consensus_encoding!(GetHeadersMessage, version, locator_hashes, stop_hash); diff --git a/bitcoin/src/p2p/message_network.rs b/bitcoin/src/p2p/message_network.rs index a3dbc409d..1cadc8347 100644 --- a/bitcoin/src/p2p/message_network.rs +++ b/bitcoin/src/p2p/message_network.rs @@ -73,6 +73,10 @@ impl VersionMessage { relay: false, } } + /// Set the version number. + pub fn with_version(self, version: u32) -> Self { + Self { version, ..self } + } } impl_consensus_encoding!( diff --git a/fuzz/fuzz_targets/bitcoin/deser_net_msg.rs b/fuzz/fuzz_targets/bitcoin/deser_net_msg.rs index 906bcee9f..f22b293ab 100644 --- a/fuzz/fuzz_targets/bitcoin/deser_net_msg.rs +++ b/fuzz/fuzz_targets/bitcoin/deser_net_msg.rs @@ -1,7 +1,7 @@ use honggfuzz::fuzz; fn do_test(data: &[u8]) { - let _: Result = + let _: Result, _> = bitcoin::consensus::encode::deserialize(data); } From 90ac9b9769ed51b77fa924be3c64a502e370da3b Mon Sep 17 00:00:00 2001 From: mducroux Date: Tue, 15 Jul 2025 10:13:22 +0200 Subject: [PATCH 44/53] feat(pow): add difficulty calculation for pre-digishield blocks (5,001-144,999) (#9) [XC-431](https://dfinity.atlassian.net/browse/XC-431?atlOrigin=eyJpIjoiNGZlYzIwYTg0NmE2NGNhOWE2NmUxZTBiNTdjNzE2NGIiLCJwIjoiaiJ9): This PR adds the correct difficulty calculation for pre-digishield blocks (i.e. blocks 5,001-144,999). It follows the previous PR https://github.com/dfinity/rust-dogecoin/pull/8 which added the logic for block period 0-5,000. The main change is the addition of the correct `min_timespan` for a difficulty adjustment interval allowed for a given block height, as defined by [dogecoin's difficulty adjustment algorithm](https://github.com/dogecoin/dogecoin/blob/51cbc1fd5d0d045dda2ad84f53572bbf524c6a8e/src/dogecoin.cpp#L57). The relevant code can be found here: https://github.com/dfinity/rust-dogecoin/commit/09daa88aba496392b049252ff87ed1cf1545ddb4#diff-874f3e7cfb3e02a8a906534570346fd48a5a58615069afa5a60ceaf4b9542d0eR471-R478 [XC-431]: https://dfinity.atlassian.net/browse/XC-431?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ --- bitcoin/src/dogecoin/mod.rs | 34 ++++++++++++++++++++-------------- bitcoin/src/pow.rs | 21 +++++++++++++++------ 2 files changed, 35 insertions(+), 20 deletions(-) diff --git a/bitcoin/src/dogecoin/mod.rs b/bitcoin/src/dogecoin/mod.rs index 03f46eede..c1da29305 100644 --- a/bitcoin/src/dogecoin/mod.rs +++ b/bitcoin/src/dogecoin/mod.rs @@ -354,7 +354,7 @@ mod tests { let start_time: u64 = 1386325540; // Genesis block unix time let end_time: u64 = 1386475638; // Block 239 unix time let timespan = end_time - start_time; // Slower than expected (150,098 seconds diff) - let adjustment = CompactTarget::from_next_work_required_dogecoin(starting_bits, timespan, ¶ms); + let adjustment = CompactTarget::from_next_work_required_dogecoin(starting_bits, timespan, ¶ms, 240); let adjustment_bits = CompactTarget::from_consensus(0x1e0fffff); // Block 240 compact target assert_eq!(adjustment, adjustment_bits); } @@ -366,7 +366,7 @@ mod tests { let start_time: u64 = 1386475638; // Block 239 unix time let end_time: u64 = 1386475840; // Block 479 unix time let timespan = end_time - start_time; // Faster than expected (202 seconds diff) - let adjustment = CompactTarget::from_next_work_required_dogecoin(starting_bits, timespan, ¶ms); + let adjustment = CompactTarget::from_next_work_required_dogecoin(starting_bits, timespan, ¶ms, 480); let adjustment_bits = CompactTarget::from_consensus(0x1e00ffff); // Block 480 compact target assert_eq!(adjustment, adjustment_bits); } @@ -386,7 +386,7 @@ mod tests { bits: epoch_start.bits, nonce: epoch_start.nonce }; - let adjustment = CompactTarget::from_header_difficulty_adjustment_dogecoin(epoch_start, current, params); + let adjustment = CompactTarget::from_header_difficulty_adjustment_dogecoin(epoch_start, current, params, 240); let adjustment_bits = CompactTarget::from_consensus(0x1e0fffff); // Block 240 compact target assert_eq!(adjustment, adjustment_bits); } @@ -415,7 +415,7 @@ mod tests { bits: starting_bits, nonce: 0 }; - let adjustment = CompactTarget::from_header_difficulty_adjustment_dogecoin(epoch_start, current, params); + let adjustment = CompactTarget::from_header_difficulty_adjustment_dogecoin(epoch_start, current, params, 480); let adjustment_bits = CompactTarget::from_consensus(0x1e00ffff); // Block 480 compact target assert_eq!(adjustment, adjustment_bits); } @@ -423,25 +423,31 @@ mod tests { #[test] fn compact_target_from_maximum_upward_difficulty_adjustment() { let params = Params::new(Network::Dogecoin); + let heights = vec![5000, 10000, 15000]; let starting_bits = CompactTarget::from_consensus(21403001); // Arbitrary difficulty let timespan = (0.06 * params.bitcoin_params.pow_target_timespan as f64) as u64; // > 16x Faster than expected - let got = CompactTarget::from_next_work_required_dogecoin(starting_bits, timespan, params); - let want = Target::from_compact(starting_bits) - .min_transition_threshold_dogecoin(5000) - .to_compact_lossy(); - assert_eq!(got, want); + for height in heights { + let got = CompactTarget::from_next_work_required_dogecoin(starting_bits, timespan, ¶ms, height); + let want = Target::from_compact(starting_bits) + .min_transition_threshold_dogecoin(height) + .to_compact_lossy(); + assert_eq!(got, want); + } } #[test] fn compact_target_from_minimum_downward_difficulty_adjustment() { let params = Params::new(Network::Dogecoin); + let heights = vec![5000, 10000, 15000]; let starting_bits = CompactTarget::from_consensus(21403001); // Arbitrary difficulty let timespan = 5 * params.bitcoin_params.pow_target_timespan; // > 4x Slower than expected - let got = CompactTarget::from_next_work_required_dogecoin(starting_bits, timespan, ¶ms); - let want = Target::from_compact(starting_bits) - .max_transition_threshold(params) - .to_compact_lossy(); - assert_eq!(got, want); + for height in heights { + let got = CompactTarget::from_next_work_required_dogecoin(starting_bits, timespan, ¶ms, height); + let want = Target::from_compact(starting_bits) + .max_transition_threshold(¶ms) + .to_compact_lossy(); + assert_eq!(got, want); + } } #[test] diff --git a/bitcoin/src/pow.rs b/bitcoin/src/pow.rs index 7cc1cd4f8..d7bf4b6fe 100644 --- a/bitcoin/src/pow.rs +++ b/bitcoin/src/pow.rs @@ -455,9 +455,10 @@ impl CompactTarget { /// ref: /// /// Given the previous Target, represented as a [`CompactTarget`], the difficulty is adjusted - /// by taking the timespan between them, and multipling the current [`CompactTarget`] by a factor + /// by taking the timespan between them, and multiplying the current [`CompactTarget`] by a factor /// of the net timespan and expected timespan. The [`CompactTarget`] may not increase by more than - /// a factor of 4, adjust beyond the maximum threshold for the network, or decrease... TODO + /// a factor of 4, adjust beyond the maximum threshold for the network, or decrease by a factor + /// of 16, 8, or 4 depending on block height. /// /// # Returns /// @@ -465,7 +466,8 @@ impl CompactTarget { pub fn from_next_work_required_dogecoin( last: CompactTarget, timespan: u64, - params: impl AsRef + params: impl AsRef, + height: u32 ) -> CompactTarget { let params = params.as_ref(); if params.no_pow_retargeting { @@ -473,8 +475,14 @@ impl CompactTarget { } // Comments relate to the `pow.cpp` file from Core. // ref: - let min_timespan = params.pow_target_timespan >> 4; // Lines 64 - let max_timespan = params.pow_target_timespan << 2; // Lines 65 + let max_timespan = params.pow_target_timespan << 2; + let min_timespan = if height > 10000 { // Lines 57-66 + params.pow_target_timespan >> 2 + } else if height > 5000 { + params.pow_target_timespan >> 3 + } else { + params.pow_target_timespan >> 4 + }; let actual_timespan = timespan.clamp(min_timespan, max_timespan); // Lines 69-72 nModulatedTimespan let prev_target: Target = last.into(); let maximum_retarget = prev_target.max_transition_threshold(params); // bnPowLimit @@ -544,10 +552,11 @@ impl CompactTarget { last_epoch_boundary: Header, current: Header, params: impl AsRef, + height: u32 ) -> CompactTarget { let timespan = current.time - last_epoch_boundary.time; let bits = current.bits; - CompactTarget::from_next_work_required_dogecoin(bits, timespan.into(), params) + CompactTarget::from_next_work_required_dogecoin(bits, timespan.into(), params, height) } /// Creates a [`CompactTarget`] from a consensus encoded `u32`. From 6849413fac9109b895aa1e85423c9154ab0d5c4b Mon Sep 17 00:00:00 2001 From: mducroux Date: Mon, 21 Jul 2025 10:11:48 +0200 Subject: [PATCH 45/53] refactor(params): simpler and more consistent Dogecoin params (#12) Changes to Dogecoin parameters, in preparation for DigiShield difficulty adjustment PR https://github.com/dfinity/rust-dogecoin/pull/11: - replace `struct Params` encapsulating `bitcoin_params` and `dogecoin_params` into unified `Params` structure. - change `timespan` and `pow_target_timespan` types from `u64` to `i64`. The motivation is twofold: 1) [Dogecoin core](https://github.com/dogecoin/dogecoin/blob/51cbc1fd5d0d045dda2ad84f53572bbf524c6a8e/src/consensus/params.h#L67) is using `int64_t` type throughout, 2) manipulating negative `timespan` is easier and safer to handle, which is going to be the case when switching to DigiShield where difficulty adjustment interval is only 1 block. - change `pow_target_spacing` type from `u64` to `i64` for consistency with Dogecoin. --- bitcoin/src/dogecoin/constants.rs | 2 +- bitcoin/src/dogecoin/mod.rs | 48 +++++++++++----------- bitcoin/src/dogecoin/params.rs | 67 ++++++++++++++++++------------- bitcoin/src/pow.rs | 34 +++++++++++----- 4 files changed, 88 insertions(+), 63 deletions(-) diff --git a/bitcoin/src/dogecoin/constants.rs b/bitcoin/src/dogecoin/constants.rs index 17649be4e..458fdadd7 100644 --- a/bitcoin/src/dogecoin/constants.rs +++ b/bitcoin/src/dogecoin/constants.rs @@ -81,7 +81,7 @@ pub fn genesis_block(params: impl AsRef) -> Block { let hash: sha256d::Hash = txdata[0].compute_txid().into(); let merkle_root: TxMerkleNode = hash.into(); - match params.dogecoin_params.network { + match params.network { Network::Dogecoin => Block { header: block::Header { version: block::Version::ONE, diff --git a/bitcoin/src/dogecoin/mod.rs b/bitcoin/src/dogecoin/mod.rs index c1da29305..a71415f30 100644 --- a/bitcoin/src/dogecoin/mod.rs +++ b/bitcoin/src/dogecoin/mod.rs @@ -17,7 +17,6 @@ use crate::dogecoin::params::Params; use crate::internal_macros::impl_consensus_encoding; use crate::io::{Read, Write}; use crate::p2p::Magic; -use crate::params::Params as BitcoinParams; use crate::prelude::*; use crate::{io, BlockHash, Transaction}; use core::fmt; @@ -178,12 +177,6 @@ impl Network { } } -impl AsRef for Network { - fn as_ref(&self) -> &BitcoinParams { - &Self::params(*self).bitcoin_params - } -} - impl AsRef for Network { fn as_ref(&self) -> &Params { self.params() @@ -337,7 +330,7 @@ mod tests { assert_eq!(serialize(&header.block_hash_with_scrypt()), test.output); } } - + #[test] fn max_target_from_compact() { // The highest possible target in Dogecoin is defined as 0x1e0fffff @@ -348,25 +341,27 @@ mod tests { } #[test] - fn compact_target_from_upwards_difficulty_adjustment() { + fn compact_target_from_downwards_difficulty_adjustment() { + let height = 240; let params = Params::new(Network::Dogecoin); let starting_bits = CompactTarget::from_consensus(0x1e0ffff0); // Genesis compact target on Mainnet - let start_time: u64 = 1386325540; // Genesis block unix time - let end_time: u64 = 1386475638; // Block 239 unix time + let start_time: i64 = 1386325540; // Genesis block unix time + let end_time: i64 = 1386475638; // Block 239 unix time let timespan = end_time - start_time; // Slower than expected (150,098 seconds diff) - let adjustment = CompactTarget::from_next_work_required_dogecoin(starting_bits, timespan, ¶ms, 240); + let adjustment = CompactTarget::from_next_work_required_dogecoin(starting_bits, timespan, ¶ms, height); let adjustment_bits = CompactTarget::from_consensus(0x1e0fffff); // Block 240 compact target assert_eq!(adjustment, adjustment_bits); } #[test] - fn compact_target_from_downwards_difficulty_adjustment() { + fn compact_target_from_upwards_difficulty_adjustment() { + let height = 480; let params = Params::new(Network::Dogecoin); let starting_bits = CompactTarget::from_consensus(0x1e0fffff); // Block 240 compact target - let start_time: u64 = 1386475638; // Block 239 unix time - let end_time: u64 = 1386475840; // Block 479 unix time + let start_time: i64 = 1386475638; // Block 239 unix time + let end_time: i64 = 1386475840; // Block 479 unix time let timespan = end_time - start_time; // Faster than expected (202 seconds diff) - let adjustment = CompactTarget::from_next_work_required_dogecoin(starting_bits, timespan, ¶ms, 480); + let adjustment = CompactTarget::from_next_work_required_dogecoin(starting_bits, timespan, ¶ms, height); let adjustment_bits = CompactTarget::from_consensus(0x1e00ffff); // Block 480 compact target assert_eq!(adjustment, adjustment_bits); } @@ -375,6 +370,8 @@ mod tests { fn compact_target_from_downwards_difficulty_adjustment_using_headers() { use crate::{block::Version, dogecoin::constants::genesis_block, TxMerkleNode}; use hashes::Hash; + + let height = 240; let params = Params::new(Network::Dogecoin); let epoch_start = genesis_block(¶ms).header; // Block 239, the only information used are `bits` and `time` @@ -386,7 +383,7 @@ mod tests { bits: epoch_start.bits, nonce: epoch_start.nonce }; - let adjustment = CompactTarget::from_header_difficulty_adjustment_dogecoin(epoch_start, current, params, 240); + let adjustment = CompactTarget::from_header_difficulty_adjustment_dogecoin(epoch_start, current, params, height); let adjustment_bits = CompactTarget::from_consensus(0x1e0fffff); // Block 240 compact target assert_eq!(adjustment, adjustment_bits); } @@ -395,6 +392,8 @@ mod tests { fn compact_target_from_upwards_difficulty_adjustment_using_headers() { use crate::{block::Version, TxMerkleNode}; use hashes::Hash; + + let height = 480; let params = Params::new(Network::Dogecoin); let starting_bits = CompactTarget::from_consensus(0x1e0fffff); // Block 479 compact target // Block 239, the only information used is `time` @@ -415,7 +414,7 @@ mod tests { bits: starting_bits, nonce: 0 }; - let adjustment = CompactTarget::from_header_difficulty_adjustment_dogecoin(epoch_start, current, params, 480); + let adjustment = CompactTarget::from_header_difficulty_adjustment_dogecoin(epoch_start, current, params, height); let adjustment_bits = CompactTarget::from_consensus(0x1e00ffff); // Block 480 compact target assert_eq!(adjustment, adjustment_bits); } @@ -425,7 +424,7 @@ mod tests { let params = Params::new(Network::Dogecoin); let heights = vec![5000, 10000, 15000]; let starting_bits = CompactTarget::from_consensus(21403001); // Arbitrary difficulty - let timespan = (0.06 * params.bitcoin_params.pow_target_timespan as f64) as u64; // > 16x Faster than expected + let timespan = (0.06 * params.pow_target_timespan as f64) as i64; // > 16x Faster than expected for height in heights { let got = CompactTarget::from_next_work_required_dogecoin(starting_bits, timespan, ¶ms, height); let want = Target::from_compact(starting_bits) @@ -440,11 +439,11 @@ mod tests { let params = Params::new(Network::Dogecoin); let heights = vec![5000, 10000, 15000]; let starting_bits = CompactTarget::from_consensus(21403001); // Arbitrary difficulty - let timespan = 5 * params.bitcoin_params.pow_target_timespan; // > 4x Slower than expected + let timespan = 5 * params.pow_target_timespan; // > 4x Slower than expected for height in heights { let got = CompactTarget::from_next_work_required_dogecoin(starting_bits, timespan, ¶ms, height); let want = Target::from_compact(starting_bits) - .max_transition_threshold(¶ms) + .max_transition_threshold_dogecoin(¶ms) .to_compact_lossy(); assert_eq!(got, want); } @@ -452,11 +451,12 @@ mod tests { #[test] fn compact_target_from_adjustment_is_max_target() { + let height = 480; let params = Params::new(Network::Dogecoin); let starting_bits = CompactTarget::from_consensus(0x1e0fffff); // Block 240 compact target (max target) - let timespan = 5 * params.bitcoin_params.pow_target_timespan; // > 4x Slower than expected - let got = CompactTarget::from_next_work_required(starting_bits, timespan, ¶ms); - let want = params.bitcoin_params.max_attainable_target.to_compact_lossy(); + let timespan = 4 * params.pow_target_timespan; // 4x Slower than expected + let got = CompactTarget::from_next_work_required_dogecoin(starting_bits, timespan, ¶ms, height); + let want = params.max_attainable_target.to_compact_lossy(); assert_eq!(got, want); } diff --git a/bitcoin/src/dogecoin/params.rs b/bitcoin/src/dogecoin/params.rs index 78e9ab044..fc71e9fd1 100644 --- a/bitcoin/src/dogecoin/params.rs +++ b/bitcoin/src/dogecoin/params.rs @@ -7,25 +7,49 @@ //! use crate::dogecoin::Network; -use crate::network::Network as BitcoinNetwork; -use crate::params::Params as BitcoinParams; use crate::Target; /// Parameters that influence chain consensus. -#[derive(Debug, Clone)] -pub struct Params { - /// Parameters inherited from Bitcoin, reused for Dogecoin consensus. - pub bitcoin_params: BitcoinParams, - /// Parameters that are not inherited from Bitcoin. - pub dogecoin_params: DogecoinParams, -} - -/// Dogecoin-specific consensus parameters. #[non_exhaustive] #[derive(Debug, Clone)] -pub struct DogecoinParams { +pub struct Params { /// Network for which parameters are valid. pub network: Network, + /// Time when BIP16 becomes active. + pub bip16_time: u32, + /// Block height at which BIP34 becomes active. + pub bip34_height: u32, + /// Block height at which BIP65 becomes active. + pub bip65_height: u32, + /// Block height at which BIP66 becomes active. + pub bip66_height: u32, + /// Minimum blocks including miner confirmation. + pub rule_change_activation_threshold: u32, + /// Number of blocks with the same set of rules. + pub miner_confirmation_window: u32, + /// Proof of work limit value. It contains the lowest possible difficulty. + #[deprecated(since = "0.32.0", note = "field renamed to max_attainable_target")] + pub pow_limit: Target, + /// The maximum **attainable** target value for these params. + /// + /// Not all target values are attainable because consensus code uses the compact format to + /// represent targets (see [`crate::CompactTarget`]). + /// + /// Note that this value differs from Dogecoin Core's powLimit field in that this value is + /// attainable, but Dogecoin Core's is not. Specifically, because targets in Bitcoin are always + /// rounded to the nearest float expressible in "compact form", not all targets are attainable. + /// Still, this should not affect consensus as the only place where the non-compact form of + /// this is used in Dogecoin Core's consensus algorithm is in comparison and there are no + /// compact-expressible values between Dogecoin Core's and the limit expressed here. + pub max_attainable_target: Target, + /// Expected amount of time to mine one block. + pub pow_target_spacing: i64, + /// Difficulty recalculation interval. + pub pow_target_timespan: i64, + /// Determines whether minimal difficulty may be used for blocks or not. + pub allow_min_difficulty_blocks: bool, + /// Determines whether retargeting is disabled for this network or not. + pub no_pow_retargeting: bool, } /// The mainnet parameters. @@ -47,8 +71,7 @@ impl Params { /// The mainnet parameters. pub const MAINNET: Params = Params { - bitcoin_params: BitcoinParams { - network: BitcoinNetwork::Bitcoin, + network: Network::Dogecoin, bip16_time: 1333238400, // Apr 1 2012 bip34_height: 1034383, // 80d1364201e5df97e696c03bdd24dc885e8617b9de51e453c10a4f629b1e797a bip65_height: 3464751, // 34cd2cbba4ba366f47e5aa0db5f02c19eba2adf679ceb6653ac003bdc9a0ef1f @@ -61,14 +84,11 @@ impl Params { pow_target_timespan: 4 * 60 * 60, // pre-digishield: 4 hours allow_min_difficulty_blocks: false, no_pow_retargeting: false, - }, - dogecoin_params: DogecoinParams { network: Network::Dogecoin }, }; /// The Dogecoin testnet parameters. pub const TESTNET: Params = Params { - bitcoin_params: BitcoinParams { - network: BitcoinNetwork::Testnet, + network: Network::Testnet, bip16_time: 1333238400, // Apr 1 2012 bip34_height: 708658, // 21b8b97dcdb94caa67c7f8f6dbf22e61e0cfe0e46e1fff3528b22864659e9b38 bip65_height: 1854705, // 955bd496d23790aba1ecfacb722b089a6ae7ddabaedf7d8fb0878f48308a71f9 @@ -81,14 +101,11 @@ impl Params { pow_target_timespan: 4 * 60 * 60, // pre-digishield: 4 hours allow_min_difficulty_blocks: true, no_pow_retargeting: false, - }, - dogecoin_params: DogecoinParams { network: Network::Testnet }, }; /// The Dogecoin regtest parameters. pub const REGTEST: Params = Params { - bitcoin_params: BitcoinParams { - network: BitcoinNetwork::Regtest, + network: Network::Regtest, bip16_time: 1333238400, // Apr 1 2012 bip34_height: 100000000, // not activated on regtest bip65_height: 1351, @@ -101,8 +118,6 @@ impl Params { pow_target_timespan: 4 * 60 * 60, // pre-digishield: 4 hours allow_min_difficulty_blocks: true, no_pow_retargeting: true, - }, - dogecoin_params: DogecoinParams { network: Network::Regtest }, }; /// Creates parameters set for the given network. @@ -115,10 +130,6 @@ impl Params { } } -impl AsRef for Params { - fn as_ref(&self) -> &BitcoinParams { &self.bitcoin_params } -} - impl AsRef for Params { fn as_ref(&self) -> &Params { self } } diff --git a/bitcoin/src/pow.rs b/bitcoin/src/pow.rs index d7bf4b6fe..18bb2f83d 100644 --- a/bitcoin/src/pow.rs +++ b/bitcoin/src/pow.rs @@ -19,6 +19,7 @@ use crate::block::Header; use crate::blockdata::block::BlockHash; use crate::consensus::encode::{self, Decodable, Encodable}; use crate::consensus::Params; +use crate::dogecoin::params::Params as DogecoinParams; use crate::error::{ContainsPrefixError, MissingPrefixError, ParseIntError, PrefixedHexError, UnprefixedHexError}; /// Implement traits and methods shared by `Target` and `Work`. @@ -306,7 +307,7 @@ impl Target { } /// Computes the minimum valid [`Target`] threshold allowed for a block in which a difficulty - /// adjustment occurs. + /// adjustment occurs for Bitcoin. /// /// The difficulty can only decrease or increase by a factor of 4 max on each difficulty /// adjustment period. @@ -317,7 +318,7 @@ impl Target { pub fn min_transition_threshold(&self) -> Self { Self(self.0 >> 2) } /// Computes the minimum valid [`Target`] threshold allowed for a block in which a difficulty - /// adjustment occurs. + /// adjustment occurs for Dogecoin. /// /// The difficulty can only decrease by a factor of 4, 8, or 16 max on each difficulty /// adjustment period, depending on the height. @@ -338,7 +339,7 @@ impl Target { } /// Computes the maximum valid [`Target`] threshold allowed for a block in which a difficulty - /// adjustment occurs. + /// adjustment occurs for Bitcoin. /// /// The difficulty can only decrease or increase by a factor of 4 max on each difficulty /// adjustment period. @@ -350,6 +351,19 @@ impl Target { cmp::min(self.max_transition_threshold_unchecked(), max_attainable) } + /// Computes the maximum valid [`Target`] threshold allowed for a block in which a difficulty + /// adjustment occurs for Dogecoin. + /// + /// The difficulty can only decrease or increase by a factor of 4 max on each difficulty + /// adjustment period. + /// + /// We also check that the calculated target is not greater than the maximum allowed target, + /// this value is network specific - hence the `params` parameter. + pub fn max_transition_threshold_dogecoin(&self, params: impl AsRef) -> Self { + let max_attainable = params.as_ref().max_attainable_target; + cmp::min(self.max_transition_threshold_unchecked(), max_attainable) + } + /// Computes the maximum valid [`Target`] threshold allowed for a block in which a difficulty /// adjustment occurs. /// @@ -465,12 +479,12 @@ impl CompactTarget { /// The expected [`CompactTarget`] recalculation. pub fn from_next_work_required_dogecoin( last: CompactTarget, - timespan: u64, - params: impl AsRef, + timespan: i64, + params: impl AsRef, height: u32 ) -> CompactTarget { let params = params.as_ref(); - if params.no_pow_retargeting { + if params.no_pow_retargeting { return last; } // Comments relate to the `pow.cpp` file from Core. @@ -485,10 +499,10 @@ impl CompactTarget { }; let actual_timespan = timespan.clamp(min_timespan, max_timespan); // Lines 69-72 nModulatedTimespan let prev_target: Target = last.into(); - let maximum_retarget = prev_target.max_transition_threshold(params); // bnPowLimit + let maximum_retarget = prev_target.max_transition_threshold_dogecoin(params); // bnPowLimit let retarget = prev_target.0; // bnNew - let retarget = retarget.mul(actual_timespan.into()); - let retarget = retarget.div(params.pow_target_timespan.into()); + let retarget = retarget.mul((actual_timespan as u64).into()); // Line 80 + let retarget = retarget.div((params.pow_target_timespan as u64).into()); // Line 81 let retarget = Target(retarget); if retarget.ge(&maximum_retarget) { return maximum_retarget.to_compact_lossy(); @@ -551,7 +565,7 @@ impl CompactTarget { pub fn from_header_difficulty_adjustment_dogecoin( last_epoch_boundary: Header, current: Header, - params: impl AsRef, + params: impl AsRef, height: u32 ) -> CompactTarget { let timespan = current.time - last_epoch_boundary.time; From 5d203f94005955ff7651e0309620af4d856f51a7 Mon Sep 17 00:00:00 2001 From: mducroux Date: Mon, 4 Aug 2025 10:05:42 +0200 Subject: [PATCH 46/53] feat(pow): add digishield difficulty adjustment algorithm (#13) [XC-434](https://dfinity.atlassian.net/browse/XC-434?atlOrigin=eyJpIjoiZjk1YjYwOGE4MDgxNDFkZjg2YTQ5NWU2NTZmMDlhMmIiLCJwIjoiaiJ9): Adds digishield difficulty adjustment algorithm. Ref: https://github.com/dogecoin/dogecoin/blob/2c513d0172e8bc86fe9a337693b26f2fdf68a013/src/dogecoin.cpp#L51 [XC-434]: https://dfinity.atlassian.net/browse/XC-434?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ --- bitcoin/src/dogecoin/mod.rs | 126 ++++++++++++++++++++++++++++++--- bitcoin/src/dogecoin/params.rs | 89 +++++++++++++++++++---- bitcoin/src/pow.rs | 67 +++++++++++------- 3 files changed, 234 insertions(+), 48 deletions(-) diff --git a/bitcoin/src/dogecoin/mod.rs b/bitcoin/src/dogecoin/mod.rs index a71415f30..5f5256d14 100644 --- a/bitcoin/src/dogecoin/mod.rs +++ b/bitcoin/src/dogecoin/mod.rs @@ -353,6 +353,19 @@ mod tests { assert_eq!(adjustment, adjustment_bits); } + #[test] + fn compact_target_from_downards_difficulty_adjustment_digishield() { + let height = 1531886; + let params = Params::new(Network::Dogecoin); + let starting_bits = CompactTarget::from_consensus(0x1b01c45a); // Block 1_531_885 compact target + let start_time: i64 = 1483302792; // Block 1_531_884 unix time + let end_time: i64 = 1483302869; // Block 1_531_885 unix time + let timespan = end_time - start_time; // Slower than expected (77 seconds diff) + let adjustment = CompactTarget::from_next_work_required_dogecoin(starting_bits, timespan, ¶ms, height); + let adjustment_bits = CompactTarget::from_consensus(0x1b01d36e); // Block 1_531_886 compact target + assert_eq!(adjustment, adjustment_bits); + } + #[test] fn compact_target_from_upwards_difficulty_adjustment() { let height = 480; @@ -366,6 +379,19 @@ mod tests { assert_eq!(adjustment, adjustment_bits); } + #[test] + fn compact_target_from_upwards_difficulty_adjustment_digishield() { + let height = 1531882; + let params = Params::new(Network::Dogecoin); + let starting_bits = CompactTarget::from_consensus(0x1b01dc29); // Block 1_531_881 compact target + let start_time: i64 = 1483302572; // Block 1_531_880 unix time + let end_time: i64 = 1483302608; // Block 1_531_881 unix time + let timespan = end_time - start_time; // Faster than expected (36 seconds diff) + let adjustment = CompactTarget::from_next_work_required_dogecoin(starting_bits, timespan, ¶ms, height); + let adjustment_bits = CompactTarget::from_consensus(0x1b01c45a); // Block 1_531_882 compact target + assert_eq!(adjustment, adjustment_bits); + } + #[test] fn compact_target_from_downwards_difficulty_adjustment_using_headers() { use crate::{block::Version, dogecoin::constants::genesis_block, TxMerkleNode}; @@ -388,6 +414,36 @@ mod tests { assert_eq!(adjustment, adjustment_bits); } + #[test] + fn compact_target_from_downwards_difficulty_adjustment_using_headers_digishield() { + use crate::{block::Version, TxMerkleNode}; + use std::str::FromStr; + + let height = 1_131_290; + let params = Params::new(Network::Dogecoin); + // Block 1_131_288, the only information used is `time` + let epoch_start = Header { + version: Version::from_consensus(6422787), + prev_blockhash: BlockHash::from_str("ac0ffad025605732b310be7edf52111fa9511ffc54f06d21aab1c50d4085b39f").expect("failed to parse block hash"), + merkle_root: TxMerkleNode::from_str("80c67973ef43f2df8a3641dac7da16ea59f55e4d77b9206c6e5cfa25d3bf094b").expect("failed to parse merkle root"), + time: 1458248044, + bits: CompactTarget::from_consensus(0x1b01e7c1), + nonce: 0 + }; + // Block 1_131_289, the only information used are `bits` and `time` + let current = Header { + version: Version::from_consensus(6422787), + prev_blockhash: BlockHash::from_str("7724f7b3f9652ebc121ce101a10bfabd6815518b2814bd16f7a2dcc13dd121ec").expect("failed to parse block hash"), + merkle_root: TxMerkleNode::from_str("33c13df68d2f74c76367659cc95436510ed5504ef3c53ae90679ec12ab4e8b81").expect("failed to parse merkle root"), + time: 1458248269, + bits: CompactTarget::from_consensus(0x1b01cf5d), + nonce: 0 + }; + let adjustment = CompactTarget::from_header_difficulty_adjustment_dogecoin(epoch_start, current, params, height); + let adjustment_bits = CompactTarget::from_consensus(0x1b0269d1); // Block 1_131_290 compact target + assert_eq!(adjustment, adjustment_bits); + } + #[test] fn compact_target_from_upwards_difficulty_adjustment_using_headers() { use crate::{block::Version, TxMerkleNode}; @@ -419,16 +475,55 @@ mod tests { assert_eq!(adjustment, adjustment_bits); } + #[test] + fn compact_target_from_upwards_difficulty_adjustment_using_headers_digishield() { + use crate::{block::Version, TxMerkleNode}; + use std::str::FromStr; + + let height = 1_131_286; + let params = Params::new(Network::Dogecoin); + // Block 1_131_284, the only information used is `time` + let epoch_start = Header { + version: Version::from_consensus(6422787), + prev_blockhash: BlockHash::from_str("a695a2cc43bd5c5f32acecada764b8764b044f067909b997d4f98a6733c3fa70").expect("failed to parse block hash"), + merkle_root: TxMerkleNode::from_str("806736d9e0cab2de97e7afc9f2031c5a0413c0bff00d82cc38fa0d568d2f7135").expect("failed to parse merkle root"), + time: 1458247987, + bits: CompactTarget::from_consensus(0x1b02f5b6), + nonce: 0 + }; + // Block 1_131_285, the only information used are `bits` and `time` + let current = Header { + version: Version::from_consensus(6422787), + prev_blockhash: BlockHash::from_str("db185a7d97060e13dd53ff759f9280d473d7bb6fccc8883fbc8f1fa1f071fc82").expect("failed to parse block hash"), + merkle_root: TxMerkleNode::from_str("20419a4d74c0284e241ca5d3c91ea2b533d8a6502e4b6e4a7f8a2fc50d42796e").expect("failed to parse merkle root"), + time: 1458247995, + bits: CompactTarget::from_consensus(0x1b029d4f), + nonce: 0 + }; + let adjustment = CompactTarget::from_header_difficulty_adjustment_dogecoin(epoch_start, current, params, height); + let adjustment_bits = CompactTarget::from_consensus(0x1b025a60); // Block 1_131_286 compact target + assert_eq!(adjustment, adjustment_bits); + } + #[test] fn compact_target_from_maximum_upward_difficulty_adjustment() { + let pre_digishield_heights = vec![5_000, 10_000, 15_000]; + let digishield_heights = vec![145_000, 1_000_000]; + let starting_bits = CompactTarget::from_consensus(0x1b025a60); // Arbitrary difficulty let params = Params::new(Network::Dogecoin); - let heights = vec![5000, 10000, 15000]; - let starting_bits = CompactTarget::from_consensus(21403001); // Arbitrary difficulty - let timespan = (0.06 * params.pow_target_timespan as f64) as i64; // > 16x Faster than expected - for height in heights { + for height in pre_digishield_heights { + let timespan = (0.06 * params.pow_target_timespan(height) as f64) as i64; // > 16x Faster than expected + let got = CompactTarget::from_next_work_required_dogecoin(starting_bits, timespan, ¶ms, height); + let want = Target::from_compact(starting_bits) + .min_transition_threshold_dogecoin(¶ms, height) + .to_compact_lossy(); + assert_eq!(got, want); + } + for height in digishield_heights { + let timespan = -params.pow_target_timespan(height); // Negative timespan let got = CompactTarget::from_next_work_required_dogecoin(starting_bits, timespan, ¶ms, height); let want = Target::from_compact(starting_bits) - .min_transition_threshold_dogecoin(height) + .min_transition_threshold_dogecoin(¶ms, height) .to_compact_lossy(); assert_eq!(got, want); } @@ -436,14 +531,23 @@ mod tests { #[test] fn compact_target_from_minimum_downward_difficulty_adjustment() { + let pre_digishield_heights = vec![5_000, 10_000, 15_000]; + let digishield_heights = vec![145_000, 1_000_000]; + let starting_bits = CompactTarget::from_consensus(0x1b02f5b6); // Arbitrary difficulty let params = Params::new(Network::Dogecoin); - let heights = vec![5000, 10000, 15000]; - let starting_bits = CompactTarget::from_consensus(21403001); // Arbitrary difficulty - let timespan = 5 * params.pow_target_timespan; // > 4x Slower than expected - for height in heights { + for height in pre_digishield_heights { + let timespan = 4 * params.pow_target_timespan(height); // 4x Slower than expected + let got = CompactTarget::from_next_work_required_dogecoin(starting_bits, timespan, ¶ms, height); + let want = Target::from_compact(starting_bits) + .max_transition_threshold_dogecoin(¶ms, height) + .to_compact_lossy(); + assert_eq!(got, want); + } + for height in digishield_heights { + let timespan = 5 * params.pow_target_timespan(height); // 5x Slower than expected let got = CompactTarget::from_next_work_required_dogecoin(starting_bits, timespan, ¶ms, height); let want = Target::from_compact(starting_bits) - .max_transition_threshold_dogecoin(¶ms) + .max_transition_threshold_dogecoin(¶ms, height) .to_compact_lossy(); assert_eq!(got, want); } @@ -454,7 +558,7 @@ mod tests { let height = 480; let params = Params::new(Network::Dogecoin); let starting_bits = CompactTarget::from_consensus(0x1e0fffff); // Block 240 compact target (max target) - let timespan = 4 * params.pow_target_timespan; // 4x Slower than expected + let timespan = 4 * params.pow_target_timespan(height); // 4x Slower than expected let got = CompactTarget::from_next_work_required_dogecoin(starting_bits, timespan, ¶ms, height); let want = params.max_attainable_target.to_compact_lossy(); assert_eq!(got, want); diff --git a/bitcoin/src/dogecoin/params.rs b/bitcoin/src/dogecoin/params.rs index fc71e9fd1..284116715 100644 --- a/bitcoin/src/dogecoin/params.rs +++ b/bitcoin/src/dogecoin/params.rs @@ -9,6 +9,11 @@ use crate::dogecoin::Network; use crate::Target; + +const ONE_SECOND: i64 = 1; +const ONE_MINUTE: i64 = 60; +const FOUR_HOURS: i64 = 4 * 60 * 60; + /// Parameters that influence chain consensus. #[non_exhaustive] #[derive(Debug, Clone)] @@ -44,12 +49,10 @@ pub struct Params { pub max_attainable_target: Target, /// Expected amount of time to mine one block. pub pow_target_spacing: i64, - /// Difficulty recalculation interval. - pub pow_target_timespan: i64, - /// Determines whether minimal difficulty may be used for blocks or not. - pub allow_min_difficulty_blocks: bool, /// Determines whether retargeting is disabled for this network or not. pub no_pow_retargeting: bool, + /// Height after which the Digishield difficulty adjustment algorithm is used. + pub digishield_activation_height: u32, } /// The mainnet parameters. @@ -70,6 +73,7 @@ impl Params { pub const DOGECOIN: Params = Params::MAINNET; /// The mainnet parameters. + /// Ref: pub const MAINNET: Params = Params { network: Network::Dogecoin, bip16_time: 1333238400, // Apr 1 2012 @@ -80,13 +84,13 @@ impl Params { miner_confirmation_window: 10080, // 60 * 24 * 7 = 10,080 blocks, or one week pow_limit: Target::MAX_ATTAINABLE_MAINNET_DOGE, max_attainable_target: Target::MAX_ATTAINABLE_MAINNET_DOGE, - pow_target_spacing: 60, // 1 minute - pow_target_timespan: 4 * 60 * 60, // pre-digishield: 4 hours - allow_min_difficulty_blocks: false, + pow_target_spacing: ONE_MINUTE, // 1 minute no_pow_retargeting: false, + digishield_activation_height: 145000, }; /// The Dogecoin testnet parameters. + /// Ref: pub const TESTNET: Params = Params { network: Network::Testnet, bip16_time: 1333238400, // Apr 1 2012 @@ -97,13 +101,13 @@ impl Params { miner_confirmation_window: 10080, // 60 * 24 * 7 = 10,080 blocks, or one week pow_limit: Target::MAX_ATTAINABLE_TESTNET_DOGE, max_attainable_target: Target::MAX_ATTAINABLE_TESTNET_DOGE, - pow_target_spacing: 60, // 1 minute - pow_target_timespan: 4 * 60 * 60, // pre-digishield: 4 hours - allow_min_difficulty_blocks: true, + pow_target_spacing: ONE_MINUTE, // 1 minute no_pow_retargeting: false, + digishield_activation_height: 145000, }; /// The Dogecoin regtest parameters. + /// Ref: pub const REGTEST: Params = Params { network: Network::Regtest, bip16_time: 1333238400, // Apr 1 2012 @@ -114,10 +118,9 @@ impl Params { miner_confirmation_window: 720, pow_limit: Target::MAX_ATTAINABLE_REGTEST_DOGE, max_attainable_target: Target::MAX_ATTAINABLE_REGTEST_DOGE, - pow_target_spacing: 1, // regtest: 1 second blocks - pow_target_timespan: 4 * 60 * 60, // pre-digishield: 4 hours - allow_min_difficulty_blocks: true, + pow_target_spacing: ONE_SECOND, // regtest: 1 second blocks no_pow_retargeting: true, + digishield_activation_height: 10, }; /// Creates parameters set for the given network. @@ -128,8 +131,68 @@ impl Params { Network::Regtest => Params::REGTEST, } } + + /// Checks if Digishield difficulty adjustment is activated at the given block height. + pub const fn is_digishield_activated(&self, height: u32) -> bool { + height >= self.digishield_activation_height + } + + /// Returns the target timespan (in seconds) used for PoW retargeting at the given block height. + pub const fn pow_target_timespan(&self, height: u32) -> i64 { + if !self.is_digishield_activated(height) { + FOUR_HOURS + } else { + match self.network { + Network::Dogecoin => ONE_MINUTE, + Network::Testnet => ONE_MINUTE, + Network::Regtest => ONE_SECOND, + } + } + } + + /// Determines whether minimal difficulty may be used for mining blocks. + pub const fn allow_min_difficulty_blocks(&self, height: u32) -> bool { + match self.network { + Network::Dogecoin => false, + Network::Testnet => match height { + 0..=144_999 => true, + 145_000..=157_499 => false, + 157_500.. => true, + } + Network::Regtest => true, + } + } } impl AsRef for Params { fn as_ref(&self) -> &Params { self } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn digishield_activation() { + let pre_digishield_heights = [5_000, 10_000, 144_999]; + let digishield_heights = [145_000, 145_001, 1_000_000]; + let params = [Params::MAINNET, Params::TESTNET]; + for param in params { + for &height in pre_digishield_heights.iter() { + assert!(!param.is_digishield_activated(height)); + } + for &height in digishield_heights.iter() { + assert!(param.is_digishield_activated(height)); + } + } + + let pre_digishield_heights_regtest = [0, 5, 9]; + let digishield_heights_regtest = [10, 11, 100]; + for &height in pre_digishield_heights_regtest.iter() { + assert!(!Params::REGTEST.is_digishield_activated(height)); + } + for &height in digishield_heights_regtest.iter() { + assert!(Params::REGTEST.is_digishield_activated(height)); + } + } +} diff --git a/bitcoin/src/pow.rs b/bitcoin/src/pow.rs index 18bb2f83d..962bb4203 100644 --- a/bitcoin/src/pow.rs +++ b/bitcoin/src/pow.rs @@ -320,21 +320,25 @@ impl Target { /// Computes the minimum valid [`Target`] threshold allowed for a block in which a difficulty /// adjustment occurs for Dogecoin. /// - /// The difficulty can only decrease by a factor of 4, 8, or 16 max on each difficulty - /// adjustment period, depending on the height. + /// Pre-Digishield: The target can only decrease by a factor of 4, 8, or 16 max in each + /// difficulty adjustment period, depending on the height. + /// + /// Digishield: The target can decrease by 25 % max of the previous target in one adjustment. /// - /// ref: + /// ref: /// /// # Returns /// /// In line with Dogecoin Core this function may return a target value of zero. - pub fn min_transition_threshold_dogecoin(&self, height: u32) -> Self { - if height > 10000 { - Self(self.0 >> 2) - } else if height > 5000 { - Self(self.0 >> 3) + pub fn min_transition_threshold_dogecoin(&self, params: impl AsRef, height: u32) -> Self { + if params.as_ref().is_digishield_activated(height) { + Self(self.0 - (self.0 >> 2)) } else { - Self(self.0 >> 4) + match height { + 0..=5_000 => Self(self.0 >> 4), + 5_001..=10_000 => Self(self.0 >> 3), + _ => Self(self.0 >> 2), + } } } @@ -354,14 +358,20 @@ impl Target { /// Computes the maximum valid [`Target`] threshold allowed for a block in which a difficulty /// adjustment occurs for Dogecoin. /// - /// The difficulty can only decrease or increase by a factor of 4 max on each difficulty + /// Pre-Digishield: The target can only increase by a factor of 4 max in each difficulty /// adjustment period. /// + /// Digishield: The target can increase by 50 % max of the previous target in one adjustment. + /// /// We also check that the calculated target is not greater than the maximum allowed target, /// this value is network specific - hence the `params` parameter. - pub fn max_transition_threshold_dogecoin(&self, params: impl AsRef) -> Self { + pub fn max_transition_threshold_dogecoin(&self, params: impl AsRef, height: u32) -> Self { let max_attainable = params.as_ref().max_attainable_target; - cmp::min(self.max_transition_threshold_unchecked(), max_attainable) + if params.as_ref().is_digishield_activated(height) { + cmp::min(Self(self.0 + (self.0 >> 1)), max_attainable) + } else { + cmp::min(self.max_transition_threshold_unchecked(), max_attainable) + } } /// Computes the maximum valid [`Target`] threshold allowed for a block in which a difficulty @@ -489,20 +499,29 @@ impl CompactTarget { } // Comments relate to the `pow.cpp` file from Core. // ref: - let max_timespan = params.pow_target_timespan << 2; - let min_timespan = if height > 10000 { // Lines 57-66 - params.pow_target_timespan >> 2 - } else if height > 5000 { - params.pow_target_timespan >> 3 - } else { - params.pow_target_timespan >> 4 - }; - let actual_timespan = timespan.clamp(min_timespan, max_timespan); // Lines 69-72 nModulatedTimespan + let retarget_timespan = params.pow_target_timespan(height); // Line 44 + let mut modulated_timespan = timespan; // Lines 45-46 + if params.is_digishield_activated(height) { // Lines 50-56 + modulated_timespan = retarget_timespan + (modulated_timespan - retarget_timespan) / 8; // Line 53 + let (min_timespan, max_timespan) = ( + retarget_timespan - (retarget_timespan >> 2), + retarget_timespan + (retarget_timespan >> 1), + ); + modulated_timespan = modulated_timespan.clamp(min_timespan, max_timespan); // Lines 69-72 + } else { // Lines 57-66 + let max_timespan = retarget_timespan << 2; + let min_timespan = match height { + 0..=5_000 => retarget_timespan >> 4, + 5_001..=10_000 => retarget_timespan >> 3, + _ => retarget_timespan >> 2, + }; + modulated_timespan = modulated_timespan.clamp(min_timespan, max_timespan); // Lines 69-72 + } let prev_target: Target = last.into(); - let maximum_retarget = prev_target.max_transition_threshold_dogecoin(params); // bnPowLimit + let maximum_retarget = prev_target.max_transition_threshold_dogecoin(params, height); // bnPowLimit let retarget = prev_target.0; // bnNew - let retarget = retarget.mul((actual_timespan as u64).into()); // Line 80 - let retarget = retarget.div((params.pow_target_timespan as u64).into()); // Line 81 + let retarget = retarget.mul((modulated_timespan as u64).into()); // Line 80 + let retarget = retarget.div((retarget_timespan as u64).into()); // Line 81 let retarget = Target(retarget); if retarget.ge(&maximum_retarget) { return maximum_retarget.to_compact_lossy(); From 363471f8924b565c3438d145dc8a9cd5b9214f8f Mon Sep 17 00:00:00 2001 From: Paul Liu Date: Mon, 4 Aug 2025 17:01:21 +0800 Subject: [PATCH 47/53] feat: pass p2p protocol version explicitly (#10) Pass protocol version explicitly when constructing network messages. Remove the previous addition of `with_version` method because it could be confusing when to use it. This is not a compatible change, but modifications required are fairly minimum. Hopefully it will not be a problem in the future. --- bitcoin/examples/handshake.rs | 1 + bitcoin/src/p2p/message.rs | 2 ++ bitcoin/src/p2p/message_blockdata.rs | 17 ++++------------- bitcoin/src/p2p/message_network.rs | 8 ++------ 4 files changed, 9 insertions(+), 19 deletions(-) diff --git a/bitcoin/examples/handshake.rs b/bitcoin/examples/handshake.rs index 4106cbf16..6c674bc05 100644 --- a/bitcoin/examples/handshake.rs +++ b/bitcoin/examples/handshake.rs @@ -99,6 +99,7 @@ fn build_version_message(address: SocketAddr) -> NetworkMessage { // Construct the message NetworkMessage::Version(message_network::VersionMessage::new( + p2p::PROTOCOL_VERSION, services, timestamp as i64, addr_recv, diff --git a/bitcoin/src/p2p/message.rs b/bitcoin/src/p2p/message.rs index 80d655921..1b559afc8 100644 --- a/bitcoin/src/p2p/message.rs +++ b/bitcoin/src/p2p/message.rs @@ -594,10 +594,12 @@ mod test { NetworkMessage::GetData(vec![Inventory::Transaction(hash([45u8; 32]).into())]), NetworkMessage::NotFound(vec![Inventory::Error]), NetworkMessage::GetBlocks(GetBlocksMessage::new( + crate::p2p::PROTOCOL_VERSION, vec![hash([1u8; 32]).into(), hash([4u8; 32]).into()], hash([5u8; 32]).into(), )), NetworkMessage::GetHeaders(GetHeadersMessage::new( + crate::p2p::PROTOCOL_VERSION, vec![hash([10u8; 32]).into(), hash([40u8; 32]).into()], hash([50u8; 32]).into(), )), diff --git a/bitcoin/src/p2p/message_blockdata.rs b/bitcoin/src/p2p/message_blockdata.rs index db74f397f..40a9d89b4 100644 --- a/bitcoin/src/p2p/message_blockdata.rs +++ b/bitcoin/src/p2p/message_blockdata.rs @@ -13,7 +13,6 @@ use crate::blockdata::block::BlockHash; use crate::blockdata::transaction::{Txid, Wtxid}; use crate::consensus::encode::{self, Decodable, Encodable}; use crate::internal_macros::impl_consensus_encoding; -use crate::p2p; /// An inventory item. #[derive(PartialEq, Eq, Clone, Debug, Copy, Hash, PartialOrd, Ord)] @@ -127,12 +126,8 @@ pub struct GetHeadersMessage { impl GetBlocksMessage { /// Construct a new `getblocks` message - pub fn new(locator_hashes: Vec, stop_hash: BlockHash) -> GetBlocksMessage { - GetBlocksMessage { version: p2p::PROTOCOL_VERSION, locator_hashes, stop_hash } - } - /// Set the version number. - pub fn with_version(self, version: u32) -> Self { - Self { version, ..self } + pub fn new(version: u32, locator_hashes: Vec, stop_hash: BlockHash) -> GetBlocksMessage { + GetBlocksMessage { version, locator_hashes, stop_hash } } } @@ -140,12 +135,8 @@ impl_consensus_encoding!(GetBlocksMessage, version, locator_hashes, stop_hash); impl GetHeadersMessage { /// Construct a new `getheaders` message - pub fn new(locator_hashes: Vec, stop_hash: BlockHash) -> GetHeadersMessage { - GetHeadersMessage { version: p2p::PROTOCOL_VERSION, locator_hashes, stop_hash } - } - /// Set the version number. - pub fn with_version(self, version: u32) -> Self { - Self { version, ..self } + pub fn new(version: u32, locator_hashes: Vec, stop_hash: BlockHash) -> GetHeadersMessage { + GetHeadersMessage { version, locator_hashes, stop_hash } } } diff --git a/bitcoin/src/p2p/message_network.rs b/bitcoin/src/p2p/message_network.rs index 1cadc8347..57575693d 100644 --- a/bitcoin/src/p2p/message_network.rs +++ b/bitcoin/src/p2p/message_network.rs @@ -11,7 +11,6 @@ use io::{Read, Write}; use crate::consensus::{encode, Decodable, Encodable, ReadExt}; use crate::internal_macros::impl_consensus_encoding; -use crate::p2p; use crate::p2p::address::Address; use crate::p2p::ServiceFlags; use crate::prelude::*; @@ -53,6 +52,7 @@ pub struct VersionMessage { impl VersionMessage { /// Constructs a new `version` message with `relay` set to false pub fn new( + version: u32, services: ServiceFlags, timestamp: i64, receiver: Address, @@ -62,7 +62,7 @@ impl VersionMessage { start_height: i32, ) -> VersionMessage { VersionMessage { - version: p2p::PROTOCOL_VERSION, + version, services, timestamp, receiver, @@ -73,10 +73,6 @@ impl VersionMessage { relay: false, } } - /// Set the version number. - pub fn with_version(self, version: u32) -> Self { - Self { version, ..self } - } } impl_consensus_encoding!( From ae548e793e545e2328a1acce290207309b6244d1 Mon Sep 17 00:00:00 2001 From: mducroux Date: Wed, 27 Aug 2025 14:05:35 +0200 Subject: [PATCH 48/53] feat: add auxpow validation (#14) [XC-435](https://dfinity.atlassian.net/browse/XC-435): Adds AuxPow validation used in merged mining. References: - Dogecoin core: https://github.com/dogecoin/dogecoin/blob/51cbc1fd5d0d045dda2ad84f53572bbf524c6a8e/src/auxpow.cpp#L81 - Merged mining specification: https://en.bitcoin.it/wiki/Merged_mining_specification [XC-435]: https://dfinity.atlassian.net/browse/XC-435?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ --- bitcoin/src/dogecoin/auxpow.rs | 1189 +++++++++++++++++++++++++++++ bitcoin/src/dogecoin/constants.rs | 9 +- bitcoin/src/dogecoin/mod.rs | 265 ++++--- bitcoin/src/dogecoin/params.rs | 22 +- bitcoin/src/pow.rs | 6 +- 5 files changed, 1394 insertions(+), 97 deletions(-) create mode 100644 bitcoin/src/dogecoin/auxpow.rs diff --git a/bitcoin/src/dogecoin/auxpow.rs b/bitcoin/src/dogecoin/auxpow.rs new file mode 100644 index 000000000..a5993fe8e --- /dev/null +++ b/bitcoin/src/dogecoin/auxpow.rs @@ -0,0 +1,1189 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Dogecoin AuxPow validation. +//! +//! This module provides functionality for validating Auxiliary Proof-of-Work (AuxPoW) +//! blocks used in Dogecoin's merged mining. + +use core::fmt; + +use hashes::Hash; + +use crate::consensus::Encodable; +use crate::internal_macros::impl_consensus_encoding; +use crate::prelude::*; +use crate::{BlockHash, Transaction, TxMerkleNode}; + +/// AuxPow version bit, see +pub const VERSION_AUXPOW: i32 = 1 << 8; +/// Merged mining header, see +pub const MERGED_MINING_HEADER: [u8; 4] = [0xfa, 0xbe, b'm', b'm']; + +/// AuxPow validation error. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum AuxPowValidationError { + /// Aux POW does not originate from a valid coinbase transaction + AuxPowNotFromCoinbase, + /// Chain ID is duplicated in both parent and current block + ParentHasSameChainId, + /// Aux POW blockchain merkle branch is too long + ChainMerkleBranchTooLong, + /// Aux POW coinbase transaction has invalid merkle proof + InvalidCoinbaseMerkleProof, + /// Aux POW coinbase transaction has no inputs + CoinbaseHasNoInputs, + /// Invalid script in coinbase transaction + InvalidAuxPowCoinbaseScript(AuxPowCoinbaseScriptValidationError), +} + +impl fmt::Display for AuxPowValidationError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + AuxPowValidationError::AuxPowNotFromCoinbase => + write!(f, "Aux POW does not originate from a valid coinbase transaction"), + AuxPowValidationError::ParentHasSameChainId => + write!(f, "Chain ID duplicated in both parent and current block"), + AuxPowValidationError::ChainMerkleBranchTooLong => + write!(f, "Aux POW blockchain merkle branch too long"), + AuxPowValidationError::InvalidCoinbaseMerkleProof => + write!(f, "Aux POW coinbase transaction has invalid merkle proof"), + AuxPowValidationError::CoinbaseHasNoInputs => + write!(f, "Aux POW coinbase transaction has no inputs"), + AuxPowValidationError::InvalidAuxPowCoinbaseScript(err) => write!(f, "{}", err), + } + } +} + +/// AuxPow coinbase script validation error. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum AuxPowCoinbaseScriptValidationError { + /// Missing blockchain merkle root in the coinbase transaction + MissingMerkleRoot, + /// Multiple merged mining headers in the coinbase transaction + MultipleHeaders, + /// Merged mining header is not just before the blockchain merkle root + HeaderNotAdjacent, + /// Blockchain merkle root must start in the first 20 bytes of the coinbase transaction + LegacyRootTooFar, + /// Missing blockchain merkle tree size and nonce in the coinbase transaction + MissingMerkleSizeAndNonce, + /// Blockchain merkle branch size does not match merkle size in the coinbase transaction + MerkleSizeMismatch, + /// Blockchain index does not match expected value derived from nonce and chain ID + InvalidChainIndex, +} + +impl fmt::Display for AuxPowCoinbaseScriptValidationError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + AuxPowCoinbaseScriptValidationError::MissingMerkleRoot => + write!(f, "Aux POW missing blockchain merkle root in coinbase transaction"), + AuxPowCoinbaseScriptValidationError::MultipleHeaders => + write!(f, "Aux POW with multiple merged mining headers in coinbase transaction"), + AuxPowCoinbaseScriptValidationError::HeaderNotAdjacent => + write!(f, "Aux POW merged mining header not just before blockchain merkle root"), + AuxPowCoinbaseScriptValidationError::LegacyRootTooFar => + write!(f, "Aux POW blockchain merkle root must start in first 20 bytes of the coinbase transaction"), + AuxPowCoinbaseScriptValidationError::MissingMerkleSizeAndNonce => + write!(f, "Aux POW missing blockchain merkle tree size and nonce in coinbase transaction"), + AuxPowCoinbaseScriptValidationError::MerkleSizeMismatch => + write!(f, "Aux POW merkle blockchain branch size does not match merkle size in coinbase transaction"), + AuxPowCoinbaseScriptValidationError::InvalidChainIndex => + write!(f, "Aux POW blockchain index does not match expected value derived from nonce and chain ID"), + } + } +} + +impl From for AuxPowValidationError { + fn from(err: AuxPowCoinbaseScriptValidationError) -> Self { + AuxPowValidationError::InvalidAuxPowCoinbaseScript(err) + } +} + +/// Data for merged-mining AuxPow. +/// +/// It contains the parent block's coinbase tx that can be verified to be in the parent block. +/// The coinbase transaction's input contains the hash of the auxiliary (merged-mined) block header. +#[derive(PartialEq, Eq, Clone, Debug)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde", serde(crate = "actual_serde"))] +pub struct AuxPow { + /// The parent block's coinbase tx. + pub coinbase_tx: Transaction, + /// The parent block's hash. + pub parent_hash: BlockHash, + /// The Merkle branch linking the coinbase tx to the parent block's Merkle root. + pub coinbase_branch: Vec, + /// The index of the coinbase tx in the parent block's Merkle tree. Must be 0. + pub coinbase_index: i32, + /// The Merkle branch linking the auxiliary block header to the blockchain Merkle root present + /// in the coinbase tx. + pub blockchain_branch: Vec, + /// The index of the auxiliary block header in the Merkle tree. + pub blockchain_index: i32, + /// Parent block header on which the PoW is done. + pub parent_block_header: crate::blockdata::block::Header, +} + +impl_consensus_encoding!( + AuxPow, + coinbase_tx, + parent_hash, + coinbase_branch, + coinbase_index, + blockchain_branch, + blockchain_index, + parent_block_header +); + +impl AuxPow { + /// Helper method to produce SHA256D(left + right) - same as [`crate::merkle_tree::PartialMerkleTree::parent_hash`] + fn parent_hash(left: TxMerkleNode, right: TxMerkleNode) -> TxMerkleNode { + let mut encoder = TxMerkleNode::engine(); + left.consensus_encode(&mut encoder).expect("engines don't error"); + right.consensus_encode(&mut encoder).expect("engines don't error"); + TxMerkleNode::from_engine(encoder) + } + + /// Computes the merkle root from a child hash and its merkle branch proof. + pub fn compute_merkle_root( + hash: BlockHash, + branch: &[TxMerkleNode], + index: i32, + ) -> TxMerkleNode { + let mut result_hash = TxMerkleNode::from_byte_array(hash.to_byte_array()); + let mut index = index; + + for branch_hash in branch { + if index & 1 == 1 { + // Hash is on the right, branch element on the left + result_hash = Self::parent_hash(*branch_hash, result_hash); + } else { + // Hash is on the left, branch element on the right + result_hash = Self::parent_hash(result_hash, *branch_hash); + } + index >>= 1; + } + + result_hash + } + + /// Validates the merged mining header and merkle tree data in the coinbase script. + /// + /// The validation includes: + /// + /// 1. Merkle root: Ensures the blockchain merkle root is present + /// 2. Merged mining header: Ensures the merged mining header (0xfabe6d6d) is present + /// 3. Merged mining header uniqueness: Ensures only one merged mining header exists + /// 4. Merkle tree: Verifies that the merkle tree size matches the blockchain branch size + /// 5. Merkle tree index: Verifies the AuxPow header's index in the blockchain merkle tree + /// + /// # Arguments + /// + /// * `script` - The coinbase transaction input script containing the merged mining data + /// * `blockchain_merkle_root` - The merkle root of the headers of the merged-mined blockchains + /// * `chain_id` - Chain ID of the blockchain used for deterministic index calculation + /// + /// # Returns + /// + /// `Ok(())` if all validations pass, or an `AuxPowValidationError` describing the failure + /// + /// # Merged Mining Protocol + /// + /// The coinbase script embeds merged mining data in this format: + /// ```text + /// [...prefix] [merged_mining_header] [blockchain_merkle_root] [merkle_size] [merkle_nonce] [suffix...] + /// ``` + /// Where: + /// - `merged_mining_header`: 4-byte magic header (0xfabe6d6d) + /// - `blockchain_merkle_root`: 32-byte root of the blockchain merkle tree + /// - `merkle_size`: 4-byte little-endian merkle tree size (2^height) + /// - `merkle_nonce`: 4-byte little-endian nonce to calculate deterministic slot of headers into merkle tree + fn check_merged_mining_coinbase_script( + &self, + script: &[u8], + blockchain_merkle_root: &[u8; 32], + chain_id: i32, + ) -> Result<(), AuxPowCoinbaseScriptValidationError> { + let root_pos = Self::find_bytes(script, blockchain_merkle_root) + .ok_or(AuxPowCoinbaseScriptValidationError::MissingMerkleRoot)?; + + match Self::find_bytes(script, &MERGED_MINING_HEADER) { + Some(header_pos) => { + // Check for multiple headers + let search_start = header_pos + MERGED_MINING_HEADER.len(); + if Self::find_bytes(&script[search_start..], &MERGED_MINING_HEADER).is_some() { + return Err(AuxPowCoinbaseScriptValidationError::MultipleHeaders); + } + // Check that the header immediately precedes blockchain merkle root + if header_pos + MERGED_MINING_HEADER.len() != root_pos { + return Err(AuxPowCoinbaseScriptValidationError::HeaderNotAdjacent); + } + } + None => { + // For backward compatibility: merkle root must start early in coinbase + // 8-12 bytes are enough to encode extraNonce and nBits + if root_pos > 20 { + return Err(AuxPowCoinbaseScriptValidationError::LegacyRootTooFar); + } + } + } + + let pos_after_root = root_pos + blockchain_merkle_root.len(); + let remaining_script = &script[pos_after_root..]; + + if remaining_script.len() < 8 { + return Err(AuxPowCoinbaseScriptValidationError::MissingMerkleSizeAndNonce); + } + + let size_bytes = + [remaining_script[0], remaining_script[1], remaining_script[2], remaining_script[3]]; + let size = u32::from_le_bytes(size_bytes); + + let merkle_height = self.blockchain_branch.len(); + if size != (1u32 << merkle_height) { + return Err(AuxPowCoinbaseScriptValidationError::MerkleSizeMismatch); + } + + let nonce_bytes = + [remaining_script[4], remaining_script[5], remaining_script[6], remaining_script[7]]; + let nonce = u32::from_le_bytes(nonce_bytes); + + let expected_index = Self::get_expected_index(nonce, chain_id, merkle_height); + if self.blockchain_index != expected_index { + return Err(AuxPowCoinbaseScriptValidationError::InvalidChainIndex); + } + + Ok(()) + } + + /// Returns the byte offset of the first occurrence of `needle` in `haystack`. + /// If the `needle` is not found, returns `None`. + fn find_bytes(haystack: &[u8], needle: &[u8]) -> Option { + if needle.is_empty() { + return Some(0); + } + haystack.windows(needle.len()).position(|window| window == needle) + } + + /// Calculates the expected index of the AuxPow block header in the blockchain merkle tree. + /// + /// Given the merkle tree size, nonce, and chain ID, computes a pseudo-random slot in the blockchain + /// merkle tree. This prevents replay attacks in merged mining by assigning each merged-mined + /// block header a deterministic position in the blockchain merkle tree. + /// + /// Ref: + pub fn get_expected_index(nonce: u32, chain_id: i32, merkle_height: usize) -> i32 { + // The original C++ implementation mentions that this computation can overflow but that + // this is not an issue. In C++, unsigned integer overflows automatically wrap around. + // To replicate the wrapping behavior, we use `wrapping_mul` and `wrapping_add`. + + let mut rand = nonce; + rand = rand.wrapping_mul(1103515245).wrapping_add(12345); + rand = rand.wrapping_add(chain_id as u32); + rand = rand.wrapping_mul(1103515245).wrapping_add(12345); + + (rand % (1u32 << merkle_height)) as i32 + } + + /// Validates the AuxPow structure for merged mining. + /// + /// Ref: + /// + /// # Arguments + /// + /// * `aux_block_hash` - Hash of the AuxPow block being merged-mined + /// * `chain_id` - Chain ID of the auxiliary blockchain (e.g. 98 for Dogecoin) + /// * `strict_chain_id` - If true, enforces that parent and auxiliary chains have different chain IDs + /// + /// # Returns + /// + /// `Ok(())` if the AuxPoW is valid, or an `AuxPowValidationError` describing the validation failure + pub fn check( + &self, + aux_block_hash: BlockHash, + chain_id: i32, + strict_chain_id: bool, + ) -> Result<(), AuxPowValidationError> { + if self.coinbase_index != 0 { + return Err(AuxPowValidationError::AuxPowNotFromCoinbase); + } + + if strict_chain_id && self.parent_block_header.extract_chain_id() == chain_id { + return Err(AuxPowValidationError::ParentHasSameChainId); + } + + if self.blockchain_branch.len() > 30 { + return Err(AuxPowValidationError::ChainMerkleBranchTooLong); + } + + let blockchain_merkle_root = Self::compute_merkle_root( + aux_block_hash, + &self.blockchain_branch, + self.blockchain_index, + ); + + let mut blockchain_merkle_root_le = blockchain_merkle_root.to_byte_array(); + blockchain_merkle_root_le.reverse(); + + let coinbase_hash = self.coinbase_tx.compute_txid(); + let transactions_merkle_root = Self::compute_merkle_root( + BlockHash::from_byte_array(coinbase_hash.to_byte_array()), + &self.coinbase_branch, + self.coinbase_index, + ); + + if transactions_merkle_root != self.parent_block_header.merkle_root { + return Err(AuxPowValidationError::InvalidCoinbaseMerkleProof); + } + + if self.coinbase_tx.input.is_empty() { + return Err(AuxPowValidationError::CoinbaseHasNoInputs); + } + + let script = &self.coinbase_tx.input[0].script_sig; + + self.check_merged_mining_coinbase_script( + script.as_bytes(), + &blockchain_merkle_root_le, + chain_id, + ) + .map_err(AuxPowValidationError::InvalidAuxPowCoinbaseScript)?; + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use core::str::FromStr; + + use hashes::Hash; + + use super::*; + use crate::block::{Header as PureHeader, Version}; + use crate::consensus::encode::deserialize_hex; + use crate::{CompactTarget, ScriptBuf, TxIn, TxOut, Txid, Witness}; + + const PARENT_BLOCK_CHAIN_ID: i32 = 42; + const AUXPOW_BLOCK_CHAIN_ID: i32 = 98; // Dogecoin chain ID + const BASE_VERSION: i32 = 0x00000005; + const NONCE: u32 = 7; + const MERKLE_HEIGHT: usize = 30; + + /// Helper to create a dummy blockchain branch + fn build_blockchain_merkle_branch(merkle_height: usize) -> Vec { + (0..merkle_height).map(|i| TxMerkleNode::from_byte_array([i as u8; 32])).collect() + } + + /// Helper to create a minimal coinbase transaction with the given script + fn coinbase_from_script(script: ScriptBuf) -> Transaction { + Transaction { + version: crate::transaction::Version::ONE, + lock_time: crate::absolute::LockTime::ZERO, + input: vec![TxIn { + previous_output: crate::OutPoint::null(), + script_sig: script, + sequence: crate::Sequence::MAX, + witness: Witness::default(), + }], + output: vec![TxOut { + value: crate::Amount::from_sat(5000000000), + script_pubkey: ScriptBuf::new(), + }], + } + } + + /// Helper to create an AuxPow coinbase transaction's script + fn build_auxpow_coinbase_script( + with_header: bool, + blockchain_merkle_root: &[u8; 32], + merkle_height: usize, + nonce: u32, + ) -> ScriptBuf { + let mut data = Vec::new(); + + if with_header { + data.extend_from_slice(&MERGED_MINING_HEADER); + } + + // Reverse endianness (big-endian → little-endian) + let mut blockchain_merkle_root_le = *blockchain_merkle_root; + blockchain_merkle_root_le.reverse(); + data.extend_from_slice(&blockchain_merkle_root_le); + + let size = 1u32 << merkle_height; + data.extend_from_slice(&size.to_le_bytes()); + data.extend_from_slice(&nonce.to_le_bytes()); + + let mut script_data = vec![0x01, 0x02]; // Some prefix data + script_data.extend_from_slice(&data); + + ScriptBuf::from_bytes(script_data) + } + + /// Helper to create an AuxPow coinbase transaction + fn build_auxpow_coinbase( + with_header: bool, + blockchain_merkle_root: &[u8; 32], + merkle_height: usize, + nonce: u32, + ) -> Transaction { + let script = + build_auxpow_coinbase_script(with_header, blockchain_merkle_root, merkle_height, nonce); + + coinbase_from_script(script) + } + + /// Helper to assemble an AuxPow struct from a coinbase transaction, expected index, and blockchain branch + fn assemble_auxpow( + coinbase_tx: Transaction, + expected_index: i32, + blockchain_branch: Vec, + ) -> AuxPow { + let parent_block_header = PureHeader { + version: Version::from_consensus(BASE_VERSION | (PARENT_BLOCK_CHAIN_ID << 16)), + prev_blockhash: BlockHash::from_byte_array([0; 32]), + merkle_root: TxMerkleNode::from_byte_array(coinbase_tx.compute_txid().to_byte_array()), + time: 0, + bits: CompactTarget::from_consensus(0x1e0ffff0), + nonce: 0, + }; + + AuxPow { + coinbase_tx, + parent_hash: BlockHash::from_byte_array([2; 32]), // Dummy value (this is not checked at all) + coinbase_branch: vec![], // There is only a coinbase transaction in the block + coinbase_index: 0, + blockchain_branch, + blockchain_index: expected_index, + parent_block_header, + } + } + + /// Helper to create a valid AuxPow + fn build_auxpow( + aux_block_hash: BlockHash, + chain_id: i32, + merkle_height: usize, + nonce: u32, + with_header: bool, + ) -> AuxPow { + let expected_index = AuxPow::get_expected_index(nonce, chain_id, merkle_height); + + let blockchain_branch = build_blockchain_merkle_branch(merkle_height); + + let blockchain_merkle_root = + AuxPow::compute_merkle_root(aux_block_hash, &blockchain_branch, expected_index); + + let coinbase_tx = build_auxpow_coinbase( + with_header, + &blockchain_merkle_root.to_byte_array(), + merkle_height, + nonce, + ); + + assemble_auxpow(coinbase_tx, expected_index, blockchain_branch) + } + + #[test] + fn test_valid_auxpow_modern_format() { + let aux_block_hash = BlockHash::from_byte_array([0x12; 32]); + let chain_id = AUXPOW_BLOCK_CHAIN_ID; + let merkle_height = MERKLE_HEIGHT; + let nonce = NONCE; + + let auxpow = build_auxpow(aux_block_hash, chain_id, merkle_height, nonce, true); + + assert!(auxpow.check(aux_block_hash, chain_id, true).is_ok()); + } + + #[test] + fn test_valid_auxpow_legacy_format() { + let aux_block_hash = BlockHash::from_byte_array([0x12; 32]); + let chain_id = AUXPOW_BLOCK_CHAIN_ID; + let merkle_height = MERKLE_HEIGHT; + let nonce = NONCE; + + let auxpow = build_auxpow(aux_block_hash, chain_id, merkle_height, nonce, false); + + assert!(auxpow.check(aux_block_hash, chain_id, true).is_ok()); + } + + #[test] + fn test_auxpow_not_from_coinbase() { + let aux_block_hash = BlockHash::from_byte_array([0x12; 32]); + let chain_id = AUXPOW_BLOCK_CHAIN_ID; + let merkle_height = MERKLE_HEIGHT; + let nonce = NONCE; + + let mut auxpow = build_auxpow(aux_block_hash, chain_id, merkle_height, nonce, true); + auxpow.coinbase_index = 1; // Not coinbase + + assert_eq!( + auxpow.check(aux_block_hash, chain_id, true), + Err(AuxPowValidationError::AuxPowNotFromCoinbase) + ); + } + + #[test] + fn test_parent_has_same_chain_id() { + let aux_block_hash = BlockHash::from_byte_array([0x12; 32]); + let chain_id = AUXPOW_BLOCK_CHAIN_ID; + let merkle_height = MERKLE_HEIGHT; + let nonce = NONCE; + + let mut auxpow = build_auxpow(aux_block_hash, chain_id, merkle_height, nonce, true); + + // Set parent block to have same chain ID + auxpow.parent_block_header.version = + Version::from_consensus(BASE_VERSION | (AUXPOW_BLOCK_CHAIN_ID << 16)); + + assert_eq!( + auxpow.check(aux_block_hash, chain_id, true), + Err(AuxPowValidationError::ParentHasSameChainId) + ); + + assert!(auxpow.check(aux_block_hash, chain_id, false).is_ok()); + } + + #[test] + fn test_chain_merkle_branch_too_long() { + let aux_block_hash = BlockHash::from_byte_array([0x12; 32]); + let chain_id = AUXPOW_BLOCK_CHAIN_ID; + let merkle_height = 31; // Too long (>30) + let nonce = NONCE; + + let auxpow = build_auxpow(aux_block_hash, chain_id, merkle_height, nonce, true); + + assert_eq!( + auxpow.check(aux_block_hash, chain_id, true), + Err(AuxPowValidationError::ChainMerkleBranchTooLong) + ); + } + + #[test] + fn test_invalid_coinbase_merkle_proof() { + let aux_block_hash = BlockHash::from_byte_array([0x12; 32]); + let chain_id = AUXPOW_BLOCK_CHAIN_ID; + let merkle_height = MERKLE_HEIGHT; + let nonce = NONCE; + + let mut auxpow = build_auxpow(aux_block_hash, chain_id, merkle_height, nonce, true); + + // Tamper with parent block merkle root + auxpow.parent_block_header.merkle_root = TxMerkleNode::from_byte_array([0xff; 32]); + + assert_eq!( + auxpow.check(aux_block_hash, chain_id, true), + Err(AuxPowValidationError::InvalidCoinbaseMerkleProof) + ); + } + + #[test] + fn test_coinbase_has_no_inputs() { + let aux_block_hash = BlockHash::from_byte_array([0x12; 32]); + let chain_id = AUXPOW_BLOCK_CHAIN_ID; + let merkle_height = MERKLE_HEIGHT; + let nonce = NONCE; + + let expected_index = AuxPow::get_expected_index(nonce, chain_id, merkle_height); + + let blockchain_branch = build_blockchain_merkle_branch(merkle_height); + let blockchain_merkle_root = + AuxPow::compute_merkle_root(aux_block_hash, &blockchain_branch, expected_index); + let mut coinbase_tx = build_auxpow_coinbase( + true, + &blockchain_merkle_root.to_byte_array(), + merkle_height, + nonce, + ); + + coinbase_tx.input.clear(); // Remove inputs + + let auxpow = assemble_auxpow(coinbase_tx, expected_index, blockchain_branch); + + assert_eq!( + auxpow.check(aux_block_hash, chain_id, true), + Err(AuxPowValidationError::CoinbaseHasNoInputs) + ); + } + + #[test] + fn test_modified_aux_block_hash() { + let aux_block_hash = BlockHash::from_byte_array([0x12; 32]); + let modified_hash = BlockHash::from_byte_array([0x13; 32]); // Modified + let chain_id = AUXPOW_BLOCK_CHAIN_ID; + let merkle_height = MERKLE_HEIGHT; + let nonce = NONCE; + + let auxpow = build_auxpow(aux_block_hash, chain_id, merkle_height, nonce, true); + + assert_eq!( + auxpow.check(modified_hash, chain_id, true), + Err(AuxPowCoinbaseScriptValidationError::MissingMerkleRoot.into()) + ); + } + + #[test] + fn test_wrong_chain_id() { + let aux_block_hash = BlockHash::from_byte_array([0x12; 32]); + let chain_id = AUXPOW_BLOCK_CHAIN_ID; + let wrong_chain_id = AUXPOW_BLOCK_CHAIN_ID + 1; + let merkle_height = MERKLE_HEIGHT; + let nonce = NONCE; + + let auxpow = build_auxpow(aux_block_hash, chain_id, merkle_height, nonce, true); + + assert_eq!( + auxpow.check(aux_block_hash, wrong_chain_id, true), + Err(AuxPowCoinbaseScriptValidationError::InvalidChainIndex.into()) + ); + } + + #[test] + fn test_missing_blockchain_merkle_root() { + let aux_block_hash = BlockHash::from_byte_array([0x12; 32]); + let chain_id = AUXPOW_BLOCK_CHAIN_ID; + let merkle_height = MERKLE_HEIGHT; + let nonce = NONCE; + + let expected_index = AuxPow::get_expected_index(nonce, chain_id, merkle_height); + + let blockchain_branch = build_blockchain_merkle_branch(merkle_height); + let wrong_blockchain_merkle_root = [0; 32]; + let coinbase_tx = + build_auxpow_coinbase(true, &wrong_blockchain_merkle_root, merkle_height, nonce); + + let auxpow = assemble_auxpow(coinbase_tx, expected_index, blockchain_branch); + + assert_eq!( + auxpow.check(aux_block_hash, chain_id, true), + Err(AuxPowCoinbaseScriptValidationError::MissingMerkleRoot.into()) + ); + } + + #[test] + fn test_multiple_headers() { + let aux_block_hash = BlockHash::from_byte_array([0x12; 32]); + let chain_id = AUXPOW_BLOCK_CHAIN_ID; + let merkle_height = MERKLE_HEIGHT; + let nonce = NONCE; + + let expected_index = AuxPow::get_expected_index(nonce, chain_id, merkle_height); + let blockchain_branch = build_blockchain_merkle_branch(merkle_height); + let blockchain_merkle_root = + AuxPow::compute_merkle_root(aux_block_hash, &blockchain_branch, expected_index) + .to_byte_array(); + let wrong_blockchain_merkle_root = [0; 32]; + + // Legacy: Two blockchain merkle roots with no headers + // Correct blockchain merkle root first + let script_begin = + build_auxpow_coinbase_script(false, &blockchain_merkle_root, merkle_height, nonce); + let script_end = build_auxpow_coinbase_script( + false, + &wrong_blockchain_merkle_root, + merkle_height, + nonce, + ); + let script = + ScriptBuf::from_bytes([script_begin.into_bytes(), script_end.into_bytes()].concat()); + let coinbase_tx = coinbase_from_script(script); + let auxpow = assemble_auxpow(coinbase_tx, expected_index, blockchain_branch.clone()); + assert!(auxpow.check(aux_block_hash, chain_id, true).is_ok()); + + // Wrong blockchain merkle root first + let script_begin = build_auxpow_coinbase_script( + false, + &wrong_blockchain_merkle_root, + merkle_height, + nonce, + ); + let script_end = + build_auxpow_coinbase_script(false, &blockchain_merkle_root, merkle_height, nonce); + let script = + ScriptBuf::from_bytes([script_begin.into_bytes(), script_end.into_bytes()].concat()); + let coinbase_tx = coinbase_from_script(script); + let auxpow = assemble_auxpow(coinbase_tx, expected_index, blockchain_branch.clone()); + assert_eq!( + auxpow.check(aux_block_hash, chain_id, true), + Err(AuxPowCoinbaseScriptValidationError::LegacyRootTooFar.into()) + ); + + // Merged mining header present with wrong blockchain merkle root following it + let script_begin = + build_auxpow_coinbase_script(false, &blockchain_merkle_root, merkle_height, nonce); + let script_end = + build_auxpow_coinbase_script(true, &wrong_blockchain_merkle_root, merkle_height, nonce); + let script = + ScriptBuf::from_bytes([script_begin.into_bytes(), script_end.into_bytes()].concat()); + let coinbase_tx = coinbase_from_script(script); + let auxpow = assemble_auxpow(coinbase_tx, expected_index, blockchain_branch.clone()); + assert_eq!( + auxpow.check(aux_block_hash, chain_id, true), + Err(AuxPowCoinbaseScriptValidationError::HeaderNotAdjacent.into()) + ); + + let script_begin = + build_auxpow_coinbase_script(true, &wrong_blockchain_merkle_root, merkle_height, nonce); + let script_end = + build_auxpow_coinbase_script(false, &blockchain_merkle_root, merkle_height, nonce); + let script = + ScriptBuf::from_bytes([script_begin.into_bytes(), script_end.into_bytes()].concat()); + let coinbase_tx = coinbase_from_script(script); + let auxpow = assemble_auxpow(coinbase_tx, expected_index, blockchain_branch.clone()); + assert_eq!( + auxpow.check(aux_block_hash, chain_id, true), + Err(AuxPowCoinbaseScriptValidationError::HeaderNotAdjacent.into()) + ); + + // Multiple headers in coinbase gets rejected + let script_begin = + build_auxpow_coinbase_script(true, &wrong_blockchain_merkle_root, merkle_height, nonce); + let script_end = + build_auxpow_coinbase_script(true, &blockchain_merkle_root, merkle_height, nonce); + let script = + ScriptBuf::from_bytes([script_begin.into_bytes(), script_end.into_bytes()].concat()); + let coinbase_tx = coinbase_from_script(script); + let auxpow = assemble_auxpow(coinbase_tx, expected_index, blockchain_branch.clone()); + assert_eq!( + auxpow.check(aux_block_hash, chain_id, true), + Err(AuxPowCoinbaseScriptValidationError::MultipleHeaders.into()) + ); + + let script_begin = + build_auxpow_coinbase_script(true, &blockchain_merkle_root, merkle_height, nonce); + let script_end = + build_auxpow_coinbase_script(true, &wrong_blockchain_merkle_root, merkle_height, nonce); + let script = + ScriptBuf::from_bytes([script_begin.into_bytes(), script_end.into_bytes()].concat()); + let coinbase_tx = coinbase_from_script(script); + let auxpow = assemble_auxpow(coinbase_tx, expected_index, blockchain_branch.clone()); + assert_eq!( + auxpow.check(aux_block_hash, chain_id, true), + Err(AuxPowCoinbaseScriptValidationError::MultipleHeaders.into()) + ); + + // Correct blockchain merkle root after merged mining header is accepted + let script_begin = + build_auxpow_coinbase_script(true, &blockchain_merkle_root, merkle_height, nonce); + let script_end = build_auxpow_coinbase_script( + false, + &wrong_blockchain_merkle_root, + merkle_height, + nonce, + ); + let script = + ScriptBuf::from_bytes([script_begin.into_bytes(), script_end.into_bytes()].concat()); + let coinbase_tx = coinbase_from_script(script); + let auxpow = assemble_auxpow(coinbase_tx, expected_index, blockchain_branch.clone()); + assert!(auxpow.check(aux_block_hash, chain_id, true).is_ok()); + + let script_begin = build_auxpow_coinbase_script( + false, + &wrong_blockchain_merkle_root, + merkle_height, + nonce, + ); + let script_end = + build_auxpow_coinbase_script(true, &blockchain_merkle_root, merkle_height, nonce); + let script = + ScriptBuf::from_bytes([script_begin.into_bytes(), script_end.into_bytes()].concat()); + let coinbase_tx = coinbase_from_script(script); + let auxpow = assemble_auxpow(coinbase_tx, expected_index, blockchain_branch.clone()); + assert!(auxpow.check(aux_block_hash, chain_id, true).is_ok()); + } + + #[test] + fn test_header_not_adjacent() { + let aux_block_hash = BlockHash::from_byte_array([0x12; 32]); + let chain_id = AUXPOW_BLOCK_CHAIN_ID; + let merkle_height = MERKLE_HEIGHT; + let nonce = NONCE; + + let expected_index = AuxPow::get_expected_index(nonce, chain_id, merkle_height); + let blockchain_branch = build_blockchain_merkle_branch(merkle_height); + let blockchain_merkle_root = + AuxPow::compute_merkle_root(aux_block_hash, &blockchain_branch, expected_index); + + let mut script_data = vec![0x01, 0x02]; + script_data.extend_from_slice(&MERGED_MINING_HEADER); + script_data.push(0xff); // Extra byte between header and merkle root + let mut blockchain_merkle_root_le = blockchain_merkle_root.to_byte_array(); + blockchain_merkle_root_le.reverse(); + script_data.extend_from_slice(&blockchain_merkle_root_le); + script_data.extend_from_slice(&(1u32 << merkle_height).to_le_bytes()); + script_data.extend_from_slice(&nonce.to_le_bytes()); + + let script = ScriptBuf::from_bytes(script_data); + let coinbase_tx = coinbase_from_script(script); + + let auxpow = assemble_auxpow(coinbase_tx, expected_index, blockchain_branch); + + assert_eq!( + auxpow.check(aux_block_hash, chain_id, true), + Err(AuxPowCoinbaseScriptValidationError::HeaderNotAdjacent.into()) + ); + } + + #[test] + fn test_legacy_root_too_far() { + let aux_block_hash = BlockHash::from_byte_array([0x12; 32]); + let chain_id = AUXPOW_BLOCK_CHAIN_ID; + let merkle_height = MERKLE_HEIGHT; + let nonce = NONCE; + + // Create legacy format with merkle root too far from start + let expected_index = AuxPow::get_expected_index(nonce, chain_id, merkle_height); + let blockchain_branch = build_blockchain_merkle_branch(merkle_height); + let blockchain_merkle_root = + AuxPow::compute_merkle_root(aux_block_hash, &blockchain_branch, expected_index); + + let mut script_data = vec![0; 25]; // 25 bytes prefix (>20, too far) + let mut blockchain_merkle_root_le = blockchain_merkle_root.to_byte_array(); + blockchain_merkle_root_le.reverse(); + script_data.extend_from_slice(&blockchain_merkle_root_le); + script_data.extend_from_slice(&(1u32 << merkle_height).to_le_bytes()); + script_data.extend_from_slice(&nonce.to_le_bytes()); + + let script = ScriptBuf::from_bytes(script_data); + let coinbase_tx = coinbase_from_script(script); + + let auxpow = assemble_auxpow(coinbase_tx, expected_index, blockchain_branch); + + assert_eq!( + auxpow.check(aux_block_hash, chain_id, true), + Err(AuxPowCoinbaseScriptValidationError::LegacyRootTooFar.into()) + ); + } + + #[test] + fn test_missing_merkle_size_and_nonce() { + let aux_block_hash = BlockHash::from_byte_array([0x12; 32]); + let chain_id = AUXPOW_BLOCK_CHAIN_ID; + let merkle_height = MERKLE_HEIGHT; + let nonce = NONCE; + + let expected_index = AuxPow::get_expected_index(nonce, chain_id, merkle_height); + let blockchain_branch = build_blockchain_merkle_branch(merkle_height); + let blockchain_merkle_root = + AuxPow::compute_merkle_root(aux_block_hash, &blockchain_branch, expected_index); + + let script = build_auxpow_coinbase_script( + true, + &blockchain_merkle_root.to_byte_array(), + merkle_height, + nonce, + ); + let mut script_bytes = script.into_bytes(); + script_bytes.truncate(script_bytes.len() - 8); // Remove last 8 bytes (size + nonce) + let coinbase_tx = coinbase_from_script(ScriptBuf::from_bytes(script_bytes)); + + let auxpow = assemble_auxpow(coinbase_tx, expected_index, blockchain_branch); + + assert_eq!( + auxpow.check(aux_block_hash, chain_id, true), + Err(AuxPowCoinbaseScriptValidationError::MissingMerkleSizeAndNonce.into()) + ); + } + + #[test] + fn test_incorrect_merkle_size_in_coinbase() { + let aux_block_hash = BlockHash::from_byte_array([0x12; 32]); + let chain_id = AUXPOW_BLOCK_CHAIN_ID; + let merkle_height = MERKLE_HEIGHT; + let nonce = NONCE; + + let expected_index = AuxPow::get_expected_index(nonce, chain_id, merkle_height); + let blockchain_branch = build_blockchain_merkle_branch(merkle_height); + let blockchain_merkle_root = + AuxPow::compute_merkle_root(aux_block_hash, &blockchain_branch, expected_index); + + let script = build_auxpow_coinbase_script( + true, + &blockchain_merkle_root.to_byte_array(), + merkle_height - 1, // Incorrect merkle size + nonce, + ); + let coinbase_tx = coinbase_from_script(script); + + let auxpow = assemble_auxpow(coinbase_tx, expected_index, blockchain_branch); + + assert_eq!( + auxpow.check(aux_block_hash, chain_id, true), + Err(AuxPowCoinbaseScriptValidationError::MerkleSizeMismatch.into()) + ); + } + + #[test] + fn test_incorrect_nonce_in_coinbase() { + let aux_block_hash = BlockHash::from_byte_array([0x12; 32]); + let chain_id = AUXPOW_BLOCK_CHAIN_ID; + let merkle_height = MERKLE_HEIGHT; + let nonce = NONCE; + + let expected_index = AuxPow::get_expected_index(nonce, chain_id, merkle_height); + let blockchain_branch = build_blockchain_merkle_branch(merkle_height); + let blockchain_merkle_root = + AuxPow::compute_merkle_root(aux_block_hash, &blockchain_branch, expected_index); + + let script = build_auxpow_coinbase_script( + true, + &blockchain_merkle_root.to_byte_array(), + merkle_height, + nonce + 3, // Incorrect nonce + ); + let coinbase_tx = coinbase_from_script(script); + + let auxpow = assemble_auxpow(coinbase_tx, expected_index, blockchain_branch); + + assert_eq!( + auxpow.check(aux_block_hash, chain_id, true), + Err(AuxPowCoinbaseScriptValidationError::InvalidChainIndex.into()) + ); + } + + #[test] + fn test_invalid_chain_index() { + let aux_block_hash = BlockHash::from_byte_array([0x12; 32]); + let chain_id = AUXPOW_BLOCK_CHAIN_ID; + let merkle_height = MERKLE_HEIGHT; + let nonce = NONCE; + + let expected_index = AuxPow::get_expected_index(nonce, chain_id, merkle_height); + let wrong_index = expected_index + 1; + + let blockchain_branch = build_blockchain_merkle_branch(merkle_height); + + let blockchain_merkle_root = + AuxPow::compute_merkle_root(aux_block_hash, &blockchain_branch, wrong_index); + + let coinbase_tx = build_auxpow_coinbase( + true, + &blockchain_merkle_root.to_byte_array(), + merkle_height, + nonce, + ); + + let auxpow = assemble_auxpow(coinbase_tx, expected_index, blockchain_branch); + + assert_eq!( + auxpow.check(aux_block_hash, chain_id, true), + Err(AuxPowCoinbaseScriptValidationError::MissingMerkleRoot.into()) + ); + } + + #[test] + fn test_valid_auxpow_dogecoin_mainnet_single_merged_mined_chain() { + // AuxPow information for Dogecoin mainnet block, height 2_679_506 + let coinbase_tx = deserialize_hex::("02000000010000000000000000000000000000000000000000000000000000000000000000ffffffff4d03119d1804ec4eb05c2f4254432e434f4d2f4c5443fabe6d6d283fa35edb604a913ead7e776b534f1661723da6721131eee75a39738ed2e8f8010000000000000001000a7de000000000000000ffffffff02fef83d95000000001976a91497b22b5ff0e6fd5e06c2592bdff0bfd677009f4788ac0000000000000000266a24aa21a9ed710de897f5217b49832c28a07a7f71c924d6c502efacbd3afdfe78565c9cc0d200000000").unwrap(); + let coinbase_tx_id = + Txid::from_str("d7f98ad4f597cbd536529b35739d4ae8f13b788d0f6e9f4408c4b457410eed9c") + .unwrap(); + + assert_eq!(coinbase_tx.compute_txid(), coinbase_tx_id); + assert_eq!(coinbase_tx.input.len(), 1); + + let coinbase_script = ScriptBuf::from_hex("03119d1804ec4eb05c2f4254432e434f4d2f4c5443fabe6d6d283fa35edb604a913ead7e776b534f1661723da6721131eee75a39738ed2e8f8010000000000000001000a7de000000000000000").unwrap(); + assert_eq!(coinbase_script, coinbase_tx.input.first().unwrap().script_sig); + + // Litecoin mainnet block, height 1_613_073 + let parent_block_header = deserialize_hex::("000000208adac0c3312ce198ee1bc0b0ca079a648b47f7a2d36b92680a164e9f3472b681bb83e66a3c19332e7825a0e4e6e5bed63192d8c0bf1321803d06881cbd470f44ec4eb05c8075011a855549e5").unwrap(); + let parent_hash = + BlockHash::from_str("5c87f7add34f31c644275476761e4273ec27c6bb31d383f27693df84f78a4275") + .unwrap(); + + assert_eq!(parent_block_header.block_hash(), parent_hash); + + let coinbase_branch = vec![ + TxMerkleNode::from_str( + "6d8897d112189fdf9af120b70d16a26783ffda7a2f1968984fee9fb9e7764b79", + ) + .unwrap(), + TxMerkleNode::from_str( + "37a820e558328ef2ec366c4e83e9e627cb34ced06a140423686b5ee6def33e89", + ) + .unwrap(), + TxMerkleNode::from_str( + "f1369a82235d4c3a922444ae6764ee00efa9c040ea13c8ce2b83d2b1865cb5ae", + ) + .unwrap(), + TxMerkleNode::from_str( + "3859ccd5dd341126af04fe193e021294cde59bcb06ebf9195c4406f96a98dd1a", + ) + .unwrap(), + TxMerkleNode::from_str( + "37d8306e628ade2df4f868ce235ac0f4f5e32195664fdf865e43ed236299e81b", + ) + .unwrap(), + TxMerkleNode::from_str( + "827e84f9933bd63ebab743610f2a94c2d8d1cf2e79d0879a27da359b06bee33d", + ) + .unwrap(), + ]; + + let blockchain_branch = vec![]; + + let auxpow_mainnet_2_679_506 = AuxPow { + coinbase_tx, + parent_hash, + coinbase_branch, + coinbase_index: 0, + blockchain_branch, + blockchain_index: 0, + parent_block_header, + }; + + let aux_block_hash = + BlockHash::from_str("283fa35edb604a913ead7e776b534f1661723da6721131eee75a39738ed2e8f8") + .unwrap(); + + assert!(auxpow_mainnet_2_679_506.check(aux_block_hash, AUXPOW_BLOCK_CHAIN_ID, true).is_ok()); + } + + #[test] + fn test_valid_auxpow_dogecoin_mainnet_multiple_merged_mined_chains() { + // AuxPow information for Dogecoin mainnet block, height 1_000_000 + let coinbase_tx = deserialize_hex::("01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff4c0350c90d047cbb6d5608028f23145c045200fabe6d6d5ae93eb8863161c9cd991ba9f7b0fa4353b645310b2edabb123066dea3c9150140000000000000000d2f6e6f64655374726174756d2f0000000001e81c0695000000001976a9145da2560b857f5ba7874de4a1173e67b4d509c46688ac00000000").unwrap(); + let coinbase_tx_id = + Txid::from_str("02454219e64615c8a9aa813fb520a924a36229f5a658de069b5a4c588ffaa209") + .unwrap(); + + assert_eq!(coinbase_tx.compute_txid(), coinbase_tx_id); + assert_eq!(coinbase_tx.input.len(), 1); + + let coinbase_script = ScriptBuf::from_hex("0350c90d047cbb6d5608028f23145c045200fabe6d6d5ae93eb8863161c9cd991ba9f7b0fa4353b645310b2edabb123066dea3c9150140000000000000000d2f6e6f64655374726174756d2f").unwrap(); + assert_eq!(coinbase_script, coinbase_tx.input.first().unwrap().script_sig); + + // Litecoin mainnet block, height 903_504 + let parent_block_header = deserialize_hex::("03000000d37139870c8a6853cdbdb0eba43956efccedd7f50dcd57ca13187200da12320a6102f658ec72bd6b3550dd54d3b3c9e4b7063ec4662e94e768fb1cdda77e678cd4ba6d56f542011bb8a07866").unwrap(); + let parent_hash = + BlockHash::from_str("3d6a5046000041f2517ca0d436b558640803cfa2596d1d2febfe6079d84eb358") + .unwrap(); + + assert_eq!(parent_block_header.block_hash(), parent_hash); + + let coinbase_branch = vec![ + TxMerkleNode::from_str( + "e77d6288e0f8280ab954bcef89f5d7d524c8137f3929aa12f5e81f753032f936", + ) + .unwrap(), + TxMerkleNode::from_str( + "8c104741e043384a401bd56e815b6c36f88457e9940958d1e5648cc51f71c889", + ) + .unwrap(), + ]; + + let blockchain_branch = vec![ + TxMerkleNode::from_str( + "0000000000000000000000000000000000000000000000000000000000000000", + ) + .unwrap(), + TxMerkleNode::from_str( + "f98c4e9736d8eb8bb46299798906695c755369a3df99a93ffdded1713f1cf6e2", + ) + .unwrap(), + TxMerkleNode::from_str( + "48e55233b9707330def98c80a2105eaa5fac8f687d872ffb4b4741fa2bdb247d", + ) + .unwrap(), + TxMerkleNode::from_str( + "c77d206e2f3cc38e18b85aaa89a8f142a9bf0f4106925d39708f91083e7d8594", + ) + .unwrap(), + TxMerkleNode::from_str( + "c98b626e977b43fffd7a2cdd179bd15dc8566b0c21252562f944f40ab5870ff7", + ) + .unwrap(), + TxMerkleNode::from_str( + "4433bbe69867ef956764b17c57ccb89189c901709a8aa771ca119e1d8437912c", + ) + .unwrap(), + ]; + + let auxpow_mainnet_1_000_000 = AuxPow { + coinbase_tx, + parent_hash, + coinbase_branch, + coinbase_index: 0, + blockchain_branch, + blockchain_index: 56, + parent_block_header, + }; + + let aux_block_hash = + BlockHash::from_str("6aae55bea74235f0c80bd066349d4440c31f2d0f27d54265ecd484d8c1d11b47") + .unwrap(); + + assert!(auxpow_mainnet_1_000_000.check(aux_block_hash, AUXPOW_BLOCK_CHAIN_ID, true).is_ok()); + } + + #[test] + fn test_valid_auxpow_dogecoin_mainnet_legacy() { + // AuxPow information for Dogecoin mainnet block, height 1_731_044 + let coinbase_tx = deserialize_hex::("01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff6403c77a1245c608010910b320f69f1eb5e1b7bb45fbc108e4379b5a77dee5a3b0a406038989b964153901000000000000000076000000e1f54700580000262f425443432f20000000000000000000000000000000000000000000000000000000000000000000000002f2aff395000000001976a914088b3b2a325c12f47e60b87f7bdd7f280e144e0388ac0000000000000000266a24aa21a9ed79084e12cbfa366a9758d398d62cf8195a17a72c87583f64a568797872369aea00000000").unwrap(); + let coinbase_tx_id = + Txid::from_str("7bae97400ac3ec58aa03703a3228d125b89823c017b8100715250171c9fb9dc5") + .unwrap(); + + assert_eq!(coinbase_tx.compute_txid(), coinbase_tx_id); + assert_eq!(coinbase_tx.input.len(), 1); + + let coinbase_script = ScriptBuf::from_hex("03c77a1245c608010910b320f69f1eb5e1b7bb45fbc108e4379b5a77dee5a3b0a406038989b964153901000000000000000076000000e1f54700580000262f425443432f2000000000000000000000000000000000000000000000000000000000000000").unwrap(); + assert_eq!(coinbase_script, coinbase_tx.input.first().unwrap().script_sig); + + // Verify that the coinbase script does not contain the MERGED_MINING_HEADER + // This test confirms it's a legacy AuxPow (without the merged mining header format) + assert!(AuxPow::find_bytes(coinbase_script.as_bytes(), &MERGED_MINING_HEADER).is_none()); + + // Litecoin mainnet block, height 1_211_079 + let parent_block_header = deserialize_hex::("0000002086ac5d670b08f9c812a3778dcafd42ab0bf79d3e48b3a043ce56af64c47182cf8cc521420ec9f8f15ea770a2c91b85f5a72a02f18942a58a47210222b4924202832328596887451a04cbc280").unwrap(); + let parent_hash = + BlockHash::from_str("3865ad8eea8a2b3b7452e5c75d411156907f65b42593e2d48b2ff95974897245") + .unwrap(); + + assert_eq!(parent_block_header.block_hash(), parent_hash); + + let coinbase_branch = vec![ + TxMerkleNode::from_str( + "2f7aeb2615e5251d107339a2e4d7177ac341f71b2d0d5931695f6133a64f497e", + ) + .unwrap(), + TxMerkleNode::from_str( + "c9d13f7eb2a2fc00f6d4a4f941c214868ae508a2c410eee2e37936d742167d0c", + ) + .unwrap(), + TxMerkleNode::from_str( + "bc82db9b6fd74cfba991bc2e422f6db3f2d6916053fcc6feaa5eca588b8bff1c", + ) + .unwrap(), + TxMerkleNode::from_str( + "be46c579c5859312688104b479e2527058a5da9ce9dd5f7ef4247cfa3384ca99", + ) + .unwrap(), + TxMerkleNode::from_str( + "f9813969c631dcd4c536fa7f5f371d840c8270644651d3c2536fb5abb0d846c2", + ) + .unwrap(), + ]; + + let blockchain_branch = vec![]; + + let auxpow_mainnet_1_731_044 = AuxPow { + coinbase_tx, + parent_hash, + coinbase_branch, + coinbase_index: 0, + blockchain_branch, + blockchain_index: 0, + parent_block_header, + }; + + let aux_block_hash = + BlockHash::from_str("10b320f69f1eb5e1b7bb45fbc108e4379b5a77dee5a3b0a406038989b9641539") + .unwrap(); + + assert!(auxpow_mainnet_1_731_044.check(aux_block_hash, AUXPOW_BLOCK_CHAIN_ID, true).is_ok()); + } +} diff --git a/bitcoin/src/dogecoin/constants.rs b/bitcoin/src/dogecoin/constants.rs index 458fdadd7..799473e16 100644 --- a/bitcoin/src/dogecoin/constants.rs +++ b/bitcoin/src/dogecoin/constants.rs @@ -90,8 +90,7 @@ pub fn genesis_block(params: impl AsRef) -> Block { time: 1386325540, bits: CompactTarget::from_consensus(0x1e0ffff0), nonce: 99943, - }, - auxpow: None, + }.into(), txdata, }, Network::Testnet => Block { @@ -102,8 +101,7 @@ pub fn genesis_block(params: impl AsRef) -> Block { time: 1391503289, bits: CompactTarget::from_consensus(0x1e0ffff0), nonce: 997879, - }, - auxpow: None, + }.into(), txdata, }, Network::Regtest => Block { @@ -114,8 +112,7 @@ pub fn genesis_block(params: impl AsRef) -> Block { time: 1296688602, bits: CompactTarget::from_consensus(0x207fffff), nonce: 2, - }, - auxpow: None, + }.into(), txdata, }, } diff --git a/bitcoin/src/dogecoin/mod.rs b/bitcoin/src/dogecoin/mod.rs index 5f5256d14..ce248125f 100644 --- a/bitcoin/src/dogecoin/mod.rs +++ b/bitcoin/src/dogecoin/mod.rs @@ -8,10 +8,11 @@ pub mod address; pub mod constants; pub mod params; +pub mod auxpow; pub use address::*; -use crate::block::{Header, TxMerkleNode}; +use crate::block::{Header as PureHeader, TxMerkleNode, Version}; use crate::consensus::{encode, Decodable, Encodable}; use crate::dogecoin::params::Params; use crate::internal_macros::impl_consensus_encoding; @@ -20,57 +21,105 @@ use crate::p2p::Magic; use crate::prelude::*; use crate::{io, BlockHash, Transaction}; use core::fmt; +use core::ops::{Deref, DerefMut}; +use crate::dogecoin::auxpow::{AuxPow, VERSION_AUXPOW}; -/// AuxPow version bit, see -pub const VERSION_AUXPOW: i32 = 1 << 8; - -fn is_auxpow(header: Header) -> bool { (header.version.to_consensus() & VERSION_AUXPOW) != 0 } - -/// Data for merge-mining AuxPoW. +/// Dogecoin block header. +/// +/// ### Dogecoin Core References /// -/// It contains the parent block's coinbase tx that can be verified to be in the parent block. -/// The transaction's input contains the hash to the actual merge-mined block. +/// * [CBlockHeader definition](https://github.com/dogecoin/dogecoin/blob/7237da74b8c356568644cbe4fba19d994704355b/src/primitives/block.h#L23) #[derive(PartialEq, Eq, Clone, Debug)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "serde", serde(crate = "actual_serde"))] -pub struct AuxPow { - /// The parent block's coinbase tx. - pub coinbase_tx: Transaction, - /// The parent block's hash. - pub parent_hash: BlockHash, - /// The Merkle branch linking the coinbase tx to the parent block's Merkle root. - pub coinbase_branch: Vec, - /// The index of the coinbase tx in the Merkle tree. - pub coinbase_index: i32, - /// The Merkle branch linking the merge-mined block to the coinbase tx. - pub blockchain_branch: Vec, - /// The index of the merged-mined block in the Merkle tree. - pub blockchain_index: i32, - /// Parent block header (on which the PoW is done). - pub parent_block: Header, +pub struct Header { + /// Block header without AuxPow information. + pub pure_header: PureHeader, + /// AuxPoW structure, present if merged mining was used to mine this block. + pub aux_pow: Option, } -impl_consensus_encoding!( - AuxPow, - coinbase_tx, - parent_hash, - coinbase_branch, - coinbase_index, - blockchain_branch, - blockchain_index, - parent_block -); +impl Deref for Header { + type Target = PureHeader; + fn deref(&self) -> &Self::Target { + &self.pure_header + } +} + +impl DerefMut for Header { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.pure_header + } +} + +impl From for Header { + fn from(pure_header: PureHeader) -> Self { + Self { pure_header, aux_pow: None } + } +} + +impl Decodable for Header { + #[inline] + fn consensus_decode_from_finite_reader( + r: &mut R, + ) -> Result { + let pure_header: PureHeader = Decodable::consensus_decode_from_finite_reader(r)?; + let aux_pow = if pure_header.has_auxpow_bit() { + Some(Decodable::consensus_decode_from_finite_reader(r)?) + } else { + None + }; + + Ok(Self { pure_header, aux_pow }) + } +} + +impl Encodable for Header { + #[inline] + fn consensus_encode(&self, w: &mut W) -> Result { + let mut len = 0; + len += self.pure_header.consensus_encode(w)?; + if let Some(ref aux_pow) = self.aux_pow { + len += aux_pow.consensus_encode(w)?; + } + Ok(len) + } +} + +impl PureHeader { + /// Checks if a block header indicates it was merged mined and contains AuxPow information. + pub fn has_auxpow_bit(&self) -> bool { + (self.version.to_consensus() & VERSION_AUXPOW) != 0 + } + + /// Extracts the chain ID from the block header's version field. + pub fn extract_chain_id(&self) -> i32 { + self.version.to_consensus() >> 16 + } + + /// Determines if a block header represents a legacy (pre-AuxPoW) block. + pub fn is_legacy(&self) -> bool { + self.version == Version::ONE + // Random v2 block with no AuxPoW, treat as legacy + || (self.version == Version::TWO && self.extract_chain_id() == 0) + } + + /// Extracts the base version number from a block header, removing AuxPoW and chain ID bits. + pub fn extract_base_version(&self) -> i32 { + self.version.to_consensus() % VERSION_AUXPOW + } +} /// Dogecoin block. /// /// A collection of transactions with an attached proof of work. -/// The AuxPoW is present if the block was mined using merge-mining. +/// The AuxPoW data is present in `header` if the block was mined using merged-mining. /// -/// See [Bitcoin Wiki: Block][wiki-block] and [Bitcoin Wiki: Merged_mining_specification][merge-mining] +/// See [Bitcoin Wiki: Block][wiki-block] and [Bitcoin Wiki: Merged_mining_specification][merged-mining] /// for more information. /// /// [wiki-block]: https://en.bitcoin.it/wiki/Block -/// [merge-mining]: https://en.bitcoin.it/wiki/Merged_mining_specification +/// [merged-mining]: https://en.bitcoin.it/wiki/Merged_mining_specification /// /// ### Dogecoin Core References /// @@ -79,10 +128,8 @@ impl_consensus_encoding!( #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "serde", serde(crate = "actual_serde"))] pub struct Block { - /// The block header. + /// The Dogecoin block header. pub header: Header, - /// AuxPoW structure, present if merged mining was used to mine this block. - pub auxpow: Option, /// List of transactions contained in the block. pub txdata: Vec, } @@ -112,35 +159,7 @@ impl Block { } } -impl Decodable for Block { - #[inline] - fn consensus_decode_from_finite_reader( - r: &mut R, - ) -> Result { - let header: Header = Decodable::consensus_decode_from_finite_reader(r)?; - let auxpow = if is_auxpow(header) { - Some(Decodable::consensus_decode_from_finite_reader(r)?) - } else { - None - }; - let txdata = Decodable::consensus_decode_from_finite_reader(r)?; - - Ok(Self { header, auxpow, txdata }) - } -} - -impl Encodable for Block { - #[inline] - fn consensus_encode(&self, w: &mut W) -> Result { - let mut len = 0; - len += self.header.consensus_encode(w)?; - if let Some(ref auxpow) = self.auxpow { - len += auxpow.consensus_encode(w)?; - } - len += self.txdata.consensus_encode(w)?; - Ok(len) - } -} +impl_consensus_encoding!(Block, header, txdata); /// The cryptocurrency network to act on. #[derive(Copy, PartialEq, Eq, PartialOrd, Ord, Clone, Hash, Debug)] @@ -208,20 +227,20 @@ impl core::str::FromStr for Network { #[cfg(test)] mod tests { - use hex::test_hex_unwrap as hex; - + use hex::{test_hex_unwrap as hex}; + use hashes::Hash; use super::*; use crate::block::{ValidationError, Version}; use crate::consensus::encode::{deserialize, serialize}; use crate::{CompactTarget, Target, Work}; use crate::{Network as BitcoinNetwork}; - #[test] fn dogecoin_block_test() { // Mainnet Dogecoin block 5794c80b80d9c33e0737a5353cd52b1f097f61d8d2b9f471e1702345080e0002 let some_block = hex!("01000000c76fe7f8ec09989d32b7907966fbd347134f80a7b71efce55fec502aa126ba3894b3065289ff8ba1ab4e8391771174d47cf2c974ebd24a1bdafd6c107d5a7a207d78bb52de8f001c00da8c3c0201000000010000000000000000000000000000000000000000000000000000000000000000ffffffff2602bc6d062f503253482f047178bb5208f8042975030000000d2f7374726174756d506f6f6c2f000000000100629b29c45500001976a91450e9fe87c705dcd4b7523b47e3314c2115f5d5df88ac0000000001000000015f48fabf4425324df2b5e58f4e9c771297f76f5fa37db7556f6fc1d22742da1f010000006a473044022062d29d2d26f7d826e7b72257486e294d284832743c7803a2901eb07e326b25a002207efc391b0f4e724c9d518075c0e056cc425540f845b0fd419ba8a9d49d69288301210297a2568525760a98454d84f5e5adba9fd0a41726a6fb774ddc407279e41e2061ffffffff0240bab598200000001976a91401348a2b83aeb6b1ba2a174a1a40b7c75fbeb12088ac0040be40250000001976a914025407d928ef333979d064ae233353d80e29d58c88ac00000000"); let cutoff_block = hex!("01000000c76fe7f8ec09989d32b7907966fbd347134f80a7b71efce55fec502aa126ba3894b3065289ff8ba1ab4e8391771174d47cf2c974ebd24a1bdafd6c107d5a7a207d78bb52de8f001c00da8c3c0201000000010000000000000000000000000000000000000000000000000000000000000000ffffffff2602bc6d062f503253482f047178bb5208f8042975030000000d2f7374726174756d506f6f6c2f000000000100629b29c45500001976a91450e9fe87c705dcd4b7523b47e3314c2115f5d5df88ac0000000001000000015f48fabf4425324df2b5e58f4e9c771297f76f5fa37db7556f6fc1d22742da1f010000006a473044022062d29d2d26f7d826e7b72257486e294d284832743c7803a2901eb07e326b25a002207efc391b0f4e724c9d518075c0e056cc425540f845b0fd419ba8a9d49d69288301210297a2568525760a98454d84f5e5adba9fd0a41726a6fb774ddc407279e41e2061ffffffff0240bab598200000001976a91401348a2b83aeb6b1ba2a174a1a40b7c75fbeb12088ac0040be40250000001976a914025407d928ef333979d064ae233353d80e29d58c88ac"); + let header = &some_block[0..80]; let currhash = hex!("02000e08452370e171f4b9d2d8617f091f2bd53c35a537073ec3d9800bc89457"); let prevhash = hex!("c76fe7f8ec09989d32b7907966fbd347134f80a7b71efce55fec502aa126ba38"); @@ -237,7 +256,7 @@ mod tests { assert_eq!(serialize(&real_decode.header.block_hash()), currhash); assert_eq!(real_decode.header.version, Version::ONE); assert_eq!(serialize(&real_decode.header.prev_blockhash), prevhash); - // assert_eq!(real_decode.header.merkle_root, real_decode.compute_merkle_root().unwrap()); + assert_eq!(real_decode.header.merkle_root, real_decode.compute_merkle_root().unwrap()); assert_eq!(serialize(&real_decode.header.merkle_root), merkle); assert_eq!(real_decode.header.time, 1388017789); assert_eq!(real_decode.header.bits, CompactTarget::from_consensus(469798878)); @@ -252,9 +271,81 @@ mod tests { assert_eq!(real_decode.header.difficulty(BitcoinNetwork::Bitcoin), 455); assert_eq!(real_decode.header.difficulty_float(), 455.52430084170516); + assert!(!real_decode.header.has_auxpow_bit()); + assert_eq!(real_decode.header.extract_chain_id(), 0); + assert_eq!(real_decode.header.extract_base_version(), 1); + assert!(real_decode.header.is_legacy()); + + assert!(real_decode.header.aux_pow.is_none()); + + assert_eq!(serialize(&real_decode.header.pure_header), header); + assert_eq!(serialize(&real_decode.header), header); assert_eq!(serialize(&real_decode), some_block); } + #[test] + fn dogecoin_block_test_with_auxpow() { + // Mainnet Dogecoin block d3ea48350b102b90acf9eac6629072d5f697c02faf360b26d365e7b2bfb98070 + let block = hex!("020162001e21ad14bc1ef20cf2d58e2b755ae4a7bfb75c906c74ef3dbb97cc57dcd77581b14423c43517df2b4f3277731daba29d0d865b515a6f19f5fb61d5799b28f2c9b48713540fa8071b0000000001000000010000000000000000000000000000000000000000000000000000000000000000ffffffff4f032ac209fabe6d6dd3ea48350b102b90acf9eac6629072d5f697c02faf360b26d365e7b2bfb980700100000000000000062f503253482f04ce871354080811312915000000092f7374726174756d2f000000000100f2052a010000001976a914f332ec6f1729495e7edcd8ce9d887742567fe60988ac0000000006d2bbd93141ea6d2c8434caeb01828a2a522275b66d2b21fe4ed8230cfe65a101ad2f07c348abdc05f57e2e7d8763488aad71df9f557aec46ae0207ae2bb74a1500000000000000000002000000f288b555ed9b44c814afbbbac135d95e0984a5cc7cb554fccbd2ca27c5e423cebf3021ed058ac83eaa0f64b0d405fc99216209b7e56deeeefceb3629210d1cabcb8713545a50021bc40227b50301000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0d0300b1050101062f503253482fffffffff010085fd36af050000232102c9cbaeb767cc8c884204601f322c6977890cdd3d274f8b1a704ae00382102191ac0000000001000000011fcccc77028fc5fb96d60ee5f258da271ba9b4fdec12594a5dd2efa1fb5bd14b010000006b483045022100f4a17664176706f74877433dbfb8e68a5c1e45730da9248a8da3bab833cea1ca022018fee77621c20f196b82c3c8fd663959b0c8ea985b0407bdbac1414a5a5a21bf0121033fcc1cb9c1b7b11758eb2cd3a25b4ff917a5e248f5d6fcc74160dd6a450acf8bffffffff020759dd6e000000001976a914a553c686ac1aa534ffcb3b694c463944175a6f3c88ac8548f276890700001976a914a1ea13863020f36897b671ad328d98e9364f12b488ac000000000100000001215c45fc31d3beae4a5c76efbb0925d0b4bace72f62248aa3777927eecb42152000000006b483045022100bd3f18da6acd8180ed99c7c7fc6feab18653008cc58be5c32a470193aea330860220719c64121805606240c48dab2e8e3f3d82501b78b78597043ca134abf4c85443012103e6435d9ad2a3f3ff2d2c58aef41075df4eb5417c313d3ea4d5bb87ab46241940ffffffff0140ab8aac140000001976a91481db1aa49ebc6a71cad96949eb28e22af85eb0bd88ac00000000"); + let header = &block[0..398]; + let pure_header = &header[0..80]; + let auxpow = &header[80..]; + let auxpow_coinbase_tx = &auxpow[..164]; + let auxpow_parent_hash = &auxpow[164..196]; + let auxpow_coinbase_branch = &auxpow[196..229]; + let auxpow_coinbase_index = &auxpow[229..233]; + let auxpow_blockchain_branch = &auxpow[233..234]; + let auxpow_blockchain_index = &auxpow[234..238]; + let auxpow_parent_block_header = &auxpow[238..]; + + let currhash = hex!("7080b9bfb2e765d3260b36af2fc097f6d5729062c6eaf9ac902b100b3548ead3"); + let prevhash = hex!("1e21ad14bc1ef20cf2d58e2b755ae4a7bfb75c906c74ef3dbb97cc57dcd77581"); + let merkle = hex!("b14423c43517df2b4f3277731daba29d0d865b515a6f19f5fb61d5799b28f2c9"); + + let decode: Result = deserialize(&block); + assert!(decode.is_ok()); + let block_decode = decode.unwrap(); + + assert_eq!(serialize(&block_decode.header.block_hash()), currhash); + assert_eq!(block_decode.header.version, Version::from_consensus(6422786)); + assert_eq!(serialize(&block_decode.header.prev_blockhash), prevhash); + assert_eq!(block_decode.header.merkle_root, block_decode.compute_merkle_root().unwrap()); + assert_eq!(serialize(&block_decode.header.merkle_root), merkle); + assert_eq!(block_decode.header.time, 1410566068); + assert_eq!(block_decode.header.bits, CompactTarget::from_consensus(453486607)); + assert_eq!(block_decode.header.nonce, 0); + + // Should fail because AuxPow is used + assert_eq!( + block_decode.header.validate_pow_with_scrypt(block_decode.header.target()), + Err(ValidationError::BadProofOfWork) + ); + // Bitcoin network is used because Dogecoin's difficulty calculation is based on Bitcoin's, + // which uses Bitcoin's `max_attainable_target` value + assert_eq!(block_decode.header.difficulty(BitcoinNetwork::Bitcoin), 8559); + assert_eq!(block_decode.header.difficulty_float(), 8559.417587564147); + + assert!(block_decode.header.has_auxpow_bit()); + assert_eq!(block_decode.header.extract_chain_id(), 98); + assert_eq!(block_decode.header.extract_base_version(), 2); + assert!(!block_decode.header.is_legacy()); + + assert!(block_decode.header.aux_pow.is_some()); + let auxpow_decode = block_decode.header.aux_pow.as_ref().unwrap(); + assert_eq!(serialize(&auxpow_decode.coinbase_tx), auxpow_coinbase_tx); + assert_eq!(auxpow_decode.parent_hash.to_byte_array(), auxpow_parent_hash); + assert_eq!(serialize(&auxpow_decode.coinbase_branch), auxpow_coinbase_branch); + assert_eq!(auxpow_decode.coinbase_index.to_le_bytes(), auxpow_coinbase_index); + assert_eq!(serialize(&auxpow_decode.blockchain_branch), auxpow_blockchain_branch); + assert_eq!(auxpow_decode.blockchain_index.to_le_bytes(), auxpow_blockchain_index); + assert_eq!(serialize(&auxpow_decode.parent_block_header), auxpow_parent_block_header); + + assert_eq!(serialize(&auxpow_decode), auxpow); + assert_eq!(serialize(&block_decode.header.pure_header), pure_header); + assert_eq!(serialize(&block_decode.header), header); + assert_eq!(serialize(&block_decode), block); + } + #[test] fn validate_pow_with_scrypt_test() { let some_header = hex!("01000000c76fe7f8ec09989d32b7907966fbd347134f80a7b71efce55fec502aa126ba3894b3065289ff8ba1ab4e8391771174d47cf2c974ebd24a1bdafd6c107d5a7a207d78bb52de8f001c00da8c3c"); @@ -401,14 +492,14 @@ mod tests { let params = Params::new(Network::Dogecoin); let epoch_start = genesis_block(¶ms).header; // Block 239, the only information used are `bits` and `time` - let current = Header { + let current = PureHeader { version: Version::ONE, prev_blockhash: BlockHash::all_zeros(), merkle_root: TxMerkleNode::all_zeros(), time: 1386475638, bits: epoch_start.bits, nonce: epoch_start.nonce - }; + }.into(); let adjustment = CompactTarget::from_header_difficulty_adjustment_dogecoin(epoch_start, current, params, height); let adjustment_bits = CompactTarget::from_consensus(0x1e0fffff); // Block 240 compact target assert_eq!(adjustment, adjustment_bits); @@ -422,23 +513,23 @@ mod tests { let height = 1_131_290; let params = Params::new(Network::Dogecoin); // Block 1_131_288, the only information used is `time` - let epoch_start = Header { + let epoch_start = PureHeader { version: Version::from_consensus(6422787), prev_blockhash: BlockHash::from_str("ac0ffad025605732b310be7edf52111fa9511ffc54f06d21aab1c50d4085b39f").expect("failed to parse block hash"), merkle_root: TxMerkleNode::from_str("80c67973ef43f2df8a3641dac7da16ea59f55e4d77b9206c6e5cfa25d3bf094b").expect("failed to parse merkle root"), time: 1458248044, bits: CompactTarget::from_consensus(0x1b01e7c1), nonce: 0 - }; + }.into(); // Block 1_131_289, the only information used are `bits` and `time` - let current = Header { + let current = PureHeader { version: Version::from_consensus(6422787), prev_blockhash: BlockHash::from_str("7724f7b3f9652ebc121ce101a10bfabd6815518b2814bd16f7a2dcc13dd121ec").expect("failed to parse block hash"), merkle_root: TxMerkleNode::from_str("33c13df68d2f74c76367659cc95436510ed5504ef3c53ae90679ec12ab4e8b81").expect("failed to parse merkle root"), time: 1458248269, bits: CompactTarget::from_consensus(0x1b01cf5d), nonce: 0 - }; + }.into(); let adjustment = CompactTarget::from_header_difficulty_adjustment_dogecoin(epoch_start, current, params, height); let adjustment_bits = CompactTarget::from_consensus(0x1b0269d1); // Block 1_131_290 compact target assert_eq!(adjustment, adjustment_bits); @@ -453,23 +544,23 @@ mod tests { let params = Params::new(Network::Dogecoin); let starting_bits = CompactTarget::from_consensus(0x1e0fffff); // Block 479 compact target // Block 239, the only information used is `time` - let epoch_start = Header { + let epoch_start = PureHeader{ version: Version::ONE, prev_blockhash: BlockHash::all_zeros(), merkle_root: TxMerkleNode::all_zeros(), time: 1386475638, bits: starting_bits, nonce: 0 - }; + }.into(); // Block 479, the only information used are `bits` and `time` - let current = Header { + let current = PureHeader{ version: Version::ONE, prev_blockhash: BlockHash::all_zeros(), merkle_root: TxMerkleNode::all_zeros(), time: 1386475840, bits: starting_bits, nonce: 0 - }; + }.into(); let adjustment = CompactTarget::from_header_difficulty_adjustment_dogecoin(epoch_start, current, params, height); let adjustment_bits = CompactTarget::from_consensus(0x1e00ffff); // Block 480 compact target assert_eq!(adjustment, adjustment_bits); @@ -483,23 +574,23 @@ mod tests { let height = 1_131_286; let params = Params::new(Network::Dogecoin); // Block 1_131_284, the only information used is `time` - let epoch_start = Header { + let epoch_start = PureHeader { version: Version::from_consensus(6422787), prev_blockhash: BlockHash::from_str("a695a2cc43bd5c5f32acecada764b8764b044f067909b997d4f98a6733c3fa70").expect("failed to parse block hash"), merkle_root: TxMerkleNode::from_str("806736d9e0cab2de97e7afc9f2031c5a0413c0bff00d82cc38fa0d568d2f7135").expect("failed to parse merkle root"), time: 1458247987, bits: CompactTarget::from_consensus(0x1b02f5b6), nonce: 0 - }; + }.into(); // Block 1_131_285, the only information used are `bits` and `time` - let current = Header { + let current = PureHeader { version: Version::from_consensus(6422787), prev_blockhash: BlockHash::from_str("db185a7d97060e13dd53ff759f9280d473d7bb6fccc8883fbc8f1fa1f071fc82").expect("failed to parse block hash"), merkle_root: TxMerkleNode::from_str("20419a4d74c0284e241ca5d3c91ea2b533d8a6502e4b6e4a7f8a2fc50d42796e").expect("failed to parse merkle root"), time: 1458247995, bits: CompactTarget::from_consensus(0x1b029d4f), nonce: 0 - }; + }.into(); let adjustment = CompactTarget::from_header_difficulty_adjustment_dogecoin(epoch_start, current, params, height); let adjustment_bits = CompactTarget::from_consensus(0x1b025a60); // Block 1_131_286 compact target assert_eq!(adjustment, adjustment_bits); diff --git a/bitcoin/src/dogecoin/params.rs b/bitcoin/src/dogecoin/params.rs index 284116715..44a8b3f07 100644 --- a/bitcoin/src/dogecoin/params.rs +++ b/bitcoin/src/dogecoin/params.rs @@ -49,10 +49,16 @@ pub struct Params { pub max_attainable_target: Target, /// Expected amount of time to mine one block. pub pow_target_spacing: i64, - /// Determines whether retargeting is disabled for this network or not. + /// Whether retargeting is disabled for this network or not. pub no_pow_retargeting: bool, /// Height after which the Digishield difficulty adjustment algorithm is used. pub digishield_activation_height: u32, + /// Height after which merged mining is allowed. + pub auxpow_height: u32, + /// Whether to enforce that the parent and auxiliary block headers have different chain IDs. + pub strict_chain_id: bool, + /// Expected chain ID for validating AuxPoW headers. + pub auxpow_chain_id: i32, } /// The mainnet parameters. @@ -86,6 +92,9 @@ impl Params { max_attainable_target: Target::MAX_ATTAINABLE_MAINNET_DOGE, pow_target_spacing: ONE_MINUTE, // 1 minute no_pow_retargeting: false, + auxpow_height: 371_337, + strict_chain_id: true, + auxpow_chain_id: 0x0062, digishield_activation_height: 145000, }; @@ -103,6 +112,9 @@ impl Params { max_attainable_target: Target::MAX_ATTAINABLE_TESTNET_DOGE, pow_target_spacing: ONE_MINUTE, // 1 minute no_pow_retargeting: false, + auxpow_height: 158_100, + strict_chain_id: false, + auxpow_chain_id: 0x0062, digishield_activation_height: 145000, }; @@ -120,6 +132,9 @@ impl Params { max_attainable_target: Target::MAX_ATTAINABLE_REGTEST_DOGE, pow_target_spacing: ONE_SECOND, // regtest: 1 second blocks no_pow_retargeting: true, + auxpow_height: 20, + strict_chain_id: true, + auxpow_chain_id: 0x0062, digishield_activation_height: 10, }; @@ -162,6 +177,11 @@ impl Params { Network::Regtest => true, } } + + /// Checks if legacy blocks can be mined at the given block height. + pub const fn allow_legacy_blocks(&self, height: u32) -> bool { + height < self.auxpow_height + } } impl AsRef for Params { diff --git a/bitcoin/src/pow.rs b/bitcoin/src/pow.rs index 962bb4203..12e15698e 100644 --- a/bitcoin/src/pow.rs +++ b/bitcoin/src/pow.rs @@ -19,7 +19,7 @@ use crate::block::Header; use crate::blockdata::block::BlockHash; use crate::consensus::encode::{self, Decodable, Encodable}; use crate::consensus::Params; -use crate::dogecoin::params::Params as DogecoinParams; +use crate::dogecoin::{params::Params as DogecoinParams, Header as DogecoinHeader}; use crate::error::{ContainsPrefixError, MissingPrefixError, ParseIntError, PrefixedHexError, UnprefixedHexError}; /// Implement traits and methods shared by `Target` and `Work`. @@ -582,8 +582,8 @@ impl CompactTarget { /// /// The expected [`CompactTarget`] recalculation. pub fn from_header_difficulty_adjustment_dogecoin( - last_epoch_boundary: Header, - current: Header, + last_epoch_boundary: DogecoinHeader, + current: DogecoinHeader, params: impl AsRef, height: u32 ) -> CompactTarget { From a47e2d28e94e4c35341d7167abc70b6bc8e5967d Mon Sep 17 00:00:00 2001 From: Paul Liu Date: Thu, 4 Sep 2025 20:29:29 +0800 Subject: [PATCH 49/53] feat: make Header a parameter of NetworkMessage (#16) Because Dogecoin's Header type is now different than Bitcoin's, The NetworkMessage type has to take Header as a parameter too. --- bitcoin/examples/handshake.rs | 4 +- bitcoin/src/p2p/message.rs | 68 +++++++++++++++------- fuzz/fuzz_targets/bitcoin/deser_net_msg.rs | 6 +- 3 files changed, 53 insertions(+), 25 deletions(-) diff --git a/bitcoin/examples/handshake.rs b/bitcoin/examples/handshake.rs index 6c674bc05..77a55d0b7 100644 --- a/bitcoin/examples/handshake.rs +++ b/bitcoin/examples/handshake.rs @@ -9,8 +9,8 @@ use bitcoin::consensus::{encode, Decodable}; use bitcoin::p2p::{self, address, message, message_network}; use bitcoin::secp256k1::rand::Rng; -type NetworkMessage = message::NetworkMessage; -type RawNetworkMessage = message::RawNetworkMessage; +type NetworkMessage = message::NetworkMessage; +type RawNetworkMessage = message::RawNetworkMessage; fn main() { // This example establishes a connection to a Bitcoin node, sends the initial diff --git a/bitcoin/src/p2p/message.rs b/bitcoin/src/p2p/message.rs index 1b559afc8..36c9f8107 100644 --- a/bitcoin/src/p2p/message.rs +++ b/bitcoin/src/p2p/message.rs @@ -11,7 +11,7 @@ use core::{fmt, iter}; use hashes::{sha256d, Hash}; use io::{Read, Write}; -use crate::blockdata::{block, transaction}; +use crate::blockdata::transaction; use crate::consensus::encode::{self, CheckedData, Decodable, Encodable, VarInt}; use crate::merkle_tree::MerkleBlock; use crate::p2p::address::{AddrV2Message, Address}; @@ -150,9 +150,9 @@ impl std::error::Error for CommandStringError { /// A Network message #[derive(Clone, Debug, PartialEq, Eq)] -pub struct RawNetworkMessage { +pub struct RawNetworkMessage { magic: Magic, - payload: NetworkMessage, + payload: NetworkMessage, payload_len: u32, checksum: [u8; 4], } @@ -160,7 +160,7 @@ pub struct RawNetworkMessage { /// A Network message payload. Proper documentation is available on at /// [Bitcoin Wiki: Protocol Specification](https://en.bitcoin.it/wiki/Protocol_specification) #[derive(Clone, PartialEq, Eq, Debug)] -pub enum NetworkMessage { +pub enum NetworkMessage { /// `version` Version(message_network::VersionMessage), /// `verack` @@ -184,7 +184,7 @@ pub enum NetworkMessage { /// `block` Block(Block), /// `headers` - Headers(Vec), + Headers(Vec
), /// `sendheaders` SendHeaders, /// `getaddr` @@ -243,7 +243,7 @@ pub enum NetworkMessage { }, } -impl NetworkMessage { +impl NetworkMessage { /// Return the message command as a static string reference. /// /// This returns `"unknown"` for [NetworkMessage::Unknown], @@ -300,9 +300,9 @@ impl NetworkMessage { } } -impl RawNetworkMessage { +impl RawNetworkMessage { /// Creates a [RawNetworkMessage] - pub fn new(magic: Magic, payload: NetworkMessage) -> Self { + pub fn new(magic: Magic, payload: NetworkMessage) -> Self { let mut engine = sha256d::Hash::engine(); let payload_len = payload.consensus_encode(&mut engine).expect("engine doesn't error"); let payload_len = u32::try_from(payload_len).expect("network message use u32 as length"); @@ -312,12 +312,12 @@ impl RawNetworkMessage { } /// Consumes the [RawNetworkMessage] instance and returns the inner payload. - pub fn into_payload(self) -> NetworkMessage { + pub fn into_payload(self) -> NetworkMessage { self.payload } /// The actual message data - pub fn payload(&self) -> &NetworkMessage { + pub fn payload(&self) -> &NetworkMessage { &self.payload } @@ -335,9 +335,9 @@ impl RawNetworkMessage { pub fn command(&self) -> CommandString { self.payload.command() } } -struct HeaderSerializationWrapper<'a>(&'a Vec); +struct HeaderSerializationWrapper<'a, Header>(&'a Vec
); -impl<'a> Encodable for HeaderSerializationWrapper<'a> { +impl<'a, Header: Encodable> Encodable for HeaderSerializationWrapper<'a, Header> { #[inline] fn consensus_encode(&self, w: &mut W) -> Result { let mut len = 0; @@ -350,7 +350,7 @@ impl<'a> Encodable for HeaderSerializationWrapper<'a> { } } -impl Encodable for NetworkMessage { +impl Encodable for NetworkMessage { fn consensus_encode(&self, writer: &mut W) -> Result { match self { NetworkMessage::Version(ref dat) => dat.consensus_encode(writer), @@ -395,7 +395,7 @@ impl Encodable for NetworkMessage { } } -impl Encodable for RawNetworkMessage { +impl Encodable for RawNetworkMessage { fn consensus_encode(&self, w: &mut W) -> Result { let mut len = 0; len += self.magic.consensus_encode(w)?; @@ -407,9 +407,9 @@ impl Encodable for RawNetworkMessage { } } -struct HeaderDeserializationWrapper(Vec); +struct HeaderDeserializationWrapper
(Vec
); -impl Decodable for HeaderDeserializationWrapper { +impl Decodable for HeaderDeserializationWrapper
{ #[inline] fn consensus_decode_from_finite_reader( r: &mut R, @@ -435,7 +435,7 @@ impl Decodable for HeaderDeserializationWrapper { } } -impl Decodable for RawNetworkMessage { +impl Decodable for RawNetworkMessage { fn consensus_decode_from_finite_reader( r: &mut R, ) -> Result { @@ -549,9 +549,9 @@ mod test { use hex::test_hex_unwrap as hex; use super::message_network::{Reject, RejectReason, VersionMessage}; - use super::{block, AddrV2Message, Address, CommandString, Magic, MerkleBlock}; + use super::{AddrV2Message, Address, CommandString, Magic, MerkleBlock}; use crate::bip152::BlockTransactionsRequest; - use crate::blockdata::block::Block; + use crate::blockdata::block; use crate::blockdata::script::ScriptBuf; use crate::blockdata::transaction::Transaction; use crate::consensus::encode::{deserialize, deserialize_partial, serialize}; @@ -563,14 +563,40 @@ mod test { CFCheckpt, CFHeaders, CFilter, GetCFCheckpt, GetCFHeaders, GetCFilters, }; use crate::p2p::ServiceFlags; + use block::{Block, Header}; - type NetworkMessage = super::NetworkMessage; - type RawNetworkMessage = super::RawNetworkMessage; + type NetworkMessage = super::NetworkMessage; + type RawNetworkMessage = super::RawNetworkMessage; fn hash(slice: [u8; 32]) -> Hash { Hash::from_slice(&slice).unwrap() } + #[test] + fn dogecoin_ser_der_raw_network_message_test() { + type NetworkMessage = super::NetworkMessage; + type RawNetworkMessage = super::RawNetworkMessage; + + // Dogecoin mainnet block 62959fd2246701d38917fe59920523ad66111b68e360fe3a60ebdca2dd6d4546 (height 1000013) + let bytes = hex!("03016200916a0b2966c72c1bb533469388c7e5d771e0d5570e897fe98453baa8226c2d7e9d73ce00d335ed419b390659c89e3af25ab2ffc8db0f1144cbf2ab8867b140e23dbe6d569e42031b0000000001000000010000000000000000000000000000000000000000000000000000000000000000ffffffff3f0359c90d04566dbe3d2cfabe6d6df5cee89f3766065a3f9ddc4ab57a1adf830944af0ef431e9d4f59c2f4e8985e70800000000000000085600096a01000000ffffffff0100f90295000000001976a91457757ed3d226faf12bd43983896ec81e7fca369a88ac0000000010ce3fcdb40c53bb040487a2ff745a7384314d4c24809aba52a35c74a5be4ca5000000000003d7bec81d9cd6968e141a0d0c1645b9dcca96ab9fb57dbc6f63a4ef669a0ad099de79681c0a67d2f2de006742ab85320b9ecc7df8f9979eff946d1f6964d3ab5956b1698e938dbe001e487469ad0c84c1b008757a7a78908378db499a602949bd00000000030000005d24356ff4b4111265187fd2e89dc7e58019221fba2e4ad0ec789ed38bfd9be152ac7cf211bf53770edf319ccc29cf8692076446430a195ed1c55ad458612d1e3dbe6d56f542011b3a4a43490101000000010000000000000000000000000000000000000000000000000000000000000000ffffffff09034d420f04566dbe3dffffffff010010a5d4e80000001976a914d1895519d5281aa005487b1ff07f3307aced8d6e88ac00000000"); + + let block: crate::dogecoin::Block = deserialize(&bytes).unwrap(); + let header: crate::dogecoin::Header = deserialize(&bytes[0..446]).unwrap(); + + assert!(header.aux_pow.is_some()); + + // Test only Block and Header messages, which are different than Bitcoin. + let msgs = vec![ + NetworkMessage::Block(block), + NetworkMessage::Headers(vec![header]), + ]; + + for msg in msgs { + let raw_msg = RawNetworkMessage::new(Magic::from_bytes([57, 0, 0, 0]), msg); + assert_eq!(deserialize::(&serialize(&raw_msg)).unwrap(), raw_msg); + } + } + #[test] fn full_round_ser_der_raw_network_message_test() { let version_msg: VersionMessage = deserialize(&hex!("721101000100000000000000e6e0845300000000010000000000000000000000000000000000ffff0000000000000100000000000000fd87d87eeb4364f22cf54dca59412db7208d47d920cffce83ee8102f5361746f7368693a302e392e39392f2c9f040001")).unwrap(); diff --git a/fuzz/fuzz_targets/bitcoin/deser_net_msg.rs b/fuzz/fuzz_targets/bitcoin/deser_net_msg.rs index f22b293ab..22b5dfcad 100644 --- a/fuzz/fuzz_targets/bitcoin/deser_net_msg.rs +++ b/fuzz/fuzz_targets/bitcoin/deser_net_msg.rs @@ -1,8 +1,10 @@ use honggfuzz::fuzz; fn do_test(data: &[u8]) { - let _: Result, _> = - bitcoin::consensus::encode::deserialize(data); + let _: Result< + bitcoin::p2p::message::RawNetworkMessage, + _, + > = bitcoin::consensus::encode::deserialize(data); } fn main() { From d275ae7aa5f59f5bc14b42a3e2db364d8f53daeb Mon Sep 17 00:00:00 2001 From: mducroux Date: Fri, 5 Sep 2025 10:43:22 +0200 Subject: [PATCH 50/53] test: add additional tests adapted from core's dogecoin_tests.cpp (#15) This PR adds additional tests for the `from_next_work_required_dogecoin` method and `params` module. The goal is to replicate the tests found in Dogecoin core [dogecoin_tests.cpp](https://github.com/dogecoin/dogecoin/blob/master/src/test/dogecoin_tests.cpp). --- bitcoin/src/dogecoin/mod.rs | 362 +++++++++++++++++---------------- bitcoin/src/dogecoin/params.rs | 39 ++++ bitcoin/src/pow.rs | 4 +- 3 files changed, 224 insertions(+), 181 deletions(-) diff --git a/bitcoin/src/dogecoin/mod.rs b/bitcoin/src/dogecoin/mod.rs index ce248125f..8982af0df 100644 --- a/bitcoin/src/dogecoin/mod.rs +++ b/bitcoin/src/dogecoin/mod.rs @@ -432,229 +432,233 @@ mod tests { } #[test] - fn compact_target_from_downwards_difficulty_adjustment() { - let height = 240; - let params = Params::new(Network::Dogecoin); - let starting_bits = CompactTarget::from_consensus(0x1e0ffff0); // Genesis compact target on Mainnet - let start_time: i64 = 1386325540; // Genesis block unix time - let end_time: i64 = 1386475638; // Block 239 unix time - let timespan = end_time - start_time; // Slower than expected (150,098 seconds diff) - let adjustment = CompactTarget::from_next_work_required_dogecoin(starting_bits, timespan, ¶ms, height); - let adjustment_bits = CompactTarget::from_consensus(0x1e0fffff); // Block 240 compact target - assert_eq!(adjustment, adjustment_bits); - } - - #[test] - fn compact_target_from_downards_difficulty_adjustment_digishield() { - let height = 1531886; - let params = Params::new(Network::Dogecoin); - let starting_bits = CompactTarget::from_consensus(0x1b01c45a); // Block 1_531_885 compact target - let start_time: i64 = 1483302792; // Block 1_531_884 unix time - let end_time: i64 = 1483302869; // Block 1_531_885 unix time - let timespan = end_time - start_time; // Slower than expected (77 seconds diff) - let adjustment = CompactTarget::from_next_work_required_dogecoin(starting_bits, timespan, ¶ms, height); - let adjustment_bits = CompactTarget::from_consensus(0x1b01d36e); // Block 1_531_886 compact target - assert_eq!(adjustment, adjustment_bits); - } - - #[test] - fn compact_target_from_upwards_difficulty_adjustment() { - let height = 480; - let params = Params::new(Network::Dogecoin); - let starting_bits = CompactTarget::from_consensus(0x1e0fffff); // Block 240 compact target - let start_time: i64 = 1386475638; // Block 239 unix time - let end_time: i64 = 1386475840; // Block 479 unix time - let timespan = end_time - start_time; // Faster than expected (202 seconds diff) - let adjustment = CompactTarget::from_next_work_required_dogecoin(starting_bits, timespan, ¶ms, height); - let adjustment_bits = CompactTarget::from_consensus(0x1e00ffff); // Block 480 compact target - assert_eq!(adjustment, adjustment_bits); - } - - #[test] - fn compact_target_from_upwards_difficulty_adjustment_digishield() { - let height = 1531882; - let params = Params::new(Network::Dogecoin); - let starting_bits = CompactTarget::from_consensus(0x1b01dc29); // Block 1_531_881 compact target - let start_time: i64 = 1483302572; // Block 1_531_880 unix time - let end_time: i64 = 1483302608; // Block 1_531_881 unix time - let timespan = end_time - start_time; // Faster than expected (36 seconds diff) - let adjustment = CompactTarget::from_next_work_required_dogecoin(starting_bits, timespan, ¶ms, height); - let adjustment_bits = CompactTarget::from_consensus(0x1b01c45a); // Block 1_531_882 compact target - assert_eq!(adjustment, adjustment_bits); - } - - #[test] - fn compact_target_from_downwards_difficulty_adjustment_using_headers() { - use crate::{block::Version, dogecoin::constants::genesis_block, TxMerkleNode}; - use hashes::Hash; - - let height = 240; - let params = Params::new(Network::Dogecoin); - let epoch_start = genesis_block(¶ms).header; - // Block 239, the only information used are `bits` and `time` - let current = PureHeader { - version: Version::ONE, - prev_blockhash: BlockHash::all_zeros(), - merkle_root: TxMerkleNode::all_zeros(), - time: 1386475638, - bits: epoch_start.bits, - nonce: epoch_start.nonce - }.into(); - let adjustment = CompactTarget::from_header_difficulty_adjustment_dogecoin(epoch_start, current, params, height); - let adjustment_bits = CompactTarget::from_consensus(0x1e0fffff); // Block 240 compact target - assert_eq!(adjustment, adjustment_bits); - } - - #[test] - fn compact_target_from_downwards_difficulty_adjustment_using_headers_digishield() { - use crate::{block::Version, TxMerkleNode}; - use std::str::FromStr; - - let height = 1_131_290; - let params = Params::new(Network::Dogecoin); - // Block 1_131_288, the only information used is `time` - let epoch_start = PureHeader { - version: Version::from_consensus(6422787), - prev_blockhash: BlockHash::from_str("ac0ffad025605732b310be7edf52111fa9511ffc54f06d21aab1c50d4085b39f").expect("failed to parse block hash"), - merkle_root: TxMerkleNode::from_str("80c67973ef43f2df8a3641dac7da16ea59f55e4d77b9206c6e5cfa25d3bf094b").expect("failed to parse merkle root"), - time: 1458248044, - bits: CompactTarget::from_consensus(0x1b01e7c1), - nonce: 0 - }.into(); - // Block 1_131_289, the only information used are `bits` and `time` - let current = PureHeader { - version: Version::from_consensus(6422787), - prev_blockhash: BlockHash::from_str("7724f7b3f9652ebc121ce101a10bfabd6815518b2814bd16f7a2dcc13dd121ec").expect("failed to parse block hash"), - merkle_root: TxMerkleNode::from_str("33c13df68d2f74c76367659cc95436510ed5504ef3c53ae90679ec12ab4e8b81").expect("failed to parse merkle root"), - time: 1458248269, - bits: CompactTarget::from_consensus(0x1b01cf5d), - nonce: 0 - }.into(); - let adjustment = CompactTarget::from_header_difficulty_adjustment_dogecoin(epoch_start, current, params, height); - let adjustment_bits = CompactTarget::from_consensus(0x1b0269d1); // Block 1_131_290 compact target - assert_eq!(adjustment, adjustment_bits); - } - - #[test] - fn compact_target_from_upwards_difficulty_adjustment_using_headers() { - use crate::{block::Version, TxMerkleNode}; - use hashes::Hash; - + fn compact_target_from_adjustment_is_max_target() { let height = 480; let params = Params::new(Network::Dogecoin); - let starting_bits = CompactTarget::from_consensus(0x1e0fffff); // Block 479 compact target - // Block 239, the only information used is `time` - let epoch_start = PureHeader{ - version: Version::ONE, - prev_blockhash: BlockHash::all_zeros(), - merkle_root: TxMerkleNode::all_zeros(), - time: 1386475638, - bits: starting_bits, - nonce: 0 - }.into(); - // Block 479, the only information used are `bits` and `time` - let current = PureHeader{ - version: Version::ONE, - prev_blockhash: BlockHash::all_zeros(), - merkle_root: TxMerkleNode::all_zeros(), - time: 1386475840, - bits: starting_bits, - nonce: 0 - }.into(); - let adjustment = CompactTarget::from_header_difficulty_adjustment_dogecoin(epoch_start, current, params, height); - let adjustment_bits = CompactTarget::from_consensus(0x1e00ffff); // Block 480 compact target - assert_eq!(adjustment, adjustment_bits); + let starting_bits = CompactTarget::from_consensus(0x1e0fffff); // Max target + let timespan = 4 * params.pow_target_timespan(height); // 4x Slower than expected + let got = CompactTarget::from_next_work_required_dogecoin(starting_bits, timespan, ¶ms, height); + let want = params.max_attainable_target.to_compact_lossy(); + assert_eq!(got, want); } #[test] - fn compact_target_from_upwards_difficulty_adjustment_using_headers_digishield() { - use crate::{block::Version, TxMerkleNode}; - use std::str::FromStr; - - let height = 1_131_286; + fn compact_target_from_adjustment_is_max_target_digishield() { + let height = 145_000; let params = Params::new(Network::Dogecoin); - // Block 1_131_284, the only information used is `time` - let epoch_start = PureHeader { - version: Version::from_consensus(6422787), - prev_blockhash: BlockHash::from_str("a695a2cc43bd5c5f32acecada764b8764b044f067909b997d4f98a6733c3fa70").expect("failed to parse block hash"), - merkle_root: TxMerkleNode::from_str("806736d9e0cab2de97e7afc9f2031c5a0413c0bff00d82cc38fa0d568d2f7135").expect("failed to parse merkle root"), - time: 1458247987, - bits: CompactTarget::from_consensus(0x1b02f5b6), - nonce: 0 - }.into(); - // Block 1_131_285, the only information used are `bits` and `time` - let current = PureHeader { - version: Version::from_consensus(6422787), - prev_blockhash: BlockHash::from_str("db185a7d97060e13dd53ff759f9280d473d7bb6fccc8883fbc8f1fa1f071fc82").expect("failed to parse block hash"), - merkle_root: TxMerkleNode::from_str("20419a4d74c0284e241ca5d3c91ea2b533d8a6502e4b6e4a7f8a2fc50d42796e").expect("failed to parse merkle root"), - time: 1458247995, - bits: CompactTarget::from_consensus(0x1b029d4f), - nonce: 0 - }.into(); - let adjustment = CompactTarget::from_header_difficulty_adjustment_dogecoin(epoch_start, current, params, height); - let adjustment_bits = CompactTarget::from_consensus(0x1b025a60); // Block 1_131_286 compact target - assert_eq!(adjustment, adjustment_bits); + let starting_bits = CompactTarget::from_consensus(0x1e0fffff); // Max target + let timespan = 5 * params.pow_target_timespan(height); // 5x Slower than expected + let got = CompactTarget::from_next_work_required_dogecoin(starting_bits, timespan, ¶ms, height); + let want = params.max_attainable_target.to_compact_lossy(); + assert_eq!(got, want); } #[test] - fn compact_target_from_maximum_upward_difficulty_adjustment() { + fn compact_target_from_minimum_downward_difficulty_adjustment() { let pre_digishield_heights = vec![5_000, 10_000, 15_000]; let digishield_heights = vec![145_000, 1_000_000]; - let starting_bits = CompactTarget::from_consensus(0x1b025a60); // Arbitrary difficulty + let starting_bits = CompactTarget::from_consensus(0x1b02f5b6); // Arbitrary difficulty let params = Params::new(Network::Dogecoin); for height in pre_digishield_heights { - let timespan = (0.06 * params.pow_target_timespan(height) as f64) as i64; // > 16x Faster than expected + let timespan = 4 * params.pow_target_timespan(height); // 4x Slower than expected let got = CompactTarget::from_next_work_required_dogecoin(starting_bits, timespan, ¶ms, height); let want = Target::from_compact(starting_bits) - .min_transition_threshold_dogecoin(¶ms, height) + .max_transition_threshold_dogecoin(¶ms, height) .to_compact_lossy(); assert_eq!(got, want); } for height in digishield_heights { - let timespan = -params.pow_target_timespan(height); // Negative timespan + let timespan = 5 * params.pow_target_timespan(height); // 5x Slower than expected let got = CompactTarget::from_next_work_required_dogecoin(starting_bits, timespan, ¶ms, height); let want = Target::from_compact(starting_bits) - .min_transition_threshold_dogecoin(¶ms, height) + .max_transition_threshold_dogecoin(¶ms, height) .to_compact_lossy(); assert_eq!(got, want); } } #[test] - fn compact_target_from_minimum_downward_difficulty_adjustment() { + fn should_compute_target() { + #[derive(Debug)] + struct TestCase<'a> { + name: &'a str, + height: u32, + starting_bits: CompactTarget, + start_time: i64, + end_time: i64, + expected_adjustment_bits: CompactTarget, + } + + let test_cases = vec![ + TestCase { + name: "Downwards difficulty adjustment (timespan: 150,098 s, slower than expected)", + height: 240, + starting_bits: CompactTarget::from_consensus(0x1e0ffff0), // Genesis compact target on Mainnet + start_time: 1386325540, // Genesis block unix time + end_time: 1386475638, // Block 239 unix time + expected_adjustment_bits: CompactTarget::from_consensus(0x1e0fffff) // Block 240 compact target + }, + // Adapted from: + TestCase { + name: "Downwards difficulty adjustment Digishield (timespan: 252 s, slower than expected)", + height: 145_000, + starting_bits: CompactTarget::from_consensus(0x1b499dfd), // Block 145_000 compact target, + start_time: 1395094427, // Block 144_999 unix time + end_time: 1395094679, // Block 145_000 unix time + expected_adjustment_bits: CompactTarget::from_consensus(0x1b671062), // Block 145_001 compact target + }, + // Adapted from: + TestCase { + name: "Downwards difficulty adjustment Digishield (timespan: 525 s, slower than expected)", + height: 145_000, + starting_bits: CompactTarget::from_consensus(0x1b3439cd), // Block 145_107 compact target + start_time: 1395100835, // Block 145_106 unix time + end_time: 1395101360, // Block 145_107 unix time + expected_adjustment_bits: CompactTarget::from_consensus(0x1b4e56b3), // Block 145_108 compact target + }, + TestCase { + name: "Downwards difficulty adjustment Digishield (timespan: 225 s, slower than expected)", + height: 1_131_290, + starting_bits: CompactTarget::from_consensus(0x1b01cf5d), // Block 1_131_289 compact target + start_time: 1458248044, // Block 1_131_288 unix time + end_time: 1458248269, // Block 1_131_289 unix time + expected_adjustment_bits: CompactTarget::from_consensus(0x1b0269d1) // Block 1_131_290 compact target + }, + TestCase { + name: "Downwards difficulty adjustment Digishield (timespan: 77 s, slower than expected)", + height: 1_531_886, + starting_bits: CompactTarget::from_consensus(0x1b01c45a), // Block 1_531_885 compact target + start_time: 1483302792, // Block 1_531_884 unix time + end_time: 1483302869, // Block 1_531_885 unix time + expected_adjustment_bits: CompactTarget::from_consensus(0x1b01d36e) // Block 1_531_886 compact target + }, + TestCase { + name: "Upwards difficulty adjustment (timespan: 202 s, faster than expected)", + height: 480, + starting_bits: CompactTarget::from_consensus(0x1e0fffff), // Block 240 compact target + start_time: 1386475638, // Block 239 unix time + end_time: 1386475840, // Block 479 unix time + expected_adjustment_bits: CompactTarget::from_consensus(0x1e00ffff) // Block 480 compact target + }, + // Adapted from: + TestCase { + name: "Upwards difficulty adjustment (timespan: 12,105 s, faster than expected)", + height: 0, + starting_bits: CompactTarget::from_consensus(0x1c1a1206), // Block 9_359 compact target, + start_time: 1386942008, // Block 9_359 unix time, + end_time: 1386954113, // Block 9_599 unix time, + expected_adjustment_bits: CompactTarget::from_consensus(0x1c15ea59), // Block 9_600 compact target, + }, + // Adapted from: + TestCase { + name: "Upwards difficulty adjustment Digishield (timespan: -70 s, faster than expected)", + height: 145_000, + starting_bits: CompactTarget::from_consensus(0x1b446f21), // Block 149_423 compact target + start_time: 1395380517, // Block 149_422 unix time + end_time: 1395380447, // Block 149_423 unix time + expected_adjustment_bits: CompactTarget::from_consensus(0x1b335358), // Block 149_424 compact target + }, + TestCase { + name: "Upwards difficulty adjustment Digishield (timespan: 8 s, faster than expected)", + height: 1_131_286, + starting_bits: CompactTarget::from_consensus(0x1b029d4f), // Block 1_131_285 compact target + start_time: 1458247987, // Block 1_131_284 unix time + end_time: 1458247995, // Block 1_131_285 unix time + expected_adjustment_bits: CompactTarget::from_consensus(0x1b025a60) // Block 1_131_286 compact target + }, + TestCase { + name: "Upwards difficulty adjustment Digishield (timespan: 36 s, faster than expected)", + height: 1_531_882, + starting_bits: CompactTarget::from_consensus(0x1b01dc29), // Block 1_531_881 compact target + start_time: 1483302572, // Block 1_531_880 unix time + end_time: 1483302608, // Block 1_531_881 unix time + expected_adjustment_bits: CompactTarget::from_consensus(0x1b01c45a) // Block 1_531_882 compact target + }, + // Adapted from: + TestCase { + name: "Difficulty adjustment rounding Digishield (timespan: 48 s, faster than expected)", + height: 145_000, + starting_bits: CompactTarget::from_consensus(0x1b671062), // Block 145_001 compact target + start_time: 1395094679, // Block 145_000 unix time + end_time: 1395094727, // Block 145_001 unix time + expected_adjustment_bits: CompactTarget::from_consensus(0x1b6558a4), // Block 145_002 compact target + }, + ]; + + let params = Params::new(Network::Dogecoin); + + // Test difficulty adjustment + for test_case in test_cases.iter() { + let timespan = test_case.end_time - test_case.start_time; + let adjustment = CompactTarget::from_next_work_required_dogecoin( + test_case.starting_bits, + timespan, + ¶ms, + test_case.height, + ); + assert_eq!( + adjustment, test_case.expected_adjustment_bits, + "Unexpected adjustment bits for test case: {}", + test_case.name + ); + } + + // Test difficulty adjustment using headers + for test_case in test_cases.iter() { + let start_header = PureHeader { + version: Version::ONE, + prev_blockhash: BlockHash::all_zeros(), + merkle_root: TxMerkleNode::all_zeros(), + time: test_case.start_time as u32, + bits: CompactTarget::from_consensus(0x1e0fffff), // Note: this value does not matter + nonce: 0 + }.into(); + let end_header = PureHeader { + version: Version::ONE, + prev_blockhash: BlockHash::all_zeros(), + merkle_root: TxMerkleNode::all_zeros(), + time: test_case.end_time as u32, + bits: test_case.starting_bits, + nonce: 0 + }.into(); + let adjustment = CompactTarget::from_header_difficulty_adjustment_dogecoin( + start_header, + end_header, + ¶ms, + test_case.height + ); + assert_eq!( + adjustment, test_case.expected_adjustment_bits, + "Unexpected adjustment bits for test case using headers: {}", + test_case.name + ); + } + } + + #[test] + fn compact_target_from_maximum_upward_difficulty_adjustment() { let pre_digishield_heights = vec![5_000, 10_000, 15_000]; let digishield_heights = vec![145_000, 1_000_000]; - let starting_bits = CompactTarget::from_consensus(0x1b02f5b6); // Arbitrary difficulty + let starting_bits = CompactTarget::from_consensus(0x1b025a60); // Arbitrary difficulty let params = Params::new(Network::Dogecoin); for height in pre_digishield_heights { - let timespan = 4 * params.pow_target_timespan(height); // 4x Slower than expected + let timespan = (0.06 * params.pow_target_timespan(height) as f64) as i64; // > 16x Faster than expected let got = CompactTarget::from_next_work_required_dogecoin(starting_bits, timespan, ¶ms, height); let want = Target::from_compact(starting_bits) - .max_transition_threshold_dogecoin(¶ms, height) + .min_transition_threshold_dogecoin(¶ms, height) .to_compact_lossy(); assert_eq!(got, want); } for height in digishield_heights { - let timespan = 5 * params.pow_target_timespan(height); // 5x Slower than expected + let timespan = -params.pow_target_timespan(height); // Negative timespan let got = CompactTarget::from_next_work_required_dogecoin(starting_bits, timespan, ¶ms, height); let want = Target::from_compact(starting_bits) - .max_transition_threshold_dogecoin(¶ms, height) + .min_transition_threshold_dogecoin(¶ms, height) .to_compact_lossy(); assert_eq!(got, want); } } - #[test] - fn compact_target_from_adjustment_is_max_target() { - let height = 480; - let params = Params::new(Network::Dogecoin); - let starting_bits = CompactTarget::from_consensus(0x1e0fffff); // Block 240 compact target (max target) - let timespan = 4 * params.pow_target_timespan(height); // 4x Slower than expected - let got = CompactTarget::from_next_work_required_dogecoin(starting_bits, timespan, ¶ms, height); - let want = params.max_attainable_target.to_compact_lossy(); - assert_eq!(got, want); - } - #[test] fn roundtrip_compact_target() { let consensus = 0x1e0f_ffff; diff --git a/bitcoin/src/dogecoin/params.rs b/bitcoin/src/dogecoin/params.rs index 44a8b3f07..d5af4c7d9 100644 --- a/bitcoin/src/dogecoin/params.rs +++ b/bitcoin/src/dogecoin/params.rs @@ -215,4 +215,43 @@ mod tests { assert!(Params::REGTEST.is_digishield_activated(height)); } } + + #[test] + fn hardfork_parameters() { + let params = &Params::MAINNET; + + // Initial parameters (pre-Digishield era) + let initial_height = 0; + assert_eq!(params.pow_target_timespan(initial_height), 14400); // 4 hours + assert!(params.allow_legacy_blocks(initial_height)); + assert!(!params.is_digishield_activated(initial_height)); + + let initial_end_height = 144999; + assert_eq!(params.pow_target_timespan(initial_end_height), 14400); // 4 hours + assert!(params.allow_legacy_blocks(initial_end_height)); + assert!(!params.is_digishield_activated(initial_end_height)); + + // Digishield parameters + let digishield_height = 145000; + assert_eq!(params.pow_target_timespan(digishield_height), 60); // 1 minute + assert!(params.allow_legacy_blocks(digishield_height)); + assert!(params.is_digishield_activated(digishield_height)); + + let digishield_end_height = 371336; + assert_eq!(params.pow_target_timespan(digishield_end_height), 60); // 1 minute + assert!(params.allow_legacy_blocks(digishield_end_height)); + assert!(params.is_digishield_activated(digishield_end_height)); + + // AuxPow parameters + let auxpow_height = 371337; + assert_eq!(params.pow_target_timespan(auxpow_height), 60); // 1 minute + assert!(!params.allow_legacy_blocks(auxpow_height)); + assert!(params.is_digishield_activated(auxpow_height)); + + // Arbitrary point after last hard-fork + let auxpow_high_height = 700000; + assert_eq!(params.pow_target_timespan(auxpow_high_height), 60); // 1 minute + assert!(!params.allow_legacy_blocks(auxpow_high_height)); + assert!(params.is_digishield_activated(auxpow_high_height)); + } } diff --git a/bitcoin/src/pow.rs b/bitcoin/src/pow.rs index 12e15698e..3670656a3 100644 --- a/bitcoin/src/pow.rs +++ b/bitcoin/src/pow.rs @@ -587,9 +587,9 @@ impl CompactTarget { params: impl AsRef, height: u32 ) -> CompactTarget { - let timespan = current.time - last_epoch_boundary.time; + let timespan = (current.time as i64) - (last_epoch_boundary.time as i64); let bits = current.bits; - CompactTarget::from_next_work_required_dogecoin(bits, timespan.into(), params, height) + CompactTarget::from_next_work_required_dogecoin(bits, timespan, params, height) } /// Creates a [`CompactTarget`] from a consensus encoded `u32`. From 556c95e6eeae94c6e22f09a9193c75280d9cb0ae Mon Sep 17 00:00:00 2001 From: mducroux Date: Mon, 3 Nov 2025 08:52:44 +0100 Subject: [PATCH 51/53] chore: update README.md (#17) Updates the README.md file to references the upstream rust-bitcoin README file and list the main differences between rust-dogecoin and rust-bitcoin. --- .github/workflows/cron-daily-kani.yml | 4 +- .github/workflows/rust.yml | 4 +- README.md | 212 +++++--------------------- bitcoin/src/taproot/mod.rs | 2 +- 4 files changed, 40 insertions(+), 182 deletions(-) diff --git a/.github/workflows/cron-daily-kani.yml b/.github/workflows/cron-daily-kani.yml index dff1e54be..9b1e7ecaf 100644 --- a/.github/workflows/cron-daily-kani.yml +++ b/.github/workflows/cron-daily-kani.yml @@ -5,10 +5,12 @@ on: - cron: '59 23 * * *' # midnight every day. jobs: run-kani: - runs-on: ubuntu-20.04 + runs-on: ubuntu-24.04 steps: - name: 'Checkout your code.' uses: actions/checkout@v4 - name: 'Run Kani on your code.' uses: model-checking/kani-github-action@v1.1 + with: + args: "--package bitcoin --package bitcoin-units" diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index bbb2a6f00..f25170545 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -295,11 +295,11 @@ jobs: Kani: name: Kani codegen - stable toolchain - runs-on: ubuntu-20.04 + runs-on: ubuntu-24.04 steps: - name: "Checkout repo" uses: actions/checkout@v4 - name: "Build Kani proofs" uses: model-checking/kani-github-action@v1.1 with: - args: "--only-codegen" + args: "--only-codegen --package bitcoin --package bitcoin-units" diff --git a/README.md b/README.md index 664950da2..b7ecd9b82 100644 --- a/README.md +++ b/README.md @@ -1,71 +1,67 @@
-

Rust Bitcoin

+

Rust Dogecoin

Rust Bitcoin logo by Hunter Trujillo, see license and source files under /logo

Library with support for de/serialization, parsing and executing on data-structures - and network messages related to Bitcoin. + and network messages related to Dogecoin.

Crate Info - CC0 1.0 Universal Licensed + Apache License 2.0 CI Status API Docs Rustc Version 1.56.1+ - Chat on IRC - + kani

-[Documentation](https://docs.rs/bitcoin/) +This library is a fork of [rust-bitcoin](https://github.com/rust-bitcoin/rust-bitcoin), adapted to support the **Dogecoin** network. For reference, see the original [rust-bitcoin README](https://github.com/dfinity/rust-dogecoin/blob/master/README.md). -Supports (or should support) +The goal of this project is to provide Dogecoin-compatible types, consensus rules, and utilities, following the architecture of rust-bitcoin, in particular: -* De/serialization of Bitcoin protocol network messages -* De/serialization of blocks and transactions -* Script de/serialization -* Private keys and address creation, de/serialization and validation (including full BIP32 support) -* PSBT v0 de/serialization and all but the Input Finalizer role. Use [rust-miniscript](https://docs.rs/miniscript/latest/miniscript/psbt/index.html) to finalize. +- **Scrypt PoW**: Scrypt-based proof-of-work validation (instead of SHA-256d used in Bitcoin) +- **AuxPoW/Merged Mining**: AuxPow validation and Dogecoin's merged mining with other chains +- **Difficulty Adjustment**: Dogecoin difficulty adjustment algorithms (pre-Digishield and Digishield) +- **Network Parameters**: Consensus parameters for mainnet, testnet, and regtest +- **Addresses**: Dogecoin-specific base58 addresses (P2PKH and P2SH) +- **Genesis Blocks**: Genesis block definitions for mainnet, testnet, and regtest -For JSONRPC interaction with Bitcoin Core, it is recommended to use -[rust-bitcoincore-rpc](https://github.com/rust-bitcoin/rust-bitcoincore-rpc). +## Differences from rust-bitcoin -It is recommended to always use [cargo-crev](https://github.com/crev-dev/cargo-crev) to verify the -trustworthiness of each of your dependencies, including this one. +### 1. Core rust-bitcoin code modifications -## Known limitations +The following core files have been modified from the upstream rust-bitcoin library: -### Consensus +- `bitcoin/src/blockdata/block.rs`: Add scrypt-based proof-of-work validation (`block_hash_with_scrypt()` and `validate_pow_with_scrypt()`) to support Dogecoin's scrypt hashing algorithm. -This library **must not** be used for consensus code (i.e. fully validating blockchain data). It -technically supports doing this, but doing so is very ill-advised because there are many deviations, -known and unknown, between this library and the Bitcoin Core reference implementation. In a -consensus based cryptocurrency such as Bitcoin it is critical that all parties are using the same -rules to validate data, and this library is simply unable to implement the same rules as Core. +- `bitcoin/src/pow.rs`: Implement Dogecoin's difficulty adjustment algorithms: + - Pre-Digishield (blocks 0-144,999) with variable transition thresholds based on block height ranges + - Digishield (blocks 145,000+) + - Helper methods `min_transition_threshold_dogecoin()` and `max_transition_threshold_dogecoin()` for proper difficulty bounds -Given the complexity of both C++ and Rust, it is unlikely that this will ever be fixed, and there -are no plans to do so. Of course, patches to fix specific consensus incompatibilities are welcome. +- `bitcoin/src/p2p/message.rs`: Generic `RawNetworkMessage` and `NetworkMessage` over `Header` and `Block` types to support both Bitcoin and Dogecoin block and header formats (which can include AuxPoW information). -### Support for 16-bit pointer sizes +### 2. Dogecoin-Specific module ([bitcoin/src/dogecoin/](bitcoin/src/dogecoin/)) -16-bit pointer sizes are not supported and we can't promise they will be. If you care about them -please let us know, so we can know how large the interest is and possibly decide to support them. +A Dogecoin module has been added with the following components: -## Documentation +- `mod.rs`: Dogecoin types including: + - `Header`: Dogecoin block header with optional AuxPoW information + - `Block`: Dogecoin block structure supporting both legacy and merged-mined blocks + - `Network`: Dogecoin network enum (mainnet, testnet, regtest) -Currently can be found on [docs.rs/bitcoin](https://docs.rs/bitcoin/). Patches to add usage examples -and to expand on existing docs would be extremely appreciated. +- `auxpow.rs`: Implementation of Auxiliary Proof-of-Work (merged mining) validation -## Contributing +- `params.rs`: Dogecoin consensus parameters for all networks (mainnet, testnet, regtest), such as Digishield and AuxPoW activation height, +BIP activation heights, target spacing, max attainable targets, etc -Contributions are generally welcome. If you intend to make larger changes please discuss them in an -issue before PRing them to avoid duplicate work and architectural mismatches. If you have any -questions or ideas you want to discuss please join us in -[#bitcoin-rust](https://web.libera.chat/?channel=#bitcoin-rust) on -[libera.chat](https://libera.chat). +- `constants.rs`: Dogecoin-specific constants, such as Genesis block definition, and address prefixes -For more information please see `./CONTRIBUTING.md`. +- `address/`: Dogecoin address handling with support for P2PKH and P2SH address types + +**Note**: Advanced Bitcoin features such as Taproot, SegWit v1, and PSBT v2 are not applicable to Dogecoin and are not supported in this fork. ## Minimum Supported Rust Version (MSRV) @@ -74,146 +70,6 @@ This library should always compile with any combination of features on **Rust 1. To build with the MSRV you will likely need to pin a bunch of dependencies, see `./contrib/test.sh` for the current list. -## External dependencies - -We integrate with a few external libraries, most notably `serde`. These -are available via feature flags. To ensure compatibility and MSRV stability we -provide two lock files as a means of inspecting compatible versions: -`Cargo-minimal.lock` containing minimal versions of dependencies and -`Cargo-recent.lock` containing recent versions of dependencies tested in our CI. - -We do not provide any guarantees about the content of these lock files outside -of "our CI didn't fail with these versions". Specifically, we do not guarantee -that the committed hashes are free from malware. It is your responsibility to -review them. - -## Installing Rust - -Rust can be installed using your package manager of choice or [rustup.rs](https://rustup.rs). The -former way is considered more secure since it typically doesn't involve trust in the CA system. But -you should be aware that the version of Rust shipped by your distribution might be out of date. -Generally this isn't a problem for `rust-bitcoin` since we support much older versions than the -current stable one (see MSRV section). - -## Building - -The cargo feature `std` is enabled by default. At least one of the features `std` or `no-std` or -both must be enabled. - -Enabling the `no-std` feature does not disable `std`. To disable the `std` feature you must disable -default features. The `no-std` feature only enables additional features required for this crate to -be usable without `std`. Both can be enabled without conflict. - -The library can be built and tested using [`cargo`](https://github.com/rust-lang/cargo/): - -``` -git clone git@github.com:rust-bitcoin/rust-bitcoin.git -cd rust-bitcoin -cargo build -``` - -You can run tests with: - -``` -cargo test -``` - -Please refer to the [`cargo` documentation](https://doc.rust-lang.org/stable/cargo/) for more -detailed instructions. - -### Just - -We support [`just`](https://just.systems/man/en/) for running dev workflow commands. Run `just` from -your shell to see list available sub-commands. - -### Building the docs - -We build docs with the nightly toolchain, you may wish to use the following shell alias to check -your documentation changes build correctly. - -``` -alias build-docs='RUSTDOCFLAGS="--cfg docsrs" cargo +nightly rustdoc --features="$FEATURES" -- -D rustdoc::broken-intra-doc-links' -``` - -## Testing - -Unit and integration tests are available for those interested, along with benchmarks. For project -developers, especially new contributors looking for something to work on, we do: - -- Fuzz testing with [`Hongfuzz`](https://github.com/rust-fuzz/honggfuzz-rs) -- Mutation testing with [`Mutagen`](https://github.com/llogiq/mutagen) -- Code verification with [`Kani`](https://github.com/model-checking/kani) - -There are always more tests to write and more bugs to find, contributions to our testing efforts -extremely welcomed. Please consider testing code a first class citizen, we definitely do take PRs -improving and cleaning up test code. - -### Unit/Integration tests - -Run as for any other Rust project `cargo test --all-features`. - -### Benchmarks - -We use a custom Rust compiler configuration conditional to guard the bench mark code. To run the -bench marks use: `RUSTFLAGS='--cfg=bench' cargo +nightly bench`. - -### Mutation tests - -We have started doing mutation testing with [mutagen](https://github.com/llogiq/mutagen). To run -these tests first install the latest dev version with `cargo +nightly install --git https://github.com/llogiq/mutagen` -then run with `RUSTFLAGS='--cfg=mutate' cargo +nightly mutagen`. - -### Code verification - -We have started using [kani](https://github.com/model-checking/kani), install with `cargo install --locked kani-verifier` - (no need to run `cargo kani setup`). Run the tests with `cargo kani`. - -## Pull Requests - -Every PR needs at least two reviews to get merged. During the review phase maintainers and -contributors are likely to leave comments and request changes. Please try to address them, otherwise -your PR might get closed without merging after a longer time of inactivity. If your PR isn't ready -for review yet please mark it by prefixing the title with `WIP: `. - -### CI Pipeline - -The CI pipeline requires approval before being run on each MR. - -In order to speed up the review process the CI pipeline can be run locally using -[act](https://github.com/nektos/act). The `fuzz` and `Cross` jobs will be skipped when using `act` -due to caching being unsupported at this time. We do not *actively* support `act` but will merge PRs -fixing `act` issues. - -### Githooks - -To assist devs in catching errors _before_ running CI we provide some githooks. If you do not -already have locally configured githooks you can use the ones in this repository by running, in the -root directory of the repository: -``` -git config --local core.hooksPath githooks/ -``` - -Alternatively add symlinks in your `.git/hooks` directory to any of the githooks we provide. - -## Policy on Altcoins/Altchains - -Since the altcoin landscape includes projects which [frequently appear and disappear, and are poorly -designed anyway](https://download.wpsoftware.net/bitcoin/alts.pdf) we do not support any altcoins. -Supporting Bitcoin properly is already difficult enough and we do not want to increase the -maintenance burden and decrease API stability by adding support for other coins. - -Our code is public domain so by all means fork it and go wild :) - - -## Release Notes - -Release notes are done per crate, see: - -- [bitcoin CHANGELOG](bitcoin/CHANGELOG.md) -- [hashes CHANGELOG](hashes/CHANGELOG.md) -- [internals CHANGELOG](internals/CHANGELOG.md) - - ## Licensing This project is a fork of [rust-bitcoin](https://github.com/rust-bitcoin/rust-bitcoin/tree/master), originally licensed under CC0 v1.0 Universal. diff --git a/bitcoin/src/taproot/mod.rs b/bitcoin/src/taproot/mod.rs index 54576c578..f44e318ac 100644 --- a/bitcoin/src/taproot/mod.rs +++ b/bitcoin/src/taproot/mod.rs @@ -703,7 +703,7 @@ impl TapTree { /// Gets the inner [`NodeInfo`] of this tree root. pub fn into_node_info(self) -> NodeInfo { self.0 } - /// Returns [`TapTreeIter<'_>`] iterator for a taproot script tree, operating in DFS order over + /// Returns [`ScriptLeaves<'_>`] iterator for a taproot script tree, operating in DFS order over /// tree [`ScriptLeaf`]s. pub fn script_leaves(&self) -> ScriptLeaves { ScriptLeaves { leaf_iter: self.0.leaf_nodes() } } From df5bb2c4a2422cec3cf0bf699074a5d07ed7560e Mon Sep 17 00:00:00 2001 From: mducroux Date: Fri, 7 Nov 2025 13:18:51 +0100 Subject: [PATCH 52/53] chore: add changelog v0.32.5-doge.0 and rust dogecoin logo (#18) --- .github/workflows/cron-daily-kani.yml | 2 +- .github/workflows/rust.yml | 2 +- Cargo.toml | 2 +- README.md | 11 +- bitcoin/CHANGELOG.md | 41 +++++ bitcoin/Cargo.toml | 9 +- bitcoin/embedded/Cargo.toml | 2 +- bitcoin/src/lib.rs | 8 +- fuzz/Cargo.toml | 2 +- logo/README.md | 27 +-- logo/rust-bitcoin-inkscape.svg | 244 -------------------------- logo/rust-bitcoin-large.png | Bin 87059 -> 0 bytes logo/rust-bitcoin-optimized.svg | 1 - logo/rust-bitcoin.png | Bin 14406 -> 0 bytes logo/rust-btc+doge.svg | 27 +++ logo/rust-btc+doge@4x.png | Bin 0 -> 70509 bytes logo/rust-doge.svg | 16 ++ logo/rust-doge@4x.png | Bin 0 -> 26482 bytes 18 files changed, 111 insertions(+), 283 deletions(-) delete mode 100644 logo/rust-bitcoin-inkscape.svg delete mode 100644 logo/rust-bitcoin-large.png delete mode 100644 logo/rust-bitcoin-optimized.svg delete mode 100644 logo/rust-bitcoin.png create mode 100644 logo/rust-btc+doge.svg create mode 100644 logo/rust-btc+doge@4x.png create mode 100644 logo/rust-doge.svg create mode 100644 logo/rust-doge@4x.png diff --git a/.github/workflows/cron-daily-kani.yml b/.github/workflows/cron-daily-kani.yml index 9b1e7ecaf..b1e69f537 100644 --- a/.github/workflows/cron-daily-kani.yml +++ b/.github/workflows/cron-daily-kani.yml @@ -13,4 +13,4 @@ jobs: - name: 'Run Kani on your code.' uses: model-checking/kani-github-action@v1.1 with: - args: "--package bitcoin --package bitcoin-units" + args: "--package bitcoin-dogecoin --package bitcoin-units" diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index f25170545..5e851b627 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -302,4 +302,4 @@ jobs: - name: "Build Kani proofs" uses: model-checking/kani-github-action@v1.1 with: - args: "--only-codegen --package bitcoin --package bitcoin-units" + args: "--only-codegen --package bitcoin-dogecoin --package bitcoin-units" diff --git a/Cargo.toml b/Cargo.toml index 7cf9a0f32..a0a4dbdc3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,7 +5,7 @@ resolver = "2" [patch.crates-io.base58ck] path = "base58" -[patch.crates-io.bitcoin] +[patch.crates-io.bitcoin-dogecoin] path = "bitcoin" [patch.crates-io.bitcoin_hashes] diff --git a/README.md b/README.md index b7ecd9b82..c322c8055 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,18 @@

Rust Dogecoin

- Rust Bitcoin logo by Hunter Trujillo, see license and source files under /logo + Rust Dogecoin logo by DFINITY Foundation, see license and source files under /logo

Library with support for de/serialization, parsing and executing on data-structures - and network messages related to Dogecoin. + and network messages related to Bitcoin and Dogecoin.

- Crate Info + Crate Info Apache License 2.0 - CI Status - API Docs + CI Status + API Docs Rustc Version 1.56.1+ - kani

diff --git a/bitcoin/CHANGELOG.md b/bitcoin/CHANGELOG.md index caa0de726..e2f5b9476 100644 --- a/bitcoin/CHANGELOG.md +++ b/bitcoin/CHANGELOG.md @@ -12,6 +12,47 @@ - Backport - Add `XOnlyPublicKey` support for PSBT key retrieval and improve Taproot signing [#4443](https://github.com/rust-bitcoin/rust-bitcoin/pull/4443) - Backport - Add methods to retrieve inner types [#4450](https://github.com/rust-bitcoin/rust-bitcoin/pull/4450) +# 0.32.5-doge.0 - 2025-11-03 + +Initial release of the rust-dogecoin crate, a fork of rust-bitcoin adapted for Dogecoin. + +### rust-bitcoin v0.32.5 modifications + +- **Scrypt Proof-of-Work**: Add scrypt-based PoW validation (`block_hash_with_scrypt()` and `validate_pow_with_scrypt()`) to support Dogecoin's PoW algorithm. +- **Difficulty Adjustment Algorithms**: + - Pre-Digishield algorithm (blocks 0-144,999) with variable transition thresholds based on block height ranges + - Digishield algorithm (blocks 145,000+) + - Methods `min_transition_threshold_dogecoin()` and `max_transition_threshold_dogecoin()` +- **Generic Network Messages**: Make `RawNetworkMessage` and `NetworkMessage` generic over `Header` and `Block` types to support AuxPoW blocks +- Updated license to Apache-2.0 for new Dogecoin-specific code + +### New Dogecoin module (`bitcoin/src/dogecoin/`) + +- **Core Types** (`mod.rs`): + - `Header`: Dogecoin block header with optional AuxPoW data + - `Block`: Block structure supporting both legacy and AuxPoW blocks + - `Network`: Dogecoin mainnet, Testnet, Regtest + - Helper methods for AuxPoW bit detection, chain ID extraction, and legacy block identification + +- **AuxPoW Support** (`auxpow.rs`): + - AuxPow validation (coinbase script validation, merkle branch verification, chain ID checks) + - Error types for AuxPow validation failures + +- **Consensus Parameters** (`params.rs`): + - Dogecoin PoW parameters and methods: target spacing, max attainable target, pow target timespan + - Digishield and AuxPoW activation height + - Chain ID for merged mining + - BIP activation heights + +- **Constants** (`constants.rs`): + - Genesis block definitions for mainnet, testnet, and regtest + - Prefixes for P2PKH and P2SH addresses + +- **Address Handling** (`address/`): + - Base58 address encoding/decoding with Dogecoin-specific prefixes + - Support for P2PKH and P2SH address types + - Network validation and address parsing + # 0.32.5 - 2024-11-27 - Backport - Re-export `bech32` crate [#3662](https://github.com/rust-bitcoin/rust-bitcoin/pull/3662) diff --git a/bitcoin/Cargo.toml b/bitcoin/Cargo.toml index 5897ab9ef..e7e985edb 100644 --- a/bitcoin/Cargo.toml +++ b/bitcoin/Cargo.toml @@ -1,8 +1,8 @@ [package] -name = "bitcoin" +name = "bitcoin-dogecoin" version = "0.32.5-doge.0" -license = "CC0-1.0" -repository = "https://github.com/rust-dogecoin/rust-dogecoin/" +license = "Apache-2.0 OR CC0-1.0" +repository = "https://github.com/dfinity/rust-dogecoin" description = "General purpose library for using and interoperating with Bitcoin and Dogecoin." categories = ["cryptography::cryptocurrencies"] keywords = [ "crypto", "bitcoin", "dogecoin"] @@ -11,6 +11,9 @@ edition = "2021" rust-version = "1.56.1" exclude = ["tests", "contrib"] +[lib] +name = "bitcoin" + [features] default = [ "std", "secp-recovery" ] std = ["base58/std", "bech32/std", "hashes/std", "hex/std", "internals/std", "io/std", "secp256k1/std", "units/std"] diff --git a/bitcoin/embedded/Cargo.toml b/bitcoin/embedded/Cargo.toml index b12f41550..7beae92c7 100644 --- a/bitcoin/embedded/Cargo.toml +++ b/bitcoin/embedded/Cargo.toml @@ -15,7 +15,7 @@ cortex-m-rt = "0.6.10" cortex-m-semihosting = "0.3.3" panic-halt = "0.2.0" alloc-cortex-m = "0.4.1" -bitcoin = { path="../", default-features = false, features = ["secp-lowmemory"] } +bitcoin = { package = "bitcoin-dogecoin", path="../", default-features = false, features = ["secp-lowmemory"] } [[bin]] name = "embedded" diff --git a/bitcoin/src/lib.rs b/bitcoin/src/lib.rs index c335874e9..a863e435e 100644 --- a/bitcoin/src/lib.rs +++ b/bitcoin/src/lib.rs @@ -1,10 +1,10 @@ // SPDX-License-Identifier: CC0-1.0 -//! # Rust Bitcoin Library +//! # Rust Dogecoin Library //! -//! This is a library that supports the Bitcoin network protocol and associated -//! primitives. It is designed for Rust programs built to work with the Bitcoin -//! network. +//! This is a library that supports both the Bitcoin and Dogecoin network protocol +//! and associated primitives. It is designed for Rust programs built to work with +//! the Bitcoin and Dogecoin network. //! //! Except for its dependency on libsecp256k1 (and optionally libbitcoinconsensus), //! this library is written entirely in Rust. It illustrates the benefits of diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index f76d61899..10915e03e 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -11,7 +11,7 @@ cargo-fuzz = true [dependencies] honggfuzz = { version = "0.5.55", default-features = false } -bitcoin = { path = "../bitcoin", features = [ "serde" ] } +bitcoin = { package = "bitcoin-dogecoin", path = "../bitcoin", features = [ "serde" ] } serde = { version = "1.0.103", features = [ "derive" ] } serde_json = "1.0" diff --git a/logo/README.md b/logo/README.md index 71a563dba..efbb520e3 100644 --- a/logo/README.md +++ b/logo/README.md @@ -1,32 +1,19 @@ -# Rust Bitcoin Logo +# Rust Dogecoin Logo + +This logo is based on the [rust-bitcoin logo](https://github.com/rust-bitcoin/rust-bitcoin/tree/master/logo) and incorporates the Dogecoin "Ð" sign inside the Rust meshed gear. ## Files Included are: -- [rust-bitcoin-inkscape.svg](./rust-bitcoin-inkscape.svg) - The Inkscape source file with the things used to make the logo in case adjustments are desired -- [rust-bitcoin-optimized.svg](./rust-bitcoin-optimized.svg) - An optimized SVG for embedding or rendering at any size desired -- [rust-bitcoin.png](./rust-bitcoin.png) - The PNG logo rendered at 300px used by this project -- [rust-bitcoin-large.png](./rust-bitcoin-large.png) - A larger size 1024px x 1024px for convenience for embedding in presentations - -## Author - -Hunter Trujillo, @cryptoquick on [Twitter](https://twitter.com/cryptoquick), [GitHub](https://github.com/cryptoquick), and Telegram. +- [rust-doge.svg](./rust-doge.svg) - An SVG of the Rust Dogecoin logo for embedding or rendering at any size desired +- [rust-doge@4x.png](./rust-doge@4x.png) - A high-resolution PNG of the Rust Dogecoin logo render at 600px x 600px +- [rust-btc+doge.svg](./rust-btc+doge.svg) - An SVG of the Rust Bitcoin logo together with the Rust Dogecoin logo +- [rust-btc+doge@4x.png](./rust-btc+doge@4x.png) - A high-resolution PNG of the Rust Bitcoin + Dogecoin logo render at 1440px x 680px ## License Licensed in the public domain under [CC0 1.0 Universal Public Domain Dedication](https://creativecommons.org/publicdomain/zero/1.0/), and the author of this work rescinds all claims to copyright or coercion or acts of force from any nation state over this work for any purpose -Bitcoin Logo is licensed under the CC Public Domain Dedication: and - Rust Logo is licensed under CC-BY, which allows reuse and modifications for any purpose, as long as distributors give appropriate credit and indicate changes have been made. See here: -## Acknowledgements - -Acknowledgement for the runners up in this PR: https://github.com/rust-bitcoin/rust-bitcoin/pull/891#issuecomment-1074476858 - -In particular, the Rust Bitcoin Wizard gear was an incredibly inspired piece of art. Also, the meshed gears design was beloved by some but not all. - -Thank you to the Rust Bitcoin maintainers and community, your timely responses and guidance was appreciated. - -Also, thank you to the voters on the Rust in Bitcoin Telegram group: . diff --git a/logo/rust-bitcoin-inkscape.svg b/logo/rust-bitcoin-inkscape.svg deleted file mode 100644 index 9b504a344..000000000 --- a/logo/rust-bitcoin-inkscape.svg +++ /dev/null @@ -1,244 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/logo/rust-bitcoin-large.png b/logo/rust-bitcoin-large.png deleted file mode 100644 index f0cce3403f6f875654702fcae1df75212403fa21..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 87059 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 ++|*Tp3nVwa$^`XTHF!}9(H%z!E0N41{GSGZ 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

nWgLZ{xm&^a9 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 diff --git a/logo/rust-bitcoin-optimized.svg b/logo/rust-bitcoin-optimized.svg deleted file mode 100644 index 31cf374ba..000000000 --- a/logo/rust-bitcoin-optimized.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/logo/rust-bitcoin.png b/logo/rust-bitcoin.png deleted file mode 100644 index 0f59dd168e0ce89d0f9cc82dadf0e8ac6c77bc83..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 14406 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 M07*qoM6N<$f_7WJ3jhEB diff --git a/logo/rust-btc+doge.svg b/logo/rust-btc+doge.svg new file mode 100644 index 000000000..83e4b1f90 --- /dev/null +++ b/logo/rust-btc+doge.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/logo/rust-btc+doge@4x.png b/logo/rust-btc+doge@4x.png new file mode 100644 index 0000000000000000000000000000000000000000..7781d631bb1c98bbcc22c2a2219bb7555a6b4c75 GIT binary patch literal 70509 zcmeFZ^;cDG)HS?mkOn0sloBMR>(D8kB3%+nBi)TOO2?r)lnwz20SW0mG{~X5>%5!$ z`JV4Dcz=8ugTa6?*w?;d&9&xS^E#1gDss3_D4qZSfU6)ctpNaNIRJoMf{70PB~a_# zYw!n_qr9FA0Ib(Euc3WCdvn zEf1u<`?s0~RxJo$qf5nK0h5Lvo5+V%lLh(S7WB<8Z9kCBvQKe;_dsI|pW;jOKnv0h zpx-FjQqR*35V=CsGvNQK{ZdjRJ={XqBBiGuYCBTl{0Et-dHQ_eascIzLVWn=2fqt9 zr*EhVHFzDLE0@`j!8iGTU;o!uU@Mdm_(kaL&yrF&_9{ihS5!#wJ$KUlMiHhrGgkm~&jV)V}YpcN+lUr5u0@jN@d*_&W-#>G9-g%8A)F$ow1Y zsa(YUgH)MZjJHJn&DZ#YXnr-yv5_k4y^B8h)PUz68uy0H7Iqq;HPa%cAbhao>C8H(SH}+_i<`(^?i6J(soxB!2#d zV^bA={*RBuhKgz~C;sg@ZMF@J9VV7U5qHJRL3%pRu{+_8l8}UrKq8$??%k{<8E=U< zWN30fV46Q1(bV&E*q$dJYTun&bs`=t%7S!=8DK4Pr?7f1WUYyWMa`4H5pQaD0FX&=M($qr9qU z3!mWV7H`J49cuei?ViH$&meve*>XE!QSj$!25x?=g|a0NW)w`zNRWrxSpWr{}+CmBq-kxY(Td z=>gS$r+#;%)A6E}td{He++GHJbo^`f??3nMUG=pOO8S|1b5i>PW7(~kqwnq4Z$8ip zjT-vM4qaG9i4DD#_0hfeP>ER*wfiB%ueRnF%UXj;i(9e+s3~PHB}t^^lzsfitWYk| zWi?o`q;!6X0j_#>^XN(9^|obBjkDL)-j*`I^4((o$6y-nz7@Um`L&$Y+V!A?;;G%V zl00u)f4)Gh1{G{5)n#oxjlL`Pn-BlIk>ia~M}a3ynSRu?sr%pk`#9*%wJszM#ZvVs z&%9kRDjno@Tvx&6-3Y;cnUPy)VBP;KbkYVtcaf)&f_!Pl+SRUBC}ZaZ4k-i{e4d2u z=QZc2nsLw-42VlU?*9m95mbM#^LDrq-!H2y-p&8M8)obJ%2-ojgHLl5T%S3(P@-WI zi>RVErs%J(U@L|5pb*ip5vq%>?ho_9pMh%`05?Z1jPHKoH}|IfY-1+!!7iq>u7!MR z5D1<9;faEdFP!ymRwTC&RYq?YxlGB*E(v?H2m2R~4pq{u;;NRZS8lTuUB2VdF24&Q z_!VkhFLHhnBL+`f!S+TKf-hM`>(5HsmTkf!^;;^;Y~QfHvE1& zc;i||+8AMW(>yFn;z0BF%;)Vo=RU&cJIU`$xQbR+AKv50uCz`I`l}qQiEBmsFq5w`b}s%VAWaYFAihZhhbai->K01Q&{%h1FjVC_3&j{o z3_`t5|zM^@~_D!1Gw$cui1R`u7w5%Kf>nnR0R${TI1a~n) zN@2(4jgKj3dXKB41vY**LoOOt{u5{x)-eehU?vUVuDD`gxS(8MRnsp<1h7)pXVI?R-4;br4Ev5W?1N{Bdd0LeY<=DrumtloJ9T>l}f+a)Vc@=^A@4&LRXO z?w{vd;N^9Et%)ipJ5~~y)JVqk==FI45YQo81I2H$7m|@ev-6}v=lS}9d!>$j8$8k?_34%C zblB(*szbRp@IP8imq&Y`E(M(v=lpOo~8j}jr^Acfmrvm&DxmVgz(Yf z-&gd_icP5`^gb<4c5Jcni}_(F;?ZEpXA*Fn(pm^SW?N!6P=M_;_w4k@KP2( z?BOsxE+301gH=_i$#&DA$C01!_nr~9GBRK+6ZdMF zCR$T6$j1tIf#i;l5IKO>83aZZLUq+lY}|F6+^?B_2WG*4cfPk<&D<`y@At$vt?)%M zZ{45cJKUC0%DP&wx=RHMf=Jn)tAYH{y@}vvJAYRS;r@0Q?ONl-7c?xWXp5il=8@pW zTLNz=Ux3*gWWca1rw)t`^*0({$7k<)Xg=~r$_w0o;<-j65FWbd89(@mL#w>}b1OlLyvu0jk0*BH}o+HbLzS=%QsLn$R8}qsAdb)5C z$6I8P^Ud({*``p8Dfut4yKKDJP+dO{Rw)8@duyK3fctZJ)QGuo(V_OO`v_#k())oB zFV|w#9sidCIQiHJwvDe9`O=(bc7r^WipZ_Z+W zjH049P9NQ+UN(S{U<>7lA_{B@b_KH^75kk=zWe#2JI4PX)*AGyY7?Gzbvy{Ye;g=O zyR9wQBa@BN+n|BY6r+5j5$cpkKKGX`IYaATYNinR{cUQ=P&l24^6Ro<;_Un&Ho^rQg9YTMY4G13 zyktYs-%B@V1YJX`7@fM8*#tmc$-dt zm1#4x*l!Z|XEG=06_6(b9*@{>TDlegB_s$>@aWMB!}-FRyY0~2Ype1H(Y<#nrYYxG zy`PkP+Vm5ULMN7p&g$jYb9yPw4=**Mz?V4Lp6fl2I)fNT5Z3PRS~L7rjOCfpY5I~l zLgFuQnacGW%oYE-u!$hBCLQfad*81Tvxlqq0v`nyM&^{`@lP*@Asw4&;w)ea-kQ~i z>L`S_OKxeoi@_IikPiMUh;Lm|LT3HFSI5W8`@jL8>~Htho6_LVh@7)Holk^)?Uwbb z3XOTHvOrtDWp`>`6Vy8BxA~skD+^Y)PIsLJIxrgv9)tgDF65z9@Vu)%&aQA-nO} zX)VuvrX1fd!+NBHFK4EQovu>{F(eEUESnD-uKc~!eA|xHug;fK2MW#evKwE zP%4GN6A)kYY2ep6{h?^)wy780{o;tH4pDRE$A8%>v~};ZpA=6kQ`k#cypRNdFMp#P zxemlF|CQo~cgx_>tvwu6oH0i24*d1J+T-bvyX5}UJHjx4#l- zBE$O-_-B>^X<7y3iH#&b3A(| zNVPijeCk^a6V2RrFFtGxnCB*5)pG3QV&}$y3}pEp=h9zAwGm2~_8Cb?6wDw`Qxa&` z))lfoFZQaI&!mV|-*KMpaL|1|*z6M5^5Or+bLOJ0+nig}PJS+f!LP>wXhZ@OWHsNP zbe-$js6nW=<{RNYNy2Yo3Eyba(;EEM|MKR2I5&)a$lc*>KIB;h z_8IS8NZr3pnSVt&m()X(iVSm}GcqKXKE%smBh30HPW}a}ei#j4P4iS+FHe^wKVw<6 zh3`xe&wK+L3Egy~0msvjM7Nnxo?!4d;Yhz6GHudU0D4$STeU*WX z6Zyr{&Ye@(erLG}O;5JhZ2^tcJy2GLUpb_)>EdHN^+yoXeXFR&*9}KoNVaD0j(_jm z92eXiXIj0(M+-ik%%JJbORIA3=ElfynvN^kY3!)}**}Y%EY3BGgp2d%;U>H>c&VDC zdQUXd@A~w{HBVT<`};s0YFHUL=WeDb=1+5f4!%Nl|LyEu``mt>u#s4%FluWsLf9@8 z8*9opJapO`UZJp%-)jWCIAS2XS~vr; z=PTf8Ntb4mBf}}Gz(b12&@c1>tNPo-H(3zRnKZbcYRn{k{Gp|*t1ufc1OO}+bw0b5 z+5epYARX6%=7FqK$a(a_igME+{94 z!!X_TzkA!%&1bSeOT#+4LHMI}u&iUna;9o#mv*a18_x&1o4r{*>n}$o!{mhQxX48p z_wl>FsbBOhWSb0rqybtl!F@E7H*{Kc&DU9!cK(z*-r1Q6VDj>$ehq0}zIQwFIcO;K zUHw8p0R#j_%c~`CG%LoEA`IK6H3RzAg30)J=O+TDYUO4enADq{ggAIn7y!H&%z(hX zN?*=9EVIkY=I!3|?WE)0SzoZC-y?ZxK190nK@4ejXl7^*(Z+-|YU^o>P`+>A!#g;e z{JP&jI!xe|tq5|Jo20N6Rvt@~fAxoakcVMPyNYaWh9krCb;08xRjpxzA#A2RV8La8 zZNNsWLz~&oU*~%8uMKpjY60_b`Yo0vGKD+?vbBjC9i657gn_iqh1_nGH~t45Ci{JR zc~oR?M+tI~(G7A15G@)bwwE%3pRKD1-8R!&NLf#mRL{+jfUM$--QXvys1$isqWQQ7 zKUB+~-#`BZp6(6B&O)IxCc`4x?<-C>W)G&12*b{u>d(|qJ_4aQov_w!Qsk!YWXA6D z*lKF|Z`rktrUuE?JhngwA&6rKs5t%Sb$kv~?VU~YSBqovbOFVC%|{a~m}$m+ z3@Z4spa~?ya5FFH*9(s(RzM>h#F}`o<9!c(;p-$*HVx}|~%1 zZLQ@X>@ekgJL6WYb*j^{uviA&+cfnGBs;pdZ@LTv#bXW3UP#h@#>@$1bs_%d=lk)n zb13huWKI-&1=TxHZe~CVJ7V#0e(7c?8MlGIJ4y~L#kCcQzM+Z7dKhRSYkZIfaLQN} zoCA}`b{=ldNQK@s_$<8)Wyj%`?Z7lBt^)*yVF@>xzQ^y-S9bXPu17izlU8EEYHm`a zh#}AbOBx*c8(4jyTlCx}wHO+7?FF7H+e|tfRI2{RPDM|+W`u8{!7}Csa2L*E3b)_L z0>OmbOem=A52SMs2MAefUPVpnozk3z`2wAFcmL4ZzM2%XJ;N%h8={y=?!!`lzyI74m!xD^W5T=U2dqZl2?r8 z?_J^_te-&!RhxEi*Zr!pg)#RKaXz0p&i_wpq~G$__7@g5yCX%W)kVgwS}56@q?cg# z(8+hV+Y$jq#M9^spPPTD+DOjP;PD%Uou|<#-`ENO(w(We%rR9hv|f+WPvDl~jvqoU z=rytb9W)&P2)OyrMQk2w8AO$y!f3r<=wsyyXk)6M#5dAR-U1s@>7gU9#c=9MJUZAZ zL})DCmdeCB)aFd%QJbaFFgR(uta8M)NDI}C7mT4x>R2C|zhDEuatAlXrgHj(ut`T} zb3PnAhBK%kRGj!onOW>T7yuh8tvfGO_E!WQ>wQEg=H3?7*%+sh(uRv5jW*KE41|n` zBb8z@l>>mQCzx!kyWtt*9W@lC^RzdGx2^OLOQd)?%gDgi)AiP!dSQFmvs9Dv#!`6x z0yM_+7NaK&%i083uMzwG`8iNJPR0yFRAK=DSIz3NA$hDDf$xm6G=dc|(}$h3-*2yQ z8GS9UCqdSqYt57c!>%lQaAhLf%nR>rI=AFQ?I%yF*rH{ySgrqCm=_S7yi8om*0!_m@l?S~O~OMJS}`<&d$mLLq0 zIq)l>J?0CIqeBnw&3S|Q;&M}eJ{>sN96M9$$)@JxWf zROYHRK>n*ZGGjB?bQn)<(Xo5?(>i!dhEm$$N~p!_5lL}!1e^D<1AHhP!g^4&ubQHa z7+f56{}unHEoz`H%%B#Oc0tdYpEao0-q`%BhLK*o zu)hP@rS@kPU?Z8U=0hVs&AD@*wD&E6JSP(t7wfCo=U{paaOcwCHd|1b`shu|`3zNq zPd1Gctd!)jD5YC}^(()&kJ`x}VHx39xfS7O6E(w|{`7tFQj30kn7Z*0wQL!d0t<{j z*c*J<4INMs$X(7%cK8;9TFYP#!GiL$N~U>}Rvsuw9pP-G5dyw2edyA6j`pV;;ao6^ z{zeIf1`hxFR+stWt@oZg`w?PHYs);g?X>Z->9Z*Ce}AeGpH)E}>d(Z^d4`#*=6L=G zEghIdoxaGY@jKU*72;prb`p{ScdsJ5G%1%PPv+W#AYo9CJR zRCXBR?WUwyyn>aJAgAR>TYoVvmIM^?nZWeJ;jOLpNU>?##3!5lt2M9t zS-qdx7msl!MgCiC$9q5QJiIvG_@nWn(~Wbj(!(Gg+*>r_2+yqXsr|u=^49J#FO^Wu z1F?#Jg1g+4yQQx?d*&k^2a<|<@$Qzls4TU1z?u{gGwIanBB0FDndx?cNzwb0FTR~Q z<_63x(QV^walHtR{5a)8lHD>`Kfbr=`U5DWsM-Jiq^!bmAeK6(p8pY~TxtZ_&wt{j zyhJr4D$%mf96MjErcBC8$IXR50>TKy3Qb;Kih>u(IV>NwU;rd->>dreStGl~H8lM1 z7yUI6PKT>X_2loFep&0qD+KoMY{YlL?bjRK4zIJDZM*H*qiG4V6P7W-ENxU;fhX)^ z@ABfT9~}>?c9c)5&dk z?2>Tsuna!FF0lU6z(bBz>)!)B3I3WW_j=&(u6VF#_bCO|9K1RFxf!8kK@fIKz0*-w z1fC5NKn!BQ)|T5h-!bpqiAh<@7)86a>GIb^!Hv7zmwPTZ4FH!jwr%_6dHEL*hVzRl zX9U<~Cqd~368BGF$qAb{Ia;kUe0B~ls7K`-j%8gC?C%fSyEDC_BGQ?gz#AqGrLSS2IA z2~?kY#&^tXyKb$4dFj0XxgmuiRj-!oea1-z%_Q0!p-LDZDRy+s7rh2VD}%UFw3QzN zmP_vGl0P0vOajNn@+AEI;pf$+R_>L0U&OkpT@(^fs$PpoLW>B%4sy`)n~hJsw$%|O zUTtP^)KCt14ibV$7m2E2h5;hpONS=Td1aaEkx%=gsd{^j9P{Ve{6Q#^zoW76g5DJ~ zX8u>QeI>Zc{-PupP`4j(?e4Xh|db@}$j@-5NCNcfO# zf#!p_WUQ)C#u9(@Yd1=R>?^O7zqSD2m^{M|f)V`-zPpDd7kI2V&K?>*39U3c7g9`5 z($547{_mzTa^LGO9b4u|eS5X)GA3W6-2~BcWl*pzD@tBa(g_a)qMGGfMyinswF=8G zWYIg|8D;Zs`y6+ukuF27&Z&?9i*#INTV!c*JAf7INx|8>fGfwP2!qFUI^Dgkwxma! zk?f*~KWaF7q{&p;F2=HQjrS`iw$k-wEUei{re9-z*eUeif)xW_g-aCSU#xfucGo4u zNT+fJyN`_&T#r&H+`S5CSA}3CwY5cOiSxo;1`PCA`3B^5M9G~ce6F{Ch9&V=Rx1#X zuOv?AL_CPld<%0e5p7C+majddzEhNq1+5)^5OGh$Z%E8q5D%Pm}U#U%%tqWOyy{CZqhf;KZQd(q&V>fyyr7Jq>Ky(BNZ8&C5!js-t znhp`MiYH9c0}`*z>WQYGoc|czNf^}&V$)3^l-cshR6oQ9#asCo-amdaisOiw`#q%% zFIf?1nO!lJV|ay45SYmH=ddRRJ4ZWUw&a^~{=*vB38+>9h0r#4-!km1{m@?=$VoDk zLaNQhwtLu>>DH~~NPsvWZ-M&Pg62{!;@dI8C;ilM+($N>`MBrl{@2*v5Bq9@7lWVA%+ zItX+6wMc(gkFa9nr$LmK?qj>i}DnNIV)6Q|08x zX#M5lLr)8D<-e70aJD4RD_9+90Rc+2ttm4L3c;@ma?8fTMl6xca==|F?w5*xVzf?+ z_L`Ee8aY8wA(iQ&0ucDh)dhAP-VD+4NPw-Qmsnn4FL|yVTp^T(!v5FD)&*Vd(X|OO zu37^vI@yWRY47keMd>Kpt*Yi{HnoRqd}qqMPG*aIU>A0N%ke@Nl7vVgA__>z*eeDp zYCwr;8Cf#RUqBz1{%41QH;9Y#=i(`75x*X01BD$I<5BO(!j_Mzo6vVeTkg)qV1Y{) z$8oLVe*OhqunP2JD?5SvgyN!#G9_C&!fcP{pHRfZuGgkS+yx2A+Di|?T=!q=oW3)a zBlN+lr-v>;_Hk8tqZdbOzN5S(ju3TJVho_>pz49tb1jj1!g)%6kosIw6iHUeBCo70Y>J(Q`x(ltqk+v?)zSHeU*Uz=>W;)(m6G+uY&j>6IYz zR{0bcUiW-z9TQN<4MHi+E%PU)eIwL(S?sKrxUy{+`IQS+8PLp;(lNi-Tz3}N_w{Y7 zJ7fD&7W8NRZF~7CW zHL^@IwW7!^9uu_WsyRwjn>}dl4mGix^bJ)e6|3AaVfBZ}u=eGsOK}=I~ zHC8`Uf`(i)hXQ@so8goC$Bm?)zAp?y8(*AhX}zekviLW_0j*Z-F*pyi+H?BZeF6w_S2CHuhe5xTL~Q$Fu!~I6jC+6z2{D zwSH>;FJ7_g?yh)hM7h2Yq2pza8CJdfq4PDO%@Df8gHCwk}Xp;zFEsrf=ruMk@X0UtK zD2MFvExkX|e6tKGjqKt&2$=Et=x!3X_|l>Z!_C3h>$e|$O*?PvD$KK*i;u<^@|qIPy)r+Z14Xr~Kk*AmjtoQQ+TQ<-qKE zNmmu7)YeR1(TV53@V++|!3JmBa|GS+iKgX2aUXuF`_PO;5(}B(u+h`*ySC@DD?SvY zlKu@>QZfP9v;neZKMkSRV-YoL4ol54d&t+*(XkN7N1|)^IikgezC`xi$MHp7Y&4rR z7gpB0tA#qDA7{2QM3PB(H(+>sLuD|LR!Xd#9Exja+ou}aCqt`u-Wr(2;qT-oC}1eajpMh9zEln!Z_GGV z$(hIl&4#ycYe|ZU&qX!_$%U9?pJtQ6&iv9Ck7jj zV#;F|=-bjjjFx9B#API6iNs=t0DO#}!E4nvevN9`mKbgcd58$v4dkvw+> zTqctc}#`THyEoF}0PCS22_?4hftP9bBZ+Qhtr*~K)4FB~B%+6RdClgxF` zpA!*)&6=e6GmGd&tad)x!@>Sp*{4|phy1~Nq4K0mu9~e?txf8UxhF#nyM5ff*(Nd3 zlf8Z2rBCLTQ%W@)Ise_7Rr5u)&F>)}(~A0ruPT^2`yfn`2h+FGuxyb50og&DjAu`n zq+^D#Fm_8<*`Hk5>RcB7Al5BDrQO`0v6kApwwAJPEA6GqzpZ-N+m=855OE)qu5oR{ zV(nkl^9lv5wOayKAFw|vh|n3MUvNF6AI>VbM_>KQ=L<8r6}e6V;E%_@#EaJ)pCsPT zbju*Ik0@2{YvbDXbGNA7eX&%}%Hh%s%}f+4J$Y9Rim-O-YZ@@a19@u#-EHu{4}!IV5iZ7 z)t7YJfTj8 z9bkL=qbUss(aeEc_L1Bn1K0i?`(s_|@}iEM*>2O%dF>_9fxrD$=kxQCt)!Oh&%iIN z_3_zKY0zI#9acR@KqQ7qP(&Fm_;irN4!{Du*M09vS}(HueIaH!~WXn zw(Qaf+{kWfFo)l_4R~~;${0c ztS-h3oF=_Q7i@l#i%kM?chMX>@NOhkubPLu%3$VU2l_r^{{m3hXc6}eaM!4x3~7^! zFi}}t3@Y1VXuKbNsWMuegP4m@QFuSs*C*OoNC*nE)L0tb){(GtU1qvh@I_z{fEt!?moG`@)vrv`;kPm{d5f7IcJ;gsh1M2UeWxrpc`ccXWi9y zQ-j;%Vl#u6a)ck1Dh)3e3#vLrC9V1Tn<|u~fE_a!tO0jCWc}$Pl!QINt*^UGsdwe9YWz(%rVIpO!#HtX{!O74;VFJ;=%*1aSX zA{C*`zXT_E`Gw%-u1P1`%b~!ip0YV3(d9x?G7okFl6JA8=Adrn>yBN)j>ZcKL9%0= zy@t4?*7eCMStKBHyQ6&OU~c^k7Bu9H6jS{nX|$PNe`>B2MRQ(bOizKk3*IhL>L;T6 z#!GjiJr-w4+4B5!;Pe?1djQa4axo+JlPaRhfQPk4HJ)mw^dr8Hd-Tp_phtBdN8DcO zz!8d47u@H}VZTg+Z?45p9% z=Rxq!LP!Xs5B0l`l)ClR$!KZorTnUB7~t_~5x<~T(%o0}Q4gcVDO@b5!z~IHf3T09 zBd1Sg^ob4AH{b-7tM2z-kYWCuE^;wd3XsYcmS+n6uyAhoz2BvikcRt^Ow_UC?;T$U zzZxTUvjO1m269u&vENe*^v+YqLcYg=%b38#ez7oxi<^X&c6`0&@UzX1e)-hK5vn(7M!C zm2(r7Q2wGB8_+xcw0=XEj2kIA^rK}ArSGqarLBC}0dy~Di2C*bDq^}Eo>@{D6n>$@H+O{-;+iL^sWJFB< zC!&E|Z9B7UiOckv%~JfBZS5 zwa<#I4NlzX#?NTq*0Sc;w->0R%=z@fA`4VX*~Nc*v&u8ncb<+=7&Mzok%4Dn7}n>m zb#D>3js-Pe_bW!m-CQ(hmuFD@2__VAaM~BYwu)#|x>n{s1Go9Q2J(yoDcHGvDQ?v* z_9lOXrVbe-Y&AMquGMIuNA5#Lzxsc>FyU%}>O`i(Pceh?f2B8|(fA-p(W8$5C>Uzu zY0{^+T&RT02A6kKi*!gGJ%>s^2Hosq&R8(F58irxq0HV>h!cgMr>o_-Vemgvk}>8< zpK=HFRZ<=Yr$9v8`S!Ehm&d}WH*#P^%UoVH36XNIRhBviI$-KBCL2{Mh8TR`EMvUE{YOShP2O`@dF2jR5Mc$r7|3?RO@edOv2I2q4AC_oLeq z2<{sRAj`!*?{x*pN1L7i>)IvrWUJPbnQiitsz+r(6e%EN3?+-zD|>>efgt@z@bmloQF7{+ z14uW}F%0CbOmk!*_eWs73>*6xC6#87IhtxWbIpdOaR0lKj0Os}Gc*rlo>$CrW}BFB zT7`{PF`D}u7saY_l@C-213#GW)}VfS*3vDC6Q;(x1J%n{%tm~@F{r6r-hZkA`H-_wzMg~RLV;( zK{A`owTKT7eZyyIph`yl^3Z4MrrP5E`O}l9 zCt%val_-&R&QUmJW0w4X6t14L zU0$gycztFN#vWp5-TAJ5Evdow&{ur_yrvpVSA_1&XI8%<&1GvXbK0cZ28FT|DzCo5 zM3CqQ*xEk`6vf40)A&!XJXv+&n}tp&=4kw=p0;qTf_rBl2~sW`TIY2B%+Mk23GyYp zkpa)N?S{U@1zD|~4`OFy<}9w)#@XglrPQ=aaezTAChZ_JKGDZ#VWS>QNzGR&OhPji zSx5mJs36yut|W$xpJ%eMN8I@wUnfZfSyNWnk+HEwL{IXSyQJyU6qOi=#+ECRugPHk z3@-EI5T}tvZN41r-0HoO7aeXK$dAhKBH?(Kb$9cu(?#-*ko%vGjy&7fPv7Tu{F5i& z{K4p`6eiicq#0Vkie4CAdzm7jC}Y^S(g1ezv_M54d73gJo-pl1NzJ##Nm)(A&A8)L zukl$=ibCnz%hO-lHM6xS?(WAQiQ93ex>}cbt{Q!fha+@d4nNHTx3d36cqJ+pWFo*W z@w#!3zUkQXmIu~yWYpfv0CqsP`$r?BHtQl%M&4zoc~zIuUcE7ao>%CjMy6OfD%SE8 z!H~ii2E^5K3a~76aM%y-d!tcSgZo!tss<1b>Mi`C2}hrnpX(NL?@W#A6<9rL&h6{f z;!!H*^{EuhS)~e_;rqr{&)KANpFxT~vxJBA*>y9+oqfSmCBm?oZq!l8truIsM(dR|! z1ZppYEN^e$;~8^1*OIGZ7$Vg>^;Pg>iaN-OPL57@~WqQFA$rz zKg9uy4Kh?DR|&Q51T!Vk;#mDF*2=+~7X63bZGVWS5H)n-b5ZWwB^@D>X(&cbm}^->W0V5f#O98EstucAxmzl`BOwjIPuF!)w1R8C)uBPKSvJxu8Ih6-sa-8PZNV7DOlxUk-Qu`=`5Lmh|RK z3>y9bH%)!#Rd3dPXE&BJz~jo7io$S)6ffYFa| zwyOnoWug~s`)Z5F_WUfuyf!LQGa?6GTa*)5WTqGQ>;bajRB!&vqi~l>^xdEU&J1j9@e57I6dIJ}}L@rQhnG zR^EUzQr$u*IiQt9v8MAoNpSqA1}Z-nu71L-&cR~P2N{UgJt-B#L6Z# zJ?g@JrpcM9JRr94{K!Mekx&z zY1yk*JtUAfR;pbV$U$7=tkcElDg3Cr6zh2V?$3{lPIT<2Mc)n`uz><;mnX_t%74eI z7EE@?e@|BIVMZ@FeWtQ+HRb5_x%K>q!ScJHR(v%Jv;pZ*ayjeDXn5nLQqPIbND8TS zs&n2xY?(jnoRxFJ#3kja1L$6($!N%!Wco%buUM>3fcU zD?>hGuGB7$7+ne`cR&A%EB9^ENP^eM@?|gN{S1P&dn=yzV5fR6S-d@EY;6Cfw%h;6 zP(>$Y_O;7bpu>z{1|b5`K&Wf5Z1*ue5vxYbD68J{vIr7cH2x!QG%_Rv z^0R2HE#$l~y7#Lg%``)*J?&)a*V17UcC&@^Kjp_tIu^VI?#GqoTd_$!v*X=}H*7%v z?+tXBP3_NO9$8BA!||-$+b-<)@*(g)AVxe9@pirETRoji_Z?aC;aKGkpaX~JeTV`Z zgT_r?Vjs;0g(mh*b`ccr{AS9IEZ$4?C(BbG-BGs)qXuC@GTeY;^JzI@}#snX~3 zrvkqbBd!Mxg7OtwlNOX$O`!8?9{c-4g{br^*#6PS+czS;+-pg zgO6(G+ixHlyWZK2o$AT}PT=2`FLf(#Rx6x++z>L5uSrhlzTa*m z`~@Dm%Se-Q@5os}|L~2H2z$?e-;~Q+#dewGwxLyCZJzn0$8Q>p1hwJz6_dA|JZ^MN zULdXfLQgvewNhab-jhkI?3cXDJt9gi6Z>}2R}7Zx=EnP*H!J4WyuwXn<_%1sd`}95 zZWJN*+*TUe0N5dwIPayHtkV9~ZI>`1@0I)Mdide#d-qo@Grr89!^lN6nNS6TcfI}c zB_1t?Ge;QDp8t|bqsq4p-hxwV5ImB(FlO!GZyY_@Iq!4K=w1b>)edm|_ z$q#i6-Jdr?IQHnpPTKw2@$zfl7r;O05Q>w2+ja4E*jTYle~nQiIrWo|K`;Bt)?JDy zvCO3Gbv%2L1=H(XS`f?f5rU!@k43~@hUJ)x*j4)(y5<19corhnriv9B#=y!jOe z6#6C+A~=Rr<9<0D2H|I*zpH3BzRI8b>j1SWcr5Kc^+3Nq9W+Or>h(J8FJX)1k_zFq zcd!9Ztj6!GqK*XbwJg)xt!N1?wcFrnH-n(2TrRT}>DD%S;S>D6VR9nlYf+Ek1G6k( zXuTAcPac@eE$h%&5Cy|n}B37+-C;_$WIt?RJC+aZ8k|Le}ZqQ(Ll|T+x*oH`pvQ{P@wj+LVMu3We6i25feqmA1Do-;d=P(5|F4=QzN48LU z9pQIuI9mqgT?^m=yL2%gu0&HO= z9Fh0em4cpi&~=tFQZ_@vU4+s71Yt3uyvfW1FqxE2?~fXU(UvKqIs9N-sc}} zC999dypghfuzpw!3WJ__5#2ylxxuSDbn)ZJuye@O0~iGb;z_=W?`LV@|4Bsga{ojw zq?W(KSS7d4O#XYmb$(cW6gCtEjOPjZf&Yb4 z{DBwI!IHp4VWGbWo=6%~NV~vDKl(YPxKpXep5}PK5&5eS9kDZ=Y+FIh+==dNxn<%4 zJS3j<=t@_o*irB$2?Er_{-rtF?va0cw4xk?Jt*9kXC zn&S$PpN<(YZrl$Ywv%wiga+neeH1+5!Bxi067kMP>^~mycp=nLOmCwY>oO*Z^~9v3 zxm<+c@|HgY+*wjdOkatMYge1?XA^Pao!7RQxQUWiI8SvxQr!IZ*RB4ScbP>Xq{qO_ zb)Tt$I{IA>hT*=i)4__CQux1*@#7!)YS$S`APJ3w69eJQTOkAx6oRxjveVgi2FnB8 zJUlO4-z-4xG+Q}H3y6?hJNK*oJ%kOuSFY5MKqe5G{*OUKka~zlDvNn<-zo763ps#5 zbCiqD2Ypm#U(nvDB^N7q9bR73p!WqtpVZdh5CX%5puzW7d!zsTH?;e*5l!?(2qf#j zQyIWllw&S^g|p(?ydB0E601E7S)vS@1bN$=j&@7w;(}i_WDwyqG5BKPn-dxj7gVvVGS0SZ!nwrz;B~_uwV*FN+FVg)55Yg_6rys4K2z&dY2O z3}ozGR)FvK)%^kR^5Q?}l(A9Cp2dj@gj7C37)sbTDV6eTDQh6C>cDH*ir8!q~o8ubMy78eMVh1i!z^K zy0!3t{{vH6rxuLyGci)BTBsph>pT|r0kriZnmQo&W}rjuGw8uK0}Wa7Lg|JGm$GiF z8)&%g-E`b-9N``Hs@X+Mk2fI zaw(=M$cT!dwZVW8Bm3kWqWPh#X_P$DY4x>Vdzh zrtNP8nDiTPoViodSe;xA?;Fczz{e}cd%tij&(`5n+bR^fFbl>#5k|lV$G<^-V^n+%l)>vMwI1Zri|se_natyA=fcpeW(ltM(gd1+ysU zc3sk&w1P7Yoz_ceH|<7;M9R7`#ucW>?t1YT@_`cktOQGG0=AiATT}%ni7yWPTZ8om zpGLR+dw+fU^(lCvhOBKHrZtZ~&YNFDTynjHKG~7@^fkHZjiA{f%W zfvWq(ROkC0&{(+dKKXxWItzvz+H?_kri$({$R%NrZG+<*`B^kuV@jnBrx1n)2bX@OXof5$p- ze!G}9f$5vx-O-U5H92x29v3$o@FXwK!MMu>?99z2 zw^x0+xew6let8d|`MKsds{64H%)*v0YkeyR&=<=Coc*QF;74&kh7=q(;~Fy{3dLXM zD#$OdF5dLCYW=UgPmhwLQ(d`$%R@>UcvmxMQhYtZ958H%{s&Q8Nyf2HGz`h~zjrwG zl8t9ll8{#t^iqu_{b~#j>M$jx>4)e7uz?o)uPo`qQST6UTprXFve8M{?BcN4S-J6G zGtbyy;@s&l+b8_E_xbVk9TzRk$&me9-&QLCv<($?e_l9n+NVs(f7Oml)-krhFzc~ zD(T=I^gxd1H@hAIp+f3IVO_oXqfWJh>%+)jTNAG)bWtIRC2gkCd!t4?4WxgLYpT8Y z+j8rpAVOvz8=M+t9YpBJ!8yB9VH}7*wBV+m5_uXtix{3gS1=|s{|UxgD=UQpp$RVO zuW=opLZoJC6ot6xu*t&T>Z7E5JzAJNaSQz(z`P~+L2;^glhHO)F| zu9(dS(ZR&h_IH8m7`yO&TD;llC38fMPnKVMzH!wrCuwEy44mpXn^+aT-+E@2;|6^6 z)00w?z~_&rRg%oLph~ht1}@Q*9)cxDO}`5gRC%(?o^&|~CB~Mkus+4%R@Qv%!XGI) zx|YWZwvzoY{xDmaC*cPH<6EJ{ELhEzhd&bYHGtDo0ly?K4x|aBe_R$9^X z?Q=30wKJ0L`t2uKLMSm^x-)_;csM`W`%|C^X+G##@r{dgg*7-_pmv# z?RMxVCu|MVyPwCRBrl?K{YHNvQ}gO;PRe$ZS6}gpd^Hn4my48`pe2@c8_br-FZoLnl0W_bb93@4^fQ5i)P3& za8daH<%;F|=jToaz4t6)<$w8+;)V3T@fsgq?%kifnmECNSY+bn*_AB?)Uec1h%2r^3xnK3mb;>%uFc|lS?hr#mOeT ztDuyd?@k}qf*L%Z!l0z;+vJw`&BM6cU~^lrl!N*T8Pb|ppQ>c5%(dOQ!`H%qW)|(6 zUW|< zCOnrM{N@yF)ZGnHNJ$iIu{TLBme}({kLPD@`v*WU&=+)uR1H(pQa#LhC2W}qqj%9R zd~^!mmoN5}Z`aqJoR2!ERivgu(-FE{{yDkp7)|-gg+U1$S%WnA-)=o1G z9;^Rqql?(cK{g9fA7_=3_BTra@F#!?t$_K5*Vok#`khT?__UaOfd$-Fr)TT|L!;S# zh~R+AutG^mGY0YALJC5#K~3*R^^j7cduxUCWfhGU2!W^eI`2F5dAb6452MgE*b6IX;N^T`mZj}xShy7uJYzku|x1NI0o+s7zB z`6D2=BGf6c^ZIs6gHMG!ujZf-H|N(exn;6g@^kEOiNja#szACGF|3Ii*2voBBcPn+ zJB|N(I;e>a364lu8>-!;f@X!dh?R9c&Y$^*3j2gbUeE^eJY!lw)yb>#{xA7F)cyB` z=2CLBi{B(z`825Ql1GYg=mJ$H5fm+Wj`P_(|nmkWHOvO<$t%9*%WH?6Dy0# zd%*aXG}&I`23uR0Z~g?J00mtB>pP>1rEG}?4(QL)5jc6uE;%v;Ex*8BPe1vO90EM}gp9^`g^` zK#p^0R7eYG>Yfv1G@3&4?i+ITg~yh!viLUV#47GA!9QTG{0IwBq z^y;%GKkDKJ361$*LcQ7Fk*hueeyP;nE;;C$hMUJIQAfz4g6&*ejHEo_=ieVTDH=tw zbWkCWs#f?N*oQ1P#~ZE=o7&k{Ni{~7O0n&YBb;!@p0+-{FX~SP$}P;t?y^_9vVFRb zwN6Dqlp^bS2_`XmxYn5c)~#;Y>d^C?!67t;0t~a@yHwNCIP7|i<}1cjPi#(C%<~`0 zd?uIvZH{q7yUD^;wCc&fWB35^B5Uh?p1E%QG?NlB_%>xaaLb?j4QzJ!ouQU-0%zrm zLy<5)D|9fh7r}5TY=5!{6(EFj8CnpLl2qAC{DYC}c21E2@!R@lJV#44Bi9&}`Blio z6bCK2?4w+yb>x!QJAm)C(X>$|r^w-PydWK;mFr~abKT$#FUhk;owPOY`1B-Ol+YTf zXHHEynv@XjBO)9rL(@cI%p6V znf6c8T)o<^JU+36gi!yM^x|C^eFBLuqlirfHr49X$8?AN(fDi|>`g572$*iO-nv7r zvDl(zF=t9QbS=L7Frht(_-b6>O?P$)pBmhvyBLL)qZ+1R>ylr{&c7F{J^!Rwy9JKX zZ;cB0dyt62zqNlTQAxGj9^E2Oq2fceDMu6feD*O$i9hb<3(*kBki>{dksv*t=2NmZxTvp2jI08 zu&^wA7O60pd44a^zs?io4^wlM55Ne9jOZW+2oKO)zL^Rt(f*K%&ncEn(S>CD0A((xTR8G0G_E{<@82D7Z{f z(+xarfmrg?bz>uuW{D4a1d%^i2wEUuvaLNeXupc)6xM*~n3A=yWI zzBT+#8$E!hgIybdf&SgGkI)Sp|52a^gYz&EiwGt)e#XosP#~;GiX}#SkeY{0 zl3&M!VtmA=D~gOZ`0(dC3=+`~DoKY8DEkv6x!umvJzhSre+pzv!$sdvwTCgGoZllB zNn}FJQ?O|RRk$ChAB;pVWnesaO3D9FzJ+=yOCRXB66k)Pd$mx|io#i8=z+$@pa##R ze*EzU=7&i?6dIJPYs})QEWbwDyT`)x0KHhE9Da1Mup=L0o)QJty0T5!_-Nz@<1#WQ zS;^W($A9-WaM{&Yg8#86O!s2Xp4J=f<+$X@Gq0NWOg>G(C3%iIUC2x6HyAplBxfye zFWIY`%*{KOOVyW_|V0A|Bc3Ic3Bq9u9Z-|yMnB%Arc zpIY=S!seZAMAv0#2S0K4tkIE6#P~I~bAvVi2;^Xuft^6y%l^u5Ee!1Ojdgs~2gvH_ zhIon7iMzW}atShoDcQn={R1WMdcsOzVPa1-5m-thNcwhf71(~fI`YP&iBLiFITlV$ zaXjdX9$h*k{Au?r9#?}Zz$t(fP4p=ty$ZtCm3fo(>@2c$yv(lZi#$jLow$;_RZTtk zChiVj+MaYfNZa1_!bw%=_w~K(#IvaJcfLCjA#yg!!672*Oqp_US)gTI*X~S+CuKkn zY`|=9ZioDi8k{_5W3GC;^hwT@W7>puo6samTD|dZ&9)RTmI_3>J-g>ECC%#U950mO z(`}EQRPZK$9PuZUJnS8A4h|OdS==VVnhjGzTh`+VdUgJ8C-orUIaS%iOM%zQ#cNCH zh}e&J*QlbRIA_-=iBo2R`3rNB!LHRYi!W=yJD~G~XtF9}AbGp?=5KQz{qhUv^NWA} zXF5G_*YrfWLJH&3#%}!0%vKh|JataQ`s&_J@PZ(f5ybWnhtEF0qXzpYmWuwQeQF8?Vt?W3F6 zWBj;~WXW{QV=e%{1CdVZW<)ZVXk+_N2#xUv#FmPsc-rut$2fW#)h~EeB=Xzzx85mb zpThn~*%jVvgbk-T`s7>9&CQ;ePOrZT7#e+afu+gFnDY!`Jkm!wz@}!bROR-6a(kqI6BK+ z2#1be_>l;@h<)7Oh4~=j0AjoDYv!v&DNElgbrUFupcvCRBaBK4)*2;Jd!6Cj+UiEh zrZwGEW*x7qd(I-OnSMz&JTuCLaxDU47fyEkP~g77_g!Zgi1D?!X(u6 z2r3$!4AT9dQ;<4i$YY!bw%ulGTY9c_tzM`}KCXfku98mw z_Qv2KGl4(a7XNdB- zBz`QD4MX0LxTnHp`d*|!`x@7JB$9k$gY;%t>v~dr?D|P`)uEuX9H|F$dL=?Ynay30p zfBn^M7X~`T31aFwIezUjvlue9|G0G3omBiq=72VmnWtjSNBnZ6z5e@8 zTs0TRLiLew`VdF;v?s<6Bah|_;SJu|*ya*Quu_q=KfEn=CY4yUznUul(;XhQ>|5st zSwgJBWii6TAva|6*=-N*@KC?^q2Ig&()`)*ZtL%5Tx4e96TvRssa4O4DCNSFrkJn~ zKPkwT_|A>h{AIHA6(zH|Sri*Iwx@d=Pu6kbT^y5IUd0C>s;Oj6hq6v9U~IZU-xgn( zp~MqcB_)(~A+<=7LqZ8uq}j!~AD41akZ{sn2qz*diQ^ZMlv4fhdZSJt0d?J|Cms4M zD$Rl&7Ph}@23$13DsnsOp#Jj$#gL3rMXuRS2suGMWD2Y97R2+M%i*1g$(Sgvy5hVL zUT^Vn?8GX624A)>ptvnH56jj$j;#P;UFG@e>;Y_iiHueW*_9QMJ>6Db8jL(@rD-}P_F zi1oDQA*LKwPgsOc1)nRGw4!~$FlD#-uHNZt*va|%6IL?Rb@2J+t{=Cdd4YOBD@d^y z8~AJ@FfmmmN|273UGS04V62EUYzJ<-E{}o8I!#FI2v~R{p9j}DQb~}BiLVjhfB~Ed z)b`=s4fC5;0WeaA(O(Pebwg&1rV>^arztdzd~et1_c7Hvdq}ht%VP($UC9z0SSdDR zIg#5`_k4b=YOf|Im|DoqX+vlpgH@61rWmJJ zOcl_|ln;YO5%z-q?U%^~;?D$PGI!3jKcm6lJ$m+%V;)->Q`Fav=aL#oqW7)lnBaD# zA&zKfd%6^S+Ae#j3puN3?vEM5)%%tc>zG}TG-!uvCf3Fo|s_e&E z2|bfN1Ikx#uRi=GBjR5y-~jJnoTovE6Z0uR)2#R0ea+5^6-%-RGq3X72akT-Oi?6% zVU1nV7HaTzm{dW9mKTxSyL*?@|EO>aqmIG#EX?H~&)P1rBjZVYW)A+BLio*R>;klx zka^qRLKOSK$H589N|Gr3J0%iW`Qa}I`Z3e}k10DR0HLtycg%FtyS>-6&FL*8;@~b) z2k&>I_Kd&A+wQ&GGt8pdoH%%R&&_U6UplOb;XwtPm_kQ@6tdFt2n@r`gT-e2l{nFG z0|~rK&Ge$<5l`sY-8*+n62dy>q~Nn+Zor}Cw83Iap@r5e2VTVA)d%0_q%!ULc!gGi zCQ;qRfnT6oG+5$cu&)wVATeV@LBKwo+3+~mciQQoeFi3S?;@}0XrELQt1 z`oVM325VK^xzTVXGH7_RC;JEo%|g^w#JlaOEZ4fT=Y(Tn^AA2gU#TPdBl7ycwheRj z#P4SrVD5Jo>&BtA$~>GYRy}yt0iVzA)Bo?q1=XCxq+%8_m}&piCz+v@o`RSgHKBv# zMw01%RBa!^t#T~(Zz$J0H=u0eN5ho$$er=%Q{P2El3fV3r$5~QKh1X_ci6Ku< zA3r?PDD)yUR|!N0a+=&5Y^Kcda7d->quPf=suko44@s208p&-+T@3oYOlu|jLE{`< z=-{E{)1V!(-{BR($AvGnh9}I)H|hjE$$IhNCB{Y z{y^9&NW{|^L!KKZYff$;*GU`tPDYF}crhx02!w+%i6+Ho2@CL~II(n(TCp6O^HQ;0 zEfiqGXJAUC!wv-u?MX`+zvh>w%9EtJeWzeU_nF3RX5y0FI2p;(jfxjC^R-TD6f-Z* z75lMWpS2V_vRn|aWEHr8#DjnwU?RzeZPlN|8T~R_dk1?005k7iQjQIx;Rfu@^!gNb zv%Hq_6>R|~t{)c~VNvFciosss5^&Nh$gVw3!$Tuf{-lE|*l6OfYND>d9xwjLzx^yW zUs+tfBkr-X(ag75=;eY0!7JoF&5#X3B5!GH{5xxau;9h06_f%W^trf6bo&B-SA$Q= zQXvvp3osTNl`BIcV5X3N zaCc^7Jz{$-g)g;RP|9C-+m#=VD`$BL*1QG$b3~R(89u+>GDv7EsAjgwv+x%@vKg($HIOt18;QIEHu+R`S0s!hX zfYx@f43}O%`}$fbBPo(FL<-D-0t4^SGLu=3GkE(-H^`>X-Ni1|;e}yzR;ZJ^X)TTI zrY7Gn-{|2?DIL-nga4CxaDVW(jn>`^IgT@kR;!aL?%?-!(KQ3}!?T%*FKT6tvCe@o zazgQ10|%58ZD=nmCn)DX2luzRUSX1t-v$ML^G9ig@*&j{;`B+B@wT)J&Ei=tZD3bm zBrps}VTu|ulnw}@g>Mu-T+`p=-VP$yz}=yrWn9#&83+NMeDf{-I(VS%mylGaX3d2o zeke4b!~Z?ao{*WXfeK7-D}0M^@`my<#gN0ERekt|l0~OI zn=hV>4l3iqpydUaJ7=A#=Rrzi!?6*Vm7QPAJvgujipKiXeH~Ly8~y{*O zPlZq-lv$VL(RIPscvtFvu7jADZ}V@!<}^Qrj6mWlZQuUIxf0r8&vMs^zx9arTS zGj6x=_kNo8?$vNVhPJX9Y4J*k9{oq)6N0En63d^6nBNF_LT4sC*8nGz>-P&j!>5a4 z<*FN7wjO5)QyN=z38@gUL5aFFN1X7=?Y7GLez=qPiJA_UaWJ6p(^k+0Y z7czBzaf9u3^0@R~IV=tgHAc1mDPeN&@k@6}XR?`zadKOF?<(RcF*`NWmXp{QhGa)< zHqF0PIKVbk)QzS=SsAR>a=V**$o(!r8h8wjOvPq>ht1aC3TuV7+o=F)Pd;Q~D9NgSpi$VgKkU>5=*za4*~>85S-|fqmY`zLt?&2eJZk-!h$U;n!i(V{ zze9QK#QhW*%mge7X_Wu_j5dG<$i_otn1Bx{V|6TXg zl#&#}_|Gma==dYMq-q~sUxxqZCx?Z2MalUatl8Rh+N5&r<^)id%~(ZLQM$n6nO^|V zt@W10o(%SVu}X>lO+A`Xc#UTEM;VFJmAvrG7;AFeHc}{ zTwprxem7|^AU2k9H4G%-1?sQ9;E!<09Mc947H}>h($uooU} zggxI7K|^rH*KES@HhE35@J z57j8UQBvlMR*4s+(VSF@y&ZceBXO6;ZIb%2yW!l>wr({^5ust^jpx6_p(7}(NT>xE!$LQ?g(Yj)Y%;tN+waDYwvahz z^NT1qxVfm!qp&V3IuHlaZK2q~hIzCG$P*OJNm9}(KQ}z?J4eUO+n3EXx!Z>A>`Sc1 z|7AslFB5|30-qNE`;*j%zo}Ao9*bxbP8n)9+bT8)5v3tO??w3ucgO1`fzn4Bk^g(M zl5`6Mh6;)WnQiVg9u-wsOhtCaASyIw_~+2+&DgYAP;d(iU1W}-&%k%J^v`!sC<8s* zG$%bIO@?N@L??|=p>s;FKg?U0DY4v^@=RqOFAP>XZrl_^=}$>QL}gCeBh7sNrj`&0 ziO>)``~N)%SV^J*riOOHqpLD^ge;F%_P_a6o*idaNr?TpIEG50sIw4|eySKERrZoy zZ*RVl_F5NxuCR0Isc&a(eA#TlC`fsz;=miWU&o^r15scs$zwl87M+w{1H=y#1u6a8 zQb8taD9-jx0cgGBt>0I!0!+(uY9)1rb}7MT47{S89~0!V757FOnT5ebQ3S zzm^)!0zM@@$R8n(2f?7tnn$T5JO`P}xS~t-7iAMzBW50F?KNxd0+@Mp|LtMutRx@r z+*;9$*p_-?QpzqIt7u%x;U7QJ2)^H)nR=kW=M5XYMs`>y{PEG^OJW(Z4F5@!B&~?1 z*MALxG+uV}&+cVBnE^rpl-_D=*?Zp#JzfMnfdMNj_s88bm+DoqZ(`!l8Wyn6Gl#ES z2g4ykv>UUoLCw+267P6pZC{c~k)>Eh2C8?~BP^ZN#NZSOzJ-4Pmm&GPA$Sif;QV#+ z0m{0ElZp6$0DdNIt_==Jy-y=UM=h!>qSg1sPp&yJ;+d5sO%%AIkeB?lU`F4QR#lJj zvg`6#6R?pKX#!)1R&z%zeQ1q8(=R~Q6We8l7)%)((i|fLaYdtqc$0+wH`Doc3?Ri_ z$kI2gae{iklL?3G!5ne(G30_`%6-E<>()C@T>o4V25L&599@q%6x8$*(#SY@kpiJE znv);h3_s*RSs&RHR6w)qNI$u^$Bjgzvgt5}f;*P%Q>}S5|Gis6tFKxC74&5SOfu4) zPip%A8+2Rs>|07I79s5@rA8AyS{^)@xd~{9tS@op)wPHBS-bo^ZTHGT_CmXC484v% z5kw&Tlf3qV;A-Q`T)RK~k$=|4PQ;kGOW1xW(A1FtPZzz8?mN}ioT5Z`m%5efANy|&?~C=b z)0JmIV44Q+(RzI|R!Mdll2W#J-$IZJT=y^MVqTVv!n78Pt|vajF&?MoJOVs)HfptB z<5SQ{s6(IpiwOT9k<{ClMYu>F7UNx^v>G5^g5`=7Eca8j$Q=v|3b4B`czC*JBtYzl zqQwsghqjV9GaRIr><;31(!SJm1;PxYZ2Xp&AW``KZIqZF=+{)`^~U{<@zRO*M9%UH z1H=XCM6ssFEaZd-A&!e3p;XBW$71~zIV!9hh|omHb@q0by;tZImrBoWteNF_jtWRqXKok_igka-xsGl<2Py5r?CB;R6nhs z9SL$YVasx;(z~}9^~VmJPRx(>1pU}eg5f`R=^ruwYNmVNOug}mn(u$xn-6d@W8ZS@ zRb}N%hBx}8Dx@n1|3M0hPuXRO+M-^@IB-1Vi1Rt`Ny-nj)NFu$0FK1|xvJ_-%Ll(_ zV(g(YBjxPo%K(yC)p>y5f;aZ#ua!>Bs&?4~T4L!EBJPb8_`IbbGX}L8?MvPA1$TIt zVVr05?d-VNh-T|reC^=VWqHd_nNB7(gGw3@)Jf_8EaRCsj^w?oiqrv36NZ{eA&bF# z*)RL95{Dd(Uaf&TpcuIJ?VDW2{ealHVos)8is7s`yvxmeO(at*3!o4}5&hxSW!@E0 zJ0bI%k7b-|braG$t&yw~1QOlS9}-U%vA?SFSG8j=^S0AGWHSEM&wNPf|Ip2~ar8rc z4Bg2n`3WXZ8YyH?om-~!QeU%<^-U&}Rc4RAMwJyq?&qPH-B{QZZzj&6g(+1EZ`Vy| z>yHG8H|A2$*=i;Vsfg-RQq>pV?%P|%CEp8Iz!SpSxms8wLJtCdTI5WZ$v(K>-R1Ql zjiHex#f{HhYBf|CEwNL=cyc=by`5yv+a}>fN8jxY15}I45WTRj8uj~-WdSJZud%{; z0~EzM)3g!hl?lkrFFS!~!je4I{9TV1j2)`1%hfemH{kU@amam*aSnkMd_ef%0Ca7t ztJMtU9daVs2Znzd1la#O-QLaq5$V9hJzqFL#!@mX0kH_P= zh?Xrm|L7XUU9abJN3?H$Xn9uWzE7J^zvmn<{-QmTzRz(i6%TvphIijTWW9sBGLEX2 z&xa`P;b8ael7h`43P{7Oh%mMG1TVFVfk+E}6i7N7Jun4(>1luavq%$oEZHKEn85y` zIJ@OC@18%A2f#DQ7`IMA!UP;mlO%C_(9-i51n0|l7hE1%v3ozFM)BSJxIp?a`Da}8 zX%9i!XloR?DLnZ{s^f_7zx-oWZYL?D;tSXJRJac~Ns3=wY0g1!G9M$ZI)+`&aHw$EQctDx>9v6MkhpF^?t|AEGe0!!Bg!VjRU2Dx3gprj-3^H?M0 zN%i(o1-*Y}6(RfQ_|hlIEoRS9K%h+^=zh1TCSp#ubO(xj!-F_Zb~T*asXyx>R@bbR zKjIyxA?oWlqOcCg?3ERbwhbGV7vD>?CKV3P{UtUL!ukH6|HRgPOZ{C>j?3bDz8Mur zt&c*k-2cw8ED%|O;Dod^xX#1ttI)G2&5oY8TBZs#L@=>Ac*iFYJV2eaw!xV20f^Dp zayQvWMBYDH7wEAbnLUcU`$ixk|Gz1XqTA|-kj-rNl@4TOr=-A0_Z-yxbdi}@w^<|D zA~)~?--asIP5qfHC+32RZ5qGtBp>^rum4KbUf>RPCt~u44M9Yed2<#rxj!eQXbjg2 zeicx9C!vP=t`r~y`oz{*O1_*qH>=|Eq&ZhMsu2}w{PS%V|MG{d^c#?_hHrX<{-;I3 z%m<~Sum5*4ip~AB`owU^#;<_^4Qjhj73av8R)H;R+tOF&(g5!+ugt-jWihYx)K@6!l^zlhAd$6QyDXatd=cuGeI?L_ zK$B!(-=m*ZRhz`~@j^$>^en&pneam1`^@pdICt?eR?{K-QGrD=;EpOjdhxFnA!q@G z7QD@cr-j3g^IYH2FsxeT-cmb3ldH=qpy5fvavM&8X(7_%V{W_S^bJkiwyC_~*!X-X z`#(KA+Od{Gr*cTudR&o?Lj*ziPbt6+tF*?}t)*MSbKKXHMsy4mivS@?-jnX!K^oF#U zIY{*VsnbpCTB#N_?o>g04IIKD6Y|7df#cJ?LGY>e$NUe}KLgwkD~wdMXn{<qwd*`k{mC(448!_4YD3-*ZeC86gx4nRUu-Msst{Yp$Sc^brbs}UZgm4=*^Iy3 z3@Vqys9U%4ZCjl4oSk8!iViI2_pgji-oOnD_pTh||$1H-{(Bp%+sH*g8U z6F~B#kn|WmG`~a8ybjG(nPnhHziD8-LD%>>_)pD$0$E5#tmm1I^M_Lv!wS@MS=mu; zr0n*=mV4`)_gixvEwbx{d5g`H1CTpLBsktylSThLSv5m1}L2S;` z82P=_(_c=eMHzdg9jr1Rp1)sqESy7h+r%mer~c|EcIYRDlP24^GGr?5)cc@MNm;qD zc!py>iQFK?CFSV{aU3=LD2IYln&I;~}mJZSG@!}r=)%UinT=H{)DhJ4K#;ttV8OoGxnV@P*uly{Dw=EI5%3oc z9Y{7Il);bdbTOv!6*F-{u_28|m-|_?cNkd;s@d~?tkP}*+Rp<7o=~7F0esgH`TWCI z_e**qocy!)cwJ{8AjGv}f=ZTiDTC1du6GZ60in#**o)zTq@QZ;Y5d{URI$7j_91+H z)_j!`LKL5!JSCT#I|H%yX#Pq6x)vqhXRlGvcE$_drP^@@aR^pOyhhc-no~VyzAE#Z zXv~7!-B@*BpRZ=@SoU-iRcB>>VU%m1G z>&P}33GEEd!v>r4n_uD%-9DBL-+OL3Pukb?`m!ldr#dxfvTU5@?Ha!1_VFtVJgzjI zWvNt+r`u@gd@h4=UgK+gy-{mpyV|8s)IkL16P8C{M9l%CpPrcK-o^BI-P&wFcR7eCdYW}XlEBaOs2{a&Y1gw3mL7z+Tc})5-}Y35ibNv3a3&0I zZ{r3~StxzDlra6!gRCKXRwzudb1Iy)be4(=P=cdN&Y&4!^{n#OS=_i@4kjF?A$_-C zZyTDsB$A1n5Hkvb#JWeO2Th_eOIXO)w+RLmF`ti?snE=JVCLz2FXIkqFN3`p)CzY? zV2$dGfnoqt@S&mYSA~?vOp-(>HoY~b%)|K0-<;u+=SEngzY&dgd$5*N@@4hSC)jqe z!CZVR#J(I*a53oMFqY)B2OItaV1peoSIzW021zp`p~zhs+EU=UJ*kVc2~W}F%TO&9 z-`c|iuMvKr*K#Lz8hsJ_n(m|ndM5eMR-8OX8=(|%Tnv8=%tEtSg1;=v8W-B>hjWHH z@qG1KlrH30g2KdqoQwnUcChc?g>tcXp*3H~dQ&wwSqdc$jtHgXIa<0EnP1T-=}cRX zD@I<7{Jywb7AdkwPfR1OVr8Glh6_=@P$wXNEi~}#E-3i>F5hT+)HXJDFET`Uj*c5# z?2%nR$VzfPk|YNGmPDCj>yXKm>tvbM)>z_Y?OGFmNf?uuX;! zdX?@7&|p3^zmoRUb7yq?=|U?ssWL=y>1eZy|MJR3W+-&B@bde_tMyR+az4oRlrQ&C z1y*bg22t6M23XhNQ827a;nP2EO+T4`B5A(Hc3dS4Lp;sv>n-A*DY6(_AoQu|NvAhs zmmZ~0=(bVs_m}mNxAiFB@^Jn2g&!UP`zPwQKJFv2-Al7zqE(Xp+Sc`h$8COn?LVc z4}!aV)a>28NBgVb!azE{93Ste$i5_besgZbR4S3lJ$4*=;NdBZ%sY0Ed!-}%0Z&}M zh)9NU)j)>>kq|X^qtGW1L3#Pe2k!>j!&$<@1TFmQTZjoF^i=KUcs<|T7LnjxxNMGZ z8(5Qztiukx0n9Xxy;Gl%9_%H~cy6bUehBlW5jF>Y@w+uJA00;+ozX;HNk#lYKhfb? zcwp(8K&o|-T<04T9gthR=w@_=ALKkyOQVOT*rjn8d1_P0>x@laTJxq-JNSRC$U)hA zN;v`fG>{W~?1Fan(&wA{=V%wn2EW=`!8w`UA3bDyqkZ_E#$2 zf50I#%w&V_uSF<);_%uh&*^kru=$O5yV%(hQRByk>n5$l?Aq!*)__!i#dXGr2_{xF zZPqJM>vp9nzv17Ik~a}CKVFKGjbprjTcMcWz${>hBoE$etB=E_vx4qgn1^T9QE_Iw zSTx!8@1h!#(uMWwaB###7L1=BkN6iv#|6!qrIbmH{#sl+*ru9QkEiH_@tl1bGE}NZ zQk437Obm$>LEUDQQGA|M3a?M-ORo6s(p(8rH!hUrV5!Zll5e~p!-=y8I4)0Z2Id~o zt28WhY4|{>TF;a?bTcm|7!!i$r#jP#9Z;q*Yr17P5Xx-;c z^`qubUeBf;L@bysk@uym4i+zlm#7cW<`ksO*w+!sKZ2}@nz|cmktz0K7o}G=o+#r3 zp(FBQv%aQ6r{@VOc(>t)p1Fj3WF09};O$Iy7@$BN?IJ`Afej#6(f>V2q_%FQjPgR1SitwN}(eB$sXb7>{*4c}QWiCypKU zqW)eMgIa%^_e!vHuGVIqPdWu#%)8f^e(pYd{$DB{=x%+E*?#UsB|ZT+c_OESo3OGc zpKa1~F6v*o+G;!VtU3L3viawqs+>6<=;4Dsi0UEIkwrPb%t%5M2>JAtLGS?YxfWAj z{ufctrP|v@7!GIjkb<6D$E2B|83oeL6%B{gkSGC|3U(eYeGz=u@mEmNm zZuvD5(z(PlG?BgIS>eo|w83olI*zOy3wVWnk-Iy);bk6a^0#4Fdji|6flaEkKjR>3 zZ>z#0`d4GQxDprPS=Q|3JNr0lpm_kgP6TAh_C1aX03mO+$vb14nNRdSkGm}-c7GK> zl;gE=lN*VdR9qEo@V<%iJuuL{aeuwU`r4ukn(r|@GjD6S&XMJD585mE6rD!mnma9h zNN!iA-F~|GA#U{!`@fzIAwDAL8%?4b^pz2MjqorZ!mbsH4?nGhDDRzpZWhs|N{AJR z#0PuPh`qc^MQ?s1JxS~QehcM1lxVjUJU{QR@Oq3nS;>Lqu?!fo*>DW*#&1JL%!0Hf z8bn*#17*t!jWrLg>R%oNLmOH-ig1fBN_;BXyZW5EVcNgC7vDNE5=@{w&^00&zdYt7 z<)i8leRE0^BH@Wr2Mr%_-zq%GXsvI7g)$zZSWRm(T zjiJpR7c0x%A2GZ1Iw`szej5+_DZlE(Gjbf-sV_=gn(nqz&3Bhm7jVp9HMj)0yySeT z1||yDfALREZ(3EIXrwFy)=15pqFUJTgZd6^y#-KN$1L;>G$-BCG@wEPuoEWANp+ye z`)iJ>+DneNN$G`POC&5(?%Jd4#ZKd#><%1F)U#ci#+{Nd#_(T#^>)0dN6-&V9z6wL zGVO1G7@Mn2xDjQS|FswC9)$xKmRQJ%qj?X4*1b`7`gj3Fl_CE)umvxJowEGj5`-4v?f^n9+wb48?d9V(5y6(n~}VP8DbS02|M_IgrR89r2$lT#mn zJ-yWeul%w$C+W`g9g0N$uS)Kgy`5LW)u5c~EGN@ex-zYi~Tk0RfX1!-?1#xO7UG?;`gnTMQ7Br;HcjVK(CV&?G*O{&N-x6RMH)W+&O%bPX=G+^fcA< z=U6wg>YvBTHUFe@;FExM3g0OV7ovEp305x)(5SH`B0>JjMya#QvdZHR<+QaBfxBGs zx@Fe1IwAP!xG-pg=;A{ZGLcSlDUPDR?^7Emtb|@T;9?I$Yi-XMNHsRHWW|`#*O+XW z-uWrxQ6EU>Td#-h_}yb(dO7YN66GY^?L9f@4(d72eUU1Ql}kC1Mui4>6dn3j)qEt; zGPJGb*-y6)k3o*ixkZYfu{an==S+q#3InAuGj0&KYC%j@@mX>-}7aR|C3^;EuPEkGbj>- zTK;fDb0fC9?AHf=4WU_wDXPPNo z^X&t$$h@1G9R#!`?PEK;sl2BQ{0~XzXEcbK7O$jJnwgeMR*BJ%+WlBpR7&t;>nb-0EbZ%_SAxCEu1AskV5xnM?bdf8 zjvYAbCfbqOvTX5EL>)MgT3)kerm!J4RvbTgpBH|%@^%oCojAI(Jy^+3-*xemBGPeh zcq7z1!J*}1D5HMBr(%w21c3}L_?hc*Mf;IAQVaqsio{#~{1S~81I*|}r?uE0k+#6h zg;SLFTA92j=$*i`uV4MM+P`fuH|~*&8(E+BSd|`*@HNa`!1PjT;APq0+Tc_~le^4r zE1>oFOFTP4Jn)l~h%?B{pK6kP(l${-;-8K#!cI<8h+jQr)^et1J{~Utjc&Eh;~k{Ne6BwNBZ2P*ik5|IMdj{k;MU@+52J>L+Te`ggoNXx`_( zXM~1n!s@pG(00+85KK(xZQwR^A@S*iT@|)j#52)*zblswd8R-!f9o3=6%Y~F{>>p; zFHPy+^@TYAV0W{Nu6`15dRRX3_PX=yToyk-@t%C@?gX{xu`cMmv~jm*ZmZv1PZ)`j z7@hs``=OBHo?O(|Yp}b$)Ufg{!0s`(ZnNv+?Y?K;1@S;15@bl+N926T+;PXeS3eff z<+kZB1LiSU2p$nn6G>d{!L5k<5z7}ZZyST)mNzw8E5KB@!jTkmt^O&UoYk~q4*?se zh7Dpfn)_+^Cb)CmSaB?MOmWL4qGj$Y?6*2!c{tJFk%|@RkG5}DsXF}iK4+x1R^1n# zmU0JYaXK9QsaQOf9SaQ%pZh!(DemD%Bi1v%(1YyqBh$SkpRvAxDG&JV<$LiKD8W$1 zZM)KN*RI6U0ddC9Q~@i+Y5n%d9d-(AF?r+uD9I?zdsA>8_WCIV6HJYzMI(>`T%Qh zle23>*;Z+?%Sd-VmG^3pdi=I4gkkS+FN!bd7#- z%$J0sJNKEZ;+&-*dNIW;myNwyb7E$EH;*yS(aZPgy(C6<~xw3X|M^z^r4;^i6{PX$OI>6f1CL*E04f&oKa^-M>`#p zJ6T6qLT%1tF?;CxGI3ASLs;McBkC)|qU^q}hb{@FrKP*OrIAjhLqTegkZvTD4v_`{ zNeSs;2mxtf=pMQ|hjQS(d4B)vdcV!5xliqV_St*wwL)$A51SUy`bsozyc~HnD)I}3 zUEo}6*d`UYjStz5qc=`j!CDEefrH+Yw^9?IQzYxAD*rZ%-z@ripKq>`H16gufoDDn z+3tGDEJ?=)Ui@;DwaLkhuS$nXzg!~tUO18mM~4;N>_$Kv3;-G02tW=iKOZ!URNGjqTs!Wq4$}Q}bo1_~SGbm(?L>Ve$5vr>2=K^5m zx_R6<)Ab%69k3S0tGi?AaW}W3lENFx!vX8Ve-JIlfNSCUXFG^KrFbn_r} zV|9VW_kd)jbfkNqo-h7)ocz$R?+=u!QPW}JF{n>sOy7@QRkuH?8Wg4T%R`0NR9WVq z1ZtKDLZs&_qKHRxvS%Owa>essxV}2wZz5BKcA5W#=055e3#>s z9a;UB;0U0Nwj|?tl-3L}gFLnY-|9B@85j;QpuW$y0z!b~DGQQ#3@m<)GSl}bG+j75 zjQS(1Lg&^#^Ov1ostfRs)WvnhZ6tZwGG7;YGQm-EaE0HM5qI^Fmn+u1wY%kC2qxBS z%|}$`Ia}I$Z4c>lWNCwj{lrk=x9TQpx7PSk4%M`5#WangcRA39r4edR?*SEW-jCO8 zv_T!iJp>I-kUmEFn?z}V_20f@O$pF8`z^Ql{Q2^nE$09wFh)6GjL*`>l~xn25#pPo zf2V#*mzeR*A6S4jpLmnIesXQ#NFg-km3Zpze>+o8w&oPry6=$?C-llgv*V~dZiG&t zxJ_|+OzFu2@hH4u(Ea*0iC6t^(#p+A_50%Ws2CpFif0ai=EFoSb?y0HnrhyG_rHR6 zs$8dp_J4L8v=KKobp#xG>Z^#}a)EE(bdUi0a8DvK>Am}m&`0F}+TkZmeUb7B6D@4i zwd})q_!gh2kcU-#)6?j#oY*40#9zHsJX!UmpZ9;sp5n63#CvGC-`Y~F@sg=)JF)Uu zYo?iMylcY1kA_aV|g8k@!3HTB)0=R5^|x%BPmJBijc=eKeI=<1=8vkRET+-$j)EN)HTi#fZOS-&2wJG`jfP$Y z%U`?J7gu+~z#sWv+26Ex?`|VzJ{~w7qfb@hZcop;RlF{ec60f|own0(TbdOLow=1X zVWwgENBrD^MaCI7$%v1xol$3(BYC>2?0_*L!V@{5Wnb^5DMv*-hYk!J^nnF+M+VT- zxEu;5IJzzO7K>Fe`hJt>{{bD{a*(hTpck(tq|MzL(=G(?Bncf3aU6jkN3?IYJe^{8k0 zf@Ci7lkd$ZFSP?U5h z^$;WDcAqsJD^LEw;FceC>SSE)woT}jw&moMrbWHm|DL(sx~n2*KGuk6X}~iH>3b>e{NJZxfuV z99zWxaodJ`HV9~mhA@mSVvKI+O3zw8oOrR1k|XHbx#W6*1qHo$iV1sBL43dSRjnmA z6pzmw-dE6v84uMTx7(ZF3k1TpPr(W$3<}WuCghB$C3xuqDAMd#!lDl{Y(|BqN%0!}%9Gm@(LGS#k|1Gn=&^H+ z14E2dQnDgL!UWGk@f@)Vz>ZA~Z>=~@r4LeeQtebZS-^%sD~?0Q^zw&mH}SvGl0$?d ztd&3jVI2ZLD8=?Lv6ZMKJ;d3wrF6M^@hK+_PdYmYOs3)8vo?OC)p9FBPx{m<`Tuo@RnZSvr)jDZ#-ZT0_zyU}0X-Z}#OQrpm%Dm2ux6h}m z%#5CX;Xgck$>-xTaw*~zOGty+JWLIlAcT`rG$}_)2J4u5bj*Vx@2l@-G@La{h+A%g z?!MU9JH_5^>~GaXDL~Z);!|xsm*z-KsdfqG2xFu)?q2a%if;jzKqV(fnzzGMdqB3p|gh2f_7pph|A$%j;0|GeCP|IlE|wRD^n_4ZK> zIXF(+)uTu0tv5!J`BUbv;)%r{OI+e<-uF!Ve5gBns8mCN5Elbt2~&Vr!Y@LA(HC&MkOTfL zBtYB(#0wDVZKe>JUTz@@^p+Q9feKZ8tNzJ1?RUU4zec?iNzOyR&7=4=AOk;kiVKAfe zbD;E@UN`WE^?5&+{`gIy@?z>iHA|XT6NW11*II{3%hte?{iy*{fH{+*efmqVd(qXK z785>=3PVUm34|G2ih%itIKGReS)d-Evrd-;eaYRIq5h;D#MUgQTq1Rf;VYo$CHp3O zgqEuvVVL%xvTs;^vQlQ#((!{?aA#^M9;YlD-r44gmDYzYplA>M2J^ne1FQSl>`o2- z9}(K}m9)ncOB(6r#xG3<$FKt@qo|hoW zGCGp$iB^cuSCEur8n{u%ok53=II)(? zO!P1B`R9VOHSzCRo~FOP&&v#4z4xcSmvq`JYht|5PO=&iJi}SYdQvM7;JN|Y(7%zX z%Ema?iRwpTpDmZ?0XjnofFf9dFL6X7e0XWQdpfTB$_a`mq48bQgQ5jaJ&b-aSGp-? zo-473l27NwYqGU>sGG|#s~-84iOoZ9(%?@^+yqH&SU0y&XEOc=IDl(tDwCA^QVEc9EKer=!pJQQ6xjMOIOl*lR_yb%Xoz^`A-3>8 zesqAbu4za+=+d#1+wpy{)wu|?8RqFZ2vM6*Mlz=qQx+T*6^k99JUG7|uucd>4|Iqc zIFjWb-(7qk^b^AL9q)U?S1nOl7&a@c%|rg;QTG+!faAr%11LW__B4o5j-VM2>nOdK zcHwWZ@^yeB@K$LW%~!2aBJI2}$OsuBzs*aCDF+C?ANw*7_ay#PgW8jDZvJ(E>LG9P zpW#p%!Re}2;5=^{wdAb;D?A!y0n^B0oV%MgG!ilnXZH+Z$BIHsP7{zYi%!>HZ?0gw-peJ zS=)#Ek}Jn#;#zwGl8gK;-zQ-2zwf)4G54{MtNTqj2Y{Jj^Ml`&HclEwwju5W!j+&49M~?`3us!bTT_fHl09AQ2yu_a--ZnTBEluVG6NVob1@fP1Zuqd-2wQ zOP;%jD6uu6c={A<&NQWi4&(aJm4g7(O?INe!#VxDAq`I~t!r$XHbo9k6ns?B(45ob zDR0C9O2GGEp1r+3GiEG?iwH{FmgVLsHMp|tnIsBatW*FwCn-zB!gmG~Y*X|m9~OD9E}OdFvlQwp ze{>73zYj~~4Lm@RA~)XUIaZrJUwji?bE!O|lJ(&9H>u2C6vw4>g`&7%(w!|O7=-I= zsf-K7cFElgTvmoFA1gCOA!z?{3EV8Q0!Hv@Idpw|@nMlrl+wl0x%mMxEyFex-SqzI z!g79jy_TM`Qav630dWGXe}jtfS5Ifrtco289`~I+fi9NE9vNolX?CvM+-$|EDzeqD zTDM!iip`oI=Rl|?cqvcd|o8sU;S1+3vnf$Ag@%_s2dt@o)pWc*I^qR?26Yi zbpZPSFyY+Q!tOmmw0#aGjzDpTa*HMZ+{bKVPec*93Cr*~dc%YE{x0@fZ=8!dDQVJT?X)INL!nxqr1>LyBt+&R|A@+CZtRwhYP81Pjoy? zf=7-xw;rp-IPp4Hri&IPH5u|z4xZcV9K}fA;;+h46Qh+0SYd4SVLKH7W|T~t5uDt? zb;I8Puw4zla@}}TE>3<5phd9>w3B}1F`UtVBPYX}oDN^{|e#k^N8h|zN zBv;dESl%THmQxjH^aAb&y@BM&ns9tYA9I*-_YsGdN@+c?hZG0^JoQylfB+Y>it~rt z^p?2JO`ec;pK;LP*q>&+iAYmP z?I{bn^TX~8Vfk+e^m_j1p4_tU=RGbC%~}MF=!}$`WMJ(_g06J@qNH0DLA>Gu0Gb8} z06DsBJ_eSm6>mD_*(Dsb!-mY&)dtN$hgkR`7HAw8%(UHq*LgzL&!!(1D_*^N@L9BI z<|FcDicYAIe4Q8ceU(Ncnu@#_X74d2`ACjk8)EnB_0G?nUArrRf5+EMo)x)_X3?Wg z!rrqpZ$9zSBA8f=S;IKw0cp`h0RK7?U8WYfSve!QFPl)`Zw}&)b0%*n>6HJizW9a= z6MGRH^fLD@&wsVJVBr!#GM)Z{OdjwuUUI+OxpuR@4OA1#8Kbq9zaERRm(0arqm zUtyi1wHPT}y#&rm!Rvfzg(^V?p^{+71CeMIa)~K5vVzaObiXHc*v2bvxT3av=6gr9 z_Z4IA5!8Zl5Zuldw&MdK@(aEQX@@`5wL#y+uc==JKm6oUOZ)U-KP2e7LBqqd@(Isc z zfe;az2Lk=kZcYlMsQj0)hkq@tSF%^(^nS4hoX)o*GRjh3Ms#5d*)_;18yQJT&FbU1 zcf)W8yNTh<;fi3|+V6E<%lxfTTmk;_L>-%s9-@cjJ1QBm=0DNoEX5EsLq$QvT^gEl z$F(TdLIRX>Q2HKy2sg!->Mj-#JFt?cJHbcG)ATRCSk8XWwq;i@R4Qy($wi4 zAY>}RV{W^RJ3@rD1!51!%-KzSJKkFQP9;ru|7)lqus6R?u}(PY&o5lLBZ|GK_;-38 zFnO<|J!`bCowp5P3U<}Ui@$iWzelZ{G9>~{({wTS+nqV4cl(IN%M$W~iY7=QlL*xz zFS#rrlsc7*9cfcIsYfe0qlMr02hwN%udDI&H*DkdW8^Z1RSl5RWQ&Y@^-j=Pa62V8Z?EC%7kl-awx=$~iT~ zub;%N`lo*w>N+tbSI3If#O_^bRbc)j9?6k{ecA`E05Tw|SO;u;Pxzg@OlloS7KSN)uh(4tauy;NqB| z0BUkfE_MfpahQci(&sO0HL|>x{bO7%lr#GHk`=%B=aN(>N@c)xH3!ADHLZX zy%}gR{kQ)MDPhbMjylqTvJq{*vUvozEkc@bR>0nIm}H1X^Z|g?Ax%(VG<}Qg^AD)m zKx5bAe!mCEkmee4VqW$I6}4nNYd0oS93;-$uGPi3S@dCEG~mk)p03%4+?l9a7LB&y zV-r3MuI{(f6A(H+yvY{rxTnuJ!W&+u!2+EDK>2l`$yQ(QFArBMZ^n-wzU#=95jAwQ zA@j$t-aZ+``2fuH9}Cd3x8Cv{js0h>y!9syox(4SCd#dZ=n8dD_m39mm!BuA%rEHG z+{p^Vk=W+_)R@EY#%*KB)l)Bq*a+z8xyIeEw*iIDcNA;s81&?{lQLX&x^T7ewrf{` zdo!1aZKhA7;X^L}ebT3wSSv&y(0G^u+V!eEoPyzAR3z>PvDk*xV}e}8@m+RhH z)@V_P;yg`n0*Qa&C&Th5Hf!R*4`Xtg(Wm|fccrOOCy~ao{vD1k0VRi}f{E3pKJPM4w$9>C$?xeR1*>w%sovsQtXKbZv}2 zj_7am`{fTK#`LBJ{_$ruUJgp5%R^MZjI%SMcB~Wrp20lVQ=gix(Qq zl*+z?UEjg+OeW#CU@a{V-=<-cyMo1qG~KjJOsl{WHRi;Yl38V&VkXWGwp6S4ZryM6 zgmdqh#=c*8a$D@)$GN49IcY_cspv`r-!S}XeI8&9KY&>Osa#$Ny~Lto6!A(uK%Jp^ zA)7EIiExtcKzIrhd1&*!;0A~STf%?y;$jv%db!-11v(u%RFS7%Y{rLIKWGidG)0`& zc|FK3*49ud38F~XMqQjQP>3hd8To-0J+#x<$+%<7?7M&x9_)3yYDsLD zjhdK3Nu0{IGyKA~nj599zvvhJEvt3Ts^BqmLiz<4-e5vXb-+-je?R1ph*8zx%^?$-o5?d;nk5NTB5_G zYc|*DioIGxz;Va3$k9teQH;J9A+)10^d_zEykk1vrVre7G71AjAOnW5z9(kJDn!*1 z-;X`Pz^DVZm7TIak=oX_OgHs!k$*SlWHG{5(GEl$EXE>HG-F#I(B~tjDMpbdzN=Zc zKkg8C=1;k#O22+V1yN2e6jiZ_8D&QK&`#dhrFR5UEs>~xIR7qP0{S>;0t|(L3@RT5Vje ze3bs8uYU zF1V4fJnins(KpqrtAZm~R}&R%_ShLZWwZl8qKD2FM zqnJG}LulUFD2z6>%6dbB>}@Y%=0^U~aEM?6QC7#paxJ}4f*Dq68+{??nUC2YwEh>2 zF6C)j)=YrnMP2iUFuqTX+cFj3DuM?-YIUM}{t>bz9(7hs{{Tq%0A0p^aPIQsj7gvL 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%0D;!AT19hx!1JjhsP$k(qa6jk@kyuUR zJuE=kydmrCzSH%{mWRs8rnVYBFHpN5x*4CW z!J*7mYZUD5Bu~ad!s;ip#)AP%d8sHLK>)D?i@v#j4`yh63c&7QN#Sg;Fw^fxOC)Ju z&HL0$Sx-`Z1?fl#yXlJv8gqVKXk%Pd?0j_YU?I3@l-&jNQ$ccY5HS;ntsqT@X>Lq{ zo_7X8Of^abY5HygpSTjG`qiEA{1mos&h4qCeKonL8#@j#K2f6ooyBqzIHxfr&ojeX zd;))rp9HM0-m3W#IoE#Q@AliTC%^{d)>K}On}~%-p|{%GjhU48yW|+DC*4fXGyM6n zJEtii*%&63D0#?lhVPzBc5wLrn*CzU9QlNPeTOXO-7!Eg_4n6XR3@3Zz2VH=!w`rT zeQND-mGL|vmT>mR5g1ThTL^1Ot#AgImc3Qv^&ic~l0OdF^8RYxn$WMfUdKkzL^^&w zMhJiU4(TUM^1=UBJrlT_uf!l(xZH`CKxf`c-A~h1|=D_OMB=zN**st zS5I~Dgq=)~w!13eumwZ3kY9DuCSVfz1)cjA`Z^fO$=|zUw*POcS$Yu5SoLw&Z7S)T z%*&NrSk|{4-a?L{TSFxe*x>#-nE_Z&<-2f2Rf|U<*{Dg>dyc0N6t>j*0U@g>GwwJX zj9|_0qI%w(Qz%Y%AL3(ELB07fQnZ%epigWm|8*`T_a#lNZ~{S$WR9i8XPUNl+?ZFU zjW5A|mHmQ#m-lm-UpV@XE0XI0ngn{=2ruKMdum}ag=_J#?Vcl}bhDMD87DPQ-eOVNETIw2TW_RJZdbKKt&3!s=hws<- z@RQJ~VM(^+@dBY^?nSa_2tmvn@UTBPu~#8?XRpe~!Fn+c4E8%WY6*@?2*rACHLify6VrkGE@ zb-5#N@wis&Vt7wXKQH66=q$}(I9I27ntoD>7Q=VO(*bHSl=OjErvIWQj|vC!!}bK$ zl9BF@(sLtRKuXZ+`z9HBL=v)Nr*>O zOLqxT3~ha!bLYgPOMGMDn2u%tD|>jrX);qg=IbJ5V zp+%$*L!T7sZgLAHpEfG!O~sZRC-@?1P@>4!1eBqUOJk`MDL5~}wgFT7D}?lBiRm?U zTf#0t{Ux!Irtpc>B%TcX4reBuTvq?vyh}CM5?SD`L#YhMQ3p{#_uyuzq;R04oK1L| z@AU}MTiq3;j!O;^gP&HvT~7>OWXm+l1;-uod(hu2DGwQU@^6&qW3$}G5E#5gaq;G! zsq{}^z3q&u%ytz`e;rCZ#x^wh&-dG!j0u}_daG;6s<8Jk0{L#@sEiUe_& z27*?!`O@`ud3GnQNHHB*Iu?!Il6{8Z`2+eH?&aXl=ess>6QefyLnry3X^15$TZ%UP zM}x~s-?Oi2M2c5Qo{Y+kID%L|l#d%be?aY@U|#-DZuM%k_bdAz_9o{v@o`#t3vFj# zsyOhy+~qw_#-*Au;w$H7^^bme=3{hYhD8F9bdg+#YXV<1_da$5X5#9IX8MVL%SGi^-TZZlgoV}%Uh{GR!JDdhz zu|``mepUNN0eE_+BF&E_+9Mi6j@5TQR*kQVp6WS(%R{vEE7#AC8B!y$_#gvJT3N7b zGn8#xhj8V4v4RInZI^$SMg6c`zT-b+7YWvriBw~gKKty*XPAL|H=^9!Mda()OgfvV z2$)(P9w94layXMyp!>KPg$_=6jq|IU`a0c|xHi$7-x$cH?!P{D&}`PZX!01l;&h{u z<+xGRwp602Gw}_fG+x5X@Mp;iz@nC*etgk#n50{_$ay~J2w0V37vBk} z%8K!oUglo{BukHJZXm&3uP5AsbJhN%S3DE?T0H5spcV$`bLym@^tgc|W7!wgtlCGW;8FXynG-WC(yjd4&ccQx zGiue&;w76UxIWk|Y%3kWI@4`83%NIJ-_1xtZ4SP)_$b=mi zj&IJy{vVh_y;Z>ymh5pFsOnjn4xDf~&fmUm2(XnVFE7B+LsF7Puq&!x4CVWLUzc-j z=#CB2b2YO&n$1$6vOFc!6gx=WMtm|eMwQg$>8N2sBvqj`;V!&u{4`h^s{ds{WSPLt zUO%n^pZ;yJ%a6mA0O@r}8;iCy#9SQjr)4C8gg`E~1lP7(?)NHH8@ULvRx{)d!magg z6A)BT?etcp)GiozCg$qiHZSBBFFeqiQ_FmI19(9D2;a%awtb3sYf~rW<9JR3il3#& z>`!ipo_?EMia~jlP~T^qP+!Dxxf|7qi}3eBjkyGX#p!(6QOmWeiDj-)H}pmowM=ri z!R)~1T(C5Qcru4~hSR;~(zq>Tuw>4t>cN2}zMnLR6pl-1P6W716i2bK13Lvwg@=ne zeH2jFUr_^nsqkDPTOJ%y$=xeg8?%dQH-~6`Iw|hvC!?j4gvAtccjCKgzt4sIK<@+c z(UJv~uyE9#kk^ULxyZRpAnJhj9tMU$NG{n-EOqVcDBviYZ9s6gOt1Fv(<_`?F)Gdy zSkK7v6-4|9rFZ)$Nu0n>bZE(irL)&(JMI>26u~FF%ho^`#Qh+%8yz0kxNh{F8iG|D%qjQ|i_1_2lRNWU$x#6P<2Oe6v=$Bl{_@j4gkGP4B3ZYA^jIZ!baZQ?rGv zMz*`=++QT~o%wDOG5>-VTsLVp-w?&(oQx#}O#O%61bb3(M8|J#*J-7eDZ;_!}hhu;eZotj#sO8y-6m^boBb7{#$D%23@gr zL-eeko%$@2Y`qWXtga3*(Dyq1r=m~ua~V{ejYdbCk+=O7^zW)F6dU3bn^ zgRY;Cj5i(_OqqSj3jg-<{XKilI#Q(C#ufm432l}YSG;Z>bLF6N4vkNW&z#cR{11Fy z4~2X#kqB(con{>48tquu6s)KJf}-b&II6hJc*jAhkV(0UNG80*k+?U60bBW0Br|b! z=+zsN{gQ@Uy-J%$%X@2T{(h3N%E9&@ub6+(6mbG#YTtHmcL{!~P{1Zf6#{QlCPssB z<;>NdE+3eyUz1#faDa>7a|XM@Ea!3d+d5#fl@rR>_FbD>b|TJ&h;#NO{|auqF77;SGs`>MHkCdrJ$i%T zPC-F0F&9yFOX~;4dF8*cso`$h>@B55wuTq|ik^s6QZZqtakotlwu{CwZ&&g3&fr15 zmghv-IJR9_5M=iK>%zdN*>+Bi7WZY&uAUrdXxNm`?CIMlkNwCLdfz&? z!hYg2KOMX^yxH(@Hi4y^rA=F-hxA=}#OV`@$yYuvAQn)B#0WjlC4WXI*6?rvKJNAb z@4Bx&ps%V{;$uu@( zFX1#93WM5YBjnXB`tv7u>{RS@$p&z?_?XV9o%G4&RPY+PTckbVvxtbR@3*IKfFhFd z3|q*r1*9_{m#<)9NurB31DE_%S4o%*G9U6XY+^)^n5!7IT6cKoCSFW7;gD?z1SMY* z<&;|nURLPd!C6kZyaho8Za*$jKa13pC~vOsY)|_%EDeLloxr7prPvtJi-O z4m-Vfjp$CDoO}`9HgV-D2I3(eWDM|Kt3!1DVCZvtZSHG|BauZ4j`yFJ$5e6*f;qPYmLf0-<v@dFqP>qxypF7 zb+b_XtTk)yE;7EIM#@c%Y}5kmL>{ij&jAS$;V_jlv?A!#z^W< z@SjjZok1Xu7sLi?(^1mSH=mzgDj5%tv;h}UzK5MfokN`eJk31rmxe7|Csruy*Oo0e zi8~E_3OhbnaR~?$69ab^+e7N%&RMR+$VXmcU;JSBujbtMA9Dp!K3Tp@Bbd_(3lPZQ+#G~v&tfpuwK^b2K2a!-J{rK_G8;!zwjfljM_Q$(C zccCcsg={$yGaTG#=VD4mD#m!&UerrH3Hb=`4r~DfEDI0vb^qjc@c(ulo}q^g#SF2F zM|=;P&jl$pj~o|Iq)?wgzZq?W4RJiEz)u{4s_gg_&+K{=6^fc^Pg4C~CG8)Di7VXl zdjWP^R9y`Je$L*m?zp0L;P~eX=1dk@H?ofk>{+vO$O;8LU`bbZe_5m@ZQ=N63w<$e zhcC9-pCz6G20!>=IvKPYqf)&#-Eb|NsIw^pOggg8ih~7<`r*07=E*qf^TOMf5cMOl z4Ujr`JDUQnwBhjq=F3tlb{~Cg*iW^$LWI(KbmxVNj8d<6&qohVB$Zt+FI=2vA09D_ zvpFSS0?f~^T)2*FsA|!vTqNw_l!KitHL^1>&*3KbidOzHv-2PcM1B80WZ#aaO?W1# zJD1(+oKD@lLo1i;F4hCIaYmk>H4_qR=a98ULQ&*C$}H}W9SFer(Tl;J0y7W{^=CHE zQ0yn(puUwY1hn_)Cs{|vaMBEro6Eg>DpLBDk|^RV{_|tmUu=}sj2@tnC+xgpE<}u~ zC!*7j#}JPU00P~Ei>mCO+}<4r1|GI~tX@PNwY_k&9I*nGVQ*y}R9HL5`(ebZI=OWT zf+91efN62KvSm>a0xV+Lo5&TB$zTNEqQn@@cu3Ill>5HGVslyW>b?~thIm#NCUiRh zqLnbM9rPMhq`3MJ*q;=b6t+{l^KUL`?vu^^IBXa3^mfjWAo3~8!p)wayr))%)=W0O zb@N5vPu}~kEop;ZKMwkn5)%xBKSRU8O~9LaLa>WY`X3!xl-qL4aP3s=C(N^1?<-BP zn*0C)GX9g{0yC>EwTOz8S;?AU7QxN}hT+%LKIt!Bkox8>g4+5(qDE!NFgRKJ_HI?JGKX_(axv zhr!SKKKe0x#cg+7lJ7Ab)NTx5=ns1 zP!b^FW5q7Ixw$TuGCr1hm&9S0B7><)ddV5tMC-{ z?>GQFl4%@00}f2ss~?}$;{YV{;&;h!-f2a=yaT?Y!*Yucdhpdf)sD8kY;HX({pbAx zK8zT~pw;fz2Rx26NwaGtua%e_uK*JUCKrY(>0nf=W(j;P*9|UW=y(eZJ`?WjR>7jR z)j9{EEUb6|%da{+087HO&hJS~t7WrykEOGyA}Gr4`nn8?nNwAgi=i(ktC^6XdoOmr z{+-&o)`cW)<7HqNPod9AgP7Skeu-sNGfsbE2xyET%TS^f4Z7*Z0ID<8uOS^4+47!6 zrgz5N1)skjIvvBa4QF^9WLww#LKg>W4+UMIx=PT+n`Eo#*ca-oHWgM?Toyd(^(%L5 zk4J1HHN-AYdEA8@5zhaF4Gqx!1LMRk>C*_fg}&~y9n=i2Pt)b7z^NFP#CLSVqjTNV z!O*vGcqcgRyJbJH>jO{`#AG&u8=iJfVf2eQYCHMwCN5%aZWIV-!8ZN>fB!F6fNbsN YR+h}0xVcLudGvGTUMfkKN*M(FAC-Ab(f|Me literal 0 HcmV?d00001 diff --git a/logo/rust-doge.svg b/logo/rust-doge.svg new file mode 100644 index 000000000..e5c9666a3 --- /dev/null +++ b/logo/rust-doge.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/logo/rust-doge@4x.png b/logo/rust-doge@4x.png new file mode 100644 index 0000000000000000000000000000000000000000..1d481dd9057f6204387d8443543ec95e1a3e61cd GIT binary patch literal 26482 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≪$5Yw~iL3)sb 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 KRV`Px3H^WDPhkoG literal 0 HcmV?d00001 From cf61b2b36bb974d76a91eba5f2cd95c1844718a7 Mon Sep 17 00:00:00 2001 From: mducroux Date: Tue, 18 Nov 2025 11:16:09 +0100 Subject: [PATCH 53/53] docs: fix docs.rs build (#19) Following upstream commit https://github.com/rust-bitcoin/rust-bitcoin/commit/3f3324058941f18032ab9cc5c89815fff21474fd, the `doc_auto_cfg` feature is removed to fix the docs.rs build. Additionally, we enable the `doc_notable_trait` feature flag, following upstream commit https://github.com/rust-bitcoin/rust-bitcoin/commit/dfb76c1e15933fe0058c1bb69c4b1b9acddceee8 --- README.md | 2 +- bitcoin/CHANGELOG.md | 5 +++++ bitcoin/Cargo.toml | 2 +- bitcoin/src/lib.rs | 2 +- 4 files changed, 8 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index c322c8055..4a0dfc01e 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@

Rust Dogecoin

- Rust Dogecoin logo by DFINITY Foundation, see license and source files under /logo + Rust Dogecoin logo by DFINITY Foundation, see license and source files under /logo

Library with support for de/serialization, parsing and executing on data-structures and network messages related to Bitcoin and Dogecoin. diff --git a/bitcoin/CHANGELOG.md b/bitcoin/CHANGELOG.md index e2f5b9476..0ce2481f9 100644 --- a/bitcoin/CHANGELOG.md +++ b/bitcoin/CHANGELOG.md @@ -12,6 +12,11 @@ - Backport - Add `XOnlyPublicKey` support for PSBT key retrieval and improve Taproot signing [#4443](https://github.com/rust-bitcoin/rust-bitcoin/pull/4443) - Backport - Add methods to retrieve inner types [#4450](https://github.com/rust-bitcoin/rust-bitcoin/pull/4450) +# 0.32.5-doge.1 - 2025-11-10 + +- Fix build doc on [docs.rs](https://docs.rs/crate/bitcoin-dogecoin/latest) +- Add rust-dogecoin logo on githubusercontent + # 0.32.5-doge.0 - 2025-11-03 Initial release of the rust-dogecoin crate, a fork of rust-bitcoin adapted for Dogecoin. diff --git a/bitcoin/Cargo.toml b/bitcoin/Cargo.toml index e7e985edb..20a6649e7 100644 --- a/bitcoin/Cargo.toml +++ b/bitcoin/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bitcoin-dogecoin" -version = "0.32.5-doge.0" +version = "0.32.5-doge.1" license = "Apache-2.0 OR CC0-1.0" repository = "https://github.com/dfinity/rust-dogecoin" description = "General purpose library for using and interoperating with Bitcoin and Dogecoin." diff --git a/bitcoin/src/lib.rs b/bitcoin/src/lib.rs index a863e435e..a8d7c0584 100644 --- a/bitcoin/src/lib.rs +++ b/bitcoin/src/lib.rs @@ -31,7 +31,7 @@ #![cfg_attr(all(not(feature = "std"), not(test)), no_std)] // Experimental features we need. -#![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![cfg_attr(docsrs, feature(doc_notable_trait))] #![cfg_attr(bench, feature(test))] // Coding conventions. #![warn(missing_docs)]