From 7e079285e29850ba45b6af0e8216622a2b82cfa0 Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Thu, 17 Apr 2025 17:13:56 +0200 Subject: [PATCH 01/10] mod+docs+server: bump to latest lnd version --- docs/examples/basic-price-oracle/go.mod | 6 +++--- docs/examples/basic-price-oracle/go.sum | 12 ++++++------ go.mod | 6 +++--- go.sum | 12 ++++++------ server.go | 25 +++++++++++++++---------- tapchannel/aux_traffic_shaper.go | 4 ++-- 6 files changed, 35 insertions(+), 30 deletions(-) diff --git a/docs/examples/basic-price-oracle/go.mod b/docs/examples/basic-price-oracle/go.mod index 04bd25836d..5dce9e481c 100644 --- a/docs/examples/basic-price-oracle/go.mod +++ b/docs/examples/basic-price-oracle/go.mod @@ -94,16 +94,16 @@ require ( github.com/klauspost/compress v1.17.9 // indirect github.com/lib/pq v1.10.9 // indirect github.com/lightninglabs/gozmq v0.0.0-20191113021534-d20a764486bf // indirect - github.com/lightninglabs/lndclient v0.19.0-3 // indirect + github.com/lightninglabs/lndclient v0.19.0-4 // indirect github.com/lightninglabs/neutrino v0.16.1 // indirect github.com/lightninglabs/neutrino/cache v1.1.2 // indirect github.com/lightningnetwork/lightning-onion v1.2.1-0.20240712235311-98bd56499dfb // indirect - github.com/lightningnetwork/lnd v0.19.0-beta.rc2.0.20250417120008-a304be6bad91 // indirect + github.com/lightningnetwork/lnd v0.19.0-beta.rc2.0.20250423092132-a35ace7371af // indirect github.com/lightningnetwork/lnd/cert v1.2.2 // indirect github.com/lightningnetwork/lnd/clock v1.1.1 // indirect github.com/lightningnetwork/lnd/fn/v2 v2.0.8 // indirect github.com/lightningnetwork/lnd/healthcheck v1.2.6 // indirect - github.com/lightningnetwork/lnd/kvdb v1.4.15 // indirect + github.com/lightningnetwork/lnd/kvdb v1.4.16 // indirect github.com/lightningnetwork/lnd/queue v1.1.1 // indirect github.com/lightningnetwork/lnd/sqldb v1.0.9 // indirect github.com/lightningnetwork/lnd/ticker v1.1.1 // indirect diff --git a/docs/examples/basic-price-oracle/go.sum b/docs/examples/basic-price-oracle/go.sum index 330d3395f9..0c8b266eca 100644 --- a/docs/examples/basic-price-oracle/go.sum +++ b/docs/examples/basic-price-oracle/go.sum @@ -438,8 +438,8 @@ github.com/lightninglabs/gozmq v0.0.0-20191113021534-d20a764486bf/go.mod h1:vxmQ github.com/lightninglabs/lightning-node-connect v0.2.5-alpha h1:ZRVChwczFXK0CEbxOCWwUA6TIZvrkE0APd1T3WjFAwg= github.com/lightninglabs/lightning-node-connect/hashmailrpc v1.0.2 h1:Er1miPZD2XZwcfE4xoS5AILqP1mj7kqnhbBSxW9BDxY= github.com/lightninglabs/lightning-node-connect/hashmailrpc v1.0.2/go.mod h1:antQGRDRJiuyQF6l+k6NECCSImgCpwaZapATth2Chv4= -github.com/lightninglabs/lndclient v0.19.0-3 h1:PGGlDaz8x1dXGowDfAWhbuDqXTKNaJyb7SOTrRdG1es= -github.com/lightninglabs/lndclient v0.19.0-3/go.mod h1:5YMrFx00NvcmUHGZRxT4Qw/gOfR5x50/ReJmJ6w0yVk= +github.com/lightninglabs/lndclient v0.19.0-4 h1:U+koisg716/i51kf5ENI5+9a1joXcPXeJYl3q0s4/co= +github.com/lightninglabs/lndclient v0.19.0-4/go.mod h1:LP3FM3JGBdvOX8Lum9x1r7q54oiftoqaq4EYhtpp/fk= github.com/lightninglabs/neutrino v0.16.1 h1:5Kz4ToxncEVkpKC6fwUjXKtFKJhuxlG3sBB3MdJTJjs= github.com/lightninglabs/neutrino v0.16.1/go.mod h1:L+5UAccpUdyM7yDgmQySgixf7xmwBgJtOfs/IP26jCs= github.com/lightninglabs/neutrino/cache v1.1.2 h1:C9DY/DAPaPxbFC+xNNEI/z1SJY9GS3shmlu5hIQ798g= @@ -448,8 +448,8 @@ github.com/lightninglabs/protobuf-go-hex-display v1.33.0-hex-display h1:Y2WiPkBS github.com/lightninglabs/protobuf-go-hex-display v1.33.0-hex-display/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= github.com/lightningnetwork/lightning-onion v1.2.1-0.20240712235311-98bd56499dfb h1:yfM05S8DXKhuCBp5qSMZdtSwvJ+GFzl94KbXMNB1JDY= github.com/lightningnetwork/lightning-onion v1.2.1-0.20240712235311-98bd56499dfb/go.mod h1:c0kvRShutpj3l6B9WtTsNTBUtjSmjZXbJd9ZBRQOSKI= -github.com/lightningnetwork/lnd v0.19.0-beta.rc2.0.20250417120008-a304be6bad91 h1:LzLA7+J/fP1VrK4BcyAt86cg4/bkfY38gYRBJoy109o= -github.com/lightningnetwork/lnd v0.19.0-beta.rc2.0.20250417120008-a304be6bad91/go.mod h1:v4Y0gLAIqqxY83J+4HilQHIiScIy2ok2GSuBFtoc1zc= +github.com/lightningnetwork/lnd v0.19.0-beta.rc2.0.20250423092132-a35ace7371af h1:+t8N7kmI7YVu7Hzv8pPiMVCTjnSRi/qOxbAkXa5rn+0= +github.com/lightningnetwork/lnd v0.19.0-beta.rc2.0.20250423092132-a35ace7371af/go.mod h1:nCkZ6G6twxDKn31117M0BNfN5JSAmJVAHNTwYrn31BQ= github.com/lightningnetwork/lnd/cert v1.2.2 h1:71YK6hogeJtxSxw2teq3eGeuy4rHGKcFf0d0Uy4qBjI= github.com/lightningnetwork/lnd/cert v1.2.2/go.mod h1:jQmFn/Ez4zhDgq2hnYSw8r35bqGVxViXhX6Cd7HXM6U= github.com/lightningnetwork/lnd/clock v1.1.1 h1:OfR3/zcJd2RhH0RU+zX/77c0ZiOnIMsDIBjgjWdZgA0= @@ -458,8 +458,8 @@ github.com/lightningnetwork/lnd/fn/v2 v2.0.8 h1:r2SLz7gZYQPVc3IZhU82M66guz3Zk2oY github.com/lightningnetwork/lnd/fn/v2 v2.0.8/go.mod h1:TOzwrhjB/Azw1V7aa8t21ufcQmdsQOQMDtxVOQWNl8s= github.com/lightningnetwork/lnd/healthcheck v1.2.6 h1:1sWhqr93GdkWy4+6U7JxBfcyZIE78MhIHTJZfPx7qqI= github.com/lightningnetwork/lnd/healthcheck v1.2.6/go.mod h1:Mu02um4CWY/zdTOvFje7WJgJcHyX2zq/FG3MhOAiGaQ= -github.com/lightningnetwork/lnd/kvdb v1.4.15 h1:3eN6uGcubvGB5itPp1D0D4uEEkIMYht3w0LDnqLzAWI= -github.com/lightningnetwork/lnd/kvdb v1.4.15/go.mod h1:HW+bvwkxNaopkz3oIgBV6NEnV4jCEZCACFUcNg4xSjM= +github.com/lightningnetwork/lnd/kvdb v1.4.16 h1:9BZgWdDfjmHRHLS97cz39bVuBAqMc4/p3HX1xtUdbDI= +github.com/lightningnetwork/lnd/kvdb v1.4.16/go.mod h1:HW+bvwkxNaopkz3oIgBV6NEnV4jCEZCACFUcNg4xSjM= github.com/lightningnetwork/lnd/queue v1.1.1 h1:99ovBlpM9B0FRCGYJo6RSFDlt8/vOkQQZznVb18iNMI= github.com/lightningnetwork/lnd/queue v1.1.1/go.mod h1:7A6nC1Qrm32FHuhx/mi1cieAiBZo5O6l8IBIoQxvkz4= github.com/lightningnetwork/lnd/sqldb v1.0.9 h1:7OHi+Hui823mB/U9NzCdlZTAGSVdDCbjp33+6d/Q+G0= diff --git a/go.mod b/go.mod index 61d6821cfb..2aeced3f2d 100644 --- a/go.mod +++ b/go.mod @@ -27,9 +27,9 @@ require ( github.com/lib/pq v1.10.9 github.com/lightninglabs/aperture v0.3.8-beta github.com/lightninglabs/lightning-node-connect/hashmailrpc v1.0.2 - github.com/lightninglabs/lndclient v0.19.0-3 + github.com/lightninglabs/lndclient v0.19.0-4 github.com/lightninglabs/neutrino/cache v1.1.2 - github.com/lightningnetwork/lnd v0.19.0-beta.rc2.0.20250417120008-a304be6bad91 + github.com/lightningnetwork/lnd v0.19.0-beta.rc2.0.20250423092132-a35ace7371af github.com/lightningnetwork/lnd/cert v1.2.2 github.com/lightningnetwork/lnd/clock v1.1.1 github.com/lightningnetwork/lnd/fn/v2 v2.0.8 @@ -127,7 +127,7 @@ require ( github.com/lightninglabs/neutrino v0.16.1 // indirect github.com/lightningnetwork/lightning-onion v1.2.1-0.20240712235311-98bd56499dfb // indirect github.com/lightningnetwork/lnd/healthcheck v1.2.6 // indirect - github.com/lightningnetwork/lnd/kvdb v1.4.15 // indirect + github.com/lightningnetwork/lnd/kvdb v1.4.16 // indirect github.com/lightningnetwork/lnd/queue v1.1.1 // indirect github.com/lightningnetwork/lnd/sqldb v1.0.9 // indirect github.com/lightningnetwork/lnd/ticker v1.1.1 // indirect diff --git a/go.sum b/go.sum index 488a66f8ad..11f2834f3b 100644 --- a/go.sum +++ b/go.sum @@ -492,8 +492,8 @@ github.com/lightninglabs/lightning-node-connect v0.2.5-alpha h1:ZRVChwczFXK0CEbx github.com/lightninglabs/lightning-node-connect v0.2.5-alpha/go.mod h1:A9Pof9fETkH+F67BnOmrBDThPKstqp73wlImWOZvTXQ= github.com/lightninglabs/lightning-node-connect/hashmailrpc v1.0.2 h1:Er1miPZD2XZwcfE4xoS5AILqP1mj7kqnhbBSxW9BDxY= github.com/lightninglabs/lightning-node-connect/hashmailrpc v1.0.2/go.mod h1:antQGRDRJiuyQF6l+k6NECCSImgCpwaZapATth2Chv4= -github.com/lightninglabs/lndclient v0.19.0-3 h1:PGGlDaz8x1dXGowDfAWhbuDqXTKNaJyb7SOTrRdG1es= -github.com/lightninglabs/lndclient v0.19.0-3/go.mod h1:5YMrFx00NvcmUHGZRxT4Qw/gOfR5x50/ReJmJ6w0yVk= +github.com/lightninglabs/lndclient v0.19.0-4 h1:U+koisg716/i51kf5ENI5+9a1joXcPXeJYl3q0s4/co= +github.com/lightninglabs/lndclient v0.19.0-4/go.mod h1:LP3FM3JGBdvOX8Lum9x1r7q54oiftoqaq4EYhtpp/fk= github.com/lightninglabs/neutrino v0.16.1 h1:5Kz4ToxncEVkpKC6fwUjXKtFKJhuxlG3sBB3MdJTJjs= github.com/lightninglabs/neutrino v0.16.1/go.mod h1:L+5UAccpUdyM7yDgmQySgixf7xmwBgJtOfs/IP26jCs= github.com/lightninglabs/neutrino/cache v1.1.2 h1:C9DY/DAPaPxbFC+xNNEI/z1SJY9GS3shmlu5hIQ798g= @@ -502,8 +502,8 @@ github.com/lightninglabs/protobuf-go-hex-display v1.33.0-hex-display h1:Y2WiPkBS github.com/lightninglabs/protobuf-go-hex-display v1.33.0-hex-display/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= github.com/lightningnetwork/lightning-onion v1.2.1-0.20240712235311-98bd56499dfb h1:yfM05S8DXKhuCBp5qSMZdtSwvJ+GFzl94KbXMNB1JDY= github.com/lightningnetwork/lightning-onion v1.2.1-0.20240712235311-98bd56499dfb/go.mod h1:c0kvRShutpj3l6B9WtTsNTBUtjSmjZXbJd9ZBRQOSKI= -github.com/lightningnetwork/lnd v0.19.0-beta.rc2.0.20250417120008-a304be6bad91 h1:LzLA7+J/fP1VrK4BcyAt86cg4/bkfY38gYRBJoy109o= -github.com/lightningnetwork/lnd v0.19.0-beta.rc2.0.20250417120008-a304be6bad91/go.mod h1:v4Y0gLAIqqxY83J+4HilQHIiScIy2ok2GSuBFtoc1zc= +github.com/lightningnetwork/lnd v0.19.0-beta.rc2.0.20250423092132-a35ace7371af h1:+t8N7kmI7YVu7Hzv8pPiMVCTjnSRi/qOxbAkXa5rn+0= +github.com/lightningnetwork/lnd v0.19.0-beta.rc2.0.20250423092132-a35ace7371af/go.mod h1:nCkZ6G6twxDKn31117M0BNfN5JSAmJVAHNTwYrn31BQ= github.com/lightningnetwork/lnd/cert v1.2.2 h1:71YK6hogeJtxSxw2teq3eGeuy4rHGKcFf0d0Uy4qBjI= github.com/lightningnetwork/lnd/cert v1.2.2/go.mod h1:jQmFn/Ez4zhDgq2hnYSw8r35bqGVxViXhX6Cd7HXM6U= github.com/lightningnetwork/lnd/clock v1.1.1 h1:OfR3/zcJd2RhH0RU+zX/77c0ZiOnIMsDIBjgjWdZgA0= @@ -512,8 +512,8 @@ github.com/lightningnetwork/lnd/fn/v2 v2.0.8 h1:r2SLz7gZYQPVc3IZhU82M66guz3Zk2oY github.com/lightningnetwork/lnd/fn/v2 v2.0.8/go.mod h1:TOzwrhjB/Azw1V7aa8t21ufcQmdsQOQMDtxVOQWNl8s= github.com/lightningnetwork/lnd/healthcheck v1.2.6 h1:1sWhqr93GdkWy4+6U7JxBfcyZIE78MhIHTJZfPx7qqI= github.com/lightningnetwork/lnd/healthcheck v1.2.6/go.mod h1:Mu02um4CWY/zdTOvFje7WJgJcHyX2zq/FG3MhOAiGaQ= -github.com/lightningnetwork/lnd/kvdb v1.4.15 h1:3eN6uGcubvGB5itPp1D0D4uEEkIMYht3w0LDnqLzAWI= -github.com/lightningnetwork/lnd/kvdb v1.4.15/go.mod h1:HW+bvwkxNaopkz3oIgBV6NEnV4jCEZCACFUcNg4xSjM= +github.com/lightningnetwork/lnd/kvdb v1.4.16 h1:9BZgWdDfjmHRHLS97cz39bVuBAqMc4/p3HX1xtUdbDI= +github.com/lightningnetwork/lnd/kvdb v1.4.16/go.mod h1:HW+bvwkxNaopkz3oIgBV6NEnV4jCEZCACFUcNg4xSjM= github.com/lightningnetwork/lnd/queue v1.1.1 h1:99ovBlpM9B0FRCGYJo6RSFDlt8/vOkQQZznVb18iNMI= github.com/lightningnetwork/lnd/queue v1.1.1/go.mod h1:7A6nC1Qrm32FHuhx/mi1cieAiBZo5O6l8IBIoQxvkz4= github.com/lightningnetwork/lnd/sqldb v1.0.9 h1:7OHi+Hui823mB/U9NzCdlZTAGSVdDCbjp33+6d/Q+G0= diff --git a/server.go b/server.go index 96aec57cdf..c37d895cf9 100644 --- a/server.go +++ b/server.go @@ -997,16 +997,19 @@ func (s *Server) ChannelFinalized(pid funding.PendingChanID) error { // // NOTE: This method is part of the routing.TlvTrafficShaper interface. func (s *Server) ShouldHandleTraffic(cid lnwire.ShortChannelID, - fundingBlob lfn.Option[tlv.Blob]) (bool, error) { + fundingBlob, htlcBlob lfn.Option[tlv.Blob]) (bool, error) { - srvrLog.Debugf("HandleTraffic called (cid=%v, fundingBlob=%x)", cid, - fundingBlob.UnwrapOr(tlv.Blob{})) + srvrLog.Debugf("HandleTraffic called (cid=%v, fundingBlob=%v, "+ + "htlcBlob=%v)", cid, fundingBlob.UnwrapOr(tlv.Blob{}), + htlcBlob.UnwrapOr(tlv.Blob{})) if err := s.waitForReady(); err != nil { return false, err } - return s.cfg.AuxTrafficShaper.ShouldHandleTraffic(cid, fundingBlob) + return s.cfg.AuxTrafficShaper.ShouldHandleTraffic( + cid, fundingBlob, htlcBlob, + ) } // PaymentBandwidth returns the available bandwidth for a custom channel decided @@ -1016,20 +1019,22 @@ func (s *Server) ShouldHandleTraffic(cid lnwire.ShortChannelID, // called first. // // NOTE: This method is part of the routing.TlvTrafficShaper interface. -func (s *Server) PaymentBandwidth(htlcBlob, commitmentBlob lfn.Option[tlv.Blob], - linkBandwidth, htlcAmt lnwire.MilliSatoshi, +func (s *Server) PaymentBandwidth(fundingBlob, htlcBlob, + commitmentBlob lfn.Option[tlv.Blob], linkBandwidth, + htlcAmt lnwire.MilliSatoshi, htlcView lnwallet.AuxHtlcView) (lnwire.MilliSatoshi, error) { - srvrLog.Debugf("PaymentBandwidth called, htlcBlob=%v, "+ - "commitmentBlob=%v", spew.Sdump(htlcBlob), - spew.Sdump(commitmentBlob)) + srvrLog.Debugf("PaymentBandwidth called, fundingBlob=%v, htlcBlob=%v, "+ + "commitmentBlob=%v", spew.Sdump(fundingBlob), + spew.Sdump(htlcBlob), spew.Sdump(commitmentBlob)) if err := s.waitForReady(); err != nil { return 0, err } return s.cfg.AuxTrafficShaper.PaymentBandwidth( - htlcBlob, commitmentBlob, linkBandwidth, htlcAmt, htlcView, + fundingBlob, htlcBlob, commitmentBlob, linkBandwidth, htlcAmt, + htlcView, ) } diff --git a/tapchannel/aux_traffic_shaper.go b/tapchannel/aux_traffic_shaper.go index b40728132d..64f7aec75c 100644 --- a/tapchannel/aux_traffic_shaper.go +++ b/tapchannel/aux_traffic_shaper.go @@ -82,7 +82,7 @@ func (s *AuxTrafficShaper) Stop() error { // can be skipped and the bandwidth returned by PaymentBandwidth should be used // instead. func (s *AuxTrafficShaper) ShouldHandleTraffic(_ lnwire.ShortChannelID, - fundingBlob lfn.Option[tlv.Blob]) (bool, error) { + fundingBlob, _ lfn.Option[tlv.Blob]) (bool, error) { // If there is no auxiliary blob in the channel, it's not a custom // channel, and we don't need to handle it. @@ -110,7 +110,7 @@ func (s *AuxTrafficShaper) ShouldHandleTraffic(_ lnwire.ShortChannelID, // is no bandwidth available. To find out if a channel is a custom channel that // should be handled by the traffic shaper, the HandleTraffic method should be // called first. -func (s *AuxTrafficShaper) PaymentBandwidth(htlcBlob, +func (s *AuxTrafficShaper) PaymentBandwidth(_, htlcBlob, commitmentBlob lfn.Option[tlv.Blob], linkBandwidth, htlcAmt lnwire.MilliSatoshi, htlcView lnwallet.AuxHtlcView) (lnwire.MilliSatoshi, error) { From 443de9ff9191ff4eb82b1c61a8f4380b3ad765c9 Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Thu, 17 Apr 2025 17:14:29 +0200 Subject: [PATCH 02/10] server: use log closures for better performance To avoid needing to call spew.Sdump() when the trace log level isn't even being used, we wrap the calls in a function closure instead. That way the potentially CPU intensive spew only happens when the trace log level is actually enabled. --- server.go | 36 +++++++++++++++++++++--------------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/server.go b/server.go index c37d895cf9..c26802d21a 100644 --- a/server.go +++ b/server.go @@ -12,7 +12,6 @@ import ( "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/wire" - "github.com/davecgh/go-spew/spew" proxy "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" "github.com/lightninglabs/lndclient" "github.com/lightninglabs/taproot-assets/address" @@ -34,6 +33,7 @@ import ( "github.com/lightningnetwork/lnd/lncfg" "github.com/lightningnetwork/lnd/lnrpc" "github.com/lightningnetwork/lnd/lntypes" + "github.com/lightningnetwork/lnd/lnutils" "github.com/lightningnetwork/lnd/lnwallet" lnwl "github.com/lightningnetwork/lnd/lnwallet" "github.com/lightningnetwork/lnd/lnwallet/chancloser" @@ -999,9 +999,9 @@ func (s *Server) ChannelFinalized(pid funding.PendingChanID) error { func (s *Server) ShouldHandleTraffic(cid lnwire.ShortChannelID, fundingBlob, htlcBlob lfn.Option[tlv.Blob]) (bool, error) { - srvrLog.Debugf("HandleTraffic called (cid=%v, fundingBlob=%v, "+ - "htlcBlob=%v)", cid, fundingBlob.UnwrapOr(tlv.Blob{}), - htlcBlob.UnwrapOr(tlv.Blob{})) + srvrLog.Debugf("HandleTraffic called, cid=%v, fundingBlob=%v, "+ + "htlcBlob=%v", cid, lnutils.SpewLogClosure(fundingBlob), + lnutils.SpewLogClosure(htlcBlob)) if err := s.waitForReady(); err != nil { return false, err @@ -1025,8 +1025,9 @@ func (s *Server) PaymentBandwidth(fundingBlob, htlcBlob, htlcView lnwallet.AuxHtlcView) (lnwire.MilliSatoshi, error) { srvrLog.Debugf("PaymentBandwidth called, fundingBlob=%v, htlcBlob=%v, "+ - "commitmentBlob=%v", spew.Sdump(fundingBlob), - spew.Sdump(htlcBlob), spew.Sdump(commitmentBlob)) + "commitmentBlob=%v", lnutils.SpewLogClosure(fundingBlob), + lnutils.SpewLogClosure(htlcBlob), + lnutils.SpewLogClosure(commitmentBlob)) if err := s.waitForReady(); err != nil { return 0, err @@ -1048,7 +1049,8 @@ func (s *Server) ProduceHtlcExtraData(totalAmount lnwire.MilliSatoshi, lnwire.CustomRecords, error) { srvrLog.Debugf("ProduceHtlcExtraData called, totalAmount=%d, "+ - "htlcBlob=%v", totalAmount, spew.Sdump(htlcCustomRecords)) + "htlcBlob=%v", totalAmount, + lnutils.SpewLogClosure(htlcCustomRecords)) if err := s.waitForReady(); err != nil { return 0, nil, err @@ -1079,7 +1081,8 @@ func (s *Server) AuxCloseOutputs( desc chancloser.AuxCloseDesc) (lfn.Option[chancloser.AuxCloseOutputs], error) { - srvrLog.Tracef("AuxCloseOutputs called, desc=%v", spew.Sdump(desc)) + srvrLog.Tracef("AuxCloseOutputs called, desc=%v", + lnutils.SpewLogClosure(desc)) if err := s.waitForReady(); err != nil { return lfn.None[chancloser.AuxCloseOutputs](), err @@ -1096,7 +1099,8 @@ func (s *Server) ShutdownBlob( req chancloser.AuxShutdownReq) (lfn.Option[lnwire.CustomRecords], error) { - srvrLog.Tracef("ShutdownBlob called, req=%v", spew.Sdump(req)) + srvrLog.Tracef("ShutdownBlob called, req=%v", + lnutils.SpewLogClosure(req)) if err := s.waitForReady(); err != nil { return lfn.None[lnwire.CustomRecords](), err @@ -1114,7 +1118,7 @@ func (s *Server) FinalizeClose(desc chancloser.AuxCloseDesc, closeTx *wire.MsgTx) error { srvrLog.Tracef("FinalizeClose called, desc=%v, closeTx=%v", - spew.Sdump(desc), spew.Sdump(closeTx)) + lnutils.SpewLogClosure(desc), lnutils.SpewLogClosure(closeTx)) if err := s.waitForReady(); err != nil { return err @@ -1128,7 +1132,8 @@ func (s *Server) FinalizeClose(desc chancloser.AuxCloseDesc, // // NOTE: This method is part of the lnwallet.AuxContractResolver interface. func (s *Server) ResolveContract(req lnwl.ResolutionReq) lfn.Result[tlv.Blob] { - srvrLog.Tracef("ResolveContract called, req=%v", spew.Sdump(req)) + srvrLog.Tracef("ResolveContract called, req=%v", + lnutils.SpewLogClosure(req)) if err := s.waitForReady(); err != nil { return lfn.Err[tlv.Blob](err) @@ -1146,7 +1151,7 @@ func (s *Server) DeriveSweepAddr(inputs []input.Input, change lnwl.AddrWithKey) lfn.Result[sweep.SweepOutput] { srvrLog.Tracef("DeriveSweepAddr called, inputs=%v, change=%v", - spew.Sdump(inputs), spew.Sdump(change)) + lnutils.SpewLogClosure(inputs), lnutils.SpewLogClosure(change)) if err := s.waitForReady(); err != nil { return lfn.Err[sweep.SweepOutput](err) @@ -1163,7 +1168,7 @@ func (s *Server) ExtraBudgetForInputs( inputs []input.Input) lfn.Result[btcutil.Amount] { srvrLog.Tracef("ExtraBudgetForInputs called, inputs=%v", - spew.Sdump(inputs)) + lnutils.SpewLogClosure(inputs)) if err := s.waitForReady(); err != nil { return lfn.Err[btcutil.Amount](err) @@ -1181,8 +1186,9 @@ func (s *Server) NotifyBroadcast(req *sweep.BumpRequest, outpointToTxIndex map[wire.OutPoint]int) error { srvrLog.Tracef("NotifyBroadcast called, req=%v, tx=%v, fee=%v, "+ - "out_index=%v", spew.Sdump(req), spew.Sdump(tx), fee, - spew.Sdump(outpointToTxIndex)) + "out_index=%v", lnutils.SpewLogClosure(req), + lnutils.SpewLogClosure(tx), fee, + lnutils.SpewLogClosure(outpointToTxIndex)) if err := s.waitForReady(); err != nil { return err From 00b52884c164cc3067e3079024c4065f43cff379 Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Wed, 23 Apr 2025 13:37:54 +0200 Subject: [PATCH 03/10] tapchannel+tapchannelmsg: add group key to funding blob To be able to detect certain asset related routing conditions, we'll want to commit the group key of assets into the funding blob of the channel. --- rfqmsg/custom_channel_data.go | 1 + tapchannel/aux_funding_controller.go | 80 ++++++++++++++++++++++- tapchannelmsg/custom_channel_data.go | 9 +++ tapchannelmsg/custom_channel_data_test.go | 4 +- tapchannelmsg/records.go | 53 +++++++++++---- tapchannelmsg/records_test.go | 13 +++- 6 files changed, 143 insertions(+), 17 deletions(-) diff --git a/rfqmsg/custom_channel_data.go b/rfqmsg/custom_channel_data.go index cd4deb3e85..e5f0a8dde6 100644 --- a/rfqmsg/custom_channel_data.go +++ b/rfqmsg/custom_channel_data.go @@ -37,6 +37,7 @@ type JsonAssetChannel struct { OutgoingHtlcs []JsonAssetTranche `json:"outgoing_htlcs"` IncomingHtlcs []JsonAssetTranche `json:"incoming_htlcs"` Capacity uint64 `json:"capacity"` + GroupKey string `json:"group_key,omitempty"` LocalBalance uint64 `json:"local_balance"` RemoteBalance uint64 `json:"remote_balance"` OutgoingHtlcBalance uint64 `json:"outgoing_htlc_balance"` diff --git a/tapchannel/aux_funding_controller.go b/tapchannel/aux_funding_controller.go index b323b8a21a..52d126aaab 100644 --- a/tapchannel/aux_funding_controller.go +++ b/tapchannel/aux_funding_controller.go @@ -576,7 +576,8 @@ func newCommitBlobAndLeaves(pendingFunding *pendingAssetFunding, // both sides are able to construct the funding output, and will be able to // store the appropriate funding blobs. func (p *pendingAssetFunding) toAuxFundingDesc(req *bindFundingReq, - decimalDisplay uint8) (*lnwallet.AuxFundingDesc, error) { + decimalDisplay uint8, + groupKey *btcec.PublicKey) (*lnwallet.AuxFundingDesc, error) { // First, we'll map all the assets into asset outputs that'll be stored // in the open channel struct on the lnd side. @@ -584,7 +585,9 @@ func (p *pendingAssetFunding) toAuxFundingDesc(req *bindFundingReq, // With all the outputs assembled, we'll now map that to the open // channel wrapper that'll go in the set of TLV blobs. - openChanDesc := cmsg.NewOpenChannel(assetOutputs, decimalDisplay) + openChanDesc := cmsg.NewOpenChannel( + assetOutputs, decimalDisplay, groupKey, + ) // Now we'll encode the 3 TLV blobs that lnd will store: the main one // for the funding details, and then the blobs for the local and remote @@ -1933,8 +1936,20 @@ func (f *FundingController) chanFunder() { continue } + groupKey, err := f.fundingAssetGroupKey( + ctxc, fundingFlow.assetOutputs(), + ) + if err != nil { + fErr := fmt.Errorf("unable to determine group "+ + "key: %w", err) + f.cfg.ErrReporter.ReportError( + ctxc, fundingFlow.peerPub, pid, fErr, + ) + continue + } + fundingDesc, err := fundingFlow.toAuxFundingDesc( - req, decimalDisplay, + req, decimalDisplay, groupKey, ) if err != nil { fErr := fmt.Errorf("unable to create aux "+ @@ -2029,6 +2044,65 @@ func (f *FundingController) fundingAssetDecimalDisplay(ctx context.Context, return decimalDisplay, nil } +// fundingAssetGroupKey determines the group key of the funding asset(s). If no +// group key was used to fund the channel, then nil is returned. +func (f *FundingController) fundingAssetGroupKey(ctx context.Context, + assetOutputs []*cmsg.AssetOutput) (*btcec.PublicKey, error) { + + // We now check the group key of each funding asset, to make sure we + // know the meta information for each asset. And we also verify that + // each asset tranche has the same group key. + var groupKey *btcec.PublicKey + for _, a := range assetOutputs { + info, err := f.cfg.AssetSyncer.QueryAssetInfo( + ctx, a.AssetID.Val, + ) + switch { + // If the asset isn't a grouped asset (or we don't know the + // asset), then we just continue. + case errors.Is(err, address.ErrAssetGroupUnknown): + continue + + case err != nil: + return nil, fmt.Errorf("unable to fetch group info: %w", + err) + } + + switch { + // We haven't set the group key before and have found one now, + // perfect. Let's assume that's our group key we'll use. + case groupKey == nil && info.GroupKey != nil: + groupKey = &info.GroupKey.GroupPubKey + + // If we already have a group key, then we need to verify that + // the group key of this asset matches the one we already have. + case groupKey != nil && info.GroupKey != nil: + if !groupKey.IsEqual(&info.GroupKey.GroupPubKey) { + return nil, fmt.Errorf("group key mismatch: "+ + "expected %x, got %x", + groupKey.SerializeCompressed(), + info.GroupPubKey.SerializeCompressed()) + } + + // If a previous asset resulted in a group key, every following + // one must also result in the same one. If we can't find one + // now, it means we either don't know about the asset (not + // synced) or it's not a grouped asset. + case groupKey != nil && info.GroupKey == nil: + return nil, fmt.Errorf("group key mismatch: "+ + "expected %x, got nil", + groupKey.SerializeCompressed()) + + // If we don't have a group key yet, and the asset isn't a + // grouped asset, then we just continue. + case groupKey == nil && info.GroupKey == nil: + continue + } + } + + return groupKey, nil +} + // channelAcceptor is a callback that's called by the lnd client when a new // channel is proposed. This function is responsible for deciding whether to // accept the channel based on the channel parameters, and to also set some diff --git a/tapchannelmsg/custom_channel_data.go b/tapchannelmsg/custom_channel_data.go index 2fc2e1bd25..fe7dba18d1 100644 --- a/tapchannelmsg/custom_channel_data.go +++ b/tapchannelmsg/custom_channel_data.go @@ -7,6 +7,7 @@ import ( "fmt" "io" + "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/wire" "github.com/lightninglabs/taproot-assets/rfqmsg" "github.com/lightningnetwork/lnd/lnrpc" @@ -84,6 +85,14 @@ func (c *ChannelCustomData) AsJson() ([]byte, error) { IncomingHtlcBalance: c.LocalCommit.IncomingHtlcAssets.Val.Sum(), } + c.OpenChan.GroupKey.ValOpt().WhenSome(func(key *btcec.PublicKey) { + if key != nil { + resp.GroupKey = hex.EncodeToString( + key.SerializeCompressed(), + ) + } + }) + // First, we encode the funding state, which lists all assets committed // to the channel at the time of channel opening. for _, output := range c.OpenChan.Assets() { diff --git a/tapchannelmsg/custom_channel_data_test.go b/tapchannelmsg/custom_channel_data_test.go index bd23d189e7..5a728c77a5 100644 --- a/tapchannelmsg/custom_channel_data_test.go +++ b/tapchannelmsg/custom_channel_data_test.go @@ -39,7 +39,9 @@ func TestReadChannelCustomData(t *testing.T) { output3 := NewAssetOutput(assetID3, 3000, proof3) output4 := NewAssetOutput(assetID4, 4000, proof4) - fundingState := NewOpenChannel([]*AssetOutput{output1, output2}, 11) + fundingState := NewOpenChannel( + []*AssetOutput{output1, output2}, 11, nil, + ) commitState := NewCommitment( []*AssetOutput{output1}, []*AssetOutput{output2}, map[input.HtlcIndex][]*AssetOutput{ diff --git a/tapchannelmsg/records.go b/tapchannelmsg/records.go index 959649f62f..b9b977978c 100644 --- a/tapchannelmsg/records.go +++ b/tapchannelmsg/records.go @@ -94,11 +94,23 @@ type OpenChannel struct { // this value needs to be the same for all assets. Otherwise, they would // not be fungible. DecimalDisplay tlv.RecordT[tlv.TlvType1, uint8] + + // GroupKey is the optional group key used to fund this channel. + GroupKey tlv.OptionalRecordT[tlv.TlvType2, *btcec.PublicKey] } // NewOpenChannel creates a new OpenChannel record with the given funded assets. -func NewOpenChannel(fundedAssets []*AssetOutput, - decimalDisplay uint8) *OpenChannel { +func NewOpenChannel(fundedAssets []*AssetOutput, decimalDisplay uint8, + groupKey *btcec.PublicKey) *OpenChannel { + + var optGroupRecord tlv.OptionalRecordT[tlv.TlvType2, *btcec.PublicKey] + if groupKey != nil { + optGroupRecord = tlv.SomeRecordT[tlv.TlvType2]( + tlv.NewPrimitiveRecord[tlv.TlvType2]( + groupKey, + ), + ) + } return &OpenChannel{ FundedAssets: tlv.NewRecordT[tlv.TlvType0]( @@ -109,6 +121,7 @@ func NewOpenChannel(fundedAssets []*AssetOutput, DecimalDisplay: tlv.NewPrimitiveRecord[tlv.TlvType1]( decimalDisplay, ), + GroupKey: optGroupRecord, } } @@ -118,17 +131,18 @@ func (o *OpenChannel) Assets() []*AssetOutput { return o.FundedAssets.Val.Outputs } -// records returns the records that make up the OpenChannel. -func (o *OpenChannel) records() []tlv.Record { - return []tlv.Record{ +// Encode serializes the OpenChannel to the given io.Writer. +func (o *OpenChannel) Encode(w io.Writer) error { + tlvRecords := []tlv.Record{ o.FundedAssets.Record(), o.DecimalDisplay.Record(), } -} -// Encode serializes the OpenChannel to the given io.Writer. -func (o *OpenChannel) Encode(w io.Writer) error { - tlvRecords := o.records() + o.GroupKey.WhenSome( + func(r tlv.RecordT[tlv.TlvType2, *btcec.PublicKey]) { + tlvRecords = append(tlvRecords, r.Record()) + }, + ) // Create the tlv stream. tlvStream, err := tlv.NewStream(tlvRecords...) @@ -141,13 +155,30 @@ func (o *OpenChannel) Encode(w io.Writer) error { // Decode deserializes the OpenChannel from the given io.Reader. func (o *OpenChannel) Decode(r io.Reader) error { + groupKey := o.GroupKey.Zero() + + tlvRecords := []tlv.Record{ + o.FundedAssets.Record(), + o.DecimalDisplay.Record(), + groupKey.Record(), + } + // Create the tlv stream. - tlvStream, err := tlv.NewStream(o.records()...) + tlvStream, err := tlv.NewStream(tlvRecords...) if err != nil { return err } - return tlvStream.Decode(r) + tlvs, err := tlvStream.DecodeWithParsedTypes(r) + if err != nil { + return err + } + + if _, ok := tlvs[groupKey.TlvType()]; ok { + o.GroupKey = tlv.SomeRecordT(groupKey) + } + + return nil } // Bytes returns the serialized OpenChannel record. diff --git a/tapchannelmsg/records_test.go b/tapchannelmsg/records_test.go index 888911f7c9..1f6222b608 100644 --- a/tapchannelmsg/records_test.go +++ b/tapchannelmsg/records_test.go @@ -65,6 +65,7 @@ func TestOpenChannel(t *testing.T) { require.NoError(t, err) randProof, err := proof.Decode(proofBytes) require.NoError(t, err) + randGroupKey := test.RandPubKey(t) testCases := []struct { name string @@ -78,14 +79,22 @@ func TestOpenChannel(t *testing.T) { name: "channel with funded asset", channel: NewOpenChannel([]*AssetOutput{ NewAssetOutput([32]byte{1}, 1000, *randProof), - }, 0), + }, 0, nil), }, { name: "channel with multiple funded assets", channel: NewOpenChannel([]*AssetOutput{ NewAssetOutput([32]byte{1}, 1000, *randProof), NewAssetOutput([32]byte{2}, 2000, *randProof), - }, 11), + }, 11, nil), + }, + { + name: "channel with multiple funded assets and group " + + "key", + channel: NewOpenChannel([]*AssetOutput{ + NewAssetOutput([32]byte{1}, 1000, *randProof), + NewAssetOutput([32]byte{2}, 2000, *randProof), + }, 11, randGroupKey), }, } From 1268a356386c9d046f76f21489b8b55a18f859a7 Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Thu, 17 Apr 2025 17:16:20 +0200 Subject: [PATCH 04/10] rfqmsg+tapchannelmsg: add helper functions for asset ID detection To easily find out if all asset IDs from a set are actually committed to a channel, we add helper functions for the different channel messages that we can encounter in our subsystems (depending on whether we get the message from a blob in a hook or as JSON over the RPC interface). --- rfqmsg/custom_channel_data.go | 34 +++++++++++++++++++++++++++++ tapchannelmsg/records.go | 41 +++++++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+) diff --git a/rfqmsg/custom_channel_data.go b/rfqmsg/custom_channel_data.go index e5f0a8dde6..19d3e2a25f 100644 --- a/rfqmsg/custom_channel_data.go +++ b/rfqmsg/custom_channel_data.go @@ -1,5 +1,12 @@ package rfqmsg +import ( + "strings" + + "github.com/lightninglabs/taproot-assets/asset" + "github.com/lightninglabs/taproot-assets/fn" +) + // JsonAssetBalance is a struct that represents the balance of a single asset ID // within a channel. type JsonAssetBalance struct { @@ -44,6 +51,33 @@ type JsonAssetChannel struct { IncomingHtlcBalance uint64 `json:"incoming_htlc_balance"` } +// HasAllAssetIDs checks if the OpenChannel contains all asset IDs in the +// provided set. It returns true if all asset IDs are present, false otherwise. +func (c *JsonAssetChannel) HasAllAssetIDs(ids fn.Set[asset.ID]) bool { + // There is a possibility that we're checking the asset ID from an HTLC + // that hasn't been materialized yet and could actually contain a group + // key x-coordinate. That should only be the case if there is a single + // asset ID. + if len(ids) == 1 && c.GroupKey != "" { + assetID := ids.ToSlice()[0] + if strings.Contains(c.GroupKey, assetID.String()) { + return true + } + } + + availableIDStrings := fn.NewSet(fn.Map( + c.FundingAssets, func(fundingAsset JsonAssetUtxo) string { + return fundingAsset.AssetGenesis.AssetID + }, + )...) + targetIDStrings := fn.NewSet(fn.Map( + ids.ToSlice(), func(id asset.ID) string { + return id.String() + }, + )...) + return targetIDStrings.Subset(availableIDStrings) +} + // JsonAssetChannelBalances is a struct that represents the balance information // of all assets within open and pending channels. type JsonAssetChannelBalances struct { diff --git a/tapchannelmsg/records.go b/tapchannelmsg/records.go index b9b977978c..0bb06e9757 100644 --- a/tapchannelmsg/records.go +++ b/tapchannelmsg/records.go @@ -8,6 +8,7 @@ import ( "net/url" "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2/schnorr" "github.com/btcsuite/btcd/txscript" "github.com/lightninglabs/taproot-assets/asset" "github.com/lightninglabs/taproot-assets/fn" @@ -188,6 +189,46 @@ func (o *OpenChannel) Bytes() []byte { return buf.Bytes() } +// HasAllAssetIDs checks if the OpenChannel contains all asset IDs in the +// provided set. It returns true if all asset IDs are present, false otherwise. +func (o *OpenChannel) HasAllAssetIDs(ids fn.Set[asset.ID]) bool { + // There is a possibility that we're checking the asset ID from an HTLC + // that hasn't been materialized yet and could actually contain a group + // key x-coordinate. That should only be the case if there is a single + // asset ID. + if len(ids) == 1 && o.GroupKey.IsSome() { + assetID := ids.ToSlice()[0] + groupKeyMatch := lfn.MapOptionZ( + o.GroupKey.ValOpt(), + func(groupKey *btcec.PublicKey) bool { + if groupKey == nil { + return false + } + + return bytes.Equal( + assetID[:], schnorr.SerializePubKey( + groupKey, + ), + ) + }, + ) + + // Only if we get a match do we short-circuit the explicit asset + // ID check. + if groupKeyMatch { + return true + } + } + + availableIDs := fn.NewSet(fn.Map( + o.Assets(), func(output *AssetOutput) asset.ID { + return output.AssetID.Val + }, + )...) + + return ids.Subset(availableIDs) +} + // DecodeOpenChannel deserializes an OpenChannel from the given blob. func DecodeOpenChannel(blob tlv.Blob) (*OpenChannel, error) { var o OpenChannel From 12f8993e008a745490f8b443f4e2c9ea6a52c837 Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Thu, 17 Apr 2025 17:18:06 +0200 Subject: [PATCH 05/10] tapcfg+tapchannel: validate incoming channel in invoice mgr We need to make sure an asset HTLC actually comes through the correct channel that commits to that asset in the first place. --- tapcfg/server.go | 1 + tapchannel/aux_invoice_manager.go | 111 ++++++++++++++++++--- tapchannel/aux_invoice_manager_test.go | 131 ++++++++++++++++++++++--- 3 files changed, 217 insertions(+), 26 deletions(-) diff --git a/tapcfg/server.go b/tapcfg/server.go index ddbc753c0f..4970f49a9f 100644 --- a/tapcfg/server.go +++ b/tapcfg/server.go @@ -487,6 +487,7 @@ func genServerConfig(cfg *Config, cfgLogger btclog.Logger, ChainParams: &tapChainParams, InvoiceHtlcModifier: lndInvoicesClient, RfqManager: rfqManager, + LightningClient: lndServices.Client, }, ) auxChanCloser := tapchannel.NewAuxChanCloser( diff --git a/tapchannel/aux_invoice_manager.go b/tapchannel/aux_invoice_manager.go index 92be86be7b..5bea9c1dc6 100644 --- a/tapchannel/aux_invoice_manager.go +++ b/tapchannel/aux_invoice_manager.go @@ -2,6 +2,7 @@ package tapchannel import ( "context" + "encoding/json" "fmt" "sync" @@ -13,7 +14,9 @@ import ( "github.com/lightninglabs/taproot-assets/rfqmath" "github.com/lightninglabs/taproot-assets/rfqmsg" "github.com/lightninglabs/taproot-assets/taprpc" + "github.com/lightningnetwork/lnd/invoices" "github.com/lightningnetwork/lnd/lnrpc" + "github.com/lightningnetwork/lnd/lnutils" "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/routing/route" ) @@ -77,6 +80,10 @@ type InvoiceManagerConfig struct { // accepted quotes for determining the incoming value of invoice related // HTLCs. RfqManager RfqManager + + // LndClient is the lnd client that will be used to interact with the + // lnd node. + LightningClient lndclient.LightningClient } // AuxInvoiceManager is a Taproot Asset auxiliary invoice manager that can be @@ -87,6 +94,12 @@ type AuxInvoiceManager struct { cfg *InvoiceManagerConfig + // channelFundingCache is a cache used to store the channel funding + // information for the channels that are used to receive assets. The map + // is keyed by the main channel ID, and the value is the asset channel + // funding information. + channelFundingCache lnutils.SyncMap[uint64, rfqmsg.JsonAssetChannel] + // ContextGuard provides a wait group and main quit channel that can be // used to create guarded contexts. *fn.ContextGuard @@ -206,7 +219,7 @@ func (s *AuxInvoiceManager) handleInvoiceAccept(ctx context.Context, } // We now run some validation checks on the asset HTLC. - err = s.validateAssetHTLC(ctx, htlc) + err = s.validateAssetHTLC(ctx, htlc, resp.CircuitKey) if err != nil { log.Errorf("Failed to validate asset HTLC: %v", err) @@ -268,11 +281,11 @@ func (s *AuxInvoiceManager) handleInvoiceAccept(ctx context.Context, return resp, nil } -// identifierFromQuote retrieves the quote by looking up the rfq manager's maps -// of accepted quotes based on the passed rfq ID. If there's a match, the asset -// specifier is returned. -func (s *AuxInvoiceManager) identifierFromQuote( - rfqID rfqmsg.ID) (asset.Specifier, error) { +// identifierAndPeerFromQuote retrieves the quote by looking up the rfq +// manager's maps of accepted quotes based on the passed rfq ID. If there's a +// match, the asset specifier and peer are returned. +func (s *AuxInvoiceManager) identifierAndPeerFromQuote( + rfqID rfqmsg.ID) (asset.Specifier, route.Vertex, error) { acceptedBuyQuotes := s.cfg.RfqManager.PeerAcceptedBuyQuotes() acceptedSellQuotes := s.cfg.RfqManager.LocalAcceptedSellQuotes() @@ -280,23 +293,28 @@ func (s *AuxInvoiceManager) identifierFromQuote( buyQuote, isBuy := acceptedBuyQuotes[rfqID.Scid()] sellQuote, isSell := acceptedSellQuotes[rfqID.Scid()] - var specifier asset.Specifier + var ( + specifier asset.Specifier + peer route.Vertex + ) switch { case isBuy: specifier = buyQuote.Request.AssetSpecifier + peer = buyQuote.Peer case isSell: specifier = sellQuote.Request.AssetSpecifier + peer = sellQuote.Peer } err := specifier.AssertNotEmpty() if err != nil { - return specifier, fmt.Errorf("rfqID does not match any "+ + return specifier, peer, fmt.Errorf("rfqID does not match any "+ "accepted buy or sell quote: %v", err) } - return specifier, nil + return specifier, peer, nil } // priceFromQuote retrieves the price from the accepted quote for the given RFQ @@ -390,12 +408,12 @@ func isAssetInvoice(invoice *lnrpc.Invoice, rfqLookup RfqLookup) bool { // validateAssetHTLC runs a couple of checks on the provided asset HTLC. func (s *AuxInvoiceManager) validateAssetHTLC(ctx context.Context, - htlc *rfqmsg.Htlc) error { + htlc *rfqmsg.Htlc, circuitKey invoices.CircuitKey) error { rfqID := htlc.RfqID.ValOpt().UnsafeFromSome() // Retrieve the asset identifier from the RFQ quote. - identifier, err := s.identifierFromQuote(rfqID) + identifier, peer, err := s.identifierAndPeerFromQuote(rfqID) if err != nil { return fmt.Errorf("could not extract assetID from "+ "quote: %v", err) @@ -403,6 +421,7 @@ func (s *AuxInvoiceManager) validateAssetHTLC(ctx context.Context, // Check for each of the asset balances of the HTLC that the identifier // matches that of the RFQ quote. + assetIDs := fn.NewSet[asset.ID]() for _, v := range htlc.Balances() { match, err := s.cfg.RfqManager.AssetMatchesSpecifier( ctx, identifier, v.AssetID.Val, @@ -415,11 +434,81 @@ func (s *AuxInvoiceManager) validateAssetHTLC(ctx context.Context, return fmt.Errorf("asset ID %s does not match %s", v.AssetID.Val.String(), identifier.String()) } + + assetIDs.Add(v.AssetID.Val) + } + + assetData, err := s.fetchChannelAssetData(ctx, circuitKey.ChanID, peer) + if err != nil { + return fmt.Errorf("unable to fetch channel asset data: %w", err) + } + + if !assetData.HasAllAssetIDs(assetIDs) { + return fmt.Errorf("channel %d does not have all asset IDs "+ + "required for HTLC settlement", + circuitKey.ChanID) } return nil } +// fetchChannelAssetData retrieves the asset channel data for the provided +// channel ID. If the cache doesn't contain the data, it is queried from the +// backing lnd node. +func (s *AuxInvoiceManager) fetchChannelAssetData(ctx context.Context, + chanID lnwire.ShortChannelID, + peer route.Vertex) (*rfqmsg.JsonAssetChannel, error) { + + // Do we have the information cached? Great, no lookup necessary. We + // don't need to worry about cache invalidation because the funding + // information remains constant for the lifetime of the channel. + cachedAssetData, ok := s.channelFundingCache.Load(chanID.ToUint64()) + if ok { + return &cachedAssetData, nil + } + + // We also need to validate that the HTLC is actually the correct asset + // and arrived through the correct asset channel. + channels, err := s.cfg.LightningClient.ListChannels( + ctx, true, false, lndclient.WithPeer(peer[:]), + ) + if err != nil { + return nil, fmt.Errorf("unable to list channels: %w", err) + } + + var inboundChannel *lndclient.ChannelInfo + for _, channel := range channels { + if channel.ChannelID == chanID.ToUint64() { + inboundChannel = &channel + break + } + } + + if inboundChannel == nil { + return nil, fmt.Errorf("unable to find channel with short "+ + "channel ID %d", chanID.ToUint64()) + } + + if len(inboundChannel.CustomChannelData) == 0 { + return nil, fmt.Errorf("channel %d does not have custom "+ + "channel data, can't accept asset HTLC over non-asset "+ + "channel", inboundChannel.ChannelID) + } + + var assetData rfqmsg.JsonAssetChannel + err = json.Unmarshal(inboundChannel.CustomChannelData, &assetData) + if err != nil { + return nil, fmt.Errorf("unable to unmarshal channel asset "+ + "data: %w", err) + } + + // We cache the asset data for the channel so we don't have to look it + // up again. + s.channelFundingCache.Store(chanID.ToUint64(), assetData) + + return &assetData, nil +} + // Stop signals for an aux invoice manager to gracefully exit. func (s *AuxInvoiceManager) Stop() error { var stopErr error diff --git a/tapchannel/aux_invoice_manager_test.go b/tapchannel/aux_invoice_manager_test.go index e04132bc61..415f31cc4b 100644 --- a/tapchannel/aux_invoice_manager_test.go +++ b/tapchannel/aux_invoice_manager_test.go @@ -1,8 +1,10 @@ package tapchannel import ( + "bytes" "context" "crypto/sha256" + "encoding/json" "fmt" "math/big" "testing" @@ -12,9 +14,11 @@ import ( "github.com/lightninglabs/lndclient" "github.com/lightninglabs/taproot-assets/asset" "github.com/lightninglabs/taproot-assets/fn" + "github.com/lightninglabs/taproot-assets/internal/test" "github.com/lightninglabs/taproot-assets/rfq" "github.com/lightninglabs/taproot-assets/rfqmath" "github.com/lightninglabs/taproot-assets/rfqmsg" + "github.com/lightningnetwork/lnd/invoices" "github.com/lightningnetwork/lnd/lnrpc" "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/routing/route" @@ -298,7 +302,9 @@ func (m *mockHtlcModifierProperty) HtlcModifier(ctx context.Context, } } else { if assetValueMsat != res.AmtPaid { - m.t.Errorf("unexpected final asset value") + m.t.Errorf("unexpected final asset value, "+ + "wanted %d, got %d", assetValueMsat, + res.AmtPaid) } } } @@ -309,9 +315,30 @@ func (m *mockHtlcModifierProperty) HtlcModifier(ctx context.Context, return nil } +type mockLndClient struct { + lndclient.LightningClient + + channels []lndclient.ChannelInfo +} + +// ListChannels retrieves all channels of the backing lnd node. +func (m *mockLndClient) ListChannels(_ context.Context, _, _ bool, + _ ...lndclient.ListChannelsOption) ([]lndclient.ChannelInfo, error) { + + return m.channels, nil +} + // TestAuxInvoiceManager tests that the htlc modifications of the aux invoice // manager align with our expectations. func TestAuxInvoiceManager(t *testing.T) { + randCircuitKey := func() invoices.CircuitKey { + return invoices.CircuitKey{ + ChanID: lnwire.NewShortChanIDFromInt( + test.RandInt[uint64](), + ), + } + } + testCases := []struct { name string buyQuotes rfq.BuyAcceptMap @@ -324,6 +351,7 @@ func TestAuxInvoiceManager(t *testing.T) { name: "non asset invoice", requests: []lndclient.InvoiceHtlcModifyRequest{ { + CircuitKey: randCircuitKey(), Invoice: &lnrpc.Invoice{}, ExitHtlcAmt: 1234, }, @@ -338,6 +366,7 @@ func TestAuxInvoiceManager(t *testing.T) { name: "non asset routing hints", requests: []lndclient.InvoiceHtlcModifyRequest{ { + CircuitKey: randCircuitKey(), Invoice: &lnrpc.Invoice{ RouteHints: testNonAssetHints(), ValueMsat: 1_000_000, @@ -360,6 +389,7 @@ func TestAuxInvoiceManager(t *testing.T) { name: "asset invoice, no custom records", requests: []lndclient.InvoiceHtlcModifyRequest{ { + CircuitKey: randCircuitKey(), Invoice: &lnrpc.Invoice{ RouteHints: testRouteHints(), PaymentAddr: []byte{1, 1, 1}, @@ -382,6 +412,7 @@ func TestAuxInvoiceManager(t *testing.T) { name: "asset invoice, custom records", requests: []lndclient.InvoiceHtlcModifyRequest{ { + CircuitKey: randCircuitKey(), Invoice: &lnrpc.Invoice{ RouteHints: testRouteHints(), ValueMsat: 3_000_000, @@ -417,6 +448,7 @@ func TestAuxInvoiceManager(t *testing.T) { name: "asset invoice, not enough amt", requests: []lndclient.InvoiceHtlcModifyRequest{ { + CircuitKey: randCircuitKey(), Invoice: &lnrpc.Invoice{ RouteHints: testRouteHints(), ValueMsat: 10_000_000, @@ -453,6 +485,7 @@ func TestAuxInvoiceManager(t *testing.T) { name: "btc invoice, custom records", requests: []lndclient.InvoiceHtlcModifyRequest{ { + CircuitKey: randCircuitKey(), Invoice: &lnrpc.Invoice{ ValueMsat: 10_000_000, PaymentAddr: []byte{1, 1, 1}, @@ -477,6 +510,7 @@ func TestAuxInvoiceManager(t *testing.T) { name: "asset invoice, wrong asset htlc", requests: []lndclient.InvoiceHtlcModifyRequest{ { + CircuitKey: randCircuitKey(), Invoice: &lnrpc.Invoice{ RouteHints: testRouteHints(), ValueMsat: 3_000_000, @@ -513,6 +547,7 @@ func TestAuxInvoiceManager(t *testing.T) { name: "asset invoice, group key rfq", requests: []lndclient.InvoiceHtlcModifyRequest{ { + CircuitKey: randCircuitKey(), Invoice: &lnrpc.Invoice{ RouteHints: testRouteHints(), ValueMsat: 20_000_000, @@ -557,6 +592,7 @@ func TestAuxInvoiceManager(t *testing.T) { name: "asset invoice, group key rfq, bad htlc", requests: []lndclient.InvoiceHtlcModifyRequest{ { + CircuitKey: randCircuitKey(), Invoice: &lnrpc.Invoice{ RouteHints: testRouteHints(), ValueMsat: 20_000_000, @@ -604,11 +640,17 @@ func TestAuxInvoiceManager(t *testing.T) { t.Logf("Running AuxInvoiceManager test case: %v", testCase.name) + channels, err := genChannelsFromRequests(testCase.requests) + require.NoError(t, err) + // Instantiate mock rfq manager. mockRfq := &mockRfqManager{ peerBuyQuotes: testCase.buyQuotes, localSellQuotes: testCase.sellQuotes, } + mockLnd := &mockLndClient{ + channels: channels, + } done := make(chan bool) @@ -626,10 +668,11 @@ func TestAuxInvoiceManager(t *testing.T) { ChainParams: testChainParams, InvoiceHtlcModifier: mockModifier, RfqManager: mockRfq, + LightningClient: mockLnd, }, ) - err := manager.Start() + err = manager.Start() require.NoError(t, err) // If the manager is not done processing the htlc modification @@ -777,7 +820,13 @@ func genHtlc(t *rapid.T, balance []*rfqmsg.AssetBalance, func genRequest(t *rapid.T) (lndclient.InvoiceHtlcModifyRequest, uint64, asset.ID, rfqmsg.ID) { - request := lndclient.InvoiceHtlcModifyRequest{} + request := lndclient.InvoiceHtlcModifyRequest{ + CircuitKey: invoices.CircuitKey{ + ChanID: lnwire.NewShortChanIDFromInt( + rapid.Uint64().Draw(t, "chan_id"), + ), + }, + } rfqID := genRandomRfqID(t) @@ -800,13 +849,12 @@ func genRequest(t *rapid.T) (lndclient.InvoiceHtlcModifyRequest, uint64, // genRequests generates a random array of requests to be processed by the // AuxInvoiceManager. It also returns the rfq map with the related rfq quotes. func genRequests(t *rapid.T) ([]lndclient.InvoiceHtlcModifyRequest, - rfq.BuyAcceptMap) { + rfq.BuyAcceptMap, []lndclient.ChannelInfo) { rfqMap := rfq.BuyAcceptMap{} numRequests := rapid.IntRange(1, 5).Draw(t, "requestsLen") - requests := make([]lndclient.InvoiceHtlcModifyRequest, 0) - + var requests []lndclient.InvoiceHtlcModifyRequest for range numRequests { req, numAssets, assetID, scid := genRequest(t) requests = append(requests, req) @@ -819,7 +867,58 @@ func genRequests(t *rapid.T) ([]lndclient.InvoiceHtlcModifyRequest, genBuyQuotes(t, rfqMap, numAssets, quoteAmt, assetID, scid) } - return requests, rfqMap + channels, err := genChannelsFromRequests(requests) + require.NoError(t, err) + + return requests, rfqMap, channels +} + +// genChannelsFromRequests generates a list of channel info instances +// based on the provided requests. +func genChannelsFromRequests( + r []lndclient.InvoiceHtlcModifyRequest) ([]lndclient.ChannelInfo, + error) { + + channels := make([]lndclient.ChannelInfo, len(r)) + for i, req := range r { + var ( + buf bytes.Buffer + htlc rfqmsg.Htlc + jsonAssetChan rfqmsg.JsonAssetChannel + ) + err := req.WireCustomRecords.SerializeTo(&buf) + if err != nil { + return nil, err + } + + err = htlc.Decode(&buf) + if err != nil { + return nil, err + } + + jsonAssetChan.FundingAssets = make( + []rfqmsg.JsonAssetUtxo, len(htlc.Balances()), + ) + for idx, balance := range htlc.Balances() { + fundingAsset := &jsonAssetChan.FundingAssets[idx] + fundingAssetGen := &fundingAsset.AssetGenesis + fundingAssetGen.AssetID = balance.AssetID.Val.String() + + jsonAssetChan.GroupKey += balance.AssetID.Val.String() + } + + jsonChan, err := json.Marshal(jsonAssetChan) + if err != nil { + return nil, err + } + + channels[i] = lndclient.ChannelInfo{ + ChannelID: req.CircuitKey.ChanID.ToUint64(), + CustomChannelData: jsonChan, + } + } + + return channels, nil } // genRandomVertex generates a route.Vertex instance filled with random bytes. @@ -898,11 +997,14 @@ func genBuyQuotes(t *rapid.T, rfqMap rfq.BuyAcceptMap, units, amtMsat uint64, // testInvoiceManager creates an array of requests to be processed by the // AuxInvoiceManager. Uses the enhanced HtlcModifierMockProperty instance. func testInvoiceManager(t *rapid.T) { - requests, rfqMap := genRequests(t) + requests, rfqMap, channels := genRequests(t) mockRfq := &mockRfqManager{ peerBuyQuotes: rfqMap, } + mockLnd := &mockLndClient{ + channels: channels, + } done := make(chan bool) @@ -913,13 +1015,12 @@ func testInvoiceManager(t *rapid.T) { t: t, } - manager := NewAuxInvoiceManager( - &InvoiceManagerConfig{ - ChainParams: testChainParams, - InvoiceHtlcModifier: mockModifier, - RfqManager: mockRfq, - }, - ) + manager := NewAuxInvoiceManager(&InvoiceManagerConfig{ + ChainParams: testChainParams, + InvoiceHtlcModifier: mockModifier, + RfqManager: mockRfq, + LightningClient: mockLnd, + }) err := manager.Start() require.NoError(t, err) From 69398490454d62ff7f8039d6bd1b9cb0929abe6d Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Thu, 17 Apr 2025 17:24:41 +0200 Subject: [PATCH 06/10] tapchannel: move code as preparation We move a block of code further down to where it's going to be used, so the next commit(s) will have a more easy to digest diff. --- tapchannel/aux_traffic_shaper.go | 44 ++++++++++++++++---------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/tapchannel/aux_traffic_shaper.go b/tapchannel/aux_traffic_shaper.go index 64f7aec75c..002ecccbef 100644 --- a/tapchannel/aux_traffic_shaper.go +++ b/tapchannel/aux_traffic_shaper.go @@ -144,28 +144,6 @@ func (s *AuxTrafficShaper) PaymentBandwidth(_, htlcBlob, return linkBandwidth, nil } - // Get the minimum HTLC amount, which is just above dust. - minHtlcAmt := rfqmath.DefaultOnChainHtlcMSat - - // LND calls this hook twice. Once to see if the overall budget of the - // node is enough, and then during pathfinding to actually see if - // there's enough balance in the channel to make the payment attempt. - // - // When doing the overall balance check, we don't know what the actual - // htlcAmt is in satoshis, so a value of 0 will be passed here. Let's at - // least check if we can afford the min amount above dust. If the actual - // htlc amount ends up being greater when calling this method during - // pathfinding, we will still check it below. - - // If the passed htlcAmt is below dust, then assume the dust amount. At - // this point we know we are sending assets, so we cannot anchor them to - // dust amounts. Dust HTLCs are added to the fees and aren't - // materialized in an on-chain output, so we wouldn't have anything - // to anchor the asset commitment to. - if htlcAmt < minHtlcAmt { - htlcAmt = minHtlcAmt - } - commitment, err := cmsg.DecodeCommitment(commitmentBytes) if err != nil { return 0, fmt.Errorf("error decoding commitment blob: %w", err) @@ -196,6 +174,28 @@ func (s *AuxTrafficShaper) PaymentBandwidth(_, htlcBlob, return prettyPrintLocalView(*decodedView) })) + // Get the minimum HTLC amount, which is just above dust. + minHtlcAmt := rfqmath.DefaultOnChainHtlcMSat + + // LND calls this hook twice. Once to see if the overall budget of the + // node is enough, and then during pathfinding to actually see if + // there's enough balance in the channel to make the payment attempt. + // + // When doing the overall balance check, we don't know what the actual + // htlcAmt is in satoshis, so a value of 0 will be passed here. Let's at + // least check if we can afford the min amount above dust. If the actual + // htlc amount ends up being greater when calling this method during + // pathfinding, we will still check it below. + + // If the passed htlcAmt is below dust, then assume the dust amount. At + // this point we know we are sending assets, so we cannot anchor them to + // dust amounts. Dust HTLCs are added to the fees and aren't + // materialized in an on-chain output, so we wouldn't have anything + // to anchor the asset commitment to. + if htlcAmt < minHtlcAmt { + htlcAmt = minHtlcAmt + } + // If the HTLC carries asset units (keysend, forwarding), then there's // no need to do any RFQ related math. We can directly compare the asset // units of the HTLC with those in our local balance. From 74b40b254508e1ec2d56ff64e62bffc4089a9ec9 Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Thu, 17 Apr 2025 17:26:03 +0200 Subject: [PATCH 07/10] server+tapchannel: always handle traffic for asset HTLCs This fixes the first part of the issue: We didn't tell lnd that we wanted to handle traffic for non-asset channels. But that's wrong, because then lnd will pick non-asset channels for HTLCs in some situations. So we explicitly need to tell lnd there is no bandwidth in non-asset channels if an asset HTLC should be forwarded (or sent). --- tapchannel/aux_traffic_shaper.go | 79 +++++++++++++++----------------- 1 file changed, 38 insertions(+), 41 deletions(-) diff --git a/tapchannel/aux_traffic_shaper.go b/tapchannel/aux_traffic_shaper.go index 002ecccbef..e5ffeb55af 100644 --- a/tapchannel/aux_traffic_shaper.go +++ b/tapchannel/aux_traffic_shaper.go @@ -81,27 +81,30 @@ func (s *AuxTrafficShaper) Stop() error { // it is handled by the traffic shaper, then the normal bandwidth calculation // can be skipped and the bandwidth returned by PaymentBandwidth should be used // instead. -func (s *AuxTrafficShaper) ShouldHandleTraffic(_ lnwire.ShortChannelID, - fundingBlob, _ lfn.Option[tlv.Blob]) (bool, error) { - - // If there is no auxiliary blob in the channel, it's not a custom - // channel, and we don't need to handle it. - if fundingBlob.IsNone() { - log.Tracef("No aux funding blob set, not handling traffic") +func (s *AuxTrafficShaper) ShouldHandleTraffic(cid lnwire.ShortChannelID, + _, htlcBlob lfn.Option[tlv.Blob]) (bool, error) { + + // The rule here is simple: If the HTLC is an asset HTLC, we _need_ to + // handle the bandwidth. Because of non-strict forwarding in lnd, it + // could otherwise be the case that we forward an asset HTLC on a + // non-asset channel, which would be a problem. + htlcBytes := htlcBlob.UnwrapOr(nil) + if len(htlcBytes) == 0 { + log.Tracef("Empty HTLC blob, not handling traffic for %v", cid) return false, nil } - // If we can successfully decode the channel blob as a channel capacity - // information, we know that this is a custom channel. - err := lfn.MapOptionZ(fundingBlob, func(blob tlv.Blob) error { - _, err := cmsg.DecodeOpenChannel(blob) - return err - }) - if err != nil { - return false, err + // If there are no asset HTLC custom records, we don't need to do + // anything as this is a regular payment. + if !rfqmsg.HasAssetHTLCEntries(htlcBytes) { + log.Tracef("No asset HTLC custom records, not handling "+ + "traffic for %v", cid) + return false, nil } - // No error, so this is a custom channel, we'll want to decide. + // If this _is_ an asset HTLC, we definitely want to handle the + // bandwidth for this channel, so we can deny forwarding asset HTLCs + // over non-asset channels. return true, nil } @@ -110,38 +113,32 @@ func (s *AuxTrafficShaper) ShouldHandleTraffic(_ lnwire.ShortChannelID, // is no bandwidth available. To find out if a channel is a custom channel that // should be handled by the traffic shaper, the HandleTraffic method should be // called first. -func (s *AuxTrafficShaper) PaymentBandwidth(_, htlcBlob, +func (s *AuxTrafficShaper) PaymentBandwidth(fundingBlob, htlcBlob, commitmentBlob lfn.Option[tlv.Blob], linkBandwidth, htlcAmt lnwire.MilliSatoshi, htlcView lnwallet.AuxHtlcView) (lnwire.MilliSatoshi, error) { - // If the commitment or HTLC blob is not set, we don't have any - // information about the channel and cannot determine the available - // bandwidth from a taproot asset perspective. We return the link - // bandwidth as a fallback. - if commitmentBlob.IsNone() || htlcBlob.IsNone() { - log.Tracef("No commitment or HTLC blob set, returning link "+ - "bandwidth %v", linkBandwidth) - return linkBandwidth, nil - } - - commitmentBytes := commitmentBlob.UnsafeFromSome() - htlcBytes := htlcBlob.UnsafeFromSome() - - // Sometimes the blob is set but actually empty, in which case we also - // don't have any information about the channel. - if len(commitmentBytes) == 0 || len(htlcBytes) == 0 { - log.Tracef("Empty commitment or HTLC blob, returning link "+ - "bandwidth %v", linkBandwidth) + fundingBlobBytes := fundingBlob.UnwrapOr(nil) + htlcBytes := htlcBlob.UnwrapOr(nil) + commitmentBytes := commitmentBlob.UnwrapOr(nil) + + // If the HTLC is not an asset HTLC, we can just return the normal link + // bandwidth, as we don't need to do any special math. We shouldn't even + // get here in the first place, since the ShouldHandleTraffic function + // should return false in this case. + if len(htlcBytes) == 0 || !rfqmsg.HasAssetHTLCEntries(htlcBytes) { + log.Tracef("Empty HTLC blob or no asset HTLC custom records, "+ + "returning link bandwidth %v", linkBandwidth) return linkBandwidth, nil } - // If there are no asset HTLC custom records, we don't need to do - // anything as this is a regular payment. - if !rfqmsg.HasAssetHTLCEntries(htlcBytes) { - log.Tracef("No asset HTLC custom records, returning link "+ - "bandwidth %v", linkBandwidth) - return linkBandwidth, nil + // If this is an asset HTLC but the channel is not an asset channel, we + // MUST deny forwarding the HTLC. + if len(commitmentBytes) == 0 || len(fundingBlobBytes) == 0 { + log.Tracef("Empty commitment or funding blob, cannot forward" + + "asset HTLC over non-asset channel, returning 0 " + + "bandwidth") + return 0, nil } commitment, err := cmsg.DecodeCommitment(commitmentBytes) From b0f4d2397f6caf47d67c04224d8e59796b21793f Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Thu, 17 Apr 2025 17:19:34 +0200 Subject: [PATCH 08/10] tapchannel: validate channel assets for bandwidth This is the third part of the fix: We need to make sure that we don't pick an asset channel that has the wrong type of assets when telling lnd what channel it can use. --- tapchannel/aux_traffic_shaper.go | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/tapchannel/aux_traffic_shaper.go b/tapchannel/aux_traffic_shaper.go index e5ffeb55af..361a9835d9 100644 --- a/tapchannel/aux_traffic_shaper.go +++ b/tapchannel/aux_traffic_shaper.go @@ -141,6 +141,11 @@ func (s *AuxTrafficShaper) PaymentBandwidth(fundingBlob, htlcBlob, return 0, nil } + fundingChan, err := cmsg.DecodeOpenChannel(fundingBlobBytes) + if err != nil { + return 0, fmt.Errorf("error decoding funding blob: %w", err) + } + commitment, err := cmsg.DecodeCommitment(commitmentBytes) if err != nil { return 0, fmt.Errorf("error decoding commitment blob: %w", err) @@ -151,6 +156,23 @@ func (s *AuxTrafficShaper) PaymentBandwidth(fundingBlob, htlcBlob, return 0, fmt.Errorf("error decoding HTLC blob: %w", err) } + // Before we do any further checks, we actually need to make sure that + // the HTLC is compatible with this channel. Because of `lnd`'s + // non-strict forwarding, if there are multiple asset channels, the + // wrong one could be chosen if we signal there's bandwidth. So we need + // to tell `lnd` it can't use this channel if the assets aren't + // compatible. + htlcAssetIDs := fn.NewSet[asset.ID](fn.Map( + htlc.Balances(), func(b *rfqmsg.AssetBalance) asset.ID { + return b.AssetID.Val + })..., + ) + if !fundingChan.HasAllAssetIDs(htlcAssetIDs) { + log.Tracef("HTLC asset IDs %v not compatible with asset IDs "+ + "of channel, returning 0 bandwidth", htlcAssetIDs) + return 0, nil + } + // With the help of the latest HtlcView, let's calculate a more precise // local balance. This is useful in order to not forward HTLCs that may // never be settled. Other HTLCs that may also call into this method are From ef9856749b38ae0548fa14bda5946baf12d41ea1 Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Wed, 23 Apr 2025 13:25:04 +0200 Subject: [PATCH 09/10] asset+tapsend: add more debugging statements and helpers To debug commitment issues, it's useful to log the exact asset leaf that is committed. To be able to easily decode (and compare) that commitment, we also add a unit test that outputs the leaf as JSON. --- asset/asset_test.go | 25 +++++++++++++++++++++++++ asset/testdata/asset.hex | 1 + tapsend/send.go | 9 +++++++-- 3 files changed, 33 insertions(+), 2 deletions(-) create mode 100644 asset/testdata/asset.hex diff --git a/asset/asset_test.go b/asset/asset_test.go index 841dc2f8a8..0f93e8e069 100644 --- a/asset/asset_test.go +++ b/asset/asset_test.go @@ -4,6 +4,9 @@ import ( "bytes" "crypto/sha256" "encoding/hex" + "encoding/json" + "os" + "path/filepath" "reflect" "testing" @@ -106,6 +109,8 @@ var ( GroupPubKey: *pubKey, }, } + + assetHexFileName = filepath.Join("testdata", "asset.hex") ) // TestGenesisAssetClassification tests that the multiple forms of genesis asset @@ -1222,3 +1227,23 @@ func TestExternalKeyPubKey(t *testing.T) { }) } } + +// TestDecodeAsset tests that we can decode an asset from a hex file. This is +// mostly useful for debugging purposes. +func TestDecodeAsset(t *testing.T) { + fileContent, err := os.ReadFile(assetHexFileName) + require.NoError(t, err) + + assetBytes, err := hex.DecodeString(string(fileContent)) + require.NoError(t, err) + + var a Asset + err = a.Decode(bytes.NewReader(assetBytes)) + require.NoError(t, err) + + ta := NewTestFromAsset(t, &a) + assetJSON, err := json.MarshalIndent(ta, "", "\t") + require.NoError(t, err) + + t.Logf("Decoded asset: %v", string(assetJSON)) +} diff --git a/asset/testdata/asset.hex b/asset/testdata/asset.hex new file mode 100644 index 0000000000..58942c536e --- /dev/null +++ b/asset/testdata/asset.hex @@ -0,0 +1 @@ +0001010265fca6685a399ad7e9088ba3911c5b2f02b07cffc319f8086e3f129fbf647c4537000000011b69746573742d61737365742d63656e74732d7472616e6368652d32811ad3c42f355c915d1fc4ba4ed71337092191431308f975d7acbc88a09ab98100000000000401000603fd138a0901040bad01ab01651145af966796fc5f4e7ec057acd24b38c5d0060bfe8e0c74e4c9464c08993a330000000017e137755dac067b0e1d91e33d077ab3482fbe1c382d424412c4620a8e3455eb02e9fa4e023746d43a7440b4148fb00f83f8b22ecb67625313fcb54442df2bdfb403420140791e35d3b49d0c1a1ec6415ba419f027fb4fcf254773e9c54ba77715a793e33c67175901e020b9ed87ab2161aa17a572def28d638ca3656fd5f27d6fd974ae280e020000102102e9fa4e023746d43a7440b4148fb00f83f8b22ecb67625313fcb54442df2bdfb4112102f37e9d09521076209768a6028aa2b42000b042a0635cb90f257c8b00c34a3688 \ No newline at end of file diff --git a/tapsend/send.go b/tapsend/send.go index bb007e9896..bcce1e3623 100644 --- a/tapsend/send.go +++ b/tapsend/send.go @@ -1584,6 +1584,9 @@ func LogCommitment(prefix string, idx int, prefix, idx, tapCommitment.Version, merkleRoot[:], internalKey.SerializeCompressed(), pkScript, trimmedMerkleRoot) for _, a := range tapCommitment.CommittedAssets() { + var buf bytes.Buffer + _ = a.Encode(&buf) + groupKey := "" if a.GroupKey != nil { groupKey = hex.EncodeToString( @@ -1592,9 +1595,11 @@ func LogCommitment(prefix string, idx int, } log.Tracef("%v commitment asset_id=%v, script_key=%x, "+ "group_key=%v, amount=%d, version=%d, "+ - "split_commitment=%v", prefix, a.ID(), + "split_commitment=%v, encoded=%x", prefix, a.ID(), a.ScriptKey.PubKey.SerializeCompressed(), groupKey, - a.Amount, a.Version, a.SplitCommitmentRoot != nil) + a.Amount, a.Version, a.SplitCommitmentRoot != nil, + buf.Bytes()) + } } From 8133f3613781dcc0a4fb138d9f732f5eb62aee2d Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Wed, 23 Apr 2025 13:35:53 +0200 Subject: [PATCH 10/10] tapdb: fix passive asset re-anchoring When re-anchoring a passive asset, we need to reset any time locks it previously had on it. Because the re-anchoring is a normal transfer with just a simple signature, we need to clear any previous restrictions. The ReAnchorPassiveAssets query basically needs to mirror what the asset.CopySpendTemplate() method does. --- tapdb/assets_store.go | 1 - tapdb/sqlc/queries/transfers.sql | 6 +++++- tapdb/sqlc/transfers.sql.go | 6 +++++- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/tapdb/assets_store.go b/tapdb/assets_store.go index 6b51261c76..99f0d8d85c 100644 --- a/tapdb/assets_store.go +++ b/tapdb/assets_store.go @@ -2288,7 +2288,6 @@ func (a *AssetStore) queryCommitments(ctx context.Context, map[wire.OutPoint]*commitment.TapCommitment, ) for anchorPoint := range chainAnchorToAssets { - anchorPoint := anchorPoint anchorUTXO := anchorPoints[anchorPoint] anchoredAssets := chainAnchorToAssets[anchorPoint] anchoredAltLeaves := anchorAltLeaves[anchorPoint] diff --git a/tapdb/sqlc/queries/transfers.sql b/tapdb/sqlc/queries/transfers.sql index 0bcabaf5fa..0d7757b858 100644 --- a/tapdb/sqlc/queries/transfers.sql +++ b/tapdb/sqlc/queries/transfers.sql @@ -137,8 +137,12 @@ RETURNING asset_id; -- name: ReAnchorPassiveAssets :exec UPDATE assets SET anchor_utxo_id = @new_anchor_utxo_id, + -- The following fields need to be the same fields we reset in + -- Asset.CopySpendTemplate. split_commitment_root_hash = NULL, - split_commitment_root_value = NULL + split_commitment_root_value = NULL, + lock_time = 0, + relative_lock_time = 0 WHERE asset_id = @asset_id; -- name: DeleteAssetWitnesses :exec diff --git a/tapdb/sqlc/transfers.sql.go b/tapdb/sqlc/transfers.sql.go index faeb2b7e09..5b1cb1c8a4 100644 --- a/tapdb/sqlc/transfers.sql.go +++ b/tapdb/sqlc/transfers.sql.go @@ -687,8 +687,12 @@ func (q *Queries) QueryProofTransferAttempts(ctx context.Context, arg QueryProof const ReAnchorPassiveAssets = `-- name: ReAnchorPassiveAssets :exec UPDATE assets SET anchor_utxo_id = $1, + -- The following fields need to be the same fields we reset in + -- Asset.CopySpendTemplate. split_commitment_root_hash = NULL, - split_commitment_root_value = NULL + split_commitment_root_value = NULL, + lock_time = 0, + relative_lock_time = 0 WHERE asset_id = $2 `