lib.rs 50 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435
  1. //! Domain types for the ledger.
  2. //!
  3. //! These types model the UTXO-style ledger where value is held as **postings** —
  4. //! signed amounts owned by exactly one account. An account's balance is simply the
  5. //! sum of its active postings, which eliminates the need for running balance fields
  6. //! and makes the system trivially auditable by replaying the transfer log.
  7. pub mod autoid;
  8. use serde::{Deserialize, Serialize};
  9. use std::collections::BTreeMap;
  10. use std::fmt;
  11. use std::sync::atomic::{AtomicU64, Ordering};
  12. // ---------------------------------------------------------------------------
  13. // ToBytes trait
  14. // ---------------------------------------------------------------------------
  15. /// Deterministic binary serialization. Every domain type can produce its
  16. /// canonical byte representation.
  17. pub trait ToBytes {
  18. /// Returns the canonical byte representation of this value.
  19. fn to_bytes(&self) -> Vec<u8>;
  20. }
  21. // ---------------------------------------------------------------------------
  22. // Binary encoding helpers — big-endian, deterministic
  23. // ---------------------------------------------------------------------------
  24. /// Version byte prepended to canonical serializations for forward compatibility.
  25. /// Bumped to 2 when `Cent` moved to a fixed 16-byte canonical encoding (ADR-0011).
  26. /// Bumped to 3 when `AccountId` gained a `subaccount` leg folded into its
  27. /// canonical bytes (ADR-0012).
  28. /// Bumped to 4 when the vestigial `UserData` fields were removed from the
  29. /// `Envelope` and `Account` preimages.
  30. pub const CANONICAL_VERSION: u8 = 4;
  31. /// Append a `u16` in big-endian to `buf`.
  32. pub fn write_u16(buf: &mut Vec<u8>, v: u16) {
  33. buf.extend_from_slice(&v.to_be_bytes());
  34. }
  35. /// Append a `u32` in big-endian to `buf`.
  36. pub fn write_u32(buf: &mut Vec<u8>, v: u32) {
  37. buf.extend_from_slice(&v.to_be_bytes());
  38. }
  39. /// Append a `u64` in big-endian to `buf`.
  40. pub fn write_u64(buf: &mut Vec<u8>, v: u64) {
  41. buf.extend_from_slice(&v.to_be_bytes());
  42. }
  43. /// Append an `i64` in big-endian to `buf`.
  44. pub fn write_i64(buf: &mut Vec<u8>, v: i64) {
  45. buf.extend_from_slice(&v.to_be_bytes());
  46. }
  47. /// Append a `u128` in big-endian to `buf`.
  48. pub fn write_u128(buf: &mut Vec<u8>, v: u128) {
  49. buf.extend_from_slice(&v.to_be_bytes());
  50. }
  51. // ---------------------------------------------------------------------------
  52. // Identifiers
  53. // ---------------------------------------------------------------------------
  54. /// Stable account identity. Used in all public APIs.
  55. ///
  56. /// An account is a base `id` plus a `subaccount`. `sub = 0` is the main account
  57. /// (the default when subaccounts are not used); a non-zero `sub` is a
  58. /// subaccount of the same base id. Each `(id, sub)` is a full account record
  59. /// with its own policy and lifecycle. See ADR-0012 and ADR-0015.
  60. ///
  61. /// Both legs are stored as `i64` (they hash and persist as full `i64`), but the
  62. /// IBAN-style string form ([`Display`](fmt::Display) / [`FromStr`](std::str::FromStr))
  63. /// encodes only the low `ID_BITS` of `id` (a 63-bit snowflake never sets the
  64. /// sign bit) and the low `SUB_BITS` of `sub`. That is what lets the code fit in
  65. /// a fixed 20 characters. Values outside those ranges still hash, persist, and
  66. /// compare correctly, but do not round-trip through the string form.
  67. #[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
  68. pub struct AccountId {
  69. /// Base account id (a 63-bit snowflake; the sign bit is always 0).
  70. pub id: i64,
  71. /// Subaccount id; `0` is the main account. The string form encodes the low
  72. /// [`SUB_BITS`] bits, so a subaccount id must fit in that range to round-trip.
  73. pub sub: i64,
  74. }
  75. /// Pairs an [`AccountId`] with a snapshot hash — the double-SHA256 of the
  76. /// account's state at a point in time. Stored on [`Transfer`] to record which
  77. /// account versions a transfer was executed against. Internal type — the
  78. /// public API uses [`AccountId`].
  79. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
  80. pub struct AccountSnapshotId {
  81. /// The account (subaccount) this snapshot belongs to.
  82. pub account: AccountId,
  83. /// Double-SHA256 of the account's state at the time of the snapshot.
  84. pub snapshot_id: [u8; 32],
  85. }
  86. /// Identifies an asset (USD, EUR, BTC, …). Conservation is enforced per asset,
  87. /// so each asset is an independent conservation boundary.
  88. #[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
  89. pub struct AssetId(pub u32);
  90. /// Content-addressed transfer identifier — the double-SHA256 of the canonical
  91. /// serialization. This makes the id both the idempotency key and the
  92. /// tamper-evidence artifact.
  93. #[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
  94. pub struct EnvelopeId(pub [u8; 32]);
  95. /// Uniquely identifies a posting within the ledger. The `(transfer, index)` pair
  96. /// ties every posting back to the transfer that created it, which is the basis
  97. /// of the provenance graph.
  98. #[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
  99. pub struct PostingId {
  100. /// The transfer that created this posting.
  101. pub transfer: EnvelopeId,
  102. /// Zero-based position within the transfer's created postings.
  103. pub index: u16,
  104. }
  105. // ---------------------------------------------------------------------------
  106. // Cent — re-exported from kuatia-money (swappable integer backing)
  107. // ---------------------------------------------------------------------------
  108. pub use kuatia_money::{Amount, Cent, OverflowError, ParseAmountError};
  109. impl ToBytes for Cent {
  110. fn to_bytes(&self) -> Vec<u8> {
  111. self.to_canonical_bytes().to_vec()
  112. }
  113. }
  114. // ---------------------------------------------------------------------------
  115. // Debug / Display impls for identifiers
  116. // ---------------------------------------------------------------------------
  117. impl fmt::Debug for AccountId {
  118. fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
  119. if self.sub == 0 {
  120. write!(f, "AccountId({})", self.id)
  121. } else {
  122. write!(f, "AccountId({}.{})", self.id, self.sub)
  123. }
  124. }
  125. }
  126. impl fmt::Display for AccountId {
  127. /// IBAN-style machine format: a fixed 20 characters, an 18-character base-36
  128. /// body followed by two trailing ISO 7064 mod-97 check digits, with no
  129. /// country code. The `(id, sub)` pair is packed into a 93-bit value and run
  130. /// through a keyed format-preserving permutation (see [`set_id_seed`]) before
  131. /// encoding, so the body does not reveal the raw ids. Round-trips via
  132. /// [`FromStr`](std::str::FromStr); [`to_grouped`](AccountId::to_grouped) adds
  133. /// the presentation spacing (five groups of four).
  134. fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
  135. let body = base36_body(obfuscate(pack(self.id, self.sub), id_seed()));
  136. write!(f, "{body}{:02}", check_digits(&body))
  137. }
  138. }
  139. impl fmt::Debug for AssetId {
  140. fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
  141. write!(f, "AssetId({:#010x})", self.0)
  142. }
  143. }
  144. impl fmt::Debug for EnvelopeId {
  145. fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
  146. write!(f, "EnvelopeId({})", hex(&self.0))
  147. }
  148. }
  149. impl fmt::Debug for PostingId {
  150. fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
  151. f.debug_struct("PostingId")
  152. .field("transfer", &self.transfer)
  153. .field("index", &self.index)
  154. .finish()
  155. }
  156. }
  157. fn hex(bytes: &[u8]) -> String {
  158. bytes.iter().map(|b| format!("{b:02x}")).collect()
  159. }
  160. // ---------------------------------------------------------------------------
  161. // IBAN-style string view for AccountId (ADR-0012)
  162. // ---------------------------------------------------------------------------
  163. /// Bits of the base `id` the string form encodes. A snowflake id keeps the sign
  164. /// bit clear, so its value fits in 63 bits.
  165. pub const ID_BITS: u32 = 63;
  166. /// Bits of the `sub` leg the string form encodes. Together with [`ID_BITS`] this
  167. /// is the 93-bit packed value the code carries, which fits in the 18 base-36
  168. /// characters of the code body. A subaccount id must fit in this many bits to
  169. /// round-trip through the string form.
  170. pub const SUB_BITS: u32 = 30;
  171. /// Number of base-36 characters in the code body. `36^18 > 2^93`, so the packed
  172. /// `(id, sub)` value always fits.
  173. const BODY_LEN: usize = 18;
  174. /// Mask selecting the low [`ID_BITS`] + [`SUB_BITS`] = 93 bits of the packed value.
  175. const PACK_MASK: u128 = (1u128 << (ID_BITS + SUB_BITS)) - 1;
  176. /// Pack `(id, sub)` into the 93-bit value the code encodes: the low [`ID_BITS`]
  177. /// of `id` in the high part, the low [`SUB_BITS`] of `sub` in the low part.
  178. fn pack(id: i64, sub: i64) -> u128 {
  179. let id = (id as u128) & ((1u128 << ID_BITS) - 1);
  180. let sub = (sub as u128) & ((1u128 << SUB_BITS) - 1);
  181. (id << SUB_BITS) | sub
  182. }
  183. /// Inverse of [`pack`]: split the 93-bit value back into the two legs. The `id`
  184. /// part is at most 63 bits, so it is always a non-negative `i64`.
  185. fn unpack(p: u128) -> (i64, i64) {
  186. let sub = (p & ((1u128 << SUB_BITS) - 1)) as i64;
  187. let id = (p >> SUB_BITS) as i64;
  188. (id, sub)
  189. }
  190. /// Encode a value `< 2^93` as exactly [`BODY_LEN`] base-36 digits (`0-9A-Z`),
  191. /// zero-padded on the left. `36^18 > 2^93`, so this never truncates.
  192. fn base36_body(mut v: u128) -> String {
  193. const D: &[u8; 36] = b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
  194. let mut out = [b'0'; BODY_LEN];
  195. let mut i = out.len();
  196. while v > 0 && i > 0 {
  197. i -= 1;
  198. out[i] = D[(v % 36) as usize];
  199. v /= 36;
  200. }
  201. out.iter().map(|&b| b as char).collect()
  202. }
  203. /// Expand an IBAN string to its numeric form for the checksum: digits stay,
  204. /// letters `A-Z` become `10..35`.
  205. fn iban_expand(s: &str) -> String {
  206. let mut out = String::with_capacity(s.len() * 2);
  207. for c in s.bytes() {
  208. if c.is_ascii_digit() {
  209. out.push(c as char);
  210. } else {
  211. let v = (c - b'A') as u32 + 10;
  212. out.push_str(&v.to_string());
  213. }
  214. }
  215. out
  216. }
  217. /// ISO 7064 mod-97-10 over a decimal string, computed iteratively so the input
  218. /// length is unbounded.
  219. fn mod97(digits: &str) -> u32 {
  220. let mut rem = 0u32;
  221. for b in digits.bytes() {
  222. rem = (rem * 10 + (b - b'0') as u32) % 97;
  223. }
  224. rem
  225. }
  226. /// The two mod-97 check digits for a base-36 body, IBAN-style but with no
  227. /// country code: `98 - (expand(body ++ "00") mod 97)`.
  228. fn check_digits(body: &str) -> u32 {
  229. 98 - mod97(&iban_expand(&format!("{body}00")))
  230. }
  231. // ---------------------------------------------------------------------------
  232. // Account-code obfuscation (ADR-0012, ADR-0015)
  233. //
  234. // The account code's body is a base-36 rendering of the packed (id, sub) value.
  235. // Without mixing, small ids render as long runs of zeros that reveal their value
  236. // and sequence. To hide that from outsiders, the 93-bit packed value is run
  237. // through a keyed format-preserving permutation before encoding, and inverted on
  238. // parse. The permutation is a Feistel network over 2^94 restricted to the 93-bit
  239. // domain by cycle-walking (re-encrypt while the result exceeds the domain), which
  240. // keeps it a bijection on the exact set of packable values.
  241. //
  242. // This is obfuscation, not security: anyone with the seed can decode it, so it
  243. // is not a substitute for authorization. The seed has a default and can be set
  244. // once at startup via `set_id_seed`; changing it changes every code, so it must
  245. // be stable across a deployment.
  246. // ---------------------------------------------------------------------------
  247. /// Default seed for the account-code obfuscation permutation. Override at
  248. /// startup with [`set_id_seed`], before any code is issued or parsed.
  249. pub const DEFAULT_ID_SEED: u64 = 0x9E37_79B9_7F4A_7C15;
  250. /// Process-global seed keying the account-code permutation.
  251. static ID_SEED: AtomicU64 = AtomicU64::new(DEFAULT_ID_SEED);
  252. /// Set the process-global seed that keys the account-code obfuscation. Call once
  253. /// at startup: every [`AccountId`] string form depends on it, so changing it
  254. /// after codes are issued invalidates the previously issued ones.
  255. pub fn set_id_seed(seed: u64) {
  256. ID_SEED.store(seed, Ordering::Relaxed);
  257. }
  258. /// The current process-global account-code seed.
  259. pub fn id_seed() -> u64 {
  260. ID_SEED.load(Ordering::Relaxed)
  261. }
  262. /// Number of Feistel rounds. Four rounds of a strong round function give a
  263. /// strong pseudo-random permutation (Luby-Rackoff), which is ample for
  264. /// obfuscation.
  265. const FEISTEL_ROUNDS: usize = 4;
  266. /// SplitMix64 finalizer: a strong 64-bit avalanche mixer.
  267. fn mix64(mut z: u64) -> u64 {
  268. z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
  269. z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
  270. z ^ (z >> 31)
  271. }
  272. /// Per-round subkey derived from the seed and round index.
  273. fn round_key(seed: u64, round: usize) -> u64 {
  274. mix64(seed ^ (round as u64).wrapping_mul(0x9E37_79B9_7F4A_7C15))
  275. }
  276. /// Bits per Feistel half. Two halves give a 94-bit block, the smallest even
  277. /// width that covers the 93-bit packed domain (one bit of headroom that
  278. /// cycle-walking absorbs).
  279. const FEISTEL_HALF_BITS: u32 = 47;
  280. /// Mask selecting one [`FEISTEL_HALF_BITS`]-wide half.
  281. const FEISTEL_HALF_MASK: u128 = (1u128 << FEISTEL_HALF_BITS) - 1;
  282. /// Keyed Feistel permutation over the 94-bit block `(l, r)` (two 47-bit halves).
  283. /// A balanced Feistel with any round function is a bijection on the full 2^94
  284. /// space, which is what [`obfuscate`] cycle-walks down to the 93-bit domain.
  285. fn feistel94(block: u128, seed: u64) -> u128 {
  286. let mut l = (block >> FEISTEL_HALF_BITS) & FEISTEL_HALF_MASK;
  287. let mut r = block & FEISTEL_HALF_MASK;
  288. for round in 0..FEISTEL_ROUNDS {
  289. let f = (mix64(r as u64 ^ round_key(seed, round)) as u128) & FEISTEL_HALF_MASK;
  290. let next = (l ^ f) & FEISTEL_HALF_MASK;
  291. l = r;
  292. r = next;
  293. }
  294. (l << FEISTEL_HALF_BITS) | r
  295. }
  296. /// Inverse of [`feistel94`] under the same seed.
  297. fn feistel94_inv(block: u128, seed: u64) -> u128 {
  298. let mut l = (block >> FEISTEL_HALF_BITS) & FEISTEL_HALF_MASK;
  299. let mut r = block & FEISTEL_HALF_MASK;
  300. for round in (0..FEISTEL_ROUNDS).rev() {
  301. let f = (mix64(l as u64 ^ round_key(seed, round)) as u128) & FEISTEL_HALF_MASK;
  302. let prev = (r ^ f) & FEISTEL_HALF_MASK;
  303. r = l;
  304. l = prev;
  305. }
  306. (l << FEISTEL_HALF_BITS) | r
  307. }
  308. /// Keyed format-preserving permutation over the 93-bit packed domain. Applies
  309. /// [`feistel94`] and cycle-walks (re-encrypts while the result exceeds the
  310. /// domain) so the output is always a valid packed value. Since the domain is
  311. /// half of 2^94, this averages about two iterations and always terminates.
  312. fn obfuscate(p: u128, seed: u64) -> u128 {
  313. let mut v = p;
  314. loop {
  315. v = feistel94(v, seed);
  316. if v <= PACK_MASK {
  317. return v;
  318. }
  319. }
  320. }
  321. /// Inverse of [`obfuscate`] under the same seed: cycle-walk [`feistel94_inv`].
  322. fn deobfuscate(y: u128, seed: u64) -> u128 {
  323. let mut v = y;
  324. loop {
  325. v = feistel94_inv(v, seed);
  326. if v <= PACK_MASK {
  327. return v;
  328. }
  329. }
  330. }
  331. // ---------------------------------------------------------------------------
  332. // Identifier constructors
  333. // ---------------------------------------------------------------------------
  334. impl Default for AccountId {
  335. fn default() -> Self {
  336. // Process-global generator: a per-thread one could mint the same id on
  337. // two threads within a millisecond, yielding duplicate account ids.
  338. static GEN: crate::autoid::AutoId = crate::autoid::AutoId::new();
  339. Self {
  340. id: GEN.next(),
  341. sub: 0,
  342. }
  343. }
  344. }
  345. impl AccountId {
  346. /// Create the main account (`sub = 0`) for a base `id`.
  347. pub const fn new(id: i64) -> Self {
  348. Self { id, sub: 0 }
  349. }
  350. /// Create a specific subaccount of a base `id`.
  351. pub const fn with_sub(id: i64, sub: i64) -> Self {
  352. Self { id, sub }
  353. }
  354. /// Return the main account of this id (`sub` set to `0`).
  355. pub const fn base(&self) -> Self {
  356. Self {
  357. id: self.id,
  358. sub: 0,
  359. }
  360. }
  361. /// Whether this is the main account (`sub == 0`).
  362. pub const fn is_main(&self) -> bool {
  363. self.sub == 0
  364. }
  365. /// IBAN-style presentation format: the machine [`Display`](fmt::Display)
  366. /// form grouped into blocks of four with a single space. The code is a fixed
  367. /// 20 characters, so this is always five groups of four
  368. /// (e.g. `K3P9 WM2X Q7ND V8HT 2R47`).
  369. pub fn to_grouped(&self) -> String {
  370. let machine = self.to_string();
  371. let mut out = String::with_capacity(machine.len() + machine.len() / 4);
  372. for (i, c) in machine.chars().enumerate() {
  373. if i > 0 && i % 4 == 0 {
  374. out.push(' ');
  375. }
  376. out.push(c);
  377. }
  378. out
  379. }
  380. }
  381. impl From<AccountSnapshotId> for AccountId {
  382. fn from(snap: AccountSnapshotId) -> Self {
  383. snap.account
  384. }
  385. }
  386. /// Returned when a string is not a valid [`AccountId`] code: wrong structure,
  387. /// non-base-36 body, or a failed mod-97 checksum.
  388. #[derive(Debug, Clone, PartialEq, Eq)]
  389. pub struct ParseAccountIdError;
  390. impl fmt::Display for ParseAccountIdError {
  391. fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
  392. write!(f, "invalid AccountId: not a checksum-valid account code")
  393. }
  394. }
  395. impl std::error::Error for ParseAccountIdError {}
  396. impl std::str::FromStr for AccountId {
  397. type Err = ParseAccountIdError;
  398. /// Parse an IBAN-style account code back into the two legs. Any spaces
  399. /// (grouped display format) and dashes (URL-safe separator) are ignored and
  400. /// the input is upper-cased first, so `K3P9...`, `K3P9 WM2X ...`, and
  401. /// `K3P9-WM2X-...` all parse to the same id. The value must reduce to an
  402. /// 18-character base-36 body followed by two check digits, the ISO 7064
  403. /// mod-97 checksum must pass, and the decoded body must lie in the packable
  404. /// domain — so a mistyped or otherwise invalid id is rejected here rather
  405. /// than reaching the store.
  406. fn from_str(s: &str) -> Result<Self, Self::Err> {
  407. let cleaned: String = s
  408. .chars()
  409. .filter(|c| !c.is_whitespace() && *c != '-')
  410. .map(|c| c.to_ascii_uppercase())
  411. .collect();
  412. // 18-char base-36 body + 2 trailing check digits.
  413. if cleaned.len() != BODY_LEN + 2 {
  414. return Err(ParseAccountIdError);
  415. }
  416. let body = &cleaned[0..BODY_LEN];
  417. let check = &cleaned[BODY_LEN..BODY_LEN + 2];
  418. let is_base36 = |b: u8| b.is_ascii_digit() || b.is_ascii_uppercase();
  419. if !check.bytes().all(|b| b.is_ascii_digit()) || !body.bytes().all(is_base36) {
  420. return Err(ParseAccountIdError);
  421. }
  422. // Checksum-valid iff the expanded (body ++ check) reduces to 1 under
  423. // mod-97.
  424. if mod97(&iban_expand(&format!("{body}{check}"))) != 1 {
  425. return Err(ParseAccountIdError);
  426. }
  427. // Decode the body, reject anything outside the 93-bit packable domain
  428. // (the permutation's image), then invert the permutation and unpack.
  429. let obf = u128::from_str_radix(body, 36).map_err(|_| ParseAccountIdError)?;
  430. if obf > PACK_MASK {
  431. return Err(ParseAccountIdError);
  432. }
  433. let (id, sub) = unpack(deobfuscate(obf, id_seed()));
  434. Ok(Self { id, sub })
  435. }
  436. }
  437. impl AssetId {
  438. /// Create an `AssetId` from a `u32`.
  439. pub const fn new(id: u32) -> Self {
  440. Self(id)
  441. }
  442. }
  443. /// Identifies a book — a named scope for transfers.
  444. #[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
  445. pub struct BookId(pub i64);
  446. impl fmt::Debug for BookId {
  447. fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
  448. write!(f, "BookId({})", self.0)
  449. }
  450. }
  451. /// The implicit book used when a transfer does not name one. Fixed so that two
  452. /// otherwise-identical transfers hash to the same [`EnvelopeId`] — a random
  453. /// default would break content-addressed idempotency.
  454. pub const DEFAULT_BOOK: BookId = BookId(0);
  455. impl Default for BookId {
  456. /// Deterministic: returns [`DEFAULT_BOOK`]. Use [`BookId::generate`] to mint
  457. /// a fresh unique id for a real book.
  458. fn default() -> Self {
  459. DEFAULT_BOOK
  460. }
  461. }
  462. impl BookId {
  463. /// Create a `BookId` from an `i64`.
  464. pub const fn new(id: i64) -> Self {
  465. Self(id)
  466. }
  467. /// Mint a fresh, process-unique book id. Unlike [`Default`], this is not
  468. /// stable across calls — use it when creating a new [`Book`], never for the
  469. /// implicit book of a transfer.
  470. pub fn generate() -> Self {
  471. // Process-global so the "process-unique" contract holds across threads;
  472. // a per-thread generator can repeat an id on another thread.
  473. static GEN: crate::autoid::AutoId = crate::autoid::AutoId::new();
  474. Self(GEN.next())
  475. }
  476. }
  477. /// Identifies a reservation — the owner token stamped on a posting while it is
  478. /// `PendingInactive`, so only the saga that reserved it may finalize or release it.
  479. #[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
  480. pub struct ReservationId(pub i64);
  481. impl fmt::Debug for ReservationId {
  482. fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
  483. write!(f, "ReservationId({})", self.0)
  484. }
  485. }
  486. impl ReservationId {
  487. /// Create a `ReservationId` from an `i64`.
  488. pub const fn new(id: i64) -> Self {
  489. Self(id)
  490. }
  491. }
  492. impl Default for ReservationId {
  493. fn default() -> Self {
  494. // One process-global generator, not one per thread: its atomic counter
  495. // makes every reservation id unique across threads. A `thread_local`
  496. // generator lets two sagas on different threads mint the same id within
  497. // a millisecond, which collapses the reservation-ownership check and
  498. // allows a double-spend under concurrency.
  499. static GEN: crate::autoid::AutoId = crate::autoid::AutoId::new();
  500. Self(GEN.next())
  501. }
  502. }
  503. // ---------------------------------------------------------------------------
  504. // Book
  505. // ---------------------------------------------------------------------------
  506. /// A Book is a transfer policy scope: it gates which accounts and assets may
  507. /// participate in a transfer. It is **not** the chronological entry log (the
  508. /// transfer log plays that role), and it does **not** partition balances —
  509. /// balances are global; a Book only gates participation.
  510. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
  511. pub struct Book {
  512. /// Stable identity for this book.
  513. pub id: BookId,
  514. /// Human-readable name.
  515. pub name: String,
  516. /// Participation rules for this book.
  517. pub policy: BookPolicy,
  518. }
  519. /// The participation rules for a [`Book`]. An empty field means "no restriction".
  520. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
  521. pub struct BookPolicy {
  522. /// If non-empty, only these assets may appear in movements.
  523. pub allowed_assets: Vec<AssetId>,
  524. /// If non-empty, accounts with ANY of these flags may participate.
  525. pub allowed_flags: AccountFlags,
  526. /// If non-empty, these specific accounts may participate (in addition to flag matches).
  527. pub allowed_accounts: Vec<AccountId>,
  528. }
  529. /// Builder for constructing [`Book`] values.
  530. pub struct BookBuilder {
  531. book: Book,
  532. }
  533. impl BookBuilder {
  534. /// Create a new book builder with the given name.
  535. pub fn new(name: impl Into<String>) -> Self {
  536. Self {
  537. book: Book {
  538. id: BookId::generate(),
  539. name: name.into(),
  540. policy: BookPolicy {
  541. allowed_assets: Vec::new(),
  542. allowed_flags: AccountFlags::empty(),
  543. allowed_accounts: Vec::new(),
  544. },
  545. },
  546. }
  547. }
  548. /// Set the book id explicitly.
  549. pub fn id(mut self, id: BookId) -> Self {
  550. self.book.id = id;
  551. self
  552. }
  553. /// Add an allowed asset.
  554. pub fn allow_asset(mut self, asset: AssetId) -> Self {
  555. self.book.policy.allowed_assets.push(asset);
  556. self
  557. }
  558. /// Set allowed account flags — accounts with ANY of these flags may participate.
  559. pub fn allow_flags(mut self, flags: AccountFlags) -> Self {
  560. self.book.policy.allowed_flags = flags;
  561. self
  562. }
  563. /// Add a specific allowed account.
  564. pub fn allow_account(mut self, account: AccountId) -> Self {
  565. self.book.policy.allowed_accounts.push(account);
  566. self
  567. }
  568. /// Consume the builder and return the [`Book`].
  569. pub fn build(self) -> Book {
  570. self.book
  571. }
  572. }
  573. // ---------------------------------------------------------------------------
  574. // Posting
  575. // ---------------------------------------------------------------------------
  576. /// Lifecycle state of a [`Posting`].
  577. ///
  578. /// ```text
  579. /// Active ──reserve──▶ PendingInactive ──finalize──▶ Inactive (void)
  580. /// ▲ ▲ │
  581. /// │ └─── release (no-op) ┘
  582. /// └────── release ────────┘ (compensation)
  583. /// ```
  584. ///
  585. /// `reserve_postings` and `release_postings` are batch operations:
  586. /// - **reserve**: all postings must be Active, otherwise the batch fails.
  587. /// - **release**: Active is a no-op, PendingInactive reverts to Active,
  588. /// Inactive (void) fails the batch.
  589. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
  590. pub enum PostingStatus {
  591. /// Available for consumption and counted in balance.
  592. Active,
  593. /// Reserved for a transfer; not available for other consumption.
  594. /// Reverts to `Active` on compensation via `release_postings`.
  595. PendingInactive,
  596. /// Consumed by a committed transfer. Kept for audit trail (void).
  597. /// Cannot be released.
  598. Inactive,
  599. }
  600. /// A signed amount of one asset, owned by exactly one account.
  601. ///
  602. /// A positive posting is value controlled by the account; a negative posting is
  603. /// an offset position (issuance, external flow, overdraft, or system balancing).
  604. /// Negative postings are allowed on every policy except `NoOverdraft`.
  605. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
  606. pub struct Posting {
  607. /// Unique identifier derived from the creating transfer.
  608. pub id: PostingId,
  609. /// The account (subaccount) that owns this posting.
  610. pub owner: AccountId,
  611. /// The asset this posting denominates.
  612. pub asset: AssetId,
  613. /// Signed: positive = value controlled by the account, negative = offset position.
  614. pub value: Cent,
  615. /// Lifecycle state — only `Active` postings count toward balance.
  616. pub status: PostingStatus,
  617. /// Owner token while `PendingInactive`. `Some(rid)` iff reserved by saga
  618. /// `rid`; `None` when `Active` or `Inactive`. Only the holder of a matching
  619. /// `ReservationId` may finalize or release a reserved posting.
  620. pub reservation: Option<ReservationId>,
  621. }
  622. impl Posting {
  623. /// Construct an `Active`, unreserved posting.
  624. pub fn new(id: PostingId, owner: AccountId, asset: AssetId, value: Cent) -> Self {
  625. Self {
  626. id,
  627. owner,
  628. asset,
  629. value,
  630. status: PostingStatus::Active,
  631. reservation: None,
  632. }
  633. }
  634. /// Returns `true` if this posting's status is [`PostingStatus::Active`].
  635. pub fn is_active(&self) -> bool {
  636. self.status == PostingStatus::Active
  637. }
  638. }
  639. /// A posting to be created — carries no id yet because the [`PostingId`] depends
  640. /// on the [`EnvelopeId`], which is computed during validation.
  641. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
  642. pub struct NewPosting {
  643. /// The account (subaccount) that will own the created posting.
  644. pub owner: AccountId,
  645. /// The asset this posting denominates.
  646. pub asset: AssetId,
  647. /// Signed amount: positive = value controlled by the account, negative = offset position.
  648. pub value: Cent,
  649. /// Informational provenance — who funded this posting.
  650. pub payer: Option<AccountId>,
  651. }
  652. // ---------------------------------------------------------------------------
  653. // Transfer
  654. // ---------------------------------------------------------------------------
  655. /// Free-form key→value metadata.
  656. pub type Metadata = BTreeMap<String, Vec<u8>>;
  657. /// The unit of atomicity — all of its consumptions and creations apply together
  658. /// or not at all. This is the resolved, internal form produced by the saga
  659. /// pipeline from a [`Transfer`] intent.
  660. #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
  661. pub struct Envelope {
  662. /// Posting ids consumed (spent) by this envelope.
  663. pub consumes: Vec<PostingId>,
  664. /// New postings created by this envelope.
  665. pub creates: Vec<NewPosting>,
  666. /// Account version pins for optimistic concurrency.
  667. pub account_snapshots: Vec<AccountSnapshotId>,
  668. /// Book this envelope belongs to.
  669. pub book: BookId,
  670. /// Free-form key-value metadata.
  671. pub metadata: Metadata,
  672. }
  673. impl Envelope {
  674. /// Posting ids consumed (spent) by this envelope.
  675. pub fn consumes(&self) -> &[PostingId] {
  676. &self.consumes
  677. }
  678. /// New postings created by this envelope.
  679. pub fn creates(&self) -> &[NewPosting] {
  680. &self.creates
  681. }
  682. /// Account version pins for optimistic concurrency.
  683. pub fn account_snapshots(&self) -> &[AccountSnapshotId] {
  684. &self.account_snapshots
  685. }
  686. /// Book this envelope belongs to.
  687. pub fn book(&self) -> BookId {
  688. self.book
  689. }
  690. /// Free-form key-value metadata.
  691. pub fn metadata(&self) -> &Metadata {
  692. &self.metadata
  693. }
  694. /// Deduplicated, sorted list of account references in the created postings.
  695. pub fn referenced_accounts(&self) -> Vec<AccountId> {
  696. let mut ids: Vec<AccountId> = self.creates.iter().map(|p| p.owner).collect();
  697. ids.sort();
  698. ids.dedup();
  699. ids
  700. }
  701. /// Set account snapshots.
  702. pub fn set_account_snapshots(&mut self, snapshots: Vec<AccountSnapshotId>) {
  703. self.account_snapshots = snapshots;
  704. }
  705. }
  706. // ---------------------------------------------------------------------------
  707. // EnvelopeBuilder
  708. // ---------------------------------------------------------------------------
  709. /// Builder for constructing [`Envelope`] values.
  710. #[derive(Default)]
  711. pub struct EnvelopeBuilder {
  712. envelope: Envelope,
  713. }
  714. impl EnvelopeBuilder {
  715. /// Create an empty builder.
  716. pub fn new() -> Self {
  717. Self::default()
  718. }
  719. /// Set the posting ids to consume.
  720. pub fn consumes(mut self, ids: Vec<PostingId>) -> Self {
  721. self.envelope.consumes = ids;
  722. self
  723. }
  724. /// Set the new postings to create.
  725. pub fn creates(mut self, postings: Vec<NewPosting>) -> Self {
  726. self.envelope.creates = postings;
  727. self
  728. }
  729. /// Set the book.
  730. pub fn book(mut self, book: BookId) -> Self {
  731. self.envelope.book = book;
  732. self
  733. }
  734. /// Set the account version pins.
  735. pub fn account_snapshots(mut self, snapshots: Vec<AccountSnapshotId>) -> Self {
  736. self.envelope.account_snapshots = snapshots;
  737. self
  738. }
  739. /// Set the free-form metadata.
  740. pub fn metadata(mut self, metadata: Metadata) -> Self {
  741. self.envelope.metadata = metadata;
  742. self
  743. }
  744. /// Consume the builder and return the [`Envelope`].
  745. pub fn build(self) -> Envelope {
  746. self.envelope
  747. }
  748. }
  749. // ---------------------------------------------------------------------------
  750. // Account
  751. // ---------------------------------------------------------------------------
  752. /// Controls how much an account can spend beyond its posting-backed balance.
  753. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
  754. pub enum AccountPolicy {
  755. /// Balance must stay >= 0.
  756. NoOverdraft,
  757. /// Balance must stay >= `floor` (floor < 0).
  758. CappedOverdraft {
  759. /// Minimum allowed balance (must be negative).
  760. floor: Cent,
  761. },
  762. /// No floor — the account can go arbitrarily negative.
  763. UncappedOverdraft,
  764. /// Fees, settlement, market-making, minting. No balance constraints.
  765. SystemAccount,
  766. /// Boundary account representing value entering/leaving the ledger; holds
  767. /// the offset (negative) side of deposits.
  768. ExternalAccount,
  769. }
  770. bitflags::bitflags! {
  771. /// Lifecycle and user-defined flags for an [`Account`].
  772. ///
  773. /// Bits 0–7 are reserved for system flags. Bits 8–31 are available for
  774. /// user-defined flags, which can be used with [`BookPolicy::allowed_flags`]
  775. /// to scope which accounts may participate in a book.
  776. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
  777. pub struct AccountFlags: u32 {
  778. /// Account may not be the source or destination of any transfer.
  779. const FROZEN = 1 << 0;
  780. /// Terminal — no further activity.
  781. const CLOSED = 1 << 1;
  782. /// Holding account for an inflight (authorize/confirm/void) transaction.
  783. /// Parks funds between authorize and settlement; closed once drained.
  784. const INFLIGHT = 1 << 2;
  785. // Bits 3–7: reserved for future system flags.
  786. // Bits 8–31: user-defined.
  787. /// User-defined flag 0.
  788. const USER_0 = 1 << 8;
  789. /// User-defined flag 1.
  790. const USER_1 = 1 << 9;
  791. /// User-defined flag 2.
  792. const USER_2 = 1 << 10;
  793. /// User-defined flag 3.
  794. const USER_3 = 1 << 11;
  795. /// User-defined flag 4.
  796. const USER_4 = 1 << 12;
  797. /// User-defined flag 5.
  798. const USER_5 = 1 << 13;
  799. /// User-defined flag 6.
  800. const USER_6 = 1 << 14;
  801. /// User-defined flag 7.
  802. const USER_7 = 1 << 15;
  803. }
  804. }
  805. /// A registered entity that must exist before it can transact.
  806. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
  807. pub struct Account {
  808. /// Stable identity for this account (base account plus subaccount).
  809. pub id: AccountId,
  810. /// Monotonically increasing version, starts at 1 on creation.
  811. pub version: u64,
  812. /// Overdraft / balance policy.
  813. pub policy: AccountPolicy,
  814. /// Lifecycle flags (frozen, closed).
  815. pub flags: AccountFlags,
  816. /// Book this entity belongs to.
  817. pub book: BookId,
  818. /// Free-form key-value metadata.
  819. pub metadata: Metadata,
  820. }
  821. impl Account {
  822. /// Create a version-1 main-subaccount account with the given policy: no flags,
  823. /// the default book, and empty metadata. Convenience for the common case; set
  824. /// the other fields explicitly when you need them.
  825. pub fn new(id: AccountId, policy: AccountPolicy) -> Self {
  826. Self::new_ref(id, policy)
  827. }
  828. /// Like [`Account::new`] but for a specific subaccount reference.
  829. pub fn new_ref(id: AccountId, policy: AccountPolicy) -> Self {
  830. Self {
  831. id,
  832. version: 1,
  833. policy,
  834. flags: AccountFlags::empty(),
  835. book: DEFAULT_BOOK,
  836. metadata: Metadata::new(),
  837. }
  838. }
  839. /// Returns `true` if the account has the `FROZEN` flag set.
  840. pub fn is_frozen(&self) -> bool {
  841. self.flags.contains(AccountFlags::FROZEN)
  842. }
  843. /// Returns `true` if the account has the `CLOSED` flag set.
  844. pub fn is_closed(&self) -> bool {
  845. self.flags.contains(AccountFlags::CLOSED)
  846. }
  847. }
  848. // ---------------------------------------------------------------------------
  849. // Receipt
  850. // ---------------------------------------------------------------------------
  851. /// Confirmation of a committed transfer, carrying its content-addressed id.
  852. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
  853. pub struct Receipt {
  854. /// Content-addressed id of the committed transfer.
  855. pub transfer_id: EnvelopeId,
  856. }
  857. // ---------------------------------------------------------------------------
  858. // Transfer — intent-based API
  859. // ---------------------------------------------------------------------------
  860. /// A single movement within a transfer: move value from one account to another.
  861. ///
  862. /// Every operation (pay, deposit, withdraw) is expressed as one or more
  863. /// movements. The resolve step aggregates net debits per account and selects
  864. /// postings only for accounts with a positive net debit.
  865. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
  866. pub struct Movement {
  867. /// Account (subaccount) being debited.
  868. pub from: AccountId,
  869. /// Account (subaccount) being credited.
  870. pub to: AccountId,
  871. /// Asset to transfer.
  872. pub asset: AssetId,
  873. /// Amount to transfer (may be negative for offset postings).
  874. pub amount: Cent,
  875. }
  876. /// A transfer intent — one or more movements to execute atomically.
  877. ///
  878. /// The saga pipeline resolves movements into concrete postings ([`Envelope`])
  879. /// during execution. Callers express *what* should happen, not *which postings*
  880. /// to consume.
  881. #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
  882. pub struct Transfer {
  883. /// Movements to execute atomically.
  884. pub movements: Vec<Movement>,
  885. /// Book this entity belongs to.
  886. pub book: BookId,
  887. /// Free-form key-value metadata.
  888. pub metadata: Metadata,
  889. }
  890. /// Builder for constructing [`Transfer`] values.
  891. #[derive(Default)]
  892. pub struct TransferBuilder {
  893. transfer: Transfer,
  894. }
  895. impl TransferBuilder {
  896. /// Create an empty builder.
  897. pub fn new() -> Self {
  898. Self::default()
  899. }
  900. /// Add a raw movement between main subaccounts.
  901. pub fn movement(self, from: AccountId, to: AccountId, asset: AssetId, amount: Cent) -> Self {
  902. self.movement_ref(from, to, asset, amount)
  903. }
  904. /// Add a raw movement between specific subaccounts.
  905. pub fn movement_ref(
  906. mut self,
  907. from: AccountId,
  908. to: AccountId,
  909. asset: AssetId,
  910. amount: Cent,
  911. ) -> Self {
  912. self.transfer.movements.push(Movement {
  913. from,
  914. to,
  915. asset,
  916. amount,
  917. });
  918. self
  919. }
  920. /// Add a pay movement between main subaccounts.
  921. pub fn pay(self, from: AccountId, to: AccountId, asset: AssetId, amount: Cent) -> Self {
  922. self.movement(from, to, asset, amount)
  923. }
  924. /// Add a pay movement between two specific subaccounts. See
  925. /// [`movement_ref`](Self::movement_ref).
  926. pub fn pay_ref(self, from: AccountId, to: AccountId, asset: AssetId, amount: Cent) -> Self {
  927. self.movement_ref(from, to, asset, amount)
  928. }
  929. /// Add a deposit: creates an offset posting on the external account and
  930. /// credits the target account. Pushes two movements whose net debit on the
  931. /// external account is zero.
  932. pub fn deposit(
  933. self,
  934. to: AccountId,
  935. asset: AssetId,
  936. amount: Cent,
  937. external: AccountId,
  938. ) -> Result<Self, OverflowError> {
  939. let neg = amount.checked_neg()?;
  940. Ok(self
  941. .movement(external, external, asset, neg)
  942. .movement(external, to, asset, amount))
  943. }
  944. /// Add a withdrawal: move value from an account to an external destination.
  945. pub fn withdraw(
  946. self,
  947. from: AccountId,
  948. asset: AssetId,
  949. amount: Cent,
  950. external: AccountId,
  951. ) -> Self {
  952. self.movement(from, external, asset, amount)
  953. }
  954. /// Set the book.
  955. pub fn book(mut self, book: BookId) -> Self {
  956. self.transfer.book = book;
  957. self
  958. }
  959. /// Set the free-form metadata.
  960. pub fn metadata(mut self, metadata: Metadata) -> Self {
  961. self.transfer.metadata = metadata;
  962. self
  963. }
  964. /// Consume the builder and return the [`Transfer`].
  965. pub fn build(self) -> Transfer {
  966. self.transfer
  967. }
  968. }
  969. // ---------------------------------------------------------------------------
  970. // ToBytes implementations
  971. // ---------------------------------------------------------------------------
  972. impl ToBytes for AccountId {
  973. fn to_bytes(&self) -> Vec<u8> {
  974. // Base id then subaccount, both big-endian, so the subaccount is folded
  975. // into every content hash (envelope ids, posting ids, snapshots).
  976. let mut buf = Vec::with_capacity(16);
  977. buf.extend_from_slice(&self.id.to_be_bytes());
  978. buf.extend_from_slice(&self.sub.to_be_bytes());
  979. buf
  980. }
  981. }
  982. impl ToBytes for AccountSnapshotId {
  983. fn to_bytes(&self) -> Vec<u8> {
  984. let mut buf = Vec::with_capacity(48);
  985. buf.extend_from_slice(&self.account.to_bytes());
  986. buf.extend_from_slice(&self.snapshot_id);
  987. buf
  988. }
  989. }
  990. impl ToBytes for AssetId {
  991. fn to_bytes(&self) -> Vec<u8> {
  992. self.0.to_be_bytes().to_vec()
  993. }
  994. }
  995. impl ToBytes for EnvelopeId {
  996. fn to_bytes(&self) -> Vec<u8> {
  997. self.0.to_vec()
  998. }
  999. }
  1000. impl ToBytes for PostingId {
  1001. fn to_bytes(&self) -> Vec<u8> {
  1002. let mut buf = Vec::with_capacity(34);
  1003. buf.extend_from_slice(&self.transfer.0);
  1004. write_u16(&mut buf, self.index);
  1005. buf
  1006. }
  1007. }
  1008. impl ToBytes for AccountPolicy {
  1009. fn to_bytes(&self) -> Vec<u8> {
  1010. let mut buf = Vec::with_capacity(9);
  1011. match self {
  1012. Self::NoOverdraft => buf.push(0),
  1013. Self::CappedOverdraft { floor } => {
  1014. buf.push(1);
  1015. buf.extend(floor.to_bytes());
  1016. }
  1017. Self::UncappedOverdraft => buf.push(2),
  1018. Self::SystemAccount => buf.push(3),
  1019. Self::ExternalAccount => buf.push(4),
  1020. }
  1021. buf
  1022. }
  1023. }
  1024. impl ToBytes for AccountFlags {
  1025. fn to_bytes(&self) -> Vec<u8> {
  1026. self.bits().to_be_bytes().to_vec()
  1027. }
  1028. }
  1029. impl ToBytes for BookId {
  1030. fn to_bytes(&self) -> Vec<u8> {
  1031. self.0.to_be_bytes().to_vec()
  1032. }
  1033. }
  1034. impl ToBytes for NewPosting {
  1035. fn to_bytes(&self) -> Vec<u8> {
  1036. let mut buf = Vec::new();
  1037. buf.extend(self.owner.to_bytes());
  1038. buf.extend_from_slice(&self.asset.0.to_be_bytes());
  1039. buf.extend(self.value.to_bytes());
  1040. match &self.payer {
  1041. Some(p) => {
  1042. buf.push(1);
  1043. buf.extend(p.to_bytes());
  1044. }
  1045. None => buf.push(0),
  1046. }
  1047. buf
  1048. }
  1049. }
  1050. impl ToBytes for Posting {
  1051. fn to_bytes(&self) -> Vec<u8> {
  1052. let mut buf = Vec::new();
  1053. buf.extend(self.id.to_bytes());
  1054. buf.extend(self.owner.to_bytes());
  1055. buf.extend_from_slice(&self.asset.0.to_be_bytes());
  1056. buf.extend(self.value.to_bytes());
  1057. buf.push(match self.status {
  1058. PostingStatus::Active => 0,
  1059. PostingStatus::PendingInactive => 1,
  1060. PostingStatus::Inactive => 2,
  1061. });
  1062. buf
  1063. }
  1064. }
  1065. impl ToBytes for Envelope {
  1066. fn to_bytes(&self) -> Vec<u8> {
  1067. let mut buf = Vec::new();
  1068. buf.push(CANONICAL_VERSION);
  1069. write_u32(&mut buf, self.consumes.len() as u32);
  1070. for pid in &self.consumes {
  1071. buf.extend(pid.to_bytes());
  1072. }
  1073. write_u32(&mut buf, self.creates.len() as u32);
  1074. for np in &self.creates {
  1075. buf.extend(np.to_bytes());
  1076. }
  1077. write_u32(&mut buf, self.account_snapshots.len() as u32);
  1078. for snap in &self.account_snapshots {
  1079. buf.extend(snap.to_bytes());
  1080. }
  1081. buf.extend(self.book.to_bytes());
  1082. write_u32(&mut buf, self.metadata.len() as u32);
  1083. for (key, value) in &self.metadata {
  1084. let key_bytes = key.as_bytes();
  1085. write_u32(&mut buf, key_bytes.len() as u32);
  1086. buf.extend_from_slice(key_bytes);
  1087. write_u32(&mut buf, value.len() as u32);
  1088. buf.extend_from_slice(value);
  1089. }
  1090. buf
  1091. }
  1092. }
  1093. impl ToBytes for Account {
  1094. fn to_bytes(&self) -> Vec<u8> {
  1095. let mut buf = Vec::new();
  1096. buf.push(CANONICAL_VERSION);
  1097. buf.extend(self.id.to_bytes());
  1098. write_u64(&mut buf, self.version);
  1099. buf.extend(self.policy.to_bytes());
  1100. buf.extend(self.flags.to_bytes());
  1101. buf.extend(self.book.to_bytes());
  1102. write_u32(&mut buf, self.metadata.len() as u32);
  1103. for (key, value) in &self.metadata {
  1104. let key_bytes = key.as_bytes();
  1105. write_u32(&mut buf, key_bytes.len() as u32);
  1106. buf.extend_from_slice(key_bytes);
  1107. write_u32(&mut buf, value.len() as u32);
  1108. buf.extend_from_slice(value);
  1109. }
  1110. buf
  1111. }
  1112. }
  1113. impl ToBytes for Receipt {
  1114. fn to_bytes(&self) -> Vec<u8> {
  1115. self.transfer_id.0.to_vec()
  1116. }
  1117. }
  1118. #[cfg(test)]
  1119. mod account_id_tests {
  1120. use super::*;
  1121. use std::str::FromStr;
  1122. /// Largest subaccount id that round-trips through the string form.
  1123. const MAX_SUB: i64 = (1i64 << SUB_BITS) - 1;
  1124. #[test]
  1125. fn code_structure() {
  1126. let s = AccountId::with_sub(5, 7).to_string();
  1127. // Fixed 20 chars: an 18-char base-36 body then two check digits, no
  1128. // country code.
  1129. assert_eq!(s.len(), 20);
  1130. assert_eq!(s.len(), BODY_LEN + 2);
  1131. // The two trailing check digits are numeric.
  1132. assert!(s[BODY_LEN..].bytes().all(|b| b.is_ascii_digit()));
  1133. // The body is permuted, so it does NOT expose the raw legs the way an
  1134. // unmixed base-36 rendering (a run of zeros then "5"/"7") would.
  1135. assert_ne!(&s[..BODY_LEN], "000000000000000527");
  1136. }
  1137. #[test]
  1138. fn code_round_trips() {
  1139. for acc in [
  1140. AccountId::new(0),
  1141. AccountId::new(100),
  1142. AccountId::with_sub(5, 7),
  1143. AccountId::with_sub(987654321, 12345),
  1144. // The corners of the encodable domain: a full 63-bit id and the
  1145. // widest subaccount.
  1146. AccountId::new(i64::MAX),
  1147. AccountId::with_sub(i64::MAX, MAX_SUB),
  1148. AccountId::with_sub(0, MAX_SUB),
  1149. ] {
  1150. let s = acc.to_string();
  1151. assert_eq!(s.len(), 20, "length {s}");
  1152. assert_eq!(AccountId::from_str(&s).unwrap(), acc, "round-trip {s}");
  1153. }
  1154. }
  1155. #[test]
  1156. fn out_of_range_legs_truncate_to_the_encodable_domain() {
  1157. // The string form encodes only the low ID_BITS / SUB_BITS. A subaccount
  1158. // one past the encodable range wraps to 0 (documented narrowing), so it
  1159. // shares its code with the main account rather than corrupting it.
  1160. let over = AccountId::with_sub(7, 1i64 << SUB_BITS);
  1161. assert_eq!(
  1162. AccountId::from_str(&over.to_string()).unwrap(),
  1163. AccountId::new(7)
  1164. );
  1165. }
  1166. #[test]
  1167. fn parses_a_fixed_vector() {
  1168. // A hardcoded, checksum-valid code (under DEFAULT_ID_SEED) pins the
  1169. // exact encoding, permutation, and checksum, so an accidental change to
  1170. // any of them is caught by a failing parse.
  1171. let code = "KUJLQEL8IX2XGTBK4425";
  1172. let expected = AccountId::with_sub(987654321, 12345);
  1173. assert_eq!(AccountId::from_str(code).unwrap(), expected);
  1174. // The grouped (spaced, lower-cased) form parses to the same value.
  1175. assert_eq!(
  1176. AccountId::from_str("kujl qel8 ix2x gtbk 4425").unwrap(),
  1177. expected
  1178. );
  1179. // Display reproduces the exact machine form.
  1180. assert_eq!(expected.to_string(), code);
  1181. }
  1182. #[test]
  1183. fn obfuscation_permutation_is_invertible_across_seeds() {
  1184. for &seed in &[0u64, 1, DEFAULT_ID_SEED, u64::MAX] {
  1185. for &p in &[0u128, 1, 5 << SUB_BITS | 7, PACK_MASK, PACK_MASK - 1] {
  1186. let y = obfuscate(p, seed);
  1187. assert!(y <= PACK_MASK, "output escapes the domain: {y}");
  1188. assert_eq!(deobfuscate(y, seed), p, "seed={seed} p={p}");
  1189. }
  1190. }
  1191. }
  1192. #[test]
  1193. fn obfuscation_hides_structure() {
  1194. // The default seed is in force.
  1195. assert_eq!(id_seed(), DEFAULT_ID_SEED);
  1196. // Sequential base ids do not produce visibly related codes (avalanche).
  1197. let a = AccountId::new(100).to_string();
  1198. let b = AccountId::new(101).to_string();
  1199. let shared = a.bytes().zip(b.bytes()).take_while(|(x, y)| x == y).count();
  1200. assert!(shared < 4, "codes share too long a prefix: {a} vs {b}");
  1201. // A base account and its subaccount are likewise not obviously related.
  1202. let main = AccountId::new(100).to_string();
  1203. let sub = AccountId::with_sub(100, 1).to_string();
  1204. assert_ne!(main, sub);
  1205. }
  1206. #[test]
  1207. fn grouped_format_is_five_groups_of_four_and_re_parses() {
  1208. let acc = AccountId::with_sub(5, 7);
  1209. let grouped = acc.to_grouped();
  1210. let groups: Vec<&str> = grouped.split(' ').collect();
  1211. assert_eq!(groups.len(), 5);
  1212. assert!(groups.iter().all(|g| g.len() == 4));
  1213. // Grouped format (with spaces) and lower case both parse back.
  1214. assert_eq!(AccountId::from_str(&grouped).unwrap(), acc);
  1215. assert_eq!(AccountId::from_str(&grouped.to_lowercase()).unwrap(), acc);
  1216. }
  1217. #[test]
  1218. fn parses_with_spaces_or_dashes_for_url_safety() {
  1219. let acc = AccountId::with_sub(987654321, 12345);
  1220. let machine = acc.to_string(); // 20 chars, no separators (URL-safe)
  1221. // The same code grouped with spaces (display) or dashes (URL-safe
  1222. // separator) parses back to the same id, as does a mixed/irregular form.
  1223. let spaced = acc.to_grouped();
  1224. let dashed = spaced.replace(' ', "-");
  1225. let mixed = format!("{}-{} {}", &machine[0..4], &machine[4..12], &machine[12..]);
  1226. for s in [&machine, &spaced, &dashed, &mixed] {
  1227. assert_eq!(AccountId::from_str(s).unwrap(), acc, "parse {s}");
  1228. }
  1229. }
  1230. #[test]
  1231. fn from_str_rejects_bad_checksum_and_junk() {
  1232. let good = AccountId::with_sub(5, 7).to_string();
  1233. assert!(AccountId::from_str(&good).is_ok());
  1234. // A helper to overwrite one character while keeping the length.
  1235. let with_char_at = |i: usize, c: char| {
  1236. let mut v: Vec<char> = good.chars().collect();
  1237. v[i] = c;
  1238. v.into_iter().collect::<String>()
  1239. };
  1240. // Flip the last check digit: still numeric and right length, but the
  1241. // checksum no longer matches, so it is rejected.
  1242. let last = good.len() - 1;
  1243. let flipped = with_char_at(last, if good.ends_with('8') { '9' } else { '8' });
  1244. assert!(AccountId::from_str(&flipped).is_err(), "bad checksum");
  1245. // Structurally malformed inputs are all rejected.
  1246. assert!(AccountId::from_str("").is_err(), "empty");
  1247. assert!(AccountId::from_str("not-a-code").is_err(), "junk");
  1248. assert!(AccountId::from_str(&good[..19]).is_err(), "too short");
  1249. assert!(
  1250. AccountId::from_str(&format!("{good}0")).is_err(),
  1251. "too long"
  1252. );
  1253. // A check digit that is not a digit (the last two positions).
  1254. assert!(
  1255. AccountId::from_str(&with_char_at(BODY_LEN, 'A')).is_err(),
  1256. "alpha check"
  1257. );
  1258. // A non-base-36 character in the body (survives space/dash stripping).
  1259. assert!(
  1260. AccountId::from_str(&with_char_at(5, '*')).is_err(),
  1261. "non-base36 body"
  1262. );
  1263. }
  1264. }