Imported from kamilsa/blobsim (
AGENTS.md). Install upstream withnpx skills add kamilsa/blobsim. Copyright stays with the author.
Agent Notes
Source Of Truth
- Trust
src/main.rs,src/network.rs,src/el_net.rs, andsrc/state_machine.rsover README-style docs when they disagree. README.md,GEMINI.md, and.github/copilot-instructions.mdcontain stale protocol/CLI references such as--persona,/cl/bids/1,/el/blob_hash/1, and/sim/devp2p/1; the current CLI uses repeatable--role, and EL traffic is raw TCP inel_net.rs.
Commands
- Fast verification:
cargo check. - Formatting/linting:
cargo fmt --checkandcargo clippy -- -D warnings. - Shadow binary:
cargo build --releasewritestarget/release/blob-sim. - There are a few
#[test]s insrc/types.rs(cargo test); usecargo checkplus a local smoke run for behavior changes. - Minimal CL+EL smoke run uses three processes — blobs originate at a blob-spammer, so a proposer/builder without an EL blob source produces blobless proposals. The block-producing node is a combined
--role proposer --role builder(it proposes at t=0 committing to its pooled blobs). Use--slots 3+: slot 0 can start before peers connect, and the first proposal drains an empty pool:cargo run -- --role blob-spammer --el-port 9200 --seed 7 --node-id 1 --slots 3 cargo run -- --role proposer --role builder --port 9000 --el-port 9100 --seed 1 --slots 3 --el-peer 127.0.0.1:9200 cargo run -- --role validator --port 9001 --el-port 9101 --seed 2 --slots 3 --peer /ip4/127.0.0.1/udp/9000/quic-v1 --el-peer 127.0.0.1:9200 bash run_network.shbuilds and launches a local CL network, but its defaults start 102 nodes and clearlogs/; pass small counts and a custom--log-dirwhen experimenting.run_network.shcurrently wires only CL--peerflags, not EL--el-port/--el-peer, so it does not exercise EL request/response traffic without changes.
Architecture Boundaries
main.rsowns clap parsing, tracing setup, CL swarm creation, EL actor spawn, metrics creation, andrun_node(...)orchestration.network.rsis consensus-layer only: libp2p QUIC + gossipsub, with topics/cl/beacon_block/1,/cl/payload_envelope/1, and/cl/blob_sidecar/1. With--enable-partial-columnsit also joins per-subnet/cl/data_column_sidecar/{subnet}/1topics viasubscribe_partial(gossipsub 1.3 partial messages).partial.rsimplements the gossipsub 1.3 partial-message protocol for data column sidecars: thePartial/Metadatatrait impls (OutgoingPartialColumn,MaybeKnownMetadata), the per-block header tracker, and the cell assembler.libp2pis pinned inCargo.tomlto theblobsim-patchesbranch of a rust-libp2p fork (upstream master rev891bf049+ three gossipsub patches), because thepartial-messagesfeature (gossipsub 0.50 / umbrella 0.57) is not yet on crates.io and the partial exchange needs local fixes: a longer partial-state TTL, no "stale data" skip on republish, and a byte count returned frompublish_partial. See the comment above[dependencies]inCargo.toml; to change a patch, rebase that branch and bump therev.el_net.rsis execution-layer only: a Tokio TCP actor using[u32 big-endian length | msg_id byte | RLP body]frames, no libp2p, no discovery, no RLPx. The actor never awaits socket writes itself — each connection has its own reader and writer task (a slow peer must not block the actor or other peers).- Requires a patched Shadow for EL frames ≥ 64 KiB (the 128 KiB
FullPayloadResponse). Stock Shadow'stcp_sendUserDatacaps every send at 65535 bytes even when the send buffer has space (src/main/host/descriptor/tcp.c,MIN(nBytes, 65535)), so a partial write happens on a non-full buffer; edge-triggered epoll users (tokio/mio) treat the short write as "wait for EPOLLOUT", no writability edge ever fires, and the connection deadlocks — blocking sockets are unaffected. The fix (remove the cap:remaining = MIN(nBytes, space)) is committed in the shadow-arm fork (~/dev/shadow, commitae04b0890) and published askamilsa/shadow-arm:tcpfix(also:latest) on Docker Hub; Shadow's own tcp/epoll/send_recv test suite passes with it. Worth upstreaming to shadow/shadow. Do NOT reintroduce app-level chunking to work around this — it distorts the simulated wire behavior. state_machine.rsowns all 12-second slot timing and role behavior; do not move slot logic intonetwork.rsor swarm construction intostate_machine.rs.types.rsdefines roles and wire messages: CL gossip is JSON viaGossipMessage; EL messages are RLP viaElMessage::encode/decode.metrics.rsemitstarget: "metrics"METRIC/SUMMARYper-slot counter lines and per-messagetrafficevents;events.rsemits the structuredtarget: "event"EVENT …stream (seemetrics.md). The analysis pipeline isnotebooks/loaders.py(parses the logs into DataFrames) →notebooks/analysis.ipynb(§1–§5 Plotly) →scripts/render_notebooks.py(papermill + nbconvert →site/rendered/) → the Astro observatory (site/, served byuv run shadow-sim.py --serve). If you add or rename a log field, updatemetrics.md, the_*_COLScontracts inloaders.py, and the consuming notebook cell; never reuse the reservedEVENTkeyskind/t_ms/slotas field names.
Repo-Specific Constraints
- CLI roles are repeatable:
--role proposer|builder|validator|blob-spammer. In the current model a proposer is also a builder (there is no bid). - Blob pipeline: blobs originate at blob-spammers over EL networking and propagate via the sparse blobpool. For each announced blob, non-builder CL peers independently choose sampler behavior (85%, pull stable custody-set cells plus one random extra) or provider behavior (15%, pull the full payload). Builders never generate blob data — they always behave as providers on EL and pool the full blobs they receive (
ElBlobPool, keyed by announced hash). At slot start a builder takes up toMAX_BLOBS_PER_BLOCKnot-yet-included blobs from its pool for the block (overflow stays pooled for a later slot). The proposal (SignedBeaconBlock) published at t=0 carriesblob_kzg_commitmentsthat embed those announced hashes (commitment_for_blob_hash), so it names exactly the EL blobs the block includes. A validator that sees the proposal matches the commitments' hashes against its own EL pool (localgetBlobs) and starts propagating custody columns, then evicts those included blobs from its pool. Inclusion tracking (ElBlobPool.included, aINCLUDED_WINDOW_SLOTS-slot window) prevents a blob from being re-pooled or re-included across slots. The t=4-6 payload-reveal envelope carries no commitments (they were already in the proposal) but does carry a configurable-size execution-block body (SignedExecutionPayloadEnvelope.payload, sized by[sim].exec_payload_size_kib→--exec-payload-size, default 128 KiB); a validator that missed the proposal instead triggers off a received partial column's header. (PTC has been removed for now.) --enable-partial-columnsswitches CL blob propagation to data column sidecars over gossipsub 1.3 partial messages (cell-level deltas); the baseline/cl/blob_sidecar/1full path is used otherwise.engine_getBlobsis modeled as a local read of the node's EL blob pool (full blobs the EL previously received over EL networking via announce → full-payload pulls) — never a network request from the CL side.--disable-get-blobs(only meaningful with partials) makes nodes ignore that pool and pull all custody cells from peers over CL.- Preserve deterministic simulation behavior: derive keypairs and random choices from the
--seedpath andStdRng::seed_from_u64; do not usethread_rng()or OS entropy. - Preserve Shadow-compatible timing: phase deadlines use
tokio::time::Instant/sleep_until; do not add wall-clock reads such asSystemTime::now()orUtc::now()to simulation logic. - Cryptographic fields are dummy bytes by design. Keep BLS/KZG-sized fields as
Vec<u8>where needed because serde only handles fixed arrays up to 32 bytes by default. Cargo.tomlpatchesquinn-udptopatch/quinn-udp, a local fallback UDP implementation; keep this in mind when touchinglibp2p/QUIC dependency versions.