Skip to content

Commit

Permalink
Merge pull request #328 from lightninglabs/universe_rpc_fixes
Browse files Browse the repository at this point in the history
universe: RPC fixes
  • Loading branch information
Roasbeef authored Jun 16, 2023
2 parents e74932b + 8c49fcb commit 7ab8a57
Show file tree
Hide file tree
Showing 8 changed files with 139 additions and 77 deletions.
37 changes: 5 additions & 32 deletions cmd/tapcli/universe.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,8 @@ package main
import (
"bytes"
"encoding/hex"
"errors"
"fmt"
"strconv"
"strings"

"github.com/btcsuite/btcd/chaincfg/chainhash"
tap "github.com/lightninglabs/taproot-assets"
"github.com/lightninglabs/taproot-assets/proof"
"github.com/lightninglabs/taproot-assets/taprpc/universerpc"
Expand Down Expand Up @@ -280,41 +276,18 @@ var universeProofQueryCommand = cli.Command{
Action: universeProofQuery,
}

func parseUniOutpoint(ctx *cli.Context) (*universerpc.Outpoint, error) {
// Parse a bitcoin outpoint in the form txid:index into a
// wire.OutPoint struct.
parts := strings.Split(ctx.String(outpointName), ":")
if len(parts) != 2 {
return nil, errors.New("outpoint should be of " +
"the form txid:index")
}
txidStr := parts[0]
if hex.DecodedLen(len(txidStr)) != chainhash.HashSize {
return nil, fmt.Errorf("invalid hex-encoded "+
"txid %v", txidStr)
}

outputIndex, err := strconv.ParseInt(parts[1], 10, 32)
if err != nil {
return nil, fmt.Errorf("invalid output "+
"index: %v", err)
}

return &universerpc.Outpoint{
HashStr: txidStr,
Index: int32(outputIndex),
}, nil
}

func parseAssetKey(ctx *cli.Context) (*universerpc.AssetKey, error) {
outpoint, err := parseUniOutpoint(ctx)
outpoint, err := tap.UnmarshalOutpoint(ctx.String(outpointName))
if err != nil {
return nil, err
}

return &universerpc.AssetKey{
Outpoint: &universerpc.AssetKey_Op{
Op: outpoint,
Op: &unirpc.Outpoint{
HashStr: outpoint.Hash.String(),
Index: int32(outpoint.Index),
},
},
ScriptKey: &universerpc.AssetKey_ScriptKeyStr{
ScriptKeyStr: ctx.String(scriptKeyName),
Expand Down
22 changes: 19 additions & 3 deletions itest/assertions.go
Original file line number Diff line number Diff line change
Expand Up @@ -479,6 +479,22 @@ func assertAddr(t *testing.T, expected *taprpc.Asset, actual *taprpc.Addr) {
require.NotEqual(t, expected.ScriptKey, actual.ScriptKey)
}

// assertEqualAsset asserts that two taprpc.Asset objects are equal, ignoring
// node-specific fields like if script keys are local, if the asset is spent,
// or if the anchor information is populated.
func assertAsset(t *testing.T, expected, actual *taprpc.Asset) {
require.Equal(t, expected.Version, actual.Version)
require.Equal(t, expected.AssetGenesis, actual.AssetGenesis)
require.Equal(t, expected.AssetType, actual.AssetType)
require.Equal(t, expected.Amount, actual.Amount)
require.Equal(t, expected.LockTime, actual.LockTime)
require.Equal(t, expected.RelativeLockTime, actual.RelativeLockTime)
require.Equal(t, expected.ScriptVersion, actual.ScriptVersion)
require.Equal(t, expected.ScriptKey, actual.ScriptKey)
require.Equal(t, expected.AssetGroup, actual.AssetGroup)
require.Equal(t, expected.PrevWitnesses, actual.PrevWitnesses)
}

// assertBalanceByID asserts that the balance of a single asset,
// specified by ID, on the given daemon is correct.
func assertBalanceByID(t *testing.T, tapd *tapdHarness, id []byte,
Expand Down Expand Up @@ -778,15 +794,15 @@ func assertUniverseStats(t *testing.T, node *tapdHarness,
}

if numProofs != int(uniStats.NumTotalProofs) {
return fmt.Errorf("expected %v, got %v",
return fmt.Errorf("expected %v proofs, got %v",
numProofs, uniStats.NumTotalProofs)
}
if numSyncs != int(uniStats.NumTotalSyncs) {
return fmt.Errorf("expected %v, got %v",
return fmt.Errorf("expected %v syncs, got %v",
numSyncs, uniStats.NumTotalSyncs)
}
if numAssets != int(uniStats.NumTotalAssets) {
return fmt.Errorf("expected %v, got %v",
return fmt.Errorf("expected %v assets, got %v",
numAssets, uniStats.NumTotalAssets)
}

Expand Down
41 changes: 41 additions & 0 deletions itest/universe_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ import (
"fmt"
"io"

"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcec/v2/schnorr"
tap "github.com/lightninglabs/taproot-assets"
"github.com/lightninglabs/taproot-assets/fn"
"github.com/lightninglabs/taproot-assets/taprpc"
unirpc "github.com/lightninglabs/taproot-assets/taprpc/universerpc"
Expand Down Expand Up @@ -122,6 +124,44 @@ func testUniverseSync(t *harnessTest) {
)
assertUniverseKeysEqual(t.t, uniIDs, t.tapd, bob)
assertUniverseLeavesEqual(t.t, uniIDs, t.tapd, bob)

// We should also be able to fetch an asset from Bob's Universe, and
// query for that asset with the compressed script key.
firstAssetID := rpcSimpleAssets[0].AssetGenesis.AssetId
firstScriptKey := hex.EncodeToString(rpcSimpleAssets[0].ScriptKey)
firstOutpoint, err := tap.UnmarshalOutpoint(
rpcSimpleAssets[0].ChainAnchor.AnchorOutpoint,
)
require.NoError(t.t, err)
require.Len(t.t, firstScriptKey, btcec.PubKeyBytesLenCompressed*2)

firstAssetProofQuery := unirpc.UniverseKey{
Id: &unirpc.ID{
Id: &unirpc.ID_AssetId{
AssetId: firstAssetID,
},
},
LeafKey: &unirpc.AssetKey{
Outpoint: &unirpc.AssetKey_Op{
Op: &unirpc.Outpoint{
HashStr: firstOutpoint.Hash.String(),
Index: int32(firstOutpoint.Index),
},
},
ScriptKey: &unirpc.AssetKey_ScriptKeyStr{
ScriptKeyStr: firstScriptKey,
},
},
}

// The asset fetched from the universe should match the asset minted
// on the main node, ignoring the zero prev witness from minting.
firstAssetUniProof, err := bob.QueryProof(ctxt, &firstAssetProofQuery)
require.NoError(t.t, err)

firstAssetFromUni := firstAssetUniProof.AssetLeaf.Asset
firstAssetFromUni.PrevWitnesses = nil
assertAsset(t.t, rpcSimpleAssets[0], firstAssetFromUni)
}

// testUniverseREST tests that we're able to properly query the universe state
Expand Down Expand Up @@ -232,6 +272,7 @@ func testUniverseFederation(t *harnessTest) {

// Now that Bob is active, we'll make a set of assets with the main node.
firstAsset := mintAssetsConfirmBatch(t, t.tapd, simpleAssets[:1])
require.Len(t.t, firstAsset, 1)

// We'll now add the main node, as a member of Bob's Universe
// federation. We expect that their state is synchronized shortly after
Expand Down
100 changes: 63 additions & 37 deletions rpcserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -1097,7 +1097,7 @@ func (r *rpcServer) ExportProof(ctx context.Context,
return nil, fmt.Errorf("a valid script key must be specified")
}

scriptKey, err := btcec.ParsePubKey(in.ScriptKey)
scriptKey, err := parseUserKey(in.ScriptKey)
if err != nil {
return nil, fmt.Errorf("invalid script key: %w", err)
}
Expand Down Expand Up @@ -1977,6 +1977,23 @@ func marshalScriptKey(scriptKey asset.ScriptKey) *taprpc.ScriptKey {
return rpcScriptKey
}

// parseUserKey parses a user-provided script or group key, which can be in
// either the Schnorr or Compressed format.
func parseUserKey(scriptKey []byte) (*btcec.PublicKey, error) {
switch len(scriptKey) {
case schnorr.PubKeyBytesLen:
return schnorr.ParsePubKey(scriptKey)

// Truncate the key and then parse as a Schnorr key.
case btcec.PubKeyBytesLenCompressed:
return schnorr.ParsePubKey(scriptKey[1:])

default:
return nil, fmt.Errorf("unknown script key length: %v",
len(scriptKey))
}
}

// marshalKeyDescriptor marshals the native key descriptor into the RPC
// counterpart.
func marshalKeyDescriptor(desc keychain.KeyDescriptor) *taprpc.KeyDescriptor {
Expand Down Expand Up @@ -2162,7 +2179,7 @@ func unmarshalUniID(rpcID *unirpc.ID) (universe.Identifier, error) {
}, nil

case rpcID.GetGroupKey() != nil:
groupKey, err := schnorr.ParsePubKey(rpcID.GetGroupKey())
groupKey, err := parseUserKey(rpcID.GetGroupKey())
if err != nil {
return universe.Identifier{}, err
}
Expand All @@ -2179,7 +2196,7 @@ func unmarshalUniID(rpcID *unirpc.ID) (universe.Identifier, error) {

// TODO(roasbeef): reuse with above

groupKey, err := schnorr.ParsePubKey(groupKeyBytes)
groupKey, err := parseUserKey(groupKeyBytes)
if err != nil {
return universe.Identifier{}, err
}
Expand Down Expand Up @@ -2330,6 +2347,34 @@ func (r *rpcServer) AssetLeaves(ctx context.Context,
return resp, nil
}

// unmarshalOutpoint unmarshals an outpoint from a string received via RPC.
func UnmarshalOutpoint(outpoint string) (*wire.OutPoint, error) {
parts := strings.Split(outpoint, ":")
if len(parts) != 2 {
return nil, errors.New("outpoint should be of form txid:index")
}

txidStr := parts[0]
if hex.DecodedLen(len(txidStr)) != chainhash.HashSize {
return nil, fmt.Errorf("invalid hex-encoded txid %v", txidStr)
}

txid, err := chainhash.NewHashFromStr(txidStr)
if err != nil {
return nil, err
}

outputIndex, err := strconv.Atoi(parts[1])
if err != nil {
return nil, fmt.Errorf("invalid output index: %v", err)
}

return &wire.OutPoint{
Hash: *txid,
Index: uint32(outputIndex),
}, nil
}

// unmarshalLeafKey unmarshals a leaf key from the RPC form.
func unmarshalLeafKey(key *unirpc.AssetKey) (universe.BaseKey, error) {
var (
Expand All @@ -2339,9 +2384,7 @@ func unmarshalLeafKey(key *unirpc.AssetKey) (universe.BaseKey, error) {

switch {
case key.GetScriptKeyBytes() != nil:
pubKey, err := schnorr.ParsePubKey(
key.GetScriptKeyBytes(),
)
pubKey, err := parseUserKey(key.GetScriptKeyBytes())
if err != nil {
return baseKey, err
}
Expand All @@ -2356,9 +2399,7 @@ func unmarshalLeafKey(key *unirpc.AssetKey) (universe.BaseKey, error) {
return baseKey, err
}

pubKey, err := schnorr.ParsePubKey(
scriptKeyBytes,
)
pubKey, err := parseUserKey(scriptKeyBytes)
if err != nil {
return baseKey, err
}
Expand All @@ -2376,32 +2417,13 @@ func unmarshalLeafKey(key *unirpc.AssetKey) (universe.BaseKey, error) {
case key.GetOpStr() != "":
// Parse a bitcoin outpoint in the form txid:index into a
// wire.OutPoint struct.
parts := strings.Split(key.GetOpStr(), ":")
if len(parts) != 2 {
return baseKey, errors.New("outpoint should be of " +
"the form txid:index")
}
txidStr := parts[0]
if hex.DecodedLen(len(txidStr)) != chainhash.HashSize {
return baseKey, fmt.Errorf("invalid hex-encoded "+
"txid %v", txidStr)
}

txid, err := chainhash.NewHashFromStr(txidStr)
outpointStr := key.GetOpStr()
outpoint, err := UnmarshalOutpoint(outpointStr)
if err != nil {
return baseKey, err
}

outputIndex, err := strconv.Atoi(parts[1])
if err != nil {
return baseKey, fmt.Errorf("invalid output "+
"index: %v", err)
}

baseKey.MintingOutpoint = wire.OutPoint{
Hash: *txid,
Index: uint32(outputIndex),
}
baseKey.MintingOutpoint = *outpoint

case key.GetOutpoint() != nil:
op := key.GetOp()
Expand Down Expand Up @@ -2440,22 +2462,26 @@ func (r *rpcServer) marshalIssuanceProof(ctx context.Context,
req *unirpc.UniverseKey,
proof *universe.IssuanceProof) (*unirpc.AssetProofResponse, error) {

uniRoot, err := marshalUniverseRoot(universe.BaseRoot{
Node: proof.UniverseRoot,
})
uniProof, err := marshalUniverseProof(proof.InclusionProof)
if err != nil {
return nil, err
}
uniProof, err := marshalUniverseProof(proof.InclusionProof)

assetLeaf, err := r.marshalAssetLeaf(ctx, proof.Leaf)
if err != nil {
return nil, err
}

assetLeaf, err := r.marshalAssetLeaf(ctx, proof.Leaf)
uniRoot, err := marshalUniverseRoot(universe.BaseRoot{
Node: proof.UniverseRoot,
})
if err != nil {
return nil, err
}

uniRoot.AssetName = assetLeaf.Asset.AssetGenesis.Name
uniRoot.Id = req.Id

return &unirpc.AssetProofResponse{
Req: req,
UniverseRoot: uniRoot,
Expand Down Expand Up @@ -2741,7 +2767,7 @@ func (r *rpcServer) ProveAssetOwnership(ctx context.Context,
return nil, fmt.Errorf("a valid script key must be specified")
}

scriptKey, err := btcec.ParsePubKey(in.ScriptKey)
scriptKey, err := parseUserKey(in.ScriptKey)
if err != nil {
return nil, fmt.Errorf("invalid script key: %w", err)
}
Expand Down
4 changes: 1 addition & 3 deletions tapdb/universe_federation.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package tapdb
import (
"context"
"errors"
"fmt"
"time"

"github.com/lightninglabs/taproot-assets/fn"
Expand Down Expand Up @@ -121,8 +120,7 @@ func (u *UniverseFederationDB) AddServers(ctx context.Context,
// Add context to unique constraint errors.
var uniqueConstraintErr *ErrSqlUniqueConstraintViolation
if errors.As(err, &uniqueConstraintErr) {
return fmt.Errorf("universe name is already added: %w",
err)
return universe.ErrDuplicateUniverse
}

return err
Expand Down
2 changes: 1 addition & 1 deletion tapdb/universe_federation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ func TestUniverseFederationCRUD(t *testing.T) {
// If we try to insert them all again, then we should get an error as
// we ensure the host names are unique.
err = fedDB.AddServers(ctx, addrs...)
require.ErrorContains(t, err, "universe name is already added")
require.ErrorIs(t, err, universe.ErrDuplicateUniverse)

// Next, we should be able to fetch all the active hosts.
dbAddrs, err := fedDB.UniverseServers(ctx)
Expand Down
6 changes: 5 additions & 1 deletion universe/auto_syncer.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package universe

import (
"context"
"errors"
"fmt"
"sync"
"time"
Expand Down Expand Up @@ -108,7 +109,10 @@ func (f *FederationEnvoy) Start() error {
return NewServerAddrFromStr(a)
})
err := f.AddServer(serverAddrs...)
if err != nil {
// On restart, we'll get an error for universe servers already
// inserted in our DB, since we can't store duplicates.
// We can safely ignore that error.
if !errors.Is(err, ErrDuplicateUniverse) {
log.Warnf("unable to add universe servers: %v", err)
}

Expand Down
Loading

0 comments on commit 7ab8a57

Please sign in to comment.