Skip to content

Switch chacha#4360

Open
Abeeujah wants to merge 2 commits intolightningdevkit:mainfrom
Abeeujah:switch-chacha
Open

Switch chacha#4360
Abeeujah wants to merge 2 commits intolightningdevkit:mainfrom
Abeeujah:switch-chacha

Conversation

@Abeeujah
Copy link

closes #4316

This PR switches the ChaChaPoly encryption algorithm to the chacha20-poly1305 = "0.1.2" crate, the following impls have been modified

  • EncryptedOurPeerStorage
  • PeerChannelEncryptor

The chacha20-poly1305 dependency was only added because the issue description by @TheBlueMatt requested it be added, and also, thanks to @tnull for the reference implementation provided.

@ldk-reviews-bot
Copy link

ldk-reviews-bot commented Jan 29, 2026

👋 I see @TheBlueMatt was un-assigned.
If you'd like another reviewer assignment, please click here.

Copy link
Collaborator

@TheBlueMatt TheBlueMatt left a comment

Choose a reason for hiding this comment

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

You should try to remove the existing chacha20poly1305rfc module entirely. Is there some limitations of the upstream implementation that prevented that?

@TheBlueMatt TheBlueMatt removed the request for review from valentinewallace January 29, 2026 18:15
@Abeeujah
Copy link
Author

@TheBlueMatt at the moment, streams.rs lightning/src/crypto/streams.rs still depends on it, I'm currently working on switching it to the chacha20-poly1305 crate as well.

Copy link
Collaborator

@TheBlueMatt TheBlueMatt left a comment

Choose a reason for hiding this comment

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

Looks like you still didn't manage to remove chacha20poly1305rfc.rs (and chacha20.rs and poly1305.rs)?

@ldk-reviews-bot
Copy link

👋 The first review has been submitted!

Do you think this PR is ready for a second reviewer? If so, click here to assign a second reviewer.

@Abeeujah Abeeujah requested a review from TheBlueMatt January 30, 2026 21:30
struct ChaChaDualPolyReader<'a, R: Read> {
chacha: &'a mut ChaCha20,
poly: &'a mut Poly1305,
poly_aad: &'a mut Poly1305,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Nah, we definitely don't want to do the full poly1305 validation twice. We'll have to upstream a Clone for Poly1305, mind opening a PR upstream at https://github.com/rust-bitcoin/rust-bitcoin/?

apoelstra added a commit to rust-bitcoin/rust-bitcoin that referenced this pull request Feb 25, 2026
…ctures

0df1825 dev: implement standard traits for ChaCha20-Poly1305 data types (Abeeujah)

Pull request description:

  In `rust-lightning`, `ChaCha20-Poly1305` encryption is being refactored from the local `ChaCha20Poly1305RFC` to the `chacha20-poly1305` crate.
  
  Previous local implementation had support for streaming in `ChaCha20Poly1305RFC` implementations, but the `chacha20-poly1305` doesn't have implementations that supports streaming, this leads to using the `ChaCha20` and `Poly1305` primitives.
  
  To avoid computing `Poly1305` validation twice for input and additional authenticated data, it would be cool if `Poly1305` implements `Clone`, which saves computing the MAC twice.
  
  [Downstream PR](lightningdevkit/rust-lightning#4360 (comment))


ACKs for top commit:
  tcharding:
    ACK 0df1825
  apoelstra:
    ACK 0df1825; successfully ran local tests


Tree-SHA512: eae30d8f996d525a2891e0264fdb1996accaf3c57530f5d0134f63425d1bda82ecbd4112678d91892bd15079ae36db61109beccd7c93d5dc914b8284b30c968f
@Abeeujah
Copy link
Author

Abeeujah commented Mar 1, 2026

GM @TheBlueMatt upstream PR has been merged, I've pointed the crate to the git rev pending when a newer crate is released, although it doesn't look like the module supports fuzzing, I'd love to get your review on the next course of action

@TheBlueMatt
Copy link
Collaborator

TheBlueMatt commented Mar 2, 2026

Oh good point yea, can you look into upstreaming our fuzzing logic as well? Could use a different cfg flag.

@TheBlueMatt TheBlueMatt removed their request for review March 2, 2026 14:50
@Abeeujah Abeeujah force-pushed the switch-chacha branch 3 times, most recently from 1aa98b9 to a1dbdb8 Compare March 17, 2026 18:54
bech32 = { version = "0.11.0", default-features = false }
bitcoin = { version = "0.32.4", default-features = false, features = ["secp-recovery"] }

chacha20-poly1305 = { git = "https://github.com/Abeeujah/rust-bitcoin.git", rev = "fdf898d8651dd876fc36ee77da31350ed35349f9", default-features = false }
Copy link
Collaborator

Choose a reason for hiding this comment

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

Issue: Git dependency prevents crates.io publishing

crates.io does not allow git dependencies. Since the lightning crate is published to crates.io, this dependency must come from a published crate before this can be merged/released. Using git + rev is fine for development/testing, but this is a release blocker.

Is there a timeline for the chacha20-poly1305 crate from rust-bitcoin being published to crates.io?

pub(crate) mod chacha20poly1305rfc;
pub(crate) mod poly1305;
pub(crate) mod streams;
pub(crate) mod utils;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Potential issue: Fuzzing support regression

The deleted chacha20.rs and poly1305.rs files contained #[cfg(fuzzing)] no-op implementations:

  • Fuzzing ChaCha20::process_in_place was a no-op (data passed through unencrypted)
  • Fuzzing Poly1305 ignored all input and returned the first 16 bytes of the key as the tag

These no-ops allowed fuzzers (e.g., peer_crypt, full_stack, chanmon_consistency) to exercise protocol logic without needing to produce valid ciphertexts/MACs.

Does the external chacha20-poly1305 crate provide equivalent #[cfg(fuzzing)] no-op implementations? If not, fuzzers that rely on transparent crypto will be broken — the handshake MAC checks in PeerChannelEncryptor will almost always fail on arbitrary fuzz input, preventing exploration of post-handshake code paths.

The #[cfg(fuzzing)] blocks preserved in streams.rs (lines 50-53, 144-147, 240-243) only change the Poly1305 key source but don't address the ChaCha20 encryption being real vs. no-op.

Comment on lines 249 to 252
while chacha_stream.read.bytes_remain() {
let mut buf = [0; 256];
chacha_stream.read(&mut buf)?;
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

Pre-existing bug (worth fixing while refactoring): Missing 0-byte read guard

Unlike the ChaChaTriPolyReader drain loop (line 155-159), this drain loop does not check for a 0-byte return from read(). If the underlying reader returns EOF while bytes_remain() is still > 0 (e.g., the reader has less data than expected), this will infinite-loop.

The TriPoly variant correctly handles this:

if chacha_stream.read(&mut buf)? == 0 {
    return Err(DecodeError::ShortRead);
}

There's even a test for the TriPoly case (short_read_chacha_tri_read_adapter) but no equivalent test for this path. Consider adding the same guard here.


let mut mac = Poly1305::new(&mac_key[..32]);
chacha.process_in_place(&mut plaintext[..]);
let mut mac = Poly1305::new(mac_key[..32].try_into().unwrap());
Copy link
Collaborator

Choose a reason for hiding this comment

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

Inconsistency: No #[cfg(fuzzing)] block for Poly1305 initialization here

The other three Poly1305 initialization sites in this file all have #[cfg(fuzzing)] blocks that use the encryption key directly instead of the derived mac_key. This site does not. In the old code, this was not an issue because the fuzzing Poly1305 module-level #[cfg(fuzzing)] switch handled it automatically. With the external crate, this site always uses mac_key even in fuzzing mode, making it inconsistent with the other sites.

Comment on lines +843 to +844
let seek_pos = (packet_data.len() - pos) as u32;
let mut chacha = ChaCha20::new(Key::new(keys.rho), Nonce::new([0; 12]), seek_pos);
Copy link
Collaborator

Choose a reason for hiding this comment

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

Bug: Block counter vs byte offset mismatch

seek_pos is a byte count (packet_data.len() - pos), but the 3rd argument to ChaCha20::new is the initial block counter (each ChaCha20 block is 64 bytes). This is evident from how the same constructor is used in sign/mod.rs and inbound_payment.rs, where the value passed corresponds to the old code's d[0] block counter.

The old code consumed packet_data.len() - pos individual bytes from the keystream:

for _ in 0..(packet_data.len() - pos) {
    let mut dummy = [0; 1];
    chacha.process_in_place(&mut dummy);
}

The new code sets the block counter to this byte count, which would start the keystream at byte seek_pos * 64 instead of byte seek_pos. For a typical 1300-byte onion packet on the first iteration, this would start at byte 83,200 instead of byte 1300.

This will produce incorrect filler data, breaking onion packet construction and causing all multi-hop payments to fail.

The fix would need to compute the block offset and handle the remainder:

let skip_bytes = packet_data.len() - pos;
let block_offset = (skip_bytes / 64) as u32;
let byte_remainder = skip_bytes % 64;
let mut chacha = ChaCha20::new(Key::new(keys.rho), Nonce::new([0; 12]), block_offset);
if byte_remainder > 0 {
    let mut dummy = [0u8; 64];
    chacha.apply_keystream(&mut dummy[..byte_remainder]);
}

Copy link
Author

Choose a reason for hiding this comment

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

The third argument to ChaCha20::new is a byte offset (seek), not a block counter. The constructor internally divides by 64 to get the block count and takes the remainder as the byte offset within that block. So passing seek_pos = packet_data.len() - pos correctly seeks to that byte position in the keystream.

Comment on lines +153 to +159
let mut salt = [0 as u8; 12];
let block_height_bytes = block_height.to_be_bytes();
salt[0..4].copy_from_slice(&block_height_bytes);
let tx_index_bytes = tx_index.to_be_bytes();
salt[4..8].copy_from_slice(&tx_index_bytes);

let mut chacha = ChaCha20::new(fake_scid_rand_bytes, &salt);
let mut chacha = ChaCha20::new(Key::new(*fake_scid_rand_bytes), Nonce::new(salt), 0);
Copy link
Collaborator

Choose a reason for hiding this comment

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

Bug: Nonce format change breaks keystream compatibility

The salt was changed from 8 bytes to 12 bytes. With an 8-byte nonce, the old ChaCha20::new placed values in the state as:

  • d[0]=counter, d[1]=0, d[2]=block_height, d[3]=tx_index

With a 12-byte nonce (RFC 8439), the state becomes:

  • d[0]=counter, d[1]=block_height, d[2]=tx_index, d[3]=0

These produce different keystreams for any non-zero block_height or tx_index. This means:

  1. get_encrypted_vout now returns different values than before for the same inputs
  2. is_valid_phantom / is_valid_intercept will fail to validate fake SCIDs generated by old code
  3. Any outstanding invoices using phantom or intercept SCIDs will stop working after upgrade

The test change from get_encrypted_vout(0, 0, ...) to get_encrypted_vout(1, 0, ...) below confirms the behavior changed — the old test only passed because with all-zero nonce the 8-byte and 12-byte layouts happened to coincide, but with block_height=1 they diverge.

To preserve backward compatibility, you need to place block_height and tx_index at the same position in the 12-byte nonce as they occupied in the 8-byte nonce:

let mut salt = [0u8; 12];
salt[4..8].copy_from_slice(&block_height_bytes);
salt[8..12].copy_from_slice(&tx_index_bytes);

run: |
cd fuzz
RUSTFLAGS="--cfg=fuzzing --cfg=secp256k1_fuzz --cfg=hashes_fuzz" cargo test --verbose --color always --lib --bins -j8
RUSTFLAGS="--cfg=fuzzing --cfg=secp256k1_fuzz --cfg=hashes_fuzz --cfg=chacha20_poly1305_fuzz" cargo test --verbose --color always --lib --bins -j8
Copy link
Collaborator

Choose a reason for hiding this comment

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

Issue: --cfg=chacha20_poly1305_fuzz not added to ci-fuzz.sh

This flag is added here for the sanity check, but fuzz/ci-fuzz.sh (line 16) sets its own RUSTFLAGS without --cfg=chacha20_poly1305_fuzz:

export RUSTFLAGS="--cfg=secp256k1_fuzz --cfg=hashes_fuzz"

This means the actual honggfuzz runs will compile the chacha20-poly1305 crate without its fuzzing mode, so real ChaCha20-Poly1305 operations will be performed during fuzzing. Fuzzers won't be able to generate valid MACs, severely limiting their ability to explore post-handshake/post-decryption code paths.

Comment on lines 725 to 726
) -> Result<msgs::OnionPacket, ()> {
let mut packet_data = [0; ONION_DATA_LEN];
Copy link
Collaborator

Choose a reason for hiding this comment

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

Semantic concern: 8-byte nonce → 12-byte nonce changes keystream for non-zero nonces

All the ChaCha20::new calls in this file use Nonce::new([0; 12]) (all-zero nonce), replacing the old &[0u8; 8]. Since the nonce is all zeros, the ChaCha20 state is identical regardless of nonce length, so there's no behavioral change here.

However, this is a subtle correctness invariant: this only works because the nonce is [0; 12]. If anyone later changes the nonce to a non-zero value based on the old 8-byte pattern without adjusting the position within the 12-byte array, it would silently break (as happened in scid_utils.rs). Worth a brief comment noting this equivalence holds only for all-zero nonces.

@ldk-claude-review-bot
Copy link
Collaborator

ldk-claude-review-bot commented Mar 17, 2026

Migrates ChaCha20-Poly1305 encryption from the local crypto
module to rust-bitcoin's `chacha20-poly1305` crate.

Integrated the crate across all modules (Router,
PeerStorage, Onion Utils, etc.) and removed the deprecated internal
implementation. Updated test vectors to align with the new 12-byte
nonce construction and ensured SCID encryption remains consistent.
@TheBlueMatt
Copy link
Collaborator

Cool, so I guess we're just waiting for an upstream release? Can you fix the rustfmt job in the mean time?

Cargo fmt formats the code using rustfmt and fixes the format diff
reported by the ci rustfmt workflow, when there are formatting
issues.
@codecov
Copy link

codecov bot commented Mar 19, 2026

Codecov Report

❌ Patch coverage is 84.27673% with 25 lines in your changes missing coverage. Please review.
✅ Project coverage is 86.10%. Comparing base (066859b) to head (b5c8b57).
⚠️ Report is 2765 commits behind head on main.

Files with missing lines Patch % Lines
lightning/src/ln/funding.rs 0.00% 21 Missing ⚠️
lightning/src/crypto/streams.rs 93.22% 3 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #4360      +/-   ##
==========================================
+ Coverage   84.55%   86.10%   +1.54%     
==========================================
  Files         135      158      +23     
  Lines       76569   106736   +30167     
  Branches    76569   106736   +30167     
==========================================
+ Hits        64745    91906   +27161     
- Misses       9783    12214    +2431     
- Partials     2041     2616     +575     
Flag Coverage Δ
tests 86.10% <84.27%> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

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.

Switch to chacha20-poly1305 crate

4 participants