Rotifer v0.27.0: Content-derived gene IDs for the LAN P2P node
v0.27.0 changes what a gene's ID is. It is no longer a name someone typed; it is v2: followed by a SHA-256 computed from the gene's contract and code. The P2P announcement that carries that ID now has a frozen wire forma
v0.27.0 changes what a gene's ID is. It is no longer a name someone typed; it is v2: followed by a SHA-256 computed from the gene's contract and code. The P2P announcement that carries that ID now has a frozen wire format, and every announcement is validated before a node forwards it.
We checked it the way a user would: two real machines on one Wi-Fi, both installed from npm at 0.27.0. An announcement made on one appeared on the other within 1.36 seconds, with the same gene ID on both ends.
@rotifer/playground is now at v0.27.0 on npm.
The ID comes from the content
The problem
Until now, a P2P announcement identified a gene by a string the publisher supplied, in practice its name. Any node could announce hello-world, and any node could announce a different gene under the same name. A name is a label the publisher claims. It tells a receiver nothing about what the gene actually is.
The fix
A gene ID v2 names a gene by what it contains:
v2:127ed2e889c02b4a6e20180501b47bd27bf9b3b7d8515eff505d56da7ed0c5d2
The ID is a SHA-256 over two things: a canonical projection of the gene's contract (domain, input and output schemas, fidelity, semantic requirements, dependencies, network permissions), serialized with the JSON Canonicalization Scheme, and a digest of the gene's body. For a Native or Hybrid gene the body digest covers every standard section of the compiled WASM module, not just the code section, so a module cannot change its embedded data or imports and keep its ID. For a Wrapped gene it covers the source files the gene declares.
What stays out is just as deliberate: author, creation time, SemVer version, description, tags. Who published a gene and when does not change what the gene is. Two genes with the same contract and the same code get the same ID, and changing one byte of logic or one field of the contract gives a different one.
rotifer wrap now prints it:
rotifer init demo-net && cd demo-net
rotifer wrap hello-world
โ Gene 'hello-world' wrapped successfully
Gene ID (v2): v2:127ed2e889c02b4a6e20180501b47bd27bf9b3b7d8515eff505d56da7ed0c5d2
Anyone running the bundled hello-world gene gets that exact string. Source build, CI build and npm install all agree on it.
The edges
Some genes have no v2 ID, and the CLI says so instead of guessing: a Native gene that has not been compiled, a dependency written as a wildcard, a Wrapped gene whose directory holds several candidate source files and no declared sources list. Such a gene cannot be announced on the network.
A Wrapped gene's ID covers its wrapper, not the remote service it calls. If that service changes behavior, the ID does not change. That is what fidelity: "Wrapped" already declares, and the ID does not paper over it.
The full definition, including the byte layout, is in docs/gene-id-v2.md. An independent Python reference implementation and shared test vectors sit next to the Rust code, and every implementation must reproduce every vector byte for byte.
A frozen wire format, validated before relay
What changed on the wire
Announcements are now protobuf on a new topic, /rotifer/p2p/1/announcements. Version 1 of this format is frozen: later changes get a new version rather than quietly altering this one. The message has no publisher field at all. The publisher of an announcement is the GossipSub-authenticated source, so a node cannot claim to be someone else by writing a different name into the payload.
Before a node forwards an announcement, it validates it. Malformed payloads, stale or future-dated timestamps (outside a five-minute window), oversized messages, and IDs that are not well-formed v2 gene IDs are rejected and not relayed. The receiver shows the name and version as labels, with a reminder that the gene ID is derived from the content.
Upgrade both sides
The older JSON announcements on the old topic and the new protobuf ones do not interoperate. A 0.26.x node cannot see a 0.27 announcement, and the reverse. If you run more than one machine, upgrade all of them.
What we attacked
The P2P module has an adversarial test suite that runs in CI against real libp2p nodes over loopback. Each hostile message is followed by a legitimate probe from the same attacker, so "not delivered" means "rejected", not "lost". The scenarios: impersonation, replay of an hour-old announcement, a timestamp an hour ahead, garbage and old-format payloads, a burst of 100 messages from one identity, a directory name passed off as a gene ID, and an invalid announcement sent through a relay toward a far node. On wire format v1 every one is stopped, and the relay case stops at the relay.
Limits, stated plainly: rate limiting is per identity, and identities are free to generate, so many fresh identities can still crowd out a node's received list. A node that is only ever given an attacker's bootstrap address will only see the attacker. Both need more than a LAN node can provide, and neither is addressed in this release.
Two machines, one Wi-Fi
Here is what we ran. Two Macs on the same network, a fresh npm install -g @rotifer/[email protected] on each. Machine B started a node. Machine A started a node with B's address as bootstrap, and rotifer network peers listed B within seconds. B ran rotifer network announce hello-world. A's rotifer network received showed:
[email protected]: general ยท Wrapped ยท v2:127ed2e889c0โฆ ยท from 12D3KooW9xLQhbKVโฆ
The prefix matches the full ID that B printed, and the from PeerId is B's. We measured on A's clock alone, so there is no clock skew between machines: 1.356 seconds from pressing enter to the announcement showing up. That figure is an upper bound. It includes starting the CLI process on B, computing the ID, and A polling for results every 0.1 seconds.
You can repeat it with two machines you own:
# on both machines
npm install -g @rotifer/[email protected]
# machine A
rotifer network start --host 0.0.0.0 --port 9878
rotifer network status # copy the /ip4/192.168.x.x line
# machine B
rotifer network start --host 0.0.0.0 --port 9878 \
--bootstrap /ip4/192.168.x.x/tcp/9878
rotifer network peers
# machine A: announce from a directory made by `rotifer init`
rotifer network announce hello-world
# machine B
rotifer network received
On first start, macOS may ask whether node can accept incoming connections. Allow it. The step-by-step guide, with troubleshooting, is in docs/p2p-lan-demo.md.
Where this sits
A network where genes find each other and compete by merit is the goal. This release is a smaller, checkable piece of it: on a local network, a gene is identified by its content, announced in a frozen format, and checked before it spreads.
What exists today: machines on one LAN, a manual --bootstrap address, announcements that carry metadata and the gene ID, not the gene binary. What does not exist yet: a public network. NAT traversal, bootstrap nodes anyone can reach, and search across peers are later work, and you cannot yet join from outside your own network.
Also in this release: three bundled Wrapped genes now declare their source files, so they have IDs, and the sandbox engine and TLS library moved to patched versions for new security advisories.
Install / Upgrade
npm install -g @rotifer/[email protected]
Upgrade every machine in a P2P setup together.
This article was originally published on rotifer.dev. Follow the project on GitHub or install the CLI: npm i -g @rotifer/playground.
Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes โ full credit and traffic to the original publisher.