Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
ca196cd
tapgarden: support externally signed mint anchors
SadiqJaf Aug 13, 2026
840ac26
tapgarden: harden custom genesis publication retries
SadiqJaf Aug 13, 2026
6b74f67
tapgarden: validate custom anchor publication policy
SadiqJaf Aug 13, 2026
bc6531c
tapgarden: preserve custom anchor on broadcast retry
SadiqJaf Aug 13, 2026
1df51f2
tapgarden: recover rejected custom anchor publishes
SadiqJaf Aug 13, 2026
b18a9aa
tapgarden: harden custom anchor lifecycle
SadiqJaf Aug 14, 2026
263d28a
tapgarden: close custom anchor lifecycle gaps
SadiqJaf Aug 14, 2026
33b262c
tapgarden: adopt recovered custom caretakers
SadiqJaf Aug 14, 2026
f8fd925
tapgarden: harden custom genesis anchor lifecycle
SadiqJaf Aug 15, 2026
f238d7b
itest: cover foreign MuSig2 custom mint anchors
SadiqJaf Aug 15, 2026
800424f
Merge upstream main into custom genesis anchor branch
SadiqJaf Aug 21, 2026
7b07289
tapgarden: scope custom anchor leases per batch
SadiqJaf Aug 21, 2026
2197aa2
tapgarden: make mint failure cleanup deterministic
SadiqJaf Aug 21, 2026
7595b00
tapgarden: detach lease rollback context
SadiqJaf Aug 21, 2026
8cd7e11
taproot-assets: preserve optional custom anchor interfaces
SadiqJaf Aug 21, 2026
c41efb5
build: fix custom anchor CI checks
SadiqJaf Aug 21, 2026
609b8b5
style: clean up custom anchor lint
SadiqJaf Aug 21, 2026
db41bb4
tapgarden: reserve admission after publish failure
SadiqJaf Aug 21, 2026
fa77c1f
tapgarden: clear custom publish state with active helper
SadiqJaf Aug 21, 2026
f55bfdb
tapgarden: retain publish marker until commit
SadiqJaf Aug 21, 2026
ed3e5bc
tapgarden: detach pre-persistence lease cleanup
SadiqJaf Aug 21, 2026
e648a34
tapgarden: fix custom anchor race and lint
SadiqJaf Aug 21, 2026
cd3121a
Merge upstream main into custom genesis anchor branch
SadiqJaf Aug 26, 2026
27fd632
tapgarden: fix custom anchor CI regressions
SadiqJaf Aug 27, 2026
4f4cbba
Merge upstream main into issue 721 custom genesis PSBT
SadiqJaf Aug 28, 2026
d3bc723
Merge branch 'lightninglabs:main' into codex/issue-721-custom-genesis…
SadiqJaf Sep 2, 2026
b5b089a
lndservices: batch custom anchor lease discovery
SadiqJaf Sep 2, 2026
3105d25
Merge remote-tracking branch 'upstream/main' into codex/issue-721-cus…
SadiqJaf Sep 4, 2026
7cc9a17
tapgarden: safely copy custom anchor staging batch
SadiqJaf Sep 5, 2026
542192b
Merge upstream main into custom genesis PSBT support
SadiqJaf Sep 8, 2026
c78723d
tapdb: merge upstream orphan scan into custom genesis PSBT support
SadiqJaf Sep 9, 2026
7a79f7d
Merge upstream main into custom genesis PSBT support
SadiqJaf Sep 14, 2026
d85ccfd
tapgarden: integrate anchoring watcher into custom genesis support
SadiqJaf Sep 19, 2026
b26b0bd
tapgarden: merge upstream main into custom genesis PSBT support
cursoragent Sep 29, 2026
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
101 changes: 97 additions & 4 deletions cmd/commands/assets.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (

"github.com/btcsuite/btcd/wire/v2"
"github.com/lightninglabs/taproot-assets/address"
"github.com/lightninglabs/taproot-assets/fn"
"github.com/lightninglabs/taproot-assets/rpcserver"
"github.com/lightninglabs/taproot-assets/tapcfg"
"github.com/lightninglabs/taproot-assets/taprpc"
Expand Down Expand Up @@ -126,6 +127,13 @@ const (
shortResponseName = "short"
enableSupplyCommitmentsName = "enable_supply_commitments"
feeRateName = "sat_per_vbyte"
anchorPsbtName = "anchor_psbt"
assetAnchorOutputIndexName = "asset_anchor_output_index"
changeOutputIndexName = "change_output_index"
noChangeOutputName = "no_change_output"
preCommitOutputIndexName = "pre_commit_output_index"
signedPsbtName = "signed_psbt"
outputPsbtName = "output_psbt"
skipProofCourierPingCheckName = "skip-proof-courier-ping-check"
assetAmountName = "amount"
burnOverrideConfirmationName = "override_confirmation_destroy_assets"
Expand Down Expand Up @@ -238,6 +246,7 @@ var mintAssetCommand = cli.Command{
listBatchesCommand,
fundBatchCommand,
sealBatchCommand,
prepareBatchCommand,
finalizeBatchCommand,
cancelBatchCommand,
},
Expand Down Expand Up @@ -496,6 +505,14 @@ var fundBatchCommand = cli.Command{
Usage: "if set, the fee rate in sat/vB to use for " +
"the minting transaction",
},
cli.StringFlag{
Name: anchorPsbtName,
Usage: "caller-funded PSBT file",
},
cli.Uint64Flag{Name: assetAnchorOutputIndexName},
cli.Int64Flag{Name: changeOutputIndexName},
cli.BoolFlag{Name: noChangeOutputName},
cli.Uint64Flag{Name: preCommitOutputIndexName},
},
Action: fundBatch,
}
Expand All @@ -510,10 +527,40 @@ func fundBatch(ctx *cli.Context) error {
return err
}

resp, err := client.FundBatch(ctxc, &mintrpc.FundBatchRequest{
req := &mintrpc.FundBatchRequest{
ShortResponse: ctx.Bool(shortResponseName),
FeeRate: feeRate,
})
}
if path := ctx.String(anchorPsbtName); path != "" {
anchorPath := tapcfg.CleanAndExpandPath(path)
req.AnchorPsbt, err = os.ReadFile(anchorPath)
if err != nil {
return err
}
assetIdx := ctx.Uint64(assetAnchorOutputIndexName)
if assetIdx > math.MaxUint32 {
return fmt.Errorf("asset anchor output index out of " +
"range")
}
changeIdx := ctx.Int64(changeOutputIndexName)
if changeIdx < 0 || changeIdx > math.MaxInt32 {
return fmt.Errorf("change output index out of range")
}
req.AssetAnchorOutputIndex = uint32(assetIdx)
req.ChangeOutputIndex = int32(changeIdx)
req.NoChangeOutput = ctx.Bool(noChangeOutputName)
if ctx.IsSet(preCommitOutputIndexName) {
preCommitIdx := ctx.Uint64(preCommitOutputIndexName)
if preCommitIdx > math.MaxUint32 {
return fmt.Errorf(
"pre-commitment output " +
"index out of range",
)
}
req.PreCommitOutputIndex = fn.Ptr(uint32(preCommitIdx))
}
}
resp, err := client.FundBatch(ctxc, req)
if err != nil {
return fmt.Errorf("unable to fund batch: %w", err)
}
Expand All @@ -522,6 +569,40 @@ func fundBatch(ctx *cli.Context) error {
return nil
}

var prepareBatchCommand = cli.Command{
Name: "prepare",
Usage: "prepare a custom anchor batch for external signing",
Flags: []cli.Flag{cli.StringFlag{
Name: outputPsbtName,
Usage: "write the raw prepared PSBT to this file",
}},
Action: prepareBatch,
}

func prepareBatch(ctx *cli.Context) error {
client, cleanUp := getMintClient(ctx)
defer cleanUp()
resp, err := client.PrepareBatch(
getContext(), &mintrpc.PrepareBatchRequest{},
)
if err != nil {
return fmt.Errorf("unable to prepare batch: %w", err)
}
if path := ctx.String(outputPsbtName); path != "" {
if resp.Batch == nil {
return fmt.Errorf("prepare response has no batch")
}
path = tapcfg.CleanAndExpandPath(path)
err = os.WriteFile(path, resp.Batch.BatchPsbt, 0o600)
if err != nil {
return fmt.Errorf("unable to write prepared PSBT: %w",
err)
}
}
printRespJSON(resp)
return nil
}

var sealBatchCommand = cli.Command{
Name: "seal",
Usage: "seal a batch",
Expand Down Expand Up @@ -616,6 +697,10 @@ var finalizeBatchCommand = cli.Command{
Usage: "if set, the fee rate in sat/vB to use for " +
"the minting transaction",
},
cli.StringFlag{
Name: signedPsbtName,
Usage: "externally signed PSBT file",
},
},
Action: finalizeBatch,
}
Expand All @@ -630,10 +715,18 @@ func finalizeBatch(ctx *cli.Context) error {
return err
}

resp, err := client.FinalizeBatch(ctxc, &mintrpc.FinalizeBatchRequest{
req := &mintrpc.FinalizeBatchRequest{
ShortResponse: ctx.Bool(shortResponseName),
FeeRate: feeRate,
})
}
if path := ctx.String(signedPsbtName); path != "" {
signedPath := tapcfg.CleanAndExpandPath(path)
req.SignedPsbt, err = os.ReadFile(signedPath)
if err != nil {
return err
}
}
resp, err := client.FinalizeBatch(ctxc, req)
if err != nil {
return fmt.Errorf("unable to finalize batch: %w", err)
}
Expand Down
5 changes: 5 additions & 0 deletions docs/release-notes/release-notes-0.9.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,11 @@

## Functional Enhancements

* [PR#2238](https://github.com/lightninglabs/taproot-assets/pull/2238)
adds a caller-controlled Bitcoin anchor PSBT flow for minting batches,
including a prepare/finalize signing boundary, durable restart recovery,
and wallet-input lease management.

* [PR#2266](https://github.com/lightninglabs/taproot-assets/pull/2266)
adds the 'watcher': a daemon-wide service that records each
act of staking local state on a chain outcome as a durable
Expand Down
67 changes: 59 additions & 8 deletions itest/assertions.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,43 @@ func AssetAnchorCheck(txid, blockHash chainhash.Hash) AssetCheck {
}
}

// AssetAnchorOutpointCheck returns a check function that tests an asset's full
// anchor outpoint and block hash.
func AssetAnchorOutpointCheck(
outpoint wire.OutPoint, blockHash chainhash.Hash) AssetCheck {

return func(a *taprpc.Asset) error {
if a.ChainAnchor == nil {
return fmt.Errorf("asset is missing chain anchor field")
}

anchorOutpoint, err := wire.NewOutPointFromString(
a.ChainAnchor.AnchorOutpoint,
)
if err != nil {
return fmt.Errorf("unable to parse outpoint: %w", err)
}
if *anchorOutpoint != outpoint {
return fmt.Errorf(
"unexpected asset anchor "+
"outpoint, got %v wanted %v",
anchorOutpoint, outpoint,
)
}

anchorBlockHash := a.ChainAnchor.AnchorBlockHash
if anchorBlockHash != blockHash.String() {
return fmt.Errorf(
"unexpected asset anchor block "+
"hash, got %v wanted %x",
anchorBlockHash, blockHash[:],
)
}

return nil
}
}

// AssetScriptKeyIsLocalCheck returns a check function that tests an asset's
// script key for being a local key.
func AssetScriptKeyIsLocalCheck(isLocal bool) AssetCheck {
Expand Down Expand Up @@ -2115,6 +2152,20 @@ func AssertAssetsMinted(t *testing.T, tapClient commands.RpcClientsBundle,
assetRequests []*mintrpc.MintAssetRequest, mintTXID,
blockHash chainhash.Hash) []*taprpc.Asset {

return AssertAssetsMintedAtOutpoint(
t, tapClient, assetRequests, wire.OutPoint{Hash: mintTXID},
blockHash,
)
}

// AssertAssetsMintedAtOutpoint makes sure all assets in the minting request
// were minted at the exact anchor outpoint and block. The function returns the
// list of minted assets.
func AssertAssetsMintedAtOutpoint(t *testing.T,
tapClient commands.RpcClientsBundle,
assetRequests []*mintrpc.MintAssetRequest, mintOutpoint wire.OutPoint,
blockHash chainhash.Hash) []*taprpc.Asset {

ctx := context.Background()

// The rest of the anchor information should now be populated as well.
Expand All @@ -2130,7 +2181,8 @@ func AssertAssetsMinted(t *testing.T, tapClient commands.RpcClientsBundle,
ctx, &taprpc.ListAssetRequest{
ScriptKeyType: allScriptKeysQuery,
AnchorOutpoint: &taprpc.OutPoint{
Txid: mintTXID[:],
Txid: mintOutpoint.Hash[:],
OutputIndex: mintOutpoint.Index,
},
},
)
Expand Down Expand Up @@ -2167,7 +2219,7 @@ func AssertAssetsMinted(t *testing.T, tapClient commands.RpcClientsBundle,
mintedAsset := AssertAssetState(
t, confirmedAssets, assetRequest.Asset.Name,
metaHash[:],
AssetAnchorCheck(mintTXID, blockHash),
AssetAnchorOutpointCheck(mintOutpoint, blockHash),
scriptKeyLocalCheck,
AssetVersionCheck(assetRequest.Asset.AssetVersion),
func(a *taprpc.Asset) error {
Expand All @@ -2178,13 +2230,9 @@ func AssertAssetsMinted(t *testing.T, tapClient commands.RpcClientsBundle,
"outpoint")
}

if firstOutpoint == "" {
firstOutpoint = anchor.AnchorOutpoint

return nil
}
if firstOutpoint != "" &&
anchor.AnchorOutpoint != firstOutpoint {

if anchor.AnchorOutpoint != firstOutpoint {
return fmt.Errorf("unexpected anchor "+
"outpoint, got %v wanted %v",
anchor.AnchorOutpoint,
Expand All @@ -2195,6 +2243,9 @@ func AssertAssetsMinted(t *testing.T, tapClient commands.RpcClientsBundle,
return fmt.Errorf("missing block " +
"height")
}
if firstOutpoint == "" {
firstOutpoint = anchor.AnchorOutpoint
}

return nil
},
Expand Down
Loading
Loading