use std::sync::OnceLock;
use miden_protocol::account::AccountId;
use miden_protocol::asset::AssetAmount;
use miden_protocol::crypto::dsa::ecdsa_k256_keccak::PublicKey;
use miden_protocol::crypto::utils::Deserializable;
use miden_protocol::{Felt, Word};
use miden_standards::interop::eth::EthAmount;
use serde::Deserialize;
use crate::xreserve::encoding::CircleDomain;
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct VectorFile {
pub version: u32,
pub families: Families,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Families {
pub b32: Vec<B32Vector>,
pub amt: Vec<AmtVector>,
pub aid: Vec<AidVector>,
pub di: Vec<DiVector>,
pub att: Vec<AttVector>,
pub bn: Vec<BnVector>,
pub mi: Vec<MiVector>,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct B32Vector {
pub id: String,
pub tv: Vec<String>,
pub bytes32: String,
pub packed_felts: Vec<String>,
pub expected_key: [String; 4],
#[serde(default)]
pub lossless_error: Option<String>,
pub cite: String,
pub derivation: String,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct AmtVector {
pub id: String,
pub tv: Vec<String>,
pub kind: String,
#[serde(default)]
pub uint256_be: Option<String>,
#[serde(default)]
pub expected_y: Option<String>,
#[serde(default)]
pub expected_variant: Option<String>,
#[serde(default)]
pub mode: Option<String>,
pub cite: String,
pub derivation: String,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct AidVector {
pub id: String,
pub tv: Vec<String>,
pub bytes32: String,
#[serde(default)]
pub prefix_felt: Option<String>,
#[serde(default)]
pub suffix_felt: Option<String>,
#[serde(default)]
pub expected_variant: Option<String>,
pub cite: String,
pub derivation: String,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct DiVector {
pub id: String,
pub tv: Vec<String>,
pub kind: String,
#[serde(default)]
pub bytes_hex: Option<String>,
pub preimage_felts: Vec<String>,
pub len_felts: u64,
#[serde(default)]
pub staging_len_felts: Option<u64>,
#[serde(default)]
pub fields: Option<DiFields>,
#[serde(default)]
pub expected_variant: Option<String>,
#[serde(default)]
pub masm_err: Option<String>,
#[serde(default)]
pub mode: Option<String>,
pub cite: String,
pub derivation: String,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct DiFields {
pub magic: u32,
pub version: u32,
pub remote_domain: CircleDomain,
pub hook_data_len: u32,
pub amount: u64,
pub max_fee: u64,
pub amount_hex: String,
pub remote_token_hex: String,
pub remote_recipient_hex: String,
pub local_token_hex: String,
pub local_depositor_hex: String,
pub max_fee_hex: String,
pub nonce_hex: String,
pub packed: Vec<PackedField>,
pub remote_token_felts: Vec<String>,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct PackedField {
pub name: String,
pub felt_off: u64,
pub felts: Vec<String>,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct MiVector {
pub id: String,
pub tv: Vec<String>,
pub kind: String,
pub payload_hex: String,
pub faucet_prefix_felt: String,
pub faucet_suffix_felt: String,
pub remote_domain: CircleDomain,
#[serde(default)]
pub amount_felt: Option<String>,
#[serde(default)]
pub carried_felts: Vec<String>,
#[serde(default)]
pub rebuilt_preimage_felts: Vec<String>,
#[serde(default)]
pub expected_variant: Option<String>,
pub cite: String,
pub derivation: String,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct AttVector {
pub id: String,
pub tv: Vec<String>,
pub pubkey_hex: String,
pub packed_felts: Vec<String>,
pub expected_commitment: [String; 4],
pub digest_hex: String,
pub digest_felts: Vec<String>,
pub sig_hex: String,
pub sig_felts: Vec<String>,
pub v_byte: u8,
pub payload_hex: String,
pub cite: String,
pub derivation: String,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct BnVector {
pub id: String,
pub tv: Vec<String>,
pub kind: String,
#[serde(default)]
pub dest_domain: Option<CircleDomain>,
#[serde(default)]
pub dest_recipient: Option<String>,
pub items: Vec<String>,
#[serde(default)]
pub expected_variant: Option<String>,
pub cite: String,
pub derivation: String,
}
static FILE: OnceLock<VectorFile> = OnceLock::new();
pub fn load() -> &'static VectorFile {
FILE.get_or_init(|| {
let path = crate::vectors_path();
let raw = std::fs::read_to_string(&path)
.unwrap_or_else(|e| panic!("cannot read canonical vector artifact {path:?}: {e}"));
serde_json::from_str(&raw)
.unwrap_or_else(|e| panic!("canonical vector artifact does not parse: {e}"))
})
}
pub fn parse_hex32(s: &str) -> [u8; 32] {
let bytes = parse_hex(s);
bytes
.as_slice()
.try_into()
.unwrap_or_else(|_| panic!("expected 32 bytes, got {}", bytes.len()))
}
pub fn parse_hex(s: &str) -> Vec<u8> {
let s = s.strip_prefix("0x").unwrap_or(s);
(0..s.len())
.step_by(2)
.map(|i| u8::from_str_radix(&s[i..i + 2], 16).expect("valid hex"))
.collect()
}
pub fn felt_from_hex(s: &str) -> Felt {
let v = u64::from_str_radix(s.strip_prefix("0x").expect("0x-prefixed felt"), 16)
.expect("valid u64 hex");
Felt::try_from(v).expect("felt within the field")
}
pub fn word_from_hex(w: &[String; 4]) -> Word {
Word::new([
felt_from_hex(&w[0]),
felt_from_hex(&w[1]),
felt_from_hex(&w[2]),
felt_from_hex(&w[3]),
])
}
impl B32Vector {
pub fn bytes32(&self) -> [u8; 32] {
parse_hex32(&self.bytes32)
}
pub fn packed_felts_values(&self) -> Vec<Felt> {
self.packed_felts.iter().map(|s| felt_from_hex(s)).collect()
}
}
impl AmtVector {
pub fn amount(&self) -> EthAmount {
EthAmount::new(parse_hex32(self.uint256_be.as_deref().expect("vector carries uint256_be")))
}
pub fn expected_amount(&self) -> miden_protocol::asset::AssetAmount {
let y: u64 = self.expected_y.as_deref().expect("accept vector").parse().expect("u64");
miden_protocol::asset::AssetAmount::new(y).expect("vector amount within bounds")
}
}
impl AidVector {
pub fn expected_felts(&self) -> [Felt; 2] {
[
felt_from_hex(self.prefix_felt.as_deref().expect("prefix felt")),
felt_from_hex(self.suffix_felt.as_deref().expect("suffix felt")),
]
}
}
impl DiVector {
pub fn bytes(&self) -> Vec<u8> {
parse_hex(self.bytes_hex.as_deref().expect("byte-level vector"))
}
pub fn preimage_values(&self) -> Vec<Felt> {
self.preimage_felts.iter().map(|s| felt_from_hex(s)).collect()
}
}
impl MiVector {
pub fn payload(&self) -> Vec<u8> {
parse_hex(&self.payload_hex)
}
pub fn faucet_id(&self) -> AccountId {
AccountId::try_from_elements(
felt_from_hex(&self.faucet_suffix_felt),
felt_from_hex(&self.faucet_prefix_felt),
)
.expect("the vector's faucet id is canonical")
}
pub fn amount(&self) -> AssetAmount {
let felt = felt_from_hex(self.amount_felt.as_deref().expect("accept vector"));
AssetAmount::new(felt.as_canonical_u64()).expect("the vector's amount is in range")
}
pub fn carried_values(&self) -> Vec<Felt> {
self.carried_felts.iter().map(|s| felt_from_hex(s)).collect()
}
pub fn rebuilt_preimage_values(&self) -> Vec<Felt> {
self.rebuilt_preimage_felts.iter().map(|s| felt_from_hex(s)).collect()
}
}
impl AttVector {
pub fn pubkey(&self) -> [u8; 33] {
let b = parse_hex(&self.pubkey_hex);
b.as_slice()
.try_into()
.unwrap_or_else(|_| panic!("expected 33 bytes, got {}", b.len()))
}
pub fn digest(&self) -> [u8; 32] {
parse_hex32(&self.digest_hex)
}
pub fn public_key(&self) -> PublicKey {
PublicKey::read_from_bytes(&self.pubkey()).unwrap_or_else(|e| {
panic!("vector {}: pubkey does not decode to a curve point: {e}", self.id)
})
}
pub fn sig(&self) -> [u8; 65] {
let b = parse_hex(&self.sig_hex);
b.as_slice()
.try_into()
.unwrap_or_else(|_| panic!("expected 65 bytes, got {}", b.len()))
}
pub fn payload(&self) -> Vec<u8> {
parse_hex(&self.payload_hex)
}
pub fn packed_felts_values(&self) -> Vec<Felt> {
self.packed_felts.iter().map(|s| felt_from_hex(s)).collect()
}
pub fn digest_felts_values(&self) -> Vec<Felt> {
self.digest_felts.iter().map(|s| felt_from_hex(s)).collect()
}
pub fn sig_felts_values(&self) -> Vec<Felt> {
self.sig_felts.iter().map(|s| felt_from_hex(s)).collect()
}
pub fn expected_commitment_word(&self) -> Word {
word_from_hex(&self.expected_commitment)
}
}
impl DiFields {
pub fn bytes32(&self, name: &str) -> [u8; 32] {
let hex = match name {
"amount" => &self.amount_hex,
"remote_token" => &self.remote_token_hex,
"remote_recipient" => &self.remote_recipient_hex,
"local_token" => &self.local_token_hex,
"local_depositor" => &self.local_depositor_hex,
"max_fee" => &self.max_fee_hex,
"nonce" => &self.nonce_hex,
other => panic!("unknown bytes32 field {other}"),
};
parse_hex32(hex)
}
}
impl BnVector {
pub fn dest_recipient(&self) -> crate::xreserve::encoding::ForeignChainAddress {
crate::xreserve::encoding::ForeignChainAddress::new(parse_hex32(
self.dest_recipient.as_deref().expect("accept vector carries dest_recipient"),
))
}
pub fn items_words(&self) -> Vec<Word> {
let felts: Vec<Felt> = self.items.iter().map(|s| felt_from_hex(s)).collect();
let (words, remainder) = felts.as_chunks::<{ Word::NUM_ELEMENTS }>();
assert!(remainder.is_empty(), "{}: items are a whole number of words", self.id);
words.iter().map(|chunk| Word::new(*chunk)).collect()
}
pub fn expected_struct(&self) -> crate::xreserve::encoding::XReserveBurnItems {
crate::xreserve::encoding::XReserveBurnItems {
dest_domain: self.dest_domain.expect("accept vector carries dest_domain"),
dest_recipient: self.dest_recipient(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn artifact_guard() {
const TV_TAG_ALLOWLIST: [&str; 3] =
["amt-guard-limb-not-u32", "amt-rej-witness-over", "amt-rej-witness-under"];
let v = load();
assert_eq!(v.version, 1);
assert_eq!(v.families.b32.len(), 4, "b32 family");
assert_eq!(v.families.amt.len(), 4, "amt family");
assert_eq!(v.families.aid.len(), 5, "aid family");
assert_eq!(v.families.di.len(), 10, "di family");
assert_eq!(v.families.att.len(), 4, "att family");
assert_eq!(v.families.bn.len(), 8, "bn family");
assert_eq!(v.families.mi.len(), 6, "mi family");
let no_provenance = |cite: &str, derivation: &str| cite.is_empty() || derivation.is_empty();
let tv_ok = |id: &str, tv: &[String]| !tv.is_empty() || TV_TAG_ALLOWLIST.contains(&id);
let kind_ok = |kind: &str| kind == "accept" || kind == "reject";
for e in &v.families.b32 {
assert!(!no_provenance(&e.cite, &e.derivation), "{} provenance", e.id);
assert!(tv_ok(&e.id, &e.tv), "{}: empty tv tags and not allowlisted", e.id);
}
for e in &v.families.amt {
assert!(kind_ok(&e.kind), "{}: kind", e.id);
assert!(!no_provenance(&e.cite, &e.derivation), "{} provenance", e.id);
assert!(tv_ok(&e.id, &e.tv), "{}: empty tv tags and not allowlisted", e.id);
}
for e in &v.families.aid {
assert!(!no_provenance(&e.cite, &e.derivation), "{} provenance", e.id);
assert!(tv_ok(&e.id, &e.tv), "{}: empty tv tags and not allowlisted", e.id);
}
for e in &v.families.di {
assert!(kind_ok(&e.kind), "{}: kind", e.id);
assert!(!no_provenance(&e.cite, &e.derivation), "{} provenance", e.id);
assert!(tv_ok(&e.id, &e.tv), "{}: empty tv tags and not allowlisted", e.id);
}
for e in &v.families.att {
assert!(!no_provenance(&e.cite, &e.derivation), "{} provenance", e.id);
assert!(tv_ok(&e.id, &e.tv), "{}: empty tv tags and not allowlisted", e.id);
}
for e in &v.families.bn {
assert!(kind_ok(&e.kind), "{}: kind", e.id);
assert!(!no_provenance(&e.cite, &e.derivation), "{} provenance", e.id);
assert!(tv_ok(&e.id, &e.tv), "{}: empty tv tags and not allowlisted", e.id);
}
for e in &v.families.mi {
assert!(kind_ok(&e.kind), "{}: kind", e.id);
assert!(!no_provenance(&e.cite, &e.derivation), "{} provenance", e.id);
assert!(tv_ok(&e.id, &e.tv), "{}: empty tv tags and not allowlisted", e.id);
}
}
}