#![no_std]
pub mod accounts;
pub mod anchor_idl;
pub mod clientgen;
pub mod codama;
pub mod python_client;
pub mod rust_client;
use core::fmt;
use hopper_core::account::HEADER_LEN;
use hopper_core::field_map::FieldInfo;
use hopper_runtime::layout::LayoutInfo;
use hopper_runtime::{AccountView, LayoutContract};
#[cfg(feature = "receipt")]
pub use hopper_core::receipt::{
CompatImpact, DecodedReceipt, NarrativeRisk, Phase, ReceiptExplain, ReceiptNarrative,
};
#[cfg(feature = "policy")]
pub use hopper_core::policy::PolicyClass;
pub const MANIFEST_SEED: &[u8] = b"hopper:manifest";
pub const MANIFEST_MAGIC: [u8; 8] = *b"HOPRMNFT";
pub const MANIFEST_HEADER_LEN: usize = 20;
pub const MANIFEST_VERSION: u32 = 1;
pub const MANIFEST_COMPRESS_NONE: u8 = 0;
pub const MANIFEST_COMPRESS_ZLIB: u8 = 1;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[repr(u8)]
pub enum FieldIntent {
Balance = 0,
Authority = 1,
Timestamp = 2,
Counter = 3,
Index = 4,
BasisPoints = 5,
Flag = 6,
Address = 7,
Hash = 8,
PDASeed = 9,
Version = 10,
Bump = 11,
Nonce = 12,
Supply = 13,
Limit = 14,
Threshold = 15,
Owner = 16,
Delegate = 17,
Status = 18,
Custom = 255,
}
impl FieldIntent {
pub fn name(self) -> &'static str {
match self {
Self::Balance => "balance",
Self::Authority => "authority",
Self::Timestamp => "timestamp",
Self::Counter => "counter",
Self::Index => "index",
Self::BasisPoints => "basis_points",
Self::Flag => "flag",
Self::Address => "address",
Self::Hash => "hash",
Self::PDASeed => "pda_seed",
Self::Version => "version",
Self::Bump => "bump",
Self::Nonce => "nonce",
Self::Supply => "supply",
Self::Limit => "limit",
Self::Threshold => "threshold",
Self::Owner => "owner",
Self::Delegate => "delegate",
Self::Status => "status",
Self::Custom => "custom",
}
}
pub fn is_monetary(self) -> bool {
matches!(self, Self::Balance | Self::BasisPoints | Self::Supply)
}
pub fn is_identity(self) -> bool {
matches!(
self,
Self::Authority | Self::Address | Self::Owner | Self::Delegate
)
}
pub fn is_authority_sensitive(self) -> bool {
matches!(self, Self::Authority | Self::Owner | Self::Delegate)
}
pub fn is_init_only(self) -> bool {
matches!(self, Self::PDASeed | Self::Bump)
}
pub fn is_governance(self) -> bool {
matches!(self, Self::Threshold | Self::Limit | Self::Status)
}
}
impl fmt::Display for FieldIntent {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.name())
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[repr(u8)]
pub enum MutationClass {
ReadOnly = 0,
AppendOnly = 1,
InPlace = 2,
Resizing = 3,
AuthoritySensitive = 4,
Financial = 5,
StateTransition = 6,
}
impl MutationClass {
pub const fn name(self) -> &'static str {
match self {
Self::ReadOnly => "read-only",
Self::AppendOnly => "append-only",
Self::InPlace => "in-place",
Self::Resizing => "resizing",
Self::AuthoritySensitive => "authority-sensitive",
Self::Financial => "financial",
Self::StateTransition => "state-transition",
}
}
pub const fn is_mutating(self) -> bool {
!matches!(self, Self::ReadOnly)
}
pub const fn requires_snapshot(self) -> bool {
!matches!(self, Self::ReadOnly)
}
pub const fn requires_authority(self) -> bool {
matches!(
self,
Self::AuthoritySensitive | Self::Financial | Self::Resizing | Self::StateTransition
)
}
}
impl fmt::Display for MutationClass {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.name())
}
}
#[derive(Clone, Copy, Debug)]
pub struct LayoutBehavior {
pub requires_signer: bool,
pub affects_balance: bool,
pub affects_authority: bool,
pub mutation_class: MutationClass,
}
impl LayoutBehavior {
pub const READ_ONLY: Self = Self {
requires_signer: false,
affects_balance: false,
affects_authority: false,
mutation_class: MutationClass::ReadOnly,
};
pub const STANDARD: Self = Self {
requires_signer: true,
affects_balance: false,
affects_authority: false,
mutation_class: MutationClass::InPlace,
};
pub const FINANCIAL: Self = Self {
requires_signer: true,
affects_balance: true,
affects_authority: false,
mutation_class: MutationClass::Financial,
};
pub const APPEND_ONLY: Self = Self {
requires_signer: true,
affects_balance: false,
affects_authority: false,
mutation_class: MutationClass::AppendOnly,
};
}
impl fmt::Display for LayoutBehavior {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "mutation={}", self.mutation_class)?;
if self.requires_signer {
write!(f, " signer")?;
}
if self.affects_balance {
write!(f, " balance")?;
}
if self.affects_authority {
write!(f, " authority")?;
}
Ok(())
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[repr(u8)]
pub enum LayoutStabilityGrade {
Stable = 0,
Evolving = 1,
MigrationSensitive = 2,
UnsafeToEvolve = 3,
}
impl LayoutStabilityGrade {
pub const fn name(self) -> &'static str {
match self {
Self::Stable => "stable",
Self::Evolving => "evolving",
Self::MigrationSensitive => "migration-sensitive",
Self::UnsafeToEvolve => "unsafe-to-evolve",
}
}
pub fn compute(manifest: &LayoutManifest) -> Self {
let mut authority_count = 0u16;
let mut financial_count = 0u16;
let mut init_only_count = 0u16;
let mut has_custom = false;
let mut i = 0;
while i < manifest.field_count {
let intent = manifest.fields[i].intent;
if intent.is_authority_sensitive() {
authority_count += 1;
}
if intent.is_monetary() {
financial_count += 1;
}
if intent.is_init_only() {
init_only_count += 1;
}
if matches!(intent, FieldIntent::Custom) {
has_custom = true;
}
i += 1;
}
if authority_count > 2 && financial_count > 2 {
return Self::UnsafeToEvolve;
}
if authority_count > 1 || financial_count > 2 {
return Self::MigrationSensitive;
}
if has_custom && manifest.field_count > 8 {
return Self::Evolving;
}
if init_only_count > 0 {
return Self::Stable;
}
Self::Evolving
}
}
impl fmt::Display for LayoutStabilityGrade {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.name())
}
}
#[derive(Clone, Copy, Debug)]
pub struct FieldDescriptor {
pub name: &'static str,
pub canonical_type: &'static str,
pub size: u16,
pub offset: u16,
pub intent: FieldIntent,
}
#[derive(Clone, Copy, Debug)]
pub struct LayoutManifest {
pub name: &'static str,
pub disc: u8,
pub version: u8,
pub layout_id: [u8; 8],
pub total_size: usize,
pub field_count: usize,
pub fields: &'static [FieldDescriptor],
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct LayoutFingerprint {
pub wire_hash: [u8; 8],
pub semantic_hash: [u8; 8],
}
impl LayoutFingerprint {
pub const fn from_manifest(manifest: &LayoutManifest) -> Self {
Self {
wire_hash: manifest.layout_id,
semantic_hash: Self::compute_semantic(manifest.fields),
}
}
pub const fn is_identical(&self, other: &Self) -> bool {
let mut i = 0;
while i < 8 {
if self.wire_hash[i] != other.wire_hash[i] {
return false;
}
if self.semantic_hash[i] != other.semantic_hash[i] {
return false;
}
i += 1;
}
true
}
pub const fn wire_matches_but_semantics_differ(&self, other: &Self) -> bool {
let mut wire_eq = true;
let mut sem_eq = true;
let mut i = 0;
while i < 8 {
if self.wire_hash[i] != other.wire_hash[i] {
wire_eq = false;
}
if self.semantic_hash[i] != other.semantic_hash[i] {
sem_eq = false;
}
i += 1;
}
wire_eq && !sem_eq
}
const fn compute_semantic(fields: &[FieldDescriptor]) -> [u8; 8] {
const FNV_OFFSET: u64 = 0xcbf29ce484222325;
const FNV_PRIME: u64 = 0x00000100000001B3;
let mut hash = FNV_OFFSET;
let mut i = 0;
while i < fields.len() {
let name = fields[i].name.as_bytes();
let mut j = 0;
while j < name.len() {
hash ^= name[j] as u64;
hash = hash.wrapping_mul(FNV_PRIME);
j += 1;
}
let ty = fields[i].canonical_type.as_bytes();
j = 0;
while j < ty.len() {
hash ^= ty[j] as u64;
hash = hash.wrapping_mul(FNV_PRIME);
j += 1;
}
hash ^= fields[i].size as u64;
hash = hash.wrapping_mul(FNV_PRIME);
hash ^= fields[i].offset as u64;
hash = hash.wrapping_mul(FNV_PRIME);
hash ^= fields[i].intent as u8 as u64;
hash = hash.wrapping_mul(FNV_PRIME);
i += 1;
}
hash.to_le_bytes()
}
}
impl fmt::Display for LayoutFingerprint {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "wire=")?;
let mut i = 0;
while i < 8 {
let _ = write!(f, "{:02x}", self.wire_hash[i]);
i += 1;
}
write!(f, " sem=")?;
i = 0;
while i < 8 {
let _ = write!(f, "{:02x}", self.semantic_hash[i]);
i += 1;
}
Ok(())
}
}
#[inline]
pub fn is_append_compatible(older: &LayoutManifest, newer: &LayoutManifest) -> bool {
older.disc == newer.disc
&& newer.version > older.version
&& newer.total_size >= older.total_size
&& older.layout_id != newer.layout_id
}
#[inline]
pub fn requires_migration(older: &LayoutManifest, newer: &LayoutManifest) -> bool {
older.disc == newer.disc && older.layout_id != newer.layout_id
}
#[inline]
pub fn is_backward_readable(older: &LayoutManifest, newer: &LayoutManifest) -> bool {
if older.disc != newer.disc {
return false;
}
if newer.field_count < older.field_count {
return false;
}
let mut i = 0;
while i < older.field_count {
let old_f = &older.fields[i];
if i >= newer.field_count {
return false;
}
let new_f = &newer.fields[i];
if !const_str_eq(old_f.name, new_f.name)
|| !const_str_eq(old_f.canonical_type, new_f.canonical_type)
|| old_f.size != new_f.size
{
return false;
}
i += 1;
}
true
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum CompatibilityVerdict {
Identical,
WireCompatible,
AppendSafe,
MigrationRequired,
Incompatible,
}
impl CompatibilityVerdict {
#[inline]
pub fn between(older: &LayoutManifest, newer: &LayoutManifest) -> Self {
if older.layout_id == newer.layout_id {
return Self::Identical;
}
if older.disc != newer.disc {
return Self::Incompatible;
}
let backward = is_backward_readable(older, newer);
if backward
&& older.field_count == newer.field_count
&& older.total_size == newer.total_size
{
return Self::WireCompatible;
}
if backward {
Self::AppendSafe
} else {
Self::MigrationRequired
}
}
#[inline]
pub const fn name(self) -> &'static str {
match self {
Self::Identical => "identical",
Self::WireCompatible => "wire-compatible",
Self::AppendSafe => "append-safe",
Self::MigrationRequired => "migration-required",
Self::Incompatible => "incompatible",
}
}
#[inline]
pub const fn is_safe(self) -> bool {
matches!(
self,
Self::Identical | Self::WireCompatible | Self::AppendSafe
)
}
#[inline]
pub const fn is_backward_readable(self) -> bool {
matches!(
self,
Self::Identical | Self::WireCompatible | Self::AppendSafe
)
}
#[inline]
pub const fn requires_migration(self) -> bool {
matches!(self, Self::MigrationRequired)
}
pub fn refine_with_roles<const N: usize>(self, report: &SegmentMigrationReport<N>) -> Self {
match self {
Self::MigrationRequired => {
let mut i = 0;
let mut all_soft = true;
while i < report.count {
let adv = &report.advice[i];
if adv.must_preserve && !adv.clearable && !adv.rebuildable {
all_soft = false;
break;
}
i += 1;
}
if all_soft && report.count > 0 {
Self::AppendSafe
} else {
self
}
}
Self::AppendSafe => {
let mut i = 0;
while i < report.count {
if report.advice[i].immutable {
return Self::MigrationRequired;
}
i += 1;
}
self
}
_ => self,
}
}
}
impl fmt::Display for CompatibilityVerdict {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.name())
}
}
pub struct CompatibilityExplain {
pub verdict: CompatibilityVerdict,
pub added_fields: [&'static str; 16],
pub added_count: u8,
pub removed_fields: [&'static str; 16],
pub removed_count: u8,
pub changed_fields: [&'static str; 16],
pub changed_count: u8,
pub semantic_drift: bool,
pub summary: &'static str,
}
impl CompatibilityExplain {
pub fn between(older: &LayoutManifest, newer: &LayoutManifest) -> Self {
let verdict = CompatibilityVerdict::between(older, newer);
let mut added = [""; 16];
let mut added_n = 0u8;
let mut removed = [""; 16];
let mut removed_n = 0u8;
let mut changed = [""; 16];
let mut changed_n = 0u8;
let shared = if older.field_count < newer.field_count {
older.field_count
} else {
newer.field_count
};
let mut i = 0;
while i < shared {
let old_f = &older.fields[i];
let new_f = &newer.fields[i];
let name_eq = const_str_eq(old_f.name, new_f.name);
let type_eq = const_str_eq(old_f.canonical_type, new_f.canonical_type);
let size_eq = old_f.size == new_f.size;
if !(name_eq && type_eq && size_eq) {
if (changed_n as usize) < 16 {
changed[changed_n as usize] = old_f.name;
changed_n += 1;
}
}
i += 1;
}
while i < newer.field_count {
if (added_n as usize) < 16 {
added[added_n as usize] = newer.fields[i].name;
added_n += 1;
}
i += 1;
}
let mut j = shared;
while j < older.field_count {
if (removed_n as usize) < 16 {
removed[removed_n as usize] = older.fields[j].name;
removed_n += 1;
}
j += 1;
}
let fp_old = LayoutFingerprint::from_manifest(older);
let fp_new = LayoutFingerprint::from_manifest(newer);
let semantic_drift = fp_old.wire_matches_but_semantics_differ(&fp_new);
let summary = match verdict {
CompatibilityVerdict::Identical => "Layouts are byte-identical. No action needed.",
CompatibilityVerdict::WireCompatible => {
if semantic_drift {
"Wire layout matches but field semantics changed. Review field intents."
} else {
"Wire layout matches with metadata-only changes. Safe to deploy."
}
}
CompatibilityVerdict::AppendSafe => {
"New fields appended at the end. Old readers still work."
}
CompatibilityVerdict::MigrationRequired => {
"Breaking field changes. Migration instruction required before deploy."
}
CompatibilityVerdict::Incompatible => {
"Different discriminators. These are unrelated account types."
}
};
Self {
verdict,
added_fields: added,
added_count: added_n,
removed_fields: removed,
removed_count: removed_n,
changed_fields: changed,
changed_count: changed_n,
semantic_drift,
summary,
}
}
}
impl fmt::Display for CompatibilityExplain {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "Verdict: {} ({})", self.verdict.name(), self.summary)?;
if self.added_count > 0 {
write!(f, " Added:")?;
let mut i = 0;
while i < self.added_count as usize {
write!(f, " {}", self.added_fields[i])?;
i += 1;
}
writeln!(f)?;
}
if self.removed_count > 0 {
write!(f, " Removed:")?;
let mut i = 0;
while i < self.removed_count as usize {
write!(f, " {}", self.removed_fields[i])?;
i += 1;
}
writeln!(f)?;
}
if self.changed_count > 0 {
write!(f, " Changed:")?;
let mut i = 0;
while i < self.changed_count as usize {
write!(f, " {}", self.changed_fields[i])?;
i += 1;
}
writeln!(f)?;
}
if self.semantic_drift {
writeln!(
f,
" Warning: semantic drift detected (wire matches but meaning changed)"
)?;
}
Ok(())
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum FieldCompat {
Identical,
Changed,
Added,
Removed,
}
#[inline]
pub fn compare_fields<'a, const N: usize>(
older: &'a LayoutManifest,
newer: &'a LayoutManifest,
) -> FieldCompatReport<'a, N> {
let mut report = FieldCompatReport {
entries: [FieldCompatEntry {
name: "",
status: FieldCompat::Identical,
}; N],
count: 0,
is_append_safe: true,
};
let shared = if older.field_count < newer.field_count {
older.field_count
} else {
newer.field_count
};
let mut i = 0;
while i < shared && report.count < N {
let old_f = &older.fields[i];
let new_f = &newer.fields[i];
let name_eq = const_str_eq(old_f.name, new_f.name);
let type_eq = const_str_eq(old_f.canonical_type, new_f.canonical_type);
let size_eq = old_f.size == new_f.size;
let status = if name_eq && type_eq && size_eq {
FieldCompat::Identical
} else {
report.is_append_safe = false;
FieldCompat::Changed
};
report.entries[report.count] = FieldCompatEntry {
name: old_f.name,
status,
};
report.count += 1;
i += 1;
}
while i < newer.field_count && report.count < N {
report.entries[report.count] = FieldCompatEntry {
name: newer.fields[i].name,
status: FieldCompat::Added,
};
report.count += 1;
i += 1;
}
let mut j = shared;
while j < older.field_count && report.count < N {
report.entries[report.count] = FieldCompatEntry {
name: older.fields[j].name,
status: FieldCompat::Removed,
};
report.count += 1;
report.is_append_safe = false;
j += 1;
}
report
}
#[derive(Clone, Copy)]
pub struct FieldCompatEntry<'a> {
pub name: &'a str,
pub status: FieldCompat,
}
pub struct FieldCompatReport<'a, const N: usize> {
pub entries: [FieldCompatEntry<'a>; N],
pub count: usize,
pub is_append_safe: bool,
}
impl<'a, const N: usize> FieldCompatReport<'a, N> {
#[inline(always)]
pub fn len(&self) -> usize {
self.count
}
#[inline(always)]
pub fn is_empty(&self) -> bool {
self.count == 0
}
#[inline(always)]
pub fn get(&self, i: usize) -> Option<&FieldCompatEntry<'a>> {
if i < self.count {
Some(&self.entries[i])
} else {
None
}
}
#[inline(always)]
pub fn is_append_safe(&self) -> bool {
self.is_append_safe
}
#[inline]
pub fn count_status(&self, status: FieldCompat) -> usize {
let mut n = 0;
let mut i = 0;
while i < self.count {
if self.entries[i].status == status {
n += 1;
}
i += 1;
}
n
}
}
#[derive(Clone, Copy)]
pub struct DecodedHeader {
pub disc: u8,
pub version: u8,
pub flags: u16,
pub layout_id: [u8; 8],
pub reserved: [u8; 4],
}
#[inline]
pub fn decode_header(data: &[u8]) -> Option<DecodedHeader> {
if data.len() < HEADER_LEN {
return None;
}
Some(DecodedHeader {
disc: data[0],
version: data[1],
flags: u16::from_le_bytes([data[2], data[3]]),
layout_id: [
data[4], data[5], data[6], data[7], data[8], data[9], data[10], data[11],
],
reserved: [data[12], data[13], data[14], data[15]],
})
}
#[inline]
pub fn identify_account<'a>(
data: &[u8],
manifests: &'a [LayoutManifest],
) -> Option<(usize, &'a LayoutManifest)> {
let header = decode_header(data)?;
let mut i = 0;
while i < manifests.len() {
let m = &manifests[i];
if m.disc == header.disc && m.layout_id == header.layout_id {
return Some((i, m));
}
i += 1;
}
None
}
#[derive(Clone, Copy)]
pub struct DecodedSegment {
pub id: [u8; 4],
pub offset: u32,
pub size: u32,
pub flags: u16,
pub version: u8,
}
#[inline]
pub fn decode_segments<const N: usize>(data: &[u8]) -> Option<(usize, [DecodedSegment; N])> {
let registry_start = HEADER_LEN;
if data.len() < registry_start + 4 {
return None;
}
let count = u16::from_le_bytes([data[registry_start], data[registry_start + 1]]) as usize;
if count > N {
return None;
}
let entries_start = registry_start + 4;
let mut segments = [DecodedSegment {
id: [0; 4],
offset: 0,
size: 0,
flags: 0,
version: 0,
}; N];
let mut i = 0;
while i < count {
let off = entries_start + i * 16;
if off + 16 > data.len() {
return None;
}
segments[i] = DecodedSegment {
id: [data[off], data[off + 1], data[off + 2], data[off + 3]],
offset: u32::from_le_bytes([
data[off + 4],
data[off + 5],
data[off + 6],
data[off + 7],
]),
size: u32::from_le_bytes([
data[off + 8],
data[off + 9],
data[off + 10],
data[off + 11],
]),
flags: u16::from_le_bytes([data[off + 12], data[off + 13]]),
version: data[off + 14],
};
i += 1;
}
Some((count, segments))
}
pub struct ManifestRegistry<const N: usize> {
manifests: [Option<LayoutManifest>; N],
count: usize,
}
impl<const N: usize> ManifestRegistry<N> {
#[inline(always)]
pub const fn empty() -> Self {
Self {
manifests: [None; N],
count: 0,
}
}
#[inline]
pub const fn from_slice(manifests: &[LayoutManifest]) -> Self {
let mut reg = Self::empty();
let mut i = 0;
while i < manifests.len() && i < N {
reg.manifests[i] = Some(manifests[i]);
reg.count += 1;
i += 1;
}
reg
}
#[inline(always)]
pub const fn len(&self) -> usize {
self.count
}
#[inline(always)]
pub const fn is_empty(&self) -> bool {
self.count == 0
}
#[inline]
pub fn identify(&self, data: &[u8]) -> Option<(usize, &LayoutManifest)> {
let header = decode_header(data)?;
let mut i = 0;
while i < self.count {
if let Some(m) = &self.manifests[i] {
if m.disc == header.disc && m.layout_id == header.layout_id {
return Some((i, m));
}
}
i += 1;
}
None
}
#[inline]
pub fn get(&self, index: usize) -> Option<&LayoutManifest> {
if index < self.count {
self.manifests[index].as_ref()
} else {
None
}
}
#[inline]
pub fn find_by_disc(&self, disc: u8) -> Option<&LayoutManifest> {
let mut i = 0;
while i < self.count {
if let Some(m) = &self.manifests[i] {
if m.disc == disc {
return Some(m);
}
}
i += 1;
}
None
}
#[inline]
pub fn find_by_layout_id(&self, layout_id: &[u8; 8]) -> Option<&LayoutManifest> {
let mut i = 0;
while i < self.count {
if let Some(m) = &self.manifests[i] {
if &m.layout_id == layout_id {
return Some(m);
}
}
i += 1;
}
None
}
}
#[inline]
fn const_str_eq(a: &str, b: &str) -> bool {
let a = a.as_bytes();
let b = b.as_bytes();
if a.len() != b.len() {
return false;
}
let mut i = 0;
while i < a.len() {
if a[i] != b[i] {
return false;
}
i += 1;
}
true
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum MigrationPolicy {
NoOp,
AppendOnly,
RequiresMigration,
Incompatible,
}
#[derive(Clone, Copy)]
pub struct MigrationStep<'a> {
pub action: MigrationAction,
pub field: &'a str,
pub offset: u16,
pub size: u16,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum MigrationAction {
CopyPrefix,
ZeroInit,
UpdateHeader,
Realloc,
}
pub struct MigrationPlan<'a, const N: usize> {
pub policy: MigrationPolicy,
pub steps: [MigrationStep<'a>; N],
pub step_count: usize,
pub old_size: usize,
pub new_size: usize,
pub copy_bytes: usize,
pub zero_bytes: usize,
pub backward_readable: bool,
}
impl<'a, const N: usize> MigrationPlan<'a, N> {
pub fn generate(older: &'a LayoutManifest, newer: &'a LayoutManifest) -> Self {
let mut plan = Self {
policy: MigrationPolicy::NoOp,
steps: [MigrationStep {
action: MigrationAction::CopyPrefix,
field: "",
offset: 0,
size: 0,
}; N],
step_count: 0,
old_size: older.total_size,
new_size: newer.total_size,
copy_bytes: 0,
zero_bytes: 0,
backward_readable: is_backward_readable(older, newer),
};
if older.layout_id == newer.layout_id {
plan.policy = MigrationPolicy::NoOp;
return plan;
}
if older.disc != newer.disc {
plan.policy = MigrationPolicy::Incompatible;
return plan;
}
let report = compare_fields::<32>(older, newer);
if !report.is_append_safe {
plan.policy = MigrationPolicy::RequiresMigration;
} else {
plan.policy = MigrationPolicy::AppendOnly;
}
let mut compatible_end: u16 = HEADER_LEN as u16;
let mut i = 0;
while i < report.count {
if report.entries[i].status == FieldCompat::Identical {
let mut j = 0;
while j < older.field_count {
if const_str_eq(older.fields[j].name, report.entries[i].name) {
let field_end = older.fields[j].offset + older.fields[j].size;
if field_end > compatible_end {
compatible_end = field_end;
}
break;
}
j += 1;
}
}
i += 1;
}
if compatible_end > HEADER_LEN as u16 && plan.step_count < N {
let copy_size = compatible_end - HEADER_LEN as u16;
plan.steps[plan.step_count] = MigrationStep {
action: MigrationAction::CopyPrefix,
field: "",
offset: HEADER_LEN as u16,
size: copy_size,
};
plan.copy_bytes = copy_size as usize;
plan.step_count += 1;
}
if newer.total_size != older.total_size && plan.step_count < N {
plan.steps[plan.step_count] = MigrationStep {
action: MigrationAction::Realloc,
field: "",
offset: 0,
size: newer.total_size as u16,
};
plan.step_count += 1;
}
i = 0;
while i < report.count {
if report.entries[i].status == FieldCompat::Added && plan.step_count < N {
let mut j = 0;
while j < newer.field_count {
if const_str_eq(newer.fields[j].name, report.entries[i].name) {
plan.steps[plan.step_count] = MigrationStep {
action: MigrationAction::ZeroInit,
field: report.entries[i].name,
offset: newer.fields[j].offset,
size: newer.fields[j].size,
};
plan.zero_bytes += newer.fields[j].size as usize;
plan.step_count += 1;
break;
}
j += 1;
}
}
i += 1;
}
if plan.step_count < N {
plan.steps[plan.step_count] = MigrationStep {
action: MigrationAction::UpdateHeader,
field: "",
offset: 0,
size: HEADER_LEN as u16,
};
plan.step_count += 1;
}
plan
}
#[inline(always)]
pub fn len(&self) -> usize {
self.step_count
}
#[inline(always)]
pub fn is_empty(&self) -> bool {
self.step_count == 0
}
#[inline(always)]
pub fn requires_data_copy(&self) -> bool {
self.policy == MigrationPolicy::RequiresMigration
}
#[inline(always)]
pub fn step(&self, i: usize) -> Option<&MigrationStep<'a>> {
if i < self.step_count {
Some(&self.steps[i])
} else {
None
}
}
#[inline]
pub fn for_each_step<F: FnMut(usize, &MigrationStep<'a>)>(&self, mut f: F) {
let mut i = 0;
while i < self.step_count {
f(i, &self.steps[i]);
i += 1;
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[repr(u8)]
pub enum SegmentRoleHint {
Core = 0,
Extension = 1,
Journal = 2,
Index = 3,
Cache = 4,
Audit = 5,
Shard = 6,
Unclassified = 7,
}
impl SegmentRoleHint {
#[inline(always)]
pub fn from_flags(flags: u16) -> Self {
match (flags >> 12) & 0x0F {
0 => Self::Core,
1 => Self::Extension,
2 => Self::Journal,
3 => Self::Index,
4 => Self::Cache,
5 => Self::Audit,
6 => Self::Shard,
_ => Self::Unclassified,
}
}
#[inline(always)]
pub fn name(self) -> &'static str {
match self {
Self::Core => "Core",
Self::Extension => "Extension",
Self::Journal => "Journal",
Self::Index => "Index",
Self::Cache => "Cache",
Self::Audit => "Audit",
Self::Shard => "Shard",
Self::Unclassified => "Unclassified",
}
}
#[inline(always)]
pub fn must_preserve(self) -> bool {
matches!(
self,
Self::Core | Self::Extension | Self::Audit | Self::Shard
)
}
#[inline(always)]
pub fn is_rebuildable(self) -> bool {
matches!(self, Self::Index | Self::Cache)
}
#[inline(always)]
pub fn is_clearable(self) -> bool {
matches!(self, Self::Journal | Self::Index | Self::Cache)
}
#[inline(always)]
pub fn is_append_only(self) -> bool {
matches!(self, Self::Journal | Self::Audit)
}
#[inline(always)]
pub fn is_immutable(self) -> bool {
matches!(self, Self::Audit)
}
#[inline(always)]
pub fn requires_migration_copy(self) -> bool {
matches!(self, Self::Core | Self::Audit)
}
#[inline(always)]
pub fn is_safe_to_drop(self) -> bool {
matches!(self, Self::Cache)
}
}
#[derive(Clone, Copy)]
pub struct SegmentAdvice {
pub id: [u8; 4],
pub size: u32,
pub role: SegmentRoleHint,
pub must_preserve: bool,
pub clearable: bool,
pub rebuildable: bool,
pub append_only: bool,
pub immutable: bool,
}
pub struct SegmentMigrationReport<const N: usize> {
pub advice: [SegmentAdvice; N],
pub count: usize,
pub preserve_bytes: u32,
pub clearable_bytes: u32,
pub rebuildable_bytes: u32,
}
impl<const N: usize> SegmentMigrationReport<N> {
pub fn analyze(segments: &[DecodedSegment], count: usize) -> Self {
let mut report = Self {
advice: [SegmentAdvice {
id: [0; 4],
size: 0,
role: SegmentRoleHint::Unclassified,
must_preserve: false,
clearable: false,
rebuildable: false,
append_only: false,
immutable: false,
}; N],
count: 0,
preserve_bytes: 0,
clearable_bytes: 0,
rebuildable_bytes: 0,
};
let mut i = 0;
while i < count && i < N {
let seg = &segments[i];
let role = SegmentRoleHint::from_flags(seg.flags);
report.advice[i] = SegmentAdvice {
id: seg.id,
size: seg.size,
role,
must_preserve: role.must_preserve(),
clearable: role.is_clearable(),
rebuildable: role.is_rebuildable(),
append_only: role.is_append_only(),
immutable: role.is_immutable(),
};
if role.must_preserve() {
report.preserve_bytes += seg.size;
}
if role.is_clearable() {
report.clearable_bytes += seg.size;
}
if role.is_rebuildable() {
report.rebuildable_bytes += seg.size;
}
report.count += 1;
i += 1;
}
report
}
pub fn must_preserve_count(&self) -> usize {
let mut n = 0;
let mut i = 0;
while i < self.count {
if self.advice[i].must_preserve {
n += 1;
}
i += 1;
}
n
}
pub fn clearable_count(&self) -> usize {
let mut n = 0;
let mut i = 0;
while i < self.count {
if self.advice[i].clearable {
n += 1;
}
i += 1;
}
n
}
}
impl<const N: usize> fmt::Display for SegmentMigrationReport<N> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "Segment Migration Advice ({} segments):", self.count)?;
let mut i = 0;
while i < self.count {
let a = &self.advice[i];
write!(f, " [{}] {} ({} bytes):", i, a.role.name(), a.size)?;
if a.must_preserve {
write!(f, " MUST-PRESERVE")?;
}
if a.clearable {
write!(f, " clearable")?;
}
if a.rebuildable {
write!(f, " rebuildable")?;
}
if a.append_only {
write!(f, " append-only")?;
}
if a.immutable {
write!(f, " immutable")?;
}
writeln!(f)?;
i += 1;
}
writeln!(
f,
" preserve={} bytes, clearable={} bytes, rebuildable={} bytes",
self.preserve_bytes, self.clearable_bytes, self.rebuildable_bytes
)?;
Ok(())
}
}
impl fmt::Display for DecodedHeader {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"Header {{ disc: {}, ver: {}, flags: 0x{:04x}, layout_id: ",
self.disc, self.version, self.flags,
)?;
write_hex(f, &self.layout_id)?;
write!(f, " }}")
}
}
impl fmt::Debug for DecodedHeader {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"DecodedHeader {{ disc: {}, version: {}, flags: 0x{:04x}, layout_id: ",
self.disc, self.version, self.flags,
)?;
write_hex(f, &self.layout_id)?;
write!(f, ", reserved: ")?;
write_hex(f, &self.reserved)?;
write!(f, " }}")
}
}
impl fmt::Display for DecodedSegment {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Segment {{ id: ")?;
write_hex(f, &self.id)?;
write!(
f,
", offset: {}, size: {}, flags: 0x{:04x}, ver: {} }}",
self.offset, self.size, self.flags, self.version,
)
}
}
impl fmt::Debug for DecodedSegment {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "DecodedSegment {{ id: ")?;
write_hex(f, &self.id)?;
write!(
f,
", offset: {}, size: {}, flags: 0x{:04x}, version: {} }}",
self.offset, self.size, self.flags, self.version,
)
}
}
impl fmt::Display for FieldCompat {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
FieldCompat::Identical => write!(f, "identical"),
FieldCompat::Changed => write!(f, "changed"),
FieldCompat::Added => write!(f, "added"),
FieldCompat::Removed => write!(f, "removed"),
}
}
}
impl fmt::Display for MigrationPolicy {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
MigrationPolicy::NoOp => write!(f, "no-op"),
MigrationPolicy::AppendOnly => write!(f, "append-only"),
MigrationPolicy::RequiresMigration => write!(f, "requires-migration"),
MigrationPolicy::Incompatible => write!(f, "incompatible"),
}
}
}
impl fmt::Display for MigrationAction {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
MigrationAction::CopyPrefix => write!(f, "copy-prefix"),
MigrationAction::ZeroInit => write!(f, "zero-init"),
MigrationAction::UpdateHeader => write!(f, "update-header"),
MigrationAction::Realloc => write!(f, "realloc"),
}
}
}
impl<'a> fmt::Display for MigrationStep<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{} @ offset={}, size={}",
self.action, self.offset, self.size
)?;
if !self.field.is_empty() {
write!(f, " (field: {})", self.field)?;
}
Ok(())
}
}
impl<'a, const N: usize> fmt::Display for MigrationPlan<'a, N> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "MigrationPlan ({}):", self.policy)?;
writeln!(
f,
" old_size={}, new_size={}",
self.old_size, self.new_size
)?;
writeln!(
f,
" copy={} bytes, zero={} bytes",
self.copy_bytes, self.zero_bytes
)?;
let mut i = 0;
while i < self.step_count {
writeln!(f, " step {}: {}", i, self.steps[i])?;
i += 1;
}
Ok(())
}
}
impl fmt::Display for LayoutManifest {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(
f,
"{} v{} (disc={}, size={})",
self.name, self.version, self.disc, self.total_size
)?;
write!(f, " layout_id: ")?;
write_hex(f, &self.layout_id)?;
writeln!(f)?;
let mut i = 0;
while i < self.field_count {
let field = &self.fields[i];
writeln!(
f,
" [{:>3}..{:>3}] {} : {} ({} bytes)",
field.offset,
field.offset + field.size,
field.name,
field.canonical_type,
field.size,
)?;
i += 1;
}
Ok(())
}
}
pub fn format_header(data: &[u8]) -> Option<DecodedHeader> {
decode_header(data)
}
pub fn format_segment_map<const N: usize>(data: &[u8]) -> Option<SegmentMap<N>> {
let (count, segments) = decode_segments::<N>(data)?;
Some(SegmentMap { count, segments })
}
pub struct SegmentMap<const N: usize> {
pub count: usize,
pub segments: [DecodedSegment; N],
}
impl<const N: usize> fmt::Display for SegmentMap<N> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "Segment Map ({} segments):", self.count)?;
let reg_end = HEADER_LEN + 4 + self.count * 16;
writeln!(f, " [ 0..{:>3}] Header", HEADER_LEN)?;
writeln!(f, " [{:>3}..{:>3}] Registry", HEADER_LEN, reg_end)?;
let mut i = 0;
while i < self.count {
let seg = &self.segments[i];
let end = seg.offset + seg.size;
write!(f, " [{:>3}..{:>3}] Segment {} (id=", seg.offset, end, i)?;
write_hex(f, &seg.id)?;
writeln!(f, ", {} bytes, v{})", seg.size, seg.version)?;
i += 1;
}
Ok(())
}
}
fn write_hex(f: &mut fmt::Formatter<'_>, bytes: &[u8]) -> fmt::Result {
for b in bytes {
write!(f, "{:02x}", b)?;
}
Ok(())
}
#[derive(Clone, Copy, Debug)]
pub struct AccountEntry {
pub name: &'static str,
pub writable: bool,
pub signer: bool,
pub layout_ref: &'static str,
}
#[derive(Clone, Copy, Debug)]
pub struct ArgDescriptor {
pub name: &'static str,
pub canonical_type: &'static str,
pub size: u16,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ArgParseError {
TooShort {
required: u16,
got: u16,
},
}
impl fmt::Display for ArgParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ArgParseError::TooShort { required, got } => {
write!(f, "args: too short (required {}, got {})", required, got)
}
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct ErrorDescriptor {
pub name: &'static str,
pub code: u32,
pub invariant: &'static str,
pub doc: &'static str,
}
#[derive(Clone, Copy, Debug)]
pub struct ConstantDescriptor {
pub name: &'static str,
pub ty: &'static str,
pub value: &'static str,
pub docs: &'static str,
}
#[derive(Clone, Copy, Debug)]
pub struct ErrorRegistry {
pub enum_name: &'static str,
pub errors: &'static [ErrorDescriptor],
}
impl ErrorRegistry {
pub fn find_by_code(&self, code: u32) -> Option<&ErrorDescriptor> {
let mut i = 0;
while i < self.errors.len() {
if self.errors[i].code == code {
return Some(&self.errors[i]);
}
i += 1;
}
None
}
pub fn invariant_for(&self, code: u32) -> Option<&'static str> {
self.find_by_code(code).and_then(|d| {
if d.invariant.is_empty() {
None
} else {
Some(d.invariant)
}
})
}
}
#[derive(Clone, Copy, Debug)]
pub struct InstructionDescriptor {
pub name: &'static str,
pub tag: u8,
pub args: &'static [ArgDescriptor],
pub accounts: &'static [AccountEntry],
pub capabilities: &'static [&'static str],
pub policy_pack: &'static str,
pub receipt_expected: bool,
}
#[derive(Clone, Copy)]
pub struct EventDescriptor {
pub name: &'static str,
pub tag: u8,
pub fields: &'static [FieldDescriptor],
}
#[derive(Clone, Copy)]
pub struct PolicyDescriptor {
pub name: &'static str,
pub capabilities: &'static [&'static str],
pub requirements: &'static [&'static str],
pub invariants: &'static [&'static str],
pub receipt_profile: &'static str,
}
#[derive(Clone, Copy)]
pub struct LayoutMetadata {
pub name: &'static str,
pub segment_roles: &'static [&'static str],
pub append_safe: bool,
pub migration_required: bool,
pub rebuildable: bool,
pub policy_pack: &'static str,
pub invariant_pack: &'static [&'static str],
pub receipt_profile: &'static str,
pub phase_requirements: &'static [&'static str],
pub trust_profile: &'static str,
pub manager_hints: &'static [&'static str],
}
#[derive(Clone, Copy)]
pub struct CompatibilityPair {
pub from_layout: &'static str,
pub from_version: u8,
pub to_layout: &'static str,
pub to_version: u8,
pub policy: MigrationPolicy,
pub backward_readable: bool,
}
#[derive(Clone, Copy)]
pub struct ProgramManifest {
pub name: &'static str,
pub version: &'static str,
pub description: &'static str,
pub layouts: &'static [LayoutManifest],
pub layout_metadata: &'static [LayoutMetadata],
pub instructions: &'static [InstructionDescriptor],
pub events: &'static [EventDescriptor],
pub policies: &'static [PolicyDescriptor],
pub compatibility_pairs: &'static [CompatibilityPair],
pub tooling_hints: &'static [&'static str],
pub contexts: &'static [crate::accounts::ContextDescriptor],
}
#[derive(Clone, Copy)]
pub struct PdaSeedHint {
pub kind: &'static str,
pub value: &'static str,
}
#[derive(Clone, Copy)]
pub struct IdlAccountEntry {
pub name: &'static str,
pub writable: bool,
pub signer: bool,
pub layout_ref: &'static str,
pub pda_seeds: &'static [PdaSeedHint],
}
#[derive(Clone, Copy)]
pub struct IdlInstructionDescriptor {
pub name: &'static str,
pub tag: u8,
pub args: &'static [ArgDescriptor],
pub accounts: &'static [IdlAccountEntry],
}
#[derive(Clone, Copy)]
pub struct ProgramIdl {
pub name: &'static str,
pub version: &'static str,
pub description: &'static str,
pub instructions: &'static [IdlInstructionDescriptor],
pub accounts: &'static [LayoutManifest],
pub events: &'static [EventDescriptor],
pub fingerprints: &'static [([u8; 8], &'static str)],
}
impl ProgramIdl {
pub const fn empty() -> Self {
Self {
name: "",
version: "",
description: "",
instructions: &[],
accounts: &[],
events: &[],
fingerprints: &[],
}
}
pub const fn instruction_count(&self) -> usize {
self.instructions.len()
}
pub const fn account_count(&self) -> usize {
self.accounts.len()
}
pub fn find_instruction(&self, name: &str) -> Option<&IdlInstructionDescriptor> {
let mut i = 0;
while i < self.instructions.len() {
if const_str_eq(self.instructions[i].name, name) {
return Some(&self.instructions[i]);
}
i += 1;
}
None
}
pub fn find_account(&self, name: &str) -> Option<&LayoutManifest> {
let mut i = 0;
while i < self.accounts.len() {
if const_str_eq(self.accounts[i].name, name) {
return Some(&self.accounts[i]);
}
i += 1;
}
None
}
}
impl fmt::Display for ProgramIdl {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "IDL: {} {}", self.name, self.version)?;
if !self.description.is_empty() {
writeln!(f, " {}", self.description)?;
}
writeln!(f)?;
writeln!(f, "Instructions ({}):", self.instructions.len())?;
for ix in self.instructions.iter() {
write!(
f,
" {:>2} {:16} args={} accounts={}",
ix.tag,
ix.name,
ix.args.len(),
ix.accounts.len()
)?;
writeln!(f)?;
}
writeln!(f)?;
writeln!(f, "Accounts ({}):", self.accounts.len())?;
for a in self.accounts.iter() {
write!(
f,
" {:16} disc={} v{} {} bytes id=",
a.name, a.disc, a.version, a.total_size
)?;
write_hex(f, &a.layout_id)?;
writeln!(f)?;
}
if !self.events.is_empty() {
writeln!(f)?;
writeln!(f, "Events ({}):", self.events.len())?;
for e in self.events.iter() {
writeln!(f, " {:>2} {:16} fields={}", e.tag, e.name, e.fields.len())?;
}
}
Ok(())
}
}
#[derive(Clone, Copy)]
pub struct CodamaInstruction {
pub name: &'static str,
pub discriminator: u8,
pub args: &'static [ArgDescriptor],
pub accounts: &'static [IdlAccountEntry],
}
#[derive(Clone, Copy)]
pub struct CodamaAccount {
pub name: &'static str,
pub discriminator: u8,
pub size: usize,
pub fields: &'static [FieldDescriptor],
}
#[derive(Clone, Copy)]
pub struct CodamaEvent {
pub name: &'static str,
pub discriminator: u8,
pub fields: &'static [FieldDescriptor],
}
#[derive(Clone, Copy)]
pub struct CodamaProjection {
pub name: &'static str,
pub version: &'static str,
pub instructions: &'static [CodamaInstruction],
pub accounts: &'static [CodamaAccount],
pub events: &'static [CodamaEvent],
}
impl CodamaProjection {
pub const fn empty() -> Self {
Self {
name: "",
version: "",
instructions: &[],
accounts: &[],
events: &[],
}
}
}
impl fmt::Display for CodamaProjection {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "Codama: {} {}", self.name, self.version)?;
writeln!(f)?;
writeln!(f, "Instructions ({}):", self.instructions.len())?;
for ix in self.instructions.iter() {
writeln!(
f,
" {:>2} {:16} args={} accounts={}",
ix.discriminator,
ix.name,
ix.args.len(),
ix.accounts.len()
)?;
}
writeln!(f)?;
writeln!(f, "Accounts ({}):", self.accounts.len())?;
for a in self.accounts.iter() {
writeln!(
f,
" {:16} disc={} {} bytes fields={}",
a.name,
a.discriminator,
a.size,
a.fields.len()
)?;
}
if !self.events.is_empty() {
writeln!(f)?;
writeln!(f, "Events ({}):", self.events.len())?;
for e in self.events.iter() {
writeln!(
f,
" {:>2} {:16} fields={}",
e.discriminator,
e.name,
e.fields.len()
)?;
}
}
Ok(())
}
}
impl ProgramManifest {
pub const fn empty() -> Self {
Self {
name: "",
version: "",
description: "",
layouts: &[],
layout_metadata: &[],
instructions: &[],
events: &[],
policies: &[],
compatibility_pairs: &[],
tooling_hints: &[],
contexts: &[],
}
}
pub const fn layout_count(&self) -> usize {
self.layouts.len()
}
pub const fn instruction_count(&self) -> usize {
self.instructions.len()
}
pub fn find_layout_by_disc(&self, disc: u8) -> Option<&LayoutManifest> {
let mut i = 0;
while i < self.layouts.len() {
if self.layouts[i].disc == disc {
return Some(&self.layouts[i]);
}
i += 1;
}
None
}
pub fn find_layout_by_id(&self, layout_id: &[u8; 8]) -> Option<&LayoutManifest> {
let mut i = 0;
while i < self.layouts.len() {
if self.layouts[i].layout_id == *layout_id {
return Some(&self.layouts[i]);
}
i += 1;
}
None
}
pub fn identify_from_data(&self, data: &[u8]) -> Option<&LayoutManifest> {
let header = decode_header(data)?;
if let Some(m) = self.find_layout_by_id(&header.layout_id) {
return Some(m);
}
self.find_layout_by_disc(header.disc)
}
pub fn find_instruction(&self, tag: u8) -> Option<&InstructionDescriptor> {
let mut i = 0;
while i < self.instructions.len() {
if self.instructions[i].tag == tag {
return Some(&self.instructions[i]);
}
i += 1;
}
None
}
pub fn find_policy(&self, name: &str) -> Option<&PolicyDescriptor> {
let mut i = 0;
while i < self.policies.len() {
if self.policies[i].name == name {
return Some(&self.policies[i]);
}
i += 1;
}
None
}
pub fn find_layout_metadata(&self, name: &str) -> Option<&LayoutMetadata> {
let mut i = 0;
while i < self.layout_metadata.len() {
if const_str_eq(self.layout_metadata[i].name, name) {
return Some(&self.layout_metadata[i]);
}
i += 1;
}
None
}
pub fn find_compat_pair(
&self,
from_name: &str,
from_ver: u8,
to_name: &str,
to_ver: u8,
) -> Option<&CompatibilityPair> {
let mut i = 0;
while i < self.compatibility_pairs.len() {
let cp = &self.compatibility_pairs[i];
if const_str_eq(cp.from_layout, from_name)
&& cp.from_version == from_ver
&& const_str_eq(cp.to_layout, to_name)
&& cp.to_version == to_ver
{
return Some(cp);
}
i += 1;
}
None
}
}
impl fmt::Display for ProgramManifest {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "Program: {} {}", self.name, self.version)?;
if !self.description.is_empty() {
writeln!(f, " {}", self.description)?;
}
writeln!(f)?;
writeln!(f, "Layouts ({}):", self.layouts.len())?;
for m in self.layouts.iter() {
write!(
f,
" {:16} v{} disc={} {} bytes fingerprint=",
m.name, m.version, m.disc, m.total_size
)?;
write_hex(f, &m.layout_id)?;
if let Some(meta) = self.find_layout_metadata(m.name) {
if !meta.trust_profile.is_empty() {
write!(f, " trust={}", meta.trust_profile)?;
}
if meta.append_safe {
write!(f, " append-safe")?;
}
if meta.migration_required {
write!(f, " migration-required")?;
}
}
writeln!(f)?;
}
writeln!(f)?;
writeln!(f, "Instructions ({}):", self.instructions.len())?;
for ix in self.instructions.iter() {
write!(
f,
" {:>2} {:16} accounts={}",
ix.tag,
ix.name,
ix.accounts.len()
)?;
if !ix.capabilities.is_empty() {
write!(f, " caps=")?;
for (j, c) in ix.capabilities.iter().enumerate() {
if j > 0 {
write!(f, ",")?;
}
write!(f, "{}", c)?;
}
}
if ix.receipt_expected {
write!(f, " receipt=yes")?;
}
writeln!(f)?;
}
writeln!(f)?;
if !self.policies.is_empty() {
writeln!(f, "Policies ({}):", self.policies.len())?;
for p in self.policies.iter() {
write!(f, " {:24}", p.name)?;
for (j, r) in p.requirements.iter().enumerate() {
if j > 0 {
write!(f, " + ")?;
}
write!(f, "{}", r)?;
}
if !p.receipt_profile.is_empty() {
write!(f, " receipt={}", p.receipt_profile)?;
}
writeln!(f)?;
}
writeln!(f)?;
}
if !self.events.is_empty() {
writeln!(f, "Events ({}):", self.events.len())?;
for e in self.events.iter() {
writeln!(f, " {:>2} {:16} fields={}", e.tag, e.name, e.fields.len())?;
}
writeln!(f)?;
}
if !self.compatibility_pairs.is_empty() {
writeln!(f, "Compatibility ({}):", self.compatibility_pairs.len())?;
for cp in self.compatibility_pairs.iter() {
writeln!(
f,
" {} v{} -> {} v{} {}{}",
cp.from_layout,
cp.from_version,
cp.to_layout,
cp.to_version,
cp.policy,
if cp.backward_readable {
" backward-readable"
} else {
""
},
)?;
}
}
Ok(())
}
}
pub struct DecodedField<'a> {
pub name: &'a str,
pub canonical_type: &'a str,
pub raw: &'a [u8],
pub offset: u16,
pub size: u16,
}
impl<'a> DecodedField<'a> {
pub fn format_value(&self, buf: &mut [u8]) -> usize {
match self.canonical_type {
"WireU64" | "LeU64" if self.raw.len() >= 8 => {
let v = u64::from_le_bytes([
self.raw[0],
self.raw[1],
self.raw[2],
self.raw[3],
self.raw[4],
self.raw[5],
self.raw[6],
self.raw[7],
]);
format_u64(v, buf)
}
"WireU32" | "LeU32" if self.raw.len() >= 4 => {
let v =
u32::from_le_bytes([self.raw[0], self.raw[1], self.raw[2], self.raw[3]]) as u64;
format_u64(v, buf)
}
"WireU16" | "LeU16" if self.raw.len() >= 2 => {
let v = u16::from_le_bytes([self.raw[0], self.raw[1]]) as u64;
format_u64(v, buf)
}
"WireBool" | "LeBool" if !self.raw.is_empty() => {
if self.raw[0] != 0 {
let len = 4usize.min(buf.len());
buf[..len].copy_from_slice(&b"true"[..len]);
len
} else {
let len = 5usize.min(buf.len());
buf[..len].copy_from_slice(&b"false"[..len]);
len
}
}
"u8" if self.raw.len() == 1 => format_u64(self.raw[0] as u64, buf),
_ if self.size == 32 => {
format_hex_truncated(self.raw, buf)
}
_ => format_hex_truncated(self.raw, buf),
}
}
}
pub fn decode_account_fields<'a, const N: usize>(
data: &'a [u8],
manifest: &'a LayoutManifest,
) -> (usize, [Option<DecodedField<'a>>; N]) {
let mut fields: [Option<DecodedField<'a>>; N] = [const { None }; N];
let count = manifest.field_count.min(N);
let mut i = 0;
while i < count {
let fd = &manifest.fields[i];
let start = fd.offset as usize;
let end = start + fd.size as usize;
if end <= data.len() {
fields[i] = Some(DecodedField {
name: fd.name,
canonical_type: fd.canonical_type,
raw: &data[start..end],
offset: fd.offset,
size: fd.size,
});
}
i += 1;
}
(count, fields)
}
fn format_u64(mut v: u64, buf: &mut [u8]) -> usize {
if v == 0 {
if !buf.is_empty() {
buf[0] = b'0';
return 1;
}
return 0;
}
let mut tmp = [0u8; 20];
let mut pos = 0;
while v > 0 && pos < 20 {
tmp[pos] = b'0' + (v % 10) as u8;
v /= 10;
pos += 1;
}
let len = pos.min(buf.len());
let mut i = 0;
while i < len {
buf[i] = tmp[pos - 1 - i];
i += 1;
}
len
}
fn format_hex_truncated(bytes: &[u8], buf: &mut [u8]) -> usize {
const HEX: &[u8; 16] = b"0123456789abcdef";
let max_bytes = if bytes.len() > 8 { 8 } else { bytes.len() };
let mut pos = 0;
if buf.len() >= 2 {
buf[0] = b'0';
buf[1] = b'x';
pos = 2;
}
let mut i = 0;
while i < max_bytes && pos + 1 < buf.len() {
buf[pos] = HEX[(bytes[i] >> 4) as usize];
buf[pos + 1] = HEX[(bytes[i] & 0xf) as usize];
pos += 2;
i += 1;
}
if bytes.len() > 8 && pos + 3 <= buf.len() {
buf[pos] = b'.';
buf[pos + 1] = b'.';
buf[pos + 2] = b'.';
pos += 3;
}
pos
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct HopperSchemaPointer {
pub schema_version: u16,
pub pointer_flags: u16,
pub manifest_hash: [u8; 32],
pub idl_hash: [u8; 32],
pub codama_hash: [u8; 32],
pub uri_len: u16,
pub uri: [u8; 192],
}
impl HopperSchemaPointer {
pub const DISC: u8 = 255;
pub const PAYLOAD_LEN: usize = 2 + 2 + 32 + 32 + 32 + 2 + 192;
pub const ACCOUNT_LEN: usize = HEADER_LEN + Self::PAYLOAD_LEN;
pub const PDA_SEED: &'static [u8] = b"hopper-schema";
pub const FLAG_HAS_MANIFEST: u16 = 0x0001;
pub const FLAG_HAS_IDL: u16 = 0x0002;
pub const FLAG_HAS_CODAMA: u16 = 0x0004;
pub const FLAG_HAS_URI: u16 = 0x0008;
pub const FLAG_URI_IS_IPFS: u16 = 0x0010;
pub const FLAG_URI_IS_ARWEAVE: u16 = 0x0020;
pub fn uri_str(&self) -> &str {
let len = (self.uri_len as usize).min(192);
core::str::from_utf8(&self.uri[..len]).unwrap_or("")
}
#[inline(always)]
pub fn has_flag(&self, flag: u16) -> bool {
self.pointer_flags & flag != 0
}
}
#[derive(Clone, Copy, Debug)]
pub struct SemanticLint {
pub severity: LintSeverity,
pub code: &'static str,
pub message: &'static str,
pub field: &'static str,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[repr(u8)]
pub enum LintSeverity {
Info = 0,
Warning = 1,
Error = 2,
}
impl LintSeverity {
pub const fn name(self) -> &'static str {
match self {
Self::Info => "info",
Self::Warning => "warning",
Self::Error => "error",
}
}
}
pub fn lint_layout<const N: usize>(
manifest: &LayoutManifest,
behavior: &LayoutBehavior,
) -> (usize, [SemanticLint; N]) {
let mut lints = [SemanticLint {
severity: LintSeverity::Info,
code: "",
message: "",
field: "",
}; N];
let mut count = 0usize;
let mut i = 0;
while i < manifest.field_count {
let field = &manifest.fields[i];
if field.intent.is_authority_sensitive()
&& behavior.mutation_class.is_mutating()
&& !behavior.requires_signer
{
if count < N {
lints[count] = SemanticLint {
severity: LintSeverity::Error,
code: "E001",
message:
"Authority-sensitive field in mutable layout without signer requirement",
field: field.name,
};
count += 1;
}
}
if field.intent.is_monetary()
&& behavior.mutation_class.is_mutating()
&& !matches!(behavior.mutation_class, MutationClass::Financial)
{
if count < N {
lints[count] = SemanticLint {
severity: LintSeverity::Warning,
code: "W001",
message: "Monetary field in layout without financial mutation class",
field: field.name,
};
count += 1;
}
}
if field.intent.is_init_only()
&& behavior.mutation_class.is_mutating()
&& !matches!(behavior.mutation_class, MutationClass::AppendOnly)
{
if count < N {
lints[count] = SemanticLint {
severity: LintSeverity::Warning,
code: "W002",
message: "Init-only field (PDA seed or bump) in mutable layout. Consider making read-only or append-only.",
field: field.name,
};
count += 1;
}
}
i += 1;
}
if behavior.mutation_class.is_mutating() && !behavior.requires_signer {
if count < N {
lints[count] = SemanticLint {
severity: LintSeverity::Warning,
code: "W003",
message: "Mutable layout does not require signer. Verify this is intentional.",
field: "",
};
count += 1;
}
}
if behavior.affects_balance {
let mut has_balance = false;
let mut j = 0;
while j < manifest.field_count {
if manifest.fields[j].intent.is_monetary() {
has_balance = true;
}
j += 1;
}
if !has_balance && count < N {
lints[count] = SemanticLint {
severity: LintSeverity::Warning,
code: "W004",
message: "Layout behavior declares affects_balance but no monetary fields found",
field: "",
};
count += 1;
}
}
(count, lints)
}
#[cfg(feature = "policy")]
pub fn lint_policy<const N: usize>(
behavior: &LayoutBehavior,
policy: hopper_core::policy::PolicyClass,
) -> (usize, [SemanticLint; N]) {
let mut lints = [SemanticLint {
severity: LintSeverity::Info,
code: "",
message: "",
field: "",
}; N];
let mut count = 0usize;
if matches!(behavior.mutation_class, MutationClass::Financial)
&& !matches!(policy, hopper_core::policy::PolicyClass::Financial)
{
if count < N {
lints[count] = SemanticLint {
severity: LintSeverity::Warning,
code: "W005",
message: "Financial mutation class but policy class is not Financial",
field: "",
};
count += 1;
}
}
if matches!(policy, hopper_core::policy::PolicyClass::Financial)
&& !matches!(behavior.mutation_class, MutationClass::Financial)
{
if count < N {
lints[count] = SemanticLint {
severity: LintSeverity::Warning,
code: "W006",
message: "Financial policy class but mutation class is not Financial",
field: "",
};
count += 1;
}
}
(count, lints)
}
impl fmt::Display for SemanticLint {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"[{}] {}: {}",
self.severity.name(),
self.code,
self.message
)?;
if !self.field.is_empty() {
write!(f, " (field: {})", self.field)?;
}
Ok(())
}
}
pub struct OperatingProfile {
pub financial_fields: [&'static str; 16],
pub financial_count: u8,
pub authority_surfaces: [&'static str; 16],
pub authority_count: u8,
pub append_only_segments: [&'static str; 8],
pub append_only_count: u8,
pub migration_sensitive: [&'static str; 8],
pub migration_sensitive_count: u8,
pub stability_grades: [(&'static str, LayoutStabilityGrade); 8],
pub stability_count: u8,
pub has_financial_ops: bool,
pub has_cpi_ops: bool,
pub has_migration_paths: bool,
pub has_receipts: bool,
}
impl OperatingProfile {
pub fn from_manifest(manifest: &ProgramManifest) -> Self {
let mut profile = Self {
financial_fields: [""; 16],
financial_count: 0,
authority_surfaces: [""; 16],
authority_count: 0,
append_only_segments: [""; 8],
append_only_count: 0,
migration_sensitive: [""; 8],
migration_sensitive_count: 0,
stability_grades: [("", LayoutStabilityGrade::Stable); 8],
stability_count: 0,
has_financial_ops: false,
has_cpi_ops: false,
has_migration_paths: !manifest.compatibility_pairs.is_empty(),
has_receipts: false,
};
let mut li = 0;
while li < manifest.layouts.len() {
let layout = &manifest.layouts[li];
if (profile.stability_count as usize) < 8 {
profile.stability_grades[profile.stability_count as usize] =
(layout.name, LayoutStabilityGrade::compute(layout));
profile.stability_count += 1;
}
let mut fi = 0;
while fi < layout.field_count {
let field = &layout.fields[fi];
if field.intent.is_monetary() && (profile.financial_count as usize) < 16 {
profile.financial_fields[profile.financial_count as usize] = field.name;
profile.financial_count += 1;
}
if field.intent.is_authority_sensitive() && (profile.authority_count as usize) < 16
{
profile.authority_surfaces[profile.authority_count as usize] = field.name;
profile.authority_count += 1;
}
fi += 1;
}
li += 1;
}
let mut mi = 0;
while mi < manifest.layout_metadata.len() {
let meta = &manifest.layout_metadata[mi];
let mut si = 0;
while si < meta.segment_roles.len() {
let role_name = meta.segment_roles[si];
if (const_str_eq(role_name, "Journal") || const_str_eq(role_name, "Audit"))
&& (profile.append_only_count as usize) < 8
{
profile.append_only_segments[profile.append_only_count as usize] = role_name;
profile.append_only_count += 1;
}
if const_str_eq(role_name, "Core")
&& (profile.migration_sensitive_count as usize) < 8
{
profile.migration_sensitive[profile.migration_sensitive_count as usize] =
meta.name;
profile.migration_sensitive_count += 1;
}
si += 1;
}
mi += 1;
}
let mut ii = 0;
while ii < manifest.instructions.len() {
let ix = &manifest.instructions[ii];
if ix.receipt_expected {
profile.has_receipts = true;
}
let mut ci = 0;
while ci < ix.capabilities.len() {
if const_str_eq(ix.capabilities[ci], "MutatesTreasury") {
profile.has_financial_ops = true;
}
if const_str_eq(ix.capabilities[ci], "ExternalCall") {
profile.has_cpi_ops = true;
}
ci += 1;
}
ii += 1;
}
profile
}
}
impl fmt::Display for OperatingProfile {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "Operating Profile:")?;
if self.financial_count > 0 {
write!(f, " Financial fields:")?;
let mut i = 0;
while i < self.financial_count as usize {
write!(f, " {}", self.financial_fields[i])?;
i += 1;
}
writeln!(f)?;
}
if self.authority_count > 0 {
write!(f, " Authority surfaces:")?;
let mut i = 0;
while i < self.authority_count as usize {
write!(f, " {}", self.authority_surfaces[i])?;
i += 1;
}
writeln!(f)?;
}
if self.append_only_count > 0 {
write!(f, " Append-only segments:")?;
let mut i = 0;
while i < self.append_only_count as usize {
write!(f, " {}", self.append_only_segments[i])?;
i += 1;
}
writeln!(f)?;
}
if self.stability_count > 0 {
writeln!(f, " Stability grades:")?;
let mut i = 0;
while i < self.stability_count as usize {
let (name, grade) = self.stability_grades[i];
writeln!(f, " {}: {}", name, grade.name())?;
i += 1;
}
}
write!(f, " Features:")?;
if self.has_financial_ops {
write!(f, " financial")?;
}
if self.has_cpi_ops {
write!(f, " cpi")?;
}
if self.has_migration_paths {
write!(f, " migration")?;
}
if self.has_receipts {
write!(f, " receipts")?;
}
writeln!(f)?;
Ok(())
}
}
pub struct HopperIdl {
pub base: ProgramIdl,
pub policies: &'static [PolicyDescriptor],
pub compatibility: &'static [CompatibilityPair],
pub receipt_profiles: &'static [ReceiptProfile],
pub segment_metadata: &'static [IdlSegmentDescriptor],
pub contexts: &'static [crate::accounts::ContextDescriptor],
}
#[derive(Clone, Copy)]
pub struct ReceiptProfile {
pub name: &'static str,
pub expected_phase: &'static str,
pub expects_balance_change: bool,
pub expects_authority_change: bool,
pub expects_journal_append: bool,
pub min_changed_fields: u8,
}
impl fmt::Display for ReceiptProfile {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}(phase={}", self.name, self.expected_phase)?;
if self.expects_balance_change {
write!(f, " balance")?;
}
if self.expects_authority_change {
write!(f, " authority")?;
}
if self.expects_journal_append {
write!(f, " journal")?;
}
if self.min_changed_fields > 0 {
write!(f, " min_fields={}", self.min_changed_fields)?;
}
write!(f, ")")
}
}
#[derive(Clone, Copy)]
pub struct IdlSegmentDescriptor {
pub name: &'static str,
pub role: &'static str,
pub append_only: bool,
pub rebuildable: bool,
pub must_preserve: bool,
}
impl fmt::Display for IdlSegmentDescriptor {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}(role={}", self.name, self.role)?;
if self.append_only {
write!(f, " append-only")?;
}
if self.rebuildable {
write!(f, " rebuildable")?;
}
if self.must_preserve {
write!(f, " must-preserve")?;
}
write!(f, ")")
}
}
impl HopperIdl {
pub const fn empty() -> Self {
Self {
base: ProgramIdl::empty(),
policies: &[],
compatibility: &[],
receipt_profiles: &[],
segment_metadata: &[],
contexts: &[],
}
}
}
impl fmt::Display for HopperIdl {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.base)?;
if !self.policies.is_empty() {
writeln!(f)?;
writeln!(f, "Policies ({}):", self.policies.len())?;
for p in self.policies.iter() {
write!(f, " {:24}", p.name)?;
for (j, r) in p.requirements.iter().enumerate() {
if j > 0 {
write!(f, " + ")?;
}
write!(f, "{}", r)?;
}
writeln!(f)?;
}
}
if !self.compatibility.is_empty() {
writeln!(f)?;
writeln!(f, "Compatibility ({}):", self.compatibility.len())?;
for cp in self.compatibility.iter() {
writeln!(
f,
" {} v{} -> {} v{} {}",
cp.from_layout, cp.from_version, cp.to_layout, cp.to_version, cp.policy,
)?;
}
}
if !self.receipt_profiles.is_empty() {
writeln!(f)?;
writeln!(f, "Receipt Profiles ({}):", self.receipt_profiles.len())?;
for rp in self.receipt_profiles.iter() {
writeln!(
f,
" {:24} phase={} balance={} authority={} journal={}",
rp.name,
rp.expected_phase,
rp.expects_balance_change,
rp.expects_authority_change,
rp.expects_journal_append,
)?;
}
}
if !self.segment_metadata.is_empty() {
writeln!(f)?;
writeln!(f, "Segments ({}):", self.segment_metadata.len())?;
for s in self.segment_metadata.iter() {
write!(f, " {:16} role={}", s.name, s.role)?;
if s.append_only {
write!(f, " append-only")?;
}
if s.rebuildable {
write!(f, " rebuildable")?;
}
if s.must_preserve {
write!(f, " must-preserve")?;
}
writeln!(f)?;
}
}
if !self.contexts.is_empty() {
writeln!(f)?;
writeln!(f, "Contexts ({}):", self.contexts.len())?;
for ctx in self.contexts.iter() {
write!(f, " {}", ctx)?;
}
}
Ok(())
}
}
#[derive(Clone, Copy, Debug)]
pub struct ManagerMetadata {
pub layout: LayoutInfo,
pub fields: &'static [FieldInfo],
}
#[derive(Clone, Copy, Debug)]
pub struct SchemaBundle {
pub manager: ManagerMetadata,
pub manifest: LayoutManifest,
}
pub trait SchemaExport: LayoutContract {
#[inline(always)]
fn layout_info() -> LayoutInfo {
<Self as LayoutContract>::layout_info_static()
}
#[inline(always)]
fn field_map() -> &'static [FieldInfo] {
<Self as LayoutContract>::fields()
}
#[inline(always)]
fn manager_metadata() -> ManagerMetadata {
ManagerMetadata {
layout: Self::layout_info(),
fields: Self::field_map(),
}
}
#[inline(always)]
fn schema_bundle() -> SchemaBundle {
SchemaBundle {
manager: Self::manager_metadata(),
manifest: Self::layout_manifest(),
}
}
fn layout_manifest() -> LayoutManifest;
}
pub trait AccountSchemaExt {
fn manager_metadata_for<T: SchemaExport>(&self) -> Option<ManagerMetadata>;
fn schema_bundle_for<T: SchemaExport>(&self) -> Option<SchemaBundle>;
}
impl AccountSchemaExt for AccountView {
#[inline]
fn manager_metadata_for<T: SchemaExport>(&self) -> Option<ManagerMetadata> {
let info = self.layout_info()?;
if info.matches::<T>() {
Some(T::manager_metadata())
} else {
None
}
}
#[inline]
fn schema_bundle_for<T: SchemaExport>(&self) -> Option<SchemaBundle> {
let info = self.layout_info()?;
if info.matches::<T>() {
Some(T::schema_bundle())
} else {
None
}
}
}
#[cfg(test)]
mod tests {
use super::*;
const V1_FIELDS: &[FieldDescriptor] = &[
FieldDescriptor {
name: "authority",
canonical_type: "[u8;32]",
size: 32,
offset: 16,
intent: FieldIntent::Custom,
},
FieldDescriptor {
name: "balance",
canonical_type: "WireU64",
size: 8,
offset: 48,
intent: FieldIntent::Custom,
},
];
const V2_FIELDS: &[FieldDescriptor] = &[
FieldDescriptor {
name: "authority",
canonical_type: "[u8;32]",
size: 32,
offset: 16,
intent: FieldIntent::Custom,
},
FieldDescriptor {
name: "balance",
canonical_type: "WireU64",
size: 8,
offset: 48,
intent: FieldIntent::Custom,
},
FieldDescriptor {
name: "bump",
canonical_type: "u8",
size: 1,
offset: 56,
intent: FieldIntent::Custom,
},
];
const V1_MANIFEST: LayoutManifest = LayoutManifest {
name: "Vault",
disc: 1,
version: 1,
layout_id: [1, 2, 3, 4, 5, 6, 7, 8],
total_size: 56,
field_count: 2,
fields: V1_FIELDS,
};
const V2_MANIFEST: LayoutManifest = LayoutManifest {
name: "Vault",
disc: 1,
version: 2,
layout_id: [10, 20, 30, 40, 50, 60, 70, 80],
total_size: 57,
field_count: 3,
fields: V2_FIELDS,
};
#[test]
fn no_op_for_identical() {
let plan = MigrationPlan::<16>::generate(&V1_MANIFEST, &V1_MANIFEST);
assert_eq!(plan.policy, MigrationPolicy::NoOp);
assert_eq!(plan.step_count, 0);
}
#[test]
fn append_only_migration() {
let plan = MigrationPlan::<16>::generate(&V1_MANIFEST, &V2_MANIFEST);
assert_eq!(plan.policy, MigrationPolicy::AppendOnly);
assert!(plan.step_count >= 3); assert_eq!(plan.old_size, 56);
assert_eq!(plan.new_size, 57);
assert!(plan.copy_bytes > 0);
assert!(plan.zero_bytes > 0);
assert_eq!(plan.steps[0].action, MigrationAction::CopyPrefix);
let mut found_zero = false;
let mut i = 0;
while i < plan.step_count {
if plan.steps[i].action == MigrationAction::ZeroInit {
assert_eq!(plan.steps[i].field, "bump");
assert_eq!(plan.steps[i].size, 1);
found_zero = true;
}
i += 1;
}
assert!(found_zero);
}
#[test]
fn incompatible_different_disc() {
let other = LayoutManifest {
disc: 99,
..V2_MANIFEST
};
let plan = MigrationPlan::<16>::generate(&V1_MANIFEST, &other);
assert_eq!(plan.policy, MigrationPolicy::Incompatible);
}
#[test]
fn breaking_change_detected() {
let changed_fields: &[FieldDescriptor] = &[
FieldDescriptor {
name: "authority",
canonical_type: "WireU64",
size: 8,
offset: 16,
intent: FieldIntent::Custom,
},
FieldDescriptor {
name: "balance",
canonical_type: "WireU64",
size: 8,
offset: 24,
intent: FieldIntent::Custom,
},
];
let breaking = LayoutManifest {
name: "Vault",
disc: 1,
version: 2,
layout_id: [99; 8],
total_size: 32,
field_count: 2,
fields: changed_fields,
};
let plan = MigrationPlan::<16>::generate(&V1_MANIFEST, &breaking);
assert_eq!(plan.policy, MigrationPolicy::RequiresMigration);
}
#[test]
fn verdict_identical() {
let v = CompatibilityVerdict::between(&V1_MANIFEST, &V1_MANIFEST);
assert_eq!(v, CompatibilityVerdict::Identical);
assert!(v.is_safe());
assert!(v.is_backward_readable());
assert!(!v.requires_migration());
}
#[test]
fn verdict_append_safe() {
let v = CompatibilityVerdict::between(&V1_MANIFEST, &V2_MANIFEST);
assert_eq!(v, CompatibilityVerdict::AppendSafe);
assert!(v.is_safe());
assert!(v.is_backward_readable());
assert!(!v.requires_migration());
}
#[test]
fn verdict_migration_required() {
let changed_fields: &[FieldDescriptor] = &[
FieldDescriptor {
name: "authority",
canonical_type: "WireU64",
size: 8,
offset: 16,
intent: FieldIntent::Custom,
},
FieldDescriptor {
name: "balance",
canonical_type: "WireU64",
size: 8,
offset: 24,
intent: FieldIntent::Custom,
},
];
let breaking = LayoutManifest {
name: "Vault",
disc: 1,
version: 2,
layout_id: [99; 8],
total_size: 32,
field_count: 2,
fields: changed_fields,
};
let v = CompatibilityVerdict::between(&V1_MANIFEST, &breaking);
assert_eq!(v, CompatibilityVerdict::MigrationRequired);
assert!(!v.is_safe());
assert!(!v.is_backward_readable());
assert!(v.requires_migration());
}
#[test]
fn verdict_wire_compatible() {
let semantic_variant = LayoutManifest {
layout_id: [77; 8], ..V1_MANIFEST };
let v = CompatibilityVerdict::between(&V1_MANIFEST, &semantic_variant);
assert_eq!(v, CompatibilityVerdict::WireCompatible);
assert!(v.is_safe());
assert!(v.is_backward_readable());
assert!(!v.requires_migration());
}
#[test]
fn verdict_incompatible() {
let other = LayoutManifest {
disc: 99,
..V2_MANIFEST
};
let v = CompatibilityVerdict::between(&V1_MANIFEST, &other);
assert_eq!(v, CompatibilityVerdict::Incompatible);
assert!(!v.is_safe());
}
#[test]
fn verdict_names() {
assert_eq!(CompatibilityVerdict::Identical.name(), "identical");
assert_eq!(
CompatibilityVerdict::WireCompatible.name(),
"wire-compatible"
);
assert_eq!(CompatibilityVerdict::AppendSafe.name(), "append-safe");
assert_eq!(
CompatibilityVerdict::MigrationRequired.name(),
"migration-required"
);
assert_eq!(CompatibilityVerdict::Incompatible.name(), "incompatible");
}
#[test]
fn segment_advice_core_must_preserve() {
let segs = [DecodedSegment {
id: [1, 0, 0, 0],
offset: 36,
size: 100,
flags: 0x0000, version: 1,
}];
let report = SegmentMigrationReport::<4>::analyze(&segs, 1);
assert_eq!(report.count, 1);
assert_eq!(report.advice[0].role, SegmentRoleHint::Core);
assert!(report.advice[0].must_preserve);
assert!(!report.advice[0].clearable);
assert_eq!(report.preserve_bytes, 100);
}
#[test]
fn segment_advice_journal_clearable() {
let segs = [DecodedSegment {
id: [2, 0, 0, 0],
offset: 136,
size: 256,
flags: 0x2000, version: 1,
}];
let report = SegmentMigrationReport::<4>::analyze(&segs, 1);
assert_eq!(report.advice[0].role, SegmentRoleHint::Journal);
assert!(report.advice[0].clearable);
assert!(report.advice[0].append_only);
assert!(!report.advice[0].must_preserve);
assert_eq!(report.clearable_bytes, 256);
}
#[test]
fn segment_advice_cache_rebuildable() {
let segs = [DecodedSegment {
id: [3, 0, 0, 0],
offset: 400,
size: 64,
flags: 0x4000, version: 1,
}];
let report = SegmentMigrationReport::<4>::analyze(&segs, 1);
assert_eq!(report.advice[0].role, SegmentRoleHint::Cache);
assert!(report.advice[0].clearable);
assert!(report.advice[0].rebuildable);
}
#[test]
fn segment_advice_audit_immutable() {
let segs = [DecodedSegment {
id: [4, 0, 0, 0],
offset: 200,
size: 32,
flags: 0x5000, version: 1,
}];
let report = SegmentMigrationReport::<4>::analyze(&segs, 1);
assert_eq!(report.advice[0].role, SegmentRoleHint::Audit);
assert!(report.advice[0].must_preserve);
assert!(report.advice[0].immutable);
assert!(report.advice[0].append_only);
assert!(!report.advice[0].clearable);
}
#[test]
fn segment_advice_mixed_report() {
let segs = [
DecodedSegment {
id: [1, 0, 0, 0],
offset: 36,
size: 100,
flags: 0x0000,
version: 1,
},
DecodedSegment {
id: [2, 0, 0, 0],
offset: 136,
size: 200,
flags: 0x2000,
version: 1,
},
DecodedSegment {
id: [3, 0, 0, 0],
offset: 336,
size: 64,
flags: 0x4000,
version: 1,
},
];
let report = SegmentMigrationReport::<8>::analyze(&segs, 3);
assert_eq!(report.count, 3);
assert_eq!(report.must_preserve_count(), 1);
assert_eq!(report.clearable_count(), 2);
assert_eq!(report.preserve_bytes, 100);
assert_eq!(report.clearable_bytes, 264);
assert_eq!(report.rebuildable_bytes, 64);
}
#[test]
fn segment_role_hint_requires_migration_copy() {
assert!(SegmentRoleHint::Core.requires_migration_copy());
assert!(SegmentRoleHint::Audit.requires_migration_copy());
assert!(!SegmentRoleHint::Extension.requires_migration_copy());
assert!(!SegmentRoleHint::Journal.requires_migration_copy());
assert!(!SegmentRoleHint::Index.requires_migration_copy());
assert!(!SegmentRoleHint::Cache.requires_migration_copy());
assert!(!SegmentRoleHint::Shard.requires_migration_copy());
}
#[test]
fn segment_role_hint_is_safe_to_drop() {
assert!(SegmentRoleHint::Cache.is_safe_to_drop());
assert!(!SegmentRoleHint::Core.is_safe_to_drop());
assert!(!SegmentRoleHint::Extension.is_safe_to_drop());
assert!(!SegmentRoleHint::Journal.is_safe_to_drop());
assert!(!SegmentRoleHint::Index.is_safe_to_drop());
assert!(!SegmentRoleHint::Audit.is_safe_to_drop());
assert!(!SegmentRoleHint::Shard.is_safe_to_drop());
}
static PM_LAYOUTS: &[LayoutManifest] = &[
LayoutManifest {
name: "Vault",
disc: 1,
version: 1,
layout_id: [1, 2, 3, 4, 5, 6, 7, 8],
total_size: 57,
field_count: 0,
fields: &[],
},
LayoutManifest {
name: "Config",
disc: 2,
version: 1,
layout_id: [8, 7, 6, 5, 4, 3, 2, 1],
total_size: 43,
field_count: 0,
fields: &[],
},
];
static PM_INSTRUCTIONS: &[InstructionDescriptor] = &[
InstructionDescriptor {
name: "deposit",
tag: 1,
args: &[],
accounts: &[],
capabilities: &["MutatesState"],
policy_pack: "TREASURY_WRITE",
receipt_expected: true,
},
InstructionDescriptor {
name: "withdraw",
tag: 2,
args: &[],
accounts: &[],
capabilities: &["MutatesState", "TransfersTokens"],
policy_pack: "TREASURY_WRITE",
receipt_expected: true,
},
];
static PM_POLICIES: &[PolicyDescriptor] = &[PolicyDescriptor {
name: "TREASURY_WRITE",
capabilities: &["MutatesState"],
requirements: &["SignerAuthority"],
invariants: &[],
receipt_profile: "default-mutation",
}];
#[test]
fn program_manifest_find_layout_by_disc() {
let prog = ProgramManifest {
name: "test",
version: "0.1.0",
description: "",
layouts: PM_LAYOUTS,
layout_metadata: &[],
instructions: &[],
events: &[],
policies: &[],
compatibility_pairs: &[],
tooling_hints: &[],
contexts: &[],
};
assert_eq!(prog.layout_count(), 2);
assert!(prog.find_layout_by_disc(1).is_some());
assert_eq!(prog.find_layout_by_disc(1).unwrap().name, "Vault");
assert!(prog.find_layout_by_disc(2).is_some());
assert!(prog.find_layout_by_disc(3).is_none());
}
#[test]
fn program_manifest_find_layout_by_id() {
let prog = ProgramManifest {
name: "test",
version: "0.1.0",
description: "",
layouts: PM_LAYOUTS,
layout_metadata: &[],
instructions: &[],
events: &[],
policies: &[],
compatibility_pairs: &[],
tooling_hints: &[],
contexts: &[],
};
let id = [1, 2, 3, 4, 5, 6, 7, 8];
assert!(prog.find_layout_by_id(&id).is_some());
let bad_id = [0, 0, 0, 0, 0, 0, 0, 0];
assert!(prog.find_layout_by_id(&bad_id).is_none());
}
#[test]
fn program_manifest_identify_from_data() {
static ID_LAYOUTS: &[LayoutManifest] = &[LayoutManifest {
name: "Vault",
disc: 1,
version: 1,
layout_id: [0x10, 0x20, 0x30, 0x40, 0x50, 0x60, 0x70, 0x80],
total_size: 57,
field_count: 0,
fields: &[],
}];
let prog = ProgramManifest {
name: "test",
version: "0.1.0",
description: "",
layouts: ID_LAYOUTS,
layout_metadata: &[],
instructions: &[],
events: &[],
policies: &[],
compatibility_pairs: &[],
tooling_hints: &[],
contexts: &[],
};
let mut data = [0u8; 57];
data[0] = 1; data[1] = 1; data[4..12].copy_from_slice(&[0x10, 0x20, 0x30, 0x40, 0x50, 0x60, 0x70, 0x80]);
let result = prog.identify_from_data(&data);
assert!(result.is_some());
assert_eq!(result.unwrap().name, "Vault");
}
#[test]
fn program_manifest_find_instruction() {
let prog = ProgramManifest {
name: "test",
version: "0.1.0",
description: "",
layouts: &[],
layout_metadata: &[],
instructions: PM_INSTRUCTIONS,
events: &[],
policies: &[],
compatibility_pairs: &[],
tooling_hints: &[],
contexts: &[],
};
assert_eq!(prog.instruction_count(), 2);
assert_eq!(prog.find_instruction(1).unwrap().name, "deposit");
assert_eq!(prog.find_instruction(2).unwrap().name, "withdraw");
assert!(prog.find_instruction(3).is_none());
}
#[test]
fn program_manifest_find_policy() {
let prog = ProgramManifest {
name: "test",
version: "0.1.0",
description: "",
layouts: &[],
layout_metadata: &[],
instructions: &[],
events: &[],
policies: PM_POLICIES,
compatibility_pairs: &[],
tooling_hints: &[],
contexts: &[],
};
assert!(prog.find_policy("TREASURY_WRITE").is_some());
assert!(prog.find_policy("NONEXISTENT").is_none());
}
#[test]
fn decode_account_fields_basic() {
static DECODE_FIELDS: &[FieldDescriptor] = &[
FieldDescriptor {
name: "balance",
canonical_type: "WireU64",
size: 8,
offset: 16,
intent: FieldIntent::Custom,
},
FieldDescriptor {
name: "bump",
canonical_type: "u8",
size: 1,
offset: 24,
intent: FieldIntent::Custom,
},
];
static DECODE_MANIFEST: LayoutManifest = LayoutManifest {
name: "Test",
disc: 1,
version: 1,
layout_id: [0; 8],
total_size: 25,
field_count: 2,
fields: DECODE_FIELDS,
};
let mut data = [0u8; 25];
let balance_bytes = 1000u64.to_le_bytes();
data[16..24].copy_from_slice(&balance_bytes);
data[24] = 254;
let (count, decoded) = decode_account_fields::<8>(&data, &DECODE_MANIFEST);
assert_eq!(count, 2);
assert!(decoded[0].is_some());
assert_eq!(decoded[0].as_ref().unwrap().name, "balance");
assert!(decoded[1].is_some());
assert_eq!(decoded[1].as_ref().unwrap().name, "bump");
assert_eq!(decoded[1].as_ref().unwrap().raw[0], 254);
}
#[test]
fn decoded_field_format_wire_u64() {
let raw = 42u64.to_le_bytes();
let field = DecodedField {
name: "balance",
canonical_type: "WireU64",
raw: &raw,
offset: 16,
size: 8,
};
let mut buf = [0u8; 32];
let len = field.format_value(&mut buf);
assert_eq!(&buf[..len], b"42");
}
#[test]
fn decoded_field_format_wire_u32() {
let raw = 65535u32.to_le_bytes();
let field = DecodedField {
name: "count",
canonical_type: "WireU32",
raw: &raw,
offset: 0,
size: 4,
};
let mut buf = [0u8; 32];
let len = field.format_value(&mut buf);
assert_eq!(&buf[..len], b"65535");
}
#[test]
fn decoded_field_format_bool() {
let raw_true = [1u8];
let field = DecodedField {
name: "frozen",
canonical_type: "WireBool",
raw: &raw_true,
offset: 0,
size: 1,
};
let mut buf = [0u8; 32];
let len = field.format_value(&mut buf);
assert_eq!(&buf[..len], b"true");
let raw_false = [0u8];
let field2 = DecodedField {
name: "frozen",
canonical_type: "WireBool",
raw: &raw_false,
offset: 0,
size: 1,
};
let len = field2.format_value(&mut buf);
assert_eq!(&buf[..len], b"false");
}
#[test]
fn decoded_field_format_address() {
let raw = [0xABu8; 32];
let field = DecodedField {
name: "authority",
canonical_type: "[u8;32]",
raw: &raw,
offset: 0,
size: 32,
};
let mut buf = [0u8; 64];
let len = field.format_value(&mut buf);
let s = core::str::from_utf8(&buf[..len]).unwrap();
assert!(s.starts_with("0x"));
assert!(s.ends_with("..."));
}
#[test]
fn format_u64_basic() {
let mut buf = [0u8; 32];
let len = super::format_u64(12345, &mut buf);
assert_eq!(&buf[..len], b"12345");
let len = super::format_u64(0, &mut buf);
assert_eq!(&buf[..len], b"0");
let len = super::format_u64(u64::MAX, &mut buf);
let expected = b"18446744073709551615";
assert_eq!(&buf[..len], &expected[..]);
}
#[test]
fn format_hex_truncated_short() {
let mut buf = [0u8; 64];
let len = super::format_hex_truncated(&[0xAB, 0xCD], &mut buf);
assert_eq!(&buf[..len], b"0xabcd");
}
#[test]
fn format_hex_truncated_long() {
let mut buf = [0u8; 64];
let data = [0xFFu8; 32];
let len = super::format_hex_truncated(&data, &mut buf);
let s = core::str::from_utf8(&buf[..len]).unwrap();
assert!(s.starts_with("0x"));
assert!(s.ends_with("..."));
assert_eq!(len, 21); }
#[test]
fn program_manifest_display() {
let prog = ProgramManifest {
name: "test_program",
version: "0.1.0",
description: "A test",
layouts: PM_LAYOUTS,
layout_metadata: &[],
instructions: PM_INSTRUCTIONS,
events: &[],
policies: PM_POLICIES,
compatibility_pairs: &[],
tooling_hints: &[],
contexts: &[],
};
extern crate alloc;
use alloc::format;
let s = format!("{}", prog);
assert!(s.contains("test_program"));
assert!(s.contains("Vault"));
assert!(s.contains("deposit"));
assert!(s.contains("MutatesState"));
assert!(s.contains("TREASURY_WRITE"));
assert!(s.contains("SignerAuthority"));
}
#[test]
fn program_manifest_empty() {
let prog = ProgramManifest::empty();
assert_eq!(prog.layout_count(), 0);
assert_eq!(prog.instruction_count(), 0);
assert!(prog.find_layout_by_disc(0).is_none());
assert!(prog.find_instruction(0).is_none());
assert!(prog.identify_from_data(&[0u8; 16]).is_none());
}
#[test]
fn decode_header_empty_buffer() {
assert!(decode_header(&[]).is_none());
}
#[test]
fn decode_header_one_byte() {
assert!(decode_header(&[0xFF]).is_none());
}
#[test]
fn decode_header_fifteen_bytes() {
assert!(decode_header(&[0u8; 15]).is_none());
}
#[test]
fn decode_header_exact_sixteen() {
let h = decode_header(&[0u8; 16]);
assert!(h.is_some());
let h = h.unwrap();
assert_eq!(h.disc, 0);
assert_eq!(h.version, 0);
}
#[test]
fn decode_header_large_buffer() {
let data = [0xABu8; 1024];
let h = decode_header(&data).unwrap();
assert_eq!(h.disc, 0xAB);
assert_eq!(h.version, 0xAB);
}
#[test]
fn decode_segments_too_short() {
assert!(decode_segments::<8>(&[0u8; 19]).is_none());
}
#[test]
fn decode_segments_zero_count() {
let mut data = [0u8; 20];
data[16] = 0; data[17] = 0; let result = decode_segments::<8>(&data);
assert!(result.is_some());
let (n, _) = result.unwrap();
assert_eq!(n, 0);
}
#[test]
fn compare_fields_identical_empty() {
let a = LayoutManifest {
name: "A",
disc: 1,
version: 1,
layout_id: [0; 8],
total_size: 16,
field_count: 0,
fields: &[],
};
let b = LayoutManifest {
name: "B",
disc: 1,
version: 1,
layout_id: [0; 8],
total_size: 16,
field_count: 0,
fields: &[],
};
let report = compare_fields::<8>(&a, &b);
assert_eq!(report.count, 0);
assert!(report.is_append_safe);
}
static SINGLE_FIELD: &[FieldDescriptor] = &[FieldDescriptor {
name: "x",
canonical_type: "u8",
size: 1,
offset: 16,
intent: FieldIntent::Custom,
}];
#[test]
fn compare_fields_all_removed() {
let a = LayoutManifest {
name: "A",
disc: 1,
version: 1,
layout_id: [1; 8],
total_size: 17,
field_count: 1,
fields: SINGLE_FIELD,
};
let b = LayoutManifest {
name: "B",
disc: 1,
version: 2,
layout_id: [2; 8],
total_size: 16,
field_count: 0,
fields: &[],
};
let report = compare_fields::<8>(&a, &b);
assert_eq!(report.count, 1);
assert!(!report.is_append_safe);
}
static OLD_TYPE_FIELD: &[FieldDescriptor] = &[FieldDescriptor {
name: "x",
canonical_type: "u8",
size: 1,
offset: 16,
intent: FieldIntent::Custom,
}];
static NEW_TYPE_FIELD: &[FieldDescriptor] = &[FieldDescriptor {
name: "x",
canonical_type: "u16",
size: 2,
offset: 16,
intent: FieldIntent::Custom,
}];
#[test]
fn compare_fields_type_change_detected() {
let a = LayoutManifest {
name: "A",
disc: 1,
version: 1,
layout_id: [1; 8],
total_size: 17,
field_count: 1,
fields: OLD_TYPE_FIELD,
};
let b = LayoutManifest {
name: "B",
disc: 1,
version: 2,
layout_id: [2; 8],
total_size: 18,
field_count: 1,
fields: NEW_TYPE_FIELD,
};
let report = compare_fields::<8>(&a, &b);
assert_eq!(report.entries[0].status, FieldCompat::Changed);
assert!(!report.is_append_safe);
}
#[test]
fn verdict_different_disc_is_incompatible() {
let a = LayoutManifest {
name: "A",
disc: 1,
version: 1,
layout_id: [1; 8],
total_size: 16,
field_count: 0,
fields: &[],
};
let b = LayoutManifest {
name: "B",
disc: 2,
version: 1,
layout_id: [2; 8],
total_size: 16,
field_count: 0,
fields: &[],
};
assert_eq!(
CompatibilityVerdict::between(&a, &b),
CompatibilityVerdict::Incompatible
);
}
#[test]
fn verdict_same_id_is_identical() {
let a = LayoutManifest {
name: "A",
disc: 1,
version: 1,
layout_id: [9; 8],
total_size: 16,
field_count: 0,
fields: &[],
};
assert_eq!(
CompatibilityVerdict::between(&a, &a),
CompatibilityVerdict::Identical
);
}
#[test]
fn compatibility_explain_between_identical() {
let a = LayoutManifest {
name: "A",
disc: 1,
version: 1,
layout_id: [9; 8],
total_size: 16,
field_count: 0,
fields: &[],
};
let exp = CompatibilityExplain::between(&a, &a);
assert_eq!(exp.verdict, CompatibilityVerdict::Identical);
assert_eq!(exp.added_count, 0);
assert_eq!(exp.removed_count, 0);
assert!(!exp.semantic_drift);
}
static APPEND_OLD: &[FieldDescriptor] = &[FieldDescriptor {
name: "a",
canonical_type: "u8",
size: 1,
offset: 16,
intent: FieldIntent::Custom,
}];
static APPEND_NEW: &[FieldDescriptor] = &[
FieldDescriptor {
name: "a",
canonical_type: "u8",
size: 1,
offset: 16,
intent: FieldIntent::Custom,
},
FieldDescriptor {
name: "b",
canonical_type: "u8",
size: 1,
offset: 17,
intent: FieldIntent::Custom,
},
];
#[test]
fn compatibility_explain_append_counts_fields() {
let older = LayoutManifest {
name: "T",
disc: 1,
version: 1,
layout_id: [1; 8],
total_size: 17,
field_count: 1,
fields: APPEND_OLD,
};
let newer = LayoutManifest {
name: "T",
disc: 1,
version: 2,
layout_id: [2; 8],
total_size: 18,
field_count: 2,
fields: APPEND_NEW,
};
let exp = CompatibilityExplain::between(&older, &newer);
assert_eq!(exp.verdict, CompatibilityVerdict::AppendSafe);
assert_eq!(exp.added_count, 1);
assert_eq!(exp.added_fields[0], "b");
}
#[test]
fn layout_fingerprint_deterministic() {
let m = LayoutManifest {
name: "X",
disc: 1,
version: 1,
layout_id: [5; 8],
total_size: 16,
field_count: 0,
fields: &[],
};
let fp1 = LayoutFingerprint::from_manifest(&m);
let fp2 = LayoutFingerprint::from_manifest(&m);
assert_eq!(fp1.wire_hash, fp2.wire_hash);
assert_eq!(fp1.semantic_hash, fp2.semantic_hash);
}
static FP_CUSTOM: &[FieldDescriptor] = &[FieldDescriptor {
name: "x",
canonical_type: "u8",
size: 1,
offset: 16,
intent: FieldIntent::Custom,
}];
static FP_BALANCE: &[FieldDescriptor] = &[FieldDescriptor {
name: "x",
canonical_type: "u8",
size: 1,
offset: 16,
intent: FieldIntent::Balance,
}];
#[test]
fn layout_fingerprint_differs_on_intent_change() {
let m1 = LayoutManifest {
name: "T",
disc: 1,
version: 1,
layout_id: [1; 8],
total_size: 17,
field_count: 1,
fields: FP_CUSTOM,
};
let m2 = LayoutManifest {
name: "T",
disc: 1,
version: 1,
layout_id: [1; 8],
total_size: 17,
field_count: 1,
fields: FP_BALANCE,
};
let fp1 = LayoutFingerprint::from_manifest(&m1);
let fp2 = LayoutFingerprint::from_manifest(&m2);
assert_eq!(fp1.wire_hash, fp2.wire_hash);
assert_ne!(fp1.semantic_hash, fp2.semantic_hash);
}
static LINT_AUTH_FIELD: &[FieldDescriptor] = &[FieldDescriptor {
name: "auth",
canonical_type: "[u8;32]",
size: 32,
offset: 16,
intent: FieldIntent::Authority,
}];
#[test]
fn lint_layout_authority_without_signer() {
let m = LayoutManifest {
name: "T",
disc: 1,
version: 1,
layout_id: [0; 8],
total_size: 48,
field_count: 1,
fields: LINT_AUTH_FIELD,
};
let behavior = LayoutBehavior {
requires_signer: false,
affects_balance: false,
affects_authority: true,
mutation_class: MutationClass::InPlace,
};
let (n, lints) = lint_layout::<8>(&m, &behavior);
assert!(n >= 1);
assert_eq!(lints[0].code, "E001");
}
#[test]
fn lint_layout_clean_passes() {
let m = LayoutManifest {
name: "T",
disc: 1,
version: 1,
layout_id: [0; 8],
total_size: 48,
field_count: 1,
fields: LINT_AUTH_FIELD,
};
let behavior = LayoutBehavior {
requires_signer: true,
affects_balance: false,
affects_authority: true,
mutation_class: MutationClass::AuthoritySensitive,
};
let (n, _) = lint_layout::<8>(&m, &behavior);
assert_eq!(n, 0);
}
#[test]
fn mutation_class_properties() {
assert!(!MutationClass::ReadOnly.is_mutating());
assert!(MutationClass::InPlace.is_mutating());
assert!(MutationClass::Financial.requires_snapshot());
assert!(MutationClass::AuthoritySensitive.requires_authority());
assert!(!MutationClass::AppendOnly.requires_authority());
}
static SEED_FIELD: &[FieldDescriptor] = &[FieldDescriptor {
name: "seed",
canonical_type: "[u8;32]",
size: 32,
offset: 16,
intent: FieldIntent::PDASeed,
}];
#[test]
fn layout_stability_grade_stable_with_init_only() {
let m = LayoutManifest {
name: "T",
disc: 1,
version: 1,
layout_id: [0; 8],
total_size: 48,
field_count: 1,
fields: SEED_FIELD,
};
assert_eq!(
LayoutStabilityGrade::compute(&m),
LayoutStabilityGrade::Stable
);
}
#[test]
fn layout_stability_grade_evolving_with_custom() {
let m = LayoutManifest {
name: "T",
disc: 1,
version: 1,
layout_id: [0; 8],
total_size: 17,
field_count: 1,
fields: SINGLE_FIELD,
};
assert_eq!(
LayoutStabilityGrade::compute(&m),
LayoutStabilityGrade::Evolving
);
}
static GRADE_HEAVY: &[FieldDescriptor] = &[
FieldDescriptor {
name: "auth1",
canonical_type: "[u8;32]",
size: 32,
offset: 16,
intent: FieldIntent::Authority,
},
FieldDescriptor {
name: "auth2",
canonical_type: "[u8;32]",
size: 32,
offset: 48,
intent: FieldIntent::Owner,
},
FieldDescriptor {
name: "auth3",
canonical_type: "[u8;32]",
size: 32,
offset: 80,
intent: FieldIntent::Delegate,
},
FieldDescriptor {
name: "bal1",
canonical_type: "WireU64",
size: 8,
offset: 112,
intent: FieldIntent::Balance,
},
FieldDescriptor {
name: "bal2",
canonical_type: "WireU64",
size: 8,
offset: 120,
intent: FieldIntent::Supply,
},
FieldDescriptor {
name: "bal3",
canonical_type: "WireU64",
size: 8,
offset: 128,
intent: FieldIntent::Balance,
},
];
#[test]
fn layout_stability_grade_unsafe_to_evolve_heavy() {
let m = LayoutManifest {
name: "T",
disc: 1,
version: 1,
layout_id: [0; 8],
total_size: 136,
field_count: 6,
fields: GRADE_HEAVY,
};
let grade = LayoutStabilityGrade::compute(&m);
assert_eq!(grade, LayoutStabilityGrade::UnsafeToEvolve);
}
#[test]
fn field_intent_new_variants_coverage() {
assert_eq!(FieldIntent::PDASeed.name(), "pda_seed");
assert_eq!(FieldIntent::Version.name(), "version");
assert_eq!(FieldIntent::Bump.name(), "bump");
assert_eq!(FieldIntent::Status.name(), "status");
assert!(FieldIntent::Owner.is_authority_sensitive());
assert!(FieldIntent::Delegate.is_authority_sensitive());
assert!(FieldIntent::Threshold.is_governance());
assert!(FieldIntent::Bump.is_init_only());
assert!(FieldIntent::PDASeed.is_init_only());
assert!(FieldIntent::Supply.is_monetary());
}
#[test]
fn refine_verdict_softens_with_rebuildable_segments() {
let advice = [
SegmentAdvice {
id: [0; 4],
size: 100,
role: SegmentRoleHint::Cache,
must_preserve: false,
clearable: true,
rebuildable: true,
append_only: false,
immutable: false,
},
SegmentAdvice {
id: [0; 4],
size: 0,
role: SegmentRoleHint::Unclassified,
must_preserve: false,
clearable: false,
rebuildable: false,
append_only: false,
immutable: false,
},
];
let report = SegmentMigrationReport {
advice,
count: 1,
preserve_bytes: 0,
clearable_bytes: 100,
rebuildable_bytes: 100,
};
let refined = CompatibilityVerdict::MigrationRequired.refine_with_roles(&report);
assert_eq!(refined, CompatibilityVerdict::AppendSafe);
}
#[test]
fn refine_verdict_escalates_with_immutable_segment() {
let advice = [SegmentAdvice {
id: [0; 4],
size: 50,
role: SegmentRoleHint::Audit,
must_preserve: true,
clearable: false,
rebuildable: false,
append_only: true,
immutable: true,
}];
let report = SegmentMigrationReport {
advice,
count: 1,
preserve_bytes: 50,
clearable_bytes: 0,
rebuildable_bytes: 0,
};
let refined = CompatibilityVerdict::AppendSafe.refine_with_roles(&report);
assert_eq!(refined, CompatibilityVerdict::MigrationRequired);
}
#[test]
#[cfg(feature = "policy")]
fn lint_policy_financial_mismatch() {
let behavior = LayoutBehavior {
requires_signer: true,
affects_balance: true,
affects_authority: false,
mutation_class: MutationClass::Financial,
};
let (n, lints) = lint_policy::<8>(&behavior, hopper_core::policy::PolicyClass::Write);
assert!(n >= 1);
assert_eq!(lints[0].code, "W005");
}
#[test]
#[cfg(feature = "policy")]
fn lint_policy_reverse_mismatch() {
let behavior = LayoutBehavior {
requires_signer: true,
affects_balance: false,
affects_authority: false,
mutation_class: MutationClass::InPlace,
};
let (n, lints) = lint_policy::<8>(&behavior, hopper_core::policy::PolicyClass::Financial);
assert!(n >= 1);
assert_eq!(lints[0].code, "W006");
}
#[test]
#[cfg(feature = "policy")]
fn lint_policy_clean_when_aligned() {
let behavior = LayoutBehavior {
requires_signer: true,
affects_balance: true,
affects_authority: false,
mutation_class: MutationClass::Financial,
};
let (n, _) = lint_policy::<8>(&behavior, hopper_core::policy::PolicyClass::Financial);
assert_eq!(n, 0);
}
#[test]
fn display_field_intent() {
extern crate alloc;
use alloc::format;
assert_eq!(format!("{}", FieldIntent::Balance), "balance");
assert_eq!(format!("{}", FieldIntent::Authority), "authority");
}
#[test]
fn display_mutation_class() {
extern crate alloc;
use alloc::format;
assert_eq!(format!("{}", MutationClass::Financial), "financial");
assert_eq!(format!("{}", MutationClass::ReadOnly), "read-only");
}
#[test]
fn display_layout_stability_grade() {
extern crate alloc;
use alloc::format;
assert_eq!(format!("{}", LayoutStabilityGrade::Stable), "stable");
assert_eq!(
format!("{}", LayoutStabilityGrade::UnsafeToEvolve),
"unsafe-to-evolve"
);
}
#[test]
fn display_compatibility_verdict() {
extern crate alloc;
use alloc::format;
assert_eq!(format!("{}", CompatibilityVerdict::Identical), "identical");
assert_eq!(
format!("{}", CompatibilityVerdict::MigrationRequired),
"migration-required"
);
}
#[test]
fn display_layout_fingerprint() {
extern crate alloc;
use alloc::format;
let fp = LayoutFingerprint {
wire_hash: [0xAB, 0xCD, 0, 0, 0, 0, 0, 0],
semantic_hash: [0, 0, 0, 0, 0, 0, 0xFF, 0x01],
};
let s = format!("{}", fp);
assert!(s.starts_with("wire=abcd"));
assert!(s.contains("sem="));
assert!(s.ends_with("ff01"));
}
#[test]
fn display_receipt_profile() {
extern crate alloc;
use alloc::format;
let rp = ReceiptProfile {
name: "test",
expected_phase: "Mutate",
expects_balance_change: true,
expects_authority_change: false,
expects_journal_append: false,
min_changed_fields: 2,
};
let s = format!("{}", rp);
assert!(s.contains("test"));
assert!(s.contains("Mutate"));
assert!(s.contains("balance"));
assert!(s.contains("min_fields=2"));
}
#[test]
fn display_idl_segment_descriptor() {
extern crate alloc;
use alloc::format;
let sd = IdlSegmentDescriptor {
name: "core",
role: "Core",
append_only: false,
rebuildable: false,
must_preserve: true,
};
let s = format!("{}", sd);
assert!(s.contains("core"));
assert!(s.contains("Core"));
assert!(s.contains("must-preserve"));
assert!(!s.contains("append-only"));
}
}