Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add plugin for Peerswap atomic swap based local liquidity management protocol #2496

Closed
wants to merge 32 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
0a894a0
Add ability to send a custom lightning messages from a loaded plugin
remyers Nov 18, 2022
c84a15f
Fix UnknownMessage to interop with CLN PeerSwap; breaks other Eclair …
remyers Nov 8, 2022
5565bad
Fix ChannelUpdate message without type to not parse as an UnknownMessage
remyers Nov 18, 2022
364f81c
Update to use metered solution for channelUpdateCodecWithType
remyers Nov 18, 2022
8606def
Address comments from t-bast
remyers Nov 24, 2022
a6e6527
Change to send unknown messages via the switchboard instead of the re…
remyers Nov 29, 2022
ec4886e
Add serialization for peerswap messages
remyers Oct 27, 2022
e153d23
Add peerswap on-chain transactions
remyers Oct 27, 2022
84f5b2e
Add peerswap shared helper functions and data structures
remyers Oct 27, 2022
72f8e3a
Add peerswap key manager
remyers Oct 27, 2022
f2e7349
Add peerswap db support
remyers Oct 27, 2022
c0ae605
Add peerswap maker and tests
remyers Oct 27, 2022
283bf7b
Add peerswap taker and tests
remyers Oct 27, 2022
21bd548
Add peerswap swap register and tests
remyers Oct 27, 2022
c25a2e6
Add peerswap integration tests
remyers Oct 27, 2022
92de629
Create peerswap plugin
remyers Oct 27, 2022
7f58a75
Disable premium check and allow message resends for CLN integration t…
remyers Nov 8, 2022
b527e8e
Add test for maker with insufficient on-chain balance
remyers Nov 22, 2022
c3fff15
Remove unused/unsafe AbortSwap command
remyers Nov 23, 2022
036e7f6
Spawn swap actors with `stop` supervisor strategy
remyers Nov 24, 2022
42b29ec
Remove dependency on unsealing TransactionWithInputInfo trait
remyers Nov 25, 2022
9d3a74e
Refactor to send unknown messages via the switchboard instead of the …
remyers Nov 29, 2022
f9b7463
Add find function to swap db
remyers Dec 5, 2022
cc1e298
Query payment db for payment before waiting for event
remyers Dec 5, 2022
d272226
Change to safe resume supervision strategy
remyers Dec 5, 2022
c2c8460
Clean up responses and commands
remyers Dec 9, 2022
ad33a79
Fixed to subscribe to specific payments events
remyers Dec 12, 2022
e335323
cleaned up code
remyers Dec 13, 2022
ad28e4e
Added explanation comments for fee and premium values
remyers Dec 13, 2022
3403272
Replace verbose status messages with fewer user-friendly state messages
remyers Dec 14, 2022
59933dd
Remove route blinding feature when creating invoices
remyers Dec 30, 2022
32e7e2d
Update to add basic database and seed files information
remyers Dec 30, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,11 @@ class Peer(val nodeParams: NodeParams, remoteNodeId: PublicKey, wallet: OnChainA
context.system.eventStream.publish(UnknownMessageReceived(self, remoteNodeId, unknownMsg, d.connectionInfo))
stay()

case Event(RelayUnknownMessage(unknownMsg: UnknownMessage), d: ConnectedData) if nodeParams.pluginMessageTags.contains(unknownMsg.tag) =>
logMessage(unknownMsg, "OUT")
d.peerConnection forward unknownMsg
stay()

case Event(unhandledMsg: LightningMessage, _) =>
log.warning("ignoring message {}", unhandledMsg)
stay()
Expand Down Expand Up @@ -571,6 +576,8 @@ object Peer {
case class ConnectionDown(peerConnection: ActorRef) extends RemoteTypes

case class RelayOnionMessage(messageId: ByteVector32, msg: OnionMessage, replyTo_opt: Option[typed.ActorRef[Status]])

case class RelayUnknownMessage(unknownMessage: UnknownMessage)
// @formatter:on

def makeChannelParams(nodeParams: NodeParams, initFeatures: Features[InitFeature], defaultFinalScriptPubkey: ByteVector, walletStaticPaymentBasepoint: Option[PublicKey], isInitiator: Boolean, dualFunded: Boolean, fundingAmount: Satoshi): LocalParams = {
Expand Down
10 changes: 9 additions & 1 deletion eclair-core/src/main/scala/fr/acinq/eclair/io/Switchboard.scala
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import fr.acinq.eclair.io.MessageRelay.RelayPolicy
import fr.acinq.eclair.io.Peer.PeerInfoResponse
import fr.acinq.eclair.remote.EclairInternalsSerializer.RemoteTypes
import fr.acinq.eclair.router.Router.RouterConf
import fr.acinq.eclair.wire.protocol.OnionMessage
import fr.acinq.eclair.wire.protocol.{OnionMessage, UnknownMessage}
import fr.acinq.eclair.{SubscriptionsComplete, NodeParams}

/**
Expand Down Expand Up @@ -115,6 +115,12 @@ class Switchboard(nodeParams: NodeParams, peerFactory: Switchboard.PeerFactory)
case RelayMessage(messageId, prevNodeId, nextNodeId, dataToRelay, relayPolicy, replyTo) =>
val relay = context.spawn(Behaviors.supervise(MessageRelay()).onFailure(typed.SupervisorStrategy.stop), s"relay-message-$messageId")
relay ! MessageRelay.RelayMessage(messageId, self, prevNodeId.getOrElse(nodeParams.nodeId), nextNodeId, dataToRelay, relayPolicy, replyTo)

case ForwardUnknownMessage(remoteNodeId, msg) =>
getPeer(remoteNodeId) match {
case Some(peer) => peer ! Peer.RelayUnknownMessage(msg)
case None => log.error(s"Peer $remoteNodeId not found, could not forward unknown message: $msg")
}
}

/**
Expand Down Expand Up @@ -169,6 +175,8 @@ object Switchboard {
case class RouterPeerConf(routerConf: RouterConf, peerConf: PeerConnection.Conf) extends RemoteTypes

case class RelayMessage(messageId: ByteVector32, prevNodeId: Option[PublicKey], nextNodeId: PublicKey, message: OnionMessage, relayPolicy: RelayPolicy, replyTo_opt: Option[typed.ActorRef[MessageRelay.Status]])

case class ForwardUnknownMessage(nodeId: PublicKey, msg: UnknownMessage)
// @formatter:on

}
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import fr.acinq.eclair.wire.protocol.LightningMessageCodecs.{channelFlagsCodec,
import fr.acinq.eclair.{BlockHeight, CltvExpiry, MilliSatoshi, MilliSatoshiLong, UInt64}
import scodec.bits.ByteVector
import scodec.codecs._
import scodec.{Attempt, Codec}
import scodec.{Attempt, Codec, Err}

/**
* see https://github.com/lightningnetwork/lightning-rfc/blob/master/04-onion-routing.md
Expand Down Expand Up @@ -90,7 +90,10 @@ object FailureMessageCodecs {
val NODE = 0x2000
val UPDATE = 0x1000

val channelUpdateCodecWithType = meteredLightningMessageCodec.narrow[ChannelUpdate](f => Attempt.successful(f.asInstanceOf[ChannelUpdate]), g => g)
val channelUpdateCodecWithType = meteredLightningMessageCodec.narrow[ChannelUpdate]({
case f: ChannelUpdate => Attempt.successful(f)
case _ => Attempt.failure(Err("not a ChanelUpdate message"))
}, g => g)

// NB: for historical reasons some implementations were including/omitting the message type (258 for ChannelUpdate)
// this codec supports both versions for decoding, and will encode with the message type
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -417,7 +417,7 @@ object LightningMessageCodecs {

val unknownMessageCodec: Codec[UnknownMessage] = (
("tag" | uint16) ::
("message" | varsizebinarydata)
("message" | bytes)
).as[UnknownMessage]

val lightningMessageCodec = discriminated[LightningMessage].by(uint16)
Expand Down
11 changes: 10 additions & 1 deletion eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import akka.actor.Status.Failure
import akka.actor.{ActorContext, ActorRef, ActorSystem, FSM, PoisonPill, Status}
import akka.testkit.{TestFSMRef, TestKit, TestProbe}
import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey
import fr.acinq.bitcoin.scalacompat.{Block, Btc, SatoshiLong, Script}
import fr.acinq.bitcoin.scalacompat.{Block, Btc, ByteVector32, SatoshiLong, Script}
import fr.acinq.eclair.FeatureSupport.{Mandatory, Optional}
import fr.acinq.eclair.Features._
import fr.acinq.eclair.TestConstants._
Expand Down Expand Up @@ -616,6 +616,15 @@ class PeerSpec extends FixtureSpec {
peer ! RelayOnionMessage(messageId, msg, Some(probe.ref.toTyped))
probe.expectMsg(MessageRelay.Disconnected(messageId))
}

test("send UnknownMessage to peer if tag registered by a plugin") { f =>
import f._
val probe = TestProbe()
val unknownMessage = UnknownMessage(60003, ByteVector32.One)
connect(remoteNodeId, peer, peerConnection, switchboard, channels = Set(ChannelCodecsSpec.normal))
probe.send(peer, Peer.RelayUnknownMessage(unknownMessage))
peerConnection.expectMsgType[UnknownMessage]
}
}

object PeerSpec {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package fr.acinq.eclair.io
import akka.actor.typed.scaladsl.adapter.ClassicActorRefOps
import akka.actor.{Actor, ActorContext, ActorRef, Props, Status}
import akka.testkit.{TestActorRef, TestProbe}
import fr.acinq.bitcoin.scalacompat.ByteVector64
import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64}
import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey
import fr.acinq.eclair.TestConstants._
import fr.acinq.eclair.channel.ChannelIdAssigned
Expand Down Expand Up @@ -138,6 +138,18 @@ class SwitchboardSpec extends TestKitBaseClass with AnyFunSuiteLike {
peer.expectMsg(Peer.GetPeerInfo(Some(probe.ref.toTyped)))
}

test("forward UnknownMessage to peer") {
val unknownMessage = UnknownMessage(60003, ByteVector32.One)
val (probe, peer) = (TestProbe(), TestProbe())
val switchboard = TestActorRef(new Switchboard(Alice.nodeParams, FakePeerFactory(probe, peer)))
val knownPeerNodeId = randomKey().publicKey
probe.send(switchboard, Peer.Connect(knownPeerNodeId, None, probe.ref, isPersistent = true))
peer.expectMsgType[Peer.Init]
peer.expectMsgType[Peer.Connect]
probe.send(switchboard, ForwardUnknownMessage(knownPeerNodeId, unknownMessage))
peer.expectMsg(Peer.RelayUnknownMessage(unknownMessage))
}

}

object SwitchboardSpec {
Expand Down
45 changes: 45 additions & 0 deletions plugins/peerswap/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Peerswap plugin

This plugin allows implements the PeerSwap protocol: https://github.com/ElementsProject/peerswap-spec/blob/main/peer-protocol.md

Disclaimer: PeerSwap is beta-grade software.

We currently only recommend using PeerSwap with small balances or on signet/testnet

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

## Build

To build this plugin, run the following command in this directory:

```sh
mvn package
```

## Run

To run eclair with this plugin, start eclair with the following command:

```sh
eclair-node-<version>/bin/eclair-node.sh <path-to-plugin-jar>/peerswap-plugin-<version>.jar
```

## Commands

```sh
eclair-cli swapin --shortChannelId=<short-channel-id>> --amountSat=<amount>
eclair-cli swapout --shortChannelId=<short-channel-id>> --amountSat=<amount>
eclair-cli listswaps
eclair-cli swaphistory
eclair-cli cancelswap --swapId=<swap-id>
```

## Persistence

This plugin stores its data into a sqlite database named `peer-swap.sqlite`.
It uses that database to ensure swaps are correctly executed even after a restart of the node.
You can check the status of pending swap by reading directly from that database or using the command `listwaps`.

## Seed

The seed used to generate keys for swaps is stored in the `swap_seed.dat` file. This seed should be backed up and always kept secret.
164 changes: 164 additions & 0 deletions plugins/peerswap/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Copyright 2022 ACINQ SAS
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>fr.acinq.eclair</groupId>
<artifactId>eclair_2.13</artifactId>
<version>0.7.1-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>

<artifactId>peerswap-plugin_2.13</artifactId>
<packaging>jar</packaging>
<name>peerswap-plugin</name>

<build>
<plugins>
<plugin>
<groupId>com.googlecode.maven-download-plugin</groupId>
<artifactId>download-maven-plugin</artifactId>
<version>1.3.0</version>
<executions>
<execution>
<id>download-bitcoind</id>
<phase>generate-test-resources</phase>
<goals>
<goal>wget</goal>
</goals>
<configuration>
<skip>${maven.test.skip}</skip>
<url>${bitcoind.url}</url>
<unpack>true</unpack>
<outputDirectory>${project.build.directory}</outputDirectory>
<md5>${bitcoind.md5}</md5>
<sha1>${bitcoind.sha1}</sha1>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.2.1</version>
<configuration>
<transformers>
<!-- Add a manifest entry for Main-Class with the FQDN of the implementation. -->
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<manifestEntries>
<Main-Class>fr.acinq.eclair.plugins.peerswap.PeerSwapPlugin</Main-Class>
</manifestEntries>
</transformer>
</transformers>
</configuration>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>

<profiles>
<profile>
<id>default</id>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
<properties>
<bitcoind.url>https://bitcoincore.org/bin/bitcoin-core-0.21.1/bitcoin-0.21.1-x86_64-linux-gnu.tar.gz</bitcoind.url>
<bitcoind.md5>e283a98b5e9f0b58e625e1dde661201d</bitcoind.md5>
<bitcoind.sha1>5101e29b39c33cc8e40d5f3b46dda37991b037a0</bitcoind.sha1>
</properties>
</profile>
<profile>
<id>Mac</id>
<activation>
<os>
<family>mac</family>
</os>
</activation>
<properties>
<bitcoind.url>https://bitcoincore.org/bin/bitcoin-core-0.21.1/bitcoin-0.21.1-osx64.tar.gz</bitcoind.url>
<bitcoind.md5>dfd1f323678eede14ae2cf6afb26ff6a</bitcoind.md5>
<bitcoind.sha1>4273696f90a2648f90142438221f5d1ade16afa2</bitcoind.sha1>
</properties>
</profile>
<profile>
<id>Windows</id>
<activation>
<os>
<family>Windows</family>
</os>
</activation>
<properties>
<bitcoind.url>https://bitcoincore.org/bin/bitcoin-core-0.21.1/bitcoin-0.21.1-win64.zip</bitcoind.url>
<bitcoind.md5>1c6f5081ea68dcec7eddb9e6cdfc508d</bitcoind.md5>
<bitcoind.sha1>a782cd413fc736f05fad3831d6a9f59dde779520</bitcoind.sha1>
</properties>
</profile>
</profiles>

<dependencies>
<dependency>
<groupId>org.scala-lang</groupId>
<artifactId>scala-library</artifactId>
<version>${scala.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>fr.acinq.eclair</groupId>
<artifactId>eclair-core_${scala.version.short}</artifactId>
<version>${project.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>fr.acinq.eclair</groupId>
<artifactId>eclair-node_${scala.version.short}</artifactId>
<version>${project.version}</version>
<scope>provided</scope>
</dependency>
<!-- TESTS -->
<dependency>
<groupId>com.typesafe.akka</groupId>
<artifactId>akka-testkit_${scala.version.short}</artifactId>
<version>${akka.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.typesafe.akka</groupId>
<artifactId>akka-actor-testkit-typed_${scala.version.short}</artifactId>
<version>${akka.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>fr.acinq.eclair</groupId>
<artifactId>eclair-core_${scala.version.short}</artifactId>
<version>${project.version}</version>
<classifier>tests</classifier>
<type>test-jar</type>
<scope>test</scope>
</dependency>
</dependencies>

</project>
Loading