Implementation Best Practices
Practical conventions for engineers working in the leviathan Aiken codebase, organized by the development lifecycle.
1. Project & Build Discipline
Toolchain pinning:
aiken.tomlandaiken.lockmust be committed and treated as authoritative — Aiken's compiled script hash is a function of source and compiler version, so an unpinned toolchain can silently produce a different on-chain hash than what was audited. The repo'sflake.nix/flake.lockpairing exists specifically to make the build reproducible outside of any one developer's local environment; CI and auditors should build from the flake, not from ambientaikeninstalls.Build before review:
aiken buildshould be run (and itsplutus.jsonblueprint diffed) as part of every PR that touchesvalidators/orlib/, since a change that looks purely cosmetic in Aiken source can still change the compiled hash.
2. Testing Conventions
Unit-test library logic directly. Because business logic lives in
lib/account(see Modular Design Patterns), the majority of test coverage should betestblocks against library functions with hand-constructed fixtures — this is both faster to write and far more precise about which invariant is being checked than trying to assemble a fullScriptContextfor every case.test rejects_stale_sequence_number() { let prior = ChainLinkDatum { sequence_no: 5, .. } let attempted = ChainLinkDatum { sequence_no: 5, .. } // not incremented !continue_chain(prior, attempted, ..) }Negative-path coverage is not optional. For every validator branch, there must be at least one test asserting rejection under a specific violation (stale sequence number, missing signature, wrong oracle reference, mismatched predecessor hash) — not just a happy-path acceptance test. Given the fail-closed requirement (FR-5), the negative paths are the ones actually protecting user funds.
Property-style fuzzing for numeric/ordering logic. Sequence-number and chain-depth-bound logic (SR-6) should be tested against randomized valid and boundary-adjacent inputs (
max_depth,max_depth + 1), not just fixed examples.Golden-file testing of the compiled blueprint. Because
plutus.jsonis what actually gets deployed, CI should diff it against a committed golden file and require an explicit, reviewed update when it changes — this catches accidental hash drift from toolchain or dependency changes, not just logic changes.
3. Code Review & Audit Practices
Review the trusted computing base first.
lib/accountcarries FR-4, FR-6, and SR-3/SR-4 — the modules with the widest blast radius. Reviews should budget disproportionate time here relative to validator-file diffs.Explicit input/output correlation, never implicit ordering. Per SR-4, any PR that indexes into
ctx.transaction.inputs/outputsby a hardcoded position rather than by an explicitfind_input/correlation-token lookup should be blocked in review — this is the single most common source of Cardano-specific validator bugs (double satisfaction, wrong-output attacks).Diff the datum schema like an API contract. A datum field addition, removal, or reordering is a breaking change to every UTxO already sitting at that script address under the old schema. Schema changes require a documented migration or versioning strategy (a
schema_versionfield is the simplest option) before merge.Treat
Revert/fallback branches as the highest-priority review target, not the lowest — per FR-7, this is the code path relied upon precisely when everything else about the system has already gone wrong.
4. Reference-Input Discipline
Use reference inputs, not spent inputs, for anything read-only (time oracle, protocol parameters, account permission lookups) — spending a UTxO to merely read it introduces unnecessary contention on that UTxO and defeats part of the point of the architecture (avoiding concurrency bottlenecks).
Reference inputs must always be checked against an expected policy ID / script hash before their datum is trusted — never assume a reference input's provenance from its position alone.
5. Fee & Cost-Model Awareness
Validator logic should be profiled with
aiken build --trace-level verbose(or the equivalent execution-unit reporting) against realistic chain-extension depths, since execution-unit cost compounds with datum size and lookup complexity — a validator that's cheap for chain depth 1 is not necessarily cheap at the SR-6 maximum depth bound.Prefer
Hash<Blake2b_256, Data>commitments over embedding full predecessor data in a datum (see Modular Design Patterns) specifically to keep per-link datum size — and therefore per-step fee cost — constant regardless of chain depth.
6. Versioning & Upgrade Path
Any validator whose script hash changes (logic fix, dependency bump that affects compiled output) requires a migration validator or explicit account-level opt-in path — Leviathan's account-abstraction layer should never silently redirect existing account UTxOs to a new script address without an on-chain-auditable user action, consistent with SR-1 (no custodial intermediary, including an implicit one introduced by unilateral migration).
Protocol parameters (SR-5) should be upgradable only through a governance-gated reference UTxO update, never through a validator code change — this keeps parameter tuning separate from (and lower-risk than) logic changes.
7. Documentation-as-Code
Every exported
lib/accountfunction should carry an Aiken doc-comment describing its precondition/postcondition contract, andaiken docsshould be run and published as part of release, so the security assumptions encoded in requirements FR-1–FR-8 and SR-1–SR-7 are traceable directly from source rather than living only in this GitBook.
Last updated