diff --git a/Cargo.lock b/Cargo.lock index c291eb7e1e..442c6c5fbd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4607,6 +4607,7 @@ dependencies = [ "nimiq-transaction", "nimiq-utils", "nimiq-vrf", + "nimiq-web-client", "nimiq_rpc", "percentage", "rand", diff --git a/blockchain-interface/src/abstract_blockchain.rs b/blockchain-interface/src/abstract_blockchain.rs index f3064044df..e647359e5d 100644 --- a/blockchain-interface/src/abstract_blockchain.rs +++ b/blockchain-interface/src/abstract_blockchain.rs @@ -87,6 +87,9 @@ pub trait AbstractBlockchain { /// Obtains the hash associated with the genesis block. fn get_genesis_hash(&self) -> Blake2bHash; + /// Obtains the genesis block. + fn get_genesis_block(&self) -> Block; + /// Fetches a given block, by its hash. fn get_block(&self, hash: &Blake2bHash, include_body: bool) -> Result; diff --git a/blockchain-proxy/src/blockchain_proxy.rs b/blockchain-proxy/src/blockchain_proxy.rs index 8a2ddd27c0..3fd1ef6516 100644 --- a/blockchain-proxy/src/blockchain_proxy.rs +++ b/blockchain-proxy/src/blockchain_proxy.rs @@ -149,6 +149,10 @@ impl AbstractBlockchain for BlockchainReadProxy<'_> { gen_blockchain_match!(self, BlockchainReadProxy, get_genesis_hash) } + fn get_genesis_block(&self) -> Block { + gen_blockchain_match!(self, BlockchainReadProxy, get_genesis_block) + } + fn get_block(&self, hash: &Blake2bHash, include_body: bool) -> Result { gen_blockchain_match!(self, BlockchainReadProxy, get_block, hash, include_body) } diff --git a/blockchain/src/blockchain/abstract_blockchain.rs b/blockchain/src/blockchain/abstract_blockchain.rs index f120915efb..a71197e7cf 100644 --- a/blockchain/src/blockchain/abstract_blockchain.rs +++ b/blockchain/src/blockchain/abstract_blockchain.rs @@ -87,6 +87,11 @@ impl AbstractBlockchain for Blockchain { self.genesis_hash.clone() } + fn get_genesis_block(&self) -> Block { + self.get_block_at(self.genesis_block_number, true, None) + .unwrap() + } + fn get_block(&self, hash: &Blake2bHash, include_body: bool) -> Result { self.get_block(hash, include_body, None) } diff --git a/consensus/src/consensus/consensus_proxy.rs b/consensus/src/consensus/consensus_proxy.rs index 4a5c18820c..aea196ad00 100644 --- a/consensus/src/consensus/consensus_proxy.rs +++ b/consensus/src/consensus/consensus_proxy.rs @@ -358,9 +358,7 @@ impl ConsensusProxy { log::warn!(peer = %peer_id, "The genesis hash from the peer does not match our own"); continue; } - } - - if election_head.hash() == block_hash + } else if election_head.hash() == block_hash || election_head.header.parent_election_hash == block_hash { already_proven = true; diff --git a/light-blockchain/src/abstract_blockchain.rs b/light-blockchain/src/abstract_blockchain.rs index 2b018c0c38..5686ec7cdc 100644 --- a/light-blockchain/src/abstract_blockchain.rs +++ b/light-blockchain/src/abstract_blockchain.rs @@ -71,6 +71,10 @@ impl AbstractBlockchain for LightBlockchain { self.genesis_block.hash() } + fn get_genesis_block(&self) -> Block { + self.genesis_block.clone() + } + fn get_blocks( &self, start_block_hash: &Blake2bHash, diff --git a/mempool/tests/filter.rs b/mempool/tests/filter.rs index 4b1ab02a45..6853bee57d 100644 --- a/mempool/tests/filter.rs +++ b/mempool/tests/filter.rs @@ -17,7 +17,7 @@ fn it_can_blacklist_transactions() { Coin::try_from(100).unwrap(), Coin::try_from(1).unwrap(), 123, - NetworkId::Main, + NetworkId::MainAlbatross, ); let hash: Blake2bHash = tx.hash(); @@ -40,7 +40,7 @@ fn it_accepts_and_rejects_transactions() { Coin::try_from(0).unwrap(), Coin::try_from(0).unwrap(), 0, - NetworkId::Main, + NetworkId::MainAlbatross, ); assert!(!f.accepts_transaction(&tx)); diff --git a/pow-migration/Cargo.toml b/pow-migration/Cargo.toml index bf3d50f58b..405f203f65 100644 --- a/pow-migration/Cargo.toml +++ b/pow-migration/Cargo.toml @@ -74,6 +74,7 @@ build-data = "0.2" serde_json = "1.0" nimiq-test-log = { workspace = true } +nimiq-web-client = { workspace = true, features = ["primitives"] } [features] pow-migration-tests = [] diff --git a/pow-migration/src/history.rs b/pow-migration/src/history.rs index bb59927d96..97ff25621c 100644 --- a/pow-migration/src/history.rs +++ b/pow-migration/src/history.rs @@ -269,11 +269,13 @@ pub async fn get_history_store_height(env: MdbxDatabase, network_id: NetworkId) #[cfg(test)] mod test { + use nimiq_web_client::common::transaction::Transaction as WebTransaction; + use super::*; static TRANSACTIONS: &str = r#" [ - { + { "hash": "b216c4d1b655ebc918fcd25212f1f5abb1ed82a45a2114ee7109f92dba955f5c", "blockHash": "cbca3812447d51983a55beb2d16464b45dba19db29abdbbb7b9be8cead4a66a2", "blockNumber": 2815085, @@ -552,6 +554,106 @@ mod test { "flags": 0, "validityStartHeight": 2815092, "networkId": 42 + }, + { + "hash": "1ae3f9d84e1951863b34f9de0f30e6d4a59619cd0cb4dd70fbd75c330bc14cbc", + "blockHash": "0000000000000000000000000000000000000000000000000000000000000000", + "blockNumber": 3381335, + "timestamp": 1727529953, + "confirmations": 1818875, + "from": "0000000000000000000000000000000000000000", + "fromAddress": "NQ12 0B4A X8HK CDYF LVLR RVVT DKB3 FS1G Y2DS", + "fromType": 0, + "to": "0000000000000000000000000000000000000000", + "toAddress": "NQ14 PC35 FMEJ X6PT J74X 8CHK FN66 RN68 Y2AC", + "toType": 1, + "value": 20000000, + "fee": 0, + "data": "5fb123ae5c64698b85e03e06a8259c4fd9e8152500339858000000020000000000989680", + "flags": 1, + "validityStartHeight": 3381332, + "proof": "26dcb47806d6d1a0f51ab470d518799dfc29236c79dad4c9c1ce4d09c64651af00b7f06e9118254767372c8d07f3f5cff6d753097129624b8f83a16652d518233bcb5d35e136cc50d0fed7aea67b5548c62017c1719cba80c0d943495a862f4b0b", + "networkId": 42 + }, + { + "hash": "d98625628971998dd9f5b29fc0f95615254cfd60b342fe729620889d7b3b15ac", + "blockHash": "0000000000000000000000000000000000000000000000000000000000000000", + "blockNumber": 3454647, + "timestamp": 1731952574, + "confirmations": 1745013, + "from": "0000000000000000000000000000000000000000", + "fromAddress": "NQ19 LYQQ BL3N E4K6 AXCN CFB4 TDD4 LAN0 BYK1", + "fromType": 0, + "to": "0000000000000000000000000000000000000000", + "toAddress": "NQ72 C5TJ 9RU2 HM64 SH8P D222 GCCE 41N3 HXC1", + "toType": 2, + "value": 515101564, + "fee": 0, + "data": "a7f185d076712665799663d64db5a4a2ac05fe61eb933bf41fdcc9bc2ebf439305a0b2c64d5aea34033913543fa4e5b6c41176ee552d314db28d786bd87f103ee25f49f4e2555e51d1010034b71d", + "flags": 1, + "validityStartHeight": 3454645, + "proof": "05cd72f08df886acf3f7ae7b5abe7aca89c2191f092a2c121b1d7b0d26d2244500b2fba4e35847d6e3e4b4292e797cb256ec8f5f42ce9f347bbfc9c0ff19deaa3c9b5171ca98347ff1b2f3d48918059efcc4662f98eb4a53702942be7174430a0e", + "networkId": 42 + }, + { + "hash": "9b0448c37c40ecf48bf60a1240dda67085b12ab9f6848a3c9f026682acd66621", + "blockHash": "0000000000000000000000000000000000000000000000000000000000000000", + "blockNumber": 3454649, + "timestamp": 1731952691, + "confirmations": 1744481, + "from": "0000000000000000000000000000000000000000", + "fromAddress": "NQ72 C5TJ 9RU2 HM64 SH8P D222 GCCE 41N3 HXC1", + "fromType": 2, + "to": "0000000000000000000000000000000000000000", + "toAddress": "NQ56 VE9K PV0Y TK4T QBMY 8E9G B85J QR6M MSHL", + "toType": 0, + "value": 515101564, + "fee": 0, + "data": "", + "flags": 0, + "validityStartHeight": 3454647, + "proof": "0103013913543fa4e5b6c41176ee552d314db28d786bd87f103ee25f49f4e2555e51d1bff5b88ef94cd7c2ba354a8e4b50fef063ab1659646570b34effbb48f36ecb4c08600ec9f0d44dc8d43275c705d7780caa31497d2620da4d7838d10574a6dfa100410b82decb73b7c6f4047b4fb504000c364edd9a3337e5194b60f896d31904ccab8bf310cf808fd98a9b3b13096b6701d53bbba8402465d08cb99948c8407500", + "networkId": 42 + }, + { + "hash": "25ab1a2708b2ca845ffebaa6a1cfa6cb0e99f7221b71f0036caff0930ab24bc3", + "blockHash": "0000000000000000000000000000000000000000000000000000000000000000", + "blockNumber": 3455024, + "timestamp": 1731974479, + "confirmations": 1745593, + "from": "0000000000000000000000000000000000000000", + "fromAddress": "NQ83 5445 36JA 0528 2H8H 7PEJ 3S82 5H9X AT6J", + "fromType": 2, + "to": "0000000000000000000000000000000000000000", + "toAddress": "NQ54 S7LK APF3 KNDN 4YCE V9UY 22VN V160 S96D", + "toType": 2, + "value": 85957876150, + "fee": 0, + "data": "15f81c52d072d974d4a8463f8c7c36950be8228aa70b9e44a448b5183ac4e186cd749d3d889fff8401000000000000000000000000000000000000000000000000000000000000000001003506ee", + "flags": 1, + "validityStartHeight": 3455022, + "proof": "0291b21f4b100273bd7034f6369c29d1f7ba72dba7de6720ad3cd8b8191621891300d18f33335132492722f0c7024573bc73ec074eddc7b8a83bc1fccc11ce2f6cf126d1500003b788c969cca371439e48c7decc7ff5dcb7851aca4475e53f9bf901381418b67ad00bf1d056858aa31f10c5bf3d70a44bfe9c5245e3e5bf3f798189005ee1023a0af18e6388df3ba29ed03d5e8834da00307d326545d6cee0b56b9bba6d9fc42d22970efa47c83f9f1b67dbe34f0fef968dd943fbdafa54c9f9e0ee0d", + "networkId": 42 + }, + { + "hash": "19a8088a3450e0f46870a678a7c062edf543449a3f2b48196b0d92ac7260a6c3", + "blockHash": "0000000000000000000000000000000000000000000000000000000000000000", + "blockNumber": 2802559, + "timestamp": 1700088904, + "confirmations": 2515175, + "from": "0000000000000000000000000000000000000000", + "fromAddress": "NQ15 VH3S CFFN BJVE LBGM 7YJA 4B85 8A9V CNTU", + "fromType": 0, + "to": "0000000000000000000000000000000000000000", + "toAddress": "NQ86 UVS1 UJP6 J1SX QVDP EXF7 GYR2 YEN5 JKV9", + "toType": 0, + "value": 300000, + "fee": 0, + "data": "", + "flags": 0, + "validityStartHeight": 2802559, + "proof": "c79090f344bf7ed4cdd6c25512ee61d1d5fe9cff643263342996ba3448df189f0280de8d7ee7e54f301095294d494024430c8b251b4ebf9b1384922dc7f9dd24422f830e231d26cdc3bbd1f55f1918757568522acae62c21e8046190ea84d6e8ff160caadca71723067d5080d6c3858b61ef8cdf286326818e90ddbefe23af2d529cef7654be5b99bb418786d49e164b24f9db1c482545d8e4473804a53b889e4b07", + "networkId": 1 } ]"#; @@ -560,7 +662,11 @@ mod test { let pow_transactions: Vec = serde_json::from_str(TRANSACTIONS).unwrap(); for txn in pow_transactions { let pos_transaction = from_pow_transaction(&txn).unwrap(); - assert_eq!(txn.hash, pos_transaction.hash::().to_hex()) + assert_eq!(txn.hash, pos_transaction.hash::().to_hex()); + + let web_transaction: WebTransaction = pos_transaction.into(); + // Convert with the genesis block number and timestamp of mainnet. + web_transaction.to_plain_transaction(Some(3456000), Some(1732034720000)); } } } diff --git a/primitives/account/tests/accounts.rs b/primitives/account/tests/accounts.rs index f22465f161..ca0963375b 100644 --- a/primitives/account/tests/accounts.rs +++ b/primitives/account/tests/accounts.rs @@ -80,7 +80,7 @@ fn it_can_commit_and_revert_a_block_body() { Coin::from_u64_unchecked(10), Coin::ZERO, 1, - NetworkId::Main, + NetworkId::MainAlbatross, ); let transactions = vec![tx]; @@ -192,7 +192,7 @@ fn it_correctly_rewards_validators() { value1, fee1, 2, - NetworkId::Main, + NetworkId::MainAlbatross, ); let tx2 = Transaction::new_basic( @@ -201,7 +201,7 @@ fn it_correctly_rewards_validators() { value2, fee2, 2, - NetworkId::Main, + NetworkId::MainAlbatross, ); // Validator 2 mines second block. @@ -261,7 +261,7 @@ fn it_checks_for_sufficient_funds() { Coin::try_from(10).unwrap(), Coin::from_u64_unchecked(1), 1, - NetworkId::Main, + NetworkId::MainAlbatross, ); let reward = Inherent::Reward { diff --git a/primitives/transaction/src/account/basic_account.rs b/primitives/transaction/src/account/basic_account.rs index e18d3cc072..6a57f26359 100644 --- a/primitives/transaction/src/account/basic_account.rs +++ b/primitives/transaction/src/account/basic_account.rs @@ -2,8 +2,8 @@ use nimiq_primitives::{account::AccountType, policy::Policy}; use nimiq_serde::Deserialize; use crate::{ - account::AccountTransactionVerification, SignatureProof, Transaction, TransactionError, - TransactionFlags, + account::AccountTransactionVerification, PoWSignatureProof, SignatureProof, Transaction, + TransactionError, TransactionFlags, }; /// The verifier trait for a basic account. This only uses data available in the transaction. @@ -56,7 +56,11 @@ impl AccountTransactionVerification for BasicAccountVerifier { } // Verify signer & signature. - let signature_proof = SignatureProof::deserialize_all(&transaction.proof)?; + let signature_proof = if transaction.network_id.is_albatross() { + SignatureProof::deserialize_all(&transaction.proof)? + } else { + PoWSignatureProof::deserialize_all(&transaction.proof)?.into_pos() + }; if !signature_proof.is_signed_by(&transaction.sender) || !signature_proof.verify(&transaction.serialize_content()) diff --git a/primitives/transaction/src/account/htlc_contract.rs b/primitives/transaction/src/account/htlc_contract.rs index e7cad51fcf..2c4a302896 100644 --- a/primitives/transaction/src/account/htlc_contract.rs +++ b/primitives/transaction/src/account/htlc_contract.rs @@ -10,8 +10,8 @@ use nimiq_primitives::account::AccountType; use nimiq_serde::{Deserialize, Serialize}; use crate::{ - account::AccountTransactionVerification, SignatureProof, Transaction, TransactionError, - TransactionFlags, + account::AccountTransactionVerification, PoWSignatureProof, SignatureProof, Transaction, + TransactionError, TransactionFlags, }; /// The verifier trait for a hash time locked contract. This only uses data available in the transaction. @@ -46,18 +46,28 @@ impl AccountTransactionVerification for HashedTimeLockedContractVerifier { return Err(TransactionError::InvalidForRecipient); } - if transaction.recipient_data.len() != (20 * 2 + 1 + 32 + 1 + 8) - && transaction.recipient_data.len() != (20 * 2 + 1 + 64 + 1 + 8) - { - warn!( - data_len = transaction.recipient_data.len(), - ?transaction, - "Invalid data length. For the following transaction", - ); - return Err(TransactionError::InvalidData); - } + if transaction.network_id.is_albatross() { + if transaction.recipient_data.len() != (20 * 2 + 1 + 32 + 1 + 8) + && transaction.recipient_data.len() != (20 * 2 + 1 + 64 + 1 + 8) + { + warn!( + data_len = transaction.recipient_data.len(), + ?transaction, + "Invalid data length. For the following transaction", + ); + return Err(TransactionError::InvalidData); + } - CreationTransactionData::parse(transaction)?.verify() + CreationTransactionData::parse(transaction)?.verify() + } else { + // PoW HTLC creation data specified the timeout (last field) as a u32 block number instead of a timestamp. + if transaction.recipient_data.len() != (20 * 2 + 1 + 32 + 1 + 4) + && transaction.recipient_data.len() != (20 * 2 + 1 + 64 + 1 + 4) + { + return Err(TransactionError::InvalidData); + } + PoWCreationTransactionData::parse(transaction)?.verify() + } } fn verify_outgoing_transaction(transaction: &Transaction) -> Result<(), TransactionError> { @@ -71,8 +81,13 @@ impl AccountTransactionVerification for HashedTimeLockedContractVerifier { return Err(TransactionError::Overflow); } - let proof = OutgoingHTLCTransactionProof::parse(transaction)?; - proof.verify(transaction)?; + if transaction.network_id.is_albatross() { + let proof = OutgoingHTLCTransactionProof::parse(transaction)?; + proof.verify(transaction)?; + } else { + let proof = PoWOutgoingHTLCTransactionProof::parse(transaction)?; + proof.verify(transaction)?; + } Ok(()) } @@ -203,17 +218,31 @@ add_serialization_fns_typed_arr!(AnyHash64, AnyHash64::SIZE); #[derive(Clone, Debug, Default, Deserialize, Serialize)] pub struct CreationTransactionData { + /// Address that is allowed to redeem the funds after the timeout. pub sender: Address, + /// Address that is allowed to redeem the funds before the timeout. pub recipient: Address, + /// The hash root of the contract. The recipient can redeem the funds before the timeout by providing + /// a pre-image that hashes to this root. pub hash_root: AnyHash, + /// The number of times the pre-image must be hashed to match the `hash_root`. Must be at least 1. + /// A number higher than 1 allows the recipient to provide an already hashed pre-image, with the + /// remaining number of hashes required to match the `hash_root` corresponding to the fraction of + /// the funds that can be claimed. pub hash_count: u8, #[serde(with = "nimiq_serde::fixint::be")] + /// The timeout as a millisecond timestamp before which the `recipient` and after which the `sender` + /// can claim the funds. pub timeout: u64, } impl CreationTransactionData { + pub fn parse_data(data: &[u8]) -> Result { + Ok(Self::deserialize_all(data)?) + } + pub fn parse(transaction: &Transaction) -> Result { - Ok(Self::deserialize_all(&transaction.recipient_data)?) + Self::parse_data(&transaction.recipient_data) } pub fn verify(&self) -> Result<(), TransactionError> { @@ -225,6 +254,65 @@ impl CreationTransactionData { } } +/// This struct represents HTLC creation data in the Proof-of-Work chain. The only difference to the data in +/// the Albatross chain is that the `timeout` was a u32 block number in PoW instead of a u64 timestamp. +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +pub struct PoWCreationTransactionData { + /// Address that is allowed to redeem the funds after the timeout. + pub sender: Address, + /// Address that is allowed to redeem the funds before the timeout. + pub recipient: Address, + /// The hash root of the contract. The recipient can redeem the funds before the timeout by providing + /// a pre-image that hashes to this root. + pub hash_root: AnyHash, + /// The number of times the pre-image must be hashed to match the `hash_root`. Must be at least 1. + /// A number higher than 1 allows the recipient to provide an already hashed pre-image, with the + /// remaining number of hashes required to match the `hash_root` corresponding to the fraction of + /// the funds that can be claimed. + pub hash_count: u8, + #[serde(with = "nimiq_serde::fixint::be")] + /// The timeout as a block height before which the `recipient` and after which the `sender` + /// can claim the funds. + pub timeout: u32, +} + +impl PoWCreationTransactionData { + pub fn parse_data(data: &[u8]) -> Result { + Ok(Self::deserialize_all(data)?) + } + + pub fn parse(transaction: &Transaction) -> Result { + Self::parse_data(&transaction.recipient_data) + } + + pub fn verify(&self) -> Result<(), TransactionError> { + if self.hash_count == 0 { + return Err(TransactionError::InvalidData); + } + Ok(()) + } + + pub fn into_pos( + self, + genesis_block_number: u32, + genesis_timestamp: u64, + ) -> CreationTransactionData { + let timeout = if self.timeout <= genesis_block_number { + genesis_timestamp - (genesis_block_number - self.timeout) as u64 * 60_000 + } else { + genesis_timestamp + (self.timeout - genesis_block_number) as u64 * 60_000 + }; + + CreationTransactionData { + sender: self.sender, + recipient: self.recipient, + hash_root: self.hash_root, + hash_count: self.hash_count, + timeout, + } + } +} + /// The `OutgoingHTLCTransactionProof` represents a serializable form of all possible proof types /// for a transaction from a HTLC contract. /// @@ -340,6 +428,135 @@ impl OutgoingHTLCTransactionProof { } } +/// This struct represents a HTLC redeem proof for the regular transfer case in the Proof-of-Work chain. +/// Differences to the Proof-of-Stake is the serialization (PoW had a different position for the algorithm type +/// and no PreImage type prefix) and that the signature proof is a PoWSignatureProof and thus shorter than in PoS. +#[derive(Clone, Debug)] +pub struct PoWRegularTransfer { + // PoW regular transfers encode the hash algorithm as the first u8 byte, + // but in Rust, the algorithm is encoded in the hash_root AnyHash enum. + hash_depth: u8, + hash_root: AnyHash, + pre_image: PreImage, + signature_proof: PoWSignatureProof, +} + +/// Enum over the different types of outgoing HTLC transaction proofs in the Proof-of-Work chain. +/// The differences to Proof-of-Stake are the variant IDs (they start at 1 in PoW, while they start at 0 in PoS) +/// and that all signature proofs are PoWSignatureProofs. +#[derive(Clone, Debug, Deserialize)] +#[repr(u8)] +pub enum PoWOutgoingHTLCTransactionProof { + DummyZero, // In PoW, RegularTransfer has ID 1, so we need a dummy ID 0 + RegularTransfer(PoWRegularTransfer), + EarlyResolve { + signature_proof_recipient: PoWSignatureProof, + signature_proof_sender: PoWSignatureProof, + }, + TimeoutResolve { + signature_proof_sender: PoWSignatureProof, + }, +} + +impl PoWOutgoingHTLCTransactionProof { + pub fn parse(transaction: &Transaction) -> Result { + Ok(Self::deserialize_all(&transaction.proof)?) + } + + pub fn verify(&self, transaction: &Transaction) -> Result<(), TransactionError> { + // Verify proof. + let tx_content = transaction.serialize_content(); + let tx_buf = tx_content.as_slice(); + + match self { + PoWOutgoingHTLCTransactionProof::DummyZero => { + return Err(TransactionError::InvalidProof); + } + PoWOutgoingHTLCTransactionProof::RegularTransfer(PoWRegularTransfer { + hash_depth, + hash_root, + pre_image, + signature_proof, + }) => { + let mut tmp_hash = pre_image.clone(); + for _ in 0..*hash_depth { + match &hash_root { + AnyHash::Blake2b(_) => { + tmp_hash = PreImage::from( + Blake2bHasher::default().digest(tmp_hash.as_bytes()), + ); + } + AnyHash::Sha256(_) => { + tmp_hash = + PreImage::from(Sha256Hasher::default().digest(tmp_hash.as_bytes())); + } + AnyHash::Sha512(_) => { + tmp_hash = + PreImage::from(Sha512Hasher::default().digest(tmp_hash.as_bytes())); + } + } + } + + if hash_root.as_bytes() != tmp_hash.as_bytes() { + return Err(TransactionError::InvalidProof); + } + + if !signature_proof.verify(tx_buf) { + return Err(TransactionError::InvalidProof); + } + } + PoWOutgoingHTLCTransactionProof::EarlyResolve { + signature_proof_recipient, + signature_proof_sender, + } => { + if !signature_proof_recipient.verify(tx_buf) + || !signature_proof_sender.verify(tx_buf) + { + return Err(TransactionError::InvalidProof); + } + } + PoWOutgoingHTLCTransactionProof::TimeoutResolve { + signature_proof_sender, + } => { + if !signature_proof_sender.verify(tx_buf) { + return Err(TransactionError::InvalidProof); + } + } + } + + Ok(()) + } + + pub fn into_pos(self) -> OutgoingHTLCTransactionProof { + match self { + Self::DummyZero => panic!("DummyZero is not a valid PoWOutgoingHTLCTransactionProof"), + Self::RegularTransfer(PoWRegularTransfer { + hash_depth, + hash_root, + pre_image, + signature_proof, + }) => OutgoingHTLCTransactionProof::RegularTransfer { + hash_depth, + hash_root, + pre_image, + signature_proof: signature_proof.into_pos(), + }, + Self::EarlyResolve { + signature_proof_recipient, + signature_proof_sender, + } => OutgoingHTLCTransactionProof::EarlyResolve { + signature_proof_recipient: signature_proof_recipient.into_pos(), + signature_proof_sender: signature_proof_sender.into_pos(), + }, + Self::TimeoutResolve { + signature_proof_sender, + } => OutgoingHTLCTransactionProof::TimeoutResolve { + signature_proof_sender: signature_proof_sender.into_pos(), + }, + } + } +} + mod serde_derive { use std::{borrow::Cow, fmt, str::FromStr}; @@ -348,10 +565,17 @@ mod serde_derive { ser::{Serialize, SerializeStruct, Serializer}, }; - use super::{AnyHash, AnyHash32, AnyHash64, PreImage}; + use super::{AnyHash, AnyHash32, AnyHash64, PoWRegularTransfer, PoWSignatureProof, PreImage}; const ANYHASH_FIELDS: &[&str] = &["algorithm", "hash"]; const PREIMAGE_FIELDS: &[&str] = &["type", "pre_image"]; + const POW_REGULAR_TRANSFER_FIELDS: &[&str] = &[ + "hash_algorithm", + "hash_depth", + "hash_root", + "pre_image", + "signature_proof", + ]; #[derive(nimiq_serde::Deserialize)] #[serde(field_identifier, rename_all = "lowercase")] @@ -362,6 +586,7 @@ mod serde_derive { struct PreImageVisitor; struct AnyHashVisitor; + struct PoWRegularTransferVisitor; impl Serialize for AnyHash { fn serialize(&self, serializer: S) -> Result @@ -574,6 +799,94 @@ mod serde_derive { } } } + + impl<'de> Visitor<'de> for PoWRegularTransferVisitor { + type Value = PoWRegularTransfer; + + fn expecting(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { + write!(f, "a PoWRegularTransfer") + } + + fn visit_seq(self, mut seq: A) -> Result + where + A: SeqAccess<'de>, + { + let hash_algorithm: u8 = seq + .next_element()? + .ok_or_else(|| Error::invalid_length(0, &self))?; + + let hash_depth: u8 = seq + .next_element()? + .ok_or_else(|| Error::invalid_length(1, &self))?; + + let hash_root = match hash_algorithm { + 1u8 => { + let hash_root: AnyHash32 = seq + .next_element()? + .ok_or_else(|| Error::invalid_length(2, &self))?; + AnyHash::Blake2b(hash_root) + } + 3u8 => { + let hash_root: AnyHash32 = seq + .next_element()? + .ok_or_else(|| Error::invalid_length(2, &self))?; + AnyHash::Sha256(hash_root) + } + 4u8 => { + let hash_root: AnyHash64 = seq + .next_element()? + .ok_or_else(|| Error::invalid_length(2, &self))?; + + AnyHash::Sha512(hash_root) + } + _ => { + return Err(Error::custom(format!( + "Invalid hash algorithm type: {}", + hash_algorithm + ))) + } + }; + + let pre_image = match hash_root { + AnyHash::Blake2b(_) | AnyHash::Sha256(_) => { + let pre_image: AnyHash32 = seq + .next_element()? + .ok_or_else(|| Error::invalid_length(3, &self))?; + PreImage::PreImage32(pre_image) + } + AnyHash::Sha512(_) => { + let pre_image: AnyHash64 = seq + .next_element()? + .ok_or_else(|| Error::invalid_length(3, &self))?; + PreImage::PreImage64(pre_image) + } + }; + + let signature_proof: PoWSignatureProof = seq + .next_element()? + .ok_or_else(|| Error::invalid_length(4, &self))?; + + Ok(PoWRegularTransfer { + hash_depth, + hash_root, + pre_image, + signature_proof, + }) + } + } + + impl<'de> Deserialize<'de> for PoWRegularTransfer { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + deserializer.deserialize_struct( + "PoWRegularTransfer", + POW_REGULAR_TRANSFER_FIELDS, + PoWRegularTransferVisitor, + ) + } + } } #[cfg(test)] @@ -581,7 +894,7 @@ mod tests { use nimiq_serde::{Deserialize, Serialize}; use nimiq_test_log::test; - use super::{AnyHash, AnyHash32, AnyHash64, PreImage}; + use super::{AnyHash, AnyHash32, AnyHash64, PoWOutgoingHTLCTransactionProof, PreImage}; fn sample_anyhashes() -> [AnyHash; 3] { let hash_32 = AnyHash32([0xC; AnyHash32::SIZE]); @@ -756,4 +1069,11 @@ mod tests { ) .is_err()); // too large for 64 byte } + + #[test] + fn it_can_correctly_deserialize_pow_outgoing_htlc_transaction_proof() { + let bin = hex::decode("0103013913543fa4e5b6c41176ee552d314db28d786bd87f103ee25f49f4e2555e51d1bff5b88ef94cd7c2ba354a8e4b50fef063ab1659646570b34effbb48f36ecb4c08600ec9f0d44dc8d43275c705d7780caa31497d2620da4d7838d10574a6dfa100410b82decb73b7c6f4047b4fb504000c364edd9a3337e5194b60f896d31904ccab8bf310cf808fd98a9b3b13096b6701d53bbba8402465d08cb99948c8407500") + .unwrap(); + let _ = PoWOutgoingHTLCTransactionProof::deserialize_from_vec(&bin).unwrap(); + } } diff --git a/primitives/transaction/src/account/vesting_contract.rs b/primitives/transaction/src/account/vesting_contract.rs index df03749f0a..b776c88828 100644 --- a/primitives/transaction/src/account/vesting_contract.rs +++ b/primitives/transaction/src/account/vesting_contract.rs @@ -3,8 +3,8 @@ use nimiq_primitives::{account::AccountType, coin::Coin}; use nimiq_serde::{Deserialize, Serialize, SerializedSize}; use crate::{ - account::AccountTransactionVerification, SignatureProof, Transaction, TransactionError, - TransactionFlags, + account::AccountTransactionVerification, PoWSignatureProof, SignatureProof, Transaction, + TransactionError, TransactionFlags, }; /// The verifier trait for a basic account. This only uses data available in the transaction. @@ -39,7 +39,11 @@ impl AccountTransactionVerification for VestingContractVerifier { return Err(TransactionError::InvalidForRecipient); } - CreationTransactionData::parse(transaction).map(|_| ()) + if transaction.network_id.is_albatross() { + CreationTransactionData::parse(transaction).map(|_| ()) + } else { + PoWCreationTransactionData::parse(transaction).map(|_| ()) + } } fn verify_outgoing_transaction(transaction: &Transaction) -> Result<(), TransactionError> { @@ -54,7 +58,11 @@ impl AccountTransactionVerification for VestingContractVerifier { } // Verify signature. - let signature_proof = SignatureProof::deserialize_all(&transaction.proof)?; + let signature_proof = if transaction.network_id.is_albatross() { + SignatureProof::deserialize_all(&transaction.proof)? + } else { + PoWSignatureProof::deserialize_all(&transaction.proof)?.into_pos() + }; if !signature_proof.verify(&transaction.serialize_content()) { warn!("Invalid signature for this transaction:\n{:?}", transaction); @@ -72,7 +80,7 @@ impl AccountTransactionVerification for VestingContractVerifier { pub struct CreationTransactionData { /// The owner of the contract, the only address that can interact with it. pub owner: Address, - /// The block height at which the release schedule starts. + /// The timestamp at which the release schedule starts. pub start_time: u64, /// The frequency at which funds are released. pub time_step: u64, @@ -117,7 +125,7 @@ impl CreationTransactionData { pub fn parse_data(data: &[u8], tx_value: Coin) -> Result { Ok(match data.len() { CreationTransactionData8::SIZE => { - // Only timestamp: vest full amount at that time + // Only step length: vest full amount at that time let CreationTransactionData8 { owner, time_step } = CreationTransactionData8::deserialize_all(data)?; CreationTransactionData { @@ -162,8 +170,9 @@ impl CreationTransactionData { _ => return Err(TransactionError::InvalidData), }) } - pub fn parse(tx: &Transaction) -> Result { - CreationTransactionData::parse_data(&tx.recipient_data, tx.value) + + pub fn parse(transaction: &Transaction) -> Result { + CreationTransactionData::parse_data(&transaction.recipient_data, transaction.value) } pub fn to_tx_data(&self) -> Vec { @@ -198,3 +207,127 @@ impl CreationTransactionData { } } } + +/// This struct represents vesting contract creation data in the Proof-of-Work chain. The differences +/// to the data in the Albatross chain are that the vesting period start and step length were u32 block numbers +/// in PoW instead of a u64 timestamps, making this serialization shorter. +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +pub struct PoWCreationTransactionData { + /// The owner of the contract, the only address that can interact with it. + pub owner: Address, + /// The block height at which the release schedule starts. + pub start_block: u32, + /// The frequency at which funds are released. + pub step_blocks: u32, + /// The amount released at each [`step_blocks`](Self::step_blocks). + pub step_amount: Coin, + /// Initially locked balance. + pub total_amount: Coin, +} + +#[derive(Deserialize, Serialize, SerializedSize)] +struct PoWCreationTransactionData4 { + pub owner: Address, + #[serde(with = "nimiq_serde::fixint::be")] + #[serialize_size(fixed_size)] + pub step_blocks: u32, +} +#[derive(Deserialize, Serialize, SerializedSize)] +struct PoWCreationTransactionData16 { + pub owner: Address, + #[serde(with = "nimiq_serde::fixint::be")] + #[serialize_size(fixed_size)] + pub start_block: u32, + #[serde(with = "nimiq_serde::fixint::be")] + #[serialize_size(fixed_size)] + pub step_blocks: u32, + pub step_amount: Coin, +} +#[derive(Deserialize, Serialize, SerializedSize)] +struct PoWCreationTransactionData24 { + pub owner: Address, + #[serde(with = "nimiq_serde::fixint::be")] + #[serialize_size(fixed_size)] + pub start_block: u32, + #[serde(with = "nimiq_serde::fixint::be")] + #[serialize_size(fixed_size)] + pub step_blocks: u32, + pub step_amount: Coin, + pub total_amount: Coin, +} + +impl PoWCreationTransactionData { + pub fn parse_data(data: &[u8], tx_value: Coin) -> Result { + Ok(match data.len() { + PoWCreationTransactionData4::SIZE => { + // Only step length: vest full amount at that block + let PoWCreationTransactionData4 { owner, step_blocks } = + PoWCreationTransactionData4::deserialize_all(data)?; + PoWCreationTransactionData { + owner, + start_block: 1, // PoW genesis block number + step_blocks, + step_amount: tx_value, + total_amount: tx_value, + } + } + PoWCreationTransactionData16::SIZE => { + let PoWCreationTransactionData16 { + owner, + start_block, + step_blocks, + step_amount, + } = PoWCreationTransactionData16::deserialize_all(data)?; + PoWCreationTransactionData { + owner, + start_block, + step_blocks, + step_amount, + total_amount: tx_value, + } + } + PoWCreationTransactionData24::SIZE => { + let PoWCreationTransactionData24 { + owner, + start_block, + step_blocks, + step_amount, + total_amount, + } = PoWCreationTransactionData24::deserialize_all(data)?; + PoWCreationTransactionData { + owner, + start_block, + step_blocks, + step_amount, + total_amount, + } + } + _ => return Err(TransactionError::InvalidData), + }) + } + + pub fn parse(transaction: &Transaction) -> Result { + PoWCreationTransactionData::parse_data(&transaction.recipient_data, transaction.value) + } + + pub fn into_pos( + self, + genesis_block_number: u32, + genesis_timestamp: u64, + ) -> CreationTransactionData { + let start_time = if self.start_block <= genesis_block_number { + genesis_timestamp - (genesis_block_number - self.start_block) as u64 * 60_000 + } else { + genesis_timestamp + (self.start_block - genesis_block_number) as u64 * 60_000 + }; + let time_step = self.step_blocks as u64 * 60_000; + + CreationTransactionData { + owner: self.owner, + start_time, + time_step, + step_amount: self.step_amount, + total_amount: self.total_amount, + } + } +} diff --git a/primitives/transaction/src/signature_proof.rs b/primitives/transaction/src/signature_proof.rs index 035d7d0e4b..e53d3c7d59 100644 --- a/primitives/transaction/src/signature_proof.rs +++ b/primitives/transaction/src/signature_proof.rs @@ -6,7 +6,7 @@ use nimiq_hash::{Blake2bHash, Hash, HashOutput, Sha256Hash}; use nimiq_keys::{Address, Ed25519PublicKey, Ed25519Signature, PublicKey, Signature}; use nimiq_primitives::policy::Policy; use nimiq_serde::{Deserialize, Serialize, SerializedMaxSize}; -use nimiq_utils::merkle::Blake2bMerklePath; +use nimiq_utils::merkle::{Blake2bMerklePath, PoWBlake2bMerklePath}; use url::Url; #[derive(Clone, Debug)] @@ -350,6 +350,45 @@ impl WebauthnExtraFields { } } +/// This struct represents signature proofs in the Proof-of-Work chain. The difference to proofs on the +/// Albatross chain are that PoW signature could only be of type Ed25519 (they had no type-and-flags byte) +/// and that the merkle path had a different serialization. +#[derive(Clone, Debug, Deserialize)] +pub struct PoWSignatureProof { + pub public_key: Ed25519PublicKey, + pub merkle_path: PoWBlake2bMerklePath, + pub signature: Ed25519Signature, +} + +impl PoWSignatureProof { + pub fn verify(&self, message: &[u8]) -> bool { + self.public_key.verify(&self.signature, message) + } + + pub fn into_pos(self) -> SignatureProof { + SignatureProof { + public_key: PublicKey::Ed25519(self.public_key), + merkle_path: self.merkle_path.into_pos(), + signature: Signature::Ed25519(self.signature), + webauthn_fields: None, + } + } +} + +#[test] +fn it_can_correctly_deserialize_pow_signature_proof() { + let bin = hex::decode("08600ec9f0d44dc8d43275c705d7780caa31497d2620da4d7838d10574a6dfa100410b82decb73b7c6f4047b4fb504000c364edd9a3337e5194b60f896d31904ccab8bf310cf808fd98a9b3b13096b6701d53bbba8402465d08cb99948c8407500") + .unwrap(); + let _ = PoWSignatureProof::deserialize_all(&bin).unwrap(); +} + +#[test] +fn it_can_correctly_deserialize_pow_multisig_signature_proof() { + let bin = hex::decode("c79090f344bf7ed4cdd6c25512ee61d1d5fe9cff643263342996ba3448df189f0280de8d7ee7e54f301095294d494024430c8b251b4ebf9b1384922dc7f9dd24422f830e231d26cdc3bbd1f55f1918757568522acae62c21e8046190ea84d6e8ff160caadca71723067d5080d6c3858b61ef8cdf286326818e90ddbefe23af2d529cef7654be5b99bb418786d49e164b24f9db1c482545d8e4473804a53b889e4b07") + .unwrap(); + let _ = PoWSignatureProof::deserialize_all(&bin).unwrap(); +} + mod serde_derive { use std::fmt; diff --git a/tools/src/signtx/main.rs b/tools/src/signtx/main.rs index 21120ae309..096612baa4 100644 --- a/tools/src/signtx/main.rs +++ b/tools/src/signtx/main.rs @@ -96,7 +96,7 @@ fn run_app() -> Result<(), Error> { .ok_or(AppError::ValidityStartHeight)?; let network_id = match matches.get_one::("network_id") { Some(s) => NetworkId::from_str(s)?, - None => NetworkId::Main, + None => NetworkId::MainAlbatross, }; Transaction::new_basic( from_address, diff --git a/transaction-builder/src/lib.rs b/transaction-builder/src/lib.rs index e83b3fc964..fab409e3e6 100644 --- a/transaction-builder/src/lib.rs +++ b/transaction-builder/src/lib.rs @@ -139,7 +139,7 @@ impl TransactionBuilder { /// recipient, /// Coin::from_u64_unchecked(100), /// 1, - /// NetworkId::Main + /// NetworkId::MainAlbatross /// ); /// /// let proof_builder = builder.generate().unwrap(); @@ -211,7 +211,7 @@ impl TransactionBuilder { /// recipient, /// Coin::from_u64_unchecked(100), /// 1, - /// NetworkId::Main + /// NetworkId::MainAlbatross /// ); /// builder.with_fee(Coin::from_u64_unchecked(1337)); /// @@ -267,7 +267,7 @@ impl TransactionBuilder { /// recipient, /// Coin::from_u64_unchecked(100), /// 1, - /// NetworkId::Main + /// NetworkId::MainAlbatross /// ); /// builder.with_fee(Coin::from_u64_unchecked(1337)); /// @@ -297,7 +297,7 @@ impl TransactionBuilder { /// use nimiq_primitives::networks::NetworkId; /// /// let mut builder = TransactionBuilder::new(); - /// builder.with_network_id(NetworkId::Main); + /// builder.with_network_id(NetworkId::MainAlbatross); /// ``` pub fn with_network_id(&mut self, network_id: NetworkId) -> &mut Self { self.network_id = Some(network_id); @@ -346,7 +346,7 @@ impl TransactionBuilder { /// recipient, /// Coin::from_u64_unchecked(100), /// 1, - /// NetworkId::Main + /// NetworkId::MainAlbatross /// ); /// builder.with_fee(Coin::from_u64_unchecked(1337)); /// diff --git a/transaction-builder/src/proof/htlc_contract.rs b/transaction-builder/src/proof/htlc_contract.rs index 0bbcc5c369..02961183bf 100644 --- a/transaction-builder/src/proof/htlc_contract.rs +++ b/transaction-builder/src/proof/htlc_contract.rs @@ -50,7 +50,7 @@ impl HtlcProofBuilder { /// recipient, /// Coin::from_u64_unchecked(100), /// 1, - /// NetworkId::Main + /// NetworkId::MainAlbatross /// ); /// /// let proof_builder = builder.generate().unwrap(); @@ -62,7 +62,7 @@ impl HtlcProofBuilder { /// /// let final_transaction = htlc_proof_builder.generate(); /// assert!(final_transaction.is_some()); - /// assert!(final_transaction.unwrap().verify(NetworkId::Main).is_ok()); + /// assert!(final_transaction.unwrap().verify(NetworkId::MainAlbatross).is_ok()); /// ``` pub fn signature_with_key_pair(&self, key_pair: &KeyPair) -> SignatureProof { let signature = key_pair.sign(&self.transaction.serialize_content()); @@ -95,7 +95,7 @@ impl HtlcProofBuilder { /// recipient, /// Coin::from_u64_unchecked(100), /// 1, - /// NetworkId::Main + /// NetworkId::MainAlbatross /// ); /// /// let proof_builder = builder.generate().unwrap(); @@ -106,7 +106,7 @@ impl HtlcProofBuilder { /// /// let final_transaction = htlc_proof_builder.generate(); /// assert!(final_transaction.is_some()); - /// assert!(final_transaction.unwrap().verify(NetworkId::Main).is_ok()); + /// assert!(final_transaction.unwrap().verify(NetworkId::MainAlbatross).is_ok()); /// ``` /// /// [`signature_with_key_pair`]: struct.HtlcProofBuilder.html#method.signature_with_key_pair @@ -144,7 +144,7 @@ impl HtlcProofBuilder { /// recipient, /// Coin::from_u64_unchecked(100), /// 1, - /// NetworkId::Main + /// NetworkId::MainAlbatross /// ); /// /// let proof_builder = builder.generate().unwrap(); @@ -156,7 +156,7 @@ impl HtlcProofBuilder { /// /// let final_transaction = htlc_proof_builder.generate(); /// assert!(final_transaction.is_some()); - /// assert!(final_transaction.unwrap().verify(NetworkId::Main).is_ok()); + /// assert!(final_transaction.unwrap().verify(NetworkId::MainAlbatross).is_ok()); /// ``` /// /// [`signature_with_key_pair`]: struct.HtlcProofBuilder.html#method.signature_with_key_pair @@ -241,7 +241,7 @@ impl HtlcProofBuilder { /// recipient, /// Coin::from_u64_unchecked(100), /// 1, - /// NetworkId::Main + /// NetworkId::MainAlbatross /// ); /// /// let proof_builder = builder.generate().unwrap(); @@ -252,7 +252,7 @@ impl HtlcProofBuilder { /// /// let final_transaction = htlc_proof_builder.generate(); /// assert!(final_transaction.is_some()); - /// assert!(final_transaction.unwrap().verify(NetworkId::Main).is_ok()); + /// assert!(final_transaction.unwrap().verify(NetworkId::MainAlbatross).is_ok()); /// ``` /// /// [`signature_with_key_pair`]: struct.HtlcProofBuilder.html#method.signature_with_key_pair @@ -313,7 +313,7 @@ impl HtlcProofBuilder { /// recipient, /// Coin::from_u64_unchecked(100), /// 1, - /// NetworkId::Main + /// NetworkId::MainAlbatross /// ); /// /// let proof_builder = builder.generate().unwrap(); @@ -324,7 +324,7 @@ impl HtlcProofBuilder { /// /// let final_transaction = htlc_proof_builder.generate(); /// assert!(final_transaction.is_some()); - /// assert!(final_transaction.unwrap().verify(NetworkId::Main).is_ok()); + /// assert!(final_transaction.unwrap().verify(NetworkId::MainAlbatross).is_ok()); /// ``` /// /// [`signature_with_key_pair`]: struct.HtlcProofBuilder.html#method.signature_with_key_pair diff --git a/transaction-builder/src/proof/mod.rs b/transaction-builder/src/proof/mod.rs index 9728a6e67d..64791357ac 100644 --- a/transaction-builder/src/proof/mod.rs +++ b/transaction-builder/src/proof/mod.rs @@ -84,7 +84,7 @@ impl TransactionProofBuilder { /// recipient, /// Coin::from_u64_unchecked(100), /// 1, - /// NetworkId::Main + /// NetworkId::MainAlbatross /// ); /// builder.with_fee(Coin::from_u64_unchecked(1337)); /// @@ -123,7 +123,7 @@ impl TransactionProofBuilder { /// recipient, /// Coin::from_u64_unchecked(100), /// 1, - /// NetworkId::Main + /// NetworkId::MainAlbatross /// ); /// builder.with_fee(Coin::from_u64_unchecked(1337)); /// @@ -175,7 +175,7 @@ impl TransactionProofBuilder { /// recipient, /// Coin::from_u64_unchecked(100), /// 1, - /// NetworkId::Main + /// NetworkId::MainAlbatross /// ); /// /// let proof_builder = builder.generate().unwrap(); @@ -186,7 +186,7 @@ impl TransactionProofBuilder { /// /// let final_transaction = htlc_proof_builder.generate(); /// assert!(final_transaction.is_some()); - /// assert!(final_transaction.unwrap().verify(NetworkId::Main).is_ok()); + /// assert!(final_transaction.unwrap().verify(NetworkId::MainAlbatross).is_ok()); /// ``` /// /// [`HtlcProofBuilder`]: htlc_contract/struct.HtlcProofBuilder.html @@ -226,7 +226,7 @@ impl TransactionProofBuilder { /// recipient.generate().unwrap(), /// Coin::from_u64_unchecked(0), // must be zero because of signaling transaction /// 1, - /// NetworkId::Main + /// NetworkId::MainAlbatross /// ); /// /// let proof_builder = tx_builder.generate().unwrap(); @@ -241,7 +241,7 @@ impl TransactionProofBuilder { /// /// let final_transaction = basic_proof_builder.generate(); /// assert!(final_transaction.is_some()); - /// assert!(final_transaction.unwrap().verify(NetworkId::Main).is_ok()); + /// assert!(final_transaction.unwrap().verify(NetworkId::MainAlbatross).is_ok()); /// ``` /// /// [`StakingDataBuilder`]: staking_contract/struct.StakingDataBuilder.html @@ -279,7 +279,7 @@ impl TransactionProofBuilder { /// recipient, /// Coin::from_u64_unchecked(100), /// 1, - /// NetworkId::Main + /// NetworkId::MainAlbatross /// ); /// /// let proof_builder = tx_builder.generate().unwrap(); diff --git a/utils/src/merkle/mod.rs b/utils/src/merkle/mod.rs index 84baa49075..8df888d61d 100644 --- a/utils/src/merkle/mod.rs +++ b/utils/src/merkle/mod.rs @@ -1,6 +1,8 @@ use std::{borrow::Cow, cmp::Ordering, error, fmt, io::Write, marker::PhantomData}; use nimiq_hash::{Blake2bHash, HashOutput, Hasher, SerializeContent}; +#[cfg(test)] +use nimiq_serde::Deserialize as NimiqDeserialize; use serde::{ de::{Error, SeqAccess, Visitor}, ser::SerializeStruct, @@ -244,6 +246,125 @@ impl<'de, H: HashOutput> Deserialize<'de> for MerklePath { pub type Blake2bMerklePath = MerklePath; +#[test] +fn it_can_correctly_deserialize_merkle_path() { + // PoS merkle paths have the u8 count of nodes as the first byte, then the left-bits prefixed by + // their own varint length (0b01 here), then the node hashes themselves, prefixed by another varint length + // which is the same as the count in the first byte. + let bin = hex::decode("02018002de8d7ee7e54f301095294d494024430c8b251b4ebf9b1384922dc7f9dd24422f830e231d26cdc3bbd1f55f1918757568522acae62c21e8046190ea84d6e8ff16") + .unwrap(); + let _ = MerklePath::::deserialize_all(&bin).unwrap(); +} + +/// This struct represents the serialization of merkle paths in the Proof-of-Work chain, which was +/// different from the serialization in the Proof-of-Stake chain (it was more efficient but harder to parse). +#[derive(Clone, Debug, Default, Eq, PartialEq)] +pub struct PoWMerklePath { + nodes: Vec>, +} + +impl PoWMerklePath { + pub fn empty() -> Self { + PoWMerklePath { nodes: Vec::new() } + } + + pub fn into_pos(self) -> MerklePath { + MerklePath { nodes: self.nodes } + } +} + +struct PoWMerklePathVisitor { + phantom: PhantomData, +} + +impl<'de, H: HashOutput> Visitor<'de> for PoWMerklePathVisitor { + type Value = PoWMerklePath; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("struct PoWMerklePath") + } + + fn visit_seq(self, mut seq: A) -> Result + where + A: SeqAccess<'de>, + { + let count: u8 = seq + .next_element()? + .ok_or_else(|| A::Error::invalid_length(0, &self))?; + if count == 0 { + return Ok(PoWMerklePath::empty()); + } + let count = count as usize; + let left_bits_size = count.div_ceil(8); + let mut left_bits = Vec::with_capacity(left_bits_size); + for i in 0..left_bits_size { + let bits: u8 = seq + .next_element()? + .ok_or_else(|| A::Error::invalid_length(i, &self))?; + left_bits.push(bits); + } + let mut node_hashes = Vec::with_capacity(count); + for i in 0..count { + let hash: H = seq + .next_element()? + .ok_or_else(|| A::Error::invalid_length(i, &self))?; + node_hashes.push(hash); + } + let mut nodes: Vec> = Vec::with_capacity(count); + for (i, node_hash) in node_hashes.iter().enumerate().take(count) { + nodes.push(MerklePathNode { + left: MerklePath::::decompress(i, &left_bits), + hash: node_hash.clone(), + }); + } + Ok(PoWMerklePath { nodes }) + } +} + +impl<'de, H: HashOutput> Deserialize<'de> for PoWMerklePath { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + // I have tried many ways to deserialize the PoW merkle path format with serde/postcard, but none + // allowed me to read the raw bytes and then parse vecs for which I already knew the length of (so + // that serde/postcard does not try to read a length prefix). + // I cannot use `deserializer.deserialize_bytes()` or anything similar that deserializes vecs, because + // they internally read the first byte as the length byte and then only let you work with that number + // of following bytes. + // Additionally, the number of fields passed to `deserializer.deserialize_stuct()` limits the number + // of times one can call `seq.next_element()` in the visitor before it throws a `SerdeDeCustom` error. + // Since we don't know how many nodes we need to parse from the input without looking at the input first + // (need to read that first byte), we cannot use it. + // `deserializer.deserialize_tuple()` has the same limitation, but we can work around it by giving it + // the maximum number of fields we will ever need to parse (length of a merkle path is serialized as a u8, + // so can be at most 255. That means the highest possible number of elements is 1 length field + + // 255.div_ceil(8) bitset bytes + 255 nodes = 288 elements). The deserializer doesn't complain when we + // parse less elements than indicated, so we can return from the visitor at any time when we have parsed + // all that we need to. + // (Technically one can do the same with `deserializer.deserialize_struct()`, but one would have to pass in + // a list of 288 strings for the `fields`. So using `deserialize_tuple()` that takes just a number is much + // easier.) + deserializer.deserialize_tuple( + 288, + PoWMerklePathVisitor { + phantom: PhantomData, + }, + ) + } +} + +pub type PoWBlake2bMerklePath = PoWMerklePath; + +#[test] +fn it_can_correctly_deserialize_pow_merkle_path() { + // PoW merkle paths have the u8 count of nodes as the first byte, then the left-bits, then immediately the + // node hashes themselves - no length bytes inbetween. + let bin = hex::decode("0280de8d7ee7e54f301095294d494024430c8b251b4ebf9b1384922dc7f9dd24422f830e231d26cdc3bbd1f55f1918757568522acae62c21e8046190ea84d6e8ff16") + .unwrap(); + let _ = PoWMerklePath::::deserialize_all(&bin).unwrap(); +} + #[derive(Clone, Debug, Eq, PartialEq)] struct MerklePathNode { hash: H, diff --git a/wallet/tests/wallet.rs b/wallet/tests/wallet.rs index 2dbc18ced2..fd4e73f7a2 100644 --- a/wallet/tests/wallet.rs +++ b/wallet/tests/wallet.rs @@ -25,9 +25,9 @@ fn test_create_transaction() { Coin::from_u64_unchecked(42), Coin::ZERO, 0, - NetworkId::Main, + NetworkId::MainAlbatross, ); - assert_eq!(Ok(()), transaction.verify(NetworkId::Main)); + assert_eq!(Ok(()), transaction.verify(NetworkId::MainAlbatross)); } #[test] diff --git a/web-client/Cargo.toml b/web-client/Cargo.toml index 05c848dbdf..3598fe9a55 100644 --- a/web-client/Cargo.toml +++ b/web-client/Cargo.toml @@ -19,7 +19,7 @@ maintenance = { status = "experimental" } workspace = true [lib] -crate-type = ["cdylib"] +crate-type = ["rlib", "cdylib"] [dependencies] futures = { workspace = true } diff --git a/web-client/example/index.html b/web-client/example/index.html index 358f936633..d8df4cb613 100644 --- a/web-client/example/index.html +++ b/web-client/example/index.html @@ -26,8 +26,11 @@
- - +
+
+
+
+ See console for output.