diff --git a/bindings/uniffi/src/lib.rs b/bindings/uniffi/src/lib.rs index cde06a9..e4687f8 100644 --- a/bindings/uniffi/src/lib.rs +++ b/bindings/uniffi/src/lib.rs @@ -105,6 +105,7 @@ fn parse_data_updates( #[derive(uniffi::Record)] pub struct Zone { pub anchor: u32, + pub anchor_hash: Vec, pub sovereignty: String, pub handle: String, pub canonical: String, @@ -120,6 +121,7 @@ pub struct Zone { fn zone_from_inner(z: &libveritas::Zone) -> Zone { Zone { anchor: z.anchor, + anchor_hash: z.anchor_hash.to_vec(), sovereignty: z.sovereignty.to_string(), handle: z.handle.to_string(), canonical: z.canonical.to_string(), @@ -227,8 +229,18 @@ fn zone_to_inner(z: &Zone) -> Result { CommitmentState::Unknown => libveritas::ProvableOption::Unknown, }; + let mut anchor_hash = [0u8; 32]; + if z.anchor_hash.len() == 32 { + anchor_hash.copy_from_slice(&z.anchor_hash); + } else if !z.anchor_hash.is_empty() { + return Err(VeritasError::InvalidInput { + msg: format!("anchor_hash must be 32 bytes, got {}", z.anchor_hash.len()), + }); + } + Ok(libveritas::Zone { anchor: z.anchor, + anchor_hash, sovereignty: match z.sovereignty.as_str() { "sovereign" => libveritas::SovereigntyState::Sovereign, "pending" => libveritas::SovereigntyState::Pending, @@ -874,10 +886,6 @@ pub struct VerifiedMessage { #[uniffi::export] impl VerifiedMessage { - pub fn root_id(&self) -> Vec { - self.inner.root_id.to_vec() - } - pub fn zones(&self) -> Vec { self.inner.zones.iter().map(zone_from_inner).collect() } diff --git a/bindings/wasm/src/lib.rs b/bindings/wasm/src/lib.rs index b3de2ba..934545d 100644 --- a/bindings/wasm/src/lib.rs +++ b/bindings/wasm/src/lib.rs @@ -479,12 +479,6 @@ pub struct VerifiedMessage { #[wasm_bindgen] impl VerifiedMessage { - /// The root id this message was verified against. - #[wasm_bindgen(js_name = "rootId")] - pub fn root_id(&self) -> Vec { - self.inner.root_id.to_vec() - } - /// All verified zones as plain JS objects. pub fn zones(&self) -> Result { let array = js_sys::Array::new(); diff --git a/testutil/src/lib.rs b/testutil/src/lib.rs index 9ff326a..6926774 100644 --- a/testutil/src/lib.rs +++ b/testutil/src/lib.rs @@ -533,6 +533,7 @@ impl TestHandleTree { delegate: ProvableOption::Unknown, commitment: ProvableOption::Unknown, num_id, + anchor_hash: [0u8; 32], }; let signature = sign_zone(&zone, &self.ds.ptr.keypair); diff --git a/veritas/src/lib.rs b/veritas/src/lib.rs index 9cd97b3..e0f48be 100644 --- a/veritas/src/lib.rs +++ b/veritas/src/lib.rs @@ -61,7 +61,6 @@ pub const VERIFY_ENABLE_SNARK: u32 = 1 << 1; /// Contains the verified zones and the original message data. /// The message can be used to construct certificates for storage. pub struct VerifiedMessage { - pub root_id: [u8; 32], pub zones: Vec, pub message: msg::Message, } @@ -184,6 +183,12 @@ impl fmt::Display for SovereigntyState { pub struct Zone { /// The block height of the anchor used to prove this zone (snapshot version). pub anchor: u32, + /// Hash of the root anchor this zone was verified against. + #[serde( + serialize_with = "serialize_hash", + deserialize_with = "deserialize_hash" + )] + pub anchor_hash: Hash, /// The sovereignty state indicating finality of the zone's commitment. pub sovereignty: SovereigntyState, /// Human-readable name (e.g., "nested1.alice@bitcoin"). @@ -355,6 +360,7 @@ impl BorshDeserialize for ProvableOption { impl BorshSerialize for Zone { fn serialize(&self, writer: &mut W) -> std::io::Result<()> { BorshSerialize::serialize(&self.anchor, writer)?; + BorshSerialize::serialize(&self.anchor_hash, writer)?; BorshSerialize::serialize(&self.sovereignty, writer)?; BorshSerialize::serialize(&self.canonical, writer)?; BorshSerialize::serialize(&self.handle, writer)?; @@ -371,6 +377,7 @@ impl BorshSerialize for Zone { impl BorshDeserialize for Zone { fn deserialize_reader(reader: &mut R) -> std::io::Result { let anchor = u32::deserialize_reader(reader)?; + let anchor_hash = <[u8; 32]>::deserialize_reader(reader)?; let sovereignty = SovereigntyState::deserialize_reader(reader)?; let canonical = SName::deserialize_reader(reader)?; let handle = SName::deserialize_reader(reader)?; @@ -386,6 +393,7 @@ impl BorshDeserialize for Zone { let num_id = Option::::deserialize_reader(reader)?; Ok(Zone { anchor, + anchor_hash, sovereignty, handle, canonical, @@ -512,12 +520,13 @@ impl Zone { /// Returns the zone serialized for signing. /// - /// The `anchor` and `records` fields are zeroed out so delegate - /// signatures remain valid across different anchor snapshots and - /// don't include owner-signed records. + /// The `anchor`, `anchor_hash`, and `records` fields are zeroed out so + /// delegate signatures remain valid across different anchor snapshots + /// and don't include owner-signed records. pub fn signing_bytes(&self) -> Vec { let mut zone = self.clone(); zone.anchor = 0; + zone.anchor_hash = [0u8; 32]; zone.records = sip7::RecordSet::default(); borsh::to_vec(&zone).expect("zone serialization should not fail") } @@ -844,8 +853,12 @@ impl Veritas { let resolver = names::NameResolver::from_zones(&zones); resolver.expand_zones(&mut zones); + let anchor_hash = compute_root_id(&anchor); + for zone in &mut zones { + zone.anchor_hash = anchor_hash; + } + Ok(VerifiedMessage { - root_id: compute_root_id(&anchor), zones, message: msg::Message { chain: msg.chain, @@ -1190,6 +1203,7 @@ impl Veritas { delegate: ProvableOption::Unknown, commitment: ProvableOption::Unknown, num_id, + anchor_hash: [0u8; 32], }; // Verify records signature if present @@ -1312,6 +1326,7 @@ fn verify_temporary_handle( delegate: ProvableOption::Unknown, commitment: ProvableOption::Unknown, num_id, + anchor_hash: [0u8; 32], }; zone.verify_signature(handle.signature.as_ref().unwrap(), signer) @@ -1399,6 +1414,7 @@ fn verify_final_handle( delegate: ProvableOption::Unknown, commitment: ProvableOption::Unknown, num_id: Some(num_id), + anchor_hash: [0u8; 32], }; Ok(zone) @@ -1646,6 +1662,31 @@ fn decode_journal( }) } +fn serialize_hash(hash: &Hash, serializer: S) -> Result +where + S: Serializer, +{ + if serializer.is_human_readable() { + serializer.serialize_str(&hex::encode(hash)) + } else { + serializer.serialize_bytes(hash) + } +} + +fn deserialize_hash<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + if deserializer.is_human_readable() { + let s: String = ::deserialize(deserializer)?; + let mut bytes = [0u8; 32]; + hex::decode_to_slice(&s, &mut bytes).map_err(serde::de::Error::custom)?; + Ok(bytes) + } else { + <[u8; 32] as Deserialize>::deserialize(deserializer) + } +} + fn serialize_option_hash(hash: &Option, serializer: S) -> Result where S: Serializer, diff --git a/veritas/src/names.rs b/veritas/src/names.rs index d6df3ab..6906681 100644 --- a/veritas/src/names.rs +++ b/veritas/src/names.rs @@ -420,6 +420,7 @@ mod tests { delegate: crate::ProvableOption::Unknown, commitment: crate::ProvableOption::Unknown, num_id: None, + anchor_hash: [0u8; 32], } } diff --git a/veritas/tests/integration_tests.rs b/veritas/tests/integration_tests.rs index 508d166..964ce80 100644 --- a/veritas/tests/integration_tests.rs +++ b/veritas/tests/integration_tests.rs @@ -476,6 +476,7 @@ impl TestHandleTree { delegate: ProvableOption::Unknown, commitment: ProvableOption::Unknown, num_id, + anchor_hash: [0u8; 32], }; let signature = sign_zone(&zone, &self.ds.ptr.keypair); @@ -1006,6 +1007,7 @@ fn verify_uses_better_cached_zone() { delegate: ProvableOption::Unknown, commitment: ProvableOption::Unknown, num_id: None, + anchor_hash: [0u8; 32], }; let ctx = QueryContext::from_zones(vec![cached_zone.clone()]);