pub fn template_hash(prefix: &[u8], suffix: &[u8]) -> [u8; 32] {
let mut state = blake2b_simd::Params::new().hash_length(32).to_state();
state.update(&(prefix.len() as u64).to_le_bytes());
state.update(prefix);
state.update(&(suffix.len() as u64).to_le_bytes());
state.update(suffix);
let mut out = [0u8; 32];
out.copy_from_slice(state.finalize().as_bytes());
out
}
const OP_0: u8 = 0x00;
const OP_DATA_4: u8 = 0x04;
const OP_PUSHDATA1: u8 = 0x4c;
const OP_PUSHDATA2: u8 = 0x4d;
const OP_PUSHDATA4: u8 = 0x4e;
const OP_1: u8 = 0x51;
const OP_16: u8 = 0x60;
const OP_IF: u8 = 0x63;
const OP_NOTIF: u8 = 0x64;
const OP_ELSE: u8 = 0x67;
const OP_ENDIF: u8 = 0x68;
const OP_VERIFY: u8 = 0x69;
const OP_RETURN: u8 = 0x6a;
const OP_TOALTSTACK: u8 = 0x6b;
const OP_FROMALTSTACK: u8 = 0x6c;
const OP_DROP: u8 = 0x75;
const OP_DUP: u8 = 0x76;
const OP_EQUAL: u8 = 0x87;
const OP_SUB: u8 = 0x94;
const OP_NUMEQUAL: u8 = 0x9c;
const OP_LESSTHANOREQUAL: u8 = 0xa1;
const OP_GREATERTHANOREQUAL: u8 = 0xa2;
const OP_ACTIVE_INPUT_INDEX: u8 = 0xb9;
const OP_AUTH_OUTPUT_COUNT: u8 = 0xcb;
const OP_AUTH_OUTPUT_IDX: u8 = 0xcc;
const OP_COV_OUTPUT_COUNT: u8 = 0xd2;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Generation {
Fork,
V1,
}
impl Generation {
pub fn as_str(self) -> &'static str {
match self {
Generation::Fork => "fork",
Generation::V1 => "1.0",
}
}
pub fn hash_name(self) -> &'static str {
match self {
Generation::Fork => "BLAKE2b",
Generation::V1 => "BLAKE3",
}
}
pub fn template_hash(self, prefix: &[u8], suffix: &[u8]) -> [u8; 32] {
match self {
Generation::Fork => template_hash(prefix, suffix),
Generation::V1 => crate::kcc1::template_hash(prefix, suffix),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Terminator {
OpReturn,
DropFail,
}
impl Terminator {
fn bytes(self) -> &'static [u8] {
match self {
Terminator::OpReturn => &[OP_RETURN],
Terminator::DropFail => &[OP_DROP, OP_0, OP_VERIFY],
}
}
pub fn as_str(self) -> &'static str {
match self {
Terminator::OpReturn => "op-return",
Terminator::DropFail => "drop-fail",
}
}
}
const TERMINATORS: [Terminator; 2] = [Terminator::OpReturn, Terminator::DropFail];
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Split {
Proven,
Candidate,
}
impl Split {
pub fn as_str(self) -> &'static str {
match self {
Split::Proven => "dispatch",
Split::Candidate => "leaf-candidate",
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Dispatch {
Leaf,
Selector(u8),
Tag([u8; 4]),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Authorised {
Exact(u8),
Range { fixed: u8, min: u8, max: u8 },
}
impl Authorised {
pub fn exact(self) -> Option<u8> {
match self {
Authorised::Exact(n) => Some(n),
Authorised::Range { .. } => None,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Entrypoint {
pub dispatch: Dispatch,
pub authorised: Authorised,
}
impl Entrypoint {
pub fn selector(&self) -> Option<u8> {
match self.dispatch {
Dispatch::Selector(s) => Some(s),
_ => None,
}
}
pub fn tag(&self) -> Option<[u8; 4]> {
match self.dispatch {
Dispatch::Tag(t) => Some(t),
_ => None,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Argent {
pub split: Split,
pub generation: Generation,
pub terminator: Option<Terminator>,
pub head_len: usize,
pub state_len: usize,
pub code_len: usize,
pub entrypoints: Vec<Entrypoint>,
pub template_hash: [u8; 32],
state_push_ends: Vec<usize>,
}
impl Argent {
pub fn entrypoint_count(&self) -> usize {
self.entrypoints.len()
}
pub fn slices<'a>(&self, program: &'a [u8]) -> (&'a [u8], &'a [u8], &'a [u8]) {
assert_eq!(
program.len(),
self.head_len + self.state_len + self.code_len,
"slices() must be handed the program this split was read from"
);
let (head, rest) = program.split_at(self.head_len);
let (state, code) = rest.split_at(self.state_len);
(head, state, code)
}
pub fn candidate_splits(&self, program: &[u8]) -> Vec<(usize, [u8; 32])> {
let code_start = self.head_len + self.state_len;
let code = &program[code_start..];
std::iter::once(self.head_len)
.chain(self.state_push_ends.iter().copied())
.map(|boundary| {
(
boundary,
self.generation.template_hash(&program[..boundary], code),
)
})
.collect()
}
}
fn step(program: &[u8], at: usize) -> Option<(usize, u8, Option<usize>)> {
let op = *program.get(at)?;
let after = at + 1;
let (len, header) = match op {
OP_0 => return Some((after, op, Some(0))),
0x01..=0x4b => (op as usize, 1),
OP_PUSHDATA1 => (*program.get(after)? as usize, 2),
OP_PUSHDATA2 => (
u16::from_le_bytes(program.get(after..after + 2)?.try_into().ok()?) as usize,
3,
),
OP_PUSHDATA4 => (
u32::from_le_bytes(program.get(after..after + 4)?.try_into().ok()?) as usize,
5,
),
_ => return Some((after, op, None)),
};
let end = at.checked_add(header)?.checked_add(len)?;
(end <= program.len()).then_some((end, op, Some(len)))
}
pub fn pushes(window: &[u8]) -> Option<Vec<&[u8]>> {
let mut at = 0usize;
let mut out = Vec::new();
while at < window.len() {
let (next, op, data) = step(window, at)?;
let len = data?;
let payload_start = next - len;
out.push(if op == OP_0 { &window[at..at] } else { &window[payload_start..next] });
at = next;
}
(!out.is_empty()).then_some(out)
}
pub fn script_num(payload: &[u8]) -> Option<i64> {
if payload.is_empty() {
return Some(0);
}
if payload.len() > 8 {
return None;
}
let mut bytes = [0u8; 8];
bytes[..payload.len()].copy_from_slice(payload);
let negative = payload[payload.len() - 1] & 0x80 != 0;
bytes[payload.len() - 1] &= 0x7f;
let magnitude = i64::from_le_bytes(bytes);
Some(if negative { -magnitude } else { magnitude })
}
fn small_int(program: &[u8], at: usize) -> Option<u8> {
match *program.get(at)? {
OP_0 => Some(0),
op @ OP_1..=OP_16 => Some(op - OP_1 + 1),
_ => None,
}
}
pub fn dispatch_envelope(program: &[u8]) -> Option<(usize, Terminator)> {
let mut at = program.len();
while at > 0 && program[at - 1] == OP_ENDIF {
at -= 1;
}
let rungs = program.len() - at;
if rungs == 0 {
return None;
}
let terminator = TERMINATORS
.into_iter()
.find(|t| program[..at].ends_with(t.bytes()))?;
Some((rungs, terminator))
}
fn range_bounds(program: &[u8], at: usize) -> Option<(u8, u8)> {
if *program.get(at)? != OP_DUP {
return None;
}
let min = small_int(program, at + 1)?;
if program.get(at + 2..at + 4)? != [OP_GREATERTHANOREQUAL, OP_VERIFY] {
return None;
}
if *program.get(at + 4)? != OP_DUP {
return None;
}
let max = small_int(program, at + 5)?;
if program.get(at + 6..at + 8)? != [OP_LESSTHANOREQUAL, OP_VERIFY] {
return None;
}
(min <= max).then_some((min, max))
}
fn count_site(program: &[u8], at: usize) -> Option<Authorised> {
match small_int(program, at + 1) {
Some(n) if program.get(at + 2..at + 4)? == [OP_NUMEQUAL, OP_VERIFY] => Some(Authorised::Exact(n)),
Some(fixed) if *program.get(at + 2)? == OP_SUB => {
range_bounds(program, at + 3).map(|(min, max)| Authorised::Range { fixed, min, max })
}
Some(_) => None,
None => range_bounds(program, at + 1).map(|(min, max)| Authorised::Range { fixed: 0, min, max }),
}
}
fn scan_branch(program: &[u8], body: usize) -> Option<(usize, Authorised)> {
let mut depth = 1usize;
let mut at = body;
let mut authorised: Option<Authorised> = None;
let mut literal: Vec<u8> = Vec::new();
let mut prev: Option<(usize, u8, bool)> = None;
let mut prev2: Option<(usize, u8, bool)> = None;
let mut continuation_checked = false;
let else_at = loop {
let (next, op, data) = step(program, at)?;
if data.is_none() {
let ranged = matches!(authorised, Some(Authorised::Range { .. }));
match op {
OP_IF | OP_NOTIF => depth += 1,
OP_ENDIF => {
depth = depth.checked_sub(1)?;
if depth == 0 {
return None;
}
}
OP_ELSE if depth == 1 => break at,
OP_AUTH_OUTPUT_COUNT if at >= 1 && program[at - 1] == OP_ACTIVE_INPUT_INDEX => {
match count_site(program, at) {
Some(site) => {
if depth != 1 || authorised.is_some() {
return None;
}
authorised = Some(site);
}
None if !continuation_checked
&& depth == 1
&& authorised.is_some()
&& at >= 2
&& prev == Some((at - 1, OP_ACTIVE_INPUT_INDEX, false))
&& prev2 == Some((at - 2, OP_COV_OUTPUT_COUNT, false))
&& program.get(at + 1..at + 3) == Some(&[OP_NUMEQUAL, OP_VERIFY][..]) =>
{
continuation_checked = true;
}
None if ranged => {}
None => return None,
}
}
OP_AUTH_OUTPUT_IDX => {
let literal_site = at >= 2
&& program[at - 2] == OP_ACTIVE_INPUT_INDEX
&& small_int(program, at - 1).is_some();
if literal_site {
if depth != 1 {
return None;
}
literal.push(small_int(program, at - 1)?);
} else if !ranged {
return None;
}
}
_ => {}
}
}
prev2 = prev;
prev = Some((at, op, data.is_some()));
at = next;
};
let authorised = authorised?;
let mut sorted = literal.clone();
sorted.sort_unstable();
let pinned_once = match authorised {
Authorised::Exact(n) => sorted == (0..n).collect::<Vec<u8>>(),
Authorised::Range { fixed, .. } => {
sorted.windows(2).all(|w| w[0] != w[1]) && sorted.iter().all(|j| *j < fixed)
}
};
pinned_once.then_some((else_at, authorised))
}
fn rung(program: &[u8], at: usize, index: usize) -> Option<(Dispatch, usize, Generation)> {
if *program.get(at)? == OP_DATA_4 {
let tag: [u8; 4] = program.get(at + 1..at + 5)?.try_into().ok()?;
if program.get(at + 5..at + 8)? != [OP_EQUAL, OP_IF, OP_DROP] {
return None;
}
return Some((Dispatch::Tag(tag), at + 8, Generation::V1));
}
let selector = u8::try_from(index).ok()?;
if small_int(program, at)? != selector {
return None;
}
if program.get(at + 1..at + 4)? != [OP_NUMEQUAL, OP_IF, OP_DROP] {
return None;
}
Some((Dispatch::Selector(selector), at + 4, Generation::Fork))
}
pub fn recognize_dispatch(program: &[u8]) -> Option<Argent> {
let (rungs, terminator) = dispatch_envelope(program)?;
if *program.first()? != OP_TOALTSTACK {
return None;
}
let mut at = 1usize;
let mut state_push_ends = Vec::new();
loop {
let (next, op, data) = step(program, at)?;
if op == OP_FROMALTSTACK {
break;
}
data?;
state_push_ends.push(next);
at = next;
}
let state_end = at;
at = state_end + 1;
let mut entrypoints: Vec<Entrypoint> = Vec::with_capacity(rungs);
let mut generation: Option<Generation> = None;
for index in 0..rungs {
if *program.get(at)? != OP_DUP {
return None;
}
let (dispatch, body, form) = rung(program, at + 1, index)?;
if generation.is_some_and(|g| g != form) {
return None;
}
generation = Some(form);
if entrypoints.iter().any(|e| e.dispatch == dispatch) {
return None;
}
let (else_at, authorised) = scan_branch(program, body)?;
entrypoints.push(Entrypoint {
dispatch,
authorised,
});
at = else_at + 1;
}
if at != program.len() - rungs - terminator.bytes().len() {
return None;
}
let generation = generation?;
if generation == Generation::V1 && terminator != Terminator::OpReturn {
return None;
}
let state_len = state_end - 1;
Some(Argent {
split: Split::Proven,
generation,
terminator: Some(terminator),
head_len: 1,
state_len,
code_len: program.len() - state_end,
entrypoints,
template_hash: generation.template_hash(&program[..1], &program[state_end..]),
state_push_ends,
})
}
pub fn recognize_leaf(program: &[u8]) -> Option<Argent> {
if *program.first()? == OP_TOALTSTACK {
return None;
}
let mut at = 0usize;
let mut state_push_ends = Vec::new();
loop {
let (next, _, data) = step(program, at)?;
if data.is_none() {
break;
}
state_push_ends.push(next);
at = next;
if at >= program.len() {
return None;
}
}
if state_push_ends.is_empty() {
return None;
}
let cut = at;
let mut authorised: Option<u8> = None;
let mut indices: Vec<u8> = Vec::new();
let mut scan = cut;
while scan < program.len() {
let (next, op, data) = step(program, scan)?;
if data.is_none() {
match op {
OP_AUTH_OUTPUT_COUNT
if scan >= 1
&& program[scan - 1] == OP_ACTIVE_INPUT_INDEX
&& authorised.is_none() =>
{
let n = small_int(program, scan + 1)?;
if program.get(scan + 2..scan + 4)? != [OP_NUMEQUAL, OP_VERIFY] {
return None;
}
authorised = Some(n);
}
OP_AUTH_OUTPUT_IDX => {
if scan < 2 || program[scan - 2] != OP_ACTIVE_INPUT_INDEX {
return None;
}
indices.push(small_int(program, scan - 1)?);
}
_ => {}
}
}
scan = next;
}
let authorised = authorised?;
let mut sorted = indices.clone();
sorted.sort_unstable();
if sorted != (0..authorised).collect::<Vec<u8>>() {
return None;
}
Some(Argent {
split: Split::Candidate,
generation: Generation::Fork,
terminator: None,
head_len: 0,
state_len: cut,
code_len: program.len() - cut,
entrypoints: vec![Entrypoint {
dispatch: Dispatch::Leaf,
authorised: Authorised::Exact(authorised),
}],
template_hash: template_hash(&[], &program[cut..]),
state_push_ends,
})
}
pub fn recognize(program: &[u8]) -> Option<Argent> {
recognize_dispatch(program).or_else(|| recognize_leaf(program))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_guard_is_part_of_the_template() {
let a = template_hash(&[OP_TOALTSTACK], &[OP_FROMALTSTACK, 0x51]);
let b = template_hash(&[], &[OP_FROMALTSTACK, 0x51]);
assert_ne!(a, b);
}
#[test]
fn the_two_arms_name_the_same_bytes_differently() {
let prefix = [OP_TOALTSTACK];
let suffix = [OP_FROMALTSTACK, 0x51];
assert_ne!(
Generation::Fork.template_hash(&prefix, &suffix),
Generation::V1.template_hash(&prefix, &suffix)
);
assert_eq!(Generation::Fork.template_hash(&prefix, &suffix), template_hash(&prefix, &suffix));
assert_eq!(
Generation::V1.template_hash(&prefix, &suffix),
crate::kcc1::template_hash(&prefix, &suffix)
);
assert_eq!((Generation::Fork.as_str(), Generation::V1.as_str()), ("fork", "1.0"));
}
#[test]
fn step_refuses_a_truncated_push() {
assert_eq!(step(&[0x02, 0xaa], 0), None);
assert_eq!(step(&[OP_PUSHDATA1], 0), None);
assert_eq!(step(&[OP_PUSHDATA2, 0x10, 0x00], 0), None);
assert_eq!(step(&[OP_0], 0), Some((1, OP_0, Some(0))));
assert_eq!(step(&[OP_1], 0), Some((1, OP_1, None)));
}
#[test]
fn small_int_covers_exactly_the_literal_opcodes() {
assert_eq!(small_int(&[OP_0], 0), Some(0));
assert_eq!(small_int(&[OP_1], 0), Some(1));
assert_eq!(small_int(&[OP_16], 0), Some(16));
assert_eq!(small_int(&[0x4f], 0), None); assert_eq!(small_int(&[0x01, 0x03], 0), None); assert_eq!(small_int(&[], 0), None);
}
#[test]
fn count_sites_read_exact_and_ranged_preambles() {
let exact = [OP_ACTIVE_INPUT_INDEX, OP_AUTH_OUTPUT_COUNT, 0x52, OP_NUMEQUAL, OP_VERIFY];
assert_eq!(count_site(&exact, 1), Some(Authorised::Exact(2)));
let ranged = [
OP_ACTIVE_INPUT_INDEX, OP_AUTH_OUTPUT_COUNT, 0x52, OP_SUB,
OP_DUP, 0x51, OP_GREATERTHANOREQUAL, OP_VERIFY,
OP_DUP, 0x53, OP_LESSTHANOREQUAL, OP_VERIFY,
];
assert_eq!(count_site(&ranged, 1), Some(Authorised::Range { fixed: 2, min: 1, max: 3 }));
let bare = [
OP_ACTIVE_INPUT_INDEX, OP_AUTH_OUTPUT_COUNT,
OP_DUP, 0x51, OP_GREATERTHANOREQUAL, OP_VERIFY,
OP_DUP, 0x53, OP_LESSTHANOREQUAL, OP_VERIFY,
];
assert_eq!(count_site(&bare, 1), Some(Authorised::Range { fixed: 0, min: 1, max: 3 }));
let operand = [OP_ACTIVE_INPUT_INDEX, OP_AUTH_OUTPUT_COUNT, 0x51, OP_SUB, OP_AUTH_OUTPUT_IDX];
assert_eq!(count_site(&operand, 1), None);
let inverted = [
OP_ACTIVE_INPUT_INDEX, OP_AUTH_OUTPUT_COUNT,
OP_DUP, 0x53, OP_GREATERTHANOREQUAL, OP_VERIFY,
OP_DUP, 0x51, OP_LESSTHANOREQUAL, OP_VERIFY,
];
assert_eq!(count_site(&inverted, 1), None);
}
#[test]
fn empty_and_tiny_inputs_are_refused_without_panicking() {
for program in [
&[][..],
&[OP_ENDIF][..],
&[OP_TOALTSTACK][..],
&[0x6a, 0x68][..],
&[OP_TOALTSTACK, OP_FROMALTSTACK, OP_DUP, OP_DATA_4][..],
] {
assert_eq!(recognize(program), None);
}
}
#[test]
fn a_state_window_is_a_run_of_pushes_and_nothing_else() {
let mut window = vec![0x20];
window.extend_from_slice(&[0u8; 32]);
window.extend_from_slice(&[0x08, 0x05, 0, 0, 0, 0, 0, 0, 0]);
window.push(OP_0);
let got = pushes(&window).expect("three pushes");
assert_eq!(got.len(), 3);
assert_eq!(got[0].len(), 32);
assert_eq!(script_num(got[1]), Some(5));
assert!(got[2].is_empty());
assert_eq!(script_num(got[2]), Some(0));
let mut broken = window.clone();
broken.push(OP_DUP);
assert_eq!(pushes(&broken), None);
assert_eq!(pushes(&[0x05, 0xaa]), None);
assert_eq!(pushes(&[]), None);
}
#[test]
fn script_numbers_read_as_the_engine_reads_them() {
assert_eq!(script_num(&[0x01]), Some(1));
assert_eq!(script_num(&[0x81]), Some(-1));
assert_eq!(script_num(&[0xff, 0x00]), Some(255));
assert_eq!(script_num(&[0xff, 0x80]), Some(-255));
assert_eq!(script_num(&[0, 0, 0, 0, 0, 0, 0, 0x40]), Some(1 << 62));
assert_eq!(script_num(&[0; 9]), None, "nine bytes is not a script number");
}
#[test]
fn fork_era_artifacts_commit_to_the_same_template_hash_the_chain_checks() {
for (fixture, contracts) in [
(
include_str!("../fixtures/argent/tickets.artifact.json"),
["Issuer", "Ticket"],
),
(
include_str!("../fixtures/argent/icc_kcc20_asset.artifact.json"),
["KCC20", "MinterProxy"],
),
] {
let doc: serde_json::Value = serde_json::from_str(fixture).unwrap();
let listed: Vec<&str> = doc["sil_abi"]["contracts"]
.as_array()
.unwrap()
.iter()
.map(|c| c["name"].as_str().unwrap())
.collect();
assert_eq!(listed, contracts);
for contract in doc["sil_abi"]["contracts"].as_array().unwrap() {
let script = hex::decode(contract["compiled"]["script_hex"].as_str().unwrap()).unwrap();
let span = &contract["compiled"]["state_span"];
let offset = span["offset"].as_u64().unwrap() as usize;
let len = span["len"].as_u64().unwrap() as usize;
let claimed = contract["compiled"]["template_hash_hex"].as_str().unwrap();
let ours = template_hash(&script[..offset], &script[offset + len..]);
assert_eq!(hex::encode(ours), claimed, "{}", contract["name"]);
let seen = recognize(&script).expect("argentc output is argent");
assert_eq!((seen.head_len, seen.state_len), (offset, len));
assert_eq!(seen.template_hash, ours);
assert_eq!(seen.generation, Generation::Fork);
assert_eq!(seen.terminator, None);
assert_eq!(seen.entrypoints[0].dispatch, Dispatch::Leaf);
assert!(pushes(&script[offset..offset + len]).is_some());
}
}
}
#[test]
fn the_rule_5_continuation_check_is_read_once_per_branch() {
const SITE: [u8; 5] = [0xd2, 0xb9, 0xcb, 0x9c, 0x69];
let doc: serde_json::Value =
serde_json::from_str(include_str!("../fixtures/argent/open_icc_core.v1.artifact.json")).unwrap();
let cell: Vec<u8> = doc["sil_abi"]["contracts"]["Cell"]["compiled"]["bytecode"]
.as_array()
.unwrap()
.iter()
.map(|b| b.as_u64().unwrap() as u8)
.collect();
let sites: Vec<usize> = cell.windows(5).enumerate().filter(|(_, w)| *w == SITE).map(|(i, _)| i).collect();
assert!(!sites.is_empty(), "the e76ee07 Cell carries the rule-5 check");
let seen = recognize(&cell).expect("a real e76ee07 build is read");
assert_eq!(seen.generation, Generation::V1);
assert_eq!(seen.entrypoint_count(), 2);
let mut doubled = cell.clone();
doubled.splice(sites[0] + 5..sites[0] + 5, SITE);
assert!(recognize(&doubled).is_none(), "a second rule-5 check in one branch is refused");
}
#[test]
fn v1_artifacts_commit_to_the_blake3_template_hash_the_program_checks() {
for (fixture, contracts) in [
(
include_str!("../fixtures/argent/tickets.v1.artifact.json"),
&["Issuer", "Ticket"][..],
),
(
include_str!("../fixtures/argent/icc_kcc20_asset.v1.artifact.json"),
&["KCC20", "MinterProxy"][..],
),
(
include_str!("../fixtures/argent/entry_range_outputs.v1.artifact.json"),
&["Account", "Batch"][..],
),
(
include_str!("../fixtures/argent/entry_range_only.v1.artifact.json"),
&["Account", "Batch"][..],
),
(
include_str!("../fixtures/argent/open_icc_core.v1.artifact.json"),
&["Cell"][..],
),
(
include_str!("../fixtures/argent/stones.v1.artifact.json"),
&["League", "Player", "StonesGame", "StonesSettle"][..],
),
(
include_str!("../fixtures/argent/entry_range_inputs.v1.artifact.json"),
&["Account", "Batch"][..],
),
] {
let doc: serde_json::Value = serde_json::from_str(fixture).unwrap();
let map = doc["sil_abi"]["contracts"].as_object().unwrap();
let listed: Vec<&str> = map.keys().map(String::as_str).collect();
assert_eq!(listed, contracts);
for (name, contract) in map {
let script: Vec<u8> = contract["compiled"]["bytecode"]
.as_array()
.unwrap()
.iter()
.map(|b| b.as_u64().unwrap() as u8)
.collect();
let claimed: Vec<u8> = contract["compiled"]["template_hash"]
.as_array()
.unwrap()
.iter()
.map(|b| b.as_u64().unwrap() as u8)
.collect();
let span = &contract["compiled"]["state_span"];
let offset = span["offset"].as_u64().unwrap() as usize;
let len = span["len"].as_u64().unwrap() as usize;
let seen = recognize(&script).unwrap_or_else(|| panic!("{name} is 1.0 argent"));
assert_eq!(seen.generation, Generation::V1, "{name}");
assert_eq!(seen.split, Split::Proven, "{name}");
assert_eq!(seen.terminator, Some(Terminator::OpReturn), "{name}");
assert_eq!((seen.head_len, seen.state_len), (offset, len), "{name}");
assert_eq!(seen.template_hash.as_slice(), claimed.as_slice(), "{name}");
assert_eq!(
seen.template_hash,
crate::kcc1::template_hash(&script[..offset], &script[offset + len..])
);
assert_ne!(
seen.template_hash,
template_hash(&script[..offset], &script[offset + len..]),
"{name}: the fork arm must not name a 1.0 build"
);
let entries = contract["entries"].as_object().unwrap();
assert_eq!(seen.entrypoint_count(), entries.len(), "{name}");
let mut declared: Vec<String> = entries
.values()
.map(|e| e["dispatch_tag"].as_str().unwrap().to_string())
.collect();
declared.sort();
let mut read: Vec<String> = seen
.entrypoints
.iter()
.map(|e| hex::encode(e.tag().expect("a 1.0 rung carries a tag")))
.collect();
read.sort();
assert_eq!(read, declared, "{name}");
assert!(seen.entrypoints.iter().all(|e| e.selector().is_none()));
}
}
let range: serde_json::Value =
serde_json::from_str(include_str!("../fixtures/argent/entry_range_outputs.v1.artifact.json")).unwrap();
let batch: Vec<u8> = range["sil_abi"]["contracts"]["Batch"]["compiled"]["bytecode"]
.as_array()
.unwrap()
.iter()
.map(|b| b.as_u64().unwrap() as u8)
.collect();
let seen = recognize(&batch).unwrap();
assert_eq!(
seen.entrypoints[0].authorised,
Authorised::Range { fixed: 2, min: 1, max: 3 },
"first and last are fixed, next is Account[1..=MAX_ACCOUNTS]"
);
assert_eq!(seen.entrypoints[0].authorised.exact(), None);
let account: Vec<u8> = range["sil_abi"]["contracts"]["Account"]["compiled"]["bytecode"]
.as_array()
.unwrap()
.iter()
.map(|b| b.as_u64().unwrap() as u8)
.collect();
assert_eq!(recognize(&account).unwrap().entrypoints[0].authorised, Authorised::Exact(0));
let only: serde_json::Value =
serde_json::from_str(include_str!("../fixtures/argent/entry_range_only.v1.artifact.json")).unwrap();
let batch: Vec<u8> = only["sil_abi"]["contracts"]["Batch"]["compiled"]["bytecode"]
.as_array()
.unwrap()
.iter()
.map(|b| b.as_u64().unwrap() as u8)
.collect();
assert_eq!(
recognize(&batch).unwrap().entrypoints[0].authorised,
Authorised::Range { fixed: 0, min: 1, max: 3 }
);
}
#[test]
fn each_v1_clause_is_load_bearing() {
let doc: serde_json::Value =
serde_json::from_str(include_str!("../fixtures/argent/tickets.v1.artifact.json")).unwrap();
let good: Vec<u8> = doc["sil_abi"]["contracts"]["Ticket"]["compiled"]["bytecode"]
.as_array()
.unwrap()
.iter()
.map(|b| b.as_u64().unwrap() as u8)
.collect();
let seen = recognize(&good).expect("1.0 argent");
let rung_at = 1 + seen.state_len + 1; assert_eq!(good[rung_at], OP_DUP);
assert_eq!(good[rung_at + 1], OP_DATA_4);
let mut wrong_compare = good.clone();
wrong_compare[rung_at + 6] = OP_NUMEQUAL;
assert_eq!(recognize(&wrong_compare), None);
let mut old_tail = good.clone();
let tail = old_tail.len() - 2; assert_eq!(&old_tail[tail..], &[OP_RETURN, OP_ENDIF]);
old_tail.splice(tail..tail + 1, [OP_DROP, OP_0, OP_VERIFY]);
assert_eq!(dispatch_envelope(&old_tail), Some((1, Terminator::DropFail)));
assert_eq!(recognize(&old_tail), None);
let mut no_guard = good.clone();
no_guard[0] = OP_DROP;
assert_eq!(recognize(&no_guard), None);
}
}