// 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 blake38 } from "@asentum/crypto";
import {
  BlockHeaderSchema,
  LastCommitSchema,
  SignedTransactionSchema,
  ZERO_HASH as ZERO_HASH2,
  emptyLastCommit
} from "@asentum/types";

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

// src/chain/vm-integration.ts
import {
  addressBytesFromPublicKey as addressBytesFromPublicKey3,
  blake3 as blake35,
  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 blake34 } from "@asentum/crypto";
import "@asentum/types";

// src/chain/cron-registry.ts
import { blake3 as blake32 } from "@asentum/crypto";
import "@asentum/types";
function bytesToHex(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.blockTimestamp), '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 = blake32(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: bytesToHex(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/vault-registry.ts
import { blake3 as blake33 } from "@asentum/crypto";
import "@asentum/types";

// src/chain/vault-contract-sources.ts
var VAULT_CONTRACT_SOURCE = "// AsentumVault \u2014 encrypted secret storage with per-contract authorization\n// and event-driven Worker dispatch.\n//\n// Concept (full design in notes/asentum-workers-vault-design.md):\n//   - User encrypts an API key (or any secret \u22644 KB) to a Worker's\n//     public keypair off-chain, stores the ciphertext here.\n//   - User declares which contracts may TRIGGER decryption of that secret.\n//   - When an authorized contract calls requestDecryption(), the Vault\n//     emits a WorkRequested event scoped to the chosen Worker.\n//   - The Worker (off-chain process) sees the event, decrypts the\n//     ciphertext, performs the contract-specified action (usually an\n//     outbound HTTP call), and writes back a fulfillment via fulfillWork().\n//\n// Trust model: the Worker holds the decryption key. You trust the Worker\n// operator with the secret. What you GAIN over \"trust Vercel\" is that\n// every call is gated by a publicly-auditable contract: rules about when\n// to fire, who can trigger, what payload is allowed are all enforced by\n// the chain, not the Worker.\n//\n// Storage layout:\n//   secret:<sid>             \u2192 JSON { owner, workerId, ciphertext,\n//                                     authorized: [addrs], createdAt,\n//                                     updatedAt, expiresAt, deleted? }\n//   index:<owner>:<sid>      \u2192 '1'\n//   job:<jobId>              \u2192 JSON { sid, requester, payload, requestedAt,\n//                                     fulfilledAt?, status?, hash?,\n//                                     statusCode?, body? }\n//\n// sid format: hex sha256 of (owner + name + workerId), 64 hex chars.\n// jobId format: hex sha256 of (sid + nonce + requester + blockNumber),\n// 64 hex chars. Both computed off-chain by the caller \u2014 chain only\n// validates length/uniqueness.\n\nconst SECRET_PREFIX = 'secret:';\nconst OWNER_SECRET_INDEX_PREFIX = 'index:';\nconst JOB_PREFIX = 'job:';\n\nconst SID_HEX_LEN = 64;                  // 32-byte sha256\nconst JOB_ID_HEX_LEN = 64;\nconst MAX_CIPHERTEXT_HEX_LEN = 16384;    // ~8 KB binary, generous for \u22644 KB plaintext + overhead\nconst MAX_PAYLOAD_LEN = 2000;            // JSON-stringified payload cap\nconst MAX_BODY_LEN = 1024;               // full-body writeback cap (when resultMode === 'body')\nconst MAX_AUTHORIZED = 32;               // arbitrary, prevents griefing\n\n// Result writeback modes the contract owner can request:\n//   'hash' (default) \u2014 Worker writes back sha256(responseBody) only.\n//                       Privacy-preserving, cheaper, suits most cases.\n//   'body'           \u2014 Worker writes back the actual response body\n//                       (\u2264MAX_BODY_LEN bytes). For oracle-style use\n//                       where the contract needs the data.\nconst VALID_RESULT_MODES = ['hash', 'body'];\n\n({\n  // \u2500\u2500\u2500 Secret lifecycle \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\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n  // Store a new secret. Caller becomes the owner.\n  //\n  //   sid             secret ID (hex sha256(owner||name||workerId))\n  //   workerId        the Worker this ciphertext was encrypted to\n  //   ciphertext      hex-encoded hybrid X25519+ML-KEM ciphertext bundle\n  //   authorized      array of contract addresses allowed to trigger decryption\n  //   expiresAt       Unix seconds; after this, requestDecryption refuses\n  put(sid, workerId, ciphertext, authorized, expiresAt) {\n    const id = String(sid == null ? '' : sid).toLowerCase();\n    assert(/^[0-9a-f]+$/.test(id) && id.length === SID_HEX_LEN,\n      'sid must be ' + SID_HEX_LEN + ' hex chars');\n    assert(!storage.has(SECRET_PREFIX + id),\n      'secret already exists; use rotate() to overwrite');\n\n    const wid = String(workerId == null ? '' : workerId);\n    assert(wid.length > 0, 'workerId required');\n\n    const ct = String(ciphertext == null ? '' : ciphertext).toLowerCase();\n    assert(/^[0-9a-f]+$/.test(ct), 'ciphertext must be hex');\n    assert(ct.length <= MAX_CIPHERTEXT_HEX_LEN,\n      'ciphertext too long (max ' + MAX_CIPHERTEXT_HEX_LEN + ' hex chars)');\n\n    const authList = normalizeAuthorized(authorized);\n    const owner = String(msg.sender).toLowerCase();\n    const now = String(chain.blockNumber);\n    const exp = String(expiresAt == null || expiresAt === '' ? '0' : expiresAt);\n\n    const record = {\n      sid: id,\n      owner: owner,\n      workerId: wid,\n      ciphertext: ct,\n      authorized: authList,\n      createdAt: now,\n      updatedAt: now,\n      expiresAt: exp,\n      deleted: false,\n    };\n    storage.set(SECRET_PREFIX + id, JSON.stringify(record));\n    storage.set(OWNER_SECRET_INDEX_PREFIX + owner + ':' + id, '1');\n\n    emit('SecretPut', {\n      sid: id,\n      owner: owner,\n      workerId: wid,\n      authorizedCount: String(authList.length),\n    });\n    return true;\n  },\n\n  // Overwrite an existing secret (re-encrypt + rotate). Owner only.\n  // Used when the underlying API key is rotated, or when the Worker's\n  // pubkey changed (WorkerKeyRotated) and the secret needs re-encryption\n  // to the new pubkey.\n  rotate(sid, newWorkerId, newCiphertext, newExpiresAt) {\n    const id = String(sid).toLowerCase();\n    const raw = storage.get(SECRET_PREFIX + id);\n    assert(raw, 'no such secret');\n    const record = JSON.parse(raw);\n    assert(!record.deleted, 'secret is soft-deleted; un-delete first or put() a new sid');\n    assert(record.owner === String(msg.sender).toLowerCase(),\n      'only owner can rotate');\n\n    const ct = String(newCiphertext == null ? '' : newCiphertext).toLowerCase();\n    assert(/^[0-9a-f]+$/.test(ct), 'ciphertext must be hex');\n    assert(ct.length <= MAX_CIPHERTEXT_HEX_LEN,\n      'ciphertext too long (max ' + MAX_CIPHERTEXT_HEX_LEN + ' hex chars)');\n\n    const wid = String(newWorkerId == null ? '' : newWorkerId);\n    assert(wid.length > 0, 'workerId required');\n\n    record.workerId = wid;\n    record.ciphertext = ct;\n    record.expiresAt = String(newExpiresAt == null || newExpiresAt === '' ? record.expiresAt : newExpiresAt);\n    record.updatedAt = String(chain.blockNumber);\n    storage.set(SECRET_PREFIX + id, JSON.stringify(record));\n\n    emit('SecretRotated', { sid: id, owner: record.owner, workerId: wid });\n    return true;\n  },\n\n  // \u2500\u2500\u2500 Authorization \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\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n  authorize(sid, contractAddr) {\n    const id = String(sid).toLowerCase();\n    const raw = storage.get(SECRET_PREFIX + id);\n    assert(raw, 'no such secret');\n    const record = JSON.parse(raw);\n    assert(record.owner === String(msg.sender).toLowerCase(),\n      'only owner can authorize');\n\n    const addr = String(contractAddr).toLowerCase();\n    assert(/^0x[0-9a-f]{40}$/.test(addr), 'contractAddr must be 0x-prefixed 20-byte hex');\n\n    if (record.authorized.indexOf(addr) === -1) {\n      assert(record.authorized.length < MAX_AUTHORIZED,\n        'authorized list at cap (' + MAX_AUTHORIZED + ')');\n      record.authorized.push(addr);\n      record.updatedAt = String(chain.blockNumber);\n      storage.set(SECRET_PREFIX + id, JSON.stringify(record));\n      emit('SecretAuthorized', { sid: id, contract: addr });\n    }\n    return true;\n  },\n\n  revoke(sid, contractAddr) {\n    const id = String(sid).toLowerCase();\n    const raw = storage.get(SECRET_PREFIX + id);\n    assert(raw, 'no such secret');\n    const record = JSON.parse(raw);\n    assert(record.owner === String(msg.sender).toLowerCase(),\n      'only owner can revoke');\n\n    const addr = String(contractAddr).toLowerCase();\n    const idx = record.authorized.indexOf(addr);\n    if (idx !== -1) {\n      record.authorized.splice(idx, 1);\n      record.updatedAt = String(chain.blockNumber);\n      storage.set(SECRET_PREFIX + id, JSON.stringify(record));\n      emit('SecretRevoked', { sid: id, contract: addr });\n    }\n    return true;\n  },\n\n  canUse(sid, contractAddr) {\n    const raw = storage.get(SECRET_PREFIX + String(sid).toLowerCase());\n    if (!raw) return false;\n    const record = JSON.parse(raw);\n    if (record.deleted) return false;\n    return record.authorized.indexOf(String(contractAddr).toLowerCase()) !== -1;\n  },\n\n  // \u2500\u2500\u2500 Decryption request flow \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\u2500\u2500\n\n  // Called by an AUTHORIZED contract (msg.sender check) to request the\n  // Worker decrypt a secret and perform the specified action.\n  //\n  // The contract crafts `payload` \u2014 typically a JSON object describing\n  // the outbound HTTP call: { url, method, body, headers, resultMode }.\n  // The Vault doesn't interpret the payload; the Worker does. This means\n  // the AUTHORIZING CONTRACT is the policy: whatever it chooses to put\n  // in `payload` is what the Worker will be asked to do. Workers also\n  // enforce their own per-host allowlist as defense in depth (see design\n  // doc \xA7 Worker protocol).\n  //\n  // jobId is provided by the caller (must be unique) so the contract\n  // can correlate the eventual fulfillment back to its own state.\n  requestDecryption(sid, jobId, payload) {\n    const id = String(sid).toLowerCase();\n    const raw = storage.get(SECRET_PREFIX + id);\n    assert(raw, 'no such secret');\n    const record = JSON.parse(raw);\n    assert(!record.deleted, 'secret is soft-deleted');\n\n    // expiry gate\n    if (record.expiresAt !== '0') {\n      const nowTs = BigInt(chain.blockTimestamp || 0);\n      assert(nowTs < BigInt(record.expiresAt),\n        'secret expired at ' + record.expiresAt);\n    }\n\n    // Authorization: contract caller must be in the authorized list.\n    const requester = String(msg.sender).toLowerCase();\n    assert(record.authorized.indexOf(requester) !== -1,\n      'caller not authorized for this secret');\n\n    // jobId uniqueness \u2014 prevents replay and lets contracts use jobId\n    // as a deterministic correlation key.\n    const jid = String(jobId == null ? '' : jobId).toLowerCase();\n    assert(/^[0-9a-f]+$/.test(jid) && jid.length === JOB_ID_HEX_LEN,\n      'jobId must be ' + JOB_ID_HEX_LEN + ' hex chars');\n    assert(!storage.has(JOB_PREFIX + jid),\n      'jobId already exists');\n\n    // Payload is opaque to the Vault; we just length-check it.\n    const pl = String(payload == null ? '' : payload);\n    assert(pl.length <= MAX_PAYLOAD_LEN,\n      'payload too long (max ' + MAX_PAYLOAD_LEN + ' chars)');\n\n    const job = {\n      jobId: jid,\n      sid: id,\n      requester: requester,\n      payload: pl,\n      workerId: record.workerId,\n      requestedAt: String(chain.blockNumber),\n      fulfilledAt: '',\n      status: 'pending',  // 'pending' | 'ok' | 'error'\n      hash: '',\n      statusCode: '',\n      body: '',\n      error: '',\n    };\n    storage.set(JOB_PREFIX + jid, JSON.stringify(job));\n\n    // Worker subscribes by workerId.\n    emit('WorkRequested', {\n      workerId: record.workerId,\n      jobId: jid,\n      sid: id,\n      requester: requester,\n      payload: pl,\n    });\n    return jid;\n  },\n\n  // \u2500\u2500\u2500 Fulfillment writeback \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\u2500\u2500\u2500\u2500\n\n  // Called by the Worker after performing the HTTP call. Records the\n  // result on-chain. Idempotent: a second call for the same jobId fails.\n  //\n  // The Vault verifies that msg.sender is the registered OWNER of the\n  // workerId associated with this job \u2014 but for cross-contract verify\n  // we'd need to read WorkerRegistry. To keep this MVP self-contained\n  // and not depend on a cross-call (which the VM may or may not allow\n  // in the current rev), we instead let any address fulfill BUT we\n  // record `fulfillerAddress` in the event so contracts and indexers\n  // can verify the fulfiller's identity off-chain against the registry.\n  // V2: tighten this to an in-contract registry lookup if the VM\n  // supports cross-contract reads, or move registry data inline.\n  //\n  //   jobId         the job to fulfill\n  //   status        'ok' or 'error'\n  //   statusCode    HTTP status code from the upstream call (string)\n  //   hash          sha256 hex of response body (always written)\n  //   body          optional response body (only when contract requested\n  //                 resultMode='body' in payload; \u2264MAX_BODY_LEN bytes)\n  //   error         optional error message when status='error'\n  fulfillWork(jobId, status, statusCode, hash, body, error) {\n    const jid = String(jobId).toLowerCase();\n    const raw = storage.get(JOB_PREFIX + jid);\n    assert(raw, 'no such job');\n    const job = JSON.parse(raw);\n    assert(job.status === 'pending', 'job already fulfilled');\n\n    const st = String(status);\n    assert(st === 'ok' || st === 'error', 'status must be ok or error');\n\n    const ph = String(hash == null ? '' : hash).toLowerCase();\n    if (ph.length > 0) {\n      assert(/^[0-9a-f]+$/.test(ph) && ph.length === 64,\n        'hash must be 64 hex chars when present');\n    }\n\n    const bd = String(body == null ? '' : body);\n    assert(bd.length <= MAX_BODY_LEN,\n      'body too long (max ' + MAX_BODY_LEN + ' chars)');\n\n    job.fulfilledAt = String(chain.blockNumber);\n    job.status = st;\n    job.statusCode = String(statusCode == null ? '' : statusCode);\n    job.hash = ph;\n    job.body = bd;\n    job.error = String(error == null ? '' : error);\n    job.fulfiller = String(msg.sender).toLowerCase();\n    storage.set(JOB_PREFIX + jid, JSON.stringify(job));\n\n    emit('WorkFulfilled', {\n      workerId: job.workerId,\n      jobId: jid,\n      status: st,\n      statusCode: job.statusCode,\n      hash: ph,\n      fulfiller: job.fulfiller,\n    });\n\n    // Best-effort push-back: if the requester is a contract that implements\n    // onFulfilled(jobId, status, statusCode, hash, body, error), deliver the\n    // result straight into it (Chainlink-style callback). If it isn't a\n    // contract, or has no such method, or the callback reverts, we swallow the\n    // error \u2014 the result is already durably stored and readable via getJob().\n    // The callee sees msg.sender == this Vault, so it can trust the source.\n    if (/^0x[0-9a-f]{40}$/.test(job.requester)) {\n      try {\n        E(job.requester).onFulfilled(jid, st, job.statusCode, ph, bd, job.error);\n      } catch (_e) {\n        emit('CallbackFailed', { jobId: jid, requester: job.requester });\n      }\n    }\n    return true;\n  },\n\n  // \u2500\u2500\u2500 Deletion (soft only) \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\u2500\u2500\u2500\u2500\u2500\n\n  // Mark a secret as deleted. Owner only. The ciphertext remains in\n  // storage for audit purposes but requestDecryption() will refuse it.\n  // Hard-delete is intentionally not supported \u2014 preserves audit trail.\n  // To free the storage slot, the owner can rotate() to an empty\n  // ciphertext after delete().\n  delete(sid) {\n    const id = String(sid).toLowerCase();\n    const raw = storage.get(SECRET_PREFIX + id);\n    assert(raw, 'no such secret');\n    const record = JSON.parse(raw);\n    assert(record.owner === String(msg.sender).toLowerCase(),\n      'only owner can delete');\n    assert(!record.deleted, 'already deleted');\n\n    record.deleted = true;\n    record.updatedAt = String(chain.blockNumber);\n    storage.set(SECRET_PREFIX + id, JSON.stringify(record));\n\n    emit('SecretDeleted', { sid: id, owner: record.owner });\n    return true;\n  },\n\n  // \u2500\u2500\u2500 Views \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\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n  getSecret(sid) {\n    const raw = storage.get(SECRET_PREFIX + String(sid).toLowerCase());\n    if (!raw) return null;\n    return JSON.parse(raw);\n  },\n\n  getJob(jobId) {\n    const raw = storage.get(JOB_PREFIX + String(jobId).toLowerCase());\n    if (!raw) return null;\n    return JSON.parse(raw);\n  },\n\n  hasSecret(sid) {\n    return storage.has(SECRET_PREFIX + String(sid).toLowerCase());\n  },\n});\n\nfunction normalizeAuthorized(input) {\n  if (input == null) return [];\n  // Accept either an array or a JSON string.\n  let arr = input;\n  if (typeof input === 'string') {\n    try { arr = JSON.parse(input); } catch { arr = []; }\n  }\n  if (!Array.isArray(arr)) return [];\n  assert(arr.length <= MAX_AUTHORIZED,\n    'authorized list too long (max ' + MAX_AUTHORIZED + ')');\n  const out = [];\n  for (let i = 0; i < arr.length; i++) {\n    const addr = String(arr[i]).toLowerCase();\n    assert(/^0x[0-9a-f]{40}$/.test(addr),\n      'authorized[' + i + '] must be 0x-prefixed 20-byte hex');\n    if (out.indexOf(addr) === -1) out.push(addr);\n  }\n  return out;\n}\n";
var WORKER_REGISTRY_SOURCE = "// AsentumWorkerRegistry \u2014 discovery + pubkey directory for Asentum Workers.\n//\n// A Worker is an off-chain process that decrypts Vault secrets and makes\n// HTTP calls on behalf of authorized contracts. Workers register here so\n// that:\n//   1. Users encrypting secrets can find a Worker's public encryption keys.\n//   2. The Vault can verify a fulfillment tx came from the registered owner.\n//   3. Anyone can audit which Workers were active at any block height.\n//\n// Per the design doc (notes/asentum-workers-vault-design.md), each Worker\n// holds a HYBRID encryption keypair: X25519 for the classical leg + ML-KEM-768\n// for the post-quantum leg. A secret is encrypted to BOTH legs and survives\n// either one being broken. This contract just stores the public halves +\n// the owner's chain address.\n//\n// Workers also commit to a policy hash (sha256 of allowed-hosts.json +\n// worker version). Lets auditors see exactly what HTTP destinations the\n// Worker was willing to call at any point in history.\n//\n// Storage layout:\n//   worker:<workerId>          \u2192 JSON { owner, x25519Pub, mlkemPub,\n//                                       broadcastUrl, policyHash, version,\n//                                       registeredAt, updatedAt, metadata }\n//   index:<owner>:<workerId>   \u2192 '1'   (lets us list a single owner's workers)\n//\n// All workerIds are case-sensitive opaque strings (e.g. \"asentum-default\",\n// \"acme-corp-prod\"). Recommend [a-z0-9-]{3,64}.\n\nconst WORKER_PREFIX = 'worker:';\nconst OWNER_INDEX_PREFIX = 'index:';\n\nconst MAX_WORKER_ID_LEN = 64;\nconst MAX_BROADCAST_URL_LEN = 200;\nconst MAX_METADATA_LEN = 1000;\nconst X25519_PUB_HEX_LEN = 64;        // 32 bytes\nconst MLKEM_PUB_HEX_LEN = 2368;       // 1184 bytes for ML-KEM-768\nconst POLICY_HASH_HEX_LEN = 64;       // 32 bytes (sha256)\n\n({\n  // Register a new Worker. Caller becomes the owner. Owner is the only\n  // address allowed to update / deregister this workerId.\n  //\n  //   workerId      opaque ID, becomes the discovery key\n  //   x25519Pub     hex public key (32 bytes / 64 hex chars)\n  //   mlkemPub      hex public key (1184 bytes / 2368 hex chars)\n  //   broadcastUrl  optional HTTP URL where the Worker accepts pushes\n  //   policyHash    hex sha256 of the Worker's allowed-hosts policy + version\n  //   version       freeform string (e.g. \"@asentum/worker@0.1.0\")\n  //   metadata      optional JSON-stringifiable extra fields\n  register(workerId, x25519Pub, mlkemPub, broadcastUrl, policyHash, version, metadata) {\n    const id = String(workerId == null ? '' : workerId);\n    assert(id.length > 0, 'workerId required');\n    assert(id.length <= MAX_WORKER_ID_LEN,\n      'workerId too long (max ' + MAX_WORKER_ID_LEN + ' chars)');\n    assert(!storage.has(WORKER_PREFIX + id),\n      'workerId already registered: ' + id);\n\n    const x25519 = String(x25519Pub == null ? '' : x25519Pub).toLowerCase();\n    const mlkem = String(mlkemPub == null ? '' : mlkemPub).toLowerCase();\n    const url = String(broadcastUrl == null ? '' : broadcastUrl);\n    const ph = String(policyHash == null ? '' : policyHash).toLowerCase();\n    const ver = String(version == null ? '' : version);\n    const meta = String(metadata == null ? '' : metadata);\n\n    assert(/^[0-9a-f]+$/.test(x25519) && x25519.length === X25519_PUB_HEX_LEN,\n      'x25519Pub must be ' + X25519_PUB_HEX_LEN + ' hex chars');\n    assert(/^[0-9a-f]+$/.test(mlkem) && mlkem.length === MLKEM_PUB_HEX_LEN,\n      'mlkemPub must be ' + MLKEM_PUB_HEX_LEN + ' hex chars');\n    assert(/^[0-9a-f]+$/.test(ph) && ph.length === POLICY_HASH_HEX_LEN,\n      'policyHash must be ' + POLICY_HASH_HEX_LEN + ' hex chars');\n    assert(url.length <= MAX_BROADCAST_URL_LEN,\n      'broadcastUrl too long (max ' + MAX_BROADCAST_URL_LEN + ' chars)');\n    assert(meta.length <= MAX_METADATA_LEN,\n      'metadata too long (max ' + MAX_METADATA_LEN + ' chars)');\n\n    const owner = String(msg.sender).toLowerCase();\n    const now = String(chain.blockNumber);\n\n    const record = {\n      workerId: id,\n      owner: owner,\n      x25519Pub: x25519,\n      mlkemPub: mlkem,\n      broadcastUrl: url,\n      policyHash: ph,\n      version: ver,\n      metadata: meta,\n      registeredAt: now,\n      updatedAt: now,\n    };\n    storage.set(WORKER_PREFIX + id, JSON.stringify(record));\n    storage.set(OWNER_INDEX_PREFIX + owner + ':' + id, '1');\n\n    emit('WorkerRegistered', {\n      workerId: id,\n      owner: owner,\n      policyHash: ph,\n      version: ver,\n    });\n    return true;\n  },\n\n  // Rotate a Worker's encryption keypair AND its policy hash. Owner only.\n  // Existing ciphertexts encrypted to the OLD pubkeys become unrecoverable\n  // unless the old key is also retained off-chain \u2014 but that's a per-Worker\n  // operational concern, the chain just records the rotation. Dependent\n  // contracts can react to WorkerKeyRotated to re-encrypt their secrets.\n  rotate(workerId, newX25519Pub, newMlkemPub, newPolicyHash, newVersion) {\n    const id = String(workerId);\n    const raw = storage.get(WORKER_PREFIX + id);\n    assert(raw, 'no such workerId: ' + id);\n    const record = JSON.parse(raw);\n    assert(record.owner === String(msg.sender).toLowerCase(),\n      'only owner can rotate');\n\n    const x25519 = String(newX25519Pub == null ? '' : newX25519Pub).toLowerCase();\n    const mlkem = String(newMlkemPub == null ? '' : newMlkemPub).toLowerCase();\n    const ph = String(newPolicyHash == null ? '' : newPolicyHash).toLowerCase();\n    const ver = String(newVersion == null ? '' : newVersion);\n\n    assert(/^[0-9a-f]+$/.test(x25519) && x25519.length === X25519_PUB_HEX_LEN,\n      'x25519Pub must be ' + X25519_PUB_HEX_LEN + ' hex chars');\n    assert(/^[0-9a-f]+$/.test(mlkem) && mlkem.length === MLKEM_PUB_HEX_LEN,\n      'mlkemPub must be ' + MLKEM_PUB_HEX_LEN + ' hex chars');\n    assert(/^[0-9a-f]+$/.test(ph) && ph.length === POLICY_HASH_HEX_LEN,\n      'policyHash must be ' + POLICY_HASH_HEX_LEN + ' hex chars');\n\n    record.x25519Pub = x25519;\n    record.mlkemPub = mlkem;\n    record.policyHash = ph;\n    record.version = ver;\n    record.updatedAt = String(chain.blockNumber);\n    storage.set(WORKER_PREFIX + id, JSON.stringify(record));\n\n    emit('WorkerKeyRotated', {\n      workerId: id,\n      owner: record.owner,\n      policyHash: ph,\n      version: ver,\n    });\n    return true;\n  },\n\n  // Update the off-chain endpoint where the Worker is reachable. Owner only.\n  // Lighter weight than rotate(): doesn't invalidate any secrets, just\n  // updates how to talk to the Worker. Use this for moving a Worker\n  // between hosts without rotating keys.\n  setBroadcastUrl(workerId, broadcastUrl) {\n    const id = String(workerId);\n    const raw = storage.get(WORKER_PREFIX + id);\n    assert(raw, 'no such workerId: ' + id);\n    const record = JSON.parse(raw);\n    assert(record.owner === String(msg.sender).toLowerCase(),\n      'only owner can update broadcastUrl');\n\n    const url = String(broadcastUrl == null ? '' : broadcastUrl);\n    assert(url.length <= MAX_BROADCAST_URL_LEN,\n      'broadcastUrl too long (max ' + MAX_BROADCAST_URL_LEN + ' chars)');\n\n    record.broadcastUrl = url;\n    record.updatedAt = String(chain.blockNumber);\n    storage.set(WORKER_PREFIX + id, JSON.stringify(record));\n\n    emit('WorkerEndpointUpdated', { workerId: id, broadcastUrl: url });\n    return true;\n  },\n\n  // Remove a Worker from the registry. Owner only.\n  // Secrets encrypted to this Worker remain on-chain but are now\n  // unaddressable \u2014 practical effect is they become orphan ciphertext.\n  // The owner should rotate dependent Vault secrets to a new Worker\n  // BEFORE deregistering, or after if recovery isn't needed.\n  deregister(workerId) {\n    const id = String(workerId);\n    const raw = storage.get(WORKER_PREFIX + id);\n    assert(raw, 'no such workerId: ' + id);\n    const record = JSON.parse(raw);\n    assert(record.owner === String(msg.sender).toLowerCase(),\n      'only owner can deregister');\n\n    storage.delete(WORKER_PREFIX + id);\n    storage.delete(OWNER_INDEX_PREFIX + record.owner + ':' + id);\n    emit('WorkerDeregistered', { workerId: id, owner: record.owner });\n    return true;\n  },\n\n  // View: full record for `workerId`, or null if not registered.\n  get(workerId) {\n    const id = String(workerId);\n    const raw = storage.get(WORKER_PREFIX + id);\n    if (!raw) return null;\n    return JSON.parse(raw);\n  },\n\n  // View: bool \u2014 is this workerId registered?\n  exists(workerId) {\n    return storage.has(WORKER_PREFIX + String(workerId));\n  },\n\n  // View: is `address` the registered owner of `workerId`?\n  // Vault uses this to verify a fulfillment tx is authentic.\n  isOwner(workerId, address) {\n    const raw = storage.get(WORKER_PREFIX + String(workerId));\n    if (!raw) return false;\n    const record = JSON.parse(raw);\n    return record.owner === String(address).toLowerCase();\n  },\n});\n";

// src/chain/vault-registry.ts
function bytesToHex2(bytes) {
  let h = "0x";
  for (let i = 0; i < bytes.length; i++) h += (bytes[i] ?? 0).toString(16).padStart(2, "0");
  return h;
}
var VAULT_ADDRESS = (() => {
  const addr = new Uint8Array(20);
  addr[19] = 8;
  return addr;
})();
var WORKER_REGISTRY_ADDRESS = (() => {
  const addr = new Uint8Array(20);
  addr[19] = 9;
  return addr;
})();
async function installOne(opts, address, source) {
  const { store, vm, chainId } = opts;
  const sourceBytes = new TextEncoder().encode(source);
  const codeHash = blake33(sourceBytes);
  await store.putCode(codeHash, sourceBytes);
  const zeroHash = new Uint8Array(32);
  await store.setAccount(address, {
    balance: 0n,
    nonce: 0n,
    codeHash,
    storageRoot: zeroHash
  });
  const inMem = /* @__PURE__ */ new Map();
  vm.deploy(
    {
      source,
      context: {
        sender: "0x" + "00".repeat(20),
        contractAddress: bytesToHex2(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(address, k, v);
  }
  const storageRoot = await store.computeContractStorageRoot(address);
  await store.setAccount(address, {
    balance: 0n,
    nonce: 0n,
    codeHash,
    storageRoot
  });
}
async function installVaultContract(opts) {
  await installOne(opts, VAULT_ADDRESS, VAULT_CONTRACT_SOURCE);
}
async function installWorkerRegistry(opts) {
  await installOne(opts, WORKER_REGISTRY_ADDRESS, WORKER_REGISTRY_SOURCE);
}

// src/chain/system-contracts.ts
function reservedAddress(marker) {
  const addr = new Uint8Array(20);
  addr[19] = marker;
  return addr;
}
var STAKING_CONTRACT_ADDRESS = reservedAddress(1);
var SLASHING_CONTRACT_ADDRESS = reservedAddress(2);
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 = 500n * (10n ** 18n);            // 500 ASE
const MAX_SELF_BOND_WEI = 50000000n * (10n ** 18n);       // 50,000,000 ASE
// 30-day unbonding window at 2-second blocks = 1,296,000 blocks.
const UNBONDING_BLOCKS = 1296000n;

// \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;

// Chain-level peer discovery: every validator publishes a broadcastUrl
// on-chain at bond time so other validators can find them. Without this,
// the cluster relies on a hardcoded ASENTUM_PEERS env var per box and
// any new bond becomes a "ghost validator" \u2014 bonded but never receiving
// consensus messages because no peer knows their IP. URLs are capped to
// prevent state bloat and validated to prevent garbage that would crash
// downstream broadcast loops.
const MAX_BROADCAST_URL_LEN = 256;
function isValidBroadcastUrl(s) {
  if (typeof s !== 'string') return false;
  if (s.length === 0) return false;
  if (s.length > MAX_BROADCAST_URL_LEN) return false;
  // Lightweight check: must start with http:// or https:// and contain a host.
  // Rejecting malformed URLs at bond time keeps the broadcast loop fast \u2014 it
  // never has to try-catch a parse on every iteration.
  if (s.indexOf('http://') !== 0 && s.indexOf('https://') !== 0) return false;
  const rest = s.slice(s.indexOf('://') + 3);
  if (rest.length === 0) return false;
  return true;
}

// Chain-level ANT discovery: a validator publishes its ANT transport address
// (host:port) on-chain so the baked can dial + admit it as a consensus peer
// without anyone hand-editing an allowlist. Distinct from broadcastUrl (the
// http RPC endpoint); ANT is the raw consensus transport on its own port.
// Optional: if empty, peers fall back to deriving it from broadcastUrl's host
// plus the standard ANT port. Format is host:port, validated to keep dial
// loops fast and prevent state bloat.
const MAX_ANT_ADDR_LEN = 128;
function isValidAntAddr(s) {
  if (typeof s !== 'string') return false;
  if (s.length === 0 || s.length > MAX_ANT_ADDR_LEN) return false;
  const i = s.lastIndexOf(':');
  if (i <= 0 || i === s.length - 1) return false;
  const host = s.slice(0, i);
  const port = s.slice(i + 1);
  if (host.length === 0) return false;
  if (!/^[0-9]+$/.test(port)) return false;
  const p = Number(port);
  return p >= 1 && p <= 65535;
}

({
  init(args) {
    const initial = (args && args.initialValidators) || [];
    const addresses = [];
    for (const v of initial) {
      const addr = String(v.address).toLowerCase();
      addresses.push(addr);
      // Genesis validators may include a broadcastUrl. If absent (legacy
      // genesis specs), default to empty \u2014 they can submit
      // updateBroadcastUrl later. Empty URLs are tolerated for genesis-baked
      // validators only; runtime bond requires a valid URL.
      const broadcastUrl = v.broadcastUrl ? String(v.broadcastUrl) : '';
      const antAddr = v.antAddr ? String(v.antAddr) : '';
      storage.set(VALIDATOR_PREFIX + addr, JSON.stringify({
        address: addr,
        pubKey: String(v.pubKey),
        stake: String(v.stake),
        bondedAt: String(chain.blockNumber),
        broadcastUrl,
        antAddr,
      }));
    }
    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.
  //
  // broadcastUrl is the http(s) endpoint other validators will use to
  // send this validator proposals and votes. Required and validated.
  // Without it, a bonded validator would be a "ghost" \u2014 bonded but never
  // reachable. Existing bonds calling bond() to top up may pass empty to
  // keep their current URL.
  bond(pubKey, broadcastUrl, antAddr) {
    const sender = String(msg.sender).toLowerCase();
    const amt = BigInt(msg.value);
    assert(amt > 0n, 'bond requires non-zero msg.value');

    const antGiven = antAddr !== undefined && antAddr !== null && antAddr !== '';
    if (antGiven) {
      assert(isValidAntAddr(antAddr), 'invalid antAddr: must be host:port');
    }

    const existingRaw = storage.get(VALIDATOR_PREFIX + sender);
    let validator;
    let newTotal;
    if (existingRaw) {
      validator = JSON.parse(existingRaw);
      newTotal = BigInt(validator.stake) + amt;
      // Top-up bond: broadcastUrl optional. If passed and valid, update.
      // If passed and INVALID, reject (don't silently keep the stale one).
      if (broadcastUrl !== undefined && broadcastUrl !== null && broadcastUrl !== '') {
        assert(isValidBroadcastUrl(broadcastUrl), 'invalid broadcastUrl: must be http(s)://host[:port], \u2264256 chars');
        validator.broadcastUrl = String(broadcastUrl);
      }
      if (antGiven) validator.antAddr = String(antAddr);
    } else {
      // First-time bond: broadcastUrl REQUIRED. antAddr optional (peers derive
      // it from broadcastUrl's host + standard ANT port if unset).
      assert(isValidBroadcastUrl(broadcastUrl), 'first bond requires a broadcastUrl (http(s)://host:port)');
      validator = {
        address: sender,
        pubKey: String(pubKey),
        stake: '0',
        bondedAt: String(chain.blockNumber),
        broadcastUrl: String(broadcastUrl),
        antAddr: antGiven ? String(antAddr) : '',
      };
      newTotal = amt;
    }

    assert(newTotal >= MIN_SELF_BOND_WEI, 'bond below MIN_SELF_BOND (500 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, broadcastUrl: validator.broadcastUrl, antAddr: validator.antAddr || '' });
    return true;
  },

  // Update the broadcastUrl for an already-bonded validator. Required when
  // a validator changes IP (move boxes, NAT reassignment, etc). Only the
  // bonded validator's own address can update their URL.
  //
  // No stake required \u2014 this is metadata only \u2014 but the validator must
  // already be in the bonded set.
  updateBroadcastUrl(broadcastUrl) {
    const sender = String(msg.sender).toLowerCase();
    const existingRaw = storage.get(VALIDATOR_PREFIX + sender);
    assert(existingRaw, 'not a bonded validator');
    assert(isValidBroadcastUrl(broadcastUrl), 'invalid broadcastUrl: must be http(s)://host[:port], \u2264256 chars');
    const validator = JSON.parse(existingRaw);
    const oldUrl = validator.broadcastUrl || '';
    validator.broadcastUrl = String(broadcastUrl);
    storage.set(VALIDATOR_PREFIX + sender, JSON.stringify(validator));
    emit('BroadcastUrlUpdated', { validator: sender, oldUrl, newUrl: validator.broadcastUrl });
    return true;
  },

  // Update the ANT transport address (host:port) for an already-bonded
  // validator, so the baked can dial it as a consensus peer. Metadata only,
  // no stake required, but the validator must already be bonded. Empty clears
  // it (peers then derive from broadcastUrl's host + standard ANT port).
  updateAntAddr(antAddr) {
    const sender = String(msg.sender).toLowerCase();
    const existingRaw = storage.get(VALIDATOR_PREFIX + sender);
    assert(existingRaw, 'not a bonded validator');
    if (antAddr !== undefined && antAddr !== null && antAddr !== '') {
      assert(isValidAntAddr(antAddr), 'invalid antAddr: must be host:port');
    }
    const validator = JSON.parse(existingRaw);
    const oldAddr = validator.antAddr || '';
    validator.antAddr = antAddr ? String(antAddr) : '';
    storage.set(VALIDATOR_PREFIX + sender, JSON.stringify(validator));
    emit('AntAddrUpdated', { validator: sender, oldAddr, newAddr: validator.antAddr });
    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');

    // #446 re-admission: record the height at which this validator last
    // re-declared readiness. Applies to CORE validators too (whose entry is
    // otherwise ignored for reserve selection) \u2014 so a long-dormant validator
    // that comes back online and re-submits gets a fresh grace window in the
    // dormancy filter (computeFreshEpochEntrants), breaking the "dormant ->
    // can't sign -> stays dormant" trap. Deterministic: chain.blockNumber is
    // identical on every node applying this block.
    validator.enteredAt = String(currentBlock);
    storage.set(VALIDATOR_PREFIX + sender, JSON.stringify(validator));

    // 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) {
  let contractSource = STAKING_CONTRACT_SOURCE;
  const parsePositiveInt = (v) => {
    if (v === void 0 || !/^[0-9]+$/.test(v)) return null;
    const n = BigInt(v);
    return n > 0n ? n : null;
  };
  const eb = parsePositiveInt(process.env.ASENTUM_EPOCH_BLOCKS);
  const ew = parsePositiveInt(process.env.ASENTUM_EPOCH_ENTRY_WINDOW);
  if (eb !== null) contractSource = contractSource.replace("const EPOCH_BLOCKS = 1440n;", `const EPOCH_BLOCKS = ${eb}n;`);
  if (ew !== null) contractSource = contractSource.replace("const EPOCH_ENTRY_WINDOW = 100n;", `const EPOCH_ENTRY_WINDOW = ${ew}n;`);
  const sourceBytes = new TextEncoder().encode(contractSource);
  const codeHash = blake34(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: contractSource,
      context: {
        sender: "0x" + "00".repeat(20),
        contractAddress: bytesToHex3(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),
          broadcastUrl: v.broadcastUrl,
          antAddr: v.antAddr
        }))
      }
    ]
  );
  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 = blake34(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: bytesToHex3(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 = blake34(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: bytesToHex3(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 bytesToHex3(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 blake35(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 = blake35(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(bytesToHex4(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(bytesToHex4(contractAddress).toLowerCase(), source);
  contractStorageCache.set(bytesToHex4(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(bytesToHex4(contractAddress).toLowerCase(), freshStorageAdapter);
    allStorageAdapters.push({ address: contractAddress, adapter: freshStorageAdapter });
    callStack.push(bytesToHex4(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.push(...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: bytesToHex4(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 senderNow = await store.getAccount(senderAddress) ?? sender;
  const updatedSender = {
    balance: senderNow.balance - totalDebit,
    nonce: sender.nonce + 1n,
    codeHash: senderNow.codeHash,
    storageRoot: senderNow.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);
  const deleted = /* @__PURE__ */ new Set();
  return {
    get(key) {
      return current.get(key);
    },
    set(key, value) {
      current.set(key, value);
      deleted.delete(key);
    },
    delete(key) {
      deleted.add(key);
      return current.delete(key);
    },
    has(key) {
      return current.has(key);
    },
    // Sorted prefix scan over the working set. Any contract using
    // storage.keys(prefix) (e.g. cron registry, worker registry index)
    // depends on this method existing on both the tx-execution path
    // (this adapter) and the read-only path (readOnlyStorage in
    // runChainCall / runReadOnlyCall). Missing keys() here would make
    // scheduled cron jobs, registry listings, etc. fail non-deterministically
    // depending on which caller path invoked the contract.
    keys(prefix) {
      const p = prefix ?? "";
      const out = [];
      for (const k of current.keys()) {
        if (deleted.has(k)) continue;
        if (p === "" || k.startsWith(p)) out.push(k);
      }
      out.sort();
      return out;
    },
    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: bytesToHex4(sender),
    contractAddress: bytesToHex4(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 bytesToHex4(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 blake36(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 blake37 } 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 blake37(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 = blake38(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 = blake38(BlockHeaderSchema.serialize(parentHeader));
  const lastCommit = options.lastCommit ?? emptyLastCommit();
  const lastCommitHash = blake38(LastCommitSchema.serialize(lastCommit));
  const header = {
    parentHash,
    stateRoot,
    transactionsRoot,
    receiptsRoot,
    number: blockNumber,
    timestamp,
    gasLimit,
    gasUsed,
    baseFeePerGas,
    proposer,
    lastCommitBitmap: options.lastCommitBitmap,
    lastCommitHash
  };
  return {
    block: { header, transactions: applied, lastCommit },
    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 blake38(combined);
}

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

// src/chain/ase-token.ts
import { blake3 as blake39 } from "@asentum/crypto";
import "@asentum/types";
function bytesToHex5(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 = blake39(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: bytesToHex5(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 ?? []
    });
    await installVaultContract({
      store,
      vm: options.stakingVm,
      chainId: options.chainId
    });
    await installWorkerRegistry({
      store,
      vm: options.stakingVm,
      chainId: options.chainId
    });
  }
  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),
    // Genesis has no predecessor to commit to. See emptyLastCommit()
    // for the canonical empty-cert value used in block 1 too.
    lastCommitHash: new Uint8Array(32)
  };
  return { header, transactions: [], lastCommit: emptyLastCommit2() };
}

// 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