Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
194 changes: 194 additions & 0 deletions docs/experimental-hnsr.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
# Experimental HNSR proof of concept

This branch contains a deliberately bounded, regtest-only implementation of
the unnamed `HNS_NODE_V1` path from the draft **Handshake P2P Rendezvous and
Authenticated Service Relay** HIP.

It is reference code for exercising the wire shape, authorization boundaries,
and lifecycle on actual `hsd` peers. It is not a production relay, a permanent
wire assignment, or a claim that every phase of the draft HIP is implemented.

## Private assignments

| Symbol | Private value |
| --- | ---: |
| HNSR rendezvous service | `0x04000000` |
| HNSR relay service | `0x08000000` |
| HNSR packet type | `0xf3` |

The roles cannot be enabled outside regtest. Nodes on other networks do not
advertise either bit. These values are collision-prone experimental values and
must be replaced if the protocol receives assigned values.

## Implemented trial profile

The branch implements:

- the version-1 HNSR envelope and all 21 reserved opcode numbers;
- strict envelope length, flag, version, opcode, and context checks;
- regtest-only role advertisement;
- iterative `FINDNODE` / `NODES` discovery with XOR ordering, parallelism of
three, a 32-query bound, authenticated rendezvous contacts, and connections
to newly discovered Handshake peers;
- publisher-driven route replication with a configurable replica count and
minimum-store quorum;
- deterministic bounded `SAMPLEROUTES` discovery for unnamed node routes;
- endpoint-signed `RESERVE`, relay-signed `OFFER`, endpoint `CONFIRM`, and
jointly authenticated relay tickets;
- reservation `RENEW` and signed `WITHDRAW`, including retirement of replaced
tickets;
- strict-DER, low-S secp256k1 signatures with network- and domain-separated
digests;
- self-authorized unnamed endpoint delegations and route records for
`HNS_NODE_V1`;
- bounded, expiring, sequence-aware in-memory route storage with global,
per-key, and per-publishing-peer limits;
- `PUTROUTE` / `PUTRESULT` and exact-key `GETROUTE` / `ROUTES`;
- multi-relay records, renewed-record republishing, sequential relay failover,
and failure reporting;
- `OPEN` / `INCOMING` / `ACCEPT` / `OPENED` circuit establishment;
- opaque `DATA`, directional `WINDOW`, and `CLOSE` forwarding;
- per-ticket circuit and byte limits, bounded frames, and relay-side
directional credit enforcement;
- bounded relay queues with burst-yield scheduling, control-request admission,
request-byte limits, and observable queue/drop counters;
- immediate local reservation invalidation when the endpoint peer disconnects;
and
- actual inbound and outbound `Peer` objects in the ordinary HSD pool, running
complete end-to-end Brontide and Handshake peer sessions over virtual circuit
sockets.

The proof-of-concept handler does not forward to a requester-selected host or
port. A circuit can terminate only at the exact live peer connection bound to
the signed reservation.

## Milestone boundaries

The following groups are deliberately separated so that completion of the
unnamed-node experiment is not confused with named services, client product
work, or public-network readiness.

### Unnamed-node Phase 1: implemented here

The branch completes the directly executable unnamed `HNS_NODE_V1` slice:

- two independent relay candidates in each tested route;
- four iteratively discovered rendezvous nodes and four-copy publication;
- random unnamed-node sampling and exact-key lookup;
- reservation, renewal, replacement publication, withdrawal, disconnect, and
stale-ticket rejection;
- rendezvous loss and first-relay failure recovery;
- real inner full-node block traffic; and
- flow-control, scheduler saturation, control admission, and zero-drop checks.

The rendezvous table is intentionally the bounded live/recently-connected
contact set for this regtest phase. It exercises iterative XOR routing but is
not yet the persistent bucket implementation required for a public network.

### Phase 1B: named service authorization and profiles

Still to implement before claiming the HIP's complete Phase 1 service surface:

- authenticated HNS authority lookup and canonical TXT root-key parsing;
- service authorizations and named endpoint delegations;
- named route-key derivation and authorization-chain validation; and
- the `HNS_WEB_V1` handler and origin rules.

These features are not prerequisites for review of the unnamed full-node
transport, but they are prerequisites for claiming named HNS service support.

### Phase 2: bounded testnet hardening

Still required before any testnet experiment:

- persistent routing buckets and optional durable route storage;
- eight-replica, multi-path churn tests over larger and adversarial topologies;
- public-address admission, routability, per-prefix, and netgroup policy;
- peer-dial budgets, topology scoring, republish/failover timers, and restart
recovery; and
- scheduler integration that explicitly prioritizes blocks, headers, proofs,
and transaction traffic, plus operational telemetry.

### Phase 3: node, mobile, and browser integration

Still required for user-facing adoption:

- RPCs and configuration/status APIs;
- address-manager, wallet, and SPV discovery integration;
- Android foreground/background lifecycle and network-change handling;
- HNS-aware browser navigation and named-origin behavior; and
- operator documentation, compatibility behavior, and upgrade UX.

### Public-network and production readiness

Permanent service/packet assignments, production abuse controls, reputation or
payment policy, privacy review, deployment gates, and sustained public-network
load measurements remain outside this PoC. The regtest-only feature guard stays
in place until those questions are resolved.

## Reproducible trial

From this branch:

```sh
npm ci
NODE_BACKEND=js npm run test-file -- \
test/hnsr-test.js test/brontide-test.js test/net-test.js
NODE_BACKEND=js node scripts/run-hnsr-regtest-trial.js \
docs/hnsr-regtest-phase1.json
```

`NODE_BACKEND=js` selects bcrypto's portable JavaScript backend and is not a
protocol requirement.

The trial starts eight independently keyed, independently prefixed FullNodes:

```text
Endpoint (no listener) ==> Relay A, Relay B, Rendezvous 0
Requester ==> Rendezvous 0
Rendezvous 0 ==> Rendezvous 1 ==> Rendezvous 2 ==> Rendezvous 3

Requester == inner HNS peer ==> surviving relay ==> Endpoint
```

It then:

1. iteratively discovers all four rendezvous nodes from one bootstrap;
2. reserves both relays and stores one signed route at all four rendezvous
nodes;
3. discovers the route with `SAMPLEROUTES`;
4. renews both tickets, republishes a higher sequence, and withdraws the old
reservations;
5. issues 72 concurrent lookup requests and verifies bounded admission;
6. stops one rendezvous node and retrieves the refreshed record from the three
survivors;
7. stops Relay A and verifies automatic fallback to Relay B;
8. constructs ordinary inbound/outbound HSD `Peer` objects over the circuit
and verifies both inner static identities;
9. sends 1,000 ordinary Handshake pings while mining and relaying a real block;
10. proves only endpoint and requester reach height 1 while both relays and all
four rendezvous chains remain at height 0;
11. verifies bounded queues, multiple scheduler yields, a control reservation
during load, and zero relay drops; and
12. disconnects the endpoint, retrieves the intentionally stale route, and
confirms the surviving relay rejects its invalid ticket.

The checked-in `docs/hnsr-regtest-phase1.json` is one passing run. It records
topology, discovery, replica survival, lifecycle transitions, selected relay,
block-only inner convergence, saturation counters, admission results, opcode
counts, and a ciphertext transcript hash. Random values change on every run.

## Configuration surface

The following illustrative flags are recognized by `FullNode`:

```text
--experimental-hnsr
--experimental-hnsr-endpoint
--experimental-hnsr-relay
--experimental-hnsr-rendezvous
--experimental-hnsr-timeout=<milliseconds>
```

The relay role also requires the ordinary peer listener. Endpoint and requester
roles advertise no HNSR service bit.
114 changes: 114 additions & 0 deletions docs/hnsr-regtest-phase1.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
{
"schema": 2,
"network": "regtest",
"assignment": {
"rendezvousServiceBit": "0x4000000",
"relayServiceBit": "0x8000000",
"packetType": "0xf3"
},
"topology": {
"fullNodes": 8,
"relays": 2,
"rendezvousNodes": 4,
"endpointListeners": 0,
"outerTransport": "authenticated Handshake Brontide",
"innerTransport": "end-to-end authenticated Handshake Brontide"
},
"discovery": {
"bootstrapRendezvous": 1,
"endpointKnownRendezvous": 4,
"requesterKnownRendezvous": 4,
"sampledRecords": 1,
"sampledEndpointFound": true,
"iterativeLookupLiveNodes": 3
},
"replication": {
"requestedCopies": 4,
"initialStoredCopies": 4,
"refreshedStoredCopies": 4,
"survivingStores": [
1,
1,
1
],
"rendezvousFailureRecovered": true
},
"lifecycle": {
"initialSequence": 1,
"refreshedSequence": 2,
"renewedTickets": 2,
"oldTicketsWithdrawn": 2,
"staleRouteStillReturned": true,
"disconnectedReservationInvalidated": true,
"staleTicketRejected": true
},
"failover": {
"firstRelayStopped": true,
"failedCandidates": 1,
"selectedRelay": "03664a4d9746167b043a17cd9b9f2211f92ef37992b2025cc3d4570b302150d94a",
"selectedSecondRelay": true
},
"innerPeer": {
"profile": "HNS_NODE_V1",
"actualHsdPeerObjects": true,
"versionVerack": true,
"endpointAuthenticated": true,
"requesterAuthenticated": true
},
"blockTraffic": {
"hash": "6d2615113467bbee7a6ed2215e28b81f2e0210a2a383b5854fd2105d086cdb9e",
"endpointHeight": 1,
"requesterHeight": 1,
"controlNodeHeights": [
0,
0,
0,
0,
0,
0
],
"deliveredOnlyByInnerPeer": true,
"latencyMs": 3020
},
"saturation": {
"pingPackets": 1000,
"relayFrames": 8043,
"relayBytes": 107461,
"schedulerFlushes": 17,
"maximumQueuedBytes": 44129,
"queueLimitBytes": 65536,
"relayDrops": 0,
"controlReservationLatencyMs": 3082,
"admissionRequests": 72,
"admissionAccepted": 64,
"admissionRateLimited": 8
},
"relayView": {
"plaintextBlockHashObserved": false,
"transcriptSHA256": "36257106017f1ef8693af71b67fa59bc568bfefb6a32dc09fcee745b42641712"
},
"observedOpcodes": {
"RESERVE": 3,
"OFFER": 5,
"CONFIRM": 5,
"CONFIRMED": 8,
"FINDNODE": 18,
"NODES": 18,
"PUTROUTE": 8,
"PUTRESULT": 8,
"SAMPLEROUTES": 3,
"ROUTES": 73,
"RENEW": 2,
"WITHDRAW": 3,
"GETROUTE": 78,
"ERROR": 9,
"OPEN": 2,
"INCOMING": 1,
"ACCEPT": 1,
"OPENED": 1,
"DATA": 16086,
"WINDOW": 16086,
"CLOSE": 1
},
"result": "pass"
}
54 changes: 54 additions & 0 deletions lib/net/common.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,60 @@ exports.services = {
BLOOM: 1 << 1
};

/**
* Private regtest-only HNSR proof-of-concept assignments.
*
* These values are deliberately not protocol assignments and MUST NOT be
* advertised on public networks. They occupy the same experimental namespace
* as the companion DNS relay prototypes.
* @const {Number}
*/

exports.EXPERIMENTAL_HNSR_RENDEZVOUS_SERVICE = 0x04000000;
exports.EXPERIMENTAL_HNSR_RELAY_SERVICE = 0x08000000;
exports.EXPERIMENTAL_HNSR = 0xf3;

exports.services.EXPERIMENTAL_HNSR_RENDEZVOUS =
exports.EXPERIMENTAL_HNSR_RENDEZVOUS_SERVICE;
exports.services.EXPERIMENTAL_HNSR_RELAY =
exports.EXPERIMENTAL_HNSR_RELAY_SERVICE;

/**
* Regtest HNSR proof-of-concept limits.
* @enum {Number}
*/

exports.hnsr = {
VERSION: 1,
MAX_PACKET_SIZE: 65535,
MAX_RECORD_SIZE: 8192,
MAX_RECORDS_PER_KEY: 16,
MAX_STORED_RECORDS: 50000,
MAX_CONTACTS: 16,
MAX_ROUTING_CONTACTS: 2048,
MAX_FIND_QUERIES: 32,
ROUTE_REPLICATION: 8,
MIN_ROUTE_STORES: 3,
MAX_DATA_SIZE: 16384,
MAX_CIRCUIT_QUEUE: 65536,
MAX_SOCKET_QUEUE: 8 * 1000 * 1000 + 65536,
MAX_RELAY_QUEUE: 8 * 1024 * 1024,
RELAY_BURST: 32768,
MIN_WINDOW: 16384,
DEFAULT_WINDOW: 65536,
MAX_WINDOW: 1048576,
DEFAULT_TIMEOUT: 5000,
MAX_TICKET_LIFETIME: 7200,
MAX_ROUTE_LIFETIME: 7200,
MAX_CIRCUITS: 32,
MAX_RESERVATIONS: 1024,
MAX_RESERVATIONS_PER_PEER: 16,
MAX_SIGNATURE_SIZE: 80,
MAX_REQUESTS_PER_SECOND: 64,
MAX_REQUEST_BYTES_PER_SECOND: 1048576,
MAX_STORES_PER_PEER: 256
};

/**
* Our node's services (we support everything).
* @const {Number}
Expand Down
Loading