Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion channel-funding/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<parent>
<groupId>fr.acinq.eclair</groupId>
<artifactId>eclair-plugins_2.13</artifactId>
<version>0.13.0-SNAPSHOT</version>
<version>0.14.0-SNAPSHOT</version>
</parent>

<artifactId>channel-funding-plugin</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ class OpenChannelInterceptor(config: ChannelFundingPluginConfig, router: ActorRe
}

private def acceptOpenChannel(o: InterceptOpenChannelReceived): Unit = {
o.replyTo ! AcceptOpenChannel(o.temporaryChannelId, o.defaultParams, config.fundingPolicy.fundingAmountFor(o.openChannelNonInitiator.remoteNodeId).map(amount => LiquidityAds.AddFunding(amount, rates_opt = None)))
o.replyTo ! AcceptOpenChannel(o.temporaryChannelId, config.fundingPolicy.fundingAmountFor(o.openChannelNonInitiator.remoteNodeId).map(amount => LiquidityAds.AddFunding(amount, rates_opt = None)))
}

private def rejectOpenChannel(o: InterceptOpenChannelReceived, error: String): Unit = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,23 +19,23 @@ package fr.acinq.eclair.plugins.channelfunding
import akka.actor.testkit.typed.scaladsl.{ScalaTestWithActorTestKit, TestProbe}
import akka.actor.typed.ActorRef
import com.typesafe.config.ConfigFactory
import fr.acinq.bitcoin.scalacompat.SatoshiLong
import fr.acinq.eclair.io.OpenChannelInterceptor.{DefaultParams, OpenChannelNonInitiator}
import fr.acinq.bitcoin.scalacompat.{Crypto, SatoshiLong}
import fr.acinq.eclair.channel.ChannelTypes
import fr.acinq.eclair.io.OpenChannelInterceptor.OpenChannelNonInitiator
import fr.acinq.eclair.io.PeerSpec.{createOpenChannelMessage, createOpenDualFundedChannelMessage}
import fr.acinq.eclair.router.Router.{GetNode, PublicNode, UnknownNode}
import fr.acinq.eclair.wire.protocol._
import fr.acinq.eclair.{AcceptOpenChannel, CltvExpiryDelta, Features, InterceptOpenChannelCommand, InterceptOpenChannelReceived, InterceptOpenChannelResponse, MilliSatoshiLong, RejectOpenChannel, TimestampSecondLong, randomBytes64, randomKey}
import fr.acinq.eclair.{AcceptOpenChannel, Features, InterceptOpenChannelCommand, InterceptOpenChannelReceived, InterceptOpenChannelResponse, RejectOpenChannel, TimestampSecondLong, randomBytes64, randomKey}
import org.scalatest.funsuite.FixtureAnyFunSuiteLike
import org.scalatest.{Outcome, Tag}

import scala.concurrent.duration.DurationInt

class OpenChannelInterceptorSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("application")) with FixtureAnyFunSuiteLike {
val remoteNodeId = randomKey().publicKey
val whitelistedRemoteNodeId = randomKey().publicKey
val peerAddress = NodeAddress.fromParts("127.0.0.1", 9735).get
val defaultParams = DefaultParams(100 sat, 100000 msat, 100 msat, CltvExpiryDelta(288), 10)
val openChannel = OpenChannelNonInitiator(remoteNodeId, Left(createOpenChannelMessage()), Features.empty, Features.empty, TestProbe[Any]().ref, peerAddress)
val remoteNodeId: Crypto.PublicKey = randomKey().publicKey
val whitelistedRemoteNodeId: Crypto.PublicKey = randomKey().publicKey
val peerAddress: NodeAddress = NodeAddress.fromParts("127.0.0.1", 9735).get
val openChannel = OpenChannelNonInitiator(remoteNodeId, Left(createOpenChannelMessage(ChannelTypes.AnchorOutputsZeroFeeHtlcTx())), Features.empty, Features.empty, TestProbe[Any]().ref, peerAddress)
val announcement = NodeAnnouncement(randomBytes64(), Features.empty, 1 unixsec, remoteNodeId, Color(100.toByte, 200.toByte, 300.toByte), "node-alias", NodeAddress.fromParts("1.2.3.4", 42000).get :: Nil)

case class FixtureParam(router: TestProbe[Any], peer: TestProbe[InterceptOpenChannelResponse], openChannelInterceptor: ActorRef[InterceptOpenChannelCommand])
Expand Down Expand Up @@ -63,21 +63,21 @@ class OpenChannelInterceptorSpec extends ScalaTestWithActorTestKit(ConfigFactory
import f._

// request from public peer
openChannelInterceptor ! InterceptOpenChannelReceived(peer.ref, openChannel, defaultParams)
openChannelInterceptor ! InterceptOpenChannelReceived(peer.ref, openChannel)
router.expectMessageType[GetNode].replyTo ! PublicNode(announcement, 2, 10000 sat)
assert(peer.expectMessageType[AcceptOpenChannel] == AcceptOpenChannel(openChannel.temporaryChannelId, defaultParams, None))
assert(peer.expectMessageType[AcceptOpenChannel] == AcceptOpenChannel(openChannel.temporaryChannelId, None))

// request from private peer
openChannelInterceptor ! InterceptOpenChannelReceived(peer.ref, openChannel, defaultParams)
openChannelInterceptor ! InterceptOpenChannelReceived(peer.ref, openChannel)
router.expectMessageType[GetNode].replyTo ! UnknownNode(remoteNodeId)
assert(peer.expectMessageType[AcceptOpenChannel] == AcceptOpenChannel(openChannel.temporaryChannelId, defaultParams, None))
assert(peer.expectMessageType[AcceptOpenChannel] == AcceptOpenChannel(openChannel.temporaryChannelId, None))
}

test("approve and contribute to dual-funded channel request") { f =>
import f._

val openChannelDualFunded = OpenChannelNonInitiator(whitelistedRemoteNodeId, Right(createOpenDualFundedChannelMessage()), Features.empty, Features.empty, TestProbe[Any]().ref, peerAddress)
openChannelInterceptor ! InterceptOpenChannelReceived(peer.ref, openChannelDualFunded, defaultParams)
val openChannelDualFunded = OpenChannelNonInitiator(whitelistedRemoteNodeId, Right(createOpenDualFundedChannelMessage(ChannelTypes.AnchorOutputsZeroFeeHtlcTx())), Features.empty, Features.empty, TestProbe[Any]().ref, peerAddress)
openChannelInterceptor ! InterceptOpenChannelReceived(peer.ref, openChannelDualFunded)
router.expectNoMessage(100 millis) // we don't check requirements for whitelisted nodes
val accept = peer.expectMessageType[AcceptOpenChannel]
assert(accept.temporaryChannelId == openChannelDualFunded.temporaryChannelId)
Expand All @@ -88,17 +88,17 @@ class OpenChannelInterceptorSpec extends ScalaTestWithActorTestKit(ConfigFactory
import f._

// from public peer with too low capacity
openChannelInterceptor ! InterceptOpenChannelReceived(peer.ref, openChannel, defaultParams)
openChannelInterceptor ! InterceptOpenChannelReceived(peer.ref, openChannel)
router.expectMessageType[GetNode].replyTo ! PublicNode(announcement, 2, 9999 sat)
assert(peer.expectMessageType[RejectOpenChannel].error.toAscii.contains("total capacity"))

// from public peer with too few channels
openChannelInterceptor ! InterceptOpenChannelReceived(peer.ref, openChannel, defaultParams)
openChannelInterceptor ! InterceptOpenChannelReceived(peer.ref, openChannel)
router.expectMessageType[GetNode].replyTo ! PublicNode(announcement, 1, 10000 sat)
assert(peer.expectMessageType[RejectOpenChannel].error.toAscii.contains("active channels"))

// from private peer
openChannelInterceptor ! InterceptOpenChannelReceived(peer.ref, openChannel, defaultParams)
openChannelInterceptor ! InterceptOpenChannelReceived(peer.ref, openChannel)
router.expectMessageType[GetNode].replyTo ! UnknownNode(remoteNodeId)
assert(peer.expectMessageType[RejectOpenChannel].error.toAscii.contains("no public channels"))
}
Expand Down
2 changes: 1 addition & 1 deletion custom-offer/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<parent>
<groupId>fr.acinq.eclair</groupId>
<artifactId>eclair-plugins_2.13</artifactId>
<version>0.13.0-SNAPSHOT</version>
<version>0.14.0-SNAPSHOT</version>
</parent>

<artifactId>custom-offer</artifactId>
Expand Down
2 changes: 1 addition & 1 deletion historical-gossip/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<parent>
<groupId>fr.acinq.eclair</groupId>
<artifactId>eclair-plugins_2.13</artifactId>
<version>0.13.0-SNAPSHOT</version>
<version>0.14.0-SNAPSHOT</version>
</parent>

<artifactId>historical-gossip-plugin</artifactId>
Expand Down
2 changes: 1 addition & 1 deletion offline-commands/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<parent>
<groupId>fr.acinq.eclair</groupId>
<artifactId>eclair-plugins_2.13</artifactId>
<version>0.13.0-SNAPSHOT</version>
<version>0.14.0-SNAPSHOT</version>
</parent>

<artifactId>offline-commands-plugin</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import akka.util.Timeout
import fr.acinq.bitcoin.scalacompat.Script
import fr.acinq.eclair.api.directives.EclairDirectives
import fr.acinq.eclair.api.serde.FormParamExtractors._
import fr.acinq.eclair.blockchain.fee.{FeeratePerByte, FeeratePerKw}
import fr.acinq.eclair.blockchain.fee.FeeratePerByte
import fr.acinq.eclair.channel.ClosingFeerates
import fr.acinq.eclair.plugins.offlinecommands.OfflineChannelsCloser.CloseChannels
import scodec.bits.ByteVector
Expand All @@ -44,9 +44,9 @@ object ApiHandlers {
(channelIds, forceCloseAfterHours_opt, scriptPubKey_opt, preferredFeerate_opt, minFeerate_opt, maxFeerate_opt) =>
val forceCloseAfter_opt = forceCloseAfterHours_opt.map(FiniteDuration(_, TimeUnit.HOURS))
val closingFeerates_opt = preferredFeerate_opt.map(preferredPerByte => {
val preferredFeerate = FeeratePerKw(preferredPerByte)
val minFeerate = minFeerate_opt.map(feerate => FeeratePerKw(feerate)).getOrElse(preferredFeerate / 2)
val maxFeerate = maxFeerate_opt.map(feerate => FeeratePerKw(feerate)).getOrElse(preferredFeerate * 2)
val preferredFeerate = preferredPerByte.perKw
val minFeerate = minFeerate_opt.map(feerate => feerate.perKw).getOrElse(preferredFeerate / 2)
val maxFeerate = maxFeerate_opt.map(feerate => feerate.perKw).getOrElse(preferredFeerate * 2)
ClosingFeerates(preferredFeerate, minFeerate, maxFeerate)
})
if (scriptPubKey_opt.forall(Script.isNativeWitnessScript)) {
Expand Down
5 changes: 3 additions & 2 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@

<groupId>fr.acinq.eclair</groupId>
<artifactId>eclair-plugins_2.13</artifactId>
<version>0.13.0-SNAPSHOT</version>
<version>0.14.0-SNAPSHOT</version>
<packaging>pom</packaging>

<modules>
<module>historical-gossip</module>
<module>offline-commands</module>
<module>custom-offer</module>
<module>channel-funding</module>
<module>utxo-validator</module>
</modules>

<description>Official eclair plugins</description>
Expand Down Expand Up @@ -54,7 +55,7 @@
<scalatest.version>3.2.12</scalatest.version>
<akka.version>2.6.20</akka.version>
<akka.http.version>10.2.7</akka.http.version>
<eclair.version>0.13.0-SNAPSHOT</eclair.version>
<eclair.version>0.14.0-SNAPSHOT</eclair.version>
</properties>

<build>
Expand Down
23 changes: 23 additions & 0 deletions utxo-validator/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# UTXO validator plugin

This plugin provides an example of how to validate channels or splice transactions that contain UTXOs provided by an untrusted remote node.
This can only be done after the transaction has been created and published, because in some cases only the remote node has access to the fully signed transaction.
It is then possible to immediately close the channel, before its funding transaction has enough confirmations, to prevent it from being used.

Disclaimer: this plugin is for demonstration purposes only, node operators should fork this plugin and implement whatever policies make sense for their node.

## 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>/utxo-validator-<version>.jar
```
84 changes: 84 additions & 0 deletions utxo-validator/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
<?xml version="1.0" encoding="UTF-8"?>
<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-plugins_2.13</artifactId>
<version>0.14.0-SNAPSHOT</version>
</parent>

<artifactId>utxo-validator-plugin</artifactId>
<packaging>jar</packaging>
<name>utxo-validator-plugin</name>

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.2.1</version>
<configuration>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<manifestEntries>
<Main-Class>fr.acinq.eclair.plugins.utxovalidator.UtxoValidatorPlugin</Main-Class>
</manifestEntries>
</transformer>
</transformers>
</configuration>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>

<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>
8 changes: 8 additions & 0 deletions utxo-validator/src/main/resources/reference.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
utxo-validator {
// A list of blacklisted utxos: if peers use one of those to open a channel with us, we will immediately close it.
blacklisted-utxos = [
""
]
// When the funding transaction cannot be found in the mempool or the blockchain, we will try again after this delay.
fetch-tx-frequency = 10 minutes
}
Loading