1pub mod argent;
44pub mod disasm;
45pub mod kcc1;
46pub mod kcc0020;
47pub mod kcc20;
48pub mod observed;
49pub mod report;
50pub mod vesting;
51
52use disasm::{disassemble, Instruction, OpGroup};
53use std::ops::Range;
54
55#[derive(Clone, Debug, serde::Serialize)]
57pub struct Field {
58 pub name: &'static str,
59 #[serde(serialize_with = "hex_ser")]
60 pub value: Vec<u8>,
61}
62
63fn hex_ser<S: serde::Serializer>(bytes: &[u8], s: S) -> Result<S::Ok, S::Error> {
64 s.serialize_str(&hex::encode(bytes))
65}
66
67#[derive(Clone, Debug, serde::Serialize)]
69pub struct Decoded {
70 pub decoder: &'static str,
72 pub instructions: Vec<Instruction>,
73 pub truncated: bool,
74 pub pushes: Vec<Vec<u8>>,
76 pub uses_covenant_ops: bool,
77 pub uses_zk_ops: bool,
78 pub zk_system: Option<&'static str>,
82 pub template: Option<&'static str>,
84 #[serde(skip_serializing_if = "Option::is_none")]
90 pub generation: Option<&'static str>,
91 pub fields: Vec<Field>,
93}
94
95pub trait StateDecoder: Send + Sync {
96 fn name(&self) -> &'static str;
97 fn decode(&self, spk_version: u16, script: &[u8]) -> Option<Decoded>;
99}
100
101pub struct DisasmDecoder;
103
104fn base_decode(name: &'static str, script: &[u8]) -> Decoded {
105 let (instructions, truncated) = disassemble(script);
106 Decoded {
107 decoder: name,
108 pushes: instructions.iter().filter_map(|i| i.data.clone()).collect(),
109 uses_covenant_ops: instructions.iter().any(|i| i.group == OpGroup::Covenant),
110 uses_zk_ops: instructions.iter().any(|i| i.group == OpGroup::Zk),
111 zk_system: zk_system_from(&instructions),
112 instructions,
113 truncated,
114 template: None,
115 generation: None,
116 fields: vec![],
117 }
118}
119
120pub fn zk_system(script: &[u8]) -> Option<&'static str> {
140 let (instructions, _) = disassemble(script);
141 zk_system_from(&instructions)
142}
143
144fn zk_system_from(instructions: &[Instruction]) -> Option<&'static str> {
145 if !instructions.iter().any(|i| i.group == OpGroup::Zk) {
147 return None;
148 }
149 let sizes: Vec<usize> = instructions
150 .iter()
151 .filter_map(|i| i.data.as_ref().map(|d| d.len()))
152 .collect();
153 if sizes.iter().any(|&n| n >= 1024) {
155 return Some("risc0");
156 }
157 if sizes.iter().any(|&n| (128..=256).contains(&n)) {
161 return Some("groth16");
162 }
163 if sizes.iter().any(|&n| (300..1024).contains(&n)) {
166 return Some("succinct proof (inferred)");
167 }
168 None
169}
170
171impl StateDecoder for DisasmDecoder {
172 fn name(&self) -> &'static str {
173 "disasm"
174 }
175 fn decode(&self, _spk_version: u16, script: &[u8]) -> Option<Decoded> {
176 Some(base_decode(self.name(), script))
177 }
178}
179
180pub struct P2pkStateDecoder;
183
184impl StateDecoder for P2pkStateDecoder {
185 fn name(&self) -> &'static str {
186 "p2pk-state"
187 }
188 fn decode(&self, _spk_version: u16, script: &[u8]) -> Option<Decoded> {
189 let ok = matches!(script.len(), 34 | 35)
190 && script[0] as usize == script.len() - 2
191 && script[script.len() - 1] == 0xac;
192 if !ok {
193 return None;
194 }
195 let mut d = base_decode(self.name(), script);
196 d.template = Some("p2pk state");
197 d.fields = vec![Field {
198 name: "owner_pubkey",
199 value: script[1..script.len() - 1].to_vec(),
200 }];
201 Some(d)
202 }
203}
204
205pub struct P2shCommitmentDecoder;
208
209impl StateDecoder for P2shCommitmentDecoder {
210 fn name(&self) -> &'static str {
211 "p2sh-commitment"
212 }
213 fn decode(&self, _spk_version: u16, script: &[u8]) -> Option<Decoded> {
214 let hash = p2sh_hash(script)?.to_vec();
215 let mut d = base_decode(self.name(), script);
216 d.template = Some("p2sh commitment");
217 d.fields = vec![Field {
218 name: "program_hash",
219 value: hash,
220 }];
221 Some(d)
222 }
223}
224
225enum SkelItem {
231 Op(u8),
233 ConstPush { value: Vec<u8>, raw: Vec<u8> },
237 Slot(&'static str),
239}
240
241pub fn encode_push(value: &[u8]) -> Vec<u8> {
245 match value {
246 [] => vec![0x00], [0x81] => vec![0x4f], [v] if (1..=16).contains(v) => vec![0x50 + v], _ if value.len() <= 75 => {
250 let mut out = Vec::with_capacity(value.len() + 1);
251 out.push(value.len() as u8);
252 out.extend_from_slice(value);
253 out
254 }
255 _ if value.len() <= 0xff => {
256 let mut out = vec![0x4c, value.len() as u8];
257 out.extend_from_slice(value);
258 out
259 }
260 _ if value.len() <= 0xffff => {
261 let mut out = vec![0x4d, (value.len() & 0xff) as u8, (value.len() >> 8) as u8];
262 out.extend_from_slice(value);
263 out
264 }
265 _ => {
266 let n = value.len() as u32;
267 let mut out = vec![
268 0x4e,
269 (n & 0xff) as u8,
270 (n >> 8 & 0xff) as u8,
271 (n >> 16 & 0xff) as u8,
272 (n >> 24) as u8,
273 ];
274 out.extend_from_slice(value);
275 out
276 }
277 }
278}
279
280pub struct Skeleton {
281 pub name: &'static str,
282 pub generation: Option<&'static str>,
287 items: Vec<SkelItem>,
288 param_order: Vec<&'static str>,
290}
291
292fn push_value(inst: &Instruction) -> Option<Vec<u8>> {
295 if let Some(data) = &inst.data {
296 return Some(data.clone());
297 }
298 match inst.opcode {
299 0x00 => Some(vec![]),
300 0x4f => Some(vec![0x81]),
301 0x51..=0x60 => Some(vec![inst.opcode - 0x50]),
302 _ => None,
303 }
304}
305
306fn is_push(inst: &Instruction) -> bool {
307 inst.group == OpGroup::Push
308}
309
310impl Skeleton {
311 pub fn derive(
317 name: &'static str,
318 a: &[u8],
319 b: &[u8],
320 sentinels: &[(&'static str, Vec<u8>)],
321 ) -> Option<Skeleton> {
322 let (ia, ta) = disassemble(a);
323 let (ib, tb) = disassemble(b);
324 if ta || tb || ia.len() != ib.len() {
325 return None;
326 }
327 let mut items = Vec::with_capacity(ia.len());
328 for (i, (x, y)) in ia.iter().zip(&ib).enumerate() {
329 match (is_push(x), is_push(y)) {
330 (false, false) => {
331 if x.opcode != y.opcode {
332 return None;
333 }
334 items.push(SkelItem::Op(x.opcode));
335 }
336 (true, true) => {
337 let vx = push_value(x)?;
338 let vy = push_value(y)?;
339 if vx == vy {
340 let end = ia.get(i + 1).map_or(a.len(), |n| n.offset);
342 items.push(SkelItem::ConstPush {
343 value: vx,
344 raw: a[x.offset..end].to_vec(),
345 });
346 } else {
347 let (label, _) = sentinels.iter().find(|(_, s)| *s == vx)?;
348 items.push(SkelItem::Slot(label));
349 }
350 }
351 _ => return None,
352 }
353 }
354 Some(Skeleton {
355 name,
356 generation: None,
357 items,
358 param_order: sentinels.iter().map(|(l, _)| *l).collect(),
359 })
360 }
361
362 pub fn with_generation(mut self, generation: &'static str) -> Self {
366 self.generation = Some(generation);
367 self
368 }
369
370 pub fn derive_observed(
380 name: &'static str,
381 instances: &[&[u8]],
382 labels: &[&'static str],
383 ) -> Option<Skeleton> {
384 if instances.len() < 2 {
385 return None;
386 }
387 let mut streams = Vec::with_capacity(instances.len());
388 for bytes in instances {
389 let (insts, truncated) = disassemble(bytes);
390 if truncated
391 || streams
392 .first()
393 .is_some_and(|f: &Vec<Instruction>| f.len() != insts.len())
394 {
395 return None;
396 }
397 streams.push(insts);
398 }
399 let first = &streams[0];
400 let mut items = Vec::with_capacity(first.len());
401 let mut slots: Vec<Vec<Vec<u8>>> = Vec::new();
403 for (i, inst) in first.iter().enumerate() {
404 if !is_push(inst) {
405 if streams
406 .iter()
407 .any(|s| is_push(&s[i]) || s[i].opcode != inst.opcode)
408 {
409 return None;
410 }
411 items.push(SkelItem::Op(inst.opcode));
412 continue;
413 }
414 let vector = streams
415 .iter()
416 .map(|s| push_value(&s[i]))
417 .collect::<Option<Vec<_>>>()?;
418 if vector.iter().all(|v| *v == vector[0]) {
419 let end = first.get(i + 1).map_or(instances[0].len(), |n| n.offset);
420 items.push(SkelItem::ConstPush {
421 value: vector[0].clone(),
422 raw: instances[0][inst.offset..end].to_vec(),
423 });
424 } else {
425 let slot = match slots.iter().position(|s| *s == vector) {
426 Some(idx) => idx,
427 None => {
428 slots.push(vector);
429 slots.len() - 1
430 }
431 };
432 items.push(SkelItem::Slot(labels.get(slot).copied()?));
433 }
434 }
435 if slots.len() != labels.len() {
438 return None;
439 }
440 Some(Skeleton {
441 name,
442 generation: None,
443 items,
444 param_order: labels.to_vec(),
445 })
446 }
447
448 pub fn params(&self) -> &[&'static str] {
450 &self.param_order
451 }
452
453 pub fn emit(&self, args: &[(&str, &[u8])]) -> Option<Vec<u8>> {
459 let mut out = Vec::new();
460 for item in &self.items {
461 match item {
462 SkelItem::Op(op) => out.push(*op),
463 SkelItem::ConstPush { raw, .. } => out.extend_from_slice(raw),
464 SkelItem::Slot(label) => {
465 let (_, value) = args.iter().find(|(l, _)| l == label)?;
466 out.extend_from_slice(&encode_push(value));
467 }
468 }
469 }
470 Some(out)
471 }
472
473 pub fn slot_ranges(&self, program: &[u8]) -> Option<Vec<(&'static str, Range<usize>)>> {
485 let (instructions, truncated) = disassemble(program);
486 if truncated {
487 return None;
488 }
489 self.match_script(&instructions)?;
490 let mut out = Vec::new();
491 for (item, inst) in self.items.iter().zip(&instructions) {
492 let SkelItem::Slot(label) = item else { continue };
493 let data = inst.data.as_ref()?;
494 let prefix = match inst.opcode {
496 0x01..=0x4b => 1,
497 0x4c => 2,
498 0x4d => 3,
499 0x4e => 5,
500 _ => return None, };
502 let start = inst.offset + prefix;
503 out.push((*label, start..start + data.len()));
504 }
505 Some(out)
506 }
507
508 fn match_script(&self, instructions: &[Instruction]) -> Option<Vec<Field>> {
509 if instructions.len() != self.items.len() {
510 return None;
511 }
512 let mut values: Vec<(&'static str, Vec<u8>)> = Vec::new();
513 for (item, inst) in self.items.iter().zip(instructions) {
514 if !match_skel_item(item, inst, &mut values) {
515 return None;
516 }
517 }
518 Some(fields_in_order(&self.param_order, &values))
519 }
520}
521
522fn match_skel_item(
526 item: &SkelItem,
527 inst: &Instruction,
528 values: &mut Vec<(&'static str, Vec<u8>)>,
529) -> bool {
530 match item {
531 SkelItem::Op(op) => !is_push(inst) && inst.opcode == *op,
532 SkelItem::ConstPush { value, .. } => push_value(inst).as_ref() == Some(value),
533 SkelItem::Slot(label) => {
534 let Some(v) = push_value(inst) else {
535 return false;
536 };
537 match values.iter().find(|(l, _)| l == label) {
538 Some((_, prev)) => *prev == v,
539 None => {
540 values.push((label, v));
541 true
542 }
543 }
544 }
545 }
546}
547
548fn fields_in_order(order: &[&'static str], values: &[(&'static str, Vec<u8>)]) -> Vec<Field> {
549 order
550 .iter()
551 .filter_map(|label| {
552 values.iter().find(|(l, _)| l == label).map(|(_, v)| Field {
553 name: label,
554 value: v.clone(),
555 })
556 })
557 .collect()
558}
559
560fn same_shape(a: &Instruction, b: &Instruction) -> bool {
564 match (push_value(a), push_value(b)) {
565 (Some(x), Some(y)) => x.len() == y.len(),
566 (None, None) => a.opcode == b.opcode,
567 _ => false,
568 }
569}
570
571pub struct RepeatSkeleton {
577 pub name: &'static str,
578 prefix: Vec<SkelItem>,
579 group: Vec<SkelItem>,
580 suffix: Vec<SkelItem>,
581 min_repeats: usize,
582 param_order: Vec<&'static str>,
583 group_params: Vec<&'static str>,
584}
585
586impl RepeatSkeleton {
587 pub fn derive(
598 name: &'static str,
599 long: &[&[u8]],
600 short: &[&[u8]],
601 labels: &[&'static str],
602 group_labels: &[&'static str],
603 ) -> Option<RepeatSkeleton> {
604 if long.len() < 2 || short.is_empty() {
605 return None;
606 }
607 let parse = |set: &[&[u8]]| -> Option<Vec<Vec<Instruction>>> {
608 let mut streams = Vec::with_capacity(set.len());
609 for bytes in set {
610 let (insts, truncated) = disassemble(bytes);
611 if truncated
612 || streams
613 .first()
614 .is_some_and(|f: &Vec<Instruction>| f.len() != insts.len())
615 {
616 return None;
617 }
618 streams.push(insts);
619 }
620 Some(streams)
621 };
622 let la = parse(long)?;
623 let lb = parse(short)?;
624 let (a0, b0) = (&la[0], &lb[0]);
625 let g = a0.len().checked_sub(b0.len()).filter(|g| *g > 0)?;
626 let mut p = 0;
629 while p < b0.len() && same_shape(&a0[p], &b0[p]) {
630 p += 1;
631 }
632 let mut s = 0;
633 while s < b0.len() - p && same_shape(&a0[a0.len() - 1 - s], &b0[b0.len() - 1 - s]) {
634 s += 1;
635 }
636 if a0.len() - p - s < g {
637 return None;
638 }
639 p = a0.len() - s - g; while p > 0 && same_shape(&a0[p - 1], &a0[p + g - 1]) {
641 p -= 1; }
643 let extra = b0.len().checked_sub(p + s)?;
644 if extra % g != 0 {
645 return None;
646 }
647 let min_repeats = extra / g;
648
649 let mut slots: Vec<Vec<Vec<u8>>> = Vec::new();
652 let mut build =
653 |positions: &mut dyn Iterator<Item = (usize, usize)>| -> Option<Vec<SkelItem>> {
654 let mut items = Vec::new();
655 for (ia, ib) in positions {
656 let inst = &a0[ia];
657 if !is_push(inst) {
658 let ok = la
659 .iter()
660 .all(|x| !is_push(&x[ia]) && x[ia].opcode == inst.opcode)
661 && lb
662 .iter()
663 .all(|x| !is_push(&x[ib]) && x[ib].opcode == inst.opcode);
664 if !ok {
665 return None;
666 }
667 items.push(SkelItem::Op(inst.opcode));
668 continue;
669 }
670 let vector = la
671 .iter()
672 .map(|x| push_value(&x[ia]))
673 .chain(lb.iter().map(|x| push_value(&x[ib])))
674 .collect::<Option<Vec<_>>>()?;
675 if vector.iter().all(|v| *v == vector[0]) {
676 let end = a0.get(ia + 1).map_or(long[0].len(), |n| n.offset);
677 items.push(SkelItem::ConstPush {
678 value: vector[0].clone(),
679 raw: long[0][inst.offset..end].to_vec(),
680 });
681 } else {
682 let slot = match slots.iter().position(|x| *x == vector) {
683 Some(idx) => idx,
684 None => {
685 slots.push(vector);
686 slots.len() - 1
687 }
688 };
689 items.push(SkelItem::Slot(labels.get(slot).copied()?));
690 }
691 }
692 Some(items)
693 };
694 let prefix = build(&mut (0..p).map(|i| (i, i)))?;
695 let suffix = build(&mut (0..s).map(|j| (a0.len() - s + j, b0.len() - s + j)))?;
696 if slots.len() != labels.len() {
697 return None;
698 }
699
700 let mut group = Vec::with_capacity(g);
702 let mut pushes = 0;
703 for inst in &a0[p..p + g] {
704 if is_push(inst) {
705 group.push(SkelItem::Slot(group_labels.get(pushes)?));
706 pushes += 1;
707 } else {
708 group.push(SkelItem::Op(inst.opcode));
709 }
710 }
711 if pushes != group_labels.len() {
712 return None;
713 }
714 let mut group_params: Vec<&'static str> = Vec::new();
715 for label in group_labels {
716 if !group_params.contains(label) {
717 group_params.push(label);
718 }
719 }
720
721 let skel = RepeatSkeleton {
722 name,
723 prefix,
724 group,
725 suffix,
726 min_repeats,
727 param_order: labels.to_vec(),
728 group_params,
729 };
730 for bytes in long.iter().chain(short) {
733 let (insts, _) = disassemble(bytes);
734 skel.match_script(&insts)?;
735 }
736 Some(skel)
737 }
738
739 pub fn params(&self) -> &[&'static str] {
741 &self.param_order
742 }
743
744 pub fn group_params(&self) -> &[&'static str] {
746 &self.group_params
747 }
748
749 fn match_script(&self, instructions: &[Instruction]) -> Option<Vec<Field>> {
752 let fixed = self.prefix.len() + self.suffix.len();
753 let extra = instructions.len().checked_sub(fixed)?;
754 if self.group.is_empty() || extra % self.group.len() != 0 {
755 return None;
756 }
757 let repeats = extra / self.group.len();
758 if repeats < self.min_repeats {
759 return None;
760 }
761 let mut values: Vec<(&'static str, Vec<u8>)> = Vec::new();
762 for (item, inst) in self.prefix.iter().zip(instructions) {
763 if !match_skel_item(item, inst, &mut values) {
764 return None;
765 }
766 }
767 let mut at = self.prefix.len();
768 let mut repeat_fields: Vec<Field> = Vec::new();
769 for _ in 0..repeats {
770 let mut rv: Vec<(&'static str, Vec<u8>)> = Vec::new();
773 for item in &self.group {
774 if !match_skel_item(item, &instructions[at], &mut rv) {
775 return None;
776 }
777 at += 1;
778 }
779 repeat_fields.extend(fields_in_order(&self.group_params, &rv));
780 }
781 for (item, inst) in self.suffix.iter().zip(&instructions[at..]) {
782 if !match_skel_item(item, inst, &mut values) {
783 return None;
784 }
785 }
786 let mut fields = fields_in_order(&self.param_order, &values);
787 fields.append(&mut repeat_fields);
788 Some(fields)
789 }
790}
791
792pub struct TemplateDecoder {
794 skeletons: Vec<Skeleton>,
795 repeats: Vec<RepeatSkeleton>,
796}
797
798impl TemplateDecoder {
799 pub fn new(skeletons: Vec<Skeleton>) -> Self {
800 Self {
801 skeletons,
802 repeats: vec![],
803 }
804 }
805
806 pub fn with_repeats(skeletons: Vec<Skeleton>, repeats: Vec<RepeatSkeleton>) -> Self {
807 Self { skeletons, repeats }
808 }
809}
810
811impl StateDecoder for TemplateDecoder {
812 fn name(&self) -> &'static str {
813 "template"
814 }
815 fn decode(&self, _spk_version: u16, script: &[u8]) -> Option<Decoded> {
816 let (instructions, truncated) = disassemble(script);
817 if truncated {
818 return None;
819 }
820 let hit = self
821 .skeletons
822 .iter()
823 .find_map(|s| {
824 s.match_script(&instructions)
825 .map(|f| (s.name, s.generation, f))
826 })
827 .or_else(|| {
828 self.repeats
829 .iter()
830 .find_map(|s| s.match_script(&instructions).map(|f| (s.name, None, f)))
831 });
832 let (name, generation, fields) = hit?;
833 let mut d = base_decode("template", script);
834 d.template = Some(name);
835 d.generation = generation;
836 d.fields = fields;
837 Some(d)
838 }
839}
840
841pub const SILVERSCRIPT_GENERATIONS: &[(&str, &str)] = &[
870 (SILVERSCRIPT_GEN_D25BD34, "kaspanet/silverscript d25bd34"),
871 (
872 SILVERSCRIPT_GEN_FORK,
873 "michaelsutton/silverscript d57e5dff62f2a9e0e3dd2d5f203d051911f6276d",
874 ),
875 (
876 SILVERSCRIPT_GEN_V1,
877 "kaspanet/silverscript v1.0.0 3ed973335b59269293564805cc2c58a14595ec03",
878 ),
879];
880pub const SILVERSCRIPT_GEN_D25BD34: &str = "d25bd34";
884pub const SILVERSCRIPT_GEN_FORK: &str = "fork";
888pub const SILVERSCRIPT_GEN_V1: &str = "v1";
892
893const SENT_A32: [u8; 32] = [0x11; 32];
894const SENT_B32: [u8; 32] = [0x22; 32];
895const SENT_C32: [u8; 32] = [0x33; 32];
896const SENT_D32: [u8; 32] = [0x44; 32];
897
898const MECENAS_A: &str = "6b6c76009c637502e803b100c3201111111111111111111111111111111111111111111111111111111111111111030000207c7e01ac7e876902e803b9be760400e1f50594527994760400e1f505547993a16300c252795479949c696700c20400e1f5059c6951c3b9bf876951c2789c6968007a75007a75007a75516776519c637578aa2033333333333333333333333333333333333333333333333333333333333333338769765279ac69757551677500696868";
900const MECENAS_B: &str = "6b6c76009c637502d007b100c3202222222222222222222222222222222222222222222222222222222222222222030000207c7e01ac7e876902e803b9be760480b2e60e94527994760480b2e60e547993a16300c252795479949c696700c20480b2e60e9c6951c3b9bf876951c2789c6968007a75007a75007a75516776519c637578aa2044444444444444444444444444444444444444444444444444444444444444448769765279ac69757551677500696868";
901const ESCROW_A: &str = "78aa2033333333333333333333333333333333333333333333333333333333333333338769765279ac6900c2b9be02e803949c6900c3201111111111111111111111111111111111111111111111111111111111111111030000207c7e01ac7e8700c3202222222222222222222222222222222222222222222222222222222222222222030000207c7e01ac7e879b69757551";
902const ESCROW_B: &str = "78aa2044444444444444444444444444444444444444444444444444444444444444448769765279ac6900c2b9be02e803949c6900c3202222222222222222222222222222222222222222222222222222222222222222030000207c7e01ac7e8700c3201111111111111111111111111111111111111111111111111111111111111111030000207c7e01ac7e879b69757551";
903const LASTWILL_A: &str = "6b6c76009c637502b400b178aa2033333333333333333333333333333333333333333333333333333333333333338769765279ac697575516776519c637578aa2044444444444444444444444444444444444444444444444444444444444444448769765279ac697575516776529c637578aa2011111111111111111111111111111111111111111111111111111111111111118769765279ac6900c2b9be02e803949c6900c3b9bf876975755167750069686868";
904const LASTWILL_B: &str = "6b6c76009c637502b400b178aa2044444444444444444444444444444444444444444444444444444444444444448769765279ac697575516776519c637578aa2033333333333333333333333333333333333333333333333333333333333333338769765279ac697575516776529c637578aa2022222222222222222222222222222222222222222222222222222222222222228769765279ac6900c2b9be02e803949c6900c3b9bf876975755167750069686868";
905
906const MECENAS_FORK_A: &str = "6b6c76009c637502e803b100c3030000202011111111111111111111111111111111111111111111111111111111111111117e01ac7e876902e803b9be760400e1f50594527994760400e1f505547993a16300c252795479949c696700c20400e1f5059c6951c3b9bf876951c2789c6968757575516776519c637578aa2033333333333333333333333333333333333333333333333333333333333333338769765279ac69757551676a6868";
911const MECENAS_FORK_B: &str = "6b6c76009c637502d007b100c3030000202022222222222222222222222222222222222222222222222222222222222222227e01ac7e876902e803b9be760480b2e60e94527994760480b2e60e547993a16300c252795479949c696700c20480b2e60e9c6951c3b9bf876951c2789c6968757575516776519c637578aa2044444444444444444444444444444444444444444444444444444444444444448769765279ac69757551676a6868";
912const ESCROW_FORK_A: &str = "78aa2033333333333333333333333333333333333333333333333333333333333333338769765279ac6900c2b9be02e803949c6900c3030000202011111111111111111111111111111111111111111111111111111111111111117e01ac7e8700c3030000202022222222222222222222222222222222222222222222222222222222222222227e01ac7e879b69757551";
913const ESCROW_FORK_B: &str = "78aa2044444444444444444444444444444444444444444444444444444444444444448769765279ac6900c2b9be02e803949c6900c3030000202022222222222222222222222222222222222222222222222222222222222222227e01ac7e8700c3030000202011111111111111111111111111111111111111111111111111111111111111117e01ac7e879b69757551";
914const LASTWILL_FORK_A: &str = "6b6c76009c637502b400b178aa2033333333333333333333333333333333333333333333333333333333333333338769765279ac697575516776519c637578aa2044444444444444444444444444444444444444444444444444444444444444448769765279ac697575516776529c637578aa2011111111111111111111111111111111111111111111111111111111111111118769765279ac6900c2b9be02e803949c6900c3b9bf8769757551676a686868";
915const LASTWILL_FORK_B: &str = "6b6c76009c637502b400b178aa2044444444444444444444444444444444444444444444444444444444444444448769765279ac697575516776519c637578aa2033333333333333333333333333333333333333333333333333333333333333338769765279ac697575516776529c637578aa2022222222222222222222222222222222222222222222222222222222222222228769765279ac6900c2b9be02e803949c6900c3b9bf8769757551676a686868";
916
917const MECENAS_V1_A: &str = "7604a9ad9e7787637502e8037600050000000001a569b1030000202011111111111111111111111111111111111111111111111111111111111111117e01ac7e00c378876902e803b9be760400e1f50594527994760400e1f505547993a16300c252795479949c696700c20400e1f5059c69b9bf51c378876951c252799c69756875757575516776043566d492876375788201209d75768201419d7578aa2033333333333333333333333333333333333333333333333333333333333333338769765279ac69757551676a6868";
928const MECENAS_V1_B: &str = "7604a9ad9e7787637502d0077600050000000001a569b1030000202022222222222222222222222222222222222222222222222222222222222222227e01ac7e00c378876902e803b9be760480b2e60e94527994760480b2e60e547993a16300c252795479949c696700c20480b2e60e9c69b9bf51c378876951c252799c69756875757575516776043566d492876375788201209d75768201419d7578aa2044444444444444444444444444444444444444444444444444444444444444448769765279ac69757551676a6868";
929const ESCROW_V1_A: &str = "7604514bee64876375788201209d75768201419d7578aa2033333333333333333333333333333333333333333333333333333333333333338769765279ac6902e803b9be789400c2789c69030000202011111111111111111111111111111111111111111111111111111111111111117e01ac7e030000202022222222222222222222222222222222222222222222222222222222222222227e01ac7e00c352798700c352798778789b69757575757575757551676a68";
930const ESCROW_V1_B: &str = "7604514bee64876375788201209d75768201419d7578aa2044444444444444444444444444444444444444444444444444444444444444448769765279ac6902e803b9be789400c2789c69030000202022222222222222222222222222222222222222222222222222222222222222227e01ac7e030000202011111111111111111111111111111111111111111111111111111111111111117e01ac7e00c352798700c352798778789b69757575757575757551676a68";
931const LASTWILL_V1_A: &str = "7604f0719a23876375788201209d75768201419d7502b4007600050000000001a569b178aa2033333333333333333333333333333333333333333333333333333333333333338769765279ac6975755167760479f22b76876375788201209d75768201419d7578aa2044444444444444444444444444444444444444444444444444444444444444448769765279ac69757551677604bd741282876375788201209d75768201419d7578aa2011111111111111111111111111111111111111111111111111111111111111118769765279ac6902e803b9be789400c2789c69b9bf00c3788769757575757551676a686868";
932const LASTWILL_V1_B: &str = "7604f0719a23876375788201209d75768201419d7502b4007600050000000001a569b178aa2044444444444444444444444444444444444444444444444444444444444444448769765279ac6975755167760479f22b76876375788201209d75768201419d7578aa2033333333333333333333333333333333333333333333333333333333333333338769765279ac69757551677604bd741282876375788201209d75768201419d7578aa2022222222222222222222222222222222222222222222222222222222222222228769765279ac6902e803b9be789400c2789c69b9bf00c3788769757575757551676a686868";
933
934pub const SILVERSCRIPT_DUMPS: &[(&str, &str, &str, &str)] = &[
939 ("SilverScript · Mecenas", SILVERSCRIPT_GEN_D25BD34, MECENAS_A, MECENAS_B),
940 ("SilverScript · Escrow", SILVERSCRIPT_GEN_D25BD34, ESCROW_A, ESCROW_B),
941 ("SilverScript · LastWill", SILVERSCRIPT_GEN_D25BD34, LASTWILL_A, LASTWILL_B),
942 ("SilverScript · Mecenas", SILVERSCRIPT_GEN_FORK, MECENAS_FORK_A, MECENAS_FORK_B),
943 ("SilverScript · Escrow", SILVERSCRIPT_GEN_FORK, ESCROW_FORK_A, ESCROW_FORK_B),
944 ("SilverScript · LastWill", SILVERSCRIPT_GEN_FORK, LASTWILL_FORK_A, LASTWILL_FORK_B),
945 ("SilverScript · Mecenas", SILVERSCRIPT_GEN_V1, MECENAS_V1_A, MECENAS_V1_B),
946 ("SilverScript · Escrow", SILVERSCRIPT_GEN_V1, ESCROW_V1_A, ESCROW_V1_B),
947 ("SilverScript · LastWill", SILVERSCRIPT_GEN_V1, LASTWILL_V1_A, LASTWILL_V1_B),
948];
949
950pub fn snum(v: i64) -> Vec<u8> {
955 let mut out = Vec::new();
956 let mut abs = v.unsigned_abs();
957 while abs > 0 {
958 out.push((abs & 0xff) as u8);
959 abs >>= 8;
960 }
961 if let Some(last) = out.last() {
962 if last & 0x80 != 0 {
963 out.push(0);
964 }
965 }
966 out
967}
968
969pub fn silverscript_sentinels(name: &str) -> Option<Vec<(&'static str, Vec<u8>)>> {
972 Some(match name {
973 "SilverScript · Mecenas" => vec![
975 ("recipient", SENT_A32.to_vec()),
976 ("funder_hash", SENT_C32.to_vec()),
977 ("pledge", snum(100_000_000)),
978 ("period", snum(1000)),
979 ],
980 "SilverScript · Escrow" => vec![
982 ("arbiter_hash", SENT_C32.to_vec()),
983 ("buyer", SENT_A32.to_vec()),
984 ("seller", SENT_B32.to_vec()),
985 ],
986 "SilverScript · LastWill" => vec![
988 ("inheritor_hash", SENT_C32.to_vec()),
989 ("cold_hash", SENT_D32.to_vec()),
990 ("hot_hash", SENT_A32.to_vec()),
991 ],
992 _ => return None,
993 })
994}
995
996pub fn silverscript_skeletons() -> Vec<Skeleton> {
1001 let hex2 = |s: &str| hex::decode(s).expect("template dump hex");
1002 let mut out = Vec::new();
1003 for (name, generation, a, b) in SILVERSCRIPT_DUMPS {
1004 let Some(sentinels) = silverscript_sentinels(name) else {
1005 continue;
1006 };
1007 if let Some(s) = Skeleton::derive(name, &hex2(a), &hex2(b), &sentinels) {
1008 out.push(s.with_generation(generation));
1009 }
1010 }
1011 out
1012}
1013
1014pub fn p2sh_hash(spk: &[u8]) -> Option<&[u8]> {
1017 (spk.len() == 35 && spk[0] == 0xaa && spk[1] == 0x20 && spk[34] == 0x87).then(|| &spk[2..34])
1018}
1019
1020const X402_ESCROW_A: &str = "6b0877777777777777776c76009c6375527982589d757882589d75b9cbb9cfd2789c697856795579b2519c69b5009c69b3519c69b4529c69b9009c69b9bd080000000000000000876978014001417f0101876978202222222222222222222222222222222222222222222222222222222222222222ac69b9cf76d0519c6976d2519c69b9cb519c69b900cc519c6900d5200000000000000000000000000000000000000000000000000000000000000000876951d5788769587920dd08a0f48c41d89eda672cef33c3d43a4c2bd11083efbcd62dbbae0e018ec30b2033333333333333333333333333333333333333333333333333333333333333337e52797e53797ea8201111111111111111111111111111111111111111111111111111111111111111d769785779557900a269785679a0697600a069765279577994a169b9be78789f6900c200a06900c25279a16900c3a8204444444444444444444444444444444444444444444444444444444444444444876951c2785379949c695779519c690108577953799358cd7eb976c902b20394765193bc7c7eb976c97602a883937cbc7eaa02000001aa7e01207e7c7e01877eb900ccc38875757575757575757575757575516776519c6375b9cbb9cfd2789c69785379b2519c69b5009c69b351a069b400a069b452a169b9009c69b9bd080000000000000000876976014001417f0101876976201111111111111111111111111111111111111111111111111111111111111111ac69b9cf76d0519c6976d2519c69b9cb519c69b900cc009c6900d5788769527900a26900c2b9bea069b4529c6351d5200000000000000000000000000000000000000000000000000000000000000000876951c200a06951c3a82055555555555555555555555555555555555555555555555555555555555555558769685379519c690108537958cd7eb976c902b20394765193bc7c7eb976c97602a883937cbc7eaa02000001aa7e01207e7c7e01877eb900ccc388757575757575516776529c6375b2519c69b3519c69b4519c69b9009c69b9bd0800000000000000008769086666666666666666b078014001417f0101876978201111111111111111111111111111111111111111111111111111111111111111ac69b9cf76d0519c6976d2009c69b9cb009c6900d5200000000000000000000000000000000000000000000000000000000000000000876900c200a06900c2b9bea16900c3a8205555555555555555555555555555555555555555555555555555555555555555876975757551676a686868";
1029const X402_ESCROW_B: &str = "6b080e0e0e0e0e0e0e0e6c76009c6375527982589d757882589d75b9cbb9cfd2789c697856795579b2519c69b5009c69b3519c69b4529c69b9009c69b9bd080000000000000000876978014001417f0101876978209999999999999999999999999999999999999999999999999999999999999999ac69b9cf76d0519c6976d2519c69b9cb519c69b900cc519c6900d5200000000000000000000000000000000000000000000000000000000000000000876951d5788769587920dd08a0f48c41d89eda672cef33c3d43a4c2bd11083efbcd62dbbae0e018ec30b20aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa7e52797e53797ea8208888888888888888888888888888888888888888888888888888888888888888d769785779557900a269785679a0697600a069765279577994a169b9be78789f6900c200a06900c25279a16900c3a820bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb876951c2785379949c695779519c690108577953799358cd7eb976c902b20394765193bc7c7eb976c97602a883937cbc7eaa02000001aa7e01207e7c7e01877eb900ccc38875757575757575757575757575516776519c6375b9cbb9cfd2789c69785379b2519c69b5009c69b351a069b400a069b452a169b9009c69b9bd080000000000000000876976014001417f0101876976208888888888888888888888888888888888888888888888888888888888888888ac69b9cf76d0519c6976d2519c69b9cb519c69b900cc009c6900d5788769527900a26900c2b9bea069b4529c6351d5200000000000000000000000000000000000000000000000000000000000000000876951c200a06951c3a820cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc8769685379519c690108537958cd7eb976c902b20394765193bc7c7eb976c97602a883937cbc7eaa02000001aa7e01207e7c7e01877eb900ccc388757575757575516776529c6375b2519c69b3519c69b4519c69b9009c69b9bd080000000000000000876908ddddddddddddddddb078014001417f0101876978208888888888888888888888888888888888888888888888888888888888888888ac69b9cf76d0519c6976d2009c69b9cb009c6900d5200000000000000000000000000000000000000000000000000000000000000000876900c200a06900c2b9bea16900c3a820cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc876975757551676a686868";
1030
1031pub fn x402_skeletons() -> Vec<Skeleton> {
1036 let hex2 = |s: &str| hex::decode(s).expect("template dump hex");
1037 let mut out = Vec::new();
1038 if let Some(s) = Skeleton::derive(
1043 "x402 · escrow v2",
1044 &hex2(X402_ESCROW_A),
1045 &hex2(X402_ESCROW_B),
1046 &[
1047 ("client", vec![0x11; 32]),
1048 ("server", vec![0x22; 32]),
1049 ("network_hash", vec![0x33; 32]),
1050 ("payout_hash", vec![0x44; 32]),
1051 ("refund_hash", vec![0x55; 32]),
1052 ("timeout_daa", vec![0x66; 8]),
1053 ("settled_total", vec![0x77; 8]),
1054 ],
1055 ) {
1056 out.push(s);
1057 }
1058 out
1059}
1060
1061pub fn p2sh_reveal(spk: &[u8], sig_script: &[u8]) -> Option<Vec<u8>> {
1065 let hash = p2sh_hash(spk)?;
1066 let (instructions, truncated) = disassemble(sig_script);
1067 if truncated {
1068 return None;
1069 }
1070 let redeem = instructions.last()?.data.clone()?;
1071 let digest = blake2b_simd::Params::new().hash_length(32).hash(&redeem);
1072 (digest.as_bytes() == hash).then_some(redeem)
1073}
1074
1075pub struct Registry {
1077 decoders: Vec<Box<dyn StateDecoder>>,
1078}
1079
1080impl Default for Registry {
1081 fn default() -> Self {
1082 let mut skeletons = silverscript_skeletons();
1083 skeletons.extend(x402_skeletons());
1084 skeletons.extend(observed::observed_skeletons());
1085 Self {
1086 decoders: vec![
1087 Box::new(TemplateDecoder::with_repeats(
1088 skeletons,
1089 observed::observed_repeat_skeletons(),
1090 )),
1091 Box::new(P2pkStateDecoder),
1092 Box::new(P2shCommitmentDecoder),
1093 ],
1094 }
1095 }
1096}
1097
1098impl Registry {
1099 pub fn register(&mut self, decoder: Box<dyn StateDecoder>) {
1100 self.decoders.push(decoder);
1101 }
1102
1103 pub fn decode(&self, spk_version: u16, script: &[u8]) -> Decoded {
1104 self.decoders
1105 .iter()
1106 .find_map(|d| d.decode(spk_version, script))
1107 .or_else(|| DisasmDecoder.decode(spk_version, script))
1108 .expect("disasm fallback always decodes")
1109 }
1110}
1111
1112#[cfg(test)]
1113mod tests {
1114 use super::*;
1115
1116 #[test]
1117 fn p2pk_state_labels_owner() {
1118 let mut script = vec![0x20];
1119 script.extend([0x7f; 32]);
1120 script.push(0xac);
1121 let d = Registry::default().decode(0, &script);
1122 assert_eq!(d.template, Some("p2pk state"));
1123 assert_eq!(d.fields.len(), 1);
1124 assert_eq!(d.fields[0].name, "owner_pubkey");
1125 assert_eq!(d.fields[0].value, vec![0x7f; 32]);
1126 }
1127
1128 #[test]
1129 fn p2sh_commitment_labels_hash() {
1130 let mut script = vec![0xaa, 0x20];
1131 script.extend([0x42; 32]);
1132 script.push(0x87);
1133 let d = Registry::default().decode(0, &script);
1134 assert_eq!(d.template, Some("p2sh commitment"));
1135 assert_eq!(d.fields[0].name, "program_hash");
1136 assert_eq!(d.fields[0].value, vec![0x42; 32]);
1137 }
1138
1139 #[test]
1140 fn all_silverscript_skeletons_derive() {
1141 let got: Vec<_> = silverscript_skeletons()
1142 .iter()
1143 .map(|s| (s.name, s.generation))
1144 .collect();
1145 let want: Vec<_> = SILVERSCRIPT_DUMPS
1146 .iter()
1147 .map(|(n, g, _, _)| (*n, Some(*g)))
1148 .collect();
1149 assert_eq!(
1150 got, want,
1151 "every embedded compiler dump must derive a skeleton, in table order"
1152 );
1153 assert_eq!(got.len(), 9, "three families, three generations each");
1154 for (id, _) in SILVERSCRIPT_GENERATIONS {
1155 assert_eq!(
1156 got.iter().filter(|(_, g)| *g == Some(*id)).count(),
1157 3,
1158 "generation {id} must carry all three families"
1159 );
1160 }
1161 }
1162
1163 #[test]
1169 fn every_generation_names_its_own_dumps() {
1170 let reg = Registry::default();
1171 for (name, generation, a, b) in SILVERSCRIPT_DUMPS {
1172 for dump in [a, b] {
1173 let d = reg.decode(0, &hex::decode(dump).unwrap());
1174 assert_eq!(d.template, Some(*name), "{name} {generation}");
1175 assert_eq!(d.generation, Some(*generation), "{name} {generation}");
1176 }
1177 }
1178 for (dump, generation) in [
1181 (MECENAS_FORK_B, SILVERSCRIPT_GEN_FORK),
1182 (MECENAS_V1_B, SILVERSCRIPT_GEN_V1),
1183 ] {
1184 let d = reg.decode(0, &hex::decode(dump).unwrap());
1185 assert_eq!(d.generation, Some(generation));
1186 let get = |n: &str| {
1187 d.fields
1188 .iter()
1189 .find(|f| f.name == n)
1190 .map(|f| f.value.clone())
1191 };
1192 assert_eq!(get("recipient"), Some(vec![0x22; 32]));
1193 assert_eq!(get("funder_hash"), Some(vec![0x44; 32]));
1194 assert_eq!(get("pledge"), Some(snum(250_000_000)));
1195 assert_eq!(get("period"), Some(snum(2000)));
1196 }
1197 let mut forged = hex::decode(ESCROW_V1_A).unwrap();
1200 forged[2] ^= 0x01; let d = reg.decode(0, &forged);
1202 assert_eq!(d.template, None, "a changed dispatch tag must not name");
1203 }
1204
1205 #[test]
1206 fn skeleton_matches_real_compiled_instances_and_labels_args() {
1207 let reg = Registry::default();
1208
1209 let d = reg.decode(0, &hex::decode(MECENAS_B).unwrap());
1211 assert_eq!(d.template, Some("SilverScript · Mecenas"));
1212 let get = |n: &str| {
1213 d.fields
1214 .iter()
1215 .find(|f| f.name == n)
1216 .map(|f| f.value.clone())
1217 };
1218 assert_eq!(get("recipient"), Some(vec![0x22; 32]));
1219 assert_eq!(get("funder_hash"), Some(vec![0x44; 32]));
1220 assert_eq!(get("pledge"), Some(snum(250_000_000)));
1221 assert_eq!(get("period"), Some(snum(2000)));
1222
1223 let d = reg.decode(0, &hex::decode(ESCROW_A).unwrap());
1226 assert_eq!(d.template, Some("SilverScript · Escrow"));
1227 let get = |n: &str| {
1228 d.fields
1229 .iter()
1230 .find(|f| f.name == n)
1231 .map(|f| f.value.clone())
1232 };
1233 assert_eq!(get("arbiter_hash"), Some(vec![0x33; 32]));
1234 assert_eq!(get("buyer"), Some(vec![0x11; 32]));
1235 assert_eq!(get("seller"), Some(vec![0x22; 32]));
1236
1237 let d = reg.decode(0, &hex::decode(LASTWILL_B).unwrap());
1239 assert_eq!(d.template, Some("SilverScript · LastWill"));
1240
1241 let mut broken = hex::decode(MECENAS_A).unwrap();
1243 let last = broken.len() - 1;
1244 broken[last] = 0x51;
1245 let d = reg.decode(0, &broken);
1246 assert_eq!(d.template, None);
1247 }
1248
1249 const X402_LIVE_TN10: &str = "6b0880d1f008000000006c76009c6375527982589d757882589d75b9cbb9cfd2789c697856795579b2519c69b5009c69b3519c69b4529c69b9009c69b9bd080000000000000000876978014001417f010187697820bdbe5f45ef356d152e45dc56233e9b83a739f66672289fb709f541eef4cedd75ac69b9cf76d0519c6976d2519c69b9cb519c69b900cc519c6900d5200000000000000000000000000000000000000000000000000000000000000000876951d5788769587920dd08a0f48c41d89eda672cef33c3d43a4c2bd11083efbcd62dbbae0e018ec30b203b06c42cd879a4951fe465f255164917ac183fabd3facb8dc33705b7bbd649367e52797e53797ea820e6f0c82b07b9e0786064f95b9a007ee3bd7ef8b69c73d911a290c7547f28d75ad769785779557900a269785679a0697600a069765279577994a169b9be78789f6900c200a06900c25279a16900c3a8201b3e09a7a671767054967df9b649daf212e68f9d1698f51bbfd83c599f50fa20876951c2785379949c695779519c690108577953799358cd7eb976c902b20394765193bc7c7eb976c97602a883937cbc7eaa02000001aa7e01207e7c7e01877eb900ccc38875757575757575757575757575516776519c6375b9cbb9cfd2789c69785379b2519c69b5009c69b351a069b400a069b452a169b9009c69b9bd080000000000000000876976014001417f010187697620e6f0c82b07b9e0786064f95b9a007ee3bd7ef8b69c73d911a290c7547f28d75aac69b9cf76d0519c6976d2519c69b9cb519c69b900cc009c6900d5788769527900a26900c2b9bea069b4529c6351d5200000000000000000000000000000000000000000000000000000000000000000876951c200a06951c3a8200e35c71594d24df8ddcbc23fd8a7627aec1e1806f66d3ee83d7eb1179cb74cb58769685379519c690108537958cd7eb976c902b20394765193bc7c7eb976c97602a883937cbc7eaa02000001aa7e01207e7c7e01877eb900ccc388757575757575516776529c6375b2519c69b3519c69b4519c69b9009c69b9bd08000000000000000087690851842d2000000000b078014001417f010187697820e6f0c82b07b9e0786064f95b9a007ee3bd7ef8b69c73d911a290c7547f28d75aac69b9cf76d0519c6976d2009c69b9cb009c6900d5200000000000000000000000000000000000000000000000000000000000000000876900c200a06900c2b9bea16900c3a8200e35c71594d24df8ddcbc23fd8a7627aec1e1806f66d3ee83d7eb1179cb74cb5876975757551676a686868";
1255
1256 #[test]
1257 fn x402_escrow_skeleton_derives_and_names_the_live_tn10_channel() {
1258 let names: Vec<_> = x402_skeletons().iter().map(|s| s.name).collect();
1259 assert_eq!(names, ["x402 · escrow v2"], "the embedded x402 dumps must derive");
1260
1261 let reg = Registry::default();
1262
1263 let d = reg.decode(0, &hex::decode(X402_ESCROW_B).unwrap());
1266 assert_eq!(d.template, Some("x402 · escrow v2"));
1267 let get = |n: &str| {
1268 d.fields
1269 .iter()
1270 .find(|f| f.name == n)
1271 .map(|f| f.value.clone())
1272 };
1273 assert_eq!(get("client"), Some(vec![0x88; 32]));
1274 assert_eq!(get("server"), Some(vec![0x99; 32]));
1275 assert_eq!(get("network_hash"), Some(vec![0xaa; 32]));
1276 assert_eq!(get("payout_hash"), Some(vec![0xbb; 32]));
1277 assert_eq!(get("refund_hash"), Some(vec![0xcc; 32]));
1278 assert_eq!(get("timeout_daa"), Some(vec![0xdd; 8]));
1279 assert_eq!(get("settled_total"), Some(vec![0x0e; 8]));
1280 assert_eq!(d.fields.len(), 7, "each repeated slot reports one field");
1281
1282 let d = reg.decode(0, &hex::decode(X402_LIVE_TN10).unwrap());
1284 assert_eq!(d.template, Some("x402 · escrow v2"));
1285 let get = |n: &str| {
1286 d.fields
1287 .iter()
1288 .find(|f| f.name == n)
1289 .map(|f| f.value.clone())
1290 };
1291 assert_eq!(
1292 get("settled_total"),
1293 Some(150_000_000i64.to_le_bytes().to_vec()),
1294 "on-chain settled total must equal the channel's two claims"
1295 );
1296 assert_eq!(
1297 get("network_hash").map(hex::encode),
1298 Some("3b06c42cd879a4951fe465f255164917ac183fabd3facb8dc33705b7bbd64936".into())
1300 );
1301 assert_eq!(
1302 get("client").map(hex::encode),
1303 Some("e6f0c82b07b9e0786064f95b9a007ee3bd7ef8b69c73d911a290c7547f28d75a".into())
1304 );
1305
1306 let mut forged = hex::decode(X402_LIVE_TN10).unwrap();
1309 forged[261] ^= 0x01;
1310 let d = reg.decode(0, &forged);
1311 assert_eq!(d.template, None, "inconsistent inlined client key must not name");
1312 }
1313
1314 #[test]
1315 fn emit_with_sentinel_args_reproduces_each_dump() {
1316 let skels = silverscript_skeletons();
1319 for (name, generation, dump, _) in SILVERSCRIPT_DUMPS {
1320 let skel = skels
1321 .iter()
1322 .find(|s| s.name == *name && s.generation == Some(*generation))
1323 .expect("skeleton");
1324 let args = silverscript_sentinels(name).expect("sentinels");
1325 let args_ref: Vec<(&str, &[u8])> =
1326 args.iter().map(|(l, v)| (*l, v.as_slice())).collect();
1327 let emitted = skel.emit(&args_ref).expect("emit");
1328 assert_eq!(hex::encode(&emitted), *dump, "{name} {generation} emit != dump");
1329 }
1330 }
1331
1332 #[test]
1333 fn emit_round_trips_fresh_args_including_small_int_selector() {
1334 let reg = Registry::default();
1335 let skels = silverscript_skeletons();
1336 let mecenas = skels
1337 .iter()
1338 .find(|s| s.name == "SilverScript · Mecenas")
1339 .unwrap();
1340 let recipient = vec![0xab; 32];
1342 let funder = vec![0xcd; 32];
1343 let pledge = snum(180);
1344 let period = snum(6);
1345 let args: Vec<(&str, &[u8])> = vec![
1346 ("recipient", &recipient),
1347 ("funder_hash", &funder),
1348 ("pledge", &pledge),
1349 ("period", &period),
1350 ];
1351 let emitted = mecenas.emit(&args).expect("emit");
1352 let d = reg.decode(0, &emitted);
1353 assert_eq!(d.template, Some("SilverScript · Mecenas"));
1354 let get = |n: &str| {
1355 d.fields
1356 .iter()
1357 .find(|f| f.name == n)
1358 .map(|f| f.value.clone())
1359 };
1360 assert_eq!(get("recipient"), Some(recipient));
1361 assert_eq!(get("funder_hash"), Some(funder));
1362 assert_eq!(get("pledge"), Some(snum(180)));
1363 assert_eq!(get("period"), Some(snum(6)));
1364 assert_eq!(encode_push(&snum(6)), vec![0x56]);
1366 assert_eq!(encode_push(&[0xab; 32])[0], 0x20);
1368 }
1369
1370 #[test]
1371 fn zk_system_classifier() {
1372 let mut plain = vec![0x20];
1374 plain.extend([0x00; 32]);
1375 plain.push(0xac);
1376 assert_eq!(zk_system(&plain), None);
1377 assert_eq!(Registry::default().decode(0, &plain).zk_system, None);
1378
1379 let mut groth = encode_push(&[0x01; 192]);
1381 groth.push(0xa6);
1382 assert_eq!(zk_system(&groth), Some("groth16"));
1383 assert!(Registry::default().decode(0, &groth).uses_zk_ops);
1384 assert_eq!(
1385 Registry::default().decode(0, &groth).zk_system,
1386 Some("groth16")
1387 );
1388
1389 let mut risc0 = encode_push(&vec![0xab; 2048]);
1391 risc0.extend(encode_push(&[0xcd; 32])); risc0.push(0xa6);
1393 assert_eq!(zk_system(&risc0), Some("risc0"));
1394
1395 let mut ambiguous = encode_push(&[0x07; 32]);
1397 ambiguous.push(0xa6);
1398 assert_eq!(zk_system(&ambiguous), None);
1399 }
1400
1401 #[test]
1402 fn observed_skeletons_all_derive() {
1403 let names: Vec<_> = observed::observed_skeletons()
1404 .iter()
1405 .map(|s| s.name)
1406 .collect();
1407 assert_eq!(
1408 names,
1409 [
1410 "PURE",
1411 "genesis0 · list",
1412 "genesis0 · buy",
1413 "genesis0 · list",
1414 "genesis0 · buy",
1415 "genesis0 · collection",
1416 "KCC20 token",
1417 "KCC20 token",
1418 "KCC20 token",
1419 "KCC20 minter",
1420 "genesis0 · root",
1425 "genesis0 · slot-mint",
1426 "genesis0 · slot-mint",
1427 "genesis0 · slot-mint",
1428 "genesis0 · slot-mint",
1429 "genesis0 · buy",
1430 "genesis0 · list",
1431 "genesis0 · style-list",
1432 "genesis0 · dutch-list",
1433 "genesis0 · auction",
1434 "genesis0 · krc-gate-boot",
1435 "KasWare · vault-schnorr",
1436 "KasWare · vault-pqcold",
1437 "KasWare · vault-htlc",
1438 "KasWare · vault-dms",
1439 "KaspaCom · token",
1440 "Zealous · token cell",
1441 "Zealous · token cell",
1442 "Zealous · curve",
1445 "Zealous · pool",
1446 "Zealous · position cell",
1447 "Zealous · liquidity position",
1448 "Zealous · curve",
1449 "Zealous · curve",
1450 "Zealous · curve",
1451 "Zealous · curve",
1452 "Zealous · pool",
1453 "Zealous · pool",
1454 "Zealous · pool",
1455 "Zealous · pool",
1456 "Zealous · pool",
1457 "Zealous · pool",
1458 "Zealous · pool",
1459 "Zealous · pool",
1460 "Zealous · pool",
1461 "Zealous · position cell",
1462 "Zealous · position cell",
1463 "Zealous · liquidity position",
1464 "Zealous · liquidity position",
1465 "Zealous · factory",
1466 "Zealous · factory",
1467 "Zealous · factory",
1468 "Zealous · token cell",
1469 "Zealous · token cell",
1470 "Zealous · token cell",
1471 "KaspaRocket · lp-amm",
1472 "KaspaRocket · lp-amm",
1473 "KaspaRocket · lp-amm",
1474 "KaspaRocket · order-escrow",
1475 "KaspaRocket · order-escrow",
1476 "KaspaKaha · pool",
1477 "KaspaKaha · pool",
1478 "KaspaKaha · pool",
1479 "KaspaKaha · pool",
1480 "KaspaKaha · pool",
1481 "KaspaKaha · pool",
1482 "KaspaKaha · pool",
1483 "KaspaKaha · pool",
1484 "KaspaKaha · pool",
1485 "KaspaKaha · pool",
1486 "KaspaCom · token",
1487 "KaspaCom · wrapped token",
1488 "KaspaCom · wrapped token",
1489 "KaspaCom · wrapped token",
1490 "KaspaCom · wrapped token",
1491 "KaspaCom · wrapped token",
1492 "KaspaCom · wrapped token",
1493 "KaspaCom · wrapped token",
1494 "KaspaCom · wrapped token",
1495 "KaspaCom · wrapper",
1496 "KaspaCom · wrapper",
1497 "KaspaCom · wrapper",
1498 "KaspaCom · wrapper",
1499 "genesis0 · mint",
1500 "KRON · curve",
1501 "KRON · curve",
1502 "KRON · curve",
1503 "KRON · curve",
1504 "KRON · pool",
1505 "KRON · pool",
1506 "KRON · pool",
1507 "KRON · pool",
1508 "allowance tn-f",
1509 "log tn-f",
1510 "genesis0 · krc-gate-active",
1511 "KForge · curve",
1512 "KForge · token cell",
1513 "KForge · pool",
1514 ],
1515 "every on-chain fixture pair must derive a skeleton"
1516 );
1517 let repeats: Vec<_> = observed::observed_repeat_skeletons()
1518 .iter()
1519 .map(|s| s.name)
1520 .collect();
1521 assert_eq!(repeats, ["genesis0 · slot-mint"]);
1522 }
1523
1524 #[test]
1525 fn observed_families_match_their_fixture_programs() {
1526 let reg = Registry::default();
1527 let get = |d: &Decoded, n: &str| {
1528 d.fields
1529 .iter()
1530 .find(|f| f.name == n)
1531 .map(|f| f.value.clone())
1532 };
1533
1534 let pure = include_bytes!("../fixtures/pure_a.bin");
1536 let d = reg.decode(0, pure);
1537 assert_eq!(d.template, Some("PURE"));
1538 assert_eq!(
1539 get(&d, "signer_pubkey").map(hex::encode).as_deref(),
1540 Some("4df3c68074217004ad86fca1e63b91b73e625d9140063f21992231fdfdfa8936")
1541 );
1542
1543 let d = reg.decode(0, include_bytes!("../fixtures/kcc20_b_a.bin"));
1546 assert_eq!(d.template, Some("KCC20 token"));
1547 assert_eq!(get(&d, "owner_identifier").map(|v| v.len()), Some(32));
1548 assert_eq!(get(&d, "identifier_type"), Some(vec![0x02]));
1549 assert_eq!(get(&d, "amount"), Some(vec![0; 8]));
1550 assert_eq!(get(&d, "is_minter"), Some(vec![0x01]));
1551 let d = reg.decode(0, include_bytes!("../fixtures/kcc20_a_a.bin"));
1553 assert_eq!(d.template, Some("KCC20 token"));
1554 assert_eq!(get(&d, "identifier_type"), Some(vec![0x00]));
1555 assert_eq!(
1556 get(&d, "amount").map(hex::encode).as_deref(),
1557 Some("a00f000000000000")
1558 );
1559 assert_eq!(get(&d, "is_minter"), Some(vec![0x00]));
1560
1561 let d = reg.decode(0, include_bytes!("../fixtures/kcc20_minter_a.bin"));
1564 assert_eq!(d.template, Some("KCC20 minter"));
1565 assert_eq!(d.fields.len(), 2);
1566 assert!(d.fields.iter().all(|f| f.value.len() == 32));
1567
1568 for (fixture, want) in [
1570 (
1571 include_bytes!("../fixtures/g0_list_v1_a.bin").as_slice(),
1572 "genesis0 · list",
1573 ),
1574 (
1575 include_bytes!("../fixtures/g0_buy_v1_a.bin").as_slice(),
1576 "genesis0 · buy",
1577 ),
1578 (
1579 include_bytes!("../fixtures/g0_list_v2_b.bin").as_slice(),
1580 "genesis0 · list",
1581 ),
1582 (
1583 include_bytes!("../fixtures/g0_buy_v2_b.bin").as_slice(),
1584 "genesis0 · buy",
1585 ),
1586 (
1587 include_bytes!("../fixtures/g0_col_a.bin").as_slice(),
1588 "genesis0 · collection",
1589 ),
1590 ] {
1591 assert_eq!(
1592 reg.decode(0, fixture).template,
1593 Some(want),
1594 "fixture for {want}"
1595 );
1596 }
1597 let d = reg.decode(0, include_bytes!("../fixtures/g0_list_v1_a.bin"));
1599 let tmpl = get(&d, "next_state_template").expect("next_state_template");
1600 assert_eq!(tmpl.len(), 396);
1601 }
1602
1603 #[test]
1604 fn slot_mint_repeat_matcher_covers_all_arities() {
1605 let reg = Registry::default();
1606 let get_all = |d: &Decoded, n: &str| {
1607 d.fields
1608 .iter()
1609 .filter(|f| f.name == n)
1610 .map(|f| f.value.clone())
1611 .collect::<Vec<_>>()
1612 };
1613
1614 let di4m = include_bytes!("../fixtures/slot_mint_di4m_a.bin");
1616 let d = reg.decode(0, di4m);
1617 assert_eq!(d.template, Some("genesis0 · slot-mint"));
1618 assert_eq!(get_all(&d, "lane_tag"), vec![b"DI4M2".to_vec()]);
1619 assert_eq!(get_all(&d, "min_outputs"), vec![vec![0x04]]);
1620 let hashes = get_all(&d, "output_spk_hash");
1621 assert_eq!(hashes.len(), 2, "two repeats in the DI4M2 arity");
1622 assert_eq!(
1623 hex::encode(&hashes[0]),
1624 "c90a93233366793d3a3576e9677a7e1f31ff85c80ba999fc5ca5dbaeac73a544",
1625 "first repeat pins the shared marketplace output"
1626 );
1627
1628 let d = reg.decode(0, include_bytes!("../fixtures/slot_mint_gz4m_a.bin"));
1630 assert_eq!(d.template, Some("genesis0 · slot-mint"));
1631 assert_eq!(get_all(&d, "lane_tag"), vec![b"GZ4M1".to_vec()]);
1632 assert_eq!(get_all(&d, "min_outputs"), vec![vec![0x03]]);
1633 assert_eq!(get_all(&d, "output_spk_hash").len(), 1);
1634
1635 let (insts, _) = disassemble(di4m);
1638 let start = insts[102].offset;
1639 let end = insts[111].offset;
1640 let mut three = di4m[..end].to_vec();
1641 three.extend_from_slice(&di4m[start..end]);
1642 three.extend_from_slice(&di4m[end..]);
1643 let d = reg.decode(0, &three);
1644 assert_eq!(
1645 d.template,
1646 Some("genesis0 · slot-mint"),
1647 "extra repeat must still match"
1648 );
1649 assert_eq!(get_all(&d, "output_spk_hash").len(), 3);
1650
1651 let mut ragged = di4m[..end].to_vec();
1653 ragged.extend_from_slice(&di4m[start..start + 1]); ragged.extend_from_slice(&di4m[end..]);
1655 assert_eq!(reg.decode(0, &ragged).template, None);
1656 }
1657
1658 #[test]
1659 fn derive_observed_edge_cases() {
1660 let a = include_bytes!("../fixtures/pure_a.bin").as_slice();
1661 let b = include_bytes!("../fixtures/pure_b.bin").as_slice();
1662 assert!(Skeleton::derive_observed("x", &[a], &["k"]).is_none());
1664 assert!(Skeleton::derive_observed("x", &[a, b], &[]).is_none());
1666 assert!(Skeleton::derive_observed("x", &[a, b], &["k", "extra"]).is_none());
1667 assert!(Skeleton::derive_observed("x", &[a, b], &["k"]).is_some());
1668 let other = include_bytes!("../fixtures/g0_col_a.bin").as_slice();
1670 assert!(Skeleton::derive_observed("x", &[a, other], &["k"]).is_none());
1671 assert!(RepeatSkeleton::derive("x", &[a, b], &[], &["k"], &[]).is_none());
1673 }
1674
1675 #[test]
1676 fn zk_band_boundaries() {
1677 let probe = |n: usize| {
1678 let mut s = encode_push(&vec![0x5a; n]);
1679 s.push(0xa6); zk_system(&s)
1681 };
1682 assert_eq!(probe(127), None);
1683 assert_eq!(probe(128), Some("groth16"));
1684 assert_eq!(probe(256), Some("groth16"));
1685 assert_eq!(
1686 probe(257),
1687 None,
1688 "gap above the Groth16 band stays unattributed"
1689 );
1690 assert_eq!(probe(299), None);
1691 assert_eq!(probe(300), Some("succinct proof (inferred)"));
1692 assert_eq!(probe(1023), Some("succinct proof (inferred)"));
1693 assert_eq!(probe(1024), Some("risc0"));
1694 }
1695
1696 #[test]
1697 fn p2sh_reveal_verifies_and_peels() {
1698 let redeem = vec![0xb9, 0xcf, 0x51]; let hash = blake2b_simd::Params::new().hash_length(32).hash(&redeem);
1700 let mut spk = vec![0xaa, 0x20];
1701 spk.extend_from_slice(hash.as_bytes());
1702 spk.push(0x87);
1703 let mut sig = vec![0x02, 0x01, 0x02, 0x03];
1705 sig.extend_from_slice(&redeem);
1706 assert_eq!(p2sh_reveal(&spk, &sig), Some(redeem.clone()));
1707
1708 let mut bad_sig = vec![0x03];
1710 bad_sig.extend_from_slice(&[0x51, 0x52, 0x53]);
1711 assert_eq!(p2sh_reveal(&spk, &bad_sig), None);
1712
1713 assert_eq!(p2sh_reveal(&[0xac], &sig), None);
1715 }
1716}