use crate::kcc20::blake2b_256;
use crate::p2sh_hash;
use std::collections::BTreeSet;
const PREFIX: &[u8] = include_bytes!("../fixtures/vesting_kron_prefix.bin");
const SUFFIX: &[u8] = include_bytes!("../fixtures/vesting_kron_suffix.bin");
pub const KRON_VESTING_TEMPLATE_HASH: [u8; 32] = [
0x4e, 0xd8, 0x40, 0x6f, 0x89, 0x60, 0x76, 0xf0, 0x33, 0x4f, 0xf2, 0x97, 0xd0, 0x21, 0x5d, 0x60,
0xdb, 0x16, 0x4d, 0xd8, 0x5e, 0x99, 0x5b, 0x62, 0x93, 0x91, 0xe4, 0xaf, 0x98, 0x42, 0xce, 0x18,
];
fn splice_state(
creator: &[u8; 32],
total: u64,
start: u64,
duration: u64,
claimed: u64,
) -> Vec<u8> {
let mut s = Vec::with_capacity(69);
s.push(0x20);
s.extend_from_slice(creator);
for v in [total, start, duration, claimed] {
s.push(0x08);
s.extend_from_slice(&v.to_le_bytes());
}
s
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct VestingState {
pub creator: [u8; 32],
pub total: u64,
pub start_score: u64,
pub duration_score: u64,
pub claimed: u64,
}
fn program_for_state(state: &VestingState) -> Vec<u8> {
let encoded = splice_state(
&state.creator,
state.total,
state.start_score,
state.duration_score,
state.claimed,
);
let mut program = Vec::with_capacity(PREFIX.len() + encoded.len() + SUFFIX.len());
program.extend_from_slice(PREFIX);
program.extend_from_slice(&encoded);
program.extend_from_slice(SUFFIX);
program
}
pub fn decode_state(program: &[u8]) -> Option<VestingState> {
const STATE_LEN: usize = 69;
if program.len() != PREFIX.len() + STATE_LEN + SUFFIX.len()
|| !program.starts_with(PREFIX)
|| !program.ends_with(SUFFIX)
{
return None;
}
let state = &program[PREFIX.len()..PREFIX.len() + STATE_LEN];
if state.first().copied() != Some(0x20) {
return None;
}
let creator = state.get(1..33)?.try_into().ok()?;
let mut at = 33usize;
let mut next_u64 = || {
if state.get(at).copied() != Some(0x08) {
return None;
}
let value = u64::from_le_bytes(state.get(at + 1..at + 9)?.try_into().ok()?);
at += 9;
Some(value)
};
let decoded = VestingState {
creator,
total: next_u64()?,
start_score: next_u64()?,
duration_score: next_u64()?,
claimed: next_u64()?,
};
(at == STATE_LEN && decoded.claimed <= decoded.total && decoded.duration_score > 0)
.then_some(decoded)
}
pub fn state_commitment(state: &VestingState) -> [u8; 32] {
blake2b_256(&program_for_state(state))
}
pub fn prove_state(output_spk: &[u8], state: &VestingState) -> bool {
let Some(want) = p2sh_hash(output_spk) else {
return false;
};
state_commitment(state) == want
}
pub fn recover_continuation_state(
output_spk: &[u8],
creator: &[u8; 32],
total: u64,
start_score: u64,
duration_score: u64,
witness: &[u8],
) -> Option<VestingState> {
let (instructions, _) = crate::disasm::disassemble(witness);
let mut candidates = BTreeSet::from([0u64]);
for inst in instructions {
let bytes = match (inst.opcode, inst.data) {
(_, Some(data)) if data.len() <= 8 => data,
(0x00, None) => Vec::new(),
(op @ 0x51..=0x60, None) => vec![op - 0x50],
_ => continue,
};
let mut padded = [0u8; 8];
padded[..bytes.len()].copy_from_slice(&bytes);
let value = u64::from_le_bytes(padded);
if value <= total {
candidates.insert(value);
}
}
let matches: Vec<_> = candidates
.into_iter()
.map(|claimed| VestingState {
creator: *creator,
total,
start_score,
duration_score,
claimed,
})
.filter(|state| prove_state(output_spk, state))
.collect();
let [only] = matches.as_slice() else {
return None;
};
Some(*only)
}
pub fn prove_genesis_lock(
output_spk: &[u8],
creator: &[u8; 32],
total: u64,
start_score: u64,
duration_score: u64,
) -> bool {
if duration_score == 0 {
return false;
}
let Some(want) = p2sh_hash(output_spk) else {
return false;
};
let state = VestingState {
creator: *creator,
total,
start_score,
duration_score,
claimed: 0,
};
blake2b_256(&program_for_state(&state)) == want
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_template_hashes_to_the_curve_pinned_value() {
let mut cat = Vec::with_capacity(PREFIX.len() + SUFFIX.len());
cat.extend_from_slice(PREFIX);
cat.extend_from_slice(SUFFIX);
assert_eq!(blake2b_256(&cat), KRON_VESTING_TEMPLATE_HASH);
}
const ANSEM_LOCK_SPK: [u8; 35] = [
0xaa, 0x20, 0x0b, 0x1f, 0xb6, 0xee, 0xd9, 0x44, 0xc6, 0xa1, 0xd3, 0x58, 0x1f, 0xaf, 0x3c,
0x42, 0x23, 0x00, 0xf3, 0x41, 0xf8, 0x49, 0x96, 0x82, 0x34, 0x9f, 0x0b, 0x55, 0x30, 0x1d,
0xe3, 0xcb, 0x6d, 0x38, 0x87,
];
const ANSEM_CREATOR: [u8; 32] = [
0x98, 0x8a, 0x0b, 0x5e, 0x4d, 0xc7, 0xe8, 0xa2, 0x44, 0x9d, 0x24, 0x95, 0x4b, 0x67, 0xf8,
0x2a, 0x30, 0x90, 0x79, 0xea, 0x83, 0x0d, 0x28, 0x64, 0x28, 0x1f, 0x86, 0xff, 0xb4, 0x94,
0xe5, 0x6a,
];
const ANSEM_TOTAL: u64 = 100_000_000;
const ANSEM_START: u64 = 499_658_470;
const ANSEM_DURATION: u64 = 298_796_626;
#[test]
fn the_ansem_mainnet_lock_proves_its_listed_creator() {
assert!(prove_genesis_lock(
&ANSEM_LOCK_SPK,
&ANSEM_CREATOR,
ANSEM_TOTAL,
ANSEM_START,
ANSEM_DURATION,
));
}
#[test]
fn a_wrong_claim_reproduces_nothing() {
let mut wrong_key = ANSEM_CREATOR;
wrong_key[0] ^= 1;
assert!(!prove_genesis_lock(
&ANSEM_LOCK_SPK,
&wrong_key,
ANSEM_TOTAL,
ANSEM_START,
ANSEM_DURATION,
));
assert!(!prove_genesis_lock(
&ANSEM_LOCK_SPK,
&ANSEM_CREATOR,
ANSEM_TOTAL,
ANSEM_START,
ANSEM_DURATION + 1,
));
}
#[test]
fn a_non_p2sh_output_fails_closed() {
let mut bare = vec![0x20];
bare.extend_from_slice(&ANSEM_CREATOR);
bare.push(0xac);
assert!(!prove_genesis_lock(
&bare,
&ANSEM_CREATOR,
ANSEM_TOTAL,
ANSEM_START,
ANSEM_DURATION,
));
}
fn spk_for(state: &VestingState) -> Vec<u8> {
let mut spk = vec![0xaa, 0x20];
spk.extend_from_slice(&blake2b_256(&program_for_state(state)));
spk.push(0x87);
spk
}
#[test]
fn a_revealed_state_round_trips_only_for_the_pinned_template() {
let state = VestingState {
creator: ANSEM_CREATOR,
total: ANSEM_TOTAL,
start_score: ANSEM_START,
duration_score: ANSEM_DURATION,
claimed: 12_345_678,
};
let program = program_for_state(&state);
assert_eq!(decode_state(&program), Some(state));
assert!(prove_state(&spk_for(&state), &state));
let mut other_build = program;
*other_build.last_mut().unwrap() ^= 1;
assert_eq!(decode_state(&other_build), None);
}
#[test]
fn a_live_continuation_is_recovered_only_by_commitment_match() {
let state = VestingState {
creator: ANSEM_CREATOR,
total: ANSEM_TOTAL,
start_score: ANSEM_START,
duration_score: ANSEM_DURATION,
claimed: 25_000_000,
};
let mut witness = vec![0x08];
witness.extend_from_slice(&state.claimed.to_le_bytes());
assert_eq!(
recover_continuation_state(
&spk_for(&state),
&state.creator,
state.total,
state.start_score,
state.duration_score,
&witness,
),
Some(state)
);
witness[1] ^= 1;
assert_eq!(
recover_continuation_state(
&spk_for(&state),
&state.creator,
state.total,
state.start_score,
state.duration_score,
&witness,
),
None
);
}
#[test]
fn a_zero_duration_schedule_is_never_admitted() {
let state = VestingState {
creator: ANSEM_CREATOR,
total: ANSEM_TOTAL,
start_score: ANSEM_START,
duration_score: 0,
claimed: 0,
};
assert!(!prove_genesis_lock(
&spk_for(&state),
&state.creator,
state.total,
state.start_score,
state.duration_score,
));
}
}