Skip to content

smite: add BOLT 4 onion routing (Sphinx) - #170

Open
erickcestari wants to merge 1 commit into
lnfuzz:masterfrom
erickcestari:bolt4-onion
Open

smite: add BOLT 4 onion routing (Sphinx)#170
erickcestari wants to merge 1 commit into
lnfuzz:masterfrom
erickcestari:bolt4-onion

Conversation

@erickcestari

Copy link
Copy Markdown
Contributor

Adds an onion module that builds and peels BOLT 4 packets.

  • keys: shared secrets and the rho/mu/um/pad/ammag/ammagext key derivation, plus the zero-nonce ChaCha20 stream.
  • payload: the HopPayload TLV (types 2, 4, 6, 8, 10, 12, 16, 18), with forward() and receive() constructors.
  • packet: OnionBuilder for the 1366-byte packet, and peel for the reverse direction.

Verified against onion-test.json and onion-error-test.json from the BOLT repository: the reference packet is reproduced byte for byte, peels at every hop, and the derived shared secrets and keys match.

What this enables today: constructing a spec-valid onion for any route and decrypting one addressed to us. Malformed onions are reachable through OnionBuilder::raw_hop, which accepts the reserved payload lengths 0 and 1, non-TLV bytes, and oversized payloads, and through the public fields of OnionPacket.

Deferred to follow-ups:

  • Failure decryption. KeyType::Ammag and Um are derived here but unused; obfuscating a reason, recovering the erring hop, and the failure codes are not implemented, so update_fail_htlc.reason stays opaque.
  • Attribution data crypto. The wire type already exists in bolt::attribution_data; the ammagext stream and the 210 truncated HMACs over it do not.
  • Route blinding. HopPayload carries encrypted_recipient_data and current_path_key, and peel accepts a path key, but there is no blinded path builder or encrypted_data TLV to produce either side.
  • Onion messages (type 513), which use a variable-size packet.

Depends on #166

@ekzyis

ekzyis commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

#166 was merged, so I think this can be rebased on master now

Adds an `onion` module that builds and peels BOLT 4 packets.

- `keys`: shared secrets and the rho/mu/um/pad/ammag/ammagext key
  derivation, plus the zero-nonce ChaCha20 stream.
- `payload`: the `HopPayload` TLV (types 2, 4, 6, 8, 10, 12, 16, 18),
  with `forward()` and `receive()` constructors.
- `packet`: `OnionBuilder` for the 1366-byte packet, and `peel` for the
  reverse direction.

Verified against `onion-test.json` and `onion-error-test.json` from the
BOLT repository, vendored verbatim: the reference packet is reproduced
byte for byte, peels at every hop, and the derived shared secrets and
keys match.

What this enables today: constructing a spec-valid onion for any route
and decrypting one addressed to us. Malformed onions are reachable
through `OnionBuilder::raw_hop`, which accepts the reserved payload
lengths 0 and 1, non-TLV bytes, and oversized payloads, and through the
public fields of `OnionPacket`.

Deferred to follow-ups:

- Failure decryption. `KeyType::Ammag` and `Um` are derived here but
  unused; obfuscating a `reason`, recovering the erring hop, and the
  failure codes are not implemented, so `update_fail_htlc.reason` stays
  opaque.
- Attribution data crypto. The wire type already exists in
  `bolt::attribution_data`; the `ammagext` stream and the 210 truncated
  HMACs over it do not.
- Route blinding. `HopPayload` carries `encrypted_recipient_data` and
  `current_path_key`, and `peel` accepts a path key, but there is no
  blinded path builder or `encrypted_data` TLV to produce either side.
- Onion messages (type 513), which use a variable-size packet.
@erickcestari

Copy link
Copy Markdown
Contributor Author

#166 was merged, so I think this can be rebased on master now

Thanks for the reminder. I've rebased it and dropped the old tu32/tu64 commit.

Comment thread smite/src/onion/packet.rs

/// [`PAYMENT_HOP_PAYLOADS_SIZE`] as a `u64`, to compare against wire-declared
/// lengths.
const PAYMENT_HOP_PAYLOADS_CAPACITY: u64 = PAYMENT_HOP_PAYLOADS_SIZE as u64;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This wasn't used for any comparisons, but only for returning errors (OnionError::PayloadsTooLong), so we could use PAYMENT_HOP_PAYLOADS_SIZE as u64 in those places as well, similar to the needed as u64 used just before it

Comment thread smite/src/onion/packet.rs
impl OnionPacket {
/// Encodes the packet to its 1366-byte wire representation.
#[must_use]
pub fn encode(&self) -> Vec<u8> {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OnionPacket will only be used in UpdateAddHtlc, Considering that, do we need both encode/decode and a WireFormat impl for it?

Can we define only one of them? I think if we define this as a type in UpdateAddHtlc, then we could use WireFormat for consistency. But, I don't think defining a separate type is a good idea, since we'd then have to handle that type throughout the IR as well, when we could simply use Vec to send an arbitrary onion packet. So I think we'll only use this internally, using encode/decode makes more sense to me. WDYT?

@erickcestari erickcestari Aug 24, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it doesn't make much sense to send an arbitrary onion packet with random bytes. I think a more appropriate approach would be:

#111 (comment)

If we chose to follow this, it would make sense to define OnionPacket as a type in UpdateAddHtlc.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apologies for not being clear, what I meant is that we definitely need OnionPacket as a struct to create a valid onion, but we definitely want to leave room for sending malformed onions as well (possibly using #178). So, in UpdateAddHtlc, we will keep the onion packet as a Vec only I think

What I meant earlier was that we now have two ways to encode/decode OnionPacket, and it would be better to have only one. Implementing encode/decode makes more sense to me than implementing WireFormat for it

Comment thread smite/src/onion/packet.rs
/// the packet that reaches hop `k` is fully determined by the shared secrets of
/// hops `0..k`. The sender must reproduce it or the HMACs will not match.
fn generate_filler(shared_secrets: &[SharedSecret], hops: &[Hop]) -> Vec<u8> {
debug_assert!(!hops.is_empty(), "build rejects empty routes");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caller already rejects empty hops before calling this, so do we really need an assert here? Also, debug_assert! won't be useful for us since we are running the campaign in a release build

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right. The idea was to test the invariant when running the tests. However, I'll remove those since the unit tests should already cover it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants