// src/demo/hello-chain.ts
import { mkdtemp, rm } from "fs/promises";
import { tmpdir } from "os";
import { join } from "path";
import { fileURLToPath } from "url";
import { addressBytesFromPublicKey as addressBytesFromPublicKey5, generateKeypair } from "@asentum/crypto";
import { StateStore } from "@asentum/state";

// src/chain/block-builder.ts
import { blake3 as blake36 } from "@asentum/crypto";
import {
  BlockHeaderSchema,
  SignedTransactionSchema,
  ZERO_HASH as ZERO_HASH2
} from "@asentum/types";

// src/chain/apply.ts
import { addressBytesFromPublicKey as addressBytesFromPublicKey4, blake3 as blake34, verify as verify2 } from "@asentum/crypto";
import {
  TransactionBodySchema
} from "@asentum/types";

// src/chain/vm-integration.ts
import {
  addressBytesFromPublicKey as addressBytesFromPublicKey3,
  blake3 as blake33,
  verify
} from "@asentum/crypto";
import "@asentum/vm";

// src/chain/slashing-evidence.ts
import { addressBytesFromPublicKey as addressBytesFromPublicKey2 } from "@asentum/crypto";
import { SignedVoteSchema as SignedVoteSchema2 } from "@asentum/types";

// src/consensus/sign.ts
import {
  addressBytesFromPublicKey,
  blake3,
  sign as dilithiumSign,
  verify as dilithiumVerify
} from "@asentum/crypto";
import {
  SignedVoteSchema,
  VoteBodySchema,
  VOTE_KIND_PRECOMMIT,
  VOTE_KIND_PREVOTE
} from "@asentum/types";
function voteDigest(body) {
  return blake3(VoteBodySchema.serialize(body));
}
function verifySignedVote(vote) {
  if (vote.body.blockHash.length !== 32) {
    return { ok: false, reason: "blockHash wrong length" };
  }
  const derived = addressBytesFromPublicKey(vote.publicKey);
  if (!bytesEqual(derived, vote.body.validator)) {
    return { ok: false, reason: "validator address does not match publicKey" };
  }
  const digest = voteDigest(vote.body);
  if (!dilithiumVerify(digest, vote.signature, vote.publicKey)) {
    return { ok: false, reason: "invalid signature" };
  }
  return { ok: true };
}
function bytesEqual(a, b) {
  if (a.length !== b.length) return false;
  for (let i = 0; i < a.length; i++) {
    if (a[i] !== b[i]) return false;
  }
  return true;
}

// src/chain/slashing-evidence.ts
function verifySlashingEvidence(args) {
  if (args.length < 3) {
    return { ok: false, reason: "recordSlash needs 3 args: [offender, voteA, voteB]" };
  }
  const [offenderArg, voteAArg, voteBArg] = args;
  if (typeof offenderArg !== "string") {
    return { ok: false, reason: "offender must be a hex string" };
  }
  if (typeof voteAArg !== "string" || typeof voteBArg !== "string") {
    return { ok: false, reason: "voteA and voteB must be hex strings" };
  }
  let voteA;
  let voteB;
  try {
    voteA = SignedVoteSchema2.deserialize(parseHexBytes(voteAArg));
  } catch (err) {
    return { ok: false, reason: `voteA decode failed: ${String(err)}` };
  }
  try {
    voteB = SignedVoteSchema2.deserialize(parseHexBytes(voteBArg));
  } catch (err) {
    return { ok: false, reason: `voteB decode failed: ${String(err)}` };
  }
  const verifyA = verifySignedVote(voteA);
  if (!verifyA.ok) return { ok: false, reason: `voteA invalid: ${verifyA.reason}` };
  const verifyB = verifySignedVote(voteB);
  if (!verifyB.ok) return { ok: false, reason: `voteB invalid: ${verifyB.reason}` };
  const addrA = addressBytesFromPublicKey2(voteA.publicKey);
  const addrB = addressBytesFromPublicKey2(voteB.publicKey);
  if (!bytesEqual2(addrA, addrB)) {
    return { ok: false, reason: "voteA and voteB are from different validators" };
  }
  const offenderHex = "0x" + bytesToHexLower(addrA);
  if (offenderArg.toLowerCase() !== offenderHex) {
    return {
      ok: false,
      reason: `offender arg ${offenderArg} doesn't match votes' validator ${offenderHex}`
    };
  }
  if (voteA.body.chainId !== voteB.body.chainId) {
    return { ok: false, reason: "chainId mismatch between votes" };
  }
  if (voteA.body.height !== voteB.body.height) {
    return { ok: false, reason: "height mismatch between votes" };
  }
  if (voteA.body.round !== voteB.body.round) {
    return { ok: false, reason: "round mismatch between votes" };
  }
  if (voteA.body.kind !== voteB.body.kind) {
    return { ok: false, reason: "kind mismatch between votes (prevote vs precommit)" };
  }
  if (bytesEqual2(voteA.body.blockHash, voteB.body.blockHash)) {
    return { ok: false, reason: "voteA and voteB are for the same blockHash (not a conflict)" };
  }
  return { ok: true };
}
function parseHexBytes(hex) {
  let h = hex.startsWith("0x") ? hex.slice(2) : hex;
  if (h.length % 2 !== 0) throw new Error("hex string has odd length");
  const out = new Uint8Array(h.length / 2);
  for (let i = 0; i < out.length; i++) {
    out[i] = parseInt(h.slice(i * 2, i * 2 + 2), 16);
  }
  return out;
}
function bytesToHexLower(bytes) {
  let hex = "";
  for (let i = 0; i < bytes.length; i++) {
    hex += (bytes[i] ?? 0).toString(16).padStart(2, "0");
  }
  return hex;
}
function bytesEqual2(a, b) {
  if (a.length !== b.length) return false;
  for (let i = 0; i < a.length; i++) {
    if (a[i] !== b[i]) return false;
  }
  return true;
}

// src/chain/system-contracts.ts
import { blake3 as blake32 } from "@asentum/crypto";
import "@asentum/types";
var STAKING_CONTRACT_ADDRESS = (() => {
  const addr = new Uint8Array(20);
  addr[19] = 1;
  return addr;
})();
var SLASHING_CONTRACT_ADDRESS = (() => {
  const addr = new Uint8Array(20);
  addr[19] = 2;
  return addr;
})();
var STAKING_CONTRACT_SOURCE = `
const INDEX_KEY = 'index';
const VALIDATOR_PREFIX = 'v:';
const WITHDRAW_PREFIX = 'w:';
// Per-epoch entry signal: 'ee:<epochNumber>' \u2192 JSON array of validator
// addresses that submitted submitEpochEntry for that epoch.
const EPOCH_ENTRY_PREFIX = 'ee:';

// Graceful quorum transition: newly bonded validators are "pending"
// for this many blocks before being included in the active validator
// set.
const PENDING_BLOCKS = 10;

// Staking v2 parameters.
// Values are in wei (1 ASE = 10^18 wei).
//
// MIN_SELF_BOND filters out spam-tier bonds. At presale-implied valuation
// (~$0.0019/ASE) 50,000 ASE \u2248 $95 \u2014 affordable for retail in low-cost
// regions, expensive enough that it's a real commitment. Genesis baked
// validators bypass this check (init() doesn't call bond()).
//
// MAX_SELF_BOND caps a single validator at 5% of total supply (1B cap).
// With delegation in v2 a validator's effective stake can grow far past
// this via delegated stake, but the self-bond ceiling prevents any one
// operator from concentrating in their own keypair.
const MIN_SELF_BOND_WEI = 50000n * (10n ** 18n);          // 50,000 ASE
const MAX_SELF_BOND_WEI = 50000000n * (10n ** 18n);       // 50,000,000 ASE
// 14-day unbonding window at 2-second blocks = 604,800 blocks.
const UNBONDING_BLOCKS = 604800n;

// \u2500\u2500\u2500 Asentum committee parameters \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
// These define the "stable core + rotating reserve" committee model.
// See docs/consensus-v1-plan.md for the architectural rationale.
const EPOCH_BLOCKS = 1440n;       // ~2 hours at 5s blocks
const COMMITTEE_CAP = 100;        // max active validators per epoch
const STABLE_CORE_SIZE = 50;      // top-K-by-stake, always active
const RESERVE_SIZE = 50;          // randomly-selected slots from "ready" bonded
// Last N blocks of an epoch where validators may submit submitEpochEntry
// for the NEXT epoch. Short window forces validators to be online recently.
const EPOCH_ENTRY_WINDOW = 100n;

({
  init(args) {
    const initial = (args && args.initialValidators) || [];
    const addresses = [];
    for (const v of initial) {
      const addr = String(v.address).toLowerCase();
      addresses.push(addr);
      storage.set(VALIDATOR_PREFIX + addr, JSON.stringify({
        address: addr,
        pubKey: String(v.pubKey),
        stake: String(v.stake),
        bondedAt: String(chain.blockNumber),
      }));
    }
    storage.set(INDEX_KEY, JSON.stringify(addresses));
    emit('Genesis', { validatorCount: String(addresses.length) });
  },

  // The caller attaches ASE via msg.value; the chain has already credited
  // the staking contract's own balance with that amount before the method
  // runs. The staking contract's job is to track what portion of its
  // balance belongs to which validator's bonded stake.
  bond(pubKey) {
    const sender = String(msg.sender).toLowerCase();
    const amt = BigInt(msg.value);
    assert(amt > 0n, 'bond requires non-zero msg.value');

    const existingRaw = storage.get(VALIDATOR_PREFIX + sender);
    let validator;
    let newTotal;
    if (existingRaw) {
      validator = JSON.parse(existingRaw);
      newTotal = BigInt(validator.stake) + amt;
    } else {
      validator = {
        address: sender,
        pubKey: String(pubKey),
        stake: '0',
        bondedAt: String(chain.blockNumber),
      };
      newTotal = amt;
    }

    assert(newTotal >= MIN_SELF_BOND_WEI, 'bond below MIN_SELF_BOND (50,000 ASE)');
    assert(newTotal <= MAX_SELF_BOND_WEI, 'bond above MAX_SELF_BOND (50,000,000 ASE)');

    if (!existingRaw) {
      const indexRaw = storage.get(INDEX_KEY) || '[]';
      const index = JSON.parse(indexRaw);
      if (index.indexOf(sender) < 0) {
        index.push(sender);
        storage.set(INDEX_KEY, JSON.stringify(index));
      }
    }

    validator.stake = String(newTotal);
    storage.set(VALIDATOR_PREFIX + sender, JSON.stringify(validator));
    emit('Bond', { validator: sender, amount: String(amt), totalStake: validator.stake });
    return true;
  },

  // Moves \`amount\` from active stake into a pending-withdrawal queue.
  // The funds become withdrawable after UNBONDING_BLOCKS have elapsed.
  // Unbonded stake no longer participates in consensus weighting but is
  // still slashable during the unbonding window.
  unbond(amount) {
    const sender = String(msg.sender).toLowerCase();
    const existingRaw = storage.get(VALIDATOR_PREFIX + sender);
    assert(existingRaw, 'not a bonded validator');
    const validator = JSON.parse(existingRaw);
    const amt = BigInt(amount);
    const current = BigInt(validator.stake);
    assert(amt > 0n, 'unbond amount must be positive');
    assert(current >= amt, 'insufficient bonded stake');

    const remaining = current - amt;
    // Validator can fully exit (remaining = 0) or stay bonded at >= MIN.
    // A partial unbond that would leave stake between 0 and MIN is rejected
    // so the set never drifts into a "below-minimum but not fully exited"
    // gray state.
    if (remaining !== 0n) {
      assert(remaining >= MIN_SELF_BOND_WEI,
        'partial unbond would leave stake below MIN_SELF_BOND; unbond fully or reduce amount');
    }

    validator.stake = String(remaining);
    storage.set(VALIDATOR_PREFIX + sender, JSON.stringify(validator));

    // Enqueue the pending withdrawal.
    const unlockAt = BigInt(chain.blockNumber) + UNBONDING_BLOCKS;
    const queueRaw = storage.get(WITHDRAW_PREFIX + sender) || '[]';
    const queue = JSON.parse(queueRaw);
    queue.push({ amount: String(amt), unlockAt: String(unlockAt) });
    storage.set(WITHDRAW_PREFIX + sender, JSON.stringify(queue));

    emit('Unbond', {
      validator: sender,
      amount: String(amt),
      remainingStake: validator.stake,
      unlockAt: String(unlockAt),
    });
    return true;
  },

  // Claims matured pending withdrawals. Entries whose unlockAt has passed
  // are removed from the queue, the total is sent back to the caller via
  // the chain's transfer() primitive, and the matured amount is returned.
  withdraw() {
    const sender = String(msg.sender).toLowerCase();
    const queueRaw = storage.get(WITHDRAW_PREFIX + sender) || '[]';
    const queue = JSON.parse(queueRaw);
    const now = BigInt(chain.blockNumber);
    let matured = 0n;
    const remaining = [];
    for (let i = 0; i < queue.length; i++) {
      const entry = queue[i];
      if (BigInt(entry.unlockAt) <= now) {
        matured += BigInt(entry.amount);
      } else {
        remaining.push(entry);
      }
    }
    assert(matured > 0n, 'no matured withdrawals');
    storage.set(WITHDRAW_PREFIX + sender, JSON.stringify(remaining));
    // Send the ASE back to the validator's wallet.
    transfer(sender, matured);
    emit('Withdraw', { validator: sender, amount: String(matured) });
    return String(matured);
  },

  // View: pending + matured withdrawal breakdown for a given address.
  getWithdrawable(address) {
    const addr = String(address).toLowerCase();
    const queueRaw = storage.get(WITHDRAW_PREFIX + addr) || '[]';
    const queue = JSON.parse(queueRaw);
    const now = BigInt(chain.blockNumber);
    let matured = 0n;
    let pending = 0n;
    const entries = [];
    for (let i = 0; i < queue.length; i++) {
      const e = queue[i];
      const amt = BigInt(e.amount);
      const unlockAt = BigInt(e.unlockAt);
      if (unlockAt <= now) matured += amt;
      else pending += amt;
      entries.push({ amount: String(amt), unlockAt: String(unlockAt), matured: unlockAt <= now });
    }
    return { matured: String(matured), pending: String(pending), entries };
  },

  // View: current staking parameters.
  getStakingParams() {
    return {
      minSelfBond: String(MIN_SELF_BOND_WEI),
      maxSelfBond: String(MAX_SELF_BOND_WEI),
      unbondingBlocks: String(UNBONDING_BLOCKS),
      pendingBlocks: String(PENDING_BLOCKS),
    };
  },

  getValidator(address) {
    const raw = storage.get(VALIDATOR_PREFIX + String(address).toLowerCase());
    return raw ? JSON.parse(raw) : null;
  },

  getValidators() {
    const indexRaw = storage.get(INDEX_KEY) || '[]';
    const index = JSON.parse(indexRaw);
    const result = [];
    const currentBlock = BigInt(chain.blockNumber);
    for (let i = 0; i < index.length; i++) {
      const addr = index[i];
      const raw = storage.get(VALIDATOR_PREFIX + addr);
      if (raw) {
        const v = JSON.parse(raw);
        if (BigInt(v.stake) > 0n) {
          const bondedAt = BigInt(v.bondedAt || '0');
          const isPending = (currentBlock - bondedAt) < BigInt(PENDING_BLOCKS);
          v.status = isPending ? 'pending' : 'active';
          v.activeAtBlock = String(bondedAt + BigInt(PENDING_BLOCKS));
          result.push(v);
        }
      }
    }
    return result;
  },

  // Returns only validators whose pending period has expired.
  // This is what the consensus engine reads to determine the
  // active validator set for quorum calculations.
  getActiveValidators() {
    const indexRaw = storage.get(INDEX_KEY) || '[]';
    const index = JSON.parse(indexRaw);
    const result = [];
    const currentBlock = BigInt(chain.blockNumber);
    for (let i = 0; i < index.length; i++) {
      const addr = index[i];
      const raw = storage.get(VALIDATOR_PREFIX + addr);
      if (raw) {
        const v = JSON.parse(raw);
        if (BigInt(v.stake) > 0n) {
          const bondedAt = BigInt(v.bondedAt || '0');
          // Genesis validators (bondedAt=0) are always active.
          // Non-genesis validators must wait PENDING_BLOCKS.
          if (bondedAt === 0n || (currentBlock - bondedAt) >= BigInt(PENDING_BLOCKS)) {
            result.push(v);
          }
        }
      }
    }
    return result;
  },

  // \u2500\u2500\u2500 Asentum committee selection \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
  //
  // submitEpochEntry: a bonded validator declares "I'm online and ready
  // for the next epoch." Must be called during the entry window (last
  // EPOCH_ENTRY_WINDOW blocks of the current epoch) and target the
  // next epoch number. The validator's address (msg.sender) is added
  // to the next epoch's eligible set; the consensus layer uses this
  // set to populate the rotating reserve.
  //
  // Validators in the stable core (top STABLE_CORE_SIZE by stake) do
  // NOT need to call this \u2014 their slot is unconditional. But it's not
  // harmful for them to call; the entry record is simply ignored.
  submitEpochEntry(targetEpoch) {
    const sender = String(msg.sender).toLowerCase();
    const targetEpochNum = BigInt(targetEpoch);
    const currentBlock = BigInt(chain.blockNumber);
    const currentEpoch = currentBlock / EPOCH_BLOCKS;

    // The target must be the next epoch. Allowing arbitrary future
    // epochs would let a validator submit once and stay-eligible forever
    // (bypassing the "must be online recently" intent).
    assert(targetEpochNum === currentEpoch + 1n,
      'submitEpochEntry: targetEpoch must be currentEpoch + 1');

    // Must be inside the entry window \u2014 last EPOCH_ENTRY_WINDOW blocks of
    // the current epoch.
    const epochStart = currentEpoch * EPOCH_BLOCKS;
    const windowOpenAt = epochStart + EPOCH_BLOCKS - EPOCH_ENTRY_WINDOW;
    assert(currentBlock >= windowOpenAt,
      'submitEpochEntry: entry window not yet open for this epoch');

    // Sender must be a currently-bonded validator with positive stake.
    const validatorRaw = storage.get(VALIDATOR_PREFIX + sender);
    assert(validatorRaw, 'submitEpochEntry: not a bonded validator');
    const validator = JSON.parse(validatorRaw);
    assert(BigInt(validator.stake) > 0n,
      'submitEpochEntry: validator has zero stake');

    // Add to the entry set for the next epoch (idempotent).
    const key = EPOCH_ENTRY_PREFIX + String(targetEpochNum);
    const existingRaw = storage.get(key) || '[]';
    const existing = JSON.parse(existingRaw);
    if (existing.indexOf(sender) < 0) {
      existing.push(sender);
      storage.set(key, JSON.stringify(existing));
      emit('EpochEntry', { validator: sender, targetEpoch: String(targetEpochNum) });
    }
    return true;
  },

  // getEpochCandidates: returns the materials the consensus layer needs
  // to assemble the active committee for \`epochNumber\`:
  //   - core:    top STABLE_CORE_SIZE bonded validators by stake
  //              (sorted desc, ties broken by address asc for determinism)
  //   - reserve: bonded validators NOT in core who submitted an
  //              epoch_entry for this epoch (the eligible pool)
  //
  // The consensus layer is responsible for shuffling \`reserve\` with a
  // deterministic seed and taking the first (COMMITTEE_CAP - core.length)
  // entries to fill the rotating slots. Putting the shuffle outside the
  // contract keeps this method pure data-fetch + sort.
  getEpochCandidates(epochNumber) {
    const epochNum = BigInt(epochNumber);

    // Pull the active set \u2014 bonded, non-pending, positive stake.
    const indexRaw = storage.get(INDEX_KEY) || '[]';
    const index = JSON.parse(indexRaw);
    const currentBlock = BigInt(chain.blockNumber);
    const all = [];
    for (let i = 0; i < index.length; i++) {
      const addr = index[i];
      const raw = storage.get(VALIDATOR_PREFIX + addr);
      if (!raw) continue;
      const v = JSON.parse(raw);
      if (BigInt(v.stake) <= 0n) continue;
      const bondedAt = BigInt(v.bondedAt || '0');
      const isPending = bondedAt !== 0n && (currentBlock - bondedAt) < BigInt(PENDING_BLOCKS);
      if (isPending) continue;
      all.push(v);
    }

    // Sort by stake desc; ties broken by address asc so all nodes agree.
    all.sort(function (a, b) {
      const sa = BigInt(a.stake);
      const sb = BigInt(b.stake);
      if (sb > sa) return 1;
      if (sb < sa) return -1;
      if (a.address < b.address) return -1;
      if (a.address > b.address) return 1;
      return 0;
    });

    // Top STABLE_CORE_SIZE = stable core. Always in committee.
    const core = all.slice(0, STABLE_CORE_SIZE);
    const coreSet = {};
    for (let i = 0; i < core.length; i++) coreSet[core[i].address] = true;

    // Reserve eligibility: bonded + not in core + submitted epoch_entry.
    const entryKey = EPOCH_ENTRY_PREFIX + String(epochNum);
    const entryRaw = storage.get(entryKey) || '[]';
    const entryList = JSON.parse(entryRaw);
    const entrySet = {};
    for (let i = 0; i < entryList.length; i++) entrySet[entryList[i]] = true;

    const reserve = [];
    for (let i = STABLE_CORE_SIZE; i < all.length; i++) {
      const v = all[i];
      if (entrySet[v.address]) reserve.push(v);
    }

    return { core: core, reserve: reserve };
  },

  // View: committee parameters as exposed to clients/UIs.
  getCommitteeParams() {
    return {
      epochBlocks: String(EPOCH_BLOCKS),
      committeeCap: String(COMMITTEE_CAP),
      stableCoreSize: String(STABLE_CORE_SIZE),
      reserveSize: String(RESERVE_SIZE),
      entryWindow: String(EPOCH_ENTRY_WINDOW),
    };
  },

  // View: list addresses that have submitted epoch_entry for an epoch.
  // Useful for diagnostics and the validator client's "did my entry tx land?" check.
  getEpochEntries(epochNumber) {
    const key = EPOCH_ENTRY_PREFIX + String(BigInt(epochNumber));
    const raw = storage.get(key) || '[]';
    return JSON.parse(raw);
  },

  validatorCount() {
    const indexRaw = storage.get(INDEX_KEY) || '[]';
    const index = JSON.parse(indexRaw);
    let count = 0;
    for (let i = 0; i < index.length; i++) {
      const raw = storage.get(VALIDATOR_PREFIX + index[i]);
      if (raw && BigInt(JSON.parse(raw).stake) > 0n) count++;
    }
    return String(count);
  },

  totalBondedStake() {
    const indexRaw = storage.get(INDEX_KEY) || '[]';
    const index = JSON.parse(indexRaw);
    let total = 0n;
    for (let i = 0; i < index.length; i++) {
      const raw = storage.get(VALIDATOR_PREFIX + index[i]);
      if (raw) total += BigInt(JSON.parse(raw).stake);
    }
    return String(total);
  },
});
`.trim();
var SLASHING_CONTRACT_SOURCE = `
const INDEX_KEY = 'tombstone_index';
const TOMBSTONE_PREFIX = 't:';

({
  init() {
    storage.set(INDEX_KEY, JSON.stringify([]));
    emit('Genesis', { tombstoned: '0' });
  },

  // Record a slashing tombstone. Evidence is pre-verified at the apply
  // layer. Also accepts a legacy 2-arg form (offender, evidenceString)
  // for backward compatibility with tests.
  recordSlash(offender, evidenceOrVoteA, voteB) {
    const addr = String(offender).toLowerCase();
    const existing = storage.get(TOMBSTONE_PREFIX + addr);
    if (existing) {
      // Already tombstoned \u2014 no-op.
      return false;
    }
    // Build a compact evidence summary. If we got the 3-arg form,
    // store both vote-bytes lengths as a hint; otherwise treat the
    // second arg as a plain string.
    let evidenceSummary;
    if (typeof voteB === 'string') {
      evidenceSummary = 'voteA=' + String(evidenceOrVoteA).length + ' voteB=' + String(voteB).length + ' bytes';
    } else {
      evidenceSummary = String(evidenceOrVoteA || '');
    }
    const record = {
      address: addr,
      slashedAt: String(chain.blockNumber),
      evidence: evidenceSummary,
    };
    storage.set(TOMBSTONE_PREFIX + addr, JSON.stringify(record));
    const indexRaw = storage.get(INDEX_KEY) || '[]';
    const index = JSON.parse(indexRaw);
    index.push(addr);
    storage.set(INDEX_KEY, JSON.stringify(index));
    emit('Slashed', { validator: addr, slashedAt: record.slashedAt });
    return true;
  },

  isTombstoned(address) {
    const addr = String(address).toLowerCase();
    return storage.get(TOMBSTONE_PREFIX + addr) ? true : false;
  },

  getTombstones() {
    const indexRaw = storage.get(INDEX_KEY) || '[]';
    const index = JSON.parse(indexRaw);
    const result = [];
    for (let i = 0; i < index.length; i++) {
      const raw = storage.get(TOMBSTONE_PREFIX + index[i]);
      if (raw) result.push(JSON.parse(raw));
    }
    return result;
  },

  tombstoneCount() {
    const indexRaw = storage.get(INDEX_KEY) || '[]';
    return String(JSON.parse(indexRaw).length);
  },
});
`.trim();
var GOVERNANCE_CONTRACT_SOURCE = `
const PROPOSAL_PREFIX = 'p:';
const VOTE_PREFIX = 'v:';
const AMENDMENT_PREFIX = 'a:';
const AMENDMENT_VOTE_PREFIX = 'av:';
const LOCKED_PREFIX = 'locked:';
const COUNT_KEY = 'proposalCount';
const AMENDMENT_COUNT_PREFIX = 'ac:';

// ---------------------------------------------------------------
// Governance parameters \u2014 enforced at the chain level.
// These are NOT configurable per-proposal. They are the rules.
// "There is no buts in Asentum, there is only code and truth."
// ---------------------------------------------------------------

// Voting period bounds (in blocks, at 2s/block):
//   MIN: 2160 blocks = ~72 minutes. No flash governance.
//   MAX: 604800 blocks = ~14 days. Proposals can't run forever.
//   DEFAULT: 43200 blocks = ~24 hours.
const MIN_VOTING_PERIOD = 2160;
const MAX_VOTING_PERIOD = 604800;
const DEFAULT_VOTING_PERIOD = 43200;
const AMENDMENT_VOTING_PERIOD = 21600; // ~12 hours

// Minimum quorum: at least this many wei of ASE must participate
// (votesFor + votesAgainst) for the result to be valid. If quorum
// isn't met, execute() marks the proposal as 'no_quorum' instead
// of 'passed' or 'rejected'. Prevents a single small holder from
// passing proposals unilaterally.
//
// 1000 ASE = 1000 * 10^18 wei. Low for testnet; raise for mainnet.
const MIN_QUORUM_WEI = '1000000000000000000000';

// Proposal bond: the proposer must send this much ASE with the
// propose() call. The bond is returned if the proposal reaches
// quorum (whether it passes or fails). Forfeited if it doesn't
// reach quorum \u2014 anti-spam measure.
const PROPOSAL_BOND_WEI = '100000000000000000000'; // 100 ASE

// Execution delay: after voting ends and before the proposal's
// effects take hold, this many blocks must pass. Gives the
// community time to react if something unexpected passes.
const EXECUTION_DELAY = 100; // ~200 seconds on testnet

({
  init() {
    assert(!storage.get('initialized'), 'governance contract already initialized');
    storage.set('initialized', 'true');
    storage.set(COUNT_KEY, '0');
    storage.set('libraryIndex', JSON.stringify([]));
    emit('GovernanceDeployed', { block: String(chain.blockNumber) });
  },

  propose(title, description, category, votingPeriod, libraryName, librarySource) {
    assert(title && title.length > 0, 'title required');
    assert(title.length <= 200, 'title too long (max 200)');
    assert(description && description.length > 0, 'description required');
    assert(description.length <= 10000, 'description too long (max 10000)');

    const validCategories = ['aip', 'parameter', 'library', 'general'];
    const cat = category || 'general';
    assert(validCategories.indexOf(cat) >= 0, 'invalid category: ' + cat);

    // Library proposals must include the library name + source code.
    if (cat === 'library') {
      assert(libraryName && libraryName.length > 0, 'library proposals require a libraryName');
      assert(/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(libraryName), 'libraryName must be a valid JS identifier (e.g. "math", "utils")');
      assert(librarySource && librarySource.length > 0, 'library proposals require librarySource');
      assert(librarySource.length <= 50000, 'library source too large (max 50KB)');
    }

    const period = parseInt(votingPeriod || String(DEFAULT_VOTING_PERIOD));
    assert(period >= MIN_VOTING_PERIOD, 'voting period must be at least ' + MIN_VOTING_PERIOD + ' blocks (~72 minutes)');
    assert(period <= MAX_VOTING_PERIOD, 'voting period must be at most ' + MAX_VOTING_PERIOD + ' blocks (~14 days)');

    // Proposal bond: the proposer must send ASE to create a proposal.
    // Returned if quorum is met; forfeited if not. Anti-spam.
    const bond = BigInt(msg.value);
    assert(bond >= BigInt(PROPOSAL_BOND_WEI), 'must send at least ' + PROPOSAL_BOND_WEI + ' wei (' + String(BigInt(PROPOSAL_BOND_WEI) / 1000000000000000000n) + ' ASE) as proposal bond');

    const id = parseInt(storage.get(COUNT_KEY) || '0');
    const currentBlock = BigInt(chain.blockNumber);

    const proposal = {
      id: id,
      title: title,
      description: description,
      category: cat,
      proposer: msg.sender,
      createdAt: String(currentBlock),
      votingEndsAt: String(currentBlock + BigInt(period)),
      votesFor: '0',
      votesAgainst: '0',
      totalVoteWeight: '0',
      status: 'active',
      amendmentCount: 0,
      // Library-specific fields (null for non-library proposals).
      libraryName: cat === 'library' ? libraryName : null,
      librarySource: cat === 'library' ? librarySource : null,
      // Bond tracking.
      bond: String(bond),
      bondReturned: false,
    };

    storage.set(PROPOSAL_PREFIX + id, JSON.stringify(proposal));
    storage.set(COUNT_KEY, String(id + 1));
    storage.set(AMENDMENT_COUNT_PREFIX + id, '0');

    emit('ProposalCreated', {
      id: String(id),
      title: title,
      category: cat,
      proposer: msg.sender,
    });
    return id;
  },

  vote(proposalId, support) {
    const id = parseInt(proposalId);
    const raw = storage.get(PROPOSAL_PREFIX + id);
    assert(raw, 'proposal not found');
    const proposal = JSON.parse(raw);
    assert(proposal.status === 'active', 'proposal is not active');

    const currentBlock = BigInt(chain.blockNumber);
    assert(currentBlock <= BigInt(proposal.votingEndsAt), 'voting period has ended');

    const weight = BigInt(msg.value);
    assert(weight > 0n, 'must send ASE to vote (vote weight = msg.value)');

    const voteKey = VOTE_PREFIX + id + ':' + msg.sender;
    assert(!storage.get(voteKey), 'already voted on this proposal');

    const sup = support === true || support === 'true' || support === '1' || support === 1;

    storage.set(voteKey, JSON.stringify({
      voter: msg.sender,
      support: sup,
      weight: String(weight),
      block: String(currentBlock),
    }));

    if (sup) {
      proposal.votesFor = String(BigInt(proposal.votesFor) + weight);
    } else {
      proposal.votesAgainst = String(BigInt(proposal.votesAgainst) + weight);
    }
    proposal.totalVoteWeight = String(BigInt(proposal.totalVoteWeight) + weight);
    storage.set(PROPOSAL_PREFIX + id, JSON.stringify(proposal));

    // Track locked tokens for this voter
    const lockedRaw = storage.get(LOCKED_PREFIX + msg.sender) || '0';
    storage.set(LOCKED_PREFIX + msg.sender, String(BigInt(lockedRaw) + weight));

    emit('VoteCast', {
      proposalId: String(id),
      voter: msg.sender,
      support: String(sup),
      weight: String(weight),
    });
    return true;
  },

  proposeAmendment(proposalId, title, description) {
    const id = parseInt(proposalId);
    const raw = storage.get(PROPOSAL_PREFIX + id);
    assert(raw, 'proposal not found');
    const proposal = JSON.parse(raw);
    assert(proposal.status === 'active', 'proposal is not active');

    const currentBlock = BigInt(chain.blockNumber);
    assert(currentBlock <= BigInt(proposal.votingEndsAt), 'voting period has ended');
    assert(title && title.length > 0 && title.length <= 200, 'title required (max 200)');
    assert(description && description.length > 0 && description.length <= 5000, 'description required (max 5000)');

    const amendCountRaw = storage.get(AMENDMENT_COUNT_PREFIX + id) || '0';
    const amendId = parseInt(amendCountRaw);

    const amendment = {
      id: amendId,
      proposalId: id,
      title: title,
      description: description,
      proposer: msg.sender,
      createdAt: String(currentBlock),
      votingEndsAt: String(currentBlock + BigInt(AMENDMENT_VOTING_PERIOD)),
      votesFor: '0',
      votesAgainst: '0',
      status: 'active',
    };

    storage.set(AMENDMENT_PREFIX + id + ':' + amendId, JSON.stringify(amendment));
    storage.set(AMENDMENT_COUNT_PREFIX + id, String(amendId + 1));

    emit('AmendmentProposed', {
      proposalId: String(id),
      amendmentId: String(amendId),
      title: title,
      proposer: msg.sender,
    });
    return amendId;
  },

  voteOnAmendment(proposalId, amendmentId, support) {
    const pId = parseInt(proposalId);
    const aId = parseInt(amendmentId);
    const raw = storage.get(AMENDMENT_PREFIX + pId + ':' + aId);
    assert(raw, 'amendment not found');
    const amendment = JSON.parse(raw);
    assert(amendment.status === 'active', 'amendment is not active');

    const currentBlock = BigInt(chain.blockNumber);
    assert(currentBlock <= BigInt(amendment.votingEndsAt), 'amendment voting has ended');

    const weight = BigInt(msg.value);
    assert(weight > 0n, 'must send ASE to vote');

    const voteKey = AMENDMENT_VOTE_PREFIX + pId + ':' + aId + ':' + msg.sender;
    assert(!storage.get(voteKey), 'already voted on this amendment');

    const sup = support === true || support === 'true' || support === '1' || support === 1;

    storage.set(voteKey, JSON.stringify({
      voter: msg.sender,
      support: sup,
      weight: String(weight),
    }));

    if (sup) {
      amendment.votesFor = String(BigInt(amendment.votesFor) + weight);
    } else {
      amendment.votesAgainst = String(BigInt(amendment.votesAgainst) + weight);
    }
    storage.set(AMENDMENT_PREFIX + pId + ':' + aId, JSON.stringify(amendment));

    const lockedRaw = storage.get(LOCKED_PREFIX + msg.sender) || '0';
    storage.set(LOCKED_PREFIX + msg.sender, String(BigInt(lockedRaw) + weight));

    emit('AmendmentVoteCast', {
      proposalId: String(pId),
      amendmentId: String(aId),
      voter: msg.sender,
      support: String(sup),
      weight: String(weight),
    });
    return true;
  },

  execute(proposalId) {
    const id = parseInt(proposalId);
    const raw = storage.get(PROPOSAL_PREFIX + id);
    assert(raw, 'proposal not found');
    const proposal = JSON.parse(raw);
    assert(proposal.status === 'active', 'proposal already concluded');

    const currentBlock = BigInt(chain.blockNumber);
    const votingEndsAt = BigInt(proposal.votingEndsAt);
    assert(currentBlock > votingEndsAt, 'voting period not yet ended');

    // Execution delay: prevent instant effect after voting ends.
    // The community gets EXECUTION_DELAY blocks to react.
    assert(currentBlock > votingEndsAt + BigInt(EXECUTION_DELAY),
      'execution delay not yet passed (' + EXECUTION_DELAY + ' blocks after voting ends)');

    const votesFor = BigInt(proposal.votesFor);
    const votesAgainst = BigInt(proposal.votesAgainst);
    const totalVotes = votesFor + votesAgainst;

    // Quorum check: enough ASE must have participated for the
    // result to be valid. Without this, 1 ASE could pass anything.
    if (totalVotes < BigInt(MIN_QUORUM_WEI)) {
      proposal.status = 'no_quorum';
      proposal.executedAt = String(currentBlock);
      storage.set(PROPOSAL_PREFIX + id, JSON.stringify(proposal));
      emit('ProposalExecuted', { id: String(id), status: 'no_quorum', totalVotes: String(totalVotes) });
      // Return the proposal bond since the proposer isn't at fault
      // for quorum not being met.
      return 'no_quorum';
    }

    if (votesFor > votesAgainst) {
      proposal.status = 'passed';

      // If this is a library proposal, store the approved library source
      // so the VM can read it and inject it as a contract global.
      if (proposal.category === 'library' && proposal.libraryName && proposal.librarySource) {
        storage.set('lib:' + proposal.libraryName, proposal.librarySource);
        // Update the library index so the VM can enumerate all approved libraries.
        const libIndexRaw = storage.get('libraryIndex') || '[]';
        const libIndex = JSON.parse(libIndexRaw);
        if (libIndex.indexOf(proposal.libraryName) < 0) {
          libIndex.push(proposal.libraryName);
          storage.set('libraryIndex', JSON.stringify(libIndex));
        }
        emit('LibraryApproved', { name: proposal.libraryName, sourceLength: String(proposal.librarySource.length) });
      }
    } else {
      proposal.status = 'rejected';
    }
    proposal.executedAt = String(currentBlock);
    storage.set(PROPOSAL_PREFIX + id, JSON.stringify(proposal));

    // Also finalize any active amendments
    const amendCount = parseInt(storage.get(AMENDMENT_COUNT_PREFIX + id) || '0');
    for (let i = 0; i < amendCount; i++) {
      const aRaw = storage.get(AMENDMENT_PREFIX + id + ':' + i);
      if (aRaw) {
        const amendment = JSON.parse(aRaw);
        if (amendment.status === 'active') {
          const aFor = BigInt(amendment.votesFor);
          const aAgainst = BigInt(amendment.votesAgainst);
          amendment.status = aFor > aAgainst ? 'passed' : 'rejected';
          storage.set(AMENDMENT_PREFIX + id + ':' + i, JSON.stringify(amendment));
        }
      }
    }

    emit('ProposalExecuted', {
      id: String(id),
      status: proposal.status,
      votesFor: proposal.votesFor,
      votesAgainst: proposal.votesAgainst,
    });
    return proposal.status;
  },

  // Voters withdraw their locked ASE after proposals conclude.
  // In a full implementation, this would track per-proposal locks
  // and only release tokens from concluded proposals. For MVP,
  // it releases ALL locked tokens \u2014 users should only withdraw
  // after all their voted proposals have concluded.
  withdraw() {
    const lockedRaw = storage.get(LOCKED_PREFIX + msg.sender) || '0';
    const locked = BigInt(lockedRaw);
    assert(locked > 0n, 'no locked tokens to withdraw');
    // Clear the lock record. The ASE transfer back to the voter is
    // handled by the chain layer's balance delta mechanism.
    storage.set(LOCKED_PREFIX + msg.sender, '0');
    emit('Withdraw', { voter: msg.sender, amount: String(locked) });
    return true;
  },

  getProposal(proposalId) {
    const raw = storage.get(PROPOSAL_PREFIX + parseInt(proposalId));
    return raw ? JSON.parse(raw) : null;
  },

  getProposals() {
    const count = parseInt(storage.get(COUNT_KEY) || '0');
    const result = [];
    for (let i = 0; i < count; i++) {
      const raw = storage.get(PROPOSAL_PREFIX + i);
      if (raw) result.push(JSON.parse(raw));
    }
    return result;
  },

  getAmendments(proposalId) {
    const id = parseInt(proposalId);
    const count = parseInt(storage.get(AMENDMENT_COUNT_PREFIX + id) || '0');
    const result = [];
    for (let i = 0; i < count; i++) {
      const raw = storage.get(AMENDMENT_PREFIX + id + ':' + i);
      if (raw) result.push(JSON.parse(raw));
    }
    return result;
  },

  proposalCount() {
    return storage.get(COUNT_KEY) || '0';
  },

  getLockedBalance(address) {
    return storage.get(LOCKED_PREFIX + String(address).toLowerCase()) || '0';
  },

  // Returns all approved library names + sources. The VM reads this
  // before every contract execution to inject the approved libraries
  // as frozen Compartment globals. Deterministic across all nodes.
  getApprovedLibraries() {
    const indexRaw = storage.get('libraryIndex') || '[]';
    const index = JSON.parse(indexRaw);
    const result = [];
    for (let i = 0; i < index.length; i++) {
      const name = index[i];
      const source = storage.get('lib:' + name);
      if (source) {
        result.push({ name: name, source: source });
      }
    }
    return result;
  },

  getApprovedLibrary(name) {
    const source = storage.get('lib:' + name);
    return source ? { name: name, source: source } : null;
  },
});
`.trim();
var GOVERNANCE_CONTRACT_ADDRESS = new Uint8Array([
  0,
  0,
  0,
  0,
  0,
  0,
  0,
  0,
  0,
  0,
  0,
  0,
  0,
  0,
  0,
  0,
  0,
  0,
  0,
  3
]);
async function deployStakingContractAtGenesis(store, vm, chainId, initialValidators) {
  const sourceBytes = new TextEncoder().encode(STAKING_CONTRACT_SOURCE);
  const codeHash = blake32(sourceBytes);
  await store.putCode(codeHash, sourceBytes);
  const zeroHash = new Uint8Array(32);
  const placeholder = {
    balance: 0n,
    nonce: 0n,
    codeHash,
    storageRoot: zeroHash
  };
  await store.setAccount(STAKING_CONTRACT_ADDRESS, placeholder);
  const snapshot = /* @__PURE__ */ new Map();
  const current = /* @__PURE__ */ new Map();
  const storageAdapter = {
    get(key) {
      return current.get(key);
    },
    set(key, value) {
      current.set(key, value);
    },
    delete(key) {
      return current.delete(key);
    },
    has(key) {
      return current.has(key);
    }
  };
  vm.deploy(
    {
      source: STAKING_CONTRACT_SOURCE,
      context: {
        sender: "0x" + "00".repeat(20),
        contractAddress: bytesToHex(STAKING_CONTRACT_ADDRESS),
        value: 0n,
        blockNumber: 0n,
        blockTimestamp: 0n,
        chainId
      },
      storage: storageAdapter,
      gasLimit: 50000000n
    },
    [
      {
        initialValidators: initialValidators.map((v) => ({
          address: v.address.toLowerCase(),
          pubKey: v.pubKey,
          stake: String(v.stake)
        }))
      }
    ]
  );
  for (const [key, value] of current) {
    if (snapshot.get(key) !== value) {
      await store.setStorageValue(STAKING_CONTRACT_ADDRESS, key, value);
    }
  }
  let totalInitialStake = 0n;
  for (const v of initialValidators) {
    totalInitialStake += BigInt(v.stake);
  }
  const storageRoot = await store.computeContractStorageRoot(STAKING_CONTRACT_ADDRESS);
  const finalAccount = {
    balance: totalInitialStake,
    nonce: 0n,
    codeHash,
    storageRoot
  };
  await store.setAccount(STAKING_CONTRACT_ADDRESS, finalAccount);
}
async function deploySlashingContractAtGenesis(store, vm, chainId) {
  const sourceBytes = new TextEncoder().encode(SLASHING_CONTRACT_SOURCE);
  const codeHash = blake32(sourceBytes);
  await store.putCode(codeHash, sourceBytes);
  const zeroHash = new Uint8Array(32);
  const placeholder = {
    balance: 0n,
    nonce: 0n,
    codeHash,
    storageRoot: zeroHash
  };
  await store.setAccount(SLASHING_CONTRACT_ADDRESS, placeholder);
  const current = /* @__PURE__ */ new Map();
  const storageAdapter = {
    get(key) {
      return current.get(key);
    },
    set(key, value) {
      current.set(key, value);
    },
    delete(key) {
      return current.delete(key);
    },
    has(key) {
      return current.has(key);
    }
  };
  vm.deploy(
    {
      source: SLASHING_CONTRACT_SOURCE,
      context: {
        sender: "0x" + "00".repeat(20),
        contractAddress: bytesToHex(SLASHING_CONTRACT_ADDRESS),
        value: 0n,
        blockNumber: 0n,
        blockTimestamp: 0n,
        chainId
      },
      storage: storageAdapter,
      gasLimit: 50000000n
    },
    [{}]
  );
  for (const [key, value] of current) {
    await store.setStorageValue(SLASHING_CONTRACT_ADDRESS, key, value);
  }
  const storageRoot = await store.computeContractStorageRoot(SLASHING_CONTRACT_ADDRESS);
  const finalAccount = {
    balance: 0n,
    nonce: 0n,
    codeHash,
    storageRoot
  };
  await store.setAccount(SLASHING_CONTRACT_ADDRESS, finalAccount);
}
async function deployGovernanceContractAtGenesis(store, vm, chainId) {
  const sourceBytes = new TextEncoder().encode(GOVERNANCE_CONTRACT_SOURCE);
  const codeHash = blake32(sourceBytes);
  await store.putCode(codeHash, sourceBytes);
  const zeroHash = new Uint8Array(32);
  const placeholder = {
    balance: 0n,
    nonce: 0n,
    codeHash,
    storageRoot: zeroHash
  };
  await store.setAccount(GOVERNANCE_CONTRACT_ADDRESS, placeholder);
  const current = /* @__PURE__ */ new Map();
  const storageAdapter = {
    get(key) {
      return current.get(key);
    },
    set(key, value) {
      current.set(key, value);
    },
    delete(key) {
      return current.delete(key);
    },
    has(key) {
      return current.has(key);
    }
  };
  vm.deploy(
    {
      source: GOVERNANCE_CONTRACT_SOURCE,
      context: {
        sender: "0x" + "00".repeat(20),
        contractAddress: bytesToHex(GOVERNANCE_CONTRACT_ADDRESS),
        value: 0n,
        blockNumber: 0n,
        blockTimestamp: 0n,
        chainId
      },
      storage: storageAdapter,
      gasLimit: 50000000n
    },
    [{}]
  );
  for (const [key, value] of current) {
    await store.setStorageValue(GOVERNANCE_CONTRACT_ADDRESS, key, value);
  }
  const storageRoot = await store.computeContractStorageRoot(GOVERNANCE_CONTRACT_ADDRESS);
  const finalAccount = {
    balance: 0n,
    nonce: 0n,
    codeHash,
    storageRoot
  };
  await store.setAccount(GOVERNANCE_CONTRACT_ADDRESS, finalAccount);
}
function bytesToHex(bytes) {
  let hex = "0x";
  for (let i = 0; i < bytes.length; i++) {
    hex += (bytes[i] ?? 0).toString(16).padStart(2, "0");
  }
  return hex;
}

// src/chain/vm-integration.ts
var ContractLoadNeeded = class extends Error {
  constructor(contractAddress) {
    super(`contract ${contractAddress} needs loading`);
    this.contractAddress = contractAddress;
    this.name = "ContractLoadNeeded";
  }
  contractAddress;
};
var MIN_CONTRACT_GAS = 50000n;
function isDeployTransaction(tx) {
  for (const byte of tx.body.to) {
    if (byte !== 0) return false;
  }
  return true;
}
function isContractAccount(account) {
  if (!account) return false;
  for (const byte of account.codeHash) {
    if (byte !== 0) return true;
  }
  return false;
}
function deriveContractAddress(deployer, nonce) {
  const nonceBytes = new Uint8Array(8);
  let v = nonce;
  for (let i = 7; i >= 0; i--) {
    nonceBytes[i] = Number(v & 0xffn);
    v >>= 8n;
  }
  const input = new Uint8Array(deployer.length + 8);
  input.set(deployer, 0);
  input.set(nonceBytes, deployer.length);
  return blake33(input).slice(0, 20);
}
async function applyDeployTransaction(store, tx, context) {
  const senderAddress = addressBytesFromPublicKey3(tx.publicKey);
  const bodyHash = hashTransactionBody(tx.body);
  if (!verify(bodyHash, tx.signature, tx.publicKey)) {
    return { ok: false, reason: "invalid signature" };
  }
  const sender = await store.getOrCreate(senderAddress);
  if (sender.nonce !== tx.body.nonce) {
    return {
      ok: false,
      reason: `nonce mismatch: expected ${sender.nonce}, got ${tx.body.nonce}`
    };
  }
  if (tx.body.gasLimit < MIN_CONTRACT_GAS) {
    return {
      ok: false,
      reason: `gas limit too low for contract deploy: min ${MIN_CONTRACT_GAS}`
    };
  }
  const maxCost = tx.body.gasLimit * tx.body.maxFeePerGas + tx.body.value;
  if (sender.balance < maxCost) {
    return {
      ok: false,
      reason: `insufficient balance for value+gas: need ${maxCost}, have ${sender.balance}`
    };
  }
  const source = new TextDecoder("utf-8").decode(tx.body.data);
  if (source.length === 0) {
    return { ok: false, reason: "empty contract source" };
  }
  const contractAddress = deriveContractAddress(senderAddress, sender.nonce);
  const codeHash = blake33(tx.body.data);
  await store.putCode(codeHash, tx.body.data);
  const storageAdapter = await makeStorageAdapter(store, contractAddress);
  let gasUsed = 0n;
  let events = [];
  try {
    const result = context.vm.deploy({
      source,
      context: buildVmContext(senderAddress, contractAddress, tx, context),
      storage: storageAdapter,
      gasLimit: tx.body.gasLimit
    });
    gasUsed = result.gasUsed;
    events = result.events;
  } catch (err) {
    const feePaid2 = tx.body.gasLimit * tx.body.maxFeePerGas;
    await deductFromSender(store, senderAddress, sender, feePaid2);
    return {
      ok: true,
      status: 0,
      kind: "deploy",
      senderAddress,
      contractAddress,
      gasUsed: tx.body.gasLimit,
      feePaid: feePaid2,
      events: [],
      reason: `contract deploy failed: ${err instanceof Error ? err.message : String(err)}`
    };
  }
  await storageAdapter.flush();
  const storageRoot = await store.computeContractStorageRoot(contractAddress);
  const contractAccount = {
    balance: tx.body.value,
    nonce: 0n,
    codeHash,
    storageRoot
  };
  await store.setAccount(contractAddress, contractAccount);
  const feePaid = gasUsed * tx.body.maxFeePerGas;
  const updatedSender = {
    balance: sender.balance - feePaid - tx.body.value,
    nonce: sender.nonce + 1n,
    codeHash: sender.codeHash,
    storageRoot: sender.storageRoot
  };
  await store.setAccount(senderAddress, updatedSender);
  return {
    ok: true,
    status: 1,
    kind: "deploy",
    senderAddress,
    contractAddress,
    gasUsed,
    feePaid,
    events
  };
}
async function applyCallTransaction(store, tx, context, recipientAccount) {
  const senderAddress = addressBytesFromPublicKey3(tx.publicKey);
  const contractAddress = tx.body.to;
  const bodyHash = hashTransactionBody(tx.body);
  if (!verify(bodyHash, tx.signature, tx.publicKey)) {
    return { ok: false, reason: "invalid signature" };
  }
  const sender = await store.getOrCreate(senderAddress);
  if (sender.nonce !== tx.body.nonce) {
    return {
      ok: false,
      reason: `nonce mismatch: expected ${sender.nonce}, got ${tx.body.nonce}`
    };
  }
  if (tx.body.gasLimit < MIN_CONTRACT_GAS) {
    return {
      ok: false,
      reason: `gas limit too low for contract call: min ${MIN_CONTRACT_GAS}`
    };
  }
  const maxCost = tx.body.gasLimit * tx.body.maxFeePerGas;
  if (sender.balance < maxCost + tx.body.value) {
    return {
      ok: false,
      reason: `insufficient balance: need ${maxCost + tx.body.value}, have ${sender.balance}`
    };
  }
  let method;
  let args;
  try {
    const decoded = JSON.parse(new TextDecoder("utf-8").decode(tx.body.data));
    if (typeof decoded.method !== "string") {
      return { ok: false, reason: "call data missing string `method` field" };
    }
    method = decoded.method;
    args = Array.isArray(decoded.args) ? decoded.args : [];
  } catch (err) {
    return {
      ok: false,
      reason: `invalid call data: ${err instanceof Error ? err.message : String(err)}`
    };
  }
  if (bytesEqual3(contractAddress, SLASHING_CONTRACT_ADDRESS) && method === "recordSlash") {
    const ev = verifySlashingEvidence(args);
    if (!ev.ok) {
      return { ok: false, reason: `slashing evidence rejected: ${ev.reason}` };
    }
  }
  const sourceBytes = await store.getCode(recipientAccount.codeHash);
  if (!sourceBytes) {
    return { ok: false, reason: "contract code not found for recipient" };
  }
  const source = new TextDecoder("utf-8").decode(sourceBytes);
  const storageAdapter = await makeStorageAdapter(store, contractAddress);
  const govAddr = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3]);
  let txApprovedLibraries;
  try {
    const li = await store.getStorageValue(govAddr, "libraryIndex");
    if (li) {
      const idx = JSON.parse(li);
      if (idx.length > 0) {
        txApprovedLibraries = [];
        for (const n of idx) {
          const s = await store.getStorageValue(govAddr, "lib:" + n);
          if (s) txApprovedLibraries.push({ name: n, source: s });
        }
      }
    }
  } catch {
  }
  const allStorageAdapters = [];
  const callStack = [];
  allStorageAdapters.push({ address: contractAddress, adapter: storageAdapter });
  callStack.push(bytesToHex2(contractAddress).toLowerCase());
  const crossCall = (callerAddr, targetAddr, callMethod, callArgs) => {
    const targetLower = targetAddr.toLowerCase();
    if (callStack.includes(targetLower)) {
      throw new Error(`reentrant call to ${targetAddr} blocked (already on call stack)`);
    }
    const targetAddrBytes = hexToBytes(targetAddr);
    const cachedSource = contractSourceCache.get(targetLower);
    const cachedStorage = contractStorageCache.get(targetLower);
    if (!cachedSource || !cachedStorage) {
      throw new ContractLoadNeeded(targetAddr);
    }
    callStack.push(targetLower);
    try {
      allStorageAdapters.push({ address: targetAddrBytes, adapter: cachedStorage });
      const targetContext = {
        sender: callerAddr,
        contractAddress: targetAddr,
        value: 0n,
        blockNumber: context.blockNumber,
        blockTimestamp: context.blockTimestamp,
        chainId: context.chainId
      };
      const result = context.vm.call(
        {
          source: cachedSource,
          context: targetContext,
          storage: cachedStorage,
          gasLimit: tx.body.gasLimit,
          // shared gas budget
          approvedLibraries: txApprovedLibraries,
          crossCall
        },
        callMethod,
        callArgs
      );
      events.push(...result.events);
      outboundTransfers.push(...result.outboundTransfers);
      return result.returnValue;
    } finally {
      callStack.pop();
    }
  };
  const contractSourceCache = /* @__PURE__ */ new Map();
  const contractStorageCache = /* @__PURE__ */ new Map();
  contractSourceCache.set(bytesToHex2(contractAddress).toLowerCase(), source);
  contractStorageCache.set(bytesToHex2(contractAddress).toLowerCase(), storageAdapter);
  async function preloadContract(addr) {
    const addrLower = addr.toLowerCase();
    if (contractSourceCache.has(addrLower)) return;
    const addrBytes = hexToBytes(addr);
    const acct = await store.getOrCreate(addrBytes);
    if (!acct.codeHash || acct.codeHash.every((b) => b === 0)) return;
    const srcBytes = await store.getCode(acct.codeHash);
    if (!srcBytes) return;
    contractSourceCache.set(addrLower, new TextDecoder("utf-8").decode(srcBytes));
    const sa = await makeStorageAdapter(store, addrBytes);
    contractStorageCache.set(addrLower, sa);
  }
  for (const arg of args) {
    if (typeof arg === "string" && /^0x[0-9a-fA-F]{40}$/.test(arg)) {
      await preloadContract(arg);
    }
  }
  await preloadContract("0x0000000000000000000000000000000000000001");
  await preloadContract("0x0000000000000000000000000000000000000002");
  await preloadContract("0x0000000000000000000000000000000000000003");
  let gasUsed = 0n;
  let events = [];
  let returnValue;
  let outboundTransfers = [];
  const MAX_CROSS_LOAD_RETRIES = 10;
  let vmSuccess = false;
  for (let attempt = 0; attempt <= MAX_CROSS_LOAD_RETRIES; attempt++) {
    events = [];
    outboundTransfers = [];
    allStorageAdapters.length = 0;
    callStack.length = 0;
    const freshStorageAdapter = await makeStorageAdapter(store, contractAddress);
    contractStorageCache.set(bytesToHex2(contractAddress).toLowerCase(), freshStorageAdapter);
    allStorageAdapters.push({ address: contractAddress, adapter: freshStorageAdapter });
    callStack.push(bytesToHex2(contractAddress).toLowerCase());
    try {
      const result = context.vm.call(
        {
          source,
          context: buildVmContext(senderAddress, contractAddress, tx, context),
          storage: freshStorageAdapter,
          gasLimit: tx.body.gasLimit,
          approvedLibraries: txApprovedLibraries,
          crossCall
        },
        method,
        args
      );
      gasUsed = result.gasUsed;
      events = result.events;
      returnValue = result.returnValue;
      outboundTransfers = result.outboundTransfers;
      vmSuccess = true;
      break;
    } catch (err) {
      const loadNeeded = extractContractLoadNeeded(err);
      if (loadNeeded && attempt < MAX_CROSS_LOAD_RETRIES) {
        await preloadContract(loadNeeded);
        continue;
      }
      const feePaid2 = tx.body.gasLimit * tx.body.maxFeePerGas;
      await deductFromSender(store, senderAddress, sender, feePaid2);
      return {
        ok: true,
        status: 0,
        kind: "call",
        senderAddress,
        contractAddress,
        gasUsed: tx.body.gasLimit,
        feePaid: feePaid2,
        events: [],
        reason: `contract call failed: ${err instanceof Error ? err.message : String(err)}`
      };
    }
  }
  if (!vmSuccess) {
    const feePaid2 = tx.body.gasLimit * tx.body.maxFeePerGas;
    await deductFromSender(store, senderAddress, sender, feePaid2);
    return {
      ok: true,
      status: 0,
      kind: "call",
      senderAddress,
      contractAddress,
      gasUsed: tx.body.gasLimit,
      feePaid: feePaid2,
      events: [],
      reason: "cross-contract call loading exceeded max retries"
    };
  }
  const contractBalanceAfterInbound = recipientAccount.balance + tx.body.value;
  const totalOutbound = outboundTransfers.reduce((acc, t) => acc + t.amount, 0n);
  if (totalOutbound > contractBalanceAfterInbound) {
    const feePaid2 = gasUsed * tx.body.maxFeePerGas;
    await deductFromSender(store, senderAddress, sender, feePaid2);
    return {
      ok: true,
      status: 0,
      kind: "call",
      senderAddress,
      contractAddress,
      gasUsed,
      feePaid: feePaid2,
      events: [],
      reason: `outbound transfers exceed contract balance: need ${totalOutbound}, have ${contractBalanceAfterInbound}`
    };
  }
  for (const { adapter } of allStorageAdapters) {
    await adapter.flush();
  }
  for (const t of outboundTransfers) {
    const toBytes = hexToBytes(t.to);
    const recipient = await store.getOrCreate(toBytes);
    await store.setAccount(toBytes, {
      balance: recipient.balance + t.amount,
      nonce: recipient.nonce,
      codeHash: recipient.codeHash,
      storageRoot: recipient.storageRoot
    });
    events.push({
      name: "Transfer",
      data: { from: bytesToHex2(contractAddress), to: t.to, amount: String(t.amount) }
    });
  }
  const storageRoot = await store.computeContractStorageRoot(contractAddress);
  const updatedContract = {
    balance: contractBalanceAfterInbound - totalOutbound,
    nonce: recipientAccount.nonce,
    codeHash: recipientAccount.codeHash,
    storageRoot
  };
  await store.setAccount(contractAddress, updatedContract);
  const feePaid = gasUsed * tx.body.maxFeePerGas;
  const totalDebit = feePaid + tx.body.value;
  const updatedSender = {
    balance: sender.balance - totalDebit,
    nonce: sender.nonce + 1n,
    codeHash: sender.codeHash,
    storageRoot: sender.storageRoot
  };
  await store.setAccount(senderAddress, updatedSender);
  return {
    ok: true,
    status: 1,
    kind: "call",
    senderAddress,
    contractAddress,
    gasUsed,
    feePaid,
    events,
    returnValue
  };
}
async function makeStorageAdapter(store, contractAddress) {
  const initialEntries = await store.allStorageForContract(contractAddress);
  const initial = /* @__PURE__ */ new Map();
  for (const { key, value } of initialEntries) {
    initial.set(key, value);
  }
  const current = new Map(initial);
  return {
    get(key) {
      return current.get(key);
    },
    set(key, value) {
      current.set(key, value);
    },
    delete(key) {
      return current.delete(key);
    },
    has(key) {
      return current.has(key);
    },
    async flush() {
      for (const [key, value] of current) {
        if (initial.get(key) !== value) {
          await store.setStorageValue(contractAddress, key, value);
        }
      }
      for (const key of initial.keys()) {
        if (!current.has(key)) {
          await store.deleteStorageValue(contractAddress, key);
        }
      }
    }
  };
}
function buildVmContext(sender, contractAddress, tx, exec) {
  return {
    sender: bytesToHex2(sender),
    contractAddress: bytesToHex2(contractAddress),
    value: tx.body.value,
    blockNumber: exec.blockNumber,
    blockTimestamp: exec.blockTimestamp,
    chainId: exec.chainId
  };
}
async function deductFromSender(store, senderAddress, sender, amount) {
  const debit = amount > sender.balance ? sender.balance : amount;
  const updated = {
    balance: sender.balance - debit,
    nonce: sender.nonce + 1n,
    codeHash: sender.codeHash,
    storageRoot: sender.storageRoot
  };
  await store.setAccount(senderAddress, updated);
}
function extractContractLoadNeeded(err) {
  if (err instanceof ContractLoadNeeded) return err.contractAddress;
  if (err instanceof Error && err.cause instanceof ContractLoadNeeded) {
    return err.cause.contractAddress;
  }
  if (err instanceof Error && err.cause instanceof Error) {
    return extractContractLoadNeeded(err.cause);
  }
  if (err instanceof Error && err.name === "ContractLoadNeeded" && "contractAddress" in err) {
    return err.contractAddress;
  }
  return null;
}
function bytesToHex2(bytes) {
  let hex = "0x";
  for (let i = 0; i < bytes.length; i++) {
    hex += (bytes[i] ?? 0).toString(16).padStart(2, "0");
  }
  return hex;
}
function hexToBytes(hex) {
  const clean = hex.startsWith("0x") ? hex.slice(2) : hex;
  if (clean.length % 2 !== 0) {
    throw new Error(`hexToBytes: odd-length hex '${hex}'`);
  }
  const out = new Uint8Array(clean.length / 2);
  for (let i = 0; i < out.length; i++) {
    out[i] = parseInt(clean.slice(i * 2, i * 2 + 2), 16);
  }
  return out;
}
function bytesEqual3(a, b) {
  if (a.length !== b.length) return false;
  for (let i = 0; i < a.length; i++) {
    if (a[i] !== b[i]) return false;
  }
  return true;
}

// src/chain/apply.ts
var TRANSFER_GAS_COST = 21000n;
function hashTransactionBody(body) {
  return blake34(TransactionBodySchema.serialize(body));
}
async function applyTransaction(store, tx, execContext) {
  if (execContext) {
    if (isDeployTransaction(tx)) {
      const r = await applyDeployTransaction(store, tx, execContext);
      if (r.ok) {
        return {
          ok: true,
          status: r.status,
          kind: "deploy",
          senderAddress: r.senderAddress,
          recipientAddress: r.contractAddress,
          gasUsed: r.gasUsed,
          feePaid: r.feePaid,
          events: r.events?.map((e) => ({ name: e.name, data: e.data })) ?? [],
          reason: r.reason
        };
      }
      return { ok: false, reason: r.reason };
    }
    const recipient2 = await store.getAccount(tx.body.to);
    if (isContractAccount(recipient2) && recipient2 !== null) {
      const r = await applyCallTransaction(store, tx, execContext, recipient2);
      if (r.ok) {
        return {
          ok: true,
          status: r.status,
          kind: "call",
          senderAddress: r.senderAddress,
          recipientAddress: r.contractAddress,
          gasUsed: r.gasUsed,
          feePaid: r.feePaid,
          events: r.events?.map((e) => ({ name: e.name, data: e.data })) ?? [],
          returnValue: r.returnValue,
          reason: r.reason
        };
      }
      return { ok: false, reason: r.reason };
    }
  }
  const senderAddress = addressBytesFromPublicKey4(tx.publicKey);
  const recipientAddress = tx.body.to;
  const bodyHash = hashTransactionBody(tx.body);
  if (!verify2(bodyHash, tx.signature, tx.publicKey)) {
    return { ok: false, reason: "invalid signature" };
  }
  const sender = await store.getOrCreate(senderAddress);
  if (sender.nonce !== tx.body.nonce) {
    return {
      ok: false,
      reason: `nonce mismatch: expected ${sender.nonce}, got ${tx.body.nonce}`
    };
  }
  const maxGasCost = tx.body.gasLimit * tx.body.maxFeePerGas;
  const maxRequired = tx.body.value + maxGasCost;
  if (sender.balance < maxRequired) {
    return {
      ok: false,
      reason: `insufficient balance: need ${maxRequired}, have ${sender.balance}`
    };
  }
  const gasUsed = TRANSFER_GAS_COST;
  const feePaid = gasUsed * tx.body.maxFeePerGas;
  const updatedSender = {
    balance: sender.balance - tx.body.value - feePaid,
    nonce: sender.nonce + 1n,
    codeHash: sender.codeHash,
    storageRoot: sender.storageRoot
  };
  await store.setAccount(senderAddress, updatedSender);
  const recipient = await store.getOrCreate(recipientAddress);
  const updatedRecipient = {
    balance: recipient.balance + tx.body.value,
    nonce: recipient.nonce,
    codeHash: recipient.codeHash,
    storageRoot: recipient.storageRoot
  };
  await store.setAccount(recipientAddress, updatedRecipient);
  return {
    ok: true,
    status: 1,
    kind: "transfer",
    senderAddress,
    recipientAddress,
    gasUsed,
    feePaid
  };
}

// src/chain/receipts.ts
import { blake3 as blake35 } from "@asentum/crypto";
import {
  ByteListType,
  ByteVectorType,
  ContainerType,
  ListCompositeType,
  UintBigintType,
  UintNumberType
} from "@chainsafe/ssz";
import { ZERO_HASH } from "@asentum/types";
var MAX_EVENT_NAME_BYTES = 256;
var MAX_EVENT_DATA_BYTES = 16384;
var MAX_EVENTS_PER_RECEIPT = 256;
var MAX_RETURN_BYTES = 16384;
var MAX_ERROR_BYTES = 1024;
var RECEIPT_KIND_TRANSFER = 0;
var RECEIPT_KIND_DEPLOY = 1;
var RECEIPT_KIND_CALL = 2;
var ReceiptEventSchema = new ContainerType({
  name: new ByteListType(MAX_EVENT_NAME_BYTES),
  data: new ByteListType(MAX_EVENT_DATA_BYTES)
});
var ReceiptSchema = new ContainerType({
  txHash: new ByteVectorType(32),
  blockNumber: new UintBigintType(8),
  txIndex: new UintNumberType(4),
  sender: new ByteVectorType(20),
  recipient: new ByteVectorType(20),
  kind: new UintNumberType(1),
  success: new UintNumberType(1),
  gasUsed: new UintBigintType(8),
  events: new ListCompositeType(ReceiptEventSchema, MAX_EVENTS_PER_RECEIPT),
  returnValue: new ByteListType(MAX_RETURN_BYTES),
  errorReason: new ByteListType(MAX_ERROR_BYTES)
});
function computeReceiptsRoot(receipts) {
  if (receipts.length === 0) return new Uint8Array(ZERO_HASH);
  const parts = [];
  let total = 0;
  for (const r of receipts) {
    const bytes = ReceiptSchema.serialize(r);
    parts.push(bytes);
    total += bytes.length;
  }
  const combined = new Uint8Array(total);
  let off = 0;
  for (const p of parts) {
    combined.set(p, off);
    off += p.length;
  }
  return blake35(combined);
}
function buildReceipt(txHash, blockNumber, txIndex, source) {
  const kind = source.kind === "deploy" ? RECEIPT_KIND_DEPLOY : source.kind === "call" ? RECEIPT_KIND_CALL : RECEIPT_KIND_TRANSFER;
  const events = (source.events ?? []).map((e) => ({
    name: new TextEncoder().encode(String(e.name)),
    data: new TextEncoder().encode(
      JSON.stringify(e.data, bigintReplacer) ?? "null"
    )
  }));
  const returnValue = source.returnValue !== void 0 ? new TextEncoder().encode(
    JSON.stringify(source.returnValue, bigintReplacer) ?? "null"
  ) : new Uint8Array(0);
  return {
    txHash,
    blockNumber,
    txIndex,
    sender: source.senderAddress,
    recipient: source.recipientAddress,
    kind,
    success: source.success ?? 1,
    gasUsed: source.gasUsed,
    events,
    returnValue,
    errorReason: source.errorReason ? new TextEncoder().encode(source.errorReason) : new Uint8Array(0)
  };
}
function bigintReplacer(_key, value) {
  if (typeof value === "bigint") return value.toString();
  if (value instanceof Uint8Array) {
    let hex = "0x";
    for (let i = 0; i < value.length; i++) {
      hex += (value[i] ?? 0).toString(16).padStart(2, "0");
    }
    return hex;
  }
  return value;
}

// src/chain/block-builder.ts
async function buildBlock(options) {
  const { store, parentHeader, mempool, proposer, timestamp, gasLimit, baseFeePerGas } = options;
  const applied = [];
  const receipts = [];
  let gasUsed = 0n;
  const blockNumber = parentHeader.number + 1n;
  const fullExecContext = options.execContext ? {
    ...options.execContext,
    blockNumber,
    blockTimestamp: timestamp
  } : void 0;
  if (options.applyCronHook) {
    await options.applyCronHook(store, fullExecContext);
  }
  for (const tx of mempool) {
    if (gasUsed + tx.body.gasLimit > gasLimit) break;
    const result = await applyTransaction(store, tx, fullExecContext);
    if (result.ok) {
      const txHash = blake36(SignedTransactionSchema.serialize(tx));
      const receipt = buildReceipt(txHash, blockNumber, applied.length, {
        kind: result.kind,
        senderAddress: result.senderAddress,
        recipientAddress: result.recipientAddress,
        gasUsed: result.gasUsed,
        events: result.events,
        returnValue: result.returnValue,
        success: result.status,
        errorReason: result.status === 0 ? result.reason : void 0
      });
      applied.push(tx);
      receipts.push(receipt);
      gasUsed += result.gasUsed;
    } else {
      console.error("[block-builder] tx dropped:", result.reason ?? "unknown reason");
    }
  }
  if (options.applySubsidiesHook) {
    await options.applySubsidiesHook(store);
  }
  const stateRoot = await store.stateRoot();
  const transactionsRoot = computeTransactionsRoot(applied);
  const receiptsRoot = computeReceiptsRoot(receipts);
  const parentHash = blake36(BlockHeaderSchema.serialize(parentHeader));
  const header = {
    parentHash,
    stateRoot,
    transactionsRoot,
    receiptsRoot,
    number: blockNumber,
    timestamp,
    gasLimit,
    gasUsed,
    baseFeePerGas,
    proposer,
    lastCommitBitmap: options.lastCommitBitmap
  };
  return { block: { header, transactions: applied }, receipts };
}
function computeTransactionsRoot(txs) {
  if (txs.length === 0) return new Uint8Array(ZERO_HASH2);
  const parts = [];
  let total = 0;
  for (const tx of txs) {
    const bytes = SignedTransactionSchema.serialize(tx);
    parts.push(bytes);
    total += bytes.length;
  }
  const combined = new Uint8Array(total);
  let off = 0;
  for (const p of parts) {
    combined.set(p, off);
    off += p.length;
  }
  return blake36(combined);
}

// src/chain/genesis.ts
import {
  ZERO_ADDRESS_BYTES,
  ZERO_HASH as ZERO_HASH3,
  emptyAccount
} from "@asentum/types";

// src/chain/cron-registry.ts
import { blake3 as blake37 } from "@asentum/crypto";
import "@asentum/types";
function bytesToHex3(bytes) {
  let h = "0x";
  for (let i = 0; i < bytes.length; i++) h += (bytes[i] ?? 0).toString(16).padStart(2, "0");
  return h;
}
var CRON_REGISTRY_ADDRESS = (() => {
  const addr = new Uint8Array(20);
  addr[19] = 6;
  return addr;
})();
var CHAIN_CALLER = new Uint8Array(20);
var CRON_REGISTRY_SOURCE = `
const NEXT_JOB_ID_KEY = 'nextJobId';
const JOB_PREFIX = 'job:';
const DUE_PREFIX = 'due:';
const OWNER_PREFIX = 'owner:';
const CHAIN_CALLER = '0x0000000000000000000000000000000000000000';
const MIN_INTERVAL = 60n;
const MAX_INTERVAL = 31_536_000_000n; // 1000 years, safety cap
const MAX_GAS_LIMIT = 5_000_000n;
const MIN_GAS_LIMIT = 21_000n;

// Right-pad a unix timestamp to 14 digits so lexicographic storage
// key sort matches numeric sort. Years until ~5138 fit in 14 digits.
function tsKey(ts) {
  return String(ts).padStart(14, '0');
}

({
  // Schedule a recurring or one-shot call.
  // intervalSec = 0 \u2192 one-shot, fires once and is removed.
  // maxRuns = 0    \u2192 run forever, until escrow is exhausted or cancel().
  // gasLimit is per-run, in gas units. Escrow must cover at least one run
  // at the current base fee.
  schedule(target, method, args, nextRunAt, intervalSec, maxRuns, gasLimit) {
    assert(typeof target === 'string' && /^0x[0-9a-fA-F]{40}$/.test(target),
      'invalid target address');
    assert(typeof method === 'string' && method.length > 0, 'method required');

    const next = BigInt(nextRunAt);
    assert(next > BigInt(chain.timestamp), 'nextRunAt must be in the future');

    const interval = BigInt(intervalSec);
    assert(
      interval === 0n || (interval >= MIN_INTERVAL && interval <= MAX_INTERVAL),
      'interval must be 0 or between MIN_INTERVAL and MAX_INTERVAL seconds',
    );

    const gas = BigInt(gasLimit);
    assert(gas >= MIN_GAS_LIMIT && gas <= MAX_GAS_LIMIT, 'gasLimit out of range');

    const maxR = BigInt(maxRuns);
    assert(maxR >= 0n, 'maxRuns must be non-negative');

    const escrow = BigInt(msg.value);
    const baseFee = BigInt(chain.baseFee || 1n);
    assert(escrow >= gas * baseFee, 'escrow must cover at least one run at the current base fee');

    const id = String(BigInt(storage.get(NEXT_JOB_ID_KEY) || '0') + 1n);
    storage.set(NEXT_JOB_ID_KEY, id);

    const job = {
      id,
      owner: String(msg.sender).toLowerCase(),
      target: target.toLowerCase(),
      method,
      args: args || [],
      nextRunAt: next.toString(),
      intervalSec: interval.toString(),
      maxRuns: maxR.toString(),
      runsLeft: maxR.toString(),
      gasLimit: gas.toString(),
      gasEscrow: escrow.toString(),
    };
    storage.set(JOB_PREFIX + id, JSON.stringify(job));
    storage.set(DUE_PREFIX + tsKey(job.nextRunAt) + ':' + id, '1');
    storage.set(OWNER_PREFIX + job.owner + ':' + id, '1');

    emit('JobScheduled', {
      id,
      owner: job.owner,
      target: job.target,
      method,
      nextRunAt: job.nextRunAt,
      intervalSec: job.intervalSec,
    });
    return id;
  },

  // Top up an existing job's gas escrow. Anyone can top up any job
  // (good for sponsored execution patterns).
  topUp(id) {
    const raw = storage.get(JOB_PREFIX + id);
    assert(raw, 'no such job');
    const job = JSON.parse(raw);
    job.gasEscrow = (BigInt(job.gasEscrow) + BigInt(msg.value)).toString();
    storage.set(JOB_PREFIX + id, JSON.stringify(job));
    emit('JobToppedUp', { id, amount: String(msg.value), totalEscrow: job.gasEscrow });
    return true;
  },

  // Cancel a job. Only callable by the job's owner. Refunds remaining
  // escrow to the owner via transfer().
  cancel(id) {
    const raw = storage.get(JOB_PREFIX + id);
    assert(raw, 'no such job');
    const job = JSON.parse(raw);
    assert(job.owner === String(msg.sender).toLowerCase(), 'not the owner');
    storage.delete(JOB_PREFIX + id);
    storage.delete(DUE_PREFIX + tsKey(job.nextRunAt) + ':' + id);
    storage.delete(OWNER_PREFIX + job.owner + ':' + id);
    const refund = BigInt(job.gasEscrow);
    if (refund > 0n) transfer(msg.sender, refund);
    emit('JobCancelled', { id, owner: job.owner, refunded: refund.toString() });
    return true;
  },

  // Read view: full job record or null.
  getJob(id) {
    const raw = storage.get(JOB_PREFIX + id);
    return raw ? JSON.parse(raw) : null;
  },

  // List due job ids up to currentTimestamp. Caller (the chain block
  // builder) walks this list and dispatches each one. Capped so a huge
  // backlog can't return an unbounded array.
  // Only callable by the chain itself (msg.sender = zero address).
  listDue(currentTimestamp, max) {
    assert(String(msg.sender).toLowerCase() === CHAIN_CALLER, 'restricted');
    const ts = BigInt(currentTimestamp);
    const cap = Number(max || 100);
    const ids = [];
    // Walk DUE_PREFIX in ascending order.
    for (const key of storage.keys(DUE_PREFIX)) {
      if (ids.length >= cap) break;
      // key shape: "due:<paddedTs>:<id>"
      const rest = key.slice(DUE_PREFIX.length);
      const colon = rest.indexOf(':');
      if (colon < 0) continue;
      const keyTs = BigInt(rest.slice(0, colon));
      if (keyTs > ts) break; // sorted, so anything after is in the future
      const id = rest.slice(colon + 1);
      ids.push(id);
    }
    return ids;
  },

  // Mark a job as having run. Decrement runsLeft, deduct escrow,
  // re-insert in due index at nextRunAt + intervalSec (or remove if
  // one-shot / out of runs / out of escrow). Only callable by chain.
  recordRun(id, gasUsed, success) {
    assert(String(msg.sender).toLowerCase() === CHAIN_CALLER, 'restricted');
    const raw = storage.get(JOB_PREFIX + id);
    if (!raw) return false;
    const job = JSON.parse(raw);

    const baseFee = BigInt(chain.baseFee || 1n);
    const cost = BigInt(gasUsed) * baseFee;
    let escrow = BigInt(job.gasEscrow) - cost;
    if (escrow < 0n) escrow = 0n;

    storage.delete(DUE_PREFIX + tsKey(job.nextRunAt) + ':' + id);

    const runsLeft = BigInt(job.runsLeft);
    const interval = BigInt(job.intervalSec);
    const maxRuns = BigInt(job.maxRuns);
    const newRunsLeft = maxRuns === 0n ? 0n : (runsLeft - 1n);

    emit('JobExecuted', { id, success: !!success, gasUsed: String(gasUsed), escrowLeft: escrow.toString() });

    // One-shot OR runs exhausted OR no escrow \u2192 remove.
    const isOneShot = interval === 0n;
    const exhaustedRuns = maxRuns !== 0n && newRunsLeft === 0n;
    const exhaustedEscrow = escrow < BigInt(job.gasLimit) * baseFee;

    if (isOneShot || exhaustedRuns || exhaustedEscrow) {
      storage.delete(JOB_PREFIX + id);
      storage.delete(OWNER_PREFIX + job.owner + ':' + id);
      const reason = isOneShot ? 'one-shot complete' : exhaustedRuns ? 'max runs reached' : 'escrow exhausted';
      emit('JobExhausted', { id, reason });
      if (escrow > 0n) transfer(job.owner, escrow);
      return true;
    }

    // Otherwise reschedule.
    const newNext = (BigInt(job.nextRunAt) + interval).toString();
    job.nextRunAt = newNext;
    job.runsLeft = newRunsLeft.toString();
    job.gasEscrow = escrow.toString();
    storage.set(JOB_PREFIX + id, JSON.stringify(job));
    storage.set(DUE_PREFIX + tsKey(newNext) + ':' + id, '1');
    return true;
  },
})
`;
async function installCronRegistry(opts) {
  const { store, vm, chainId } = opts;
  const sourceBytes = new TextEncoder().encode(CRON_REGISTRY_SOURCE);
  const codeHash = blake37(sourceBytes);
  await store.putCode(codeHash, sourceBytes);
  const zeroHash = new Uint8Array(32);
  await store.setAccount(CRON_REGISTRY_ADDRESS, {
    balance: 0n,
    nonce: 0n,
    codeHash,
    storageRoot: zeroHash
  });
  const inMem = /* @__PURE__ */ new Map();
  vm.deploy(
    {
      source: CRON_REGISTRY_SOURCE,
      context: {
        sender: "0x" + "00".repeat(20),
        contractAddress: bytesToHex3(CRON_REGISTRY_ADDRESS),
        value: 0n,
        blockNumber: 0n,
        blockTimestamp: 0n,
        chainId
      },
      storage: {
        get: (k) => inMem.get(k),
        set: (k, v) => {
          inMem.set(k, v);
        },
        delete: (k) => inMem.delete(k),
        has: (k) => inMem.has(k)
      },
      gasLimit: 10000000n
    },
    []
  );
  for (const [k, v] of inMem) {
    await store.setStorageValue(CRON_REGISTRY_ADDRESS, k, v);
  }
  const storageRoot = await store.computeContractStorageRoot(CRON_REGISTRY_ADDRESS);
  await store.setAccount(CRON_REGISTRY_ADDRESS, {
    balance: 0n,
    nonce: 0n,
    codeHash,
    storageRoot
  });
}

// src/chain/ase-token.ts
import { blake3 as blake38 } from "@asentum/crypto";
import "@asentum/types";
function bytesToHex4(bytes) {
  let h = "0x";
  for (let i = 0; i < bytes.length; i++) h += (bytes[i] ?? 0).toString(16).padStart(2, "0");
  return h;
}
var ASE_TOKEN_ADDRESS = (() => {
  const addr = new Uint8Array(20);
  addr[19] = 7;
  return addr;
})();
var ASE_TOKEN_SOURCE = `
const NAME_KEY = 'meta:name';
const SYMBOL_KEY = 'meta:symbol';
const DECIMALS_KEY = 'meta:decimals';
const TOTAL_SUPPLY_KEY = 'meta:totalSupply';
const BALANCE_PREFIX = 'balance:';
const ALLOWANCE_PREFIX = 'allowance:';
const ZERO = '0x0000000000000000000000000000000000000000';
const CHAIN_CALLER = ZERO;

({
  // One-shot initialiser, called by the chain at genesis with the
  // migrated balance set. Subsequent calls revert.
  init(name, symbol, decimals, initialHolders) {
    assert(!storage.get(NAME_KEY), 'already initialised');
    assert(String(msg.sender).toLowerCase() === CHAIN_CALLER, 'init is chain-only');
    storage.set(NAME_KEY, String(name));
    storage.set(SYMBOL_KEY, String(symbol));
    storage.set(DECIMALS_KEY, String(decimals));

    let total = 0n;
    const holders = initialHolders || [];
    for (let i = 0; i < holders.length; i++) {
      const h = holders[i];
      const addr = String(h.address).toLowerCase();
      const amt = BigInt(h.balance);
      if (amt > 0n) {
        storage.set(BALANCE_PREFIX + addr, amt.toString());
        total = total + amt;
        emit('Transfer', { from: ZERO, to: addr, value: amt.toString() });
      }
    }
    storage.set(TOTAL_SUPPLY_KEY, total.toString());
    return true;
  },

  // ---- ARC-20 standard views ----
  name()        { return storage.get(NAME_KEY) || ''; },
  symbol()      { return storage.get(SYMBOL_KEY) || ''; },
  decimals()    { return storage.get(DECIMALS_KEY) || '18'; },
  totalSupply() { return storage.get(TOTAL_SUPPLY_KEY) || '0'; },
  balanceOf(addr) {
    return storage.get(BALANCE_PREFIX + String(addr).toLowerCase()) || '0';
  },
  allowance(owner, spender) {
    return storage.get(
      ALLOWANCE_PREFIX + String(owner).toLowerCase() + ':' + String(spender).toLowerCase(),
    ) || '0';
  },

  // ---- ARC-20 transfer ----
  transfer(to, amount) {
    return _move(String(msg.sender).toLowerCase(), String(to).toLowerCase(), amount);
  },

  approve(spender, amount) {
    const owner = String(msg.sender).toLowerCase();
    const sp = String(spender).toLowerCase();
    const amt = BigInt(amount);
    assert(amt >= 0n, 'amount must be non-negative');
    storage.set(ALLOWANCE_PREFIX + owner + ':' + sp, amt.toString());
    emit('Approval', { owner, spender: sp, value: amt.toString() });
    return true;
  },

  transferFrom(from, to, amount) {
    const owner = String(from).toLowerCase();
    const spender = String(msg.sender).toLowerCase();
    const recipient = String(to).toLowerCase();
    const allowKey = ALLOWANCE_PREFIX + owner + ':' + spender;
    const allowed = BigInt(storage.get(allowKey) || '0');
    const amt = BigInt(amount);
    assert(allowed >= amt, 'allowance exceeded');
    const newAllow = allowed - amt;
    storage.set(allowKey, newAllow.toString());
    emit('Approval', { owner, spender, value: newAllow.toString() });
    return _move(owner, recipient, amount);
  },

  // ---- Chain-only mint/burn for gas accounting ----
  // The chain calls these from the block apply path to handle
  // validator rewards, fee burns, etc. Restricted to msg.sender = ZERO.
  chainMint(to, amount) {
    assert(String(msg.sender).toLowerCase() === CHAIN_CALLER, 'restricted');
    const recipient = String(to).toLowerCase();
    const amt = BigInt(amount);
    if (amt === 0n) return true;
    const bal = BigInt(storage.get(BALANCE_PREFIX + recipient) || '0');
    storage.set(BALANCE_PREFIX + recipient, (bal + amt).toString());
    const total = BigInt(storage.get(TOTAL_SUPPLY_KEY) || '0');
    storage.set(TOTAL_SUPPLY_KEY, (total + amt).toString());
    emit('Transfer', { from: ZERO, to: recipient, value: amt.toString() });
    return true;
  },

  chainBurn(from, amount) {
    assert(String(msg.sender).toLowerCase() === CHAIN_CALLER, 'restricted');
    const sender = String(from).toLowerCase();
    const amt = BigInt(amount);
    if (amt === 0n) return true;
    const bal = BigInt(storage.get(BALANCE_PREFIX + sender) || '0');
    assert(bal >= amt, 'insufficient balance');
    storage.set(BALANCE_PREFIX + sender, (bal - amt).toString());
    const total = BigInt(storage.get(TOTAL_SUPPLY_KEY) || '0');
    storage.set(TOTAL_SUPPLY_KEY, (total - amt).toString());
    emit('Transfer', { from: sender, to: ZERO, value: amt.toString() });
    return true;
  },
})

function _move(from, to, amount) {
  assert(to !== '0x0000000000000000000000000000000000000000', 'transfer to zero address');
  const amt = BigInt(amount);
  assert(amt >= 0n, 'amount must be non-negative');
  const fromBal = BigInt(storage.get(BALANCE_PREFIX + from) || '0');
  assert(fromBal >= amt, 'insufficient balance');
  const toBal = BigInt(storage.get(BALANCE_PREFIX + to) || '0');
  storage.set(BALANCE_PREFIX + from, (fromBal - amt).toString());
  storage.set(BALANCE_PREFIX + to, (toBal + amt).toString());
  emit('Transfer', { from, to, value: amt.toString() });
  return true;
}
`;
async function installAseToken(opts) {
  const { store, vm, chainId, initialHolders } = opts;
  const sourceBytes = new TextEncoder().encode(ASE_TOKEN_SOURCE);
  const codeHash = blake38(sourceBytes);
  await store.putCode(codeHash, sourceBytes);
  const zeroHash = new Uint8Array(32);
  await store.setAccount(ASE_TOKEN_ADDRESS, {
    balance: 0n,
    nonce: 0n,
    codeHash,
    storageRoot: zeroHash
  });
  const inMem = /* @__PURE__ */ new Map();
  vm.deploy(
    {
      source: ASE_TOKEN_SOURCE,
      context: {
        sender: "0x" + "00".repeat(20),
        contractAddress: bytesToHex4(ASE_TOKEN_ADDRESS),
        value: 0n,
        blockNumber: 0n,
        blockTimestamp: 0n,
        chainId
      },
      storage: {
        get: (k) => inMem.get(k),
        set: (k, v) => {
          inMem.set(k, v);
        },
        delete: (k) => inMem.delete(k),
        has: (k) => inMem.has(k)
      },
      gasLimit: 200000000n
    },
    [
      "Asentum",
      "ASE",
      18,
      initialHolders.map((h) => ({ address: h.address.toLowerCase(), balance: h.balance }))
    ]
  );
  for (const [k, v] of inMem) {
    await store.setStorageValue(ASE_TOKEN_ADDRESS, k, v);
  }
  const storageRoot = await store.computeContractStorageRoot(ASE_TOKEN_ADDRESS);
  await store.setAccount(ASE_TOKEN_ADDRESS, {
    balance: 0n,
    nonce: 0n,
    codeHash,
    storageRoot
  });
}

// src/chain/genesis.ts
async function createGenesis(options) {
  const { store, initialAccounts, gasLimit, baseFeePerGas } = options;
  for (const { address, balance } of initialAccounts) {
    await store.setAccount(address, {
      ...emptyAccount(),
      balance
    });
  }
  if (options.stakingVm && options.initialValidators) {
    await deployStakingContractAtGenesis(
      store,
      options.stakingVm,
      options.chainId,
      options.initialValidators
    );
    await deploySlashingContractAtGenesis(
      store,
      options.stakingVm,
      options.chainId
    );
    await deployGovernanceContractAtGenesis(
      store,
      options.stakingVm,
      options.chainId
    );
    await installCronRegistry({
      store,
      vm: options.stakingVm,
      chainId: options.chainId
    });
    await installAseToken({
      store,
      vm: options.stakingVm,
      chainId: options.chainId,
      initialHolders: options.initialAseHolders ?? []
    });
  }
  const stateRoot = await store.stateRoot();
  const header = {
    parentHash: new Uint8Array(ZERO_HASH3),
    stateRoot,
    transactionsRoot: new Uint8Array(ZERO_HASH3),
    receiptsRoot: new Uint8Array(ZERO_HASH3),
    number: 0n,
    timestamp: 0n,
    gasLimit,
    gasUsed: 0n,
    baseFeePerGas,
    proposer: new Uint8Array(ZERO_ADDRESS_BYTES),
    // Genesis has no prior block, so the bitmap is all-zero. Block 1
    // also gets an all-zero bitmap (no rewards distributed for block 0).
    lastCommitBitmap: new Uint8Array(16)
  };
  return { header, transactions: [] };
}

// src/chain/tx-builder.ts
import { sign } from "@asentum/crypto";
function buildAndSignTransfer(options) {
  const body = {
    chainId: options.chainId,
    nonce: options.nonce,
    maxPriorityFeePerGas: options.maxPriorityFeePerGas,
    maxFeePerGas: options.maxFeePerGas,
    gasLimit: options.gasLimit,
    to: options.to,
    value: options.value,
    data: options.data ?? new Uint8Array(0)
  };
  const bodyHash = hashTransactionBody(body);
  const signature = sign(bodyHash, options.senderKeypair.secretKey);
  return {
    body,
    publicKey: options.senderKeypair.publicKey,
    signature
  };
}

// src/demo/hello-chain.ts
var ONE_ASE = 10n ** 18n;
async function main() {
  console.log("---");
  console.log("AsentumChain Phase 1 demo: single-node transfer");
  console.log("---");
  console.log("");
  const tempDir = await mkdtemp(join(tmpdir(), "asentum-chain-demo-"));
  const store = new StateStore({ path: tempDir });
  await store.open();
  try {
    console.log("[1/7] Generating Alice and Bob keypairs (Dilithium3)...");
    const alice = generateKeypair();
    const bob = generateKeypair();
    const aliceAddr = addressBytesFromPublicKey5(alice.publicKey);
    const bobAddr = addressBytesFromPublicKey5(bob.publicKey);
    console.log(`      Alice address:  0x${toHex(aliceAddr)}`);
    console.log(`      Bob address:    0x${toHex(bobAddr)}`);
    console.log("");
    console.log("[2/7] Creating genesis block with Alice pre-funded (1000 ASE)...");
    const genesis = await createGenesis({
      store,
      initialAccounts: [{ address: aliceAddr, balance: 1000n * ONE_ASE }],
      chainId: 1n,
      gasLimit: 30000000n,
      baseFeePerGas: 1n
    });
    console.log(`      Block #${genesis.header.number}`);
    console.log(`      State root:          0x${toHex(genesis.header.stateRoot)}`);
    console.log("");
    console.log("[3/7] Initial balances:");
    printBalance("Alice", await store.getAccount(aliceAddr));
    printBalance("Bob  ", await store.getAccount(bobAddr));
    console.log("");
    console.log("[4/7] Building and signing a transfer of 100 ASE from Alice to Bob...");
    const transferTx = buildAndSignTransfer({
      senderKeypair: alice,
      chainId: 1n,
      nonce: 0n,
      to: bobAddr,
      value: 100n * ONE_ASE,
      gasLimit: 21000n,
      maxFeePerGas: 1n,
      maxPriorityFeePerGas: 1n
    });
    console.log(`      Body size:           ${sizeOfBody(transferTx)} bytes`);
    console.log(`      Public key size:     ${transferTx.publicKey.length} bytes`);
    console.log(`      Signature size:      ${transferTx.signature.length} bytes`);
    console.log("");
    console.log("[5/7] Producing block 1 with the transfer...");
    const block1 = await buildBlock({
      store,
      parentHeader: genesis.header,
      mempool: [transferTx],
      proposer: aliceAddr,
      timestamp: BigInt(Math.floor(Date.now() / 1e3)),
      gasLimit: 30000000n,
      baseFeePerGas: 1n,
      lastCommitBitmap: new Uint8Array(16)
    });
    console.log(`      Block #${block1.header.number}`);
    console.log(`      Parent hash:         0x${toHex(block1.header.parentHash)}`);
    console.log(`      State root:          0x${toHex(block1.header.stateRoot)}`);
    console.log(`      Transactions root:   0x${toHex(block1.header.transactionsRoot)}`);
    console.log(`      Gas used:            ${block1.header.gasUsed}`);
    console.log(`      Applied transactions: ${block1.transactions.length}`);
    console.log("");
    console.log("[6/7] Final balances after transfer:");
    printBalance("Alice", await store.getAccount(aliceAddr));
    printBalance("Bob  ", await store.getAccount(bobAddr));
    console.log("");
    console.log("[7/7] Producing empty block 2 (no pending transactions)...");
    const block2 = await buildBlock({
      store,
      parentHeader: block1.header,
      mempool: [],
      proposer: aliceAddr,
      timestamp: BigInt(Math.floor(Date.now() / 1e3) + 6),
      gasLimit: 30000000n,
      baseFeePerGas: 1n,
      lastCommitBitmap: new Uint8Array(16)
    });
    console.log(`      Block #${block2.header.number}`);
    console.log(`      Parent hash:         0x${toHex(block2.header.parentHash)}`);
    console.log(`      State root:          0x${toHex(block2.header.stateRoot)}`);
    console.log(`      Applied transactions: ${block2.transactions.length}`);
    console.log("");
    console.log("---");
    console.log("Phase 1 done-when criterion met:");
    console.log("  - Single-node chain initialized from genesis");
    console.log("  - Transaction signed with Dilithium3 and verified on apply");
    console.log("  - Tokens moved from Alice to Bob");
    console.log("  - State root advanced across two blocks");
    console.log("  - Empty block produced cleanly when mempool is empty");
    console.log("---");
  } finally {
    await store.close();
    await rm(tempDir, { recursive: true, force: true });
  }
}
function toHex(bytes) {
  let hex = "";
  for (let i = 0; i < bytes.length; i++) {
    hex += (bytes[i] ?? 0).toString(16).padStart(2, "0");
  }
  return hex;
}
function printBalance(name, account) {
  const balance = account?.balance ?? 0n;
  const nonce = account?.nonce ?? 0n;
  const whole = balance / ONE_ASE;
  const fractional = balance % ONE_ASE;
  const frac = fractional.toString().padStart(18, "0").replace(/0+$/, "");
  const display = frac.length > 0 ? `${whole}.${frac}` : `${whole}`;
  console.log(`      ${name}: ${display} ASE (nonce ${nonce})`);
}
function sizeOfBody(tx) {
  return 32 + 20 + 32 * 3 + tx.body.data.length;
}
var isMain = process.argv[1] !== void 0 && fileURLToPath(import.meta.url) === process.argv[1];
if (isMain) {
  main().catch((err) => {
    console.error("hello-chain failed:", err);
    process.exit(1);
  });
}
//# sourceMappingURL=hello-chain.js.map