diff --git a/Eclair-ChannelFundingPlugin/.classpath b/Eclair-ChannelFundingPlugin/.classpath new file mode 100644 index 0000000..a8067c5 --- /dev/null +++ b/Eclair-ChannelFundingPlugin/.classpath @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/Eclair-ChannelFundingPlugin/pom.xml b/Eclair-ChannelFundingPlugin/pom.xml new file mode 100644 index 0000000..de278d4 --- /dev/null +++ b/Eclair-ChannelFundingPlugin/pom.xml @@ -0,0 +1,179 @@ + + + 4.0.0 + + fr.acinq.eclair.plugin + ChannelFundingPlugin + 0.0.3 + jar + + + UTF-8 + 2.13.10 + 2.13 + 0.9.0 + 2.6.20 + 10.2.7 + 3.8.5 + 0.28 + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.5 + + 1.8 + 1.8 + + + + + org.apache.maven.plugins + maven-jar-plugin + 3.2.2 + + + + false + fr.acinq.ChannelInterceptor.ChannelFundingPlugin + + + + + + + + + maven-assembly-plugin + 2.2.1 + + + jar-with-dependencies + + + + fr.acinq.ChannelInterceptor.ChannelFundingPlugin + + + + + + make-assembly + package + + single + + + + + + + + + fr.acinq + bitcoin-lib_${scala.version.short} + ${bitcoinlib.version} + provided + + + org.scala-lang + scala-library + ${scala.version} + provided + + + + fr.acinq.eclair + eclair-core_2.13 + ${eclair.version} + provided + + + fr.acinq.eclair + eclair-node_2.13 + ${eclair.version} + provided + + + org.clapper + grizzled-slf4j_2.13 + 1.3.4 + provided + + + com.typesafe.akka + akka-http_${scala.version.short} + ${akka.http.version} + + + com.typesafe.akka + akka-stream_${scala.version.short} + ${akka.version} + provided + + + com.typesafe.akka + akka-actor-typed_${scala.version.short} + ${akka.version} + provided + + + org.json4s + json4s-native_${scala.version.short} + 3.6.10 + provided + + + com.softwaremill.sttp.client3 + json4s_${scala.version.short} + ${sttp.version} + provided + + + com.softwaremill.sttp + okhttp-backend_${scala.version.short} + 1.7.2 + provided + + + org.bouncycastle + bcprov-jdk18on + 1.71 + provided + + + org.slf4j + slf4j-api + 1.7.36 + provided + + + org.json + json + 20160810 + + + + org.xerial + sqlite-jdbc + 3.39.3.0 + + + org.postgresql + postgresql + 42.3.3 + + + com.google.code.gson + gson + 2.10.1 + + + + + \ No newline at end of file diff --git a/Eclair-ChannelFundingPlugin/readme.md b/Eclair-ChannelFundingPlugin/readme.md new file mode 100644 index 0000000..00c4814 --- /dev/null +++ b/Eclair-ChannelFundingPlugin/readme.md @@ -0,0 +1,7 @@ +#ChannelFundingPlugin + +This is a plugin for eclair lightning node. +It lets you create rules for new incoming channels. + +There is a sample config here: + diff --git a/Eclair-ChannelFundingPlugin/src/main/java/fr/acinq/ChannelInterceptor/ChannelFundingPlugin.java b/Eclair-ChannelFundingPlugin/src/main/java/fr/acinq/ChannelInterceptor/ChannelFundingPlugin.java new file mode 100644 index 0000000..7ca77b0 --- /dev/null +++ b/Eclair-ChannelFundingPlugin/src/main/java/fr/acinq/ChannelInterceptor/ChannelFundingPlugin.java @@ -0,0 +1,117 @@ +package fr.acinq.ChannelInterceptor; + + +import akka.actor.ActorSystem; +import akka.actor.typed.ActorRef; +import akka.actor.typed.Behavior; +import akka.actor.typed.javadsl.Adapter; +import akka.actor.typed.SupervisorStrategy; +import akka.actor.typed.javadsl.Behaviors; +import fr.acinq.eclair.InterceptOpenChannelCommand; +import fr.acinq.eclair.InterceptOpenChannelPlugin; +import fr.acinq.eclair.Kit; +import fr.acinq.eclair.NodeParams; +import fr.acinq.eclair.Plugin; +import fr.acinq.eclair.PluginParams; +import fr.acinq.eclair.Setup; +import java.io.File; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.typesafe.config.Config; +import com.typesafe.config.ConfigFactory; + +/** + * Intercept OpenChannel messages received by the node and respond by continuing the process + * of accepting the request, potentially with different local parameters, or failing the request. + */ +public class ChannelFundingPlugin implements Plugin { + final static Logger logger = LoggerFactory.getLogger(ChannelFundingPlugin.class); + + private OpenChannelInterceptorKit pluginKit; + private File configFile; + + @Override + public PluginParams params() { + return new InterceptOpenChannelPlugin() { + @Override + public String name() { + return "ChannelFundingPlugin"; + } + + @Override + public ActorRef openChannelInterceptor() { + return pluginKit.openChannelInterceptor(); + } + }; + } + + @Override + public void onSetup(Setup setup) { + File datadir = new File(setup.datadir().toPath().toAbsolutePath().toString()); + File resourcesDir = new File(datadir,"/plugin-resources/ChannelFunding/"); + configFile = new File(resourcesDir,"ChannelFunding.conf"); + logger.info("Using config file: "+configFile.getAbsolutePath()); + + //we load the config once at startup to check if at least hte default accept rule is set + try { + Config PluginConf = ConfigFactory.parseFile(configFile).resolve(); + + @SuppressWarnings("unused") + boolean accept = PluginConf.getBoolean("open-channel-interceptor.default.accept"); + @SuppressWarnings("unused") + long min_channel_size = PluginConf.getLong("open-channel-interceptor.default.min_channel_size"); + @SuppressWarnings("unused") + long max_channel_size = PluginConf.getLong("open-channel-interceptor.default.max_channel_size"); + @SuppressWarnings("unused") + int min_active_channels = PluginConf.getInt("open-channel-interceptor.default.min_active_channels"); + } catch (Throwable e) + { + logger.error("Can not read default accept rules from config file"); + logger.error(""+e); + } + + logger.info("ChannelFundingPlugin finished startup"); + } + + @Override + public void onKit(Kit kit) { + + ActorSystem actorSystem = kit.system(); + Behavior MyBehaviour = Behaviors.supervise(OpenChannelInterceptor.apply(configFile.getAbsolutePath(), Adapter.toTyped(kit.router()))) + .onFailure(SupervisorStrategy.restart()); + + akka.actor.typed.ActorRef openChannelInterceptor = Adapter.spawnAnonymous(actorSystem, MyBehaviour); + + + pluginKit = new OpenChannelInterceptorKit(kit.nodeParams(), kit.system(), openChannelInterceptor); + + logger.info("ChannelFundingPlugin subscribed to events"); + } + +} + +class OpenChannelInterceptorKit { + private final NodeParams nodeParams; + private final ActorSystem system; + private final ActorRef openChannelInterceptor; + + public OpenChannelInterceptorKit(NodeParams nodeParams, ActorSystem system, ActorRef openChannelInterceptor) { + this.nodeParams = nodeParams; + this.system = system; + this.openChannelInterceptor = openChannelInterceptor; + } + + public NodeParams nodeParams() { + return nodeParams; + } + + public ActorSystem system() { + return system; + } + + public ActorRef openChannelInterceptor() { + return openChannelInterceptor; + } +} diff --git a/Eclair-ChannelFundingPlugin/src/main/java/fr/acinq/ChannelInterceptor/OpenChannelInterceptor.java b/Eclair-ChannelFundingPlugin/src/main/java/fr/acinq/ChannelInterceptor/OpenChannelInterceptor.java new file mode 100644 index 0000000..313fa20 --- /dev/null +++ b/Eclair-ChannelFundingPlugin/src/main/java/fr/acinq/ChannelInterceptor/OpenChannelInterceptor.java @@ -0,0 +1,176 @@ +package fr.acinq.ChannelInterceptor; + +import akka.actor.typed.Behavior; + +import java.io.File; +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.typesafe.config.Config; +import com.typesafe.config.ConfigFactory; + +import akka.actor.typed.ActorRef; +import akka.actor.typed.javadsl.ActorContext; +import akka.actor.typed.javadsl.Behaviors; +import fr.acinq.eclair.router.Router; +import fr.acinq.eclair.router.Router.GetNode; +import fr.acinq.eclair.router.Router.PublicNode; +import fr.acinq.eclair.router.Router.UnknownNode; +import fr.acinq.eclair.wire.protocol.Error; +import fr.acinq.eclair.AcceptOpenChannel; +import fr.acinq.eclair.InterceptOpenChannelCommand; +import fr.acinq.eclair.InterceptOpenChannelReceived; +import fr.acinq.eclair.RejectOpenChannel; + +/** + * Intercept OpenChannel and OpenDualFundedChannel messages received by the node. Respond to the peer + * that received the request with AcceptOpenChannel to continue the open channel process, + * optionally with modified default parameters, or fail the request by responding to the initiator + * with RejectOpenChannel and an Error message. + * + * This example plugin decides how much funds (if any) the non-initiator should put into a dual-funded channel. It also + * demonstrates how to reject requests from nodes with less than a minimum amount of total capacity or too few public + * channels. + * + * Note: only one open channel request can be processed at a time. + */ +public class OpenChannelInterceptor { + final static Logger logger = LoggerFactory.getLogger(OpenChannelInterceptor.class); + + + + private static class WrappedGetNodeResponse implements InterceptOpenChannelCommand { + private final InterceptOpenChannelReceived interceptOpenChannelReceived; + private final Router.GetNodeResponse response; + + public WrappedGetNodeResponse(InterceptOpenChannelReceived interceptOpenChannelReceived, Router.GetNodeResponse response) { + this.interceptOpenChannelReceived = interceptOpenChannelReceived; + this.response = response; + } + + public InterceptOpenChannelReceived getInterceptOpenChannelReceived() { + return interceptOpenChannelReceived; + } + + public Router.GetNodeResponse getResponse() { + return response; + } + } + + public static Behavior apply(String configPath, ActorRef router) { + return Behaviors.setup(context -> new OpenChannelInterceptor(configPath, router, context).start()); + } + + + private final String ConfigPath; + private final ActorRef router; + private final ActorContext context; + + private OpenChannelInterceptor(String ConfigPath, ActorRef router, ActorContext context) { + logger.info("Interceptor created"); + this.ConfigPath = ConfigPath; + this.router = router; + this.context = context; + } + + private Behavior start() { + return Behaviors.receiveMessage(interceptOpenChannelCommand -> { + if (interceptOpenChannelCommand instanceof InterceptOpenChannelReceived) { + InterceptOpenChannelReceived o = (InterceptOpenChannelReceived) interceptOpenChannelCommand; + ActorRef adapter = context.messageAdapter(Router.GetNodeResponse.class, nodeResponse -> new WrappedGetNodeResponse(o, nodeResponse)); + router.tell(new GetNode(adapter, o.openChannelNonInitiator().remoteNodeId())); + return start(); + } else if (interceptOpenChannelCommand instanceof WrappedGetNodeResponse) { + WrappedGetNodeResponse wrappedGetNodeResponse = (WrappedGetNodeResponse) interceptOpenChannelCommand; + InterceptOpenChannelReceived o = wrappedGetNodeResponse.getInterceptOpenChannelReceived(); + Router.GetNodeResponse response = wrappedGetNodeResponse.getResponse(); + + logger.info("Node info: "+response); + logger.debug("Channel request: "+o); + + try { + Config PluginConf = ConfigFactory.parseFile(new File(ConfigPath)).resolve(); + boolean accept = PluginConf.getBoolean("open-channel-interceptor.default.accept"); + long min_channel_size = PluginConf.getLong("open-channel-interceptor.default.min_channel_size"); + long max_channel_size = PluginConf.getLong("open-channel-interceptor.default.max_channel_size"); + int min_active_channels = PluginConf.getInt("open-channel-interceptor.default.min_active_channels"); + + @SuppressWarnings("unchecked") + List Overrides = (List) PluginConf.getConfigList("open-channel-interceptor.override"); + + + + if (response instanceof PublicNode) { + PublicNode publicNode = (PublicNode) response; + + logger.info("Got Request from: "+publicNode.announcement().nodeId()); + + + for(Config config : Overrides) + { + //we check if the Nodeid is on a override list, if so we overwrite the defaults + List NodeIds = config.getStringList("NodeIds"); + if(NodeIds.contains(publicNode.announcement().nodeId().toString())) + { + accept = config.getBoolean("accept"); + min_channel_size = config.getLong("min_channel_size"); + max_channel_size = config.getLong("max_channel_size"); + min_active_channels = config.getInt("min_active_channels"); + } + } + + + String error = ""; + if(!accept) { + error = "rejected, not accepting new channels"; + } else + { + if (publicNode.activeChannels() < min_active_channels) { + error = "rejected, less than " + min_active_channels + " active channels"; + } + if (o.remoteFundingAmount().toLong() < min_channel_size) { + error = "rejected, min channel size " + min_channel_size; + } + if (o.remoteFundingAmount().toLong() > max_channel_size) { + error = "rejected, max channel size " + max_channel_size; + } + } + + if(error.equals("")) { + logger.info("channel accepted"); + acceptOpenChannel(o); + }else { + logger.info("channel rejected: "+error); + rejectOpenChannel(o, error); + } + + } else if (response instanceof UnknownNode) { + String error = "rejected, no public channels"; + logger.info("channel rejected: "+error); + rejectOpenChannel(o, error); + } + } catch (Throwable e) + { + //if anything goes wrong during checking of the request, we deny the request with a generic error and log the exception + logger.error("Exception: "+this.getClass()+" 1 ",e); + rejectOpenChannel(o, "rejected, internal error"); + } + + + return start(); + } else { + return Behaviors.same(); + } + }); + } + + private void acceptOpenChannel(InterceptOpenChannelReceived o) { + o.replyTo().tell(new AcceptOpenChannel(o.temporaryChannelId(), o.defaultParams())); + } + + private void rejectOpenChannel(InterceptOpenChannelReceived o, String error) { + o.replyTo().tell(new RejectOpenChannel(o.temporaryChannelId(), Error.apply(o.temporaryChannelId(), error))); + } +} diff --git a/Eclair-ChannelFundingPlugin/src/main/resources/ChannelFunding.conf b/Eclair-ChannelFundingPlugin/src/main/resources/ChannelFunding.conf new file mode 100644 index 0000000..4182c27 --- /dev/null +++ b/Eclair-ChannelFundingPlugin/src/main/resources/ChannelFunding.conf @@ -0,0 +1,24 @@ +open-channel-interceptor { + default { + accept=true + min_channel_size=1000000 + max_channel_size=2000000 + min_active_channels = 20 + } + override = [ + { + NodeIds = ["NoeID1","NoeID2"] + accept=true + min_channel_size=1000000 + max_channel_size=3000000 + min_active_channels = 20 + }, + { + NodeIds = ["NoeID3"] + accept=true + min_channel_size=1000000 + max_channel_size=5000000 + min_active_channels = 20 + } + ] +} \ No newline at end of file