expand onion routing design: DAG structure, multi-path payments, multi-destination support, new onion packet format, settlement modes, and anonymity properties

This commit is contained in:
2025-11-30 22:46:22 +00:00
parent 0865c21689
commit c2965f5c41

View File

@@ -1758,141 +1758,407 @@ Failed payments immediately update local routing table and optionally broadcast
### DAG-Structured Onion Routing
Payments use onion-encrypted routing with a directed acyclic graph (DAG) structure rather than a single linear path. Multiple parallel paths propagate the payment attempt, but only one path settles—the first to succeed claims the preimage and cancels all others.
Payments use onion-encrypted routing with a directed acyclic graph (DAG) structure rather than a single linear path. The onion format supports:
- Multiple parallel paths for redundancy (first-wins settlement)
- Multiple destinations for split payments (multi-party settlement)
- Embedded branch and join instructions within uniform-size onion packets
#### Multi-Path Onion Structure
#### Uniform Onion Packet Size
All onion packets use a fixed 8 KB (8192 bytes) size regardless of path complexity:
```
dag_onion = {
payment_hash: H(preimage)
total_amount: full payment amount
paths: [
{path_id: 0, encrypted_route: onion_0, amount_fraction: 1.0},
{path_id: 1, encrypted_route: onion_1, amount_fraction: 1.0},
{path_id: 2, encrypted_route: onion_2, amount_fraction: 1.0},
]
settlement_mode: "first_wins" // or "proportional" for split payments
ONION_PACKET_SIZE = 8192 bytes
onion_packet = {
version: 1 byte
ephemeral_pubkey: 33 bytes
encrypted_payload: 8126 bytes
hmac: 32 bytes
}
```
Each path carries an onion-encrypted route capable of settling the full payment. The sender initiates all paths simultaneously.
The large uniform size accommodates:
- Up to 32 hops in a linear path
- Embedded branch instructions (split to multiple next-hops)
- Embedded join instructions (reconvergence points)
- Multiple terminal destinations with separate amounts
- Random padding indistinguishable from real routing data
#### Onion Layer Construction
**Size rationale:**
Each hop in each path receives an encrypted layer revealing only:
- Next hop (or final destination marker)
- Amount to forward
- Path identifier
- Expiry delta
- Encrypted payload for next hop
| Routing Complexity | Approximate Payload Required |
|--------------------|------------------------------|
| Simple 10-hop linear | ~2 KB |
| 3-path DAG, 10 hops each | ~4 KB |
| Multi-destination (5 recipients) | ~3 KB |
| Complex DAG with 3 destinations | ~6 KB |
| Maximum (32 hops, 8 destinations, full DAG) | ~8 KB |
The 8 KB size provides headroom for complex payment structures while remaining efficient for simple payments (excess is random padding).
#### Onion Layer Structure
Each layer contains routing instructions that may specify linear forwarding, branching, or termination:
```
onion_layer = {
next_hop: pubkey of next node (or null if final)
path_id: which DAG path this layer belongs to
forward_amount: amount to pass to next hop
expiry: absolute block height deadline
inner_onion: encrypted payload for next hop
padding: random bytes to fixed size (prevents path length leakage)
layer_type: enum { FORWARD, BRANCH, JOIN, TERMINAL }
payload: type-specific data
inner_onion: encrypted remainder (variable size within packet)
padding: random bytes to maintain fixed packet size
}
encrypted_layer = encrypt(
shared_secret(sender_ephemeral, hop_pubkey),
onion_layer
)
// FORWARD: Simple single next-hop
forward_payload = {
next_hop: pubkey of next node
forward_amount: amount to pass
expiry: block height deadline
path_id: DAG path identifier
}
// BRANCH: Split to multiple next-hops (DAG divergence)
branch_payload = {
branches: [
{
next_hop: pubkey of branch destination
branch_amount: amount for this branch
expiry: block height deadline
branch_onion: encrypted onion for this branch (embedded)
branch_id: unique identifier for this branch
},
...
]
branch_mode: enum { ALL_REQUIRED, FIRST_WINS, THRESHOLD(n) }
}
// JOIN: Reconvergence point expecting multiple incoming branches
join_payload = {
expected_branches: [branch_id, ...]
forward_amount: total to forward after join
next_hop: pubkey of next node after join
timeout: blocks to wait for all branches
}
// TERMINAL: Final destination
terminal_payload = {
destination: pubkey of recipient
amount: payment amount for this destination
payment_hash: H(preimage) for this destination's HTLC
payment_data: encrypted memo/invoice data (optional)
}
```
**Path-specific ephemeral keys:**
#### Multi-Destination Payments
Each path uses independent ephemeral key derivation:
```
path_ephemeral[i] = sender_private × hash("path" || payment_hash || i)
```
This ensures that even if one path is compromised, other paths reveal no information about the sender.
#### DAG Topology
The DAG structure allows paths to diverge and reconverge:
A single payment can terminate at multiple destinations on different DAG branches:
```
Multi-destination payment structure:
Sender A pays:
- 50 tokens to Destination G
- 30 tokens to Destination H
- 20 tokens to Destination I
┌───B───────────→ G (50 tokens)
A ──── BRANCH ──┼───C───D───────→ H (30 tokens)
└───E───F───────→ I (20 tokens)
```
**Multi-destination onion construction:**
```
multi_dest_onion = {
payment_id: unique payment identifier
total_amount: sum of all destination amounts
destinations: [
{dest: G, amount: 50, payment_hash: H(preimage_G)},
{dest: H, amount: 30, payment_hash: H(preimage_H)},
{dest: I, amount: 20, payment_hash: H(preimage_I)},
]
settlement_mode: "all_required" | "threshold(n)" | "any"
}
```
Each destination receives a separate HTLC with its own payment_hash. Settlement modes:
| Mode | Behavior |
|------|----------|
| all_required | All destinations must receive and settle; any failure cancels all |
| threshold(n) | At least n destinations must settle; others may fail |
| any | Each destination settles independently |
#### Branch Embedding
Branch instructions are embedded within the onion layer, with each branch containing its own encrypted sub-onion:
```
function construct_branch_layer(branches, shared_secret):
branch_payloads = []
for branch in branches:
// Construct sub-onion for this branch
sub_onion = construct_onion(branch.path, branch.ephemeral)
// Embed sub-onion in branch payload
branch_payloads.append({
next_hop: branch.first_hop,
branch_amount: branch.amount,
expiry: branch.expiry,
branch_onion: sub_onion, // Full 8KB embedded
branch_id: branch.id
})
// Encrypt branch layer
layer = {
layer_type: BRANCH,
payload: {
branches: branch_payloads,
branch_mode: payment.branch_mode
}
}
return encrypt(shared_secret, layer)
```
**Branch processing by intermediate node:**
```
function process_branch_layer(encrypted_layer, node_private_key):
// Decrypt layer
shared_secret = ECDH(node_private_key, ephemeral_pubkey)
layer = decrypt(shared_secret, encrypted_layer)
if layer.layer_type != BRANCH:
return process_other_layer_type(layer)
// Process each branch
for branch in layer.payload.branches:
// Extract embedded sub-onion
sub_onion = branch.branch_onion
// Create HTLC for this branch
create_htlc(
next_hop: branch.next_hop,
amount: branch.branch_amount,
expiry: branch.expiry,
onion: sub_onion
)
// Track branches for potential join
register_pending_branches(layer.payload.branches)
```
#### Join Points (Reconvergence)
Join points aggregate multiple incoming branches before forwarding:
```
Join point processing:
B ──┐
A ── BRANCH ────┼──→ F (JOIN) ──→ G (destination)
D ──┘
Node F receives:
- HTLC from B with branch_id=0, amount=60
- HTLC from D with branch_id=1, amount=40
F's join layer specifies:
expected_branches: [0, 1]
forward_amount: 100
next_hop: G
```
**Join processing:**
```
function process_join(join_layer, incoming_htlcs):
expected = set(join_layer.expected_branches)
received = set(htlc.branch_id for htlc in incoming_htlcs)
if received != expected:
if timeout_reached:
// Cancel all received HTLCs
for htlc in incoming_htlcs:
cancel_htlc(htlc, reason="incomplete_join")
return FAIL
// Wait for remaining branches
return PENDING
// All branches received, forward aggregated payment
total = sum(htlc.amount for htlc in incoming_htlcs)
if total < join_layer.forward_amount:
// Insufficient funds across branches
cancel_all(incoming_htlcs, reason="insufficient_join_amount")
return FAIL
// Create forwarding HTLC
create_htlc(
next_hop: join_layer.next_hop,
amount: join_layer.forward_amount,
expiry: min(htlc.expiry for htlc in incoming_htlcs) - safety_margin,
onion: join_layer.inner_onion
)
return SUCCESS
```
#### Multi-Path Redundancy with Multi-Destination
Combining redundant paths with multiple destinations:
```
Payment: A pays 100 to G and 50 to H
Path redundancy for G:
┌───B───┐
│ │
A───┼───C───┼───F───G (destination)
A───┼───C───┼───F───G (100 tokens, first-wins)
│ │
└───D───E
───────────
│ JOIN
A───┴───D───E───────→ H (50 tokens)
Path 0: A → B → F → G
Path 1: A → C → F → G
Path 2: A → D → E → G
Construction:
- Paths to G (via B→F and C→F) use same payment_hash, first-wins
- Path to H uses different payment_hash
- All embedded in single 8KB onion from A
```
**Advantages:**
- If B fails, paths 1 and 2 can still succeed
- Intermediate node F receives from multiple sources but only forwards once
- Reconvergence points (F) reduce total locked liquidity
#### Onion Padding Strategy
#### First-Wins Settlement
When the destination receives HTLCs from multiple paths for the same payment_hash:
Random padding is cryptographically indistinguishable from encrypted routing data:
```
settlement_protocol = {
1. Destination receives HTLC on path 0
2. Destination receives HTLC on path 1
3. Destination receives HTLC on path 2
4. Destination chooses ONE path to settle (e.g., lowest fees, first arrival)
5. Destination reveals preimage ONLY to chosen path
6. Chosen path settles backward (HTLCs fulfilled)
7. Other paths timeout and cancel (HTLCs expire)
function pad_onion(payload, target_size):
current_size = len(payload)
padding_needed = target_size - current_size
// Generate deterministic-looking random padding
// (seeded from shared secret so it appears encrypted)
padding = generate_pseudo_random_bytes(
seed: hash(shared_secret || "padding"),
length: padding_needed
)
return payload || padding
function generate_pseudo_random_bytes(seed, length):
// Use ChaCha20 in counter mode with the seed
output = []
for i in 0..(length / 64):
block = ChaCha20(key=seed[0:32], nonce=i, input=zeros(64))
output.append(block)
return output[0:length]
```
**Indistinguishability properties:**
| Property | Guarantee |
|----------|-----------|
| Padding vs payload | Encrypted padding indistinguishable from encrypted routing |
| Simple vs complex | 10-hop linear looks identical to 32-hop DAG |
| Single vs multi-dest | Cannot determine number of destinations |
| Branch points | Cannot identify which nodes perform branching |
#### Path-Specific Ephemeral Keys
Each branch in the DAG uses independent ephemeral key derivation:
```
// Root ephemeral for payment
root_ephemeral = random_scalar()
// Derive branch-specific ephemeral keys
branch_ephemeral[branch_id] = root_ephemeral × hash(
"branch" || payment_id || branch_id || root_ephemeral × G
)
// Each branch's path uses its ephemeral independently
for hop in branch.path:
ephemeral_pubkey[hop] = derive_hop_ephemeral(branch_ephemeral[branch_id], hop)
```
This ensures:
- Branches cannot be correlated by ephemeral key analysis
- Compromise of one branch reveals nothing about others
- Join points cannot link incoming branches except by branch_id
#### Settlement Modes
**First-wins (redundant paths to single destination):**
```
first_wins_settlement = {
paths: [path_0, path_1, path_2]
destination: G
amount: 100
payment_hash: H(preimage)
Settlement:
- First path to deliver HTLC wins
- Destination reveals preimage to winning path only
- Other paths timeout and cancel
}
```
**Cancellation propagation:**
**All-required (multi-destination atomic):**
```
all_required_settlement = {
destinations: [
{dest: G, amount: 50, hash: H(preimage_G)},
{dest: H, amount: 30, hash: H(preimage_H)},
]
Settlement:
- All destinations must receive their HTLCs
- Preimages revealed only if all destinations ready
- Any failure cancels entire payment
- Uses hash chain: preimage_G = hash(preimage_H || secret)
}
```
**Threshold (partial success acceptable):**
```
threshold_settlement = {
destinations: [G, H, I, J]
threshold: 3
amounts: [25, 25, 25, 25]
Settlement:
- At least 3 of 4 destinations must succeed
- Successful destinations settle independently
- Failed destinations timeout
- Sender accepts partial delivery
}
```
#### Cancellation Propagation
```
cancel_signal = {
payment_hash: the payment being cancelled
path_id: which path to cancel
reason: "alternative_path_settled"
signature: sender or intermediate hop signature
payment_id: payment being cancelled
branch_id: specific branch to cancel (or ALL)
reason: enum { PATH_SETTLED, TIMEOUT, INSUFFICIENT_CAPACITY, JOIN_FAILED }
signature: proof of authority to cancel
}
Propagation:
1. Cancel signal sent backwards along branch
2. Each hop releases locked capacity
3. At branch points, cancel propagates to all sub-branches
4. At join points, cancel waits for all incoming branches
```
Nodes receiving cancel_signal immediately release locked capacity without waiting for HTLC timeout. This is an optimization—security does not depend on it since HTLCs will timeout regardless.
#### Onion Encryption Scheme
Using ECDH with ephemeral keys and ChaCha20-Poly1305:
```
For each hop i in path:
ephemeral_pubkey[i] = ephemeral_private × G (for i=0)
= ephemeral_pubkey[i-1] × blinding_factor[i-1]
shared_secret[i] = ECDH(ephemeral_private, hop_pubkey[i])
= hop_private[i] × ephemeral_pubkey[i]
blinding_factor[i] = hash("blind" || ephemeral_pubkey[i] || shared_secret[i])
rho[i] = hash("rho" || shared_secret[i]) // encryption key
mu[i] = hash("mu" || shared_secret[i]) // MAC key
encrypted_payload[i] = ChaCha20(rho[i], onion_layer[i])
mac[i] = HMAC-SHA256(mu[i], encrypted_payload[i])
```
**Fixed-size onions:**
All onion packets are fixed size (e.g., 1300 bytes) regardless of path length. Each hop:
1. Decrypts their layer
2. Strips their portion
3. Pads the end with random bytes
4. Forwards the still-fixed-size packet
This prevents intermediate nodes from learning path position.
Cancellation is an optimization—HTLCs will timeout regardless, but prompt cancellation improves capital efficiency.
#### Path Selection Algorithm
@@ -1999,8 +2265,11 @@ If any path fails, the destination cannot reconstruct the preimage, and all path
| Property | Guarantee |
|----------|-----------|
| Sender anonymity | Intermediate hops cannot identify sender |
| Receiver anonymity | Intermediate hops cannot identify receiver |
| Path length | Hidden by fixed-size onions |
| Multi-path correlation | Paths use independent ephemeral keys |
| Amount | Each hop only sees forwarding amount |
| Receiver anonymity | Intermediate hops cannot identify receiver (even with multi-dest) |
| Path length | Hidden by 8 KB uniform onion size |
| Path complexity | Simple linear indistinguishable from complex DAG |
| Multi-path correlation | Branches use independent ephemeral keys |
| Multi-destination hiding | Cannot determine number of destinations |
| Branch point hiding | Cannot identify which nodes perform branching |
| Amount per destination | Only final destination sees its amount |
| Payment linkage | Different payment_hash per attempt prevents linking retries |