diff --git a/config.go b/config.go index cf45a3c33..80509bfa5 100644 --- a/config.go +++ b/config.go @@ -10,6 +10,7 @@ import ( "github.com/lightninglabs/taproot-assets/monitoring" "github.com/lightninglabs/taproot-assets/proof" "github.com/lightninglabs/taproot-assets/rfq" + "github.com/lightninglabs/taproot-assets/tapchannel" "github.com/lightninglabs/taproot-assets/tapdb" "github.com/lightninglabs/taproot-assets/tapfreighter" "github.com/lightninglabs/taproot-assets/tapgarden" @@ -125,6 +126,8 @@ type Config struct { UniverseStats universe.Telemetry + AuxLeafCreator *tapchannel.AuxLeafCreator + // UniversePublicAccess is flag which, If true, and the Universe server // is on a public interface, valid proof from remote parties will be // accepted, and proofs will be queryable by remote parties. diff --git a/go.mod b/go.mod index 81ab34846..b3ee70dbe 100644 --- a/go.mod +++ b/go.mod @@ -33,6 +33,7 @@ require ( github.com/lightningnetwork/lnd v0.17.0-beta.rc6.0.20240501135111-81970eac6a5a github.com/lightningnetwork/lnd/cert v1.2.2 github.com/lightningnetwork/lnd/clock v1.1.1 + github.com/lightningnetwork/lnd/fn v1.0.5 github.com/lightningnetwork/lnd/tlv v1.2.6 github.com/lightningnetwork/lnd/tor v1.1.2 github.com/ory/dockertest/v3 v3.10.0 @@ -121,7 +122,6 @@ require ( github.com/lightninglabs/lightning-node-connect v0.2.5-alpha // indirect github.com/lightninglabs/neutrino v0.16.0 // indirect github.com/lightningnetwork/lightning-onion v1.2.1-0.20230823005744-06182b1d7d2f // indirect - github.com/lightningnetwork/lnd/fn v1.0.5 // indirect github.com/lightningnetwork/lnd/healthcheck v1.2.4 // indirect github.com/lightningnetwork/lnd/kvdb v1.4.6 // indirect github.com/lightningnetwork/lnd/queue v1.1.1 // indirect diff --git a/server.go b/server.go index d1b9fbdbf..d679b9793 100644 --- a/server.go +++ b/server.go @@ -18,9 +18,14 @@ import ( "github.com/lightninglabs/taproot-assets/taprpc" "github.com/lightningnetwork/lnd" "github.com/lightningnetwork/lnd/build" + "github.com/lightningnetwork/lnd/channeldb" + lfn "github.com/lightningnetwork/lnd/fn" "github.com/lightningnetwork/lnd/lncfg" "github.com/lightningnetwork/lnd/lnrpc" + "github.com/lightningnetwork/lnd/lnwallet" + "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/macaroons" + "github.com/lightningnetwork/lnd/tlv" "google.golang.org/grpc" "google.golang.org/grpc/keepalive" "gopkg.in/macaroon-bakery.v2/bakery" @@ -174,6 +179,11 @@ func (s *Server) initialize(interceptorChain *rpcperms.InterceptorChain) error { return fmt.Errorf("unable to start RFQ manager: %w", err) } + // Start the auxiliary leaf creator. + if err := s.cfg.AuxLeafCreator.Start(); err != nil { + return fmt.Errorf("unable to start aux leaf creator: %w", err) + } + if s.cfg.UniversePublicAccess { err := s.cfg.UniverseFederation.SetAllowPublicAccess() if err != nil { @@ -610,6 +620,10 @@ func (s *Server) Stop() error { return err } + if err := s.cfg.AuxLeafCreator.Stop(); err != nil { + return err + } + if s.macaroonService != nil { err := s.macaroonService.Stop() if err != nil { @@ -623,3 +637,98 @@ func (s *Server) Stop() error { return nil } + +// A compile-time check to ensure that Server fully implements the +// lnwallet.AuxLeafStore interface. +var _ lnwallet.AuxLeafStore = (*Server)(nil) + +// FetchLeavesFromView attempts to fetch the auxiliary leaves that correspond to +// the passed aux blob, and pending fully evaluated HTLC view. +// +// NOTE: This method is part of the lnwallet.AuxLeafStore interface. +func (s *Server) FetchLeavesFromView(chanState *channeldb.OpenChannel, + prevBlob tlv.Blob, view *lnwallet.HtlcView, isOurCommit bool, + ourBalance, theirBalance lnwire.MilliSatoshi, + keys lnwallet.CommitmentKeyRing) (lfn.Option[lnwallet.CommitAuxLeaves], + lnwallet.CommitSortFunc, error) { + + srvrLog.Debugf("FetchLeavesFromView called") + + select { + case <-s.ready: + case <-s.quit: + return lfn.None[lnwallet.CommitAuxLeaves](), nil, + fmt.Errorf("tapd is shutting down") + } + + return s.cfg.AuxLeafCreator.FetchLeavesFromView( + chanState, prevBlob, view, isOurCommit, ourBalance, + theirBalance, keys, + ) +} + +// FetchLeavesFromCommit attempts to fetch the auxiliary leaves that +// correspond to the passed aux blob, and an existing channel +// commitment. +// +// NOTE: This method is part of the lnwallet.AuxLeafStore interface. +func (s *Server) FetchLeavesFromCommit(chanState *channeldb.OpenChannel, + com channeldb.ChannelCommitment, + keys lnwallet.CommitmentKeyRing) (lfn.Option[lnwallet.CommitAuxLeaves], + error) { + + srvrLog.Debugf("FetchLeavesFromCommit called") + + select { + case <-s.ready: + case <-s.quit: + return lfn.None[lnwallet.CommitAuxLeaves](), + fmt.Errorf("tapd is shutting down") + } + + return s.cfg.AuxLeafCreator.FetchLeavesFromCommit(chanState, com, keys) +} + +// FetchLeavesFromRevocation attempts to fetch the auxiliary leaves +// from a channel revocation that stores balance + blob information. +// +// NOTE: This method is part of the lnwallet.AuxLeafStore interface. +func (s *Server) FetchLeavesFromRevocation( + rev *channeldb.RevocationLog) (lfn.Option[lnwallet.CommitAuxLeaves], + error) { + + srvrLog.Debugf("FetchLeavesFromRevocation called") + + select { + case <-s.ready: + case <-s.quit: + return lfn.None[lnwallet.CommitAuxLeaves](), + fmt.Errorf("tapd is shutting down") + } + + return s.cfg.AuxLeafCreator.FetchLeavesFromRevocation(rev) +} + +// ApplyHtlcView serves as the state transition function for the custom +// channel's blob. Given the old blob, and an HTLC view, then a new +// blob should be returned that reflects the pending updates. +// +// NOTE: This method is part of the lnwallet.AuxLeafStore interface. +func (s *Server) ApplyHtlcView(chanState *channeldb.OpenChannel, + prevBlob tlv.Blob, originalView *lnwallet.HtlcView, isOurCommit bool, + ourBalance, theirBalance lnwire.MilliSatoshi, + keys lnwallet.CommitmentKeyRing) (lfn.Option[tlv.Blob], error) { + + srvrLog.Debugf("ApplyHtlcView called") + + select { + case <-s.ready: + case <-s.quit: + return lfn.None[tlv.Blob](), fmt.Errorf("tapd is shutting down") + } + + return s.cfg.AuxLeafCreator.ApplyHtlcView( + chanState, prevBlob, originalView, isOurCommit, ourBalance, + theirBalance, keys, + ) +} diff --git a/tapcfg/server.go b/tapcfg/server.go index 583fa9d9b..f849f1fe0 100644 --- a/tapcfg/server.go +++ b/tapcfg/server.go @@ -14,6 +14,7 @@ import ( "github.com/lightninglabs/taproot-assets/asset" "github.com/lightninglabs/taproot-assets/proof" "github.com/lightninglabs/taproot-assets/rfq" + "github.com/lightninglabs/taproot-assets/tapchannel" "github.com/lightninglabs/taproot-assets/tapdb" "github.com/lightninglabs/taproot-assets/tapdb/sqlc" "github.com/lightninglabs/taproot-assets/tapfreighter" @@ -340,6 +341,12 @@ func genServerConfig(cfg *Config, cfgLogger btclog.Logger, return nil, err } + auxLeafCreator := tapchannel.NewAuxLeafCreator( + &tapchannel.LeafCreatorConfig{ + ChainParams: &tapChainParams, + }, + ) + return &tap.Config{ DebugLevel: cfg.DebugLevel, RuntimeID: runtimeID, @@ -413,6 +420,7 @@ func genServerConfig(cfg *Config, cfgLogger btclog.Logger, UniverseQueriesPerSecond: cfg.Universe.UniverseQueriesPerSecond, UniverseQueriesBurst: cfg.Universe.UniverseQueriesBurst, RfqManager: rfqManager, + AuxLeafCreator: auxLeafCreator, LogWriter: cfg.LogWriter, DatabaseConfig: &tap.DatabaseConfig{ RootKeyStore: tapdb.NewRootKeyStore(rksDB), diff --git a/tapchannel/aux_leaf_creator.go b/tapchannel/aux_leaf_creator.go new file mode 100644 index 000000000..5f3fa9a86 --- /dev/null +++ b/tapchannel/aux_leaf_creator.go @@ -0,0 +1,443 @@ +package tapchannel + +import ( + "bytes" + "context" + "fmt" + "sync" + "time" + + "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcd/wire" + "github.com/lightninglabs/taproot-assets/address" + "github.com/lightninglabs/taproot-assets/fn" + "github.com/lightninglabs/taproot-assets/proof" + cmsg "github.com/lightninglabs/taproot-assets/tapchannelmsg" + "github.com/lightninglabs/taproot-assets/tapsend" + "github.com/lightningnetwork/lnd/channeldb" + lfn "github.com/lightningnetwork/lnd/fn" + "github.com/lightningnetwork/lnd/input" + "github.com/lightningnetwork/lnd/lnwallet" + "github.com/lightningnetwork/lnd/lnwire" + "github.com/lightningnetwork/lnd/tlv" +) + +const ( + // DefaultTimeout is the default timeout we use for RPC and database + // operations. + DefaultTimeout = 30 * time.Second +) + +// LeafCreatorConfig defines the configuration for the auxiliary leaf creator. +type LeafCreatorConfig struct { + ChainParams *address.ChainParams +} + +// AuxLeafCreator is a Taproot Asset auxiliary leaf creator that can be used to +// create auxiliary leaves for Taproot Asset channels. +type AuxLeafCreator struct { + startOnce sync.Once + stopOnce sync.Once + + cfg *LeafCreatorConfig + + // ContextGuard provides a wait group and main quit channel that can be + // used to create guarded contexts. + *fn.ContextGuard +} + +// NewAuxLeafCreator creates a new Taproot Asset auxiliary leaf creator based on +// the passed config. +func NewAuxLeafCreator(cfg *LeafCreatorConfig) *AuxLeafCreator { + return &AuxLeafCreator{ + cfg: cfg, + ContextGuard: &fn.ContextGuard{ + DefaultTimeout: DefaultTimeout, + Quit: make(chan struct{}), + }, + } +} + +// Start attempts to start a new aux leaf creator. +func (c *AuxLeafCreator) Start() error { + var startErr error + c.startOnce.Do(func() { + log.Info("Starting aux leaf creator") + }) + return startErr +} + +// Stop signals for a custodian to gracefully exit. +func (c *AuxLeafCreator) Stop() error { + var stopErr error + c.stopOnce.Do(func() { + log.Info("Stopping aux leaf creator") + + close(c.Quit) + c.Wg.Wait() + }) + + return stopErr +} + +// A compile-time check to ensure that AuxLeafCreator fully implements the +// lnwallet.AuxLeafStore interface. +var _ lnwallet.AuxLeafStore = (*AuxLeafCreator)(nil) + +// FetchLeavesFromView attempts to fetch the auxiliary leaves that correspond to +// the passed aux blob, and pending fully evaluated HTLC view. +func (c *AuxLeafCreator) FetchLeavesFromView(chanState *channeldb.OpenChannel, + prevBlob tlv.Blob, originalView *lnwallet.HtlcView, isOurCommit bool, + ourBalance, theirBalance lnwire.MilliSatoshi, + keys lnwallet.CommitmentKeyRing) (lfn.Option[lnwallet.CommitAuxLeaves], + lnwallet.CommitSortFunc, error) { + + none := lfn.None[lnwallet.CommitAuxLeaves]() + + // If the channel has no custom blob, we don't need to do anything. + if chanState.CustomBlob.IsNone() { + return none, nil, nil + } + + chanAssetState, err := cmsg.DecodeOpenChannel( + chanState.CustomBlob.UnsafeFromSome(), + ) + if err != nil { + return none, nil, fmt.Errorf("unable to decode channel asset "+ + "state: %w", err) + } + + prevState, err := cmsg.DecodeCommitment(prevBlob) + if err != nil { + return none, nil, fmt.Errorf("unable to decode prev commit "+ + "state: %w", err) + } + + allocations, newCommitment, err := c.generateAllocations( + prevState, chanState, chanAssetState, isOurCommit, ourBalance, + theirBalance, originalView, keys, + ) + if err != nil { + return none, nil, fmt.Errorf("unable to generate allocations: "+ + "%w", err) + } + + customCommitSort := func(tx *wire.MsgTx, uint32s []uint32) error { + return InPlaceCustomCommitSort(tx, uint32s, allocations) + } + + return lfn.Some(newCommitment.Leaves()), customCommitSort, nil +} + +// FetchLeavesFromCommit attempts to fetch the auxiliary leaves that correspond +// to the passed aux blob, and an existing channel commitment. +func (c *AuxLeafCreator) FetchLeavesFromCommit(chanState *channeldb.OpenChannel, + com channeldb.ChannelCommitment, + keys lnwallet.CommitmentKeyRing) (lfn.Option[lnwallet.CommitAuxLeaves], + error) { + + none := lfn.None[lnwallet.CommitAuxLeaves]() + + // If the commitment has no custom blob, we don't need to do anything. + if com.CustomBlob.IsNone() { + return none, nil + } + + commitment, err := cmsg.DecodeCommitment( + com.CustomBlob.UnsafeFromSome(), + ) + if err != nil { + return none, fmt.Errorf("unable to decode commitment: %w", err) + } + + incomingHtlcs := commitment.IncomingHtlcAssets.Val.HtlcOutputs + incomingHtlcLeaves := commitment.AuxLeaves.Val.IncomingHtlcLeaves. + Val.HtlcAuxLeaves + outgoingHtlcs := commitment.OutgoingHtlcAssets.Val.HtlcOutputs + outgoingHtlcLeaves := commitment.AuxLeaves.Val. + OutgoingHtlcLeaves.Val.HtlcAuxLeaves + for idx := range com.Htlcs { + htlc := com.Htlcs[idx] + htlcIdx := htlc.HtlcIndex + + if htlc.Incoming { + htlcOutputs := incomingHtlcs[htlcIdx].Outputs + auxLeaf := incomingHtlcLeaves[htlcIdx].AuxLeaf + leaf, err := CreateSecondLevelHtlcTx( + chanState, com.CommitTx, htlc.Amt.ToSatoshis(), + keys, c.cfg.ChainParams, htlcOutputs, + ) + if err != nil { + return none, fmt.Errorf("unable to create "+ + "second level HTLC leaf: %w", err) + } + + existingLeaf := lfn.MapOption( + func(l cmsg.TapLeafRecord) txscript.TapLeaf { + return l.Leaf + }, + )(auxLeaf.ValOpt()) + + incomingHtlcLeaves[htlcIdx] = cmsg.NewHtlcAuxLeaf( + input.HtlcAuxLeaf{ + AuxTapLeaf: existingLeaf, + SecondLevelLeaf: leaf, + }, + ) + } else { + htlcOutputs := outgoingHtlcs[htlcIdx].Outputs + auxLeaf := outgoingHtlcLeaves[htlcIdx].AuxLeaf + leaf, err := CreateSecondLevelHtlcTx( + chanState, com.CommitTx, htlc.Amt.ToSatoshis(), + keys, c.cfg.ChainParams, htlcOutputs, + ) + if err != nil { + return none, fmt.Errorf("unable to create "+ + "second level HTLC leaf: %w", err) + } + + existingLeaf := lfn.MapOption( + func(l cmsg.TapLeafRecord) txscript.TapLeaf { + return l.Leaf + }, + )(auxLeaf.ValOpt()) + + outgoingHtlcLeaves[htlcIdx] = cmsg.NewHtlcAuxLeaf( + input.HtlcAuxLeaf{ + AuxTapLeaf: existingLeaf, + SecondLevelLeaf: leaf, + }, + ) + } + } + + return lfn.Some(commitment.Leaves()), nil +} + +// FetchLeavesFromRevocation attempts to fetch the auxiliary leaves +// from a channel revocation that stores balance + blob information. +func (c *AuxLeafCreator) FetchLeavesFromRevocation( + rev *channeldb.RevocationLog) (lfn.Option[lnwallet.CommitAuxLeaves], + error) { + + none := lfn.None[lnwallet.CommitAuxLeaves]() + + // If the revocation has no custom blob, we don't need to do anything. + if rev.CustomBlob.ValOpt().IsNone() { + return none, nil + } + + commitment, err := cmsg.DecodeCommitment( + rev.CustomBlob.ValOpt().UnsafeFromSome(), + ) + if err != nil { + return none, fmt.Errorf("unable to decode commitment: %w", err) + } + + return lfn.Some(commitment.Leaves()), nil +} + +// ApplyHtlcView serves as the state transition function for the custom +// channel's blob. Given the old blob, and an HTLC view, then a new +// blob should be returned that reflects the pending updates. +func (c *AuxLeafCreator) ApplyHtlcView(chanState *channeldb.OpenChannel, + prevBlob tlv.Blob, originalView *lnwallet.HtlcView, isOurCommit bool, + ourBalance, theirBalance lnwire.MilliSatoshi, + keys lnwallet.CommitmentKeyRing) (lfn.Option[tlv.Blob], error) { + + none := lfn.None[tlv.Blob]() + + // If the channel has no custom blob, we don't need to do anything. + if chanState.CustomBlob.IsNone() { + return none, nil + } + + chanAssetState, err := cmsg.DecodeOpenChannel( + chanState.CustomBlob.UnsafeFromSome(), + ) + if err != nil { + return none, fmt.Errorf("unable to decode channel asset "+ + "state: %w", err) + } + + prevState, err := cmsg.DecodeCommitment(prevBlob) + if err != nil { + return none, fmt.Errorf("unable to decode prev commit state: "+ + "%w", err) + } + + _, newCommitment, err := c.generateAllocations( + prevState, chanState, chanAssetState, isOurCommit, ourBalance, + theirBalance, originalView, keys, + ) + if err != nil { + return none, fmt.Errorf("unable to generate allocations: %w", + err) + } + + var buf bytes.Buffer + err = newCommitment.Encode(&buf) + if err != nil { + return none, fmt.Errorf("unable to encode commitment: %w", err) + } + + return lfn.Some(buf.Bytes()), nil +} + +// generateAllocations generates allocations for a channel commitment. +func (c *AuxLeafCreator) generateAllocations(prevState *cmsg.Commitment, + chanState *channeldb.OpenChannel, chanAssetState *cmsg.OpenChannel, + isOurCommit bool, ourBalance, theirBalance lnwire.MilliSatoshi, + originalView *lnwallet.HtlcView, + keys lnwallet.CommitmentKeyRing) ([]*Allocation, *cmsg.Commitment, + error) { + + log.Tracef("Generating allocations, ourCommit=%v, ourBalance=%d, "+ + "theirBalance=%d", isOurCommit, ourBalance, theirBalance) + + // Everywhere we have a isOurCommit boolean we define the local/remote + // balances as seen from the perspective of the local node. So if this + // is not our commit, then the previous state we take the balance from + // is flipped from the point of view of the rest of the code. So we + // need to flip the balances here in order for the rest of the code to + // work correctly. + localAssetStartBalance := prevState.LocalAssets.Val.Sum() + remoteAssetStartBalance := prevState.RemoteAssets.Val.Sum() + if !isOurCommit { + localAssetStartBalance, remoteAssetStartBalance = + remoteAssetStartBalance, localAssetStartBalance + } + + // Process all HTLCs in the view to compute the new asset balance. + ourAssetBalance, theirAssetBalance, filteredView, err := ComputeView( + localAssetStartBalance, remoteAssetStartBalance, + isOurCommit, originalView, + ) + if err != nil { + return nil, nil, fmt.Errorf("unable to compute view: %w", err) + } + + dustLimit := chanState.LocalChanCfg.DustLimit + if !isOurCommit { + dustLimit = chanState.RemoteChanCfg.DustLimit + } + + log.Tracef("Computed view, ourCommit=%v, ourAssetBalance=%d, "+ + "theirAssetBalance=%d, dustLimit=%v", isOurCommit, + ourAssetBalance, theirAssetBalance, dustLimit) + + // Make sure that every output that carries an asset balance has a + // corresponding non-dust BTC output. + wantLocalAnchor, wantRemoteAnchor, err := SanityCheckAmounts( + ourBalance.ToSatoshis(), theirBalance.ToSatoshis(), + ourAssetBalance, theirAssetBalance, filteredView, + chanState.ChanType, isOurCommit, dustLimit, + ) + if err != nil { + return nil, nil, fmt.Errorf("error checking amounts: %w", err) + } + + // With all the balances checked, we can now create allocation entries + // for each on-chain output. An allocation is a helper struct to keep + // track of the original on-chain output, the keys/scripts involved on + // the BTC level as well as the asset UTXOs that are being distributed. + allocations, err := CreateAllocations( + chanState, ourBalance.ToSatoshis(), theirBalance.ToSatoshis(), + ourAssetBalance, theirAssetBalance, wantLocalAnchor, + wantRemoteAnchor, filteredView, isOurCommit, keys, + ) + if err != nil { + return nil, nil, fmt.Errorf("unable to create allocations: %w", + err) + } + + log.Tracef("Created allocations, ourCommit=%v, allocations=%v", + isOurCommit, limitSpewer.Sdump(allocations)) + + inputProofs := fn.Map( + chanAssetState.Assets(), + func(o *cmsg.AssetOutput) *proof.Proof { + return &o.Proof.Val + }, + ) + + // Now we can distribute the inputs according to the allocations. This + // creates a virtual packet for each distinct asset ID that is committed + // to the channel. + vPackets, err := DistributeCoins( + inputProofs, allocations, c.cfg.ChainParams, + ) + if err != nil { + return nil, nil, fmt.Errorf("unable to distribute coins: %w", + err) + } + + // Prepare the output assets for each virtual packet, then create the + // output commitments. + ctx := context.Background() + for idx := range vPackets { + err := tapsend.PrepareOutputAssets(ctx, vPackets[idx]) + if err != nil { + return nil, nil, fmt.Errorf("unable to prepare output "+ + "assets: %w", err) + } + } + + outCommitments, err := tapsend.CreateOutputCommitments(vPackets) + if err != nil { + return nil, nil, fmt.Errorf("unable to create output "+ + "commitments: %w", err) + } + + // The output commitment is all we need to create the auxiliary leaves. + // We map the output commitments (which are keyed by on-chain output + // index) back to the allocation. + err = AssignOutputCommitments(allocations, outCommitments) + if err != nil { + return nil, nil, fmt.Errorf("unable to assign alloc output "+ + "commitments: %w", err) + } + + // We don't actually have the real commitment transaction yet, so for + // now we'll make a dummy version. Once we go to force close, we'll + // know the real commitment transaction, and can update the proofs + // below. + fakeCommitTx, err := FakeCommitTx( + chanState.FundingOutpoint, allocations, + ) + if err != nil { + return nil, nil, fmt.Errorf("unable to create fake commit tx: "+ + "%w", err) + } + + // Now we have all the information we need to create the asset proofs. + for idx := range vPackets { + vPkt := vPackets[idx] + for outIdx := range vPkt.Outputs { + proofSuffix, err := tapsend.CreateProofSuffixCustom( + fakeCommitTx, vPkt, outCommitments, outIdx, + vPackets, ExclusionProofsFromAllocations( + allocations, + ), + ) + if err != nil { + return nil, nil, fmt.Errorf("unable to create "+ + "proof suffix for output %d: %w", + outIdx, err) + } + + vPkt.Outputs[outIdx].ProofSuffix = proofSuffix + } + } + + // Next, we can convert the allocations to auxiliary leaves and from + // those construct our Commitment struct that will in the end also hold + // our proof suffixes. + newCommitment, err := ToCommitment(allocations, vPackets) + if err != nil { + return nil, nil, fmt.Errorf("unable to convert to commitment: "+ + "%w", err) + } + + return allocations, newCommitment, nil +}