Skip to content
Open
Show file tree
Hide file tree
Changes from 47 commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
edd9646
tapgarden+tapdb: collapse batch-state two-truth dualism
jtobin May 19, 2026
8bc4718
tapdb: pin batch-state memory-coherence invariant via test
jtobin May 19, 2026
f372862
tapgarden: split fundBatch into create vs. apply
jtobin May 19, 2026
eccd3a6
tapgarden: persist seedlings before mirroring them in-memory
jtobin May 19, 2026
23a6f0a
tapgarden: pin validateSeedling/commitSeedling split via test
jtobin May 19, 2026
f527173
tapgarden: find the unique anchor seedling deterministically
jtobin May 19, 2026
f0b3a13
tapgarden: pin uniqueAnchorSeedling contract via test
jtobin May 19, 2026
8cb33b7
tapgarden: drop MintingOutputKey cache; make it pure in its sibling
jtobin May 19, 2026
5fd3049
tapgarden: pin MintingOutputKey purity-in-sibling contract
jtobin May 19, 2026
c6fd14f
tapdb: enforce singleton pre-broadcast minting batch invariant
jtobin May 19, 2026
6013b76
tapgarden: enforce + recover from singleton batch invariant
jtobin May 19, 2026
d71b181
tapd: add --repair.cancel-duplicate-batches recovery flag
jtobin May 19, 2026
2dd3d05
tapdb: dedupe supply_update_events via content hash
jtobin May 20, 2026
a1fd507
tapgarden: import minting output before persisting Broadcast state
jtobin May 20, 2026
022f4f2
itest: pin SignAndFinalizePsbt determinism
jtobin May 20, 2026
5a24c83
tapgarden: add rapid restart-recovery property test for caretaker
jtobin May 20, 2026
2a5590b
tapgarden: make MintingBatch.Copy actually deep
jtobin May 20, 2026
5899008
tapgarden: cap rapid harness iterations to 30 by default
jtobin May 20, 2026
4970ca6
tapgarden: bind cancel responses to their request
jtobin May 20, 2026
1a4a212
tapgarden: snapshot caretaker batch before returning to caller
jtobin May 20, 2026
0aa5254
tapgarden: harden caretaker anchor-index and cancel-reader invariants
jtobin May 20, 2026
8e37cb7
tapgarden: make caretaker completion send abandonable on shutdown
jtobin May 20, 2026
23cd262
tapdb: roll back empty supply commit transitions on event dedup
jtobin May 20, 2026
560c6fe
tapnode: hoist node-side interfaces out of tapgarden
jtobin May 20, 2026
cd2ea67
tapgarden: preserve empty-vs-nil distinction in copyMetaReveal
jtobin May 21, 2026
3321d77
tapnode/tapnodemock: hoist node-side mocks out of tapgarden
jtobin May 21, 2026
7f16beb
tapreorg: extract re-org watcher into its own package
jtobin May 21, 2026
17da882
tapcustody: extract custodian into its own package
jtobin May 21, 2026
3b6148a
tapgarden: collapse state-request plumbing into typed closures
jtobin May 21, 2026
4fe5ad1
asset+proof: hoist deep-copy helpers to their proper types
jtobin May 21, 2026
7b113a5
tapgarden: split MintingStore into BatchStore and MintingRefReader
jtobin May 21, 2026
f0c50cc
tapgarden: share GardenKit between planter and caretaker
jtobin May 21, 2026
a39b9d5
tapgarden: return VerboseBatch directly from FundBatch
jtobin May 21, 2026
e9b486b
tapgarden: drop MintingState enum and SeedlingUpdate.NewState
jtobin May 21, 2026
023f011
tapgarden: rename BatchCaretaker to Cultivator
jtobin May 21, 2026
3f7ca95
tapgarden: name PendingAssetGroup's parts
jtobin May 21, 2026
37e2aff
tapgarden: collapse AssetMintEvent.BatchState into Batch.State()
jtobin May 21, 2026
20bdf69
tapgarden: document UpdateTapSibling's BatchStore-only contract
jtobin May 21, 2026
caee246
tapgarden: drop the Planter interface
jtobin May 21, 2026
4ad39fd
tapgarden+tapdb: thread typed PreCommitBindData through binding API
jtobin May 21, 2026
e0fcf0b
tapdb: extract SupplyPreCommitStore as supply-commit's read gateway
jtobin May 21, 2026
3b69a9e
tapgarden+supplycommit: add GenesisTxAugmenter interface + impl
jtobin May 21, 2026
7cc26c2
tapgarden: route batch minting through GenesisTxAugmenter
jtobin May 21, 2026
8d5e686
tapgarden: delete supply-commit accidents now subsumed by augmenter
jtobin May 21, 2026
3b47f5d
itest+supplyverifier: drop unused tapgarden imports
jtobin May 21, 2026
c54c13a
tapnode: move group verifier generators out of tapgarden
jtobin May 21, 2026
785423d
tapgarden+universe: extract universe publication via MintProofPublisher
jtobin May 21, 2026
bed9f08
docs: add release note
jtobin Jun 4, 2026
8c29974
tapdb: dedupe legacy duplicate events during migration 62 backfill
jtobin Jun 4, 2026
f21a6d7
tapgarden+tapdb: misc review fixes, plus lint
jtobin Jun 4, 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
85 changes: 85 additions & 0 deletions asset/asset.go
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,17 @@ func (g Genesis) TagHash() [sha256.Size]byte {
return sha256.Sum256([]byte(g.Tag))
}

// Copy returns a deep copy of *Genesis. Every field is value-typed
// (OutPoint, string, fixed-size array, scalars), so a struct value
// copy is itself a deep copy. Returns nil if g is nil.
func (g *Genesis) Copy() *Genesis {
if g == nil {
return nil
}
cpy := *g
return &cpy
}

// ID serves as a unique identifier of an asset, resulting from:
//
// sha256(genesisOutPoint || sha256(tag) || sha256(metadata) ||
Expand Down Expand Up @@ -1165,6 +1176,18 @@ func (a *AssetGroup) IsGroupAnchor() (bool, error) {
return a.GroupKey.IsGroupAnchor(a.Genesis.ID())
}

// Copy returns a deep copy of *AssetGroup. The embedded *Genesis and
// *GroupKey are both cloned independently. Returns nil if a is nil.
func (a *AssetGroup) Copy() *AssetGroup {
if a == nil {
return nil
}
return &AssetGroup{
Genesis: a.Genesis.Copy(),
GroupKey: a.GroupKey.Copy(),
}
}

// ExternalKey represents an external key used for deriving and managing
// hierarchical deterministic (HD) wallet addresses according to BIP-86.
type ExternalKey struct {
Expand Down Expand Up @@ -1246,6 +1269,22 @@ func (e *ExternalKey) PubKey() (btcec.PublicKey, error) {
return *pubKey, nil
}

// Copy returns a deep copy of an ExternalKey. The DerivationPath
// slice is duplicated; the embedded hdkeychain.ExtendedKey is value-
// copied (its internal byte slices are private and never mutated
// externally, so shared references are safe in practice).
func (e ExternalKey) Copy() ExternalKey {
out := ExternalKey{
XPub: e.XPub,
MasterFingerprint: e.MasterFingerprint,
}
if e.DerivationPath != nil {
out.DerivationPath = make([]uint32, len(e.DerivationPath))
copy(out.DerivationPath, e.DerivationPath)
}
return out
}

// EqualKeyDescriptors returns true if the two key descriptors are equal.
func EqualKeyDescriptors(a, o keychain.KeyDescriptor) bool {
if a.KeyLocator != o.KeyLocator {
Expand All @@ -1259,6 +1298,29 @@ func EqualKeyDescriptors(a, o keychain.KeyDescriptor) bool {
return a.PubKey.IsEqual(o.PubKey)
}

// CopyPubKey clones a btcec.PublicKey by value-copying its underlying
// secp256k1 struct (two FieldVal coordinates -- entirely value-typed).
// Returns nil if pk is nil. Free function since btcec.PublicKey is a
// foreign type we cannot add methods to.
func CopyPubKey(pk *btcec.PublicKey) *btcec.PublicKey {
if pk == nil {
return nil
}
cpy := *pk
return &cpy
}

// CopyKeyDescriptor returns a KeyDescriptor whose PubKey points to a
// fresh PublicKey value. KeyLocator is two uint32 fields and copied
// trivially. Free function since keychain.KeyDescriptor is a foreign
// type we cannot add methods to.
func CopyKeyDescriptor(kd keychain.KeyDescriptor) keychain.KeyDescriptor {
return keychain.KeyDescriptor{
KeyLocator: kd.KeyLocator,
PubKey: CopyPubKey(kd.PubKey),
}
}

// ScriptKeyDerivationMethod is the method used to derive the script key of an
// asset send output from the recipient's internal key and the asset ID of
// the output. This is used to ensure that the script keys are unique for each
Expand Down Expand Up @@ -1354,6 +1416,20 @@ func (ts *TweakedScriptKey) IsEqual(other *TweakedScriptKey) bool {
return EqualKeyDescriptors(ts.RawKey, other.RawKey)
}

// Copy returns a deep copy of *TweakedScriptKey: the raw key
// descriptor is cloned and the tweak bytes are duplicated. Returns
// nil if ts is nil.
func (ts *TweakedScriptKey) Copy() *TweakedScriptKey {
if ts == nil {
return nil
}
return &TweakedScriptKey{
RawKey: CopyKeyDescriptor(ts.RawKey),
Tweak: bytes.Clone(ts.Tweak),
Type: ts.Type,
}
}

// ScriptKey represents a tweaked Taproot output key encumbering the different
// ways an asset can be spent.
type ScriptKey struct {
Expand Down Expand Up @@ -1400,6 +1476,15 @@ func (s *ScriptKey) IsEqual(otherScriptKey *ScriptKey) bool {
return s.PubKey.IsEqual(otherScriptKey.PubKey)
}

// Copy returns a deep copy of ScriptKey: the tweaked sub-key is
// cloned and the pubkey is value-copied.
func (s ScriptKey) Copy() ScriptKey {
return ScriptKey{
PubKey: CopyPubKey(s.PubKey),
TweakedScriptKey: s.TweakedScriptKey.Copy(),
}
}

// DeclaredAsKnown returns true if this script key has either been derived by
// the local wallet or was explicitly declared to be known by using the
// DeclareScriptKey RPC. Knowing the key conceptually means the key belongs to
Expand Down
23 changes: 23 additions & 0 deletions asset/group_key.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,29 @@ func (g *GroupKey) IsGroupAnchor(assetID ID) (bool, error) {
return expectedGroupPubKey.IsEqual(derivedGroupPubKey), nil
}

// Copy returns a deep copy of *GroupKey: all slice/witness
// substructure is cloned and the embedded key descriptor is rebuilt.
// Returns nil if g is nil.
func (g *GroupKey) Copy() *GroupKey {
if g == nil {
return nil
}
out := &GroupKey{
Version: g.Version,
RawKey: CopyKeyDescriptor(g.RawKey),
GroupPubKey: g.GroupPubKey,
TapscriptRoot: bytes.Clone(g.TapscriptRoot),
CustomTapscriptRoot: g.CustomTapscriptRoot,
}
Comment thread
jtobin marked this conversation as resolved.
if g.Witness != nil {
out.Witness = make(wire.TxWitness, len(g.Witness))
for i, w := range g.Witness {
out.Witness[i] = bytes.Clone(w)
}
}
return out
}

// GroupKeyRequest contains the essential fields used to derive a group key.
type GroupKeyRequest struct {
// Version is the version of the group key construction.
Expand Down
2 changes: 1 addition & 1 deletion backup/rehydrate.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (
)

// ChainQuerier is the interface needed to fetch blockchain data during proof
// rehydration. The tapgarden.ChainBridge interface satisfies this.
// rehydration. The tapnode.ChainBridge interface satisfies this.
type ChainQuerier interface {
// GetBlockByHeight returns a full block given its height.
GetBlockByHeight(ctx context.Context,
Expand Down
13 changes: 13 additions & 0 deletions cmd/tapd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,19 @@ func main() {
os.Exit(0)
}

// If the operator has invoked tapd in one-shot repair mode, run
// the requested repair against the database and exit before
// constructing the full server. The repair tool opens the DB
// with migrations skipped, so it can recover a legacy DB whose
// state would otherwise block a migration from applying.
if cfg.Repair != nil && cfg.Repair.CancelDuplicateBatches {
if err := tapcfg.RunRepairTool(cfg, cfgLogger); err != nil {
_, _ = fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
os.Exit(0)
}

// Enable http profiling server if requested.
if cfg.Profile != "" {
go func() {
Expand Down
90 changes: 90 additions & 0 deletions itest/sign_finalize_psbt_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package itest

import (
"bytes"
"context"

"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/btcutil/psbt"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/lightninglabs/taproot-assets/lndservices"
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
"github.com/stretchr/testify/require"
)

// testSignAndFinalizePsbtDeterministic pins the assumption that
// Wallet.SignAndFinalizePsbt produces byte-identical output when invoked
// twice on the same unsigned PSBT. The minting caretaker's Committed
// branch relies on this on restart: a crash after sign-and-finalize but
// before persisting the signed PSBT causes the next run to re-sign the
// same unsigned PSBT loaded from disk, and we expect the resulting
// signed bytes to match. lnd uses BIP-340 RFC-6979 deterministic
// Schnorr nonces, so this should hold, but it is load-bearing for
// idempotent restart semantics and worth verifying directly.
func testSignAndFinalizePsbtDeterministic(t *harnessTest) {
ctxb := context.Background()
ctx, cancel := context.WithCancel(ctxb)
defer cancel()

lndClient, err := t.newLndClient(t.tapd.cfg.LndNode)
require.NoError(t.t, err)
defer lndClient.Close()

walletAnchor := lndservices.NewLndRpcWalletAnchor(
&lndClient.LndServices,
)

// Build a minimal unsigned tx with one P2TR-shaped dummy output;
// lnd will fund it by adding a wallet input and a change output.
dummyScript := append(
[]byte{txscript.OP_1, txscript.OP_DATA_32},
bytes.Repeat([]byte{0x00}, 32)...,
)
tx := wire.NewMsgTx(2)
tx.AddTxOut(&wire.TxOut{
Value: int64(btcutil.Amount(1000)),
PkScript: dummyScript,
})

unsignedPkt, err := psbt.NewFromUnsignedTx(tx)
require.NoError(t.t, err)

fundedPkt, err := walletAnchor.FundPsbt(
ctx, unsignedPkt, 1, chainfee.SatPerKWeight(3000), -1,
)
require.NoError(t.t, err)

// SignAndFinalizePsbt mutates the input, so each call gets its
// own deep-cloned copy of the unsigned-but-funded PSBT. Round-
// tripping through Serialize/NewFromRawBytes is the cleanest way
// to get an independent value.
clonePsbt := func(p *psbt.Packet) *psbt.Packet {
var buf bytes.Buffer
require.NoError(t.t, p.Serialize(&buf))
clone, err := psbt.NewFromRawBytes(
bytes.NewReader(buf.Bytes()), false,
)
require.NoError(t.t, err)
return clone
}

signed1, err := walletAnchor.SignAndFinalizePsbt(
ctx, clonePsbt(fundedPkt.Pkt),
)
require.NoError(t.t, err)

signed2, err := walletAnchor.SignAndFinalizePsbt(
ctx, clonePsbt(fundedPkt.Pkt),
)
require.NoError(t.t, err)

var buf1, buf2 bytes.Buffer
require.NoError(t.t, signed1.Serialize(&buf1))
require.NoError(t.t, signed2.Serialize(&buf2))

require.Equal(t.t, buf1.Bytes(), buf2.Bytes(),
"SignAndFinalizePsbt must produce byte-identical output "+
"for the same unsigned input; the minting caretaker "+
"restart path relies on this")
}
3 changes: 1 addition & 2 deletions itest/supply_commit_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import (
"github.com/lightninglabs/taproot-assets/mssmt"
"github.com/lightninglabs/taproot-assets/proof"
"github.com/lightninglabs/taproot-assets/rpcserver"
"github.com/lightninglabs/taproot-assets/tapgarden"
"github.com/lightninglabs/taproot-assets/taprpc"
"github.com/lightninglabs/taproot-assets/taprpc/mintrpc"
unirpc "github.com/lightninglabs/taproot-assets/taprpc/universerpc"
Expand Down Expand Up @@ -65,7 +64,7 @@ func assertAnchorTxPreCommitOut(
)
require.NoError(t.t, err)

expectedTxOut, err := tapgarden.PreCommitTxOut(*delegationKey)
expectedTxOut, err := supplycommit.PreCommitTxOut(*delegationKey)
require.NoError(t.t, err)

// The pre-commitment output should be present in the anchor tx exactly
Expand Down
4 changes: 4 additions & 0 deletions itest/test_list_on_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ var allTestCases = []*testCase{
name: "mint external group key chantools",
test: testMintExternalGroupKeyChantools,
},
{
name: "sign and finalize psbt deterministic",
test: testSignAndFinalizePsbtDeterministic,
},
{
name: "mint asset decimal display",
test: testMintAssetWithDecimalDisplayMetaField,
Expand Down
12 changes: 6 additions & 6 deletions lndservices/chain_bridge.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import (
"github.com/lightninglabs/taproot-assets/fn"
"github.com/lightninglabs/taproot-assets/proof"
"github.com/lightninglabs/taproot-assets/tapdb"
"github.com/lightninglabs/taproot-assets/tapgarden"
"github.com/lightninglabs/taproot-assets/tapnode"
"github.com/lightningnetwork/lnd/chainntnfs"
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
)
Expand All @@ -31,7 +31,7 @@ var (
errTxNotFound = fmt.Errorf("transaction not found in proof file")
)

// LndRpcChainBridge is an implementation of the tapgarden.ChainBridge
// LndRpcChainBridge is an implementation of the tapnode.ChainBridge
// interface backed by an active remote lnd node.
type LndRpcChainBridge struct {
// lnd is the active lnd services client.
Expand Down Expand Up @@ -371,22 +371,22 @@ func (l *LndRpcChainBridge) GenProofChainLookup(
}

// A compile time assertion to ensure LndRpcChainBridge meets the
// tapgarden.ChainBridge interface.
var _ tapgarden.ChainBridge = (*LndRpcChainBridge)(nil)
// tapnode.ChainBridge interface.
var _ tapnode.ChainBridge = (*LndRpcChainBridge)(nil)

// ProofChainLookup is an implementation of the asset.ChainLookup interface
// that uses a proof file to look up block height information of previous inputs
// while validating proofs.
type ProofChainLookup struct {
chainBridge tapgarden.ChainBridge
chainBridge tapnode.ChainBridge

assetStore *tapdb.AssetStore

proofFile *proof.File
}

// NewProofChainLookup creates a new ProofChainLookup instance.
func NewProofChainLookup(chainBridge tapgarden.ChainBridge,
func NewProofChainLookup(chainBridge tapnode.ChainBridge,
assetStore *tapdb.AssetStore, proofFile *proof.File) *ProofChainLookup {

return &ProofChainLookup{
Expand Down
6 changes: 3 additions & 3 deletions lndservices/key_ring.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (
"github.com/btcsuite/btcd/btcec/v2"
"github.com/lightninglabs/lndclient"
"github.com/lightninglabs/taproot-assets/asset"
"github.com/lightninglabs/taproot-assets/tapgarden"
"github.com/lightninglabs/taproot-assets/tapnode"
"github.com/lightningnetwork/lnd/keychain"
)

Expand Down Expand Up @@ -108,5 +108,5 @@ func (l *LndRpcKeyRing) DeriveSharedKey(ctx context.Context,
}

// A compile time assertion to ensure LndRpcKeyRing meets the
// tapgarden.KeyRing interface.
var _ tapgarden.KeyRing = (*LndRpcKeyRing)(nil)
// tapnode.KeyRing interface.
var _ tapnode.KeyRing = (*LndRpcKeyRing)(nil)
8 changes: 4 additions & 4 deletions lndservices/wallet_anchor.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import (
"github.com/lightninglabs/lndclient"
"github.com/lightninglabs/taproot-assets/fn"
"github.com/lightninglabs/taproot-assets/tapfreighter"
"github.com/lightninglabs/taproot-assets/tapgarden"
"github.com/lightninglabs/taproot-assets/tapnode"
"github.com/lightninglabs/taproot-assets/tapsend"
"github.com/lightningnetwork/lnd/lnrpc/walletrpc"
"github.com/lightningnetwork/lnd/lnwallet"
Expand All @@ -28,7 +28,7 @@ import (
// useful in high fee environments where we'd otherwise fail to fund the psbt.
const DefaultPsbtMaxFeeRatio = 0.75

// LndRpcWalletAnchor is an implementation of the tapgarden.WalletAnchor
// LndRpcWalletAnchor is an implementation of the tapnode.WalletAnchor
// interfaced backed by an active remote lnd node.
type LndRpcWalletAnchor struct {
lnd *lndclient.LndServices
Expand Down Expand Up @@ -275,7 +275,7 @@ func (l *LndRpcWalletAnchor) MinRelayFee(
}

// A compile time assertion to ensure LndRpcWalletAnchor meets the
// tapgarden.WalletAnchor interface.
var _ tapgarden.WalletAnchor = (*LndRpcWalletAnchor)(nil)
// tapnode.WalletAnchor interface.
var _ tapnode.WalletAnchor = (*LndRpcWalletAnchor)(nil)

var _ tapfreighter.WalletAnchor = (*LndRpcWalletAnchor)(nil)
Loading
Loading