diff --git a/routing_plugin.go b/routing_plugin.go new file mode 100644 index 0000000..dbcee8a --- /dev/null +++ b/routing_plugin.go @@ -0,0 +1,116 @@ +package loop + +import ( + "context" + "fmt" + "sync" + + "github.com/btcsuite/btcutil" + "github.com/lightninglabs/lndclient" + "github.com/lightningnetwork/lnd/routing/route" + "github.com/lightningnetwork/lnd/zpay32" +) + +var ( + // ErrRoutingPluginNotApplicable means that the selected routing plugin + // is not able to enhance routing given the current conditions and + // therefore shouldn't be used. + ErrRoutingPluginNotApplicable = fmt.Errorf("routing plugin not " + + "applicable") + + // ErrRoutingPluginNoMoreRetries means that the routing plugin can't + // effectively help the payment with more retries. + ErrRoutingPluginNoMoreRetries = fmt.Errorf("routing plugin can't " + + "retry more") +) + +var ( + routingPluginMx sync.Mutex + routingPluginInstance RoutingPlugin +) + +// RoutingPlugin is a generic interface for off-chain payment helpers. +type RoutingPlugin interface { + // Init initializes the routing plugin. + Init(ctx context.Context, target route.Vertex, + routeHints [][]zpay32.HopHint, amt btcutil.Amount) error + + // Done deinitializes the routing plugin (restoring any state the + // plugin might have changed). + Done(ctx context.Context) error + + // BeforePayment is called before each payment. Attempt counter is + // passed, counting attempts from 1. + BeforePayment(ctx context.Context, attempt int, maxAttempts int) error +} + +// makeRoutingPlugin is a helper to instantiate routing plugins. +func makeRoutingPlugin(plugin RoutingPluginType, + _ lndclient.LndServices) RoutingPlugin { + + return nil +} + +// AcquireRoutingPlugin will return a RoutingPlugin instance (or nil). As the +// LND instance used is a shared resource, currently only one requestor will be +// able to acquire a RoutingPlugin instance. If someone is already holding the +// instance a nil is returned. +func AcquireRoutingPlugin(ctx context.Context, pluginType RoutingPluginType, + lnd lndclient.LndServices, target route.Vertex, + routeHints [][]zpay32.HopHint, amt btcutil.Amount) ( + RoutingPlugin, error) { + + routingPluginMx.Lock() + defer routingPluginMx.Unlock() + + // Another swap is already using the routing plugin. + if routingPluginInstance != nil { + return nil, nil + } + + routingPluginInstance = makeRoutingPlugin(pluginType, lnd) + if routingPluginInstance == nil { + return nil, nil + } + + // Initialize the plugin with the passed parameters. + err := routingPluginInstance.Init(ctx, target, routeHints, amt) + if err != nil { + if err == ErrRoutingPluginNotApplicable { + // Since the routing plugin is not applicable for this + // payment, we can immediately destruct it. + if err := routingPluginInstance.Done(ctx); err != nil { + log.Errorf("Error while releasing routing "+ + "plugin: %v", err) + } + + // ErrRoutingPluginNotApplicable is non critical, so + // we're masking this error as we can continue the swap + // flow without the routing plugin. + err = nil + } + + routingPluginInstance = nil + return nil, err + } + + return routingPluginInstance, nil +} + +// ReleaseRoutingPlugin will release the RoutingPlugin, allowing other +// requestors to acquire the instance. +func ReleaseRoutingPlugin(ctx context.Context) { + routingPluginMx.Lock() + defer routingPluginMx.Unlock() + + if routingPluginInstance == nil { + return + } + + if err := routingPluginInstance.Done(ctx); err != nil { + log.Errorf("Error while releasing routing plugin: %v", + err) + } + + routingPluginInstance = nil +} diff --git a/routing_plugin_test.go b/routing_plugin_test.go new file mode 100644 index 0000000..31f9010 --- /dev/null +++ b/routing_plugin_test.go @@ -0,0 +1,119 @@ +package loop + +import ( + "context" + "testing" + + "github.com/btcsuite/btcd/btcec" + "github.com/btcsuite/btcutil" + "github.com/lightninglabs/lndclient" + "github.com/lightninglabs/loop/test" + "github.com/lightningnetwork/lnd/routing/route" + "github.com/stretchr/testify/require" +) + +var ( + alice = route.Vertex{1} + bob = route.Vertex{2} + charlie = route.Vertex{3} + dave = route.Vertex{4} + eugene = route.Vertex{5} + loopNode = route.Vertex{99} + + privFrank, _ = btcec.NewPrivateKey(btcec.S256()) + frankPubKey = privFrank.PubKey() + frank = route.NewVertex(frankPubKey) + + privGeorge, _ = btcec.NewPrivateKey(btcec.S256()) + georgePubKey = privGeorge.PubKey() + george = route.NewVertex(georgePubKey) +) + +// testChan holds simplified test data for channels. +type testChan struct { + nodeID1 route.Vertex + nodeID2 route.Vertex + chanID uint64 + capacity int64 + feeBase1 int64 + feeRate1 int64 + feeBase2 int64 + feeRate2 int64 +} + +// makeTestNetwork is a helper creating mocked network data from test inputs. +func makeTestNetwork(channels []testChan) ([]lndclient.ChannelInfo, + map[uint64]*lndclient.ChannelEdge) { + + chanInfos := make([]lndclient.ChannelInfo, len(channels)) + edges := make(map[uint64]*lndclient.ChannelEdge, len(channels)) + for i, ch := range channels { + chanInfos[i] = lndclient.ChannelInfo{ + ChannelID: ch.chanID, + } + + edges[ch.chanID] = &lndclient.ChannelEdge{ + ChannelID: ch.chanID, + Capacity: btcutil.Amount(ch.capacity), + Node1: ch.nodeID1, + Node2: ch.nodeID2, + Node1Policy: &lndclient.RoutingPolicy{ + FeeBaseMsat: ch.feeBase1, + FeeRateMilliMsat: ch.feeRate1, + }, + Node2Policy: &lndclient.RoutingPolicy{ + FeeBaseMsat: ch.feeBase2, + FeeRateMilliMsat: ch.feeRate2, + }, + } + } + + return chanInfos, edges +} + +func TestRoutingPluginAcquireRelease(t *testing.T) { + mockLnd := test.NewMockLnd() + + // _____Bob_____ + // / \ + // Alice Dave---Loop + // \___ ___/ + // Charlie + // + channels := []testChan{ + {alice, bob, 1, 1000, 1000, 1, 1000, 1}, + {alice, charlie, 2, 1000, 1000, 1, 1000, 1}, + {bob, dave, 3, 1000, 1000, 1, 1000, 1}, + {charlie, dave, 4, 1000, 1000, 100, 1000, 1}, + {dave, loopNode, 5, 1000, 1000, 1, 1000, 1}, + } + + mockLnd.Channels, mockLnd.ChannelEdges = makeTestNetwork(channels) + lnd := lndclient.LndServices{ + Client: mockLnd.Client, + Router: mockLnd.Router, + } + + target := loopNode + amt := btcutil.Amount(50) + ctx := context.TODO() + + // RoutingPluginNone returns nil. + plugin, err := AcquireRoutingPlugin( + ctx, RoutingPluginNone, lnd, target, nil, amt, + ) + require.Nil(t, plugin) + require.NoError(t, err) + + // Attempting to acquire RoutingPluginNone again still returns nil. + plugin, err = AcquireRoutingPlugin( + ctx, RoutingPluginNone, lnd, target, nil, amt, + ) + require.Nil(t, plugin) + require.NoError(t, err) + + // Call ReleaseRoutingPlugin twice to ensure we can call it even when no + // plugin is acquired. + ReleaseRoutingPlugin(ctx) + ReleaseRoutingPlugin(ctx) +} diff --git a/server_mock_test.go b/server_mock_test.go index 6350833..4c1356d 100644 --- a/server_mock_test.go +++ b/server_mock_test.go @@ -243,14 +243,14 @@ func (s *serverMock) Probe(ctx context.Context, amt btcutil.Amount, return nil } -func (s *serverMock) RecommendRoutingPlugin(_ context.Context, _ lntypes.Hash) ( - RoutingPluginType, error) { +func (s *serverMock) RecommendRoutingPlugin(_ context.Context, _ lntypes.Hash, + _ [32]byte) (RoutingPluginType, error) { return RoutingPluginNone, nil } func (s *serverMock) ReportRoutingResult(_ context.Context, _ lntypes.Hash, - _ RoutingPluginType, _ bool, _ int32, _ int64) error { + _ [32]byte, _ RoutingPluginType, _ bool, _ int32, _ int64) error { return nil }