diff --git a/plugins/peerswap/README.md b/plugins/peerswap/README.md new file mode 100644 index 0000000000..f6a39221c8 --- /dev/null +++ b/plugins/peerswap/README.md @@ -0,0 +1,19 @@ +# Peerswap plugin + +This plugin allows implements the PeerSwap protocol: https://github.com/ElementsProject/peerswap-spec/blob/main/peer-protocol.md + +## 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-/bin/eclair-node.sh /peerswap-plugin-.jar +``` diff --git a/plugins/peerswap/pom.xml b/plugins/peerswap/pom.xml new file mode 100644 index 0000000000..b76826a23f --- /dev/null +++ b/plugins/peerswap/pom.xml @@ -0,0 +1,163 @@ + + + + + 4.0.0 + + fr.acinq.eclair + eclair_2.13 + 0.7.1-SNAPSHOT + + + peerswap-plugin_2.13 + jar + peerswap-plugin + + + + + com.googlecode.maven-download-plugin + download-maven-plugin + 1.3.0 + + + download-bitcoind + generate-test-resources + + wget + + + ${maven.test.skip} + ${bitcoind.url} + true + ${project.build.directory} + ${bitcoind.md5} + ${bitcoind.sha1} + + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.2.1 + + + + + + fr.acinq.eclair.plugins.peerswap.PeerSwapPlugin + + + + + + + package + + shade + + + + + + + + + + default + + true + + + https://bitcoincore.org/bin/bitcoin-core-0.21.1/bitcoin-0.21.1-x86_64-linux-gnu.tar.gz + e283a98b5e9f0b58e625e1dde661201d + 5101e29b39c33cc8e40d5f3b46dda37991b037a0 + + + + Mac + + + mac + + + + https://bitcoincore.org/bin/bitcoin-core-0.21.1/bitcoin-0.21.1-osx64.tar.gz + dfd1f323678eede14ae2cf6afb26ff6a + 4273696f90a2648f90142438221f5d1ade16afa2 + + + + Windows + + + Windows + + + + https://bitcoincore.org/bin/bitcoin-core-0.21.1/bitcoin-0.21.1-win64.zip + 1c6f5081ea68dcec7eddb9e6cdfc508d + a782cd413fc736f05fad3831d6a9f59dde779520 + + + + + + + org.scala-lang + scala-library + ${scala.version} + provided + + + fr.acinq.eclair + eclair-core_${scala.version.short} + ${project.version} + provided + + + fr.acinq.eclair + eclair-node_${scala.version.short} + ${project.version} + provided + + + + com.typesafe.akka + akka-testkit_${scala.version.short} + ${akka.version} + test + + + com.typesafe.akka + akka-actor-testkit-typed_${scala.version.short} + ${akka.version} + test + + + fr.acinq.eclair + eclair-core_${scala.version.short} + ${project.version} + tests + test-jar + test + + + + diff --git a/plugins/peerswap/src/main/scala/fr/acinq/eclair/plugins/peerswap/ApiHandlers.scala b/plugins/peerswap/src/main/scala/fr/acinq/eclair/plugins/peerswap/ApiHandlers.scala new file mode 100644 index 0000000000..330f6d0184 --- /dev/null +++ b/plugins/peerswap/src/main/scala/fr/acinq/eclair/plugins/peerswap/ApiHandlers.scala @@ -0,0 +1,66 @@ +/* + * 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. + */ + +package fr.acinq.eclair.plugins.peerswap + +import akka.http.scaladsl.common.{NameReceptacle, NameUnmarshallerReceptacle} +import akka.http.scaladsl.server.Route +import fr.acinq.bitcoin.scalacompat.{ByteVector32, Satoshi} +import fr.acinq.eclair.api.directives.EclairDirectives +import fr.acinq.eclair.api.serde.FormParamExtractors._ + +object ApiHandlers { + + import fr.acinq.eclair.api.serde.JsonSupport.{marshaller, serialization} + import fr.acinq.eclair.plugins.peerswap.json.PeerSwapJsonSerializers.formats + + def registerRoutes(kit: PeerSwapKit, eclairDirectives: EclairDirectives): Route = { + import eclairDirectives._ + + val swapIdFormParam: NameUnmarshallerReceptacle[ByteVector32] = "swapId".as[ByteVector32](sha256HashUnmarshaller) + + val amountSatFormParam: NameReceptacle[Satoshi] = "amountSat".as[Satoshi] + + val swapIn: Route = postRequest("swapin") { implicit t => + formFields(shortChannelIdFormParam, amountSatFormParam) { (channelId, amount) => + complete(kit.swapIn(channelId, amount)) + } + } + + val swapOut: Route = postRequest("swapout") { implicit t => + formFields(shortChannelIdFormParam, amountSatFormParam) { (channelId, amount) => + complete(kit.swapOut(channelId, amount)) + } + } + + val listSwaps: Route = postRequest("listswaps") { implicit t => + complete(kit.listSwaps()) + } + + val cancelSwap: Route = postRequest("cancelswap") { implicit t => + formFields(swapIdFormParam) { swapId => + complete(kit.cancelSwap(swapId.toString())) + } + } + + val peerSwapRoutes: Route = swapIn ~ swapOut ~ listSwaps ~ cancelSwap + + peerSwapRoutes + } + +} + + diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/swap/LocalSwapKeyManager.scala b/plugins/peerswap/src/main/scala/fr/acinq/eclair/plugins/peerswap/LocalSwapKeyManager.scala similarity index 99% rename from eclair-core/src/main/scala/fr/acinq/eclair/swap/LocalSwapKeyManager.scala rename to plugins/peerswap/src/main/scala/fr/acinq/eclair/plugins/peerswap/LocalSwapKeyManager.scala index d226b5dee9..0fafb2d965 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/swap/LocalSwapKeyManager.scala +++ b/plugins/peerswap/src/main/scala/fr/acinq/eclair/plugins/peerswap/LocalSwapKeyManager.scala @@ -14,7 +14,7 @@ * limitations under the License. */ -package fr.acinq.eclair.swap +package fr.acinq.eclair.plugins.peerswap import com.google.common.cache.{CacheBuilder, CacheLoader, LoadingCache} import fr.acinq.bitcoin.scalacompat.DeterministicWallet._ diff --git a/plugins/peerswap/src/main/scala/fr/acinq/eclair/plugins/peerswap/PeerSwapPlugin.scala b/plugins/peerswap/src/main/scala/fr/acinq/eclair/plugins/peerswap/PeerSwapPlugin.scala new file mode 100644 index 0000000000..5292ed05cd --- /dev/null +++ b/plugins/peerswap/src/main/scala/fr/acinq/eclair/plugins/peerswap/PeerSwapPlugin.scala @@ -0,0 +1,101 @@ +/* + * 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. + */ + +package fr.acinq.eclair.plugins.peerswap + +import akka.actor.ActorSystem +import akka.actor.typed.scaladsl.AskPattern.Askable +import akka.actor.typed.scaladsl.Behaviors +import akka.actor.typed.scaladsl.adapter.{ClassicActorSystemOps, ClassicSchedulerOps} +import akka.actor.typed.{ActorRef, SupervisorStrategy} +import akka.http.scaladsl.server.Route +import akka.util.Timeout +import fr.acinq.bitcoin.scalacompat.Satoshi +import fr.acinq.eclair.api.directives.EclairDirectives +import fr.acinq.eclair.db.sqlite.SqliteUtils +import fr.acinq.eclair.plugins.peerswap.SwapResponses.{Response, Status} +import fr.acinq.eclair.plugins.peerswap.db.SwapsDb +import fr.acinq.eclair.plugins.peerswap.db.sqlite.SqliteSwapsDb +import fr.acinq.eclair.{CustomFeaturePlugin, Feature, InitFeature, Kit, NodeFeature, NodeParams, Plugin, PluginParams, RouteProvider, Setup, ShortChannelId} +import grizzled.slf4j.Logging +import scodec.bits.ByteVector + +import java.io.File +import java.nio.file.Files +import scala.concurrent.Future + +/** + * This plugin implements the PeerSwap protocol: https://github.com/ElementsProject/peerswap-spec/blob/main/peer-protocol.md + */ +object PeerSwapPlugin { + // TODO: derive this set from peerSwapMessageCodec tags + val peerSwapTags: Set[Int] = Set(42069, 42071, 42073, 42075, 42077, 42079, 42081) +} + +class PeerSwapPlugin extends Plugin with RouteProvider with Logging { + + var db: SwapsDb = _ + var swapKeyManager: LocalSwapKeyManager = _ + var pluginKit: PeerSwapKit = _ + + case object PeerSwapFeature extends Feature with InitFeature with NodeFeature { + val rfcName = "peer_swap_plugin_prototype" + val mandatory = 158 + } + + override def params: PluginParams = new CustomFeaturePlugin { + // @formatter:off + override def messageTags: Set[Int] = PeerSwapPlugin.peerSwapTags + override def feature: Feature = PeerSwapFeature + override def name: String = "PeerSwap" + // @formatter:on + } + + override def onSetup(setup: Setup): Unit = { + val chain = setup.config.getString("chain") + val chainDir = new File(setup.datadir, chain) + db = new SqliteSwapsDb(SqliteUtils.openSqliteFile(chainDir, "peer-swap.sqlite", exclusiveLock = false, journalMode = "wal", syncFlag = "normal")) + + // load seed + val seedFilename: String = "swap_seed.dat" + val seedPath: File = new File(setup.datadir, seedFilename) + val swapSeed: ByteVector = ByteVector(Files.readAllBytes(seedPath.toPath)) + swapKeyManager = new LocalSwapKeyManager(swapSeed, NodeParams.hashFromChain(chain)) + } + + override def onKit(kit: Kit): Unit = { + val data = db.restore().toSet + val swapRegister = kit.system.spawn(Behaviors.supervise(SwapRegister(kit.nodeParams, kit.paymentInitiator, kit.watcher, kit.register, kit.wallet, swapKeyManager, db, data)).onFailure(SupervisorStrategy.restart), "peerswap-plugin-swap-register") + pluginKit = PeerSwapKit(kit.nodeParams, kit.system, swapRegister) + } + + override def route(eclairDirectives: EclairDirectives): Route = ApiHandlers.registerRoutes(pluginKit, eclairDirectives) + +} + +case class PeerSwapKit(nodeParams: NodeParams, system: ActorSystem, swapRegister: ActorRef[SwapRegister.Command]) { + def swapIn(shortChannelId: ShortChannelId, amount: Satoshi)(implicit timeout: Timeout): Future[Response] = + swapRegister.ask(ref => SwapRegister.SwapInRequested(ref, amount, shortChannelId))(timeout, system.scheduler.toTyped) + + def swapOut(shortChannelId: ShortChannelId, amount: Satoshi)(implicit timeout: Timeout): Future[Response] = + swapRegister.ask(ref => SwapRegister.SwapOutRequested(ref, amount, shortChannelId))(timeout, system.scheduler.toTyped) + + def listSwaps()(implicit timeout: Timeout): Future[Iterable[Status]] = + swapRegister.ask(ref => SwapRegister.ListPendingSwaps(ref))(timeout, system.scheduler.toTyped) + + def cancelSwap(swapId: String)(implicit timeout: Timeout): Future[Response] = + swapRegister.ask(ref => SwapRegister.CancelSwapRequested(ref, swapId))(timeout, system.scheduler.toTyped) +} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/swap/SwapCommands.scala b/plugins/peerswap/src/main/scala/fr/acinq/eclair/plugins/peerswap/SwapCommands.scala similarity index 93% rename from eclair-core/src/main/scala/fr/acinq/eclair/swap/SwapCommands.scala rename to plugins/peerswap/src/main/scala/fr/acinq/eclair/plugins/peerswap/SwapCommands.scala index 79037bbdd0..74b7de5d4e 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/swap/SwapCommands.scala +++ b/plugins/peerswap/src/main/scala/fr/acinq/eclair/plugins/peerswap/SwapCommands.scala @@ -14,7 +14,7 @@ * limitations under the License. */ -package fr.acinq.eclair.swap +package fr.acinq.eclair.plugins.peerswap import akka.actor.typed.ActorRef import fr.acinq.bitcoin.scalacompat.Satoshi @@ -23,9 +23,9 @@ import fr.acinq.eclair.blockchain.OnChainWallet.MakeFundingTxResponse import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.{WatchFundingDeeplyBuriedTriggered, WatchOutputSpentTriggered, WatchTxConfirmedTriggered} import fr.acinq.eclair.channel.{CMD_GET_CHANNEL_DATA, ChannelData, RES_GET_CHANNEL_DATA, Register} import fr.acinq.eclair.payment.{Bolt11Invoice, PaymentEvent} -import fr.acinq.eclair.swap.SwapData._ -import fr.acinq.eclair.swap.SwapResponses.{Response, Status} -import fr.acinq.eclair.wire.protocol.{HasSwapId, OpeningTxBroadcasted, SwapInRequest, SwapOutRequest} +import fr.acinq.eclair.plugins.peerswap.SwapResponses.{Response, Status} +import fr.acinq.eclair.plugins.peerswap.wire.protocol.{HasSwapId, OpeningTxBroadcasted, SwapInRequest, SwapOutRequest} +import fr.acinq.eclair.wire.protocol.UnknownMessage object SwapCommands { @@ -45,7 +45,7 @@ object SwapCommands { sealed trait AwaitAgreementMessages extends SwapCommand case class SwapMessageReceived(message: HasSwapId) extends AwaitAgreementMessages with CreateOpeningTxMessages with AwaitClaimPaymentMessages with AwaitFeePaymentMessages with AwaitOpeningTxConfirmedMessages with ValidateTxMessages with ClaimSwapMessages with PayFeeInvoiceMessages with SendAgreementMessages - case class ForwardFailureAdapter(result: Register.ForwardFailure[HasSwapId]) extends AwaitAgreementMessages + case class ForwardFailureAdapter(result: Register.ForwardFailure[UnknownMessage]) extends AwaitAgreementMessages sealed trait CreateOpeningTxMessages extends SwapCommand case class InvoiceResponse(invoice: Bolt11Invoice) extends CreateOpeningTxMessages @@ -80,7 +80,7 @@ object SwapCommands { sealed trait SendAgreementMessages extends SwapCommand sealed trait AwaitFeePaymentMessages extends SwapCommand - case class ForwardShortIdFailureAdapter(result: Register.ForwardShortIdFailure[HasSwapId]) extends AwaitFeePaymentMessages with SendCoopCloseMessages with SendAgreementMessages + case class ForwardShortIdFailureAdapter(result: Register.ForwardShortIdFailure[UnknownMessage]) extends AwaitFeePaymentMessages with SendCoopCloseMessages with SendAgreementMessages sealed trait ValidateTxMessages extends SwapCommand case class ValidInvoice(invoice: Bolt11Invoice) extends ValidateTxMessages diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/swap/SwapData.scala b/plugins/peerswap/src/main/scala/fr/acinq/eclair/plugins/peerswap/SwapData.scala similarity index 75% rename from eclair-core/src/main/scala/fr/acinq/eclair/swap/SwapData.scala rename to plugins/peerswap/src/main/scala/fr/acinq/eclair/plugins/peerswap/SwapData.scala index 8372181e1d..a10eebf8a4 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/swap/SwapData.scala +++ b/plugins/peerswap/src/main/scala/fr/acinq/eclair/plugins/peerswap/SwapData.scala @@ -14,17 +14,16 @@ * limitations under the License. */ -package fr.acinq.eclair.swap +package fr.acinq.eclair.plugins.peerswap import fr.acinq.eclair.payment.Bolt11Invoice -import fr.acinq.eclair.swap -import fr.acinq.eclair.swap.SwapRole.SwapRole -import fr.acinq.eclair.wire.protocol.{OpeningTxBroadcasted, SwapAgreement, SwapRequest} +import fr.acinq.eclair.plugins.peerswap.SwapRole.SwapRole +import fr.acinq.eclair.plugins.peerswap.wire.protocol.{OpeningTxBroadcasted, SwapAgreement, SwapRequest} object SwapRole extends Enumeration { type SwapRole = Value - val Maker: swap.SwapRole.Value = Value(1, "Maker") - val Taker: swap.SwapRole.Value = Value(2, "Taker") + val Maker: SwapRole.Value = Value(1, "Maker") + val Taker: SwapRole.Value = Value(2, "Taker") } case class SwapData(request: SwapRequest, agreement: SwapAgreement, invoice: Bolt11Invoice, openingTxBroadcasted: OpeningTxBroadcasted, swapRole: SwapRole, isInitiator: Boolean) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/swap/SwapEvents.scala b/plugins/peerswap/src/main/scala/fr/acinq/eclair/plugins/peerswap/SwapEvents.scala similarity index 97% rename from eclair-core/src/main/scala/fr/acinq/eclair/swap/SwapEvents.scala rename to plugins/peerswap/src/main/scala/fr/acinq/eclair/plugins/peerswap/SwapEvents.scala index 94046592b2..0debd87535 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/swap/SwapEvents.scala +++ b/plugins/peerswap/src/main/scala/fr/acinq/eclair/plugins/peerswap/SwapEvents.scala @@ -14,7 +14,7 @@ * limitations under the License. */ -package fr.acinq.eclair.swap +package fr.acinq.eclair.plugins.peerswap import fr.acinq.bitcoin.scalacompat.Transaction import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.WatchTxConfirmedTriggered diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/swap/SwapHelpers.scala b/plugins/peerswap/src/main/scala/fr/acinq/eclair/plugins/peerswap/SwapHelpers.scala similarity index 87% rename from eclair-core/src/main/scala/fr/acinq/eclair/swap/SwapHelpers.scala rename to plugins/peerswap/src/main/scala/fr/acinq/eclair/plugins/peerswap/SwapHelpers.scala index c8eaff12f1..4dfdfba580 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/swap/SwapHelpers.scala +++ b/plugins/peerswap/src/main/scala/fr/acinq/eclair/plugins/peerswap/SwapHelpers.scala @@ -14,7 +14,7 @@ * limitations under the License. */ -package fr.acinq.eclair.swap +package fr.acinq.eclair.plugins.peerswap import akka.actor import akka.actor.typed.eventstream.EventStream @@ -33,11 +33,13 @@ import fr.acinq.eclair.channel.{CMD_GET_CHANNEL_DATA, ChannelData, RES_GET_CHANN import fr.acinq.eclair.db.PaymentType import fr.acinq.eclair.payment.send.PaymentInitiator.SendPaymentToNode import fr.acinq.eclair.payment.{Bolt11Invoice, PaymentEvent} -import fr.acinq.eclair.swap.SwapCommands._ -import fr.acinq.eclair.swap.SwapEvents.TransactionPublished -import fr.acinq.eclair.swap.SwapTransactions.makeSwapOpeningTxOut +import fr.acinq.eclair.plugins.peerswap.SwapCommands._ +import fr.acinq.eclair.plugins.peerswap.SwapEvents.TransactionPublished +import fr.acinq.eclair.plugins.peerswap.transactions.SwapTransactions.makeSwapOpeningTxOut +import fr.acinq.eclair.plugins.peerswap.wire.protocol.PeerSwapMessageCodecs.peerSwapMessageCodecWithFallback +import fr.acinq.eclair.plugins.peerswap.wire.protocol.{HasSwapId, OpeningTxBroadcasted} import fr.acinq.eclair.transactions.Transactions.{TransactionWithInputInfo, checkSpendable} -import fr.acinq.eclair.wire.protocol.{HasSwapId, OpeningTxBroadcasted} +import fr.acinq.eclair.wire.protocol.UnknownMessage import fr.acinq.eclair.{NodeParams, ShortChannelId, TimestampSecond, randomBytes32} import scala.concurrent.ExecutionContext.Implicits.global @@ -87,17 +89,23 @@ object SwapHelpers { def paymentEventAdapter(context: ActorContext[SwapCommand]): ActorRef[PaymentEvent] = context.messageAdapter[PaymentEvent](PaymentEventReceived) - def sendShortId(register: actor.ActorRef, shortChannelId: ShortChannelId)(message: HasSwapId)(implicit context: ActorContext[SwapCommand]): Unit = - register ! Register.ForwardShortId[HasSwapId](forwardShortIdAdapter(context), shortChannelId, message) + def sendShortId(register: actor.ActorRef, shortChannelId: ShortChannelId)(message: HasSwapId)(implicit context: ActorContext[SwapCommand]): Unit = { + val encoded = peerSwapMessageCodecWithFallback.encode(message).require + val unknownMessage = UnknownMessage(encoded.sliceToInt(0, 16, signed = false), encoded.toByteVector) + register ! Register.ForwardShortId(forwardShortIdAdapter(context), shortChannelId, unknownMessage) + } - def forwardShortIdAdapter(context: ActorContext[SwapCommand]): ActorRef[Register.ForwardShortIdFailure[HasSwapId]] = - context.messageAdapter[Register.ForwardShortIdFailure[HasSwapId]](ForwardShortIdFailureAdapter) + def forwardShortIdAdapter(context: ActorContext[SwapCommand]): ActorRef[Register.ForwardShortIdFailure[UnknownMessage]] = + context.messageAdapter[Register.ForwardShortIdFailure[UnknownMessage]](ForwardShortIdFailureAdapter) - def send(register: actor.ActorRef, channelId: ByteVector32)(message: HasSwapId)(implicit context: ActorContext[SwapCommand]): Unit = - register ! Register.Forward(forwardAdapter(context), channelId, message) + def send(register: actor.ActorRef, channelId: ByteVector32)(message: HasSwapId)(implicit context: ActorContext[SwapCommand]): Unit = { + val encoded = peerSwapMessageCodecWithFallback.encode(message).require + val unknownMessage = UnknownMessage(encoded.sliceToInt(0, 16, signed = false), encoded.toByteVector) + register ! Register.Forward(forwardAdapter(context), channelId, unknownMessage) + } - def forwardAdapter(context: ActorContext[SwapCommand]): ActorRef[Register.ForwardFailure[HasSwapId]] = - context.messageAdapter[Register.ForwardFailure[HasSwapId]](ForwardFailureAdapter) + def forwardAdapter(context: ActorContext[SwapCommand]): ActorRef[Register.ForwardFailure[UnknownMessage]] = + context.messageAdapter[Register.ForwardFailure[UnknownMessage]](ForwardFailureAdapter) def fundOpening(wallet: OnChainWallet, feeRatePerKw: FeeratePerKw)(amount: Satoshi, makerPubkey: PublicKey, takerPubkey: PublicKey, invoice: Bolt11Invoice)(implicit context: ActorContext[SwapCommand]): Unit = { // setup conditions satisfied, create the opening tx diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/swap/SwapKeyManager.scala b/plugins/peerswap/src/main/scala/fr/acinq/eclair/plugins/peerswap/SwapKeyManager.scala similarity index 98% rename from eclair-core/src/main/scala/fr/acinq/eclair/swap/SwapKeyManager.scala rename to plugins/peerswap/src/main/scala/fr/acinq/eclair/plugins/peerswap/SwapKeyManager.scala index 87b6738721..c72bcfe3c5 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/swap/SwapKeyManager.scala +++ b/plugins/peerswap/src/main/scala/fr/acinq/eclair/plugins/peerswap/SwapKeyManager.scala @@ -14,7 +14,7 @@ * limitations under the License. */ -package fr.acinq.eclair.swap +package fr.acinq.eclair.plugins.peerswap import fr.acinq.bitcoin.scalacompat.DeterministicWallet.{ExtendedPrivateKey, ExtendedPublicKey} import fr.acinq.bitcoin.scalacompat.{ByteVector64, DeterministicWallet, Protocol} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/swap/SwapMaker.scala b/plugins/peerswap/src/main/scala/fr/acinq/eclair/plugins/peerswap/SwapMaker.scala similarity index 94% rename from eclair-core/src/main/scala/fr/acinq/eclair/swap/SwapMaker.scala rename to plugins/peerswap/src/main/scala/fr/acinq/eclair/plugins/peerswap/SwapMaker.scala index e1447cde19..cae2594889 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/swap/SwapMaker.scala +++ b/plugins/peerswap/src/main/scala/fr/acinq/eclair/plugins/peerswap/SwapMaker.scala @@ -14,7 +14,7 @@ * limitations under the License. */ -package fr.acinq.eclair.swap +package fr.acinq.eclair.plugins.peerswap import akka.actor import akka.actor.typed.eventstream.EventStream.Publish @@ -31,15 +31,15 @@ import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.{WatchFundingDeeplyBuriedT import fr.acinq.eclair.blockchain.fee.FeeratePerKw import fr.acinq.eclair.payment.receive.MultiPartHandler.{CreateInvoiceActor, ReceivePayment} import fr.acinq.eclair.payment.{Bolt11Invoice, PaymentReceived} -import fr.acinq.eclair.swap.SwapCommands._ -import fr.acinq.eclair.swap.SwapEvents._ -import fr.acinq.eclair.swap.SwapHelpers._ -import fr.acinq.eclair.swap.SwapResponses.{CreateFailed, Error, Fail, InternalError, InvalidMessage, PeerCanceled, SwapError, SwapStatus, UserCanceled} -import fr.acinq.eclair.swap.SwapRole.Maker -import fr.acinq.eclair.swap.SwapScripts.claimByCsvDelta -import fr.acinq.eclair.swap.SwapTransactions._ -import fr.acinq.eclair.transactions.Transactions.{SwapClaimByCoopTx, SwapClaimByCsvTx} -import fr.acinq.eclair.wire.protocol._ +import fr.acinq.eclair.plugins.peerswap.SwapCommands._ +import fr.acinq.eclair.plugins.peerswap.SwapEvents._ +import fr.acinq.eclair.plugins.peerswap.SwapHelpers._ +import fr.acinq.eclair.plugins.peerswap.SwapResponses.{CreateFailed, Error, Fail, InternalError, InvalidMessage, PeerCanceled, SwapError, SwapStatus, UserCanceled} +import fr.acinq.eclair.plugins.peerswap.SwapRole.Maker +import fr.acinq.eclair.plugins.peerswap.SwapScripts.claimByCsvDelta +import fr.acinq.eclair.plugins.peerswap.db.SwapsDb +import fr.acinq.eclair.plugins.peerswap.transactions.SwapTransactions._ +import fr.acinq.eclair.plugins.peerswap.wire.protocol._ import fr.acinq.eclair.{NodeParams, ShortChannelId, TimestampSecond} import scodec.bits.ByteVector @@ -106,22 +106,22 @@ object SwapMaker { */ - def apply(nodeParams: NodeParams, watcher: ActorRef[ZmqWatcher.Command], register: actor.ActorRef, wallet: OnChainWallet): Behavior[SwapCommands.SwapCommand] = + def apply(nodeParams: NodeParams, watcher: ActorRef[ZmqWatcher.Command], register: actor.ActorRef, wallet: OnChainWallet, keyManager: SwapKeyManager, db: SwapsDb): Behavior[SwapCommands.SwapCommand] = Behaviors.setup { context => Behaviors.receiveMessagePartial { case StartSwapInSender(amount, swapId, shortChannelId) => - new SwapMaker(shortChannelId, nodeParams, watcher, register, wallet, context) + new SwapMaker(shortChannelId, nodeParams, watcher, register, wallet, keyManager, db, context) .createSwap(amount, swapId) case StartSwapOutReceiver(request: SwapOutRequest) => ShortChannelId.fromCoordinates(request.scid) match { - case Success(shortChannelId) => new SwapMaker(shortChannelId, nodeParams, watcher, register, wallet, context) + case Success(shortChannelId) => new SwapMaker(shortChannelId, nodeParams, watcher, register, wallet, keyManager, db, context) .validateRequest(request) case Failure(e) => context.log.error(s"received swap request with invalid shortChannelId: $request, $e") Behaviors.stopped } case RestoreSwap(d) => ShortChannelId.fromCoordinates(d.request.scid) match { - case Success(shortChannelId) => new SwapMaker(shortChannelId, nodeParams, watcher, register, wallet, context) + case Success(shortChannelId) => new SwapMaker(shortChannelId, nodeParams, watcher, register, wallet, keyManager, db, context) .awaitClaimPayment(d.request, d.agreement, d.invoice, d.openingTxBroadcasted, d.isInitiator) case Failure(e) => context.log.error(s"could not restore swap sender with invalid shortChannelId: $d, $e") Behaviors.stopped @@ -131,11 +131,10 @@ object SwapMaker { } } -private class SwapMaker(shortChannelId: ShortChannelId, nodeParams: NodeParams, watcher: ActorRef[ZmqWatcher.Command], register: actor.ActorRef, wallet: OnChainWallet, implicit val context: ActorContext[SwapCommands.SwapCommand]) { +private class SwapMaker(shortChannelId: ShortChannelId, nodeParams: NodeParams, watcher: ActorRef[ZmqWatcher.Command], register: actor.ActorRef, wallet: OnChainWallet, keyManager: SwapKeyManager, db: SwapsDb, implicit val context: ActorContext[SwapCommands.SwapCommand]) { val protocolVersion = 2 val noAsset = "" implicit val timeout: Timeout = 30 seconds - private val keyManager: SwapKeyManager = nodeParams.swapKeyManager private implicit val feeRatePerKw: FeeratePerKw = nodeParams.onChainFeeConf.feeEstimator.getFeeratePerKw(target = nodeParams.onChainFeeConf.feeTargets.fundingBlockTarget) private val openingFee = (feeRatePerKw * openingTxWeight / 1000).toLong // TODO: how should swap out initiator calculate an acceptable swap opening tx fee? private val maxPremium = (feeRatePerKw * claimByInvoiceTxWeight / 1000).toLong // TODO: how should swap sender calculate an acceptable premium? @@ -223,7 +222,7 @@ private class SwapMaker(shortChannelId: ShortChannelId, nodeParams: NodeParams, commitOpening(wallet)(request.swapId, invoice, fundingResponse, "swap-in-sender-opening") Behaviors.same case OpeningTxCommitted(invoice, openingTxBroadcasted) => - nodeParams.db.swaps.add(SwapData(request, agreement, invoice, openingTxBroadcasted, Maker, isInitiator)) + db.add(SwapData(request, agreement, invoice, openingTxBroadcasted, Maker, isInitiator)) awaitClaimPayment(request, agreement, invoice, openingTxBroadcasted, isInitiator) case OpeningTxFailed(error, None) => swapCanceled(InternalError(request.swapId, s"failed to fund swap open tx, error: $error")) case OpeningTxFailed(error, Some(r)) => rollback(wallet)(error, r.fundingTx) @@ -338,7 +337,7 @@ private class SwapMaker(shortChannelId: ShortChannelId, nodeParams: NodeParams, def swapCompleted(event: SwapEvent): Behavior[SwapCommand] = { context.system.eventStream ! Publish(event) context.log.info(s"completed swap: $event.") - nodeParams.db.swaps.addResult(event) + db.addResult(event) Behaviors.stopped } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/swap/SwapRegister.scala b/plugins/peerswap/src/main/scala/fr/acinq/eclair/plugins/peerswap/SwapRegister.scala similarity index 73% rename from eclair-core/src/main/scala/fr/acinq/eclair/swap/SwapRegister.scala rename to plugins/peerswap/src/main/scala/fr/acinq/eclair/plugins/peerswap/SwapRegister.scala index e15521a531..2ee0b3c0e1 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/swap/SwapRegister.scala +++ b/plugins/peerswap/src/main/scala/fr/acinq/eclair/plugins/peerswap/SwapRegister.scala @@ -14,22 +14,27 @@ * limitations under the License. */ -package fr.acinq.eclair.swap +package fr.acinq.eclair.plugins.peerswap import akka.actor import akka.actor.typed import akka.actor.typed.ActorRef.ActorRefOps import akka.actor.typed.scaladsl.AskPattern.Askable +import akka.actor.typed.scaladsl.adapter.TypedActorRefOps import akka.actor.typed.scaladsl.{ActorContext, Behaviors} import akka.actor.typed.{ActorRef, Behavior, SupervisorStrategy} import fr.acinq.bitcoin.scalacompat.Satoshi import fr.acinq.eclair.blockchain.OnChainWallet import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher -import fr.acinq.eclair.swap.SwapCommands._ -import fr.acinq.eclair.swap.SwapRegister.Command -import fr.acinq.eclair.swap.SwapResponses.{Response, Status, SwapOpened} -import fr.acinq.eclair.wire.protocol.{HasSwapId, SwapInRequest, SwapOutRequest} +import fr.acinq.eclair.io.UnknownMessageReceived +import fr.acinq.eclair.plugins.peerswap.SwapCommands._ +import fr.acinq.eclair.plugins.peerswap.SwapRegister.Command +import fr.acinq.eclair.plugins.peerswap.SwapResponses.{Response, Status, SwapOpened} +import fr.acinq.eclair.plugins.peerswap.db.SwapsDb +import fr.acinq.eclair.plugins.peerswap.wire.protocol.PeerSwapMessageCodecs.peerSwapMessageCodec +import fr.acinq.eclair.plugins.peerswap.wire.protocol.{HasSwapId, SwapInRequest, SwapOutRequest} import fr.acinq.eclair.{NodeParams, ShortChannelId, randomBytes32} +import scodec.Attempt import scala.concurrent.duration.DurationInt import scala.concurrent.{Await, Future} @@ -43,6 +48,7 @@ object SwapRegister { } sealed trait RegisteringMessages extends Command + case class PluginMessageReceived(message: UnknownMessageReceived) extends RegisteringMessages case class SwapInRequested(replyTo: ActorRef[Response], amount: Satoshi, shortChannelId: ShortChannelId) extends RegisteringMessages with ReplyToMessages case class SwapOutRequested(replyTo: ActorRef[Response], amount: Satoshi, shortChannelId: ShortChannelId) extends RegisteringMessages with ReplyToMessages case class MessageReceived(message: HasSwapId) extends RegisteringMessages @@ -51,12 +57,12 @@ object SwapRegister { case class CancelSwapRequested(replyTo: ActorRef[Response], swapId: String) extends RegisteringMessages with ReplyToMessages // @formatter:on - def apply(nodeParams: NodeParams, paymentInitiator: actor.ActorRef, watcher: ActorRef[ZmqWatcher.Command], register: actor.ActorRef, wallet: OnChainWallet, data: Set[SwapData]): Behavior[Command] = Behaviors.setup { context => - new SwapRegister(context, nodeParams, paymentInitiator, watcher, register, wallet, data).initializing + def apply(nodeParams: NodeParams, paymentInitiator: actor.ActorRef, watcher: ActorRef[ZmqWatcher.Command], register: actor.ActorRef, wallet: OnChainWallet, keyManager: SwapKeyManager, db: SwapsDb, data: Set[SwapData]): Behavior[Command] = Behaviors.setup { context => + new SwapRegister(context, nodeParams, paymentInitiator, watcher, register, wallet, keyManager, db, data).initializing } } -private class SwapRegister(context: ActorContext[Command], nodeParams: NodeParams, paymentInitiator: actor.ActorRef, watcher: ActorRef[ZmqWatcher.Command], register: actor.ActorRef, wallet: OnChainWallet, data: Set[SwapData]) { +private class SwapRegister(context: ActorContext[Command], nodeParams: NodeParams, paymentInitiator: actor.ActorRef, watcher: ActorRef[ZmqWatcher.Command], register: actor.ActorRef, wallet: OnChainWallet, keyManager: SwapKeyManager, db: SwapsDb, data: Set[SwapData]) { import SwapRegister._ private def myReceive[B <: Command : ClassTag](stateName: String)(f: B => Behavior[Command]): Behavior[Command] = @@ -72,9 +78,9 @@ private class SwapRegister(context: ActorContext[Command], nodeParams: NodeParam val swaps = data.map { state => val swap: typed.ActorRef[SwapCommands.SwapCommand] = { state.swapRole match { - case SwapRole.Maker => context.spawn(Behaviors.supervise(SwapMaker(nodeParams, watcher, register, wallet)) + case SwapRole.Maker => context.spawn(Behaviors.supervise(SwapMaker(nodeParams, watcher, register, wallet, keyManager, db)) .onFailure(typed.SupervisorStrategy.restart), "SwapMaker-" + state.request.scid) - case SwapRole.Taker => context.spawn(Behaviors.supervise(SwapTaker(nodeParams, paymentInitiator, watcher, register, wallet)) + case SwapRole.Taker => context.spawn(Behaviors.supervise(SwapTaker(nodeParams, paymentInitiator, watcher, register, wallet, keyManager, db)) .onFailure(typed.SupervisorStrategy.restart), "SwapTaker-" + state.request.scid) } } @@ -85,13 +91,22 @@ private class SwapRegister(context: ActorContext[Command], nodeParams: NodeParam registering(swaps) } + def watchForUnknownMessage(watch: Boolean)(implicit context: ActorContext[Command]): Unit = + if (watch) context.system.classicSystem.eventStream.subscribe(unknownMessageAdapter(context).toClassic, classOf[UnknownMessageReceived]) + else context.system.classicSystem.eventStream.unsubscribe(unknownMessageAdapter(context).toClassic, classOf[UnknownMessageReceived]) + + def unknownMessageAdapter(context: ActorContext[Command]): ActorRef[UnknownMessageReceived] = { + context.messageAdapter[UnknownMessageReceived](PluginMessageReceived) + } + private def registering(swaps: Map[String, ActorRef[SwapCommands.SwapCommand]]): Behavior[Command] = { // TODO: fail requests for swaps on a channel if one already exists for the channel; keep a list of channels with active swaps // TODO: check currently registered swaps, and swap db, to prevent reuse of a swapId + watchForUnknownMessage(watch = true)(context) myReceive[RegisteringMessages]("registering") { case SwapInRequested(replyTo, amount, shortChannelId) => val swapId = randomBytes32().toHex - val swap = context.spawn(Behaviors.supervise(SwapMaker(nodeParams, watcher, register, wallet)) + val swap = context.spawn(Behaviors.supervise(SwapMaker(nodeParams, watcher, register, wallet, keyManager, db)) .onFailure(SupervisorStrategy.restart), "Swap-" + shortChannelId.toString) context.watchWith(swap, SwapTerminated(swapId)) swap ! StartSwapInSender(amount, swapId, shortChannelId) @@ -100,7 +115,7 @@ private class SwapRegister(context: ActorContext[Command], nodeParams: NodeParam case SwapOutRequested(replyTo, amount, shortChannelId) => val swapId = randomBytes32().toHex - val swap = context.spawn(Behaviors.supervise(SwapTaker(nodeParams, paymentInitiator, watcher, register, wallet)) + val swap = context.spawn(Behaviors.supervise(SwapTaker(nodeParams, paymentInitiator, watcher, register, wallet, keyManager, db)) .onFailure(SupervisorStrategy.restart), "Swap-" + shortChannelId.toString) context.watchWith(swap, SwapTerminated(swapId)) swap ! StartSwapOutSender(amount, swapId, shortChannelId) @@ -108,19 +123,28 @@ private class SwapRegister(context: ActorContext[Command], nodeParams: NodeParam registering(swaps + (swapId -> swap)) case MessageReceived(request: SwapInRequest) => - val swap = context.spawn(Behaviors.supervise(SwapTaker(nodeParams, paymentInitiator, watcher, register, wallet)) + val swap = context.spawn(Behaviors.supervise(SwapTaker(nodeParams, paymentInitiator, watcher, register, wallet, keyManager, db)) .onFailure(SupervisorStrategy.restart), "Swap-"+ request.scid) context.watchWith(swap, SwapTerminated(request.swapId)) swap ! StartSwapInReceiver(request) registering(swaps + (request.swapId -> swap)) case MessageReceived(request: SwapOutRequest) => - val swap = context.spawn(Behaviors.supervise(SwapMaker(nodeParams, watcher, register, wallet)) + val swap = context.spawn(Behaviors.supervise(SwapMaker(nodeParams, watcher, register, wallet, keyManager, db)) .onFailure(SupervisorStrategy.restart), "Swap-" + request.scid) context.watchWith(swap, SwapTerminated(request.swapId)) swap ! StartSwapOutReceiver(request) registering(swaps + (request.swapId -> swap)) + case PluginMessageReceived(unknownMessageReceived) => + if (PeerSwapPlugin.peerSwapTags.contains(unknownMessageReceived.message.tag)) { + peerSwapMessageCodec.decode(unknownMessageReceived.message.data.toBitVector) match { + case Attempt.Successful(m) => context.self ! MessageReceived(m.value) + case _ => context.log.error(s"could not decode peerswap message $unknownMessageReceived") + } + } + Behaviors.same + case MessageReceived(msg) => swaps.get(msg.swapId) match { case Some(swap) => swap ! SwapMessageReceived(msg) Behaviors.same diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/swap/SwapResponses.scala b/plugins/peerswap/src/main/scala/fr/acinq/eclair/plugins/peerswap/SwapResponses.scala similarity index 94% rename from eclair-core/src/main/scala/fr/acinq/eclair/swap/SwapResponses.scala rename to plugins/peerswap/src/main/scala/fr/acinq/eclair/plugins/peerswap/SwapResponses.scala index 1846a8574f..b90ce77540 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/swap/SwapResponses.scala +++ b/plugins/peerswap/src/main/scala/fr/acinq/eclair/plugins/peerswap/SwapResponses.scala @@ -14,10 +14,10 @@ * limitations under the License. */ -package fr.acinq.eclair.swap +package fr.acinq.eclair.plugins.peerswap import fr.acinq.eclair.payment.Bolt11Invoice -import fr.acinq.eclair.wire.protocol.{HasSwapId, OpeningTxBroadcasted, SwapAgreement, SwapRequest} +import fr.acinq.eclair.plugins.peerswap.wire.protocol.{HasSwapId, OpeningTxBroadcasted, SwapAgreement, SwapRequest} object SwapResponses { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/swap/SwapScripts.scala b/plugins/peerswap/src/main/scala/fr/acinq/eclair/plugins/peerswap/SwapScripts.scala similarity index 98% rename from eclair-core/src/main/scala/fr/acinq/eclair/swap/SwapScripts.scala rename to plugins/peerswap/src/main/scala/fr/acinq/eclair/plugins/peerswap/SwapScripts.scala index 8d527d7c5a..102ce3868e 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/swap/SwapScripts.scala +++ b/plugins/peerswap/src/main/scala/fr/acinq/eclair/plugins/peerswap/SwapScripts.scala @@ -14,7 +14,7 @@ * limitations under the License. */ -package fr.acinq.eclair.swap +package fr.acinq.eclair.plugins.peerswap import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.bitcoin.scalacompat._ diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/swap/SwapTaker.scala b/plugins/peerswap/src/main/scala/fr/acinq/eclair/plugins/peerswap/SwapTaker.scala similarity index 95% rename from eclair-core/src/main/scala/fr/acinq/eclair/swap/SwapTaker.scala rename to plugins/peerswap/src/main/scala/fr/acinq/eclair/plugins/peerswap/SwapTaker.scala index 7a17630213..753c90857d 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/swap/SwapTaker.scala +++ b/plugins/peerswap/src/main/scala/fr/acinq/eclair/plugins/peerswap/SwapTaker.scala @@ -14,7 +14,7 @@ * limitations under the License. */ -package fr.acinq.eclair.swap +package fr.acinq.eclair.plugins.peerswap import akka.actor import akka.actor.typed.eventstream.EventStream.Publish @@ -28,14 +28,14 @@ import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.{WatchOutputSpentTriggered, WatchTxConfirmedTriggered} import fr.acinq.eclair.blockchain.fee.FeeratePerKw import fr.acinq.eclair.payment.{Bolt11Invoice, PaymentEvent, PaymentFailed, PaymentSent} -import fr.acinq.eclair.swap.SwapCommands._ -import fr.acinq.eclair.swap.SwapEvents._ -import fr.acinq.eclair.swap.SwapHelpers._ -import fr.acinq.eclair.swap.SwapResponses.{CreateFailed, Error, Fail, InternalError, InvalidMessage, PeerCanceled, SwapError, SwapStatus, UserCanceled} -import fr.acinq.eclair.swap.SwapRole.Taker -import fr.acinq.eclair.swap.SwapTransactions._ -import fr.acinq.eclair.transactions.Transactions.SwapClaimByCoopTx -import fr.acinq.eclair.wire.protocol._ +import fr.acinq.eclair.plugins.peerswap.SwapCommands._ +import fr.acinq.eclair.plugins.peerswap.SwapEvents._ +import fr.acinq.eclair.plugins.peerswap.SwapHelpers._ +import fr.acinq.eclair.plugins.peerswap.SwapResponses.{CreateFailed, Error, Fail, InternalError, InvalidMessage, PeerCanceled, SwapError, SwapStatus, UserCanceled} +import fr.acinq.eclair.plugins.peerswap.SwapRole.Taker +import fr.acinq.eclair.plugins.peerswap.db.SwapsDb +import fr.acinq.eclair.plugins.peerswap.transactions.SwapTransactions._ +import fr.acinq.eclair.plugins.peerswap.wire.protocol._ import fr.acinq.eclair.{NodeParams, ShortChannelId, ToMilliSatoshiConversion} import scodec.bits.ByteVector @@ -102,22 +102,22 @@ object SwapTaker { */ - def apply(nodeParams: NodeParams, paymentInitiator: actor.ActorRef, watcher: ActorRef[ZmqWatcher.Command], register: actor.ActorRef, wallet: OnChainWallet): Behavior[SwapCommand] = + def apply(nodeParams: NodeParams, paymentInitiator: actor.ActorRef, watcher: ActorRef[ZmqWatcher.Command], register: actor.ActorRef, wallet: OnChainWallet, keyManager: SwapKeyManager, db: SwapsDb): Behavior[SwapCommand] = Behaviors.setup { context => Behaviors.receiveMessagePartial { case StartSwapOutSender(amount, swapId, shortChannelId) => - new SwapTaker(shortChannelId, nodeParams, paymentInitiator, watcher, register, wallet, context) + new SwapTaker(shortChannelId, nodeParams, paymentInitiator, watcher, register, wallet, keyManager, db, context) .createSwap(amount, swapId) case StartSwapInReceiver(request: SwapInRequest) => ShortChannelId.fromCoordinates(request.scid) match { - case Success(shortChannelId) => new SwapTaker(shortChannelId, nodeParams, paymentInitiator, watcher, register, wallet, context) + case Success(shortChannelId) => new SwapTaker(shortChannelId, nodeParams, paymentInitiator, watcher, register, wallet, keyManager, db, context) .validateRequest(request) case Failure(e) => context.log.error(s"received swap request with invalid shortChannelId: $request, $e") Behaviors.stopped } case RestoreSwap(d) => ShortChannelId.fromCoordinates(d.request.scid) match { - case Success(shortChannelId) => new SwapTaker(shortChannelId, nodeParams, paymentInitiator, watcher, register, wallet, context) + case Success(shortChannelId) => new SwapTaker(shortChannelId, nodeParams, paymentInitiator, watcher, register, wallet, keyManager, db, context) .awaitOpeningTxConfirmed(d.request, d.agreement, d.openingTxBroadcasted, d.isInitiator) case Failure(e) => context.log.error(s"could not restore swap receiver with invalid shortChannelId: $d, $e") Behaviors.stopped @@ -127,12 +127,11 @@ object SwapTaker { } } -private class SwapTaker(shortChannelId: ShortChannelId, nodeParams: NodeParams, paymentInitiator: actor.ActorRef, watcher: ActorRef[ZmqWatcher.Command], register: actor.ActorRef, wallet: OnChainWallet, implicit val context: ActorContext[SwapCommands.SwapCommand]) { +private class SwapTaker(shortChannelId: ShortChannelId, nodeParams: NodeParams, paymentInitiator: actor.ActorRef, watcher: ActorRef[ZmqWatcher.Command], register: actor.ActorRef, wallet: OnChainWallet, keyManager: SwapKeyManager, db: SwapsDb, implicit val context: ActorContext[SwapCommands.SwapCommand]) { val protocolVersion = 2 val noAsset = "" implicit val timeout: Timeout = 30 seconds - private val keyManager: SwapKeyManager = nodeParams.swapKeyManager private val feeRatePerKw: FeeratePerKw = nodeParams.onChainFeeConf.feeEstimator.getFeeratePerKw(target = nodeParams.onChainFeeConf.feeTargets.fundingBlockTarget) private val premium = (feeRatePerKw * claimByInvoiceTxWeight / 1000).toLong.sat // TODO: how should swap receiver calculate an acceptable premium? private val maxOpeningFee = (feeRatePerKw * openingTxWeight / 1000).toLong.sat // TODO: how should swap out initiator calculate an acceptable swap opening tx fee? @@ -262,7 +261,7 @@ private class SwapTaker(shortChannelId: ShortChannelId, nodeParams: NodeParams, receiveSwapMessage[ValidateTxMessages](context, "validateOpeningTx") { case ValidInvoice(invoice) if validOpeningTx(openingTx, openingTxBroadcasted.scriptOut, (request.amount + agreement.premium).sat, makerPubkey(request, agreement, isInitiator), takerPubkey(request.swapId), invoice.paymentHash) => - nodeParams.db.swaps.add(SwapData(request, agreement, invoice, openingTxBroadcasted, Taker, isInitiator)) + db.add(SwapData(request, agreement, invoice, openingTxBroadcasted, Taker, isInitiator)) payClaimInvoice(request, agreement, openingTxBroadcasted, invoice, openingTx, isInitiator) case ValidInvoice(_) => sendCoopClose(request,s"Invalid opening tx: $openingTx", Some(openingTxBroadcasted)) case InvalidInvoice(reason) => sendCoopClose(request, reason, Some(openingTxBroadcasted)) @@ -291,8 +290,8 @@ private class SwapTaker(shortChannelId: ShortChannelId, nodeParams: NodeParams, } def claimSwap(request: SwapRequest, agreement: SwapAgreement, openingTxBroadcasted: OpeningTxBroadcasted, invoice: Bolt11Invoice, paymentPreimage: ByteVector32, openingTx: Transaction, isInitiator: Boolean): Behavior[SwapCommand] = { - val inputInfo = makeSwapOpeningInputInfo(openingTx.hash, openingTxBroadcasted.scriptOut.toInt, (request.amount + agreement.premium).sat, makerPubkey(request, agreement, isInitiator), takerPubkey(request.swapId), invoice.paymentHash) - val claimByInvoiceTx = makeSwapClaimByInvoiceTx((request.amount + agreement.premium).sat, makerPubkey(request, agreement, isInitiator), takerPrivkey(request.swapId), paymentPreimage, feeRatePerKw, openingTx.hash, openingTxBroadcasted.scriptOut.toInt) + val inputInfo = makeSwapOpeningInputInfo(openingTx.txid, openingTxBroadcasted.scriptOut.toInt, (request.amount + agreement.premium).sat, makerPubkey(request, agreement, isInitiator), takerPubkey(request.swapId), invoice.paymentHash) + val claimByInvoiceTx = makeSwapClaimByInvoiceTx((request.amount + agreement.premium).sat, makerPubkey(request, agreement, isInitiator), takerPrivkey(request.swapId), paymentPreimage, feeRatePerKw, openingTx.txid, openingTxBroadcasted.scriptOut.toInt) def claimByInvoiceConfirmedAdapter: ActorRef[WatchTxConfirmedTriggered] = context.messageAdapter[WatchTxConfirmedTriggered](ClaimTxConfirmed) watchForTxConfirmation(watcher)(claimByInvoiceConfirmedAdapter, claimByInvoiceTx.txid, nodeParams.channelConf.minDepthBlocks) diff --git a/plugins/peerswap/src/main/scala/fr/acinq/eclair/plugins/peerswap/db/DualSwapsDb.scala b/plugins/peerswap/src/main/scala/fr/acinq/eclair/plugins/peerswap/db/DualSwapsDb.scala new file mode 100644 index 0000000000..54ec84eae3 --- /dev/null +++ b/plugins/peerswap/src/main/scala/fr/acinq/eclair/plugins/peerswap/db/DualSwapsDb.scala @@ -0,0 +1,55 @@ +/* + * 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. + */ + +package fr.acinq.eclair.plugins.peerswap.db + +import com.google.common.util.concurrent.ThreadFactoryBuilder +import fr.acinq.eclair.db.DualDatabases.runAsync +import fr.acinq.eclair.plugins.peerswap.SwapData +import fr.acinq.eclair.plugins.peerswap.SwapEvents.SwapEvent + +import java.util.concurrent.Executors +import scala.concurrent.ExecutionContext + +case class DualSwapsDb(primary: SwapsDb, secondary: SwapsDb) extends SwapsDb { + + private implicit val ec: ExecutionContext = ExecutionContext.fromExecutor(Executors.newSingleThreadExecutor(new ThreadFactoryBuilder().setNameFormat("db-pending-commands").build())) + + override def add(swapData: SwapData): Unit = { + runAsync(secondary.add(swapData)) + primary.add(swapData) + } + + override def addResult(swapEvent: SwapEvent): Unit = { + runAsync(secondary.addResult(swapEvent)) + primary.addResult(swapEvent) + } + + override def remove(swapId: String): Unit = { + runAsync(secondary.remove(swapId)) + primary.remove(swapId) + } + + override def restore(): Seq[SwapData] = { + runAsync(secondary.restore()) + primary.restore() + } + + override def list(): Seq[SwapData] = { + runAsync(secondary.list()) + primary.list() + } +} \ No newline at end of file diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/SwapsDb.scala b/plugins/peerswap/src/main/scala/fr/acinq/eclair/plugins/peerswap/db/SwapsDb.scala similarity index 91% rename from eclair-core/src/main/scala/fr/acinq/eclair/db/SwapsDb.scala rename to plugins/peerswap/src/main/scala/fr/acinq/eclair/plugins/peerswap/db/SwapsDb.scala index 2be2ce2d2a..f36da50f1e 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/SwapsDb.scala +++ b/plugins/peerswap/src/main/scala/fr/acinq/eclair/plugins/peerswap/db/SwapsDb.scala @@ -14,13 +14,13 @@ * limitations under the License. */ -package fr.acinq.eclair.db +package fr.acinq.eclair.plugins.peerswap.db import fr.acinq.eclair.payment.Bolt11Invoice -import fr.acinq.eclair.swap.SwapEvents.SwapEvent -import fr.acinq.eclair.swap.SwapRole.Maker -import fr.acinq.eclair.swap.{SwapData, SwapRole} -import fr.acinq.eclair.wire.protocol._ +import fr.acinq.eclair.plugins.peerswap.SwapEvents.SwapEvent +import fr.acinq.eclair.plugins.peerswap.SwapRole.Maker +import fr.acinq.eclair.plugins.peerswap.wire.protocol._ +import fr.acinq.eclair.plugins.peerswap.{SwapData, SwapRole} import org.json4s.jackson.JsonMethods.{compact, parse, render} import org.json4s.jackson.Serialization diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgSwapsDb.scala b/plugins/peerswap/src/main/scala/fr/acinq/eclair/plugins/peerswap/db/pg/PgSwapsDb.scala similarity index 91% rename from eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgSwapsDb.scala rename to plugins/peerswap/src/main/scala/fr/acinq/eclair/plugins/peerswap/db/pg/PgSwapsDb.scala index cfacf49137..8b265b5a8a 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgSwapsDb.scala +++ b/plugins/peerswap/src/main/scala/fr/acinq/eclair/plugins/peerswap/db/pg/PgSwapsDb.scala @@ -14,15 +14,15 @@ * limitations under the License. */ -package fr.acinq.eclair.db.pg +package fr.acinq.eclair.plugins.peerswap.db.pg import fr.acinq.eclair.db.Monitoring.Metrics.withMetrics import fr.acinq.eclair.db.Monitoring.Tags.DbBackends -import fr.acinq.eclair.db.SwapsDb -import fr.acinq.eclair.db.SwapsDb.{getSwapData, setSwapData} import fr.acinq.eclair.db.pg.PgUtils.PgLock.NoLock.withLock -import fr.acinq.eclair.swap.SwapData -import fr.acinq.eclair.swap.SwapEvents.SwapEvent +import fr.acinq.eclair.plugins.peerswap.SwapData +import fr.acinq.eclair.plugins.peerswap.SwapEvents.SwapEvent +import fr.acinq.eclair.plugins.peerswap.db.SwapsDb +import fr.acinq.eclair.plugins.peerswap.db.SwapsDb.{getSwapData, setSwapData} import grizzled.slf4j.Logging import javax.sql.DataSource @@ -34,7 +34,7 @@ object PgSwapsDb { class PgSwapsDb(implicit ds: DataSource) extends SwapsDb with Logging { - import PgUtils._ + import fr.acinq.eclair.db.pg.PgUtils._ import ExtendedResultSet._ import PgSwapsDb._ diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteSwapsDb.scala b/plugins/peerswap/src/main/scala/fr/acinq/eclair/plugins/peerswap/db/sqlite/SqliteSwapsDb.scala similarity index 90% rename from eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteSwapsDb.scala rename to plugins/peerswap/src/main/scala/fr/acinq/eclair/plugins/peerswap/db/sqlite/SqliteSwapsDb.scala index 56f13623d7..5bf480d0ac 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteSwapsDb.scala +++ b/plugins/peerswap/src/main/scala/fr/acinq/eclair/plugins/peerswap/db/sqlite/SqliteSwapsDb.scala @@ -14,14 +14,14 @@ * limitations under the License. */ -package fr.acinq.eclair.db.sqlite +package fr.acinq.eclair.plugins.peerswap.db.sqlite import fr.acinq.eclair.db.Monitoring.Metrics.withMetrics import fr.acinq.eclair.db.Monitoring.Tags.DbBackends -import fr.acinq.eclair.db.SwapsDb -import fr.acinq.eclair.db.SwapsDb.{getSwapData, setSwapData} -import fr.acinq.eclair.swap.SwapData -import fr.acinq.eclair.swap.SwapEvents.SwapEvent +import fr.acinq.eclair.plugins.peerswap.SwapData +import fr.acinq.eclair.plugins.peerswap.SwapEvents.SwapEvent +import fr.acinq.eclair.plugins.peerswap.db.SwapsDb +import fr.acinq.eclair.plugins.peerswap.db.SwapsDb.{getSwapData, setSwapData} import grizzled.slf4j.Logging import java.sql.Connection @@ -33,7 +33,7 @@ object SqliteSwapsDb { class SqliteSwapsDb (val sqlite: Connection) extends SwapsDb with Logging { - import SqliteUtils._ + import fr.acinq.eclair.db.sqlite.SqliteUtils._ import ExtendedResultSet._ import SqliteSwapsDb._ diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/json/PeerSwapJsonSerializers.scala b/plugins/peerswap/src/main/scala/fr/acinq/eclair/plugins/peerswap/json/PeerSwapJsonSerializers.scala similarity index 95% rename from eclair-core/src/main/scala/fr/acinq/eclair/json/PeerSwapJsonSerializers.scala rename to plugins/peerswap/src/main/scala/fr/acinq/eclair/plugins/peerswap/json/PeerSwapJsonSerializers.scala index 9c446963c6..6f473d1d40 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/json/PeerSwapJsonSerializers.scala +++ b/plugins/peerswap/src/main/scala/fr/acinq/eclair/plugins/peerswap/json/PeerSwapJsonSerializers.scala @@ -14,9 +14,10 @@ * limitations under the License. */ -package fr.acinq.eclair.json +package fr.acinq.eclair.plugins.peerswap.json -import fr.acinq.eclair.wire.protocol._ +import fr.acinq.eclair.json.MinimalSerializer +import fr.acinq.eclair.plugins.peerswap.wire.protocol._ import org.json4s.JsonAST._ import org.json4s.jackson.Serialization import org.json4s.{Formats, JField, JObject, JString, jackson} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/swap/SwapTransactions.scala b/plugins/peerswap/src/main/scala/fr/acinq/eclair/plugins/peerswap/transactions/SwapTransactions.scala similarity index 90% rename from eclair-core/src/main/scala/fr/acinq/eclair/swap/SwapTransactions.scala rename to plugins/peerswap/src/main/scala/fr/acinq/eclair/plugins/peerswap/transactions/SwapTransactions.scala index 49e1d401e7..a0a399dba0 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/swap/SwapTransactions.scala +++ b/plugins/peerswap/src/main/scala/fr/acinq/eclair/plugins/peerswap/transactions/SwapTransactions.scala @@ -14,7 +14,7 @@ * limitations under the License. */ -package fr.acinq.eclair.swap +package fr.acinq.eclair.plugins.peerswap.transactions import fr.acinq.bitcoin.SigHash.SIGHASH_ALL import fr.acinq.bitcoin.SigVersion.SIGVERSION_WITNESS_V0 @@ -22,16 +22,18 @@ import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} import fr.acinq.bitcoin.scalacompat.Script._ import fr.acinq.bitcoin.scalacompat.{TxOut, _} import fr.acinq.eclair.blockchain.fee.FeeratePerKw -import fr.acinq.eclair.swap.SwapScripts._ +import fr.acinq.eclair.plugins.peerswap.SwapScripts._ import fr.acinq.eclair.transactions.Scripts.der -import fr.acinq.eclair.transactions.Transactions.{InputInfo, weight2fee} +import fr.acinq.eclair.transactions.Transactions.{InputInfo, TransactionWithInputInfo, weight2fee} import scodec.bits.ByteVector -/** - * Created by remyers on 06/05/2022. - */ object SwapTransactions { + // TODO: find alternative to unsealing TransactionWithInputInfo + case class SwapClaimByInvoiceTx(override val input: InputInfo, override val tx: Transaction) extends TransactionWithInputInfo { override def desc: String = "swap-claimbyinvoice-tx" } + case class SwapClaimByCoopTx(override val input: InputInfo, override val tx: Transaction) extends TransactionWithInputInfo { override def desc: String = "swap-claimbycoop-tx" } + case class SwapClaimByCsvTx(override val input: InputInfo, override val tx: Transaction) extends TransactionWithInputInfo { override def desc: String = "swap-claimbycsv-tx" } + /** * This default sig takes 72B when encoded in DER (incl. 1B for the trailing sig hash), it is used for fee estimation * It is 72 bytes because our signatures are normalized (low-s) and will take up 72 bytes at most in DER format diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/PeerSwapMessageCodecs.scala b/plugins/peerswap/src/main/scala/fr/acinq/eclair/plugins/peerswap/wire/protocol/PeerSwapMessageCodecs.scala similarity index 84% rename from eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/PeerSwapMessageCodecs.scala rename to plugins/peerswap/src/main/scala/fr/acinq/eclair/plugins/peerswap/wire/protocol/PeerSwapMessageCodecs.scala index f99c0770f9..057ce7578e 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/PeerSwapMessageCodecs.scala +++ b/plugins/peerswap/src/main/scala/fr/acinq/eclair/plugins/peerswap/wire/protocol/PeerSwapMessageCodecs.scala @@ -14,10 +14,10 @@ * limitations under the License. */ -package fr.acinq.eclair.wire.protocol +package fr.acinq.eclair.plugins.peerswap.wire.protocol import fr.acinq.eclair.KamonExt -import fr.acinq.eclair.json.PeerSwapJsonSerializers.formats +import fr.acinq.eclair.plugins.peerswap.json.PeerSwapJsonSerializers.formats import fr.acinq.eclair.wire.Monitoring.{Metrics, Tags} import fr.acinq.eclair.wire.protocol.CommonCodecs._ import org.json4s._ @@ -27,9 +27,6 @@ import scodec.bits.BitVector import scodec.codecs._ import scodec.{Attempt, Codec} -/** - * Created by remyers on 29/03/2022. - */ object PeerSwapMessageCodecs { val swapInRequestCodec: Codec[SwapInRequest] = limitedSizeBytes(65533, utf8) @@ -65,7 +62,7 @@ object PeerSwapMessageCodecs { ("message" | varsizebinarydata) ).as[UnknownPeerSwapMessage] - val peerSwapMessageCodec: DiscriminatorCodec[PeerSwapMessage, Int] = discriminated[PeerSwapMessage].by(uint16) + val peerSwapMessageCodec: DiscriminatorCodec[HasSwapId, Int] = discriminated[HasSwapId].by(uint16) .typecase(42069, swapInRequestCodec) .typecase(42071, swapOutRequestCodec) .typecase(42073, swapInAgreementCodec) @@ -74,10 +71,10 @@ object PeerSwapMessageCodecs { .typecase(42079, canceledCodec) .typecase(42081, coopCloseCodec) - val peerSwapMessageCodecWithFallback: Codec[PeerSwapMessage] = discriminatorWithDefault(peerSwapMessageCodec, unknownPeerSwapMessageCodec.upcast) + val peerSwapMessageCodecWithFallback: Codec[HasSwapId] = discriminatorWithDefault(peerSwapMessageCodec, unknownPeerSwapMessageCodec.upcast) - val meteredPeerSwapMessageCodec: Codec[PeerSwapMessage] = Codec[PeerSwapMessage]( - (msg: PeerSwapMessage) => KamonExt.time(Metrics.EncodeDuration.withTag(Tags.MessageType, msg.getClass.getSimpleName))(peerSwapMessageCodecWithFallback.encode(msg)), + val meteredPeerSwapMessageCodec: Codec[HasSwapId] = Codec[HasSwapId]( + (msg: HasSwapId) => KamonExt.time(Metrics.EncodeDuration.withTag(Tags.MessageType, msg.getClass.getSimpleName))(peerSwapMessageCodecWithFallback.encode(msg)), (bits: BitVector) => { // this is a bit more involved, because we don't know beforehand what the type of the message will be val begin = System.nanoTime() diff --git a/plugins/peerswap/src/main/scala/fr/acinq/eclair/plugins/peerswap/wire/protocol/PeerSwapMessageTypes.scala b/plugins/peerswap/src/main/scala/fr/acinq/eclair/plugins/peerswap/wire/protocol/PeerSwapMessageTypes.scala new file mode 100644 index 0000000000..b55588150c --- /dev/null +++ b/plugins/peerswap/src/main/scala/fr/acinq/eclair/plugins/peerswap/wire/protocol/PeerSwapMessageTypes.scala @@ -0,0 +1,67 @@ +/* + * 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. + */ + +package fr.acinq.eclair.plugins.peerswap.wire.protocol + +import fr.acinq.eclair.plugins.peerswap.json.PeerSwapJsonSerializers +import org.json4s.jackson.Serialization +import scodec.bits.ByteVector + +sealed trait HasSwapId extends Serializable { def swapId: String } + +sealed abstract class JSonBlobMessage() extends HasSwapId { + def json: String = { + Serialization.write(this)(PeerSwapJsonSerializers.formats) + } +} + +sealed trait HasSwapVersion { def protocolVersion: Long} + +sealed trait SwapRequest extends JSonBlobMessage with HasSwapId with HasSwapVersion { + def asset: String + def network: String + def scid: String + def amount: Long + def pubkey: String +} + +case class SwapInRequest(protocolVersion: Long, swapId: String, asset: String, network: String, scid: String, amount: Long, pubkey: String) extends SwapRequest + +case class SwapOutRequest(protocolVersion: Long, swapId: String, asset: String, network: String, scid: String, amount: Long, pubkey: String) extends SwapRequest + +sealed trait SwapAgreement extends JSonBlobMessage with HasSwapId with HasSwapVersion { + def pubkey: String + def premium: Long + def payreq: String +} + +case class SwapInAgreement(protocolVersion: Long, swapId: String, pubkey: String, premium: Long) extends SwapAgreement { + override def payreq: String = "" +} + +case class SwapOutAgreement(protocolVersion: Long, swapId: String, pubkey: String, payreq: String) extends SwapAgreement { + override def premium: Long = 0 +} + +case class OpeningTxBroadcasted(swapId: String, payreq: String, txId: String, scriptOut: Long, blindingKey: String) extends JSonBlobMessage with HasSwapId + +case class CancelSwap(swapId: String, message: String) extends JSonBlobMessage with HasSwapId + +case class CoopClose(swapId: String, message: String, privkey: String) extends JSonBlobMessage with HasSwapId + +case class UnknownPeerSwapMessage(tag: Int, data: ByteVector) extends HasSwapId { + def swapId: String = "unknown" +} diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/swap/PeerSwapSpec.scala b/plugins/peerswap/src/test/scala/fr/acinq/eclair/plugins/peerswap/PeerSwapSpec.scala similarity index 51% rename from eclair-core/src/test/scala/fr/acinq/eclair/swap/PeerSwapSpec.scala rename to plugins/peerswap/src/test/scala/fr/acinq/eclair/plugins/peerswap/PeerSwapSpec.scala index 7460f7c58b..3c0502f850 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/swap/PeerSwapSpec.scala +++ b/plugins/peerswap/src/test/scala/fr/acinq/eclair/plugins/peerswap/PeerSwapSpec.scala @@ -14,20 +14,22 @@ * limitations under the License. */ -package fr.acinq.eclair.swap +package fr.acinq.eclair.plugins.peerswap -import fr.acinq.bitcoin.scalacompat.Crypto +import akka.actor.testkit.typed.scaladsl.ScalaTestWithActorTestKit +import com.typesafe.config.{Config, ConfigFactory} import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} -import fr.acinq.eclair.ShortChannelId +import fr.acinq.bitcoin.scalacompat.{Block, Crypto} +import fr.acinq.eclair.crypto.keymanager.{LocalChannelKeyManager, LocalNodeKeyManager} +import fr.acinq.eclair.{NodeParams, ShortChannelId, TestDatabases, TestFeeEstimator, randomBytes32} import org.scalatest.TryValues.convertTryToSuccessOrFailure -import org.scalatest.funsuite.AnyFunSuite +import org.scalatest.funsuite.AnyFunSuiteLike import scodec.bits._ -/** - * Created by remyers on 04/04/2022. - */ +import java.util.UUID +import java.util.concurrent.atomic.AtomicLong -class PeerSwapSpec extends AnyFunSuite { +class PeerSwapSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("application")) with AnyFunSuiteLike { val protocolVersion = 2 val swapId = hex"dd650741ee45fbad5df209bfb5aea9537e2e6d946cc7ece3b4492bbae0732634" val asset = "" @@ -44,4 +46,16 @@ class PeerSwapSpec extends AnyFunSuite { val privkey: PrivateKey = dummyKey(1) def dummyKey(fill: Byte): Crypto.PrivateKey = PrivateKey(ByteVector.fill(32)(fill)) + + val defaultConf: Config = ConfigFactory.load("reference.conf").getConfig("eclair") + + def makeNodeParamsWithDefaults(conf: Config): NodeParams = { + val blockCount = new AtomicLong(0) + val nodeKeyManager = new LocalNodeKeyManager(randomBytes32(), chainHash = Block.TestnetGenesisBlock.hash) + val channelKeyManager = new LocalChannelKeyManager(randomBytes32(), chainHash = Block.TestnetGenesisBlock.hash) + val feeEstimator = new TestFeeEstimator() + val db = TestDatabases.inMemoryDb() + NodeParams.makeNodeParams(conf, UUID.fromString("01234567-0123-4567-89ab-0123456789ab"), nodeKeyManager, channelKeyManager, None, db, blockCount, feeEstimator) + } + } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/swap/SwapInReceiverSpec.scala b/plugins/peerswap/src/test/scala/fr/acinq/eclair/plugins/peerswap/SwapInReceiverSpec.scala similarity index 89% rename from eclair-core/src/test/scala/fr/acinq/eclair/swap/SwapInReceiverSpec.scala rename to plugins/peerswap/src/test/scala/fr/acinq/eclair/plugins/peerswap/SwapInReceiverSpec.scala index d605b031fc..13977739a8 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/swap/SwapInReceiverSpec.scala +++ b/plugins/peerswap/src/test/scala/fr/acinq/eclair/plugins/peerswap/SwapInReceiverSpec.scala @@ -14,7 +14,7 @@ * limitations under the License. */ -package fr.acinq.eclair.swap +package fr.acinq.eclair.plugins.peerswap import akka.actor.testkit.typed.scaladsl.{ScalaTestWithActorTestKit, TestProbe} import akka.actor.typed.ActorRef @@ -33,17 +33,21 @@ import fr.acinq.eclair.channel.DATA_NORMAL import fr.acinq.eclair.channel.Register.ForwardShortId import fr.acinq.eclair.payment.send.PaymentInitiator.SendPaymentToNode import fr.acinq.eclair.payment.{Bolt11Invoice, PaymentSent} -import fr.acinq.eclair.swap.SwapCommands._ -import fr.acinq.eclair.swap.SwapEvents.{ClaimByInvoiceConfirmed, SwapEvent, TransactionPublished} -import fr.acinq.eclair.swap.SwapResponses.{Status, SwapStatus} -import fr.acinq.eclair.swap.SwapTransactions.{claimByInvoiceTxWeight, makeSwapClaimByInvoiceTx, makeSwapOpeningTxOut} +import fr.acinq.eclair.plugins.peerswap.SwapCommands._ +import fr.acinq.eclair.plugins.peerswap.SwapEvents.{ClaimByInvoiceConfirmed, SwapEvent, TransactionPublished} +import fr.acinq.eclair.plugins.peerswap.SwapResponses.{Status, SwapStatus} +import fr.acinq.eclair.plugins.peerswap.db.sqlite.SqliteSwapsDb +import fr.acinq.eclair.plugins.peerswap.transactions.SwapTransactions.{claimByInvoiceTxWeight, makeSwapClaimByInvoiceTx, makeSwapOpeningTxOut} +import fr.acinq.eclair.plugins.peerswap.wire.protocol.PeerSwapMessageCodecs.swapInAgreementCodec +import fr.acinq.eclair.plugins.peerswap.wire.protocol.{OpeningTxBroadcasted, SwapInAgreement, SwapInRequest} import fr.acinq.eclair.wire.internal.channel.ChannelCodecsSpec -import fr.acinq.eclair.wire.protocol.{OpeningTxBroadcasted, SwapInAgreement, SwapInRequest} +import fr.acinq.eclair.wire.protocol.UnknownMessage import fr.acinq.eclair.{BlockHeight, CltvExpiryDelta, NodeParams, ShortChannelId, TestConstants, ToMilliSatoshiConversion, randomBytes32} import grizzled.slf4j.Logging import org.scalatest.funsuite.FixtureAnyFunSuiteLike import org.scalatest.{BeforeAndAfterAll, Outcome} +import java.sql.DriverManager import java.util.UUID import scala.concurrent.duration._ @@ -52,13 +56,14 @@ case class SwapInReceiverSpec() extends ScalaTestWithActorTestKit(ConfigFactory. override implicit val timeout: Timeout = Timeout(30 seconds) val protocolVersion = 2 val noAsset = "" - val network: String = NodeParams.chainFromHash(TestConstants.Bob.nodeParams.chainHash) + val network: String = NodeParams.chainFromHash(TestConstants.Alice.nodeParams.chainHash) val amount: Satoshi = 1000 sat val swapId: String = ByteVector32.Zeroes.toHex val channelData: DATA_NORMAL = ChannelCodecsSpec.normal val shortChannelId: ShortChannelId = channelData.shortIds.real.toOption.get val channelId: ByteVector32 = channelData.channelId - val keyManager: SwapKeyManager = TestConstants.Bob.nodeParams.swapKeyManager + val keyManager: SwapKeyManager = new LocalSwapKeyManager(TestConstants.Alice.seed, TestConstants.Alice.nodeParams.chainHash) + val db = new SqliteSwapsDb(DriverManager.getConnection("jdbc:sqlite::memory:")) val makerPrivkey: PrivateKey = PrivateKey(randomBytes32()) val takerPrivkey: PrivateKey = keyManager.openingPrivateKey(SwapKeyManager.keyPath(swapId)).privateKey val makerNodeId: PublicKey = PrivateKey(randomBytes32()).publicKey @@ -72,6 +77,7 @@ case class SwapInReceiverSpec() extends ScalaTestWithActorTestKit(ConfigFactory. val scriptOut: Long = 0 val blindingKey: String = "" val request: SwapInRequest = SwapInRequest(protocolVersion, swapId, noAsset, network, shortChannelId.toString, amount.toLong, makerPubkey.toHex) + def expectUnknownMessage(register: TestProbe[Any]): UnknownMessage = register.expectMessageType[ForwardShortId[UnknownMessage]].message override def withFixture(test: OneArgTest): Outcome = { val watcher = testKit.createTestProbe[ZmqWatcher.Command]() @@ -91,7 +97,7 @@ case class SwapInReceiverSpec() extends ScalaTestWithActorTestKit(ConfigFactory. // subscribe to notification events from SwapInReceiver when a payment is successfully received or claimed via coop or csv testKit.system.eventStream ! Subscribe[SwapEvent](swapEvents.ref) - val swapInReceiver = testKit.spawn(Behaviors.monitor(monitor.ref, SwapTaker(TestConstants.Bob.nodeParams, paymentInitiator.ref.toClassic, watcher.ref, register.ref.toClassic, wallet)), "swap-in-receiver") + val swapInReceiver = testKit.spawn(Behaviors.monitor(monitor.ref, SwapTaker(TestConstants.Bob.nodeParams, paymentInitiator.ref.toClassic, watcher.ref, register.ref.toClassic, wallet, keyManager, db)), "swap-in-receiver") withFixture(test.toNoArgTest(FixtureParam(swapInReceiver, userCli, monitor, register, relayer, router, paymentInitiator, switchboard, paymentHandler, sender, TestConstants.Bob.nodeParams, watcher, wallet, swapEvents))) } @@ -155,7 +161,7 @@ case class SwapInReceiverSpec() extends ScalaTestWithActorTestKit(ConfigFactory. monitor.expectMessage(StartSwapInReceiver(request)) // SwapInReceiver:SwapInAgreement -> SwapInSender - val agreement = register.expectMessageType[ForwardShortId[SwapInAgreement]].message + val agreement = swapInAgreementCodec.decode(expectUnknownMessage(register).data.drop(2).toBitVector).require.value // Maker:OpeningTxBroadcasted -> Taker val openingTxBroadcasted = OpeningTxBroadcasted(swapId, invoice.toString, txid, scriptOut, blindingKey) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/swap/SwapInSenderSpec.scala b/plugins/peerswap/src/test/scala/fr/acinq/eclair/plugins/peerswap/SwapInSenderSpec.scala similarity index 85% rename from eclair-core/src/test/scala/fr/acinq/eclair/swap/SwapInSenderSpec.scala rename to plugins/peerswap/src/test/scala/fr/acinq/eclair/plugins/peerswap/SwapInSenderSpec.scala index beaa1f5f20..7759dfb554 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/swap/SwapInSenderSpec.scala +++ b/plugins/peerswap/src/test/scala/fr/acinq/eclair/plugins/peerswap/SwapInSenderSpec.scala @@ -14,7 +14,7 @@ * limitations under the License. */ -package fr.acinq.eclair.swap +package fr.acinq.eclair.plugins.peerswap import akka.actor.testkit.typed.scaladsl.{ScalaTestWithActorTestKit, TestProbe} import akka.actor.typed.ActorRef @@ -24,7 +24,7 @@ import akka.actor.typed.scaladsl.adapter._ import akka.util.Timeout import com.typesafe.config.ConfigFactory import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} -import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, Satoshi, SatoshiLong, Transaction} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, Satoshi, SatoshiLong, Transaction} import fr.acinq.eclair.blockchain.OnChainWallet.OnChainBalance import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._ @@ -32,16 +32,20 @@ import fr.acinq.eclair.blockchain.{DummyOnChainWallet, OnChainWallet} import fr.acinq.eclair.channel.DATA_NORMAL import fr.acinq.eclair.channel.Register.ForwardShortId import fr.acinq.eclair.payment.{Bolt11Invoice, PaymentReceived} -import fr.acinq.eclair.swap.SwapCommands._ -import fr.acinq.eclair.swap.SwapEvents._ -import fr.acinq.eclair.swap.SwapResponses.{Status, SwapStatus} +import fr.acinq.eclair.plugins.peerswap.SwapCommands._ +import fr.acinq.eclair.plugins.peerswap.SwapEvents._ +import fr.acinq.eclair.plugins.peerswap.SwapResponses.{Status, SwapStatus} +import fr.acinq.eclair.plugins.peerswap.db.sqlite.SqliteSwapsDb +import fr.acinq.eclair.plugins.peerswap.wire.protocol.PeerSwapMessageCodecs.{openingTxBroadcastedCodec, swapInRequestCodec} +import fr.acinq.eclair.plugins.peerswap.wire.protocol.{CoopClose, OpeningTxBroadcasted, SwapInAgreement, SwapInRequest} import fr.acinq.eclair.wire.internal.channel.ChannelCodecsSpec -import fr.acinq.eclair.wire.protocol.{CoopClose, OpeningTxBroadcasted, SwapInAgreement, SwapInRequest} -import fr.acinq.eclair.{BlockHeight, CltvExpiryDelta, ShortChannelId, TestConstants, TimestampMilli, ToMilliSatoshiConversion, randomBytes32} +import fr.acinq.eclair.wire.protocol.UnknownMessage +import fr.acinq.eclair.{BlockHeight, CltvExpiryDelta, NodeParams, ShortChannelId, TestConstants, TimestampMilli, ToMilliSatoshiConversion, randomBytes32} import grizzled.slf4j.Logging import org.scalatest.funsuite.FixtureAnyFunSuiteLike import org.scalatest.{BeforeAndAfterAll, Outcome} +import java.sql.DriverManager import scala.concurrent.duration._ import scala.concurrent.{ExecutionContext, Future} @@ -50,13 +54,14 @@ case class SwapInSenderSpec() extends ScalaTestWithActorTestKit(ConfigFactory.lo override implicit val timeout: Timeout = Timeout(30 seconds) val protocolVersion = 2 val noAsset = "" - val network: String = Block.RegtestGenesisBlock.hash.toString() + val network: String = NodeParams.chainFromHash(TestConstants.Alice.nodeParams.chainHash) val amount: Satoshi = 1000 sat val swapId: String = ByteVector32.Zeroes.toHex val channelData: DATA_NORMAL = ChannelCodecsSpec.normal val shortChannelId: ShortChannelId = channelData.shortIds.real.toOption.get val channelId: ByteVector32 = channelData.channelId - val keyManager: SwapKeyManager = TestConstants.Alice.nodeParams.swapKeyManager + val keyManager: SwapKeyManager = new LocalSwapKeyManager(TestConstants.Alice.seed, TestConstants.Alice.nodeParams.chainHash) + val db = new SqliteSwapsDb(DriverManager.getConnection("jdbc:sqlite::memory:")) val makerPrivkey: PrivateKey = keyManager.openingPrivateKey(SwapKeyManager.keyPath(swapId)).privateKey val takerPrivkey: PrivateKey = PrivateKey(randomBytes32()) val makerNodeId: PublicKey = PrivateKey(randomBytes32()).publicKey @@ -68,6 +73,7 @@ case class SwapInSenderSpec() extends ScalaTestWithActorTestKit(ConfigFactory.lo val blindingKey: String = "" val request: SwapInRequest = SwapInRequest(protocolVersion, swapId, noAsset, network, shortChannelId.toString, amount.toLong, makerPubkey.toHex) val agreement: SwapInAgreement = SwapInAgreement(protocolVersion, swapId, makerPubkey.toHex, premium) + def expectUnknownMessage(register: TestProbe[Any]): UnknownMessage = register.expectMessageType[ForwardShortId[UnknownMessage]].message override def withFixture(test: OneArgTest): Outcome = { val watcher = testKit.createTestProbe[ZmqWatcher.Command]() @@ -88,7 +94,7 @@ case class SwapInSenderSpec() extends ScalaTestWithActorTestKit(ConfigFactory.lo // subscribe to notification events from SwapInSender when a payment is successfully received or claimed via coop or csv testKit.system.eventStream ! Subscribe[SwapEvent](swapEvents.ref) - val swapInSender = testKit.spawn(Behaviors.monitor(monitor.ref, SwapMaker(TestConstants.Alice.nodeParams, watcher.ref, register.ref.toClassic, wallet)), "swap-in-sender") + val swapInSender = testKit.spawn(Behaviors.monitor(monitor.ref, SwapMaker(TestConstants.Alice.nodeParams, watcher.ref, register.ref.toClassic, wallet, keyManager, db)), "swap-in-sender") withFixture(test.toNoArgTest(FixtureParam(swapInSender, userCli, monitor, register, relayer, router, paymentInitiator, switchboard, paymentHandler, sender, watcher, wallet, swapEvents))) } @@ -135,17 +141,17 @@ case class SwapInSenderSpec() extends ScalaTestWithActorTestKit(ConfigFactory.lo swapInSender ! StartSwapInSender(amount, swapId, shortChannelId) // SwapInSender: SwapInRequest -> SwapInSender - val swapInRequest = register.expectMessageType[ForwardShortId[SwapInRequest]] + val swapInRequest = swapInRequestCodec.decode(expectUnknownMessage(register).data.drop(2).toBitVector).require.value // SwapInReceiver: SwapInAgreement -> SwapInSender - swapInSender ! SwapMessageReceived(SwapInAgreement(swapInRequest.message.protocolVersion, swapInRequest.message.swapId, takerPubkey.toString(), premium)) + swapInSender ! SwapMessageReceived(SwapInAgreement(swapInRequest.protocolVersion, swapInRequest.swapId, takerPubkey.toString(), premium)) // SwapInSender publishes opening tx on-chain val openingTx = swapEvents.expectMessageType[TransactionPublished].tx // SwapInSender:OpeningTxBroadcasted -> SwapInReceiver - val openingTxBroadcasted = register.expectMessageType[ForwardShortId[OpeningTxBroadcasted]] - val invoice = Bolt11Invoice.fromString(openingTxBroadcasted.message.payreq).get + val openingTxBroadcasted = openingTxBroadcastedCodec.decode(expectUnknownMessage(register).data.drop(2).toBitVector).require.value + val invoice = Bolt11Invoice.fromString(openingTxBroadcasted.payreq).get // wait for SwapInSender to subscribe to PaymentEventReceived messages swapEvents.expectNoMessage() @@ -176,7 +182,7 @@ case class SwapInSenderSpec() extends ScalaTestWithActorTestKit(ConfigFactory.lo swapInSender ! RestoreSwap(swapData) // resend OpeningTxBroadcasted when swap restored - register.expectMessageType[ForwardShortId[OpeningTxBroadcasted]] + openingTxBroadcastedCodec.decode(expectUnknownMessage(register).data.drop(2).toBitVector).require.value // wait for SwapInSender to subscribe to PaymentEventReceived messages swapEvents.expectNoMessage() @@ -213,7 +219,7 @@ case class SwapInSenderSpec() extends ScalaTestWithActorTestKit(ConfigFactory.lo swapInSender ! RestoreSwap(swapData) // resend OpeningTxBroadcasted when swap restored - register.expectMessageType[ForwardShortId[OpeningTxBroadcasted]] + openingTxBroadcastedCodec.decode(expectUnknownMessage(register).data.drop(2).toBitVector).require.value // wait to subscribe to PaymentEventReceived messages swapEvents.expectNoMessage() diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/swap/SwapIntegrationFixture.scala b/plugins/peerswap/src/test/scala/fr/acinq/eclair/plugins/peerswap/SwapIntegrationFixture.scala similarity index 60% rename from eclair-core/src/test/scala/fr/acinq/eclair/swap/SwapIntegrationFixture.scala rename to plugins/peerswap/src/test/scala/fr/acinq/eclair/plugins/peerswap/SwapIntegrationFixture.scala index 29967139bf..7929e6c67c 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/swap/SwapIntegrationFixture.scala +++ b/plugins/peerswap/src/test/scala/fr/acinq/eclair/plugins/peerswap/SwapIntegrationFixture.scala @@ -1,6 +1,9 @@ -package fr.acinq.eclair.swap +package fr.acinq.eclair.plugins.peerswap import akka.actor.ActorSystem +import akka.actor.typed.scaladsl.Behaviors +import akka.actor.typed.scaladsl.adapter.{ClassicActorRefOps, ClassicActorSystemOps} +import akka.actor.typed.{ActorRef, SupervisorStrategy} import akka.testkit.{TestKit, TestProbe} import fr.acinq.bitcoin.scalacompat.{ByteVector32, SatoshiLong} import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.WatchExternalChannelSpent @@ -8,13 +11,16 @@ import fr.acinq.eclair.channel.{DATA_NORMAL, RealScidStatus} import fr.acinq.eclair.integration.basic.fixtures.MinimalNodeFixture import fr.acinq.eclair.integration.basic.fixtures.MinimalNodeFixture.{confirmChannel, confirmChannelDeep, connect, getChannelData, getRouterData, openChannel} import fr.acinq.eclair.payment.PaymentEvent -import fr.acinq.eclair.swap.SwapEvents.SwapEvent -import fr.acinq.eclair.{BlockHeight, NodeParams} +import fr.acinq.eclair.plugins.peerswap.SwapEvents.SwapEvent +import fr.acinq.eclair.plugins.peerswap.db.sqlite.SqliteSwapsDb +import fr.acinq.eclair.{BlockHeight, NodeParams, TestConstants} import org.scalatest.concurrent.Eventually.eventually -case class SwapProbes(cli: TestProbe, paymentEvents: TestProbe, swapEvents: TestProbe) +import java.sql.DriverManager -case class SwapIntegrationFixture(system: ActorSystem, alice: MinimalNodeFixture, bob: MinimalNodeFixture, aliceSwap: SwapProbes, bobSwap: SwapProbes, channelId: ByteVector32) { +case class SwapActors(cli: TestProbe, paymentEvents: TestProbe, swapEvents: TestProbe, swapRegister: ActorRef[SwapRegister.Command]) + +case class SwapIntegrationFixture(system: ActorSystem, alice: MinimalNodeFixture, bob: MinimalNodeFixture, aliceSwap: SwapActors, bobSwap: SwapActors, channelId: ByteVector32) { implicit val implicitSystem: ActorSystem = system def cleanup(): Unit = { @@ -25,12 +31,17 @@ case class SwapIntegrationFixture(system: ActorSystem, alice: MinimalNodeFixture } object SwapIntegrationFixture { + def swapRegister(node: MinimalNodeFixture): ActorRef[SwapRegister.Command] = { + val keyManager: SwapKeyManager = new LocalSwapKeyManager(TestConstants.Alice.seed, node.nodeParams.chainHash) + val db = new SqliteSwapsDb(DriverManager.getConnection("jdbc:sqlite::memory:")) + node.system.spawn(Behaviors.supervise(SwapRegister(node.nodeParams, node.paymentInitiator, node.watcher.ref.toTyped, node.register, node.wallet, keyManager, db, Set())).onFailure(SupervisorStrategy.stop), s"swap-register-${node.nodeParams.alias}") + } def apply(aliceParams: NodeParams, bobParams: NodeParams): SwapIntegrationFixture = { val system = ActorSystem("system-test") val alice = MinimalNodeFixture(aliceParams) val bob = MinimalNodeFixture(bobParams) - val aliceSwap = SwapProbes(TestProbe()(alice.system), TestProbe()(alice.system), TestProbe()(alice.system)) - val bobSwap = SwapProbes(TestProbe()(bob.system), TestProbe()(bob.system), TestProbe()(bob.system)) + val aliceSwap = SwapActors(TestProbe()(alice.system), TestProbe()(alice.system), TestProbe()(alice.system), swapRegister(alice)) + val bobSwap = SwapActors(TestProbe()(bob.system), TestProbe()(bob.system), TestProbe()(bob.system), swapRegister(bob)) alice.system.eventStream.subscribe(aliceSwap.paymentEvents.ref, classOf[PaymentEvent]) alice.system.eventStream.subscribe(aliceSwap.swapEvents.ref, classOf[SwapEvent]) bob.system.eventStream.subscribe(bobSwap.paymentEvents.ref, classOf[PaymentEvent]) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/swap/SwapIntegrationSpec.scala b/plugins/peerswap/src/test/scala/fr/acinq/eclair/plugins/peerswap/SwapIntegrationSpec.scala similarity index 82% rename from eclair-core/src/test/scala/fr/acinq/eclair/swap/SwapIntegrationSpec.scala rename to plugins/peerswap/src/test/scala/fr/acinq/eclair/plugins/peerswap/SwapIntegrationSpec.scala index 4c461a7bae..e4e93c16bf 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/swap/SwapIntegrationSpec.scala +++ b/plugins/peerswap/src/test/scala/fr/acinq/eclair/plugins/peerswap/SwapIntegrationSpec.scala @@ -1,4 +1,4 @@ -package fr.acinq.eclair.swap +package fr.acinq.eclair.plugins.peerswap import akka.actor.typed.scaladsl.adapter._ import akka.actor.{ActorSystem, Kill} @@ -10,13 +10,15 @@ import fr.acinq.eclair.channel.{DATA_NORMAL, RealScidStatus} import fr.acinq.eclair.integration.basic.fixtures.MinimalNodeFixture import fr.acinq.eclair.integration.basic.fixtures.composite.TwoNodesFixture import fr.acinq.eclair.payment.{PaymentEvent, PaymentReceived, PaymentSent} -import fr.acinq.eclair.swap.SwapEvents._ -import fr.acinq.eclair.swap.SwapRegister.{CancelSwapRequested, ListPendingSwaps, SwapInRequested, SwapOutRequested} -import fr.acinq.eclair.swap.SwapResponses.{Status, SwapOpened} -import fr.acinq.eclair.swap.SwapScripts.claimByCsvDelta -import fr.acinq.eclair.swap.SwapTransactions.{claimByInvoiceTxWeight, openingTxWeight} +import fr.acinq.eclair.plugins.peerswap.SwapEvents._ +import fr.acinq.eclair.plugins.peerswap.SwapIntegrationFixture.swapRegister +import fr.acinq.eclair.plugins.peerswap.SwapRegister.{CancelSwapRequested, ListPendingSwaps, SwapInRequested, SwapOutRequested} +import fr.acinq.eclair.plugins.peerswap.SwapResponses.{Status, SwapOpened} +import fr.acinq.eclair.plugins.peerswap.SwapScripts.claimByCsvDelta +import fr.acinq.eclair.plugins.peerswap.transactions.SwapTransactions.{claimByInvoiceTxWeight, openingTxWeight} +import fr.acinq.eclair.plugins.peerswap.wire.protocol.SwapInAgreement import fr.acinq.eclair.testutils.FixtureSpec -import fr.acinq.eclair.{BlockHeight, ShortChannelId} +import fr.acinq.eclair.{BlockHeight, ShortChannelId, randomKey} import org.scalatest.TestData import org.scalatest.concurrent.{IntegrationPatience, PatienceConfiguration} import scodec.bits.HexStringSyntax @@ -39,8 +41,9 @@ class SwapIntegrationSpec extends FixtureSpec with IntegrationPatience { override def createFixture(testData: TestData): FixtureParam = { // seeds have been chosen so that node ids start with 02aaaa for alice, 02bbbb for bob, etc. val aliceParams = nodeParamsFor("alice", ByteVector32(hex"b4acd47335b25ab7b84b8c020997b12018592bb4631b868762154d77fa8b93a3")) + .copy(pluginParams = Seq(new PeerSwapPlugin().params)) val bobParams = nodeParamsFor("bob", ByteVector32(hex"7620226fec887b0b2ebe76492e5a3fd3eb0e47cd3773263f6a81b59a704dc492")) - .copy(invoiceExpiry = 2 seconds) + .copy(invoiceExpiry = 2 seconds, pluginParams = Seq(new PeerSwapPlugin().params)) TwoNodesFixture(aliceParams, bobParams) } @@ -48,9 +51,9 @@ class SwapIntegrationSpec extends FixtureSpec with IntegrationPatience { fixture.cleanup() } - def swapProbes(alice: MinimalNodeFixture, bob: MinimalNodeFixture)(implicit system: ActorSystem): (SwapProbes, SwapProbes) = { - val aliceSwap = SwapProbes(TestProbe()(alice.system), TestProbe()(alice.system), TestProbe()(alice.system)) - val bobSwap = SwapProbes(TestProbe()(bob.system), TestProbe()(bob.system), TestProbe()(bob.system)) + def swapActors(alice: MinimalNodeFixture, bob: MinimalNodeFixture)(implicit system: ActorSystem): (SwapActors, SwapActors) = { + val aliceSwap = SwapActors(TestProbe()(alice.system), TestProbe()(alice.system), TestProbe()(alice.system), swapRegister(alice)) + val bobSwap = SwapActors(TestProbe()(bob.system), TestProbe()(bob.system), TestProbe()(bob.system), swapRegister(bob)) alice.system.eventStream.subscribe(aliceSwap.paymentEvents.ref, classOf[PaymentEvent]) alice.system.eventStream.subscribe(aliceSwap.swapEvents.ref, classOf[SwapEvent]) bob.system.eventStream.subscribe(bobSwap.paymentEvents.ref, classOf[PaymentEvent]) @@ -79,7 +82,7 @@ class SwapIntegrationSpec extends FixtureSpec with IntegrationPatience { test("swap in - claim by invoice") { f => import f._ - val (aliceSwap, bobSwap) = swapProbes(alice, bob) + val (aliceSwap, bobSwap) = swapActors(alice, bob) val shortChannelId = connectNodes(alice, bob) // bob must have enough on-chain balance to send @@ -91,14 +94,14 @@ class SwapIntegrationSpec extends FixtureSpec with IntegrationPatience { bob.wallet.confirmedBalance = amount + premium // swap in sender (bob) requests a swap in with swap in receiver (alice) - bob.swapRegister ! SwapInRequested(bobSwap.cli.ref, amount, shortChannelId) + bobSwap.swapRegister ! SwapInRequested(bobSwap.cli.ref.toTyped, amount, shortChannelId) val swapId = bobSwap.cli.expectMsgType[SwapOpened].swapId // swap in sender (bob) confirms opening tx published val openingTx = bobSwap.swapEvents.expectMsgType[TransactionPublished].tx // bob has status of 1 pending swap - bob.swapRegister ! ListPendingSwaps(bobSwap.cli.ref) + bobSwap.swapRegister ! ListPendingSwaps(bobSwap.cli.ref.toTyped) val bobStatus = bobSwap.cli.expectMsgType[Iterable[Status]] assert(bobStatus.size == 1) assert(bobStatus.head.swapId === swapId) @@ -124,7 +127,7 @@ class SwapIntegrationSpec extends FixtureSpec with IntegrationPatience { test("swap in - claim by coop, receiver does not have sufficient channel balance") { f => import f._ - val (aliceSwap, bobSwap) = swapProbes(alice, bob) + val (aliceSwap, bobSwap) = swapActors(alice, bob) val shortChannelId = connectNodes(alice, bob) // swap more satoshis than alice has available in the channel to send to bob @@ -136,7 +139,7 @@ class SwapIntegrationSpec extends FixtureSpec with IntegrationPatience { bob.wallet.confirmedBalance = amount + premium // swap in sender (bob) requests a swap in with swap in receiver (alice) - bob.swapRegister ! SwapInRequested(bobSwap.cli.ref, amount, shortChannelId) + bobSwap.swapRegister ! SwapInRequested(bobSwap.cli.ref.toTyped, amount, shortChannelId) val swapId = bobSwap.cli.expectMsgType[SwapOpened].swapId // swap in sender (bob) confirms opening tx published @@ -144,13 +147,13 @@ class SwapIntegrationSpec extends FixtureSpec with IntegrationPatience { assert(openingTx.txOut.head.amount == amount + premium) // bob has status of 1 pending swap - bob.swapRegister ! ListPendingSwaps(bobSwap.cli.ref) + bobSwap.swapRegister ! ListPendingSwaps(bobSwap.cli.ref.toTyped) val bobStatus = bobSwap.cli.expectMsgType[Iterable[Status]] assert(bobStatus.size == 1) assert(bobStatus.head.swapId === swapId) // alice has status of 1 pending swap - alice.swapRegister ! ListPendingSwaps(aliceSwap.cli.ref) + aliceSwap.swapRegister ! ListPendingSwaps(aliceSwap.cli.ref.toTyped) val aliceStatus = aliceSwap.cli.expectMsgType[Iterable[Status]] assert(aliceStatus.size == 1) assert(aliceStatus.head.swapId == swapId) @@ -179,7 +182,7 @@ class SwapIntegrationSpec extends FixtureSpec with IntegrationPatience { test("swap in - claim by csv, receiver does not pay after opening tx confirmed") { f => import f._ - val (_, bobSwap) = swapProbes(alice, bob) + val (aliceSwap, bobSwap) = swapActors(alice, bob) val shortChannelId = connectNodes(alice, bob) // bob must have enough on-chain balance to send @@ -191,7 +194,7 @@ class SwapIntegrationSpec extends FixtureSpec with IntegrationPatience { bob.wallet.confirmedBalance = amount + premium // swap in sender (bob) requests a swap in with swap in receiver (alice) - bob.swapRegister ! SwapInRequested(bobSwap.cli.ref, amount, shortChannelId) + bobSwap.swapRegister ! SwapInRequested(bobSwap.cli.ref.toTyped, amount, shortChannelId) val swapId = bobSwap.cli.expectMsgType[SwapOpened].swapId // swap in sender (bob) confirms opening tx published @@ -199,10 +202,10 @@ class SwapIntegrationSpec extends FixtureSpec with IntegrationPatience { assert(openingTx.txOut.head.amount == amount + premium) // swap in receiver (alice) stops unexpectedly - alice.swapRegister ! Kill + aliceSwap.swapRegister.toClassic ! Kill // bob has status of 1 pending swap - bob.swapRegister ! ListPendingSwaps(bobSwap.cli.ref) + bobSwap.swapRegister ! ListPendingSwaps(bobSwap.cli.ref.toTyped) val bobStatus = bobSwap.cli.expectMsgType[Iterable[Status]] assert(bobStatus.size == 1) assert(bobStatus.head.swapId === swapId) @@ -221,7 +224,7 @@ class SwapIntegrationSpec extends FixtureSpec with IntegrationPatience { test("swap in - claim by coop, receiver cancels while waiting for opening tx to confirm") { f => import f._ - val (aliceSwap, bobSwap) = swapProbes(alice, bob) + val (aliceSwap, bobSwap) = swapActors(alice, bob) val shortChannelId = connectNodes(alice, bob) // bob must have enough on-chain balance to send @@ -233,14 +236,14 @@ class SwapIntegrationSpec extends FixtureSpec with IntegrationPatience { bob.wallet.confirmedBalance = amount + premium // swap in sender (bob) requests a swap in with swap in receiver (alice) - bob.swapRegister ! SwapInRequested(bobSwap.cli.ref, amount, shortChannelId) + bobSwap.swapRegister ! SwapInRequested(bobSwap.cli.ref.toTyped, amount, shortChannelId) val swapId = bobSwap.cli.expectMsgType[SwapOpened].swapId // swap in sender (bob) confirms opening tx is published, but NOT yet confirmed on-chain val openingTx = bobSwap.swapEvents.expectMsgType[TransactionPublished].tx // swap in receiver (alice) sends CoopClose before the opening tx has been confirmed on-chain - alice.swapRegister ! CancelSwapRequested(aliceSwap.cli.ref, swapId) + aliceSwap.swapRegister ! CancelSwapRequested(aliceSwap.cli.ref.toTyped, swapId) val claimByCoopEvent = aliceSwap.swapEvents.expectMsgType[ClaimByCoopOffered] assert(claimByCoopEvent.swapId == swapId) @@ -258,7 +261,7 @@ class SwapIntegrationSpec extends FixtureSpec with IntegrationPatience { test("swap out - claim by invoice") { f => import f._ - val (aliceSwap, bobSwap) = swapProbes(alice, bob) + val (aliceSwap, bobSwap) = swapActors(alice, bob) val shortChannelId = connectNodes(alice, bob) // bob must have enough on-chain balance to send @@ -270,7 +273,7 @@ class SwapIntegrationSpec extends FixtureSpec with IntegrationPatience { bob.wallet.confirmedBalance = amount + fee // swap out receiver (alice) requests a swap out with swap out sender (bob) - alice.swapRegister ! SwapOutRequested(aliceSwap.cli.ref, amount, shortChannelId) + aliceSwap.swapRegister ! SwapOutRequested(aliceSwap.cli.ref.toTyped, amount, shortChannelId) val swapId = aliceSwap.cli.expectMsgType[SwapOpened].swapId // swap out receiver (alice) sends a payment of `fee` to swap out sender (bob) @@ -282,7 +285,7 @@ class SwapIntegrationSpec extends FixtureSpec with IntegrationPatience { assert(openingTx.txOut.head.amount == amount) // bob has status of 1 pending swap - bob.swapRegister ! ListPendingSwaps(bobSwap.cli.ref) + bobSwap.swapRegister ! ListPendingSwaps(bobSwap.cli.ref.toTyped) val bobStatus = bobSwap.cli.expectMsgType[Iterable[Status]] assert(bobStatus.size == 1) assert(bobStatus.head.swapId === swapId) @@ -304,4 +307,20 @@ class SwapIntegrationSpec extends FixtureSpec with IntegrationPatience { assert(bobSwap.swapEvents.expectMsgType[ClaimByInvoicePaid].swapId == swapId) } + test("eclair forwards swap messages to the SwapRegister") { f => + + + val protocolVersion = 2 + val swapId = hex"dd650741ee45fbad5df209bfb5aea9537e2e6d946cc7ece3b4492bbae0732634" + val premium = 10 + val responderPubkey = randomKey().publicKey + + val swapInAgreement = SwapInAgreement(protocolVersion, swapId.toHex, responderPubkey.toString, premium) + + // TODO: add message to SwapRegister which forwards messages to channel peer + // alice.peer.send(peer, swapInAgreement) + //val messageReceived = alice.swapRegister.expectMsgType[MessageReceived] + //assert(messageReceived.message === swapInAgreement) + } + } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/swap/SwapOutReceiverSpec.scala b/plugins/peerswap/src/test/scala/fr/acinq/eclair/plugins/peerswap/SwapOutReceiverSpec.scala similarity index 81% rename from eclair-core/src/test/scala/fr/acinq/eclair/swap/SwapOutReceiverSpec.scala rename to plugins/peerswap/src/test/scala/fr/acinq/eclair/plugins/peerswap/SwapOutReceiverSpec.scala index 2cc57eb13f..e9b29e86aa 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/swap/SwapOutReceiverSpec.scala +++ b/plugins/peerswap/src/test/scala/fr/acinq/eclair/plugins/peerswap/SwapOutReceiverSpec.scala @@ -14,7 +14,7 @@ * limitations under the License. */ -package fr.acinq.eclair.swap +package fr.acinq.eclair.plugins.peerswap import akka.actor.testkit.typed.scaladsl.{ScalaTestWithActorTestKit, TestProbe} import akka.actor.typed.ActorRef @@ -31,17 +31,21 @@ import fr.acinq.eclair.blockchain.{DummyOnChainWallet, OnChainWallet} import fr.acinq.eclair.channel.DATA_NORMAL import fr.acinq.eclair.channel.Register.ForwardShortId import fr.acinq.eclair.payment.{Bolt11Invoice, PaymentReceived} -import fr.acinq.eclair.swap.SwapCommands._ -import fr.acinq.eclair.swap.SwapEvents.{ClaimByInvoicePaid, SwapEvent, TransactionPublished} -import fr.acinq.eclair.swap.SwapResponses.{Status, SwapStatus} -import fr.acinq.eclair.swap.SwapTransactions.openingTxWeight +import fr.acinq.eclair.plugins.peerswap.SwapCommands._ +import fr.acinq.eclair.plugins.peerswap.SwapEvents.{ClaimByInvoicePaid, SwapEvent, TransactionPublished} +import fr.acinq.eclair.plugins.peerswap.SwapResponses.{Status, SwapStatus} +import fr.acinq.eclair.plugins.peerswap.db.sqlite.SqliteSwapsDb +import fr.acinq.eclair.plugins.peerswap.transactions.SwapTransactions.openingTxWeight +import fr.acinq.eclair.plugins.peerswap.wire.protocol.PeerSwapMessageCodecs.{openingTxBroadcastedCodec, swapOutAgreementCodec} +import fr.acinq.eclair.plugins.peerswap.wire.protocol.SwapOutRequest import fr.acinq.eclair.wire.internal.channel.ChannelCodecsSpec -import fr.acinq.eclair.wire.protocol.{OpeningTxBroadcasted, SwapOutAgreement, SwapOutRequest} +import fr.acinq.eclair.wire.protocol.UnknownMessage import fr.acinq.eclair.{NodeParams, ShortChannelId, TestConstants, TimestampMilli, ToMilliSatoshiConversion, randomBytes32} import grizzled.slf4j.Logging import org.scalatest.funsuite.FixtureAnyFunSuiteLike import org.scalatest.{BeforeAndAfterAll, Outcome} +import java.sql.DriverManager import scala.concurrent.duration._ // with BitcoindService @@ -57,7 +61,7 @@ case class SwapOutReceiverSpec() extends ScalaTestWithActorTestKit(ConfigFactory val channelData: DATA_NORMAL = ChannelCodecsSpec.normal val shortChannelId: ShortChannelId = channelData.shortIds.real.toOption.get val channelId: ByteVector32 = channelData.channelId - val keyManager: SwapKeyManager = TestConstants.Alice.nodeParams.swapKeyManager + val keyManager: SwapKeyManager = new LocalSwapKeyManager(TestConstants.Alice.seed, TestConstants.Alice.nodeParams.chainHash) val makerPrivkey: PrivateKey = keyManager.openingPrivateKey(SwapKeyManager.keyPath(swapId)).privateKey val takerPrivkey: PrivateKey = PrivateKey(randomBytes32()) val makerNodeId: PublicKey = PrivateKey(randomBytes32()).publicKey @@ -69,6 +73,7 @@ case class SwapOutReceiverSpec() extends ScalaTestWithActorTestKit(ConfigFactory val scriptOut: Long = 0 val blindingKey: String = "" val request: SwapOutRequest = SwapOutRequest(protocolVersion, swapId, noAsset, network, shortChannelId.toString, amount.toLong, takerPubkey.toHex) + def expectUnknownMessage(register: TestProbe[Any]): UnknownMessage = register.expectMessageType[ForwardShortId[UnknownMessage]].message override def withFixture(test: OneArgTest): Outcome = { val watcher = testKit.createTestProbe[ZmqWatcher.Command]() @@ -84,18 +89,20 @@ case class SwapOutReceiverSpec() extends ScalaTestWithActorTestKit(ConfigFactory val sender = testKit.createTestProbe[Any]() val swapEvents = testKit.createTestProbe[SwapEvent]() val monitor = testKit.createTestProbe[SwapCommands.SwapCommand]() + val keyManager: SwapKeyManager = new LocalSwapKeyManager(TestConstants.Alice.seed, TestConstants.Alice.nodeParams.chainHash) + val db = new SqliteSwapsDb(DriverManager.getConnection("jdbc:sqlite::memory:")) // subscribe to notification events from SwapInReceiver when a payment is successfully received or claimed via coop or csv testKit.system.eventStream ! Subscribe[SwapEvent](swapEvents.ref) - val swapInSender = testKit.spawn(Behaviors.monitor(monitor.ref, SwapMaker(TestConstants.Alice.nodeParams, watcher.ref, register.ref.toClassic, wallet)), "swap-out-receiver") + val swapInSender = testKit.spawn(Behaviors.monitor(monitor.ref, SwapMaker(TestConstants.Alice.nodeParams, watcher.ref, register.ref.toClassic, wallet, keyManager, db)), "swap-out-receiver") withFixture(test.toNoArgTest(FixtureParam(swapInSender, userCli, monitor, register, relayer, router, paymentInitiator, switchboard, paymentHandler, sender, TestConstants.Bob.nodeParams, watcher, wallet, swapEvents))) } case class FixtureParam(swapInSender: ActorRef[SwapCommands.SwapCommand], userCli: TestProbe[Status], monitor: TestProbe[SwapCommands.SwapCommand], register: TestProbe[Any], relayer: TestProbe[Any], router: TestProbe[Any], paymentInitiator: TestProbe[Any], switchboard: TestProbe[Any], paymentHandler: TestProbe[Any], sender: TestProbe[Any], nodeParams: NodeParams, watcher: TestProbe[ZmqWatcher.Command], wallet: OnChainWallet, swapEvents: TestProbe[SwapEvent]) - test("happy path for new swap out") { f => + test("happy path for new swap out receiver") { f => import f._ // start new SwapInSender @@ -103,7 +110,7 @@ case class SwapOutReceiverSpec() extends ScalaTestWithActorTestKit(ConfigFactory monitor.expectMessage(StartSwapOutReceiver(request)) // SwapInSender:SwapOutAgreement -> SwapInReceiver - val agreement = register.expectMessageType[ForwardShortId[SwapOutAgreement]].message + val agreement = swapOutAgreementCodec.decode(expectUnknownMessage(register).data.drop(2).toBitVector).require.value assert(agreement.pubkey == makerPubkey.toHex) // SwapInReceiver pays the fee invoice @@ -117,8 +124,8 @@ case class SwapOutReceiverSpec() extends ScalaTestWithActorTestKit(ConfigFactory assert(openingTx.txOut.head.amount == amount) // SwapInSender:OpeningTxBroadcasted -> SwapInReceiver - val openingTxBroadcasted = register.expectMessageType[ForwardShortId[OpeningTxBroadcasted]] - val paymentInvoice = Bolt11Invoice.fromString(openingTxBroadcasted.message.payreq).get + val openingTxBroadcasted = openingTxBroadcastedCodec.decode(expectUnknownMessage(register).data.drop(2).toBitVector).require.value + val paymentInvoice = Bolt11Invoice.fromString(openingTxBroadcasted.payreq).get // wait for SwapInSender to subscribe to PaymentEventReceived messages swapEvents.expectNoMessage() diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/swap/SwapOutSenderSpec.scala b/plugins/peerswap/src/test/scala/fr/acinq/eclair/plugins/peerswap/SwapOutSenderSpec.scala similarity index 85% rename from eclair-core/src/test/scala/fr/acinq/eclair/swap/SwapOutSenderSpec.scala rename to plugins/peerswap/src/test/scala/fr/acinq/eclair/plugins/peerswap/SwapOutSenderSpec.scala index 24ae6e9380..8bdb2e0036 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/swap/SwapOutSenderSpec.scala +++ b/plugins/peerswap/src/test/scala/fr/acinq/eclair/plugins/peerswap/SwapOutSenderSpec.scala @@ -14,7 +14,7 @@ * limitations under the License. */ -package fr.acinq.eclair.swap +package fr.acinq.eclair.plugins.peerswap import akka.actor.testkit.typed.scaladsl.{ScalaTestWithActorTestKit, TestProbe} import akka.actor.typed.ActorRef @@ -33,17 +33,21 @@ import fr.acinq.eclair.channel.DATA_NORMAL import fr.acinq.eclair.channel.Register.ForwardShortId import fr.acinq.eclair.payment.send.PaymentInitiator.SendPaymentToNode import fr.acinq.eclair.payment.{Bolt11Invoice, PaymentSent} -import fr.acinq.eclair.swap.SwapCommands._ -import fr.acinq.eclair.swap.SwapEvents.{ClaimByInvoiceConfirmed, SwapEvent, TransactionPublished} -import fr.acinq.eclair.swap.SwapResponses.{Status, SwapStatus} -import fr.acinq.eclair.swap.SwapTransactions.{makeSwapClaimByInvoiceTx, makeSwapOpeningTxOut} +import fr.acinq.eclair.plugins.peerswap.SwapCommands._ +import fr.acinq.eclair.plugins.peerswap.SwapEvents.{ClaimByInvoiceConfirmed, SwapEvent, TransactionPublished} +import fr.acinq.eclair.plugins.peerswap.SwapResponses.{Status, SwapStatus} +import fr.acinq.eclair.plugins.peerswap.db.sqlite.SqliteSwapsDb +import fr.acinq.eclair.plugins.peerswap.transactions.SwapTransactions.{makeSwapClaimByInvoiceTx, makeSwapOpeningTxOut} +import fr.acinq.eclair.plugins.peerswap.wire.protocol.PeerSwapMessageCodecs.swapOutRequestCodec +import fr.acinq.eclair.plugins.peerswap.wire.protocol.{OpeningTxBroadcasted, SwapOutAgreement, SwapOutRequest} import fr.acinq.eclair.wire.internal.channel.ChannelCodecsSpec -import fr.acinq.eclair.wire.protocol.{OpeningTxBroadcasted, SwapOutAgreement, SwapOutRequest} +import fr.acinq.eclair.wire.protocol.UnknownMessage import fr.acinq.eclair.{BlockHeight, CltvExpiryDelta, NodeParams, ShortChannelId, TestConstants, ToMilliSatoshiConversion, randomBytes32} import grizzled.slf4j.Logging import org.scalatest.funsuite.FixtureAnyFunSuiteLike import org.scalatest.{BeforeAndAfterAll, Outcome} +import java.sql.DriverManager import java.util.UUID import scala.concurrent.duration._ @@ -59,7 +63,7 @@ case class SwapOutSenderSpec() extends ScalaTestWithActorTestKit(ConfigFactory.l val channelData: DATA_NORMAL = ChannelCodecsSpec.normal val shortChannelId: ShortChannelId = channelData.shortIds.real.toOption.get val channelId: ByteVector32 = channelData.channelId - val keyManager: SwapKeyManager = TestConstants.Bob.nodeParams.swapKeyManager + val keyManager: SwapKeyManager = new LocalSwapKeyManager(TestConstants.Bob.seed, TestConstants.Bob.nodeParams.chainHash) val makerPrivkey: PrivateKey = PrivateKey(randomBytes32()) val takerPrivkey: PrivateKey = keyManager.openingPrivateKey(SwapKeyManager.keyPath(swapId)).privateKey val makerNodeId: PublicKey = PrivateKey(randomBytes32()).publicKey @@ -75,6 +79,7 @@ case class SwapOutSenderSpec() extends ScalaTestWithActorTestKit(ConfigFactory.l val scriptOut: Long = 0 val blindingKey: String = "" val request: SwapOutRequest = SwapOutRequest(protocolVersion, swapId, noAsset, network, shortChannelId.toString, amount.toLong, makerPubkey.toHex) + def expectUnknownMessage(register: TestProbe[Any]): UnknownMessage = register.expectMessageType[ForwardShortId[UnknownMessage]].message override def withFixture(test: OneArgTest): Outcome = { val watcher = testKit.createTestProbe[ZmqWatcher.Command]() @@ -90,18 +95,20 @@ case class SwapOutSenderSpec() extends ScalaTestWithActorTestKit(ConfigFactory.l val sender = testKit.createTestProbe[Any]() val swapEvents = testKit.createTestProbe[SwapEvent]() val monitor = testKit.createTestProbe[SwapCommands.SwapCommand]() + val keyManager: SwapKeyManager = new LocalSwapKeyManager(TestConstants.Bob.seed, TestConstants.Bob.nodeParams.chainHash) + val db = new SqliteSwapsDb(DriverManager.getConnection("jdbc:sqlite::memory:")) // subscribe to notification events from SwapInReceiver when a payment is successfully received or claimed via coop or csv testKit.system.eventStream ! Subscribe[SwapEvent](swapEvents.ref) - val swapInReceiver = testKit.spawn(Behaviors.monitor(monitor.ref, SwapTaker(TestConstants.Bob.nodeParams, paymentInitiator.ref.toClassic, watcher.ref, register.ref.toClassic, wallet)), "swap-out-sender") + val swapOutSender = testKit.spawn(Behaviors.monitor(monitor.ref, SwapTaker(TestConstants.Bob.nodeParams, paymentInitiator.ref.toClassic, watcher.ref, register.ref.toClassic, wallet, keyManager, db)), "swap-out-sender") - withFixture(test.toNoArgTest(FixtureParam(swapInReceiver, userCli, monitor, register, relayer, router, paymentInitiator, switchboard, paymentHandler, sender, TestConstants.Bob.nodeParams, watcher, wallet, swapEvents))) + withFixture(test.toNoArgTest(FixtureParam(swapOutSender, userCli, monitor, register, relayer, router, paymentInitiator, switchboard, paymentHandler, sender, TestConstants.Bob.nodeParams, watcher, wallet, swapEvents))) } case class FixtureParam(swapOutSender: ActorRef[SwapCommands.SwapCommand], userCli: TestProbe[Status], monitor: TestProbe[SwapCommands.SwapCommand], register: TestProbe[Any], relayer: TestProbe[Any], router: TestProbe[Any], paymentInitiator: TestProbe[Any], switchboard: TestProbe[Any], paymentHandler: TestProbe[Any], sender: TestProbe[Any], nodeParams: NodeParams, watcher: TestProbe[ZmqWatcher.Command], wallet: OnChainWallet, swapEvents: TestProbe[SwapEvent]) - test("happy path for new swap out") { f => + test("happy path for new swap out sender") { f => import f._ // start new SwapOutSender @@ -109,7 +116,7 @@ case class SwapOutSenderSpec() extends ScalaTestWithActorTestKit(ConfigFactory.l monitor.expectMessageType[StartSwapOutSender] // SwapOutSender: SwapOutRequest -> SwapOutReceiver - val request = register.expectMessageType[ForwardShortId[SwapOutRequest]].message + val request = swapOutRequestCodec.decode(expectUnknownMessage(register).data.drop(2).toBitVector).require.value assert(request.pubkey == takerPubkey.toHex) // SwapOutReceiver: SwapOutAgreement -> SwapOutSender (request fee) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/swap/SwapRegisterSpec.scala b/plugins/peerswap/src/test/scala/fr/acinq/eclair/plugins/peerswap/SwapRegisterSpec.scala similarity index 62% rename from eclair-core/src/test/scala/fr/acinq/eclair/swap/SwapRegisterSpec.scala rename to plugins/peerswap/src/test/scala/fr/acinq/eclair/plugins/peerswap/SwapRegisterSpec.scala index 319a295a44..2a5cfe15e3 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/swap/SwapRegisterSpec.scala +++ b/plugins/peerswap/src/test/scala/fr/acinq/eclair/plugins/peerswap/SwapRegisterSpec.scala @@ -14,7 +14,7 @@ * limitations under the License. */ -package fr.acinq.eclair.swap +package fr.acinq.eclair.plugins.peerswap import akka.actor.testkit.typed.scaladsl.{ScalaTestWithActorTestKit, TestProbe} import akka.actor.typed.eventstream.EventStream.{Publish, Subscribe} @@ -31,32 +31,41 @@ import fr.acinq.eclair.blockchain.{DummyOnChainWallet, OnChainWallet} import fr.acinq.eclair.channel.DATA_NORMAL import fr.acinq.eclair.channel.Register.ForwardShortId import fr.acinq.eclair.payment.{Bolt11Invoice, PaymentReceived, PaymentSent} -import fr.acinq.eclair.swap.SwapEvents.{ClaimByInvoiceConfirmed, ClaimByInvoicePaid, SwapEvent, TransactionPublished} -import fr.acinq.eclair.swap.SwapRegister.{MessageReceived, SwapInRequested, SwapTerminated} -import fr.acinq.eclair.swap.SwapResponses.{Response, SwapOpened} -import fr.acinq.eclair.swap.SwapTransactions.{makeSwapClaimByInvoiceTx, makeSwapOpeningTxOut} +import fr.acinq.eclair.plugins.peerswap.SwapEvents.{ClaimByInvoiceConfirmed, ClaimByInvoicePaid, SwapEvent, TransactionPublished} +import fr.acinq.eclair.plugins.peerswap.SwapRegister.{MessageReceived, SwapInRequested, SwapTerminated} +import fr.acinq.eclair.plugins.peerswap.SwapResponses.{Response, SwapOpened} +import fr.acinq.eclair.plugins.peerswap.db.sqlite.SqliteSwapsDb +import fr.acinq.eclair.plugins.peerswap.transactions.SwapTransactions.{makeSwapClaimByInvoiceTx, makeSwapOpeningTxOut} +import fr.acinq.eclair.plugins.peerswap.wire.protocol.PeerSwapMessageCodecs.{openingTxBroadcastedCodec, swapInRequestCodec} +import fr.acinq.eclair.plugins.peerswap.wire.protocol._ import fr.acinq.eclair.wire.internal.channel.ChannelCodecsSpec -import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{BlockHeight, CltvExpiryDelta, NodeParams, ShortChannelId, TestConstants, TimestampMilli, ToMilliSatoshiConversion} -import org.mockito.scalatest.IdiomaticMockito +import fr.acinq.eclair.wire.protocol.UnknownMessage +import fr.acinq.eclair.{BlockHeight, CltvExpiryDelta, NodeParams, ShortChannelId, TestConstants, TimestampMilli, ToMilliSatoshiConversion, randomBytes32} import org.scalatest.funsuite.FixtureAnyFunSuiteLike import org.scalatest.matchers.should.Matchers import org.scalatest.{BeforeAndAfterAll, Outcome, ParallelTestExecution} import scodec.bits.HexStringSyntax +import java.sql.DriverManager import java.util.UUID import scala.concurrent.duration._ import scala.concurrent.{ExecutionContext, Future} +import scala.language.postfixOps -class SwapRegisterSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("application")) with BeforeAndAfterAll with Matchers with FixtureAnyFunSuiteLike with IdiomaticMockito with ParallelTestExecution { +class SwapRegisterSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("application")) with BeforeAndAfterAll with Matchers with FixtureAnyFunSuiteLike with ParallelTestExecution { override implicit val timeout: Timeout = Timeout(30 seconds) val protocolVersion = 2 val noAsset = "" val network: String = NodeParams.chainFromHash(TestConstants.Alice.nodeParams.chainHash) val amount: Satoshi = 1000 sat val fee: Satoshi = 22 sat - val swapId0: String = ByteVector32.Zeroes.toHex - val swapId1: String = ByteVector32.One.toHex + + def paymentPreimage(index: Int): ByteVector32 = index match { + case 0 => ByteVector32.Zeroes + case 1 => ByteVector32.One + case _ => randomBytes32() + } + def swapId(index: Int): String = paymentPreimage(index).toHex val channelData: DATA_NORMAL = ChannelCodecsSpec.normal val shortChannelId: ShortChannelId = channelData.shortIds.real.toOption.get val channelId: ByteVector32 = channelData.channelId @@ -66,17 +75,22 @@ class SwapRegisterSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("app val blindingKey = "" val txId: String = ByteVector32.One.toHex + val aliceKeyManager: SwapKeyManager = new LocalSwapKeyManager(TestConstants.Alice.seed, TestConstants.Alice.nodeParams.chainHash) + val aliceDb = new SqliteSwapsDb(DriverManager.getConnection("jdbc:sqlite::memory:")) + + val bobKeyManager: SwapKeyManager = new LocalSwapKeyManager(TestConstants.Bob.seed, TestConstants.Bob.nodeParams.chainHash) + val bobDb = new SqliteSwapsDb(DriverManager.getConnection("jdbc:sqlite::memory:")) + val aliceNodeId: PublicKey = TestConstants.Alice.nodeParams.nodeId - val alicePrivkey: PrivateKey = TestConstants.Alice.nodeParams.swapKeyManager.openingPrivateKey(SwapKeyManager.keyPath(swapId0)).privateKey - val alicePubkey: PublicKey = alicePrivkey.publicKey - val bobPrivkey: PrivateKey = TestConstants.Alice.nodeParams.swapKeyManager.openingPrivateKey(SwapKeyManager.keyPath(swapId1)).privateKey - val bobPubkey: PublicKey = bobPrivkey.publicKey - val paymentPreimage0: ByteVector32 = ByteVector32.Zeroes - val paymentPreimage1: ByteVector32 = ByteVector32.One - val invoice0: Bolt11Invoice = Bolt11Invoice(TestConstants.Alice.nodeParams.chainHash, Some(amount.toMilliSatoshi), Crypto.sha256(paymentPreimage0), alicePrivkey, Left("PeerSwap payment invoice0"), CltvExpiryDelta(18)) - val feeInvoice: Bolt11Invoice = Bolt11Invoice(TestConstants.Alice.nodeParams.chainHash, Some(fee.toMilliSatoshi), Crypto.sha256(paymentPreimage1), alicePrivkey, Left("PeerSwap fee invoice"), CltvExpiryDelta(18)) - val invoice1: Bolt11Invoice = Bolt11Invoice(TestConstants.Alice.nodeParams.chainHash, Some(amount.toMilliSatoshi), Crypto.sha256(paymentPreimage1), alicePrivkey, Left("PeerSwap payment invoice1"), CltvExpiryDelta(18)) + def alicePrivkey(swapId: String): PrivateKey = aliceKeyManager.openingPrivateKey(SwapKeyManager.keyPath(swapId)).privateKey + def alicePubkey(swapId: String): PublicKey = alicePrivkey(swapId).publicKey + def bobPrivkey(swapId: String): PrivateKey = bobKeyManager.openingPrivateKey(SwapKeyManager.keyPath(swapId)).privateKey + def bobPubkey(swapId: String): PublicKey = bobPrivkey(swapId).publicKey + val invoice0: Bolt11Invoice = Bolt11Invoice(TestConstants.Alice.nodeParams.chainHash, Some(amount.toMilliSatoshi), Crypto.sha256(paymentPreimage(0)), alicePrivkey(swapId(0)), Left("PeerSwap payment invoice0"), CltvExpiryDelta(18)) + val feeInvoice: Bolt11Invoice = Bolt11Invoice(TestConstants.Alice.nodeParams.chainHash, Some(fee.toMilliSatoshi), Crypto.sha256(paymentPreimage(1)), bobPrivkey(swapId(1)), Left("PeerSwap fee invoice1"), CltvExpiryDelta(18)) + val invoice1: Bolt11Invoice = Bolt11Invoice(TestConstants.Alice.nodeParams.chainHash, Some(amount.toMilliSatoshi), Crypto.sha256(paymentPreimage(1)), bobPrivkey(swapId(1)), Left("PeerSwap payment invoice1"), CltvExpiryDelta(18)) val feeRatePerKw: FeeratePerKw = TestConstants.Alice.nodeParams.onChainFeeConf.feeEstimator.getFeeratePerKw(target = TestConstants.Alice.nodeParams.onChainFeeConf.feeTargets.fundingBlockTarget) + def expectUnknownMessage(register: TestProbe[Any]): UnknownMessage = register.expectMessageType[ForwardShortId[UnknownMessage]].message override def withFixture(test: OneArgTest): Outcome = { val userCli = testKit.createTestProbe[Response]() @@ -100,60 +114,60 @@ class SwapRegisterSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("app test("restore the swap register from the database") { f => import f._ - val swapInRequest: SwapInRequest = SwapInRequest(protocolVersion, swapId0, noAsset, network, shortChannelId.toString, amount.toLong, alicePubkey.toString()) - val swapInAgreement: SwapInAgreement = SwapInAgreement(protocolVersion, swapId0, bobPubkey.toString(), premium) - val swapOutRequest: SwapOutRequest = SwapOutRequest(protocolVersion, swapId1, noAsset, network, shortChannelId.toString, amount.toLong, bobPubkey.toString()) - val swapOutAgreement: SwapOutAgreement = SwapOutAgreement(protocolVersion, swapId1, alicePubkey.toString(), feeInvoice.toString) - val openingTxBroadcasted0: OpeningTxBroadcasted = OpeningTxBroadcasted(swapId0, invoice0.toString, txId, scriptOut, blindingKey) - val openingTxBroadcasted1: OpeningTxBroadcasted = OpeningTxBroadcasted(swapId1, invoice1.toString, txId, scriptOut, blindingKey) + val swapInRequest: SwapInRequest = SwapInRequest(protocolVersion, swapId(0), noAsset, network, shortChannelId.toString, amount.toLong, alicePubkey(swapId(0)).toString()) + val swapInAgreement: SwapInAgreement = SwapInAgreement(protocolVersion, swapId(0), bobPubkey(swapId(0)).toString(), premium) + val swapOutRequest: SwapOutRequest = SwapOutRequest(protocolVersion, swapId(1), noAsset, network, shortChannelId.toString, amount.toLong, bobPubkey(swapId(1)).toString()) + val swapOutAgreement: SwapOutAgreement = SwapOutAgreement(protocolVersion, swapId(1), bobPubkey(swapId(1)).toString(), feeInvoice.toString) + val openingTxBroadcasted0: OpeningTxBroadcasted = OpeningTxBroadcasted(swapId(0), invoice0.toString, txId, scriptOut, blindingKey) + val openingTxBroadcasted1: OpeningTxBroadcasted = OpeningTxBroadcasted(swapId(1), invoice1.toString, txId, scriptOut, blindingKey) val savedData: Set[SwapData] = Set(SwapData(swapInRequest, swapInAgreement, invoice0, openingTxBroadcasted0, swapRole = SwapRole.Maker, isInitiator = true), SwapData(swapOutRequest, swapOutAgreement, invoice1, openingTxBroadcasted1, swapRole = SwapRole.Taker, isInitiator = true)) - val swapRegister = testKit.spawn(Behaviors.monitor(monitor.ref, SwapRegister(TestConstants.Alice.nodeParams, paymentHandler.ref.toClassic, watcher.ref, register.ref.toClassic, wallet, savedData)), "SwapRegister") + val swapRegister = testKit.spawn(Behaviors.monitor(monitor.ref, SwapRegister(TestConstants.Alice.nodeParams, paymentHandler.ref.toClassic, watcher.ref, register.ref.toClassic, wallet, aliceKeyManager, aliceDb, savedData)), "SwapRegister") // wait for SwapMaker and SwapTaker to subscribe to PaymentEventReceived messages swapEvents.expectNoMessage() - // Taker: payment(paymentHash) -> Maker + // swapId0 - Taker: payment(paymentHash) -> Maker val paymentHash0 = Bolt11Invoice.fromString(openingTxBroadcasted0.payreq).get.paymentHash val paymentReceived0 = PaymentReceived(paymentHash0, Seq(PaymentReceived.PartialPayment(amount.toMilliSatoshi, channelId, TimestampMilli(1553784963659L)))) testKit.system.eventStream ! Publish(paymentReceived0) - // SwapRegister received notice that SwapInSender swap completed + // swapId0 - SwapRegister received notice that SwapInSender swap completed val swap0Completed = swapEvents.expectMessageType[ClaimByInvoicePaid] - assert(swap0Completed.swapId === swapId0) + assert(swap0Completed.swapId === swapId(0)) - // SwapRegister receives notification that the swap Maker actor stopped - assert(monitor.expectMessageType[SwapTerminated].swapId === swapId0) + // swapId0: SwapRegister receives notification that the swap Maker actor stopped + assert(monitor.expectMessageType[SwapTerminated].swapId === swapId(0)) - // ZmqWatcher -> Taker, trigger confirmation of opening transaction - val openingTx = Transaction(2, Seq(), Seq(makeSwapOpeningTxOut(swapOutRequest.amount.sat, alicePubkey, bobPubkey, invoice1.paymentHash)), 0) + // swapId1 - ZmqWatcher -> Taker, trigger confirmation of opening transaction + val openingTx = Transaction(2, Seq(), Seq(makeSwapOpeningTxOut(swapOutRequest.amount.sat, bobPubkey(swapId(1)), alicePubkey(swapId(1)), invoice1.paymentHash)), 0) watcher.expectMessageType[WatchTxConfirmed].replyTo ! WatchTxConfirmedTriggered(BlockHeight(1), 0, openingTx) - // wait for Taker to subscribe to PaymentEventReceived messages + // swapId1 - wait for Taker to subscribe to PaymentEventReceived messages swapEvents.expectNoMessage() - // Taker validates the invoice and opening transaction before paying the invoice - testKit.system.eventStream ! Publish(PaymentSent(UUID.randomUUID(), invoice1.paymentHash, paymentPreimage1, amount.toMilliSatoshi, aliceNodeId, PaymentSent.PartialPayment(UUID.randomUUID(), amount.toMilliSatoshi, 0.sat.toMilliSatoshi, channelId, None) :: Nil)) + // swapId1 - Taker validates the invoice and opening transaction before paying the invoice + testKit.system.eventStream ! Publish(PaymentSent(UUID.randomUUID(), invoice1.paymentHash, paymentPreimage(1), amount.toMilliSatoshi, aliceNodeId, PaymentSent.PartialPayment(UUID.randomUUID(), amount.toMilliSatoshi, 0.sat.toMilliSatoshi, channelId, None) :: Nil)) - // ZmqWatcher -> Taker, trigger confirmation of claim-by-invoice transaction - val claimByInvoiceTx = makeSwapClaimByInvoiceTx(swapOutRequest.amount.sat, bobPubkey, alicePrivkey, paymentPreimage1, feeRatePerKw, openingTx.hash, 0) + // swapId1 - ZmqWatcher -> Taker, trigger confirmation of claim-by-invoice transaction + val claimByInvoiceTx = makeSwapClaimByInvoiceTx(swapOutRequest.amount.sat, bobPubkey(swapId(1)), alicePrivkey(swapId(1)), paymentPreimage(1), feeRatePerKw, openingTx.hash, 0) watcher.expectMessageType[WatchTxConfirmed].replyTo ! WatchTxConfirmedTriggered(BlockHeight(6), 0, claimByInvoiceTx) - // SwapRegister received notice that SwapOutSender completed + // swapId1 - SwapRegister received notice that SwapOutSender completed swapEvents.expectMessageType[TransactionPublished] - assert(swapEvents.expectMessageType[ClaimByInvoiceConfirmed].swapId === swapId1) + assert(swapEvents.expectMessageType[ClaimByInvoiceConfirmed].swapId === swapId(1)) - // SwapRegister receives notification that the swap Taker actor stopped - assert(monitor.expectMessageType[SwapTerminated].swapId === swapId1) + // swapId1 - SwapRegister receives notification that the swap Taker actor stopped + assert(monitor.expectMessageType[SwapTerminated].swapId === swapId(1)) testKit.stop(swapRegister) } - test("register a new swap in the swap register ") { f => + test("register a new swap in the swap register") { f => import f._ // initialize SwapRegister - val swapRegister = testKit.spawn(Behaviors.monitor(monitor.ref, SwapRegister(TestConstants.Alice.nodeParams, paymentHandler.ref.toClassic, watcher.ref, register.ref.toClassic, wallet, Set())), "SwapRegister") + val swapRegister = testKit.spawn(Behaviors.monitor(monitor.ref, SwapRegister(TestConstants.Alice.nodeParams, paymentHandler.ref.toClassic, watcher.ref, register.ref.toClassic, wallet, aliceKeyManager, aliceDb, Set())), "SwapRegister") swapEvents.expectNoMessage() userCli.expectNoMessage() @@ -163,21 +177,21 @@ class SwapRegisterSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("app monitor.expectMessageType[SwapInRequested] // Alice:SwapInRequest -> Bob - val swapInRequest = register.expectMessageType[ForwardShortId[SwapInRequest]] - assert(swapId === swapInRequest.message.swapId) + val swapInRequest = swapInRequestCodec.decode(expectUnknownMessage(register).data.drop(2).toBitVector).require.value + assert(swapId === swapInRequest.swapId) // Bob: SwapInAgreement -> Alice - swapRegister ! MessageReceived(SwapInAgreement(swapInRequest.message.protocolVersion, swapInRequest.message.swapId, bobPayoutPubkey.toString(), premium)) + swapRegister ! MessageReceived(SwapInAgreement(swapInRequest.protocolVersion, swapInRequest.swapId, bobPayoutPubkey.toString(), premium)) monitor.expectMessageType[MessageReceived] // SwapInSender confirms opening tx published swapEvents.expectMessageType[TransactionPublished] // Alice:OpeningTxBroadcasted -> Bob - val openingTxBroadcasted = register.expectMessageType[ForwardShortId[OpeningTxBroadcasted]] + val openingTxBroadcasted = openingTxBroadcastedCodec.decode(expectUnknownMessage(register).data.drop(2).toBitVector).require.value // Bob: payment(paymentHash) -> Alice - val paymentHash = Bolt11Invoice.fromString(openingTxBroadcasted.message.payreq).get.paymentHash + val paymentHash = Bolt11Invoice.fromString(openingTxBroadcasted.payreq).get.paymentHash val paymentReceived = PaymentReceived(paymentHash, Seq(PaymentReceived.PartialPayment(amount.toMilliSatoshi, channelId, TimestampMilli(1553784963659L)))) testKit.system.eventStream ! Publish(paymentReceived) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/SwapsDbSpec.scala b/plugins/peerswap/src/test/scala/fr/acinq/eclair/plugins/peerswap/db/SwapsDbSpec.scala similarity index 63% rename from eclair-core/src/test/scala/fr/acinq/eclair/db/SwapsDbSpec.scala rename to plugins/peerswap/src/test/scala/fr/acinq/eclair/plugins/peerswap/db/SwapsDbSpec.scala index 3b0eacf604..1ff74692a9 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/SwapsDbSpec.scala +++ b/plugins/peerswap/src/test/scala/fr/acinq/eclair/plugins/peerswap/db/SwapsDbSpec.scala @@ -14,38 +14,34 @@ * limitations under the License. */ -package fr.acinq.eclair.db - +package fr.acinq.eclair.plugins.peerswap.db import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} import fr.acinq.bitcoin.scalacompat.{ByteVector32, Crypto, Satoshi, SatoshiLong} -import fr.acinq.eclair.TestDatabases.{TestPgDatabases, TestSqliteDatabases} -import fr.acinq.eclair.db.pg.PgSwapsDb -import fr.acinq.eclair.db.sqlite.SqliteSwapsDb import fr.acinq.eclair.payment.PaymentReceived.PartialPayment import fr.acinq.eclair.payment.{Bolt11Invoice, PaymentReceived} -import fr.acinq.eclair.swap.SwapEvents.ClaimByInvoicePaid -import fr.acinq.eclair.swap.SwapRole.{Maker, SwapRole, Taker} -import fr.acinq.eclair.swap.{SwapData, SwapKeyManager} -import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{CltvExpiryDelta, TestConstants, ToMilliSatoshiConversion, randomBytes32} +import fr.acinq.eclair.plugins.peerswap.SwapEvents.ClaimByInvoicePaid +import fr.acinq.eclair.plugins.peerswap.SwapRole.{Maker, SwapRole, Taker} +import fr.acinq.eclair.plugins.peerswap.db.sqlite.SqliteSwapsDb +import fr.acinq.eclair.plugins.peerswap.wire.protocol._ +import fr.acinq.eclair.plugins.peerswap.{LocalSwapKeyManager, SwapData, SwapKeyManager} +import fr.acinq.eclair.{CltvExpiryDelta, NodeParams, TestConstants, ToMilliSatoshiConversion, randomBytes32} import org.scalatest.funsuite.AnyFunSuite +import java.sql.DriverManager import java.util.concurrent.Executors import scala.concurrent.duration._ import scala.concurrent.{Await, ExecutionContext, ExecutionContextExecutor, Future} class SwapsDbSpec extends AnyFunSuite { - import fr.acinq.eclair.TestDatabases.forAllDbs - val protocolVersion = 2 val noAsset = "" - val network: String = TestConstants.Alice.nodeParams.chainHash.toString() + val network: String = NodeParams.chainFromHash(TestConstants.Alice.nodeParams.chainHash) val amount: Satoshi = 1000 sat val fee: Satoshi = 100 sat - val makerKeyManager: SwapKeyManager = TestConstants.Alice.nodeParams.swapKeyManager - val takerKeyManager: SwapKeyManager = TestConstants.Bob.nodeParams.swapKeyManager + val makerKeyManager: SwapKeyManager = new LocalSwapKeyManager(TestConstants.Alice.seed, TestConstants.Alice.nodeParams.chainHash) + val takerKeyManager: SwapKeyManager = new LocalSwapKeyManager(TestConstants.Bob.seed, TestConstants.Bob.nodeParams.chainHash) val makerNodeId: PublicKey = PrivateKey(randomBytes32()).publicKey val premium = 10 val txid: String = ByteVector32.One.toHex @@ -78,52 +74,45 @@ class SwapsDbSpec extends AnyFunSuite { } test("init database two times in a row") { - forAllDbs { - case sqlite: TestSqliteDatabases => - new SqliteSwapsDb(sqlite.connection) - new SqliteSwapsDb(sqlite.connection) - case pg: TestPgDatabases => - new PgSwapsDb()(pg.datasource) - new PgSwapsDb()(pg.datasource) - } + val connection = DriverManager.getConnection("jdbc:sqlite::memory:") + new SqliteSwapsDb(connection) + new SqliteSwapsDb(connection) } test("add/list/addResult/restore/remove swaps") { - forAllDbs { dbs => - val db = dbs.swaps + val db = new SqliteSwapsDb(DriverManager.getConnection("jdbc:sqlite::memory:")) + assert(db.list().isEmpty) - val swap_1 = swapData(randomBytes32().toString(),isInitiator = true, Maker) - val swap_2 = swapData(randomBytes32().toString(),isInitiator = false, Maker) - val swap_3 = swapData(randomBytes32().toString(),isInitiator = true, Taker) - val swap_4 = swapData(randomBytes32().toString(),isInitiator = false, Taker) + val swap_1 = swapData(randomBytes32().toString(),isInitiator = true, Maker) + val swap_2 = swapData(randomBytes32().toString(),isInitiator = false, Maker) + val swap_3 = swapData(randomBytes32().toString(),isInitiator = true, Taker) + val swap_4 = swapData(randomBytes32().toString(),isInitiator = false, Taker) - assert(db.list().toSet == Set.empty) - db.add(swap_1) - assert(db.list().toSet == Set(swap_1)) - db.add(swap_1) // duplicate is ignored - assert(db.list().size == 1) - db.add(swap_2) - db.add(swap_3) - db.add(swap_4) - assert(db.list().toSet == Set(swap_1, swap_2, swap_3, swap_4)) - db.addResult(paymentCompleteResult(swap_2.request.swapId)) - assert(db.restore().toSet == Set(swap_1, swap_3, swap_4)) - db.remove(swap_2.request.swapId) - assert(db.list().toSet == Set(swap_1, swap_3, swap_4)) - assert(db.restore().toSet == Set(swap_1, swap_3, swap_4)) - } + assert(db.list().toSet == Set.empty) + db.add(swap_1) + assert(db.list().toSet == Set(swap_1)) + db.add(swap_1) // duplicate is ignored + assert(db.list().size == 1) + db.add(swap_2) + db.add(swap_3) + db.add(swap_4) + assert(db.list().toSet == Set(swap_1, swap_2, swap_3, swap_4)) + db.addResult(paymentCompleteResult(swap_2.request.swapId)) + assert(db.restore().toSet == Set(swap_1, swap_3, swap_4)) + db.remove(swap_2.request.swapId) + assert(db.list().toSet == Set(swap_1, swap_3, swap_4)) + assert(db.restore().toSet == Set(swap_1, swap_3, swap_4)) } test("concurrent swap updates") { - forAllDbs { dbs => - val db = dbs.swaps - implicit val ec: ExecutionContextExecutor = ExecutionContext.fromExecutor(Executors.newFixedThreadPool(8)) - val futures = for (_ <- 0 until 2500) yield { - Future(db.add(swapData(randomBytes32().toString(),isInitiator = true, Maker))) - } - val res = Future.sequence(futures) - Await.result(res, 60 seconds) + val db = new SqliteSwapsDb(DriverManager.getConnection("jdbc:sqlite::memory:")) + assert(db.list().isEmpty) + + implicit val ec: ExecutionContextExecutor = ExecutionContext.fromExecutor(Executors.newFixedThreadPool(8)) + val futures = for (_ <- 0 until 2500) yield { + Future(db.add(swapData(randomBytes32().toString(),isInitiator = true, Maker))) } + val res = Future.sequence(futures) + Await.result(res, 60 seconds) } - } \ No newline at end of file diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/swap/PeerSwapJsonSerializersSpec.scala b/plugins/peerswap/src/test/scala/fr/acinq/eclair/plugins/peerswap/json/PeerSwapJsonSerializersSpec.scala similarity index 95% rename from eclair-core/src/test/scala/fr/acinq/eclair/swap/PeerSwapJsonSerializersSpec.scala rename to plugins/peerswap/src/test/scala/fr/acinq/eclair/plugins/peerswap/json/PeerSwapJsonSerializersSpec.scala index 0737687b03..86b163c3bb 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/swap/PeerSwapJsonSerializersSpec.scala +++ b/plugins/peerswap/src/test/scala/fr/acinq/eclair/plugins/peerswap/json/PeerSwapJsonSerializersSpec.scala @@ -14,17 +14,14 @@ * limitations under the License. */ -package fr.acinq.eclair.swap +package fr.acinq.eclair.plugins.peerswap.json -import fr.acinq.eclair.json.PeerSwapJsonSerializers.formats -import fr.acinq.eclair.wire.protocol._ +import fr.acinq.eclair.plugins.peerswap.PeerSwapSpec +import fr.acinq.eclair.plugins.peerswap.json.PeerSwapJsonSerializers.formats +import fr.acinq.eclair.plugins.peerswap.wire.protocol._ import org.json4s.jackson.JsonMethods.{compact, parse, render} import org.json4s.jackson.Serialization -/** - * Created by remyers on 03/30/2022. - */ - class PeerSwapJsonSerializersSpec extends PeerSwapSpec { test("encode/decode SwapInRequest to/from json") { val json = s"""{"protocol_version":$protocolVersion,"swap_id":"${swapId.toHex}","asset":"$asset","network":"$network","scid":"$shortId","amount":$amount,"pubkey":"$pubkey"}""".stripMargin diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/swap/SwapTransactionsSpec.scala b/plugins/peerswap/src/test/scala/fr/acinq/eclair/plugins/peerswap/transactions/SwapTransactionsSpec.scala similarity index 95% rename from eclair-core/src/test/scala/fr/acinq/eclair/swap/SwapTransactionsSpec.scala rename to plugins/peerswap/src/test/scala/fr/acinq/eclair/plugins/peerswap/transactions/SwapTransactionsSpec.scala index 0e14d6d432..f58b495225 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/swap/SwapTransactionsSpec.scala +++ b/plugins/peerswap/src/test/scala/fr/acinq/eclair/plugins/peerswap/transactions/SwapTransactionsSpec.scala @@ -14,7 +14,7 @@ * limitations under the License. */ -package fr.acinq.eclair.swap +package fr.acinq.eclair.plugins.peerswap.transactions import akka.actor.typed.ActorRef import akka.actor.typed.scaladsl.adapter.{ClassicActorRefOps, ClassicActorSystemOps} @@ -30,9 +30,9 @@ import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinCoreClient import fr.acinq.eclair.blockchain.fee.FeeratePerKw import fr.acinq.eclair.channel.publish.FinalTxPublisher import fr.acinq.eclair.channel.publish.TxPublisher.TxPublishContext -import fr.acinq.eclair.swap.SwapTransactions._ +import fr.acinq.eclair.plugins.peerswap.transactions.SwapTransactions._ import fr.acinq.eclair.transactions.Transactions -import fr.acinq.eclair.transactions.Transactions.{SwapClaimByCoopTx, SwapClaimByCsvTx, SwapClaimByInvoiceTx, checkSpendable} +import fr.acinq.eclair.transactions.Transactions.checkSpendable import grizzled.slf4j.Logging import org.scalatest.BeforeAndAfterAll import org.scalatest.funsuite.AnyFunSuiteLike @@ -40,10 +40,6 @@ import org.scalatest.funsuite.AnyFunSuiteLike import java.util.UUID import scala.concurrent.ExecutionContext.Implicits.global -/** - * Created by remyers on 06/05/2022. - */ - class SwapTransactionsSpec extends TestKitBaseClass with AnyFunSuiteLike with BitcoindService with BeforeAndAfterAll with Logging { val makerRefundPriv: PrivateKey = PrivateKey(randomBytes32()) val takerPaymentPriv: PrivateKey = PrivateKey(randomBytes32()) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/swap/PeerSwapMessageCodecsSpec.scala b/plugins/peerswap/src/test/scala/fr/acinq/eclair/plugins/peerswap/wire/protocol/PeerSwapMessageCodecsSpec.scala similarity index 97% rename from eclair-core/src/test/scala/fr/acinq/eclair/swap/PeerSwapMessageCodecsSpec.scala rename to plugins/peerswap/src/test/scala/fr/acinq/eclair/plugins/peerswap/wire/protocol/PeerSwapMessageCodecsSpec.scala index b8c952c0e4..15c7eb8bef 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/swap/PeerSwapMessageCodecsSpec.scala +++ b/plugins/peerswap/src/test/scala/fr/acinq/eclair/plugins/peerswap/wire/protocol/PeerSwapMessageCodecsSpec.scala @@ -14,16 +14,12 @@ * limitations under the License. */ -package fr.acinq.eclair.swap +package fr.acinq.eclair.plugins.peerswap.wire.protocol -import fr.acinq.eclair.wire.protocol.PeerSwapMessageCodecs.peerSwapMessageCodecWithFallback -import fr.acinq.eclair.wire.protocol._ +import fr.acinq.eclair.plugins.peerswap.PeerSwapSpec +import fr.acinq.eclair.plugins.peerswap.wire.protocol.PeerSwapMessageCodecs.peerSwapMessageCodecWithFallback import scodec.bits.HexStringSyntax -/** - * Created by remyers on 30/03/2022. - */ - class PeerSwapMessageCodecsSpec extends PeerSwapSpec { test("encode/decode SwapInRequest messages to/from binary") { diff --git a/pom.xml b/pom.xml index ce714f5b7b..5be9073ac0 100644 --- a/pom.xml +++ b/pom.xml @@ -27,6 +27,7 @@ eclair-core eclair-front eclair-node + plugins/peerswap A scala implementation of the Lightning Network