Skip to content

UDX: Go, Dart, JS

This page shows how to build a libp2p host that reaches a go-ricochet server in Go, Dart and JavaScript. The server listens on UDX only, so this is the one step every client has to get right.

A server’s address is a UDX multiaddr followed by its peer ID:

/ip4/203.0.113.10/udp/55223/udx/p2p/12D3KooW...
Part Meaning
/ip4/203.0.113.10 or /ip6/... Server IP; the UDX transports dial IP addresses, not DNS names
/udp/55223 UDP port; 55223 unless the operator changed server.port
/udx UDX on top of UDP (multiaddr protocol code 0x0300)
/p2p/12D3KooW... Server peer ID, printed at startup; see First run & your peer ID

Each libp2p connection is one UDX connection. Its first UDX stream is upgraded like a TCP socket: multistream-select, then Noise, then Yamux. All three implementations below do this, so a host in any of them needs:

Layer Value
Transport UDX, wire version 3
Security Noise (/noise)
Muxer Yamux (/yamux/1.0.0)

UDX wire version 3 does not interoperate with version 2: a mismatched peer looks unreachable. go-udx v0.1.3, dart_udx 3.0 and later, and js-udx all speak version 3.

Use go-libp2p-udx-transport v0.1.8, the version go-ricochet 1.1.0 itself uses. This host is the one in go-ricochet’s examples/mailbox and ricochet-bench.

Terminal window
go get github.com/stephanfeb/go-libp2p-udx-transport@v0.1.8
host.go
// Sketch: a UDX-only host, then a connection to the server.
h, err := libp2p.New(
libp2p.Identity(priv),
libp2p.NoTransports,
libp2p.Transport(udxtransport.NewTransport),
libp2p.ListenAddrStrings("/ip4/0.0.0.0/udp/0/udx"),
libp2p.Security(noise.ID, noise.New),
libp2p.Muxer("/yamux/1.0.0", yamux.DefaultTransport),
)
if err != nil {
return err
}
info, err := peer.AddrInfoFromString("/ip4/203.0.113.10/udp/55223/udx/p2p/12D3KooW...")
if err != nil {
return err
}
if err := h.Connect(ctx, *info); err != nil {
return err
}

Imports: github.com/libp2p/go-libp2p, .../core/peer, .../p2p/muxer/yamux, .../p2p/security/noise and udxtransport "github.com/stephanfeb/go-libp2p-udx-transport". Pass h to client.New; see Go: your own libp2p host to add UDX to a host that keeps other transports. The UDX protocol itself is documented at cryptopeer.net/udx/go.

dart_libp2p ships a UDXTransport built on dart_udx. Use dart_libp2p 3.0.0 or later, whose dart_udx speaks wire version 3; the current release is 4.7.1, on dart_udx 4.1. The host below is the one the ricochet package’s interop harness (bin/store_interop_test.dart) runs against go-ricochet.

host.dart
// Sketch, after bin/store_interop_test.dart in ricochet-dart-client 0.2.4.
Future<BasicHost> createHost(KeyPair keyPair) async {
final connMgr = ConnectionManager(idleTimeout: const Duration(seconds: 60));
final udxTransport = UDXTransport(connManager: connMgr);
final yamuxConfig = MultiplexerConfig(
keepAliveInterval: const Duration(seconds: 60),
maxStreamWindowSize: 1024 * 1024,
initialStreamWindowSize: 256 * 1024,
streamWriteTimeout: const Duration(seconds: 30),
maxStreams: 256,
);
final options = <p2p_config.Option>[
await p2p_config.Libp2p.identity(keyPair),
await p2p_config.Libp2p.connManager(connMgr),
await p2p_config.Libp2p.transport(udxTransport),
await p2p_config.Libp2p.security(await NoiseSecurity.create(keyPair)),
await p2p_config.Libp2p.muxer('/yamux/1.0.0', (Conn secureConn, bool isClient) {
return YamuxSession(secureConn as TransportConn, yamuxConfig, isClient, null);
}),
await p2p_config.Libp2p.listenAddrs([MultiAddr('/ip4/0.0.0.0/udp/0/udx')]),
];
return await p2p_config.Libp2p.new_(options) as BasicHost;
}
// Connect to the server.
final host = await createHost(await crypto_ed25519.generateEd25519KeyPair());
await host.start();
final target = MultiAddr('/ip4/203.0.113.10/udp/55223/udx/p2p/12D3KooW...');
final serverId = PeerId.fromString(target.valueForProtocol(Protocols.p2p.name)!);
await host.connect(
AddrInfo(serverId, [target.decapsulate(Protocols.p2p.name)!]),
context: core_context.Context(),
);

The imports are listed at the top of the harness file. Hand host to SFClient; see Dart client.

js-udx provides @stephanfeb/libp2p-udx, a js-libp2p v3 transport. It runs on Node.js 22 or later, not in browsers, and is not on npm yet: install it from source as its Getting started guide describes.

node.ts
// From the js-udx README: a js-libp2p node that speaks UDX, Noise and Yamux.
import { createLibp2p } from 'libp2p'
import { noise } from '@chainsafe/libp2p-noise'
import { yamux } from '@chainsafe/libp2p-yamux'
import { udx } from '@stephanfeb/libp2p-udx'
const node = await createLibp2p({
addresses: { listen: ['/ip4/0.0.0.0/udp/0/udx'] },
transports: [udx()],
connectionEncrypters: [noise()],
streamMuxers: [yamux()]
})

There is no Ricochet client for JavaScript, so you open protocol streams and write the frames yourself. A capacity query on the admin protocol looks like this:

capacity.ts
// Sketch: not tested against go-ricochet. One length-prefixed JSON frame each way.
import { multiaddr } from '@multiformats/multiaddr'
const server = multiaddr('/ip4/203.0.113.10/udp/55223/udx/p2p/12D3KooW...')
const stream = await node.dialProtocol(server, '/sf-network/admin/1.0.0')
const body = new TextEncoder().encode(JSON.stringify({ operationType: 'queryCapacity' }))
const frame = new Uint8Array(4 + body.length)
new DataView(frame.buffer).setUint32(0, body.length) // big-endian length
frame.set(body, 4)
stream.send(frame)
await stream.close()
const chunks: Uint8Array[] = []
for await (const chunk of stream) chunks.push(chunk.subarray())
const reply = Buffer.concat(chunks)
console.log(JSON.parse(reply.subarray(4, 4 + reply.readUInt32BE(0)).toString()))

js-udx’s interop tests run js-libp2p against go-libp2p-udx-transport and dart-libp2p in both directions. The frame format and every request shape are on Writing your own client.