diff --git a/Cargo.lock b/Cargo.lock index efa349a1a..42b4bbc1a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3549,7 +3549,6 @@ dependencies = [ "derive_more 2.1.1", "ethereum-types", "foreign-chain-rpc-interfaces", - "hex", "http 1.4.0", "httpmock", "jsonrpsee", @@ -3568,8 +3567,8 @@ version = "3.7.0" dependencies = [ "derive_more 2.1.1", "ethereum-types", - "hex", "jsonrpsee", + "mpc-primitives", "serde", "serde_json", ] @@ -5720,11 +5719,11 @@ dependencies = [ "borsh", "derive_more 2.1.1", "hex", + "paste", "rand 0.8.5", "schemars 0.8.22", "serde", "serde_json", - "serde_with", "thiserror 2.0.18", ] diff --git a/Cargo.toml b/Cargo.toml index 434b524c1..2a9f47802 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -161,6 +161,7 @@ near-sdk = { version = "5.24.1", features = [ near-workspaces = { version = "0.22.1" } num_enum = { version = "0.7.6", features = ["complex-expressions"] } pairing = { version = "0.23.0" } +paste = "1.0" pprof = { version = "0.15.0", features = [ "cpp", "flamegraph", @@ -177,7 +178,10 @@ regex = "1.12.3" reqwest = { version = "0.13.2", features = ["multipart", "json"] } rmp-serde = "1.3.1" rstest = { version = "0.26.1" } -rustls = { version = "0.23.37", default-features = false, features = ["ring", "std"] } +rustls = { version = "0.23.37", default-features = false, features = [ + "ring", + "std", +] } serde = { version = "1.0", features = ["derive"] } serde_bytes = "0.11.19" serde_json = "1.0" diff --git a/crates/contract/src/lib.rs b/crates/contract/src/lib.rs index 3cfc9e28e..d77dcd44a 100644 --- a/crates/contract/src/lib.rs +++ b/crates/contract/src/lib.rs @@ -1353,7 +1353,7 @@ impl MpcContract { }; let participant = AuthenticatedParticipantId::new(threshold_parameters.participants())?; - let votes = self.tee_state.vote(code_hash.clone(), &participant); + let votes = self.tee_state.vote(code_hash, &participant); let tee_upgrade_deadline_duration = Duration::from_secs(self.config.tee_upgrade_deadline_duration_seconds); @@ -1391,7 +1391,7 @@ impl MpcContract { }; let participant = AuthenticatedParticipantId::new(threshold_parameters.participants())?; - let action = LauncherVoteAction::Add(launcher_hash.clone()); + let action = LauncherVoteAction::Add(launcher_hash); let votes = self.tee_state.vote_launcher(action, &participant); let tee_upgrade_deadline_duration = @@ -1429,7 +1429,7 @@ impl MpcContract { }; let participant = AuthenticatedParticipantId::new(threshold_parameters.participants())?; - let action = LauncherVoteAction::Remove(launcher_hash.clone()); + let action = LauncherVoteAction::Remove(launcher_hash); let votes = self.tee_state.vote_launcher(action, &participant); // Removal requires ALL participants to vote @@ -4504,9 +4504,7 @@ mod tests { .block_timestamp(block_timestamp_ns) .build()); - contract - .vote_code_hash(mpc_hash.clone()) - .expect("vote succeeds"); + contract.vote_code_hash(*mpc_hash).expect("vote succeeds"); } } @@ -4527,7 +4525,7 @@ mod tests { .build()); contract - .vote_add_launcher_hash(launcher_hash.clone()) + .vote_add_launcher_hash(launcher_hash) .expect("launcher vote succeeds"); } } @@ -4821,7 +4819,7 @@ mod tests { .predecessor_account_id(account_id.clone()) .build()); contract - .vote_add_launcher_hash(launcher_hash.clone()) + .vote_add_launcher_hash(launcher_hash) .expect("vote should succeed"); } assert!( @@ -4836,7 +4834,7 @@ mod tests { .predecessor_account_id(account_id.clone()) .build()); contract - .vote_add_launcher_hash(launcher_hash.clone()) + .vote_add_launcher_hash(launcher_hash) .expect("vote should succeed"); let allowed = contract.allowed_launcher_image_hashes(); @@ -4858,10 +4856,10 @@ mod tests { // Same participant votes twice — should count as 1 vote contract - .vote_add_launcher_hash(launcher_hash.clone()) + .vote_add_launcher_hash(launcher_hash) .expect("vote should succeed"); contract - .vote_add_launcher_hash(launcher_hash.clone()) + .vote_add_launcher_hash(launcher_hash) .expect("duplicate vote should succeed"); assert!( @@ -4878,14 +4876,14 @@ mod tests { let launcher_hash_2 = make_launcher_hash(0xDD); // Add two launcher hashes so removal of one doesn't hit the "last entry" guard - for hash in [&launcher_hash, &launcher_hash_2] { + for hash in [launcher_hash, launcher_hash_2] { for (account_id, _, _) in participant_list { testing_env!(VMContextBuilder::new() .signer_account_id(account_id.clone()) .predecessor_account_id(account_id.clone()) .build()); contract - .vote_add_launcher_hash(hash.clone()) + .vote_add_launcher_hash(hash) .expect("add vote should succeed"); } } @@ -4898,7 +4896,7 @@ mod tests { .predecessor_account_id(account_id.clone()) .build()); contract - .vote_remove_launcher_hash(launcher_hash.clone()) + .vote_remove_launcher_hash(launcher_hash) .expect("remove vote should succeed"); } assert_eq!( @@ -4914,7 +4912,7 @@ mod tests { .predecessor_account_id(account_id.clone()) .build()); contract - .vote_remove_launcher_hash(launcher_hash.clone()) + .vote_remove_launcher_hash(launcher_hash) .expect("remove vote should succeed"); assert_eq!( @@ -4937,7 +4935,7 @@ mod tests { .predecessor_account_id(account_id.clone()) .build()); contract - .vote_add_launcher_hash(launcher_hash.clone()) + .vote_add_launcher_hash(launcher_hash) .expect("add vote should succeed"); } assert_eq!(contract.allowed_launcher_image_hashes().len(), 1); @@ -4949,7 +4947,7 @@ mod tests { .predecessor_account_id(account_id.clone()) .build()); contract - .vote_remove_launcher_hash(launcher_hash.clone()) + .vote_remove_launcher_hash(launcher_hash) .expect("remove vote should succeed"); } assert_eq!( @@ -4977,7 +4975,7 @@ mod tests { .block_timestamp(block_ts) .build()); contract - .vote_code_hash(mpc_hash.clone()) + .vote_code_hash(mpc_hash) .expect("mpc vote should succeed"); } @@ -4989,7 +4987,7 @@ mod tests { .block_timestamp(block_ts) .build()); contract - .vote_add_launcher_hash(launcher_hash.clone()) + .vote_add_launcher_hash(launcher_hash) .expect("launcher vote should succeed"); } @@ -5020,7 +5018,7 @@ mod tests { .predecessor_account_id(account_id.clone()) .build()); contract - .vote_add_launcher_hash(launcher_hash.clone()) + .vote_add_launcher_hash(launcher_hash) .expect("vote should succeed"); } assert_eq!(contract.allowed_launcher_image_hashes().len(), 1); @@ -5033,7 +5031,7 @@ mod tests { .predecessor_account_id(account_id.clone()) .build()); contract - .vote_add_launcher_hash(launcher_hash_2.clone()) + .vote_add_launcher_hash(launcher_hash_2) .expect("vote should succeed"); // Only 1 vote for hash_2, should not be added yet @@ -5063,12 +5061,12 @@ mod tests { .predecessor_account_id(account_0.clone()) .build()); contract - .vote_add_launcher_hash(launcher_hash.clone()) + .vote_add_launcher_hash(launcher_hash) .expect("vote should succeed"); let votes = &contract.launcher_hash_votes().vote_by_account; assert_eq!(votes.len(), 1); - let expected_action = LauncherVoteAction::Add(launcher_hash.clone()); + let expected_action = LauncherVoteAction::Add(launcher_hash); assert!(votes.values().all(|v| *v == expected_action)); // Second vote @@ -5078,7 +5076,7 @@ mod tests { .predecessor_account_id(account_1.clone()) .build()); contract - .vote_add_launcher_hash(launcher_hash.clone()) + .vote_add_launcher_hash(launcher_hash) .expect("vote should succeed"); let votes = &contract.launcher_hash_votes().vote_by_account; @@ -5092,7 +5090,7 @@ mod tests { .predecessor_account_id(account_2.clone()) .build()); contract - .vote_add_launcher_hash(launcher_hash.clone()) + .vote_add_launcher_hash(launcher_hash) .expect("vote should succeed"); assert!( @@ -5121,7 +5119,7 @@ mod tests { .predecessor_account_id(account.clone()) .build()); contract - .vote_code_hash(code_hash.clone()) + .vote_code_hash(code_hash) .expect("vote should succeed"); let votes = &contract.code_hash_votes().proposal_by_account; @@ -5153,7 +5151,7 @@ mod tests { .block_timestamp(block_ts) .build()); contract - .vote_code_hash(mpc_hash_1.clone()) + .vote_code_hash(mpc_hash_1) .expect("mpc vote should succeed"); } @@ -5165,7 +5163,7 @@ mod tests { .block_timestamp(block_ts) .build()); contract - .vote_add_launcher_hash(launcher_hash.clone()) + .vote_add_launcher_hash(launcher_hash) .expect("launcher vote should succeed"); } assert_eq!(contract.allowed_launcher_compose_hashes().len(), 1); @@ -5179,7 +5177,7 @@ mod tests { .block_timestamp(block_ts) .build()); contract - .vote_code_hash(mpc_hash_2.clone()) + .vote_code_hash(mpc_hash_2) .expect("mpc vote 2 should succeed"); } @@ -5218,7 +5216,7 @@ mod tests { .block_timestamp(ts) .build()); contract - .vote_code_hash(hash.clone()) + .vote_code_hash(hash) .expect("mpc vote should succeed"); } }; @@ -5231,7 +5229,7 @@ mod tests { .block_timestamp(ts) .build()); contract - .vote_add_launcher_hash(hash.clone()) + .vote_add_launcher_hash(hash) .expect("launcher vote should succeed"); } }; @@ -5242,12 +5240,12 @@ mod tests { let m2 = NodeImageHash::from([0x22; 32]); let m3 = NodeImageHash::from([0x33; 32]); - vote_mpc(&mut contract, m1.clone(), t0); - vote_launcher(&mut contract, l1.clone(), t0); + vote_mpc(&mut contract, m1, t0); + vote_launcher(&mut contract, l1, t0); assert_eq!(contract.allowed_launcher_compose_hashes().len(), 1); let t1 = t0 + day; - vote_mpc(&mut contract, m2.clone(), t1); + vote_mpc(&mut contract, m2, t1); assert_eq!(contract.allowed_launcher_compose_hashes().len(), 2); let t2 = t1 + upgrade_deadline + sec; @@ -5262,14 +5260,14 @@ mod tests { "stored compose hashes persist even after MPC hash expires" ); - vote_launcher(&mut contract, l2.clone(), t2); + vote_launcher(&mut contract, l2, t2); assert_eq!( contract.allowed_launcher_compose_hashes().len(), 3, "L2 paired only with valid M2, not expired M1" ); - vote_mpc(&mut contract, m3.clone(), t2); + vote_mpc(&mut contract, m3, t2); assert_eq!( contract.allowed_launcher_compose_hashes().len(), 5, diff --git a/crates/contract/src/tee/measurements.rs b/crates/contract/src/tee/measurements.rs index b27c8a0e9..bf4d58e40 100644 --- a/crates/contract/src/tee/measurements.rs +++ b/crates/contract/src/tee/measurements.rs @@ -5,55 +5,25 @@ use std::collections::BTreeMap; use crate::primitives::{key_state::AuthenticatedParticipantId, participants::Participants}; -/// Generates a 48-byte digest newtype with hex JSON serialization and borsh support. -macro_rules! digest_newtype { - ($(#[$meta:meta])* $name:ident) => { - #[serde_with::serde_as] - #[derive( - Debug, Clone, PartialEq, Eq, - serde::Serialize, serde::Deserialize, - BorshSerialize, BorshDeserialize, - )] - #[cfg_attr( - all(feature = "abi", not(target_arch = "wasm32")), - derive(borsh::BorshSchema, schemars::JsonSchema) - )] - $(#[$meta])* - #[serde(transparent)] - pub struct $name { - #[serde_as(as = "serde_with::hex::Hex")] - bytes: [u8; 48], - } - - impl From<[u8; 48]> for $name { - fn from(bytes: [u8; 48]) -> Self { Self { bytes } } - } - - impl From<$name> for [u8; 48] { - fn from(h: $name) -> [u8; 48] { h.bytes } - } - }; -} - -digest_newtype!( +mpc_primitives::define_hash!( /// SHA-384 digest of the MRTD (Module Run-Time Data) TDX measurement. - MrtdHash + MrtdHash, 48 ); -digest_newtype!( +mpc_primitives::define_hash!( /// SHA-384 digest of the RTMR0 TDX measurement. - Rtmr0Hash + Rtmr0Hash, 48 ); -digest_newtype!( +mpc_primitives::define_hash!( /// SHA-384 digest of the RTMR1 TDX measurement. - Rtmr1Hash + Rtmr1Hash, 48 ); -digest_newtype!( +mpc_primitives::define_hash!( /// SHA-384 digest of the RTMR2 TDX measurement. - Rtmr2Hash + Rtmr2Hash, 48 ); -digest_newtype!( +mpc_primitives::define_hash!( /// SHA-384 digest of the key provider event. - KeyProviderEventDigest + KeyProviderEventDigest, 48 ); /// Tracks votes for adding or removing OS measurements. diff --git a/crates/contract/src/tee/proposal.rs b/crates/contract/src/tee/proposal.rs index 5f9ddbf1f..90e0d9689 100644 --- a/crates/contract/src/tee/proposal.rs +++ b/crates/contract/src/tee/proposal.rs @@ -33,7 +33,7 @@ impl CodeHashesVotes { ) -> u64 { if self .proposal_by_account - .insert(participant.clone(), proposal.clone()) + .insert(participant.clone(), proposal) .is_some() { log!("removed old vote for signer"); @@ -64,7 +64,7 @@ impl CodeHashesVotes { .filter(|(participant_id, _)| { participants.is_participant_given_participant_id(&participant_id.get()) }) - .map(|(participant_id, vote)| (participant_id.clone(), vote.clone())) + .map(|(participant_id, vote)| (participant_id.clone(), *vote)) .collect(); CodeHashesVotes { proposal_by_account: remaining, @@ -338,10 +338,7 @@ impl AllowedLauncherImages { /// Returns all allowed launcher image hashes. pub fn launcher_hashes(&self) -> Vec { - self.entries - .iter() - .map(|e| e.launcher_hash.clone()) - .collect() + self.entries.iter().map(|e| e.launcher_hash).collect() } } @@ -485,16 +482,16 @@ mod tests { let mpc_hashes = vec![dummy_code_hash(10), dummy_code_hash(20)]; // Add first launcher - assert!(allowed.add(launcher_1.clone(), &mpc_hashes)); + assert!(allowed.add(launcher_1, &mpc_hashes)); assert_eq!(allowed.launcher_hashes().len(), 1); // Should have 2 compose hashes (one per MPC image) assert_eq!(allowed.all_compose_hashes().len(), 2); // Adding the same launcher again returns false - assert!(!allowed.add(launcher_1.clone(), &mpc_hashes)); + assert!(!allowed.add(launcher_1, &mpc_hashes)); // Add second launcher - assert!(allowed.add(launcher_2.clone(), &mpc_hashes)); + assert!(allowed.add(launcher_2, &mpc_hashes)); assert_eq!(allowed.launcher_hashes().len(), 2); assert_eq!(allowed.all_compose_hashes().len(), 4); @@ -515,7 +512,7 @@ mod tests { let launcher = dummy_launcher_hash(1); let mpc_hash_1 = dummy_code_hash(10); - allowed.add(launcher.clone(), &[mpc_hash_1.clone()]); + allowed.add(launcher, &[mpc_hash_1]); assert_eq!(allowed.all_compose_hashes().len(), 1); // Add a new MPC image — should add one compose hash per launcher diff --git a/crates/contract/src/tee/tee_state.rs b/crates/contract/src/tee/tee_state.rs index fa10ead76..72e706689 100644 --- a/crates/contract/src/tee/tee_state.rs +++ b/crates/contract/src/tee/tee_state.rs @@ -1270,7 +1270,7 @@ mod tests { ctx.signer_account_id(account_id.clone()); testing_env!(ctx.build()); let auth_id = AuthenticatedParticipantId::new(&all_participants).unwrap(); - tee_state.votes.vote(malicious_hash.clone(), &auth_id); + tee_state.votes.vote(malicious_hash, &auth_id); } assert_eq!(tee_state.votes.proposal_by_account.len(), 2); @@ -1289,7 +1289,7 @@ mod tests { ctx.signer_account_id(p2_account.clone()); testing_env!(ctx.build()); let auth_id = AuthenticatedParticipantId::new(&new_participants).unwrap(); - let vote_count = tee_state.votes.vote(malicious_hash.clone(), &auth_id); + let vote_count = tee_state.votes.vote(malicious_hash, &auth_id); assert_eq!(vote_count, 1, "Only the fresh vote from P2 should count"); } diff --git a/crates/contract/tests/inprocess/attestation_submission.rs b/crates/contract/tests/inprocess/attestation_submission.rs index f979f905b..b6742db84 100644 --- a/crates/contract/tests/inprocess/attestation_submission.rs +++ b/crates/contract/tests/inprocess/attestation_submission.rs @@ -351,7 +351,7 @@ macro_rules! assert_allowed_docker_image_hashes { .contract .allowed_docker_image_hashes() .iter() - .map(|hash| *hash.clone()) + .map(|hash| **hash) .collect(); res.reverse(); diff --git a/crates/contract/tests/sandbox/tee.rs b/crates/contract/tests/sandbox/tee.rs index 2b99ebff9..9fc3daf3e 100644 --- a/crates/contract/tests/sandbox/tee.rs +++ b/crates/contract/tests/sandbox/tee.rs @@ -53,7 +53,7 @@ async fn test_vote_code_hash_basic_threshold_and_stability() -> Result<()> { ) .await?; let allowed_hashes = get_allowed_hashes(&contract).await; - assert_eq!(allowed_hashes, vec![allowed_mpc_image_digest.clone()]); + assert_eq!(allowed_hashes, vec![allowed_mpc_image_digest]); // Additional votes - should not change the allowed hashes const EXTRA_VOTES_TO_TEST_STABILITY: usize = 4; @@ -66,7 +66,7 @@ async fn test_vote_code_hash_basic_threshold_and_stability() -> Result<()> { .await?; // Should still have exactly one hash let allowed_hashes = get_allowed_hashes(&contract).await; - assert_eq!(allowed_hashes, vec![allowed_mpc_image_digest.clone()]); + assert_eq!(allowed_hashes, vec![allowed_mpc_image_digest]); } Ok(()) @@ -100,7 +100,7 @@ async fn test_vote_code_hash_approved_hashes_persist_after_vote_changes() -> Res // Verify first hash is allowed let allowed_hashes = get_allowed_hashes(&contract).await; - assert_eq!(allowed_hashes, vec![first_hash.clone()]); + assert_eq!(allowed_hashes, vec![first_hash]); // Participant 0 changes vote to second hash vote_for_hash(&mpc_signer_accounts[0], &contract, &second_hash).await?; @@ -108,7 +108,7 @@ async fn test_vote_code_hash_approved_hashes_persist_after_vote_changes() -> Res // First hash should still be allowed // Second hash should not be allowed yet (only 1 vote) let allowed_hashes = get_allowed_hashes(&contract).await; - assert_eq!(allowed_hashes, vec![first_hash.clone()]); + assert_eq!(allowed_hashes, vec![first_hash]); // Participants 2..threshold votes for second hash - should reach threshold for account in mpc_signer_accounts @@ -121,10 +121,7 @@ async fn test_vote_code_hash_approved_hashes_persist_after_vote_changes() -> Res // Now both hashes should be allowed let allowed_hashes = get_allowed_hashes(&contract).await; - assert_eq!( - allowed_hashes, - vec![second_hash.clone(), first_hash.clone()] - ); + assert_eq!(allowed_hashes, vec![second_hash, first_hash]); // Participant 1 also changes vote to second hash vote_for_hash(&mpc_signer_accounts[1], &contract, &second_hash).await?; @@ -132,10 +129,7 @@ async fn test_vote_code_hash_approved_hashes_persist_after_vote_changes() -> Res // Both hashes should still be allowed (once a hash reaches threshold, it stays) // Second hash should still be allowed (threshold + 1 votes) let allowed_hashes = get_allowed_hashes(&contract).await; - assert_eq!( - allowed_hashes, - vec![second_hash.clone(), first_hash.clone()] - ); + assert_eq!(allowed_hashes, vec![second_hash, first_hash]); Ok(()) } diff --git a/crates/contract/tests/snapshots/abi__abi_has_not_changed.snap b/crates/contract/tests/snapshots/abi__abi_has_not_changed.snap index 03c8a3d9a..51180a154 100644 --- a/crates/contract/tests/snapshots/abi__abi_has_not_changed.snap +++ b/crates/contract/tests/snapshots/abi__abi_has_not_changed.snap @@ -1864,24 +1864,19 @@ expression: abi ], "properties": { "key_provider_event_digest": { - "type": "string", - "pattern": "^(?:[0-9A-Fa-f]{2})*$" + "$ref": "#/definitions/KeyProviderEventDigest" }, "mrtd": { - "type": "string", - "pattern": "^(?:[0-9A-Fa-f]{2})*$" + "$ref": "#/definitions/MrtdHash" }, "rtmr0": { - "type": "string", - "pattern": "^(?:[0-9A-Fa-f]{2})*$" + "$ref": "#/definitions/Rtmr0Hash" }, "rtmr1": { - "type": "string", - "pattern": "^(?:[0-9A-Fa-f]{2})*$" + "$ref": "#/definitions/Rtmr1Hash" }, "rtmr2": { - "type": "string", - "pattern": "^(?:[0-9A-Fa-f]{2})*$" + "$ref": "#/definitions/Rtmr2Hash" } } }, @@ -2587,6 +2582,12 @@ expression: abi } } }, + "KeyProviderEventDigest": { + "type": "string", + "maxLength": 96, + "minLength": 96, + "pattern": "^[0-9a-fA-F]+$" + }, "Keyset": { "description": "Represents a key for every domain in a specific epoch.", "type": "object", @@ -2802,6 +2803,12 @@ expression: abi } ] }, + "MrtdHash": { + "type": "string", + "maxLength": 96, + "minLength": 96, + "pattern": "^[0-9a-fA-F]+$" + }, "NodeId": { "type": "object", "required": [ @@ -3372,6 +3379,24 @@ expression: abi } } }, + "Rtmr0Hash": { + "type": "string", + "maxLength": 96, + "minLength": 96, + "pattern": "^[0-9a-fA-F]+$" + }, + "Rtmr1Hash": { + "type": "string", + "maxLength": 96, + "minLength": 96, + "pattern": "^[0-9a-fA-F]+$" + }, + "Rtmr2Hash": { + "type": "string", + "maxLength": 96, + "minLength": 96, + "pattern": "^[0-9a-fA-F]+$" + }, "RunningContractState": { "description": "State when the contract is ready for signature operations.", "type": "object", diff --git a/crates/foreign-chain-inspector/Cargo.toml b/crates/foreign-chain-inspector/Cargo.toml index 4a9c49c58..e9d63717a 100644 --- a/crates/foreign-chain-inspector/Cargo.toml +++ b/crates/foreign-chain-inspector/Cargo.toml @@ -8,18 +8,17 @@ license.workspace = true derive_more = { workspace = true } ethereum-types = { workspace = true } foreign-chain-rpc-interfaces = { workspace = true } -hex = { workspace = true } http = { workspace = true } jsonrpsee = { workspace = true } mpc-primitives = { workspace = true } near-mpc-contract-interface = { workspace = true } -serde = { workspace = true } thiserror = { workspace = true } [dev-dependencies] assert_matches = { workspace = true } httpmock = { workspace = true } rstest = { workspace = true } +serde = { workspace = true } serde_json = { workspace = true } tokio = { workspace = true } diff --git a/crates/foreign-chain-inspector/src/abstract_chain.rs b/crates/foreign-chain-inspector/src/abstract_chain.rs index 96b6fab1b..d2623e628 100644 --- a/crates/foreign-chain-inspector/src/abstract_chain.rs +++ b/crates/foreign-chain-inspector/src/abstract_chain.rs @@ -1,8 +1,6 @@ pub use ethereum_types; -use crate::hash::hash_newtype; - pub mod inspector; -hash_newtype!(AbstractBlockHash); -hash_newtype!(AbstractTransactionHash); +mpc_primitives::define_hash!(AbstractBlockHash, 32); +mpc_primitives::define_hash!(AbstractTransactionHash, 32); diff --git a/crates/foreign-chain-inspector/src/bitcoin.rs b/crates/foreign-chain-inspector/src/bitcoin.rs index 00283fe13..835c59c04 100644 --- a/crates/foreign-chain-inspector/src/bitcoin.rs +++ b/crates/foreign-chain-inspector/src/bitcoin.rs @@ -1,9 +1,7 @@ -use crate::hash::hash_newtype; - pub mod inspector; -hash_newtype!(BitcoinBlockHash); -hash_newtype!(BitcoinTransactionHash); +mpc_primitives::define_hash!(BitcoinBlockHash, 32); +mpc_primitives::define_hash!(BitcoinTransactionHash, 32); #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum BitcoinExtractedValue { diff --git a/crates/foreign-chain-inspector/src/hash.rs b/crates/foreign-chain-inspector/src/hash.rs deleted file mode 100644 index 14c4ad88d..000000000 --- a/crates/foreign-chain-inspector/src/hash.rs +++ /dev/null @@ -1,74 +0,0 @@ -/// Parses an N-byte hash from a hex string. -pub(crate) fn parse_hex_hash>( - s: &str, -) -> Result { - let decoded = hex::decode(s)?; - let bytes: [u8; N] = decoded.try_into().map_err(|v: Vec| { - mpc_primitives::hash::HashParseError::InvalidLength { - expected: N, - got: v.len(), - } - })?; - Ok(T::from(bytes)) -} - -/// Generates a 32-byte hash newtype with hex JSON serialization and `FromStr`. -/// Unlike the primitives crate's `hash_newtype!`, this does not include borsh or schema support. -macro_rules! hash_newtype { - ($(#[$meta:meta])* $name:ident) => { - #[derive( - Debug, - Clone, - PartialEq, - Eq, - PartialOrd, - Ord, - Hash, - derive_more::Deref, - derive_more::AsRef, - derive_more::Into, - )] - $(#[$meta])* - pub struct $name { - #[deref] - #[as_ref] - #[into] - bytes: [u8; 32], - } - - impl serde::Serialize for $name { - fn serialize(&self, serializer: S) -> Result { - serializer.serialize_str(&hex::encode(&self.bytes)) - } - } - - impl<'de> serde::Deserialize<'de> for $name { - fn deserialize>(deserializer: D) -> Result { - let s = ::deserialize(deserializer)?; - crate::hash::parse_hex_hash::<32, Self>(&s).map_err(serde::de::Error::custom) - } - } - - impl From<[u8; 32]> for $name { - fn from(bytes: [u8; 32]) -> Self { - Self { bytes } - } - } - - impl core::str::FromStr for $name { - type Err = mpc_primitives::hash::HashParseError; - - fn from_str(s: &str) -> Result { - crate::hash::parse_hex_hash::<32, Self>(s) - } - } - - impl $name { - pub fn as_hex(&self) -> String { - hex::encode(self.as_ref()) - } - } - }; -} - -pub(crate) use hash_newtype; diff --git a/crates/foreign-chain-inspector/src/lib.rs b/crates/foreign-chain-inspector/src/lib.rs index 61e469cae..9d5db44b5 100644 --- a/crates/foreign-chain-inspector/src/lib.rs +++ b/crates/foreign-chain-inspector/src/lib.rs @@ -6,7 +6,6 @@ use thiserror::Error; pub mod abstract_chain; pub mod bitcoin; pub mod contract_interface_conversions; -mod hash; pub mod starknet; pub trait ForeignChainInspector { diff --git a/crates/foreign-chain-inspector/src/starknet.rs b/crates/foreign-chain-inspector/src/starknet.rs index ffa8cd2d8..1dbdb35d1 100644 --- a/crates/foreign-chain-inspector/src/starknet.rs +++ b/crates/foreign-chain-inspector/src/starknet.rs @@ -1,10 +1,9 @@ -use crate::hash::hash_newtype; use near_mpc_contract_interface::types::StarknetLog; pub mod inspector; -hash_newtype!(StarknetBlockHash); -hash_newtype!(StarknetTransactionHash); +mpc_primitives::define_hash!(StarknetBlockHash, 32); +mpc_primitives::define_hash!(StarknetTransactionHash, 32); #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum StarknetExtractedValue { diff --git a/crates/foreign-chain-rpc-interfaces/Cargo.toml b/crates/foreign-chain-rpc-interfaces/Cargo.toml index 78c551738..742cd0fc0 100644 --- a/crates/foreign-chain-rpc-interfaces/Cargo.toml +++ b/crates/foreign-chain-rpc-interfaces/Cargo.toml @@ -7,8 +7,8 @@ license.workspace = true [dependencies] derive_more = { workspace = true } ethereum-types = { workspace = true } -hex = { workspace = true } jsonrpsee = { workspace = true } +mpc-primitives = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } diff --git a/crates/foreign-chain-rpc-interfaces/src/bitcoin.rs b/crates/foreign-chain-rpc-interfaces/src/bitcoin.rs index d4c1f1048..105e632b5 100644 --- a/crates/foreign-chain-rpc-interfaces/src/bitcoin.rs +++ b/crates/foreign-chain-rpc-interfaces/src/bitcoin.rs @@ -3,43 +3,8 @@ use crate::to_rpc_params_impl; use jsonrpsee::core::traits::ToRpcParams; use serde::{Deserialize, Serialize}; -macro_rules! hash_newtype { - ($(#[$meta:meta])* $name:ident) => { - #[derive( - Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, - derive_more::Deref, derive_more::AsRef, derive_more::Into, - )] - $(#[$meta])* - pub struct $name { - #[deref] #[as_ref] #[into] - bytes: [u8; 32], - } - - impl serde::Serialize for $name { - fn serialize(&self, serializer: S) -> Result { - serializer.serialize_str(&hex::encode(&self.bytes)) - } - } - - impl<'de> serde::Deserialize<'de> for $name { - fn deserialize>(deserializer: D) -> Result { - let s = ::deserialize(deserializer)?; - let decoded = hex::decode(&s).map_err(serde::de::Error::custom)?; - let bytes: [u8; 32] = decoded.try_into().map_err(|v: Vec| { - serde::de::Error::custom(format!("expected 32 bytes, got {}", v.len())) - })?; - Ok(Self { bytes }) - } - } - - impl From<[u8; 32]> for $name { - fn from(bytes: [u8; 32]) -> Self { Self { bytes } } - } - }; -} - -hash_newtype!(TransportBitcoinBlockHash); -hash_newtype!(TransportBitcoinTransactionHash); +mpc_primitives::define_hash!(TransportBitcoinBlockHash, 32); +mpc_primitives::define_hash!(TransportBitcoinTransactionHash, 32); /// Partial RPC response for `getrawtransaction`. See link below for full spec; /// diff --git a/crates/mpc-attestation/src/attestation.rs b/crates/mpc-attestation/src/attestation.rs index c469ecd00..57e0caf2e 100644 --- a/crates/mpc-attestation/src/attestation.rs +++ b/crates/mpc-attestation/src/attestation.rs @@ -340,7 +340,7 @@ mod tests { let hash_constrained_attestation = VerifiedAttestation::Mock(MockAttestation::WithConstraints { - mpc_docker_image_hash: Some(allowed_hash.clone()), + mpc_docker_image_hash: Some(allowed_hash), launcher_docker_compose_hash: None, expiry_timestamp_seconds: None, }); @@ -411,7 +411,7 @@ mod tests { let hash_constrained_attestation = VerifiedAttestation::Mock(MockAttestation::WithConstraints { mpc_docker_image_hash: None, - launcher_docker_compose_hash: Some(allowed_hash.clone()), + launcher_docker_compose_hash: Some(allowed_hash), expiry_timestamp_seconds: None, }); diff --git a/crates/node/src/tee/allowed_image_hashes_watcher.rs b/crates/node/src/tee/allowed_image_hashes_watcher.rs index 90ebd8bba..08bc5c184 100644 --- a/crates/node/src/tee/allowed_image_hashes_watcher.rs +++ b/crates/node/src/tee/allowed_image_hashes_watcher.rs @@ -245,7 +245,7 @@ mod tests { let _join_handle = tokio::spawn(monitor_allowed_image_hashes( cancellation_token.child_token(), - current_hash.clone(), + *current_hash, receiver, storage_mock, sender_shutdown, @@ -316,7 +316,7 @@ mod tests { let current_image = image_hash_1(); let allowed_image = image_hash_2(); - let allowed_list = vec![allowed_image.clone()]; + let allowed_list = vec![allowed_image]; let expected_non_empty = NonEmptyVec::from_vec(allowed_list.clone()).unwrap(); let cancellation_token = CancellationToken::new(); diff --git a/crates/primitives/Cargo.toml b/crates/primitives/Cargo.toml index 7d54e8a8e..e127cea66 100644 --- a/crates/primitives/Cargo.toml +++ b/crates/primitives/Cargo.toml @@ -11,8 +11,8 @@ abi = ["borsh/unstable__schema", "schemars"] borsh = { workspace = true } derive_more = { workspace = true } hex = { workspace = true } +paste = { workspace = true } serde = { workspace = true } -serde_with = { workspace = true } thiserror = { workspace = true } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] diff --git a/crates/primitives/src/hash.rs b/crates/primitives/src/hash.rs index 9d5aadd18..eb7e92abf 100644 --- a/crates/primitives/src/hash.rs +++ b/crates/primitives/src/hash.rs @@ -1,5 +1,5 @@ -use alloc::{string::String, vec::Vec}; -use core::str::FromStr; +use alloc::{format, string::String, vec::Vec}; +use core::{marker::PhantomData, str::FromStr}; use hex::FromHexError; use thiserror::Error; @@ -11,125 +11,263 @@ pub enum HashParseError { InvalidLength { expected: usize, got: usize }, } -/// Generates a hash newtype wrapping `[u8; $n]` with hex serde, borsh, `FromStr`, -/// `Deref`, `AsRef`, `Into`, and (when the `abi` feature is active) BorshSchema / JsonSchema. -macro_rules! hash_newtype { - ($(#[$meta:meta])* $name:ident, $n:literal) => { - #[serde_with::serde_as] - #[derive( - Debug, - Clone, - PartialEq, - Eq, - PartialOrd, - Ord, - Hash, - serde::Serialize, - serde::Deserialize, - borsh::BorshSerialize, - borsh::BorshDeserialize, - derive_more::Deref, - derive_more::AsRef, - derive_more::Into, - )] - $(#[$meta])* - #[serde(transparent)] - pub struct $name { - #[deref] - #[as_ref] - #[into] - #[serde_as(as = "serde_with::hex::Hex")] - bytes: [u8; $n], - } +/// Marker trait binding a hash type name to a specific byte length. +/// +/// Each concrete hash type defines a zero-sized spec struct and implements this trait +/// for exactly one `N`. This prevents constructing a `HashDigest` — the +/// compiler rejects it because the trait bound `S: HashSpec` is not satisfied. +pub trait HashSpec { + const NAME: &'static str; +} - impl From<[u8; $n]> for $name { - fn from(bytes: [u8; $n]) -> Self { - Self::new(bytes) - } - } +/// A fixed-size hash digest with hex serialization. +/// +/// `S` is a zero-sized marker implementing [`HashSpec`] that binds the type name +/// to the byte length `N`. All trait implementations are generic — adding a new hash +/// type requires only a spec struct, a trait impl, and a type alias. +#[derive(derive_more::Deref, derive_more::AsRef, derive_more::Into)] +pub struct HashDigest, const N: usize> { + #[deref] + #[as_ref] + #[into] + bytes: [u8; N], + #[into(skip)] + _marker: PhantomData, +} - impl $name { - /// Converts the hash to a hexadecimal string representation. - pub fn as_hex(&self) -> String { - hex::encode(self.as_ref()) - } +// Manual impls to avoid spurious `S: Trait` bounds from derive macros. - pub fn as_bytes(&self) -> [u8; $n] { - self.bytes - } +impl, const N: usize> Clone for HashDigest { + fn clone(&self) -> Self { + *self + } +} - pub const fn new(bytes: [u8; $n]) -> Self { - Self { bytes } - } - } +impl, const N: usize> Copy for HashDigest {} - impl FromStr for $name { - type Err = HashParseError; - - fn from_str(s: &str) -> Result { - let decoded_hex_bytes = hex::decode(s)?; - let hash_bytes: [u8; $n] = - decoded_hex_bytes - .try_into() - .map_err(|v: Vec| HashParseError::InvalidLength { - expected: $n, - got: v.len(), - })?; - Ok(hash_bytes.into()) - } +impl, const N: usize> PartialEq for HashDigest { + fn eq(&self, other: &Self) -> bool { + self.bytes == other.bytes + } +} + +impl, const N: usize> Eq for HashDigest {} + +impl, const N: usize> PartialOrd for HashDigest { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl, const N: usize> Ord for HashDigest { + fn cmp(&self, other: &Self) -> core::cmp::Ordering { + self.bytes.cmp(&other.bytes) + } +} + +impl, const N: usize> core::hash::Hash for HashDigest { + fn hash(&self, state: &mut H) { + self.bytes.hash(state); + } +} + +// -- Debug ------------------------------------------------------------------- + +impl, const N: usize> core::fmt::Debug for HashDigest { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + // Encode hex directly into a stack buffer to avoid allocating a String. + let mut buf = [0u8; 2 * 128]; // max 128-byte hashes (256 hex chars) + let hex_buf = &mut buf[..2 * N]; + hex::encode_to_slice(self.bytes, hex_buf).map_err(|_| core::fmt::Error)?; + let hex_str = core::str::from_utf8(hex_buf).map_err(|_| core::fmt::Error)?; + write!(f, "{}({})", S::NAME, hex_str) + } +} + +// -- Serde (hex string) ------------------------------------------------------ + +impl, const N: usize> serde::Serialize for HashDigest { + fn serialize(&self, serializer: Ser) -> Result { + serializer.serialize_str(&hex::encode(self.bytes)) + } +} + +impl<'de, S: HashSpec, const N: usize> serde::Deserialize<'de> for HashDigest { + fn deserialize>(deserializer: D) -> Result { + let hex_str = ::deserialize(deserializer)?; + let decoded = hex::decode(&hex_str).map_err(serde::de::Error::custom)?; + let bytes: [u8; N] = decoded.try_into().map_err(|v: Vec| { + serde::de::Error::custom(format!("expected {} bytes, got {}", N, v.len())) + })?; + Ok(Self::new(bytes)) + } +} + +// -- Borsh ------------------------------------------------------------------- + +impl, const N: usize> borsh::BorshSerialize for HashDigest { + fn serialize(&self, writer: &mut W) -> borsh::io::Result<()> { + self.bytes.serialize(writer) + } +} + +impl, const N: usize> borsh::BorshDeserialize for HashDigest { + fn deserialize_reader(reader: &mut R) -> borsh::io::Result { + let bytes = <[u8; N]>::deserialize_reader(reader)?; + Ok(Self::new(bytes)) + } +} + +// -- BorshSchema (behind `abi` feature) -------------------------------------- + +#[cfg(all(feature = "abi", not(target_arch = "wasm32")))] +impl, const N: usize> borsh::BorshSchema for HashDigest { + fn declaration() -> borsh::schema::Declaration { + S::NAME.to_string() + } + + fn add_definitions_recursively( + definitions: &mut alloc::collections::BTreeMap< + borsh::schema::Declaration, + borsh::schema::Definition, + >, + ) { + let byte_array_decl = format!("[u8; {}]", N); + definitions.insert( + Self::declaration(), + borsh::schema::Definition::Struct { + fields: borsh::schema::Fields::NamedFields(alloc::vec![( + "bytes".into(), + byte_array_decl, + )]), + }, + ); + } +} + +// -- JsonSchema (behind `abi` feature) --------------------------------------- + +#[cfg(all(feature = "abi", not(target_arch = "wasm32")))] +impl, const N: usize> schemars::JsonSchema for HashDigest { + fn schema_name() -> String { + S::NAME.to_string() + } + + fn json_schema(_generator: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema { + let hex_len = (N * 2) as u32; + schemars::schema::Schema::Object(schemars::schema::SchemaObject { + instance_type: Some(schemars::schema::SingleOrVec::Single(Box::new( + schemars::schema::InstanceType::String, + ))), + string: Some(Box::new(schemars::schema::StringValidation { + min_length: Some(hex_len), + max_length: Some(hex_len), + pattern: Some("^[0-9a-fA-F]+$".to_string()), + })), + ..Default::default() + }) + } +} + +// -- From / Into / constructors ---------------------------------------------- + +impl, const N: usize> From<[u8; N]> for HashDigest { + fn from(bytes: [u8; N]) -> Self { + Self::new(bytes) + } +} + +impl, const N: usize> HashDigest { + /// Converts the hash to a hexadecimal string representation. + pub fn as_hex(&self) -> String { + hex::encode(self.as_ref()) + } + + pub fn as_bytes(&self) -> [u8; N] { + self.bytes + } + + pub const fn new(bytes: [u8; N]) -> Self { + Self { + bytes, + _marker: PhantomData, } + } +} - #[cfg(all(feature = "abi", not(target_arch = "wasm32")))] - impl borsh::BorshSchema for $name { - fn declaration() -> borsh::schema::Declaration { - alloc::format!(stringify!($name)) - } +// -- FromStr ----------------------------------------------------------------- + +impl, const N: usize> FromStr for HashDigest { + type Err = HashParseError; + + fn from_str(s: &str) -> Result { + let decoded_hex_bytes = hex::decode(s)?; + let hash_bytes: [u8; N] = + decoded_hex_bytes + .try_into() + .map_err(|v: Vec| HashParseError::InvalidLength { + expected: N, + got: v.len(), + })?; + Ok(hash_bytes.into()) + } +} - fn add_definitions_recursively( - definitions: &mut alloc::collections::BTreeMap< - borsh::schema::Declaration, - borsh::schema::Definition, - >, - ) { - let byte_array_decl = alloc::format!("[u8; {}]", $n); - definitions.insert( - Self::declaration(), - borsh::schema::Definition::Struct { - fields: borsh::schema::Fields::NamedFields(alloc::vec![ - ("bytes".into(), byte_array_decl), - ]), - }, - ); +// ============================================================================ +// define_hash! convenience macro +// ============================================================================ + +/// Defines a new hash type backed by [`HashDigest`]. +/// +/// Generates a zero-sized spec struct (`Spec`), implements [`HashSpec`] for it, +/// and creates a type alias `` = `HashDigest<Spec, N>`. +/// +/// # Example +/// +/// ```ignore +/// mpc_primitives::define_hash!( +/// /// SHA-256 hash of a Bitcoin block. +/// BitcoinBlockHash, 32 +/// ); +/// ``` +#[macro_export] +macro_rules! define_hash { + ($(#[$meta:meta])* $name:ident, $n:literal) => { + $crate::_macro_deps::paste::paste! { + #[doc(hidden)] + pub struct [<$name Spec>]; + + impl $crate::_macro_deps::borsh::BorshSerialize for [<$name Spec>] { + fn serialize( + &self, _writer: &mut W, + ) -> $crate::_macro_deps::borsh::io::Result<()> { + Ok(()) + } } - } - #[cfg(all(feature = "abi", not(target_arch = "wasm32")))] - impl schemars::JsonSchema for $name { - fn schema_name() -> String { - alloc::format!(stringify!($name)) + impl $crate::_macro_deps::borsh::BorshDeserialize for [<$name Spec>] { + fn deserialize_reader( + _reader: &mut R, + ) -> $crate::_macro_deps::borsh::io::Result { + Ok(Self) + } } - fn json_schema( - _generator: &mut schemars::r#gen::SchemaGenerator, - ) -> schemars::schema::Schema { - let hex_len = ($n * 2) as u32; - schemars::schema::Schema::Object(schemars::schema::SchemaObject { - instance_type: Some(schemars::schema::SingleOrVec::Single(Box::new( - schemars::schema::InstanceType::String, - ))), - string: Some(Box::new(schemars::schema::StringValidation { - min_length: Some(hex_len), - max_length: Some(hex_len), - pattern: Some("^[0-9a-fA-F]+$".to_string()), - })), - ..Default::default() - }) + impl $crate::hash::HashSpec<$n> for [<$name Spec>] { + const NAME: &'static str = stringify!($name); } + + $(#[$meta])* + pub type $name = $crate::hash::HashDigest<[<$name Spec>], $n>; } }; } -hash_newtype!( +// ============================================================================ +// Concrete hash types +// ============================================================================ + +define_hash!( /// Hash of a Docker image running in the TEE environment. Used as a proposal for a new TEE /// code hash to add to the whitelist, together with the TEE quote (which includes the RTMR3 /// measurement and more). @@ -140,7 +278,7 @@ hash_newtype!( /// Hash of the MPC node's Docker image. pub type NodeImageHash = DockerImageHash; -hash_newtype!( +define_hash!( /// Hash of the launcher's Docker Compose file used to run the MPC node in the TEE environment. /// It is computed from the launcher's Docker Compose template populated with the launcher image /// hash and the MPC node's Docker image hash. @@ -148,7 +286,7 @@ hash_newtype!( 32 ); -hash_newtype!( +define_hash!( /// Hash of the launcher Docker image itself. Voted on by participants to allow /// launcher upgrades without contract redeployment. LauncherImageHash, @@ -164,8 +302,8 @@ mod tests { use borsh::BorshDeserialize; use rand::{RngCore, SeedableRng, rngs::StdRng}; - hash_newtype!(TestHash, 32); - hash_newtype!(TestHash48, 48); + define_hash!(TestHash, 32); + define_hash!(TestHash48, 48); #[test] fn test_from_bytes_array() { diff --git a/crates/primitives/src/lib.rs b/crates/primitives/src/lib.rs index 6a4f4542a..b0e9a5f58 100644 --- a/crates/primitives/src/lib.rs +++ b/crates/primitives/src/lib.rs @@ -4,3 +4,10 @@ extern crate alloc; pub mod hash; + +/// Re-exports used by the [`define_hash!`] macro. Not part of the public API. +#[doc(hidden)] +pub mod _macro_deps { + pub use ::borsh; + pub use ::paste; +}