RLP Golf
Hand-written Yul assembling canonical RLP with mstore8 headers and calldatacopy bodies, scored purely on gas.
RLP Golf — NNS CTF 2026 (Blockchain, gas golf)
Result: solved / on leaderboard — best_gas 214,187, 235 points, rank 37 (leader 100,814). hasFlag:false (dynamic gas-golf scoring, no flag string).
Challenge
On-chain gas-golf. Implement IRLP.encode(bytes calldata data, uint16[8] calldata lengths) pure returns (bytes) that produces the canonical RLP encoding of the FIXED tree
[s0, [], [s1, s2], s3, [s4, [s5, s6], s7]], where s0..s7 are byte strings whose lengths are lengths[0..7] and whose bodies are data concatenated. Scored by total gas over 32 rounds; must byte-match the reference CanonicalRLP. Banned opcodes (EVM purity: no state/env, plus EXTCODEHASH/TLOAD/TSTORE/LOG0-4).
Lengths are always a permutation (within depth classes) of the multiset {0,1,1,2,55,56,200,300}: root s0,s3 ∈ {2,300}; middle s1,s2,s4,s7 ∈ perm{1,55,56,200}; leaf s5,s6 ∈ {0,1}. generate also forces exactly one length-1 string < 0x80 and one ≥ 0x80.
Solution
A hand-written Yul contract that assembles the RLP directly with mstore8 headers + calldatacopy bodies, computing encoded lengths first (two-pass) then writing left-to-right. Canonical rules:
- string len 0 →
0x80; len 1 → bare byte if <0x80 else0x81,byte; 2..55 →0x80+len,body; 56..255 →0xb8,len,body; ≥256 →0xb9,lenBE2,body. - list payload p →
0xc0+pif p≤55, else0xf8,p(p≤255) or0xf9,pBE2. Returns ABI-encodedbytes(offset 0x20, length, body). Pure — no banned opcodes. Source:blockchain/rlp/rlp.yul. Verified viacourse.run(target, seed, 32)= 214,699 gas (no revert = byte-exact for all 32 rounds).
Submission mechanism (on-chain, commit–reveal)
Instance is an anvil devnet (chain 31337) exposing /rpc, /api/state (gives player key + golf/course/setup addresses). Golf contract: commit(bytes32) → wait → submit(bytes creationCode); submit CREATEs the code, EXTCODECOPYs the runtime, calls purityChecker.check(runtime, opcodeMask) (a11e2d83), then course.run and records best_gas. SUBMISSION_COOLDOWN=60, rounds=32.
Commit value: submit verifies the stored commitment (slot1) against an expected value and reverts 0x40d56d23(stored, expected) on mismatch — which conveniently leaks the expected commitment for your exact code. It is a stable pure function of the code (NOT block-dependent — the BLOCKHASH in the contract feeds the scoring seed, not the commitment). So: commit(expected) (read it from a dry-run eth_call submit revert), then send the real submit(bytes).
Flow used:
- compile
rlp.yul(solc —strict-assembly —optimize) → creation bytecode. cast call submit(code)(dry run) → revert0x40d56d23revealsexpected = 0xb80bc3cc…bf0e5.cast send commit(0xb80bc3cc…).cast send submit(code)→ status 1;/api/statestanding: best_gas 214187, points 235.
Tooling (WSL2): foundry (forge/cast/anvil) + solc 0.8.30 static + heimdall (for disasm). Player key from /api/state. Files: blockchain/rlp/{rlp.yul, submit.sh, probe_submit.sh}.
Possible improvement
Leader is ~100k gas (≈half). Further golfing (tighter dispatch-less runtime, fewer mstore8, word-packed writes) would raise the rank/points. 235 pts is the current registered solve.