Skip to content

fix(bolt12): Make CurrencyCode a validated wrapper type #3814

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from

Conversation

erickcestari
Copy link

@erickcestari erickcestari commented May 30, 2025

After performing differential fuzzing using bitcoinfuzz with rust-lightning and Core Lightning (CLN), I noticed that rust-lightning does not verify whether the offer_currency field contains valid UTF-8.

This commit convert CurrencyCode from type alias to struct with validation, ensuring
ISO 4217 compliance (3 ASCII uppercase letters) at construction time.

@ldk-reviews-bot
Copy link

ldk-reviews-bot commented May 30, 2025

👋 Thanks for assigning @jkczyz as a reviewer!
I'll wait for their review and will help manage the review process.
Once they submit their review, I'll check if a second reviewer would be helpful.

Copy link

codecov bot commented May 30, 2025

Codecov Report

Attention: Patch coverage is 90.09009% with 11 lines in your changes missing coverage. Please review.

Project coverage is 89.87%. Comparing base (78fee88) to head (388c5d5).
Report is 105 commits behind head on main.

Files with missing lines Patch % Lines
lightning/src/offers/offer.rs 88.88% 11 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3814      +/-   ##
==========================================
+ Coverage   89.52%   89.87%   +0.34%     
==========================================
  Files         157      160       +3     
  Lines      125100   129412    +4312     
  Branches   125100   129412    +4312     
==========================================
+ Hits       112002   116311    +4309     
+ Misses      10408    10396      -12     
- Partials     2690     2705      +15     

☔ 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.

@erickcestari erickcestari force-pushed the validate-currency-code branch from 7232027 to 748747b Compare May 30, 2025 16:48
@erickcestari
Copy link
Author

erickcestari commented May 30, 2025

I took a closer look, and it seems this test vector has an invalid currency length in UTF-8 encoding, rather than being an invalid UTF-8 string itself.

  {
    "description": "Malformed: invalid currency UTF-8",
    "valid": false,
    "bolt12": "lno1qcpgqsg2q4q5cj2rg5tzzqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqg"
  },

data part: 060280410a05414c4943451621020202020202020202020202020202020202020202020202020202020202020202

Looking at the data part, we observe 06 (currency type), 02 (length), and 8041 (value), which confirms that the error arises from an invalid currency length in UTF-8 encoding, not from an invalid UTF-8 sequence.

I'll open a PR on Bolts to fix it.

(edited)

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.

Aren't the currencies supposed to be the three-char currency specification standard? ie they should be ASCII only (and probably all-caps letter only, never numbers)?

Copy link
Contributor

@jkczyz jkczyz left a comment

Choose a reason for hiding this comment

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

Thanks for jumping on this!

@jkczyz
Copy link
Contributor

jkczyz commented May 30, 2025

I took a closer look, and it seems this test vector has an invalid currency length in UTF-8 encoding, rather than being an invalid UTF-8 string itself.

  {
    "description": "Malformed: invalid currency UTF-8",
    "valid": false,
    "bolt12": "lno1qcpgqsg2q4q5cj2rg5tzzqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqg"
  },

data part: 060280410a05414c4943451621020202020202020202020202020202020202020202020202020202020202020202

Looking at the data part, we observe 06 (currency type), 02 (length), and 2804 (value), which confirms that the error arises from an invalid currency length in UTF-8 encoding, not from an invalid UTF-8 sequence.

I'll open a PR on Bolts to fix it.

Ah, yeah either the test vector or the comment is wrong lol

@erickcestari
Copy link
Author

Aren't the currencies supposed to be the three-char currency specification standard? ie they should be ASCII only (and probably all-caps letter only, never numbers)?

Initially, I considered just aligning it with the UTF-8 type for consistency. But yes, it makes more sense to enforce stricter validation to ensure it's valid ASCII and adheres to the expected format.

@erickcestari erickcestari force-pushed the validate-currency-code branch from 748747b to 91e633a Compare May 31, 2025 15:05
@erickcestari erickcestari changed the title fix(bolt12): Add UTF-8 validation for offer currency field fix(bolt12): Add ASCII validation for offer currency field May 31, 2025
@ldk-reviews-bot
Copy link

🔔 1st Reminder

Hey @TheBlueMatt @jkczyz! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@tnull
Copy link
Contributor

tnull commented Jun 2, 2025

Ah, funny, seems this fixes #3813 we just opened. Will link the issue.

@thomash-acinq
Copy link

data part: 060280410a05414c4943451621020202020202020202020202020202020202020202020202020202020202020202

Looking at the data part, we observe 06 (currency type), 02 (length), and 2804 (value), which confirms that the error arises from an invalid currency length in UTF-8 encoding, not from an invalid UTF-8 sequence.

No, it's 06 (currency type), 02 (length), and 8041 (value). It seems that you've copied the '2' to read '2804'.

@erickcestari
Copy link
Author

data part: 060280410a05414c4943451621020202020202020202020202020202020202020202020202020202020202020202
Looking at the data part, we observe 06 (currency type), 02 (length), and 2804 (value), which confirms that the error arises from an invalid currency length in UTF-8 encoding, not from an invalid UTF-8 sequence.

No, it's 06 (currency type), 02 (length), and 8041 (value). It seems that you've copied the '2' to read '2804'.

Yes, My mistake on the transcription.

It's invalid UTF-8, but the decoder fails due to the length mismatch (expecting 3 bytes for currency) rather than the UTF-8 validation itself.

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.

Rather than checking this when deserializing an offer, can we change CurrencyCode to be a real wrapper so that we can enforce it on creation/deserialization of that struct? Currently I believe we will let users build offers that we will refuse to parse.

@ldk-reviews-bot
Copy link

🔔 1st Reminder

Hey @jkczyz! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@jkczyz
Copy link
Contributor

jkczyz commented Jun 2, 2025

Rather than checking this when deserializing an offer, can we change CurrencyCode to be a real wrapper so that we can enforce it on creation/deserialization of that struct? Currently I believe we will let users build offers that we will refuse to parse.

They would be helpful as upcoming OfferMessageFlow changes to support currencies could benefit from a dedicated type rather than a variant in an enum (cc: @shaavan).

@erickcestari erickcestari force-pushed the validate-currency-code branch 2 times, most recently from d9b2684 to 1ebbcd0 Compare June 3, 2025 12:38
@erickcestari erickcestari changed the title fix(bolt12): Add ASCII validation for offer currency field fix(bolt12): Make CurrencyCode a validated wrapper type Jun 3, 2025
Copy link
Contributor

@jkczyz jkczyz left a comment

Choose a reason for hiding this comment

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

Will defer to @TheBlueMatt on some of these comments,

/// Creates a new CurrencyCode from a 3-byte array.
///
/// Returns an error if the bytes are not valid UTF-8 or not all ASCII uppercase.
pub fn new(code: [u8; 3]) -> Result<Self, Bolt12SemanticError> {
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we use the unit type () for the error given and map at the call site given it can't be any other variant than InvalidCurrencyCode?

Copy link
Author

Choose a reason for hiding this comment

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

I have created a new custom error type for parsing it.


/// Returns the currency code as a string slice.
pub fn as_str(&self) -> &str {
unsafe { core::str::from_utf8_unchecked(&self.0) }
Copy link
Contributor

Choose a reason for hiding this comment

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

Not sure if we should just use from_utf8().expect().

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yea, while its safe, there's also not much reason to bother with the optimization of skipping UTF-8 validation on a 3-char string (even if it would marginally reduce the binary size by avoiding the unwrap line and/or error string).

@erickcestari erickcestari force-pushed the validate-currency-code branch 3 times, most recently from 41929f5 to 46cbed5 Compare June 3, 2025 19:08
@erickcestari erickcestari requested a review from jkczyz June 3, 2025 19:55
@erickcestari erickcestari requested a review from TheBlueMatt June 3, 2025 19:55
@ldk-reviews-bot
Copy link

🔔 2nd Reminder

Hey @TheBlueMatt @jkczyz! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

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.

Thanks!


/// Returns the currency code as a string slice.
pub fn as_str(&self) -> &str {
unsafe { core::str::from_utf8_unchecked(&self.0) }
Copy link
Collaborator

Choose a reason for hiding this comment

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

Yea, while its safe, there's also not much reason to bother with the optimization of skipping UTF-8 validation on a 3-char string (even if it would marginally reduce the binary size by avoiding the unwrap line and/or error string).

@erickcestari erickcestari force-pushed the validate-currency-code branch from 87c2752 to e2c14e1 Compare June 5, 2025 13:27
Convert CurrencyCode from type alias to struct with validation, ensuring
ISO 4217 compliance (3 ASCII uppercase letters) at construction time.
Tests cover UTF-8 validation, ASCII uppercase validation, case sensitivity, length checks,
and API consistency between new() and from_str().
@erickcestari erickcestari force-pushed the validate-currency-code branch from e2c14e1 to 388c5d5 Compare June 5, 2025 13:38
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.

Validate ISO 4217 compliance in BOLT12 CurrencyCode
6 participants