Skip to content

Latest commit

 

History

38 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Radish

A Radicle client and node, in Zig. Early work in progress, verified against Heartwood, the RFCs, git, and a live radicle-node.

Radicle 1.x runs over Noise/TCP. 2.0 moves to QUIC via iroh, and the two do not interoperate. Radish implements 1.x in src/net/ and is experimenting with 2.x in src/quic/; storage, identities, sigrefs and COBs are shared.

Radish is seeded on the radicle network as rad:z4VSyUhaBGUJQrFdS7nWULf1dJdos, so it can clone itself:

radish clone rad.0x51.dev 8776 z6Mkhh3TfBZeGW4z4uufMp7caXoBf2wcpDWDrRsELqWqmT6Y \
  rad:z4VSyUhaBGUJQrFdS7nWULf1dJdos radish.git --require-verified

Clones are verified, not just downloaded: every remote's rad/sigrefs signature is checked against its node id, each signed oid must be present in the packfile, and each ref on disk must be signed at the oid it points to. Verification reports rather than filters, matching heartwood's validate.

CLI

radish ping        <host> <port> <node-id>              handshake + ping/pong
radish announce    <host> <port> <node-id> [alias]      send a signed NodeAnnouncement
radish subscribe   <host> <port> <node-id> [frames]     listen to gossip (nodes + inventory)
radish fetch-probe <host> <port> <node-id> <rid>        open a git stream, read the v2 advertisement
radish clone       <host> <port> <node-id> <rid> <dir>  clone a repo into <dir> (bare), verifying each remote
  --require-verified                                    exit non-zero if any remote fails verification
  --profile                                             print connection counters on exit
radish seeds       <rid>                                who holds a repo; needs --from or --dir
  --dir <path>                                          remotes in a local clone (no network)
  --from <host>:<port>:<node-id>                        seeds announcing it over gossip
  --frames <n>                                          gossip frames to observe (default 200)
radish peers       <host>:<port>:<node-id>              nodes seen announcing themselves, with addresses
  --frames <n>                                          gossip frames to observe (default 200)
radish fetch-deps  <manifest> [dir]                     resolve `.rad` dependencies (default .rad-deps)
  --from <host>:<port>:<node-id>                        fetch from this node instead of locating a seed
radish serve       <port> [sessions]                    answer inbound connections (one at a time)
radish quic ping   <host> <port>                        2.x: handshake, then a gossip ping/pong
radish quic subscribe <host> <port> [messages]          2.x: listen to gossip (default 200)
radish quic clone  <host> <port> <rid> <dir>            2.x: clone a repo into <dir> (bare)
  --require-verified                                    exit non-zero if any remote fails verification
  --profile                                             print connection counters on exit
radish quic probe  <host> <port> [alpn] [sni]           2.x: send an Initial, read the reply

Zig packages over Radicle (proof of concept)

A Zig dependency can be named by RID instead of a URL, authenticated end to end. fetch-deps clones each one, checks it really is the repo that was asked for, resolves the identity document's defaultBranch from a delegate's namespace, and checks that commit out.

Write only .rad:

.dependencies = .{
    .radish = .{
        .rad = "z4VSyUhaBGUJQrFdS7nWULf1dJdos",
    },
},

then run:

radish fetch-deps build.zig.zon
zig build

fetch-deps rewrites the manifest in place, preserving comments and formatting:

.radish = .{
    .rad = "z4VSyUhaBGUJQrFdS7nWULf1dJdos",
    .path = ".rad-deps/radish",
    .rad_hash = "radtree-1-fa8889c5...",
},

With no node named, radish asks a bootstrap seed who announces the RID. Name one to avoid leaning on the public bootstraps:

.radish = .{
    .rad = "z4VSyUhaBGUJQrFdS7nWULf1dJdos",
    .node = "rad.0x51.dev:8776:z6Mkhh3TfBZeGW4z4uufMp7caXoBf2wcpDWDrRsELqWqmT6Y",
},

.node is a preference and falls back to discovery if unreachable; --from overrides both and does not fall back. Pin an exact commit with .rev, or omit it to follow defaultBranch.

Limitations

  • Zig requires url or path, so .rad cannot stand alone, and it will not hash a path dependency. Hence the generated .path and .rad_hash, which radish checks and Zig ignores. Native support needs a rad variant of Zig's location union.
  • Discovery needs an entry point. The bootstrap seeds in net/seeds.zig are hardcoded.
  • Without .rev, delegates publishing different heads for defaultBranch fails rather than picking one.
  • No transitive dependencies, no lockfile. A matching .rad_hash skips the network; anything else is re-cloned in full.

Radish is NOT a good network citizen

Easy to point at seeds you do not run, so, explicitly:

  • Every connection uses a throwaway identity, and serve regenerates its key per run. Rate limits and bans do not accumulate against a rotating identity.
  • Radish drains gossip and disconnects. It relays nothing and seeds nothing.
  • Locating a seed costs a full subscribe per dependency, spent whether or not the RID turns up.

Prefer .node or --from to point at a seed you run, and do not put fetch-deps in a loop.

QUIC (2.x)

Independent of src/net/, and pinned to RFC 9001 Appendix A and RFC 8448 vectors, and completed against a live radicle 2.x node: packet protection, long and short headers, the frames a handshake needs, and mutual authentication with raw public keys, through to HANDSHAKE_DONE. A public server refuses raw public keys, so the peer has to be iroh; nix build .#radicle-ng provides one.

One stream runs on top, with flow control, key updates, RFC 9002 loss recovery and NewReno. 2.x gossip runs over it, reusing the 1.x message codec, and so does git protocol v2: the radicle/git/1 ALPN carries git bytes unframed, so cloning is the same ls-refs, packfile, index and verify as 1.x over a different session. A 12 MB packfile clones and verifies against a local node.

The server side answers now: a ClientHello is checked and negotiated, and the ServerHello and the flight behind it go back. An endpoint binds a port and accepts one connection, so a client and a server of ours handshake to HANDSHAKE_DONE over a socket. One at a time, though: finding a connection by id is not built, nor Retry, nor stateless reset.

Build

Requires a Zig 0.17-dev toolchain; a Nix flake pins it.

nix develop --command zig build test
nix develop --command zig build

The pack indexer is the Zig toolchain's own git.zig, compiled from source as a module. Stock 0.17-dev writes a zeroed CRC32 table that real git rejects, so the flake pins a branch carrying the fix (ziglang/zig#36328). Override with -Dgitpack=/path/to/git.zig, or drop the pin once it lands upstream.

References

Each function cites the section it implements. These are the documents:

License

MIT

About

A Radicle client (and node) in Zig - sovereign, peer-to-peer code forge.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Contributors

Languages