1use std::cmp::Ordering;
25use std::hash::{Hash, Hasher};
26
27use aluvm::alu::regs::Status;
28use aluvm::alu::{CoreConfig, CoreExt, Lib, LibId, LibSite, Vm};
29use aluvm::{fe256, GfaConfig, RegE};
30use amplify::confinement::{SmallVec, TinyOrdMap, TinyString};
31use amplify::num::u256;
32use amplify::Bytes32;
33use commit_verify::{CommitId, CommitmentId, DigestExt, ReservedBytes, Sha256};
34
35use crate::{
36 CellAddr, ContractId, Identity, Instr, Operation, StateCell, StateValue, VerifiedOperation,
37 VmContext, LIB_NAME_ULTRASONIC,
38};
39
40pub type CallId = u16;
42
43#[derive(Clone, Eq, Debug)]
49#[derive(CommitEncode)]
50#[commit_encode(strategy = strict, id = CodexId)]
51#[derive(StrictType, StrictDumb, StrictEncode, StrictDecode)]
52#[strict_type(lib = LIB_NAME_ULTRASONIC)]
53#[cfg_attr(feature = "serde", derive(Serialize, Deserialize), serde(rename_all = "camelCase"))]
54pub struct Codex {
55 pub version: ReservedBytes<1>,
74 pub name: TinyString,
76 pub developer: Identity,
78 pub timestamp: i64,
85 pub features: ReservedBytes<4>,
89 pub field_order: u256,
92 pub verification_config: CoreConfig,
94 pub input_config: CoreConfig,
103 pub verifiers: TinyOrdMap<CallId, LibSite>,
105}
106
107impl PartialOrd for Codex {
108 fn partial_cmp(&self, other: &Self) -> Option<Ordering> { Some(self.cmp(other)) }
109}
110impl Ord for Codex {
111 fn cmp(&self, other: &Self) -> Ordering { self.commit_id().cmp(&other.commit_id()) }
112}
113impl PartialEq for Codex {
114 fn eq(&self, other: &Self) -> bool { self.commit_id() == other.commit_id() }
115}
116impl Hash for Codex {
117 fn hash<H: Hasher>(&self, state: &mut H) { state.write(&self.commit_id().to_byte_array()); }
118}
119
120impl Codex {
121 pub fn codex_id(&self) -> CodexId { self.commit_id() }
129
130 pub fn verify(
162 &self,
163 contract_id: ContractId,
164 operation: Operation,
165 memory: &impl Memory,
166 repo: &impl LibRepo,
167 ) -> Result<VerifiedOperation, CallError> {
168 let resolver = |lib_id: LibId| {
169 let lib = repo.get_lib(lib_id)?;
170 if lib.lib_id() != lib_id {
172 panic!(
173 "The library returned by the `LibRepo` provided for the contract operation \
174 verification doesn't match the requested library id. This error indicates \
175 that the software using the consensus verification is invalid or compromised."
176 )
177 }
178 Some(lib)
179 };
180
181 if operation.contract_id != contract_id {
182 return Err(CallError::WrongContract {
183 expected: contract_id,
184 found: operation.contract_id,
185 });
186 }
187
188 let mut vm_inputs = Vm::<Instr<LibId>>::with(self.input_config, GfaConfig {
190 field_order: self.field_order,
191 });
192 let len = operation.destructible_in.len();
193 let mut destructible_inputs = SmallVec::with_capacity(len);
194 for input in &operation.destructible_in {
195 let cell = memory
197 .destructible(input.addr)
198 .ok_or(CallError::NoReadOnceInput(input.addr))?;
199
200 let _res = destructible_inputs.push((*input, cell));
203 debug_assert!(_res.is_ok());
204 }
205
206 let len = operation.immutable_in.len();
208 let mut immutable_inputs = SmallVec::with_capacity(len);
209 for addr in &operation.immutable_in {
210 let data = memory
211 .immutable(*addr)
212 .ok_or(CallError::NoImmutableInput(*addr))?;
213 let _res = immutable_inputs.push(data);
216 debug_assert!(_res.is_ok());
217 }
218
219 let entry_point = self
221 .verifiers
222 .get(&operation.call_id)
223 .ok_or(CallError::NotFound(operation.call_id))?;
224 let context = VmContext {
225 witness: operation.witness,
226 destructible_input: destructible_inputs.as_slice(),
227 immutable_input: immutable_inputs.as_slice(),
228 destructible_output: operation.destructible_out.as_slice(),
229 immutable_output: operation.immutable_out.as_slice(),
230 };
231 let mut vm_main = Vm::<Instr<LibId>>::with(self.verification_config, GfaConfig {
232 field_order: self.field_order,
233 });
234 if vm_main.exec(*entry_point, &context, resolver) == Status::Fail {
235 if let Some(err_code) = vm_main.core.cx.get(RegE::E1) {
236 return Err(CallError::Script(err_code));
237 } else {
238 return Err(CallError::ScriptUnspecified);
239 }
240 }
241
242 for (input_no, (_, cell)) in context.destructible_input.iter().enumerate() {
247 if let Some(lock) = cell.lock.and_then(|l| l.script) {
251 vm_inputs.core.cx.set_inro_index(input_no as u16);
253
254 if vm_inputs.exec(lock, &context, resolver) == Status::Fail {
255 return Err(CallError::Lock(vm_inputs.core.cx.get(RegE::E8)));
257 }
258 vm_inputs.reset();
259 }
260 }
261
262 Ok(VerifiedOperation::new_unchecked(operation.opid(), operation))
263 }
264}
265
266pub trait Memory {
270 fn destructible(&self, addr: CellAddr) -> Option<StateCell>;
273 fn immutable(&self, addr: CellAddr) -> Option<StateValue>;
276}
277
278pub trait LibRepo {
281 fn get_lib(&self, lib_id: LibId) -> Option<&Lib>;
287}
288
289#[derive(Copy, Clone, Eq, PartialEq, Debug, Display, Error)]
294#[display(doc_comments)]
295pub enum CallError {
296 #[cfg_attr(
298 feature = "baid64",
299 display = "operation doesn't belong to the current contract {expected} (operation \
300 contract is {found})."
301 )]
302 #[cfg_attr(
303 not(feature = "baid64"),
304 display = "operation doesn't belong to the current contract."
305 )]
306 WrongContract {
307 expected: ContractId,
309 found: ContractId,
311 },
312
313 NotFound(CallId),
315
316 #[cfg_attr(
318 feature = "baid64",
319 display = "operation references destructible memory cell {0} which was not defined."
320 )]
321 #[cfg_attr(
322 not(feature = "baid64"),
323 display = "operation references destructible memory cell {0:?} which was not defined."
324 )]
325 NoReadOnceInput(CellAddr),
326
327 #[cfg_attr(
329 feature = "baid64",
330 display = "operation references immutable memory cell {0} which was not defined."
331 )]
332 #[cfg_attr(
333 not(feature = "baid64"),
334 display = "operation references immutable memory cell {0:?} which was not defined."
335 )]
336 NoImmutableInput(CellAddr),
338
339 Lock(Option<fe256>),
341
342 Script(fe256),
344
345 ScriptUnspecified,
347}
348
349#[derive(Wrapper, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Debug, From)]
351#[wrapper(AsSlice, Deref, BorrowSlice, Hex, Index, RangeOps)]
352#[derive(StrictType, StrictDumb, StrictEncode, StrictDecode)]
353#[strict_type(lib = LIB_NAME_ULTRASONIC)]
354#[cfg_attr(
355 all(feature = "serde", not(feature = "baid64")),
356 derive(Serialize, Deserialize),
357 serde(transparent)
358)]
359pub struct CodexId(
360 #[from]
361 #[from([u8; 32])]
362 Bytes32,
363);
364
365#[cfg(all(feature = "serde", feature = "baid64"))]
366impl_serde_str_bin_wrapper!(CodexId, Bytes32);
367
368impl From<Sha256> for CodexId {
369 fn from(hasher: Sha256) -> Self { hasher.finish().into() }
370}
371
372impl CommitmentId for CodexId {
373 const TAG: &'static str = "urn:ubideco:sonic:codex#2025-05-15";
374}
375
376#[cfg(feature = "baid64")]
377mod _baid4 {
378 use core::fmt::{self, Display, Formatter};
379 use core::str::FromStr;
380
381 use baid64::{Baid64ParseError, DisplayBaid64, FromBaid64Str};
382
383 use super::*;
384
385 impl DisplayBaid64 for CodexId {
386 const HRI: &'static str = "codex";
387 const CHUNKING: bool = true;
388 const PREFIX: bool = false;
389 const EMBED_CHECKSUM: bool = false;
390 const MNEMONIC: bool = true;
391 fn to_baid64_payload(&self) -> [u8; 32] { self.to_byte_array() }
392 }
393 impl FromBaid64Str for CodexId {}
394 impl FromStr for CodexId {
395 type Err = Baid64ParseError;
396 fn from_str(s: &str) -> Result<Self, Self::Err> { Self::from_baid64_str(s) }
397 }
398 impl Display for CodexId {
399 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { self.fmt_baid64(f) }
400 }
401}
402
403#[cfg(test)]
404mod test {
405 #![cfg_attr(coverage_nightly, coverage(off))]
406
407 use core::str::FromStr;
408 use std::collections::HashMap;
409
410 use aluvm::alu::aluasm;
411 use aluvm::{zk_aluasm, FIELD_ORDER_SECP};
412 use amplify::ByteArray;
413 use commit_verify::Digest;
414 use strict_encoding::StrictDumb;
415
416 use super::*;
417 use crate::{uasm, AuthToken, CellLock, Input};
418
419 #[test]
420 fn codex_id_display() {
421 let id = CodexId::from_byte_array(Sha256::digest(b"test"));
422 assert_eq!(
423 format!("{id}"),
424 "n4bQgYhM-fWWaL_q-gxVrQFa-O~TxsrC-4Is0V1s-FbDwCgg#berlin-river-delta"
425 );
426 assert_eq!(
427 format!("{id:-}"),
428 "codex:n4bQgYhM-fWWaL_q-gxVrQFa-O~TxsrC-4Is0V1s-FbDwCgg#berlin-river-delta"
429 );
430 assert_eq!(format!("{id:#}"), "n4bQgYhM-fWWaL_q-gxVrQFa-O~TxsrC-4Is0V1s-FbDwCgg");
431 }
432
433 #[test]
434 fn codex_id_from_str() {
435 let id = CodexId::from_byte_array(Sha256::digest(b"test"));
436 assert_eq!(
437 CodexId::from_str(
438 "n4bQgYhM-fWWaL_q-gxVrQFa-O~TxsrC-4Is0V1s-FbDwCgg#berlin-river-delta"
439 )
440 .unwrap(),
441 id
442 );
443 assert_eq!(
444 CodexId::from_str(
445 "codex:n4bQgYhM-fWWaL_q-gxVrQFa-O~TxsrC-4Is0V1s-FbDwCgg#berlin-river-delta"
446 )
447 .unwrap(),
448 id
449 );
450 assert_eq!(
451 CodexId::from_str("codex:n4bQgYhM-fWWaL_q-gxVrQFa-O~TxsrC-4Is0V1s-FbDwCgg").unwrap(),
452 id
453 );
454 assert_eq!(
455 CodexId::from_str("n4bQgYhM-fWWaL_q-gxVrQFa-O~TxsrC-4Is0V1s-FbDwCgg").unwrap(),
456 id
457 );
458 }
459
460 #[derive(Clone, Eq, PartialEq, Debug, Default)]
461 pub struct DumbMemory {
462 pub destructible: HashMap<CellAddr, StateCell>,
463 pub immutable: HashMap<CellAddr, StateValue>,
464 }
465
466 impl Memory for DumbMemory {
467 fn destructible(&self, addr: CellAddr) -> Option<StateCell> {
468 self.destructible.get(&addr).copied()
469 }
470
471 fn immutable(&self, addr: CellAddr) -> Option<StateValue> {
472 self.immutable.get(&addr).copied()
473 }
474 }
475
476 impl LibRepo for Lib {
477 fn get_lib(&self, lib_id: LibId) -> Option<&Lib> {
478 if lib_id == self.lib_id() {
479 Some(self)
480 } else {
481 None
482 }
483 }
484 }
485
486 fn lib_success() -> Lib { Lib::assemble(&aluasm! { stop; }).unwrap() }
487 fn lib_failure_none() -> Lib {
488 Lib::assemble(&zk_aluasm! {
489 clr E1;
490 test E2;
491 chk CO;
492 })
493 .unwrap()
494 }
495 fn lib_failure_one() -> Lib {
496 Lib::assemble(&zk_aluasm! {
497 put E1, 1;
498 test E2;
499 chk CO;
500 })
501 .unwrap()
502 }
503 const SECRET: u8 = 48;
504 fn lib_lock() -> Lib {
505 assert_eq!(SECRET, 48);
506 Lib::assemble(&uasm! {
507 stop;
508 put E1, 48; ldi auth;
511 eq EA, E1; put E8, 1; chk CO;
514 put E2, 1; eq EB, E2; chk CO;
517
518 ldi witness;
519 eq EA, E1; put E8, 2; chk CO;
522
523 put E8, 3; test EB; not CO;
526 chk CO;
527 test EC; not CO;
529 chk CO;
530 test ED;
531 not CO;
532 chk CO;
533 })
534 .unwrap()
535 }
536
537 fn test_stand(modify: impl FnOnce(&mut Codex, &mut Operation, &mut DumbMemory)) {
538 test_stand_script(lib_success(), modify)
539 }
540
541 fn test_stand_script(
542 repo: Lib,
543 modify: impl FnOnce(&mut Codex, &mut Operation, &mut DumbMemory),
544 ) {
545 test_stand_repo(repo.lib_id(), repo, modify)
546 }
547
548 fn test_stand_repo(
549 lib_id: LibId,
550 repo: impl LibRepo,
551 modify: impl FnOnce(&mut Codex, &mut Operation, &mut DumbMemory),
552 ) {
553 let mut codex = Codex::strict_dumb();
554 codex.field_order = FIELD_ORDER_SECP;
555 codex.verification_config = CoreConfig { halt: true, complexity_lim: Some(10_000_000) };
556 codex.input_config = CoreConfig { halt: true, complexity_lim: Some(10_000_000) };
557 codex.verifiers = tiny_bmap! { 0 => LibSite::new(lib_id, 0) };
558
559 let contract_id = ContractId::from_byte_array(Sha256::digest(b"test"));
560 let mut operation = Operation::strict_dumb();
561 operation.contract_id = contract_id;
562 operation.call_id = 0;
563 let mut memory = DumbMemory::default();
564
565 modify(&mut codex, &mut operation, &mut memory);
566
567 codex
568 .verify(contract_id, operation, &memory, &repo)
569 .unwrap();
570 }
571
572 #[test]
573 fn verify_dumb() { test_stand(|_codex, _operation, _memory| {}); }
574
575 #[test]
576 #[should_panic(
577 expected = "WrongContract { expected: ContractId(Array<32>(9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08)), found: ContractId(Array<32>(8810ad581e59f2bc3928b261707a71308f7e139eb04820366dc4d5c18d980225))"
578 )]
579 fn verify_contract_id() {
580 test_stand(|_codex, operation, _memory| {
581 operation.contract_id = ContractId::from_byte_array(Sha256::digest(b"wrong"));
582 });
583 }
584
585 #[test]
586 #[should_panic(
587 expected = "NoImmutableInput(CellAddr { opid: Opid(Array<32>(0000000000000000000000000000000000000000000000000000000000000000)), pos: 0 })"
588 )]
589 fn verify_no_immutable() {
590 test_stand(|_codex, operation, _memory| {
591 operation.immutable_in = small_vec![CellAddr::strict_dumb()];
592 });
593 }
594
595 #[test]
596 #[should_panic(
597 expected = " NoReadOnceInput(CellAddr { opid: Opid(Array<32>(0000000000000000000000000000000000000000000000000000000000000000)), pos: 0 })"
598 )]
599 fn verify_no_destructible() {
600 test_stand(|_codex, operation, _memory| {
601 operation.destructible_in =
602 small_vec![Input { addr: CellAddr::strict_dumb(), witness: none!() }];
603 });
604 }
605
606 #[test]
607 fn verify_immutable() {
608 test_stand(|_codex, operation, memory| {
609 let addr = CellAddr::strict_dumb();
610 memory.immutable.insert(addr, StateValue::strict_dumb());
611 operation.immutable_in = small_vec![addr];
612 });
613 }
614
615 #[test]
616 fn verify_destructible() {
617 test_stand(|_codex, operation, memory| {
618 let addr = CellAddr::strict_dumb();
619 memory.destructible.insert(addr, StateCell::strict_dumb());
620 operation.destructible_in = small_vec![Input { addr, witness: none!() }];
621 });
622 }
623
624 #[test]
625 fn verify_protected_dumb() {
626 test_stand_script(lib_lock(), |_codex, operation, memory| {
627 let addr = CellAddr::strict_dumb();
628 memory.destructible.insert(addr, StateCell {
629 data: StateValue::None,
630 auth: AuthToken::strict_dumb(),
631 lock: Some(CellLock::with_script(lib_lock().lib_id(), 0)),
632 });
633 operation.destructible_in = small_vec![Input { addr, witness: none!() }];
634 });
635 }
636
637 #[test]
638 fn verify_protected() {
639 test_stand_script(lib_lock(), |_codex, operation, memory| {
640 let addr = CellAddr::strict_dumb();
641 memory.destructible.insert(addr, StateCell {
642 data: StateValue::None,
643 auth: AuthToken::from(fe256::from(SECRET)),
644 lock: Some(CellLock::with_script(lib_lock().lib_id(), 1)),
645 });
646 operation.destructible_in = small_vec![Input {
647 addr,
648 witness: StateValue::Single { first: fe256::from(SECRET) }
649 }];
650 });
651 }
652
653 #[test]
654 #[should_panic(
655 expected = "Lock(Some(fe256(0x0000000000000000000000000000000000000000000000000000000000000001)))"
656 )]
657 fn verify_protected_failure1() {
658 test_stand_script(lib_lock(), |_codex, operation, memory| {
659 let addr = CellAddr::strict_dumb();
660 memory.destructible.insert(addr, StateCell {
661 data: StateValue::None,
662 auth: AuthToken::strict_dumb(),
663 lock: Some(CellLock::with_script(lib_lock().lib_id(), 1)),
664 });
665 operation.destructible_in = small_vec![Input {
666 addr,
667 witness: StateValue::Single { first: fe256::from(SECRET) }
668 }];
669 });
670 }
671
672 #[test]
673 #[should_panic(
674 expected = "Lock(Some(fe256(0x0000000000000000000000000000000000000000000000000000000000000002)))"
675 )]
676 fn verify_protected_failure2() {
677 test_stand_script(lib_lock(), |_codex, operation, memory| {
678 let addr = CellAddr::strict_dumb();
679 memory.destructible.insert(addr, StateCell {
680 data: StateValue::None,
681 auth: AuthToken::from(fe256::from(SECRET)),
682 lock: Some(CellLock::with_script(lib_lock().lib_id(), 1)),
683 });
684 operation.destructible_in = small_vec![Input { addr, witness: StateValue::None }];
685 });
686 }
687
688 #[test]
689 #[should_panic(
690 expected = "Lock(Some(fe256(0x0000000000000000000000000000000000000000000000000000000000000003)))"
691 )]
692 fn verify_protected_failure3() {
693 test_stand_script(lib_lock(), |_codex, operation, memory| {
694 let addr = CellAddr::strict_dumb();
695 memory.destructible.insert(addr, StateCell {
696 data: StateValue::None,
697 auth: AuthToken::from(fe256::from(SECRET)),
698 lock: Some(CellLock::with_script(lib_lock().lib_id(), 1)),
699 });
700 operation.destructible_in = small_vec![Input {
701 addr,
702 witness: StateValue::Double {
703 first: fe256::from(SECRET),
704 second: fe256::from(SECRET)
705 }
706 }];
707 });
708 }
709
710 #[test]
711 #[should_panic(expected = "ScriptUnspecified")]
712 fn verify_script_failure_unspecified() {
713 test_stand_script(lib_failure_none(), |_codex, _operation, _memory| {});
714 }
715
716 #[test]
717 #[should_panic(
718 expected = "Script(fe256(0x0000000000000000000000000000000000000000000000000000000000000001))"
719 )]
720 fn verify_script_failure_code() {
721 test_stand_script(lib_failure_one(), |_codex, _operation, _memory| {});
722 }
723
724 #[test]
725 #[should_panic(expected = "NotFound(0)")]
726 fn verify_no_verifier() {
727 test_stand_script(lib_success(), |codex, _operation, _memory| {
728 codex.verifiers.clear();
729 });
730 }
731
732 #[test]
733 #[should_panic(expected = "ScriptUnspecified")]
734 fn verify_lib_absent() {
735 test_stand_script(lib_success(), |codex, _operation, _memory| {
736 codex
737 .verifiers
738 .insert(0, LibSite::new(lib_failure_one().lib_id(), 0))
739 .unwrap();
740 });
741 }
742
743 #[test]
744 #[should_panic(expected = "ScriptUnspecified")]
745 fn verify_lib_wrong_pos() {
746 test_stand_script(lib_success(), |codex, _operation, _memory| {
747 codex
748 .verifiers
749 .insert(0, LibSite::new(lib_success().lib_id(), 1))
750 .unwrap();
751 });
752 }
753
754 #[test]
755 #[should_panic(expected = "The library returned by the `LibRepo` provided for the contract \
756 operation verification doesn't match the requested library id. \
757 This error indicates that the software using the consensus \
758 verification is invalid or compromised.")]
759 fn verify_wrong_lib_id() {
760 struct InvalidRepo(Lib);
761 impl LibRepo for InvalidRepo {
762 fn get_lib(&self, _lib_id: LibId) -> Option<&Lib> { Some(&self.0) }
763 }
764 let repo = InvalidRepo(lib_failure_one());
765 test_stand_repo(lib_success().lib_id(), repo, |_codex, _operation, _memory| {});
766 }
767}