Skip to content

[group key addrs 7/7]: send and receive support for V2 addresses #1587

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 11 commits into
base: more-group-key-refactors
Choose a base branch
from

Conversation

guggero
Copy link
Member

@guggero guggero commented Jun 6, 2025

Depends on #1658.

This PR introduces a new V2 address that:

  • Allows grouped assets to be sent.
  • Allows the amount to be defined as zero by the recipient, allowing the sender to choose the amount.
  • Can be re-used indefinitely without any privacy downsides.
  • Use a new proof courier type to transfer an encrypted send fragment from the sender to the receiver using a mailbox system (but a more robust mailbox than the previous hashmailbox implementation).

proof/send.go Outdated
// ScriptKeyDerivationUniquePedersen means the script key is derived
// using the address's recipient ID key and a single leaf that contains
// an un-spendable Pedersen commitment key
// (OP_CHECKSIG <NUMS_key + asset_id * G>). This can be used to
Copy link
Member

Choose a reason for hiding this comment

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

Ah ok, so this is just to ensure unique script keys for each unique asset ID in a send fragment, but we still plan on inserting that new asset TLV containing the shared secret hash to make all asset commitments for a given addr unique?

Copy link
Member Author

Choose a reason for hiding this comment

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

Correct. Will add the odd TLV value to make sure identical sends (e.g. same amounts and same asset IDs) will still produce a different on-chain output for better privacy.

// TxProof is the proof of the transaction that contains the asset
// outputs that are being sent. This is used as proof-of work to show
// to the auth mailbox server.
TxProof TxProof
Copy link
Member

Choose a reason for hiding this comment

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

Looks like we're duplicating the block header + height here across TxProof and SendFragment. Or is the idea that the encoding is smart enough to only encode once and copy the fields over as needed?

Copy link
Member Author

Choose a reason for hiding this comment

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

The envelope itself isn't encoded, only the SendFragment within. So the TxProof here is is what's required to send the fragment to the server.
Perhaps "envelope" isn't the best analogy here, as that's also sent along with a letter/message. Going to rename this to SendManifest instead, perhaps that makes more sense.

proof/courier.go Outdated
Proof: &mboxrpc.SendMessageRequest_TxProof{
TxProof: rpcProof,
},
// TODO(guggero): what value?
Copy link
Member

Choose a reason for hiding this comment

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

Good question...I think will depend on what we end up using as the max block height/expiry value for the server.

Copy link
Member Author

Choose a reason for hiding this comment

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

Went with 210k blocks for now, can perhaps make it configurable later on. And currently we don't delete messages yet. Not sure if we want to add message cleanup (both after user ACK and expiry) in this PR or whether I should create an issue for it.

rpcserver.go Outdated
if err != nil {
return nil, fmt.Errorf("unable to decode "+
"addr: %w", err)
}

// TODO(guggero): Need to add send fragment envelopes to
Copy link
Member

Choose a reason for hiding this comment

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

Can spin out into a sub-issue.

Do the vPSBTs actually need to be updated though? IIUC, we just need to specify the new proof courier semantics.

Copy link
Member Author

Choose a reason for hiding this comment

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

Actually, I think we should be able to support the vPSBT flow by just adding the TAP address as a new field to the vOutput. Otherwise we'd have to add safety checks to make sure users don't use the vPSBT flow with V2 addresses and then fail at the proof transfer. So adding vPSBT support is probably the easier way.
Added it as a TODO that I'm going to address before re-requesting review.

"service handle: %w", err)
}

err = courier.DeliverFragment(ctx, envelope)
Copy link
Member

Choose a reason for hiding this comment

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

Any reason to not fold this into the loop below where we'll attempt to send out the proofs/fragments in parallel?

Copy link
Member Author

Choose a reason for hiding this comment

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

Good point. The cardinality is different for fragments vs. proofs. But I made it so that we can always try again to send the same message, so we just try in the same loop. Also makes the whole backoff and re-try logic work seamlessly.

// required for V2 addresses that don't specify an amount (amount of zero).
// If an address does specify an amount, then it cannot be overridden in
// this map.
map<string, uint64> address_amounts = 5;
Copy link
Member

Choose a reason for hiding this comment

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

Hmm, for parsing, what if we allowed tap_addrs to be blank if this is specified, then we only read out address_amounts. This way a user doesn't need to specify an addr twice if it's a v2 addr.

Another alternative would be to upgrade tap_addrs to be a new message type, that can specify the addr, then also a custom amount.

Mainly thinking about how we can streamline the process of a new user potentially unknowingly using a re-useable addr.

Copy link
Member Author

Choose a reason for hiding this comment

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

Good point here. I updated the RPC to have two fields, the old tap_addrs and new addresses_with_amounts list, both being mutually exclusive. So we can deprecate the older one eventually.

@guggero guggero force-pushed the group-key-addr-part-2 branch from a5c2fd0 to b482ea3 Compare June 19, 2025 11:48
@guggero guggero changed the title [group key addrs 3/?]: send and receive support for V2 addresses [group key addrs 4/4]: send and receive support for V2 addresses Jun 19, 2025
@guggero guggero changed the title [group key addrs 4/4]: send and receive support for V2 addresses [group key addrs 5/5]: send and receive support for V2 addresses Jun 23, 2025
@guggero guggero force-pushed the group-key-addr-part-2 branch from b482ea3 to 8f8e164 Compare June 23, 2025 14:55
@guggero guggero changed the base branch from group-key-addr to add-mailbox-server June 23, 2025 14:55
@guggero guggero force-pushed the add-mailbox-server branch from 094b31e to 3d1a126 Compare June 24, 2025 08:27
@guggero guggero force-pushed the group-key-addr-part-2 branch from 8f8e164 to 060159b Compare June 24, 2025 08:27
@guggero guggero force-pushed the add-mailbox-server branch 3 times, most recently from dc426c6 to 74953ff Compare June 25, 2025 08:49
@guggero guggero force-pushed the group-key-addr-part-2 branch 2 times, most recently from db7f36c to a3dbdd3 Compare June 26, 2025 09:14
@guggero guggero changed the title [group key addrs 5/5]: send and receive support for V2 addresses [group key addrs 6/6]: send and receive support for V2 addresses Jun 26, 2025
@guggero guggero changed the base branch from add-mailbox-server to pedersen-keys-refactor June 26, 2025 09:17
@guggero guggero force-pushed the pedersen-keys-refactor branch from 5687720 to 46d9159 Compare June 26, 2025 10:18
@guggero guggero force-pushed the group-key-addr-part-2 branch from a3dbdd3 to ad2c782 Compare June 26, 2025 10:19
@guggero guggero force-pushed the pedersen-keys-refactor branch 2 times, most recently from e5fdf7e to eea552e Compare June 27, 2025 10:56
@guggero guggero force-pushed the group-key-addr-part-2 branch 2 times, most recently from ae1aefb to cd9b477 Compare June 30, 2025 15:33
@guggero guggero force-pushed the pedersen-keys-refactor branch from eea552e to 555c711 Compare June 30, 2025 15:34
// addresses_with_amounts list to specify the amount to send to each
// address. The tap_addrs and addresses_with_amounts lists are mutually
// exclusive, meaning that if addresses_with_amounts is set, then tap_addrs
// must be empty, and vice versa.
repeated string tap_addrs = 1;

// The optional fee rate to use for the minting transaction, in sat/kw.
uint32 fee_rate = 2;

Choose a reason for hiding this comment

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

Copy link
Member Author

Choose a reason for hiding this comment

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

Legacy reasons, should fix in separate PR.

Choose a reason for hiding this comment

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

Created #1661 to track this.

// addresses_with_amounts list to specify the amount to send to each
// address. The tap_addrs and addresses_with_amounts lists are mutually
// exclusive, meaning that if addresses_with_amounts is set, then tap_addrs
// must be empty, and vice versa.
repeated string tap_addrs = 1;

// The optional fee rate to use for the minting transaction, in sat/kw.
uint32 fee_rate = 2;

Choose a reason for hiding this comment

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

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, but not in this PR.

Choose a reason for hiding this comment

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

Created #1662 to track this.

@@ -1374,12 +1409,17 @@ message AddrReceivesResponse {
}

message SendAssetRequest {

Choose a reason for hiding this comment

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

Do we want to add a min_confs and/or spend_unconfirmed option like in SendCoinsRequest (https://github.com/lightningnetwork/lnd/blob/6b326152d419d0777f739a23c0d9330164d26c4d/lnrpc/lightning.proto#L1231-L1238) or does no one use those options?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, but not in this PR.

Copy link
Member Author

Choose a reason for hiding this comment

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

spend_unconfirmed will never work with Taproot Assets, because to create a valid proof the transaction always has to be in a block. Or I guess not "never", just not without a major protocol enhancement/upgrade.

Choose a reason for hiding this comment

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

Created #1663 to track this

@ZZiigguurraatt
Copy link

I need some guidance on what litd commit I can put this PR into. If I try to build from lightninglabs/lightning-terminal@a0915a3 using the TAPROOT_ASSETS_VERSION build arg in dev.Dockerfile (https://github.com/lightninglabs/lightning-terminal/blob/a0915a30462f1a06b9355154382fb883bcd9ba70/dev.Dockerfile#L38C5-L38C27) I get

239.1 /go/pkg/mod/github.com/lightninglabs/[email protected]/rpcutils/marshal.go:163:14: undefined: taprpc.ScriptKeyType_SCRIPT_KEY_UNIQUE_PEDERSEN
239.1 /go/pkg/mod/github.com/lightninglabs/[email protected]/rpcutils/marshal.go:190:17: undefined: taprpc.ScriptKeyType_SCRIPT_KEY_UNIQUE_PEDERSEN

@ZZiigguurraatt
Copy link

I need some guidance on what litd commit I can put this PR into. If I try to build from lightninglabs/lightning-terminal@a0915a3 using the TAPROOT_ASSETS_VERSION build arg in dev.Dockerfile (https://github.com/lightninglabs/lightning-terminal/blob/a0915a30462f1a06b9355154382fb883bcd9ba70/dev.Dockerfile#L38C5-L38C27) I get

239.1 /go/pkg/mod/github.com/lightninglabs/[email protected]/rpcutils/marshal.go:163:14: undefined: taprpc.ScriptKeyType_SCRIPT_KEY_UNIQUE_PEDERSEN
239.1 /go/pkg/mod/github.com/lightninglabs/[email protected]/rpcutils/marshal.go:190:17: undefined: taprpc.ScriptKeyType_SCRIPT_KEY_UNIQUE_PEDERSEN

I'm also using the NO_UI build arg with litd, not sure if that is relevant?

@guggero guggero force-pushed the group-key-addr-part-2 branch from 2de283d to 3bbfa95 Compare July 17, 2025 12:13
@guggero
Copy link
Member Author

guggero commented Jul 17, 2025

Do we have an itest for testing non-grouped assets with the new V2 address format with and without an amount specified?

I've added an integration test for that in the latest push.

This PR seems very big and I'd like to re-iterate the importance of #1492 .

I've added release notes (and the template) to the last commit. We'll need to back-fill this with PRs already merged for v0.7.0, will do that in a separate PR.

Copy link
Contributor

@ffranr ffranr left a comment

Choose a reason for hiding this comment

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

Maybe we should spin the first 7 commits into their own PR since they don't involve db migration. I've reviewed up to the 10th commit: multi: add addr v2 send support.

Comment on lines 1461 to 1552
// To make sure we have the correct internal key with
// the family and index set, we attempt to fetch it
// from the database. If it exists, then we know we
// stored it with the correct information.
dbKey, err := p.cfg.AssetWallet.FetchScriptKey(
ctx, key.PubKey,
)
if err == nil {
return p.cfg.KeyRing.IsLocalKey(
ctx, dbKey.RawKey,
)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think we're handling all database errors correctly here. For example, if we fail to read from the database, an error might be returned and we miss the opportunity to classify it as local.

We should handle errors normally here and base the remote/local distinction on address.ErrScriptKeyNotFound.

Do we need to keep the fallback method below? It would be cleaner and easier to debug any issues if we relied solely on the database query method above.

Copy link
Member Author

Choose a reason for hiding this comment

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

Okay, I added an error return value to the callback.

Yes, we need the fallback, in case the user only registered the internal key but not the outer script key.

Comment on lines 248 to 250
if err != nil {
return nil, proof.ErrTxMerkleProofExists
}
Copy link
Contributor

Choose a reason for hiding this comment

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

I think you mean to do if err == nil here?

Comment on lines +126 to 132
// different ways an asset can be spent. For V2 addresses, this key is
// not the tweaked Taproot output key but instead the bare/raw/internal
// script key. The sender will use this key to encrypt the send fragment
// that they post to the proof courier's mailbox. The raw script key
// will also be used by the sender to derive different Taproot output
// script keys for each asset ID.
ScriptKey btcec.PublicKey
Copy link
Contributor

Choose a reason for hiding this comment

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

// not the tweaked Taproot output key but instead the bare/raw/internal
// script key.

"tweaked Taproot output key" is not a thing, right? The Taproot output key itself is the result of tweaking the internal public key; it is not something that gets tweaked further.

but instead the bare/raw/internal script key.

Is it correct to just say: "but the Taproot internal public key"?

Comment on lines +143 to 146
// The amount is allowed to be zero for V2 addresses, where the sender
// will post a fragment containing the asset IDs and amounts to the
// proof courier's mailbox.
Amount uint64
Copy link
Contributor

Choose a reason for hiding this comment

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

All these edge cases for V2, would be nice to make Tap into an interface and have a distinct TapV2 type. And then struct documentation and fields can be specific to the V2 version.

Comment on lines 534 to 536
// addressDecoder returns a decoder function that can handle nil Taproot
// addresses.
func addressDecoder(a **address.Tap,
Copy link
Contributor

Choose a reason for hiding this comment

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

I would name this newAddressDecoder to hint that's it's a function factory rather than the actual decoder function. And same below for encode. I'm not sure what's most golang idiomatic.

Copy link
Member Author

Choose a reason for hiding this comment

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

All the xxxDecoder and xxxEncoder functions in this file work the same way. I don't necessarily disagree with you but want to keep things consistent and not expand the diff by renaming all of the other ones too.

Comment on lines +59 to +69
// SendOutput holds the information about a single asset output that was sent
// as part of an incoming asset transfer. Each event can have multiple outputs,
// in case multiple tranches of a grouped asset were transferred using a V2
// address.
type SendOutput struct {
Copy link
Contributor

Choose a reason for hiding this comment

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

Perhaps we can generalize the name to AssetOutput, since the distinction between send and receive doesn’t seem relevant here. Also, SendOutput doesn’t clearly convey that this refers to an asset-level output.

Comment on lines +8 to +10
-- An event can now have multiple outputs, in case multiple tranches of a
-- grouped asset were sent as part of a V2 address transfer.
CREATE TABLE IF NOT EXISTS addr_event_outputs (
Copy link
Contributor

Choose a reason for hiding this comment

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

These are asset outputs I think. To give more clarity and confidence to a future reader, can we say: An event can now have multiple asset outputs

Comment on lines +1366 to +1369
// We generate an ephemeral key pair that we'll use to encrypt the
// send fragment before sending it to the receiver. The public key of
// the pair will be part of the encrypted payload, so we can throw away
// the private key after doing the ECDH operation below.
Copy link
Contributor

Choose a reason for hiding this comment

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

I think we should mention the "shared secret" in this comment, something like:

The private key is used solely to derive a shared secret. Since the counterparty receives the public key, they can derive the same shared secret on their side. Once we've completed the derivation, the private key is no longer needed and can be safely discarded.

@ZZiigguurraatt
Copy link

Please update

github.com/lightningnetwork/lnd v0.19.2-beta.rc2
so that we are no longer on RC.

@ffranr
Copy link
Contributor

ffranr commented Jul 18, 2025

Please update

github.com/lightningnetwork/lnd v0.19.2-beta.rc2

so that we are no longer on RC.

@ZZiigguurraatt merged here: #1657

@ZZiigguurraatt
Copy link

Can you update

github.com/lightningnetwork/lightning-onion v1.2.1-0.20240712235311-98bd56499dfb // indirect
to github.com/lightningnetwork/lightning-onion v1.2.1-0.20240815225420-8b40adf04ab9 to match 8849fdb ?

@ZZiigguurraatt
Copy link

Please update

github.com/lightningnetwork/lnd v0.19.2-beta.rc2

so that we are no longer on RC.

@ZZiigguurraatt merged here: #1657

you're saying this just needs a rebase then?

@guggero
Copy link
Member Author

guggero commented Jul 18, 2025

Please update

github.com/lightningnetwork/lnd v0.19.2-beta.rc2

so that we are no longer on RC.

@ZZiigguurraatt merged here: #1657

you're saying this just needs a rebase then?

yes, on it

guggero added 11 commits July 18, 2025 16:42
This commit adds a new proof courier protocol that's based on the
universe RPC protocol but also adds the auth mailbox capability.

We assume that all our official universes will be upgraded to run an
auth mailbox compatible universe, so we change the protocol for all our
default instances.
This is a preparatory commit that deals with all the database changes
required to allow a single address receive event to have multiple asset
outputs, and with that multiple proofs associated with it.
As a preparation for creating events from wire transactions that aren't
notified by the lnd wallet (and therefore don't come through the
lndclient calls), we make the arguments to GetOrCreateEvents a bit more
explicit.
We refactor the proof handling to allow multiple outputs to be processed
and proofs for them to be fetched.
Adds the new --addrs_with_amounts flag that allows to specify the amount
to send to an address that allows sending user-defined amounts (address
V2).
@guggero guggero force-pushed the group-key-addr-part-2 branch from 3bbfa95 to b581962 Compare July 18, 2025 14:43
@guggero guggero changed the base branch from main to more-group-key-refactors July 18, 2025 14:43
@guggero guggero changed the title [group key addrs 6/6]: send and receive support for V2 addresses [group key addrs 7/7]: send and receive support for V2 addresses Jul 18, 2025
are ideal as re-usable, long-term static addresses (with on-chain privacy
guarantees similar to BIP-0352 Silent Payments).
V2 addresses require the use of a proof courier that supports the
new `authmailbox+universerpc://` protocol. Any `tapd` that runs with version

Choose a reason for hiding this comment

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

I thought we decided we were going to make it auto discover the courier type, or do you want to create an issue to do that in a separate commit?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Status: 👀 In review
Development

Successfully merging this pull request may close these issues.

[feature]: add support for re-useable group key based addresses [feature]: support of passing arbitary amount to a taproot address
6 participants