Independent Linux command. Canonical git remote:
https://github.com/CoNET-project/CoNET-L0D.
Do not ../.. import BeamioContract, SilentPassUI, CoNET-SI, or x402sdk.
web3:// is the CoNET Web3 Application Protocol. It is a wallet-addressed
application locator and signed wire contract carried by Layer Minus (L0).
conet-l0d is the Linux server/client runtime for that protocol.
| Platform | Implementation |
|---|---|
| Linux server | --proxy for request/response; --proxyDuplex for continuous streams |
| Linux client | --clientDuplex web3://HOST:PORT with one 127.0.0.1 listener per logical port |
| Browser / Windows / macOS / phone | Client-side web3:// library over HTTPS/SSE, OpenPGP, and Web Crypto |
The daemon does not patch geth, Prysm beacon-chain, or validator. It must
never read validator keystores or restart chain infrastructure.
web3:// is an application locator, not ERC-4804 content:
web3://0x<40-hex>/service/path
web3://ExactBeamioTag.web3/service/path
BeamioTag matching is exact (CoNET ≠ CONET). Never select
search-users.results[0] without an exact identity match. The destination must
have a usable AddressPGP identity.
Keep these roles separate:
- application destination EOA;
- communication route identity;
- main paid wallet (
billing_eoa); - validator deposit keystore;
- fee recipient.
Each [[l0.proxies]] or --proxy entry maps a logical application port to a
request/response upstream host:port.
Each [[l0.proxy_duplex]] or --proxyDuplex entry maps a logical application
port to a persistent bidirectional upstream. A duplex line is allocated only
after an explicit signed new-line request addressed to mainWallet:port.
Every line has its own:
- temporary wallet and OpenPGP route identity;
- AES key;
- opaque
pipe_handle; - occupied socket;
- bounded byte queues.
Temporary identities are memory-only and are not registered in AddressPGP.
The client announces the mailbox SI (route PGP + node wallet) that accepted
its l0_listen SSE inside duplex_offer. The server's accept packet carries
the mailbox SI of its own temporary SSE. Entry SI posts wrap the peer's
user-PGP ciphertext with that mailbox SI PGP so forwarding does not need
searchKey. Destroyed on EOF, failed HTTP status, timeout, or socket close.
Lines on the same logical port must not share wallets, keys, handles, queues,
or sockets.
The occupied line is a byte transport. Forward bytes unchanged to the configured upstream with bounded asynchronous bidirectional copying. A failed line is isolated from all other sessions.
Multi-port publishing (for example :8400 plus :4200) must use one
[[l0.channels]] routing EOA per port because SI exclusive occupy is one line
per listen wallet. Collapsing ports onto one identity causes sustained
l0_connect HTTP 409 responses.
Inbound duplex_offer matching uses billing_eoa plus logical port as a
routing lookup. It does not authorize allocation. Attach only to a known,
pre-registered pipe_handle or temporary listen wallet. Reject unknown, stale,
or ambiguous offers without creating a session.
A host that is also a client of another wallet's same logical port keeps
those lines in different roles. Every duplex line already has an independent
pipe_handle / session map key (dest, port, session_id). Do not treat
session.port as a global "this daemon is proxying" switch.
maybe_start_proxy_drain may attach a local upstream TCP to --proxyDuplex
only when the session was allocated by the inbound proxy handshake
(mainWallet:port + firstChunk) and therefore carries
DuplexLineRole::Proxy. Sessions created by --clientDuplex /
l0.client_duplex are DuplexLineRole::Peer and must never dial local
geth/beacon merely because they reuse the same logical application port
(for example client → hub :4200 while this host also exposes
proxy_duplex for :4200).
A single daemon may run client_duplex and proxy_duplex together.
Local listen ports for clients must be free OS ports
(web3://<billing>:8400@18400, @14200, …). Offer matching still uses
billing_eoa as mainWallet, never the per-port channel routing_eoa.
--clientDuplex web3://HOST:PORT (or [l0].client_duplex)
exposes a remote application through a local TCP listener on 127.0.0.1.
The same logical PORT may map to several remotes. Repeat the flag for more
lines (:8400 to hub A and :8400 to hub B, or :8400 and :4200). The
same (host, PORT) listed twice is rejected. Each remote gets its own
127.0.0.1 listener. The preferred loopback bind is PORT. If that port is
occupied, the runtime walks PORT+10000, PORT+20000, … An optional
Linux-only @LOCAL suffix pins the loopback bind and does not walk.
Each TcpListener.accept() event is the sole connection handle. A newly
accepted socket receives a fresh temporary wallet, OpenPGP identity, AES key,
opaque pipe_handle, return queue, and occupied line. Open l0_listen on the
configured mailbox SI without AddressPGP registration; include the temporary
user-PGP key ID so mailbox can index the local SSE pool.
All bytes from that socket stay attached to the same handle until EOF/error.
Return bytes may be written only to that socket. Concurrent sockets on one
local port therefore remain independent. Do not prefix application bytes with
a private header; the accepted socket and encrypted pipe_handle provide
correlation.
The client pauses a new local socket after its first bytes, creates exactly
one temporary identity, waits for its temporary l0_listen SSE handshake
(mailbox SI wallet), then sends those bytes as firstChunk in duplex_offer
together with the mailbox SI PGP/wallet. An existing handle must never allocate
a second line.
A duplex proxy may open a line only when the recovered signer, configured
billingWallet, mainWallet:port, explicit --proxyDuplex target, and
non-empty firstChunk all match.
The proxy:
- creates and registers its own temporary route;
- waits for that route's
l0_listenSSE; - connects the configured upstream and forwards
firstChunk; - pauses after the first upstream reply;
- returns that reply as
responseChunkinduplex_accept(posted once); - reverse-occupies the initiator route if its return line is still empty; and
- starts upstream-to-pipe forwarding.
If any of those temporary-line steps fail, the proxy does not retry the
same wallet/listen/accept. It posts one duplex_reject (retryable=true)
as mailbox work onto the initiator request listen (listenUserPgp /
listenRoutePgp). The client L0d closes that local TCP; only a new
application connect may mint another temporary line.
Do not wait for a second local protocol chunk before establishing both
directions. The client decrypts the accept with its per-socket temporary PGP
key, writes responseChunk, then occupies immediately. An empty first AES blob
is valid. Reject unrelated, unsigned, stale, or ambiguous offers.
HTTP /post has exactly one top-level field:
{"data":"<OpenPGP armor>"}Do not add cleartext routing fields or hop-signature headers.
Use the deployed SI commands l0_listen and l0_connect. Application
duplex_offer, duplex_accept, duplex_reject, and encrypted stream frames
are peer application messages, not SI commands. Do not invent duplex_*,
p2p_stream_*, or listenKind: "l1p2p" SI commands.
The first duplex_offer is encrypted to the receiver's registered user PGP
and wrapped to that receiver's registered mailbox B so Entry can deliver
it. The offer JSON must announce the initiator's temporary listen mailbox
(listenRoutePgp / listenMailboxWallet / listenUserPgp / listenWallet):
the exclusive SSE that will receive duplex_accept. That mailbox B is not
the POST entry. Entry A/C is always a random qualified SI, distinct from B.
Encrypt duplex_accept to the initiator listenUserPgp and wrap it as
mailbox work { "data": innerArmor } (no NoPush) to the announced
initiator listenRoutePgp. Mailbox B matches the inner user-PGP key ID
against idle l0_listen (userPgpKeyId / l0ListenByPgp) and copies the
armor onto that SSE. It must not getRoute a temporary user PGP.
The accept body carries the server's per-line temporary identity
(listenWallet, listenUserPgp, listenRoutePgp, listenMailboxWallet)
plus responseChunk. The server must not l0_connect the initiator mailbox
until that accept has been processed and the initiator has occupied the
server mailbox; occupying first converts the SSE to a pipe and drops the
accept. After acceptance, l0_connect occupies those announced temporary
mailboxes.
AES keys are memory-only and must never be exposed to a mailbox-decryptable command. Outbound application data must be either:
- encrypted on an accepted occupied line; or
- encrypted to the destination user PGP and wrapped to mailbox B's route PGP.
Never POST plaintext application data.
Each line incarnation uses duplex::new_pipe_handle(): a random 64-character
lowercase hexadecimal value not derived from wallet, port, IP, or route data.
l0_pipe_end is accepted only on the occupied TCP connection that owns the
same handle:
{"type":"l0_pipe_end","pipe_handle":"<64 lowercase hex>","reason":"transport_closed"}The object must not contain wallet, connector, sessionId, or
session_id. Reject missing, malformed, or mismatched handles. Do not parse
this object in the SSE ingest path and do not use an l0_listen_released SSE
notice. Hop-local failures must not expose cross-hop correlation or end-to-end
AES keys.
Occupy TCP EOF must clear the active sender and enter the same retry path as an explicit error. Install the sender only after the occupy response is HTTP 200 and remains open. If a healthy exclusive listen is rebuilt while the SSE is still live, rebuild outbound connects for attached sessions.
walletAddress is the temporary communication identity. billingWallet is
the configured paid account and EIP-191 signer. They must never be silently
conflated.
Every mailbox command keeps the communication identity in walletAddress and
the paid account in billingWallet. The deployed SI verifier recovers against
billingWallet while retaining walletAddress for routing and mailbox
ownership.
Peer application offers/accepts are also signed by the configured paid account. When the signer differs from the communication identity, bind both in the signed command:
{"walletAddress":"<temporary-channel-wallet>","billingWallet":"<main-paid-wallet>"}Each [[l0.channels]] entry owns exactly one port. Configure
[l0].billing_eoa and [l0].billing_eth_key_file for the main paid account.
conet-l0d gateway receives signed conet_web3_request_v1 envelopes through
mailbox SSE, validates them, forwards permitted GET/HEAD requests to a loopback
HTTP origin, and posts an encrypted conet_web3_response_v1 through an Entry
to the requester mailbox.
Gateway requirements:
- secrets are local files with restrictive permissions;
- never pass secrets in CLI arguments, environment variables, logs, or git;
- upstream is loopback by default;
- request/response bodies and timeouts are bounded;
- the inbound mailbox stream is receive-only; and
- the requester receives the response through its own mailbox.
The 2026-08 authorized lab proved Geth TCP, Prysm TCP, UDP echo, DHT-port
traffic, and live discv5 using local overlay endpoints such as 100.64.0.5
and 100.64.0.6. These are historical lab facts, not requirements of the
Web3 Application Protocol.
Do not claim L0 as the production slot-critical path until the GitBook publication gate compares it against public P2P with multi-Guardian and multi-mailbox diversity. Public L1 joining remains the production default.
Operational recovery belongs in the dedicated beacon recovery rule and
developers/l1-node.md; do not duplicate a second playbook here. Never restart
geth, beacon, validator, or wipe chain data without explicit same-message
authorization.
| Event | Required behavior |
|---|---|
start |
Validate configuration, open configured server/client endpoints, start mailbox tasks, then write process state |
SIGINT / SIGTERM / stop |
Stop accepting lines, close sessions and listeners, then remove process state |
teardown |
Remove stale daemon-owned runtime state after confirming no live process owns it |
gateway |
Own only mailbox tasks and the loopback origin adapter |
Cleanup must never delete another process's files or sockets.
The crate and public GitBook must tell one story:
web3://is the application protocol over L0;- Linux servers/clients may use
conet-l0d; - browsers and Windows/macOS/phone apps may implement the client protocol directly; and
- L1 consensus transport is under development, not the public join default.
When the whitepaper, this file, MVP, CLI, or public wire contract changes, update in the same task:
src/docs/gitbook/applications/web3-url.md— canonical product and platform page;src/docs/gitbook/l0/web3-application-protocol.md— canonical protocol;src/docs/gitbook/l0/mailbox-routing.md— mailbox work vsgetRoute;src/docs/gitbook/l0/duplex-forward.md— offer/accept/occupy order;src/docs/gitbook/l0/si-developer-guide.md— SI inbound catalog;src/docs/gitbook/developers/conet-l0d.md— Linux CLI/config reference;src/docs/gitbook/developers/l1-node.md— only if L1 status changes; andSUMMARY.md, section indexes,l0/using-l0.md, andresources.md.
Do not duplicate the same setup steps across Applications, Developers, and L0 protocol pages. Link to the canonical page instead.
If an SI command or /post contract changes, also update the L0 protocol and
SI developer pages required by the global DePIN GitBook sync rule.
Do not run src/docs/scripts/deployGitbook.sh unless the user requests a
deployment.
cargo test
cargo build --releaseKeep cargo fmt --check, cargo clippy --all-targets --all-features, and tests
green. Tests use local mocks/wiremock and must not contact production services.
With l0.si_pool_from_contract = true (the default), use GuardianNodesInfoV6 discovery rather than requiring static SI URLs. Pool acquisition qualifies TCP :80, randomizes candidates, and applies a failure cooldown for future connections. Exclusive/temp mailbox B is a random pool SI (on-chain route PGP); Entry A/C is a different random pool SI (A/C ≠ B). conet-l0d start --debugSI [SI] pins mailbox B only (file via [l0].temp_listen_mailbox_route_pgp_file or an SI URL/domain/key id). A pool_full response is a mailbox-local capacity failure: the current duplex must terminate, while a later APP-created duplex may choose another candidate. Static lists are used only when the pool is disabled or unavailable; do not reintroduce a Chat mining SSE for a pure --clientDuplex spoke.
The mailbox SSE lifecycle contract is:
- idle
l0_listenis kept alive by the SI's: keepalivecomment every 15 seconds; - an L0d receiver closes and rebuilds the SSE after 180 seconds without any SSE comment, handshake, or valid frame;
- after
l0_connect, L0d sends an encryptedduplex_pingevery 60 seconds and SI reclaims either occupied socket after 180 seconds without input; close,error, EOF, failed writes, and unusable sockets release the local owner and socket;- retrying a failed listen releases the old temporary identity and local TCP first.
pool_fullwhile establishingl0_listenis terminal for that duplex incarnation: the worker drops its ready signal withErr(pool_full), closes the APP TCP socket (or sends an encryptedduplex_rejectto the peer), and does not repeat the same 3-second POST with that temporary wallet. Pre-warm cools for at least 30 seconds before minting another temporary listen.- Any
l0_connectoccupation failure is also terminal for that duplex incarnation. Both sides discard the pipe handle/session; the peer receives an encryptedduplex_rejectwithreason=pipe_failedandretryable=truewhen a final control delivery is possible. Only the APP may create a new duplex.
The 180-second rule is a receive-side contract. A successful one-way SSE write is not an application acknowledgement of a remote peer; TCP keepalive and close/error events remain part of remote-exit cleanup.