Skip to content

Commit 79026b4

Browse files
authored
Merge pull request #277 from gosuda/fix/all-path-multi-hop
refactor(discovery): unify route planning and optimize discovery lifecycle
2 parents 267c30d + 5868b8a commit 79026b4

21 files changed

Lines changed: 1072 additions & 618 deletions

File tree

cmd/portal-loadtest/main.go

Lines changed: 6 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@ import (
3333
func main() {
3434
clients := flag.Int("clients", 100, "number of synthetic clients")
3535
relays := flag.Int("relays", 5, "number of synthetic relays")
36-
multiHop := flag.Int("multi-hop", 0, "multi-hop depth (0 = priority; ≥2 = multi-hop)")
3736
flag.Parse()
3837

3938
if *clients <= 0 {
@@ -44,31 +43,6 @@ func main() {
4443
fmt.Fprintln(os.Stderr, "portal-loadtest: -relays must be > 0")
4544
os.Exit(1)
4645
}
47-
// MultiHopDepth ≤ 1 causes SelectMultiHop to return nil (see mols.go).
48-
// Reject 1 explicitly; 0 means priority mode.
49-
if *multiHop == 1 {
50-
fmt.Fprintln(os.Stderr, "portal-loadtest: -multi-hop=1 is not valid; use 0 for priority or ≥2 for multi-hop")
51-
os.Exit(1)
52-
}
53-
54-
mode := "priority"
55-
if *multiHop >= 2 {
56-
mode = "multihop"
57-
}
58-
59-
// Build K synthetic relay states. We construct discovery.RelayState values
60-
// directly (not via RelaySet.InsertAnnounced) because the public announce
61-
// path requires real EVM-signed descriptors. Selection functions are called
62-
// directly so that no signature gate runs.
63-
//
64-
// For priority mode: states without an observed descriptor (LastSeenAt zero)
65-
// are accepted into the auto pool by SelectPriority; the expiry/protocol
66-
// gates only fire when hasObservedDescriptor() is true.
67-
//
68-
// For multi-hop mode: SelectMultiHop requires hasObservedDescriptor, a
69-
// non-expired ExpiresAt, and HasOverlayPeer()==true. We populate those
70-
// fields with dummy-but-valid values using a far-future ExpiresAt and a
71-
// syntactically valid WireGuard public key placeholder.
7246
now := time.Now().UTC()
7347
relayStates := make([]discovery.RelayState, *relays)
7448
for i := range relayStates {
@@ -77,19 +51,10 @@ func main() {
7751
Descriptor: types.RelayDescriptor{
7852
APIHTTPSAddr: relayURL,
7953
},
54+
LastSeenAt: now,
8055
}
81-
if mode == "multihop" {
82-
// Populate the fields required by SelectMultiHop's eligibility
83-
// gates: hasObservedDescriptor (LastSeenAt non-zero), valid ExpiresAt,
84-
// and HasOverlayPeer() = SupportsOverlay && WireGuardPublicKey != "" &&
85-
// WireGuardPort in [1, 65535].
86-
rs.LastSeenAt = now
87-
rs.Descriptor.IssuedAt = now
88-
rs.Descriptor.ExpiresAt = now.Add(24 * time.Hour)
89-
rs.Descriptor.SupportsOverlay = true
90-
rs.Descriptor.WireGuardPublicKey = fmt.Sprintf("synthetic-wg-key-%d", i+1)
91-
rs.Descriptor.WireGuardPort = 51820
92-
}
56+
rs.Descriptor.IssuedAt = now
57+
rs.Descriptor.ExpiresAt = now.Add(24 * time.Hour)
9358
relayStates[i] = rs
9459
}
9560

@@ -98,16 +63,8 @@ func main() {
9863
// would make all clients pick identically, falsely appearing as 100% imbalance.
9964
picks := make(map[string]int, *relays) // relay URL → count of clients that picked it first
10065
for i := 0; i < *clients; i++ {
101-
cs := discovery.RouteState{
102-
LocalAddress: fmt.Sprintf("synthetic-client-%d", i),
103-
MultiHopDepth: *multiHop,
104-
}
105-
var outputURLs []string
106-
if mode == "multihop" {
107-
outputURLs = discovery.SelectMultiHop(relayStates, cs)
108-
} else {
109-
outputURLs = discovery.SelectPriority(relayStates, cs)
110-
}
66+
localAddr := fmt.Sprintf("synthetic-client-%d", i)
67+
outputURLs := discovery.RankRelayPool(relayStates, localAddr)
11168
if len(outputURLs) == 0 {
11269
// All relays were filtered; skip this client.
11370
continue
@@ -139,7 +96,7 @@ func main() {
13996
pval := igamc(float64(df)/2.0, chi2/2.0)
14097

14198
// Print results.
142-
header := fmt.Sprintf("portal-loadtest: N=%d clients, K=%d relays, mode=%s", *clients, *relays, mode)
99+
header := fmt.Sprintf("portal-loadtest: N=%d clients, K=%d relays, mode=mols", *clients, *relays)
143100
fmt.Println(header)
144101
fmt.Printf("%-45s %6s %8s\n", "relay", "picks", "expected")
145102
fmt.Println("---------------------------------------------------------------")

cmd/portal-tunnel/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,9 +99,9 @@ Common `portal expose` flags:
9999
--name Public hostname prefix; auto-generated when omitted
100100
--relays Additional relay API URLs, comma-separated
101101
--discovery Include registry relays and relay discovery expansion
102-
--max-active-relays Maximum auto-selected relays
102+
--max-active-relays Maximum auto-selected single-hop relays; multi-hop uses every eligible relay as an entry
103103
--multi-hop Ordered multi-hop relay API URLs, comma-separated
104-
--multi-hop-depth Automatically select one multi-hop route with this hop count
104+
--multi-hop-depth Automatically create this-depth multi-hop routes for every eligible entry relay
105105
--ban-mitm Ban relay when the MITM self-probe detects termination
106106
--identity-path Identity JSON file path; created automatically when missing
107107
--identity-json Identity JSON payload; overrides --identity-path when set

cmd/portal-tunnel/main.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,8 +97,8 @@ func runExposeCommand(args []string) error {
9797
utils.BoolFlagEnv(fs, &flags.udp, "udp", false, "Enable public UDP relay in addition to the default TCP relay", "UDP_ENABLED")
9898
utils.StringFlagEnv(fs, &flags.udpAddr, "udp-addr", "", "Local UDP target address for relayed datagrams (host:port or port only); defaults to the target when --udp is enabled", "UDP_ADDR")
9999
utils.BoolFlagEnv(fs, &flags.tcp, "tcp", false, "Request a dedicated TCP port on the relay for raw TCP services (no TLS; e.g., Minecraft, game servers)", "TCP_ENABLED")
100-
utils.IntFlagEnv(fs, &flags.maxActiveRelays, "max-active-relays", 3, nil, "Maximum number of auto-selected relays to keep connected; explicit --relays are always included", "MAX_ACTIVE_RELAYS")
101-
utils.IntFlagEnv(fs, &flags.multiHopDepth, "multi-hop-depth", 0, nil, "Automatically select one multi-hop route with this hop count; 0 or 1 disables multi-hop", "MULTI_HOP_DEPTH")
100+
utils.IntFlagEnv(fs, &flags.maxActiveRelays, "max-active-relays", 3, nil, "Maximum auto-selected single-hop relays to keep connected; multi-hop uses every eligible relay as an entry", "MAX_ACTIVE_RELAYS")
101+
utils.IntFlagEnv(fs, &flags.multiHopDepth, "multi-hop-depth", 0, nil, "Automatically create multi-hop routes at this hop count for every eligible entry relay; 0 or 1 disables multi-hop", "MULTI_HOP_DEPTH")
102102
utils.StringFlag(fs, &flags.metricsAddr, "metrics-addr", "", "Optional address (host:port) to serve Prometheus /metrics. Empty = disabled.")
103103

104104
if err := utils.ParseFlagSet(fs, args, printExposeUsage); err != nil {

docs/src/routes/cli-reference/+page.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -87,9 +87,9 @@ not supported.
8787
|------|------|---------|-------------|
8888
| `--relays` | string | registry | Additional relay API URLs, comma-separated |
8989
| `--discovery` | bool | `true` | Include registry relays and relay discovery expansion |
90-
| `--max-active-relays` | int | `3` | Maximum auto-selected relays to keep connected; explicit relays are always included |
90+
| `--max-active-relays` | int | `3` | Maximum auto-selected single-hop relays to keep connected; multi-hop uses every eligible relay as an entry; explicit relays are always included |
9191
| `--multi-hop` | string | | Ordered multi-hop relay API URLs, comma-separated |
92-
| `--multi-hop-depth` | int | `0` | Automatically select one multi-hop route with this hop count; `0` or `1` disables multi-hop |
92+
| `--multi-hop-depth` | int | `0` | Automatically create this-depth multi-hop routes for every eligible entry relay; `0` or `1` disables multi-hop |
9393
| `--ban-mitm` | bool | `false` | Ban relay when the MITM self-probe detects TLS termination |
9494
| `--identity-path` | string | `identity.json` | Identity JSON file path; created automatically when missing |
9595
| `--identity-json` | string | | Identity JSON payload; overrides `--identity-path` contents and is persisted there when both are set |
@@ -167,7 +167,7 @@ Use an explicit multi-hop route:
167167
portal expose 3000 --multi-hop https://entry.example.com,https://exit.example.com
168168
```
169169

170-
Ask Portal to select one three-hop route:
170+
Ask Portal to create three-hop routes for every eligible entry relay:
171171

172172
```bash
173173
portal expose 3000 --multi-hop-depth 3

docs/src/routes/configuration/+page.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -141,8 +141,8 @@ The `portal expose` subcommand accepts the following flags. Flags that read from
141141
| `--relays` | | string | _(registry)_ | Additional Portal relay server API URLs (comma-separated; scheme omitted defaults to https) |
142142
| `--discovery` | | bool | `true` | Include public registry relays and discover additional relay bootstraps |
143143
| `--multi-hop` | `MULTI_HOP` | string | | Ordered multi-hop relay API URLs, comma-separated |
144-
| `--multi-hop-depth` | `MULTI_HOP_DEPTH` | int | `0` | Automatically select one multi-hop route with this hop count; 0 or 1 disables multi-hop |
145-
| `--max-active-relays` | `MAX_ACTIVE_RELAYS` | int | `3` | Maximum auto-selected relays to keep connected; explicit relays are always included |
144+
| `--multi-hop-depth` | `MULTI_HOP_DEPTH` | int | `0` | Automatically create this-depth multi-hop routes for every eligible entry relay; 0 or 1 disables multi-hop |
145+
| `--max-active-relays` | `MAX_ACTIVE_RELAYS` | int | `3` | Maximum auto-selected single-hop relays to keep connected; multi-hop uses every eligible relay as an entry; explicit relays are always included |
146146
| `--ban-mitm` | `BAN_MITM` | bool | `false` | Ban relay when the MITM self-probe detects TLS termination |
147147

148148
### Identity
@@ -258,7 +258,7 @@ Tunnel fields mirror `portal expose` flags:
258258
| `relays` | string array | Explicit relay API URLs |
259259
| `discovery` | bool | Include registry and relay discovery expansion |
260260
| `multi_hop` | string array | Ordered multi-hop relay path |
261-
| `multi_hop_depth` | int | Automatically select one multi-hop route with this depth |
261+
| `multi_hop_depth` | int | Automatically create this-depth multi-hop routes for every eligible entry relay |
262262
| `identity_path` | string | Tunnel identity JSON file path. When omitted, one tunnel uses the platform default `identity.json`; multiple tunnels use `<state-dir>/<tunnel-id>/identity.json` |
263263
| `identity_json` | string | Identity JSON payload; overrides `identity_path` contents and is persisted there when both are set |
264264
| `udp`, `udp_addr`, `tcp` | bool/string | UDP and raw TCP relay options |

docs/src/routes/portal-agent/+page.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ tunnel or `Routes` for routed HTTP. Routes use this syntax:
168168
Each entry is `PATH=UPSTREAM [METHOD[,METHOD...]:USDC_AMOUNT]`. Fill `X402 Pay
169169
To` when any route has an amount, and set `X402 Testnet` to `true` for Sui
170170
testnet. The form also accepts explicit `Relays`,
171-
`Discovery`, and `Max Relays`; max relays caps auto-selected discovery relays
171+
`Discovery`, and `Max Relays`; max relays caps auto-selected single-hop discovery relays
172172
while explicit relays are still included.
173173

174174
After creation, routed HTTP paths, x402 payment amounts, payment network, and
@@ -191,7 +191,7 @@ Common fields:
191191
| `http_routes` | Routed HTTP mappings; cannot be combined with `target` or `udp` |
192192
| `relays` | Explicit relay API URLs |
193193
| `discovery` | Include registry and relay discovery expansion |
194-
| `max_active_relays` | Maximum auto-selected relays kept connected |
194+
| `max_active_relays` | Maximum auto-selected single-hop relays kept connected; multi-hop uses every eligible relay as an entry |
195195
| `identity_path` | Tunnel identity JSON path |
196196
| `identity_json` | Identity JSON payload; persisted to `identity_path` when both are set |
197197
| `udp`, `udp_addr` | UDP transport settings |

portal/discovery/announce_test.go

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,20 @@ func TestInsertAnnouncedRejectsUnsigned(t *testing.T) {
6767
signing := mustSigningIdentity(t)
6868
now := time.Now().UTC().Truncate(time.Microsecond)
6969
desc := mustUnsignedDescriptor(t, signing, "https://relay-unsigned.example")
70-
if err := set.InsertAnnounced(desc, now); err == nil {
71-
t.Fatal("expected unsigned reject")
70+
err := set.InsertAnnounced(desc, now)
71+
if err == nil || err.Error() != "relay descriptor is not signed" {
72+
t.Fatalf("InsertAnnounced() error = %v, want unsigned descriptor error", err)
73+
}
74+
}
75+
76+
func TestInsertAnnouncedRejectsExpired(t *testing.T) {
77+
set := NewRelaySet(nil)
78+
signing := mustSigningIdentity(t)
79+
now := time.Now().UTC().Truncate(time.Microsecond)
80+
desc := mustSignedDescriptor(t, signing, "https://relay-expired.example", now.Add(-DiscoveryDescriptorTTL-time.Second))
81+
err := set.InsertAnnounced(desc, now)
82+
if err == nil || err.Error() != "relay descriptor already expired" {
83+
t.Fatalf("InsertAnnounced() error = %v, want expired descriptor error", err)
7284
}
7385
}
7486

0 commit comments

Comments
 (0)