diff --git a/server.go b/server.go index 5bf70bc5d..821cc3f9b 100644 --- a/server.go +++ b/server.go @@ -824,7 +824,9 @@ func (s *Server) FetchLeavesFromView( // The aux leaf creator is fully stateless, and we don't need to wait // for the server to be started before being able to use it. - return tapchannel.FetchLeavesFromView(s.chainParams, in) + return tapchannel.FetchLeavesFromView( + s.chainParams, in, s.cfg.AuxChanNegotiator, + ) } // FetchLeavesFromCommit attempts to fetch the auxiliary leaves that @@ -845,6 +847,7 @@ func (s *Server) FetchLeavesFromCommit(chanState lnwl.AuxChanState, // for the server to be started before being able to use it. return tapchannel.FetchLeavesFromCommit( s.chainParams, chanState, com, keys, whoseCommit, + s.cfg.AuxChanNegotiator, ) } @@ -880,7 +883,9 @@ func (s *Server) ApplyHtlcView( // The aux leaf creator is fully stateless, and we don't need to wait // for the server to be started before being able to use it. - return tapchannel.ApplyHtlcView(s.chainParams, in) + return tapchannel.ApplyHtlcView( + s.chainParams, in, s.cfg.AuxChanNegotiator, + ) } // InlineParseCustomData replaces any custom data binary blob in the given RPC diff --git a/tapcfg/server.go b/tapcfg/server.go index 63b948e4b..e5483020e 100644 --- a/tapcfg/server.go +++ b/tapcfg/server.go @@ -536,6 +536,7 @@ func genServerConfig(cfg *Config, cfgLogger btclog.Logger, DefaultCourierAddr: proofCourierAddr, AssetSyncer: addrBook, FeatureBits: lndFeatureBitsVerifier, + AuxChanNegotiator: auxChanNegotiator, ErrChan: mainErrChan, }, ) @@ -567,7 +568,8 @@ func genServerConfig(cfg *Config, cfgLogger btclog.Logger, GroupVerifier: tapgarden.GenGroupVerifier( context.Background(), assetMintingStore, ), - ChainBridge: chainBridge, + ChainBridge: chainBridge, + AuxChanNegotiator: auxChanNegotiator, }, ) auxSweeper := tapchannel.NewAuxSweeper( diff --git a/tapchannel/aux_closer.go b/tapchannel/aux_closer.go index b0527bc49..f8fa095f1 100644 --- a/tapchannel/aux_closer.go +++ b/tapchannel/aux_closer.go @@ -15,6 +15,7 @@ import ( "github.com/lightninglabs/taproot-assets/fn" "github.com/lightninglabs/taproot-assets/proof" "github.com/lightninglabs/taproot-assets/tapchannelmsg" + "github.com/lightninglabs/taproot-assets/tapfeatures" "github.com/lightninglabs/taproot-assets/tapfreighter" "github.com/lightninglabs/taproot-assets/tapgarden" "github.com/lightninglabs/taproot-assets/tappsbt" @@ -62,6 +63,11 @@ type AuxChanCloserCfg struct { // ChainBridge is used to fetch blocks from the main chain. ChainBridge tapgarden.ChainBridge + + // AuxChanNegotiator is responsible for producing the extra tlv blob + // that is encapsulated in the init and reestablish peer messages. This + // helps us communicate custom feature bits with our peer. + AuxChanNegotiator *tapfeatures.AuxChannelNegotiator } // assetCloseInfo houses the information we need to finalize the close of an @@ -450,11 +456,21 @@ func (a *AuxChanCloser) AuxCloseOutputs( "packets: %w", err) } + features := a.cfg.AuxChanNegotiator.GetChannelFeatures( + lnwire.NewChanIDFromOutPoint(desc.ChanPoint), + ) + supportSTXO := features.HasFeature(tapfeatures.STXOOptional) + + var opts []tapsend.OutputCommitmentOption + if !supportSTXO { + opts = append(opts, tapsend.WithNoSTXOProofs()) + } + // With the outputs prepared, we can now create the set of output // commitments, then with the output index locations known, we can set // the output indexes in the allocations. outCommitments, err := tapsend.CreateOutputCommitments( - vPackets, tapsend.WithNoSTXOProofs(), + vPackets, opts..., ) if err != nil { return none, fmt.Errorf("unable to create output "+ @@ -723,10 +739,22 @@ func (a *AuxChanCloser) FinalizeClose(desc chancloser.AuxCloseDesc, closeInfo.allocations, ) + features := a.cfg.AuxChanNegotiator.GetChannelFeatures( + lnwire.NewChanIDFromOutPoint(desc.ChanPoint), + ) + supportSTXO := features.HasFeature( + tapfeatures.STXOOptional, + ) + + var opts []proof.GenOption + if !supportSTXO { + opts = append(opts, proof.WithNoSTXOProofs()) + } + proofSuffix, err := tapsend.CreateProofSuffixCustom( closeTx, vPkt, closeInfo.outputCommitments, outIdx, closeInfo.vPackets, exclusionCreator, - proof.WithNoSTXOProofs(), + opts..., ) if err != nil { return fmt.Errorf("unable to create proof "+ diff --git a/tapchannel/aux_funding_controller.go b/tapchannel/aux_funding_controller.go index be4b4ebfe..3794eb9c1 100644 --- a/tapchannel/aux_funding_controller.go +++ b/tapchannel/aux_funding_controller.go @@ -28,6 +28,7 @@ import ( "github.com/lightninglabs/taproot-assets/rfq" cmsg "github.com/lightninglabs/taproot-assets/tapchannelmsg" "github.com/lightninglabs/taproot-assets/tapdb" + "github.com/lightninglabs/taproot-assets/tapfeatures" "github.com/lightninglabs/taproot-assets/tapfreighter" "github.com/lightninglabs/taproot-assets/tapgarden" "github.com/lightninglabs/taproot-assets/tappsbt" @@ -41,6 +42,7 @@ import ( "github.com/lightningnetwork/lnd/lnwallet/chainfee" "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/msgmux" + "github.com/lightningnetwork/lnd/routing/route" ) const ( @@ -265,6 +267,11 @@ type FundingControllerCfg struct { // to fund asset channels. FeatureBits FeatureBitVerifer + // AuxChanNegotiator is responsible for producing the extra tlv blob + // that is encapsulated in the init and reestablish peer messages. This + // helps us communicate custom feature bits with our peer. + AuxChanNegotiator *tapfeatures.AuxChannelNegotiator + // ErrChan is used to report errors back to the main server. ErrChan chan<- error } @@ -415,6 +422,8 @@ type pendingAssetFunding struct { initiator bool + stxo bool + amt uint64 pushAmt btcutil.Amount @@ -520,7 +529,8 @@ func (p *pendingAssetFunding) addInputProofChunk( func newCommitBlobAndLeaves(pendingFunding *pendingAssetFunding, lndOpenChan lnwallet.AuxChanState, assetOpenChan *cmsg.OpenChannel, keyRing lntypes.Dual[lnwallet.CommitmentKeyRing], - whoseCommit lntypes.ChannelParty) ([]byte, lnwallet.CommitAuxLeaves, + whoseCommit lntypes.ChannelParty, + stxo bool) ([]byte, lnwallet.CommitAuxLeaves, error) { chanAssets := assetOpenChan.FundedAssets.Val.Outputs @@ -567,6 +577,7 @@ func newCommitBlobAndLeaves(pendingFunding *pendingAssetFunding, fakePrevState, lndOpenChan, assetOpenChan, whoseCommit, localSatBalance, remoteSatBalance, fakeView, pendingFunding.chainParams, keyRing.GetForParty(whoseCommit), + stxo, ) if err != nil { return nil, lnwallet.CommitAuxLeaves{}, err @@ -609,12 +620,14 @@ func (p *pendingAssetFunding) toAuxFundingDesc(req *bindFundingReq, // This will be the information for the very first state (state 0). localCommitBlob, localAuxLeaves, err := newCommitBlobAndLeaves( p, req.openChan, openChanDesc, req.keyRing, lntypes.Local, + p.stxo, ) if err != nil { return nil, err } remoteCommitBlob, remoteAuxLeaves, err := newCommitBlobAndLeaves( p, req.openChan, openChanDesc, req.keyRing, lntypes.Remote, + p.stxo, ) if err != nil { return nil, err @@ -1078,12 +1091,15 @@ func (f *FundingController) signAllVPackets(ctx context.Context, // complete, but unsigned PSBT packet that can be used to create out asset // channel. func (f *FundingController) anchorVPackets(fundedPkt *tapsend.FundedPsbt, - allPackets []*tappsbt.VPacket) ([]*proof.Proof, error) { + allPackets []*tappsbt.VPacket, stxo bool) ([]*proof.Proof, error) { log.Infof("Anchoring funding vPackets to funding PSBT") // Given the set of vPackets we've created, we'll now merge them all to // create a map from output index to final tap commitment. + // We don't use STXO proofs here as we already manually added the + // corresponding alt-leaves in a previous step. Creating the commitments + // here with STXO generation will lead to a conflict in the alt leaves. outputCommitments, err := tapsend.CreateOutputCommitments( allPackets, tapsend.WithNoSTXOProofs(), ) @@ -1114,11 +1130,16 @@ func (f *FundingController) anchorVPackets(fundedPkt *tapsend.FundedPsbt, for idx := range allPackets { vPkt := allPackets[idx] + var opts []proof.GenOption + if !stxo { + opts = append(opts, proof.WithNoSTXOProofs()) + } + for vOutIdx := range vPkt.Outputs { proofSuffix, err := tapsend.CreateProofSuffix( fundedPkt.Pkt.UnsignedTx, fundedPkt.Pkt.Outputs, vPkt, outputCommitments, vOutIdx, allPackets, - proof.WithNoSTXOProofs(), + opts..., ) if err != nil { return nil, fmt.Errorf("unable to create "+ @@ -1211,7 +1232,8 @@ func (f *FundingController) sendAssetFundingCreated(ctx context.Context, // ultimately broadcasting the funding transaction. func (f *FundingController) completeChannelFunding(ctx context.Context, fundingState *pendingAssetFunding, - fundedVpkt *tapfreighter.FundedVPacket) (*wire.OutPoint, error) { + fundedVpkt *tapfreighter.FundedVPacket, + stxoEnabled bool) (*wire.OutPoint, error) { log.Debugf("Finalizing funding vPackets and PSBT...") @@ -1322,7 +1344,7 @@ func (f *FundingController) completeChannelFunding(ctx context.Context, // PSBT. This'll update all the pkScripts for our funding output and // change. fundingOutputProofs, err := f.anchorVPackets( - finalFundedPsbt, signedPkts, + finalFundedPsbt, signedPkts, stxoEnabled, ) if err != nil { return nil, fmt.Errorf("unable to anchor vPackets: %w", err) @@ -1547,6 +1569,28 @@ func (f *FundingController) processFundingMsg(ctx context.Context, "commitment: %w", err) } + supportSTXO := f.cfg.AuxChanNegotiator.GetPeerFeatures( + route.Vertex(msg.PeerPub.SerializeCompressed()), + ).HasFeature(tapfeatures.STXOOptional) + + assetFunding.stxo = supportSTXO + + // If we've the feature bit negotiation resulted in the use of + // STXO proofs, then we need to collect the alt leaves and + // merge them to the funding commitment. + if supportSTXO { + altLeaves, err := asset.CollectSTXO( + &assetProof.AssetOutput.Val, + ) + if err != nil { + return tempPID, err + } + + assetFunding.fundingAssetCommitment.MergeAltLeaves( + altLeaves, + ) + } + // Do we expect more proofs to be incoming? if !assetProof.Last.Val { return tempPID, nil @@ -1704,6 +1748,13 @@ func (f *FundingController) processFundingReq(fundingFlows fundingFlowIndex, } } + // Now let's see if we should be using STXOs for this channel funding. + supportSTXO := f.cfg.AuxChanNegotiator.GetPeerFeatures( + route.Vertex(fundReq.PeerPub.SerializeCompressed()), + ).HasFeature(tapfeatures.STXOOptional) + + fundingState.stxo = supportSTXO + // Register a defer to execute if none of the setup below succeeds. // This ensures we always unlock the UTXO. var setupSuccess bool @@ -1740,6 +1791,23 @@ func (f *FundingController) processFundingReq(fundingFlows fundingFlowIndex, "packet: %w", err) } + // If our peer supports STXO we go ahead and append the + // appropriate alt leaves to each VOutput. This is needed in + // this step as this tapscript root will be kept as reference by + // LND to identify the funding outpoint. Later when we continue + // the funding process we'll skip creating/merging the leaves + // again to avoid a conflict in the alt leaf tree. + if supportSTXO { + for _, o := range pkt.Outputs { + altLeaves, err := asset.CollectSTXO(o.Asset) + if err != nil { + return err + } + + o.AltLeaves = append(o.AltLeaves, altLeaves...) + } + } + err = fundingState.addToFundingCommitment( fundingOut.Asset.Copy(), ) @@ -1747,6 +1815,15 @@ func (f *FundingController) processFundingReq(fundingFlows fundingFlowIndex, return fmt.Errorf("unable to add asset to funding "+ "commitment: %w", err) } + + // We now merge the alt leaves to the funding commitment. + for _, o := range pkt.Outputs { + commitment := fundingState.fundingAssetCommitment + err = commitment.MergeAltLeaves(o.AltLeaves) + if err != nil { + return err + } + } } tapsend.LogCommitment( @@ -1805,7 +1882,7 @@ func (f *FundingController) processFundingReq(fundingFlows fundingFlowIndex, } chanPoint, err := f.completeChannelFunding( - fundReq.ctx, fundingState, fundingVpkt, + fundReq.ctx, fundingState, fundingVpkt, supportSTXO, ) if err != nil { // If anything went wrong during the funding process, diff --git a/tapchannel/aux_leaf_creator.go b/tapchannel/aux_leaf_creator.go index 75796115d..11dff907c 100644 --- a/tapchannel/aux_leaf_creator.go +++ b/tapchannel/aux_leaf_creator.go @@ -10,11 +10,14 @@ import ( "github.com/lightninglabs/taproot-assets/address" "github.com/lightninglabs/taproot-assets/fn" cmsg "github.com/lightninglabs/taproot-assets/tapchannelmsg" + "github.com/lightninglabs/taproot-assets/tapfeatures" "github.com/lightningnetwork/lnd/channeldb" lfn "github.com/lightningnetwork/lnd/fn/v2" "github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/lntypes" lnwl "github.com/lightningnetwork/lnd/lnwallet" + "github.com/lightningnetwork/lnd/lnwire" + "github.com/lightningnetwork/lnd/routing/route" "github.com/lightningnetwork/lnd/tlv" ) @@ -24,10 +27,22 @@ const ( DefaultTimeout = 30 * time.Second ) +// FeatureBitFetcher is responsible for fetching feature bits either by +// referencing a remote peer, or an established channel. +type FeatureBitFetcher interface { + // GetPeerFeatures returns the negotiated features with the given peer. + GetPeerFeatures(peer route.Vertex) *lnwire.FeatureVector + + // GetChannelFeatures returns the negotiated features that are active + // over the channel identifier by the provided channelID. + GetChannelFeatures(cid lnwire.ChannelID) *lnwire.FeatureVector +} + // FetchLeavesFromView attempts to fetch the auxiliary leaves that correspond to // the passed aux blob, and pending fully evaluated HTLC view. func FetchLeavesFromView(chainParams *address.ChainParams, - in lnwl.CommitDiffAuxInput) lfn.Result[lnwl.CommitDiffAuxResult] { + in lnwl.CommitDiffAuxInput, + bitFetcher FeatureBitFetcher) lfn.Result[lnwl.CommitDiffAuxResult] { type returnType = lnwl.CommitDiffAuxResult @@ -50,10 +65,16 @@ func FetchLeavesFromView(chainParams *address.ChainParams, "commit state: %w", err)) } + supportsSTXO := bitFetcher.GetChannelFeatures( + lnwire.NewChanIDFromOutPoint( + in.ChannelState.FundingOutpoint, + ), + ).HasFeature(tapfeatures.STXOOptional) + allocations, newCommitment, err := GenerateCommitmentAllocations( prevState, in.ChannelState, chanAssetState, in.WhoseCommit, in.OurBalance, in.TheirBalance, in.UnfilteredView, chainParams, - in.KeyRing, + in.KeyRing, supportsSTXO, ) if err != nil { return lfn.Err[returnType](fmt.Errorf("unable to generate "+ @@ -80,8 +101,8 @@ func FetchLeavesFromView(chainParams *address.ChainParams, // to the passed aux blob, and an existing channel commitment. func FetchLeavesFromCommit(chainParams *address.ChainParams, chanState lnwl.AuxChanState, com channeldb.ChannelCommitment, - keys lnwl.CommitmentKeyRing, - whoseCommit lntypes.ChannelParty) lfn.Result[lnwl.CommitDiffAuxResult] { + keys lnwl.CommitmentKeyRing, whoseCommit lntypes.ChannelParty, + bitFetcher FeatureBitFetcher) lfn.Result[lnwl.CommitDiffAuxResult] { type returnType = lnwl.CommitDiffAuxResult @@ -90,6 +111,9 @@ func FetchLeavesFromCommit(chainParams *address.ChainParams, return lfn.Ok(lnwl.CommitDiffAuxResult{}) } + supportSTXO := bitFetcher.GetPeerFeatures(chanState.PeerPubKey). + HasFeature(tapfeatures.STXOOptional) + commitment, err := cmsg.DecodeCommitment( com.CustomBlob.UnsafeFromSome(), ) @@ -129,7 +153,7 @@ func FetchLeavesFromCommit(chainParams *address.ChainParams, leaf, err := CreateSecondLevelHtlcTx( chanState, com.CommitTx, htlc.Amt.ToSatoshis(), keys, chainParams, htlcOutputs, cltvTimeout, - htlc.HtlcIndex, + htlc.HtlcIndex, supportSTXO, ) if err != nil { return lfn.Err[returnType](fmt.Errorf("unable "+ @@ -170,7 +194,7 @@ func FetchLeavesFromCommit(chainParams *address.ChainParams, leaf, err := CreateSecondLevelHtlcTx( chanState, com.CommitTx, htlc.Amt.ToSatoshis(), keys, chainParams, htlcOutputs, cltvTimeout, - htlc.HtlcIndex, + htlc.HtlcIndex, supportSTXO, ) if err != nil { return lfn.Err[returnType](fmt.Errorf("unable "+ @@ -225,7 +249,8 @@ func FetchLeavesFromRevocation( // channel's blob. Given the old blob, and an HTLC view, then a new // blob should be returned that reflects the pending updates. func ApplyHtlcView(chainParams *address.ChainParams, - in lnwl.CommitDiffAuxInput) lfn.Result[lfn.Option[tlv.Blob]] { + in lnwl.CommitDiffAuxInput, + bitFetcher FeatureBitFetcher) lfn.Result[lfn.Option[tlv.Blob]] { type returnType = lfn.Option[tlv.Blob] @@ -248,10 +273,18 @@ func ApplyHtlcView(chainParams *address.ChainParams, "commit state: %w", err)) } + supportSTXO := bitFetcher.GetChannelFeatures( + lnwire.NewChanIDFromOutPoint( + in.ChannelState.FundingOutpoint, + ), + ).HasFeature( + tapfeatures.STXOOptional, + ) + _, newCommitment, err := GenerateCommitmentAllocations( prevState, in.ChannelState, chanAssetState, in.WhoseCommit, in.OurBalance, in.TheirBalance, in.UnfilteredView, chainParams, - in.KeyRing, + in.KeyRing, supportSTXO, ) if err != nil { return lfn.Err[returnType](fmt.Errorf("unable to generate "+ diff --git a/tapchannel/commitment.go b/tapchannel/commitment.go index d802bed09..76018ef69 100644 --- a/tapchannel/commitment.go +++ b/tapchannel/commitment.go @@ -497,7 +497,7 @@ func GenerateCommitmentAllocations(prevState *cmsg.Commitment, whoseCommit lntypes.ChannelParty, ourBalance, theirBalance lnwire.MilliSatoshi, originalView lnwallet.AuxHtlcView, chainParams *address.ChainParams, - keys lnwallet.CommitmentKeyRing) ([]*tapsend.Allocation, + keys lnwallet.CommitmentKeyRing, stxo bool) ([]*tapsend.Allocation, *cmsg.Commitment, error) { log.Tracef("Generating allocations, whoseCommit=%v, ourBalance=%d, "+ @@ -589,8 +589,13 @@ func GenerateCommitmentAllocations(prevState *cmsg.Commitment, "packets: %w", err) } + var opts []tapsend.OutputCommitmentOption + if !stxo { + opts = append(opts, tapsend.WithNoSTXOProofs()) + } + outCommitments, err := tapsend.CreateOutputCommitments( - vPackets, tapsend.WithNoSTXOProofs(), + vPackets, opts..., ) if err != nil { return nil, nil, fmt.Errorf("unable to create output "+ @@ -622,11 +627,16 @@ func GenerateCommitmentAllocations(prevState *cmsg.Commitment, for idx := range vPackets { vPkt := vPackets[idx] for outIdx := range vPkt.Outputs { + var opts []proof.GenOption + if !stxo { + opts = append(opts, proof.WithNoSTXOProofs()) + } + proofSuffix, err := tapsend.CreateProofSuffixCustom( fakeCommitTx, vPkt, outCommitments, outIdx, vPackets, tapsend.NonAssetExclusionProofs( allocations, - ), proof.WithNoSTXOProofs(), + ), opts..., ) if err != nil { return nil, nil, fmt.Errorf("unable to create "+ @@ -1432,7 +1442,7 @@ func CreateSecondLevelHtlcTx(chanState lnwallet.AuxChanState, commitTx *wire.MsgTx, htlcAmt btcutil.Amount, keys lnwallet.CommitmentKeyRing, chainParams *address.ChainParams, htlcOutputs []*cmsg.AssetOutput, htlcTimeout fn.Option[uint32], - htlcIndex uint64) (input.AuxTapLeaf, error) { + htlcIndex uint64, stxo bool) (input.AuxTapLeaf, error) { none := input.NoneTapLeaf() @@ -1445,8 +1455,13 @@ func CreateSecondLevelHtlcTx(chanState lnwallet.AuxChanState, "packets: %w", err) } + var opts []tapsend.OutputCommitmentOption + if !stxo { + opts = append(opts, tapsend.WithNoSTXOProofs()) + } + outCommitments, err := tapsend.CreateOutputCommitments( - vPackets, tapsend.WithNoSTXOProofs(), + vPackets, opts..., ) if err != nil { return none, fmt.Errorf("unable to create output commitments: "+