ソースを参照

Co-locate the canonical-bytes contract so the hash preimage is auditable

The content-addressing contract (what bytes get hashed, in what order, and
under which version byte) is the definition EnvelopeId and the account
snapshot hashes depend on. It was split across three regions of the types
crate: the ToBytes trait, CANONICAL_VERSION, and the write helpers near the
top; the Cent impl in the middle; and the other fifteen impls at the bottom.
Auditing "is every field folded in, in the right order" meant holding three
windows open, which is exactly the bug class the CANONICAL_VERSION comments
track.

Move the whole contract into a canonical module: the trait, the version byte,
the big-endian write helpers, and every impl ToBytes, in that order, with a
module doc that states the encoding rules. The preimage surface is now visible
at once. The public API is unchanged; lib.rs re-exports ToBytes,
CANONICAL_VERSION, and the write helpers, so downstream paths still resolve.
Cesar Rodas 2 日 前
コミット
cb49d9c434
2 ファイル変更233 行追加218 行削除
  1. 226 0
      crates/kuatia-types/src/canonical.rs
  2. 7 218
      crates/kuatia-types/src/lib.rs

+ 226 - 0
crates/kuatia-types/src/canonical.rs

@@ -0,0 +1,226 @@
+//! Canonical binary serialization — the content-addressing contract.
+//!
+//! Everything that defines the hash preimage lives here in one place: the
+//! [`ToBytes`] trait, the [`CANONICAL_VERSION`] byte, the big-endian write
+//! helpers, and every `impl ToBytes`. [`EnvelopeId`](crate::EnvelopeId) and the
+//! account snapshot hashes are the double-SHA256 of these bytes, so a change to
+//! any impl below changes those ids. Keeping the trait, the version, and all the
+//! impls together makes the preimage auditable at a glance: what bytes are
+//! hashed, in what order, is visible without holding three windows open.
+//!
+//! Encoding rules: integers are big-endian, variable-length sequences are
+//! prefixed with a `u32` length, and the top-level [`Envelope`](crate::Envelope)
+//! and [`Account`](crate::Account) preimages begin with [`CANONICAL_VERSION`].
+
+use crate::{
+    Account, AccountFlags, AccountId, AccountPolicy, AccountSnapshotId, AssetId, BookId, Cent,
+    Envelope, EnvelopeId, NewPosting, Posting, PostingId, Receipt,
+};
+
+/// Deterministic binary serialization. Every domain type can produce its
+/// canonical byte representation.
+pub trait ToBytes {
+    /// Returns the canonical byte representation of this value.
+    fn to_bytes(&self) -> Vec<u8>;
+}
+
+/// Version byte prepended to canonical serializations for forward compatibility.
+/// Bumped to 2 when `Cent` moved to a fixed 16-byte canonical encoding (ADR-0011).
+/// Bumped to 3 when `AccountId` gained a `subaccount` leg folded into its
+/// canonical bytes (ADR-0012).
+/// Bumped to 4 when the vestigial `UserData` fields were removed from the
+/// `Envelope` and `Account` preimages.
+pub const CANONICAL_VERSION: u8 = 4;
+
+/// Append a `u16` in big-endian to `buf`.
+pub fn write_u16(buf: &mut Vec<u8>, v: u16) {
+    buf.extend_from_slice(&v.to_be_bytes());
+}
+
+/// Append a `u32` in big-endian to `buf`.
+pub fn write_u32(buf: &mut Vec<u8>, v: u32) {
+    buf.extend_from_slice(&v.to_be_bytes());
+}
+
+/// Append a `u64` in big-endian to `buf`.
+pub fn write_u64(buf: &mut Vec<u8>, v: u64) {
+    buf.extend_from_slice(&v.to_be_bytes());
+}
+
+/// Append an `i64` in big-endian to `buf`.
+pub fn write_i64(buf: &mut Vec<u8>, v: i64) {
+    buf.extend_from_slice(&v.to_be_bytes());
+}
+
+/// Append a `u128` in big-endian to `buf`.
+pub fn write_u128(buf: &mut Vec<u8>, v: u128) {
+    buf.extend_from_slice(&v.to_be_bytes());
+}
+
+impl ToBytes for Cent {
+    fn to_bytes(&self) -> Vec<u8> {
+        self.to_canonical_bytes().to_vec()
+    }
+}
+
+impl ToBytes for AccountId {
+    fn to_bytes(&self) -> Vec<u8> {
+        // Base id then subaccount, both big-endian, so the subaccount is folded
+        // into every content hash (envelope ids, posting ids, snapshots).
+        let mut buf = Vec::with_capacity(16);
+        buf.extend_from_slice(&self.id.to_be_bytes());
+        buf.extend_from_slice(&self.sub.to_be_bytes());
+        buf
+    }
+}
+
+impl ToBytes for AccountSnapshotId {
+    fn to_bytes(&self) -> Vec<u8> {
+        let mut buf = Vec::with_capacity(48);
+        buf.extend_from_slice(&self.account.to_bytes());
+        buf.extend_from_slice(&self.snapshot_id);
+        buf
+    }
+}
+
+impl ToBytes for AssetId {
+    fn to_bytes(&self) -> Vec<u8> {
+        self.0.to_be_bytes().to_vec()
+    }
+}
+
+impl ToBytes for EnvelopeId {
+    fn to_bytes(&self) -> Vec<u8> {
+        self.0.to_vec()
+    }
+}
+
+impl ToBytes for PostingId {
+    fn to_bytes(&self) -> Vec<u8> {
+        let mut buf = Vec::with_capacity(34);
+        buf.extend_from_slice(&self.transfer.0);
+        write_u16(&mut buf, self.index);
+        buf
+    }
+}
+
+impl ToBytes for AccountPolicy {
+    fn to_bytes(&self) -> Vec<u8> {
+        let mut buf = Vec::with_capacity(9);
+        match self {
+            Self::NoOverdraft => buf.push(0),
+            Self::CappedOverdraft { floor } => {
+                buf.push(1);
+                buf.extend(floor.to_bytes());
+            }
+            Self::UncappedOverdraft => buf.push(2),
+            Self::SystemAccount => buf.push(3),
+            Self::ExternalAccount => buf.push(4),
+        }
+        buf
+    }
+}
+
+impl ToBytes for AccountFlags {
+    fn to_bytes(&self) -> Vec<u8> {
+        self.bits().to_be_bytes().to_vec()
+    }
+}
+
+impl ToBytes for BookId {
+    fn to_bytes(&self) -> Vec<u8> {
+        self.0.to_be_bytes().to_vec()
+    }
+}
+
+impl ToBytes for NewPosting {
+    fn to_bytes(&self) -> Vec<u8> {
+        let mut buf = Vec::new();
+        buf.extend(self.owner.to_bytes());
+        buf.extend_from_slice(&self.asset.0.to_be_bytes());
+        buf.extend(self.value.to_bytes());
+        match &self.payer {
+            Some(p) => {
+                buf.push(1);
+                buf.extend(p.to_bytes());
+            }
+            None => buf.push(0),
+        }
+        buf
+    }
+}
+
+impl ToBytes for Posting {
+    fn to_bytes(&self) -> Vec<u8> {
+        let mut buf = Vec::new();
+        buf.extend(self.id.to_bytes());
+        buf.extend(self.owner.to_bytes());
+        buf.extend_from_slice(&self.asset.0.to_be_bytes());
+        buf.extend(self.value.to_bytes());
+        buf
+    }
+}
+
+impl ToBytes for Envelope {
+    fn to_bytes(&self) -> Vec<u8> {
+        let mut buf = Vec::new();
+        buf.push(CANONICAL_VERSION);
+
+        write_u32(&mut buf, self.consumes.len() as u32);
+        for pid in &self.consumes {
+            buf.extend(pid.to_bytes());
+        }
+
+        write_u32(&mut buf, self.creates.len() as u32);
+        for np in &self.creates {
+            buf.extend(np.to_bytes());
+        }
+
+        write_u32(&mut buf, self.account_snapshots.len() as u32);
+        for snap in &self.account_snapshots {
+            buf.extend(snap.to_bytes());
+        }
+
+        buf.extend(self.book.to_bytes());
+
+        write_u32(&mut buf, self.metadata.len() as u32);
+        for (key, value) in &self.metadata {
+            let key_bytes = key.as_bytes();
+            write_u32(&mut buf, key_bytes.len() as u32);
+            buf.extend_from_slice(key_bytes);
+            write_u32(&mut buf, value.len() as u32);
+            buf.extend_from_slice(value);
+        }
+
+        buf
+    }
+}
+
+impl ToBytes for Account {
+    fn to_bytes(&self) -> Vec<u8> {
+        let mut buf = Vec::new();
+        buf.push(CANONICAL_VERSION);
+        buf.extend(self.id.to_bytes());
+        write_u64(&mut buf, self.version);
+        buf.extend(self.policy.to_bytes());
+        buf.extend(self.flags.to_bytes());
+        buf.extend(self.book.to_bytes());
+
+        write_u32(&mut buf, self.metadata.len() as u32);
+        for (key, value) in &self.metadata {
+            let key_bytes = key.as_bytes();
+            write_u32(&mut buf, key_bytes.len() as u32);
+            buf.extend_from_slice(key_bytes);
+            write_u32(&mut buf, value.len() as u32);
+            buf.extend_from_slice(value);
+        }
+
+        buf
+    }
+}
+
+impl ToBytes for Receipt {
+    fn to_bytes(&self) -> Vec<u8> {
+        self.transfer_id.0.to_vec()
+    }
+}

+ 7 - 218
crates/kuatia-types/src/lib.rs

@@ -17,53 +17,14 @@ use serde::{Deserialize, Serialize};
 use std::collections::BTreeMap;
 use std::fmt;
 
-// ---------------------------------------------------------------------------
-// ToBytes trait
-// ---------------------------------------------------------------------------
-
-/// Deterministic binary serialization. Every domain type can produce its
-/// canonical byte representation.
-pub trait ToBytes {
-    /// Returns the canonical byte representation of this value.
-    fn to_bytes(&self) -> Vec<u8>;
-}
-
-// ---------------------------------------------------------------------------
-// Binary encoding helpers — big-endian, deterministic
-// ---------------------------------------------------------------------------
-
-/// Version byte prepended to canonical serializations for forward compatibility.
-/// Bumped to 2 when `Cent` moved to a fixed 16-byte canonical encoding (ADR-0011).
-/// Bumped to 3 when `AccountId` gained a `subaccount` leg folded into its
-/// canonical bytes (ADR-0012).
-/// Bumped to 4 when the vestigial `UserData` fields were removed from the
-/// `Envelope` and `Account` preimages.
-pub const CANONICAL_VERSION: u8 = 4;
-
-/// Append a `u16` in big-endian to `buf`.
-pub fn write_u16(buf: &mut Vec<u8>, v: u16) {
-    buf.extend_from_slice(&v.to_be_bytes());
-}
+mod canonical;
 
-/// Append a `u32` in big-endian to `buf`.
-pub fn write_u32(buf: &mut Vec<u8>, v: u32) {
-    buf.extend_from_slice(&v.to_be_bytes());
-}
-
-/// Append a `u64` in big-endian to `buf`.
-pub fn write_u64(buf: &mut Vec<u8>, v: u64) {
-    buf.extend_from_slice(&v.to_be_bytes());
-}
-
-/// Append an `i64` in big-endian to `buf`.
-pub fn write_i64(buf: &mut Vec<u8>, v: i64) {
-    buf.extend_from_slice(&v.to_be_bytes());
-}
-
-/// Append a `u128` in big-endian to `buf`.
-pub fn write_u128(buf: &mut Vec<u8>, v: u128) {
-    buf.extend_from_slice(&v.to_be_bytes());
-}
+// The content-addressing contract (trait, version byte, write helpers, and
+// every `impl ToBytes`) lives in `canonical`. Re-exported here so the public
+// surface stays `kuatia_types::{ToBytes, CANONICAL_VERSION, write_*}`.
+pub use canonical::{
+    CANONICAL_VERSION, ToBytes, write_i64, write_u16, write_u32, write_u64, write_u128,
+};
 
 // ---------------------------------------------------------------------------
 // Identifiers
@@ -131,12 +92,6 @@ pub struct PostingId {
 
 pub use kuatia_money::{Amount, Cent, OverflowError, ParseAmountError};
 
-impl ToBytes for Cent {
-    fn to_bytes(&self) -> Vec<u8> {
-        self.to_canonical_bytes().to_vec()
-    }
-}
-
 // ---------------------------------------------------------------------------
 // Debug / Display impls for identifiers
 // ---------------------------------------------------------------------------
@@ -838,169 +793,3 @@ impl TransferBuilder {
         self.transfer
     }
 }
-
-// ---------------------------------------------------------------------------
-// ToBytes implementations
-// ---------------------------------------------------------------------------
-
-impl ToBytes for AccountId {
-    fn to_bytes(&self) -> Vec<u8> {
-        // Base id then subaccount, both big-endian, so the subaccount is folded
-        // into every content hash (envelope ids, posting ids, snapshots).
-        let mut buf = Vec::with_capacity(16);
-        buf.extend_from_slice(&self.id.to_be_bytes());
-        buf.extend_from_slice(&self.sub.to_be_bytes());
-        buf
-    }
-}
-
-impl ToBytes for AccountSnapshotId {
-    fn to_bytes(&self) -> Vec<u8> {
-        let mut buf = Vec::with_capacity(48);
-        buf.extend_from_slice(&self.account.to_bytes());
-        buf.extend_from_slice(&self.snapshot_id);
-        buf
-    }
-}
-
-impl ToBytes for AssetId {
-    fn to_bytes(&self) -> Vec<u8> {
-        self.0.to_be_bytes().to_vec()
-    }
-}
-
-impl ToBytes for EnvelopeId {
-    fn to_bytes(&self) -> Vec<u8> {
-        self.0.to_vec()
-    }
-}
-
-impl ToBytes for PostingId {
-    fn to_bytes(&self) -> Vec<u8> {
-        let mut buf = Vec::with_capacity(34);
-        buf.extend_from_slice(&self.transfer.0);
-        write_u16(&mut buf, self.index);
-        buf
-    }
-}
-
-impl ToBytes for AccountPolicy {
-    fn to_bytes(&self) -> Vec<u8> {
-        let mut buf = Vec::with_capacity(9);
-        match self {
-            Self::NoOverdraft => buf.push(0),
-            Self::CappedOverdraft { floor } => {
-                buf.push(1);
-                buf.extend(floor.to_bytes());
-            }
-            Self::UncappedOverdraft => buf.push(2),
-            Self::SystemAccount => buf.push(3),
-            Self::ExternalAccount => buf.push(4),
-        }
-        buf
-    }
-}
-
-impl ToBytes for AccountFlags {
-    fn to_bytes(&self) -> Vec<u8> {
-        self.bits().to_be_bytes().to_vec()
-    }
-}
-
-impl ToBytes for BookId {
-    fn to_bytes(&self) -> Vec<u8> {
-        self.0.to_be_bytes().to_vec()
-    }
-}
-
-impl ToBytes for NewPosting {
-    fn to_bytes(&self) -> Vec<u8> {
-        let mut buf = Vec::new();
-        buf.extend(self.owner.to_bytes());
-        buf.extend_from_slice(&self.asset.0.to_be_bytes());
-        buf.extend(self.value.to_bytes());
-        match &self.payer {
-            Some(p) => {
-                buf.push(1);
-                buf.extend(p.to_bytes());
-            }
-            None => buf.push(0),
-        }
-        buf
-    }
-}
-
-impl ToBytes for Posting {
-    fn to_bytes(&self) -> Vec<u8> {
-        let mut buf = Vec::new();
-        buf.extend(self.id.to_bytes());
-        buf.extend(self.owner.to_bytes());
-        buf.extend_from_slice(&self.asset.0.to_be_bytes());
-        buf.extend(self.value.to_bytes());
-        buf
-    }
-}
-
-impl ToBytes for Envelope {
-    fn to_bytes(&self) -> Vec<u8> {
-        let mut buf = Vec::new();
-        buf.push(CANONICAL_VERSION);
-
-        write_u32(&mut buf, self.consumes.len() as u32);
-        for pid in &self.consumes {
-            buf.extend(pid.to_bytes());
-        }
-
-        write_u32(&mut buf, self.creates.len() as u32);
-        for np in &self.creates {
-            buf.extend(np.to_bytes());
-        }
-
-        write_u32(&mut buf, self.account_snapshots.len() as u32);
-        for snap in &self.account_snapshots {
-            buf.extend(snap.to_bytes());
-        }
-
-        buf.extend(self.book.to_bytes());
-
-        write_u32(&mut buf, self.metadata.len() as u32);
-        for (key, value) in &self.metadata {
-            let key_bytes = key.as_bytes();
-            write_u32(&mut buf, key_bytes.len() as u32);
-            buf.extend_from_slice(key_bytes);
-            write_u32(&mut buf, value.len() as u32);
-            buf.extend_from_slice(value);
-        }
-
-        buf
-    }
-}
-
-impl ToBytes for Account {
-    fn to_bytes(&self) -> Vec<u8> {
-        let mut buf = Vec::new();
-        buf.push(CANONICAL_VERSION);
-        buf.extend(self.id.to_bytes());
-        write_u64(&mut buf, self.version);
-        buf.extend(self.policy.to_bytes());
-        buf.extend(self.flags.to_bytes());
-        buf.extend(self.book.to_bytes());
-
-        write_u32(&mut buf, self.metadata.len() as u32);
-        for (key, value) in &self.metadata {
-            let key_bytes = key.as_bytes();
-            write_u32(&mut buf, key_bytes.len() as u32);
-            buf.extend_from_slice(key_bytes);
-            write_u32(&mut buf, value.len() as u32);
-            buf.extend_from_slice(value);
-        }
-
-        buf
-    }
-}
-
-impl ToBytes for Receipt {
-    fn to_bytes(&self) -> Vec<u8> {
-        self.transfer_id.0.to_vec()
-    }
-}