Skip to content

btcutil: Add library functions for BIP-0352 silent payment _send_ support#2244

Open
guggero wants to merge 8 commits into
btcsuite:masterfrom
guggero:silent-payments
Open

btcutil: Add library functions for BIP-0352 silent payment _send_ support#2244
guggero wants to merge 8 commits into
btcsuite:masterfrom
guggero:silent-payments

Conversation

@guggero

@guggero guggero commented Sep 1, 2024

Copy link
Copy Markdown
Collaborator

Adds library/API functionality for Silent Payment send support.

Implements the following BIPs (or pull requests to BIPs):

Goal is to address send support first as that's rather straightforward. Receive support is much more involved as it requires scanning the chain in a specific manner.

@coveralls

coveralls commented Oct 20, 2024

Copy link
Copy Markdown

Pull Request Test Coverage Report for Build 20168757637

Details

  • 522 of 731 (71.41%) changed or added relevant lines in 9 files are covered.
  • 97 unchanged lines in 4 files lost coverage.
  • Overall coverage increased (+0.05%) to 55.009%

Changes Missing Coverage Covered Lines Changed/Added Lines %
btcutil/silentpayments/output.go 53 59 89.83%
btcutil/silentpayments/secp.go 40 51 78.43%
btcutil/psbt/partial_output.go 27 39 69.23%
btcutil/psbt/partial_input.go 32 46 69.57%
btcutil/psbt/psbt.go 40 54 74.07%
btcutil/psbt/silentpayments.go 40 58 68.97%
btcutil/silentpayments/input.go 81 102 79.41%
btcutil/silentpayments/dleq.go 104 131 79.39%
btcutil/silentpayments/address.go 105 191 54.97%
Files with Coverage Reduction New Missed Lines %
mempool/mempool.go 1 66.56%
btcutil/gcs/gcs.go 4 80.95%
database/ffldb/blockio.go 4 88.81%
rpcclient/infrastructure.go 88 39.75%
Totals Coverage Status
Change from base Build 19121431262: 0.05%
Covered Lines: 31631
Relevant Lines: 57501

💛 - Coveralls

@guggero guggero force-pushed the silent-payments branch 4 times, most recently from 2a9a931 to 996c4d3 Compare December 31, 2024 08:19
@saubyk saubyk added this to the v0.25 milestone Feb 13, 2025
@saubyk saubyk requested a review from kcalvinalvin February 18, 2025 00:48
@guggero guggero marked this pull request as ready for review March 28, 2025 20:37
@guggero guggero force-pushed the silent-payments branch 2 times, most recently from 88fa441 to 8a4fe70 Compare March 28, 2025 20:47
@benma

benma commented Aug 14, 2025

Copy link
Copy Markdown
Contributor

This is great work and very welcome. However, BIP-375 specifies exclusion for PSBTv0, so this will not be compatible with other PSBT libraries/wallets. Imho it would be best to focus on adding support for PSBTv2 first (#2328).

@myxmaster

Copy link
Copy Markdown

Hey, just wanted to drop a quick note as an LND user who's been following this PR and the overall silent payments effort.

I understand that PSBTv2 support (#2328) would be needed for full BIP375 compliance and interoperability with external signers/hardware wallets. But even without that, having SP send support land in btcutil would already be a huge step forward, since it would unblock LND PR lightningnetwork/lnd#9398 and allow LND users to send to silent payment addresses using LND's internal wallet and signer.

Would love to see this land. Thanks for all the great work on this @guggero!

@TechLateef

Copy link
Copy Markdown

Hey @guggero and @benma , just a heads up that I'm starting work on the PSBTv2 support requested in #2328. Hopefully, that will help unblock full BIP375 compliance for this PR soon!

Comment thread silentpayments/dleq.go Outdated
B := ScalarMult(privateKey.Key, scanKey)
G := Generator()

proof, err := DLEQProof(privateKey, B, G, r, nil)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

CreateShare passes the computed ECDH share (a·scanKey) as the B argument to DLEQProof. DLEQProof then computes C = a·B, so this proof is for a²·scanKey, not for the returned share. A verifier using (A=privateKey.PubKey(), B=scanKey, C=share) will reject it. This should pass scanKey to DLEQProof and return the separately computed share.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Great catch! Fixed and added unit test.

Comment thread silentpayments/dleq.go
}

// Let rand = hash_BIP0374/nonce(t || cbytes(A) || cbytes(C)).
rand := chainhash.TaggedHash(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Current BIP374 includes the optional message in the nonce derivation: hash_BIP0374/nonce(t || cbytes(A) || cbytes(C) || m'). This implementation omits m, which can leak a if the same aux randomness is reused with different messages.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Yes, this was updated later on in the BIP. Fixed and did a full review on the latest version of the BIP.

Comment thread silentpayments/input.go
// These types are supported in any case.
return true, script, nil

case txscript.ScriptHashTy:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This accepts every P2SH output as silent-payment compatible. BIP352 only includes P2SH-P2WPKH in shared-secret derivation; arbitrary P2SH/multisig inputs should be excluded or otherwise identified precisely, otherwise sender and receiver can derive different secrets.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Correct. Passing in the public key now so we can at least assert the address wasn't made from an incorrect (e.g. uncompressed key).

Comment thread silentpayments/address.go
return nil, errors.New("invalid bech32 version")
}

regrouped, err := bech32.ConvertBits(data[1:], 5, 8, false)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

DecodeAddress slices data[1:] and later reads data[0] before checking that data is non-empty. A checksum-valid Bech32m string with an empty data part can panic here instead of returning an error.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Yes, good catch. Added a fuzz test as well and ran it for a few hours on a beefy machine.

Comment thread psbt/partial_output.go
Comment thread psbt/partial_output.go
Comment thread silentpayments/dleq.go Outdated
func DLEQVerify(A, B, C, G *btcec.PublicKey, proof [64]byte, m *[32]byte) bool {
// Fail if any of is_infinite(A), is_infinite(B), is_infinite(C),
// is_infinite(G).
if !A.IsOnCurve() || !B.IsOnCurve() || !C.IsOnCurve() {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The comment quotes BIP374's requirement to fail if any of A, B, C, or G is infinite/off-curve, but the code only checks A, B, and C. Since G is caller-supplied, it should be validated too.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Makes sense, fixed.

Comment thread silentpayments/address.go Outdated

idx := 0
grouped := make([][]Address, len(groups))
for _, group := range groups {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

GroupByScanKey builds the returned slice by ranging over a map, so group order is randomized. Since AddressOutputKeys documents that recipients are ordered by output index and returns a flat result slice, this can make the returned output order nondeterministic for multiple scan-key groups.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Very good point, addressed and unit tested.

@yyforyongyu yyforyongyu left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Fired some agents to review it while catching up with the bip🤓

@ping-localhost

Copy link
Copy Markdown

I currently use btcutil.DecodeAddress to validate if a provided Bitcoin address is valid. Seeing how it hasn't been changed, I'll have to make a fallback for Silent Payments once this is merged?

Is it maybe a good idea to add a more generic ValidateAddress-function that can deal with all types? Just brain dumping, so feel free to ignore me.

@guggero

guggero commented Jun 18, 2026

Copy link
Copy Markdown
Collaborator Author

I currently use btcutil.DecodeAddress to validate if a provided Bitcoin address is valid. Seeing how it hasn't been changed, I'll have to make a fallback for Silent Payments once this is merged?

Is it maybe a good idea to add a more generic ValidateAddress-function that can deal with all types? Just brain dumping, so feel free to ignore me.

Hmm, that's a valid point. But I'm not sure what would be best here. Adding anything to the existing DecodeAddress in the now-extracted address/v2 package can't be done, as that would introduce a circular package dependency.
And adding a new DecodeAnyAddress would need to return an address.Address interface. But that interface requires the ScriptAddress() []byte method to be defined that can be used to produce the pkScript of a receiver output.
But that's exactly the complication with Silent Payment addresses: They are dependent on the inputs being spent.
So for safety reasons, the ScriptAddress method would need to panic for Silent Payment addresses and any caller would need to know how to deal with them.

So perhaps a simple silentpayments.IsValidAddress() would be enough to make the detection easier?

@guggero

guggero commented Jun 18, 2026

Copy link
Copy Markdown
Collaborator Author

Thanks a lot for the review, @yyforyongyu! I addressed all your points and did a full (agentic) review against the three updated BIPs and made sure all updated test vectors also pass. Should be quite solid now.

@ping-localhost

Copy link
Copy Markdown

So perhaps a simple silentpayments.IsValidAddress() would be enough to make the detection easier?

Yeah, that would help already! Anything to make the process easier is very much appreciated.

@guggero

guggero commented Jun 18, 2026

Copy link
Copy Markdown
Collaborator Author

So perhaps a simple silentpayments.IsValidAddress() would be enough to make the detection easier?

Yeah, that would help already! Anything to make the process easier is very much appreciated.

Okay, I've added IsValidAddress(string) bool.

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.

8 participants