Aleph Bootstrap
@le-space/aleph-bootstrap is the shared package for relay bootstrap
registration and discovery on Aleph.
It covers two sides of the flow:
- relay deployment paths publish fresh public multiaddrs as Aleph
POSTmessages - browser or Node libp2p apps query those posts before creating their bootstrap peer discovery service
Shared Defaults
@le-space/aleph-bootstrap is a shared package, not a simple-todo-specific
integration.
Today it is used by:
simple-todouniversal-connectivity/js-peer- shared relay deployment flows in
relay-button, including theuc-go-peerandorbitdb-relayrootfs profiles - the
ucan-storerootfs/deployment profile forNomadKids/ucan-store
The package still ships with a backward-compatible default namespace rooted in
the earliest simple-todo integration:
- channel:
simple-todo - ref:
simple-todo-bootstrap - post type:
relay-bootstrap-v2
The channel and ref can be overridden when a consumer needs an app-specific
namespace or stronger isolation between environments. The post type is fixed to
relay-bootstrap-v2.
Relay Registration
The shared deploy flows now register relay bootstrap addresses automatically
for the uc-go-peer and orbitdb-relay RootFS profiles when they are launched
through:
- the shared GitHub Action VM deploy path
- the Relay Button browser UI path
ucan-store is also a shared RootFS/deployment profile, but it intentionally
does not publish relay bootstrap records. It exposes service discovery through
guest metadata and /.well-known/ucan-store.json instead.
Registrations use the compact relay-bootstrap-v2 shape. It stores the
browser-ready bootstrap list directly in multiaddrs, caps it to the best
public browser-dialable candidates, and omits the duplicate
browserMultiaddrs array.
The registration payload stores:
peerIdmultiaddrsregistrationIdwhen a producer can provide a stable relay identityprofileversionupdatedAt
Only public multiaddrs are published. Loopback, RFC1918, link-local, and
localhost-style addresses are filtered out before the Aleph POST is signed.
Browser transports are preferred in this order:
/tcp/443/tls/ws, other /tls/ws, WebTransport, WebRTC direct, then other
websocket addresses.
Reading the channel: two scopes
discoverAlephBootstrapMultiaddrs takes profile and registrationId, and
a consumer that dials what it finds wants both.
profile excludes relays it cannot use: an orbitdb app that dials a
uc-go-peer relay never forms a shared circuit and stops at
candidates: 0.
registrationId excludes its own past. Every ephemeral relay an E2E run
starts registers under the production profile, and the record survives the
machine — guests self-publish with generated keys, so no remaining key can
FORGET it. Measured in simple-todo: a browser probe wave against the unscoped
list exhausted its outbound stream budget on dead addresses and discarded the
one live relay with them, leaving both browsers with nothing to dial.
Omitting both returns the whole channel, which is what a dashboard wants and what anything dialling does not.
Relay Button Registration Lifecycle
For uc-go-peer, the browser Relay Button flow is now a staged handoff rather
than a single direct bootstrap publish:
- the browser waits for Aleph runtime networking and mapped ports
- if Aleph reserved a
2n6hostname, the browser waits for that HTTPS route to become active before handingproxyUrlto the guest - the browser publishes a short-lived
vm-bootstrap-configaggregate keyed by the deployment token - the guest consumes that aggregate and emits a
vm-bootstrap-config-statussignal - the browser prefers relay metadata from that guest signal when it already
contains
peerIdand public multiaddrs - otherwise the browser polls relay metadata, preferring the secure
https://relay-name.2n6.me/bootstrap/metadataendpoint when available - the browser waits for the guest to publish its own
relay-bootstrap-v2POSTon Aleph - if that guest registration is delayed, the browser publishes a fallback
bootstrap record from the owner wallet using the same
registrationId - after success, the temporary bootstrap-config aggregate is removed
This matters because the relay bootstrap record is now tied to the actual runtime addresses seen after deployment, not just to the deployment wallet or to build-time assumptions.
Browser Path And Workflow Path
The browser UI path and the GitHub workflow path now converge on the same final bootstrap registration shape, but they still differ in how they hand off guest configuration:
- browser
uc-go-peer- publishes runtime networking into Aleph via
vm-bootstrap-config - waits for the guest
vm-bootstrap-config-statussignal - can continue without HTTPS if the
2n6route never activates
- publishes runtime networking into Aleph via
- Node/workflow deploy path
- configures the guest directly through the temporary setup endpoint on the mapped host port
- polls guest
/metadatauntil public relay multiaddrs are ready - publishes the final bootstrap registration from the deploy runner
Both paths ultimately use the same shared registration primitives:
createRelayBootstrapRegistrationId(...)publishRelayBootstrapRegistration(...)waitForRelayBootstrapRegistration(...)
Runtime Suitability Checks
Proxy-backed HTTPS activation is only usable when the runtime exposes a public guest IPv6. If a CRN reports a private or otherwise non-globally-routable guest IPv6, the deploy code now treats that runtime as unsuitable for proxy-backed HTTPS:
- the browser UI marks the runtime as unusable, cleans up the failed attempt, and retries another CRN when possible
- the workflow path does the same cleanup-and-retry behavior before treating that attempt as successful
This is not just a browser fetch quirk. It is a runtime suitability problem for the proxy-backed HTTPS path itself.
Current Signing Model
Today the codebase supports two signing modes for v2 bootstrap records:
- wallet-signed records
- dual-key records with owner authorization plus relay proof
Current profile behavior:
uc-go-peer- when
ALEPH_VM_BOOTSTRAP_PUBLISHER_PRIVATE_KEYis supplied, the deploy flow derives a libp2p secp256k1 identity from that same publisher keyB - that protobuf-encoded libp2p key is written into the guest before first
start, so the relay
peerIdcomes from the same underlying key material as the Aleph publisher identity - if owner key
Ais also supplied, the owner authorization can be minted up front and stored in the first guest configure call
- when
orbitdb-relay- now also accepts a preseeded libp2p secp256k1 identity derived from
publisher key
B - when publisher key
Bis supplied, the relaypeerIdcomes from the same underlying key material as the Aleph publisher identity - keeps libp2p AutoTLS enabled by default; the guest first advertises direct
plain websocket candidates, then the AutoTLS refresh service appends
*.libp2p.directsecure websocket addresses after the relay exposes its serving zone - when Aleph reserves a
2n6proxy hostname, Caddy only serves that hostname and uses HTTP-01 certificate validation, leaving libp2p AutoTLS to manage the direct secure websocket hostnames - when publisher key
Bis not supplied, it falls back to the older self-generated Ed25519 relay identity - in that fallback mode, owner authorization is still written back into the
guest with a second
no_startconfigure call after runtime metadata is known
- now also accepts a preseeded libp2p secp256k1 identity derived from
publisher key
So the remaining trust gap is now narrower than before:
uc-go-peercan already bind publisher keyBand relaypeerIdto the same secp256k1 rootorbitdb-relaycan now do the same when publisher keyBis supplied- wallet-signed v2 records are accepted by default
Target Signing Model
The intended next model is a dual-key relay bootstrap record.
Roles:
- owner key
A: the deployment wallet that creates or sponsors the Aleph VM - relay key
B: a separate key held by the running relay VM
Target proof chain:
- owner key
Asigns an authorization statement for relay keyB - relay key
Bsigns the current bootstrap record payload - the bootstrap
POSTis published by relay keyB - readers verify both signatures before trusting the record
This gives the relay enough autonomy to refresh its own bootstrap record while still preserving a cryptographic relationship to the original deployment wallet.
Important design note:
- this is intentionally a dual-key model
- it replaces earlier ideas about reusing the deployer wallet directly or relying on Aleph-specific publish-on-behalf delegation as the primary long term design
- if the relay peer identity is later made compatible with the same signing key material, that can simplify verification further, but the baseline plan is still dual-key authorization plus relay-side signing
Current Transport Reality
Today, relay-button submits those signed Aleph POST messages
through Aleph's HTTP/Core Channel Node gateway.
Aleph's platform documentation also describes peer-to-peer message broadcast as
another supported transport, but the practical first-party integration surface
we found for that path is Aleph's separate p2p-service bridge rather than a
browser-ready TypeScript SDK.
That means the current implementation should be understood as:
- decentralized at the wallet-signing layer
- gateway-based at submission time
- public and REST-queryable at read time
App Discovery
Typical discovery looks like this:
import { createLibp2pAlephBootstrap } from '@le-space/aleph-bootstrap'
const alephBootstrap = await createLibp2pAlephBootstrap()
If an app wants only the raw multiaddr list, call:
import { discoverAlephBootstrapMultiaddrs } from '@le-space/aleph-bootstrap'
const list = await discoverAlephBootstrapMultiaddrs()
By default, discovery:
- queries
https://api2.aleph.im/api/v0/posts.json - loads only
relay-bootstrap-v2posts - fails clearly if the API returns a legacy
relay-bootstraprecord - skips entries older than 7 days
- keeps only the newest fresh record per relay identity
- sorts fetched records by recency client-side, even though Aleph currently returns page 1 newest-first for this query
- deduplicates the returned multiaddrs
- verifies dual-key records when they are present
Important caveat:
- the 7-day cutoff is our application-level freshness rule
- we still do not have source-backed proof from Aleph's public docs about when
ordinary bootstrap
POSTmessages are automatically pruned, if ever - relays should eventually republish and forget their own old bootstrap records instead of assuming network-side cleanup
Current implementation detail:
- when a bootstrap record includes
registrationId, discovery collapses to the newest fresh record for that relay identity - otherwise discovery falls back to the newest fresh record per sender address
- invalid dual-key records are ignored during discovery
- wallet-signed v2 records are accepted by default
Current UI state handling also relies on registrationId:
- the Relay Button panel marks registrations that match a live instance as confirmed instance-linked registrations
- registrations for the current wallet that no longer match any instance are shown as orphan bootstrap registrations
- deleting an instance now forgets the instance and its linked bootstrap registration records together
- orphan registrations can be forgotten independently from the panel
Discovery can be tightened further with:
verifyDualKeyAttestation: trueThis is the default behavior for posts that already carry dual-key proof objects.requireDualKeyAttestation: trueThis rejects wallet-signed records and only accepts dual-key-attested relay registrations.
Example:
import { discoverAlephBootstrapMultiaddrs } from '@le-space/aleph-bootstrap'
const list = await discoverAlephBootstrapMultiaddrs({
requireDualKeyAttestation: true,
})
Dual-Key Helpers
The package now also exposes low-level helpers for dual-key record creation and verification:
signRelayBootstrapAuthorization(...)signRelayBootstrapProof(...)verifyRelayBootstrapAuthorization(...)verifyRelayBootstrapProof(...)verifyRelayBootstrapDualKeyContent(...)relayBootstrapTrustMode(...)
Validation
The shared monorepo now has two validation layers for this flow:
- local unit coverage in
packages/aleph-bootstrap/testandpackages/core/test/bootstrap-registration.test.ts - an opt-in live round-trip script at
scripts/test-aleph-bootstrap-live.mjs
Run the deterministic local checks with:
pnpm --filter @le-space/aleph-bootstrap test
pnpm --filter @le-space/core test
Run the live Aleph round-trip with a funded or otherwise valid wallet key:
ALEPH_BOOTSTRAP_TEST_PRIVATE_KEY=0xyourkey pnpm test:aleph-bootstrap:live
The live check:
- publishes dummy public multiaddrs into Aleph as a
relay-bootstrap-v2POST - republishes the same relay identity with refreshed public multiaddrs
- requests
FORGETfor the older bootstrap record - polls
posts.jsonuntil the refreshed post is visible - re-runs discovery through
discoverAlephBootstrapMultiaddrs() - verifies that private and localhost multiaddrs were filtered out
- verifies that discovery selects the refreshed browser bootstrap addrs instead of the older ones
An optional variant also uses a fresh random ephemeral wallet key:
ALEPH_BOOTSTRAP_TEST_USE_EPHEMERAL_KEY=true pnpm test:aleph-bootstrap:live
In our live testing, that ephemeral-key mode was also accepted by Aleph for the
same bootstrap POST and FORGET lifecycle. For this bootstrap flow, that is
strong evidence that public write access is effectively open to arbitrary valid
keys, which reinforces the need for consumer-side anti-spam handling.
Periodic Refresh
Two refresh paths now exist:
- external
@le-space/noderefresh-bootstrapmode - in-guest periodic refresh from the relay VM itself
@le-space/node now supports:
ALEPH_VM_MODE=refresh-bootstrap
Expected environment inputs:
ALEPH_VM_PRIVATE_KEYALEPH_VM_NAMEALEPH_VM_PROFILEALEPH_VM_RELAY_PEER_IDALEPH_VM_PROBE_MULTIADDRS_JSONALEPH_VM_BROWSER_BOOTSTRAP_MULTIADDRS_JSON- optional
ALEPH_VM_ROOTFS_VERSION - optional
ALEPH_VM_BOOTSTRAP_REGISTRATION_ID - optional
ALEPH_VM_BOOTSTRAP_FORGET_PREVIOUS - optional
ALEPH_VM_PUBLISHER_PRIVATE_KEY - optional
ALEPH_VM_OWNER_PRIVATE_KEY
That mode republishes the relay bootstrap registration and, by default, forgets older self-owned records for the same registration ID.
Current dual-key behavior in external refresh-bootstrap:
- if only
ALEPH_VM_PRIVATE_KEYis set, refresh behaves like the legacy wallet-signed model - if
ALEPH_VM_PUBLISHER_PRIVATE_KEYis set, the relay bootstrapPOSTis published by that publisher identity - if
ALEPH_VM_OWNER_PRIVATE_KEYis also set, the refresh path embeds the owner authorization plus relay proof objects automatically
This external mode remains useful for one-off repair or manual refresh, but it is no longer the only heartbeat path.
Current in-guest refresh behavior:
- deploy/configure stores
ALEPH_BOOTSTRAP_PUBLISHER_PRIVATE_KEY,ALEPH_BOOTSTRAP_REGISTRATION_ID, and a base64-encodedALEPH_BOOTSTRAP_OWNER_AUTHORIZATION_B64record in the relay VM env file - both relay profiles can additionally store a precomputed libp2p secp256k1
identity derived from publisher key
B, so the runtimepeerIdis known before the relay first starts - if
orbitdb-relayis deployed without publisher keyB, it still generates owner authorization after the real relaypeerIdis known and writes it back into the guest with a secondno_startconfigure call - the guest runs a systemd timer:
uc-go-peer-bootstrap-refresh.timerorbitdb-relay-bootstrap-refresh.timer
- the timer invokes a local Python refresher that:
- calls the profile-specific
*-describe.pyhelper - signs the relay proof with publisher key
B - republishes the bootstrap
POST - forgets older self-owned records for the same registration ID
- calls the profile-specific
This keeps the owner key A out of the long-lived guest state once the
authorization record has been minted.
Live Validation Requirements
Requirements and caveats for validating publish and discovery end to end against the real network:
- an EVM private key available as
ALEPH_BOOTSTRAP_TEST_PRIVATE_KEYorALEPH_PRIVATE_KEY - small indexing delay on Aleph before a newly published
POSTappears inposts.json - dedicated
channelandrefvalues if you want isolation from the sharedsimple-todonamespace during testing - browser consumers still need their own project-level checks because bootstrap discovery can be correct while unrelated app type errors still exist
Known Limits
Attestation is opt-in. requireDualKeyAttestation defaults to false, so
a consumer that does not ask for it will accept legacy wallet-signed records.
The default cannot flip until every relay producer emits an attested record —
tracked in
#108.
Until it does, treat the shared namespace as publicly writable: consumer-side filtering is what carries the trust, not the namespace.
Liveness is not provable from Aleph. A record says a relay announced these addresses, not that they answer. Check at dial time; see Relay dialability timeline for how that has gone wrong in practice.
Transport. Publication and discovery go over Aleph's REST/CCN path. Moving them to direct P2P libp2p flows is under investigation in #5.
Consumer Notes
universal-connectivity/js-peer and simple-todo should use this package in
place of hardcoded bootstrap multiaddrs or direct @libp2p/bootstrap static
lists.