diff --git a/cmd/tapd-integrated/main.go b/cmd/tapd-integrated/main.go index de534d6e57..6fa61ee737 100644 --- a/cmd/tapd-integrated/main.go +++ b/cmd/tapd-integrated/main.go @@ -15,6 +15,7 @@ import ( "os" "path/filepath" "sync" + "time" "github.com/btcsuite/btclog/v2" "github.com/jessevdk/go-flags" @@ -257,6 +258,7 @@ func run() error { BlockUntilChainSynced: true, BlockUntilUnlocked: true, CallerCtx: ctx, + RPCTimeout: 2 * time.Minute, } if cfg.Lnd.NoMacaroons { // Use a dummy macaroon that lndclient can deserialize. diff --git a/go.mod b/go.mod index 79c2e29f7b..20bbfba2dd 100644 --- a/go.mod +++ b/go.mod @@ -22,8 +22,8 @@ require ( github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.0-rc.0 github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 - github.com/jackc/pgconn v1.14.3 github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438 + github.com/jackc/pgx/v5 v5.9.2 github.com/jessevdk/go-flags v1.6.1 github.com/lib/pq v1.10.9 github.com/lightninglabs/aperture v0.4.0 @@ -111,14 +111,11 @@ require ( github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/jackc/chunkreader/v2 v2.0.1 // indirect + github.com/jackc/pgconn v1.14.3 // indirect github.com/jackc/pgio v1.0.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgproto3/v2 v2.3.3 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect - github.com/jackc/pgtype v1.14.4 // indirect - github.com/jackc/pgx/v4 v4.18.3 // indirect - github.com/jackc/pgx/v5 v5.9.2 // indirect - github.com/jackc/puddle v1.3.0 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jackpal/gateway v1.0.5 // indirect github.com/jackpal/go-nat-pmp v0.0.0-20170405195558-28a68d0c24ad // indirect @@ -133,7 +130,7 @@ require ( github.com/lightninglabs/gozmq v0.0.0-20191113021534-d20a764486bf // indirect github.com/lightninglabs/lightning-node-connect/gbn v1.0.2-0.20250610182311-2f1d46ef18b7 // indirect github.com/lightninglabs/lightning-node-connect/mailbox v1.0.2-0.20250610182311-2f1d46ef18b7 // indirect - github.com/lightninglabs/neutrino v0.16.2 // indirect + github.com/lightninglabs/neutrino v0.16.3-0.20260508212153-0f87fa7c4b36 // indirect github.com/lightningnetwork/lightning-onion v1.3.0 // indirect github.com/lightningnetwork/lnd/actor v0.0.6 // indirect github.com/lightningnetwork/lnd/healthcheck v1.2.6 // indirect @@ -229,6 +226,28 @@ replace github.com/lightninglabs/taproot-assets/taprpc => ./taprpc // Needed for healthcheck import. replace github.com/prometheus/common => github.com/prometheus/common v0.26.0 +replace github.com/lightningnetwork/lnd => github.com/GeorgeTsagk/lnd v0.0.0-20260515101220-c025901edf46 + +replace github.com/lightningnetwork/lnd/sqldb => github.com/GeorgeTsagk/lnd/sqldb v0.0.0-20260515101220-c025901edf46 + +replace github.com/lightningnetwork/lnd/tlv => github.com/GeorgeTsagk/lnd/tlv v0.0.0-20260515101220-c025901edf46 + +replace github.com/lightningnetwork/lnd/fn/v2 => github.com/GeorgeTsagk/lnd/fn/v2 v2.0.0-20260515101220-c025901edf46 + +replace github.com/lightningnetwork/lnd/cert => github.com/GeorgeTsagk/lnd/cert v0.0.0-20260515101220-c025901edf46 + +replace github.com/lightningnetwork/lnd/clock => github.com/GeorgeTsagk/lnd/clock v0.0.0-20260515101220-c025901edf46 + +replace github.com/lightningnetwork/lnd/healthcheck => github.com/GeorgeTsagk/lnd/healthcheck v0.0.0-20260515101220-c025901edf46 + +replace github.com/lightningnetwork/lnd/kvdb => github.com/GeorgeTsagk/lnd/kvdb v0.0.0-20260515101220-c025901edf46 + +replace github.com/lightningnetwork/lnd/queue => github.com/GeorgeTsagk/lnd/queue v0.0.0-20260515101220-c025901edf46 + +replace github.com/lightningnetwork/lnd/tor => github.com/GeorgeTsagk/lnd/tor v0.0.0-20260515101220-c025901edf46 + +replace github.com/lightningnetwork/lnd/actor => github.com/GeorgeTsagk/lnd/actor v0.0.0-20260515101220-c025901edf46 + // Needed because lnd master requires a btcwallet version with neutrino // v0.16.2 context.Context support, but this pseudo-version is treated as a // pre-release by Go modules and would be overridden by the tagged v0.16.17 diff --git a/go.sum b/go.sum index 2a1c5734e3..e2507af3f3 100644 --- a/go.sum +++ b/go.sum @@ -603,10 +603,29 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03 github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/GeorgeTsagk/lnd v0.0.0-20260515101220-c025901edf46 h1:dLUpzLBHrOnUvGNTL5yPzPfbkJhq/x4tuV0A6pec/K0= +github.com/GeorgeTsagk/lnd v0.0.0-20260515101220-c025901edf46/go.mod h1:hG4elDX4kx7J967tswRksh2XAmPjPIJ9REToBlle7XU= +github.com/GeorgeTsagk/lnd/actor v0.0.0-20260515101220-c025901edf46 h1:91mX4BJLua5jBzJ+Wu9IktEGgClHliy7lfg0XY7UwnA= +github.com/GeorgeTsagk/lnd/actor v0.0.0-20260515101220-c025901edf46/go.mod h1:YAsoniSbY/cAM9HTVNfZLvt7RI6swDxy6wzPspTcMZg= +github.com/GeorgeTsagk/lnd/cert v0.0.0-20260515101220-c025901edf46 h1:Xl4GKmcXw+RIzbtEh9K0tNfc+4Huafctq6ThklVS6FQ= +github.com/GeorgeTsagk/lnd/cert v0.0.0-20260515101220-c025901edf46/go.mod h1:g0Vq5mXKRTVAWOyhJLNNMe8byqlUPXmIC5wzahbQzTQ= +github.com/GeorgeTsagk/lnd/clock v0.0.0-20260515101220-c025901edf46 h1:N/IEucdTDUquLLBB5olbTNv2c3aHEt1feNsdRq5fPgk= +github.com/GeorgeTsagk/lnd/clock v0.0.0-20260515101220-c025901edf46/go.mod h1:0sOYW0SNSFGX5tDIb+axdeu83gccfhl5WvC1+bZFCwc= +github.com/GeorgeTsagk/lnd/fn/v2 v2.0.0-20260515101220-c025901edf46 h1:7Sefwgdtc2pn+PmInRAu10O28FmvFW/MESPylENUDEw= +github.com/GeorgeTsagk/lnd/fn/v2 v2.0.0-20260515101220-c025901edf46/go.mod h1:hTqCE3YivAweJUguzTSco+ygOQ8m6NeFeLDdC57AZEs= +github.com/GeorgeTsagk/lnd/healthcheck v0.0.0-20260515101220-c025901edf46 h1:YaEQMfLdN6dJEZO2zrcONogXy1YSypXvgSKmBqtCsLY= +github.com/GeorgeTsagk/lnd/healthcheck v0.0.0-20260515101220-c025901edf46/go.mod h1:Tc9vHY5edyLPwR6us7wu1lKuZ8tCRGMvvlNHKkqxI8s= +github.com/GeorgeTsagk/lnd/kvdb v0.0.0-20260515101220-c025901edf46 h1:6tX5SzffC0R4enxY4GaKzcXuOwhxxTHXE6FvHPiAROs= +github.com/GeorgeTsagk/lnd/kvdb v0.0.0-20260515101220-c025901edf46/go.mod h1:2MA/W/JNuQXK9+mqTz0rFw2zl7mTJNVhbdDhYOmBcH0= +github.com/GeorgeTsagk/lnd/queue v0.0.0-20260515101220-c025901edf46 h1:2Ex1+vUxAOsAh+DQL7X9jlqpHfnqeqziDuQCITIeU1c= +github.com/GeorgeTsagk/lnd/queue v0.0.0-20260515101220-c025901edf46/go.mod h1:WGDUw3XZ+ttVtcrkaEYq+hSTfKna9if/vTdgyTkA+QE= +github.com/GeorgeTsagk/lnd/sqldb v0.0.0-20260515101220-c025901edf46 h1:SBBd0+XJma6gpnMSArFpf4rZpGTblmukMNFSpB2rqZE= +github.com/GeorgeTsagk/lnd/sqldb v0.0.0-20260515101220-c025901edf46/go.mod h1:PQvrB+2SlYuyAoe0ac/SUIMZ9rP73jYlanIN8O7/eA0= +github.com/GeorgeTsagk/lnd/tlv v0.0.0-20260515101220-c025901edf46 h1:YlxFEsPeDdjBpH6NiRcWsUOhYuwoQSjZ6+oESxeUoN8= +github.com/GeorgeTsagk/lnd/tlv v0.0.0-20260515101220-c025901edf46/go.mod h1:kPIYfi7ZGxilkM2LjODBKTb0oexX49OZIIB1ufX7sfA= +github.com/GeorgeTsagk/lnd/tor v0.0.0-20260515101220-c025901edf46 h1:dRtQOHSlCSZh6p5HsK+u2EI/uLkETIuo34/pIjmtEB0= +github.com/GeorgeTsagk/lnd/tor v0.0.0-20260515101220-c025901edf46/go.mod h1:DANQ6QCQBiR+CqrM+mKudgkMy6CR/RS4ALDbjb2W4FU= github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c/go.mod h1:X0CRv0ky0k6m906ixxpzmDRLvX58TFUKS2eePweuyxk= -github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= -github.com/Masterminds/semver/v3 v3.2.0 h1:3MEsd0SM6jqZojhjLWWeBY+Kcjy9i6MQAeY7YgDP83g= -github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/NebulousLabs/fastrand v0.0.0-20181203155948-6fb6489aac4e h1:n+DcnTNkQnHlwpsrHoQtkrJIO7CBx029fw6oR4vIob4= @@ -719,8 +738,6 @@ github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWH github.com/cncf/xds/go v0.0.0-20220314180256-7f1daf1720fc/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20230105202645-06c439db220b/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= -github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= github.com/cockroachdb/datadriven v1.0.2 h1:H9MtNqVoVhvd9nCBwOyDjUEdZCREqbIdCJD93PBm/jA= github.com/cockroachdb/datadriven v1.0.2/go.mod h1:a9RdTaap04u637JoCzcUoIcDmvwSUtcUFtT/C3kJlTU= github.com/coder/websocket v1.8.13 h1:f3QZdXy7uGVz+4uCJy2nTZyM0yTBj8yANEHhqlXZ9FE= @@ -733,8 +750,6 @@ github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151X github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= -github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= -github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf h1:iW4rZ826su+pqaw19uhpSCzhj44qo35pNgKFGqzDKkU= github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= @@ -742,7 +757,6 @@ github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSV github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= @@ -837,9 +851,6 @@ github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MG github.com/goccy/go-yaml v1.15.23 h1:WS0GAX1uNPDLUvLkNU2vXq6oTnsmfVFocjQ/4qA48qo= github.com/goccy/go-yaml v1.15.23/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= -github.com/gofrs/uuid v4.2.0+incompatible h1:yyYWMnhkhrKwwr8gAOcOCYxOOscHgDS9yZgBrnJfGa0= -github.com/gofrs/uuid v4.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= @@ -977,62 +988,25 @@ github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpO github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo= github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8= github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= -github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA= -github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE= -github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s= -github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o= -github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY= -github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI= github.com/jackc/pgconn v1.14.3 h1:bVoTr12EGANZz66nZPkMInAV/KHD2TxH9npjXXgiB3w= github.com/jackc/pgconn v1.14.3/go.mod h1:RZbme4uasqzybK2RK5c65VsHxoyaml09lx3tXOcO/VM= github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438 h1:Dj0L5fhJ9F82ZJyVOmBx6msDp/kfd1t9GRfny/mfJA0= github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438/go.mod h1:a/s9Lp5W7n/DD0VrVoyJ00FbP2ytTPDVOivvn2bMlds= github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE= github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8= -github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE= -github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c= github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65 h1:DadwsjnMwFjfWc9y5Wi/+Zz7xoE5ALHsRQlOctkOiHc= github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= -github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78= -github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA= -github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg= -github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= -github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= -github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= -github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= github.com/jackc/pgproto3/v2 v2.3.3 h1:1HLSx5H+tXR9pW3in3zaztoEwQYRC9SQaYUHjTSUOag= github.com/jackc/pgproto3/v2 v2.3.3/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= -github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= -github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= -github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg= -github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc= -github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw= -github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM= -github.com/jackc/pgtype v1.14.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4= -github.com/jackc/pgtype v1.14.4 h1:fKuNiCumbKTAIxQwXfB/nsrnkEI6bPJrrSiMKgbJ2j8= -github.com/jackc/pgtype v1.14.4/go.mod h1:aKeozOde08iifGosdJpz9MBZonJOUJxqNpPBcMJTlVA= -github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y= -github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM= -github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc= -github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs= -github.com/jackc/pgx/v4 v4.18.2/go.mod h1:Ey4Oru5tH5sB6tV7hDmfWFahwF15Eb7DNXlRKx2CkVw= -github.com/jackc/pgx/v4 v4.18.3 h1:dE2/TrEsGX3RBprb3qryqSV9Y60iZN1C6i8IrmW9/BA= -github.com/jackc/pgx/v4 v4.18.3/go.mod h1:Ey4Oru5tH5sB6tV7hDmfWFahwF15Eb7DNXlRKx2CkVw= github.com/jackc/pgx/v5 v5.9.2 h1:3ZhOzMWnR4yJ+RW1XImIPsD1aNSz4T4fyP7zlQb56hw= github.com/jackc/pgx/v5 v5.9.2/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4= -github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= -github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= -github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= -github.com/jackc/puddle v1.3.0 h1:eHK/5clGOatcjX3oWGBO/MpxpbHzSwud5EWTSCI+MX0= -github.com/jackc/puddle v1.3.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jackpal/gateway v1.0.5 h1:qzXWUJfuMdlLMtt0a3Dgt+xkWQiA5itDEITVJtuSwMc= @@ -1082,7 +1056,6 @@ github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa02 github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= @@ -1091,14 +1064,9 @@ github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NB github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/libdns/libdns v0.2.1 h1:Wu59T7wSHRgtA0cfxC+n1c/e+O3upJGWytknkmFEDis= @@ -1117,38 +1085,16 @@ github.com/lightninglabs/lndclient v0.20.0-6 h1:sh23eZkOpHxe39c4QRYwhsM7qbnJlS++ github.com/lightninglabs/lndclient v0.20.0-6/go.mod h1:gBtIFPGmC2xIspGIv/G5+HiPSGJsFD8uIow7Oke1HFI= github.com/lightninglabs/migrate/v4 v4.18.2-9023d66a-fork-pr-2.0.20251211093704-71c1eef09789 h1:7kX7vUgHUazAHcCJ6uzBDa4/2MEGEbMEfa01GtfqmTQ= github.com/lightninglabs/migrate/v4 v4.18.2-9023d66a-fork-pr-2.0.20251211093704-71c1eef09789/go.mod h1:99BKpIi6ruaaXRM1A77eqZ+FWPQ3cfRa+ZVy5bmWMaY= -github.com/lightninglabs/neutrino v0.16.2 h1:jHMMDLPX8asfwgN0/C4BY8uVaYupFzZYuWQkX8Go3fk= -github.com/lightninglabs/neutrino v0.16.2/go.mod h1:fNjnbuSPw4lRsVAzvjC1JG7IE7rqae/mbek2tNkN/Dw= +github.com/lightninglabs/neutrino v0.16.3-0.20260508212153-0f87fa7c4b36 h1:d6FuJQ6YjWqBdMJ3fmk9BgjyMFyRKecKxoGq//PHdh0= +github.com/lightninglabs/neutrino v0.16.3-0.20260508212153-0f87fa7c4b36/go.mod h1:fNjnbuSPw4lRsVAzvjC1JG7IE7rqae/mbek2tNkN/Dw= github.com/lightninglabs/neutrino/cache v1.1.3 h1:rgnabC41W+XaPuBTQrdeFjFCCAVKh1yctAgmb3Se9zA= github.com/lightninglabs/neutrino/cache v1.1.3/go.mod h1:qxkJb+pUxR5p84jl5uIGFCR4dGdFkhNUwMSxw3EUWls= github.com/lightninglabs/protobuf-go-hex-display v1.34.2-hex-display h1:w7FM5LH9Z6CpKxl13mS48idsu6F+cEZf0lkyiV+Dq9g= github.com/lightninglabs/protobuf-go-hex-display v1.34.2-hex-display/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= github.com/lightningnetwork/lightning-onion v1.3.0 h1:FqILgHjD6euc/Muo1VOzZ4+XDPuFnw6EYROBq0rR/5c= github.com/lightningnetwork/lightning-onion v1.3.0/go.mod h1:nP85zMHG7c0si/eHBbSQpuDCtnIXfSvFrK3tW6YWzmU= -github.com/lightningnetwork/lnd v0.20.0-beta.rc4.0.20260421084739-a8a3e13120eb h1:qhzjbUJau0bZCUAGlJwjxKLHI0337Z0KR+37aFoLhbg= -github.com/lightningnetwork/lnd v0.20.0-beta.rc4.0.20260421084739-a8a3e13120eb/go.mod h1:hrJPOxkleTu3y3K32ehBziq8y/zqQqCPQKfwktS35aw= -github.com/lightningnetwork/lnd/actor v0.0.6 h1:Ge8N2wivARG+27qJBwTlB0vwsypStZYZy8vk4Zl38sU= -github.com/lightningnetwork/lnd/actor v0.0.6/go.mod h1:YAsoniSbY/cAM9HTVNfZLvt7RI6swDxy6wzPspTcMZg= -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= -github.com/lightningnetwork/lnd/clock v1.1.1/go.mod h1:mGnAhPyjYZQJmebS7aevElXKTFDuO+uNFFfMXK1W8xQ= -github.com/lightningnetwork/lnd/fn/v2 v2.0.9 h1:ZytG4ltPac/sCyg1EJDn10RGzPIDJeyennUMRdOw7Y8= -github.com/lightningnetwork/lnd/fn/v2 v2.0.9/go.mod h1:aPUJHJ31S+Lgoo8I5SxDIjnmeCifqujaiTXKZqpav3w= -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.16 h1:9BZgWdDfjmHRHLS97cz39bVuBAqMc4/p3HX1xtUdbDI= -github.com/lightningnetwork/lnd/kvdb v1.4.16/go.mod h1:HW+bvwkxNaopkz3oIgBV6NEnV4jCEZCACFUcNg4xSjM= -github.com/lightningnetwork/lnd/queue v1.1.2-0.20260328114253-ea8a6657729e h1:DTDPwSfhAi6HYmk4oAMRyvJy5bfwWwxzbsDWYvolQFQ= -github.com/lightningnetwork/lnd/queue v1.1.2-0.20260328114253-ea8a6657729e/go.mod h1:WGDUw3XZ+ttVtcrkaEYq+hSTfKna9if/vTdgyTkA+QE= -github.com/lightningnetwork/lnd/sqldb v1.0.13-0.20260410061304-0b82a89fdae1 h1:wlzpC1pDoeNs1kqBQ7Dqv9LXz6krsqfqPZLGHdTeAIo= -github.com/lightningnetwork/lnd/sqldb v1.0.13-0.20260410061304-0b82a89fdae1/go.mod h1:XaG3d8AR7/e6+HUw5jvNvm+gs6MowB+iE9myFH8Rc14= github.com/lightningnetwork/lnd/ticker v1.1.1 h1:J/b6N2hibFtC7JLV77ULQp++QLtCwT6ijJlbdiZFbSM= github.com/lightningnetwork/lnd/ticker v1.1.1/go.mod h1:waPTRAAcwtu7Ji3+3k+u/xH5GHovTsCoSVpho0KDvdA= -github.com/lightningnetwork/lnd/tlv v1.3.2 h1:MO4FCk7F4k5xPMqVZF6Nb/kOpxlwPrUQpYjmyKny5s0= -github.com/lightningnetwork/lnd/tlv v1.3.2/go.mod h1:pJuiBj1ecr1WWLOtcZ+2+hu9Ey25aJWFIsjmAoPPnmc= -github.com/lightningnetwork/lnd/tor v1.1.6 h1:WHUumk7WgU6BUFsqHuqszI9P6nfhMeIG+rjJBlVE6OE= -github.com/lightningnetwork/lnd/tor v1.1.6/go.mod h1:qSRB8llhAK+a6kaTPWOLLXSZc6Hg8ZC0mq1sUQ/8JfI= github.com/ltcsuite/ltcd v0.0.0-20190101042124-f37f8bf35796 h1:sjOGyegMIhvgfq5oaue6Td+hxZuf3tDC8lAPrFldqFw= github.com/ltcsuite/ltcd v0.0.0-20190101042124-f37f8bf35796/go.mod h1:3p7ZTf9V1sNPI5H8P3NkTFF4LuwMdPl2DodF60qAKqY= github.com/ltcsuite/ltcutil v0.0.0-20181217130922-17f3b04680b6/go.mod h1:8Vg/LTOO0KYa/vlHWJ6XZAevPQThGH5sufO0Hrou/lA= @@ -1157,11 +1103,7 @@ github.com/lyft/protoc-gen-star v0.6.0/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuz github.com/lyft/protoc-gen-star v0.6.1/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA= github.com/lyft/protoc-gen-star/v2 v2.0.1/go.mod h1:RcCdONR2ScXaYnQC5tUzxzlpA3WVYF7/opLeUgcQs/o= github.com/mattn/go-colorable v0.0.6/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= -github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= -github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-isatty v0.0.0-20160806122752-66b8e73f3f5c/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= -github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= @@ -1257,19 +1199,10 @@ github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTE github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= -github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= -github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= -github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w= github.com/ruudk/golang-pdf417 v0.0.0-20201230142125-a7e3863a1245/go.mod h1:pQAZKsJ8yyVxGRWYNEm9oFB8ieLgKFnamEyDmSA0BRk= -github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= -github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= -github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= -github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= -github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= -github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= @@ -1284,7 +1217,6 @@ github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= @@ -1329,7 +1261,6 @@ github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1 github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= -github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec h1:FpfFs4EhNehiVfzQttTuxanPIT43FtkkCFypIod8LHo= gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec/go.mod h1:BZ1RAoRPbCxum9Grlv5aeksu2H8BiKehBYooU2LFiOQ= go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo= @@ -1381,10 +1312,6 @@ go.opentelemetry.io/proto/otlp v0.15.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI go.opentelemetry.io/proto/otlp v0.19.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U= go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= -go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= -go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= -go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= -go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ= go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= @@ -1392,15 +1319,8 @@ go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= -go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= -go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= -go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= -go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= -go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= -go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= -go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= go.uber.org/zap v1.18.1/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw= go.uber.org/zap v1.23.0 h1:OjGQ5KQDEUawVHxNwQgPpiypGHOxo2mNZsOqTak4fFY= @@ -1410,20 +1330,14 @@ go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20180723164146-c126467f60eb/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= -golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= -golang.org/x/crypto v0.20.0/go.mod h1:Xwo95rrVNIoSMx9wa1JroENMToLWn3RNVrTBpLHgZPQ= golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -1501,7 +1415,6 @@ golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -1554,8 +1467,6 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= -golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -1607,12 +1518,9 @@ golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1621,11 +1529,9 @@ golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190804053845-51ab0e2deafa/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1702,11 +1608,8 @@ golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= @@ -1715,8 +1618,6 @@ golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= -golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= -golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU= golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -1735,7 +1636,6 @@ golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -1755,19 +1655,15 @@ golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3 golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190927191325-030b2cf1153e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= @@ -1776,7 +1672,6 @@ golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= @@ -1818,8 +1713,6 @@ golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA= golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc= -golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -2099,7 +1992,6 @@ gopkg.in/errgo.v1 v1.0.1 h1:oQFRXzZ7CkBGdm1XZm/EbQYaYNNEElNBOd09M6cqNso= gopkg.in/errgo.v1 v1.0.1/go.mod h1:3NjfXwocQRYAPTq4/fzX+CwUhPRcR/azYRhj8G+LqMo= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= -gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= gopkg.in/macaroon-bakery.v2 v2.1.0 h1:9Jw/+9XHBSutkaeVpWhDx38IcSNLJwWUICkOK98DHls= gopkg.in/macaroon-bakery.v2 v2.1.0/go.mod h1:B4/T17l+ZWGwxFSZQmlBwp25x+og7OkhETfr3S9MbIA= gopkg.in/macaroon.v2 v2.1.0 h1:HZcsjBCzq9t0eBPMKqTN/uSN6JOm78ZJ2INbqcBQOUI= diff --git a/itest/custom_channels/assertions.go b/itest/custom_channels/assertions.go index 6b1d5af217..b48c294f67 100644 --- a/itest/custom_channels/assertions.go +++ b/itest/custom_channels/assertions.go @@ -305,7 +305,7 @@ func mineBlocksSlow(t *ccHarnessTest, net *itest.IntegratedNetworkHarness, var txids []*chainhash.Hash var err error if numTxs > 0 { - txids, err = waitForNTxsInMempool( + txids, err = waitForAtLeastNTxsInMempool( net.Miner, numTxs, wait.MinerMempoolTimeout, ) @@ -367,8 +367,8 @@ func waitForNTxsInMempool(m *miner.HarnessMiner, n int, } } -// waitForAtLeastNTxsInMempool polls until finding at least n transactions -// in the miner's mempool, returning all txids present. +// waitForAtLeastNTxsInMempool polls until finding at least n +// transactions in the miner's mempool, returning all txids present. func waitForAtLeastNTxsInMempool(m *miner.HarnessMiner, n int, timeout time.Duration) ([]*chainhash.Hash, error) { @@ -388,7 +388,8 @@ func waitForAtLeastNTxsInMempool(m *miner.HarnessMiner, n int, if len(mempool) >= n { result := make( - []*chainhash.Hash, len(mempool), + []*chainhash.Hash, + len(mempool), ) for i := range mempool { result[i] = &mempool[i] @@ -399,6 +400,14 @@ func waitForAtLeastNTxsInMempool(m *miner.HarnessMiner, n int, } } +// waitForNonEmptyMempool polls until the mempool is non-empty, then +// returns a snapshot of its current contents. +func waitForNonEmptyMempool(m *miner.HarnessMiner, + timeout time.Duration) ([]*chainhash.Hash, error) { + + return waitForAtLeastNTxsInMempool(m, 1, timeout) +} + // assertTxInBlock asserts that a transaction with the given hash is included // in the block. func assertTxInBlock(t *ccHarnessTest, block *wire.MsgBlock, diff --git a/itest/custom_channels/breach_test.go b/itest/custom_channels/breach_test.go index 4c41a63044..e3ef1303df 100644 --- a/itest/custom_channels/breach_test.go +++ b/itest/custom_channels/breach_test.go @@ -4,11 +4,15 @@ package custom_channels import ( "context" + "encoding/hex" "fmt" + "os" "slices" + "strconv" "time" "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/wire" "github.com/lightninglabs/taproot-assets/itest" "github.com/lightninglabs/taproot-assets/proof" "github.com/lightninglabs/taproot-assets/taprpc" @@ -17,17 +21,85 @@ import ( "github.com/lightninglabs/taproot-assets/tapscript" fn "github.com/lightningnetwork/lnd/fn/v2" "github.com/lightningnetwork/lnd/lnrpc" + "github.com/lightningnetwork/lnd/lnrpc/invoicesrpc" "github.com/lightningnetwork/lnd/lntest/node" "github.com/lightningnetwork/lnd/lntest/port" + "github.com/lightningnetwork/lnd/lntest/wait" "github.com/lightningnetwork/lnd/lnwallet/chainfee" "github.com/stretchr/testify/require" ) +const breachNumHodlInvoicesEnv = "TAPD_BREACH_HODL_INVOICES_PER_SIDE" + +func numBreachHodlInvoices(t *ccHarnessTest) int { + // Default to the most demanding scenario the breach recovery path + // currently supports, so the test exercises the full pre-signed + // second-level HTLC flow out of the box. The env var still allows + // scaling the same breach flow down or up without editing the test + // body. + raw := os.Getenv(breachNumHodlInvoicesEnv) + if raw == "" { + return 6 + } + + numInvoices, err := strconv.Atoi(raw) + require.NoErrorf( + t.t, err, "invalid %s value %q", breachNumHodlInvoicesEnv, raw, + ) + require.GreaterOrEqualf( + t.t, numInvoices, 1, "%s must be at least 1", + breachNumHodlInvoicesEnv, + ) + + return numInvoices +} + +func currentAssetBalance(node *itest.IntegratedNode, assetID []byte) uint64 { + resp, err := asTapd(node).ListBalances( + context.Background(), &taprpc.ListBalancesRequest{ + GroupBy: &taprpc.ListBalancesRequest_AssetId{ + AssetId: true, + }, + AssetFilter: assetID, + }, + ) + if err != nil { + return 0 + } + + balance, ok := resp.AssetBalances[hex.EncodeToString(assetID)] + if !ok { + return 0 + } + + return balance.Balance +} + +func logRecentTransfers(t *ccHarnessTest, node *itest.IntegratedNode) { + resp, err := asTapd(node).ListTransfers( + context.Background(), &taprpc.ListTransfersRequest{}, + ) + require.NoError(t.t, err) + + start := max(0, len(resp.Transfers)-8) + t.t.Logf("%s recent transfers (%d total):", node.Cfg.Name, + len(resp.Transfers)) + for i := start; i < len(resp.Transfers); i++ { + tr := resp.Transfers[i] + t.t.Logf(" [%d] label=%q height_hint=%d block_height=%d "+ + "outputs=%d", + i, tr.Label, tr.AnchorTxHeightHint, + tr.AnchorTxBlockHeight, len(tr.Outputs)) + } +} + // testCustomChannelsBreach tests the breach/justice scenario for custom -// channels. Dave backs up his DB state, one more payment advances the state, -// then Dave restores the old state and force-closes (broadcasting a revoked -// commitment). Charlie detects the breach and sweeps both outputs, recovering -// all channel funds. +// channels with in-flight HTLCs. Dave backs up his DB state (which has active +// hodl invoice HTLCs), the HTLCs are settled to advance the state, then Dave +// restores the old state and force-closes (broadcasting a revoked commitment +// with HTLC outputs). Charlie is suspended during the breach so Dave can +// advance HTLCs to second level. Charlie is then resumed, detects the breach, +// and sweeps all outputs including second-level HTLC outputs. func testCustomChannelsBreach(ctx context.Context, net *itest.IntegratedNetworkHarness, t *ccHarnessTest) { @@ -37,16 +109,26 @@ func testCustomChannelsBreach(ctx context.Context, net.FeeService.SetFeeRate(chainfee.SatPerKWeight(1000), 1) lndArgs := slices.Clone(lndArgsTemplate) + lndArgs = append(lndArgs, "--bitcoin.defaultremotedelay=144") tapdArgs := slices.Clone(tapdArgsTemplate) - // We use Charlie as the proof courier. But in order for Charlie to - // also use itself, we need to define its port upfront. - charliePort := port.NextAvailablePort() + // Use Zane as a dedicated universe node that stays online as proof + // courier, so that Dave can still import proofs and advance HTLCs + // to second level even when Charlie is suspended. + // + // We allocate a port for Zane's RPC upfront and add it as an extra + // --rpclisten so the proof courier address is known before Zane + // starts (same pattern as core_test.go). + zanePort := port.NextAvailablePort() + zaneLndArgs := append(slices.Clone(lndArgs), fmt.Sprintf( + "--rpclisten=127.0.0.1:%d", zanePort, + )) tapdArgs = append(tapdArgs, fmt.Sprintf( "--proofcourieraddr=%s://%s", proof.UniverseRpcCourierType, - fmt.Sprintf(node.ListenerFormat, charliePort), + fmt.Sprintf(node.ListenerFormat, zanePort), )) + zane := net.NewNode("Zane", zaneLndArgs, tapdArgs) // Charlie will be the breached party. We set --nolisten to ensure // Dave won't be able to connect to him and trigger the channel @@ -56,9 +138,6 @@ func testCustomChannelsBreach(ctx context.Context, charlieLndArgs := append( slices.Clone(lndArgs), "--nolisten", "--minbackoff=1h", ) - charlieLndArgs = append(charlieLndArgs, fmt.Sprintf( - "--rpclisten=127.0.0.1:%d", charliePort, - )) // For this simple test, we'll just have Charlie -> Dave as an assets // channel. @@ -71,6 +150,11 @@ func testCustomChannelsBreach(ctx context.Context, connectAllNodes(t.t, net, nodes) fundAllNodes(t.t, net, nodes) + // Connect Zane to Dave directly, and have Charlie connect outbound + // to Zane (since Charlie has --nolisten and can't accept inbound). + net.EnsureConnected(t.t, zane, dave) + net.EnsureConnected(t.t, charlie, zane) + // Now we'll make an asset for Charlie that we'll use in the test to // open a channel. mintedAssets := itest.MintAssetsConfirmBatch( @@ -85,7 +169,7 @@ func testCustomChannelsBreach(ctx context.Context, assetID := cents.AssetGenesis.AssetId t.Logf("Minted %d lightning cents, syncing universes...", cents.Amount) - syncUniverses(t.t, charlie, dave) + syncUniverses(t.t, charlie, zane, dave) t.Logf("Universes synced between all nodes, distributing assets...") // Next we can open an asset channel from Charlie -> Dave, then kick @@ -119,12 +203,12 @@ func testCustomChannelsBreach(ctx context.Context, ) // Make sure that Charlie properly uploaded funding proof to the - // Universe server. + // Universe server (Zane is the proof courier). fundingScriptTree := tapscript.NewChannelFundingScriptTree() fundingScriptKey := fundingScriptTree.TaprootKey fundingScriptTreeBytes := fundingScriptKey.SerializeCompressed() assertUniverseProofExists( - t.t, charlie, assetID, nil, fundingScriptTreeBytes, + t.t, zane, assetID, nil, fundingScriptTreeBytes, fmt.Sprintf( "%v:%v", assetFundResp.Txid, assetFundResp.OutputIndex, @@ -141,33 +225,138 @@ func testCustomChannelsBreach(ctx context.Context, require.NoError(t.t, net.AssertNodeKnown(charlie, dave)) require.NoError(t.t, net.AssertNodeKnown(dave, charlie)) - // Next, we'll make keysend payments from Charlie to Dave. We'll use - // this to reach a state where both parties have funds in the channel. + numHodlInvoices := numBreachHodlInvoices(t) + + // Next, we'll make keysend payments from Charlie to Dave. We scale + // the number of rebalancing payments with the requested HTLC count so + // Dave has enough BTC to carry the higher-count in-flight HTLC set on + // his local commitment without falling below reserve/fee constraints. + // Without this, higher-count variants can fail before the breach with + // one HTLC missing from the intended revoked state. const ( - numPayments = 5 - keySendAmount = 100 - btcAmt = int64(5_000) + keySendAmount = 200 + btcAmt = int64(10_000) ) - for i := 0; i < numPayments; i++ { + numBalancePayments := max(5, numHodlInvoices) + for i := 0; i < numBalancePayments; i++ { sendAssetKeySendPayment( t.t, charlie, dave, keySendAmount, assetID, fn.Some(btcAmt), ) } - logBalance(t.t, nodes, assetID, "after keysend -- breach state") + logBalance(t.t, nodes, assetID, "after keysend -- balanced state") + + // Now create hodl invoices on both sides to ensure HTLCs exist on + // the commitment we're about to backup. This tests the revoked HTLC + // sweep paths (TaprootHtlcOfferedRevoke, TaprootHtlcAcceptedRevoke). + const ( + htlcAmount = 200 + ) + var ( + daveHodlInvoices []assetHodlInvoice + charlieHodlInvoices []assetHodlInvoice + ) + + t.Logf("Creating %d hodl invoices per peer...", numHodlInvoices) + + // Use a shorter invoice expiry so that the RFQ quote's 5-minute + // lifetime is always sufficient, even when creating many invoices. + shortExpiry := withInvoiceExpiry(120) + + // Create Dave's hodl invoices (Charlie pays = outgoing HTLCs). + for i := 0; i < numHodlInvoices; i++ { + daveHodlInvoices = append( + daveHodlInvoices, createAssetHodlInvoice( + t.t, charlie, dave, htlcAmount, assetID, + shortExpiry, + ), + ) + } + + // Create Charlie's hodl invoices (Dave pays = incoming HTLCs). + for i := 0; i < numHodlInvoices; i++ { + charlieHodlInvoices = append( + charlieHodlInvoices, createAssetHodlInvoice( + t.t, dave, charlie, htlcAmount, assetID, + shortExpiry, + ), + ) + } + + // Pay all invoices but don't settle (HTLCs stay in flight). + payOpt := withFailure( + lnrpc.Payment_IN_FLIGHT, + lnrpc.PaymentFailureReason_FAILURE_REASON_NONE, + ) + + t.Logf("Paying hodl invoices to create HTLCs on commitment...") + + for _, daveInv := range daveHodlInvoices { + payInvoiceWithAssets( + t.t, charlie, dave, daveInv.payReq, assetID, payOpt, + ) + } + + for _, charlieInv := range charlieHodlInvoices { + payInvoiceWithAssets( + t.t, dave, charlie, charlieInv.payReq, assetID, payOpt, + ) + } + + // Verify HTLCs are active on both sides. + expectedHtlcs := numHodlInvoices * 2 + assertNumHtlcs(t.t, charlie, expectedHtlcs) + assertNumHtlcs(t.t, dave, expectedHtlcs) + + logBalance(t.t, nodes, assetID, "after hodl invoices -- breach state") // Now we'll create an on disk snapshot that we'll use to restore - // back to as our breached state. + // back to as our breached state. This state has active HTLCs! require.NoError(t.t, net.StopAndBackupDB(dave)) connectAllNodes(t.t, net, nodes) - // We'll send one more keysend payment now to revoke the state we - // were just at above. + // Settle all the hodl invoices to revoke the state with HTLCs. + // This will cause the backed-up state to become revoked, which + // will trigger the breach detection when Dave broadcasts it. + t.Logf("Settling hodl invoices to revoke breach state...") + + for _, daveInv := range daveHodlInvoices { + _, err := dave.InvoicesClient.SettleInvoice( + ctx, &invoicesrpc.SettleInvoiceMsg{ + Preimage: daveInv.preimage[:], + }, + ) + require.NoError(t.t, err) + } + + for _, charlieInv := range charlieHodlInvoices { + _, err := charlie.InvoicesClient.SettleInvoice( + ctx, &invoicesrpc.SettleInvoiceMsg{ + Preimage: charlieInv.preimage[:], + }, + ) + require.NoError(t.t, err) + } + + // Wait for all settled HTLCs to clear from the channel state before we + // send one more keysend. At higher HTLC counts the settlement wave can + // lag the immediate post-settle payment and leave the channel balance + // temporarily unavailable for another asset payment, making the setup + // fail for reasons unrelated to the breach-recovery logic. + assertNumHtlcs(t.t, charlie, 0) + assertNumHtlcs(t.t, dave, 0) + + // Send one more keysend to ensure the state with settled HTLCs is + // committed and the previous state (with active HTLCs) is revoked. sendAssetKeySendPayment( t.t, charlie, dave, keySendAmount, assetID, fn.Some(btcAmt), ) - logBalance(t.t, nodes, assetID, "after keysend -- final state") + + assertNumHtlcs(t.t, charlie, 0) + assertNumHtlcs(t.t, dave, 0) + + logBalance(t.t, nodes, assetID, "after settling HTLCs -- final state") // With the final state achieved, we'll now restore Dave (who will // be force closing) to that old state, the breach state. @@ -181,43 +370,273 @@ func testCustomChannelsBreach(ctx context.Context, FundingTxidStr: assetFundResp.Txid, }, } + + // Suspend Charlie BEFORE the breach so Dave can advance HTLCs to + // second level without Charlie's justice tx interfering. + t.Logf("Suspending Charlie before breach...") + restartCharlie, err := net.SuspendNode(charlie) + require.NoError(t.t, err) + _, breachTxid, err := net.CloseChannel(dave, daveChanPoint, true) require.NoError(t.t, err) t.Logf("Channel closed! Mining blocks, close_txid=%v", breachTxid) - // Next, we'll mine a block to confirm the breach transaction. + // Mine a block to confirm the breach transaction. mineBlocks(t, net, 1, 1) - // We should be able to find the transfer of the breach for both - // parties. - locateAssetTransfers(t.t, charlie, *breachTxid) - locateAssetTransfers(t.t, dave, *breachTxid) + // Mine blocks to let Dave's HTLC timeout resolvers advance HTLCs to + // second level. CLTV delta is 80, so we need ~100 blocks. We must + // NOT mine enough for the CSV delay (144) on second-level outputs + // to expire, or Dave will sweep them before Charlie can. + // + // We mine in small batches and check the mempool between batches to + // give Dave's sweeper time to broadcast second-level HTLC txs. + t.Logf("Mining blocks to let Dave advance HTLCs to 2nd level...") + var secondLevelTxns []*wire.MsgTx + breachHash := breachTxid + const ( + totalBlocks = 100 + batchSize = 10 + ) + var allBlocks []*wire.MsgBlock + for mined := uint32(0); mined < totalBlocks; { + // Check mempool before mining the next batch. + mempool := net.Miner.GetRawMempool() + if len(mempool) > 0 { + t.Logf("Mempool has %d txns at height offset %d", + len(mempool), mined) + for _, txid := range mempool { + rawTx := net.Miner.GetRawTransaction(txid) + tx := rawTx.MsgTx() + for _, txIn := range tx.TxIn { + if txIn.PreviousOutPoint.Hash == + *breachHash { + + t.Logf("Found 2nd-level tx "+ + "%v in mempool "+ + "spending breach "+ + "output %d", + tx.TxHash(), + txIn.PreviousOutPoint.Index) + } + } + } + } + + // Mine a batch, including any mempool txs in first block. + n := batchSize + if mined+uint32(n) > totalBlocks { + n = int(totalBlocks - mined) + } + blocks := mineBlocks(t, net, uint32(n), len(mempool)) + allBlocks = append(allBlocks, blocks...) + mined += uint32(n) + + t.Logf("Mined %d/%d blocks", mined, totalBlocks) + } + + // Scan all mined blocks for second-level txns (txs spending from + // the breach transaction). + for _, block := range allBlocks { + for _, tx := range block.Transactions { + for _, txIn := range tx.TxIn { + if txIn.PreviousOutPoint.Hash == *breachHash { + t.Logf("Found 2nd-level tx %v "+ + "spending breach output %d", + tx.TxHash(), + txIn.PreviousOutPoint.Index) + + secondLevelTxns = append( + secondLevelTxns, tx, + ) + } + } + } + } + t.Logf("Found %d second-level txns total", len(secondLevelTxns)) + require.NotEmpty(t.t, secondLevelTxns, + "expected second-level HTLC transactions") + + // Log the breach tx outputs for reference. + breachTx := net.Miner.GetRawTransaction(*breachTxid) + for i, out := range breachTx.MsgTx().TxOut { + t.Logf("Breach output %d: value=%d pkscript=%x", + i, out.Value, out.PkScript) + } - // With the breach transaction mined, Charlie should now have a - // transaction in the mempool sweeping *both* commitment outputs. - // We use a generous timeout because Charlie needs to process the - // block, detect the breach, and construct the justice transaction. - charlieJusticeTxid, err := waitForNTxsInMempool( - net.Miner, 1, time.Second*30, + // Log second-level tx details. + for i, tx := range secondLevelTxns { + for j, out := range tx.TxOut { + t.Logf("2nd-level tx %d output %d: value=%d "+ + "pkscript=%x", i, j, out.Value, out.PkScript) + } + } + + // Now resume Charlie. She should detect the breach and attempt + // justice, including sweeping any second-level HTLC outputs. + t.Logf("Resuming Charlie...") + restartCharlie() + + // Wait for Charlie's justice tx(s) in the mempool. The breach + // arbitrator can publish a variable number of justice variants here + // depending on how many HTLCs have already moved to second level, and + // it may replace them while converging on the final set. Wait for the + // mempool to become non-empty, then mine whatever set is present. + t.Logf("Waiting for Charlie's justice txns in mempool...") + charlieJusticeTxids, err := waitForNonEmptyMempool( + net.Miner, wait.MinerMempoolTimeout, + ) + require.NoError(t.t, err, + "expected Charlie's justice tx(s) in mempool") + + t.Logf("Charlie justice txids: %v", charlieJusticeTxids) + + // Log justice tx details. The BRAR may replace txs between our + // mempool query and the GetRawTransaction call, so tolerate errors. + for _, txid := range charlieJusticeTxids { + justiceTx := net.Miner.GetRawTransaction(*txid) + t.Logf("Justice tx %v has %d inputs:", + txid, len(justiceTx.MsgTx().TxIn)) + for _, txIn := range justiceTx.MsgTx().TxIn { + t.Logf(" input: %v (witness len=%d)", + txIn.PreviousOutPoint, len(txIn.Witness)) + } + for i, out := range justiceTx.MsgTx().TxOut { + t.Logf(" output %d: value=%d", i, out.Value) + } + } + + // Mine the justice transaction(s). The breach arbiter may create + // multiple justice tx variants (spendAll, split variants, and + // individual second-level sweeps). Poll the mempool briefly in + // case more txs arrive, then mine a block. + mp := net.Miner.GetRawMempool() + t.Logf("Mempool has %d txs before mining justice block", + len(mp)) + mineBlocks(t, net, 1, len(mp)) + t.Logf("Justice tx confirmed") + + // After the first justice tx confirms, the breach arbiter may detect + // that some outputs were spent to second level and create follow-up + // justice txs. Give it a few blocks to react. + t.Logf("Mining blocks to let breach arbiter react to " + + "second-level...") + for i := 0; i < 10; i++ { + // Give the breach arbiter time to detect spends, + // morph inputs, and broadcast new justice tx variants. + time.Sleep(2 * time.Second) + + mp := net.Miner.GetRawMempool() + + if len(mp) > 0 { + t.Logf("Found %d txns in mempool after %d blocks:", + len(mp), i) + for _, txid := range mp { + raw := net.Miner.GetRawTransaction(txid) + tx := raw.MsgTx() + t.Logf(" tx %v: %d inputs, %d outputs", + txid, len(tx.TxIn), len(tx.TxOut)) + for _, in := range tx.TxIn { + t.Logf(" input: %v "+ + "(witness len=%d)", + in.PreviousOutPoint, + len(in.Witness)) + } + for j, out := range tx.TxOut { + t.Logf(" output %d: value=%d", + j, out.Value) + } + } + + mineBlocks(t, net, 1, len(mp)) + t.Logf("Mined second-level justice tx") + } else { + mineBlocks(t, net, 1, 0) + } + } + + // Mine a few more blocks to trigger the porter's confirmation + // detection for all justice sweep transfers. The porter processes + // transfers sequentially and needs block notifications to complete + // the historical confirmation scan. + mineBlocks(t, net, 3, 0) + + // After sweeping, Charlie should have all the asset balance back. + // For larger HTLC counts the porter can need significantly more + // block notifications to reconcile all confirmed justice sweeps. Poll + // balance directly here so the stress variants fail with useful + // transfer-state logs instead of a generic final-balance mismatch. + var balanceChecks int + require.Eventually(t.t, func() bool { + balanceChecks++ + currentBalance := currentAssetBalance(charlie, assetID) + if currentBalance == + ccItestAsset.Amount { + + return true + } + + if balanceChecks == 1 || balanceChecks%10 == 0 { + t.Logf("Charlie recovered balance still pending: got=%d "+ + "want=%d after %d checks", currentBalance, + ccItestAsset.Amount, balanceChecks) + logRecentTransfers(t, charlie) + } + + mineBlocks(t, net, 1, 0) + time.Sleep(time.Second) + + return false + }, 2*time.Minute, 2*time.Second) + + assertBalance( + t.t, charlie, ccItestAsset.Amount, + itest.WithAssetID(assetID), + ) + + t.Logf("Charlie balance restored after breach") + + // Give the porter time to fully finalize all justice sweep + // transfers. The balance assertion above succeeds as soon as the + // assets appear, but the anchor output metadata may still be + // processing. + mineBlocks(t, net, 3, 0) + + // Verify the recovered assets are spendable by sending ALL of + // them from Charlie to Dave. This proves the full proof chain + // (funding → commitment → second-level → justice → spend) is + // valid end-to-end for every recovered output. + sendAmt := ccItestAsset.Amount + ctxSend := context.Background() + daveAddr, err := asTapd(dave).NewAddr( + ctxSend, &taprpc.NewAddrRequest{ + Amt: sendAmt, + AssetId: assetID, + ProofCourierAddr: fmt.Sprintf( + "%s://%s", + proof.UniverseRpcCourierType, + zane.RPCAddr(), + ), + }, ) require.NoError(t.t, err) - t.Logf("Charlie justice txid: %v", charlieJusticeTxid) + itest.AssertAddrCreated(t.t, asTapd(dave), cents, daveAddr) + _, err = asTapd(charlie).SendAsset( + ctxSend, &taprpc.SendAssetRequest{ + TapAddrs: []string{daveAddr.Encoded}, + }, + ) + require.NoError(t.t, err) - // Next, we'll mine a block to confirm Charlie's justice transaction. + // Mine the send transaction. mineBlocks(t, net, 1, 1) - // Charlie should now have a transfer for his justice transaction. - locateAssetTransfers(t.t, charlie, *charlieJusticeTxid[0]) - - // Charlie's balance should now be the same as before the breach - // attempt: the amount he minted at the very start. - charlieBalance := ccItestAsset.Amount + // Verify Dave received all the assets. assertBalance( - t.t, charlie, charlieBalance, itest.WithAssetID(assetID), - itest.WithNumUtxos(3), + t.t, dave, sendAmt, itest.WithAssetID(assetID), ) - t.Logf("Charlie balance after breach: %d", charlieBalance) + t.Logf("Post-breach on-chain spend successful — proof chain valid") } diff --git a/itest/custom_channels/core_test.go b/itest/custom_channels/core_test.go index 75a3acc6b9..92855f7e4f 100644 --- a/itest/custom_channels/core_test.go +++ b/itest/custom_channels/core_test.go @@ -193,7 +193,7 @@ func testCustomChannels(ctx context.Context, // Dave, making it possible to send another asset HTLC below, sending // all assets back to Charlie (so we have enough balance for further // tests). - sendKeySendPayment(t.t, charlie, dave, 2000) + sendKeySendPayment(t.t, charlie, dave, 10_000) logBalance(t.t, nodes, assetID, "after BTC only keysend") // Let's keysend the rest of the balance back to Charlie. @@ -365,7 +365,7 @@ func testCustomChannels(ctx context.Context, t.Logf("Closing Dave -> Yara channel") closeAssetChannelAndAssert( t, net, dave, yara, chanPointDY, [][]byte{assetID}, nil, - charlie, assertDefaultCoOpCloseBalance(false, true), + charlie, assertDefaultCoOpCloseBalance(true, true), ) t.Logf("Closing Erin -> Fabia channel") diff --git a/itest/custom_channels/force_close_test.go b/itest/custom_channels/force_close_test.go index 419a41227f..9390526fa2 100644 --- a/itest/custom_channels/force_close_test.go +++ b/itest/custom_channels/force_close_test.go @@ -226,7 +226,7 @@ func testCustomChannelsForceClose(ctx context.Context, t.Logf("Universe proofs located!") // We should also have a new sweep transaction in the mempool. - _, err = waitForNTxsInMempool( + _, err = waitForAtLeastNTxsInMempool( net.Miner, 1, ccShortTimeout, ) require.NoError(t.t, err) @@ -250,7 +250,7 @@ func testCustomChannelsForceClose(ctx context.Context, mineBlocks(t, net, 4, 0) // We expect that Charlie's sweep transaction has been broadcast. - charlieSweepTxid, err := waitForNTxsInMempool( + charlieSweepTxid, err := waitForAtLeastNTxsInMempool( net.Miner, 1, ccShortTimeout, ) require.NoError(t.t, err) diff --git a/itest/custom_channels/forward_bandwidth_test.go b/itest/custom_channels/forward_bandwidth_test.go index f0c7b4b344..c64752986f 100644 --- a/itest/custom_channels/forward_bandwidth_test.go +++ b/itest/custom_channels/forward_bandwidth_test.go @@ -138,7 +138,7 @@ func testCustomChannelsForwardBandwidth(ctx context.Context, ) // Test case 2: We cannot pay an invoice from Charlie to Fabia. - invoiceResp := createAssetInvoice(t.t, erin, fabia, 123, assetID) + invoiceResp := createAssetInvoice(t.t, erin, fabia, 200, assetID) payInvoiceWithSatoshi( t.t, charlie, invoiceResp, withFailure(lnrpc.Payment_FAILED, failureNoRoute), @@ -215,7 +215,7 @@ func testCustomChannelsForwardBandwidth(ctx context.Context, // Let's make sure we can still use the channel between Erin and Fabia // by doing a satoshi keysend payment. - sendKeySendPayment(t.t, erin, fabia, 2000) + sendKeySendPayment(t.t, erin, fabia, 10_000) logBalance(t.t, nodes, assetID, "after BTC only keysend") // Finally, we close the channel between Erin and Fabia to make sure diff --git a/itest/custom_channels/group_tranches_force_close_test.go b/itest/custom_channels/group_tranches_force_close_test.go index e088f11320..b516a1ab86 100644 --- a/itest/custom_channels/group_tranches_force_close_test.go +++ b/itest/custom_channels/group_tranches_force_close_test.go @@ -240,7 +240,7 @@ func testCustomChannelsGroupTranchesForceClose(ctx context.Context, t.Logf("Universe proofs located!") // We should also have a new sweep transaction in the mempool. - fabiaSweepTxid, err := waitForNTxsInMempool( + fabiaSweepTxid, err := waitForAtLeastNTxsInMempool( net.Miner, 1, ccShortTimeout, ) require.NoError(t.t, err) @@ -260,7 +260,7 @@ func testCustomChannelsGroupTranchesForceClose(ctx context.Context, mineBlocks(t, net, 4, 0) // We expect that Erin's sweep transaction has been broadcast. - _, err = waitForNTxsInMempool( + _, err = waitForAtLeastNTxsInMempool( net.Miner, 1, ccShortTimeout, ) require.NoError(t.t, err) diff --git a/itest/custom_channels/grouped_asset_test.go b/itest/custom_channels/grouped_asset_test.go index 0049d28802..ecbbd7c3b1 100644 --- a/itest/custom_channels/grouped_asset_test.go +++ b/itest/custom_channels/grouped_asset_test.go @@ -169,7 +169,7 @@ func testCustomChannelsGroupedAsset(_ context.Context, // We should also be able to do a non-asset (BTC only) keysend // payment. - sendKeySendPayment(t.t, charlie, dave, 2000) + sendKeySendPayment(t.t, charlie, dave, 10_000) logBalance(t.t, nodes, assetID, "after BTC only keysend") // ------------ @@ -333,7 +333,7 @@ func testCustomChannelsGroupedAsset(_ context.Context, closeAssetChannelAndAssert( t, net, dave, yara, chanPointDY, [][]byte{assetID}, groupID, charlie, - assertDefaultCoOpCloseBalance(false, true), + assertDefaultCoOpCloseBalance(true, true), ) t.Logf("Closing Erin -> Fabia channel") diff --git a/itest/custom_channels/helpers.go b/itest/custom_channels/helpers.go index 5245d60a94..7eb5a95b2d 100644 --- a/itest/custom_channels/helpers.go +++ b/itest/custom_channels/helpers.go @@ -214,6 +214,7 @@ type invoiceConfig struct { groupKey []byte msats lnwire.MilliSatoshi routeHints []*lnrpc.RouteHint + expiry int64 } func defaultInvoiceConfig() *invoiceConfig { @@ -230,6 +231,12 @@ func withInvoiceErrSubStr(errSubStr string) invoiceOpt { } } +func withInvoiceExpiry(seconds int64) invoiceOpt { + return func(c *invoiceConfig) { + c.expiry = seconds + } +} + func withInvGroupKey(groupKey []byte) invoiceOpt { return func(c *invoiceConfig) { c.groupKey = groupKey @@ -1257,8 +1264,10 @@ func sendAssetKeySendPayment(t *testing.T, src, dst *itest.IntegratedNode, customRecords[record.KeySendType] = preimage[:] sendReq := &routerrpc.SendPaymentRequest{ - Dest: dst.PubKey[:], - Amt: btcAmt.UnwrapOr(500), + Dest: dst.PubKey[:], + Amt: btcAmt.UnwrapOr( + int64(rfqmath.DefaultOnChainHtlcSat), + ), DestCustomRecords: customRecords, PaymentHash: hash[:], TimeoutSeconds: int32(PaymentTimeout.Seconds()), @@ -2294,6 +2303,9 @@ func createAssetHodlInvoice(t *testing.T, dstRfqPeer, defer cancel() timeoutSeconds := int64(rfq.DefaultInvoiceExpiry.Seconds()) + if cfg.expiry > 0 { + timeoutSeconds = cfg.expiry + } var rfqPeer []byte if dstRfqPeer != nil { @@ -3061,7 +3073,7 @@ func assertForceCloseSweeps(ctx context.Context, walletrpc.WitnessType_TAPROOT_HTLC_ACCEPTED_REMOTE_SUCCESS, ) - _, err = waitForNTxsInMempool( + memTxs, err := waitForAtLeastNTxsInMempool( net.Miner, 1, ccShortTimeout, ) require.NoError(t.t, err) @@ -3070,19 +3082,19 @@ func assertForceCloseSweeps(ctx context.Context, // both his commitment output, and the incoming HTLC that we just // settled above. We use the txid from the mined block (not from the // mempool check above) because the sweeper may RBF between the two. - bobSweepBlocks1 := mineBlocks(t, net, 1, 1) + bobSweepBlocks1 := mineBlocks(t, net, 1, len(memTxs)) // At this point, we should have the next sweep transaction in the // mempool: Bob's incoming HTLC sweep directly off the commitment // transaction. - _, err = waitForNTxsInMempool( + memTxs, err = waitForAtLeastNTxsInMempool( net.Miner, 1, ccShortTimeout, ) require.NoError(t.t, err) // We'll now mine the next block, which should confirm Bob's HTLC sweep // transaction. - bobSweepBlocks2 := mineBlocks(t, net, 1, 1) + bobSweepBlocks2 := mineBlocks(t, net, 1, len(memTxs)) // Wait for tapd to process the confirmed sweep transactions before // checking balances. We extract the txid from the mined blocks rather @@ -3111,12 +3123,12 @@ func assertForceCloseSweeps(ctx context.Context, // sweep her to-local output. mineBlocks(t, net, 1, 0) - _, err = waitForNTxsInMempool( + memTxs, err = waitForAtLeastNTxsInMempool( net.Miner, 1, ccShortTimeout, ) require.NoError(t.t, err) - aliceToLocalBlocks := mineBlocks(t, net, 1, 1) + aliceToLocalBlocks := mineBlocks(t, net, 1, len(memTxs)) // Wait for tapd to register the to-local sweep transfer. We use the // txid from the mined block to avoid RBF mismatches. @@ -3158,14 +3170,15 @@ func assertForceCloseSweeps(ctx context.Context, // If the block mined above didn't also mine our sweep, then we'll mine // one final block which will confirm Alice's sweep transaction. if len(sweepBlocks[0].Transactions) == 1 { - _, err := waitForNTxsInMempool( + memTxs, err := waitForAtLeastNTxsInMempool( net.Miner, 1, ccShortTimeout, ) require.NoError(t.t, err) - // With the sweep transaction in the mempool, we'll mine a block - // to confirm the sweep. - sweepBlocks = mineBlocks(t, net, 1, 1) + // With the sweep transaction(s) in the mempool, mine a + // block to confirm them. Under DeterministicHTLCs the + // sweeper may broadcast multiple txs at once. + sweepBlocks = mineBlocks(t, net, 1, len(memTxs)) } // Use the txid from the mined block to avoid RBF mismatches. @@ -3195,12 +3208,12 @@ func assertForceCloseSweeps(ctx context.Context, // If the block mined above didn't also mine our sweep, then we'll mine // one final block which will confirm Alice's sweep transaction. if len(sweepBlocks[0].Transactions) == 1 { - _, err := waitForNTxsInMempool( + memTxs, err := waitForAtLeastNTxsInMempool( net.Miner, 1, ccShortTimeout, ) require.NoError(t.t, err) - sweepBlocks = mineBlocks(t, net, 1, 1) + sweepBlocks = mineBlocks(t, net, 1, len(memTxs)) } sweepTxHash = sweepBlocks[0].Transactions[1].TxHash() @@ -3362,14 +3375,14 @@ func assertForceCloseSweeps(ctx context.Context, // If the block mined above didn't also mine our sweep, then we'll mine // one final block which will confirm Alice's sweep transaction. if len(sweepBlocks[0].Transactions) == 1 { - _, err := waitForNTxsInMempool( + memTxs, err := waitForAtLeastNTxsInMempool( net.Miner, 1, ccShortTimeout, ) require.NoError(t.t, err) // We'll mine one final block which will confirm Alice's sweep // transaction. - sweepBlocks = mineBlocks(t, net, 1, 1) + sweepBlocks = mineBlocks(t, net, 1, len(memTxs)) } sweepTxHash = sweepBlocks[0].Transactions[1].TxHash() diff --git a/itest/custom_channels/limit_constraints_test.go b/itest/custom_channels/limit_constraints_test.go index 38df67f763..7ba8d39b25 100644 --- a/itest/custom_channels/limit_constraints_test.go +++ b/itest/custom_channels/limit_constraints_test.go @@ -394,7 +394,7 @@ func testCustomChannelsLimitConstraints(_ context.Context, TaprootAssetChannelsClient.AddInvoice( ctxb, &tchrpc.AddInvoiceRequest{ AssetId: assetID, - AssetAmount: 1_000_000, + AssetAmount: 2_000_000, PeerPubkey: charlie.PubKey[:], InvoiceRequest: &lnrpc.Invoice{ Expiry: 60, @@ -422,7 +422,7 @@ func testCustomChannelsLimitConstraints(_ context.Context, TaprootAssetChannelsClient.AddInvoice( ctxb, &tchrpc.AddInvoiceRequest{ AssetId: assetID, - AssetAmount: 1_000_000, + AssetAmount: 2_000_000, PeerPubkey: charlie.PubKey[:], InvoiceRequest: &lnrpc.Invoice{ Expiry: 60, @@ -442,12 +442,12 @@ func testCustomChannelsLimitConstraints(_ context.Context, TaprootAssetChannelsClient.AddInvoice( ctxb, &tchrpc.AddInvoiceRequest{ AssetId: assetID, - AssetAmount: 1_000_000, + AssetAmount: 2_000_000, PeerPubkey: charlie.PubKey[:], InvoiceRequest: &lnrpc.Invoice{ Expiry: 60, }, - AssetMinAmt: fn.Ptr[uint64](2_000_000), + AssetMinAmt: fn.Ptr[uint64](3_000_000), }, ) require.ErrorContains(t.t, err, "exceeds max amount") diff --git a/itest/custom_channels/liquidity_test.go b/itest/custom_channels/liquidity_test.go index b50accec1b..b3fcc45f99 100644 --- a/itest/custom_channels/liquidity_test.go +++ b/itest/custom_channels/liquidity_test.go @@ -337,7 +337,7 @@ func testCustomChannelsLiquidityEdgeCasesCore(ctx context.Context, // Edge case: Now Charlie creates a tiny asset invoice to be paid for by // Yara with satoshi. This is a multi-hop payment going over 2 asset // channels, where the total asset value is less than the default anchor - // amount of 354 sats. + // amount of 2124 sats (6x dust limit). createAssetInvoice( t.t, dave, charlie, 1, assetID, withInvoiceErrSubStr( "minimal transportable amount", diff --git a/itest/custom_channels/self_payment_test.go b/itest/custom_channels/self_payment_test.go index a9c2d014ad..71c39ea63f 100644 --- a/itest/custom_channels/self_payment_test.go +++ b/itest/custom_channels/self_payment_test.go @@ -32,7 +32,6 @@ func testCustomChannelsSelfPayment(_ context.Context, lndArgs := slices.Clone(lndArgsTemplate) tapdArgs := slices.Clone(tapdArgsTemplate) - // We use Alice as the proof courier. But in order for Alice to also // use itself, we need to define its port upfront. alicePort := port.NextAvailablePort() @@ -128,12 +127,12 @@ func testCustomChannelsSelfPayment(_ context.Context, t.Logf("Key sending 15k assets from Alice to Bob...") const ( assetKeySendAmount = 15_000 - numInvoicePayments = 10 + numInvoicePayments = 5 assetInvoiceAmount = 1_234 btcInvoiceAmount = 10_000 btcKeySendAmount = 200_000 btcReserveAmount = 2000 - btcHtlcCost = numInvoicePayments * 354 + btcHtlcCost = numInvoicePayments * 2124 ) sendAssetKeySendPayment( t.t, alice, bob, assetKeySendAmount, assetID, diff --git a/itest/custom_channels/strict_forwarding_test.go b/itest/custom_channels/strict_forwarding_test.go index 2fd5b8bb2b..25ab75bc8c 100644 --- a/itest/custom_channels/strict_forwarding_test.go +++ b/itest/custom_channels/strict_forwarding_test.go @@ -149,7 +149,7 @@ func testCustomChannelsStrictForwarding(_ context.Context, // satoshi, where we will check whether Dave's strict forwarding // works as expected. Charlie is only used as a dummy RFQ peer in this // case, Erin totally ignores the RFQ hint and just pays with sats. - assetInvoice := createAssetInvoice(t.t, charlie, dave, 40, assetID) + assetInvoice := createAssetInvoice(t.t, charlie, dave, 200, assetID) ctx := context.Background() assetInvoiceStream, err := dave.InvoicesClient.SubscribeSingleInvoice( @@ -193,7 +193,7 @@ func testCustomChannelsStrictForwarding(_ context.Context, // Edge case: We now try the opposite: Dave creates a BTC invoice but // Charlie tries to pay it with assets. This should fail as well. - btcInvoice := createNormalInvoice(t.t, dave, 1_000) + btcInvoice := createNormalInvoice(t.t, dave, 5_000) btcInvoiceStream, err := dave.InvoicesClient.SubscribeSingleInvoice( ctx, &invoicesrpc.SubscribeSingleInvoiceRequest{ RHash: btcInvoice.RHash, diff --git a/itest/custom_channels/v1_upgrade_test.go b/itest/custom_channels/v1_upgrade_test.go index 4e62d50cc8..a95fbb666b 100644 --- a/itest/custom_channels/v1_upgrade_test.go +++ b/itest/custom_channels/v1_upgrade_test.go @@ -140,7 +140,7 @@ func testCustomChannelsV1Upgrade(ctx context.Context, sendAssetKeySendPayment( t.t, charlie, dave, 50, assetID, fn.None[int64](), ) - sendKeySendPayment(t.t, charlie, dave, 1_000) + sendKeySendPayment(t.t, charlie, dave, 10_000) } logBalance(t.t, nodes, assetID, "before upgrade") @@ -271,7 +271,7 @@ func testCustomChannelsV1Upgrade(ctx context.Context, // With the breach transaction mined, Dave should now have a // transaction in the mempool sweeping *both* commitment outputs. - daveJusticeTxid, err := waitForNTxsInMempool( + daveJusticeTxid, err := waitForAtLeastNTxsInMempool( net.Miner, 1, time.Second*5, ) require.NoError(t.t, err) diff --git a/itest/custom_channels/vars.go b/itest/custom_channels/vars.go index 567ae4d92f..d74aa97248 100644 --- a/itest/custom_channels/vars.go +++ b/itest/custom_channels/vars.go @@ -112,7 +112,7 @@ const ( // DefaultPushSat is the default push amount in satoshis when opening // custom channels. - DefaultPushSat int64 = 1062 + DefaultPushSat int64 = 6372 // assetBurnConfirmationText is the text that needs to be set on the // RPC to confirm an asset burn. diff --git a/itest/integrated_harness.go b/itest/integrated_harness.go index 557f14f229..57191a1e87 100644 --- a/itest/integrated_harness.go +++ b/itest/integrated_harness.go @@ -668,6 +668,25 @@ func (h *IntegratedNetworkHarness) StopAndRestoreDB( return nil } +// SuspendNode stops the given node without cleaning it up. It returns a +// closure that can be called to restart the node. +func (h *IntegratedNetworkHarness) SuspendNode( + node *IntegratedNode) (func(), error) { + + node.Stop() + + restart := func() { + // Remove the ready file so Start() waits for the new + // instance. + if node.readyFile != "" { + _ = os.Remove(node.readyFile) + } + node.Start() + } + + return restart, nil +} + // copyAll recursively copies all files and directories from srcDir to dstDir. func copyAll(dstDir, srcDir string) error { entries, err := os.ReadDir(srcDir) diff --git a/proof/verified.go b/proof/verified.go index f62f531080..cfa8393797 100644 --- a/proof/verified.go +++ b/proof/verified.go @@ -40,6 +40,26 @@ func (v verifiedAnnotatedProof) AnnotatedProof() *AnnotatedProof { // verified prevents external packages from implementing VerifiedAnnotatedProof. func (v verifiedAnnotatedProof) verified() {} +// AssumeVerifiedAnnotatedProofs wraps the given proofs as verified without +// running the proof verifier. This is used when importing already-confirmed +// channel transactions whose asset-level witnesses are placeholders (e.g. +// second-level HTLC transactions). The BTC-level on-chain confirmation serves +// as proof of validity, making VM-level witness verification unnecessary. +// +// The confirmHeight parameter is the block height at which the transaction +// was confirmed, serving as attestation that the caller has verified on-chain +// confirmation. A height of 0 will cause a panic to prevent misuse. +func AssumeVerifiedAnnotatedProofs(confirmHeight uint32, + proofs ...*AnnotatedProof) []VerifiedAnnotatedProof { + + if confirmHeight == 0 { + panic("AssumeVerifiedAnnotatedProofs requires a " + + "non-zero confirmation height") + } + + return fn.Map(proofs, newVerifiedAnnotatedProof) +} + // VerifyAnnotatedProofs verifies and enriches the given proofs with a default // verifier and returns the verified wrappers. func VerifyAnnotatedProofs(ctx context.Context, vCtx VerifierCtx, diff --git a/proof/verifier.go b/proof/verifier.go index 0439cb3001..2da30f0c3c 100644 --- a/proof/verifier.go +++ b/proof/verifier.go @@ -1107,8 +1107,11 @@ func (p *Proof) VerifyProofIntegrity(ctx context.Context, vCtx VerifierCtx, // 1. A transaction that spends the previous asset output has a valid // merkle proof within a block in the chain. if !TxSpendsPrevOut(&p.AnchorTx, &p.PrevOut) { - return nil, fmt.Errorf("%w: doesn't spend prev output", - commitment.ErrInvalidTaprootProof) + return nil, fmt.Errorf("%w: doesn't spend prev output "+ + "(anchor_txid=%v, prev_out=%v, num_inputs=%d)", + commitment.ErrInvalidTaprootProof, + p.AnchorTx.TxHash(), p.PrevOut, + len(p.AnchorTx.TxIn)) } if !verificationParams.SkipChainVerification { @@ -1134,15 +1137,42 @@ func (p *Proof) VerifyProofIntegrity(ctx context.Context, vCtx VerifierCtx, // TODO(jhb): check for genesis asset and populate asset fields before // further verification - // The VerifyProofs method will verify the following steps: // 2. A valid inclusion proof for the resulting asset is included. - // 3. A valid inclusion proof for the split root, if the resulting asset - // is a split asset. + tapCommitment, err := p.verifyInclusionProof() + if err != nil { + return nil, fmt.Errorf("invalid inclusion proof: %w", err) + } + + // 3. A valid inclusion proof for the split root, if the resulting + // asset is a split asset. + if p.Asset.HasSplitCommitmentWitness() { + if p.SplitRootProof == nil { + return nil, ErrMissingSplitRootProof + } + if err := p.verifySplitRootProof(); err != nil { + return nil, err + } + } + // 4. A set of valid exclusion proofs for the resulting asset are - // included. - tapCommitment, err := p.VerifyProofs() + // included. + exclusionCommitVersion, err := p.verifyExclusionProofs() if err != nil { - return nil, fmt.Errorf("error verifying proofs: %w", err) + return nil, fmt.Errorf("invalid exclusion proof: %w", err) + } + + if exclusionCommitVersion != nil { + if !commitment.IsSimilarTapCommitmentVersion( + &tapCommitment.Version, + exclusionCommitVersion, + ) { + + return nil, fmt.Errorf("mixed commitment "+ + "versions, inclusion %d, "+ + "exclusion %d", + tapCommitment.Version, + *exclusionCommitVersion) + } } // 5. If this is a genesis asset, start by verifying the diff --git a/rfq/manager_test.go b/rfq/manager_test.go index 8cb06cd04d..b268ff5de3 100644 --- a/rfq/manager_test.go +++ b/rfq/manager_test.go @@ -461,7 +461,7 @@ func createChannelWithCustomData(t *testing.T, id asset.ID, localBalance, ), }, nil, nil, lnwallet.CommitAuxLeaves{}, - false, + false, tpchmsg.SigHashAll, ), OpenChan: *tpchmsg.NewOpenChannel( []*tpchmsg.AssetOutput{ diff --git a/rfqmath/convert.go b/rfqmath/convert.go index 039b94bfe2..52146cebac 100644 --- a/rfqmath/convert.go +++ b/rfqmath/convert.go @@ -11,9 +11,13 @@ import ( var ( // DefaultOnChainHtlcSat is the default amount that we consider as the - // smallest HTLC amount that can be sent on-chain. This needs to be - // greater than the dust limit for an HTLC. - DefaultOnChainHtlcSat = lnwallet.DustLimitForSize( + // smallest HTLC amount that can be sent on-chain. We use 6x the dust + // limit to provide enough headroom for the baked-in second-level HTLC + // transaction fees under SigHashDefault (where the sweeper cannot add + // wallet inputs) and to comfortably clear the mempool minimum relay + // fee even when nodes compute fee rate using raw serialized size + // rather than virtual size. + DefaultOnChainHtlcSat = 6 * lnwallet.DustLimitForSize( input.UnknownWitnessSize, ) diff --git a/rfqmath/convert_test.go b/rfqmath/convert_test.go index 2188bc08ed..3d5470b06a 100644 --- a/rfqmath/convert_test.go +++ b/rfqmath/convert_test.go @@ -388,7 +388,7 @@ func TestConvertMilliSatoshiToUnits(t *testing.T) { }, expectedUnits: 1, expectedMinTransportUnits: 1, - expectedMinTransportMSat: 20_354_000, + expectedMinTransportMSat: 22_124_000, }, { // 5k USD per BTC @ decimal display 6. @@ -399,7 +399,7 @@ func TestConvertMilliSatoshiToUnits(t *testing.T) { }.ScaleTo(6), expectedUnits: 10_000, expectedMinTransportUnits: 1, - expectedMinTransportMSat: 20_354_000, + expectedMinTransportMSat: 22_124_000, }, { // 50k USD per BTC @ decimal display 6. @@ -410,7 +410,7 @@ func TestConvertMilliSatoshiToUnits(t *testing.T) { }.ScaleTo(6), expectedUnits: 1000, expectedMinTransportUnits: 1, - expectedMinTransportMSat: 2_326_308, + expectedMinTransportMSat: 4_096_308, }, { // 50M USD per BTC @ decimal display 6. @@ -420,8 +420,8 @@ func TestConvertMilliSatoshiToUnits(t *testing.T) { Scale: 2, }.ScaleTo(6), expectedUnits: 62595061158, - expectedMinTransportUnits: 179, - expectedMinTransportMSat: 355_972, + expectedMinTransportUnits: 1076, + expectedMinTransportMSat: 2_125_972, }, { // 50k USD per BTC @ decimal display 6. @@ -432,7 +432,7 @@ func TestConvertMilliSatoshiToUnits(t *testing.T) { }.ScaleTo(6), expectedUnits: 2_570, expectedMinTransportUnits: 1, - expectedMinTransportMSat: 2_326_304, + expectedMinTransportMSat: 4_096_304, }, { // 7.341M JPY per BTC @ decimal display 6. @@ -442,8 +442,8 @@ func TestConvertMilliSatoshiToUnits(t *testing.T) { Scale: 0, }.ScaleTo(6), expectedUnits: 367_092, - expectedMinTransportUnits: 25, - expectedMinTransportMSat: 367_620, + expectedMinTransportUnits: 155, + expectedMinTransportMSat: 2_137_620, }, { // 7.341M JPY per BTC @ decimal display 2. @@ -453,8 +453,8 @@ func TestConvertMilliSatoshiToUnits(t *testing.T) { Scale: 0, }.ScaleTo(4), expectedUnits: 3_670, - expectedMinTransportUnits: 25, - expectedMinTransportMSat: 367_620, + expectedMinTransportUnits: 155, + expectedMinTransportMSat: 2_137_620, }, } diff --git a/server.go b/server.go index ba0df2827c..ba71e50d92 100644 --- a/server.go +++ b/server.go @@ -1,6 +1,7 @@ package taprootassets import ( + "bytes" "context" "fmt" "net/http" @@ -12,6 +13,7 @@ import ( "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" proxy "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" "github.com/lightninglabs/lndclient" @@ -27,6 +29,7 @@ import ( "github.com/lightninglabs/taproot-assets/tapchannel" cmsg "github.com/lightninglabs/taproot-assets/tapchannelmsg" "github.com/lightninglabs/taproot-assets/tapconfig" + "github.com/lightninglabs/taproot-assets/tapfeatures" "github.com/lightninglabs/taproot-assets/taprpc" "github.com/lightningnetwork/lnd/build" "github.com/lightningnetwork/lnd/channeldb" @@ -976,8 +979,9 @@ func (s *Server) FetchLeavesFromCommit(chanState lnwl.AuxChanState, // from a channel revocation that stores balance + blob information. // // NOTE: This method is part of the lnwallet.AuxLeafStore interface. -func (s *Server) FetchLeavesFromRevocation( - r *channeldb.RevocationLog) lfn.Result[lnwl.CommitDiffAuxResult] { +func (s *Server) FetchLeavesFromRevocation(r *channeldb.RevocationLog, + chanState lnwl.AuxChanState, keys lnwl.CommitmentKeyRing, + commitTx *wire.MsgTx) lfn.Result[lnwl.CommitDiffAuxResult] { srvrLog.Debugf("FetchLeavesFromRevocation called, ourBalance=%v, "+ "teirBalance=%v, numHtlcs=%d", r.OurBalance, r.TheirBalance, @@ -985,7 +989,9 @@ func (s *Server) FetchLeavesFromRevocation( // 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.FetchLeavesFromRevocation(r) + return tapchannel.FetchLeavesFromRevocation( + r, chanState, keys, commitTx, s.chainParams, + ) } // ApplyHtlcView serves as the state transition function for the custom @@ -1131,6 +1137,80 @@ func (s *Server) VerifySecondLevelSigs(chanState lnwl.AuxChanState, ) } +// HtlcSigHashType returns the sighash type to use for HTLC second-level +// transactions for the given channel. The request carries either a ChanID +// (for live feature-negotiation lookups on new commitments) or a CommitBlob +// (for existing commitments), or both. +// +// When a ChanID is present and the server is configured, the live negotiated +// features are checked first. The CommitBlob is used as a fallback (or as +// the sole source when no ChanID is provided). +// +// NOTE: This method is part of the lnwallet.AuxSigner interface. +func (s *Server) HtlcSigHashType( + req lnwl.HtlcSigHashReq) lfn.Option[txscript.SigHashType] { + + // If a ChanID was provided and the server is fully configured, + // check live feature negotiation state first. + if req.ChanID.IsSome() && s != nil && s.cfg != nil && + s.cfg.AuxChanNegotiator != nil { + + chanID := req.ChanID.UnwrapOr(lnwire.ChannelID{}) + + features := s.cfg.AuxChanNegotiator.GetChannelFeatures( + chanID, + ) + + hasSigHashDefault := features.HasFeature( + tapfeatures.DeterministicHTLCsOptional, + ) + + srvrLog.Debugf("HtlcSigHashType called for "+ + "chan_id=%x, "+ + "deterministic_htlcs_negotiated=%v", + chanID[:], hasSigHashDefault) + + if hasSigHashDefault { + return lfn.Some(txscript.SigHashDefault) + } + } + + // Fall back to the commitment blob cache. + return s.htlcSigHashFromBlob(req.CommitBlob) +} + +// htlcSigHashFromBlob decodes the commitment blob and checks the cached +// SigHashDefault flag. This is used for existing commitments (breach, +// resolution) where the blob is the source of truth, and as a fallback +// during startup when the peer hasn't reconnected yet. +func (s *Server) htlcSigHashFromBlob( + commitBlob lfn.Option[tlv.Blob]) lfn.Option[txscript.SigHashType] { + + blob, err := commitBlob.UnwrapOrErr( + fmt.Errorf("no commit blob"), + ) + if err != nil { + return lfn.None[txscript.SigHashType]() + } + + var c cmsg.Commitment + if decErr := c.Decode(bytes.NewReader(blob)); decErr != nil { + srvrLog.Errorf("HtlcSigHashType: unable to decode "+ + "commit blob (%d bytes): %v", len(blob), decErr) + + return lfn.None[txscript.SigHashType]() + } + + if c.SigHashDefault.Val { + srvrLog.Debugf("HtlcSigHashType: using cached " + + "SigHashDefault from commit blob") + + return lfn.Some(txscript.SigHashDefault) + } + + return lfn.None[txscript.SigHashType]() +} + // DescFromPendingChanID takes a pending channel ID, that may already be // known due to prior custom channel messages, and maybe returns an aux // funding desc which can be used to modify how a channel is funded. @@ -1382,24 +1462,27 @@ func (s *Server) ExtraBudgetForInputs( return tapchannel.ExtraBudgetForInputs(inputs) } -// NotifyBroadcast is used to notify external callers of the broadcast of a -// sweep transaction, generated by the passed BumpRequest. +// NotifyBroadcast is called by lnd's sweeper to notify us of a sweep +// transaction broadcast, generated by the passed BumpRequest. // // NOTE: This method is part of the sweep.AuxSweeper interface. func (s *Server) NotifyBroadcast(req *sweep.BumpRequest, tx *wire.MsgTx, fee btcutil.Amount, - outpointToTxIndex map[wire.OutPoint]int) error { + outpointToTxIndex map[wire.OutPoint]int, + opts sweep.AuxNotifyOpts) error { - srvrLog.Tracef("NotifyBroadcast called, req=%v, tx=%v, fee=%v, "+ - "out_index=%v", lnutils.SpewLogClosure(req), - lnutils.SpewLogClosure(tx), fee, - lnutils.SpewLogClosure(outpointToTxIndex)) + srvrLog.Infof("NotifyBroadcast called, skip_broadcast=%v, "+ + "skip_proof_verify=%v, tx=%v, fee=%v", + opts.SkipBroadcast, opts.SkipProofVerify, + tx.TxHash(), fee) if err := s.waitForReady(); err != nil { return err } - return s.cfg.AuxSweeper.NotifyBroadcast(req, tx, fee, outpointToTxIndex) + return s.cfg.AuxSweeper.NotifyBroadcast( + req, tx, fee, outpointToTxIndex, opts, + ) } // GetInitRecords is called when sending an init message to a peer. It returns diff --git a/tapchannel/auf_leaf_signer_test.go b/tapchannel/auf_leaf_signer_test.go index 05786a890c..62e55c81e2 100644 --- a/tapchannel/auf_leaf_signer_test.go +++ b/tapchannel/auf_leaf_signer_test.go @@ -111,7 +111,8 @@ func setupAuxLeafSigner(t *testing.T, numJobs int32) (*AuxLeafSigner, } com := cmsg.NewCommitment( - nil, nil, outgoingHtlcs, nil, lnwallet.CommitAuxLeaves{}, false, + nil, nil, outgoingHtlcs, nil, + lnwallet.CommitAuxLeaves{}, false, cmsg.SigHashAll, ) cancelChan := make(chan struct{}) diff --git a/tapchannel/aux_closer.go b/tapchannel/aux_closer.go index 842144a88c..b552571fa9 100644 --- a/tapchannel/aux_closer.go +++ b/tapchannel/aux_closer.go @@ -706,7 +706,8 @@ func (a *AuxChanCloser) ShutdownBlob( func shipChannelTxn(txSender tapfreighter.Porter, chanTx *wire.MsgTx, outputCommitments tappsbt.OutputCommitments, vPkts []*tappsbt.VPacket, closeFee int64, - anchorTxHeightHint fn.Option[uint32]) error { + anchorTxHeightHint fn.Option[uint32], skipBroadcast bool, + parcelOpts ...tapfreighter.PreAnchoredParcelOpt) error { chanTxPsbt, err := tapsend.PrepareAnchoringTemplate(vPkts) if err != nil { @@ -735,8 +736,8 @@ func shipChannelTxn(txSender tapfreighter.Porter, chanTx *wire.MsgTx, } parcelLabel := fmt.Sprintf("channel-tx-%s", chanTx.TxHash().String()) preSignedParcel := tapfreighter.NewPreAnchoredParcel( - vPkts, nil, closeAnchor, false, parcelLabel, - anchorTxHeightHint, + vPkts, nil, closeAnchor, skipBroadcast, parcelLabel, + anchorTxHeightHint, parcelOpts..., ) _, err = txSender.RequestShipment(preSignedParcel) if err != nil { @@ -913,6 +914,7 @@ func (a *AuxChanCloser) FinalizeClose(desc types.AuxCloseDesc, err := shipChannelTxn( a.cfg.TxSender, closeTx, closeInfo.outputCommitments, closeInfo.vPackets, closeInfo.closeFee, fn.None[uint32](), + false, ) if err != nil { return err diff --git a/tapchannel/aux_funding_controller.go b/tapchannel/aux_funding_controller.go index 6ec7ca6fa1..7b58fba3fd 100644 --- a/tapchannel/aux_funding_controller.go +++ b/tapchannel/aux_funding_controller.go @@ -432,6 +432,8 @@ type pendingAssetFunding struct { stxo bool + sigHashDefault bool + amt uint64 pushAmt btcutil.Amount @@ -556,7 +558,7 @@ func newCommitBlobAndLeaves(pendingFunding *pendingAssetFunding, lndOpenChan lnwallet.AuxChanState, assetOpenChan *cmsg.OpenChannel, keyRing lntypes.Dual[lnwallet.CommitmentKeyRing], whoseCommit lntypes.ChannelParty, - stxo bool) ([]byte, lnwallet.CommitAuxLeaves, + stxo, sigHashDefault bool) ([]byte, lnwallet.CommitAuxLeaves, error) { chanAssets := assetOpenChan.FundedAssets.Val.Outputs @@ -591,7 +593,7 @@ func newCommitBlobAndLeaves(pendingFunding *pendingAssetFunding, // needs the sum of the remote+local assets, so we'll populate that. fakePrevState := cmsg.NewCommitment( localAssets, remoteAssets, nil, nil, lnwallet.CommitAuxLeaves{}, - stxo, + stxo, cmsg.SigHashAll, ) // Just like above, we don't have a real HTLC view here, so we'll pass @@ -604,7 +606,7 @@ func newCommitBlobAndLeaves(pendingFunding *pendingAssetFunding, fakePrevState, lndOpenChan, assetOpenChan, whoseCommit, localSatBalance, remoteSatBalance, fakeView, pendingFunding.chainParams, keyRing.GetForParty(whoseCommit), - stxo, + stxo, sigHashDefault, ) if err != nil { return nil, lnwallet.CommitAuxLeaves{}, err @@ -647,14 +649,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, + p.stxo, p.sigHashDefault, ) if err != nil { return nil, err } remoteCommitBlob, remoteAuxLeaves, err := newCommitBlobAndLeaves( p, req.openChan, openChanDesc, req.keyRing, lntypes.Remote, - p.stxo, + p.stxo, p.sigHashDefault, ) if err != nil { return nil, err @@ -1806,6 +1808,9 @@ func (f *FundingController) processFundingReq(fundingFlows fundingFlowIndex, supportSTXO := features.HasFeature(tapfeatures.STXOOptional) fundingState.stxo = supportSTXO + fundingState.sigHashDefault = features.HasFeature( + tapfeatures.DeterministicHTLCsOptional, + ) // Now that we know the final funding asset root along with the splits, // we can derive the tapscript root that'll be used alongside the diff --git a/tapchannel/aux_leaf_creator.go b/tapchannel/aux_leaf_creator.go index bbf0737ea1..e29046fadd 100644 --- a/tapchannel/aux_leaf_creator.go +++ b/tapchannel/aux_leaf_creator.go @@ -68,11 +68,14 @@ func FetchLeavesFromView(chainParams *address.ChainParams, ) supportsSTXO := features.HasFeature(tapfeatures.STXOOptional) + sigHashDefault := features.HasFeature( + tapfeatures.DeterministicHTLCsOptional, + ) allocations, newCommitment, err := GenerateCommitmentAllocations( prevState, in.ChannelState, chanAssetState, in.WhoseCommit, in.OurBalance, in.TheirBalance, in.UnfilteredView, chainParams, - in.KeyRing, supportsSTXO, + in.KeyRing, supportsSTXO, sigHashDefault, ) if err != nil { return lfn.Err[returnType](fmt.Errorf("unable to generate "+ @@ -221,8 +224,13 @@ func FetchLeavesFromCommit(chainParams *address.ChainParams, // FetchLeavesFromRevocation attempts to fetch the auxiliary leaves // from a channel revocation that stores balance + blob information. -func FetchLeavesFromRevocation( - r *channeldb.RevocationLog) lfn.Result[lnwl.CommitDiffAuxResult] { +// The additional parameters (chanState, keys, commitTx, chainParams) +// are needed to compute second-level HTLC auxiliary leaves at runtime, +// since these are not stored in the commitment blob. +func FetchLeavesFromRevocation(r *channeldb.RevocationLog, + chanState lnwl.AuxChanState, keys lnwl.CommitmentKeyRing, + commitTx *wire.MsgTx, + chainParams *address.ChainParams) lfn.Result[lnwl.CommitDiffAuxResult] { type returnType = lnwl.CommitDiffAuxResult @@ -235,13 +243,136 @@ func FetchLeavesFromRevocation( "to decode commitment: %w", err)) } + leaves := commitment.Leaves() + + // If we have the commit tx and chain params, we + // can compute the second-level HTLC aux leaves + // that aren't stored in the commitment blob. + if commitTx != nil && chainParams != nil { + err = populateSecondLevelLeaves( + r, commitment, chanState, keys, + commitTx, chainParams, &leaves, + ) + if err != nil { + return lfn.Err[returnType]( + fmt.Errorf("unable to "+ + "populate second "+ + "level leaves: %w", + err), + ) + } + } + return lfn.Ok(lnwl.CommitDiffAuxResult{ - AuxLeaves: lfn.Some(commitment.Leaves()), + AuxLeaves: lfn.Some(leaves), }) }, ) } +// populateSecondLevelLeaves computes the second-level HTLC aux leaves +// for each HTLC in the revocation log and populates them in the given +// leaves struct. This mirrors the logic in FetchLeavesFromCommit. +func populateSecondLevelLeaves(r *channeldb.RevocationLog, + commitment *cmsg.Commitment, chanState lnwl.AuxChanState, + keys lnwl.CommitmentKeyRing, commitTx *wire.MsgTx, + chainParams *address.ChainParams, + leaves *lnwl.CommitAuxLeaves) error { + + supportSTXO := commitment.STXO.Val + + 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 _, htlcEntry := range r.HTLCEntries { + // Skip HTLCs without an index. + htlcIdxOpt := htlcEntry.HtlcIndex.ValOpt() + if htlcIdxOpt.IsNone() { + continue + } + + htlcIdx := htlcIdxOpt.UnsafeFromSome().Int() + htlcAmt := htlcEntry.Amt.Val.Int() + + if htlcEntry.Incoming.Val { + htlcOutputs := incomingHtlcs[htlcIdx].Outputs + auxLeaf := incomingHtlcLeaves[htlcIdx].AuxLeaf + + if len(htlcOutputs) == 0 { + continue + } + + // For incoming HTLCs on the remote party's + // commitment, they'll need to go to the second + // level to time it out. + cltvTimeout := fn.Some( + htlcEntry.RefundTimeout.Val, + ) + + leaf, err := CreateSecondLevelHtlcTx( + chanState, commitTx, htlcAmt, + keys, chainParams, htlcOutputs, + cltvTimeout, htlcIdx, supportSTXO, + ) + if err != nil { + return fmt.Errorf("unable to create "+ + "second level incoming HTLC "+ + "leaf: %w", err) + } + + existingLeaf := lfn.MapOption( + func(l cmsg.TapLeafRecord) txscript.TapLeaf { + return l.Leaf + }, + )(auxLeaf.ValOpt()) + + leaves.IncomingHtlcLeaves[htlcIdx] = input.HtlcAuxLeaf{ + AuxTapLeaf: existingLeaf, + SecondLevelLeaf: leaf, + } + } else { + htlcOutputs := outgoingHtlcs[htlcIdx].Outputs + auxLeaf := outgoingHtlcLeaves[htlcIdx].AuxLeaf + + if len(htlcOutputs) == 0 { + continue + } + + // For outgoing HTLCs on the remote party's + // commitment, they don't need a CLTV timeout + // (they go to second level via the success path). + leaf, err := CreateSecondLevelHtlcTx( + chanState, commitTx, htlcAmt, + keys, chainParams, htlcOutputs, + fn.None[uint32](), htlcIdx, + supportSTXO, + ) + if err != nil { + return fmt.Errorf("unable to create "+ + "second level outgoing HTLC "+ + "leaf: %w", err) + } + + existingLeaf := lfn.MapOption( + func(l cmsg.TapLeafRecord) txscript.TapLeaf { + return l.Leaf + }, + )(auxLeaf.ValOpt()) + + leaves.OutgoingHtlcLeaves[htlcIdx] = input.HtlcAuxLeaf{ + AuxTapLeaf: existingLeaf, + SecondLevelLeaf: leaf, + } + } + } + + return 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. @@ -279,11 +410,14 @@ func ApplyHtlcView(chainParams *address.ChainParams, supportSTXO := features.HasFeature( tapfeatures.STXOOptional, ) + sigHashDefault := features.HasFeature( + tapfeatures.DeterministicHTLCsOptional, + ) _, newCommitment, err := GenerateCommitmentAllocations( prevState, in.ChannelState, chanAssetState, in.WhoseCommit, in.OurBalance, in.TheirBalance, in.UnfilteredView, chainParams, - in.KeyRing, supportSTXO, + in.KeyRing, supportSTXO, sigHashDefault, ) if err != nil { return lfn.Err[returnType](fmt.Errorf("unable to generate "+ diff --git a/tapchannel/aux_leaf_signer.go b/tapchannel/aux_leaf_signer.go index 4ad34bea50..72ddb0e6f8 100644 --- a/tapchannel/aux_leaf_signer.go +++ b/tapchannel/aux_leaf_signer.go @@ -24,7 +24,6 @@ import ( "github.com/lightninglabs/taproot-assets/vm" lfn "github.com/lightningnetwork/lnd/fn/v2" "github.com/lightningnetwork/lnd/input" - "github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/lnwallet" "github.com/lightningnetwork/lnd/lnwallet/btcwallet" "github.com/lightningnetwork/lnd/lnwire" @@ -235,8 +234,12 @@ func VerifySecondLevelSigs(chainParams *address.ChainParams, verifyJobs[idx].BaseAuxJob, ) if err != nil { - return fmt.Errorf("error verifying second level sig: "+ - "%w", err) + return fmt.Errorf("error verifying second "+ + "level sig (idx=%d htlcIdx=%d "+ + "incoming=%v whoseCommit=%v): %w", + idx, verifyJob.HTLC.HtlcIndex, + verifyJob.Incoming, + verifyJob.WhoseCommit, err) } } @@ -302,7 +305,12 @@ func (s *AuxLeafSigner) processAuxSigBatch(chanState lnwallet.AuxChanState, htlcs = com.OutgoingHtlcAssets.Val.HtlcOutputs htlcOutputs []*cmsg.AssetOutput ) - if sigJob.Incoming { + // Use IncomingHTLCLookup (not Incoming) to find the + // asset outputs. Incoming controls the script variant, + // while IncomingHTLCLookup controls which HTLC asset + // list to search. These differ for revocation self- + // signing where the Incoming flag is flipped. + if sigJob.IncomingHTLCLookup { htlcs = com.IncomingHtlcAssets.Val.HtlcOutputs } for outIndex := range htlcs { @@ -329,6 +337,9 @@ func (s *AuxLeafSigner) processAuxSigBatch(chanState lnwallet.AuxChanState, } } + // NOTE: The blob's stored script keys are already correct + // for the commitment being revoked. No override needed. + resp, err := s.generateHtlcSignature( chanState, commitTx, htlcOutputs, sigJob.SignDesc, sigJob.BaseAuxJob, @@ -361,11 +372,14 @@ func verifyHtlcSignature(chainParams *address.ChainParams, keyRing lnwallet.CommitmentKeyRing, sigs []*cmsg.AssetSig, htlcOutputs []*cmsg.AssetOutput, baseJob lnwallet.BaseAuxJob) error { - // If we're validating a signature for an outgoing HTLC, then it's an - // outgoing HTLC for the remote party, so we'll need to sign it with the - // proper lock time. + // Determine the timeout. If explicit timeout is set (revocation + // verification), use it. Otherwise derive from Incoming. var htlcTimeout fn.Option[uint32] - if !baseJob.Incoming { + if v, err := baseJob.HtlcTimeout.UnwrapOrErr( + fmt.Errorf("no timeout"), + ); err == nil { + htlcTimeout = fn.Some(v) + } else if !baseJob.Incoming { htlcTimeout = fn.Some(baseJob.HTLC.Timeout) } @@ -402,9 +416,10 @@ func verifyHtlcSignature(chainParams *address.ChainParams, return err } - // We are always verifying the signature of the remote party, - // which are for our commitment transaction. - const whoseCommit = lntypes.Local + // Use the WhoseCommit field from the job. For the normal + // CommitSig verification flow this is Local (verifying + // remote's sigs on our commitment). + whoseCommit := baseJob.WhoseCommit htlcScript, err := lnwallet.GenTaprootHtlcScript( baseJob.Incoming, whoseCommit, baseJob.HTLC.Timeout, @@ -416,8 +431,10 @@ func verifyHtlcSignature(chainParams *address.ChainParams, "verify second level: %w", err) } + wsToSign := htlcScript.WitnessScriptToSign() + leafToVerify := txscript.TapLeaf{ - Script: htlcScript.WitnessScriptToSign(), + Script: wsToSign, LeafVersion: txscript.BaseLeafVersion, } validator := &schnorrSigValidator{ @@ -438,52 +455,67 @@ func verifyHtlcSignature(chainParams *address.ChainParams, // applySignDescToVIn applies the sign descriptor to the virtual input. This // entails updating all the input bip32, taproot, and witness fields with the // information from the sign descriptor. This function returns the public key -// that should be used to verify the generated signature, and also the leaf to -// be signed. +// that should be used to verify the generated signature. For scriptspend, it +// also returns the leaf to be signed. For breach scenarios (keyspend), the +// leaf will be empty. func applySignDescToVIn(signDesc input.SignDescriptor, vIn *tappsbt.VInput, chainParams *address.ChainParams, tapscriptRoot []byte) (btcec.PublicKey, txscript.TapLeaf) { - leafToSign := txscript.TapLeaf{ - Script: signDesc.WitnessScript, - LeafVersion: txscript.BaseLeafVersion, - } - vIn.TaprootLeafScript = []*psbt.TaprootTapLeafScript{ - { - Script: leafToSign.Script, - LeafVersion: leafToSign.LeafVersion, - }, - } + var leafToSign txscript.TapLeaf + // Detect breach scenario: both tweaks present means revocation + // (DoubleTweak) + HTLC index (SingleTweak). In normal force + // close, only one tweak is set. See also aux_sweeper.go which + // uses len(ctrlBlock)==0 for the same detection when the + // control block is available. + isBreach := len(signDesc.SingleTweak) > 0 && + signDesc.DoubleTweak != nil + + // Set up derivation paths for the key. deriv, trDeriv := tappsbt.Bip32DerivationFromKeyDesc( signDesc.KeyDesc, chainParams.HDCoinType, ) vIn.Bip32Derivation = []*psbt.Bip32Derivation{deriv} - vIn.TaprootBip32Derivation = []*psbt.TaprootBip32Derivation{ - trDeriv, - } - vIn.TaprootBip32Derivation[0].LeafHashes = [][]byte{ - fn.ByteSlice(leafToSign.TapHash()), + vIn.TaprootBip32Derivation = []*psbt.TaprootBip32Derivation{trDeriv} + + if !isBreach { + // For normal sweeps (scriptspend), set up the leaf script. + leafToSign = txscript.TapLeaf{ + Script: signDesc.WitnessScript, + LeafVersion: txscript.BaseLeafVersion, + } + vIn.TaprootLeafScript = []*psbt.TaprootTapLeafScript{ + { + Script: leafToSign.Script, + LeafVersion: leafToSign.LeafVersion, + }, + } + vIn.TaprootBip32Derivation[0].LeafHashes = [][]byte{ + fn.ByteSlice(leafToSign.TapHash()), + } } + vIn.SighashType = signDesc.HashType vIn.TaprootMerkleRoot = tapscriptRoot - // Apply single or double tweaks if present in the sign - // descriptor. At the same time, we apply the tweaks to a copy - // of the public key, so we can validate the produced signature. + // Apply single or double tweaks if present in the sign descriptor. At + // the same time, we apply the tweaks to a copy of the public key, so we + // can validate the produced signature. + // + // For breach scenarios, both DoubleTweak and SingleTweak are present. + // Both are added to the PSBT unknowns keyed by their type, so the + // append order here doesn't matter — the signer identifies them by + // key type, not position. However, when deriving the verification + // public key below, we must apply DoubleTweak (revocation) before + // SingleTweak (HTLC index) because DeriveRevocationPubkey hashes + // its input key, making the operations non-commutative. + // + // For normal force closes, only one tweak is present at a time. signingKey := signDesc.KeyDesc.PubKey - if len(signDesc.SingleTweak) > 0 { - key := btcwallet.PsbtKeyTypeInputSignatureTweakSingle - vIn.Unknowns = append(vIn.Unknowns, &psbt.Unknown{ - Key: key, - Value: signDesc.SingleTweak, - }) - signingKey = input.TweakPubKeyWithTweak( - signingKey, signDesc.SingleTweak, - ) - } - if signDesc.DoubleTweak != nil { + if isBreach { + // Breach scenario: set both tweaks in PSBT unknowns. key := btcwallet.PsbtKeyTypeInputSignatureTweakDouble vIn.Unknowns = append(vIn.Unknowns, &psbt.Unknown{ Key: key, @@ -493,6 +525,41 @@ func applySignDescToVIn(signDesc input.SignDescriptor, vIn *tappsbt.VInput, signingKey = input.DeriveRevocationPubkey( signingKey, signDesc.DoubleTweak.PubKey(), ) + + key = btcwallet.PsbtKeyTypeInputSignatureTweakSingle + vIn.Unknowns = append(vIn.Unknowns, &psbt.Unknown{ + Key: key, + Value: signDesc.SingleTweak, + }) + + signingKey = input.TweakPubKeyWithTweak( + signingKey, signDesc.SingleTweak, + ) + } else { + // Normal force close: Apply tweaks in the original order. + // Apply SingleTweak first (if present), then DoubleTweak. + if len(signDesc.SingleTweak) > 0 { + key := btcwallet.PsbtKeyTypeInputSignatureTweakSingle + vIn.Unknowns = append(vIn.Unknowns, &psbt.Unknown{ + Key: key, + Value: signDesc.SingleTweak, + }) + + signingKey = input.TweakPubKeyWithTweak( + signingKey, signDesc.SingleTweak, + ) + } + if signDesc.DoubleTweak != nil { + key := btcwallet.PsbtKeyTypeInputSignatureTweakDouble + vIn.Unknowns = append(vIn.Unknowns, &psbt.Unknown{ + Key: key, + Value: signDesc.DoubleTweak.Serialize(), + }) + + signingKey = input.DeriveRevocationPubkey( + signingKey, signDesc.DoubleTweak.PubKey(), + ) + } } return *signingKey, leafToSign @@ -505,11 +572,15 @@ func (s *AuxLeafSigner) generateHtlcSignature(chanState lnwallet.AuxChanState, signDesc input.SignDescriptor, baseJob lnwallet.BaseAuxJob) (lnwallet.AuxSigJobResp, error) { - // If we're generating a signature for an incoming HTLC, then it's an - // outgoing HTLC for the remote party, so we'll need to sign it with the - // proper lock time. + // Determine the timeout for the second-level tx. If an explicit + // timeout is set on the job (revocation signing), use it + // directly. Otherwise derive from Incoming (normal CommitSig). var htlcTimeout fn.Option[uint32] - if baseJob.Incoming { + if v, err := baseJob.HtlcTimeout.UnwrapOrErr( + fmt.Errorf("no timeout"), + ); err == nil { + htlcTimeout = fn.Some(v) + } else if baseJob.Incoming { htlcTimeout = fn.Some(baseJob.HTLC.Timeout) } @@ -522,9 +593,10 @@ func (s *AuxLeafSigner) generateHtlcSignature(chanState lnwallet.AuxChanState, "second level packets: %w", err) } - // We are always signing the commitment transaction of the remote party, - // which is why we set whoseCommit to remote. - const whoseCommit = lntypes.Remote + // Use the WhoseCommit field from the job to determine which + // party's commitment we're signing. For the normal CommitSig + // flow this is Remote; for revocation self-signing this is Local. + whoseCommit := baseJob.WhoseCommit htlcScript, err := lnwallet.GenTaprootHtlcScript( baseJob.Incoming, whoseCommit, baseJob.HTLC.Timeout, @@ -538,6 +610,12 @@ func (s *AuxLeafSigner) generateHtlcSignature(chanState lnwallet.AuxChanState, tapscriptRoot := htlcScript.TapscriptRoot + // Note: signDesc.WitnessScript comes from the SignJob (BTC-level). + // For normal CommitSig, it matches htlcScript.WitnessScriptToSign(). + // For revocation self-signing, it may differ (Local vs Remote + // perspective). The signDesc's leaf is what the signer actually + // signs over. + var sigs []*cmsg.AssetSig for _, vPacket := range vPackets { vIn := vPacket.Inputs[0] diff --git a/tapchannel/aux_sweeper.go b/tapchannel/aux_sweeper.go index 5af5255b8f..dfcc2599ac 100644 --- a/tapchannel/aux_sweeper.go +++ b/tapchannel/aux_sweeper.go @@ -17,6 +17,7 @@ import ( "github.com/btcsuite/btcd/wire" "github.com/lightninglabs/taproot-assets/address" "github.com/lightninglabs/taproot-assets/asset" + "github.com/lightninglabs/taproot-assets/commitment" "github.com/lightninglabs/taproot-assets/fn" "github.com/lightninglabs/taproot-assets/proof" "github.com/lightninglabs/taproot-assets/tapchannelmsg" @@ -26,6 +27,7 @@ import ( "github.com/lightninglabs/taproot-assets/tappsbt" "github.com/lightninglabs/taproot-assets/tapscript" "github.com/lightninglabs/taproot-assets/tapsend" + "github.com/lightningnetwork/lnd/channeldb" lfn "github.com/lightningnetwork/lnd/fn/v2" "github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/keychain" @@ -88,6 +90,9 @@ type broadcastReq struct { // sure we make proofs properly for the pre-signed HTLC transactions. outpointToTxIndex map[wire.OutPoint]int + // opts contains the notify options from the caller. + opts sweep.AuxNotifyOpts + // resp is the error result of the broadcast. resp chan error } @@ -323,7 +328,7 @@ func (a *AuxSweeper) createSweepVpackets(sweepInputs []*cmsg.AssetOutput, } for outIdx := range vPackets[idx].Outputs { - //nolint:lll + //nolint:llll vPackets[idx].Outputs[outIdx].ProofDeliveryAddress = courierAddr } } @@ -351,30 +356,82 @@ func (a *AuxSweeper) signSweepVpackets(vPackets []*tappsbt.VPacket, // single asset from our commitment output. vIn := vPacket.Inputs[0] - // Next, we'll apply the sign desc to the vIn, setting the PSBT - // specific fields. Along the way, we'll apply any relevant - // tweaks to generate the key we'll use to verify the - // signature. - signingKey, leafToSign := applySignDescToVIn( - signDesc, vIn, &a.cfg.ChainParams, tapTweak, + // Detect breach scenario: no control block means keyspend + // (revocation). Normal force close uses scriptspend + // (control block present). See also aux_leaf_signer.go + // which uses DoubleTweak+SingleTweak for the same + // detection when the control block isn't available. + isBreach := len(ctrlBlock) == 0 + + var ( + signingKey btcec.PublicKey + leafToSign txscript.TapLeaf + signMethod input.SignMethod + tapLeafOpt lfn.Option[txscript.TapLeaf] ) - // In this case, the witness isn't special, so we'll set the - // control block now for it. - vIn.TaprootLeafScript[0].ControlBlock = ctrlBlock + if isBreach { + // For breach scenarios (HTLC revocations), the + // common function applies both DoubleTweak + // (revocation) and SingleTweak (HTLC index) to + // derive the private key for signing. We discard + // the returned signingKey because for breach + // verification we use the asset's script key + // instead (see below). + _, leafToSign = applySignDescToVIn( + signDesc, vIn, &a.cfg.ChainParams, tapTweak, + ) + + // For keyspend, we need to verify the signature against + // the asset's script key, not the derived signing key. + // The asset script key was set during commitment + // creation and incorporates both the revocation key + // derivation and the HTLC index tweak via + // TweakHtlcTree(), which also recomputes the taproot + // output key with the script root. + inputAsset := vIn.Asset() + signingKey = *inputAsset.ScriptKey.PubKey + signMethod = input.TaprootKeySpendSignMethod + + tapLeafOpt = lfn.None[txscript.TapLeaf]() + } else { + // For normal force close sweeps, we use scriptspend. + signingKey, leafToSign = applySignDescToVIn( + signDesc, vIn, &a.cfg.ChainParams, tapTweak, + ) + + // This is a normal scriptspend (not a breach + // keyspend), so set the control block that + // applySignDescToVIn prepared. + vIn.TaprootLeafScript[0].ControlBlock = ctrlBlock + + signMethod = input.TaprootScriptSpendSignMethod + + tapLeafOpt = lfn.Some(leafToSign) + } - log.Debugf("signing vPacket for input=%v", - limitSpewer.Sdump(vIn.PrevID)) + log.Tracef("signing vPacket[%d]: isBreach=%v, "+ + "signMethod=%v, signingKey=%x, "+ + "inputScriptKey=%x, tapTweak=%x, "+ + "singleTweak=%x, doubleTweak=%v", + vPktIndex, isBreach, + signMethod, + signingKey.SerializeCompressed(), + vIn.Asset().ScriptKey.PubKey. + SerializeCompressed(), + tapTweak, + signDesc.SingleTweak, + signDesc.DoubleTweak != nil) // With everything set, we can now sign the new leaf we'll // sweep into. ctxb := context.Background() signed, err := a.cfg.Signer.SignVirtualPacket( - ctxb, vPacket, tapfreighter.SkipInputProofVerify(), + ctxb, vPacket, tapfreighter.WithValidator(&schnorrSigValidator{ pubKey: signingKey, - tapLeaf: lfn.Some(leafToSign), - signMethod: input.TaprootScriptSpendSignMethod, + tapLeaf: tapLeafOpt, + signMethod: signMethod, }), ) if err != nil { @@ -406,7 +463,7 @@ func (a *AuxSweeper) signSweepVpackets(vPackets []*tappsbt.VPacket, // With the sig obtained, we'll now insert the // signature at the specified index. - //nolint:lll + //nolint:llll sigIndex, err := secondLevelSigIndex.UnwrapOrErr( fmt.Errorf("no sig index"), ) @@ -421,7 +478,7 @@ func (a *AuxSweeper) signSweepVpackets(vPackets []*tappsbt.VPacket, newAsset := vPacket.Outputs[0].Asset - //nolint:lll + //nolint:llll prevWitness := newAsset.PrevWitnesses[0].TxWitness prevWitness = slices.Insert( prevWitness, int(sigIndex), auxSigBytes, @@ -551,6 +608,32 @@ func (a *AuxSweeper) createAndSignSweepVpackets( }, )(desc.auxSigInfo).UnwrapOr(resReq.SignDesc) + // For HTLC revocation sweeps (breach scenarios), we need to + // apply the HTLC index tweak to the SignDesc. This is indicated + // by an empty control block (keyspend path). The HTLC index is + // passed in resReq.HtlcID. This tweak is applied at the ASSET + // level only (not Bitcoin level). + // + // IMPORTANT: Only apply this for breach scenarios, NOT for + // normal force close HTLC sweeps. + isBreach := len(desc.ctrlBlockBytes) == 0 + if isBreach { + // For breach scenarios, the HTLC ID must be present + // to compute the single tweak. + htlcID, err := resReq.HtlcID.UnwrapOrErr(errNoHtlcID) + if err != nil { + return lfn.Err[returnType](err) + } + + // Derive the single tweak from the HTLC index, + // using the same function used during commitment + // creation to ensure consistency. + tweakScalar := ScriptKeyTweakFromHtlcIndex(htlcID) + var singleTweak [32]byte + tweakScalar.PutBytesUnchecked(singleTweak[:]) + signDesc.SingleTweak = singleTweak[:] + } + err := a.signSweepVpackets( vPkts, signDesc, desc.scriptTree.TapTweak(), desc.ctrlBlockBytes, desc.auxSigInfo, @@ -907,7 +990,7 @@ func localHtlcTimeoutSweepDesc(req lnwallet.ResolutionReq, ctrlBlockBytes: ctrlBlockBytes, relativeDelay: lfn.Some(uint64(req.CsvDelay)), absoluteDelay: lfn.Some(uint64(htlcExpiry)), - auxSigInfo: req.AuxSigDesc, + auxSigInfo: breachAuxSigInfo(req), secondLevelSigIndex: sigIndex, }, secondLevel: lfn.Some(secondLevelDesc), @@ -1008,13 +1091,172 @@ func localHtlcSuccessSweepDesc(req lnwallet.ResolutionReq, scriptTree: htlcScriptTree, ctrlBlockBytes: ctrlBlockBytes, relativeDelay: lfn.Some(uint64(req.CsvDelay)), - auxSigInfo: req.AuxSigDesc, + auxSigInfo: breachAuxSigInfo(req), secondLevelSigIndex: sigIndex, }, secondLevel: lfn.Some(secondLevelDesc), }) } +// tweakHtlcScriptTree applies the HTLC index tweak to the script tree's +// internal key, returning a new HtlcScriptTree with the tweaked keys but +// the original leaves and tapscript structure preserved. +func tweakHtlcScriptTree(tree *input.HtlcScriptTree, + index input.HtlcIndex) *input.HtlcScriptTree { + + tweakedTree := TweakHtlcTree(tree.ScriptTree, index) + + return &input.HtlcScriptTree{ + ScriptTree: input.ScriptTree{ + InternalKey: tweakedTree.InternalKey, + TaprootKey: tweakedTree.TaprootKey, + TapscriptTree: tree.TapscriptTree, + TapscriptRoot: tree.TapscriptRoot, + }, + SuccessTapLeaf: tree.SuccessTapLeaf, + TimeoutTapLeaf: tree.TimeoutTapLeaf, + } +} + +// htlcOfferedRevokeSweepDesc creates a sweep descriptor for a revoked HTLC +// where htlc.Incoming=false in the remote's commitment log (meaning we're +// sending to them). We use the revocation key to keyspend immediately. +// +// IMPORTANT: Like all other HTLC sweep descriptors, we must use a TWEAKED +// keyring where the RevocationKey has the HTLC index tweak applied. This +// matches how the HTLC was created during commitment generation. +func htlcOfferedRevokeSweepDesc(originalKeyRing *lnwallet.CommitmentKeyRing, + payHash []byte, htlcExpiry uint32, + index input.HtlcIndex) lfn.Result[tapscriptSweepDescs] { + + type returnType = tapscriptSweepDescs + + // IMPORTANT: We must match the creation flow exactly: + // 1. Create script tree with UNTWEAKED keyring + // 2. Then apply HTLC index tweak to the tree's internal key + htlcScriptTree, err := input.ReceiverHTLCScriptTaproot( + htlcExpiry, originalKeyRing.LocalHtlcKey, + originalKeyRing.RemoteHtlcKey, originalKeyRing.RevocationKey, + payHash, lntypes.Remote, input.NoneTapLeaf(), + ) + if err != nil { + return lfn.Err[returnType](err) + } + + // Apply the HTLC index tweak to the tree, matching how HTLCs are + // created in commitment.go. + tweakedHtlcTree := tweakHtlcScriptTree(htlcScriptTree, index) + + // For revoked HTLCs, we use keyspend (not scriptspend), so we don't + // need a control block. The revocation key spend path allows immediate + // sweep without CSV delays. + return lfn.Ok(tapscriptSweepDescs{ + firstLevel: tapscriptSweepDesc{ + scriptTree: tweakedHtlcTree, + }, + }) +} + +// htlcAcceptedRevokeSweepDesc creates a sweep descriptor for a revoked HTLC +// that was accepted by the remote party (incoming from their perspective). We +// use the revocation key to keyspend immediately. +// +// IMPORTANT: Like all other HTLC sweep descriptors, we must use a TWEAKED +// keyring where the RevocationKey has the HTLC index tweak applied. This +// matches how the HTLC was created during commitment generation. +func htlcAcceptedRevokeSweepDesc(originalKeyRing *lnwallet.CommitmentKeyRing, + payHash []byte, index input.HtlcIndex) lfn.Result[tapscriptSweepDescs] { + + type returnType = tapscriptSweepDescs + + // IMPORTANT: We must match the creation flow exactly: + // 1. Create script tree with UNTWEAKED keyring + // 2. Then apply HTLC index tweak to the tree's internal key + // + // During creation, GenTaprootHtlcScript is called with the untweaked + // keyring, then TweakHtlcTree applies the index tweak. We must do + // the same here. + // + // IMPORTANT: We must match the creation flow exactly: + // 1. Create script tree with UNTWEAKED keyring + // 2. Then apply HTLC index tweak to the tree's internal key + htlcScriptTree, err := input.SenderHTLCScriptTaproot( + originalKeyRing.RemoteHtlcKey, originalKeyRing.LocalHtlcKey, + originalKeyRing.RevocationKey, payHash, lntypes.Remote, + input.NoneTapLeaf(), + ) + if err != nil { + return lfn.Err[returnType](err) + } + + // Apply the HTLC index tweak to the tree, matching how HTLCs are + // created in commitment.go. + tweakedHtlcTree := tweakHtlcScriptTree(htlcScriptTree, index) + + // For revoked HTLCs, we use keyspend (not scriptspend), so we don't + // need a control block. The revocation key spend path allows immediate + // sweep without CSV delays. + return lfn.Ok(tapscriptSweepDescs{ + firstLevel: tapscriptSweepDesc{ + scriptTree: tweakedHtlcTree, + }, + }) +} + +// htlcSecondLevelRevokeSweepDesc creates a sweep descriptor for a revoked +// second-level HTLC transaction. The revocation key is the internal key of +// the second-level script tree, so we sweep via keyspend (no control block +// needed), matching the LND-side TaprootHtlcSpendRevoke witness generation. +// +// IMPORTANT: Like all other HTLC sweep descriptors, we must use a TWEAKED +// keyring where the RevocationKey has the HTLC index tweak applied. This +// matches how the HTLC was created during commitment generation. +func htlcSecondLevelRevokeSweepDesc( + originalKeyRing *lnwallet.CommitmentKeyRing, csvDelay uint32, + index input.HtlcIndex) lfn.Result[tapscriptSweepDescs] { + + type returnType = tapscriptSweepDescs + + // IMPORTANT: We must match the creation flow exactly: + // 1. Create script tree with UNTWEAKED keyring and NO aux leaf + // 2. Then apply HTLC index tweak to the tree's internal key + // + // The aux leaf is intentionally omitted here. During commitment + // generation, the ASSET-level script key is derived from the tree + // WITHOUT the aux leaf (createSecondLevelHtlcAllocations passes + // None). The aux leaf only affects the BTC-level on-chain output, + // not the asset-level script key derivation. + secondLevelScriptTree, err := input.TaprootSecondLevelScriptTree( + originalKeyRing.RevocationKey, originalKeyRing.ToLocalKey, + csvDelay, lfn.None[txscript.TapLeaf](), + ) + if err != nil { + return lfn.Err[returnType](err) + } + + // Now apply the HTLC index tweak to the tree, matching how HTLCs + // are created in commitment.go. + tweakedTree := TweakHtlcTree(secondLevelScriptTree.ScriptTree, index) + + // Create a new SecondLevelScriptTree with the tweaked keys. + tweakedScriptTree := &input.SecondLevelScriptTree{ + ScriptTree: input.ScriptTree{ + InternalKey: tweakedTree.InternalKey, + TaprootKey: tweakedTree.TaprootKey, + TapscriptTree: secondLevelScriptTree.TapscriptTree, + TapscriptRoot: secondLevelScriptTree.TapscriptRoot, + }, + } + + // For second-level revocations, we use keyspend (the internal key + // is the revocation key), so no control block is needed. + return lfn.Ok(tapscriptSweepDescs{ + firstLevel: tapscriptSweepDesc{ + scriptTree: tweakedScriptTree, + }, + }) +} + // assetOutputToVPacket converts an asset outputs to the corresponding vPackets // that can be used to complete the proof needed to import a commitment // transaction. This new vPacket is added to the specified map. @@ -1070,15 +1312,20 @@ func assetOutputToVPacket(fundingInputProofs map[asset.ID]*proof.Proof, Interactive: true, AnchorOutputIndex: inclusionProof.OutputIndex, AnchorOutputInternalKey: inclusionProof.InternalKey, - //nolint:lll + //nolint:llll AnchorOutputTapscriptSibling: inclusionProof.CommitmentProof.TapSiblingPreimage, ScriptKey: scriptKey, ProofSuffix: &assetProof, } - // While we're here, we'll also replace the transaction stored in the - // proof with the correct one. + // Replace the transaction stored in the proof with the real + // commitment tx, and set PrevOut to the funding outpoint (the + // input the commitment tx spends). The virtual proof from the + // commitment blob has placeholder values for these fields. vOut.ProofSuffix.AnchorTx = *commitTx + if len(commitTx.TxIn) > 0 { + vOut.ProofSuffix.PrevOut = commitTx.TxIn[0].PreviousOutPoint + } // Finally, we'll set the delivery address to the default courier, so // we publish the proof in the specified Universe. @@ -1637,7 +1884,7 @@ func (a *AuxSweeper) importCommitTx(req lnwallet.ResolutionReq, return err } } - //nolint:lll + //nolint:llll for _, outgoingHTLCs := range commitState.OutgoingHtlcAssets.Val.HtlcOutputs { for _, outgoingHTLC := range outgoingHTLCs.Outputs { err := assetOutputToVPacket( @@ -1650,7 +1897,7 @@ func (a *AuxSweeper) importCommitTx(req lnwallet.ResolutionReq, } } } - //nolint:lll + //nolint:llll for _, incomingHTLCs := range commitState.IncomingHtlcAssets.Val.HtlcOutputs { for _, incomingHTLC := range incomingHTLCs.Outputs { err := assetOutputToVPacket( @@ -1729,15 +1976,513 @@ func (a *AuxSweeper) importCommitTx(req lnwallet.ResolutionReq, heightHint = fn.Some(req.CommitTxBlockHeight) } + // We set the skipBroadcast flag because this is called after a force + // close - the commitment transaction is already confirmed on-chain. return shipChannelTxn( a.cfg.TxSender, req.CommitTx, outCommitments, vPackets, - int64(req.CommitFee), heightHint, + int64(req.CommitFee), heightHint, true, + ) +} + +// breachAuxSigInfo returns the AuxSigDesc for non-breach resolution +// requests. For breach cases (Breach close type), AuxSigDesc contains +// the HTLC-level sig for the proof import — NOT for the justice sweep. +// Passing it to the sweep descriptor would corrupt the keyspend witness. +func breachAuxSigInfo( + req lnwallet.ResolutionReq) lfn.Option[lnwallet.AuxSigDesc] { + + if req.CloseType == lnwallet.Breach { + return lfn.None[lnwallet.AuxSigDesc]() + } + + return req.AuxSigDesc +} + +// verifyAuxSigCandidate checks whether a remote schnorr signature +// is valid for a given candidate witness, leaf script, and virtual +// transaction components. Returns true if the signature verifies +// against the first key in the 2-of-2 tapscript leaf. +func verifyAuxSigCandidate(candidate wire.TxWitness, + remoteSigBytes []byte, vIn *tappsbt.VInput, + newAsset *asset.Asset) bool { + + if len(candidate) < 4 { + return false + } + + leafScript := candidate[len(candidate)-2] + + // 2-of-2 CHECKSIGVERIFY/CHECKSIG tapscript: + // <0x20><32-byte-key><0xad><0x20><32-byte-key><0xac> + // = 1+32+1+1+32+1 = 68 bytes. + if len(leafScript) != 68 || len(remoteSigBytes) != 64 { + return false + } + + key1Bytes := leafScript[1:33] + key1, k1Err := schnorr.ParsePubKey(key1Bytes) + sig, sErr := schnorr.ParseSignature(remoteSigBytes) + if k1Err != nil || sErr != nil { + return false + } + + tapLeaf := txscript.NewBaseTapLeaf(leafScript) + prevAsset := vIn.Asset() + pof, pfErr := tapscript.InputPrevOutFetcher( + *prevAsset, + ) + if pfErr != nil { + return false + } + + prevID := newAsset.PrevWitnesses[0].PrevID + virtualTx, _, vtErr := tapscript.VirtualTx( + newAsset, commitment.InputSet{ + *prevID: prevAsset, + }, + ) + if vtErr != nil { + return false + } + + vtCopy := asset.VirtualTxWithInput( + virtualTx, newAsset.LockTime, + newAsset.RelativeLockTime, 0, candidate, + ) + sh := txscript.NewTxSigHashes(vtCopy, pof) + sigHash, shErr := txscript.CalcTapscriptSignaturehash( + sh, txscript.SigHashDefault, vtCopy, 0, + pof, tapLeaf, + ) + if shErr != nil { + return false + } + + return sig.Verify(sigHash, key1) +} + +// signSecondLevelImport constructs valid asset-level witnesses for +// second-level HTLC vPackets. If AuxSigDesc is present (breach case), +// it signs with our local HTLC key and inserts the remote party's +// pre-stored signature to produce a full 2-of-2 witness. If AuxSigDesc +// is absent, falls back to a placeholder witness. +func (a *AuxSweeper) signSecondLevelImport( + req lnwallet.ResolutionReq, + secondLevelPkts []*tappsbt.VPacket, + commitState *cmsg.Commitment) error { + + auxSigDesc, err := req.AuxSigDesc.UnwrapOrErr( + fmt.Errorf("no AuxSigDesc on resolution request"), + ) + if err != nil { + // No AuxSigDesc available — set placeholder witnesses. + log.Warnf("No AuxSigDesc for second-level import, " + + "using placeholder witnesses") + + for _, vPkt := range secondLevelPkts { + for _, vOut := range vPkt.Outputs { + if vOut.Asset == nil { + continue + } + + wErr := vOut.Asset.UpdateTxWitness( + 0, wire.TxWitness{{0x01}}, + ) + if wErr != nil { + return wErr + } + } + } + + return nil + } + + // Determine if this is an incoming HTLC by checking which + // HTLC list contains the assets. The original offered/accepted + // type is lost after convertToSecondLevelRevoke. + htlcID, idErr := req.HtlcID.UnwrapOrErr( + fmt.Errorf("no HTLC ID for second-level sign"), + ) + if idErr != nil { + return idErr + } + // The commitment state labels HTLCs from OUR perspective: + // OutgoingHtlcAssets = HTLCs we offered (outgoing from us). + // If HTLC is not in our outgoing, it's incoming to us. + outgoing := commitState.OutgoingHtlcAssets.Val + outMatch := outgoing.FilterByHtlcIndex(htlcID) + isIncoming := len(outMatch) == 0 + + // Reconstruct the HTLC script using the commitment construction + // perspective. The asset-level HTLC script key was created with + // (isIncoming, whoseCommit=Remote) and no aux leaf (see + // GenerateCommitmentAllocations). req.KeyRing is the breach-time + // keyRing from our (non-breaching) perspective, which matches the + // commitment construction keyRing. + payHash, pErr := req.PayHash.UnwrapOrErr(errNoPayHash) + if pErr != nil { + return pErr + } + + htlcTimeout := req.CltvDelay.UnwrapOr(0) + + // Build the commitment-perspective HTLC script for the control + // block and script tree. This matches the asset's script key. + htlcScript, sErr := lnwallet.GenTaprootHtlcScript( + isIncoming, lntypes.Remote, htlcTimeout, payHash, + req.KeyRing, lfn.None[txscript.TapLeaf](), + ) + if sErr != nil { + return fmt.Errorf("generating HTLC script: %w", sErr) + } + + // The AuxSig was signed by the breaching party from their LOCAL + // perspective. Both perspectives produce the SAME leaves (just + // mapped to different path names). The signer uses: + // incoming → success path (claim with preimage) + // outgoing → timeout path (reclaim after timeout) + // Both perspectives produce byte-identical leaves, so we can + // use the commitment script tree directly. We just need to + // select the correct leaf. + // Determine the spending path from the on-chain second-level + // tx. The asset-level mirrors the BTC-level: if the BTC + // witness contains a preimage (32-byte element at index 1), + // it's the success path. Otherwise it's the timeout path. + isSuccessPath := false + var preimage []byte + if req.SecondLevelTx != nil { + for _, txIn := range req.SecondLevelTx.TxIn { + if len(txIn.Witness) >= 4 && + len(txIn.Witness[1]) == 32 { + + isSuccessPath = true + preimage = txIn.Witness[1] + break + } + } + } + + var ( + scriptPath input.ScriptPath + witnessScript []byte + ) + if isSuccessPath { + scriptPath = input.ScriptPathSuccess + witnessScript = htlcScript.SuccessTapLeaf.Script + } else { + scriptPath = input.ScriptPathTimeout + witnessScript = htlcScript.TimeoutTapLeaf.Script + } + _, htlcTree, tErr := LeavesFromTapscriptScriptTree(htlcScript) + if tErr != nil { + return fmt.Errorf("extracting HTLC tree: %w", tErr) + } + + tweakedTree := TweakHtlcTree(htlcTree, htlcID) + tapscriptRoot := tweakedTree.TapscriptRoot + + ctrlBlock, cbErr := htlcScript.CtrlBlockForPath(scriptPath) + if cbErr != nil { + return fmt.Errorf("getting ctrl block: %w", cbErr) + } + ctrlBlock.InternalKey = tweakedTree.InternalKey + ctrlBlock.OutputKeyYIsOdd = tweakedTree.TaprootKey. + SerializeCompressed()[0] == 0x03 + + ctrlBlockBytes, cbErr := ctrlBlock.ToBytes() + if cbErr != nil { + return fmt.Errorf("serializing ctrl block: %w", cbErr) + } + + // Sign each vPacket with our local HTLC key and insert the + // remote party's signature. + signDesc := auxSigDesc.SignDetails.SignDesc + signDesc.WitnessScript = witnessScript + + for vPktIdx, vPkt := range secondLevelPkts { + if len(vPkt.Inputs) != 1 { + return fmt.Errorf("expected 1 input, got %d", + len(vPkt.Inputs)) + } + + vIn := vPkt.Inputs[0] + + // Set up the vInput for signing with our HTLC key. + signingKey, leafToSign := applySignDescToVIn( + signDesc, vIn, &a.cfg.ChainParams, + tapscriptRoot, + ) + + if len(vIn.TaprootLeafScript) > 0 { + vIn.TaprootLeafScript[0].ControlBlock = ctrlBlockBytes + } + + // Sign the virtual packet with our key. + ctxb := context.Background() + signed, signErr := a.cfg.Signer.SignVirtualPacket( + ctxb, vPkt, + tapfreighter.WithValidator( + &schnorrSigValidator{ + pubKey: signingKey, + tapLeaf: lfn.Some(leafToSign), + signMethod: input. + TaprootScriptSpendSignMethod, + }, + ), + ) + if signErr != nil { + return fmt.Errorf("signing vPacket %d: %w", + vPktIdx, signErr) + } + + if len(signed) != 1 || signed[0] != 0 { + return fmt.Errorf("unexpected sign result for " + + "vPacket") + } + + // Insert Dave's sig (AuxSig) into the witness. After + // SignVirtualPacket, the witness is [ourSig, script, cb]. + // We insert Dave's sig at index 1 to get: + // [ourSig, daveSig, script, cb] + // Stack pops top-first: daveSig checked by SenderKey + // CHECKSIGVERIFY, then ourSig by ReceiverKey CHECKSIG. + // Try both primary and alt AuxSigs. The virtual tx + // path may not match the BTC on-chain path due to + // how CreateSecondLevelHtlcPackets determines the + // spending direction. We try primary first, then alt. + sigCandidates := [][]byte{auxSigDesc.AuxSig} + if len(auxSigDesc.AuxSigAlt) > 0 { + sigCandidates = append( + sigCandidates, auxSigDesc.AuxSigAlt, + ) + } + + newAsset := vPkt.Outputs[0].Asset + basePrevWitness := newAsset.PrevWitnesses[0].TxWitness + + var bestWitness wire.TxWitness + for ci, sigBlob := range sigCandidates { + assetSigs, decErr := cmsg.DecodeAssetSigListRecord( + sigBlob, + ) + if decErr != nil { + log.Warnf("Decoding candidate %d: %v", + ci, decErr) + continue + } + + if vPktIdx >= len(assetSigs.Sigs) { + continue + } + + remoteSig := assetSigs.Sigs[vPktIdx] + remoteSigBytes := remoteSig.Sig.Val.RawBytes() + + // Build candidate witness. + candidate := make( + wire.TxWitness, len(basePrevWitness), + ) + copy(candidate, basePrevWitness) + candidate = slices.Insert( + candidate, 1, remoteSigBytes, + ) + if isSuccessPath && len(preimage) > 0 { + candidate = slices.Insert( + candidate, 2, preimage, + ) + } + + // Verify the remote sig against the + // leaf's first key. + sigOK := verifyAuxSigCandidate( + candidate, remoteSigBytes, + vIn, newAsset, + ) + + if sigOK { + bestWitness = candidate + break + } + } + + if bestWitness == nil { + // Fallback: use primary sig even if + // verification failed, so we get a + // diagnostic error later. + fbSigs, fbErr := cmsg. + DecodeAssetSigListRecord( + sigCandidates[0], + ) + if fbErr != nil { + return fmt.Errorf("decoding "+ + "fallback sig: %w", fbErr) + } + + if vPktIdx >= len(fbSigs.Sigs) { + return fmt.Errorf("vPkt index "+ + "%d out of range (have "+ + "%d sigs)", vPktIdx, + len(fbSigs.Sigs)) + } + + fbSigBytes := fbSigs.Sigs[vPktIdx]. + Sig.Val.RawBytes() + bestWitness = make( + wire.TxWitness, + len(basePrevWitness), + ) + copy(bestWitness, basePrevWitness) + bestWitness = slices.Insert( + bestWitness, 1, fbSigBytes, + ) + if isSuccessPath && len(preimage) > 0 { + bestWitness = slices.Insert( + bestWitness, 2, preimage, + ) + } + } + + if wErr := newAsset.UpdateTxWitness( + 0, bestWitness, + ); wErr != nil { + return fmt.Errorf("updating witness: %w", wErr) + } + } + + return nil +} + +// importSecondLevelHtlcTx imports the second-level HTLC transition +// proof into the archive with valid asset-level witnesses. +// outCommitments should be the pre-computed output commitments from +// the caller's CreateOutputCommitments call — we must NOT call +// CreateOutputCommitments again because commitPacket mutates +// vOut.AltLeaves in place (appends STXO assets), and a second call +// would produce duplicate alt leaf keys. +func (a *AuxSweeper) importSecondLevelHtlcTx( + req lnwallet.ResolutionReq, + secondLevelPkts []*tappsbt.VPacket, + secondLevelAllocs []*tapsend.Allocation, + outCommitments tappsbt.OutputCommitments, + commitState *cmsg.Commitment) error { + + secondLevelTx := req.SecondLevelTx + if secondLevelTx == nil { + return fmt.Errorf("no second-level tx provided") + } + + ctx := context.Background() + secondLevelTxHash := secondLevelTx.TxHash() + + // Check if already imported. + existingParcels, err := a.cfg.TxSender.QueryParcels( + ctx, fn.Some(secondLevelTxHash), false, + ) + if err != nil { + return fmt.Errorf("querying second-level parcels: %w", err) + } + if len(existingParcels) > 0 { + log.Infof("Second-level tx %v already imported", + secondLevelTxHash) + return nil + } + + log.Infof("Importing second-level HTLC tx %v (height=%d)", + secondLevelTxHash, req.SecondLevelTxBlockHeight) + + supportSTXO := commitState.STXO.Val + + // NOTE: We intentionally skip PrepareOutputAssets here because + // CreateSecondLevelHtlcPackets (the caller that produced + // secondLevelPkts) already calls PrepareOutputAssets. Calling it + // twice would add duplicate alt leaves and cause + // CreateOutputCommitments to fail with ErrDuplicateAltLeafKey. + + // If the AuxSigDesc is available, construct valid asset-level + // witnesses by signing with our local HTLC key and combining + // with the remote party's pre-stored signature. This makes the + // proof chain fully valid and the recovered assets spendable. + log.Infof("Signing second-level HTLC import for tx %v", + secondLevelTxHash) + if err := a.signSecondLevelImport( + req, secondLevelPkts, commitState, + ); err != nil { + return fmt.Errorf("signing second-level import: %w", err) + } + + // NOTE: We use the pre-computed outCommitments passed by the + // caller. We must NOT call CreateOutputCommitments again because + // commitPacket mutates vOut.AltLeaves in place (appends STXO + // assets). A second call would produce duplicate alt leaf keys. + // Similarly, AssignOutputCommitments was already called by the + // caller. + + var proofOpts []proof.GenOption + if !supportSTXO { + proofOpts = append(proofOpts, proof.WithNoSTXOProofs()) + } + + // Create proof suffixes. With SIGHASH_DEFAULT, the second-level + // tx is deterministic (1 output), so our allocations cover all + // outputs and exclusion proofs can be properly created. + exclusionCreator := tapsend.NonAssetExclusionProofs( + secondLevelAllocs, + ) + for idx := range secondLevelPkts { + vPkt := secondLevelPkts[idx] + for outIdx := range vPkt.Outputs { + proofSuffix, err := tapsend.CreateProofSuffixCustom( + secondLevelTx, vPkt, outCommitments, + outIdx, secondLevelPkts, + exclusionCreator, proofOpts..., + ) + if err != nil { + return fmt.Errorf("unable to create proof "+ + "suffix for output %d: %w", + outIdx, err) + } + + vPkt.Outputs[outIdx].ProofSuffix = proofSuffix + + log.Debugf("Second-level proof suffix created, "+ + "PrevOut=%v, AnchorTx=%v", + proofSuffix.PrevOut, + proofSuffix.AnchorTx.TxHash()) + } + } + + // Ship the second-level tx. It's already confirmed. + heightHint := fn.None[uint32]() + if req.SecondLevelTxBlockHeight > 0 { + heightHint = fn.Some(req.SecondLevelTxBlockHeight) + } + + var parcelOpts []tapfreighter.PreAnchoredParcelOpt + + log.Infof("Shipping second-level HTLC tx %v (height=%d)", + secondLevelTxHash, req.SecondLevelTxBlockHeight) + err = shipChannelTxn( + a.cfg.TxSender, secondLevelTx, outCommitments, + secondLevelPkts, 0, heightHint, true, + parcelOpts..., ) + if err != nil { + return fmt.Errorf("shipping second-level tx: %w", err) + } + + log.Infof("Second-level HTLC import succeeded for tx %v", + secondLevelTxHash) + + return nil } // errNoPayHash is an error returned when no payment hash is provided. var errNoPayHash = fmt.Errorf("no payment hash provided") +// errNoHtlcID is an error returned when no HTLC ID is provided for a keyspend +// scenario that requires it. +var errNoHtlcID = fmt.Errorf("no HTLC ID provided for keyspend") + // resolveContract takes in a resolution request and resolves it by creating a // serialized resolution blob that contains the virtual packets needed to sweep // the funds from the contract. @@ -1770,6 +2515,15 @@ func (a *AuxSweeper) resolveContract( return lfn.Err[returnType](err) } + // Extract the HTLC ID from the request. HTLC resolution types + // require a valid ID; commit output types don't use it. We + // unwrap here and check the error in cases that need it, + // rather than silently using a bogus index with + // FilterByHtlcIndex. + htlcID, htlcIDErr := req.HtlcID.UnwrapOrErr( + fmt.Errorf("no HTLC ID in resolution request"), + ) + var ( sweepDesc lfn.Result[tapscriptSweepDescs] assetOutputs []*cmsg.AssetOutput @@ -1821,7 +2575,9 @@ func (a *AuxSweeper) resolveContract( // assets for the remote party, which are actually the HTLCs we // sent outgoing. We only care about this particular HTLC, so // we'll filter out the rest. - htlcID := req.HtlcID.UnwrapOr(math.MaxUint64) + if htlcIDErr != nil { + return lfn.Err[returnType](htlcIDErr) + } htlcOutputs := commitState.OutgoingHtlcAssets.Val assetOutputs = htlcOutputs.FilterByHtlcIndex(htlcID) @@ -1843,7 +2599,9 @@ func (a *AuxSweeper) resolveContract( // In this case, it's an outgoing HTLC from the PoV of the // remote party, which is incoming for us. We'll only sweep this // HTLC, so we'll filter out the rest. - htlcID := req.HtlcID.UnwrapOr(math.MaxUint64) + if htlcIDErr != nil { + return lfn.Err[returnType](htlcIDErr) + } htlcOutputs := commitState.IncomingHtlcAssets.Val assetOutputs = htlcOutputs.FilterByHtlcIndex(htlcID) @@ -1865,7 +2623,9 @@ func (a *AuxSweeper) resolveContract( case input.TaprootHtlcLocalOfferedTimeout: // Like the other HTLC cases, there's only a single output we // care about here. - htlcID := req.HtlcID.UnwrapOr(math.MaxUint64) + if htlcIDErr != nil { + return lfn.Err[returnType](htlcIDErr) + } htlcOutputs := commitState.OutgoingHtlcAssets.Val assetOutputs = htlcOutputs.FilterByHtlcIndex(htlcID) @@ -1880,7 +2640,9 @@ func (a *AuxSweeper) resolveContract( // needed to sweep both this output, as well as the second level // output it creates. case input.TaprootHtlcAcceptedLocalSuccess: - htlcID := req.HtlcID.UnwrapOr(math.MaxUint64) + if htlcIDErr != nil { + return lfn.Err[returnType](htlcIDErr) + } htlcOutputs := commitState.IncomingHtlcAssets.Val assetOutputs = htlcOutputs.FilterByHtlcIndex(htlcID) @@ -1890,14 +2652,321 @@ func (a *AuxSweeper) resolveContract( needsSecondLevel = true - default: - // TODO(guggero): Need to do HTLC revocation cases here. - // IMPORTANT: Remember that we applied the HTLC index as a tweak - // to the revocation key on the asset level! That means the - // tweak to the first-level HTLC script key's internal key - // (which is the revocation key) MUST be applied when creating - // a breach sweep transaction! + // Revoked HTLC offered by remote party (outgoing HTLC from their side). + // We sweep this using the revocation key (keyspend). + case input.TaprootHtlcOfferedRevoke: + // Filter for the specific HTLC we're sweeping. The remote party + // offered this HTLC to us (sending to us), so from + // their PoV it's outgoing and stored in their + // OutgoingHtlcAssets. We sweep it using the revocation + // key since they broadcast a revoked state. + if htlcIDErr != nil { + return lfn.Err[returnType](htlcIDErr) + } + htlcOutputs := commitState.OutgoingHtlcAssets.Val + assetOutputs = htlcOutputs.FilterByHtlcIndex(htlcID) + + payHash, err := req.PayHash.UnwrapOrErr(errNoPayHash) + if err != nil { + return lfn.Err[tlv.Blob](err) + } + + htlcExpiry := req.CltvDelay.UnwrapOr(0) + + // Create sweep descriptor for revoked offered HTLC. + sweepDesc = htlcOfferedRevokeSweepDesc( + req.KeyRing, payHash[:], htlcExpiry, htlcID, + ) + + // Revoked HTLC accepted by remote party (incoming HTLC from their + // side). We sweep this using the revocation key (keyspend). + case input.TaprootHtlcAcceptedRevoke: + // Filter for the specific HTLC we're sweeping. We sent this + // HTLC to the remote party (they accepted/received it), so from + // their PoV it's incoming and stored in their + // IncomingHtlcAssets. We sweep it using the revocation key + // since they broadcast a revoked state. + if htlcIDErr != nil { + return lfn.Err[returnType](htlcIDErr) + } + htlcOutputs := commitState.IncomingHtlcAssets.Val + assetOutputs = htlcOutputs.FilterByHtlcIndex(htlcID) + + payHash, err := req.PayHash.UnwrapOrErr(errNoPayHash) + if err != nil { + return lfn.Err[tlv.Blob](err) + } + + // Create sweep descriptor for revoked accepted HTLC. + sweepDesc = htlcAcceptedRevokeSweepDesc( + req.KeyRing, payHash[:], htlcID, + ) + + // Revoked second-level HTLC transaction. We sweep this using the + // revocation path. + case input.TaprootHtlcSecondLevelRevoke: + // For second-level HTLCs, we need to determine if this was + // originally an offered or accepted HTLC to know which asset + // outputs to filter. + if htlcIDErr != nil { + return lfn.Err[returnType](htlcIDErr) + } + + // Try outgoing first (offered HTLCs). + htlcOutputs := commitState.OutgoingHtlcAssets.Val + assetOutputs = htlcOutputs.FilterByHtlcIndex(htlcID) + + // Determine CLTV timeout: incoming HTLCs on the remote + // party's commitment need a timeout for the second-level + // transaction. + var cltvTimeout fn.Option[uint32] + + // If not found in outgoing, try incoming (accepted HTLCs). + if len(assetOutputs) == 0 { + log.Debugf("HTLC ID %d not found in outgoing "+ + "assets, trying incoming", htlcID) + + htlcOutputs = commitState.IncomingHtlcAssets.Val + assetOutputs = htlcOutputs.FilterByHtlcIndex(htlcID) + + // Incoming HTLCs on the remote commitment need a + // CLTV timeout for the second-level tx. + req.CltvDelay.WhenSome(func(v uint32) { + cltvTimeout = fn.Some(v) + }) + } + + // Save the commitment-level asset outputs before they're + // replaced, as we need them for importing the second-level + // tx (which takes commitment-level inputs). + commitAssetOutputs := assetOutputs + + // Re-anchor the commitment-level asset outputs to the real + // commitment tx BEFORE creating second-level packets. + // CreateSecondLevelHtlcPackets copies the proofs from + // these outputs, so the proofs must have correct block + // headers before the copy. + // + // NOTE: We log and continue on error rather than failing + // the entire sweep. The BTC-level justice tx must still + // proceed to recover funds. A failure here means the + // asset proof chain will be incomplete, but this can be + // repaired later by re-importing the proof. + ctxImport := context.Background() + if err := reanchorAssetOutputs( + ctxImport, a.cfg.ChainBridge, *req.CommitTx, + req.CommitTxBlockHeight, commitAssetOutputs, + ); err != nil { + log.Errorf("Unable to re-anchor commit asset "+ + "outputs for second-level import: %v", err) + } + + // Construct a minimal AuxChanState for the second-level + // packet creation. + auxChanState := lnwallet.AuxChanState{ + ChanType: req.ChanType, + IsInitiator: req.Initiator, + LocalChanCfg: channeldb.ChannelConfig{ + CommitmentParams: channeldb.CommitmentParams{ + CsvDelay: uint16(req.CsvDelay), + }, + }, + } + + // Create the second-level virtual packets. This gives us + // both the aux leaf (for the sweep descriptor) AND the + // output assets with the correct second-level script keys. + // The proofs from commitAssetOutputs are now re-anchored + // with the correct block headers. + // + // Use the actual on-chain second-level tx output + // value (after fee deduction) rather than the HTLC + // amount. The signer used the fee-deducted value + // when creating the second-level tx. + secondLevelBtcAmt := req.HtlcAmt + if req.SecondLevelTx != nil && + len(req.SecondLevelTx.TxOut) > 0 { + + secondLevelBtcAmt = btcutil.Amount( + req.SecondLevelTx.TxOut[0].Value, + ) + } + + secondLevelPkts, secondLevelAllocs, err := + CreateSecondLevelHtlcPackets( + auxChanState, req.CommitTx, + secondLevelBtcAmt, *req.KeyRing, + &a.cfg.ChainParams, assetOutputs, + cltvTimeout, htlcID, + ) + if err != nil { + return lfn.Errf[returnType]("unable to create "+ + "second-level packets: %w", err) + } + + // Compute the aux leaf from the allocations (same as + // CreateSecondLevelHtlcTx does). + var opts []tapsend.OutputCommitmentOption + if !commitState.STXO.Val { + opts = append( + opts, tapsend.WithNoSTXOProofs(), + ) + } + outCommitments, err := tapsend.CreateOutputCommitments( + secondLevelPkts, opts..., + ) + if err != nil { + return lfn.Errf[returnType]("unable to create "+ + "output commitments: %w", err) + } + err = tapsend.AssignOutputCommitments( + secondLevelAllocs, outCommitments, + ) + if err != nil { + return lfn.Errf[returnType]("unable to assign "+ + "output commitments: %w", err) + } + + // Import the second-level tx into the proof archive. This + // creates the commitment → second-level proof transition + // so the sweep can build a valid proof chain. + // + // NOTE: Like reanchorAssetOutputs above, we log and + // continue on error to avoid blocking the BTC-level + // justice sweep. The proof chain gap can be repaired + // after the fact. + if req.SecondLevelTx != nil { + importErr := a.importSecondLevelHtlcTx( + req, secondLevelPkts, + secondLevelAllocs, outCommitments, + commitState, + ) + if importErr != nil { + log.Errorf("Unable to import "+ + "second-level HTLC "+ + "tx: %v", importErr) + } + } + + // Replace the commitment-level asset outputs with the + // second-level outputs from the vPackets. After a + // successful import, each vPkt output has a valid + // ProofSuffix with correct inclusion proof, block + // header, merkle proof, and asset data. Use that + // directly instead of building a stub from the + // commitment-level proof. + var secondLevelAssetOutputs []*cmsg.AssetOutput + for i, vPkt := range secondLevelPkts { + if len(vPkt.Outputs) == 0 || i >= len(assetOutputs) { + continue + } + + vOut := vPkt.Outputs[0] + if vOut.ProofSuffix != nil { + // Use the proof suffix from the + // successful import — it has the + // correct inclusion proof and taproot + // commitment for the 2nd-level output. + // But the block data may not be + // populated yet (the porter finalizes + // asynchronously), so we add it here. + if req.SecondLevelTx != nil && + req.SecondLevelTxBlockHeight > 0 { + + stxParams, ppErr := proofParamsForCommitTx( //nolint:lll + ctxImport, + a.cfg.ChainBridge, + req.SecondLevelTxBlockHeight, + *req.SecondLevelTx, + ) + if ppErr != nil { + log.Warnf("Unable to get "+ + "proof params for "+ + "2nd-level tx: %v", + ppErr) + } else { + upErr := vOut.ProofSuffix.UpdateTransitionProof( //nolint:lll + &stxParams, + ) + if upErr != nil { + log.Warnf("Unable "+ + "to update "+ + "proof: %v", + upErr) + } + } + } + + log.Debugf("Using imported proof "+ + "suffix for output %d", i) + + secondLevelAssetOutputs = append( + secondLevelAssetOutputs, + cmsg.NewAssetOutput( + assetOutputs[i].AssetID.Val, + assetOutputs[i].Amount.Val, + *vOut.ProofSuffix, + ), + ) + } else { + // Fallback: build a minimal stub if + // the import didn't produce a suffix. + outAsset := vOut.Asset + secondLevelProof := assetOutputs[i].Proof.Val + secondLevelProof.Asset = *outAsset + + if req.SecondLevelTx != nil { + stx := req.SecondLevelTx + secondLevelProof.AnchorTx = *stx + + if len(stx.TxIn) > 0 { + prevOut := stx.TxIn[0] + secondLevelProof.PrevOut = + prevOut.PreviousOutPoint + } + + slProof := &secondLevelProof + slProof.InclusionProof.OutputIndex = 0 + } + + log.Warnf("No proof suffix for "+ + "output %d, using fallback "+ + "stub", i) + + secondLevelAssetOutputs = append( + secondLevelAssetOutputs, + cmsg.NewAssetOutput( + assetOutputs[i].AssetID.Val, + assetOutputs[i].Amount.Val, + secondLevelProof, + ), + ) + } + } + + if len(secondLevelAssetOutputs) > 0 { + assetOutputs = secondLevelAssetOutputs + } + + log.Infof("Second-level revoke: htlcID=%d, "+ + "numAssetOutputs=%d, csvDelay=%d", + htlcID, len(assetOutputs), req.CsvDelay) + + for i, ao := range assetOutputs { + log.Infof(" assetOutput[%d]: scriptKey=%x, "+ + "amount=%d", + i, + ao.Proof.Val.Asset.ScriptKey.PubKey. + SerializeCompressed(), + ao.Amount.Val) + } + // Create sweep descriptor for revoked second-level HTLC. + sweepDesc = htlcSecondLevelRevokeSweepDesc( + req.KeyRing, req.CsvDelay, htlcID, + ) + + default: return lfn.Errf[returnType]("unknown resolution type: %v", req.Type) } @@ -1949,12 +3018,22 @@ func (a *AuxSweeper) resolveContract( // The input proofs above were made originally using the fake commit tx // as an anchor. We now know the real commit tx, so we'll bind each // proof to the actual commitment output that carries the asset. - if err := reanchorAssetOutputs( - ctx, a.cfg.ChainBridge, commitTx, req.CommitTxBlockHeight, - assetOutputs, - ); err != nil { - return lfn.Errf[returnType]("unable to re-anchor asset "+ - "outputs: %w", err) + // For second-level revocations, the asset outputs have been replaced + // with second-level outputs. We re-anchor them to the second-level tx + // instead of the commitment tx. + isSecondLevelRevoke := req.Type == input.TaprootHtlcSecondLevelRevoke + if !isSecondLevelRevoke { + // For second-level revocations, the proof's AnchorTx and + // OutputIndex are set in the morph block above to reference + // the second-level tx. For all other types, re-anchor to the + // commitment tx. + if err := reanchorAssetOutputs( + ctx, a.cfg.ChainBridge, commitTx, + req.CommitTxBlockHeight, assetOutputs, + ); err != nil { + return lfn.Errf[returnType]("unable to re-anchor "+ + "asset outputs: %w", err) + } } log.Infof("Sweeping %v asset outputs (second_level=%v): %v", @@ -1997,7 +3076,7 @@ func (a *AuxSweeper) resolveContract( // level packets yet, as we don't know what the sweeping // transaction will look like. So we'll just create them. secondLevelPkts, err = lfn.MapOption( - //nolint:lll + //nolint:llll func(desc tapscriptSweepDesc) lfn.Result[packetList] { return a.createSweepVpackets( secondLevelInputs, lfn.Ok(desc), req, @@ -2018,7 +3097,7 @@ func (a *AuxSweeper) resolveContract( prevAsset := firstLevelPkts[pktIdx].Outputs[0].Asset for inputIdx, vIn := range vPkt.Inputs { - //nolint:lll + //nolint:llll prevScriptKey := prevAsset.ScriptKey vIn.PrevID.ScriptKey = asset.ToSerialized( prevScriptKey.PubKey, @@ -2325,7 +3404,7 @@ func (a *AuxSweeper) sweepContracts(inputs []input.Input, vIn.PrevID.OutPoint = prevOut } for _, vOut := range vPkt.Outputs { - //nolint:lll + //nolint:llll vOut.Asset.PrevWitnesses[0].PrevID.OutPoint = prevOut } } @@ -2392,11 +3471,29 @@ func sweepExclusionProofGen(sweepInternalKey keychain.KeyDescriptor, // transition proof for it, then registering the sweep with the porter. func (a *AuxSweeper) registerAndBroadcastSweep(req *sweep.BumpRequest, sweepTx *wire.MsgTx, fee btcutil.Amount, - outpointToTxIndex map[wire.OutPoint]int) error { + outpointToTxIndex map[wire.OutPoint]int, + opts sweep.AuxNotifyOpts) error { // TODO(roasbeef): need to handle replacement -- will porter just // upsert in place? + // Check if this sweep was already registered (e.g. after a + // restart). The porter's InsertAssetTransfer is a plain INSERT, + // so duplicate calls would create duplicate transfer rows. + sweepTxHash := sweepTx.TxHash() + + existingParcels, err := a.cfg.TxSender.QueryParcels( + context.Background(), fn.Some(sweepTxHash), false, + ) + if err != nil { + return fmt.Errorf("querying sweep parcels: %w", err) + } + if len(existingParcels) > 0 { + log.Infof("Sweep tx %v already registered, skipping", + sweepTxHash) + return nil + } + log.Infof("Register broadcast of sweep_tx=%v", limitSpewer.Sdump(sweepTx)) @@ -2454,6 +3551,114 @@ func (a *AuxSweeper) registerAndBroadcastSweep(req *sweep.BumpRequest, return err } + // For first level outputs, ensure PrevID.OutPoint matches the actual + // BTC input outpoint. This is critical for second-level revoke inputs + // where the vPacket's PrevID.OutPoint still references the commitment + // HTLC output, but the justice tx actually spends the second-level tx + // output. Without this update, the Porter cannot find the input proof. + for _, sweepSet := range vPkts.firstLevel { + actualOutpoint := sweepSet.btcInput.OutPoint() + for _, vPkt := range sweepSet.vPkts { + for _, vIn := range vPkt.Inputs { + if vIn.PrevID.OutPoint != actualOutpoint { + log.Infof("Updating firstLevel "+ + "PrevID.OutPoint from %v "+ + "to %v", + vIn.PrevID.OutPoint, + actualOutpoint) + + vIn.PrevID.OutPoint = actualOutpoint + } + } + + for _, vOut := range vPkt.Outputs { + prevWit := &vOut.Asset.PrevWitnesses[0] + if prevWit.PrevID.OutPoint != actualOutpoint { + prevWit.PrevID.OutPoint = actualOutpoint + } + } + } + } + + // If LookupInputProofs is set, replace the stale proofs in the + // vPacket inputs with the latest proofs from the archive. This + // is needed when the resolution blob carries a commit-level + // proof but the asset has since been imported at the + // second-level (e.g. after a breach second-level HTLC import). + if opts.LookupInputProofs { + ctx := context.Background() + + // Only look up proofs for inputs that are spending + // second-level HTLC outputs. These carry stale + // commit-level proofs that need replacing with the + // imported second-level proof. Regular first-level + // sweeps have correct proofs in their resolution blob. + isSecondLevelInput := func(inp input.Input) bool { + wt := inp.WitnessType() + return wt == input.HtlcSecondLevelRevoke || + wt == input.TaprootHtlcSecondLevelRevoke + } + for _, sweepPkt := range vPkts.firstLevel { + if !isSecondLevelInput(sweepPkt.btcInput) { + continue + } + for _, vPkt := range sweepPkt.vPkts { + for _, vIn := range vPkt.Inputs { + locator := proof.Locator{ + ScriptKey: *vIn.Asset(). + ScriptKey.PubKey, + OutPoint: &vIn.PrevID. + OutPoint, + } + proofBlob, err := a.cfg. + ProofArchive.FetchProof( + ctx, locator, + ) + if err != nil { + log.Warnf("Unable to "+ + "lookup input "+ + "proof for %v: %v", + vIn.PrevID.OutPoint, + err) + continue + } + + var f proof.File + if err := f.Decode( + bytes.NewReader(proofBlob), + ); err != nil { + log.Warnf("Unable to "+ + "decode proof "+ + "for %v: %v", + vIn.PrevID.OutPoint, + err) + continue + } + + last, err := f.LastProof() + if err != nil { + log.Warnf("Unable to "+ + "get last proof "+ + "for %v: %v", + vIn.PrevID.OutPoint, + err) + continue + } + + log.Infof("Replaced input "+ + "proof for %v "+ + "(AnchorTx=%v, "+ + "PrevOut=%v)", + vIn.PrevID.OutPoint, + last.AnchorTx.TxHash(), + last.PrevOut) + + vIn.Proof = last + } + } + } + } + // For any second level outputs we're sweeping, we'll need to sign for // it, as now we know the txid of the sweeping transaction. for _, sweepSet := range vPkts.secondLevel { @@ -2464,7 +3669,7 @@ func (a *AuxSweeper) registerAndBroadcastSweep(req *sweep.BumpRequest, } for _, vOut := range vPkt.Outputs { - //nolint:lll + //nolint:llll vOut.Asset.PrevWitnesses[0].PrevID.OutPoint = prevOut } } @@ -2572,25 +3777,43 @@ func (a *AuxSweeper) registerAndBroadcastSweep(req *sweep.BumpRequest, log.Infof("Proofs generated for sweep_tx=%v", limitSpewer.Sdump(sweepTx)) - // Add a best-effort height hint for sweep transactions. If the sweep is - // mined quickly, this helps the confirmation registration catch up - // deterministically when we hand the parcel to the porter. + // Add a best-effort height hint for sweep transactions. If the + // caller provided a confirmation height (e.g. from a confirmed + // justice tx), use it directly — this is critical when the tx was + // confirmed several blocks ago and the chain tip has advanced past + // it. Otherwise, fall back to the current height. heightHint := fn.None[uint32]() - currentHeight, err := a.cfg.ChainBridge.CurrentHeight( - context.Background(), - ) - if err != nil { - log.Warnf("Unable to fetch current height for sweep tx %v "+ - "height hint: %v", sweepTx.TxHash(), err) + if opts.ConfirmHeight > 0 { + heightHint = fn.Some(opts.ConfirmHeight) } else { - heightHint = fn.Some(currentHeight) + currentHeight, err := a.cfg.ChainBridge.CurrentHeight( + context.Background(), + ) + if err != nil { + log.Warnf("Unable to fetch current height for "+ + "sweep tx %v height hint: %v", + sweepTx.TxHash(), err) + } else { + heightHint = fn.Some(currentHeight) + } } // With the output commitments re-created, we have all we need to log // and ship the transaction. + // The skip flags are set independently by the caller (LND). + // SkipBroadcast means the tx is already confirmed on-chain. + // SkipProofVerify means input proofs may contain placeholder + // witnesses that cannot pass VM-level validation. + var parcelOpts []tapfreighter.PreAnchoredParcelOpt + if opts.SkipProofVerify { + parcelOpts = append( + parcelOpts, tapfreighter.WithSkipProofVerify(), + ) + } + return shipChannelTxn( a.cfg.TxSender, sweepTx, outCommitments, allVpkts, int64(fee), - heightHint, + heightHint, opts.SkipBroadcast, parcelOpts..., ) } @@ -2612,7 +3835,7 @@ func (a *AuxSweeper) contractResolver() { case req := <-a.broadcastReqs: req.resp <- a.registerAndBroadcastSweep( req.req, req.tx, req.fee, - req.outpointToTxIndex, + req.outpointToTxIndex, req.opts, ) case <-a.quit: @@ -2707,13 +3930,15 @@ func (a *AuxSweeper) ExtraBudgetForInputs( // sweep transaction, generated by the passed BumpRequest. func (a *AuxSweeper) NotifyBroadcast(req *sweep.BumpRequest, tx *wire.MsgTx, fee btcutil.Amount, - outpointToTxIndex map[wire.OutPoint]int) error { + outpointToTxIndex map[wire.OutPoint]int, + opts sweep.AuxNotifyOpts) error { auxReq := &broadcastReq{ req: req, tx: tx, fee: fee, outpointToTxIndex: outpointToTxIndex, + opts: opts, resp: make(chan error, 1), } diff --git a/tapchannel/aux_sweeper_test.go b/tapchannel/aux_sweeper_test.go new file mode 100644 index 0000000000..bc70010c3f --- /dev/null +++ b/tapchannel/aux_sweeper_test.go @@ -0,0 +1,166 @@ +package tapchannel + +import ( + "crypto/sha256" + "testing" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2/schnorr" + "github.com/btcsuite/btcd/txscript" + lfn "github.com/lightningnetwork/lnd/fn/v2" + "github.com/lightningnetwork/lnd/input" + "github.com/lightningnetwork/lnd/lnwallet" + "github.com/stretchr/testify/require" +) + +// TestRevocationSweepDescSignVerify tests that the revocation sweep descriptor +// functions produce taproot output keys consistent with the signing key derived +// from the same base material. For each revocation type (offered, accepted, +// second-level), it performs a full sign+verify round-trip using the same +// routines used in production to create the scripts and derive the keys. +func TestRevocationSweepDescSignVerify(t *testing.T) { + t.Parallel() + + // Generate base key material. In production, revokeBasePriv is our + // revocation base point secret, and commitSecret is the per-commitment + // secret revealed when the remote party broadcasts a revoked + // commitment. + revokeBasePriv, err := btcec.NewPrivateKey() + require.NoError(t, err) + + commitSecret, err := btcec.NewPrivateKey() + require.NoError(t, err) + + // Generate HTLC keys and delay key for the commitment keyring. + localHtlcPriv, err := btcec.NewPrivateKey() + require.NoError(t, err) + + remoteHtlcPriv, err := btcec.NewPrivateKey() + require.NoError(t, err) + + toLocalPriv, err := btcec.NewPrivateKey() + require.NoError(t, err) + + // Derive the revocation public key using the standard LND routine. + // This key becomes the internal key of all HTLC taproot outputs. + revocationKey := input.DeriveRevocationPubkey( + revokeBasePriv.PubKey(), commitSecret.PubKey(), + ) + + keyRing := &lnwallet.CommitmentKeyRing{ + RevocationKey: revocationKey, + LocalHtlcKey: localHtlcPriv.PubKey(), + RemoteHtlcKey: remoteHtlcPriv.PubKey(), + ToLocalKey: toLocalPriv.PubKey(), + } + + payHash := sha256.Sum256([]byte("test preimage")) + htlcIndex := input.HtlcIndex(42) + csvDelay := uint32(144) + htlcExpiry := uint32(800_000) + + // Derive the signing private key that the LND signer computes when + // processing a breach sweep: + // 1. DeriveRevocationPrivKey (DoubleTweak) — recovers the revocation + // private key from our base secret and the revealed commit secret. + // 2. TweakPrivKey with HTLC index (SingleTweak) — applies the + // asset-level HTLC index tweak. + revocationPriv := input.DeriveRevocationPrivKey( + revokeBasePriv, commitSecret, + ) + + tweakScalar := ScriptKeyTweakFromHtlcIndex(htlcIndex) + var singleTweak [32]byte + tweakScalar.PutBytesUnchecked(singleTweak[:]) + signingPriv := input.TweakPrivKey(revocationPriv, singleTweak[:]) + + // Verify that the private key tweak path is consistent with the public + // key tweak path. This confirms that TweakPrivKey + SingleTweak on the + // private side produces the same result as TweakPubKeyWithTweak on the + // public side. + derivedInternalKey := input.TweakPubKeyWithTweak( + revocationKey, singleTweak[:], + ) + require.Equal( + t, derivedInternalKey.SerializeCompressed(), + signingPriv.PubKey().SerializeCompressed(), + "private key tweak path should match public key tweak path", + ) + + testCases := []struct { + name string + getSweepDescs func() lfn.Result[tapscriptSweepDescs] + }{ + { + name: "offered HTLC revocation", + getSweepDescs: func() lfn.Result[tapscriptSweepDescs] { + return htlcOfferedRevokeSweepDesc( + keyRing, payHash[:], htlcExpiry, + htlcIndex, + ) + }, + }, + { + name: "accepted HTLC revocation", + getSweepDescs: func() lfn.Result[tapscriptSweepDescs] { + return htlcAcceptedRevokeSweepDesc( + keyRing, payHash[:], htlcIndex, + ) + }, + }, + { + name: "second-level HTLC revocation", + getSweepDescs: func() lfn.Result[tapscriptSweepDescs] { + return htlcSecondLevelRevokeSweepDesc( + keyRing, csvDelay, htlcIndex, + ) + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + // Get the sweep descriptor. + descs := tc.getSweepDescs().UnwrapOrFail(t) + desc := descs.firstLevel + + // Revocation sweeps use keyspend (no control block). + require.Empty(t, desc.ctrlBlockBytes, + "revocation sweep should use keyspend") + + // Verify the descriptor's internal key matches what + // we derived from applying both tweaks to the base + // keys on the public key side. + tree := desc.scriptTree.Tree() + require.Equal( + t, + derivedInternalKey.SerializeCompressed(), + tree.InternalKey.SerializeCompressed(), + "descriptor internal key should match "+ + "derived key", + ) + + // Apply the taproot tweak for keyspend signing. + // This mirrors what RawTxInTaprootSignature does + // internally. + tapTweak := desc.scriptTree.TapTweak() + taprootPriv := txscript.TweakTaprootPrivKey( + *signingPriv, tapTweak, + ) + + // Sign a test message. + testMsg := sha256.Sum256([]byte(tc.name)) + sig, err := schnorr.Sign(taprootPriv, testMsg[:]) + require.NoError(t, err) + + // Verify the signature against the taproot output + // key from the descriptor. This is the key that the + // UTXO is locked to on-chain. + require.True( + t, sig.Verify(testMsg[:], tree.TaprootKey), + "signature should verify against taproot "+ + "output key", + ) + }) + } +} diff --git a/tapchannel/commitment.go b/tapchannel/commitment.go index e96bccd0ca..9278a21273 100644 --- a/tapchannel/commitment.go +++ b/tapchannel/commitment.go @@ -410,6 +410,7 @@ func SanityCheckAmounts(ourBalance, theirBalance btcutil.Amount, if !lnwallet.HtlcIsDust( chanType, false, whoseCommit, feePerKw, entry.Amount.ToSatoshis(), dustLimit, + true, ) { numHTLCs++ @@ -419,6 +420,7 @@ func SanityCheckAmounts(ourBalance, theirBalance btcutil.Amount, if !lnwallet.HtlcIsDust( chanType, true, whoseCommit, feePerKw, entry.Amount.ToSatoshis(), dustLimit, + true, ) { numHTLCs++ @@ -431,6 +433,7 @@ func SanityCheckAmounts(ourBalance, theirBalance btcutil.Amount, isDust := lnwallet.HtlcIsDust( chanType, false, whoseCommit, feePerKw, entry.Amount.ToSatoshis(), dustLimit, + true, ) if rfqmsg.Sum(entry.AssetBalances) > 0 && isDust { return false, false, fmt.Errorf("outgoing HTLC asset "+ @@ -446,6 +449,7 @@ func SanityCheckAmounts(ourBalance, theirBalance btcutil.Amount, isDust := lnwallet.HtlcIsDust( chanType, true, whoseCommit, feePerKw, entry.Amount.ToSatoshis(), dustLimit, + true, ) if rfqmsg.Sum(entry.AssetBalances) > 0 && isDust { return false, false, fmt.Errorf("incoming HTLC asset "+ @@ -497,8 +501,9 @@ func GenerateCommitmentAllocations(prevState *cmsg.Commitment, whoseCommit lntypes.ChannelParty, ourBalance, theirBalance lnwire.MilliSatoshi, originalView lnwallet.AuxHtlcView, chainParams *address.ChainParams, - keys lnwallet.CommitmentKeyRing, stxo bool) ([]*tapsend.Allocation, - *cmsg.Commitment, error) { + keys lnwallet.CommitmentKeyRing, stxo, + sigHashDefault bool) ([]*tapsend.Allocation, *cmsg.Commitment, + error) { log.Tracef("Generating allocations, whoseCommit=%v, ourBalance=%d, "+ "theirBalance=%d", whoseCommit, ourBalance, theirBalance) @@ -651,7 +656,9 @@ func GenerateCommitmentAllocations(prevState *cmsg.Commitment, // 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, stxo) + newCommitment, err := ToCommitment( + allocations, vPackets, stxo, sigHashDefault, + ) if err != nil { return nil, nil, fmt.Errorf("unable to convert to commitment: "+ "%w", err) @@ -785,7 +792,7 @@ func CreateAllocations(chanState lnwallet.AuxChanState, ourBalance, isDust := lnwallet.HtlcIsDust( chanState.ChanType, isIncoming, whoseCommit, filteredView.FeePerKw, htlc.Amount.ToSatoshis(), - dustLimit, + dustLimit, true, ) if isDust { // We need to error out, as a dust HTLC carrying assets @@ -882,7 +889,7 @@ func CreateAllocations(chanState lnwallet.AuxChanState, ourBalance, isDust := lnwallet.HtlcIsDust( chanState.ChanType, isIncoming, whoseCommit, filteredView.FeePerKw, htlc.Amount.ToSatoshis(), - dustLimit, + dustLimit, true, ) if isDust { return nil @@ -1155,7 +1162,8 @@ func LeavesFromTapscriptScriptTree( // ToCommitment converts the allocations to a Commitment struct. func ToCommitment(allocations []*tapsend.Allocation, - vPackets []*tappsbt.VPacket, stxo bool) (*cmsg.Commitment, error) { + vPackets []*tappsbt.VPacket, stxo, + sigHashDefault bool) (*cmsg.Commitment, error) { var ( localAssets []*cmsg.AssetOutput @@ -1278,7 +1286,7 @@ func ToCommitment(allocations []*tapsend.Allocation, return cmsg.NewCommitment( localAssets, remoteAssets, outgoingHtlcs, incomingHtlcs, - auxLeaves, stxo, + auxLeaves, stxo, cmsg.SigHashTypeFromBool(sigHashDefault), ), nil } diff --git a/tapchannelmsg/custom_channel_data_test.go b/tapchannelmsg/custom_channel_data_test.go index 7ae5a186d2..f9abed7fae 100644 --- a/tapchannelmsg/custom_channel_data_test.go +++ b/tapchannelmsg/custom_channel_data_test.go @@ -50,7 +50,7 @@ func TestReadChannelCustomData(t *testing.T) { }, map[input.HtlcIndex][]*AssetOutput{ 2: {output4}, }, lnwallet.CommitAuxLeaves{}, - false, + false, SigHashAll, ) fundingBlob := fundingState.Bytes() @@ -158,19 +158,19 @@ func TestReadBalanceCustomData(t *testing.T) { openChannel1 := NewCommitment( []*AssetOutput{output1}, []*AssetOutput{output2}, nil, nil, - lnwallet.CommitAuxLeaves{}, false, + lnwallet.CommitAuxLeaves{}, false, SigHashAll, ) openChannel2 := NewCommitment( []*AssetOutput{output2}, []*AssetOutput{output3}, nil, nil, - lnwallet.CommitAuxLeaves{}, false, + lnwallet.CommitAuxLeaves{}, false, SigHashAll, ) pendingChannel1 := NewCommitment( []*AssetOutput{output3}, nil, nil, nil, - lnwallet.CommitAuxLeaves{}, false, + lnwallet.CommitAuxLeaves{}, false, SigHashAll, ) pendingChannel2 := NewCommitment( nil, []*AssetOutput{output1}, nil, nil, - lnwallet.CommitAuxLeaves{}, false, + lnwallet.CommitAuxLeaves{}, false, SigHashAll, ) var customChannelData bytes.Buffer diff --git a/tapchannelmsg/records.go b/tapchannelmsg/records.go index b93d027dec..b2511eaf6e 100644 --- a/tapchannelmsg/records.go +++ b/tapchannelmsg/records.go @@ -457,13 +457,49 @@ type Commitment struct { // STXO is a flag indicating whether this commitment supports stxo // proofs. STXO tlv.RecordT[tlv.TlvType5, bool] + + // SigHashDefault is a flag indicating whether HTLC second-level + // transactions for this commitment use SigHashDefault. This is cached + // from the negotiated feature bits so that it is available after + // restart without requiring the peer to be online. + SigHashDefault tlv.RecordT[tlv.TlvType6, bool] +} + +// SigHashType indicates the sighash mode used for HTLC second-level +// transactions in a commitment. +type SigHashType uint8 + +const ( + // SigHashAll indicates standard SigHashAll signing, where the + // sweeper can add wallet inputs to bump fees. + SigHashAll SigHashType = iota + + // SigHashDefault indicates SigHashDefault signing with baked-in + // fees, used for taproot asset channels. + SigHashDefault +) + +// IsSigHashDefault returns true if this is SigHashDefault mode. +func (s SigHashType) IsSigHashDefault() bool { + return s == SigHashDefault +} + +// SigHashTypeFromBool converts a boolean sigHashDefault flag to a +// SigHashType value. +func SigHashTypeFromBool(sigHashDefault bool) SigHashType { + if sigHashDefault { + return SigHashDefault + } + + return SigHashAll } // NewCommitment creates a new Commitment record with the given local and remote // assets, and incoming and outgoing HTLCs. func NewCommitment(localAssets, remoteAssets []*AssetOutput, outgoingHtlcs, incomingHtlcs map[input.HtlcIndex][]*AssetOutput, - auxLeaves lnwallet.CommitAuxLeaves, stxo bool) *Commitment { + auxLeaves lnwallet.CommitAuxLeaves, stxo bool, + sigHashType SigHashType) *Commitment { return &Commitment{ LocalAssets: tlv.NewRecordT[tlv.TlvType0]( @@ -490,6 +526,9 @@ func NewCommitment(localAssets, remoteAssets []*AssetOutput, outgoingHtlcs, ), ), STXO: tlv.NewPrimitiveRecord[tlv.TlvType5](stxo), + SigHashDefault: tlv.NewPrimitiveRecord[tlv.TlvType6]( + sigHashType.IsSigHashDefault(), + ), } } @@ -502,6 +541,7 @@ func (c *Commitment) records() []tlv.Record { c.IncomingHtlcAssets.Record(), c.AuxLeaves.Record(), c.STXO.Record(), + c.SigHashDefault.Record(), } } diff --git a/tapchannelmsg/records_test.go b/tapchannelmsg/records_test.go index 8a1d2cf7e0..2a0ef55bfb 100644 --- a/tapchannelmsg/records_test.go +++ b/tapchannelmsg/records_test.go @@ -215,7 +215,7 @@ func TestCommitment(t *testing.T) { name: "commitment with empty HTLC maps", commitment: NewCommitment( nil, nil, nil, nil, lnwallet.CommitAuxLeaves{}, - false, + false, SigHashAll, ), }, { @@ -229,7 +229,8 @@ func TestCommitment(t *testing.T) { NewAssetOutput( [32]byte{1}, 1000, *randProof, ), - }, nil, nil, lnwallet.CommitAuxLeaves{}, false, + }, nil, nil, + lnwallet.CommitAuxLeaves{}, false, SigHashAll, ), }, { @@ -243,7 +244,8 @@ func TestCommitment(t *testing.T) { NewAssetOutput( [32]byte{1}, 1000, *randProof, ), - }, nil, nil, lnwallet.CommitAuxLeaves{}, true, + }, nil, nil, + lnwallet.CommitAuxLeaves{}, true, SigHashAll, ), }, { @@ -334,7 +336,7 @@ func TestCommitment(t *testing.T) { }, }, }, - false, + false, SigHashAll, ), }, } diff --git a/tapdb/assets_store.go b/tapdb/assets_store.go index 52b9adb498..a5d8df3210 100644 --- a/tapdb/assets_store.go +++ b/tapdb/assets_store.go @@ -2710,6 +2710,26 @@ func (a *AssetStore) LogPendingParcel(ctx context.Context, "tx: %w", err) } + existingTransfers, err := q.QueryAssetTransfers( + ctx, TransferQuery{ + AnchorTxHash: newAnchorTXID[:], + }, + ) + if err != nil { + return fmt.Errorf("unable to query existing asset "+ + "transfers: %w", err) + } + if len(existingTransfers) > 0 { + // Breach recovery can re-deliver a NotifyBroadcast + // for the same pre-anchored sweep/import tx. Treat + // the transfer shell as idempotent by anchor txid so + // we don't strand duplicate pending rows for the + // same on-chain spend. + log.Warnf("Skipping duplicate pending parcel for "+ + "anchor_txid=%v", newAnchorTXID) + return nil + } + // The transfer itself is just a shell which the inputs and // outputs will reference. We'll insert this next, so we can // use its ID. @@ -3410,7 +3430,11 @@ func (a *AssetStore) LogAnchorTxConfirm(ctx context.Context, AnchorPoint: inputs[idx].AnchorPoint, }, ) - if err != nil { + if err == nil { + continue + } + + if !errors.Is(err, sql.ErrNoRows) { return fmt.Errorf("unable to set asset spent: "+ "%w, script_key=%v, asset_id=%v, "+ "anchor_point=%v", err, @@ -3418,6 +3442,47 @@ func (a *AssetStore) LogAnchorTxConfirm(ctx context.Context, spew.Sdump(inputs[idx].AssetID), spew.Sdump(inputs[idx].AnchorPoint)) } + + // Some breach-recovery flows can learn about a + // confirmed successor spend even if the predecessor + // output never materialized in the assets table. In + // that case we still need a template asset row to + // create the successor asset and keep the porter + // moving instead of leaving the transfer in a + // permanent pending state even though the BTC spend + // confirmed. + now := sqlTime(a.clock.Now().UTC()) + templateAssets, queryErr := q.QueryAssets( + ctx, QueryAssetFilters{ + AssetIDFilter: inputs[idx].AssetID, + Now: now, + NumLimit: 1, + ScriptKeyType: scriptKeyTypesForQuery( + false, + fn.None[asset.ScriptKeyType](), + ), + }, + ) + if queryErr != nil { + return fmt.Errorf("unable to find template "+ + "asset: %w", queryErr) + } + if len(templateAssets) == 0 { + return fmt.Errorf("unable to find template "+ + "asset for missing spent input, "+ + "script_key=%v, asset_id=%v, "+ + "anchor_point=%v", + spew.Sdump(inputs[idx].ScriptKey), + spew.Sdump(inputs[idx].AssetID), + spew.Sdump(inputs[idx].AnchorPoint)) + } + + copyTemplateIDs[assetID] = + templateAssets[0].AssetPrimaryKey + log.Warnf("Missing asset row for confirmed transfer "+ + "input, continuing with fallback template: "+ + "asset_id=%x, anchor_point=%x", + inputs[idx].AssetID, inputs[idx].AnchorPoint) } // Now is the time to fetch our outputs and create new assets diff --git a/tapdb/assets_store_test.go b/tapdb/assets_store_test.go index b814254acf..a0683ee14d 100644 --- a/tapdb/assets_store_test.go +++ b/tapdb/assets_store_test.go @@ -1907,6 +1907,9 @@ func TestAssetExportLog(t *testing.T) { require.NoError(t, assetsStore.LogPendingParcel( ctx, spendDelta, leaseOwner, leaseExpiry, )) + require.NoError(t, assetsStore.LogPendingParcel( + ctx, spendDelta, leaseOwner, leaseExpiry, + )) assetID := inputAsset.ID() receiverIdentifier := tapfreighter.NewOutputIdentifier( diff --git a/tapdb/sqlerrors.go b/tapdb/sqlerrors.go index d714c34a1c..0b67082358 100644 --- a/tapdb/sqlerrors.go +++ b/tapdb/sqlerrors.go @@ -5,8 +5,8 @@ import ( "fmt" "strings" - "github.com/jackc/pgconn" "github.com/jackc/pgerrcode" + "github.com/jackc/pgx/v5/pgconn" "modernc.org/sqlite" sqlite3 "modernc.org/sqlite/lib" ) diff --git a/tapfeatures/aux_feature_bits.go b/tapfeatures/aux_feature_bits.go index 135e1e3a19..eaf979ab9b 100644 --- a/tapfeatures/aux_feature_bits.go +++ b/tapfeatures/aux_feature_bits.go @@ -18,14 +18,29 @@ const ( // STXOOptional is a feature bit that declares the STXO proofs as an // optional feature. STXOOptional lnwire.FeatureBit = 3 + + // DeterministicHTLCsRequired is a feature bit that enables + // deterministic second-level HTLC transactions as a required + // feature. When negotiated, second-level HTLC transactions use + // SigHashDefault (making them fully deterministic), and the + // revoking party includes dual-path AuxSigs in RevokeAndAck + // so the honest party can reconstruct valid proofs for breach + // recovery. + DeterministicHTLCsRequired lnwire.FeatureBit = 4 + + // DeterministicHTLCsOptional is the optional variant of the + // deterministic HTLCs feature bit. + DeterministicHTLCsOptional lnwire.FeatureBit = 5 ) // featureNames keeps track of the string description of known features. var featureNames = map[lnwire.FeatureBit]string{ - NoOpHTLCsRequired: "noop-htlcs", - NoOpHTLCsOptional: "noop-htlcs", - STXORequired: "stxo-proofs", - STXOOptional: "stxo-proofs", + NoOpHTLCsRequired: "noop-htlcs", + NoOpHTLCsOptional: "noop-htlcs", + STXORequired: "stxo-proofs", + STXOOptional: "stxo-proofs", + DeterministicHTLCsRequired: "deterministic-htlcs", + DeterministicHTLCsOptional: "deterministic-htlcs", } // ourFeatures returns a slice containing all of the locally supported features. @@ -35,6 +50,7 @@ func ourFeatures() []lnwire.FeatureBit { return []lnwire.FeatureBit{ NoOpHTLCsOptional, STXOOptional, + DeterministicHTLCsOptional, } } diff --git a/tapfreighter/chain_porter.go b/tapfreighter/chain_porter.go index 0dd2dd221b..430b2b240f 100644 --- a/tapfreighter/chain_porter.go +++ b/tapfreighter/chain_porter.go @@ -609,19 +609,34 @@ func (p *ChainPorter) storeProofs(sendPkg *sendPackage) error { } sendPkg.FinalProofs[outKey] = outputProof - vCtx := proof.VerifierCtx{ - HeaderVerifier: headerVerifier, - MerkleVerifier: proof.DefaultMerkleVerifier, - GroupVerifier: p.cfg.GroupVerifier, - ChainLookupGen: p.cfg.ChainBridge, - IgnoreChecker: p.cfg.IgnoreChecker, - } + var verifiedOutputProofs []proof.VerifiedAnnotatedProof + if sendPkg.SkipProofVerify { + // For already-confirmed channel txs with + // placeholder witnesses, skip the VM-level + // verification and trust the on-chain + // confirmation. + verifiedOutputProofs = + proof.AssumeVerifiedAnnotatedProofs( + confEvent.BlockHeight, + outputProof, + ) + } else { + vCtx := proof.VerifierCtx{ + HeaderVerifier: headerVerifier, + MerkleVerifier: proof.DefaultMerkleVerifier, + GroupVerifier: p.cfg.GroupVerifier, + ChainLookupGen: p.cfg.ChainBridge, + IgnoreChecker: p.cfg.IgnoreChecker, + } - verifiedOutputProofs, err := proof.VerifyAnnotatedProofs( - ctx, vCtx, outputProof, - ) - if err != nil { - return fmt.Errorf("error verifying proof: %w", err) + verifiedOutputProofs, err = + proof.VerifyAnnotatedProofs( + ctx, vCtx, outputProof, + ) + if err != nil { + return fmt.Errorf("error verifying "+ + "proof: %w", err) + } } // Import proof into proof archive. diff --git a/tapfreighter/parcel.go b/tapfreighter/parcel.go index e7d335e241..db7cf69cdc 100644 --- a/tapfreighter/parcel.go +++ b/tapfreighter/parcel.go @@ -436,19 +436,41 @@ type PreAnchoredParcel struct { // anchorTxHeightHint is an optional height hint for the anchor // transaction. anchorTxHeightHint fn.Option[uint32] + + // skipProofVerify skips the output proof verification step. This is + // used when importing already-confirmed channel transactions whose + // asset-level witnesses cannot be reconstructed (e.g. second-level + // HTLC transactions where the HTLC script keys require signatures + // we don't possess). The BTC-level confirmation serves as proof of + // validity. + skipProofVerify bool } // A compile-time assertion to ensure PreAnchoredParcel implements the Parcel // interface. var _ Parcel = (*PreAnchoredParcel)(nil) +// PreAnchoredParcelOpt is a functional option for NewPreAnchoredParcel. +type PreAnchoredParcelOpt func(*PreAnchoredParcel) + +// WithSkipProofVerify returns an option that skips the output proof +// verification step in the porter. This is used when importing +// already-confirmed channel transactions whose asset-level witnesses +// cannot be reconstructed. +func WithSkipProofVerify() PreAnchoredParcelOpt { + return func(p *PreAnchoredParcel) { + p.skipProofVerify = true + } +} + // NewPreAnchoredParcel creates a new PreAnchoredParcel. func NewPreAnchoredParcel(vPackets []*tappsbt.VPacket, passiveAssets []*tappsbt.VPacket, anchorTx *tapsend.AnchorTransaction, skipAnchorTxBroadcast bool, label string, - anchorTxHeightHint fn.Option[uint32]) *PreAnchoredParcel { + anchorTxHeightHint fn.Option[uint32], + opts ...PreAnchoredParcelOpt) *PreAnchoredParcel { - return &PreAnchoredParcel{ + p := &PreAnchoredParcel{ parcelKit: &parcelKit{ respChan: make(chan *OutboundParcel, 1), errChan: make(chan error, 1), @@ -460,6 +482,11 @@ func NewPreAnchoredParcel(vPackets []*tappsbt.VPacket, label: label, anchorTxHeightHint: anchorTxHeightHint, } + for _, opt := range opts { + opt(p) + } + + return p } // pkg returns the send package that should be delivered. @@ -467,16 +494,25 @@ func (p *PreAnchoredParcel) pkg() *sendPackage { log.Infof("New anchored delivery request with %d packets", len(p.virtualPackets)) + // When proof verification is skipped (e.g. for already-confirmed + // channel txs with placeholder witnesses), jump directly to the + // store state. + startState := SendStateVerifyPreBroadcast + if p.skipProofVerify { + startState = SendStateStorePreBroadcast + } + // Initialize a package the signed virtual transaction and input // commitment. return &sendPackage{ Parcel: p, - SendState: SendStateVerifyPreBroadcast, + SendState: startState, VirtualPackets: p.virtualPackets, PassiveAssets: p.passiveAssets, AnchorTx: p.anchorTx, Label: p.label, SkipAnchorTxBroadcast: p.skipAnchorTxBroadcast, + SkipProofVerify: p.skipProofVerify, } } @@ -589,6 +625,12 @@ type sendPackage struct { // broadcast should be skipped. Useful when an external system handles // broadcasting, such as in custom transaction packaging workflows. SkipAnchorTxBroadcast bool + + // SkipProofVerify skips asset witness verification for both + // pre-broadcast and post-confirmation steps. Used when importing + // already-confirmed channel transactions whose asset-level witnesses + // are placeholders. + SkipProofVerify bool } // ConvertToTransfer prepares the finished send data for storing to the database