diff --git a/Cargo.lock b/Cargo.lock index 7b86a3cb63..159a126006 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4617,6 +4617,7 @@ dependencies = [ "nimiq-transaction", "nimiq-utils", "nimiq-vrf", + "nimiq-web-client", "nimiq_rpc", "percentage", "rand", diff --git a/pow-migration/Cargo.toml b/pow-migration/Cargo.toml index bf3d50f58b..b5f979cc8d 100644 --- a/pow-migration/Cargo.toml +++ b/pow-migration/Cargo.toml @@ -72,6 +72,7 @@ build-data = "0.2" [dev-dependencies] serde_json = "1.0" +nimiq-web-client = { workspace = true, default-features = false, features = ["primitives"] } nimiq-test-log = { workspace = true } diff --git a/pow-migration/src/history.rs b/pow-migration/src/history.rs index bb59927d96..13225d8a8e 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,86 @@ 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 } ]"#; @@ -560,7 +642,10 @@ 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(); + web_transaction.to_plain_transaction(Some(3456000), Some(1732034720000)); } } } 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 68585671f8..b6282bad2a 100644 --- a/primitives/transaction/src/account/htlc_contract.rs +++ b/primitives/transaction/src/account/htlc_contract.rs @@ -46,18 +46,27 @@ 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 { + 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 +80,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(()) } @@ -248,6 +262,13 @@ impl PoWCreationTransactionData { 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_number: u32, genesis_timestamp: u64) -> CreationTransactionData { let timeout = if self.timeout <= genesis_number { genesis_timestamp - (genesis_number - self.timeout) as u64 * 60_000 @@ -405,6 +426,74 @@ pub enum PoWOutgoingHTLCTransactionProof { } 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"), diff --git a/primitives/transaction/src/account/vesting_contract.rs b/primitives/transaction/src/account/vesting_contract.rs index 8ffe27d6d2..83dc6741e9 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); @@ -294,6 +302,9 @@ impl PoWCreationTransactionData { _ => return Err(TransactionError::InvalidData), }) } + pub fn parse(transaction: &Transaction) -> Result { + PoWCreationTransactionData::parse_data(&transaction.recipient_data, transaction.value) + } pub fn into_pos(self, genesis_number: u32, genesis_timestamp: u64) -> CreationTransactionData { let start_time = if self.start_block <= genesis_number { diff --git a/primitives/transaction/src/signature_proof.rs b/primitives/transaction/src/signature_proof.rs index 09702be40f..28e6603e23 100644 --- a/primitives/transaction/src/signature_proof.rs +++ b/primitives/transaction/src/signature_proof.rs @@ -358,6 +358,10 @@ pub struct PoWSignatureProof { } 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), 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/src/common/hashed_time_locked_contract.rs b/web-client/src/common/hashed_time_locked_contract.rs index 762fce09a7..fcb4d2c67d 100644 --- a/web-client/src/common/hashed_time_locked_contract.rs +++ b/web-client/src/common/hashed_time_locked_contract.rs @@ -44,9 +44,9 @@ impl HashedTimeLockedContract { ) -> Result { let data = if as_pow { let genesis_number = - genesis_number.ok_or(JsError::new("Genesis number is required"))?; + genesis_number.ok_or_else(|| JsError::new("Genesis number is required"))?; let genesis_timestamp = - genesis_timestamp.ok_or(JsError::new("Genesis timestamp is required"))?; + genesis_timestamp.ok_or_else(|| JsError::new("Genesis timestamp is required"))?; PoWCreationTransactionData::parse_data(bytes)? .into_pos(genesis_number, genesis_timestamp) } else { diff --git a/web-client/src/common/vesting_contract.rs b/web-client/src/common/vesting_contract.rs index ab14a22d99..00fdc8a680 100644 --- a/web-client/src/common/vesting_contract.rs +++ b/web-client/src/common/vesting_contract.rs @@ -47,9 +47,9 @@ impl VestingContract { ) -> Result { let data = if as_pow { let genesis_number = - genesis_number.ok_or(JsError::new("Genesis number is required"))?; + genesis_number.ok_or_else(|| JsError::new("Genesis number is required"))?; let genesis_timestamp = - genesis_timestamp.ok_or(JsError::new("Genesis timestamp is required"))?; + genesis_timestamp.ok_or_else(|| JsError::new("Genesis timestamp is required"))?; PoWCreationTransactionData::parse_data(bytes, tx_value)? .into_pos(genesis_number, genesis_timestamp) } else { diff --git a/web-client/src/lib.rs b/web-client/src/lib.rs index 9222f3b05a..5fbb76cdaf 100644 --- a/web-client/src/lib.rs +++ b/web-client/src/lib.rs @@ -3,7 +3,7 @@ extern crate alloc; // Required for wasm-bindgen-derive #[cfg(feature = "client")] mod client; #[cfg(any(feature = "client", feature = "primitives"))] -mod common; +pub mod common; #[cfg(any(feature = "crypto", feature = "primitives"))] mod crypto_utils; #[cfg(feature = "primitives")]