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:
@@ -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 |
|
||||
|
||||
Reference in New Issue
Block a user