Repository navigation
Releases: Azizzzss/cupel
Release list
v0.8.0
Not phase F. The oracle waits while what exists is made to earn its place: a
chain with traffic on it, the pool it waits in, two execution clients as well
as three consensus clients, a 3D scene that keeps time and names who is who,
pictures on the front page, and walkthroughs that CI now runs. Bringing Reth up
turned up the release's most consequential fix — the devnet's execution clients
had never peered.
Added
-
Reth on the devnet. Node 3 runs Reth under Teku instead of a third geth,
so client diversity now holds on both layers: a block Reth builds for Teku's
proposer has to be executed and accepted by the two geth nodes before
Lighthouse and Prysm attest to it, and the other way round. Verified on a
fresh devnet — blocks built by each accepted by the other, one head hash on
all three. A swap rather than a fourth node, because the 22/21/21 validator
split and the finality arithmetic the walkthroughs teach depend on there
being three.The flags follow geth's for the same reasons; the differences are written
beside them innetwork.yml(no init step, no tip floor to disable, archive
unless told otherwise, metrics at the root path, a health check over bash's
/dev/tcpbecause the image has no curl). The banner,network status,
walkthrough 4, the agreement table and the 3D scene's labels all name both
clients on each node, and tests check that the compose file,NODESand the
control room's copy agree. -
cupel traffic: a chain with something happening on it. Every picture of
an idle chain is the same flat line, so this sends what a used one carries —
ether payments, token mints and transfers, vault deposits and redemptions,
wrapping and unwrapping ether — from development accounts 0, 1 and 3 at once,
in bursts of none to five every second or two. Blocks stand tall or stay
empty, and gas varies because the work does. Account 2 never sends: it is the
signer's key, and its audit log stays about the signer.Now and then it does, on purpose, the two things people are most surprised
by: a transaction that reverts, which is still mined and still pays for its
gas, and one sent with a nonce too far ahead, which is accepted, parks in
queuedand runs only once the gap in front of it is filled. Each is
explained the first time it happens.--cleanturns both off, and a clean run
exits non-zero if anything reverted or was accepted and never included — the
second being how geth reports a tip below its floor, which is not at all. At
the end it asks for every receipt and says what became of each transaction.Everything is signed locally and sent raw, so it needs no Foundry, and it
works against either chain:--rpc http://127.0.0.1:8555sends to a devnet
node directly. It replaces the throwaway script that fed the 3D page. -
The 3D scene names its clients, and keeps time. In network mode each
marker carries its client's name —node1 Lighthouse— on a row of its own
with a line down to the marker, so the scene reads without the legend under
it. The labels are HTML moved to each marker's place on screen every frame,
so they stay crisp and follow the theme; rows are handed out from the right
so no line ever crosses a label.Blocks now sit at their place in time rather than one after another: one
box-width is the slot time on the devnet and the block time in the lab, a
slot nobody proposed in leaves a gap the width of a block, and a line across
the floor marks the moment each epoch began. The legend says how much time
the window covers — fifty seconds in the lab, ten minutes on the devnet —
which is the difference the two lanes used to hide. Absolute seconds were
tried on paper and rejected: at a fixed scale the devnet lane is twelve times
longer and every box a pixel wide. -
The pool, as a page. Every other page shows transactions that ran; this
one shows where they wait first.txpool_contentthrough the gateway, read
four times a second in lab mode: pending (ready for the next block),
queued (accepted and parked — with the nonce each one is waiting for,
worked out from the sender's pending run or its account nonce), and just
left, with how long each stayed. That last is shown as the range the
readings allow rather than a single number: two readings a few milliseconds
apart, one either side of a block, once timed everything at 0.0s. In network
mode it also shows each node's own counts, because there is no network-wide
pool — only each node's view of one. Calls are named (Token.transfer,
Vault.deposit) on this page and the transaction page.The selectors behind those names, and the ones the page already used to read
the contracts, are now checked against the bytecode in the committed genesis
file. A comment had claimed that check existed; nothing did it. -
The README shows the thing. A moving picture of the 3D page under
cupel trafficat the top, and the overview, a block and the walkthrough in
the control-room section, each in the reader's own theme. An audit flagged a
project about making machinery visible with not one picture of it on the
front page. CI now fails if the README names an image that is not in the
repository — a broken screenshot fails nothing on its own.docs/shoot.mjs
is how they were taken: a headless Chrome or Edge over the DevTools protocol,
nothing to install, with the commands for each image written at the top. -
The chain as a solid object. A page that draws the block window in three
dimensions: one box per block, running away from the camera in the order they
were made, as tall as the gas each used, orange where a block carried
transactions and grey where it was empty. Point at a block for its number,
transactions and gas; click it to open the block page. In network mode a
marker floats above the block each client calls the head, and a client whose
head is outside the window is drawn at the end of the lane and named as ahead
or behind rather than placed on a block the page never fetched.Nothing in the scene is ornament — the length is the window, the heights are
gas, the colour is whether anything was in the block. The height scale climbs
in round steps and the legend prints the number it has reached: scaled
straight to the busiest block in view, every box grows the moment that block
falls out of a window that moves every second, and a block that has not
changed appears to have.three.js is larger than the rest of the interface put together, so the page is
loaded on its own and only a reader who opens it pays to parse it. The binary
carries it either way.
Fixed
-
Walkthrough 4 mistook "nothing finalised yet" for a dead node. Asked for
the finalised block before there is one, geth answers with an error
(finalized block not found) and Reth answers with the genesis block. The
walkthrough treated geth's error as silence — with three geth nodes it said
no execution client was answering at all, while all three were. It now tells
a transport failure from an answer that happens to be an error, prints "none
yet" and "genesis" for the two spellings, and says why they differ. -
The walkthroughs are tested. They are what a newcomer meets first, and
nothing ran them. CI now runs 1 and 2 against the lab chain and 3 and 4
against the devnet after finality, and checks each for the conclusion it
exists to reach — "Four calls, one block", "they agree", "one block, from two
implementations" — since a walkthrough printing the wrong verdict still exits
0. It also checks that a devnet walkthrough run against the lab refuses and
names the command that would start the devnet. -
The devnet's execution clients never found each other. Given no
--bootnodes,devp2p discv4 listenbootstraps from mainnet's, so the
devnet's bootnode joined the public discovery network, filled its table with
mainnet nodes, and answered every devnet node's lookup with them —
--netrestrictdiscarded them all, and geth loggedLooking for peers tried=0for ever. A crawl of the bootnode found 136 nodes, one of them on
the devnet. Blocks travel over the consensus network, so nothing visibly
broke; what broke was transaction gossip — a transaction sent to node1 waited
for one of node1's own validators to propose. The bootnode is now given an
explicitly empty list, the mesh forms in seconds,network statusshows
execution peers beside consensus peers and says what an isolated execution
client means, and CI requires every node to see the other two. -
The 3D page drew sixty frames a second for nobody. The browser stops a
background tab by itself, but not a canvas scrolled out of view while the
legend under it is being read — that kept drawing at full rate, which on a
laptop is a fan and a battery. The scene now stops outright when the tab is
hidden or the canvas is off screen (measured in headless Edge: 0 animation
frames in two seconds, against 140–290 in view), and resumes when it comes
back. With reduced motion asked for it draws only when something changes, and
a new block appears rather than gliding in — the glide takes about as long as
a lab block, so it would have kept the scene drawing anyway. The client
markers also stopped spinning under reduced motion; they used to ignore it. -
The control room fetched its own typefaces over the internet. The page
linked fonts.googleapis.com for IBM Plex and Fraunces. It is compiled into a
binary and served from localhost, and this project exists to run a chain on a
laptop with no network — so offline, which is most of the point, every heading
fell back to Georgia and every table to the system sans, with nothing to say
why. Six woff2 files travel with it now, latin only, and a test asserts the
page names no font host and that each file is served asfont/woff2. -
**T...
v0.7.0
Added
-
A control room.
cupel upandcupel network upnow print a second
address alongside the RPC: a page showing the head, the last twelve blocks as
they arrive, what the gateway's upstreams are doing, and — in network mode —
what each of the three consensus clients calls the head, the justified epoch
and the finalised one.The agreement panel reports four outcomes rather than two, because a client
that is unreachable, one on a chain that has not finalised yet, one that is
merely behind by some epochs, and one that genuinely disagrees at the same
epoch are different situations and only the last is alarming.It also produces blocks. The button runs the same four Engine API calls the
producer runs once a second, with a copy kept of every request and response,
and shows them in order with what each is for — keyed by method and
occurrence, sinceforkchoiceUpdatedappears twice and the two calls do
different jobs.Everything else on the page is read straight from the clients by the browser.
Producing a block is the exception: it needs an authenticated call to a port
bound to localhost, so there is one endpoint,POST /api/produce, holding the
head under a lock for the whole sequence — each call names the parent, so two
producers sharing a head would have the second build on a block the first had
already replaced.The page is compiled into the binary with
rust-embed, so a release carries
its own front end and running Cupel never needs a JavaScript toolchain. -
The control room, second pass. A strip across the top with the vitals —
mode, chain id, the head as it ticks, the clock in network mode, the gateway,
the binary's version, and how old the oldest answer on the page is — and
pages down the side: Overview, Blocks, Accounts, Gateway, Consensus in
network mode, the walkthrough in lab mode. Routes live in the hash, because
the bundle refers to its assets relatively so the binary can mount it
anywhere, and a path with a second segment would resolve them under it. A
page the running mode does not have says so; a hash nothing lives at gets a
page too; a page that throws is the only thing that goes.Every block number opens the block — the header as the client sent it, the
transactions in full — and every transaction opens what was sent, what it
cost and what happened, with the genesis contracts' events decoded by name
and everything else shown as the topics and data it is. Bounded on purpose:
fifty blocks, fifty more on request, five hundred at most. The recent
window, not an explorer; the explorer is phase G. The accounts page shows
the four development accounts with live balances and nonces, their keys
behind a click, and the three contracts with names, symbols and supplies
read by hand-rolledeth_call. The addresses are mirrored from the Rust
side and a Rust test reads the TypeScript to check the two agree.Nothing pretends to be current. Every source remembers when it last
succeeded; a panel keeps its numbers across a failed request and, three
intervals later, greys them and says when they were last true. Stopping geth
no longer leaves a page that looks like a chain making blocks, and a node
whose last answer is too old counts as not answering in the verdict — never
as a silent vote for whichever root it last reported.Push instead of poll. The page subscribes to
newHeadsover geth's
WebSocket and to each beacon node's event stream, and fetches the moment a
block is announced — over HTTP, through the gateway in lab mode, so the
socket is a doorbell and not a data path. Polling stretches while a socket
is open and never stops, so a dead-but-open socket cannot freeze the page.
Each panel says "live" or "polling"; a tab brought back from the background
asks everything at once.Tests, for the first time: Vitest over the arithmetic — verdicts, slot
maths, ABI and log decoding, block and transaction parsing, formatting,
routes, freshness, the two wire formats — run in CI beside the lint and type
checks. A theme switch that remembers itself, and a tab title that mirrors
the head.
Changed
GET /api/modecarries the binary's version beside the mode, and the strip
shows it: the page is compiled into the binary, so it is exactly as old as
the process serving it.- Network-mode geth publishes a WebSocket per node, on 8558–8560, for the
control room'snewHeadssubscription; lab mode's has been on 8547 all
along. A test reads the compose file to check the flags and the ports are
there. The three execution containers are recreated on the nextup; their
chains persist.
Fixed
-
No browser could call the gateway. A JSON-RPC request carries
content-type: application/json, which is never a simple request, so a
browser asks permission first — and the route accepted only POST, so the
preflight got405 Method Not Allowedand the real call was never sent. Every
browser client had been unable to reach the gateway since it was written,
whilecastandforgeworked perfectly, because neither of them asks
permission. The README had been recommendingviemagainst it the whole time.Separately, the
Access-Control-Allow-Originheader was set by the JSON-RPC
handler alone, so/healthand/metricsreturned correct bodies that
browsers dropped unread. It is a response layer now — one place, every route,
nothing for a third handler to forget. The test for it needed the router
extracted first: the test module had been building its own copy of the routes,
which is exactly how a layer goes missing from the real one while the suite
stays green. -
Teku sent no CORS headers either.
--rest-api-host-allowlistis a Host
header allowlist and not the same idea; without--rest-api-cors-originsTeku
answerscurlperfectly and a page throws the answer away. Three clients,
three spellings of one concept: Lighthouse has--http-allow-origin, Prysm
--http-cors-domain, Teku this. -
The committed bundle was never committed.
ui/distis compiled into the
binary, and a baredist/in the root ignore file — plus Vite's scaffolded
ui/.gitignore, either alone sufficient — kept it out of the repository while
a comment directly above the rule explained why it was in. Nothing failed
locally, because the working tree had the files; the first machine to find out
would have been CI, on all four jobs at once, with#[derive(RustEmbed)] folder '.../ui/dist' does not exist. There is auijob now that lints, typechecks,
rebuilds the bundle from source and fails if it differs from what is
committed — the same check genesis and the diagrams already get. -
The walkthrough about slots stated the slot time from memory. Walkthrough
3 reads the head slot, the epoch and the proposer duties off the running
chain, and then printed "6 seconds, so an epoch is 3 minutes 12" from a string
in the source — wrong on both counts, in the one lesson whose entire subject
is how a chain divides time. It now asks the chain forSECONDS_PER_SLOTand
SLOTS_PER_EPOCHand derives every duration from them, so the lesson cannot
disagree with the thing it is describing. -
The devnet's slot time was never six seconds.
SECONDS_PER_SLOT=6was
passed to the genesis generator from the first day of network mode and the
generator does not template that value, so the key was simply absent from the
config it produced and all three clients used the mainnet preset's twelve. The
chain was correct throughout. Nothing errored. The only symptom was that
finality arrived twice as late as the banner, the design document and CI's
arithmetic all said it would — and CI's finality window was set from the wrong
number, so the job failed with "no finalised epoch" on a chain that was going
to finalise nine minutes later.Writing the key in by hand is not a fix: Lighthouse validates the config
against the preset compiled into it and refuses to start with YAML
configuration incompatible with spec constants for mainnet. Six-second slots
need minimal-preset binaries. So the number is twelve everywhere now,init
checks the generated config against what the code assumes rather than trusting
it, and CI asks the running chain what it is using and fails if it disagrees —
because the failure being guarded against was never a wrong value, it was a
value nobody wrote and nobody missed. -
The banner printed
http://127.0.0.1:8545whatever--bindwas given. That
is true for0.0.0.0, where localhost still reaches the socket, and a lie for
any other address. -
Metrics never worked on plain Linux Docker. Prometheus scraped the clients
throughhost.docker.internal, which gives a container a route to the host
and nothing more: the ports are published on127.0.0.1, so the scrape
arrived on the bridge address where nothing was listening. Every target was
refused and every panel drew an empty box — indistinguishable, on a lab chain,
from a quiet one. Docker Desktop's port proxy made it work anyway, which is
why it went unnoticed for three phases.The clients are now scraped by container name over the network Cupel already
created, which is both correct and one hop shorter;cupel observeattaches
Prometheus to whichever of the two networks exists, because compose treats an
external network it cannot find as an error and which one exists depends on
the mode. The gateway is the one target that genuinely runs on the host, and
cupel up --bind 0.0.0.0now makes it reachable. The signer is deliberately
not covered by that flag. -
The dashboard check waited for the wrong thing.
upappears as soon as
Prometheus has attempted a scrape and is0when the target refused, so
waiting for the series to exist was waiting for the first failure. Every panel
was then em...
v0.6.0
Five phases built the apparatus. This is the first release aimed at somebody
using it rather than at the thing working.
Added
-
Guided walkthroughs —
cupel lab. Numbered lessons that do real work
against a running chain and narrate it: the four Engine API calls that make a
block, printed request and response; the three states a transaction passes
through and which question distinguishes them; slots, epochs and what has to
happen twice before a block is final; and the same question asked of three
clients at once. Nothing is simulated and nothing is pre-recorded — with no
chain up, a walkthrough fails, which is the honest outcome.The prose lives in
crates/cupel/src/lab.rsbeside the code that produces the
output, because a lesson kept in a separate document drifts from the thing it
describes and there is no way to notice. -
The producer can narrate itself.
Config::record_exchangesmakes
produce_blockkeep a copy of each Engine API call inProduced::exchanges,
so walkthrough 1 shows the real sequence rather than a reimplementation of it
standing next to a lesson about it. Off by default: producing a block is the
hot path and a payload can be large, so the copies are made only when
something intends to read them. -
Release binaries. A tag builds
cupelfor Linux, macOS on both
architectures, and Windows, smoke-tests each one, and attaches them to the
release with checksums and notes lifted from this file. Docker is needed
either way, but asking somebody to install Rust before they can look at a
block is a tax with nothing behind it. -
The architecture diagrams live in the repository, one file per theme so
GitHub picks the right one, generated bydocs/make-diagrams.pyand checked
in CI against it — a diagram edited by hand and not regenerated is a picture
of a system that no longer exists. -
A third dashboard, Cupel — execution. Prometheus had been scraping geth
since phase C and almost nothing read the result: two metrics out of the 899
the node exposes. This is the node's own view — head with the safe and
finalised markers trailing it, the transaction pool, Engine API latency and
call rate, JSON-RPC, peers, state cache, disk — in both modes, told apart by a
nodelabel rather than by a separate dashboard.The panel that earns it is Transactions geth refused. A transaction paying
under--miner.gaspriceis accepted into the pool, returns a hash, and is then
skipped by the payload builder for ever; nothing errors and nothing logs, so
the only symptom is a transaction pending while empty blocks keep coming.
txpool_underpricedis where that says so, and now it is on a screen. -
Metrics in lab mode.
compose/lab.ymlhad no--metricsflags at all, so
the default one-command mode exposed nothing and thecupel-executionscrape
job existed only for the devnet. geth now serves metrics on6060there,
scraped as a fourth target in the same job, labelledlab. Port6060rather
than6061because the first devnet node holds6061, and the README promises
both modes can run at once — a clash would have surfaced as a Docker bind
error saying nothing about Cupel. -
CI checks every dashboard panel against a live Prometheus.
check-dashboards.shpulls every
exprout of every dashboard, asks a real Prometheus scraping a real chain,
and fails on any that returns no series. Both the lab and network jobs run it,
in their own mode, with an explicit skip-list for panels whose emptiness is a
property of the mode rather than a fault.This exists because a dashboard cannot fail loudly. Rename a metric upstream
and the panel does not error — it draws an empty box, which on a lab chain
looks exactly like a quiet one. Both well-known community geth dashboards died
this way and still render: the EF devops fork is InfluxDB-only and last revised
in 2021, sochain_execution,chain_validation,chain_writeand
trie_memcache_*are all gone from geth 1.17; the other needs a JSON-RPC
exporter last touched in 2019, before the merge. Neither was imported, and this
check is what stops the same rot starting here.
Changed
- Reframed around the machinery rather than its failure modes. The project
is for showing how Ethereum works — the Engine API handshake that produces a
block, three clients arriving at the same finalised chain, a network forming
from a single bootnode — and the writing now leads with that. The Solidity
tests are unchanged in what they run and renamed for what they demonstrate:
test_allowance_isReplacedNotAdjustedrather thantest_approveRace_…,
test_shares_anInflatedPriceRoundsTheNextDepositDownrather than
test_inflationAttack_…. An allowance being a standing permission and a share
price being a ratio with a remainder are facts about the standards; they read
better as such.
Phase F: a Chainlink oracle.