diff --git a/client_test.go b/client_test.go index cf38c4b..b381a50 100644 --- a/client_test.go +++ b/client_test.go @@ -12,7 +12,9 @@ import ( "github.com/lightninglabs/loop/lndclient" "github.com/lightninglabs/loop/loopdb" "github.com/lightninglabs/loop/test" + "github.com/lightningnetwork/lnd/lnrpc" "github.com/lightningnetwork/lnd/lntypes" + "github.com/stretchr/testify/require" ) var ( @@ -261,6 +263,10 @@ func testSuccess(ctx *testContext, amt btcutil.Amount, hash lntypes.Hash, ctx.AssertRegisterSpendNtfn(confIntent.PkScript) + // Assert that a call to track payment was sent, and respond with status + // in flight so that our swap will push its preimage to the server. + ctx.trackPayment(lnrpc.Payment_IN_FLIGHT) + // Publish tick. ctx.expiryChan <- testTime @@ -287,6 +293,13 @@ func testSuccess(ctx *testContext, amt btcutil.Amount, hash lntypes.Hash, ctx.T.Fatalf("incorrect preimage") } + // Since we successfully published our sweep, we expect the preimage to + // have been pushed to our mock server. + preimage, err := lntypes.MakePreimage(clientPreImage) + require.NoError(ctx.T, err) + + ctx.assertPreimagePush(preimage) + // Simulate server pulling payment. signalSwapPaymentResult(nil) diff --git a/go.mod b/go.mod index c5052c0..7fd61c2 100644 --- a/go.mod +++ b/go.mod @@ -13,6 +13,7 @@ require ( github.com/lightninglabs/protobuf-hex-display v1.3.3-0.20191212020323-b444784ce75d github.com/lightningnetwork/lnd v0.10.0-beta.rc6.0.20200603030653-09bb9db78246 github.com/lightningnetwork/lnd/queue v1.0.4 + github.com/stretchr/testify v1.4.0 github.com/urfave/cli v1.20.0 golang.org/x/net v0.0.0-20191002035440-2ec189313ef0 golang.org/x/text v0.3.2 // indirect diff --git a/go.sum b/go.sum index 3828901..b673904 100644 --- a/go.sum +++ b/go.sum @@ -1,14 +1,10 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -git.schwanenlied.me/yawning/bsaes.git v0.0.0-20180720073208-c0276d75487e h1:F2x1bq7RaNCIuqYpswggh1+c1JmwdnkHNC9wy1KDip0= git.schwanenlied.me/yawning/bsaes.git v0.0.0-20180720073208-c0276d75487e/go.mod h1:BWqTsj8PgcPriQJGl7el20J/7TuT1d/hSyFDXMEpoEo= github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/NebulousLabs/fastrand v0.0.0-20181203155948-6fb6489aac4e h1:n+DcnTNkQnHlwpsrHoQtkrJIO7CBx029fw6oR4vIob4= github.com/NebulousLabs/fastrand v0.0.0-20181203155948-6fb6489aac4e/go.mod h1:Bdzq+51GR4/0DIhaICZEOm+OHvXGwwB2trKZ8B4Y6eQ= -github.com/NebulousLabs/go-upnp v0.0.0-20180202185039-29b680b06c82 h1:MG93+PZYs9PyEsj/n5/haQu2gK0h4tUtSy9ejtMwWa0= github.com/NebulousLabs/go-upnp v0.0.0-20180202185039-29b680b06c82/go.mod h1:GbuBk21JqF+driLX3XtJYNZjGa45YDoa9IqCTzNSfEc= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= -github.com/Yawning/aez v0.0.0-20180114000226-4dad034d9db2 h1:2be4ykKKov3M1yISM2E8gnGXZ/N2SsPawfnGiXxaYEU= github.com/Yawning/aez v0.0.0-20180114000226-4dad034d9db2/go.mod h1:9pIqrY6SXNL8vjRQE5Hd/OL5GyK/9MrGUWs87z/eFfk= github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmHS9iAKVt9AyzRSqNU1qabPih5BY= github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA= @@ -23,14 +19,12 @@ github.com/beorn7/perks v1.0.0 h1:HWo1m869IqiPhD389kmkxeTalrjNbbJTC8LXupb+sl0= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/btcsuite/btcd v0.0.0-20190629003639-c26ffa870fd8/go.mod h1:3J08xEfcugPacsc34/LKRU2yO7YmuT8yt28J8k2+rrI= github.com/btcsuite/btcd v0.0.0-20190824003749-130ea5bddde3/go.mod h1:3J08xEfcugPacsc34/LKRU2yO7YmuT8yt28J8k2+rrI= -github.com/btcsuite/btcd v0.20.1-beta h1:Ik4hyJqN8Jfyv3S4AGBOmyouMsYE3EdYODkMbQjwPGw= github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ= github.com/btcsuite/btcd v0.20.1-beta.0.20200513120220-b470eee47728/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ= github.com/btcsuite/btcd v0.20.1-beta.0.20200515232429-9f0179fd2c46 h1:QyTpiR5nQe94vza2qkvf7Ns8XX2Rjh/vdIhO3RzGj4o= github.com/btcsuite/btcd v0.20.1-beta.0.20200515232429-9f0179fd2c46/go.mod h1:Yktc19YNjh/Iz2//CX0vfRTS4IJKM/RKO5YZ9Fn+Pgo= github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f h1:bAs4lUbRJpnnkd9VhRV3jjAVU7DJVjMaK+IsvSeZvFo= github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA= -github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d h1:yJzD/yFppdVCf6ApMkVy8cUxV0XrxdP9rVf6D87/Mng= github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg= github.com/btcsuite/btcutil v1.0.2 h1:9iZ1Terx9fMIOtq1VrwdqfsATL9MC2l8ZrUY6YZ2uts= github.com/btcsuite/btcutil v1.0.2/go.mod h1:j9HUFwoQRsZL3V4n+qG+CUnEGHOarIxfC3Le2Yhbcts= @@ -48,7 +42,6 @@ github.com/btcsuite/btcwallet/walletdb v1.0.0/go.mod h1:bZTy9RyYZh9fLnSua+/CD48T github.com/btcsuite/btcwallet/walletdb v1.2.0/go.mod h1:9cwc1Yyg4uvd4ZdfdoMnALji+V9gfWSMfxEdLdR5Vwc= github.com/btcsuite/btcwallet/walletdb v1.3.1 h1:lW1Ac3F1jJY4K11P+YQtRNcP5jFk27ASfrV7C6mvRU0= github.com/btcsuite/btcwallet/walletdb v1.3.1/go.mod h1:9cwc1Yyg4uvd4ZdfdoMnALji+V9gfWSMfxEdLdR5Vwc= -github.com/btcsuite/btcwallet/wtxmgr v1.0.0 h1:aIHgViEmZmZfe0tQQqF1xyd2qBqFWxX5vZXkkbjtbeA= github.com/btcsuite/btcwallet/wtxmgr v1.0.0/go.mod h1:vc4gBprll6BP0UJ+AIGDaySoc7MdAmZf8kelfNb8CFY= github.com/btcsuite/btcwallet/wtxmgr v1.1.1-0.20200515224913-e0e62245ecbe h1:yQbJVYfsKbdqDQNLxd4hhiLSiMkIygefW5mSHMsdKpc= github.com/btcsuite/btcwallet/wtxmgr v1.1.1-0.20200515224913-e0e62245ecbe/go.mod h1:OwC0W0HhUszbWdvJvH6xvgabKSJ0lXl11YbmmqF9YXQ= @@ -135,9 +128,7 @@ github.com/grpc-ecosystem/grpc-gateway v1.12.2 h1:D0EVSTwQoQOyfY35QNSuPJA4jpZRtk github.com/grpc-ecosystem/grpc-gateway v1.12.2/go.mod h1:8XEsbTttt/W+VvjtQhLACqCisSPWTxCZ7sBRjU6iH9c= github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= -github.com/jackpal/gateway v1.0.5 h1:qzXWUJfuMdlLMtt0a3Dgt+xkWQiA5itDEITVJtuSwMc= github.com/jackpal/gateway v1.0.5/go.mod h1:lTpwd4ACLXmpyiCTRtfiNyVnUmqT9RivzCDQetPfnjA= -github.com/jackpal/go-nat-pmp v0.0.0-20170405195558-28a68d0c24ad h1:heFfj7z0pGsNCekUlsFhO2jstxO4b5iQ665LjwM5mDc= github.com/jackpal/go-nat-pmp v0.0.0-20170405195558-28a68d0c24ad/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= github.com/jedib0t/go-pretty v4.3.0+incompatible/go.mod h1:XemHduiw8R651AF9Pt4FwCTKeG3oo7hrHJAoznj9nag= github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= @@ -178,7 +169,6 @@ github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/lightninglabs/gozmq v0.0.0-20191113021534-d20a764486bf h1:HZKvJUHlcXI/f/O0Avg7t8sqkPo78HFzjmeYFl6DPnc= github.com/lightninglabs/gozmq v0.0.0-20191113021534-d20a764486bf/go.mod h1:vxmQPeIQxPf6Jf9rM8R+B4rKBqLA2AjttNxkFBL2Plk= -github.com/lightninglabs/neutrino v0.11.0 h1:lPpYFCtsfJX2W5zI4pWycPmbbBdr7zU+BafYdLoD6k0= github.com/lightninglabs/neutrino v0.11.0/go.mod h1:CuhF0iuzg9Sp2HO6ZgXgayviFTn1QHdSTJlMncK80wg= github.com/lightninglabs/neutrino v0.11.1-0.20200316235139-bffc52e8f200 h1:j4iZ1XlUAPQmW6oSzMcJGILYsRHNs+4O3Gk+2Ms5Dww= github.com/lightninglabs/neutrino v0.11.1-0.20200316235139-bffc52e8f200/go.mod h1:MlZmoKa7CJP3eR1s5yB7Rm5aSyadpKkxqAwLQmog7N0= @@ -191,7 +181,6 @@ github.com/lightningnetwork/lnd v0.10.0-beta.rc6.0.20200603030653-09bb9db78246/g github.com/lightningnetwork/lnd/cert v1.0.2/go.mod h1:fmtemlSMf5t4hsQmcprSoOykypAPp+9c+0d0iqTScMo= github.com/lightningnetwork/lnd/clock v1.0.0 h1:U9uknV7DwBsNUzc+x11tx1bPvoXiEYcJ9wrIemRibKE= github.com/lightningnetwork/lnd/clock v1.0.0/go.mod h1:KnQudQ6w0IAMZi1SgvecLZQZ43ra2vpDNj7H/aasemg= -github.com/lightningnetwork/lnd/queue v1.0.1 h1:jzJKcTy3Nj5lQrooJ3aaw9Lau3I0IwvQR5sqtjdv2R0= github.com/lightningnetwork/lnd/queue v1.0.1/go.mod h1:vaQwexir73flPW43Mrm7JOgJHmcEFBWWSl9HlyASoms= github.com/lightningnetwork/lnd/queue v1.0.4 h1:8Dq3vxAFSACPy+pKN88oPFhuCpCoAAChPBwa4BJxH4k= github.com/lightningnetwork/lnd/queue v1.0.4/go.mod h1:YTkTVZCxz8tAYreH27EO3s8572ODumWrNdYW2E/YKxg= @@ -256,7 +245,6 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5 h1:LnC5Kc/wtumK+WB441p7ynQJzVuNRJiqddSIE3IlSEQ= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= -github.com/tv42/zbase32 v0.0.0-20160707012821-501572607d02 h1:tcJ6OjwOMvExLlzrAVZute09ocAGa7KqOON60++Gz4E= github.com/tv42/zbase32 v0.0.0-20160707012821-501572607d02/go.mod h1:tHlrkM198S068ZqfrO6S8HsoJq2bF3ETfTL+kt4tInY= github.com/urfave/cli v1.18.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= github.com/urfave/cli v1.20.0 h1:fDqGv3UG/4jbVl/QkFwEdddtEDjh/5Ov6X+0B/3bPaw= @@ -280,7 +268,6 @@ golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnf golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/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-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200115085410-6d4e4cb37c7d h1:2+ZP7EfsZV7Vvmx3TIqSlSzATMkTAKqM14YGFPoSKjI= golang.org/x/crypto v0.0.0-20200115085410-6d4e4cb37c7d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37 h1:cg5LA/zNPRzIXIWSCxQW10Rvpy94aQh3LT/ShoCpkHw= golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= @@ -320,7 +307,6 @@ golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20190209173611-3b5209105503/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-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190904154756-749cb33beabd h1:DBH9mDw0zluJT/R+nGuV3jWFWLFaHyYZWD4tOT+cjn0= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42 h1:vEOn+mP2zCOVzKckCZy6YsCtDblrpj/w7B9nxGNELpg= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -373,7 +359,6 @@ gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkep gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.3 h1:fvjTMHxHEw/mxHbtzPi3JCcKXQRAnQTBRo6YCJSVHKI= gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/loopout.go b/loopout.go index b233c0b..4531778 100644 --- a/loopout.go +++ b/loopout.go @@ -706,6 +706,20 @@ func (s *loopOutSwap) waitForHtlcSpendConfirmed(globalCtx context.Context, return nil, fmt.Errorf("register spend ntfn: %v", err) } + // Track our payment status so that we can detect whether our off chain + // htlc is settled. We track this information to determine whether it is + // necessary to continue trying to push our preimage to the server. + trackChan, trackErrChan, err := s.lnd.Router.TrackPayment( + ctx, s.hash, + ) + if err != nil { + return nil, fmt.Errorf("track payment: %v", err) + } + + // paymentComplete tracks whether our payment is complete, and is used + // to decide whether we need to push our preimage to the server. + var paymentComplete bool + timerChan := s.timerFactory(republishDelay) for { select { @@ -719,6 +733,45 @@ func (s *loopOutSwap) waitForHtlcSpendConfirmed(globalCtx context.Context, case err := <-spendErr: return nil, err + // Receive status updates for our payment so that we can detect + // whether we've successfully pushed our preimage. + case status, ok := <-trackChan: + // If our channel has been closed, indicating that the + // server is finished providing updates because the + // payment has reached a terminal state, we replace + // the closed channel with nil so that we will no longer + // listen on it. + if !ok { + trackChan = nil + continue + } + + if status.State == lnrpc.Payment_SUCCEEDED { + s.log.Infof("Off chain payment succeeded") + + paymentComplete = true + } + + // If we receive a track payment error that indicates that the + // server stream is complete, we ignore it because we want to + // continue this loop beyond the completion of the payment. + case err, ok := <-trackErrChan: + // If our channel has been closed, indicating that the + // server is finished providing updates because the + // payment has reached a terminal state, we replace + // the closed channel with nil so that we will no longer + // listen on it. + if !ok { + trackErrChan = nil + continue + } + + // Otherwise, if we receive a non-nil error, we return + // it. + if err != nil { + return nil, err + } + // New block arrived, update height and restart the republish // timer. case notification := <-s.blockEpochChan: @@ -733,6 +786,12 @@ func (s *loopOutSwap) waitForHtlcSpendConfirmed(globalCtx context.Context, return nil, err } + // If our off chain payment is not yet complete, we + // try to push our preimage to the server. + if !paymentComplete { + s.pushPreimage(ctx) + } + // Context canceled. case <-globalCtx.Done(): return nil, globalCtx.Err() @@ -740,6 +799,26 @@ func (s *loopOutSwap) waitForHtlcSpendConfirmed(globalCtx context.Context, } } +// pushPreimage pushes our preimage to the server if we have already revealed +// our preimage on chain with a sweep attempt. +func (s *loopOutSwap) pushPreimage(ctx context.Context) { + // If we have not yet revealed our preimage through a sweep, we do not + // push the preimage because we may choose to never sweep if fees are + // too high. + if s.state != loopdb.StatePreimageRevealed { + return + } + + s.log.Infof("Pushing preimage to server") + + // Push the preimage to the server, just log server errors since we rely + // on our payment state rather than the server response to judge the + // outcome of our preimage push. + if err := s.server.PushLoopOutPreimage(ctx, s.Preimage); err != nil { + s.log.Warnf("Could not push preimage: %v", err) + } +} + // sweep tries to sweep the given htlc to a destination address. It takes into // account the max miner fee and marks the preimage as revealed when it // published the tx. diff --git a/loopout_test.go b/loopout_test.go index bf97699..67a0da4 100644 --- a/loopout_test.go +++ b/loopout_test.go @@ -14,6 +14,9 @@ import ( "github.com/lightninglabs/loop/loopdb" "github.com/lightninglabs/loop/sweep" "github.com/lightninglabs/loop/test" + "github.com/lightningnetwork/lnd/lnrpc" + "github.com/lightningnetwork/lnd/lnwallet/chainfee" + "github.com/stretchr/testify/require" ) // TestLoopOutPaymentParameters tests the first part of the loop out process up @@ -216,6 +219,7 @@ func TestCustomSweepConfTarget(t *testing.T) { lnd := test.NewMockLnd() ctx := test.NewContext(t, lnd) + server := newServerMock() // Use the highest sweep confirmation target before we attempt to use // the default. @@ -228,7 +232,7 @@ func TestCustomSweepConfTarget(t *testing.T) { ctx.Lnd.SetFeeEstimate(DefaultSweepConfTarget, 10000) cfg := newSwapConfig( - &lnd.LndServices, newStoreMock(t), newServerMock(), + &lnd.LndServices, newStoreMock(t), server, ) swap, err := newLoopOutSwap( @@ -295,6 +299,10 @@ func TestCustomSweepConfTarget(t *testing.T) { // to sweep it using the custom confirmation target. ctx.AssertRegisterSpendNtfn(swap.htlc.PkScript) + // Assert that we made a query to track our payment, as required for + // preimage push tracking. + trackPayment := ctx.AssertTrackPayment() + expiryChan <- time.Now() // Expect a signing request for the HTLC success transaction. @@ -348,6 +356,19 @@ func TestCustomSweepConfTarget(t *testing.T) { // confirmation target. _ = assertSweepTx(testRequest.SweepConfTarget) + // Once we have published an on chain sweep, we expect a preimage to + // have been pushed to our server. + preimage := <-server.preimagePush + require.Equal(t, swap.Preimage, preimage) + + // Now that we have pushed our preimage to the sever, we send an update + // indicating that our off chain htlc is settled. We do this so that + // we don't have to keep consuming preimage pushes from our server mock + // for every sweep attempt. + trackPayment.Updates <- lndclient.PaymentStatus{ + State: lnrpc.Payment_SUCCEEDED, + } + // We'll then notify the height at which we begin using the default // confirmation target. defaultConfTargetHeight := ctx.Lnd.Height + testLoopOutOnChainCltvDelta - @@ -376,3 +397,189 @@ func TestCustomSweepConfTarget(t *testing.T) { t.Fatal(err) } } + +// TestPreimagePush tests or logic that decides whether to push our preimage to +// the server. First, we test the case where we have not yet disclosed our +// preimage with a sweep, so we do not want to push our preimage yet. Next, we +// broadcast a sweep attempt and push our preimage to the server. In this stage +// we mock a server failure by not sending a settle update for our payment. +// Finally, we make a last sweep attempt, push the preimage (because we have +// not detected our settle) and settle the off chain htlc, indicating that the +// server successfully settled using the preimage push. In this test, we need +// to start with a fee rate that will be too high, then progress to an +// acceptable one. We do this by starting with a high confirmation target with +// a high fee, and setting the default confirmation fee (which our swap will +// drop down to if it is not confirming in time) to a lower fee. This is not +// intuitive (lower confs having lower fees), but it allows up to mock fee +// changes. +func TestPreimagePush(t *testing.T) { + defer test.Guard(t)() + + lnd := test.NewMockLnd() + ctx := test.NewContext(t, lnd) + server := newServerMock() + + // Start with a high confirmation delta which will have a very high fee + // attached to it. + testRequest.SweepConfTarget = testLoopOutOnChainCltvDelta - + DefaultSweepConfTargetDelta - 1 + + // We set our mock fee estimate for our target sweep confs to be our + // max miner fee *2, so that our fee will definitely be above what we + // are willing to pay, and we will not sweep. + ctx.Lnd.SetFeeEstimate( + testRequest.SweepConfTarget, chainfee.SatPerKWeight( + testRequest.MaxMinerFee*2, + ), + ) + + // We set the fee estimate for our default confirmation target very + // low, so that once we drop down to our default confs we will start + // trying to sweep the preimage. + ctx.Lnd.SetFeeEstimate(DefaultSweepConfTarget, 1) + + cfg := newSwapConfig( + &lnd.LndServices, newStoreMock(t), server, + ) + + swap, err := newLoopOutSwap( + context.Background(), cfg, ctx.Lnd.Height, testRequest, + ) + require.NoError(t, err) + + // Set up the required dependencies to execute the swap. + sweeper := &sweep.Sweeper{Lnd: &lnd.LndServices} + blockEpochChan := make(chan interface{}) + statusChan := make(chan SwapInfo) + expiryChan := make(chan time.Time) + timerFactory := func(_ time.Duration) <-chan time.Time { + return expiryChan + } + + errChan := make(chan error) + go func() { + err := swap.execute(context.Background(), &executeConfig{ + statusChan: statusChan, + blockEpochChan: blockEpochChan, + timerFactory: timerFactory, + sweeper: sweeper, + }, ctx.Lnd.Height) + if err != nil { + log.Error(err) + } + errChan <- err + }() + + // The swap should be found in its initial state. + cfg.store.(*storeMock).assertLoopOutStored() + state := <-statusChan + require.Equal(t, loopdb.StateInitiated, state.State) + + // We'll then pay both the swap and prepay invoice, which should trigger + // the server to publish the on-chain HTLC. + signalSwapPaymentResult := ctx.AssertPaid(swapInvoiceDesc) + signalPrepaymentResult := ctx.AssertPaid(prepayInvoiceDesc) + + signalSwapPaymentResult(nil) + signalPrepaymentResult(nil) + + // Notify the confirmation notification for the HTLC. + ctx.AssertRegisterConf() + + blockEpochChan <- ctx.Lnd.Height + 1 + + htlcTx := wire.NewMsgTx(2) + htlcTx.AddTxOut(&wire.TxOut{ + Value: int64(swap.AmountRequested), + PkScript: swap.htlc.PkScript, + }) + + ctx.NotifyConf(htlcTx) + + // The client should then register for a spend of the HTLC and attempt + // to sweep it using the custom confirmation target. + ctx.AssertRegisterSpendNtfn(swap.htlc.PkScript) + + // Assert that we made a query to track our payment, as required for + // preimage push tracking. + trackPayment := ctx.AssertTrackPayment() + + // Tick the expiry channel, we are still using our client confirmation + // target at this stage which has fees higher than our max acceptable + // fee. We do not expect a sweep attempt at this point. Since our + // preimage is not revealed, we also do not expect a preimage push. + expiryChan <- testTime + + // Now, we notify the height at which the client will start using the + // default confirmation target. This has the effect of lowering our fees + // so that the client still start sweeping. + defaultConfTargetHeight := ctx.Lnd.Height + testLoopOutOnChainCltvDelta - + DefaultSweepConfTargetDelta + blockEpochChan <- defaultConfTargetHeight + + // This time when we tick the expiry chan, our fees are lower than the + // swap max, so we expect it to prompt a sweep. + expiryChan <- testTime + + // Expect a signing request for the HTLC success transaction. + <-ctx.Lnd.SignOutputRawChannel + + // This is the first time we have swept, so we expect our preimage + // revealed state to be set. + cfg.store.(*storeMock).assertLoopOutState(loopdb.StatePreimageRevealed) + status := <-statusChan + require.Equal( + t, status.State, loopdb.SwapState(loopdb.StatePreimageRevealed), + ) + + // We expect the sweep tx to have been published. + ctx.ReceiveTx() + + // Once we have published an on chain sweep, we expect a preimage to + // have been pushed to the server after the sweep. + preimage := <-server.preimagePush + require.Equal(t, swap.Preimage, preimage) + + // To mock a server failure, we do not send a payment settled update + // for our off chain payment yet. We also do not confirm our sweep on + // chain yet so we can test our preimage push retry logic. Instead, we + // tick the expiry chan again to prompt another sweep. + expiryChan <- testTime + + // We expect another signing request for out sweep, and publish of our + // sweep transaction. + <-ctx.Lnd.SignOutputRawChannel + ctx.ReceiveTx() + + // Since we have not yet been notified of an off chain settle, and we + // have attempted to sweep again, we expect another preimage push + // attempt. + preimage = <-server.preimagePush + require.Equal(t, swap.Preimage, preimage) + + // This time, we send a payment succeeded update into our payment stream + // to reflect that the server received our preimage push and settled off + // chain. + trackPayment.Updates <- lndclient.PaymentStatus{ + State: lnrpc.Payment_SUCCEEDED, + } + + // We tick one last time, this time expecting a sweep but no preimage + // push. The test's mocked preimage channel is un-buffered, so our test + // would hang if we pushed the preimage here. + expiryChan <- testTime + <-ctx.Lnd.SignOutputRawChannel + sweepTx := ctx.ReceiveTx() + + // Finally, we put this swap out of its misery and notify a successful + // spend our our sweepTx and assert that the swap succeeds. + ctx.NotifySpend(sweepTx, 0) + + cfg.store.(*storeMock).assertLoopOutState(loopdb.StateSuccess) + status = <-statusChan + require.Equal( + t, status.State, loopdb.SwapState(loopdb.StateSuccess), + ) + + require.NoError(t, <-errChan) +} diff --git a/looprpc/server.pb.go b/looprpc/server.pb.go index 46d8156..2e907ff 100644 --- a/looprpc/server.pb.go +++ b/looprpc/server.pb.go @@ -39,18 +39,26 @@ const ( //Loop will use native segwit (P2WSH) htlcs by default, while externally //published htlcs may use native (P2WSH) or nested (NP2WSH) segwit as well. ProtocolVersion_NATIVE_SEGWIT_LOOP_IN ProtocolVersion = 2 + // + //Once the on chain loop out htlc is confirmed, the client can push the swap + //preimage to the server to speed up claim of their off chain htlc (acquiring + //incoming liquidity more quickly than if the server waited for the on chain + //claim tx). + ProtocolVersion_PREIMAGE_PUSH_LOOP_OUT ProtocolVersion = 3 ) var ProtocolVersion_name = map[int32]string{ 0: "LEGACY", 1: "MULTI_LOOP_OUT", 2: "NATIVE_SEGWIT_LOOP_IN", + 3: "PREIMAGE_PUSH_LOOP_OUT", } var ProtocolVersion_value = map[string]int32{ - "LEGACY": 0, - "MULTI_LOOP_OUT": 1, - "NATIVE_SEGWIT_LOOP_IN": 2, + "LEGACY": 0, + "MULTI_LOOP_OUT": 1, + "NATIVE_SEGWIT_LOOP_IN": 2, + "PREIMAGE_PUSH_LOOP_OUT": 3, } func (x ProtocolVersion) String() string { @@ -771,6 +779,91 @@ func (m *ServerLoopInTerms) GetMaxSwapAmount() uint64 { return 0 } +// ServerLoopOutPushPreimageRequest pushes a preimage to the server. Note that +// this call returns with no error after the server acknowledges the preimage +// and does not block until the invoice is settled. +type ServerLoopOutPushPreimageRequest struct { + // The protocol version that the client adheres to. + ProtocolVersion ProtocolVersion `protobuf:"varint,1,opt,name=protocol_version,json=protocolVersion,proto3,enum=looprpc.ProtocolVersion" json:"protocol_version,omitempty"` + // + //Preimage is the preimage of the loop out swap that we wish to push to the + //server to speed up off-chain claim once the on-chain htlc has confirmed. + Preimage []byte `protobuf:"bytes,2,opt,name=preimage,proto3" json:"preimage,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *ServerLoopOutPushPreimageRequest) Reset() { *m = ServerLoopOutPushPreimageRequest{} } +func (m *ServerLoopOutPushPreimageRequest) String() string { return proto.CompactTextString(m) } +func (*ServerLoopOutPushPreimageRequest) ProtoMessage() {} +func (*ServerLoopOutPushPreimageRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_ad098daeda4239f7, []int{12} +} + +func (m *ServerLoopOutPushPreimageRequest) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_ServerLoopOutPushPreimageRequest.Unmarshal(m, b) +} +func (m *ServerLoopOutPushPreimageRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_ServerLoopOutPushPreimageRequest.Marshal(b, m, deterministic) +} +func (m *ServerLoopOutPushPreimageRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_ServerLoopOutPushPreimageRequest.Merge(m, src) +} +func (m *ServerLoopOutPushPreimageRequest) XXX_Size() int { + return xxx_messageInfo_ServerLoopOutPushPreimageRequest.Size(m) +} +func (m *ServerLoopOutPushPreimageRequest) XXX_DiscardUnknown() { + xxx_messageInfo_ServerLoopOutPushPreimageRequest.DiscardUnknown(m) +} + +var xxx_messageInfo_ServerLoopOutPushPreimageRequest proto.InternalMessageInfo + +func (m *ServerLoopOutPushPreimageRequest) GetProtocolVersion() ProtocolVersion { + if m != nil { + return m.ProtocolVersion + } + return ProtocolVersion_LEGACY +} + +func (m *ServerLoopOutPushPreimageRequest) GetPreimage() []byte { + if m != nil { + return m.Preimage + } + return nil +} + +type ServerLoopOutPushPreimageResponse struct { + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *ServerLoopOutPushPreimageResponse) Reset() { *m = ServerLoopOutPushPreimageResponse{} } +func (m *ServerLoopOutPushPreimageResponse) String() string { return proto.CompactTextString(m) } +func (*ServerLoopOutPushPreimageResponse) ProtoMessage() {} +func (*ServerLoopOutPushPreimageResponse) Descriptor() ([]byte, []int) { + return fileDescriptor_ad098daeda4239f7, []int{13} +} + +func (m *ServerLoopOutPushPreimageResponse) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_ServerLoopOutPushPreimageResponse.Unmarshal(m, b) +} +func (m *ServerLoopOutPushPreimageResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_ServerLoopOutPushPreimageResponse.Marshal(b, m, deterministic) +} +func (m *ServerLoopOutPushPreimageResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_ServerLoopOutPushPreimageResponse.Merge(m, src) +} +func (m *ServerLoopOutPushPreimageResponse) XXX_Size() int { + return xxx_messageInfo_ServerLoopOutPushPreimageResponse.Size(m) +} +func (m *ServerLoopOutPushPreimageResponse) XXX_DiscardUnknown() { + xxx_messageInfo_ServerLoopOutPushPreimageResponse.DiscardUnknown(m) +} + +var xxx_messageInfo_ServerLoopOutPushPreimageResponse proto.InternalMessageInfo + func init() { proto.RegisterEnum("looprpc.ProtocolVersion", ProtocolVersion_name, ProtocolVersion_value) proto.RegisterType((*ServerLoopOutRequest)(nil), "looprpc.ServerLoopOutRequest") @@ -785,63 +878,70 @@ func init() { proto.RegisterType((*ServerLoopInQuoteResponse)(nil), "looprpc.ServerLoopInQuoteResponse") proto.RegisterType((*ServerLoopInTermsRequest)(nil), "looprpc.ServerLoopInTermsRequest") proto.RegisterType((*ServerLoopInTerms)(nil), "looprpc.ServerLoopInTerms") + proto.RegisterType((*ServerLoopOutPushPreimageRequest)(nil), "looprpc.ServerLoopOutPushPreimageRequest") + proto.RegisterType((*ServerLoopOutPushPreimageResponse)(nil), "looprpc.ServerLoopOutPushPreimageResponse") } func init() { proto.RegisterFile("server.proto", fileDescriptor_ad098daeda4239f7) } var fileDescriptor_ad098daeda4239f7 = []byte{ - // 810 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xac, 0x55, 0xdd, 0x52, 0xe2, 0x48, - 0x14, 0xde, 0x04, 0x04, 0x39, 0xf2, 0x67, 0xef, 0xba, 0x1b, 0x50, 0xb6, 0x90, 0xaa, 0xb5, 0x28, - 0x2f, 0xb4, 0xca, 0xbd, 0xdb, 0x3b, 0xd6, 0x5f, 0x6a, 0x59, 0xd1, 0x88, 0x6e, 0xed, 0x55, 0xa6, - 0x85, 0x1e, 0x49, 0x4d, 0x48, 0xc7, 0xa4, 0x41, 0x79, 0x93, 0x99, 0x97, 0x98, 0xb7, 0x99, 0x57, - 0x98, 0xaa, 0xb9, 0x9c, 0x37, 0x98, 0xea, 0xee, 0x44, 0x92, 0x10, 0x54, 0xaa, 0xbc, 0x33, 0xe7, - 0x7c, 0x76, 0x7f, 0xdf, 0x77, 0xce, 0xd7, 0x40, 0xde, 0x23, 0xee, 0x84, 0xb8, 0x7b, 0x8e, 0x4b, - 0x19, 0x45, 0x59, 0x8b, 0x52, 0xc7, 0x75, 0xfa, 0xd5, 0xad, 0x3b, 0x4a, 0xef, 0x2c, 0xb2, 0x8f, - 0x1d, 0x73, 0x1f, 0xdb, 0x36, 0x65, 0x98, 0x99, 0xd4, 0xf6, 0x24, 0xac, 0xf1, 0x4d, 0x81, 0x5f, - 0xae, 0xc4, 0xff, 0x75, 0x28, 0x75, 0xba, 0x63, 0xa6, 0x93, 0xfb, 0x31, 0xf1, 0x18, 0xda, 0x86, - 0xbc, 0x4b, 0xfa, 0xc4, 0x9c, 0x10, 0xd7, 0xf8, 0x40, 0xa6, 0x9a, 0x52, 0x57, 0x9a, 0x79, 0x7d, - 0x2d, 0xa8, 0xfd, 0x43, 0xa6, 0x68, 0x13, 0x72, 0xde, 0x03, 0x76, 0x8c, 0x21, 0xf6, 0x86, 0x9a, - 0x2a, 0xfa, 0xab, 0xbc, 0x70, 0x86, 0xbd, 0x21, 0x2a, 0x43, 0x0a, 0x8f, 0x98, 0x96, 0xaa, 0x2b, - 0xcd, 0xb4, 0xce, 0xff, 0x44, 0x7f, 0x41, 0x45, 0xc0, 0x9d, 0xf1, 0xad, 0x65, 0xf6, 0x05, 0x0b, - 0x63, 0x40, 0xf0, 0xc0, 0x32, 0x6d, 0xa2, 0xa5, 0xeb, 0x4a, 0x33, 0xa5, 0xff, 0xc6, 0x01, 0x17, - 0xb3, 0xfe, 0x91, 0xdf, 0x46, 0x87, 0x50, 0x16, 0x7c, 0xfb, 0xd4, 0x32, 0x26, 0xc4, 0xf5, 0x4c, - 0x6a, 0x6b, 0x2b, 0x75, 0xa5, 0x59, 0x3c, 0xd0, 0xf6, 0x7c, 0xa1, 0x7b, 0x17, 0x3e, 0xe0, 0x46, - 0xf6, 0xf5, 0x92, 0x13, 0x2d, 0x34, 0x3e, 0x2a, 0xb0, 0x11, 0xd3, 0xea, 0x39, 0xd4, 0xf6, 0x08, - 0x17, 0x2b, 0xa8, 0x99, 0xf6, 0x84, 0x9a, 0x7d, 0x22, 0xc4, 0xe6, 0xf4, 0x35, 0x5e, 0x6b, 0xcb, - 0x12, 0xfa, 0x03, 0x8a, 0x8e, 0x4b, 0x1c, 0x3c, 0x7d, 0x02, 0xa9, 0x02, 0x54, 0x90, 0xd5, 0x00, - 0x56, 0x03, 0xf0, 0x88, 0x3d, 0xf0, 0x4d, 0x4b, 0x09, 0x53, 0x72, 0xb2, 0xc2, 0x2d, 0xfb, 0x15, - 0x32, 0xe4, 0xd1, 0x31, 0xdd, 0xa9, 0x10, 0xbc, 0xa2, 0xfb, 0x5f, 0x8d, 0xcf, 0x0a, 0x54, 0x22, - 0xd4, 0x2e, 0xc7, 0x94, 0x91, 0x60, 0x16, 0xbe, 0x97, 0xca, 0x2b, 0xbd, 0x54, 0x97, 0xf7, 0x32, - 0xb5, 0xac, 0x97, 0x9f, 0x54, 0x40, 0xf3, 0x84, 0xd1, 0x2e, 0xac, 0x4b, 0x5e, 0x78, 0x3a, 0x22, - 0x36, 0x33, 0x06, 0xc4, 0x63, 0xbe, 0x9b, 0x25, 0xc1, 0x47, 0xd6, 0x8f, 0xb8, 0xaa, 0x0a, 0x88, - 0x6d, 0x31, 0xde, 0x93, 0x80, 0x72, 0x96, 0x7f, 0x9f, 0x10, 0x82, 0x76, 0xa0, 0x10, 0xb4, 0x0c, - 0x17, 0x33, 0x22, 0xf8, 0xa5, 0xfe, 0x56, 0x35, 0x45, 0x0e, 0xe5, 0x84, 0x10, 0x1d, 0x33, 0xe1, - 0xb6, 0x3f, 0x14, 0xee, 0x4f, 0x5a, 0xf8, 0x93, 0x93, 0x95, 0xd6, 0x88, 0xa1, 0x5d, 0x28, 0x8d, - 0x4c, 0xdb, 0x10, 0x47, 0xe1, 0x11, 0x1d, 0xdb, 0x4c, 0x2c, 0x4d, 0x5a, 0x1c, 0x54, 0x18, 0x99, - 0xf6, 0xd5, 0x03, 0x76, 0x5a, 0xa2, 0x21, 0xb0, 0xf8, 0x31, 0x82, 0xcd, 0x84, 0xb0, 0xf8, 0x31, - 0x84, 0xad, 0x01, 0xf4, 0x2d, 0x36, 0x31, 0x06, 0xc4, 0x62, 0x58, 0xcb, 0x8a, 0x49, 0xe6, 0x78, - 0xe5, 0x88, 0x17, 0x1a, 0xef, 0x62, 0xb3, 0xec, 0x11, 0x77, 0xe4, 0x05, 0xb3, 0x4c, 0x72, 0x5f, - 0x59, 0xd6, 0xfd, 0x41, 0xcc, 0x7c, 0x71, 0x03, 0xda, 0x99, 0x97, 0x2b, 0x57, 0x26, 0x26, 0x75, - 0x67, 0x5e, 0xaa, 0xea, 0xe3, 0xc2, 0x32, 0x1b, 0x5f, 0x15, 0xf8, 0x79, 0x76, 0x4d, 0xdb, 0x0e, - 0x24, 0x44, 0x77, 0x5c, 0x89, 0xef, 0xf8, 0x92, 0xcf, 0x42, 0x3c, 0x7b, 0xe9, 0xf9, 0xec, 0x55, - 0x60, 0xd5, 0xc2, 0x1e, 0x33, 0x86, 0xd4, 0x11, 0x03, 0xcc, 0xeb, 0x59, 0xfe, 0x7d, 0x46, 0x9d, - 0x44, 0x3b, 0x33, 0xcb, 0xda, 0x79, 0x19, 0x7e, 0x03, 0xb9, 0xce, 0xd9, 0xb3, 0xf0, 0xd2, 0x1b, - 0x38, 0x0b, 0xb4, 0x1a, 0x09, 0xf4, 0x3d, 0x68, 0xe1, 0x23, 0x5f, 0x88, 0x73, 0x92, 0x0a, 0x75, - 0x59, 0x15, 0x5f, 0x22, 0x6f, 0xc8, 0xd3, 0x9d, 0xbe, 0x96, 0x70, 0xda, 0x94, 0x17, 0xd2, 0xa6, - 0x26, 0xa7, 0x2d, 0x21, 0x4e, 0xe9, 0x25, 0xe2, 0xb4, 0xf2, 0xba, 0x38, 0x65, 0xe2, 0x71, 0x32, - 0xa2, 0x56, 0xbe, 0x7d, 0x9a, 0xfa, 0xb0, 0x3e, 0x77, 0xc1, 0x5b, 0x87, 0x69, 0xb7, 0x03, 0xa5, - 0x18, 0x11, 0x04, 0x90, 0xe9, 0x1c, 0x9f, 0xb6, 0x0e, 0xff, 0x2f, 0xff, 0x84, 0x10, 0x14, 0xff, - 0xbd, 0xee, 0xf4, 0xda, 0x46, 0xa7, 0xdb, 0xbd, 0x30, 0xba, 0xd7, 0xbd, 0xb2, 0x82, 0x2a, 0xb0, - 0x71, 0xde, 0xea, 0xb5, 0x6f, 0x8e, 0x8d, 0xab, 0xe3, 0xd3, 0xff, 0xda, 0x3d, 0xd9, 0x6b, 0x9f, - 0x97, 0xd5, 0x83, 0xef, 0x29, 0x00, 0x7e, 0xb8, 0xe4, 0x8d, 0xba, 0x90, 0x8f, 0xbc, 0x04, 0x8d, - 0x27, 0xf1, 0x0b, 0x1f, 0xa2, 0xea, 0xe6, 0x33, 0x18, 0xd4, 0x85, 0xe2, 0x39, 0x79, 0xf0, 0x4b, - 0xfc, 0x22, 0x54, 0x4b, 0x86, 0x07, 0xa7, 0xfd, 0xbe, 0xa8, 0xed, 0xaf, 0xdf, 0x8c, 0xa1, 0xfc, - 0xa1, 0x58, 0xc0, 0x30, 0x9c, 0x93, 0x45, 0x0c, 0xe5, 0x01, 0x1d, 0x58, 0x0b, 0x8f, 0x6b, 0x3b, - 0x01, 0x1b, 0xdd, 0x95, 0x6a, 0x75, 0x31, 0x04, 0x75, 0xa0, 0xe0, 0xeb, 0x6d, 0x8b, 0xe1, 0xa2, - 0xad, 0x44, 0x70, 0x70, 0x54, 0x6d, 0x41, 0xd7, 0x17, 0xdb, 0x0b, 0xb8, 0x49, 0xaa, 0xc9, 0xdc, - 0x22, 0x52, 0x1b, 0xcf, 0x41, 0xe4, 0xa9, 0xb7, 0x19, 0xb1, 0xb7, 0x7f, 0xfe, 0x08, 0x00, 0x00, - 0xff, 0xff, 0xd3, 0x5d, 0x9c, 0x31, 0xe8, 0x09, 0x00, 0x00, + // 884 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xac, 0x55, 0x41, 0x73, 0xda, 0x46, + 0x14, 0xae, 0x04, 0xc6, 0xe6, 0x19, 0xdb, 0x64, 0xd3, 0xa4, 0x82, 0xc4, 0x1d, 0x4c, 0xa7, 0x1e, + 0xd7, 0x07, 0x67, 0x26, 0xbd, 0xf5, 0x46, 0x63, 0x62, 0x33, 0x25, 0x86, 0x08, 0x9c, 0x4e, 0x4f, + 0xea, 0x06, 0x5e, 0x8d, 0xa6, 0x42, 0xbb, 0x91, 0x16, 0x6c, 0xce, 0xfd, 0x13, 0xed, 0x9f, 0xe8, + 0xbf, 0xe9, 0xb9, 0xb7, 0xce, 0xf4, 0x5f, 0x64, 0xb4, 0xbb, 0x32, 0x48, 0x08, 0xdb, 0xcc, 0xf8, + 0xc6, 0xbe, 0xf7, 0xe9, 0xed, 0xfb, 0xbe, 0xb7, 0xdf, 0x03, 0x4a, 0x21, 0x06, 0x53, 0x0c, 0x4e, + 0x78, 0xc0, 0x04, 0x23, 0x9b, 0x1e, 0x63, 0x3c, 0xe0, 0x83, 0xea, 0xcb, 0x2b, 0xc6, 0xae, 0x3c, + 0x7c, 0x45, 0xb9, 0xfb, 0x8a, 0xfa, 0x3e, 0x13, 0x54, 0xb8, 0xcc, 0x0f, 0x15, 0xac, 0xfe, 0xbf, + 0x01, 0x5f, 0xf6, 0xe4, 0x77, 0x6d, 0xc6, 0x78, 0x67, 0x22, 0x6c, 0xfc, 0x34, 0xc1, 0x50, 0x90, + 0x03, 0x28, 0x05, 0x38, 0x40, 0x77, 0x8a, 0x81, 0xf3, 0x3b, 0xce, 0x2c, 0xa3, 0x66, 0x1c, 0x95, + 0xec, 0xed, 0x38, 0xf6, 0x13, 0xce, 0xc8, 0x0b, 0x28, 0x86, 0xd7, 0x94, 0x3b, 0x23, 0x1a, 0x8e, + 0x2c, 0x53, 0xe6, 0xb7, 0xa2, 0xc0, 0x39, 0x0d, 0x47, 0xa4, 0x0c, 0x39, 0x3a, 0x16, 0x56, 0xae, + 0x66, 0x1c, 0xe5, 0xed, 0xe8, 0x27, 0xf9, 0x01, 0x2a, 0x12, 0xce, 0x27, 0x1f, 0x3d, 0x77, 0x20, + 0xbb, 0x70, 0x86, 0x48, 0x87, 0x9e, 0xeb, 0xa3, 0x95, 0xaf, 0x19, 0x47, 0x39, 0xfb, 0xab, 0x08, + 0xd0, 0x9d, 0xe7, 0x4f, 0x75, 0x9a, 0xbc, 0x81, 0xb2, 0xec, 0x77, 0xc0, 0x3c, 0x67, 0x8a, 0x41, + 0xe8, 0x32, 0xdf, 0xda, 0xa8, 0x19, 0x47, 0xbb, 0xaf, 0xad, 0x13, 0x4d, 0xf4, 0xa4, 0xab, 0x01, + 0x1f, 0x54, 0xde, 0xde, 0xe3, 0xc9, 0x40, 0xfd, 0x4f, 0x03, 0x9e, 0xa5, 0xb8, 0x86, 0x9c, 0xf9, + 0x21, 0x46, 0x64, 0x65, 0x6b, 0xae, 0x3f, 0x65, 0xee, 0x00, 0x25, 0xd9, 0xa2, 0xbd, 0x1d, 0xc5, + 0x5a, 0x2a, 0x44, 0xbe, 0x85, 0x5d, 0x1e, 0x20, 0xa7, 0xb3, 0x5b, 0x90, 0x29, 0x41, 0x3b, 0x2a, + 0x1a, 0xc3, 0xf6, 0x01, 0x42, 0xf4, 0x87, 0x5a, 0xb4, 0x9c, 0x14, 0xa5, 0xa8, 0x22, 0x91, 0x64, + 0xcf, 0xa1, 0x80, 0x37, 0xdc, 0x0d, 0x66, 0x92, 0xf0, 0x86, 0xad, 0x4f, 0xf5, 0xbf, 0x0d, 0xa8, + 0x24, 0x5a, 0x7b, 0x3f, 0x61, 0x02, 0xe3, 0x59, 0x68, 0x2d, 0x8d, 0x07, 0x6a, 0x69, 0xae, 0xaf, + 0x65, 0x6e, 0x5d, 0x2d, 0xff, 0x32, 0x81, 0x2c, 0x37, 0x4c, 0x8e, 0xe1, 0x89, 0xea, 0x8b, 0xce, + 0xc6, 0xe8, 0x0b, 0x67, 0x88, 0xa1, 0xd0, 0x6a, 0xee, 0xc9, 0x7e, 0x54, 0xfc, 0x34, 0x62, 0x55, + 0x01, 0xf9, 0x5a, 0x9c, 0xdf, 0x30, 0x6e, 0x79, 0x33, 0x3a, 0xbf, 0x45, 0x24, 0x87, 0xb0, 0x13, + 0xa7, 0x9c, 0x80, 0x0a, 0x94, 0xfd, 0xe5, 0x7e, 0x34, 0x2d, 0x43, 0x0d, 0xe5, 0x2d, 0xa2, 0x4d, + 0x85, 0x54, 0x5b, 0x0f, 0x25, 0xd2, 0x27, 0x2f, 0xf5, 0x29, 0xaa, 0x48, 0x63, 0x2c, 0xc8, 0x31, + 0xec, 0x8d, 0x5d, 0xdf, 0x91, 0xa5, 0xe8, 0x98, 0x4d, 0x7c, 0x21, 0x1f, 0x4d, 0x5e, 0x16, 0xda, + 0x19, 0xbb, 0x7e, 0xef, 0x9a, 0xf2, 0x86, 0x4c, 0x48, 0x2c, 0xbd, 0x49, 0x60, 0x0b, 0x0b, 0x58, + 0x7a, 0xb3, 0x80, 0xdd, 0x07, 0x18, 0x78, 0x62, 0xea, 0x0c, 0xd1, 0x13, 0xd4, 0xda, 0x94, 0x93, + 0x2c, 0x46, 0x91, 0xd3, 0x28, 0x50, 0xff, 0x35, 0x35, 0xcb, 0x3e, 0x06, 0xe3, 0x30, 0x9e, 0x65, + 0x96, 0xfa, 0xc6, 0xba, 0xea, 0x0f, 0x53, 0xe2, 0xcb, 0x1b, 0xc8, 0xe1, 0x32, 0x5d, 0xf5, 0x64, + 0x52, 0x54, 0x0f, 0x97, 0xa9, 0x9a, 0x1a, 0xb7, 0x48, 0xb3, 0xfe, 0x9f, 0x01, 0x4f, 0xe7, 0xd7, + 0xb4, 0xfc, 0x98, 0x42, 0xf2, 0x8d, 0x1b, 0xe9, 0x37, 0xbe, 0xe6, 0x5a, 0x48, 0x7b, 0x2f, 0xbf, + 0xec, 0xbd, 0x0a, 0x6c, 0x79, 0x34, 0x14, 0xce, 0x88, 0x71, 0x39, 0xc0, 0x92, 0xbd, 0x19, 0x9d, + 0xcf, 0x19, 0xcf, 0x94, 0xb3, 0xb0, 0xae, 0x9c, 0xef, 0x17, 0x77, 0x60, 0xc4, 0x73, 0xbe, 0x16, + 0xee, 0xdb, 0x81, 0x73, 0x43, 0x9b, 0x09, 0x43, 0x7f, 0x02, 0x6b, 0xb1, 0xe4, 0x3d, 0x76, 0xce, + 0x62, 0x61, 0xae, 0xcb, 0xe2, 0x9f, 0xc4, 0x0e, 0xb9, 0xbd, 0x53, 0x73, 0x59, 0x74, 0x9b, 0x71, + 0x8f, 0xdb, 0xcc, 0x6c, 0xb7, 0x65, 0xd8, 0x29, 0xbf, 0x86, 0x9d, 0x36, 0x1e, 0x66, 0xa7, 0x42, + 0xda, 0x4e, 0x4e, 0x52, 0xca, 0xc7, 0x77, 0xd3, 0x00, 0x9e, 0x2c, 0x5d, 0xf0, 0xe8, 0x66, 0xfa, + 0xc3, 0x80, 0x5a, 0xc2, 0xb3, 0xdd, 0x49, 0x38, 0xea, 0x06, 0xe8, 0x8e, 0xe9, 0x15, 0x3e, 0x26, + 0x1d, 0x52, 0x85, 0x2d, 0xae, 0xeb, 0xc6, 0xf6, 0x8b, 0xcf, 0xf5, 0x6f, 0xe0, 0xe0, 0x8e, 0x26, + 0xd4, 0x53, 0x39, 0x1e, 0xc1, 0x5e, 0xea, 0x12, 0x02, 0x50, 0x68, 0x37, 0xcf, 0x1a, 0x6f, 0x7e, + 0x29, 0x7f, 0x41, 0x08, 0xec, 0xbe, 0xbb, 0x6c, 0xf7, 0x5b, 0x4e, 0xbb, 0xd3, 0xe9, 0x3a, 0x9d, + 0xcb, 0x7e, 0xd9, 0x20, 0x15, 0x78, 0x76, 0xd1, 0xe8, 0xb7, 0x3e, 0x34, 0x9d, 0x5e, 0xf3, 0xec, + 0xe7, 0x56, 0x5f, 0xe5, 0x5a, 0x17, 0x65, 0x93, 0x54, 0xe1, 0x79, 0xd7, 0x6e, 0xb6, 0xde, 0x35, + 0xce, 0x9a, 0x4e, 0xf7, 0xb2, 0x77, 0x3e, 0xff, 0x2c, 0xf7, 0xfa, 0xdf, 0x3c, 0x40, 0xa4, 0x91, + 0xea, 0x89, 0x74, 0xa0, 0x94, 0x58, 0x68, 0xf5, 0x5b, 0xd2, 0x2b, 0xf7, 0x69, 0xf5, 0xc5, 0x1d, + 0x18, 0xd2, 0x81, 0xdd, 0x0b, 0xbc, 0xd6, 0xa1, 0xe8, 0x22, 0xb2, 0x9f, 0x0d, 0x8f, 0xab, 0x7d, + 0xbd, 0x2a, 0xad, 0x5d, 0xe4, 0xc1, 0xd3, 0x0c, 0xe5, 0xc8, 0x77, 0xd9, 0x9f, 0x65, 0x8c, 0xb8, + 0x7a, 0xfc, 0x10, 0xa8, 0xbe, 0x6d, 0xae, 0x87, 0xfa, 0x77, 0x5d, 0xa1, 0xc7, 0xe2, 0x72, 0x59, + 0xa5, 0x87, 0x2a, 0xd0, 0x86, 0xed, 0xc5, 0x37, 0x7e, 0x90, 0x81, 0x4d, 0x1a, 0xac, 0x5a, 0x5d, + 0x0d, 0x21, 0x6d, 0xd8, 0xd1, 0xea, 0xb6, 0xa4, 0x23, 0xc8, 0xcb, 0x4c, 0x70, 0x5c, 0x6a, 0x7f, + 0x45, 0x56, 0x93, 0xed, 0xc7, 0xbd, 0xa9, 0x56, 0xb3, 0x7b, 0x4b, 0x50, 0xad, 0xdf, 0x05, 0x51, + 0x55, 0x3f, 0x16, 0xa4, 0x3b, 0xbe, 0xff, 0x1c, 0x00, 0x00, 0xff, 0xff, 0x76, 0xc4, 0xb9, 0x29, + 0x1d, 0x0b, 0x00, 0x00, } // Reference imports to suppress errors if they are not otherwise used. @@ -858,6 +958,7 @@ const _ = grpc.SupportPackageIsVersion4 type SwapServerClient interface { LoopOutTerms(ctx context.Context, in *ServerLoopOutTermsRequest, opts ...grpc.CallOption) (*ServerLoopOutTerms, error) NewLoopOutSwap(ctx context.Context, in *ServerLoopOutRequest, opts ...grpc.CallOption) (*ServerLoopOutResponse, error) + LoopOutPushPreimage(ctx context.Context, in *ServerLoopOutPushPreimageRequest, opts ...grpc.CallOption) (*ServerLoopOutPushPreimageResponse, error) LoopOutQuote(ctx context.Context, in *ServerLoopOutQuoteRequest, opts ...grpc.CallOption) (*ServerLoopOutQuote, error) LoopInTerms(ctx context.Context, in *ServerLoopInTermsRequest, opts ...grpc.CallOption) (*ServerLoopInTerms, error) NewLoopInSwap(ctx context.Context, in *ServerLoopInRequest, opts ...grpc.CallOption) (*ServerLoopInResponse, error) @@ -890,6 +991,15 @@ func (c *swapServerClient) NewLoopOutSwap(ctx context.Context, in *ServerLoopOut return out, nil } +func (c *swapServerClient) LoopOutPushPreimage(ctx context.Context, in *ServerLoopOutPushPreimageRequest, opts ...grpc.CallOption) (*ServerLoopOutPushPreimageResponse, error) { + out := new(ServerLoopOutPushPreimageResponse) + err := c.cc.Invoke(ctx, "/looprpc.SwapServer/LoopOutPushPreimage", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + func (c *swapServerClient) LoopOutQuote(ctx context.Context, in *ServerLoopOutQuoteRequest, opts ...grpc.CallOption) (*ServerLoopOutQuote, error) { out := new(ServerLoopOutQuote) err := c.cc.Invoke(ctx, "/looprpc.SwapServer/LoopOutQuote", in, out, opts...) @@ -930,6 +1040,7 @@ func (c *swapServerClient) LoopInQuote(ctx context.Context, in *ServerLoopInQuot type SwapServerServer interface { LoopOutTerms(context.Context, *ServerLoopOutTermsRequest) (*ServerLoopOutTerms, error) NewLoopOutSwap(context.Context, *ServerLoopOutRequest) (*ServerLoopOutResponse, error) + LoopOutPushPreimage(context.Context, *ServerLoopOutPushPreimageRequest) (*ServerLoopOutPushPreimageResponse, error) LoopOutQuote(context.Context, *ServerLoopOutQuoteRequest) (*ServerLoopOutQuote, error) LoopInTerms(context.Context, *ServerLoopInTermsRequest) (*ServerLoopInTerms, error) NewLoopInSwap(context.Context, *ServerLoopInRequest) (*ServerLoopInResponse, error) @@ -976,6 +1087,24 @@ func _SwapServer_NewLoopOutSwap_Handler(srv interface{}, ctx context.Context, de return interceptor(ctx, in, info, handler) } +func _SwapServer_LoopOutPushPreimage_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ServerLoopOutPushPreimageRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(SwapServerServer).LoopOutPushPreimage(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/looprpc.SwapServer/LoopOutPushPreimage", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(SwapServerServer).LoopOutPushPreimage(ctx, req.(*ServerLoopOutPushPreimageRequest)) + } + return interceptor(ctx, in, info, handler) +} + func _SwapServer_LoopOutQuote_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(ServerLoopOutQuoteRequest) if err := dec(in); err != nil { @@ -1060,6 +1189,10 @@ var _SwapServer_serviceDesc = grpc.ServiceDesc{ MethodName: "NewLoopOutSwap", Handler: _SwapServer_NewLoopOutSwap_Handler, }, + { + MethodName: "LoopOutPushPreimage", + Handler: _SwapServer_LoopOutPushPreimage_Handler, + }, { MethodName: "LoopOutQuote", Handler: _SwapServer_LoopOutQuote_Handler, diff --git a/looprpc/server.proto b/looprpc/server.proto index de8996e..d8d256e 100644 --- a/looprpc/server.proto +++ b/looprpc/server.proto @@ -9,6 +9,9 @@ service SwapServer { rpc NewLoopOutSwap (ServerLoopOutRequest) returns (ServerLoopOutResponse); + rpc LoopOutPushPreimage (ServerLoopOutPushPreimageRequest) + returns (ServerLoopOutPushPreimageResponse); + rpc LoopOutQuote (ServerLoopOutQuoteRequest) returns (ServerLoopOutQuote); rpc LoopInTerms (ServerLoopInTermsRequest) returns (ServerLoopInTerms); @@ -37,6 +40,14 @@ enum ProtocolVersion { published htlcs may use native (P2WSH) or nested (NP2WSH) segwit as well. */ NATIVE_SEGWIT_LOOP_IN = 2; + + /* + Once the on chain loop out htlc is confirmed, the client can push the swap + preimage to the server to speed up claim of their off chain htlc (acquiring + incoming liquidity more quickly than if the server waited for the on chain + claim tx). + */ + PREIMAGE_PUSH_LOOP_OUT = 3; } message ServerLoopOutRequest { @@ -143,3 +154,20 @@ message ServerLoopInTerms { uint64 min_swap_amount = 1; uint64 max_swap_amount = 2; } + +// ServerLoopOutPushPreimageRequest pushes a preimage to the server. Note that +// this call returns with no error after the server acknowledges the preimage +// and does not block until the invoice is settled. +message ServerLoopOutPushPreimageRequest { + // The protocol version that the client adheres to. + ProtocolVersion protocol_version = 1; + + /* + Preimage is the preimage of the loop out swap that we wish to push to the + server to speed up off-chain claim once the on-chain htlc has confirmed. + */ + bytes preimage = 2; +} + +message ServerLoopOutPushPreimageResponse { +} diff --git a/server_mock_test.go b/server_mock_test.go index 21dba55..c58ced9 100644 --- a/server_mock_test.go +++ b/server_mock_test.go @@ -36,6 +36,9 @@ type serverMock struct { swapInvoice string swapHash lntypes.Hash + + // preimagePush is a channel that preimage pushes are sent into. + preimagePush chan lntypes.Preimage } var _ swapServerClient = (*serverMock)(nil) @@ -49,6 +52,8 @@ func newServerMock() *serverMock { prepayInvoiceAmt: 100, height: 600, + + preimagePush: make(chan lntypes.Preimage), } } @@ -151,6 +156,15 @@ func (s *serverMock) NewLoopInSwap(ctx context.Context, return resp, nil } +func (s *serverMock) PushLoopOutPreimage(_ context.Context, + preimage lntypes.Preimage) error { + + // Push the preimage into the mock's preimage channel. + s.preimagePush <- preimage + + return nil +} + func (s *serverMock) GetLoopInTerms(ctx context.Context) ( *LoopInTerms, error) { diff --git a/swap_server_client.go b/swap_server_client.go index c679668..b4ab95f 100644 --- a/swap_server_client.go +++ b/swap_server_client.go @@ -22,7 +22,7 @@ import ( // protocolVersion defines the version of the protocol that is currently // supported by the loop client. -const protocolVersion = looprpc.ProtocolVersion_NATIVE_SEGWIT_LOOP_IN +const protocolVersion = looprpc.ProtocolVersion_PREIMAGE_PUSH_LOOP_OUT type swapServerClient interface { GetLoopOutTerms(ctx context.Context) ( @@ -44,6 +44,9 @@ type swapServerClient interface { swapPublicationDeadline time.Time) ( *newLoopOutResponse, error) + PushLoopOutPreimage(ctx context.Context, + preimage lntypes.Preimage) error + NewLoopInSwap(ctx context.Context, swapHash lntypes.Hash, amount btcutil.Amount, senderKey [33]byte, swapInvoice string, lastHop *route.Vertex) ( @@ -215,6 +218,23 @@ func (s *grpcSwapServerClient) NewLoopOutSwap(ctx context.Context, }, nil } +// PushLoopOutPreimage pushes a preimage to the server. +func (s *grpcSwapServerClient) PushLoopOutPreimage(ctx context.Context, + preimage lntypes.Preimage) error { + + rpcCtx, rpcCancel := context.WithTimeout(ctx, globalCallTimeout) + defer rpcCancel() + + _, err := s.server.LoopOutPushPreimage(rpcCtx, + &looprpc.ServerLoopOutPushPreimageRequest{ + ProtocolVersion: protocolVersion, + Preimage: preimage[:], + }, + ) + + return err +} + func (s *grpcSwapServerClient) NewLoopInSwap(ctx context.Context, swapHash lntypes.Hash, amount btcutil.Amount, senderKey [33]byte, swapInvoice string, lastHop *route.Vertex) (*newLoopInResponse, error) { diff --git a/test/context.go b/test/context.go index 2ee09f9..fbb8850 100644 --- a/test/context.go +++ b/test/context.go @@ -94,6 +94,24 @@ func (ctx *Context) AssertRegisterSpendNtfn(script []byte) { } } +// AssertTrackPayment asserts that a call was made to track payment, and +// returns the track payment message so that it can be used to send updates +// to the test. +func (ctx *Context) AssertTrackPayment() TrackPaymentMessage { + ctx.T.Helper() + + var msg TrackPaymentMessage + select { + case msg = <-ctx.Lnd.TrackPaymentChannel: + + case <-time.After(Timeout): + DumpGoroutines() + ctx.T.Fatalf("payment not tracked") + } + + return msg +} + // AssertRegisterConf asserts that a register for conf has been received. func (ctx *Context) AssertRegisterConf() *ConfRegistration { ctx.T.Helper() diff --git a/testcontext_test.go b/testcontext_test.go index e49ec0d..5bdaf2f 100644 --- a/testcontext_test.go +++ b/testcontext_test.go @@ -7,12 +7,15 @@ import ( "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcutil" + "github.com/lightninglabs/loop/lndclient" "github.com/lightninglabs/loop/loopdb" "github.com/lightninglabs/loop/swap" "github.com/lightninglabs/loop/sweep" "github.com/lightninglabs/loop/test" "github.com/lightningnetwork/lnd/chainntnfs" + "github.com/lightningnetwork/lnd/lnrpc" "github.com/lightningnetwork/lnd/lntypes" + "github.com/stretchr/testify/require" ) var ( @@ -231,3 +234,30 @@ func (ctx *testContext) publishHtlc(script []byte, Index: 0, } } + +// trackPayment asserts that a call to track payment was sent and sends the +// status provided into the updates channel. +func (ctx *testContext) trackPayment(status lnrpc.Payment_PaymentStatus) { + trackPayment := ctx.Context.AssertTrackPayment() + + select { + case trackPayment.Updates <- lndclient.PaymentStatus{ + State: status, + }: + + case <-time.After(test.Timeout): + ctx.T.Fatalf("could not send payment update") + } +} + +// assertPreimagePush asserts that we made an attempt to push our preimage to +// the server. +func (ctx *testContext) assertPreimagePush(preimage lntypes.Preimage) { + select { + case pushedPreimage := <-ctx.serverMock.preimagePush: + require.Equal(ctx.T, preimage, pushedPreimage) + + case <-time.After(test.Timeout): + ctx.T.Fatalf("preimage not pushed") + } +}