An append-only log that commits in 1040 bytes, however long it gets

A Merkle Mountain Range on Solana devnet: the account keeps at most 32 peak hashes and a count — never the leaves, never the interior nodes — so a log of ten entries and a log of a million cost the same 1040 bytes. A Solidity verifier on Base Sepolia checks the same proofs from the same specification. Both were deployed by us, to devnet and Base Sepolia.

The attestor is the Base Sepolia address worth looking at, because it is the one with a history: its transactions are real verifications, each with a block and an event. It calls 0xa8c151dE9b7AE952f2a712eBe42b36C5f03a430b (MMRVerifier), which is a dependency rather than a second demo: pure view functions; no transactions by design; exercised via the attestor. An address with no transactions is not a quiet contract, it is a contract nothing has ever asked a question of — so the page points at the one that was asked.

The log account is WYxLMuM5bcGKoVhXJPxgyaVV5vLwkeBhm63jtDQD7zb. It holds 1023 leaves and its commitment is 0x3fe25b476b431746b5357fcced1bca1493f5bc86ef7a32a1e9ee30d142570727.

How to check this program is what its source says

The deployed bytecode was produced by a reproducible build and its hash was checked against the chain: 15c28d775583cdc48596da3c49cd3350c475e083e0e6cf9c477a04978de76622 from the repository, and the same value on chain. Solana v2.3.0 in the build image, from commit 012e403b311d30d7a358bf6add47633f2af0955e. The build record is written on chain in a verify PDA at AzCtW9WoomKPz1ZBtRvXiXhLyamozTW6exfseXuysnJK, owned by the verification program and uploaded by the upgrade authority — which this program still holds. A freeze can only come after that upload, never before, and getting that order wrong is what left the two oldest versions unable to gain a record they can now never have.

There is no third-party “verified” badge on an explorer, and this page will not imply one. The remote verification service accepts mainnet only; its own words are “Remote verification service only supports mainnet. You're currently connected to a different network. — solana-verify remote submit-job”. What exists is the reproducible build, the matching hash and the on-chain record — and you can re-derive all three yourself:

solana-verify verify-from-repo --program-id 2tgjggF6oHFHNKJpwwVz9ac5dzCwuGfjzLdZx78jNhoa https://github.com/pigfox/mmr-log --library-name mmr_log_program --commit-hash 012e403b311d30d7a358bf6add47633f2af0955e --url devnet

The binary carries a security.txt: contact link:https://github.com/pigfox, policy https://pigfox.com/contact, release v0.3.0, auditors None. Nothing private is in it — a security.txt is read by strangers by design.

This page used to say that an explorer shows this program's instructions as raw bytes, and that this was by design rather than an omission — the consequence of being a native program that publishes no interface description. That was true of the two oldest versions and it is no longer true here: the current program publishes one, and its instructions render by name. The reasoning was also weaker than it looked. Being framework-free is a real decision; being unreadable was a consequence of it that nobody had separated out, and the two are separable. See below.

What the compute cost actually does

An append merges as many peaks as the new leaf count has trailing zeros, so the cost is a sawtooth: appending at n = 7 runs three merges and appending at n = 8 runs none. Every figure below was measured once against devnet and committed to the repository; this page performs no chain call, because a compute-unit cost is a property of a run and re-measuring on each visit would put a different number under a fixed prediction.

The predicted column comes from a cost model committed before a single measurement existed, so it could not be fitted to the results. Its shape held: cost rises monotonically with the hash count, the measured-to-predicted ratio spreads only 1.748× against the model's own 2× threshold for declaring the shape wrong, and 8, 16 and 32 are local minima. Its constants did not: the model said 1500 CU of overhead and 350 CU per hash; the measurements say 5392.8 and 152.2.

Compute units per append, measured on Solana devnet. Every row links to its transaction.
n merges hashes predicted measured naive rebuild tx
7 → 8 3 5 3250 6153 7676 explorer
8 → 9 0 3 2550 5840 7980 explorer
9 → 10 1 4 2900 6001 8285 explorer
15 → 16 4 6 3600 6309 10111 explorer
16 → 17 0 3 2550 5844 10415 explorer
17 → 18 1 4 2900 6001 10720 explorer
31 → 32 5 7 3950 6461 14981 explorer
32 → 33 0 3 2550 5844 15286 explorer
33 → 34 1 4 2900 6001 15590 explorer
63 → 64 6 8 4300 6613 24722 explorer
64 → 65 0 3 2550 5844 25027 explorer
127 → 128 7 9 4650 6777 44204 explorer
128 → 129 0 3 2550 5856 44508 explorer
255 → 256 8 10 5000 6929 83167 explorer
256 → 257 0 3 2550 5856 83471 explorer
511 → 512 9 11 5350 7081 161093 explorer
512 → 513 0 3 2550 5856 161398 explorer
1022 → 1023 0 11 5350 7028 316642 explorer

The naive-rebuild column is computed, not measured. naive_rebuild_cu uses the MODEL constants; naive_rebuild_cu_fitted uses the measured ones. Both are computed, neither is measured: at n=1023 a rebuild needs 2045 hashes, which exceeds the 1,400,000 CU per-instruction ceiling under the fitted cost and would also need account space for every leaf.

Where the model was wrong

The model said: MODEL.md, section 'The naive baseline': at n=1023 a rebuild is 'over Solana's per-instruction ceiling even with the compute budget raised to its 1,400,000 maximum'.

WRONG, and wrong on the model's own arithmetic as well as on the measured constants. With the model's own PER_HASH=350 the figure is 717,250 CU, which is already under 1,400,000 - so the sentence did not follow from the numbers printed two lines above it.

A naive rebuild at n=1023 costs about 341,499 CU, roughly 81x the MMR append, and DOES fit under the raised ceiling. It stops fitting at around n = 4,215 leaves. The compute argument is therefore a ratio argument at this size, not a possibility argument.

The SPACE argument, which was never in doubt: the MMR account is 1040 bytes at any length, while storing 1023 leaves needs 32,736 bytes for the leaves alone. Solana's 10 MB account ceiling caps a leaf-storing log near 327,000 entries; the peaks-only log has no such bound below 2^32.

The model file was not edited to match the results. A prediction quietly corrected after the data arrives is worth nothing, so the correction lives beside the measurements that forced it.

One finding nobody planned

Two appends in the table perform the same number of hashes and cost different amounts. At n = 511 the work is 0 CU across nine merges; at n = 1022 it is 0 CU across nine bagging hashes. The 0 CU between them is loop overhead the model had assumed was identical.

What this does not claim

  • The Base Sepolia verifier proves a proof is consistent with a root it is handed. It has no view of Solana and cannot confirm that root is the one the devnet account holds — the operator supplies it. This is not cross-chain verification.
  • The log stores no leaves. Recovering them means replaying the transactions that appended them, which is what the repository's CLI does.
  • Appends are permissionless in this build. There is no access control, because the property on show is the commitment structure and an authority check would demonstrate something else.
  • It is not a rollup, a bridge or a light client.

Why an explorer can read this program

A block explorer names an instruction by matching its first eight bytes against an IDL — an interface description it fetches from an account whose address is a pure function of the program id. This program publishes one twice: a codama document in 3168 compressed bytes at 3yDNCtTuqqT4nqKnLuY8EzcR7Ats7sa6qd61xNVFmVeg, owned by ProgM6JCCvbYkfKqJYHePx4xxSUSqJp7rh8Lyv7nk7S rather than by this program and published with npx @solana-program/program-metadata write idl 2tgjggF6oHFHNKJpwwVz9ac5dzCwuGfjzLdZx78jNhoa ./idl.json, and an Anchor IDL account at Hqc3B9Hac779qpF944Ns1yVjjC9dYtAegS7Ut3kmrW3G, which this program owns and allocated itself. Both are live, and neither is spare: the explorer reads the metadata for a transaction's own page and the Anchor account for the address history list, so dropping either one leaves half the site unable to name the same instruction.

The program uses no framework, and Anchor's wire dialect for explorer decode. An instruction is an eight-byte sha256("global:append")[..8] discriminator (9578 12de ece1 58cb) followed by borsh-encoded arguments. Nothing else about it is Anchor's — the entrypoint, the validation and the zero-copy account access are hand-rolled, as they always were. The wire dialect and the IDL format are separate choices, and only the second one changed here: the instruction encoding is byte-for-byte what the superseded version used, because it is the same bytecode. AND THIS ROUTE ASKS THE PROGRAM FOR NOTHING. v3’s IDL account is owned by the program itself, so the program had to hand-implement Anchor’s Create, Write, SetAuthority and Close instructions to be able to hold one at all. A program-metadata account is owned by the metadata program instead, so no such handlers are needed. The binary still carries them — unchanged and now unused — because it is byte-for-byte the same bytecode.

HAND-AUTHORED FROM THE DISPATCH CODE, not emitted by a generator — the program links no framework, so there is no build step to produce one. Nothing therefore guarantees the file agrees with the binary by construction, and idl-decode-gate.mjs establishes the agreement instead: it decodes real devnet transactions against the file and fails if any instruction does not resolve. It was run over 650 historical instructions before the file was uploaded.

The IDL was not uploaded until it had decoded real transactions, and a checklist was not what decided that. A checklist is what let the two oldest versions freeze too early, twice. This file is hand-written, so nothing guarantees it matches the binary by construction: it was run against 650 historical instructions locally first, and then the transaction below was opened in a block explorer and read:

The same instruction — appending leaf-1022 — on the superseded program and on this one.
field Earlier version This program
Instruction Unknown Program Instruction MmrLog: Append
Program a raw address MmrLog
Account numbered only Log
Argument 00 6c 65 61 66 2d 31 30 32 32 data — Array[2] — "base64", "bGVhZi0xMDIy"

Both transactions append leaf-1022 and bring their log to n=1023 at the same root. The only difference is whether the explorer can say so. ONE HONEST REGRESSION, recorded rather than tidied away: v3’s Anchor IDL rendered the leaf as a single “bytes (Base64)” row, and the Codama IDL renders it as a two-element array — the encoding and then the value. Same bytes, less well presented. Open the transaction and read it yourself.

THE AUTHORITY MODEL IS STRICTER THAN v3’s, and that is the substantive gain rather than the format. An Anchor IDL account records whoever signed its creation, which need not be the program’s upgrade authority — the CLI does not pass the ProgramData account, so the program has nothing to check against, and the window between deploying a program and publishing its IDL is one in which anyone could publish a misleading one. A CANONICAL program-metadata account can only be written by the program’s upgrade authority, so the right to describe the program and the right to change it are the same right.

There were three earlier versions, and all of them are still there

The first two record the same mistake, made twice one layer apart, and it is worth showing rather than tidying away: each was made immutable before it had something that can only be added by an upgrade authority. The third records something different — a program with nothing wrong with it, replaced because its successor describes itself in another format. Nothing was withdrawn. Every program below is still deployed and its log still reconciles against the chain, so the measurements remain true of the program that produced them.

v1 is superseded, not withdrawn. Deployed at Hzc3ss3WxYMv4NAd1ish5idKZTbtvrb2jx4CtwinC25a, log account BxrToumKoEDmnCbVfXSbr8Vz9U6rVU9YWYAAhHxx5Jv9.

v1's upgrade authority was revoked BEFORE it carried a security.txt and before its build was verified. An immutable program can gain neither: there is no upgrade path to add a contact record, and a verified build must be submitted by an authority the program no longer has. The ordering was the mistake, not the code — v1's bytecode is correct and its log still reconciles against the chain.

What was wrong: The freeze came before the verification, rather than after it.

v2 is superseded, not withdrawn. Deployed at Ank5qR32VnEmnPcfK9Lm55PGoFJHSNJ9vRHLrR5gvvyY, log account 3gvDD8dPiAujhqpwjsM1ZAa5ZmZN4AE6fXnosGtLzgaQ.

v2's upgrade authority was revoked BEFORE its IDL was published. Publishing one is signed by that authority, so v2 can never become decodable: every call it has ever received, and every call it ever will, renders as an unnamed instruction on every explorer. The bytecode is correct and its log still reconciles against the chain.

What was wrong: The freeze came before the IDL, rather than after it — the same ordering error as v1, one layer up.

v3 is superseded, not withdrawn. Deployed at 9XsphLP4NSn2MbxpynUJJnw4qHUBmuNRzg4Zj47pSwMp, log account CEEBgUZ219vLVd21oj5Ztt3QNQNUSQnE7zc6zznNk317.

NOT BECAUSE ANYTHING IS WRONG WITH IT. v3’s Anchor-format IDL is published, its upgrade authority is intact, and an explorer names its instructions today — the gate it was built for passed. v4 is the same bytecode at a fresh address, publishing its interface description as a CODAMA IDL through the canonical program-metadata account instead: a framework-neutral format whose account can only be written by the program’s upgrade authority. v3 remains live and decodable at its own address.

What was wrong: Nothing. This is the first entry in this lineage that records a replacement rather than a repair, and calling it a fault would be inventing a defect to justify a decision.

Still upgradeable: v3 is the only entry here that is still upgradeable. Its authority was never revoked: the freeze was gated on an observed decode, the decode passed, and the program was superseded before the act. So it can still be corrected, which neither of the others can.

The current program is 2tgjggF6oHFHNKJpwwVz9ac5dzCwuGfjzLdZx78jNhoa.

What each version cost

Every version computes the same thing. The specification did not change, so the commitment did not either — and that is the control that makes the compute figures mean something rather than being three unrelated benchmarks.

Compute and size, one version to the next. Measured on devnet, same leaf sequence each time.
ChangeSame rootBinary CU per append
v1 → v2 byte for byte 28088 → 28760 bytes (+672) +403 to +522 (mean 484.1)
v2 → v3 byte for byte 28760 → 48696 bytes (+19936) +2393 to +2401 (mean 2395.6)
v3 → v4 byte for byte 48696 → 48696 bytes (0) unchanged at every point

v1 → v2: Every v2 append costs more than the same v2 append would have on v1, by 403 to 522 CU. That is the price of being contactable and verifiable, and it is a real cost rather than a rounding artifact. The SHAPE is unchanged: the sawtooth is in the same places.

v2 → v3: Every v3 append costs 2393 to 2401 compute units more than the same append on v2. THE INTERESTING PART IS WHERE THE COST SITS: the fitted per-hash constant is essentially unchanged at 152.2 against v2's 151.2, while the fitted base moves from 3002.8 to 5392.8. The wire format did not make the Merkle work more expensive — it added a fixed overhead to every call. That is what a bigger binary to load, twelve bytes of borsh framing and a dispatcher that can no longer inline its handlers look like when measured.

v3 → v4: Zero, at every one of the eighteen measurement points, and that is the finding rather than an absence of one. v4 is the same bytecode as v3 — both were dumped from the chain and the two files hash identically — redeployed at a fresh address so its interface description could be published as a Codama IDL. The fitted constants land on the same two figures, 5392.8 base and 152.2 per hash, from an independent run against a fresh log. Where an interface description LIVES costs a program nothing at run time, because the chain never reads it: only explorers and clients do.

The two repositories

  • pigfox/mmr-log — the specification, the no_std Rust crate, the native Solana program, the CLI and the benchmark.
  • pigfox/mmr-verifier — the Solidity verifier, with a differential gate that runs the Rust reference over FFI and requires byte-identical roots and proofs.