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.
2tgjggF6oHFHNKJpwwVz9ac5dzCwuGfjzLdZx78jNhoa
the program
0x3ecC299134803640B8690521d2aAFF3657fe3202
the attestor — verification as a transaction
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.
| 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:
| 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.
| Change | Same root | Binary | 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_stdRust 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.