1use alloc::collections::{BTreeMap, BTreeSet};
26use alloc::vec::Vec;
27use core::error::Error;
28use core::fmt::{Debug, Formatter};
29
30use amplify::confinement::SmallOrdMap;
31use amplify::ByteArray;
32use single_use_seals::{PublishedWitness, SealError, SealWitness};
33use ultrasonic::{
34 AuthToken, CallError, CellAddr, Codex, ContractId, LibRepo, Memory, Operation, Opid, VerifiedOperation,
35};
36
37use crate::{RgbSeal, RgbSealDef, LIB_NAME_RGB};
38
39#[derive(StrictType, StrictDumb, StrictEncode, StrictDecode)]
44#[strict_type(lib = LIB_NAME_RGB)]
45#[cfg_attr(
46 feature = "serde",
47 derive(Serialize, Deserialize),
48 serde(
49 rename_all = "camelCase",
50 bound = "Seal::Definition: serde::Serialize + for<'d> serde::Deserialize<'d>, Seal::PubWitness: \
51 serde::Serialize + for<'d> serde::Deserialize<'d>, Seal::CliWitness: serde::Serialize + for<'d> \
52 serde::Deserialize<'d>"
53 )
54)]
55pub struct OperationSeals<Seal: RgbSeal> {
56 pub operation: Operation,
58 pub defined_seals: SmallOrdMap<u16, Seal::Definition>,
60 pub witness: Option<SealWitness<Seal>>,
62}
63
64impl<Seal: RgbSeal> Clone for OperationSeals<Seal>
65where
66 Seal::PubWitness: Clone,
67 Seal::CliWitness: Clone,
68{
69 fn clone(&self) -> Self {
70 Self {
71 operation: self.operation.clone(),
72 defined_seals: self.defined_seals.clone(),
73 witness: self.witness.clone(),
74 }
75 }
76}
77
78pub trait ReadOperation: Sized {
80 type Seal: RgbSeal;
82
83 fn read_operation(&mut self) -> Result<Option<OperationSeals<Self::Seal>>, impl Error + 'static>;
86}
87
88pub trait ContractApi<Seal: RgbSeal> {
93 fn contract_id(&self) -> ContractId;
97
98 fn codex(&self) -> &Codex;
102
103 fn repo(&self) -> &impl LibRepo;
107
108 fn memory(&self) -> &impl Memory;
111
112 fn is_known(&self, opid: Opid) -> bool;
117
118 fn apply_operation(&mut self, op: VerifiedOperation);
125
126 fn apply_seals(&mut self, opid: Opid, seals: SmallOrdMap<u16, Seal::Definition>);
132
133 fn apply_witness(&mut self, opid: Opid, witness: SealWitness<Seal>);
138}
139
140pub trait ContractVerify<Seal: RgbSeal>: ContractApi<Seal> {
149 fn evaluate<R: ReadOperation<Seal = Seal>>(&mut self, mut reader: R) -> Result<(), VerificationError<Seal>> {
152 let contract_id = self.contract_id();
153 let codex_id = self.codex().codex_id();
154
155 let mut is_genesis = true;
156 let mut seals = BTreeMap::<CellAddr, Seal>::new();
157
158 while let Some(mut block) = reader
159 .read_operation()
160 .map_err(|e| VerificationError::Stream(Box::new(e)))?
161 {
162 if is_genesis {
165 if block.operation.contract_id.to_byte_array() != codex_id.to_byte_array() {
166 return Err(VerificationError::NoCodexCommitment);
167 }
168 block.operation.contract_id = contract_id;
169 }
170 let opid = block.operation.opid();
171
172 let defined = block
174 .operation
175 .destructible_out
176 .iter()
177 .map(|cell| cell.auth)
178 .collect::<BTreeSet<_>>();
179 let reported = block
180 .defined_seals
181 .values()
182 .map(|seal| seal.auth_token())
183 .collect::<BTreeSet<_>>();
184 if !reported.is_subset(&defined) {
187 let sources = block
188 .defined_seals
189 .iter()
190 .map(|(pos, seal)| (*pos, seal.to_string()))
191 .collect();
192 return Err(VerificationError::SealsDefinitionMismatch { opid, reported, defined, sources });
193 }
194
195 let mut closed_seals = Vec::<Seal>::new();
197 for input in &block.operation.destructible_in {
198 let seal = seals
199 .remove(&input.addr)
200 .ok_or(VerificationError::SealUnknown(input.addr))?;
201 closed_seals.push(seal);
202 }
203
204 let operation = if self.is_known(opid) {
207 None
208 } else {
209 let verified = self
211 .codex()
212 .verify(contract_id, block.operation, self.memory(), self.repo())?;
213 Some(verified)
214 };
215
216 let mut seal_sources: BTreeSet<_> = block
220 .defined_seals
221 .iter()
222 .filter_map(|(pos, seal)| seal.to_src().map(|seal| (CellAddr::new(opid, *pos), seal)))
223 .collect();
224
225 if let Some(witness) = block.witness {
226 let msg = opid.to_byte_array();
227 witness
228 .verify_seals_closing(&closed_seals, msg.into())
229 .map_err(|e| VerificationError::SealsNotClosed(witness.published.pub_id(), opid, e))?;
230
231 let pub_id = witness.published.pub_id();
233 let iter = block
234 .defined_seals
235 .iter()
236 .filter(|(_, seal)| seal.to_src().is_none())
237 .map(|(pos, seal)| (CellAddr::new(opid, *pos), seal.resolve(pub_id)));
238 seal_sources.extend(iter);
239
240 self.apply_witness(opid, witness);
241 } else if !closed_seals.is_empty() {
242 return Err(VerificationError::NoWitness(opid));
243 }
244
245 seals.extend(seal_sources);
246 if is_genesis {
247 is_genesis = false
248 } else if let Some(operation) = operation {
249 self.apply_operation(operation);
250 }
251
252 if !block.defined_seals.is_empty() {
253 self.apply_seals(opid, block.defined_seals);
254 }
255 }
256
257 Ok(())
258 }
259}
260
261impl<Seal: RgbSeal, C: ContractApi<Seal>> ContractVerify<Seal> for C {}
262
263#[derive(Display, Error, From)]
265#[display(doc_comments)]
266pub enum VerificationError<Seal: RgbSeal> {
267 Stream(Box<dyn Error>),
271
272 NoCodexCommitment,
274
275 NoWitness(Opid),
277
278 SealsNotClosed(<Seal::PubWitness as PublishedWitness<Seal>>::PubId, Opid, SealError<Seal>),
282
283 SealUnknown(CellAddr),
285
286 #[allow(missing_docs)]
295 SealsDefinitionMismatch {
296 opid: Opid,
297 reported: BTreeSet<AuthToken>,
298 defined: BTreeSet<AuthToken>,
299 sources: BTreeMap<u16, String>,
300 },
301
302 #[from]
304 #[display(inner)]
305 Vm(CallError),
306}
307
308impl<Seal: RgbSeal> Debug for VerificationError<Seal> {
310 fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { write!(f, "{self}") }
311}
312
313#[cfg(test)]
314mod test {
315 #![cfg_attr(coverage_nightly, coverage(off))]
316
317 use std::collections::HashMap;
318 use std::convert::Infallible;
319 use std::vec;
320
321 use bp::seals::{TxoSeal, TxoSealExt, WOutpoint, WTxoSeal};
322 use bp::{Outpoint, Sats, ScriptPubkey, SeqNo, Tx, TxIn, TxOut, Vout};
323 use strict_encoding::StrictDumb;
324 use ultrasonic::aluvm::alu::{aluasm, CoreConfig, Lib, LibId, LibSite};
325 use ultrasonic::aluvm::FIELD_ORDER_SECP;
326 use ultrasonic::{fe256, CodexId, Genesis, Identity, Input, StateCell, StateData, StateValue};
327
328 use super::*;
329
330 #[derive(Clone)]
331 struct TestReader(vec::IntoIter<OperationSeals<TxoSeal>>);
332 impl ReadOperation for TestReader {
333 type Seal = TxoSeal;
334 fn read_operation(&mut self) -> Result<Option<OperationSeals<Self::Seal>>, impl Error + 'static> {
335 Result::<_, Infallible>::Ok(self.0.next())
336 }
337 }
338 impl TestReader {
339 pub fn new(vec: Vec<OperationSeals<TxoSeal>>) -> Self { Self(vec.into_iter()) }
340 }
341
342 struct TestContract {
343 pub codex: Codex,
344 pub contract_id: ContractId,
345 pub libs: HashMap<LibId, Lib>,
346 pub global: HashMap<CellAddr, StateValue>,
347 pub owned: HashMap<CellAddr, StateCell>,
348 pub known_ops: BTreeMap<Opid, Operation>,
349 pub seal_definitions: BTreeMap<Opid, HashMap<u16, WTxoSeal>>,
350 pub witnesses: BTreeMap<Opid, Vec<SealWitness<TxoSeal>>>,
351 }
352 impl Memory for TestContract {
353 fn destructible(&self, addr: CellAddr) -> Option<StateCell> { self.owned.get(&addr).cloned() }
354 fn immutable(&self, addr: CellAddr) -> Option<StateValue> { self.global.get(&addr).cloned() }
355 }
356 impl LibRepo for TestContract {
357 fn get_lib(&self, lib_id: LibId) -> Option<&Lib> { self.libs.get(&lib_id) }
358 }
359 impl ContractApi<TxoSeal> for TestContract {
360 fn contract_id(&self) -> ContractId { self.contract_id }
361 fn codex(&self) -> &Codex { &self.codex }
362 fn repo(&self) -> &impl LibRepo { self }
363 fn memory(&self) -> &impl Memory { self }
364 fn is_known(&self, opid: Opid) -> bool { self.known_ops.contains_key(&opid) }
365 fn apply_operation(&mut self, op: VerifiedOperation) {
366 let opid = op.opid();
367 let op = op.into_operation();
368 for (no, inp) in op.immutable_out.iter().enumerate() {
369 self.global
370 .insert(CellAddr::new(opid, no as u16), inp.value);
371 }
372 for (no, inp) in op.destructible_out.iter().enumerate() {
373 self.owned.insert(CellAddr::new(opid, no as u16), *inp);
374 }
375 self.known_ops.insert(opid, op);
376 }
377 fn apply_seals(&mut self, opid: Opid, seals: SmallOrdMap<u16, WTxoSeal>) {
378 self.seal_definitions.entry(opid).or_default().extend(seals);
379 }
380 fn apply_witness(&mut self, opid: Opid, witness: SealWitness<TxoSeal>) {
381 self.witnesses.entry(opid).or_default().push(witness);
382 }
383 }
384
385 fn lib() -> Lib {
386 let code = aluasm! {
387 stop;
388 };
389 Lib::assemble(&code).unwrap()
390 }
391
392 fn codex() -> Codex {
393 let lib_id = lib().lib_id();
394 Codex {
395 name: tiny_s!("TestCodex"),
396 developer: Identity::default(),
397 version: default!(),
398 features: default!(),
399 timestamp: 1732529307,
400 field_order: FIELD_ORDER_SECP,
401 input_config: CoreConfig::default(),
402 verification_config: CoreConfig::default(),
403 verifiers: tiny_bmap! {
404 0 => LibSite::new(lib_id, 0),
405 },
406 }
407 }
408
409 const SEAL_WOUT: WTxoSeal = WTxoSeal {
410 primary: WOutpoint::Wout(Vout::from_u32(0)),
411 secondary: TxoSealExt::Fallback(Outpoint::coinbase()),
412 };
413
414 const SEAL_1: WTxoSeal = WTxoSeal {
415 primary: WOutpoint::Extern(Outpoint::coinbase()),
416 secondary: TxoSealExt::Fallback(Outpoint::coinbase()),
417 };
418
419 fn genesis() -> Genesis {
420 let mut genesis = Genesis::strict_dumb();
421 genesis.codex_id = codex().codex_id();
422 genesis.immutable_out = small_vec![StateData::new(0u64, 1000u64)];
423 genesis.destructible_out = small_vec![StateCell {
424 data: StateValue::None,
425 auth: SEAL_1.auth_token(),
426 lock: None
427 }];
428 genesis
429 }
430
431 fn contract() -> TestContract {
432 let lib = lib();
433 let lib_id = lib.lib_id();
434 let genesis = genesis();
435 let genesis_op = genesis.to_operation(ContractId::strict_dumb());
436 let genesis_opid = genesis_op.opid();
437 TestContract {
438 codex: codex(),
439 contract_id: ContractId::strict_dumb(),
440 libs: map! { lib_id => lib },
441 global: none!(),
442 owned: map! { CellAddr::new(genesis_opid, 0) => genesis_op.destructible_out[0] },
443 known_ops: bmap! { genesis_opid => genesis_op },
444 seal_definitions: bmap! { genesis_opid => none!() },
445 witnesses: bmap! { genesis_opid => none!() },
446 }
447 }
448
449 fn operation() -> Operation {
450 let genesis = genesis();
451 let contract = contract();
452 let genesis_op = genesis.to_operation(contract.contract_id);
453 let genesis_opid = genesis_op.opid();
454 Operation {
455 version: default!(),
456 contract_id: contract.contract_id,
457 call_id: 0,
458 nonce: fe256::ZERO,
459 witness: StateValue::None,
460 destructible_in: small_vec![Input {
461 addr: CellAddr::new(genesis_opid, 0),
462 witness: StateValue::None
463 }],
464 immutable_in: Default::default(),
465 destructible_out: Default::default(),
466 immutable_out: Default::default(),
467 }
468 }
469
470 #[allow(clippy::result_large_err)]
471 fn run(reader: TestReader) -> Result<(), VerificationError<TxoSeal>> {
472 let mut contract = contract();
473 contract.evaluate(reader.clone())?;
474
475 let mut ops = bmap! {};
477 let mut seals = bmap! {};
478 let mut witnesses = bmap! {};
479 for entry in reader.0 {
480 let opid = entry.operation.opid();
481 ops.insert(opid, entry.operation);
482 seals.insert(opid, entry.defined_seals.into_iter().collect());
483 witnesses.insert(opid, entry.witness.into_iter().collect());
484 }
485
486 ops.pop_first();
487 let (genesis_opid, genesis_op) = contract.known_ops.first_key_value().unwrap();
488 ops.insert(*genesis_opid, genesis_op.clone());
489 seals.pop_first();
490 let (genesis_opid, definitions) = contract.seal_definitions.first_key_value().unwrap();
491 seals.insert(*genesis_opid, definitions.clone());
492 witnesses.pop_first();
493 let (genesis_opid, definitions) = contract.witnesses.first_key_value().unwrap();
494 witnesses.insert(*genesis_opid, definitions.clone());
495
496 assert_eq!(ops, contract.known_ops);
497 assert_eq!(seals, contract.seal_definitions);
498 assert_eq!(witnesses, contract.witnesses);
499 Ok(())
500 }
501
502 #[test]
503 fn empty() {
504 let reader = TestReader::new(vec![]);
505 run(reader).unwrap();
506 }
507
508 #[test]
509 fn genesis_only() {
510 let genesis = genesis();
511 let genesis_op = genesis.to_operation(genesis.codex_id.to_byte_array().into());
512
513 let reader =
514 TestReader::new(vec![OperationSeals { operation: genesis_op, defined_seals: none!(), witness: None }]);
515 run(reader).unwrap();
516 }
517
518 #[test]
519 #[should_panic(expected = "genesis does not commit to the codex id; a wrong contract genesis is used.")]
520 fn invalid_genesis() {
521 let mut genesis = genesis();
522 genesis.codex_id = CodexId::from_byte_array([0xAD; 32]);
523 let genesis_op = genesis.to_operation(genesis.codex_id.to_byte_array().into());
524
525 let reader =
526 TestReader::new(vec![OperationSeals { operation: genesis_op, defined_seals: none!(), witness: None }]);
527 run(reader).unwrap();
528 }
529
530 #[test]
531 #[should_panic(expected = "seals, reported to be defined by the operation \
532 k7fHvPyBlnM8m1n0QUaqNhB0I8kTwWXmi7nB_ZjTGVc, do match the assignments in the \
533 operation.
534Actual operation seals from the assignments: {
535 AuthToken(
536 fe256(
537 0x0000141b74832b85ca7bc7e2899cc3e5617a29ac4340f09b105524a6f62bd597,
538 ),
539 ),
540}
541Reported seals: {
542 AuthToken(
543 fe256(
544 0x000046c31ad97975e90e4ab2ee247f0e2f39ec8461823023e977cc14bcda14f5,
545 ),
546 ),
547}
548Sources for the reported seals: {
549 0: \"~:0/00000000000000000000000000000000000000000000000000000000000000000000000000000000\",
550}")]
551 fn invalid_seals() {
552 let genesis = genesis();
553 let genesis_op = genesis.to_operation(genesis.codex_id.to_byte_array().into());
554
555 let reader = TestReader::new(vec![OperationSeals {
556 operation: genesis_op,
557 defined_seals: small_bmap! { 0 => WTxoSeal::strict_dumb() },
558 witness: None,
559 }]);
560 run(reader).unwrap();
561 }
562
563 #[test]
564 #[should_panic(expected = "unknown seal definition for cell address k7fHvPyBlnM8m1n0QUaqNhB0I8kTwWXmi7nB_ZjTGVc:0.")]
565 fn seal_unknown() {
566 let genesis = genesis();
567 let genesis_op = genesis.to_operation(genesis.codex_id.to_byte_array().into());
568 let operation = operation();
569
570 let reader = TestReader::new(vec![
571 OperationSeals { operation: genesis_op, defined_seals: none!(), witness: None },
572 OperationSeals { operation, defined_seals: none!(), witness: None },
573 ]);
574 run(reader).unwrap();
575 }
576
577 #[test]
578 #[should_panic(expected = "unknown seal definition for cell address k7fHvPyBlnM8m1n0QUaqNhB0I8kTwWXmi7nB_ZjTGVc:0.")]
579 fn genesis_with_wout() {
580 let mut genesis = genesis();
581 genesis.destructible_out[0].auth = SEAL_WOUT.auth_token();
582 let genesis_op = genesis.to_operation(genesis.codex_id.to_byte_array().into());
583 let operation = operation();
584
585 let reader = TestReader::new(vec![
586 OperationSeals {
587 operation: genesis_op,
588 defined_seals: small_bmap! { 0 => SEAL_WOUT},
589 witness: None,
590 },
591 OperationSeals { operation, defined_seals: none!(), witness: None },
592 ]);
593 run(reader).unwrap();
594 }
595
596 #[test]
597 #[should_panic(expected = "no witness known for the operation KAPb7ikgqk_ofCp1fZWm_T6XfKjuCwNf9BlZfPpoR0E.")]
598 fn no_witness() {
599 let genesis = genesis();
600 let genesis_op = genesis.to_operation(genesis.codex_id.to_byte_array().into());
601 let operation = operation();
602
603 let reader = TestReader::new(vec![
604 OperationSeals {
605 operation: genesis_op,
606 defined_seals: small_bmap! { 0 => SEAL_1 },
607 witness: None,
608 },
609 OperationSeals { operation, defined_seals: none!(), witness: None },
610 ]);
611 run(reader).unwrap();
612 }
613
614 #[test]
615 #[should_panic(expected = "single-use seals are not closed properly with witness \
616 4ebd325a4b394cff8c57e8317ccf5a8d0e2bdf1b8526f8aad6c8e43d8240621a for operation \
617 KAPb7ikgqk_ofCp1fZWm_T6XfKjuCwNf9BlZfPpoR0E.
618Details: seal \
619 0000000000000000000000000000000000000000000000000000000000000000:0/\
620 0000000000000000000000000000000000000000000000000000000000000000:0 is not included in \
621 the public witness 4ebd325a4b394cff8c57e8317ccf5a8d0e2bdf1b8526f8aad6c8e43d8240621a")]
622 fn seals_unclosed() {
623 let genesis = genesis();
624 let genesis_op = genesis.to_operation(genesis.codex_id.to_byte_array().into());
625 let operation = operation();
626
627 let reader = TestReader::new(vec![
628 OperationSeals {
629 operation: genesis_op,
630 defined_seals: small_bmap! { 0 => SEAL_1 },
631 witness: None,
632 },
633 OperationSeals {
634 operation,
635 defined_seals: none!(),
636 witness: Some(SealWitness::new(strict_dumb!(), strict_dumb!())),
637 },
638 ]);
639 run(reader).unwrap();
640 }
641
642 #[test]
643 #[should_panic(expected = "ingle-use seals are not closed properly with witness \
644 1692606e775129a6733b6dc48ec7f5771f8e30d8c5304c0949d36efad2411812 for operation \
645 KAPb7ikgqk_ofCp1fZWm_T6XfKjuCwNf9BlZfPpoR0E.
646Details: seal \
647 0000000000000000000000000000000000000000000000000000000000000000:0/\
648 0000000000000000000000000000000000000000000000000000000000000000:0 is not included in \
649 the public witness 1692606e775129a6733b6dc48ec7f5771f8e30d8c5304c0949d36efad2411812")]
650 fn not_spending_utxo() {
651 let genesis = genesis();
652 let genesis_op = genesis.to_operation(genesis.codex_id.to_byte_array().into());
653 let operation = operation();
654
655 let mut witness = Tx::strict_dumb();
656 witness
657 .outputs
658 .push(TxOut {
659 value: Sats::ZERO,
660 script_pubkey: ScriptPubkey::op_return(&[]),
661 })
662 .unwrap();
663
664 let reader = TestReader::new(vec![
665 OperationSeals {
666 operation: genesis_op,
667 defined_seals: small_bmap! { 0 => SEAL_1 },
668 witness: None,
669 },
670 OperationSeals {
671 operation,
672 defined_seals: none!(),
673 witness: Some(SealWitness::new(witness, strict_dumb!())),
674 },
675 ]);
676 run(reader).unwrap();
677 }
678
679 #[test]
680 #[should_panic(expected = "single-use seals are not closed properly with witness \
681 0520b790b442e9c023e2ea0e0e284fbe60086d64f01037082f19464b44f9642e for operation \
682 KAPb7ikgqk_ofCp1fZWm_T6XfKjuCwNf9BlZfPpoR0E.
683Details: seal \
684 0000000000000000000000000000000000000000000000000000000000000000:0/\
685 0000000000000000000000000000000000000000000000000000000000000000:0 is not included in \
686 the public witness 0520b790b442e9c023e2ea0e0e284fbe60086d64f01037082f19464b44f9642e")]
687 fn missing_commitment() {
688 let genesis = genesis();
689 let genesis_op = genesis.to_operation(genesis.codex_id.to_byte_array().into());
690 let operation = operation();
691
692 let mut witness = Tx::strict_dumb();
693 witness
694 .inputs
695 .push(TxIn {
696 prev_output: Outpoint::coinbase(),
697 sig_script: none!(),
698 sequence: SeqNo::from_consensus_u32(0),
699 witness: none!(),
700 })
701 .unwrap();
702 witness
703 .outputs
704 .push(TxOut {
705 value: Sats::ZERO,
706 script_pubkey: ScriptPubkey::op_return(&[]),
707 })
708 .unwrap();
709
710 let reader = TestReader::new(vec![
711 OperationSeals {
712 operation: genesis_op,
713 defined_seals: small_bmap! { 0 => SEAL_1 },
714 witness: None,
715 },
716 OperationSeals {
717 operation,
718 defined_seals: none!(),
719 witness: Some(SealWitness::new(witness, strict_dumb!())),
720 },
721 ]);
722 run(reader).unwrap();
723 }
724}