Skip to content
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

SealBatch RPC endpoint accepts signed group PSBT #1295

Merged
merged 6 commits into from
Jan 20, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 12 additions & 6 deletions cmd/tapcli/assets.go
Original file line number Diff line number Diff line change
Expand Up @@ -461,7 +461,16 @@ var sealBatchCommand = cli.Command{
cli.StringSliceFlag{
Name: "group_signatures",
Usage: "the asset ID and signature, separated by a " +
"colon",
"colon. This flag should not be used in " +
"conjunction with 'signed_group_psbt'; use " +
"one or the other.",
},
cli.StringSliceFlag{
Name: "signed_group_psbt",
Usage: "a signed group PSBT for a single asset group " +
guggero marked this conversation as resolved.
Show resolved Hide resolved
"in the batch. This flag should not be used " +
"in conjunction with 'group_signatures'; use " +
"one or the other.",
},
},
Hidden: true,
Expand All @@ -474,13 +483,10 @@ func sealBatch(ctx *cli.Context) error {
defer cleanUp()

req := &mintrpc.SealBatchRequest{
ShortResponse: ctx.Bool(shortResponseName),
ShortResponse: ctx.Bool(shortResponseName),
SignedGroupVirtualPsbts: ctx.StringSlice("signed_group_psbt"),
}

// TODO(guggero): Actually just ask for the signed PSBT back and extract
// the signature from there. We can query the pending batch to get the
// asset ID of each PSBT (can match by the unsigned transaction's input
// prev out).
sigs := ctx.StringSlice("group_signatures")
for _, witness := range sigs {
parts := strings.Split(witness, ":")
Expand Down
91 changes: 1 addition & 90 deletions docs/external-group-key.md
Original file line number Diff line number Diff line change
Expand Up @@ -249,99 +249,10 @@ Successfully signed PSBT:
cHNidP8BAF4CAAAAATKZro+KSjqg4YQvE0bqBCbWuii3ekKAdufOGo57L8lwAAAAAAAAAAAAAQBlzR0AAAAAIlEgxoSpM86Pyu/bCRKhOc6/2TLXDGXnUnXn69FQqD8gw7cAAAAAAAEBKwBlzR0AAAAAIlEgqUflbsA2uA/P2qYe6qclsUox7koFCR3h1xkwh4+M1wQBCEIBQAv/X4PJqGyO2YzL2uJgIK+gDFGCTIFkzAq29ThWcBuW5mFIc7aQX1CBtxHSXiF8/jn+F5sWeL0pve1ZKxY7L4EAAA==
```

## Step 5b: Extract the signature from the PSBT

TODO(guggero/ffranr): Replace this step by allowing the CLI to take the signed
PSBT instead. We can list the batch to get the asset ID of each pending asset
and match the PSBT's input previous output to find out what signed PSBT belongs
to what asset (in case there are multiple).

```shell
$ bitcoin-cli decodepsbt cHNidP8BAF4CAAAAATKZro+KSjqg4YQvE0bqBCbWuii3ekKAdufOGo57L8lwAAAAAAAAAAAAAQBlzR0AAAAAIlEgxoSpM86Pyu/bCRKhOc6/2TLXDGXnUnXn69FQqD8gw7cAAAAAAAEBKwBlzR0AAAAAIlEgqUflbsA2uA/P2qYe6qclsUox7koFCR3h1xkwh4+M1wQBCEIBQAv/X4PJqGyO2YzL2uJgIK+gDFGCTIFkzAq29ThWcBuW5mFIc7aQX1CBtxHSXiF8/jn+F5sWeL0pve1ZKxY7L4EAAA==

{
"tx": {
"txid": "1994e0f5e6beee0a58f76b2f806894f94d93ea0a21c6e380b7634f76c358c38a",
"hash": "1994e0f5e6beee0a58f76b2f806894f94d93ea0a21c6e380b7634f76c358c38a",
"version": 2,
"size": 94,
"vsize": 94,
"weight": 376,
"locktime": 0,
"vin": [
{
"txid": "70c92f7b8e1acee77680427ab728bad62604ea46132f84e1a03a4a8a8fae9932",
"vout": 0,
"scriptSig": {
"asm": "",
"hex": ""
},
"sequence": 0
}
],
"vout": [
{
"value": 5.00000000,
"n": 0,
"scriptPubKey": {
"asm": "1 c684a933ce8fcaefdb0912a139cebfd932d70c65e75275e7ebd150a83f20c3b7",
"desc": "addr(bcrt1pc6z2jv7w3l9wlkcfz2snnn4lmyedwrr9uaf8telt69g2s0eqcwmsnuxkp4)#ramnsz8q",
"hex": "5120c684a933ce8fcaefdb0912a139cebfd932d70c65e75275e7ebd150a83f20c3b7",
"address": "bcrt1pc6z2jv7w3l9wlkcfz2snnn4lmyedwrr9uaf8telt69g2s0eqcwmsnuxkp4",
"type": "witness_v1_taproot"
}
}
]
},
"global_xpubs": [
],
"psbt_version": 0,
"proprietary": [
],
"unknown": {
},
"inputs": [
{
"witness_utxo": {
"amount": 5.00000000,
"scriptPubKey": {
"asm": "1 a947e56ec036b80fcfdaa61eeaa725b14a31ee4a05091de1d71930878f8cd704",
"desc": "rawtr(a947e56ec036b80fcfdaa61eeaa725b14a31ee4a05091de1d71930878f8cd704)#fnjmj6gz",
"hex": "5120a947e56ec036b80fcfdaa61eeaa725b14a31ee4a05091de1d71930878f8cd704",
"address": "bcrt1p49r72mkqx6uqln765c0w4fe9k99rrmj2q5y3mcwhrycg0ruv6uzqgnfa6h",
"type": "witness_v1_taproot"
}
},
"final_scriptwitness": [
"0bff5f83c9a86c8ed98ccbdae26020afa00c51824c8164cc0ab6f53856701b96e6614873b6905f5081b711d25e217cfe39fe179b1678bd29bded592b163b2f81"
]
}
],
"outputs": [
{
}
],
"fee": 0.00000000
}

```

We'll take the witness as the signature for the next step:

```json
"final_scriptwitness": [
"0bff5f83c9a86c8ed98ccbdae26020afa00c51824c8164cc0ab6f53856701b96e6614873b6905f5081b711d25e217cfe39fe179b1678bd29bded592b163b2f81"
]
```

## Step 6: Seal the batch with the signature

TODO(guggero/ffranr): Use signed PSBT instead, currently it's
`--group_signatures <asset_id>:<signature>`, but should be
`--signed_group_psbt` or something like that.

```shell
$ tapcli assets mint seal --group_signatures c4b0771c1bd1334bf20df5204c162702a6dc765a9cb15b1bc9e3c91e0282061b:0bff5f83c9a86c8ed98ccbdae26020afa00c51824c8164cc0ab6f53856701b96e6614873b6905f5081b711d25e217cfe39fe179b1678bd29bded592b163b2f81
$ tapcli assets mint seal --signed_group_psbt cHNidP8BAF4CAAAAATKZro+KSjqg4YQvE0bqBCbWuii3ekKAdufOGo57L8lwAAAAAAAAAAAAAQBlzR0AAAAAIlEgxoSpM86Pyu/bCRKhOc6/2TLXDGXnUnXn69FQqD8gw7cAAAAAAAEBKwBlzR0AAAAAIlEgqUflbsA2uA/P2qYe6qclsUox7koFCR3h1xkwh4+M1wQBCEIBQAv/X4PJqGyO2YzL2uJgIK+gDFGCTIFkzAq29ThWcBuW5mFIc7aQX1CBtxHSXiF8/jn+F5sWeL0pve1ZKxY7L4EAAA==
ffranr marked this conversation as resolved.
Show resolved Hide resolved

{
"batch": {
Expand Down
66 changes: 7 additions & 59 deletions itest/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ package itest
import (
"bytes"
"context"
"fmt"
"log"
"testing"
"time"

Expand Down Expand Up @@ -32,7 +30,6 @@ import (
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/lntest/node"
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
"github.com/lightningnetwork/lnd/tlv"
"github.com/stretchr/testify/require"
"google.golang.org/grpc"
"google.golang.org/protobuf/proto"
Expand Down Expand Up @@ -779,38 +776,28 @@ func MintAssetExternalSigner(t *harnessTest, tapNode *tapdHarness,
callbackRes := externalSignerCallback(fundResp.Batch.UnsealedAssets)

// Extract group witness from signed PSBTs.
var groupWitnesses []*taprpc.GroupWitness
var signedGroupVirtualPsbts []string
for idx := range callbackRes {
res := callbackRes[idx]
signedPsbt := res.SignedPsbt
genesisAssetID := res.AssetID

// Sanity check signed PSBT.
require.Len(t.t, signedPsbt.Inputs, 1)
require.Len(t.t, signedPsbt.Outputs, 1)

// Extract witness from signed PSBT.
witnessStack, err := DeserializeWitnessStack(
signedPsbt.Inputs[0].FinalScriptWitness,
// Encode the signed PSBT as a string.
signedPsbtStr, err := signedPsbt.B64Encode()
require.NoError(t.t, err)
signedGroupVirtualPsbts = append(
signedGroupVirtualPsbts, signedPsbtStr,
)
if err != nil {
log.Fatalf("Failed to deserialize witness stack: %v",
err)
}

groupWitness := taprpc.GroupWitness{
GenesisId: genesisAssetID[:],
Witness: witnessStack,
}

groupWitnesses = append(groupWitnesses, &groupWitness)
}

// Seal the batch with the group witnesses.
ctxt, cancel = context.WithTimeout(ctxb, defaultWaitTimeout)

sealReq := mintrpc.SealBatchRequest{
GroupWitnesses: groupWitnesses,
SignedGroupVirtualPsbts: signedGroupVirtualPsbts,
}
sealResp, err := tapNode.SealBatch(ctxt, &sealReq)
require.NoError(t.t, err)
Expand Down Expand Up @@ -844,45 +831,6 @@ func MintAssetExternalSigner(t *harnessTest, tapNode *tapdHarness,
return batchAssets
}

// DeserializeWitnessStack deserializes a serialized witness stack into a
// [][]byte.
//
// TODO(ffranr): Reconcile this function with asset.TxWitnessDecoder.
func DeserializeWitnessStack(serialized []byte) ([][]byte, error) {
var (
// buf is a general scratch buffer used when reading.
buf [8]byte

stack [][]byte
)
reader := bytes.NewReader(serialized)

// Read the number of witness elements (compact size integer)
count, err := tlv.ReadVarInt(reader, &buf)
if err != nil {
return nil, fmt.Errorf("failed to read witness count: %w", err)
}

// Read each witness element
for i := uint64(0); i < count; i++ {
elementSize, err := tlv.ReadVarInt(reader, &buf)
if err != nil {
return nil, fmt.Errorf("failed to read witness "+
"element size: %w", err)
}

// Read the witness element data
element := make([]byte, elementSize)
if _, err := reader.Read(element); err != nil {
return nil, fmt.Errorf("failed to read witness "+
"element data: %w", err)
}
stack = append(stack, element)
}

return stack, nil
}

// SyncUniverses syncs the universes of two tapd instances and waits until they
// are in sync.
func SyncUniverses(ctx context.Context, t *testing.T, clientTapd,
Expand Down
21 changes: 20 additions & 1 deletion rpcserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -780,6 +780,7 @@ func (r *rpcServer) FundBatch(ctx context.Context,
func (r *rpcServer) SealBatch(ctx context.Context,
req *mintrpc.SealBatchRequest) (*mintrpc.SealBatchResponse, error) {

// Unmarshal group witnesses from the request.
Copy link
Member

Choose a reason for hiding this comment

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

So the idea is that the old way and new way could even be mixed? Since both seem to be parsed and then merged (at least with a collision check, so nothing is overwritten).

Not sure if that's more confusing to the user rather than helpful? I think just from an API perspective it makes sense to keep the old way (as that's perhaps easier to use programmatically). But perhaps we should disallow using both at the same time, to avoid some confusion?

Choose a reason for hiding this comment

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

the old way still works for me with this branch, but as mentioned above, the new way fails

Copy link
Member

Choose a reason for hiding this comment

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

Did you switch between branches? Or use my Ledger branch before I rebased it on top of this one?
Not being able to parse the PSBT probably means it's because of the bump in the psbt library that isn't in this branch. So can you try again with the current version of #1301 that includes this PR?

Choose a reason for hiding this comment

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

Still getting unable to parse signed group virtual PSBT: unexpected EOF with cold-group-key-pedersen. I'm using chantools to generate the PSBT, by the way. Maybe I'm not supposed to use that?

Copy link
Member

Choose a reason for hiding this comment

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

Hmm, weird. Can you paste the PSBT here please?

Choose a reason for hiding this comment

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

You can give it a try here: https://github.com/lightninglabs/tapdvalidation/tree/sealbatch-signed-group-psbt-test . Follow README.md, except you'll have to add an extra step to checkout this branch before you run ./start_mini_META.

Choose a reason for hiding this comment

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

You can see here the difference between the old way that worked and the new way that doesn't work: https://github.com/lightninglabs/tapdvalidation/commit/e95ee5b432443570632cc36202c14fb902b4cc9a.

Copy link

@ZZiigguurraatt ZZiigguurraatt Jan 18, 2025

Choose a reason for hiding this comment

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

Okay, I figured out my issue. It has been fixed here: https://github.com/lightninglabs/tapdvalidation/commit/2aa7f2f05eb43c7f3e11eb1381759f240057c231 . The problem is I passed a string as signed_group_virtual_psbts instead of an array of strings.

Can we get a more useful test and error message for this mistake? unable to parse signed group virtual PSBT: unexpected EOF is super uninformative. I'm a little confused why gRPC accepts a single string instead of require an array for "repeated" fields. I'm not sure why it doesn't enforce this, but I've made this mistake before and the error messages never actually lead me to understand the mistake in a meaningful way.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

SealBatchRequest in mintrpc is given as

type SealBatchRequest struct {
    state                   protoimpl.MessageState
    sizeCache               protoimpl.SizeCache
    unknownFields           protoimpl.UnknownFields
    ShortResponse           bool                   `protobuf:"varint,1,opt,name=short_response,json=shortResponse,proto3" json:"short_response,omitempty"`
    GroupWitnesses          []*taprpc.GroupWitness `protobuf:"bytes,2,rep,name=group_witnesses,json=groupWitnesses,proto3" json:"group_witnesses,omitempty"`
    SignedGroupVirtualPsbts []string               `protobuf:"bytes,3,rep,name=signed_group_virtual_psbts,json=signedGroupVirtualPsbts,proto3" json:"signed_group_virtual_psbts,omitempty"`
}

Are you using JSON and Python? I think in golang the type checker would have caught this.

unable to parse signed group virtual PSBT: unexpected EOF is super uninformative

I'll modify the error message to include the signed PSBT as seen by tapd.

Choose a reason for hiding this comment

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

Using Python.

It seems to do more clear error messages if I mix up a string , bytes, or int, but for some reason does not check arrays very well.

var groupWitnesses []asset.PendingGroupWitness
for i := range req.GroupWitnesses {
wit, err := taprpc.UnmarshalGroupWitness(req.GroupWitnesses[i])
Expand All @@ -790,9 +791,27 @@ func (r *rpcServer) SealBatch(ctx context.Context,
groupWitnesses = append(groupWitnesses, *wit)
}

// Unmarshal signed group virtual PSBTs from the request.
var groupPSBTs []psbt.Packet
for i := range req.SignedGroupVirtualPsbts {
groupPsbt := req.SignedGroupVirtualPsbts[i]

// Decode the signed group virtual PSBT.
r := bytes.NewReader([]byte(groupPsbt))
psbtPacket, err := psbt.NewFromRawBytes(r, true)
if err != nil {
return nil, fmt.Errorf("unable to parse signed "+
"group virtual PSBT (signed_psbt=%s): %w",
groupPsbt, err)
}

groupPSBTs = append(groupPSBTs, *psbtPacket)
}

batch, err := r.cfg.AssetMinter.SealBatch(
tapgarden.SealParams{
GroupWitnesses: groupWitnesses,
GroupWitnesses: groupWitnesses,
SignedGroupVirtualPsbts: groupPSBTs,
},
)
if err != nil {
Expand Down
Loading
Loading