diff --git a/cmd/loop/liquidity.go b/cmd/loop/liquidity.go index 8a9b17a3..6a70bb45 100644 --- a/cmd/loop/liquidity.go +++ b/cmd/loop/liquidity.go @@ -343,7 +343,8 @@ var setParamsCommand = &cli.Command{ &cli.StringFlag{ Name: "loopinsource", Usage: "the loop-in source to use for autoloop rules: " + - "wallet or static-address.", + "wallet or static-address. Static-address " + + "requires loopd --experimental.", }, &cli.BoolFlag{ Name: "easyautoloop", diff --git a/docs/loop.1 b/docs/loop.1 index ff8c7ebf..dd044978 100644 --- a/docs/loop.1 +++ b/docs/loop.1 @@ -360,7 +360,7 @@ update the parameters set for the liquidity manager \fB--localbalancesat\fP="": the target size of total local balance in satoshis, used by easy autoloop. (default: 0) .PP -\fB--loopinsource\fP="": the loop-in source to use for autoloop rules: wallet or static-address. +\fB--loopinsource\fP="": the loop-in source to use for autoloop rules: wallet or static-address. Static-address requires loopd --experimental. .PP \fB--maxamt\fP="": the maximum amount in satoshis that the autoloop client will dispatch per-swap. (default: 0) diff --git a/docs/loop.md b/docs/loop.md index fc3cb901..62f1d86f 100644 --- a/docs/loop.md +++ b/docs/loop.md @@ -371,7 +371,7 @@ The following flags are supported: | `--minamt="…"` | the minimum amount in satoshis that the autoloop client will dispatch per-swap | uint | `0` | | `--maxamt="…"` | the maximum amount in satoshis that the autoloop client will dispatch per-swap | uint | `0` | | `--htlc_conf="…"` | the confirmation target for loop in on-chain htlcs | int | `0` | -| `--loopinsource="…"` | the loop-in source to use for autoloop rules: wallet or static-address | string | +| `--loopinsource="…"` | the loop-in source to use for autoloop rules: wallet or static-address. Static-address requires loopd --experimental | string | | `--easyautoloop` | set to true to enable easy autoloop, which will automatically dispatch swaps in order to meet the target local balance | bool | `false` | | `--localbalancesat="…"` | the target size of total local balance in satoshis, used by easy autoloop | uint | `0` | | `--easyautoloop_excludepeer="…"` | list of peer pubkeys (hex) to exclude from easy autoloop channel selection; repeat --easyautoloop_excludepeer for multiple peers | string | `[]` | diff --git a/liquidity/liquidity.go b/liquidity/liquidity.go index 8a32adcd..2a286a31 100644 --- a/liquidity/liquidity.go +++ b/liquidity/liquidity.go @@ -168,6 +168,11 @@ var ( // account address type is not or vice versa. ErrAccountAndAddrType = errors.New("account and address type have " + "to be both either set or unset") + + // ErrStaticAddressAutoloopExperimental is returned when static-address + // autoloop is configured without the daemon experimental flag. + ErrStaticAddressAutoloopExperimental = errors.New("static address " + + "autoloop requires --experimental") ) // Config contains the external functionality required to run the @@ -226,6 +231,10 @@ type Config struct { request *loop.StaticAddressLoopInRequest) ( *StaticLoopInDispatchResult, error) + // EnableStaticAddressAutoloop allows the liquidity manager to accept + // static-address loop-in sources for new autoloop parameters. + EnableStaticAddressAutoloop bool + // LoopInTerms returns the terms for a loop in swap. LoopInTerms func(ctx context.Context, initiator string) (*loop.LoopInTerms, error) @@ -392,6 +401,15 @@ func (m *Manager) SetParameters(ctx context.Context, func (m *Manager) setParameters(ctx context.Context, params Parameters) error { + // Static-address autoloop is still experimental. Check this before + // any network-dependent validation so persisted params cannot start + // the feature after a restart without the daemon opt-in. + if params.LoopInSource == LoopInSourceStaticAddress && + !m.cfg.EnableStaticAddressAutoloop { + + return ErrStaticAddressAutoloopExperimental + } + restrictions, err := m.cfg.Restrictions( ctx, swap.TypeOut, getInitiator(m.params), ) diff --git a/liquidity/liquidity_test.go b/liquidity/liquidity_test.go index 77744733..a7f8977a 100644 --- a/liquidity/liquidity_test.go +++ b/liquidity/liquidity_test.go @@ -387,6 +387,7 @@ func TestPersistParams(t *testing.T) { LoopInSource: clientrpc.LoopInSource_LOOP_IN_SOURCE_STATIC_ADDRESS, } cfg, _ := newTestConfig() + cfg.EnableStaticAddressAutoloop = true manager := NewManager(cfg) ctx := t.Context() @@ -440,6 +441,26 @@ func TestPersistParams(t *testing.T) { ) } +// TestStaticAddressAutoloopRequiresExperimental verifies that static-address +// loop-in sources are rejected unless the daemon enabled the experimental +// static autoloop path. +func TestStaticAddressAutoloopRequiresExperimental(t *testing.T) { + ctx := t.Context() + + cfg, _ := newTestConfig() + manager := NewManager(cfg) + + params := manager.GetParameters() + params.LoopInSource = LoopInSourceStaticAddress + + err := manager.setParameters(ctx, params) + require.ErrorIs(t, err, ErrStaticAddressAutoloopExperimental) + + cfg.EnableStaticAddressAutoloop = true + err = manager.setParameters(ctx, params) + require.NoError(t, err) +} + // TestRestrictedSuggestions tests getting of swap suggestions when we have // other in-flight swaps. We setup our manager with a set of channels and rules // that require a loop out swap, focusing on the filtering our of channels that diff --git a/liquidity/static_loopin_test.go b/liquidity/static_loopin_test.go index 55860cd5..9690a00f 100644 --- a/liquidity/static_loopin_test.go +++ b/liquidity/static_loopin_test.go @@ -286,6 +286,7 @@ func TestSuggestSwapsStaticLoopInNoCandidate(t *testing.T) { ctx := t.Context() cfg, lnd := newTestConfig() + cfg.EnableStaticAddressAutoloop = true cfg.PrepareStaticLoopIn = func(context.Context, route.Vertex, btcutil.Amount, btcutil.Amount, string, string, []string) (*PreparedStaticLoopIn, error) { @@ -330,6 +331,7 @@ func TestSuggestSwapsMixedInFlightCount(t *testing.T) { ctx := t.Context() cfg, lnd := newTestConfig() + cfg.EnableStaticAddressAutoloop = true cfg.PrepareStaticLoopIn = func(_ context.Context, peer route.Vertex, _, _ btcutil.Amount, label, initiator string, _ []string) (*PreparedStaticLoopIn, error) { @@ -388,6 +390,7 @@ func TestAutoLoopDispatchesStaticLoopIn(t *testing.T) { ctx := t.Context() cfg, lnd := newTestConfig() + cfg.EnableStaticAddressAutoloop = true var ( prepareCalls int diff --git a/loopd/config.go b/loopd/config.go index 63c47269..0e0e065e 100644 --- a/loopd/config.go +++ b/loopd/config.go @@ -192,7 +192,7 @@ type Config struct { MaxStaticAddrHtlcFeePercentage float64 `long:"maxstaticaddrhtlcfeepercentage" description:"The maximum fee percentage that the server can charge for the htlc tx."` MaxStaticAddrHtlcBackupFeePercentage float64 `long:"maxstaticaddrhtlcbackupfeepercentage" description:"The maximum fee percentage that the server can charge for the htlc backup tx. The backup transaction is only used in rare cases when the regular htlc tx is not confirmed on time. These backup transactions refer to high fee or extremely high fee transactions in the API."` - EnableExperimental bool `long:"experimental" description:"Enable experimental features: reservations"` + EnableExperimental bool `long:"experimental" description:"Enable experimental features: reservations, static autoloop"` MigrationRPCBatchSize int `long:"migrationrpcbatchsize" description:"The RPC batch size to use during migrations."` diff --git a/loopd/daemon.go b/loopd/daemon.go index 9b00f4e0..28f05a95 100644 --- a/loopd/daemon.go +++ b/loopd/daemon.go @@ -735,7 +735,7 @@ func (d *Daemon) initialize(withMacaroonService bool) error { } liquidityMgr := getLiquidityManager( - swapClient, staticLoopInManager, + swapClient, staticLoopInManager, d.cfg.EnableExperimental, ) // Now finally fully initialize the swap client RPC server instance. diff --git a/loopd/swapclient_server.go b/loopd/swapclient_server.go index f26f1d91..8a4a41cf 100644 --- a/loopd/swapclient_server.go +++ b/loopd/swapclient_server.go @@ -1395,6 +1395,18 @@ func (s *swapClientServer) SetLiquidityParams(ctx context.Context, in *looprpc.SetLiquidityParamsRequest) (*looprpc.SetLiquidityParamsResponse, error) { + enableExperimental := s.config != nil && s.config.EnableExperimental + params := in.GetParameters() + if params.GetLoopInSource() == + looprpc.LoopInSource_LOOP_IN_SOURCE_STATIC_ADDRESS && + !enableExperimental { + + return nil, status.Error( + codes.FailedPrecondition, + liquidity.ErrStaticAddressAutoloopExperimental.Error(), + ) + } + err := s.liquidityMgr.SetParameters(ctx, in.Parameters) if err != nil { return nil, err diff --git a/loopd/swapclient_server_test.go b/loopd/swapclient_server_test.go index d7a30f6d..fff26c64 100644 --- a/loopd/swapclient_server_test.go +++ b/loopd/swapclient_server_test.go @@ -28,6 +28,8 @@ import ( "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/routing/route" "github.com/stretchr/testify/require" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" ) var ( @@ -281,6 +283,29 @@ func TestStaticAddressLoopInRejectsReservedLabel(t *testing.T) { require.ErrorContains(t, err, labels.ErrReservedPrefix.Error()) } +// TestSetLiquidityParamsRejectsStaticAutoloopWithoutExperimental verifies that +// users must restart loopd with --experimental before enabling static-address +// autoloop. +func TestSetLiquidityParamsRejectsStaticAutoloopWithoutExperimental( + t *testing.T) { + + server := &swapClientServer{ + config: &Config{}, + } + + _, err := server.SetLiquidityParams( + t.Context(), &looprpc.SetLiquidityParamsRequest{ + Parameters: &looprpc.LiquidityParameters{ + LoopInSource: looprpc. + LoopInSource_LOOP_IN_SOURCE_STATIC_ADDRESS, + }, + }, + ) + require.Error(t, err) + require.Equal(t, codes.FailedPrecondition, status.Code(err)) + require.ErrorContains(t, err, "--experimental") +} + // TestRPCAutoloopReasonStaticLoopInNoCandidate verifies that the new planner // reason is exposed over rpc. func TestRPCAutoloopReasonStaticLoopInNoCandidate(t *testing.T) { diff --git a/loopd/utils.go b/loopd/utils.go index a73434ca..f92eb1bf 100644 --- a/loopd/utils.go +++ b/loopd/utils.go @@ -120,7 +120,8 @@ func openDatabase(cfg *Config, chainParams *chaincfg.Params) (loopdb.SwapStore, } func getLiquidityManager(client *loop.Client, - staticLoopInManager *loopin.Manager) *liquidity.Manager { + staticLoopInManager *loopin.Manager, + enableExperimental bool) *liquidity.Manager { listStaticLoopIn := func( ctx context.Context) ([]*liquidity.StaticLoopInInfo, error) { @@ -259,6 +260,7 @@ func getLiquidityManager(client *loop.Client, PutLiquidityParams: client.Store.PutLiquidityParams, FetchLiquidityParams: client.Store.FetchLiquidityParams, } + mngrCfg.EnableStaticAddressAutoloop = enableExperimental return liquidity.NewManager(mngrCfg) }