Skip to content

Releases: Azizzzss/cupel

v0.8.0

Choose a tag to compare

@github-actions github-actions released this 25 Sep 02:24

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 in network.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/tcp because 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, NODES and 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
    queued and runs only once the gap in front of it is filled. Each is
    explained the first time it happens. --clean turns 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:8555 sends 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_content through 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 traffic at 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 listen bootstraps 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 —
    --netrestrict discarded them all, and geth logged Looking for peers tried=0 for 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 status shows
    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 as font/woff2.

  • **T...

Read more

v0.7.0

Choose a tag to compare

@github-actions github-actions released this 16 Sep 10:15

Added

  • A control room. cupel up and cupel network up now 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, since forkchoiceUpdated appears 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-rolled eth_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 newHeads over 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/mode carries 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's newHeads subscription; 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 next up; 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 got 405 Method Not Allowed and the real call was never sent. Every
    browser client had been unable to reach the gateway since it was written,
    while cast and forge worked perfectly, because neither of them asks
    permission. The README had been recommending viem against it the whole time.

    Separately, the Access-Control-Allow-Origin header was set by the JSON-RPC
    handler alone, so /health and /metrics returned 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-allowlist is a Host
    header allowlist and not the same idea; without --rest-api-cors-origins Teku
    answers curl perfectly 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/dist is compiled into the
    binary, and a bare dist/ 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 a ui job 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 for SECONDS_PER_SLOT and
    SLOTS_PER_EPOCH and 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=6 was
    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:8545 whatever --bind was given. That
    is true for 0.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
    through host.docker.internal, which gives a container a route to the host
    and nothing more: the ports are published on 127.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 observe attaches
    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.0 now makes it reachable. The signer is deliberately
    not covered by that flag.

  • The dashboard check waited for the wrong thing. up appears as soon as
    Prometheus has attempted a scrape and is 0 when the target refused, so
    waiting for the series to exist was waiting for the first failure. Every panel
    was then em...

Read more

v0.6.0

Choose a tag to compare

@github-actions github-actions released this 06 Sep 17:47

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.rs beside 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_exchanges makes
    produce_block keep a copy of each Engine API call in Produced::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 cupel for 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 by docs/make-diagrams.py and 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
    node label rather than by a separate dashboard.

    The panel that earns it is Transactions geth refused. A transaction paying
    under --miner.gasprice is 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_underpriced is where that says so, and now it is on a screen.

  • Metrics in lab mode. compose/lab.yml had no --metrics flags at all, so
    the default one-command mode exposed nothing and the cupel-execution scrape
    job existed only for the devnet. geth now serves metrics on 6060 there,
    scraped as a fourth target in the same job, labelled lab. Port 6060 rather
    than 6061 because the first devnet node holds 6061, 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.sh pulls every
    expr out 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, so chain_execution, chain_validation, chain_write and
    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_isReplacedNotAdjusted rather than test_approveRace_…,
    test_shares_anInflatedPriceRoundsTheNextDepositDown rather 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.