Skip to content
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
9 changes: 8 additions & 1 deletion asset/asset.go
Original file line number Diff line number Diff line change
Expand Up @@ -706,7 +706,7 @@ type GroupKeyRequest struct {

// NewAsset is the asset which we are requesting group membership for.
// A successful request will produce a witness that authorizes this
// to be a member of this asset group.
// asset to be a member of this asset group.
NewAsset *Asset
}

Expand Down Expand Up @@ -752,6 +752,13 @@ type GroupKeyReveal struct {
TapscriptRoot []byte
}

// PendingGroupWitness specifies the asset group witness for an asset seedling
// in an unsealed minting batch.
type PendingGroupWitness struct {
GenID ID
Witness wire.TxWitness
}

// GroupPubKey returns the group public key derived from the group key reveal.
func (g *GroupKeyReveal) GroupPubKey(assetID ID) (*btcec.PublicKey, error) {
rawKey, err := g.RawKey.ToPubKey()
Expand Down
128 changes: 107 additions & 21 deletions cmd/tapcli/assets.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,8 @@ var mintAssetCommand = cli.Command{
Action: mintAsset,
Subcommands: []cli.Command{
listBatchesCommand,
fundBatchCommand,
sealBatchCommand,
finalizeBatchCommand,
cancelBatchCommand,
},
Expand Down Expand Up @@ -176,6 +178,28 @@ func parseMetaType(metaType string,
}
}

func parseFeeRate(ctx *cli.Context) (uint32, error) {
if ctx.IsSet(feeRateName) {
userFeeRate := ctx.Uint64(feeRateName)
if userFeeRate > math.MaxUint32 {
return 0, fmt.Errorf("fee rate exceeds 2^32")
}

// Convert from sat/vB to sat/kw. Round up to the fee floor if
// the specified feerate is too low.
feeRate := chainfee.SatPerKVByte(userFeeRate * 1000).
FeePerKWeight()

if feeRate < chainfee.FeePerKwFloor {
feeRate = chainfee.FeePerKwFloor
}

return uint32(feeRate), nil
}

return uint32(0), nil
}

func mintAsset(ctx *cli.Context) error {
switch {
case ctx.String(assetTagName) == "":
Expand Down Expand Up @@ -291,11 +315,14 @@ func mintAsset(ctx *cli.Context) error {
return nil
}

var finalizeBatchCommand = cli.Command{
Name: "finalize",
ShortName: "f",
Usage: "finalize a batch",
Description: "Attempt to finalize a pending batch.",
var fundBatchCommand = cli.Command{
Name: "fund",
Usage: "fund a batch",
Description: `
Attempt to fund a pending batch, or create a new funded batch if no
batch exists yet. This is only needed if batch funding should happen
separately from batch finalization. Otherwise, finalize can be used.
`,
Flags: []cli.Flag{
cli.BoolFlag{
Name: shortResponseName,
Expand All @@ -310,29 +337,88 @@ var finalizeBatchCommand = cli.Command{
"the minting transaction",
},
},
Action: finalizeBatch,
Action: fundBatch,
}

func parseFeeRate(ctx *cli.Context) (uint32, error) {
if ctx.IsSet(feeRateName) {
userFeeRate := ctx.Uint64(feeRateName)
if userFeeRate > math.MaxUint32 {
return 0, fmt.Errorf("fee rate exceeds 2^32")
}
func fundBatch(ctx *cli.Context) error {
ctxc := getContext()
client, cleanUp := getMintClient(ctx)
defer cleanUp()

// Convert from sat/vB to sat/kw. Round up to the fee floor if
// the specified feerate is too low.
feeRate := chainfee.SatPerKVByte(userFeeRate * 1000).
FeePerKWeight()
feeRate, err := parseFeeRate(ctx)
if err != nil {
return err
}

if feeRate < chainfee.FeePerKwFloor {
feeRate = chainfee.FeePerKwFloor
}
resp, err := client.FundBatch(ctxc, &mintrpc.FundBatchRequest{
ShortResponse: ctx.Bool(shortResponseName),
FeeRate: feeRate,
})
if err != nil {
return fmt.Errorf("unable to fund batch: %w", err)
}

return uint32(feeRate), nil
printRespJSON(resp)
return nil
}

var sealBatchCommand = cli.Command{
Name: "seal",
Usage: "seal a batch",
Description: `
Attempt to seal the pending batch by creating asset group witnesses for
all assets in the batch. Custom witnesses can only be submitted via RPC.
This command is only needed if batch sealing should happen separately
from batch finalization. Otherwise, finalize can be used.
`,
Flags: []cli.Flag{
cli.BoolFlag{
Name: shortResponseName,
Usage: "if true, then the current assets within the " +
"batch will not be returned in the response " +
"in order to avoid printing a large amount " +
"of data in case of large batches",
},
},
Hidden: true,
Action: sealBatch,
}

func sealBatch(ctx *cli.Context) error {
ctxc := getContext()
client, cleanUp := getMintClient(ctx)
defer cleanUp()

resp, err := client.SealBatch(ctxc, &mintrpc.SealBatchRequest{
ShortResponse: ctx.Bool(shortResponseName),
})
if err != nil {
return fmt.Errorf("unable to seal batch: %w", err)
}

return uint32(0), nil
printRespJSON(resp)
return nil
}

var finalizeBatchCommand = cli.Command{
Name: "finalize",
Usage: "finalize a batch",
Description: "Attempt to finalize a pending batch.",
Flags: []cli.Flag{
cli.BoolFlag{
Name: shortResponseName,
Usage: "if true, then the current assets within the " +
"batch will not be returned in the response " +
"in order to avoid printing a large amount " +
"of data in case of large batches",
},
cli.Uint64Flag{
Name: feeRateName,
Usage: "if set, the fee rate in sat/vB to use for " +
"the minting transaction",
},
},
Action: finalizeBatch,
}

func finalizeBatch(ctx *cli.Context) error {
Expand Down
10 changes: 5 additions & 5 deletions internal/test/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ var (

HexCompressedPubKeyLen = hex.EncodedLen(btcec.PubKeyBytesLenCompressed)
HexTaprootPkScript = hex.EncodedLen(input.P2TRSize)

DefaultHashLockWitness = []byte("foobar")
)

// RandBool rolls a random boolean.
Expand Down Expand Up @@ -426,8 +428,7 @@ func ReadTestDataFile(t *testing.T, fileName string) string {
func BuildTapscriptTreeNoReveal(t *testing.T,
internalKey *btcec.PublicKey) txscript.TapBranch {

hashLockWitness := []byte("foobar")
hashLockLeaf := ScriptHashLock(t, hashLockWitness)
hashLockLeaf := ScriptHashLock(t, bytes.Clone(DefaultHashLockWitness))
sigLeaf := ScriptSchnorrSig(t, internalKey)

tree := txscript.AssembleTaprootScriptTree(hashLockLeaf, sigLeaf)
Expand All @@ -445,9 +446,8 @@ func BuildTapscriptTree(t *testing.T, useHashLock, valid bool,

// Let's create a taproot asset script now. This is a hash lock with a
// simple preimage of "foobar".
hashLockWitness := []byte("foobar")
invalidHashLockWitness := []byte("not-foobar")
hashLockLeaf := ScriptHashLock(t, hashLockWitness)
hashLockLeaf := ScriptHashLock(t, bytes.Clone(DefaultHashLockWitness))

// Let's add a second script output as well to test the partial reveal.
sigLeaf := ScriptSchnorrSig(t, internalKey)
Expand All @@ -465,7 +465,7 @@ func BuildTapscriptTree(t *testing.T, useHashLock, valid bool,
testTapScript = input.TapscriptPartialReveal(
internalKey, hashLockLeaf, inclusionProof[:],
)
scriptWitness = hashLockWitness
scriptWitness = DefaultHashLockWitness

if !valid {
scriptWitness = invalidHashLockWitness
Expand Down
88 changes: 84 additions & 4 deletions itest/assertions.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,79 @@ func AssetScriptKeyIsBurnCheck(isBurn bool) AssetCheck {
}
}

func AssetScriptKeyCheck(scriptKey *taprpc.ScriptKey) AssetCheck {
return func(a *taprpc.Asset) error {
if scriptKey == nil {
return nil
}

if !bytes.Equal(scriptKey.PubKey, a.ScriptKey[1:]) {
return fmt.Errorf("unexpected script key, "+
"wanted %x, got %x ", scriptKey.PubKey,
a.ScriptKey[1:])
}

return nil
}
}

func AssetIsGroupedCheck(newGrouped, grouped bool) AssetCheck {
return func(a *taprpc.Asset) error {
needsGroup := newGrouped || grouped
if !needsGroup {
return nil
}

if needsGroup && a.AssetGroup == nil {
return fmt.Errorf("unexpected missing asset group")
}

return nil
}
}

func AssetGroupInternalKeyCheck(key *taprpc.KeyDescriptor) AssetCheck {
return func(a *taprpc.Asset) error {
if key == nil {
return nil
}

if a.AssetGroup == nil {
return fmt.Errorf("unexpected missing asset group")
}

expectedKey := key.RawKeyBytes
groupInternal := a.AssetGroup.RawGroupKey
if !bytes.Equal(expectedKey, groupInternal) {
return fmt.Errorf("mistmatched group internal "+
"key, wanted %x, got %x ", expectedKey,
groupInternal)
}

return nil
}
}

func AssetGroupTapscriptRootCheck(root []byte) AssetCheck {
return func(a *taprpc.Asset) error {
if len(root) == 0 {
return nil
}

switch {
case a.AssetGroup == nil:
return fmt.Errorf("unexpected missing asset group")

case !bytes.Equal(root, a.AssetGroup.TapscriptRoot):
return fmt.Errorf("mistmatched group tapscript roots, "+
"wanted %x, got %x",
root, a.AssetGroup.TapscriptRoot)
}

return nil
}
}

// AssetVersionCheck returns a check function that tests an asset's version.
func AssetVersionCheck(version taprpc.AssetVersion) AssetCheck {
return func(a *taprpc.Asset) error {
Expand Down Expand Up @@ -316,9 +389,9 @@ func WaitForBatchState(t *testing.T, ctx context.Context,
len(batchResp.Batches))
}

if batchResp.Batches[0].State != targetState {
if batchResp.Batches[0].Batch.State != targetState {
return fmt.Errorf("expected batch state %v, got %v",
targetState, batchResp.Batches[0].State)
targetState, batchResp.Batches[0].Batch.State)
}

return nil
Expand Down Expand Up @@ -1648,8 +1721,7 @@ func VerifyGroupAnchor(t *testing.T, assets []*taprpc.Asset,
// AssertAssetsMinted makes sure all assets in the minting request were in fact
// minted in the given anchor TX and block. The function returns the list of
// minted assets.
func AssertAssetsMinted(t *testing.T,
tapClient TapdClient,
func AssertAssetsMinted(t *testing.T, tapClient TapdClient,
assetRequests []*mintrpc.MintAssetRequest, mintTXID,
blockHash chainhash.Hash) []*taprpc.Asset {

Expand Down Expand Up @@ -1706,6 +1778,14 @@ func AssertAssetsMinted(t *testing.T,

return nil
},
AssetScriptKeyCheck(assetRequest.Asset.ScriptKey),
AssetIsGroupedCheck(
assetRequest.Asset.NewGroupedAsset,
assetRequest.Asset.GroupedAsset,
),
AssetGroupTapscriptRootCheck(
assetRequest.Asset.GroupTapscriptRoot,
),
)

assetList = append(assetList, mintedAsset)
Expand Down
19 changes: 11 additions & 8 deletions itest/assets_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -290,18 +290,20 @@ func testMintAssetNameCollisionError(t *harnessTest) {
allBatches := rpcBatches.Batches
require.Len(t.t, allBatches, 2)

isCollidingBatch := func(batch *mintrpc.MintingBatch) bool {
if len(batch.Assets) == 0 {
isCollidingBatch := func(batch *mintrpc.VerboseBatch) bool {
if len(batch.Batch.Assets) == 0 {
return false
}

return batch.Assets[0].AssetType == taprpc.AssetType_COLLECTIBLE
assetType := batch.Batch.Assets[0].AssetType

return assetType == taprpc.AssetType_COLLECTIBLE
}
batchCollide, err := fn.First(allBatches, isCollidingBatch)
require.NoError(t.t, err)

require.Len(t.t, batchCollide.Assets, 1)
equalityCheck(assetCollide.Asset, batchCollide.Assets[0])
require.Len(t.t, batchCollide.Batch.Assets, 1)
equalityCheck(assetCollide.Asset, batchCollide.Batch.Assets[0])

cancelBatchKey, err := t.tapd.CancelBatch(
ctxt, &mintrpc.CancelBatchRequest{},
Expand All @@ -323,11 +325,12 @@ func testMintAssetNameCollisionError(t *harnessTest) {

require.Len(t.t, cancelBatch.Batches, 1)
cancelBatchCollide := cancelBatch.Batches[0]
require.Len(t.t, cancelBatchCollide.Assets, 1)
require.Len(t.t, cancelBatchCollide.Batch.Assets, 1)
equalityCheckSeedlings(
batchCollide.Assets[0], cancelBatchCollide.Assets[0],
batchCollide.Batch.Assets[0],
cancelBatchCollide.Batch.Assets[0],
)
cancelBatchState := cancelBatchCollide.State
cancelBatchState := cancelBatchCollide.Batch.State
require.Equal(
t.t, cancelBatchState,
mintrpc.BatchState_BATCH_STATE_SEEDLING_CANCELLED,
Expand Down
Loading