Shekyl Stats

Refreshing...

Network

Connected
Seed Nodes Active
--

Chain

Current Block
0
Network Height
—
Top Block Hash
--
Block Time Target
2 min

Rewards

Last Block Reward
0.000000 SKL
Difficulty
0
Estimated Hash Rate
0 H/s

Supply

Circulating Supply
--
Remaining Supply
--
Total Burned
0 SKL

Economics

Release Multiplier
0
Burn Rate
0%

Staking

Stake Ratio
0
Staker Pool
0.000000 SKL
Staker Emission Share
0%
Total Staked
N/A
Staking Height
N/A
Tier 0 Lock Blocks
N/A
Tier 1 Lock Blocks
N/A
Tier 2 Lock Blocks
N/A

Protocol

Transaction Format
TransactionV3
Membership Proof
FCMP++
Spend Auth
Ed25519 + ML-DSA-65
Confidentiality
Stealth + BP+

Node

TX Pool Size
0
Database Size
0 B
Node Version
--
Sync Status
Syncing

Development digest — 23 September 2026

Started by Shekyl Foundation • 0 replies • 20 views

0

Shekyl FoundationAutomated

9/28/2026, 5:21:11 AM

State of dev as of 1b4a58046cde, 23 September 2026. A window dominated by removal: an inherited pruning scheme, an inherited connection limit, a second transaction parser and a bond operation all left the tree, and in each case the reason for the removal is the interesting part. One design question about archival bonds was settled, and the first real chains were replayed end to end through the node's new Rust validator.

An archival bond cannot change after it is posted — RULED

Shekyl pays operators to keep historical chain data available. An operator does this under a pseudonymous identity, which the design calls a persona: the persona posts a bond naming the pieces of data (shards) it will hold, earns for serving them, and can be penalised if it fails to. Until this window the design also allowed a persona to adjust its holdings in place — add a shard here, drop one there — through an update posted against the existing bond.

The problem with that is not the mechanism, it is the trail it leaves. Linking on-chain events to one operator depends on repetition: a series of events of the same kind, from the same pseudonym, over time, is something an observer can cluster. A single event cannot be clustered. In-place holdings updates are precisely such a series, so they would have turned each persona into a steadily growing record of its operator's behaviour.

The Foundation ratified the alternative on 20 September: a persona's bond is fixed for its whole life. To change what it holds, an operator starts a new persona that bonds the new set, while the old one releases and drains, with an overlap so service does not lapse. Each persona therefore authors at most one event of each kind across its lifetime, bond amounts are standardised so the amount says nothing about who posted it, and serving itself is recorded by miners rather than by the persona. The ruling records this reasoning as part of the design, because an in-place update will always look like an obvious efficiency improvement to someone who has not seen it.

The idea is not new. It was proposed early and set aside while shards were still going to be assigned by the system, which would have forced holdings to change involuntarily. Once operators were allowed to choose their own shards, that objection no longer applied, and the question could be decided. The code followed the same day: the in-place update is refused at the wire, and its identifier is kept as unassigned so it cannot quietly return. The one remaining in-place operation — restoring a bond after a penalty, without changing its holdings or moving funds — was renamed from "rebond" to "reinstate", because nearly everyone read the old name as "post a new bond".

The inherited pruning engine and its peer advertisement are deleted — LANDED

The node code Shekyl inherited includes a pruning scheme in which each node keeps a different stripe of old transaction data, and advertises which stripe it holds so peers know whom to ask. Shekyl's design does not work that way: every node prunes identically, and the long-term copy of old data is held by bonded archivers. The stripe engine was therefore dead weight, and this window it was removed in full, along with the --prune-blockchain and --sync-pruned-blocks options and a separate, older tx-data pruning routine that nothing called any more.

The part of the removal that mattered most was on the wire. The stripe a node held was carried as a self-declared value attached to its address in the peer lists that nodes gossip to each other. Every node we observed sent the same value for it, so any other value would have stood out — a marker attached to an address and passed around the network, of the kind that helps someone map which nodes are connected to which. Peer selection also acted on the gossiped copy. The field is now gone from the handshake and from peer lists, and a CI check refuses its return, so it cannot be ported back with some later rewrite of the networking code.

What replaces it was also decided this window (RULED). Old transaction data is grouped into shards. Once a shard closes, it spends one full epoch as the window in which archivers take their copies, and its prunable bodies are discarded on every node at the end of that epoch. Because the rule is stated in terms of the chain rather than any node's own disk, every node discards the same data at the same point without having to look at what it currently holds. An earlier version, ruled a few days before, used a fixed window counted in blocks. It was reopened when the premises behind that window turned out not to hold, and the ruling records which ones and why.

For anyone running a node from dev: the database format changed with these deletions, and an older data directory is refused at startup with an instruction to delete it and resync. The on-disk peer list from an older build is discarded when the node starts.

A limit on connections per address is deleted, and the remaining limit is read from the machine — LANDED

Nodes inherited a limit on how many inbound connections they would accept from one IP address, defaulting to one. The assumption behind it is that one address is one operator. That stopped being true some time ago: behind carrier-grade NAT, one address can be hundreds of unrelated subscribers.

The effect showed up on the test network. Two nodes behind the same NAT ended up connected to entirely separate sets of seed nodes, because each seed would accept only one of them. When fewer seeds were reachable than that split required, one of the two nodes had no peers at all. We looked for a better value for the limit and concluded there was none: it measures the wrong thing, so it was removed. The option now fails at startup with a message explaining why.

That left one bound on inbound connections: a total. Rather than choose a number for it, the node now works it out from what the operating system allows the process to have open, minus what the node has already used, minus what it has set aside for its own outbound connections. The arithmetic lives in Rust and is written so that the answer does not depend on how many peers happen to be connected when it is recalculated. An earlier version got this wrong in a way only visible under load, and the fix has its own test. An explicit --in-peers setting still takes precedence.

We also measured what an idle inbound connection costs in memory on the reference low-end device, a Raspberry Pi 4. The answer was close to nothing, far below what the daemon uses to start. A figure taken on a development machine had been about thirty times higher, and would have been the wrong basis for a network-wide default. So memory does not decide how many peers a node should serve. That remains a separate question, and this measurement rules out one of the possible answers.

One parser for a transaction's extra field, and a closed format for the miner's — LANDED

Every transaction has an "extra" field, a tagged list of small items such as the public keys a wallet needs to find its outputs. Until this window the node parsed it with inherited C++ code and the wallet parsed it with Rust. Two parsers for the same bytes can disagree, and where they disagree a wallet and a node can form different views of the same block. The C++ parser has now been deleted. The node reads the field through the same Rust codec the wallet uses, so the two agree because they run the same code. The inherited tests were replaced one for one with tests on the same bytes, and the fuzzer that exercised the old parser was ported rather than dropped.

The more consequential change is to the miner's own transaction in each block. Its extra field used to accept a free-form "extra nonce" of arbitrary bytes chosen by the miner. That was a place to write anything into every block permanently, and a way for a mining pool to mark its blocks. On the consensus side nothing bounded the field's size either. The field now has to match an exact list of items in an exact order, and nothing else is allowed.

How the nonce got there is worth telling. The first ruling removed the nonce entirely, and it named the cost: a block header's own nonce can be exhausted quickly by a large pool, which would then have to rebuild its work more often. The final form keeps a nonce but fixes its width. The width is large enough that it never limits a miner, and because it is fixed, its length reveals nothing. The block-template RPC bounds its reserved space to match, and an option for writing arbitrary messages into mined blocks was deleted outright. Because the format of the first block changed too, the genesis blocks for all networks were regenerated.

The new Rust validator replays real chains and agrees with the old one — LANDED

The node is in the middle of moving its consensus rules from inherited C++ into a Rust validator with its own storage. The main risk in a move like this is not a crash. It is a quiet disagreement: the new code accepts a block the old code rejects, or ends up with slightly different state. You find that by running both over the same chains and comparing results, not by reading the code.

A replay tool built this window does exactly that. It fetches a chain from a running node, feeds it through the Rust validator and store, and at the end compares a digest of the resulting state with one computed from the old node's database. On chains of 301 and 2,301 blocks from a local test network the digests matched, including across the boundary where the proof-of-work key changes. A fixture with a chain reorganisation matched after the switch. The comparison was also fed a deliberately wrong checkpoint to confirm it fails when it should.

A second piece tests the other direction. It takes a valid chain and breaks one block in one specific way: a bad version, a missing parent, a timestamp too far in the future, a proof-of-work that only passes under the wrong key, and so on, nine kinds in all. The expected result for each is written from the specification, not copied from how the C++ behaves. For every rule the Rust validator already implements, the broken block is refused by exactly that rule at exactly that height, and everything before it is accepted.

To be plain about scope: the daemon does not yet run the Rust validator. The rules are being moved over in slices (IN FLIGHT), and a replay so far exercises only the ones that have arrived. A replay of a chain from the public test network has not been possible yet, because that network was started from an earlier genesis block that current builds no longer share. The tool is in place so that each new slice is checked against real chains as it lands.


This post describes the state of the dev branch at 1b4a58046cde on 23 September 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 #808, PR #809, PR #821, PR #823, PR #832, PR #812, PR #829, PR #825, PR #826, PR #837, PR #804, PR #811, PR #822

Sign in to reply and vote.