Shekyl FoundationAutomated
10/12/2026, 11:16:42 PM
State of dev as of 924601d21bed, 12 October 2026.
Three days in which two things the earlier posts described as ruled were carried out: Monero's upgrade machinery is gone in one change, and the first consensus piece of the secret archival draw is in every block. Alongside that, the signature scheme the draw's receipts will use was built, the design for how a wallet reaches a node on another machine was ratified, and a node's own transactions stopped travelling on a link of their own.
Monero's upgrade machinery is deleted — LANDED
Monero changes its consensus rules at numbered network upgrades. A table maps block heights to versions, and much of the node asks that table which version is in force before deciding what to do. Shekyl inherited all of it and starts at a single block version, so the table had one row and every question put to it had one answer. The two posts before this one reported the ruling to delete it and the removal of its voting half. The rest went this window, in one change. It was done in one change because removing the mechanism a piece at a time would have left an upgrade system with pieces missing, which is harder to reason about than either having one or not.
What left: the table and the class around it, the block-version argument that was passed through the transaction pool, the transaction checks, the reward calculation and the coinbase builder, the start-up step that unwound blocks made under an older version, and the warning a node printed when it saw a block version above its own. The main commit touches 142 files and removes about 3,900 lines. The rule that remains is a comparison against two constants: a block's version is 1.0 at every height, on every network, and both validators refuse anything else. No valid chain is affected, because that was already the only version a block could carry.
Node operators will see a few surfaces disappear. The hard_fork_info RPC method and the console command of the same name are gone, get_version no longer lists upgrades, status no longer prints a version or a countdown, and the import tool loses --drop-hard-fork. The peer-to-peer handshake no longer carries a block-version field, and a peer that still sends one is read with the field ignored.
The deletion deliberately leaves nothing behind for a future upgrade to reuse. The policy is that a consensus change begins as a design document, and whatever activates it is written then, for that change. How a future change would activate has not been decided, and removing Monero's answer does not decide it. The Rust validator keeps its own small structure naming which rule set applies at which height. It has one entry, the same on all three networks, and it is not derived from the table that was deleted.
Every block commits to a seed for the secret draw — LANDED
The post of 7 October described the ruled design for testing archivers: each block's producer commits to a random seed in its block, the seed and the block hash select which archivers to test, and the producer later reveals the seed with the signed receipts it collected. That post said none of it was built. The first consensus piece now is. Every coinbase transaction must carry a 32-byte commitment in a fixed position, and a block without exactly one is refused. One Rust implementation writes the field and judges it, and the C++ validator calls that implementation, so the two cannot disagree about the layout.
The rule applies from the genesis block, with no activation height. Admitting the field first and requiring it later would have been a second consensus change at the later point, and the test network runs tagged builds whose next tag resets it in any case, so there was no chain to stay compatible with. The cost is that the genesis block of every network was regenerated to carry the field, so every network identifier changed and a node on an earlier build cannot sync with one on this build. Separately, the alpha.10 testnet build moved the testnet genesis for an unrelated reason, the header byte pinned to zero in the last post. An alpha.9 testnet data directory cannot be carried forward; the changelog has the operator steps.
What the field holds needs stating carefully. The commitment is a hash over a per-block signing key and a seed. A block producer draws a real key and a real seed, commits to them, and discards both before the function returns. The part of the node that will keep them, read from the selected archivers and file the reveal is not written. So every block commits to a seed that nobody can open, and no test is issued. Validators check that the field is present and in its place and do not judge its contents. The secrets are drawn inside the Rust code that writes the coinbase, and the C++ side never holds them.
Two other pieces moved. The earlier public draw, the one anyone could compute in advance, is deleted. The 7 October post said it would go in the same change as the records it replaces. It did not: it had no caller in a running node, so it was removed on its own, and the records the running node still uses stay until their replacements land. The other piece is settlement, the step that turns a period's tests into one verdict for each archiver and each part of history it holds. It is built in the Rust validator, and both the penalty and the reward now read that single verdict instead of gathering evidence separately. With no tests issued it writes nothing, and the Rust validator is not yet the one that judges blocks. The rule for how many tests a block issues was also checked against the economics simulator: the integer arithmetic in the node and the simulator's floating-point model gave the same count in every block of the run.
A second hybrid signature scheme, for receipts — LANDED
The receipts archivers sign are short-lived: once a settlement period closes they stop mattering. The ruling reported on 7 October gave them their own signature scheme, FN-DSA, which is smaller than the ML-DSA scheme used for identities, and kept an archiver's long-lived identity on the finalised standard. That scheme now exists in the cryptography library, as a hybrid of Ed25519 and FN-DSA-1024.
It uses the same construction as the existing hybrid. The post-quantum half signs first, and the Ed25519 half then signs the message together with the post-quantum signature. A verifier that skipped the post-quantum half could not reconstruct what Ed25519 signed, so a shortcut fails to verify instead of verifying with half the protection. Both schemes now share one implementation of that construction. A byte identifying the scheme is bound into what is signed, so a signature cannot be relabelled as the other scheme's, and the new scheme is refused as a transaction authorisation: a receipt key cannot sign for an output. An encoded signature is 1,356 bytes against 3,385 for the identity scheme. On an x86 development machine signing takes about 457 microseconds and verifying about 58. The figures for the smallest supported device have not been taken.
FN-DSA is not yet a final standard, and the library the node depends on says of itself that keys and signatures from a pre-1.0 version may stop verifying under a later one. A test network can absorb that with a reset. A chain with a real genesis cannot, because archivers' receipt keys would be recorded on it. So a check in the build pipeline refuses to produce a genesis build while the library is below 1.0. It decides what counts as a genesis build by recognising pre-release tags and treating everything else as genesis, so a tag in a shape it does not recognise is refused. The first version of the check did the opposite, recognising one shape as genesis and passing everything else; it was reversed before the change merged.
Each archival identity a wallet holds now derives a receipt key from the wallet seed, so a restored wallet derives the same key. Nothing signs receipts with it yet. The one place the scheme runs in a node is the per-block key inside the commitment described above. Putting the receipt key into an archiver's bond record and moving receipts onto it are separate changes, not built.
One encrypted channel between wallet and node — RULED
A node's RPC accepts connections only from its own machine and refuses every other address, with no override. That is a safe default, and it leaves no supported way for a wallet on one machine to use a node on another machine that the same person runs. The earlier plan for that case was TLS with both ends pinned to each other's certificates. It was never built, and this window it was replaced by a ratified design.
The design is a single channel for every RPC connection that leaves the machine: a Noise handshake that authenticates both ends, then encrypted records carrying the same RPC as before. The reason for leaving TLS is narrow and the design document is explicit about it. Shekyl requires that the wallet path be hybrid, classical and post-quantum together, and here that includes authentication, not only the key exchange. TLS authenticates a peer by a certificate signature, and a hybrid identity there needs certificate formats and verification that the TLS library in use does not ship as a supported path. Noise authenticates by key agreement, so a hybrid identity is two long-term keys, X25519 and ML-KEM-768, whose shared secrets both go into the handshake. The document names what would reopen the question: a TLS library that supports hybrid peer authentication. The wallet knows the node's public keys in advance; the node learns which wallet is calling during the handshake, encrypted, and checks it against a list the operator enrolled. An enrolled key carries a ceiling on what it may see and do, which replaces the separate restricted listener. A caller running as the same user on the same machine uses an owner-only socket and needs no keys.
The ratification authorises no implementation by itself; the work is authorised one slice at a time. The first slice was evidence, and it landed. A formal model of the handshake was checked with ProVerif, and all 31 results came out as predicted beforehand, including that the node's authentication and the secrecy of the first request and reply survive either X25519 or ML-KEM being broken, though not both. The wallet's authentication is complete only when its first encrypted record arrives, not at the handshake message that names it, and the model confirmed that as well. Three deliberate edits to the handshake were also modelled, and each broke the property it was expected to break. A check that only ever passes proves little, so the CI job fails if a broken variant passes.
The second piece of evidence was a timing measurement. The handshake's first message asks the node to run an ML-KEM operation with its long-term key on input from a stranger, so the time that operation takes must not depend on the input. The pass and fail lines were written down before any run. The first run on a Raspberry Pi 4 failed all four comparisons. The lines were not moved and that record still says failed. The cause was traced to the harness, which was preparing its inputs inside the timed loop; the difference it produced was about 0.015% of one operation. A new measurement was registered with fresh inputs and two added comparisons of a class against itself, which void the run if they show a difference. It passed on the Pi and on an x86 laptop, and the compiled code on both was read for input-dependent branches. The result excludes a dependence of 0.5% of one operation or more and nothing smaller, on two processor designs, for timing only. The channel itself is not built: no listener, no enrolment, and no wallet speaks it.
A node's own transactions now share a link with relayed ones — LANDED
Shekyl relays transactions the way Dandelion++ describes. A transaction first travels along a short chain of single peers and is only then broadcast widely, so that the node which first broadcasts it is usually not the node that created it. Each node picks two peers to forward along for a period of about ten minutes. A node with Tor sent its own transactions differently: over a separately chosen Tor connection, so that the first peer to see them never learned the node's address. The weakness was in what else used that connection. Because it was not one of the two forwarding slots, almost nothing else was scheduled onto it. In the project's relay model, 82% of what a node sent on that connection was its own.
The routing that replaced it reserves one of the two forwarding slots for connections that hide the node's address. The node's own transactions leave through that slot's peer, and so do the transactions it forwards there for others. In the same model the share that is the node's own falls to between 61% and 64%, against 64% in the model's baseline of plain Dandelion++ with no Tor at all. These are model results at a fixed network size, not measurements of a live network. The separate draw is deleted. If no address-hiding connection is available, the node holds its own transactions until the next period, and does not send them on a connection that would show its address.
The open question was what to do when the chosen peer disconnects in the middle of a period, since an observer who can make connections drop might try to steer the node onto a peer it controls. The recommendation before measuring was to follow whichever peer refilled the slot. The first ruling was different: fix the whole set of address-hiding peers at the start and step through them. Both were then simulated against an observer dropping connections on purpose, and the first ruling was withdrawn the same day. Stepping through the whole set ends on the observer's peer with certainty once every honest one has been dropped, and following the refilled slot gets worse the more of the network's candidate addresses the observer supplies. The rule that shipped fixes the slot's peer and one alternate when the period starts. However many connections are dropped afterwards, the chance of ending up on the observer's peer stays where it was at that moment. The cost is that a node which loses both holds its own transactions for the remainder of the period.
This is live. The C++ relay asks the Rust relay for its routing decision, so the merged rule chooses first hops in a running node.
What a node tells callers about itself — LANDED
get_info is the RPC call most wallets and monitoring scripts make first. It moved from C++ to Rust this window, with the C++ handler deleted, and the move was used to settle what each field means. Anyone who reads this call from their own code should check the changelog, because several fields changed meaning or can be missing. The RPC version is 3.50 at this commit.
The main change is that absence is now a value a client must handle. A node's restricted listener, the one meant for callers the operator does not know, used to return every field and fill the ones it would not disclose with stand-ins: zero connections, an empty version string, a database size rounded up, a free-space figure of the largest possible number. A client could not tell "zero peers" from "not telling you". The wallet could not either: against a restricted node it added up the zeroed connection counts and raised a false alarm that the node had no peers. A restricted reply now omits those fields entirely. Each field is in one of three states: a value, zero included, is the answer; null means there is no answer; a missing field means it was not disclosed to this caller. A reply carrying only some of a withheld group does not decode, so a field lost by accident is never read as a decision to withhold.
What is withheld is the group of facts that describe this particular node and not the network: its version, start time, disk figures and connection counts. A wallet still needs to know whether the node can relay, so a new boolean, has_peers, is told to every caller. It is deliberately not a count. The counts themselves now cover every transport, Tor included, and count only peers that completed a handshake. They used to be clearnet-only, and one was computed by subtracting a figure from one table from a total in another, which could wrap around to a very large number. Counting only completed handshakes also means an outsider cannot raise a node's reported connectivity by opening connections and leaving them.
Three smaller changes follow the same reasoning. target_height is null when the node has no sync target, where it used to be zero, which also meant "synchronised". The pool size counts only transactions the node has broadcast, for every caller; the number it holds and has not yet broadcast is told only to the node's own administrator, because the existence of such a transaction says something about where it came from. And emission_era, a label left over from an idea that was retired, is deleted.
This post describes the state of the dev branch at 924601d21bed on 12 October 2026.
Shekyl is pre-genesis and under active design; anything marked IN FLIGHT may
change or be abandoned. Where a later post contradicts this one, the later post
is correct.
References: PR #1009, PR #1032, PR #1029, PR #1007, PR #1019, PR #1030, PR #1050, PR #1016, PR #1031, PR #1006, PR #1020, PR #1034, PR #1038, PR #1018, PR #1017, PR #1035