Discovered 2026-05-31 by the ergots runner (Dasher, ts-runner/) on its first run of
the committed santa-eval corpus (33 files / 269 entries) against the JVM-blessed expected.
ergots is the first independent conformer, so a Dasher-vs-blessed mismatch is a genuine
JVM-vs-ergots divergence (the JVM sigma-state 6.0.3 is canonical per BOOTSTRAP decision 1).
ergots is a v5/mainnet library; Dasher's gate runs the v5 input bucket. The runner emits one faithful outcome for every entry (no abstain — scope is which vectors you run, not a runner behavior). Current gate: 1705 / 1705 — fully green (was 1632/73, then 1678/27); every value/cost divergence and the 27 not-implemented methods below are now fixed in ergots. The repr (Header-ts) findings remain open but live in the v6 bucket (not run by the v5 gate) — documented below.
Reproduce: cd ts-runner && npm test, or
node ts-runner/dist/runner.js vectors/eval/v5/<op>.json.
RESOLVED: fixed in sigma-rust
d59d8d9f(ergo-node-integration) + ergots6171d32; after ergots'distrebuild Dasher matches the JVM on all six (12/12 covered nice). The e2e gate flipped 6→0, kept as a regression guard. The record + root-cause analysis below stands.
ergots undercharges JIT cost by 5 per lambda application — it omits the AddToEnvironment
(FixedCost 5) charge that sigma-state makes on every environment insertion, including
lambda-argument binding. (ergots already charges it for BlockValue ValDefs; the
FuncValue/Apply lambda path was missed.) Each evaluable v2 entry is a closed
Apply(FuncValue([arg], <body>), [OptionGet(GetVar(1))]) that applies the user function once
→ −5; the higher-order-lambda applies five lambdas → −25. v1 closed trees have no
Apply/FuncValue, so they match the JVM exactly.
| op / entry | JVM (canonical) | ergots | Δ |
|---|---|---|---|
Box.getReg (Box_properties_equivalence_new_features #0) |
135 | 130 | −5 |
Coll.getOrElse with lazy default (Coll(1) #0) |
101 | 96 | −5 |
substConstants v6.0 ErgoTree v0 (#0) |
351 | 346 | −5 |
substConstants v6.0 ErgoTree v0 (#1) |
351 | 346 | −5 |
Option.getOrElse with lazy default (Some(2) #0) |
90 | 85 | −5 |
higher_order_lambdas (map over Coll(1,2)) |
408 | 383 | −25 (5 lambdas) |
Root cause (confirmed on both sides — empirically via cost-traced runs and source):
- ergots charges 0 for lambda-arg binding:
~/projects/ergots/packages/ergoscript/src/eval/apply.ts:62(bodyEnv.extend(argId, arg)with noctx.addCost);func-value.tscharges onlyFUNC_VALUE_COST = 5(closure creation). - sigma-state charges 5:
tmp/sigma-src/sigma/ast/values.scala:1047(E.addFixedCost(AddToEnvironmentDesc_CostKind, …)), withAddToEnvironmentDesc_CostKind = FixedCost(JitCost(5))atvalues.scala:1064. - Every other wrapper node matches 1:1: Apply 30, FuncValue 5, GetVar 10, OptionGet 15, ValUse 5. The
higher_order_lambdasmap per-item envelope also matches; its extra −20 is just four more instances of the same omission (5 lambda bindings × 5).
Upstream: sigma-rust carries the same gap, narrowed to one site. It already charges
ADD_TO_ENV_COST for BlockValue ValDefs (eval/block.rs:30,45, commit 50f44685, with a comment
documenting the Scala parity gap) and for coll-op lambdas (6ded99cb) — but not for the
generic Apply→lambda binding (eval/apply.rs:36: env.insert(*idx, arg_v) with no charge; Apply
pays only its own add_jit_cost(30) at line 18). ergots has the identical shape: its coll-map
per-item charge is correct, so the hole is likewise the generic Apply path (apply.ts), not
coll-ops. This is the same class as the three cost undercharges in
eval-jvm-vs-sigma-rust.md (also first surfaced via ergots), one site further.
Fix: charge 5 (AddToEnvironment) per lambda-arg binding in the generic Apply eval — in
ergots (apply.ts) and upstream sigma-rust (apply.rs:36, mirroring block.rs:30/45); verify no
double-charge against the already-fixed coll-op path. One fix closes all 6. Route the sigma-rust half
to the open JIT-cost-parity PR #854 mwaddip:jit-costing (the line of work already carrying the
BlockValue/coll-op ADD_TO_ENV fixes).
| op / entry | JVM (canonical) | ergots |
|---|---|---|
Header.checkPow (Header_new_methods #0) |
Boolean true, cost 774 | cannot parse input |
Global.deserializeTo[Header] (Global.deserializeTo_header #0) |
Boolean true, cost 677 | cannot parse input |
Both inputs are Headers carrying a synthetic timestamp 4928911477310178288 (≈4.9×10¹⁸).
ergots stores Header.timestamp as a JS number and rejects any value > Number.MAX_SAFE_INTEGER
(2⁵³) with ReaderError('vlq-overflow') — a deliberate NIP-08 byte-exact-round-trip decision at
~/projects/ergots/packages/scorex/src/header.ts:69. The JVM uses a full Long, accepts the
header, and evaluates. Real chain timestamps fit in <2⁴⁵, so this never bites mainnet — only this
synthetic spec value. Dasher records these as unrepresentable outcomes (value/cost null, error: "unrepresentable") — present in actuals, never omitted. They live in the v6 corpus, so
they are not exercised by ergots' v5 gate; they remain a real, open repr bug for ergots'
future v6 support.
Fix: represent Header.timestamp as bigint (full u64) to match consensus.
Surfaced 2026-06-05 when ergots closed its serialize(AvlTree) eval gap. It previously threw
method-not-implemented for Global.serialize[AvlTree] (ts-runner/test/runner.test.ts carried that as
a not-impl anchor, now re-pinned to a faithfulness check). With the gap closed, Dasher grades
vectors/eval/v6/authored/Global.serialize_AvlTree.json value-nice, cost-coal — the serialized bytes
match the JVM exactly, but the JIT cost is uniformly 1 under:
| op / entry | JVM (canonical) | ergots | Δ |
|---|---|---|---|
Global.serialize[AvlTree] (dummy#0) |
127 | 126 | −1 |
Global.serialize[AvlTree] (withValueLen#1) |
127 | 126 | −1 |
Both entries: value (the Coll[Byte] wire encoding of the AvlTree) is byte-identical to the bless; only
the cost differs, by a fixed −1. This is the serialize-cost-metering family — value agrees, the JIT
cost is short by a small fixed amount — the same axis as the other Global.serialize[T] cost divergences
the v6/authored serialize work tracked (AvlTree was a cost-coal entry for sigma-rust too). The exact
ergots site is not source-confirmed from the SANTA session (cross-repo); route to ergots to locate the
missing 1-unit charge in its Global.serialize cost path.
Visible only via ./conform (the v6/authored slice). Dasher's ts-runner e2e gate walks the v5 bucket
only, so it does not grade this; the re-pinned runner.test.ts asserts runner faithfulness (no crash, a
value present), not the cost match — the cost divergence is graded by ./conform and recorded here, per
divergences-are-the-deliverable (never pinned green).
Route: prompts/ergots-serialize-avltree-cost.md.
RESOLVED: all 73 closed. ergots landed every value/cost fix and then the 27 not-implemented methods (
Coll.updated/updateMany/GroupElement.negate,35eac6b); SANTA re-blessed box inputs to ≥protocol-min (santa-rebless-min-box-value). Dasher v5 = 1705/1705, pinned as a full-green regression guard. The record below stands.
Dasher's run of the v5 spec corpus (vectors/eval/v5/, 84 files / 1705 entries, extracted
at ErgoTree v2) against the JVM-blessed expected, ergots' declared v5 scope: originally 1632
nice, 73 RED — 10 value/error, 36 cost, 27 not-implemented, and 0 reject. The corpus now
includes 147 harvested reject vectors (the spec's Failure-expected cases blessed as coarse
errored); ergots rejects every input the JVM rejects, so reject divergences are 0 (135 score
reject-nice). The 27 not-implemented are the 3 v5 methods ergots lacks (Coll.updated,
Coll.updateMany, GroupElement.negate — confirmed v5 in sigma-state methods.scala
v5Methods, NOT v6-gated): their 15 accept cases plus the 12 updated/updateMany reject
cases. No "abstain" — every entry yields a faithful outcome; gaps are honest RED, routed via
prompts/ergots-v5-method-gaps.md, the 46 value/cost divergences via
prompts/ergots-v5-divergences.md. All ergots-vs-JVM (JVM canonical).
Value/error (10):
- Negation overflow (4) —
Numeric_Negation_equivalence-{128, 32768, 2147483648, 9223372036854775808}(#0/#9/#18/#26): JVM wraps-MIN_VALUE→MIN_VALUE; ergotserrored. (ergots negation appears checked; sigma-state wraps.) - substConstants (5) —
substConstants_equivalence(Coll[Byte], pos): JVM returns the substitutedColl[Byte]; ergotserrored. Input-specific — ergots evals the v6 substConstants fine. - flatMap empty-Coll type (1) —
Coll_flatMap_method_equivalence :: Coll()#0: JVMColl[SByte][], ergotsColl[SAny][]. Empty input → no first-iteration elem refinement (ergotsmir/expr-tpe.ts:147-150, in-code acknowledged).
Cost (36, ergots mostly under-charges; distinct from the resolved AddToEnvironment −5):
Coll.flatMap (Δ −90…−190) · Coll.indexOf (Δ ±3…16) · NEQ-nested-colls (Δ −1…−18) ·
SigmaProp.propBytes (Δ −18…−210 — ergots flat 111; JVM scales with the sigma-tree's serialized size).
Full per-entry deltas are in the e2e cost-divergences: block.
- Cost —
AddToEnvironmentlambda undercharge (6 entries): RESOLVED 2026-06-01 — fixed in sigma-rust (d59d8d9f) + ergots (6171d32); ergotsdistrebuilt; Dasher nice on all 12 covered. The e2e gate flipped 6→0 (kept as a regression guard). - Repr —
Header.timestampcap (2 entries): OPEN — route to ergots. - v6 —
serialize[AvlTree]cost (2 entries): OPEN (surfaced 2026-06-05 when ergots closed theserialize(AvlTree)not-impl gap) — ergots undercharges by 1 (126 vs JVM 127); value agrees byte-for-byte. Visible via./conform(v6/authored), not the v5 e2e gate. Route to ergots. - v5 corpus (2026-06-01 → RESOLVED 2026-06-02): originally 73 RED — 10 value (negation overflow ×4, substConstants ×5, flatMap empty-type ×1) + 36 cost (flatMap / indexOf / NEQ / propBytes) + 27 not-implemented (Coll.updated / Coll.updateMany / GroupElement.negate — accept + reject cases) + 0 reject. All fixed in ergots (value/cost via the routed fixes; the 27 methods via
35eac6b); SANTA re-blessed box inputs to ≥min. Dasher v5 = 1705/1705 — the e2e pins are now a full-green guard. - Reject arm (2026-06-01): the corpus now harvests
LanguageSpecificationV5/V6's Failure-expected cases as coarseerroredreject vectors (147 v5 + 24 v6). Dasher's reject bucket found 0 divergences — ergots rejects every input the JVM rejects. Coarse only; the rejection-reason taxonomy stays deferred (runner-contract §7). - Dasher's e2e gate (
ts-runner/test/e2e.test.ts) pins each RED count; when ergots fixes any, the count drops and the gate flags it for re-baselining.