From f3604cffaf9df9c8fffd5142f5dc9387fcfc74b9 Mon Sep 17 00:00:00 2001 From: Fabrice Drouin Date: Mon, 24 Jan 2022 16:32:13 +0100 Subject: [PATCH 001/121] Document onchain wallet backup. (#2143) Eclair does not include an onchain wallet and instead uses the wallet of the bitcoin node it is connected to. Users must also backup this wallet. --- README.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 4ce0fa7672..7e05e7929c 100644 --- a/README.md +++ b/README.md @@ -176,7 +176,14 @@ eclair-node--/bin/eclair-node.sh -Dlogback.configurationFile ### Backup -The files that you need to backup are located in your data directory. You must backup: +You need to backup: +- your bitcoin core wallet +- your eclair channels + +For bitcoin core, you need to backup the wallet file for the wallet that eclair is using. You only need to this once, when the wallet is +created (see https://github.com/bitcoin/bitcoin/blob/master/doc/managing-wallets.md for more information). + +For eclair, the files that you need to backup are located in your data directory. You must backup: * your seeds (`node_seed.dat` and `channel_seed.dat`) * your channel database (`eclair.sqlite.bak` under directory `mainnet`, `testnet` or `regtest` depending on which chain you're running on) From 8758d50df26ba2e7007f5ab4243eaf556fd32d35 Mon Sep 17 00:00:00 2001 From: Fabrice Drouin Date: Mon, 24 Jan 2022 18:04:25 +0100 Subject: [PATCH 002/121] Update cluster documentation [ci skip] (#2122) Update cluster documentation --- docs/Cluster.md | 36 +++++++++++++++++++++++++++--------- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/docs/Cluster.md b/docs/Cluster.md index 2ad0d746ea..e93e42ff25 100644 --- a/docs/Cluster.md +++ b/docs/Cluster.md @@ -37,11 +37,13 @@ The goal is to offload your node from connection and routing table management: ### Prerequisite -You already have a node up and running in a standalone setup (with Bitcoin Core properly configured, etc.). +You already have a lightning node up and running in a standalone setup (with Bitcoin Core properly configured, etc.). You know what your `node id` is. -In the following, what we previously called `eclair-node` will be called `backend`. It is to be launched, configured and backed-up exactly like in a standalone setup. +Conventions used in this document: +- what we previously called `eclair-node` will be called `backend`. It is to be launched, configured and backed-up exactly like in a standalone setup. +- `node` refer to *akka cluster nodes*, not to be confused with lightning nodes. Together, all *cluster nodes* form a single logical *lighting node*. ### Minimal/Demo setup @@ -72,7 +74,7 @@ NB: we override the ports, otherwise they would conflict since in this example e ### Production setup In production you should: -- run multiple `frontend`s +- run multiple `frontend` servers - run one app per server - enable `tcp-tls` to encrypt communications between members of the cluster with your own generated certificate (see below) - use a load balancer to hide all your `frontend` servers under the same ip address @@ -86,31 +88,47 @@ We use a self-signed certificate, which offers a good compromise. More advanced > Have a single set of keys and a single certificate for all nodes and disable hostname checking > - The single set of keys and the single certificate is distributed to all nodes. The certificate can be self-signed as it is distributed both as a certificate for authentication but also as the trusted certificate. > - If the keys/certificate are lost, someone else can connect to your cluster. -> - Adding nodes to the cluster is simple as the key material can be deployed / distributed to the new node. +> - Adding nodes to the cluster is simple as the key material can be deployed / distributed to the new cluster node. Generate a self-signed certificate (set a strong password): ```shell $ keytool -genkeypair -v \ -keystore akka-cluster-tls.jks \ -dname "O=ACME, C=FR" \ - -keypass:env \ - -storepass:env \ + -keypass \ + -storepass \ -keyalg RSA \ -keysize 4096 \ -validity 9999 ``` -Copy the resulting certificate to your `.eclair` directory: +Copy the resulting certificate to the `.eclair` directory on your backend node and all your frontend nodes: ```shell $ cp akka-cluster-tls.jks ~/.eclair ``` +Add this to `eclair.conf` on all your frontend nodes: +``` +akka.remote.artery.transport = "tls-tcp" +``` -Add this to your `eclair.conf`: +#### Run cluster nodes on separate servers + +Start all your frontend nodes with the following environment variables: +* BACKEND_IP set to the IP address of your backend node +* LOCAL_IP set to the IP address of this frontend node (this is typically a private IP address, reachable from your backend node) +* NODE_PUB_KEY set to your node public key +* AKKA_TLS_PASSWORD set to the password of your Akka certificate + +Add this to `eclair.conf` on your backend node: ``` -AKKA_TLS_PASSWORD= akka.remote.artery.transport = "tls-tcp" +akka.remote.artery.canonical.hostname="ip-of-this-backend-node" +akka.cluster.seed-nodes=["akka://eclair-node@ip-of-this-backend-node:25520"] ``` +Start your backend node with the following environment variables: +* AKKA_TLS_PASSWORD set to the password of your Akka certificate + ### AWS Deployment For convenience, we provide a prebuilt AWS Beanstalk bundle for the `frontend` (choose a WebServer environment type, and Java platform). From 75ef66e54c387ea7833465d8d34e8f6627bcbff2 Mon Sep 17 00:00:00 2001 From: Pierre-Marie Padiou Date: Tue, 25 Jan 2022 17:23:41 +0100 Subject: [PATCH 003/121] Front: use IMDSv2 to retrieve the instance ip (#2146) This is used by the built-in AWS Beanstalk package. --- eclair-front/modules/awseb/run.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/eclair-front/modules/awseb/run.sh b/eclair-front/modules/awseb/run.sh index e3d7d8ecc1..9c6f0401a8 100644 --- a/eclair-front/modules/awseb/run.sh +++ b/eclair-front/modules/awseb/run.sh @@ -1,4 +1,6 @@ -export LOCAL_IP=$(curl -s 169.254.169.254/latest/meta-data/local-ipv4) +# see https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instancedata-data-retrieval.html +TOKEN_IMDSV2=$(curl -s -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600") +export LOCAL_IP=$(curl -s -H "X-aws-ec2-metadata-token: $TOKEN_IMDSV2" http://169.254.169.254/latest/meta-data/local-ipv4) export HOSTNAME=$(hostname) # make the eclair home directory From 57c2cc5df907f2d848daf3d40cf2ceb6ff603ede Mon Sep 17 00:00:00 2001 From: Pierre-Marie Padiou Date: Tue, 25 Jan 2022 18:35:32 +0100 Subject: [PATCH 004/121] Type `ChannelFlags` instead of using a raw `Byte` (#2148) This has the same kind of impact as the typing of `Features`. In particular we can now explicitly set the values in `eclair.conf`: `eclair.channel.channel-flags.announce-channel=true`. I took the opportunity to move this channel-related config key in its own config section, with the goal that we move the other fields in a follow-up PR. It also has the nice side effect of providing a pretty json formatting out of the box: ```json "channelFlags": { "announceChannel": true } ``` The `open` method of the API has been updated: `channelFlags` has been replaced by a new `announceChannel` parameter. --- docs/release-notes/eclair-vnext.md | 1 + eclair-core/src/main/resources/reference.conf | 7 ++++++- .../main/scala/fr/acinq/eclair/Eclair.scala | 6 +++--- .../scala/fr/acinq/eclair/NodeParams.scala | 10 +++++++--- .../fr/acinq/eclair/channel/ChannelData.scala | 13 ++++++++----- .../fr/acinq/eclair/channel/Commitments.scala | 4 ++-- .../main/scala/fr/acinq/eclair/io/Peer.scala | 2 +- .../channel/version0/ChannelCodecs0.scala | 2 +- .../channel/version0/ChannelTypes0.scala | 2 +- .../channel/version1/ChannelCodecs1.scala | 2 +- .../channel/version2/ChannelCodecs2.scala | 2 +- .../channel/version3/ChannelCodecs3.scala | 2 +- .../eclair/wire/protocol/CommonCodecs.scala | 3 +++ .../protocol/LightningMessageCodecs.scala | 2 +- .../wire/protocol/LightningMessageTypes.scala | 4 ++-- .../fr/acinq/eclair/EclairImplSpec.scala | 4 ++-- .../scala/fr/acinq/eclair/TestConstants.scala | 6 +++--- .../eclair/channel/CommitmentsSpec.scala | 4 ++-- .../fr/acinq/eclair/channel/FuzzySpec.scala | 2 +- .../ChannelStateTestsHelperMethods.scala | 2 +- .../a/WaitForAcceptChannelStateSpec.scala | 4 ++-- .../a/WaitForOpenChannelStateSpec.scala | 2 +- .../b/WaitForFundingCreatedStateSpec.scala | 2 +- .../b/WaitForFundingInternalStateSpec.scala | 2 +- .../b/WaitForFundingSignedStateSpec.scala | 2 +- .../c/WaitForFundingConfirmedStateSpec.scala | 2 +- .../c/WaitForFundingLockedStateSpec.scala | 2 +- .../channel/states/h/ClosingStateSpec.scala | 2 +- .../integration/PaymentIntegrationSpec.scala | 2 +- .../interop/rustytests/RustyTestsSpec.scala | 2 +- .../scala/fr/acinq/eclair/io/PeerSpec.scala | 2 +- .../eclair/payment/PaymentPacketSpec.scala | 3 ++- .../internal/channel/ChannelCodecsSpec.scala | 19 ++++++++++++++----- .../wire/protocol/CommonCodecsSpec.scala | 18 +++++++++++++++++- .../protocol/LightningMessageCodecsSpec.scala | 6 +++--- .../acinq/eclair/api/handlers/Channel.scala | 6 +++--- 36 files changed, 99 insertions(+), 57 deletions(-) diff --git a/docs/release-notes/eclair-vnext.md b/docs/release-notes/eclair-vnext.md index d5eec51a97..fe805211bb 100644 --- a/docs/release-notes/eclair-vnext.md +++ b/docs/release-notes/eclair-vnext.md @@ -176,6 +176,7 @@ This release contains many other API updates: - `findroute`, `findroutetonode` and `findroutebetweennodes` now accept `--maxFeeMsat` to specify an upper bound of fees (#1969) - `getsentinfo` output includes `failedNode` field for all failed routes - for `payinvoice` and `sendtonode`, `--feeThresholdSat` has been renamed to `--maxFeeFlatSat` +- for `open`, `--channelFlags` has been replaced by `--announceChannel` - the `networkstats` API has been removed Have a look at our [API documentation](https://acinq.github.io/eclair) for more details. diff --git a/eclair-core/src/main/resources/reference.conf b/eclair-core/src/main/resources/reference.conf index 2e382f743e..5d3e88d078 100644 --- a/eclair-core/src/main/resources/reference.conf +++ b/eclair-core/src/main/resources/reference.conf @@ -69,7 +69,12 @@ eclair { # } ] sync-whitelist = [] // a list of public keys; if non-empty, we will only do the initial sync with those peers - channel-flags = 1 // announce channels + + channel { + channel-flags { + announce-channel = true + } + } dust-limit-satoshis = 546 max-remote-dust-limit-satoshis = 600 diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala index 662ff8dc3f..55a5cf9c43 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala @@ -88,7 +88,7 @@ trait Eclair { def disconnect(nodeId: PublicKey)(implicit timeout: Timeout): Future[String] - def open(nodeId: PublicKey, fundingAmount: Satoshi, pushAmount_opt: Option[MilliSatoshi], channelType_opt: Option[SupportedChannelType], fundingFeeratePerByte_opt: Option[FeeratePerByte], flags_opt: Option[Int], openTimeout_opt: Option[Timeout])(implicit timeout: Timeout): Future[ChannelOpenResponse] + def open(nodeId: PublicKey, fundingAmount: Satoshi, pushAmount_opt: Option[MilliSatoshi], channelType_opt: Option[SupportedChannelType], fundingFeeratePerByte_opt: Option[FeeratePerByte], announceChannel_opt: Option[Boolean], openTimeout_opt: Option[Timeout])(implicit timeout: Timeout): Future[ChannelOpenResponse] def close(channels: List[ApiTypes.ChannelIdentifier], scriptPubKey_opt: Option[ByteVector], closingFeerates_opt: Option[ClosingFeerates])(implicit timeout: Timeout): Future[Map[ApiTypes.ChannelIdentifier, Either[Throwable, CommandResponse[CMD_CLOSE]]]] @@ -177,7 +177,7 @@ class EclairImpl(appKit: Kit) extends Eclair with Logging { (appKit.switchboard ? Peer.Disconnect(nodeId)).mapTo[String] } - override def open(nodeId: PublicKey, fundingAmount: Satoshi, pushAmount_opt: Option[MilliSatoshi], channelType_opt: Option[SupportedChannelType], fundingFeeratePerByte_opt: Option[FeeratePerByte], flags_opt: Option[Int], openTimeout_opt: Option[Timeout])(implicit timeout: Timeout): Future[ChannelOpenResponse] = { + override def open(nodeId: PublicKey, fundingAmount: Satoshi, pushAmount_opt: Option[MilliSatoshi], channelType_opt: Option[SupportedChannelType], fundingFeeratePerByte_opt: Option[FeeratePerByte], announceChannel_opt: Option[Boolean], openTimeout_opt: Option[Timeout])(implicit timeout: Timeout): Future[ChannelOpenResponse] = { // we want the open timeout to expire *before* the default ask timeout, otherwise user will get a generic response val openTimeout = openTimeout_opt.getOrElse(Timeout(20 seconds)) (appKit.switchboard ? Peer.OpenChannel( @@ -186,7 +186,7 @@ class EclairImpl(appKit: Kit) extends Eclair with Logging { pushMsat = pushAmount_opt.getOrElse(0 msat), channelType_opt = channelType_opt, fundingTxFeeratePerKw_opt = fundingFeeratePerByte_opt.map(FeeratePerKw(_)), - channelFlags = flags_opt.map(_.toByte), + channelFlags = announceChannel_opt.map(announceChannel => ChannelFlags(announceChannel = announceChannel)), timeout_opt = Some(openTimeout))).mapTo[ChannelOpenResponse] } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala index e931c1d5f7..badf738984 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala @@ -21,7 +21,7 @@ import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.bitcoin.{Block, ByteVector32, Crypto, Satoshi} import fr.acinq.eclair.Setup.Seeds import fr.acinq.eclair.blockchain.fee._ -import fr.acinq.eclair.channel.Channel +import fr.acinq.eclair.channel.{Channel, ChannelFlags} import fr.acinq.eclair.channel.Channel.UnhandledExceptionStrategy import fr.acinq.eclair.crypto.Noise.KeyPair import fr.acinq.eclair.crypto.keymanager.{ChannelKeyManager, NodeKeyManager} @@ -86,7 +86,7 @@ case class NodeParams(nodeKeyManager: NodeKeyManager, initialRandomReconnectDelay: FiniteDuration, maxReconnectInterval: FiniteDuration, chainHash: ByteVector32, - channelFlags: Byte, + channelFlags: ChannelFlags, watchSpentWindow: FiniteDuration, paymentRequestExpiry: FiniteDuration, multiPartPaymentExpiry: FiniteDuration, @@ -220,6 +220,8 @@ object NodeParams extends Logging { "router.path-finding.ratio-channel-capacity" -> "router.path-finding.default.ratios.channel-capacity", "router.path-finding.hop-cost-base-msat" -> "router.path-finding.default.hop-cost.fee-base-msat", "router.path-finding.hop-cost-millionths" -> "router.path-finding.default.hop-cost.fee-proportional-millionths", + // v0.6.3 + "channel-flags" -> "channel.channel-flags", ) deprecatedKeyPaths.foreach { case (old, new_) => require(!config.hasPath(old), s"configuration key '$old' has been replaced by '$new_'") @@ -232,6 +234,8 @@ object NodeParams extends Logging { val chain = config.getString("chain") val chainHash = hashFromChain(chain) + val channelFlags = ChannelFlags(announceChannel = config.getBoolean("channel.channel-flags.announce-channel")) + val color = ByteVector.fromValidHex(config.getString("node-color")) require(color.size == 3, "color should be a 3-bytes hex buffer") @@ -449,7 +453,7 @@ object NodeParams extends Logging { initialRandomReconnectDelay = FiniteDuration(config.getDuration("initial-random-reconnect-delay").getSeconds, TimeUnit.SECONDS), maxReconnectInterval = FiniteDuration(config.getDuration("max-reconnect-interval").getSeconds, TimeUnit.SECONDS), chainHash = chainHash, - channelFlags = config.getInt("channel-flags").toByte, + channelFlags = channelFlags, watchSpentWindow = watchSpentWindow, paymentRequestExpiry = FiniteDuration(config.getDuration("payment-request-expiry").getSeconds, TimeUnit.SECONDS), multiPartPaymentExpiry = FiniteDuration(config.getDuration("multi-part-payment-expiry").getSeconds, TimeUnit.SECONDS), diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala index 13ac0b1462..441b6bddc5 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala @@ -84,7 +84,7 @@ case class INPUT_INIT_FUNDER(temporaryChannelId: ByteVector32, localParams: LocalParams, remote: ActorRef, remoteInit: Init, - channelFlags: Byte, + channelFlags: ChannelFlags, channelConfig: ChannelConfig, channelType: SupportedChannelType) case class INPUT_INIT_FUNDEE(temporaryChannelId: ByteVector32, @@ -400,7 +400,7 @@ final case class DATA_WAIT_FOR_FUNDING_CREATED(temporaryChannelId: ByteVector32, pushAmount: MilliSatoshi, initialFeeratePerKw: FeeratePerKw, remoteFirstPerCommitmentPoint: PublicKey, - channelFlags: Byte, + channelFlags: ChannelFlags, channelConfig: ChannelConfig, channelFeatures: ChannelFeatures, lastSent: AcceptChannel) extends ChannelData { @@ -414,7 +414,7 @@ final case class DATA_WAIT_FOR_FUNDING_SIGNED(channelId: ByteVector32, localSpec: CommitmentSpec, localCommitTx: CommitTx, remoteCommit: RemoteCommit, - channelFlags: Byte, + channelFlags: ChannelFlags, channelConfig: ChannelConfig, channelFeatures: ChannelFeatures, lastSent: FundingCreated) extends ChannelData @@ -492,8 +492,11 @@ case class RemoteParams(nodeId: PublicKey, initFeatures: Features, shutdownScript: Option[ByteVector]) +case class ChannelFlags(announceChannel: Boolean) { + override def toString: String = s"ChannelFlags(announceChannel=$announceChannel)" +} object ChannelFlags { - val AnnounceChannel = 0x01.toByte - val Empty = 0x00.toByte + val Private: ChannelFlags = ChannelFlags(announceChannel = false) + val Public: ChannelFlags = ChannelFlags(announceChannel = true) } // @formatter:on diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala index f8ba682fe3..5b483b21b5 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala @@ -74,7 +74,7 @@ case class Commitments(channelId: ByteVector32, channelConfig: ChannelConfig, channelFeatures: ChannelFeatures, localParams: LocalParams, remoteParams: RemoteParams, - channelFlags: Byte, + channelFlags: ChannelFlags, localCommit: LocalCommit, remoteCommit: RemoteCommit, localChanges: LocalChanges, remoteChanges: RemoteChanges, localNextHtlcId: Long, remoteNextHtlcId: Long, @@ -204,7 +204,7 @@ case class Commitments(channelId: ByteVector32, val remoteNodeId: PublicKey = remoteParams.nodeId - val announceChannel: Boolean = (channelFlags & 0x01) != 0 + val announceChannel: Boolean = channelFlags.announceChannel val capacity: Satoshi = commitInput.txOut.amount diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala index 55da17df76..0bf41b50de 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala @@ -465,7 +465,7 @@ object Peer { } case class Disconnect(nodeId: PublicKey) extends PossiblyHarmful - case class OpenChannel(remoteNodeId: PublicKey, fundingSatoshis: Satoshi, pushMsat: MilliSatoshi, channelType_opt: Option[SupportedChannelType], fundingTxFeeratePerKw_opt: Option[FeeratePerKw], channelFlags: Option[Byte], timeout_opt: Option[Timeout]) extends PossiblyHarmful { + case class OpenChannel(remoteNodeId: PublicKey, fundingSatoshis: Satoshi, pushMsat: MilliSatoshi, channelType_opt: Option[SupportedChannelType], fundingTxFeeratePerKw_opt: Option[FeeratePerKw], channelFlags: Option[ChannelFlags], timeout_opt: Option[Timeout]) extends PossiblyHarmful { require(pushMsat <= fundingSatoshis, s"pushMsat must be less or equal to fundingSatoshis") require(fundingSatoshis >= 0.sat, s"fundingSatoshis must be positive") require(pushMsat >= 0.msat, s"pushMsat must be positive") diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelCodecs0.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelCodecs0.scala index 602662aa2f..d7f8010b70 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelCodecs0.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelCodecs0.scala @@ -272,7 +272,7 @@ private[channel] object ChannelCodecs0 { ("channelVersion" | channelVersionCodec) >>:~ { channelVersion => ("localParams" | localParamsCodec(channelVersion)) :: ("remoteParams" | remoteParamsCodec) :: - ("channelFlags" | byte) :: + ("channelFlags" | channelflags) :: ("localCommit" | localCommitCodec) :: ("remoteCommit" | remoteCommitCodec) :: ("localChanges" | localChangesCodec) :: diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelTypes0.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelTypes0.scala index f136208bb3..fbf34c097e 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelTypes0.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelTypes0.scala @@ -181,7 +181,7 @@ private[channel] object ChannelTypes0 { case class Commitments(channelVersion: ChannelVersion, localParams: LocalParams, remoteParams: RemoteParams, - channelFlags: Byte, + channelFlags: ChannelFlags, localCommit: LocalCommit, remoteCommit: RemoteCommit, localChanges: LocalChanges, remoteChanges: RemoteChanges, localNextHtlcId: Long, remoteNextHtlcId: Long, diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version1/ChannelCodecs1.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version1/ChannelCodecs1.scala index 9658868cc5..4247bd7235 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version1/ChannelCodecs1.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version1/ChannelCodecs1.scala @@ -188,7 +188,7 @@ private[channel] object ChannelCodecs1 { ("channelVersion" | channelVersionCodec) >>:~ { channelVersion => ("localParams" | localParamsCodec(channelVersion)) :: ("remoteParams" | remoteParamsCodec) :: - ("channelFlags" | byte) :: + ("channelFlags" | channelflags) :: ("localCommit" | localCommitCodec) :: ("remoteCommit" | remoteCommitCodec) :: ("localChanges" | localChangesCodec) :: diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version2/ChannelCodecs2.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version2/ChannelCodecs2.scala index 25e8f7e831..52e776f29e 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version2/ChannelCodecs2.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version2/ChannelCodecs2.scala @@ -223,7 +223,7 @@ private[channel] object ChannelCodecs2 { ("channelVersion" | channelVersionCodec) >>:~ { channelVersion => ("localParams" | localParamsCodec(channelVersion)) :: ("remoteParams" | remoteParamsCodec) :: - ("channelFlags" | byte) :: + ("channelFlags" | channelflags) :: ("localCommit" | localCommitCodec) :: ("remoteCommit" | remoteCommitCodec) :: ("localChanges" | localChangesCodec) :: diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3.scala index 0dde1cd9b2..6528016cf5 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3.scala @@ -266,7 +266,7 @@ private[channel] object ChannelCodecs3 { (("channelFeatures" | channelFeaturesCodec) >>:~ { channelFeatures => ("localParams" | localParamsCodec(channelFeatures)) :: ("remoteParams" | remoteParamsCodec) :: - ("channelFlags" | byte) :: + ("channelFlags" | channelflags) :: ("localCommit" | localCommitCodec) :: ("remoteCommit" | remoteCommitCodec) :: ("localChanges" | localChangesCodec) :: diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/CommonCodecs.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/CommonCodecs.scala index 344e0e0e70..13cb0a41c7 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/CommonCodecs.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/CommonCodecs.scala @@ -19,6 +19,7 @@ package fr.acinq.eclair.wire.protocol import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} import fr.acinq.bitcoin.{ByteVector32, ByteVector64, Satoshi} import fr.acinq.eclair.blockchain.fee.FeeratePerKw +import fr.acinq.eclair.channel.ChannelFlags import fr.acinq.eclair.crypto.Mac32 import fr.acinq.eclair.{BlockHeight, CltvExpiry, CltvExpiryDelta, MilliSatoshi, ShortChannelId, TimestampSecond, UInt64} import org.apache.commons.codec.binary.Base32 @@ -109,6 +110,8 @@ object CommonCodecs { val listofsignatures: Codec[List[ByteVector64]] = listOfN(uint16, bytes64) + val channelflags: Codec[ChannelFlags] = (ignore(7) dropLeft bool).as[ChannelFlags] + val ipv4address: Codec[Inet4Address] = bytes(4).xmap(b => InetAddress.getByAddress(b.toArray).asInstanceOf[Inet4Address], a => ByteVector(a.getAddress)) val ipv6address: Codec[Inet6Address] = bytes(16).exmap(b => Attempt.fromTry(Try(Inet6Address.getByAddress(null, b.toArray, null))), a => Attempt.fromTry(Try(ByteVector(a.getAddress)))) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecs.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecs.scala index 3299b44f80..94cfb4c7a3 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecs.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecs.scala @@ -91,7 +91,7 @@ object LightningMessageCodecs { ("delayedPaymentBasepoint" | publicKey) :: ("htlcBasepoint" | publicKey) :: ("firstPerCommitmentPoint" | publicKey) :: - ("channelFlags" | byte) :: + ("channelFlags" | channelflags) :: ("tlvStream" | OpenChannelTlv.openTlvCodec)).as[OpenChannel] val acceptChannelCodec: Codec[AcceptChannel] = ( diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageTypes.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageTypes.scala index ff88977696..6420e94ab0 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageTypes.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageTypes.scala @@ -20,7 +20,7 @@ import com.google.common.base.Charsets import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} import fr.acinq.bitcoin.{ByteVector32, ByteVector64, Satoshi} import fr.acinq.eclair.blockchain.fee.FeeratePerKw -import fr.acinq.eclair.channel.ChannelType +import fr.acinq.eclair.channel.{ChannelFlags, ChannelType} import fr.acinq.eclair.{BlockHeight, CltvExpiry, CltvExpiryDelta, Features, MilliSatoshi, ShortChannelId, TimestampSecond, UInt64} import scodec.bits.ByteVector @@ -102,7 +102,7 @@ case class OpenChannel(chainHash: ByteVector32, delayedPaymentBasepoint: PublicKey, htlcBasepoint: PublicKey, firstPerCommitmentPoint: PublicKey, - channelFlags: Byte, + channelFlags: ChannelFlags, tlvStream: TlvStream[OpenChannelTlv] = TlvStream.empty) extends ChannelMessage with HasTemporaryChannelId with HasChainHash { val upfrontShutdownScript_opt: Option[ByteVector] = tlvStream.get[ChannelTlv.UpfrontShutdownScriptTlv].map(_.script) val channelType_opt: Option[ChannelType] = tlvStream.get[ChannelTlv.ChannelTypeTlv].map(_.channelType) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/EclairImplSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/EclairImplSpec.scala index b34920df1d..eb01e19300 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/EclairImplSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/EclairImplSpec.scala @@ -95,12 +95,12 @@ class EclairImplSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with I val nodeId = PublicKey(hex"030bb6a5e0c6b203c7e2180fb78c7ba4bdce46126761d8201b91ddac089cdecc87") // standard conversion - eclair.open(nodeId, fundingAmount = 10000000L sat, pushAmount_opt = None, channelType_opt = None, fundingFeeratePerByte_opt = Some(FeeratePerByte(5 sat)), flags_opt = None, openTimeout_opt = None) + eclair.open(nodeId, fundingAmount = 10000000L sat, pushAmount_opt = None, channelType_opt = None, fundingFeeratePerByte_opt = Some(FeeratePerByte(5 sat)), announceChannel_opt = None, openTimeout_opt = None) val open = switchboard.expectMsgType[OpenChannel] assert(open.fundingTxFeeratePerKw_opt === Some(FeeratePerKw(1250 sat))) // check that minimum fee rate of 253 sat/bw is used - eclair.open(nodeId, fundingAmount = 10000000L sat, pushAmount_opt = None, channelType_opt = Some(ChannelTypes.StaticRemoteKey), fundingFeeratePerByte_opt = Some(FeeratePerByte(1 sat)), flags_opt = None, openTimeout_opt = None) + eclair.open(nodeId, fundingAmount = 10000000L sat, pushAmount_opt = None, channelType_opt = Some(ChannelTypes.StaticRemoteKey), fundingFeeratePerByte_opt = Some(FeeratePerByte(1 sat)), announceChannel_opt = None, openTimeout_opt = None) val open1 = switchboard.expectMsgType[OpenChannel] assert(open1.fundingTxFeeratePerKw_opt === Some(FeeratePerKw.MinimumFeeratePerKw)) assert(open1.channelType_opt === Some(ChannelTypes.StaticRemoteKey)) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala b/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala index 55f31f0716..488c6363e4 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala @@ -21,7 +21,7 @@ import fr.acinq.eclair.FeatureSupport.{Mandatory, Optional} import fr.acinq.eclair.Features._ import fr.acinq.eclair.blockchain.fee._ import fr.acinq.eclair.channel.Channel.UnhandledExceptionStrategy -import fr.acinq.eclair.channel.LocalParams +import fr.acinq.eclair.channel.{ChannelFlags, LocalParams} import fr.acinq.eclair.crypto.keymanager.{LocalChannelKeyManager, LocalNodeKeyManager} import fr.acinq.eclair.io.MessageRelay.RelayAll import fr.acinq.eclair.io.{Peer, PeerConnection} @@ -139,7 +139,7 @@ object TestConstants { initialRandomReconnectDelay = 5 seconds, maxReconnectInterval = 1 hour, chainHash = Block.RegtestGenesisBlock.hash, - channelFlags = 1, + channelFlags = ChannelFlags.Public, watchSpentWindow = 1 second, paymentRequestExpiry = 1 hour, multiPartPaymentExpiry = 30 seconds, @@ -272,7 +272,7 @@ object TestConstants { initialRandomReconnectDelay = 5 seconds, maxReconnectInterval = 1 hour, chainHash = Block.RegtestGenesisBlock.hash, - channelFlags = 1, + channelFlags = ChannelFlags.Public, watchSpentWindow = 1 second, paymentRequestExpiry = 1 hour, multiPartPaymentExpiry = 30 seconds, diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/CommitmentsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/CommitmentsSpec.scala index 0ac2406757..3407b31375 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/CommitmentsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/CommitmentsSpec.scala @@ -483,7 +483,7 @@ object CommitmentsSpec { ChannelFeatures(), localParams, remoteParams, - channelFlags = if (announceChannel) ChannelFlags.AnnounceChannel else ChannelFlags.Empty, + channelFlags = ChannelFlags(announceChannel = announceChannel), LocalCommit(0, CommitmentSpec(Set.empty, feeRatePerKw, toLocal, toRemote), CommitTxAndRemoteSig(CommitTx(commitmentInput, Transaction(2, Nil, Nil, 0)), ByteVector64.Zeroes), Nil), RemoteCommit(0, CommitmentSpec(Set.empty, feeRatePerKw, toRemote, toLocal), randomBytes32(), randomKey().publicKey), LocalChanges(Nil, Nil, Nil), @@ -506,7 +506,7 @@ object CommitmentsSpec { ChannelFeatures(), localParams, remoteParams, - channelFlags = if (announceChannel) ChannelFlags.AnnounceChannel else ChannelFlags.Empty, + channelFlags = ChannelFlags(announceChannel = announceChannel), LocalCommit(0, CommitmentSpec(Set.empty, FeeratePerKw(0 sat), toLocal, toRemote), CommitTxAndRemoteSig(CommitTx(commitmentInput, Transaction(2, Nil, Nil, 0)), ByteVector64.Zeroes), Nil), RemoteCommit(0, CommitmentSpec(Set.empty, FeeratePerKw(0 sat), toRemote, toLocal), randomBytes32(), randomKey().publicKey), LocalChanges(Nil, Nil, Nil), diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/FuzzySpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/FuzzySpec.scala index 28fd3dae94..814ea3fbf9 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/FuzzySpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/FuzzySpec.scala @@ -79,7 +79,7 @@ class FuzzySpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Channe registerA ! alice registerB ! bob // no announcements - alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, Alice.channelParams, pipe, bobInit, channelFlags = 0x00.toByte, ChannelConfig.standard, ChannelTypes.Standard) + alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, Alice.channelParams, pipe, bobInit, channelFlags = ChannelFlags.Private, ChannelConfig.standard, ChannelTypes.Standard) alice2blockchain.expectMsgType[TxPublisher.SetChannelId] bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, Bob.channelParams, pipe, aliceInit, ChannelConfig.standard, ChannelTypes.Standard) bob2blockchain.expectMsgType[TxPublisher.SetChannelId] diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala index d9625321dc..55a88459f4 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala @@ -181,7 +181,7 @@ trait ChannelStateTestsHelperMethods extends TestKitBase { val channelConfig = ChannelConfig.standard val (aliceParams, bobParams, channelType) = computeFeatures(setup, tags) - val channelFlags = if (tags.contains(ChannelStateTestsTags.ChannelsPublic)) ChannelFlags.AnnounceChannel else ChannelFlags.Empty + val channelFlags = ChannelFlags(announceChannel = tags.contains(ChannelStateTestsTags.ChannelsPublic)) val initialFeeratePerKw = if (tags.contains(ChannelStateTestsTags.AnchorOutputs) || tags.contains(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) TestConstants.anchorOutputsFeeratePerKw else TestConstants.feeratePerKw val (fundingSatoshis, pushMsat) = if (tags.contains(ChannelStateTestsTags.NoPushMsat)) { (TestConstants.fundingSatoshis, 0.msat) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForAcceptChannelStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForAcceptChannelStateSpec.scala index cc5138ac00..f6c4502dc4 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForAcceptChannelStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForAcceptChannelStateSpec.scala @@ -62,7 +62,7 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS val bobInit = Init(bobParams.initFeatures) within(30 seconds) { val fundingAmount = if (test.tags.contains(ChannelStateTestsTags.Wumbo)) Btc(5).toSatoshi else TestConstants.fundingSatoshis - alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, fundingAmount, TestConstants.pushMsat, initialFeeratePerKw, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, ChannelFlags.Empty, channelConfig, channelType) + alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, fundingAmount, TestConstants.pushMsat, initialFeeratePerKw, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, ChannelFlags.Private, channelConfig, channelType) bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType) alice2bob.expectMsgType[OpenChannel] alice2bob.forward(bob) @@ -155,7 +155,7 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS // Bob advertises support for anchor outputs, but Alice doesn't. val aliceParams = Alice.channelParams val bobParams = Bob.channelParams.copy(initFeatures = Features(Features.StaticRemoteKey -> FeatureSupport.Optional, Features.AnchorOutputs -> FeatureSupport.Optional)) - alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.anchorOutputsFeeratePerKw, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, Init(bobParams.initFeatures), ChannelFlags.Empty, channelConfig, ChannelTypes.AnchorOutputs) + alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.anchorOutputsFeeratePerKw, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, Init(bobParams.initFeatures), ChannelFlags.Private, channelConfig, ChannelTypes.AnchorOutputs) bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, bobParams, bob2alice.ref, Init(bobParams.initFeatures), channelConfig, ChannelTypes.AnchorOutputs) val open = alice2bob.expectMsgType[OpenChannel] assert(open.channelType_opt === Some(ChannelTypes.AnchorOutputs)) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForOpenChannelStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForOpenChannelStateSpec.scala index caeb6da5a5..20284df76c 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForOpenChannelStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForOpenChannelStateSpec.scala @@ -55,7 +55,7 @@ class WaitForOpenChannelStateSpec extends TestKitBaseClass with FixtureAnyFunSui val aliceInit = Init(aliceParams.initFeatures) val bobInit = Init(bobParams.initFeatures) within(30 seconds) { - alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, initialFeeratePerKw, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, ChannelFlags.Empty, channelConfig, channelType) + alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, initialFeeratePerKw, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, ChannelFlags.Private, channelConfig, channelType) bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType) awaitCond(bob.stateName == WAIT_FOR_OPEN_CHANNEL) withFixture(test.toNoArgTest(FixtureParam(alice, bob, alice2bob, bob2alice, bob2blockchain))) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingCreatedStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingCreatedStateSpec.scala index 6c2819db0c..1967d53e5e 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingCreatedStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingCreatedStateSpec.scala @@ -63,7 +63,7 @@ class WaitForFundingCreatedStateSpec extends TestKitBaseClass with FixtureAnyFun val aliceInit = Init(aliceParams.initFeatures) val bobInit = Init(bobParams.initFeatures) within(30 seconds) { - alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, fundingSatoshis, pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, ChannelFlags.Empty, channelConfig, channelType) + alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, fundingSatoshis, pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, ChannelFlags.Private, channelConfig, channelType) alice2blockchain.expectMsgType[TxPublisher.SetChannelId] bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType) bob2blockchain.expectMsgType[TxPublisher.SetChannelId] diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingInternalStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingInternalStateSpec.scala index 93e1684c8f..6c6116e99d 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingInternalStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingInternalStateSpec.scala @@ -46,7 +46,7 @@ class WaitForFundingInternalStateSpec extends TestKitBaseClass with FixtureAnyFu val aliceInit = Init(aliceParams.initFeatures) val bobInit = Init(bobParams.initFeatures) within(30 seconds) { - alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, ChannelFlags.Empty, channelConfig, channelType) + alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, ChannelFlags.Private, channelConfig, channelType) bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType) alice2bob.expectMsgType[OpenChannel] alice2bob.forward(bob) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingSignedStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingSignedStateSpec.scala index 1a8d538b9d..fb6d20a29e 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingSignedStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingSignedStateSpec.scala @@ -62,7 +62,7 @@ class WaitForFundingSignedStateSpec extends TestKitBaseClass with FixtureAnyFunS val aliceInit = Init(aliceParams.initFeatures) val bobInit = Init(bobParams.initFeatures) within(30 seconds) { - alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, fundingSatoshis, pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, ChannelFlags.Empty, channelConfig, channelType) + alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, fundingSatoshis, pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, ChannelFlags.Private, channelConfig, channelType) alice2blockchain.expectMsgType[TxPublisher.SetChannelId] bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType) bob2blockchain.expectMsgType[TxPublisher.SetChannelId] diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingConfirmedStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingConfirmedStateSpec.scala index ab3a5ed684..b5a7c01326 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingConfirmedStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingConfirmedStateSpec.scala @@ -52,7 +52,7 @@ class WaitForFundingConfirmedStateSpec extends TestKitBaseClass with FixtureAnyF within(30 seconds) { val listener = TestProbe() system.eventStream.subscribe(listener.ref, classOf[TransactionPublished]) - alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, ChannelFlags.Empty, channelConfig, channelType) + alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, ChannelFlags.Private, channelConfig, channelType) alice2blockchain.expectMsgType[TxPublisher.SetChannelId] bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType) bob2blockchain.expectMsgType[TxPublisher.SetChannelId] diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingLockedStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingLockedStateSpec.scala index 933f063294..67954c4ca7 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingLockedStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingLockedStateSpec.scala @@ -49,7 +49,7 @@ class WaitForFundingLockedStateSpec extends TestKitBaseClass with FixtureAnyFunS val bobInit = Init(bobParams.initFeatures) within(30 seconds) { alice.underlyingActor.nodeParams.db.peers.addOrUpdateRelayFees(bobParams.nodeId, relayFees) - alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, ChannelFlags.Empty, channelConfig, channelType) + alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, ChannelFlags.Private, channelConfig, channelType) alice2blockchain.expectMsgType[TxPublisher.SetChannelId] bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType) bob2blockchain.expectMsgType[TxPublisher.SetChannelId] diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala index 9a50a368c2..f9c76c7656 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala @@ -67,7 +67,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val (aliceParams, bobParams, channelType) = computeFeatures(setup, test.tags) val aliceInit = Init(aliceParams.initFeatures) val bobInit = Init(bobParams.initFeatures) - alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, ChannelFlags.Empty, channelConfig, channelType) + alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, ChannelFlags.Private, channelConfig, channelType) alice2blockchain.expectMsgType[SetChannelId] bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType) bob2blockchain.expectMsgType[SetChannelId] diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/PaymentIntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/PaymentIntegrationSpec.scala index 61e0316126..4bedba0d95 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/PaymentIntegrationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/PaymentIntegrationSpec.scala @@ -58,7 +58,7 @@ import scala.jdk.CollectionConverters._ class PaymentIntegrationSpec extends IntegrationSpec { test("start eclair nodes") { - instantiateEclairNode("A", ConfigFactory.parseMap(Map("eclair.node-alias" -> "A", "eclair.expiry-delta-blocks" -> 130, "eclair.server.port" -> 29730, "eclair.api.port" -> 28080, "eclair.channel-flags" -> 0).asJava).withFallback(withDefaultCommitment).withFallback(commonConfig)) // A's channels are private + instantiateEclairNode("A", ConfigFactory.parseMap(Map("eclair.node-alias" -> "A", "eclair.expiry-delta-blocks" -> 130, "eclair.server.port" -> 29730, "eclair.api.port" -> 28080, "eclair.channel.channel-flags.announce-channel" -> false).asJava).withFallback(withDefaultCommitment).withFallback(commonConfig)) // A's channels are private instantiateEclairNode("B", ConfigFactory.parseMap(Map("eclair.node-alias" -> "B", "eclair.expiry-delta-blocks" -> 131, "eclair.server.port" -> 29731, "eclair.api.port" -> 28081, "eclair.trampoline-payments-enable" -> true).asJava).withFallback(withDefaultCommitment).withFallback(commonConfig)) instantiateEclairNode("C", ConfigFactory.parseMap(Map("eclair.node-alias" -> "C", "eclair.expiry-delta-blocks" -> 132, "eclair.server.port" -> 29732, "eclair.api.port" -> 28082, "eclair.trampoline-payments-enable" -> true).asJava).withFallback(withAnchorOutputsZeroFeeHtlcTxs).withFallback(commonConfig)) instantiateEclairNode("D", ConfigFactory.parseMap(Map("eclair.node-alias" -> "D", "eclair.expiry-delta-blocks" -> 133, "eclair.server.port" -> 29733, "eclair.api.port" -> 28083, "eclair.trampoline-payments-enable" -> true).asJava).withFallback(withDefaultCommitment).withFallback(commonConfig)) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/interop/rustytests/RustyTestsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/interop/rustytests/RustyTestsSpec.scala index 4dedaa1129..bf4015238b 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/interop/rustytests/RustyTestsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/interop/rustytests/RustyTestsSpec.scala @@ -74,7 +74,7 @@ class RustyTestsSpec extends TestKitBaseClass with Matchers with FixtureAnyFunSu val bobInit = Init(Bob.channelParams.initFeatures) // alice and bob will both have 1 000 000 sat feeEstimator.setFeerate(FeeratesPerKw.single(FeeratePerKw(10000 sat))) - alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, 2000000 sat, 1000000000 msat, feeEstimator.getFeeratePerKw(target = 2), feeEstimator.getFeeratePerKw(target = 6), Alice.channelParams, pipe, bobInit, ChannelFlags.Empty, channelConfig, channelType) + alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, 2000000 sat, 1000000000 msat, feeEstimator.getFeeratePerKw(target = 2), feeEstimator.getFeeratePerKw(target = 6), Alice.channelParams, pipe, bobInit, ChannelFlags.Private, channelConfig, channelType) alice2blockchain.expectMsgType[TxPublisher.SetChannelId] bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, Bob.channelParams, pipe, aliceInit, channelConfig, channelType) bob2blockchain.expectMsgType[TxPublisher.SetChannelId] diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala index d30af3c8a2..cb270d72f5 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala @@ -534,7 +534,7 @@ object PeerSpec { } def createOpenChannelMessage(openTlv: TlvStream[OpenChannelTlv] = TlvStream.empty): protocol.OpenChannel = { - protocol.OpenChannel(Block.RegtestGenesisBlock.hash, randomBytes32(), 25000 sat, 0 msat, 483 sat, UInt64(100), 1000 sat, 1 msat, TestConstants.feeratePerKw, CltvExpiryDelta(144), 10, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, 0, openTlv) + protocol.OpenChannel(Block.RegtestGenesisBlock.hash, randomBytes32(), 25000 sat, 0 msat, 483 sat, UInt64(100), 1000 sat, 1 msat, TestConstants.feeratePerKw, CltvExpiryDelta(144), 10, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, ChannelFlags.Private, openTlv) } } \ No newline at end of file diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentPacketSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentPacketSpec.scala index 9fd9c8eec1..91127ac598 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentPacketSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentPacketSpec.scala @@ -371,7 +371,8 @@ object PaymentPacketSpec { val params = LocalParams(null, null, null, null, null, null, null, 0, isFunder = true, null, None, null) val remoteParams = RemoteParams(randomKey().publicKey, null, null, null, null, null, maxAcceptedHtlcs = 0, null, null, null, null, null, null, None) val commitInput = InputInfo(OutPoint(randomBytes32(), 1), TxOut(testCapacity, Nil), Nil) - new Commitments(channelId, ChannelConfig.standard, ChannelFeatures(), params, remoteParams, 0.toByte, null, null, null, null, 0, 0, Map.empty, null, commitInput, null) { + val channelFlags = ChannelFlags.Private + new Commitments(channelId, ChannelConfig.standard, ChannelFeatures(), params, remoteParams, channelFlags, null, null, null, null, 0, 0, Map.empty, null, commitInput, null) { override lazy val availableBalanceForSend: MilliSatoshi = testAvailableBalanceForSend.max(0 msat) override lazy val availableBalanceForReceive: MilliSatoshi = testAvailableBalanceForReceive.max(0 msat) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/ChannelCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/ChannelCodecsSpec.scala index 7ae0a93c94..d9f093c086 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/ChannelCodecsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/ChannelCodecsSpec.scala @@ -29,10 +29,12 @@ import fr.acinq.eclair.router.Announcements import fr.acinq.eclair.transactions.Transactions.{AnchorOutputsCommitmentFormat, CommitTx, TxOwner} import fr.acinq.eclair.transactions._ import fr.acinq.eclair.wire.internal.channel.ChannelCodecs._ -import fr.acinq.eclair.wire.protocol.UpdateAddHtlc +import fr.acinq.eclair.wire.protocol.{CommonCodecs, UpdateAddHtlc} import org.json4s.jackson.Serialization import org.scalatest.funsuite.AnyFunSuite +import scodec.DecodeResult import scodec.bits._ +import scodec.codecs.byte import java.util.UUID import scala.io.Source @@ -71,6 +73,12 @@ class ChannelCodecsSpec extends AnyFunSuite { assert(ref === sigs) } + test("nonreg for channel flags codec") { + // make sure that we correctly decode data encoded with the previous standard 'byte' codec + assert(CommonCodecs.channelflags.decode(byte.encode(0).require).require === DecodeResult(ChannelFlags(announceChannel = false), BitVector.empty)) + assert(CommonCodecs.channelflags.decode(byte.encode(1).require).require === DecodeResult(ChannelFlags(announceChannel = true), BitVector.empty)) + } + test("backward compatibility DATA_WAIT_FOR_FUNDING_CONFIRMED_COMPAT_01_Codec") { // this is a DATA_WAIT_FOR_FUNDING_CONFIRMED encoded with the previous version of the codec (at commit 997aceea82942769649bf03e95c5ea549a7beb0c) val bin_old = hex"000001033785fe882e8682340d11df42213f182755531bd587ae305e0062f563a52d841800049b86343ecd1baa49e0304a6e32dddeb50000000000000222000000012a05f2000000000000004e2000000000000000010090001e800bd48a66494d6275d73cc4b8e167bf840c9261a471271ec380000000c501c99c425578eb58841cbf2f7f2e435e796c654697b8076d4cedc90a7e1389589a0000000000000111000000009502f900000000000000271000000000000000008048000f01419f088c6cd366cadb656148952b730fdf73ccbcf030758c008e6a900f756bfb011fa7aae5886b9c34c5f264d996a7a1def7566424c0f90db8b688794b9ca43db8017331002fd85b09961fa68a1a6c2bc995e717ecf99f10c428a79457a46ce5d47b01325bcce8670aa8e0373c279531553ef280791c283189b1acdee55c21d239cdd90196e7a6fc79329b936e87a07b2d429e7b61ff89db92a4c66ec70e2562a14664570000000140410080000000000000000000000002ee000000003b9aca000000000000000000001278970eefbc9e9c7f44d0ab40555711618404aebd87d861ceca9867ce4f7bfe5d000000000015c0420f0000000000110010326963ce09728e7b80fd51c6d6b7f64d6e124cf55c0d2e887bc862499e325da00023a91081419f088c6cd366cadb656148952b730fdf73ccbcf030758c008e6a900f756bfb1081484d9633cfc4a9ab9f4220a7b3c679e648f18e53a7dadb7154242943b587415aa957009d81000000000080f8970eefbc9e9c7f44d0ab40555711618404aebd87d861ceca9867ce4f7bfe5d00000000007b7ef94000a1400f000000000011001013dc34f3aebaa16836581958168fcc55a5719f1a0c312ea9033c110afb4c677c82002418228110806fb9c53b82a46021cf2b48a18dc49434e9d207f3bff2f09c84e5df5f9526057481100c5bab065bd059dafb7ccef1ce14a850a4bb206b132b53cb9df70faa5be8812e00a39822011021c7d70a781a0ceb7605bed811e9aacdbff8c71f2904d54a7f57a46bd735c9f901101b6fde320e8cc9388538ab65bb2f55ac7cec1218163ec0d590e13ac3fbd7d60e80a3a91081419f088c6cd366cadb656148952b730fdf73ccbcf030758c008e6a900f756bfb1081484d9633cfc4a9ab9f4220a7b3c679e648f18e53a7dadb7154242943b587415aa95770612810000000000000000000000000000002ee0000000000000000000000003b9aca0030e78713eebb77ebfb57a70dee19a85a4e54a11fdf74c9fda7fc108ceaa942db8133978aafdb8e7232a61dca8495134873c1c9ceebb926c85256f432b73b111a9f00000000000000000000000000000000000000000000000000000000000040f5a7aef68b8bc802d20427a43145bfbeded7d322c363f661569a38c58064da6700093c4b8777de4f4e3fa26855a02aab88b0c202575ec3ec30e7654c33e727bdff2e80000000000ae0210780000000000880081934b1e704b9473dc07ea8e36b5bfb26b709267aae0697443de43124cf192ed00011d48840a0cf84463669b3656db2b0a44a95b987efb9e65e78183ac60047354807bab5fd8840a426cb19e7e254d5cfa11053d9e33cf32478c729d3ed6db8aa1214a1dac3a0ad54ab80001e25c3bbef27a71fd1342ad01555c45861012baf61f61873b2a619f393deff9740237cd8e4ea0093bb773bab03a938d5e35e9ebbb5b321ffd7ee031feff96eb82f8970eefbc9e9c7f44d0ab40555711618404aebd87d861ceca9867ce4f7bfe5d000006b0aade318a54c3339808654a781d421412758912d2df1b039399ffed52d3b784f698e9230da056f09212e8ac5cebe7ab1283c08aa3cd548ed5e1a4f81ebfe10" @@ -126,11 +134,11 @@ class ChannelCodecsSpec extends AnyFunSuite { // this test makes sure that we actually produce the same objects than previous versions of eclair val refs = Map( hex"00000303933884AAF1D6B108397E5EFE5C86BCF2D8CA8D2F700EDA99DB9214FC2712B134000456E4167E3C0EB8C856C79CA31C97C0AA0000000000000222000000012A05F2000000000000028F5C000000000000000102D0001E000BD48A2402E80B723C42EE3E42938866EC6686ABB7ABF64380000000C501A7F2974C5074E9E10DBB3F0D9B8C40932EC63ABC610FAD7EB6B21C6D081A459B000000000000011E80000001EEFFFE5C00000000000147AE00000000000001F403F000F18146779F781067ED04B4957E14F1C5623AB653039B2B1D49910240848E4E682DB20131AD64F76FAF90CD7DE26892F1BDAB82FB9E02EF6538D82FF4204B5348F02AE081A5388E9474769D69C4F60A763AE0CCDB5228A06281DE64408871A927297FDFD8818B6383985ABD4F0AC22E73791CF3A4D63C592FA2648242D34B8334B1539E823381BB1F1404C37D9C2318F5FC6B1BF7ECF5E6835B779E3BE09BADCF6DF1F51DCFBC80000000C0808000000000000EFD80000000007F00000000061A0A4880000001EDE5F3C3801203B9C79160B5123CADE575E370480B57B0C816EB35DD7B27372EDA858622EB1E808000000015FFFFFF800000000011001029DFB814F6502A68D6F83B6049E3D2948A2080084083750626532FDB437169C20023A9108146779F781067ED04B4957E14F1C5623AB653039B2B1D49910240848E4E682DB21081B30694071254D8B3B9537320C014B8CB1052E5514F5EFC19CF2EB806308D5CF1A95700AD0100000000008083B9C79160B5123CADE575E370480B57B0C816EB35DD7B27372EDA858622EB1E80800000001961B4C001618F8180000000001100102E648BA30998A28C02C2DFD9DDCD0E0BA064DA199C55186485AFAB296B94E704426FFE00000000000B000A67D9B9FAADB91650E0146B1F742E5C16006708890200239822011026A6925C659D006FEB42D639F1E42DD13224EE49AA34E71B612CF96DB66A8CD4011032C22F653C54CC5E41098227427650644266D80DED45B7387AE0FFC10E529C4680A418228110807CB47D9C1A14CB832FB361C398EA672C9542F34A90BAD4288FA6AC5FC9E9845C01101CF71CAE9252D389135D8C606225DCF1E0333CCDF1FAE84B74FC5D3D440C25F880A3A9108146779F781067ED04B4957E14F1C5623AB653039B2B1D49910240848E4E682DB21081B30694071254D8B3B9537320C014B8CB1052E5514F5EFC19CF2EB806308D5CF1A9573D7C531000000000000000000F3180000000007F00000001EDE5F3C380000000061A0A48D64CA627B243AD5915A2E5D0BAD026762028DDF3304992B83A26D6C11735FC5F01ED56D769BDE7F6A068AF1A4BCFDF950321F3A4744B01B1DDC7498677F112AE1A80000000000000000000000000000000000000658000000000000819800040D37301C10C9419287E9A3B704EB6D7F45CC145DD77DCE8A63B0A47C8AB67467D800901DCE3C8B05A891E56F2BAF1B82405ABD8640B759AEEBD939B976D42C311758F40400000000AFFFFFFC00000000008800814EFDC0A7B2815346B7C1DB024F1E94A451040042041BA83132997EDA1B8B4E10011D48840A33BCFBC0833F6825A4ABF0A78E2B11D5B2981CD958EA4C881204247273416D90840D9834A03892A6C59DCA9B990600A5C65882972A8A7AF7E0CE7975C031846AE78D4AB8002000EC0003FFFFFFFF86801076D98A575A4CDFD0E3F44D1BB3CD3BBAF3BD04C38FED439ED90D88DF932A9296801A80007FFFFFFFF4008136A9D5896669E8724C5120FB6B36C241EF3CEF68AE0316161F04A9EE3EAFF36000FC0003FFFFFFFF86780106E4B5CC4155733A2427082907338051A5DA1E7CA6432840A5528ECAFFA3FB628801B80007FFFFFFFF10020CA4E125E9126107745D4354D4187ABCDE323117857A1DCEB7CCF60B2AAFA80C6003A0000FFFFFFFFE1C0080981575FD981A73A848CC0243CB467BF451F6811DAF4D71CAD8CE8B1E96DB190C01000003FFFFFFFF867400814C747E0FD8290BE8A3B8B3F73015A261479A71780CD3A0A9270234E4B394409C00D80003FFFFFFFF90020E1B9C9B10A97F15F5E1BB27FC8AC670DF8DADEAE4EDFAFB23BDD0AC705FDF51600340000FFFFFFFFF0020AD2581F3494A17B0BE3F63516D53F028A204FD3156D8B21AA4E57A8738D2062080007FFFFFFFF0CE83B9C79160B5123CADE575E370480B57B0C816EB35DD7B27372EDA858622EB1E0B8C1E00000B8000FA46CC2C7E9AB4A37C64216CD65C944E6D73998419D1A1AD2827AB6BC85B32280230764E374064EC82A3751E789607E23BEAE93FB0EDDD5E7FA803767079662E80EAEF384E2AFCB68049D9DC246119E77BD2ED4112330760CAB6CD3671CFCE006C584B9C95E0B554261E00154D40806EA694F44751B328A9291BAD124EFD5664280936EC92D27B242737E7E3E83B4704BA367B7DA5108F2F6EDFB1C38EE721A369E77EED71B12090BAEAAAC322C1457E31AB0C4DE5D9351943F10FD747742616A1AABD09F680B37D4105A8872695EE9B97FAB8985FAA9D747D45046229BF265CEEB300A40FE23040C5F335E0515496C58EE47418B72331FCC6F47A31A9B33B8E000008692FFAFF04D2AE211E9461FB39D875D74F32E4109D21D5A03D46612000000002E307800002E0002069FCA5D3141D3A78436ECFC366E31024CBB18EAF1843EB5FADAC871B42069166C0726710955E3AD621072FCBDFCB90D79E5B1951A5EE01DB533B72429F84E2562680519DE7DE0419FB412D255F853C71588EAD94C0E6CAC7526440902123939A0B6C806CC1A501C495362CEE54DCC830052E32C414B95453D7BF0673CBAE018C23573C69C694A8F88483050257A7366B838489731E5776B6FA0F02573401176D3E7FAEEF11E95A671420586631255F51A0EC2CF4D4D9F69D587712070FE1FB9316B71868692FFAFF04D2AE211E9461FB39D875D74F32E4109D21D5A03D46612000000002E307800002E0002BA11BBBA0202012000000000000007D0000007D0000000C800000007CFFFF83000" - -> """{"type":"DATA_NORMAL","commitments":{"channelId":"07738f22c16a24795bcaebc6e09016af61902dd66bbaf64e6e5db50b0c45d63c","channelConfig":[],"channelFeatures":[],"localParams":{"nodeId":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","fundingKeyPath":{"path":[1457788542,1007597768,1455922339,479707306]},"dustLimit":546,"maxHtlcValueInFlightMsat":5000000000,"channelReserve":167772,"htlcMinimum":1,"toSelfDelay":720,"maxAcceptedHtlcs":30,"isFunder":false,"defaultFinalScriptPubKey":"a9144805d016e47885dc7c852710cdd8cd0d576f57ec87","initFeatures":{"activated":{"option_data_loss_protect":"optional","initial_routing_sync":"optional","gossip_queries":"optional"},"unknown":[]}},"remoteParams":{"nodeId":"034fe52e98a0e9d3c21b767e1b371881265d8c7578c21f5afd6d6438da10348b36","dustLimit":573,"maxHtlcValueInFlightMsat":16609443000,"channelReserve":167772,"htlcMinimum":1000,"toSelfDelay":2016,"maxAcceptedHtlcs":483,"fundingPubKey":"028cef3ef020cfda09692afc29e38ac4756ca60736563a93220481091c9cd05b64","revocationBasepoint":"02635ac9eedf5f219afbc4d125e37b5705f73c05deca71b05fe84096a691e055c1","paymentBasepoint":"034a711d28e8ed3ad389ec14ec75c199b6a45140c503bcc88110e3524e52ffbfb1","delayedPaymentBasepoint":"0316c70730b57a9e15845ce6f239e749ac78b25f44c90485a697066962a73d0467","htlcBasepoint":"03763e280986fb384631ebf8d637efd9ebcd06b6ef3c77c1375b9edbe3ea3b9f79","initFeatures":{"activated":{"option_data_loss_protect":"mandatory","gossip_queries":"optional"},"unknown":[]}},"channelFlags":1,"localCommit":{"index":7675,"spec":{"htlcs":[],"commitTxFeerate":254,"toLocal":204739729,"toRemote":16572475271},"commitTxAndRemoteSig":{"commitTx":{"txid":"e25a866b79212015e01e155e530fb547abc8276869f8740a9948e52ca231f1e4","tx":"020000000107738f22c16a24795bcaebc6e09016af61902dd66bbaf64e6e5db50b0c45d63d010000000032c3698002c31f0300000000002200205cc91746133145180585bfb3bb9a1c1740c9b43338aa30c90b5f5652d729ce0884dffc0000000000160014cfb373f55b722ca1c028d63ee85cb82c00ce11127af8a620"},"remoteSig":"4d4d24b8cb3a00dfd685ac73e3c85ba26449dc935469ce36c259f2db6cd519a865845eca78a998bc8213044e84eca0c884cdb01bda8b6e70f5c1ff821ca5388d"},"htlcTxsAndRemoteSigs":[]},"remoteCommit":{"index":7779,"spec":{"htlcs":[],"commitTxFeerate":254,"toLocal":16572475271,"toRemote":204739729},"txid":"ac994c4f64875ab22b45cba175a04cec4051bbe660932570744dad822e6bf8be","remotePerCommitmentPoint":"03daadaed37bcfed40d15e34979fbf2a0643e748e8960363bb8e930cefe2255c35"},"localChanges":{"proposed":[],"signed":[],"acked":[]},"remoteChanges":{"proposed":[],"acked":[],"signed":[]},"localNextHtlcId":203,"remoteNextHtlcId":4147,"originChannels":{},"remoteNextCommitInfo":"034dcc0704325064a1fa68edc13adb5fd173051775df73a298ec291f22ad9d19f6","commitInput":{"outPoint":"3dd6450c0bb55d6e4ef6ba6bd62d9061af1690e0c6ebca5b79246ac1228f7307:1","amountSatoshis":16777215},"remotePerCommitmentSecrets":null},"shortChannelId":"1513532x23x1","buried":true,"channelAnnouncement":{"nodeSignature1":"d2366163f4d5a51be3210b66b2e4a2736b9ccc20ce8d0d69413d5b5e42d991401183b271ba032764151ba8f3c4b03f11df5749fd876eeaf3fd401bb383cb3174","nodeSignature2":"075779c27157e5b4024ecee12308cf3bde976a0891983b0655b669b38e7e700362c25ce4af05aaa130f000aa6a04037534a7a23a8d99454948dd689277eab321","bitcoinSignature1":"4049b7649693d92139bf3f1f41da3825d1b3dbed2884797b76fd8e1c77390d1b4f3bf76b8d890485d7555619160a2bf18d58626f2ec9a8ca1f887eba3ba130b5","bitcoinSignature2":"0d55e84fb4059bea082d443934af74dcbfd5c4c2fd54eba3ea2823114df932e7759805207f1182062f99af028aa4b62c7723a0c5b9198fe637a3d18d4d99dc70","features":{"activated":{},"unknown":[]},"chainHash":"43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000","shortChannelId":"1513532x23x1","nodeId1":"034fe52e98a0e9d3c21b767e1b371881265d8c7578c21f5afd6d6438da10348b36","nodeId2":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","bitcoinKey1":"028cef3ef020cfda09692afc29e38ac4756ca60736563a93220481091c9cd05b64","bitcoinKey2":"03660d280e24a9b16772a6e6418029719620a5caa29ebdf8339e5d700c611ab9e3","tlvStream":{"records":[],"unknown":[]}},"channelUpdate":{"signature":"4e34a547c424182812bd39b35c1c244b98f2bbb5b7d07812b9a008bb69f3fd77788f4ad338a102c331892afa8d076167a6a6cfb4eac3b890387f0fdc98b5b8c3","chainHash":"43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000","shortChannelId":"1513532x23x1","timestamp":{"iso":"2019-06-18T12:49:33Z","unix":1560862173},"channelFlags":{"isEnabled":true,"isNode1":false},"cltvExpiryDelta":144,"htlcMinimumMsat":1000,"feeBaseMsat":1000,"feeProportionalMillionths":100,"htlcMaximumMsat":16777215000,"tlvStream":{"records":[],"unknown":[]}}}""", + -> """{"type":"DATA_NORMAL","commitments":{"channelId":"07738f22c16a24795bcaebc6e09016af61902dd66bbaf64e6e5db50b0c45d63c","channelConfig":[],"channelFeatures":[],"localParams":{"nodeId":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","fundingKeyPath":{"path":[1457788542,1007597768,1455922339,479707306]},"dustLimit":546,"maxHtlcValueInFlightMsat":5000000000,"channelReserve":167772,"htlcMinimum":1,"toSelfDelay":720,"maxAcceptedHtlcs":30,"isFunder":false,"defaultFinalScriptPubKey":"a9144805d016e47885dc7c852710cdd8cd0d576f57ec87","initFeatures":{"activated":{"option_data_loss_protect":"optional","initial_routing_sync":"optional","gossip_queries":"optional"},"unknown":[]}},"remoteParams":{"nodeId":"034fe52e98a0e9d3c21b767e1b371881265d8c7578c21f5afd6d6438da10348b36","dustLimit":573,"maxHtlcValueInFlightMsat":16609443000,"channelReserve":167772,"htlcMinimum":1000,"toSelfDelay":2016,"maxAcceptedHtlcs":483,"fundingPubKey":"028cef3ef020cfda09692afc29e38ac4756ca60736563a93220481091c9cd05b64","revocationBasepoint":"02635ac9eedf5f219afbc4d125e37b5705f73c05deca71b05fe84096a691e055c1","paymentBasepoint":"034a711d28e8ed3ad389ec14ec75c199b6a45140c503bcc88110e3524e52ffbfb1","delayedPaymentBasepoint":"0316c70730b57a9e15845ce6f239e749ac78b25f44c90485a697066962a73d0467","htlcBasepoint":"03763e280986fb384631ebf8d637efd9ebcd06b6ef3c77c1375b9edbe3ea3b9f79","initFeatures":{"activated":{"option_data_loss_protect":"mandatory","gossip_queries":"optional"},"unknown":[]}},"channelFlags":{"announceChannel":true},"localCommit":{"index":7675,"spec":{"htlcs":[],"commitTxFeerate":254,"toLocal":204739729,"toRemote":16572475271},"commitTxAndRemoteSig":{"commitTx":{"txid":"e25a866b79212015e01e155e530fb547abc8276869f8740a9948e52ca231f1e4","tx":"020000000107738f22c16a24795bcaebc6e09016af61902dd66bbaf64e6e5db50b0c45d63d010000000032c3698002c31f0300000000002200205cc91746133145180585bfb3bb9a1c1740c9b43338aa30c90b5f5652d729ce0884dffc0000000000160014cfb373f55b722ca1c028d63ee85cb82c00ce11127af8a620"},"remoteSig":"4d4d24b8cb3a00dfd685ac73e3c85ba26449dc935469ce36c259f2db6cd519a865845eca78a998bc8213044e84eca0c884cdb01bda8b6e70f5c1ff821ca5388d"},"htlcTxsAndRemoteSigs":[]},"remoteCommit":{"index":7779,"spec":{"htlcs":[],"commitTxFeerate":254,"toLocal":16572475271,"toRemote":204739729},"txid":"ac994c4f64875ab22b45cba175a04cec4051bbe660932570744dad822e6bf8be","remotePerCommitmentPoint":"03daadaed37bcfed40d15e34979fbf2a0643e748e8960363bb8e930cefe2255c35"},"localChanges":{"proposed":[],"signed":[],"acked":[]},"remoteChanges":{"proposed":[],"acked":[],"signed":[]},"localNextHtlcId":203,"remoteNextHtlcId":4147,"originChannels":{},"remoteNextCommitInfo":"034dcc0704325064a1fa68edc13adb5fd173051775df73a298ec291f22ad9d19f6","commitInput":{"outPoint":"3dd6450c0bb55d6e4ef6ba6bd62d9061af1690e0c6ebca5b79246ac1228f7307:1","amountSatoshis":16777215},"remotePerCommitmentSecrets":null},"shortChannelId":"1513532x23x1","buried":true,"channelAnnouncement":{"nodeSignature1":"d2366163f4d5a51be3210b66b2e4a2736b9ccc20ce8d0d69413d5b5e42d991401183b271ba032764151ba8f3c4b03f11df5749fd876eeaf3fd401bb383cb3174","nodeSignature2":"075779c27157e5b4024ecee12308cf3bde976a0891983b0655b669b38e7e700362c25ce4af05aaa130f000aa6a04037534a7a23a8d99454948dd689277eab321","bitcoinSignature1":"4049b7649693d92139bf3f1f41da3825d1b3dbed2884797b76fd8e1c77390d1b4f3bf76b8d890485d7555619160a2bf18d58626f2ec9a8ca1f887eba3ba130b5","bitcoinSignature2":"0d55e84fb4059bea082d443934af74dcbfd5c4c2fd54eba3ea2823114df932e7759805207f1182062f99af028aa4b62c7723a0c5b9198fe637a3d18d4d99dc70","features":{"activated":{},"unknown":[]},"chainHash":"43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000","shortChannelId":"1513532x23x1","nodeId1":"034fe52e98a0e9d3c21b767e1b371881265d8c7578c21f5afd6d6438da10348b36","nodeId2":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","bitcoinKey1":"028cef3ef020cfda09692afc29e38ac4756ca60736563a93220481091c9cd05b64","bitcoinKey2":"03660d280e24a9b16772a6e6418029719620a5caa29ebdf8339e5d700c611ab9e3","tlvStream":{"records":[],"unknown":[]}},"channelUpdate":{"signature":"4e34a547c424182812bd39b35c1c244b98f2bbb5b7d07812b9a008bb69f3fd77788f4ad338a102c331892afa8d076167a6a6cfb4eac3b890387f0fdc98b5b8c3","chainHash":"43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000","shortChannelId":"1513532x23x1","timestamp":{"iso":"2019-06-18T12:49:33Z","unix":1560862173},"channelFlags":{"isEnabled":true,"isNode1":false},"cltvExpiryDelta":144,"htlcMinimumMsat":1000,"feeBaseMsat":1000,"feeProportionalMillionths":100,"htlcMaximumMsat":16777215000,"tlvStream":{"records":[],"unknown":[]}}}""", hex"00000303933884AAF1D6B108397E5EFE5C86BCF2D8CA8D2F700EDA99DB9214FC2712B1340004D443ECE9D9C43A11A19B554BAAA6AD150000000000000222000000003B9ACA0000000000000249F000000000000000010090001E800BD48A22F4C80A42CC8BB29A764DBAEFC95674931FBE9A4380000000C50134D4A745996002F219B5FDBA1E045374DF589ECA06ABE23CECAE47343E65EDCF800000000000011E80000001BA90824000000000000124F800000000000001F4038500F1810AE1AF8A1D6F56F80855E26705F191BB07CD4E2434BC5BB1698E7E5880E2266201E8BFEEEEED725775B8116F6F82CF8E87835A5B45B184E56F272AD70D6078118601E06212B8C8F2E25B73EE7974FDCDF007E389B437BBFE238CCC3F3BF7121B6C5E81AA8589D21E9584B24A11F3ABBA5DAD48D121DD63C57A69CD767119C05DA159CB81A649D8CC0E136EB8DFBD2268B69DCA86F8CE4A604235A03D9D37AE7B07FC563F80000000C080800000000000271C000000000177000000002808B14600000001970039BA00123767F0F4F00D5E9FDF24177EF2872343D9F8FAEC65D3048BA575E70E00A0AB08800000000015E070F20000000000110010584241B5FB364208F6E64A80D1166DAD866186B10C015ED0283FF1C308C2105A0023A910810AE1AF8A1D6F56F80855E26705F191BB07CD4E2434BC5BB1698E7E5880E226621081DE8ADFA110DC8A94D8B9E9EF616BAE8598287C8F82AFDF0FC068697D570266FDA95700AD81000000000080B767F0F4F00D5E9FDF24177EF2872343D9F8FAEC65D3048BA575E70E00A0AB0880000000003E7AEDC0011ABE8A00000000001100101A9CE4B6AEF469590BC7BCC51DCEEAE9C86084055A63CC01E443C733FBE400B9B5B16800000000000B000A5E5700106D1A7097E4DE87EBAF1F8F2773842FA482002418228110805E84989A81F51ABD9D11889AE43E68FAD93659DEC019F1B8C0ADBF15A57B118B81101DCC1256F9306439AD3962C043FC47A5179CAAA001CCB23342BE0E8D92E4022780A4182281108074F306DA3751B84EC5FFB155BDCA7B8E02208BBDBC8D4F3327ABA557BF27CD1701102EF4AC8CC92F469DA9642D4D4162BC545F8B34ADE15B7D6F99808AA22B086B0180A3A910810AE1AF8A1D6F56F80855E26705F191BB07CD4E2434BC5BB1698E7E5880E226621081DE8ADFA110DC8A94D8B9E9EF616BAE8598287C8F82AFDF0FC068697D570266FDA9576F8099900000000000000000271C00000000017700000001970039BA000000002808B14648CE00AE97051EE10A3C361263F81A98165CE4AA7BA076933D4266E533585F24815C15DEACF0691332B38ECF23EC39982C5C978C748374A01BA9B30D501EE4F26E8000000000000000000000000000000000001224000000000000004B800040A911C460F1467952E3B99BED072F81BFB4454FF389636DCB399FE6A78113C28580091BB3F87A7806AF4FEF920BBF794391A1ECFC7D7632E98245D2BAF3870050558440000000000AF0387900000000000880082C2120DAFD9B21047B732540688B36D6C330C3588600AF68141FF8E18461082D0011D488408570D7C50EB7AB7C042AF13382F8C8DD83E6A7121A5E2DD8B4C73F2C407113310840EF456FD0886E454A6C5CF4F7B0B5D742CC143E47C157EF87E03434BEAB81337ED4AB8001C00F40003FFFFFFFEC7200403248A1D44DFA3AC9EC237D452C936400CAA86E9517CCCF2A8F77B7493CD70B6A00780001FFFFFFFF63A0041826829646B907A97FBD1455EA8673A12B8E7AA6EA790F7802E955CE3B69DE57E006E0001FFFFFFFF640081E51EB1F91218821E680B50E4B22DF8B094385BD33ACAE36BFC9E8C2F5AD2DA5400EC0003FFFFFFFEC7801047C26AD5435658D063EBCF73A5D0EEFE73ED6B73426246E8DFB3A21D1C4C7465001900007FFFFFFFE0040B115AC58BAAA900195893EA3B2AB408D2AD348AD047E3B6CB15E599625E38608006A0001FFFFFFFF7002033C39A21A38BB61F6FB33623771A9356D8885B7C12C939C770C939EF826286C200360000FFFFFFFFB4008104EF4271064A0973B053727C3E67352D00E25CAEED944F50782449CEAE8F50960001FFFFFFFF6390DD9FC3D3C0357A7F7C905DFBCA1C8D0F67E3EBB1974C122E95D79C380282AC222B21FA0007920001295AA1FB77029F7620A90EF7AE6A6CD31E4588B93264A7ADB76152D535C52E90B9E1B7C2376DABA316A6290F1A9730D4E5E44D0B1CB0EE6A795702E6A6BCDFCDA1A4BFEBFC134AB8847A5187ECE761D75D3CCB904274875680F51984800000000AC87E8001E480002E884D2A8080804800000000000001F4000001F40000003200000001BF08EB000" - -> """{"type":"DATA_NORMAL","commitments":{"channelId":"6ecfe1e9e01abd3fbe482efde50e4687b3f1f5d8cba609174aebce1c01415611","channelConfig":[],"channelFeatures":[],"localParams":{"nodeId":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","fundingKeyPath":{"path":[3561221353,3653515793,2711311691,2863050005]},"dustLimit":546,"maxHtlcValueInFlightMsat":1000000000,"channelReserve":150000,"htlcMinimum":1,"toSelfDelay":144,"maxAcceptedHtlcs":30,"isFunder":true,"defaultFinalScriptPubKey":"a91445e990148599176534ec9b75df92ace9263f7d3487","initFeatures":{"activated":{"option_data_loss_protect":"optional","initial_routing_sync":"optional","gossip_queries":"optional"},"unknown":[]}},"remoteParams":{"nodeId":"0269a94e8b32c005e4336bfb743c08a6e9beb13d940d57c479d95c8e687ccbdb9f","dustLimit":573,"maxHtlcValueInFlightMsat":14850000000,"channelReserve":150000,"htlcMinimum":1000,"toSelfDelay":1802,"maxAcceptedHtlcs":483,"fundingPubKey":"0215c35f143adeadf010abc4ce0be323760f9a9c486978b762d31cfcb101c44cc4","revocationBasepoint":"03d17fdddddae4aeeb7022dedf059f1d0f06b4b68b6309cade4e55ae1ac0f0230c","paymentBasepoint":"03c0c4257191e5c4b6e7dcf2e9fb9be00fc713686f77fc4719987e77ee2436d8bd","delayedPaymentBasepoint":"03550b13a43d2b09649423e75774bb5a91a243bac78af4d39aece23380bb42b397","htlcBasepoint":"034c93b1981c26dd71bf7a44d16d3b950df19c94c0846b407b3a6f5cf60ff8ac7f","initFeatures":{"activated":{"option_data_loss_protect":"mandatory","gossip_queries":"optional"},"unknown":[]}},"channelFlags":1,"localCommit":{"index":20024,"spec":{"htlcs":[],"commitTxFeerate":750,"toLocal":1343316620,"toRemote":13656683380},"commitTxAndRemoteSig":{"commitTx":{"txid":"65fe0b1f079fa763448df3ab8d94b1ad7d377c061121376be90b9c0c1bb0cd43","tx":"02000000016ecfe1e9e01abd3fbe482efde50e4687b3f1f5d8cba609174aebce1c0141561100000000007cf5db8002357d1400000000002200203539c96d5de8d2b2178f798a3b9dd5d390c1080ab4c79803c8878e67f7c801736b62d00000000000160014bcae0020da34e12fc9bd0fd75e3f1e4ee7085f49df013320"},"remoteSig":"bd09313503ea357b3a231135c87cd1f5b26cb3bd8033e371815b7e2b4af623173b9824adf260c8735a72c58087f88f4a2f39554003996466857c1d1b25c8044f"},"htlcTxsAndRemoteSigs":[]},"remoteCommit":{"index":20024,"spec":{"htlcs":[],"commitTxFeerate":750,"toLocal":13656683380,"toRemote":1343316620},"txid":"919c015d2e0a3dc214786c24c7f035302cb9c954f740ed267a84cdca66b0be49","remotePerCommitmentPoint":"02b82bbd59e0d22665671d9e47d8733058b92f18e906e9403753661aa03dc9e4dd"},"localChanges":{"proposed":[],"signed":[],"acked":[]},"remoteChanges":{"proposed":[],"acked":[],"signed":[]},"localNextHtlcId":9288,"remoteNextHtlcId":151,"originChannels":{},"remoteNextCommitInfo":"02a4471183c519e54b8ee66fb41cbe06fed1153fce258db72ce67f9a9e044f0a16","commitInput":{"outPoint":"115641011cceeb4a1709a6cbd8f5f1b387460ee5fd2e48be3fbd1ae0e9e1cf6e:0","amountSatoshis":15000000},"remotePerCommitmentSecrets":null},"shortChannelId":"1413373x969x0","buried":true,"channelUpdate":{"signature":"52b543f6ee053eec41521def5cd4d9a63c8b117264c94f5b6ec2a5aa6b8a5d2173c36f846edb57462d4c521e352e61a9cbc89a163961dcd4f2ae05cd4d79bf9b","chainHash":"43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000","shortChannelId":"1413373x969x0","timestamp":{"iso":"2019-06-24T09:39:33Z","unix":1561369173},"channelFlags":{"isEnabled":true,"isNode1":false},"cltvExpiryDelta":144,"htlcMinimumMsat":1000,"feeBaseMsat":1000,"feeProportionalMillionths":100,"htlcMaximumMsat":15000000000,"tlvStream":{"records":[],"unknown":[]}}}""", + -> """{"type":"DATA_NORMAL","commitments":{"channelId":"6ecfe1e9e01abd3fbe482efde50e4687b3f1f5d8cba609174aebce1c01415611","channelConfig":[],"channelFeatures":[],"localParams":{"nodeId":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","fundingKeyPath":{"path":[3561221353,3653515793,2711311691,2863050005]},"dustLimit":546,"maxHtlcValueInFlightMsat":1000000000,"channelReserve":150000,"htlcMinimum":1,"toSelfDelay":144,"maxAcceptedHtlcs":30,"isFunder":true,"defaultFinalScriptPubKey":"a91445e990148599176534ec9b75df92ace9263f7d3487","initFeatures":{"activated":{"option_data_loss_protect":"optional","initial_routing_sync":"optional","gossip_queries":"optional"},"unknown":[]}},"remoteParams":{"nodeId":"0269a94e8b32c005e4336bfb743c08a6e9beb13d940d57c479d95c8e687ccbdb9f","dustLimit":573,"maxHtlcValueInFlightMsat":14850000000,"channelReserve":150000,"htlcMinimum":1000,"toSelfDelay":1802,"maxAcceptedHtlcs":483,"fundingPubKey":"0215c35f143adeadf010abc4ce0be323760f9a9c486978b762d31cfcb101c44cc4","revocationBasepoint":"03d17fdddddae4aeeb7022dedf059f1d0f06b4b68b6309cade4e55ae1ac0f0230c","paymentBasepoint":"03c0c4257191e5c4b6e7dcf2e9fb9be00fc713686f77fc4719987e77ee2436d8bd","delayedPaymentBasepoint":"03550b13a43d2b09649423e75774bb5a91a243bac78af4d39aece23380bb42b397","htlcBasepoint":"034c93b1981c26dd71bf7a44d16d3b950df19c94c0846b407b3a6f5cf60ff8ac7f","initFeatures":{"activated":{"option_data_loss_protect":"mandatory","gossip_queries":"optional"},"unknown":[]}},"channelFlags":{"announceChannel":true},"localCommit":{"index":20024,"spec":{"htlcs":[],"commitTxFeerate":750,"toLocal":1343316620,"toRemote":13656683380},"commitTxAndRemoteSig":{"commitTx":{"txid":"65fe0b1f079fa763448df3ab8d94b1ad7d377c061121376be90b9c0c1bb0cd43","tx":"02000000016ecfe1e9e01abd3fbe482efde50e4687b3f1f5d8cba609174aebce1c0141561100000000007cf5db8002357d1400000000002200203539c96d5de8d2b2178f798a3b9dd5d390c1080ab4c79803c8878e67f7c801736b62d00000000000160014bcae0020da34e12fc9bd0fd75e3f1e4ee7085f49df013320"},"remoteSig":"bd09313503ea357b3a231135c87cd1f5b26cb3bd8033e371815b7e2b4af623173b9824adf260c8735a72c58087f88f4a2f39554003996466857c1d1b25c8044f"},"htlcTxsAndRemoteSigs":[]},"remoteCommit":{"index":20024,"spec":{"htlcs":[],"commitTxFeerate":750,"toLocal":13656683380,"toRemote":1343316620},"txid":"919c015d2e0a3dc214786c24c7f035302cb9c954f740ed267a84cdca66b0be49","remotePerCommitmentPoint":"02b82bbd59e0d22665671d9e47d8733058b92f18e906e9403753661aa03dc9e4dd"},"localChanges":{"proposed":[],"signed":[],"acked":[]},"remoteChanges":{"proposed":[],"acked":[],"signed":[]},"localNextHtlcId":9288,"remoteNextHtlcId":151,"originChannels":{},"remoteNextCommitInfo":"02a4471183c519e54b8ee66fb41cbe06fed1153fce258db72ce67f9a9e044f0a16","commitInput":{"outPoint":"115641011cceeb4a1709a6cbd8f5f1b387460ee5fd2e48be3fbd1ae0e9e1cf6e:0","amountSatoshis":15000000},"remotePerCommitmentSecrets":null},"shortChannelId":"1413373x969x0","buried":true,"channelUpdate":{"signature":"52b543f6ee053eec41521def5cd4d9a63c8b117264c94f5b6ec2a5aa6b8a5d2173c36f846edb57462d4c521e352e61a9cbc89a163961dcd4f2ae05cd4d79bf9b","chainHash":"43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000","shortChannelId":"1413373x969x0","timestamp":{"iso":"2019-06-24T09:39:33Z","unix":1561369173},"channelFlags":{"isEnabled":true,"isNode1":false},"cltvExpiryDelta":144,"htlcMinimumMsat":1000,"feeBaseMsat":1000,"feeProportionalMillionths":100,"htlcMaximumMsat":15000000000,"tlvStream":{"records":[],"unknown":[]}}}""", hex"0200020000000303933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b13400098c4b989bbdced820a77a7186c2320e7d176a5c8b5c16d6ac2af3889d6bc8bf8080000001000000000000022200000004a817c80000000000000249f0000000000000000102d0001eff1600148061b7fbd2d84ed1884177ea785faecb2080b10302e56c8eca8d4f00df84ac34c23f49c006d57d316b7ada5c346e9d4211e11604b300000004080aa982027455aef8453d92f4706b560b61527cc217ddf14da41770e8ed6607190a1851b8000000000000023d000000037521048000000000000249f00000000000000001070a01e302eff5309b9368340edc6114d738b3590e6969bec4e95d8a080cf185e8b9ce5e4b0343bf4bfbaea5c100f1f2bf1cdf82a0ef97c9a0069a2aec631e7c3084ba929b7503c54e7d5ccfc13f1a6c7a441ffcfac86248574d1bc0fe9773836f4c724ea7b2bd03765aaac2e8fa6dbce7de5143072e9d9d5e96a1fd451d02fe4ff803f413f303f8022f3b055b0d35cde31dec5263a8ed638433e3424a4e197c06d94053985a364a5700000004808a52a1010000000000000004000000001046000000037e11d6000000000000000000245986f644357956c1674f385e0878fb7bb44ab3d57ea9911fab98af8a71e1ad1b000000002bc0e1e40000000000220020690fb50de412adf9b20a7fc6c8fb86f1bfd4ebc1ef8e2d96a5a196560798d944475221023ced05ed1ab328b67477376d68a69ecd0f371a9d5843c6c3be4d31498d516d8d2102eff5309b9368340edc6114d738b3590e6969bec4e95d8a080cf185e8b9ce5e4b52aefd013b020000000001015986f644357956c1674f385e0878fb7bb44ab3d57ea9911fab98af8a71e1ad1b0000000000c2d6178001f8d5e4000000000022002080f1dfe71a865b605593e169677c952aaa1196fc2f541ef7d21c3b1006527b61040047304402207f8c1936d0a50671c993890f887c78c6019abc2a2e8018899dcdc0e891fd2b090220046b56afa2cb7e9470073c238654ecf584bcf5c00b96b91e38335a70e2739ec901483045022100871afd240e20a171b9cba46f20555f848c5850f94ec7da7b33b9eeaf6af6653c0220119cda8cbf5f80986d6a4f0db2590c734d1de399a7060a477b5d94df0183625b01475221023ced05ed1ab328b67477376d68a69ecd0f371a9d5843c6c3be4d31498d516d8d2102eff5309b9368340edc6114d738b3590e6969bec4e95d8a080cf185e8b9ce5e4b52aed7782c20000000000000000000040000000010460000000000000000000000037e11d600b5f2287b2d5edf4df5602a3c287db3b938c3f1a943e40715886db5bd400f95d802e7e1abac1feb54ee3ac2172c9e2231f77765df57664fb44a6dc2e4aa9e6a9a6a000000000000000000000000000000000000000000000000000000000000ff03fd10fe44564e2d7e1550099785c2c1bad32a5ae0feeef6e27f0c108d18b4931d245986f644357956c1674f385e0878fb7bb44ab3d57ea9911fab98af8a71e1ad1b000000002bc0e1e40000000000220020690fb50de412adf9b20a7fc6c8fb86f1bfd4ebc1ef8e2d96a5a196560798d944475221023ced05ed1ab328b67477376d68a69ecd0f371a9d5843c6c3be4d31498d516d8d2102eff5309b9368340edc6114d738b3590e6969bec4e95d8a080cf185e8b9ce5e4b52ae0001003e0000fffffffffffc0080474b8cf7bb98217dd8dc475cb7c057a3465d466728978bbb909d0a05d4ae7bbe0001fffffffffff85986f644357956c1674f385e0878fb7bb44ab3d57ea9911fab98af8a71e1ad1b1eedce0000010000fffffd01ae98d7a81bc1aa92fcfb74ced2213e85e0d92ae8ac622bf294b3551c7c27f6f84f782f3b318e4d0eb2c67ac719a7c65afcf85bf159f6ceea9427be54920134196992f6ed0e059db72105a13ec0e799bb08896cad8b4feb7e9ec7283c309b5f43123af1bd9e913fc2db018edadde8932d6992408f10c1ad020504361972dfa7fef09bbc2b568cef3c8c006f7860106fd5984bcc271ff06c4829db2a665e59b7c0b22c311a340ff2ab9bcb74a50db10ed85503ad2d248d95af8151aca8ef96248e8f84b3075922385fbaf012f057e7ee84ecbc14c84880520b26d6fd22ab5f107db606a906efdcf0f88ffbe32dc6ecc10131e1ff0dc8d68dad89c98562557f00448b000043497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea3309000000001eedce0000010000027455aef8453d92f4706b560b61527cc217ddf14da41770e8ed6607190a1851b803933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b13402eff5309b9368340edc6114d738b3590e6969bec4e95d8a080cf185e8b9ce5e4b023ced05ed1ab328b67477376d68a69ecd0f371a9d5843c6c3be4d31498d516d8d88710d73875607575f3d84bb507dd87cca5b85f0cdac84f4ccecce7af3a55897525a45070fe26c0ea43e9580d4ea4cfa62ee3273e5546911145cba6bbf56e59d8e43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea3309000000001eedce000001000060e6eb14010100900000000000000001000003e800000064000000037e11d6000000" - -> """{"type":"DATA_NORMAL","commitments":{"channelId":"5986f644357956c1674f385e0878fb7bb44ab3d57ea9911fab98af8a71e1ad1b","channelConfig":["funding_pubkey_based_channel_keypath"],"channelFeatures":["option_static_remotekey"],"localParams":{"nodeId":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","fundingKeyPath":{"path":[2353764507,3184449568,2809819526,3258060413,392846475,1545000620,720603293,1808318336,2147483649]},"dustLimit":546,"maxHtlcValueInFlightMsat":20000000000,"channelReserve":150000,"htlcMinimum":1,"toSelfDelay":720,"maxAcceptedHtlcs":30,"isFunder":true,"defaultFinalScriptPubKey":"00148061b7fbd2d84ed1884177ea785faecb2080b103","walletStaticPaymentBasepoint":"02e56c8eca8d4f00df84ac34c23f49c006d57d316b7ada5c346e9d4211e11604b3","initFeatures":{"activated":{"option_support_large_channel":"optional","gossip_queries_ex":"optional","option_data_loss_protect":"optional","var_onion_optin":"mandatory","option_static_remotekey":"optional","payment_secret":"optional","option_shutdown_anysegwit":"optional","basic_mpp":"optional","gossip_queries":"optional"},"unknown":[]}},"remoteParams":{"nodeId":"027455aef8453d92f4706b560b61527cc217ddf14da41770e8ed6607190a1851b8","dustLimit":573,"maxHtlcValueInFlightMsat":14850000000,"channelReserve":150000,"htlcMinimum":1,"toSelfDelay":1802,"maxAcceptedHtlcs":483,"fundingPubKey":"02eff5309b9368340edc6114d738b3590e6969bec4e95d8a080cf185e8b9ce5e4b","revocationBasepoint":"0343bf4bfbaea5c100f1f2bf1cdf82a0ef97c9a0069a2aec631e7c3084ba929b75","paymentBasepoint":"03c54e7d5ccfc13f1a6c7a441ffcfac86248574d1bc0fe9773836f4c724ea7b2bd","delayedPaymentBasepoint":"03765aaac2e8fa6dbce7de5143072e9d9d5e96a1fd451d02fe4ff803f413f303f8","htlcBasepoint":"022f3b055b0d35cde31dec5263a8ed638433e3424a4e197c06d94053985a364a57","initFeatures":{"activated":{"option_upfront_shutdown_script":"optional","payment_secret":"mandatory","option_data_loss_protect":"mandatory","var_onion_optin":"optional","option_static_remotekey":"mandatory","option_support_large_channel":"optional","option_anchors_zero_fee_htlc_tx":"optional","basic_mpp":"optional","gossip_queries":"optional"},"unknown":[31]}},"channelFlags":1,"localCommit":{"index":4,"spec":{"htlcs":[],"commitTxFeerate":4166,"toLocal":15000000000,"toRemote":0},"commitTxAndRemoteSig":{"commitTx":{"txid":"fa747ecb6f718c6831cc7148cf8d65c3468d2bb6c202605e2b82d2277491222f","tx":"02000000015986f644357956c1674f385e0878fb7bb44ab3d57ea9911fab98af8a71e1ad1b0000000000c2d6178001f8d5e4000000000022002080f1dfe71a865b605593e169677c952aaa1196fc2f541ef7d21c3b1006527b61d7782c20"},"remoteSig":"871afd240e20a171b9cba46f20555f848c5850f94ec7da7b33b9eeaf6af6653c119cda8cbf5f80986d6a4f0db2590c734d1de399a7060a477b5d94df0183625b"},"htlcTxsAndRemoteSigs":[]},"remoteCommit":{"index":4,"spec":{"htlcs":[],"commitTxFeerate":4166,"toLocal":0,"toRemote":15000000000},"txid":"b5f2287b2d5edf4df5602a3c287db3b938c3f1a943e40715886db5bd400f95d8","remotePerCommitmentPoint":"02e7e1abac1feb54ee3ac2172c9e2231f77765df57664fb44a6dc2e4aa9e6a9a6a"},"localChanges":{"proposed":[],"signed":[],"acked":[]},"remoteChanges":{"proposed":[],"acked":[],"signed":[]},"localNextHtlcId":0,"remoteNextHtlcId":0,"originChannels":{},"remoteNextCommitInfo":"03fd10fe44564e2d7e1550099785c2c1bad32a5ae0feeef6e27f0c108d18b4931d","commitInput":{"outPoint":"1bade1718aaf98ab1f91a97ed5b34ab47bfb78085e384f67c156793544f68659:0","amountSatoshis":15000000},"remotePerCommitmentSecrets":null},"shortChannelId":"2026958x1x0","buried":true,"channelAnnouncement":{"nodeSignature1":"98d7a81bc1aa92fcfb74ced2213e85e0d92ae8ac622bf294b3551c7c27f6f84f782f3b318e4d0eb2c67ac719a7c65afcf85bf159f6ceea9427be549201341969","nodeSignature2":"92f6ed0e059db72105a13ec0e799bb08896cad8b4feb7e9ec7283c309b5f43123af1bd9e913fc2db018edadde8932d6992408f10c1ad020504361972dfa7fef0","bitcoinSignature1":"9bbc2b568cef3c8c006f7860106fd5984bcc271ff06c4829db2a665e59b7c0b22c311a340ff2ab9bcb74a50db10ed85503ad2d248d95af8151aca8ef96248e8f","bitcoinSignature2":"84b3075922385fbaf012f057e7ee84ecbc14c84880520b26d6fd22ab5f107db606a906efdcf0f88ffbe32dc6ecc10131e1ff0dc8d68dad89c98562557f00448b","features":{"activated":{},"unknown":[]},"chainHash":"43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000","shortChannelId":"2026958x1x0","nodeId1":"027455aef8453d92f4706b560b61527cc217ddf14da41770e8ed6607190a1851b8","nodeId2":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","bitcoinKey1":"02eff5309b9368340edc6114d738b3590e6969bec4e95d8a080cf185e8b9ce5e4b","bitcoinKey2":"023ced05ed1ab328b67477376d68a69ecd0f371a9d5843c6c3be4d31498d516d8d","tlvStream":{"records":[],"unknown":[]}},"channelUpdate":{"signature":"710d73875607575f3d84bb507dd87cca5b85f0cdac84f4ccecce7af3a55897525a45070fe26c0ea43e9580d4ea4cfa62ee3273e5546911145cba6bbf56e59d8e","chainHash":"43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000","shortChannelId":"2026958x1x0","timestamp":{"iso":"2021-07-08T12:09:56Z","unix":1625746196},"channelFlags":{"isEnabled":true,"isNode1":false},"cltvExpiryDelta":144,"htlcMinimumMsat":1,"feeBaseMsat":1000,"feeProportionalMillionths":100,"htlcMaximumMsat":15000000000,"tlvStream":{"records":[],"unknown":[]}}}""" + -> """{"type":"DATA_NORMAL","commitments":{"channelId":"5986f644357956c1674f385e0878fb7bb44ab3d57ea9911fab98af8a71e1ad1b","channelConfig":["funding_pubkey_based_channel_keypath"],"channelFeatures":["option_static_remotekey"],"localParams":{"nodeId":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","fundingKeyPath":{"path":[2353764507,3184449568,2809819526,3258060413,392846475,1545000620,720603293,1808318336,2147483649]},"dustLimit":546,"maxHtlcValueInFlightMsat":20000000000,"channelReserve":150000,"htlcMinimum":1,"toSelfDelay":720,"maxAcceptedHtlcs":30,"isFunder":true,"defaultFinalScriptPubKey":"00148061b7fbd2d84ed1884177ea785faecb2080b103","walletStaticPaymentBasepoint":"02e56c8eca8d4f00df84ac34c23f49c006d57d316b7ada5c346e9d4211e11604b3","initFeatures":{"activated":{"option_support_large_channel":"optional","gossip_queries_ex":"optional","option_data_loss_protect":"optional","var_onion_optin":"mandatory","option_static_remotekey":"optional","payment_secret":"optional","option_shutdown_anysegwit":"optional","basic_mpp":"optional","gossip_queries":"optional"},"unknown":[]}},"remoteParams":{"nodeId":"027455aef8453d92f4706b560b61527cc217ddf14da41770e8ed6607190a1851b8","dustLimit":573,"maxHtlcValueInFlightMsat":14850000000,"channelReserve":150000,"htlcMinimum":1,"toSelfDelay":1802,"maxAcceptedHtlcs":483,"fundingPubKey":"02eff5309b9368340edc6114d738b3590e6969bec4e95d8a080cf185e8b9ce5e4b","revocationBasepoint":"0343bf4bfbaea5c100f1f2bf1cdf82a0ef97c9a0069a2aec631e7c3084ba929b75","paymentBasepoint":"03c54e7d5ccfc13f1a6c7a441ffcfac86248574d1bc0fe9773836f4c724ea7b2bd","delayedPaymentBasepoint":"03765aaac2e8fa6dbce7de5143072e9d9d5e96a1fd451d02fe4ff803f413f303f8","htlcBasepoint":"022f3b055b0d35cde31dec5263a8ed638433e3424a4e197c06d94053985a364a57","initFeatures":{"activated":{"option_upfront_shutdown_script":"optional","payment_secret":"mandatory","option_data_loss_protect":"mandatory","var_onion_optin":"optional","option_static_remotekey":"mandatory","option_support_large_channel":"optional","option_anchors_zero_fee_htlc_tx":"optional","basic_mpp":"optional","gossip_queries":"optional"},"unknown":[31]}},"channelFlags":{"announceChannel":true},"localCommit":{"index":4,"spec":{"htlcs":[],"commitTxFeerate":4166,"toLocal":15000000000,"toRemote":0},"commitTxAndRemoteSig":{"commitTx":{"txid":"fa747ecb6f718c6831cc7148cf8d65c3468d2bb6c202605e2b82d2277491222f","tx":"02000000015986f644357956c1674f385e0878fb7bb44ab3d57ea9911fab98af8a71e1ad1b0000000000c2d6178001f8d5e4000000000022002080f1dfe71a865b605593e169677c952aaa1196fc2f541ef7d21c3b1006527b61d7782c20"},"remoteSig":"871afd240e20a171b9cba46f20555f848c5850f94ec7da7b33b9eeaf6af6653c119cda8cbf5f80986d6a4f0db2590c734d1de399a7060a477b5d94df0183625b"},"htlcTxsAndRemoteSigs":[]},"remoteCommit":{"index":4,"spec":{"htlcs":[],"commitTxFeerate":4166,"toLocal":0,"toRemote":15000000000},"txid":"b5f2287b2d5edf4df5602a3c287db3b938c3f1a943e40715886db5bd400f95d8","remotePerCommitmentPoint":"02e7e1abac1feb54ee3ac2172c9e2231f77765df57664fb44a6dc2e4aa9e6a9a6a"},"localChanges":{"proposed":[],"signed":[],"acked":[]},"remoteChanges":{"proposed":[],"acked":[],"signed":[]},"localNextHtlcId":0,"remoteNextHtlcId":0,"originChannels":{},"remoteNextCommitInfo":"03fd10fe44564e2d7e1550099785c2c1bad32a5ae0feeef6e27f0c108d18b4931d","commitInput":{"outPoint":"1bade1718aaf98ab1f91a97ed5b34ab47bfb78085e384f67c156793544f68659:0","amountSatoshis":15000000},"remotePerCommitmentSecrets":null},"shortChannelId":"2026958x1x0","buried":true,"channelAnnouncement":{"nodeSignature1":"98d7a81bc1aa92fcfb74ced2213e85e0d92ae8ac622bf294b3551c7c27f6f84f782f3b318e4d0eb2c67ac719a7c65afcf85bf159f6ceea9427be549201341969","nodeSignature2":"92f6ed0e059db72105a13ec0e799bb08896cad8b4feb7e9ec7283c309b5f43123af1bd9e913fc2db018edadde8932d6992408f10c1ad020504361972dfa7fef0","bitcoinSignature1":"9bbc2b568cef3c8c006f7860106fd5984bcc271ff06c4829db2a665e59b7c0b22c311a340ff2ab9bcb74a50db10ed85503ad2d248d95af8151aca8ef96248e8f","bitcoinSignature2":"84b3075922385fbaf012f057e7ee84ecbc14c84880520b26d6fd22ab5f107db606a906efdcf0f88ffbe32dc6ecc10131e1ff0dc8d68dad89c98562557f00448b","features":{"activated":{},"unknown":[]},"chainHash":"43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000","shortChannelId":"2026958x1x0","nodeId1":"027455aef8453d92f4706b560b61527cc217ddf14da41770e8ed6607190a1851b8","nodeId2":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","bitcoinKey1":"02eff5309b9368340edc6114d738b3590e6969bec4e95d8a080cf185e8b9ce5e4b","bitcoinKey2":"023ced05ed1ab328b67477376d68a69ecd0f371a9d5843c6c3be4d31498d516d8d","tlvStream":{"records":[],"unknown":[]}},"channelUpdate":{"signature":"710d73875607575f3d84bb507dd87cca5b85f0cdac84f4ccecce7af3a55897525a45070fe26c0ea43e9580d4ea4cfa62ee3273e5546911145cba6bbf56e59d8e","chainHash":"43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000","shortChannelId":"2026958x1x0","timestamp":{"iso":"2021-07-08T12:09:56Z","unix":1625746196},"channelFlags":{"isEnabled":true,"isNode1":false},"cltvExpiryDelta":144,"htlcMinimumMsat":1,"feeBaseMsat":1000,"feeProportionalMillionths":100,"htlcMaximumMsat":15000000000,"tlvStream":{"records":[],"unknown":[]}}}""" ) refs.foreach { case (oldbin, refjson) => @@ -310,7 +318,8 @@ object ChannelCodecsSpec { val localCommit = LocalCommit(0, CommitmentSpec(htlcs.toSet, FeeratePerKw(1500 sat), 50000000 msat, 70000000 msat), CommitTxAndRemoteSig(CommitTx(commitmentInput, commitTx), remoteSig), Nil) val remoteCommit = RemoteCommit(0, CommitmentSpec(htlcs.map(_.opposite).toSet, FeeratePerKw(1500 sat), 50000 msat, 700000 msat), ByteVector32(hex"0303030303030303030303030303030303030303030303030303030303030303"), PrivateKey(ByteVector.fill(32)(4)).publicKey) val channelId = htlcs.headOption.map(_.add.channelId).getOrElse(ByteVector32.Zeroes) - val commitments = Commitments(channelId, ChannelConfig.standard, ChannelFeatures(), localParams, remoteParams, channelFlags = 0x01.toByte, localCommit, remoteCommit, LocalChanges(Nil, Nil, Nil), RemoteChanges(Nil, Nil, Nil), + val channelFlags = ChannelFlags.Public + val commitments = Commitments(channelId, ChannelConfig.standard, ChannelFeatures(), localParams, remoteParams, channelFlags, localCommit, remoteCommit, LocalChanges(Nil, Nil, Nil), RemoteChanges(Nil, Nil, Nil), localNextHtlcId = 32L, remoteNextHtlcId = 4L, originChannels = origins, diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/CommonCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/CommonCodecsSpec.scala index 05fa6eb687..215d738ca9 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/CommonCodecsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/CommonCodecsSpec.scala @@ -20,12 +20,13 @@ import com.google.common.net.InetAddresses import fr.acinq.bitcoin.Crypto.PrivateKey import fr.acinq.bitcoin._ import fr.acinq.eclair.blockchain.fee.FeeratePerKw +import fr.acinq.eclair.channel.ChannelFlags import fr.acinq.eclair.crypto.Hmac256 import fr.acinq.eclair.wire.protocol.CommonCodecs._ import fr.acinq.eclair.{UInt64, randomBytes32} import org.scalatest.funsuite.AnyFunSuite import scodec.DecodeResult -import scodec.bits.{BitVector, HexStringSyntax} +import scodec.bits.{BinStringSyntax, BitVector, HexStringSyntax} import scodec.codecs.uint32 import java.net.{Inet4Address, Inet6Address, InetAddress} @@ -137,6 +138,21 @@ class CommonCodecsSpec extends AnyFunSuite { } } + test("encode/decode channel flags") { + val testCases = Map( + bin"00000000" -> ChannelFlags(announceChannel = false), + bin"00000001" -> ChannelFlags(announceChannel = true), + ) + testCases.foreach { case (bin, obj) => + assert(channelflags.decode(bin).require === DecodeResult(obj, BitVector.empty)) + assert(channelflags.encode(obj).require === bin) + } + + // BOLT 2: The receiving node MUST [...] ignore undefined bits in channel_flags. + assert(channelflags.decode(bin"11111111").require === DecodeResult(ChannelFlags(announceChannel = true), BitVector.empty)) + assert(channelflags.decode(bin"11111110").require === DecodeResult(ChannelFlags(announceChannel = false), BitVector.empty)) + } + test("encode/decode with rgb codec") { val color = Color(47.toByte, 255.toByte, 142.toByte) val bin = rgb.encode(color).require diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecsSpec.scala index e7c1e6325b..dd9442351a 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecsSpec.scala @@ -20,7 +20,7 @@ import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} import fr.acinq.bitcoin.{Block, ByteVector32, ByteVector64, SatoshiLong} import fr.acinq.eclair._ import fr.acinq.eclair.blockchain.fee.FeeratePerKw -import fr.acinq.eclair.channel.ChannelTypes +import fr.acinq.eclair.channel.{ChannelFlags, ChannelTypes} import fr.acinq.eclair.json.JsonSerializers import fr.acinq.eclair.router.Announcements import fr.acinq.eclair.wire.protocol.LightningMessageCodecs._ @@ -152,7 +152,7 @@ class LightningMessageCodecsSpec extends AnyFunSuite { } test("encode/decode open_channel") { - val defaultOpen = OpenChannel(ByteVector32.Zeroes, ByteVector32.Zeroes, 1 sat, 1 msat, 1 sat, UInt64(1), 1 sat, 1 msat, FeeratePerKw(1 sat), CltvExpiryDelta(1), 1, publicKey(1), point(2), point(3), point(4), point(5), point(6), 0.toByte) + val defaultOpen = OpenChannel(ByteVector32.Zeroes, ByteVector32.Zeroes, 1 sat, 1 msat, 1 sat, UInt64(1), 1 sat, 1 msat, FeeratePerKw(1 sat), CltvExpiryDelta(1), 1, publicKey(1), point(2), point(3), point(4), point(5), point(6), ChannelFlags.Private) // Legacy encoding that omits the upfront_shutdown_script and trailing tlv stream. // To allow extending all messages with TLV streams, the upfront_shutdown_script was moved to a TLV stream extension // in https://github.com/lightningnetwork/lightning-rfc/pull/714 and made mandatory when including a TLV stream. @@ -254,7 +254,7 @@ class LightningMessageCodecsSpec extends AnyFunSuite { } test("encode/decode all channel messages") { - val open = OpenChannel(randomBytes32(), randomBytes32(), 3 sat, 4 msat, 5 sat, UInt64(6), 7 sat, 8 msat, FeeratePerKw(9 sat), CltvExpiryDelta(10), 11, publicKey(1), point(2), point(3), point(4), point(5), point(6), 0.toByte) + val open = OpenChannel(randomBytes32(), randomBytes32(), 3 sat, 4 msat, 5 sat, UInt64(6), 7 sat, 8 msat, FeeratePerKw(9 sat), CltvExpiryDelta(10), 11, publicKey(1), point(2), point(3), point(4), point(5), point(6), ChannelFlags.Private) val accept = AcceptChannel(randomBytes32(), 3 sat, UInt64(4), 5 sat, 6 msat, 7, CltvExpiryDelta(8), 9, publicKey(1), point(2), point(3), point(4), point(5), point(6)) val funding_created = FundingCreated(randomBytes32(), bin32(0), 3, randomBytes64()) val funding_signed = FundingSigned(randomBytes32(), randomBytes64()) diff --git a/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Channel.scala b/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Channel.scala index 4ac5155d04..340e0845da 100644 --- a/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Channel.scala +++ b/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Channel.scala @@ -33,8 +33,8 @@ trait Channel { import fr.acinq.eclair.api.serde.JsonSupport.{formats, marshaller, serialization} val open: Route = postRequest("open") { implicit t => - formFields(nodeIdFormParam, "fundingSatoshis".as[Satoshi], "pushMsat".as[MilliSatoshi].?, "channelType".?, "fundingFeerateSatByte".as[FeeratePerByte].?, "channelFlags".as[Int].?, "openTimeoutSeconds".as[Timeout].?) { - (nodeId, fundingSatoshis, pushMsat, channelType, fundingFeerateSatByte, channelFlags, openTimeout_opt) => + formFields(nodeIdFormParam, "fundingSatoshis".as[Satoshi], "pushMsat".as[MilliSatoshi].?, "channelType".?, "fundingFeerateSatByte".as[FeeratePerByte].?, "announceChannel".as[Boolean].?, "openTimeoutSeconds".as[Timeout].?) { + (nodeId, fundingSatoshis, pushMsat, channelType, fundingFeerateSatByte, announceChannel_opt, openTimeout_opt) => val (channelTypeOk, channelType_opt) = channelType match { case Some(str) if str == ChannelTypes.Standard.toString => (true, Some(ChannelTypes.Standard)) case Some(str) if str == ChannelTypes.StaticRemoteKey.toString => (true, Some(ChannelTypes.StaticRemoteKey)) @@ -47,7 +47,7 @@ trait Channel { reject(MalformedFormFieldRejection("channelType", s"Channel type not supported: must be ${ChannelTypes.Standard.toString}, ${ChannelTypes.StaticRemoteKey.toString}, ${ChannelTypes.AnchorOutputs.toString} or ${ChannelTypes.AnchorOutputsZeroFeeHtlcTx.toString}")) } else { complete { - eclairApi.open(nodeId, fundingSatoshis, pushMsat, channelType_opt, fundingFeerateSatByte, channelFlags, openTimeout_opt) + eclairApi.open(nodeId, fundingSatoshis, pushMsat, channelType_opt, fundingFeerateSatByte, announceChannel_opt, openTimeout_opt) } } } From ffecd62cc133bb64aa916ed8d066b8ab6d7597ec Mon Sep 17 00:00:00 2001 From: Pierre-Marie Padiou Date: Thu, 27 Jan 2022 15:16:26 +0100 Subject: [PATCH 005/121] Move channel parameters to their own conf section (#2149) For consistency with existing sections: `router`, `relay`, `peer-connection`, etc. --- eclair-core/src/main/resources/reference.conf | 82 ++++++------ .../scala/fr/acinq/eclair/NodeParams.scala | 118 +++++++++--------- .../fr/acinq/eclair/channel/Channel.scala | 61 ++++++--- .../fr/acinq/eclair/channel/Helpers.scala | 24 ++-- .../channel/publish/FinalTxPublisher.scala | 2 +- .../channel/publish/MempoolTxMonitor.scala | 4 +- .../publish/ReplaceableTxPrePublisher.scala | 4 +- .../publish/ReplaceableTxPublisher.scala | 2 +- .../eclair/channel/publish/TxPublisher.scala | 4 +- .../channel/publish/TxTimeLocksMonitor.scala | 2 +- .../main/scala/fr/acinq/eclair/io/Peer.scala | 16 +-- .../payment/receive/MultiPartHandler.scala | 6 +- .../eclair/payment/relay/NodeRelay.scala | 2 +- .../payment/send/PaymentInitiator.scala | 4 +- .../remote/EclairInternalsSerializer.scala | 1 + .../scala/fr/acinq/eclair/router/Router.scala | 5 +- .../scala/fr/acinq/eclair/StartupSpec.scala | 2 +- .../scala/fr/acinq/eclair/TestConstants.scala | 90 ++++++------- .../fr/acinq/eclair/channel/HelpersSpec.scala | 16 +-- .../fr/acinq/eclair/channel/RestoreSpec.scala | 6 +- .../publish/MempoolTxMonitorSpec.scala | 8 +- .../ChannelStateTestsHelperMethods.scala | 16 +-- .../a/WaitForAcceptChannelStateSpec.scala | 10 +- .../a/WaitForOpenChannelStateSpec.scala | 14 +-- .../b/WaitForFundingCreatedStateSpec.scala | 8 +- .../b/WaitForFundingSignedStateSpec.scala | 8 +- .../channel/states/e/NormalStateSpec.scala | 32 ++--- .../channel/states/e/OfflineStateSpec.scala | 4 +- .../integration/ChannelIntegrationSpec.scala | 18 +-- .../eclair/integration/IntegrationSpec.scala | 12 +- .../integration/PaymentIntegrationSpec.scala | 20 +-- .../PerformanceIntegrationSpec.scala | 4 +- .../scala/fr/acinq/eclair/io/PeerSpec.scala | 2 +- .../eclair/payment/MultiPartHandlerSpec.scala | 8 +- .../eclair/payment/PaymentInitiatorSpec.scala | 2 +- .../eclair/router/AnnouncementsSpec.scala | 2 +- 36 files changed, 324 insertions(+), 295 deletions(-) diff --git a/eclair-core/src/main/resources/reference.conf b/eclair-core/src/main/resources/reference.conf index 5d3e88d078..10bab482ba 100644 --- a/eclair-core/src/main/resources/reference.conf +++ b/eclair-core/src/main/resources/reference.conf @@ -16,8 +16,6 @@ eclair { password = "" // password for basic auth, must be non empty if json-rpc api is enabled } - watch-spent-window = 1 minute // at startup watches will be put back within that window to reduce herd effect; must be > 0s - bitcoind { host = "localhost" rpcport = 8332 @@ -74,45 +72,49 @@ eclair { channel-flags { announce-channel = true } - } - dust-limit-satoshis = 546 - max-remote-dust-limit-satoshis = 600 - htlc-minimum-msat = 1 - // The following parameters apply to each HTLC direction (incoming or outgoing), which means that the total HTLC limits will be twice what is set here - max-htlc-value-in-flight-msat = 5000000000 // 50 mBTC - max-accepted-htlcs = 30 - - reserve-to-funding-ratio = 0.01 // recommended by BOLT #2 - max-reserve-to-funding-ratio = 0.05 // channel reserve can't be more than 5% of the funding amount (recommended: 1%) + dust-limit-satoshis = 546 + max-remote-dust-limit-satoshis = 600 + htlc-minimum-msat = 1 + // The following parameters apply to each HTLC direction (incoming or outgoing), which means that the total HTLC limits will be twice what is set here + max-htlc-value-in-flight-msat = 5000000000 // 50 mBTC + max-accepted-htlcs = 30 + + reserve-to-funding-ratio = 0.01 // recommended by BOLT #2 + max-reserve-to-funding-ratio = 0.05 // channel reserve can't be more than 5% of the funding amount (recommended: 1%) + min-funding-satoshis = 100000 + max-funding-satoshis = 16777215 // to open channels larger than 16777215 you must enable the large_channel_support feature in 'eclair.features' + + to-remote-delay-blocks = 720 // number of blocks that the other node's to-self outputs must be delayed (720 ~ 5 days) + max-to-local-delay-blocks = 2016 // maximum number of blocks that we are ready to accept for our own delayed outputs (2016 ~ 2 weeks) + mindepth-blocks = 3 + expiry-delta-blocks = 144 + // When we receive the preimage for an HTLC and want to fulfill it but the upstream peer stops responding, we want to + // avoid letting its HTLC-timeout transaction become enforceable on-chain (otherwise there is a race condition between + // our HTLC-success and their HTLC-timeout). + // We will close the channel when the HTLC-timeout will happen in less than this number. + // NB: this number effectively reduces the expiry-delta-blocks, so you may want to take that into account and increase + // expiry-delta-blocks. + fulfill-safety-before-timeout-blocks = 24 + min-final-expiry-delta-blocks = 30 // Bolt 11 invoice's min_final_cltv_expiry; must be strictly greater than fulfill-safety-before-timeout-blocks + max-block-processing-delay = 30 seconds // we add a random delay before processing blocks, capped at this value, to prevent herd effect + max-tx-publish-retry-delay = 60 seconds // we add a random delay before retrying failed transaction publication + + // The default strategy, when we encounter an unhandled exception or internal error, is to locally force-close the + // channel. Not only is there a delay before the channel balance gets refunded, but if the exception was due to some + // misconfiguration or bug in eclair that affects all channels, we risk force-closing all channels. + // This is why an alternative behavior is to simply log an error and stop the node. Note that if you don't closely + // monitor your node, there is a risk that your peers take advantage of the downtime to try and cheat by publishing a + // revoked commitment. Additionally, while there is no known way of triggering an internal error in eclair from the + // outside, there may very well be a bug that allows just that, which could be used as a way to remotely stop the node + // (with the default behavior, it would "only" cause a local force-close of the channel). + unhandled-exception-strategy = "local-close" // local-close or stop + + revocation-timeout = 20 seconds // after sending a commit_sig, we will wait for at most that duration before disconnecting + } balance-check-interval = 1 hour - to-remote-delay-blocks = 720 // number of blocks that the other node's to-self outputs must be delayed (720 ~ 5 days) - max-to-local-delay-blocks = 2016 // maximum number of blocks that we are ready to accept for our own delayed outputs (2016 ~ 2 weeks) - mindepth-blocks = 3 - expiry-delta-blocks = 144 - // When we receive the preimage for an HTLC and want to fulfill it but the upstream peer stops responding, we want to - // avoid letting its HTLC-timeout transaction become enforceable on-chain (otherwise there is a race condition between - // our HTLC-success and their HTLC-timeout). - // We will close the channel when the HTLC-timeout will happen in less than this number. - // NB: this number effectively reduces the expiry-delta-blocks, so you may want to take that into account and increase - // expiry-delta-blocks. - fulfill-safety-before-timeout-blocks = 24 - min-final-expiry-delta-blocks = 30 // Bolt 11 invoice's min_final_cltv_expiry; must be strictly greater than fulfill-safety-before-timeout-blocks - max-block-processing-delay = 30 seconds // we add a random delay before processing blocks, capped at this value, to prevent herd effect - max-tx-publish-retry-delay = 60 seconds // we add a random delay before retrying failed transaction publication - - // The default strategy, when we encounter an unhandled exception or internal error, is to locally force-close the - // channel. Not only is there a delay before the channel balance gets refunded, but if the exception was due to some - // misconfiguration or bug in eclair that affects all channels, we risk force-closing all channels. - // This is why an alternative behavior is to simply log an error and stop the node. Note that if you don't closely - // monitor your node, there is a risk that your peers take advantage of the downtime to try and cheat by publishing a - // revoked commitment. Additionally, while there is no known way of triggering an internal error in eclair from the - // outside, there may very well be a bug that allows just that, which could be used as a way to remotely stop the node - // (with the default behavior, it would "only" cause a local force-close of the channel). - unhandled-exception-strategy = "local-close" // local-close or stop - relay { fees { // Fees for public channels @@ -197,8 +199,6 @@ eclair { update-fee-min-diff-ratio = 0.1 } - revocation-timeout = 20 seconds // after sending a commit_sig, we will wait for at most that duration before disconnecting - peer-connection { auth-timeout = 15 seconds // will disconnect if connection authentication doesn't happen within that timeframe init-timeout = 15 seconds // will disconnect if initialization doesn't happen within that timeframe @@ -213,13 +213,13 @@ eclair { payment-request-expiry = 1 hour // default expiry for payment requests generated by this node multi-part-payment-expiry = 60 seconds // default expiry for receiving all parts of a multi-part payment - min-funding-satoshis = 100000 - max-funding-satoshis = 16777215 // to open channels larger than 16777215 you must enable the large_channel_support feature in 'eclair.features' max-payment-attempts = 5 autoprobe-count = 0 // number of parallel tasks that send test payments to detect invalid channels router { + watch-spent-window = 1 minute // at startup watches will be put back within that window to reduce herd effect; must be > 0s + channel-exclude-duration = 60 seconds // when a temporary channel failure is returned, we exclude the channel from our payment routes for this duration broadcast-interval = 60 seconds // see BOLT #7 init-timeout = 5 minutes diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala index badf738984..017d8a91b6 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala @@ -22,7 +22,7 @@ import fr.acinq.bitcoin.{Block, ByteVector32, Crypto, Satoshi} import fr.acinq.eclair.Setup.Seeds import fr.acinq.eclair.blockchain.fee._ import fr.acinq.eclair.channel.{Channel, ChannelFlags} -import fr.acinq.eclair.channel.Channel.UnhandledExceptionStrategy +import fr.acinq.eclair.channel.Channel.{ChannelConf, UnhandledExceptionStrategy} import fr.acinq.eclair.crypto.Noise.KeyPair import fr.acinq.eclair.crypto.keymanager.{ChannelKeyManager, NodeKeyManager} import fr.acinq.eclair.db._ @@ -62,36 +62,16 @@ case class NodeParams(nodeKeyManager: NodeKeyManager, private val overrideFeatures: Map[PublicKey, Features], syncWhitelist: Set[PublicKey], pluginParams: Seq[PluginParams], - dustLimit: Satoshi, - maxRemoteDustLimit: Satoshi, + channelConf: ChannelConf, onChainFeeConf: OnChainFeeConf, - maxHtlcValueInFlightMsat: UInt64, - maxAcceptedHtlcs: Int, - expiryDelta: CltvExpiryDelta, - fulfillSafetyBeforeTimeout: CltvExpiryDelta, - minFinalExpiryDelta: CltvExpiryDelta, - maxBlockProcessingDelay: FiniteDuration, - maxTxPublishRetryDelay: FiniteDuration, - htlcMinimum: MilliSatoshi, - toRemoteDelay: CltvExpiryDelta, - maxToLocalDelay: CltvExpiryDelta, - minDepthBlocks: Int, relayParams: RelayParams, - reserveToFundingRatio: Double, - maxReserveToFundingRatio: Double, - unhandledExceptionStrategy: UnhandledExceptionStrategy, db: Databases, - revocationTimeout: FiniteDuration, autoReconnect: Boolean, initialRandomReconnectDelay: FiniteDuration, maxReconnectInterval: FiniteDuration, chainHash: ByteVector32, - channelFlags: ChannelFlags, - watchSpentWindow: FiniteDuration, paymentRequestExpiry: FiniteDuration, multiPartPaymentExpiry: FiniteDuration, - minFundingSatoshis: Satoshi, - maxFundingSatoshis: Satoshi, peerConnectionConf: PeerConnection.Conf, routerConf: RouterConf, socksProxy_opt: Option[Socks5ProxyParams], @@ -222,6 +202,26 @@ object NodeParams extends Logging { "router.path-finding.hop-cost-millionths" -> "router.path-finding.default.hop-cost.fee-proportional-millionths", // v0.6.3 "channel-flags" -> "channel.channel-flags", + "dust-limit-satoshis" -> "channel.dust-limit-satoshis", + "max-remote-dust-limit-satoshis" -> "channel.max-remote-dust-limit-satoshis", + "htlc-minimum-msat" -> "channel.htlc-minimum-msat", + "max-htlc-value-in-flight-msat" -> "channel.max-htlc-value-in-flight-msat", + "max-accepted-htlcs" -> "channel.max-accepted-htlcs", + "reserve-to-funding-ratio" -> "channel.reserve-to-funding-ratio", + "max-reserve-to-funding-ratio" -> "channel.max-reserve-to-funding-ratio", + "min-funding-satoshis" -> "channel.min-funding-satoshis", + "max-funding-satoshis" -> "channel.max-funding-satoshis", + "to-remote-delay-blocks" -> "channel.to-remote-delay-blocks", + "max-to-local-delay-blocks" -> "channel.max-to-local-delay-blocks", + "mindepth-blocks" -> "channel.mindepth-blocks", + "expiry-delta-blocks" -> "channel.expiry-delta-blocks", + "fulfill-safety-before-timeout-blocks" -> "channel.fulfill-safety-before-timeout-blocks", + "min-final-expiry-delta-blocks" -> "channel.min-final-expiry-delta-blocks", + "max-block-processing-delay" -> "channel.max-block-processing-delay", + "max-tx-publish-retry-delay" -> "channel.max-tx-publish-retry-delay", + "unhandled-exception-strategy" -> "channel.unhandled-exception-strategy", + "revocation-timeout" -> "channel.revocation-timeout", + "watch-spent-window" -> "router.watch-spent-window", ) deprecatedKeyPaths.foreach { case (old, new_) => require(!config.hasPath(old), s"configuration key '$old' has been replaced by '$new_'") @@ -239,29 +239,29 @@ object NodeParams extends Logging { val color = ByteVector.fromValidHex(config.getString("node-color")) require(color.size == 3, "color should be a 3-bytes hex buffer") - val watchSpentWindow = FiniteDuration(config.getDuration("watch-spent-window").getSeconds, TimeUnit.SECONDS) - require(watchSpentWindow > 0.seconds, "watch-spent-window must be strictly greater than 0") + val watchSpentWindow = FiniteDuration(config.getDuration("router.watch-spent-window").getSeconds, TimeUnit.SECONDS) + require(watchSpentWindow > 0.seconds, "router.watch-spent-window must be strictly greater than 0") - val dustLimitSatoshis = Satoshi(config.getLong("dust-limit-satoshis")) + val dustLimitSatoshis = Satoshi(config.getLong("channel.dust-limit-satoshis")) if (chainHash == Block.LivenetGenesisBlock.hash) { require(dustLimitSatoshis >= Channel.MIN_DUST_LIMIT, s"dust limit must be greater than ${Channel.MIN_DUST_LIMIT}") } - val htlcMinimum = MilliSatoshi(config.getInt("htlc-minimum-msat")) - require(htlcMinimum > 0.msat, "htlc-minimum-msat must be strictly greater than 0") + val htlcMinimum = MilliSatoshi(config.getInt("channel.htlc-minimum-msat")) + require(htlcMinimum > 0.msat, "channel.htlc-minimum-msat must be strictly greater than 0") - val maxAcceptedHtlcs = config.getInt("max-accepted-htlcs") - require(maxAcceptedHtlcs <= Channel.MAX_ACCEPTED_HTLCS, s"max-accepted-htlcs must be lower than ${Channel.MAX_ACCEPTED_HTLCS}") + val maxAcceptedHtlcs = config.getInt("channel.max-accepted-htlcs") + require(maxAcceptedHtlcs <= Channel.MAX_ACCEPTED_HTLCS, s"channel.max-accepted-htlcs must be lower than ${Channel.MAX_ACCEPTED_HTLCS}") - val maxToLocalCLTV = CltvExpiryDelta(config.getInt("max-to-local-delay-blocks")) - val offeredCLTV = CltvExpiryDelta(config.getInt("to-remote-delay-blocks")) + val maxToLocalCLTV = CltvExpiryDelta(config.getInt("channel.max-to-local-delay-blocks")) + val offeredCLTV = CltvExpiryDelta(config.getInt("channel.to-remote-delay-blocks")) require(maxToLocalCLTV <= Channel.MAX_TO_SELF_DELAY && offeredCLTV <= Channel.MAX_TO_SELF_DELAY, s"CLTV delay values too high, max is ${Channel.MAX_TO_SELF_DELAY}") - val expiryDelta = CltvExpiryDelta(config.getInt("expiry-delta-blocks")) - val fulfillSafetyBeforeTimeout = CltvExpiryDelta(config.getInt("fulfill-safety-before-timeout-blocks")) - require(fulfillSafetyBeforeTimeout * 2 < expiryDelta, "fulfill-safety-before-timeout-blocks must be smaller than expiry-delta-blocks / 2 because it effectively reduces that delta; if you want to increase this value, you may want to increase expiry-delta-blocks as well") - val minFinalExpiryDelta = CltvExpiryDelta(config.getInt("min-final-expiry-delta-blocks")) - require(minFinalExpiryDelta > fulfillSafetyBeforeTimeout, "min-final-expiry-delta-blocks must be strictly greater than fulfill-safety-before-timeout-blocks; otherwise it may lead to undesired channel closure") + val expiryDelta = CltvExpiryDelta(config.getInt("channel.expiry-delta-blocks")) + val fulfillSafetyBeforeTimeout = CltvExpiryDelta(config.getInt("channel.fulfill-safety-before-timeout-blocks")) + require(fulfillSafetyBeforeTimeout * 2 < expiryDelta, "channel.fulfill-safety-before-timeout-blocks must be smaller than channel.expiry-delta-blocks / 2 because it effectively reduces that delta; if you want to increase this value, you may want to increase expiry-delta-blocks as well") + val minFinalExpiryDelta = CltvExpiryDelta(config.getInt("channel.min-final-expiry-delta-blocks")) + require(minFinalExpiryDelta > fulfillSafetyBeforeTimeout, "channel.min-final-expiry-delta-blocks must be strictly greater than channel.fulfill-safety-before-timeout-blocks; otherwise it may lead to undesired channel closure") val nodeAlias = config.getString("node-alias") require(nodeAlias.getBytes("UTF-8").length <= 32, "invalid alias, too long (max allowed 32 bytes)") @@ -369,7 +369,7 @@ object NodeParams extends Logging { PathFindingExperimentConf(experiments.toMap) } - val unhandledExceptionStrategy = config.getString("unhandled-exception-strategy") match { + val unhandledExceptionStrategy = config.getString("channel.unhandled-exception-strategy") match { case "local-close" => UnhandledExceptionStrategy.LocalClose case "stop" => UnhandledExceptionStrategy.Stop } @@ -398,8 +398,28 @@ object NodeParams extends Logging { pluginParams = pluginParams, overrideFeatures = overrideFeatures, syncWhitelist = syncWhitelist, - dustLimit = dustLimitSatoshis, - maxRemoteDustLimit = Satoshi(config.getLong("max-remote-dust-limit-satoshis")), + channelConf = ChannelConf( + channelFlags = channelFlags, + dustLimit = dustLimitSatoshis, + maxRemoteDustLimit = Satoshi(config.getLong("channel.max-remote-dust-limit-satoshis")), + htlcMinimum = htlcMinimum, + maxHtlcValueInFlightMsat = UInt64(config.getLong("channel.max-htlc-value-in-flight-msat")), + maxAcceptedHtlcs = maxAcceptedHtlcs, + reserveToFundingRatio = config.getDouble("channel.reserve-to-funding-ratio"), + maxReserveToFundingRatio = config.getDouble("channel.max-reserve-to-funding-ratio"), + minFundingSatoshis = Satoshi(config.getLong("channel.min-funding-satoshis")), + maxFundingSatoshis = Satoshi(config.getLong("channel.max-funding-satoshis")), + toRemoteDelay = offeredCLTV, + maxToLocalDelay = maxToLocalCLTV, + minDepthBlocks = config.getInt("channel.mindepth-blocks"), + expiryDelta = expiryDelta, + fulfillSafetyBeforeTimeout = fulfillSafetyBeforeTimeout, + minFinalExpiryDelta = minFinalExpiryDelta, + maxBlockProcessingDelay = FiniteDuration(config.getDuration("channel.max-block-processing-delay").getSeconds, TimeUnit.SECONDS), + maxTxPublishRetryDelay = FiniteDuration(config.getDuration("channel.max-tx-publish-retry-delay").getSeconds, TimeUnit.SECONDS), + unhandledExceptionStrategy = unhandledExceptionStrategy, + revocationTimeout = FiniteDuration(config.getDuration("channel.revocation-timeout").getSeconds, TimeUnit.SECONDS) + ), onChainFeeConf = OnChainFeeConf( feeTargets = feeTargets, feeEstimator = feeEstimator, @@ -428,37 +448,18 @@ object NodeParams extends Logging { nodeId -> tolerance }.toMap ), - maxHtlcValueInFlightMsat = UInt64(config.getLong("max-htlc-value-in-flight-msat")), - maxAcceptedHtlcs = maxAcceptedHtlcs, - expiryDelta = expiryDelta, - fulfillSafetyBeforeTimeout = fulfillSafetyBeforeTimeout, - minFinalExpiryDelta = minFinalExpiryDelta, - maxBlockProcessingDelay = FiniteDuration(config.getDuration("max-block-processing-delay").getSeconds, TimeUnit.SECONDS), - maxTxPublishRetryDelay = FiniteDuration(config.getDuration("max-tx-publish-retry-delay").getSeconds, TimeUnit.SECONDS), - htlcMinimum = htlcMinimum, - toRemoteDelay = CltvExpiryDelta(config.getInt("to-remote-delay-blocks")), - maxToLocalDelay = CltvExpiryDelta(config.getInt("max-to-local-delay-blocks")), - minDepthBlocks = config.getInt("mindepth-blocks"), relayParams = RelayParams( publicChannelFees = getRelayFees(config.getConfig("relay.fees.public-channels")), privateChannelFees = getRelayFees(config.getConfig("relay.fees.private-channels")), minTrampolineFees = getRelayFees(config.getConfig("relay.fees.min-trampoline")), ), - reserveToFundingRatio = config.getDouble("reserve-to-funding-ratio"), - maxReserveToFundingRatio = config.getDouble("max-reserve-to-funding-ratio"), - unhandledExceptionStrategy = unhandledExceptionStrategy, db = database, - revocationTimeout = FiniteDuration(config.getDuration("revocation-timeout").getSeconds, TimeUnit.SECONDS), autoReconnect = config.getBoolean("auto-reconnect"), initialRandomReconnectDelay = FiniteDuration(config.getDuration("initial-random-reconnect-delay").getSeconds, TimeUnit.SECONDS), maxReconnectInterval = FiniteDuration(config.getDuration("max-reconnect-interval").getSeconds, TimeUnit.SECONDS), chainHash = chainHash, - channelFlags = channelFlags, - watchSpentWindow = watchSpentWindow, paymentRequestExpiry = FiniteDuration(config.getDuration("payment-request-expiry").getSeconds, TimeUnit.SECONDS), multiPartPaymentExpiry = FiniteDuration(config.getDuration("multi-part-payment-expiry").getSeconds, TimeUnit.SECONDS), - minFundingSatoshis = Satoshi(config.getLong("min-funding-satoshis")), - maxFundingSatoshis = Satoshi(config.getLong("max-funding-satoshis")), peerConnectionConf = PeerConnection.Conf( authTimeout = FiniteDuration(config.getDuration("peer-connection.auth-timeout").getSeconds, TimeUnit.SECONDS), initTimeout = FiniteDuration(config.getDuration("peer-connection.init-timeout").getSeconds, TimeUnit.SECONDS), @@ -470,6 +471,7 @@ object NodeParams extends Logging { maxOnionMessagesPerSecond = config.getInt("onion-messages.max-per-peer-per-second") ), routerConf = RouterConf( + watchSpentWindow = watchSpentWindow, channelExcludeDuration = FiniteDuration(config.getDuration("router.channel-exclude-duration").getSeconds, TimeUnit.SECONDS), routerBroadcastInterval = FiniteDuration(config.getDuration("router.broadcast-interval").getSeconds, TimeUnit.SECONDS), requestNodeAnnouncements = config.getBoolean("router.sync.request-node-announcements"), diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala index 118422c5ed..8300448866 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala @@ -63,6 +63,27 @@ import scala.util.{Failure, Random, Success, Try} object Channel { + case class ChannelConf(channelFlags: ChannelFlags, + dustLimit: Satoshi, + maxRemoteDustLimit: Satoshi, + htlcMinimum: MilliSatoshi, + maxHtlcValueInFlightMsat: UInt64, + maxAcceptedHtlcs: Int, + reserveToFundingRatio: Double, + maxReserveToFundingRatio: Double, + minFundingSatoshis: Satoshi, + maxFundingSatoshis: Satoshi, + toRemoteDelay: CltvExpiryDelta, + maxToLocalDelay: CltvExpiryDelta, + minDepthBlocks: Int, + expiryDelta: CltvExpiryDelta, + fulfillSafetyBeforeTimeout: CltvExpiryDelta, + minFinalExpiryDelta: CltvExpiryDelta, + maxBlockProcessingDelay: FiniteDuration, + maxTxPublishRetryDelay: FiniteDuration, + unhandledExceptionStrategy: UnhandledExceptionStrategy, + revocationTimeout: FiniteDuration) + trait TxPublisherFactory { def spawnTxPublisher(context: ActorContext, remoteNodeId: PublicKey): typed.ActorRef[TxPublisher.Command] } @@ -164,7 +185,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, remo // this will be used to detect htlc timeouts context.system.eventStream.subscribe(self, classOf[CurrentBlockHeight]) // the constant delay by which we delay processing of blocks (it will be smoothened among all channels) - private val blockProcessingDelay = Random.nextLong(nodeParams.maxBlockProcessingDelay.toMillis + 1).millis + private val blockProcessingDelay = Random.nextLong(nodeParams.channelConf.maxBlockProcessingDelay.toMillis + 1).millis // this will be used to make sure the current commitment fee is up-to-date context.system.eventStream.subscribe(self, classOf[CurrentFeerates]) @@ -303,9 +324,9 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, remo val fees = getRelayFees(nodeParams, remoteNodeId, data.commitments) if (fees.feeBase != normal.channelUpdate.feeBaseMsat || fees.feeProportionalMillionths != normal.channelUpdate.feeProportionalMillionths || - nodeParams.expiryDelta != normal.channelUpdate.cltvExpiryDelta) { + nodeParams.channelConf.expiryDelta != normal.channelUpdate.cltvExpiryDelta) { log.info("refreshing channel_update due to configuration changes") - self ! CMD_UPDATE_RELAY_FEE(ActorRef.noSender, fees.feeBase, fees.feeProportionalMillionths, Some(nodeParams.expiryDelta)) + self ! CMD_UPDATE_RELAY_FEE(ActorRef.noSender, fees.feeBase, fees.feeProportionalMillionths, Some(nodeParams.channelConf.expiryDelta)) } // we need to periodically re-send channel updates, otherwise channel will be considered stale and get pruned by network // we take into account the date of the last update so that we don't send superfluous updates when we restart the app @@ -347,7 +368,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, remo context.system.eventStream.publish(ChannelCreated(self, peer, remoteNodeId, isFunder = false, open.temporaryChannelId, open.feeratePerKw, None)) val fundingPubkey = keyManager.fundingPublicKey(localParams.fundingKeyPath).publicKey val channelKeyPath = keyManager.keyPath(localParams, channelConfig) - val minimumDepth = Helpers.minDepthForFunding(nodeParams, open.fundingSatoshis) + val minimumDepth = Helpers.minDepthForFunding(nodeParams.channelConf, open.fundingSatoshis) // In order to allow TLV extensions and keep backwards-compatibility, we include an empty upfront_shutdown_script if this feature is not used. // See https://github.com/lightningnetwork/lightning-rfc/pull/714. val localShutdownScript = if (Features.canUseFeature(localParams.initFeatures, remoteInit.features, Features.UpfrontShutdownScript)) localParams.defaultFinalScriptPubKey else ByteVector.empty @@ -521,7 +542,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, remo // NB: we don't send a ChannelSignatureSent for the first commit log.info(s"waiting for them to publish the funding tx for channelId=$channelId fundingTxid=${commitInput.outPoint.txid}") watchFundingTx(commitments) - val fundingMinDepth = Helpers.minDepthForFunding(nodeParams, fundingAmount) + val fundingMinDepth = Helpers.minDepthForFunding(nodeParams.channelConf, fundingAmount) blockchain ! WatchFundingConfirmed(self, commitInput.outPoint.txid, fundingMinDepth) goto(WAIT_FOR_FUNDING_CONFIRMED) using DATA_WAIT_FOR_FUNDING_CONFIRMED(commitments, None, nodeParams.currentBlockHeight, None, Right(fundingSigned)) storing() sending fundingSigned } @@ -561,7 +582,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, remo context.system.eventStream.publish(ChannelSignatureReceived(self, commitments)) log.info(s"publishing funding tx for channelId=$channelId fundingTxid=${commitInput.outPoint.txid}") watchFundingTx(commitments) - blockchain ! WatchFundingConfirmed(self, commitInput.outPoint.txid, nodeParams.minDepthBlocks) + blockchain ! WatchFundingConfirmed(self, commitInput.outPoint.txid, nodeParams.channelConf.minDepthBlocks) log.info(s"committing txid=${fundingTx.txid}") // we will publish the funding tx only after the channel state has been written to disk because we want to @@ -617,7 +638,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, remo Try(Transaction.correctlySpends(commitments.fullySignedLocalCommitTx(keyManager).tx, Seq(fundingTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)) match { case Success(_) => log.info(s"channelId=${commitments.channelId} was confirmed at blockHeight=$blockHeight txIndex=$txIndex") - blockchain ! WatchFundingLost(self, commitments.commitInput.outPoint.txid, nodeParams.minDepthBlocks) + blockchain ! WatchFundingLost(self, commitments.commitInput.outPoint.txid, nodeParams.channelConf.minDepthBlocks) if (!d.commitments.localParams.isFunder) context.system.eventStream.publish(TransactionPublished(commitments.channelId, remoteNodeId, fundingTx, 0 sat, "funding")) context.system.eventStream.publish(TransactionConfirmed(commitments.channelId, remoteNodeId, fundingTx)) val channelKeyPath = keyManager.keyPath(d.commitments.localParams, commitments.channelConfig) @@ -670,7 +691,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, remo context.system.eventStream.publish(ShortChannelIdAssigned(self, commitments.channelId, shortChannelId, None)) // we create a channel_update early so that we can use it to send payments through this channel, but it won't be propagated to other nodes since the channel is not yet announced val fees = getRelayFees(nodeParams, remoteNodeId, commitments) - val initialChannelUpdate = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, shortChannelId, nodeParams.expiryDelta, d.commitments.remoteParams.htlcMinimum, fees.feeBase, fees.feeProportionalMillionths, commitments.capacity.toMilliSatoshi, enable = Helpers.aboveReserve(d.commitments)) + val initialChannelUpdate = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, shortChannelId, nodeParams.channelConf.expiryDelta, d.commitments.remoteParams.htlcMinimum, fees.feeBase, fees.feeProportionalMillionths, commitments.capacity.toMilliSatoshi, enable = Helpers.aboveReserve(d.commitments)) // we need to periodically re-send channel updates, otherwise channel will be considered stale and get pruned by network context.system.scheduler.scheduleWithFixedDelay(initialDelay = REFRESH_CHANNEL_UPDATE_INTERVAL, delay = REFRESH_CHANNEL_UPDATE_INTERVAL, receiver = self, message = BroadcastChannelUpdate(PeriodicRefresh)) goto(NORMAL) using DATA_NORMAL(commitments.copy(remoteNextCommitInfo = Right(nextPerCommitmentPoint)), shortChannelId, buried = false, None, initialChannelUpdate, None, None, None) storing() @@ -817,7 +838,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, remo } context.system.eventStream.publish(ChannelSignatureSent(self, commitments1)) // we expect a quick response from our peer - startSingleTimer(RevocationTimeout.toString, RevocationTimeout(commitments1.remoteCommit.index, peer), nodeParams.revocationTimeout) + startSingleTimer(RevocationTimeout.toString, RevocationTimeout(commitments1.remoteCommit.index, peer), nodeParams.channelConf.revocationTimeout) handleCommandSuccess(c, d.copy(commitments = commitments1)).storing().sending(commit).acking(commitments1.localChanges.signed) case Left(cause) => handleCommandError(cause, c) } @@ -1174,7 +1195,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, remo } context.system.eventStream.publish(ChannelSignatureSent(self, commitments1)) // we expect a quick response from our peer - startSingleTimer(RevocationTimeout.toString, RevocationTimeout(commitments1.remoteCommit.index, peer), nodeParams.revocationTimeout) + startSingleTimer(RevocationTimeout.toString, RevocationTimeout(commitments1.remoteCommit.index, peer), nodeParams.channelConf.revocationTimeout) handleCommandSuccess(c, d.copy(commitments = commitments1)).storing().sending(commit).acking(commitments1.localChanges.signed) case Left(cause) => handleCommandError(cause, c) } @@ -1467,7 +1488,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, remo case Event(WatchOutputSpentTriggered(tx), d: DATA_CLOSING) => // one of the outputs of the local/remote/revoked commit was spent // we just put a watch to be notified when it is confirmed - blockchain ! WatchTxConfirmed(self, tx.txid, nodeParams.minDepthBlocks) + blockchain ! WatchTxConfirmed(self, tx.txid, nodeParams.channelConf.minDepthBlocks) // when a remote or local commitment tx containing outgoing htlcs is published on the network, // we watch it in order to extract payment preimage if funds are pulled by the counterparty // we can then use these preimages to fulfill origin htlcs @@ -1502,7 +1523,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, remo val (localCommitPublished1, claimHtlcTx_opt) = Closing.claimLocalCommitHtlcTxOutput(localCommitPublished, keyManager, d.commitments, tx, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) claimHtlcTx_opt.foreach(claimHtlcTx => { txPublisher ! PublishFinalTx(claimHtlcTx, claimHtlcTx.fee, None) - blockchain ! WatchTxConfirmed(self, claimHtlcTx.tx.txid, nodeParams.minDepthBlocks) + blockchain ! WatchTxConfirmed(self, claimHtlcTx.tx.txid, nodeParams.channelConf.minDepthBlocks) }) Closing.updateLocalCommitPublished(localCommitPublished1, tx) }), @@ -1662,10 +1683,10 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, remo when(SYNCING)(handleExceptions { case Event(_: ChannelReestablish, d: DATA_WAIT_FOR_FUNDING_CONFIRMED) => val minDepth = if (d.commitments.localParams.isFunder) { - nodeParams.minDepthBlocks + nodeParams.channelConf.minDepthBlocks } else { // when we're fundee we scale the min_depth confirmations depending on the funding amount - Helpers.minDepthForFunding(nodeParams, d.commitments.commitInput.txOut.amount) + Helpers.minDepthForFunding(nodeParams.channelConf, d.commitments.commitInput.txOut.amount) } // we put back the watch (operation is idempotent) because the event may have been fired while we were in OFFLINE blockchain ! WatchFundingConfirmed(self, d.commitments.commitInput.outPoint.txid, minDepth) @@ -1704,7 +1725,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, remo commitments1.remoteNextCommitInfo match { case Left(_) => // we expect them to (re-)send the revocation immediately - startSingleTimer(RevocationTimeout.toString, RevocationTimeout(commitments1.remoteCommit.index, peer), nodeParams.revocationTimeout) + startSingleTimer(RevocationTimeout.toString, RevocationTimeout(commitments1.remoteCommit.index, peer), nodeParams.channelConf.revocationTimeout) case _ => () } @@ -2145,7 +2166,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, remo val knownSpendingTxs = Set(commitments.localCommit.commitTxAndRemoteSig.commitTx.tx.txid, commitments.remoteCommit.txid) ++ commitments.remoteNextCommitInfo.left.toSeq.map(_.nextRemoteCommit.txid).toSet ++ additionalKnownSpendingTxs blockchain ! WatchFundingSpent(self, commitments.commitInput.outPoint.txid, commitments.commitInput.outPoint.index.toInt, knownSpendingTxs) // TODO: implement this? (not needed if we use a reasonable min_depth) - //blockchain ! WatchLost(self, commitments.commitInput.outPoint.txid, nodeParams.minDepthBlocks, BITCOIN_FUNDING_LOST) + //blockchain ! WatchLost(self, commitments.commitInput.outPoint.txid, nodeParams.channelConf.minDepthBlocks, BITCOIN_FUNDING_LOST) } /** @@ -2281,7 +2302,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, remo private def handleNewBlock(c: CurrentBlockHeight, d: HasCommitments) = { val timedOutOutgoing = d.commitments.timedOutOutgoingHtlcs(c.blockHeight) - val almostTimedOutIncoming = d.commitments.almostTimedOutIncomingHtlcs(c.blockHeight, nodeParams.fulfillSafetyBeforeTimeout) + val almostTimedOutIncoming = d.commitments.almostTimedOutIncomingHtlcs(c.blockHeight, nodeParams.channelConf.fulfillSafetyBeforeTimeout) if (timedOutOutgoing.nonEmpty) { // Downstream timed out. handleLocalError(HtlcsTimedoutDownstream(d.channelId, timedOutOutgoing), d, Some(c)) @@ -2338,7 +2359,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, remo spendLocalCurrent(dd) sending error case _ => // unhandled exception: we apply the configured strategy - nodeParams.unhandledExceptionStrategy match { + nodeParams.channelConf.unhandledExceptionStrategy match { case UnhandledExceptionStrategy.LocalClose => spendLocalCurrent(dd) sending error case UnhandledExceptionStrategy.Stop => @@ -2399,7 +2420,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, remo // the funder pays the fee val fee = if (isFunder) closingTx.fee else 0.sat txPublisher ! PublishFinalTx(closingTx, fee, None) - blockchain ! WatchTxConfirmed(self, closingTx.tx.txid, nodeParams.minDepthBlocks) + blockchain ! WatchTxConfirmed(self, closingTx.tx.txid, nodeParams.channelConf.minDepthBlocks) } private def spendLocalCurrent(d: HasCommitments) = { @@ -2438,7 +2459,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, remo */ private def watchConfirmedIfNeeded(txs: Iterable[Transaction], irrevocablySpent: Map[OutPoint, Transaction]): Unit = { val (skip, process) = txs.partition(Closing.inputsAlreadySpent(_, irrevocablySpent)) - process.foreach(tx => blockchain ! WatchTxConfirmed(self, tx.txid, nodeParams.minDepthBlocks)) + process.foreach(tx => blockchain ! WatchTxConfirmed(self, tx.txid, nodeParams.channelConf.minDepthBlocks)) skip.foreach(tx => log.info(s"no need to watch txid=${tx.txid}, it has already been confirmed")) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala index 5893d59575..7564fe6d8f 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala @@ -23,7 +23,7 @@ import fr.acinq.bitcoin._ import fr.acinq.eclair._ import fr.acinq.eclair.blockchain.OnChainAddressGenerator import fr.acinq.eclair.blockchain.fee.{FeeEstimator, FeeTargets, FeeratePerKw} -import fr.acinq.eclair.channel.Channel.REFRESH_CHANNEL_UPDATE_INTERVAL +import fr.acinq.eclair.channel.Channel.{ChannelConf, REFRESH_CHANNEL_UPDATE_INTERVAL} import fr.acinq.eclair.crypto.Generators import fr.acinq.eclair.crypto.keymanager.ChannelKeyManager import fr.acinq.eclair.db.ChannelsDb @@ -70,13 +70,13 @@ object Helpers { * @param fundingSatoshis funding amount of the channel * @return number of confirmations needed */ - def minDepthForFunding(nodeParams: NodeParams, fundingSatoshis: Satoshi): Long = fundingSatoshis match { - case funding if funding <= Channel.MAX_FUNDING => nodeParams.minDepthBlocks + def minDepthForFunding(channelConf: ChannelConf, fundingSatoshis: Satoshi): Long = fundingSatoshis match { + case funding if funding <= Channel.MAX_FUNDING => channelConf.minDepthBlocks case funding => val blockReward = 6.25 // this is true as of ~May 2020, but will be too large after 2024 val scalingFactor = 15 val blocksToReachFunding = (((scalingFactor * funding.toBtc.toDouble) / blockReward).ceil + 1).toInt - nodeParams.minDepthBlocks.max(blocksToReachFunding) + channelConf.minDepthBlocks.max(blocksToReachFunding) } def extractShutdownScript(channelId: ByteVector32, localFeatures: Features, remoteFeatures: Features, upfrontShutdownScript_opt: Option[ByteVector]): Either[ChannelException, Option[ByteVector]] = { @@ -104,16 +104,16 @@ object Helpers { // MUST reject the channel. if (nodeParams.chainHash != open.chainHash) return Left(InvalidChainHash(open.temporaryChannelId, local = nodeParams.chainHash, remote = open.chainHash)) - if (open.fundingSatoshis < nodeParams.minFundingSatoshis || open.fundingSatoshis > nodeParams.maxFundingSatoshis) return Left(InvalidFundingAmount(open.temporaryChannelId, open.fundingSatoshis, nodeParams.minFundingSatoshis, nodeParams.maxFundingSatoshis)) + if (open.fundingSatoshis < nodeParams.channelConf.minFundingSatoshis || open.fundingSatoshis > nodeParams.channelConf.maxFundingSatoshis) return Left(InvalidFundingAmount(open.temporaryChannelId, open.fundingSatoshis, nodeParams.channelConf.minFundingSatoshis, nodeParams.channelConf.maxFundingSatoshis)) // BOLT #2: Channel funding limits - if (open.fundingSatoshis >= Channel.MAX_FUNDING && !localFeatures.hasFeature(Features.Wumbo)) return Left(InvalidFundingAmount(open.temporaryChannelId, open.fundingSatoshis, nodeParams.minFundingSatoshis, Channel.MAX_FUNDING)) + if (open.fundingSatoshis >= Channel.MAX_FUNDING && !localFeatures.hasFeature(Features.Wumbo)) return Left(InvalidFundingAmount(open.temporaryChannelId, open.fundingSatoshis, nodeParams.channelConf.minFundingSatoshis, Channel.MAX_FUNDING)) // BOLT #2: The receiving node MUST fail the channel if: push_msat is greater than funding_satoshis * 1000. if (open.pushMsat > open.fundingSatoshis) return Left(InvalidPushAmount(open.temporaryChannelId, open.pushMsat, open.fundingSatoshis.toMilliSatoshi)) // BOLT #2: The receiving node MUST fail the channel if: to_self_delay is unreasonably large. - if (open.toSelfDelay > Channel.MAX_TO_SELF_DELAY || open.toSelfDelay > nodeParams.maxToLocalDelay) return Left(ToSelfDelayTooHigh(open.temporaryChannelId, open.toSelfDelay, nodeParams.maxToLocalDelay)) + if (open.toSelfDelay > Channel.MAX_TO_SELF_DELAY || open.toSelfDelay > nodeParams.channelConf.maxToLocalDelay) return Left(ToSelfDelayTooHigh(open.temporaryChannelId, open.toSelfDelay, nodeParams.channelConf.maxToLocalDelay)) // BOLT #2: The receiving node MUST fail the channel if: max_accepted_htlcs is greater than 483. if (open.maxAcceptedHtlcs > Channel.MAX_ACCEPTED_HTLCS) return Left(InvalidMaxAcceptedHtlcs(open.temporaryChannelId, open.maxAcceptedHtlcs, Channel.MAX_ACCEPTED_HTLCS)) @@ -121,7 +121,7 @@ object Helpers { // BOLT #2: The receiving node MUST fail the channel if: it considers feerate_per_kw too small for timely processing. if (isFeeTooSmall(open.feeratePerKw)) return Left(FeerateTooSmall(open.temporaryChannelId, open.feeratePerKw)) - if (open.dustLimitSatoshis > nodeParams.maxRemoteDustLimit) return Left(DustLimitTooLarge(open.temporaryChannelId, open.dustLimitSatoshis, nodeParams.maxRemoteDustLimit)) + if (open.dustLimitSatoshis > nodeParams.channelConf.maxRemoteDustLimit) return Left(DustLimitTooLarge(open.temporaryChannelId, open.dustLimitSatoshis, nodeParams.channelConf.maxRemoteDustLimit)) // BOLT #2: The receiving node MUST fail the channel if: dust_limit_satoshis is greater than channel_reserve_satoshis. if (open.dustLimitSatoshis > open.channelReserveSatoshis) return Left(DustLimitTooLarge(open.temporaryChannelId, open.dustLimitSatoshis, open.channelReserveSatoshis)) @@ -145,7 +145,7 @@ object Helpers { // now, but it will be done later when we receive `funding_created` val reserveToFundingRatio = open.channelReserveSatoshis.toLong.toDouble / Math.max(open.fundingSatoshis.toLong, 1) - if (reserveToFundingRatio > nodeParams.maxReserveToFundingRatio) return Left(ChannelReserveTooHigh(open.temporaryChannelId, open.channelReserveSatoshis, reserveToFundingRatio, nodeParams.maxReserveToFundingRatio)) + if (reserveToFundingRatio > nodeParams.channelConf.maxReserveToFundingRatio) return Left(ChannelReserveTooHigh(open.temporaryChannelId, open.channelReserveSatoshis, reserveToFundingRatio, nodeParams.channelConf.maxReserveToFundingRatio)) val channelFeatures = ChannelFeatures(channelType, localFeatures, remoteFeatures) extractShutdownScript(open.temporaryChannelId, localFeatures, remoteFeatures, open.upfrontShutdownScript_opt).map(script_opt => (channelFeatures, script_opt)) @@ -175,14 +175,14 @@ object Helpers { if (accept.dustLimitSatoshis < Channel.MIN_DUST_LIMIT) return Left(DustLimitTooSmall(accept.temporaryChannelId, accept.dustLimitSatoshis, Channel.MIN_DUST_LIMIT)) } - if (accept.dustLimitSatoshis > nodeParams.maxRemoteDustLimit) return Left(DustLimitTooLarge(open.temporaryChannelId, accept.dustLimitSatoshis, nodeParams.maxRemoteDustLimit)) + if (accept.dustLimitSatoshis > nodeParams.channelConf.maxRemoteDustLimit) return Left(DustLimitTooLarge(open.temporaryChannelId, accept.dustLimitSatoshis, nodeParams.channelConf.maxRemoteDustLimit)) // BOLT #2: The receiving node MUST fail the channel if: dust_limit_satoshis is greater than channel_reserve_satoshis. if (accept.dustLimitSatoshis > accept.channelReserveSatoshis) return Left(DustLimitTooLarge(accept.temporaryChannelId, accept.dustLimitSatoshis, accept.channelReserveSatoshis)) // if minimum_depth is unreasonably large: // MAY reject the channel. - if (accept.toSelfDelay > Channel.MAX_TO_SELF_DELAY || accept.toSelfDelay > nodeParams.maxToLocalDelay) return Left(ToSelfDelayTooHigh(accept.temporaryChannelId, accept.toSelfDelay, nodeParams.maxToLocalDelay)) + if (accept.toSelfDelay > Channel.MAX_TO_SELF_DELAY || accept.toSelfDelay > nodeParams.channelConf.maxToLocalDelay) return Left(ToSelfDelayTooHigh(accept.temporaryChannelId, accept.toSelfDelay, nodeParams.channelConf.maxToLocalDelay)) // if channel_reserve_satoshis is less than dust_limit_satoshis within the open_channel message: // MUST reject the channel. @@ -193,7 +193,7 @@ object Helpers { if (open.channelReserveSatoshis < accept.dustLimitSatoshis) return Left(DustLimitAboveOurChannelReserve(accept.temporaryChannelId, accept.dustLimitSatoshis, open.channelReserveSatoshis)) val reserveToFundingRatio = accept.channelReserveSatoshis.toLong.toDouble / Math.max(open.fundingSatoshis.toLong, 1) - if (reserveToFundingRatio > nodeParams.maxReserveToFundingRatio) return Left(ChannelReserveTooHigh(open.temporaryChannelId, accept.channelReserveSatoshis, reserveToFundingRatio, nodeParams.maxReserveToFundingRatio)) + if (reserveToFundingRatio > nodeParams.channelConf.maxReserveToFundingRatio) return Left(ChannelReserveTooHigh(open.temporaryChannelId, accept.channelReserveSatoshis, reserveToFundingRatio, nodeParams.channelConf.maxReserveToFundingRatio)) val channelFeatures = ChannelFeatures(channelType, localFeatures, remoteFeatures) extractShutdownScript(accept.temporaryChannelId, localFeatures, remoteFeatures, accept.upfrontShutdownScript_opt).map(script_opt => (channelFeatures, script_opt)) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/FinalTxPublisher.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/FinalTxPublisher.scala index bc09975151..e712e49f3c 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/FinalTxPublisher.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/FinalTxPublisher.scala @@ -102,7 +102,7 @@ private class FinalTxPublisher(nodeParams: NodeParams, case ParentTxOk => publish() case ParentTxMissing => log.debug("parent tx is missing, retrying after delay...") - timers.startSingleTimer(CheckParentTx, (1 + Random.nextLong(nodeParams.maxTxPublishRetryDelay.toMillis)).millis) + timers.startSingleTimer(CheckParentTx, (1 + Random.nextLong(nodeParams.channelConf.maxTxPublishRetryDelay.toMillis)).millis) Behaviors.same case UnknownFailure(reason) => log.error("could not check parent tx: ", reason) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/MempoolTxMonitor.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/MempoolTxMonitor.scala index afbe247f86..0bfb3e0ec2 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/MempoolTxMonitor.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/MempoolTxMonitor.scala @@ -136,7 +136,7 @@ private class MempoolTxMonitor(nodeParams: NodeParams, context.system.eventStream ! EventStream.Subscribe(messageAdapter) Behaviors.receiveMessagePartial { case WrappedCurrentBlockHeight(currentBlockHeight) => - timers.startSingleTimer(CheckTxConfirmationsKey, CheckTxConfirmations(currentBlockHeight), (1 + Random.nextLong(nodeParams.maxTxPublishRetryDelay.toMillis)).millis) + timers.startSingleTimer(CheckTxConfirmationsKey, CheckTxConfirmations(currentBlockHeight), (1 + Random.nextLong(nodeParams.channelConf.maxTxPublishRetryDelay.toMillis)).millis) Behaviors.same case CheckTxConfirmations(currentBlockHeight) => context.pipeToSelf(bitcoinClient.getTxConfirmations(cmd.tx.txid)) { @@ -149,7 +149,7 @@ private class MempoolTxMonitor(nodeParams: NodeParams, if (confirmations == 0) { cmd.replyTo ! TxInMempool(cmd.tx.txid, currentBlockHeight) Behaviors.same - } else if (confirmations < nodeParams.minDepthBlocks) { + } else if (confirmations < nodeParams.channelConf.minDepthBlocks) { log.info("txid={} has {} confirmations, waiting to reach min depth", cmd.tx.txid, confirmations) cmd.replyTo ! TxRecentlyConfirmed(cmd.tx.txid, confirmations) Behaviors.same diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPrePublisher.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPrePublisher.scala index beed33a4a9..43d8c667c0 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPrePublisher.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPrePublisher.scala @@ -181,7 +181,7 @@ private class ReplaceableTxPrePublisher(nodeParams: NodeParams, // - their commit is not confirmed: if it is, there is no need to publish our htlc transactions // - if this is an htlc-success transaction, we have the preimage context.pipeToSelf(bitcoinClient.getTxConfirmations(cmd.commitments.remoteCommit.txid)) { - case Success(Some(depth)) if depth >= nodeParams.minDepthBlocks => RemoteCommitTxConfirmed + case Success(Some(depth)) if depth >= nodeParams.channelConf.minDepthBlocks => RemoteCommitTxConfirmed case Success(_) => ParentTxOk case Failure(reason) => UnknownFailure(reason) } @@ -243,7 +243,7 @@ private class ReplaceableTxPrePublisher(nodeParams: NodeParams, // - our commit is not confirmed: if it is, there is no need to publish our claim-htlc transactions // - if this is a claim-htlc-success transaction, we have the preimage context.pipeToSelf(bitcoinClient.getTxConfirmations(cmd.commitments.localCommit.commitTxAndRemoteSig.commitTx.tx.txid)) { - case Success(Some(depth)) if depth >= nodeParams.minDepthBlocks => LocalCommitTxConfirmed + case Success(Some(depth)) if depth >= nodeParams.channelConf.minDepthBlocks => LocalCommitTxConfirmed case Success(_) => ParentTxOk case Failure(reason) => UnknownFailure(reason) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPublisher.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPublisher.scala index 20a837a61e..95a55320f1 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPublisher.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPublisher.scala @@ -220,7 +220,7 @@ private class ReplaceableTxPublisher(nodeParams: NodeParams, None } // We avoid a herd effect whenever we fee bump transactions. - targetFeerate_opt.foreach(targetFeerate => timers.startSingleTimer(BumpFeeKey, BumpFee(targetFeerate), (1 + Random.nextLong(nodeParams.maxTxPublishRetryDelay.toMillis)).millis)) + targetFeerate_opt.foreach(targetFeerate => timers.startSingleTimer(BumpFeeKey, BumpFee(targetFeerate), (1 + Random.nextLong(nodeParams.channelConf.maxTxPublishRetryDelay.toMillis)).millis)) Behaviors.same case BumpFee(targetFeerate) => fundReplacement(targetFeerate, tx) case UpdateConfirmationTarget(target) => diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/TxPublisher.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/TxPublisher.scala index edca99afde..2aac067a88 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/TxPublisher.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/TxPublisher.scala @@ -259,7 +259,7 @@ private class TxPublisher(nodeParams: NodeParams, factory: TxPublisher.ChildFact // Our transaction has been evicted from the mempool because it depended on an unconfirmed input that has // been replaced. We should be able to retry right now with new wallet inputs (no need to wait for a new // block). - timers.startSingleTimer(cmd, (1 + Random.nextLong(nodeParams.maxTxPublishRetryDelay.toMillis)).millis) + timers.startSingleTimer(cmd, (1 + Random.nextLong(nodeParams.channelConf.maxTxPublishRetryDelay.toMillis)).millis) run(pending2, retryNextBlock, channelContext) case TxRejectedReason.CouldNotFund => // We don't have enough funds at the moment to afford our target feerate, but it may change once pending @@ -293,7 +293,7 @@ private class TxPublisher(nodeParams: NodeParams, factory: TxPublisher.ChildFact case WrappedCurrentBlockHeight(currentBlockHeight) => if (retryNextBlock.nonEmpty) { log.info("{} transactions are still pending at block {}, retrying {} transactions that previously failed", pending.size, currentBlockHeight, retryNextBlock.length) - retryNextBlock.foreach(cmd => timers.startSingleTimer(cmd, (1 + Random.nextLong(nodeParams.maxTxPublishRetryDelay.toMillis)).millis)) + retryNextBlock.foreach(cmd => timers.startSingleTimer(cmd, (1 + Random.nextLong(nodeParams.channelConf.maxTxPublishRetryDelay.toMillis)).millis)) } run(pending, Seq.empty, channelContext) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/TxTimeLocksMonitor.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/TxTimeLocksMonitor.scala index 90332a07eb..c009834a52 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/TxTimeLocksMonitor.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/TxTimeLocksMonitor.scala @@ -85,7 +85,7 @@ private class TxTimeLocksMonitor(nodeParams: NodeParams, case WrappedCurrentBlockHeight(currentBlockHeight) => if (cltvTimeout <= currentBlockHeight) { context.system.eventStream ! EventStream.Unsubscribe(messageAdapter) - timers.startSingleTimer(CheckRelativeTimeLock, (1 + Random.nextLong(nodeParams.maxTxPublishRetryDelay.toMillis)).millis) + timers.startSingleTimer(CheckRelativeTimeLock, (1 + Random.nextLong(nodeParams.channelConf.maxTxPublishRetryDelay.toMillis)).millis) Behaviors.same } else { Behaviors.same diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala index 0bf41b50de..45b248dd97 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala @@ -141,7 +141,7 @@ class Peer(val nodeParams: NodeParams, remoteNodeId: PublicKey, wallet: OnChainA } else if (c.fundingSatoshis >= Channel.MAX_FUNDING && !d.remoteFeatures.hasFeature(Wumbo)) { sender() ! Status.Failure(new RuntimeException(s"fundingSatoshis=${c.fundingSatoshis} is too big, the remote peer doesn't support wumbo")) stay() - } else if (c.fundingSatoshis > nodeParams.maxFundingSatoshis) { + } else if (c.fundingSatoshis > nodeParams.channelConf.maxFundingSatoshis) { sender() ! Status.Failure(new RuntimeException(s"fundingSatoshis=${c.fundingSatoshis} is too big for the current settings, increase 'eclair.max-funding-satoshis' (see eclair.conf)")) stay() } else { @@ -154,7 +154,7 @@ class Peer(val nodeParams: NodeParams, remoteNodeId: PublicKey, wallet: OnChainA val channelFeeratePerKw = nodeParams.onChainFeeConf.getCommitmentFeerate(remoteNodeId, channelType, c.fundingSatoshis, None) val fundingTxFeeratePerKw = c.fundingTxFeeratePerKw_opt.getOrElse(nodeParams.onChainFeeConf.feeEstimator.getFeeratePerKw(target = nodeParams.onChainFeeConf.feeTargets.fundingBlockTarget)) log.info(s"requesting a new channel with type=$channelType fundingSatoshis=${c.fundingSatoshis}, pushMsat=${c.pushMsat} and fundingFeeratePerByte=${c.fundingTxFeeratePerKw_opt} temporaryChannelId=$temporaryChannelId localParams=$localParams") - channel ! INPUT_INIT_FUNDER(temporaryChannelId, c.fundingSatoshis, c.pushMsat, channelFeeratePerKw, fundingTxFeeratePerKw, localParams, d.peerConnection, d.remoteInit, c.channelFlags.getOrElse(nodeParams.channelFlags), channelConfig, channelType) + channel ! INPUT_INIT_FUNDER(temporaryChannelId, c.fundingSatoshis, c.pushMsat, channelFeeratePerKw, fundingTxFeeratePerKw, localParams, d.peerConnection, d.remoteInit, c.channelFlags.getOrElse(nodeParams.channelConf.channelFlags), channelConfig, channelType) stay() using d.copy(channels = d.channels + (TemporaryChannelId(temporaryChannelId) -> channel)) } @@ -505,12 +505,12 @@ object Peer { LocalParams( nodeParams.nodeId, nodeParams.channelKeyManager.newFundingKeyPath(isFunder), // we make sure that funder and fundee key path end differently - dustLimit = nodeParams.dustLimit, - maxHtlcValueInFlightMsat = nodeParams.maxHtlcValueInFlightMsat, - channelReserve = (fundingAmount * nodeParams.reserveToFundingRatio).max(nodeParams.dustLimit), // BOLT #2: make sure that our reserve is above our dust limit - htlcMinimum = nodeParams.htlcMinimum, - toSelfDelay = nodeParams.toRemoteDelay, // we choose their delay - maxAcceptedHtlcs = nodeParams.maxAcceptedHtlcs, + dustLimit = nodeParams.channelConf.dustLimit, + maxHtlcValueInFlightMsat = nodeParams.channelConf.maxHtlcValueInFlightMsat, + channelReserve = (fundingAmount * nodeParams.channelConf.reserveToFundingRatio).max(nodeParams.channelConf.dustLimit), // BOLT #2: make sure that our reserve is above our dust limit + htlcMinimum = nodeParams.channelConf.htlcMinimum, + toSelfDelay = nodeParams.channelConf.toRemoteDelay, // we choose their delay + maxAcceptedHtlcs = nodeParams.channelConf.maxAcceptedHtlcs, isFunder = isFunder, defaultFinalScriptPubKey = defaultFinalScriptPubkey, walletStaticPaymentBasepoint = walletStaticPaymentBasepoint, diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/MultiPartHandler.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/MultiPartHandler.scala index 896b0fd5df..25a9f4a8ff 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/MultiPartHandler.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/MultiPartHandler.scala @@ -98,7 +98,7 @@ class MultiPartHandler(nodeParams: NodeParams, register: ActorRef, db: IncomingP Map(Features.PaymentSecret -> FeatureSupport.Mandatory, Features.VariableLengthOnion -> FeatureSupport.Mandatory) } // Insert a fake invoice and then restart the incoming payment handler - val paymentRequest = PaymentRequest(nodeParams.chainHash, amount, paymentHash, nodeParams.privateKey, desc, nodeParams.minFinalExpiryDelta, paymentSecret = p.payload.paymentSecret, features = PaymentRequestFeatures(features)) + val paymentRequest = PaymentRequest(nodeParams.chainHash, amount, paymentHash, nodeParams.privateKey, desc, nodeParams.channelConf.minFinalExpiryDelta, paymentSecret = p.payload.paymentSecret, features = PaymentRequestFeatures(features)) log.debug("generated fake payment request={} from amount={} (KeySend)", PaymentRequest.write(paymentRequest), amount) db.addIncomingPayment(paymentRequest, paymentPreimage, paymentType = PaymentType.KeySend) ctx.self ! p @@ -241,7 +241,7 @@ object MultiPartHandler { paymentHash, nodeParams.privateKey, description, - nodeParams.minFinalExpiryDelta, + nodeParams.channelConf.minFinalExpiryDelta, fallbackAddress_opt, expirySeconds = Some(expirySeconds), extraHops = extraHops, @@ -305,7 +305,7 @@ object MultiPartHandler { } private def validatePaymentCltv(nodeParams: NodeParams, payment: IncomingPaymentPacket.FinalPacket, record: IncomingPayment)(implicit log: LoggingAdapter): Boolean = { - val minExpiry = record.paymentRequest.minFinalCltvExpiryDelta.getOrElse(nodeParams.minFinalExpiryDelta).toCltvExpiry(nodeParams.currentBlockHeight) + val minExpiry = record.paymentRequest.minFinalCltvExpiryDelta.getOrElse(nodeParams.channelConf.minFinalExpiryDelta).toCltvExpiry(nodeParams.currentBlockHeight) if (payment.add.cltvExpiry < minExpiry) { log.warning("received payment with expiry too small for amount={} totalAmount={}", payment.add.amountMsat, payment.payload.totalAmount) false diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/NodeRelay.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/NodeRelay.scala index fc02f467eb..f957e4148a 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/NodeRelay.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/NodeRelay.scala @@ -106,7 +106,7 @@ object NodeRelay { val fee = nodeFee(nodeParams.relayParams.minTrampolineFees, payloadOut.amountToForward) if (upstream.amountIn - payloadOut.amountToForward < fee) { Some(TrampolineFeeInsufficient) - } else if (upstream.expiryIn - payloadOut.outgoingCltv < nodeParams.expiryDelta) { + } else if (upstream.expiryIn - payloadOut.outgoingCltv < nodeParams.channelConf.expiryDelta) { Some(TrampolineExpiryTooSoon) } else if (payloadOut.outgoingCltv <= CltvExpiry(nodeParams.currentBlockHeight)) { Some(TrampolineExpiryTooSoon) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentInitiator.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentInitiator.scala index 5b39035302..c8f14a73b5 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentInitiator.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentInitiator.scala @@ -133,7 +133,7 @@ class PaymentInitiator(nodeParams: NodeParams, outgoingPaymentFactory: PaymentIn case pf: PaymentFailed => pending.get(pf.id).foreach { case pp: PendingTrampolinePayment => val trampolineRoute = Seq( - NodeHop(nodeParams.nodeId, pp.r.trampolineNodeId, nodeParams.expiryDelta, 0 msat), + NodeHop(nodeParams.nodeId, pp.r.trampolineNodeId, nodeParams.channelConf.expiryDelta, 0 msat), NodeHop(pp.r.trampolineNodeId, pp.r.recipientNodeId, pp.r.trampolineAttempts.last._2, pp.r.trampolineAttempts.last._1) ) val decryptedFailures = pf.failures.collect { case RemoteFailure(_, _, Sphinx.DecryptedFailurePacket(_, f)) => f } @@ -194,7 +194,7 @@ class PaymentInitiator(nodeParams: NodeParams, outgoingPaymentFactory: PaymentIn private def buildTrampolinePayment(r: SendRequestedPayment, trampolineNodeId: PublicKey, trampolineFees: MilliSatoshi, trampolineExpiryDelta: CltvExpiryDelta): Try[(MilliSatoshi, CltvExpiry, OnionRoutingPacket)] = { val trampolineRoute = Seq( - NodeHop(nodeParams.nodeId, trampolineNodeId, nodeParams.expiryDelta, 0 msat), + NodeHop(nodeParams.nodeId, trampolineNodeId, nodeParams.channelConf.expiryDelta, 0 msat), NodeHop(trampolineNodeId, r.recipientNodeId, trampolineExpiryDelta, trampolineFees) // for now we only use a single trampoline hop ) val finalPayload = if (r.paymentRequest.features.allowMultiPart) { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/remote/EclairInternalsSerializer.scala b/eclair-core/src/main/scala/fr/acinq/eclair/remote/EclairInternalsSerializer.scala index 8bea5c32de..615b516361 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/remote/EclairInternalsSerializer.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/remote/EclairInternalsSerializer.scala @@ -87,6 +87,7 @@ object EclairInternalsSerializer { ).as[PathFindingExperimentConf] val routerConfCodec: Codec[RouterConf] = ( + ("watchSpentWindow" | finiteDurationCodec) :: ("channelExcludeDuration" | finiteDurationCodec) :: ("routerBroadcastInterval" | finiteDurationCodec) :: ("requestNodeAnnouncements" | bool(8)) :: diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala index bdb7409d5b..0912854c7d 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala @@ -88,7 +88,7 @@ class Router(val nodeParams: NodeParams, watcher: typed.ActorRef[ZmqWatcher.Comm val txid = pc.fundingTxid val TxCoordinates(_, _, outputIndex) = ShortChannelId.coordinates(pc.ann.shortChannelId) // avoid herd effect at startup because watch-spent are intensive in terms of rpc calls to bitcoind - context.system.scheduler.scheduleOnce(Random.nextLong(nodeParams.watchSpentWindow.toSeconds).seconds) { + context.system.scheduler.scheduleOnce(Random.nextLong(nodeParams.routerConf.watchSpentWindow.toSeconds).seconds) { watcher ! WatchExternalChannelSpent(self, txid, outputIndex, pc.ann.shortChannelId) } } @@ -297,7 +297,8 @@ object Router { ) } - case class RouterConf(channelExcludeDuration: FiniteDuration, + case class RouterConf(watchSpentWindow: FiniteDuration, + channelExcludeDuration: FiniteDuration, routerBroadcastInterval: FiniteDuration, requestNodeAnnouncements: Boolean, encodingType: EncodingType, diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/StartupSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/StartupSpec.scala index a66c500730..ada367f4a5 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/StartupSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/StartupSpec.scala @@ -261,7 +261,7 @@ class StartupSpec extends AnyFunSuite { } test("NodeParams should fail if htlc-minimum-msat is set to 0") { - val noHtlcMinimumConf = ConfigFactory.parseString("htlc-minimum-msat = 0") + val noHtlcMinimumConf = ConfigFactory.parseString("channel.htlc-minimum-msat = 0") assert(Try(makeNodeParamsWithDefaults(noHtlcMinimumConf.withFallback(defaultConf))).isFailure) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala b/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala index 488c6363e4..3c6af660ac 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala @@ -20,7 +20,7 @@ import fr.acinq.bitcoin.{Block, ByteVector32, Satoshi, SatoshiLong, Script} import fr.acinq.eclair.FeatureSupport.{Mandatory, Optional} import fr.acinq.eclair.Features._ import fr.acinq.eclair.blockchain.fee._ -import fr.acinq.eclair.channel.Channel.UnhandledExceptionStrategy +import fr.acinq.eclair.channel.Channel.{ChannelConf, UnhandledExceptionStrategy} import fr.acinq.eclair.channel.{ChannelFlags, LocalParams} import fr.acinq.eclair.crypto.keymanager.{LocalChannelKeyManager, LocalNodeKeyManager} import fr.acinq.eclair.io.MessageRelay.RelayAll @@ -99,8 +99,28 @@ object TestConstants { pluginParams = List(pluginParams), overrideFeatures = Map.empty, syncWhitelist = Set.empty, - dustLimit = 1100 sat, - maxRemoteDustLimit = 1500 sat, + channelConf = ChannelConf( + dustLimit = 1100 sat, + maxRemoteDustLimit = 1500 sat, + maxHtlcValueInFlightMsat = UInt64(500000000), + maxAcceptedHtlcs = 100, + expiryDelta = CltvExpiryDelta(144), + fulfillSafetyBeforeTimeout = CltvExpiryDelta(6), + minFinalExpiryDelta = CltvExpiryDelta(18), + maxBlockProcessingDelay = 10 millis, + maxTxPublishRetryDelay = 10 millis, + htlcMinimum = 0 msat, + minDepthBlocks = 3, + toRemoteDelay = CltvExpiryDelta(144), + maxToLocalDelay = CltvExpiryDelta(1000), + reserveToFundingRatio = 0.01, // note: not used (overridden below) + maxReserveToFundingRatio = 0.05, + unhandledExceptionStrategy = UnhandledExceptionStrategy.LocalClose, + revocationTimeout = 20 seconds, + channelFlags = ChannelFlags.Public, + minFundingSatoshis = 1000 sat, + maxFundingSatoshis = 16777215 sat, + ), onChainFeeConf = OnChainFeeConf( feeTargets = FeeTargets(6, 2, 36, 12, 18, 0), feeEstimator = new TestFeeEstimator, @@ -109,17 +129,6 @@ object TestConstants { defaultFeerateTolerance = FeerateTolerance(0.5, 8.0, anchorOutputsFeeratePerKw, DustTolerance(25_000 sat, closeOnUpdateFeeOverflow = true)), perNodeFeerateTolerance = Map.empty ), - maxHtlcValueInFlightMsat = UInt64(500000000), - maxAcceptedHtlcs = 100, - expiryDelta = CltvExpiryDelta(144), - fulfillSafetyBeforeTimeout = CltvExpiryDelta(6), - minFinalExpiryDelta = CltvExpiryDelta(18), - maxBlockProcessingDelay = 10 millis, - maxTxPublishRetryDelay = 10 millis, - htlcMinimum = 0 msat, - minDepthBlocks = 3, - toRemoteDelay = CltvExpiryDelta(144), - maxToLocalDelay = CltvExpiryDelta(1000), relayParams = RelayParams( publicChannelFees = RelayFees( feeBase = 546000 msat, @@ -130,21 +139,13 @@ object TestConstants { minTrampolineFees = RelayFees( feeBase = 548000 msat, feeProportionalMillionths = 30)), - reserveToFundingRatio = 0.01, // note: not used (overridden below) - maxReserveToFundingRatio = 0.05, - unhandledExceptionStrategy = UnhandledExceptionStrategy.LocalClose, db = TestDatabases.inMemoryDb(), - revocationTimeout = 20 seconds, autoReconnect = false, initialRandomReconnectDelay = 5 seconds, maxReconnectInterval = 1 hour, chainHash = Block.RegtestGenesisBlock.hash, - channelFlags = ChannelFlags.Public, - watchSpentWindow = 1 second, paymentRequestExpiry = 1 hour, multiPartPaymentExpiry = 30 seconds, - minFundingSatoshis = 1000 sat, - maxFundingSatoshis = 16777215 sat, peerConnectionConf = PeerConnection.Conf( authTimeout = 10 seconds, initTimeout = 10 seconds, @@ -156,6 +157,7 @@ object TestConstants { maxOnionMessagesPerSecond = 10 ), routerConf = RouterConf( + watchSpentWindow = 1 second, channelExcludeDuration = 60 seconds, routerBroadcastInterval = 5 seconds, requestNodeAnnouncements = true, @@ -232,8 +234,28 @@ object TestConstants { pluginParams = Nil, overrideFeatures = Map.empty, syncWhitelist = Set.empty, - dustLimit = 1000 sat, - maxRemoteDustLimit = 1500 sat, + channelConf = ChannelConf( + dustLimit = 1000 sat, + maxRemoteDustLimit = 1500 sat, + maxHtlcValueInFlightMsat = UInt64.MaxValue, // Bob has no limit on the combined max value of in-flight htlcs + maxAcceptedHtlcs = 30, + expiryDelta = CltvExpiryDelta(144), + fulfillSafetyBeforeTimeout = CltvExpiryDelta(6), + minFinalExpiryDelta = CltvExpiryDelta(18), + maxBlockProcessingDelay = 10 millis, + maxTxPublishRetryDelay = 10 millis, + htlcMinimum = 1000 msat, + minDepthBlocks = 3, + toRemoteDelay = CltvExpiryDelta(144), + maxToLocalDelay = CltvExpiryDelta(1000), + reserveToFundingRatio = 0.01, // note: not used (overridden below) + maxReserveToFundingRatio = 0.05, + unhandledExceptionStrategy = UnhandledExceptionStrategy.LocalClose, + revocationTimeout = 20 seconds, + channelFlags = ChannelFlags.Public, + minFundingSatoshis = 1000 sat, + maxFundingSatoshis = 16777215 sat, + ), onChainFeeConf = OnChainFeeConf( feeTargets = FeeTargets(6, 2, 36, 12, 18, 0), feeEstimator = new TestFeeEstimator, @@ -242,17 +264,6 @@ object TestConstants { defaultFeerateTolerance = FeerateTolerance(0.75, 1.5, anchorOutputsFeeratePerKw, DustTolerance(30_000 sat, closeOnUpdateFeeOverflow = true)), perNodeFeerateTolerance = Map.empty ), - maxHtlcValueInFlightMsat = UInt64.MaxValue, // Bob has no limit on the combined max value of in-flight htlcs - maxAcceptedHtlcs = 30, - expiryDelta = CltvExpiryDelta(144), - fulfillSafetyBeforeTimeout = CltvExpiryDelta(6), - minFinalExpiryDelta = CltvExpiryDelta(18), - maxBlockProcessingDelay = 10 millis, - maxTxPublishRetryDelay = 10 millis, - htlcMinimum = 1000 msat, - minDepthBlocks = 3, - toRemoteDelay = CltvExpiryDelta(144), - maxToLocalDelay = CltvExpiryDelta(1000), relayParams = RelayParams( publicChannelFees = RelayFees( feeBase = 546000 msat, @@ -263,21 +274,13 @@ object TestConstants { minTrampolineFees = RelayFees( feeBase = 548000 msat, feeProportionalMillionths = 30)), - reserveToFundingRatio = 0.01, // note: not used (overridden below) - maxReserveToFundingRatio = 0.05, - unhandledExceptionStrategy = UnhandledExceptionStrategy.LocalClose, db = TestDatabases.inMemoryDb(), - revocationTimeout = 20 seconds, autoReconnect = false, initialRandomReconnectDelay = 5 seconds, maxReconnectInterval = 1 hour, chainHash = Block.RegtestGenesisBlock.hash, - channelFlags = ChannelFlags.Public, - watchSpentWindow = 1 second, paymentRequestExpiry = 1 hour, multiPartPaymentExpiry = 30 seconds, - minFundingSatoshis = 1000 sat, - maxFundingSatoshis = 16777215 sat, peerConnectionConf = PeerConnection.Conf( authTimeout = 10 seconds, initTimeout = 10 seconds, @@ -289,6 +292,7 @@ object TestConstants { maxOnionMessagesPerSecond = 10 ), routerConf = RouterConf( + watchSpentWindow = 1 second, channelExcludeDuration = 60 seconds, routerBroadcastInterval = 5 seconds, requestNodeAnnouncements = true, diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/HelpersSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/HelpersSpec.scala index 185b2f4e7a..ba5134f264 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/HelpersSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/HelpersSpec.scala @@ -39,13 +39,13 @@ class HelpersSpec extends TestKitBaseClass with AnyFunSuiteLike with ChannelStat implicit val log: akka.event.LoggingAdapter = akka.event.NoLogging test("compute the funding tx min depth according to funding amount") { - assert(Helpers.minDepthForFunding(nodeParams, Btc(1)) == 4) - assert(Helpers.minDepthForFunding(nodeParams.copy(minDepthBlocks = 6), Btc(1)) == 6) // 4 conf would be enough but we use min-depth=6 - assert(Helpers.minDepthForFunding(nodeParams, Btc(6.25)) == 16) // we use scaling_factor=15 and a fixed block reward of 6.25BTC - assert(Helpers.minDepthForFunding(nodeParams, Btc(12.50)) == 31) - assert(Helpers.minDepthForFunding(nodeParams, Btc(12.60)) == 32) - assert(Helpers.minDepthForFunding(nodeParams, Btc(30)) == 73) - assert(Helpers.minDepthForFunding(nodeParams, Btc(50)) == 121) + assert(Helpers.minDepthForFunding(nodeParams.channelConf, Btc(1)) == 4) + assert(Helpers.minDepthForFunding(nodeParams.channelConf.copy(minDepthBlocks = 6), Btc(1)) == 6) // 4 conf would be enough but we use min-depth=6 + assert(Helpers.minDepthForFunding(nodeParams.channelConf, Btc(6.25)) == 16) // we use scaling_factor=15 and a fixed block reward of 6.25BTC + assert(Helpers.minDepthForFunding(nodeParams.channelConf, Btc(12.50)) == 31) + assert(Helpers.minDepthForFunding(nodeParams.channelConf, Btc(12.60)) == 32) + assert(Helpers.minDepthForFunding(nodeParams.channelConf, Btc(30)) == 73) + assert(Helpers.minDepthForFunding(nodeParams.channelConf, Btc(50)) == 121) } test("compute refresh delay") { @@ -176,7 +176,7 @@ class HelpersSpec extends TestKitBaseClass with AnyFunSuiteLike with ChannelStat def findTimedOutHtlcs(f: Fixture, withoutHtlcId: Boolean): Unit = { import f._ - val dustLimit = alice.underlyingActor.nodeParams.dustLimit + val dustLimit = alice.underlyingActor.nodeParams.channelConf.dustLimit val commitmentFormat = alice.stateData.asInstanceOf[DATA_CLOSING].commitments.commitmentFormat val localCommit = alice.stateData.asInstanceOf[DATA_CLOSING].commitments.localCommit val remoteCommit = bob.stateData.asInstanceOf[DATA_CLOSING].commitments.remoteCommit diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/RestoreSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/RestoreSpec.scala index 577ea871cd..2f15699727 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/RestoreSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/RestoreSpec.scala @@ -210,10 +210,10 @@ class RestoreSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Chan Alice.nodeParams .modify(_.relayParams.privateChannelFees.feeProportionalMillionths).setTo(2345), Alice.nodeParams - .modify(_.expiryDelta).setTo(CltvExpiryDelta(147)), + .modify(_.channelConf.expiryDelta).setTo(CltvExpiryDelta(147)), Alice.nodeParams .modify(_.relayParams.privateChannelFees.feeProportionalMillionths).setTo(2345) - .modify(_.expiryDelta).setTo(CltvExpiryDelta(147)), + .modify(_.channelConf.expiryDelta).setTo(CltvExpiryDelta(147)), ) foreach { newConfig => val newAlice: TestFSMRef[ChannelState, ChannelData, Channel] = TestFSMRef(new Channel(newConfig, wallet, Bob.nodeParams.nodeId, alice2blockchain.ref, relayerA.ref, FakeTxPublisherFactory(alice2blockchain)), alicePeer.ref) @@ -223,7 +223,7 @@ class RestoreSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Chan assert(!Announcements.areSameIgnoreFlags(u1.channelUpdate, oldStateData.channelUpdate)) assert(u1.channelUpdate.feeBaseMsat === newConfig.relayParams.privateChannelFees.feeBase) assert(u1.channelUpdate.feeProportionalMillionths === newConfig.relayParams.privateChannelFees.feeProportionalMillionths) - assert(u1.channelUpdate.cltvExpiryDelta === newConfig.expiryDelta) + assert(u1.channelUpdate.cltvExpiryDelta === newConfig.channelConf.expiryDelta) newAlice ! INPUT_RECONNECTED(alice2bob.ref, aliceInit, bobInit) bob ! INPUT_RECONNECTED(bob2alice.ref, bobInit, aliceInit) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/MempoolTxMonitorSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/MempoolTxMonitorSpec.scala index 705f29aa78..817b8d43ca 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/MempoolTxMonitorSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/MempoolTxMonitorSpec.scala @@ -86,13 +86,13 @@ class MempoolTxMonitorSpec extends TestKitBaseClass with AnyFunSuiteLike with Bi probe.expectMsg(TxInMempool(tx.txid, currentBlockHeight(probe))) probe.expectNoMessage(100 millis) - assert(TestConstants.Alice.nodeParams.minDepthBlocks > 1) + assert(TestConstants.Alice.nodeParams.channelConf.minDepthBlocks > 1) generateBlocks(1) system.eventStream.publish(CurrentBlockHeight(currentBlockHeight(probe))) probe.expectMsg(TxRecentlyConfirmed(tx.txid, 1)) probe.expectNoMessage(100 millis) // we wait for more than one confirmation to protect against reorgs - generateBlocks(TestConstants.Alice.nodeParams.minDepthBlocks - 1) + generateBlocks(TestConstants.Alice.nodeParams.channelConf.minDepthBlocks - 1) system.eventStream.publish(CurrentBlockHeight(currentBlockHeight(probe))) probe.expectMsg(TxDeeplyBuried(tx)) } @@ -109,7 +109,7 @@ class MempoolTxMonitorSpec extends TestKitBaseClass with AnyFunSuiteLike with Bi monitor ! Publish(probe.ref, tx2, tx2.txIn.head.outPoint, "test-tx", 10 sat) waitTxInMempool(bitcoinClient, tx2.txid, probe) - generateBlocks(TestConstants.Alice.nodeParams.minDepthBlocks) + generateBlocks(TestConstants.Alice.nodeParams.channelConf.minDepthBlocks) system.eventStream.publish(CurrentBlockHeight(currentBlockHeight(probe))) probe.expectMsg(TxDeeplyBuried(tx2)) } @@ -257,7 +257,7 @@ class MempoolTxMonitorSpec extends TestKitBaseClass with AnyFunSuiteLike with Bi assert(txPublished.miningFee === 15.sat) assert(txPublished.desc === "test-tx") - generateBlocks(TestConstants.Alice.nodeParams.minDepthBlocks) + generateBlocks(TestConstants.Alice.nodeParams.channelConf.minDepthBlocks) system.eventStream.publish(CurrentBlockHeight(currentBlockHeight(probe))) eventListener.expectMsg(TransactionConfirmed(txPublished.channelId, txPublished.remoteNodeId, tx)) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala index 55a88459f4..ae27b6e12e 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala @@ -121,15 +121,15 @@ trait ChannelStateTestsHelperMethods extends TestKitBase { system.eventStream.subscribe(channelUpdateListener.ref, classOf[LocalChannelDown]) val router = TestProbe() val finalNodeParamsA = nodeParamsA - .modify(_.dustLimit).setToIf(tags.contains(ChannelStateTestsTags.HighDustLimitDifferenceAliceBob))(5000 sat) - .modify(_.dustLimit).setToIf(tags.contains(ChannelStateTestsTags.HighDustLimitDifferenceBobAlice))(1000 sat) - .modify(_.maxRemoteDustLimit).setToIf(tags.contains(ChannelStateTestsTags.HighDustLimitDifferenceAliceBob))(10000 sat) - .modify(_.maxRemoteDustLimit).setToIf(tags.contains(ChannelStateTestsTags.HighDustLimitDifferenceBobAlice))(10000 sat) + .modify(_.channelConf.dustLimit).setToIf(tags.contains(ChannelStateTestsTags.HighDustLimitDifferenceAliceBob))(5000 sat) + .modify(_.channelConf.dustLimit).setToIf(tags.contains(ChannelStateTestsTags.HighDustLimitDifferenceBobAlice))(1000 sat) + .modify(_.channelConf.maxRemoteDustLimit).setToIf(tags.contains(ChannelStateTestsTags.HighDustLimitDifferenceAliceBob))(10000 sat) + .modify(_.channelConf.maxRemoteDustLimit).setToIf(tags.contains(ChannelStateTestsTags.HighDustLimitDifferenceBobAlice))(10000 sat) val finalNodeParamsB = nodeParamsB - .modify(_.dustLimit).setToIf(tags.contains(ChannelStateTestsTags.HighDustLimitDifferenceAliceBob))(1000 sat) - .modify(_.dustLimit).setToIf(tags.contains(ChannelStateTestsTags.HighDustLimitDifferenceBobAlice))(5000 sat) - .modify(_.maxRemoteDustLimit).setToIf(tags.contains(ChannelStateTestsTags.HighDustLimitDifferenceAliceBob))(10000 sat) - .modify(_.maxRemoteDustLimit).setToIf(tags.contains(ChannelStateTestsTags.HighDustLimitDifferenceBobAlice))(10000 sat) + .modify(_.channelConf.dustLimit).setToIf(tags.contains(ChannelStateTestsTags.HighDustLimitDifferenceAliceBob))(1000 sat) + .modify(_.channelConf.dustLimit).setToIf(tags.contains(ChannelStateTestsTags.HighDustLimitDifferenceBobAlice))(5000 sat) + .modify(_.channelConf.maxRemoteDustLimit).setToIf(tags.contains(ChannelStateTestsTags.HighDustLimitDifferenceAliceBob))(10000 sat) + .modify(_.channelConf.maxRemoteDustLimit).setToIf(tags.contains(ChannelStateTestsTags.HighDustLimitDifferenceBobAlice))(10000 sat) val alice: TestFSMRef[ChannelState, ChannelData, Channel] = TestFSMRef(new Channel(finalNodeParamsA, wallet, finalNodeParamsB.nodeId, alice2blockchain.ref, relayerA.ref, FakeTxPublisherFactory(alice2blockchain), origin_opt = Some(aliceOrigin.ref)), alicePeer.ref) val bob: TestFSMRef[ChannelState, ChannelData, Channel] = TestFSMRef(new Channel(finalNodeParamsB, wallet, finalNodeParamsA.nodeId, bob2blockchain.ref, relayerB.ref, FakeTxPublisherFactory(bob2blockchain)), bobPeer.ref) SetupFixture(alice, bob, aliceOrigin, alice2bob, bob2alice, alice2blockchain, bob2blockchain, router, relayerA, relayerB, channelUpdateListener, wallet, alicePeer, bobPeer) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForAcceptChannelStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForAcceptChannelStateSpec.scala index f6c4502dc4..911d41a17a 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForAcceptChannelStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForAcceptChannelStateSpec.scala @@ -44,12 +44,12 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS import com.softwaremill.quicklens._ val aliceNodeParams = Alice.nodeParams .modify(_.chainHash).setToIf(test.tags.contains("mainnet"))(Block.LivenetGenesisBlock.hash) - .modify(_.maxFundingSatoshis).setToIf(test.tags.contains("high-max-funding-size"))(Btc(100)) - .modify(_.maxRemoteDustLimit).setToIf(test.tags.contains("high-remote-dust-limit"))(15000 sat) + .modify(_.channelConf.maxFundingSatoshis).setToIf(test.tags.contains("high-max-funding-size"))(Btc(100)) + .modify(_.channelConf.maxRemoteDustLimit).setToIf(test.tags.contains("high-remote-dust-limit"))(15000 sat) val bobNodeParams = Bob.nodeParams .modify(_.chainHash).setToIf(test.tags.contains("mainnet"))(Block.LivenetGenesisBlock.hash) - .modify(_.maxFundingSatoshis).setToIf(test.tags.contains("high-max-funding-size"))(Btc(100)) + .modify(_.channelConf.maxFundingSatoshis).setToIf(test.tags.contains("high-max-funding-size"))(Btc(100)) val setup = init(aliceNodeParams, bobNodeParams, wallet = new NoOpOnChainWallet()) @@ -209,7 +209,7 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS val highDustLimitSatoshis = 2000.sat alice ! accept.copy(dustLimitSatoshis = highDustLimitSatoshis) val error = alice2bob.expectMsgType[Error] - assert(error === Error(accept.temporaryChannelId, DustLimitTooLarge(accept.temporaryChannelId, highDustLimitSatoshis, Alice.nodeParams.maxRemoteDustLimit).getMessage)) + assert(error === Error(accept.temporaryChannelId, DustLimitTooLarge(accept.temporaryChannelId, highDustLimitSatoshis, Alice.nodeParams.channelConf.maxRemoteDustLimit).getMessage)) awaitCond(alice.stateName == CLOSED) aliceOrigin.expectMsgType[Status.Failure] } @@ -220,7 +220,7 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS val delayTooHigh = CltvExpiryDelta(10000) alice ! accept.copy(toSelfDelay = delayTooHigh) val error = alice2bob.expectMsgType[Error] - assert(error === Error(accept.temporaryChannelId, ToSelfDelayTooHigh(accept.temporaryChannelId, delayTooHigh, Alice.nodeParams.maxToLocalDelay).getMessage)) + assert(error === Error(accept.temporaryChannelId, ToSelfDelayTooHigh(accept.temporaryChannelId, delayTooHigh, Alice.nodeParams.channelConf.maxToLocalDelay).getMessage)) awaitCond(alice.stateName == CLOSED) aliceOrigin.expectMsgType[Status.Failure] } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForOpenChannelStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForOpenChannelStateSpec.scala index 20284df76c..518281c635 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForOpenChannelStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForOpenChannelStateSpec.scala @@ -43,7 +43,7 @@ class WaitForOpenChannelStateSpec extends TestKitBaseClass with FixtureAnyFunSui import com.softwaremill.quicklens._ val bobNodeParams = Bob.nodeParams - .modify(_.maxFundingSatoshis).setToIf(test.tags.contains("max-funding-satoshis"))(Btc(1)) + .modify(_.channelConf.maxFundingSatoshis).setToIf(test.tags.contains("max-funding-satoshis"))(Btc(1)) val setup = init(nodeParamsB = bobNodeParams) @@ -118,7 +118,7 @@ class WaitForOpenChannelStateSpec extends TestKitBaseClass with FixtureAnyFunSui val lowFunding = 100.sat bob ! open.copy(fundingSatoshis = lowFunding) val error = bob2alice.expectMsgType[Error] - assert(error === Error(open.temporaryChannelId, InvalidFundingAmount(open.temporaryChannelId, lowFunding, Bob.nodeParams.minFundingSatoshis, Bob.nodeParams.maxFundingSatoshis).getMessage)) + assert(error === Error(open.temporaryChannelId, InvalidFundingAmount(open.temporaryChannelId, lowFunding, Bob.nodeParams.channelConf.minFundingSatoshis, Bob.nodeParams.channelConf.maxFundingSatoshis).getMessage)) awaitCond(bob.stateName == CLOSED) } @@ -128,17 +128,17 @@ class WaitForOpenChannelStateSpec extends TestKitBaseClass with FixtureAnyFunSui val highFundingMsat = 100000000.sat bob ! open.copy(fundingSatoshis = highFundingMsat) val error = bob2alice.expectMsgType[Error] - assert(error.toAscii === Error(open.temporaryChannelId, InvalidFundingAmount(open.temporaryChannelId, highFundingMsat, Bob.nodeParams.minFundingSatoshis, Bob.nodeParams.maxFundingSatoshis).getMessage).toAscii) + assert(error.toAscii === Error(open.temporaryChannelId, InvalidFundingAmount(open.temporaryChannelId, highFundingMsat, Bob.nodeParams.channelConf.minFundingSatoshis, Bob.nodeParams.channelConf.maxFundingSatoshis).getMessage).toAscii) awaitCond(bob.stateName == CLOSED) } test("recv OpenChannel (fundingSatoshis > max-funding-satoshis)", Tag(ChannelStateTestsTags.Wumbo)) { f => import f._ val open = alice2bob.expectMsgType[OpenChannel] - val highFundingSat = Bob.nodeParams.maxFundingSatoshis + Btc(1) + val highFundingSat = Bob.nodeParams.channelConf.maxFundingSatoshis + Btc(1) bob ! open.copy(fundingSatoshis = highFundingSat) val error = bob2alice.expectMsgType[Error] - assert(error.toAscii === Error(open.temporaryChannelId, InvalidFundingAmount(open.temporaryChannelId, highFundingSat, Bob.nodeParams.minFundingSatoshis, Bob.nodeParams.maxFundingSatoshis).getMessage).toAscii) + assert(error.toAscii === Error(open.temporaryChannelId, InvalidFundingAmount(open.temporaryChannelId, highFundingSat, Bob.nodeParams.channelConf.minFundingSatoshis, Bob.nodeParams.channelConf.maxFundingSatoshis).getMessage).toAscii) } test("recv OpenChannel (invalid max accepted htlcs)") { f => @@ -167,7 +167,7 @@ class WaitForOpenChannelStateSpec extends TestKitBaseClass with FixtureAnyFunSui val delayTooHigh = CltvExpiryDelta(10000) bob ! open.copy(toSelfDelay = delayTooHigh) val error = bob2alice.expectMsgType[Error] - assert(error === Error(open.temporaryChannelId, ToSelfDelayTooHigh(open.temporaryChannelId, delayTooHigh, Alice.nodeParams.maxToLocalDelay).getMessage)) + assert(error === Error(open.temporaryChannelId, ToSelfDelayTooHigh(open.temporaryChannelId, delayTooHigh, Alice.nodeParams.channelConf.maxToLocalDelay).getMessage)) awaitCond(bob.stateName == CLOSED) } @@ -223,7 +223,7 @@ class WaitForOpenChannelStateSpec extends TestKitBaseClass with FixtureAnyFunSui val dustLimitTooHigh = 2000.sat bob ! open.copy(dustLimitSatoshis = dustLimitTooHigh) val error = bob2alice.expectMsgType[Error] - assert(error === Error(open.temporaryChannelId, DustLimitTooLarge(open.temporaryChannelId, dustLimitTooHigh, Bob.nodeParams.maxRemoteDustLimit).getMessage)) + assert(error === Error(open.temporaryChannelId, DustLimitTooLarge(open.temporaryChannelId, dustLimitTooHigh, Bob.nodeParams.channelConf.maxRemoteDustLimit).getMessage)) awaitCond(bob.stateName == CLOSED) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingCreatedStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingCreatedStateSpec.scala index 1967d53e5e..6a55fff7c3 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingCreatedStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingCreatedStateSpec.scala @@ -43,9 +43,9 @@ class WaitForFundingCreatedStateSpec extends TestKitBaseClass with FixtureAnyFun override def withFixture(test: OneArgTest): Outcome = { import com.softwaremill.quicklens._ val aliceNodeParams = Alice.nodeParams - .modify(_.maxFundingSatoshis).setToIf(test.tags.contains(ChannelStateTestsTags.Wumbo))(Btc(100)) + .modify(_.channelConf.maxFundingSatoshis).setToIf(test.tags.contains(ChannelStateTestsTags.Wumbo))(Btc(100)) val bobNodeParams = Bob.nodeParams - .modify(_.maxFundingSatoshis).setToIf(test.tags.contains(ChannelStateTestsTags.Wumbo))(Btc(100)) + .modify(_.channelConf.maxFundingSatoshis).setToIf(test.tags.contains(ChannelStateTestsTags.Wumbo))(Btc(100)) val (fundingSatoshis, pushMsat) = if (test.tags.contains("funder_below_reserve")) { (1000100 sat, (1000000 sat).toMilliSatoshi) // toLocal = 100 satoshis @@ -85,7 +85,7 @@ class WaitForFundingCreatedStateSpec extends TestKitBaseClass with FixtureAnyFun bob2blockchain.expectMsgType[TxPublisher.SetChannelId] bob2blockchain.expectMsgType[WatchFundingSpent] val watchConfirmed = bob2blockchain.expectMsgType[WatchFundingConfirmed] - assert(watchConfirmed.minDepth === Alice.nodeParams.minDepthBlocks) + assert(watchConfirmed.minDepth === Alice.nodeParams.channelConf.minDepthBlocks) } test("recv FundingCreated (wumbo)", Tag(ChannelStateTestsTags.Wumbo)) { f => @@ -98,7 +98,7 @@ class WaitForFundingCreatedStateSpec extends TestKitBaseClass with FixtureAnyFun bob2blockchain.expectMsgType[WatchFundingSpent] val watchConfirmed = bob2blockchain.expectMsgType[WatchFundingConfirmed] // when we are fundee, we use a higher min depth for wumbo channels - assert(watchConfirmed.minDepth > Bob.nodeParams.minDepthBlocks) + assert(watchConfirmed.minDepth > Bob.nodeParams.channelConf.minDepthBlocks) } test("recv FundingCreated (funder can't pay fees)", Tag("funder_below_reserve")) { f => diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingSignedStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingSignedStateSpec.scala index fb6d20a29e..b58481e05a 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingSignedStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingSignedStateSpec.scala @@ -44,9 +44,9 @@ class WaitForFundingSignedStateSpec extends TestKitBaseClass with FixtureAnyFunS override def withFixture(test: OneArgTest): Outcome = { import com.softwaremill.quicklens._ val aliceNodeParams = Alice.nodeParams - .modify(_.maxFundingSatoshis).setToIf(test.tags.contains(ChannelStateTestsTags.Wumbo))(Btc(100)) + .modify(_.channelConf.maxFundingSatoshis).setToIf(test.tags.contains(ChannelStateTestsTags.Wumbo))(Btc(100)) val bobNodeParams = Bob.nodeParams - .modify(_.maxFundingSatoshis).setToIf(test.tags.contains(ChannelStateTestsTags.Wumbo))(Btc(100)) + .modify(_.channelConf.maxFundingSatoshis).setToIf(test.tags.contains(ChannelStateTestsTags.Wumbo))(Btc(100)) val (fundingSatoshis, pushMsat) = if (test.tags.contains(ChannelStateTestsTags.Wumbo)) { (Btc(5).toSatoshi, TestConstants.pushMsat) @@ -90,7 +90,7 @@ class WaitForFundingSignedStateSpec extends TestKitBaseClass with FixtureAnyFunS assert(txPublished.tx.txid === fundingTxId) assert(txPublished.miningFee > 0.sat) val watchConfirmed = alice2blockchain.expectMsgType[WatchFundingConfirmed] - assert(watchConfirmed.minDepth === Alice.nodeParams.minDepthBlocks) + assert(watchConfirmed.minDepth === Alice.nodeParams.channelConf.minDepthBlocks) aliceOrigin.expectMsgType[ChannelOpenResponse.ChannelOpened] } @@ -102,7 +102,7 @@ class WaitForFundingSignedStateSpec extends TestKitBaseClass with FixtureAnyFunS alice2blockchain.expectMsgType[WatchFundingSpent] val watchConfirmed = alice2blockchain.expectMsgType[WatchFundingConfirmed] // when we are funder, we keep our regular min depth even for wumbo channels - assert(watchConfirmed.minDepth === Alice.nodeParams.minDepthBlocks) + assert(watchConfirmed.minDepth === Alice.nodeParams.channelConf.minDepthBlocks) aliceOrigin.expectMsgType[ChannelOpenResponse.ChannelOpened] } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala index 094a21c062..c63d9ade71 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala @@ -448,8 +448,8 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val sender = TestProbe() val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] assert(alice.underlyingActor.nodeParams.onChainFeeConf.feerateToleranceFor(bob.underlyingActor.nodeParams.nodeId).dustTolerance.maxExposure === 25_000.sat) - assert(alice.underlyingActor.nodeParams.dustLimit === 1100.sat) - assert(bob.underlyingActor.nodeParams.dustLimit === 1000.sat) + assert(alice.underlyingActor.nodeParams.channelConf.dustLimit === 1100.sat) + assert(bob.underlyingActor.nodeParams.channelConf.dustLimit === 1000.sat) // Alice sends HTLCs to Bob that add 21 000 sat to the dust exposure. // She signs them but Bob doesn't answer yet. @@ -472,8 +472,8 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val sender = TestProbe() val initialState = bob.stateData.asInstanceOf[DATA_NORMAL] assert(bob.underlyingActor.nodeParams.onChainFeeConf.feerateToleranceFor(alice.underlyingActor.nodeParams.nodeId).dustTolerance.maxExposure === 30_000.sat) - assert(alice.underlyingActor.nodeParams.dustLimit === 1100.sat) - assert(bob.underlyingActor.nodeParams.dustLimit === 1000.sat) + assert(alice.underlyingActor.nodeParams.channelConf.dustLimit === 1100.sat) + assert(bob.underlyingActor.nodeParams.channelConf.dustLimit === 1000.sat) // Bob sends HTLCs to Alice that add 21 000 sat to the dust exposure. // He signs them but Alice doesn't answer yet. @@ -753,7 +753,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with import f._ val sender = TestProbe() // for the test to be really useful we have constraint on parameters - assert(Alice.nodeParams.dustLimit > Bob.nodeParams.dustLimit) + assert(Alice.nodeParams.channelConf.dustLimit > Bob.nodeParams.channelConf.dustLimit) // and a low feerate to avoid messing with dust exposure limits val currentFeerate = FeeratePerKw(2500 sat) alice.feeEstimator.setFeerate(FeeratesPerKw.single(currentFeerate)) @@ -761,10 +761,10 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with updateFee(currentFeerate, alice, bob, alice2bob, bob2alice) // we're gonna exchange two htlcs in each direction, the goal is to have bob's commitment have 4 htlcs, and alice's // commitment only have 3. We will then check that alice indeed persisted 4 htlcs, and bob only 3. - val aliceMinReceive = Alice.nodeParams.dustLimit + weight2fee(currentFeerate, DefaultCommitmentFormat.htlcSuccessWeight) - val aliceMinOffer = Alice.nodeParams.dustLimit + weight2fee(currentFeerate, DefaultCommitmentFormat.htlcTimeoutWeight) - val bobMinReceive = Bob.nodeParams.dustLimit + weight2fee(currentFeerate, DefaultCommitmentFormat.htlcSuccessWeight) - val bobMinOffer = Bob.nodeParams.dustLimit + weight2fee(currentFeerate, DefaultCommitmentFormat.htlcTimeoutWeight) + val aliceMinReceive = Alice.nodeParams.channelConf.dustLimit + weight2fee(currentFeerate, DefaultCommitmentFormat.htlcSuccessWeight) + val aliceMinOffer = Alice.nodeParams.channelConf.dustLimit + weight2fee(currentFeerate, DefaultCommitmentFormat.htlcTimeoutWeight) + val bobMinReceive = Bob.nodeParams.channelConf.dustLimit + weight2fee(currentFeerate, DefaultCommitmentFormat.htlcSuccessWeight) + val bobMinOffer = Bob.nodeParams.channelConf.dustLimit + weight2fee(currentFeerate, DefaultCommitmentFormat.htlcTimeoutWeight) val a2b_1 = bobMinReceive + 10.sat // will be in alice and bob tx val a2b_2 = bobMinReceive + 20.sat // will be in alice and bob tx val b2a_1 = aliceMinReceive + 10.sat // will be in alice and bob tx @@ -1363,16 +1363,16 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with test("recv RevokeAndAck (over max dust htlc exposure in local commit only with pending local changes)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs), Tag(ChannelStateTestsTags.HighDustLimitDifferenceAliceBob)) { f => import f._ val sender = TestProbe() - assert(alice.underlyingActor.nodeParams.dustLimit === 5000.sat) - assert(bob.underlyingActor.nodeParams.dustLimit === 1000.sat) + assert(alice.underlyingActor.nodeParams.channelConf.dustLimit === 5000.sat) + assert(bob.underlyingActor.nodeParams.channelConf.dustLimit === 1000.sat) testRevokeAndAckDustOverflowSingleCommit(f) } test("recv RevokeAndAck (over max dust htlc exposure in remote commit only with pending local changes)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs), Tag(ChannelStateTestsTags.HighDustLimitDifferenceBobAlice)) { f => import f._ val sender = TestProbe() - assert(alice.underlyingActor.nodeParams.dustLimit === 1000.sat) - assert(bob.underlyingActor.nodeParams.dustLimit === 5000.sat) + assert(alice.underlyingActor.nodeParams.channelConf.dustLimit === 1000.sat) + assert(bob.underlyingActor.nodeParams.channelConf.dustLimit === 5000.sat) testRevokeAndAckDustOverflowSingleCommit(f) } @@ -2746,7 +2746,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with bob ! CMD_FULFILL_HTLC(htlc.id, r, commit = true) bob2alice.expectMsgType[UpdateFulfillHtlc] bob2alice.expectMsgType[CommitSig] - bob ! CurrentBlockHeight(htlc.cltvExpiry.blockHeight - Bob.nodeParams.fulfillSafetyBeforeTimeout.toInt) + bob ! CurrentBlockHeight(htlc.cltvExpiry.blockHeight - Bob.nodeParams.channelConf.fulfillSafetyBeforeTimeout.toInt) val ChannelErrorOccurred(_, _, _, _, LocalError(err), isFatal) = listener.expectMsgType[ChannelErrorOccurred] assert(isFatal) @@ -2779,7 +2779,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with bob ! CMD_FULFILL_HTLC(htlc.id, r, commit = false) bob2alice.expectMsgType[UpdateFulfillHtlc] bob2alice.expectNoMessage(500 millis) - bob ! CurrentBlockHeight(htlc.cltvExpiry.blockHeight - Bob.nodeParams.fulfillSafetyBeforeTimeout.toInt) + bob ! CurrentBlockHeight(htlc.cltvExpiry.blockHeight - Bob.nodeParams.channelConf.fulfillSafetyBeforeTimeout.toInt) val ChannelErrorOccurred(_, _, _, _, LocalError(err), isFatal) = listener.expectMsgType[ChannelErrorOccurred] assert(isFatal) @@ -2816,7 +2816,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with bob2alice.forward(alice) alice2bob.expectMsgType[RevokeAndAck] alice2bob.forward(bob) - bob ! CurrentBlockHeight(htlc.cltvExpiry.blockHeight - Bob.nodeParams.fulfillSafetyBeforeTimeout.toInt) + bob ! CurrentBlockHeight(htlc.cltvExpiry.blockHeight - Bob.nodeParams.channelConf.fulfillSafetyBeforeTimeout.toInt) val ChannelErrorOccurred(_, _, _, _, LocalError(err), isFatal) = listener.expectMsgType[ChannelErrorOccurred] assert(isFatal) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/OfflineStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/OfflineStateSpec.scala index 747c7f12b6..c7853ebe23 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/OfflineStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/OfflineStateSpec.scala @@ -536,7 +536,7 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // We simulate a pending fulfill on that HTLC but not relayed. // When it is close to expiring upstream, we should close the channel. bob.underlyingActor.nodeParams.db.pendingCommands.addSettlementCommand(initialState.channelId, CMD_FULFILL_HTLC(htlc.id, r, commit = true)) - bob ! CurrentBlockHeight(htlc.cltvExpiry.blockHeight - bob.underlyingActor.nodeParams.fulfillSafetyBeforeTimeout.toInt) + bob ! CurrentBlockHeight(htlc.cltvExpiry.blockHeight - bob.underlyingActor.nodeParams.channelConf.fulfillSafetyBeforeTimeout.toInt) val ChannelErrorOccurred(_, _, _, _, LocalError(err), isFatal) = listener.expectMsgType[ChannelErrorOccurred] assert(isFatal) @@ -567,7 +567,7 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // We simulate a pending failure on that HTLC. // Even if we get close to expiring upstream we shouldn't close the channel, because we have nothing to lose. bob ! CMD_FAIL_HTLC(htlc.id, Right(IncorrectOrUnknownPaymentDetails(0 msat, BlockHeight(0)))) - bob ! CurrentBlockHeight(htlc.cltvExpiry.blockHeight - bob.underlyingActor.nodeParams.fulfillSafetyBeforeTimeout.toInt) + bob ! CurrentBlockHeight(htlc.cltvExpiry.blockHeight - bob.underlyingActor.nodeParams.channelConf.fulfillSafetyBeforeTimeout.toInt) bob2blockchain.expectNoMessage(250 millis) alice2blockchain.expectNoMessage(250 millis) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/ChannelIntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/ChannelIntegrationSpec.scala index fa64e7d786..5f27628ab5 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/ChannelIntegrationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/ChannelIntegrationSpec.scala @@ -452,9 +452,9 @@ abstract class ChannelIntegrationSpec extends IntegrationSpec { class StandardChannelIntegrationSpec extends ChannelIntegrationSpec { test("start eclair nodes") { - instantiateEclairNode("A", ConfigFactory.parseMap(Map("eclair.node-alias" -> "A", "eclair.expiry-delta-blocks" -> 40, "eclair.fulfill-safety-before-timeout-blocks" -> 12, "eclair.server.port" -> 29740, "eclair.api.port" -> 28090).asJava).withFallback(withDefaultCommitment).withFallback(commonConfig)) - instantiateEclairNode("C", ConfigFactory.parseMap(Map("eclair.node-alias" -> "C", "eclair.expiry-delta-blocks" -> 40, "eclair.fulfill-safety-before-timeout-blocks" -> 12, "eclair.server.port" -> 29741, "eclair.api.port" -> 28091).asJava).withFallback(withAnchorOutputs).withFallback(commonConfig)) - instantiateEclairNode("F", ConfigFactory.parseMap(Map("eclair.node-alias" -> "F", "eclair.expiry-delta-blocks" -> 40, "eclair.fulfill-safety-before-timeout-blocks" -> 12, "eclair.server.port" -> 29742, "eclair.api.port" -> 28092).asJava).withFallback(withDefaultCommitment).withFallback(commonConfig)) + instantiateEclairNode("A", ConfigFactory.parseMap(Map("eclair.node-alias" -> "A", "eclair.channel.expiry-delta-blocks" -> 40, "eclair.channel.fulfill-safety-before-timeout-blocks" -> 12, "eclair.server.port" -> 29740, "eclair.api.port" -> 28090).asJava).withFallback(withDefaultCommitment).withFallback(commonConfig)) + instantiateEclairNode("C", ConfigFactory.parseMap(Map("eclair.node-alias" -> "C", "eclair.channel.expiry-delta-blocks" -> 40, "eclair.channel.fulfill-safety-before-timeout-blocks" -> 12, "eclair.server.port" -> 29741, "eclair.api.port" -> 28091).asJava).withFallback(withAnchorOutputs).withFallback(commonConfig)) + instantiateEclairNode("F", ConfigFactory.parseMap(Map("eclair.node-alias" -> "F", "eclair.channel.expiry-delta-blocks" -> 40, "eclair.channel.fulfill-safety-before-timeout-blocks" -> 12, "eclair.server.port" -> 29742, "eclair.api.port" -> 28092).asJava).withFallback(withDefaultCommitment).withFallback(commonConfig)) } test("connect nodes") { @@ -779,9 +779,9 @@ class AnchorOutputChannelIntegrationSpec extends AnchorChannelIntegrationSpec { override val commitmentFormat = Transactions.UnsafeLegacyAnchorOutputsCommitmentFormat test("start eclair nodes") { - instantiateEclairNode("A", ConfigFactory.parseMap(Map("eclair.node-alias" -> "A", "eclair.expiry-delta-blocks" -> 40, "eclair.fulfill-safety-before-timeout-blocks" -> 12, "eclair.server.port" -> 29750, "eclair.api.port" -> 28093).asJava).withFallback(withStaticRemoteKey).withFallback(commonConfig)) - instantiateEclairNode("C", ConfigFactory.parseMap(Map("eclair.node-alias" -> "C", "eclair.expiry-delta-blocks" -> 40, "eclair.fulfill-safety-before-timeout-blocks" -> 12, "eclair.server.port" -> 29751, "eclair.api.port" -> 28094).asJava).withFallback(withAnchorOutputs).withFallback(commonConfig)) - instantiateEclairNode("F", ConfigFactory.parseMap(Map("eclair.node-alias" -> "F", "eclair.expiry-delta-blocks" -> 40, "eclair.fulfill-safety-before-timeout-blocks" -> 12, "eclair.server.port" -> 29753, "eclair.api.port" -> 28095).asJava).withFallback(withAnchorOutputs).withFallback(commonConfig)) + instantiateEclairNode("A", ConfigFactory.parseMap(Map("eclair.node-alias" -> "A", "eclair.channel.expiry-delta-blocks" -> 40, "eclair.channel.fulfill-safety-before-timeout-blocks" -> 12, "eclair.server.port" -> 29750, "eclair.api.port" -> 28093).asJava).withFallback(withStaticRemoteKey).withFallback(commonConfig)) + instantiateEclairNode("C", ConfigFactory.parseMap(Map("eclair.node-alias" -> "C", "eclair.channel.expiry-delta-blocks" -> 40, "eclair.channel.fulfill-safety-before-timeout-blocks" -> 12, "eclair.server.port" -> 29751, "eclair.api.port" -> 28094).asJava).withFallback(withAnchorOutputs).withFallback(commonConfig)) + instantiateEclairNode("F", ConfigFactory.parseMap(Map("eclair.node-alias" -> "F", "eclair.channel.expiry-delta-blocks" -> 40, "eclair.channel.fulfill-safety-before-timeout-blocks" -> 12, "eclair.server.port" -> 29753, "eclair.api.port" -> 28095).asJava).withFallback(withAnchorOutputs).withFallback(commonConfig)) } test("connect nodes") { @@ -819,9 +819,9 @@ class AnchorOutputZeroFeeHtlcTxsChannelIntegrationSpec extends AnchorChannelInte override val commitmentFormat = Transactions.ZeroFeeHtlcTxAnchorOutputsCommitmentFormat test("start eclair nodes") { - instantiateEclairNode("A", ConfigFactory.parseMap(Map("eclair.node-alias" -> "A", "eclair.expiry-delta-blocks" -> 40, "eclair.fulfill-safety-before-timeout-blocks" -> 12, "eclair.server.port" -> 29760, "eclair.api.port" -> 28096).asJava).withFallback(withStaticRemoteKey).withFallback(commonConfig)) - instantiateEclairNode("C", ConfigFactory.parseMap(Map("eclair.node-alias" -> "C", "eclair.expiry-delta-blocks" -> 40, "eclair.fulfill-safety-before-timeout-blocks" -> 12, "eclair.server.port" -> 29761, "eclair.api.port" -> 28097).asJava).withFallback(withAnchorOutputsZeroFeeHtlcTxs).withFallback(commonConfig)) - instantiateEclairNode("F", ConfigFactory.parseMap(Map("eclair.node-alias" -> "F", "eclair.expiry-delta-blocks" -> 40, "eclair.fulfill-safety-before-timeout-blocks" -> 12, "eclair.server.port" -> 29763, "eclair.api.port" -> 28098).asJava).withFallback(withAnchorOutputsZeroFeeHtlcTxs).withFallback(commonConfig)) + instantiateEclairNode("A", ConfigFactory.parseMap(Map("eclair.node-alias" -> "A", "eclair.channel.expiry-delta-blocks" -> 40, "eclair.channel.fulfill-safety-before-timeout-blocks" -> 12, "eclair.server.port" -> 29760, "eclair.api.port" -> 28096).asJava).withFallback(withStaticRemoteKey).withFallback(commonConfig)) + instantiateEclairNode("C", ConfigFactory.parseMap(Map("eclair.node-alias" -> "C", "eclair.channel.expiry-delta-blocks" -> 40, "eclair.channel.fulfill-safety-before-timeout-blocks" -> 12, "eclair.server.port" -> 29761, "eclair.api.port" -> 28097).asJava).withFallback(withAnchorOutputsZeroFeeHtlcTxs).withFallback(commonConfig)) + instantiateEclairNode("F", ConfigFactory.parseMap(Map("eclair.node-alias" -> "F", "eclair.channel.expiry-delta-blocks" -> 40, "eclair.channel.fulfill-safety-before-timeout-blocks" -> 12, "eclair.server.port" -> 29763, "eclair.api.port" -> 28098).asJava).withFallback(withAnchorOutputsZeroFeeHtlcTxs).withFallback(commonConfig)) } test("connect nodes") { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/IntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/IntegrationSpec.scala index 14a6e64132..49656e2ef7 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/IntegrationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/IntegrationSpec.scala @@ -81,14 +81,14 @@ abstract class IntegrationSpec extends TestKitBaseClass with BitcoindService wit "eclair.bitcoind.zmqblock" -> s"tcp://127.0.0.1:$bitcoindZmqBlockPort", "eclair.bitcoind.zmqtx" -> s"tcp://127.0.0.1:$bitcoindZmqTxPort", "eclair.bitcoind.wallet" -> defaultWallet, - "eclair.mindepth-blocks" -> 2, - "eclair.max-htlc-value-in-flight-msat" -> 100000000000L, - "eclair.max-block-processing-delay" -> "2 seconds", + "eclair.channel.mindepth-blocks" -> 2, + "eclair.channel.max-htlc-value-in-flight-msat" -> 100000000000L, + "eclair.channel.max-block-processing-delay" -> "2 seconds", + "eclair.channel.to-remote-delay-blocks" -> 24, + "eclair.channel.max-funding-satoshis" -> 500000000, "eclair.router.broadcast-interval" -> "2 seconds", "eclair.auto-reconnect" -> false, - "eclair.to-remote-delay-blocks" -> 24, - "eclair.multi-part-payment-expiry" -> "20 seconds", - "eclair.max-funding-satoshis" -> 500000000).asJava).withFallback(ConfigFactory.load()) + "eclair.multi-part-payment-expiry" -> "20 seconds").asJava).withFallback(ConfigFactory.load()) private val commonFeatures = ConfigFactory.parseMap(Map( s"eclair.features.${DataLossProtect.rfcName}" -> "optional", diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/PaymentIntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/PaymentIntegrationSpec.scala index 4bedba0d95..1d90f2090a 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/PaymentIntegrationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/PaymentIntegrationSpec.scala @@ -58,13 +58,13 @@ import scala.jdk.CollectionConverters._ class PaymentIntegrationSpec extends IntegrationSpec { test("start eclair nodes") { - instantiateEclairNode("A", ConfigFactory.parseMap(Map("eclair.node-alias" -> "A", "eclair.expiry-delta-blocks" -> 130, "eclair.server.port" -> 29730, "eclair.api.port" -> 28080, "eclair.channel.channel-flags.announce-channel" -> false).asJava).withFallback(withDefaultCommitment).withFallback(commonConfig)) // A's channels are private - instantiateEclairNode("B", ConfigFactory.parseMap(Map("eclair.node-alias" -> "B", "eclair.expiry-delta-blocks" -> 131, "eclair.server.port" -> 29731, "eclair.api.port" -> 28081, "eclair.trampoline-payments-enable" -> true).asJava).withFallback(withDefaultCommitment).withFallback(commonConfig)) - instantiateEclairNode("C", ConfigFactory.parseMap(Map("eclair.node-alias" -> "C", "eclair.expiry-delta-blocks" -> 132, "eclair.server.port" -> 29732, "eclair.api.port" -> 28082, "eclair.trampoline-payments-enable" -> true).asJava).withFallback(withAnchorOutputsZeroFeeHtlcTxs).withFallback(commonConfig)) - instantiateEclairNode("D", ConfigFactory.parseMap(Map("eclair.node-alias" -> "D", "eclair.expiry-delta-blocks" -> 133, "eclair.server.port" -> 29733, "eclair.api.port" -> 28083, "eclair.trampoline-payments-enable" -> true).asJava).withFallback(withDefaultCommitment).withFallback(commonConfig)) - instantiateEclairNode("E", ConfigFactory.parseMap(Map("eclair.node-alias" -> "E", "eclair.expiry-delta-blocks" -> 134, "eclair.server.port" -> 29734, "eclair.api.port" -> 28084).asJava).withFallback(withAnchorOutputsZeroFeeHtlcTxs).withFallback(commonConfig)) - instantiateEclairNode("F", ConfigFactory.parseMap(Map("eclair.node-alias" -> "F", "eclair.expiry-delta-blocks" -> 135, "eclair.server.port" -> 29735, "eclair.api.port" -> 28085, "eclair.trampoline-payments-enable" -> true).asJava).withFallback(commonConfig)) - instantiateEclairNode("G", ConfigFactory.parseMap(Map("eclair.node-alias" -> "G", "eclair.expiry-delta-blocks" -> 136, "eclair.server.port" -> 29736, "eclair.api.port" -> 28086, "eclair.relay.fees.public-channels.fee-base-msat" -> 1010, "eclair.relay.fees.public-channels.fee-proportional-millionths" -> 102, "eclair.trampoline-payments-enable" -> true).asJava).withFallback(commonConfig)) + instantiateEclairNode("A", ConfigFactory.parseMap(Map("eclair.node-alias" -> "A", "eclair.channel.expiry-delta-blocks" -> 130, "eclair.server.port" -> 29730, "eclair.api.port" -> 28080, "eclair.channel.channel-flags.announce-channel" -> false).asJava).withFallback(withDefaultCommitment).withFallback(commonConfig)) // A's channels are private + instantiateEclairNode("B", ConfigFactory.parseMap(Map("eclair.node-alias" -> "B", "eclair.channel.expiry-delta-blocks" -> 131, "eclair.server.port" -> 29731, "eclair.api.port" -> 28081, "eclair.trampoline-payments-enable" -> true).asJava).withFallback(withDefaultCommitment).withFallback(commonConfig)) + instantiateEclairNode("C", ConfigFactory.parseMap(Map("eclair.node-alias" -> "C", "eclair.channel.expiry-delta-blocks" -> 132, "eclair.server.port" -> 29732, "eclair.api.port" -> 28082, "eclair.trampoline-payments-enable" -> true).asJava).withFallback(withAnchorOutputsZeroFeeHtlcTxs).withFallback(commonConfig)) + instantiateEclairNode("D", ConfigFactory.parseMap(Map("eclair.node-alias" -> "D", "eclair.channel.expiry-delta-blocks" -> 133, "eclair.server.port" -> 29733, "eclair.api.port" -> 28083, "eclair.trampoline-payments-enable" -> true).asJava).withFallback(withDefaultCommitment).withFallback(commonConfig)) + instantiateEclairNode("E", ConfigFactory.parseMap(Map("eclair.node-alias" -> "E", "eclair.channel.expiry-delta-blocks" -> 134, "eclair.server.port" -> 29734, "eclair.api.port" -> 28084).asJava).withFallback(withAnchorOutputsZeroFeeHtlcTxs).withFallback(commonConfig)) + instantiateEclairNode("F", ConfigFactory.parseMap(Map("eclair.node-alias" -> "F", "eclair.channel.expiry-delta-blocks" -> 135, "eclair.server.port" -> 29735, "eclair.api.port" -> 28085, "eclair.trampoline-payments-enable" -> true).asJava).withFallback(commonConfig)) + instantiateEclairNode("G", ConfigFactory.parseMap(Map("eclair.node-alias" -> "G", "eclair.channel.expiry-delta-blocks" -> 136, "eclair.server.port" -> 29736, "eclair.api.port" -> 28086, "eclair.relay.fees.public-channels.fee-base-msat" -> 1010, "eclair.relay.fees.public-channels.fee-proportional-millionths" -> 102, "eclair.trampoline-payments-enable" -> true).asJava).withFallback(commonConfig)) } test("connect nodes") { @@ -181,7 +181,7 @@ class PaymentIntegrationSpec extends IntegrationSpec { nodes("B").register ! Register.ForwardShortId(sender.ref, shortIdBC, CMD_GETINFO(ActorRef.noSender)) val commitmentBC = sender.expectMsgType[RES_GETINFO].data.asInstanceOf[DATA_NORMAL].commitments // we then forge a new channel_update for B-C... - val channelUpdateBC = Announcements.makeChannelUpdate(Block.RegtestGenesisBlock.hash, nodes("B").nodeParams.privateKey, nodes("C").nodeParams.nodeId, shortIdBC, nodes("B").nodeParams.expiryDelta + 1, nodes("C").nodeParams.htlcMinimum, nodes("B").nodeParams.relayParams.publicChannelFees.feeBase, nodes("B").nodeParams.relayParams.publicChannelFees.feeProportionalMillionths, 500000000 msat) + val channelUpdateBC = Announcements.makeChannelUpdate(Block.RegtestGenesisBlock.hash, nodes("B").nodeParams.privateKey, nodes("C").nodeParams.nodeId, shortIdBC, nodes("B").nodeParams.channelConf.expiryDelta + 1, nodes("C").nodeParams.channelConf.htlcMinimum, nodes("B").nodeParams.relayParams.publicChannelFees.feeBase, nodes("B").nodeParams.relayParams.publicChannelFees.feeProportionalMillionths, 500000000 msat) // ...and notify B's relayer nodes("B").system.eventStream.publish(LocalChannelUpdate(system.deadLetters, commitmentBC.channelId, shortIdBC, commitmentBC.remoteParams.nodeId, None, channelUpdateBC, commitmentBC)) // we retrieve a payment hash from D @@ -215,11 +215,11 @@ class PaymentIntegrationSpec extends IntegrationSpec { logger.info(s"channelUpdateBC=$channelUpdateBC") logger.info(s"channelUpdateBC_new=$channelUpdateBC_new") assert(channelUpdateBC_new.timestamp > channelUpdateBC.timestamp) - assert(channelUpdateBC_new.cltvExpiryDelta == nodes("B").nodeParams.expiryDelta) + assert(channelUpdateBC_new.cltvExpiryDelta == nodes("B").nodeParams.channelConf.expiryDelta) awaitCond({ sender.send(nodes("A").router, Router.GetChannelsMap) val u = updateFor(nodes("B").nodeParams.nodeId, sender.expectMsgType[Map[ShortChannelId, PublicChannel]].apply(channelUpdateBC.shortChannelId)).get - u.cltvExpiryDelta == nodes("B").nodeParams.expiryDelta + u.cltvExpiryDelta == nodes("B").nodeParams.channelConf.expiryDelta }, max = 30 seconds, interval = 1 second) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/PerformanceIntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/PerformanceIntegrationSpec.scala index 2951239564..832d9db522 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/PerformanceIntegrationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/PerformanceIntegrationSpec.scala @@ -43,8 +43,8 @@ class PerformanceIntegrationSpec extends IntegrationSpec { test("start eclair nodes") { val commonPerfTestConfig = ConfigFactory.parseMap(Map( - "eclair.max-funding-satoshis" -> 100_000_000, - "eclair.max-accepted-htlcs" -> Channel.MAX_ACCEPTED_HTLCS, + "eclair.channel.max-funding-satoshis" -> 100_000_000, + "eclair.channel.max-accepted-htlcs" -> Channel.MAX_ACCEPTED_HTLCS, "eclair.file-backup.enabled" -> false, ).asJava) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala index cb270d72f5..2a53f838d9 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala @@ -74,7 +74,7 @@ class PeerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Paralle .modify(_.features).setToIf(test.tags.contains("wumbo"))(Features(Wumbo -> Optional)) .modify(_.features).setToIf(test.tags.contains("anchor_outputs"))(Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional)) .modify(_.features).setToIf(test.tags.contains("anchor_outputs_zero_fee_htlc_tx"))(Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTx -> Optional)) - .modify(_.maxFundingSatoshis).setToIf(test.tags.contains("high-max-funding-satoshis"))(Btc(0.9)) + .modify(_.channelConf.maxFundingSatoshis).setToIf(test.tags.contains("high-max-funding-satoshis"))(Btc(0.9)) .modify(_.autoReconnect).setToIf(test.tags.contains("auto_reconnect"))(true) if (test.tags.contains("with_node_announcement")) { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartHandlerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartHandlerSpec.scala index 697d2789d9..f3f226a681 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartHandlerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartHandlerSpec.scala @@ -75,7 +75,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike val register = TestProbe() val eventListener = TestProbe() system.eventStream.subscribe(eventListener.ref, classOf[PaymentEvent]) - withFixture(test.toNoArgTest(FixtureParam(nodeParams, nodeParams.minFinalExpiryDelta.toCltvExpiry(nodeParams.currentBlockHeight), register, eventListener, TestProbe()))) + withFixture(test.toNoArgTest(FixtureParam(nodeParams, nodeParams.channelConf.minFinalExpiryDelta.toCltvExpiry(nodeParams.currentBlockHeight), register, eventListener, TestProbe()))) } } @@ -173,12 +173,12 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike sender.send(handlerWithMpp, ReceivePayment(Some(42000 msat), Left("1 coffee"))) val pr1 = sender.expectMsgType[PaymentRequest] - assert(pr1.minFinalCltvExpiryDelta === Some(nodeParams.minFinalExpiryDelta)) + assert(pr1.minFinalCltvExpiryDelta === Some(nodeParams.channelConf.minFinalExpiryDelta)) assert(pr1.expiry === Some(Alice.nodeParams.paymentRequestExpiry.toSeconds)) sender.send(handlerWithMpp, ReceivePayment(Some(42000 msat), Left("1 coffee with custom expiry"), expirySeconds_opt = Some(60))) val pr2 = sender.expectMsgType[PaymentRequest] - assert(pr2.minFinalCltvExpiryDelta === Some(nodeParams.minFinalExpiryDelta)) + assert(pr2.minFinalCltvExpiryDelta === Some(nodeParams.channelConf.minFinalExpiryDelta)) assert(pr2.expiry === Some(60)) } @@ -285,7 +285,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike val pr = sender.expectMsgType[PaymentRequest] assert(pr.features.allowMultiPart) - val lowCltvExpiry = nodeParams.fulfillSafetyBeforeTimeout.toCltvExpiry(nodeParams.currentBlockHeight) + val lowCltvExpiry = nodeParams.channelConf.fulfillSafetyBeforeTimeout.toCltvExpiry(nodeParams.currentBlockHeight) val add = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, pr.paymentHash, lowCltvExpiry, TestConstants.emptyOnionPacket) sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createMultiPartPayload(add.amountMsat, 1000 msat, add.cltvExpiry, pr.paymentSecret.get, pr.paymentMetadata))) val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentInitiatorSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentInitiatorSpec.scala index e387ba113b..3b082ff9ec 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentInitiatorSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentInitiatorSpec.scala @@ -436,7 +436,7 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike multiPartPayFsm.send(initiator, failed) val failure = sender.expectMsgType[PaymentFailed] - assert(failure.failures === Seq(LocalFailure(finalAmount, Seq(NodeHop(nodeParams.nodeId, b, nodeParams.expiryDelta, 0 msat), NodeHop(b, c, CltvExpiryDelta(24), 25000 msat)), RouteNotFound))) + assert(failure.failures === Seq(LocalFailure(finalAmount, Seq(NodeHop(nodeParams.nodeId, b, nodeParams.channelConf.expiryDelta, 0 msat), NodeHop(b, c, CltvExpiryDelta(24), 25000 msat)), RouteNotFound))) eventListener.expectMsg(failure) sender.expectNoMessage(100 millis) eventListener.expectNoMessage(100 millis) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/AnnouncementsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/AnnouncementsSpec.scala index e1b1e4eced..fc0be00213 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/AnnouncementsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/AnnouncementsSpec.scala @@ -99,7 +99,7 @@ class AnnouncementsSpec extends AnyFunSuite { } test("create valid signed channel update announcement") { - val ann = makeChannelUpdate(Block.RegtestGenesisBlock.hash, Alice.nodeParams.privateKey, randomKey().publicKey, ShortChannelId(45561L), Alice.nodeParams.expiryDelta, Alice.nodeParams.htlcMinimum, Alice.nodeParams.relayParams.publicChannelFees.feeBase, Alice.nodeParams.relayParams.publicChannelFees.feeProportionalMillionths, 500000000 msat) + val ann = makeChannelUpdate(Block.RegtestGenesisBlock.hash, Alice.nodeParams.privateKey, randomKey().publicKey, ShortChannelId(45561L), Alice.nodeParams.channelConf.expiryDelta, Alice.nodeParams.channelConf.htlcMinimum, Alice.nodeParams.relayParams.publicChannelFees.feeBase, Alice.nodeParams.relayParams.publicChannelFees.feeProportionalMillionths, 500000000 msat) assert(checkSig(ann, Alice.nodeParams.nodeId)) assert(checkSig(ann, randomKey().publicKey) === false) } From 1f6a7afd47e989b7c083ae9da075a9e1dd88e457 Mon Sep 17 00:00:00 2001 From: Pierre-Marie Padiou Date: Thu, 27 Jan 2022 16:29:46 +0100 Subject: [PATCH 006/121] Have sqlite also write to jdbc url file (#2153) Eclair will now refuse to start if the database config is changed from `sqlite` to `postgres` or the opposite. --- .../scala/fr/acinq/eclair/db/Databases.scala | 48 +++++++++++-------- .../scala/fr/acinq/eclair/db/pg/PgUtils.scala | 3 -- .../scala/fr/acinq/eclair/TestDatabases.scala | 6 ++- .../fr/acinq/eclair/db/PgUtilsSpec.scala | 5 +- .../fr/acinq/eclair/db/SqliteUtilsSpec.scala | 33 ++++++++++++- 5 files changed, 66 insertions(+), 29 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/Databases.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/Databases.scala index 2e78f6d9d1..8f6f768f6d 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/Databases.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/Databases.scala @@ -73,15 +73,18 @@ object Databases extends Logging { } object SqliteDatabases { - def apply(auditJdbc: Connection, networkJdbc: Connection, eclairJdbc: Connection): SqliteDatabases = SqliteDatabases( - network = new SqliteNetworkDb(networkJdbc), - audit = new SqliteAuditDb(auditJdbc), - channels = new SqliteChannelsDb(eclairJdbc), - peers = new SqlitePeersDb(eclairJdbc), - payments = new SqlitePaymentsDb(eclairJdbc), - pendingCommands = new SqlitePendingCommandsDb(eclairJdbc), - backupConnection = eclairJdbc - ) + def apply(auditJdbc: Connection, networkJdbc: Connection, eclairJdbc: Connection, jdbcUrlFile_opt: Option[File]): SqliteDatabases = { + jdbcUrlFile_opt.foreach(checkIfDatabaseUrlIsUnchanged("sqlite", _)) + SqliteDatabases( + network = new SqliteNetworkDb(networkJdbc), + audit = new SqliteAuditDb(auditJdbc), + channels = new SqliteChannelsDb(eclairJdbc), + peers = new SqlitePeersDb(eclairJdbc), + payments = new SqlitePaymentsDb(eclairJdbc), + pendingCommands = new SqlitePendingCommandsDb(eclairJdbc), + backupConnection = eclairJdbc + ) + } } case class PostgresDatabases private(network: PgNetworkDb, @@ -241,19 +244,22 @@ object Databases extends Logging { databases } + } - private def checkIfDatabaseUrlIsUnchanged(url: String, urlFile: File): Unit = { - def readString(path: Path): String = Files.readAllLines(path).get(0) + /** We raise this exception when the jdbc url changes, to prevent using a different server involuntarily. */ + case class JdbcUrlChanged(before: String, after: String) extends RuntimeException(s"The database URL has changed since the last start. It was `$before`, now it's `$after`. If this was intended, make sure you have migrated your data, otherwise your channels will be force-closed and you may lose funds.") - def writeString(path: Path, string: String): Unit = Files.write(path, java.util.Arrays.asList(string)) + private def checkIfDatabaseUrlIsUnchanged(url: String, urlFile: File): Unit = { + def readString(path: Path): String = Files.readAllLines(path).get(0) - if (urlFile.exists()) { - val oldUrl = readString(urlFile.toPath) - if (oldUrl != url) - throw JdbcUrlChanged(oldUrl, url) - } else { - writeString(urlFile.toPath, url) - } + def writeString(path: Path, string: String): Unit = Files.write(path, java.util.Arrays.asList(string)) + + if (urlFile.exists()) { + val oldUrl = readString(urlFile.toPath) + if (oldUrl != url) + throw JdbcUrlChanged(oldUrl, url) + } else { + writeString(urlFile.toPath, url) } } @@ -278,10 +284,12 @@ object Databases extends Logging { */ def sqlite(dbdir: File): SqliteDatabases = { dbdir.mkdirs() + val jdbcUrlFile = new File(dbdir, "last_jdbcurl") SqliteDatabases( eclairJdbc = SqliteUtils.openSqliteFile(dbdir, "eclair.sqlite", exclusiveLock = true, journalMode = "wal", syncFlag = "full"), // there should only be one process writing to this file networkJdbc = SqliteUtils.openSqliteFile(dbdir, "network.sqlite", exclusiveLock = false, journalMode = "wal", syncFlag = "normal"), // we don't need strong durability guarantees on the network db - auditJdbc = SqliteUtils.openSqliteFile(dbdir, "audit.sqlite", exclusiveLock = false, journalMode = "wal", syncFlag = "full") + auditJdbc = SqliteUtils.openSqliteFile(dbdir, "audit.sqlite", exclusiveLock = false, journalMode = "wal", syncFlag = "full"), + jdbcUrlFile_opt = Some(jdbcUrlFile) ) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgUtils.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgUtils.scala index 23d662c5d9..bea3a81171 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgUtils.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgUtils.scala @@ -32,9 +32,6 @@ import scala.concurrent.duration._ object PgUtils extends JdbcUtils { - /** We raise this exception when the jdbc url changes, to prevent using a different server involuntarily. */ - case class JdbcUrlChanged(before: String, after: String) extends RuntimeException(s"The database URL has changed since the last start. It was `$before`, now it's `$after`") - sealed trait PgLock { def obtainExclusiveLock(implicit ds: DataSource): Unit diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/TestDatabases.scala b/eclair-core/src/test/scala/fr/acinq/eclair/TestDatabases.scala index 868707f4d3..e626c06f7b 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/TestDatabases.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/TestDatabases.scala @@ -41,7 +41,7 @@ object TestDatabases { def inMemoryDb(): Databases = { val connection = sqliteInMemory() - val dbs = Databases.SqliteDatabases(connection, connection, connection) + val dbs = Databases.SqliteDatabases(connection, connection, connection, jdbcUrlFile_opt = None) dbs.copy(channels = new SqliteChannelsDbWithValidation(dbs.channels)) } @@ -89,7 +89,9 @@ object TestDatabases { // @formatter:off override val connection: SQLiteConnection = sqliteInMemory() override lazy val db: Databases = { - val dbs = Databases.SqliteDatabases(connection, connection, connection) + val jdbcUrlFile: File = new File(TestUtils.BUILD_DIRECTORY, s"jdbcUrlFile_${UUID.randomUUID()}.tmp") + jdbcUrlFile.deleteOnExit() + val dbs = Databases.SqliteDatabases(connection, connection, connection, jdbcUrlFile_opt = Some(jdbcUrlFile)) dbs.copy(channels = new SqliteChannelsDbWithValidation(dbs.channels)) } override def close(): Unit = () diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/PgUtilsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/PgUtilsSpec.scala index ff90d0ea41..3a65c0d8f0 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/PgUtilsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/PgUtilsSpec.scala @@ -1,11 +1,12 @@ package fr.acinq.eclair.db import com.opentable.db.postgres.embedded.EmbeddedPostgres -import com.typesafe.config.{Config, ConfigFactory, ConfigValue} +import com.typesafe.config.{Config, ConfigFactory} +import fr.acinq.eclair.db.Databases.JdbcUrlChanged import fr.acinq.eclair.db.DbEventHandler.ChannelEvent import fr.acinq.eclair.db.pg.PgUtils.ExtendedResultSet._ import fr.acinq.eclair.db.pg.PgUtils.PgLock.{LeaseLock, LockFailure, LockFailureHandler} -import fr.acinq.eclair.db.pg.PgUtils.{JdbcUrlChanged, migrateTable, using} +import fr.acinq.eclair.db.pg.PgUtils.{migrateTable, using} import fr.acinq.eclair.payment.ChannelPaymentRelayed import fr.acinq.eclair.router.Announcements import fr.acinq.eclair.wire.internal.channel.ChannelCodecsSpec diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteUtilsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteUtilsSpec.scala index 03082bb10b..feead2ce86 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteUtilsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteUtilsSpec.scala @@ -16,11 +16,16 @@ package fr.acinq.eclair.db -import java.sql.SQLException -import fr.acinq.eclair.{TestConstants, TestDatabases} +import fr.acinq.eclair.db.Databases.JdbcUrlChanged import fr.acinq.eclair.db.sqlite.SqliteUtils.using +import fr.acinq.eclair.{TestDatabases, TestUtils} import org.scalatest.funsuite.AnyFunSuite +import java.io.File +import java.nio.file.Files +import java.sql.SQLException +import java.util.UUID + class SqliteUtilsSpec extends AnyFunSuite { test("using with auto-commit disabled") { @@ -74,4 +79,28 @@ class SqliteUtilsSpec extends AnyFunSuite { } } + test("jdbc url check") { + val datadir = new File(TestUtils.BUILD_DIRECTORY, s"sqlite_test_${UUID.randomUUID()}") + val jdbcUrlPath = new File(datadir, "last_jdbcurl") + datadir.mkdirs() + + // first start : write to file + val db1 = Databases.sqlite(datadir) + db1.channels.close() + + assert(Files.readString(jdbcUrlPath.toPath).trim == "sqlite") + + // 2nd start : no-op + val db2 = Databases.sqlite(datadir) + db2.channels.close() + + // we modify the file + Files.writeString(jdbcUrlPath.toPath, "postgres") + + // boom + intercept[JdbcUrlChanged] { + Databases.sqlite(datadir) + } + } + } From 0333e11c79de838cfd2bd7946d0c29b2bae3a8d6 Mon Sep 17 00:00:00 2001 From: Pierre-Marie Padiou Date: Mon, 31 Jan 2022 12:49:39 +0100 Subject: [PATCH 007/121] Database migration Sqlite->Postgres (#2156) Add a migration tool and a step-by-step guide for users who want to switch from Sqlite to Postgres. --- docs/PostgreSQL.md | 32 +- eclair-core/pom.xml | 6 + eclair-core/src/main/resources/reference.conf | 6 +- .../scala/fr/acinq/eclair/db/Databases.scala | 36 +- .../fr/acinq/eclair/db/DualDatabases.scala | 311 +++++++++--------- .../fr/acinq/eclair/db/jdbc/JdbcUtils.scala | 5 + .../eclair/db/migration/CompareAuditDb.scala | 280 ++++++++++++++++ .../db/migration/CompareChannelsDb.scala | 80 +++++ .../acinq/eclair/db/migration/CompareDb.scala | 81 +++++ .../db/migration/CompareNetworkDb.scala | 73 ++++ .../db/migration/ComparePaymentsDb.scala | 87 +++++ .../eclair/db/migration/ComparePeersDb.scala | 51 +++ .../migration/ComparePendingCommandsDb.scala | 33 ++ .../eclair/db/migration/MigrateAuditDb.scala | 188 +++++++++++ .../db/migration/MigrateChannelsDb.scala | 56 ++++ .../acinq/eclair/db/migration/MigrateDb.scala | 53 +++ .../db/migration/MigrateNetworkDb.scala | 74 +++++ .../db/migration/MigratePaymentsDb.scala | 60 ++++ .../eclair/db/migration/MigratePeersDb.scala | 41 +++ .../migration/MigratePendingCommandsDb.scala | 28 ++ .../eclair/db/sqlite/SqliteAuditDb.scala | 2 +- .../eclair/db/sqlite/SqliteNetworkDb.scala | 2 +- .../eclair/db/sqlite/SqlitePaymentsDb.scala | 2 +- .../eclair/db/sqlite/SqlitePeersDb.scala | 2 +- .../db/sqlite/SqlitePendingCommandsDb.scala | 2 +- .../fr/acinq/eclair/db/DbMigrationSpec.scala | 117 +++++++ .../acinq/eclair/db/DualDatabasesSpec.scala | 86 +++++ .../fr/acinq/eclair/db/PgUtilsSpec.scala | 25 +- .../fr/acinq/eclair/db/SqliteUtilsSpec.scala | 8 +- 29 files changed, 1642 insertions(+), 185 deletions(-) create mode 100644 eclair-core/src/main/scala/fr/acinq/eclair/db/migration/CompareAuditDb.scala create mode 100644 eclair-core/src/main/scala/fr/acinq/eclair/db/migration/CompareChannelsDb.scala create mode 100644 eclair-core/src/main/scala/fr/acinq/eclair/db/migration/CompareDb.scala create mode 100644 eclair-core/src/main/scala/fr/acinq/eclair/db/migration/CompareNetworkDb.scala create mode 100644 eclair-core/src/main/scala/fr/acinq/eclair/db/migration/ComparePaymentsDb.scala create mode 100644 eclair-core/src/main/scala/fr/acinq/eclair/db/migration/ComparePeersDb.scala create mode 100644 eclair-core/src/main/scala/fr/acinq/eclair/db/migration/ComparePendingCommandsDb.scala create mode 100644 eclair-core/src/main/scala/fr/acinq/eclair/db/migration/MigrateAuditDb.scala create mode 100644 eclair-core/src/main/scala/fr/acinq/eclair/db/migration/MigrateChannelsDb.scala create mode 100644 eclair-core/src/main/scala/fr/acinq/eclair/db/migration/MigrateDb.scala create mode 100644 eclair-core/src/main/scala/fr/acinq/eclair/db/migration/MigrateNetworkDb.scala create mode 100644 eclair-core/src/main/scala/fr/acinq/eclair/db/migration/MigratePaymentsDb.scala create mode 100644 eclair-core/src/main/scala/fr/acinq/eclair/db/migration/MigratePeersDb.scala create mode 100644 eclair-core/src/main/scala/fr/acinq/eclair/db/migration/MigratePendingCommandsDb.scala create mode 100644 eclair-core/src/test/scala/fr/acinq/eclair/db/DbMigrationSpec.scala create mode 100644 eclair-core/src/test/scala/fr/acinq/eclair/db/DualDatabasesSpec.scala diff --git a/docs/PostgreSQL.md b/docs/PostgreSQL.md index e312da36de..315d45b3f8 100644 --- a/docs/PostgreSQL.md +++ b/docs/PostgreSQL.md @@ -111,4 +111,34 @@ Eclair stores the latest database settings in the `${data-dir}/last_jdbcurl` fil The node operator can force Eclair to accept new database connection settings by removing the `last_jdbcurl` file. - \ No newline at end of file + +### Migrating from Sqlite to Postgres + +Eclair supports migrating your existing node from Sqlite to Postgres. Note that the opposite (from Postgres to Sqlite) is not supported. + +:warning: Once you have migrated from Sqlite to Postgres there is no going back! + +To migrate from Sqlite to Postgres, follow these steps: +1. Stop Eclair +2. Edit `eclair.conf` + 1. Set `eclair.db.postgres.*` as explained in the section [Connection Settings](#connection-settings). + 2. Set `eclair.db.driver=dual-sqlite-primary`. This will make Eclair use both databases backends. All calls to sqlite will be replicated in postgres. + 3. Set `eclair.db.dual.migrate-on-restart=true`. This will make Eclair migrate the data from Sqlite to Postgres at startup. + 4. Set `eclair.db.dual.compare-on-restart=true`. This will make Eclair compare Sqlite and Postgres at startup. The result of the comparison is displayed in the logs. +3. Delete the file `~/.eclair/last_jdbcurl`. The purpose of this file is to prevent accidental change in the database backend. +4. Start Eclair. You should see in the logs: + 1. `migrating all tables...` + 2. `migration complete` + 3. `comparing all tables...` + 4. `comparison complete identical=true` (NB: if `identical=false`, contact support) +5. Eclair should then finish startup and operate normally. Data has been migrated to Postgres, and Sqlite/Postgres will be maintained in sync going forward. +6. Edit `eclair.conf` and set `eclair.db.dual.migrate-on-restart=false` but do not restart Eclair yet. +7. We recommend that you leave Eclair in dual db mode for a while, to make sure that you don't have issues with your new Postgres database. This a good time to set up [Backups and replication](#backups-and-replication). +8. After some time has passed, restart Eclair. You should see in the logs: + 1. `comparing all tables...` + 2. `comparison complete identical=true` (NB: if `identical=false`, contact support) +9. At this point we have confidence that the Postgres backend works normally, and we are ready to drop Sqlite for good. +10. Edit `eclair.conf` + 1. Set `eclair.db.driver=postgres` + 2. Set `eclair.db.dual.compare-on-restart=false` +11. Restart Eclair. From this moment, you cannot go back to Sqlite! If you try to do so, Eclair will refuse to start. \ No newline at end of file diff --git a/eclair-core/pom.xml b/eclair-core/pom.xml index 80603e99c3..a027974b5f 100644 --- a/eclair-core/pom.xml +++ b/eclair-core/pom.xml @@ -204,6 +204,12 @@ scodec-core_${scala.version.short} 1.11.8 + + + org.scodec + scodec-bits_${scala.version.short} + 1.1.25 + commons-codec commons-codec diff --git a/eclair-core/src/main/resources/reference.conf b/eclair-core/src/main/resources/reference.conf index 10bab482ba..ee5e42285e 100644 --- a/eclair-core/src/main/resources/reference.conf +++ b/eclair-core/src/main/resources/reference.conf @@ -311,7 +311,7 @@ eclair { } db { - driver = "sqlite" // sqlite, postgres + driver = "sqlite" // sqlite, postgres, dual-sqlite-primary, dual-postgres-primary postgres { database = "eclair" host = "localhost" @@ -353,6 +353,10 @@ eclair { } } } + dual { + migrate-on-restart = false // migrate sqlite -> postgres on restart (only applies if sqlite is primary) + compare-on-restart = false // compare sqlite and postgres dbs on restart (only applies if sqlite is primary) + } } file-backup { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/Databases.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/Databases.scala index 8f6f768f6d..9713cfbf1b 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/Databases.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/Databases.scala @@ -21,6 +21,7 @@ import akka.actor.{ActorSystem, CoordinatedShutdown} import com.typesafe.config.Config import com.zaxxer.hikari.{HikariConfig, HikariDataSource} import fr.acinq.eclair.TimestampMilli +import fr.acinq.eclair.db.migration.{CompareDb, MigrateDb} import fr.acinq.eclair.db.pg.PgUtils.PgLock.LockFailureHandler import fr.acinq.eclair.db.pg.PgUtils._ import fr.acinq.eclair.db.pg._ @@ -267,13 +268,25 @@ object Databases extends Logging { db match { case Some(d) => d case None => + val jdbcUrlFile = new File(chaindir, "last_jdbcurl") dbConfig.getString("driver") match { - case "sqlite" => Databases.sqlite(chaindir) - case "postgres" => Databases.postgres(dbConfig, instanceId, chaindir) - case "dual" => - val sqlite = Databases.sqlite(chaindir) - val postgres = Databases.postgres(dbConfig, instanceId, chaindir) - DualDatabases(sqlite, postgres) + case "sqlite" => Databases.sqlite(chaindir, jdbcUrlFile_opt = Some(jdbcUrlFile)) + case "postgres" => Databases.postgres(dbConfig, instanceId, chaindir, jdbcUrlFile_opt = Some(jdbcUrlFile)) + case dual@("dual-sqlite-primary" | "dual-postgres-primary") => + logger.info(s"using $dual database mode") + val sqlite = Databases.sqlite(chaindir, jdbcUrlFile_opt = None) + val postgres = Databases.postgres(dbConfig, instanceId, chaindir, jdbcUrlFile_opt = None) + val (primary, secondary) = if (dual == "dual-sqlite-primary") (sqlite, postgres) else (postgres, sqlite) + val dualDb = DualDatabases(primary, secondary) + if (primary == sqlite) { + if (dbConfig.getBoolean("dual.migrate-on-restart")) { + MigrateDb.migrateAll(dualDb) + } + if (dbConfig.getBoolean("dual.compare-on-restart")) { + CompareDb.compareAll(dualDb) + } + } + dualDb case driver => throw new RuntimeException(s"unknown database driver `$driver`") } } @@ -282,18 +295,17 @@ object Databases extends Logging { /** * Given a parent folder it creates or loads all the databases from a JDBC connection */ - def sqlite(dbdir: File): SqliteDatabases = { + def sqlite(dbdir: File, jdbcUrlFile_opt: Option[File]): SqliteDatabases = { dbdir.mkdirs() - val jdbcUrlFile = new File(dbdir, "last_jdbcurl") SqliteDatabases( eclairJdbc = SqliteUtils.openSqliteFile(dbdir, "eclair.sqlite", exclusiveLock = true, journalMode = "wal", syncFlag = "full"), // there should only be one process writing to this file networkJdbc = SqliteUtils.openSqliteFile(dbdir, "network.sqlite", exclusiveLock = false, journalMode = "wal", syncFlag = "normal"), // we don't need strong durability guarantees on the network db auditJdbc = SqliteUtils.openSqliteFile(dbdir, "audit.sqlite", exclusiveLock = false, journalMode = "wal", syncFlag = "full"), - jdbcUrlFile_opt = Some(jdbcUrlFile) + jdbcUrlFile_opt = jdbcUrlFile_opt ) } - def postgres(dbConfig: Config, instanceId: UUID, dbdir: File, lockExceptionHandler: LockFailureHandler = LockFailureHandler.logAndStop)(implicit system: ActorSystem): PostgresDatabases = { + def postgres(dbConfig: Config, instanceId: UUID, dbdir: File, jdbcUrlFile_opt: Option[File], lockExceptionHandler: LockFailureHandler = LockFailureHandler.logAndStop)(implicit system: ActorSystem): PostgresDatabases = { dbdir.mkdirs() val database = dbConfig.getString("postgres.database") val host = dbConfig.getString("postgres.host") @@ -328,8 +340,6 @@ object Databases extends Logging { case unknownLock => throw new RuntimeException(s"unknown postgres lock type: `$unknownLock`") } - val jdbcUrlFile = new File(dbdir, "last_jdbcurl") - val safetyChecks_opt = if (dbConfig.getBoolean("postgres.safety-checks.enabled")) { Some(PostgresDatabases.SafetyChecks( localChannelsMaxAge = FiniteDuration(dbConfig.getDuration("postgres.safety-checks.max-age.local-channels").getSeconds, TimeUnit.SECONDS), @@ -345,7 +355,7 @@ object Databases extends Logging { hikariConfig = hikariConfig, instanceId = instanceId, lock = lock, - jdbcUrlFile_opt = Some(jdbcUrlFile), + jdbcUrlFile_opt = jdbcUrlFile_opt, readOnlyUser_opt = readOnlyUser_opt, resetJsonColumns = resetJsonColumns, safetyChecks_opt = safetyChecks_opt diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/DualDatabases.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/DualDatabases.scala index 9ca1a2ed56..29f9b3e1ce 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/DualDatabases.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/DualDatabases.scala @@ -6,8 +6,7 @@ import fr.acinq.eclair.channel._ import fr.acinq.eclair.db.Databases.{FileBackup, PostgresDatabases, SqliteDatabases} import fr.acinq.eclair.db.DbEventHandler.ChannelEvent import fr.acinq.eclair.db.DualDatabases.runAsync -import fr.acinq.eclair.db.pg._ -import fr.acinq.eclair.db.sqlite._ +import fr.acinq.eclair.io.Peer import fr.acinq.eclair.payment._ import fr.acinq.eclair.payment.relay.Relayer.RelayFees import fr.acinq.eclair.router.Router @@ -23,25 +22,30 @@ import scala.concurrent.{ExecutionContext, Future} import scala.util.{Failure, Success, Try} /** - * An implementation of [[Databases]] where there are two separate underlying db, one sqlite and one postgres. - * Sqlite is the main database, but we also replicate all calls to postgres. - * Calls to postgres are made asynchronously in a dedicated thread pool, so that it doesn't have any performance impact. + * An implementation of [[Databases]] where there are two separate underlying db, one primary and one secondary. + * All calls to primary are replicated asynchronously to secondary. + * Calls to secondary are made asynchronously in a dedicated thread pool, so that it doesn't have any performance impact. */ -case class DualDatabases(sqlite: SqliteDatabases, postgres: PostgresDatabases) extends Databases with FileBackup { +case class DualDatabases(primary: Databases, secondary: Databases) extends Databases with FileBackup { - override val network: NetworkDb = DualNetworkDb(sqlite.network, postgres.network) + override val network: NetworkDb = DualNetworkDb(primary.network, secondary.network) - override val audit: AuditDb = DualAuditDb(sqlite.audit, postgres.audit) + override val audit: AuditDb = DualAuditDb(primary.audit, secondary.audit) - override val channels: ChannelsDb = DualChannelsDb(sqlite.channels, postgres.channels) + override val channels: ChannelsDb = DualChannelsDb(primary.channels, secondary.channels) - override val peers: PeersDb = DualPeersDb(sqlite.peers, postgres.peers) + override val peers: PeersDb = DualPeersDb(primary.peers, secondary.peers) - override val payments: PaymentsDb = DualPaymentsDb(sqlite.payments, postgres.payments) + override val payments: PaymentsDb = DualPaymentsDb(primary.payments, secondary.payments) - override val pendingCommands: PendingCommandsDb = DualPendingCommandsDb(sqlite.pendingCommands, postgres.pendingCommands) + override val pendingCommands: PendingCommandsDb = DualPendingCommandsDb(primary.pendingCommands, secondary.pendingCommands) - override def backup(backupFile: File): Unit = sqlite.backup(backupFile) + /** if one of the database supports file backup, we use it */ + override def backup(backupFile: File): Unit = (primary, secondary) match { + case (f: FileBackup, _) => f.backup(backupFile) + case (_, f: FileBackup) => f.backup(backupFile) + case _ => () + } } object DualDatabases extends Logging { @@ -55,360 +59,369 @@ object DualDatabases extends Logging { throw t } } + + def getDatabases(dualDatabases: DualDatabases): (SqliteDatabases, PostgresDatabases) = + (dualDatabases.primary, dualDatabases.secondary) match { + case (sqliteDb: SqliteDatabases, postgresDb: PostgresDatabases) => + (sqliteDb, postgresDb) + case (postgresDb: PostgresDatabases, sqliteDb: SqliteDatabases) => + (sqliteDb, postgresDb) + case _ => throw new IllegalArgumentException("there must be one sqlite and one postgres in dual db mode") + } } -case class DualNetworkDb(sqlite: SqliteNetworkDb, postgres: PgNetworkDb) extends NetworkDb { +case class DualNetworkDb(primary: NetworkDb, secondary: NetworkDb) extends NetworkDb { private implicit val ec: ExecutionContext = ExecutionContext.fromExecutor(Executors.newSingleThreadExecutor(new ThreadFactoryBuilder().setNameFormat("db-network").build())) override def addNode(n: NodeAnnouncement): Unit = { - runAsync(postgres.addNode(n)) - sqlite.addNode(n) + runAsync(secondary.addNode(n)) + primary.addNode(n) } override def updateNode(n: NodeAnnouncement): Unit = { - runAsync(postgres.updateNode(n)) - sqlite.updateNode(n) + runAsync(secondary.updateNode(n)) + primary.updateNode(n) } override def getNode(nodeId: Crypto.PublicKey): Option[NodeAnnouncement] = { - runAsync(postgres.getNode(nodeId)) - sqlite.getNode(nodeId) + runAsync(secondary.getNode(nodeId)) + primary.getNode(nodeId) } override def removeNode(nodeId: Crypto.PublicKey): Unit = { - runAsync(postgres.removeNode(nodeId)) - sqlite.removeNode(nodeId) + runAsync(secondary.removeNode(nodeId)) + primary.removeNode(nodeId) } override def listNodes(): Seq[NodeAnnouncement] = { - runAsync(postgres.listNodes()) - sqlite.listNodes() + runAsync(secondary.listNodes()) + primary.listNodes() } override def addChannel(c: ChannelAnnouncement, txid: ByteVector32, capacity: Satoshi): Unit = { - runAsync(postgres.addChannel(c, txid, capacity)) - sqlite.addChannel(c, txid, capacity) + runAsync(secondary.addChannel(c, txid, capacity)) + primary.addChannel(c, txid, capacity) } override def updateChannel(u: ChannelUpdate): Unit = { - runAsync(postgres.updateChannel(u)) - sqlite.updateChannel(u) + runAsync(secondary.updateChannel(u)) + primary.updateChannel(u) } override def removeChannels(shortChannelIds: Iterable[ShortChannelId]): Unit = { - runAsync(postgres.removeChannels(shortChannelIds)) - sqlite.removeChannels(shortChannelIds) + runAsync(secondary.removeChannels(shortChannelIds)) + primary.removeChannels(shortChannelIds) } override def listChannels(): SortedMap[ShortChannelId, Router.PublicChannel] = { - runAsync(postgres.listChannels()) - sqlite.listChannels() + runAsync(secondary.listChannels()) + primary.listChannels() } override def addToPruned(shortChannelIds: Iterable[ShortChannelId]): Unit = { - runAsync(postgres.addToPruned(shortChannelIds)) - sqlite.addToPruned(shortChannelIds) + runAsync(secondary.addToPruned(shortChannelIds)) + primary.addToPruned(shortChannelIds) } override def removeFromPruned(shortChannelId: ShortChannelId): Unit = { - runAsync(postgres.removeFromPruned(shortChannelId)) - sqlite.removeFromPruned(shortChannelId) + runAsync(secondary.removeFromPruned(shortChannelId)) + primary.removeFromPruned(shortChannelId) } override def isPruned(shortChannelId: ShortChannelId): Boolean = { - runAsync(postgres.isPruned(shortChannelId)) - sqlite.isPruned(shortChannelId) + runAsync(secondary.isPruned(shortChannelId)) + primary.isPruned(shortChannelId) } override def close(): Unit = { - runAsync(postgres.close()) - sqlite.close() + runAsync(secondary.close()) + primary.close() } } -case class DualAuditDb(sqlite: SqliteAuditDb, postgres: PgAuditDb) extends AuditDb { +case class DualAuditDb(primary: AuditDb, secondary: AuditDb) extends AuditDb { private implicit val ec: ExecutionContext = ExecutionContext.fromExecutor(Executors.newSingleThreadExecutor(new ThreadFactoryBuilder().setNameFormat("db-audit").build())) override def add(channelLifecycle: DbEventHandler.ChannelEvent): Unit = { - runAsync(postgres.add(channelLifecycle)) - sqlite.add(channelLifecycle) + runAsync(secondary.add(channelLifecycle)) + primary.add(channelLifecycle) } override def add(paymentSent: PaymentSent): Unit = { - runAsync(postgres.add(paymentSent)) - sqlite.add(paymentSent) + runAsync(secondary.add(paymentSent)) + primary.add(paymentSent) } override def add(paymentReceived: PaymentReceived): Unit = { - runAsync(postgres.add(paymentReceived)) - sqlite.add(paymentReceived) + runAsync(secondary.add(paymentReceived)) + primary.add(paymentReceived) } override def add(paymentRelayed: PaymentRelayed): Unit = { - runAsync(postgres.add(paymentRelayed)) - sqlite.add(paymentRelayed) + runAsync(secondary.add(paymentRelayed)) + primary.add(paymentRelayed) } override def add(txPublished: TransactionPublished): Unit = { - runAsync(postgres.add(txPublished)) - sqlite.add(txPublished) + runAsync(secondary.add(txPublished)) + primary.add(txPublished) } override def add(txConfirmed: TransactionConfirmed): Unit = { - runAsync(postgres.add(txConfirmed)) - sqlite.add(txConfirmed) + runAsync(secondary.add(txConfirmed)) + primary.add(txConfirmed) } override def add(channelErrorOccurred: ChannelErrorOccurred): Unit = { - runAsync(postgres.add(channelErrorOccurred)) - sqlite.add(channelErrorOccurred) + runAsync(secondary.add(channelErrorOccurred)) + primary.add(channelErrorOccurred) } override def addChannelUpdate(channelUpdateParametersChanged: ChannelUpdateParametersChanged): Unit = { - runAsync(postgres.addChannelUpdate(channelUpdateParametersChanged)) - sqlite.addChannelUpdate(channelUpdateParametersChanged) + runAsync(secondary.addChannelUpdate(channelUpdateParametersChanged)) + primary.addChannelUpdate(channelUpdateParametersChanged) } override def addPathFindingExperimentMetrics(metrics: PathFindingExperimentMetrics): Unit = { - runAsync(postgres.addPathFindingExperimentMetrics(metrics)) - sqlite.addPathFindingExperimentMetrics(metrics) + runAsync(secondary.addPathFindingExperimentMetrics(metrics)) + primary.addPathFindingExperimentMetrics(metrics) } override def listSent(from: TimestampMilli, to: TimestampMilli): Seq[PaymentSent] = { - runAsync(postgres.listSent(from, to)) - sqlite.listSent(from, to) + runAsync(secondary.listSent(from, to)) + primary.listSent(from, to) } override def listReceived(from: TimestampMilli, to: TimestampMilli): Seq[PaymentReceived] = { - runAsync(postgres.listReceived(from, to)) - sqlite.listReceived(from, to) + runAsync(secondary.listReceived(from, to)) + primary.listReceived(from, to) } override def listRelayed(from: TimestampMilli, to: TimestampMilli): Seq[PaymentRelayed] = { - runAsync(postgres.listRelayed(from, to)) - sqlite.listRelayed(from, to) + runAsync(secondary.listRelayed(from, to)) + primary.listRelayed(from, to) } override def listNetworkFees(from: TimestampMilli, to: TimestampMilli): Seq[AuditDb.NetworkFee] = { - runAsync(postgres.listNetworkFees(from, to)) - sqlite.listNetworkFees(from, to) + runAsync(secondary.listNetworkFees(from, to)) + primary.listNetworkFees(from, to) } override def stats(from: TimestampMilli, to: TimestampMilli): Seq[AuditDb.Stats] = { - runAsync(postgres.stats(from, to)) - sqlite.stats(from, to) + runAsync(secondary.stats(from, to)) + primary.stats(from, to) } override def close(): Unit = { - runAsync(postgres.close()) - sqlite.close() + runAsync(secondary.close()) + primary.close() } } -case class DualChannelsDb(sqlite: SqliteChannelsDb, postgres: PgChannelsDb) extends ChannelsDb { +case class DualChannelsDb(primary: ChannelsDb, secondary: ChannelsDb) extends ChannelsDb { private implicit val ec: ExecutionContext = ExecutionContext.fromExecutor(Executors.newSingleThreadExecutor(new ThreadFactoryBuilder().setNameFormat("db-channels").build())) override def addOrUpdateChannel(state: HasCommitments): Unit = { - runAsync(postgres.addOrUpdateChannel(state)) - sqlite.addOrUpdateChannel(state) + runAsync(secondary.addOrUpdateChannel(state)) + primary.addOrUpdateChannel(state) } override def getChannel(channelId: ByteVector32): Option[HasCommitments] = { - runAsync(postgres.getChannel(channelId)) - sqlite.getChannel(channelId) + runAsync(secondary.getChannel(channelId)) + primary.getChannel(channelId) } override def updateChannelMeta(channelId: ByteVector32, event: ChannelEvent.EventType): Unit = { - runAsync(postgres.updateChannelMeta(channelId, event)) - sqlite.updateChannelMeta(channelId, event) + runAsync(secondary.updateChannelMeta(channelId, event)) + primary.updateChannelMeta(channelId, event) } override def removeChannel(channelId: ByteVector32): Unit = { - runAsync(postgres.removeChannel(channelId)) - sqlite.removeChannel(channelId) + runAsync(secondary.removeChannel(channelId)) + primary.removeChannel(channelId) } override def listLocalChannels(): Seq[HasCommitments] = { - runAsync(postgres.listLocalChannels()) - sqlite.listLocalChannels() + runAsync(secondary.listLocalChannels()) + primary.listLocalChannels() } override def addHtlcInfo(channelId: ByteVector32, commitmentNumber: Long, paymentHash: ByteVector32, cltvExpiry: CltvExpiry): Unit = { - runAsync(postgres.addHtlcInfo(channelId, commitmentNumber, paymentHash, cltvExpiry)) - sqlite.addHtlcInfo(channelId, commitmentNumber, paymentHash, cltvExpiry) + runAsync(secondary.addHtlcInfo(channelId, commitmentNumber, paymentHash, cltvExpiry)) + primary.addHtlcInfo(channelId, commitmentNumber, paymentHash, cltvExpiry) } override def listHtlcInfos(channelId: ByteVector32, commitmentNumber: Long): Seq[(ByteVector32, CltvExpiry)] = { - runAsync(postgres.listHtlcInfos(channelId, commitmentNumber)) - sqlite.listHtlcInfos(channelId, commitmentNumber) + runAsync(secondary.listHtlcInfos(channelId, commitmentNumber)) + primary.listHtlcInfos(channelId, commitmentNumber) } override def close(): Unit = { - runAsync(postgres.close()) - sqlite.close() + runAsync(secondary.close()) + primary.close() } } -case class DualPeersDb(sqlite: SqlitePeersDb, postgres: PgPeersDb) extends PeersDb { +case class DualPeersDb(primary: PeersDb, secondary: PeersDb) extends PeersDb { private implicit val ec: ExecutionContext = ExecutionContext.fromExecutor(Executors.newSingleThreadExecutor(new ThreadFactoryBuilder().setNameFormat("db-peers").build())) override def addOrUpdatePeer(nodeId: Crypto.PublicKey, address: NodeAddress): Unit = { - runAsync(postgres.addOrUpdatePeer(nodeId, address)) - sqlite.addOrUpdatePeer(nodeId, address) + runAsync(secondary.addOrUpdatePeer(nodeId, address)) + primary.addOrUpdatePeer(nodeId, address) } override def removePeer(nodeId: Crypto.PublicKey): Unit = { - runAsync(postgres.removePeer(nodeId)) - sqlite.removePeer(nodeId) + runAsync(secondary.removePeer(nodeId)) + primary.removePeer(nodeId) } override def getPeer(nodeId: Crypto.PublicKey): Option[NodeAddress] = { - runAsync(postgres.getPeer(nodeId)) - sqlite.getPeer(nodeId) + runAsync(secondary.getPeer(nodeId)) + primary.getPeer(nodeId) } override def listPeers(): Map[Crypto.PublicKey, NodeAddress] = { - runAsync(postgres.listPeers()) - sqlite.listPeers() + runAsync(secondary.listPeers()) + primary.listPeers() } override def addOrUpdateRelayFees(nodeId: Crypto.PublicKey, fees: RelayFees): Unit = { - runAsync(postgres.addOrUpdateRelayFees(nodeId, fees)) - sqlite.addOrUpdateRelayFees(nodeId, fees) + runAsync(secondary.addOrUpdateRelayFees(nodeId, fees)) + primary.addOrUpdateRelayFees(nodeId, fees) } override def getRelayFees(nodeId: Crypto.PublicKey): Option[RelayFees] = { - runAsync(postgres.getRelayFees(nodeId)) - sqlite.getRelayFees(nodeId) + runAsync(secondary.getRelayFees(nodeId)) + primary.getRelayFees(nodeId) } override def close(): Unit = { - runAsync(postgres.close()) - sqlite.close() + runAsync(secondary.close()) + primary.close() } } -case class DualPaymentsDb(sqlite: SqlitePaymentsDb, postgres: PgPaymentsDb) extends PaymentsDb { +case class DualPaymentsDb(primary: PaymentsDb, secondary: PaymentsDb) extends PaymentsDb { private implicit val ec: ExecutionContext = ExecutionContext.fromExecutor(Executors.newSingleThreadExecutor(new ThreadFactoryBuilder().setNameFormat("db-payments").build())) override def listPaymentsOverview(limit: Int): Seq[PlainPayment] = { - runAsync(postgres.listPaymentsOverview(limit)) - sqlite.listPaymentsOverview(limit) + runAsync(secondary.listPaymentsOverview(limit)) + primary.listPaymentsOverview(limit) } override def close(): Unit = { - runAsync(postgres.close()) - sqlite.close() + runAsync(secondary.close()) + primary.close() } override def addIncomingPayment(pr: PaymentRequest, preimage: ByteVector32, paymentType: String): Unit = { - runAsync(postgres.addIncomingPayment(pr, preimage, paymentType)) - sqlite.addIncomingPayment(pr, preimage, paymentType) + runAsync(secondary.addIncomingPayment(pr, preimage, paymentType)) + primary.addIncomingPayment(pr, preimage, paymentType) } override def receiveIncomingPayment(paymentHash: ByteVector32, amount: MilliSatoshi, receivedAt: TimestampMilli): Boolean = { - runAsync(postgres.receiveIncomingPayment(paymentHash, amount, receivedAt)) - sqlite.receiveIncomingPayment(paymentHash, amount, receivedAt) + runAsync(secondary.receiveIncomingPayment(paymentHash, amount, receivedAt)) + primary.receiveIncomingPayment(paymentHash, amount, receivedAt) } override def getIncomingPayment(paymentHash: ByteVector32): Option[IncomingPayment] = { - runAsync(postgres.getIncomingPayment(paymentHash)) - sqlite.getIncomingPayment(paymentHash) + runAsync(secondary.getIncomingPayment(paymentHash)) + primary.getIncomingPayment(paymentHash) } override def removeIncomingPayment(paymentHash: ByteVector32): Try[Unit] = { - runAsync(postgres.removeIncomingPayment(paymentHash)) - sqlite.removeIncomingPayment(paymentHash) + runAsync(secondary.removeIncomingPayment(paymentHash)) + primary.removeIncomingPayment(paymentHash) } override def listIncomingPayments(from: TimestampMilli, to: TimestampMilli): Seq[IncomingPayment] = { - runAsync(postgres.listIncomingPayments(from, to)) - sqlite.listIncomingPayments(from, to) + runAsync(secondary.listIncomingPayments(from, to)) + primary.listIncomingPayments(from, to) } override def listPendingIncomingPayments(from: TimestampMilli, to: TimestampMilli): Seq[IncomingPayment] = { - runAsync(postgres.listPendingIncomingPayments(from, to)) - sqlite.listPendingIncomingPayments(from, to) + runAsync(secondary.listPendingIncomingPayments(from, to)) + primary.listPendingIncomingPayments(from, to) } override def listExpiredIncomingPayments(from: TimestampMilli, to: TimestampMilli): Seq[IncomingPayment] = { - runAsync(postgres.listExpiredIncomingPayments(from, to)) - sqlite.listExpiredIncomingPayments(from, to) + runAsync(secondary.listExpiredIncomingPayments(from, to)) + primary.listExpiredIncomingPayments(from, to) } override def listReceivedIncomingPayments(from: TimestampMilli, to: TimestampMilli): Seq[IncomingPayment] = { - runAsync(postgres.listReceivedIncomingPayments(from, to)) - sqlite.listReceivedIncomingPayments(from, to) + runAsync(secondary.listReceivedIncomingPayments(from, to)) + primary.listReceivedIncomingPayments(from, to) } override def addOutgoingPayment(outgoingPayment: OutgoingPayment): Unit = { - runAsync(postgres.addOutgoingPayment(outgoingPayment)) - sqlite.addOutgoingPayment(outgoingPayment) + runAsync(secondary.addOutgoingPayment(outgoingPayment)) + primary.addOutgoingPayment(outgoingPayment) } override def updateOutgoingPayment(paymentResult: PaymentSent): Unit = { - runAsync(postgres.updateOutgoingPayment(paymentResult)) - sqlite.updateOutgoingPayment(paymentResult) + runAsync(secondary.updateOutgoingPayment(paymentResult)) + primary.updateOutgoingPayment(paymentResult) } override def updateOutgoingPayment(paymentResult: PaymentFailed): Unit = { - runAsync(postgres.updateOutgoingPayment(paymentResult)) - sqlite.updateOutgoingPayment(paymentResult) + runAsync(secondary.updateOutgoingPayment(paymentResult)) + primary.updateOutgoingPayment(paymentResult) } override def getOutgoingPayment(id: UUID): Option[OutgoingPayment] = { - runAsync(postgres.getOutgoingPayment(id)) - sqlite.getOutgoingPayment(id) + runAsync(secondary.getOutgoingPayment(id)) + primary.getOutgoingPayment(id) } override def listOutgoingPayments(parentId: UUID): Seq[OutgoingPayment] = { - runAsync(postgres.listOutgoingPayments(parentId)) - sqlite.listOutgoingPayments(parentId) + runAsync(secondary.listOutgoingPayments(parentId)) + primary.listOutgoingPayments(parentId) } override def listOutgoingPayments(paymentHash: ByteVector32): Seq[OutgoingPayment] = { - runAsync(postgres.listOutgoingPayments(paymentHash)) - sqlite.listOutgoingPayments(paymentHash) + runAsync(secondary.listOutgoingPayments(paymentHash)) + primary.listOutgoingPayments(paymentHash) } override def listOutgoingPayments(from: TimestampMilli, to: TimestampMilli): Seq[OutgoingPayment] = { - runAsync(postgres.listOutgoingPayments(from, to)) - sqlite.listOutgoingPayments(from, to) + runAsync(secondary.listOutgoingPayments(from, to)) + primary.listOutgoingPayments(from, to) } } -case class DualPendingCommandsDb(sqlite: SqlitePendingCommandsDb, postgres: PgPendingCommandsDb) extends PendingCommandsDb { +case class DualPendingCommandsDb(primary: PendingCommandsDb, secondary: PendingCommandsDb) extends PendingCommandsDb { private implicit val ec: ExecutionContext = ExecutionContext.fromExecutor(Executors.newSingleThreadExecutor(new ThreadFactoryBuilder().setNameFormat("db-pending-commands").build())) override def addSettlementCommand(channelId: ByteVector32, cmd: HtlcSettlementCommand): Unit = { - runAsync(postgres.addSettlementCommand(channelId, cmd)) - sqlite.addSettlementCommand(channelId, cmd) + runAsync(secondary.addSettlementCommand(channelId, cmd)) + primary.addSettlementCommand(channelId, cmd) } override def removeSettlementCommand(channelId: ByteVector32, htlcId: Long): Unit = { - runAsync(postgres.removeSettlementCommand(channelId, htlcId)) - sqlite.removeSettlementCommand(channelId, htlcId) + runAsync(secondary.removeSettlementCommand(channelId, htlcId)) + primary.removeSettlementCommand(channelId, htlcId) } override def listSettlementCommands(channelId: ByteVector32): Seq[HtlcSettlementCommand] = { - runAsync(postgres.listSettlementCommands(channelId)) - sqlite.listSettlementCommands(channelId) + runAsync(secondary.listSettlementCommands(channelId)) + primary.listSettlementCommands(channelId) } override def listSettlementCommands(): Seq[(ByteVector32, HtlcSettlementCommand)] = { - runAsync(postgres.listSettlementCommands()) - sqlite.listSettlementCommands() + runAsync(secondary.listSettlementCommands()) + primary.listSettlementCommands() } override def close(): Unit = { - runAsync(postgres.close()) - sqlite.close() + runAsync(secondary.close()) + primary.close() } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/jdbc/JdbcUtils.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/jdbc/JdbcUtils.scala index dc08381680..c6771c856b 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/jdbc/JdbcUtils.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/jdbc/JdbcUtils.scala @@ -153,6 +153,11 @@ trait JdbcUtils { ByteVector.fromValidHex(s) } + def getByteVectorFromHexNullable(columnLabel: String): Option[ByteVector] = { + val s = rs.getString(columnLabel) + if (rs.wasNull()) None else Some(ByteVector.fromValidHex(s)) + } + def getByteVector32FromHex(columnLabel: String): ByteVector32 = { val s = rs.getString(columnLabel) ByteVector32(ByteVector.fromValidHex(s)) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/CompareAuditDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/CompareAuditDb.scala new file mode 100644 index 0000000000..5d532e70a8 --- /dev/null +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/CompareAuditDb.scala @@ -0,0 +1,280 @@ +package fr.acinq.eclair.db.migration + +import fr.acinq.eclair.db.migration.CompareDb._ +import scodec.bits.ByteVector + +import java.sql.{Connection, ResultSet} + +object CompareAuditDb { + + private def compareSentTable(conn1: Connection, conn2: Connection): Boolean = { + val table1 = "sent" + val table2 = "audit.sent" + + def hash1(rs: ResultSet): ByteVector = { + long(rs, "amount_msat") ++ + long(rs, "fees_msat") ++ + long(rs, "recipient_amount_msat") ++ + string(rs, "payment_id") ++ + string(rs, "parent_payment_id") ++ + bytes(rs, "payment_hash") ++ + bytes(rs, "payment_preimage") ++ + bytes(rs, "recipient_node_id") ++ + bytes(rs, "to_channel_id") ++ + longts(rs, "timestamp") + } + + def hash2(rs: ResultSet): ByteVector = { + long(rs, "amount_msat") ++ + long(rs, "fees_msat") ++ + long(rs, "recipient_amount_msat") ++ + string(rs, "payment_id") ++ + string(rs, "parent_payment_id") ++ + hex(rs, "payment_hash") ++ + hex(rs, "payment_preimage") ++ + hex(rs, "recipient_node_id") ++ + hex(rs, "to_channel_id") ++ + ts(rs, "timestamp") + } + + compareTable(conn1, conn2, table1, table2, hash1, hash2) + } + + private def compareReceivedTable(conn1: Connection, conn2: Connection): Boolean = { + val table1 = "received" + val table2 = "audit.received" + + def hash1(rs: ResultSet): ByteVector = { + long(rs, "amount_msat") ++ + bytes(rs, "payment_hash") ++ + bytes(rs, "from_channel_id") ++ + longts(rs, "timestamp") + } + + def hash2(rs: ResultSet): ByteVector = { + long(rs, "amount_msat") ++ + hex(rs, "payment_hash") ++ + hex(rs, "from_channel_id") ++ + ts(rs, "timestamp") + } + + compareTable(conn1, conn2, table1, table2, hash1, hash2) + } + + private def compareRelayedTable(conn1: Connection, conn2: Connection): Boolean = { + val table1 = "relayed" + val table2 = "audit.relayed" + + def hash1(rs: ResultSet): ByteVector = { + bytes(rs, "payment_hash") ++ + long(rs, "amount_msat") ++ + bytes(rs, "channel_id") ++ + string(rs, "direction") ++ + string(rs, "relay_type") ++ + longts(rs, "timestamp") + } + + def hash2(rs: ResultSet): ByteVector = { + hex(rs, "payment_hash") ++ + long(rs, "amount_msat") ++ + hex(rs, "channel_id") ++ + string(rs, "direction") ++ + string(rs, "relay_type") ++ + ts(rs, "timestamp") + } + + compareTable(conn1, conn2, table1, table2, hash1, hash2) + } + + private def compareRelayedTrampolineTable(conn1: Connection, conn2: Connection): Boolean = { + val table1 = "relayed_trampoline" + val table2 = "audit.relayed_trampoline" + + def hash1(rs: ResultSet): ByteVector = { + bytes(rs, "payment_hash") ++ + long(rs, "amount_msat") ++ + bytes(rs, "next_node_id") ++ + longts(rs, "timestamp") + } + + def hash2(rs: ResultSet): ByteVector = { + hex(rs, "payment_hash") ++ + long(rs, "amount_msat") ++ + hex(rs, "next_node_id") ++ + ts(rs, "timestamp") + } + + compareTable(conn1, conn2, table1, table2, hash1, hash2) + } + + private def compareTransactionsPublishedTable(conn1: Connection, conn2: Connection): Boolean = { + val table1 = "transactions_published" + val table2 = "audit.transactions_published" + + def hash1(rs: ResultSet): ByteVector = { + bytes(rs, "tx_id") ++ + bytes(rs, "channel_id") ++ + bytes(rs, "node_id") ++ + long(rs, "mining_fee_sat") ++ + string(rs, "tx_type") ++ + longts(rs, "timestamp") + } + + def hash2(rs: ResultSet): ByteVector = { + hex(rs, "tx_id") ++ + hex(rs, "channel_id") ++ + hex(rs, "node_id") ++ + long(rs, "mining_fee_sat") ++ + string(rs, "tx_type") ++ + ts(rs, "timestamp") + } + + compareTable(conn1, conn2, table1, table2, hash1, hash2) + } + + private def compareTransactionsConfirmedTable(conn1: Connection, conn2: Connection): Boolean = { + val table1 = "transactions_confirmed" + val table2 = "audit.transactions_confirmed" + + def hash1(rs: ResultSet): ByteVector = { + bytes(rs, "tx_id") ++ + bytes(rs, "channel_id") ++ + bytes(rs, "node_id") ++ + longts(rs, "timestamp") + } + + def hash2(rs: ResultSet): ByteVector = { + hex(rs, "tx_id") ++ + hex(rs, "channel_id") ++ + hex(rs, "node_id") ++ + ts(rs, "timestamp") + } + + compareTable(conn1, conn2, table1, table2, hash1, hash2) + } + + private def compareChannelEventsTable(conn1: Connection, conn2: Connection): Boolean = { + val table1 = "channel_events" + val table2 = "audit.channel_events" + + def hash1(rs: ResultSet): ByteVector = { + bytes(rs, "channel_id") ++ + bytes(rs, "node_id") ++ + long(rs, "capacity_sat") ++ + bool(rs, "is_funder") ++ + bool(rs, "is_private") ++ + string(rs, "event") ++ + longts(rs, "timestamp") + } + + def hash2(rs: ResultSet): ByteVector = { + hex(rs, "channel_id") ++ + hex(rs, "node_id") ++ + long(rs, "capacity_sat") ++ + bool(rs, "is_funder") ++ + bool(rs, "is_private") ++ + string(rs, "event") ++ + ts(rs, "timestamp") + } + + compareTable(conn1, conn2, table1, table2, hash1, hash2) + } + + private def compareChannelErrorsTable(conn1: Connection, conn2: Connection): Boolean = { + val table1 = "channel_errors WHERE error_name <> 'CannotAffordFees'" + val table2 = "audit.channel_errors" + + def hash1(rs: ResultSet): ByteVector = { + bytes(rs, "channel_id") ++ + bytes(rs, "node_id") ++ + string(rs, "error_name") ++ + string(rs, "error_message") ++ + bool(rs, "is_fatal") ++ + longts(rs, "timestamp") + } + + def hash2(rs: ResultSet): ByteVector = { + hex(rs, "channel_id") ++ + hex(rs, "node_id") ++ + string(rs, "error_name") ++ + string(rs, "error_message") ++ + bool(rs, "is_fatal") ++ + ts(rs, "timestamp") + } + + compareTable(conn1, conn2, table1, table2, hash1, hash2) + } + + private def compareChannelUpdatesTable(conn1: Connection, conn2: Connection): Boolean = { + val table1 = "channel_updates" + val table2 = "audit.channel_updates" + + def hash1(rs: ResultSet): ByteVector = { + bytes(rs, "channel_id") ++ + bytes(rs, "node_id") ++ + long(rs, "fee_base_msat") ++ + long(rs, "fee_proportional_millionths") ++ + long(rs, "cltv_expiry_delta") ++ + long(rs, "htlc_minimum_msat") ++ + long(rs, "htlc_maximum_msat") ++ + longts(rs, "timestamp") + } + + def hash2(rs: ResultSet): ByteVector = { + hex(rs, "channel_id") ++ + hex(rs, "node_id") ++ + long(rs, "fee_base_msat") ++ + long(rs, "fee_proportional_millionths") ++ + long(rs, "cltv_expiry_delta") ++ + long(rs, "htlc_minimum_msat") ++ + long(rs, "htlc_maximum_msat") ++ + ts(rs, "timestamp") + } + + compareTable(conn1, conn2, table1, table2, hash1, hash2) + } + + private def comparePathFindingMetricsTable(conn1: Connection, conn2: Connection): Boolean = { + val table1 = "path_finding_metrics" + val table2 = "audit.path_finding_metrics" + + def hash1(rs: ResultSet): ByteVector = { + long(rs, "amount_msat") ++ + long(rs, "fees_msat") ++ + string(rs, "status") ++ + long(rs, "duration_ms") ++ + longts(rs, "timestamp") ++ + bool(rs, "is_mpp") ++ + string(rs, "experiment_name") ++ + bytes(rs, "recipient_node_id") + + } + + def hash2(rs: ResultSet): ByteVector = { + long(rs, "amount_msat") ++ + long(rs, "fees_msat") ++ + string(rs, "status") ++ + long(rs, "duration_ms") ++ + ts(rs, "timestamp") ++ + bool(rs, "is_mpp") ++ + string(rs, "experiment_name") ++ + hex(rs, "recipient_node_id") + } + + compareTable(conn1, conn2, table1, table2, hash1, hash2) + } + + def compareAllTables(conn1: Connection, conn2: Connection): Boolean = { + compareSentTable(conn1, conn2) && + compareReceivedTable(conn1, conn2) && + compareRelayedTable(conn1, conn2) && + compareRelayedTrampolineTable(conn1, conn2) && + compareTransactionsPublishedTable(conn1, conn2) && + compareTransactionsConfirmedTable(conn1, conn2) && + compareChannelEventsTable(conn1, conn2) && + compareChannelErrorsTable(conn1, conn2) && + compareChannelUpdatesTable(conn1, conn2) && + comparePathFindingMetricsTable(conn1, conn2) + } + +} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/CompareChannelsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/CompareChannelsDb.scala new file mode 100644 index 0000000000..7f166a4d36 --- /dev/null +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/CompareChannelsDb.scala @@ -0,0 +1,80 @@ +package fr.acinq.eclair.db.migration + +import fr.acinq.eclair.BlockHeight +import fr.acinq.eclair.channel.{DATA_CLOSING, DATA_WAIT_FOR_FUNDING_CONFIRMED} +import fr.acinq.eclair.db.migration.CompareDb._ +import fr.acinq.eclair.wire.internal.channel.ChannelCodecs.stateDataCodec +import scodec.bits.ByteVector + +import java.sql.{Connection, ResultSet} + +object CompareChannelsDb { + + private def compareChannelsTable(conn1: Connection, conn2: Connection): Boolean = { + val table1 = "local_channels" + val table2 = "local.channels" + + def hash1(rs: ResultSet): ByteVector = { + val data = ByteVector(rs.getBytes("data")) + val data_modified = stateDataCodec.decode(data.bits).require.value match { + case c: DATA_WAIT_FOR_FUNDING_CONFIRMED => stateDataCodec.encode(c.copy(waitingSince = BlockHeight(0))).require.toByteVector + case c: DATA_CLOSING => stateDataCodec.encode(c.copy(waitingSince = BlockHeight(0))).require.toByteVector + case _ => data + } + bytes(rs, "channel_id") ++ + data_modified ++ + bool(rs, "is_closed") ++ + longtsnull(rs, "created_timestamp") ++ + longtsnull(rs, "last_payment_sent_timestamp") ++ + longtsnull(rs, "last_payment_received_timestamp") ++ + longtsnull(rs, "last_connected_timestamp") ++ + longtsnull(rs, "closed_timestamp") + } + + def hash2(rs: ResultSet): ByteVector = { + val data = ByteVector(rs.getBytes("data")) + val data_modified = stateDataCodec.decode(data.bits).require.value match { + case c: DATA_WAIT_FOR_FUNDING_CONFIRMED => stateDataCodec.encode(c.copy(waitingSince = BlockHeight(0))).require.toByteVector + case c: DATA_CLOSING => stateDataCodec.encode(c.copy(waitingSince = BlockHeight(0))).require.toByteVector + case _ => data + } + hex(rs, "channel_id") ++ + data_modified ++ + bool(rs, "is_closed") ++ + tsnull(rs, "created_timestamp") ++ + tsnull(rs, "last_payment_sent_timestamp") ++ + tsnull(rs, "last_payment_received_timestamp") ++ + tsnull(rs, "last_connected_timestamp") ++ + tsnull(rs, "closed_timestamp") + } + + compareTable(conn1, conn2, table1, table2, hash1, hash2) + } + + private def compareHtlcInfosTable(conn1: Connection, conn2: Connection): Boolean = { + val table1 = "htlc_infos" + val table2 = "local.htlc_infos" + + def hash1(rs: ResultSet): ByteVector = { + bytes(rs, "channel_id") ++ + long(rs, "commitment_number") ++ + bytes(rs, "payment_hash") ++ + long(rs, "cltv_expiry") + } + + def hash2(rs: ResultSet): ByteVector = { + hex(rs, "channel_id") ++ + long(rs, "commitment_number") ++ + hex(rs, "payment_hash") ++ + long(rs, "cltv_expiry") + } + + compareTable(conn1, conn2, table1, table2, hash1, hash2) + } + + def compareAllTables(conn1: Connection, conn2: Connection): Boolean = { + compareChannelsTable(conn1, conn2) && + compareHtlcInfosTable(conn1, conn2) + } + +} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/CompareDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/CompareDb.scala new file mode 100644 index 0000000000..d5efd6047a --- /dev/null +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/CompareDb.scala @@ -0,0 +1,81 @@ +package fr.acinq.eclair.db.migration + +import fr.acinq.eclair.db.Databases.{PostgresDatabases, SqliteDatabases} +import fr.acinq.eclair.db.DualDatabases +import fr.acinq.eclair.db.pg.PgUtils +import grizzled.slf4j.Logging +import scodec.bits.ByteVector + +import java.sql.{Connection, ResultSet} + +object CompareDb extends Logging { + + def compareTable(conn1: Connection, + conn2: Connection, + table1: String, + table2: String, + hash1: ResultSet => ByteVector, + hash2: ResultSet => ByteVector): Boolean = { + val rs1 = conn1.prepareStatement(s"SELECT * FROM $table1").executeQuery() + val rs2 = conn2.prepareStatement(s"SELECT * FROM $table2").executeQuery() + + var hashes1 = List.empty[ByteVector] + while (rs1.next()) { + hashes1 = hash1(rs1) +: hashes1 + } + + var hashes2 = List.empty[ByteVector] + while (rs2.next()) { + hashes2 = hash2(rs2) +: hashes2 + } + + val res = hashes1.sorted == hashes2.sorted + + if (res) { + logger.info(s"tables $table1/$table2 are identical") + } else { + val diff1 = hashes1 diff hashes2 + val diff2 = hashes2 diff hashes1 + logger.warn(s"tables $table1/$table2 are different diff1=${diff1.take(3).map(_.toHex.take(128))} diff2=${diff2.take(3).map(_.toHex.take(128))}") + } + + res + } + + // @formatter:off + import fr.acinq.eclair.db.jdbc.JdbcUtils.ExtendedResultSet._ + def bytes(rs: ResultSet, columnName: String): ByteVector = rs.getByteVector(columnName) + def bytesnull(rs: ResultSet, columnName: String): ByteVector = rs.getByteVectorNullable(columnName).getOrElse(ByteVector.fromValidHex("deadbeef")) + def hex(rs: ResultSet, columnName: String): ByteVector = rs.getByteVectorFromHex(columnName) + def hexnull(rs: ResultSet, columnName: String): ByteVector = rs.getByteVectorFromHexNullable(columnName).getOrElse(ByteVector.fromValidHex("deadbeef")) + def string(rs: ResultSet, columnName: String): ByteVector = ByteVector(rs.getString(columnName).getBytes) + def stringnull(rs: ResultSet, columnName: String): ByteVector = ByteVector(rs.getStringNullable(columnName).getOrElse("").getBytes) + def bool(rs: ResultSet, columnName: String): ByteVector = ByteVector.fromByte(if (rs.getBoolean(columnName)) 1 else 0) + def long(rs: ResultSet, columnName: String): ByteVector = ByteVector.fromLong(rs.getLong(columnName)) + def longnull(rs: ResultSet, columnName: String): ByteVector = ByteVector.fromLong(rs.getLongNullable(columnName).getOrElse(42)) + def longts(rs: ResultSet, columnName: String): ByteVector = ByteVector.fromLong((rs.getLong(columnName).toDouble / 1_000_000).round) + def longtsnull(rs: ResultSet, columnName: String): ByteVector = ByteVector.fromLong(rs.getLongNullable(columnName).map(l => (l.toDouble/1_000_000).round).getOrElse(42)) + def int(rs: ResultSet, columnName: String): ByteVector = ByteVector.fromInt(rs.getInt(columnName)) + def ts(rs: ResultSet, columnName: String): ByteVector = ByteVector.fromLong((rs.getTimestamp(columnName).getTime.toDouble / 1_000_000).round) + def tsnull(rs: ResultSet, columnName: String): ByteVector = ByteVector.fromLong(rs.getTimestampNullable(columnName).map(t => (t.getTime.toDouble / 1_000_000).round).getOrElse(42)) + def tssec(rs: ResultSet, columnName: String): ByteVector = ByteVector.fromLong((rs.getTimestamp(columnName).toInstant.getEpochSecond.toDouble / 1_000_000).round) + def tssecnull(rs: ResultSet, columnName: String): ByteVector = ByteVector.fromLong(rs.getTimestampNullable(columnName).map(t => (t.toInstant.getEpochSecond.toDouble / 1_000_000).round).getOrElse(42)) + // @formatter:on + + def compareAll(dualDatabases: DualDatabases): Unit = { + logger.info("comparing all tables...") + val (sqliteDb: SqliteDatabases, postgresDb: PostgresDatabases) = DualDatabases.getDatabases(dualDatabases) + PgUtils.inTransaction { postgres => + val result = List( + CompareChannelsDb.compareAllTables(sqliteDb.channels.sqlite, postgres), + ComparePendingCommandsDb.compareAllTables(sqliteDb.pendingCommands.sqlite, postgres), + ComparePeersDb.compareAllTables(sqliteDb.peers.sqlite, postgres), + ComparePaymentsDb.compareAllTables(sqliteDb.payments.sqlite, postgres), + CompareNetworkDb.compareAllTables(sqliteDb.network.sqlite, postgres), + CompareAuditDb.compareAllTables(sqliteDb.audit.sqlite, postgres) + ).forall(_ == true) + logger.info(s"comparison complete identical=$result") + }(postgresDb.dataSource) + } + +} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/CompareNetworkDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/CompareNetworkDb.scala new file mode 100644 index 0000000000..8b91bb31d4 --- /dev/null +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/CompareNetworkDb.scala @@ -0,0 +1,73 @@ +package fr.acinq.eclair.db.migration + +import fr.acinq.eclair.db.migration.CompareDb._ +import scodec.bits.ByteVector + +import java.sql.{Connection, ResultSet} + +object CompareNetworkDb { + + private def compareNodesTable(conn1: Connection, conn2: Connection): Boolean = { + val table1 = "nodes" + val table2 = "network.nodes" + + def hash1(rs: ResultSet): ByteVector = { + bytes(rs, "node_id") ++ + bytes(rs, "data") + } + + def hash2(rs: ResultSet): ByteVector = { + hex(rs, "node_id") ++ + bytes(rs, "data") + } + + compareTable(conn1, conn2, table1, table2, hash1, hash2) + } + + private def compareChannelsTable(conn1: Connection, conn2: Connection): Boolean = { + val table1 = "channels" + val table2 = "network.public_channels" + + def hash1(rs: ResultSet): ByteVector = { + long(rs, "short_channel_id") ++ + string(rs, "txid") ++ + bytes(rs, "channel_announcement") ++ + long(rs, "capacity_sat") ++ + bytesnull(rs, "channel_update_1") ++ + bytesnull(rs, "channel_update_2") + } + + def hash2(rs: ResultSet): ByteVector = { + long(rs, "short_channel_id") ++ + string(rs, "txid") ++ + bytes(rs, "channel_announcement") ++ + long(rs, "capacity_sat") ++ + bytesnull(rs, "channel_update_1") ++ + bytesnull(rs, "channel_update_2") + } + + compareTable(conn1, conn2, table1, table2, hash1, hash2) + } + + private def comparePrunedTable(conn1: Connection, conn2: Connection): Boolean = { + val table1 = "pruned" + val table2 = "network.pruned_channels" + + def hash1(rs: ResultSet): ByteVector = { + long(rs, "short_channel_id") + } + + def hash2(rs: ResultSet): ByteVector = { + long(rs, "short_channel_id") + } + + compareTable(conn1, conn2, table1, table2, hash1, hash2) + } + + def compareAllTables(conn1: Connection, conn2: Connection): Boolean = { + compareNodesTable(conn1, conn2) && + compareChannelsTable(conn1, conn2) && + comparePrunedTable(conn1, conn2) + } + +} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/ComparePaymentsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/ComparePaymentsDb.scala new file mode 100644 index 0000000000..17265931c0 --- /dev/null +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/ComparePaymentsDb.scala @@ -0,0 +1,87 @@ +package fr.acinq.eclair.db.migration + +import fr.acinq.eclair.db.migration.CompareDb._ +import scodec.bits.ByteVector + +import java.sql.{Connection, ResultSet} + +object ComparePaymentsDb { + + private def compareReceivedPaymentsTable(conn1: Connection, conn2: Connection): Boolean = { + val table1 = "received_payments" + val table2 = "payments.received" + + def hash1(rs: ResultSet): ByteVector = { + bytes(rs, "payment_hash") ++ + string(rs, "payment_type") ++ + bytes(rs, "payment_preimage") ++ + string(rs, "payment_request") ++ + longnull(rs, "received_msat") ++ + longts(rs, "created_at") ++ + longts(rs, "expire_at") ++ + longtsnull(rs, "received_at") + } + + def hash2(rs: ResultSet): ByteVector = { + hex(rs, "payment_hash") ++ + string(rs, "payment_type") ++ + hex(rs, "payment_preimage") ++ + string(rs, "payment_request") ++ + longnull(rs, "received_msat") ++ + ts(rs, "created_at") ++ + ts(rs, "expire_at") ++ + tsnull(rs, "received_at") + } + + compareTable(conn1, conn2, table1, table2, hash1, hash2) + } + + private def compareSentPaymentsTable(conn1: Connection, conn2: Connection): Boolean = { + val table1 = "sent_payments" + val table2 = "payments.sent" + + def hash1(rs: ResultSet): ByteVector = { + string(rs, "id") ++ + string(rs, "parent_id") ++ + stringnull(rs, "external_id") ++ + bytes(rs, "payment_hash") ++ + bytesnull(rs, "payment_preimage") ++ + string(rs, "payment_type") ++ + long(rs, "amount_msat") ++ + longnull(rs, "fees_msat") ++ + long(rs, "recipient_amount_msat") ++ + bytes(rs, "recipient_node_id") ++ + stringnull(rs, "payment_request") ++ + bytesnull(rs, "payment_route") ++ + bytesnull(rs, "failures") ++ + longts(rs, "created_at") ++ + longtsnull(rs, "completed_at") + } + + def hash2(rs: ResultSet): ByteVector = { + string(rs, "id") ++ + string(rs, "parent_id") ++ + stringnull(rs, "external_id") ++ + hex(rs, "payment_hash") ++ + hexnull(rs, "payment_preimage") ++ + string(rs, "payment_type") ++ + long(rs, "amount_msat") ++ + longnull(rs, "fees_msat") ++ + long(rs, "recipient_amount_msat") ++ + hex(rs, "recipient_node_id") ++ + stringnull(rs, "payment_request") ++ + bytesnull(rs, "payment_route") ++ + bytesnull(rs, "failures") ++ + ts(rs, "created_at") ++ + tsnull(rs, "completed_at") + } + + compareTable(conn1, conn2, table1, table2, hash1, hash2) + } + + def compareAllTables(conn1: Connection, conn2: Connection): Boolean = { + compareReceivedPaymentsTable(conn1, conn2) && + compareSentPaymentsTable(conn1, conn2) + } + +} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/ComparePeersDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/ComparePeersDb.scala new file mode 100644 index 0000000000..c0c034fa97 --- /dev/null +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/ComparePeersDb.scala @@ -0,0 +1,51 @@ +package fr.acinq.eclair.db.migration + +import fr.acinq.eclair.db.migration.CompareDb._ +import scodec.bits.ByteVector + +import java.sql.{Connection, ResultSet} + +object ComparePeersDb { + + private def comparePeersTable(conn1: Connection, conn2: Connection): Boolean = { + val table1 = "peers" + val table2 = "local.peers" + + def hash1(rs: ResultSet): ByteVector = { + bytes(rs, "node_id") ++ + bytes(rs, "data") + } + + def hash2(rs: ResultSet): ByteVector = { + hex(rs, "node_id") ++ + bytes(rs, "data") + } + + compareTable(conn1, conn2, table1, table2, hash1, hash2) + } + + private def compareRelayFeesTable(conn1: Connection, conn2: Connection): Boolean = { + val table1 = "relay_fees" + val table2 = "local.relay_fees" + + def hash1(rs: ResultSet): ByteVector = { + bytes(rs, "node_id") ++ + long(rs, "fee_base_msat") ++ + long(rs, "fee_proportional_millionths") + } + + def hash2(rs: ResultSet): ByteVector = { + hex(rs, "node_id") ++ + long(rs, "fee_base_msat") ++ + long(rs, "fee_proportional_millionths") + } + + compareTable(conn1, conn2, table1, table2, hash1, hash2) + } + + def compareAllTables(conn1: Connection, conn2: Connection): Boolean = { + comparePeersTable(conn1, conn2) && + compareRelayFeesTable(conn1, conn2) + } + +} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/ComparePendingCommandsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/ComparePendingCommandsDb.scala new file mode 100644 index 0000000000..eb099ceea4 --- /dev/null +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/ComparePendingCommandsDb.scala @@ -0,0 +1,33 @@ +package fr.acinq.eclair.db.migration + +import fr.acinq.eclair.db.migration.CompareDb._ +import scodec.bits.ByteVector + +import java.sql.{Connection, ResultSet} + +object ComparePendingCommandsDb { + + private def comparePendingSettlementCommandsTable(conn1: Connection, conn2: Connection): Boolean = { + val table1 = "pending_settlement_commands" + val table2 = "local.pending_settlement_commands" + + def hash1(rs: ResultSet): ByteVector = { + bytes(rs, "channel_id") ++ + long(rs, "htlc_id") ++ + bytes(rs, "data") + } + + def hash2(rs: ResultSet): ByteVector = { + hex(rs, "channel_id") ++ + long(rs, "htlc_id") ++ + bytes(rs, "data") + } + + compareTable(conn1, conn2, table1, table2, hash1, hash2) + } + + def compareAllTables(conn1: Connection, conn2: Connection): Boolean = { + comparePendingSettlementCommandsTable(conn1, conn2) + } + +} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/MigrateAuditDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/MigrateAuditDb.scala new file mode 100644 index 0000000000..28705cd272 --- /dev/null +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/MigrateAuditDb.scala @@ -0,0 +1,188 @@ +package fr.acinq.eclair.db.migration + +import fr.acinq.eclair.db.jdbc.JdbcUtils.ExtendedResultSet._ +import fr.acinq.eclair.db.migration.MigrateDb.{checkVersions, migrateTable} + +import java.sql.{Connection, PreparedStatement, ResultSet, Timestamp} +import java.time.Instant + +object MigrateAuditDb { + + private def migrateSentTable(source: Connection, destination: Connection): Int = { + val sourceTable = "sent" + val insertSql = "INSERT INTO audit.sent (amount_msat, fees_msat, recipient_amount_msat, payment_id, parent_payment_id, payment_hash, payment_preimage, recipient_node_id, to_channel_id, timestamp) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)" + + def migrate(rs: ResultSet, insertStatement: PreparedStatement): Unit = { + insertStatement.setLong(1, rs.getLong("amount_msat")) + insertStatement.setLong(2, rs.getLong("fees_msat")) + insertStatement.setLong(3, rs.getLong("recipient_amount_msat")) + insertStatement.setString(4, rs.getString("payment_id")) + insertStatement.setString(5, rs.getString("parent_payment_id")) + insertStatement.setString(6, rs.getByteVector32("payment_hash").toHex) + insertStatement.setString(7, rs.getByteVector32("payment_preimage").toHex) + insertStatement.setString(8, rs.getByteVector("recipient_node_id").toHex) + insertStatement.setString(9, rs.getByteVector32("to_channel_id").toHex) + insertStatement.setTimestamp(10, Timestamp.from(Instant.ofEpochMilli(rs.getLong("timestamp")))) + } + + migrateTable(source, destination, sourceTable, insertSql, migrate) + } + + private def migrateReceivedTable(source: Connection, destination: Connection): Int = { + val sourceTable = "received" + val insertSql = "INSERT INTO audit.received (amount_msat, payment_hash, from_channel_id, timestamp) VALUES (?, ?, ?, ?)" + + def migrate(rs: ResultSet, insertStatement: PreparedStatement): Unit = { + insertStatement.setLong(1, rs.getLong("amount_msat")) + insertStatement.setString(2, rs.getByteVector32("payment_hash").toHex) + insertStatement.setString(3, rs.getByteVector32("from_channel_id").toHex) + insertStatement.setTimestamp(4, Timestamp.from(Instant.ofEpochMilli(rs.getLong("timestamp")))) + } + + migrateTable(source, destination, sourceTable, insertSql, migrate) + } + + private def migrateRelayedTable(source: Connection, destination: Connection): Int = { + val sourceTable = "relayed" + val insertSql = "INSERT INTO audit.relayed (payment_hash, amount_msat, channel_id, direction, relay_type, timestamp) VALUES (?, ?, ?, ?, ?, ?)" + + def migrate(rs: ResultSet, insertStatement: PreparedStatement): Unit = { + insertStatement.setString(1, rs.getByteVector32("payment_hash").toHex) + insertStatement.setLong(2, rs.getLong("amount_msat")) + insertStatement.setString(3, rs.getByteVector32("channel_id").toHex) + insertStatement.setString(4, rs.getString("direction")) + insertStatement.setString(5, rs.getString("relay_type")) + insertStatement.setTimestamp(6, Timestamp.from(Instant.ofEpochMilli(rs.getLong("timestamp")))) + } + + migrateTable(source, destination, sourceTable, insertSql, migrate) + } + + private def migrateRelayedTrampolineTable(source: Connection, destination: Connection): Int = { + val sourceTable = "relayed_trampoline" + val insertSql = "INSERT INTO audit.relayed_trampoline (payment_hash, amount_msat, next_node_id, timestamp) VALUES (?, ?, ?, ?)" + + def migrate(rs: ResultSet, insertStatement: PreparedStatement): Unit = { + insertStatement.setString(1, rs.getByteVector32("payment_hash").toHex) + insertStatement.setLong(2, rs.getLong("amount_msat")) + insertStatement.setString(3, rs.getByteVector("next_node_id").toHex) + insertStatement.setTimestamp(4, Timestamp.from(Instant.ofEpochMilli(rs.getLong("timestamp")))) + } + + migrateTable(source, destination, sourceTable, insertSql, migrate) + } + + private def migrateTransactionsPublishedTable(source: Connection, destination: Connection): Int = { + val sourceTable = "transactions_published" + val insertSql = "INSERT INTO audit.transactions_published (tx_id, channel_id, node_id, mining_fee_sat, tx_type, timestamp) VALUES (?, ?, ?, ?, ?, ?)" + + def migrate(rs: ResultSet, insertStatement: PreparedStatement): Unit = { + insertStatement.setString(1, rs.getByteVector32("tx_id").toHex) + insertStatement.setString(2, rs.getByteVector32("channel_id").toHex) + insertStatement.setString(3, rs.getByteVector("node_id").toHex) + insertStatement.setLong(4, rs.getLong("mining_fee_sat")) + insertStatement.setString(5, rs.getString("tx_type")) + insertStatement.setTimestamp(6, Timestamp.from(Instant.ofEpochMilli(rs.getLong("timestamp")))) + } + + migrateTable(source, destination, sourceTable, insertSql, migrate) + } + + private def migrateTransactionsConfirmedTable(source: Connection, destination: Connection): Int = { + val sourceTable = "transactions_confirmed" + val insertSql = "INSERT INTO audit.transactions_confirmed (tx_id, channel_id, node_id, timestamp) VALUES (?, ?, ?, ?)" + + def migrate(rs: ResultSet, insertStatement: PreparedStatement): Unit = { + insertStatement.setString(1, rs.getByteVector32("tx_id").toHex) + insertStatement.setString(2, rs.getByteVector32("channel_id").toHex) + insertStatement.setString(3, rs.getByteVector("node_id").toHex) + insertStatement.setTimestamp(4, Timestamp.from(Instant.ofEpochMilli(rs.getLong("timestamp")))) + } + + migrateTable(source, destination, sourceTable, insertSql, migrate) + } + + private def migrateChannelEventsTable(source: Connection, destination: Connection): Int = { + val sourceTable = "channel_events" + val insertSql = "INSERT INTO audit.channel_events (channel_id, node_id, capacity_sat, is_funder, is_private, event, timestamp) VALUES (?, ?, ?, ?, ?, ?, ?)" + + def migrate(rs: ResultSet, insertStatement: PreparedStatement): Unit = { + insertStatement.setString(1, rs.getByteVector32("channel_id").toHex) + insertStatement.setString(2, rs.getByteVector("node_id").toHex) + insertStatement.setLong(3, rs.getLong("capacity_sat")) + insertStatement.setBoolean(4, rs.getBoolean("is_funder")) + insertStatement.setBoolean(5, rs.getBoolean("is_private")) + insertStatement.setString(6, rs.getString("event")) + insertStatement.setTimestamp(7, Timestamp.from(Instant.ofEpochMilli(rs.getLong("timestamp")))) + } + + migrateTable(source, destination, sourceTable, insertSql, migrate) + } + + private def migrateChannelErrorsTable(source: Connection, destination: Connection): Int = { + val sourceTable = "channel_errors WHERE error_name <> 'CannotAffordFees'" + val insertSql = "INSERT INTO audit.channel_errors (channel_id, node_id, error_name, error_message, is_fatal, timestamp) VALUES (?, ?, ?, ?, ?, ?)" + + def migrate(rs: ResultSet, insertStatement: PreparedStatement): Unit = { + insertStatement.setString(1, rs.getByteVector32("channel_id").toHex) + insertStatement.setString(2, rs.getByteVector("node_id").toHex) + insertStatement.setString(3, rs.getString("error_name")) + insertStatement.setString(4, rs.getString("error_message")) + insertStatement.setBoolean(5, rs.getBoolean("is_fatal")) + insertStatement.setTimestamp(6, Timestamp.from(Instant.ofEpochMilli(rs.getLong("timestamp")))) + } + + migrateTable(source, destination, sourceTable, insertSql, migrate) + } + + private def migrateChannelUpdatesTable(source: Connection, destination: Connection): Int = { + val sourceTable = "channel_updates" + val insertSql = "INSERT INTO audit.channel_updates (channel_id, node_id, fee_base_msat, fee_proportional_millionths, cltv_expiry_delta, htlc_minimum_msat, htlc_maximum_msat, timestamp) VALUES (?, ?, ?, ?, ?, ?, ?, ?)" + + def migrate(rs: ResultSet, insertStatement: PreparedStatement): Unit = { + insertStatement.setString(1, rs.getByteVector32("channel_id").toHex) + insertStatement.setString(2, rs.getByteVector("node_id").toHex) + insertStatement.setLong(3, rs.getLong("fee_base_msat")) + insertStatement.setLong(4, rs.getLong("fee_proportional_millionths")) + insertStatement.setLong(5, rs.getLong("cltv_expiry_delta")) + insertStatement.setLong(6, rs.getLong("htlc_minimum_msat")) + insertStatement.setLong(7, rs.getLong("htlc_maximum_msat")) + insertStatement.setTimestamp(8, Timestamp.from(Instant.ofEpochMilli(rs.getLong("timestamp")))) + } + + migrateTable(source, destination, sourceTable, insertSql, migrate) + } + + private def migratePathFindingMetricsTable(source: Connection, destination: Connection): Int = { + val sourceTable = "path_finding_metrics" + val insertSql = "INSERT INTO audit.path_finding_metrics (amount_msat, fees_msat, status, duration_ms, timestamp, is_mpp, experiment_name, recipient_node_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?)" + + def migrate(rs: ResultSet, insertStatement: PreparedStatement): Unit = { + insertStatement.setLong(1, rs.getLong("amount_msat")) + insertStatement.setLong(2, rs.getLong("fees_msat")) + insertStatement.setString(3, rs.getString("status")) + insertStatement.setLong(4, rs.getLong("duration_ms")) + insertStatement.setTimestamp(5, Timestamp.from(Instant.ofEpochMilli(rs.getLong("timestamp")))) + insertStatement.setBoolean(6, rs.getBoolean("is_mpp")) + insertStatement.setString(7, rs.getString("experiment_name")) + insertStatement.setString(8, rs.getByteVector("recipient_node_id").toHex) + } + + migrateTable(source, destination, sourceTable, insertSql, migrate) + } + + def migrateAllTables(source: Connection, destination: Connection): Unit = { + checkVersions(source, destination, "audit", 8, 10) + migrateSentTable(source, destination) + migrateReceivedTable(source, destination) + migrateRelayedTable(source, destination) + migrateRelayedTrampolineTable(source, destination) + migrateTransactionsPublishedTable(source, destination) + migrateTransactionsConfirmedTable(source, destination) + migrateChannelEventsTable(source, destination) + migrateChannelErrorsTable(source, destination) + migrateChannelUpdatesTable(source, destination) + migratePathFindingMetricsTable(source, destination) + } + +} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/MigrateChannelsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/MigrateChannelsDb.scala new file mode 100644 index 0000000000..0758b1efe5 --- /dev/null +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/MigrateChannelsDb.scala @@ -0,0 +1,56 @@ +package fr.acinq.eclair.db.migration + +import fr.acinq.eclair.db.jdbc.JdbcUtils.ExtendedResultSet._ +import fr.acinq.eclair.db.migration.MigrateDb.{checkVersions, migrateTable} +import fr.acinq.eclair.wire.internal.channel.ChannelCodecs.stateDataCodec +import scodec.bits.BitVector + +import java.sql.{Connection, PreparedStatement, ResultSet, Timestamp} +import java.time.Instant + +object MigrateChannelsDb { + + private def migrateChannelsTable(source: Connection, destination: Connection): Int = { + val sourceTable = "local_channels" + val insertSql = "INSERT INTO local.channels (channel_id, data, json, is_closed, created_timestamp, last_payment_sent_timestamp, last_payment_received_timestamp, last_connected_timestamp, closed_timestamp) VALUES (?, ?, ?::JSONB, ?, ?, ?, ?, ?, ?)" + + import fr.acinq.eclair.json.JsonSerializers._ + + def migrate(rs: ResultSet, insertStatement: PreparedStatement): Unit = { + insertStatement.setString(1, rs.getByteVector32("channel_id").toHex) + insertStatement.setBytes(2, rs.getBytes("data")) + val state = stateDataCodec.decode(BitVector(rs.getBytes("data"))).require.value + val json = serialization.writePretty(state) + insertStatement.setString(3, json) + insertStatement.setBoolean(4, rs.getBoolean("is_closed")) + insertStatement.setTimestamp(5, rs.getLongNullable("created_timestamp").map(l => Timestamp.from(Instant.ofEpochMilli(l))).orNull) + insertStatement.setTimestamp(6, rs.getLongNullable("last_payment_sent_timestamp").map(l => Timestamp.from(Instant.ofEpochMilli(l))).orNull) + insertStatement.setTimestamp(7, rs.getLongNullable("last_payment_received_timestamp").map(l => Timestamp.from(Instant.ofEpochMilli(l))).orNull) + insertStatement.setTimestamp(8, rs.getLongNullable("last_connected_timestamp").map(l => Timestamp.from(Instant.ofEpochMilli(l))).orNull) + insertStatement.setTimestamp(9, rs.getLongNullable("closed_timestamp").map(l => Timestamp.from(Instant.ofEpochMilli(l))).orNull) + } + + migrateTable(source, destination, sourceTable, insertSql, migrate) + } + + private def migrateHtlcInfos(source: Connection, destination: Connection): Int = { + val sourceTable = "htlc_infos" + val insertSql = "INSERT INTO local.htlc_infos (channel_id, commitment_number, payment_hash, cltv_expiry) VALUES (?, ?, ?, ?)" + + def migrate(rs: ResultSet, insertStatement: PreparedStatement): Unit = { + insertStatement.setString(1, rs.getByteVector32("channel_id").toHex) + insertStatement.setLong(2, rs.getLong("commitment_number")) + insertStatement.setString(3, rs.getByteVector32("payment_hash").toHex) + insertStatement.setLong(4, rs.getLong("cltv_expiry")) + } + + migrateTable(source, destination, sourceTable, insertSql, migrate) + } + + def migrateAllTables(source: Connection, destination: Connection): Unit = { + checkVersions(source, destination, "channels", 4, 7) + migrateChannelsTable(source, destination) + migrateHtlcInfos(source, destination) + } + +} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/MigrateDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/MigrateDb.scala new file mode 100644 index 0000000000..331e62b1da --- /dev/null +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/MigrateDb.scala @@ -0,0 +1,53 @@ +package fr.acinq.eclair.db.migration + +import fr.acinq.eclair.db.Databases.{PostgresDatabases, SqliteDatabases} +import fr.acinq.eclair.db.DualDatabases +import fr.acinq.eclair.db.jdbc.JdbcUtils +import fr.acinq.eclair.db.pg.PgUtils +import grizzled.slf4j.Logging + +import java.sql.{Connection, PreparedStatement, ResultSet} + +object MigrateDb extends Logging { + + private def getVersion(conn: Connection, + dbName: String): Int = { + val statement = conn.prepareStatement(s"SELECT version FROM versions WHERE db_name='$dbName'") + val res = statement.executeQuery() + res.next() + res.getInt("version") + } + + def checkVersions(source: Connection, + destination: Connection, + dbName: String, + expectedSourceVersion: Int, + expectedDestinationVersion: Int): Unit = { + val actualSourceVersion = getVersion(source, dbName) + val actualDestinationVersion = getVersion(destination, dbName) + require(actualSourceVersion == expectedSourceVersion, s"unexpected version for source db=$dbName expected=$expectedSourceVersion actual=$actualSourceVersion") + require(actualDestinationVersion == expectedDestinationVersion, s"unexpected version for destination db=$dbName expected=$expectedDestinationVersion actual=$actualDestinationVersion") + } + + def migrateTable(source: Connection, + destination: Connection, + sourceTable: String, + insertSql: String, + migrate: (ResultSet, PreparedStatement) => Unit): Int = + JdbcUtils.migrateTable(source, destination, sourceTable, insertSql, migrate)(logger) + + def migrateAll(dualDatabases: DualDatabases): Unit = { + logger.info("migrating all tables...") + val (sqliteDb: SqliteDatabases, postgresDb: PostgresDatabases) = DualDatabases.getDatabases(dualDatabases) + PgUtils.inTransaction { postgres => + MigrateChannelsDb.migrateAllTables(sqliteDb.channels.sqlite, postgres) + MigratePendingCommandsDb.migrateAllTables(sqliteDb.pendingCommands.sqlite, postgres) + MigratePeersDb.migrateAllTables(sqliteDb.peers.sqlite, postgres) + MigratePaymentsDb.migrateAllTables(sqliteDb.payments.sqlite, postgres) + MigrateNetworkDb.migrateAllTables(sqliteDb.network.sqlite, postgres) + MigrateAuditDb.migrateAllTables(sqliteDb.audit.sqlite, postgres) + logger.info("migration complete") + }(postgresDb.dataSource) + } + +} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/MigrateNetworkDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/MigrateNetworkDb.scala new file mode 100644 index 0000000000..807e97f185 --- /dev/null +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/MigrateNetworkDb.scala @@ -0,0 +1,74 @@ +package fr.acinq.eclair.db.migration + +import fr.acinq.eclair.db.jdbc.JdbcUtils.ExtendedResultSet._ +import fr.acinq.eclair.db.migration.MigrateDb.{checkVersions, migrateTable} +import fr.acinq.eclair.wire.protocol.LightningMessageCodecs.{channelAnnouncementCodec, channelUpdateCodec, nodeAnnouncementCodec} +import scodec.bits.BitVector + +import java.sql.{Connection, PreparedStatement, ResultSet} + +object MigrateNetworkDb { + + private def migrateNodesTable(source: Connection, destination: Connection): Int = { + val sourceTable = "nodes" + val insertSql = "INSERT INTO network.nodes (node_id, data, json) VALUES (?, ?, ?::JSONB)" + + import fr.acinq.eclair.json.JsonSerializers._ + + def migrate(rs: ResultSet, insertStatement: PreparedStatement): Unit = { + insertStatement.setString(1, rs.getByteVector("node_id").toHex) + insertStatement.setBytes(2, rs.getBytes("data")) + val state = nodeAnnouncementCodec.decode(BitVector(rs.getBytes("data"))).require.value + val json = serialization.writePretty(state) + insertStatement.setString(3, json) + } + + migrateTable(source, destination, sourceTable, insertSql, migrate) + } + + private def migrateChannelsTable(source: Connection, destination: Connection): Int = { + val sourceTable = "channels" + val insertSql = "INSERT INTO network.public_channels (short_channel_id, txid, channel_announcement, capacity_sat, channel_update_1, channel_update_2, channel_announcement_json, channel_update_1_json, channel_update_2_json) VALUES (?, ?, ?, ?, ?, ?, ?::JSONB, ?::JSONB, ?::JSONB)" + + import fr.acinq.eclair.json.JsonSerializers._ + + def migrate(rs: ResultSet, insertStatement: PreparedStatement): Unit = { + insertStatement.setLong(1, rs.getLong("short_channel_id")) + insertStatement.setString(2, rs.getString("txid")) + insertStatement.setBytes(3, rs.getBytes("channel_announcement")) + insertStatement.setLong(4, rs.getLong("capacity_sat")) + insertStatement.setBytes(5, rs.getBytes("channel_update_1")) + insertStatement.setBytes(6, rs.getBytes("channel_update_2")) + val ann = channelAnnouncementCodec.decode(rs.getBitVectorOpt("channel_announcement").get).require.value + val channel_update_1_opt = rs.getBitVectorOpt("channel_update_1").map(channelUpdateCodec.decode(_).require.value) + val channel_update_2_opt = rs.getBitVectorOpt("channel_update_2").map(channelUpdateCodec.decode(_).require.value) + val json = serialization.writePretty(ann) + val u1_json = channel_update_1_opt.map(serialization.writePretty(_)).orNull + val u2_json = channel_update_2_opt.map(serialization.writePretty(_)).orNull + insertStatement.setString(7, json) + insertStatement.setString(8, u1_json) + insertStatement.setString(9, u2_json) + } + + migrateTable(source, destination, sourceTable, insertSql, migrate) + } + + private def migratePrunedTable(source: Connection, destination: Connection): Int = { + val sourceTable = "pruned" + val insertSql = "INSERT INTO network.pruned_channels (short_channel_id) VALUES (?)" + + def migrate(rs: ResultSet, insertStatement: PreparedStatement): Unit = { + insertStatement.setLong(1, rs.getLong("short_channel_id")) + } + + migrateTable(source, destination, sourceTable, insertSql, migrate) + } + + def migrateAllTables(source: Connection, destination: Connection): Unit = { + checkVersions(source, destination, "network", 2, 4) + migrateNodesTable(source, destination) + migrateChannelsTable(source, destination) + migratePrunedTable(source, destination) + } + +} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/MigratePaymentsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/MigratePaymentsDb.scala new file mode 100644 index 0000000000..ac2e885bf3 --- /dev/null +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/MigratePaymentsDb.scala @@ -0,0 +1,60 @@ +package fr.acinq.eclair.db.migration + +import fr.acinq.eclair.db.jdbc.JdbcUtils.ExtendedResultSet._ +import fr.acinq.eclair.db.migration.MigrateDb.{checkVersions, migrateTable} + +import java.sql.{Connection, PreparedStatement, ResultSet, Timestamp} +import java.time.Instant + +object MigratePaymentsDb { + + private def migrateReceivedPaymentsTable(source: Connection, destination: Connection): Int = { + val sourceTable = "received_payments" + val insertSql = "INSERT INTO payments.received (payment_hash, payment_type, payment_preimage, payment_request, received_msat, created_at, expire_at, received_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)" + + def migrate(rs: ResultSet, insertStatement: PreparedStatement): Unit = { + insertStatement.setString(1, rs.getByteVector("payment_hash").toHex) + insertStatement.setString(2, rs.getString("payment_type")) + insertStatement.setString(3, rs.getByteVector("payment_preimage").toHex) + insertStatement.setString(4, rs.getString("payment_request")) + insertStatement.setObject(5, rs.getLongNullable("received_msat").orNull) + insertStatement.setTimestamp(6, Timestamp.from(Instant.ofEpochMilli(rs.getLong("created_at")))) + insertStatement.setTimestamp(7, Timestamp.from(Instant.ofEpochMilli(rs.getLong("expire_at")))) + insertStatement.setObject(8, rs.getLongNullable("received_at").map(l => Timestamp.from(Instant.ofEpochMilli(l))).orNull) + } + + migrateTable(source, destination, sourceTable, insertSql, migrate) + } + + private def migrateSentPaymentsTable(source: Connection, destination: Connection): Int = { + val sourceTable = "sent_payments" + val insertSql = "INSERT INTO payments.sent (id, parent_id, external_id, payment_hash, payment_preimage, payment_type, amount_msat, fees_msat, recipient_amount_msat, recipient_node_id, payment_request, payment_route, failures, created_at, completed_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)" + + def migrate(rs: ResultSet, insertStatement: PreparedStatement): Unit = { + insertStatement.setString(1, rs.getString("id")) + insertStatement.setString(2, rs.getString("parent_id")) + insertStatement.setString(3, rs.getStringNullable("external_id").orNull) + insertStatement.setString(4, rs.getByteVector("payment_hash").toHex) + insertStatement.setString(5, rs.getByteVector32Nullable("payment_preimage").map(_.toHex).orNull) + insertStatement.setString(6, rs.getString("payment_type")) + insertStatement.setLong(7, rs.getLong("amount_msat")) + insertStatement.setObject(8, rs.getLongNullable("fees_msat").orNull) + insertStatement.setLong(9, rs.getLong("recipient_amount_msat")) + insertStatement.setString(10, rs.getByteVector("recipient_node_id").toHex) + insertStatement.setString(11, rs.getStringNullable("payment_request").orNull) + insertStatement.setBytes(12, rs.getBytes("payment_route")) + insertStatement.setBytes(13, rs.getBytes("failures")) + insertStatement.setTimestamp(14, Timestamp.from(Instant.ofEpochMilli(rs.getLong("created_at")))) + insertStatement.setObject(15, rs.getLongNullable("completed_at").map(l => Timestamp.from(Instant.ofEpochMilli(l))).orNull) + } + + migrateTable(source, destination, sourceTable, insertSql, migrate) + } + + def migrateAllTables(source: Connection, destination: Connection): Unit = { + checkVersions(source, destination, "payments", 4, 6) + migrateReceivedPaymentsTable(source, destination) + migrateSentPaymentsTable(source, destination) + } + +} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/MigratePeersDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/MigratePeersDb.scala new file mode 100644 index 0000000000..77021f3a3d --- /dev/null +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/MigratePeersDb.scala @@ -0,0 +1,41 @@ +package fr.acinq.eclair.db.migration + +import fr.acinq.eclair.db.jdbc.JdbcUtils.ExtendedResultSet._ +import fr.acinq.eclair.db.migration.MigrateDb.{checkVersions, migrateTable} + +import java.sql.{Connection, PreparedStatement, ResultSet} + +object MigratePeersDb { + + private def migratePeersTable(source: Connection, destination: Connection): Int = { + val sourceTable = "peers" + val insertSql = "INSERT INTO local.peers (node_id, data) VALUES (?, ?)" + + def migrate(rs: ResultSet, insertStatement: PreparedStatement): Unit = { + insertStatement.setString(1, rs.getByteVector("node_id").toHex) + insertStatement.setBytes(2, rs.getBytes("data")) + } + + migrateTable(source, destination, sourceTable, insertSql, migrate) + } + + private def migrateRelayFeesTable(source: Connection, destination: Connection): Int = { + val sourceTable = "relay_fees" + val insertSql = "INSERT INTO local.relay_fees (node_id, fee_base_msat, fee_proportional_millionths) VALUES (?, ?, ?)" + + def migrate(rs: ResultSet, insertStatement: PreparedStatement): Unit = { + insertStatement.setString(1, rs.getByteVector("node_id").toHex) + insertStatement.setLong(2, rs.getLong("fee_base_msat")) + insertStatement.setLong(3, rs.getLong("fee_proportional_millionths")) + } + + migrateTable(source, destination, sourceTable, insertSql, migrate) + } + + def migrateAllTables(source: Connection, destination: Connection): Unit = { + checkVersions(source, destination, "peers", 2, 3) + migratePeersTable(source, destination) + migrateRelayFeesTable(source, destination) + } + +} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/MigratePendingCommandsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/MigratePendingCommandsDb.scala new file mode 100644 index 0000000000..6a01208766 --- /dev/null +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/MigratePendingCommandsDb.scala @@ -0,0 +1,28 @@ +package fr.acinq.eclair.db.migration + +import fr.acinq.eclair.db.jdbc.JdbcUtils.ExtendedResultSet._ +import fr.acinq.eclair.db.migration.MigrateDb.{checkVersions, migrateTable} + +import java.sql.{Connection, PreparedStatement, ResultSet} + +object MigratePendingCommandsDb { + + private def migratePendingSettlementCommandsTable(source: Connection, destination: Connection): Int = { + val sourceTable = "pending_settlement_commands" + val insertSql = "INSERT INTO local.pending_settlement_commands (channel_id, htlc_id, data) VALUES (?, ?, ?)" + + def migrate(rs: ResultSet, insertStatement: PreparedStatement): Unit = { + insertStatement.setString(1, rs.getByteVector("channel_id").toHex) + insertStatement.setLong(2, rs.getLong("htlc_id")) + insertStatement.setBytes(3, rs.getBytes("data")) + } + + migrateTable(source, destination, sourceTable, insertSql, migrate) + } + + def migrateAllTables(source: Connection, destination: Connection): Unit = { + checkVersions(source, destination, "pending_relay", 2, 3) + migratePendingSettlementCommandsTable(source, destination) + } + +} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteAuditDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteAuditDb.scala index 74c01a145e..ed2f403241 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteAuditDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteAuditDb.scala @@ -37,7 +37,7 @@ object SqliteAuditDb { val CURRENT_VERSION = 8 } -class SqliteAuditDb(sqlite: Connection) extends AuditDb with Logging { +class SqliteAuditDb(val sqlite: Connection) extends AuditDb with Logging { import SqliteUtils._ import ExtendedResultSet._ diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteNetworkDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteNetworkDb.scala index a376a4295e..3da17af57e 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteNetworkDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteNetworkDb.scala @@ -34,7 +34,7 @@ object SqliteNetworkDb { val DB_NAME = "network" } -class SqliteNetworkDb(sqlite: Connection) extends NetworkDb with Logging { +class SqliteNetworkDb(val sqlite: Connection) extends NetworkDb with Logging { import SqliteNetworkDb._ import SqliteUtils.ExtendedResultSet._ diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala index 2dce58d6e1..fbaef7851a 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala @@ -33,7 +33,7 @@ import java.util.UUID import scala.concurrent.duration._ import scala.util.{Failure, Success, Try} -class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging { +class SqlitePaymentsDb(val sqlite: Connection) extends PaymentsDb with Logging { import SqlitePaymentsDb._ import SqliteUtils.ExtendedResultSet._ diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePeersDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePeersDb.scala index 7b4e0f2f2c..04bfcb2bea 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePeersDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePeersDb.scala @@ -35,7 +35,7 @@ object SqlitePeersDb { val CURRENT_VERSION = 2 } -class SqlitePeersDb(sqlite: Connection) extends PeersDb with Logging { +class SqlitePeersDb(val sqlite: Connection) extends PeersDb with Logging { import SqlitePeersDb._ import SqliteUtils.ExtendedResultSet._ diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePendingCommandsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePendingCommandsDb.scala index 82ae1111fa..46b6ea80f4 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePendingCommandsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePendingCommandsDb.scala @@ -31,7 +31,7 @@ object SqlitePendingCommandsDb { val CURRENT_VERSION = 2 } -class SqlitePendingCommandsDb(sqlite: Connection) extends PendingCommandsDb with Logging { +class SqlitePendingCommandsDb(val sqlite: Connection) extends PendingCommandsDb with Logging { import SqlitePendingCommandsDb._ import SqliteUtils.ExtendedResultSet._ diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/DbMigrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/DbMigrationSpec.scala new file mode 100644 index 0000000000..5d1a4fef8f --- /dev/null +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/DbMigrationSpec.scala @@ -0,0 +1,117 @@ +package fr.acinq.eclair.db + +import akka.actor.ActorSystem +import com.opentable.db.postgres.embedded.EmbeddedPostgres +import com.zaxxer.hikari.HikariConfig +import fr.acinq.eclair.db.Databases.{PostgresDatabases, SqliteDatabases} +import fr.acinq.eclair.db.migration._ +import fr.acinq.eclair.db.pg.PgUtils.PgLock +import fr.acinq.eclair.db.pg._ +import org.scalatest.Ignore +import org.scalatest.funsuite.AnyFunSuite +import org.sqlite.SQLiteConfig + +import java.io.File +import java.sql.{Connection, DriverManager} +import java.util.UUID +import javax.sql.DataSource + +/** + * To run this test, create a `migration` directory in your project's `user.dir` + * and copy your sqlite files to it (eclair.sqlite, network.sqlite, audit.sqlite). + * Then remove the `Ignore` annotation and run the test. + */ +@Ignore +class DbMigrationSpec extends AnyFunSuite { + + import DbMigrationSpec._ + + test("eclair migration test") { + val sqlite = loadSqlite("migration\\eclair.sqlite") + val postgresDatasource = EmbeddedPostgres.start().getPostgresDatabase + + new PgChannelsDb()(postgresDatasource, PgLock.NoLock) + new PgPendingCommandsDb()(postgresDatasource, PgLock.NoLock) + new PgPeersDb()(postgresDatasource, PgLock.NoLock) + new PgPaymentsDb()(postgresDatasource, PgLock.NoLock) + + PgUtils.inTransaction { postgres => + MigrateChannelsDb.migrateAllTables(sqlite, postgres) + MigratePendingCommandsDb.migrateAllTables(sqlite, postgres) + MigratePeersDb.migrateAllTables(sqlite, postgres) + MigratePaymentsDb.migrateAllTables(sqlite, postgres) + assert(CompareChannelsDb.compareAllTables(sqlite, postgres)) + assert(ComparePendingCommandsDb.compareAllTables(sqlite, postgres)) + assert(ComparePeersDb.compareAllTables(sqlite, postgres)) + assert(ComparePaymentsDb.compareAllTables(sqlite, postgres)) + }(postgresDatasource) + + sqlite.close() + } + + test("network migration test") { + val sqlite = loadSqlite("migration\\network.sqlite") + val postgresDatasource = EmbeddedPostgres.start().getPostgresDatabase + + new PgNetworkDb()(postgresDatasource) + + PgUtils.inTransaction { postgres => + MigrateNetworkDb.migrateAllTables(sqlite, postgres) + assert(CompareNetworkDb.compareAllTables(sqlite, postgres)) + }(postgresDatasource) + + sqlite.close() + } + + test("audit migration test") { + val sqlite = loadSqlite("migration\\audit.sqlite") + val postgresDatasource = EmbeddedPostgres.start().getPostgresDatabase + + new PgAuditDb()(postgresDatasource) + + PgUtils.inTransaction { postgres => + MigrateAuditDb.migrateAllTables(sqlite, postgres) + assert(CompareAuditDb.compareAllTables(sqlite, postgres)) + }(postgresDatasource) + + sqlite.close() + } + + test("full migration") { + // we need to open in read/write because of the getVersion call + val sqlite = SqliteDatabases( + auditJdbc = loadSqlite("migration\\audit.sqlite", readOnly = false), + eclairJdbc = loadSqlite("migration\\eclair.sqlite", readOnly = false), + networkJdbc = loadSqlite("migration\\network.sqlite", readOnly = false), + jdbcUrlFile_opt = None + ) + val postgres = { + val pg = EmbeddedPostgres.start() + val datasource: DataSource = pg.getPostgresDatabase + val hikariConfig = new HikariConfig + hikariConfig.setDataSource(datasource) + PostgresDatabases( + hikariConfig = hikariConfig, + instanceId = UUID.randomUUID(), + lock = PgLock.NoLock, + jdbcUrlFile_opt = None, + readOnlyUser_opt = None, + resetJsonColumns = false, + safetyChecks_opt = None + )(ActorSystem()) + } + val dualDb = DualDatabases(sqlite, postgres) + MigrateDb.migrateAll(dualDb) + CompareDb.compareAll(dualDb) + } + +} + +object DbMigrationSpec { + def loadSqlite(path: String, readOnly: Boolean = true): Connection = { + val sqliteConfig = new SQLiteConfig() + sqliteConfig.setReadOnly(readOnly) + val dbFile = new File(path) + DriverManager.getConnection(s"jdbc:sqlite:$dbFile", sqliteConfig.toProperties) + } +} diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/DualDatabasesSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/DualDatabasesSpec.scala new file mode 100644 index 0000000000..a9ae922f23 --- /dev/null +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/DualDatabasesSpec.scala @@ -0,0 +1,86 @@ +package fr.acinq.eclair.db + +import com.opentable.db.postgres.embedded.EmbeddedPostgres +import com.typesafe.config.{Config, ConfigFactory} +import fr.acinq.eclair.wire.internal.channel.ChannelCodecsSpec +import fr.acinq.eclair.{TestKitBaseClass, TestUtils} +import org.scalatest.funsuite.AnyFunSuiteLike + +import java.io.File +import java.util.UUID + +class DualDatabasesSpec extends TestKitBaseClass with AnyFunSuiteLike { + + def fixture(driver: String): DualDatabases = { + val pg = EmbeddedPostgres.start() + val config = DualDatabasesSpec.testConfig(pg.getPort, driver) + val datadir = new File(TestUtils.BUILD_DIRECTORY, s"pg_test_${UUID.randomUUID()}") + datadir.mkdirs() + val instanceId = UUID.randomUUID() + Databases.init(config, instanceId, datadir).asInstanceOf[DualDatabases] + } + + test("sqlite primary") { + val db = fixture("dual-sqlite-primary") + + db.channels.addOrUpdateChannel(ChannelCodecsSpec.normal) + assert(db.primary.channels.listLocalChannels().nonEmpty) + awaitCond(db.primary.channels.listLocalChannels() === db.secondary.channels.listLocalChannels()) + } + + test("postgres primary") { + val db = fixture("dual-postgres-primary") + + db.channels.addOrUpdateChannel(ChannelCodecsSpec.normal) + assert(db.primary.channels.listLocalChannels().nonEmpty) + awaitCond(db.primary.channels.listLocalChannels() === db.secondary.channels.listLocalChannels()) + } +} + +object DualDatabasesSpec { + def testConfig(port: Int, driver: String): Config = + ConfigFactory.parseString( + s""" + |driver = $driver + |postgres { + | database = "" + | host = "localhost" + | port = $port + | username = "postgres" + | password = "" + | readonly-user = "" + | reset-json-columns = false + | pool { + | max-size = 10 // recommended value = number_of_cpu_cores * 2 + | connection-timeout = 30 seconds + | idle-timeout = 10 minutes + | max-life-time = 30 minutes + | } + | lock-type = "lease" // lease or none (do not use none in production) + | lease { + | interval = 5 seconds // lease-interval must be greater than lease-renew-interval + | renew-interval = 2 seconds + | lock-timeout = 5 seconds // timeout for the lock statement on the lease table + | auto-release-at-shutdown = false // automatically release the lock when eclair is stopping + | } + | safety-checks { + | // a set of basic checks on data to make sure we use the correct database + | enabled = false + | max-age { + | local-channels = 3 minutes + | network-nodes = 30 minutes + | audit-relayed = 10 minutes + | } + | min-count { + | local-channels = 10 + | network-nodes = 3000 + | network-channels = 20000 + | } + | } + |} + |dual { + | migrate-on-restart = false // migrate sqlite -> postgres on restart (only applies if sqlite is primary) + | compare-on-restart = false // compare sqlite and postgres dbs on restart + |} + |""".stripMargin) +} diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/PgUtilsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/PgUtilsSpec.scala index 3a65c0d8f0..a11283f4a9 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/PgUtilsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/PgUtilsSpec.scala @@ -32,12 +32,12 @@ class PgUtilsSpec extends TestKitBaseClass with AnyFunSuiteLike with Eventually datadir.mkdirs() val instanceId1 = UUID.randomUUID() // this will lock the database for this instance id - val db1 = Databases.postgres(config, instanceId1, datadir, LockFailureHandler.logAndThrow) + val db1 = Databases.postgres(config, instanceId1, datadir, None, LockFailureHandler.logAndThrow) assert( intercept[LockFailureHandler.LockException] { // this will fail because the database is already locked for a different instance id - Databases.postgres(config, UUID.randomUUID(), datadir, LockFailureHandler.logAndThrow) + Databases.postgres(config, UUID.randomUUID(), datadir, None, LockFailureHandler.logAndThrow) }.lockFailure === LockFailure.AlreadyLocked(instanceId1)) // we can renew the lease at will @@ -48,7 +48,7 @@ class PgUtilsSpec extends TestKitBaseClass with AnyFunSuiteLike with Eventually assert( intercept[LockFailureHandler.LockException] { // this will fail because the database is already locked for a different instance id - Databases.postgres(config, UUID.randomUUID(), datadir, LockFailureHandler.logAndThrow) + Databases.postgres(config, UUID.randomUUID(), datadir, None, LockFailureHandler.logAndThrow) }.lockFailure === LockFailure.AlreadyLocked(instanceId1)) // we close the first connection @@ -59,7 +59,7 @@ class PgUtilsSpec extends TestKitBaseClass with AnyFunSuiteLike with Eventually // now we can put a lock with a different instance id val instanceId2 = UUID.randomUUID() - val db2 = Databases.postgres(config, instanceId2, datadir, LockFailureHandler.logAndThrow) + val db2 = Databases.postgres(config, instanceId2, datadir, None, LockFailureHandler.logAndThrow) // we close the second connection db2.dataSource.close() @@ -68,7 +68,7 @@ class PgUtilsSpec extends TestKitBaseClass with AnyFunSuiteLike with Eventually // but we don't wait for the previous lease to expire, so we can't take over right now assert(intercept[LockFailureHandler.LockException] { // this will fail because even if we have acquired the table lock, the previous lease still hasn't expired - Databases.postgres(config, UUID.randomUUID(), datadir, LockFailureHandler.logAndThrow) + Databases.postgres(config, UUID.randomUUID(), datadir, None, LockFailureHandler.logAndThrow) }.lockFailure === LockFailure.AlreadyLocked(instanceId2)) pg.close() @@ -81,7 +81,7 @@ class PgUtilsSpec extends TestKitBaseClass with AnyFunSuiteLike with Eventually datadir.mkdirs() val instanceId1 = UUID.randomUUID() // this will lock the database for this instance id - val db = Databases.postgres(config, instanceId1, datadir, LockFailureHandler.logAndThrow) + val db = Databases.postgres(config, instanceId1, datadir, None, LockFailureHandler.logAndThrow) implicit val ds: DataSource = db.dataSource // dummy query works @@ -133,8 +133,9 @@ class PgUtilsSpec extends TestKitBaseClass with AnyFunSuiteLike with Eventually val config = PgUtilsSpec.testConfig(pg.getPort) val datadir = new File(TestUtils.BUILD_DIRECTORY, s"pg_test_${UUID.randomUUID()}") datadir.mkdirs() + val jdbcUrlPath = new File(datadir, "last_jdbcurl") // this will lock the database for this instance id - val db = Databases.postgres(config, UUID.randomUUID(), datadir, LockFailureHandler.logAndThrow) + val db = Databases.postgres(config, UUID.randomUUID(), datadir, Some(jdbcUrlPath), LockFailureHandler.logAndThrow) // we close the first connection db.dataSource.close() @@ -143,7 +144,7 @@ class PgUtilsSpec extends TestKitBaseClass with AnyFunSuiteLike with Eventually // here we change the config to simulate an involuntary change in the server we connect to val config1 = ConfigFactory.parseString("postgres.port=1234").withFallback(config) intercept[JdbcUrlChanged] { - Databases.postgres(config1, UUID.randomUUID(), datadir, LockFailureHandler.logAndThrow) + Databases.postgres(config1, UUID.randomUUID(), datadir, Some(jdbcUrlPath), LockFailureHandler.logAndThrow) } pg.close() @@ -156,7 +157,7 @@ class PgUtilsSpec extends TestKitBaseClass with AnyFunSuiteLike with Eventually .withFallback(PgUtilsSpec.testConfig(pg.getPort)) val datadir = new File(TestUtils.BUILD_DIRECTORY, s"pg_test_${UUID.randomUUID()}") datadir.mkdirs() - Databases.postgres(config, UUID.randomUUID(), datadir, LockFailureHandler.logAndThrow) + Databases.postgres(config, UUID.randomUUID(), datadir, None, LockFailureHandler.logAndThrow) } test("safety checks") { @@ -166,7 +167,7 @@ class PgUtilsSpec extends TestKitBaseClass with AnyFunSuiteLike with Eventually datadir.mkdirs() { - val db = Databases.postgres(baseConfig, UUID.randomUUID(), datadir, LockFailureHandler.logAndThrow) + val db = Databases.postgres(baseConfig, UUID.randomUUID(), datadir, None, LockFailureHandler.logAndThrow) db.channels.addOrUpdateChannel(ChannelCodecsSpec.normal) db.channels.updateChannelMeta(ChannelCodecsSpec.normal.channelId, ChannelEvent.EventType.Created) db.network.addNode(Announcements.makeNodeAnnouncement(randomKey(), "node-A", Color(50, 99, -80), Nil, Features.empty, TimestampSecond.now() - 45.days)) @@ -196,7 +197,7 @@ class PgUtilsSpec extends TestKitBaseClass with AnyFunSuiteLike with Eventually | } |}""".stripMargin) val config = safetyConfig.withFallback(baseConfig) - val db = Databases.postgres(config, UUID.randomUUID(), datadir, LockFailureHandler.logAndThrow) + val db = Databases.postgres(config, UUID.randomUUID(), datadir, None, LockFailureHandler.logAndThrow) db.dataSource.close() } @@ -221,7 +222,7 @@ class PgUtilsSpec extends TestKitBaseClass with AnyFunSuiteLike with Eventually |}""".stripMargin) val config = safetyConfig.withFallback(baseConfig) intercept[IllegalArgumentException] { - Databases.postgres(config, UUID.randomUUID(), datadir, LockFailureHandler.logAndThrow) + Databases.postgres(config, UUID.randomUUID(), datadir, None, LockFailureHandler.logAndThrow) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteUtilsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteUtilsSpec.scala index feead2ce86..93ed45f2e2 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteUtilsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteUtilsSpec.scala @@ -81,17 +81,17 @@ class SqliteUtilsSpec extends AnyFunSuite { test("jdbc url check") { val datadir = new File(TestUtils.BUILD_DIRECTORY, s"sqlite_test_${UUID.randomUUID()}") - val jdbcUrlPath = new File(datadir, "last_jdbcurl") datadir.mkdirs() + val jdbcUrlPath = new File(datadir, "last_jdbcurl") // first start : write to file - val db1 = Databases.sqlite(datadir) + val db1 = Databases.sqlite(datadir, Some(jdbcUrlPath)) db1.channels.close() assert(Files.readString(jdbcUrlPath.toPath).trim == "sqlite") // 2nd start : no-op - val db2 = Databases.sqlite(datadir) + val db2 = Databases.sqlite(datadir, Some(jdbcUrlPath)) db2.channels.close() // we modify the file @@ -99,7 +99,7 @@ class SqliteUtilsSpec extends AnyFunSuite { // boom intercept[JdbcUrlChanged] { - Databases.sqlite(datadir) + Databases.sqlite(datadir, Some(jdbcUrlPath)) } } From a804905c0bf68b7ed590475a07333f8c7060ae0e Mon Sep 17 00:00:00 2001 From: Bastien Teinturier <31281497+t-bast@users.noreply.github.com> Date: Tue, 1 Feb 2022 08:34:50 +0100 Subject: [PATCH 008/121] Eclair v0.7.0 release (#2158) --- docs/release-notes/eclair-v0.7.0.md | 344 ++++++++++++++++++++++++++++ docs/release-notes/eclair-vnext.md | 232 ------------------- eclair-core/pom.xml | 2 +- eclair-front/pom.xml | 2 +- eclair-node/pom.xml | 2 +- pom.xml | 2 +- 6 files changed, 348 insertions(+), 236 deletions(-) create mode 100644 docs/release-notes/eclair-v0.7.0.md delete mode 100644 docs/release-notes/eclair-vnext.md diff --git a/docs/release-notes/eclair-v0.7.0.md b/docs/release-notes/eclair-v0.7.0.md new file mode 100644 index 0000000000..dc476c8e36 --- /dev/null +++ b/docs/release-notes/eclair-v0.7.0.md @@ -0,0 +1,344 @@ +# Eclair v0.7.0 + +This release adds official support for two long-awaited lightning features: anchor outputs and onion messages. +Support for the PostreSQL database backend is also now production ready! + +This release also includes a few bug fixes and many new (smaller) features. +It is fully compatible with 0.6.2 (and all previous versions of eclair). + +Because this release changes the default type of channels that your node will use, make sure you read the release notes carefully! + +## Major changes + +### Anchor outputs activated by default + +Experimental support for anchor outputs channels was first introduced in [eclair v0.4.2](https://github.com/ACINQ/eclair/releases/tag/v0.4.2). +Going from an experimental feature to production-ready required a lot more work than we expected! +What seems to be a simple change in transaction structure had a lot of impact in many unexpected places, and fundamentally changes the mechanisms to ensure funds safety. + +When using anchor outputs, you will need to keep utxos available in your `bitcoind` wallet to fee-bump HTLC transactions when channels are force-closed. +Eclair will warn you via the `notifications.log` file when your `bitcoind` wallet balance is too low to protect your funds against malicious channel peers. +Eclair will wait for you to add funds to your wallet and automatically retry, but you may be at risk in the meantime if you had pending payments at the time of the force-close. + +We recommend activating debug logs for the transaction publication mechanisms. +This will make it much easier to follow the trail of RBF-ed transactions, and shouldn't be too noisy unless you constantly have a lot of channels force-closing. +To activate these logs, simply add the following line to your `logback.xml`: + +```xml + +``` + +You don't even need to restart eclair, it will automatically pick up the logging configuration update after a short while. + +If you don't want to use anchor outputs, you can disable the feature in your `eclair.conf`: + +```conf +eclair.features.option_anchors_zero_fee_htlc_tx = disabled +``` + +### Postgres database backend graduates to production-ready + +Postgres support was introduced in Eclair 0.4.1 as beta, and has since been improved continuously over several versions. It is now production-ready and is the recommended database backend for larger nodes. + +Postgres offers superior administration capabilities, advanced JSON queries over channel data, streaming replication, and more, which makes it a great choice for enterprise setups. + +A [step-by-step migration guide](https://github.com/ACINQ/eclair/blob/master/docs/PostgreSQL.md#migrating-from-sqlite-to-postgres) from Sqlite to Postgres is provided for users who wish to do the switch. + +Sqlite remains fully supported. + +### Alternate strategy to avoid mass force-close of channels in certain cases + +The default strategy, when an unhandled exception or internal error happens, is to locally force-close the channel. Not only is there a delay before the channel balance gets refunded, but if the exception was due to some misconfiguration or bug in eclair that affects all channels, we risk force-closing all channels. + +This is why an alternative behavior is to simply log an error and stop the node. Note that if you don't closely monitor your node, there is a risk that your peers take advantage of the downtime to try and cheat by publishing a revoked commitment. Additionally, while there is no known way of triggering an internal error in eclair from the outside, there may very well be a bug that allows just that, which could be used as a way to remotely stop the node (with the default behavior, it would "only" cause a local force-close of the channel). + +### Separate log for important notifications + +Eclair added a new log file (`notifications.log`) for important notifications that require an action from the node operator. +Node operators should watch this file very regularly. + +An event is also sent to the event stream for every such notification. +This lets plugins notify the node operator via external systems (push notifications, email, etc). + +### Support for onion messages + +Eclair now supports the `option_onion_messages` feature (see . +This feature is enabled by default: eclair will automatically relay onion messages it receives. + +By default, eclair will only accept and relay onion messages from peers with whom you have channels. +You can change that strategy by updating `eclair.onion-messages.relay-policy` in your `eclair.conf`. + +Eclair applies some rate-limiting on the number of messages that can be relayed to and from each peer. +You can choose what limits to apply by updating `eclair.onion-messages.max-per-peer-per-second` in your `eclair.conf`. + +Whenever an onion message for your node is received, eclair will emit an event, that can for example be received on the websocket (`onion-message-received`). + +You can also send onion messages via the API. +This will be covered in the API changes section below. + +To disable the feature, you can simply update your `eclair.conf`: + +```conf +eclair.features.option_onion_messages = disabled +``` + +### Support for `option_payment_metadata` + +Eclair now supports the `option_payment_metadata` feature (see ). +This feature will let recipients generate "light" invoices that don't need to be stored locally until they're paid. +This is particularly useful for payment hubs that generate a lot of invoices (e.g. to be displayed on a website) but expect only a fraction of them to actually be paid. + +Eclair includes a small `payment_metadata` field in all invoices it generates. +This lets node operators verify that payers actually support that feature. + +### Optional safety checks when using Postgres + +When using postgres, at startup we optionally run a few basic safety checks, e.g. the number of local channels, how long since the last local channel update, etc. The goal is to make sure that we are connected to the correct database instance. + +Those checks are disabled by default because they wouldn't pass on a fresh new node with zero channels. You should enable them when you already have channels, so that there is something to compare to, and the values should be specific to your setup, particularly for local channels. Configuration is done by overriding `max-age` and `min-count` values in your `eclair.conf`: + +```conf +eclair.db.postgres.safety-checks + { + enabled = true + max-age { + local-channels = 3 minutes + network-nodes = 30 minutes + audit-relayed = 10 minutes + } + min-count { + local-channels = 10 + network-nodes = 3000 + network-channels = 20000 + } +} +``` + +### API changes + +#### Timestamps + +All timestamps are now returned as an object with two attributes: + +- `iso`: ISO-8601 format with GMT time zone. Precision may be second or millisecond depending on the timestamp. +- `unix`: seconds since epoch formats (seconds since epoch). Precision is always second. + +Examples: + +- second-precision timestamp: + - before: + + ```json + { + "timestamp": 1633357961 + } + ``` + + - after + + ```json + { + "timestamp": { + "iso": "2021-10-04T14:32:41Z", + "unix": 1633357961 + } + } + ``` + +- milli-second precision timestamp: + - before: + + ```json + { + "timestamp": 1633357961456 + } + ``` + + - after (note how the unix format is in second precision): + + ```json + { + "timestamp": { + "iso": "2021-10-04T14:32:41.456Z", + "unix": 1633357961 + } + } + ``` + +#### Sending onion messages + +You can now send onion messages with `sendonionmessage`. + +There are two ways to specify the recipient: + +- when you're sending to a known `nodeId`, you must set it in the `--recipientNode` field +- when you're sending to an unknown node behind a blinded route, you must provide the blinded route in the `--recipientBlindedRoute` field (hex-encoded) + +If you're not connected to the recipient and don't have channels with them, eclair will try connecting to them based on the best address it knows (usually from their `node_announcement`). +If that fails, or if you don't want to expose your `nodeId` by directly connecting to the recipient, you should find a route to them and specify the nodes in that route in the `--intermediateNodes` field. + +You can send arbitrary content to the recipient, by providing a hex-encoded tlv in the `--content` field. + +If you expect a response, you should provide a route from the recipient back to you in the `--replyPath` field. +Eclair will create a corresponding blinded route, and the API will wait for a response (or timeout if it doesn't receive a response). + +#### Balance + +The detailed balance json format has been slightly updated for channels in state `normal` and `shutdown`, and `closing`. + +Amounts corresponding to incoming htlcs for which we knew the preimage were previously included in `toLocal`, they are +now grouped with outgoing htlcs amounts and the field has been renamed from `htlcOut` to `htlcs`. + +#### Miscellaneous + +This release contains many other API updates: + +- `deleteinvoice` allows you to remove unpaid invoices (#1984) +- `findroute`, `findroutetonode` and `findroutebetweennodes` supports new output format `full` (#1969) +- `findroute`, `findroutetonode` and `findroutebetweennodes` now accept `--ignoreNodeIds` to specify nodes you want to be ignored in path-finding (#1969) +- `findroute`, `findroutetonode` and `findroutebetweennodes` now accept `--ignoreShortChannelIds` to specify channels you want to be ignored in path-finding (#1969) +- `findroute`, `findroutetonode` and `findroutebetweennodes` now accept `--maxFeeMsat` to specify an upper bound of fees (#1969) +- `getsentinfo` output includes `failedNode` field for all failed routes +- for `payinvoice` and `sendtonode`, `--feeThresholdSat` has been renamed to `--maxFeeFlatSat` +- for `open`, `--channelFlags` has been replaced by `--announceChannel` +- the `networkstats` API has been removed + +Have a look at our [API documentation](https://acinq.github.io/eclair) for more details. + +### Miscellaneous improvements and bug fixes + +- Eclair now supports cookie authentication for Bitcoin Core RPC (#1986) +- Eclair echoes back the remote peer's IP address in `init` (#1973) +- Eclair now supports [circular rebalancing](https://github.com/ACINQ/eclair/blob/master/docs/CircularRebalancing.md) + +## Verifying signatures + +You will need `gpg` and our release signing key 7A73FE77DE2C4027. Note that you can get it: + +- from our website: https://acinq.co/pgp/drouinf.asc +- from github user @sstone, a committer on eclair: https://api.github.com/users/sstone/gpg_keys + +To import our signing key: + +```sh +$ gpg --import drouinf.asc +``` + +To verify the release file checksums and signatures: + +```sh +$ gpg -d SHA256SUMS.asc > SHA256SUMS.stripped +$ sha256sum -c SHA256SUMS.stripped +``` + +## Building + +Eclair builds are deterministic. To reproduce our builds, please use the following environment (*): + +- Ubuntu 20.04 +- AdoptOpenJDK 11.0.6 +- Maven 3.8.1 + +Use the following command to generate the eclair-node package: + +```sh +mvn clean install -DskipTests +``` + +That should generate `eclair-node/target/eclair-node-0.7.0-XXXXXXX-bin.zip` with sha256 checksums that match the one we provide and sign in `SHA256SUMS.asc` + +(*) You may be able to build the exact same artefacts with other operating systems or versions of JDK 11, we have not tried everything. + +## Upgrading + +This release is fully compatible with previous eclair versions. You don't need to close your channels, just stop eclair, upgrade and restart. + +## Changelog + +- [57bf860](https://github.com/ACINQ/eclair/commit/57bf86044ec4e2ff09d9639e986d04f88fde2305) Back to Dev (#1993) +- [df63ea4](https://github.com/ACINQ/eclair/commit/df63ea47835af6c61dbf37933707752ed06812c1) Deprecation warning for relay fees config (#2012) +- [498e9a7](https://github.com/ACINQ/eclair/commit/498e9a7db1deb5274380ee8dd47e78fef95a854f) Remove CoinUtils.scala. (#2013) +- [b22b1cb](https://github.com/ACINQ/eclair/commit/b22b1cbea738102a7c63fdbf1366254ec0dffbfe) Fix API hanging on invalid remote params (#2008) +- [9057c8e](https://github.com/ACINQ/eclair/commit/9057c8e90a9b8301b575fd90ac50f123fc0d203f) Minor improvements (#1998) +- [b4d285f](https://github.com/ACINQ/eclair/commit/b4d285f1c4c2cccfcd8b052e451a3b428e8996b0) Proper types for UNIX timestamps (#1990) +- [6018988](https://github.com/ACINQ/eclair/commit/601898864d18316ba759fa8844a2d846c729819d) Check serialization consistency in all channel tests (#1994) +- [6b202c3](https://github.com/ACINQ/eclair/commit/6b202c392bc2fad31bc33affc40577dc7577ed3d) Add low-level route blinding features (#1962) +- [f3b1604](https://github.com/ACINQ/eclair/commit/f3b16047eb188395454ae1479c4fd62f12ca9cdb) Add API to delete an invoice (#1984) +- [93481d9](https://github.com/ACINQ/eclair/commit/93481d99433b35278fe1746dc089df58f8935a9a) Higher `walletpassphrase` timeout in tests (#2022) +- [bdef833](https://github.com/ACINQ/eclair/commit/bdef8337e89bf5f31e7cb76e700c6b51fadd673f) Additional parameters for findroute* API calls (#1969) +- [9274582](https://github.com/ACINQ/eclair/commit/9274582679f0b7cbce675182019ce82c75a85305) Balance: take signed fulfills into account (#2023) +- [4e9190a](https://github.com/ACINQ/eclair/commit/4e9190aaee9207ce83152fc89e90e2c5c041a7a1) Minor: higher timeout in payment fsm test (#2026) +- [28d04ba](https://github.com/ACINQ/eclair/commit/28d04ba7a79b6fa3a20a9f3afeee43a639fc74ea) Store blinding pubkey for introduction node (#2024) +- [570dc22](https://github.com/ACINQ/eclair/commit/570dc223da6a3a92f7cf90efe6d3299800f990c3) Fix flaky transaction published event test (#2020) +- [99a8896](https://github.com/ACINQ/eclair/commit/99a889636b0dd3dc5c587a82bc032e6089e9a30c) ignoreShortChannelIds should disable edges in both directions (#2032) +- [494e346](https://github.com/ACINQ/eclair/commit/494e346231c9016d4ee71b92b99b76d891b3aeed) Minor: put htlc info logs in debug (#2030) +- [765a0c5](https://github.com/ACINQ/eclair/commit/765a0c5436a8e2ec1f469f1b066910f1205c4c99) Add log file for important notifications (#1982) +- [1573f7b](https://github.com/ACINQ/eclair/commit/1573f7be05931bb722c085eda7577c1e9731f04e) EncryptedRecipientData TLV stream should not be length-prefixed (#2029) +- [e54aaa8](https://github.com/ACINQ/eclair/commit/e54aaa84bef071c189f3fcfd7fb0e687a0d0dcbb) API: fix default time boundaries (#2035) +- [c5fa39f](https://github.com/ACINQ/eclair/commit/c5fa39f75480e859c39bd3b5708123152a067c98) Front: stop the jvm after coordinated shutdown (#2028) +- [2e9f8d9](https://github.com/ACINQ/eclair/commit/2e9f8d9f9eea47df251698852b7eb695f271f078) Cookie-based authentication for Bitcoin Core RPC (#1986) +- [2c0c24e](https://github.com/ACINQ/eclair/commit/2c0c24e1e1565a33fee1e0a3adf69a5d0be582f2) Rework channel reestablish (#2036) +- [4f458d3](https://github.com/ACINQ/eclair/commit/4f458d356ce039b1d9b6ab361b71e07427c96ba2) Alternate strategy for unhandled exceptions (#2037) +- [9f65f3a](https://github.com/ACINQ/eclair/commit/9f65f3a3a9ebedfcc7186ad0422a58d477d1b090) Make compatibility code for `waitingSince` work on testnet (#2041) +- [1f613ec](https://github.com/ACINQ/eclair/commit/1f613ec7a328de6a4f6551aaa3ad31ec3c1cb8a6) Handle mutual close published from the outside (#2046) +- [f7a79d1](https://github.com/ACINQ/eclair/commit/f7a79d10b4ddb6f91a4c66983c7a1620cc572e72) Fix response for `updaterelayfee` (#2047) +- [3dc4ae1](https://github.com/ACINQ/eclair/commit/3dc4ae1099fa1a2a3bb3742955f3c61a5c6704bc) Refactor payment onion utilities (#2051) +- [b45dd00](https://github.com/ACINQ/eclair/commit/b45dd0078ecf56d88098a74e5f2887a20c8aa4c1) Refactor sphinx payment packet (#2052) +- [4ac8236](https://github.com/ACINQ/eclair/commit/4ac823620b5bc734b365aa7edce572c0e79d00a0) Remove dumpprivkey from tests (#2053) +- [083dc3c](https://github.com/ACINQ/eclair/commit/083dc3c8da0c78414e63ae001aba126cdef80662) Onion messages (#1957) +- [333e9ef](https://github.com/ACINQ/eclair/commit/333e9ef04f0c74ca54bcf41360f895e675a3b28e) Clarify route blinding types (#2059) +- [6cc37cb](https://github.com/ACINQ/eclair/commit/6cc37cbd4f8192162e916ddabe3a612fafed06a2) Simplify onion message codec (#2060) +- [fb96e5e](https://github.com/ACINQ/eclair/commit/fb96e5eb3f29bce372af1dd392e169fd1bb8b3f1) Add failed node ID field to `FailureSummary` (#2042) +- [59b4035](https://github.com/ACINQ/eclair/commit/59b403559bdfc0ba884503bb12a14846b7aa5d2d) Relay onion messages (#2061) +- [9792c72](https://github.com/ACINQ/eclair/commit/9792c725c777d18d538e16d55ac89a847ba8beef) Rename feeThresholdSat to maxFeeFlatSat (#2079) +- [4ad502c](https://github.com/ACINQ/eclair/commit/4ad502c4c897af8f37a62bb0dfc9504ff5c9c0fc) Abort HTLC tx publisher if remote commit confirms (#2080) +- [589e84f](https://github.com/ACINQ/eclair/commit/589e84feabcb623898d2cfa62ecac70453a24645) Support private channels in SendToRoute (#2082) +- [86aed63](https://github.com/ACINQ/eclair/commit/86aed633e9741f4f604edf0d9aa507bbd12b0cfe) Remove misleading claim-htlc-success log (#2076) +- [bacb31c](https://github.com/ACINQ/eclair/commit/bacb31cf8d6db9ea323f0366a859686b632411ca) Add channel type feature bit (#2073) +- [3003289](https://github.com/ACINQ/eclair/commit/3003289328c091527f248b118e9280657417481d) No error when updating the relay fees not in normal state (#2086) +- [a470d41](https://github.com/ACINQ/eclair/commit/a470d41a9592e34d8b3b27e209f4991ac0247a34) Write received onion messages to the websocket (#2091) +- [62cc073](https://github.com/ACINQ/eclair/commit/62cc073d670e9e7407e2a8c5819292f7a829346f) Remove network stats computation (#2094) +- [ee852d6](https://github.com/ACINQ/eclair/commit/ee852d6320b567f5c5e463bdb2c2e69f1ae011c8) (Minor) Handle disconnect request when offline (#2095) +- [40cc458](https://github.com/ACINQ/eclair/commit/40cc4580438691434c5bb22a87c79b23cbc8502a) Switchboard exposes peer information (#2097) +- [3451974](https://github.com/ACINQ/eclair/commit/34519749d240e2a00b9cdc1e9e9933feeaa9a51a) Raise default connection timeout values (#2093) +- [ac9e274](https://github.com/ACINQ/eclair/commit/ac9e274f0d1083319f70e08d495f20e27dd4cccd) Add message relay policies (#2099) +- [535daec](https://github.com/ACINQ/eclair/commit/535daec0657acf2e05f5c8fca046ae9ab3f70434) Fix unhandled event in DbEventHandler (#2103) +- [4ebc8b5](https://github.com/ACINQ/eclair/commit/4ebc8b5d6b64746b8828a81765d7860c0dda5c80) Notify node operator on low fee bumping reserve (#2104) +- [576c0f6](https://github.com/ACINQ/eclair/commit/576c0f6e39783755ada0efe079e2ec3164a01842) Increase timeout onion message integration tests (#2106) +- [3d88c43](https://github.com/ACINQ/eclair/commit/3d88c43b12ab1e0c4fd4030f408119df4e4c0136) Kill idle peers (#2096) +- [8ff7dc7](https://github.com/ACINQ/eclair/commit/8ff7dc713a5024397fd687dbd2c9ccdae6aac567) Avoid default arguments in test helpers (#2108) +- [7e7de53](https://github.com/ACINQ/eclair/commit/7e7de53d1de4cba79280f08f3d4d6cf227ba42d2) Filter out non-standard channels from `channelInfo` API (#2107) +- [ec13281](https://github.com/ACINQ/eclair/commit/ec132810a574a2832933b9b2432fb50a7abd41b7) Add nightly build with bitcoind master (#2027) +- [c370abe](https://github.com/ACINQ/eclair/commit/c370abe5c3c0333c1e650383da736c5f0721b7be) Rate limit onion messages (#2090) +- [8afb02a](https://github.com/ACINQ/eclair/commit/8afb02ab97b216e7d9fd67454830b84d19115fc6) Add payment hash to `ClaimHtlcSuccessTx` (#2101) +- [bf0969a](https://github.com/ACINQ/eclair/commit/bf0969a3081800713b488e432c5fd99b5506c8a6) Add defenses against looping paths (#2109) +- [7693696](https://github.com/ACINQ/eclair/commit/76936961f9cd6c794bab90879269372f5723c7ae) Fix potential loop in path-finding (#2111) +- [0b807d2](https://github.com/ACINQ/eclair/commit/0b807d257a1ee1d33830fb91a11aa47ba7d3dcb0) Set max timestamp in API call to 9999/12/31 (#2112) +- [2827be8](https://github.com/ACINQ/eclair/commit/2827be875d2ebb5f941c7e2a876a214469d698be) Make `ClaimHtlcTx` replaceable (#2102) +- [1fd6344](https://github.com/ACINQ/eclair/commit/1fd6344a5d2a89bf7993193678520f2dc27a60bd) Define 9999-12-31 as max value for timestamps (#2118) +- [fda3818](https://github.com/ACINQ/eclair/commit/fda3818c6a38688dbc19a8bd7f0e10632876075c) Rename raw tx publisher to final tx publisher (#2119) +- [7421098](https://github.com/ACINQ/eclair/commit/7421098c4455efb2548d620ce2197bab610a2a69) Process replies to onion messages (#2117) +- [27579a5](https://github.com/ACINQ/eclair/commit/27579a5786db083700bb668a93eb86583bdf9d74) (Minor) Use `sys` package instead of `System` when applicable (#2124) +- [6e88532](https://github.com/ACINQ/eclair/commit/6e88532d18874f04b964029ce00b4008cbee3102) Add support for `option_payment_metadata` (#2063) +- [7f7ee03](https://github.com/ACINQ/eclair/commit/7f7ee0389926279b2d3277f2914f61e19c99a1f2) Handle onion creation failures (#2087) +- [546ca23](https://github.com/ACINQ/eclair/commit/546ca23984d3010e22b7bb1695220a9eed3cb00d) Activate onion messages (#2133) +- [27e29ec](https://github.com/ACINQ/eclair/commit/27e29ecebf42da223e53bfd9f1b4da3dd65953f3) Fix onion message tests by changing the rate limiter (#2132) +- [88dffbc](https://github.com/ACINQ/eclair/commit/88dffbc28c48ebbcf977333a84f9af7ff128b39a) Publish docker images (#2130) +- [40f7ff4](https://github.com/ACINQ/eclair/commit/40f7ff4034f3df32f5c5dc4f6a6af30d408cfcc5) Replaceable txs fee bumping (#2113) +- [58f9ebc](https://github.com/ACINQ/eclair/commit/58f9ebc6243563bee187478ab8d35e4315eb856d) Use BlockHeight everywhere (#2129) +- [52a6ee9](https://github.com/ACINQ/eclair/commit/52a6ee905969b66b0d912129bd29e3b58acea43d) Rename TxPublishLogContext (#2131) +- [e2b1b26](https://github.com/ACINQ/eclair/commit/e2b1b26919ed886b0e178c55a001585a38abfb16) Activate anchor outputs (#2134) +- [2f07b3e](https://github.com/ACINQ/eclair/commit/2f07b3ec0b8934bede851af93dcdb6145be585fb) (Minor) Nits (#2139) +- [f8d507b](https://github.com/ACINQ/eclair/commit/f8d507bbdd935ef5ca4eb8ce280300fdb3c1996a) Send remote address in init (#1973) +- [c180ca2](https://github.com/ACINQ/eclair/commit/c180ca2ef10531307d3441aad5b83f139eeb1c40) Postgres: add safety checks at startup (#2140) +- [0a37213](https://github.com/ACINQ/eclair/commit/0a37213483e75fbf1974ce4f6ec830596a79ae2b) Add randomization on node addresses (#2123) +- [85f9455](https://github.com/ACINQ/eclair/commit/85f94554c0c513011cabd0149d581f3d7505686f) Fix error logs formatting in TxPublisher (#2136) +- [2d64187](https://github.com/ACINQ/eclair/commit/2d641870731075f9c4ee5eef2e84f9368f410464) More aggressive confirmation target when low on utxos (#2141) +- [d59d434](https://github.com/ACINQ/eclair/commit/d59d4343c82cdd6fecddec5e44ec2cd95dbd1571) Improve `getsentinfo` accuracy (#2142) +- [f3604cf](https://github.com/ACINQ/eclair/commit/f3604cffaf9df9c8fffd5142f5dc9387fcfc74b9) Document onchain wallet backup. (#2143) +- [8758d50](https://github.com/ACINQ/eclair/commit/8758d50df26ba2e7007f5ab4243eaf556fd32d35) Update cluster documentation [ci skip] (#2122) +- [75ef66e](https://github.com/ACINQ/eclair/commit/75ef66e54c387ea7833465d8d34e8f6627bcbff2) Front: use IMDSv2 to retrieve the instance ip (#2146) +- [57c2cc5](https://github.com/ACINQ/eclair/commit/57c2cc5df907f2d848daf3d40cf2ceb6ff603ede) Type `ChannelFlags` instead of using a raw `Byte` (#2148) +- [ffecd62](https://github.com/ACINQ/eclair/commit/ffecd62cc133bb64aa916ed8d066b8ab6d7597ec) Move channel parameters to their own conf section (#2149) +- [1f6a7af](https://github.com/ACINQ/eclair/commit/1f6a7afd47e989b7c083ae9da075a9e1dd88e457) Have sqlite also write to jdbc url file (#2153) +- [0333e11](https://github.com/ACINQ/eclair/commit/0333e11c79de838cfd2bd7946d0c29b2bae3a8d6) Database migration Sqlite->Postgres (#2156) diff --git a/docs/release-notes/eclair-vnext.md b/docs/release-notes/eclair-vnext.md deleted file mode 100644 index fe805211bb..0000000000 --- a/docs/release-notes/eclair-vnext.md +++ /dev/null @@ -1,232 +0,0 @@ -# Eclair vnext - - - -## Major changes - -### Anchor outputs activated by default - -Experimental support for anchor outputs channels was first introduced in [eclair v0.4.2](https://github.com/ACINQ/eclair/releases/tag/v0.4.2). -Going from an experimental feature to production-ready required a lot more work than we expected! -What seems to be a simple change in transaction structure had a lot of impact in many unexpected places, and fundamentally changes the mechanisms to ensure funds safety. - -When using anchor outputs, you will need to keep utxos available in your `bitcoind` wallet to fee-bump HTLC transactions when channels are force-closed. -Eclair will warn you via the `notifications.log` file when your `bitcoind` wallet balance is too low to protect your funds against malicious channel peers. -Eclair will wait for you to add funds to your wallet and automatically retry, but you may be at risk in the meantime if you had pending payments at the time of the force-close. - -We recommend activating debug logs for the transaction publication mechanisms. -This will make it much easier to follow the trail of RBF-ed transactions, and shouldn't be too noisy unless you constantly have a lot of channels force-closing. -To activate these logs, simply add the following line to your `logback.xml`: - -```xml - -``` - -You don't even need to restart eclair, it will automatically pick up the logging configuration update after a short while. - -If you don't want to use anchor outputs, you can disable the feature in your `eclair.conf`: - -```conf -eclair.features.option_anchors_zero_fee_htlc_tx = disabled -``` - -### Alternate strategy to avoid mass force-close of channels in certain cases - -The default strategy, when an unhandled exception or internal error happens, is to locally force-close the channel. Not only is there a delay before the channel balance gets refunded, but if the exception was due to some misconfiguration or bug in eclair that affects all channels, we risk force-closing all channels. - -This is why an alternative behavior is to simply log an error and stop the node. Note that if you don't closely monitor your node, there is a risk that your peers take advantage of the downtime to try and cheat by publishing a revoked commitment. Additionally, while there is no known way of triggering an internal error in eclair from the outside, there may very well be a bug that allows just that, which could be used as a way to remotely stop the node (with the default behavior, it would "only" cause a local force-close of the channel). - -### Separate log for important notifications - -Eclair added a new log file (`notifications.log`) for important notifications that require an action from the node operator. -Node operators should watch this file very regularly. - -An event is also sent to the event stream for every such notification. -This lets plugins notify the node operator via external systems (push notifications, email, etc). - -### Support for onion messages - -Eclair now supports the `option_onion_messages` feature (see . -This feature is enabled by default: eclair will automatically relay onion messages it receives. - -By default, eclair will only accept and relay onion messages from peers with whom you have channels. -You can change that strategy by updating `eclair.onion-messages.relay-policy` in your `eclair.conf`. - -Eclair applies some rate-limiting on the number of messages that can be relayed to and from each peer. -You can choose what limits to apply by updating `eclair.onion-messages.max-per-peer-per-second` in your `eclair.conf`. - -Whenever an onion message for your node is received, eclair will emit an event, that can for example be received on the websocket (`onion-message-received`). - -You can also send onion messages via the API. -This will be covered in the API changes section below. - -To disable the feature, you can simply update your `eclair.conf`: - -```conf -eclair.features.option_onion_messages = disabled -``` - -### Support for `option_payment_metadata` - -Eclair now supports the `option_payment_metadata` feature (see https://github.com/lightning/bolts/pull/912). -This feature will let recipients generate "light" invoices that don't need to be stored locally until they're paid. -This is particularly useful for payment hubs that generate a lot of invoices (e.g. to be displayed on a website) but expect only a fraction of them to actually be paid. - -Eclair includes a small `payment_metadata` field in all invoices it generates. -This lets node operators verify that payers actually support that feature. - -### Optional safety checks when using Postgres - -When using postgres, at startup we optionally run a few basic safety checks, e.g. the number of local channels, how long since the last local channel update, etc. The goal is to make sure that we are connected to the correct database instance. - -Those checks are disabled by default because they wouldn't pass on a fresh new node with zero channels. You should enable them when you already have channels, so that there is something to compare to, and the values should be specific to your setup, particularly for local channels. Configuration is done by overriding `max-age` and `min-count` values in your `eclair.conf`: -``` -eclair.db.postgres.safety-checks - { - enabled = true - max-age { - local-channels = 3 minutes - network-nodes = 30 minutes - audit-relayed = 10 minutes - } - min-count { - local-channels = 10 - network-nodes = 3000 - network-channels = 20000 - } -} -``` - -### API changes - -#### Timestamps - -All timestamps are now returned as an object with two attributes: - -- `iso`: ISO-8601 format with GMT time zone. Precision may be second or millisecond depending on the timestamp. -- `unix`: seconds since epoch formats (seconds since epoch). Precision is always second. - -Examples: - -- second-precision timestamp: - - before: - ```json - { - "timestamp": 1633357961 - } - ``` - - after - ```json - { - "timestamp": { - "iso": "2021-10-04T14:32:41Z", - "unix": 1633357961 - } - } - ``` -- milli-second precision timestamp: - - before: - ```json - { - "timestamp": 1633357961456 - } - ``` - - after (note how the unix format is in second precision): - ```json - { - "timestamp": { - "iso": "2021-10-04T14:32:41.456Z", - "unix": 1633357961 - } - } - ``` - -#### Sending onion messages - -You can now send onion messages with `sendonionmessage`. - -There are two ways to specify the recipient: - -- when you're sending to a known `nodeId`, you must set it in the `--recipientNode` field -- when you're sending to an unknown node behind a blinded route, you must provide the blinded route in the `--recipientBlindedRoute` field (hex-encoded) - -If you're not connected to the recipient and don't have channels with them, eclair will try connecting to them based on the best address it knows (usually from their `node_announcement`). -If that fails, or if you don't want to expose your `nodeId` by directly connecting to the recipient, you should find a route to them and specify the nodes in that route in the `--intermediateNodes` field. - -You can send arbitrary content to the recipient, by providing a hex-encoded tlv in the `--content` field. - -If you expect a response, you should provide a route from the recipient back to you in the `--replyPath` field. -Eclair will create a corresponding blinded route, and the API will wait for a response (or timeout if it doesn't receive a response). - -#### Balance - -The detailed balance json format has been slightly updated for channels in state `normal` and `shutdown`, and `closing`. - -Amounts corresponding to incoming htlcs for which we knew the preimage were previously included in `toLocal`, they are -now grouped with outgoing htlcs amounts and the field has been renamed from `htlcOut` to `htlcs`. - -#### Miscellaneous - -This release contains many other API updates: - -- `deleteinvoice` allows you to remove unpaid invoices (#1984) -- `findroute`, `findroutetonode` and `findroutebetweennodes` supports new output format `full` (#1969) -- `findroute`, `findroutetonode` and `findroutebetweennodes` now accept `--ignoreNodeIds` to specify nodes you want to be ignored in path-finding (#1969) -- `findroute`, `findroutetonode` and `findroutebetweennodes` now accept `--ignoreShortChannelIds` to specify channels you want to be ignored in path-finding (#1969) -- `findroute`, `findroutetonode` and `findroutebetweennodes` now accept `--maxFeeMsat` to specify an upper bound of fees (#1969) -- `getsentinfo` output includes `failedNode` field for all failed routes -- for `payinvoice` and `sendtonode`, `--feeThresholdSat` has been renamed to `--maxFeeFlatSat` -- for `open`, `--channelFlags` has been replaced by `--announceChannel` -- the `networkstats` API has been removed - -Have a look at our [API documentation](https://acinq.github.io/eclair) for more details. - -### Miscellaneous improvements and bug fixes - -- Eclair now supports cookie authentication for Bitcoin Core RPC (#1986) - -## Verifying signatures - -You will need `gpg` and our release signing key 7A73FE77DE2C4027. Note that you can get it: - -- from our website: https://acinq.co/pgp/drouinf.asc -- from github user @sstone, a committer on eclair: https://api.github.com/users/sstone/gpg_keys - -To import our signing key: - -```sh -$ gpg --import drouinf.asc -``` - -To verify the release file checksums and signatures: - -```sh -$ gpg -d SHA256SUMS.asc > SHA256SUMS.stripped -$ sha256sum -c SHA256SUMS.stripped -``` - -## Building - -Eclair builds are deterministic. To reproduce our builds, please use the following environment (*): - -- Ubuntu 20.04 -- AdoptOpenJDK 11.0.6 -- Maven 3.8.1 - -Use the following command to generate the eclair-node package: - -```sh -mvn clean install -DskipTests -``` - -That should generate `eclair-node/target/eclair-node--XXXXXXX-bin.zip` with sha256 checksums that match the one we provide and sign in `SHA256SUMS.asc` - -(*) You may be able to build the exact same artefacts with other operating systems or versions of JDK 11, we have not tried everything. - -## Upgrading - -This release is fully compatible with previous eclair versions. You don't need to close your channels, just stop eclair, upgrade and restart. - -## Changelog - - diff --git a/eclair-core/pom.xml b/eclair-core/pom.xml index a027974b5f..326e467ac4 100644 --- a/eclair-core/pom.xml +++ b/eclair-core/pom.xml @@ -21,7 +21,7 @@ fr.acinq.eclair eclair_2.13 - 0.6.3-SNAPSHOT + 0.7.0 eclair-core_2.13 diff --git a/eclair-front/pom.xml b/eclair-front/pom.xml index 5f92d0f0c2..42107bb7af 100644 --- a/eclair-front/pom.xml +++ b/eclair-front/pom.xml @@ -21,7 +21,7 @@ fr.acinq.eclair eclair_2.13 - 0.6.3-SNAPSHOT + 0.7.0 eclair-front_2.13 diff --git a/eclair-node/pom.xml b/eclair-node/pom.xml index a46b4adbd5..054d3a4be7 100644 --- a/eclair-node/pom.xml +++ b/eclair-node/pom.xml @@ -21,7 +21,7 @@ fr.acinq.eclair eclair_2.13 - 0.6.3-SNAPSHOT + 0.7.0 eclair-node_2.13 diff --git a/pom.xml b/pom.xml index cf6eb02664..203c262990 100644 --- a/pom.xml +++ b/pom.xml @@ -20,7 +20,7 @@ fr.acinq.eclair eclair_2.13 - 0.6.3-SNAPSHOT + 0.7.0 pom From 44510698f7c07ffd4c9f42223a4bcec6230062a1 Mon Sep 17 00:00:00 2001 From: Bastien Teinturier <31281497+t-bast@users.noreply.github.com> Date: Tue, 1 Feb 2022 10:42:29 +0100 Subject: [PATCH 009/121] Back to Dev (#2159) --- docs/release-notes/eclair-vnext.md | 61 ++++++++++++++++++++++++++++++ eclair-core/pom.xml | 2 +- eclair-front/pom.xml | 2 +- eclair-node/pom.xml | 2 +- pom.xml | 2 +- 5 files changed, 65 insertions(+), 4 deletions(-) create mode 100644 docs/release-notes/eclair-vnext.md diff --git a/docs/release-notes/eclair-vnext.md b/docs/release-notes/eclair-vnext.md new file mode 100644 index 0000000000..2b481a1a2d --- /dev/null +++ b/docs/release-notes/eclair-vnext.md @@ -0,0 +1,61 @@ +# Eclair vnext + + + +## Major changes + + + +### API changes + + + +### Miscellaneous improvements and bug fixes + + + +## Verifying signatures + +You will need `gpg` and our release signing key 7A73FE77DE2C4027. Note that you can get it: + +- from our website: https://acinq.co/pgp/drouinf.asc +- from github user @sstone, a committer on eclair: https://api.github.com/users/sstone/gpg_keys + +To import our signing key: + +```sh +$ gpg --import drouinf.asc +``` + +To verify the release file checksums and signatures: + +```sh +$ gpg -d SHA256SUMS.asc > SHA256SUMS.stripped +$ sha256sum -c SHA256SUMS.stripped +``` + +## Building + +Eclair builds are deterministic. To reproduce our builds, please use the following environment (*): + +- Ubuntu 20.04 +- AdoptOpenJDK 11.0.6 +- Maven 3.8.1 + +Use the following command to generate the eclair-node package: + +```sh +mvn clean install -DskipTests +``` + +That should generate `eclair-node/target/eclair-node--XXXXXXX-bin.zip` with sha256 checksums that match the one we provide and sign in `SHA256SUMS.asc` + +(*) You may be able to build the exact same artefacts with other operating systems or versions of JDK 11, we have not tried everything. + +## Upgrading + +This release is fully compatible with previous eclair versions. You don't need to close your channels, just stop eclair, upgrade and restart. + +## Changelog + + diff --git a/eclair-core/pom.xml b/eclair-core/pom.xml index 326e467ac4..df61da28ec 100644 --- a/eclair-core/pom.xml +++ b/eclair-core/pom.xml @@ -21,7 +21,7 @@ fr.acinq.eclair eclair_2.13 - 0.7.0 + 0.7.1-SNAPSHOT eclair-core_2.13 diff --git a/eclair-front/pom.xml b/eclair-front/pom.xml index 42107bb7af..8166c379c8 100644 --- a/eclair-front/pom.xml +++ b/eclair-front/pom.xml @@ -21,7 +21,7 @@ fr.acinq.eclair eclair_2.13 - 0.7.0 + 0.7.1-SNAPSHOT eclair-front_2.13 diff --git a/eclair-node/pom.xml b/eclair-node/pom.xml index 054d3a4be7..385cf66486 100644 --- a/eclair-node/pom.xml +++ b/eclair-node/pom.xml @@ -21,7 +21,7 @@ fr.acinq.eclair eclair_2.13 - 0.7.0 + 0.7.1-SNAPSHOT eclair-node_2.13 diff --git a/pom.xml b/pom.xml index 203c262990..7cfed54aab 100644 --- a/pom.xml +++ b/pom.xml @@ -20,7 +20,7 @@ fr.acinq.eclair eclair_2.13 - 0.7.0 + 0.7.1-SNAPSHOT pom From cc61f121ecbf70061cc191ae68c98469ae8419bd Mon Sep 17 00:00:00 2001 From: Bastien Teinturier <31281497+t-bast@users.noreply.github.com> Date: Wed, 2 Feb 2022 15:36:22 +0100 Subject: [PATCH 010/121] Fix publish loop when funding tx double-spent (#2162) If a funding tx is double-spent, we can't publish our commit tx. However, the previous code would retry very regularly in a loop, polluting the logs. When that happens, we now only retry when a new block is found. --- .../channel/publish/MempoolTxMonitor.scala | 8 +++---- .../eclair/channel/publish/TxPublisher.scala | 21 ++++++++++++------ .../publish/MempoolTxMonitorSpec.scala | 8 +++---- .../publish/ReplaceableTxPublisherSpec.scala | 2 +- .../channel/publish/TxPublisherSpec.scala | 22 ++++++++++++++++++- 5 files changed, 44 insertions(+), 17 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/MempoolTxMonitor.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/MempoolTxMonitor.scala index 0bfb3e0ec2..031d1089aa 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/MempoolTxMonitor.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/MempoolTxMonitor.scala @@ -122,8 +122,8 @@ private class MempoolTxMonitor(nodeParams: NodeParams, log.info("could not publish tx: a conflicting mempool transaction is already in the mempool") sendFinalResult(TxRejected(cmd.tx.txid, TxRejectedReason.ConflictingTxUnconfirmed)) } else { - log.info("could not publish tx: one of our wallet inputs is not available") - sendFinalResult(TxRejected(cmd.tx.txid, TxRejectedReason.WalletInputGone)) + log.info("could not publish tx: one of our inputs cannot be found") + sendFinalResult(TxRejected(cmd.tx.txid, TxRejectedReason.InputGone)) } case CheckInputFailed(reason) => log.error("could not check input status: ", reason) @@ -174,8 +174,8 @@ private class MempoolTxMonitor(nodeParams: NodeParams, log.info("tx was evicted from the mempool: a conflicting transaction replaced it") sendFinalResult(TxRejected(cmd.tx.txid, TxRejectedReason.ConflictingTxUnconfirmed)) } else { - log.info("tx was evicted from the mempool: one of our wallet inputs disappeared") - sendFinalResult(TxRejected(cmd.tx.txid, TxRejectedReason.WalletInputGone)) + log.info("tx was evicted from the mempool: one of our inputs disappeared") + sendFinalResult(TxRejected(cmd.tx.txid, TxRejectedReason.InputGone)) } case CheckInputFailed(reason) => log.error("could not check input status: ", reason) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/TxPublisher.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/TxPublisher.scala index 2aac067a88..499b057f7b 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/TxPublisher.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/TxPublisher.scala @@ -107,8 +107,8 @@ object TxPublisher { object TxRejectedReason { /** We don't have enough funds in our wallet to reach the given feerate. */ case object CouldNotFund extends TxRejectedReason - /** The transaction was published but then evicted from the mempool, because one of its wallet inputs disappeared (e.g. unconfirmed output of a transaction that was replaced). */ - case object WalletInputGone extends TxRejectedReason + /** The transaction was published but then evicted from the mempool, because one of its inputs disappeared (e.g. unconfirmed output of a transaction that was replaced). */ + case object InputGone extends TxRejectedReason /** A conflicting transaction spending the same input has been confirmed. */ case object ConflictingTxConfirmed extends TxRejectedReason /** A conflicting transaction spending the same input is in the mempool and we failed to replace it. */ @@ -255,12 +255,19 @@ private class TxPublisher(nodeParams: NodeParams, factory: TxPublisher.ChildFact stopAttempts(rejectedAttempts) val pending2 = if (remainingAttempts.isEmpty) pending - cmd.input else pending + (cmd.input -> remainingAttempts) reason match { - case TxRejectedReason.WalletInputGone => + case TxRejectedReason.InputGone => // Our transaction has been evicted from the mempool because it depended on an unconfirmed input that has - // been replaced. We should be able to retry right now with new wallet inputs (no need to wait for a new - // block). - timers.startSingleTimer(cmd, (1 + Random.nextLong(nodeParams.channelConf.maxTxPublishRetryDelay.toMillis)).millis) - run(pending2, retryNextBlock, channelContext) + // been replaced. + cmd match { + case _: PublishReplaceableTx => + // We should be able to retry right now with new wallet inputs (no need to wait for a new block). + timers.startSingleTimer(cmd, (1 + Random.nextLong(nodeParams.channelConf.maxTxPublishRetryDelay.toMillis)).millis) + run(pending2, retryNextBlock, channelContext) + case _: PublishFinalTx => + // The transaction cannot be replaced, so there is no point in retrying immediately, let's wait until + // the next block to see if our input comes back to the mempool. + run(pending2, retryNextBlock ++ rejectedAttempts.map(_.cmd), channelContext) + } case TxRejectedReason.CouldNotFund => // We don't have enough funds at the moment to afford our target feerate, but it may change once pending // transactions confirm, so we retry when a new block is found. diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/MempoolTxMonitorSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/MempoolTxMonitorSpec.scala index 817b8d43ca..5533916a1c 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/MempoolTxMonitorSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/MempoolTxMonitorSpec.scala @@ -148,7 +148,7 @@ class MempoolTxMonitorSpec extends TestKitBaseClass with AnyFunSuiteLike with Bi val tx = createSpendP2WPKH(parentTx, priv, priv.publicKey, 5_000 sat, 0, 0) val txUnknownInput = tx.copy(txIn = tx.txIn ++ Seq(TxIn(OutPoint(randomBytes32(), 13), Nil, 0))) monitor ! Publish(probe.ref, txUnknownInput, txUnknownInput.txIn.head.outPoint, "test-tx", 10 sat) - probe.expectMsg(TxRejected(txUnknownInput.txid, WalletInputGone)) + probe.expectMsg(TxRejected(txUnknownInput.txid, InputGone)) } test("publish failed (confirmed parent, wallet input doesn't exist)") { @@ -161,7 +161,7 @@ class MempoolTxMonitorSpec extends TestKitBaseClass with AnyFunSuiteLike with Bi val tx = createSpendP2WPKH(parentTx, priv, priv.publicKey, 5_000 sat, 0, 0) val txUnknownInput = tx.copy(txIn = tx.txIn ++ Seq(TxIn(OutPoint(randomBytes32(), 13), Nil, 0))) monitor ! Publish(probe.ref, txUnknownInput, txUnknownInput.txIn.head.outPoint, "test-tx", 10 sat) - probe.expectMsg(TxRejected(txUnknownInput.txid, WalletInputGone)) + probe.expectMsg(TxRejected(txUnknownInput.txid, InputGone)) } test("publish failed (wallet input spent by conflicting confirmed transaction)") { @@ -176,7 +176,7 @@ class MempoolTxMonitorSpec extends TestKitBaseClass with AnyFunSuiteLike with Bi val tx = createSpendManyP2WPKH(Seq(parentTx, walletTx), priv, priv.publicKey, 5_000 sat, 0, 0) monitor ! Publish(probe.ref, tx, tx.txIn.head.outPoint, "test-tx", 10 sat) - probe.expectMsg(TxRejected(tx.txid, WalletInputGone)) + probe.expectMsg(TxRejected(tx.txid, InputGone)) } test("publish succeeds then transaction is replaced by an unconfirmed tx") { @@ -235,7 +235,7 @@ class MempoolTxMonitorSpec extends TestKitBaseClass with AnyFunSuiteLike with Bi // When a new block is found, we detect that the transaction has been evicted. generateBlocks(1) system.eventStream.publish(CurrentBlockHeight(currentBlockHeight(probe))) - probe.expectMsg(TxRejected(tx.txid, WalletInputGone)) + probe.expectMsg(TxRejected(tx.txid, InputGone)) } test("emit transaction events") { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPublisherSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPublisherSpec.scala index c5fa39956b..ae2a92b651 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPublisherSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPublisherSpec.scala @@ -278,7 +278,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w val result = probe.expectMsgType[TxRejected] assert(result.cmd === anchorTx) - assert(result.reason === WalletInputGone) + assert(result.reason === InputGone) // Since our wallet input is gone, we will retry and discover that a commit tx has been confirmed. val publisher2 = createPublisher() diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/TxPublisherSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/TxPublisherSpec.scala index add2d7908c..7e81c6242b 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/TxPublisherSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/TxPublisherSpec.scala @@ -186,7 +186,7 @@ class TxPublisherSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike { val attempt2 = factory.expectMsgType[ReplaceableTxPublisherSpawned] attempt2.actor.expectMsgType[ReplaceableTxPublisher.Publish] - txPublisher ! TxRejected(attempt2.id, cmd2, WalletInputGone) + txPublisher ! TxRejected(attempt2.id, cmd2, InputGone) attempt2.actor.expectMsg(ReplaceableTxPublisher.Stop) attempt1.actor.expectNoMessage(100 millis) // this error doesn't impact other publishing attempts @@ -195,6 +195,26 @@ class TxPublisherSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike { assert(attempt3.actor.expectMsgType[ReplaceableTxPublisher.Publish].cmd === cmd2) } + test("publishing attempt fails (main input gone)") { f => + import f._ + + val input = OutPoint(randomBytes32(), 3) + val tx = Transaction(2, TxIn(input, Nil, 0) :: Nil, Nil, 0) + val cmd = PublishFinalTx(tx, input, "final-tx", 0 sat, None) + txPublisher ! cmd + val attempt1 = factory.expectMsgType[FinalTxPublisherSpawned] + attempt1.actor.expectMsgType[FinalTxPublisher.Publish] + + txPublisher ! TxRejected(attempt1.id, cmd, InputGone) + attempt1.actor.expectMsg(FinalTxPublisher.Stop) + + // We don't retry until a new block is found. + factory.expectNoMessage(100 millis) + system.eventStream.publish(CurrentBlockHeight(BlockHeight(8200))) + val attempt2 = factory.expectMsgType[FinalTxPublisherSpawned] + assert(attempt2.actor.expectMsgType[FinalTxPublisher.Publish].cmd === cmd) + } + test("publishing attempt fails (not enough funds)") { f => import f._ From 648f93f682f7b980507158fd3dc65d0a1c30962a Mon Sep 17 00:00:00 2001 From: Bastien Teinturier <31281497+t-bast@users.noreply.github.com> Date: Wed, 2 Feb 2022 15:36:45 +0100 Subject: [PATCH 011/121] Ignore revoked htlcs after restart (#2161) When we restart, if a downstream channel is closing with a revoked commit, we should fail the corresponding htlcs in upstream channels. Otherwise they will be ignored until they timeout, which will cause the upstream channel to force-close. --- .../relay/PostRestartHtlcCleaner.scala | 2 + .../payment/PostRestartHtlcCleanerSpec.scala | 58 +++++++++++++++++-- 2 files changed, 54 insertions(+), 6 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/PostRestartHtlcCleaner.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/PostRestartHtlcCleaner.scala index e26e652db4..0e7e766d7e 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/PostRestartHtlcCleaner.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/PostRestartHtlcCleaner.scala @@ -366,11 +366,13 @@ object PostRestartHtlcCleaner { val overriddenHtlcs: Set[Long] = (closingType_opt match { case Some(c: Closing.LocalClose) => Closing.overriddenOutgoingHtlcs(d, c.localCommitPublished.commitTx) case Some(c: Closing.RemoteClose) => Closing.overriddenOutgoingHtlcs(d, c.remoteCommitPublished.commitTx) + case Some(c: Closing.RevokedClose) => Closing.overriddenOutgoingHtlcs(d, c.revokedCommitPublished.commitTx) case _ => Set.empty[UpdateAddHtlc] }).map(_.id) val irrevocablySpent = closingType_opt match { case Some(c: Closing.LocalClose) => c.localCommitPublished.irrevocablySpent.values.toSeq case Some(c: Closing.RemoteClose) => c.remoteCommitPublished.irrevocablySpent.values.toSeq + case Some(c: Closing.RevokedClose) => c.revokedCommitPublished.irrevocablySpent.values.toSeq case _ => Nil } val timedOutHtlcs: Set[Long] = (closingType_opt match { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PostRestartHtlcCleanerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PostRestartHtlcCleanerSpec.scala index 15beebcb7f..e2dac443f5 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PostRestartHtlcCleanerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PostRestartHtlcCleanerSpec.scala @@ -21,8 +21,9 @@ import akka.actor.ActorRef import akka.event.LoggingAdapter import akka.testkit.TestProbe import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.bitcoin.{Block, ByteVector32, Crypto, Satoshi, SatoshiLong} +import fr.acinq.bitcoin.{Block, ByteVector32, Crypto, OutPoint, Satoshi, SatoshiLong, Script, Transaction, TxIn, TxOut} import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.WatchTxConfirmedTriggered +import fr.acinq.eclair.channel.Helpers.Closing import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel.states.ChannelStateTestsHelperMethods import fr.acinq.eclair.db.{OutgoingPayment, OutgoingPaymentStatus, PaymentType} @@ -30,10 +31,11 @@ import fr.acinq.eclair.payment.OutgoingPaymentPacket.{Upstream, buildCommand} import fr.acinq.eclair.payment.PaymentPacketSpec._ import fr.acinq.eclair.payment.relay.{PostRestartHtlcCleaner, Relayer} import fr.acinq.eclair.router.Router.ChannelHop +import fr.acinq.eclair.transactions.Transactions.{ClaimRemoteDelayedOutputTx, InputInfo} import fr.acinq.eclair.transactions.{DirectedHtlc, IncomingHtlc, OutgoingHtlc} import fr.acinq.eclair.wire.internal.channel.ChannelCodecsSpec import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{BlockHeight, CltvExpiry, CltvExpiryDelta, CustomCommitmentsPlugin, MilliSatoshi, MilliSatoshiLong, NodeParams, TestConstants, TestKitBaseClass, TimestampMilliLong, randomBytes32} +import fr.acinq.eclair.{BlockHeight, CltvExpiry, CltvExpiryDelta, CustomCommitmentsPlugin, MilliSatoshi, MilliSatoshiLong, NodeParams, TestConstants, TestKitBaseClass, TimestampMilliLong, randomBytes32, randomKey} import org.scalatest.funsuite.FixtureAnyFunSuiteLike import org.scalatest.{Outcome, ParallelTestExecution} import scodec.bits.ByteVector @@ -284,7 +286,7 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit register.expectNoMessage(100 millis) } - test("ignore htlcs in closing downstream channels that have already been settled upstream") { f => + test("ignore htlcs in downstream channels that have already been settled upstream") { f => import f._ val testCase = setupTrampolinePayments(nodeParams) @@ -441,6 +443,50 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit assert(brokenHtlcs.notRelayed.isEmpty) } + test("ignore htlcs in downstream revoked close") { f => + import f._ + + val (paymentHash1, paymentHash2) = (randomBytes32(), randomBytes32()) + val htlc_ab = Seq( + buildHtlcIn(1, channelId_ab_1, paymentHash1), + buildHtlcIn(2, channelId_ab_1, paymentHash1), + buildHtlcIn(4, channelId_ab_1, paymentHash2), + ) + val upstreamChannel = ChannelCodecsSpec.makeChannelDataNormal(htlc_ab, Map.empty) + val htlc_bc = Seq( + buildHtlcOut(2, channelId_bc_1, paymentHash1), + buildHtlcOut(3, channelId_bc_1, paymentHash1), + buildHtlcOut(4, channelId_bc_1, paymentHash1), + buildHtlcOut(5, channelId_bc_1, paymentHash2), + ) + val origins: Map[Long, Origin] = Map( + 2L -> Origin.TrampolineRelayedCold((channelId_ab_1, 1L) :: (channelId_ab_1, 2L) :: Nil), + 3L -> Origin.TrampolineRelayedCold((channelId_ab_1, 1L) :: (channelId_ab_1, 2L) :: Nil), + 4L -> Origin.TrampolineRelayedCold((channelId_ab_1, 1L) :: (channelId_ab_1, 2L) :: Nil), + 5L -> Origin.ChannelRelayedCold(channelId_ab_1, 4, 550 msat, 500 msat), + ) + val downstreamChannel = { + val normal = ChannelCodecsSpec.makeChannelDataNormal(htlc_bc, origins) + // NB: this isn't actually a revoked commit tx, but we don't check that here, if the channel says it's a revoked + // commit we accept it as such, so it simplifies the test. + val revokedCommitTx = normal.commitments.localCommit.commitTxAndRemoteSig.commitTx.tx.copy(txOut = Seq(TxOut(4500 sat, Script.pay2wpkh(randomKey().publicKey)))) + val dummyClaimMainTx = Transaction(2, Seq(TxIn(OutPoint(revokedCommitTx, 0), Nil, 0)), Seq(revokedCommitTx.txOut.head.copy(amount = 4000 sat)), 0) + val dummyClaimMain = ClaimRemoteDelayedOutputTx(InputInfo(OutPoint(revokedCommitTx, 0), revokedCommitTx.txOut.head, Nil), dummyClaimMainTx) + val rcp = RevokedCommitPublished(revokedCommitTx, Some(dummyClaimMain), None, Nil, Nil, Map(revokedCommitTx.txIn.head.outPoint -> revokedCommitTx)) + DATA_CLOSING(normal.commitments, None, BlockHeight(0), Nil, revokedCommitPublished = List(rcp)) + } + + nodeParams.db.channels.addOrUpdateChannel(upstreamChannel) + nodeParams.db.channels.addOrUpdateChannel(downstreamChannel) + assert(Closing.isClosed(downstreamChannel, None) === None) + + val (_, postRestart) = f.createRelayer(nodeParams) + sender.send(postRestart, PostRestartHtlcCleaner.GetBrokenHtlcs) + val brokenHtlcs = sender.expectMsgType[PostRestartHtlcCleaner.BrokenHtlcs] + assert(brokenHtlcs.relayedOut.isEmpty) + assert(brokenHtlcs.notRelayed === htlc_ab.map(htlc => PostRestartHtlcCleaner.IncomingHtlc(htlc.add, None))) + } + test("handle a channel relay htlc-fail") { f => import f._ @@ -687,13 +733,13 @@ object PostRestartHtlcCleanerSpec { IncomingHtlc(UpdateAddHtlc(channelId, htlcId, cmd.amount, cmd.paymentHash, cmd.cltvExpiry, cmd.onion)) } - def buildForwardFail(add: UpdateAddHtlc, origin: Origin.Cold) = + def buildForwardFail(add: UpdateAddHtlc, origin: Origin.Cold): RES_ADD_SETTLED[Origin.Cold, HtlcResult.Fail] = RES_ADD_SETTLED(origin, add, HtlcResult.RemoteFail(UpdateFailHtlc(add.channelId, add.id, ByteVector.empty))) - def buildForwardOnChainFail(add: UpdateAddHtlc, origin: Origin.Cold) = + def buildForwardOnChainFail(add: UpdateAddHtlc, origin: Origin.Cold): RES_ADD_SETTLED[Origin.Cold, HtlcResult.Fail] = RES_ADD_SETTLED(origin, add, HtlcResult.OnChainFail(HtlcsTimedoutDownstream(add.channelId, Set(add)))) - def buildForwardFulfill(add: UpdateAddHtlc, origin: Origin.Cold, preimage: ByteVector32) = + def buildForwardFulfill(add: UpdateAddHtlc, origin: Origin.Cold, preimage: ByteVector32): RES_ADD_SETTLED[Origin.Cold, HtlcResult.Fulfill] = RES_ADD_SETTLED(origin, add, HtlcResult.RemoteFulfill(UpdateFulfillHtlc(add.channelId, add.id, preimage))) case class LocalPaymentTest(parentId: UUID, childIds: Seq[UUID], fails: Seq[RES_ADD_SETTLED[Origin.Cold, HtlcResult.Fail]], fulfills: Seq[RES_ADD_SETTLED[Origin.Cold, HtlcResult.Fulfill]]) From 8a65e35c8f1778e931b71821a9b32b13e68a1b93 Mon Sep 17 00:00:00 2001 From: Bastien Teinturier <31281497+t-bast@users.noreply.github.com> Date: Wed, 2 Feb 2022 17:28:03 +0100 Subject: [PATCH 012/121] Don't broadcast commit before funding locked (#2163) If a channel is force-closed before the funding transaction is confirmed, broadcasting our local commit can be a problem if the funding tx is double spent. When that happens, the channel stays stuck in the closing state trying to publish a commit tx with an invalid input. If we haven't even seen the funding tx in the mempool, we have no way of being sure that it was double spent, so we would need to keep trying forever, which pollutes the logs with publishing errors. Whenever the funding transaction isn't confirmed and we have nothing at stake, we now directly go to the closed state without publishing our commitment. This will be an issue for peers who lost state and rely on us for dataloss protection, but it's not worth exposing ourselves to that annoying edge case. Our peers should be able to at least keep state long enough for the funding tx to confirm or for them to force-close. --- .../fr/acinq/eclair/channel/Channel.scala | 1 + .../c/WaitForFundingConfirmedStateSpec.scala | 24 ++++++++++++++----- .../c/WaitForFundingLockedStateSpec.scala | 21 ++++++++++++---- 3 files changed, 35 insertions(+), 11 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala index 8300448866..80f350e17d 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala @@ -2391,6 +2391,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, remo case negotiating@DATA_NEGOTIATING(_, _, _, _, Some(bestUnpublishedClosingTx)) => // if we were in the process of closing and already received a closing sig from the counterparty, it's always better to use that handleMutualClose(bestUnpublishedClosingTx, Left(negotiating)) + case d: DATA_WAIT_FOR_FUNDING_CONFIRMED if Closing.nothingAtStake(d) => goto(CLOSED) // the channel was never used and the funding tx may be double-spent case hasCommitments: HasCommitments => spendLocalCurrent(hasCommitments) // NB: we publish the commitment even if we have nothing at stake (in a dataloss situation our peer will send us an error just for that) case _ => goto(CLOSED) // when there is no commitment yet, we just go to CLOSED state in case an error occurs } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingConfirmedStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingConfirmedStateSpec.scala index b5a7c01326..ff1d7b44d5 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingConfirmedStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingConfirmedStateSpec.scala @@ -23,11 +23,11 @@ import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._ import fr.acinq.eclair.channel.Channel.{BITCOIN_FUNDING_PUBLISH_FAILED, BITCOIN_FUNDING_TIMEOUT} import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel.publish.TxPublisher -import fr.acinq.eclair.channel.states.ChannelStateTestsBase +import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsTags} import fr.acinq.eclair.transactions.Scripts.multiSig2of2 import fr.acinq.eclair.wire.protocol.{AcceptChannel, Error, FundingCreated, FundingLocked, FundingSigned, Init, OpenChannel} -import fr.acinq.eclair.{BlockHeight, TestConstants, TestKitBaseClass, TimestampSecond, randomKey} -import org.scalatest.Outcome +import fr.acinq.eclair.{BlockHeight, MilliSatoshiLong, TestConstants, TestKitBaseClass, TimestampSecond, randomKey} +import org.scalatest.{Outcome, Tag} import org.scalatest.funsuite.FixtureAnyFunSuiteLike import scala.concurrent.duration._ @@ -38,7 +38,7 @@ import scala.concurrent.duration._ class WaitForFundingConfirmedStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with ChannelStateTestsBase { - case class FixtureParam(alice: TestFSMRef[ChannelState, ChannelData, Channel], bob: TestFSMRef[ChannelState, ChannelData, Channel], alice2bob: TestProbe, bob2alice: TestProbe, alice2blockchain: TestProbe, listener: TestProbe) + case class FixtureParam(alice: TestFSMRef[ChannelState, ChannelData, Channel], bob: TestFSMRef[ChannelState, ChannelData, Channel], alice2bob: TestProbe, bob2alice: TestProbe, alice2blockchain: TestProbe, bob2blockchain: TestProbe, listener: TestProbe) override def withFixture(test: OneArgTest): Outcome = { @@ -46,13 +46,15 @@ class WaitForFundingConfirmedStateSpec extends TestKitBaseClass with FixtureAnyF import setup._ val channelConfig = ChannelConfig.standard + val pushMsat = if (test.tags.contains(ChannelStateTestsTags.NoPushMsat)) 0.msat else TestConstants.pushMsat val (aliceParams, bobParams, channelType) = computeFeatures(setup, test.tags) val aliceInit = Init(aliceParams.initFeatures) val bobInit = Init(bobParams.initFeatures) + within(30 seconds) { val listener = TestProbe() system.eventStream.subscribe(listener.ref, classOf[TransactionPublished]) - alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, ChannelFlags.Private, channelConfig, channelType) + alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, ChannelFlags.Private, channelConfig, channelType) alice2blockchain.expectMsgType[TxPublisher.SetChannelId] bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType) bob2blockchain.expectMsgType[TxPublisher.SetChannelId] @@ -65,11 +67,14 @@ class WaitForFundingConfirmedStateSpec extends TestKitBaseClass with FixtureAnyF bob2alice.expectMsgType[FundingSigned] bob2alice.forward(alice) alice2blockchain.expectMsgType[TxPublisher.SetChannelId] + bob2blockchain.expectMsgType[TxPublisher.SetChannelId] alice2blockchain.expectMsgType[WatchFundingSpent] + bob2blockchain.expectMsgType[WatchFundingSpent] alice2blockchain.expectMsgType[WatchFundingConfirmed] + bob2blockchain.expectMsgType[WatchFundingConfirmed] listener.expectMsgType[TransactionPublished] // alice has published the funding transaction awaitCond(alice.stateName == WAIT_FOR_FUNDING_CONFIRMED) - withFixture(test.toNoArgTest(FixtureParam(alice, bob, alice2bob, bob2alice, alice2blockchain, listener))) + withFixture(test.toNoArgTest(FixtureParam(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain, listener))) } } @@ -208,6 +213,13 @@ class WaitForFundingConfirmedStateSpec extends TestKitBaseClass with FixtureAnyF assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === tx.txid) } + test("recv Error (nothing at stake)", Tag(ChannelStateTestsTags.NoPushMsat)) { f => + import f._ + bob ! Error(ByteVector32.Zeroes, "funding double-spent") + bob2blockchain.expectNoMessage(100 millis) // we don't publish our commit tx when we have nothing at stake + awaitCond(bob.stateName == CLOSED) + } + test("recv CMD_CLOSE") { f => import f._ val sender = TestProbe() diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingLockedStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingLockedStateSpec.scala index 67954c4ca7..76dcae683c 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingLockedStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingLockedStateSpec.scala @@ -21,12 +21,12 @@ import fr.acinq.bitcoin.{ByteVector32, Transaction} import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._ import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel.publish.TxPublisher -import fr.acinq.eclair.channel.states.ChannelStateTestsBase +import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsTags} import fr.acinq.eclair.payment.relay.Relayer.RelayFees import fr.acinq.eclair.wire.protocol._ import fr.acinq.eclair.{BlockHeight, MilliSatoshiLong, TestConstants, TestKitBaseClass} -import org.scalatest.Outcome import org.scalatest.funsuite.FixtureAnyFunSuiteLike +import org.scalatest.{Outcome, Tag} import scala.concurrent.duration._ @@ -38,18 +38,20 @@ class WaitForFundingLockedStateSpec extends TestKitBaseClass with FixtureAnyFunS val relayFees: RelayFees = RelayFees(999 msat, 1234) - case class FixtureParam(alice: TestFSMRef[ChannelState, ChannelData, Channel], bob: TestFSMRef[ChannelState, ChannelData, Channel], alice2bob: TestProbe, bob2alice: TestProbe, alice2blockchain: TestProbe, router: TestProbe) + case class FixtureParam(alice: TestFSMRef[ChannelState, ChannelData, Channel], bob: TestFSMRef[ChannelState, ChannelData, Channel], alice2bob: TestProbe, bob2alice: TestProbe, alice2blockchain: TestProbe, bob2blockchain: TestProbe, router: TestProbe) override def withFixture(test: OneArgTest): Outcome = { val setup = init() import setup._ val channelConfig = ChannelConfig.standard + val pushMsat = if (test.tags.contains(ChannelStateTestsTags.NoPushMsat)) 0.msat else TestConstants.pushMsat val (aliceParams, bobParams, channelType) = computeFeatures(setup, test.tags) val aliceInit = Init(aliceParams.initFeatures) val bobInit = Init(bobParams.initFeatures) + within(30 seconds) { alice.underlyingActor.nodeParams.db.peers.addOrUpdateRelayFees(bobParams.nodeId, relayFees) - alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, ChannelFlags.Private, channelConfig, channelType) + alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, ChannelFlags.Private, channelConfig, channelType) alice2blockchain.expectMsgType[TxPublisher.SetChannelId] bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType) bob2blockchain.expectMsgType[TxPublisher.SetChannelId] @@ -76,7 +78,7 @@ class WaitForFundingLockedStateSpec extends TestKitBaseClass with FixtureAnyFunS alice2bob.expectMsgType[FundingLocked] awaitCond(alice.stateName == WAIT_FOR_FUNDING_LOCKED) awaitCond(bob.stateName == WAIT_FOR_FUNDING_LOCKED) - withFixture(test.toNoArgTest(FixtureParam(alice, bob, alice2bob, bob2alice, alice2blockchain, router))) + withFixture(test.toNoArgTest(FixtureParam(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain, router))) } } @@ -121,6 +123,15 @@ class WaitForFundingLockedStateSpec extends TestKitBaseClass with FixtureAnyFunS assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === tx.txid) } + test("recv Error (nothing at stake)", Tag(ChannelStateTestsTags.NoPushMsat)) { f => + import f._ + val tx = bob.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_LOCKED].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx + bob ! Error(ByteVector32.Zeroes, "funding double-spent") + awaitCond(bob.stateName == CLOSING) + assert(bob2blockchain.expectMsgType[TxPublisher.PublishFinalTx].tx.txid === tx.txid) + assert(bob2blockchain.expectMsgType[WatchTxConfirmed].txId === tx.txid) + } + test("recv CMD_CLOSE") { f => import f._ val sender = TestProbe() From 4307bada516aae604a9ba0894ae679146ea971bc Mon Sep 17 00:00:00 2001 From: Thomas HUET <81159533+thomash-acinq@users.noreply.github.com> Date: Wed, 2 Feb 2022 17:29:15 +0100 Subject: [PATCH 013/121] Refactoring of PaymentRequest (#2144) Refactoring of `PaymentRequest` to pave the way for bolt12 invoices `PaymentRequest` is renamed to `Invoice` and is now a trait extended by `Bolt11Invoice` and `Bolt12Invoice`. `Bolt12Invoice` will come in a future PR. Co-authored-by: t-bast --- eclair-core/src/main/resources/reference.conf | 2 +- .../main/scala/fr/acinq/eclair/Eclair.scala | 68 +-- .../main/scala/fr/acinq/eclair/Features.scala | 2 +- .../scala/fr/acinq/eclair/NodeParams.scala | 6 +- .../fr/acinq/eclair/db/DualDatabases.scala | 2 +- .../scala/fr/acinq/eclair/db/PaymentsDb.scala | 32 +- .../fr/acinq/eclair/db/pg/PgPaymentsDb.scala | 34 +- .../eclair/db/sqlite/SqlitePaymentsDb.scala | 38 +- .../acinq/eclair/json/JsonSerializers.scala | 16 +- .../acinq/eclair/message/OnionMessages.scala | 20 +- ...ymentRequest.scala => Bolt11Invoice.scala} | 199 ++++---- .../fr/acinq/eclair/payment/Invoice.scala | 58 +++ .../acinq/eclair/payment/PaymentEvents.scala | 2 +- .../acinq/eclair/payment/PaymentPacket.scala | 6 +- .../payment/receive/MultiPartHandler.scala | 58 +-- .../acinq/eclair/payment/send/Autoprobe.scala | 10 +- .../send/MultiPartPaymentLifecycle.scala | 4 +- .../payment/send/PaymentInitiator.scala | 72 +-- .../payment/send/PaymentLifecycle.scala | 6 +- .../eclair/router/RouteCalculation.scala | 4 +- .../scala/fr/acinq/eclair/router/Router.scala | 2 +- .../eclair/wire/protocol/PaymentOnion.scala | 8 +- .../fr/acinq/eclair/EclairImplSpec.scala | 23 +- .../scala/fr/acinq/eclair/FeaturesSpec.scala | 2 +- .../scala/fr/acinq/eclair/TestConstants.scala | 4 +- .../fr/acinq/eclair/channel/FuzzySpec.scala | 2 +- .../fr/acinq/eclair/db/PaymentsDbSpec.scala | 94 ++-- .../integration/ChannelIntegrationSpec.scala | 12 +- .../integration/PaymentIntegrationSpec.scala | 188 +++---- .../PerformanceIntegrationSpec.scala | 3 +- .../fr/acinq/eclair/io/MessageRelaySpec.scala | 14 +- .../acinq/eclair/io/PeerConnectionSpec.scala | 10 +- .../scala/fr/acinq/eclair/io/PeerSpec.scala | 4 +- .../eclair/json/JsonSerializersSpec.scala | 14 +- .../eclair/message/OnionMessagesSpec.scala | 10 +- .../fr/acinq/eclair/message/PostmanSpec.scala | 14 +- ...uestSpec.scala => Bolt11InvoiceSpec.scala} | 479 +++++++++--------- .../eclair/payment/MultiPartHandlerSpec.scala | 236 ++++----- .../MultiPartPaymentLifecycleSpec.scala | 2 +- .../eclair/payment/PaymentInitiatorSpec.scala | 174 +++---- .../eclair/payment/PaymentLifecycleSpec.scala | 4 +- .../eclair/payment/PaymentPacketSpec.scala | 13 +- .../payment/PostRestartHtlcCleanerSpec.scala | 2 +- .../payment/relay/NodeRelayerSpec.scala | 28 +- .../eclair/router/RouteCalculationSpec.scala | 2 +- .../fr/acinq/eclair/router/RouterSpec.scala | 2 +- .../protocol/LightningMessageCodecsSpec.scala | 2 +- .../wire/protocol/PaymentOnionSpec.scala | 2 +- .../api/directives/ExtraDirectives.scala | 6 +- .../eclair/api/handlers/PathFinding.scala | 4 +- .../acinq/eclair/api/handlers/Payment.scala | 4 +- .../api/serde/FormParamExtractors.scala | 4 +- .../src/test/resources/api/received-expired | 2 +- .../src/test/resources/api/received-pending | 2 +- .../src/test/resources/api/received-success | 2 +- .../fr/acinq/eclair/api/ApiServiceSpec.scala | 20 +- 56 files changed, 1040 insertions(+), 993 deletions(-) rename eclair-core/src/main/scala/fr/acinq/eclair/payment/{PaymentRequest.scala => Bolt11Invoice.scala} (76%) create mode 100644 eclair-core/src/main/scala/fr/acinq/eclair/payment/Invoice.scala rename eclair-core/src/test/scala/fr/acinq/eclair/payment/{PaymentRequestSpec.scala => Bolt11InvoiceSpec.scala} (61%) diff --git a/eclair-core/src/main/resources/reference.conf b/eclair-core/src/main/resources/reference.conf index ee5e42285e..abdfc139e8 100644 --- a/eclair-core/src/main/resources/reference.conf +++ b/eclair-core/src/main/resources/reference.conf @@ -211,7 +211,7 @@ eclair { initial-random-reconnect-delay = 5 seconds // we add a random delay before the first reconnection attempt, capped by this value max-reconnect-interval = 1 hour // max interval between two reconnection attempts, after the exponential backoff period - payment-request-expiry = 1 hour // default expiry for payment requests generated by this node + invoice-expiry = 1 hour // default expiry for invoices generated by this node multi-part-payment-expiry = 60 seconds // default expiry for receiving all parts of a multi-part payment max-payment-attempts = 5 diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala index 55a5cf9c43..dffdb1ee92 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala @@ -19,6 +19,9 @@ package fr.acinq.eclair import akka.actor.ActorRef import akka.actor.typed.scaladsl.AskPattern.Askable import akka.actor.typed.scaladsl.adapter.ClassicSchedulerOps +import akka.actor.{ActorRef, typed} +import akka.actor.typed.scaladsl.AskPattern.{Askable, schedulerFromActorSystem} +import akka.actor.typed.scaladsl.adapter.{ClassicActorSystemOps, ClassicSchedulerOps} import akka.pattern._ import akka.util.Timeout import com.softwaremill.quicklens.ModifyPimp @@ -64,9 +67,7 @@ case class AuditResponse(sent: Seq[PaymentSent], received: Seq[PaymentReceived], // @formatter:off case class SignedMessage(nodeId: PublicKey, message: String, signature: ByteVector) case class VerifiedMessage(valid: Boolean, publicKey: PublicKey) -// @formatter:on -// @formatter:off case class SendOnionMessageResponsePayload(encodedReplyPath: Option[String], replyPath: Option[Sphinx.RouteBlinding.BlindedRoute], unknownTlvs: Map[String, ByteVector]) case class SendOnionMessageResponse(sent: Boolean, failureMessage: Option[String], response: Option[SendOnionMessageResponsePayload]) // @formatter:on @@ -104,15 +105,15 @@ trait Eclair { def nodes(nodeIds_opt: Option[Set[PublicKey]] = None)(implicit timeout: Timeout): Future[Iterable[NodeAnnouncement]] - def receive(description: Either[String, ByteVector32], amount_opt: Option[MilliSatoshi], expire_opt: Option[Long], fallbackAddress_opt: Option[String], paymentPreimage_opt: Option[ByteVector32])(implicit timeout: Timeout): Future[PaymentRequest] + def receive(description: Either[String, ByteVector32], amount_opt: Option[MilliSatoshi], expire_opt: Option[Long], fallbackAddress_opt: Option[String], paymentPreimage_opt: Option[ByteVector32])(implicit timeout: Timeout): Future[Bolt11Invoice] def newAddress(): Future[String] def receivedInfo(paymentHash: ByteVector32)(implicit timeout: Timeout): Future[Option[IncomingPayment]] - def send(externalId_opt: Option[String], amount: MilliSatoshi, invoice: PaymentRequest, maxAttempts_opt: Option[Int] = None, maxFeeFlat_opt: Option[Satoshi] = None, maxFeePct_opt: Option[Double] = None, pathFindingExperimentName_opt: Option[String] = None)(implicit timeout: Timeout): Future[UUID] + def send(externalId_opt: Option[String], amount: MilliSatoshi, invoice: Bolt11Invoice, maxAttempts_opt: Option[Int] = None, maxFeeFlat_opt: Option[Satoshi] = None, maxFeePct_opt: Option[Double] = None, pathFindingExperimentName_opt: Option[String] = None)(implicit timeout: Timeout): Future[UUID] - def sendBlocking(externalId_opt: Option[String], amount: MilliSatoshi, invoice: PaymentRequest, maxAttempts_opt: Option[Int] = None, maxFeeFlat_opt: Option[Satoshi] = None, maxFeePct_opt: Option[Double] = None, pathFindingExperimentName_opt: Option[String] = None)(implicit timeout: Timeout): Future[Either[PreimageReceived, PaymentEvent]] + def sendBlocking(externalId_opt: Option[String], amount: MilliSatoshi, invoice: Bolt11Invoice, maxAttempts_opt: Option[Int] = None, maxFeeFlat_opt: Option[Satoshi] = None, maxFeePct_opt: Option[Double] = None, pathFindingExperimentName_opt: Option[String] = None)(implicit timeout: Timeout): Future[Either[PreimageReceived, PaymentEvent]] def sendWithPreimage(externalId_opt: Option[String], recipientNodeId: PublicKey, amount: MilliSatoshi, paymentPreimage: ByteVector32 = randomBytes32(), maxAttempts_opt: Option[Int] = None, maxFeeFlat_opt: Option[Satoshi] = None, maxFeePct_opt: Option[Double] = None, pathFindingExperimentName_opt: Option[String] = None)(implicit timeout: Timeout): Future[UUID] @@ -120,11 +121,11 @@ trait Eclair { def sendOnChain(address: String, amount: Satoshi, confirmationTarget: Long): Future[ByteVector32] - def findRoute(targetNodeId: PublicKey, amount: MilliSatoshi, pathFindingExperimentName_opt: Option[String], assistedRoutes: Seq[Seq[PaymentRequest.ExtraHop]] = Seq.empty, includeLocalChannelCost: Boolean = false, ignoreNodeIds: Seq[PublicKey] = Seq.empty, ignoreShortChannelIds: Seq[ShortChannelId] = Seq.empty, maxFee_opt: Option[MilliSatoshi] = None)(implicit timeout: Timeout): Future[RouteResponse] + def findRoute(targetNodeId: PublicKey, amount: MilliSatoshi, pathFindingExperimentName_opt: Option[String], assistedRoutes: Seq[Seq[Bolt11Invoice.ExtraHop]] = Seq.empty, includeLocalChannelCost: Boolean = false, ignoreNodeIds: Seq[PublicKey] = Seq.empty, ignoreShortChannelIds: Seq[ShortChannelId] = Seq.empty, maxFee_opt: Option[MilliSatoshi] = None)(implicit timeout: Timeout): Future[RouteResponse] - def findRouteBetween(sourceNodeId: PublicKey, targetNodeId: PublicKey, amount: MilliSatoshi, pathFindingExperimentName_opt: Option[String], assistedRoutes: Seq[Seq[PaymentRequest.ExtraHop]] = Seq.empty, includeLocalChannelCost: Boolean = false, ignoreNodeIds: Seq[PublicKey] = Seq.empty, ignoreShortChannelIds: Seq[ShortChannelId] = Seq.empty, maxFee_opt: Option[MilliSatoshi] = None)(implicit timeout: Timeout): Future[RouteResponse] + def findRouteBetween(sourceNodeId: PublicKey, targetNodeId: PublicKey, amount: MilliSatoshi, pathFindingExperimentName_opt: Option[String], assistedRoutes: Seq[Seq[Bolt11Invoice.ExtraHop]] = Seq.empty, includeLocalChannelCost: Boolean = false, ignoreNodeIds: Seq[PublicKey] = Seq.empty, ignoreShortChannelIds: Seq[ShortChannelId] = Seq.empty, maxFee_opt: Option[MilliSatoshi] = None)(implicit timeout: Timeout): Future[RouteResponse] - def sendToRoute(amount: MilliSatoshi, recipientAmount_opt: Option[MilliSatoshi], externalId_opt: Option[String], parentId_opt: Option[UUID], invoice: PaymentRequest, finalCltvExpiryDelta: CltvExpiryDelta, route: PredefinedRoute, trampolineSecret_opt: Option[ByteVector32] = None, trampolineFees_opt: Option[MilliSatoshi] = None, trampolineExpiryDelta_opt: Option[CltvExpiryDelta] = None, trampolineNodes_opt: Seq[PublicKey] = Nil)(implicit timeout: Timeout): Future[SendPaymentToRouteResponse] + def sendToRoute(amount: MilliSatoshi, recipientAmount_opt: Option[MilliSatoshi], externalId_opt: Option[String], parentId_opt: Option[UUID], invoice: Bolt11Invoice, finalCltvExpiryDelta: CltvExpiryDelta, route: PredefinedRoute, trampolineSecret_opt: Option[ByteVector32] = None, trampolineFees_opt: Option[MilliSatoshi] = None, trampolineExpiryDelta_opt: Option[CltvExpiryDelta] = None, trampolineNodes_opt: Seq[PublicKey] = Nil)(implicit timeout: Timeout): Future[SendPaymentToRouteResponse] def audit(from: TimestampSecond, to: TimestampSecond)(implicit timeout: Timeout): Future[AuditResponse] @@ -132,11 +133,11 @@ trait Eclair { def channelStats(from: TimestampSecond, to: TimestampSecond)(implicit timeout: Timeout): Future[Seq[Stats]] - def getInvoice(paymentHash: ByteVector32)(implicit timeout: Timeout): Future[Option[PaymentRequest]] + def getInvoice(paymentHash: ByteVector32)(implicit timeout: Timeout): Future[Option[Bolt11Invoice]] - def pendingInvoices(from: TimestampSecond, to: TimestampSecond)(implicit timeout: Timeout): Future[Seq[PaymentRequest]] + def pendingInvoices(from: TimestampSecond, to: TimestampSecond)(implicit timeout: Timeout): Future[Seq[Bolt11Invoice]] - def allInvoices(from: TimestampSecond, to: TimestampSecond)(implicit timeout: Timeout): Future[Seq[PaymentRequest]] + def allInvoices(from: TimestampSecond, to: TimestampSecond)(implicit timeout: Timeout): Future[Seq[Bolt11Invoice]] def deleteInvoice(paymentHash: ByteVector32): Future[String] @@ -249,9 +250,9 @@ class EclairImpl(appKit: Kit) extends Eclair with Logging { } } - override def receive(description: Either[String, ByteVector32], amount_opt: Option[MilliSatoshi], expire_opt: Option[Long], fallbackAddress_opt: Option[String], paymentPreimage_opt: Option[ByteVector32])(implicit timeout: Timeout): Future[PaymentRequest] = { + override def receive(description: Either[String, ByteVector32], amount_opt: Option[MilliSatoshi], expire_opt: Option[Long], fallbackAddress_opt: Option[String], paymentPreimage_opt: Option[ByteVector32])(implicit timeout: Timeout): Future[Bolt11Invoice] = { fallbackAddress_opt.map { fa => fr.acinq.eclair.addressToPublicKeyScript(fa, appKit.nodeParams.chainHash) } // if it's not a bitcoin address throws an exception - (appKit.paymentHandler ? ReceivePayment(amount_opt, description, expire_opt, fallbackAddress_opt = fallbackAddress_opt, paymentPreimage_opt = paymentPreimage_opt)).mapTo[PaymentRequest] + (appKit.paymentHandler ? ReceivePayment(amount_opt, description, expire_opt, fallbackAddress_opt = fallbackAddress_opt, paymentPreimage_opt = paymentPreimage_opt)).mapTo[Bolt11Invoice] } override def newAddress(): Future[String] = { @@ -282,7 +283,7 @@ class EclairImpl(appKit: Kit) extends Eclair with Logging { } } - override def findRoute(targetNodeId: PublicKey, amount: MilliSatoshi, pathFindingExperimentName_opt: Option[String], assistedRoutes: Seq[Seq[PaymentRequest.ExtraHop]] = Seq.empty, includeLocalChannelCost: Boolean = false, ignoreNodeIds: Seq[PublicKey] = Seq.empty, ignoreShortChannelIds: Seq[ShortChannelId] = Seq.empty, maxFee_opt: Option[MilliSatoshi] = None)(implicit timeout: Timeout): Future[RouteResponse] = + override def findRoute(targetNodeId: PublicKey, amount: MilliSatoshi, pathFindingExperimentName_opt: Option[String], assistedRoutes: Seq[Seq[Bolt11Invoice.ExtraHop]] = Seq.empty, includeLocalChannelCost: Boolean = false, ignoreNodeIds: Seq[PublicKey] = Seq.empty, ignoreShortChannelIds: Seq[ShortChannelId] = Seq.empty, maxFee_opt: Option[MilliSatoshi] = None)(implicit timeout: Timeout): Future[RouteResponse] = findRouteBetween(appKit.nodeParams.nodeId, targetNodeId, amount, pathFindingExperimentName_opt, assistedRoutes, includeLocalChannelCost, ignoreNodeIds, ignoreShortChannelIds, maxFee_opt) private def getRouteParams(pathFindingExperimentName_opt: Option[String]): Either[IllegalArgumentException, RouteParams] = { @@ -295,7 +296,7 @@ class EclairImpl(appKit: Kit) extends Eclair with Logging { } } - override def findRouteBetween(sourceNodeId: PublicKey, targetNodeId: PublicKey, amount: MilliSatoshi, pathFindingExperimentName_opt: Option[String], assistedRoutes: Seq[Seq[PaymentRequest.ExtraHop]] = Seq.empty, includeLocalChannelCost: Boolean = false, ignoreNodeIds: Seq[PublicKey] = Seq.empty, ignoreShortChannelIds: Seq[ShortChannelId] = Seq.empty, maxFee_opt: Option[MilliSatoshi] = None)(implicit timeout: Timeout): Future[RouteResponse] = { + override def findRouteBetween(sourceNodeId: PublicKey, targetNodeId: PublicKey, amount: MilliSatoshi, pathFindingExperimentName_opt: Option[String], assistedRoutes: Seq[Seq[Bolt11Invoice.ExtraHop]] = Seq.empty, includeLocalChannelCost: Boolean = false, ignoreNodeIds: Seq[PublicKey] = Seq.empty, ignoreShortChannelIds: Seq[ShortChannelId] = Seq.empty, maxFee_opt: Option[MilliSatoshi] = None)(implicit timeout: Timeout): Future[RouteResponse] = { getRouteParams(pathFindingExperimentName_opt) match { case Right(routeParams) => val maxFee = maxFee_opt.getOrElse(routeParams.getMaxFee(amount)) @@ -308,10 +309,10 @@ class EclairImpl(appKit: Kit) extends Eclair with Logging { } } - override def sendToRoute(amount: MilliSatoshi, recipientAmount_opt: Option[MilliSatoshi], externalId_opt: Option[String], parentId_opt: Option[UUID], invoice: PaymentRequest, finalCltvExpiryDelta: CltvExpiryDelta, route: PredefinedRoute, trampolineSecret_opt: Option[ByteVector32], trampolineFees_opt: Option[MilliSatoshi], trampolineExpiryDelta_opt: Option[CltvExpiryDelta], trampolineNodes_opt: Seq[PublicKey])(implicit timeout: Timeout): Future[SendPaymentToRouteResponse] = { - val recipientAmount = recipientAmount_opt.getOrElse(invoice.amount.getOrElse(amount)) + override def sendToRoute(amount: MilliSatoshi, recipientAmount_opt: Option[MilliSatoshi], externalId_opt: Option[String], parentId_opt: Option[UUID], invoice: Bolt11Invoice, finalCltvExpiryDelta: CltvExpiryDelta, route: PredefinedRoute, trampolineSecret_opt: Option[ByteVector32], trampolineFees_opt: Option[MilliSatoshi], trampolineExpiryDelta_opt: Option[CltvExpiryDelta], trampolineNodes_opt: Seq[PublicKey])(implicit timeout: Timeout): Future[SendPaymentToRouteResponse] = { + val recipientAmount = recipientAmount_opt.getOrElse(invoice.amount_opt.getOrElse(amount)) val sendPayment = SendPaymentToRoute(amount, recipientAmount, invoice, finalCltvExpiryDelta, route, externalId_opt, parentId_opt, trampolineSecret_opt, trampolineFees_opt.getOrElse(0 msat), trampolineExpiryDelta_opt.getOrElse(CltvExpiryDelta(0)), trampolineNodes_opt) - if (invoice.isExpired) { + if (invoice.isExpired()) { Future.failed(new IllegalArgumentException("invoice has expired")) } else if (route.isEmpty) { Future.failed(new IllegalArgumentException("missing payment route")) @@ -326,7 +327,7 @@ class EclairImpl(appKit: Kit) extends Eclair with Logging { } } - private def createSendPaymentRequest(externalId_opt: Option[String], amount: MilliSatoshi, invoice: PaymentRequest, maxAttempts_opt: Option[Int], maxFeeFlat_opt: Option[Satoshi], maxFeePct_opt: Option[Double], pathFindingExperimentName_opt: Option[String]): Either[IllegalArgumentException, SendPaymentToNode] = { + private def createSendPaymentRequest(externalId_opt: Option[String], amount: MilliSatoshi, invoice: Bolt11Invoice, maxAttempts_opt: Option[Int], maxFeeFlat_opt: Option[Satoshi], maxFeePct_opt: Option[Double], pathFindingExperimentName_opt: Option[String]): Either[IllegalArgumentException, SendPaymentToNode] = { val maxAttempts = maxAttempts_opt.getOrElse(appKit.nodeParams.maxPaymentAttempts) getRouteParams(pathFindingExperimentName_opt) match { case Right(defaultRouteParams) => @@ -335,7 +336,7 @@ class EclairImpl(appKit: Kit) extends Eclair with Logging { .modify(_.boundaries.maxFeeFlat).setToIfDefined(maxFeeFlat_opt.map(_.toMilliSatoshi)) externalId_opt match { case Some(externalId) if externalId.length > externalIdMaxLength => Left(new IllegalArgumentException(s"externalId is too long: cannot exceed $externalIdMaxLength characters")) - case _ if invoice.isExpired => Left(new IllegalArgumentException("invoice has expired")) + case _ if invoice.isExpired() => Left(new IllegalArgumentException("invoice has expired")) case _ => invoice.minFinalCltvExpiryDelta match { case Some(minFinalCltvExpiryDelta) => Right(SendPaymentToNode(amount, invoice, maxAttempts, minFinalCltvExpiryDelta, externalId_opt, assistedRoutes = invoice.routingInfo, routeParams = routeParams)) case None => Right(SendPaymentToNode(amount, invoice, maxAttempts, externalId = externalId_opt, assistedRoutes = invoice.routingInfo, routeParams = routeParams)) @@ -345,14 +346,14 @@ class EclairImpl(appKit: Kit) extends Eclair with Logging { } } - override def send(externalId_opt: Option[String], amount: MilliSatoshi, invoice: PaymentRequest, maxAttempts_opt: Option[Int], maxFeeFlat_opt: Option[Satoshi], maxFeePct_opt: Option[Double], pathFindingExperimentName_opt: Option[String])(implicit timeout: Timeout): Future[UUID] = { + override def send(externalId_opt: Option[String], amount: MilliSatoshi, invoice: Bolt11Invoice, maxAttempts_opt: Option[Int], maxFeeFlat_opt: Option[Satoshi], maxFeePct_opt: Option[Double], pathFindingExperimentName_opt: Option[String])(implicit timeout: Timeout): Future[UUID] = { createSendPaymentRequest(externalId_opt, amount, invoice, maxAttempts_opt, maxFeeFlat_opt, maxFeePct_opt, pathFindingExperimentName_opt) match { case Left(ex) => Future.failed(ex) case Right(req) => (appKit.paymentInitiator ? req).mapTo[UUID] } } - override def sendBlocking(externalId_opt: Option[String], amount: MilliSatoshi, invoice: PaymentRequest, maxAttempts_opt: Option[Int], maxFeeFlat_opt: Option[Satoshi], maxFeePct_opt: Option[Double], pathFindingExperimentName_opt: Option[String])(implicit timeout: Timeout): Future[Either[PreimageReceived, PaymentEvent]] = { + override def sendBlocking(externalId_opt: Option[String], amount: MilliSatoshi, invoice: Bolt11Invoice, maxAttempts_opt: Option[Int], maxFeeFlat_opt: Option[Satoshi], maxFeePct_opt: Option[Double], pathFindingExperimentName_opt: Option[String])(implicit timeout: Timeout): Future[Either[PreimageReceived, PaymentEvent]] = { createSendPaymentRequest(externalId_opt, amount, invoice, maxAttempts_opt, maxFeeFlat_opt, maxFeePct_opt, pathFindingExperimentName_opt) match { case Left(ex) => Future.failed(ex) case Right(req) => (appKit.paymentInitiator ? req.copy(blockUntilComplete = true)).map { @@ -392,9 +393,9 @@ class EclairImpl(appKit: Kit) extends Eclair with Logging { val paymentType = "placeholder" val dummyOutgoingPayment = pending match { case PendingSpontaneousPayment(_, r) => OutgoingPayment(paymentId, paymentId, r.externalId, paymentHash, paymentType, r.recipientAmount, r.recipientAmount, r.recipientNodeId, TimestampMilli.now(), None, OutgoingPaymentStatus.Pending) - case PendingPaymentToNode(_, r) => OutgoingPayment(paymentId, paymentId, r.externalId, paymentHash, paymentType, r.recipientAmount, r.recipientAmount, r.recipientNodeId, TimestampMilli.now(), Some(r.paymentRequest), OutgoingPaymentStatus.Pending) - case PendingPaymentToRoute(_, r) => OutgoingPayment(paymentId, paymentId, r.externalId, paymentHash, paymentType, r.recipientAmount, r.recipientAmount, r.recipientNodeId, TimestampMilli.now(), Some(r.paymentRequest), OutgoingPaymentStatus.Pending) - case PendingTrampolinePayment(_, _, r) => OutgoingPayment(paymentId, paymentId, None, paymentHash, paymentType, r.recipientAmount, r.recipientAmount, r.recipientNodeId, TimestampMilli.now(), Some(r.paymentRequest), OutgoingPaymentStatus.Pending) + case PendingPaymentToNode(_, r) => OutgoingPayment(paymentId, paymentId, r.externalId, paymentHash, paymentType, r.recipientAmount, r.recipientAmount, r.recipientNodeId, TimestampMilli.now(), Some(r.invoice), OutgoingPaymentStatus.Pending) + case PendingPaymentToRoute(_, r) => OutgoingPayment(paymentId, paymentId, r.externalId, paymentHash, paymentType, r.recipientAmount, r.recipientAmount, r.recipientNodeId, TimestampMilli.now(), Some(r.invoice), OutgoingPaymentStatus.Pending) + case PendingTrampolinePayment(_, _, r) => OutgoingPayment(paymentId, paymentId, None, paymentHash, paymentType, r.recipientAmount, r.recipientAmount, r.recipientNodeId, TimestampMilli.now(), Some(r.invoice), OutgoingPaymentStatus.Pending) } dummyOutgoingPayment +: outgoingDbPayments } @@ -424,16 +425,16 @@ class EclairImpl(appKit: Kit) extends Eclair with Logging { Future(appKit.nodeParams.db.audit.stats(from.toTimestampMilli, to.toTimestampMilli)) } - override def allInvoices(from: TimestampSecond, to: TimestampSecond)(implicit timeout: Timeout): Future[Seq[PaymentRequest]] = Future { - appKit.nodeParams.db.payments.listIncomingPayments(from.toTimestampMilli, to.toTimestampMilli).map(_.paymentRequest) + override def allInvoices(from: TimestampSecond, to: TimestampSecond)(implicit timeout: Timeout): Future[Seq[Bolt11Invoice]] = Future { + appKit.nodeParams.db.payments.listIncomingPayments(from.toTimestampMilli, to.toTimestampMilli).map(_.invoice) } - override def pendingInvoices(from: TimestampSecond, to: TimestampSecond)(implicit timeout: Timeout): Future[Seq[PaymentRequest]] = Future { - appKit.nodeParams.db.payments.listPendingIncomingPayments(from.toTimestampMilli, to.toTimestampMilli).map(_.paymentRequest) + override def pendingInvoices(from: TimestampSecond, to: TimestampSecond)(implicit timeout: Timeout): Future[Seq[Bolt11Invoice]] = Future { + appKit.nodeParams.db.payments.listPendingIncomingPayments(from.toTimestampMilli, to.toTimestampMilli).map(_.invoice) } - override def getInvoice(paymentHash: ByteVector32)(implicit timeout: Timeout): Future[Option[PaymentRequest]] = Future { - appKit.nodeParams.db.payments.getIncomingPayment(paymentHash).map(_.paymentRequest) + override def getInvoice(paymentHash: ByteVector32)(implicit timeout: Timeout): Future[Option[Bolt11Invoice]] = Future { + appKit.nodeParams.db.payments.getIncomingPayment(paymentHash).map(_.invoice) } override def deleteInvoice(paymentHash: ByteVector32): Future[String] = { @@ -540,12 +541,12 @@ class EclairImpl(appKit: Kit) extends Eclair with Logging { codecs.list(TlvCodecs.genericTlv).decode(userCustomContent.bits) match { case Attempt.Successful(DecodeResult(userCustomTlvs, _)) => val replyPathId = randomBytes32() - val replyRoute = replyPath.map(hops => OnionMessages.buildRoute(randomKey(), hops.map(OnionMessages.IntermediateNode(_)), Left(OnionMessages.Recipient(appKit.nodeParams.nodeId, Some(replyPathId))))) + val replyRoute = replyPath.map(hops => OnionMessages.buildRoute(randomKey(), hops.map(OnionMessages.IntermediateNode(_)), OnionMessages.Recipient(appKit.nodeParams.nodeId, Some(replyPathId)))) val (nextNodeId, message) = OnionMessages.buildMessage( randomKey(), randomKey(), intermediateNodes.map(OnionMessages.IntermediateNode(_)), - destination match { case Left(key) => Left(OnionMessages.Recipient(key, None)) case Right(route) => Right(route) }, + destination match { case Left(key) => OnionMessages.Recipient(key, None) case Right(route) => OnionMessages.BlindedPath(route) }, replyRoute.map(OnionMessagePayloadTlv.ReplyPath(_) :: Nil).getOrElse(Nil), userCustomTlvs) appKit.postman.ask(ref => Postman.SendMessage(nextNodeId, message, replyPath.map(_ => replyPathId), ref, appKit.nodeParams.onionMessageConfig.timeout))(timeout, appKit.system.scheduler.toTyped).mapTo[Postman.OnionMessageResponse].map { @@ -558,6 +559,5 @@ class EclairImpl(appKit: Kit) extends Eclair with Logging { } case Attempt.Failure(cause) => Future.successful(SendOnionMessageResponse(sent = false, failureMessage = Some(s"the `content` field is invalid, it must contain encoded tlvs: ${cause.message}"), response = None)) } - } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala index aefea4bd6c..4107f1bd46 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala @@ -87,7 +87,7 @@ case class Features(activated: Map[Feature, FeatureSupport], unknown: Set[Unknow def nodeAnnouncementFeatures(): Features = Features(activated.collect { case (f: NodeFeature, s) => (f: Feature, s) }, unknown) // NB: we don't include unknown features in invoices, which means plugins cannot inject invoice features. - def invoiceFeatures(): Map[Feature with InvoiceFeature, FeatureSupport] = activated.collect { case (f: InvoiceFeature, s) => (f, s) } + def invoiceFeatures(): Features = Features(activated.collect { case (f: InvoiceFeature, s) => (f: Feature, s) }) def toByteVector: ByteVector = { val activatedFeatureBytes = toByteVectorFromIndex(activated.map { case (feature, support) => feature.supportBit(support) }.toSet) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala index 017d8a91b6..e625da73b7 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala @@ -70,7 +70,7 @@ case class NodeParams(nodeKeyManager: NodeKeyManager, initialRandomReconnectDelay: FiniteDuration, maxReconnectInterval: FiniteDuration, chainHash: ByteVector32, - paymentRequestExpiry: FiniteDuration, + invoiceExpiry: FiniteDuration, multiPartPaymentExpiry: FiniteDuration, peerConnectionConf: PeerConnection.Conf, routerConf: RouterConf, @@ -222,6 +222,8 @@ object NodeParams extends Logging { "unhandled-exception-strategy" -> "channel.unhandled-exception-strategy", "revocation-timeout" -> "channel.revocation-timeout", "watch-spent-window" -> "router.watch-spent-window", + // v0.7.1 + "payment-request-expiry" -> "invoice-expiry", ) deprecatedKeyPaths.foreach { case (old, new_) => require(!config.hasPath(old), s"configuration key '$old' has been replaced by '$new_'") @@ -458,7 +460,7 @@ object NodeParams extends Logging { initialRandomReconnectDelay = FiniteDuration(config.getDuration("initial-random-reconnect-delay").getSeconds, TimeUnit.SECONDS), maxReconnectInterval = FiniteDuration(config.getDuration("max-reconnect-interval").getSeconds, TimeUnit.SECONDS), chainHash = chainHash, - paymentRequestExpiry = FiniteDuration(config.getDuration("payment-request-expiry").getSeconds, TimeUnit.SECONDS), + invoiceExpiry = FiniteDuration(config.getDuration("invoice-expiry").getSeconds, TimeUnit.SECONDS), multiPartPaymentExpiry = FiniteDuration(config.getDuration("multi-part-payment-expiry").getSeconds, TimeUnit.SECONDS), peerConnectionConf = PeerConnection.Conf( authTimeout = FiniteDuration(config.getDuration("peer-connection.auth-timeout").getSeconds, TimeUnit.SECONDS), diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/DualDatabases.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/DualDatabases.scala index 29f9b3e1ce..acd3dfae28 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/DualDatabases.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/DualDatabases.scala @@ -319,7 +319,7 @@ case class DualPaymentsDb(primary: PaymentsDb, secondary: PaymentsDb) extends Pa primary.close() } - override def addIncomingPayment(pr: PaymentRequest, preimage: ByteVector32, paymentType: String): Unit = { + override def addIncomingPayment(pr: Bolt11Invoice, preimage: ByteVector32, paymentType: String): Unit = { runAsync(secondary.addIncomingPayment(pr, preimage, paymentType)) primary.addIncomingPayment(pr, preimage, paymentType) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala index fca1b86a66..38cb89dc44 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala @@ -31,11 +31,11 @@ trait PaymentsDb extends IncomingPaymentsDb with OutgoingPaymentsDb with Payment trait IncomingPaymentsDb { /** Add a new expected incoming payment (not yet received). */ - def addIncomingPayment(pr: PaymentRequest, preimage: ByteVector32, paymentType: String = PaymentType.Standard): Unit + def addIncomingPayment(pr: Bolt11Invoice, preimage: ByteVector32, paymentType: String = PaymentType.Standard): Unit /** - * Mark an incoming payment as received (paid). The received amount may exceed the payment request amount. - * If there was no matching payment request in the DB, this will return false. + * Mark an incoming payment as received (paid). The received amount may exceed the invoice amount. + * If there was no matching invoice in the DB, this will return false. */ def receiveIncomingPayment(paymentHash: ByteVector32, amount: MilliSatoshi, receivedAt: TimestampMilli = TimestampMilli.now()): Boolean @@ -96,16 +96,16 @@ case object PaymentType { /** * An incoming payment received by this node. - * At first it is in a pending state once the payment request has been generated, then will become either a success (if - * we receive a valid HTLC) or a failure (if the payment request expires). + * At first it is in a pending state once the invoice has been generated, then will become either a success (if + * we receive a valid HTLC) or a failure (if the invoice expires). * - * @param paymentRequest Bolt 11 payment request. - * @param paymentPreimage pre-image associated with the payment request's payment_hash. + * @param invoice Bolt 11 invoice. + * @param paymentPreimage pre-image associated with the invoice's payment_hash. * @param paymentType distinguish different payment types (standard, swaps, etc). - * @param createdAt absolute time in milli-seconds since UNIX epoch when the payment request was generated. + * @param createdAt absolute time in milli-seconds since UNIX epoch when the invoice was generated. * @param status current status of the payment. */ -case class IncomingPayment(paymentRequest: PaymentRequest, +case class IncomingPayment(invoice: Bolt11Invoice, paymentPreimage: ByteVector32, paymentType: String, createdAt: TimestampMilli, @@ -124,7 +124,7 @@ object IncomingPaymentStatus { /** * Payment has been successfully received. * - * @param amount amount of the payment received, in milli-satoshis (may exceed the payment request amount). + * @param amount amount of the payment received, in milli-satoshis (may exceed the invoice amount). * @param receivedAt absolute time in milli-seconds since UNIX epoch when the payment was received. */ case class Received(amount: MilliSatoshi, receivedAt: TimestampMilli) extends IncomingPaymentStatus @@ -144,7 +144,7 @@ object IncomingPaymentStatus { * @param recipientAmount amount that will be received by the final recipient. * @param recipientNodeId id of the final recipient. * @param createdAt absolute time in milli-seconds since UNIX epoch when the payment was created. - * @param paymentRequest Bolt 11 payment request (if paying from an invoice). + * @param invoice invoice (if paying from an invoice). * @param status current status of the payment. */ case class OutgoingPayment(id: UUID, @@ -156,7 +156,7 @@ case class OutgoingPayment(id: UUID, recipientAmount: MilliSatoshi, recipientNodeId: PublicKey, createdAt: TimestampMilli, - paymentRequest: Option[PaymentRequest], + invoice: Option[Invoice], status: OutgoingPaymentStatus) sealed trait OutgoingPaymentStatus @@ -231,7 +231,7 @@ trait PaymentsOverviewDb { } /** - * Generic payment trait holding only the minimum information in the most plain type possible. Notably, payment request + * Generic payment trait holding only the minimum information in the most plain type possible. Notably, the invoice * is kept as a String, because deserialization is costly. *

* This object should only be used for a high level snapshot of the payments stored in the payment database. @@ -241,7 +241,7 @@ trait PaymentsOverviewDb { sealed trait PlainPayment { val paymentHash: ByteVector32 val paymentType: String - val paymentRequest: Option[String] + val invoice: Option[String] val finalAmount: Option[MilliSatoshi] val createdAt: TimestampMilli val completedAt: Option[TimestampMilli] @@ -250,7 +250,7 @@ sealed trait PlainPayment { case class PlainIncomingPayment(paymentHash: ByteVector32, paymentType: String, finalAmount: Option[MilliSatoshi], - paymentRequest: Option[String], + invoice: Option[String], status: IncomingPaymentStatus, createdAt: TimestampMilli, completedAt: Option[TimestampMilli], @@ -261,7 +261,7 @@ case class PlainOutgoingPayment(parentId: Option[UUID], paymentHash: ByteVector32, paymentType: String, finalAmount: Option[MilliSatoshi], - paymentRequest: Option[String], + invoice: Option[String], status: OutgoingPaymentStatus, createdAt: TimestampMilli, completedAt: Option[TimestampMilli]) extends PlainPayment diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgPaymentsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgPaymentsDb.scala index b50d3656a4..cf7ff97f68 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgPaymentsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgPaymentsDb.scala @@ -23,7 +23,7 @@ import fr.acinq.eclair.db.Monitoring.Tags.DbBackends import fr.acinq.eclair.db.PaymentsDb.{decodeFailures, decodeRoute, encodeFailures, encodeRoute} import fr.acinq.eclair.db._ import fr.acinq.eclair.db.pg.PgUtils.PgLock -import fr.acinq.eclair.payment.{PaymentFailed, PaymentRequest, PaymentSent} +import fr.acinq.eclair.payment.{Bolt11Invoice, PaymentFailed, Invoice, PaymentSent} import fr.acinq.eclair.{MilliSatoshi, TimestampMilli, TimestampMilliLong} import grizzled.slf4j.Logging import scodec.bits.BitVector @@ -106,7 +106,7 @@ class PgPaymentsDb(implicit ds: DataSource, lock: PgLock) extends PaymentsDb wit statement.setLong(7, sent.recipientAmount.toLong) statement.setString(8, sent.recipientNodeId.value.toHex) statement.setTimestamp(9, sent.createdAt.toSqlTimestamp) - statement.setString(10, sent.paymentRequest.map(PaymentRequest.write).orNull) + statement.setString(10, sent.invoice.map(_.toString).orNull) statement.executeUpdate() } } @@ -157,7 +157,7 @@ class PgPaymentsDb(implicit ds: DataSource, lock: PgLock) extends PaymentsDb wit MilliSatoshi(rs.getLong("recipient_amount_msat")), PublicKey(rs.getByteVectorFromHex("recipient_node_id")), TimestampMilli(rs.getTimestamp("created_at").getTime), - rs.getStringNullable("payment_request").map(PaymentRequest.read), + rs.getStringNullable("payment_request").map(Invoice.fromString), status ) } @@ -220,15 +220,15 @@ class PgPaymentsDb(implicit ds: DataSource, lock: PgLock) extends PaymentsDb wit } } - override def addIncomingPayment(pr: PaymentRequest, preimage: ByteVector32, paymentType: String): Unit = withMetrics("payments/add-incoming", DbBackends.Postgres) { + override def addIncomingPayment(invoice: Bolt11Invoice, preimage: ByteVector32, paymentType: String): Unit = withMetrics("payments/add-incoming", DbBackends.Postgres) { withLock { pg => using(pg.prepareStatement("INSERT INTO payments.received (payment_hash, payment_preimage, payment_type, payment_request, created_at, expire_at) VALUES (?, ?, ?, ?, ?, ?)")) { statement => - statement.setString(1, pr.paymentHash.toHex) + statement.setString(1, invoice.paymentHash.toHex) statement.setString(2, preimage.toHex) statement.setString(3, paymentType) - statement.setString(4, PaymentRequest.write(pr)) - statement.setTimestamp(5, pr.timestamp.toSqlTimestamp) - statement.setTimestamp(6, (pr.timestamp + pr.expiry.getOrElse(PaymentRequest.DEFAULT_EXPIRY_SECONDS).seconds).toSqlTimestamp) + statement.setString(4, invoice.toString) + statement.setTimestamp(5, invoice.createdAt.toSqlTimestamp) + statement.setTimestamp(6, (invoice.createdAt + invoice.relativeExpiry.toSeconds).toSqlTimestamp) statement.executeUpdate() } } @@ -247,19 +247,19 @@ class PgPaymentsDb(implicit ds: DataSource, lock: PgLock) extends PaymentsDb wit } private def parseIncomingPayment(rs: ResultSet): IncomingPayment = { - val paymentRequest = rs.getString("payment_request") + val invoice = rs.getString("payment_request") IncomingPayment( - PaymentRequest.read(paymentRequest), + Bolt11Invoice.fromString(invoice), rs.getByteVector32FromHex("payment_preimage"), rs.getString("payment_type"), TimestampMilli.fromSqlTimestamp(rs.getTimestamp("created_at")), - buildIncomingPaymentStatus(rs.getMilliSatoshiNullable("received_msat"), Some(paymentRequest), rs.getTimestampNullable("received_at").map(TimestampMilli.fromSqlTimestamp))) + buildIncomingPaymentStatus(rs.getMilliSatoshiNullable("received_msat"), Some(invoice), rs.getTimestampNullable("received_at").map(TimestampMilli.fromSqlTimestamp))) } - private def buildIncomingPaymentStatus(amount_opt: Option[MilliSatoshi], serializedPaymentRequest_opt: Option[String], receivedAt_opt: Option[TimestampMilli]): IncomingPaymentStatus = { + private def buildIncomingPaymentStatus(amount_opt: Option[MilliSatoshi], serializedInvoice_opt: Option[String], receivedAt_opt: Option[TimestampMilli]): IncomingPaymentStatus = { amount_opt match { case Some(amount) => IncomingPaymentStatus.Received(amount, receivedAt_opt.getOrElse(0 unixms)) - case None if serializedPaymentRequest_opt.exists(PaymentRequest.fastHasExpired) => IncomingPaymentStatus.Expired + case None if serializedInvoice_opt.exists(Bolt11Invoice.fastHasExpired) => IncomingPaymentStatus.Expired case None => IncomingPaymentStatus.Pending } } @@ -388,20 +388,20 @@ class PgPaymentsDb(implicit ds: DataSource, lock: PgLock) extends PaymentsDb wit val externalId_opt = rs.getStringNullable("external_id") val paymentHash = rs.getByteVector32FromHex("payment_hash") val paymentType = rs.getString("payment_type") - val paymentRequest_opt = rs.getStringNullable("payment_request") + val invoice_opt = rs.getStringNullable("payment_request") val amount_opt = rs.getMilliSatoshiNullable("final_amount") val createdAt = TimestampMilli.fromSqlTimestamp(rs.getTimestamp("created_at")) val completedAt_opt = rs.getTimestampNullable("completed_at").map(TimestampMilli.fromSqlTimestamp) val expireAt_opt = rs.getTimestampNullable("expire_at").map(TimestampMilli.fromSqlTimestamp) if (rs.getString("type") == "received") { - val status: IncomingPaymentStatus = buildIncomingPaymentStatus(amount_opt, paymentRequest_opt, completedAt_opt) - PlainIncomingPayment(paymentHash, paymentType, amount_opt, paymentRequest_opt, status, createdAt, completedAt_opt, expireAt_opt) + val status: IncomingPaymentStatus = buildIncomingPaymentStatus(amount_opt, invoice_opt, completedAt_opt) + PlainIncomingPayment(paymentHash, paymentType, amount_opt, invoice_opt, status, createdAt, completedAt_opt, expireAt_opt) } else { val preimage_opt = rs.getByteVector32Nullable("payment_preimage") // note that the resulting status will not contain any details (routes, failures...) val status: OutgoingPaymentStatus = buildOutgoingPaymentStatus(preimage_opt, None, None, completedAt_opt, None) - PlainOutgoingPayment(parentId, externalId_opt, paymentHash, paymentType, amount_opt, paymentRequest_opt, status, createdAt, completedAt_opt) + PlainOutgoingPayment(parentId, externalId_opt, paymentHash, paymentType, amount_opt, invoice_opt, status, createdAt, completedAt_opt) } }.toSeq } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala index fbaef7851a..aad5e3a129 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala @@ -23,7 +23,7 @@ import fr.acinq.eclair.db.Monitoring.Tags.DbBackends import fr.acinq.eclair.db.PaymentsDb.{decodeFailures, decodeRoute, encodeFailures, encodeRoute} import fr.acinq.eclair.db._ import fr.acinq.eclair.db.sqlite.SqliteUtils._ -import fr.acinq.eclair.payment.{PaymentFailed, PaymentRequest, PaymentSent} +import fr.acinq.eclair.payment.{Bolt11Invoice, PaymentFailed, Invoice, PaymentSent} import fr.acinq.eclair.{MilliSatoshi, TimestampMilli, TimestampMilliLong} import grizzled.slf4j.Logging import scodec.bits.BitVector @@ -58,9 +58,9 @@ class SqlitePaymentsDb(val sqlite: Connection) extends PaymentsDb with Logging { statement.executeUpdate("DROP table _sent_payments_old") statement.executeUpdate("ALTER TABLE received_payments RENAME TO _received_payments_old") - // We make payment request expiration not null in the received_payments table. + // We make invoice expiration not null in the received_payments table. // When it was previously set to NULL the default expiry should apply. - statement.executeUpdate(s"UPDATE _received_payments_old SET expire_at = created_at + ${PaymentRequest.DEFAULT_EXPIRY_SECONDS} WHERE expire_at IS NULL") + statement.executeUpdate(s"UPDATE _received_payments_old SET expire_at = created_at + ${Bolt11Invoice.DEFAULT_EXPIRY_SECONDS} WHERE expire_at IS NULL") statement.executeUpdate("CREATE TABLE received_payments (payment_hash BLOB NOT NULL PRIMARY KEY, payment_preimage BLOB NOT NULL, payment_request TEXT NOT NULL, received_msat INTEGER, created_at INTEGER NOT NULL, expire_at INTEGER NOT NULL, received_at INTEGER)") statement.executeUpdate("INSERT INTO received_payments (payment_hash, payment_preimage, payment_request, received_msat, created_at, expire_at, received_at) SELECT payment_hash, preimage, payment_request, received_msat, created_at, expire_at, received_at FROM _received_payments_old") statement.executeUpdate("DROP table _received_payments_old") @@ -132,7 +132,7 @@ class SqlitePaymentsDb(val sqlite: Connection) extends PaymentsDb with Logging { statement.setLong(7, sent.recipientAmount.toLong) statement.setBytes(8, sent.recipientNodeId.value.toArray) statement.setLong(9, sent.createdAt.toLong) - statement.setString(10, sent.paymentRequest.map(PaymentRequest.write).orNull) + statement.setString(10, sent.invoice.map(_.toString).orNull) statement.executeUpdate() } } @@ -178,7 +178,7 @@ class SqlitePaymentsDb(val sqlite: Connection) extends PaymentsDb with Logging { MilliSatoshi(rs.getLong("recipient_amount_msat")), PublicKey(rs.getByteVector("recipient_node_id")), TimestampMilli(rs.getLong("created_at")), - rs.getStringNullable("payment_request").map(PaymentRequest.read), + rs.getStringNullable("payment_request").map(Invoice.fromString), status ) } @@ -231,14 +231,14 @@ class SqlitePaymentsDb(val sqlite: Connection) extends PaymentsDb with Logging { } } - override def addIncomingPayment(pr: PaymentRequest, preimage: ByteVector32, paymentType: String): Unit = withMetrics("payments/add-incoming", DbBackends.Sqlite) { + override def addIncomingPayment(invoice: Bolt11Invoice, preimage: ByteVector32, paymentType: String): Unit = withMetrics("payments/add-incoming", DbBackends.Sqlite) { using(sqlite.prepareStatement("INSERT INTO received_payments (payment_hash, payment_preimage, payment_type, payment_request, created_at, expire_at) VALUES (?, ?, ?, ?, ?, ?)")) { statement => - statement.setBytes(1, pr.paymentHash.toArray) + statement.setBytes(1, invoice.paymentHash.toArray) statement.setBytes(2, preimage.toArray) statement.setString(3, paymentType) - statement.setString(4, PaymentRequest.write(pr)) - statement.setLong(5, pr.timestamp.toTimestampMilli.toLong) // BOLT11 timestamp is in seconds - statement.setLong(6, (pr.timestamp + pr.expiry.getOrElse(PaymentRequest.DEFAULT_EXPIRY_SECONDS)).toLong.seconds.toMillis) + statement.setString(4, invoice.toString) + statement.setLong(5, invoice.createdAt.toTimestampMilli.toLong) // BOLT11 timestamp is in seconds + statement.setLong(6, (invoice.createdAt + invoice.relativeExpiry).toLong.seconds.toMillis) statement.executeUpdate() } } @@ -254,19 +254,19 @@ class SqlitePaymentsDb(val sqlite: Connection) extends PaymentsDb with Logging { } private def parseIncomingPayment(rs: ResultSet): IncomingPayment = { - val paymentRequest = rs.getString("payment_request") + val invoice = rs.getString("payment_request") IncomingPayment( - PaymentRequest.read(paymentRequest), + Bolt11Invoice.fromString(invoice), rs.getByteVector32("payment_preimage"), rs.getString("payment_type"), TimestampMilli(rs.getLong("created_at")), - buildIncomingPaymentStatus(rs.getMilliSatoshiNullable("received_msat"), Some(paymentRequest), rs.getLongNullable("received_at").map(TimestampMilli(_)))) + buildIncomingPaymentStatus(rs.getMilliSatoshiNullable("received_msat"), Some(invoice), rs.getLongNullable("received_at").map(TimestampMilli(_)))) } - private def buildIncomingPaymentStatus(amount_opt: Option[MilliSatoshi], serializedPaymentRequest_opt: Option[String], receivedAt_opt: Option[TimestampMilli]): IncomingPaymentStatus = { + private def buildIncomingPaymentStatus(amount_opt: Option[MilliSatoshi], serializedInvoice_opt: Option[String], receivedAt_opt: Option[TimestampMilli]): IncomingPaymentStatus = { amount_opt match { case Some(amount) => IncomingPaymentStatus.Received(amount, receivedAt_opt.getOrElse(0 unixms)) - case None if serializedPaymentRequest_opt.exists(PaymentRequest.fastHasExpired) => IncomingPaymentStatus.Expired + case None if serializedInvoice_opt.exists(Bolt11Invoice.fastHasExpired) => IncomingPaymentStatus.Expired case None => IncomingPaymentStatus.Pending } } @@ -378,20 +378,20 @@ class SqlitePaymentsDb(val sqlite: Connection) extends PaymentsDb with Logging { val externalId_opt = rs.getStringNullable("external_id") val paymentHash = rs.getByteVector32("payment_hash") val paymentType = rs.getString("payment_type") - val paymentRequest_opt = rs.getStringNullable("payment_request") + val invoice_opt = rs.getStringNullable("payment_request") val amount_opt = rs.getMilliSatoshiNullable("final_amount") val createdAt = TimestampMilli(rs.getLong("created_at")) val completedAt_opt = rs.getLongNullable("completed_at").map(TimestampMilli(_)) val expireAt_opt = rs.getLongNullable("expire_at").map(TimestampMilli(_)) if (rs.getString("type") == "received") { - val status: IncomingPaymentStatus = buildIncomingPaymentStatus(amount_opt, paymentRequest_opt, completedAt_opt) - PlainIncomingPayment(paymentHash, paymentType, amount_opt, paymentRequest_opt, status, createdAt, completedAt_opt, expireAt_opt) + val status: IncomingPaymentStatus = buildIncomingPaymentStatus(amount_opt, invoice_opt, completedAt_opt) + PlainIncomingPayment(paymentHash, paymentType, amount_opt, invoice_opt, status, createdAt, completedAt_opt, expireAt_opt) } else { val preimage_opt = rs.getByteVector32Nullable("payment_preimage") // note that the resulting status will not contain any details (routes, failures...) val status: OutgoingPaymentStatus = buildOutgoingPaymentStatus(preimage_opt, None, None, completedAt_opt, None) - PlainOutgoingPayment(parentId, externalId_opt, paymentHash, paymentType, amount_opt, paymentRequest_opt, status, createdAt, completedAt_opt) + PlainOutgoingPayment(parentId, externalId_opt, paymentHash, paymentType, amount_opt, invoice_opt, status, createdAt, completedAt_opt) } }.toSeq } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/json/JsonSerializers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/json/JsonSerializers.scala index 48852b5824..ac900995ce 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/json/JsonSerializers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/json/JsonSerializers.scala @@ -339,12 +339,12 @@ private case class DirectedHtlcJson(direction: String, add: UpdateAddHtlc) object DirectedHtlcSerializer extends ConvertClassSerializer[DirectedHtlc](h => DirectedHtlcJson(direction = h.direction, add = h.add)) // @formatter:on -object PaymentRequestSerializer extends MinimalSerializer({ - case p: PaymentRequest => - val expiry = p.expiry.map(ex => JField("expiry", JLong(ex))).toSeq +object InvoiceSerializer extends MinimalSerializer({ + case p: Bolt11Invoice => + val expiry = p.relativeExpiry_opt.map(ex => JField("expiry", JLong(ex))).toSeq val minFinalCltvExpiry = p.minFinalCltvExpiryDelta.map(mfce => JField("minFinalCltvExpiry", JInt(mfce.toInt))).toSeq - val amount = p.amount.map(msat => JField("amount", JLong(msat.toLong))).toSeq - val features = JField("features", Extraction.decompose(p.features.features)( + val amount = p.amount_opt.map(msat => JField("amount", JLong(msat.toLong))).toSeq + val features = JField("features", Extraction.decompose(p.features)( DefaultFormats + FeatureKeySerializer + FeatureSupportSerializer + @@ -362,9 +362,9 @@ object PaymentRequestSerializer extends MinimalSerializer({ )) val fieldList = List( JField("prefix", JString(p.prefix)), - JField("timestamp", JLong(p.timestamp.toLong)), + JField("timestamp", JLong(p.createdAt.toLong)), JField("nodeId", JString(p.nodeId.toString())), - JField("serialized", JString(PaymentRequest.write(p))), + JField("serialized", JString(p.toString)), p.description.fold(string => JField("description", JString(string)), hash => JField("descriptionHash", JString(hash.toHex))), JField("paymentHash", JString(p.paymentHash.toString()))) ++ paymentMetadata ++ @@ -533,7 +533,7 @@ object JsonSerializers { FailureTypeSerializer + NodeAddressSerializer + DirectedHtlcSerializer + - PaymentRequestSerializer + + InvoiceSerializer + JavaUUIDSerializer + OriginSerializer + ByteVector32KeySerializer + diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/message/OnionMessages.scala b/eclair-core/src/main/scala/fr/acinq/eclair/message/OnionMessages.scala index 5af9696ea8..417b49631d 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/message/OnionMessages.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/message/OnionMessages.scala @@ -16,6 +16,7 @@ package fr.acinq.eclair.message +import fr.acinq.bitcoin.ByteVector32 import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} import fr.acinq.eclair.crypto.Sphinx import fr.acinq.eclair.io.MessageRelay.RelayPolicy @@ -32,18 +33,21 @@ import scala.util.{Failure, Success} object OnionMessages { - case class OnionMessageConfig(relayPolicy: RelayPolicy, timeout: FiniteDuration) + case class OnionMessageConfig(relayPolicy: RelayPolicy, + timeout: FiniteDuration) case class IntermediateNode(nodeId: PublicKey, padding: Option[ByteVector] = None) - case class Recipient(nodeId: PublicKey, pathId: Option[ByteVector], padding: Option[ByteVector] = None) + sealed trait Destination + case class BlindedPath(route: Sphinx.RouteBlinding.BlindedRoute) extends Destination + case class Recipient(nodeId: PublicKey, pathId: Option[ByteVector], padding: Option[ByteVector] = None) extends Destination def buildRoute(blindingSecret: PrivateKey, intermediateNodes: Seq[IntermediateNode], - destination: Either[Recipient, Sphinx.RouteBlinding.BlindedRoute]): Sphinx.RouteBlinding.BlindedRoute = { + destination: Destination): Sphinx.RouteBlinding.BlindedRoute = { val last = destination match { - case Left(Recipient(nodeId, _, _)) => OutgoingNodeId(nodeId) :: Nil - case Right(Sphinx.RouteBlinding.BlindedRoute(nodeId, blindingKey, _)) => OutgoingNodeId(nodeId) :: NextBlinding(blindingKey) :: Nil + case Recipient(nodeId, _, _) => OutgoingNodeId(nodeId) :: Nil + case BlindedPath(Sphinx.RouteBlinding.BlindedRoute(nodeId, blindingKey, _)) => OutgoingNodeId(nodeId) :: NextBlinding(blindingKey) :: Nil } val intermediatePayloads = if (intermediateNodes.isEmpty) { @@ -55,11 +59,11 @@ object OnionMessages { .map(MessageOnionCodecs.blindedRelayPayloadCodec.encode(_).require.bytes) } destination match { - case Left(Recipient(nodeId, pathId, padding)) => + case Recipient(nodeId, pathId, padding) => val tlvs = padding.map(Padding(_) :: Nil).getOrElse(Nil) ++ pathId.map(PathId(_) :: Nil).getOrElse(Nil) val lastPayload = MessageOnionCodecs.blindedFinalPayloadCodec.encode(BlindedFinalPayload(TlvStream(tlvs))).require.bytes Sphinx.RouteBlinding.create(blindingSecret, intermediateNodes.map(_.nodeId) :+ nodeId, intermediatePayloads :+ lastPayload) - case Right(route) => + case BlindedPath(route) => if (intermediateNodes.isEmpty) { route } else { @@ -82,7 +86,7 @@ object OnionMessages { def buildMessage(sessionKey: PrivateKey, blindingSecret: PrivateKey, intermediateNodes: Seq[IntermediateNode], - destination: Either[Recipient, Sphinx.RouteBlinding.BlindedRoute], + destination: Destination, content: Seq[OnionMessagePayloadTlv], userCustomTlvs: Seq[GenericTlv] = Nil): (PublicKey, OnionMessage) = { val route = buildRoute(blindingSecret, intermediateNodes, destination) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentRequest.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Bolt11Invoice.scala similarity index 76% rename from eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentRequest.scala rename to eclair-core/src/main/scala/fr/acinq/eclair/payment/Bolt11Invoice.scala index 6557e89433..87d7411368 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentRequest.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Bolt11Invoice.scala @@ -18,89 +18,89 @@ package fr.acinq.eclair.payment import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} import fr.acinq.bitcoin.{Base58, Base58Check, Bech32, Block, ByteVector32, ByteVector64, Crypto} -import fr.acinq.eclair.payment.PaymentRequest._ -import fr.acinq.eclair.{CltvExpiryDelta, Feature, FeatureSupport, Features, InvoiceFeature, MilliSatoshi, MilliSatoshiLong, NodeParams, ShortChannelId, TimestampSecond, randomBytes32} +import fr.acinq.eclair.{CltvExpiryDelta, FeatureSupport, Features, MilliSatoshi, MilliSatoshiLong, ShortChannelId, TimestampSecond, randomBytes32} import scodec.bits.{BitVector, ByteOrdering, ByteVector} import scodec.codecs.{list, ubyte} import scodec.{Codec, Err} +import java.util.concurrent.TimeUnit +import scala.concurrent.duration.FiniteDuration import scala.util.{Failure, Success, Try} /** - * Lightning Payment Request + * Lightning Bolt 11 invoice * see https://github.com/lightningnetwork/lightning-rfc/blob/master/11-payment-encoding.md * - * @param prefix currency prefix; lnbc for bitcoin, lntb for bitcoin testnet - * @param amount amount to pay (empty string means no amount is specified) - * @param timestamp request timestamp (UNIX format) - * @param nodeId id of the node emitting the payment request - * @param tags payment tags; must include a single PaymentHash tag and a single PaymentSecret tag. - * @param signature request signature that will be checked against node id + * @param prefix currency prefix; lnbc for bitcoin, lntb for bitcoin testnet + * @param amount_opt amount to pay (empty string means no amount is specified) + * @param createdAt invoice timestamp (UNIX format) + * @param nodeId id of the node emitting the invoice + * @param tags payment tags; must include a single PaymentHash tag and a single PaymentSecret tag. + * @param signature invoice signature that will be checked against node id */ -case class PaymentRequest(prefix: String, amount: Option[MilliSatoshi], timestamp: TimestampSecond, nodeId: PublicKey, tags: List[PaymentRequest.TaggedField], signature: ByteVector) { +case class Bolt11Invoice(prefix: String, amount_opt: Option[MilliSatoshi], createdAt: TimestampSecond, nodeId: PublicKey, tags: List[Bolt11Invoice.TaggedField], signature: ByteVector) extends Invoice { - amount.foreach(a => require(a > 0.msat, s"amount is not valid")) - require(tags.collect { case _: PaymentRequest.PaymentHash => }.size == 1, "there must be exactly one payment hash tag") - require(tags.collect { case PaymentRequest.Description(_) | PaymentRequest.DescriptionHash(_) => }.size == 1, "there must be exactly one description tag or one description hash tag") - private val featuresErr = Features.validateFeatureGraph(features.features) + import fr.acinq.eclair.payment.Bolt11Invoice._ + + amount_opt.foreach(a => require(a > 0.msat, s"amount is not valid")) + require(tags.collect { case _: Bolt11Invoice.PaymentHash => }.size == 1, "there must be exactly one payment hash tag") + require(tags.collect { case Bolt11Invoice.Description(_) | Bolt11Invoice.DescriptionHash(_) => }.size == 1, "there must be exactly one description tag or one description hash tag") + private val featuresErr = Features.validateFeatureGraph(features) require(featuresErr.isEmpty, featuresErr.map(_.message)) - if (features.allowPaymentSecret) { - require(tags.collect { case _: PaymentRequest.PaymentSecret => }.size == 1, "there must be exactly one payment secret tag when feature bit is set") + if (features.hasFeature(Features.PaymentSecret)) { + require(tags.collect { case _: Bolt11Invoice.PaymentSecret => }.size == 1, "there must be exactly one payment secret tag when feature bit is set") } /** * @return the payment hash */ - lazy val paymentHash = tags.collectFirst { case p: PaymentRequest.PaymentHash => p.hash }.get + lazy val paymentHash = tags.collectFirst { case p: Bolt11Invoice.PaymentHash => p.hash }.get /** * @return the payment secret */ - lazy val paymentSecret = tags.collectFirst { case p: PaymentRequest.PaymentSecret => p.secret } + lazy val paymentSecret = tags.collectFirst { case p: Bolt11Invoice.PaymentSecret => p.secret } /** * @return the description of the payment, or its hash */ lazy val description: Either[String, ByteVector32] = tags.collectFirst { - case PaymentRequest.Description(d) => Left(d) - case PaymentRequest.DescriptionHash(h) => Right(h) + case Bolt11Invoice.Description(d) => Left(d) + case Bolt11Invoice.DescriptionHash(h) => Right(h) }.get /** * @return metadata about the payment (see option_payment_metadata). */ - lazy val paymentMetadata: Option[ByteVector] = tags.collectFirst { case m: PaymentRequest.PaymentMetadata => m.data } + lazy val paymentMetadata: Option[ByteVector] = tags.collectFirst { case m: Bolt11Invoice.PaymentMetadata => m.data } /** * @return the fallback address if any. It could be a script address, pubkey address, .. */ def fallbackAddress(): Option[String] = tags.collectFirst { - case f: PaymentRequest.FallbackAddress => PaymentRequest.FallbackAddress.toAddress(f, prefix) + case f: Bolt11Invoice.FallbackAddress => Bolt11Invoice.FallbackAddress.toAddress(f, prefix) } lazy val routingInfo: Seq[Seq[ExtraHop]] = tags.collect { case t: RoutingInfo => t.path } - lazy val expiry: Option[Long] = tags.collectFirst { - case expiry: PaymentRequest.Expiry => expiry.toLong + lazy val relativeExpiry_opt: Option[Long] = tags.collectFirst { + case expiry: Bolt11Invoice.Expiry => expiry.toLong } + lazy val relativeExpiry: FiniteDuration = FiniteDuration(relativeExpiry_opt.getOrElse(DEFAULT_EXPIRY_SECONDS), TimeUnit.SECONDS) + lazy val minFinalCltvExpiryDelta: Option[CltvExpiryDelta] = tags.collectFirst { - case cltvExpiry: PaymentRequest.MinFinalCltvExpiry => cltvExpiry.toCltvExpiryDelta + case cltvExpiry: Bolt11Invoice.MinFinalCltvExpiry => cltvExpiry.toCltvExpiryDelta } - lazy val features: PaymentRequestFeatures = tags.collectFirst { case f: PaymentRequestFeatures => f }.getOrElse(PaymentRequestFeatures(BitVector.empty)) - - def isExpired: Boolean = expiry match { - case Some(expiryTime) => timestamp + expiryTime <= TimestampSecond.now() - case None => timestamp + DEFAULT_EXPIRY_SECONDS <= TimestampSecond.now() - } + lazy val features: Features = tags.collectFirst { case f: InvoiceFeatures => f.features }.getOrElse(Features(BitVector.empty)) /** - * @return the hash of this payment request + * @return the hash of this payment invoice */ def hash: ByteVector32 = { - val hrp = s"$prefix${Amount.encode(amount)}".getBytes("UTF-8") - val data = Bolt11Data(timestamp, tags, ByteVector.fill(65)(0)) // fake sig that we are going to strip next + val hrp = s"$prefix${Amount.encode(amount_opt)}".getBytes("UTF-8") + val data = Bolt11Data(createdAt, tags, ByteVector.fill(65)(0)) // fake sig that we are going to strip next val bin = Codecs.bolt11DataCodec.encode(data).require val message = ByteVector.view(hrp) ++ bin.dropRight(520).toByteVector Crypto.sha256(message) @@ -108,9 +108,9 @@ case class PaymentRequest(prefix: String, amount: Option[MilliSatoshi], timestam /** * @param priv private key - * @return a signed payment request + * @return a signed payment invoice */ - def sign(priv: PrivateKey): PaymentRequest = { + def sign(priv: PrivateKey): Bolt11Invoice = { val sig64 = Crypto.sign(hash, priv) // in order to tell what the recovery id is, we actually recover the pubkey ourselves and compare it to the real one val pub0 = Crypto.recoverPublicKey(sig64, hash, 0.toByte) @@ -118,10 +118,21 @@ case class PaymentRequest(prefix: String, amount: Option[MilliSatoshi], timestam val signature = sig64 :+ recid this.copy(signature = signature) } -} -object PaymentRequest { + /** + * @return a bech32-encoded payment invoice + */ + override def toString: String = { + // currency unit is Satoshi, but we compute amounts in Millisatoshis + val hramount = Amount.encode(amount_opt) + val hrp = s"${prefix}$hramount" + val data = Codecs.bolt11DataCodec.encode(Bolt11Data(createdAt, tags, signature)).require + val int5s = eight2fiveCodec.decode(data).require.value + Bech32.encode(hrp, int5s.toArray) + } +} +object Bolt11Invoice { val DEFAULT_EXPIRY_SECONDS: Long = 3600 val prefixes = Map( @@ -130,10 +141,7 @@ object PaymentRequest { Block.LivenetGenesisBlock.hash -> "lnbc" ) - val defaultFeatures: Map[Feature with InvoiceFeature, FeatureSupport] = Map( - Features.VariableLengthOnion -> FeatureSupport.Mandatory, - Features.PaymentSecret -> FeatureSupport.Mandatory, - ) + val defaultFeatures: Features = Features((Features.VariableLengthOnion, FeatureSupport.Mandatory), (Features.PaymentSecret, FeatureSupport.Mandatory)) def apply(chainHash: ByteVector32, amount: Option[MilliSatoshi], @@ -147,8 +155,8 @@ object PaymentRequest { timestamp: TimestampSecond = TimestampSecond.now(), paymentSecret: ByteVector32 = randomBytes32(), paymentMetadata: Option[ByteVector] = None, - features: PaymentRequestFeatures = PaymentRequestFeatures(defaultFeatures)): PaymentRequest = { - require(features.requirePaymentSecret, "invoices must require a payment secret") + features: Features = defaultFeatures): Bolt11Invoice = { + require(features.hasFeature(Features.PaymentSecret, Some(FeatureSupport.Mandatory)), "invoices must require a payment secret") val prefix = prefixes(chainHash) val tags = { val defaultTags = List( @@ -159,15 +167,15 @@ object PaymentRequest { fallbackAddress.map(FallbackAddress(_)), expirySeconds.map(Expiry(_)), Some(MinFinalCltvExpiry(minFinalCltvExpiryDelta.toInt)), - Some(features) + Some(InvoiceFeatures(features)) ).flatten val routingInfoTags = extraHops.map(RoutingInfo) defaultTags ++ routingInfoTags } - PaymentRequest( + Bolt11Invoice( prefix = prefix, - amount = amount, - timestamp = timestamp, + amount_opt = amount, + createdAt = timestamp, nodeId = privateKey.publicKey, tags = tags, signature = ByteVector.empty @@ -227,15 +235,15 @@ object PaymentRequest { /** * Description * - * @param description a free-format string that will be included in the payment request + * @param description a free-format string that will be included in the invoice */ case class Description(description: String) extends TaggedField /** * Hash * - * @param hash hash that will be included in the payment request, and can be checked against the hash of a - * long description, an invoice, ... + * @param hash hash that will be included in the invoice, and can be checked against the hash of a + * long description, an SKU, ... */ case class DescriptionHash(hash: ByteVector32) extends TaggedField @@ -288,16 +296,23 @@ object PaymentRequest { } /** - * This returns a bitvector with the minimum size necessary to encode the long, left padded - * to have a length (in bits) multiples of 5 + * This returns a bitvector with the minimum size necessary to encode the long, left padded to have a length (in bits) + * that is a multiple of 5. + */ + def long2bits(l: Long): BitVector = leftPaddedBits(BitVector.fromLong(l)) + + /** + * This returns a bitvector with the minimum size necessary to encode the features, left padded to have a length (in + * bits) that is a multiple of 5. */ - def long2bits(l: Long) = { - val bin = BitVector.fromLong(l) + def features2bits(features: Features): BitVector = leftPaddedBits(features.toByteVector.bits) + + private def leftPaddedBits(bits: BitVector): BitVector = { var highest = -1 - for (i <- 0 until bin.size.toInt) { - if (highest == -1 && bin(i)) highest = i + for (i <- 0 until bits.size.toInt) { + if (highest == -1 && bits(i)) highest = i } - val nonPadded = if (highest == -1) BitVector.empty else bin.drop(highest) + val nonPadded = if (highest == -1) BitVector.empty else bits.drop(highest) nonPadded.size % 5 match { case 0 => nonPadded case remaining => BitVector.fill(5 - remaining)(high = false) ++ nonPadded @@ -315,6 +330,7 @@ object PaymentRequest { */ case class ExtraHop(nodeId: PublicKey, shortChannelId: ShortChannelId, feeBase: MilliSatoshi, feeProportionalMillionths: Long, cltvExpiryDelta: CltvExpiryDelta) + /** * Routing Info * @@ -331,7 +347,7 @@ object PaymentRequest { object Expiry { /** - * @param seconds expiry data for this payment request + * @param seconds expiry data for this invoice */ def apply(seconds: Long): Expiry = Expiry(long2bits(seconds)) } @@ -355,25 +371,7 @@ object PaymentRequest { /** * Features supported or required for receiving this payment. */ - case class PaymentRequestFeatures(bitmask: BitVector) extends TaggedField { - lazy val features: Features = Features(bitmask) - lazy val allowMultiPart: Boolean = features.hasFeature(Features.BasicMultiPartPayment) - lazy val allowPaymentSecret: Boolean = features.hasFeature(Features.PaymentSecret) - lazy val requirePaymentSecret: Boolean = features.hasFeature(Features.PaymentSecret, Some(FeatureSupport.Mandatory)) - lazy val allowTrampoline: Boolean = features.hasFeature(Features.TrampolinePayment) - - def toByteVector: ByteVector = features.toByteVector - - def areSupported(nodeParams: NodeParams): Boolean = nodeParams.features.areSupported(features) - - override def toString: String = s"Features(${bitmask.toBin})" - } - - object PaymentRequestFeatures { - def apply(features: Map[Feature with InvoiceFeature, FeatureSupport]): PaymentRequestFeatures = PaymentRequestFeatures(long2bits(features.foldLeft(0L) { - case (current, (feature, support)) => current + (1L << feature.supportBit(support)) - })) - } + case class InvoiceFeatures(features: Features) extends TaggedField object Codecs { @@ -416,7 +414,7 @@ object PaymentRequest { .typecase(2, dataCodec(bits).as[UnknownTag2]) .typecase(3, dataCodec(listOfN(extraHopsLengthCodec, extraHopCodec)).as[RoutingInfo]) .typecase(4, dataCodec(bits).as[UnknownTag4]) - .typecase(5, dataCodec(bits).as[PaymentRequestFeatures]) + .typecase(5, dataCodec(bits).xmap[Features](Features(_), features2bits).as[InvoiceFeatures]) .typecase(6, dataCodec(bits).as[Expiry]) .typecase(7, dataCodec(bits).as[UnknownTag7]) .typecase(8, dataCodec(bits).as[UnknownTag8]) @@ -512,10 +510,10 @@ object PaymentRequest { val eight2fiveCodec: Codec[List[Byte]] = list(ubyte(5)) /** - * @param input bech32-encoded payment request - * @return a payment request + * @param input bech32-encoded invoice + * @return a Bolt11 invoice */ - def read(input: String): PaymentRequest = { + def fromString(input: String): Bolt11Invoice = { // used only for data validation Bech32.decode(input) val lowercaseInput = input.toLowerCase @@ -534,10 +532,10 @@ object PaymentRequest { case Success(value) => value case Failure(e) => throw e } - PaymentRequest( + Bolt11Invoice( prefix = prefix, - amount = amount_opt, - timestamp = bolt11Data.timestamp, + amount_opt = amount_opt, + createdAt = bolt11Data.timestamp, nodeId = pub, tags = bolt11Data.taggedFields, signature = bolt11Data.signature) @@ -553,29 +551,29 @@ object PaymentRequest { } /** - * Extracts the description from a serialized payment request that is **expected to be valid**. - * Throws an error if the payment request is not valid. + * Extracts the description from a serialized invoice that is **expected to be valid**. + * Throws an error if the invoice is not valid. * - * @param input valid serialized payment request + * @param input valid serialized invoice * @return description as a String. If the description is a hash, returns the hash value as a String. */ def fastReadDescription(input: String): String = { readBoltData(input).taggedFields.collectFirst { - case PaymentRequest.Description(d) => d - case PaymentRequest.DescriptionHash(h) => h.toString() + case Bolt11Invoice.Description(d) => d + case Bolt11Invoice.DescriptionHash(h) => h.toString() }.get } /** - * Checks if a serialized payment request is expired. Timestamp is compared to the System's current time. + * Checks if a serialized invoice is expired. Timestamp is compared to the System's current time. * - * @param input valid serialized payment request - * @return true if the payment request has expired, false otherwise. + * @param input valid serialized invoice + * @return true if the invoice has expired, false otherwise. */ def fastHasExpired(input: String): Boolean = { val bolt11Data = readBoltData(input) val expiry_opt = bolt11Data.taggedFields.collectFirst { - case p: PaymentRequest.Expiry => p + case p: Bolt11Invoice.Expiry => p } val timestamp = bolt11Data.timestamp expiry_opt match { @@ -583,17 +581,4 @@ object PaymentRequest { case None => timestamp + DEFAULT_EXPIRY_SECONDS <= TimestampSecond.now() } } - - /** - * @param pr payment request - * @return a bech32-encoded payment request - */ - def write(pr: PaymentRequest): String = { - // currency unit is Satoshi, but we compute amounts in Millisatoshis - val hramount = Amount.encode(pr.amount) - val hrp = s"${pr.prefix}$hramount" - val data = Codecs.bolt11DataCodec.encode(Bolt11Data(pr.timestamp, pr.tags, pr.signature)).require - val int5s = eight2fiveCodec.decode(data).require.value - Bech32.encode(hrp, int5s.toArray) - } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Invoice.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Invoice.scala new file mode 100644 index 0000000000..78c0804612 --- /dev/null +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Invoice.scala @@ -0,0 +1,58 @@ +/* + * 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.payment + +import fr.acinq.bitcoin.ByteVector32 +import fr.acinq.bitcoin.Crypto.PublicKey +import fr.acinq.eclair.{CltvExpiryDelta, Features, MilliSatoshi, TimestampSecond} +import scodec.bits.ByteVector + +import scala.concurrent.duration.FiniteDuration + +trait Invoice { + val amount_opt: Option[MilliSatoshi] + + val createdAt: TimestampSecond + + val nodeId: PublicKey + + val paymentHash: ByteVector32 + + val paymentSecret: Option[ByteVector32] + + val paymentMetadata: Option[ByteVector] + + val description: Either[String, ByteVector32] + + val routingInfo: Seq[Seq[Bolt11Invoice.ExtraHop]] + + val relativeExpiry: FiniteDuration + + val minFinalCltvExpiryDelta: Option[CltvExpiryDelta] + + val features: Features + + def isExpired(): Boolean = createdAt + relativeExpiry.toSeconds <= TimestampSecond.now() + + def toString: String +} + +object Invoice { + def fromString(input: String): Invoice = { + Bolt11Invoice.fromString(input) + } +} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentEvents.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentEvents.scala index 933b4bfbf7..85ad38af4b 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentEvents.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentEvents.scala @@ -19,7 +19,7 @@ package fr.acinq.eclair.payment import fr.acinq.bitcoin.ByteVector32 import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.eclair.crypto.Sphinx -import fr.acinq.eclair.payment.PaymentRequest.ExtraHop +import fr.acinq.eclair.payment.Bolt11Invoice.ExtraHop import fr.acinq.eclair.payment.send.PaymentInitiator.SendPaymentConfig import fr.acinq.eclair.router.Announcements import fr.acinq.eclair.router.Router.{ChannelDesc, ChannelHop, Hop, Ignore} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentPacket.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentPacket.scala index f761c625eb..9294dfe12f 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentPacket.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentPacket.scala @@ -159,7 +159,7 @@ object OutgoingPaymentPacket { /** * Build the onion payloads for each hop. * - * @param hops the hops as computed by the router + extra routes from payment request + * @param hops the hops as computed by the router + extra routes from the invoice * @param finalPayload payload data for the final node (amount, expiry, etc) * @return a (firstAmount, firstExpiry, payloads) tuple where: * - firstAmount is the amount for the first htlc in the route @@ -180,7 +180,7 @@ object OutgoingPaymentPacket { /** * Build an encrypted onion packet with the given final payload. * - * @param hops the hops as computed by the router + extra routes from payment request, including ourselves in the first hop + * @param hops the hops as computed by the router + extra routes from the invoice, including ourselves in the first hop * @param finalPayload payload data for the final node (amount, expiry, etc) * @return a (firstAmount, firstExpiry, onion) tuple where: * - firstAmount is the amount for the first htlc in the route @@ -212,7 +212,7 @@ object OutgoingPaymentPacket { * - firstExpiry is the cltv expiry for the first trampoline node in the route * - the trampoline onion to include in final payload of a normal onion */ - def buildTrampolineToLegacyPacket(invoice: PaymentRequest, hops: Seq[NodeHop], finalPayload: PaymentOnion.FinalPayload): Try[(MilliSatoshi, CltvExpiry, Sphinx.PacketAndSecrets)] = { + def buildTrampolineToLegacyPacket(invoice: Invoice, hops: Seq[NodeHop], finalPayload: PaymentOnion.FinalPayload): Try[(MilliSatoshi, CltvExpiry, Sphinx.PacketAndSecrets)] = { // NB: the final payload will never reach the recipient, since the next-to-last node in the trampoline route will convert that to a non-trampoline payment. // We use the smallest final payload possible, otherwise we may overflow the trampoline onion size. val dummyFinalPayload = PaymentOnion.createSinglePartPayload(finalPayload.amount, finalPayload.expiry, finalPayload.paymentSecret, None) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/MultiPartHandler.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/MultiPartHandler.scala index 25a9f4a8ff..40f6c9290a 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/MultiPartHandler.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/MultiPartHandler.scala @@ -25,17 +25,17 @@ import akka.event.{DiagnosticLoggingAdapter, LoggingAdapter} import fr.acinq.bitcoin.{ByteVector32, Crypto} import fr.acinq.eclair.channel.{CMD_FAIL_HTLC, CMD_FULFILL_HTLC, RES_SUCCESS} import fr.acinq.eclair.db._ +import fr.acinq.eclair.payment.Bolt11Invoice.ExtraHop import fr.acinq.eclair.payment.Monitoring.{Metrics, Tags} -import fr.acinq.eclair.payment.PaymentRequest.{ExtraHop, PaymentRequestFeatures} -import fr.acinq.eclair.payment.{IncomingPaymentPacket, PaymentMetadataReceived, PaymentReceived, PaymentRequest} +import fr.acinq.eclair.payment.{Bolt11Invoice, IncomingPaymentPacket, Invoice, PaymentMetadataReceived, PaymentReceived} import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{Feature, FeatureSupport, Features, InvoiceFeature, Logs, MilliSatoshi, NodeParams, randomBytes32} +import fr.acinq.eclair.{FeatureSupport, Features, Logs, MilliSatoshi, NodeParams, randomBytes32} import scodec.bits.HexStringSyntax import scala.util.{Failure, Success, Try} /** - * Simple payment handler that generates payment requests and fulfills incoming htlcs. + * Simple payment handler that generates invoices and fulfills incoming htlcs. * * Created by PM on 17/06/2016. */ @@ -58,7 +58,7 @@ class MultiPartHandler(nodeParams: NodeParams, register: ActorRef, db: IncomingP override def handle(implicit ctx: ActorContext, log: DiagnosticLoggingAdapter): Receive = { case receivePayment: ReceivePayment => val child = ctx.spawnAnonymous(CreateInvoiceActor(nodeParams)) - child ! CreateInvoiceActor.CreatePaymentRequest(ctx.sender(), receivePayment) + child ! CreateInvoiceActor.CreateInvoice(ctx.sender(), receivePayment) case p: IncomingPaymentPacket.FinalPacket if doHandle(p.add.paymentHash) => val child = ctx.spawnAnonymous(GetIncomingPaymentActor(nodeParams)) @@ -92,15 +92,15 @@ class MultiPartHandler(nodeParams: NodeParams, register: ActorRef, db: IncomingP val amount = Some(p.payload.totalAmount) val paymentHash = Crypto.sha256(paymentPreimage) val desc = Left("Donation") - val features: Map[Feature with InvoiceFeature, FeatureSupport] = if (nodeParams.features.hasFeature(Features.BasicMultiPartPayment)) { - Map(Features.BasicMultiPartPayment -> FeatureSupport.Optional, Features.PaymentSecret -> FeatureSupport.Mandatory, Features.VariableLengthOnion -> FeatureSupport.Mandatory) + val features = if (nodeParams.features.hasFeature(Features.BasicMultiPartPayment)) { + Features(Features.BasicMultiPartPayment -> FeatureSupport.Optional, Features.PaymentSecret -> FeatureSupport.Mandatory, Features.VariableLengthOnion -> FeatureSupport.Mandatory) } else { - Map(Features.PaymentSecret -> FeatureSupport.Mandatory, Features.VariableLengthOnion -> FeatureSupport.Mandatory) + Features(Features.PaymentSecret -> FeatureSupport.Mandatory, Features.VariableLengthOnion -> FeatureSupport.Mandatory) } // Insert a fake invoice and then restart the incoming payment handler - val paymentRequest = PaymentRequest(nodeParams.chainHash, amount, paymentHash, nodeParams.privateKey, desc, nodeParams.channelConf.minFinalExpiryDelta, paymentSecret = p.payload.paymentSecret, features = PaymentRequestFeatures(features)) - log.debug("generated fake payment request={} from amount={} (KeySend)", PaymentRequest.write(paymentRequest), amount) - db.addIncomingPayment(paymentRequest, paymentPreimage, paymentType = PaymentType.KeySend) + val invoice = Bolt11Invoice(nodeParams.chainHash, amount, paymentHash, nodeParams.privateKey, desc, nodeParams.channelConf.minFinalExpiryDelta, paymentSecret = p.payload.paymentSecret, features = features) + log.debug("generated fake invoice={} from amount={} (KeySend)", invoice.toString, amount) + db.addIncomingPayment(invoice, paymentPreimage, paymentType = PaymentType.KeySend) ctx.self ! p case _ => Metrics.PaymentFailed.withTag(Tags.Direction, Tags.Directions.Received).withTag(Tags.Failure, "InvoiceNotFound").increment() @@ -217,25 +217,25 @@ object MultiPartHandler { // @formatter:off sealed trait Command - case class CreatePaymentRequest(replyTo: ActorRef, receivePayment: ReceivePayment) extends Command + case class CreateInvoice(replyTo: ActorRef, receivePayment: ReceivePayment) extends Command // @formatter:on def apply(nodeParams: NodeParams): Behavior[Command] = { Behaviors.setup { context => Behaviors.receiveMessage { - case CreatePaymentRequest(replyTo, receivePayment) => + case CreateInvoice(replyTo, receivePayment) => Try { import receivePayment._ val paymentPreimage = paymentPreimage_opt.getOrElse(randomBytes32()) val paymentHash = Crypto.sha256(paymentPreimage) - val expirySeconds = expirySeconds_opt.getOrElse(nodeParams.paymentRequestExpiry.toSeconds) + val expirySeconds = expirySeconds_opt.getOrElse(nodeParams.invoiceExpiry.toSeconds) val paymentMetadata = hex"2a" val invoiceFeatures = if (nodeParams.enableTrampolinePayment) { - nodeParams.features.invoiceFeatures() + (Features.TrampolinePayment -> FeatureSupport.Optional) + Features(nodeParams.features.invoiceFeatures().activated + (Features.TrampolinePayment -> FeatureSupport.Optional)) } else { nodeParams.features.invoiceFeatures() } - val paymentRequest = PaymentRequest( + val invoice = Bolt11Invoice( nodeParams.chainHash, amount_opt, paymentHash, @@ -246,13 +246,13 @@ object MultiPartHandler { expirySeconds = Some(expirySeconds), extraHops = extraHops, paymentMetadata = Some(paymentMetadata), - features = PaymentRequestFeatures(invoiceFeatures) + features = invoiceFeatures ) - context.log.debug("generated payment request={} from amount={}", PaymentRequest.write(paymentRequest), amount_opt) - nodeParams.db.payments.addIncomingPayment(paymentRequest, paymentPreimage, paymentType) - paymentRequest + context.log.debug("generated invoice={} from amount={}", invoice.toString, amount_opt) + nodeParams.db.payments.addIncomingPayment(invoice, paymentPreimage, paymentType) + invoice } match { - case Success(paymentRequest) => replyTo ! paymentRequest + case Success(invoice) => replyTo ! invoice case Failure(exception) => replyTo ! Status.Failure(exception) } Behaviors.stopped @@ -281,7 +281,7 @@ object MultiPartHandler { if (record.status.isInstanceOf[IncomingPaymentStatus.Received]) { log.warning("ignoring incoming payment for which has already been paid") false - } else if (record.paymentRequest.isExpired) { + } else if (record.invoice.isExpired()) { log.warning("received payment for expired amount={} totalAmount={}", payment.add.amountMsat, payment.payload.totalAmount) false } else { @@ -305,7 +305,7 @@ object MultiPartHandler { } private def validatePaymentCltv(nodeParams: NodeParams, payment: IncomingPaymentPacket.FinalPacket, record: IncomingPayment)(implicit log: LoggingAdapter): Boolean = { - val minExpiry = record.paymentRequest.minFinalCltvExpiryDelta.getOrElse(nodeParams.channelConf.minFinalExpiryDelta).toCltvExpiry(nodeParams.currentBlockHeight) + val minExpiry = record.invoice.minFinalCltvExpiryDelta.getOrElse(nodeParams.channelConf.minFinalExpiryDelta).toCltvExpiry(nodeParams.currentBlockHeight) if (payment.add.cltvExpiry < minExpiry) { log.warning("received payment with expiry too small for amount={} totalAmount={}", payment.add.amountMsat, payment.payload.totalAmount) false @@ -314,14 +314,14 @@ object MultiPartHandler { } } - private def validateInvoiceFeatures(payment: IncomingPaymentPacket.FinalPacket, pr: PaymentRequest)(implicit log: LoggingAdapter): Boolean = { - if (payment.payload.amount < payment.payload.totalAmount && !pr.features.allowMultiPart) { + private def validateInvoiceFeatures(payment: IncomingPaymentPacket.FinalPacket, invoice: Invoice)(implicit log: LoggingAdapter): Boolean = { + if (payment.payload.amount < payment.payload.totalAmount && !invoice.features.hasFeature(Features.BasicMultiPartPayment)) { log.warning("received multi-part payment but invoice doesn't support it for amount={} totalAmount={}", payment.add.amountMsat, payment.payload.totalAmount) false - } else if (payment.payload.amount < payment.payload.totalAmount && !pr.paymentSecret.contains(payment.payload.paymentSecret)) { + } else if (payment.payload.amount < payment.payload.totalAmount && !invoice.paymentSecret.contains(payment.payload.paymentSecret)) { log.warning("received multi-part payment with invalid secret={} for amount={} totalAmount={}", payment.payload.paymentSecret, payment.add.amountMsat, payment.payload.totalAmount) false - } else if (!pr.paymentSecret.contains(payment.payload.paymentSecret)) { + } else if (!invoice.paymentSecret.contains(payment.payload.paymentSecret)) { log.warning("received payment with invalid secret={} for amount={} totalAmount={}", payment.payload.paymentSecret, payment.add.amountMsat, payment.payload.totalAmount) false } else { @@ -332,10 +332,10 @@ object MultiPartHandler { private def validatePayment(nodeParams: NodeParams, payment: IncomingPaymentPacket.FinalPacket, record: IncomingPayment)(implicit log: LoggingAdapter): Option[CMD_FAIL_HTLC] = { // We send the same error regardless of the failure to avoid probing attacks. val cmdFail = CMD_FAIL_HTLC(payment.add.id, Right(IncorrectOrUnknownPaymentDetails(payment.payload.totalAmount, nodeParams.currentBlockHeight)), commit = true) - val paymentAmountOk = record.paymentRequest.amount.forall(a => validatePaymentAmount(payment, a)) + val paymentAmountOk = record.invoice.amount_opt.forall(a => validatePaymentAmount(payment, a)) val paymentCltvOk = validatePaymentCltv(nodeParams, payment, record) val paymentStatusOk = validatePaymentStatus(payment, record) - val paymentFeaturesOk = validateInvoiceFeatures(payment, record.paymentRequest) + val paymentFeaturesOk = validateInvoiceFeatures(payment, record.invoice) if (paymentAmountOk && paymentCltvOk && paymentStatusOk && paymentFeaturesOk) None else Some(cmdFail) } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/Autoprobe.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/Autoprobe.scala index ebcdae283f..02ae55a3c6 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/Autoprobe.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/Autoprobe.scala @@ -19,7 +19,7 @@ package fr.acinq.eclair.payment.send import akka.actor.{Actor, ActorLogging, ActorRef, Props} import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.eclair.crypto.Sphinx.DecryptedFailurePacket -import fr.acinq.eclair.payment.{PaymentEvent, PaymentFailed, PaymentRequest, RemoteFailure} +import fr.acinq.eclair.payment.{PaymentEvent, PaymentFailed, Bolt11Invoice, Invoice, RemoteFailure} import fr.acinq.eclair.router.Router import fr.acinq.eclair.wire.protocol.IncorrectOrUnknownPaymentDetails import fr.acinq.eclair.{MilliSatoshiLong, NodeParams, TimestampSecond, randomBytes32, randomLong} @@ -53,14 +53,14 @@ class Autoprobe(nodeParams: NodeParams, router: ActorRef, paymentInitiator: Acto case TickProbe => pickPaymentDestination(nodeParams.nodeId, routingData) match { case Some(targetNodeId) => - val fakeInvoice = PaymentRequest( - PaymentRequest.prefixes(nodeParams.chainHash), + val fakeInvoice = Bolt11Invoice( + Bolt11Invoice.prefixes(nodeParams.chainHash), Some(PAYMENT_AMOUNT_MSAT), TimestampSecond.now(), targetNodeId, List( - PaymentRequest.PaymentHash(randomBytes32()), // we don't even know the preimage (this needs to be a secure random!) - PaymentRequest.Description("ignored"), + Bolt11Invoice.PaymentHash(randomBytes32()), // we don't even know the preimage (this needs to be a secure random!) + Bolt11Invoice.Description("ignored"), ), ByteVector.empty) log.info(s"sending payment probe to node=$targetNodeId payment_hash=${fakeInvoice.paymentHash}") diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/MultiPartPaymentLifecycle.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/MultiPartPaymentLifecycle.scala index df978d2d56..f16f8309b6 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/MultiPartPaymentLifecycle.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/MultiPartPaymentLifecycle.scala @@ -22,9 +22,9 @@ import fr.acinq.bitcoin.ByteVector32 import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.eclair.channel.{HtlcOverriddenByLocalCommit, HtlcsTimedoutDownstream, HtlcsWillTimeoutUpstream} import fr.acinq.eclair.db.{OutgoingPayment, OutgoingPaymentStatus, PaymentType} +import fr.acinq.eclair.payment.Bolt11Invoice.ExtraHop import fr.acinq.eclair.payment.Monitoring.{Metrics, Tags} import fr.acinq.eclair.payment.OutgoingPaymentPacket.Upstream -import fr.acinq.eclair.payment.PaymentRequest.ExtraHop import fr.acinq.eclair.payment.PaymentSent.PartialPayment import fr.acinq.eclair.payment._ import fr.acinq.eclair.payment.send.PaymentInitiator.SendPaymentConfig @@ -105,7 +105,7 @@ class MultiPartPaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, if (cfg.storeInDb && d.pending.isEmpty && d.failures.isEmpty) { // In cases where we fail early (router error during the first attempt), the DB won't have an entry for that // payment, which may be confusing for users. - val dummyPayment = OutgoingPayment(id, cfg.parentId, cfg.externalId, paymentHash, PaymentType.Standard, cfg.recipientAmount, cfg.recipientAmount, cfg.recipientNodeId, TimestampMilli.now(), cfg.paymentRequest, OutgoingPaymentStatus.Pending) + val dummyPayment = OutgoingPayment(id, cfg.parentId, cfg.externalId, paymentHash, PaymentType.Standard, cfg.recipientAmount, cfg.recipientAmount, cfg.recipientNodeId, TimestampMilli.now(), cfg.invoice, OutgoingPaymentStatus.Pending) nodeParams.db.payments.addOutgoingPayment(dummyPayment) nodeParams.db.payments.updateOutgoingPayment(PaymentFailed(id, paymentHash, failure :: Nil)) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentInitiator.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentInitiator.scala index c8f14a73b5..4432941de5 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentInitiator.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentInitiator.scala @@ -22,15 +22,15 @@ import fr.acinq.bitcoin.{ByteVector32, Crypto} import fr.acinq.eclair.Features.BasicMultiPartPayment import fr.acinq.eclair.channel.Channel import fr.acinq.eclair.crypto.Sphinx +import fr.acinq.eclair.payment.Bolt11Invoice.ExtraHop import fr.acinq.eclair.payment.OutgoingPaymentPacket.Upstream -import fr.acinq.eclair.payment.PaymentRequest.ExtraHop import fr.acinq.eclair.payment._ import fr.acinq.eclair.payment.send.MultiPartPaymentLifecycle.{PreimageReceived, SendMultiPartPayment} import fr.acinq.eclair.payment.send.PaymentError._ import fr.acinq.eclair.router.RouteNotFound import fr.acinq.eclair.router.Router._ import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{BlockHeight, CltvExpiry, CltvExpiryDelta, MilliSatoshi, MilliSatoshiLong, NodeParams, randomBytes32} +import fr.acinq.eclair.{BlockHeight, CltvExpiry, CltvExpiryDelta, Features, MilliSatoshi, MilliSatoshiLong, NodeParams, randomBytes32} import java.util.UUID import scala.util.{Failure, Success, Try} @@ -51,19 +51,19 @@ class PaymentInitiator(nodeParams: NodeParams, outgoingPaymentFactory: PaymentIn // Immediately return the paymentId sender() ! paymentId } - val paymentCfg = SendPaymentConfig(paymentId, paymentId, r.externalId, r.paymentHash, r.recipientAmount, r.recipientNodeId, Upstream.Local(paymentId), Some(r.paymentRequest), storeInDb = true, publishEvent = true, recordPathFindingMetrics = true, Nil) + val paymentCfg = SendPaymentConfig(paymentId, paymentId, r.externalId, r.paymentHash, r.recipientAmount, r.recipientNodeId, Upstream.Local(paymentId), Some(r.invoice), storeInDb = true, publishEvent = true, recordPathFindingMetrics = true, Nil) val finalExpiry = r.finalExpiry(nodeParams.currentBlockHeight) - r.paymentRequest.paymentSecret match { - case _ if !r.paymentRequest.features.areSupported(nodeParams) => - sender() ! PaymentFailed(paymentId, r.paymentHash, LocalFailure(r.recipientAmount, Nil, UnsupportedFeatures(r.paymentRequest.features.features)) :: Nil) + r.invoice.paymentSecret match { + case _ if !nodeParams.features.areSupported(r.invoice.features) => + sender() ! PaymentFailed(paymentId, r.paymentHash, LocalFailure(r.recipientAmount, Nil, UnsupportedFeatures(r.invoice.features)) :: Nil) case None => sender() ! PaymentFailed(paymentId, r.paymentHash, LocalFailure(r.recipientAmount, Nil, PaymentSecretMissing) :: Nil) - case Some(paymentSecret) if r.paymentRequest.features.allowMultiPart && nodeParams.features.hasFeature(BasicMultiPartPayment) => + case Some(paymentSecret) if r.invoice.features.hasFeature(Features.BasicMultiPartPayment) && nodeParams.features.hasFeature(BasicMultiPartPayment) => val fsm = outgoingPaymentFactory.spawnOutgoingMultiPartPayment(context, paymentCfg) - fsm ! SendMultiPartPayment(self, paymentSecret, r.recipientNodeId, r.recipientAmount, finalExpiry, r.maxAttempts, r.paymentRequest.paymentMetadata, r.assistedRoutes, r.routeParams, userCustomTlvs = r.userCustomTlvs) + fsm ! SendMultiPartPayment(self, paymentSecret, r.recipientNodeId, r.recipientAmount, finalExpiry, r.maxAttempts, r.invoice.paymentMetadata, r.assistedRoutes, r.routeParams, userCustomTlvs = r.userCustomTlvs) context become main(pending + (paymentId -> PendingPaymentToNode(sender(), r))) case Some(paymentSecret) => - val finalPayload = PaymentOnion.createSinglePartPayload(r.recipientAmount, finalExpiry, paymentSecret, r.paymentRequest.paymentMetadata, r.userCustomTlvs) + val finalPayload = PaymentOnion.createSinglePartPayload(r.recipientAmount, finalExpiry, paymentSecret, r.invoice.paymentMetadata, r.userCustomTlvs) val fsm = outgoingPaymentFactory.spawnOutgoingPayment(context, paymentCfg) fsm ! PaymentLifecycle.SendPaymentToNode(self, r.recipientNodeId, finalPayload, r.maxAttempts, r.assistedRoutes, r.routeParams) context become main(pending + (paymentId -> PendingPaymentToNode(sender(), r))) @@ -85,7 +85,7 @@ class PaymentInitiator(nodeParams: NodeParams, outgoingPaymentFactory: PaymentIn r.trampolineAttempts match { case Nil => sender() ! PaymentFailed(paymentId, r.paymentHash, LocalFailure(r.recipientAmount, Nil, TrampolineFeesMissing) :: Nil) - case _ if !r.paymentRequest.features.allowTrampoline && r.paymentRequest.amount.isEmpty => + case _ if !r.invoice.features.hasFeature(Features.TrampolinePayment) && r.invoice.amount_opt.isEmpty => sender() ! PaymentFailed(paymentId, r.paymentHash, LocalFailure(r.recipientAmount, Nil, TrampolineLegacyAmountLessInvoice) :: Nil) case (trampolineFees, trampolineExpiryDelta) :: remainingAttempts => log.info(s"sending trampoline payment with trampoline fees=$trampolineFees and expiry delta=$trampolineExpiryDelta") @@ -103,9 +103,9 @@ class PaymentInitiator(nodeParams: NodeParams, outgoingPaymentFactory: PaymentIn val parentPaymentId = r.parentId.getOrElse(UUID.randomUUID()) val finalExpiry = r.finalExpiry(nodeParams.currentBlockHeight) val additionalHops = r.trampolineNodes.sliding(2).map(hop => NodeHop(hop.head, hop(1), CltvExpiryDelta(0), 0 msat)).toSeq - val paymentCfg = SendPaymentConfig(paymentId, parentPaymentId, r.externalId, r.paymentHash, r.recipientAmount, r.recipientNodeId, Upstream.Local(paymentId), Some(r.paymentRequest), storeInDb = true, publishEvent = true, recordPathFindingMetrics = false, additionalHops) + val paymentCfg = SendPaymentConfig(paymentId, parentPaymentId, r.externalId, r.paymentHash, r.recipientAmount, r.recipientNodeId, Upstream.Local(paymentId), Some(r.invoice), storeInDb = true, publishEvent = true, recordPathFindingMetrics = false, additionalHops) r.trampolineNodes match { - case _ if r.paymentRequest.paymentSecret.isEmpty => + case _ if r.invoice.paymentSecret.isEmpty => sender() ! PaymentFailed(paymentId, r.paymentHash, LocalFailure(r.recipientAmount, Nil, PaymentSecretMissing) :: Nil) case trampoline :: recipient :: Nil => log.info(s"sending trampoline payment to $recipient with trampoline=$trampoline, trampoline fees=${r.trampolineFees}, expiry delta=${r.trampolineExpiryDelta}") @@ -115,7 +115,7 @@ class PaymentInitiator(nodeParams: NodeParams, outgoingPaymentFactory: PaymentIn val trampolineSecret = r.trampolineSecret.getOrElse(randomBytes32()) sender() ! SendPaymentToRouteResponse(paymentId, parentPaymentId, Some(trampolineSecret)) val payFsm = outgoingPaymentFactory.spawnOutgoingPayment(context, paymentCfg) - payFsm ! PaymentLifecycle.SendPaymentToRoute(self, Left(r.route), PaymentOnion.createMultiPartPayload(r.amount, trampolineAmount, trampolineExpiry, trampolineSecret, r.paymentRequest.paymentMetadata, Seq(OnionPaymentPayloadTlv.TrampolineOnion(trampolineOnion))), r.paymentRequest.routingInfo) + payFsm ! PaymentLifecycle.SendPaymentToRoute(self, Left(r.route), PaymentOnion.createMultiPartPayload(r.amount, trampolineAmount, trampolineExpiry, trampolineSecret, r.invoice.paymentMetadata, Seq(OnionPaymentPayloadTlv.TrampolineOnion(trampolineOnion))), r.invoice.routingInfo) context become main(pending + (paymentId -> PendingPaymentToRoute(sender(), r))) case Failure(t) => log.warning("cannot send outgoing trampoline payment: {}", t.getMessage) @@ -124,7 +124,7 @@ class PaymentInitiator(nodeParams: NodeParams, outgoingPaymentFactory: PaymentIn case Nil => sender() ! SendPaymentToRouteResponse(paymentId, parentPaymentId, None) val payFsm = outgoingPaymentFactory.spawnOutgoingPayment(context, paymentCfg) - payFsm ! PaymentLifecycle.SendPaymentToRoute(self, Left(r.route), PaymentOnion.createMultiPartPayload(r.amount, r.recipientAmount, finalExpiry, r.paymentRequest.paymentSecret.get, r.paymentRequest.paymentMetadata), r.paymentRequest.routingInfo) + payFsm ! PaymentLifecycle.SendPaymentToRoute(self, Left(r.route), PaymentOnion.createMultiPartPayload(r.amount, r.recipientAmount, finalExpiry, r.invoice.paymentSecret.get, r.invoice.paymentMetadata), r.invoice.routingInfo) context become main(pending + (paymentId -> PendingPaymentToRoute(sender(), r))) case _ => sender() ! PaymentFailed(paymentId, r.paymentHash, LocalFailure(r.recipientAmount, Nil, TrampolineMultiNodeNotSupported) :: Nil) @@ -197,16 +197,16 @@ class PaymentInitiator(nodeParams: NodeParams, outgoingPaymentFactory: PaymentIn NodeHop(nodeParams.nodeId, trampolineNodeId, nodeParams.channelConf.expiryDelta, 0 msat), NodeHop(trampolineNodeId, r.recipientNodeId, trampolineExpiryDelta, trampolineFees) // for now we only use a single trampoline hop ) - val finalPayload = if (r.paymentRequest.features.allowMultiPart) { - PaymentOnion.createMultiPartPayload(r.recipientAmount, r.recipientAmount, r.finalExpiry(nodeParams.currentBlockHeight), r.paymentRequest.paymentSecret.get, r.paymentRequest.paymentMetadata) + val finalPayload = if (r.invoice.features.hasFeature(Features.BasicMultiPartPayment)) { + PaymentOnion.createMultiPartPayload(r.recipientAmount, r.recipientAmount, r.finalExpiry(nodeParams.currentBlockHeight), r.invoice.paymentSecret.get, r.invoice.paymentMetadata) } else { - PaymentOnion.createSinglePartPayload(r.recipientAmount, r.finalExpiry(nodeParams.currentBlockHeight), r.paymentRequest.paymentSecret.get, r.paymentRequest.paymentMetadata) + PaymentOnion.createSinglePartPayload(r.recipientAmount, r.finalExpiry(nodeParams.currentBlockHeight), r.invoice.paymentSecret.get, r.invoice.paymentMetadata) } // We assume that the trampoline node supports multi-part payments (it should). - val trampolinePacket_opt = if (r.paymentRequest.features.allowTrampoline) { + val trampolinePacket_opt = if (r.invoice.features.hasFeature(Features.TrampolinePayment)) { OutgoingPaymentPacket.buildTrampolinePacket(r.paymentHash, trampolineRoute, finalPayload) } else { - OutgoingPaymentPacket.buildTrampolineToLegacyPacket(r.paymentRequest, trampolineRoute, finalPayload) + OutgoingPaymentPacket.buildTrampolineToLegacyPacket(r.invoice, trampolineRoute, finalPayload) } trampolinePacket_opt.map { case (trampolineAmount, trampolineExpiry, trampolineOnion) => (trampolineAmount, trampolineExpiry, trampolineOnion.packet) @@ -214,13 +214,13 @@ class PaymentInitiator(nodeParams: NodeParams, outgoingPaymentFactory: PaymentIn } private def sendTrampolinePayment(paymentId: UUID, r: SendTrampolinePayment, trampolineFees: MilliSatoshi, trampolineExpiryDelta: CltvExpiryDelta): Try[Unit] = { - val paymentCfg = SendPaymentConfig(paymentId, paymentId, None, r.paymentHash, r.recipientAmount, r.recipientNodeId, Upstream.Local(paymentId), Some(r.paymentRequest), storeInDb = true, publishEvent = false, recordPathFindingMetrics = true, Seq(NodeHop(r.trampolineNodeId, r.recipientNodeId, trampolineExpiryDelta, trampolineFees))) + val paymentCfg = SendPaymentConfig(paymentId, paymentId, None, r.paymentHash, r.recipientAmount, r.recipientNodeId, Upstream.Local(paymentId), Some(r.invoice), storeInDb = true, publishEvent = false, recordPathFindingMetrics = true, Seq(NodeHop(r.trampolineNodeId, r.recipientNodeId, trampolineExpiryDelta, trampolineFees))) // We generate a random secret for this payment to avoid leaking the invoice secret to the first trampoline node. val trampolineSecret = randomBytes32() buildTrampolinePayment(r, r.trampolineNodeId, trampolineFees, trampolineExpiryDelta).map { case (trampolineAmount, trampolineExpiry, trampolineOnion) => val fsm = outgoingPaymentFactory.spawnOutgoingMultiPartPayment(context, paymentCfg) - fsm ! SendMultiPartPayment(self, trampolineSecret, r.trampolineNodeId, trampolineAmount, trampolineExpiry, nodeParams.maxPaymentAttempts, r.paymentRequest.paymentMetadata, r.paymentRequest.routingInfo, r.routeParams, Seq(OnionPaymentPayloadTlv.TrampolineOnion(trampolineOnion))) + fsm ! SendMultiPartPayment(self, trampolineSecret, r.trampolineNodeId, trampolineAmount, trampolineExpiry, nodeParams.maxPaymentAttempts, r.invoice.paymentMetadata, r.invoice.routingInfo, r.routeParams, Seq(OnionPaymentPayloadTlv.TrampolineOnion(trampolineOnion))) } } @@ -269,12 +269,12 @@ object PaymentInitiator { sealed trait SendRequestedPayment { // @formatter:off def recipientAmount: MilliSatoshi - def paymentRequest: PaymentRequest - def recipientNodeId: PublicKey = paymentRequest.nodeId - def paymentHash: ByteVector32 = paymentRequest.paymentHash + def invoice: Invoice + def recipientNodeId: PublicKey = invoice.nodeId + def paymentHash: ByteVector32 = invoice.paymentHash def fallbackFinalExpiryDelta: CltvExpiryDelta // We add one block in order to not have our htlcs fail when a new block has just been found. - def finalExpiry(currentBlockHeight: BlockHeight): CltvExpiry = paymentRequest.minFinalCltvExpiryDelta.getOrElse(fallbackFinalExpiryDelta).toCltvExpiry(currentBlockHeight + 1) + def finalExpiry(currentBlockHeight: BlockHeight): CltvExpiry = invoice.minFinalCltvExpiryDelta.getOrElse(fallbackFinalExpiryDelta).toCltvExpiry(currentBlockHeight + 1) // @formatter:on } @@ -284,17 +284,17 @@ object PaymentInitiator { * automatically by the router instead of the caller. * * @param recipientAmount amount that should be received by the final recipient (usually from a Bolt 11 invoice). - * @param paymentRequest Bolt 11 invoice. + * @param invoice Bolt 11 invoice. * @param trampolineNodeId id of the trampoline node. * @param trampolineAttempts fees and expiry delta for the trampoline node. If this list contains multiple entries, * the payment will automatically be retried in case of TrampolineFeeInsufficient errors. * For example, [(10 msat, 144), (15 msat, 288)] will first send a payment with a fee of 10 * msat and cltv of 144, and retry with 15 msat and 288 in case an error occurs. - * @param fallbackFinalExpiryDelta expiry delta for the final recipient when the [[paymentRequest]] doesn't specify it. + * @param fallbackFinalExpiryDelta expiry delta for the final recipient when the [[invoice]] doesn't specify it. * @param routeParams (optional) parameters to fine-tune the routing algorithm. */ case class SendTrampolinePayment(recipientAmount: MilliSatoshi, - paymentRequest: PaymentRequest, + invoice: Invoice, trampolineNodeId: PublicKey, trampolineAttempts: Seq[(MilliSatoshi, CltvExpiryDelta)], fallbackFinalExpiryDelta: CltvExpiryDelta = Channel.MIN_CLTV_EXPIRY_DELTA, @@ -302,9 +302,9 @@ object PaymentInitiator { /** * @param recipientAmount amount that should be received by the final recipient (usually from a Bolt 11 invoice). - * @param paymentRequest Bolt 11 invoice. + * @param invoice Bolt 11 invoice. * @param maxAttempts maximum number of retries. - * @param fallbackFinalExpiryDelta expiry delta for the final recipient when the [[paymentRequest]] doesn't specify it. + * @param fallbackFinalExpiryDelta expiry delta for the final recipient when the [[invoice]] doesn't specify it. * @param externalId (optional) externally-controlled identifier (to reconcile between application DB and eclair DB). * @param assistedRoutes (optional) routing hints (usually from a Bolt 11 invoice). * @param routeParams (optional) parameters to fine-tune the routing algorithm. @@ -312,7 +312,7 @@ object PaymentInitiator { * @param blockUntilComplete (optional) if true, wait until the payment completes before returning a result. */ case class SendPaymentToNode(recipientAmount: MilliSatoshi, - paymentRequest: PaymentRequest, + invoice: Invoice, maxAttempts: Int, fallbackFinalExpiryDelta: CltvExpiryDelta = Channel.MIN_CLTV_EXPIRY_DELTA, externalId: Option[String] = None, @@ -359,8 +359,8 @@ object PaymentInitiator { * fees into account). * @param recipientAmount amount that should be received by the final recipient (usually from a Bolt 11 invoice). * This amount may be split between multiple requests if using MPP. - * @param paymentRequest Bolt 11 invoice. - * @param fallbackFinalExpiryDelta expiry delta for the final recipient when the [[paymentRequest]] doesn't specify it. + * @param invoice Bolt 11 invoice. + * @param fallbackFinalExpiryDelta expiry delta for the final recipient when the [[invoice]] doesn't specify it. * @param route route to use to reach either the final recipient or the first trampoline node. * @param externalId (optional) externally-controlled identifier (to reconcile between application DB and eclair DB). * @param parentId id of the whole payment. When manually sending a multi-part payment, you need to make @@ -378,7 +378,7 @@ object PaymentInitiator { */ case class SendPaymentToRoute(amount: MilliSatoshi, recipientAmount: MilliSatoshi, - paymentRequest: PaymentRequest, + invoice: Invoice, fallbackFinalExpiryDelta: CltvExpiryDelta = Channel.MIN_CLTV_EXPIRY_DELTA, route: PredefinedRoute, externalId: Option[String], @@ -409,7 +409,7 @@ object PaymentInitiator { * @param recipientAmount amount that should be received by the final recipient (usually from a Bolt 11 invoice). * @param recipientNodeId id of the final recipient. * @param upstream information about the payment origin (to link upstream to downstream when relaying a payment). - * @param paymentRequest Bolt 11 invoice. + * @param invoice Bolt 11 invoice. * @param storeInDb whether to store data in the payments DB (e.g. when we're relaying a trampoline payment, we * don't want to store in the DB). * @param publishEvent whether to publish a [[fr.acinq.eclair.payment.PaymentEvent]] on success/failure (e.g. for @@ -425,7 +425,7 @@ object PaymentInitiator { recipientAmount: MilliSatoshi, recipientNodeId: PublicKey, upstream: Upstream, - paymentRequest: Option[PaymentRequest], + invoice: Option[Invoice], storeInDb: Boolean, // e.g. for trampoline we don't want to store in the DB when we're relaying payments publishEvent: Boolean, recordPathFindingMetrics: Boolean, diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentLifecycle.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentLifecycle.scala index 734ed5b8a5..62f1afdf26 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentLifecycle.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentLifecycle.scala @@ -24,9 +24,9 @@ import fr.acinq.eclair._ import fr.acinq.eclair.channel._ import fr.acinq.eclair.crypto.{Sphinx, TransportHandler} import fr.acinq.eclair.db.{OutgoingPayment, OutgoingPaymentStatus, PaymentType} +import fr.acinq.eclair.payment.Bolt11Invoice.ExtraHop import fr.acinq.eclair.payment.Monitoring.{Metrics, Tags} import fr.acinq.eclair.payment.OutgoingPaymentPacket.Upstream -import fr.acinq.eclair.payment.PaymentRequest.ExtraHop import fr.acinq.eclair.payment.PaymentSent.PartialPayment import fr.acinq.eclair.payment._ import fr.acinq.eclair.payment.send.PaymentInitiator.SendPaymentConfig @@ -60,7 +60,7 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A route => self ! RouteResponse(route :: Nil) ) if (cfg.storeInDb) { - paymentsDb.addOutgoingPayment(OutgoingPayment(id, cfg.parentId, cfg.externalId, paymentHash, PaymentType.Standard, c.finalPayload.amount, cfg.recipientAmount, cfg.recipientNodeId, TimestampMilli.now(), cfg.paymentRequest, OutgoingPaymentStatus.Pending)) + paymentsDb.addOutgoingPayment(OutgoingPayment(id, cfg.parentId, cfg.externalId, paymentHash, PaymentType.Standard, c.finalPayload.amount, cfg.recipientAmount, cfg.recipientNodeId, TimestampMilli.now(), cfg.invoice, OutgoingPaymentStatus.Pending)) } goto(WAITING_FOR_ROUTE) using WaitingForRoute(c, Nil, Ignore.empty) @@ -68,7 +68,7 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A log.debug("sending {} to {}", c.finalPayload.amount, c.targetNodeId) router ! RouteRequest(nodeParams.nodeId, c.targetNodeId, c.finalPayload.amount, c.maxFee, c.assistedRoutes, routeParams = c.routeParams, paymentContext = Some(cfg.paymentContext)) if (cfg.storeInDb) { - paymentsDb.addOutgoingPayment(OutgoingPayment(id, cfg.parentId, cfg.externalId, paymentHash, PaymentType.Standard, c.finalPayload.amount, cfg.recipientAmount, cfg.recipientNodeId, TimestampMilli.now(), cfg.paymentRequest, OutgoingPaymentStatus.Pending)) + paymentsDb.addOutgoingPayment(OutgoingPayment(id, cfg.parentId, cfg.externalId, paymentHash, PaymentType.Standard, c.finalPayload.amount, cfg.recipientAmount, cfg.recipientNodeId, TimestampMilli.now(), cfg.invoice, OutgoingPaymentStatus.Pending)) } goto(WAITING_FOR_ROUTE) using WaitingForRoute(c, Nil, Ignore.empty) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/RouteCalculation.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/RouteCalculation.scala index df9b4841cd..8c2d12755c 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/RouteCalculation.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/RouteCalculation.scala @@ -23,7 +23,7 @@ import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.bitcoin.{ByteVector32, ByteVector64, Satoshi, SatoshiLong} import fr.acinq.eclair.Logs.LogCategory import fr.acinq.eclair._ -import fr.acinq.eclair.payment.PaymentRequest.ExtraHop +import fr.acinq.eclair.payment.Bolt11Invoice.ExtraHop import fr.acinq.eclair.router.Graph.GraphStructure.DirectedGraph.graphEdgeToHop import fr.acinq.eclair.router.Graph.GraphStructure.{DirectedGraph, GraphEdge} import fr.acinq.eclair.router.Graph.{InfiniteLoop, NegativeProbability, RichWeight, RoutingHeuristics} @@ -104,7 +104,7 @@ object RouteCalculation { paymentHash_opt = r.paymentContext.map(_.paymentHash))) { implicit val sender: ActorRef = ctx.self // necessary to preserve origin when sending messages to other actors - // we convert extra routing info provided in the payment request to fake channel_update + // we convert extra routing info provided in the invoice to fake channel_update // it takes precedence over all other channel_updates we know val assistedChannels: Map[ShortChannelId, AssistedChannel] = r.assistedRoutes.flatMap(toAssistedChannels(_, r.target, r.amount)) .filterNot { case (_, ac) => ac.extraHop.nodeId == r.source } // we ignore routing hints for our own channels, we have more accurate information diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala index 0912854c7d..90dce39510 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala @@ -31,7 +31,7 @@ import fr.acinq.eclair.channel._ import fr.acinq.eclair.crypto.TransportHandler import fr.acinq.eclair.db.NetworkDb import fr.acinq.eclair.io.Peer.PeerRoutingMessage -import fr.acinq.eclair.payment.PaymentRequest.ExtraHop +import fr.acinq.eclair.payment.Bolt11Invoice.ExtraHop import fr.acinq.eclair.remote.EclairInternalsSerializer.RemoteTypes import fr.acinq.eclair.router.Graph.GraphStructure.DirectedGraph import fr.acinq.eclair.router.Graph.{HeuristicsConstants, WeightRatios} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/PaymentOnion.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/PaymentOnion.scala index 93fa55e7a9..bc156dd271 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/PaymentOnion.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/PaymentOnion.scala @@ -18,7 +18,7 @@ package fr.acinq.eclair.wire.protocol import fr.acinq.bitcoin.ByteVector32 import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.eclair.payment.PaymentRequest +import fr.acinq.eclair.payment.{Bolt11Invoice, Invoice} import fr.acinq.eclair.wire.protocol.CommonCodecs._ import fr.acinq.eclair.wire.protocol.OnionRoutingCodecs.MissingRequiredTlv import fr.acinq.eclair.wire.protocol.TlvCodecs._ @@ -171,7 +171,7 @@ object OnionPaymentPayloadTlv { * Invoice routing hints. Only included for intermediate trampoline nodes when they should convert to a legacy payment * because the final recipient doesn't support trampoline. */ - case class InvoiceRoutingInfo(extraHops: List[List[PaymentRequest.ExtraHop]]) extends OnionPaymentPayloadTlv + case class InvoiceRoutingInfo(extraHops: List[List[Bolt11Invoice.ExtraHop]]) extends OnionPaymentPayloadTlv /** An encrypted trampoline onion packet. */ case class TrampolineOnion(packet: OnionRoutingPacket) extends OnionPaymentPayloadTlv @@ -295,7 +295,7 @@ object PaymentOnion { NodeRelayPayload(TlvStream(AmountToForward(amount), OutgoingCltv(expiry), OutgoingNodeId(nextNodeId))) /** Create a trampoline inner payload instructing the trampoline node to relay via a non-trampoline payment. */ - def createNodeRelayToNonTrampolinePayload(amount: MilliSatoshi, totalAmount: MilliSatoshi, expiry: CltvExpiry, targetNodeId: PublicKey, invoice: PaymentRequest): NodeRelayPayload = { + def createNodeRelayToNonTrampolinePayload(amount: MilliSatoshi, totalAmount: MilliSatoshi, expiry: CltvExpiry, targetNodeId: PublicKey, invoice: Invoice): NodeRelayPayload = { val tlvs = Seq( Some(AmountToForward(amount)), Some(OutgoingCltv(expiry)), @@ -374,7 +374,7 @@ object PaymentOnionCodecs { private val invoiceFeatures: Codec[InvoiceFeatures] = variableSizeBytesLong(varintoverflow, bytes).as[InvoiceFeatures] - private val invoiceRoutingInfo: Codec[InvoiceRoutingInfo] = variableSizeBytesLong(varintoverflow, list(listOfN(uint8, PaymentRequest.Codecs.extraHopCodec))).as[InvoiceRoutingInfo] + private val invoiceRoutingInfo: Codec[InvoiceRoutingInfo] = variableSizeBytesLong(varintoverflow, list(listOfN(uint8, Bolt11Invoice.Codecs.extraHopCodec))).as[InvoiceRoutingInfo] private val trampolineOnion: Codec[TrampolineOnion] = variableSizeBytesLong(varintoverflow, trampolineOnionPacketCodec).as[TrampolineOnion] diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/EclairImplSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/EclairImplSpec.scala index eb01e19300..4384214a94 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/EclairImplSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/EclairImplSpec.scala @@ -30,12 +30,13 @@ import fr.acinq.eclair.blockchain.fee.{FeeratePerByte, FeeratePerKw} import fr.acinq.eclair.channel._ import fr.acinq.eclair.db._ import fr.acinq.eclair.io.Peer.OpenChannel -import fr.acinq.eclair.payment.PaymentRequest.ExtraHop +import fr.acinq.eclair.payment.Bolt11Invoice +import fr.acinq.eclair.payment.Bolt11Invoice.ExtraHop import fr.acinq.eclair.payment.receive.MultiPartHandler.ReceivePayment import fr.acinq.eclair.payment.receive.PaymentHandler import fr.acinq.eclair.payment.relay.Relayer.RelayFees import fr.acinq.eclair.payment.send.PaymentInitiator._ -import fr.acinq.eclair.payment.{PaymentFailed, PaymentRequest} +import fr.acinq.eclair.payment.{PaymentFailed, Invoice} import fr.acinq.eclair.router.RouteCalculationSpec.makeUpdateShort import fr.acinq.eclair.router.Router.{PredefinedNodeRoute, PublicChannel} import fr.acinq.eclair.router.{Announcements, Router} @@ -111,39 +112,39 @@ class EclairImplSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with I val eclair = new EclairImpl(kit) val nodePrivKey = randomKey() - val invoice0 = PaymentRequest(Block.RegtestGenesisBlock.hash, Some(123 msat), ByteVector32.Zeroes, nodePrivKey, Left("description"), CltvExpiryDelta(18)) + val invoice0 = Bolt11Invoice(Block.RegtestGenesisBlock.hash, Some(123 msat), ByteVector32.Zeroes, nodePrivKey, Left("description"), CltvExpiryDelta(18)) eclair.send(None, 123 msat, invoice0) val send = paymentInitiator.expectMsgType[SendPaymentToNode] assert(send.externalId === None) assert(send.recipientNodeId === nodePrivKey.publicKey) assert(send.recipientAmount === 123.msat) assert(send.paymentHash === ByteVector32.Zeroes) - assert(send.paymentRequest === invoice0) + assert(send.invoice === invoice0) assert(send.assistedRoutes === Seq.empty) // with assisted routes val externalId1 = "030bb6a5e0c6b203c7e2180fb78c7ba4bdce46126761d8201b91ddac089cdecc87" val hints = List(List(ExtraHop(Bob.nodeParams.nodeId, ShortChannelId("569178x2331x1"), feeBase = 10 msat, feeProportionalMillionths = 1, cltvExpiryDelta = CltvExpiryDelta(12)))) - val invoice1 = PaymentRequest(Block.RegtestGenesisBlock.hash, Some(123 msat), ByteVector32.Zeroes, nodePrivKey, Left("description"), CltvExpiryDelta(18), None, None, hints) + val invoice1 = Bolt11Invoice(Block.RegtestGenesisBlock.hash, Some(123 msat), ByteVector32.Zeroes, nodePrivKey, Left("description"), CltvExpiryDelta(18), None, None, hints) eclair.send(Some(externalId1), 123 msat, invoice1) val send1 = paymentInitiator.expectMsgType[SendPaymentToNode] assert(send1.externalId === Some(externalId1)) assert(send1.recipientNodeId === nodePrivKey.publicKey) assert(send1.recipientAmount === 123.msat) assert(send1.paymentHash === ByteVector32.Zeroes) - assert(send1.paymentRequest === invoice1) + assert(send1.invoice === invoice1) assert(send1.assistedRoutes === hints) // with finalCltvExpiry val externalId2 = "487da196-a4dc-4b1e-92b4-3e5e905e9f3f" - val invoice2 = PaymentRequest("lntb", Some(123 msat), TimestampSecond.now(), nodePrivKey.publicKey, List(PaymentRequest.MinFinalCltvExpiry(96), PaymentRequest.PaymentHash(ByteVector32.Zeroes), PaymentRequest.Description("description")), ByteVector.empty) + val invoice2 = Bolt11Invoice("lntb", Some(123 msat), TimestampSecond.now(), nodePrivKey.publicKey, List(Bolt11Invoice.MinFinalCltvExpiry(96), Bolt11Invoice.PaymentHash(ByteVector32.Zeroes), Bolt11Invoice.Description("description")), ByteVector.empty) eclair.send(Some(externalId2), 123 msat, invoice2) val send2 = paymentInitiator.expectMsgType[SendPaymentToNode] assert(send2.externalId === Some(externalId2)) assert(send2.recipientNodeId === nodePrivKey.publicKey) assert(send2.recipientAmount === 123.msat) assert(send2.paymentHash === ByteVector32.Zeroes) - assert(send2.paymentRequest === invoice2) + assert(send2.invoice === invoice2) assert(send2.fallbackFinalExpiryDelta === CltvExpiryDelta(96)) // with custom route fees parameters @@ -159,7 +160,7 @@ class EclairImplSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with I val invalidExternalId = "Robert'); DROP TABLE received_payments; DROP TABLE sent_payments; DROP TABLE payments;" assertThrows[IllegalArgumentException](Await.result(eclair.send(Some(invalidExternalId), 123 msat, invoice0), 50 millis)) - val expiredInvoice = invoice2.copy(timestamp = 0.unixsec) + val expiredInvoice = invoice2.copy(createdAt = 0.unixsec) assertThrows[IllegalArgumentException](Await.result(eclair.send(None, 123 msat, expiredInvoice), 50 millis)) } @@ -296,7 +297,7 @@ class EclairImplSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with I val paymentPreimage = randomBytes32() eclair.receive(Left("some desc"), None, None, None, Some(paymentPreimage)).pipeTo(sender.ref) - assert(sender.expectMsgType[PaymentRequest].paymentHash === Crypto.sha256(paymentPreimage)) + assert(sender.expectMsgType[Invoice].paymentHash === Crypto.sha256(paymentPreimage)) } test("sendtoroute should pass the parameters correctly") { f => @@ -307,7 +308,7 @@ class EclairImplSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with I val trampolines = Seq(randomKey().publicKey, randomKey().publicKey) val parentId = UUID.randomUUID() val secret = randomBytes32() - val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(1234 msat), ByteVector32.One, randomKey(), Right(randomBytes32()), CltvExpiryDelta(18)) + val pr = Bolt11Invoice(Block.LivenetGenesisBlock.hash, Some(1234 msat), ByteVector32.One, randomKey(), Right(randomBytes32()), CltvExpiryDelta(18)) eclair.sendToRoute(1000 msat, Some(1200 msat), Some("42"), Some(parentId), pr, CltvExpiryDelta(123), route, Some(secret), Some(100 msat), Some(CltvExpiryDelta(144)), trampolines) paymentInitiator.expectMsg(SendPaymentToRoute(1000 msat, 1200 msat, pr, CltvExpiryDelta(123), route, Some("42"), Some(parentId), Some(secret), 100 msat, CltvExpiryDelta(144), trampolines)) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/FeaturesSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/FeaturesSpec.scala index 0e2a700b1c..72c9df259c 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/FeaturesSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/FeaturesSpec.scala @@ -224,7 +224,7 @@ class FeaturesSpec extends AnyFunSuite { Map[Feature, FeatureSupport](DataLossProtect -> Optional, VariableLengthOnion -> Mandatory), Set(UnknownFeature(753), UnknownFeature(852)) )) - assert(features.invoiceFeatures() === Map[Feature, FeatureSupport](VariableLengthOnion -> Mandatory, PaymentMetadata -> Optional)) + assert(features.invoiceFeatures() === Features(VariableLengthOnion -> Mandatory, PaymentMetadata -> Optional)) } test("features to bytes") { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala b/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala index 3c6af660ac..37781da9ff 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala @@ -144,7 +144,7 @@ object TestConstants { initialRandomReconnectDelay = 5 seconds, maxReconnectInterval = 1 hour, chainHash = Block.RegtestGenesisBlock.hash, - paymentRequestExpiry = 1 hour, + invoiceExpiry = 1 hour, multiPartPaymentExpiry = 30 seconds, peerConnectionConf = PeerConnection.Conf( authTimeout = 10 seconds, @@ -279,7 +279,7 @@ object TestConstants { initialRandomReconnectDelay = 5 seconds, maxReconnectInterval = 1 hour, chainHash = Block.RegtestGenesisBlock.hash, - paymentRequestExpiry = 1 hour, + invoiceExpiry = 1 hour, multiPartPaymentExpiry = 30 seconds, peerConnectionConf = PeerConnection.Conf( authTimeout = 10 seconds, diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/FuzzySpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/FuzzySpec.scala index 814ea3fbf9..a968c65f25 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/FuzzySpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/FuzzySpec.scala @@ -129,7 +129,7 @@ class FuzzySpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Channe if (remaining > 0) { paymentHandler ! ReceivePayment(Some(requiredAmount), Left("One coffee")) context become { - case req: PaymentRequest => + case req: Invoice => sendChannel ! buildCmdAdd(req.paymentHash, req.nodeId, req.paymentSecret.get) context become { case RES_SUCCESS(_: CMD_ADD_HTLC, _) => () diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/PaymentsDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/PaymentsDbSpec.scala index e48e245af7..ad8e5778e5 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/PaymentsDbSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/PaymentsDbSpec.scala @@ -78,8 +78,8 @@ class PaymentsDbSpec extends AnyFunSuite { // add a few rows val ps1 = OutgoingPayment(UUID.randomUUID(), UUID.randomUUID(), None, paymentHash1, PaymentType.Standard, 12345 msat, 12345 msat, alice, 1000 unixms, None, OutgoingPaymentStatus.Pending) - val i1 = PaymentRequest(Block.TestnetGenesisBlock.hash, Some(500 msat), paymentHash1, davePriv, Left("Some invoice"), CltvExpiryDelta(18), expirySeconds = None, timestamp = 1 unixsec) - val pr1 = IncomingPayment(i1, preimage1, PaymentType.Standard, i1.timestamp.toTimestampMilli, IncomingPaymentStatus.Received(550 msat, 1100 unixms)) + val i1 = Bolt11Invoice(Block.TestnetGenesisBlock.hash, Some(500 msat), paymentHash1, davePriv, Left("Some invoice"), CltvExpiryDelta(18), expirySeconds = None, timestamp = 1 unixsec) + val pr1 = IncomingPayment(i1, preimage1, PaymentType.Standard, i1.createdAt.toTimestampMilli, IncomingPaymentStatus.Received(550 msat, 1100 unixms)) db.addOutgoingPayment(ps1) db.addIncomingPayment(i1, preimage1) @@ -102,10 +102,10 @@ class PaymentsDbSpec extends AnyFunSuite { val ps1 = OutgoingPayment(id1, id1, None, randomBytes32(), PaymentType.Standard, 561 msat, 561 msat, PrivateKey(ByteVector32.One).publicKey, 1000 unixms, None, OutgoingPaymentStatus.Pending) val ps2 = OutgoingPayment(id2, id2, None, randomBytes32(), PaymentType.Standard, 1105 msat, 1105 msat, PrivateKey(ByteVector32.One).publicKey, 1010 unixms, None, OutgoingPaymentStatus.Failed(Nil, 1050 unixms)) val ps3 = OutgoingPayment(id3, id3, None, paymentHash1, PaymentType.Standard, 1729 msat, 1729 msat, PrivateKey(ByteVector32.One).publicKey, 1040 unixms, None, OutgoingPaymentStatus.Succeeded(preimage1, 0 msat, Nil, 1060 unixms)) - val i1 = PaymentRequest(Block.TestnetGenesisBlock.hash, Some(12345678 msat), paymentHash1, davePriv, Left("Some invoice"), CltvExpiryDelta(18), expirySeconds = None, timestamp = 1 unixsec) - val pr1 = IncomingPayment(i1, preimage1, PaymentType.Standard, i1.timestamp.toTimestampMilli, IncomingPaymentStatus.Received(12345678 msat, 1090 unixms)) - val i2 = PaymentRequest(Block.TestnetGenesisBlock.hash, Some(12345678 msat), paymentHash2, carolPriv, Left("Another invoice"), CltvExpiryDelta(18), expirySeconds = Some(30), timestamp = 1 unixsec) - val pr2 = IncomingPayment(i2, preimage2, PaymentType.Standard, i2.timestamp.toTimestampMilli, IncomingPaymentStatus.Expired) + val i1 = Bolt11Invoice(Block.TestnetGenesisBlock.hash, Some(12345678 msat), paymentHash1, davePriv, Left("Some invoice"), CltvExpiryDelta(18), expirySeconds = None, timestamp = 1 unixsec) + val pr1 = IncomingPayment(i1, preimage1, PaymentType.Standard, i1.createdAt.toTimestampMilli, IncomingPaymentStatus.Received(12345678 msat, 1090 unixms)) + val i2 = Bolt11Invoice(Block.TestnetGenesisBlock.hash, Some(12345678 msat), paymentHash2, carolPriv, Left("Another invoice"), CltvExpiryDelta(18), expirySeconds = Some(30), timestamp = 1 unixsec) + val pr2 = IncomingPayment(i2, preimage2, PaymentType.Standard, i2.createdAt.toTimestampMilli, IncomingPaymentStatus.Expired) migrationCheck( dbs = dbs, @@ -163,7 +163,7 @@ class PaymentsDbSpec extends AnyFunSuite { using(connection.prepareStatement("INSERT INTO received_payments (payment_hash, preimage, payment_request, received_msat, created_at, received_at) VALUES (?, ?, ?, ?, ?, ?)")) { statement => statement.setBytes(1, i1.paymentHash.toArray) statement.setBytes(2, pr1.paymentPreimage.toArray) - statement.setString(3, PaymentRequest.write(i1)) + statement.setString(3, i1.toString) statement.setLong(4, pr1.status.asInstanceOf[IncomingPaymentStatus.Received].amount.toLong) statement.setLong(5, pr1.createdAt.toLong) statement.setLong(6, pr1.status.asInstanceOf[IncomingPaymentStatus.Received].receivedAt.toLong) @@ -173,9 +173,9 @@ class PaymentsDbSpec extends AnyFunSuite { using(connection.prepareStatement("INSERT INTO received_payments (payment_hash, preimage, payment_request, created_at, expire_at) VALUES (?, ?, ?, ?, ?)")) { statement => statement.setBytes(1, i2.paymentHash.toArray) statement.setBytes(2, pr2.paymentPreimage.toArray) - statement.setString(3, PaymentRequest.write(i2)) + statement.setString(3, i2.toString) statement.setLong(4, pr2.createdAt.toLong) - statement.setLong(5, (i2.timestamp + i2.expiry.get).toLong) + statement.setLong(5, (i2.createdAt + i2.relativeExpiry).toLong) statement.executeUpdate() } }, @@ -188,8 +188,8 @@ class PaymentsDbSpec extends AnyFunSuite { assert(db.getIncomingPayment(i2.paymentHash) === Some(pr2)) assert(db.listOutgoingPayments(1 unixms, 2000 unixms) === Seq(ps1, ps2, ps3)) - val i3 = PaymentRequest(Block.TestnetGenesisBlock.hash, Some(561 msat), paymentHash3, alicePriv, Left("invoice #3"), CltvExpiryDelta(18), expirySeconds = Some(30)) - val pr3 = IncomingPayment(i3, preimage3, PaymentType.Standard, i3.timestamp.toTimestampMilli, IncomingPaymentStatus.Pending) + val i3 = Bolt11Invoice(Block.TestnetGenesisBlock.hash, Some(561 msat), paymentHash3, alicePriv, Left("invoice #3"), CltvExpiryDelta(18), expirySeconds = Some(30)) + val pr3 = IncomingPayment(i3, preimage3, PaymentType.Standard, i3.createdAt.toTimestampMilli, IncomingPaymentStatus.Pending) db.addIncomingPayment(i3, pr3.paymentPreimage) val ps4 = OutgoingPayment(UUID.randomUUID(), UUID.randomUUID(), Some("1"), randomBytes32(), PaymentType.Standard, 123 msat, 123 msat, alice, 1100 unixms, Some(i3), OutgoingPaymentStatus.Pending) @@ -213,7 +213,7 @@ class PaymentsDbSpec extends AnyFunSuite { // Test data val (id1, id2, id3) = (UUID.randomUUID(), UUID.randomUUID(), UUID.randomUUID()) val parentId = UUID.randomUUID() - val invoice1 = PaymentRequest(Block.TestnetGenesisBlock.hash, Some(2834 msat), paymentHash1, bobPriv, Left("invoice #1"), CltvExpiryDelta(18), expirySeconds = Some(30)) + val invoice1 = Bolt11Invoice(Block.TestnetGenesisBlock.hash, Some(2834 msat), paymentHash1, bobPriv, Left("invoice #1"), CltvExpiryDelta(18), expirySeconds = Some(30)) val ps1 = OutgoingPayment(id1, id1, Some("42"), randomBytes32(), PaymentType.Standard, 561 msat, 561 msat, alice, 1000 unixms, None, OutgoingPaymentStatus.Failed(Seq(FailureSummary(FailureType.REMOTE, "no candy for you", List(HopSummary(hop_ab), HopSummary(hop_bc)), Some(bob))), 1020 unixms)) val ps2 = OutgoingPayment(id2, parentId, Some("42"), paymentHash1, PaymentType.Standard, 1105 msat, 1105 msat, bob, 1010 unixms, Some(invoice1), OutgoingPaymentStatus.Pending) val ps3 = OutgoingPayment(id3, parentId, None, paymentHash1, PaymentType.Standard, 1729 msat, 1729 msat, bob, 1040 unixms, None, OutgoingPaymentStatus.Succeeded(preimage1, 10 msat, Seq(HopSummary(hop_ab), HopSummary(hop_bc)), 1060 unixms)) @@ -256,7 +256,7 @@ class PaymentsDbSpec extends AnyFunSuite { statement.setLong(5, ps2.amount.toLong) statement.setBytes(6, ps2.recipientNodeId.value.toArray) statement.setLong(7, ps2.createdAt.toLong) - statement.setString(8, PaymentRequest.write(invoice1)) + statement.setString(8, invoice1.toString) statement.executeUpdate() } @@ -300,10 +300,10 @@ class PaymentsDbSpec extends AnyFunSuite { val ps1 = OutgoingPayment(id1, id1, None, randomBytes32(), PaymentType.Standard, 561 msat, 561 msat, PrivateKey(ByteVector32.One).publicKey, TimestampMilli(Instant.parse("2021-01-01T10:15:30.00Z").toEpochMilli), None, OutgoingPaymentStatus.Pending) val ps2 = OutgoingPayment(id2, id2, None, randomBytes32(), PaymentType.Standard, 1105 msat, 1105 msat, PrivateKey(ByteVector32.One).publicKey, TimestampMilli(Instant.parse("2020-05-14T13:47:21.00Z").toEpochMilli), None, OutgoingPaymentStatus.Failed(Nil, TimestampMilli(Instant.parse("2021-05-15T04:12:40.00Z").toEpochMilli))) val ps3 = OutgoingPayment(id3, id3, None, paymentHash1, PaymentType.Standard, 1729 msat, 1729 msat, PrivateKey(ByteVector32.One).publicKey, TimestampMilli(Instant.parse("2021-01-28T09:12:05.00Z").toEpochMilli), None, OutgoingPaymentStatus.Succeeded(preimage1, 0 msat, Nil, TimestampMilli.now())) - val i1 = PaymentRequest(Block.TestnetGenesisBlock.hash, Some(12345678 msat), paymentHash1, davePriv, Left("Some invoice"), CltvExpiryDelta(18), expirySeconds = None, timestamp = TimestampSecond.now()) - val pr1 = IncomingPayment(i1, preimage1, PaymentType.Standard, i1.timestamp.toTimestampMilli, IncomingPaymentStatus.Received(12345678 msat, TimestampMilli.now())) - val i2 = PaymentRequest(Block.TestnetGenesisBlock.hash, Some(12345678 msat), paymentHash2, carolPriv, Left("Another invoice"), CltvExpiryDelta(18), expirySeconds = Some(24 * 3600), timestamp = TimestampSecond(Instant.parse("2020-12-30T10:00:55.00Z").getEpochSecond)) - val pr2 = IncomingPayment(i2, preimage2, PaymentType.Standard, i2.timestamp.toTimestampMilli, IncomingPaymentStatus.Expired) + val i1 = Bolt11Invoice(Block.TestnetGenesisBlock.hash, Some(12345678 msat), paymentHash1, davePriv, Left("Some invoice"), CltvExpiryDelta(18), expirySeconds = None, timestamp = TimestampSecond.now()) + val pr1 = IncomingPayment(i1, preimage1, PaymentType.Standard, i1.createdAt.toTimestampMilli, IncomingPaymentStatus.Received(12345678 msat, TimestampMilli.now())) + val i2 = Bolt11Invoice(Block.TestnetGenesisBlock.hash, Some(12345678 msat), paymentHash2, carolPriv, Left("Another invoice"), CltvExpiryDelta(18), expirySeconds = Some(24 * 3600), timestamp = TimestampSecond(Instant.parse("2020-12-30T10:00:55.00Z").getEpochSecond)) + val pr2 = IncomingPayment(i2, preimage2, PaymentType.Standard, i2.createdAt.toTimestampMilli, IncomingPaymentStatus.Expired) migrationCheck( dbs = dbs, @@ -331,7 +331,7 @@ class PaymentsDbSpec extends AnyFunSuite { statement.setLong(7, sent.recipientAmount.toLong) statement.setString(8, sent.recipientNodeId.value.toHex) statement.setLong(9, sent.createdAt.toLong) - statement.setString(10, sent.paymentRequest.map(PaymentRequest.write).orNull) + statement.setString(10, sent.invoice.map(_.toString).orNull) sent.status match { case s: OutgoingPaymentStatus.Succeeded => statement.setLong(11, s.completedAt.toLong) @@ -347,14 +347,14 @@ class PaymentsDbSpec extends AnyFunSuite { } } - Seq((i1, preimage1), (i2, preimage2)).foreach { case (pr, preimage) => + Seq((i1, preimage1), (i2, preimage2)).foreach { case (invoice, preimage) => using(connection.prepareStatement("INSERT INTO received_payments (payment_hash, payment_preimage, payment_type, payment_request, created_at, expire_at) VALUES (?, ?, ?, ?, ?, ?)")) { statement => - statement.setString(1, pr.paymentHash.toHex) + statement.setString(1, invoice.paymentHash.toHex) statement.setString(2, preimage.toHex) statement.setString(3, PaymentType.Standard) - statement.setString(4, PaymentRequest.write(pr)) - statement.setLong(5, pr.timestamp.toTimestampMilli.toLong) // BOLT11 timestamp is in seconds - statement.setLong(6, (pr.timestamp + pr.expiry.getOrElse(PaymentRequest.DEFAULT_EXPIRY_SECONDS)).toTimestampMilli.toLong) + statement.setString(4, invoice.toString) + statement.setLong(5, invoice.createdAt.toTimestampMilli.toLong) // BOLT11 timestamp is in seconds + statement.setLong(6, (invoice.createdAt + invoice.relativeExpiry).toTimestampMilli.toLong) statement.executeUpdate() } } @@ -362,7 +362,7 @@ class PaymentsDbSpec extends AnyFunSuite { using(connection.prepareStatement("UPDATE received_payments SET (received_msat, received_at) = (? + COALESCE(received_msat, 0), ?) WHERE payment_hash = ?")) { update => update.setLong(1, pr1.status.asInstanceOf[IncomingPaymentStatus.Received].amount.toLong) update.setLong(2, pr1.status.asInstanceOf[IncomingPaymentStatus.Received].receivedAt.toLong) - update.setString(3, pr1.paymentRequest.paymentHash.toHex) + update.setString(3, pr1.invoice.paymentHash.toHex) val updated = update.executeUpdate() if (updated == 0) { throw new IllegalArgumentException("Inserted a received payment without having an invoice") @@ -403,22 +403,22 @@ class PaymentsDbSpec extends AnyFunSuite { assert(!db.receiveIncomingPayment(unknownPaymentHash, 12345678 msat)) assert(db.getIncomingPayment(unknownPaymentHash).isEmpty) - val expiredInvoice1 = PaymentRequest(Block.TestnetGenesisBlock.hash, Some(561 msat), randomBytes32(), alicePriv, Left("invoice #1"), CltvExpiryDelta(18), timestamp = 1 unixsec) - val expiredInvoice2 = PaymentRequest(Block.TestnetGenesisBlock.hash, Some(1105 msat), randomBytes32(), bobPriv, Left("invoice #2"), CltvExpiryDelta(18), timestamp = 2 unixsec, expirySeconds = Some(30)) - val expiredPayment1 = IncomingPayment(expiredInvoice1, randomBytes32(), PaymentType.Standard, expiredInvoice1.timestamp.toTimestampMilli, IncomingPaymentStatus.Expired) - val expiredPayment2 = IncomingPayment(expiredInvoice2, randomBytes32(), PaymentType.Standard, expiredInvoice2.timestamp.toTimestampMilli, IncomingPaymentStatus.Expired) + val expiredInvoice1 = Bolt11Invoice(Block.TestnetGenesisBlock.hash, Some(561 msat), randomBytes32(), alicePriv, Left("invoice #1"), CltvExpiryDelta(18), timestamp = 1 unixsec) + val expiredInvoice2 = Bolt11Invoice(Block.TestnetGenesisBlock.hash, Some(1105 msat), randomBytes32(), bobPriv, Left("invoice #2"), CltvExpiryDelta(18), timestamp = 2 unixsec, expirySeconds = Some(30)) + val expiredPayment1 = IncomingPayment(expiredInvoice1, randomBytes32(), PaymentType.Standard, expiredInvoice1.createdAt.toTimestampMilli, IncomingPaymentStatus.Expired) + val expiredPayment2 = IncomingPayment(expiredInvoice2, randomBytes32(), PaymentType.Standard, expiredInvoice2.createdAt.toTimestampMilli, IncomingPaymentStatus.Expired) - val pendingInvoice1 = PaymentRequest(Block.TestnetGenesisBlock.hash, Some(561 msat), randomBytes32(), alicePriv, Left("invoice #3"), CltvExpiryDelta(18)) - val pendingInvoice2 = PaymentRequest(Block.TestnetGenesisBlock.hash, Some(1105 msat), randomBytes32(), bobPriv, Left("invoice #4"), CltvExpiryDelta(18), expirySeconds = Some(30)) - val pendingPayment1 = IncomingPayment(pendingInvoice1, randomBytes32(), PaymentType.Standard, pendingInvoice1.timestamp.toTimestampMilli, IncomingPaymentStatus.Pending) - val pendingPayment2 = IncomingPayment(pendingInvoice2, randomBytes32(), PaymentType.SwapIn, pendingInvoice2.timestamp.toTimestampMilli, IncomingPaymentStatus.Pending) + val pendingInvoice1 = Bolt11Invoice(Block.TestnetGenesisBlock.hash, Some(561 msat), randomBytes32(), alicePriv, Left("invoice #3"), CltvExpiryDelta(18)) + val pendingInvoice2 = Bolt11Invoice(Block.TestnetGenesisBlock.hash, Some(1105 msat), randomBytes32(), bobPriv, Left("invoice #4"), CltvExpiryDelta(18), expirySeconds = Some(30)) + val pendingPayment1 = IncomingPayment(pendingInvoice1, randomBytes32(), PaymentType.Standard, pendingInvoice1.createdAt.toTimestampMilli, IncomingPaymentStatus.Pending) + val pendingPayment2 = IncomingPayment(pendingInvoice2, randomBytes32(), PaymentType.SwapIn, pendingInvoice2.createdAt.toTimestampMilli, IncomingPaymentStatus.Pending) - val paidInvoice1 = PaymentRequest(Block.TestnetGenesisBlock.hash, Some(561 msat), randomBytes32(), alicePriv, Left("invoice #5"), CltvExpiryDelta(18)) - val paidInvoice2 = PaymentRequest(Block.TestnetGenesisBlock.hash, Some(1105 msat), randomBytes32(), bobPriv, Left("invoice #6"), CltvExpiryDelta(18), expirySeconds = Some(60)) + val paidInvoice1 = Bolt11Invoice(Block.TestnetGenesisBlock.hash, Some(561 msat), randomBytes32(), alicePriv, Left("invoice #5"), CltvExpiryDelta(18)) + val paidInvoice2 = Bolt11Invoice(Block.TestnetGenesisBlock.hash, Some(1105 msat), randomBytes32(), bobPriv, Left("invoice #6"), CltvExpiryDelta(18), expirySeconds = Some(60)) val receivedAt1 = TimestampMilli.now() + 1.milli val receivedAt2 = TimestampMilli.now() + 2.milli - val payment1 = IncomingPayment(paidInvoice1, randomBytes32(), PaymentType.Standard, paidInvoice1.timestamp.toTimestampMilli, IncomingPaymentStatus.Received(561 msat, receivedAt2)) - val payment2 = IncomingPayment(paidInvoice2, randomBytes32(), PaymentType.Standard, paidInvoice2.timestamp.toTimestampMilli, IncomingPaymentStatus.Received(1111 msat, receivedAt2)) + val payment1 = IncomingPayment(paidInvoice1, randomBytes32(), PaymentType.Standard, paidInvoice1.createdAt.toTimestampMilli, IncomingPaymentStatus.Received(561 msat, receivedAt2)) + val payment2 = IncomingPayment(paidInvoice2, randomBytes32(), PaymentType.Standard, paidInvoice2.createdAt.toTimestampMilli, IncomingPaymentStatus.Received(1111 msat, receivedAt2)) db.addIncomingPayment(pendingInvoice1, pendingPayment1.paymentPreimage) db.addIncomingPayment(pendingInvoice2, pendingPayment2.paymentPreimage, PaymentType.SwapIn) @@ -450,10 +450,10 @@ class PaymentsDbSpec extends AnyFunSuite { assert(db.removeIncomingPayment(paidInvoice1.paymentHash).isFailure) db.removeIncomingPayment(paidInvoice1.paymentHash).failed.foreach(e => assert(e.getMessage === "Cannot remove a received incoming payment")) - assert(db.removeIncomingPayment(pendingPayment1.paymentRequest.paymentHash).isSuccess) - assert(db.removeIncomingPayment(pendingPayment1.paymentRequest.paymentHash).isSuccess) // idempotent - assert(db.removeIncomingPayment(expiredPayment1.paymentRequest.paymentHash).isSuccess) - assert(db.removeIncomingPayment(expiredPayment1.paymentRequest.paymentHash).isSuccess) // idempotent + assert(db.removeIncomingPayment(pendingPayment1.invoice.paymentHash).isSuccess) + assert(db.removeIncomingPayment(pendingPayment1.invoice.paymentHash).isSuccess) // idempotent + assert(db.removeIncomingPayment(expiredPayment1.invoice.paymentHash).isSuccess) + assert(db.removeIncomingPayment(expiredPayment1.invoice.paymentHash).isSuccess) // idempotent } } @@ -462,7 +462,7 @@ class PaymentsDbSpec extends AnyFunSuite { val db = dbs.payments val parentId = UUID.randomUUID() - val i1 = PaymentRequest(Block.TestnetGenesisBlock.hash, Some(123 msat), paymentHash1, davePriv, Left("Some invoice"), CltvExpiryDelta(18), expirySeconds = None, timestamp = 0 unixsec) + val i1 = Bolt11Invoice(Block.TestnetGenesisBlock.hash, Some(123 msat), paymentHash1, davePriv, Left("Some invoice"), CltvExpiryDelta(18), expirySeconds = None, timestamp = 0 unixsec) val s1 = OutgoingPayment(UUID.randomUUID(), parentId, None, paymentHash1, PaymentType.Standard, 123 msat, 600 msat, dave, 100 unixms, Some(i1), OutgoingPaymentStatus.Pending) val s2 = OutgoingPayment(UUID.randomUUID(), parentId, Some("1"), paymentHash1, PaymentType.SwapOut, 456 msat, 600 msat, dave, 200 unixms, None, OutgoingPaymentStatus.Pending) @@ -518,16 +518,16 @@ class PaymentsDbSpec extends AnyFunSuite { val db = new SqlitePaymentsDb(TestDatabases.sqliteInMemory()) // -- feed db with incoming payments - val expiredInvoice = PaymentRequest(Block.TestnetGenesisBlock.hash, Some(123 msat), randomBytes32(), alicePriv, Left("incoming #1"), CltvExpiryDelta(18), timestamp = 1 unixsec) + val expiredInvoice = Bolt11Invoice(Block.TestnetGenesisBlock.hash, Some(123 msat), randomBytes32(), alicePriv, Left("incoming #1"), CltvExpiryDelta(18), timestamp = 1 unixsec) val expiredPayment = IncomingPayment(expiredInvoice, randomBytes32(), PaymentType.Standard, 100 unixms, IncomingPaymentStatus.Expired) - val pendingInvoice = PaymentRequest(Block.TestnetGenesisBlock.hash, Some(456 msat), randomBytes32(), alicePriv, Left("incoming #2"), CltvExpiryDelta(18)) + val pendingInvoice = Bolt11Invoice(Block.TestnetGenesisBlock.hash, Some(456 msat), randomBytes32(), alicePriv, Left("incoming #2"), CltvExpiryDelta(18)) val pendingPayment = IncomingPayment(pendingInvoice, randomBytes32(), PaymentType.Standard, 120 unixms, IncomingPaymentStatus.Pending) - val paidInvoice1 = PaymentRequest(Block.TestnetGenesisBlock.hash, Some(789 msat), randomBytes32(), alicePriv, Left("incoming #3"), CltvExpiryDelta(18)) + val paidInvoice1 = Bolt11Invoice(Block.TestnetGenesisBlock.hash, Some(789 msat), randomBytes32(), alicePriv, Left("incoming #3"), CltvExpiryDelta(18)) val receivedAt1 = 150 unixms val receivedPayment1 = IncomingPayment(paidInvoice1, randomBytes32(), PaymentType.Standard, 130 unixms, IncomingPaymentStatus.Received(561 msat, receivedAt1)) - val paidInvoice2 = PaymentRequest(Block.TestnetGenesisBlock.hash, Some(888 msat), randomBytes32(), alicePriv, Left("incoming #4"), CltvExpiryDelta(18)) + val paidInvoice2 = Bolt11Invoice(Block.TestnetGenesisBlock.hash, Some(888 msat), randomBytes32(), alicePriv, Left("incoming #4"), CltvExpiryDelta(18)) val receivedAt2 = 160 unixms - val receivedPayment2 = IncomingPayment(paidInvoice2, randomBytes32(), PaymentType.Standard, paidInvoice2.timestamp.toTimestampMilli, IncomingPaymentStatus.Received(889 msat, receivedAt2)) + val receivedPayment2 = IncomingPayment(paidInvoice2, randomBytes32(), PaymentType.Standard, paidInvoice2.createdAt.toTimestampMilli, IncomingPaymentStatus.Received(889 msat, receivedAt2)) db.addIncomingPayment(pendingInvoice, pendingPayment.paymentPreimage) db.addIncomingPayment(expiredInvoice, expiredPayment.paymentPreimage) db.addIncomingPayment(paidInvoice1, receivedPayment1.paymentPreimage) @@ -538,7 +538,7 @@ class PaymentsDbSpec extends AnyFunSuite { // -- feed db with outgoing payments val parentId1 = UUID.randomUUID() val parentId2 = UUID.randomUUID() - val invoice = PaymentRequest(Block.TestnetGenesisBlock.hash, Some(1337 msat), paymentHash1, davePriv, Left("outgoing #1"), CltvExpiryDelta(18), expirySeconds = None, timestamp = 0 unixsec) + val invoice = Bolt11Invoice(Block.TestnetGenesisBlock.hash, Some(1337 msat), paymentHash1, davePriv, Left("outgoing #1"), CltvExpiryDelta(18), expirySeconds = None, timestamp = 0 unixsec) // 1st attempt, pending -> failed val outgoing1 = OutgoingPayment(UUID.randomUUID(), parentId1, None, paymentHash1, PaymentType.Standard, 123 msat, 123 msat, alice, 200 unixms, Some(invoice), OutgoingPaymentStatus.Pending) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/ChannelIntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/ChannelIntegrationSpec.scala index 5f27628ab5..ae970d342f 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/ChannelIntegrationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/ChannelIntegrationSpec.scala @@ -145,7 +145,7 @@ abstract class ChannelIntegrationSpec extends IntegrationSpec { val preimage = randomBytes32() val paymentHash = Crypto.sha256(preimage) // A sends a payment to F - val paymentReq = SendPaymentToNode(100000000 msat, PaymentRequest(Block.RegtestGenesisBlock.hash, None, paymentHash, nodes("F").nodeParams.privateKey, Left("test"), finalCltvExpiryDelta), maxAttempts = 1, routeParams = integrationTestRouteParams) + val paymentReq = SendPaymentToNode(100000000 msat, Bolt11Invoice(Block.RegtestGenesisBlock.hash, None, paymentHash, nodes("F").nodeParams.privateKey, Left("test"), finalCltvExpiryDelta), maxAttempts = 1, routeParams = integrationTestRouteParams) val paymentSender = TestProbe() paymentSender.send(nodes("A").paymentInitiator, paymentReq) val paymentId = paymentSender.expectMsgType[UUID] @@ -367,8 +367,8 @@ abstract class ChannelIntegrationSpec extends IntegrationSpec { // we now send a few htlcs C->F and F->C in order to obtain a commitments with multiple htlcs def send(amountMsat: MilliSatoshi, paymentHandler: ActorRef, paymentInitiator: ActorRef): UUID = { sender.send(paymentHandler, ReceivePayment(Some(amountMsat), Left("1 coffee"))) - val pr = sender.expectMsgType[PaymentRequest] - val sendReq = SendPaymentToNode(amountMsat, pr, maxAttempts = 1, fallbackFinalExpiryDelta = finalCltvExpiryDelta, routeParams = integrationTestRouteParams) + val invoice = sender.expectMsgType[Invoice] + val sendReq = SendPaymentToNode(amountMsat, invoice, maxAttempts = 1, fallbackFinalExpiryDelta = finalCltvExpiryDelta, routeParams = integrationTestRouteParams) sender.send(paymentInitiator, sendReq) sender.expectMsgType[UUID] } @@ -681,14 +681,14 @@ abstract class AnchorChannelIntegrationSpec extends ChannelIntegrationSpec { // let's make a payment to advance the commit index val amountMsat = 4200000.msat sender.send(nodes("F").paymentHandler, ReceivePayment(Some(amountMsat), Left("1 coffee"))) - val pr = sender.expectMsgType[PaymentRequest] + val invoice = sender.expectMsgType[Invoice] // then we make the actual payment - sender.send(nodes("C").paymentInitiator, SendPaymentToNode(amountMsat, pr, maxAttempts = 1, fallbackFinalExpiryDelta = finalCltvExpiryDelta, routeParams = integrationTestRouteParams)) + sender.send(nodes("C").paymentInitiator, SendPaymentToNode(amountMsat, invoice, maxAttempts = 1, fallbackFinalExpiryDelta = finalCltvExpiryDelta, routeParams = integrationTestRouteParams)) val paymentId = sender.expectMsgType[UUID] val ps = sender.expectMsgType[PaymentSent](60 seconds) assert(ps.id == paymentId) - assert(Crypto.sha256(ps.paymentPreimage) === pr.paymentHash) + assert(Crypto.sha256(ps.paymentPreimage) === invoice.paymentHash) // we make sure the htlc has been removed from F's commitment before we force-close awaitCond({ diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/PaymentIntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/PaymentIntegrationSpec.scala index 1d90f2090a..4dff2e291b 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/PaymentIntegrationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/PaymentIntegrationSpec.scala @@ -32,7 +32,7 @@ import fr.acinq.eclair.crypto.Sphinx.DecryptedFailurePacket import fr.acinq.eclair.crypto.TransportHandler import fr.acinq.eclair.db._ import fr.acinq.eclair.io.Peer.PeerRoutingMessage -import fr.acinq.eclair.payment.PaymentRequest.ExtraHop +import fr.acinq.eclair.payment.Bolt11Invoice.ExtraHop import fr.acinq.eclair.payment._ import fr.acinq.eclair.payment.receive.MultiPartHandler.ReceivePayment import fr.acinq.eclair.payment.relay.Relayer @@ -42,7 +42,7 @@ import fr.acinq.eclair.router.Graph.WeightRatios import fr.acinq.eclair.router.Router.{GossipDecision, PublicChannel} import fr.acinq.eclair.router.{Announcements, AnnouncementsBatchValidationSpec, Router} import fr.acinq.eclair.wire.protocol.{ChannelAnnouncement, ChannelUpdate, IncorrectOrUnknownPaymentDetails, NodeAnnouncement} -import fr.acinq.eclair.{CltvExpiryDelta, Kit, MilliSatoshiLong, ShortChannelId, TimestampMilli, randomBytes32} +import fr.acinq.eclair.{CltvExpiryDelta, Features, Kit, MilliSatoshiLong, ShortChannelId, TimestampMilli, randomBytes32} import org.json4s.JsonAST.{JString, JValue} import scodec.bits.ByteVector @@ -159,16 +159,16 @@ class PaymentIntegrationSpec extends IntegrationSpec { // first we retrieve a payment hash from D val amountMsat = 4200000.msat sender.send(nodes("D").paymentHandler, ReceivePayment(Some(amountMsat), Left("1 coffee"))) - val pr = sender.expectMsgType[PaymentRequest] - assert(pr.paymentMetadata.nonEmpty) + val invoice = sender.expectMsgType[Invoice] + assert(invoice.paymentMetadata.nonEmpty) // then we make the actual payment - sender.send(nodes("A").paymentInitiator, SendPaymentToNode(amountMsat, pr, fallbackFinalExpiryDelta = finalCltvExpiryDelta, routeParams = integrationTestRouteParams, maxAttempts = 1)) + sender.send(nodes("A").paymentInitiator, SendPaymentToNode(amountMsat, invoice, fallbackFinalExpiryDelta = finalCltvExpiryDelta, routeParams = integrationTestRouteParams, maxAttempts = 1)) val paymentId = sender.expectMsgType[UUID] val ps = sender.expectMsgType[PaymentSent] assert(ps.id == paymentId) - assert(Crypto.sha256(ps.paymentPreimage) === pr.paymentHash) - eventListener.expectMsg(PaymentMetadataReceived(pr.paymentHash, pr.paymentMetadata.get)) + assert(Crypto.sha256(ps.paymentPreimage) === invoice.paymentHash) + eventListener.expectMsg(PaymentMetadataReceived(invoice.paymentHash, invoice.paymentMetadata.get)) } test("send an HTLC A->D with an invalid expiry delta for B") { @@ -187,15 +187,15 @@ class PaymentIntegrationSpec extends IntegrationSpec { // we retrieve a payment hash from D val amountMsat = 4200000.msat sender.send(nodes("D").paymentHandler, ReceivePayment(Some(amountMsat), Left("1 coffee"))) - val pr = sender.expectMsgType[PaymentRequest] + val invoice = sender.expectMsgType[Invoice] // then we make the actual payment, do not randomize the route to make sure we route through node B - val sendReq = SendPaymentToNode(amountMsat, pr, fallbackFinalExpiryDelta = finalCltvExpiryDelta, routeParams = integrationTestRouteParams, maxAttempts = 5) + val sendReq = SendPaymentToNode(amountMsat, invoice, fallbackFinalExpiryDelta = finalCltvExpiryDelta, routeParams = integrationTestRouteParams, maxAttempts = 5) sender.send(nodes("A").paymentInitiator, sendReq) // A will receive an error from B that include the updated channel update, then will retry the payment val paymentId = sender.expectMsgType[UUID] val ps = sender.expectMsgType[PaymentSent] assert(ps.id == paymentId) - assert(Crypto.sha256(ps.paymentPreimage) === pr.paymentHash) + assert(Crypto.sha256(ps.paymentPreimage) === invoice.paymentHash) def updateFor(n: PublicKey, pc: PublicChannel): Option[ChannelUpdate] = if (n == pc.ann.nodeId1) pc.update_1_opt else if (n == pc.ann.nodeId2) pc.update_2_opt else throw new IllegalArgumentException("this node is unrelated to this channel") @@ -228,9 +228,9 @@ class PaymentIntegrationSpec extends IntegrationSpec { // first we retrieve a payment hash from D val amountMsat = 300000000.msat sender.send(nodes("D").paymentHandler, ReceivePayment(Some(amountMsat), Left("1 coffee"))) - val pr = sender.expectMsgType[PaymentRequest] + val invoice = sender.expectMsgType[Invoice] // then we make the payment (B-C has a smaller capacity than A-B and C-D) - val sendReq = SendPaymentToNode(amountMsat, pr, fallbackFinalExpiryDelta = finalCltvExpiryDelta, routeParams = integrationTestRouteParams, maxAttempts = 5) + val sendReq = SendPaymentToNode(amountMsat, invoice, fallbackFinalExpiryDelta = finalCltvExpiryDelta, routeParams = integrationTestRouteParams, maxAttempts = 5) sender.send(nodes("A").paymentInitiator, sendReq) // A will first receive an error from C, then retry and route around C: A->B->E->C->D sender.expectMsgType[UUID] @@ -240,15 +240,15 @@ class PaymentIntegrationSpec extends IntegrationSpec { test("send an HTLC A->D with an unknown payment hash") { val sender = TestProbe() val amount = 100000000 msat - val unknownInvoice = PaymentRequest(Block.RegtestGenesisBlock.hash, Some(amount), randomBytes32(), nodes("D").nodeParams.privateKey, Left("test"), finalCltvExpiryDelta) - val pr = SendPaymentToNode(amount, unknownInvoice, routeParams = integrationTestRouteParams, maxAttempts = 5) - sender.send(nodes("A").paymentInitiator, pr) + val unknownInvoice = Bolt11Invoice(Block.RegtestGenesisBlock.hash, Some(amount), randomBytes32(), nodes("D").nodeParams.privateKey, Left("test"), finalCltvExpiryDelta) + val invoice = SendPaymentToNode(amount, unknownInvoice, routeParams = integrationTestRouteParams, maxAttempts = 5) + sender.send(nodes("A").paymentInitiator, invoice) // A will receive an error from D and won't retry val paymentId = sender.expectMsgType[UUID] val failed = sender.expectMsgType[PaymentFailed] assert(failed.id == paymentId) - assert(failed.paymentHash === pr.paymentHash) + assert(failed.paymentHash === invoice.paymentHash) assert(failed.failures.size === 1) assert(failed.failures.head.asInstanceOf[RemoteFailure].e === DecryptedFailurePacket(nodes("D").nodeParams.nodeId, IncorrectOrUnknownPaymentDetails(amount, getBlockHeight()))) } @@ -258,17 +258,17 @@ class PaymentIntegrationSpec extends IntegrationSpec { // first we retrieve a payment hash from D for 2 mBTC val amountMsat = 200000000.msat sender.send(nodes("D").paymentHandler, ReceivePayment(Some(amountMsat), Left("1 coffee"))) - val pr = sender.expectMsgType[PaymentRequest] + val invoice = sender.expectMsgType[Invoice] // A send payment of only 1 mBTC - val sendReq = SendPaymentToNode(100000000 msat, pr, fallbackFinalExpiryDelta = finalCltvExpiryDelta, routeParams = integrationTestRouteParams, maxAttempts = 5) + val sendReq = SendPaymentToNode(100000000 msat, invoice, fallbackFinalExpiryDelta = finalCltvExpiryDelta, routeParams = integrationTestRouteParams, maxAttempts = 5) sender.send(nodes("A").paymentInitiator, sendReq) // A will first receive an IncorrectPaymentAmount error from D val paymentId = sender.expectMsgType[UUID] val failed = sender.expectMsgType[PaymentFailed] assert(failed.id == paymentId) - assert(failed.paymentHash === pr.paymentHash) + assert(failed.paymentHash === invoice.paymentHash) assert(failed.failures.size === 1) assert(failed.failures.head.asInstanceOf[RemoteFailure].e === DecryptedFailurePacket(nodes("D").nodeParams.nodeId, IncorrectOrUnknownPaymentDetails(100000000 msat, getBlockHeight()))) } @@ -278,17 +278,17 @@ class PaymentIntegrationSpec extends IntegrationSpec { // first we retrieve a payment hash from D for 2 mBTC val amountMsat = 200000000.msat sender.send(nodes("D").paymentHandler, ReceivePayment(Some(amountMsat), Left("1 coffee"))) - val pr = sender.expectMsgType[PaymentRequest] + val invoice = sender.expectMsgType[Invoice] // A send payment of 6 mBTC - val sendReq = SendPaymentToNode(600000000 msat, pr, fallbackFinalExpiryDelta = finalCltvExpiryDelta, routeParams = integrationTestRouteParams, maxAttempts = 5) + val sendReq = SendPaymentToNode(600000000 msat, invoice, fallbackFinalExpiryDelta = finalCltvExpiryDelta, routeParams = integrationTestRouteParams, maxAttempts = 5) sender.send(nodes("A").paymentInitiator, sendReq) // A will first receive an IncorrectPaymentAmount error from D val paymentId = sender.expectMsgType[UUID] val failed = sender.expectMsgType[PaymentFailed] assert(paymentId == failed.id) - assert(failed.paymentHash === pr.paymentHash) + assert(failed.paymentHash === invoice.paymentHash) assert(failed.failures.size === 1) assert(failed.failures.head.asInstanceOf[RemoteFailure].e === DecryptedFailurePacket(nodes("D").nodeParams.nodeId, IncorrectOrUnknownPaymentDetails(600000000 msat, getBlockHeight()))) } @@ -298,10 +298,10 @@ class PaymentIntegrationSpec extends IntegrationSpec { // first we retrieve a payment hash from D for 2 mBTC val amountMsat = 200000000.msat sender.send(nodes("D").paymentHandler, ReceivePayment(Some(amountMsat), Left("1 coffee"))) - val pr = sender.expectMsgType[PaymentRequest] + val invoice = sender.expectMsgType[Invoice] // A send payment of 3 mBTC, more than asked but it should still be accepted - val sendReq = SendPaymentToNode(300000000 msat, pr, fallbackFinalExpiryDelta = finalCltvExpiryDelta, routeParams = integrationTestRouteParams, maxAttempts = 5) + val sendReq = SendPaymentToNode(300000000 msat, invoice, fallbackFinalExpiryDelta = finalCltvExpiryDelta, routeParams = integrationTestRouteParams, maxAttempts = 5) sender.send(nodes("A").paymentInitiator, sendReq) sender.expectMsgType[UUID] } @@ -312,9 +312,9 @@ class PaymentIntegrationSpec extends IntegrationSpec { for (_ <- 0 until 7) { val amountMsat = 1000000000.msat sender.send(nodes("D").paymentHandler, ReceivePayment(Some(amountMsat), Left("1 payment"))) - val pr = sender.expectMsgType[PaymentRequest] + val invoice = sender.expectMsgType[Invoice] - val sendReq = SendPaymentToNode(amountMsat, pr, fallbackFinalExpiryDelta = finalCltvExpiryDelta, routeParams = integrationTestRouteParams, maxAttempts = 5) + val sendReq = SendPaymentToNode(amountMsat, invoice, fallbackFinalExpiryDelta = finalCltvExpiryDelta, routeParams = integrationTestRouteParams, maxAttempts = 5) sender.send(nodes("A").paymentInitiator, sendReq) sender.expectMsgType[UUID] sender.expectMsgType[PaymentSent] // the payment FSM will also reply to the sender after the payment is completed @@ -326,10 +326,10 @@ class PaymentIntegrationSpec extends IntegrationSpec { // first we retrieve a payment hash from C val amountMsat = 2000.msat sender.send(nodes("C").paymentHandler, ReceivePayment(Some(amountMsat), Left("Change from coffee"))) - val pr = sender.expectMsgType[PaymentRequest] + val invoice = sender.expectMsgType[Invoice] // the payment is requesting to use a capacity-optimized route which will select node G even though it's a bit more expensive - sender.send(nodes("A").paymentInitiator, SendPaymentToNode(amountMsat, pr, maxAttempts = 1, fallbackFinalExpiryDelta = finalCltvExpiryDelta, routeParams = integrationTestRouteParams.copy(heuristics = Left(WeightRatios(0, 0, 0, 1, RelayFees(0 msat, 0)))))) + sender.send(nodes("A").paymentInitiator, SendPaymentToNode(amountMsat, invoice, maxAttempts = 1, fallbackFinalExpiryDelta = finalCltvExpiryDelta, routeParams = integrationTestRouteParams.copy(heuristics = Left(WeightRatios(0, 0, 0, 1, RelayFees(0 msat, 0)))))) sender.expectMsgType[UUID] val ps = sender.expectMsgType[PaymentSent] ps.parts.foreach(part => assert(part.route.getOrElse(Nil).exists(_.nodeId == nodes("G").nodeParams.nodeId))) @@ -340,14 +340,14 @@ class PaymentIntegrationSpec extends IntegrationSpec { val sender = TestProbe() val amount = 1000000000L.msat sender.send(nodes("D").paymentHandler, ReceivePayment(Some(amount), Left("split the restaurant bill"))) - val pr = sender.expectMsgType[PaymentRequest] - assert(pr.features.allowMultiPart) + val invoice = sender.expectMsgType[Invoice] + assert(invoice.features.hasFeature(Features.BasicMultiPartPayment)) - sender.send(nodes("B").paymentInitiator, SendPaymentToNode(amount, pr, maxAttempts = 5, routeParams = integrationTestRouteParams)) + sender.send(nodes("B").paymentInitiator, SendPaymentToNode(amount, invoice, maxAttempts = 5, routeParams = integrationTestRouteParams)) val paymentId = sender.expectMsgType[UUID] val paymentSent = sender.expectMsgType[PaymentSent](max = 30 seconds) assert(paymentSent.id === paymentId, paymentSent) - assert(paymentSent.paymentHash === pr.paymentHash, paymentSent) + assert(paymentSent.paymentHash === invoice.paymentHash, paymentSent) assert(paymentSent.parts.length > 1, paymentSent) assert(paymentSent.recipientNodeId === nodes("D").nodeParams.nodeId, paymentSent) assert(paymentSent.recipientAmount === amount, paymentSent) @@ -367,8 +367,8 @@ class PaymentIntegrationSpec extends IntegrationSpec { assert(sent.length === 1, sent) assert(sent.head.copy(parts = sent.head.parts.sortBy(_.timestamp)) === paymentSent.copy(parts = paymentSent.parts.map(_.copy(route = None)).sortBy(_.timestamp)), sent) - awaitCond(nodes("D").nodeParams.db.payments.getIncomingPayment(pr.paymentHash).exists(_.status.isInstanceOf[IncomingPaymentStatus.Received])) - val Some(IncomingPayment(_, _, _, _, IncomingPaymentStatus.Received(receivedAmount, _))) = nodes("D").nodeParams.db.payments.getIncomingPayment(pr.paymentHash) + awaitCond(nodes("D").nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).exists(_.status.isInstanceOf[IncomingPaymentStatus.Received])) + val Some(IncomingPayment(_, _, _, _, IncomingPaymentStatus.Received(receivedAmount, _))) = nodes("D").nodeParams.db.payments.getIncomingPayment(invoice.paymentHash) assert(receivedAmount === amount) } @@ -379,21 +379,21 @@ class PaymentIntegrationSpec extends IntegrationSpec { // (the link C->D has too much capacity on D's side). val amount = 2000000000L.msat sender.send(nodes("D").paymentHandler, ReceivePayment(Some(amount), Left("well that's an expensive restaurant bill"))) - val pr = sender.expectMsgType[PaymentRequest] - assert(pr.features.allowMultiPart) + val invoice = sender.expectMsgType[Invoice] + assert(invoice.features.hasFeature(Features.BasicMultiPartPayment)) sender.send(nodes("B").relayer, Relayer.GetOutgoingChannels()) val canSend = sender.expectMsgType[Relayer.OutgoingChannels].channels.map(_.commitments.availableBalanceForSend).sum assert(canSend > amount) - sender.send(nodes("B").paymentInitiator, SendPaymentToNode(amount, pr, maxAttempts = 1, routeParams = integrationTestRouteParams)) + sender.send(nodes("B").paymentInitiator, SendPaymentToNode(amount, invoice, maxAttempts = 1, routeParams = integrationTestRouteParams)) val paymentId = sender.expectMsgType[UUID] val paymentFailed = sender.expectMsgType[PaymentFailed](max = 30 seconds) assert(paymentFailed.id === paymentId, paymentFailed) - assert(paymentFailed.paymentHash === pr.paymentHash, paymentFailed) + assert(paymentFailed.paymentHash === invoice.paymentHash, paymentFailed) assert(paymentFailed.failures.length > 1, paymentFailed) - assert(nodes("D").nodeParams.db.payments.getIncomingPayment(pr.paymentHash).get.status === IncomingPaymentStatus.Pending) + assert(nodes("D").nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).get.status === IncomingPaymentStatus.Pending) sender.send(nodes("B").relayer, Relayer.GetOutgoingChannels()) val canSend2 = sender.expectMsgType[Relayer.OutgoingChannels].channels.map(_.commitments.availableBalanceForSend).sum @@ -406,14 +406,14 @@ class PaymentIntegrationSpec extends IntegrationSpec { // This amount is greater than any channel capacity between D and C, so it should be split. val amount = 5100000000L.msat sender.send(nodes("C").paymentHandler, ReceivePayment(Some(amount), Left("lemme borrow some money"))) - val pr = sender.expectMsgType[PaymentRequest] - assert(pr.features.allowMultiPart) + val invoice = sender.expectMsgType[Invoice] + assert(invoice.features.hasFeature(Features.BasicMultiPartPayment)) - sender.send(nodes("D").paymentInitiator, SendPaymentToNode(amount, pr, maxAttempts = 3, routeParams = integrationTestRouteParams)) + sender.send(nodes("D").paymentInitiator, SendPaymentToNode(amount, invoice, maxAttempts = 3, routeParams = integrationTestRouteParams)) val paymentId = sender.expectMsgType[UUID] val paymentSent = sender.expectMsgType[PaymentSent](max = 30 seconds) assert(paymentSent.id === paymentId, paymentSent) - assert(paymentSent.paymentHash === pr.paymentHash, paymentSent) + assert(paymentSent.paymentHash === invoice.paymentHash, paymentSent) assert(paymentSent.parts.length > 1, paymentSent) assert(paymentSent.recipientAmount === amount, paymentSent) assert(paymentSent.feesPaid === 0.msat, paymentSent) // no fees when using direct channels @@ -424,8 +424,8 @@ class PaymentIntegrationSpec extends IntegrationSpec { assert(paymentParts.forall(p => p.parentId != p.id), paymentParts) assert(paymentParts.forall(p => p.status.asInstanceOf[OutgoingPaymentStatus.Succeeded].feesPaid == 0.msat), paymentParts) - awaitCond(nodes("C").nodeParams.db.payments.getIncomingPayment(pr.paymentHash).exists(_.status.isInstanceOf[IncomingPaymentStatus.Received])) - val Some(IncomingPayment(_, _, _, _, IncomingPaymentStatus.Received(receivedAmount, _))) = nodes("C").nodeParams.db.payments.getIncomingPayment(pr.paymentHash) + awaitCond(nodes("C").nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).exists(_.status.isInstanceOf[IncomingPaymentStatus.Received])) + val Some(IncomingPayment(_, _, _, _, IncomingPaymentStatus.Received(receivedAmount, _))) = nodes("C").nodeParams.db.payments.getIncomingPayment(invoice.paymentHash) assert(receivedAmount === amount) } @@ -434,20 +434,20 @@ class PaymentIntegrationSpec extends IntegrationSpec { // This amount is greater than the current capacity between D and C. val amount = 10000000000L.msat sender.send(nodes("C").paymentHandler, ReceivePayment(Some(amount), Left("lemme borrow more money"))) - val pr = sender.expectMsgType[PaymentRequest] - assert(pr.features.allowMultiPart) + val invoice = sender.expectMsgType[Invoice] + assert(invoice.features.hasFeature(Features.BasicMultiPartPayment)) sender.send(nodes("D").relayer, Relayer.GetOutgoingChannels()) val canSend = sender.expectMsgType[Relayer.OutgoingChannels].channels.map(_.commitments.availableBalanceForSend).sum assert(canSend < amount) - sender.send(nodes("D").paymentInitiator, SendPaymentToNode(amount, pr, maxAttempts = 1, routeParams = integrationTestRouteParams)) + sender.send(nodes("D").paymentInitiator, SendPaymentToNode(amount, invoice, maxAttempts = 1, routeParams = integrationTestRouteParams)) val paymentId = sender.expectMsgType[UUID] val paymentFailed = sender.expectMsgType[PaymentFailed](max = 30 seconds) assert(paymentFailed.id === paymentId, paymentFailed) - assert(paymentFailed.paymentHash === pr.paymentHash, paymentFailed) + assert(paymentFailed.paymentHash === invoice.paymentHash, paymentFailed) - val incoming = nodes("C").nodeParams.db.payments.getIncomingPayment(pr.paymentHash) + val incoming = nodes("C").nodeParams.db.payments.getIncomingPayment(invoice.paymentHash) assert(incoming.get.status === IncomingPaymentStatus.Pending, incoming) sender.send(nodes("D").relayer, Relayer.GetOutgoingChannels()) @@ -461,32 +461,32 @@ class PaymentIntegrationSpec extends IntegrationSpec { val sender = TestProbe() val amount = 4000000000L.msat sender.send(nodes("F").paymentHandler, ReceivePayment(Some(amount), Left("like trampoline much?"))) - val pr = sender.expectMsgType[PaymentRequest] - assert(pr.features.allowMultiPart) - assert(pr.features.allowTrampoline) + val invoice = sender.expectMsgType[Invoice] + assert(invoice.features.hasFeature(Features.BasicMultiPartPayment)) + assert(invoice.features.hasFeature(Features.TrampolinePayment)) // The first attempt should fail, but the second one should succeed. val attempts = (1000 msat, CltvExpiryDelta(42)) :: (1000000 msat, CltvExpiryDelta(288)) :: Nil - val payment = SendTrampolinePayment(amount, pr, nodes("G").nodeParams.nodeId, attempts, routeParams = integrationTestRouteParams) + val payment = SendTrampolinePayment(amount, invoice, nodes("G").nodeParams.nodeId, attempts, routeParams = integrationTestRouteParams) sender.send(nodes("B").paymentInitiator, payment) val paymentId = sender.expectMsgType[UUID] val paymentSent = sender.expectMsgType[PaymentSent](max = 30 seconds) assert(paymentSent.id === paymentId, paymentSent) - assert(paymentSent.paymentHash === pr.paymentHash, paymentSent) + assert(paymentSent.paymentHash === invoice.paymentHash, paymentSent) assert(paymentSent.recipientNodeId === nodes("F").nodeParams.nodeId, paymentSent) assert(paymentSent.recipientAmount === amount, paymentSent) assert(paymentSent.feesPaid === 1000000.msat, paymentSent) assert(paymentSent.nonTrampolineFees === 0.msat, paymentSent) - awaitCond(nodes("F").nodeParams.db.payments.getIncomingPayment(pr.paymentHash).exists(_.status.isInstanceOf[IncomingPaymentStatus.Received])) - val Some(IncomingPayment(_, _, _, _, IncomingPaymentStatus.Received(receivedAmount, _))) = nodes("F").nodeParams.db.payments.getIncomingPayment(pr.paymentHash) + awaitCond(nodes("F").nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).exists(_.status.isInstanceOf[IncomingPaymentStatus.Received])) + val Some(IncomingPayment(_, _, _, _, IncomingPaymentStatus.Received(receivedAmount, _))) = nodes("F").nodeParams.db.payments.getIncomingPayment(invoice.paymentHash) assert(receivedAmount === amount) awaitCond({ - val relayed = nodes("G").nodeParams.db.audit.listRelayed(start, TimestampMilli.now()).filter(_.paymentHash == pr.paymentHash) + val relayed = nodes("G").nodeParams.db.audit.listRelayed(start, TimestampMilli.now()).filter(_.paymentHash == invoice.paymentHash) relayed.nonEmpty && relayed.head.amountOut >= amount }) - val relayed = nodes("G").nodeParams.db.audit.listRelayed(start, TimestampMilli.now()).filter(_.paymentHash == pr.paymentHash).head + val relayed = nodes("G").nodeParams.db.audit.listRelayed(start, TimestampMilli.now()).filter(_.paymentHash == invoice.paymentHash).head assert(relayed.amountIn - relayed.amountOut > 0.msat, relayed) assert(relayed.amountIn - relayed.amountOut < 1000000.msat, relayed) @@ -504,31 +504,31 @@ class PaymentIntegrationSpec extends IntegrationSpec { nodes("B").system.eventStream.subscribe(eventListener.ref, classOf[PaymentMetadataReceived]) val amount = 2500000000L.msat sender.send(nodes("B").paymentHandler, ReceivePayment(Some(amount), Left("trampoline-MPP is so #reckless"))) - val pr = sender.expectMsgType[PaymentRequest] - assert(pr.features.allowMultiPart) - assert(pr.features.allowTrampoline) - assert(pr.paymentMetadata.nonEmpty) + val invoice = sender.expectMsgType[Invoice] + assert(invoice.features.hasFeature(Features.BasicMultiPartPayment)) + assert(invoice.features.hasFeature(Features.TrampolinePayment)) + assert(invoice.paymentMetadata.nonEmpty) - val payment = SendTrampolinePayment(amount, pr, nodes("C").nodeParams.nodeId, Seq((350000 msat, CltvExpiryDelta(288))), routeParams = integrationTestRouteParams) + val payment = SendTrampolinePayment(amount, invoice, nodes("C").nodeParams.nodeId, Seq((350000 msat, CltvExpiryDelta(288))), routeParams = integrationTestRouteParams) sender.send(nodes("D").paymentInitiator, payment) val paymentId = sender.expectMsgType[UUID] val paymentSent = sender.expectMsgType[PaymentSent](max = 30 seconds) assert(paymentSent.id === paymentId, paymentSent) - assert(paymentSent.paymentHash === pr.paymentHash, paymentSent) + assert(paymentSent.paymentHash === invoice.paymentHash, paymentSent) assert(paymentSent.recipientAmount === amount, paymentSent) assert(paymentSent.feesPaid === 350000.msat, paymentSent) assert(paymentSent.nonTrampolineFees === 0.msat, paymentSent) - awaitCond(nodes("B").nodeParams.db.payments.getIncomingPayment(pr.paymentHash).exists(_.status.isInstanceOf[IncomingPaymentStatus.Received])) - val Some(IncomingPayment(_, _, _, _, IncomingPaymentStatus.Received(receivedAmount, _))) = nodes("B").nodeParams.db.payments.getIncomingPayment(pr.paymentHash) + awaitCond(nodes("B").nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).exists(_.status.isInstanceOf[IncomingPaymentStatus.Received])) + val Some(IncomingPayment(_, _, _, _, IncomingPaymentStatus.Received(receivedAmount, _))) = nodes("B").nodeParams.db.payments.getIncomingPayment(invoice.paymentHash) assert(receivedAmount === amount) - eventListener.expectMsg(PaymentMetadataReceived(pr.paymentHash, pr.paymentMetadata.get)) + eventListener.expectMsg(PaymentMetadataReceived(invoice.paymentHash, invoice.paymentMetadata.get)) awaitCond({ - val relayed = nodes("C").nodeParams.db.audit.listRelayed(start, TimestampMilli.now()).filter(_.paymentHash == pr.paymentHash) + val relayed = nodes("C").nodeParams.db.audit.listRelayed(start, TimestampMilli.now()).filter(_.paymentHash == invoice.paymentHash) relayed.nonEmpty && relayed.head.amountOut >= amount }) - val relayed = nodes("C").nodeParams.db.audit.listRelayed(start, TimestampMilli.now()).filter(_.paymentHash == pr.paymentHash).head + val relayed = nodes("C").nodeParams.db.audit.listRelayed(start, TimestampMilli.now()).filter(_.paymentHash == invoice.paymentHash).head assert(relayed.amountIn - relayed.amountOut > 0.msat, relayed) assert(relayed.amountIn - relayed.amountOut < 350000.msat, relayed) @@ -556,30 +556,30 @@ class PaymentIntegrationSpec extends IntegrationSpec { val amount = 3000000000L.msat sender.send(nodes("A").paymentHandler, ReceivePayment(Some(amount), Left("trampoline to non-trampoline is so #vintage"), extraHops = routingHints)) - val pr = sender.expectMsgType[PaymentRequest] - assert(pr.features.allowMultiPart) - assert(!pr.features.allowTrampoline) - assert(pr.paymentMetadata.nonEmpty) + val invoice = sender.expectMsgType[Invoice] + assert(invoice.features.hasFeature(Features.BasicMultiPartPayment)) + assert(!invoice.features.hasFeature(Features.TrampolinePayment)) + assert(invoice.paymentMetadata.nonEmpty) - val payment = SendTrampolinePayment(amount, pr, nodes("C").nodeParams.nodeId, Seq((1000000 msat, CltvExpiryDelta(432))), routeParams = integrationTestRouteParams) + val payment = SendTrampolinePayment(amount, invoice, nodes("C").nodeParams.nodeId, Seq((1000000 msat, CltvExpiryDelta(432))), routeParams = integrationTestRouteParams) sender.send(nodes("F").paymentInitiator, payment) val paymentId = sender.expectMsgType[UUID] val paymentSent = sender.expectMsgType[PaymentSent](max = 30 seconds) assert(paymentSent.id === paymentId, paymentSent) - assert(paymentSent.paymentHash === pr.paymentHash, paymentSent) + assert(paymentSent.paymentHash === invoice.paymentHash, paymentSent) assert(paymentSent.recipientAmount === amount, paymentSent) assert(paymentSent.trampolineFees === 1000000.msat, paymentSent) - awaitCond(nodes("A").nodeParams.db.payments.getIncomingPayment(pr.paymentHash).exists(_.status.isInstanceOf[IncomingPaymentStatus.Received])) - val Some(IncomingPayment(_, _, _, _, IncomingPaymentStatus.Received(receivedAmount, _))) = nodes("A").nodeParams.db.payments.getIncomingPayment(pr.paymentHash) + awaitCond(nodes("A").nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).exists(_.status.isInstanceOf[IncomingPaymentStatus.Received])) + val Some(IncomingPayment(_, _, _, _, IncomingPaymentStatus.Received(receivedAmount, _))) = nodes("A").nodeParams.db.payments.getIncomingPayment(invoice.paymentHash) assert(receivedAmount === amount) - eventListener.expectMsg(PaymentMetadataReceived(pr.paymentHash, pr.paymentMetadata.get)) + eventListener.expectMsg(PaymentMetadataReceived(invoice.paymentHash, invoice.paymentMetadata.get)) awaitCond({ - val relayed = nodes("C").nodeParams.db.audit.listRelayed(start, TimestampMilli.now()).filter(_.paymentHash == pr.paymentHash) + val relayed = nodes("C").nodeParams.db.audit.listRelayed(start, TimestampMilli.now()).filter(_.paymentHash == invoice.paymentHash) relayed.nonEmpty && relayed.head.amountOut >= amount }) - val relayed = nodes("C").nodeParams.db.audit.listRelayed(start, TimestampMilli.now()).filter(_.paymentHash == pr.paymentHash).head + val relayed = nodes("C").nodeParams.db.audit.listRelayed(start, TimestampMilli.now()).filter(_.paymentHash == invoice.paymentHash).head assert(relayed.amountIn - relayed.amountOut > 0.msat, relayed) assert(relayed.amountIn - relayed.amountOut < 1000000.msat, relayed) @@ -596,7 +596,7 @@ class PaymentIntegrationSpec extends IntegrationSpec { // We put most of the capacity C <-> D on D's side. sender.send(nodes("D").paymentHandler, ReceivePayment(Some(8000000000L msat), Left("plz send everything"))) - val pr1 = sender.expectMsgType[PaymentRequest] + val pr1 = sender.expectMsgType[Invoice] sender.send(nodes("C").paymentInitiator, SendPaymentToNode(8000000000L msat, pr1, maxAttempts = 3, routeParams = integrationTestRouteParams)) sender.expectMsgType[UUID] sender.expectMsgType[PaymentSent](max = 30 seconds) @@ -604,18 +604,18 @@ class PaymentIntegrationSpec extends IntegrationSpec { // Now we try to send more than C's outgoing capacity to D. val amount = 2000000000L.msat sender.send(nodes("D").paymentHandler, ReceivePayment(Some(amount), Left("I iz Satoshi"))) - val pr = sender.expectMsgType[PaymentRequest] - assert(pr.features.allowMultiPart) - assert(pr.features.allowTrampoline) + val invoice = sender.expectMsgType[Invoice] + assert(invoice.features.hasFeature(Features.BasicMultiPartPayment)) + assert(invoice.features.hasFeature(Features.TrampolinePayment)) - val payment = SendTrampolinePayment(amount, pr, nodes("C").nodeParams.nodeId, Seq((250000 msat, CltvExpiryDelta(288))), routeParams = integrationTestRouteParams) + val payment = SendTrampolinePayment(amount, invoice, nodes("C").nodeParams.nodeId, Seq((250000 msat, CltvExpiryDelta(288))), routeParams = integrationTestRouteParams) sender.send(nodes("B").paymentInitiator, payment) val paymentId = sender.expectMsgType[UUID] val paymentFailed = sender.expectMsgType[PaymentFailed](max = 30 seconds) assert(paymentFailed.id === paymentId, paymentFailed) - assert(paymentFailed.paymentHash === pr.paymentHash, paymentFailed) + assert(paymentFailed.paymentHash === invoice.paymentHash, paymentFailed) - assert(nodes("D").nodeParams.db.payments.getIncomingPayment(pr.paymentHash).get.status === IncomingPaymentStatus.Pending) + assert(nodes("D").nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).get.status === IncomingPaymentStatus.Pending) val outgoingPayments = nodes("B").nodeParams.db.payments.listOutgoingPayments(paymentId) assert(outgoingPayments.nonEmpty, outgoingPayments) assert(outgoingPayments.forall(p => p.status.isInstanceOf[OutgoingPaymentStatus.Failed]), outgoingPayments) @@ -625,18 +625,18 @@ class PaymentIntegrationSpec extends IntegrationSpec { val sender = TestProbe() val amount = 2000000000L.msat // B can forward to C, but C doesn't have that much outgoing capacity to D sender.send(nodes("D").paymentHandler, ReceivePayment(Some(amount), Left("I iz not Satoshi"))) - val pr = sender.expectMsgType[PaymentRequest] - assert(pr.features.allowMultiPart) - assert(pr.features.allowTrampoline) + val invoice = sender.expectMsgType[Invoice] + assert(invoice.features.hasFeature(Features.BasicMultiPartPayment)) + assert(invoice.features.hasFeature(Features.TrampolinePayment)) - val payment = SendTrampolinePayment(amount, pr, nodes("B").nodeParams.nodeId, Seq((450000 msat, CltvExpiryDelta(288))), routeParams = integrationTestRouteParams) + val payment = SendTrampolinePayment(amount, invoice, nodes("B").nodeParams.nodeId, Seq((450000 msat, CltvExpiryDelta(288))), routeParams = integrationTestRouteParams) sender.send(nodes("A").paymentInitiator, payment) val paymentId = sender.expectMsgType[UUID] val paymentFailed = sender.expectMsgType[PaymentFailed](max = 30 seconds) assert(paymentFailed.id === paymentId, paymentFailed) - assert(paymentFailed.paymentHash === pr.paymentHash, paymentFailed) + assert(paymentFailed.paymentHash === invoice.paymentHash, paymentFailed) - assert(nodes("D").nodeParams.db.payments.getIncomingPayment(pr.paymentHash).get.status === IncomingPaymentStatus.Pending) + assert(nodes("D").nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).get.status === IncomingPaymentStatus.Pending) val outgoingPayments = nodes("A").nodeParams.db.payments.listOutgoingPayments(paymentId) assert(outgoingPayments.nonEmpty, outgoingPayments) assert(outgoingPayments.forall(p => p.status.isInstanceOf[OutgoingPaymentStatus.Failed]), outgoingPayments) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/PerformanceIntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/PerformanceIntegrationSpec.scala index 832d9db522..a1cdfe2be8 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/PerformanceIntegrationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/PerformanceIntegrationSpec.scala @@ -16,6 +16,7 @@ package fr.acinq.eclair.integration +import akka.actor.ActorRef import akka.testkit.TestProbe import com.typesafe.config.ConfigFactory import fr.acinq.bitcoin.SatoshiLong @@ -83,7 +84,7 @@ class PerformanceIntegrationSpec extends IntegrationSpec { val amountMsat = 100_000.msat // first we retrieve a payment hash from B sender.send(nodes("B").paymentHandler, ReceivePayment(Some(amountMsat), Left("1 coffee"))) - val pr = sender.expectMsgType[PaymentRequest] + val pr = sender.expectMsgType[Invoice] // then we make the actual payment sender.send(nodes("A").paymentInitiator, PaymentInitiator.SendPaymentToNode(amountMsat, pr, fallbackFinalExpiryDelta = finalCltvExpiryDelta, routeParams = integrationTestRouteParams, maxAttempts = 1)) val paymentId = sender.expectMsgType[UUID] diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/io/MessageRelaySpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/io/MessageRelaySpec.scala index 58682d6074..bced90e3ac 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/io/MessageRelaySpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/io/MessageRelaySpec.scala @@ -56,7 +56,7 @@ class MessageRelaySpec extends ScalaTestWithActorTestKit(ConfigFactory.load("app test("relay with new connection") { f => import f._ - val (_, message) = OnionMessages.buildMessage(randomKey(), randomKey(), Seq(IntermediateNode(aliceId)), Left(Recipient(bobId, None)), Nil) + val (_, message) = OnionMessages.buildMessage(randomKey(), randomKey(), Seq(IntermediateNode(aliceId)), Recipient(bobId, None), Nil) val messageId = randomBytes32() relay ! RelayMessage(messageId, switchboard.ref, randomKey().publicKey, bobId, message, RelayAll, None) @@ -69,7 +69,7 @@ class MessageRelaySpec extends ScalaTestWithActorTestKit(ConfigFactory.load("app test("relay with existing peer") { f => import f._ - val (_, message) = OnionMessages.buildMessage(randomKey(), randomKey(), Seq(IntermediateNode(aliceId)), Left(Recipient(bobId, None)), Nil) + val (_, message) = OnionMessages.buildMessage(randomKey(), randomKey(), Seq(IntermediateNode(aliceId)), Recipient(bobId, None), Nil) val messageId = randomBytes32() relay ! RelayMessage(messageId, switchboard.ref, randomKey().publicKey, bobId, message, RelayAll, None) @@ -82,7 +82,7 @@ class MessageRelaySpec extends ScalaTestWithActorTestKit(ConfigFactory.load("app test("can't open new connection") { f => import f._ - val (_, message) = OnionMessages.buildMessage(randomKey(), randomKey(), Seq(IntermediateNode(aliceId)), Left(Recipient(bobId, None)), Nil) + val (_, message) = OnionMessages.buildMessage(randomKey(), randomKey(), Seq(IntermediateNode(aliceId)), Recipient(bobId, None), Nil) val messageId = randomBytes32() relay ! RelayMessage(messageId, switchboard.ref, randomKey().publicKey, bobId, message, RelayAll, Some(probe.ref)) @@ -95,7 +95,7 @@ class MessageRelaySpec extends ScalaTestWithActorTestKit(ConfigFactory.load("app test("no channel with previous node") { f => import f._ - val (_, message) = OnionMessages.buildMessage(randomKey(), randomKey(), Seq(IntermediateNode(aliceId)), Left(Recipient(bobId, None)), Nil) + val (_, message) = OnionMessages.buildMessage(randomKey(), randomKey(), Seq(IntermediateNode(aliceId)), Recipient(bobId, None), Nil) val messageId = randomBytes32() val previousNodeId = randomKey().publicKey relay ! RelayMessage(messageId, switchboard.ref, previousNodeId, bobId, message, RelayChannelsOnly, Some(probe.ref)) @@ -111,7 +111,7 @@ class MessageRelaySpec extends ScalaTestWithActorTestKit(ConfigFactory.load("app test("no channel with next node") { f => import f._ - val (_, message) = OnionMessages.buildMessage(randomKey(), randomKey(), Seq(IntermediateNode(aliceId)), Left(Recipient(bobId, None)), Nil) + val (_, message) = OnionMessages.buildMessage(randomKey(), randomKey(), Seq(IntermediateNode(aliceId)), Recipient(bobId, None), Nil) val messageId = randomBytes32() val previousNodeId = randomKey().publicKey relay ! RelayMessage(messageId, switchboard.ref, previousNodeId, bobId, message, RelayChannelsOnly, Some(probe.ref)) @@ -131,7 +131,7 @@ class MessageRelaySpec extends ScalaTestWithActorTestKit(ConfigFactory.load("app test("channels on both ends") { f => import f._ - val (_, message) = OnionMessages.buildMessage(randomKey(), randomKey(), Seq(IntermediateNode(aliceId)), Left(Recipient(bobId, None)), Nil) + val (_, message) = OnionMessages.buildMessage(randomKey(), randomKey(), Seq(IntermediateNode(aliceId)), Recipient(bobId, None), Nil) val messageId = randomBytes32() val previousNodeId = randomKey().publicKey relay ! RelayMessage(messageId, switchboard.ref, previousNodeId, bobId, message, RelayChannelsOnly, None) @@ -150,7 +150,7 @@ class MessageRelaySpec extends ScalaTestWithActorTestKit(ConfigFactory.load("app test("no relay") { f => import f._ - val (_, message) = OnionMessages.buildMessage(randomKey(), randomKey(), Seq(IntermediateNode(aliceId)), Left(Recipient(bobId, None)), Nil) + val (_, message) = OnionMessages.buildMessage(randomKey(), randomKey(), Seq(IntermediateNode(aliceId)), Recipient(bobId, None), Nil) val messageId = randomBytes32() val previousNodeId = randomKey().publicKey relay ! RelayMessage(messageId, switchboard.ref, previousNodeId, bobId, message, NoRelay, Some(probe.ref)) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerConnectionSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerConnectionSpec.scala index d2883aa285..dadb10bff9 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerConnectionSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerConnectionSpec.scala @@ -391,7 +391,7 @@ class PeerConnectionSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wi import f._ connect(nodeParams, remoteNodeId, switchboard, router, connection, transport, peerConnection, peer, isPersistent = false) val probe = TestProbe() - val (_, message) = buildMessage(randomKey(), randomKey(), Nil, Left(Recipient(remoteNodeId, None)), Nil) + val (_, message) = buildMessage(randomKey(), randomKey(), Nil, Recipient(remoteNodeId, None), Nil) probe.send(peerConnection, message) probe watch peerConnection probe.expectTerminated(peerConnection, max = 1500 millis) @@ -407,7 +407,7 @@ class PeerConnectionSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wi import f._ connect(nodeParams, remoteNodeId, switchboard, router, connection, transport, peerConnection, peer, isPersistent = false) val probe = TestProbe() - val (_, message) = buildMessage(randomKey(), randomKey(), Nil, Left(Recipient(remoteNodeId, None)), Nil) + val (_, message) = buildMessage(randomKey(), randomKey(), Nil, Recipient(remoteNodeId, None), Nil) probe.send(peerConnection, message) probe watch peerConnection sleep(900 millis) @@ -426,7 +426,7 @@ class PeerConnectionSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wi import f._ connect(nodeParams, remoteNodeId, switchboard, router, connection, transport, peerConnection, peer, isPersistent = false) val probe = TestProbe() - val (_, message) = buildMessage(randomKey(), randomKey(), Nil, Left(Recipient(remoteNodeId, None)), Nil) + val (_, message) = buildMessage(randomKey(), randomKey(), Nil, Recipient(remoteNodeId, None), Nil) probe.send(peerConnection, message) assert(peerConnection.stateName === PeerConnection.CONNECTED) probe.send(peerConnection, FundingLocked(ByteVector32(hex"0000000000000000000000000000000000000000000000000000000000000000"), randomKey().publicKey)) @@ -439,7 +439,7 @@ class PeerConnectionSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wi test("incoming rate limiting") { f => import f._ connect(nodeParams, remoteNodeId, switchboard, router, connection, transport, peerConnection, peer, isPersistent = true) - val (_, message) = buildMessage(randomKey(), randomKey(), Nil, Left(Recipient(nodeParams.nodeId, None)), Nil) + val (_, message) = buildMessage(randomKey(), randomKey(), Nil, Recipient(nodeParams.nodeId, None), Nil) for (_ <- 1 to 30) { transport.send(peerConnection, message) } @@ -458,7 +458,7 @@ class PeerConnectionSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wi test("outgoing rate limiting") { f => import f._ connect(nodeParams, remoteNodeId, switchboard, router, connection, transport, peerConnection, peer, isPersistent = true) - val (_, message) = buildMessage(randomKey(), randomKey(), Nil, Left(Recipient(remoteNodeId, None)), Nil) + val (_, message) = buildMessage(randomKey(), randomKey(), Nil, Recipient(remoteNodeId, None), Nil) for (_ <- 1 to 30) { peer.send(peerConnection, message) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala index 2a53f838d9..45bb103978 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala @@ -506,7 +506,7 @@ class PeerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Paralle test("reply to relay request") { f => import f._ connect(remoteNodeId, peer, peerConnection, switchboard, channels = Set(ChannelCodecsSpec.normal)) - val (_, msg) = buildMessage(randomKey(), randomKey(), Nil, Left(Recipient(remoteNodeId, None)), Nil) + val (_, msg) = buildMessage(randomKey(), randomKey(), Nil, Recipient(remoteNodeId, None), Nil) val messageId = randomBytes32() val probe = TestProbe() peer ! RelayOnionMessage(messageId, msg, Some(probe.ref.toTyped)) @@ -515,7 +515,7 @@ class PeerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Paralle test("reply to relay request disconnected") { f => import f._ - val (_, msg) = buildMessage(randomKey(), randomKey(), Nil, Left(Recipient(remoteNodeId, None)), Nil) + val (_, msg) = buildMessage(randomKey(), randomKey(), Nil, Recipient(remoteNodeId, None), Nil) val messageId = randomBytes32() val probe = TestProbe() peer ! RelayOnionMessage(messageId, msg, Some(probe.ref.toTyped)) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/json/JsonSerializersSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/json/JsonSerializersSpec.scala index e9962f5685..5699b5ef2c 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/json/JsonSerializersSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/json/JsonSerializersSpec.scala @@ -25,7 +25,7 @@ import fr.acinq.eclair.balance.CheckBalance.{ClosingBalance, GlobalBalance, Main import fr.acinq.eclair.channel._ import fr.acinq.eclair.io.Peer import fr.acinq.eclair.io.Peer.PeerInfo -import fr.acinq.eclair.payment.{PaymentRequest, PaymentSettlingOnChain} +import fr.acinq.eclair.payment.{Invoice, PaymentSettlingOnChain} import fr.acinq.eclair.transactions.Transactions._ import fr.acinq.eclair.transactions.{IncomingHtlc, OutgoingHtlc} import fr.acinq.eclair.wire.internal.channel.ChannelCodecs @@ -167,21 +167,21 @@ class JsonSerializersSpec extends AnyFunSuite with Matchers { JsonSerializers.serialization.write(features)(JsonSerializers.formats) shouldBe """{"activated":{"initial_routing_sync":"optional","payment_secret":"mandatory","option_static_remotekey":"optional"},"unknown":[457,5000]}""" } - test("Payment Request") { + test("Bolt 11 invoice") { val ref = "lnbcrt50n1p0fm9cdpp5al3wvsfkc6p7fxy89eu8gm4aww9mseu9syrcqtpa4mvx42qelkwqdq9v9ekgxqrrss9qypqsqsp5wl2t45v0hj4lgud0zjxcnjccd29ts0p2kh4vpw75vnhyyzyjtjtqarpvqg33asgh3z5ghfuvhvtf39xtnu9e7aqczpgxa9quwsxkd9rnwmx06pve9awgeewxqh90dqgrhzgsqc09ek6uejr93z8puafm6gsqgrk0hy" - val pr = PaymentRequest.read(ref) + val pr = Invoice.fromString(ref) JsonSerializers.serialization.write(pr)(JsonSerializers.formats) shouldBe """{"prefix":"lnbcrt","timestamp":1587386125,"nodeId":"03b207771ddba774e318970e9972da2491ff8e54f777ad0528b6526773730248a0","serialized":"lnbcrt50n1p0fm9cdpp5al3wvsfkc6p7fxy89eu8gm4aww9mseu9syrcqtpa4mvx42qelkwqdq9v9ekgxqrrss9qypqsqsp5wl2t45v0hj4lgud0zjxcnjccd29ts0p2kh4vpw75vnhyyzyjtjtqarpvqg33asgh3z5ghfuvhvtf39xtnu9e7aqczpgxa9quwsxkd9rnwmx06pve9awgeewxqh90dqgrhzgsqc09ek6uejr93z8puafm6gsqgrk0hy","description":"asd","paymentHash":"efe2e64136c683e498872e78746ebd738bb867858107802c3daed86aa819fd9c","expiry":3600,"amount":5000,"features":{"activated":{"var_onion_optin":"optional","payment_secret":"optional"},"unknown":[]},"routingInfo":[]}""" } - test("Payment Request with routing hints") { + test("Bolt 11 invoice with routing hints") { val ref = "lntb1pst2q8xpp5qysan6j5xeq97tytxf7pfr0n75na8rztqhh03glmlgsqsyuqzgnqdqqxqrrss9qy9qsqsp5qq67gcxrn2drj5p0lc6p8wgdpqwxnc2h4s9kra5489q0fqsvhumsrzjqfqnj4upt5z6hdludky9vgk4ehzmwu2dk9rcevzczw5ywstehq79c83xr5qqqkqqqqqqqqlgqqqqqeqqjqrzjqwfn3p9278ttzzpe0e00uhyxhned3j5d9acqak5emwfpflp8z2cng838tqqqqxgqqqqqqqlgqqqqqeqqjqkxs4223x2r6sat65asfp0k2pze2rswe9np9vq08waqvsp832ffgymzgx8hgzejasesfxwcw6jj93azwq9klwuzmef3llns3n95pztgqpawp7an" - val pr = PaymentRequest.read(ref) + val pr = Invoice.fromString(ref) JsonSerializers.serialization.write(pr)(JsonSerializers.formats) shouldBe """{"prefix":"lntb","timestamp":1622474982,"nodeId":"03e89e4c3d41dc5332c2fb6cc66d12bfb9257ba681945a242f27a08d5ad210d891","serialized":"lntb1pst2q8xpp5qysan6j5xeq97tytxf7pfr0n75na8rztqhh03glmlgsqsyuqzgnqdqqxqrrss9qy9qsqsp5qq67gcxrn2drj5p0lc6p8wgdpqwxnc2h4s9kra5489q0fqsvhumsrzjqfqnj4upt5z6hdludky9vgk4ehzmwu2dk9rcevzczw5ywstehq79c83xr5qqqkqqqqqqqqlgqqqqqeqqjqrzjqwfn3p9278ttzzpe0e00uhyxhned3j5d9acqak5emwfpflp8z2cng838tqqqqxgqqqqqqqlgqqqqqeqqjqkxs4223x2r6sat65asfp0k2pze2rswe9np9vq08waqvsp832ffgymzgx8hgzejasesfxwcw6jj93azwq9klwuzmef3llns3n95pztgqpawp7an","description":"","paymentHash":"0121d9ea5436405f2c8b327c148df3f527d38c4b05eef8a3fbfa200813801226","expiry":3600,"features":{"activated":{"var_onion_optin":"optional","payment_secret":"optional","basic_mpp":"optional"},"unknown":[]},"routingInfo":[[{"nodeId":"02413957815d05abb7fc6d885622d5cdc5b7714db1478cb05813a8474179b83c5c","shortChannelId":"1975837x88x0","feeBase":1000,"feeProportionalMillionths":100,"cltvExpiryDelta":144}],[{"nodeId":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","shortChannelId":"1976152x25x0","feeBase":1000,"feeProportionalMillionths":100,"cltvExpiryDelta":144}]]}""" } - test("Payment Request with metadata") { + test("Bolt 11 invoice with metadata") { val ref = "lnbc10m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdp9wpshjmt9de6zqmt9w3skgct5vysxjmnnd9jx2mq8q8a04uqnp4q0n326hr8v9zprg8gsvezcch06gfaqqhde2aj730yg0durunfhv66sp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9q2gqqqqqqsgqy9gw6ymamd20jumvdgpfphkhp8fzhhdhycw36egcmla5vlrtrmhs9t7psfy3hkkdqzm9eq64fjg558znccds5nhsfmxveha5xe0dykgpspdha0" - val pr = PaymentRequest.read(ref) + val pr = Invoice.fromString(ref) JsonSerializers.serialization.write(pr)(JsonSerializers.formats) shouldBe """{"prefix":"lnbc","timestamp":1496314658,"nodeId":"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad","serialized":"lnbc10m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdp9wpshjmt9de6zqmt9w3skgct5vysxjmnnd9jx2mq8q8a04uqnp4q0n326hr8v9zprg8gsvezcch06gfaqqhde2aj730yg0durunfhv66sp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9q2gqqqqqqsgqy9gw6ymamd20jumvdgpfphkhp8fzhhdhycw36egcmla5vlrtrmhs9t7psfy3hkkdqzm9eq64fjg558znccds5nhsfmxveha5xe0dykgpspdha0","description":"payment metadata inside","paymentHash":"0001020304050607080900010203040506070809000102030405060708090102","paymentMetadata":"01fafaf0","amount":1000000000,"features":{"activated":{"var_onion_optin":"mandatory","payment_secret":"mandatory","option_payment_metadata":"mandatory"},"unknown":[]},"routingInfo":[]}""" } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/message/OnionMessagesSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/message/OnionMessagesSpec.scala index 6ed950f7e5..3f6e263dab 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/message/OnionMessagesSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/message/OnionMessagesSpec.scala @@ -40,7 +40,7 @@ class OnionMessagesSpec extends AnyFunSuite { val sessionKey = randomKey() val blindingSecret = randomKey() val destination = randomKey() - val (nextNodeId, message) = buildMessage(sessionKey, blindingSecret, Nil, Left(Recipient(destination.publicKey, None)), Nil) + val (nextNodeId, message) = buildMessage(sessionKey, blindingSecret, Nil, Recipient(destination.publicKey, None), Nil) assert(nextNodeId == destination.publicKey) process(destination, message) match { @@ -108,9 +108,9 @@ class OnionMessagesSpec extends AnyFunSuite { /* * Building the onion with functions from `OnionMessages` */ - val replyPath = buildRoute(blindingOverride, IntermediateNode(carol.publicKey, padding = Some(hex"0000000000000000000000000000000000000000000000000000000000000000000000")) :: Nil, Left(Recipient(dave.publicKey, pathId = Some(hex"01234567")))) + val replyPath = buildRoute(blindingOverride, IntermediateNode(carol.publicKey, padding = Some(hex"0000000000000000000000000000000000000000000000000000000000000000000000")) :: Nil, Recipient(dave.publicKey, pathId = Some(hex"01234567"))) assert(replyPath == routeFromCarol) - val (_, message) = buildMessage(sessionKey, blindingSecret, IntermediateNode(alice.publicKey) :: IntermediateNode(bob.publicKey) :: Nil, Right(replyPath), Nil) + val (_, message) = buildMessage(sessionKey, blindingSecret, IntermediateNode(alice.publicKey) :: IntermediateNode(bob.publicKey) :: Nil, BlindedPath(replyPath), Nil) assert(message == onionForAlice) /* @@ -223,10 +223,10 @@ class OnionMessagesSpec extends AnyFunSuite { val blindingSecret = randomKey() val blindingOverride = randomKey() val destination = randomKey() - val replyPath = buildRoute(blindingOverride, IntermediateNode(destination.publicKey) :: Nil, Left(Recipient(destination.publicKey, pathId = Some(hex"01234567")))) + val replyPath = buildRoute(blindingOverride, IntermediateNode(destination.publicKey) :: Nil, Recipient(destination.publicKey, pathId = Some(hex"01234567"))) assert(replyPath.blindingKey === blindingOverride.publicKey) assert(replyPath.introductionNodeId === destination.publicKey) - val (nextNodeId, message) = buildMessage(sessionKey, blindingSecret, Nil, Right(replyPath), Nil) + val (nextNodeId, message) = buildMessage(sessionKey, blindingSecret, Nil, BlindedPath(replyPath), Nil) assert(nextNodeId === destination.publicKey) assert(message.blindingKey === blindingOverride.publicKey) // blindingSecret was not used as the replyPath was used as is diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/message/PostmanSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/message/PostmanSpec.scala index ec781008c0..0a87b4dae0 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/message/PostmanSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/message/PostmanSpec.scala @@ -55,8 +55,8 @@ class PostmanSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("applicat val pathId = randomBytes32() val ourNodeId = randomKey().publicKey val recipient = randomKey().publicKey - val replyPath = OnionMessages.buildRoute(randomKey(), Nil, Left(OnionMessages.Recipient(ourNodeId, Some(pathId)))) - val (_, messageExpectingReply) = OnionMessages.buildMessage(randomKey(), randomKey(), Nil, Left(OnionMessages.Recipient(recipient, None)), ReplyPath(replyPath) :: Nil) + val replyPath = OnionMessages.buildRoute(randomKey(), Nil, OnionMessages.Recipient(ourNodeId, Some(pathId))) + val (_, messageExpectingReply) = OnionMessages.buildMessage(randomKey(), randomKey(), Nil, OnionMessages.Recipient(recipient, None), ReplyPath(replyPath) :: Nil) val payload = FinalPayload(TlvStream(EncryptedData(replyPath.encryptedPayloads.last) :: Nil, GenericTlv(UInt64(42), hex"abcd") :: Nil)) postman ! SendMessage(recipient, messageExpectingReply, Some(pathId), messageRecipient.ref, 1 second) @@ -78,8 +78,8 @@ class PostmanSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("applicat val pathId = randomBytes32() val ourNodeId = randomKey().publicKey val recipient = randomKey().publicKey - val replyPath = OnionMessages.buildRoute(randomKey(), Nil, Left(OnionMessages.Recipient(ourNodeId, Some(pathId)))) - val (_, messageExpectingReply) = OnionMessages.buildMessage(randomKey(), randomKey(), Nil, Left(OnionMessages.Recipient(recipient, None)), ReplyPath(replyPath) :: Nil) + val replyPath = OnionMessages.buildRoute(randomKey(), Nil, OnionMessages.Recipient(ourNodeId, Some(pathId))) + val (_, messageExpectingReply) = OnionMessages.buildMessage(randomKey(), randomKey(), Nil, OnionMessages.Recipient(recipient, None), ReplyPath(replyPath) :: Nil) postman ! SendMessage(recipient, messageExpectingReply, Some(pathId), messageRecipient.ref, 1 second) @@ -97,8 +97,8 @@ class PostmanSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("applicat val pathId = randomBytes32() val ourNodeId = randomKey().publicKey val recipient = randomKey().publicKey - val replyPath = OnionMessages.buildRoute(randomKey(), Nil, Left(OnionMessages.Recipient(ourNodeId, Some(pathId)))) - val (_, messageExpectingReply) = OnionMessages.buildMessage(randomKey(), randomKey(), Nil, Left(OnionMessages.Recipient(recipient, None)), ReplyPath(replyPath) :: Nil) + val replyPath = OnionMessages.buildRoute(randomKey(), Nil, OnionMessages.Recipient(ourNodeId, Some(pathId))) + val (_, messageExpectingReply) = OnionMessages.buildMessage(randomKey(), randomKey(), Nil, OnionMessages.Recipient(recipient, None), ReplyPath(replyPath) :: Nil) val payload = FinalPayload(TlvStream(EncryptedData(replyPath.encryptedPayloads.last) :: Nil, GenericTlv(UInt64(42), hex"abcd") :: Nil)) postman ! SendMessage(recipient, messageExpectingReply, Some(pathId), messageRecipient.ref, 1 millis) @@ -119,7 +119,7 @@ class PostmanSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("applicat import f._ val recipient = randomKey().publicKey - val (_, messageExpectingReply) = OnionMessages.buildMessage(randomKey(), randomKey(), Nil, Left(OnionMessages.Recipient(recipient, None)), Nil) + val (_, messageExpectingReply) = OnionMessages.buildMessage(randomKey(), randomKey(), Nil, OnionMessages.Recipient(recipient, None), Nil) postman ! SendMessage(recipient, messageExpectingReply, None, messageRecipient.ref, 1 second) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentRequestSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/Bolt11InvoiceSpec.scala similarity index 61% rename from eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentRequestSpec.scala rename to eclair-core/src/test/scala/fr/acinq/eclair/payment/Bolt11InvoiceSpec.scala index 7da8dcbe96..58b58688d4 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentRequestSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/Bolt11InvoiceSpec.scala @@ -20,20 +20,21 @@ import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} import fr.acinq.bitcoin.{Block, BtcDouble, ByteVector32, Crypto, MilliBtcDouble, SatoshiLong} import fr.acinq.eclair.FeatureSupport.{Mandatory, Optional} import fr.acinq.eclair.Features.{PaymentMetadata, PaymentSecret, _} -import fr.acinq.eclair.payment.PaymentRequest._ -import fr.acinq.eclair.{CltvExpiryDelta, Feature, FeatureSupport, Features, InvoiceFeature, MilliSatoshiLong, ShortChannelId, TestConstants, TimestampSecond, TimestampSecondLong, ToMilliSatoshiConversion} +import fr.acinq.eclair.payment.Bolt11Invoice._ +import fr.acinq.eclair.{CltvExpiryDelta, Features, MilliSatoshiLong, ShortChannelId, TestConstants, TimestampSecond, TimestampSecondLong, ToMilliSatoshiConversion} import org.scalatest.funsuite.AnyFunSuite import scodec.DecodeResult import scodec.bits._ import scodec.codecs.bits +import scala.concurrent.duration.DurationInt import scala.util.Success /** * Created by fabrice on 15/05/17. */ -class PaymentRequestSpec extends AnyFunSuite { +class Bolt11InvoiceSpec extends AnyFunSuite { val priv = PrivateKey(hex"e126f68f7eafcc8b74f54d269fe206be715000f94dac067d1c04a8ca3b2db734") val pub = priv.publicKey @@ -84,167 +85,167 @@ class PaymentRequestSpec extends AnyFunSuite { } test("verify that padding is zero") { - val codec = PaymentRequest.Codecs.alignedBytesCodec(bits) + val codec = Bolt11Invoice.Codecs.alignedBytesCodec(bits) assert(codec.decode(bin"1010101000").require == DecodeResult(bin"10101010", BitVector.empty)) assert(codec.decode(bin"1010101001").isFailure) // non-zero padding } test("Please make a donation of any amount using payment_hash 0001020304050607080900010203040506070809000102030405060708090102 to me @03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad") { val ref = "lnbc1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdpl2pkx2ctnv5sxxmmwwd5kgetjypeh2ursdae8g6twvus8g6rfwvs8qun0dfjkxaq9qrsgq357wnc5r2ueh7ck6q93dj32dlqnls087fxdwk8qakdyafkq3yap9us6v52vjjsrvywa6rt52cm9r9zqt8r2t7mlcwspyetp5h2tztugp9lfyql" - val pr = PaymentRequest.read(ref) - assert(pr.prefix == "lnbc") - assert(pr.amount.isEmpty) - assert(pr.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102") - assert(pr.paymentSecret.map(_.bytes) === Some(hex"1111111111111111111111111111111111111111111111111111111111111111")) - assert(pr.features.features === Features(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory)) - assert(pr.timestamp == TimestampSecond(1496314658L)) - assert(pr.nodeId == PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) - assert(pr.description == Left("Please consider supporting this project")) - assert(pr.fallbackAddress() === None) - assert(pr.tags.size === 4) - assert(PaymentRequest.write(pr.sign(priv)) == ref) + val invoice = Bolt11Invoice.fromString(ref) + assert(invoice.prefix == "lnbc") + assert(invoice.amount_opt.isEmpty) + assert(invoice.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102") + assert(invoice.paymentSecret.map(_.bytes) === Some(hex"1111111111111111111111111111111111111111111111111111111111111111")) + assert(invoice.features === Features(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory)) + assert(invoice.createdAt == TimestampSecond(1496314658L)) + assert(invoice.nodeId == PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) + assert(invoice.description == Left("Please consider supporting this project")) + assert(invoice.fallbackAddress() === None) + assert(invoice.tags.size === 4) + assert(invoice.sign(priv).toString == ref) } test("Please send $3 for a cup of coffee to the same peer, within 1 minute") { val ref = "lnbc2500u1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpu9qrsgquk0rl77nj30yxdy8j9vdx85fkpmdla2087ne0xh8nhedh8w27kyke0lp53ut353s06fv3qfegext0eh0ymjpf39tuven09sam30g4vgpfna3rh" - val pr = PaymentRequest.read(ref) - assert(pr.prefix == "lnbc") - assert(pr.amount === Some(250000000 msat)) - assert(pr.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102") - assert(pr.features.features === Features(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory)) - assert(pr.timestamp == TimestampSecond(1496314658L)) - assert(pr.nodeId == PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) - assert(pr.description == Left("1 cup coffee")) - assert(pr.fallbackAddress() === None) - assert(pr.tags.size === 5) - assert(PaymentRequest.write(pr.sign(priv)) == ref) + val invoice = Bolt11Invoice.fromString(ref) + assert(invoice.prefix == "lnbc") + assert(invoice.amount_opt === Some(250000000 msat)) + assert(invoice.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102") + assert(invoice.features === Features(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory)) + assert(invoice.createdAt == TimestampSecond(1496314658L)) + assert(invoice.nodeId == PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) + assert(invoice.description == Left("1 cup coffee")) + assert(invoice.fallbackAddress() === None) + assert(invoice.tags.size === 5) + assert(invoice.sign(priv).toString == ref) } test("Please send 0.0025 BTC for a cup of nonsense (ナンセンス 1杯) to the same peer, within one minute") { val ref = "lnbc2500u1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdpquwpc4curk03c9wlrswe78q4eyqc7d8d0xqzpu9qrsgqhtjpauu9ur7fw2thcl4y9vfvh4m9wlfyz2gem29g5ghe2aak2pm3ps8fdhtceqsaagty2vph7utlgj48u0ged6a337aewvraedendscp573dxr" - val pr = PaymentRequest.read(ref) - assert(pr.prefix == "lnbc") - assert(pr.amount === Some(250000000 msat)) - assert(pr.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102") - assert(pr.features.features === Features(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory)) - assert(pr.timestamp == TimestampSecond(1496314658L)) - assert(pr.nodeId == PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) - assert(pr.description == Left("ナンセンス 1杯")) - assert(pr.fallbackAddress() === None) - assert(pr.tags.size === 5) - assert(PaymentRequest.write(pr.sign(priv)) == ref) + val invoice = Bolt11Invoice.fromString(ref) + assert(invoice.prefix == "lnbc") + assert(invoice.amount_opt === Some(250000000 msat)) + assert(invoice.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102") + assert(invoice.features === Features(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory)) + assert(invoice.createdAt == TimestampSecond(1496314658L)) + assert(invoice.nodeId == PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) + assert(invoice.description == Left("ナンセンス 1杯")) + assert(invoice.fallbackAddress() === None) + assert(invoice.tags.size === 5) + assert(invoice.sign(priv).toString == ref) } test("Now send $24 for an entire list of things (hashed)") { val ref = "lnbc20m1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqs9qrsgq7ea976txfraylvgzuxs8kgcw23ezlrszfnh8r6qtfpr6cxga50aj6txm9rxrydzd06dfeawfk6swupvz4erwnyutnjq7x39ymw6j38gp7ynn44" - val pr = PaymentRequest.read(ref) - assert(pr.prefix == "lnbc") - assert(pr.amount === Some(2000000000 msat)) - assert(pr.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102") - assert(pr.features.features === Features(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory)) - assert(pr.timestamp == TimestampSecond(1496314658L)) - assert(pr.nodeId == PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) - assert(pr.description == Right(Crypto.sha256(ByteVector("One piece of chocolate cake, one icecream cone, one pickle, one slice of swiss cheese, one slice of salami, one lollypop, one piece of cherry pie, one sausage, one cupcake, and one slice of watermelon".getBytes)))) - assert(pr.fallbackAddress() === None) - assert(pr.tags.size === 4) - assert(PaymentRequest.write(pr.sign(priv)) == ref) + val invoice = Bolt11Invoice.fromString(ref) + assert(invoice.prefix == "lnbc") + assert(invoice.amount_opt === Some(2000000000 msat)) + assert(invoice.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102") + assert(invoice.features === Features(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory)) + assert(invoice.createdAt == TimestampSecond(1496314658L)) + assert(invoice.nodeId == PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) + assert(invoice.description == Right(Crypto.sha256(ByteVector("One piece of chocolate cake, one icecream cone, one pickle, one slice of swiss cheese, one slice of salami, one lollypop, one piece of cherry pie, one sausage, one cupcake, and one slice of watermelon".getBytes)))) + assert(invoice.fallbackAddress() === None) + assert(invoice.tags.size === 4) + assert(invoice.sign(priv).toString == ref) } test("The same, on testnet, with a fallback address mk2QpYatsKicvFVuTAQLBryyccRXMUaGHP") { val ref = "lntb20m1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygshp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqfpp3x9et2e20v6pu37c5d9vax37wxq72un989qrsgqdj545axuxtnfemtpwkc45hx9d2ft7x04mt8q7y6t0k2dge9e7h8kpy9p34ytyslj3yu569aalz2xdk8xkd7ltxqld94u8h2esmsmacgpghe9k8" - val pr = PaymentRequest.read(ref) - assert(pr.prefix == "lntb") - assert(pr.amount === Some(2000000000 msat)) - assert(pr.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102") - assert(pr.features.features === Features(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory)) - assert(pr.timestamp == TimestampSecond(1496314658L)) - assert(pr.nodeId == PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) - assert(pr.description == Right(Crypto.sha256(ByteVector.view("One piece of chocolate cake, one icecream cone, one pickle, one slice of swiss cheese, one slice of salami, one lollypop, one piece of cherry pie, one sausage, one cupcake, and one slice of watermelon".getBytes)))) - assert(pr.fallbackAddress() === Some("mk2QpYatsKicvFVuTAQLBryyccRXMUaGHP")) - assert(pr.tags.size == 5) - assert(PaymentRequest.write(pr.sign(priv)) == ref) + val invoice = Bolt11Invoice.fromString(ref) + assert(invoice.prefix == "lntb") + assert(invoice.amount_opt === Some(2000000000 msat)) + assert(invoice.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102") + assert(invoice.features === Features(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory)) + assert(invoice.createdAt == TimestampSecond(1496314658L)) + assert(invoice.nodeId == PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) + assert(invoice.description == Right(Crypto.sha256(ByteVector.view("One piece of chocolate cake, one icecream cone, one pickle, one slice of swiss cheese, one slice of salami, one lollypop, one piece of cherry pie, one sausage, one cupcake, and one slice of watermelon".getBytes)))) + assert(invoice.fallbackAddress() === Some("mk2QpYatsKicvFVuTAQLBryyccRXMUaGHP")) + assert(invoice.tags.size == 5) + assert(invoice.sign(priv).toString == ref) } test("On mainnet, with fallback address 1RustyRX2oai4EYYDpQGWvEL62BBGqN9T with extra routing info to go via nodes 029e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255 then 039e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255") { val ref = "lnbc20m1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqsfpp3qjmp7lwpagxun9pygexvgpjdc4jdj85fr9yq20q82gphp2nflc7jtzrcazrra7wwgzxqc8u7754cdlpfrmccae92qgzqvzq2ps8pqqqqqqpqqqqq9qqqvpeuqafqxu92d8lr6fvg0r5gv0heeeqgcrqlnm6jhphu9y00rrhy4grqszsvpcgpy9qqqqqqgqqqqq7qqzq9qrsgqdfjcdk6w3ak5pca9hwfwfh63zrrz06wwfya0ydlzpgzxkn5xagsqz7x9j4jwe7yj7vaf2k9lqsdk45kts2fd0fkr28am0u4w95tt2nsq76cqw0" - val pr = PaymentRequest.read(ref) - assert(pr.prefix == "lnbc") - assert(pr.amount === Some(2000000000 msat)) - assert(pr.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102") - assert(pr.features.features === Features(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory)) - assert(pr.timestamp == TimestampSecond(1496314658L)) - assert(pr.nodeId == PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) - assert(pr.description == Right(Crypto.sha256(ByteVector.view("One piece of chocolate cake, one icecream cone, one pickle, one slice of swiss cheese, one slice of salami, one lollypop, one piece of cherry pie, one sausage, one cupcake, and one slice of watermelon".getBytes)))) - assert(pr.fallbackAddress() === Some("1RustyRX2oai4EYYDpQGWvEL62BBGqN9T")) - assert(pr.routingInfo === List(List( + val invoice = Bolt11Invoice.fromString(ref) + assert(invoice.prefix == "lnbc") + assert(invoice.amount_opt === Some(2000000000 msat)) + assert(invoice.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102") + assert(invoice.features === Features(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory)) + assert(invoice.createdAt == TimestampSecond(1496314658L)) + assert(invoice.nodeId == PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) + assert(invoice.description == Right(Crypto.sha256(ByteVector.view("One piece of chocolate cake, one icecream cone, one pickle, one slice of swiss cheese, one slice of salami, one lollypop, one piece of cherry pie, one sausage, one cupcake, and one slice of watermelon".getBytes)))) + assert(invoice.fallbackAddress() === Some("1RustyRX2oai4EYYDpQGWvEL62BBGqN9T")) + assert(invoice.routingInfo === List(List( ExtraHop(PublicKey(hex"029e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255"), ShortChannelId("66051x263430x1800"), 1 msat, 20, CltvExpiryDelta(3)), ExtraHop(PublicKey(hex"039e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255"), ShortChannelId("197637x395016x2314"), 2 msat, 30, CltvExpiryDelta(4)) ))) - assert(pr.tags.size == 6) - assert(PaymentRequest.write(pr.sign(priv)) == ref) + assert(invoice.tags.size == 6) + assert(invoice.sign(priv).toString == ref) } test("On mainnet, with fallback (p2sh) address 3EktnHQD7RiAE6uzMj2ZifT9YgRrkSgzQX") { val ref = "lnbc20m1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygshp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqfppj3a24vwu6r8ejrss3axul8rxldph2q7z99qrsgqz6qsgww34xlatfj6e3sngrwfy3ytkt29d2qttr8qz2mnedfqysuqypgqex4haa2h8fx3wnypranf3pdwyluftwe680jjcfp438u82xqphf75ym" - val pr = PaymentRequest.read(ref) - assert(pr.prefix == "lnbc") - assert(pr.amount === Some(2000000000 msat)) - assert(pr.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102") - assert(pr.features.features === Features(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory)) - assert(pr.timestamp == TimestampSecond(1496314658L)) - assert(pr.nodeId == PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) - assert(pr.description == Right(Crypto.sha256(ByteVector.view("One piece of chocolate cake, one icecream cone, one pickle, one slice of swiss cheese, one slice of salami, one lollypop, one piece of cherry pie, one sausage, one cupcake, and one slice of watermelon".getBytes)))) - assert(pr.fallbackAddress() === Some("3EktnHQD7RiAE6uzMj2ZifT9YgRrkSgzQX")) - assert(pr.tags.size == 5) - assert(PaymentRequest.write(pr.sign(priv)) == ref) + val invoice = Bolt11Invoice.fromString(ref) + assert(invoice.prefix == "lnbc") + assert(invoice.amount_opt === Some(2000000000 msat)) + assert(invoice.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102") + assert(invoice.features === Features(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory)) + assert(invoice.createdAt == TimestampSecond(1496314658L)) + assert(invoice.nodeId == PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) + assert(invoice.description == Right(Crypto.sha256(ByteVector.view("One piece of chocolate cake, one icecream cone, one pickle, one slice of swiss cheese, one slice of salami, one lollypop, one piece of cherry pie, one sausage, one cupcake, and one slice of watermelon".getBytes)))) + assert(invoice.fallbackAddress() === Some("3EktnHQD7RiAE6uzMj2ZifT9YgRrkSgzQX")) + assert(invoice.tags.size == 5) + assert(invoice.sign(priv).toString == ref) } test("On mainnet, with fallback (p2wpkh) address bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4") { val ref = "lnbc20m1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygshp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqfppqw508d6qejxtdg4y5r3zarvary0c5xw7k9qrsgqt29a0wturnys2hhxpner2e3plp6jyj8qx7548zr2z7ptgjjc7hljm98xhjym0dg52sdrvqamxdezkmqg4gdrvwwnf0kv2jdfnl4xatsqmrnsse" - val pr = PaymentRequest.read(ref) - assert(pr.prefix == "lnbc") - assert(pr.amount === Some(2000000000 msat)) - assert(pr.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102") - assert(pr.features.features === Features(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory)) - assert(pr.timestamp == TimestampSecond(1496314658L)) - assert(pr.nodeId == PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) - assert(pr.description == Right(Crypto.sha256(ByteVector.view("One piece of chocolate cake, one icecream cone, one pickle, one slice of swiss cheese, one slice of salami, one lollypop, one piece of cherry pie, one sausage, one cupcake, and one slice of watermelon".getBytes)))) - assert(pr.fallbackAddress() === Some("bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4")) - assert(pr.tags.size == 5) - assert(PaymentRequest.write(pr.sign(priv)) == ref) + val invoice = Bolt11Invoice.fromString(ref) + assert(invoice.prefix == "lnbc") + assert(invoice.amount_opt === Some(2000000000 msat)) + assert(invoice.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102") + assert(invoice.features === Features(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory)) + assert(invoice.createdAt == TimestampSecond(1496314658L)) + assert(invoice.nodeId == PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) + assert(invoice.description == Right(Crypto.sha256(ByteVector.view("One piece of chocolate cake, one icecream cone, one pickle, one slice of swiss cheese, one slice of salami, one lollypop, one piece of cherry pie, one sausage, one cupcake, and one slice of watermelon".getBytes)))) + assert(invoice.fallbackAddress() === Some("bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4")) + assert(invoice.tags.size == 5) + assert(invoice.sign(priv).toString == ref) } test("On mainnet, with fallback (p2wsh) address bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3") { val ref = "lnbc20m1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygshp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqfp4qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q9qrsgq9vlvyj8cqvq6ggvpwd53jncp9nwc47xlrsnenq2zp70fq83qlgesn4u3uyf4tesfkkwwfg3qs54qe426hp3tz7z6sweqdjg05axsrjqp9yrrwc" - val pr = PaymentRequest.read(ref) - assert(pr.prefix == "lnbc") - assert(pr.amount === Some(2000000000 msat)) - assert(pr.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102") - assert(pr.features.features === Features(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory)) - assert(!pr.features.allowMultiPart) - assert(pr.timestamp == TimestampSecond(1496314658L)) - assert(pr.nodeId == PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) - assert(pr.description == Right(Crypto.sha256(ByteVector.view("One piece of chocolate cake, one icecream cone, one pickle, one slice of swiss cheese, one slice of salami, one lollypop, one piece of cherry pie, one sausage, one cupcake, and one slice of watermelon".getBytes)))) - assert(pr.fallbackAddress() === Some("bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3")) - assert(pr.tags.size == 5) - assert(PaymentRequest.write(pr.sign(priv)) == ref) + val invoice = Bolt11Invoice.fromString(ref) + assert(invoice.prefix == "lnbc") + assert(invoice.amount_opt === Some(2000000000 msat)) + assert(invoice.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102") + assert(invoice.features === Features(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory)) + assert(!invoice.features.hasFeature(BasicMultiPartPayment)) + assert(invoice.createdAt == TimestampSecond(1496314658L)) + assert(invoice.nodeId == PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) + assert(invoice.description == Right(Crypto.sha256(ByteVector.view("One piece of chocolate cake, one icecream cone, one pickle, one slice of swiss cheese, one slice of salami, one lollypop, one piece of cherry pie, one sausage, one cupcake, and one slice of watermelon".getBytes)))) + assert(invoice.fallbackAddress() === Some("bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3")) + assert(invoice.tags.size == 5) + assert(invoice.sign(priv).toString == ref) } test("On mainnet, with fallback (p2wsh) address bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3 and a minimum htlc cltv expiry of 12") { val ref = "lnbc20m1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygscqpvpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqsfp4qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q9qrsgq999fraffdzl6c8j7qd325dfurcq7vl0mfkdpdvve9fy3hy4lw0x9j3zcj2qdh5e5pyrp6cncvmxrhchgey64culwmjtw9wym74xm6xqqevh9r0" - val pr = PaymentRequest.read(ref) - assert(pr.prefix == "lnbc") - assert(pr.amount === Some(2000000000 msat)) - assert(pr.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102") - assert(pr.features.features === Features(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory)) - assert(!pr.features.allowMultiPart) - assert(pr.timestamp == TimestampSecond(1496314658L)) - assert(pr.nodeId == PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) - assert(pr.description == Right(Crypto.sha256(ByteVector.view("One piece of chocolate cake, one icecream cone, one pickle, one slice of swiss cheese, one slice of salami, one lollypop, one piece of cherry pie, one sausage, one cupcake, and one slice of watermelon".getBytes)))) - assert(pr.fallbackAddress() === Some("bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3")) - assert(pr.minFinalCltvExpiryDelta === Some(CltvExpiryDelta(12))) - assert(pr.tags.size == 6) - assert(PaymentRequest.write(pr.sign(priv)) == ref) + val invoice = Bolt11Invoice.fromString(ref) + assert(invoice.prefix == "lnbc") + assert(invoice.amount_opt === Some(2000000000 msat)) + assert(invoice.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102") + assert(invoice.features === Features(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory)) + assert(!invoice.features.hasFeature(BasicMultiPartPayment)) + assert(invoice.createdAt == TimestampSecond(1496314658L)) + assert(invoice.nodeId == PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) + assert(invoice.description == Right(Crypto.sha256(ByteVector.view("One piece of chocolate cake, one icecream cone, one pickle, one slice of swiss cheese, one slice of salami, one lollypop, one piece of cherry pie, one sausage, one cupcake, and one slice of watermelon".getBytes)))) + assert(invoice.fallbackAddress() === Some("bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3")) + assert(invoice.minFinalCltvExpiryDelta === Some(CltvExpiryDelta(12))) + assert(invoice.tags.size == 6) + assert(invoice.sign(priv).toString == ref) } test("On mainnet, please send $30 for coffee beans to the same peer, which supports features 8, 14 and 99, using secret 0x1111111111111111111111111111111111111111111111111111111111111111") { @@ -257,74 +258,74 @@ class PaymentRequestSpec extends AnyFunSuite { ) for (ref <- refs) { - val pr = PaymentRequest.read(ref) - assert(pr.prefix === "lnbc") - assert(pr.amount === Some(2500000000L msat)) - assert(pr.paymentHash.bytes === hex"0001020304050607080900010203040506070809000102030405060708090102") - assert(pr.paymentSecret === Some(ByteVector32(hex"1111111111111111111111111111111111111111111111111111111111111111"))) - assert(pr.timestamp === TimestampSecond(1496314658L)) - assert(pr.nodeId === PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) - assert(pr.description === Left("coffee beans")) - assert(pr.features.bitmask === bin"1000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000100000000") - assert(!pr.features.allowMultiPart) - assert(pr.features.requirePaymentSecret) - assert(!pr.features.allowTrampoline) - assert(pr.features.areSupported(TestConstants.Alice.nodeParams)) - assert(PaymentRequest.write(pr.sign(priv)) === ref.toLowerCase) + val invoice = Bolt11Invoice.fromString(ref) + assert(invoice.prefix === "lnbc") + assert(invoice.amount_opt === Some(2500000000L msat)) + assert(invoice.paymentHash.bytes === hex"0001020304050607080900010203040506070809000102030405060708090102") + assert(invoice.paymentSecret === Some(ByteVector32(hex"1111111111111111111111111111111111111111111111111111111111111111"))) + assert(invoice.createdAt === TimestampSecond(1496314658L)) + assert(invoice.nodeId === PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) + assert(invoice.description === Left("coffee beans")) + assert(features2bits(invoice.features) === bin"1000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000100000000") + assert(!invoice.features.hasFeature(BasicMultiPartPayment)) + assert(invoice.features.hasFeature(PaymentSecret, Some(Mandatory))) + assert(!invoice.features.hasFeature(TrampolinePayment)) + assert(TestConstants.Alice.nodeParams.features.invoiceFeatures().areSupported(invoice.features)) + assert(invoice.sign(priv).toString === ref.toLowerCase) } } test("On mainnet, please send $30 for coffee beans to the same peer, which supports features 8, 14, 99 and 100, using secret 0x1111111111111111111111111111111111111111111111111111111111111111") { val ref = "lnbc25m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5vdhkven9v5sxyetpdeessp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9q4psqqqqqqqqqqqqqqqqsgqtqyx5vggfcsll4wu246hz02kp85x4katwsk9639we5n5yngc3yhqkm35jnjw4len8vrnqnf5ejh0mzj9n3vz2px97evektfm2l6wqccp3y7372" - val pr = PaymentRequest.read(ref) - assert(pr.prefix === "lnbc") - assert(pr.amount === Some(2500000000L msat)) - assert(pr.paymentHash.bytes === hex"0001020304050607080900010203040506070809000102030405060708090102") - assert(pr.paymentSecret === Some(ByteVector32(hex"1111111111111111111111111111111111111111111111111111111111111111"))) - assert(pr.timestamp === TimestampSecond(1496314658L)) - assert(pr.nodeId === PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) - assert(pr.description === Left("coffee beans")) - assert(pr.fallbackAddress().isEmpty) - assert(pr.features.bitmask === bin"000011000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000100000000") - assert(!pr.features.allowMultiPart) - assert(pr.features.requirePaymentSecret) - assert(!pr.features.allowTrampoline) - assert(!pr.features.areSupported(TestConstants.Alice.nodeParams)) - assert(PaymentRequest.write(pr.sign(priv)) === ref) + val invoice = Bolt11Invoice.fromString(ref) + assert(invoice.prefix === "lnbc") + assert(invoice.amount_opt === Some(2500000000L msat)) + assert(invoice.paymentHash.bytes === hex"0001020304050607080900010203040506070809000102030405060708090102") + assert(invoice.paymentSecret === Some(ByteVector32(hex"1111111111111111111111111111111111111111111111111111111111111111"))) + assert(invoice.createdAt === TimestampSecond(1496314658L)) + assert(invoice.nodeId === PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) + assert(invoice.description === Left("coffee beans")) + assert(invoice.fallbackAddress().isEmpty) + assert(features2bits(invoice.features) === bin"000011000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000100000000") + assert(!invoice.features.hasFeature(BasicMultiPartPayment)) + assert(invoice.features.hasFeature(PaymentSecret, Some(Mandatory))) + assert(!invoice.features.hasFeature(TrampolinePayment)) + assert(!TestConstants.Alice.nodeParams.features.invoiceFeatures().areSupported(invoice.features)) + assert(invoice.sign(priv).toString === ref) } test("On mainnet, please send 0.00967878534 BTC for a list of items within one week, amount in pico-BTC") { val ref = "lnbc9678785340p1pwmna7lpp5gc3xfm08u9qy06djf8dfflhugl6p7lgza6dsjxq454gxhj9t7a0sd8dgfkx7cmtwd68yetpd5s9xar0wfjn5gpc8qhrsdfq24f5ggrxdaezqsnvda3kkum5wfjkzmfqf3jkgem9wgsyuctwdus9xgrcyqcjcgpzgfskx6eqf9hzqnteypzxz7fzypfhg6trddjhygrcyqezcgpzfysywmm5ypxxjemgw3hxjmn8yptk7untd9hxwg3q2d6xjcmtv4ezq7pqxgsxzmnyyqcjqmt0wfjjq6t5v4khxsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygsxqyjw5qcqp2rzjq0gxwkzc8w6323m55m4jyxcjwmy7stt9hwkwe2qxmy8zpsgg7jcuwz87fcqqeuqqqyqqqqlgqqqqn3qq9q9qrsgqrvgkpnmps664wgkp43l22qsgdw4ve24aca4nymnxddlnp8vh9v2sdxlu5ywdxefsfvm0fq3sesf08uf6q9a2ke0hc9j6z6wlxg5z5kqpu2v9wz" - val pr = PaymentRequest.read(ref) - assert(pr.prefix === "lnbc") - assert(pr.amount === Some(967878534 msat)) - assert(pr.paymentHash.bytes === hex"462264ede7e14047e9b249da94fefc47f41f7d02ee9b091815a5506bc8abf75f") - assert(pr.features.features === Features(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory)) - assert(pr.features.areSupported(TestConstants.Alice.nodeParams)) - assert(pr.timestamp === TimestampSecond(1572468703L)) - assert(pr.nodeId === PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) - assert(pr.description === Left("Blockstream Store: 88.85 USD for Blockstream Ledger Nano S x 1, \"Back In My Day\" Sticker x 2, \"I Got Lightning Working\" Sticker x 2 and 1 more items")) - assert(pr.fallbackAddress().isEmpty) - assert(pr.expiry === Some(604800L)) - assert(pr.minFinalCltvExpiryDelta === Some(CltvExpiryDelta(10))) - assert(pr.routingInfo === Seq(Seq(ExtraHop(PublicKey(hex"03d06758583bb5154774a6eb221b1276c9e82d65bbaceca806d90e20c108f4b1c7"), ShortChannelId("589390x3312x1"), 1000 msat, 2500, CltvExpiryDelta(40))))) - assert(PaymentRequest.write(pr.sign(priv)) === ref) + val invoice = Bolt11Invoice.fromString(ref) + assert(invoice.prefix === "lnbc") + assert(invoice.amount_opt === Some(967878534 msat)) + assert(invoice.paymentHash.bytes === hex"462264ede7e14047e9b249da94fefc47f41f7d02ee9b091815a5506bc8abf75f") + assert(invoice.features === Features(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory)) + assert(TestConstants.Alice.nodeParams.features.invoiceFeatures().areSupported(invoice.features)) + assert(invoice.createdAt === TimestampSecond(1572468703L)) + assert(invoice.nodeId === PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) + assert(invoice.description === Left("Blockstream Store: 88.85 USD for Blockstream Ledger Nano S x 1, \"Back In My Day\" Sticker x 2, \"I Got Lightning Working\" Sticker x 2 and 1 more items")) + assert(invoice.fallbackAddress().isEmpty) + assert(invoice.relativeExpiry === 604800.seconds) + assert(invoice.minFinalCltvExpiryDelta === Some(CltvExpiryDelta(10))) + assert(invoice.routingInfo === Seq(Seq(ExtraHop(PublicKey(hex"03d06758583bb5154774a6eb221b1276c9e82d65bbaceca806d90e20c108f4b1c7"), ShortChannelId("589390x3312x1"), 1000 msat, 2500, CltvExpiryDelta(40))))) + assert(invoice.sign(priv).toString === ref) } test("On mainnet, please send 0.01 BTC with payment metadata 0x01fafaf0") { val ref = "lnbc10m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdp9wpshjmt9de6zqmt9w3skgct5vysxjmnnd9jx2mq8q8a04uqsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9q2gqqqqqqsgq7hf8he7ecf7n4ffphs6awl9t6676rrclv9ckg3d3ncn7fct63p6s365duk5wrk202cfy3aj5xnnp5gs3vrdvruverwwq7yzhkf5a3xqpd05wjc" - val pr = PaymentRequest.read(ref) - assert(pr.prefix == "lnbc") - assert(pr.amount === Some(1000000000 msat)) - assert(pr.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102") - assert(pr.features.features === Features(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory, PaymentMetadata -> Mandatory)) - assert(pr.timestamp == TimestampSecond(1496314658L)) - assert(pr.nodeId == PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) - assert(pr.paymentSecret === Some(ByteVector32(hex"1111111111111111111111111111111111111111111111111111111111111111"))) - assert(pr.description == Left("payment metadata inside")) - assert(pr.paymentMetadata === Some(hex"01fafaf0")) - assert(pr.tags.size == 5) - assert(PaymentRequest.write(pr.sign(priv)) == ref) + val invoice = Bolt11Invoice.fromString(ref) + assert(invoice.prefix == "lnbc") + assert(invoice.amount_opt === Some(1000000000 msat)) + assert(invoice.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102") + assert(invoice.features === Features(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory, PaymentMetadata -> Mandatory)) + assert(invoice.createdAt == TimestampSecond(1496314658L)) + assert(invoice.nodeId == PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) + assert(invoice.paymentSecret === Some(ByteVector32(hex"1111111111111111111111111111111111111111111111111111111111111111"))) + assert(invoice.description == Left("payment metadata inside")) + assert(invoice.paymentMetadata === Some(hex"01fafaf0")) + assert(invoice.tags.size == 5) + assert(invoice.sign(priv).toString == ref) } test("reject invalid invoices") { @@ -345,33 +346,32 @@ class PaymentRequestSpec extends AnyFunSuite { "lnbc2500000001p1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpusp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9qrsgq0lzc236j96a95uv0m3umg28gclm5lqxtqqwk32uuk4k6673k6n5kfvx3d2h8s295fad45fdhmusm8sjudfhlf6dcsxmfvkeywmjdkxcp99202x" ) for (ref <- refs) { - assertThrows[Exception](PaymentRequest.read(ref)) + assertThrows[Exception](Bolt11Invoice.fromString(ref)) } } test("correctly serialize/deserialize variable-length tagged fields") { val number = 123456 - val codec = PaymentRequest.Codecs.dataCodec(scodec.codecs.bits).as[PaymentRequest.Expiry] - val field = PaymentRequest.Expiry(number) + val codec = Bolt11Invoice.Codecs.dataCodec(scodec.codecs.bits).as[Bolt11Invoice.Expiry] + val field = Bolt11Invoice.Expiry(number) assert(field.toLong == number) val serializedExpiry = codec.encode(field).require val field1 = codec.decodeValue(serializedExpiry).require assert(field1 == field) - // Now with a payment request - val pr = PaymentRequest(chainHash = Block.LivenetGenesisBlock.hash, amount = Some(123 msat), paymentHash = ByteVector32(ByteVector.fill(32)(1)), privateKey = priv, description = Left("Some invoice"), minFinalCltvExpiryDelta = CltvExpiryDelta(18), expirySeconds = Some(123456), timestamp = 12345 unixsec) - assert(pr.minFinalCltvExpiryDelta === Some(CltvExpiryDelta(18))) - val serialized = PaymentRequest.write(pr) - val pr1 = PaymentRequest.read(serialized) - assert(pr == pr1) + val invoice = Bolt11Invoice(chainHash = Block.LivenetGenesisBlock.hash, amount = Some(123 msat), paymentHash = ByteVector32(ByteVector.fill(32)(1)), privateKey = priv, description = Left("Some invoice"), minFinalCltvExpiryDelta = CltvExpiryDelta(18), expirySeconds = Some(123456), timestamp = 12345 unixsec) + assert(invoice.minFinalCltvExpiryDelta === Some(CltvExpiryDelta(18))) + val serialized = invoice.toString + val pr1 = Bolt11Invoice.fromString(serialized) + assert(invoice == pr1) } test("ignore unknown tags") { - val pr = PaymentRequest( + val invoice = Bolt11Invoice( prefix = "lntb", - amount = Some(100000 msat), - timestamp = TimestampSecond.now(), + amount_opt = Some(100000 msat), + createdAt = TimestampSecond.now(), nodeId = nodeId, tags = List( PaymentHash(ByteVector32(ByteVector.fill(32)(1))), @@ -380,8 +380,8 @@ class PaymentRequestSpec extends AnyFunSuite { ), signature = ByteVector.empty).sign(priv) - val serialized = PaymentRequest.write(pr) - val pr1 = PaymentRequest.read(serialized) + val serialized = invoice.toString + val pr1 = Bolt11Invoice.fromString(serialized) val Some(_) = pr1.tags.collectFirst { case u: UnknownTag21 => u } } @@ -409,45 +409,45 @@ class PaymentRequestSpec extends AnyFunSuite { } } - test("accept uppercase payment request") { + test("accept uppercase invoices") { val input = "lntb1500n1pwxx94fsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygspp5q3xzmwuvxpkyhz6pvg3fcfxz0259kgh367qazj62af9rs0pw07dsdpa2fjkzep6yp58garswvaz7tmvd9nksarwd9hxw6n0w4kx2tnrdakj7grfwvs8wcqzysxqr23swwl9egjej7rvvt9zdxrtpy8xuu6cckdwajfccmtz7n90ea34k3j595w77pt69s5dx5a46f4k4w5avtvjkc4l4rm8n4xmk7fe3pms3pspdd032j" - assert(PaymentRequest.write(PaymentRequest.read(input.toUpperCase())) == input) + assert(Bolt11Invoice.fromString(input.toUpperCase()).toString == input) } test("Pay 1 BTC without multiplier") { val ref = "lnbc1000m1pdkmqhusp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygspp5n2ees808r98m0rh4472yyth0c5fptzcxmexcjznrzmq8xald0cgqdqsf4ujqarfwqsxymmccqp2pv37ezvhth477nu0yhhjlcry372eef57qmldhreqnr0kx82jkupp3n7nw42u3kdyyjskdr8jhjy2vugr3skdmy8ersft36969xplkxsp2v7c58" - val pr = PaymentRequest.read(ref) - assert(pr.amount === Some(100000000000L msat)) - assert(pr.features.bitmask === BitVector.empty) + val invoice = Bolt11Invoice.fromString(ref) + assert(invoice.amount_opt === Some(100000000000L msat)) + assert(features2bits(invoice.features) === BitVector.empty) } - test("supported payment request features") { - val nodeParams = TestConstants.Alice.nodeParams.copy(features = Features(knownFeatures.map(f => f -> FeatureSupport.Optional).toMap)) + test("supported invoice features") { + val nodeParams = TestConstants.Alice.nodeParams.copy(features = Features(knownFeatures.map(f => f -> Optional).toMap)) case class Result(allowMultiPart: Boolean, requirePaymentSecret: Boolean, areSupported: Boolean) // "supported" is based on the "it's okay to be odd" rule val featureBits = Map( - PaymentRequestFeatures(bin" 00000100000100000000") -> Result(allowMultiPart = false, requirePaymentSecret = true, areSupported = true), - PaymentRequestFeatures(bin" 00010100000100000000") -> Result(allowMultiPart = true, requirePaymentSecret = true, areSupported = true), - PaymentRequestFeatures(bin" 00100100000100000000") -> Result(allowMultiPart = true, requirePaymentSecret = true, areSupported = true), - PaymentRequestFeatures(bin" 00010100000100000000") -> Result(allowMultiPart = true, requirePaymentSecret = true, areSupported = true), - PaymentRequestFeatures(bin" 00010100000100000000") -> Result(allowMultiPart = true, requirePaymentSecret = true, areSupported = true), - PaymentRequestFeatures(bin" 00100100000100000000") -> Result(allowMultiPart = true, requirePaymentSecret = true, areSupported = true), - PaymentRequestFeatures(bin" 01000100000100000000") -> Result(allowMultiPart = false, requirePaymentSecret = true, areSupported = true), - PaymentRequestFeatures(bin" 0000010000100000100000000") -> Result(allowMultiPart = false, requirePaymentSecret = true, areSupported = true), - PaymentRequestFeatures(bin" 0000011000100000100000000") -> Result(allowMultiPart = false, requirePaymentSecret = true, areSupported = true), - PaymentRequestFeatures(bin" 0000110000101000100000000") -> Result(allowMultiPart = false, requirePaymentSecret = true, areSupported = true), - PaymentRequestFeatures(bin" 0000100000101000100000000") -> Result(allowMultiPart = false, requirePaymentSecret = true, areSupported = true), - PaymentRequestFeatures(bin" 0010000000101000100000000") -> Result(allowMultiPart = false, requirePaymentSecret = true, areSupported = true), + Features(bin" 00000100000100000000") -> Result(allowMultiPart = false, requirePaymentSecret = true, areSupported = true), + Features(bin" 00010100000100000000") -> Result(allowMultiPart = true, requirePaymentSecret = true, areSupported = true), + Features(bin" 00100100000100000000") -> Result(allowMultiPart = true, requirePaymentSecret = true, areSupported = true), + Features(bin" 00010100000100000000") -> Result(allowMultiPart = true, requirePaymentSecret = true, areSupported = true), + Features(bin" 00010100000100000000") -> Result(allowMultiPart = true, requirePaymentSecret = true, areSupported = true), + Features(bin" 00100100000100000000") -> Result(allowMultiPart = true, requirePaymentSecret = true, areSupported = true), + Features(bin" 01000100000100000000") -> Result(allowMultiPart = false, requirePaymentSecret = true, areSupported = true), + Features(bin" 0000010000100000100000000") -> Result(allowMultiPart = false, requirePaymentSecret = true, areSupported = true), + Features(bin" 0000011000100000100000000") -> Result(allowMultiPart = false, requirePaymentSecret = true, areSupported = true), + Features(bin" 0000110000101000100000000") -> Result(allowMultiPart = false, requirePaymentSecret = true, areSupported = true), + Features(bin" 0000100000101000100000000") -> Result(allowMultiPart = false, requirePaymentSecret = true, areSupported = true), + Features(bin" 0010000000101000100000000") -> Result(allowMultiPart = false, requirePaymentSecret = true, areSupported = true), // those are useful for nonreg testing of the areSupported method (which needs to be updated with every new supported mandatory bit) - PaymentRequestFeatures(bin" 000001000000000100000100000000") -> Result(allowMultiPart = false, requirePaymentSecret = true, areSupported = false), - PaymentRequestFeatures(bin" 000100000000000100000100000000") -> Result(allowMultiPart = false, requirePaymentSecret = true, areSupported = true), - PaymentRequestFeatures(bin"00000010000000000000100000100000000") -> Result(allowMultiPart = false, requirePaymentSecret = true, areSupported = false), - PaymentRequestFeatures(bin"00001000000000000000100000100000000") -> Result(allowMultiPart = false, requirePaymentSecret = true, areSupported = false) + Features(bin" 000001000000000100000100000000") -> Result(allowMultiPart = false, requirePaymentSecret = true, areSupported = false), + Features(bin" 000100000000000100000100000000") -> Result(allowMultiPart = false, requirePaymentSecret = true, areSupported = true), + Features(bin"00000010000000000000100000100000000") -> Result(allowMultiPart = false, requirePaymentSecret = true, areSupported = false), + Features(bin"00001000000000000000100000100000000") -> Result(allowMultiPart = false, requirePaymentSecret = true, areSupported = false) ) for ((features, res) <- featureBits) { - val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(123 msat), ByteVector32.One, priv, Left("Some invoice"), CltvExpiryDelta(18), features = features) - assert(Result(pr.features.allowMultiPart, pr.features.requirePaymentSecret, pr.features.areSupported(nodeParams)) === res) - assert(PaymentRequest.read(PaymentRequest.write(pr)) === pr) + val invoice = Bolt11Invoice(Block.LivenetGenesisBlock.hash, Some(123 msat), ByteVector32.One, priv, Left("Some invoice"), CltvExpiryDelta(18), features = features) + assert(Result(invoice.features.hasFeature(BasicMultiPartPayment), invoice.features.hasFeature(PaymentSecret, Some(Mandatory)), nodeParams.features.areSupported(invoice.features)) === res) + assert(Bolt11Invoice.fromString(invoice.toString) === invoice) } } @@ -464,51 +464,48 @@ class PaymentRequestSpec extends AnyFunSuite { ) for ((bitmask, featureBytes) <- testCases) { - assert(PaymentRequestFeatures(bitmask).toByteVector === featureBytes) + assert(Features(bitmask).toByteVector === featureBytes) } } test("payment secret") { - val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(123 msat), ByteVector32.One, priv, Left("Some invoice"), CltvExpiryDelta(18)) - assert(pr.paymentSecret.isDefined) - assert(pr.features === PaymentRequestFeatures(Map[Feature with InvoiceFeature, FeatureSupport](VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory))) - assert(pr.features.requirePaymentSecret) + val invoice = Bolt11Invoice(Block.LivenetGenesisBlock.hash, Some(123 msat), ByteVector32.One, priv, Left("Some invoice"), CltvExpiryDelta(18)) + assert(invoice.paymentSecret.isDefined) + assert(invoice.features === Features(PaymentSecret -> Mandatory, VariableLengthOnion -> Mandatory)) + assert(invoice.features.hasFeature(PaymentSecret, Some(Mandatory))) - val pr1 = PaymentRequest.read(PaymentRequest.write(pr)) - assert(pr1.paymentSecret === pr.paymentSecret) + val pr1 = Bolt11Invoice.fromString(invoice.toString) + assert(pr1.paymentSecret === invoice.paymentSecret) - val pr2 = PaymentRequest.read("lnbc40n1pw9qjvwpp5qq3w2ln6krepcslqszkrsfzwy49y0407hvks30ec6pu9s07jur3sdpstfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqenwdencqzysxqrrss7ju0s4dwx6w8a95a9p2xc5vudl09gjl0w2n02sjrvffde632nxwh2l4w35nqepj4j5njhh4z65wyfc724yj6dn9wajvajfn5j7em6wsq2elakl") - assert(!pr2.features.requirePaymentSecret) + val pr2 = Bolt11Invoice.fromString("lnbc40n1pw9qjvwpp5qq3w2ln6krepcslqszkrsfzwy49y0407hvks30ec6pu9s07jur3sdpstfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqenwdencqzysxqrrss7ju0s4dwx6w8a95a9p2xc5vudl09gjl0w2n02sjrvffde632nxwh2l4w35nqepj4j5njhh4z65wyfc724yj6dn9wajvajfn5j7em6wsq2elakl") + assert(!pr2.features.hasFeature(PaymentSecret, Some(Mandatory))) assert(pr2.paymentSecret === None) // An invoice that sets the payment secret feature bit must provide a payment secret. assertThrows[IllegalArgumentException]( - PaymentRequest.read("lnbc1230p1pwljzn3pp5qyqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqdq52dhk6efqd9h8vmmfvdjs9qypqsqylvwhf7xlpy6xpecsnpcjjuuslmzzgeyv90mh7k7vs88k2dkxgrkt75qyfjv5ckygw206re7spga5zfd4agtdvtktxh5pkjzhn9dq2cqz9upw7") + Bolt11Invoice.fromString("lnbc1230p1pwljzn3pp5qyqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqdq52dhk6efqd9h8vmmfvdjs9qypqsqylvwhf7xlpy6xpecsnpcjjuuslmzzgeyv90mh7k7vs88k2dkxgrkt75qyfjv5ckygw206re7spga5zfd4agtdvtktxh5pkjzhn9dq2cqz9upw7") ) // A multi-part invoice must use a payment secret. - assertThrows[IllegalArgumentException]({ - val features = PaymentRequestFeatures(Map[Feature with InvoiceFeature, FeatureSupport](VariableLengthOnion -> Optional, PaymentSecret -> Optional)) - PaymentRequest(Block.LivenetGenesisBlock.hash, Some(123 msat), ByteVector32.One, priv, Left("MPP without secrets"), CltvExpiryDelta(18), features = features) - }) + assertThrows[IllegalArgumentException]( + Bolt11Invoice(Block.LivenetGenesisBlock.hash, Some(123 msat), ByteVector32.One, priv, Left("MPP without secrets"), CltvExpiryDelta(18), features = Features(VariableLengthOnion -> Optional, PaymentSecret -> Optional)) + ) } test("trampoline") { - val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(123 msat), ByteVector32.One, priv, Left("Some invoice"), CltvExpiryDelta(18)) - assert(!pr.features.allowTrampoline) + val invoice = Bolt11Invoice(Block.LivenetGenesisBlock.hash, Some(123 msat), ByteVector32.One, priv, Left("Some invoice"), CltvExpiryDelta(18)) + assert(!invoice.features.hasFeature(TrampolinePayment)) - val features1 = PaymentRequestFeatures(Map[Feature with InvoiceFeature, FeatureSupport](VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory, TrampolinePayment -> Optional)) - val pr1 = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(123 msat), ByteVector32.One, priv, Left("Some invoice"), CltvExpiryDelta(18), features = features1) - assert(!pr1.features.allowMultiPart) - assert(pr1.features.allowTrampoline) + val pr1 = Bolt11Invoice(Block.LivenetGenesisBlock.hash, Some(123 msat), ByteVector32.One, priv, Left("Some invoice"), CltvExpiryDelta(18), features = Features(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory, TrampolinePayment -> Optional)) + assert(!pr1.features.hasFeature(BasicMultiPartPayment)) + assert(pr1.features.hasFeature(TrampolinePayment)) - val features2 = PaymentRequestFeatures(Map[Feature with InvoiceFeature, FeatureSupport](VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory, BasicMultiPartPayment -> Optional, TrampolinePayment -> Optional)) - val pr2 = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(123 msat), ByteVector32.One, priv, Left("Some invoice"), CltvExpiryDelta(18), features = features2) - assert(pr2.features.allowMultiPart) - assert(pr2.features.allowTrampoline) + val pr2 = Bolt11Invoice(Block.LivenetGenesisBlock.hash, Some(123 msat), ByteVector32.One, priv, Left("Some invoice"), CltvExpiryDelta(18), features = Features(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory, BasicMultiPartPayment -> Optional, TrampolinePayment -> Optional)) + assert(pr2.features.hasFeature(BasicMultiPartPayment)) + assert(pr2.features.hasFeature(TrampolinePayment)) - val pr3 = PaymentRequest.read("lnbc40n1pw9qjvwpp5qq3w2ln6krepcslqszkrsfzwy49y0407hvks30ec6pu9s07jur3sdpstfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqenwdencqzysxqrrss7ju0s4dwx6w8a95a9p2xc5vudl09gjl0w2n02sjrvffde632nxwh2l4w35nqepj4j5njhh4z65wyfc724yj6dn9wajvajfn5j7em6wsq2elakl") - assert(!pr3.features.allowTrampoline) + val pr3 = Bolt11Invoice.fromString("lnbc40n1pw9qjvwpp5qq3w2ln6krepcslqszkrsfzwy49y0407hvks30ec6pu9s07jur3sdpstfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqenwdencqzysxqrrss7ju0s4dwx6w8a95a9p2xc5vudl09gjl0w2n02sjrvffde632nxwh2l4w35nqepj4j5njhh4z65wyfc724yj6dn9wajvajfn5j7em6wsq2elakl") + assert(!pr3.features.hasFeature(TrampolinePayment)) } test("nonreg") { @@ -575,7 +572,7 @@ class PaymentRequestSpec extends AnyFunSuite { ) for (req <- requests) { - assert(PaymentRequest.write(PaymentRequest.read(req)) == req) + assert(Bolt11Invoice.fromString(req).toString == req) } } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartHandlerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartHandlerSpec.scala index f3f226a681..71f7ac5f72 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartHandlerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartHandlerSpec.scala @@ -25,8 +25,8 @@ import fr.acinq.eclair.Features._ import fr.acinq.eclair.TestConstants.Alice import fr.acinq.eclair.channel.{CMD_FAIL_HTLC, CMD_FULFILL_HTLC, Register} import fr.acinq.eclair.db.IncomingPaymentStatus +import fr.acinq.eclair.payment.Bolt11Invoice.ExtraHop import fr.acinq.eclair.payment.PaymentReceived.PartialPayment -import fr.acinq.eclair.payment.PaymentRequest.ExtraHop import fr.acinq.eclair.payment.receive.MultiPartHandler.{DoFulfill, GetPendingPayments, PendingPayments, ReceivePayment} import fr.acinq.eclair.payment.receive.MultiPartPaymentFSM.HtlcPart import fr.acinq.eclair.payment.receive.{MultiPartPaymentFSM, PaymentHandler} @@ -86,20 +86,20 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike { sender.send(handlerWithoutMpp, ReceivePayment(Some(amountMsat), Left("1 coffee"))) - val pr = sender.expectMsgType[PaymentRequest] - val incoming = nodeParams.db.payments.getIncomingPayment(pr.paymentHash) + val invoice = sender.expectMsgType[Invoice] + val incoming = nodeParams.db.payments.getIncomingPayment(invoice.paymentHash) assert(incoming.isDefined) assert(incoming.get.status === IncomingPaymentStatus.Pending) - assert(!incoming.get.paymentRequest.isExpired) - assert(Crypto.sha256(incoming.get.paymentPreimage) === pr.paymentHash) + assert(!incoming.get.invoice.isExpired()) + assert(Crypto.sha256(incoming.get.paymentPreimage) === invoice.paymentHash) - val add = UpdateAddHtlc(ByteVector32.One, 0, amountMsat, pr.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket) - sender.send(handlerWithoutMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createSinglePartPayload(add.amountMsat, add.cltvExpiry, pr.paymentSecret.get, pr.paymentMetadata))) + val add = UpdateAddHtlc(ByteVector32.One, 0, amountMsat, invoice.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket) + sender.send(handlerWithoutMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createSinglePartPayload(add.amountMsat, add.cltvExpiry, invoice.paymentSecret.get, invoice.paymentMetadata))) register.expectMsgType[Register.Forward[CMD_FULFILL_HTLC]] val paymentReceived = eventListener.expectMsgType[PaymentReceived] assert(paymentReceived.copy(parts = paymentReceived.parts.map(_.copy(timestamp = 0 unixms))) === PaymentReceived(add.paymentHash, PartialPayment(amountMsat, add.channelId, timestamp = 0 unixms) :: Nil)) - val received = nodeParams.db.payments.getIncomingPayment(pr.paymentHash) + val received = nodeParams.db.payments.getIncomingPayment(invoice.paymentHash) assert(received.isDefined && received.get.status.isInstanceOf[IncomingPaymentStatus.Received]) assert(received.get.status.asInstanceOf[IncomingPaymentStatus.Received].copy(receivedAt = 0 unixms) === IncomingPaymentStatus.Received(amountMsat, 0 unixms)) @@ -108,17 +108,17 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike { sender.send(handlerWithMpp, ReceivePayment(Some(amountMsat), Left("another coffee with multi-part"))) - val pr = sender.expectMsgType[PaymentRequest] - assert(pr.features.allowMultiPart) - assert(nodeParams.db.payments.getIncomingPayment(pr.paymentHash).get.status === IncomingPaymentStatus.Pending) + val invoice = sender.expectMsgType[Invoice] + assert(invoice.features.hasFeature(Features.BasicMultiPartPayment)) + assert(nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).get.status === IncomingPaymentStatus.Pending) - val add = UpdateAddHtlc(ByteVector32.One, 0, amountMsat, pr.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket) - sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createSinglePartPayload(add.amountMsat, add.cltvExpiry, pr.paymentSecret.get, pr.paymentMetadata))) + val add = UpdateAddHtlc(ByteVector32.One, 0, amountMsat, invoice.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket) + sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createSinglePartPayload(add.amountMsat, add.cltvExpiry, invoice.paymentSecret.get, invoice.paymentMetadata))) register.expectMsgType[Register.Forward[CMD_FULFILL_HTLC]] val paymentReceived = eventListener.expectMsgType[PaymentReceived] assert(paymentReceived.copy(parts = paymentReceived.parts.map(_.copy(timestamp = 0 unixms))) === PaymentReceived(add.paymentHash, PartialPayment(amountMsat, add.channelId, timestamp = 0 unixms) :: Nil)) - val received = nodeParams.db.payments.getIncomingPayment(pr.paymentHash) + val received = nodeParams.db.payments.getIncomingPayment(invoice.paymentHash) assert(received.isDefined && received.get.status.isInstanceOf[IncomingPaymentStatus.Received]) assert(received.get.status.asInstanceOf[IncomingPaymentStatus.Received].copy(receivedAt = 0 unixms) === IncomingPaymentStatus.Received(amountMsat, 0 unixms)) @@ -127,21 +127,21 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike { sender.send(handlerWithMpp, ReceivePayment(Some(amountMsat), Left("bad expiry"))) - val pr = sender.expectMsgType[PaymentRequest] - assert(nodeParams.db.payments.getIncomingPayment(pr.paymentHash).get.status === IncomingPaymentStatus.Pending) + val invoice = sender.expectMsgType[Invoice] + assert(nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).get.status === IncomingPaymentStatus.Pending) - val add = UpdateAddHtlc(ByteVector32.One, 0, amountMsat, pr.paymentHash, CltvExpiryDelta(3).toCltvExpiry(nodeParams.currentBlockHeight), TestConstants.emptyOnionPacket) - sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createSinglePartPayload(add.amountMsat, add.cltvExpiry, pr.paymentSecret.get, pr.paymentMetadata))) + val add = UpdateAddHtlc(ByteVector32.One, 0, amountMsat, invoice.paymentHash, CltvExpiryDelta(3).toCltvExpiry(nodeParams.currentBlockHeight), TestConstants.emptyOnionPacket) + sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createSinglePartPayload(add.amountMsat, add.cltvExpiry, invoice.paymentSecret.get, invoice.paymentMetadata))) val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message assert(cmd.reason == Right(IncorrectOrUnknownPaymentDetails(amountMsat, nodeParams.currentBlockHeight))) - assert(nodeParams.db.payments.getIncomingPayment(pr.paymentHash).get.status === IncomingPaymentStatus.Pending) + assert(nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).get.status === IncomingPaymentStatus.Pending) eventListener.expectNoMessage(100 milliseconds) sender.expectNoMessage(50 millis) } } - test("Payment request generation should fail when the amount is not valid") { f => + test("Invoice generation should fail when the amount is not valid") { f => import f._ // negative amount should fail @@ -156,66 +156,66 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike // success with 1 mBTC sender.send(handlerWithMpp, ReceivePayment(Some(100000000 msat), Left("1 coffee"))) - val pr = sender.expectMsgType[PaymentRequest] - assert(pr.amount.contains(100000000 msat) && pr.nodeId.toString == nodeParams.nodeId.toString) + val invoice = sender.expectMsgType[Invoice] + assert(invoice.amount_opt.contains(100000000 msat) && invoice.nodeId.toString == nodeParams.nodeId.toString) } - test("Payment request generation should succeed when the amount is not set") { f => + test("Invoice generation should succeed when the amount is not set") { f => import f._ sender.send(handlerWithMpp, ReceivePayment(None, Left("This is a donation PR"))) - val pr = sender.expectMsgType[PaymentRequest] - assert(pr.amount.isEmpty && pr.nodeId.toString == Alice.nodeParams.nodeId.toString) + val invoice = sender.expectMsgType[Invoice] + assert(invoice.amount_opt.isEmpty && invoice.nodeId.toString == Alice.nodeParams.nodeId.toString) } - test("Payment request generation should handle custom expiries or use the default otherwise") { f => + test("Invoice generation should handle custom expiries or use the default otherwise") { f => import f._ sender.send(handlerWithMpp, ReceivePayment(Some(42000 msat), Left("1 coffee"))) - val pr1 = sender.expectMsgType[PaymentRequest] + val pr1 = sender.expectMsgType[Invoice] assert(pr1.minFinalCltvExpiryDelta === Some(nodeParams.channelConf.minFinalExpiryDelta)) - assert(pr1.expiry === Some(Alice.nodeParams.paymentRequestExpiry.toSeconds)) + assert(pr1.relativeExpiry === Alice.nodeParams.invoiceExpiry) sender.send(handlerWithMpp, ReceivePayment(Some(42000 msat), Left("1 coffee with custom expiry"), expirySeconds_opt = Some(60))) - val pr2 = sender.expectMsgType[PaymentRequest] + val pr2 = sender.expectMsgType[Invoice] assert(pr2.minFinalCltvExpiryDelta === Some(nodeParams.channelConf.minFinalExpiryDelta)) - assert(pr2.expiry === Some(60)) + assert(pr2.relativeExpiry === 60.seconds) } - test("Payment request generation with trampoline support") { _ => + test("Invoice generation with trampoline support") { _ => val sender = TestProbe() { val handler = TestActorRef[PaymentHandler](PaymentHandler.props(Alice.nodeParams.copy(enableTrampolinePayment = false, features = featuresWithoutMpp), TestProbe().ref)) sender.send(handler, ReceivePayment(Some(42 msat), Left("1 coffee"))) - val pr = sender.expectMsgType[PaymentRequest] - assert(!pr.features.allowMultiPart) - assert(!pr.features.allowTrampoline) + val invoice = sender.expectMsgType[Invoice] + assert(!invoice.features.hasFeature(Features.BasicMultiPartPayment)) + assert(!invoice.features.hasFeature(Features.TrampolinePayment)) } { val handler = TestActorRef[PaymentHandler](PaymentHandler.props(Alice.nodeParams.copy(enableTrampolinePayment = false, features = featuresWithMpp), TestProbe().ref)) sender.send(handler, ReceivePayment(Some(42 msat), Left("1 coffee"))) - val pr = sender.expectMsgType[PaymentRequest] - assert(pr.features.allowMultiPart) - assert(!pr.features.allowTrampoline) + val invoice = sender.expectMsgType[Invoice] + assert(invoice.features.hasFeature(Features.BasicMultiPartPayment)) + assert(!invoice.features.hasFeature(Features.TrampolinePayment)) } { val handler = TestActorRef[PaymentHandler](PaymentHandler.props(Alice.nodeParams.copy(enableTrampolinePayment = true, features = featuresWithoutMpp), TestProbe().ref)) sender.send(handler, ReceivePayment(Some(42 msat), Left("1 coffee"))) - val pr = sender.expectMsgType[PaymentRequest] - assert(!pr.features.allowMultiPart) - assert(pr.features.allowTrampoline) + val invoice = sender.expectMsgType[Invoice] + assert(!invoice.features.hasFeature(Features.BasicMultiPartPayment)) + assert(invoice.features.hasFeature(Features.TrampolinePayment)) } { val handler = TestActorRef[PaymentHandler](PaymentHandler.props(Alice.nodeParams.copy(enableTrampolinePayment = true, features = featuresWithMpp), TestProbe().ref)) sender.send(handler, ReceivePayment(Some(42 msat), Left("1 coffee"))) - val pr = sender.expectMsgType[PaymentRequest] - assert(pr.features.allowMultiPart) - assert(pr.features.allowTrampoline) + val invoice = sender.expectMsgType[Invoice] + assert(invoice.features.hasFeature(Features.BasicMultiPartPayment)) + assert(invoice.features.hasFeature(Features.TrampolinePayment)) } } - test("Generated payment request contains the provided extra hops") { f => + test("Generated invoice contains the provided extra hops") { f => import f._ val x = randomKey().publicKey @@ -227,127 +227,127 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike val route_x_t = extraHop_x_t :: Nil sender.send(handlerWithMpp, ReceivePayment(Some(42000 msat), Left("1 coffee with additional routing info"), extraHops = List(route_x_z, route_x_t))) - assert(sender.expectMsgType[PaymentRequest].routingInfo === Seq(route_x_z, route_x_t)) + assert(sender.expectMsgType[Invoice].routingInfo === Seq(route_x_z, route_x_t)) sender.send(handlerWithMpp, ReceivePayment(Some(42000 msat), Left("1 coffee without routing info"))) - assert(sender.expectMsgType[PaymentRequest].routingInfo === Nil) + assert(sender.expectMsgType[Invoice].routingInfo === Nil) } - test("PaymentHandler should reject incoming payments if the payment request is expired") { f => + test("PaymentHandler should reject incoming payments if the invoice is expired") { f => import f._ sender.send(handlerWithoutMpp, ReceivePayment(Some(1000 msat), Left("some desc"), expirySeconds_opt = Some(0))) - val pr = sender.expectMsgType[PaymentRequest] - assert(!pr.features.allowMultiPart) - assert(pr.isExpired) + val invoice = sender.expectMsgType[Invoice] + assert(!invoice.features.hasFeature(Features.BasicMultiPartPayment)) + assert(invoice.isExpired()) - val add = UpdateAddHtlc(ByteVector32.One, 0, 1000 msat, pr.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket) - sender.send(handlerWithoutMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createSinglePartPayload(add.amountMsat, add.cltvExpiry, pr.paymentSecret.get, pr.paymentMetadata))) + val add = UpdateAddHtlc(ByteVector32.One, 0, 1000 msat, invoice.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket) + sender.send(handlerWithoutMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createSinglePartPayload(add.amountMsat, add.cltvExpiry, invoice.paymentSecret.get, invoice.paymentMetadata))) register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]] - val Some(incoming) = nodeParams.db.payments.getIncomingPayment(pr.paymentHash) - assert(incoming.paymentRequest.isExpired && incoming.status === IncomingPaymentStatus.Expired) + val Some(incoming) = nodeParams.db.payments.getIncomingPayment(invoice.paymentHash) + assert(incoming.invoice.isExpired() && incoming.status === IncomingPaymentStatus.Expired) } - test("PaymentHandler should reject incoming multi-part payment if the payment request is expired") { f => + test("PaymentHandler should reject incoming multi-part payment if the invoice is expired") { f => import f._ sender.send(handlerWithMpp, ReceivePayment(Some(1000 msat), Left("multi-part expired"), expirySeconds_opt = Some(0))) - val pr = sender.expectMsgType[PaymentRequest] - assert(pr.features.allowMultiPart) - assert(pr.isExpired) + val invoice = sender.expectMsgType[Invoice] + assert(invoice.features.hasFeature(Features.BasicMultiPartPayment)) + assert(invoice.isExpired()) - val add = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, pr.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket) - sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createMultiPartPayload(add.amountMsat, 1000 msat, add.cltvExpiry, pr.paymentSecret.get, pr.paymentMetadata))) + val add = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, invoice.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket) + sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createMultiPartPayload(add.amountMsat, 1000 msat, add.cltvExpiry, invoice.paymentSecret.get, invoice.paymentMetadata))) val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message assert(cmd.reason == Right(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight))) - val Some(incoming) = nodeParams.db.payments.getIncomingPayment(pr.paymentHash) - assert(incoming.paymentRequest.isExpired && incoming.status === IncomingPaymentStatus.Expired) + val Some(incoming) = nodeParams.db.payments.getIncomingPayment(invoice.paymentHash) + assert(incoming.invoice.isExpired() && incoming.status === IncomingPaymentStatus.Expired) } - test("PaymentHandler should reject incoming multi-part payment if the payment request does not allow it") { f => + test("PaymentHandler should reject incoming multi-part payment if the invoice does not allow it") { f => import f._ sender.send(handlerWithoutMpp, ReceivePayment(Some(1000 msat), Left("no multi-part support"))) - val pr = sender.expectMsgType[PaymentRequest] - assert(!pr.features.allowMultiPart) + val invoice = sender.expectMsgType[Invoice] + assert(!invoice.features.hasFeature(Features.BasicMultiPartPayment)) - val add = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, pr.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket) - sender.send(handlerWithoutMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createMultiPartPayload(add.amountMsat, 1000 msat, add.cltvExpiry, pr.paymentSecret.get, pr.paymentMetadata))) + val add = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, invoice.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket) + sender.send(handlerWithoutMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createMultiPartPayload(add.amountMsat, 1000 msat, add.cltvExpiry, invoice.paymentSecret.get, invoice.paymentMetadata))) val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message assert(cmd.reason == Right(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight))) - assert(nodeParams.db.payments.getIncomingPayment(pr.paymentHash).get.status === IncomingPaymentStatus.Pending) + assert(nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).get.status === IncomingPaymentStatus.Pending) } test("PaymentHandler should reject incoming multi-part payment with an invalid expiry") { f => import f._ sender.send(handlerWithMpp, ReceivePayment(Some(1000 msat), Left("multi-part invalid expiry"))) - val pr = sender.expectMsgType[PaymentRequest] - assert(pr.features.allowMultiPart) + val invoice = sender.expectMsgType[Invoice] + assert(invoice.features.hasFeature(Features.BasicMultiPartPayment)) val lowCltvExpiry = nodeParams.channelConf.fulfillSafetyBeforeTimeout.toCltvExpiry(nodeParams.currentBlockHeight) - val add = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, pr.paymentHash, lowCltvExpiry, TestConstants.emptyOnionPacket) - sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createMultiPartPayload(add.amountMsat, 1000 msat, add.cltvExpiry, pr.paymentSecret.get, pr.paymentMetadata))) + val add = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, invoice.paymentHash, lowCltvExpiry, TestConstants.emptyOnionPacket) + sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createMultiPartPayload(add.amountMsat, 1000 msat, add.cltvExpiry, invoice.paymentSecret.get, invoice.paymentMetadata))) val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message assert(cmd.reason == Right(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight))) - assert(nodeParams.db.payments.getIncomingPayment(pr.paymentHash).get.status === IncomingPaymentStatus.Pending) + assert(nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).get.status === IncomingPaymentStatus.Pending) } test("PaymentHandler should reject incoming multi-part payment with an unknown payment hash") { f => import f._ sender.send(handlerWithMpp, ReceivePayment(Some(1000 msat), Left("multi-part unknown payment hash"))) - val pr = sender.expectMsgType[PaymentRequest] - assert(pr.features.allowMultiPart) + val invoice = sender.expectMsgType[Invoice] + assert(invoice.features.hasFeature(Features.BasicMultiPartPayment)) - val add = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, pr.paymentHash.reverse, defaultExpiry, TestConstants.emptyOnionPacket) - sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createMultiPartPayload(add.amountMsat, 1000 msat, add.cltvExpiry, pr.paymentSecret.get, pr.paymentMetadata))) + val add = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, invoice.paymentHash.reverse, defaultExpiry, TestConstants.emptyOnionPacket) + sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createMultiPartPayload(add.amountMsat, 1000 msat, add.cltvExpiry, invoice.paymentSecret.get, invoice.paymentMetadata))) val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message assert(cmd.reason == Right(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight))) - assert(nodeParams.db.payments.getIncomingPayment(pr.paymentHash).get.status === IncomingPaymentStatus.Pending) + assert(nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).get.status === IncomingPaymentStatus.Pending) } test("PaymentHandler should reject incoming multi-part payment with a total amount too low") { f => import f._ sender.send(handlerWithMpp, ReceivePayment(Some(1000 msat), Left("multi-part total amount too low"))) - val pr = sender.expectMsgType[PaymentRequest] - assert(pr.features.allowMultiPart) + val invoice = sender.expectMsgType[Invoice] + assert(invoice.features.hasFeature(Features.BasicMultiPartPayment)) - val add = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, pr.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket) - sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createMultiPartPayload(add.amountMsat, 999 msat, add.cltvExpiry, pr.paymentSecret.get, pr.paymentMetadata))) + val add = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, invoice.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket) + sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createMultiPartPayload(add.amountMsat, 999 msat, add.cltvExpiry, invoice.paymentSecret.get, invoice.paymentMetadata))) val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message assert(cmd.reason == Right(IncorrectOrUnknownPaymentDetails(999 msat, nodeParams.currentBlockHeight))) - assert(nodeParams.db.payments.getIncomingPayment(pr.paymentHash).get.status === IncomingPaymentStatus.Pending) + assert(nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).get.status === IncomingPaymentStatus.Pending) } test("PaymentHandler should reject incoming multi-part payment with a total amount too high") { f => import f._ sender.send(handlerWithMpp, ReceivePayment(Some(1000 msat), Left("multi-part total amount too low"))) - val pr = sender.expectMsgType[PaymentRequest] - assert(pr.features.allowMultiPart) + val invoice = sender.expectMsgType[Invoice] + assert(invoice.features.hasFeature(Features.BasicMultiPartPayment)) - val add = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, pr.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket) - sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createMultiPartPayload(add.amountMsat, 2001 msat, add.cltvExpiry, pr.paymentSecret.get, pr.paymentMetadata))) + val add = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, invoice.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket) + sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createMultiPartPayload(add.amountMsat, 2001 msat, add.cltvExpiry, invoice.paymentSecret.get, invoice.paymentMetadata))) val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message assert(cmd.reason == Right(IncorrectOrUnknownPaymentDetails(2001 msat, nodeParams.currentBlockHeight))) - assert(nodeParams.db.payments.getIncomingPayment(pr.paymentHash).get.status === IncomingPaymentStatus.Pending) + assert(nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).get.status === IncomingPaymentStatus.Pending) } test("PaymentHandler should reject incoming multi-part payment with an invalid payment secret") { f => import f._ sender.send(handlerWithMpp, ReceivePayment(Some(1000 msat), Left("multi-part invalid payment secret"))) - val pr = sender.expectMsgType[PaymentRequest] - assert(pr.features.allowMultiPart) + val invoice = sender.expectMsgType[Invoice] + assert(invoice.features.hasFeature(Features.BasicMultiPartPayment)) // Invalid payment secret. - val add = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, pr.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket) - sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createMultiPartPayload(add.amountMsat, 1000 msat, add.cltvExpiry, pr.paymentSecret.get.reverse, pr.paymentMetadata))) + val add = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, invoice.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket) + sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createMultiPartPayload(add.amountMsat, 1000 msat, add.cltvExpiry, invoice.paymentSecret.get.reverse, invoice.paymentMetadata))) val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message assert(cmd.reason == Right(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight))) - assert(nodeParams.db.payments.getIncomingPayment(pr.paymentHash).get.status === IncomingPaymentStatus.Pending) + assert(nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).get.status === IncomingPaymentStatus.Pending) } test("PaymentHandler should handle multi-part payment timeout") { f => @@ -356,13 +356,13 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike // Partial payment missing additional parts. f.sender.send(handler, ReceivePayment(Some(1000 msat), Left("1 slow coffee"))) - val pr1 = f.sender.expectMsgType[PaymentRequest] + val pr1 = f.sender.expectMsgType[Invoice] val add1 = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, pr1.paymentHash, f.defaultExpiry, TestConstants.emptyOnionPacket) f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add1, PaymentOnion.createMultiPartPayload(add1.amountMsat, 1000 msat, add1.cltvExpiry, pr1.paymentSecret.get, pr1.paymentMetadata))) // Partial payment exceeding the invoice amount, but incomplete because it promises to overpay. f.sender.send(handler, ReceivePayment(Some(1500 msat), Left("1 slow latte"))) - val pr2 = f.sender.expectMsgType[PaymentRequest] + val pr2 = f.sender.expectMsgType[Invoice] val add2 = UpdateAddHtlc(ByteVector32.One, 1, 1600 msat, pr2.paymentHash, f.defaultExpiry, TestConstants.emptyOnionPacket) f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add2, PaymentOnion.createMultiPartPayload(add2.amountMsat, 2000 msat, add2.cltvExpiry, pr2.paymentSecret.get, pr2.paymentMetadata))) @@ -396,15 +396,15 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike val preimage = randomBytes32() f.sender.send(handler, ReceivePayment(Some(1000 msat), Left("1 fast coffee"), paymentPreimage_opt = Some(preimage))) - val pr = f.sender.expectMsgType[PaymentRequest] + val invoice = f.sender.expectMsgType[Invoice] - val add1 = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, pr.paymentHash, f.defaultExpiry, TestConstants.emptyOnionPacket) - f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add1, PaymentOnion.createMultiPartPayload(add1.amountMsat, 1000 msat, add1.cltvExpiry, pr.paymentSecret.get, pr.paymentMetadata))) + val add1 = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, invoice.paymentHash, f.defaultExpiry, TestConstants.emptyOnionPacket) + f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add1, PaymentOnion.createMultiPartPayload(add1.amountMsat, 1000 msat, add1.cltvExpiry, invoice.paymentSecret.get, invoice.paymentMetadata))) // Invalid payment secret -> should be rejected. - val add2 = UpdateAddHtlc(ByteVector32.Zeroes, 42, 200 msat, pr.paymentHash, f.defaultExpiry, TestConstants.emptyOnionPacket) - f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add2, PaymentOnion.createMultiPartPayload(add2.amountMsat, 1000 msat, add2.cltvExpiry, pr.paymentSecret.get.reverse, pr.paymentMetadata))) + val add2 = UpdateAddHtlc(ByteVector32.Zeroes, 42, 200 msat, invoice.paymentHash, f.defaultExpiry, TestConstants.emptyOnionPacket) + f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add2, PaymentOnion.createMultiPartPayload(add2.amountMsat, 1000 msat, add2.cltvExpiry, invoice.paymentSecret.get.reverse, invoice.paymentMetadata))) val add3 = add2.copy(id = 43) - f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add3, PaymentOnion.createMultiPartPayload(add3.amountMsat, 1000 msat, add3.cltvExpiry, pr.paymentSecret.get, pr.paymentMetadata))) + f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add3, PaymentOnion.createMultiPartPayload(add3.amountMsat, 1000 msat, add3.cltvExpiry, invoice.paymentSecret.get, invoice.paymentMetadata))) f.register.expectMsgAllOf( Register.Forward(ActorRef.noSender, add2.channelId, CMD_FAIL_HTLC(add2.id, Right(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight)), commit = true)), @@ -414,7 +414,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike val paymentReceived = f.eventListener.expectMsgType[PaymentReceived] assert(paymentReceived.parts.map(_.copy(timestamp = 0 unixms)).toSet === Set(PartialPayment(800 msat, ByteVector32.One, 0 unixms), PartialPayment(200 msat, ByteVector32.Zeroes, 0 unixms))) - val received = nodeParams.db.payments.getIncomingPayment(pr.paymentHash) + val received = nodeParams.db.payments.getIncomingPayment(invoice.paymentHash) assert(received.isDefined && received.get.status.isInstanceOf[IncomingPaymentStatus.Received]) assert(received.get.status.asInstanceOf[IncomingPaymentStatus.Received].amount === 1000.msat) awaitCond({ @@ -423,10 +423,10 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike }) // Extraneous HTLCs should be fulfilled. - f.sender.send(handler, MultiPartPaymentFSM.ExtraPaymentReceived(pr.paymentHash, HtlcPart(1000 msat, UpdateAddHtlc(ByteVector32.One, 44, 200 msat, pr.paymentHash, add1.cltvExpiry, add1.onionRoutingPacket)), None)) + f.sender.send(handler, MultiPartPaymentFSM.ExtraPaymentReceived(invoice.paymentHash, HtlcPart(1000 msat, UpdateAddHtlc(ByteVector32.One, 44, 200 msat, invoice.paymentHash, add1.cltvExpiry, add1.onionRoutingPacket)), None)) f.register.expectMsg(Register.Forward(ActorRef.noSender, ByteVector32.One, CMD_FULFILL_HTLC(44, preimage, commit = true))) assert(f.eventListener.expectMsgType[PaymentReceived].amount === 200.msat) - val received2 = nodeParams.db.payments.getIncomingPayment(pr.paymentHash) + val received2 = nodeParams.db.payments.getIncomingPayment(invoice.paymentHash) assert(received2.get.status.asInstanceOf[IncomingPaymentStatus.Received].amount === 1200.msat) f.sender.send(handler, GetPendingPayments) @@ -439,22 +439,22 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike val preimage = randomBytes32() f.sender.send(handler, ReceivePayment(Some(1000 msat), Left("1 coffee, no sugar"), paymentPreimage_opt = Some(preimage))) - val pr = f.sender.expectMsgType[PaymentRequest] - assert(pr.features.allowMultiPart) - assert(pr.paymentHash == Crypto.sha256(preimage)) + val invoice = f.sender.expectMsgType[Invoice] + assert(invoice.features.hasFeature(Features.BasicMultiPartPayment)) + assert(invoice.paymentHash == Crypto.sha256(preimage)) - val add1 = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, pr.paymentHash, f.defaultExpiry, TestConstants.emptyOnionPacket) - f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add1, PaymentOnion.createMultiPartPayload(add1.amountMsat, 1000 msat, add1.cltvExpiry, pr.paymentSecret.get, pr.paymentMetadata))) + val add1 = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, invoice.paymentHash, f.defaultExpiry, TestConstants.emptyOnionPacket) + f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add1, PaymentOnion.createMultiPartPayload(add1.amountMsat, 1000 msat, add1.cltvExpiry, invoice.paymentSecret.get, invoice.paymentMetadata))) f.register.expectMsg(Register.Forward(ActorRef.noSender, ByteVector32.One, CMD_FAIL_HTLC(0, Right(PaymentTimeout), commit = true))) awaitCond({ f.sender.send(handler, GetPendingPayments) f.sender.expectMsgType[PendingPayments].paymentHashes.isEmpty }) - val add2 = UpdateAddHtlc(ByteVector32.One, 2, 300 msat, pr.paymentHash, f.defaultExpiry, TestConstants.emptyOnionPacket) - f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add2, PaymentOnion.createMultiPartPayload(add2.amountMsat, 1000 msat, add2.cltvExpiry, pr.paymentSecret.get, pr.paymentMetadata))) - val add3 = UpdateAddHtlc(ByteVector32.Zeroes, 5, 700 msat, pr.paymentHash, f.defaultExpiry, TestConstants.emptyOnionPacket) - f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add3, PaymentOnion.createMultiPartPayload(add3.amountMsat, 1000 msat, add3.cltvExpiry, pr.paymentSecret.get, pr.paymentMetadata))) + val add2 = UpdateAddHtlc(ByteVector32.One, 2, 300 msat, invoice.paymentHash, f.defaultExpiry, TestConstants.emptyOnionPacket) + f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add2, PaymentOnion.createMultiPartPayload(add2.amountMsat, 1000 msat, add2.cltvExpiry, invoice.paymentSecret.get, invoice.paymentMetadata))) + val add3 = UpdateAddHtlc(ByteVector32.Zeroes, 5, 700 msat, invoice.paymentHash, f.defaultExpiry, TestConstants.emptyOnionPacket) + f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add3, PaymentOnion.createMultiPartPayload(add3.amountMsat, 1000 msat, add3.cltvExpiry, invoice.paymentSecret.get, invoice.paymentMetadata))) // the fulfill are not necessarily in the same order as the commands f.register.expectMsgAllOf( @@ -463,9 +463,9 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike ) val paymentReceived = f.eventListener.expectMsgType[PaymentReceived] - assert(paymentReceived.paymentHash === pr.paymentHash) + assert(paymentReceived.paymentHash === invoice.paymentHash) assert(paymentReceived.parts.map(_.copy(timestamp = 0 unixms)).toSet === Set(PartialPayment(300 msat, ByteVector32.One, 0 unixms), PartialPayment(700 msat, ByteVector32.Zeroes, 0 unixms))) - val received = nodeParams.db.payments.getIncomingPayment(pr.paymentHash) + val received = nodeParams.db.payments.getIncomingPayment(invoice.paymentHash) assert(received.isDefined && received.get.status.isInstanceOf[IncomingPaymentStatus.Received]) assert(received.get.status.asInstanceOf[IncomingPaymentStatus.Received].amount === 1000.msat) awaitCond({ @@ -514,7 +514,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike assert(nodeParams.db.payments.getIncomingPayment(paymentHash) === None) } - test("PaymentHandler should reject incoming payments if the payment request doesn't exist") { f => + test("PaymentHandler should reject incoming payments if the invoice doesn't exist") { f => import f._ val paymentHash = randomBytes32() @@ -528,7 +528,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike assert(cmd.reason === Right(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight))) } - test("PaymentHandler should reject incoming multi-part payment if the payment request doesn't exist") { f => + test("PaymentHandler should reject incoming multi-part payment if the invoice doesn't exist") { f => import f._ val paymentHash = randomBytes32() @@ -542,7 +542,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike assert(cmd.reason === Right(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight))) } - test("PaymentHandler should fail fulfilling incoming payments if the payment request doesn't exist") { f => + test("PaymentHandler should fail fulfilling incoming payments if the invoice doesn't exist") { f => import f._ val paymentPreimage = randomBytes32() diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartPaymentLifecycleSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartPaymentLifecycleSpec.scala index f04524ea93..af0348de9f 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartPaymentLifecycleSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartPaymentLifecycleSpec.scala @@ -23,8 +23,8 @@ import fr.acinq.eclair._ import fr.acinq.eclair.channel.{ChannelUnavailable, HtlcsTimedoutDownstream, RemoteCannotAffordFeesForNewHtlc} import fr.acinq.eclair.crypto.Sphinx import fr.acinq.eclair.db.{FailureSummary, FailureType, OutgoingPaymentStatus} +import fr.acinq.eclair.payment.Bolt11Invoice.ExtraHop import fr.acinq.eclair.payment.OutgoingPaymentPacket.Upstream -import fr.acinq.eclair.payment.PaymentRequest.ExtraHop import fr.acinq.eclair.payment.relay.Relayer.RelayFees import fr.acinq.eclair.payment.send.MultiPartPaymentLifecycle._ import fr.acinq.eclair.payment.send.PaymentError.RetryExhausted diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentInitiatorSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentInitiatorSpec.scala index 3b082ff9ec..da85a58469 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentInitiatorSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentInitiatorSpec.scala @@ -24,9 +24,9 @@ import fr.acinq.eclair.Features._ import fr.acinq.eclair.UInt64.Conversions._ import fr.acinq.eclair.channel.Channel import fr.acinq.eclair.crypto.Sphinx +import fr.acinq.eclair.payment.Bolt11Invoice.ExtraHop import fr.acinq.eclair.payment.OutgoingPaymentPacket.Upstream import fr.acinq.eclair.payment.PaymentPacketSpec._ -import fr.acinq.eclair.payment.PaymentRequest.{ExtraHop, PaymentRequestFeatures} import fr.acinq.eclair.payment.PaymentSent.PartialPayment import fr.acinq.eclair.payment.send.MultiPartPaymentLifecycle.SendMultiPartPayment import fr.acinq.eclair.payment.send.PaymentError.UnsupportedFeatures @@ -37,7 +37,7 @@ import fr.acinq.eclair.router.Router._ import fr.acinq.eclair.wire.protocol.OnionPaymentPayloadTlv.{AmountToForward, KeySend, OutgoingCltv} import fr.acinq.eclair.wire.protocol.PaymentOnion.FinalTlvPayload import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{CltvExpiryDelta, Feature, FeatureSupport, Features, InvoiceFeature, MilliSatoshiLong, NodeParams, TestConstants, TestKitBaseClass, TimestampSecond, randomBytes32, randomKey} +import fr.acinq.eclair.{CltvExpiryDelta, Features, MilliSatoshiLong, NodeParams, TestConstants, TestKitBaseClass, TimestampSecond, randomBytes32, randomKey} import org.scalatest.funsuite.FixtureAnyFunSuiteLike import org.scalatest.{Outcome, Tag} import scodec.bits.{BinStringSyntax, ByteVector, HexStringSyntax} @@ -53,18 +53,18 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike case class FixtureParam(nodeParams: NodeParams, initiator: TestActorRef[PaymentInitiator], payFsm: TestProbe, multiPartPayFsm: TestProbe, sender: TestProbe, eventListener: TestProbe) - val featuresWithoutMpp: Map[Feature with InvoiceFeature, FeatureSupport] = Map( + val featuresWithoutMpp: Features = Features( VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory ) - val featuresWithMpp: Map[Feature with InvoiceFeature, FeatureSupport] = Map( + val featuresWithMpp: Features = Features( VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory, BasicMultiPartPayment -> Optional, ) - val featuresWithTrampoline: Map[Feature with InvoiceFeature, FeatureSupport] = Map( + val featuresWithTrampoline: Features = Features( VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory, BasicMultiPartPayment -> Optional, @@ -86,7 +86,7 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike override def withFixture(test: OneArgTest): Outcome = { val features = if (test.tags.contains("mpp_disabled")) featuresWithoutMpp else featuresWithMpp - val nodeParams = TestConstants.Alice.nodeParams.copy(features = Features(features.collect { case (f, s) => (f: Feature, s) })) + val nodeParams = TestConstants.Alice.nodeParams.copy(features = features) val (sender, payFsm, multiPartPayFsm) = (TestProbe(), TestProbe(), TestProbe()) val eventListener = TestProbe() system.eventStream.subscribe(eventListener.ref, classOf[PaymentEvent]) @@ -97,8 +97,8 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike test("forward payment with user custom tlv records") { f => import f._ val customRecords = Seq(GenericTlv(500L, hex"01020304"), GenericTlv(501L, hex"d34db33f")) - val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, None, paymentHash, priv_c.privateKey, Left("test"), Channel.MIN_CLTV_EXPIRY_DELTA) - val req = SendPaymentToNode(finalAmount, pr, 1, Channel.MIN_CLTV_EXPIRY_DELTA, userCustomTlvs = customRecords, routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams) + val invoice = Bolt11Invoice(Block.LivenetGenesisBlock.hash, None, paymentHash, priv_c.privateKey, Left("test"), Channel.MIN_CLTV_EXPIRY_DELTA) + val req = SendPaymentToNode(finalAmount, invoice, 1, Channel.MIN_CLTV_EXPIRY_DELTA, userCustomTlvs = customRecords, routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams) sender.send(initiator, req) sender.expectMsgType[UUID] payFsm.expectMsgType[SendPaymentConfig] @@ -124,20 +124,20 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike test("reject payment with unknown mandatory feature") { f => import f._ val taggedFields = List( - PaymentRequest.PaymentHash(paymentHash), - PaymentRequest.Description("Some invoice"), - PaymentRequest.PaymentSecret(randomBytes32()), - PaymentRequest.Expiry(3600), - PaymentRequest.PaymentRequestFeatures(bin"000001000000000000000000000000000100000100000000") // feature 42 + Bolt11Invoice.PaymentHash(paymentHash), + Bolt11Invoice.Description("Some invoice"), + Bolt11Invoice.PaymentSecret(randomBytes32()), + Bolt11Invoice.Expiry(3600), + Bolt11Invoice.InvoiceFeatures(Features(bin"000001000000000000000000000000000100000100000000")) // feature 42 ) - val pr = PaymentRequest("lnbc", Some(finalAmount), TimestampSecond.now(), randomKey().publicKey, taggedFields, ByteVector.empty) - val req = SendPaymentToNode(finalAmount + 100.msat, pr, 1, CltvExpiryDelta(42), routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams) + val invoice = Bolt11Invoice("lnbc", Some(finalAmount), TimestampSecond.now(), randomKey().publicKey, taggedFields, ByteVector.empty) + val req = SendPaymentToNode(finalAmount + 100.msat, invoice, 1, CltvExpiryDelta(42), routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams) sender.send(initiator, req) val id = sender.expectMsgType[UUID] val fail = sender.expectMsgType[PaymentFailed] assert(fail.id === id) assert(fail.failures.head.isInstanceOf[LocalFailure]) - assert(fail.failures.head.asInstanceOf[LocalFailure].t === UnsupportedFeatures(pr.features.features)) + assert(fail.failures.head.asInstanceOf[LocalFailure].t === UnsupportedFeatures(invoice.features)) } test("forward payment with pre-defined route") { f => @@ -145,45 +145,45 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike // we prioritize the invoice's finalExpiryDelta over the one from SendPaymentToRouteRequest val ignoredFinalExpiryDelta = CltvExpiryDelta(18) val finalExpiryDelta = CltvExpiryDelta(36) - val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, priv_c.privateKey, Left("Some invoice"), finalExpiryDelta) + val invoice = Bolt11Invoice(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, priv_c.privateKey, Left("Some invoice"), finalExpiryDelta) val route = PredefinedNodeRoute(Seq(a, b, c)) - val request = SendPaymentToRoute(finalAmount, finalAmount, pr, ignoredFinalExpiryDelta, route, None, None, None, 0 msat, CltvExpiryDelta(0), Nil) + val request = SendPaymentToRoute(finalAmount, finalAmount, invoice, ignoredFinalExpiryDelta, route, None, None, None, 0 msat, CltvExpiryDelta(0), Nil) sender.send(initiator, request) val payment = sender.expectMsgType[SendPaymentToRouteResponse] - payFsm.expectMsg(SendPaymentConfig(payment.paymentId, payment.parentId, None, paymentHash, finalAmount, c, Upstream.Local(payment.paymentId), Some(pr), storeInDb = true, publishEvent = true, recordPathFindingMetrics = false, Nil)) - payFsm.expectMsg(PaymentLifecycle.SendPaymentToRoute(initiator, Left(route), PaymentOnion.createSinglePartPayload(finalAmount, finalExpiryDelta.toCltvExpiry(nodeParams.currentBlockHeight + 1), pr.paymentSecret.get, pr.paymentMetadata))) + payFsm.expectMsg(SendPaymentConfig(payment.paymentId, payment.parentId, None, paymentHash, finalAmount, c, Upstream.Local(payment.paymentId), Some(invoice), storeInDb = true, publishEvent = true, recordPathFindingMetrics = false, Nil)) + payFsm.expectMsg(PaymentLifecycle.SendPaymentToRoute(initiator, Left(route), PaymentOnion.createSinglePartPayload(finalAmount, finalExpiryDelta.toCltvExpiry(nodeParams.currentBlockHeight + 1), invoice.paymentSecret.get, invoice.paymentMetadata))) sender.send(initiator, GetPayment(Left(payment.paymentId))) - sender.expectMsg(PaymentIsPending(payment.paymentId, pr.paymentHash, PendingPaymentToRoute(sender.ref, request))) - sender.send(initiator, GetPayment(Right(pr.paymentHash))) - sender.expectMsg(PaymentIsPending(payment.paymentId, pr.paymentHash, PendingPaymentToRoute(sender.ref, request))) + sender.expectMsg(PaymentIsPending(payment.paymentId, invoice.paymentHash, PendingPaymentToRoute(sender.ref, request))) + sender.send(initiator, GetPayment(Right(invoice.paymentHash))) + sender.expectMsg(PaymentIsPending(payment.paymentId, invoice.paymentHash, PendingPaymentToRoute(sender.ref, request))) - val pf = PaymentFailed(payment.paymentId, pr.paymentHash, Nil) + val pf = PaymentFailed(payment.paymentId, invoice.paymentHash, Nil) payFsm.send(initiator, pf) sender.expectMsg(pf) eventListener.expectNoMessage(100 millis) - sender.send(initiator, GetPayment(Right(pr.paymentHash))) - sender.expectMsg(NoPendingPayment(Right(pr.paymentHash))) + sender.send(initiator, GetPayment(Right(invoice.paymentHash))) + sender.expectMsg(NoPendingPayment(Right(invoice.paymentHash))) } test("forward single-part payment when multi-part deactivated", Tag("mpp_disabled")) { f => import f._ val finalExpiryDelta = CltvExpiryDelta(24) - val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, priv_c.privateKey, Left("Some MPP invoice"), finalExpiryDelta, features = PaymentRequestFeatures(featuresWithMpp)) - val req = SendPaymentToNode(finalAmount, pr, 1, /* ignored since the invoice provides it */ CltvExpiryDelta(12), routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams) + val invoice = Bolt11Invoice(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, priv_c.privateKey, Left("Some MPP invoice"), finalExpiryDelta, features = featuresWithMpp) + val req = SendPaymentToNode(finalAmount, invoice, 1, /* ignored since the invoice provides it */ CltvExpiryDelta(12), routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams) assert(req.finalExpiry(nodeParams.currentBlockHeight) === (finalExpiryDelta + 1).toCltvExpiry(nodeParams.currentBlockHeight)) sender.send(initiator, req) val id = sender.expectMsgType[UUID] - payFsm.expectMsg(SendPaymentConfig(id, id, None, paymentHash, finalAmount, c, Upstream.Local(id), Some(pr), storeInDb = true, publishEvent = true, recordPathFindingMetrics = true, Nil)) - payFsm.expectMsg(PaymentLifecycle.SendPaymentToNode(initiator, c, FinalTlvPayload(TlvStream(OnionPaymentPayloadTlv.AmountToForward(finalAmount), OnionPaymentPayloadTlv.OutgoingCltv(req.finalExpiry(nodeParams.currentBlockHeight)), OnionPaymentPayloadTlv.PaymentData(pr.paymentSecret.get, finalAmount))), 1, routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams)) + payFsm.expectMsg(SendPaymentConfig(id, id, None, paymentHash, finalAmount, c, Upstream.Local(id), Some(invoice), storeInDb = true, publishEvent = true, recordPathFindingMetrics = true, Nil)) + payFsm.expectMsg(PaymentLifecycle.SendPaymentToNode(initiator, c, FinalTlvPayload(TlvStream(OnionPaymentPayloadTlv.AmountToForward(finalAmount), OnionPaymentPayloadTlv.OutgoingCltv(req.finalExpiry(nodeParams.currentBlockHeight)), OnionPaymentPayloadTlv.PaymentData(invoice.paymentSecret.get, finalAmount))), 1, routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams)) sender.send(initiator, GetPayment(Left(id))) - sender.expectMsg(PaymentIsPending(id, pr.paymentHash, PendingPaymentToNode(sender.ref, req))) - sender.send(initiator, GetPayment(Right(pr.paymentHash))) - sender.expectMsg(PaymentIsPending(id, pr.paymentHash, PendingPaymentToNode(sender.ref, req))) + sender.expectMsg(PaymentIsPending(id, invoice.paymentHash, PendingPaymentToNode(sender.ref, req))) + sender.send(initiator, GetPayment(Right(invoice.paymentHash))) + sender.expectMsg(PaymentIsPending(id, invoice.paymentHash, PendingPaymentToNode(sender.ref, req))) - val pf = PaymentFailed(id, pr.paymentHash, Nil) + val pf = PaymentFailed(id, invoice.paymentHash, Nil) payFsm.send(initiator, pf) sender.expectMsg(pf) eventListener.expectNoMessage(100 millis) @@ -194,19 +194,19 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike test("forward multi-part payment") { f => import f._ - val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, priv_c.privateKey, Left("Some invoice"), CltvExpiryDelta(18), features = PaymentRequestFeatures(featuresWithMpp)) - val req = SendPaymentToNode(finalAmount + 100.msat, pr, 1, CltvExpiryDelta(42), routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams) + val invoice = Bolt11Invoice(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, priv_c.privateKey, Left("Some invoice"), CltvExpiryDelta(18), features = featuresWithMpp) + val req = SendPaymentToNode(finalAmount + 100.msat, invoice, 1, CltvExpiryDelta(42), routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams) sender.send(initiator, req) val id = sender.expectMsgType[UUID] - multiPartPayFsm.expectMsg(SendPaymentConfig(id, id, None, paymentHash, finalAmount + 100.msat, c, Upstream.Local(id), Some(pr), storeInDb = true, publishEvent = true, recordPathFindingMetrics = true, Nil)) - multiPartPayFsm.expectMsg(SendMultiPartPayment(initiator, pr.paymentSecret.get, c, finalAmount + 100.msat, req.finalExpiry(nodeParams.currentBlockHeight), 1, pr.paymentMetadata, routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams)) + multiPartPayFsm.expectMsg(SendPaymentConfig(id, id, None, paymentHash, finalAmount + 100.msat, c, Upstream.Local(id), Some(invoice), storeInDb = true, publishEvent = true, recordPathFindingMetrics = true, Nil)) + multiPartPayFsm.expectMsg(SendMultiPartPayment(initiator, invoice.paymentSecret.get, c, finalAmount + 100.msat, req.finalExpiry(nodeParams.currentBlockHeight), 1, invoice.paymentMetadata, routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams)) sender.send(initiator, GetPayment(Left(id))) - sender.expectMsg(PaymentIsPending(id, pr.paymentHash, PendingPaymentToNode(sender.ref, req))) - sender.send(initiator, GetPayment(Right(pr.paymentHash))) - sender.expectMsg(PaymentIsPending(id, pr.paymentHash, PendingPaymentToNode(sender.ref, req))) + sender.expectMsg(PaymentIsPending(id, invoice.paymentHash, PendingPaymentToNode(sender.ref, req))) + sender.send(initiator, GetPayment(Right(invoice.paymentHash))) + sender.expectMsg(PaymentIsPending(id, invoice.paymentHash, PendingPaymentToNode(sender.ref, req))) - val ps = PaymentSent(id, pr.paymentHash, randomBytes32(), finalAmount, priv_c.publicKey, Seq(PartialPayment(UUID.randomUUID(), finalAmount, 0 msat, randomBytes32(), None))) + val ps = PaymentSent(id, invoice.paymentHash, randomBytes32(), finalAmount, priv_c.publicKey, Seq(PartialPayment(UUID.randomUUID(), finalAmount, 0 msat, randomBytes32(), None))) payFsm.send(initiator, ps) sender.expectMsg(ps) eventListener.expectNoMessage(100 millis) @@ -217,51 +217,51 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike test("forward multi-part payment with pre-defined route") { f => import f._ - val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, priv_c.privateKey, Left("Some invoice"), CltvExpiryDelta(18), features = PaymentRequestFeatures(featuresWithMpp)) + val invoice = Bolt11Invoice(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, priv_c.privateKey, Left("Some invoice"), CltvExpiryDelta(18), features = featuresWithMpp) val route = PredefinedChannelRoute(c, Seq(channelUpdate_ab.shortChannelId, channelUpdate_bc.shortChannelId)) - val req = SendPaymentToRoute(finalAmount / 2, finalAmount, pr, Channel.MIN_CLTV_EXPIRY_DELTA, route, None, None, None, 0 msat, CltvExpiryDelta(0), Nil) + val req = SendPaymentToRoute(finalAmount / 2, finalAmount, invoice, Channel.MIN_CLTV_EXPIRY_DELTA, route, None, None, None, 0 msat, CltvExpiryDelta(0), Nil) sender.send(initiator, req) val payment = sender.expectMsgType[SendPaymentToRouteResponse] - payFsm.expectMsg(SendPaymentConfig(payment.paymentId, payment.parentId, None, paymentHash, finalAmount, c, Upstream.Local(payment.paymentId), Some(pr), storeInDb = true, publishEvent = true, recordPathFindingMetrics = false, Nil)) + payFsm.expectMsg(SendPaymentConfig(payment.paymentId, payment.parentId, None, paymentHash, finalAmount, c, Upstream.Local(payment.paymentId), Some(invoice), storeInDb = true, publishEvent = true, recordPathFindingMetrics = false, Nil)) val msg = payFsm.expectMsgType[PaymentLifecycle.SendPaymentToRoute] assert(msg.replyTo === initiator) assert(msg.route === Left(route)) assert(msg.finalPayload.amount === finalAmount / 2) assert(msg.finalPayload.expiry === req.finalExpiry(nodeParams.currentBlockHeight)) - assert(msg.finalPayload.paymentSecret === pr.paymentSecret.get) + assert(msg.finalPayload.paymentSecret === invoice.paymentSecret.get) assert(msg.finalPayload.totalAmount === finalAmount) sender.send(initiator, GetPayment(Left(payment.paymentId))) - sender.expectMsg(PaymentIsPending(payment.paymentId, pr.paymentHash, PendingPaymentToRoute(sender.ref, req))) - sender.send(initiator, GetPayment(Right(pr.paymentHash))) - sender.expectMsg(PaymentIsPending(payment.paymentId, pr.paymentHash, PendingPaymentToRoute(sender.ref, req))) + sender.expectMsg(PaymentIsPending(payment.paymentId, invoice.paymentHash, PendingPaymentToRoute(sender.ref, req))) + sender.send(initiator, GetPayment(Right(invoice.paymentHash))) + sender.expectMsg(PaymentIsPending(payment.paymentId, invoice.paymentHash, PendingPaymentToRoute(sender.ref, req))) - val pf = PaymentFailed(payment.paymentId, pr.paymentHash, Nil) + val pf = PaymentFailed(payment.paymentId, invoice.paymentHash, Nil) payFsm.send(initiator, pf) sender.expectMsg(pf) eventListener.expectNoMessage(100 millis) - sender.send(initiator, GetPayment(Right(pr.paymentHash))) - sender.expectMsg(NoPendingPayment(Right(pr.paymentHash))) + sender.send(initiator, GetPayment(Right(invoice.paymentHash))) + sender.expectMsg(NoPendingPayment(Right(invoice.paymentHash))) } test("forward trampoline payment") { f => import f._ val ignoredRoutingHints = List(List(ExtraHop(b, channelUpdate_bc.shortChannelId, feeBase = 10 msat, feeProportionalMillionths = 1, cltvExpiryDelta = CltvExpiryDelta(12)))) - val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, priv_c.privateKey, Left("Some phoenix invoice"), CltvExpiryDelta(9), features = PaymentRequestFeatures(featuresWithTrampoline), extraHops = ignoredRoutingHints) + val invoice = Bolt11Invoice(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, priv_c.privateKey, Left("Some phoenix invoice"), CltvExpiryDelta(9), features = featuresWithTrampoline, extraHops = ignoredRoutingHints) val trampolineFees = 21000 msat - val req = SendTrampolinePayment(finalAmount, pr, b, Seq((trampolineFees, CltvExpiryDelta(12))), /* ignored since the invoice provides it */ CltvExpiryDelta(18), routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams) + val req = SendTrampolinePayment(finalAmount, invoice, b, Seq((trampolineFees, CltvExpiryDelta(12))), /* ignored since the invoice provides it */ CltvExpiryDelta(18), routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams) sender.send(initiator, req) val id = sender.expectMsgType[UUID] multiPartPayFsm.expectMsgType[SendPaymentConfig] sender.send(initiator, GetPayment(Left(id))) - sender.expectMsg(PaymentIsPending(id, pr.paymentHash, PendingTrampolinePayment(sender.ref, Nil, req))) - sender.send(initiator, GetPayment(Right(pr.paymentHash))) - sender.expectMsg(PaymentIsPending(id, pr.paymentHash, PendingTrampolinePayment(sender.ref, Nil, req))) + sender.expectMsg(PaymentIsPending(id, invoice.paymentHash, PendingTrampolinePayment(sender.ref, Nil, req))) + sender.send(initiator, GetPayment(Right(invoice.paymentHash))) + sender.expectMsg(PaymentIsPending(id, invoice.paymentHash, PendingTrampolinePayment(sender.ref, Nil, req))) val msg = multiPartPayFsm.expectMsgType[SendMultiPartPayment] - assert(msg.paymentSecret !== pr.paymentSecret.get) // we should not leak the invoice secret to the trampoline node + assert(msg.paymentSecret !== invoice.paymentSecret.get) // we should not leak the invoice secret to the trampoline node assert(msg.targetNodeId === b) assert(msg.targetExpiry.toLong === currentBlockCount + 9 + 12 + 1) assert(msg.totalAmount === finalAmount + trampolineFees) @@ -270,7 +270,7 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike // Verify that the trampoline node can correctly peel the trampoline onion. val trampolineOnion = msg.additionalTlvs.head.asInstanceOf[OnionPaymentPayloadTlv.TrampolineOnion].packet - val Right(decrypted) = Sphinx.peel(priv_b.privateKey, Some(pr.paymentHash), trampolineOnion) + val Right(decrypted) = Sphinx.peel(priv_b.privateKey, Some(invoice.paymentHash), trampolineOnion) assert(!decrypted.isLastPacket) val trampolinePayload = PaymentOnionCodecs.nodeRelayPerHopPayloadCodec.decode(decrypted.payload.bits).require.value assert(trampolinePayload.amountToForward === finalAmount) @@ -282,26 +282,26 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike assert(trampolinePayload.invoiceFeatures === None) // Verify that the recipient can correctly peel the trampoline onion. - val Right(decrypted1) = Sphinx.peel(priv_c.privateKey, Some(pr.paymentHash), decrypted.nextPacket) + val Right(decrypted1) = Sphinx.peel(priv_c.privateKey, Some(invoice.paymentHash), decrypted.nextPacket) assert(decrypted1.isLastPacket) val finalPayload = PaymentOnionCodecs.finalPerHopPayloadCodec.decode(decrypted1.payload.bits).require.value assert(finalPayload.amount === finalAmount) assert(finalPayload.totalAmount === finalAmount) assert(finalPayload.expiry.toLong === currentBlockCount + 9 + 1) - assert(finalPayload.paymentSecret === pr.paymentSecret.get) + assert(finalPayload.paymentSecret === invoice.paymentSecret.get) } test("forward trampoline to legacy payment") { f => import f._ - val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, priv_c.privateKey, Left("Some eclair-mobile invoice"), CltvExpiryDelta(9)) + val invoice = Bolt11Invoice(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, priv_c.privateKey, Left("Some eclair-mobile invoice"), CltvExpiryDelta(9)) val trampolineFees = 21000 msat - val req = SendTrampolinePayment(finalAmount, pr, b, Seq((trampolineFees, CltvExpiryDelta(12))), routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams) + val req = SendTrampolinePayment(finalAmount, invoice, b, Seq((trampolineFees, CltvExpiryDelta(12))), routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams) sender.send(initiator, req) sender.expectMsgType[UUID] multiPartPayFsm.expectMsgType[SendPaymentConfig] val msg = multiPartPayFsm.expectMsgType[SendMultiPartPayment] - assert(msg.paymentSecret !== pr.paymentSecret.get) // we should not leak the invoice secret to the trampoline node + assert(msg.paymentSecret !== invoice.paymentSecret.get) // we should not leak the invoice secret to the trampoline node assert(msg.targetNodeId === b) assert(msg.targetExpiry.toLong === currentBlockCount + 9 + 12 + 1) assert(msg.totalAmount === finalAmount + trampolineFees) @@ -309,24 +309,24 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike // Verify that the trampoline node can correctly peel the trampoline onion. val trampolineOnion = msg.additionalTlvs.head.asInstanceOf[OnionPaymentPayloadTlv.TrampolineOnion].packet - val Right(decrypted) = Sphinx.peel(priv_b.privateKey, Some(pr.paymentHash), trampolineOnion) + val Right(decrypted) = Sphinx.peel(priv_b.privateKey, Some(invoice.paymentHash), trampolineOnion) assert(!decrypted.isLastPacket) val trampolinePayload = PaymentOnionCodecs.nodeRelayPerHopPayloadCodec.decode(decrypted.payload.bits).require.value assert(trampolinePayload.amountToForward === finalAmount) assert(trampolinePayload.totalAmount === finalAmount) assert(trampolinePayload.outgoingCltv.toLong === currentBlockCount + 9 + 1) assert(trampolinePayload.outgoingNodeId === c) - assert(trampolinePayload.paymentSecret === pr.paymentSecret) + assert(trampolinePayload.paymentSecret === invoice.paymentSecret) assert(trampolinePayload.invoiceFeatures === Some(hex"4100")) // var_onion_optin, payment_secret } test("reject trampoline to legacy payment for 0-value invoice") { f => import f._ // This is disabled because it would let the trampoline node steal the whole payment (if malicious). - val routingHints = List(List(PaymentRequest.ExtraHop(b, channelUpdate_bc.shortChannelId, 10 msat, 100, CltvExpiryDelta(144)))) - val pr = PaymentRequest(Block.RegtestGenesisBlock.hash, None, paymentHash, priv_a.privateKey, Left("#abittooreckless"), CltvExpiryDelta(18), None, None, routingHints, features = PaymentRequestFeatures(featuresWithMpp)) + val routingHints = List(List(Bolt11Invoice.ExtraHop(b, channelUpdate_bc.shortChannelId, 10 msat, 100, CltvExpiryDelta(144)))) + val invoice = Bolt11Invoice(Block.RegtestGenesisBlock.hash, None, paymentHash, priv_a.privateKey, Left("#abittooreckless"), CltvExpiryDelta(18), None, None, routingHints, features = featuresWithMpp) val trampolineFees = 21000 msat - val req = SendTrampolinePayment(finalAmount, pr, b, Seq((trampolineFees, CltvExpiryDelta(12))), CltvExpiryDelta(9), routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams) + val req = SendTrampolinePayment(finalAmount, invoice, b, Seq((trampolineFees, CltvExpiryDelta(12))), CltvExpiryDelta(9), routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams) sender.send(initiator, req) val id = sender.expectMsgType[UUID] val fail = sender.expectMsgType[PaymentFailed] @@ -340,9 +340,9 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike test("reject trampoline payment with onion too big") { f => import f._ val paymentMetadata = ByteVector.fromValidHex("01" * 400) - val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, priv_c.privateKey, Left("Much payment very metadata"), CltvExpiryDelta(9), features = PaymentRequestFeatures(featuresWithTrampoline), paymentMetadata = Some(paymentMetadata)) + val invoice = Bolt11Invoice(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, priv_c.privateKey, Left("Much payment very metadata"), CltvExpiryDelta(9), features = featuresWithTrampoline, paymentMetadata = Some(paymentMetadata)) val trampolineFees = 21000 msat - val req = SendTrampolinePayment(finalAmount, pr, b, Seq((trampolineFees, CltvExpiryDelta(12))), CltvExpiryDelta(18), routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams) + val req = SendTrampolinePayment(finalAmount, invoice, b, Seq((trampolineFees, CltvExpiryDelta(12))), CltvExpiryDelta(18), routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams) sender.send(initiator, req) val id = sender.expectMsgType[UUID] val fail = sender.expectMsgType[PaymentFailed] @@ -353,9 +353,9 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike test("retry trampoline payment") { f => import f._ - val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, priv_c.privateKey, Left("Some phoenix invoice"), CltvExpiryDelta(18), features = PaymentRequestFeatures(featuresWithTrampoline)) + val invoice = Bolt11Invoice(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, priv_c.privateKey, Left("Some phoenix invoice"), CltvExpiryDelta(18), features = featuresWithTrampoline) val trampolineAttempts = (21000 msat, CltvExpiryDelta(12)) :: (25000 msat, CltvExpiryDelta(24)) :: Nil - val req = SendTrampolinePayment(finalAmount, pr, b, trampolineAttempts, CltvExpiryDelta(9), routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams) + val req = SendTrampolinePayment(finalAmount, invoice, b, trampolineAttempts, CltvExpiryDelta(9), routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams) sender.send(initiator, req) val id = sender.expectMsgType[UUID] val cfg = multiPartPayFsm.expectMsgType[SendPaymentConfig] @@ -369,13 +369,13 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike sender.expectMsgType[PaymentIsPending] // Simulate a failure which should trigger a retry. - multiPartPayFsm.send(initiator, PaymentFailed(cfg.parentId, pr.paymentHash, Seq(RemoteFailure(msg1.totalAmount, Nil, Sphinx.DecryptedFailurePacket(b, TrampolineFeeInsufficient))))) + multiPartPayFsm.send(initiator, PaymentFailed(cfg.parentId, invoice.paymentHash, Seq(RemoteFailure(msg1.totalAmount, Nil, Sphinx.DecryptedFailurePacket(b, TrampolineFeeInsufficient))))) multiPartPayFsm.expectMsgType[SendPaymentConfig] val msg2 = multiPartPayFsm.expectMsgType[SendMultiPartPayment] assert(msg2.totalAmount === finalAmount + 25000.msat) // Simulate success which should publish the event and respond to the original sender. - val success = PaymentSent(cfg.parentId, pr.paymentHash, randomBytes32(), finalAmount, c, Seq(PaymentSent.PartialPayment(UUID.randomUUID(), 1000 msat, 500 msat, randomBytes32(), None))) + val success = PaymentSent(cfg.parentId, invoice.paymentHash, randomBytes32(), finalAmount, c, Seq(PaymentSent.PartialPayment(UUID.randomUUID(), 1000 msat, 500 msat, randomBytes32(), None))) multiPartPayFsm.send(initiator, success) sender.expectMsg(success) eventListener.expectMsg(success) @@ -388,9 +388,9 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike test("retry trampoline payment and fail") { f => import f._ - val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, priv_c.privateKey, Left("Some phoenix invoice"), CltvExpiryDelta(18), features = PaymentRequestFeatures(featuresWithTrampoline)) + val invoice = Bolt11Invoice(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, priv_c.privateKey, Left("Some phoenix invoice"), CltvExpiryDelta(18), features = featuresWithTrampoline) val trampolineAttempts = (21000 msat, CltvExpiryDelta(12)) :: (25000 msat, CltvExpiryDelta(24)) :: Nil - val req = SendTrampolinePayment(finalAmount, pr, b, trampolineAttempts, CltvExpiryDelta(9), routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams) + val req = SendTrampolinePayment(finalAmount, invoice, b, trampolineAttempts, CltvExpiryDelta(9), routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams) sender.send(initiator, req) sender.expectMsgType[UUID] val cfg = multiPartPayFsm.expectMsgType[SendPaymentConfig] @@ -401,13 +401,13 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike assert(msg1.totalAmount === finalAmount + 21000.msat) // Simulate a failure which should trigger a retry. - multiPartPayFsm.send(initiator, PaymentFailed(cfg.parentId, pr.paymentHash, Seq(RemoteFailure(msg1.totalAmount, Nil, Sphinx.DecryptedFailurePacket(b, TrampolineFeeInsufficient))))) + multiPartPayFsm.send(initiator, PaymentFailed(cfg.parentId, invoice.paymentHash, Seq(RemoteFailure(msg1.totalAmount, Nil, Sphinx.DecryptedFailurePacket(b, TrampolineFeeInsufficient))))) multiPartPayFsm.expectMsgType[SendPaymentConfig] val msg2 = multiPartPayFsm.expectMsgType[SendMultiPartPayment] assert(msg2.totalAmount === finalAmount + 25000.msat) // Simulate a failure that exhausts payment attempts. - val failed = PaymentFailed(cfg.parentId, pr.paymentHash, Seq(RemoteFailure(msg2.totalAmount, Nil, Sphinx.DecryptedFailurePacket(b, TemporaryNodeFailure)))) + val failed = PaymentFailed(cfg.parentId, invoice.paymentHash, Seq(RemoteFailure(msg2.totalAmount, Nil, Sphinx.DecryptedFailurePacket(b, TemporaryNodeFailure)))) multiPartPayFsm.send(initiator, failed) sender.expectMsg(failed) eventListener.expectMsg(failed) @@ -417,9 +417,9 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike test("retry trampoline payment and fail (route not found)") { f => import f._ - val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, priv_c.privateKey, Left("Some phoenix invoice"), CltvExpiryDelta(18), features = PaymentRequestFeatures(featuresWithTrampoline)) + val invoice = Bolt11Invoice(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, priv_c.privateKey, Left("Some phoenix invoice"), CltvExpiryDelta(18), features = featuresWithTrampoline) val trampolineAttempts = (21000 msat, CltvExpiryDelta(12)) :: (25000 msat, CltvExpiryDelta(24)) :: Nil - val req = SendTrampolinePayment(finalAmount, pr, b, trampolineAttempts, CltvExpiryDelta(9), routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams) + val req = SendTrampolinePayment(finalAmount, invoice, b, trampolineAttempts, CltvExpiryDelta(9), routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams) sender.send(initiator, req) sender.expectMsgType[UUID] @@ -427,7 +427,7 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike val msg1 = multiPartPayFsm.expectMsgType[SendMultiPartPayment] assert(msg1.totalAmount === finalAmount + 21000.msat) // Trampoline node couldn't find a route for the given fee. - val failed = PaymentFailed(cfg.parentId, pr.paymentHash, Seq(RemoteFailure(msg1.totalAmount, Nil, Sphinx.DecryptedFailurePacket(b, TrampolineFeeInsufficient)))) + val failed = PaymentFailed(cfg.parentId, invoice.paymentHash, Seq(RemoteFailure(msg1.totalAmount, Nil, Sphinx.DecryptedFailurePacket(b, TrampolineFeeInsufficient)))) multiPartPayFsm.send(initiator, failed) multiPartPayFsm.expectMsgType[SendPaymentConfig] val msg2 = multiPartPayFsm.expectMsgType[SendMultiPartPayment] @@ -444,14 +444,14 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike test("forward trampoline payment with pre-defined route") { f => import f._ - val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, priv_c.privateKey, Left("Some invoice"), CltvExpiryDelta(18)) + val invoice = Bolt11Invoice(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, priv_c.privateKey, Left("Some invoice"), CltvExpiryDelta(18)) val trampolineFees = 100 msat val route = PredefinedNodeRoute(Seq(a, b)) - val req = SendPaymentToRoute(finalAmount + trampolineFees, finalAmount, pr, Channel.MIN_CLTV_EXPIRY_DELTA, route, None, None, None, trampolineFees, CltvExpiryDelta(144), Seq(b, c)) + val req = SendPaymentToRoute(finalAmount + trampolineFees, finalAmount, invoice, Channel.MIN_CLTV_EXPIRY_DELTA, route, None, None, None, trampolineFees, CltvExpiryDelta(144), Seq(b, c)) sender.send(initiator, req) val payment = sender.expectMsgType[SendPaymentToRouteResponse] assert(payment.trampolineSecret.nonEmpty) - payFsm.expectMsg(SendPaymentConfig(payment.paymentId, payment.parentId, None, paymentHash, finalAmount, c, Upstream.Local(payment.paymentId), Some(pr), storeInDb = true, publishEvent = true, recordPathFindingMetrics = false, Seq(NodeHop(b, c, CltvExpiryDelta(0), 0 msat)))) + payFsm.expectMsg(SendPaymentConfig(payment.paymentId, payment.parentId, None, paymentHash, finalAmount, c, Upstream.Local(payment.paymentId), Some(invoice), storeInDb = true, publishEvent = true, recordPathFindingMetrics = false, Seq(NodeHop(b, c, CltvExpiryDelta(0), 0 msat)))) val msg = payFsm.expectMsgType[PaymentLifecycle.SendPaymentToRoute] assert(msg.route === Left(route)) assert(msg.finalPayload.amount === finalAmount + trampolineFees) @@ -462,13 +462,13 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike assert(trampolineOnion.nonEmpty) // Verify that the trampoline node can correctly peel the trampoline onion. - val Right(decrypted) = Sphinx.peel(priv_b.privateKey, Some(pr.paymentHash), trampolineOnion.get.packet) + val Right(decrypted) = Sphinx.peel(priv_b.privateKey, Some(invoice.paymentHash), trampolineOnion.get.packet) assert(!decrypted.isLastPacket) val trampolinePayload = PaymentOnionCodecs.nodeRelayPerHopPayloadCodec.decode(decrypted.payload.bits).require.value assert(trampolinePayload.amountToForward === finalAmount) assert(trampolinePayload.totalAmount === finalAmount) assert(trampolinePayload.outgoingNodeId === c) - assert(trampolinePayload.paymentSecret === pr.paymentSecret) + assert(trampolinePayload.paymentSecret === invoice.paymentSecret) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala index 824ae6da70..d2dabd413e 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala @@ -29,8 +29,8 @@ import fr.acinq.eclair.channel._ import fr.acinq.eclair.crypto.Sphinx import fr.acinq.eclair.db.{OutgoingPayment, OutgoingPaymentStatus, PaymentType} import fr.acinq.eclair.io.Peer.PeerRoutingMessage +import fr.acinq.eclair.payment.Bolt11Invoice.ExtraHop import fr.acinq.eclair.payment.OutgoingPaymentPacket.Upstream -import fr.acinq.eclair.payment.PaymentRequest.ExtraHop import fr.acinq.eclair.payment.PaymentSent.PartialPayment import fr.acinq.eclair.payment.relay.Relayer.RelayFees import fr.acinq.eclair.payment.send.PaymentInitiator.SendPaymentConfig @@ -61,7 +61,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val defaultPaymentHash = Crypto.sha256(defaultPaymentPreimage) val defaultOrigin = Origin.LocalCold(UUID.randomUUID()) val defaultExternalId = UUID.randomUUID().toString - val defaultInvoice = PaymentRequest(Block.RegtestGenesisBlock.hash, None, defaultPaymentHash, priv_d, Left("test"), Channel.MIN_CLTV_EXPIRY_DELTA) + val defaultInvoice = Bolt11Invoice(Block.RegtestGenesisBlock.hash, None, defaultPaymentHash, priv_d, Left("test"), Channel.MIN_CLTV_EXPIRY_DELTA) val defaultRouteParams = TestConstants.Alice.nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams def defaultRouteRequest(source: PublicKey, target: PublicKey, cfg: SendPaymentConfig): RouteRequest = RouteRequest(source, target, defaultAmountMsat, defaultMaxFee, paymentContext = Some(cfg.paymentContext), routeParams = defaultRouteParams) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentPacketSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentPacketSpec.scala index 91127ac598..9dd45c6aa7 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentPacketSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentPacketSpec.scala @@ -26,13 +26,12 @@ import fr.acinq.eclair.channel._ import fr.acinq.eclair.crypto.Sphinx import fr.acinq.eclair.payment.IncomingPaymentPacket.{ChannelRelayPacket, FinalPacket, NodeRelayPacket, decrypt} import fr.acinq.eclair.payment.OutgoingPaymentPacket._ -import fr.acinq.eclair.payment.PaymentRequest.PaymentRequestFeatures import fr.acinq.eclair.router.Router.{ChannelHop, NodeHop} import fr.acinq.eclair.transactions.Transactions.InputInfo import fr.acinq.eclair.wire.protocol.OnionPaymentPayloadTlv.{AmountToForward, OutgoingCltv, PaymentData} import fr.acinq.eclair.wire.protocol.PaymentOnion.{ChannelRelayTlvPayload, FinalTlvPayload} import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, Feature, FeatureSupport, InvoiceFeature, MilliSatoshi, MilliSatoshiLong, ShortChannelId, TestConstants, TimestampSecondLong, nodeFee, randomBytes32, randomKey} +import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, Features, MilliSatoshi, MilliSatoshiLong, ShortChannelId, TestConstants, TimestampSecondLong, nodeFee, randomBytes32, randomKey} import org.scalatest.BeforeAndAfterAll import org.scalatest.funsuite.AnyFunSuite import scodec.Attempt @@ -212,9 +211,9 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { // / \ // a -> b -> c d -> e - val routingHints = List(List(PaymentRequest.ExtraHop(randomKey().publicKey, ShortChannelId(42), 10 msat, 100, CltvExpiryDelta(144)))) - val invoiceFeatures = PaymentRequestFeatures(Map[Feature with InvoiceFeature, FeatureSupport](VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory, BasicMultiPartPayment -> Optional)) - val invoice = PaymentRequest(Block.RegtestGenesisBlock.hash, Some(finalAmount), paymentHash, priv_a.privateKey, Left("#reckless"), CltvExpiryDelta(18), None, None, routingHints, features = invoiceFeatures, paymentMetadata = Some(hex"010203")) + val routingHints = List(List(Bolt11Invoice.ExtraHop(randomKey().publicKey, ShortChannelId(42), 10 msat, 100, CltvExpiryDelta(144)))) + val invoiceFeatures = Features(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory, BasicMultiPartPayment -> Optional) + val invoice = Bolt11Invoice(Block.RegtestGenesisBlock.hash, Some(finalAmount), paymentHash, priv_a.privateKey, Left("#reckless"), CltvExpiryDelta(18), None, None, routingHints, features = invoiceFeatures, paymentMetadata = Some(hex"010203")) val Success((amount_ac, expiry_ac, trampolineOnion)) = buildTrampolineToLegacyPacket(invoice, trampolineHops, PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, invoice.paymentSecret.get, None)) assert(amount_ac === amount_bc) assert(expiry_ac === expiry_bc) @@ -260,8 +259,8 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { } test("fail to build a trampoline payment when too much invoice data is provided") { - val routingHintOverflow = List(List.fill(7)(PaymentRequest.ExtraHop(randomKey().publicKey, ShortChannelId(1), 10 msat, 100, CltvExpiryDelta(12)))) - val invoice = PaymentRequest(Block.RegtestGenesisBlock.hash, Some(finalAmount), paymentHash, priv_a.privateKey, Left("#reckless"), CltvExpiryDelta(18), None, None, routingHintOverflow) + val routingHintOverflow = List(List.fill(7)(Bolt11Invoice.ExtraHop(randomKey().publicKey, ShortChannelId(1), 10 msat, 100, CltvExpiryDelta(12)))) + val invoice = Bolt11Invoice(Block.RegtestGenesisBlock.hash, Some(finalAmount), paymentHash, priv_a.privateKey, Left("#reckless"), CltvExpiryDelta(18), None, None, routingHintOverflow) assert(buildTrampolineToLegacyPacket(invoice, trampolineHops, PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, invoice.paymentSecret.get, invoice.paymentMetadata)).isFailure) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PostRestartHtlcCleanerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PostRestartHtlcCleanerSpec.scala index e2dac443f5..f16d3353ee 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PostRestartHtlcCleanerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PostRestartHtlcCleanerSpec.scala @@ -152,7 +152,7 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit val preimage = randomBytes32() val paymentHash = Crypto.sha256(preimage) - val invoice = PaymentRequest(Block.TestnetGenesisBlock.hash, Some(500 msat), paymentHash, TestConstants.Bob.nodeKeyManager.nodeKey.privateKey, Left("Some invoice"), CltvExpiryDelta(18)) + val invoice = Bolt11Invoice(Block.TestnetGenesisBlock.hash, Some(500 msat), paymentHash, TestConstants.Bob.nodeKeyManager.nodeKey.privateKey, Left("Some invoice"), CltvExpiryDelta(18)) nodeParams.db.payments.addIncomingPayment(invoice, preimage) nodeParams.db.payments.receiveIncomingPayment(paymentHash, 5000 msat) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/NodeRelayerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/NodeRelayerSpec.scala index b80b16f0b7..dc2ef84c07 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/NodeRelayerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/NodeRelayerSpec.scala @@ -28,8 +28,8 @@ import fr.acinq.eclair.FeatureSupport.{Mandatory, Optional} import fr.acinq.eclair.Features.{BasicMultiPartPayment, PaymentSecret, VariableLengthOnion} import fr.acinq.eclair.channel.{CMD_FAIL_HTLC, CMD_FULFILL_HTLC, Register} import fr.acinq.eclair.crypto.Sphinx +import fr.acinq.eclair.payment.Bolt11Invoice.ExtraHop import fr.acinq.eclair.payment.OutgoingPaymentPacket.Upstream -import fr.acinq.eclair.payment.PaymentRequest.{ExtraHop, PaymentRequestFeatures} import fr.acinq.eclair.payment._ import fr.acinq.eclair.payment.relay.NodeRelayer.PaymentKey import fr.acinq.eclair.payment.send.MultiPartPaymentLifecycle.{PreimageReceived, SendMultiPartPayment} @@ -38,7 +38,7 @@ import fr.acinq.eclair.payment.send.PaymentLifecycle.SendPaymentToNode import fr.acinq.eclair.router.Router.RouteRequest import fr.acinq.eclair.router.{BalanceTooLow, RouteNotFound} import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, Feature, FeatureSupport, InvoiceFeature, MilliSatoshi, MilliSatoshiLong, NodeParams, ShortChannelId, TestConstants, UInt64, randomBytes, randomBytes32, randomKey} +import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, Features, MilliSatoshi, MilliSatoshiLong, NodeParams, ShortChannelId, TestConstants, UInt64, randomBytes, randomBytes32, randomKey} import org.scalatest.Outcome import org.scalatest.funsuite.FixtureAnyFunSuiteLike import scodec.bits.HexStringSyntax @@ -567,10 +567,10 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl // Receive an upstream multi-part payment. val hints = List(List(ExtraHop(outgoingNodeId, ShortChannelId(42), feeBase = 10 msat, feeProportionalMillionths = 1, cltvExpiryDelta = CltvExpiryDelta(12)))) - val features = PaymentRequestFeatures(Map[Feature with InvoiceFeature, FeatureSupport](VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory, BasicMultiPartPayment -> Optional)) - val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(outgoingAmount * 3), paymentHash, randomKey(), Left("Some invoice"), CltvExpiryDelta(18), extraHops = hints, paymentMetadata = Some(hex"123456"), features = features) + val features = Features(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory, BasicMultiPartPayment -> Optional) + val invoice = Bolt11Invoice(Block.LivenetGenesisBlock.hash, Some(outgoingAmount * 3), paymentHash, randomKey(), Left("Some invoice"), CltvExpiryDelta(18), extraHops = hints, paymentMetadata = Some(hex"123456"), features = features) val incomingPayments = incomingMultiPart.map(incoming => incoming.copy(innerPayload = PaymentOnion.createNodeRelayToNonTrampolinePayload( - incoming.innerPayload.amountToForward, outgoingAmount * 3, outgoingExpiry, outgoingNodeId, pr + incoming.innerPayload.amountToForward, outgoingAmount * 3, outgoingExpiry, outgoingNodeId, invoice ))) val (nodeRelayer, parent) = f.createNodeRelay(incomingPayments.head) incomingPayments.foreach(incoming => nodeRelayer ! NodeRelay.Relay(incoming)) @@ -578,8 +578,8 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl val outgoingCfg = mockPayFSM.expectMessageType[SendPaymentConfig] validateOutgoingCfg(outgoingCfg, Upstream.Trampoline(incomingMultiPart.map(_.add))) val outgoingPayment = mockPayFSM.expectMessageType[SendMultiPartPayment] - assert(outgoingPayment.paymentSecret === pr.paymentSecret.get) // we should use the provided secret - assert(outgoingPayment.paymentMetadata === pr.paymentMetadata) // we should use the provided metadata + assert(outgoingPayment.paymentSecret === invoice.paymentSecret.get) // we should use the provided secret + assert(outgoingPayment.paymentMetadata === invoice.paymentMetadata) // we should use the provided metadata assert(outgoingPayment.totalAmount === outgoingAmount) assert(outgoingPayment.targetExpiry === outgoingExpiry) assert(outgoingPayment.targetNodeId === outgoingNodeId) @@ -609,10 +609,10 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl // Receive an upstream multi-part payment. val hints = List(List(ExtraHop(outgoingNodeId, ShortChannelId(42), feeBase = 10 msat, feeProportionalMillionths = 1, cltvExpiryDelta = CltvExpiryDelta(12)))) - val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(outgoingAmount), paymentHash, randomKey(), Left("Some invoice"), CltvExpiryDelta(18), extraHops = hints, paymentMetadata = Some(hex"123456")) - assert(!pr.features.allowMultiPart) + val invoice = Bolt11Invoice(Block.LivenetGenesisBlock.hash, Some(outgoingAmount), paymentHash, randomKey(), Left("Some invoice"), CltvExpiryDelta(18), extraHops = hints, paymentMetadata = Some(hex"123456")) + assert(!invoice.features.hasFeature(BasicMultiPartPayment)) val incomingPayments = incomingMultiPart.map(incoming => incoming.copy(innerPayload = PaymentOnion.createNodeRelayToNonTrampolinePayload( - incoming.innerPayload.amountToForward, incoming.innerPayload.amountToForward, outgoingExpiry, outgoingNodeId, pr + incoming.innerPayload.amountToForward, incoming.innerPayload.amountToForward, outgoingExpiry, outgoingNodeId, invoice ))) val (nodeRelayer, parent) = f.createNodeRelay(incomingPayments.head) incomingPayments.foreach(incoming => nodeRelayer ! NodeRelay.Relay(incoming)) @@ -622,7 +622,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl val outgoingPayment = mockPayFSM.expectMessageType[SendPaymentToNode] assert(outgoingPayment.finalPayload.amount === outgoingAmount) assert(outgoingPayment.finalPayload.expiry === outgoingExpiry) - assert(outgoingPayment.finalPayload.paymentMetadata === pr.paymentMetadata) // we should use the provided metadata + assert(outgoingPayment.finalPayload.paymentMetadata === invoice.paymentMetadata) // we should use the provided metadata assert(outgoingPayment.targetNodeId === outgoingNodeId) assert(outgoingPayment.assistedRoutes === hints) // those are adapters for pay-fsm messages @@ -648,9 +648,9 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl import f._ // Receive an upstream multi-part payment. - val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(outgoingAmount), paymentHash, randomKey(), Left("Some invoice"), CltvExpiryDelta(18)) + val invoice = Bolt11Invoice(Block.LivenetGenesisBlock.hash, Some(outgoingAmount), paymentHash, randomKey(), Left("Some invoice"), CltvExpiryDelta(18)) val incomingPayments = incomingMultiPart.map(incoming => { - val innerPayload = PaymentOnion.createNodeRelayToNonTrampolinePayload(incoming.innerPayload.amountToForward, incoming.innerPayload.amountToForward, outgoingExpiry, outgoingNodeId, pr) + val innerPayload = PaymentOnion.createNodeRelayToNonTrampolinePayload(incoming.innerPayload.amountToForward, incoming.innerPayload.amountToForward, outgoingExpiry, outgoingNodeId, invoice) val invalidPayload = innerPayload.copy(records = TlvStream(innerPayload.records.records.collect { case r if !r.isInstanceOf[OnionPaymentPayloadTlv.PaymentData] => r })) // we remove the payment secret incoming.copy(innerPayload = invalidPayload) }) @@ -668,7 +668,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl assert(!outgoingCfg.publishEvent) assert(!outgoingCfg.storeInDb) assert(outgoingCfg.paymentHash === paymentHash) - assert(outgoingCfg.paymentRequest === None) + assert(outgoingCfg.invoice === None) assert(outgoingCfg.recipientAmount === outgoingAmount) assert(outgoingCfg.recipientNodeId === outgoingNodeId) assert(outgoingCfg.upstream === upstream) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/RouteCalculationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/RouteCalculationSpec.scala index a8f990e8a5..a343a66168 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/RouteCalculationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/RouteCalculationSpec.scala @@ -19,7 +19,7 @@ package fr.acinq.eclair.router import com.softwaremill.quicklens.ModifyPimp import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.bitcoin.{Block, ByteVector32, ByteVector64, Satoshi, SatoshiLong} -import fr.acinq.eclair.payment.PaymentRequest.ExtraHop +import fr.acinq.eclair.payment.Bolt11Invoice.ExtraHop import fr.acinq.eclair.payment.relay.Relayer.RelayFees import fr.acinq.eclair.router.Graph.GraphStructure.DirectedGraph.graphEdgeToHop import fr.acinq.eclair.router.Graph.GraphStructure.{DirectedGraph, GraphEdge} diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/RouterSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/RouterSpec.scala index 146a347a44..2f585b9386 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/RouterSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/RouterSpec.scala @@ -26,7 +26,7 @@ import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._ import fr.acinq.eclair.channel.{AvailableBalanceChanged, CommitmentsSpec, LocalChannelUpdate} import fr.acinq.eclair.crypto.TransportHandler import fr.acinq.eclair.io.Peer.PeerRoutingMessage -import fr.acinq.eclair.payment.PaymentRequest.ExtraHop +import fr.acinq.eclair.payment.Bolt11Invoice.ExtraHop import fr.acinq.eclair.router.Announcements.{makeChannelUpdate, makeNodeAnnouncement} import fr.acinq.eclair.router.BaseRouterSpec.channelAnnouncement import fr.acinq.eclair.router.RouteCalculationSpec.{DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, DEFAULT_ROUTE_PARAMS} diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecsSpec.scala index dd9442351a..fa2653d400 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecsSpec.scala @@ -419,7 +419,7 @@ class LightningMessageCodecsSpec extends AnyFunSuite { new FailureMessageSerializer + new NodeAddressSerializer + new DirectionSerializer + - new PaymentRequestSerializer + + new InvoiceSerializer + ShortTypeHints(List( classOf[QueryChannelRange], classOf[ReplyChannelRange], diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/PaymentOnionSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/PaymentOnionSpec.scala index 98cbb4b660..353b892adb 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/PaymentOnionSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/PaymentOnionSpec.scala @@ -19,7 +19,7 @@ package fr.acinq.eclair.wire.protocol import fr.acinq.bitcoin.ByteVector32 import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.eclair.UInt64.Conversions._ -import fr.acinq.eclair.payment.PaymentRequest.ExtraHop +import fr.acinq.eclair.payment.Bolt11Invoice.ExtraHop import fr.acinq.eclair.wire.protocol.OnionPaymentPayloadTlv._ import fr.acinq.eclair.wire.protocol.OnionRoutingCodecs.MissingRequiredTlv import fr.acinq.eclair.wire.protocol.PaymentOnion._ diff --git a/eclair-node/src/main/scala/fr/acinq/eclair/api/directives/ExtraDirectives.scala b/eclair-node/src/main/scala/fr/acinq/eclair/api/directives/ExtraDirectives.scala index 4af2e3c6f8..b56baf6f93 100644 --- a/eclair-node/src/main/scala/fr/acinq/eclair/api/directives/ExtraDirectives.scala +++ b/eclair-node/src/main/scala/fr/acinq/eclair/api/directives/ExtraDirectives.scala @@ -26,8 +26,8 @@ import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.eclair.ApiTypes.ChannelIdentifier import fr.acinq.eclair.api.serde.FormParamExtractors._ import fr.acinq.eclair.api.serde.JsonSupport._ -import fr.acinq.eclair.payment.PaymentRequest -import fr.acinq.eclair.{MilliSatoshi, ShortChannelId, TimestampSecond, TimestampSecondLong} +import fr.acinq.eclair.payment.Bolt11Invoice +import fr.acinq.eclair.{MilliSatoshi, ShortChannelId, TimestampSecond} import scala.concurrent.Future import scala.util.{Failure, Success} @@ -45,7 +45,7 @@ trait ExtraDirectives extends Directives { val fromFormParam: NameDefaultUnmarshallerReceptacle[TimestampSecond] = "from".as[TimestampSecond](timestampSecondUnmarshaller).?(TimestampSecond.min) val toFormParam: NameDefaultUnmarshallerReceptacle[TimestampSecond] = "to".as[TimestampSecond](timestampSecondUnmarshaller).?(TimestampSecond.max) val amountMsatFormParam: NameReceptacle[MilliSatoshi] = "amountMsat".as[MilliSatoshi] - val invoiceFormParam: NameReceptacle[PaymentRequest] = "invoice".as[PaymentRequest] + val invoiceFormParam: NameReceptacle[Bolt11Invoice] = "invoice".as[Bolt11Invoice] val routeFormatFormParam: NameUnmarshallerReceptacle[RouteFormat] = "format".as[RouteFormat](routeFormatUnmarshaller) val ignoreNodeIdsFormParam: NameUnmarshallerReceptacle[List[PublicKey]] = "ignoreNodeIds".as[List[PublicKey]](pubkeyListUnmarshaller) val ignoreShortChannelIdsFormParam: NameUnmarshallerReceptacle[List[ShortChannelId]] = "ignoreShortChannelIds".as[List[ShortChannelId]](shortChannelIdsUnmarshaller) diff --git a/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/PathFinding.scala b/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/PathFinding.scala index 48cc596681..f1c230c947 100644 --- a/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/PathFinding.scala +++ b/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/PathFinding.scala @@ -21,7 +21,7 @@ import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.eclair.api.Service import fr.acinq.eclair.api.directives.{EclairDirectives, RouteFormat} import fr.acinq.eclair.api.serde.FormParamExtractors._ -import fr.acinq.eclair.payment.PaymentRequest +import fr.acinq.eclair.payment.Bolt11Invoice import scala.concurrent.ExecutionContext @@ -34,7 +34,7 @@ trait PathFinding { val findRoute: Route = postRequest("findroute") { implicit t => formFields(invoiceFormParam, amountMsatFormParam.?, "pathFindingExperimentName".?, routeFormatFormParam.?, "includeLocalChannelCost".as[Boolean].?, ignoreNodeIdsFormParam.?, ignoreShortChannelIdsFormParam.?, maxFeeMsatFormParam.?) { - case (invoice@PaymentRequest(_, Some(amount), _, nodeId, _, _), None, pathFindingExperimentName_opt, routeFormat_opt, includeLocalChannelCost_opt, ignoreNodeIds_opt, ignoreChannels_opt, maxFee_opt) => + case (invoice@Bolt11Invoice(_, Some(amount), _, nodeId, _, _), None, pathFindingExperimentName_opt, routeFormat_opt, includeLocalChannelCost_opt, ignoreNodeIds_opt, ignoreChannels_opt, maxFee_opt) => complete(eclairApi.findRoute(nodeId, amount, pathFindingExperimentName_opt, invoice.routingInfo, includeLocalChannelCost_opt.getOrElse(false), ignoreNodeIds = ignoreNodeIds_opt.getOrElse(Nil), ignoreShortChannelIds = ignoreChannels_opt.getOrElse(Nil), maxFee_opt = maxFee_opt).map(r => RouteFormat.format(r, routeFormat_opt))) case (invoice, Some(overrideAmount), pathFindingExperimentName_opt, routeFormat_opt, includeLocalChannelCost_opt, ignoreNodeIds_opt, ignoreChannels_opt, maxFee_opt) => complete(eclairApi.findRoute(invoice.nodeId, overrideAmount, pathFindingExperimentName_opt, invoice.routingInfo, includeLocalChannelCost_opt.getOrElse(false), ignoreNodeIds = ignoreNodeIds_opt.getOrElse(Nil), ignoreShortChannelIds = ignoreChannels_opt.getOrElse(Nil), maxFee_opt = maxFee_opt).map(r => RouteFormat.format(r, routeFormat_opt))) diff --git a/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Payment.scala b/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Payment.scala index 67f341ec71..68058f17e6 100644 --- a/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Payment.scala +++ b/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Payment.scala @@ -22,7 +22,7 @@ import fr.acinq.bitcoin.{ByteVector32, Satoshi} import fr.acinq.eclair.api.Service import fr.acinq.eclair.api.directives.EclairDirectives import fr.acinq.eclair.api.serde.FormParamExtractors.{pubkeyListUnmarshaller, _} -import fr.acinq.eclair.payment.PaymentRequest +import fr.acinq.eclair.payment.Bolt11Invoice import fr.acinq.eclair.router.Router.{PredefinedChannelRoute, PredefinedNodeRoute} import fr.acinq.eclair.{CltvExpiryDelta, MilliSatoshi, randomBytes32} @@ -39,7 +39,7 @@ trait Payment { val payInvoice: Route = postRequest("payinvoice") { implicit t => formFields(invoiceFormParam, amountMsatFormParam.?, "maxAttempts".as[Int].?, "maxFeeFlatSat".as[Satoshi].?, "maxFeePct".as[Double].?, "externalId".?, "blocking".as[Boolean].?, "pathFindingExperimentName".?) { - case (invoice@PaymentRequest(_, Some(amount), _, nodeId, _, _), None, maxAttempts, maxFeeFlat_opt, maxFeePct_opt, externalId_opt, blocking_opt, pathFindingExperimentName_opt) => + case (invoice@Bolt11Invoice(_, Some(amount), _, nodeId, _, _), None, maxAttempts, maxFeeFlat_opt, maxFeePct_opt, externalId_opt, blocking_opt, pathFindingExperimentName_opt) => blocking_opt match { case Some(true) => complete(eclairApi.sendBlocking(externalId_opt, amount, invoice, maxAttempts, maxFeeFlat_opt, maxFeePct_opt, pathFindingExperimentName_opt)) case _ => complete(eclairApi.send(externalId_opt, amount, invoice, maxAttempts, maxFeeFlat_opt, maxFeePct_opt, pathFindingExperimentName_opt)) diff --git a/eclair-node/src/main/scala/fr/acinq/eclair/api/serde/FormParamExtractors.scala b/eclair-node/src/main/scala/fr/acinq/eclair/api/serde/FormParamExtractors.scala index 7606a92277..8096b3b3a1 100644 --- a/eclair-node/src/main/scala/fr/acinq/eclair/api/serde/FormParamExtractors.scala +++ b/eclair-node/src/main/scala/fr/acinq/eclair/api/serde/FormParamExtractors.scala @@ -25,7 +25,7 @@ import fr.acinq.eclair.api.serde.JsonSupport._ import fr.acinq.eclair.blockchain.fee.FeeratePerByte import fr.acinq.eclair.crypto.Sphinx import fr.acinq.eclair.io.NodeURI -import fr.acinq.eclair.payment.PaymentRequest +import fr.acinq.eclair.payment.Bolt11Invoice import fr.acinq.eclair.wire.protocol.MessageOnionCodecs.blindedRouteCodec import fr.acinq.eclair.{MilliSatoshi, ShortChannelId, TimestampSecond} import scodec.bits.ByteVector @@ -44,7 +44,7 @@ object FormParamExtractors { implicit val sha256HashesUnmarshaller: Unmarshaller[String, List[ByteVector32]] = listUnmarshaller(bin => ByteVector32.fromValidHex(bin)) - implicit val bolt11Unmarshaller: Unmarshaller[String, PaymentRequest] = Unmarshaller.strict { rawRequest => PaymentRequest.read(rawRequest) } + implicit val bolt11Unmarshaller: Unmarshaller[String, Bolt11Invoice] = Unmarshaller.strict { rawRequest => Bolt11Invoice.fromString(rawRequest) } implicit val shortChannelIdUnmarshaller: Unmarshaller[String, ShortChannelId] = Unmarshaller.strict { str => ShortChannelId(str) } diff --git a/eclair-node/src/test/resources/api/received-expired b/eclair-node/src/test/resources/api/received-expired index d754eceb51..d1d4b57414 100644 --- a/eclair-node/src/test/resources/api/received-expired +++ b/eclair-node/src/test/resources/api/received-expired @@ -1 +1 @@ -{"paymentRequest":{"prefix":"lnbc","timestamp":1496314658,"nodeId":"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad","serialized":"lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpuaztrnwngzn3kdzw5hydlzf03qdgm2hdq27cqv3agm2awhz5se903vruatfhq77w3ls4evs3ch9zw97j25emudupq63nyw24cg27h2rspfj9srp","description":"1 cup coffee","paymentHash":"0001020304050607080900010203040506070809000102030405060708090102","expiry":60,"amount":250000000,"features":{"activated":{},"unknown":[]},"routingInfo":[]},"paymentPreimage":"0100000000000000000000000000000000000000000000000000000000000000","paymentType":"Standard","createdAt":{"iso":"1970-01-01T00:00:00.042Z","unix":0},"status":{"type":"expired"}} \ No newline at end of file +{"invoice":{"prefix":"lnbc","timestamp":1496314658,"nodeId":"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad","serialized":"lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpuaztrnwngzn3kdzw5hydlzf03qdgm2hdq27cqv3agm2awhz5se903vruatfhq77w3ls4evs3ch9zw97j25emudupq63nyw24cg27h2rspfj9srp","description":"1 cup coffee","paymentHash":"0001020304050607080900010203040506070809000102030405060708090102","expiry":60,"amount":250000000,"features":{"activated":{},"unknown":[]},"routingInfo":[]},"paymentPreimage":"0100000000000000000000000000000000000000000000000000000000000000","paymentType":"Standard","createdAt":{"iso":"1970-01-01T00:00:00.042Z","unix":0},"status":{"type":"expired"}} \ No newline at end of file diff --git a/eclair-node/src/test/resources/api/received-pending b/eclair-node/src/test/resources/api/received-pending index 9e43ab9b12..abc7fdb9f7 100644 --- a/eclair-node/src/test/resources/api/received-pending +++ b/eclair-node/src/test/resources/api/received-pending @@ -1 +1 @@ -{"paymentRequest":{"prefix":"lnbc","timestamp":1496314658,"nodeId":"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad","serialized":"lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpuaztrnwngzn3kdzw5hydlzf03qdgm2hdq27cqv3agm2awhz5se903vruatfhq77w3ls4evs3ch9zw97j25emudupq63nyw24cg27h2rspfj9srp","description":"1 cup coffee","paymentHash":"0001020304050607080900010203040506070809000102030405060708090102","expiry":60,"amount":250000000,"features":{"activated":{},"unknown":[]},"routingInfo":[]},"paymentPreimage":"0100000000000000000000000000000000000000000000000000000000000000","paymentType":"Standard","createdAt":{"iso":"1970-01-01T00:00:00.042Z","unix":0},"status":{"type":"pending"}} \ No newline at end of file +{"invoice":{"prefix":"lnbc","timestamp":1496314658,"nodeId":"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad","serialized":"lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpuaztrnwngzn3kdzw5hydlzf03qdgm2hdq27cqv3agm2awhz5se903vruatfhq77w3ls4evs3ch9zw97j25emudupq63nyw24cg27h2rspfj9srp","description":"1 cup coffee","paymentHash":"0001020304050607080900010203040506070809000102030405060708090102","expiry":60,"amount":250000000,"features":{"activated":{},"unknown":[]},"routingInfo":[]},"paymentPreimage":"0100000000000000000000000000000000000000000000000000000000000000","paymentType":"Standard","createdAt":{"iso":"1970-01-01T00:00:00.042Z","unix":0},"status":{"type":"pending"}} \ No newline at end of file diff --git a/eclair-node/src/test/resources/api/received-success b/eclair-node/src/test/resources/api/received-success index b54154b991..6fdc44e62d 100644 --- a/eclair-node/src/test/resources/api/received-success +++ b/eclair-node/src/test/resources/api/received-success @@ -1 +1 @@ -{"paymentRequest":{"prefix":"lnbc","timestamp":1496314658,"nodeId":"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad","serialized":"lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpuaztrnwngzn3kdzw5hydlzf03qdgm2hdq27cqv3agm2awhz5se903vruatfhq77w3ls4evs3ch9zw97j25emudupq63nyw24cg27h2rspfj9srp","description":"1 cup coffee","paymentHash":"0001020304050607080900010203040506070809000102030405060708090102","expiry":60,"amount":250000000,"features":{"activated":{},"unknown":[]},"routingInfo":[]},"paymentPreimage":"0100000000000000000000000000000000000000000000000000000000000000","paymentType":"Standard","createdAt":{"iso":"1970-01-01T00:00:00.042Z","unix":0},"status":{"type":"received","amount":42,"receivedAt":{"iso":"2021-10-05T13:12:23.777Z","unix":1633439543}}} \ No newline at end of file +{"invoice":{"prefix":"lnbc","timestamp":1496314658,"nodeId":"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad","serialized":"lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpuaztrnwngzn3kdzw5hydlzf03qdgm2hdq27cqv3agm2awhz5se903vruatfhq77w3ls4evs3ch9zw97j25emudupq63nyw24cg27h2rspfj9srp","description":"1 cup coffee","paymentHash":"0001020304050607080900010203040506070809000102030405060708090102","expiry":60,"amount":250000000,"features":{"activated":{},"unknown":[]},"routingInfo":[]},"paymentPreimage":"0100000000000000000000000000000000000000000000000000000000000000","paymentType":"Standard","createdAt":{"iso":"1970-01-01T00:00:00.042Z","unix":0},"status":{"type":"received","amount":42,"receivedAt":{"iso":"2021-10-05T13:12:23.777Z","unix":1633439543}}} \ No newline at end of file diff --git a/eclair-node/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala b/eclair-node/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala index db3a12b910..642c500cfa 100644 --- a/eclair-node/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala +++ b/eclair-node/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala @@ -803,7 +803,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM test("'getreceivedinfo' 2") { val invoice = "lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpuaztrnwngzn3kdzw5hydlzf03qdgm2hdq27cqv3agm2awhz5se903vruatfhq77w3ls4evs3ch9zw97j25emudupq63nyw24cg27h2rspfj9srp" - val defaultPayment = IncomingPayment(PaymentRequest.read(invoice), ByteVector32.One, PaymentType.Standard, 42 unixms, IncomingPaymentStatus.Pending) + val defaultPayment = IncomingPayment(Bolt11Invoice.fromString(invoice), ByteVector32.One, PaymentType.Standard, 42 unixms, IncomingPaymentStatus.Pending) val eclair = mock[Eclair] val pending = randomBytes32() eclair.receivedInfo(pending)(any) returns Future.successful(Some(defaultPayment)) @@ -823,7 +823,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM test("'getreceivedinfo' 3") { val invoice = "lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpuaztrnwngzn3kdzw5hydlzf03qdgm2hdq27cqv3agm2awhz5se903vruatfhq77w3ls4evs3ch9zw97j25emudupq63nyw24cg27h2rspfj9srp" - val defaultPayment = IncomingPayment(PaymentRequest.read(invoice), ByteVector32.One, PaymentType.Standard, 42 unixms, IncomingPaymentStatus.Pending) + val defaultPayment = IncomingPayment(Bolt11Invoice.fromString(invoice), ByteVector32.One, PaymentType.Standard, 42 unixms, IncomingPaymentStatus.Pending) val eclair = mock[Eclair] val expired = randomBytes32() eclair.receivedInfo(expired)(any) returns Future.successful(Some(defaultPayment.copy(status = IncomingPaymentStatus.Expired))) @@ -843,7 +843,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM test("'getreceivedinfo' 4") { val invoice = "lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpuaztrnwngzn3kdzw5hydlzf03qdgm2hdq27cqv3agm2awhz5se903vruatfhq77w3ls4evs3ch9zw97j25emudupq63nyw24cg27h2rspfj9srp" - val defaultPayment = IncomingPayment(PaymentRequest.read(invoice), ByteVector32.One, PaymentType.Standard, 42 unixms, IncomingPaymentStatus.Pending) + val defaultPayment = IncomingPayment(Bolt11Invoice.fromString(invoice), ByteVector32.One, PaymentType.Standard, 42 unixms, IncomingPaymentStatus.Pending) val eclair = mock[Eclair] val received = randomBytes32() eclair.receivedInfo(received)(any) returns Future.successful(Some(defaultPayment.copy(status = IncomingPaymentStatus.Received(42 msat, TimestampMilli(1633439543777L))))) @@ -922,15 +922,15 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM val payment = SendPaymentToRouteResponse(UUID.fromString("487da196-a4dc-4b1e-92b4-3e5e905e9f3f"), UUID.fromString("2ad8c6d7-99cb-4238-8f67-89024b8eed0d"), None) val expected = """{"paymentId":"487da196-a4dc-4b1e-92b4-3e5e905e9f3f","parentId":"2ad8c6d7-99cb-4238-8f67-89024b8eed0d"}""" val externalId = UUID.randomUUID().toString - val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(1234 msat), ByteVector32.Zeroes, randomKey(), Left("Some invoice"), CltvExpiryDelta(24)) + val pr = Bolt11Invoice(Block.LivenetGenesisBlock.hash, Some(1234 msat), ByteVector32.Zeroes, randomKey(), Left("Some invoice"), CltvExpiryDelta(24)) val expectedRoute = PredefinedNodeRoute(Seq(PublicKey(hex"0217eb8243c95f5a3b7d4c5682d10de354b7007eb59b6807ae407823963c7547a9"), PublicKey(hex"0242a4ae0c5bef18048fbecf995094b74bfb0f7391418d71ed394784373f41e4f3"), PublicKey(hex"026ac9fcd64fb1aa1c491fc490634dc33da41d4a17b554e0adf1b32fee88ee9f28"))) val jsonNodes = serialization.write(expectedRoute.nodes) val eclair = mock[Eclair] - eclair.sendToRoute(any[MilliSatoshi], any[Option[MilliSatoshi]], any[Option[String]], any[Option[UUID]], any[PaymentRequest], any[CltvExpiryDelta], any[PredefinedNodeRoute], any[Option[ByteVector32]], any[Option[MilliSatoshi]], any[Option[CltvExpiryDelta]], any[List[PublicKey]])(any[Timeout]) returns Future.successful(payment) + eclair.sendToRoute(any[MilliSatoshi], any[Option[MilliSatoshi]], any[Option[String]], any[Option[UUID]], any[Bolt11Invoice], any[CltvExpiryDelta], any[PredefinedNodeRoute], any[Option[ByteVector32]], any[Option[MilliSatoshi]], any[Option[CltvExpiryDelta]], any[List[PublicKey]])(any[Timeout]) returns Future.successful(payment) val mockService = new MockService(eclair) - Post("/sendtoroute", FormData("nodeIds" -> jsonNodes, "amountMsat" -> "1234", "finalCltvExpiry" -> "190", "externalId" -> externalId, "invoice" -> PaymentRequest.write(pr)).toEntity) ~> + Post("/sendtoroute", FormData("nodeIds" -> jsonNodes, "amountMsat" -> "1234", "finalCltvExpiry" -> "190", "externalId" -> externalId, "invoice" -> pr.toString).toEntity) ~> addCredentials(BasicHttpCredentials("", mockApi().password)) ~> addHeader("Content-Type", "application/json") ~> Route.seal(mockService.sendToRoute) ~> @@ -945,16 +945,16 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM test("'sendtoroute' method should accept a comma separated list of pubkeys") { val payment = SendPaymentToRouteResponse(UUID.fromString("487da196-a4dc-4b1e-92b4-3e5e905e9f3f"), UUID.fromString("2ad8c6d7-99cb-4238-8f67-89024b8eed0d"), None) val expected = """{"paymentId":"487da196-a4dc-4b1e-92b4-3e5e905e9f3f","parentId":"2ad8c6d7-99cb-4238-8f67-89024b8eed0d"}""" - val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(1234 msat), ByteVector32.Zeroes, randomKey(), Left("Some invoice"), CltvExpiryDelta(24)) + val pr = Bolt11Invoice(Block.LivenetGenesisBlock.hash, Some(1234 msat), ByteVector32.Zeroes, randomKey(), Left("Some invoice"), CltvExpiryDelta(24)) val expectedRoute = PredefinedNodeRoute(Seq(PublicKey(hex"0217eb8243c95f5a3b7d4c5682d10de354b7007eb59b6807ae407823963c7547a9"), PublicKey(hex"0242a4ae0c5bef18048fbecf995094b74bfb0f7391418d71ed394784373f41e4f3"), PublicKey(hex"026ac9fcd64fb1aa1c491fc490634dc33da41d4a17b554e0adf1b32fee88ee9f28"))) val csvNodes = "0217eb8243c95f5a3b7d4c5682d10de354b7007eb59b6807ae407823963c7547a9, 0242a4ae0c5bef18048fbecf995094b74bfb0f7391418d71ed394784373f41e4f3, 026ac9fcd64fb1aa1c491fc490634dc33da41d4a17b554e0adf1b32fee88ee9f28" val eclair = mock[Eclair] - eclair.sendToRoute(any[MilliSatoshi], any[Option[MilliSatoshi]], any[Option[String]], any[Option[UUID]], any[PaymentRequest], any[CltvExpiryDelta], any[PredefinedNodeRoute], any[Option[ByteVector32]], any[Option[MilliSatoshi]], any[Option[CltvExpiryDelta]], any[List[PublicKey]])(any[Timeout]) returns Future.successful(payment) + eclair.sendToRoute(any[MilliSatoshi], any[Option[MilliSatoshi]], any[Option[String]], any[Option[UUID]], any[Bolt11Invoice], any[CltvExpiryDelta], any[PredefinedNodeRoute], any[Option[ByteVector32]], any[Option[MilliSatoshi]], any[Option[CltvExpiryDelta]], any[List[PublicKey]])(any[Timeout]) returns Future.successful(payment) val mockService = new MockService(eclair) // this test uses CSV encoded route - Post("/sendtoroute", FormData("nodeIds" -> csvNodes, "amountMsat" -> "1234", "finalCltvExpiry" -> "190", "invoice" -> PaymentRequest.write(pr)).toEntity) ~> + Post("/sendtoroute", FormData("nodeIds" -> csvNodes, "amountMsat" -> "1234", "finalCltvExpiry" -> "190", "invoice" -> pr.toString).toEntity) ~> addCredentials(BasicHttpCredentials("", mockApi().password)) ~> addHeader("Content-Type", "application/json") ~> Route.seal(mockService.sendToRoute) ~> @@ -968,7 +968,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM test("'findroute' method response should support nodeId, shortChannelId and full formats") { val serializedInvoice = "lnbc12580n1pw2ywztpp554ganw404sh4yjkwnysgn3wjcxfcq7gtx53gxczkjr9nlpc3hzvqdq2wpskwctddyxqr4rqrzjqwryaup9lh50kkranzgcdnn2fgvx390wgj5jd07rwr3vxeje0glc7z9rtvqqwngqqqqqqqlgqqqqqeqqjqrrt8smgjvfj7sg38dwtr9kc9gg3era9k3t2hvq3cup0jvsrtrxuplevqgfhd3rzvhulgcxj97yjuj8gdx8mllwj4wzjd8gdjhpz3lpqqvk2plh" - val invoice = PaymentRequest.read(serializedInvoice) + val invoice = Invoice.fromString(serializedInvoice) val mockChannelUpdate1 = ChannelUpdate( signature = ByteVector64.fromValidHex("92cf3f12e161391986eb2cd7106ddab41a23c734f8f1ed120fb64f4b91f98f690ecf930388e62965f8aefbf1adafcd25a572669a125396dcfb83615208754679"), From fa31d81d0eba76b8c2e931642f726cdb80aade99 Mon Sep 17 00:00:00 2001 From: Thomas HUET <81159533+thomash-acinq@users.noreply.github.com> Date: Wed, 2 Feb 2022 17:45:22 +0100 Subject: [PATCH 014/121] Improve postman (#2147) Makes the code a bit cleaner and fixes a bug where `Postman` could respond with both a failure to send and later a `NoReply` after the timeout in case we were expecting a reply. --- .../fr/acinq/eclair/message/Postman.scala | 59 +++++++++---------- .../fr/acinq/eclair/message/PostmanSpec.scala | 8 ++- 2 files changed, 32 insertions(+), 35 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/message/Postman.scala b/eclair-core/src/main/scala/fr/acinq/eclair/message/Postman.scala index 171c09f47a..2bb92857b0 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/message/Postman.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/message/Postman.scala @@ -53,59 +53,54 @@ object Postman { val relayMessageStatusAdapter = context.messageAdapter[MessageRelay.Status](SendingStatus) + // For messages expecting a reply, send reply or failure to send val subscribed = new mutable.HashMap[ByteVector32, ActorRef[OnionMessageResponse]]() + + // For messages not expecting a reply, send success or failure to send val sendStatusTo = new mutable.HashMap[ByteVector32, ActorRef[OnionMessageResponse]]() - val sendFailureTo = new mutable.HashMap[ByteVector32, ActorRef[OnionMessageResponse]]() Behaviors.receiveMessagePartial { case WrappedMessage(finalPayload, Some(pathId)) if pathId.length == 32 => - subscribed.get(ByteVector32(pathId)) match { - case Some(ref) => - subscribed -= ByteVector32(pathId) - ref ! Response(finalPayload) - case None => () // ignoring message with unknown pathId - } + val id = ByteVector32(pathId) + subscribed.get(id).foreach(ref => { + subscribed -= id + ref ! Response(finalPayload) + }) Behaviors.same case WrappedMessage(_, _) => // ignoring message with invalid or missing pathId Behaviors.same - case SendMessage(nextNodeId, message, None, ref, _) => + case SendMessage(nextNodeId, message, None, ref, _) => // not expecting reply val messageId = randomBytes32() sendStatusTo += (messageId -> ref) switchboard ! Switchboard.RelayMessage(messageId, None, nextNodeId, message, MessageRelay.RelayAll, Some(relayMessageStatusAdapter)) Behaviors.same - case SendMessage(nextNodeId, message, Some(pathId), ref, timeout) => - val messageId = randomBytes32() - sendFailureTo += (messageId -> ref) + case SendMessage(nextNodeId, message, Some(pathId), ref, timeout) => // expecting reply subscribed += (pathId -> ref) context.scheduleOnce(timeout, context.self, Unsubscribe(pathId)) - switchboard ! Switchboard.RelayMessage(messageId, None, nextNodeId, message, MessageRelay.RelayAll, Some(relayMessageStatusAdapter)) + switchboard ! Switchboard.RelayMessage(pathId, None, nextNodeId, message, MessageRelay.RelayAll, Some(relayMessageStatusAdapter)) Behaviors.same case Unsubscribe(pathId) => - subscribed.get(pathId).foreach(_ ! NoReply) - subscribed -= pathId + subscribed.get(pathId).foreach(ref => { + subscribed -= pathId + ref ! NoReply + }) Behaviors.same case status@SendingStatus(MessageRelay.Sent(messageId)) => - sendStatusTo.get(messageId) match { - case Some(ref) => - sendStatusTo -= messageId - ref ! status - case None => () - } + sendStatusTo.get(messageId).foreach(ref => { + sendStatusTo -= messageId + ref ! status + }) Behaviors.same case SendingStatus(status: MessageRelay.Failure) => - sendStatusTo.get(status.messageId) match { - case Some(ref) => - sendStatusTo -= status.messageId - ref ! SendingStatus(status) - case None => () - } - sendFailureTo.get(status.messageId) match { - case Some(ref) => - sendFailureTo -= status.messageId - ref ! SendingStatus(status) - case None => () - } + sendStatusTo.get(status.messageId).foreach(ref => { + sendStatusTo -= status.messageId + ref ! SendingStatus(status) + }) + subscribed.get(status.messageId).foreach(ref => { + subscribed -= status.messageId + ref ! SendingStatus(status) + }) Behaviors.same } }) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/message/PostmanSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/message/PostmanSpec.scala index 0a87b4dae0..2bd30db61a 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/message/PostmanSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/message/PostmanSpec.scala @@ -59,7 +59,7 @@ class PostmanSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("applicat val (_, messageExpectingReply) = OnionMessages.buildMessage(randomKey(), randomKey(), Nil, OnionMessages.Recipient(recipient, None), ReplyPath(replyPath) :: Nil) val payload = FinalPayload(TlvStream(EncryptedData(replyPath.encryptedPayloads.last) :: Nil, GenericTlv(UInt64(42), hex"abcd") :: Nil)) - postman ! SendMessage(recipient, messageExpectingReply, Some(pathId), messageRecipient.ref, 1 second) + postman ! SendMessage(recipient, messageExpectingReply, Some(pathId), messageRecipient.ref, 100 millis) val RelayMessage(messageId, _, nextNodeId, message, _, _) = switchboard.expectMessageType[RelayMessage] assert(nextNodeId === recipient) @@ -81,7 +81,7 @@ class PostmanSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("applicat val replyPath = OnionMessages.buildRoute(randomKey(), Nil, OnionMessages.Recipient(ourNodeId, Some(pathId))) val (_, messageExpectingReply) = OnionMessages.buildMessage(randomKey(), randomKey(), Nil, OnionMessages.Recipient(recipient, None), ReplyPath(replyPath) :: Nil) - postman ! SendMessage(recipient, messageExpectingReply, Some(pathId), messageRecipient.ref, 1 second) + postman ! SendMessage(recipient, messageExpectingReply, Some(pathId), messageRecipient.ref, 100 millis) val RelayMessage(messageId, _, nextNodeId, message, _, _) = switchboard.expectMessageType[RelayMessage] assert(nextNodeId === recipient) @@ -89,6 +89,7 @@ class PostmanSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("applicat postman ! SendingStatus(Disconnected(messageId)) messageRecipient.expectMessage(SendingStatus(Disconnected(messageId))) + messageRecipient.expectNoMessage() } test("timeout") { f => @@ -121,7 +122,7 @@ class PostmanSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("applicat val recipient = randomKey().publicKey val (_, messageExpectingReply) = OnionMessages.buildMessage(randomKey(), randomKey(), Nil, OnionMessages.Recipient(recipient, None), Nil) - postman ! SendMessage(recipient, messageExpectingReply, None, messageRecipient.ref, 1 second) + postman ! SendMessage(recipient, messageExpectingReply, None, messageRecipient.ref, 100 millis) val RelayMessage(messageId, _, nextNodeId, message, _, _) = switchboard.expectMessageType[RelayMessage] assert(nextNodeId === recipient) @@ -129,5 +130,6 @@ class PostmanSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("applicat postman ! SendingStatus(Sent(messageId)) messageRecipient.expectMessage(SendingStatus(Sent(messageId))) + messageRecipient.expectNoMessage() } } From 553727cb22d3d4a0f596b75a3ab733769bb24479 Mon Sep 17 00:00:00 2001 From: Richard Myers Date: Sun, 6 Feb 2022 20:28:50 +0100 Subject: [PATCH 015/121] Convert wiki pages in to files in the docs directory and general docs file cleanups (#2165) Created new files for pages that were in the wiki, but not already in the docs directory. Also made following fixes to README.md and existing files in the docs directory: * update bolt links to avoid redirect * link to logging guide from logging section (README.md) * fixed typo in Backup section and capitalization of Bitcoin Core (README.md) * Alice does not need trampoline feature enabled (TrampolinePayments.md) * link to 2021 edition of Trampoline PR (TrampolinePayments.md) * fixed API examples and removed quotes from password (API.md) * use --nodeIds for sendtoroute examples (TrampolinePayments.md and MultipartPayments.md) * update CLI example 3 to use jq (Usage.md) * fix typo in docs/FAQ.md * updated Guide.md to point to all pages that are guides --- CONTRIBUTING.md | 2 +- README.md | 38 +++++++++------- docs/API.md | 43 ++++++++++++++++++ docs/Architecture.md | 10 ++--- docs/Configure.md | 3 +- docs/FAQ.md | 18 ++++++++ docs/Features.md | 53 ++++++++++++++++++++++ docs/Guides.md | 14 ++++++ docs/Logging.md | 60 +++++++++++++++++++++++++ docs/MultipartPayments.md | 80 +++++++++++++++++++++++++++++++++ docs/TrampolinePayments.md | 68 ++++++++++++++++++++++++++++ docs/Usage.md | 90 ++++++++++++++++++++++++++++++++++++++ 12 files changed, 455 insertions(+), 24 deletions(-) create mode 100644 docs/API.md create mode 100644 docs/FAQ.md create mode 100644 docs/Features.md create mode 100644 docs/Guides.md create mode 100644 docs/Logging.md create mode 100644 docs/MultipartPayments.md create mode 100644 docs/TrampolinePayments.md create mode 100644 docs/Usage.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6740717247..2ed645e5ec 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -30,7 +30,7 @@ You can also use Github issues for [feature requests](https://github.com/acinq/e - [Lightning Network Whitepaper](https://lightning.network/lightning-network-paper.pdf) - [Deployable Lightning](https://github.com/ElementsProject/lightning/raw/master/doc/deployable-lightning.pdf) - [Understanding the Lightning Network](https://bitcoinmagazine.com/articles/understanding-the-lightning-network-part-building-a-bidirectional-payment-channel-1464710791) -- [Lightning Network Specification](https://github.com/lightningnetwork/lightning-rfc) +- [Lightning Network Specification](https://github.com/lightning/bolts) - [High Level Lightning Network Specification](https://medium.com/@rusty_lightning/the-bitcoin-lightning-spec-part-1-8-a7720fb1b4da) ## Recommended Skillset diff --git a/README.md b/README.md index 7e05e7929c..819509001c 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ **Eclair** (French for Lightning) is a Scala implementation of the Lightning Network. -This software follows the [Lightning Network Specifications (BOLTs)](https://github.com/lightningnetwork/lightning-rfc). Other implementations include [c-lightning](https://github.com/ElementsProject/lightning), [lnd](https://github.com/LightningNetwork/lnd), [electrum](https://github.com/spesmilo/electrum/), and [rust-lightning](https://github.com/rust-bitcoin/rust-lightning). +This software follows the [Lightning Network Specifications (BOLTs)](https://github.com/lightning/bolts). Other implementations include [c-lightning](https://github.com/ElementsProject/lightning), [lnd](https://github.com/LightningNetwork/lnd), [electrum](https://github.com/spesmilo/electrum/), and [rust-lightning](https://github.com/rust-bitcoin/rust-lightning). --- @@ -25,6 +25,7 @@ This software follows the [Lightning Network Specifications (BOLTs)](https://git * [Docker](#docker) * [Plugins](#plugins) * [Testnet usage](#testnet-usage) +* [Tools](#tools) * [Resources](#resources) --- @@ -43,10 +44,10 @@ For more information please visit the [API documentation website](https://acinq. ## Documentation -Please visit our [docs](./docs) and [wiki](https://github.com/acinq/eclair/wiki) to find detailed instructions on how to configure your -node, connect to other nodes, open channels, send and receive payments, and more advanced scenario. +Please visit our [docs](./docs) folder to find detailed instructions on how to [configure](./docs/Configure.md) your +node, connect to other nodes, open channels, send and receive payments, and help with more advanced scenarios. -You will find detailed guides and frequently asked questions there. +You will also find detailed [guides](./docs/Guides.md) and [frequently asked questions](./docs/FAQ.md) there. ## Installation @@ -95,7 +96,7 @@ Then download our latest [release](https://github.com/ACINQ/eclair/releases), un eclair-node--/bin/eclair-node.sh ``` -You can then control your node via the [eclair-cli](https://github.com/ACINQ/eclair/wiki/Usage) or the [API](https://github.com/ACINQ/eclair/wiki/API). +You can then control your node via [eclair-cli](./docs/Usage.md) or the [API](./docs/API.md). :warning: Be careful when following tutorials/guides that may be outdated or incomplete. You must thoroughly read the official eclair documentation before running your own node. @@ -168,7 +169,7 @@ eclair-node--/bin/eclair-node.sh -Declair.datadir=/tmp/node1 ### Logging -Eclair uses [`logback`](https://logback.qos.ch) for logging. To use a different configuration, and override the internal logback.xml, run: +Eclair uses [`logback`](https://logback.qos.ch) for logging. To use a [different configuration](./docs/Logging.md), and override the internal logback.xml, run: ```shell eclair-node--/bin/eclair-node.sh -Dlogback.configurationFile=/path/to/logback-custom.xml @@ -177,13 +178,13 @@ eclair-node--/bin/eclair-node.sh -Dlogback.configurationFile ### Backup You need to backup: -- your bitcoin core wallet -- your eclair channels +- your Bitcoin Core wallet +- your Eclair channels -For bitcoin core, you need to backup the wallet file for the wallet that eclair is using. You only need to this once, when the wallet is -created (see https://github.com/bitcoin/bitcoin/blob/master/doc/managing-wallets.md for more information). +For Bitcoin Core, you need to backup the wallet file for the wallet that Eclair is using. You only need to do this once, when the wallet is +created. See [Managing Wallets](https://github.com/bitcoin/bitcoin/blob/master/doc/managing-wallets.md) in the Bitcoin Core documentation for more information. -For eclair, the files that you need to backup are located in your data directory. You must backup: +For Eclair, the files that you need to backup are located in your data directory. You must backup: * your seeds (`node_seed.dat` and `channel_seed.dat`) * your channel database (`eclair.sqlite.bak` under directory `mainnet`, `testnet` or `regtest` depending on which chain you're running on) @@ -219,7 +220,7 @@ If you want to persist the data directory, you can make the volume to your host docker run -ti --rm -v "/path_on_host:/data" -e "JAVA_OPTS=-Declair.printToConsole" acinq/eclair ``` -If you enabled the API you can check the status of eclair using the command line tool: +If you enabled the API you can check the status of Eclair using the command line tool: ```shell docker exec eclair-cli -p foobar getinfo @@ -239,14 +240,14 @@ eclair-node--/bin/eclair-node.sh ### Non-exhaustive plugins list -Here are some plugins created by the eclair community. +Here are some plugins created by the Eclair community. If you need support for these plugins, head over to their respective github repository. -* [Telegram Bot for eclair alerts](https://github.com/engenegr/eclair-alarmbot-plugin) +* [Telegram Bot for Eclair alerts](https://github.com/engenegr/eclair-alarmbot-plugin) ## Testnet usage -Eclair is configured to run on mainnet by default, but you can still run it on testnet (or regtest): start your Bitcoin Node in +Eclair is configured to run on mainnet by default, but you can still run it on testnet (or regtest): start your Bitcoin node in testnet mode (add `testnet=1` in `bitcoin.conf` or start with `-testnet`), and change Eclair's chain parameter and Bitcoin RPC port: ```conf @@ -255,7 +256,7 @@ eclair.bitcoind.rpcport=18332 ``` You may also want to take advantage of the new configuration sections in `bitcoin.conf` to manage parameters that are network specific, -so you can easily run your bitcoin node on both mainnet and testnet. For example you could use: +so you can easily run your Bitcoin node on both mainnet and testnet. For example you could use: ```conf server=1 @@ -272,6 +273,11 @@ zmqpubhashblock=tcp://127.0.0.1:29001 zmqpubrawtx=tcp://127.0.0.1:29001 ``` +## Tools + +* [Demo Shop](https://starblocks.acinq.co/) - an example testnet Lightning web shop. +* [Network Explorer](https://explorer.acinq.co/) - a Lightning network visualization tool. + ## Resources * [1] [The Bitcoin Lightning Network: Scalable Off-Chain Instant Payments](https://lightning.network/lightning-network-paper.pdf) by Joseph Poon and Thaddeus Dryja diff --git a/docs/API.md b/docs/API.md new file mode 100644 index 0000000000..cd3484d69a --- /dev/null +++ b/docs/API.md @@ -0,0 +1,43 @@ +# API + +Eclair can setup a JSON API on the `eclair.api.port` port set in the [configuration](./Configure.md). You first need to enable it: + +``` +eclair.api.enabled=true +eclair.api.password=changeit +``` + +:rotating_light: **Attention:** Eclair's API should NOT be accessible from the outside world (similarly to Bitcoin Core API). + +## Payment notification + +Eclair accepts websocket connection on `ws://localhost:/ws`, and emits a message containing the payment hash of a payment when receiving a payment. + +## API calls + +This API exposes all the necessary methods to read the current state of the node, open/close channels and send/receive payments. For the full documentation please visit https://acinq.github.io/eclair + +#### Example: open a channel + +Your node listens on 8081. You want to open a 140 mBTC channel with `endurance.acinq.co` on Testnet with a 30 mBTC `push` + +1/ connect to the node with the URI: + +```shell +curl -X POST \ + -u :api_password \ + -F uri="03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134@endurance.acinq.co:9735" \ + http://127.0.0.1:8081/connect +``` + +2/ Open a channel with this node + +```shell +curl -X POST \ + -u :api_password \ + -F nodeId=03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134 \ + -F fundingSatoshis=14000000 \ + http://127.0.0.1:8081/open +``` + +Feeling tired of writing all these `curls`? Good news, a CLI bash file is available. It uses this API to interact with your node. More information about `eclair-cli` is [here](./Usage.md). \ No newline at end of file diff --git a/docs/Architecture.md b/docs/Architecture.md index c7f74cafba..ce53003a99 100644 --- a/docs/Architecture.md +++ b/docs/Architecture.md @@ -106,13 +106,13 @@ Here is a high-level view of the hierarchy of some of the main actors in the sys And a short description of each actor's role: - Switchboard: creates and deletes peers -- Peer: p2p connection to another lightning node (standard lightning messages described in [Bolt 1](https://github.com/lightningnetwork/lightning-rfc/blob/master/01-messaging.md)) -- Channel: channel with another lightning node ([Bolt 2](https://github.com/lightningnetwork/lightning-rfc/blob/master/02-peer-protocol.md)) +- Peer: p2p connection to another lightning node (standard lightning messages described in [Bolt 1](https://github.com/lightning/bolts/blob/master/01-messaging.md)) +- Channel: channel with another lightning node ([Bolt 2](https://github.com/lightning/bolts/blob/master/02-peer-protocol.md)) - Register: maps channel IDs to actors (provides a clean boundary between channel and payment components) - PaymentInitiator: entry point for sending payments - Relayer: entry point for relaying payments - PaymentHandler: entry point for receiving payments -- Router: p2p gossip and the network graph ([Bolt 7](https://github.com/lightningnetwork/lightning-rfc/blob/master/07-routing-gossip.md)) +- Router: p2p gossip and the network graph ([Bolt 7](https://github.com/lightning/bolts/blob/master/07-routing-gossip.md)) Actors have two ways of communicating: @@ -128,7 +128,7 @@ Let's dive into a few payment scenarios to show which actors are involved. When we send a payment: - we run a path-finding algorithm (`Router`) -- we split the payment into smaller chunks if [MPP](https://github.com/lightningnetwork/lightning-rfc/blob/master/04-onion-routing.md#basic-multi-part-payments) is used (`MultiPartPaymentLifecycle`) +- we split the payment into smaller chunks if [MPP](https://github.com/lightning/bolts/blob/master/04-onion-routing.md#basic-multi-part-payments) is used (`MultiPartPaymentLifecycle`) - we retry with alternative routes in some failure cases and record failing channels/payments (`PaymentLifecycle`) - we add HTLCs to some of our channels @@ -175,7 +175,7 @@ When we relay a payment: - htlcs are forwarded by channels to the relayer - the relayer identifies the type of relay requested and delegates work to a channel relayer or a node relayer -- if a node relayer is used ([trampoline payments](https://github.com/lightningnetwork/lightning-rfc/pull/829)): +- if a node relayer is used ([trampoline payments](https://github.com/lightning/bolts/pull/829)): - incoming htlcs are validated by a payment handler (similar to the flow to receive payments) - outgoing htlcs are sent out (similar to the flow to send payments) diff --git a/docs/Configure.md b/docs/Configure.md index bbf9b06f52..62f1534ebb 100644 --- a/docs/Configure.md +++ b/docs/Configure.md @@ -19,8 +19,7 @@ ## Configuration file The configuration file for eclair is named `eclair.conf`. It is located in the data directory, which is `~/.eclair` by -default. Note that eclair won't create a configuration file by itself: if you want to change eclair's configuration, you -need to **actually create the configuration file first**. The encoding must be UTF-8. +default (on Windows it is `C:\Users\YOUR_NAME\.eclair`). Note that eclair won't create a configuration file by itself: if you want to change eclair's configuration, you need to **actually create the configuration file first**. The encoding must be UTF-8. ```sh # this is the default data directory, it will be created at eclair first startup diff --git a/docs/FAQ.md b/docs/FAQ.md new file mode 100644 index 0000000000..802216d480 --- /dev/null +++ b/docs/FAQ.md @@ -0,0 +1,18 @@ +# FAQ + +## What does it mean for a channel to be "enabled" or "disabled" ? + +A channel is disabled if a `channel_update` message has been broadcast for that channel with the `disable` bit set (see [BOLT 7](https://github.com/lightning/bolts/blob/master/07-routing-gossip.md#the-channel_update-message)). It means that the channel still exists but cannot be used to route payments, until it has been re-enabled. + +Suppose you're A, with the following setup: +``` +A ---ab--> B --bc--> C +``` +And node C goes down. B will publish a channel update for channel `bc` with the `disable` bit set. +There are other cases when a channel becomes disabled, for example when its balance goes below reserve... + +Note that you can have multiple channels between the same nodes, and that some of them can be enabled while others are disabled (i.e. enable/disable is channel-specific, not node-specific). + +## How should you stop an Eclair node ? + +To stop your node you just need to kill its process, there is no API command to do this. The JVM handles the quit signal and notifies the node to perform clean-up. For example, there is a hook to cleanly free DB locks when using Postgres. diff --git a/docs/Features.md b/docs/Features.md new file mode 100644 index 0000000000..57874fe205 --- /dev/null +++ b/docs/Features.md @@ -0,0 +1,53 @@ +# Customize Features + +Eclair ships with a set of features that are activated by default, and some experimental or optional features that can be activated by users. +The list of supported features can be found in the [reference configuration](https://github.com/ACINQ/eclair/blob/master/eclair-core/src/main/resources/reference.conf). + +To enable a non-default feature, you simply need to add the following to your `eclair.conf`: + +```conf +eclair.features { + official_feature_name = optional|mandatory +} +``` + +For example, to activate `option_static_remotekey`: + +```conf +eclair.features { + option_static_remotekey = optional +} +``` + +Note that you can also disable some default features: + +```conf +eclair.features { + initial_routing_sync = disabled +} +``` + +It's usually risky to activate non-default features or disable default features: make sure you fully understand a feature (and the current implementation status, detailed in the release notes) before doing so. + +Eclair supports per-peer features. Suppose you are connected to Alice and Bob, you can use a different set of features with Alice than the one you use with Bob. When experimenting with non-default features, we recommend using this to scope the peers you want to experiment with. + +This is done with the `override-features` configuration parameter in your `eclair.conf`: + +```conf +eclair.override-features = [ + { + nodeId = "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f" + features { + initial_routing_sync = disabled + option_static_remotekey = optional + } + }, + { + nodeId = "" + features { + option_static_remotekey = optional + option_support_large_channel = optional + } + }, +] +``` \ No newline at end of file diff --git a/docs/Guides.md b/docs/Guides.md new file mode 100644 index 0000000000..6f7a19d0a9 --- /dev/null +++ b/docs/Guides.md @@ -0,0 +1,14 @@ +# Guides + +This section contains how-to guides for more advanced scenarios: + +* [Customize Logging](./Logging.md) +* [Customize Features](./Features.md) +* [Use Tor with Eclair](./Tor.md) +* [Multipart Payments](./MultipartPayments.md) +* [Trampoline Payments](./TrampolinePayments.md) +* [Monitoring Eclair](./Monitoring.md) +* [PostgreSQL Configuration](./PostgreSQL.md) +* [Perform Circular Rebalancing](./CircularRebalancing.md) +* [Clusterize your Eclair node](./Cluster.md) +* [Architecture of Eclair code](./Architecture.md) \ No newline at end of file diff --git a/docs/Logging.md b/docs/Logging.md new file mode 100644 index 0000000000..a4f7bd710a --- /dev/null +++ b/docs/Logging.md @@ -0,0 +1,60 @@ +# Logging + +### Customize Logging + +Eclair uses [logback](https://logback.qos.ch/) for logging. +By default the logging level is set to `INFO` for the whole application. +To use a different configuration, and override the internal `logback.xml`, run: + +```shell +eclair-node.sh -Dlogback.configurationFile=/path/to/logback-custom.xml +``` + +If you want parts of the application to log at the `DEBUG` logging level, you will also need to override `akka.loglevel`: + +```shell +eclair-node.sh -Dakka.loglevel=DEBUG -Dlogback.configurationFile=/path/to/logback-custom.xml +``` + +You can find the default `logback.xml` file [here](https://github.com/ACINQ/eclair/blob/master/eclair-node/src/main/resources/logback.xml). It logs everything more serious than `INFO` to a rolling file. + +If you want to debug an issue, you can change the root logger's log level to `DEBUG`: + +```xml + + + +``` + +This setting will produce a lot of logs. If you are investigating an issue with payments, you can instead turn on `DEBUG` logging only for payments-related components by adding a new `logger` before the `root`: + +```xml + + + + + +``` + +On the contrary, if you want a component to emit less logs, you can set its log level to `WARN` or `ERROR`: + +```xml + + + + + +``` + +To figure out the `name` you should use for the `logger` element, you need to look at the hierarchy of the source code in Eclair's [Github repository](https://github.com/ACINQ/eclair). You can even configure loggers for each specific class you're interested in: + +```xml + + + + + + +``` + +For more advanced use-cases, please see logback's [official documentation](https://logback.qos.ch/documentation.html). \ No newline at end of file diff --git a/docs/MultipartPayments.md b/docs/MultipartPayments.md new file mode 100644 index 0000000000..878553e935 --- /dev/null +++ b/docs/MultipartPayments.md @@ -0,0 +1,80 @@ +# Multipart Payments + +Eclair started supporting [multi-part payments](https://github.com/lightning/bolts/blob/master/04-onion-routing.md#basic-multi-part-payments) in v0.3.3. + +## Receiving multi-part payments + +Once the feature is activated, your eclair node will generate invoices that can be paid using MPP. +If the sender also supports MPP, your node will accept the payment. +It's as simple as that! + +Here is how you generate an invoice: + +```sh +eclair-cli createinvoice --amountMsat=42000000 --description="MPP is #reckless" +``` + +## Sending multi-part payments + +### With the built-in splitting algorithm + +If you try paying an invoice that supports MPP, eclair will automatically split the payment (if needed). + +You just need to use the `payinvoice` command, nothing complicated here: + +```sh +eclair-cli payinvoice --invoice=lnbc11pdkmqhupp5n2e... +``` + +However, we are still experimenting with various algorithms to efficiently split payments. +You may find that the resulting split looks less optimal that what you expected, in which case the next section is for you! + +### With your own splitting algorithm + +The CLI allows you to fully control how your payment is split and sent. This is a good way to start experimenting with MPP. + +Let's imagine that the network looks like this: + +```txt + +------- Bob -------+ + | | +Alice Dave + | | + +------- Carol -----+ + +``` + +Dave has generated an MPP invoice for 400000 msat: `lntb1500n1pwxx94fp...`. + +You want to send 150000 msat through Bob and 250000 msat through Carol. + +Initiate the payment by sending the first part: + +```sh +eclair-cli sendtoroute --amountMsat=150000 --nodeIds=$ALICE_ID,$BOB_ID,$DAVE_ID --finalCltvExpiry=16 --invoice=lntb1500n1pwxx94fp... +``` + +This will return some identifiers that must be used for the other parts: + +```json +{ + "paymentId": "4e8f2440-dbfd-4e76-bb45-a0647a966b2a", + "parentId": "cd083b31-5939-46ac-bf90-8ac5b286a9e2" +} +``` + +The `parentId` is important: this is the identifier used to link the parts together. + +Now that you have those, you can send the second part: + +```sh +eclair-cli sendtoroute --parentId=cd083b31-5939-46ac-bf90-8ac5b286a9e2 --amountMsat=250000 --nodeIds=$ALICE_ID,$CAROL_ID,$DAVE_ID --finalCltvExpiry=16 --invoice=lntb1500n1pwxx94fp... +``` + +You can then check the status of the payment with the `getsentinfo` command: + +```sh +eclair-cli getsentinfo --id=cd083b31-5939-46ac-bf90-8ac5b286a9e2 +``` + +Once Dave accepts the HTLCs you should see all the details about the payment success (preimage, route, fees, etc). diff --git a/docs/TrampolinePayments.md b/docs/TrampolinePayments.md new file mode 100644 index 0000000000..62ffa3b1cd --- /dev/null +++ b/docs/TrampolinePayments.md @@ -0,0 +1,68 @@ +# Trampoline Payments + +Eclair started supporting [trampoline payments](https://github.com/lightning/bolts/pull/829) in v0.3.3. + +It is disabled by default, as it is still being reviewed for spec acceptance. However, if you want to experiment with it, here is what you can do. + +First of all, you need to activate the feature for any node that will act as s trampoline node. Update your `eclair.conf` with the following values: + +```conf +eclair.trampoline-payments-enable=true +``` + +## Sending trampoline payments + +The CLI allows you to fully control how your payment is split and sent. This is a good way to start experimenting with Trampoline. + +Let's imagine that the network looks like this: + +```txt +Alice -----> Bob -----> Carol -----> Dave +``` + +Where Bob is a trampoline node and Alice, Carol and Dave are "normal" nodes. + +Let's imagine that Dave has generated an MPP invoice for 400000 msat: `lntb1500n1pwxx94fp...`. +Alice wants to pay that invoice using Bob as a trampoline. +To spice things up, Alice will use MPP between Bob and her, splitting the payment in two parts. + +Initiate the payment by sending the first part: + +```sh +eclair-cli sendtoroute --amountMsat=150000 --nodeIds=$ALICE_ID,$BOB_ID --trampolineNodes=$BOB_ID,$DAVE_ID --trampolineFeesMsat=100000 --trampolineCltvExpiry=450 --finalCltvExpiry=16 --invoice=lntb1500n1pwxx94fp... +``` + +Note the `trampolineFeesMsat` and `trampolineCltvExpiry`. At the moment you have to estimate those yourself. If the values you provide are too low, Bob will send an error and you can retry with higher values. In future versions, we will automatically fill those values for you. + +The command will return some identifiers that must be used for the other parts: + +```json +{ + "paymentId": "4e8f2440-dbfd-4e76-bb45-a0647a966b2a", + "parentId": "cd083b31-5939-46ac-bf90-8ac5b286a9e2", + "trampolineSecret": "9e13d1b602496871bb647b48e8ff8f15a91c07affb0a3599e995d470ac488715" +} +``` + +The `parentId` is important: this is the identifier used to link the MPP parts together. + +The `trampolineSecret` is also important: this is what prevents a malicious trampoline node from stealing money. + +Now that you have those, you can send the second part: + +```sh +eclair-cli sendtoroute --amountMsat=250000 --parentId=cd083b31-5939-46ac-bf90-8ac5b286a9e2 --trampolineSecret=9e13d1b602496871bb647b48e8ff8f15a91c07affb0a3599e995d470ac488715 --nodeIds=$ALICE_ID,$BOB_ID --trampolineNodes=$BOB_ID,$DAVE_ID --trampolineFeesMsat=100000 --trampolineCltvExpiry=450 --finalCltvExpiry=16 --invoice=lntb1500n1pwxx94fp... +``` + +Note that Alice didn't need to know about Carol. Bob will find the route to Dave through Carol on his own. That's the magic of trampoline! + +A couple gotchas: you need to make sure you specify the same `trampolineFeesMsat` and `trampolineCltvExpiry` as the first part. This is something we will improve if our users ask for a better API. + +You can then check the status of the payment with the `getsentinfo` command: + +```sh +eclair-cli getsentinfo --id=cd083b31-5939-46ac-bf90-8ac5b286a9e2 +``` + +Once Dave accepts the payment you should see all the details about the payment success (preimage, route, fees, etc). + \ No newline at end of file diff --git a/docs/Usage.md b/docs/Usage.md new file mode 100644 index 0000000000..235a44e8d3 --- /dev/null +++ b/docs/Usage.md @@ -0,0 +1,90 @@ +# Usage + +## Command line with eclair-cli + +### Windows user + +On windows, you will have to install Bash first. We recommend installing Git Bash which is packed in the [Git installer from git-scm](https://git-scm.com/downloads). + +Then you'll have to install jq: + +- Get the latest windows version from https://stedolan.github.io/jq/download/ +- Rename the `jq-win64.exe` file to `jq.exe` and move it to `C:/Users/your_name/bin` + +## eclair-cli installation + +- Download the eclair-cli file from [our sources](https://github.com/ACINQ/eclair/blob/master/eclair-core/eclair-cli) +- (optional) Move the file to `~/bin` +- Enable the [JSON API](https://github.com/ACINQ/eclair/wiki/API) in your `eclair.conf` settings. + +Run this command to list the available calls: + +```shell +./eclair-cli -p help +``` + +ℹ️ **Protip:** you can edit the `eclair-cli` file and save the API password/url so you don't have to set them every time. We will omit them in the examples below. + +Note that you may have to install jq first if it's not already installed on your machine: + +```shell +sudo apt-get install jq +``` + +## Example 1: open a channel with eclair-cli + +Your node listens on 8081. You want to open a 140 mBTC channel with `endurance.acinq.co` on Testnet. + +First connect to the endurance node: + +```shell +eclair-cli connect --uri=03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134@endurance.acinq.co:9735 +``` + +Then open a channel with endurance: + +```shell +eclair-cli open --nodeId=03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134 --fundingSatoshis=14000000 +``` + +This will broadcast a funding transaction to the bitcoin blockchain. +Once that transaction is confirmed, your lightning channel will be ready for off-chain payments. + +:warning: You should NEVER use RBF to bump the fees of that funding transaction, otherwise you won't be able to recover your funds unless your peer cooperates. If the transaction doesn't confirm, you should use CPFP to bump the fees. + +## Example 2: pay an invoice with eclair-cli + +```shell +eclair-cli payinvoice --invoice=lntb17u1pdthhsdpp5z5am8....... +``` + +## Example 3: generate a payment request for 1 mBTC + +```shell +eclair-cli createinvoice --amountMsat=100000000 --description="my first invoice" | jq .serialized +``` + +This command will return a lightning payment request, such as: + +```shell +lntb1m1pdthhh0pp5063r8hu6f6hk7tpauhgvl3nnf4ur3xntcnhujcz5w82yq7nhjuysdq6d4ujqenfwfehggrfdemx76trv5xqrrss6uxhewtmjkumpr7w6prkgttku76azfq7l8cx9v74pcv85hzyvs9n23dhu9u354xcqpnzey45ua3g2m4dywuw7udrt2sdsvjf3rawdqcpas9mah +``` + +You can check this invoice with the `parseinvoice` command: + +```shell +eclair-cli parseinvoice --invoice=lntb1m1pdthhh0pp5063r8hu6f6hk7tpauhgvl3nnf4ur3xntcnhujcz5w82yq7nhjuysdq6d4ujqenfwfehggrfdemx76trv5xqrrss6uxhewtmjkumpr7w6prkgttku76azfq7l8cx9v74pcv85hzyvs9n23dhu9u354xcqpnzey45ua3g2m4dywuw7udrt2sdsvjf3rawdqcpas9mah +``` + +Which will breakdown the invoice in human readable data. + +## Example 4: list local channels in NORMAL state +```shell +eclair-cli channels | jq '.[] | select(.state == "NORMAL")' +``` + +## Example 5: compute your total balance across all channels +We divide by `1000` because we want satoshis, not millisatoshis. +```shell +eclair-cli usablebalances | jq 'map(.canSend / 1000) | add' +``` \ No newline at end of file From 94015923827c2e49525382c8022155904f270c27 Mon Sep 17 00:00:00 2001 From: Thomas HUET <81159533+thomash-acinq@users.noreply.github.com> Date: Mon, 7 Feb 2022 09:51:38 +0100 Subject: [PATCH 016/121] Typed features (#2164) Give different types to init features / node announcement features / invoice features. We now accept more invoices (we would previously reject invoices if they required a feature that we don't support but that we know is irrelevant for invoices). --- docs/Configure.md | 4 +- eclair-core/src/main/resources/reference.conf | 2 +- .../main/scala/fr/acinq/eclair/Eclair.scala | 2 +- .../main/scala/fr/acinq/eclair/Features.scala | 69 +++---- .../scala/fr/acinq/eclair/NodeParams.scala | 23 ++- .../fr/acinq/eclair/channel/ChannelData.scala | 6 +- .../eclair/channel/ChannelFeatures.scala | 36 ++-- .../fr/acinq/eclair/channel/Helpers.scala | 8 +- .../main/scala/fr/acinq/eclair/io/Peer.scala | 8 +- .../fr/acinq/eclair/io/PeerConnection.scala | 4 +- .../acinq/eclair/payment/Bolt11Invoice.scala | 17 +- .../fr/acinq/eclair/payment/Invoice.scala | 4 +- .../payment/receive/MultiPartHandler.scala | 13 +- .../eclair/payment/send/PaymentError.scala | 4 +- .../payment/send/PaymentInitiator.scala | 2 +- .../remote/EclairInternalsSerializer.scala | 6 +- .../acinq/eclair/router/Announcements.scala | 15 +- .../scala/fr/acinq/eclair/router/Router.scala | 2 +- .../fr/acinq/eclair/router/Validation.scala | 2 +- .../channel/version0/ChannelCodecs0.scala | 2 +- .../channel/version0/ChannelTypes0.scala | 6 +- .../channel/version1/ChannelCodecs1.scala | 2 +- .../channel/version2/ChannelCodecs2.scala | 2 +- .../channel/version3/ChannelCodecs3.scala | 2 +- .../eclair/wire/protocol/ChannelTlv.scala | 2 +- .../protocol/LightningMessageCodecs.scala | 12 +- .../wire/protocol/LightningMessageTypes.scala | 8 +- .../scala/fr/acinq/eclair/FeaturesSpec.scala | 115 +++++------ .../scala/fr/acinq/eclair/StartupSpec.scala | 16 +- .../scala/fr/acinq/eclair/TestConstants.scala | 12 +- .../eclair/channel/ChannelFeaturesSpec.scala | 54 ++--- .../fr/acinq/eclair/channel/RestoreSpec.scala | 4 +- .../ChannelStateTestsHelperMethods.scala | 2 + .../channel/states/e/OfflineStateSpec.scala | 4 +- .../channel/states/h/ClosingStateSpec.scala | 4 +- .../acinq/eclair/io/PeerConnectionSpec.scala | 18 +- .../scala/fr/acinq/eclair/io/PeerSpec.scala | 8 +- .../fr/acinq/eclair/io/SwitchboardSpec.scala | 4 +- .../eclair/payment/Bolt11InvoiceSpec.scala | 188 ++++++++++++------ .../eclair/payment/MultiPartHandlerSpec.scala | 8 +- .../eclair/payment/PaymentInitiatorSpec.scala | 12 +- .../eclair/payment/PaymentPacketSpec.scala | 4 +- .../payment/relay/NodeRelayerSpec.scala | 4 +- .../eclair/router/AnnouncementsSpec.scala | 17 +- .../acinq/eclair/router/BaseRouterSpec.scala | 2 +- .../fr/acinq/eclair/router/RouterSpec.scala | 6 +- .../acinq/eclair/router/RoutingSyncSpec.scala | 2 +- .../channel/version1/ChannelCodecs1Spec.scala | 4 +- .../protocol/LightningMessageCodecsSpec.scala | 4 +- .../acinq/eclair/router/FrontRouterSpec.scala | 12 +- 50 files changed, 406 insertions(+), 361 deletions(-) diff --git a/docs/Configure.md b/docs/Configure.md index 62f1534ebb..a92000ce53 100644 --- a/docs/Configure.md +++ b/docs/Configure.md @@ -99,10 +99,10 @@ It's usually risky to activate non-default features or disable default features: Eclair supports per-peer features. Suppose you are connected to Alice and Bob, you can use a different set of features with Alice than the one you use with Bob. When experimenting with non-default features, we recommend using this to scope the peers you want to experiment with. -This is done with the `override-features` configuration parameter in your `eclair.conf`: +This is done with the `override-init-features` configuration parameter in your `eclair.conf`: ```conf -eclair.override-features = [ +eclair.override-init-features = [ { nodeId = "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f" features { diff --git a/eclair-core/src/main/resources/reference.conf b/eclair-core/src/main/resources/reference.conf index abdfc139e8..f9d469c774 100644 --- a/eclair-core/src/main/resources/reference.conf +++ b/eclair-core/src/main/resources/reference.conf @@ -60,7 +60,7 @@ eclair { trampoline_payment = disabled keysend = disabled } - override-features = [ // optional per-node features + override-init-features = [ // optional per-node features # { # nodeid = "02aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", # features { } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala index dffdb1ee92..6f98a0a5ec 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala @@ -60,7 +60,7 @@ import scala.collection.immutable.SortedMap import scala.concurrent.duration._ import scala.concurrent.{ExecutionContext, Future, Promise} -case class GetInfoResponse(version: String, nodeId: PublicKey, alias: String, color: String, features: Features, chainHash: ByteVector32, network: String, blockHeight: Int, publicAddresses: Seq[NodeAddress], onionAddress: Option[NodeAddress], instanceId: String) +case class GetInfoResponse(version: String, nodeId: PublicKey, alias: String, color: String, features: Features[FeatureScope], chainHash: ByteVector32, network: String, blockHeight: Int, publicAddresses: Seq[NodeAddress], onionAddress: Option[NodeAddress], instanceId: String) case class AuditResponse(sent: Seq[PaymentSent], received: Seq[PaymentReceived], relayed: Seq[PaymentRelayed]) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala index 4107f1bd46..65b3b349d6 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala @@ -20,6 +20,8 @@ import com.typesafe.config.Config import fr.acinq.eclair.FeatureSupport.{Mandatory, Optional} import scodec.bits.{BitVector, ByteVector} +import scala.jdk.CollectionConverters.CollectionHasAsScala + /** * Created by PM on 13/02/2017. */ @@ -61,9 +63,9 @@ trait InvoiceFeature extends FeatureScope case class UnknownFeature(bitIndex: Int) -case class Features(activated: Map[Feature, FeatureSupport], unknown: Set[UnknownFeature] = Set.empty) { +case class Features[T <: FeatureScope](activated: Map[Feature with T, FeatureSupport], unknown: Set[UnknownFeature] = Set.empty) { - def hasFeature(feature: Feature, support: Option[FeatureSupport] = None): Boolean = support match { + def hasFeature(feature: Feature with T, support: Option[FeatureSupport] = None): Boolean = support match { case Some(s) => activated.get(feature).contains(s) case None => activated.contains(feature) } @@ -71,7 +73,7 @@ case class Features(activated: Map[Feature, FeatureSupport], unknown: Set[Unknow def hasPluginFeature(feature: UnknownFeature): Boolean = unknown.contains(feature) /** NB: this method is not reflexive, see [[Features.areCompatible]] if you want symmetric validation. */ - def areSupported(remoteFeatures: Features): Boolean = { + def areSupported(remoteFeatures: Features[T]): Boolean = { // we allow unknown odd features (it's ok to be odd) val unknownFeaturesOk = remoteFeatures.unknown.forall(_.bitIndex % 2 == 1) // we verify that we activated every mandatory feature they require @@ -82,12 +84,13 @@ case class Features(activated: Map[Feature, FeatureSupport], unknown: Set[Unknow unknownFeaturesOk && knownFeaturesOk } - def initFeatures(): Features = Features(activated.collect { case (f: InitFeature, s) => (f: Feature, s) }, unknown) + def initFeatures(): Features[InitFeature] = Features[InitFeature](activated.collect { case (f: InitFeature, s) => (f, s) }, unknown) + + def nodeAnnouncementFeatures(): Features[NodeFeature] = Features[NodeFeature](activated.collect { case (f: NodeFeature, s) => (f, s) }, unknown) - def nodeAnnouncementFeatures(): Features = Features(activated.collect { case (f: NodeFeature, s) => (f: Feature, s) }, unknown) + def invoiceFeatures(): Features[InvoiceFeature] = Features[InvoiceFeature](activated.collect { case (f: InvoiceFeature, s) => (f, s) }, unknown) - // NB: we don't include unknown features in invoices, which means plugins cannot inject invoice features. - def invoiceFeatures(): Features = Features(activated.collect { case (f: InvoiceFeature, s) => (f: Feature, s) }) + def unscoped(): Features[FeatureScope] = Features[FeatureScope](activated.collect { case (f, s) => (f: Feature with FeatureScope, s) }, unknown) def toByteVector: ByteVector = { val activatedFeatureBytes = toByteVectorFromIndex(activated.map { case (feature, support) => feature.supportBit(support) }.toSet) @@ -113,47 +116,37 @@ case class Features(activated: Map[Feature, FeatureSupport], unknown: Set[Unknow object Features { - def empty = Features(Map.empty[Feature, FeatureSupport]) + def empty[T <: FeatureScope]: Features[T] = Features[T](Map.empty[Feature with T, FeatureSupport]) - def apply(features: (Feature, FeatureSupport)*): Features = Features(Map.from(features)) + def apply[T <: FeatureScope](features: (Feature with T, FeatureSupport)*): Features[T] = Features[T](Map.from(features)) - def apply(bytes: ByteVector): Features = apply(bytes.bits) + def apply(bytes: ByteVector): Features[FeatureScope] = apply(bytes.bits) - def apply(bits: BitVector): Features = { + def apply(bits: BitVector): Features[FeatureScope] = { val all = bits.toIndexedSeq.reverse.zipWithIndex.collect { case (true, idx) if knownFeatures.exists(_.optional == idx) => Right((knownFeatures.find(_.optional == idx).get, Optional)) case (true, idx) if knownFeatures.exists(_.mandatory == idx) => Right((knownFeatures.find(_.mandatory == idx).get, Mandatory)) case (true, idx) => Left(UnknownFeature(idx)) } - Features( + Features[FeatureScope]( activated = all.collect { case Right((feature, support)) => feature -> support }.toMap, unknown = all.collect { case Left(inf) => inf }.toSet ) } - /** expects to have a top level config block named "features" */ - def fromConfiguration(config: Config): Features = Features( - knownFeatures.flatMap { - feature => - getFeature(config, feature.rfcName) match { - case Some(support) => Some(feature -> support) - case _ => None - } - }.toMap) - - /** tries to extract the given feature name from the config, if successful returns its feature support */ - private def getFeature(config: Config, name: String): Option[FeatureSupport] = { - if (!config.hasPath(s"features.$name")) { - None - } else { - config.getString(s"features.$name") match { - case support if support == Mandatory.toString => Some(Mandatory) - case support if support == Optional.toString => Some(Optional) + def fromConfiguration[T <: FeatureScope](config: Config, validFeatures: Set[Feature with T]): Features[T] = Features[T]( + config.root().entrySet().asScala.flatMap { entry => + val featureName = entry.getKey + val feature: Feature with T = validFeatures.find(_.rfcName == featureName).getOrElse(throw new IllegalArgumentException(s"Invalid feature name ($featureName)")) + config.getString(featureName) match { + case support if support == Mandatory.toString => Some(feature -> Mandatory) + case support if support == Optional.toString => Some(feature -> Optional) case support if support == "disabled" => None case wrongSupport => throw new IllegalArgumentException(s"Wrong support specified ($wrongSupport)") } - } - } + }.toMap) + + def fromConfiguration(config: Config): Features[FeatureScope] = fromConfiguration[FeatureScope](config, knownFeatures) case object DataLossProtect extends Feature with InitFeature with NodeFeature { val rfcName = "option_data_loss_protect" @@ -249,7 +242,7 @@ object Features { val mandatory = 54 } - val knownFeatures: Set[Feature] = Set( + val knownFeatures: Set[Feature with FeatureScope] = Set( DataLossProtect, InitialRoutingSync, UpfrontShutdownScript, @@ -285,16 +278,16 @@ object Features { case class FeatureException(message: String) extends IllegalArgumentException(message) - def validateFeatureGraph(features: Features): Option[FeatureException] = featuresDependency.collectFirst { - case (feature, dependencies) if features.hasFeature(feature) && dependencies.exists(d => !features.hasFeature(d)) => - FeatureException(s"$feature is set but is missing a dependency (${dependencies.filter(d => !features.hasFeature(d)).mkString(" and ")})") + def validateFeatureGraph[T <: FeatureScope](features: Features[T]): Option[FeatureException] = featuresDependency.collectFirst { + case (feature, dependencies) if features.unscoped().hasFeature(feature) && dependencies.exists(d => !features.unscoped().hasFeature(d)) => + FeatureException(s"$feature is set but is missing a dependency (${dependencies.filter(d => !features.unscoped().hasFeature(d)).mkString(" and ")})") } /** Returns true if both feature sets are compatible. */ - def areCompatible(ours: Features, theirs: Features): Boolean = ours.areSupported(theirs) && theirs.areSupported(ours) + def areCompatible[T <: FeatureScope](ours: Features[T], theirs: Features[T]): Boolean = ours.areSupported(theirs) && theirs.areSupported(ours) /** returns true if both have at least optional support */ - def canUseFeature(localFeatures: Features, remoteFeatures: Features, feature: Feature): Boolean = { + def canUseFeature[T <: FeatureScope](localFeatures: Features[T], remoteFeatures: Features[T], feature: Feature with T): Boolean = { localFeatures.hasFeature(feature) && remoteFeatures.hasFeature(feature) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala index e625da73b7..94f9068fd4 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala @@ -58,8 +58,8 @@ case class NodeParams(nodeKeyManager: NodeKeyManager, color: Color, publicAddresses: List[NodeAddress], torAddress_opt: Option[NodeAddress], - features: Features, - private val overrideFeatures: Map[PublicKey, Features], + features: Features[FeatureScope], + private val overrideInitFeatures: Map[PublicKey, Features[InitFeature]], syncWhitelist: Set[PublicKey], pluginParams: Seq[PluginParams], channelConf: ChannelConf, @@ -91,7 +91,7 @@ case class NodeParams(nodeKeyManager: NodeKeyManager, def currentBlockHeight: BlockHeight = BlockHeight(blockHeight.get) /** Returns the features that should be used in our init message with the given peer. */ - def initFeaturesFor(nodeId: PublicKey): Features = overrideFeatures.getOrElse(nodeId, features).initFeatures() + def initFeaturesFor(nodeId: PublicKey): Features[InitFeature] = overrideInitFeatures.getOrElse(nodeId, features).initFeatures() } object NodeParams extends Logging { @@ -224,6 +224,7 @@ object NodeParams extends Logging { "watch-spent-window" -> "router.watch-spent-window", // v0.7.1 "payment-request-expiry" -> "invoice-expiry", + "override-features" -> "override-init-features", ) deprecatedKeyPaths.foreach { case (old, new_) => require(!config.hasPath(old), s"configuration key '$old' has been replaced by '$new_'") @@ -268,7 +269,7 @@ object NodeParams extends Logging { val nodeAlias = config.getString("node-alias") require(nodeAlias.getBytes("UTF-8").length <= 32, "invalid alias, too long (max allowed 32 bytes)") - def validateFeatures(features: Features): Unit = { + def validateFeatures(features: Features[FeatureScope]): Unit = { val featuresErr = Features.validateFeatureGraph(features) require(featuresErr.isEmpty, featuresErr.map(_.message)) require(features.hasFeature(Features.VariableLengthOnion, Some(FeatureSupport.Mandatory)), s"${Features.VariableLengthOnion.rfcName} must be enabled and mandatory") @@ -278,7 +279,7 @@ object NodeParams extends Logging { } val pluginMessageParams = pluginParams.collect { case p: CustomFeaturePlugin => p } - val features = Features.fromConfiguration(config) + val features = Features.fromConfiguration(config.getConfig("features")) validateFeatures(features) require(pluginMessageParams.forall(_.feature.mandatory > 128), "Plugin mandatory feature bit is too low, must be > 128") @@ -288,13 +289,13 @@ object NodeParams extends Logging { require(Features.knownFeatures.map(_.mandatory).intersect(pluginFeatureSet).isEmpty, "Plugin feature bit overlaps with known feature bit") require(pluginFeatureSet.size == pluginMessageParams.size, "Duplicate plugin feature bits found") - val coreAndPluginFeatures = features.copy(unknown = features.unknown ++ pluginMessageParams.map(_.pluginFeature)) + val coreAndPluginFeatures: Features[FeatureScope] = features.copy(unknown = features.unknown ++ pluginMessageParams.map(_.pluginFeature)) - val overrideFeatures: Map[PublicKey, Features] = config.getConfigList("override-features").asScala.map { e => + val overrideInitFeatures: Map[PublicKey, Features[InitFeature]] = config.getConfigList("override-init-features").asScala.map { e => val p = PublicKey(ByteVector.fromValidHex(e.getString("nodeid"))) - val f = Features.fromConfiguration(e) - validateFeatures(f) - p -> f.copy(unknown = f.unknown ++ pluginMessageParams.map(_.pluginFeature)) + val f = Features.fromConfiguration[InitFeature](e.getConfig("features"), Features.knownFeatures.collect { case f: Feature with InitFeature => f }) + validateFeatures(f.unscoped()) + p -> (f.copy(unknown = f.unknown ++ pluginMessageParams.map(_.pluginFeature)): Features[InitFeature]) }.toMap val syncWhitelist: Set[PublicKey] = config.getStringList("sync-whitelist").asScala.map(s => PublicKey(ByteVector.fromValidHex(s))).toSet @@ -398,7 +399,7 @@ object NodeParams extends Logging { torAddress_opt = torAddress_opt, features = coreAndPluginFeatures, pluginParams = pluginParams, - overrideFeatures = overrideFeatures, + overrideInitFeatures = overrideInitFeatures, syncWhitelist = syncWhitelist, channelConf = ChannelConf( channelFlags = channelFlags, diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala index 441b6bddc5..b9f33aabfd 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala @@ -24,7 +24,7 @@ import fr.acinq.eclair.payment.OutgoingPaymentPacket.Upstream import fr.acinq.eclair.transactions.CommitmentSpec import fr.acinq.eclair.transactions.Transactions._ import fr.acinq.eclair.wire.protocol.{AcceptChannel, ChannelAnnouncement, ChannelReestablish, ChannelUpdate, ClosingSigned, FailureMessage, FundingCreated, FundingLocked, FundingSigned, Init, OnionRoutingPacket, OpenChannel, Shutdown, UpdateAddHtlc, UpdateFailHtlc, UpdateFailMalformedHtlc, UpdateFulfillHtlc} -import fr.acinq.eclair.{BlockHeight, CltvExpiry, CltvExpiryDelta, Features, MilliSatoshi, ShortChannelId, UInt64} +import fr.acinq.eclair.{BlockHeight, CltvExpiry, CltvExpiryDelta, Features, InitFeature, MilliSatoshi, ShortChannelId, UInt64} import scodec.bits.ByteVector import java.util.UUID @@ -472,7 +472,7 @@ case class LocalParams(nodeId: PublicKey, isFunder: Boolean, defaultFinalScriptPubKey: ByteVector, walletStaticPaymentBasepoint: Option[PublicKey], - initFeatures: Features) + initFeatures: Features[InitFeature]) /** * @param initFeatures see [[LocalParams.initFeatures]] @@ -489,7 +489,7 @@ case class RemoteParams(nodeId: PublicKey, paymentBasepoint: PublicKey, delayedPaymentBasepoint: PublicKey, htlcBasepoint: PublicKey, - initFeatures: Features, + initFeatures: Features[InitFeature], shutdownScript: Option[ByteVector]) case class ChannelFlags(announceChannel: Boolean) { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelFeatures.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelFeatures.scala index 5a8408294e..3369ff4391 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelFeatures.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelFeatures.scala @@ -17,7 +17,7 @@ package fr.acinq.eclair.channel import fr.acinq.eclair.transactions.Transactions.{CommitmentFormat, DefaultCommitmentFormat, UnsafeLegacyAnchorOutputsCommitmentFormat, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat} -import fr.acinq.eclair.{Feature, FeatureSupport, Features} +import fr.acinq.eclair.{Feature, FeatureScope, FeatureSupport, Features, InitFeature} /** * Created by t-bast on 24/06/2021. @@ -28,7 +28,7 @@ import fr.acinq.eclair.{Feature, FeatureSupport, Features} * Even if one of these features is later disabled at the connection level, it will still apply to the channel until the * channel is upgraded or closed. */ -case class ChannelFeatures(features: Set[Feature]) { +case class ChannelFeatures(features: Set[Feature with FeatureScope]) { val channelType: SupportedChannelType = { if (hasFeature(Features.AnchorOutputsZeroFeeHtlcTx)) { @@ -45,7 +45,7 @@ case class ChannelFeatures(features: Set[Feature]) { val paysDirectlyToWallet: Boolean = channelType.paysDirectlyToWallet val commitmentFormat: CommitmentFormat = channelType.commitmentFormat - def hasFeature(feature: Feature): Boolean = features.contains(feature) + def hasFeature(feature: Feature with FeatureScope): Boolean = features.contains(feature) override def toString: String = features.mkString(",") @@ -53,13 +53,13 @@ case class ChannelFeatures(features: Set[Feature]) { object ChannelFeatures { - def apply(features: Feature*): ChannelFeatures = ChannelFeatures(Set.from(features)) + def apply(features: (Feature with FeatureScope)*): ChannelFeatures = ChannelFeatures(Set.from(features)) /** Enrich the channel type with other permanent features that will be applied to the channel. */ - def apply(channelType: ChannelType, localFeatures: Features, remoteFeatures: Features): ChannelFeatures = { + def apply(channelType: ChannelType, localFeatures: Features[InitFeature], remoteFeatures: Features[InitFeature]): ChannelFeatures = { // NB: we don't include features that can be safely activated/deactivated without impacting the channel's operation, // such as option_dataloss_protect or option_shutdown_anysegwit. - val availableFeatures: Seq[Feature] = Seq(Features.Wumbo, Features.UpfrontShutdownScript).filter(f => Features.canUseFeature(localFeatures, remoteFeatures, f)) + val availableFeatures: Seq[Feature with FeatureScope] = Seq(Features.Wumbo, Features.UpfrontShutdownScript).filter(f => Features.canUseFeature(localFeatures, remoteFeatures, f)) val allFeatures = channelType.features.toSeq ++ availableFeatures ChannelFeatures(allFeatures: _*) } @@ -69,7 +69,7 @@ object ChannelFeatures { /** A channel type is a specific set of even feature bits that represent persistent channel features as defined in Bolt 2. */ sealed trait ChannelType { /** Features representing that channel type. */ - def features: Set[Feature] + def features: Set[Feature with InitFeature] } sealed trait SupportedChannelType extends ChannelType { @@ -84,45 +84,45 @@ object ChannelTypes { // @formatter:off case object Standard extends SupportedChannelType { - override def features: Set[Feature] = Set.empty + override def features: Set[Feature with InitFeature] = Set.empty override def paysDirectlyToWallet: Boolean = false override def commitmentFormat: CommitmentFormat = DefaultCommitmentFormat override def toString: String = "standard" } case object StaticRemoteKey extends SupportedChannelType { - override def features: Set[Feature] = Set(Features.StaticRemoteKey) + override def features: Set[Feature with InitFeature] = Set(Features.StaticRemoteKey) override def paysDirectlyToWallet: Boolean = true override def commitmentFormat: CommitmentFormat = DefaultCommitmentFormat override def toString: String = "static_remotekey" } case object AnchorOutputs extends SupportedChannelType { - override def features: Set[Feature] = Set(Features.StaticRemoteKey, Features.AnchorOutputs) + override def features: Set[Feature with InitFeature] = Set(Features.StaticRemoteKey, Features.AnchorOutputs) override def paysDirectlyToWallet: Boolean = false override def commitmentFormat: CommitmentFormat = UnsafeLegacyAnchorOutputsCommitmentFormat override def toString: String = "anchor_outputs" } case object AnchorOutputsZeroFeeHtlcTx extends SupportedChannelType { - override def features: Set[Feature] = Set(Features.StaticRemoteKey, Features.AnchorOutputsZeroFeeHtlcTx) + override def features: Set[Feature with InitFeature] = Set(Features.StaticRemoteKey, Features.AnchorOutputsZeroFeeHtlcTx) override def paysDirectlyToWallet: Boolean = false override def commitmentFormat: CommitmentFormat = ZeroFeeHtlcTxAnchorOutputsCommitmentFormat override def toString: String = "anchor_outputs_zero_fee_htlc_tx" } - case class UnsupportedChannelType(featureBits: Features) extends ChannelType { - override def features: Set[Feature] = featureBits.activated.keySet + case class UnsupportedChannelType(featureBits: Features[InitFeature]) extends ChannelType { + override def features: Set[Feature with InitFeature] = featureBits.activated.keySet override def toString: String = s"0x${featureBits.toByteVector.toHex}" } // @formatter:on - private val features2ChannelType: Map[Features, SupportedChannelType] = Set(Standard, StaticRemoteKey, AnchorOutputs, AnchorOutputsZeroFeeHtlcTx) - .map(channelType => Features(channelType.features.map(_ -> FeatureSupport.Mandatory).toMap) -> channelType) + private val features2ChannelType: Map[Features[InitFeature], SupportedChannelType] = Set(Standard, StaticRemoteKey, AnchorOutputs, AnchorOutputsZeroFeeHtlcTx) + .map(channelType => Features[InitFeature](channelType.features.map(_ -> FeatureSupport.Mandatory).toMap) -> channelType) .toMap // NB: Bolt 2: features must exactly match in order to identify a channel type. - def fromFeatures(features: Features): ChannelType = features2ChannelType.getOrElse(features, UnsupportedChannelType(features)) + def fromFeatures(features: Features[InitFeature]): ChannelType = features2ChannelType.getOrElse(features, UnsupportedChannelType(features)) /** Pick the channel type based on local and remote feature bits, as defined by the spec. */ - def defaultFromFeatures(localFeatures: Features, remoteFeatures: Features): SupportedChannelType = { + def defaultFromFeatures(localFeatures: Features[InitFeature], remoteFeatures: Features[InitFeature]): SupportedChannelType = { if (Features.canUseFeature(localFeatures, remoteFeatures, Features.AnchorOutputsZeroFeeHtlcTx)) { AnchorOutputsZeroFeeHtlcTx } else if (Features.canUseFeature(localFeatures, remoteFeatures, Features.AnchorOutputs)) { @@ -135,7 +135,7 @@ object ChannelTypes { } /** Check if a given channel type is compatible with our features. */ - def areCompatible(localFeatures: Features, remoteChannelType: ChannelType): Option[SupportedChannelType] = remoteChannelType match { + def areCompatible(localFeatures: Features[InitFeature], remoteChannelType: ChannelType): Option[SupportedChannelType] = remoteChannelType match { case _: UnsupportedChannelType => None // We ensure that we support the features necessary for this channel type. case proposedChannelType: SupportedChannelType => diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala index 7564fe6d8f..c1c66eab05 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala @@ -79,7 +79,7 @@ object Helpers { channelConf.minDepthBlocks.max(blocksToReachFunding) } - def extractShutdownScript(channelId: ByteVector32, localFeatures: Features, remoteFeatures: Features, upfrontShutdownScript_opt: Option[ByteVector]): Either[ChannelException, Option[ByteVector]] = { + def extractShutdownScript(channelId: ByteVector32, localFeatures: Features[InitFeature], remoteFeatures: Features[InitFeature], upfrontShutdownScript_opt: Option[ByteVector]): Either[ChannelException, Option[ByteVector]] = { val canUseUpfrontShutdownScript = Features.canUseFeature(localFeatures, remoteFeatures, Features.UpfrontShutdownScript) val canUseAnySegwit = Features.canUseFeature(localFeatures, remoteFeatures, Features.ShutdownAnySegwit) extractShutdownScript(channelId, canUseUpfrontShutdownScript, canUseAnySegwit, upfrontShutdownScript_opt) @@ -99,7 +99,7 @@ object Helpers { /** * Called by the fundee */ - def validateParamsFundee(nodeParams: NodeParams, channelType: SupportedChannelType, localFeatures: Features, open: OpenChannel, remoteNodeId: PublicKey, remoteFeatures: Features): Either[ChannelException, (ChannelFeatures, Option[ByteVector])] = { + def validateParamsFundee(nodeParams: NodeParams, channelType: SupportedChannelType, localFeatures: Features[InitFeature], open: OpenChannel, remoteNodeId: PublicKey, remoteFeatures: Features[InitFeature]): Either[ChannelException, (ChannelFeatures, Option[ByteVector])] = { // BOLT #2: if the chain_hash value, within the open_channel, message is set to a hash of a chain that is unknown to the receiver: // MUST reject the channel. if (nodeParams.chainHash != open.chainHash) return Left(InvalidChainHash(open.temporaryChannelId, local = nodeParams.chainHash, remote = open.chainHash)) @@ -154,7 +154,7 @@ object Helpers { /** * Called by the funder */ - def validateParamsFunder(nodeParams: NodeParams, channelType: SupportedChannelType, localFeatures: Features, remoteFeatures: Features, open: OpenChannel, accept: AcceptChannel): Either[ChannelException, (ChannelFeatures, Option[ByteVector])] = { + def validateParamsFunder(nodeParams: NodeParams, channelType: SupportedChannelType, localFeatures: Features[InitFeature], remoteFeatures: Features[InitFeature], open: OpenChannel, accept: AcceptChannel): Either[ChannelException, (ChannelFeatures, Option[ByteVector])] = { accept.channelType_opt match { case Some(theirChannelType) if accept.channelType_opt != open.channelType_opt => // if channel_type is set, and channel_type was set in open_channel, and they are not equal types: MUST reject the channel. @@ -225,7 +225,7 @@ object Helpers { } def makeAnnouncementSignatures(nodeParams: NodeParams, commitments: Commitments, shortChannelId: ShortChannelId): AnnouncementSignatures = { - val features = Features.empty // empty features for now + val features = Features.empty[FeatureScope] // empty features for now val fundingPubKey = nodeParams.channelKeyManager.fundingPublicKey(commitments.localParams.fundingKeyPath) val witness = Announcements.generateChannelAnnouncementWitness( nodeParams.chainHash, diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala index 45b248dd97..5aae20924a 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala @@ -357,7 +357,7 @@ class Peer(val nodeParams: NodeParams, remoteNodeId: PublicKey, wallet: OnChainA s(e) } - def createNewChannel(nodeParams: NodeParams, initFeatures: Features, channelType: SupportedChannelType, funder: Boolean, fundingAmount: Satoshi, origin_opt: Option[ActorRef]): (ActorRef, LocalParams) = { + def createNewChannel(nodeParams: NodeParams, initFeatures: Features[InitFeature], channelType: SupportedChannelType, funder: Boolean, fundingAmount: Satoshi, origin_opt: Option[ActorRef]): (ActorRef, LocalParams) = { val (finalScript, walletStaticPaymentBasepoint) = if (channelType.paysDirectlyToWallet) { val walletKey = Helpers.getWalletPaymentBasepoint(wallet)(ExecutionContext.Implicits.global) (Script.write(Script.pay2wpkh(walletKey)), Some(walletKey)) @@ -447,8 +447,8 @@ object Peer { case class DisconnectedData(channels: Map[FinalChannelId, ActorRef]) extends Data case class ConnectedData(address: InetSocketAddress, peerConnection: ActorRef, localInit: protocol.Init, remoteInit: protocol.Init, channels: Map[ChannelId, ActorRef]) extends Data { val connectionInfo: ConnectionInfo = ConnectionInfo(address, peerConnection, localInit, remoteInit) - def localFeatures: Features = localInit.features - def remoteFeatures: Features = remoteInit.features + def localFeatures: Features[InitFeature] = localInit.features + def remoteFeatures: Features[InitFeature] = remoteInit.features } sealed trait State @@ -501,7 +501,7 @@ object Peer { case class RelayOnionMessage(messageId: ByteVector32, msg: OnionMessage, replyTo_opt: Option[typed.ActorRef[Status]]) // @formatter:on - def makeChannelParams(nodeParams: NodeParams, initFeatures: Features, defaultFinalScriptPubkey: ByteVector, walletStaticPaymentBasepoint: Option[PublicKey], isFunder: Boolean, fundingAmount: Satoshi): LocalParams = { + def makeChannelParams(nodeParams: NodeParams, initFeatures: Features[InitFeature], defaultFinalScriptPubkey: ByteVector, walletStaticPaymentBasepoint: Option[PublicKey], isFunder: Boolean, fundingAmount: Satoshi): LocalParams = { LocalParams( nodeParams.nodeId, nodeParams.channelKeyManager.newFundingKeyPath(isFunder), // we make sure that funder and fundee key path end differently diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/PeerConnection.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/PeerConnection.scala index e470842c72..4b32706a65 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/io/PeerConnection.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/PeerConnection.scala @@ -28,7 +28,7 @@ import fr.acinq.eclair.remote.EclairInternalsSerializer.RemoteTypes import fr.acinq.eclair.router.Router._ import fr.acinq.eclair.wire.protocol import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{FSMDiagnosticActorLogging, Features, Logs, TimestampMilli, TimestampSecond} +import fr.acinq.eclair.{FSMDiagnosticActorLogging, FeatureScope, Features, InitFeature, Logs, TimestampMilli, TimestampSecond} import scodec.Attempt import scodec.bits.ByteVector @@ -556,7 +556,7 @@ object PeerConnection { def outgoing: Boolean = remoteNodeId_opt.isDefined // if this is an outgoing connection, we know the node id in advance } case class Authenticated(peerConnection: ActorRef, remoteNodeId: PublicKey) extends RemoteTypes - case class InitializeConnection(peer: ActorRef, chainHash: ByteVector32, features: Features, doSync: Boolean) extends RemoteTypes + case class InitializeConnection(peer: ActorRef, chainHash: ByteVector32, features: Features[InitFeature], doSync: Boolean) extends RemoteTypes case class ConnectionReady(peerConnection: ActorRef, remoteNodeId: PublicKey, address: InetSocketAddress, outgoing: Boolean, localInit: protocol.Init, remoteInit: protocol.Init) extends RemoteTypes sealed trait ConnectionResult extends RemoteTypes diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Bolt11Invoice.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Bolt11Invoice.scala index 87d7411368..cfebfbf069 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Bolt11Invoice.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Bolt11Invoice.scala @@ -18,7 +18,7 @@ package fr.acinq.eclair.payment import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} import fr.acinq.bitcoin.{Base58, Base58Check, Bech32, Block, ByteVector32, ByteVector64, Crypto} -import fr.acinq.eclair.{CltvExpiryDelta, FeatureSupport, Features, MilliSatoshi, MilliSatoshiLong, ShortChannelId, TimestampSecond, randomBytes32} +import fr.acinq.eclair.{CltvExpiryDelta, FeatureScope, FeatureSupport, Features, InvoiceFeature, MilliSatoshi, MilliSatoshiLong, ShortChannelId, TimestampSecond, randomBytes32} import scodec.bits.{BitVector, ByteOrdering, ByteVector} import scodec.codecs.{list, ubyte} import scodec.{Codec, Err} @@ -93,7 +93,7 @@ case class Bolt11Invoice(prefix: String, amount_opt: Option[MilliSatoshi], creat case cltvExpiry: Bolt11Invoice.MinFinalCltvExpiry => cltvExpiry.toCltvExpiryDelta } - lazy val features: Features = tags.collectFirst { case f: InvoiceFeatures => f.features }.getOrElse(Features(BitVector.empty)) + lazy val features: Features[InvoiceFeature] = tags.collectFirst { case f: InvoiceFeatures => f.features.invoiceFeatures() }.getOrElse(Features.empty[InvoiceFeature]) /** * @return the hash of this payment invoice @@ -141,7 +141,7 @@ object Bolt11Invoice { Block.LivenetGenesisBlock.hash -> "lnbc" ) - val defaultFeatures: Features = Features((Features.VariableLengthOnion, FeatureSupport.Mandatory), (Features.PaymentSecret, FeatureSupport.Mandatory)) + val defaultFeatures: Features[InvoiceFeature] = Features((Features.VariableLengthOnion, FeatureSupport.Mandatory), (Features.PaymentSecret, FeatureSupport.Mandatory)) def apply(chainHash: ByteVector32, amount: Option[MilliSatoshi], @@ -155,7 +155,7 @@ object Bolt11Invoice { timestamp: TimestampSecond = TimestampSecond.now(), paymentSecret: ByteVector32 = randomBytes32(), paymentMetadata: Option[ByteVector] = None, - features: Features = defaultFeatures): Bolt11Invoice = { + features: Features[InvoiceFeature] = defaultFeatures): Bolt11Invoice = { require(features.hasFeature(Features.PaymentSecret, Some(FeatureSupport.Mandatory)), "invoices must require a payment secret") val prefix = prefixes(chainHash) val tags = { @@ -167,7 +167,8 @@ object Bolt11Invoice { fallbackAddress.map(FallbackAddress(_)), expirySeconds.map(Expiry(_)), Some(MinFinalCltvExpiry(minFinalCltvExpiryDelta.toInt)), - Some(InvoiceFeatures(features)) + // We want to keep invoices as small as possible, so we explicitly remove unknown features. + Some(InvoiceFeatures(features.copy(unknown = Set.empty).unscoped())) ).flatten val routingInfoTags = extraHops.map(RoutingInfo) defaultTags ++ routingInfoTags @@ -305,7 +306,7 @@ object Bolt11Invoice { * This returns a bitvector with the minimum size necessary to encode the features, left padded to have a length (in * bits) that is a multiple of 5. */ - def features2bits(features: Features): BitVector = leftPaddedBits(features.toByteVector.bits) + def features2bits[T <: FeatureScope](features: Features[T]): BitVector = leftPaddedBits(features.toByteVector.bits) private def leftPaddedBits(bits: BitVector): BitVector = { var highest = -1 @@ -371,7 +372,7 @@ object Bolt11Invoice { /** * Features supported or required for receiving this payment. */ - case class InvoiceFeatures(features: Features) extends TaggedField + case class InvoiceFeatures(features: Features[FeatureScope]) extends TaggedField object Codecs { @@ -414,7 +415,7 @@ object Bolt11Invoice { .typecase(2, dataCodec(bits).as[UnknownTag2]) .typecase(3, dataCodec(listOfN(extraHopsLengthCodec, extraHopCodec)).as[RoutingInfo]) .typecase(4, dataCodec(bits).as[UnknownTag4]) - .typecase(5, dataCodec(bits).xmap[Features](Features(_), features2bits).as[InvoiceFeatures]) + .typecase(5, dataCodec(bits).xmap[Features[FeatureScope]](Features(_), features2bits).as[InvoiceFeatures]) .typecase(6, dataCodec(bits).as[Expiry]) .typecase(7, dataCodec(bits).as[UnknownTag7]) .typecase(8, dataCodec(bits).as[UnknownTag8]) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Invoice.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Invoice.scala index 78c0804612..c721b57d7c 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Invoice.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Invoice.scala @@ -18,7 +18,7 @@ package fr.acinq.eclair.payment import fr.acinq.bitcoin.ByteVector32 import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.eclair.{CltvExpiryDelta, Features, MilliSatoshi, TimestampSecond} +import fr.acinq.eclair.{CltvExpiryDelta, Features, InvoiceFeature, MilliSatoshi, TimestampSecond} import scodec.bits.ByteVector import scala.concurrent.duration.FiniteDuration @@ -44,7 +44,7 @@ trait Invoice { val minFinalCltvExpiryDelta: Option[CltvExpiryDelta] - val features: Features + val features: Features[InvoiceFeature] def isExpired(): Boolean = createdAt + relativeExpiry.toSeconds <= TimestampSecond.now() diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/MultiPartHandler.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/MultiPartHandler.scala index 40f6c9290a..8aa23167f4 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/MultiPartHandler.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/MultiPartHandler.scala @@ -25,12 +25,13 @@ import akka.event.{DiagnosticLoggingAdapter, LoggingAdapter} import fr.acinq.bitcoin.{ByteVector32, Crypto} import fr.acinq.eclair.channel.{CMD_FAIL_HTLC, CMD_FULFILL_HTLC, RES_SUCCESS} import fr.acinq.eclair.db._ -import fr.acinq.eclair.payment.Bolt11Invoice.ExtraHop import fr.acinq.eclair.payment.Monitoring.{Metrics, Tags} -import fr.acinq.eclair.payment.{Bolt11Invoice, IncomingPaymentPacket, Invoice, PaymentMetadataReceived, PaymentReceived} +import fr.acinq.eclair.payment.{IncomingPaymentPacket, PaymentMetadataReceived, PaymentReceived, Invoice} import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{FeatureSupport, Features, Logs, MilliSatoshi, NodeParams, randomBytes32} +import fr.acinq.eclair.{FeatureSupport, Features, InvoiceFeature, Logs, MilliSatoshi, NodeParams, randomBytes32} import scodec.bits.HexStringSyntax +import fr.acinq.eclair.payment.Bolt11Invoice.ExtraHop +import fr.acinq.eclair.payment.Bolt11Invoice import scala.util.{Failure, Success, Try} @@ -93,9 +94,9 @@ class MultiPartHandler(nodeParams: NodeParams, register: ActorRef, db: IncomingP val paymentHash = Crypto.sha256(paymentPreimage) val desc = Left("Donation") val features = if (nodeParams.features.hasFeature(Features.BasicMultiPartPayment)) { - Features(Features.BasicMultiPartPayment -> FeatureSupport.Optional, Features.PaymentSecret -> FeatureSupport.Mandatory, Features.VariableLengthOnion -> FeatureSupport.Mandatory) + Features[InvoiceFeature](Features.BasicMultiPartPayment -> FeatureSupport.Optional, Features.PaymentSecret -> FeatureSupport.Mandatory, Features.VariableLengthOnion -> FeatureSupport.Mandatory) } else { - Features(Features.PaymentSecret -> FeatureSupport.Mandatory, Features.VariableLengthOnion -> FeatureSupport.Mandatory) + Features[InvoiceFeature](Features.PaymentSecret -> FeatureSupport.Mandatory, Features.VariableLengthOnion -> FeatureSupport.Mandatory) } // Insert a fake invoice and then restart the incoming payment handler val invoice = Bolt11Invoice(nodeParams.chainHash, amount, paymentHash, nodeParams.privateKey, desc, nodeParams.channelConf.minFinalExpiryDelta, paymentSecret = p.payload.paymentSecret, features = features) @@ -231,7 +232,7 @@ object MultiPartHandler { val expirySeconds = expirySeconds_opt.getOrElse(nodeParams.invoiceExpiry.toSeconds) val paymentMetadata = hex"2a" val invoiceFeatures = if (nodeParams.enableTrampolinePayment) { - Features(nodeParams.features.invoiceFeatures().activated + (Features.TrampolinePayment -> FeatureSupport.Optional)) + Features[InvoiceFeature](nodeParams.features.invoiceFeatures().activated + (Features.TrampolinePayment -> FeatureSupport.Optional)) } else { nodeParams.features.invoiceFeatures() } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentError.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentError.scala index 921a8c5bb5..7248019cd3 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentError.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentError.scala @@ -16,7 +16,7 @@ package fr.acinq.eclair.payment.send -import fr.acinq.eclair.Features +import fr.acinq.eclair.{Features, InvoiceFeature} sealed trait PaymentError extends Throwable @@ -25,7 +25,7 @@ object PaymentError { // @formatter:off sealed trait InvalidInvoice extends PaymentError /** The invoice contains a feature we don't support. */ - case class UnsupportedFeatures(features: Features) extends InvalidInvoice { override def getMessage: String = s"unsupported invoice features: ${features.toByteVector.toHex}" } + case class UnsupportedFeatures(features: Features[InvoiceFeature]) extends InvalidInvoice { override def getMessage: String = s"unsupported invoice features: ${features.toByteVector.toHex}" } /** The invoice is missing a payment secret. */ case object PaymentSecretMissing extends InvalidInvoice { override def getMessage: String = "invalid invoice: payment secret is missing" } // @formatter:on diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentInitiator.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentInitiator.scala index 4432941de5..b5d273d9c0 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentInitiator.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentInitiator.scala @@ -54,7 +54,7 @@ class PaymentInitiator(nodeParams: NodeParams, outgoingPaymentFactory: PaymentIn val paymentCfg = SendPaymentConfig(paymentId, paymentId, r.externalId, r.paymentHash, r.recipientAmount, r.recipientNodeId, Upstream.Local(paymentId), Some(r.invoice), storeInDb = true, publishEvent = true, recordPathFindingMetrics = true, Nil) val finalExpiry = r.finalExpiry(nodeParams.currentBlockHeight) r.invoice.paymentSecret match { - case _ if !nodeParams.features.areSupported(r.invoice.features) => + case _ if !nodeParams.features.invoiceFeatures().areSupported(r.invoice.features) => sender() ! PaymentFailed(paymentId, r.paymentHash, LocalFailure(r.recipientAmount, Nil, UnsupportedFeatures(r.invoice.features)) :: Nil) case None => sender() ! PaymentFailed(paymentId, r.paymentHash, LocalFailure(r.recipientAmount, Nil, PaymentSecretMissing) :: Nil) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/remote/EclairInternalsSerializer.scala b/eclair-core/src/main/scala/fr/acinq/eclair/remote/EclairInternalsSerializer.scala index 615b516361..1ec53767c5 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/remote/EclairInternalsSerializer.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/remote/EclairInternalsSerializer.scala @@ -31,7 +31,7 @@ import fr.acinq.eclair.wire.protocol.CommonCodecs._ import fr.acinq.eclair.wire.protocol.LightningMessageCodecs._ import fr.acinq.eclair.wire.protocol.QueryChannelRangeTlv.queryFlagsCodec import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{CltvExpiryDelta, Features} +import fr.acinq.eclair.{CltvExpiryDelta, FeatureScope, Features, InitFeature} import scodec._ import scodec.codecs._ @@ -98,7 +98,7 @@ object EclairInternalsSerializer { ("channelQueryChunkSize" | int32) :: ("pathFindingExperimentConf" | pathFindingExperimentConfCodec)).as[RouterConf] - val overrideFeaturesListCodec: Codec[List[(PublicKey, Features)]] = listOfN(uint16, publicKey ~ variableSizeBytes(uint16, featuresCodec)) + val overrideFeaturesListCodec: Codec[List[(PublicKey, Features[FeatureScope])]] = listOfN(uint16, publicKey ~ variableSizeBytes(uint16, featuresCodec)) val peerConnectionConfCodec: Codec[PeerConnection.Conf] = ( ("authTimeout" | finiteDurationCodec) :: @@ -146,7 +146,7 @@ object EclairInternalsSerializer { def initializeConnectionCodec(system: ExtendedActorSystem): Codec[PeerConnection.InitializeConnection] = ( ("peer" | actorRefCodec(system)) :: ("chainHash" | bytes32) :: - ("features" | variableSizeBytes(uint16, featuresCodec)) :: + ("features" | variableSizeBytes(uint16, initFeaturesCodec)) :: ("doSync" | bool(8))).as[PeerConnection.InitializeConnection] def connectionReadyCodec(system: ExtendedActorSystem): Codec[PeerConnection.ConnectionReady] = ( diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/Announcements.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/Announcements.scala index d07bd7ccc9..e415530120 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/Announcements.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/Announcements.scala @@ -19,7 +19,7 @@ package fr.acinq.eclair.router import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey, sha256, verifySignature} import fr.acinq.bitcoin.{ByteVector32, ByteVector64, Crypto, LexicographicalOrdering} import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{CltvExpiryDelta, Features, MilliSatoshi, ShortChannelId, TimestampSecond, TimestampSecondLong, serializationResult} +import fr.acinq.eclair.{CltvExpiryDelta, FeatureScope, Features, MilliSatoshi, NodeFeature, ShortChannelId, TimestampSecond, TimestampSecondLong, serializationResult} import scodec.bits.ByteVector import shapeless.HNil @@ -28,16 +28,16 @@ import shapeless.HNil */ object Announcements { - def channelAnnouncementWitnessEncode(chainHash: ByteVector32, shortChannelId: ShortChannelId, nodeId1: PublicKey, nodeId2: PublicKey, bitcoinKey1: PublicKey, bitcoinKey2: PublicKey, features: Features, tlvStream: TlvStream[ChannelAnnouncementTlv]): ByteVector = + def channelAnnouncementWitnessEncode(chainHash: ByteVector32, shortChannelId: ShortChannelId, nodeId1: PublicKey, nodeId2: PublicKey, bitcoinKey1: PublicKey, bitcoinKey2: PublicKey, features: Features[FeatureScope], tlvStream: TlvStream[ChannelAnnouncementTlv]): ByteVector = sha256(sha256(serializationResult(LightningMessageCodecs.channelAnnouncementWitnessCodec.encode(features :: chainHash :: shortChannelId :: nodeId1 :: nodeId2 :: bitcoinKey1 :: bitcoinKey2 :: tlvStream :: HNil)))) - def nodeAnnouncementWitnessEncode(timestamp: TimestampSecond, nodeId: PublicKey, rgbColor: Color, alias: String, features: Features, addresses: List[NodeAddress], tlvStream: TlvStream[NodeAnnouncementTlv]): ByteVector = + def nodeAnnouncementWitnessEncode(timestamp: TimestampSecond, nodeId: PublicKey, rgbColor: Color, alias: String, features: Features[FeatureScope], addresses: List[NodeAddress], tlvStream: TlvStream[NodeAnnouncementTlv]): ByteVector = sha256(sha256(serializationResult(LightningMessageCodecs.nodeAnnouncementWitnessCodec.encode(features :: timestamp :: nodeId :: rgbColor :: alias :: addresses :: tlvStream :: HNil)))) def channelUpdateWitnessEncode(chainHash: ByteVector32, shortChannelId: ShortChannelId, timestamp: TimestampSecond, channelFlags: ChannelUpdate.ChannelFlags, cltvExpiryDelta: CltvExpiryDelta, htlcMinimumMsat: MilliSatoshi, feeBaseMsat: MilliSatoshi, feeProportionalMillionths: Long, htlcMaximumMsat: Option[MilliSatoshi], tlvStream: TlvStream[ChannelUpdateTlv]): ByteVector = sha256(sha256(serializationResult(LightningMessageCodecs.channelUpdateWitnessCodec.encode(chainHash :: shortChannelId :: timestamp :: channelFlags :: cltvExpiryDelta :: htlcMinimumMsat :: feeBaseMsat :: feeProportionalMillionths :: htlcMaximumMsat :: tlvStream :: HNil)))) - def generateChannelAnnouncementWitness(chainHash: ByteVector32, shortChannelId: ShortChannelId, localNodeId: PublicKey, remoteNodeId: PublicKey, localFundingKey: PublicKey, remoteFundingKey: PublicKey, features: Features): ByteVector = + def generateChannelAnnouncementWitness(chainHash: ByteVector32, shortChannelId: ShortChannelId, localNodeId: PublicKey, remoteNodeId: PublicKey, localFundingKey: PublicKey, remoteFundingKey: PublicKey, features: Features[FeatureScope]): ByteVector = if (isNode1(localNodeId, remoteNodeId)) { channelAnnouncementWitnessEncode(chainHash, shortChannelId, localNodeId, remoteNodeId, localFundingKey, remoteFundingKey, features, TlvStream.empty) } else { @@ -68,7 +68,7 @@ object Announcements { ) } - def makeNodeAnnouncement(nodeSecret: PrivateKey, alias: String, color: Color, nodeAddresses: List[NodeAddress], features: Features, timestamp: TimestampSecond = TimestampSecond.now()): NodeAnnouncement = { + def makeNodeAnnouncement(nodeSecret: PrivateKey, alias: String, color: Color, nodeAddresses: List[NodeAddress], features: Features[NodeFeature], timestamp: TimestampSecond = TimestampSecond.now()): NodeAnnouncement = { require(alias.length <= 32) val sortedAddresses = nodeAddresses.map { case address@(_: IPv4) => (1, address) @@ -76,8 +76,7 @@ object Announcements { case address@(_: Tor2) => (3, address) case address@(_: Tor3) => (4, address) }.sortBy(_._1).map(_._2) - val nodeAnnouncementFeatures = features.nodeAnnouncementFeatures() - val witness = nodeAnnouncementWitnessEncode(timestamp, nodeSecret.publicKey, color, alias, nodeAnnouncementFeatures, sortedAddresses, TlvStream.empty) + val witness = nodeAnnouncementWitnessEncode(timestamp, nodeSecret.publicKey, color, alias, features.unscoped(), sortedAddresses, TlvStream.empty) val sig = Crypto.sign(witness, nodeSecret) NodeAnnouncement( signature = sig, @@ -85,7 +84,7 @@ object Announcements { nodeId = nodeSecret.publicKey, rgbColor = color, alias = alias, - features = nodeAnnouncementFeatures, + features = features.unscoped(), addresses = sortedAddresses ) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala index 90dce39510..c5b2695da3 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala @@ -95,7 +95,7 @@ class Router(val nodeParams: NodeParams, watcher: typed.ActorRef[ZmqWatcher.Comm // on restart we update our node announcement // note that if we don't currently have public channels, this will be ignored - val nodeAnn = Announcements.makeNodeAnnouncement(nodeParams.privateKey, nodeParams.alias, nodeParams.color, nodeParams.publicAddresses, nodeParams.features) + val nodeAnn = Announcements.makeNodeAnnouncement(nodeParams.privateKey, nodeParams.alias, nodeParams.color, nodeParams.publicAddresses, nodeParams.features.nodeAnnouncementFeatures()) self ! nodeAnn log.info(s"initialization completed, ready to process messages") diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/Validation.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/Validation.scala index a78c9e9254..36fb781148 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/Validation.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/Validation.scala @@ -114,7 +114,7 @@ object Validation { // in case we just validated our first local channel, we announce the local node if (!d0.nodes.contains(nodeParams.nodeId) && isRelatedTo(c, nodeParams.nodeId)) { log.info("first local channel validated, announcing local node") - val nodeAnn = Announcements.makeNodeAnnouncement(nodeParams.privateKey, nodeParams.alias, nodeParams.color, nodeParams.publicAddresses, nodeParams.features) + val nodeAnn = Announcements.makeNodeAnnouncement(nodeParams.privateKey, nodeParams.alias, nodeParams.color, nodeParams.publicAddresses, nodeParams.features.nodeAnnouncementFeatures()) ctx.self ! nodeAnn } // public channels that haven't yet been announced are considered as private channels diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelCodecs0.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelCodecs0.scala index d7f8010b70..26232fbf1e 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelCodecs0.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelCodecs0.scala @@ -26,7 +26,7 @@ import fr.acinq.eclair.wire.internal.channel.version0.ChannelTypes0.{HtlcTxAndSi import fr.acinq.eclair.wire.protocol.CommonCodecs._ import fr.acinq.eclair.wire.protocol.LightningMessageCodecs.{channelAnnouncementCodec, channelUpdateCodec, combinedFeaturesCodec} import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{BlockHeight, TimestampSecond} +import fr.acinq.eclair.{BlockHeight, Features, InitFeature, TimestampSecond} import scodec.Codec import scodec.bits.{BitVector, ByteVector} import scodec.codecs._ diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelTypes0.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelTypes0.scala index fbf34c097e..6b8765ab19 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelTypes0.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelTypes0.scala @@ -23,7 +23,7 @@ import fr.acinq.eclair.channel._ import fr.acinq.eclair.crypto.ShaChain import fr.acinq.eclair.transactions.CommitmentSpec import fr.acinq.eclair.transactions.Transactions._ -import fr.acinq.eclair.{BlockHeight, Feature, Features, channel} +import fr.acinq.eclair.{BlockHeight, Feature, FeatureScope, Features, channel} import scodec.bits.BitVector private[channel] object ChannelTypes0 { @@ -196,8 +196,8 @@ private[channel] object ChannelTypes0 { ChannelConfig() } val isWumboChannel = commitInput.txOut.amount > Satoshi(16777215) - val baseChannelFeatures: Set[Feature] = if (isWumboChannel) Set(Features.Wumbo) else Set.empty - val commitmentFeatures: Set[Feature] = if (channelVersion.hasAnchorOutputs) { + val baseChannelFeatures: Set[Feature with FeatureScope] = if (isWumboChannel) Set(Features.Wumbo) else Set.empty + val commitmentFeatures: Set[Feature with FeatureScope] = if (channelVersion.hasAnchorOutputs) { Set(Features.StaticRemoteKey, Features.AnchorOutputs) } else if (channelVersion.hasStaticRemotekey) { Set(Features.StaticRemoteKey) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version1/ChannelCodecs1.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version1/ChannelCodecs1.scala index 4247bd7235..e667286531 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version1/ChannelCodecs1.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version1/ChannelCodecs1.scala @@ -18,7 +18,6 @@ package fr.acinq.eclair.wire.internal.channel.version1 import fr.acinq.bitcoin.DeterministicWallet.{ExtendedPrivateKey, KeyPath} import fr.acinq.bitcoin.{ByteVector32, OutPoint, Transaction, TxOut} -import fr.acinq.eclair.BlockHeight import fr.acinq.eclair.channel._ import fr.acinq.eclair.crypto.ShaChain import fr.acinq.eclair.transactions.Transactions._ @@ -28,6 +27,7 @@ import fr.acinq.eclair.wire.internal.channel.version0.ChannelTypes0.{HtlcTxAndSi import fr.acinq.eclair.wire.protocol.CommonCodecs._ import fr.acinq.eclair.wire.protocol.LightningMessageCodecs._ import fr.acinq.eclair.wire.protocol._ +import fr.acinq.eclair.{BlockHeight, Features, InitFeature} import scodec.bits.ByteVector import scodec.codecs._ import scodec.{Attempt, Codec} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version2/ChannelCodecs2.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version2/ChannelCodecs2.scala index 52e776f29e..1b267a2ec8 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version2/ChannelCodecs2.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version2/ChannelCodecs2.scala @@ -18,7 +18,6 @@ package fr.acinq.eclair.wire.internal.channel.version2 import fr.acinq.bitcoin.DeterministicWallet.{ExtendedPrivateKey, KeyPath} import fr.acinq.bitcoin.{OutPoint, Transaction, TxOut} -import fr.acinq.eclair.BlockHeight import fr.acinq.eclair.channel._ import fr.acinq.eclair.crypto.ShaChain import fr.acinq.eclair.transactions.Transactions._ @@ -28,6 +27,7 @@ import fr.acinq.eclair.wire.internal.channel.version0.ChannelTypes0.{HtlcTxAndSi import fr.acinq.eclair.wire.protocol.CommonCodecs._ import fr.acinq.eclair.wire.protocol.LightningMessageCodecs._ import fr.acinq.eclair.wire.protocol._ +import fr.acinq.eclair.{BlockHeight, Features, InitFeature} import scodec.bits.ByteVector import scodec.codecs._ import scodec.{Attempt, Codec} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3.scala index 6528016cf5..633ba5d32a 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3.scala @@ -25,7 +25,7 @@ import fr.acinq.eclair.transactions.{CommitmentSpec, DirectedHtlc, IncomingHtlc, import fr.acinq.eclair.wire.protocol.CommonCodecs._ import fr.acinq.eclair.wire.protocol.LightningMessageCodecs._ import fr.acinq.eclair.wire.protocol.UpdateMessage -import fr.acinq.eclair.{BlockHeight, FeatureSupport, Features} +import fr.acinq.eclair.{BlockHeight, FeatureSupport, Features, InitFeature} import scodec.bits.{BitVector, ByteVector} import scodec.codecs._ import scodec.{Attempt, Codec} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/ChannelTlv.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/ChannelTlv.scala index 56e362350b..32569b164a 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/ChannelTlv.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/ChannelTlv.scala @@ -42,7 +42,7 @@ object ChannelTlv { case class ChannelTypeTlv(channelType: ChannelType) extends OpenChannelTlv with AcceptChannelTlv val channelTypeCodec: Codec[ChannelTypeTlv] = variableSizeBytesLong(varintoverflow, bytes).xmap( - b => ChannelTypeTlv(ChannelTypes.fromFeatures(Features(b))), + b => ChannelTypeTlv(ChannelTypes.fromFeatures(Features(b).initFeatures())), tlv => Features(tlv.channelType.features.map(f => f -> FeatureSupport.Mandatory).toMap).toByteVector ) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecs.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecs.scala index 94cfb4c7a3..6c176f66eb 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecs.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecs.scala @@ -18,7 +18,7 @@ package fr.acinq.eclair.wire.protocol import fr.acinq.eclair.wire.Monitoring.{Metrics, Tags} import fr.acinq.eclair.wire.protocol.CommonCodecs._ -import fr.acinq.eclair.{Features, KamonExt} +import fr.acinq.eclair.{FeatureScope, Features, InitFeature, KamonExt, NodeFeature} import scodec.bits.{BitVector, ByteVector} import scodec.codecs._ import scodec.{Attempt, Codec} @@ -29,18 +29,20 @@ import shapeless._ */ object LightningMessageCodecs { - val featuresCodec: Codec[Features] = varsizebinarydata.xmap[Features]( + val featuresCodec: Codec[Features[FeatureScope]] = varsizebinarydata.xmap[Features[FeatureScope]]( { bytes => Features(bytes) }, { features => features.toByteVector } ) + val initFeaturesCodec: Codec[Features[InitFeature]] = featuresCodec.xmap[Features[InitFeature]](_.initFeatures(), _.unscoped()) + /** For historical reasons, features are divided into two feature bitmasks. We only send from the second one, but we allow receiving in both. */ - val combinedFeaturesCodec: Codec[Features] = ( + val combinedFeaturesCodec: Codec[Features[InitFeature]] = ( ("globalFeatures" | varsizebinarydata) :: - ("localFeatures" | varsizebinarydata)).as[(ByteVector, ByteVector)].xmap[Features]( + ("localFeatures" | varsizebinarydata)).as[(ByteVector, ByteVector)].xmap[Features[InitFeature]]( { case (gf, lf) => val length = gf.length.max(lf.length) - Features(gf.padLeft(length) | lf.padLeft(length)) + Features(gf.padLeft(length) | lf.padLeft(length)).initFeatures() }, { features => (ByteVector.empty, features.toByteVector) }) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageTypes.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageTypes.scala index 6420e94ab0..42416a896e 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageTypes.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageTypes.scala @@ -21,7 +21,7 @@ import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} import fr.acinq.bitcoin.{ByteVector32, ByteVector64, Satoshi} import fr.acinq.eclair.blockchain.fee.FeeratePerKw import fr.acinq.eclair.channel.{ChannelFlags, ChannelType} -import fr.acinq.eclair.{BlockHeight, CltvExpiry, CltvExpiryDelta, Features, MilliSatoshi, ShortChannelId, TimestampSecond, UInt64} +import fr.acinq.eclair.{BlockHeight, CltvExpiry, CltvExpiryDelta, FeatureScope, Features, InitFeature, MilliSatoshi, NodeFeature, ShortChannelId, TimestampSecond, UInt64} import scodec.bits.ByteVector import java.net.{Inet4Address, Inet6Address, InetAddress, InetSocketAddress} @@ -47,7 +47,7 @@ sealed trait UpdateMessage extends HtlcMessage // <- not in the spec sealed trait HtlcSettlementMessage extends UpdateMessage { def id: Long } // <- not in the spec // @formatter:on -case class Init(features: Features, tlvStream: TlvStream[InitTlv] = TlvStream.empty) extends SetupMessage { +case class Init(features: Features[InitFeature], tlvStream: TlvStream[InitTlv] = TlvStream.empty) extends SetupMessage { val networks = tlvStream.get[InitTlv.Networks].map(_.chainHashes).getOrElse(Nil) val remoteAddress_opt = tlvStream.get[InitTlv.RemoteAddress].map(_.address) } @@ -200,7 +200,7 @@ case class ChannelAnnouncement(nodeSignature1: ByteVector64, nodeSignature2: ByteVector64, bitcoinSignature1: ByteVector64, bitcoinSignature2: ByteVector64, - features: Features, + features: Features[FeatureScope], chainHash: ByteVector32, shortChannelId: ShortChannelId, nodeId1: PublicKey, @@ -263,7 +263,7 @@ case class Tor3(tor3: String, port: Int) extends OnionAddress { override def soc // @formatter:on case class NodeAnnouncement(signature: ByteVector64, - features: Features, + features: Features[FeatureScope], timestamp: TimestampSecond, nodeId: PublicKey, rgbColor: Color, diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/FeaturesSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/FeaturesSpec.scala index 72c9df259c..345b043999 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/FeaturesSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/FeaturesSpec.scala @@ -109,7 +109,7 @@ class FeaturesSpec extends AnyFunSuite { } test("features compatibility") { - case class TestCase(ours: Features, theirs: Features, oursSupportTheirs: Boolean, theirsSupportOurs: Boolean, compatible: Boolean) + case class TestCase(ours: Features[FeatureScope], theirs: Features[FeatureScope], oursSupportTheirs: Boolean, theirsSupportOurs: Boolean, compatible: Boolean) val testCases = Seq( // Empty features TestCase( @@ -168,7 +168,7 @@ class FeaturesSpec extends AnyFunSuite { // They have unknown optional features TestCase( Features(VariableLengthOnion -> Optional), - Features(Map[Feature, FeatureSupport](VariableLengthOnion -> Optional), Set(UnknownFeature(141))), + Features[FeatureScope](Map[Feature with FeatureScope, FeatureSupport](VariableLengthOnion -> Optional), Set(UnknownFeature(141))), oursSupportTheirs = true, theirsSupportOurs = true, compatible = true @@ -176,7 +176,7 @@ class FeaturesSpec extends AnyFunSuite { // They have unknown mandatory features TestCase( Features(VariableLengthOnion -> Optional), - Features(Map[Feature, FeatureSupport](VariableLengthOnion -> Optional), Set(UnknownFeature(142))), + Features[FeatureScope](Map[Feature with FeatureScope, FeatureSupport](VariableLengthOnion -> Optional), Set(UnknownFeature(142))), oursSupportTheirs = false, theirsSupportOurs = true, compatible = false @@ -198,10 +198,10 @@ class FeaturesSpec extends AnyFunSuite { compatible = false ), // nonreg testing of future features (needs to be updated with every new supported mandatory bit) - TestCase(Features.empty, Features(Map.empty[Feature, FeatureSupport], Set(UnknownFeature(24))), oursSupportTheirs = false, theirsSupportOurs = true, compatible = false), - TestCase(Features.empty, Features(Map.empty[Feature, FeatureSupport], Set(UnknownFeature(25))), oursSupportTheirs = true, theirsSupportOurs = true, compatible = true), - TestCase(Features.empty, Features(Map.empty[Feature, FeatureSupport], Set(UnknownFeature(28))), oursSupportTheirs = false, theirsSupportOurs = true, compatible = false), - TestCase(Features.empty, Features(Map.empty[Feature, FeatureSupport], Set(UnknownFeature(29))), oursSupportTheirs = true, theirsSupportOurs = true, compatible = true), + TestCase(Features.empty, Features[FeatureScope](Map.empty[Feature with FeatureScope, FeatureSupport], Set(UnknownFeature(24))), oursSupportTheirs = false, theirsSupportOurs = true, compatible = false), + TestCase(Features.empty, Features[FeatureScope](Map.empty[Feature with FeatureScope, FeatureSupport], Set(UnknownFeature(25))), oursSupportTheirs = true, theirsSupportOurs = true, compatible = true), + TestCase(Features.empty, Features[FeatureScope](Map.empty[Feature with FeatureScope, FeatureSupport], Set(UnknownFeature(28))), oursSupportTheirs = false, theirsSupportOurs = true, compatible = false), + TestCase(Features.empty, Features[FeatureScope](Map.empty[Feature with FeatureScope, FeatureSupport], Set(UnknownFeature(29))), oursSupportTheirs = true, theirsSupportOurs = true, compatible = true), ) for (testCase <- testCases) { @@ -212,19 +212,22 @@ class FeaturesSpec extends AnyFunSuite { } test("filter features based on their usage") { - val features = Features( - Map[Feature, FeatureSupport](DataLossProtect -> Optional, InitialRoutingSync -> Optional, VariableLengthOnion -> Mandatory, PaymentMetadata -> Optional), - Set(UnknownFeature(753), UnknownFeature(852)) + val features = Features[FeatureScope]( + Map[Feature with FeatureScope, FeatureSupport](DataLossProtect -> Optional, InitialRoutingSync -> Optional, VariableLengthOnion -> Mandatory, PaymentMetadata -> Optional), + Set(UnknownFeature(753), UnknownFeature(852), UnknownFeature(65303)) ) - assert(features.initFeatures() === Features( - Map[Feature, FeatureSupport](DataLossProtect -> Optional, InitialRoutingSync -> Optional, VariableLengthOnion -> Mandatory), - Set(UnknownFeature(753), UnknownFeature(852)) + assert(features.initFeatures() === Features[InitFeature]( + Map[Feature with InitFeature, FeatureSupport](DataLossProtect -> Optional, InitialRoutingSync -> Optional, VariableLengthOnion -> Mandatory), + Set(UnknownFeature(753), UnknownFeature(852), UnknownFeature(65303)) )) - assert(features.nodeAnnouncementFeatures() === Features( - Map[Feature, FeatureSupport](DataLossProtect -> Optional, VariableLengthOnion -> Mandatory), - Set(UnknownFeature(753), UnknownFeature(852)) + assert(features.nodeAnnouncementFeatures() === Features[NodeFeature]( + Map[Feature with NodeFeature, FeatureSupport](DataLossProtect -> Optional, VariableLengthOnion -> Mandatory), + Set(UnknownFeature(753), UnknownFeature(852), UnknownFeature(65303)) + )) + assert(features.invoiceFeatures() === Features[InvoiceFeature]( + Map[Feature with InvoiceFeature, FeatureSupport](VariableLengthOnion -> Mandatory, PaymentMetadata -> Optional), + Set(UnknownFeature(753), UnknownFeature(852), UnknownFeature(65303)) )) - assert(features.invoiceFeatures() === Features(VariableLengthOnion -> Mandatory, PaymentMetadata -> Optional)) } test("features to bytes") { @@ -232,8 +235,8 @@ class FeaturesSpec extends AnyFunSuite { hex"" -> Features.empty, hex"0100" -> Features(VariableLengthOnion -> Mandatory), hex"028a8a" -> Features(DataLossProtect -> Optional, InitialRoutingSync -> Optional, ChannelRangeQueries -> Optional, VariableLengthOnion -> Optional, ChannelRangeQueriesExtended -> Optional, PaymentSecret -> Optional, BasicMultiPartPayment -> Optional), - hex"09004200" -> Features(Map[Feature, FeatureSupport](VariableLengthOnion -> Optional, PaymentSecret -> Mandatory, ShutdownAnySegwit -> Optional), Set(UnknownFeature(24))), - hex"52000000" -> Features(Map.empty[Feature, FeatureSupport], Set(UnknownFeature(25), UnknownFeature(28), UnknownFeature(30))) + hex"09004200" -> Features[FeatureScope](Map[Feature with FeatureScope, FeatureSupport](VariableLengthOnion -> Optional, PaymentSecret -> Mandatory, ShutdownAnySegwit -> Optional), Set(UnknownFeature(24))), + hex"52000000" -> Features[FeatureScope](Map.empty[Feature with FeatureScope, FeatureSupport], Set(UnknownFeature(25), UnknownFeature(28), UnknownFeature(30))) ) for ((bin, features) <- testCases) { @@ -249,16 +252,14 @@ class FeaturesSpec extends AnyFunSuite { { val conf = ConfigFactory.parseString( """ - |features { - | option_data_loss_protect = optional - | initial_routing_sync = optional - | gossip_queries = optional - | gossip_queries_ex = optional - | var_onion_optin = optional - | payment_secret = optional - | basic_mpp = optional - |} - """.stripMargin) + option_data_loss_protect = optional + initial_routing_sync = optional + gossip_queries = optional + gossip_queries_ex = optional + var_onion_optin = optional + payment_secret = optional + basic_mpp = optional + """) val features = fromConfiguration(conf) assert(features.toByteVector === hex"028a8a") @@ -276,16 +277,12 @@ class FeaturesSpec extends AnyFunSuite { { val conf = ConfigFactory.parseString( """ - | features { - | initial_routing_sync = optional - | option_data_loss_protect = optional - | gossip_queries = optional - | gossip_queries_ex = mandatory - | var_onion_optin = optional - | } - | - """.stripMargin - ) + initial_routing_sync = optional + option_data_loss_protect = optional + gossip_queries = optional + gossip_queries_ex = mandatory + var_onion_optin = optional + """) val features = fromConfiguration(conf) assert(features.toByteVector === hex"068a") @@ -303,29 +300,21 @@ class FeaturesSpec extends AnyFunSuite { { val confWithUnknownFeatures = ConfigFactory.parseString( """ - |features { - | option_non_existent = mandatory # this is ignored - | gossip_queries = optional - | payment_secret = mandatory - |} - """.stripMargin) + option_non_existent = mandatory + gossip_queries = optional + payment_secret = mandatory + """) - val features = fromConfiguration(confWithUnknownFeatures) - assert(features.toByteVector === hex"4080") - assert(Features(hex"4080") === features) - assert(features.hasFeature(ChannelRangeQueries, Some(Optional))) - assert(features.hasFeature(PaymentSecret, Some(Mandatory))) + assertThrows[RuntimeException](fromConfiguration(confWithUnknownFeatures)) } { val confWithUnknownSupport = ConfigFactory.parseString( """ - |features { - | option_data_loss_protect = what - | gossip_queries = optional - | payment_secret = mandatory - |} - """.stripMargin) + option_data_loss_protect = what + gossip_queries = optional + payment_secret = mandatory + """) assertThrows[RuntimeException](fromConfiguration(confWithUnknownSupport)) } @@ -333,14 +322,12 @@ class FeaturesSpec extends AnyFunSuite { { val confWithDisabledFeatures = ConfigFactory.parseString( """ - |features { - | option_data_loss_protect = disabled - | gossip_queries = optional - | payment_secret = mandatory - | option_support_large_channel = disabled - | gossip_queries_ex = mandatory - |} - """.stripMargin) + option_data_loss_protect = disabled + gossip_queries = optional + payment_secret = mandatory + option_support_large_channel = disabled + gossip_queries_ex = mandatory + """) val features = fromConfiguration(confWithDisabledFeatures) assert(!features.hasFeature(DataLossProtect)) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/StartupSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/StartupSpec.scala index ada367f4a5..9c308967d7 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/StartupSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/StartupSpec.scala @@ -166,7 +166,7 @@ class StartupSpec extends AnyFunSuite { test("parse human readable override features") { val perNodeConf = ConfigFactory.parseString( """ - | override-features = [ // optional per-node features + | override-init-features = [ // optional per-node features | { | nodeid = "02aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", | features { @@ -185,10 +185,10 @@ class StartupSpec extends AnyFunSuite { assert(perNodeFeatures === Features(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory, BasicMultiPartPayment -> Mandatory, ChannelType -> Optional)) } - test("filter out non-init features in node override") { + test("reject non-init features in node override") { val perNodeConf = ConfigFactory.parseString( """ - | override-features = [ // optional per-node features + | override-init-features = [ // optional per-node features | { | nodeid = "02aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", | features { @@ -211,15 +211,7 @@ class StartupSpec extends AnyFunSuite { """.stripMargin ) - val nodeParams = makeNodeParamsWithDefaults(perNodeConf.withFallback(defaultConf)) - val perNodeFeaturesA = nodeParams.initFeaturesFor(PublicKey(ByteVector.fromValidHex("02aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"))) - val perNodeFeaturesB = nodeParams.initFeaturesFor(PublicKey(ByteVector.fromValidHex("02bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"))) - val defaultNodeFeatures = nodeParams.initFeaturesFor(PublicKey(ByteVector.fromValidHex("02cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"))) - // Some features should never be sent in init messages. - assert(nodeParams.features.hasFeature(PaymentMetadata)) - assert(!perNodeFeaturesA.hasFeature(PaymentMetadata)) - assert(!perNodeFeaturesB.hasFeature(PaymentMetadata)) - assert(!defaultNodeFeatures.hasFeature(PaymentMetadata)) + assertThrows[IllegalArgumentException](makeNodeParamsWithDefaults(perNodeConf.withFallback(defaultConf))) } test("override feerate mismatch tolerance") { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala b/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala index 37781da9ff..2fc55e3645 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala @@ -84,8 +84,8 @@ object TestConstants { color = Color(1, 2, 3), publicAddresses = NodeAddress.fromParts("localhost", 9731).get :: Nil, torAddress_opt = None, - features = Features( - Map[Feature, FeatureSupport]( + features = Features[FeatureScope]( + Map[Feature with FeatureScope, FeatureSupport]( DataLossProtect -> Optional, ChannelRangeQueries -> Optional, ChannelRangeQueriesExtended -> Optional, @@ -97,7 +97,7 @@ object TestConstants { Set(UnknownFeature(TestFeature.optional)) ), pluginParams = List(pluginParams), - overrideFeatures = Map.empty, + overrideInitFeatures = Map.empty, syncWhitelist = Set.empty, channelConf = ChannelConf( dustLimit = 1100 sat, @@ -199,7 +199,7 @@ object TestConstants { def channelParams: LocalParams = Peer.makeChannelParams( nodeParams, - nodeParams.features, + nodeParams.features.initFeatures(), Script.write(Script.pay2wpkh(randomKey().publicKey)), None, isFunder = true, @@ -232,7 +232,7 @@ object TestConstants { PaymentMetadata -> Optional, ), pluginParams = Nil, - overrideFeatures = Map.empty, + overrideInitFeatures = Map.empty, syncWhitelist = Set.empty, channelConf = ChannelConf( dustLimit = 1000 sat, @@ -334,7 +334,7 @@ object TestConstants { def channelParams: LocalParams = Peer.makeChannelParams( nodeParams, - nodeParams.features, + nodeParams.features.initFeatures(), Script.write(Script.pay2wpkh(randomKey().publicKey)), None, isFunder = false, diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/ChannelFeaturesSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/ChannelFeaturesSpec.scala index e4f169cd28..b415099ebf 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/ChannelFeaturesSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/ChannelFeaturesSpec.scala @@ -20,7 +20,7 @@ import fr.acinq.eclair.FeatureSupport._ import fr.acinq.eclair.Features._ import fr.acinq.eclair.channel.states.ChannelStateTestsHelperMethods import fr.acinq.eclair.transactions.Transactions -import fr.acinq.eclair.{Features, TestKitBaseClass} +import fr.acinq.eclair.{Features, InitFeature, TestKitBaseClass} import org.scalatest.funsuite.AnyFunSuiteLike class ChannelFeaturesSpec extends TestKitBaseClass with AnyFunSuiteLike with ChannelStateTestsHelperMethods { @@ -55,7 +55,7 @@ class ChannelFeaturesSpec extends TestKitBaseClass with AnyFunSuiteLike with Cha } test("pick channel type based on local and remote features") { - case class TestCase(localFeatures: Features, remoteFeatures: Features, expectedChannelType: ChannelType) + case class TestCase(localFeatures: Features[InitFeature], remoteFeatures: Features[InitFeature], expectedChannelType: ChannelType) val testCases = Seq( TestCase(Features.empty, Features.empty, ChannelTypes.Standard), TestCase(Features(StaticRemoteKey -> Optional), Features.empty, ChannelTypes.Standard), @@ -81,28 +81,28 @@ class ChannelFeaturesSpec extends TestKitBaseClass with AnyFunSuiteLike with Cha test("create channel type from features") { val validChannelTypes = Seq( - Features.empty -> ChannelTypes.Standard, - Features(StaticRemoteKey -> Mandatory) -> ChannelTypes.StaticRemoteKey, - Features(StaticRemoteKey -> Mandatory, AnchorOutputs -> Mandatory) -> ChannelTypes.AnchorOutputs, - Features(StaticRemoteKey -> Mandatory, AnchorOutputsZeroFeeHtlcTx -> Mandatory) -> ChannelTypes.AnchorOutputsZeroFeeHtlcTx, + Features.empty[InitFeature] -> ChannelTypes.Standard, + Features[InitFeature](StaticRemoteKey -> Mandatory) -> ChannelTypes.StaticRemoteKey, + Features[InitFeature](StaticRemoteKey -> Mandatory, AnchorOutputs -> Mandatory) -> ChannelTypes.AnchorOutputs, + Features[InitFeature](StaticRemoteKey -> Mandatory, AnchorOutputsZeroFeeHtlcTx -> Mandatory) -> ChannelTypes.AnchorOutputsZeroFeeHtlcTx, ) for ((features, expected) <- validChannelTypes) { assert(ChannelTypes.fromFeatures(features) === expected) } val invalidChannelTypes = Seq( - Features(Wumbo -> Optional), - Features(StaticRemoteKey -> Optional), - Features(StaticRemoteKey -> Mandatory, Wumbo -> Optional), - Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional), - Features(StaticRemoteKey -> Mandatory, AnchorOutputs -> Optional), - Features(StaticRemoteKey -> Optional, AnchorOutputs -> Mandatory), - Features(StaticRemoteKey -> Optional, AnchorOutputsZeroFeeHtlcTx -> Optional), - Features(StaticRemoteKey -> Optional, AnchorOutputsZeroFeeHtlcTx -> Mandatory), - Features(StaticRemoteKey -> Mandatory, AnchorOutputsZeroFeeHtlcTx -> Optional), - Features(StaticRemoteKey -> Mandatory, AnchorOutputs -> Mandatory, AnchorOutputsZeroFeeHtlcTx -> Mandatory), - Features(StaticRemoteKey -> Mandatory, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTx -> Mandatory), - Features(StaticRemoteKey -> Mandatory, AnchorOutputs -> Mandatory, Wumbo -> Optional), + Features[InitFeature](Wumbo -> Optional), + Features[InitFeature](StaticRemoteKey -> Optional), + Features[InitFeature](StaticRemoteKey -> Mandatory, Wumbo -> Optional), + Features[InitFeature](StaticRemoteKey -> Optional, AnchorOutputs -> Optional), + Features[InitFeature](StaticRemoteKey -> Mandatory, AnchorOutputs -> Optional), + Features[InitFeature](StaticRemoteKey -> Optional, AnchorOutputs -> Mandatory), + Features[InitFeature](StaticRemoteKey -> Optional, AnchorOutputsZeroFeeHtlcTx -> Optional), + Features[InitFeature](StaticRemoteKey -> Optional, AnchorOutputsZeroFeeHtlcTx -> Mandatory), + Features[InitFeature](StaticRemoteKey -> Mandatory, AnchorOutputsZeroFeeHtlcTx -> Optional), + Features[InitFeature](StaticRemoteKey -> Mandatory, AnchorOutputs -> Mandatory, AnchorOutputsZeroFeeHtlcTx -> Mandatory), + Features[InitFeature](StaticRemoteKey -> Mandatory, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTx -> Mandatory), + Features[InitFeature](StaticRemoteKey -> Mandatory, AnchorOutputs -> Mandatory, Wumbo -> Optional), ) for (features <- invalidChannelTypes) { assert(ChannelTypes.fromFeatures(features) === ChannelTypes.UnsupportedChannelType(features)) @@ -110,15 +110,15 @@ class ChannelFeaturesSpec extends TestKitBaseClass with AnyFunSuiteLike with Cha } test("enrich channel type with other permanent channel features") { - assert(ChannelFeatures(ChannelTypes.Standard, Features(Wumbo -> Optional), Features.empty).features.isEmpty) - assert(ChannelFeatures(ChannelTypes.Standard, Features(Wumbo -> Optional), Features(Wumbo -> Optional)).features === Set(Wumbo)) - assert(ChannelFeatures(ChannelTypes.Standard, Features(Wumbo -> Mandatory), Features(Wumbo -> Optional)).features === Set(Wumbo)) - assert(ChannelFeatures(ChannelTypes.StaticRemoteKey, Features(Wumbo -> Optional), Features.empty).features === Set(StaticRemoteKey)) - assert(ChannelFeatures(ChannelTypes.StaticRemoteKey, Features(Wumbo -> Optional), Features(Wumbo -> Optional)).features === Set(StaticRemoteKey, Wumbo)) - assert(ChannelFeatures(ChannelTypes.AnchorOutputs, Features.empty, Features(Wumbo -> Optional)).features === Set(StaticRemoteKey, AnchorOutputs)) - assert(ChannelFeatures(ChannelTypes.AnchorOutputs, Features(Wumbo -> Optional), Features(Wumbo -> Mandatory)).features === Set(StaticRemoteKey, AnchorOutputs, Wumbo)) - assert(ChannelFeatures(ChannelTypes.AnchorOutputsZeroFeeHtlcTx, Features.empty, Features(Wumbo -> Optional)).features === Set(StaticRemoteKey, AnchorOutputsZeroFeeHtlcTx)) - assert(ChannelFeatures(ChannelTypes.AnchorOutputsZeroFeeHtlcTx, Features(Wumbo -> Optional), Features(Wumbo -> Mandatory)).features === Set(StaticRemoteKey, AnchorOutputsZeroFeeHtlcTx, Wumbo)) + assert(ChannelFeatures(ChannelTypes.Standard, Features[InitFeature](Wumbo -> Optional), Features.empty[InitFeature]).features.isEmpty) + assert(ChannelFeatures(ChannelTypes.Standard, Features[InitFeature](Wumbo -> Optional), Features[InitFeature](Wumbo -> Optional)).features === Set(Wumbo)) + assert(ChannelFeatures(ChannelTypes.Standard, Features[InitFeature](Wumbo -> Mandatory), Features[InitFeature](Wumbo -> Optional)).features === Set(Wumbo)) + assert(ChannelFeatures(ChannelTypes.StaticRemoteKey, Features[InitFeature](Wumbo -> Optional), Features.empty[InitFeature]).features === Set(StaticRemoteKey)) + assert(ChannelFeatures(ChannelTypes.StaticRemoteKey, Features[InitFeature](Wumbo -> Optional), Features[InitFeature](Wumbo -> Optional)).features === Set(StaticRemoteKey, Wumbo)) + assert(ChannelFeatures(ChannelTypes.AnchorOutputs, Features.empty[InitFeature], Features[InitFeature](Wumbo -> Optional)).features === Set(StaticRemoteKey, AnchorOutputs)) + assert(ChannelFeatures(ChannelTypes.AnchorOutputs, Features[InitFeature](Wumbo -> Optional), Features[InitFeature](Wumbo -> Mandatory)).features === Set(StaticRemoteKey, AnchorOutputs, Wumbo)) + assert(ChannelFeatures(ChannelTypes.AnchorOutputsZeroFeeHtlcTx, Features.empty[InitFeature], Features[InitFeature](Wumbo -> Optional)).features === Set(StaticRemoteKey, AnchorOutputsZeroFeeHtlcTx)) + assert(ChannelFeatures(ChannelTypes.AnchorOutputsZeroFeeHtlcTx, Features[InitFeature](Wumbo -> Optional), Features[InitFeature](Wumbo -> Mandatory)).features === Set(StaticRemoteKey, AnchorOutputsZeroFeeHtlcTx, Wumbo)) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/RestoreSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/RestoreSpec.scala index 2f15699727..2a318adc93 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/RestoreSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/RestoreSpec.scala @@ -38,9 +38,9 @@ class RestoreSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Chan } } - def aliceInit = Init(Alice.nodeParams.features) + def aliceInit = Init(Alice.nodeParams.features.initFeatures()) - def bobInit = Init(Bob.nodeParams.features) + def bobInit = Init(Bob.nodeParams.features.initFeatures()) test("use funding pubkeys from publish commitment to spend our output") { f => import f._ diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala index ae27b6e12e..7c59abfbcd 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala @@ -146,6 +146,7 @@ trait ChannelStateTestsHelperMethods extends TestKitBase { .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.ShutdownAnySegwit))(_.updated(Features.ShutdownAnySegwit, FeatureSupport.Optional)) .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.OptionUpfrontShutdownScript))(_.updated(Features.UpfrontShutdownScript, FeatureSupport.Optional)) .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.ChannelType))(_.updated(Features.ChannelType, FeatureSupport.Optional)) + .initFeatures() val bobInitFeatures = Bob.nodeParams.features .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.Wumbo))(_.updated(Features.Wumbo, FeatureSupport.Optional)) .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.StaticRemoteKey))(_.updated(Features.StaticRemoteKey, FeatureSupport.Optional)) @@ -154,6 +155,7 @@ trait ChannelStateTestsHelperMethods extends TestKitBase { .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.ShutdownAnySegwit))(_.updated(Features.ShutdownAnySegwit, FeatureSupport.Optional)) .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.OptionUpfrontShutdownScript))(_.updated(Features.UpfrontShutdownScript, FeatureSupport.Optional)) .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.ChannelType))(_.updated(Features.ChannelType, FeatureSupport.Optional)) + .initFeatures() val channelType = ChannelTypes.defaultFromFeatures(aliceInitFeatures, bobInitFeatures) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/OfflineStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/OfflineStateSpec.scala index c7853ebe23..ad22801989 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/OfflineStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/OfflineStateSpec.scala @@ -57,8 +57,8 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with } } - val aliceInit = Init(TestConstants.Alice.nodeParams.features) - val bobInit = Init(TestConstants.Bob.nodeParams.features) + val aliceInit = Init(TestConstants.Alice.nodeParams.features.initFeatures()) + val bobInit = Init(TestConstants.Bob.nodeParams.features.initFeatures()) test("re-send lost htlc and signature after first commitment") { f => import f._ diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala index f9c76c7656..e058741993 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala @@ -1018,8 +1018,8 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // then we manually replace alice's state with an older one alice.setState(OFFLINE, oldStateData) // then we reconnect them - val aliceInit = Init(TestConstants.Alice.nodeParams.features) - val bobInit = Init(TestConstants.Bob.nodeParams.features) + val aliceInit = Init(TestConstants.Alice.nodeParams.features.initFeatures()) + val bobInit = Init(TestConstants.Bob.nodeParams.features.initFeatures()) alice ! INPUT_RECONNECTED(alice2bob.ref, aliceInit, bobInit) bob ! INPUT_RECONNECTED(bob2alice.ref, bobInit, aliceInit) // peers exchange channel_reestablish messages diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerConnectionSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerConnectionSpec.scala index dadb10bff9..af9ba3e16b 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerConnectionSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerConnectionSpec.scala @@ -67,13 +67,13 @@ class PeerConnectionSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wi withFixture(test.toNoArgTest(FixtureParam(aliceParams, remoteNodeId, switchboard, router, connection, transport, peerConnection, peer))) } - def connect(aliceParams: NodeParams, remoteNodeId: PublicKey, switchboard: TestProbe, router: TestProbe, connection: TestProbe, transport: TestProbe, peerConnection: TestFSMRef[PeerConnection.State, PeerConnection.Data, PeerConnection], peer: TestProbe, remoteInit: protocol.Init = protocol.Init(Bob.nodeParams.features), doSync: Boolean = false, isPersistent: Boolean = true): Unit = { + def connect(aliceParams: NodeParams, remoteNodeId: PublicKey, switchboard: TestProbe, router: TestProbe, connection: TestProbe, transport: TestProbe, peerConnection: TestFSMRef[PeerConnection.State, PeerConnection.Data, PeerConnection], peer: TestProbe, remoteInit: protocol.Init = protocol.Init(Bob.nodeParams.features.initFeatures()), doSync: Boolean = false, isPersistent: Boolean = true): Unit = { // let's simulate a connection val probe = TestProbe() probe.send(peerConnection, PeerConnection.PendingAuth(connection.ref, Some(remoteNodeId), address, origin_opt = None, transport_opt = Some(transport.ref), isPersistent = isPersistent)) transport.send(peerConnection, TransportHandler.HandshakeCompleted(remoteNodeId)) switchboard.expectMsg(PeerConnection.Authenticated(peerConnection, remoteNodeId)) - probe.send(peerConnection, PeerConnection.InitializeConnection(peer.ref, aliceParams.chainHash, aliceParams.features, doSync)) + probe.send(peerConnection, PeerConnection.InitializeConnection(peer.ref, aliceParams.chainHash, aliceParams.features.initFeatures(), doSync)) transport.expectMsgType[TransportHandler.Listener] val localInit = transport.expectMsgType[protocol.Init] assert(localInit.networks === List(Block.RegtestGenesisBlock.hash)) @@ -101,7 +101,7 @@ class PeerConnectionSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wi probe.send(peerConnection, incomingConnection) transport.send(peerConnection, TransportHandler.HandshakeCompleted(remoteNodeId)) switchboard.expectMsg(PeerConnection.Authenticated(peerConnection, remoteNodeId)) - probe.send(peerConnection, PeerConnection.InitializeConnection(peer.ref, nodeParams.chainHash, nodeParams.features, doSync = false)) + probe.send(peerConnection, PeerConnection.InitializeConnection(peer.ref, nodeParams.chainHash, nodeParams.features.initFeatures(), doSync = false)) transport.expectMsgType[TransportHandler.Listener] val localInit = transport.expectMsgType[protocol.Init] assert(localInit.remoteAddress_opt === Some(fakeIPAddress)) @@ -133,7 +133,7 @@ class PeerConnectionSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wi probe.watch(peerConnection) probe.send(peerConnection, PeerConnection.PendingAuth(connection.ref, Some(remoteNodeId), address, origin_opt = Some(origin.ref), transport_opt = Some(transport.ref), isPersistent = true)) transport.send(peerConnection, TransportHandler.HandshakeCompleted(remoteNodeId)) - probe.send(peerConnection, PeerConnection.InitializeConnection(peer.ref, nodeParams.chainHash, nodeParams.features, doSync = true)) + probe.send(peerConnection, PeerConnection.InitializeConnection(peer.ref, nodeParams.chainHash, nodeParams.features.initFeatures(), doSync = true)) probe.expectTerminated(peerConnection, nodeParams.peerConnectionConf.initTimeout / transport.testKitSettings.TestTimeFactor + 1.second) // we don't want dilated time here origin.expectMsg(PeerConnection.ConnectionResult.InitializationFailed("initialization timed out")) } @@ -145,7 +145,7 @@ class PeerConnectionSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wi probe.watch(transport.ref) probe.send(peerConnection, PeerConnection.PendingAuth(connection.ref, Some(remoteNodeId), address, origin_opt = Some(origin.ref), transport_opt = Some(transport.ref), isPersistent = true)) transport.send(peerConnection, TransportHandler.HandshakeCompleted(remoteNodeId)) - probe.send(peerConnection, PeerConnection.InitializeConnection(peer.ref, nodeParams.chainHash, nodeParams.features, doSync = true)) + probe.send(peerConnection, PeerConnection.InitializeConnection(peer.ref, nodeParams.chainHash, nodeParams.features.initFeatures(), doSync = true)) transport.expectMsgType[TransportHandler.Listener] transport.expectMsgType[protocol.Init] transport.send(peerConnection, LightningMessageCodecs.initCodec.decode(hex"0000 00050100000000".bits).require.value) @@ -161,7 +161,7 @@ class PeerConnectionSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wi probe.watch(transport.ref) probe.send(peerConnection, PeerConnection.PendingAuth(connection.ref, Some(remoteNodeId), address, origin_opt = Some(origin.ref), transport_opt = Some(transport.ref), isPersistent = true)) transport.send(peerConnection, TransportHandler.HandshakeCompleted(remoteNodeId)) - probe.send(peerConnection, PeerConnection.InitializeConnection(peer.ref, nodeParams.chainHash, nodeParams.features, doSync = true)) + probe.send(peerConnection, PeerConnection.InitializeConnection(peer.ref, nodeParams.chainHash, nodeParams.features.initFeatures(), doSync = true)) transport.expectMsgType[TransportHandler.Listener] transport.expectMsgType[protocol.Init] transport.send(peerConnection, LightningMessageCodecs.initCodec.decode(hex"00050100000000 0000".bits).require.value) @@ -177,7 +177,7 @@ class PeerConnectionSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wi probe.watch(transport.ref) probe.send(peerConnection, PeerConnection.PendingAuth(connection.ref, Some(remoteNodeId), address, origin_opt = Some(origin.ref), transport_opt = Some(transport.ref), isPersistent = true)) transport.send(peerConnection, TransportHandler.HandshakeCompleted(remoteNodeId)) - probe.send(peerConnection, PeerConnection.InitializeConnection(peer.ref, nodeParams.chainHash, nodeParams.features, doSync = true)) + probe.send(peerConnection, PeerConnection.InitializeConnection(peer.ref, nodeParams.chainHash, nodeParams.features.initFeatures(), doSync = true)) transport.expectMsgType[TransportHandler.Listener] transport.expectMsgType[protocol.Init] // remote activated MPP but forgot payment secret @@ -194,10 +194,10 @@ class PeerConnectionSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wi probe.watch(transport.ref) probe.send(peerConnection, PeerConnection.PendingAuth(connection.ref, Some(remoteNodeId), address, origin_opt = Some(origin.ref), transport_opt = Some(transport.ref), isPersistent = true)) transport.send(peerConnection, TransportHandler.HandshakeCompleted(remoteNodeId)) - probe.send(peerConnection, PeerConnection.InitializeConnection(peer.ref, nodeParams.chainHash, nodeParams.features, doSync = true)) + probe.send(peerConnection, PeerConnection.InitializeConnection(peer.ref, nodeParams.chainHash, nodeParams.features.initFeatures(), doSync = true)) transport.expectMsgType[TransportHandler.Listener] transport.expectMsgType[protocol.Init] - transport.send(peerConnection, protocol.Init(Bob.nodeParams.features, TlvStream(InitTlv.Networks(Block.LivenetGenesisBlock.hash :: Block.SegnetGenesisBlock.hash :: Nil)))) + transport.send(peerConnection, protocol.Init(Bob.nodeParams.features.initFeatures(), TlvStream(InitTlv.Networks(Block.LivenetGenesisBlock.hash :: Block.SegnetGenesisBlock.hash :: Nil)))) transport.expectMsgType[TransportHandler.ReadAck] probe.expectTerminated(transport.ref) origin.expectMsg(PeerConnection.ConnectionResult.InitializationFailed("incompatible networks")) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala index 45bb103978..7c74e264d1 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala @@ -86,10 +86,10 @@ class PeerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Paralle withFixture(test.toNoArgTest(FixtureParam(aliceParams, remoteNodeId, peer, peerConnection, channel, switchboard))) } - def connect(remoteNodeId: PublicKey, peer: TestFSMRef[Peer.State, Peer.Data, Peer], peerConnection: TestProbe, switchboard: TestProbe, channels: Set[HasCommitments] = Set.empty, remoteInit: protocol.Init = protocol.Init(Bob.nodeParams.features)): Unit = { + def connect(remoteNodeId: PublicKey, peer: TestFSMRef[Peer.State, Peer.Data, Peer], peerConnection: TestProbe, switchboard: TestProbe, channels: Set[HasCommitments] = Set.empty, remoteInit: protocol.Init = protocol.Init(Bob.nodeParams.features.initFeatures())): Unit = { // let's simulate a connection switchboard.send(peer, Peer.Init(channels)) - val localInit = protocol.Init(peer.underlyingActor.nodeParams.features) + val localInit = protocol.Init(peer.underlyingActor.nodeParams.features.initFeatures()) switchboard.send(peer, PeerConnection.ConnectionReady(peerConnection.ref, remoteNodeId, fakeIPAddress.socketAddress, outgoing = true, localInit, remoteInit)) val probe = TestProbe() probe.send(peer, Peer.GetPeerInfo(Some(probe.ref.toTyped))) @@ -257,7 +257,7 @@ class PeerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Paralle monitor.expectMsg(FSM.Transition(reconnectionTask, ReconnectionTask.WAITING, ReconnectionTask.CONNECTING)) // we simulate a success - val dummyInit = protocol.Init(peer.underlyingActor.nodeParams.features) + val dummyInit = protocol.Init(peer.underlyingActor.nodeParams.features.initFeatures()) probe.send(peer, PeerConnection.ConnectionReady(peerConnection.ref, remoteNodeId, fakeIPAddress.socketAddress, outgoing = true, dummyInit, dummyInit)) // we make sure that the reconnection task has done a full circle @@ -353,7 +353,7 @@ class PeerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Paralle } // They want to use a channel type we don't support yet. { - val open = createOpenChannelMessage(TlvStream[OpenChannelTlv](ChannelTlv.ChannelTypeTlv(UnsupportedChannelType(Features(Map[Feature, FeatureSupport](StaticRemoteKey -> Mandatory), Set(UnknownFeature(22))))))) + val open = createOpenChannelMessage(TlvStream[OpenChannelTlv](ChannelTlv.ChannelTypeTlv(UnsupportedChannelType(Features[InitFeature](Map[Feature with InitFeature, FeatureSupport](StaticRemoteKey -> Mandatory), Set(UnknownFeature(22))))))) peerConnection.send(peer, open) peerConnection.expectMsg(Error(open.temporaryChannelId, "invalid channel_type=0x401000, expected channel_type=standard")) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/io/SwitchboardSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/io/SwitchboardSpec.scala index 3ec6f4a285..d452e3e379 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/io/SwitchboardSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/io/SwitchboardSpec.scala @@ -10,7 +10,7 @@ import fr.acinq.eclair.channel.ChannelIdAssigned import fr.acinq.eclair.io.Switchboard._ import fr.acinq.eclair.wire.internal.channel.ChannelCodecsSpec import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{Features, NodeParams, TestKitBaseClass, TimestampSecondLong, randomBytes32, randomKey} +import fr.acinq.eclair.{Features, InitFeature, NodeParams, TestKitBaseClass, TimestampSecondLong, randomBytes32, randomKey} import org.scalatest.funsuite.AnyFunSuiteLike import scodec.bits._ @@ -63,7 +63,7 @@ class SwitchboardSpec extends TestKitBaseClass with AnyFunSuiteLike { peer.expectMsg(Peer.Disconnect(remoteNodeId)) } - def sendFeatures(nodeParams: NodeParams, remoteNodeId: PublicKey, expectedFeatures: Features, expectedSync: Boolean): Unit = { + def sendFeatures(nodeParams: NodeParams, remoteNodeId: PublicKey, expectedFeatures: Features[InitFeature], expectedSync: Boolean): Unit = { val (probe, peer, peerConnection) = (TestProbe(), TestProbe(), TestProbe()) val switchboard = TestActorRef(new Switchboard(nodeParams, FakePeerFactory(probe, peer))) switchboard ! PeerConnection.Authenticated(peerConnection.ref, remoteNodeId) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/Bolt11InvoiceSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/Bolt11InvoiceSpec.scala index 58b58688d4..40fd3764ee 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/Bolt11InvoiceSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/Bolt11InvoiceSpec.scala @@ -21,7 +21,7 @@ import fr.acinq.bitcoin.{Block, BtcDouble, ByteVector32, Crypto, MilliBtcDouble, import fr.acinq.eclair.FeatureSupport.{Mandatory, Optional} import fr.acinq.eclair.Features.{PaymentMetadata, PaymentSecret, _} import fr.acinq.eclair.payment.Bolt11Invoice._ -import fr.acinq.eclair.{CltvExpiryDelta, Features, MilliSatoshiLong, ShortChannelId, TestConstants, TimestampSecond, TimestampSecondLong, ToMilliSatoshiConversion} +import fr.acinq.eclair.{CltvExpiryDelta, Feature, FeatureScope, FeatureSupport, Features, InvoiceFeature, MilliSatoshi, MilliSatoshiLong, ShortChannelId, TestConstants, TimestampSecond, TimestampSecondLong, ToMilliSatoshiConversion, UnknownFeature, randomBytes32, randomKey} import org.scalatest.funsuite.AnyFunSuite import scodec.DecodeResult import scodec.bits._ @@ -41,6 +41,46 @@ class Bolt11InvoiceSpec extends AnyFunSuite { val nodeId = pub assert(nodeId == PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) + // Copy of Bolt11Invoice.apply that doesn't strip unknown features + def createInvoiceUnsafe(chainHash: ByteVector32, + amount: Option[MilliSatoshi], + paymentHash: ByteVector32, + privateKey: PrivateKey, + description: Either[String, ByteVector32], + minFinalCltvExpiryDelta: CltvExpiryDelta, + fallbackAddress: Option[String] = None, + expirySeconds: Option[Long] = None, + extraHops: List[List[ExtraHop]] = Nil, + timestamp: TimestampSecond = TimestampSecond.now(), + paymentSecret: ByteVector32 = randomBytes32(), + paymentMetadata: Option[ByteVector] = None, + features: Features[FeatureScope] = defaultFeatures.unscoped()): Bolt11Invoice = { + require(features.hasFeature(Features.PaymentSecret, Some(FeatureSupport.Mandatory)), "invoices must require a payment secret") + val prefix = prefixes(chainHash) + val tags = { + val defaultTags = List( + Some(PaymentHash(paymentHash)), + Some(description.fold(Description, DescriptionHash)), + Some(Bolt11Invoice.PaymentSecret(paymentSecret)), + paymentMetadata.map(Bolt11Invoice.PaymentMetadata), + fallbackAddress.map(FallbackAddress(_)), + expirySeconds.map(Expiry(_)), + Some(MinFinalCltvExpiry(minFinalCltvExpiryDelta.toInt)), + Some(InvoiceFeatures(features)) + ).flatten + val routingInfoTags = extraHops.map(RoutingInfo) + defaultTags ++ routingInfoTags + } + Bolt11Invoice( + prefix = prefix, + amount_opt = amount, + createdAt = timestamp, + nodeId = privateKey.publicKey, + tags = tags, + signature = ByteVector.empty + ).sign(privateKey) + } + test("check minimal unit is used") { assert('p' === Amount.unit(1 msat)) assert('p' === Amount.unit(99 msat)) @@ -97,7 +137,7 @@ class Bolt11InvoiceSpec extends AnyFunSuite { assert(invoice.amount_opt.isEmpty) assert(invoice.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102") assert(invoice.paymentSecret.map(_.bytes) === Some(hex"1111111111111111111111111111111111111111111111111111111111111111")) - assert(invoice.features === Features(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory)) + assert(invoice.features === Features(Features.VariableLengthOnion -> Mandatory, Features.PaymentSecret -> Mandatory)) assert(invoice.createdAt == TimestampSecond(1496314658L)) assert(invoice.nodeId == PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) assert(invoice.description == Left("Please consider supporting this project")) @@ -112,7 +152,7 @@ class Bolt11InvoiceSpec extends AnyFunSuite { assert(invoice.prefix == "lnbc") assert(invoice.amount_opt === Some(250000000 msat)) assert(invoice.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102") - assert(invoice.features === Features(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory)) + assert(invoice.features === Features(Features.VariableLengthOnion -> Mandatory, Features.PaymentSecret -> Mandatory)) assert(invoice.createdAt == TimestampSecond(1496314658L)) assert(invoice.nodeId == PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) assert(invoice.description == Left("1 cup coffee")) @@ -445,8 +485,8 @@ class Bolt11InvoiceSpec extends AnyFunSuite { ) for ((features, res) <- featureBits) { - val invoice = Bolt11Invoice(Block.LivenetGenesisBlock.hash, Some(123 msat), ByteVector32.One, priv, Left("Some invoice"), CltvExpiryDelta(18), features = features) - assert(Result(invoice.features.hasFeature(BasicMultiPartPayment), invoice.features.hasFeature(PaymentSecret, Some(Mandatory)), nodeParams.features.areSupported(invoice.features)) === res) + val invoice = createInvoiceUnsafe(Block.LivenetGenesisBlock.hash, Some(123 msat), ByteVector32.One, priv, Left("Some invoice"), CltvExpiryDelta(18), features = features) + assert(Result(invoice.features.hasFeature(BasicMultiPartPayment), invoice.features.hasFeature(PaymentSecret, Some(Mandatory)), nodeParams.features.invoiceFeatures().areSupported(invoice.features)) === res) assert(Bolt11Invoice.fromString(invoice.toString) === invoice) } } @@ -510,69 +550,85 @@ class Bolt11InvoiceSpec extends AnyFunSuite { test("nonreg") { val requests = List( - "lnbc40n1pw9qjvwpp5qq3w2ln6krepcslqszkrsfzwy49y0407hvks30ec6pu9s07jur3sdpstfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqenwdencqzysxqrrss7ju0s4dwx6w8a95a9p2xc5vudl09gjl0w2n02sjrvffde632nxwh2l4w35nqepj4j5njhh4z65wyfc724yj6dn9wajvajfn5j7em6wsq2elakl", - "lnbc1500n1pwyvqwfpp5p5nxwpuk02nd2xtzwex97gtjlpdv0lxj5z08vdd0hes7a0h437qsdpa2fjkzep6yp8kumrfdejjqempd43xc6twvusxjueqd9kxcet8v9kzqct8v95kucqzysxqr23s8r9seqv6datylwtjcvlpdkukfep7g80hujz3w8t599saae7gap6j48gs97z4fvrx4t4ajra6pvdyf5ledw3tg7h2s3606qm79kk59zqpeygdhd", - "lnbc800n1pwykdmfpp5zqjae54l4ecmvm9v338vw2n07q2ehywvy4pvay53s7068t8yjvhqdqddpjkcmr0yysjzcqp27lya2lz7d80uxt6vevcwzy32227j3nsgyqlrxuwgs22u6728ldszlc70qgcs56wglrutn8jnnnelsk38d6yaqccmw8kmmdlfsyjd20qp69knex", - "lnbc300n1pwzezrnpp5zgwqadf4zjygmhf3xms8m4dd8f4mdq26unr5mfxuyzgqcgc049tqdq9dpjhjcqp23gxhs2rawqxdvr7f7lmj46tdvkncnsz8q5jp2kge8ndfm4dpevxrg5xj4ufp36x89gmaw04lgpap7e3x9jcjydwhcj9l84wmts2lg6qquvpque", - "lnbc10n1pdm2qaxpp5zlyxcc5dypurzyjamt6kk6a8rpad7je5r4w8fj79u6fktnqu085sdpl2pshjmt9de6zqen0wgsrzgrsd9ux2mrnypshggrnv96x7umgd9ejuurvv93k2tsxqzjccqp2e3nq4xh20prn9kx8etqgjjekzzjhep27mnqtyy62makh4gqc4akrzhe3nmj8lnwtd40ne5gn8myruvrt9p6vpuwmc4ghk7587erwqncpx9sds0", - "lnbc800n1pwp5uuhpp5y8aarm9j9x9cer0gah9ymfkcqq4j4rn3zr7y9xhsgar5pmaceaqqdqdvf5hgcm0d9hzzcqp2vf8ramzsgdznxd5yxhrxffuk43pst9ng7cqcez9p2zvykpcf039rp9vutpe6wfds744yr73ztyps2z58nkmflye9yt4v3d0qz8z3d9qqq3kv54", - "lnbc1500n1pdl686hpp5y7mz3lgvrfccqnk9es6trumjgqdpjwcecycpkdggnx7h6cuup90sdpa2fjkzep6ypqkymm4wssycnjzf9rjqurjda4x2cm5ypskuepqv93x7at5ypek7cqzysxqr23s5e864m06fcfp3axsefy276d77tzp0xzzzdfl6p46wvstkeqhu50khm9yxea2d9efp7lvthrta0ktmhsv52hf3tvxm0unsauhmfmp27cqqx4xxe", - "lnbc80n1pwykw99pp5965lyj4uesussdrk0lfyd2qss9m23yjdjkpmhw0975zky2xlhdtsdpl2pshjmt9de6zqen0wgsrsgrsd9ux2mrnypshggrnv96x7umgd9ejuurvv93k2tsxqzjccqp27677yc44l22jxexewew7lzka7g5864gdpr6y5v6s6tqmn8xztltk9qnna2qwrsm7gfyrpqvhaz4u3egcalpx2gxef3kvqwd44hekfxcqr7nwhf", - "lnbc2200n1pwp4pwnpp5xy5f5kl83ytwuz0sgyypmqaqtjs68s3hrwgnnt445tqv7stu5kyqdpyvf5hgcm0d9hzqmn0wssxymr0vd4kx6rpd9hqcqp25y9w3wc3ztxhemsqch640g4u00szvvfk4vxr7klsakvn8cjcunjq8rwejzy6cfwj90ulycahnq43lff8m84xqf3tusslq2w69htwwlcpfqskmc", - "lnbc300n1pwp50ggpp5x7x5a9zs26amr7rqngp2sjee2c3qc80ztvex00zxn7xkuzuhjkxqdq9dpjhjcqp2s464vnrpx7aynh26vsxx6s3m52x88dqen56pzxxnxmc9s7y5v0dprsdlv5q430zy33lcl5ll6uy60m7c9yrkjl8yxz7lgsqky3ka57qq4qeyz3", - "lnbc10n1pd6jt93pp58vtzf4gup4vvqyknfakvh59avaek22hd0026snvpdnc846ypqrdsdp0tfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqcnqvscqzysxqyd9uq3sv9xkv2sgdf2nuvs97d2wkzj5g75rljnh5wy5wqhnauvqhxd9fpq898emtz8hul8cnxmc9wtj2777ehgnnyhcrs0y5zuhy8rs0jv6cqqe24tw", - "lnbc890n1pwzu4uqpp5gy274lq0m5hzxuxy90vf65wchdszrazz9zxjdk30ed05kyjvwxrqdzq2pshjmt9de6zqen0wgsrswfqwp5hsetvwvsxzapqwdshgmmndp5hxtnsd3skxefwxqzjccqp2qjvlfyl4rmc56gerx70lxcrjjlnrjfz677ezw4lwzy6syqh4rnlql6t6n3pdfxkcal9jp98plgf2zqzz8jxfza9vjw3vd4t62ws8gkgqhv9x28", - "lnbc79760n1pd7cwyapp5gevl4mv968fs4le3tytzhr9r8tdk8cu3q7kfx348ut7xyntvnvmsdz92pskjepqw3hjqmrfva58gmnfdenjqumvda6zqmtpvd5xjmn9ypnx7u3qx5czq5msd9h8xcqzysxqrrssjzky68fdnhvee7aw089d5zltahfhy2ffa96pwf7fszjnm6mv0fzpv88jwaenm5qfg64pl768q8hf2vnvc5xsrpqd45nca2mewsv55wcpmhskah", - "lnbc90n1pduns5qpp5f5h5ghga4cp7uj9de35ksk00a2ed9jf774zy7va37k5zet5cds8sdpl2pshjmt9de6zqen0wgsrjgrsd9ux2mrnypshggrnv96x7umgd9ejuurvv93k2tsxqzjccqp28ynysm3clcq865y9umys8t2f54anlsu2wfpyfxgq09ht3qfez9x9z9fpff8wzqwzua2t9vayzm4ek3vf4k4s5cdg3a6hp9vsgg9klpgpmafvnv", - "lnbc10u1pw9nehppp5tf0cpc3nx3wpk6j2n9teqwd8kuvryh69hv65w7p5u9cqhse3nmgsdzz2p6hycmgv9ek2gr0vcsrzgrxd3hhwetjyphkugzzd96xxmmfdcsywunpwejhjctjvscqp222vxxwq70temepf6n0xlzk0asr43ppqrt0mf6eclnfd5mxf6uhv5wvsqgdvht6uqxfw2vgdku5gfyhgguepvnjfu7s4kuthtnuxy0hsq6wwv9d", - "lnbc30n1pw9qjwmpp5tcdc9wcr0avr5q96jlez09eax7djwmc475d5cylezsd652zvptjsdpstfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqenwdf4cqzysxqrrss7r8gn9d6klf2urzdjrq3x67a4u25wpeju5utusnc539aj5462y7kv9w56mndcx8jad7aa7qz8f8qpdw9qlx52feyemwd7afqxu45jxsqyzwns9", - "lnbc10u1pw9x36xpp5tlk00k0dftfx9vh40mtdlu844c9v65ad0kslnrvyuzfxqhdur46qdzz2p6hycmgv9ek2gr0vcsrzgrxd3hhwetjyphkugzzd96xxmmfdcsywunpwejhjctjvscqp2fpudmf4tt0crardf0k7vk5qs4mvys88el6e7pg62hgdt9t6ckf48l6jh4ckp87zpcnal6xnu33hxdd8k27vq2702688ww04kc065r7cqw3cqs3", - "lnbc40n1pd6jttkpp5v8p97ezd3uz4ruw4w8w0gt4yr3ajtrmaeqe23ttxvpuh0cy79axqdp0tfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqcnqvscqzysxqyd9uq3r88ajpz77z6lg4wc7srhsk7m26guuvhdlpea6889m9jnc9a25sx7rdtryjukew86mtcngl6d8zqh9trtu60cmmwfx6845q08z06p6qpl3l55t", - "lnbc1pwr7fqhpp5vhur3ahtumqz5mkramxr22597gaa9rnrjch8gxwr9h7r56umsjpqdpl235hqurfdcs9xct5daeks6tngask6etnyq58g6tswp5kutndv55jsaf3x5unj2gcqzysxqyz5vq88jysqvrwhq6qe38jdulefx0z9j7sfw85wqc6athfx9h77fjnjxjvprz76ayna0rcjllgu5ka960rul3qxvsrr9zth5plaerq96ursgpsshuee", - "lnbc10n1pw9rt5hpp5dsv5ux7xlmhmrpqnffgj6nf03mvx5zpns3578k2c5my3znnhz0gqdpstfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqenwwp3cqzysxqrrssnrasvcr5ydng283zdfpw38qtqfxjnzhdmdx9wly9dsqmsxvksrkzkqrcenu6h36g4g55q56ejk429nm4zjfgssh8uhs7gs760z63ggcqp3gyd6", - "lnbc1500n1pd7u7p4pp5d54vffcehkcy79gm0fkqrthh3y576jy9flzpy9rf6syua0s5p0jqdpa2fjkzep6ypxhjgz90pcx2unfv4hxxefqdanzqargv5s9xetrdahxggzvd9nkscqzysxqr23sklptztnk25aqzwty35gk9q7jtfzjywdfx23d8a37g2eaejrv3d9nnt87m98s4eps87q87pzfd6hkd077emjupe0pcazpt9kaphehufqqu7k37h", - "lnbc10n1pdunsmgpp5wn90mffjvkd06pe84lpa6e370024wwv7xfw0tdxlt6qq8hc7d7rqdp0tfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqcngvscqzysxqyd9uqs0cqtrum6h7dct88nkjxwxvte7hjh9pusx64tp35u0m6qhqy5dgn9j27fs37mg0w3ruf7enxlsc9xmlasgjzyyaaxqdxu9x5w0md4fspgz8twv", - "lnbc700n1pwp50wapp5w7eearwr7qjhz5vk5zq4g0t75f90mrekwnw4e795qfjxyaq27dxsdqvdp6kuar9wgeqcqp20gfw78vvasjm45l6zfxmfwn59ac9dukp36mf0y3gpquhp7rptddxy7d32ptmqukeghvamlkmve9n94sxmxglun4zwtkyhk43e6lw8qspc9y9ww", - "lnbc10n1pd6jvy5pp50x9lymptter9najcdpgrcnqn34wq34f49vmnllc57ezyvtlg8ayqdpdtfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yq6rvcqzysxqyd9uqcejk56vfz3y80u3npefpx82f0tghua88a8x2d33gmxcjm45q6l5xwurwyp9aj2p59cr0lknpk0eujfdax32v4px4m22u6zr5z40zxvqp5m85cr", - "lnbc10n1pw9pqz7pp50782e2u9s25gqacx7mvnuhg3xxwumum89dymdq3vlsrsmaeeqsxsdpstfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqenwd3ccqzysxqrrsstxqhw2kvdfwsf7c27aaae45fheq9rzndesu4mph9dq08sawa0auz7e0z7jn9qf3zphegv2ermup0fgce0phqmf73j4zx88v3ksrgeeqq9yzzad", - "lnbc1300n1pwq4fx7pp5sqmq97yfxhhk7xv7u8cuc8jgv5drse45f5pmtx6f5ng2cqm332uqdq4e2279q9zux62tc5q5t9fgcqp29a662u3p2h4h4ucdav4xrlxz2rtwvvtward7htsrldpsc5erknkyxu0x2xt9qv0u766jadeetsz9pj4rljpjy0g8ayqqt2q8esewsrqpc8v4nw", - "lnbc1u1pd7u7tnpp5s9he3ccpsmfdkzrsjns7p3wpz7veen6xxwxdca3khwqyh2ezk8kqdqdg9jxgg8sn7f27cqzysxqr23ssm4krdc4s0zqhfk97n0aclxsmaga208pa8c0hz3zyauqsjjxfj7kw6t29dkucp68s8s4zfdgp97kkmzgy25yuj0dcec85d9c50sgjqgq5jhl4e", - "lnbc1200n1pwq5kf2pp5snkm9kr0slgzfc806k4c8q93d4y57q3lz745v2hefx952rhuymrqdq509shjgrzd96xxmmfdcsscqp2w5ta9uwzhmxxp0mnhwwvnjdn6ev4huj3tha5d80ajv2p5phe8wk32yn7ch6lennx4zzawqtd34aqetataxjmrz39gzjl256walhw03gpxz79rr", - "lnbc1500n1pd7u7v0pp5s6d0wqexag3aqzugaw3gs7hw7a2wrq6l8fh9s42ndqu8zu480m0sdqvg9jxgg8zn2sscqzysxqr23sm23myatjdsp3003rlasgzwg3rlr0ca8uqdt5d79lxmdwqptufr89r5rgk4np4ag0kcw7at6s6eqdany0k6m0ezjva0cyda5arpaw7lcqgzjl7u", - "lnbc100n1pd6jv8ypp53p6fdd954h3ffmyj6av4nzcnwfuyvn9rrsc2u6y22xnfs0l0cssqdpdtfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqerscqzysxqyd9uqyefde4la0qmglafzv8q34wqsf4mtwd8ausufavkp2e7paewd3mqsg0gsdmvrknw80t92cuvu9raevrnxtpsye0utklhpunsz68a9veqpkypx9j", - "lnbc2300n1pwp50w8pp53030gw8rsqac6f3sqqa9exxwfvphsl4v4w484eynspwgv5v6vyrsdp9w35xjueqd9ejqmn0wssx67fqwpshxumhdaexgcqp2zmspcx992fvezxqkyf3rkcxc9dm2vr4ewfx42c0fccg4ea72fyd3pd6vn94tfy9t39y0hg0hupak2nv0n6pzy8culeceq8kzpwjy0tsp4fwqw5", - "lnbc10n1pwykdlhpp53392ama65h3lnc4w55yqycp9v2ackexugl0ahz4jyc7fqtyuk85qdpstfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqenwvejcqzysxqrrsszkwrx54an8lhr9h4h3d7lgpjrd370zucx0fdusaklqh2xgytr8hhgq5u0kvs56l8j53uktlmz3mqhhmn88kwwxfksnham9p6ws5pwxsqnpzyda", - "lnbc10470n1pw9qf40pp535pels2faqwau2rmqkgzn0rgtsu9u6qaxe5y6ttgjx5qm4pg0kgsdzy2pshjmt9de6zqen0wgsrzvp5xus8q6tcv4k8xgrpwss8xct5daeks6tn9ecxcctrv5hqxqzjccqp27sp3m204a7d47at5jkkewa7rvewdmpwaqh2ss72cajafyf7dts9ne67hw9pps2ud69p4fw95y9cdk35aef43cv35s0zzj37qu7s395cp2vw5mu", - "lnbc100n1pwytlgspp5365rx7ell807x5jsr7ykf2k7p5z77qvxjx8x6pfhh5298xnr6d2sdpstfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqenwvpscqzysxqrrssh9mphycg7e9lr58c267yerlcd9ka8lrljm8ygpnwu2v63jm7ax48y7qal25qy0ewpxw39r5whnqh93zw97gnnw64ss97n69975wh9gsqj7vudu", - "lnbc210n1pdunsefpp5jxn3hlj86evlwgsz5d70hquy78k28ahdwjmlagx6qly9x29pu4uqdzq2pshjmt9de6zqen0wgsryvfqwp5hsetvwvsxzapqwdshgmmndp5hxtnsd3skxefwxqzjccqp2snr8trjcrr5xyy7g63uq7mewqyp9k3d0duznw23zhynaz6pj3uwk48yffqn8p0jugv2z03dxquc8azuwr8myjgwzh69a34fl2lnmq2sppac733", - "lnbc1700n1pwr7z98pp5j5r5q5c7syavjjz7czjvng4y95w0rd8zkl7q43sm7spg9ht2sjfqdquwf6kumnfdenjqmrfva58gmnfdenscqp2jrhlc758m734gw5td4gchcn9j5cp5p38zj3tcpvgkegxewat38d3h24kn0c2ac2pleuqp5dutvw5fmk4d2v3trcqhl5pdxqq8swnldcqtq0akh", - "lnbc1500n1pdl05k5pp5nyd9netjpzn27slyj2np4slpmlz8dy69q7hygwm8ff4mey2jee5sdpa2fjkzep6ypxhjgz90pcx2unfv4hxxefqdanzqargv5s9xetrdahxggzvd9nkscqzysxqr23sqdd8t97qjc77pqa7jv7umc499jqkk0kwchapswj3xrukndr7g2nqna5x87n49uynty4pxexkt3fslyle7mwz708rs0rnnn44dnav9mgplf0aj7", - "lnbc1u1pwyvxrppp5nvm98wnqdee838wtfmhfjx9s49eduzu3rx0fqec2wenadth8pxqsdqdg9jxgg8sn7vgycqzysxqr23snuza3t8x0tvusu07epal9rqxh4cq22m64amuzd6x607s0w55a5xpefp2xlxmej9r6nktmwv5td3849y2sg7pckwk9r8vqqps8g4u66qq85mp3g", - "lnbc10n1pw9qjwppp55nx7xw3sytnfle67mh70dyukr4g4chyfmp4x4ag2hgjcts4kydnsdpstfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqenwd3ccqzysxqrrss7t24v6w7dwtd65g64qcz77clgye7n8l0j67qh32q4jrw9d2dk2444vma7j6nedgx2ywel3e9ns4r257zprsn7t5uca045xxudz9pqzsqfena6v", - "lnbc10u1pw9x373pp549mpcznu3q0r4ml095kjg38pvsdptzja8vhpyvc2avatc2cegycsdzz2p6hycmgv9ek2gr0vcsrzgrxd3hhwetjyphkugzzd96xxmmfdcsywunpwejhjctjvscqp2tgqwhzyjmpfymrshnaw6rwmy4rgrtjmmp66dr9v54xp52rsyzqd5htc3lu3k52t06fqk8yj05nsw0nnssak3ywev4n3xs3jgz42urmspjeqyw0", - "lnbc1500n1pd7u7vupp54jm8s8lmgnnru0ndwpxhm5qwllkrarasr9fy9zkunf49ct8mw9ssdqvg9jxgg8zn2sscqzysxqr23s4njradkzzaswlsgs0a6zc3cd28xc08t5car0k7su6q3u3vjvqt6xq2kpaadgt5x9suxx50rkevfw563fupzqzpc9m6dqsjcr8qt6k2sqelr838", - "lnbc720n1pwypj4epp5k2saqsjznpvevsm9mzqfan3d9fz967x5lp39g3nwsxdkusps73csdzq2pshjmt9de6zqen0wgsrwv3qwp5hsetvwvsxzapqwdshgmmndp5hxtnsd3skxefwxqzjccqp2d3ltxtq0r795emmp7yqjjmmzl55cgju004vw08f83e98d28xmw44t4styhfhgsrwxydf68m2kup7j358zdrmhevqwr0hlqwt2eceaxcq7hezhx", - "lnbc10n1pwykdacpp5kegv2kdkxmetm2tpnzfgt4640n7mgxl95jnpc6fkz6uyjdwahw8sdpstfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqenwdp5cqzysxqrrssjlny2skwtnnese9nmw99xlh7jwgtdxurhce2zcwsamevmj37kd5yzxzu55mt567seewmajra2hwyry5cv9kfzf02paerhs7tf9acdcgq24pqer", - "lnbc3100n1pwp370spp5ku7y6tfz5up840v00vgc2vmmqtpsu5ly98h09vxv9d7k9xtq8mrsdpjd35kw6r5de5kueevypkxjemgw3hxjmn89ssxc6t8dp6xu6twvucqp2sunrt8slx2wmvjzdv3vvlls9gez7g2gd37g2pwa4pnlswuxzy0w3hd5kkqdrpl4ylcdhvkvuamwjsfh79nkn52dq0qpzj8c4rf57jmgqschvrr", - "lnbc1500n1pwr7z8rpp5hyfkmnwwx7x902ys52du8pph6hdkarnqvj6fwhh9swfsg5lp94vsdpa2fjkzep6ypph2um5dajxjctvypmkzmrvv468xgrpwfjjqetkd9kzqctwvss8ycqzysxqr23s64a2h7gn25pchh8r6jpe236h925fylw2jcm4pd92w8hkmpflreph8r6s8jnnml0zu47qv6t2sj6frnle2cpanf6e027vsddgkl8hk7gpta89d0", - "lnbc1500n1pdl05v0pp5c4t5p3renelctlh0z4jpznyxna7lw9zhws868wktp8vtn8t5a8uqdpa2fjkzep6ypxxjemgw35kueeqfejhgam0wf4jqnrfw96kjerfw3ujq5r0dakq6cqzysxqr23s7k3ktaae69gpl2tfleyy2rsm0m6cy5yvf8uq7g4dmpyrwvfxzslnvryx5me4xh0fsp9jfjsqkuwpzx9ydwe6ndrm0eznarhdrfwn5gsp949n7x", - "lnbc1500n1pwyvxp3pp5ch8jx4g0ft0f6tzg008vr82wv92sredy07v46h7q3h3athx2nm2sdpa2fjkzep6ypyx7aeqfys8w6tndqsx67fqw35x2gzvv4jxwetjypvzqam0w4kxgcqzysxqr23s3hdgx90a6jcqgl84z36dv6kn6eg4klsaje2kdm84662rq7lzzzlycvne4l8d0steq5pctdp4ffeyhylgrt7ln92l8dyvrnsn9qg5qkgqrz2cra", - "lnbc1500n1pwr7z2ppp5cuzt0txjkkmpz6sgefdjjmdrsj9gl8fqyeu6hx7lj050f68yuceqdqvg9jxgg8zn2sscqzysxqr23s7442lgk6cj95qygw2hly9qw9zchhag5p5m3gyzrmws8namcsqh5nz2nm6a5sc2ln6jx59sln9a7t8vxtezels2exurr0gchz9gk0ufgpwczm3r", - "lnbc1500n1pd7u7g4pp5eam7uhxc0w4epnuflgkl62m64qu378nnhkg3vahkm7dhdcqnzl4sdqvg9jxgg8zn2sscqzysxqr23s870l2549nhsr2dfv9ehkl5z95p5rxpks5j2etr35e02z9r6haalrfjs7sz5y7wzenywp8t52w89c9u8taf9m76t2p0w0vxw243y7l4spqdue7w", - "lnbc5u1pwq2jqzpp56zhpjmfm72e8p8vmfssspe07u7zmnm5hhgynafe4y4lwz6ypusvqdzsd35kw6r5de5kuemwv468wmmjddehgmmjv4ejucm0d40n2vpsta6hqan0w3jhxhmnw3hhye2fgs7nywfhcqp2tqnqpewrz28yrvvvyzjyrvwahuy595t4w4ar3cvt5cq9jx3rmxd4p7vjgmeylfkgjrssc66a9q9hhnd4aj7gqv2zj0jr2zt0gahnv0sp9y675y", - "lnbc10n1pw9pqp3pp562wg5n7atx369mt75feu233cnm5h508mx7j0d807lqe0w45gndnqdpstfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqenwdejcqzysxqrrsszfg9lfawdhnp2m785cqgzg4c85mvgct44xdzjea9t0vu4mc22u4prjjz5qd4y7uhgg3wm57muh5wfz8l04kgyq8juwql3vaffm23akspzkmj53", - "lnbc90n1pwypjnppp5m870lhg8qjrykj6hfegawaq0ukzc099ntfezhm8jr48cw5ywgpwqdpl2pshjmt9de6zqen0wgsrjgrsd9ux2mrnypshggrnv96x7umgd9ejuurvv93k2tsxqzjccqp2s0n2u7msmypy9dh96e6exfas434td6a7f5qy5shzyk4r9dxwv0zhyxcqjkmxgnnkjvqhthadhkqvvd66f8gxkdna3jqyzhnnhfs6w3qpme2zfz", - "lnbc100n1pdunsurpp5af2vzgyjtj2q48dxl8hpfv9cskwk7q5ahefzyy3zft6jyrc4uv2qdp0tfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqcnyvccqzysxqyd9uqpcp608auvkcr22672nhwqqtul0q6dqrxryfsstttlwyvkzttxt29mxyshley6u45gf0sxc0d9dxr5fk48tj4z2z0wh6asfxhlsea57qp45tfua", - "lnbc100n1pd6hzfgpp5au2d4u2f2gm9wyz34e9rls66q77cmtlw3tzu8h67gcdcvj0dsjdqdp0tfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqcnqvscqzysxqyd9uqxg5n7462ykgs8a23l3s029dun9374xza88nlf2e34nupmc042lgps7tpwd0ue0he0gdcpfmc5mshmxkgw0hfztyg4j463ux28nh2gagqage30p", - "lnbc50n1pdl052epp57549dnjwf2wqfz5hg8khu0wlkca8ggv72f9q7x76p0a7azkn3ljsdp0tfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqcnvvscqzysxqyd9uqa2z48kchpmnyafgq2qlt4pruwyjh93emh8cd5wczwy47pkx6qzarmvl28hrnqf98m2rnfa0gx4lnw2jvhlg9l4265240av6t9vdqpzsqntwwyx", - "lnbc100n1pd7cwrypp57m4rft00sh6za2x0jwe7cqknj568k9xajtpnspql8dd38xmd7musdp0tfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqcngvscqzysxqyd9uqsxfmfv96q0d7r3qjymwsem02t5jhtq58a30q8lu5dy3jft7wahdq2f5vc5qqymgrrdyshff26ak7m7n0vqyf7t694vam4dcqkvnr65qp6wdch9", - "lnbc100n1pw9qjdgpp5lmycszp7pzce0rl29s40fhkg02v7vgrxaznr6ys5cawg437h80nsdpstfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqenwdejcqzysxqrrss47kl34flydtmu2wnszuddrd0nwa6rnu4d339jfzje6hzk6an0uax3kteee2lgx5r0629wehjeseksz0uuakzwy47lmvy2g7hja7mnpsqjmdct9", - "lnbc25m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5vdhkven9v5sxyetpdeessp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9q5sqqqqqqqqqqqqqqqpqsq67gye39hfg3zd8rgc80k32tvy9xk2xunwm5lzexnvpx6fd77en8qaq424dxgt56cag2dpt359k3ssyhetktkpqh24jqnjyw6uqd08sgptq44qu", - "lnbc25m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5vdhkven9v5sxyetpdeessp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9q4psqqqqqqqqqqqqqqqpqsqq40wa3khl49yue3zsgm26jrepqr2eghqlx86rttutve3ugd05em86nsefzh4pfurpd9ek9w2vp95zxqnfe2u7ckudyahsa52q66tgzcp6t2dyk" + "lnbc40n1pw9qjvwpp5qq3w2ln6krepcslqszkrsfzwy49y0407hvks30ec6pu9s07jur3sdpstfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqenwdencqzysxqrrss7ju0s4dwx6w8a95a9p2xc5vudl09gjl0w2n02sjrvffde632nxwh2l4w35nqepj4j5njhh4z65wyfc724yj6dn9wajvajfn5j7em6wsq2elakl" -> PublicKey(hex"02cda8c01b2303e91bec74c43093d5f1c4fd42a95671ae27bf853d7dfea9b78c06"), + "lnbc1500n1pwyvqwfpp5p5nxwpuk02nd2xtzwex97gtjlpdv0lxj5z08vdd0hes7a0h437qsdpa2fjkzep6yp8kumrfdejjqempd43xc6twvusxjueqd9kxcet8v9kzqct8v95kucqzysxqr23s8r9seqv6datylwtjcvlpdkukfep7g80hujz3w8t599saae7gap6j48gs97z4fvrx4t4ajra6pvdyf5ledw3tg7h2s3606qm79kk59zqpeygdhd" -> PublicKey(hex"03e50492eab4107a773141bb419e107bda3de3d55652e6e1a41225f06a0bbf2d56"), + "lnbc800n1pwykdmfpp5zqjae54l4ecmvm9v338vw2n07q2ehywvy4pvay53s7068t8yjvhqdqddpjkcmr0yysjzcqp27lya2lz7d80uxt6vevcwzy32227j3nsgyqlrxuwgs22u6728ldszlc70qgcs56wglrutn8jnnnelsk38d6yaqccmw8kmmdlfsyjd20qp69knex" -> PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f"), + "lnbc300n1pwzezrnpp5zgwqadf4zjygmhf3xms8m4dd8f4mdq26unr5mfxuyzgqcgc049tqdq9dpjhjcqp23gxhs2rawqxdvr7f7lmj46tdvkncnsz8q5jp2kge8ndfm4dpevxrg5xj4ufp36x89gmaw04lgpap7e3x9jcjydwhcj9l84wmts2lg6qquvpque" -> PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f"), + "lnbc10n1pdm2qaxpp5zlyxcc5dypurzyjamt6kk6a8rpad7je5r4w8fj79u6fktnqu085sdpl2pshjmt9de6zqen0wgsrzgrsd9ux2mrnypshggrnv96x7umgd9ejuurvv93k2tsxqzjccqp2e3nq4xh20prn9kx8etqgjjekzzjhep27mnqtyy62makh4gqc4akrzhe3nmj8lnwtd40ne5gn8myruvrt9p6vpuwmc4ghk7587erwqncpx9sds0" -> PublicKey(hex"024655b768ef40951b20053a5c4b951606d4d86085d51238f2c67c7dec29c792ca"), + "lnbc800n1pwp5uuhpp5y8aarm9j9x9cer0gah9ymfkcqq4j4rn3zr7y9xhsgar5pmaceaqqdqdvf5hgcm0d9hzzcqp2vf8ramzsgdznxd5yxhrxffuk43pst9ng7cqcez9p2zvykpcf039rp9vutpe6wfds744yr73ztyps2z58nkmflye9yt4v3d0qz8z3d9qqq3kv54" -> PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f"), + "lnbc1500n1pdl686hpp5y7mz3lgvrfccqnk9es6trumjgqdpjwcecycpkdggnx7h6cuup90sdpa2fjkzep6ypqkymm4wssycnjzf9rjqurjda4x2cm5ypskuepqv93x7at5ypek7cqzysxqr23s5e864m06fcfp3axsefy276d77tzp0xzzzdfl6p46wvstkeqhu50khm9yxea2d9efp7lvthrta0ktmhsv52hf3tvxm0unsauhmfmp27cqqx4xxe" -> PublicKey(hex"03e50492eab4107a773141bb419e107bda3de3d55652e6e1a41225f06a0bbf2d56"), + "lnbc80n1pwykw99pp5965lyj4uesussdrk0lfyd2qss9m23yjdjkpmhw0975zky2xlhdtsdpl2pshjmt9de6zqen0wgsrsgrsd9ux2mrnypshggrnv96x7umgd9ejuurvv93k2tsxqzjccqp27677yc44l22jxexewew7lzka7g5864gdpr6y5v6s6tqmn8xztltk9qnna2qwrsm7gfyrpqvhaz4u3egcalpx2gxef3kvqwd44hekfxcqr7nwhf" -> PublicKey(hex"024655b768ef40951b20053a5c4b951606d4d86085d51238f2c67c7dec29c792ca"), + "lnbc2200n1pwp4pwnpp5xy5f5kl83ytwuz0sgyypmqaqtjs68s3hrwgnnt445tqv7stu5kyqdpyvf5hgcm0d9hzqmn0wssxymr0vd4kx6rpd9hqcqp25y9w3wc3ztxhemsqch640g4u00szvvfk4vxr7klsakvn8cjcunjq8rwejzy6cfwj90ulycahnq43lff8m84xqf3tusslq2w69htwwlcpfqskmc" -> PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f"), + "lnbc300n1pwp50ggpp5x7x5a9zs26amr7rqngp2sjee2c3qc80ztvex00zxn7xkuzuhjkxqdq9dpjhjcqp2s464vnrpx7aynh26vsxx6s3m52x88dqen56pzxxnxmc9s7y5v0dprsdlv5q430zy33lcl5ll6uy60m7c9yrkjl8yxz7lgsqky3ka57qq4qeyz3" -> PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f"), + "lnbc10n1pd6jt93pp58vtzf4gup4vvqyknfakvh59avaek22hd0026snvpdnc846ypqrdsdp0tfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqcnqvscqzysxqyd9uq3sv9xkv2sgdf2nuvs97d2wkzj5g75rljnh5wy5wqhnauvqhxd9fpq898emtz8hul8cnxmc9wtj2777ehgnnyhcrs0y5zuhy8rs0jv6cqqe24tw" -> PublicKey(hex"03a9d79bcfab7feb0f24c3cd61a57f0f00de2225b6d31bce0bc4564efa3b1b5aaf"), + "lnbc890n1pwzu4uqpp5gy274lq0m5hzxuxy90vf65wchdszrazz9zxjdk30ed05kyjvwxrqdzq2pshjmt9de6zqen0wgsrswfqwp5hsetvwvsxzapqwdshgmmndp5hxtnsd3skxefwxqzjccqp2qjvlfyl4rmc56gerx70lxcrjjlnrjfz677ezw4lwzy6syqh4rnlql6t6n3pdfxkcal9jp98plgf2zqzz8jxfza9vjw3vd4t62ws8gkgqhv9x28" -> PublicKey(hex"024655b768ef40951b20053a5c4b951606d4d86085d51238f2c67c7dec29c792ca"), + "lnbc79760n1pd7cwyapp5gevl4mv968fs4le3tytzhr9r8tdk8cu3q7kfx348ut7xyntvnvmsdz92pskjepqw3hjqmrfva58gmnfdenjqumvda6zqmtpvd5xjmn9ypnx7u3qx5czq5msd9h8xcqzysxqrrssjzky68fdnhvee7aw089d5zltahfhy2ffa96pwf7fszjnm6mv0fzpv88jwaenm5qfg64pl768q8hf2vnvc5xsrpqd45nca2mewsv55wcpmhskah" -> PublicKey(hex"039f01ad62e5208940faff11d0bbc997582eafad7642aaf53de6a5f6551ab73400"), + "lnbc90n1pduns5qpp5f5h5ghga4cp7uj9de35ksk00a2ed9jf774zy7va37k5zet5cds8sdpl2pshjmt9de6zqen0wgsrjgrsd9ux2mrnypshggrnv96x7umgd9ejuurvv93k2tsxqzjccqp28ynysm3clcq865y9umys8t2f54anlsu2wfpyfxgq09ht3qfez9x9z9fpff8wzqwzua2t9vayzm4ek3vf4k4s5cdg3a6hp9vsgg9klpgpmafvnv" -> PublicKey(hex"024655b768ef40951b20053a5c4b951606d4d86085d51238f2c67c7dec29c792ca"), + "lnbc10u1pw9nehppp5tf0cpc3nx3wpk6j2n9teqwd8kuvryh69hv65w7p5u9cqhse3nmgsdzz2p6hycmgv9ek2gr0vcsrzgrxd3hhwetjyphkugzzd96xxmmfdcsywunpwejhjctjvscqp222vxxwq70temepf6n0xlzk0asr43ppqrt0mf6eclnfd5mxf6uhv5wvsqgdvht6uqxfw2vgdku5gfyhgguepvnjfu7s4kuthtnuxy0hsq6wwv9d" -> PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f"), + "lnbc30n1pw9qjwmpp5tcdc9wcr0avr5q96jlez09eax7djwmc475d5cylezsd652zvptjsdpstfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqenwdf4cqzysxqrrss7r8gn9d6klf2urzdjrq3x67a4u25wpeju5utusnc539aj5462y7kv9w56mndcx8jad7aa7qz8f8qpdw9qlx52feyemwd7afqxu45jxsqyzwns9" -> PublicKey(hex"02cda8c01b2303e91bec74c43093d5f1c4fd42a95671ae27bf853d7dfea9b78c06"), + "lnbc10u1pw9x36xpp5tlk00k0dftfx9vh40mtdlu844c9v65ad0kslnrvyuzfxqhdur46qdzz2p6hycmgv9ek2gr0vcsrzgrxd3hhwetjyphkugzzd96xxmmfdcsywunpwejhjctjvscqp2fpudmf4tt0crardf0k7vk5qs4mvys88el6e7pg62hgdt9t6ckf48l6jh4ckp87zpcnal6xnu33hxdd8k27vq2702688ww04kc065r7cqw3cqs3" -> PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f"), + "lnbc40n1pd6jttkpp5v8p97ezd3uz4ruw4w8w0gt4yr3ajtrmaeqe23ttxvpuh0cy79axqdp0tfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqcnqvscqzysxqyd9uq3r88ajpz77z6lg4wc7srhsk7m26guuvhdlpea6889m9jnc9a25sx7rdtryjukew86mtcngl6d8zqh9trtu60cmmwfx6845q08z06p6qpl3l55t" -> PublicKey(hex"03a9d79bcfab7feb0f24c3cd61a57f0f00de2225b6d31bce0bc4564efa3b1b5aaf"), + "lnbc1pwr7fqhpp5vhur3ahtumqz5mkramxr22597gaa9rnrjch8gxwr9h7r56umsjpqdpl235hqurfdcs9xct5daeks6tngask6etnyq58g6tswp5kutndv55jsaf3x5unj2gcqzysxqyz5vq88jysqvrwhq6qe38jdulefx0z9j7sfw85wqc6athfx9h77fjnjxjvprz76ayna0rcjllgu5ka960rul3qxvsrr9zth5plaerq96ursgpsshuee" -> PublicKey(hex"03c2abfa93eacec04721c019644584424aab2ba4dff3ac9bdab4e9c97007491dda"), + "lnbc10n1pw9rt5hpp5dsv5ux7xlmhmrpqnffgj6nf03mvx5zpns3578k2c5my3znnhz0gqdpstfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqenwwp3cqzysxqrrssnrasvcr5ydng283zdfpw38qtqfxjnzhdmdx9wly9dsqmsxvksrkzkqrcenu6h36g4g55q56ejk429nm4zjfgssh8uhs7gs760z63ggcqp3gyd6" -> PublicKey(hex"02cda8c01b2303e91bec74c43093d5f1c4fd42a95671ae27bf853d7dfea9b78c06"), + "lnbc1500n1pd7u7p4pp5d54vffcehkcy79gm0fkqrthh3y576jy9flzpy9rf6syua0s5p0jqdpa2fjkzep6ypxhjgz90pcx2unfv4hxxefqdanzqargv5s9xetrdahxggzvd9nkscqzysxqr23sklptztnk25aqzwty35gk9q7jtfzjywdfx23d8a37g2eaejrv3d9nnt87m98s4eps87q87pzfd6hkd077emjupe0pcazpt9kaphehufqqu7k37h" -> PublicKey(hex"03e50492eab4107a773141bb419e107bda3de3d55652e6e1a41225f06a0bbf2d56"), + "lnbc10n1pdunsmgpp5wn90mffjvkd06pe84lpa6e370024wwv7xfw0tdxlt6qq8hc7d7rqdp0tfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqcngvscqzysxqyd9uqs0cqtrum6h7dct88nkjxwxvte7hjh9pusx64tp35u0m6qhqy5dgn9j27fs37mg0w3ruf7enxlsc9xmlasgjzyyaaxqdxu9x5w0md4fspgz8twv" -> PublicKey(hex"03a9d79bcfab7feb0f24c3cd61a57f0f00de2225b6d31bce0bc4564efa3b1b5aaf"), + "lnbc700n1pwp50wapp5w7eearwr7qjhz5vk5zq4g0t75f90mrekwnw4e795qfjxyaq27dxsdqvdp6kuar9wgeqcqp20gfw78vvasjm45l6zfxmfwn59ac9dukp36mf0y3gpquhp7rptddxy7d32ptmqukeghvamlkmve9n94sxmxglun4zwtkyhk43e6lw8qspc9y9ww" -> PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f"), + "lnbc10n1pd6jvy5pp50x9lymptter9najcdpgrcnqn34wq34f49vmnllc57ezyvtlg8ayqdpdtfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yq6rvcqzysxqyd9uqcejk56vfz3y80u3npefpx82f0tghua88a8x2d33gmxcjm45q6l5xwurwyp9aj2p59cr0lknpk0eujfdax32v4px4m22u6zr5z40zxvqp5m85cr" -> PublicKey(hex"03a9d79bcfab7feb0f24c3cd61a57f0f00de2225b6d31bce0bc4564efa3b1b5aaf"), + "lnbc10n1pw9pqz7pp50782e2u9s25gqacx7mvnuhg3xxwumum89dymdq3vlsrsmaeeqsxsdpstfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqenwd3ccqzysxqrrsstxqhw2kvdfwsf7c27aaae45fheq9rzndesu4mph9dq08sawa0auz7e0z7jn9qf3zphegv2ermup0fgce0phqmf73j4zx88v3ksrgeeqq9yzzad" -> PublicKey(hex"02cda8c01b2303e91bec74c43093d5f1c4fd42a95671ae27bf853d7dfea9b78c06"), + "lnbc1300n1pwq4fx7pp5sqmq97yfxhhk7xv7u8cuc8jgv5drse45f5pmtx6f5ng2cqm332uqdq4e2279q9zux62tc5q5t9fgcqp29a662u3p2h4h4ucdav4xrlxz2rtwvvtward7htsrldpsc5erknkyxu0x2xt9qv0u766jadeetsz9pj4rljpjy0g8ayqqt2q8esewsrqpc8v4nw" -> PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f"), + "lnbc1u1pd7u7tnpp5s9he3ccpsmfdkzrsjns7p3wpz7veen6xxwxdca3khwqyh2ezk8kqdqdg9jxgg8sn7f27cqzysxqr23ssm4krdc4s0zqhfk97n0aclxsmaga208pa8c0hz3zyauqsjjxfj7kw6t29dkucp68s8s4zfdgp97kkmzgy25yuj0dcec85d9c50sgjqgq5jhl4e" -> PublicKey(hex"03e50492eab4107a773141bb419e107bda3de3d55652e6e1a41225f06a0bbf2d56"), + "lnbc1200n1pwq5kf2pp5snkm9kr0slgzfc806k4c8q93d4y57q3lz745v2hefx952rhuymrqdq509shjgrzd96xxmmfdcsscqp2w5ta9uwzhmxxp0mnhwwvnjdn6ev4huj3tha5d80ajv2p5phe8wk32yn7ch6lennx4zzawqtd34aqetataxjmrz39gzjl256walhw03gpxz79rr" -> PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f"), + "lnbc1500n1pd7u7v0pp5s6d0wqexag3aqzugaw3gs7hw7a2wrq6l8fh9s42ndqu8zu480m0sdqvg9jxgg8zn2sscqzysxqr23sm23myatjdsp3003rlasgzwg3rlr0ca8uqdt5d79lxmdwqptufr89r5rgk4np4ag0kcw7at6s6eqdany0k6m0ezjva0cyda5arpaw7lcqgzjl7u" -> PublicKey(hex"03e50492eab4107a773141bb419e107bda3de3d55652e6e1a41225f06a0bbf2d56"), + "lnbc100n1pd6jv8ypp53p6fdd954h3ffmyj6av4nzcnwfuyvn9rrsc2u6y22xnfs0l0cssqdpdtfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqerscqzysxqyd9uqyefde4la0qmglafzv8q34wqsf4mtwd8ausufavkp2e7paewd3mqsg0gsdmvrknw80t92cuvu9raevrnxtpsye0utklhpunsz68a9veqpkypx9j" -> PublicKey(hex"03a9d79bcfab7feb0f24c3cd61a57f0f00de2225b6d31bce0bc4564efa3b1b5aaf"), + "lnbc2300n1pwp50w8pp53030gw8rsqac6f3sqqa9exxwfvphsl4v4w484eynspwgv5v6vyrsdp9w35xjueqd9ejqmn0wssx67fqwpshxumhdaexgcqp2zmspcx992fvezxqkyf3rkcxc9dm2vr4ewfx42c0fccg4ea72fyd3pd6vn94tfy9t39y0hg0hupak2nv0n6pzy8culeceq8kzpwjy0tsp4fwqw5" -> PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f"), + "lnbc10n1pwykdlhpp53392ama65h3lnc4w55yqycp9v2ackexugl0ahz4jyc7fqtyuk85qdpstfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqenwvejcqzysxqrrsszkwrx54an8lhr9h4h3d7lgpjrd370zucx0fdusaklqh2xgytr8hhgq5u0kvs56l8j53uktlmz3mqhhmn88kwwxfksnham9p6ws5pwxsqnpzyda" -> PublicKey(hex"03a9d79bcfab7feb0f24c3cd61a57f0f00de2225b6d31bce0bc4564efa3b1b5aaf"), + "lnbc10470n1pw9qf40pp535pels2faqwau2rmqkgzn0rgtsu9u6qaxe5y6ttgjx5qm4pg0kgsdzy2pshjmt9de6zqen0wgsrzvp5xus8q6tcv4k8xgrpwss8xct5daeks6tn9ecxcctrv5hqxqzjccqp27sp3m204a7d47at5jkkewa7rvewdmpwaqh2ss72cajafyf7dts9ne67hw9pps2ud69p4fw95y9cdk35aef43cv35s0zzj37qu7s395cp2vw5mu" -> PublicKey(hex"024655b768ef40951b20053a5c4b951606d4d86085d51238f2c67c7dec29c792ca"), + "lnbc100n1pwytlgspp5365rx7ell807x5jsr7ykf2k7p5z77qvxjx8x6pfhh5298xnr6d2sdpstfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqenwvpscqzysxqrrssh9mphycg7e9lr58c267yerlcd9ka8lrljm8ygpnwu2v63jm7ax48y7qal25qy0ewpxw39r5whnqh93zw97gnnw64ss97n69975wh9gsqj7vudu" -> PublicKey(hex"03a9d79bcfab7feb0f24c3cd61a57f0f00de2225b6d31bce0bc4564efa3b1b5aaf"), + "lnbc210n1pdunsefpp5jxn3hlj86evlwgsz5d70hquy78k28ahdwjmlagx6qly9x29pu4uqdzq2pshjmt9de6zqen0wgsryvfqwp5hsetvwvsxzapqwdshgmmndp5hxtnsd3skxefwxqzjccqp2snr8trjcrr5xyy7g63uq7mewqyp9k3d0duznw23zhynaz6pj3uwk48yffqn8p0jugv2z03dxquc8azuwr8myjgwzh69a34fl2lnmq2sppac733" -> PublicKey(hex"024655b768ef40951b20053a5c4b951606d4d86085d51238f2c67c7dec29c792ca"), + "lnbc1700n1pwr7z98pp5j5r5q5c7syavjjz7czjvng4y95w0rd8zkl7q43sm7spg9ht2sjfqdquwf6kumnfdenjqmrfva58gmnfdenscqp2jrhlc758m734gw5td4gchcn9j5cp5p38zj3tcpvgkegxewat38d3h24kn0c2ac2pleuqp5dutvw5fmk4d2v3trcqhl5pdxqq8swnldcqtq0akh" -> PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f"), + "lnbc1500n1pdl05k5pp5nyd9netjpzn27slyj2np4slpmlz8dy69q7hygwm8ff4mey2jee5sdpa2fjkzep6ypxhjgz90pcx2unfv4hxxefqdanzqargv5s9xetrdahxggzvd9nkscqzysxqr23sqdd8t97qjc77pqa7jv7umc499jqkk0kwchapswj3xrukndr7g2nqna5x87n49uynty4pxexkt3fslyle7mwz708rs0rnnn44dnav9mgplf0aj7" -> PublicKey(hex"03e50492eab4107a773141bb419e107bda3de3d55652e6e1a41225f06a0bbf2d56"), + "lnbc1u1pwyvxrppp5nvm98wnqdee838wtfmhfjx9s49eduzu3rx0fqec2wenadth8pxqsdqdg9jxgg8sn7vgycqzysxqr23snuza3t8x0tvusu07epal9rqxh4cq22m64amuzd6x607s0w55a5xpefp2xlxmej9r6nktmwv5td3849y2sg7pckwk9r8vqqps8g4u66qq85mp3g" -> PublicKey(hex"03e50492eab4107a773141bb419e107bda3de3d55652e6e1a41225f06a0bbf2d56"), + "lnbc10n1pw9qjwppp55nx7xw3sytnfle67mh70dyukr4g4chyfmp4x4ag2hgjcts4kydnsdpstfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqenwd3ccqzysxqrrss7t24v6w7dwtd65g64qcz77clgye7n8l0j67qh32q4jrw9d2dk2444vma7j6nedgx2ywel3e9ns4r257zprsn7t5uca045xxudz9pqzsqfena6v" -> PublicKey(hex"02cda8c01b2303e91bec74c43093d5f1c4fd42a95671ae27bf853d7dfea9b78c06"), + "lnbc10u1pw9x373pp549mpcznu3q0r4ml095kjg38pvsdptzja8vhpyvc2avatc2cegycsdzz2p6hycmgv9ek2gr0vcsrzgrxd3hhwetjyphkugzzd96xxmmfdcsywunpwejhjctjvscqp2tgqwhzyjmpfymrshnaw6rwmy4rgrtjmmp66dr9v54xp52rsyzqd5htc3lu3k52t06fqk8yj05nsw0nnssak3ywev4n3xs3jgz42urmspjeqyw0" -> PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f"), + "lnbc1500n1pd7u7vupp54jm8s8lmgnnru0ndwpxhm5qwllkrarasr9fy9zkunf49ct8mw9ssdqvg9jxgg8zn2sscqzysxqr23s4njradkzzaswlsgs0a6zc3cd28xc08t5car0k7su6q3u3vjvqt6xq2kpaadgt5x9suxx50rkevfw563fupzqzpc9m6dqsjcr8qt6k2sqelr838" -> PublicKey(hex"03e50492eab4107a773141bb419e107bda3de3d55652e6e1a41225f06a0bbf2d56"), + "lnbc720n1pwypj4epp5k2saqsjznpvevsm9mzqfan3d9fz967x5lp39g3nwsxdkusps73csdzq2pshjmt9de6zqen0wgsrwv3qwp5hsetvwvsxzapqwdshgmmndp5hxtnsd3skxefwxqzjccqp2d3ltxtq0r795emmp7yqjjmmzl55cgju004vw08f83e98d28xmw44t4styhfhgsrwxydf68m2kup7j358zdrmhevqwr0hlqwt2eceaxcq7hezhx" -> PublicKey(hex"024655b768ef40951b20053a5c4b951606d4d86085d51238f2c67c7dec29c792ca"), + "lnbc10n1pwykdacpp5kegv2kdkxmetm2tpnzfgt4640n7mgxl95jnpc6fkz6uyjdwahw8sdpstfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqenwdp5cqzysxqrrssjlny2skwtnnese9nmw99xlh7jwgtdxurhce2zcwsamevmj37kd5yzxzu55mt567seewmajra2hwyry5cv9kfzf02paerhs7tf9acdcgq24pqer" -> PublicKey(hex"03a9d79bcfab7feb0f24c3cd61a57f0f00de2225b6d31bce0bc4564efa3b1b5aaf"), + "lnbc3100n1pwp370spp5ku7y6tfz5up840v00vgc2vmmqtpsu5ly98h09vxv9d7k9xtq8mrsdpjd35kw6r5de5kueevypkxjemgw3hxjmn89ssxc6t8dp6xu6twvucqp2sunrt8slx2wmvjzdv3vvlls9gez7g2gd37g2pwa4pnlswuxzy0w3hd5kkqdrpl4ylcdhvkvuamwjsfh79nkn52dq0qpzj8c4rf57jmgqschvrr" -> PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f"), + "lnbc1500n1pwr7z8rpp5hyfkmnwwx7x902ys52du8pph6hdkarnqvj6fwhh9swfsg5lp94vsdpa2fjkzep6ypph2um5dajxjctvypmkzmrvv468xgrpwfjjqetkd9kzqctwvss8ycqzysxqr23s64a2h7gn25pchh8r6jpe236h925fylw2jcm4pd92w8hkmpflreph8r6s8jnnml0zu47qv6t2sj6frnle2cpanf6e027vsddgkl8hk7gpta89d0" -> PublicKey(hex"03e50492eab4107a773141bb419e107bda3de3d55652e6e1a41225f06a0bbf2d56"), + "lnbc1500n1pdl05v0pp5c4t5p3renelctlh0z4jpznyxna7lw9zhws868wktp8vtn8t5a8uqdpa2fjkzep6ypxxjemgw35kueeqfejhgam0wf4jqnrfw96kjerfw3ujq5r0dakq6cqzysxqr23s7k3ktaae69gpl2tfleyy2rsm0m6cy5yvf8uq7g4dmpyrwvfxzslnvryx5me4xh0fsp9jfjsqkuwpzx9ydwe6ndrm0eznarhdrfwn5gsp949n7x" -> PublicKey(hex"03e50492eab4107a773141bb419e107bda3de3d55652e6e1a41225f06a0bbf2d56"), + "lnbc1500n1pwyvxp3pp5ch8jx4g0ft0f6tzg008vr82wv92sredy07v46h7q3h3athx2nm2sdpa2fjkzep6ypyx7aeqfys8w6tndqsx67fqw35x2gzvv4jxwetjypvzqam0w4kxgcqzysxqr23s3hdgx90a6jcqgl84z36dv6kn6eg4klsaje2kdm84662rq7lzzzlycvne4l8d0steq5pctdp4ffeyhylgrt7ln92l8dyvrnsn9qg5qkgqrz2cra" -> PublicKey(hex"03e50492eab4107a773141bb419e107bda3de3d55652e6e1a41225f06a0bbf2d56"), + "lnbc1500n1pwr7z2ppp5cuzt0txjkkmpz6sgefdjjmdrsj9gl8fqyeu6hx7lj050f68yuceqdqvg9jxgg8zn2sscqzysxqr23s7442lgk6cj95qygw2hly9qw9zchhag5p5m3gyzrmws8namcsqh5nz2nm6a5sc2ln6jx59sln9a7t8vxtezels2exurr0gchz9gk0ufgpwczm3r" -> PublicKey(hex"03e50492eab4107a773141bb419e107bda3de3d55652e6e1a41225f06a0bbf2d56"), + "lnbc1500n1pd7u7g4pp5eam7uhxc0w4epnuflgkl62m64qu378nnhkg3vahkm7dhdcqnzl4sdqvg9jxgg8zn2sscqzysxqr23s870l2549nhsr2dfv9ehkl5z95p5rxpks5j2etr35e02z9r6haalrfjs7sz5y7wzenywp8t52w89c9u8taf9m76t2p0w0vxw243y7l4spqdue7w" -> PublicKey(hex"03e50492eab4107a773141bb419e107bda3de3d55652e6e1a41225f06a0bbf2d56"), + "lnbc5u1pwq2jqzpp56zhpjmfm72e8p8vmfssspe07u7zmnm5hhgynafe4y4lwz6ypusvqdzsd35kw6r5de5kuemwv468wmmjddehgmmjv4ejucm0d40n2vpsta6hqan0w3jhxhmnw3hhye2fgs7nywfhcqp2tqnqpewrz28yrvvvyzjyrvwahuy595t4w4ar3cvt5cq9jx3rmxd4p7vjgmeylfkgjrssc66a9q9hhnd4aj7gqv2zj0jr2zt0gahnv0sp9y675y" -> PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f"), + "lnbc10n1pw9pqp3pp562wg5n7atx369mt75feu233cnm5h508mx7j0d807lqe0w45gndnqdpstfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqenwdejcqzysxqrrsszfg9lfawdhnp2m785cqgzg4c85mvgct44xdzjea9t0vu4mc22u4prjjz5qd4y7uhgg3wm57muh5wfz8l04kgyq8juwql3vaffm23akspzkmj53" -> PublicKey(hex"02cda8c01b2303e91bec74c43093d5f1c4fd42a95671ae27bf853d7dfea9b78c06"), + "lnbc90n1pwypjnppp5m870lhg8qjrykj6hfegawaq0ukzc099ntfezhm8jr48cw5ywgpwqdpl2pshjmt9de6zqen0wgsrjgrsd9ux2mrnypshggrnv96x7umgd9ejuurvv93k2tsxqzjccqp2s0n2u7msmypy9dh96e6exfas434td6a7f5qy5shzyk4r9dxwv0zhyxcqjkmxgnnkjvqhthadhkqvvd66f8gxkdna3jqyzhnnhfs6w3qpme2zfz" -> PublicKey(hex"024655b768ef40951b20053a5c4b951606d4d86085d51238f2c67c7dec29c792ca"), + "lnbc100n1pdunsurpp5af2vzgyjtj2q48dxl8hpfv9cskwk7q5ahefzyy3zft6jyrc4uv2qdp0tfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqcnyvccqzysxqyd9uqpcp608auvkcr22672nhwqqtul0q6dqrxryfsstttlwyvkzttxt29mxyshley6u45gf0sxc0d9dxr5fk48tj4z2z0wh6asfxhlsea57qp45tfua" -> PublicKey(hex"03a9d79bcfab7feb0f24c3cd61a57f0f00de2225b6d31bce0bc4564efa3b1b5aaf"), + "lnbc100n1pd6hzfgpp5au2d4u2f2gm9wyz34e9rls66q77cmtlw3tzu8h67gcdcvj0dsjdqdp0tfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqcnqvscqzysxqyd9uqxg5n7462ykgs8a23l3s029dun9374xza88nlf2e34nupmc042lgps7tpwd0ue0he0gdcpfmc5mshmxkgw0hfztyg4j463ux28nh2gagqage30p" -> PublicKey(hex"03a9d79bcfab7feb0f24c3cd61a57f0f00de2225b6d31bce0bc4564efa3b1b5aaf"), + "lnbc50n1pdl052epp57549dnjwf2wqfz5hg8khu0wlkca8ggv72f9q7x76p0a7azkn3ljsdp0tfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqcnvvscqzysxqyd9uqa2z48kchpmnyafgq2qlt4pruwyjh93emh8cd5wczwy47pkx6qzarmvl28hrnqf98m2rnfa0gx4lnw2jvhlg9l4265240av6t9vdqpzsqntwwyx" -> PublicKey(hex"03a9d79bcfab7feb0f24c3cd61a57f0f00de2225b6d31bce0bc4564efa3b1b5aaf"), + "lnbc100n1pd7cwrypp57m4rft00sh6za2x0jwe7cqknj568k9xajtpnspql8dd38xmd7musdp0tfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqcngvscqzysxqyd9uqsxfmfv96q0d7r3qjymwsem02t5jhtq58a30q8lu5dy3jft7wahdq2f5vc5qqymgrrdyshff26ak7m7n0vqyf7t694vam4dcqkvnr65qp6wdch9" -> PublicKey(hex"03a9d79bcfab7feb0f24c3cd61a57f0f00de2225b6d31bce0bc4564efa3b1b5aaf"), + "lnbc100n1pw9qjdgpp5lmycszp7pzce0rl29s40fhkg02v7vgrxaznr6ys5cawg437h80nsdpstfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqenwdejcqzysxqrrss47kl34flydtmu2wnszuddrd0nwa6rnu4d339jfzje6hzk6an0uax3kteee2lgx5r0629wehjeseksz0uuakzwy47lmvy2g7hja7mnpsqjmdct9" -> PublicKey(hex"02cda8c01b2303e91bec74c43093d5f1c4fd42a95671ae27bf853d7dfea9b78c06"), + "lnbc25m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5vdhkven9v5sxyetpdeessp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9q5sqqqqqqqqqqqqqqqpqsq67gye39hfg3zd8rgc80k32tvy9xk2xunwm5lzexnvpx6fd77en8qaq424dxgt56cag2dpt359k3ssyhetktkpqh24jqnjyw6uqd08sgptq44qu" -> PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad"), + "lnbc25m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5vdhkven9v5sxyetpdeessp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9q4psqqqqqqqqqqqqqqqpqsqq40wa3khl49yue3zsgm26jrepqr2eghqlx86rttutve3ugd05em86nsefzh4pfurpd9ek9w2vp95zxqnfe2u7ckudyahsa52q66tgzcp6t2dyk" -> PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad"), + "lnbc100n1pslczttpp5refxwyd5qvvnxsmswhqtqd50hdcwhk5edp02u3xpy6whf6eua3lqdq8w35hg6gsp56nrnqjjjj2g3wuhdwhy7r3sfu0wae603w9zme8wcq2f3myu3hm6qcqzrm9qrjgq7md5lu2hhkz657rs2a40xm2elaqda4krv6vy44my49x02azsqwr35puvgzjltd6dfth2awxcq49cx3srkl3zl34xhw7ppv840yf74wqq88rwr5" -> PublicKey(hex"036dc96e30210083a18762be096f13500004fc8af5bcca40f4872e18771ad58b4c"), + "lnbc100n1pslczttpp5refxwyd5qvvnxsmswhqtqd50hdcwhk5edp02u3xpy6whf6eua3lqdq8w35hg6gsp56nrnqjjjj2g3wuhdwhy7r3sfu0wae603w9zme8wcq2f3myu3hm6qcqzrm9qr3gqjdynggx20rz4nh98uknmtp2wkwk95zru8lfmw0cz9s3t0xpevuzpzz4k34cprpg9jfc3yp8zc827psug69j4w4pkn70rrfddcqf9wnqqcm2nc4" -> PublicKey(hex"036dc96e30210083a18762be096f13500004fc8af5bcca40f4872e18771ad58b4c"), + "lnbc100n1pslczttpp5refxwyd5qvvnxsmswhqtqd50hdcwhk5edp02u3xpy6whf6eua3lqdq8w35hg6gsp56nrnqjjjj2g3wuhdwhy7r3sfu0wae603w9zme8wcq2f3myu3hm6qcqzrm9q9sqsgqruuf6y6hd77533p6ufl3dapzzt55uj7t88mgty7hvfpy5lzvntpyn82j72fr3wqz985lh7l2f5pnju66nman5z09p24qvp2k8443skqqq38n4w" -> PublicKey(hex"036dc96e30210083a18762be096f13500004fc8af5bcca40f4872e18771ad58b4c"), + "lnbc100n1pslczttpp5refxwyd5qvvnxsmswhqtqd50hdcwhk5edp02u3xpy6whf6eua3lqdq8w35hg6gsp56nrnqjjjj2g3wuhdwhy7r3sfu0wae603w9zme8wcq2f3myu3hm6qcqzrm9qxpqqsgqh88td9f8p8ls8r6devh9lhvppwqe6e0lkvehyu8ztu76m9s8nu2x0rfp5z9jmn2ta97mex2ne6yecvtz8r0qej62lvkngpaduhgytncqts4cxs" -> PublicKey(hex"036dc96e30210083a18762be096f13500004fc8af5bcca40f4872e18771ad58b4c"), ) - for (req <- requests) { - assert(Bolt11Invoice.fromString(req).toString == req) + for ((req, nodeId) <- requests) { + assert(Bolt11Invoice.fromString(req).nodeId === nodeId) + assert(Bolt11Invoice.fromString(req).toString === req) } } + + test("no unknown feature in invoice"){ + assert(TestConstants.Alice.nodeParams.features.invoiceFeatures().unknown.nonEmpty) + val invoice = Bolt11Invoice(Block.LivenetGenesisBlock.hash, Some(123 msat), ByteVector32.One, priv, Left("Some invoice"), CltvExpiryDelta(18), features = TestConstants.Alice.nodeParams.features.invoiceFeatures()) + assert(invoice.features === Features[InvoiceFeature](Map[Feature with InvoiceFeature, FeatureSupport](PaymentSecret -> Mandatory, BasicMultiPartPayment -> Optional, PaymentMetadata -> Optional, VariableLengthOnion -> Mandatory))) + assert(Bolt11Invoice.fromString(invoice.toString) === invoice) + } + + test("Invoices can't have high features"){ + assertThrows[Exception](createInvoiceUnsafe(Block.LivenetGenesisBlock.hash, Some(123 msat), ByteVector32.One, priv, Left("Some invoice"), CltvExpiryDelta(18), features = Features[FeatureScope](Map[Feature with FeatureScope, FeatureSupport](VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory), Set(UnknownFeature(424242))))) + } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartHandlerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartHandlerSpec.scala index 71f7ac5f72..200fd7f084 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartHandlerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartHandlerSpec.scala @@ -32,7 +32,7 @@ import fr.acinq.eclair.payment.receive.MultiPartPaymentFSM.HtlcPart import fr.acinq.eclair.payment.receive.{MultiPartPaymentFSM, PaymentHandler} import fr.acinq.eclair.wire.protocol.PaymentOnion.FinalTlvPayload import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, Features, MilliSatoshiLong, NodeParams, ShortChannelId, TestConstants, TestKitBaseClass, TimestampMilliLong, randomBytes32, randomKey} +import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, FeatureScope, Features, MilliSatoshiLong, NodeParams, ShortChannelId, TestConstants, TestKitBaseClass, TimestampMilliLong, randomBytes32, randomKey} import org.scalatest.Outcome import org.scalatest.funsuite.FixtureAnyFunSuiteLike import scodec.bits.HexStringSyntax @@ -46,18 +46,18 @@ import scala.concurrent.duration._ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike { - val featuresWithoutMpp = Features( + val featuresWithoutMpp = Features[FeatureScope]( VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory, ) - val featuresWithMpp = Features( + val featuresWithMpp = Features[FeatureScope]( VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory, BasicMultiPartPayment -> Optional ) - val featuresWithKeySend = Features( + val featuresWithKeySend = Features[FeatureScope]( VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory, KeySend -> Optional diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentInitiatorSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentInitiatorSpec.scala index da85a58469..5dacd0a063 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentInitiatorSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentInitiatorSpec.scala @@ -37,7 +37,7 @@ import fr.acinq.eclair.router.Router._ import fr.acinq.eclair.wire.protocol.OnionPaymentPayloadTlv.{AmountToForward, KeySend, OutgoingCltv} import fr.acinq.eclair.wire.protocol.PaymentOnion.FinalTlvPayload import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{CltvExpiryDelta, Features, MilliSatoshiLong, NodeParams, TestConstants, TestKitBaseClass, TimestampSecond, randomBytes32, randomKey} +import fr.acinq.eclair.{CltvExpiryDelta, Feature, FeatureSupport, Features, InvoiceFeature, MilliSatoshiLong, NodeParams, TestConstants, TestKitBaseClass, TimestampSecond, UnknownFeature, randomBytes32, randomKey} import org.scalatest.funsuite.FixtureAnyFunSuiteLike import org.scalatest.{Outcome, Tag} import scodec.bits.{BinStringSyntax, ByteVector, HexStringSyntax} @@ -53,18 +53,18 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike case class FixtureParam(nodeParams: NodeParams, initiator: TestActorRef[PaymentInitiator], payFsm: TestProbe, multiPartPayFsm: TestProbe, sender: TestProbe, eventListener: TestProbe) - val featuresWithoutMpp: Features = Features( + val featuresWithoutMpp: Features[InvoiceFeature] = Features[InvoiceFeature]( VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory ) - val featuresWithMpp: Features = Features( + val featuresWithMpp: Features[InvoiceFeature] = Features[InvoiceFeature]( VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory, BasicMultiPartPayment -> Optional, ) - val featuresWithTrampoline: Features = Features( + val featuresWithTrampoline: Features[InvoiceFeature] = Features[InvoiceFeature]( VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory, BasicMultiPartPayment -> Optional, @@ -86,7 +86,7 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike override def withFixture(test: OneArgTest): Outcome = { val features = if (test.tags.contains("mpp_disabled")) featuresWithoutMpp else featuresWithMpp - val nodeParams = TestConstants.Alice.nodeParams.copy(features = features) + val nodeParams = TestConstants.Alice.nodeParams.copy(features = features.unscoped()) val (sender, payFsm, multiPartPayFsm) = (TestProbe(), TestProbe(), TestProbe()) val eventListener = TestProbe() system.eventStream.subscribe(eventListener.ref, classOf[PaymentEvent]) @@ -128,7 +128,7 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike Bolt11Invoice.Description("Some invoice"), Bolt11Invoice.PaymentSecret(randomBytes32()), Bolt11Invoice.Expiry(3600), - Bolt11Invoice.InvoiceFeatures(Features(bin"000001000000000000000000000000000100000100000000")) // feature 42 + Bolt11Invoice.InvoiceFeatures(Features[InvoiceFeature](Map[Feature with InvoiceFeature, FeatureSupport](VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory), Set(UnknownFeature(42))).unscoped()) ) val invoice = Bolt11Invoice("lnbc", Some(finalAmount), TimestampSecond.now(), randomKey().publicKey, taggedFields, ByteVector.empty) val req = SendPaymentToNode(finalAmount + 100.msat, invoice, 1, CltvExpiryDelta(42), routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentPacketSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentPacketSpec.scala index 9dd45c6aa7..8aabb5a086 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentPacketSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentPacketSpec.scala @@ -31,7 +31,7 @@ import fr.acinq.eclair.transactions.Transactions.InputInfo import fr.acinq.eclair.wire.protocol.OnionPaymentPayloadTlv.{AmountToForward, OutgoingCltv, PaymentData} import fr.acinq.eclair.wire.protocol.PaymentOnion.{ChannelRelayTlvPayload, FinalTlvPayload} import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, Features, MilliSatoshi, MilliSatoshiLong, ShortChannelId, TestConstants, TimestampSecondLong, nodeFee, randomBytes32, randomKey} +import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, Features, InvoiceFeature, MilliSatoshi, MilliSatoshiLong, ShortChannelId, TestConstants, TimestampSecondLong, nodeFee, randomBytes32, randomKey} import org.scalatest.BeforeAndAfterAll import org.scalatest.funsuite.AnyFunSuite import scodec.Attempt @@ -212,7 +212,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { // a -> b -> c d -> e val routingHints = List(List(Bolt11Invoice.ExtraHop(randomKey().publicKey, ShortChannelId(42), 10 msat, 100, CltvExpiryDelta(144)))) - val invoiceFeatures = Features(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory, BasicMultiPartPayment -> Optional) + val invoiceFeatures = Features[InvoiceFeature](VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory, BasicMultiPartPayment -> Optional) val invoice = Bolt11Invoice(Block.RegtestGenesisBlock.hash, Some(finalAmount), paymentHash, priv_a.privateKey, Left("#reckless"), CltvExpiryDelta(18), None, None, routingHints, features = invoiceFeatures, paymentMetadata = Some(hex"010203")) val Success((amount_ac, expiry_ac, trampolineOnion)) = buildTrampolineToLegacyPacket(invoice, trampolineHops, PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, invoice.paymentSecret.get, None)) assert(amount_ac === amount_bc) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/NodeRelayerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/NodeRelayerSpec.scala index dc2ef84c07..a71dc8266d 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/NodeRelayerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/NodeRelayerSpec.scala @@ -38,7 +38,7 @@ import fr.acinq.eclair.payment.send.PaymentLifecycle.SendPaymentToNode import fr.acinq.eclair.router.Router.RouteRequest import fr.acinq.eclair.router.{BalanceTooLow, RouteNotFound} import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, Features, MilliSatoshi, MilliSatoshiLong, NodeParams, ShortChannelId, TestConstants, UInt64, randomBytes, randomBytes32, randomKey} +import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, Features, InvoiceFeature, MilliSatoshi, MilliSatoshiLong, NodeParams, ShortChannelId, TestConstants, UInt64, randomBytes, randomBytes32, randomKey} import org.scalatest.Outcome import org.scalatest.funsuite.FixtureAnyFunSuiteLike import scodec.bits.HexStringSyntax @@ -567,7 +567,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl // Receive an upstream multi-part payment. val hints = List(List(ExtraHop(outgoingNodeId, ShortChannelId(42), feeBase = 10 msat, feeProportionalMillionths = 1, cltvExpiryDelta = CltvExpiryDelta(12)))) - val features = Features(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory, BasicMultiPartPayment -> Optional) + val features = Features[InvoiceFeature](VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory, BasicMultiPartPayment -> Optional) val invoice = Bolt11Invoice(Block.LivenetGenesisBlock.hash, Some(outgoingAmount * 3), paymentHash, randomKey(), Left("Some invoice"), CltvExpiryDelta(18), extraHops = hints, paymentMetadata = Some(hex"123456"), features = features) val incomingPayments = incomingMultiPart.map(incoming => incoming.copy(innerPayload = PaymentOnion.createNodeRelayToNonTrampolinePayload( incoming.innerPayload.amountToForward, outgoingAmount * 3, outgoingExpiry, outgoingNodeId, invoice diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/AnnouncementsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/AnnouncementsSpec.scala index fc0be00213..1878186dce 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/AnnouncementsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/AnnouncementsSpec.scala @@ -22,7 +22,8 @@ import fr.acinq.eclair.TestConstants.Alice import fr.acinq.eclair._ import fr.acinq.eclair.router.Announcements._ import fr.acinq.eclair.wire.protocol.ChannelUpdate.ChannelFlags -import fr.acinq.eclair.wire.protocol.NodeAddress +import fr.acinq.eclair.wire.protocol.LightningMessageCodecs.nodeAnnouncementCodec +import fr.acinq.eclair.wire.protocol.{Color, NodeAddress} import org.scalatest.funsuite.AnyFunSuite import scodec.bits._ @@ -53,7 +54,7 @@ class AnnouncementsSpec extends AnyFunSuite { } test("create valid signed node announcement") { - val features = Features( + val features = Features[FeatureScope]( Features.DataLossProtect -> FeatureSupport.Optional, Features.InitialRoutingSync -> FeatureSupport.Optional, Features.ChannelRangeQueries -> FeatureSupport.Optional, @@ -63,7 +64,7 @@ class AnnouncementsSpec extends AnyFunSuite { Features.BasicMultiPartPayment -> FeatureSupport.Optional, Features.PaymentMetadata -> FeatureSupport.Optional, ) - val ann = makeNodeAnnouncement(Alice.nodeParams.privateKey, Alice.nodeParams.alias, Alice.nodeParams.color, Alice.nodeParams.publicAddresses, features) + val ann = makeNodeAnnouncement(Alice.nodeParams.privateKey, Alice.nodeParams.alias, Alice.nodeParams.color, Alice.nodeParams.publicAddresses, features.nodeAnnouncementFeatures()) // Features should be filtered to only include node_announcement related features. assert(ann.features === Features( Features.DataLossProtect -> FeatureSupport.Optional, @@ -84,7 +85,7 @@ class AnnouncementsSpec extends AnyFunSuite { NodeAddress.fromParts("140.82.121.4", 9735).get, NodeAddress.fromParts("hsmithsxurybd7uh.onion", 9735).get, ) - val ann = makeNodeAnnouncement(Alice.nodeParams.privateKey, Alice.nodeParams.alias, Alice.nodeParams.color, addresses, Alice.nodeParams.features) + val ann = makeNodeAnnouncement(Alice.nodeParams.privateKey, Alice.nodeParams.alias, Alice.nodeParams.color, addresses, Alice.nodeParams.features.nodeAnnouncementFeatures()) assert(checkSig(ann)) assert(ann.addresses === List( NodeAddress.fromParts("140.82.121.4", 9735).get, @@ -128,4 +129,12 @@ class AnnouncementsSpec extends AnyFunSuite { assert(!channelUpdate2_disabled.channelFlags.isEnabled) } + test("announce irrelevant features") { + // This announcement has option_payment_metadata which is not a node feature + val encoded = hex"25d8bf19e2c6562a85b3109122118a50728d42c9054537b5a26c1e982407f0923a6f162cf0939bfaf58d7e7a7a7c86d7dfd02dda9b0d2036e8fd35a7afc78daa00070100000000000061fcfc1d039093816bb908c043341f3ee47ea86179627d26aa00704593503e684a0b9a38cb01020374657374206e6f646500000000000000000000000000000000000000000000000007018c5279042607" + val ann = nodeAnnouncementCodec.decode(encoded.bits).require.value + assert(ann.features.hasFeature(Features.PaymentMetadata)) + assert(checkSig(ann)) + assert(nodeAnnouncementCodec.encode(ann).require.bytes === encoded) + } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/BaseRouterSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/BaseRouterSpec.scala index bebcd2e205..ba738d4954 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/BaseRouterSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/BaseRouterSpec.scala @@ -66,7 +66,7 @@ abstract class BaseRouterSpec extends TestKitBaseClass with FixtureAnyFunSuiteLi // in the tests we are 'a', we don't define a node_a, it will be generated automatically when the router validates the first channel val node_b = makeNodeAnnouncement(priv_b, "node-B", Color(50, 99, -80), Nil, Features.empty) - val node_c = makeNodeAnnouncement(priv_c, "node-C", Color(123, 100, -40), Nil, TestConstants.Bob.nodeParams.features) + val node_c = makeNodeAnnouncement(priv_c, "node-C", Color(123, 100, -40), Nil, TestConstants.Bob.nodeParams.features.nodeAnnouncementFeatures()) val node_d = makeNodeAnnouncement(priv_d, "node-D", Color(-120, -20, 60), Nil, Features.empty) val node_e = makeNodeAnnouncement(priv_e, "node-E", Color(-50, 0, 10), Nil, Features.empty) val node_f = makeNodeAnnouncement(priv_f, "node-F", Color(30, 10, -50), Nil, Features.empty) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/RouterSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/RouterSpec.scala index 2f585b9386..4c69ac9673 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/RouterSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/RouterSpec.scala @@ -55,7 +55,7 @@ class RouterSpec extends BaseRouterSpec { // valid channel announcement, no stashing val chan_ac = channelAnnouncement(ShortChannelId(BlockHeight(420000), 5, 0), priv_a, priv_c, priv_funding_a, priv_funding_c) val update_ac = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_a, c, chan_ac.shortChannelId, CltvExpiryDelta(7), 0 msat, 766000 msat, 10, htlcMaximum) - val node_c = makeNodeAnnouncement(priv_c, "node-C", Color(123, 100, -40), Nil, TestConstants.Bob.nodeParams.features, timestamp = TimestampSecond.now() + 1) + val node_c = makeNodeAnnouncement(priv_c, "node-C", Color(123, 100, -40), Nil, TestConstants.Bob.nodeParams.features.nodeAnnouncementFeatures(), timestamp = TimestampSecond.now() + 1) peerConnection.send(router, PeerRoutingMessage(peerConnection.ref, remoteNodeId, chan_ac)) peerConnection.expectNoMessage(100 millis) // we don't immediately acknowledge the announcement (back pressure) assert(watcher.expectMsgType[ValidateRequest].ann === chan_ac) @@ -173,7 +173,7 @@ class RouterSpec extends BaseRouterSpec { // unknown channel val priv_y = randomKey() val update_ay = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_a, priv_y.publicKey, ShortChannelId(4646464), CltvExpiryDelta(7), 0 msat, 766000 msat, 10, htlcMaximum) - val node_y = makeNodeAnnouncement(priv_y, "node-Y", Color(123, 100, -40), Nil, TestConstants.Bob.nodeParams.features) + val node_y = makeNodeAnnouncement(priv_y, "node-Y", Color(123, 100, -40), Nil, TestConstants.Bob.nodeParams.features.nodeAnnouncementFeatures()) peerConnection.send(router, PeerRoutingMessage(peerConnection.ref, remoteNodeId, update_ay)) peerConnection.expectMsg(TransportHandler.ReadAck(update_ay)) peerConnection.expectMsg(GossipDecision.NoRelatedChannel(update_ay)) @@ -191,7 +191,7 @@ class RouterSpec extends BaseRouterSpec { val priv_funding_y = randomKey() // a-y will have an invalid script val chan_ay = channelAnnouncement(ShortChannelId(42002), priv_a, priv_y, priv_funding_a, priv_funding_y) val update_ay = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_a, priv_y.publicKey, chan_ay.shortChannelId, CltvExpiryDelta(7), 0 msat, 766000 msat, 10, htlcMaximum) - val node_y = makeNodeAnnouncement(priv_y, "node-Y", Color(123, 100, -40), Nil, TestConstants.Bob.nodeParams.features) + val node_y = makeNodeAnnouncement(priv_y, "node-Y", Color(123, 100, -40), Nil, TestConstants.Bob.nodeParams.features.nodeAnnouncementFeatures()) peerConnection.send(router, PeerRoutingMessage(peerConnection.ref, remoteNodeId, chan_ay)) assert(watcher.expectMsgType[ValidateRequest].ann === chan_ay) peerConnection.send(router, PeerRoutingMessage(peerConnection.ref, remoteNodeId, update_ay)) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncSpec.scala index 7d7d800e5e..5000e221e8 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncSpec.scala @@ -352,7 +352,7 @@ object RoutingSyncSpec { val channelAnn_12 = channelAnnouncement(shortChannelId, priv1, priv2, priv_funding1, priv_funding2) val channelUpdate_12 = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv1, priv2.publicKey, shortChannelId, cltvExpiryDelta = CltvExpiryDelta(7), 0 msat, feeBaseMsat = 766000 msat, feeProportionalMillionths = 10, 500000000L msat, timestamp = timestamp) val channelUpdate_21 = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv2, priv1.publicKey, shortChannelId, cltvExpiryDelta = CltvExpiryDelta(7), 0 msat, feeBaseMsat = 766000 msat, feeProportionalMillionths = 10, 500000000L msat, timestamp = timestamp) - val nodeAnnouncement_1 = makeNodeAnnouncement(priv1, "a", Color(0, 0, 0), List(), TestConstants.Bob.nodeParams.features) + val nodeAnnouncement_1 = makeNodeAnnouncement(priv1, "a", Color(0, 0, 0), List(), TestConstants.Bob.nodeParams.features.nodeAnnouncementFeatures()) val nodeAnnouncement_2 = makeNodeAnnouncement(priv2, "b", Color(0, 0, 0), List(), Features.empty) val publicChannel = PublicChannel(channelAnn_12, ByteVector32.Zeroes, Satoshi(0), Some(channelUpdate_12), Some(channelUpdate_21), None) (publicChannel, nodeAnnouncement_1, nodeAnnouncement_2) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version1/ChannelCodecs1Spec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version1/ChannelCodecs1Spec.scala index bcd1c32adc..f6f5497b16 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version1/ChannelCodecs1Spec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version1/ChannelCodecs1Spec.scala @@ -70,7 +70,7 @@ class ChannelCodecs1Spec extends AnyFunSuite { defaultFinalScriptPubKey = Script.write(Script.pay2wpkh(PrivateKey(randomBytes32()).publicKey)), walletStaticPaymentBasepoint = None, isFunder = Random.nextBoolean(), - initFeatures = Features(randomBytes(256))) + initFeatures = Features(randomBytes(256)).initFeatures()) val o1 = o.copy(walletStaticPaymentBasepoint = Some(PrivateKey(randomBytes32()).publicKey)) roundtrip(o, localParamsCodec(ChannelVersion.ZEROES)) @@ -92,7 +92,7 @@ class ChannelCodecs1Spec extends AnyFunSuite { paymentBasepoint = randomKey().publicKey, delayedPaymentBasepoint = randomKey().publicKey, htlcBasepoint = randomKey().publicKey, - initFeatures = TestConstants.Alice.nodeParams.features, + initFeatures = TestConstants.Alice.nodeParams.features.initFeatures(), shutdownScript = None) val encoded = remoteParamsCodec.encode(o).require val decoded = remoteParamsCodec.decodeValue(encoded).require diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecsSpec.scala index fa2653d400..4b993c9b4d 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecsSpec.scala @@ -18,6 +18,8 @@ package fr.acinq.eclair.wire.protocol import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} import fr.acinq.bitcoin.{Block, ByteVector32, ByteVector64, SatoshiLong} +import fr.acinq.eclair.FeatureSupport.Optional +import fr.acinq.eclair.Features.DataLossProtect import fr.acinq.eclair._ import fr.acinq.eclair.blockchain.fee.FeeratePerKw import fr.acinq.eclair.channel.{ChannelFlags, ChannelTypes} @@ -269,7 +271,7 @@ class LightningMessageCodecsSpec extends AnyFunSuite { val commit_sig = CommitSig(randomBytes32(), randomBytes64(), randomBytes64() :: randomBytes64() :: randomBytes64() :: Nil) val revoke_and_ack = RevokeAndAck(randomBytes32(), scalar(0), point(1)) val channel_announcement = ChannelAnnouncement(randomBytes64(), randomBytes64(), randomBytes64(), randomBytes64(), Features(bin(7, 9)), Block.RegtestGenesisBlock.hash, ShortChannelId(1), randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey) - val node_announcement = NodeAnnouncement(randomBytes64(), Features(bin(1, 2)), 1 unixsec, randomKey().publicKey, Color(100.toByte, 200.toByte, 300.toByte), "node-alias", IPv4(InetAddress.getByAddress(Array[Byte](192.toByte, 168.toByte, 1.toByte, 42.toByte)).asInstanceOf[Inet4Address], 42000) :: Nil) + val node_announcement = NodeAnnouncement(randomBytes64(), Features[FeatureScope](DataLossProtect -> Optional), 1 unixsec, randomKey().publicKey, Color(100.toByte, 200.toByte, 300.toByte), "node-alias", IPv4(InetAddress.getByAddress(Array[Byte](192.toByte, 168.toByte, 1.toByte, 42.toByte)).asInstanceOf[Inet4Address], 42000) :: Nil) val channel_update = ChannelUpdate(randomBytes64(), Block.RegtestGenesisBlock.hash, ShortChannelId(1), 2 unixsec, ChannelUpdate.ChannelFlags.DUMMY, CltvExpiryDelta(3), 4 msat, 5 msat, 6, None) val announcement_signatures = AnnouncementSignatures(randomBytes32(), ShortChannelId(42), randomBytes64(), randomBytes64()) val gossip_timestamp_filter = GossipTimestampFilter(Block.RegtestGenesisBlock.blockId, 100000 unixsec, 1500) diff --git a/eclair-front/src/test/scala/fr/acinq/eclair/router/FrontRouterSpec.scala b/eclair-front/src/test/scala/fr/acinq/eclair/router/FrontRouterSpec.scala index 51b2c6a7c5..51eaaa3d9d 100644 --- a/eclair-front/src/test/scala/fr/acinq/eclair/router/FrontRouterSpec.scala +++ b/eclair-front/src/test/scala/fr/acinq/eclair/router/FrontRouterSpec.scala @@ -320,12 +320,12 @@ object FrontRouterSpec { val (priv_funding_a, priv_funding_b, priv_funding_c, priv_funding_d, priv_funding_e, priv_funding_f) = (randomKey(), randomKey(), randomKey(), randomKey(), randomKey(), randomKey()) val (funding_a, funding_b, funding_c, funding_d, funding_e, funding_f) = (priv_funding_a.publicKey, priv_funding_b.publicKey, priv_funding_c.publicKey, priv_funding_d.publicKey, priv_funding_e.publicKey, priv_funding_f.publicKey) - val ann_a = makeNodeAnnouncement(priv_a, "node-A", Color(15, 10, -70), Nil, Features(hex"0200")) - val ann_b = makeNodeAnnouncement(priv_b, "node-B", Color(50, 99, -80), Nil, Features(hex"")) - val ann_c = makeNodeAnnouncement(priv_c, "node-C", Color(123, 100, -40), Nil, Features(hex"0200")) - val ann_d = makeNodeAnnouncement(priv_d, "node-D", Color(-120, -20, 60), Nil, Features(hex"00")) - val ann_e = makeNodeAnnouncement(priv_e, "node-E", Color(-50, 0, 10), Nil, Features(hex"00")) - val ann_f = makeNodeAnnouncement(priv_f, "node-F", Color(30, 10, -50), Nil, Features(hex"00")) + val ann_a = makeNodeAnnouncement(priv_a, "node-A", Color(15, 10, -70), Nil, Features(Features.VariableLengthOnion -> FeatureSupport.Optional)) + val ann_b = makeNodeAnnouncement(priv_b, "node-B", Color(50, 99, -80), Nil, Features.empty) + val ann_c = makeNodeAnnouncement(priv_c, "node-C", Color(123, 100, -40), Nil, Features(Features.VariableLengthOnion -> FeatureSupport.Optional)) + val ann_d = makeNodeAnnouncement(priv_d, "node-D", Color(-120, -20, 60), Nil, Features.empty) + val ann_e = makeNodeAnnouncement(priv_e, "node-E", Color(-50, 0, 10), Nil, Features.empty) + val ann_f = makeNodeAnnouncement(priv_f, "node-F", Color(30, 10, -50), Nil, Features.empty) val channelId_ab = ShortChannelId(BlockHeight(420000), 1, 0) val channelId_bc = ShortChannelId(BlockHeight(420000), 2, 0) From daddee1912e9fc627f0cabe841ca7504e00fef21 Mon Sep 17 00:00:00 2001 From: Richard Myers Date: Mon, 7 Feb 2022 14:21:08 +0100 Subject: [PATCH 017/121] fixup! Convert wiki pages in to files in the docs directory and general docs file cleanups (#2165) (#2167) - Fixup for #2165, Usage.md should link to ./API.md and not the deprecated wiki --- docs/Usage.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Usage.md b/docs/Usage.md index 235a44e8d3..ab73f483ae 100644 --- a/docs/Usage.md +++ b/docs/Usage.md @@ -15,7 +15,7 @@ Then you'll have to install jq: - Download the eclair-cli file from [our sources](https://github.com/ACINQ/eclair/blob/master/eclair-core/eclair-cli) - (optional) Move the file to `~/bin` -- Enable the [JSON API](https://github.com/ACINQ/eclair/wiki/API) in your `eclair.conf` settings. +- Enable the [JSON API](./API.md) in your `eclair.conf` settings. Run this command to list the available calls: From ca71a3c1526bec18fb81cb9f873de257d0559fc9 Mon Sep 17 00:00:00 2001 From: Pierre-Marie Padiou Date: Tue, 8 Feb 2022 11:37:24 +0100 Subject: [PATCH 018/121] Update a few dependencies (#2166) This updates versions of a few major dependencies: scala, akka, postgres, etc. --- eclair-core/pom.xml | 12 +- .../main/scala/fr/acinq/eclair/Setup.scala | 2 +- .../rpc/BasicBitcoinJsonRPCClient.scala | 19 +-- .../bitcoind/rpc/BitcoinJsonRPCClient.scala | 3 +- .../watchdogs/BlockchainWatchdog.scala | 17 +-- .../blockchain/watchdogs/ExplorerApi.scala | 121 +++++++++--------- .../eclair/crypto/TransportHandler.scala | 10 +- .../acinq/eclair/json/JsonSerializers.scala | 13 +- .../remote/EclairInternalsSerializer.scala | 2 +- .../acinq/eclair/TestBitcoinCoreClient.scala | 2 +- .../blockchain/bitcoind/BitcoindService.scala | 2 +- .../watchdogs/ExplorerApiSpec.scala | 2 +- .../AnnouncementsBatchValidationSpec.scala | 2 +- eclair-node/pom.xml | 2 +- pom.xml | 10 +- 15 files changed, 106 insertions(+), 113 deletions(-) diff --git a/eclair-core/pom.xml b/eclair-core/pom.xml index df61da28ec..d5df1bf005 100644 --- a/eclair-core/pom.xml +++ b/eclair-core/pom.xml @@ -160,7 +160,7 @@ - com.softwaremill.sttp + com.softwaremill.sttp.client3 okhttp-backend_${scala.version.short} ${sttp.version} @@ -168,10 +168,10 @@ org.json4s json4s-jackson_${scala.version.short} - 3.6.11 + 4.0.3 - com.softwaremill.sttp + com.softwaremill.sttp.client3 json4s_${scala.version.short} ${sttp.version} @@ -202,13 +202,13 @@ org.scodec scodec-core_${scala.version.short} - 1.11.8 + 1.11.9 org.scodec scodec-bits_${scala.version.short} - 1.1.25 + 1.1.30 commons-codec @@ -234,8 +234,8 @@ org.postgresql - 42.2.23 postgresql + 42.3.2 com.zaxxer diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala index 55aea8b6fe..8620882551 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala @@ -22,7 +22,6 @@ import akka.actor.typed.scaladsl.adapter.{ClassicActorRefOps, ClassicActorSystem import akka.actor.{ActorRef, ActorSystem, Props, SupervisorStrategy, typed} import akka.pattern.after import akka.util.Timeout -import com.softwaremill.sttp.okhttp.OkHttpFutureBackend import fr.acinq.bitcoin.{Block, ByteVector32, Satoshi} import fr.acinq.eclair.Setup.Seeds import fr.acinq.eclair.balance.{BalanceActor, ChannelsListener} @@ -49,6 +48,7 @@ import fr.acinq.eclair.wire.protocol.NodeAddress import grizzled.slf4j.Logging import org.json4s.JsonAST.JArray import scodec.bits.ByteVector +import sttp.client3.okhttp.OkHttpFutureBackend import java.io.File import java.net.InetSocketAddress diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/rpc/BasicBitcoinJsonRPCClient.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/rpc/BasicBitcoinJsonRPCClient.scala index 8137845375..2c7b30dce8 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/rpc/BasicBitcoinJsonRPCClient.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/rpc/BasicBitcoinJsonRPCClient.scala @@ -16,14 +16,15 @@ package fr.acinq.eclair.blockchain.bitcoind.rpc -import com.softwaremill.sttp._ -import com.softwaremill.sttp.json4s._ import fr.acinq.bitcoin.ByteVector32 import fr.acinq.eclair.KamonExt import fr.acinq.eclair.blockchain.Monitoring.{Metrics, Tags} import org.json4s.JsonAST.{JString, JValue} import org.json4s.jackson.Serialization import org.json4s.{CustomSerializer, DefaultFormats} +import sttp.client3._ +import sttp.client3.json4s._ +import sttp.model.StatusCode import java.nio.charset.StandardCharsets import java.nio.file.{Files, Path} @@ -31,7 +32,7 @@ import java.util.concurrent.atomic.AtomicReference import scala.concurrent.{ExecutionContext, Future} import scala.util.{Failure, Success, Try} -class BasicBitcoinJsonRPCClient(rpcAuthMethod: BitcoinJsonRPCAuthMethod, host: String = "127.0.0.1", port: Int = 8332, ssl: Boolean = false, wallet: Option[String] = None)(implicit http: SttpBackend[Future, Nothing]) extends BitcoinJsonRPCClient { +class BasicBitcoinJsonRPCClient(rpcAuthMethod: BitcoinJsonRPCAuthMethod, host: String = "127.0.0.1", port: Int = 8332, ssl: Boolean = false, wallet: Option[String] = None)(implicit sb: SttpBackend[Future, _]) extends BitcoinJsonRPCClient { // necessary to properly serialize ByteVector32 into String readable by bitcoind object ByteVector32Serializer extends CustomSerializer[ByteVector32](_ => ( { @@ -57,18 +58,18 @@ class BasicBitcoinJsonRPCClient(rpcAuthMethod: BitcoinJsonRPCAuthMethod, host: S case o => o } - private def send(requests: Seq[JsonRPCRequest], user: String, password: String)(implicit ec: ExecutionContext): Future[Response[Seq[JsonRPCResponse]]] = { + private def send(requests: Seq[JsonRPCRequest], user: String, password: String)(implicit ec: ExecutionContext): Future[Response[Either[ResponseException[String, Exception], Seq[JsonRPCResponse]]]] = { requests.groupBy(_.method).foreach { case (method, calls) => Metrics.RpcBasicInvokeCount.withTag(Tags.Method, method).increment(calls.size) } KamonExt.timeFuture(Metrics.RpcBasicInvokeDuration.withoutTags()) { for { - response <- sttp + response <- basicRequest .post(serviceUri) .body(requests) .auth.basic(user, password) .response(asJson[Seq[JsonRPCResponse]]) - .send() + .send(sb) } yield response } } @@ -78,18 +79,18 @@ class BasicBitcoinJsonRPCClient(rpcAuthMethod: BitcoinJsonRPCAuthMethod, host: S send(requests, user, password).flatMap { response => response.code match { - case StatusCodes.Unauthorized => rpcAuthMethod match { + case StatusCode.Unauthorized => rpcAuthMethod match { case _: BitcoinJsonRPCAuthMethod.UserPassword => Future.failed(new IllegalArgumentException("could not authenticate to bitcoind RPC server: check your configured user/password")) case BitcoinJsonRPCAuthMethod.SafeCookie(path, _) => // bitcoind may have restarted and generated a new cookie file, let's read it again and retry BitcoinJsonRPCAuthMethod.readCookie(path) match { case Success(cookie) => credentials.set(cookie.credentials) - send(requests, cookie.credentials.user, cookie.credentials.password).map(_.unsafeBody) + send(requests, cookie.credentials.user, cookie.credentials.password).map(_.body.fold(exc => throw exc, jvalue => jvalue)) case Failure(e) => Future.failed(e) } } - case _ => Future.successful(response.unsafeBody) + case _ => Future.successful(response.body.fold(exc => throw exc, jvalue => jvalue)) } } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/rpc/BitcoinJsonRPCClient.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/rpc/BitcoinJsonRPCClient.scala index 9fbc96b254..5373cc7947 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/rpc/BitcoinJsonRPCClient.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/rpc/BitcoinJsonRPCClient.scala @@ -16,10 +16,9 @@ package fr.acinq.eclair.blockchain.bitcoind.rpc -import java.io.IOException - import org.json4s.JsonAST.JValue +import java.io.IOException import scala.concurrent.{ExecutionContext, Future} trait BitcoinJsonRPCClient { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/watchdogs/BlockchainWatchdog.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/watchdogs/BlockchainWatchdog.scala index 848ff033c3..3309037414 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/watchdogs/BlockchainWatchdog.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/watchdogs/BlockchainWatchdog.scala @@ -19,13 +19,12 @@ package fr.acinq.eclair.blockchain.watchdogs import akka.actor.typed.Behavior import akka.actor.typed.eventstream.EventStream import akka.actor.typed.scaladsl.Behaviors -import com.softwaremill.sttp.SttpBackend import fr.acinq.bitcoin.BlockHeader import fr.acinq.eclair.NotificationsLogger.NotifyNodeOperator import fr.acinq.eclair.blockchain.CurrentBlockHeight import fr.acinq.eclair.blockchain.watchdogs.Monitoring.{Metrics, Tags} -import fr.acinq.eclair.tor.Socks5ProxyParams import fr.acinq.eclair.{BlockHeight, NodeParams, NotificationsLogger} +import sttp.client3.SttpBackend import java.util.UUID import scala.concurrent.Future @@ -43,11 +42,6 @@ object BlockchainWatchdog { case class BlockHeaderAt(blockHeight: BlockHeight, blockHeader: BlockHeader) case object NoBlockReceivedTimer - trait SupportsTor { - /** Tor proxy connection parameters */ - def socksProxy_opt: Option[Socks5ProxyParams] - } - sealed trait BlockchainWatchdogEvent /** * We are missing too many blocks compared to one of our blockchain watchdogs. @@ -72,12 +66,13 @@ object BlockchainWatchdog { def apply(nodeParams: NodeParams, maxRandomDelay: FiniteDuration, blockTimeout: FiniteDuration = 15 minutes): Behavior[Command] = { Behaviors.setup { context => val socksProxy_opt = nodeParams.socksProxy_opt.flatMap(params => if (params.useForWatchdogs) Some(params) else None) - implicit val sttpBackend: SttpBackend[Future, Nothing] = ExplorerApi.createSttpBackend(socksProxy_opt) + implicit val sb: SttpBackend[Future, _] = ExplorerApi.createSttpBackend(socksProxy_opt) val explorers = Seq( - ExplorerApi.BlockstreamExplorer(socksProxy_opt), - ExplorerApi.BlockcypherExplorer(socksProxy_opt), - ExplorerApi.MempoolSpaceExplorer(socksProxy_opt) + ExplorerApi.BlockcypherExplorer(), + // NB: if there is a proxy, we assume it is a tor proxy + ExplorerApi.BlockstreamExplorer(useTorEndpoints = socksProxy_opt.isDefined), + ExplorerApi.MempoolSpaceExplorer(useTorEndpoints = socksProxy_opt.isDefined) ).filter { e => val enabled = nodeParams.blockchainWatchdogSources.contains(e.name) if (!enabled) { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/watchdogs/ExplorerApi.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/watchdogs/ExplorerApi.scala index 6a6c002d5e..bc3b7a7fcc 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/watchdogs/ExplorerApi.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/watchdogs/ExplorerApi.scala @@ -20,17 +20,18 @@ import akka.actor.ActorSystem import akka.actor.typed.scaladsl.{ActorContext, Behaviors} import akka.actor.typed.{ActorRef, Behavior} import akka.pattern.after -import com.softwaremill.sttp.json4s.asJson -import com.softwaremill.sttp.okhttp.OkHttpFutureBackend -import com.softwaremill.sttp.{StatusCodes, SttpBackend, SttpBackendOptions, Uri, UriContext, sttp} import fr.acinq.bitcoin.{Block, BlockHeader, ByteVector32} -import fr.acinq.eclair.blockchain.watchdogs.BlockchainWatchdog.{BlockHeaderAt, LatestHeaders, SupportsTor} +import fr.acinq.eclair.blockchain.watchdogs.BlockchainWatchdog.{BlockHeaderAt, LatestHeaders} import fr.acinq.eclair.blockchain.watchdogs.Monitoring.{Metrics, Tags} import fr.acinq.eclair.tor.Socks5ProxyParams import fr.acinq.eclair.{BlockHeight, randomBytes} import org.json4s.JsonAST.{JArray, JInt, JObject, JString} import org.json4s.jackson.Serialization import org.json4s.{DefaultFormats, Serialization} +import sttp.client3._ +import sttp.client3.json4s._ +import sttp.client3.okhttp.OkHttpFutureBackend +import sttp.model.{StatusCode, Uri} import java.time.OffsetDateTime import scala.concurrent.duration.DurationInt @@ -92,9 +93,9 @@ object ExplorerApi { } } - def createSttpBackend(socksProxy_opt: Option[Socks5ProxyParams]): SttpBackend[Future, Nothing] = { + def createSttpBackend(socksProxy_opt: Option[Socks5ProxyParams]): SttpBackend[Future, _] = { val options = SttpBackendOptions(connectionTimeout = 30.seconds, proxy = None) - val sttpBackendOptions = socksProxy_opt match { + val sttpBackendOptions: SttpBackendOptions = socksProxy_opt match { case Some(proxy) => val host = proxy.address.getHostString val port = proxy.address.getPort @@ -111,7 +112,7 @@ object ExplorerApi { * Query https://blockcypher.com/ to fetch block headers. * See https://www.blockcypher.com/dev/bitcoin/#introduction. */ - case class BlockcypherExplorer(socksProxy_opt: Option[Socks5ProxyParams])(implicit val sb: SttpBackend[Future, Nothing]) extends Explorer with SupportsTor { + case class BlockcypherExplorer()(implicit val sb: SttpBackend[Future, _]) extends Explorer { override val name = "blockcypher.com" override val baseUris = Map( Block.TestnetGenesisBlock.hash -> uri"https://api.blockcypher.com/v1/btc/test3", @@ -130,60 +131,58 @@ object ExplorerApi { } yield headers } - private def getTip(baseUri: Uri)(implicit ec: ExecutionContext, sb: SttpBackend[Future, Nothing]): Future[BlockHeight] = { + private def getTip(baseUri: Uri)(implicit ec: ExecutionContext): Future[BlockHeight] = { for { - tip <- sttp.readTimeout(30 seconds).get(baseUri) + tip <- basicRequest.readTimeout(30 seconds).get(baseUri) .headers(Socks5ProxyParams.FakeFirefoxHeaders) .response(asJson[JObject]) - .send() - .map(r => { - val JInt(latestHeight) = r.unsafeBody \ "height" + .send(sb) + .map(_.body.fold(exc => throw exc, jvalue => jvalue)) + .map(json => { + val JInt(latestHeight) = json \ "height" BlockHeight(latestHeight.toLong) }) } yield tip } - private def getHeader(baseUri: Uri, blockCount: Long)(implicit ec: ExecutionContext, sb: SttpBackend[Future, Nothing]): Future[Seq[BlockHeaderAt]] = for { - header <- sttp.readTimeout(30 seconds).get(baseUri.path(baseUri.path :+ "blocks" :+ blockCount.toString)) + private def getHeader(baseUri: Uri, blockCount: Long)(implicit ec: ExecutionContext): Future[Seq[BlockHeaderAt]] = for { + header <- basicRequest.readTimeout(30 seconds).get(baseUri.addPath("blocks", blockCount.toString)) .headers(Socks5ProxyParams.FakeFirefoxHeaders) .response(asJson[JObject]) - .send() - .map(r => r.code match { + .send(sb) + .map(r => r.body match { // HTTP 404 is a "normal" error: we're trying to lookup future blocks that haven't been mined. - case StatusCodes.NotFound => Seq.empty - case _ => r.unsafeBody \ "error" match { - case JString(error) if error == s"Block $blockCount not found." => Seq.empty - case _ => Seq(r.unsafeBody) - } + case Left(res: HttpError[_]) if res.statusCode == StatusCode.NotFound => Seq.empty + case Left(otherError) => throw otherError + case Right(json) if json \ "error" == JString(s"Block $blockCount not found.") => Seq.empty + case Right(block) => + val JInt(height) = block \ "height" + val JInt(version) = block \ "ver" + val JString(time) = block \ "time" + val JInt(bits) = block \ "bits" + val JInt(nonce) = block \ "nonce" + val previousBlockHash = (block \ "prev_block").extractOpt[String].map(ByteVector32.fromValidHex(_).reverse).getOrElse(ByteVector32.Zeroes) + val merkleRoot = (block \ "mrkl_root").extractOpt[String].map(ByteVector32.fromValidHex(_).reverse).getOrElse(ByteVector32.Zeroes) + val header = BlockHeader(version.toLong, previousBlockHash, merkleRoot, OffsetDateTime.parse(time).toEpochSecond, bits.toLong, nonce.toLong) + Seq(BlockHeaderAt(BlockHeight(height.toLong), header)) }) - .map(blocks => blocks.map(block => { - val JInt(height) = block \ "height" - val JInt(version) = block \ "ver" - val JString(time) = block \ "time" - val JInt(bits) = block \ "bits" - val JInt(nonce) = block \ "nonce" - val previousBlockHash = (block \ "prev_block").extractOpt[String].map(ByteVector32.fromValidHex(_).reverse).getOrElse(ByteVector32.Zeroes) - val merkleRoot = (block \ "mrkl_root").extractOpt[String].map(ByteVector32.fromValidHex(_).reverse).getOrElse(ByteVector32.Zeroes) - val header = BlockHeader(version.toLong, previousBlockHash, merkleRoot, OffsetDateTime.parse(time).toEpochSecond, bits.toLong, nonce.toLong) - BlockHeaderAt(BlockHeight(height.toLong), header) - })) } yield header } /** Explorer API based on Esplora: see https://github.com/Blockstream/esplora/blob/master/API.md. */ - sealed trait Esplora extends Explorer with SupportsTor { - implicit val sb: SttpBackend[Future, Nothing] + sealed trait Esplora extends Explorer { + implicit val sb: SttpBackend[Future, _] override def getLatestHeaders(baseUri: Uri, currentBlockHeight: BlockHeight)(implicit context: ActorContext[Command]): Future[LatestHeaders] = { implicit val ec: ExecutionContext = context.system.executionContext for { - headers <- sttp.readTimeout(10 seconds).get(baseUri.path(baseUri.path :+ "blocks")) + headers <- basicRequest.readTimeout(10 seconds).get(baseUri.addPath("blocks")) .response(asJson[JArray]) - .send() + .send(sb) .map(r => r.code match { // HTTP 404 is a "normal" error: we're trying to lookup future blocks that haven't been mined. - case StatusCodes.NotFound => Seq.empty - case _ => r.unsafeBody.arr + case StatusCode.NotFound => Seq.empty + case _ => r.body.fold(exc => throw exc, jvalue => jvalue).arr }) .map(blocks => blocks.map(block => { val JInt(height) = block \ "height" @@ -202,36 +201,34 @@ object ExplorerApi { } /** Query https://blockstream.info/ to fetch block headers. */ - case class BlockstreamExplorer(socksProxy_opt: Option[Socks5ProxyParams])(implicit val sb: SttpBackend[Future, Nothing]) extends Esplora { + case class BlockstreamExplorer(useTorEndpoints: Boolean)(implicit val sb: SttpBackend[Future, _]) extends Esplora { override val name = "blockstream.info" - override val baseUris = socksProxy_opt match { - case Some(_) => - Map( - Block.TestnetGenesisBlock.hash -> uri"http://explorerzydxu5ecjrkwceayqybizmpjjznk5izmitf2modhcusuqlid.onion/testnet/api", - Block.LivenetGenesisBlock.hash -> uri"http://explorerzydxu5ecjrkwceayqybizmpjjznk5izmitf2modhcusuqlid.onion/api" - ) - case None => - Map( - Block.TestnetGenesisBlock.hash -> uri"https://blockstream.info/testnet/api", - Block.LivenetGenesisBlock.hash -> uri"https://blockstream.info/api" - ) + override val baseUris = if (useTorEndpoints) { + Map( + Block.TestnetGenesisBlock.hash -> uri"http://explorerzydxu5ecjrkwceayqybizmpjjznk5izmitf2modhcusuqlid.onion/testnet/api", + Block.LivenetGenesisBlock.hash -> uri"http://explorerzydxu5ecjrkwceayqybizmpjjznk5izmitf2modhcusuqlid.onion/api" + ) + } else { + Map( + Block.TestnetGenesisBlock.hash -> uri"https://blockstream.info/testnet/api", + Block.LivenetGenesisBlock.hash -> uri"https://blockstream.info/api" + ) } } /** Query https://mempool.space/ to fetch block headers. */ - case class MempoolSpaceExplorer(socksProxy_opt: Option[Socks5ProxyParams])(implicit val sb: SttpBackend[Future, Nothing]) extends Esplora { + case class MempoolSpaceExplorer(useTorEndpoints: Boolean)(implicit val sb: SttpBackend[Future, _]) extends Esplora { override val name = "mempool.space" - override val baseUris = socksProxy_opt match { - case Some(_) => - Map( - Block.TestnetGenesisBlock.hash -> uri"http://mempoolhqx4isw62xs7abwphsq7ldayuidyx2v2oethdhhj6mlo2r6ad.onion/testnet/api", - Block.LivenetGenesisBlock.hash -> uri"http://mempoolhqx4isw62xs7abwphsq7ldayuidyx2v2oethdhhj6mlo2r6ad.onion/api" - ) - case None => - Map( - Block.TestnetGenesisBlock.hash -> uri"https://mempool.space/testnet/api", - Block.LivenetGenesisBlock.hash -> uri"https://mempool.space/api" - ) + override val baseUris = if (useTorEndpoints) { + Map( + Block.TestnetGenesisBlock.hash -> uri"http://mempoolhqx4isw62xs7abwphsq7ldayuidyx2v2oethdhhj6mlo2r6ad.onion/testnet/api", + Block.LivenetGenesisBlock.hash -> uri"http://mempoolhqx4isw62xs7abwphsq7ldayuidyx2v2oethdhhj6mlo2r6ad.onion/api" + ) + } else { + Map( + Block.TestnetGenesisBlock.hash -> uri"https://mempool.space/testnet/api", + Block.LivenetGenesisBlock.hash -> uri"https://mempool.space/api" + ) } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/TransportHandler.scala b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/TransportHandler.scala index 1a333c8c78..1834908a93 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/TransportHandler.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/TransportHandler.scala @@ -177,7 +177,7 @@ class TransportHandler[T: ClassTag](keyPair: KeyPair, rs: Option[ByteVector], co when(Normal) { handleExceptions { - case Event(Tcp.Received(data), d: NormalData[T]) => + case Event(Tcp.Received(data), d: NormalData[T @unchecked]) => val (dec1, plaintextMessages) = d.decryptor.copy(buffer = d.decryptor.buffer ++ data).decrypt() if (plaintextMessages.isEmpty) { connection ! Tcp.ResumeReading @@ -188,7 +188,7 @@ class TransportHandler[T: ClassTag](keyPair: KeyPair, rs: Option[ByteVector], co stay() using NormalData(d.encryptor, dec1, d.listener, d.sendBuffer, unackedReceived, d.unackedSent) } - case Event(ReadAck(msg: T), d: NormalData[T]) => + case Event(ReadAck(msg: T), d: NormalData[T @unchecked]) => // how many occurences of this message are still unacked? val remaining = d.unackedReceived.getOrElse(msg, 0) - 1 log.debug("acking message {}", msg) @@ -203,7 +203,7 @@ class TransportHandler[T: ClassTag](keyPair: KeyPair, rs: Option[ByteVector], co stay() using d.copy(unackedReceived = unackedReceived1) } - case Event(t: T, d: NormalData[T]) => + case Event(t: T, d: NormalData[T @unchecked]) => if (d.sendBuffer.normalPriority.size + d.sendBuffer.lowPriority.size >= MAX_BUFFERED) { log.warning("send buffer overrun, closing connection") connection ! PoisonPill @@ -224,7 +224,7 @@ class TransportHandler[T: ClassTag](keyPair: KeyPair, rs: Option[ByteVector], co stay() using d.copy(encryptor = enc1, unackedSent = Some(t)) } - case Event(WriteAck, d: NormalData[T]) => + case Event(WriteAck, d: NormalData[T @unchecked]) => def send(t: T) = { diag(t, "OUT") val blob = codec.encode(t).require.toByteVector @@ -262,7 +262,7 @@ class TransportHandler[T: ClassTag](keyPair: KeyPair, rs: Option[ByteVector], co case Event(msg, d) => d match { - case n: NormalData[T] => log.warning(s"unhandled message $msg in state normal unackedSent=${n.unackedSent.size} unackedReceived=${n.unackedReceived.size} sendBuffer.lowPriority=${n.sendBuffer.lowPriority.size} sendBuffer.normalPriority=${n.sendBuffer.normalPriority.size}") + case n: NormalData[_] => log.warning(s"unhandled message $msg in state normal unackedSent=${n.unackedSent.size} unackedReceived=${n.unackedReceived.size} sendBuffer.lowPriority=${n.sendBuffer.lowPriority.size} sendBuffer.normalPriority=${n.sendBuffer.normalPriority.size}") case _ => log.warning(s"unhandled message $msg in state ${d.getClass.getSimpleName}") } stay() diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/json/JsonSerializers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/json/JsonSerializers.scala index ac900995ce..4c3479bd2e 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/json/JsonSerializers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/json/JsonSerializers.scala @@ -432,16 +432,17 @@ object OnionMessageReceivedSerializer extends ConvertClassSerializer[OnionMessag case class CustomTypeHints(custom: Map[Class[_], String]) extends TypeHints { val reverse: Map[String, Class[_]] = custom.map(_.swap) + override def typeHintFieldName: String = "type" + override val hints: List[Class[_]] = custom.keys.toList - override def hintFor(clazz: Class[_]): String = custom.getOrElse(clazz, { - throw new IllegalArgumentException(s"No type hint mapping found for $clazz") - }) + override def hintFor(clazz: Class[_]): Option[String] = custom.get(clazz) - override def classFor(hint: String): Option[Class[_]] = reverse.get(hint) + override def classFor(hint: String, parent: Class[_]): Option[Class[_]] = reverse.get(hint) } object CustomTypeHints { + val incomingPaymentStatus: CustomTypeHints = CustomTypeHints(Map( IncomingPaymentStatus.Pending.getClass -> "pending", IncomingPaymentStatus.Expired.getClass -> "expired", @@ -482,14 +483,14 @@ object CustomTypeHints { classOf[DATA_NEGOTIATING], classOf[DATA_CLOSING], classOf[DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT] - )) + ), typeHintFieldName = "type") } object JsonSerializers { implicit val serialization: Serialization.type = jackson.Serialization - implicit val formats: Formats = org.json4s.DefaultFormats.withTypeHintFieldName("type") + + implicit val formats: Formats = org.json4s.DefaultFormats + CustomTypeHints.incomingPaymentStatus + CustomTypeHints.outgoingPaymentStatus + CustomTypeHints.paymentEvent + diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/remote/EclairInternalsSerializer.scala b/eclair-core/src/main/scala/fr/acinq/eclair/remote/EclairInternalsSerializer.scala index 1ec53767c5..a97e176456 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/remote/EclairInternalsSerializer.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/remote/EclairInternalsSerializer.scala @@ -46,7 +46,7 @@ object EclairInternalsSerializer { def finiteDurationCodec: Codec[FiniteDuration] = int64.xmap(_.milliseconds, _.toMillis) - def iterable[A](codec: Codec[A]): Codec[Iterable[A]] = listOfN(uint16, codec).xmap(_.toIterable, _.toList) + def iterable[A](codec: Codec[A]): Codec[Iterable[A]] = listOfN(uint16, codec).xmap(_.toList, _.toList) val searchBoundariesCodec: Codec[SearchBoundaries] = ( ("maxFee" | millisatoshi) :: diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/TestBitcoinCoreClient.scala b/eclair-core/src/test/scala/fr/acinq/eclair/TestBitcoinCoreClient.scala index ef69920b87..ab9482360f 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/TestBitcoinCoreClient.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/TestBitcoinCoreClient.scala @@ -28,7 +28,7 @@ import scala.concurrent.{ExecutionContext, Future} /** * Created by PM on 26/04/2016. */ -class TestBitcoinCoreClient()(implicit system: ActorSystem) extends BitcoinCoreClient(new BasicBitcoinJsonRPCClient(UserPassword("", ""), "", 0)(http = null)) { +class TestBitcoinCoreClient()(implicit system: ActorSystem) extends BitcoinCoreClient(new BasicBitcoinJsonRPCClient(UserPassword("", ""), "", 0)(sb = null)) { import scala.concurrent.ExecutionContext.Implicits.global diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoindService.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoindService.scala index 5018a79097..c4ad31b42b 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoindService.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoindService.scala @@ -19,7 +19,6 @@ package fr.acinq.eclair.blockchain.bitcoind import akka.actor.{Actor, ActorRef, ActorSystem, Props} import akka.pattern.pipe import akka.testkit.{TestKitBase, TestProbe} -import com.softwaremill.sttp.okhttp.OkHttpFutureBackend import fr.acinq.bitcoin.Crypto.PrivateKey import fr.acinq.bitcoin.{Block, Btc, BtcAmount, ByteVector32, MilliBtc, OutPoint, Satoshi, Transaction, computeP2WpkhAddress} import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinJsonRPCAuthMethod.{SafeCookie, UserPassword} @@ -28,6 +27,7 @@ import fr.acinq.eclair.integration.IntegrationSpec import fr.acinq.eclair.{BlockHeight, TestUtils, randomKey} import grizzled.slf4j.Logging import org.json4s.JsonAST._ +import sttp.client3.okhttp.OkHttpFutureBackend import java.io.File import java.nio.file.Files diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/watchdogs/ExplorerApiSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/watchdogs/ExplorerApiSpec.scala index 0d127ea1a9..2d47ca8e8a 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/watchdogs/ExplorerApiSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/watchdogs/ExplorerApiSpec.scala @@ -28,7 +28,7 @@ class ExplorerApiSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl implicit val sttpBackend = ExplorerApi.createSttpBackend(None) - val explorers = Seq(BlockcypherExplorer(None), BlockstreamExplorer(None), MempoolSpaceExplorer(None)) + val explorers = Seq(BlockcypherExplorer(), BlockstreamExplorer(useTorEndpoints = false), MempoolSpaceExplorer(useTorEndpoints = false)) test("fetch latest block headers", TestTags.ExternalApi) { for (explorer <- explorers) { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/AnnouncementsBatchValidationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/AnnouncementsBatchValidationSpec.scala index 80dba79fc3..6b5b826f63 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/AnnouncementsBatchValidationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/AnnouncementsBatchValidationSpec.scala @@ -19,7 +19,7 @@ package fr.acinq.eclair.router import akka.actor.ActorSystem import akka.pattern.pipe import akka.testkit.TestProbe -import com.softwaremill.sttp.okhttp.OkHttpFutureBackend +import sttp.client3.okhttp.OkHttpFutureBackend import fr.acinq.bitcoin.Crypto.PrivateKey import fr.acinq.bitcoin.{Block, Satoshi, SatoshiLong, Script, Transaction} import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.ValidateResult diff --git a/eclair-node/pom.xml b/eclair-node/pom.xml index 385cf66486..6f75cdba85 100644 --- a/eclair-node/pom.xml +++ b/eclair-node/pom.xml @@ -101,7 +101,7 @@ de.heikoseeberger akka-http-json4s_${scala.version.short} - 1.37.0 + 1.39.2 diff --git a/pom.xml b/pom.xml index 7cfed54aab..72af6be938 100644 --- a/pom.xml +++ b/pom.xml @@ -67,14 +67,14 @@ UTF-8 11 11 - 2.13.6 + 2.13.8 2.13 - 2.6.15 - 10.2.4 - 1.7.2 + 2.6.18 + 10.2.7 + 3.4.1 0.19 24.0-android - 2.2.2 + 2.4.6 From 66aafd68723bf6a6ad2dd5612586c7cf0335b279 Mon Sep 17 00:00:00 2001 From: Thomas HUET <81159533+thomash-acinq@users.noreply.github.com> Date: Wed, 9 Feb 2022 10:49:38 +0100 Subject: [PATCH 019/121] Better logging for NegativeProbability exception (#2171) Show details that lead to this exception. --- .../main/scala/fr/acinq/eclair/router/RouteCalculation.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/RouteCalculation.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/RouteCalculation.scala index 8c2d12755c..d1c409dd93 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/RouteCalculation.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/RouteCalculation.scala @@ -135,7 +135,7 @@ object RouteCalculation { Metrics.FindRouteErrors.withTags(tags.withTag(Tags.Error, "InfiniteLoop")).increment() ctx.sender() ! Status.Failure(failure) case Failure(failure: NegativeProbability) => - log.error(s"computed negative probability $failure") + log.error(s"computed negative probability: edge=${failure.edge}, weight=${failure.weight}, heuristicsConstants=${failure.heuristicsConstants}") Metrics.FindRouteErrors.withTags(tags.withTag(Tags.Error, "NegativeProbability")).increment() ctx.sender() ! Status.Failure(failure) case Failure(t) => From 92221e8409d04bc604ad1ba062b9c34b513c51ea Mon Sep 17 00:00:00 2001 From: Thomas HUET <81159533+thomash-acinq@users.noreply.github.com> Date: Wed, 9 Feb 2022 16:25:27 +0100 Subject: [PATCH 020/121] Fix path-finding (#2172) We used to compute the edge weight before checking that the edge could actually relay the payment. Since #2111 we throw an exception in case `heuristicsConstants` are used and the edge can't relay the payment instead of just ignoring the edge. We fix this by checking that the edge can relay the payment before trying to call `addEdgeWeight`. --- .../scala/fr/acinq/eclair/router/Graph.scala | 33 ++++++++++--------- .../eclair/router/RouteCalculationSpec.scala | 19 +++++++++++ 2 files changed, 37 insertions(+), 15 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/Graph.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/Graph.scala index 22adfbc5a2..20fef16f3c 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/Graph.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/Graph.scala @@ -246,23 +246,26 @@ object Graph { } neighborEdges.foreach { edge => val neighbor = edge.desc.a - // NB: this contains the amount (including fees) that will need to be sent to `neighbor`, but the amount that - // will be relayed through that edge is the one in `currentWeight`. - val neighborWeight = addEdgeWeight(sourceNode, edge, current.weight, currentBlockHeight, wr, includeLocalChannelCost) - val canRelayAmount = current.weight.amount <= edge.capacity && + if (current.weight.amount <= edge.capacity && edge.balance_opt.forall(current.weight.amount <= _) && edge.update.htlcMaximumMsat.forall(current.weight.amount <= _) && - current.weight.amount >= edge.update.htlcMinimumMsat - if (canRelayAmount && boundaries(neighborWeight) && !ignoredEdges.contains(edge.desc) && !ignoredVertices.contains(neighbor)) { - val previousNeighborWeight = bestWeights.getOrElse(neighbor, RichWeight(MilliSatoshi(Long.MaxValue), Int.MaxValue, CltvExpiryDelta(Int.MaxValue), 0.0, MilliSatoshi(Long.MaxValue), MilliSatoshi(Long.MaxValue), Double.MaxValue)) - // if this path between neighbor and the target has a shorter distance than previously known, we select it - if (neighborWeight.weight < previousNeighborWeight.weight) { - // update the best edge for this vertex - bestEdges.put(neighbor, edge) - // add this updated node to the list for further exploration - toExplore.enqueue(WeightedNode(neighbor, neighborWeight)) // O(1) - // update the minimum known distance array - bestWeights.put(neighbor, neighborWeight) + current.weight.amount >= edge.update.htlcMinimumMsat && + !ignoredEdges.contains(edge.desc) && + !ignoredVertices.contains(neighbor)) { + // NB: this contains the amount (including fees) that will need to be sent to `neighbor`, but the amount that + // will be relayed through that edge is the one in `currentWeight`. + val neighborWeight = addEdgeWeight(sourceNode, edge, current.weight, currentBlockHeight, wr, includeLocalChannelCost) + if (boundaries(neighborWeight)) { + val previousNeighborWeight = bestWeights.getOrElse(neighbor, RichWeight(MilliSatoshi(Long.MaxValue), Int.MaxValue, CltvExpiryDelta(Int.MaxValue), 0.0, MilliSatoshi(Long.MaxValue), MilliSatoshi(Long.MaxValue), Double.MaxValue)) + // if this path between neighbor and the target has a shorter distance than previously known, we select it + if (neighborWeight.weight < previousNeighborWeight.weight) { + // update the best edge for this vertex + bestEdges.put(neighbor, edge) + // add this updated node to the list for further exploration + toExplore.enqueue(WeightedNode(neighbor, neighborWeight)) // O(1) + // update the minimum known distance array + bestWeights.put(neighbor, neighborWeight) + } } } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/RouteCalculationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/RouteCalculationSpec.scala index a343a66168..123d7a0cd4 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/RouteCalculationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/RouteCalculationSpec.scala @@ -1828,6 +1828,25 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { val route :: Nil = routes assert(route2Ids(route) === 0 :: 2 :: 3 :: 4 :: Nil) } + + test("edge too small to relay payment is ignored") { + // A ===> B ===> C <--- D + val g = DirectedGraph(List( + makeEdge(1L, a, b, 100 msat, 100), + makeEdge(2L, b, c, 100 msat, 100), + makeEdge(3L, d, c, 100 msat, 100, capacity = 1000 sat), + )) + + val hc = HeuristicsConstants( + lockedFundsRisk = 1e-7, + failureCost = RelayFees(0 msat, 0), + hopCost = RelayFees(0 msat, 0), + ) + val Success(routes) = findRoute(g, a, c, DEFAULT_AMOUNT_MSAT, 100000000 msat, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.copy(heuristics = Right(hc)), currentBlockHeight = BlockHeight(400000)) + assert(routes.distinct.length == 1) + val route :: Nil = routes + assert(route2Ids(route) === 1 :: 2 :: Nil) + } } object RouteCalculationSpec { From 53cb50a36e623c1dffea8a219167f634a3072a01 Mon Sep 17 00:00:00 2001 From: Fabrice Drouin Date: Wed, 16 Feb 2022 11:43:50 +0100 Subject: [PATCH 021/121] Restrict building and publishing docker images to new pushes on master (#2182) It is not really useful to build docker images for PRs and would not work anyway for forks that are not in the ACINQ repo. --- .github/workflows/docker-image.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index c8aa2ebe5f..8d145e39c4 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -3,8 +3,6 @@ name: Docker Image CI on: push: branches: [ master ] - pull_request: - branches: [ master ] jobs: From 99aabdabd046baf82e2ed83625ef1712b785f1a5 Mon Sep 17 00:00:00 2001 From: Bastien Teinturier <31281497+t-bast@users.noreply.github.com> Date: Thu, 17 Feb 2022 08:19:00 +0100 Subject: [PATCH 022/121] Add txId in logback.xml (#2183) We changed the logging MDC key for tx-publishing in #2131, but forgot to add a formatting rule in logback.xml for that new field, so it wasn't written to logs. --- eclair-core/src/test/resources/logback-test.xml | 2 +- eclair-front/src/main/resources/logback.xml | 6 +++--- eclair-node/src/main/resources/logback.xml | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/eclair-core/src/test/resources/logback-test.xml b/eclair-core/src/test/resources/logback-test.xml index 21441ee3ad..e3f0af56c7 100644 --- a/eclair-core/src/test/resources/logback-test.xml +++ b/eclair-core/src/test/resources/logback-test.xml @@ -20,7 +20,7 @@ System.out - %date{HH:mm:ss.SSS} %highlight(%-5level) %replace(%logger{24}){'\$.*',''}%X{category}%X{nodeId}%X{channelId}%X{paymentHash}%.-11X{parentPaymentId}%.-11X{paymentId} - %msg%ex{12}%n + %date{HH:mm:ss.SSS} %highlight(%-5level) %replace(%logger{24}){'\$.*',''}%X{category}%X{nodeId}%X{channelId}%X{paymentHash}%.-11X{parentPaymentId}%.-11X{paymentId}%.-11X{txPublishId} - %msg%ex{12}%n diff --git a/eclair-front/src/main/resources/logback.xml b/eclair-front/src/main/resources/logback.xml index f7667a5d39..bb7f4348ce 100644 --- a/eclair-front/src/main/resources/logback.xml +++ b/eclair-front/src/main/resources/logback.xml @@ -21,7 +21,7 @@ System.out false - %yellow(${HOSTNAME} %d) %highlight(%-5level) %replace(%logger{24}){'\$.*',''}%X{category}%X{nodeId}%X{channelId}%X{paymentHash}%.-11X{parentPaymentId}%.-11X{paymentId} - %msg%ex{12}%n + %yellow(${HOSTNAME} %d) %highlight(%-5level) %replace(%logger{24}){'\$.*',''}%X{category}%X{nodeId}%X{channelId}%X{paymentHash}%.-11X{parentPaymentId}%.-11X{paymentId}%.-11X{txPublishId} - %msg%ex{12}%n @@ -35,7 +35,7 @@ 5GB - %d %-5level %replace(%logger{24}){'\$.*',''}%X{category}%X{nodeId}%X{channelId}%X{paymentHash}%.-11X{parentPaymentId}%.-11X{paymentId} - %msg%ex{24}%n + %d %-5level %replace(%logger{24}){'\$.*',''}%X{category}%X{nodeId}%X{channelId}%X{paymentHash}%.-11X{parentPaymentId}%.-11X{paymentId}%.-11X{txPublishId} - %msg%ex{24}%n @@ -57,7 +57,7 @@ - %d %-5level %replace(%logger{24}){'\$.*',''}%X{category}%X{nodeId}%X{channelId}%X{paymentHash}%.-11X{parentPaymentId}%.-11X{paymentId} - %msg%ex{24}%n + %d %-5level %replace(%logger{24}){'\$.*',''}%X{category}%X{nodeId}%X{channelId}%X{paymentHash}%.-11X{parentPaymentId}%.-11X{paymentId}%.-11X{txPublishId} - %msg%ex{24}%n diff --git a/eclair-node/src/main/resources/logback.xml b/eclair-node/src/main/resources/logback.xml index 24b37b6544..4b25179b74 100644 --- a/eclair-node/src/main/resources/logback.xml +++ b/eclair-node/src/main/resources/logback.xml @@ -21,7 +21,7 @@ System.out false - %yellow(${HOSTNAME} %d) %highlight(%-5level) %replace(%logger{24}){'\$.*',''}%X{category}%X{nodeId}%X{channelId}%X{paymentHash}%.-11X{parentPaymentId}%.-11X{paymentId} - %msg%ex{12}%n + %yellow(${HOSTNAME} %d) %highlight(%-5level) %replace(%logger{24}){'\$.*',''}%X{category}%X{nodeId}%X{channelId}%X{paymentHash}%.-11X{parentPaymentId}%.-11X{paymentId}%.-11X{txPublishId} - %msg%ex{12}%n @@ -35,7 +35,7 @@ 5GB - %d %-5level %replace(%logger{24}){'\$.*',''}%X{category}%X{nodeId}%X{channelId}%X{paymentHash}%.-11X{parentPaymentId}%.-11X{paymentId} - %msg%ex{24}%n + %d %-5level %replace(%logger{24}){'\$.*',''}%X{category}%X{nodeId}%X{channelId}%X{paymentHash}%.-11X{parentPaymentId}%.-11X{paymentId}%.-11X{txPublishId} - %msg%ex{24}%n @@ -57,7 +57,7 @@ - %d %-5level %replace(%logger{24}){'\$.*',''}%X{category}%X{nodeId}%X{channelId}%X{paymentHash}%.-11X{parentPaymentId}%.-11X{paymentId} - %msg%ex{24}%n + %d %-5level %replace(%logger{24}){'\$.*',''}%X{category}%X{nodeId}%X{channelId}%X{paymentHash}%.-11X{parentPaymentId}%.-11X{paymentId}%.-11X{txPublishId} - %msg%ex{24}%n From 609c206a675ff3f98fedff7399216bd022b2a637 Mon Sep 17 00:00:00 2001 From: Bastien Teinturier <31281497+t-bast@users.noreply.github.com> Date: Thu, 17 Feb 2022 16:30:59 +0100 Subject: [PATCH 023/121] Ignore IBD in regtest (#2185) When starting bitcoind on regtest, it sets the IBD flag until a few blocks have been mined. However, this isn't a real IBD and it doesn't prevent eclair from working at all, so we should ignore that requirement. --- .../main/scala/fr/acinq/eclair/Setup.scala | 36 +++++++++---------- 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala index 8620882551..7c74fca89e 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala @@ -187,12 +187,12 @@ class Setup(val datadir: File, val (progress, initialBlockDownload, chainHash, bitcoinVersion, unspentAddresses, blocks, headers) = await(future, 30 seconds, "bicoind did not respond after 30 seconds") assert(bitcoinVersion >= 180000, "Eclair requires Bitcoin Core 0.18.0 or higher") assert(chainHash == nodeParams.chainHash, s"chainHash mismatch (conf=${nodeParams.chainHash} != bitcoind=$chainHash)") + assert(unspentAddresses.forall(address => !isPay2PubkeyHash(address)), "Your wallet contains non-segwit UTXOs. You must send those UTXOs to a bech32 address to use Eclair (check out our README for more details).") if (chainHash != Block.RegtestGenesisBlock.hash) { - assert(unspentAddresses.forall(address => !isPay2PubkeyHash(address)), "Your wallet contains non-segwit UTXOs. You must send those UTXOs to a bech32 address to use Eclair (check out our README for more details).") + assert(!initialBlockDownload, s"bitcoind should be synchronized (initialblockdownload=$initialBlockDownload)") + assert(progress > 0.999, s"bitcoind should be synchronized (progress=$progress)") + assert(headers - blocks <= 1, s"bitcoind should be synchronized (headers=$headers blocks=$blocks)") } - assert(!initialBlockDownload, s"bitcoind should be synchronized (initialblockdownload=$initialBlockDownload)") - assert(progress > 0.999, s"bitcoind should be synchronized (progress=$progress)") - assert(headers - blocks <= 1, s"bitcoind should be synchronized (headers=$headers blocks=$blocks)") logger.info(s"current blockchain height=$blocks") blockHeight.set(blocks) bitcoinClient @@ -233,21 +233,19 @@ class Setup(val datadir: File, case _ => new FallbackFeeProvider(new SmoothFeeProvider(new BitcoinCoreFeeProvider(bitcoin, defaultFeerates), smoothFeerateWindow) :: Nil, minFeeratePerByte) } - _ = system.scheduler.scheduleWithFixedDelay(0 seconds, 10 minutes)(new Runnable { - override def run(): Unit = feeProvider.getFeerates.onComplete { - case Success(feerates) => - feeratesPerKB.set(feerates) - feeratesPerKw.set(FeeratesPerKw(feerates)) - channel.Monitoring.Metrics.LocalFeeratePerKw.withoutTags().update(feeratesPerKw.get.feePerBlock(nodeParams.onChainFeeConf.feeTargets.commitmentBlockTarget).toLong.toDouble) - blockchain.Monitoring.Metrics.MempoolMinFeeratePerKw.withoutTags().update(feeratesPerKw.get.mempoolMinFee.toLong.toDouble) - system.eventStream.publish(CurrentFeerates(feeratesPerKw.get)) - logger.info(s"current feeratesPerKB=${feeratesPerKB.get} feeratesPerKw=${feeratesPerKw.get}") - feeratesRetrieved.trySuccess(Done) - case Failure(exception) => - logger.warn(s"cannot retrieve feerates: ${exception.getMessage}") - blockchain.Monitoring.Metrics.CannotRetrieveFeeratesCount.withoutTags().increment() - feeratesRetrieved.tryFailure(CannotRetrieveFeerates) - } + _ = system.scheduler.scheduleWithFixedDelay(0 seconds, 10 minutes)(() => feeProvider.getFeerates.onComplete { + case Success(feerates) => + feeratesPerKB.set(feerates) + feeratesPerKw.set(FeeratesPerKw(feerates)) + channel.Monitoring.Metrics.LocalFeeratePerKw.withoutTags().update(feeratesPerKw.get.feePerBlock(nodeParams.onChainFeeConf.feeTargets.commitmentBlockTarget).toLong.toDouble) + blockchain.Monitoring.Metrics.MempoolMinFeeratePerKw.withoutTags().update(feeratesPerKw.get.mempoolMinFee.toLong.toDouble) + system.eventStream.publish(CurrentFeerates(feeratesPerKw.get)) + logger.info(s"current feeratesPerKB=${feeratesPerKB.get} feeratesPerKw=${feeratesPerKw.get}") + feeratesRetrieved.trySuccess(Done) + case Failure(exception) => + logger.warn(s"cannot retrieve feerates: ${exception.getMessage}") + blockchain.Monitoring.Metrics.CannotRetrieveFeeratesCount.withoutTags().increment() + feeratesRetrieved.tryFailure(CannotRetrieveFeerates) }) _ <- feeratesRetrieved.future From b5b2022efc71b0d15fd98d29995dfc11f75d5b70 Mon Sep 17 00:00:00 2001 From: Bastien Teinturier <31281497+t-bast@users.noreply.github.com> Date: Mon, 21 Feb 2022 08:12:42 +0100 Subject: [PATCH 024/121] Add more PR details to contributing guidelines (#2186) * Add link to my LN articles in learning resources * Detail pull request process --- CONTRIBUTING.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2ed645e5ec..70687fd2c7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -32,6 +32,7 @@ You can also use Github issues for [feature requests](https://github.com/acinq/e - [Understanding the Lightning Network](https://bitcoinmagazine.com/articles/understanding-the-lightning-network-part-building-a-bidirectional-payment-channel-1464710791) - [Lightning Network Specification](https://github.com/lightning/bolts) - [High Level Lightning Network Specification](https://medium.com/@rusty_lightning/the-bitcoin-lightning-spec-part-1-8-a7720fb1b4da) +- [t-bast's in-depth articles about the Lightning Network](https://github.com/t-bast/lightning-docs/) ## Recommended Skillset @@ -82,6 +83,12 @@ The reason for this is two fold: it makes it easier for the reviewer to see what made between versions (since Github doesn't easily show prior versions) and it makes it easier on the PR author as they can set it to auto-squash the fixup commits on rebase. +When you have addressed a pull request comment, you should respond to it and point the reviewer +to the commit where you fixed it. A simple "This is fixed in " is enough. Do not +mark the comment as resolved: let the reviewer verify your commit, they will mark the comment +resolved if they agree with the fix. This makes it easier for reviewers to check that all their +comments have been addressed. + It's recommended to take great care in writing tests and ensuring the entire test suite has a stable successful outcome; eclair uses continuous integration techniques and having a stable build helps the reviewers with their job. From 068b1393e16e33adc971e7ef4b189cbfd82f245c Mon Sep 17 00:00:00 2001 From: Richard Myers Date: Mon, 21 Feb 2022 14:55:37 +0100 Subject: [PATCH 025/121] purge expired invoice at given time interval (#2174) When Eclair first starts, search for all unpaid expired incoming payments and purge them from the payments database. These payments take up space in the database but will no longer be paid and so should be removed to improve performance and reduce the size of the database. Purges will be triggered every 24 hours by default but can be disabled or configured with a different interval. The invoice purger only looks back 15 days for expired invoices when triggered after the initial scan to reduce its impact on database performance. Co-authored-by: Thomas HUET <81159533+thomash-acinq@users.noreply.github.com> Co-authored-by: Bastien Teinturier <31281497+t-bast@users.noreply.github.com> --- eclair-core/src/main/resources/reference.conf | 5 + .../scala/fr/acinq/eclair/NodeParams.scala | 12 +- .../payment/receive/InvoicePurger.scala | 71 ++++++++++ .../payment/receive/PaymentHandler.scala | 11 ++ .../scala/fr/acinq/eclair/TestConstants.scala | 6 +- .../payment/receive/InvoicePurgerSpec.scala | 129 ++++++++++++++++++ 6 files changed, 230 insertions(+), 4 deletions(-) create mode 100644 eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/InvoicePurger.scala create mode 100644 eclair-core/src/test/scala/fr/acinq/eclair/payment/receive/InvoicePurgerSpec.scala diff --git a/eclair-core/src/main/resources/reference.conf b/eclair-core/src/main/resources/reference.conf index f9d469c774..8a7cf27968 100644 --- a/eclair-core/src/main/resources/reference.conf +++ b/eclair-core/src/main/resources/reference.conf @@ -393,6 +393,11 @@ eclair { # Consider a message to be lost if we haven't received a reply after that amount of time reply-timeout = 20 seconds } + + purge-expired-invoices { + enabled = true // enable automatic purges of expired invoices from the database + interval = 24 hours // interval between expired invoice purges + } } akka { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala index 94f9068fd4..1ca4fdb070 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala @@ -79,7 +79,8 @@ case class NodeParams(nodeKeyManager: NodeKeyManager, enableTrampolinePayment: Boolean, balanceCheckInterval: FiniteDuration, blockchainWatchdogSources: Seq[String], - onionMessageConfig: OnionMessageConfig) { + onionMessageConfig: OnionMessageConfig, + purgeInvoicesInterval: Option[FiniteDuration]) { val privateKey: Crypto.PrivateKey = nodeKeyManager.nodeKey.privateKey val nodeId: PublicKey = nodeKeyManager.nodeId @@ -388,6 +389,12 @@ object NodeParams extends Logging { case "relay-all" => RelayAll } + val purgeInvoicesInterval = if (config.getBoolean("purge-expired-invoices.enabled")) { + Some(FiniteDuration(config.getDuration("purge-expired-invoices.interval").toMinutes, TimeUnit.MINUTES)) + } else { + None + } + NodeParams( nodeKeyManager = nodeKeyManager, channelKeyManager = channelKeyManager, @@ -491,7 +498,8 @@ object NodeParams extends Logging { onionMessageConfig = OnionMessageConfig( relayPolicy = onionMessageRelayPolicy, timeout = FiniteDuration(config.getDuration("onion-messages.reply-timeout").getSeconds, TimeUnit.SECONDS), - ) + ), + purgeInvoicesInterval = purgeInvoicesInterval ) } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/InvoicePurger.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/InvoicePurger.scala new file mode 100644 index 0000000000..28d96061cf --- /dev/null +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/InvoicePurger.scala @@ -0,0 +1,71 @@ +/* + * 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.payment.receive + +import akka.actor.typed.Behavior +import akka.actor.typed.eventstream.EventStream +import akka.actor.typed.scaladsl.{ActorContext, Behaviors} +import fr.acinq.eclair.db.IncomingPaymentsDb +import fr.acinq.eclair.payment.receive.InvoicePurger.{Command, PurgeCompleted, TickPurge} +import fr.acinq.eclair.{TimestampMilli, TimestampMilliLong} + +import scala.concurrent.duration.{DurationInt, FiniteDuration} + +/** + * This actor will purge expired invoices from the database it was initialized with at a scheduled interval. + * At startup scan the entire database for expired invoices, subsequent runs only look back 15 days. + */ +object InvoicePurger { + + def apply(paymentsDb: IncomingPaymentsDb, interval: FiniteDuration): Behavior[Command] = + Behaviors.setup { context => + // wait for purge events sent at `interval` + Behaviors.withTimers { timers => + timers.startTimerAtFixedRate(TickPurge, initialDelay = 0.seconds, interval = interval) + new InvoicePurger(paymentsDb, context).waiting(true) + } + } + + sealed trait Command + + sealed trait PurgeEvent + + // this notification is sent when we have completed our invoice purge process + case object PurgeCompleted extends PurgeEvent + + private case object TickPurge extends Command +} + +class InvoicePurger private(paymentsDb: IncomingPaymentsDb, context: ActorContext[Command]) { + + // purge at each tick unless currently purging + def waiting(fullScan: Boolean): Behavior[Command] = + Behaviors.receiveMessage { + case TickPurge => + val now = TimestampMilli.now() + val start = if (fullScan) 0 unixms else now - 15.days + val expiredPayments = paymentsDb.listExpiredIncomingPayments(start, now) + // purge expired payments + expiredPayments.foreach(p => paymentsDb.removeIncomingPayment(p.invoice.paymentHash)) + + // publish a notification when we have purged expired invoices + if (expiredPayments.nonEmpty) { + context.system.eventStream ! EventStream.Publish(PurgeCompleted) + } + waiting(fullScan = false) + } +} \ No newline at end of file diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/PaymentHandler.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/PaymentHandler.scala index f045a3a904..90a892eb3b 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/PaymentHandler.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/PaymentHandler.scala @@ -17,6 +17,9 @@ package fr.acinq.eclair.payment.receive import akka.actor.Actor.Receive +import akka.actor.typed.SupervisorStrategy +import akka.actor.typed.scaladsl.Behaviors +import akka.actor.typed.scaladsl.adapter.ClassicActorContextOps import akka.actor.{Actor, ActorContext, ActorRef, DiagnosticActorLogging, Props} import akka.event.DiagnosticLoggingAdapter import akka.event.Logging.MDC @@ -34,6 +37,14 @@ class PaymentHandler(nodeParams: NodeParams, register: ActorRef) extends Actor w // we do this instead of sending it to ourselves, otherwise there is no guarantee that this would be the first processed message private val defaultHandler = new MultiPartHandler(nodeParams, register, nodeParams.db.payments) + // Spawn an actor to purge expired invoices at a configured interval + private val purger = nodeParams.purgeInvoicesInterval match { + case Some(interval) => + context.spawn(Behaviors.supervise(InvoicePurger(nodeParams.db.payments, interval)).onFailure(SupervisorStrategy.restart), name = "purge-expired-invoices") + case _ => + log.warning("purge-expired-invoices is disabled") + } + override def receive: Receive = normal(defaultHandler.handle(context, log)) private def addReceiveHandler(handle: Receive): Receive = { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala b/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala index 2fc55e3645..c16bd94de6 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala @@ -194,7 +194,8 @@ object TestConstants { onionMessageConfig = OnionMessageConfig( relayPolicy = RelayAll, timeout = 1 minute - ) + ), + purgeInvoicesInterval = Some(24 hours) ) def channelParams: LocalParams = Peer.makeChannelParams( @@ -329,7 +330,8 @@ object TestConstants { onionMessageConfig = OnionMessageConfig( relayPolicy = RelayAll, timeout = 1 minute - ) + ), + purgeInvoicesInterval = Some(24 hours) ) def channelParams: LocalParams = Peer.makeChannelParams( diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/receive/InvoicePurgerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/receive/InvoicePurgerSpec.scala new file mode 100644 index 0000000000..e89ffd3139 --- /dev/null +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/receive/InvoicePurgerSpec.scala @@ -0,0 +1,129 @@ +/* + * 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.payment.receive + +import akka.actor.testkit.typed.scaladsl.ScalaTestWithActorTestKit +import akka.actor.typed.eventstream.EventStream +import com.typesafe.config.ConfigFactory +import fr.acinq.bitcoin.Block +import fr.acinq.eclair.TestDatabases.TestSqliteDatabases +import fr.acinq.eclair.db.{IncomingPayment, IncomingPaymentStatus, PaymentType, PaymentsDbSpec} +import fr.acinq.eclair.payment.Bolt11Invoice +import fr.acinq.eclair.payment.receive.InvoicePurger.{PurgeCompleted, PurgeEvent} +import fr.acinq.eclair.{CltvExpiryDelta, MilliSatoshiLong, TimestampMilli, TimestampMilliLong, TimestampSecond, TimestampSecondLong, randomBytes32} +import org.scalatest.funsuite.AnyFunSuiteLike + +import scala.concurrent.duration.DurationInt + +class InvoicePurgerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("application")) with AnyFunSuiteLike { + + import PaymentsDbSpec._ + + test("purge invoices on startup") { + val dbs = TestSqliteDatabases() + val db = dbs.db.payments + val count = 10 + + // create expired invoices + val expiredInvoices = Seq.fill(count)(Bolt11Invoice(Block.TestnetGenesisBlock.hash, Some(100 msat), randomBytes32(), alicePriv, Left("expired invoice"), CltvExpiryDelta(18), + timestamp = 1 unixsec)) + val expiredPayments = expiredInvoices.map(invoice => IncomingPayment(invoice, randomBytes32(), PaymentType.Standard, invoice.createdAt.toTimestampMilli, IncomingPaymentStatus.Expired)) + expiredPayments.foreach(payment => db.addIncomingPayment(payment.invoice, payment.paymentPreimage)) + + // create pending invoices + val pendingInvoices = Seq.fill(count)(Bolt11Invoice(Block.TestnetGenesisBlock.hash, Some(100 msat), randomBytes32(), alicePriv, Left("pending invoice"), CltvExpiryDelta(18))) + val pendingPayments = pendingInvoices.map(invoice => IncomingPayment(invoice, randomBytes32(), PaymentType.Standard, invoice.createdAt.toTimestampMilli, IncomingPaymentStatus.Pending)) + pendingPayments.foreach(payment => db.addIncomingPayment(payment.invoice, payment.paymentPreimage)) + + // create paid invoices + val receivedAt = TimestampMilli.now() + 1.milli + val paidInvoices = Seq.fill(count)(Bolt11Invoice(Block.TestnetGenesisBlock.hash, Some(100 msat), randomBytes32(), alicePriv, Left("paid invoice"), CltvExpiryDelta(18))) + val paidPayments = paidInvoices.map(invoice => IncomingPayment(invoice, randomBytes32(), PaymentType.Standard, invoice.createdAt.toTimestampMilli, IncomingPaymentStatus.Received(100 msat, receivedAt))) + paidPayments.foreach(payment => { + db.addIncomingPayment(payment.invoice, payment.paymentPreimage) + // receive payment + db.receiveIncomingPayment(payment.invoice.paymentHash, 100 msat, receivedAt) + }) + + val now = TimestampMilli.now() + assert(db.listIncomingPayments(0 unixms, now) === expiredPayments ++ pendingPayments ++ paidPayments) + assert(db.listIncomingPayments(now - 100.days, now) === pendingPayments ++ paidPayments) + assert(db.listPendingIncomingPayments(0 unixms, now) === pendingPayments) + assert(db.listReceivedIncomingPayments(0 unixms, now) === paidPayments) + assert(db.listExpiredIncomingPayments(0 unixms, now) === expiredPayments) + + val probe = testKit.createTestProbe[PurgeEvent]() + system.eventStream ! EventStream.Subscribe(probe.ref) + + val purger = testKit.spawn(InvoicePurger(db, 24.hours), name = "purge-expired-invoices") + + // check that purge runs before the default first interval of 24 hours + probe.expectMessage(5 seconds, PurgeCompleted) + probe.expectNoMessage() + assert(db.listExpiredIncomingPayments(0 unixms, now).isEmpty) + assert(db.listIncomingPayments(0 unixms, now) === pendingPayments ++ paidPayments) + + testKit.stop(purger) + } + + test("purge invoices after interval") { + val dbs = TestSqliteDatabases() + val db = dbs.db.payments + val interval = 5 seconds + + // add an expired invoice from before the 15 days look back period + val expiredInvoice1 = Bolt11Invoice(Block.TestnetGenesisBlock.hash, Some(100 msat), randomBytes32(), alicePriv, Left("expired invoice2"), CltvExpiryDelta(18), + timestamp = 5 unixsec) + val expiredPayment1 = IncomingPayment(expiredInvoice1, randomBytes32(), PaymentType.Standard, expiredInvoice1.createdAt.toTimestampMilli, IncomingPaymentStatus.Expired) + db.addIncomingPayment(expiredPayment1.invoice, expiredPayment1.paymentPreimage) + + // add an expired invoice from after the 15 day look back period + val expiredInvoice2 = Bolt11Invoice(Block.TestnetGenesisBlock.hash, Some(100 msat), randomBytes32(), alicePriv, Left("expired invoice2"), CltvExpiryDelta(18), + timestamp = TimestampSecond.now() - 10.days) + val expiredPayment2 = IncomingPayment(expiredInvoice2, randomBytes32(), PaymentType.Standard, expiredInvoice2.createdAt.toTimestampMilli, IncomingPaymentStatus.Expired) + db.addIncomingPayment(expiredPayment2.invoice, expiredPayment2.paymentPreimage) + + val probe = testKit.createTestProbe[PurgeEvent]() + system.eventStream ! EventStream.Subscribe(probe.ref) + + val purger = testKit.spawn(InvoicePurger(db, interval), name = "purge-expired-invoices") + + // check that the initial purge scanned the entire database + probe.expectMessage(10 seconds, PurgeCompleted) + probe.expectNoMessage() + assert(db.listExpiredIncomingPayments(0 unixms, TimestampMilli.now()).isEmpty) + + // add an expired invoice from before the 15 days look back period + val expiredInvoice3 = Bolt11Invoice(Block.TestnetGenesisBlock.hash, Some(100 msat), randomBytes32(), alicePriv, Left("expired invoice3"), CltvExpiryDelta(18), + timestamp = 5 unixsec) + val expiredPayment3 = IncomingPayment(expiredInvoice3, randomBytes32(), PaymentType.Standard, expiredInvoice3.createdAt.toTimestampMilli, IncomingPaymentStatus.Expired) + db.addIncomingPayment(expiredPayment3.invoice, expiredPayment3.paymentPreimage) + + // add another expired invoice from after the 15 day look back period + val expiredInvoice4 = Bolt11Invoice(Block.TestnetGenesisBlock.hash, Some(100 msat), randomBytes32(), alicePriv, Left("expired invoice4"), CltvExpiryDelta(18), + timestamp = TimestampSecond.now() - 10.days) + val expiredPayment4 = IncomingPayment(expiredInvoice4, randomBytes32(), PaymentType.Standard, expiredInvoice4.createdAt.toTimestampMilli, IncomingPaymentStatus.Expired) + db.addIncomingPayment(expiredPayment4.invoice, expiredPayment4.paymentPreimage) + + // check that subsequent purge runs do not go back > 15 days + probe.expectMessage(10 seconds, PurgeCompleted) + probe.expectNoMessage() + assert(db.listExpiredIncomingPayments(0 unixms, TimestampMilli.now()) === Seq(expiredPayment3)) + + testKit.stop(purger) + } +} From 8f7c6ed68e5a73043205b9f6582bf929eeaa3c43 Mon Sep 17 00:00:00 2001 From: Pierre-Marie Padiou Date: Tue, 1 Mar 2022 13:27:17 +0100 Subject: [PATCH 026/121] Refactor `Closing.claimRemoteCommitMainOutput()` (#2191) Building an inconsistent intermediate `RemoteCommitPublished` object was error-prone and defeats the purpose of strong typing. We also handle the `paysDirectlyToWallet` case directly in the method, which enables a nice factorization and better contains this now deprecated feature. --- .../fr/acinq/eclair/channel/Channel.scala | 20 +++-- .../fr/acinq/eclair/channel/Helpers.scala | 74 ++++++++----------- 2 files changed, 41 insertions(+), 53 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala index 80f350e17d..11ca879dd9 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala @@ -2526,17 +2526,15 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, remo private def handleRemoteSpentFuture(commitTx: Transaction, d: DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT) = { log.warning(s"they published their future commit (because we asked them to) in txid=${commitTx.txid}") context.system.eventStream.publish(TransactionPublished(d.channelId, remoteNodeId, commitTx, Closing.commitTxFee(d.commitments.commitInput, commitTx, d.commitments.localParams.isFunder), "future-remote-commit")) - d.commitments.channelFeatures match { - case ct if ct.paysDirectlyToWallet => - val remoteCommitPublished = RemoteCommitPublished(commitTx, None, Map.empty, List.empty, Map.empty) - val nextData = DATA_CLOSING(d.commitments, fundingTx = None, waitingSince = nodeParams.currentBlockHeight, Nil, futureRemoteCommitPublished = Some(remoteCommitPublished)) - goto(CLOSING) using nextData storing() // we don't need to claim our main output in the remote commit because it already spends to our wallet address - case _ => - val remotePerCommitmentPoint = d.remoteChannelReestablish.myCurrentPerCommitmentPoint - val remoteCommitPublished = Helpers.Closing.claimRemoteCommitMainOutput(keyManager, d.commitments, remotePerCommitmentPoint, commitTx, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) - val nextData = DATA_CLOSING(d.commitments, fundingTx = None, waitingSince = nodeParams.currentBlockHeight, Nil, futureRemoteCommitPublished = Some(remoteCommitPublished)) - goto(CLOSING) using nextData storing() calling doPublish(remoteCommitPublished, d.commitments) - } + val remotePerCommitmentPoint = d.remoteChannelReestablish.myCurrentPerCommitmentPoint + val remoteCommitPublished = RemoteCommitPublished( + commitTx = commitTx, + claimMainOutputTx = Helpers.Closing.claimRemoteCommitMainOutput(keyManager, d.commitments, remotePerCommitmentPoint, commitTx, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets), + claimHtlcTxs = Map.empty, + claimAnchorTxs = List.empty, + irrevocablySpent = Map.empty) + val nextData = DATA_CLOSING(d.commitments, fundingTx = None, waitingSince = nodeParams.currentBlockHeight, Nil, futureRemoteCommitPublished = Some(remoteCommitPublished)) + goto(CLOSING) using nextData storing() calling doPublish(remoteCommitPublished, d.commitments) } private def handleRemoteSpentNext(commitTx: Transaction, d: HasCommitments) = { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala index c1c66eab05..1fef6b68ff 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala @@ -793,60 +793,50 @@ object Helpers { } ).flatten - if (commitments.channelFeatures.paysDirectlyToWallet) { - RemoteCommitPublished( - commitTx = tx, - claimMainOutputTx = None, - claimHtlcTxs = htlcTxs, - claimAnchorTxs = claimAnchorTxs, - irrevocablySpent = Map.empty - ) - } else { - claimRemoteCommitMainOutput(keyManager, commitments, remoteCommit.remotePerCommitmentPoint, tx, feeEstimator, feeTargets).copy( - claimHtlcTxs = htlcTxs, - claimAnchorTxs = claimAnchorTxs, - ) - } + RemoteCommitPublished( + commitTx = tx, + claimMainOutputTx = claimRemoteCommitMainOutput(keyManager, commitments, remoteCommit.remotePerCommitmentPoint, tx, feeEstimator, feeTargets), + claimHtlcTxs = htlcTxs, + claimAnchorTxs = claimAnchorTxs, + irrevocablySpent = Map.empty + ) } /** - * Claim our main output only (not necessary if option_static_remotekey was negotiated). + * Claim our main output only * * @param commitments either our current commitment data in case of usual remote uncooperative closing * or our outdated commitment data in case of data loss protection procedure; in any case it is used only * to get some constant parameters, not commitment data * @param remotePerCommitmentPoint the remote perCommitmentPoint corresponding to this commitment * @param tx the remote commitment transaction that has just been published - * @return a transaction claiming our main output + * @return an optional [[ClaimRemoteCommitMainOutputTx]] transaction claiming our main output */ - def claimRemoteCommitMainOutput(keyManager: ChannelKeyManager, commitments: Commitments, remotePerCommitmentPoint: PublicKey, tx: Transaction, feeEstimator: FeeEstimator, feeTargets: FeeTargets)(implicit log: LoggingAdapter): RemoteCommitPublished = { - val channelKeyPath = keyManager.keyPath(commitments.localParams, commitments.channelConfig) - val localPubkey = Generators.derivePubKey(keyManager.paymentPoint(channelKeyPath).publicKey, remotePerCommitmentPoint) - val localPaymentPoint = keyManager.paymentPoint(channelKeyPath).publicKey - val feeratePerKwMain = feeEstimator.getFeeratePerKw(feeTargets.claimMainBlockTarget) - - val mainTx = commitments.commitmentFormat match { - case DefaultCommitmentFormat => withTxGenerationLog("remote-main") { - Transactions.makeClaimP2WPKHOutputTx(tx, commitments.localParams.dustLimit, localPubkey, commitments.localParams.defaultFinalScriptPubKey, feeratePerKwMain).map(claimMain => { - val sig = keyManager.sign(claimMain, keyManager.paymentPoint(channelKeyPath), remotePerCommitmentPoint, TxOwner.Local, commitments.commitmentFormat) - Transactions.addSigs(claimMain, localPubkey, sig) - }) - } - case _: AnchorOutputsCommitmentFormat => withTxGenerationLog("remote-main-delayed") { - Transactions.makeClaimRemoteDelayedOutputTx(tx, commitments.localParams.dustLimit, localPaymentPoint, commitments.localParams.defaultFinalScriptPubKey, feeratePerKwMain).map(claimMain => { - val sig = keyManager.sign(claimMain, keyManager.paymentPoint(channelKeyPath), TxOwner.Local, commitments.commitmentFormat) - Transactions.addSigs(claimMain, sig) - }) + def claimRemoteCommitMainOutput(keyManager: ChannelKeyManager, commitments: Commitments, remotePerCommitmentPoint: PublicKey, tx: Transaction, feeEstimator: FeeEstimator, feeTargets: FeeTargets)(implicit log: LoggingAdapter): Option[ClaimRemoteCommitMainOutputTx] = { + if (commitments.channelFeatures.paysDirectlyToWallet) { + // the commitment tx sends funds directly to our wallet, no claim tx needed + None + } else { + val channelKeyPath = keyManager.keyPath(commitments.localParams, commitments.channelConfig) + val localPubkey = Generators.derivePubKey(keyManager.paymentPoint(channelKeyPath).publicKey, remotePerCommitmentPoint) + val localPaymentPoint = keyManager.paymentPoint(channelKeyPath).publicKey + val feeratePerKwMain = feeEstimator.getFeeratePerKw(feeTargets.claimMainBlockTarget) + + commitments.commitmentFormat match { + case DefaultCommitmentFormat => withTxGenerationLog("remote-main") { + Transactions.makeClaimP2WPKHOutputTx(tx, commitments.localParams.dustLimit, localPubkey, commitments.localParams.defaultFinalScriptPubKey, feeratePerKwMain).map(claimMain => { + val sig = keyManager.sign(claimMain, keyManager.paymentPoint(channelKeyPath), remotePerCommitmentPoint, TxOwner.Local, commitments.commitmentFormat) + Transactions.addSigs(claimMain, localPubkey, sig) + }) + } + case _: AnchorOutputsCommitmentFormat => withTxGenerationLog("remote-main-delayed") { + Transactions.makeClaimRemoteDelayedOutputTx(tx, commitments.localParams.dustLimit, localPaymentPoint, commitments.localParams.defaultFinalScriptPubKey, feeratePerKwMain).map(claimMain => { + val sig = keyManager.sign(claimMain, keyManager.paymentPoint(channelKeyPath), TxOwner.Local, commitments.commitmentFormat) + Transactions.addSigs(claimMain, sig) + }) + } } } - - RemoteCommitPublished( - commitTx = tx, - claimMainOutputTx = mainTx, - claimHtlcTxs = Map.empty, - claimAnchorTxs = Nil, - irrevocablySpent = Map.empty - ) } /** From 67fb392a28617214c8298da2f1f92071f25b0233 Mon Sep 17 00:00:00 2001 From: Thomas HUET <81159533+thomash-acinq@users.noreply.github.com> Date: Tue, 1 Mar 2022 16:27:22 +0100 Subject: [PATCH 027/121] Use direct channel when available (#2192) Because of the virtual cost per hop, a path with several "good" channels may be considered better than using a direct channel. We remove the virtual cost for the first hop of the path to ensure that a direct channel is always selected when available. --- .../scala/fr/acinq/eclair/router/Graph.scala | 2 +- .../eclair/router/RouteCalculationSpec.scala | 23 +++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/Graph.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/Graph.scala index 20fef16f3c..70ac131143 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/Graph.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/Graph.scala @@ -306,7 +306,7 @@ object Graph { val totalCltv = prev.cltv + cltv weightRatios match { case Left(weightRatios) => - val hopCost = nodeFee(weightRatios.hopCost, prev.amount) + val hopCost = if (edge.desc.a == sender) 0 msat else nodeFee(weightRatios.hopCost, prev.amount) import RoutingHeuristics._ // Every edge is weighted by funding block height where older blocks add less weight. The window considered is 1 year. diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/RouteCalculationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/RouteCalculationSpec.scala index 123d7a0cd4..cf88cbc1e4 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/RouteCalculationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/RouteCalculationSpec.scala @@ -1847,6 +1847,29 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { val route :: Nil = routes assert(route2Ids(route) === 1 :: 2 :: Nil) } + + test("use direct channel when available") { + // A ===> B ===> C + // \___________/ + val recentChannelId = ShortChannelId("399990x1x2").toLong + val g = DirectedGraph(List( + makeEdge(1L, a, b, 1 msat, 1, capacity = 100_000_000 sat), + makeEdge(2L, b, c, 1 msat, 1, capacity = 100_000_000 sat), + makeEdge(recentChannelId, a, c, 1000 msat, 100), + )) + + val wr = WeightRatios( + baseFactor = 0, + cltvDeltaFactor = 0, + ageFactor = 0.5, + capacityFactor = 0.5, + hopCost = RelayFees(500 msat, 200), + ) + val Success(routes) = findRoute(g, a, c, DEFAULT_AMOUNT_MSAT, 100_000_000 msat, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.copy(heuristics = Left(wr)), currentBlockHeight = BlockHeight(400000)) + assert(routes.distinct.length == 1) + val route :: Nil = routes + assert(route2Ids(route) === recentChannelId :: Nil) + } } object RouteCalculationSpec { From ab30af8fbc09feb2b9b0af4337f4d9486727a0b6 Mon Sep 17 00:00:00 2001 From: Thomas HUET <81159533+thomash-acinq@users.noreply.github.com> Date: Wed, 2 Mar 2022 13:54:46 +0100 Subject: [PATCH 028/121] minFinalExpiryDelta is not optional (#2195) An invoice that doesn't explicitly set min_final_cltv_expiry has an default min_final_cltv_expiry of 18. This default allows invoices to be slightly shorter but does not mean that the min_final_cltv_expiry can be unknown and needs to be provided from somewhere else. --- .../main/scala/fr/acinq/eclair/Eclair.scala | 11 +++---- .../fr/acinq/eclair/channel/Channel.scala | 5 ++-- .../acinq/eclair/json/JsonSerializers.scala | 8 +++-- .../acinq/eclair/payment/Bolt11Invoice.scala | 15 +++------- .../fr/acinq/eclair/payment/Invoice.scala | 2 +- .../payment/receive/MultiPartHandler.scala | 2 +- .../payment/send/PaymentInitiator.scala | 9 +----- .../fr/acinq/eclair/EclairImplSpec.scala | 5 ++-- .../integration/ChannelIntegrationSpec.scala | 4 +-- .../integration/PaymentIntegrationSpec.scala | 16 +++++----- .../PerformanceIntegrationSpec.scala | 2 +- .../eclair/payment/Bolt11InvoiceSpec.scala | 6 ++-- .../eclair/payment/MultiPartHandlerSpec.scala | 4 +-- .../eclair/payment/PaymentInitiatorSpec.scala | 30 +++++++++---------- .../acinq/eclair/api/handlers/Payment.scala | 6 ++-- .../fr/acinq/eclair/api/ApiServiceSpec.scala | 8 ++--- 16 files changed, 58 insertions(+), 75 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala index 6f98a0a5ec..afdd80e517 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala @@ -125,7 +125,7 @@ trait Eclair { def findRouteBetween(sourceNodeId: PublicKey, targetNodeId: PublicKey, amount: MilliSatoshi, pathFindingExperimentName_opt: Option[String], assistedRoutes: Seq[Seq[Bolt11Invoice.ExtraHop]] = Seq.empty, includeLocalChannelCost: Boolean = false, ignoreNodeIds: Seq[PublicKey] = Seq.empty, ignoreShortChannelIds: Seq[ShortChannelId] = Seq.empty, maxFee_opt: Option[MilliSatoshi] = None)(implicit timeout: Timeout): Future[RouteResponse] - def sendToRoute(amount: MilliSatoshi, recipientAmount_opt: Option[MilliSatoshi], externalId_opt: Option[String], parentId_opt: Option[UUID], invoice: Bolt11Invoice, finalCltvExpiryDelta: CltvExpiryDelta, route: PredefinedRoute, trampolineSecret_opt: Option[ByteVector32] = None, trampolineFees_opt: Option[MilliSatoshi] = None, trampolineExpiryDelta_opt: Option[CltvExpiryDelta] = None, trampolineNodes_opt: Seq[PublicKey] = Nil)(implicit timeout: Timeout): Future[SendPaymentToRouteResponse] + def sendToRoute(amount: MilliSatoshi, recipientAmount_opt: Option[MilliSatoshi], externalId_opt: Option[String], parentId_opt: Option[UUID], invoice: Bolt11Invoice, route: PredefinedRoute, trampolineSecret_opt: Option[ByteVector32] = None, trampolineFees_opt: Option[MilliSatoshi] = None, trampolineExpiryDelta_opt: Option[CltvExpiryDelta] = None, trampolineNodes_opt: Seq[PublicKey] = Nil)(implicit timeout: Timeout): Future[SendPaymentToRouteResponse] def audit(from: TimestampSecond, to: TimestampSecond)(implicit timeout: Timeout): Future[AuditResponse] @@ -309,9 +309,9 @@ class EclairImpl(appKit: Kit) extends Eclair with Logging { } } - override def sendToRoute(amount: MilliSatoshi, recipientAmount_opt: Option[MilliSatoshi], externalId_opt: Option[String], parentId_opt: Option[UUID], invoice: Bolt11Invoice, finalCltvExpiryDelta: CltvExpiryDelta, route: PredefinedRoute, trampolineSecret_opt: Option[ByteVector32], trampolineFees_opt: Option[MilliSatoshi], trampolineExpiryDelta_opt: Option[CltvExpiryDelta], trampolineNodes_opt: Seq[PublicKey])(implicit timeout: Timeout): Future[SendPaymentToRouteResponse] = { + override def sendToRoute(amount: MilliSatoshi, recipientAmount_opt: Option[MilliSatoshi], externalId_opt: Option[String], parentId_opt: Option[UUID], invoice: Bolt11Invoice, route: PredefinedRoute, trampolineSecret_opt: Option[ByteVector32], trampolineFees_opt: Option[MilliSatoshi], trampolineExpiryDelta_opt: Option[CltvExpiryDelta], trampolineNodes_opt: Seq[PublicKey])(implicit timeout: Timeout): Future[SendPaymentToRouteResponse] = { val recipientAmount = recipientAmount_opt.getOrElse(invoice.amount_opt.getOrElse(amount)) - val sendPayment = SendPaymentToRoute(amount, recipientAmount, invoice, finalCltvExpiryDelta, route, externalId_opt, parentId_opt, trampolineSecret_opt, trampolineFees_opt.getOrElse(0 msat), trampolineExpiryDelta_opt.getOrElse(CltvExpiryDelta(0)), trampolineNodes_opt) + val sendPayment = SendPaymentToRoute(amount, recipientAmount, invoice, route, externalId_opt, parentId_opt, trampolineSecret_opt, trampolineFees_opt.getOrElse(0 msat), trampolineExpiryDelta_opt.getOrElse(CltvExpiryDelta(0)), trampolineNodes_opt) if (invoice.isExpired()) { Future.failed(new IllegalArgumentException("invoice has expired")) } else if (route.isEmpty) { @@ -337,10 +337,7 @@ class EclairImpl(appKit: Kit) extends Eclair with Logging { externalId_opt match { case Some(externalId) if externalId.length > externalIdMaxLength => Left(new IllegalArgumentException(s"externalId is too long: cannot exceed $externalIdMaxLength characters")) case _ if invoice.isExpired() => Left(new IllegalArgumentException("invoice has expired")) - case _ => invoice.minFinalCltvExpiryDelta match { - case Some(minFinalCltvExpiryDelta) => Right(SendPaymentToNode(amount, invoice, maxAttempts, minFinalCltvExpiryDelta, externalId_opt, assistedRoutes = invoice.routingInfo, routeParams = routeParams)) - case None => Right(SendPaymentToNode(amount, invoice, maxAttempts, externalId = externalId_opt, assistedRoutes = invoice.routingInfo, routeParams = routeParams)) - } + case _ => Right(SendPaymentToNode(amount, invoice, maxAttempts, externalId_opt, assistedRoutes = invoice.routingInfo, routeParams = routeParams)) } case Left(t) => Left(t) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala index 11ca879dd9..0356de161b 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala @@ -43,7 +43,7 @@ import fr.acinq.eclair.crypto.keymanager.ChannelKeyManager import fr.acinq.eclair.db.DbEventHandler.ChannelEvent.EventType import fr.acinq.eclair.db.PendingCommandsDb import fr.acinq.eclair.io.Peer -import fr.acinq.eclair.payment.PaymentSettlingOnChain +import fr.acinq.eclair.payment.{Bolt11Invoice, PaymentSettlingOnChain} import fr.acinq.eclair.payment.relay.Relayer import fr.acinq.eclair.router.Announcements import fr.acinq.eclair.transactions.Transactions.{ClosingTx, TxOwner} @@ -114,8 +114,7 @@ object Channel { // we won't exchange more than this many signatures when negotiating the closing fee val MAX_NEGOTIATION_ITERATIONS = 20 - // this is defined in BOLT 11 - val MIN_CLTV_EXPIRY_DELTA: CltvExpiryDelta = CltvExpiryDelta(18) + val MIN_CLTV_EXPIRY_DELTA: CltvExpiryDelta = Bolt11Invoice.DEFAULT_MIN_CLTV_EXPIRY_DELTA val MAX_CLTV_EXPIRY_DELTA: CltvExpiryDelta = CltvExpiryDelta(7 * 144) // one week // since BOLT 1.1, there is a max value for the refund delay of the main commitment tx diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/json/JsonSerializers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/json/JsonSerializers.scala index 4c3479bd2e..a563c05d7b 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/json/JsonSerializers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/json/JsonSerializers.scala @@ -341,8 +341,12 @@ object DirectedHtlcSerializer extends ConvertClassSerializer[DirectedHtlc](h => object InvoiceSerializer extends MinimalSerializer({ case p: Bolt11Invoice => - val expiry = p.relativeExpiry_opt.map(ex => JField("expiry", JLong(ex))).toSeq - val minFinalCltvExpiry = p.minFinalCltvExpiryDelta.map(mfce => JField("minFinalCltvExpiry", JInt(mfce.toInt))).toSeq + val expiry = p.tags + .collectFirst { case expiry: Bolt11Invoice.Expiry => expiry.toLong } // NB: we look at fields directly because the value has a spec-defined default + .map(ex => JField("expiry", JLong(ex))).toSeq + val minFinalCltvExpiry = p.tags + .collectFirst { case cltvExpiry: Bolt11Invoice.MinFinalCltvExpiry => cltvExpiry.toCltvExpiryDelta } // NB: we look at fields directly because the value has a spec-defined default + .map(mfce => JField("minFinalCltvExpiry", JInt(mfce.toInt))).toSeq val amount = p.amount_opt.map(msat => JField("amount", JLong(msat.toLong))).toSeq val features = JField("features", Extraction.decompose(p.features)( DefaultFormats + diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Bolt11Invoice.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Bolt11Invoice.scala index cfebfbf069..30fa05b615 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Bolt11Invoice.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Bolt11Invoice.scala @@ -77,21 +77,13 @@ case class Bolt11Invoice(prefix: String, amount_opt: Option[MilliSatoshi], creat /** * @return the fallback address if any. It could be a script address, pubkey address, .. */ - def fallbackAddress(): Option[String] = tags.collectFirst { - case f: Bolt11Invoice.FallbackAddress => Bolt11Invoice.FallbackAddress.toAddress(f, prefix) - } + def fallbackAddress(): Option[String] = tags.collectFirst { case f: Bolt11Invoice.FallbackAddress => Bolt11Invoice.FallbackAddress.toAddress(f, prefix) } lazy val routingInfo: Seq[Seq[ExtraHop]] = tags.collect { case t: RoutingInfo => t.path } - lazy val relativeExpiry_opt: Option[Long] = tags.collectFirst { - case expiry: Bolt11Invoice.Expiry => expiry.toLong - } - - lazy val relativeExpiry: FiniteDuration = FiniteDuration(relativeExpiry_opt.getOrElse(DEFAULT_EXPIRY_SECONDS), TimeUnit.SECONDS) + lazy val relativeExpiry: FiniteDuration = FiniteDuration(tags.collectFirst { case expiry: Bolt11Invoice.Expiry => expiry.toLong }.getOrElse(DEFAULT_EXPIRY_SECONDS), TimeUnit.SECONDS) - lazy val minFinalCltvExpiryDelta: Option[CltvExpiryDelta] = tags.collectFirst { - case cltvExpiry: Bolt11Invoice.MinFinalCltvExpiry => cltvExpiry.toCltvExpiryDelta - } + lazy val minFinalCltvExpiryDelta: CltvExpiryDelta = tags.collectFirst { case cltvExpiry: Bolt11Invoice.MinFinalCltvExpiry => cltvExpiry.toCltvExpiryDelta }.getOrElse(DEFAULT_MIN_CLTV_EXPIRY_DELTA) lazy val features: Features[InvoiceFeature] = tags.collectFirst { case f: InvoiceFeatures => f.features.invoiceFeatures() }.getOrElse(Features.empty[InvoiceFeature]) @@ -134,6 +126,7 @@ case class Bolt11Invoice(prefix: String, amount_opt: Option[MilliSatoshi], creat object Bolt11Invoice { val DEFAULT_EXPIRY_SECONDS: Long = 3600 + val DEFAULT_MIN_CLTV_EXPIRY_DELTA: CltvExpiryDelta = CltvExpiryDelta(18) val prefixes = Map( Block.RegtestGenesisBlock.hash -> "lnbcrt", diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Invoice.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Invoice.scala index c721b57d7c..bd2ebdfbe5 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Invoice.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Invoice.scala @@ -42,7 +42,7 @@ trait Invoice { val relativeExpiry: FiniteDuration - val minFinalCltvExpiryDelta: Option[CltvExpiryDelta] + val minFinalCltvExpiryDelta: CltvExpiryDelta val features: Features[InvoiceFeature] diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/MultiPartHandler.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/MultiPartHandler.scala index 8aa23167f4..9d699ed335 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/MultiPartHandler.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/MultiPartHandler.scala @@ -306,7 +306,7 @@ object MultiPartHandler { } private def validatePaymentCltv(nodeParams: NodeParams, payment: IncomingPaymentPacket.FinalPacket, record: IncomingPayment)(implicit log: LoggingAdapter): Boolean = { - val minExpiry = record.invoice.minFinalCltvExpiryDelta.getOrElse(nodeParams.channelConf.minFinalExpiryDelta).toCltvExpiry(nodeParams.currentBlockHeight) + val minExpiry = record.invoice.minFinalCltvExpiryDelta.toCltvExpiry(nodeParams.currentBlockHeight) if (payment.add.cltvExpiry < minExpiry) { log.warning("received payment with expiry too small for amount={} totalAmount={}", payment.add.amountMsat, payment.payload.totalAmount) false diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentInitiator.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentInitiator.scala index b5d273d9c0..cb84ba7cbf 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentInitiator.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentInitiator.scala @@ -272,9 +272,8 @@ object PaymentInitiator { def invoice: Invoice def recipientNodeId: PublicKey = invoice.nodeId def paymentHash: ByteVector32 = invoice.paymentHash - def fallbackFinalExpiryDelta: CltvExpiryDelta // We add one block in order to not have our htlcs fail when a new block has just been found. - def finalExpiry(currentBlockHeight: BlockHeight): CltvExpiry = invoice.minFinalCltvExpiryDelta.getOrElse(fallbackFinalExpiryDelta).toCltvExpiry(currentBlockHeight + 1) + def finalExpiry(currentBlockHeight: BlockHeight): CltvExpiry = invoice.minFinalCltvExpiryDelta.toCltvExpiry(currentBlockHeight + 1) // @formatter:on } @@ -290,21 +289,18 @@ object PaymentInitiator { * the payment will automatically be retried in case of TrampolineFeeInsufficient errors. * For example, [(10 msat, 144), (15 msat, 288)] will first send a payment with a fee of 10 * msat and cltv of 144, and retry with 15 msat and 288 in case an error occurs. - * @param fallbackFinalExpiryDelta expiry delta for the final recipient when the [[invoice]] doesn't specify it. * @param routeParams (optional) parameters to fine-tune the routing algorithm. */ case class SendTrampolinePayment(recipientAmount: MilliSatoshi, invoice: Invoice, trampolineNodeId: PublicKey, trampolineAttempts: Seq[(MilliSatoshi, CltvExpiryDelta)], - fallbackFinalExpiryDelta: CltvExpiryDelta = Channel.MIN_CLTV_EXPIRY_DELTA, routeParams: RouteParams) extends SendRequestedPayment /** * @param recipientAmount amount that should be received by the final recipient (usually from a Bolt 11 invoice). * @param invoice Bolt 11 invoice. * @param maxAttempts maximum number of retries. - * @param fallbackFinalExpiryDelta expiry delta for the final recipient when the [[invoice]] doesn't specify it. * @param externalId (optional) externally-controlled identifier (to reconcile between application DB and eclair DB). * @param assistedRoutes (optional) routing hints (usually from a Bolt 11 invoice). * @param routeParams (optional) parameters to fine-tune the routing algorithm. @@ -314,7 +310,6 @@ object PaymentInitiator { case class SendPaymentToNode(recipientAmount: MilliSatoshi, invoice: Invoice, maxAttempts: Int, - fallbackFinalExpiryDelta: CltvExpiryDelta = Channel.MIN_CLTV_EXPIRY_DELTA, externalId: Option[String] = None, assistedRoutes: Seq[Seq[ExtraHop]] = Nil, routeParams: RouteParams, @@ -360,7 +355,6 @@ object PaymentInitiator { * @param recipientAmount amount that should be received by the final recipient (usually from a Bolt 11 invoice). * This amount may be split between multiple requests if using MPP. * @param invoice Bolt 11 invoice. - * @param fallbackFinalExpiryDelta expiry delta for the final recipient when the [[invoice]] doesn't specify it. * @param route route to use to reach either the final recipient or the first trampoline node. * @param externalId (optional) externally-controlled identifier (to reconcile between application DB and eclair DB). * @param parentId id of the whole payment. When manually sending a multi-part payment, you need to make @@ -379,7 +373,6 @@ object PaymentInitiator { case class SendPaymentToRoute(amount: MilliSatoshi, recipientAmount: MilliSatoshi, invoice: Invoice, - fallbackFinalExpiryDelta: CltvExpiryDelta = Channel.MIN_CLTV_EXPIRY_DELTA, route: PredefinedRoute, externalId: Option[String], parentId: Option[UUID], diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/EclairImplSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/EclairImplSpec.scala index 4384214a94..bdd2efe22b 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/EclairImplSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/EclairImplSpec.scala @@ -145,7 +145,6 @@ class EclairImplSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with I assert(send2.recipientAmount === 123.msat) assert(send2.paymentHash === ByteVector32.Zeroes) assert(send2.invoice === invoice2) - assert(send2.fallbackFinalExpiryDelta === CltvExpiryDelta(96)) // with custom route fees parameters eclair.send(None, 123 msat, invoice0, maxFeeFlat_opt = Some(123 sat), maxFeePct_opt = Some(4.20)) @@ -309,8 +308,8 @@ class EclairImplSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with I val parentId = UUID.randomUUID() val secret = randomBytes32() val pr = Bolt11Invoice(Block.LivenetGenesisBlock.hash, Some(1234 msat), ByteVector32.One, randomKey(), Right(randomBytes32()), CltvExpiryDelta(18)) - eclair.sendToRoute(1000 msat, Some(1200 msat), Some("42"), Some(parentId), pr, CltvExpiryDelta(123), route, Some(secret), Some(100 msat), Some(CltvExpiryDelta(144)), trampolines) - paymentInitiator.expectMsg(SendPaymentToRoute(1000 msat, 1200 msat, pr, CltvExpiryDelta(123), route, Some("42"), Some(parentId), Some(secret), 100 msat, CltvExpiryDelta(144), trampolines)) + eclair.sendToRoute(1000 msat, Some(1200 msat), Some("42"), Some(parentId), pr, route, Some(secret), Some(100 msat), Some(CltvExpiryDelta(144)), trampolines) + paymentInitiator.expectMsg(SendPaymentToRoute(1000 msat, 1200 msat, pr, route, Some("42"), Some(parentId), Some(secret), 100 msat, CltvExpiryDelta(144), trampolines)) } test("call sendWithPreimage, which generates a random preimage, to perform a KeySend payment") { f => diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/ChannelIntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/ChannelIntegrationSpec.scala index ae970d342f..22c50677e5 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/ChannelIntegrationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/ChannelIntegrationSpec.scala @@ -368,7 +368,7 @@ abstract class ChannelIntegrationSpec extends IntegrationSpec { def send(amountMsat: MilliSatoshi, paymentHandler: ActorRef, paymentInitiator: ActorRef): UUID = { sender.send(paymentHandler, ReceivePayment(Some(amountMsat), Left("1 coffee"))) val invoice = sender.expectMsgType[Invoice] - val sendReq = SendPaymentToNode(amountMsat, invoice, maxAttempts = 1, fallbackFinalExpiryDelta = finalCltvExpiryDelta, routeParams = integrationTestRouteParams) + val sendReq = SendPaymentToNode(amountMsat, invoice, maxAttempts = 1, routeParams = integrationTestRouteParams) sender.send(paymentInitiator, sendReq) sender.expectMsgType[UUID] } @@ -684,7 +684,7 @@ abstract class AnchorChannelIntegrationSpec extends ChannelIntegrationSpec { val invoice = sender.expectMsgType[Invoice] // then we make the actual payment - sender.send(nodes("C").paymentInitiator, SendPaymentToNode(amountMsat, invoice, maxAttempts = 1, fallbackFinalExpiryDelta = finalCltvExpiryDelta, routeParams = integrationTestRouteParams)) + sender.send(nodes("C").paymentInitiator, SendPaymentToNode(amountMsat, invoice, maxAttempts = 1, routeParams = integrationTestRouteParams)) val paymentId = sender.expectMsgType[UUID] val ps = sender.expectMsgType[PaymentSent](60 seconds) assert(ps.id == paymentId) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/PaymentIntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/PaymentIntegrationSpec.scala index 4dff2e291b..c351c4153e 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/PaymentIntegrationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/PaymentIntegrationSpec.scala @@ -163,7 +163,7 @@ class PaymentIntegrationSpec extends IntegrationSpec { assert(invoice.paymentMetadata.nonEmpty) // then we make the actual payment - sender.send(nodes("A").paymentInitiator, SendPaymentToNode(amountMsat, invoice, fallbackFinalExpiryDelta = finalCltvExpiryDelta, routeParams = integrationTestRouteParams, maxAttempts = 1)) + sender.send(nodes("A").paymentInitiator, SendPaymentToNode(amountMsat, invoice, routeParams = integrationTestRouteParams, maxAttempts = 1)) val paymentId = sender.expectMsgType[UUID] val ps = sender.expectMsgType[PaymentSent] assert(ps.id == paymentId) @@ -189,7 +189,7 @@ class PaymentIntegrationSpec extends IntegrationSpec { sender.send(nodes("D").paymentHandler, ReceivePayment(Some(amountMsat), Left("1 coffee"))) val invoice = sender.expectMsgType[Invoice] // then we make the actual payment, do not randomize the route to make sure we route through node B - val sendReq = SendPaymentToNode(amountMsat, invoice, fallbackFinalExpiryDelta = finalCltvExpiryDelta, routeParams = integrationTestRouteParams, maxAttempts = 5) + val sendReq = SendPaymentToNode(amountMsat, invoice, routeParams = integrationTestRouteParams, maxAttempts = 5) sender.send(nodes("A").paymentInitiator, sendReq) // A will receive an error from B that include the updated channel update, then will retry the payment val paymentId = sender.expectMsgType[UUID] @@ -230,7 +230,7 @@ class PaymentIntegrationSpec extends IntegrationSpec { sender.send(nodes("D").paymentHandler, ReceivePayment(Some(amountMsat), Left("1 coffee"))) val invoice = sender.expectMsgType[Invoice] // then we make the payment (B-C has a smaller capacity than A-B and C-D) - val sendReq = SendPaymentToNode(amountMsat, invoice, fallbackFinalExpiryDelta = finalCltvExpiryDelta, routeParams = integrationTestRouteParams, maxAttempts = 5) + val sendReq = SendPaymentToNode(amountMsat, invoice, routeParams = integrationTestRouteParams, maxAttempts = 5) sender.send(nodes("A").paymentInitiator, sendReq) // A will first receive an error from C, then retry and route around C: A->B->E->C->D sender.expectMsgType[UUID] @@ -261,7 +261,7 @@ class PaymentIntegrationSpec extends IntegrationSpec { val invoice = sender.expectMsgType[Invoice] // A send payment of only 1 mBTC - val sendReq = SendPaymentToNode(100000000 msat, invoice, fallbackFinalExpiryDelta = finalCltvExpiryDelta, routeParams = integrationTestRouteParams, maxAttempts = 5) + val sendReq = SendPaymentToNode(100000000 msat, invoice, routeParams = integrationTestRouteParams, maxAttempts = 5) sender.send(nodes("A").paymentInitiator, sendReq) // A will first receive an IncorrectPaymentAmount error from D @@ -281,7 +281,7 @@ class PaymentIntegrationSpec extends IntegrationSpec { val invoice = sender.expectMsgType[Invoice] // A send payment of 6 mBTC - val sendReq = SendPaymentToNode(600000000 msat, invoice, fallbackFinalExpiryDelta = finalCltvExpiryDelta, routeParams = integrationTestRouteParams, maxAttempts = 5) + val sendReq = SendPaymentToNode(600000000 msat, invoice, routeParams = integrationTestRouteParams, maxAttempts = 5) sender.send(nodes("A").paymentInitiator, sendReq) // A will first receive an IncorrectPaymentAmount error from D @@ -301,7 +301,7 @@ class PaymentIntegrationSpec extends IntegrationSpec { val invoice = sender.expectMsgType[Invoice] // A send payment of 3 mBTC, more than asked but it should still be accepted - val sendReq = SendPaymentToNode(300000000 msat, invoice, fallbackFinalExpiryDelta = finalCltvExpiryDelta, routeParams = integrationTestRouteParams, maxAttempts = 5) + val sendReq = SendPaymentToNode(300000000 msat, invoice, routeParams = integrationTestRouteParams, maxAttempts = 5) sender.send(nodes("A").paymentInitiator, sendReq) sender.expectMsgType[UUID] } @@ -314,7 +314,7 @@ class PaymentIntegrationSpec extends IntegrationSpec { sender.send(nodes("D").paymentHandler, ReceivePayment(Some(amountMsat), Left("1 payment"))) val invoice = sender.expectMsgType[Invoice] - val sendReq = SendPaymentToNode(amountMsat, invoice, fallbackFinalExpiryDelta = finalCltvExpiryDelta, routeParams = integrationTestRouteParams, maxAttempts = 5) + val sendReq = SendPaymentToNode(amountMsat, invoice, routeParams = integrationTestRouteParams, maxAttempts = 5) sender.send(nodes("A").paymentInitiator, sendReq) sender.expectMsgType[UUID] sender.expectMsgType[PaymentSent] // the payment FSM will also reply to the sender after the payment is completed @@ -329,7 +329,7 @@ class PaymentIntegrationSpec extends IntegrationSpec { val invoice = sender.expectMsgType[Invoice] // the payment is requesting to use a capacity-optimized route which will select node G even though it's a bit more expensive - sender.send(nodes("A").paymentInitiator, SendPaymentToNode(amountMsat, invoice, maxAttempts = 1, fallbackFinalExpiryDelta = finalCltvExpiryDelta, routeParams = integrationTestRouteParams.copy(heuristics = Left(WeightRatios(0, 0, 0, 1, RelayFees(0 msat, 0)))))) + sender.send(nodes("A").paymentInitiator, SendPaymentToNode(amountMsat, invoice, maxAttempts = 1, routeParams = integrationTestRouteParams.copy(heuristics = Left(WeightRatios(0, 0, 0, 1, RelayFees(0 msat, 0)))))) sender.expectMsgType[UUID] val ps = sender.expectMsgType[PaymentSent] ps.parts.foreach(part => assert(part.route.getOrElse(Nil).exists(_.nodeId == nodes("G").nodeParams.nodeId))) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/PerformanceIntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/PerformanceIntegrationSpec.scala index a1cdfe2be8..516743a068 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/PerformanceIntegrationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/PerformanceIntegrationSpec.scala @@ -86,7 +86,7 @@ class PerformanceIntegrationSpec extends IntegrationSpec { sender.send(nodes("B").paymentHandler, ReceivePayment(Some(amountMsat), Left("1 coffee"))) val pr = sender.expectMsgType[Invoice] // then we make the actual payment - sender.send(nodes("A").paymentInitiator, PaymentInitiator.SendPaymentToNode(amountMsat, pr, fallbackFinalExpiryDelta = finalCltvExpiryDelta, routeParams = integrationTestRouteParams, maxAttempts = 1)) + sender.send(nodes("A").paymentInitiator, PaymentInitiator.SendPaymentToNode(amountMsat, pr, routeParams = integrationTestRouteParams, maxAttempts = 1)) val paymentId = sender.expectMsgType[UUID] sender.expectMsgType[PreimageReceived] val ps = sender.expectMsgType[PaymentSent] diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/Bolt11InvoiceSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/Bolt11InvoiceSpec.scala index 40fd3764ee..27f359ad34 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/Bolt11InvoiceSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/Bolt11InvoiceSpec.scala @@ -283,7 +283,7 @@ class Bolt11InvoiceSpec extends AnyFunSuite { assert(invoice.nodeId == PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) assert(invoice.description == Right(Crypto.sha256(ByteVector.view("One piece of chocolate cake, one icecream cone, one pickle, one slice of swiss cheese, one slice of salami, one lollypop, one piece of cherry pie, one sausage, one cupcake, and one slice of watermelon".getBytes)))) assert(invoice.fallbackAddress() === Some("bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3")) - assert(invoice.minFinalCltvExpiryDelta === Some(CltvExpiryDelta(12))) + assert(invoice.minFinalCltvExpiryDelta === CltvExpiryDelta(12)) assert(invoice.tags.size == 6) assert(invoice.sign(priv).toString == ref) } @@ -347,7 +347,7 @@ class Bolt11InvoiceSpec extends AnyFunSuite { assert(invoice.description === Left("Blockstream Store: 88.85 USD for Blockstream Ledger Nano S x 1, \"Back In My Day\" Sticker x 2, \"I Got Lightning Working\" Sticker x 2 and 1 more items")) assert(invoice.fallbackAddress().isEmpty) assert(invoice.relativeExpiry === 604800.seconds) - assert(invoice.minFinalCltvExpiryDelta === Some(CltvExpiryDelta(10))) + assert(invoice.minFinalCltvExpiryDelta === CltvExpiryDelta(10)) assert(invoice.routingInfo === Seq(Seq(ExtraHop(PublicKey(hex"03d06758583bb5154774a6eb221b1276c9e82d65bbaceca806d90e20c108f4b1c7"), ShortChannelId("589390x3312x1"), 1000 msat, 2500, CltvExpiryDelta(40))))) assert(invoice.sign(priv).toString === ref) } @@ -401,7 +401,7 @@ class Bolt11InvoiceSpec extends AnyFunSuite { assert(field1 == field) val invoice = Bolt11Invoice(chainHash = Block.LivenetGenesisBlock.hash, amount = Some(123 msat), paymentHash = ByteVector32(ByteVector.fill(32)(1)), privateKey = priv, description = Left("Some invoice"), minFinalCltvExpiryDelta = CltvExpiryDelta(18), expirySeconds = Some(123456), timestamp = 12345 unixsec) - assert(invoice.minFinalCltvExpiryDelta === Some(CltvExpiryDelta(18))) + assert(invoice.minFinalCltvExpiryDelta === CltvExpiryDelta(18)) val serialized = invoice.toString val pr1 = Bolt11Invoice.fromString(serialized) assert(invoice == pr1) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartHandlerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartHandlerSpec.scala index 200fd7f084..802ea5a8cc 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartHandlerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartHandlerSpec.scala @@ -173,12 +173,12 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike sender.send(handlerWithMpp, ReceivePayment(Some(42000 msat), Left("1 coffee"))) val pr1 = sender.expectMsgType[Invoice] - assert(pr1.minFinalCltvExpiryDelta === Some(nodeParams.channelConf.minFinalExpiryDelta)) + assert(pr1.minFinalCltvExpiryDelta === nodeParams.channelConf.minFinalExpiryDelta) assert(pr1.relativeExpiry === Alice.nodeParams.invoiceExpiry) sender.send(handlerWithMpp, ReceivePayment(Some(42000 msat), Left("1 coffee with custom expiry"), expirySeconds_opt = Some(60))) val pr2 = sender.expectMsgType[Invoice] - assert(pr2.minFinalCltvExpiryDelta === Some(nodeParams.channelConf.minFinalExpiryDelta)) + assert(pr2.minFinalCltvExpiryDelta === nodeParams.channelConf.minFinalExpiryDelta) assert(pr2.relativeExpiry === 60.seconds) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentInitiatorSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentInitiatorSpec.scala index 5dacd0a063..68bb4500d5 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentInitiatorSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentInitiatorSpec.scala @@ -98,13 +98,13 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike import f._ val customRecords = Seq(GenericTlv(500L, hex"01020304"), GenericTlv(501L, hex"d34db33f")) val invoice = Bolt11Invoice(Block.LivenetGenesisBlock.hash, None, paymentHash, priv_c.privateKey, Left("test"), Channel.MIN_CLTV_EXPIRY_DELTA) - val req = SendPaymentToNode(finalAmount, invoice, 1, Channel.MIN_CLTV_EXPIRY_DELTA, userCustomTlvs = customRecords, routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams) + val req = SendPaymentToNode(finalAmount, invoice, 1, userCustomTlvs = customRecords, routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams) sender.send(initiator, req) sender.expectMsgType[UUID] payFsm.expectMsgType[SendPaymentConfig] val FinalTlvPayload(tlvs) = payFsm.expectMsgType[PaymentLifecycle.SendPayment].finalPayload assert(tlvs.get[AmountToForward].get.amount == finalAmount) - assert(tlvs.get[OutgoingCltv].get.cltv == req.fallbackFinalExpiryDelta.toCltvExpiry(nodeParams.currentBlockHeight + 1)) + assert(tlvs.get[OutgoingCltv].get.cltv == req.invoice.minFinalCltvExpiryDelta.toCltvExpiry(nodeParams.currentBlockHeight + 1)) assert(tlvs.unknown == customRecords) } @@ -131,7 +131,7 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike Bolt11Invoice.InvoiceFeatures(Features[InvoiceFeature](Map[Feature with InvoiceFeature, FeatureSupport](VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory), Set(UnknownFeature(42))).unscoped()) ) val invoice = Bolt11Invoice("lnbc", Some(finalAmount), TimestampSecond.now(), randomKey().publicKey, taggedFields, ByteVector.empty) - val req = SendPaymentToNode(finalAmount + 100.msat, invoice, 1, CltvExpiryDelta(42), routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams) + val req = SendPaymentToNode(finalAmount + 100.msat, invoice, 1, routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams) sender.send(initiator, req) val id = sender.expectMsgType[UUID] val fail = sender.expectMsgType[PaymentFailed] @@ -142,12 +142,10 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike test("forward payment with pre-defined route") { f => import f._ - // we prioritize the invoice's finalExpiryDelta over the one from SendPaymentToRouteRequest - val ignoredFinalExpiryDelta = CltvExpiryDelta(18) val finalExpiryDelta = CltvExpiryDelta(36) val invoice = Bolt11Invoice(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, priv_c.privateKey, Left("Some invoice"), finalExpiryDelta) val route = PredefinedNodeRoute(Seq(a, b, c)) - val request = SendPaymentToRoute(finalAmount, finalAmount, invoice, ignoredFinalExpiryDelta, route, None, None, None, 0 msat, CltvExpiryDelta(0), Nil) + val request = SendPaymentToRoute(finalAmount, finalAmount, invoice, route, None, None, None, 0 msat, CltvExpiryDelta(0), Nil) sender.send(initiator, request) val payment = sender.expectMsgType[SendPaymentToRouteResponse] payFsm.expectMsg(SendPaymentConfig(payment.paymentId, payment.parentId, None, paymentHash, finalAmount, c, Upstream.Local(payment.paymentId), Some(invoice), storeInDb = true, publishEvent = true, recordPathFindingMetrics = false, Nil)) @@ -171,7 +169,7 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike import f._ val finalExpiryDelta = CltvExpiryDelta(24) val invoice = Bolt11Invoice(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, priv_c.privateKey, Left("Some MPP invoice"), finalExpiryDelta, features = featuresWithMpp) - val req = SendPaymentToNode(finalAmount, invoice, 1, /* ignored since the invoice provides it */ CltvExpiryDelta(12), routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams) + val req = SendPaymentToNode(finalAmount, invoice, 1, routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams) assert(req.finalExpiry(nodeParams.currentBlockHeight) === (finalExpiryDelta + 1).toCltvExpiry(nodeParams.currentBlockHeight)) sender.send(initiator, req) val id = sender.expectMsgType[UUID] @@ -195,7 +193,7 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike test("forward multi-part payment") { f => import f._ val invoice = Bolt11Invoice(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, priv_c.privateKey, Left("Some invoice"), CltvExpiryDelta(18), features = featuresWithMpp) - val req = SendPaymentToNode(finalAmount + 100.msat, invoice, 1, CltvExpiryDelta(42), routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams) + val req = SendPaymentToNode(finalAmount + 100.msat, invoice, 1, routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams) sender.send(initiator, req) val id = sender.expectMsgType[UUID] multiPartPayFsm.expectMsg(SendPaymentConfig(id, id, None, paymentHash, finalAmount + 100.msat, c, Upstream.Local(id), Some(invoice), storeInDb = true, publishEvent = true, recordPathFindingMetrics = true, Nil)) @@ -219,7 +217,7 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike import f._ val invoice = Bolt11Invoice(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, priv_c.privateKey, Left("Some invoice"), CltvExpiryDelta(18), features = featuresWithMpp) val route = PredefinedChannelRoute(c, Seq(channelUpdate_ab.shortChannelId, channelUpdate_bc.shortChannelId)) - val req = SendPaymentToRoute(finalAmount / 2, finalAmount, invoice, Channel.MIN_CLTV_EXPIRY_DELTA, route, None, None, None, 0 msat, CltvExpiryDelta(0), Nil) + val req = SendPaymentToRoute(finalAmount / 2, finalAmount, invoice, route, None, None, None, 0 msat, CltvExpiryDelta(0), Nil) sender.send(initiator, req) val payment = sender.expectMsgType[SendPaymentToRouteResponse] payFsm.expectMsg(SendPaymentConfig(payment.paymentId, payment.parentId, None, paymentHash, finalAmount, c, Upstream.Local(payment.paymentId), Some(invoice), storeInDb = true, publishEvent = true, recordPathFindingMetrics = false, Nil)) @@ -250,7 +248,7 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike val ignoredRoutingHints = List(List(ExtraHop(b, channelUpdate_bc.shortChannelId, feeBase = 10 msat, feeProportionalMillionths = 1, cltvExpiryDelta = CltvExpiryDelta(12)))) val invoice = Bolt11Invoice(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, priv_c.privateKey, Left("Some phoenix invoice"), CltvExpiryDelta(9), features = featuresWithTrampoline, extraHops = ignoredRoutingHints) val trampolineFees = 21000 msat - val req = SendTrampolinePayment(finalAmount, invoice, b, Seq((trampolineFees, CltvExpiryDelta(12))), /* ignored since the invoice provides it */ CltvExpiryDelta(18), routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams) + val req = SendTrampolinePayment(finalAmount, invoice, b, Seq((trampolineFees, CltvExpiryDelta(12))), routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams) sender.send(initiator, req) val id = sender.expectMsgType[UUID] multiPartPayFsm.expectMsgType[SendPaymentConfig] @@ -326,7 +324,7 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike val routingHints = List(List(Bolt11Invoice.ExtraHop(b, channelUpdate_bc.shortChannelId, 10 msat, 100, CltvExpiryDelta(144)))) val invoice = Bolt11Invoice(Block.RegtestGenesisBlock.hash, None, paymentHash, priv_a.privateKey, Left("#abittooreckless"), CltvExpiryDelta(18), None, None, routingHints, features = featuresWithMpp) val trampolineFees = 21000 msat - val req = SendTrampolinePayment(finalAmount, invoice, b, Seq((trampolineFees, CltvExpiryDelta(12))), CltvExpiryDelta(9), routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams) + val req = SendTrampolinePayment(finalAmount, invoice, b, Seq((trampolineFees, CltvExpiryDelta(12))), routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams) sender.send(initiator, req) val id = sender.expectMsgType[UUID] val fail = sender.expectMsgType[PaymentFailed] @@ -342,7 +340,7 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike val paymentMetadata = ByteVector.fromValidHex("01" * 400) val invoice = Bolt11Invoice(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, priv_c.privateKey, Left("Much payment very metadata"), CltvExpiryDelta(9), features = featuresWithTrampoline, paymentMetadata = Some(paymentMetadata)) val trampolineFees = 21000 msat - val req = SendTrampolinePayment(finalAmount, invoice, b, Seq((trampolineFees, CltvExpiryDelta(12))), CltvExpiryDelta(18), routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams) + val req = SendTrampolinePayment(finalAmount, invoice, b, Seq((trampolineFees, CltvExpiryDelta(12))), routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams) sender.send(initiator, req) val id = sender.expectMsgType[UUID] val fail = sender.expectMsgType[PaymentFailed] @@ -355,7 +353,7 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike import f._ val invoice = Bolt11Invoice(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, priv_c.privateKey, Left("Some phoenix invoice"), CltvExpiryDelta(18), features = featuresWithTrampoline) val trampolineAttempts = (21000 msat, CltvExpiryDelta(12)) :: (25000 msat, CltvExpiryDelta(24)) :: Nil - val req = SendTrampolinePayment(finalAmount, invoice, b, trampolineAttempts, CltvExpiryDelta(9), routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams) + val req = SendTrampolinePayment(finalAmount, invoice, b, trampolineAttempts, routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams) sender.send(initiator, req) val id = sender.expectMsgType[UUID] val cfg = multiPartPayFsm.expectMsgType[SendPaymentConfig] @@ -390,7 +388,7 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike import f._ val invoice = Bolt11Invoice(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, priv_c.privateKey, Left("Some phoenix invoice"), CltvExpiryDelta(18), features = featuresWithTrampoline) val trampolineAttempts = (21000 msat, CltvExpiryDelta(12)) :: (25000 msat, CltvExpiryDelta(24)) :: Nil - val req = SendTrampolinePayment(finalAmount, invoice, b, trampolineAttempts, CltvExpiryDelta(9), routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams) + val req = SendTrampolinePayment(finalAmount, invoice, b, trampolineAttempts, routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams) sender.send(initiator, req) sender.expectMsgType[UUID] val cfg = multiPartPayFsm.expectMsgType[SendPaymentConfig] @@ -419,7 +417,7 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike import f._ val invoice = Bolt11Invoice(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, priv_c.privateKey, Left("Some phoenix invoice"), CltvExpiryDelta(18), features = featuresWithTrampoline) val trampolineAttempts = (21000 msat, CltvExpiryDelta(12)) :: (25000 msat, CltvExpiryDelta(24)) :: Nil - val req = SendTrampolinePayment(finalAmount, invoice, b, trampolineAttempts, CltvExpiryDelta(9), routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams) + val req = SendTrampolinePayment(finalAmount, invoice, b, trampolineAttempts, routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams) sender.send(initiator, req) sender.expectMsgType[UUID] @@ -447,7 +445,7 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike val invoice = Bolt11Invoice(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, priv_c.privateKey, Left("Some invoice"), CltvExpiryDelta(18)) val trampolineFees = 100 msat val route = PredefinedNodeRoute(Seq(a, b)) - val req = SendPaymentToRoute(finalAmount + trampolineFees, finalAmount, invoice, Channel.MIN_CLTV_EXPIRY_DELTA, route, None, None, None, trampolineFees, CltvExpiryDelta(144), Seq(b, c)) + val req = SendPaymentToRoute(finalAmount + trampolineFees, finalAmount, invoice, route, None, None, None, trampolineFees, CltvExpiryDelta(144), Seq(b, c)) sender.send(initiator, req) val payment = sender.expectMsgType[SendPaymentToRouteResponse] assert(payment.trampolineSecret.nonEmpty) diff --git a/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Payment.scala b/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Payment.scala index 68058f17e6..912d6f15f7 100644 --- a/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Payment.scala +++ b/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Payment.scala @@ -55,15 +55,15 @@ trait Payment { val sendToRoute: Route = postRequest("sendtoroute") { implicit t => withRoute { hops => - formFields(amountMsatFormParam, "recipientAmountMsat".as[MilliSatoshi].?, invoiceFormParam, "finalCltvExpiry".as[Int], "externalId".?, "parentId".as[UUID].?, + formFields(amountMsatFormParam, "recipientAmountMsat".as[MilliSatoshi].?, invoiceFormParam, "externalId".?, "parentId".as[UUID].?, "trampolineSecret".as[ByteVector32].?, "trampolineFeesMsat".as[MilliSatoshi].?, "trampolineCltvExpiry".as[Int].?, "trampolineNodes".as[List[PublicKey]](pubkeyListUnmarshaller).?) { - (amountMsat, recipientAmountMsat_opt, invoice, finalCltvExpiry, externalId_opt, parentId_opt, trampolineSecret_opt, trampolineFeesMsat_opt, trampolineCltvExpiry_opt, trampolineNodes_opt) => { + (amountMsat, recipientAmountMsat_opt, invoice, externalId_opt, parentId_opt, trampolineSecret_opt, trampolineFeesMsat_opt, trampolineCltvExpiry_opt, trampolineNodes_opt) => { val route = hops match { case Left(shortChannelIds) => PredefinedChannelRoute(invoice.nodeId, shortChannelIds) case Right(nodeIds) => PredefinedNodeRoute(nodeIds) } complete(eclairApi.sendToRoute( - amountMsat, recipientAmountMsat_opt, externalId_opt, parentId_opt, invoice, CltvExpiryDelta(finalCltvExpiry), route, trampolineSecret_opt, trampolineFeesMsat_opt, + amountMsat, recipientAmountMsat_opt, externalId_opt, parentId_opt, invoice, route, trampolineSecret_opt, trampolineFeesMsat_opt, trampolineCltvExpiry_opt.map(CltvExpiryDelta), trampolineNodes_opt.getOrElse(Nil) )) } diff --git a/eclair-node/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala b/eclair-node/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala index 642c500cfa..8b02196a9f 100644 --- a/eclair-node/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala +++ b/eclair-node/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala @@ -927,7 +927,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM val jsonNodes = serialization.write(expectedRoute.nodes) val eclair = mock[Eclair] - eclair.sendToRoute(any[MilliSatoshi], any[Option[MilliSatoshi]], any[Option[String]], any[Option[UUID]], any[Bolt11Invoice], any[CltvExpiryDelta], any[PredefinedNodeRoute], any[Option[ByteVector32]], any[Option[MilliSatoshi]], any[Option[CltvExpiryDelta]], any[List[PublicKey]])(any[Timeout]) returns Future.successful(payment) + eclair.sendToRoute(any[MilliSatoshi], any[Option[MilliSatoshi]], any[Option[String]], any[Option[UUID]], any[Bolt11Invoice], any[PredefinedNodeRoute], any[Option[ByteVector32]], any[Option[MilliSatoshi]], any[Option[CltvExpiryDelta]], any[List[PublicKey]])(any[Timeout]) returns Future.successful(payment) val mockService = new MockService(eclair) Post("/sendtoroute", FormData("nodeIds" -> jsonNodes, "amountMsat" -> "1234", "finalCltvExpiry" -> "190", "externalId" -> externalId, "invoice" -> pr.toString).toEntity) ~> @@ -938,7 +938,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM assert(handled) assert(status == OK) assert(entityAs[String] == expected) - eclair.sendToRoute(1234 msat, None, Some(externalId), None, pr, CltvExpiryDelta(190), expectedRoute, None, None, None, Nil)(any[Timeout]).wasCalled(once) + eclair.sendToRoute(1234 msat, None, Some(externalId), None, pr, expectedRoute, None, None, None, Nil)(any[Timeout]).wasCalled(once) } } @@ -950,7 +950,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM val csvNodes = "0217eb8243c95f5a3b7d4c5682d10de354b7007eb59b6807ae407823963c7547a9, 0242a4ae0c5bef18048fbecf995094b74bfb0f7391418d71ed394784373f41e4f3, 026ac9fcd64fb1aa1c491fc490634dc33da41d4a17b554e0adf1b32fee88ee9f28" val eclair = mock[Eclair] - eclair.sendToRoute(any[MilliSatoshi], any[Option[MilliSatoshi]], any[Option[String]], any[Option[UUID]], any[Bolt11Invoice], any[CltvExpiryDelta], any[PredefinedNodeRoute], any[Option[ByteVector32]], any[Option[MilliSatoshi]], any[Option[CltvExpiryDelta]], any[List[PublicKey]])(any[Timeout]) returns Future.successful(payment) + eclair.sendToRoute(any[MilliSatoshi], any[Option[MilliSatoshi]], any[Option[String]], any[Option[UUID]], any[Bolt11Invoice], any[PredefinedNodeRoute], any[Option[ByteVector32]], any[Option[MilliSatoshi]], any[Option[CltvExpiryDelta]], any[List[PublicKey]])(any[Timeout]) returns Future.successful(payment) val mockService = new MockService(eclair) // this test uses CSV encoded route @@ -962,7 +962,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM assert(handled) assert(status == OK) assert(entityAs[String] == expected) - eclair.sendToRoute(1234 msat, None, None, None, pr, CltvExpiryDelta(190), expectedRoute, None, None, None, Nil)(any[Timeout]).wasCalled(once) + eclair.sendToRoute(1234 msat, None, None, None, pr, expectedRoute, None, None, None, Nil)(any[Timeout]).wasCalled(once) } } From dba73c8aa4311e96394fc7fa86811d854cb28360 Mon Sep 17 00:00:00 2001 From: Pierre-Marie Padiou Date: Wed, 2 Mar 2022 14:39:06 +0100 Subject: [PATCH 029/121] (Minor) Flatten `ChannelDataSpec` tests --- .../eclair/channel/ChannelDataSpec.scala | 418 ++++++++++-------- 1 file changed, 228 insertions(+), 190 deletions(-) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/ChannelDataSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/ChannelDataSpec.scala index 48983db99f..1715734ea2 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/ChannelDataSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/ChannelDataSpec.scala @@ -23,7 +23,7 @@ import fr.acinq.eclair.channel.Helpers.Closing import fr.acinq.eclair.channel.states.ChannelStateTestsHelperMethods import fr.acinq.eclair.transactions.Transactions._ import fr.acinq.eclair.wire.protocol.{CommitSig, RevokeAndAck, UpdateAddHtlc} -import fr.acinq.eclair.{MilliSatoshiLong, TestKitBaseClass} +import fr.acinq.eclair.{MilliSatoshiLong, NodeParams, TestKitBaseClass} import org.scalatest.funsuite.AnyFunSuiteLike import scodec.bits.ByteVector @@ -69,7 +69,11 @@ class ChannelDataSpec extends TestKitBaseClass with AnyFunSuiteLike with Channel Fixture(alice, HtlcWithPreimage(rb2, htlcb2), bob, HtlcWithPreimage(ra2, htlca2), TestProbe()) } - test("local commit published") { + case class LocalFixture(nodeParams: NodeParams, alice: TestFSMRef[ChannelState, ChannelData, Channel], alicePendingHtlc: HtlcWithPreimage, remainingHtlcOutpoint: OutPoint, lcp: LocalCommitPublished, rcp: RemoteCommitPublished, htlcTimeoutTxs: Seq[HtlcTimeoutTx], htlcSuccessTxs: Seq[HtlcSuccessTx], probe: TestProbe) { + val aliceClosing = alice.stateData.asInstanceOf[DATA_CLOSING] + } + + private def setupClosingChannelForLocalClose(): LocalFixture = { val f = setupClosingChannel() import f._ @@ -104,115 +108,127 @@ class ChannelDataSpec extends TestKitBaseClass with AnyFunSuiteLike with Channel assert(bobClosing.remoteCommitPublished.nonEmpty) val rcp = bobClosing.remoteCommitPublished.get - // Scenario 1: our HTLC txs are confirmed, they claim the remaining HTLC - { - val lcp3 = (htlcSuccessTxs.map(_.tx) ++ htlcTimeoutTxs.map(_.tx)).foldLeft(lcp2) { - case (current, tx) => - val (current1, Some(_)) = Closing.claimLocalCommitHtlcTxOutput(current, nodeParams.channelKeyManager, aliceClosing.commitments, tx, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) - Closing.updateLocalCommitPublished(current1, tx) - } - assert(!lcp3.isDone) - assert(lcp3.claimHtlcDelayedTxs.length === 3) + LocalFixture(nodeParams, f.alice, alicePendingHtlc, remainingHtlcOutpoint, lcp2, rcp, htlcTimeoutTxs, htlcSuccessTxs, probe) + } - val lcp4 = lcp3.claimHtlcDelayedTxs.map(_.tx).foldLeft(lcp3) { - case (current, tx) => Closing.updateLocalCommitPublished(current, tx) - } - assert(!lcp4.isDone) + test("local commit published (our HTLC txs are confirmed, they claim the remaining HTLC)") { + val f = setupClosingChannelForLocalClose() + import f._ - val theirClaimHtlcTimeout = rcp.claimHtlcTxs(remainingHtlcOutpoint) - assert(theirClaimHtlcTimeout !== None) - val lcp5 = Closing.updateLocalCommitPublished(lcp4, theirClaimHtlcTimeout.get.tx) - assert(lcp5.isDone) + val lcp3 = (htlcSuccessTxs.map(_.tx) ++ htlcTimeoutTxs.map(_.tx)).foldLeft(lcp) { + case (current, tx) => + val (current1, Some(_)) = Closing.claimLocalCommitHtlcTxOutput(current, nodeParams.channelKeyManager, aliceClosing.commitments, tx, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) + Closing.updateLocalCommitPublished(current1, tx) } + assert(!lcp3.isDone) + assert(lcp3.claimHtlcDelayedTxs.length === 3) - // Scenario 2: our HTLC txs are confirmed and we claim the remaining HTLC - { - val lcp3 = (htlcSuccessTxs.map(_.tx) ++ htlcTimeoutTxs.map(_.tx)).foldLeft(lcp2) { - case (current, tx) => - val (current1, Some(_)) = Closing.claimLocalCommitHtlcTxOutput(current, nodeParams.channelKeyManager, aliceClosing.commitments, tx, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) - Closing.updateLocalCommitPublished(current1, tx) - } - assert(!lcp3.isDone) - assert(lcp3.claimHtlcDelayedTxs.length === 3) - - val lcp4 = lcp3.claimHtlcDelayedTxs.map(_.tx).foldLeft(lcp3) { - case (current, tx) => Closing.updateLocalCommitPublished(current, tx) - } - assert(!lcp4.isDone) + val lcp4 = lcp3.claimHtlcDelayedTxs.map(_.tx).foldLeft(lcp3) { + case (current, tx) => Closing.updateLocalCommitPublished(current, tx) + } + assert(!lcp4.isDone) - alice ! CMD_FULFILL_HTLC(alicePendingHtlc.htlc.id, alicePendingHtlc.preimage, replyTo_opt = Some(probe.ref)) - probe.expectMsgType[CommandSuccess[CMD_FULFILL_HTLC]] - val aliceClosing1 = alice.stateData.asInstanceOf[DATA_CLOSING] - val lcp5 = aliceClosing1.localCommitPublished.get.copy(irrevocablySpent = lcp4.irrevocablySpent, claimHtlcDelayedTxs = lcp4.claimHtlcDelayedTxs) - assert(lcp5.htlcTxs(remainingHtlcOutpoint) !== None) - assert(lcp5.claimHtlcDelayedTxs.length === 3) + val theirClaimHtlcTimeout = rcp.claimHtlcTxs(remainingHtlcOutpoint) + assert(theirClaimHtlcTimeout !== None) + val lcp5 = Closing.updateLocalCommitPublished(lcp4, theirClaimHtlcTimeout.get.tx) + assert(lcp5.isDone) + } - val newHtlcSuccessTx = lcp5.htlcTxs(remainingHtlcOutpoint).get.tx - val (lcp6, Some(newClaimHtlcDelayedTx)) = Closing.claimLocalCommitHtlcTxOutput(lcp5, nodeParams.channelKeyManager, aliceClosing.commitments, newHtlcSuccessTx, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) - assert(lcp6.claimHtlcDelayedTxs.length === 4) + test("local commit published (our HTLC txs are confirmed and we claim the remaining HTLC)") { + val f = setupClosingChannelForLocalClose() + import f._ - val lcp7 = Closing.updateLocalCommitPublished(lcp6, newHtlcSuccessTx) - assert(!lcp7.isDone) + val lcp3 = (htlcSuccessTxs.map(_.tx) ++ htlcTimeoutTxs.map(_.tx)).foldLeft(lcp) { + case (current, tx) => + val (current1, Some(_)) = Closing.claimLocalCommitHtlcTxOutput(current, nodeParams.channelKeyManager, aliceClosing.commitments, tx, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) + Closing.updateLocalCommitPublished(current1, tx) + } + assert(!lcp3.isDone) + assert(lcp3.claimHtlcDelayedTxs.length === 3) - val lcp8 = Closing.updateLocalCommitPublished(lcp7, newClaimHtlcDelayedTx.tx) - assert(lcp8.isDone) + val lcp4 = lcp3.claimHtlcDelayedTxs.map(_.tx).foldLeft(lcp3) { + case (current, tx) => Closing.updateLocalCommitPublished(current, tx) } + assert(!lcp4.isDone) - // Scenario 3: they fulfill one of the HTLCs we sent them - { - val remoteHtlcSuccess = rcp.claimHtlcTxs.values.collectFirst { case Some(tx: ClaimHtlcSuccessTx) => tx }.get - val lcp3 = (htlcSuccessTxs.map(_.tx) ++ Seq(remoteHtlcSuccess.tx)).foldLeft(lcp2) { - case (current, tx) => - val (current1, _) = Closing.claimLocalCommitHtlcTxOutput(current, nodeParams.channelKeyManager, aliceClosing.commitments, tx, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) - Closing.updateLocalCommitPublished(current1, tx) - } - assert(lcp3.claimHtlcDelayedTxs.length === 1) - assert(!lcp3.isDone) + alice ! CMD_FULFILL_HTLC(alicePendingHtlc.htlc.id, alicePendingHtlc.preimage, replyTo_opt = Some(probe.ref)) + probe.expectMsgType[CommandSuccess[CMD_FULFILL_HTLC]] + val aliceClosing1 = alice.stateData.asInstanceOf[DATA_CLOSING] + val lcp5 = aliceClosing1.localCommitPublished.get.copy(irrevocablySpent = lcp4.irrevocablySpent, claimHtlcDelayedTxs = lcp4.claimHtlcDelayedTxs) + assert(lcp5.htlcTxs(remainingHtlcOutpoint) !== None) + assert(lcp5.claimHtlcDelayedTxs.length === 3) - val lcp4 = Closing.updateLocalCommitPublished(lcp3, lcp3.claimHtlcDelayedTxs.head.tx) - assert(!lcp4.isDone) + val newHtlcSuccessTx = lcp5.htlcTxs(remainingHtlcOutpoint).get.tx + val (lcp6, Some(newClaimHtlcDelayedTx)) = Closing.claimLocalCommitHtlcTxOutput(lcp5, nodeParams.channelKeyManager, aliceClosing.commitments, newHtlcSuccessTx, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) + assert(lcp6.claimHtlcDelayedTxs.length === 4) - val remainingHtlcTimeoutTxs = htlcTimeoutTxs.filter(_.input.outPoint != remoteHtlcSuccess.input.outPoint) - assert(remainingHtlcTimeoutTxs.length === 1) - val (lcp5, Some(remainingClaimHtlcTx)) = Closing.claimLocalCommitHtlcTxOutput(lcp4, nodeParams.channelKeyManager, aliceClosing.commitments, remainingHtlcTimeoutTxs.head.tx, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) - assert(lcp5.claimHtlcDelayedTxs.length === 2) + val lcp7 = Closing.updateLocalCommitPublished(lcp6, newHtlcSuccessTx) + assert(!lcp7.isDone) - val lcp6 = (remainingHtlcTimeoutTxs.map(_.tx) ++ Seq(remainingClaimHtlcTx.tx)).foldLeft(lcp5) { - case (current, tx) => Closing.updateLocalCommitPublished(current, tx) - } - assert(!lcp6.isDone) + val lcp8 = Closing.updateLocalCommitPublished(lcp7, newClaimHtlcDelayedTx.tx) + assert(lcp8.isDone) + } - val theirClaimHtlcTimeout = rcp.claimHtlcTxs(remainingHtlcOutpoint) - val lcp7 = Closing.updateLocalCommitPublished(lcp6, theirClaimHtlcTimeout.get.tx) - assert(lcp7.isDone) + test("local commit published (they fulfill one of the HTLCs we sent them)") { + val f = setupClosingChannelForLocalClose() + import f._ + + val remoteHtlcSuccess = rcp.claimHtlcTxs.values.collectFirst { case Some(tx: ClaimHtlcSuccessTx) => tx }.get + val lcp3 = (htlcSuccessTxs.map(_.tx) ++ Seq(remoteHtlcSuccess.tx)).foldLeft(lcp) { + case (current, tx) => + val (current1, _) = Closing.claimLocalCommitHtlcTxOutput(current, nodeParams.channelKeyManager, aliceClosing.commitments, tx, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) + Closing.updateLocalCommitPublished(current1, tx) } + assert(lcp3.claimHtlcDelayedTxs.length === 1) + assert(!lcp3.isDone) - // Scenario 4: they get back the HTLCs they sent us - { - val lcp3 = htlcTimeoutTxs.map(_.tx).foldLeft(lcp2) { - case (current, tx) => - val (current1, Some(_)) = Closing.claimLocalCommitHtlcTxOutput(current, nodeParams.channelKeyManager, aliceClosing.commitments, tx, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) - Closing.updateLocalCommitPublished(current1, tx) - } - assert(!lcp3.isDone) - assert(lcp3.claimHtlcDelayedTxs.length === 2) + val lcp4 = Closing.updateLocalCommitPublished(lcp3, lcp3.claimHtlcDelayedTxs.head.tx) + assert(!lcp4.isDone) - val lcp4 = lcp3.claimHtlcDelayedTxs.map(_.tx).foldLeft(lcp3) { - case (current, tx) => Closing.updateLocalCommitPublished(current, tx) - } - assert(!lcp4.isDone) + val remainingHtlcTimeoutTxs = htlcTimeoutTxs.filter(_.input.outPoint != remoteHtlcSuccess.input.outPoint) + assert(remainingHtlcTimeoutTxs.length === 1) + val (lcp5, Some(remainingClaimHtlcTx)) = Closing.claimLocalCommitHtlcTxOutput(lcp4, nodeParams.channelKeyManager, aliceClosing.commitments, remainingHtlcTimeoutTxs.head.tx, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) + assert(lcp5.claimHtlcDelayedTxs.length === 2) + + val lcp6 = (remainingHtlcTimeoutTxs.map(_.tx) ++ Seq(remainingClaimHtlcTx.tx)).foldLeft(lcp5) { + case (current, tx) => Closing.updateLocalCommitPublished(current, tx) + } + assert(!lcp6.isDone) + + val theirClaimHtlcTimeout = rcp.claimHtlcTxs(remainingHtlcOutpoint) + val lcp7 = Closing.updateLocalCommitPublished(lcp6, theirClaimHtlcTimeout.get.tx) + assert(lcp7.isDone) + } + + test("local commit published (they get back the HTLCs they sent us)") { + val f = setupClosingChannelForLocalClose() + import f._ - val remoteHtlcTimeoutTxs = getClaimHtlcTimeoutTxs(rcp).map(_.tx) - assert(remoteHtlcTimeoutTxs.length === 2) - val lcp5 = Closing.updateLocalCommitPublished(lcp4, remoteHtlcTimeoutTxs.head) - assert(!lcp5.isDone) + val lcp3 = htlcTimeoutTxs.map(_.tx).foldLeft(lcp) { + case (current, tx) => + val (current1, Some(_)) = Closing.claimLocalCommitHtlcTxOutput(current, nodeParams.channelKeyManager, aliceClosing.commitments, tx, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) + Closing.updateLocalCommitPublished(current1, tx) + } + assert(!lcp3.isDone) + assert(lcp3.claimHtlcDelayedTxs.length === 2) - val lcp6 = Closing.updateLocalCommitPublished(lcp5, remoteHtlcTimeoutTxs.last) - assert(lcp6.isDone) + val lcp4 = lcp3.claimHtlcDelayedTxs.map(_.tx).foldLeft(lcp3) { + case (current, tx) => Closing.updateLocalCommitPublished(current, tx) } + assert(!lcp4.isDone) + + val remoteHtlcTimeoutTxs = getClaimHtlcTimeoutTxs(rcp).map(_.tx) + assert(remoteHtlcTimeoutTxs.length === 2) + val lcp5 = Closing.updateLocalCommitPublished(lcp4, remoteHtlcTimeoutTxs.head) + assert(!lcp5.isDone) + + val lcp6 = Closing.updateLocalCommitPublished(lcp5, remoteHtlcTimeoutTxs.last) + assert(lcp6.isDone) } - test("remote commit published") { + case class RemoteFixture(bob: TestFSMRef[ChannelState, ChannelData, Channel], bobPendingHtlc: HtlcWithPreimage, remainingHtlcOutpoint: OutPoint, lcp: LocalCommitPublished, rcp: RemoteCommitPublished, claimHtlcTimeoutTxs: Seq[ClaimHtlcTimeoutTx], claimHtlcSuccessTxs: Seq[ClaimHtlcSuccessTx], probe: TestProbe) + + private def setupClosingChannelForRemoteClose(): RemoteFixture = { val f = setupClosingChannel() import f._ @@ -245,73 +261,83 @@ class ChannelDataSpec extends TestKitBaseClass with AnyFunSuiteLike with Channel assert(aliceClosing.localCommitPublished.nonEmpty) val lcp = aliceClosing.localCommitPublished.get - // Scenario 1: our claim-HTLC txs are confirmed, they claim the remaining HTLC - { - val rcp3 = (claimHtlcSuccessTxs ++ claimHtlcTimeoutTxs).map(_.tx).foldLeft(rcp2) { - case (current, tx) => Closing.updateRemoteCommitPublished(current, tx) - } - assert(!rcp3.isDone) + RemoteFixture(f.bob, f.bobPendingHtlc, remainingHtlcOutpoint, lcp, rcp2, claimHtlcTimeoutTxs, claimHtlcSuccessTxs, probe) + } + + test("remote commit published (our claim-HTLC txs are confirmed, they claim the remaining HTLC)") { + val f = setupClosingChannelForRemoteClose() + import f._ - val theirHtlcTimeout = lcp.htlcTxs(remainingHtlcOutpoint) - assert(theirHtlcTimeout !== None) - val rcp4 = Closing.updateRemoteCommitPublished(rcp3, theirHtlcTimeout.get.tx) - assert(rcp4.isDone) + val rcp3 = (claimHtlcSuccessTxs ++ claimHtlcTimeoutTxs).map(_.tx).foldLeft(rcp) { + case (current, tx) => Closing.updateRemoteCommitPublished(current, tx) } + assert(!rcp3.isDone) - // Scenario 2: our claim-HTLC txs are confirmed and we claim the remaining HTLC - { - val rcp3 = (claimHtlcSuccessTxs ++ claimHtlcTimeoutTxs).map(_.tx).foldLeft(rcp2) { - case (current, tx) => Closing.updateRemoteCommitPublished(current, tx) - } - assert(!rcp3.isDone) + val theirHtlcTimeout = lcp.htlcTxs(remainingHtlcOutpoint) + assert(theirHtlcTimeout !== None) + val rcp4 = Closing.updateRemoteCommitPublished(rcp3, theirHtlcTimeout.get.tx) + assert(rcp4.isDone) + } - bob ! CMD_FULFILL_HTLC(bobPendingHtlc.htlc.id, bobPendingHtlc.preimage, replyTo_opt = Some(probe.ref)) - probe.expectMsgType[CommandSuccess[CMD_FULFILL_HTLC]] - val bobClosing1 = bob.stateData.asInstanceOf[DATA_CLOSING] - val rcp4 = bobClosing1.remoteCommitPublished.get.copy(irrevocablySpent = rcp3.irrevocablySpent) - assert(rcp4.claimHtlcTxs(remainingHtlcOutpoint) !== None) - val newClaimHtlcSuccessTx = rcp4.claimHtlcTxs(remainingHtlcOutpoint).get + test("remote commit published (our claim-HTLC txs are confirmed and we claim the remaining HTLC)") { + val f = setupClosingChannelForRemoteClose() + import f._ - val rcp5 = Closing.updateRemoteCommitPublished(rcp4, newClaimHtlcSuccessTx.tx) - assert(rcp5.isDone) + val rcp3 = (claimHtlcSuccessTxs ++ claimHtlcTimeoutTxs).map(_.tx).foldLeft(rcp) { + case (current, tx) => Closing.updateRemoteCommitPublished(current, tx) } + assert(!rcp3.isDone) - // Scenario 3: they fulfill one of the HTLCs we sent them - { - val remoteHtlcSuccess = lcp.htlcTxs.values.collectFirst { case Some(tx: HtlcSuccessTx) => tx }.get - val rcp3 = (remoteHtlcSuccess.tx +: claimHtlcSuccessTxs.map(_.tx)).foldLeft(rcp2) { - case (current, tx) => Closing.updateRemoteCommitPublished(current, tx) - } - assert(!rcp3.isDone) + bob ! CMD_FULFILL_HTLC(bobPendingHtlc.htlc.id, bobPendingHtlc.preimage, replyTo_opt = Some(probe.ref)) + probe.expectMsgType[CommandSuccess[CMD_FULFILL_HTLC]] + val bobClosing1 = bob.stateData.asInstanceOf[DATA_CLOSING] + val rcp4 = bobClosing1.remoteCommitPublished.get.copy(irrevocablySpent = rcp3.irrevocablySpent) + assert(rcp4.claimHtlcTxs(remainingHtlcOutpoint) !== None) + val newClaimHtlcSuccessTx = rcp4.claimHtlcTxs(remainingHtlcOutpoint).get - val remainingClaimHtlcTimeoutTx = claimHtlcTimeoutTxs.filter(_.input.outPoint != remoteHtlcSuccess.input.outPoint) - assert(remainingClaimHtlcTimeoutTx.length === 1) - val rcp4 = Closing.updateRemoteCommitPublished(rcp3, remainingClaimHtlcTimeoutTx.head.tx) - assert(!rcp4.isDone) + val rcp5 = Closing.updateRemoteCommitPublished(rcp4, newClaimHtlcSuccessTx.tx) + assert(rcp5.isDone) + } + + test("remote commit published (they fulfill one of the HTLCs we sent them)") { + val f = setupClosingChannelForRemoteClose() + import f._ - val theirHtlcTimeout = lcp.htlcTxs(remainingHtlcOutpoint) - assert(theirHtlcTimeout !== None) - val rcp5 = Closing.updateRemoteCommitPublished(rcp4, theirHtlcTimeout.get.tx) - assert(rcp5.isDone) + val remoteHtlcSuccess = lcp.htlcTxs.values.collectFirst { case Some(tx: HtlcSuccessTx) => tx }.get + val rcp3 = (remoteHtlcSuccess.tx +: claimHtlcSuccessTxs.map(_.tx)).foldLeft(rcp) { + case (current, tx) => Closing.updateRemoteCommitPublished(current, tx) } + assert(!rcp3.isDone) - // Scenario 4: they get back the HTLCs they sent us - { - val rcp3 = claimHtlcTimeoutTxs.map(_.tx).foldLeft(rcp2) { - case (current, tx) => Closing.updateRemoteCommitPublished(current, tx) - } - assert(!rcp3.isDone) + val remainingClaimHtlcTimeoutTx = claimHtlcTimeoutTxs.filter(_.input.outPoint != remoteHtlcSuccess.input.outPoint) + assert(remainingClaimHtlcTimeoutTx.length === 1) + val rcp4 = Closing.updateRemoteCommitPublished(rcp3, remainingClaimHtlcTimeoutTx.head.tx) + assert(!rcp4.isDone) - val htlcTimeoutTxs = getHtlcTimeoutTxs(lcp).map(_.tx) - val rcp4 = Closing.updateRemoteCommitPublished(rcp3, htlcTimeoutTxs.head) - assert(!rcp4.isDone) + val theirHtlcTimeout = lcp.htlcTxs(remainingHtlcOutpoint) + assert(theirHtlcTimeout !== None) + val rcp5 = Closing.updateRemoteCommitPublished(rcp4, theirHtlcTimeout.get.tx) + assert(rcp5.isDone) + } + + test("remote commit published (they get back the HTLCs they sent us)") { + val f = setupClosingChannelForRemoteClose() + import f._ - val rcp5 = Closing.updateRemoteCommitPublished(rcp4, htlcTimeoutTxs.last) - assert(rcp5.isDone) + val rcp3 = claimHtlcTimeoutTxs.map(_.tx).foldLeft(rcp) { + case (current, tx) => Closing.updateRemoteCommitPublished(current, tx) } + assert(!rcp3.isDone) + + val htlcTimeoutTxs = getHtlcTimeoutTxs(lcp).map(_.tx) + val rcp4 = Closing.updateRemoteCommitPublished(rcp3, htlcTimeoutTxs.head) + assert(!rcp4.isDone) + + val rcp5 = Closing.updateRemoteCommitPublished(rcp4, htlcTimeoutTxs.last) + assert(rcp5.isDone) } - test("next remote commit published") { + private def setupClosingChannelForNextRemoteClose(): RemoteFixture = { val probe = TestProbe() val setup = init() reachNormal(setup) @@ -374,70 +400,82 @@ class ChannelDataSpec extends TestKitBaseClass with AnyFunSuiteLike with Channel assert(rcp2.isConfirmed) assert(!rcp2.isDone) - // Scenario 1: our claim-HTLC txs are confirmed, they claim the remaining HTLC - { - val rcp3 = (claimHtlcSuccessTxs ++ claimHtlcTimeoutTxs).map(_.tx).foldLeft(rcp2) { - case (current, tx) => Closing.updateRemoteCommitPublished(current, tx) - } - assert(!rcp3.isDone) + val bobPendingHtlc = HtlcWithPreimage(ra2, htlca2) + + RemoteFixture(bob, bobPendingHtlc, remainingHtlcOutpoint, lcp, rcp2, claimHtlcTimeoutTxs, claimHtlcSuccessTxs, probe) + } + + test("next remote commit published (our claim-HTLC txs are confirmed, they claim the remaining HTLC)") { + val f = setupClosingChannelForNextRemoteClose() + import f._ - val theirHtlcTimeout = lcp.htlcTxs(remainingHtlcOutpoint) - assert(theirHtlcTimeout !== None) - val rcp4 = Closing.updateRemoteCommitPublished(rcp3, theirHtlcTimeout.get.tx) - assert(rcp4.isDone) + val rcp3 = (claimHtlcSuccessTxs ++ claimHtlcTimeoutTxs).map(_.tx).foldLeft(rcp) { + case (current, tx) => Closing.updateRemoteCommitPublished(current, tx) } + assert(!rcp3.isDone) - // Scenario 2: our claim-HTLC txs are confirmed and we claim the remaining HTLC - { - val rcp3 = (claimHtlcSuccessTxs ++ claimHtlcTimeoutTxs).map(_.tx).foldLeft(rcp2) { - case (current, tx) => Closing.updateRemoteCommitPublished(current, tx) - } - assert(!rcp3.isDone) + val theirHtlcTimeout = lcp.htlcTxs(remainingHtlcOutpoint) + assert(theirHtlcTimeout !== None) + val rcp4 = Closing.updateRemoteCommitPublished(rcp3, theirHtlcTimeout.get.tx) + assert(rcp4.isDone) + } - bob ! CMD_FULFILL_HTLC(htlca2.id, ra2, replyTo_opt = Some(probe.ref)) - probe.expectMsgType[CommandSuccess[CMD_FULFILL_HTLC]] - val bobClosing1 = bob.stateData.asInstanceOf[DATA_CLOSING] - val rcp4 = bobClosing1.nextRemoteCommitPublished.get.copy(irrevocablySpent = rcp3.irrevocablySpent) - assert(rcp4.claimHtlcTxs(remainingHtlcOutpoint) !== None) - val newClaimHtlcSuccessTx = rcp4.claimHtlcTxs(remainingHtlcOutpoint).get + test("next remote commit published (our claim-HTLC txs are confirmed and we claim the remaining HTLC)") { + val f = setupClosingChannelForNextRemoteClose() + import f._ - val rcp5 = Closing.updateRemoteCommitPublished(rcp4, newClaimHtlcSuccessTx.tx) - assert(rcp5.isDone) + val rcp3 = (claimHtlcSuccessTxs ++ claimHtlcTimeoutTxs).map(_.tx).foldLeft(rcp) { + case (current, tx) => Closing.updateRemoteCommitPublished(current, tx) } + assert(!rcp3.isDone) - // Scenario 3: they fulfill one of the HTLCs we sent them - { - val remoteHtlcSuccess = lcp.htlcTxs.values.collectFirst { case Some(tx: HtlcSuccessTx) => tx }.get - val rcp3 = (remoteHtlcSuccess.tx +: claimHtlcSuccessTxs.map(_.tx)).foldLeft(rcp2) { - case (current, tx) => Closing.updateRemoteCommitPublished(current, tx) - } - assert(!rcp3.isDone) + bob ! CMD_FULFILL_HTLC(bobPendingHtlc.htlc.id, bobPendingHtlc.preimage, replyTo_opt = Some(probe.ref)) + probe.expectMsgType[CommandSuccess[CMD_FULFILL_HTLC]] + val bobClosing1 = bob.stateData.asInstanceOf[DATA_CLOSING] + val rcp4 = bobClosing1.nextRemoteCommitPublished.get.copy(irrevocablySpent = rcp3.irrevocablySpent) + assert(rcp4.claimHtlcTxs(remainingHtlcOutpoint) !== None) + val newClaimHtlcSuccessTx = rcp4.claimHtlcTxs(remainingHtlcOutpoint).get - val remainingClaimHtlcTimeoutTx = claimHtlcTimeoutTxs.filter(_.input.outPoint != remoteHtlcSuccess.input.outPoint) - assert(remainingClaimHtlcTimeoutTx.length === 1) - val rcp4 = Closing.updateRemoteCommitPublished(rcp3, remainingClaimHtlcTimeoutTx.head.tx) - assert(!rcp4.isDone) + val rcp5 = Closing.updateRemoteCommitPublished(rcp4, newClaimHtlcSuccessTx.tx) + assert(rcp5.isDone) + } + + test("next remote commit published (they fulfill one of the HTLCs we sent them)") { + val f = setupClosingChannelForNextRemoteClose() + import f._ - val theirHtlcTimeout = lcp.htlcTxs(remainingHtlcOutpoint) - assert(theirHtlcTimeout !== None) - val rcp5 = Closing.updateRemoteCommitPublished(rcp4, theirHtlcTimeout.get.tx) - assert(rcp5.isDone) + val remoteHtlcSuccess = lcp.htlcTxs.values.collectFirst { case Some(tx: HtlcSuccessTx) => tx }.get + val rcp3 = (remoteHtlcSuccess.tx +: claimHtlcSuccessTxs.map(_.tx)).foldLeft(rcp) { + case (current, tx) => Closing.updateRemoteCommitPublished(current, tx) } + assert(!rcp3.isDone) - // Scenario 4: they get back the HTLCs they sent us - { - val rcp3 = claimHtlcTimeoutTxs.map(_.tx).foldLeft(rcp2) { - case (current, tx) => Closing.updateRemoteCommitPublished(current, tx) - } - assert(!rcp3.isDone) + val remainingClaimHtlcTimeoutTx = claimHtlcTimeoutTxs.filter(_.input.outPoint != remoteHtlcSuccess.input.outPoint) + assert(remainingClaimHtlcTimeoutTx.length === 1) + val rcp4 = Closing.updateRemoteCommitPublished(rcp3, remainingClaimHtlcTimeoutTx.head.tx) + assert(!rcp4.isDone) - val htlcTimeoutTxs = getHtlcTimeoutTxs(lcp).map(_.tx) - val rcp4 = Closing.updateRemoteCommitPublished(rcp3, htlcTimeoutTxs.head) - assert(!rcp4.isDone) + val theirHtlcTimeout = lcp.htlcTxs(remainingHtlcOutpoint) + assert(theirHtlcTimeout !== None) + val rcp5 = Closing.updateRemoteCommitPublished(rcp4, theirHtlcTimeout.get.tx) + assert(rcp5.isDone) + } + + test("next remote commit published (they get back the HTLCs they sent us)") { + val f = setupClosingChannelForNextRemoteClose() + import f._ - val rcp5 = Closing.updateRemoteCommitPublished(rcp4, htlcTimeoutTxs.last) - assert(rcp5.isDone) + val rcp3 = claimHtlcTimeoutTxs.map(_.tx).foldLeft(rcp) { + case (current, tx) => Closing.updateRemoteCommitPublished(current, tx) } + assert(!rcp3.isDone) + + val htlcTimeoutTxs = getHtlcTimeoutTxs(lcp).map(_.tx) + val rcp4 = Closing.updateRemoteCommitPublished(rcp3, htlcTimeoutTxs.head) + assert(!rcp4.isDone) + + val rcp5 = Closing.updateRemoteCommitPublished(rcp4, htlcTimeoutTxs.last) + assert(rcp5.isDone) } test("revoked commit published") { From be78e0ca578726015bd32e45bd0310415061843c Mon Sep 17 00:00:00 2001 From: Pierre-Marie Padiou Date: Wed, 2 Mar 2022 18:12:32 +0100 Subject: [PATCH 030/121] Separate htlc calculation from local/remote commit (no quicklens) (#2194) When we know for sure that an incoming htlc will never be fulfilled, we can safely ignore it. We used to only handle htlc fulfillment. Fixes #2168. Note that when we receive a preimage for a htlc during a local/remote force close, we need to only update the htlcs and keep the rest of `LocalCommitPublished` untouched. In particular, we don't regenerate the claim-main tx (which can cause issues if fees change), and leave the `irrevocablySpent` untouched. Also renamed and reorganized a few helper methods. --- .../fr/acinq/eclair/channel/Channel.scala | 63 +- .../fr/acinq/eclair/channel/Commitments.scala | 8 +- .../fr/acinq/eclair/channel/Helpers.scala | 845 +++++++++--------- .../eclair/channel/ChannelDataSpec.scala | 66 +- .../fr/acinq/eclair/channel/HelpersSpec.scala | 12 +- .../states/g/NegotiatingStateSpec.scala | 4 +- 6 files changed, 549 insertions(+), 449 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala index 0356de161b..5316b2486b 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala @@ -43,8 +43,8 @@ import fr.acinq.eclair.crypto.keymanager.ChannelKeyManager import fr.acinq.eclair.db.DbEventHandler.ChannelEvent.EventType import fr.acinq.eclair.db.PendingCommandsDb import fr.acinq.eclair.io.Peer -import fr.acinq.eclair.payment.{Bolt11Invoice, PaymentSettlingOnChain} import fr.acinq.eclair.payment.relay.Relayer +import fr.acinq.eclair.payment.{Bolt11Invoice, PaymentSettlingOnChain} import fr.acinq.eclair.router.Announcements import fr.acinq.eclair.transactions.Transactions.{ClosingTx, TxOwner} import fr.acinq.eclair.transactions._ @@ -820,7 +820,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, remo Commitments.sendCommit(d.commitments, keyManager) match { case Right((commitments1, commit)) => log.debug("sending a new sig, spec:\n{}", Commitments.specs2String(commitments1)) - val nextRemoteCommit = commitments1.remoteNextCommitInfo.swap.toOption.get.nextRemoteCommit + val nextRemoteCommit = commitments1.nextRemoteCommit_opt.get val nextCommitNumber = nextRemoteCommit.index // we persist htlc data in order to be able to claim htlc outputs in case a revoked tx is published by our // counterparty, so only htlcs above remote's dust_limit matter @@ -972,7 +972,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, remo // there are no pending signed changes, let's go directly to NEGOTIATING if (d.commitments.localParams.isFunder) { // we are funder, need to initiate the negotiation by sending the first closing_signed - val (closingTx, closingSigned) = Closing.makeFirstClosingTx(keyManager, d.commitments, localShutdown.scriptPubKey, remoteShutdownScript, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets, d.closingFeerates) + val (closingTx, closingSigned) = Closing.MutualClose.makeFirstClosingTx(keyManager, d.commitments, localShutdown.scriptPubKey, remoteShutdownScript, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets, d.closingFeerates) goto(NEGOTIATING) using DATA_NEGOTIATING(d.commitments, localShutdown, remoteShutdown, List(List(ClosingTxProposed(closingTx, closingSigned))), bestUnpublishedClosingTx_opt = None) storing() sending sendList :+ closingSigned } else { // we are fundee, will wait for their closing_signed @@ -1212,7 +1212,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, remo if (commitments1.hasNoPendingHtlcsOrFeeUpdate) { if (d.commitments.localParams.isFunder) { // we are funder, need to initiate the negotiation by sending the first closing_signed - val (closingTx, closingSigned) = Closing.makeFirstClosingTx(keyManager, commitments1, localShutdown.scriptPubKey, remoteShutdown.scriptPubKey, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets, closingFeerates) + val (closingTx, closingSigned) = Closing.MutualClose.makeFirstClosingTx(keyManager, commitments1, localShutdown.scriptPubKey, remoteShutdown.scriptPubKey, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets, closingFeerates) goto(NEGOTIATING) using DATA_NEGOTIATING(commitments1, localShutdown, remoteShutdown, List(List(ClosingTxProposed(closingTx, closingSigned))), bestUnpublishedClosingTx_opt = None) storing() sending revocation :: closingSigned :: Nil } else { // we are fundee, will wait for their closing_signed @@ -1252,7 +1252,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, remo log.debug("switching to NEGOTIATING spec:\n{}", Commitments.specs2String(commitments1)) if (d.commitments.localParams.isFunder) { // we are funder, need to initiate the negotiation by sending the first closing_signed - val (closingTx, closingSigned) = Closing.makeFirstClosingTx(keyManager, commitments1, localShutdown.scriptPubKey, remoteShutdown.scriptPubKey, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets, closingFeerates) + val (closingTx, closingSigned) = Closing.MutualClose.makeFirstClosingTx(keyManager, commitments1, localShutdown.scriptPubKey, remoteShutdown.scriptPubKey, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets, closingFeerates) goto(NEGOTIATING) using DATA_NEGOTIATING(commitments1, localShutdown, remoteShutdown, List(List(ClosingTxProposed(closingTx, closingSigned))), bestUnpublishedClosingTx_opt = None) storing() sending closingSigned } else { // we are fundee, will wait for their closing_signed @@ -1309,7 +1309,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, remo case Event(c: ClosingSigned, d: DATA_NEGOTIATING) => log.info("received closing fee={}", c.feeSatoshis) val (remoteClosingFee, remoteSig) = (c.feeSatoshis, c.signature) - Closing.checkClosingSignature(keyManager, d.commitments, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey, remoteClosingFee, remoteSig) match { + Closing.MutualClose.checkClosingSignature(keyManager, d.commitments, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey, remoteClosingFee, remoteSig) match { case Right((signedClosingTx, closingSignedRemoteFees)) => val lastLocalClosingSigned_opt = d.closingTxProposed.last.lastOption if (lastLocalClosingSigned_opt.exists(_.localClosingSigned.feeSatoshis == remoteClosingFee)) { @@ -1332,7 +1332,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, remo case Some(ClosingSignedTlv.FeeRange(minFee, maxFee)) if !d.commitments.localParams.isFunder => // if we are fundee and they proposed a fee range, we pick a value in that range and they should accept it without further negotiation // we don't care much about the closing fee since they're paying it (not us) and we can use CPFP if we want to speed up confirmation - val localClosingFees = Closing.firstClosingFee(d.commitments, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) + val localClosingFees = Closing.MutualClose.firstClosingFee(d.commitments, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) if (maxFee < localClosingFees.min) { log.warning("their highest closing fee is below our minimum fee: {} < {}", maxFee, localClosingFees.min) stay() sending Warning(d.channelId, s"closing fee range must not be below ${localClosingFees.min}") @@ -1347,7 +1347,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, remo log.info("accepting their closing fee={}", remoteClosingFee) handleMutualClose(signedClosingTx, Left(d.copy(bestUnpublishedClosingTx_opt = Some(signedClosingTx)))) sending closingSignedRemoteFees } else { - val (closingTx, closingSigned) = Closing.makeClosingTx(keyManager, d.commitments, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey, ClosingFees(closingFee, minFee, maxFee)) + val (closingTx, closingSigned) = Closing.MutualClose.makeClosingTx(keyManager, d.commitments, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey, ClosingFees(closingFee, minFee, maxFee)) log.info("proposing closing fee={} in their fee range (min={} max={})", closingSigned.feeSatoshis, minFee, maxFee) val closingTxProposed1 = (d.closingTxProposed: @unchecked) match { case previousNegotiations :+ currentNegotiation => previousNegotiations :+ (currentNegotiation :+ ClosingTxProposed(closingTx, closingSigned)) @@ -1359,9 +1359,9 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, remo val lastLocalClosingFee_opt = lastLocalClosingSigned_opt.map(_.localClosingSigned.feeSatoshis) val (closingTx, closingSigned) = { // if we are fundee and we were waiting for them to send their first closing_signed, we don't have a lastLocalClosingFee, so we compute a firstClosingFee - val localClosingFees = Closing.firstClosingFee(d.commitments, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) - val nextPreferredFee = Closing.nextClosingFee(lastLocalClosingFee_opt.getOrElse(localClosingFees.preferred), remoteClosingFee) - Closing.makeClosingTx(keyManager, d.commitments, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey, localClosingFees.copy(preferred = nextPreferredFee)) + val localClosingFees = Closing.MutualClose.firstClosingFee(d.commitments, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) + val nextPreferredFee = Closing.MutualClose.nextClosingFee(lastLocalClosingFee_opt.getOrElse(localClosingFees.preferred), remoteClosingFee) + Closing.MutualClose.makeClosingTx(keyManager, d.commitments, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey, localClosingFees.copy(preferred = nextPreferredFee)) } val closingTxProposed1 = (d.closingTxProposed: @unchecked) match { case previousNegotiations :+ currentNegotiation => previousNegotiations :+ (currentNegotiation :+ ClosingTxProposed(closingTx, closingSigned)) @@ -1405,7 +1405,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, remo handleCommandError(ClosingAlreadyInProgress(d.channelId), c) } else { log.info("updating our closing feerates: {}", feerates) - val (closingTx, closingSigned) = Closing.makeFirstClosingTx(keyManager, d.commitments, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets, Some(feerates)) + val (closingTx, closingSigned) = Closing.MutualClose.makeFirstClosingTx(keyManager, d.commitments, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets, Some(feerates)) val closingTxProposed1 = d.closingTxProposed match { case previousNegotiations :+ currentNegotiation => previousNegotiations :+ (currentNegotiation :+ ClosingTxProposed(closingTx, closingSigned)) case previousNegotiations => previousNegotiations :+ List(ClosingTxProposed(closingTx, closingSigned)) @@ -1421,17 +1421,18 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, remo }) when(CLOSING)(handleExceptions { - case Event(c: CMD_FULFILL_HTLC, d: DATA_CLOSING) => - Commitments.sendFulfill(d.commitments, c) match { + case Event(c: HtlcSettlementCommand, d: DATA_CLOSING) => + (c match { + case c: CMD_FULFILL_HTLC => Commitments.sendFulfill(d.commitments, c) + case c: CMD_FAIL_HTLC => Commitments.sendFail(d.commitments, c, nodeParams.privateKey) + case c: CMD_FAIL_MALFORMED_HTLC => Commitments.sendFailMalformed(d.commitments, c) + }) match { case Right((commitments1, _)) => - log.info("got valid payment preimage, recalculating transactions to redeem the corresponding htlc on-chain") - val localCommitPublished1 = d.localCommitPublished.map(localCommitPublished => Helpers.Closing.claimCurrentLocalCommitTxOutputs(keyManager, commitments1, localCommitPublished.commitTx, nodeParams.currentBlockHeight, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets)) - val remoteCommitPublished1 = d.remoteCommitPublished.map(remoteCommitPublished => Helpers.Closing.claimRemoteCommitTxOutputs(keyManager, commitments1, commitments1.remoteCommit, remoteCommitPublished.commitTx, nodeParams.currentBlockHeight, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets)) - val nextRemoteCommitPublished1 = d.nextRemoteCommitPublished.map(remoteCommitPublished => { - require(commitments1.remoteNextCommitInfo.isLeft, "next remote commit must be defined") - val remoteCommit = commitments1.remoteNextCommitInfo.swap.toOption.get.nextRemoteCommit - Helpers.Closing.claimRemoteCommitTxOutputs(keyManager, commitments1, remoteCommit, remoteCommitPublished.commitTx, nodeParams.currentBlockHeight, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) - }) + log.info("got valid settlement for htlc={}, recalculating htlc transactions", c.id) + + val localCommitPublished1 = d.localCommitPublished.map(localCommitPublished => localCommitPublished.copy(htlcTxs = Closing.LocalClose.claimHtlcOutputs(keyManager, commitments1))) + val remoteCommitPublished1 = d.remoteCommitPublished.map(remoteCommitPublished => remoteCommitPublished.copy(claimHtlcTxs = Closing.RemoteClose.claimHtlcOutputs(keyManager, commitments1, commitments1.remoteCommit, nodeParams.onChainFeeConf.feeEstimator))) + val nextRemoteCommitPublished1 = d.nextRemoteCommitPublished.map(remoteCommitPublished => remoteCommitPublished.copy(claimHtlcTxs = Closing.RemoteClose.claimHtlcOutputs(keyManager, commitments1, commitments1.nextRemoteCommit_opt.get, nodeParams.onChainFeeConf.feeEstimator))) def republish(): Unit = { localCommitPublished1.foreach(lcp => doPublish(lcp, commitments1)) @@ -1505,7 +1506,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, remo } } val revokedCommitPublished1 = d.revokedCommitPublished.map { rev => - val (rev1, penaltyTxs) = Closing.claimRevokedHtlcTxOutputs(keyManager, d.commitments, rev, tx, nodeParams.onChainFeeConf.feeEstimator) + val (rev1, penaltyTxs) = Closing.RevokedClose.claimHtlcTxOutputs(keyManager, d.commitments, rev, tx, nodeParams.onChainFeeConf.feeEstimator) penaltyTxs.foreach(claimTx => txPublisher ! PublishFinalTx(claimTx, claimTx.fee, None)) penaltyTxs.foreach(claimTx => blockchain ! WatchOutputSpent(self, tx.txid, claimTx.input.outPoint.index.toInt, hints = Set(claimTx.tx.txid))) rev1 @@ -1519,7 +1520,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, remo val d1 = d.copy( localCommitPublished = d.localCommitPublished.map(localCommitPublished => { // If the tx is one of our HTLC txs, we now publish a 3rd-stage claim-htlc-tx that claims its output. - val (localCommitPublished1, claimHtlcTx_opt) = Closing.claimLocalCommitHtlcTxOutput(localCommitPublished, keyManager, d.commitments, tx, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) + val (localCommitPublished1, claimHtlcTx_opt) = Closing.LocalClose.claimHtlcDelayedOutput(localCommitPublished, keyManager, d.commitments, tx, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) claimHtlcTx_opt.foreach(claimHtlcTx => { txPublisher ! PublishFinalTx(claimHtlcTx, claimHtlcTx.fee, None) blockchain ! WatchTxConfirmed(self, claimHtlcTx.tx.txid, nodeParams.channelConf.minDepthBlocks) @@ -1804,7 +1805,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, remo // note: in any case we still need to keep all previously sent closing_signed, because they may publish one of them if (d.commitments.localParams.isFunder) { // we could use the last closing_signed we sent, but network fees may have changed while we were offline so it is better to restart from scratch - val (closingTx, closingSigned) = Closing.makeFirstClosingTx(keyManager, d.commitments, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets, None) + val (closingTx, closingSigned) = Closing.MutualClose.makeFirstClosingTx(keyManager, d.commitments, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets, None) val closingTxProposed1 = d.closingTxProposed :+ List(ClosingTxProposed(closingTx, closingSigned)) goto(NEGOTIATING) using d.copy(closingTxProposed = closingTxProposed1) storing() sending d.localShutdown :: closingSigned :: Nil } else { @@ -2434,7 +2435,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, remo stay() } else { val commitTx = d.commitments.fullySignedLocalCommitTx(keyManager).tx - val localCommitPublished = Helpers.Closing.claimCurrentLocalCommitTxOutputs(keyManager, d.commitments, commitTx, nodeParams.currentBlockHeight, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) + val localCommitPublished = Closing.LocalClose.claimCommitTxOutputs(keyManager, d.commitments, commitTx, nodeParams.currentBlockHeight, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) val nextData = d match { case closing: DATA_CLOSING => closing.copy(localCommitPublished = Some(localCommitPublished)) case negotiating: DATA_NEGOTIATING => DATA_CLOSING(d.commitments, fundingTx = None, waitingSince = nodeParams.currentBlockHeight, negotiating.closingTxProposed.flatten.map(_.unsignedTx), localCommitPublished = Some(localCommitPublished)) @@ -2512,7 +2513,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, remo require(commitTx.txid == d.commitments.remoteCommit.txid, "txid mismatch") context.system.eventStream.publish(TransactionPublished(d.channelId, remoteNodeId, commitTx, Closing.commitTxFee(d.commitments.commitInput, commitTx, d.commitments.localParams.isFunder), "remote-commit")) - val remoteCommitPublished = Helpers.Closing.claimRemoteCommitTxOutputs(keyManager, d.commitments, d.commitments.remoteCommit, commitTx, nodeParams.currentBlockHeight, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) + val remoteCommitPublished = Closing.RemoteClose.claimCommitTxOutputs(keyManager, d.commitments, d.commitments.remoteCommit, commitTx, nodeParams.currentBlockHeight, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) val nextData = d match { case closing: DATA_CLOSING => closing.copy(remoteCommitPublished = Some(remoteCommitPublished)) case negotiating: DATA_NEGOTIATING => DATA_CLOSING(d.commitments, fundingTx = None, waitingSince = nodeParams.currentBlockHeight, negotiating.closingTxProposed.flatten.map(_.unsignedTx), remoteCommitPublished = Some(remoteCommitPublished)) @@ -2528,7 +2529,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, remo val remotePerCommitmentPoint = d.remoteChannelReestablish.myCurrentPerCommitmentPoint val remoteCommitPublished = RemoteCommitPublished( commitTx = commitTx, - claimMainOutputTx = Helpers.Closing.claimRemoteCommitMainOutput(keyManager, d.commitments, remotePerCommitmentPoint, commitTx, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets), + claimMainOutputTx = Closing.RemoteClose.claimMainOutput(keyManager, d.commitments, remotePerCommitmentPoint, commitTx, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets), claimHtlcTxs = Map.empty, claimAnchorTxs = List.empty, irrevocablySpent = Map.empty) @@ -2544,7 +2545,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, remo require(commitTx.txid == remoteCommit.txid, "txid mismatch") context.system.eventStream.publish(TransactionPublished(d.channelId, remoteNodeId, commitTx, Closing.commitTxFee(d.commitments.commitInput, commitTx, d.commitments.localParams.isFunder), "next-remote-commit")) - val remoteCommitPublished = Helpers.Closing.claimRemoteCommitTxOutputs(keyManager, d.commitments, remoteCommit, commitTx, nodeParams.currentBlockHeight, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) + val remoteCommitPublished = Closing.RemoteClose.claimCommitTxOutputs(keyManager, d.commitments, remoteCommit, commitTx, nodeParams.currentBlockHeight, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) val nextData = d match { case closing: DATA_CLOSING => closing.copy(nextRemoteCommitPublished = Some(remoteCommitPublished)) case negotiating: DATA_NEGOTIATING => DATA_CLOSING(d.commitments, fundingTx = None, waitingSince = nodeParams.currentBlockHeight, negotiating.closingTxProposed.flatten.map(_.unsignedTx), nextRemoteCommitPublished = Some(remoteCommitPublished)) @@ -2574,7 +2575,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, remo private def handleRemoteSpentOther(tx: Transaction, d: HasCommitments) = { log.warning(s"funding tx spent in txid=${tx.txid}") - Helpers.Closing.claimRevokedRemoteCommitTxOutputs(keyManager, d.commitments, tx, nodeParams.db.channels, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) match { + Closing.RevokedClose.claimCommitTxOutputs(keyManager, d.commitments, tx, nodeParams.db.channels, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) match { case Some(revokedCommitPublished) => log.warning(s"txid=${tx.txid} was a revoked commitment, publishing the penalty tx") context.system.eventStream.publish(TransactionPublished(d.channelId, remoteNodeId, tx, Closing.commitTxFee(d.commitments.commitInput, tx, d.commitments.localParams.isFunder), "revoked-commit")) @@ -2622,7 +2623,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, remo // let's try to spend our current local tx val commitTx = d.commitments.fullySignedLocalCommitTx(keyManager).tx - val localCommitPublished = Helpers.Closing.claimCurrentLocalCommitTxOutputs(keyManager, d.commitments, commitTx, nodeParams.currentBlockHeight, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) + val localCommitPublished = Closing.LocalClose.claimCommitTxOutputs(keyManager, d.commitments, commitTx, nodeParams.currentBlockHeight, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) goto(ERR_INFORMATION_LEAK) calling doPublish(localCommitPublished, d.commitments) sending error } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala index 5b483b21b5..1b49b9a18e 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala @@ -85,6 +85,8 @@ case class Commitments(channelId: ByteVector32, require(channelFeatures.paysDirectlyToWallet == localParams.walletStaticPaymentBasepoint.isDefined, s"localParams.walletStaticPaymentBasepoint must be defined only for commitments that pay directly to our wallet (channel features: $channelFeatures") + def nextRemoteCommit_opt: Option[RemoteCommit] = remoteNextCommitInfo.swap.toOption.map(_.nextRemoteCommit) + /** * * @param scriptPubKey optional local script pubkey provided in CMD_CLOSE @@ -95,7 +97,7 @@ case class Commitments(channelId: ByteVector32, val allowAnySegwit = Features.canUseFeature(localParams.initFeatures, remoteParams.initFeatures, Features.ShutdownAnySegwit) (channelFeatures.hasFeature(Features.UpfrontShutdownScript), scriptPubKey) match { case (true, Some(script)) if script != localParams.defaultFinalScriptPubKey => Left(InvalidFinalScript(channelId)) - case (false, Some(script)) if !Closing.isValidFinalScriptPubkey(script, allowAnySegwit) => Left(InvalidFinalScript(channelId)) + case (false, Some(script)) if !Closing.MutualClose.isValidFinalScriptPubkey(script, allowAnySegwit) => Left(InvalidFinalScript(channelId)) case (false, Some(script)) => Right(script) case _ => Right(localParams.defaultFinalScriptPubKey) } @@ -110,9 +112,9 @@ case class Commitments(channelId: ByteVector32, // to check whether shutdown_any_segwit is active we check features in local and remote parameters, which are negotiated each time we connect to our peer. val allowAnySegwit = Features.canUseFeature(localParams.initFeatures, remoteParams.initFeatures, Features.ShutdownAnySegwit) (channelFeatures.hasFeature(Features.UpfrontShutdownScript), remoteParams.shutdownScript) match { - case (false, _) if !Closing.isValidFinalScriptPubkey(remoteScriptPubKey, allowAnySegwit) => Left(InvalidFinalScript(channelId)) + case (false, _) if !Closing.MutualClose.isValidFinalScriptPubkey(remoteScriptPubKey, allowAnySegwit) => Left(InvalidFinalScript(channelId)) case (false, _) => Right(remoteScriptPubKey) - case (true, None) if !Closing.isValidFinalScriptPubkey(remoteScriptPubKey, allowAnySegwit) => + case (true, None) if !Closing.MutualClose.isValidFinalScriptPubkey(remoteScriptPubKey, allowAnySegwit) => // this is a special case: they set option_upfront_shutdown_script but did not provide a script in their open/accept message Left(InvalidFinalScript(channelId)) case (true, None) => Right(remoteScriptPubKey) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala index 1fef6b68ff..dd3a21883e 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala @@ -89,7 +89,7 @@ object Helpers { (hasOptionUpfrontShutdownScript, upfrontShutdownScript_opt) match { case (true, None) => Left(MissingUpfrontShutdownScript(channelId)) case (true, Some(script)) if script.isEmpty => Right(None) // but the provided script can be empty - case (true, Some(script)) if !Closing.isValidFinalScriptPubkey(script, allowAnySegwit) => Left(InvalidFinalScript(channelId)) + case (true, Some(script)) if !Closing.MutualClose.isValidFinalScriptPubkey(script, allowAnySegwit) => Left(InvalidFinalScript(channelId)) case (true, Some(script)) => Right(Some(script)) case (false, Some(_)) => Right(None) // they provided a script but the feature is not active, we just ignore it case _ => Right(None) @@ -499,108 +499,111 @@ object Helpers { case _ => None } - // used only to compute tx weights and estimate fees - lazy val dummyPublicKey = PrivateKey(ByteVector32(ByteVector.fill(32)(1))).publicKey + object MutualClose { - def isValidFinalScriptPubkey(scriptPubKey: ByteVector, allowAnySegwit: Boolean): Boolean = { - Try(Script.parse(scriptPubKey)) match { - case Success(OP_DUP :: OP_HASH160 :: OP_PUSHDATA(pubkeyHash, _) :: OP_EQUALVERIFY :: OP_CHECKSIG :: Nil) if pubkeyHash.size == 20 => true - case Success(OP_HASH160 :: OP_PUSHDATA(scriptHash, _) :: OP_EQUAL :: Nil) if scriptHash.size == 20 => true - case Success(OP_0 :: OP_PUSHDATA(pubkeyHash, _) :: Nil) if pubkeyHash.size == 20 => true - case Success(OP_0 :: OP_PUSHDATA(scriptHash, _) :: Nil) if scriptHash.size == 32 => true - case Success((OP_1 | OP_2 | OP_3 | OP_4 | OP_5 | OP_6 | OP_7 | OP_8 | OP_9 | OP_10 | OP_11 | OP_12 | OP_13 | OP_14 | OP_15 | OP_16) :: OP_PUSHDATA(program, _) :: Nil) if allowAnySegwit && 2 <= program.length && program.length <= 40 => true - case _ => false + // used only to compute tx weights and estimate fees + lazy val dummyPublicKey = PrivateKey(ByteVector32(ByteVector.fill(32)(1))).publicKey + + def isValidFinalScriptPubkey(scriptPubKey: ByteVector, allowAnySegwit: Boolean): Boolean = { + Try(Script.parse(scriptPubKey)) match { + case Success(OP_DUP :: OP_HASH160 :: OP_PUSHDATA(pubkeyHash, _) :: OP_EQUALVERIFY :: OP_CHECKSIG :: Nil) if pubkeyHash.size == 20 => true + case Success(OP_HASH160 :: OP_PUSHDATA(scriptHash, _) :: OP_EQUAL :: Nil) if scriptHash.size == 20 => true + case Success(OP_0 :: OP_PUSHDATA(pubkeyHash, _) :: Nil) if pubkeyHash.size == 20 => true + case Success(OP_0 :: OP_PUSHDATA(scriptHash, _) :: Nil) if scriptHash.size == 32 => true + case Success((OP_1 | OP_2 | OP_3 | OP_4 | OP_5 | OP_6 | OP_7 | OP_8 | OP_9 | OP_10 | OP_11 | OP_12 | OP_13 | OP_14 | OP_15 | OP_16) :: OP_PUSHDATA(program, _) :: Nil) if allowAnySegwit && 2 <= program.length && program.length <= 40 => true + case _ => false + } } - } - def firstClosingFee(commitments: Commitments, localScriptPubkey: ByteVector, remoteScriptPubkey: ByteVector, feerates: ClosingFeerates)(implicit log: LoggingAdapter): ClosingFees = { - import commitments._ - // this is just to estimate the weight, it depends on size of the pubkey scripts - val actualLocalScript = if (channelFeatures.hasFeature(Features.UpfrontShutdownScript)) localParams.defaultFinalScriptPubKey else localScriptPubkey - val actualRemoteScript = if (channelFeatures.hasFeature(Features.UpfrontShutdownScript)) remoteParams.shutdownScript.getOrElse(remoteScriptPubkey) else remoteScriptPubkey - val dummyClosingTx = Transactions.makeClosingTx(commitInput, actualLocalScript, actualRemoteScript, localParams.isFunder, Satoshi(0), Satoshi(0), localCommit.spec) - val closingWeight = Transaction.weight(Transactions.addSigs(dummyClosingTx, dummyPublicKey, remoteParams.fundingPubKey, Transactions.PlaceHolderSig, Transactions.PlaceHolderSig).tx) - log.info(s"using feerates=$feerates for initial closing tx") - feerates.computeFees(closingWeight) - } + def firstClosingFee(commitments: Commitments, localScriptPubkey: ByteVector, remoteScriptPubkey: ByteVector, feerates: ClosingFeerates)(implicit log: LoggingAdapter): ClosingFees = { + import commitments._ + // this is just to estimate the weight, it depends on size of the pubkey scripts + val actualLocalScript = if (channelFeatures.hasFeature(Features.UpfrontShutdownScript)) localParams.defaultFinalScriptPubKey else localScriptPubkey + val actualRemoteScript = if (channelFeatures.hasFeature(Features.UpfrontShutdownScript)) remoteParams.shutdownScript.getOrElse(remoteScriptPubkey) else remoteScriptPubkey + val dummyClosingTx = Transactions.makeClosingTx(commitInput, actualLocalScript, actualRemoteScript, localParams.isFunder, Satoshi(0), Satoshi(0), localCommit.spec) + val closingWeight = Transaction.weight(Transactions.addSigs(dummyClosingTx, dummyPublicKey, remoteParams.fundingPubKey, Transactions.PlaceHolderSig, Transactions.PlaceHolderSig).tx) + log.info(s"using feerates=$feerates for initial closing tx") + feerates.computeFees(closingWeight) + } - def firstClosingFee(commitments: Commitments, localScriptPubkey: ByteVector, remoteScriptPubkey: ByteVector, feeEstimator: FeeEstimator, feeTargets: FeeTargets)(implicit log: LoggingAdapter): ClosingFees = { - val requestedFeerate = feeEstimator.getFeeratePerKw(feeTargets.mutualCloseBlockTarget) - val preferredFeerate = commitments.commitmentFormat match { - case DefaultCommitmentFormat => - // we "MUST set fee_satoshis less than or equal to the base fee of the final commitment transaction" - requestedFeerate.min(commitments.localCommit.spec.commitTxFeerate) - case _: AnchorOutputsCommitmentFormat => requestedFeerate + def firstClosingFee(commitments: Commitments, localScriptPubkey: ByteVector, remoteScriptPubkey: ByteVector, feeEstimator: FeeEstimator, feeTargets: FeeTargets)(implicit log: LoggingAdapter): ClosingFees = { + val requestedFeerate = feeEstimator.getFeeratePerKw(feeTargets.mutualCloseBlockTarget) + val preferredFeerate = commitments.commitmentFormat match { + case DefaultCommitmentFormat => + // we "MUST set fee_satoshis less than or equal to the base fee of the final commitment transaction" + requestedFeerate.min(commitments.localCommit.spec.commitTxFeerate) + case _: AnchorOutputsCommitmentFormat => requestedFeerate + } + // NB: we choose a minimum fee that ensures the tx will easily propagate while allowing low fees since we can + // always use CPFP to speed up confirmation if necessary. + val closingFeerates = ClosingFeerates(preferredFeerate, preferredFeerate.min(feeEstimator.getFeeratePerKw(1008)), preferredFeerate * 2) + firstClosingFee(commitments, localScriptPubkey, remoteScriptPubkey, closingFeerates) } - // NB: we choose a minimum fee that ensures the tx will easily propagate while allowing low fees since we can - // always use CPFP to speed up confirmation if necessary. - val closingFeerates = ClosingFeerates(preferredFeerate, preferredFeerate.min(feeEstimator.getFeeratePerKw(1008)), preferredFeerate * 2) - firstClosingFee(commitments, localScriptPubkey, remoteScriptPubkey, closingFeerates) - } - def nextClosingFee(localClosingFee: Satoshi, remoteClosingFee: Satoshi): Satoshi = ((localClosingFee + remoteClosingFee) / 4) * 2 + def nextClosingFee(localClosingFee: Satoshi, remoteClosingFee: Satoshi): Satoshi = ((localClosingFee + remoteClosingFee) / 4) * 2 - def makeFirstClosingTx(keyManager: ChannelKeyManager, commitments: Commitments, localScriptPubkey: ByteVector, remoteScriptPubkey: ByteVector, feeEstimator: FeeEstimator, feeTargets: FeeTargets, closingFeerates_opt: Option[ClosingFeerates])(implicit log: LoggingAdapter): (ClosingTx, ClosingSigned) = { - val closingFees = closingFeerates_opt match { - case Some(closingFeerates) => firstClosingFee(commitments, localScriptPubkey, remoteScriptPubkey, closingFeerates) - case None => firstClosingFee(commitments, localScriptPubkey, remoteScriptPubkey, feeEstimator, feeTargets) + def makeFirstClosingTx(keyManager: ChannelKeyManager, commitments: Commitments, localScriptPubkey: ByteVector, remoteScriptPubkey: ByteVector, feeEstimator: FeeEstimator, feeTargets: FeeTargets, closingFeerates_opt: Option[ClosingFeerates])(implicit log: LoggingAdapter): (ClosingTx, ClosingSigned) = { + val closingFees = closingFeerates_opt match { + case Some(closingFeerates) => firstClosingFee(commitments, localScriptPubkey, remoteScriptPubkey, closingFeerates) + case None => firstClosingFee(commitments, localScriptPubkey, remoteScriptPubkey, feeEstimator, feeTargets) + } + makeClosingTx(keyManager, commitments, localScriptPubkey, remoteScriptPubkey, closingFees) } - makeClosingTx(keyManager, commitments, localScriptPubkey, remoteScriptPubkey, closingFees) - } - def makeClosingTx(keyManager: ChannelKeyManager, commitments: Commitments, localScriptPubkey: ByteVector, remoteScriptPubkey: ByteVector, closingFees: ClosingFees)(implicit log: LoggingAdapter): (ClosingTx, ClosingSigned) = { - import commitments._ - val actualLocalScript = if (channelFeatures.hasFeature(Features.UpfrontShutdownScript)) localParams.defaultFinalScriptPubKey else localScriptPubkey - val actualRemoteScript = if (channelFeatures.hasFeature(Features.UpfrontShutdownScript)) remoteParams.shutdownScript.getOrElse(remoteScriptPubkey) else remoteScriptPubkey - val allowAnySegwit = Features.canUseFeature(commitments.localParams.initFeatures, commitments.remoteParams.initFeatures, Features.ShutdownAnySegwit) - require(isValidFinalScriptPubkey(actualLocalScript, allowAnySegwit), "invalid localScriptPubkey") - require(isValidFinalScriptPubkey(actualRemoteScript, allowAnySegwit), "invalid remoteScriptPubkey") - log.debug("making closing tx with closing fee={} and commitments:\n{}", closingFees.preferred, Commitments.specs2String(commitments)) - val dustLimit = localParams.dustLimit.max(remoteParams.dustLimit) - val closingTx = Transactions.makeClosingTx(commitInput, actualLocalScript, actualRemoteScript, localParams.isFunder, dustLimit, closingFees.preferred, localCommit.spec) - val localClosingSig = keyManager.sign(closingTx, keyManager.fundingPublicKey(commitments.localParams.fundingKeyPath), TxOwner.Local, commitmentFormat) - val closingSigned = ClosingSigned(channelId, closingFees.preferred, localClosingSig, TlvStream(ClosingSignedTlv.FeeRange(closingFees.min, closingFees.max))) - log.info(s"signed closing txid=${closingTx.tx.txid} with closing fee=${closingSigned.feeSatoshis}") - log.debug(s"closingTxid=${closingTx.tx.txid} closingTx=${closingTx.tx}}") - (closingTx, closingSigned) - } + def makeClosingTx(keyManager: ChannelKeyManager, commitments: Commitments, localScriptPubkey: ByteVector, remoteScriptPubkey: ByteVector, closingFees: ClosingFees)(implicit log: LoggingAdapter): (ClosingTx, ClosingSigned) = { + import commitments._ + val actualLocalScript = if (channelFeatures.hasFeature(Features.UpfrontShutdownScript)) localParams.defaultFinalScriptPubKey else localScriptPubkey + val actualRemoteScript = if (channelFeatures.hasFeature(Features.UpfrontShutdownScript)) remoteParams.shutdownScript.getOrElse(remoteScriptPubkey) else remoteScriptPubkey + val allowAnySegwit = Features.canUseFeature(commitments.localParams.initFeatures, commitments.remoteParams.initFeatures, Features.ShutdownAnySegwit) + require(isValidFinalScriptPubkey(actualLocalScript, allowAnySegwit), "invalid localScriptPubkey") + require(isValidFinalScriptPubkey(actualRemoteScript, allowAnySegwit), "invalid remoteScriptPubkey") + log.debug("making closing tx with closing fee={} and commitments:\n{}", closingFees.preferred, Commitments.specs2String(commitments)) + val dustLimit = localParams.dustLimit.max(remoteParams.dustLimit) + val closingTx = Transactions.makeClosingTx(commitInput, actualLocalScript, actualRemoteScript, localParams.isFunder, dustLimit, closingFees.preferred, localCommit.spec) + val localClosingSig = keyManager.sign(closingTx, keyManager.fundingPublicKey(commitments.localParams.fundingKeyPath), TxOwner.Local, commitmentFormat) + val closingSigned = ClosingSigned(channelId, closingFees.preferred, localClosingSig, TlvStream(ClosingSignedTlv.FeeRange(closingFees.min, closingFees.max))) + log.info(s"signed closing txid=${closingTx.tx.txid} with closing fee=${closingSigned.feeSatoshis}") + log.debug(s"closingTxid=${closingTx.tx.txid} closingTx=${closingTx.tx}}") + (closingTx, closingSigned) + } - def checkClosingSignature(keyManager: ChannelKeyManager, commitments: Commitments, localScriptPubkey: ByteVector, remoteScriptPubkey: ByteVector, remoteClosingFee: Satoshi, remoteClosingSig: ByteVector64)(implicit log: LoggingAdapter): Either[ChannelException, (ClosingTx, ClosingSigned)] = { - import commitments._ - val lastCommitFeeSatoshi = commitments.commitInput.txOut.amount - commitments.localCommit.commitTxAndRemoteSig.commitTx.tx.txOut.map(_.amount).sum - if (remoteClosingFee > lastCommitFeeSatoshi && commitments.commitmentFormat == DefaultCommitmentFormat) { - log.error(s"remote proposed a commit fee higher than the last commitment fee: remote closing fee=${remoteClosingFee.toLong} last commit fees=$lastCommitFeeSatoshi") - Left(InvalidCloseFee(commitments.channelId, remoteClosingFee)) - } else { - val (closingTx, closingSigned) = makeClosingTx(keyManager, commitments, localScriptPubkey, remoteScriptPubkey, ClosingFees(remoteClosingFee, remoteClosingFee, remoteClosingFee)) - if (checkClosingDustAmounts(closingTx)) { - val signedClosingTx = Transactions.addSigs(closingTx, keyManager.fundingPublicKey(commitments.localParams.fundingKeyPath).publicKey, remoteParams.fundingPubKey, closingSigned.signature, remoteClosingSig) - Transactions.checkSpendable(signedClosingTx) match { - case Success(_) => Right(signedClosingTx, closingSigned) - case _ => Left(InvalidCloseSignature(commitments.channelId, signedClosingTx.tx)) - } + def checkClosingSignature(keyManager: ChannelKeyManager, commitments: Commitments, localScriptPubkey: ByteVector, remoteScriptPubkey: ByteVector, remoteClosingFee: Satoshi, remoteClosingSig: ByteVector64)(implicit log: LoggingAdapter): Either[ChannelException, (ClosingTx, ClosingSigned)] = { + import commitments._ + val lastCommitFeeSatoshi = commitments.commitInput.txOut.amount - commitments.localCommit.commitTxAndRemoteSig.commitTx.tx.txOut.map(_.amount).sum + if (remoteClosingFee > lastCommitFeeSatoshi && commitments.commitmentFormat == DefaultCommitmentFormat) { + log.error(s"remote proposed a commit fee higher than the last commitment fee: remote closing fee=${remoteClosingFee.toLong} last commit fees=$lastCommitFeeSatoshi") + Left(InvalidCloseFee(commitments.channelId, remoteClosingFee)) } else { - Left(InvalidCloseAmountBelowDust(commitments.channelId, closingTx.tx)) + val (closingTx, closingSigned) = makeClosingTx(keyManager, commitments, localScriptPubkey, remoteScriptPubkey, ClosingFees(remoteClosingFee, remoteClosingFee, remoteClosingFee)) + if (checkClosingDustAmounts(closingTx)) { + val signedClosingTx = Transactions.addSigs(closingTx, keyManager.fundingPublicKey(commitments.localParams.fundingKeyPath).publicKey, remoteParams.fundingPubKey, closingSigned.signature, remoteClosingSig) + Transactions.checkSpendable(signedClosingTx) match { + case Success(_) => Right(signedClosingTx, closingSigned) + case _ => Left(InvalidCloseSignature(commitments.channelId, signedClosingTx.tx)) + } + } else { + Left(InvalidCloseAmountBelowDust(commitments.channelId, closingTx.tx)) + } } } - } - /** - * Check that all closing outputs are above bitcoin's dust limit for their script type, otherwise there is a risk - * that the closing transaction will not be relayed to miners' mempool and will not confirm. - * The various dust limits are detailed in https://github.com/lightningnetwork/lightning-rfc/blob/master/03-transactions.md#dust-limits - */ - def checkClosingDustAmounts(closingTx: ClosingTx): Boolean = { - closingTx.tx.txOut.forall(txOut => { - Try(Script.parse(txOut.publicKeyScript)) match { - case Success(OP_DUP :: OP_HASH160 :: OP_PUSHDATA(pubkeyHash, _) :: OP_EQUALVERIFY :: OP_CHECKSIG :: Nil) if pubkeyHash.size == 20 => txOut.amount >= 546.sat - case Success(OP_HASH160 :: OP_PUSHDATA(scriptHash, _) :: OP_EQUAL :: Nil) if scriptHash.size == 20 => txOut.amount >= 540.sat - case Success(OP_0 :: OP_PUSHDATA(pubkeyHash, _) :: Nil) if pubkeyHash.size == 20 => txOut.amount >= 294.sat - case Success(OP_0 :: OP_PUSHDATA(scriptHash, _) :: Nil) if scriptHash.size == 32 => txOut.amount >= 330.sat - case Success((OP_1 | OP_2 | OP_3 | OP_4 | OP_5 | OP_6 | OP_7 | OP_8 | OP_9 | OP_10 | OP_11 | OP_12 | OP_13 | OP_14 | OP_15 | OP_16) :: OP_PUSHDATA(program, _) :: Nil) if 2 <= program.length && program.length <= 40 => txOut.amount >= 354.sat - case _ => txOut.amount >= 546.sat - } - }) + /** + * Check that all closing outputs are above bitcoin's dust limit for their script type, otherwise there is a risk + * that the closing transaction will not be relayed to miners' mempool and will not confirm. + * The various dust limits are detailed in https://github.com/lightningnetwork/lightning-rfc/blob/master/03-transactions.md#dust-limits + */ + def checkClosingDustAmounts(closingTx: ClosingTx): Boolean = { + closingTx.tx.txOut.forall(txOut => { + Try(Script.parse(txOut.publicKeyScript)) match { + case Success(OP_DUP :: OP_HASH160 :: OP_PUSHDATA(pubkeyHash, _) :: OP_EQUALVERIFY :: OP_CHECKSIG :: Nil) if pubkeyHash.size == 20 => txOut.amount >= 546.sat + case Success(OP_HASH160 :: OP_PUSHDATA(scriptHash, _) :: OP_EQUAL :: Nil) if scriptHash.size == 20 => txOut.amount >= 540.sat + case Success(OP_0 :: OP_PUSHDATA(pubkeyHash, _) :: Nil) if pubkeyHash.size == 20 => txOut.amount >= 294.sat + case Success(OP_0 :: OP_PUSHDATA(scriptHash, _) :: Nil) if scriptHash.size == 32 => txOut.amount >= 330.sat + case Success((OP_1 | OP_2 | OP_3 | OP_4 | OP_5 | OP_6 | OP_7 | OP_8 | OP_9 | OP_10 | OP_11 | OP_12 | OP_13 | OP_14 | OP_15 | OP_16) :: OP_PUSHDATA(program, _) :: Nil) if 2 <= program.length && program.length <= 40 => txOut.amount >= 354.sat + case _ => txOut.amount >= 546.sat + } + }) + } } /** Wraps transaction generation in a Try and filters failures to avoid one transaction negatively impacting a whole commitment. */ @@ -627,339 +630,278 @@ object Helpers { if (isFunder) commitInput.txOut.amount - commitTx.txOut.map(_.amount).sum else 0 sat } - /** - * Claim all the HTLCs that we've received from our current commit tx. This will be done using 2nd stage HTLC transactions. - * - * @param commitments our commitment data, which include payment preimages - * @return a list of transactions (one per output of the commit tx that we can claim) - */ - def claimCurrentLocalCommitTxOutputs(keyManager: ChannelKeyManager, commitments: Commitments, tx: Transaction, currentBlockHeight: BlockHeight, feeEstimator: FeeEstimator, feeTargets: FeeTargets)(implicit log: LoggingAdapter): LocalCommitPublished = { - import commitments._ - require(localCommit.commitTxAndRemoteSig.commitTx.tx.txid == tx.txid, "txid mismatch, provided tx is not the current local commit tx") - val channelKeyPath = keyManager.keyPath(localParams, channelConfig) - val localPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, commitments.localCommit.index.toInt) - val localRevocationPubkey = Generators.revocationPubKey(remoteParams.revocationBasepoint, localPerCommitmentPoint) - val localDelayedPubkey = Generators.derivePubKey(keyManager.delayedPaymentPoint(channelKeyPath).publicKey, localPerCommitmentPoint) - val localFundingPubKey = keyManager.fundingPublicKey(commitments.localParams.fundingKeyPath).publicKey - val feeratePerKwDelayed = feeEstimator.getFeeratePerKw(feeTargets.claimMainBlockTarget) - - // first we will claim our main output as soon as the delay is over - val mainDelayedTx = withTxGenerationLog("local-main-delayed") { - Transactions.makeClaimLocalDelayedOutputTx(tx, localParams.dustLimit, localRevocationPubkey, remoteParams.toSelfDelay, localDelayedPubkey, localParams.defaultFinalScriptPubKey, feeratePerKwDelayed).map(claimDelayed => { - val sig = keyManager.sign(claimDelayed, keyManager.delayedPaymentPoint(channelKeyPath), localPerCommitmentPoint, TxOwner.Local, commitmentFormat) - Transactions.addSigs(claimDelayed, sig) - }) - } - - // those are the preimages to existing received htlcs - val preimages = commitments.localChanges.all.collect { case u: UpdateFulfillHtlc => u.paymentPreimage }.map(r => Crypto.sha256(r) -> r).toMap - - val htlcTxs: Map[OutPoint, Option[HtlcTx]] = localCommit.htlcTxsAndRemoteSigs.collect { - case HtlcTxAndRemoteSig(txInfo@HtlcSuccessTx(_, _, paymentHash, _, _), remoteSig) => - if (preimages.contains(paymentHash)) { - // incoming htlc for which we have the preimage: we can spend it immediately - txInfo.input.outPoint -> withTxGenerationLog("htlc-success") { - val localSig = keyManager.sign(txInfo, keyManager.htlcPoint(channelKeyPath), localPerCommitmentPoint, TxOwner.Local, commitmentFormat) - Right(Transactions.addSigs(txInfo, localSig, remoteSig, preimages(paymentHash), commitmentFormat)) - } - } else { - // incoming htlc for which we don't have the preimage: we can't spend it immediately, but we may learn the - // preimage later, otherwise it will eventually timeout and they will get their funds back - txInfo.input.outPoint -> None - } - case HtlcTxAndRemoteSig(txInfo: HtlcTimeoutTx, remoteSig) => - // outgoing htlc: they may or may not have the preimage, the only thing to do is try to get back our funds after timeout - txInfo.input.outPoint -> withTxGenerationLog("htlc-timeout") { - val localSig = keyManager.sign(txInfo, keyManager.htlcPoint(channelKeyPath), localPerCommitmentPoint, TxOwner.Local, commitmentFormat) - Right(Transactions.addSigs(txInfo, localSig, remoteSig, commitmentFormat)) - } - }.toMap - - // If we don't have pending HTLCs, we don't have funds at risk, so we can aim for a slower confirmation. - val confirmCommitBefore = htlcTxs.values.flatten.map(htlcTx => htlcTx.confirmBefore).minOption.getOrElse(currentBlockHeight + feeTargets.commitmentWithoutHtlcsBlockTarget) - val claimAnchorTxs: List[ClaimAnchorOutputTx] = List( - withTxGenerationLog("local-anchor") { - Transactions.makeClaimLocalAnchorOutputTx(tx, localFundingPubKey, confirmCommitBefore) - }, - withTxGenerationLog("remote-anchor") { - Transactions.makeClaimRemoteAnchorOutputTx(tx, commitments.remoteParams.fundingPubKey) - } - ).flatten - - LocalCommitPublished( - commitTx = tx, - claimMainDelayedOutputTx = mainDelayedTx, - htlcTxs = htlcTxs, - claimHtlcDelayedTxs = Nil, // we will claim these once the htlc txs are confirmed - claimAnchorTxs = claimAnchorTxs, - irrevocablySpent = Map.empty) - } + object LocalClose { - /** - * Claim the output of a 2nd-stage HTLC transaction. If the provided transaction isn't an htlc, this will be a no-op. - * - * NB: with anchor outputs, it's possible to have transactions that spend *many* HTLC outputs at once, but we're not - * doing that because it introduces a lot of subtle edge cases. - */ - def claimLocalCommitHtlcTxOutput(localCommitPublished: LocalCommitPublished, keyManager: ChannelKeyManager, commitments: Commitments, tx: Transaction, feeEstimator: FeeEstimator, feeTargets: FeeTargets)(implicit log: LoggingAdapter): (LocalCommitPublished, Option[TransactionWithInputInfo]) = { - import commitments._ - if (isHtlcSuccess(tx, localCommitPublished) || isHtlcTimeout(tx, localCommitPublished)) { - val feeratePerKwDelayed = feeEstimator.getFeeratePerKw(feeTargets.claimMainBlockTarget) + /** + * Claim all the HTLCs that we've received from our current commit tx. This will be done using 2nd stage HTLC transactions. + * + * @param commitments our commitment data, which include payment preimages + * @return a list of transactions (one per output of the commit tx that we can claim) + */ + def claimCommitTxOutputs(keyManager: ChannelKeyManager, commitments: Commitments, tx: Transaction, currentBlockHeight: BlockHeight, feeEstimator: FeeEstimator, feeTargets: FeeTargets)(implicit log: LoggingAdapter): LocalCommitPublished = { + import commitments._ + require(localCommit.commitTxAndRemoteSig.commitTx.tx.txid == tx.txid, "txid mismatch, provided tx is not the current local commit tx") val channelKeyPath = keyManager.keyPath(localParams, channelConfig) val localPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, commitments.localCommit.index.toInt) val localRevocationPubkey = Generators.revocationPubKey(remoteParams.revocationBasepoint, localPerCommitmentPoint) val localDelayedPubkey = Generators.derivePubKey(keyManager.delayedPaymentPoint(channelKeyPath).publicKey, localPerCommitmentPoint) - val htlcDelayedTx = withTxGenerationLog("htlc-delayed") { - Transactions.makeHtlcDelayedTx(tx, localParams.dustLimit, localRevocationPubkey, remoteParams.toSelfDelay, localDelayedPubkey, localParams.defaultFinalScriptPubKey, feeratePerKwDelayed).map(claimDelayed => { + val localFundingPubKey = keyManager.fundingPublicKey(commitments.localParams.fundingKeyPath).publicKey + val feeratePerKwDelayed = feeEstimator.getFeeratePerKw(feeTargets.claimMainBlockTarget) + + // first we will claim our main output as soon as the delay is over + val mainDelayedTx = withTxGenerationLog("local-main-delayed") { + Transactions.makeClaimLocalDelayedOutputTx(tx, localParams.dustLimit, localRevocationPubkey, remoteParams.toSelfDelay, localDelayedPubkey, localParams.defaultFinalScriptPubKey, feeratePerKwDelayed).map(claimDelayed => { val sig = keyManager.sign(claimDelayed, keyManager.delayedPaymentPoint(channelKeyPath), localPerCommitmentPoint, TxOwner.Local, commitmentFormat) Transactions.addSigs(claimDelayed, sig) }) } - val localCommitPublished1 = localCommitPublished.copy(claimHtlcDelayedTxs = localCommitPublished.claimHtlcDelayedTxs ++ htlcDelayedTx.toSeq) - (localCommitPublished1, htlcDelayedTx) - } else { - (localCommitPublished, None) + + val htlcTxs: Map[OutPoint, Option[HtlcTx]] = claimHtlcOutputs(keyManager, commitments) + + // If we don't have pending HTLCs, we don't have funds at risk, so we can aim for a slower confirmation. + val confirmCommitBefore = htlcTxs.values.flatten.map(htlcTx => htlcTx.confirmBefore).minOption.getOrElse(currentBlockHeight + feeTargets.commitmentWithoutHtlcsBlockTarget) + val claimAnchorTxs: List[ClaimAnchorOutputTx] = List( + withTxGenerationLog("local-anchor") { + Transactions.makeClaimLocalAnchorOutputTx(tx, localFundingPubKey, confirmCommitBefore) + }, + withTxGenerationLog("remote-anchor") { + Transactions.makeClaimRemoteAnchorOutputTx(tx, commitments.remoteParams.fundingPubKey) + } + ).flatten + + LocalCommitPublished( + commitTx = tx, + claimMainDelayedOutputTx = mainDelayedTx, + htlcTxs = htlcTxs, + claimHtlcDelayedTxs = Nil, // we will claim these once the htlc txs are confirmed + claimAnchorTxs = claimAnchorTxs, + irrevocablySpent = Map.empty) } - } - /** - * Claim all the HTLCs that we've received from their current commit tx, if the channel used option_static_remotekey - * we don't need to claim our main output because it directly pays to one of our wallet's p2wpkh addresses. - * - * @param commitments our commitment data, which include payment preimages - * @param remoteCommit the remote commitment data to use to claim outputs (it can be their current or next commitment) - * @param tx the remote commitment transaction that has just been published - * @return a list of transactions (one per output of the commit tx that we can claim) - */ - def claimRemoteCommitTxOutputs(keyManager: ChannelKeyManager, commitments: Commitments, remoteCommit: RemoteCommit, tx: Transaction, currentBlockHeight: BlockHeight, feeEstimator: FeeEstimator, feeTargets: FeeTargets)(implicit log: LoggingAdapter): RemoteCommitPublished = { - require(remoteCommit.txid == tx.txid, "txid mismatch, provided tx is not the current remote commit tx") - val (remoteCommitTx, _) = Commitments.makeRemoteTxs(keyManager, commitments.channelConfig, commitments.channelFeatures, remoteCommit.index, commitments.localParams, commitments.remoteParams, commitments.commitInput, remoteCommit.remotePerCommitmentPoint, remoteCommit.spec) - require(remoteCommitTx.tx.txid == tx.txid, "txid mismatch, cannot recompute the current remote commit tx") - val channelKeyPath = keyManager.keyPath(commitments.localParams, commitments.channelConfig) - val localFundingPubkey = keyManager.fundingPublicKey(commitments.localParams.fundingKeyPath).publicKey - val localHtlcPubkey = Generators.derivePubKey(keyManager.htlcPoint(channelKeyPath).publicKey, remoteCommit.remotePerCommitmentPoint) - val remoteHtlcPubkey = Generators.derivePubKey(commitments.remoteParams.htlcBasepoint, remoteCommit.remotePerCommitmentPoint) - val remoteRevocationPubkey = Generators.revocationPubKey(keyManager.revocationPoint(channelKeyPath).publicKey, remoteCommit.remotePerCommitmentPoint) - val remoteDelayedPaymentPubkey = Generators.derivePubKey(commitments.remoteParams.delayedPaymentBasepoint, remoteCommit.remotePerCommitmentPoint) - val localPaymentPubkey = Generators.derivePubKey(keyManager.paymentPoint(channelKeyPath).publicKey, remoteCommit.remotePerCommitmentPoint) - val outputs = makeCommitTxOutputs(!commitments.localParams.isFunder, commitments.remoteParams.dustLimit, remoteRevocationPubkey, commitments.localParams.toSelfDelay, remoteDelayedPaymentPubkey, localPaymentPubkey, remoteHtlcPubkey, localHtlcPubkey, commitments.remoteParams.fundingPubKey, localFundingPubkey, remoteCommit.spec, commitments.commitmentFormat) - - // we need to use a rather high fee for htlc-claim because we compete with the counterparty - val feeratePerKwHtlc = feeEstimator.getFeeratePerKw(target = 2) - - // those are the preimages to existing received htlcs - val preimages = commitments.localChanges.all.collect { case u: UpdateFulfillHtlc => u.paymentPreimage }.map(r => Crypto.sha256(r) -> r).toMap - - // remember we are looking at the remote commitment so IN for them is really OUT for us and vice versa - val htlcTxs: Map[OutPoint, Option[ClaimHtlcTx]] = remoteCommit.spec.htlcs.collect { - case OutgoingHtlc(add: UpdateAddHtlc) => - // NB: we first generate the tx skeleton and finalize it below if we have the preimage, so we set logSuccess to false to avoid logging twice - withTxGenerationLog("claim-htlc-success", logSuccess = false) { - Transactions.makeClaimHtlcSuccessTx(remoteCommitTx.tx, outputs, commitments.localParams.dustLimit, localHtlcPubkey, remoteHtlcPubkey, remoteRevocationPubkey, commitments.localParams.defaultFinalScriptPubKey, add, feeratePerKwHtlc, commitments.commitmentFormat) - }.map(claimHtlcTx => { - if (preimages.contains(add.paymentHash)) { + /** + * Claim the output of a local commit tx corresponding to HTLCs. + */ + def claimHtlcOutputs(keyManager: ChannelKeyManager, commitments: Commitments)(implicit log: LoggingAdapter): Map[OutPoint, Option[HtlcTx]] = { + import commitments._ + val channelKeyPath = keyManager.keyPath(localParams, channelConfig) + val localPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, commitments.localCommit.index.toInt) + + // those are the preimages to existing received htlcs + val hash2Preimage: Map[ByteVector32, ByteVector32] = commitments.localChanges.all.collect { case u: UpdateFulfillHtlc => u.paymentPreimage }.map(r => Crypto.sha256(r) -> r).toMap + val failedIncomingHtlcs: Set[Long] = commitments.localChanges.all.collect { + case u: UpdateFailHtlc => u.id + case u: UpdateFailMalformedHtlc => u.id + }.toSet + + localCommit.htlcTxsAndRemoteSigs.collect { + case HtlcTxAndRemoteSig(txInfo@HtlcSuccessTx(_, _, paymentHash, _, _), remoteSig) => + if (hash2Preimage.contains(paymentHash)) { // incoming htlc for which we have the preimage: we can spend it immediately - claimHtlcTx.input.outPoint -> withTxGenerationLog("claim-htlc-success") { - val sig = keyManager.sign(claimHtlcTx, keyManager.htlcPoint(channelKeyPath), remoteCommit.remotePerCommitmentPoint, TxOwner.Local, commitments.commitmentFormat) - Right(Transactions.addSigs(claimHtlcTx, sig, preimages(add.paymentHash))) - } + Some(txInfo.input.outPoint -> withTxGenerationLog("htlc-success") { + val localSig = keyManager.sign(txInfo, keyManager.htlcPoint(channelKeyPath), localPerCommitmentPoint, TxOwner.Local, commitmentFormat) + Right(Transactions.addSigs(txInfo, localSig, remoteSig, hash2Preimage(paymentHash), commitmentFormat)) + }) + } else if (failedIncomingHtlcs.contains(txInfo.htlcId)) { + // incoming htlc that we know for sure will never be fulfilled downstream: we can safely discard it + None } else { // incoming htlc for which we don't have the preimage: we can't spend it immediately, but we may learn the // preimage later, otherwise it will eventually timeout and they will get their funds back - claimHtlcTx.input.outPoint -> None - } - }) - case IncomingHtlc(add: UpdateAddHtlc) => - // outgoing htlc: they may or may not have the preimage, the only thing to do is try to get back our funds after timeout - // NB: we first generate the tx skeleton and finalize it below, so we set logSuccess to false to avoid logging twice - withTxGenerationLog("claim-htlc-timeout", logSuccess = false) { - Transactions.makeClaimHtlcTimeoutTx(remoteCommitTx.tx, outputs, commitments.localParams.dustLimit, localHtlcPubkey, remoteHtlcPubkey, remoteRevocationPubkey, commitments.localParams.defaultFinalScriptPubKey, add, feeratePerKwHtlc, commitments.commitmentFormat) - }.map(claimHtlcTx => { - claimHtlcTx.input.outPoint -> withTxGenerationLog("claim-htlc-timeout") { - val sig = keyManager.sign(claimHtlcTx, keyManager.htlcPoint(channelKeyPath), remoteCommit.remotePerCommitmentPoint, TxOwner.Local, commitments.commitmentFormat) - Right(Transactions.addSigs(claimHtlcTx, sig)) + Some(txInfo.input.outPoint -> None) } - }) - }.toSeq.flatten.toMap - - // If we don't have pending HTLCs, we don't have funds at risk, so we can aim for a slower confirmation. - val confirmCommitBefore = htlcTxs.values.flatten.map(htlcTx => htlcTx.confirmBefore).minOption.getOrElse(currentBlockHeight + feeTargets.commitmentWithoutHtlcsBlockTarget) - val claimAnchorTxs: List[ClaimAnchorOutputTx] = List( - withTxGenerationLog("local-anchor") { - Transactions.makeClaimLocalAnchorOutputTx(tx, localFundingPubkey, confirmCommitBefore) - }, - withTxGenerationLog("remote-anchor") { - Transactions.makeClaimRemoteAnchorOutputTx(tx, commitments.remoteParams.fundingPubKey) - } - ).flatten - - RemoteCommitPublished( - commitTx = tx, - claimMainOutputTx = claimRemoteCommitMainOutput(keyManager, commitments, remoteCommit.remotePerCommitmentPoint, tx, feeEstimator, feeTargets), - claimHtlcTxs = htlcTxs, - claimAnchorTxs = claimAnchorTxs, - irrevocablySpent = Map.empty - ) - } - - /** - * Claim our main output only - * - * @param commitments either our current commitment data in case of usual remote uncooperative closing - * or our outdated commitment data in case of data loss protection procedure; in any case it is used only - * to get some constant parameters, not commitment data - * @param remotePerCommitmentPoint the remote perCommitmentPoint corresponding to this commitment - * @param tx the remote commitment transaction that has just been published - * @return an optional [[ClaimRemoteCommitMainOutputTx]] transaction claiming our main output - */ - def claimRemoteCommitMainOutput(keyManager: ChannelKeyManager, commitments: Commitments, remotePerCommitmentPoint: PublicKey, tx: Transaction, feeEstimator: FeeEstimator, feeTargets: FeeTargets)(implicit log: LoggingAdapter): Option[ClaimRemoteCommitMainOutputTx] = { - if (commitments.channelFeatures.paysDirectlyToWallet) { - // the commitment tx sends funds directly to our wallet, no claim tx needed - None - } else { - val channelKeyPath = keyManager.keyPath(commitments.localParams, commitments.channelConfig) - val localPubkey = Generators.derivePubKey(keyManager.paymentPoint(channelKeyPath).publicKey, remotePerCommitmentPoint) - val localPaymentPoint = keyManager.paymentPoint(channelKeyPath).publicKey - val feeratePerKwMain = feeEstimator.getFeeratePerKw(feeTargets.claimMainBlockTarget) - - commitments.commitmentFormat match { - case DefaultCommitmentFormat => withTxGenerationLog("remote-main") { - Transactions.makeClaimP2WPKHOutputTx(tx, commitments.localParams.dustLimit, localPubkey, commitments.localParams.defaultFinalScriptPubKey, feeratePerKwMain).map(claimMain => { - val sig = keyManager.sign(claimMain, keyManager.paymentPoint(channelKeyPath), remotePerCommitmentPoint, TxOwner.Local, commitments.commitmentFormat) - Transactions.addSigs(claimMain, localPubkey, sig) + case HtlcTxAndRemoteSig(txInfo: HtlcTimeoutTx, remoteSig) => + // outgoing htlc: they may or may not have the preimage, the only thing to do is try to get back our funds after timeout + Some(txInfo.input.outPoint -> withTxGenerationLog("htlc-timeout") { + val localSig = keyManager.sign(txInfo, keyManager.htlcPoint(channelKeyPath), localPerCommitmentPoint, TxOwner.Local, commitmentFormat) + Right(Transactions.addSigs(txInfo, localSig, remoteSig, commitmentFormat)) }) - } - case _: AnchorOutputsCommitmentFormat => withTxGenerationLog("remote-main-delayed") { - Transactions.makeClaimRemoteDelayedOutputTx(tx, commitments.localParams.dustLimit, localPaymentPoint, commitments.localParams.defaultFinalScriptPubKey, feeratePerKwMain).map(claimMain => { - val sig = keyManager.sign(claimMain, keyManager.paymentPoint(channelKeyPath), TxOwner.Local, commitments.commitmentFormat) - Transactions.addSigs(claimMain, sig) + }.flatten.toMap + } + + /** + * Claim the output of a 2nd-stage HTLC transaction. If the provided transaction isn't an htlc, this will be a no-op. + * + * NB: with anchor outputs, it's possible to have transactions that spend *many* HTLC outputs at once, but we're not + * doing that because it introduces a lot of subtle edge cases. + */ + def claimHtlcDelayedOutput(localCommitPublished: LocalCommitPublished, keyManager: ChannelKeyManager, commitments: Commitments, tx: Transaction, feeEstimator: FeeEstimator, feeTargets: FeeTargets)(implicit log: LoggingAdapter): (LocalCommitPublished, Option[HtlcDelayedTx]) = { + import commitments._ + if (isHtlcSuccess(tx, localCommitPublished) || isHtlcTimeout(tx, localCommitPublished)) { + val feeratePerKwDelayed = feeEstimator.getFeeratePerKw(feeTargets.claimMainBlockTarget) + val channelKeyPath = keyManager.keyPath(localParams, channelConfig) + val localPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, commitments.localCommit.index.toInt) + val localRevocationPubkey = Generators.revocationPubKey(remoteParams.revocationBasepoint, localPerCommitmentPoint) + val localDelayedPubkey = Generators.derivePubKey(keyManager.delayedPaymentPoint(channelKeyPath).publicKey, localPerCommitmentPoint) + val htlcDelayedTx = withTxGenerationLog("htlc-delayed") { + Transactions.makeHtlcDelayedTx(tx, localParams.dustLimit, localRevocationPubkey, remoteParams.toSelfDelay, localDelayedPubkey, localParams.defaultFinalScriptPubKey, feeratePerKwDelayed).map(claimDelayed => { + val sig = keyManager.sign(claimDelayed, keyManager.delayedPaymentPoint(channelKeyPath), localPerCommitmentPoint, TxOwner.Local, commitmentFormat) + Transactions.addSigs(claimDelayed, sig) }) } + val localCommitPublished1 = localCommitPublished.copy(claimHtlcDelayedTxs = localCommitPublished.claimHtlcDelayedTxs ++ htlcDelayedTx.toSeq) + (localCommitPublished1, htlcDelayedTx) + } else { + (localCommitPublished, None) } } } - /** - * When an unexpected transaction spending the funding tx is detected: - * 1) we find out if the published transaction is one of remote's revoked txs - * 2) and then: - * a) if it is a revoked tx we build a set of transactions that will punish them by stealing all their funds - * b) otherwise there is nothing we can do - * - * @return a [[RevokedCommitPublished]] object containing penalty transactions if the tx is a revoked commitment - */ - def claimRevokedRemoteCommitTxOutputs(keyManager: ChannelKeyManager, commitments: Commitments, commitTx: Transaction, db: ChannelsDb, feeEstimator: FeeEstimator, feeTargets: FeeTargets)(implicit log: LoggingAdapter): Option[RevokedCommitPublished] = { - import commitments._ - require(commitTx.txIn.size == 1, "commitment tx should have 1 input") - val channelKeyPath = keyManager.keyPath(localParams, channelConfig) - val obscuredTxNumber = Transactions.decodeTxNumber(commitTx.txIn.head.sequence, commitTx.lockTime) - val localPaymentPoint = localParams.walletStaticPaymentBasepoint.getOrElse(keyManager.paymentPoint(channelKeyPath).publicKey) - // this tx has been published by remote, so we need to invert local/remote params - val txNumber = Transactions.obscuredCommitTxNumber(obscuredTxNumber, !localParams.isFunder, remoteParams.paymentBasepoint, localPaymentPoint) - require(txNumber <= 0xffffffffffffL, "txNumber must be lesser than 48 bits long") - log.warning(s"a revoked commit has been published with txnumber=$txNumber") - // now we know what commit number this tx is referring to, we can derive the commitment point from the shachain - remotePerCommitmentSecrets.getHash(0xFFFFFFFFFFFFL - txNumber) - .map(d => PrivateKey(d)) - .map(remotePerCommitmentSecret => { - val remotePerCommitmentPoint = remotePerCommitmentSecret.publicKey - val remoteDelayedPaymentPubkey = Generators.derivePubKey(remoteParams.delayedPaymentBasepoint, remotePerCommitmentPoint) - val remoteRevocationPubkey = Generators.revocationPubKey(keyManager.revocationPoint(channelKeyPath).publicKey, remotePerCommitmentPoint) - val remoteHtlcPubkey = Generators.derivePubKey(remoteParams.htlcBasepoint, remotePerCommitmentPoint) - val localPaymentPubkey = Generators.derivePubKey(keyManager.paymentPoint(channelKeyPath).publicKey, remotePerCommitmentPoint) - val localHtlcPubkey = Generators.derivePubKey(keyManager.htlcPoint(channelKeyPath).publicKey, remotePerCommitmentPoint) + object RemoteClose { + + /** + * Claim all the HTLCs that we've received from their current commit tx, if the channel used option_static_remotekey + * we don't need to claim our main output because it directly pays to one of our wallet's p2wpkh addresses. + * + * @param commitments our commitment data, which include payment preimages + * @param remoteCommit the remote commitment data to use to claim outputs (it can be their current or next commitment) + * @param tx the remote commitment transaction that has just been published + * @return a list of transactions (one per output of the commit tx that we can claim) + */ + def claimCommitTxOutputs(keyManager: ChannelKeyManager, commitments: Commitments, remoteCommit: RemoteCommit, tx: Transaction, currentBlockHeight: BlockHeight, feeEstimator: FeeEstimator, feeTargets: FeeTargets)(implicit log: LoggingAdapter): RemoteCommitPublished = { + require(remoteCommit.txid == tx.txid, "txid mismatch, provided tx is not the current remote commit tx") + + val htlcTxs: Map[OutPoint, Option[ClaimHtlcTx]] = claimHtlcOutputs(keyManager, commitments, remoteCommit, feeEstimator) + + // If we don't have pending HTLCs, we don't have funds at risk, so we can aim for a slower confirmation. + val confirmCommitBefore = htlcTxs.values.flatten.map(htlcTx => htlcTx.confirmBefore).minOption.getOrElse(currentBlockHeight + feeTargets.commitmentWithoutHtlcsBlockTarget) + val localFundingPubkey = keyManager.fundingPublicKey(commitments.localParams.fundingKeyPath).publicKey + val claimAnchorTxs: List[ClaimAnchorOutputTx] = List( + withTxGenerationLog("local-anchor") { + Transactions.makeClaimLocalAnchorOutputTx(tx, localFundingPubkey, confirmCommitBefore) + }, + withTxGenerationLog("remote-anchor") { + Transactions.makeClaimRemoteAnchorOutputTx(tx, commitments.remoteParams.fundingPubKey) + } + ).flatten + + RemoteCommitPublished( + commitTx = tx, + claimMainOutputTx = claimMainOutput(keyManager, commitments, remoteCommit.remotePerCommitmentPoint, tx, feeEstimator, feeTargets), + claimHtlcTxs = htlcTxs, + claimAnchorTxs = claimAnchorTxs, + irrevocablySpent = Map.empty + ) + } + /** + * Claim our main output only + * + * @param commitments either our current commitment data in case of usual remote uncooperative closing + * or our outdated commitment data in case of data loss protection procedure; in any case it is used only + * to get some constant parameters, not commitment data + * @param remotePerCommitmentPoint the remote perCommitmentPoint corresponding to this commitment + * @param tx the remote commitment transaction that has just been published + * @return an optional [[ClaimRemoteCommitMainOutputTx]] transaction claiming our main output + */ + def claimMainOutput(keyManager: ChannelKeyManager, commitments: Commitments, remotePerCommitmentPoint: PublicKey, tx: Transaction, feeEstimator: FeeEstimator, feeTargets: FeeTargets)(implicit log: LoggingAdapter): Option[ClaimRemoteCommitMainOutputTx] = { + if (commitments.channelFeatures.paysDirectlyToWallet) { + // the commitment tx sends funds directly to our wallet, no claim tx needed + None + } else { + val channelKeyPath = keyManager.keyPath(commitments.localParams, commitments.channelConfig) + val localPubkey = Generators.derivePubKey(keyManager.paymentPoint(channelKeyPath).publicKey, remotePerCommitmentPoint) + val localPaymentPoint = keyManager.paymentPoint(channelKeyPath).publicKey val feeratePerKwMain = feeEstimator.getFeeratePerKw(feeTargets.claimMainBlockTarget) - // we need to use a high fee here for punishment txs because after a delay they can be spent by the counterparty - val feeratePerKwPenalty = feeEstimator.getFeeratePerKw(target = 2) - // first we will claim our main output right away - val mainTx = channelFeatures match { - case ct if ct.paysDirectlyToWallet => - log.info(s"channel uses option_static_remotekey to pay directly to our wallet, there is nothing to do") - None - case ct => ct.commitmentFormat match { - case DefaultCommitmentFormat => withTxGenerationLog("claim-p2wpkh-output") { - Transactions.makeClaimP2WPKHOutputTx(commitTx, localParams.dustLimit, localPaymentPubkey, localParams.defaultFinalScriptPubKey, feeratePerKwMain).map(claimMain => { - val sig = keyManager.sign(claimMain, keyManager.paymentPoint(channelKeyPath), remotePerCommitmentPoint, TxOwner.Local, commitmentFormat) - Transactions.addSigs(claimMain, localPaymentPubkey, sig) - }) - } - case _: AnchorOutputsCommitmentFormat => withTxGenerationLog("remote-main-delayed") { - Transactions.makeClaimRemoteDelayedOutputTx(commitTx, localParams.dustLimit, localPaymentPoint, localParams.defaultFinalScriptPubKey, feeratePerKwMain).map(claimMain => { - val sig = keyManager.sign(claimMain, keyManager.paymentPoint(channelKeyPath), TxOwner.Local, commitmentFormat) - Transactions.addSigs(claimMain, sig) - }) - } + commitments.commitmentFormat match { + case DefaultCommitmentFormat => withTxGenerationLog("remote-main") { + Transactions.makeClaimP2WPKHOutputTx(tx, commitments.localParams.dustLimit, localPubkey, commitments.localParams.defaultFinalScriptPubKey, feeratePerKwMain).map(claimMain => { + val sig = keyManager.sign(claimMain, keyManager.paymentPoint(channelKeyPath), remotePerCommitmentPoint, TxOwner.Local, commitments.commitmentFormat) + Transactions.addSigs(claimMain, localPubkey, sig) + }) + } + case _: AnchorOutputsCommitmentFormat => withTxGenerationLog("remote-main-delayed") { + Transactions.makeClaimRemoteDelayedOutputTx(tx, commitments.localParams.dustLimit, localPaymentPoint, commitments.localParams.defaultFinalScriptPubKey, feeratePerKwMain).map(claimMain => { + val sig = keyManager.sign(claimMain, keyManager.paymentPoint(channelKeyPath), TxOwner.Local, commitments.commitmentFormat) + Transactions.addSigs(claimMain, sig) + }) } } + } + } - // then we punish them by stealing their main output - val mainPenaltyTx = withTxGenerationLog("main-penalty") { - Transactions.makeMainPenaltyTx(commitTx, localParams.dustLimit, remoteRevocationPubkey, localParams.defaultFinalScriptPubKey, localParams.toSelfDelay, remoteDelayedPaymentPubkey, feeratePerKwPenalty).map(txinfo => { - val sig = keyManager.sign(txinfo, keyManager.revocationPoint(channelKeyPath), remotePerCommitmentSecret, TxOwner.Local, commitmentFormat) - Transactions.addSigs(txinfo, sig) + /** + * Claim our htlc outputs only + */ + def claimHtlcOutputs(keyManager: ChannelKeyManager, commitments: Commitments, remoteCommit: RemoteCommit, feeEstimator: FeeEstimator)(implicit log: LoggingAdapter): Map[OutPoint, Option[ClaimHtlcTx]] = { + val (remoteCommitTx, _) = Commitments.makeRemoteTxs(keyManager, commitments.channelConfig, commitments.channelFeatures, remoteCommit.index, commitments.localParams, commitments.remoteParams, commitments.commitInput, remoteCommit.remotePerCommitmentPoint, remoteCommit.spec) + require(remoteCommitTx.tx.txid == remoteCommit.txid, "txid mismatch, cannot recompute the current remote commit tx") + val channelKeyPath = keyManager.keyPath(commitments.localParams, commitments.channelConfig) + val localFundingPubkey = keyManager.fundingPublicKey(commitments.localParams.fundingKeyPath).publicKey + val localHtlcPubkey = Generators.derivePubKey(keyManager.htlcPoint(channelKeyPath).publicKey, remoteCommit.remotePerCommitmentPoint) + val remoteHtlcPubkey = Generators.derivePubKey(commitments.remoteParams.htlcBasepoint, remoteCommit.remotePerCommitmentPoint) + val remoteRevocationPubkey = Generators.revocationPubKey(keyManager.revocationPoint(channelKeyPath).publicKey, remoteCommit.remotePerCommitmentPoint) + val remoteDelayedPaymentPubkey = Generators.derivePubKey(commitments.remoteParams.delayedPaymentBasepoint, remoteCommit.remotePerCommitmentPoint) + val localPaymentPubkey = Generators.derivePubKey(keyManager.paymentPoint(channelKeyPath).publicKey, remoteCommit.remotePerCommitmentPoint) + val outputs = makeCommitTxOutputs(!commitments.localParams.isFunder, commitments.remoteParams.dustLimit, remoteRevocationPubkey, commitments.localParams.toSelfDelay, remoteDelayedPaymentPubkey, localPaymentPubkey, remoteHtlcPubkey, localHtlcPubkey, commitments.remoteParams.fundingPubKey, localFundingPubkey, remoteCommit.spec, commitments.commitmentFormat) + // we need to use a rather high fee for htlc-claim because we compete with the counterparty + val feeratePerKwHtlc = feeEstimator.getFeeratePerKw(target = 2) + + // those are the preimages to existing received htlcs + val hash2Preimage: Map[ByteVector32, ByteVector32] = commitments.localChanges.all.collect { case u: UpdateFulfillHtlc => u.paymentPreimage }.map(r => Crypto.sha256(r) -> r).toMap + val failedIncomingHtlcs: Set[Long] = commitments.localChanges.all.collect { + case u: UpdateFailHtlc => u.id + case u: UpdateFailMalformedHtlc => u.id + }.toSet + + // remember we are looking at the remote commitment so IN for them is really OUT for us and vice versa + remoteCommit.spec.htlcs.collect { + case OutgoingHtlc(add: UpdateAddHtlc) => + // NB: we first generate the tx skeleton and finalize it below if we have the preimage, so we set logSuccess to false to avoid logging twice + withTxGenerationLog("claim-htlc-success", logSuccess = false) { + Transactions.makeClaimHtlcSuccessTx(remoteCommitTx.tx, outputs, commitments.localParams.dustLimit, localHtlcPubkey, remoteHtlcPubkey, remoteRevocationPubkey, commitments.localParams.defaultFinalScriptPubKey, add, feeratePerKwHtlc, commitments.commitmentFormat) + }.map(claimHtlcTx => { + if (hash2Preimage.contains(add.paymentHash)) { + // incoming htlc for which we have the preimage: we can spend it immediately + Some(claimHtlcTx.input.outPoint -> withTxGenerationLog("claim-htlc-success") { + val sig = keyManager.sign(claimHtlcTx, keyManager.htlcPoint(channelKeyPath), remoteCommit.remotePerCommitmentPoint, TxOwner.Local, commitments.commitmentFormat) + Right(Transactions.addSigs(claimHtlcTx, sig, hash2Preimage(add.paymentHash))) + }) + } else if (failedIncomingHtlcs.contains(add.id)) { + // incoming htlc that we know for sure will never be fulfilled downstream: we can safely discard it + None + } else { + // incoming htlc for which we don't have the preimage: we can't spend it immediately, but we may learn the + // preimage later, otherwise it will eventually timeout and they will get their funds back + Some(claimHtlcTx.input.outPoint -> None) + } }) - } - - // we retrieve the information needed to rebuild htlc scripts - val htlcInfos = db.listHtlcInfos(commitments.channelId, txNumber) - log.info(s"got htlcs=${htlcInfos.size} for txnumber=$txNumber") - val htlcsRedeemScripts = ( - htlcInfos.map { case (paymentHash, cltvExpiry) => Scripts.htlcReceived(remoteHtlcPubkey, localHtlcPubkey, remoteRevocationPubkey, Crypto.ripemd160(paymentHash), cltvExpiry, commitmentFormat) } ++ - htlcInfos.map { case (paymentHash, _) => Scripts.htlcOffered(remoteHtlcPubkey, localHtlcPubkey, remoteRevocationPubkey, Crypto.ripemd160(paymentHash), commitmentFormat) } - ) - .map(redeemScript => Script.write(pay2wsh(redeemScript)) -> Script.write(redeemScript)) - .toMap - - // and finally we steal the htlc outputs - val htlcPenaltyTxs = commitTx.txOut.zipWithIndex.collect { case (txOut, outputIndex) if htlcsRedeemScripts.contains(txOut.publicKeyScript) => - val htlcRedeemScript = htlcsRedeemScripts(txOut.publicKeyScript) - withTxGenerationLog("htlc-penalty") { - Transactions.makeHtlcPenaltyTx(commitTx, outputIndex, htlcRedeemScript, localParams.dustLimit, localParams.defaultFinalScriptPubKey, feeratePerKwPenalty).map(htlcPenalty => { - val sig = keyManager.sign(htlcPenalty, keyManager.revocationPoint(channelKeyPath), remotePerCommitmentSecret, TxOwner.Local, commitmentFormat) - Transactions.addSigs(htlcPenalty, sig, remoteRevocationPubkey) + case IncomingHtlc(add: UpdateAddHtlc) => + // outgoing htlc: they may or may not have the preimage, the only thing to do is try to get back our funds after timeout + // NB: we first generate the tx skeleton and finalize it below, so we set logSuccess to false to avoid logging twice + withTxGenerationLog("claim-htlc-timeout", logSuccess = false) { + Transactions.makeClaimHtlcTimeoutTx(remoteCommitTx.tx, outputs, commitments.localParams.dustLimit, localHtlcPubkey, remoteHtlcPubkey, remoteRevocationPubkey, commitments.localParams.defaultFinalScriptPubKey, add, feeratePerKwHtlc, commitments.commitmentFormat) + }.map(claimHtlcTx => { + Some(claimHtlcTx.input.outPoint -> withTxGenerationLog("claim-htlc-timeout") { + val sig = keyManager.sign(claimHtlcTx, keyManager.htlcPoint(channelKeyPath), remoteCommit.remotePerCommitmentPoint, TxOwner.Local, commitments.commitmentFormat) + Right(Transactions.addSigs(claimHtlcTx, sig)) }) - } - }.toList.flatten - - RevokedCommitPublished( - commitTx = commitTx, - claimMainOutputTx = mainTx, - mainPenaltyTx = mainPenaltyTx, - htlcPenaltyTxs = htlcPenaltyTxs, - claimHtlcDelayedPenaltyTxs = Nil, // we will generate and spend those if they publish their HtlcSuccessTx or HtlcTimeoutTx - irrevocablySpent = Map.empty - ) - }) + }) + }.toSeq.flatten.flatten.toMap + } } - /** - * Claims the output of an [[HtlcSuccessTx]] or [[HtlcTimeoutTx]] transaction using a revocation key. - * - * In case a revoked commitment with pending HTLCs is published, there are two ways the HTLC outputs can be taken as punishment: - * - by spending the corresponding output of the commitment tx, using [[HtlcPenaltyTx]] that we generate as soon as we detect that a revoked commit - * as been spent; note that those transactions will compete with [[HtlcSuccessTx]] and [[HtlcTimeoutTx]] published by the counterparty. - * - by spending the delayed output of [[HtlcSuccessTx]] and [[HtlcTimeoutTx]] if those get confirmed; because the output of these txs is protected by - * an OP_CSV delay, we will have time to spend them with a revocation key. In that case, we generate the spending transactions "on demand", - * this is the purpose of this method. - * - * NB: when anchor outputs is used, htlc transactions can be aggregated in a single transaction if they share the same - * lockTime (thanks to the use of sighash_single | sighash_anyonecanpay), so we may need to claim multiple outputs. - */ - def claimRevokedHtlcTxOutputs(keyManager: ChannelKeyManager, commitments: Commitments, revokedCommitPublished: RevokedCommitPublished, htlcTx: Transaction, feeEstimator: FeeEstimator)(implicit log: LoggingAdapter): (RevokedCommitPublished, Seq[ClaimHtlcDelayedOutputPenaltyTx]) = { - val isHtlcTx = htlcTx.txIn.map(_.outPoint.txid).contains(revokedCommitPublished.commitTx.txid) && - htlcTx.txIn.map(_.witness).collect(Scripts.extractPreimageFromHtlcSuccess.orElse(Scripts.extractPaymentHashFromHtlcTimeout)).nonEmpty - if (isHtlcTx) { - log.info(s"looks like txid=${htlcTx.txid} could be a 2nd level htlc tx spending revoked commit txid=${revokedCommitPublished.commitTx.txid}") - // Let's assume that htlcTx is an HtlcSuccessTx or HtlcTimeoutTx and try to generate a tx spending its output using a revocation key + object RevokedClose { + + /** + * When an unexpected transaction spending the funding tx is detected: + * 1) we find out if the published transaction is one of remote's revoked txs + * 2) and then: + * a) if it is a revoked tx we build a set of transactions that will punish them by stealing all their funds + * b) otherwise there is nothing we can do + * + * @return a [[RevokedCommitPublished]] object containing penalty transactions if the tx is a revoked commitment + */ + def claimCommitTxOutputs(keyManager: ChannelKeyManager, commitments: Commitments, commitTx: Transaction, db: ChannelsDb, feeEstimator: FeeEstimator, feeTargets: FeeTargets)(implicit log: LoggingAdapter): Option[RevokedCommitPublished] = { import commitments._ - val commitTx = revokedCommitPublished.commitTx - val obscuredTxNumber = Transactions.decodeTxNumber(commitTx.txIn.head.sequence, commitTx.lockTime) + require(commitTx.txIn.size == 1, "commitment tx should have 1 input") val channelKeyPath = keyManager.keyPath(localParams, channelConfig) + val obscuredTxNumber = Transactions.decodeTxNumber(commitTx.txIn.head.sequence, commitTx.lockTime) val localPaymentPoint = localParams.walletStaticPaymentBasepoint.getOrElse(keyManager.paymentPoint(channelKeyPath).publicKey) // this tx has been published by remote, so we need to invert local/remote params val txNumber = Transactions.obscuredCommitTxNumber(obscuredTxNumber, !localParams.isFunder, remoteParams.paymentBasepoint, localPaymentPoint) + require(txNumber <= 0xffffffffffffL, "txNumber must be lesser than 48 bits long") + log.warning(s"a revoked commit has been published with txnumber=$txNumber") // now we know what commit number this tx is referring to, we can derive the commitment point from the shachain remotePerCommitmentSecrets.getHash(0xFFFFFFFFFFFFL - txNumber) .map(d => PrivateKey(d)) @@ -967,26 +909,129 @@ object Helpers { val remotePerCommitmentPoint = remotePerCommitmentSecret.publicKey val remoteDelayedPaymentPubkey = Generators.derivePubKey(remoteParams.delayedPaymentBasepoint, remotePerCommitmentPoint) val remoteRevocationPubkey = Generators.revocationPubKey(keyManager.revocationPoint(channelKeyPath).publicKey, remotePerCommitmentPoint) + val remoteHtlcPubkey = Generators.derivePubKey(remoteParams.htlcBasepoint, remotePerCommitmentPoint) + val localPaymentPubkey = Generators.derivePubKey(keyManager.paymentPoint(channelKeyPath).publicKey, remotePerCommitmentPoint) + val localHtlcPubkey = Generators.derivePubKey(keyManager.htlcPoint(channelKeyPath).publicKey, remotePerCommitmentPoint) + val feeratePerKwMain = feeEstimator.getFeeratePerKw(feeTargets.claimMainBlockTarget) // we need to use a high fee here for punishment txs because after a delay they can be spent by the counterparty - val feeratePerKwPenalty = feeEstimator.getFeeratePerKw(target = 1) - - val penaltyTxs = Transactions.makeClaimHtlcDelayedOutputPenaltyTxs(htlcTx, localParams.dustLimit, remoteRevocationPubkey, localParams.toSelfDelay, remoteDelayedPaymentPubkey, localParams.defaultFinalScriptPubKey, feeratePerKwPenalty).flatMap(claimHtlcDelayedOutputPenaltyTx => { - withTxGenerationLog("htlc-delayed-penalty") { - claimHtlcDelayedOutputPenaltyTx.map(htlcDelayedPenalty => { - val sig = keyManager.sign(htlcDelayedPenalty, keyManager.revocationPoint(channelKeyPath), remotePerCommitmentSecret, TxOwner.Local, commitmentFormat) - val signedTx = Transactions.addSigs(htlcDelayedPenalty, sig) - // we need to make sure that the tx is indeed valid - Transaction.correctlySpends(signedTx.tx, Seq(htlcTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - signedTx + val feeratePerKwPenalty = feeEstimator.getFeeratePerKw(target = 2) + + // first we will claim our main output right away + val mainTx = channelFeatures match { + case ct if ct.paysDirectlyToWallet => + log.info(s"channel uses option_static_remotekey to pay directly to our wallet, there is nothing to do") + None + case ct => ct.commitmentFormat match { + case DefaultCommitmentFormat => withTxGenerationLog("claim-p2wpkh-output") { + Transactions.makeClaimP2WPKHOutputTx(commitTx, localParams.dustLimit, localPaymentPubkey, localParams.defaultFinalScriptPubKey, feeratePerKwMain).map(claimMain => { + val sig = keyManager.sign(claimMain, keyManager.paymentPoint(channelKeyPath), remotePerCommitmentPoint, TxOwner.Local, commitmentFormat) + Transactions.addSigs(claimMain, localPaymentPubkey, sig) + }) + } + case _: AnchorOutputsCommitmentFormat => withTxGenerationLog("remote-main-delayed") { + Transactions.makeClaimRemoteDelayedOutputTx(commitTx, localParams.dustLimit, localPaymentPoint, localParams.defaultFinalScriptPubKey, feeratePerKwMain).map(claimMain => { + val sig = keyManager.sign(claimMain, keyManager.paymentPoint(channelKeyPath), TxOwner.Local, commitmentFormat) + Transactions.addSigs(claimMain, sig) + }) + } + } + } + + // then we punish them by stealing their main output + val mainPenaltyTx = withTxGenerationLog("main-penalty") { + Transactions.makeMainPenaltyTx(commitTx, localParams.dustLimit, remoteRevocationPubkey, localParams.defaultFinalScriptPubKey, localParams.toSelfDelay, remoteDelayedPaymentPubkey, feeratePerKwPenalty).map(txinfo => { + val sig = keyManager.sign(txinfo, keyManager.revocationPoint(channelKeyPath), remotePerCommitmentSecret, TxOwner.Local, commitmentFormat) + Transactions.addSigs(txinfo, sig) + }) + } + + // we retrieve the information needed to rebuild htlc scripts + val htlcInfos = db.listHtlcInfos(commitments.channelId, txNumber) + log.info(s"got htlcs=${htlcInfos.size} for txnumber=$txNumber") + val htlcsRedeemScripts = ( + htlcInfos.map { case (paymentHash, cltvExpiry) => Scripts.htlcReceived(remoteHtlcPubkey, localHtlcPubkey, remoteRevocationPubkey, Crypto.ripemd160(paymentHash), cltvExpiry, commitmentFormat) } ++ + htlcInfos.map { case (paymentHash, _) => Scripts.htlcOffered(remoteHtlcPubkey, localHtlcPubkey, remoteRevocationPubkey, Crypto.ripemd160(paymentHash), commitmentFormat) } + ) + .map(redeemScript => Script.write(pay2wsh(redeemScript)) -> Script.write(redeemScript)) + .toMap + + // and finally we steal the htlc outputs + val htlcPenaltyTxs = commitTx.txOut.zipWithIndex.collect { case (txOut, outputIndex) if htlcsRedeemScripts.contains(txOut.publicKeyScript) => + val htlcRedeemScript = htlcsRedeemScripts(txOut.publicKeyScript) + withTxGenerationLog("htlc-penalty") { + Transactions.makeHtlcPenaltyTx(commitTx, outputIndex, htlcRedeemScript, localParams.dustLimit, localParams.defaultFinalScriptPubKey, feeratePerKwPenalty).map(htlcPenalty => { + val sig = keyManager.sign(htlcPenalty, keyManager.revocationPoint(channelKeyPath), remotePerCommitmentSecret, TxOwner.Local, commitmentFormat) + Transactions.addSigs(htlcPenalty, sig, remoteRevocationPubkey) }) } - }) - val revokedCommitPublished1 = revokedCommitPublished.copy(claimHtlcDelayedPenaltyTxs = revokedCommitPublished.claimHtlcDelayedPenaltyTxs ++ penaltyTxs) - (revokedCommitPublished1, penaltyTxs) - }).getOrElse((revokedCommitPublished, Nil)) - } else { - (revokedCommitPublished, Nil) + }.toList.flatten + + RevokedCommitPublished( + commitTx = commitTx, + claimMainOutputTx = mainTx, + mainPenaltyTx = mainPenaltyTx, + htlcPenaltyTxs = htlcPenaltyTxs, + claimHtlcDelayedPenaltyTxs = Nil, // we will generate and spend those if they publish their HtlcSuccessTx or HtlcTimeoutTx + irrevocablySpent = Map.empty + ) + }) + } + + /** + * Claims the output of an [[HtlcSuccessTx]] or [[HtlcTimeoutTx]] transaction using a revocation key. + * + * In case a revoked commitment with pending HTLCs is published, there are two ways the HTLC outputs can be taken as punishment: + * - by spending the corresponding output of the commitment tx, using [[HtlcPenaltyTx]] that we generate as soon as we detect that a revoked commit + * as been spent; note that those transactions will compete with [[HtlcSuccessTx]] and [[HtlcTimeoutTx]] published by the counterparty. + * - by spending the delayed output of [[HtlcSuccessTx]] and [[HtlcTimeoutTx]] if those get confirmed; because the output of these txs is protected by + * an OP_CSV delay, we will have time to spend them with a revocation key. In that case, we generate the spending transactions "on demand", + * this is the purpose of this method. + * + * NB: when anchor outputs is used, htlc transactions can be aggregated in a single transaction if they share the same + * lockTime (thanks to the use of sighash_single | sighash_anyonecanpay), so we may need to claim multiple outputs. + */ + def claimHtlcTxOutputs(keyManager: ChannelKeyManager, commitments: Commitments, revokedCommitPublished: RevokedCommitPublished, htlcTx: Transaction, feeEstimator: FeeEstimator)(implicit log: LoggingAdapter): (RevokedCommitPublished, Seq[ClaimHtlcDelayedOutputPenaltyTx]) = { + val isHtlcTx = htlcTx.txIn.map(_.outPoint.txid).contains(revokedCommitPublished.commitTx.txid) && + htlcTx.txIn.map(_.witness).collect(Scripts.extractPreimageFromHtlcSuccess.orElse(Scripts.extractPaymentHashFromHtlcTimeout)).nonEmpty + if (isHtlcTx) { + log.info(s"looks like txid=${htlcTx.txid} could be a 2nd level htlc tx spending revoked commit txid=${revokedCommitPublished.commitTx.txid}") + // Let's assume that htlcTx is an HtlcSuccessTx or HtlcTimeoutTx and try to generate a tx spending its output using a revocation key + import commitments._ + val commitTx = revokedCommitPublished.commitTx + val obscuredTxNumber = Transactions.decodeTxNumber(commitTx.txIn.head.sequence, commitTx.lockTime) + val channelKeyPath = keyManager.keyPath(localParams, channelConfig) + val localPaymentPoint = localParams.walletStaticPaymentBasepoint.getOrElse(keyManager.paymentPoint(channelKeyPath).publicKey) + // this tx has been published by remote, so we need to invert local/remote params + val txNumber = Transactions.obscuredCommitTxNumber(obscuredTxNumber, !localParams.isFunder, remoteParams.paymentBasepoint, localPaymentPoint) + // now we know what commit number this tx is referring to, we can derive the commitment point from the shachain + remotePerCommitmentSecrets.getHash(0xFFFFFFFFFFFFL - txNumber) + .map(d => PrivateKey(d)) + .map(remotePerCommitmentSecret => { + val remotePerCommitmentPoint = remotePerCommitmentSecret.publicKey + val remoteDelayedPaymentPubkey = Generators.derivePubKey(remoteParams.delayedPaymentBasepoint, remotePerCommitmentPoint) + val remoteRevocationPubkey = Generators.revocationPubKey(keyManager.revocationPoint(channelKeyPath).publicKey, remotePerCommitmentPoint) + + // we need to use a high fee here for punishment txs because after a delay they can be spent by the counterparty + val feeratePerKwPenalty = feeEstimator.getFeeratePerKw(target = 1) + + val penaltyTxs = Transactions.makeClaimHtlcDelayedOutputPenaltyTxs(htlcTx, localParams.dustLimit, remoteRevocationPubkey, localParams.toSelfDelay, remoteDelayedPaymentPubkey, localParams.defaultFinalScriptPubKey, feeratePerKwPenalty).flatMap(claimHtlcDelayedOutputPenaltyTx => { + withTxGenerationLog("htlc-delayed-penalty") { + claimHtlcDelayedOutputPenaltyTx.map(htlcDelayedPenalty => { + val sig = keyManager.sign(htlcDelayedPenalty, keyManager.revocationPoint(channelKeyPath), remotePerCommitmentSecret, TxOwner.Local, commitmentFormat) + val signedTx = Transactions.addSigs(htlcDelayedPenalty, sig) + // we need to make sure that the tx is indeed valid + Transaction.correctlySpends(signedTx.tx, Seq(htlcTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + signedTx + }) + } + }) + val revokedCommitPublished1 = revokedCommitPublished.copy(claimHtlcDelayedPenaltyTxs = revokedCommitPublished.claimHtlcDelayedPenaltyTxs ++ penaltyTxs) + (revokedCommitPublished1, penaltyTxs) + }).getOrElse((revokedCommitPublished, Nil)) + } else { + (revokedCommitPublished, Nil) + } } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/ChannelDataSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/ChannelDataSpec.scala index 1715734ea2..58da425c64 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/ChannelDataSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/ChannelDataSpec.scala @@ -22,7 +22,7 @@ import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.WatchFundingSpentTriggered import fr.acinq.eclair.channel.Helpers.Closing import fr.acinq.eclair.channel.states.ChannelStateTestsHelperMethods import fr.acinq.eclair.transactions.Transactions._ -import fr.acinq.eclair.wire.protocol.{CommitSig, RevokeAndAck, UpdateAddHtlc} +import fr.acinq.eclair.wire.protocol.{CommitSig, RevokeAndAck, UnknownNextPeer, UpdateAddHtlc} import fr.acinq.eclair.{MilliSatoshiLong, NodeParams, TestKitBaseClass} import org.scalatest.funsuite.AnyFunSuiteLike import scodec.bits.ByteVector @@ -117,7 +117,7 @@ class ChannelDataSpec extends TestKitBaseClass with AnyFunSuiteLike with Channel val lcp3 = (htlcSuccessTxs.map(_.tx) ++ htlcTimeoutTxs.map(_.tx)).foldLeft(lcp) { case (current, tx) => - val (current1, Some(_)) = Closing.claimLocalCommitHtlcTxOutput(current, nodeParams.channelKeyManager, aliceClosing.commitments, tx, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) + val (current1, Some(_)) = Closing.LocalClose.claimHtlcDelayedOutput(current, nodeParams.channelKeyManager, aliceClosing.commitments, tx, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) Closing.updateLocalCommitPublished(current1, tx) } assert(!lcp3.isDone) @@ -140,7 +140,7 @@ class ChannelDataSpec extends TestKitBaseClass with AnyFunSuiteLike with Channel val lcp3 = (htlcSuccessTxs.map(_.tx) ++ htlcTimeoutTxs.map(_.tx)).foldLeft(lcp) { case (current, tx) => - val (current1, Some(_)) = Closing.claimLocalCommitHtlcTxOutput(current, nodeParams.channelKeyManager, aliceClosing.commitments, tx, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) + val (current1, Some(_)) = Closing.LocalClose.claimHtlcDelayedOutput(current, nodeParams.channelKeyManager, aliceClosing.commitments, tx, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) Closing.updateLocalCommitPublished(current1, tx) } assert(!lcp3.isDone) @@ -159,7 +159,7 @@ class ChannelDataSpec extends TestKitBaseClass with AnyFunSuiteLike with Channel assert(lcp5.claimHtlcDelayedTxs.length === 3) val newHtlcSuccessTx = lcp5.htlcTxs(remainingHtlcOutpoint).get.tx - val (lcp6, Some(newClaimHtlcDelayedTx)) = Closing.claimLocalCommitHtlcTxOutput(lcp5, nodeParams.channelKeyManager, aliceClosing.commitments, newHtlcSuccessTx, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) + val (lcp6, Some(newClaimHtlcDelayedTx)) = Closing.LocalClose.claimHtlcDelayedOutput(lcp5, nodeParams.channelKeyManager, aliceClosing.commitments, newHtlcSuccessTx, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) assert(lcp6.claimHtlcDelayedTxs.length === 4) val lcp7 = Closing.updateLocalCommitPublished(lcp6, newHtlcSuccessTx) @@ -176,7 +176,7 @@ class ChannelDataSpec extends TestKitBaseClass with AnyFunSuiteLike with Channel val remoteHtlcSuccess = rcp.claimHtlcTxs.values.collectFirst { case Some(tx: ClaimHtlcSuccessTx) => tx }.get val lcp3 = (htlcSuccessTxs.map(_.tx) ++ Seq(remoteHtlcSuccess.tx)).foldLeft(lcp) { case (current, tx) => - val (current1, _) = Closing.claimLocalCommitHtlcTxOutput(current, nodeParams.channelKeyManager, aliceClosing.commitments, tx, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) + val (current1, _) = Closing.LocalClose.claimHtlcDelayedOutput(current, nodeParams.channelKeyManager, aliceClosing.commitments, tx, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) Closing.updateLocalCommitPublished(current1, tx) } assert(lcp3.claimHtlcDelayedTxs.length === 1) @@ -187,7 +187,7 @@ class ChannelDataSpec extends TestKitBaseClass with AnyFunSuiteLike with Channel val remainingHtlcTimeoutTxs = htlcTimeoutTxs.filter(_.input.outPoint != remoteHtlcSuccess.input.outPoint) assert(remainingHtlcTimeoutTxs.length === 1) - val (lcp5, Some(remainingClaimHtlcTx)) = Closing.claimLocalCommitHtlcTxOutput(lcp4, nodeParams.channelKeyManager, aliceClosing.commitments, remainingHtlcTimeoutTxs.head.tx, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) + val (lcp5, Some(remainingClaimHtlcTx)) = Closing.LocalClose.claimHtlcDelayedOutput(lcp4, nodeParams.channelKeyManager, aliceClosing.commitments, remainingHtlcTimeoutTxs.head.tx, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) assert(lcp5.claimHtlcDelayedTxs.length === 2) val lcp6 = (remainingHtlcTimeoutTxs.map(_.tx) ++ Seq(remainingClaimHtlcTx.tx)).foldLeft(lcp5) { @@ -206,7 +206,7 @@ class ChannelDataSpec extends TestKitBaseClass with AnyFunSuiteLike with Channel val lcp3 = htlcTimeoutTxs.map(_.tx).foldLeft(lcp) { case (current, tx) => - val (current1, Some(_)) = Closing.claimLocalCommitHtlcTxOutput(current, nodeParams.channelKeyManager, aliceClosing.commitments, tx, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) + val (current1, Some(_)) = Closing.LocalClose.claimHtlcDelayedOutput(current, nodeParams.channelKeyManager, aliceClosing.commitments, tx, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) Closing.updateLocalCommitPublished(current1, tx) } assert(!lcp3.isDone) @@ -226,6 +226,37 @@ class ChannelDataSpec extends TestKitBaseClass with AnyFunSuiteLike with Channel assert(lcp6.isDone) } + test("local commit published (our HTLC txs are confirmed and the remaining HTLC is failed)") { + val f = setupClosingChannelForLocalClose() + import f._ + + val lcp3 = (htlcSuccessTxs.map(_.tx) ++ htlcTimeoutTxs.map(_.tx)).foldLeft(lcp) { + case (current, tx) => + val (current1, Some(_)) = Closing.LocalClose.claimHtlcDelayedOutput(current, nodeParams.channelKeyManager, aliceClosing.commitments, tx, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) + Closing.updateLocalCommitPublished(current1, tx) + } + + assert(!lcp3.isDone) + assert(lcp3.claimHtlcDelayedTxs.length === 3) + + val lcp4 = lcp3.claimHtlcDelayedTxs.map(_.tx).foldLeft(lcp3) { + case (current, tx) => Closing.updateLocalCommitPublished(current, tx) + } + assert(!lcp4.isDone) + + // at this point the pending incoming htlc is waiting for a preimage + assert(lcp4.htlcTxs(remainingHtlcOutpoint) === None) + + alice ! CMD_FAIL_HTLC(1, Right(UnknownNextPeer), replyTo_opt = Some(probe.ref)) + probe.expectMsgType[CommandSuccess[CMD_FAIL_HTLC]] + val aliceClosing1 = alice.stateData.asInstanceOf[DATA_CLOSING] + val lcp5 = aliceClosing1.localCommitPublished.get.copy(irrevocablySpent = lcp4.irrevocablySpent, claimHtlcDelayedTxs = lcp4.claimHtlcDelayedTxs) + assert(!lcp5.htlcTxs.contains(remainingHtlcOutpoint)) + assert(lcp5.claimHtlcDelayedTxs.length === 3) + + assert(lcp5.isDone) + } + case class RemoteFixture(bob: TestFSMRef[ChannelState, ChannelData, Channel], bobPendingHtlc: HtlcWithPreimage, remainingHtlcOutpoint: OutPoint, lcp: LocalCommitPublished, rcp: RemoteCommitPublished, claimHtlcTimeoutTxs: Seq[ClaimHtlcTimeoutTx], claimHtlcSuccessTxs: Seq[ClaimHtlcSuccessTx], probe: TestProbe) private def setupClosingChannelForRemoteClose(): RemoteFixture = { @@ -337,6 +368,27 @@ class ChannelDataSpec extends TestKitBaseClass with AnyFunSuiteLike with Channel assert(rcp5.isDone) } + test("remote commit published (our claim-HTLC txs are confirmed and the remaining one is failed)") { + val f = setupClosingChannelForRemoteClose() + import f._ + + val rcp3 = (claimHtlcSuccessTxs ++ claimHtlcTimeoutTxs).map(_.tx).foldLeft(rcp) { + case (current, tx) => Closing.updateRemoteCommitPublished(current, tx) + } + assert(!rcp3.isDone) + + bob ! CMD_FAIL_HTLC(bobPendingHtlc.htlc.id, Right(UnknownNextPeer), replyTo_opt = Some(probe.ref)) + probe.expectMsgType[CommandSuccess[CMD_FAIL_HTLC]] + val bobClosing1 = bob.stateData.asInstanceOf[DATA_CLOSING] + val rcp4 = bobClosing1.remoteCommitPublished.get.copy(irrevocablySpent = rcp3.irrevocablySpent) + assert(!rcp4.claimHtlcTxs.contains(remainingHtlcOutpoint)) + assert(rcp4.claimHtlcTxs.size === 3) + assert(getClaimHtlcSuccessTxs(rcp4).size == 1) + assert(getClaimHtlcTimeoutTxs(rcp4).size == 2) + + assert(rcp4.isDone) + } + private def setupClosingChannelForNextRemoteClose(): RemoteFixture = { val probe = TestProbe() val setup = init() diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/HelpersSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/HelpersSpec.scala index ba5134f264..bbe15df698 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/HelpersSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/HelpersSpec.scala @@ -259,12 +259,12 @@ class HelpersSpec extends TestKitBaseClass with AnyFunSuiteLike with ChannelStat ClosingTx(InputInfo(OutPoint(ByteVector32.Zeroes, 0), TxOut(1000 sat, Nil), Nil), Transaction(2, Nil, txOut, 0), None) } - assert(Closing.checkClosingDustAmounts(toClosingTx(allOutputsAboveDust))) - assert(!Closing.checkClosingDustAmounts(toClosingTx(p2pkhBelowDust))) - assert(!Closing.checkClosingDustAmounts(toClosingTx(p2shBelowDust))) - assert(!Closing.checkClosingDustAmounts(toClosingTx(p2wpkhBelowDust))) - assert(!Closing.checkClosingDustAmounts(toClosingTx(p2wshBelowDust))) - assert(!Closing.checkClosingDustAmounts(toClosingTx(futureSegwitBelowDust))) + assert(Closing.MutualClose.checkClosingDustAmounts(toClosingTx(allOutputsAboveDust))) + assert(!Closing.MutualClose.checkClosingDustAmounts(toClosingTx(p2pkhBelowDust))) + assert(!Closing.MutualClose.checkClosingDustAmounts(toClosingTx(p2shBelowDust))) + assert(!Closing.MutualClose.checkClosingDustAmounts(toClosingTx(p2wpkhBelowDust))) + assert(!Closing.MutualClose.checkClosingDustAmounts(toClosingTx(p2wshBelowDust))) + assert(!Closing.MutualClose.checkClosingDustAmounts(toClosingTx(futureSegwitBelowDust))) } test("tell closing type") { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/g/NegotiatingStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/g/NegotiatingStateSpec.scala index 30900556be..cf5c402107 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/g/NegotiatingStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/g/NegotiatingStateSpec.scala @@ -308,8 +308,8 @@ class NegotiatingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike val bobState = bob.stateData.asInstanceOf[DATA_NEGOTIATING] val bobKeyManager = bob.underlyingActor.nodeParams.channelKeyManager val bobScript = bobState.localShutdown.scriptPubKey - val (_, aliceClosingSigned) = Closing.makeClosingTx(aliceKeyManager, aliceState.commitments, aliceScript, bobScript, ClosingFees(closingFee, closingFee, closingFee)) - val (_, bobClosingSigned) = Closing.makeClosingTx(bobKeyManager, bobState.commitments, bobScript, aliceScript, ClosingFees(closingFee, closingFee, closingFee)) + val (_, aliceClosingSigned) = Closing.MutualClose.makeClosingTx(aliceKeyManager, aliceState.commitments, aliceScript, bobScript, ClosingFees(closingFee, closingFee, closingFee)) + val (_, bobClosingSigned) = Closing.MutualClose.makeClosingTx(bobKeyManager, bobState.commitments, bobScript, aliceScript, ClosingFees(closingFee, closingFee, closingFee)) (aliceClosingSigned.copy(tlvStream = TlvStream.empty), bobClosingSigned.copy(tlvStream = TlvStream.empty)) } From c4f67e7495a3c1273a22cca5bf69dac104add564 Mon Sep 17 00:00:00 2001 From: Pierre-Marie Padiou Date: Mon, 7 Mar 2022 10:28:52 +0100 Subject: [PATCH 031/121] Bump postgres-jdbc version (#2198) See https://github.com/advisories/GHSA-673j-qm5f-xpv8. We're not vulnerable, but it doesn't cost much to update the lib. --- eclair-core/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eclair-core/pom.xml b/eclair-core/pom.xml index d5df1bf005..6df076a33b 100644 --- a/eclair-core/pom.xml +++ b/eclair-core/pom.xml @@ -235,7 +235,7 @@ org.postgresql postgresql - 42.3.2 + 42.3.3 com.zaxxer From f3c6c7885de5b4c8c48302b06b25b1e7422c763c Mon Sep 17 00:00:00 2001 From: Richard Myers Date: Tue, 8 Mar 2022 13:25:19 +0100 Subject: [PATCH 032/121] Do not fail to relay payments with insufficient fee rate that satisfy the previous fee rate (#2201) Prevent relayed payments from failing due to an insufficient fee rate caused by a delay in gossip propagation with the new fee rate. Payments that satisfy the base and proportional relay fee rates from either the current or previous local channel update will not be failed during the `relay.fees.enforcement-delay` period (default 10 minutes) after the latest local fee update. The previous fee update is only stored in memory, so after restarting the node all fee updates will be enforced immediately. To enforce a fee update with out a delay you can also issue the same update twice so that the previous and current fee rates are the same. --- eclair-core/src/main/resources/reference.conf | 2 + .../scala/fr/acinq/eclair/NodeParams.scala | 3 +- .../eclair/payment/relay/ChannelRelay.scala | 19 ++++--- .../eclair/payment/relay/ChannelRelayer.scala | 7 ++- .../acinq/eclair/payment/relay/Relayer.scala | 6 ++- .../scala/fr/acinq/eclair/TestConstants.scala | 8 +-- .../payment/relay/ChannelRelayerSpec.scala | 49 +++++++++++++++---- 7 files changed, 67 insertions(+), 27 deletions(-) diff --git a/eclair-core/src/main/resources/reference.conf b/eclair-core/src/main/resources/reference.conf index 8a7cf27968..4e7450a9ee 100644 --- a/eclair-core/src/main/resources/reference.conf +++ b/eclair-core/src/main/resources/reference.conf @@ -132,6 +132,8 @@ eclair { fee-base-msat = 1000 fee-proportional-millionths = 100 } + // Delay enforcement of channel fee updates + enforcement-delay = 10 minutes } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala index 1ca4fdb070..4492cf6d89 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala @@ -21,8 +21,8 @@ import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.bitcoin.{Block, ByteVector32, Crypto, Satoshi} import fr.acinq.eclair.Setup.Seeds import fr.acinq.eclair.blockchain.fee._ -import fr.acinq.eclair.channel.{Channel, ChannelFlags} import fr.acinq.eclair.channel.Channel.{ChannelConf, UnhandledExceptionStrategy} +import fr.acinq.eclair.channel.{Channel, ChannelFlags} import fr.acinq.eclair.crypto.Noise.KeyPair import fr.acinq.eclair.crypto.keymanager.{ChannelKeyManager, NodeKeyManager} import fr.acinq.eclair.db._ @@ -462,6 +462,7 @@ object NodeParams extends Logging { publicChannelFees = getRelayFees(config.getConfig("relay.fees.public-channels")), privateChannelFees = getRelayFees(config.getConfig("relay.fees.private-channels")), minTrampolineFees = getRelayFees(config.getConfig("relay.fees.min-trampoline")), + enforcementDelay = FiniteDuration(config.getDuration("relay.fees.enforcement-delay").getSeconds, TimeUnit.SECONDS) ), db = database, autoReconnect = config.getBoolean("auto-reconnect"), diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/ChannelRelay.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/ChannelRelay.scala index 5f37a435fb..830eeb52f5 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/ChannelRelay.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/ChannelRelay.scala @@ -28,7 +28,7 @@ import fr.acinq.eclair.payment.Monitoring.{Metrics, Tags} import fr.acinq.eclair.payment.relay.Relayer.OutgoingChannel import fr.acinq.eclair.payment.{ChannelPaymentRelayed, IncomingPaymentPacket} import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{Logs, NodeParams, ShortChannelId, channel, nodeFee} +import fr.acinq.eclair.{Logs, NodeParams, ShortChannelId, TimestampSecond, channel, nodeFee} import java.util.UUID @@ -172,7 +172,7 @@ class ChannelRelay private(nodeParams: NodeParams, def handleRelay(previousFailures: Seq[PreviouslyTried]): RelayResult = { val alreadyTried = previousFailures.map(_.shortChannelId) selectPreferredChannel(alreadyTried) - .flatMap(selectedShortChannelId => channels.get(selectedShortChannelId).map(_.channelUpdate)) match { + .flatMap(selectedShortChannelId => channels.get(selectedShortChannelId)) match { case None if previousFailures.nonEmpty => // no more channels to try val error = previousFailures @@ -182,8 +182,8 @@ class ChannelRelay private(nodeParams: NodeParams, .getOrElse(previousFailures.head) .failure RelayFailure(CMD_FAIL_HTLC(r.add.id, Right(translateLocalError(error.t, error.channelUpdate)), commit = true)) - case channelUpdate_opt => - relayOrFail(channelUpdate_opt) + case outgoingChannel_opt => + relayOrFail(outgoingChannel_opt) } } @@ -206,7 +206,7 @@ class ChannelRelay private(nodeParams: NodeParams, // and we filter again to keep the ones that are compatible with this payment (mainly fees, expiry delta) candidateChannels .map { case (shortChannelId, channelInfo) => - val relayResult = relayOrFail(Some(channelInfo.channelUpdate)) + val relayResult = relayOrFail(Some(channelInfo)) context.log.debug(s"candidate channel: shortChannelId=$shortChannelId availableForSend={} capacity={} channelUpdate={} result={}", channelInfo.commitments.availableBalanceForSend, channelInfo.commitments.capacity, @@ -250,9 +250,9 @@ class ChannelRelay private(nodeParams: NodeParams, * channel, because some parameters don't match with our settings for that channel. In that case we directly fail the * htlc. */ - def relayOrFail(channelUpdate_opt: Option[ChannelUpdate]): RelayResult = { + def relayOrFail(outgoingChannel_opt: Option[OutgoingChannel]): RelayResult = { import r._ - channelUpdate_opt match { + outgoingChannel_opt.map(_.channelUpdate) match { case None => RelayFailure(CMD_FAIL_HTLC(add.id, Right(UnknownNextPeer), commit = true)) case Some(channelUpdate) if !channelUpdate.channelFlags.isEnabled => @@ -261,7 +261,10 @@ class ChannelRelay private(nodeParams: NodeParams, RelayFailure(CMD_FAIL_HTLC(add.id, Right(AmountBelowMinimum(payload.amountToForward, channelUpdate)), commit = true)) case Some(channelUpdate) if r.expiryDelta < channelUpdate.cltvExpiryDelta => RelayFailure(CMD_FAIL_HTLC(add.id, Right(IncorrectCltvExpiry(payload.outgoingCltv, channelUpdate)), commit = true)) - case Some(channelUpdate) if r.relayFeeMsat < nodeFee(channelUpdate, payload.amountToForward) => + case Some(channelUpdate) if r.relayFeeMsat < nodeFee(channelUpdate, payload.amountToForward) && + // fees also do not satisfy the previous channel update for `enforcementDelay` seconds after current update + (TimestampSecond.now() - channelUpdate.timestamp > nodeParams.relayParams.enforcementDelay || + outgoingChannel_opt.flatMap(_.prevChannelUpdate).forall(c => r.relayFeeMsat < nodeFee(c, payload.amountToForward))) => RelayFailure(CMD_FAIL_HTLC(add.id, Right(FeeInsufficient(add.amountMsat, channelUpdate)), commit = true)) case Some(channelUpdate) => val origin = Origin.ChannelRelayedHot(addResponseAdapter.toClassic, add, payload.amountToForward) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/ChannelRelayer.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/ChannelRelayer.scala index 2ba915549b..c8809eb199 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/ChannelRelayer.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/ChannelRelayer.scala @@ -16,8 +16,6 @@ package fr.acinq.eclair.payment.relay -import java.util.UUID - import akka.actor.ActorRef import akka.actor.typed.Behavior import akka.actor.typed.eventstream.EventStream @@ -25,9 +23,9 @@ import akka.actor.typed.scaladsl.Behaviors import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.eclair.channel._ import fr.acinq.eclair.payment.IncomingPaymentPacket -import fr.acinq.eclair.router.Announcements import fr.acinq.eclair.{Logs, NodeParams, ShortChannelId} +import java.util.UUID import scala.collection.mutable /** @@ -95,7 +93,8 @@ object ChannelRelayer { case WrappedLocalChannelUpdate(LocalChannelUpdate(_, channelId, shortChannelId, remoteNodeId, _, channelUpdate, commitments)) => context.log.debug(s"updating local channel info for channelId=$channelId shortChannelId=$shortChannelId remoteNodeId=$remoteNodeId channelUpdate={} commitments={}", channelUpdate, commitments) - val channelUpdates1 = channelUpdates + (channelUpdate.shortChannelId -> Relayer.OutgoingChannel(remoteNodeId, channelUpdate, commitments)) + val prevChannelUpdate = channelUpdates.get(shortChannelId).map(_.channelUpdate) + val channelUpdates1 = channelUpdates + (channelUpdate.shortChannelId -> Relayer.OutgoingChannel(remoteNodeId, channelUpdate, prevChannelUpdate, commitments)) val node2channels1 = node2channels.addOne(remoteNodeId, channelUpdate.shortChannelId) apply(nodeParams, register, channelUpdates1, node2channels1) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/Relayer.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/Relayer.scala index c2af6d7f9d..b95982e74e 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/Relayer.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/Relayer.scala @@ -32,6 +32,7 @@ import fr.acinq.eclair.{Logs, MilliSatoshi, NodeParams, ShortChannelId} import grizzled.slf4j.Logging import scala.concurrent.Promise +import scala.concurrent.duration.FiniteDuration /** * Created by PM on 01/02/2017. @@ -119,7 +120,8 @@ object Relayer extends Logging { case class RelayParams(publicChannelFees: RelayFees, privateChannelFees: RelayFees, - minTrampolineFees: RelayFees) { + minTrampolineFees: RelayFees, + enforcementDelay: FiniteDuration) { def defaultFees(announceChannel: Boolean): RelayFees = { if (announceChannel) { publicChannelFees @@ -138,7 +140,7 @@ object Relayer extends Logging { * @param enabledOnly if true, filter out disabled channels. */ case class GetOutgoingChannels(enabledOnly: Boolean = true) - case class OutgoingChannel(nextNodeId: PublicKey, channelUpdate: ChannelUpdate, commitments: AbstractCommitments) { + case class OutgoingChannel(nextNodeId: PublicKey, channelUpdate: ChannelUpdate, prevChannelUpdate: Option[ChannelUpdate], commitments: AbstractCommitments) { def toUsableBalance: UsableBalance = UsableBalance( remoteNodeId = nextNodeId, shortChannelId = channelUpdate.shortChannelId, diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala b/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala index c16bd94de6..64ca14ba0e 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala @@ -138,7 +138,8 @@ object TestConstants { feeProportionalMillionths = 20), minTrampolineFees = RelayFees( feeBase = 548000 msat, - feeProportionalMillionths = 30)), + feeProportionalMillionths = 30), + enforcementDelay = 10 minutes), db = TestDatabases.inMemoryDb(), autoReconnect = false, initialRandomReconnectDelay = 5 seconds, @@ -195,7 +196,7 @@ object TestConstants { relayPolicy = RelayAll, timeout = 1 minute ), - purgeInvoicesInterval = Some(24 hours) + purgeInvoicesInterval = Some(24 hours), ) def channelParams: LocalParams = Peer.makeChannelParams( @@ -274,7 +275,8 @@ object TestConstants { feeProportionalMillionths = 20), minTrampolineFees = RelayFees( feeBase = 548000 msat, - feeProportionalMillionths = 30)), + feeProportionalMillionths = 30), + enforcementDelay = 10 minutes), db = TestDatabases.inMemoryDb(), autoReconnect = false, initialRandomReconnectDelay = 5 seconds, diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/ChannelRelayerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/ChannelRelayerSpec.scala index 04c8d17387..6875f18a70 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/ChannelRelayerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/ChannelRelayerSpec.scala @@ -237,6 +237,37 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, Right(FeeInsufficient(r.add.amountMsat, u.channelUpdate)), commit = true)) } + test("relay an htlc-add that would fail (fee insufficient) with a recent channel update but succeed with the previous update") { f => + import f._ + + val payload = RelayLegacyPayload(shortId1, outgoingAmount, outgoingExpiry) + val r = createValidIncomingPacket(outgoingAmount + 1.msat, CltvExpiry(400100), payload) + val u1 = createLocalUpdate(shortId1, timestamp = TimestampSecond.now(), feeBaseMsat = 1 msat, feeProportionalMillionths = 0) + + channelRelayer ! WrappedLocalChannelUpdate(u1) + channelRelayer ! Relay(r) + + // relay succeeds with current channel update (u1) with lower fees + expectFwdAdd(register, shortId1, outgoingAmount, outgoingExpiry) + + val u2 = createLocalUpdate(shortId1, timestamp = TimestampSecond.now() - 530) + + channelRelayer ! WrappedLocalChannelUpdate(u2) + channelRelayer ! Relay(r) + + // relay succeeds because the current update (u2) with higher fees occurred less than 10 minutes ago + expectFwdAdd(register, shortId1, outgoingAmount, outgoingExpiry) + + val u3 = createLocalUpdate(shortId1, timestamp = TimestampSecond.now() - 601) + + channelRelayer ! WrappedLocalChannelUpdate(u1) + channelRelayer ! WrappedLocalChannelUpdate(u3) + channelRelayer ! Relay(r) + + // relay fails because the current update (u3) with higher fees occurred more than 10 minutes ago + expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, Right(FeeInsufficient(r.add.amountMsat, u3.channelUpdate)), commit = true)) + } + test("fail to relay an htlc-add (local error)") { f => import f._ @@ -470,15 +501,15 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a } object ChannelRelayerSpec { - val paymentPreimage = randomBytes32() - val paymentHash = Crypto.sha256(paymentPreimage) + val paymentPreimage: ByteVector32 = randomBytes32() + val paymentHash: ByteVector32 = Crypto.sha256(paymentPreimage) - val outgoingAmount = 1000000 msat - val outgoingExpiry = CltvExpiry(400000) - val outgoingNodeId = randomKey().publicKey + val outgoingAmount: MilliSatoshi = 1000000 msat + val outgoingExpiry: CltvExpiry = CltvExpiry(400000) + val outgoingNodeId: PublicKey = randomKey().publicKey - val shortId1 = ShortChannelId(111111) - val shortId2 = ShortChannelId(222222) + val shortId1: ShortChannelId = ShortChannelId(111111) + val shortId2: ShortChannelId = ShortChannelId(222222) val channelIds = Map( shortId1 -> randomBytes32(), @@ -490,9 +521,9 @@ object ChannelRelayerSpec { ChannelRelayPacket(add_ab, payload, emptyOnionPacket) } - def createLocalUpdate(shortChannelId: ShortChannelId, balance: MilliSatoshi = 10000000 msat, capacity: Satoshi = 500000 sat, enabled: Boolean = true, htlcMinimum: MilliSatoshi = 0 msat): LocalChannelUpdate = { + def createLocalUpdate(shortChannelId: ShortChannelId, balance: MilliSatoshi = 10000000 msat, capacity: Satoshi = 500000 sat, enabled: Boolean = true, htlcMinimum: MilliSatoshi = 0 msat, timestamp: TimestampSecond = 0 unixsec, feeBaseMsat: MilliSatoshi = 1000 msat, feeProportionalMillionths: Long = 100): LocalChannelUpdate = { val channelId = channelIds(shortChannelId) - val update = ChannelUpdate(ByteVector64(randomBytes(64)), Block.RegtestGenesisBlock.hash, shortChannelId, 0 unixsec, ChannelUpdate.ChannelFlags(isNode1 = true, isEnabled = enabled), CltvExpiryDelta(100), htlcMinimum, 1000 msat, 100, Some(capacity.toMilliSatoshi)) + val update = ChannelUpdate(ByteVector64(randomBytes(64)), Block.RegtestGenesisBlock.hash, shortChannelId, timestamp, ChannelUpdate.ChannelFlags(isNode1 = true, isEnabled = enabled), CltvExpiryDelta(100), htlcMinimum, feeBaseMsat, feeProportionalMillionths, Some(capacity.toMilliSatoshi)) val commitments = PaymentPacketSpec.makeCommitments(channelId, testAvailableBalanceForSend = balance, testCapacity = capacity) LocalChannelUpdate(null, channelId, shortChannelId, outgoingNodeId, None, update, commitments) } From 8f2c93c1ab34ceeb5ea697be3c499916078b9130 Mon Sep 17 00:00:00 2001 From: Thomas HUET <81159533+thomash-acinq@users.noreply.github.com> Date: Fri, 11 Mar 2022 10:22:27 +0100 Subject: [PATCH 033/121] Add delayed fees to release notes (#2204) Add release notes for #2201 --- docs/release-notes/eclair-vnext.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/release-notes/eclair-vnext.md b/docs/release-notes/eclair-vnext.md index 2b481a1a2d..97a737e03b 100644 --- a/docs/release-notes/eclair-vnext.md +++ b/docs/release-notes/eclair-vnext.md @@ -12,7 +12,12 @@ ### Miscellaneous improvements and bug fixes - +#### Delay enforcement of new channel fees + +When updating the relay fees for a channel, eclair can now continue accepting to relay payments using the old fee even if they would be rejected with the new fee. +By default, eclair will still accept the old fee for 10 minutes, you can change it by setting `eclair.relay.fees.enforcement-delay` to a different value. + +If you want a specific fee update to ignore this delay, you can update the fee twice to make eclair forget about the previous fee. ## Verifying signatures From f14300e33f86e71c5fbaf0f46c75b12abaaab8a9 Mon Sep 17 00:00:00 2001 From: Richard Myers Date: Fri, 11 Mar 2022 16:26:57 +0100 Subject: [PATCH 034/121] two new min-funding config parameters for public and private channels (#2203) Add the ability to set different min-funding limits for private and public channels. Replace min-funding-satoshis config parameter with min-public-funding-satoshis and min-private-funding-satoshis. Show a deprecation message if min-funding-satoshis is defined. --- eclair-core/src/main/resources/reference.conf | 3 ++- .../scala/fr/acinq/eclair/NodeParams.scala | 4 +++- .../fr/acinq/eclair/channel/Channel.scala | 7 ++++-- .../fr/acinq/eclair/channel/Helpers.scala | 6 ++--- .../scala/fr/acinq/eclair/StartupSpec.scala | 18 ++++++++++++++- .../scala/fr/acinq/eclair/TestConstants.scala | 6 +++-- .../a/WaitForOpenChannelStateSpec.scala | 22 ++++++++++++++----- 7 files changed, 51 insertions(+), 15 deletions(-) diff --git a/eclair-core/src/main/resources/reference.conf b/eclair-core/src/main/resources/reference.conf index 4e7450a9ee..6096b75edc 100644 --- a/eclair-core/src/main/resources/reference.conf +++ b/eclair-core/src/main/resources/reference.conf @@ -82,7 +82,8 @@ eclair { reserve-to-funding-ratio = 0.01 // recommended by BOLT #2 max-reserve-to-funding-ratio = 0.05 // channel reserve can't be more than 5% of the funding amount (recommended: 1%) - min-funding-satoshis = 100000 + min-public-funding-satoshis = 100000 + min-private-funding-satoshis = 100000 max-funding-satoshis = 16777215 // to open channels larger than 16777215 you must enable the large_channel_support feature in 'eclair.features' to-remote-delay-blocks = 720 // number of blocks that the other node's to-self outputs must be delayed (720 ~ 5 days) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala index 4492cf6d89..23480c75f4 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala @@ -226,6 +226,7 @@ object NodeParams extends Logging { // v0.7.1 "payment-request-expiry" -> "invoice-expiry", "override-features" -> "override-init-features", + "channel.min-funding-satoshis" -> "channel.min-public-funding-satoshis, channel.min-private-funding-satoshis" ) deprecatedKeyPaths.foreach { case (old, new_) => require(!config.hasPath(old), s"configuration key '$old' has been replaced by '$new_'") @@ -417,7 +418,8 @@ object NodeParams extends Logging { maxAcceptedHtlcs = maxAcceptedHtlcs, reserveToFundingRatio = config.getDouble("channel.reserve-to-funding-ratio"), maxReserveToFundingRatio = config.getDouble("channel.max-reserve-to-funding-ratio"), - minFundingSatoshis = Satoshi(config.getLong("channel.min-funding-satoshis")), + minFundingPublicSatoshis = Satoshi(config.getLong("channel.min-public-funding-satoshis")), + minFundingPrivateSatoshis = Satoshi(config.getLong("channel.min-private-funding-satoshis")), maxFundingSatoshis = Satoshi(config.getLong("channel.max-funding-satoshis")), toRemoteDelay = offeredCLTV, maxToLocalDelay = maxToLocalCLTV, diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala index 5316b2486b..50e56829fe 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala @@ -71,7 +71,8 @@ object Channel { maxAcceptedHtlcs: Int, reserveToFundingRatio: Double, maxReserveToFundingRatio: Double, - minFundingSatoshis: Satoshi, + minFundingPublicSatoshis: Satoshi, + minFundingPrivateSatoshis: Satoshi, maxFundingSatoshis: Satoshi, toRemoteDelay: CltvExpiryDelta, maxToLocalDelay: CltvExpiryDelta, @@ -82,7 +83,9 @@ object Channel { maxBlockProcessingDelay: FiniteDuration, maxTxPublishRetryDelay: FiniteDuration, unhandledExceptionStrategy: UnhandledExceptionStrategy, - revocationTimeout: FiniteDuration) + revocationTimeout: FiniteDuration) { + def minFundingSatoshis(announceChannel: Boolean): Satoshi = if (announceChannel) minFundingPublicSatoshis else minFundingPrivateSatoshis + } trait TxPublisherFactory { def spawnTxPublisher(context: ActorContext, remoteNodeId: PublicKey): typed.ActorRef[TxPublisher.Command] diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala index dd3a21883e..9a52085375 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala @@ -104,10 +104,10 @@ object Helpers { // MUST reject the channel. if (nodeParams.chainHash != open.chainHash) return Left(InvalidChainHash(open.temporaryChannelId, local = nodeParams.chainHash, remote = open.chainHash)) - if (open.fundingSatoshis < nodeParams.channelConf.minFundingSatoshis || open.fundingSatoshis > nodeParams.channelConf.maxFundingSatoshis) return Left(InvalidFundingAmount(open.temporaryChannelId, open.fundingSatoshis, nodeParams.channelConf.minFundingSatoshis, nodeParams.channelConf.maxFundingSatoshis)) + if (open.fundingSatoshis < nodeParams.channelConf.minFundingSatoshis(open.channelFlags.announceChannel) || open.fundingSatoshis > nodeParams.channelConf.maxFundingSatoshis) return Left(InvalidFundingAmount(open.temporaryChannelId, open.fundingSatoshis, nodeParams.channelConf.minFundingSatoshis(open.channelFlags.announceChannel), nodeParams.channelConf.maxFundingSatoshis)) // BOLT #2: Channel funding limits - if (open.fundingSatoshis >= Channel.MAX_FUNDING && !localFeatures.hasFeature(Features.Wumbo)) return Left(InvalidFundingAmount(open.temporaryChannelId, open.fundingSatoshis, nodeParams.channelConf.minFundingSatoshis, Channel.MAX_FUNDING)) + if (open.fundingSatoshis >= Channel.MAX_FUNDING && !localFeatures.hasFeature(Features.Wumbo)) return Left(InvalidFundingAmount(open.temporaryChannelId, open.fundingSatoshis, nodeParams.channelConf.minFundingSatoshis(open.channelFlags.announceChannel), Channel.MAX_FUNDING)) // BOLT #2: The receiving node MUST fail the channel if: push_msat is greater than funding_satoshis * 1000. if (open.pushMsat > open.fundingSatoshis) return Left(InvalidPushAmount(open.temporaryChannelId, open.pushMsat, open.fundingSatoshis.toMilliSatoshi)) @@ -502,7 +502,7 @@ object Helpers { object MutualClose { // used only to compute tx weights and estimate fees - lazy val dummyPublicKey = PrivateKey(ByteVector32(ByteVector.fill(32)(1))).publicKey + lazy val dummyPublicKey: PublicKey = PrivateKey(ByteVector32(ByteVector.fill(32)(1))).publicKey def isValidFinalScriptPubkey(scriptPubKey: ByteVector, allowAnySegwit: Boolean): Boolean = { Try(Script.parse(scriptPubKey)) match { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/StartupSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/StartupSpec.scala index 9c308967d7..601d8288f8 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/StartupSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/StartupSpec.scala @@ -33,7 +33,7 @@ import scala.util.Try class StartupSpec extends AnyFunSuite { - val defaultConf = ConfigFactory.load("reference.conf").getConfig("eclair") + val defaultConf: Config = ConfigFactory.load("reference.conf").getConfig("eclair") def makeNodeParamsWithDefaults(conf: Config): NodeParams = { val blockCount = new AtomicLong(0) @@ -257,4 +257,20 @@ class StartupSpec extends AnyFunSuite { assert(Try(makeNodeParamsWithDefaults(noHtlcMinimumConf.withFallback(defaultConf))).isFailure) } + test("NodeParams should fail with deprecated channel.min-funding-satoshis set") { + val illegalGlobalFeaturesConf = ConfigFactory.parseString("channel.min-funding-satoshis = 200000") + val illegalConf = illegalGlobalFeaturesConf.withFallback(defaultConf) + + val nodeParamsAttempt1 = Try(makeNodeParamsWithDefaults(illegalConf)) + assert(nodeParamsAttempt1.isFailure && nodeParamsAttempt1.failed.get.getMessage.contains("channel.min-public-funding-satoshis, channel.min-private-funding-satoshis")) + + val newGlobalFeaturesConf = ConfigFactory.parseMap(Map( + s"channel.min-public-funding-satoshis" -> "20000", + s"channel.min-private-funding-satoshis" -> "20000", + ).asJava) + val legalConf = newGlobalFeaturesConf.withFallback(defaultConf) + val nodeParamsAttempt2 = Try(makeNodeParamsWithDefaults(legalConf)) + assert(nodeParamsAttempt2.isSuccess) + } + } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala b/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala index 64ca14ba0e..d184840433 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala @@ -118,7 +118,8 @@ object TestConstants { unhandledExceptionStrategy = UnhandledExceptionStrategy.LocalClose, revocationTimeout = 20 seconds, channelFlags = ChannelFlags.Public, - minFundingSatoshis = 1000 sat, + minFundingPublicSatoshis = 1000 sat, + minFundingPrivateSatoshis = 900 sat, maxFundingSatoshis = 16777215 sat, ), onChainFeeConf = OnChainFeeConf( @@ -255,7 +256,8 @@ object TestConstants { unhandledExceptionStrategy = UnhandledExceptionStrategy.LocalClose, revocationTimeout = 20 seconds, channelFlags = ChannelFlags.Public, - minFundingSatoshis = 1000 sat, + minFundingPublicSatoshis = 1000 sat, + minFundingPrivateSatoshis = 900 sat, maxFundingSatoshis = 16777215 sat, ), onChainFeeConf = OnChainFeeConf( diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForOpenChannelStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForOpenChannelStateSpec.scala index 518281c635..f376f8a3ac 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForOpenChannelStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForOpenChannelStateSpec.scala @@ -112,13 +112,25 @@ class WaitForOpenChannelStateSpec extends TestKitBaseClass with FixtureAnyFunSui awaitCond(bob.stateName == CLOSED) } - test("recv OpenChannel (funding too low)") { f => + test("recv OpenChannel (funding too low, public channel)") { f => import f._ val open = alice2bob.expectMsgType[OpenChannel] val lowFunding = 100.sat - bob ! open.copy(fundingSatoshis = lowFunding) + val announceChannel = true + bob ! open.copy(fundingSatoshis = lowFunding, channelFlags = ChannelFlags(announceChannel)) val error = bob2alice.expectMsgType[Error] - assert(error === Error(open.temporaryChannelId, InvalidFundingAmount(open.temporaryChannelId, lowFunding, Bob.nodeParams.channelConf.minFundingSatoshis, Bob.nodeParams.channelConf.maxFundingSatoshis).getMessage)) + assert(error === Error(open.temporaryChannelId, InvalidFundingAmount(open.temporaryChannelId, lowFunding, Bob.nodeParams.channelConf.minFundingSatoshis(announceChannel), Bob.nodeParams.channelConf.maxFundingSatoshis).getMessage)) + awaitCond(bob.stateName == CLOSED) + } + + test("recv OpenChannel (funding too low, private channel)") { f => + import f._ + val open = alice2bob.expectMsgType[OpenChannel] + val lowFunding = 100.sat + val announceChannel = false + bob ! open.copy(fundingSatoshis = lowFunding, channelFlags = ChannelFlags(announceChannel)) + val error = bob2alice.expectMsgType[Error] + assert(error === Error(open.temporaryChannelId, InvalidFundingAmount(open.temporaryChannelId, lowFunding, Bob.nodeParams.channelConf.minFundingSatoshis(announceChannel), Bob.nodeParams.channelConf.maxFundingSatoshis).getMessage)) awaitCond(bob.stateName == CLOSED) } @@ -128,7 +140,7 @@ class WaitForOpenChannelStateSpec extends TestKitBaseClass with FixtureAnyFunSui val highFundingMsat = 100000000.sat bob ! open.copy(fundingSatoshis = highFundingMsat) val error = bob2alice.expectMsgType[Error] - assert(error.toAscii === Error(open.temporaryChannelId, InvalidFundingAmount(open.temporaryChannelId, highFundingMsat, Bob.nodeParams.channelConf.minFundingSatoshis, Bob.nodeParams.channelConf.maxFundingSatoshis).getMessage).toAscii) + assert(error === Error(open.temporaryChannelId, InvalidFundingAmount(open.temporaryChannelId, highFundingMsat, Bob.nodeParams.channelConf.minFundingSatoshis(open.channelFlags.announceChannel), Bob.nodeParams.channelConf.maxFundingSatoshis).getMessage)) awaitCond(bob.stateName == CLOSED) } @@ -138,7 +150,7 @@ class WaitForOpenChannelStateSpec extends TestKitBaseClass with FixtureAnyFunSui val highFundingSat = Bob.nodeParams.channelConf.maxFundingSatoshis + Btc(1) bob ! open.copy(fundingSatoshis = highFundingSat) val error = bob2alice.expectMsgType[Error] - assert(error.toAscii === Error(open.temporaryChannelId, InvalidFundingAmount(open.temporaryChannelId, highFundingSat, Bob.nodeParams.channelConf.minFundingSatoshis, Bob.nodeParams.channelConf.maxFundingSatoshis).getMessage).toAscii) + assert(error === Error(open.temporaryChannelId, InvalidFundingAmount(open.temporaryChannelId, highFundingSat, Bob.nodeParams.channelConf.minFundingSatoshis(open.channelFlags.announceChannel), Bob.nodeParams.channelConf.maxFundingSatoshis).getMessage)) } test("recv OpenChannel (invalid max accepted htlcs)") { f => From 7b5cefaf99a4ccbe7462ff663f52447b84b124c1 Mon Sep 17 00:00:00 2001 From: Pierre-Marie Padiou Date: Tue, 15 Mar 2022 16:04:20 +0100 Subject: [PATCH 035/121] Replace `FeatureScope` by type hierarchy (#2207) Instead of defining a separate type `FeatureScope` with its own hierarchy, that was then mixed in `Feature` using the cake pattern, we go with a simpler type hierarchy for `Feature`. This significantly simplifies type declarations (no more `Feature with FeatureScope`) especially in the tests. --- .../main/scala/fr/acinq/eclair/Eclair.scala | 2 +- .../main/scala/fr/acinq/eclair/Features.scala | 47 +++++++++---------- .../scala/fr/acinq/eclair/NodeParams.scala | 8 ++-- .../eclair/channel/ChannelFeatures.scala | 22 ++++----- .../fr/acinq/eclair/channel/Helpers.scala | 2 +- .../fr/acinq/eclair/io/PeerConnection.scala | 2 +- .../acinq/eclair/json/JsonSerializers.scala | 2 +- .../acinq/eclair/payment/Bolt11Invoice.scala | 8 ++-- .../remote/EclairInternalsSerializer.scala | 4 +- .../acinq/eclair/router/Announcements.scala | 8 ++-- .../channel/version0/ChannelTypes0.scala | 6 +-- .../protocol/LightningMessageCodecs.scala | 4 +- .../wire/protocol/LightningMessageTypes.scala | 6 +-- .../scala/fr/acinq/eclair/FeaturesSpec.scala | 34 +++++++------- .../scala/fr/acinq/eclair/TestConstants.scala | 6 +-- .../eclair/channel/ChannelFeaturesSpec.scala | 42 +++++++++-------- .../scala/fr/acinq/eclair/io/PeerSpec.scala | 2 +- .../eclair/payment/Bolt11InvoiceSpec.scala | 8 ++-- .../eclair/payment/MultiPartHandlerSpec.scala | 8 ++-- .../eclair/payment/PaymentInitiatorSpec.scala | 12 ++--- .../eclair/router/AnnouncementsSpec.scala | 2 +- .../protocol/LightningMessageCodecsSpec.scala | 2 +- 22 files changed, 118 insertions(+), 119 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala index afdd80e517..4ca1fb1cea 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala @@ -60,7 +60,7 @@ import scala.collection.immutable.SortedMap import scala.concurrent.duration._ import scala.concurrent.{ExecutionContext, Future, Promise} -case class GetInfoResponse(version: String, nodeId: PublicKey, alias: String, color: String, features: Features[FeatureScope], chainHash: ByteVector32, network: String, blockHeight: Int, publicAddresses: Seq[NodeAddress], onionAddress: Option[NodeAddress], instanceId: String) +case class GetInfoResponse(version: String, nodeId: PublicKey, alias: String, color: String, features: Features[Feature], chainHash: ByteVector32, network: String, blockHeight: Int, publicAddresses: Seq[NodeAddress], onionAddress: Option[NodeAddress], instanceId: String) case class AuditResponse(sent: Seq[PaymentSent], received: Seq[PaymentReceived], relayed: Seq[PaymentRelayed]) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala index 65b3b349d6..268b53b7c2 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala @@ -34,10 +34,9 @@ object FeatureSupport { case object Optional extends FeatureSupport { override def toString: String = "optional" } } +/** Not a sealed trait, so it can be extended by plugins. */ trait Feature { - this: FeatureScope => - def rfcName: String def mandatory: Int def optional: Int = mandatory + 1 @@ -48,24 +47,22 @@ trait Feature { } override def toString = rfcName - } /** Feature scope as defined in Bolt 9. */ -sealed trait FeatureScope /** Feature that should be advertised in init messages. */ -trait InitFeature extends FeatureScope +trait InitFeature extends Feature /** Feature that should be advertised in node announcements. */ -trait NodeFeature extends FeatureScope +trait NodeFeature extends Feature /** Feature that should be advertised in invoices. */ -trait InvoiceFeature extends FeatureScope +trait InvoiceFeature extends Feature // @formatter:on case class UnknownFeature(bitIndex: Int) -case class Features[T <: FeatureScope](activated: Map[Feature with T, FeatureSupport], unknown: Set[UnknownFeature] = Set.empty) { +case class Features[T <: Feature](activated: Map[T, FeatureSupport], unknown: Set[UnknownFeature] = Set.empty) { - def hasFeature(feature: Feature with T, support: Option[FeatureSupport] = None): Boolean = support match { + def hasFeature(feature: T, support: Option[FeatureSupport] = None): Boolean = support match { case Some(s) => activated.get(feature).contains(s) case None => activated.contains(feature) } @@ -84,13 +81,13 @@ case class Features[T <: FeatureScope](activated: Map[Feature with T, FeatureSup unknownFeaturesOk && knownFeaturesOk } - def initFeatures(): Features[InitFeature] = Features[InitFeature](activated.collect { case (f: InitFeature, s) => (f, s) }, unknown) + def initFeatures(): Features[InitFeature] = Features(activated.collect { case (f: InitFeature, s) => (f, s) }, unknown) - def nodeAnnouncementFeatures(): Features[NodeFeature] = Features[NodeFeature](activated.collect { case (f: NodeFeature, s) => (f, s) }, unknown) + def nodeAnnouncementFeatures(): Features[NodeFeature] = Features(activated.collect { case (f: NodeFeature, s) => (f, s) }, unknown) - def invoiceFeatures(): Features[InvoiceFeature] = Features[InvoiceFeature](activated.collect { case (f: InvoiceFeature, s) => (f, s) }, unknown) + def invoiceFeatures(): Features[InvoiceFeature] = Features(activated.collect { case (f: InvoiceFeature, s) => (f, s) }, unknown) - def unscoped(): Features[FeatureScope] = Features[FeatureScope](activated.collect { case (f, s) => (f: Feature with FeatureScope, s) }, unknown) + def unscoped(): Features[Feature] = Features[Feature](activated.collect { case (f, s) => (f: Feature, s) }, unknown) def toByteVector: ByteVector = { val activatedFeatureBytes = toByteVectorFromIndex(activated.map { case (feature, support) => feature.supportBit(support) }.toSet) @@ -116,28 +113,28 @@ case class Features[T <: FeatureScope](activated: Map[Feature with T, FeatureSup object Features { - def empty[T <: FeatureScope]: Features[T] = Features[T](Map.empty[Feature with T, FeatureSupport]) + def empty[T <: Feature]: Features[T] = Features[T](Map.empty[T, FeatureSupport]) - def apply[T <: FeatureScope](features: (Feature with T, FeatureSupport)*): Features[T] = Features[T](Map.from(features)) + def apply[T <: Feature](features: (T, FeatureSupport)*): Features[T] = Features[T](Map.from(features)) - def apply(bytes: ByteVector): Features[FeatureScope] = apply(bytes.bits) + def apply(bytes: ByteVector): Features[Feature] = apply(bytes.bits) - def apply(bits: BitVector): Features[FeatureScope] = { + def apply(bits: BitVector): Features[Feature] = { val all = bits.toIndexedSeq.reverse.zipWithIndex.collect { case (true, idx) if knownFeatures.exists(_.optional == idx) => Right((knownFeatures.find(_.optional == idx).get, Optional)) case (true, idx) if knownFeatures.exists(_.mandatory == idx) => Right((knownFeatures.find(_.mandatory == idx).get, Mandatory)) case (true, idx) => Left(UnknownFeature(idx)) } - Features[FeatureScope]( + Features[Feature]( activated = all.collect { case Right((feature, support)) => feature -> support }.toMap, unknown = all.collect { case Left(inf) => inf }.toSet ) } - def fromConfiguration[T <: FeatureScope](config: Config, validFeatures: Set[Feature with T]): Features[T] = Features[T]( + def fromConfiguration[T <: Feature](config: Config, validFeatures: Set[T]): Features[T] = Features[T]( config.root().entrySet().asScala.flatMap { entry => val featureName = entry.getKey - val feature: Feature with T = validFeatures.find(_.rfcName == featureName).getOrElse(throw new IllegalArgumentException(s"Invalid feature name ($featureName)")) + val feature: T = validFeatures.find(_.rfcName == featureName).getOrElse(throw new IllegalArgumentException(s"Invalid feature name ($featureName)")) config.getString(featureName) match { case support if support == Mandatory.toString => Some(feature -> Mandatory) case support if support == Optional.toString => Some(feature -> Optional) @@ -146,7 +143,7 @@ object Features { } }.toMap) - def fromConfiguration(config: Config): Features[FeatureScope] = fromConfiguration[FeatureScope](config, knownFeatures) + def fromConfiguration(config: Config): Features[Feature] = fromConfiguration[Feature](config, knownFeatures) case object DataLossProtect extends Feature with InitFeature with NodeFeature { val rfcName = "option_data_loss_protect" @@ -242,7 +239,7 @@ object Features { val mandatory = 54 } - val knownFeatures: Set[Feature with FeatureScope] = Set( + val knownFeatures: Set[Feature] = Set( DataLossProtect, InitialRoutingSync, UpfrontShutdownScript, @@ -278,16 +275,16 @@ object Features { case class FeatureException(message: String) extends IllegalArgumentException(message) - def validateFeatureGraph[T <: FeatureScope](features: Features[T]): Option[FeatureException] = featuresDependency.collectFirst { + def validateFeatureGraph[T <: Feature](features: Features[T]): Option[FeatureException] = featuresDependency.collectFirst { case (feature, dependencies) if features.unscoped().hasFeature(feature) && dependencies.exists(d => !features.unscoped().hasFeature(d)) => FeatureException(s"$feature is set but is missing a dependency (${dependencies.filter(d => !features.unscoped().hasFeature(d)).mkString(" and ")})") } /** Returns true if both feature sets are compatible. */ - def areCompatible[T <: FeatureScope](ours: Features[T], theirs: Features[T]): Boolean = ours.areSupported(theirs) && theirs.areSupported(ours) + def areCompatible[T <: Feature](ours: Features[T], theirs: Features[T]): Boolean = ours.areSupported(theirs) && theirs.areSupported(ours) /** returns true if both have at least optional support */ - def canUseFeature[T <: FeatureScope](localFeatures: Features[T], remoteFeatures: Features[T], feature: Feature with T): Boolean = { + def canUseFeature[T <: Feature](localFeatures: Features[T], remoteFeatures: Features[T], feature: T): Boolean = { localFeatures.hasFeature(feature) && remoteFeatures.hasFeature(feature) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala index 23480c75f4..e05dd037fc 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala @@ -58,7 +58,7 @@ case class NodeParams(nodeKeyManager: NodeKeyManager, color: Color, publicAddresses: List[NodeAddress], torAddress_opt: Option[NodeAddress], - features: Features[FeatureScope], + features: Features[Feature], private val overrideInitFeatures: Map[PublicKey, Features[InitFeature]], syncWhitelist: Set[PublicKey], pluginParams: Seq[PluginParams], @@ -271,7 +271,7 @@ object NodeParams extends Logging { val nodeAlias = config.getString("node-alias") require(nodeAlias.getBytes("UTF-8").length <= 32, "invalid alias, too long (max allowed 32 bytes)") - def validateFeatures(features: Features[FeatureScope]): Unit = { + def validateFeatures(features: Features[Feature]): Unit = { val featuresErr = Features.validateFeatureGraph(features) require(featuresErr.isEmpty, featuresErr.map(_.message)) require(features.hasFeature(Features.VariableLengthOnion, Some(FeatureSupport.Mandatory)), s"${Features.VariableLengthOnion.rfcName} must be enabled and mandatory") @@ -291,11 +291,11 @@ object NodeParams extends Logging { require(Features.knownFeatures.map(_.mandatory).intersect(pluginFeatureSet).isEmpty, "Plugin feature bit overlaps with known feature bit") require(pluginFeatureSet.size == pluginMessageParams.size, "Duplicate plugin feature bits found") - val coreAndPluginFeatures: Features[FeatureScope] = features.copy(unknown = features.unknown ++ pluginMessageParams.map(_.pluginFeature)) + val coreAndPluginFeatures: Features[Feature] = features.copy(unknown = features.unknown ++ pluginMessageParams.map(_.pluginFeature)) val overrideInitFeatures: Map[PublicKey, Features[InitFeature]] = config.getConfigList("override-init-features").asScala.map { e => val p = PublicKey(ByteVector.fromValidHex(e.getString("nodeid"))) - val f = Features.fromConfiguration[InitFeature](e.getConfig("features"), Features.knownFeatures.collect { case f: Feature with InitFeature => f }) + val f = Features.fromConfiguration[InitFeature](e.getConfig("features"), Features.knownFeatures.collect { case f: InitFeature => f }) validateFeatures(f.unscoped()) p -> (f.copy(unknown = f.unknown ++ pluginMessageParams.map(_.pluginFeature)): Features[InitFeature]) }.toMap diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelFeatures.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelFeatures.scala index 3369ff4391..9881aa5869 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelFeatures.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelFeatures.scala @@ -17,7 +17,7 @@ package fr.acinq.eclair.channel import fr.acinq.eclair.transactions.Transactions.{CommitmentFormat, DefaultCommitmentFormat, UnsafeLegacyAnchorOutputsCommitmentFormat, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat} -import fr.acinq.eclair.{Feature, FeatureScope, FeatureSupport, Features, InitFeature} +import fr.acinq.eclair.{FeatureSupport, Features, InitFeature, Feature} /** * Created by t-bast on 24/06/2021. @@ -28,7 +28,7 @@ import fr.acinq.eclair.{Feature, FeatureScope, FeatureSupport, Features, InitFea * Even if one of these features is later disabled at the connection level, it will still apply to the channel until the * channel is upgraded or closed. */ -case class ChannelFeatures(features: Set[Feature with FeatureScope]) { +case class ChannelFeatures(features: Set[Feature]) { val channelType: SupportedChannelType = { if (hasFeature(Features.AnchorOutputsZeroFeeHtlcTx)) { @@ -45,7 +45,7 @@ case class ChannelFeatures(features: Set[Feature with FeatureScope]) { val paysDirectlyToWallet: Boolean = channelType.paysDirectlyToWallet val commitmentFormat: CommitmentFormat = channelType.commitmentFormat - def hasFeature(feature: Feature with FeatureScope): Boolean = features.contains(feature) + def hasFeature(feature: Feature): Boolean = features.contains(feature) override def toString: String = features.mkString(",") @@ -53,13 +53,13 @@ case class ChannelFeatures(features: Set[Feature with FeatureScope]) { object ChannelFeatures { - def apply(features: (Feature with FeatureScope)*): ChannelFeatures = ChannelFeatures(Set.from(features)) + def apply(features: Feature*): ChannelFeatures = ChannelFeatures(Set.from(features)) /** Enrich the channel type with other permanent features that will be applied to the channel. */ def apply(channelType: ChannelType, localFeatures: Features[InitFeature], remoteFeatures: Features[InitFeature]): ChannelFeatures = { // NB: we don't include features that can be safely activated/deactivated without impacting the channel's operation, // such as option_dataloss_protect or option_shutdown_anysegwit. - val availableFeatures: Seq[Feature with FeatureScope] = Seq(Features.Wumbo, Features.UpfrontShutdownScript).filter(f => Features.canUseFeature(localFeatures, remoteFeatures, f)) + val availableFeatures = Seq(Features.Wumbo, Features.UpfrontShutdownScript).filter(f => Features.canUseFeature(localFeatures, remoteFeatures, f)) val allFeatures = channelType.features.toSeq ++ availableFeatures ChannelFeatures(allFeatures: _*) } @@ -69,7 +69,7 @@ object ChannelFeatures { /** A channel type is a specific set of even feature bits that represent persistent channel features as defined in Bolt 2. */ sealed trait ChannelType { /** Features representing that channel type. */ - def features: Set[Feature with InitFeature] + def features: Set[InitFeature] } sealed trait SupportedChannelType extends ChannelType { @@ -84,31 +84,31 @@ object ChannelTypes { // @formatter:off case object Standard extends SupportedChannelType { - override def features: Set[Feature with InitFeature] = Set.empty + override def features: Set[InitFeature] = Set.empty override def paysDirectlyToWallet: Boolean = false override def commitmentFormat: CommitmentFormat = DefaultCommitmentFormat override def toString: String = "standard" } case object StaticRemoteKey extends SupportedChannelType { - override def features: Set[Feature with InitFeature] = Set(Features.StaticRemoteKey) + override def features: Set[InitFeature] = Set(Features.StaticRemoteKey) override def paysDirectlyToWallet: Boolean = true override def commitmentFormat: CommitmentFormat = DefaultCommitmentFormat override def toString: String = "static_remotekey" } case object AnchorOutputs extends SupportedChannelType { - override def features: Set[Feature with InitFeature] = Set(Features.StaticRemoteKey, Features.AnchorOutputs) + override def features: Set[InitFeature] = Set(Features.StaticRemoteKey, Features.AnchorOutputs) override def paysDirectlyToWallet: Boolean = false override def commitmentFormat: CommitmentFormat = UnsafeLegacyAnchorOutputsCommitmentFormat override def toString: String = "anchor_outputs" } case object AnchorOutputsZeroFeeHtlcTx extends SupportedChannelType { - override def features: Set[Feature with InitFeature] = Set(Features.StaticRemoteKey, Features.AnchorOutputsZeroFeeHtlcTx) + override def features: Set[InitFeature] = Set(Features.StaticRemoteKey, Features.AnchorOutputsZeroFeeHtlcTx) override def paysDirectlyToWallet: Boolean = false override def commitmentFormat: CommitmentFormat = ZeroFeeHtlcTxAnchorOutputsCommitmentFormat override def toString: String = "anchor_outputs_zero_fee_htlc_tx" } case class UnsupportedChannelType(featureBits: Features[InitFeature]) extends ChannelType { - override def features: Set[Feature with InitFeature] = featureBits.activated.keySet + override def features: Set[InitFeature] = featureBits.activated.keySet override def toString: String = s"0x${featureBits.toByteVector.toHex}" } // @formatter:on diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala index 9a52085375..1af44e59db 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala @@ -225,7 +225,7 @@ object Helpers { } def makeAnnouncementSignatures(nodeParams: NodeParams, commitments: Commitments, shortChannelId: ShortChannelId): AnnouncementSignatures = { - val features = Features.empty[FeatureScope] // empty features for now + val features = Features.empty[Feature] // empty features for now val fundingPubKey = nodeParams.channelKeyManager.fundingPublicKey(commitments.localParams.fundingKeyPath) val witness = Announcements.generateChannelAnnouncementWitness( nodeParams.chainHash, diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/PeerConnection.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/PeerConnection.scala index 4b32706a65..f60c7ad856 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/io/PeerConnection.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/PeerConnection.scala @@ -28,7 +28,7 @@ import fr.acinq.eclair.remote.EclairInternalsSerializer.RemoteTypes import fr.acinq.eclair.router.Router._ import fr.acinq.eclair.wire.protocol import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{FSMDiagnosticActorLogging, FeatureScope, Features, InitFeature, Logs, TimestampMilli, TimestampSecond} +import fr.acinq.eclair.{FSMDiagnosticActorLogging, Feature, Features, InitFeature, Logs, TimestampMilli, TimestampSecond} import scodec.Attempt import scodec.bits.ByteVector diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/json/JsonSerializers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/json/JsonSerializers.scala index a563c05d7b..6ea58b28e9 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/json/JsonSerializers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/json/JsonSerializers.scala @@ -35,7 +35,7 @@ import fr.acinq.eclair.transactions.DirectedHtlc import fr.acinq.eclair.transactions.Transactions._ import fr.acinq.eclair.wire.protocol.MessageOnionCodecs.blindedRouteCodec import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, Feature, FeatureSupport, MilliSatoshi, ShortChannelId, TimestampMilli, TimestampSecond, UInt64, UnknownFeature} +import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, FeatureSupport, Feature, MilliSatoshi, ShortChannelId, TimestampMilli, TimestampSecond, UInt64, UnknownFeature} import org.json4s import org.json4s.JsonAST._ import org.json4s.jackson.Serialization diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Bolt11Invoice.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Bolt11Invoice.scala index 30fa05b615..0274948faa 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Bolt11Invoice.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Bolt11Invoice.scala @@ -18,7 +18,7 @@ package fr.acinq.eclair.payment import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} import fr.acinq.bitcoin.{Base58, Base58Check, Bech32, Block, ByteVector32, ByteVector64, Crypto} -import fr.acinq.eclair.{CltvExpiryDelta, FeatureScope, FeatureSupport, Features, InvoiceFeature, MilliSatoshi, MilliSatoshiLong, ShortChannelId, TimestampSecond, randomBytes32} +import fr.acinq.eclair.{CltvExpiryDelta, Feature, FeatureSupport, Features, InvoiceFeature, MilliSatoshi, MilliSatoshiLong, ShortChannelId, TimestampSecond, randomBytes32} import scodec.bits.{BitVector, ByteOrdering, ByteVector} import scodec.codecs.{list, ubyte} import scodec.{Codec, Err} @@ -299,7 +299,7 @@ object Bolt11Invoice { * This returns a bitvector with the minimum size necessary to encode the features, left padded to have a length (in * bits) that is a multiple of 5. */ - def features2bits[T <: FeatureScope](features: Features[T]): BitVector = leftPaddedBits(features.toByteVector.bits) + def features2bits[T <: Feature](features: Features[T]): BitVector = leftPaddedBits(features.toByteVector.bits) private def leftPaddedBits(bits: BitVector): BitVector = { var highest = -1 @@ -365,7 +365,7 @@ object Bolt11Invoice { /** * Features supported or required for receiving this payment. */ - case class InvoiceFeatures(features: Features[FeatureScope]) extends TaggedField + case class InvoiceFeatures(features: Features[Feature]) extends TaggedField object Codecs { @@ -408,7 +408,7 @@ object Bolt11Invoice { .typecase(2, dataCodec(bits).as[UnknownTag2]) .typecase(3, dataCodec(listOfN(extraHopsLengthCodec, extraHopCodec)).as[RoutingInfo]) .typecase(4, dataCodec(bits).as[UnknownTag4]) - .typecase(5, dataCodec(bits).xmap[Features[FeatureScope]](Features(_), features2bits).as[InvoiceFeatures]) + .typecase(5, dataCodec(bits).xmap[Features[Feature]](Features(_), features2bits).as[InvoiceFeatures]) .typecase(6, dataCodec(bits).as[Expiry]) .typecase(7, dataCodec(bits).as[UnknownTag7]) .typecase(8, dataCodec(bits).as[UnknownTag8]) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/remote/EclairInternalsSerializer.scala b/eclair-core/src/main/scala/fr/acinq/eclair/remote/EclairInternalsSerializer.scala index a97e176456..854ecd1ada 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/remote/EclairInternalsSerializer.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/remote/EclairInternalsSerializer.scala @@ -31,7 +31,7 @@ import fr.acinq.eclair.wire.protocol.CommonCodecs._ import fr.acinq.eclair.wire.protocol.LightningMessageCodecs._ import fr.acinq.eclair.wire.protocol.QueryChannelRangeTlv.queryFlagsCodec import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{CltvExpiryDelta, FeatureScope, Features, InitFeature} +import fr.acinq.eclair.{CltvExpiryDelta, Feature, Features, InitFeature} import scodec._ import scodec.codecs._ @@ -98,7 +98,7 @@ object EclairInternalsSerializer { ("channelQueryChunkSize" | int32) :: ("pathFindingExperimentConf" | pathFindingExperimentConfCodec)).as[RouterConf] - val overrideFeaturesListCodec: Codec[List[(PublicKey, Features[FeatureScope])]] = listOfN(uint16, publicKey ~ variableSizeBytes(uint16, featuresCodec)) + val overrideFeaturesListCodec: Codec[List[(PublicKey, Features[Feature])]] = listOfN(uint16, publicKey ~ variableSizeBytes(uint16, featuresCodec)) val peerConnectionConfCodec: Codec[PeerConnection.Conf] = ( ("authTimeout" | finiteDurationCodec) :: diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/Announcements.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/Announcements.scala index e415530120..74116ba3ca 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/Announcements.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/Announcements.scala @@ -19,7 +19,7 @@ package fr.acinq.eclair.router import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey, sha256, verifySignature} import fr.acinq.bitcoin.{ByteVector32, ByteVector64, Crypto, LexicographicalOrdering} import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{CltvExpiryDelta, FeatureScope, Features, MilliSatoshi, NodeFeature, ShortChannelId, TimestampSecond, TimestampSecondLong, serializationResult} +import fr.acinq.eclair.{CltvExpiryDelta, Feature, Features, MilliSatoshi, NodeFeature, ShortChannelId, TimestampSecond, TimestampSecondLong, serializationResult} import scodec.bits.ByteVector import shapeless.HNil @@ -28,16 +28,16 @@ import shapeless.HNil */ object Announcements { - def channelAnnouncementWitnessEncode(chainHash: ByteVector32, shortChannelId: ShortChannelId, nodeId1: PublicKey, nodeId2: PublicKey, bitcoinKey1: PublicKey, bitcoinKey2: PublicKey, features: Features[FeatureScope], tlvStream: TlvStream[ChannelAnnouncementTlv]): ByteVector = + def channelAnnouncementWitnessEncode(chainHash: ByteVector32, shortChannelId: ShortChannelId, nodeId1: PublicKey, nodeId2: PublicKey, bitcoinKey1: PublicKey, bitcoinKey2: PublicKey, features: Features[Feature], tlvStream: TlvStream[ChannelAnnouncementTlv]): ByteVector = sha256(sha256(serializationResult(LightningMessageCodecs.channelAnnouncementWitnessCodec.encode(features :: chainHash :: shortChannelId :: nodeId1 :: nodeId2 :: bitcoinKey1 :: bitcoinKey2 :: tlvStream :: HNil)))) - def nodeAnnouncementWitnessEncode(timestamp: TimestampSecond, nodeId: PublicKey, rgbColor: Color, alias: String, features: Features[FeatureScope], addresses: List[NodeAddress], tlvStream: TlvStream[NodeAnnouncementTlv]): ByteVector = + def nodeAnnouncementWitnessEncode(timestamp: TimestampSecond, nodeId: PublicKey, rgbColor: Color, alias: String, features: Features[Feature], addresses: List[NodeAddress], tlvStream: TlvStream[NodeAnnouncementTlv]): ByteVector = sha256(sha256(serializationResult(LightningMessageCodecs.nodeAnnouncementWitnessCodec.encode(features :: timestamp :: nodeId :: rgbColor :: alias :: addresses :: tlvStream :: HNil)))) def channelUpdateWitnessEncode(chainHash: ByteVector32, shortChannelId: ShortChannelId, timestamp: TimestampSecond, channelFlags: ChannelUpdate.ChannelFlags, cltvExpiryDelta: CltvExpiryDelta, htlcMinimumMsat: MilliSatoshi, feeBaseMsat: MilliSatoshi, feeProportionalMillionths: Long, htlcMaximumMsat: Option[MilliSatoshi], tlvStream: TlvStream[ChannelUpdateTlv]): ByteVector = sha256(sha256(serializationResult(LightningMessageCodecs.channelUpdateWitnessCodec.encode(chainHash :: shortChannelId :: timestamp :: channelFlags :: cltvExpiryDelta :: htlcMinimumMsat :: feeBaseMsat :: feeProportionalMillionths :: htlcMaximumMsat :: tlvStream :: HNil)))) - def generateChannelAnnouncementWitness(chainHash: ByteVector32, shortChannelId: ShortChannelId, localNodeId: PublicKey, remoteNodeId: PublicKey, localFundingKey: PublicKey, remoteFundingKey: PublicKey, features: Features[FeatureScope]): ByteVector = + def generateChannelAnnouncementWitness(chainHash: ByteVector32, shortChannelId: ShortChannelId, localNodeId: PublicKey, remoteNodeId: PublicKey, localFundingKey: PublicKey, remoteFundingKey: PublicKey, features: Features[Feature]): ByteVector = if (isNode1(localNodeId, remoteNodeId)) { channelAnnouncementWitnessEncode(chainHash, shortChannelId, localNodeId, remoteNodeId, localFundingKey, remoteFundingKey, features, TlvStream.empty) } else { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelTypes0.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelTypes0.scala index 6b8765ab19..24ab351616 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelTypes0.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelTypes0.scala @@ -23,7 +23,7 @@ import fr.acinq.eclair.channel._ import fr.acinq.eclair.crypto.ShaChain import fr.acinq.eclair.transactions.CommitmentSpec import fr.acinq.eclair.transactions.Transactions._ -import fr.acinq.eclair.{BlockHeight, Feature, FeatureScope, Features, channel} +import fr.acinq.eclair.{BlockHeight, Features, Feature, channel} import scodec.bits.BitVector private[channel] object ChannelTypes0 { @@ -196,8 +196,8 @@ private[channel] object ChannelTypes0 { ChannelConfig() } val isWumboChannel = commitInput.txOut.amount > Satoshi(16777215) - val baseChannelFeatures: Set[Feature with FeatureScope] = if (isWumboChannel) Set(Features.Wumbo) else Set.empty - val commitmentFeatures: Set[Feature with FeatureScope] = if (channelVersion.hasAnchorOutputs) { + val baseChannelFeatures: Set[Feature] = if (isWumboChannel) Set(Features.Wumbo) else Set.empty + val commitmentFeatures: Set[Feature] = if (channelVersion.hasAnchorOutputs) { Set(Features.StaticRemoteKey, Features.AnchorOutputs) } else if (channelVersion.hasStaticRemotekey) { Set(Features.StaticRemoteKey) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecs.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecs.scala index 6c176f66eb..774adac8d0 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecs.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecs.scala @@ -18,7 +18,7 @@ package fr.acinq.eclair.wire.protocol import fr.acinq.eclair.wire.Monitoring.{Metrics, Tags} import fr.acinq.eclair.wire.protocol.CommonCodecs._ -import fr.acinq.eclair.{FeatureScope, Features, InitFeature, KamonExt, NodeFeature} +import fr.acinq.eclair.{Feature, Features, InitFeature, KamonExt, NodeFeature} import scodec.bits.{BitVector, ByteVector} import scodec.codecs._ import scodec.{Attempt, Codec} @@ -29,7 +29,7 @@ import shapeless._ */ object LightningMessageCodecs { - val featuresCodec: Codec[Features[FeatureScope]] = varsizebinarydata.xmap[Features[FeatureScope]]( + val featuresCodec: Codec[Features[Feature]] = varsizebinarydata.xmap[Features[Feature]]( { bytes => Features(bytes) }, { features => features.toByteVector } ) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageTypes.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageTypes.scala index 42416a896e..6a60124138 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageTypes.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageTypes.scala @@ -21,7 +21,7 @@ import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} import fr.acinq.bitcoin.{ByteVector32, ByteVector64, Satoshi} import fr.acinq.eclair.blockchain.fee.FeeratePerKw import fr.acinq.eclair.channel.{ChannelFlags, ChannelType} -import fr.acinq.eclair.{BlockHeight, CltvExpiry, CltvExpiryDelta, FeatureScope, Features, InitFeature, MilliSatoshi, NodeFeature, ShortChannelId, TimestampSecond, UInt64} +import fr.acinq.eclair.{BlockHeight, CltvExpiry, CltvExpiryDelta, Feature, Features, InitFeature, MilliSatoshi, NodeFeature, ShortChannelId, TimestampSecond, UInt64} import scodec.bits.ByteVector import java.net.{Inet4Address, Inet6Address, InetAddress, InetSocketAddress} @@ -200,7 +200,7 @@ case class ChannelAnnouncement(nodeSignature1: ByteVector64, nodeSignature2: ByteVector64, bitcoinSignature1: ByteVector64, bitcoinSignature2: ByteVector64, - features: Features[FeatureScope], + features: Features[Feature], chainHash: ByteVector32, shortChannelId: ShortChannelId, nodeId1: PublicKey, @@ -263,7 +263,7 @@ case class Tor3(tor3: String, port: Int) extends OnionAddress { override def soc // @formatter:on case class NodeAnnouncement(signature: ByteVector64, - features: Features[FeatureScope], + features: Features[Feature], timestamp: TimestampSecond, nodeId: PublicKey, rgbColor: Color, diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/FeaturesSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/FeaturesSpec.scala index 345b043999..9915faf482 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/FeaturesSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/FeaturesSpec.scala @@ -109,7 +109,7 @@ class FeaturesSpec extends AnyFunSuite { } test("features compatibility") { - case class TestCase(ours: Features[FeatureScope], theirs: Features[FeatureScope], oursSupportTheirs: Boolean, theirsSupportOurs: Boolean, compatible: Boolean) + case class TestCase(ours: Features[Feature], theirs: Features[Feature], oursSupportTheirs: Boolean, theirsSupportOurs: Boolean, compatible: Boolean) val testCases = Seq( // Empty features TestCase( @@ -168,7 +168,7 @@ class FeaturesSpec extends AnyFunSuite { // They have unknown optional features TestCase( Features(VariableLengthOnion -> Optional), - Features[FeatureScope](Map[Feature with FeatureScope, FeatureSupport](VariableLengthOnion -> Optional), Set(UnknownFeature(141))), + Features(Map(VariableLengthOnion -> Optional), unknown = Set(UnknownFeature(141))), oursSupportTheirs = true, theirsSupportOurs = true, compatible = true @@ -176,7 +176,7 @@ class FeaturesSpec extends AnyFunSuite { // They have unknown mandatory features TestCase( Features(VariableLengthOnion -> Optional), - Features[FeatureScope](Map[Feature with FeatureScope, FeatureSupport](VariableLengthOnion -> Optional), Set(UnknownFeature(142))), + Features(Map(VariableLengthOnion -> Optional), unknown = Set(UnknownFeature(142))), oursSupportTheirs = false, theirsSupportOurs = true, compatible = false @@ -198,10 +198,10 @@ class FeaturesSpec extends AnyFunSuite { compatible = false ), // nonreg testing of future features (needs to be updated with every new supported mandatory bit) - TestCase(Features.empty, Features[FeatureScope](Map.empty[Feature with FeatureScope, FeatureSupport], Set(UnknownFeature(24))), oursSupportTheirs = false, theirsSupportOurs = true, compatible = false), - TestCase(Features.empty, Features[FeatureScope](Map.empty[Feature with FeatureScope, FeatureSupport], Set(UnknownFeature(25))), oursSupportTheirs = true, theirsSupportOurs = true, compatible = true), - TestCase(Features.empty, Features[FeatureScope](Map.empty[Feature with FeatureScope, FeatureSupport], Set(UnknownFeature(28))), oursSupportTheirs = false, theirsSupportOurs = true, compatible = false), - TestCase(Features.empty, Features[FeatureScope](Map.empty[Feature with FeatureScope, FeatureSupport], Set(UnknownFeature(29))), oursSupportTheirs = true, theirsSupportOurs = true, compatible = true), + TestCase(Features.empty, Features(Map.empty, unknown = Set(UnknownFeature(24))), oursSupportTheirs = false, theirsSupportOurs = true, compatible = false), + TestCase(Features.empty, Features(Map.empty, unknown = Set(UnknownFeature(25))), oursSupportTheirs = true, theirsSupportOurs = true, compatible = true), + TestCase(Features.empty, Features(Map.empty, unknown = Set(UnknownFeature(28))), oursSupportTheirs = false, theirsSupportOurs = true, compatible = false), + TestCase(Features.empty, Features(Map.empty, unknown = Set(UnknownFeature(29))), oursSupportTheirs = true, theirsSupportOurs = true, compatible = true), ) for (testCase <- testCases) { @@ -212,20 +212,20 @@ class FeaturesSpec extends AnyFunSuite { } test("filter features based on their usage") { - val features = Features[FeatureScope]( - Map[Feature with FeatureScope, FeatureSupport](DataLossProtect -> Optional, InitialRoutingSync -> Optional, VariableLengthOnion -> Mandatory, PaymentMetadata -> Optional), + val features = Features( + Map(DataLossProtect -> Optional, InitialRoutingSync -> Optional, VariableLengthOnion -> Mandatory, PaymentMetadata -> Optional), Set(UnknownFeature(753), UnknownFeature(852), UnknownFeature(65303)) ) - assert(features.initFeatures() === Features[InitFeature]( - Map[Feature with InitFeature, FeatureSupport](DataLossProtect -> Optional, InitialRoutingSync -> Optional, VariableLengthOnion -> Mandatory), + assert(features.initFeatures() === Features( + Map(DataLossProtect -> Optional, InitialRoutingSync -> Optional, VariableLengthOnion -> Mandatory), Set(UnknownFeature(753), UnknownFeature(852), UnknownFeature(65303)) )) - assert(features.nodeAnnouncementFeatures() === Features[NodeFeature]( - Map[Feature with NodeFeature, FeatureSupport](DataLossProtect -> Optional, VariableLengthOnion -> Mandatory), + assert(features.nodeAnnouncementFeatures() === Features( + Map(DataLossProtect -> Optional, VariableLengthOnion -> Mandatory), Set(UnknownFeature(753), UnknownFeature(852), UnknownFeature(65303)) )) - assert(features.invoiceFeatures() === Features[InvoiceFeature]( - Map[Feature with InvoiceFeature, FeatureSupport](VariableLengthOnion -> Mandatory, PaymentMetadata -> Optional), + assert(features.invoiceFeatures() === Features( + Map(VariableLengthOnion -> Mandatory, PaymentMetadata -> Optional), Set(UnknownFeature(753), UnknownFeature(852), UnknownFeature(65303)) )) } @@ -235,8 +235,8 @@ class FeaturesSpec extends AnyFunSuite { hex"" -> Features.empty, hex"0100" -> Features(VariableLengthOnion -> Mandatory), hex"028a8a" -> Features(DataLossProtect -> Optional, InitialRoutingSync -> Optional, ChannelRangeQueries -> Optional, VariableLengthOnion -> Optional, ChannelRangeQueriesExtended -> Optional, PaymentSecret -> Optional, BasicMultiPartPayment -> Optional), - hex"09004200" -> Features[FeatureScope](Map[Feature with FeatureScope, FeatureSupport](VariableLengthOnion -> Optional, PaymentSecret -> Mandatory, ShutdownAnySegwit -> Optional), Set(UnknownFeature(24))), - hex"52000000" -> Features[FeatureScope](Map.empty[Feature with FeatureScope, FeatureSupport], Set(UnknownFeature(25), UnknownFeature(28), UnknownFeature(30))) + hex"09004200" -> Features(Map(VariableLengthOnion -> Optional, PaymentSecret -> Mandatory, ShutdownAnySegwit -> Optional), Set(UnknownFeature(24))), + hex"52000000" -> Features(Map.empty[Feature, FeatureSupport], Set(UnknownFeature(25), UnknownFeature(28), UnknownFeature(30))) ) for ((bin, features) <- testCases) { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala b/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala index d184840433..ea9aba1c32 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala @@ -84,8 +84,8 @@ object TestConstants { color = Color(1, 2, 3), publicAddresses = NodeAddress.fromParts("localhost", 9731).get :: Nil, torAddress_opt = None, - features = Features[FeatureScope]( - Map[Feature with FeatureScope, FeatureSupport]( + features = Features( + Map( DataLossProtect -> Optional, ChannelRangeQueries -> Optional, ChannelRangeQueriesExtended -> Optional, @@ -94,7 +94,7 @@ object TestConstants { BasicMultiPartPayment -> Optional, PaymentMetadata -> Optional, ), - Set(UnknownFeature(TestFeature.optional)) + unknown = Set(UnknownFeature(TestFeature.optional)) ), pluginParams = List(pluginParams), overrideInitFeatures = Map.empty, diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/ChannelFeaturesSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/ChannelFeaturesSpec.scala index b415099ebf..0981d3bac5 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/ChannelFeaturesSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/ChannelFeaturesSpec.scala @@ -20,7 +20,7 @@ import fr.acinq.eclair.FeatureSupport._ import fr.acinq.eclair.Features._ import fr.acinq.eclair.channel.states.ChannelStateTestsHelperMethods import fr.acinq.eclair.transactions.Transactions -import fr.acinq.eclair.{Features, InitFeature, TestKitBaseClass} +import fr.acinq.eclair.{Features, InitFeature, NodeFeature, TestKitBaseClass} import org.scalatest.funsuite.AnyFunSuiteLike class ChannelFeaturesSpec extends TestKitBaseClass with AnyFunSuiteLike with ChannelStateTestsHelperMethods { @@ -80,29 +80,31 @@ class ChannelFeaturesSpec extends TestKitBaseClass with AnyFunSuiteLike with Cha } test("create channel type from features") { + case class TestCase(features: Features[InitFeature], expectedChannelType: ChannelType) + val validChannelTypes = Seq( - Features.empty[InitFeature] -> ChannelTypes.Standard, - Features[InitFeature](StaticRemoteKey -> Mandatory) -> ChannelTypes.StaticRemoteKey, - Features[InitFeature](StaticRemoteKey -> Mandatory, AnchorOutputs -> Mandatory) -> ChannelTypes.AnchorOutputs, - Features[InitFeature](StaticRemoteKey -> Mandatory, AnchorOutputsZeroFeeHtlcTx -> Mandatory) -> ChannelTypes.AnchorOutputsZeroFeeHtlcTx, + TestCase(Features.empty[InitFeature], ChannelTypes.Standard), + TestCase(Features(StaticRemoteKey -> Mandatory), ChannelTypes.StaticRemoteKey), + TestCase(Features(StaticRemoteKey -> Mandatory, AnchorOutputs -> Mandatory), ChannelTypes.AnchorOutputs), + TestCase(Features(StaticRemoteKey -> Mandatory, AnchorOutputsZeroFeeHtlcTx -> Mandatory), ChannelTypes.AnchorOutputsZeroFeeHtlcTx), ) - for ((features, expected) <- validChannelTypes) { - assert(ChannelTypes.fromFeatures(features) === expected) + for (testCase <- validChannelTypes) { + assert(ChannelTypes.fromFeatures(testCase.features) === testCase.expectedChannelType) } - val invalidChannelTypes = Seq( - Features[InitFeature](Wumbo -> Optional), - Features[InitFeature](StaticRemoteKey -> Optional), - Features[InitFeature](StaticRemoteKey -> Mandatory, Wumbo -> Optional), - Features[InitFeature](StaticRemoteKey -> Optional, AnchorOutputs -> Optional), - Features[InitFeature](StaticRemoteKey -> Mandatory, AnchorOutputs -> Optional), - Features[InitFeature](StaticRemoteKey -> Optional, AnchorOutputs -> Mandatory), - Features[InitFeature](StaticRemoteKey -> Optional, AnchorOutputsZeroFeeHtlcTx -> Optional), - Features[InitFeature](StaticRemoteKey -> Optional, AnchorOutputsZeroFeeHtlcTx -> Mandatory), - Features[InitFeature](StaticRemoteKey -> Mandatory, AnchorOutputsZeroFeeHtlcTx -> Optional), - Features[InitFeature](StaticRemoteKey -> Mandatory, AnchorOutputs -> Mandatory, AnchorOutputsZeroFeeHtlcTx -> Mandatory), - Features[InitFeature](StaticRemoteKey -> Mandatory, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTx -> Mandatory), - Features[InitFeature](StaticRemoteKey -> Mandatory, AnchorOutputs -> Mandatory, Wumbo -> Optional), + val invalidChannelTypes: Seq[Features[InitFeature]] = Seq( + Features(Wumbo -> Optional), + Features(StaticRemoteKey -> Optional), + Features(StaticRemoteKey -> Mandatory, Wumbo -> Optional), + Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional), + Features(StaticRemoteKey -> Mandatory, AnchorOutputs -> Optional), + Features(StaticRemoteKey -> Optional, AnchorOutputs -> Mandatory), + Features(StaticRemoteKey -> Optional, AnchorOutputsZeroFeeHtlcTx -> Optional), + Features(StaticRemoteKey -> Optional, AnchorOutputsZeroFeeHtlcTx -> Mandatory), + Features(StaticRemoteKey -> Mandatory, AnchorOutputsZeroFeeHtlcTx -> Optional), + Features(StaticRemoteKey -> Mandatory, AnchorOutputs -> Mandatory, AnchorOutputsZeroFeeHtlcTx -> Mandatory), + Features(StaticRemoteKey -> Mandatory, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTx -> Mandatory), + Features(StaticRemoteKey -> Mandatory, AnchorOutputs -> Mandatory, Wumbo -> Optional), ) for (features <- invalidChannelTypes) { assert(ChannelTypes.fromFeatures(features) === ChannelTypes.UnsupportedChannelType(features)) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala index 7c74e264d1..58863d1f3f 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala @@ -353,7 +353,7 @@ class PeerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Paralle } // They want to use a channel type we don't support yet. { - val open = createOpenChannelMessage(TlvStream[OpenChannelTlv](ChannelTlv.ChannelTypeTlv(UnsupportedChannelType(Features[InitFeature](Map[Feature with InitFeature, FeatureSupport](StaticRemoteKey -> Mandatory), Set(UnknownFeature(22))))))) + val open = createOpenChannelMessage(TlvStream[OpenChannelTlv](ChannelTlv.ChannelTypeTlv(UnsupportedChannelType(Features(Map(StaticRemoteKey -> Mandatory), unknown = Set(UnknownFeature(22))))))) peerConnection.send(peer, open) peerConnection.expectMsg(Error(open.temporaryChannelId, "invalid channel_type=0x401000, expected channel_type=standard")) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/Bolt11InvoiceSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/Bolt11InvoiceSpec.scala index 27f359ad34..75375396f3 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/Bolt11InvoiceSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/Bolt11InvoiceSpec.scala @@ -21,7 +21,7 @@ import fr.acinq.bitcoin.{Block, BtcDouble, ByteVector32, Crypto, MilliBtcDouble, import fr.acinq.eclair.FeatureSupport.{Mandatory, Optional} import fr.acinq.eclair.Features.{PaymentMetadata, PaymentSecret, _} import fr.acinq.eclair.payment.Bolt11Invoice._ -import fr.acinq.eclair.{CltvExpiryDelta, Feature, FeatureScope, FeatureSupport, Features, InvoiceFeature, MilliSatoshi, MilliSatoshiLong, ShortChannelId, TestConstants, TimestampSecond, TimestampSecondLong, ToMilliSatoshiConversion, UnknownFeature, randomBytes32, randomKey} +import fr.acinq.eclair.{CltvExpiryDelta, FeatureSupport, Features, Feature, MilliSatoshi, MilliSatoshiLong, ShortChannelId, TestConstants, TimestampSecond, TimestampSecondLong, ToMilliSatoshiConversion, UnknownFeature, randomBytes32} import org.scalatest.funsuite.AnyFunSuite import scodec.DecodeResult import scodec.bits._ @@ -54,7 +54,7 @@ class Bolt11InvoiceSpec extends AnyFunSuite { timestamp: TimestampSecond = TimestampSecond.now(), paymentSecret: ByteVector32 = randomBytes32(), paymentMetadata: Option[ByteVector] = None, - features: Features[FeatureScope] = defaultFeatures.unscoped()): Bolt11Invoice = { + features: Features[Feature] = defaultFeatures.unscoped()): Bolt11Invoice = { require(features.hasFeature(Features.PaymentSecret, Some(FeatureSupport.Mandatory)), "invoices must require a payment secret") val prefix = prefixes(chainHash) val tags = { @@ -624,11 +624,11 @@ class Bolt11InvoiceSpec extends AnyFunSuite { test("no unknown feature in invoice"){ assert(TestConstants.Alice.nodeParams.features.invoiceFeatures().unknown.nonEmpty) val invoice = Bolt11Invoice(Block.LivenetGenesisBlock.hash, Some(123 msat), ByteVector32.One, priv, Left("Some invoice"), CltvExpiryDelta(18), features = TestConstants.Alice.nodeParams.features.invoiceFeatures()) - assert(invoice.features === Features[InvoiceFeature](Map[Feature with InvoiceFeature, FeatureSupport](PaymentSecret -> Mandatory, BasicMultiPartPayment -> Optional, PaymentMetadata -> Optional, VariableLengthOnion -> Mandatory))) + assert(invoice.features === Features(PaymentSecret -> Mandatory, BasicMultiPartPayment -> Optional, PaymentMetadata -> Optional, VariableLengthOnion -> Mandatory)) assert(Bolt11Invoice.fromString(invoice.toString) === invoice) } test("Invoices can't have high features"){ - assertThrows[Exception](createInvoiceUnsafe(Block.LivenetGenesisBlock.hash, Some(123 msat), ByteVector32.One, priv, Left("Some invoice"), CltvExpiryDelta(18), features = Features[FeatureScope](Map[Feature with FeatureScope, FeatureSupport](VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory), Set(UnknownFeature(424242))))) + assertThrows[Exception](createInvoiceUnsafe(Block.LivenetGenesisBlock.hash, Some(123 msat), ByteVector32.One, priv, Left("Some invoice"), CltvExpiryDelta(18), features = Features[Feature](Map[Feature, FeatureSupport](VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory), Set(UnknownFeature(424242))))) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartHandlerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartHandlerSpec.scala index 802ea5a8cc..ff99fa5b6f 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartHandlerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartHandlerSpec.scala @@ -32,7 +32,7 @@ import fr.acinq.eclair.payment.receive.MultiPartPaymentFSM.HtlcPart import fr.acinq.eclair.payment.receive.{MultiPartPaymentFSM, PaymentHandler} import fr.acinq.eclair.wire.protocol.PaymentOnion.FinalTlvPayload import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, FeatureScope, Features, MilliSatoshiLong, NodeParams, ShortChannelId, TestConstants, TestKitBaseClass, TimestampMilliLong, randomBytes32, randomKey} +import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, Feature, Features, MilliSatoshiLong, NodeParams, ShortChannelId, TestConstants, TestKitBaseClass, TimestampMilliLong, randomBytes32, randomKey} import org.scalatest.Outcome import org.scalatest.funsuite.FixtureAnyFunSuiteLike import scodec.bits.HexStringSyntax @@ -46,18 +46,18 @@ import scala.concurrent.duration._ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike { - val featuresWithoutMpp = Features[FeatureScope]( + val featuresWithoutMpp = Features[Feature]( VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory, ) - val featuresWithMpp = Features[FeatureScope]( + val featuresWithMpp = Features[Feature]( VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory, BasicMultiPartPayment -> Optional ) - val featuresWithKeySend = Features[FeatureScope]( + val featuresWithKeySend = Features[Feature]( VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory, KeySend -> Optional diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentInitiatorSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentInitiatorSpec.scala index 68bb4500d5..710ab371cb 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentInitiatorSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentInitiatorSpec.scala @@ -37,10 +37,10 @@ import fr.acinq.eclair.router.Router._ import fr.acinq.eclair.wire.protocol.OnionPaymentPayloadTlv.{AmountToForward, KeySend, OutgoingCltv} import fr.acinq.eclair.wire.protocol.PaymentOnion.FinalTlvPayload import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{CltvExpiryDelta, Feature, FeatureSupport, Features, InvoiceFeature, MilliSatoshiLong, NodeParams, TestConstants, TestKitBaseClass, TimestampSecond, UnknownFeature, randomBytes32, randomKey} +import fr.acinq.eclair.{CltvExpiryDelta, Features, InvoiceFeature, MilliSatoshiLong, NodeParams, TestConstants, TestKitBaseClass, TimestampSecond, UnknownFeature, randomBytes32, randomKey} import org.scalatest.funsuite.FixtureAnyFunSuiteLike import org.scalatest.{Outcome, Tag} -import scodec.bits.{BinStringSyntax, ByteVector, HexStringSyntax} +import scodec.bits.{ByteVector, HexStringSyntax} import java.util.UUID import scala.concurrent.duration._ @@ -53,18 +53,18 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike case class FixtureParam(nodeParams: NodeParams, initiator: TestActorRef[PaymentInitiator], payFsm: TestProbe, multiPartPayFsm: TestProbe, sender: TestProbe, eventListener: TestProbe) - val featuresWithoutMpp: Features[InvoiceFeature] = Features[InvoiceFeature]( + val featuresWithoutMpp: Features[InvoiceFeature] = Features( VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory ) - val featuresWithMpp: Features[InvoiceFeature] = Features[InvoiceFeature]( + val featuresWithMpp: Features[InvoiceFeature] = Features( VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory, BasicMultiPartPayment -> Optional, ) - val featuresWithTrampoline: Features[InvoiceFeature] = Features[InvoiceFeature]( + val featuresWithTrampoline: Features[InvoiceFeature] = Features( VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory, BasicMultiPartPayment -> Optional, @@ -128,7 +128,7 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike Bolt11Invoice.Description("Some invoice"), Bolt11Invoice.PaymentSecret(randomBytes32()), Bolt11Invoice.Expiry(3600), - Bolt11Invoice.InvoiceFeatures(Features[InvoiceFeature](Map[Feature with InvoiceFeature, FeatureSupport](VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory), Set(UnknownFeature(42))).unscoped()) + Bolt11Invoice.InvoiceFeatures(Features(Map(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory), unknown = Set(UnknownFeature(42)))) ) val invoice = Bolt11Invoice("lnbc", Some(finalAmount), TimestampSecond.now(), randomKey().publicKey, taggedFields, ByteVector.empty) val req = SendPaymentToNode(finalAmount + 100.msat, invoice, 1, routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/AnnouncementsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/AnnouncementsSpec.scala index 1878186dce..55ef00a669 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/AnnouncementsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/AnnouncementsSpec.scala @@ -54,7 +54,7 @@ class AnnouncementsSpec extends AnyFunSuite { } test("create valid signed node announcement") { - val features = Features[FeatureScope]( + val features = Features( Features.DataLossProtect -> FeatureSupport.Optional, Features.InitialRoutingSync -> FeatureSupport.Optional, Features.ChannelRangeQueries -> FeatureSupport.Optional, diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecsSpec.scala index 4b993c9b4d..759aee0dab 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecsSpec.scala @@ -271,7 +271,7 @@ class LightningMessageCodecsSpec extends AnyFunSuite { val commit_sig = CommitSig(randomBytes32(), randomBytes64(), randomBytes64() :: randomBytes64() :: randomBytes64() :: Nil) val revoke_and_ack = RevokeAndAck(randomBytes32(), scalar(0), point(1)) val channel_announcement = ChannelAnnouncement(randomBytes64(), randomBytes64(), randomBytes64(), randomBytes64(), Features(bin(7, 9)), Block.RegtestGenesisBlock.hash, ShortChannelId(1), randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey) - val node_announcement = NodeAnnouncement(randomBytes64(), Features[FeatureScope](DataLossProtect -> Optional), 1 unixsec, randomKey().publicKey, Color(100.toByte, 200.toByte, 300.toByte), "node-alias", IPv4(InetAddress.getByAddress(Array[Byte](192.toByte, 168.toByte, 1.toByte, 42.toByte)).asInstanceOf[Inet4Address], 42000) :: Nil) + val node_announcement = NodeAnnouncement(randomBytes64(), Features(DataLossProtect -> Optional), 1 unixsec, randomKey().publicKey, Color(100.toByte, 200.toByte, 300.toByte), "node-alias", IPv4(InetAddress.getByAddress(Array[Byte](192.toByte, 168.toByte, 1.toByte, 42.toByte)).asInstanceOf[Inet4Address], 42000) :: Nil) val channel_update = ChannelUpdate(randomBytes64(), Block.RegtestGenesisBlock.hash, ShortChannelId(1), 2 unixsec, ChannelUpdate.ChannelFlags.DUMMY, CltvExpiryDelta(3), 4 msat, 5 msat, 6, None) val announcement_signatures = AnnouncementSignatures(randomBytes32(), ShortChannelId(42), randomBytes64(), randomBytes64()) val gossip_timestamp_filter = GossipTimestampFilter(Block.RegtestGenesisBlock.blockId, 100000 unixsec, 1500) From 18ba90067ec04741cd9c52b3a62a04f98dd46b68 Mon Sep 17 00:00:00 2001 From: Pierre-Marie Padiou Date: Wed, 16 Mar 2022 16:33:02 +0100 Subject: [PATCH 036/121] (Minor) Fix timeout in tests (#2208) This is a classic race condition that can appear when we register to events in tests. We need to make sure that the subscription is taken into account before moving forward. --- .../channel/publish/ReplaceableTxPublisherSpec.scala | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPublisherSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPublisherSpec.scala index ae2a92b651..44b6d5949a 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPublisherSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPublisherSpec.scala @@ -36,7 +36,7 @@ import fr.acinq.eclair.channel.publish.TxPublisher._ import fr.acinq.eclair.channel.states.{ChannelStateTestsHelperMethods, ChannelStateTestsTags} import fr.acinq.eclair.transactions.Transactions import fr.acinq.eclair.transactions.Transactions._ -import fr.acinq.eclair.{BlockHeight, MilliSatoshiLong, NodeParams, TestConstants, TestFeeEstimator, TestKitBaseClass, randomBytes32, randomKey} +import fr.acinq.eclair.{BlockHeight, MilliSatoshiLong, NodeParams, NotificationsLogger, TestConstants, TestFeeEstimator, TestKitBaseClass, randomBytes32, randomKey} import org.scalatest.BeforeAndAfterAll import org.scalatest.funsuite.AnyFunSuiteLike @@ -523,8 +523,16 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w // A new block is found, and the feerate has increased for our block target, but we don't have enough funds to bump the fees. system.eventStream.subscribe(probe.ref, classOf[NotifyNodeOperator]) + // just making sure that we have been subscribed to the event, otherwise there is a possible race condition + awaitCond({ + system.eventStream.publish(NotifyNodeOperator(NotificationsLogger.Info, "ping")) + probe.msgAvailable + }, max = 30 seconds) system.eventStream.publish(CurrentBlockHeight(aliceBlockHeight() + 15)) - probe.expectMsgType[NotifyNodeOperator] + probe.fishForMessage() { + case nno: NotifyNodeOperator => nno.severity != NotificationsLogger.Info + case _ => false + } val mempoolTxs2 = getMempool() assert(mempoolTxs1.map(_.txid).toSet === mempoolTxs2.map(_.txid).toSet) } From 5af042a56afa549a3e5480ad23f0b228a869f242 Mon Sep 17 00:00:00 2001 From: Richard Myers Date: Fri, 18 Mar 2022 14:33:46 +0100 Subject: [PATCH 037/121] Add messages for when startup fails due to wrong bitcoind wallet(s) loaded (#2209) Eclair fails at startup when eclair.bitcoind.wallet is mis-configured or not loaded by bitcoind. We now show different error messages for specific failure situations and report the current wallet configuration and loaded wallets where appropriate. --- .../main/scala/fr/acinq/eclair/Setup.scala | 22 ++++-- .../blockchain/bitcoind/BitcoindService.scala | 14 ++-- .../integration/StartupIntegrationSpec.scala | 71 +++++++++++++++++++ 3 files changed, 94 insertions(+), 13 deletions(-) create mode 100644 eclair-core/src/test/scala/fr/acinq/eclair/integration/StartupIntegrationSpec.scala diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala index 7c74fca89e..1f49f6fa67 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala @@ -165,18 +165,22 @@ class Setup(val datadir: File, val future = for { json <- bitcoinClient.invoke("getblockchaininfo").recover { case e => throw BitcoinRPCConnectionException(e) } // Make sure wallet support is enabled in bitcoind. - _ <- bitcoinClient.invoke("getbalance").recover { case e => throw BitcoinWalletDisabledException(e) } + wallets <- bitcoinClient.invoke("listwallets").recover { case e => throw BitcoinWalletDisabledException(e) } + .collect { + case JArray(values) => values.map(value => value.extract[String]) + } progress = (json \ "verificationprogress").extract[Double] ibd = (json \ "initialblockdownload").extract[Boolean] blocks = (json \ "blocks").extract[Long] headers = (json \ "headers").extract[Long] chainHash <- bitcoinClient.invoke("getblockhash", 0).map(_.extract[String]).map(s => ByteVector32.fromValidHex(s)).map(_.reverse) bitcoinVersion <- bitcoinClient.invoke("getnetworkinfo").map(json => json \ "version").map(_.extract[Int]) - unspentAddresses <- bitcoinClient.invoke("listunspent").collect { case JArray(values) => - values - .filter(value => (value \ "spendable").extract[Boolean]) - .map(value => (value \ "address").extract[String]) - } + unspentAddresses <- bitcoinClient.invoke("listunspent").recover { _ => if (wallet.isEmpty && wallets.length > 1) throw BitcoinDefaultWalletException(wallets) else throw BitcoinWalletNotLoadedException(wallet.getOrElse(""), wallets) } + .collect { case JArray(values) => + values + .filter(value => (value \ "spendable").extract[Boolean]) + .map(value => (value \ "address").extract[String]) + } _ <- chain match { case "mainnet" => bitcoinClient.invoke("getrawtransaction", "2157b554dcfda405233906e461ee593875ae4b1b97615872db6a25130ecc1dd6") // coinbase of #500000 case "testnet" => bitcoinClient.invoke("getrawtransaction", "8f38a0dd41dc0ae7509081e262d791f8d53ed6f884323796d5ec7b0966dd3825") // coinbase of #1500000 @@ -402,7 +406,11 @@ case object BitcoinZMQConnectionTimeoutException extends RuntimeException("could case class BitcoinRPCConnectionException(e: Throwable) extends RuntimeException("could not connect to bitcoind using json-rpc", e) -case class BitcoinWalletDisabledException(e: Throwable) extends RuntimeException("bitcoind wallet not available", e) +case class BitcoinWalletDisabledException(e: Throwable) extends RuntimeException("bitcoind wallet support disabled", e) + +case class BitcoinDefaultWalletException(loaded: List[String]) extends RuntimeException(s"no bitcoind wallet configured, but multiple wallets loaded: ${loaded.map("\"" + _ + "\"").mkString("[", ",", "]")}") + +case class BitcoinWalletNotLoadedException(wallet: String, loaded: List[String]) extends RuntimeException(s"configured wallet \"$wallet\" not in the set of loaded bitcoind wallets: ${loaded.map("\"" + _ + "\"").mkString("[", ",", "]")}") case object EmptyAPIPasswordException extends RuntimeException("must set a password for the json-rpc api") diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoindService.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoindService.scala index c4ad31b42b..1135514485 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoindService.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoindService.scala @@ -66,7 +66,7 @@ trait BitcoindService extends Logging { var bitcoinrpcauthmethod: BitcoinJsonRPCAuthMethod = _ var bitcoincli: ActorRef = _ - def startBitcoind(useCookie: Boolean = false): Unit = { + def startBitcoind(useCookie: Boolean = false, startupFlags: String = ""): Unit = { Files.createDirectories(PATH_BITCOIND_DATADIR.toPath) if (!Files.exists(new File(PATH_BITCOIND_DATADIR.toString, "bitcoin.conf").toPath)) { val is = classOf[IntegrationSpec].getResourceAsStream("/integration/bitcoin.conf") @@ -87,7 +87,7 @@ trait BitcoindService extends Logging { Files.writeString(new File(PATH_BITCOIND_DATADIR.toString, "bitcoin.conf").toPath, conf) } - bitcoind = s"$PATH_BITCOIND -datadir=$PATH_BITCOIND_DATADIR".run() + bitcoind = s"$PATH_BITCOIND -datadir=$PATH_BITCOIND_DATADIR $startupFlags".run() bitcoinrpcauthmethod = if (useCookie) { SafeCookie(s"$PATH_BITCOIND_DATADIR/regtest/.cookie") } else { @@ -111,12 +111,14 @@ trait BitcoindService extends Logging { bitcoind.exitValue() } - def restartBitcoind(sender: TestProbe = TestProbe(), useCookie: Boolean = false): Unit = { + def restartBitcoind(sender: TestProbe = TestProbe(), useCookie: Boolean = false, startupFlags: String = "", loadWallet: Boolean = true): Unit = { stopBitcoind() - startBitcoind(useCookie = useCookie) + startBitcoind(useCookie = useCookie, startupFlags = startupFlags) waitForBitcoindUp(sender) - sender.send(bitcoincli, BitcoinReq("loadwallet", defaultWallet)) - sender.expectMsgType[JValue] + if (loadWallet) { + sender.send(bitcoincli, BitcoinReq("loadwallet", defaultWallet)) + sender.expectMsgType[JValue] + } } private def waitForBitcoindUp(sender: TestProbe): Unit = { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/StartupIntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/StartupIntegrationSpec.scala new file mode 100644 index 0000000000..1aeddcad3f --- /dev/null +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/StartupIntegrationSpec.scala @@ -0,0 +1,71 @@ +/* + * 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.integration + +import akka.testkit.TestProbe +import com.typesafe.config.ConfigFactory +import fr.acinq.eclair.blockchain.bitcoind.BitcoindService.BitcoinReq +import fr.acinq.eclair.blockchain.bitcoind.rpc.{Error, JsonRPCError} +import fr.acinq.eclair.{BitcoinDefaultWalletException, BitcoinWalletDisabledException, BitcoinWalletNotLoadedException, TestUtils} + +import scala.jdk.CollectionConverters._ + +/** + * Created by remyers on 16/03/2022. + */ + +class StartupIntegrationSpec extends IntegrationSpec { + + test("no bitcoind wallet configured and one wallet loaded") { + instantiateEclairNode("A", ConfigFactory.parseMap(Map("eclair.bitcoind.wallet" -> "", "eclair.server.port" -> TestUtils.availablePort).asJava).withFallback(withDefaultCommitment).withFallback(commonConfig)) + } + + test("no bitcoind wallet configured and two wallets loaded") { + val sender = TestProbe() + sender.send(bitcoincli, BitcoinReq("createwallet", "")) + sender.expectMsgType[Any] + val thrown = intercept[BitcoinDefaultWalletException] { + instantiateEclairNode("C", ConfigFactory.parseMap(Map("eclair.bitcoind.wallet" -> "", "eclair.server.port" -> TestUtils.availablePort).asJava).withFallback(withDefaultCommitment).withFallback(commonConfig)) + } + assert(thrown === BitcoinDefaultWalletException(List(defaultWallet, ""))) + } + + test("explicit bitcoind wallet configured and two wallets loaded") { + val sender = TestProbe() + sender.send(bitcoincli, BitcoinReq("createwallet", "")) + sender.expectMsgType[Any] + instantiateEclairNode("D", ConfigFactory.parseMap(Map("eclair.server.port" -> TestUtils.availablePort).asJava).withFallback(withDefaultCommitment).withFallback(commonConfig)) + } + + test("explicit bitcoind wallet configured but not loaded") { + val sender = TestProbe() + sender.send(bitcoincli, BitcoinReq("createwallet", "")) + sender.expectMsgType[Any] + val thrown = intercept[BitcoinWalletNotLoadedException] { + instantiateEclairNode("E", ConfigFactory.parseMap(Map("eclair.bitcoind.wallet" -> "notloaded", "eclair.server.port" -> TestUtils.availablePort).asJava).withFallback(withDefaultCommitment).withFallback(commonConfig)) + } + assert(thrown === BitcoinWalletNotLoadedException("notloaded", List(defaultWallet, ""))) + } + + test("bitcoind started with wallets disabled") { + restartBitcoind(startupFlags = "-disablewallet", loadWallet = false) + val thrown = intercept[BitcoinWalletDisabledException] { + instantiateEclairNode("F", ConfigFactory.load().getConfig("eclair").withFallback(withDefaultCommitment).withFallback(commonConfig)) + } + assert(thrown === BitcoinWalletDisabledException(e = JsonRPCError(Error(-32601, "Method not found")))) + } +} \ No newline at end of file From fd849095854547dfcf0af5e80aa906fa36cdbbf4 Mon Sep 17 00:00:00 2001 From: Thomas HUET <81159533+thomash-acinq@users.noreply.github.com> Date: Mon, 21 Mar 2022 14:43:23 +0100 Subject: [PATCH 038/121] Fix TLV tag number in exception (#2212) --- .../scala/fr/acinq/eclair/wire/protocol/MessageOnion.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/MessageOnion.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/MessageOnion.scala index 4c09d810f7..f76c310cf3 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/MessageOnion.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/MessageOnion.scala @@ -94,7 +94,7 @@ object MessageOnionCodecs { val perHopPayloadCodec: Codec[TlvStream[OnionMessagePayloadTlv]] = TlvCodecs.lengthPrefixedTlvStream[OnionMessagePayloadTlv](onionTlvCodec).complete val relayPerHopPayloadCodec: Codec[RelayPayload] = perHopPayloadCodec.narrow({ - case tlvs if tlvs.get[EncryptedData].isEmpty => Attempt.failure(MissingRequiredTlv(UInt64(10))) + case tlvs if tlvs.get[EncryptedData].isEmpty => Attempt.failure(MissingRequiredTlv(UInt64(4))) case tlvs if tlvs.get[ReplyPath].nonEmpty => Attempt.failure(ForbiddenTlv(UInt64(2))) case tlvs => Attempt.successful(RelayPayload(tlvs)) }, { @@ -102,7 +102,7 @@ object MessageOnionCodecs { }) val finalPerHopPayloadCodec: Codec[FinalPayload] = perHopPayloadCodec.narrow({ - case tlvs if tlvs.get[EncryptedData].isEmpty => Attempt.failure(MissingRequiredTlv(UInt64(10))) + case tlvs if tlvs.get[EncryptedData].isEmpty => Attempt.failure(MissingRequiredTlv(UInt64(4))) case tlvs => Attempt.successful(FinalPayload(tlvs)) }, { case FinalPayload(tlvs) => tlvs From 3e02046a52a19f9230c4bb63303154108860923d Mon Sep 17 00:00:00 2001 From: Bastien Teinturier <31281497+t-bast@users.noreply.github.com> Date: Thu, 24 Mar 2022 10:36:24 +0100 Subject: [PATCH 039/121] Add logs about local channel graph inclusion (#2184) We've seen a few reports of local channels sometimes being ignored during path-finding (e.g. #2176). When that happens, it's currently hard to know whether the channel is in the graph or not: knowing that would let us know if there is a bug in the graph management code or the path-finding algorithm itself. --- .../scala/fr/acinq/eclair/router/RouteCalculation.scala | 1 + .../main/scala/fr/acinq/eclair/router/Validation.scala | 8 +++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/RouteCalculation.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/RouteCalculation.scala index d1c409dd93..88537387b7 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/RouteCalculation.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/RouteCalculation.scala @@ -118,6 +118,7 @@ object RouteCalculation { log.info(s"finding routes ${r.source}->${r.target} with assistedChannels={} ignoreNodes={} ignoreChannels={} excludedChannels={}", assistedChannels.keys.mkString(","), r.ignore.nodes.map(_.value).mkString(","), r.ignore.channels.mkString(","), d.excludedChannels.mkString(",")) log.info("finding routes with params={}, multiPart={}", params, r.allowMultiPart) + log.info("local channels to recipient: {}", d.graph.getEdgesBetween(r.source, r.target).map(e => s"${e.desc.shortChannelId} (${e.balance_opt}/${e.capacity})").mkString(", ")) val tags = TagSet.Empty.withTag(Tags.MultiPart, r.allowMultiPart).withTag(Tags.Amount, Tags.amountBucket(r.amount)) KamonExt.time(Metrics.FindRouteDuration.withTags(tags.withTag(Tags.NumberOfRoutes, routesToFind.toLong))) { val result = if (r.allowMultiPart) { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/Validation.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/Validation.scala index 36fb781148..66b007c176 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/Validation.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/Validation.scala @@ -300,8 +300,10 @@ object Validation { // update the graph val pc1 = pc.applyChannelUpdate(update) val graph1 = if (u.channelFlags.isEnabled) { + update.left.foreach(_ => log.info("added local shortChannelId={} public={} to the network graph", u.shortChannelId, publicChannel)) d.graph.addEdge(desc, u, pc1.capacity, pc1.getBalanceSameSideAs(u)) } else { + update.left.foreach(_ => log.info("removed local shortChannelId={} public={} from the network graph", u.shortChannelId, publicChannel)) d.graph.removeEdge(desc) } d.copy(channels = d.channels + (u.shortChannelId -> pc1), rebroadcast = d.rebroadcast.copy(updates = d.rebroadcast.updates + (u -> origins)), graph = graph1) @@ -313,6 +315,7 @@ object Validation { // we also need to update the graph val pc1 = pc.applyChannelUpdate(update) val graph1 = d.graph.addEdge(desc, u, pc1.capacity, pc1.getBalanceSameSideAs(u)) + update.left.foreach(_ => log.info("added local shortChannelId={} public={} to the network graph", u.shortChannelId, publicChannel)) d.copy(channels = d.channels + (u.shortChannelId -> pc1), privateChannels = d.privateChannels - u.shortChannelId, rebroadcast = d.rebroadcast.copy(updates = d.rebroadcast.updates + (u -> origins)), graph = graph1) } } else if (d.awaiting.keys.exists(c => c.shortChannelId == u.shortChannelId)) { @@ -349,8 +352,10 @@ object Validation { // we also need to update the graph val pc1 = pc.applyChannelUpdate(update) val graph1 = if (u.channelFlags.isEnabled) { + update.left.foreach(_ => log.info("added local shortChannelId={} public={} to the network graph", u.shortChannelId, publicChannel)) d.graph.addEdge(desc, u, pc1.capacity, pc1.getBalanceSameSideAs(u)) } else { + update.left.foreach(_ => log.info("removed local shortChannelId={} public={} from the network graph", u.shortChannelId, publicChannel)) d.graph.removeEdge(desc) } d.copy(privateChannels = d.privateChannels + (u.shortChannelId -> pc1), graph = graph1) @@ -361,6 +366,7 @@ object Validation { // we also need to update the graph val pc1 = pc.applyChannelUpdate(update) val graph1 = d.graph.addEdge(desc, u, pc1.capacity, pc1.getBalanceSameSideAs(u)) + update.left.foreach(_ => log.info("added local shortChannelId={} public={} to the network graph", u.shortChannelId, publicChannel)) d.copy(privateChannels = d.privateChannels + (u.shortChannelId -> pc1), graph = graph1) } } else if (db.isPruned(u.shortChannelId) && !StaleChannels.isStale(u)) { @@ -441,7 +447,7 @@ object Validation { d } else if (d.privateChannels.contains(shortChannelId)) { // the channel was private or public-but-not-yet-announced, let's do the clean up - log.debug("removing private local channel and channel_update for channelId={} shortChannelId={}", channelId, shortChannelId) + log.info("removing private local channel and channel_update for channelId={} shortChannelId={}", channelId, shortChannelId) val desc1 = ChannelDesc(shortChannelId, localNodeId, remoteNodeId) val desc2 = ChannelDesc(shortChannelId, remoteNodeId, localNodeId) // we remove the corresponding updates from the graph From dbd9e38b5ddfa0ff88a04baf0eebd329aa4cff5a Mon Sep 17 00:00:00 2001 From: arjundashrath <54043589+arjundashrath@users.noreply.github.com> Date: Fri, 25 Mar 2022 16:13:34 +0530 Subject: [PATCH 040/121] Add explicit github permissions to bitcoin core build (#2200) The workflow that tests eclair against bitcoin's master branch doesn't need more permissions than reading the repository's code and building it. --- .github/workflows/latest-bitcoind.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/latest-bitcoind.yml b/.github/workflows/latest-bitcoind.yml index 7c687374ee..5c498d3680 100644 --- a/.github/workflows/latest-bitcoind.yml +++ b/.github/workflows/latest-bitcoind.yml @@ -5,6 +5,9 @@ on: # Run at midnight on Sunday and Wednesday. - cron: '0 0 * * 0,3' +permissions: + contents: read + jobs: regression-tests: From 682330987400b0d035ca5284e5c2fcb2fd6412df Mon Sep 17 00:00:00 2001 From: Pierre-Marie Padiou Date: Fri, 25 Mar 2022 14:32:35 +0100 Subject: [PATCH 041/121] Use `NodeAddress` everywhere instead of `InetAddress` (#2202) * use `NodeAddress` everywhere instead of `InetAddress` This makes us control more strictly when and where name resolution happens, which is important in a security-hardened setup. The `InetAddress` jdk class indeed does a lot of things behind the scenes, but now we restrict it to tcp-related classes like `Client` and `Server`. Also, in _cluster mode_ all outgoing connections (including tor) are now made on the front. * upgrade guava and rewrite nodeuri tests There is no reason to use the version of guava targetting android anymore. Also `HostAndPort` was in beta in our current lib version. We now use guava's `InetAddresses.toUriString()` to format host string, instead of manually adding brackets. Reworked `NodeURI` tests: - less repetition with one single test and multiple `testCases` - focus on non-reg (no need to verify what we know we don't support) --- .../scala/fr/acinq/eclair/NodeParams.scala | 30 ++-- .../scala/fr/acinq/eclair/io/Client.scala | 27 ++-- .../fr/acinq/eclair/io/ClientSpawner.scala | 9 +- .../scala/fr/acinq/eclair/io/NodeURI.scala | 11 +- .../main/scala/fr/acinq/eclair/io/Peer.scala | 12 +- .../fr/acinq/eclair/io/PeerConnection.scala | 16 +- .../scala/fr/acinq/eclair/io/PeerEvents.scala | 6 +- .../fr/acinq/eclair/io/ReconnectionTask.scala | 21 +-- .../scala/fr/acinq/eclair/io/Server.scala | 4 +- .../acinq/eclair/json/JsonSerializers.scala | 6 +- .../remote/EclairInternalsSerializer.scala | 19 +-- .../acinq/eclair/tor/Socks5Connection.scala | 13 +- .../wire/protocol/LightningMessageTypes.scala | 24 +-- .../scala/fr/acinq/eclair/PackageSpec.scala | 1 + .../integration/ChannelIntegrationSpec.scala | 5 +- .../eclair/integration/IntegrationSpec.scala | 5 +- .../fr/acinq/eclair/io/NodeURISpec.scala | 146 +++++------------- .../acinq/eclair/io/PeerConnectionSpec.scala | 4 +- .../scala/fr/acinq/eclair/io/PeerSpec.scala | 15 +- .../eclair/io/ReconnectionTaskSpec.scala | 4 +- .../eclair/json/JsonSerializersSpec.scala | 8 +- .../eclair/tor/Socks5ConnectionSpec.scala | 15 +- .../scala/fr/acinq/eclair/FrontSetup.scala | 4 +- .../fr/acinq/eclair/api/handlers/Node.scala | 4 +- eclair-node/src/test/resources/api/getinfo | 2 +- eclair-node/src/test/resources/api/peers | 2 +- .../fr/acinq/eclair/api/ApiServiceSpec.scala | 2 +- pom.xml | 2 +- 28 files changed, 174 insertions(+), 243 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala index e05dd037fc..0ec0fabc3b 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala @@ -166,6 +166,22 @@ object NodeParams extends Logging { def chainFromHash(chainHash: ByteVector32): String = chain2Hash.map(_.swap).getOrElse(chainHash, throw new RuntimeException(s"invalid chainHash '$chainHash'")) + def parseSocks5ProxyParams(config: Config): Option[Socks5ProxyParams] = { + if (config.getBoolean("socks5.enabled")) { + Some(Socks5ProxyParams( + address = new InetSocketAddress(config.getString("socks5.host"), config.getInt("socks5.port")), + credentials_opt = None, + randomizeCredentials = config.getBoolean("socks5.randomize-credentials"), + useForIPv4 = config.getBoolean("socks5.use-for-ipv4"), + useForIPv6 = config.getBoolean("socks5.use-for-ipv6"), + useForTor = config.getBoolean("socks5.use-for-tor"), + useForWatchdogs = config.getBoolean("socks5.use-for-watchdogs"), + )) + } else { + None + } + } + def makeNodeParams(config: Config, instanceId: UUID, nodeKeyManager: NodeKeyManager, channelKeyManager: ChannelKeyManager, torAddress_opt: Option[NodeAddress], database: Databases, blockHeight: AtomicLong, feeEstimator: FeeEstimator, pluginParams: Seq[PluginParams] = Nil): NodeParams = { @@ -302,19 +318,7 @@ object NodeParams extends Logging { val syncWhitelist: Set[PublicKey] = config.getStringList("sync-whitelist").asScala.map(s => PublicKey(ByteVector.fromValidHex(s))).toSet - val socksProxy_opt = if (config.getBoolean("socks5.enabled")) { - Some(Socks5ProxyParams( - address = new InetSocketAddress(config.getString("socks5.host"), config.getInt("socks5.port")), - credentials_opt = None, - randomizeCredentials = config.getBoolean("socks5.randomize-credentials"), - useForIPv4 = config.getBoolean("socks5.use-for-ipv4"), - useForIPv6 = config.getBoolean("socks5.use-for-ipv6"), - useForTor = config.getBoolean("socks5.use-for-tor"), - useForWatchdogs = config.getBoolean("socks5.use-for-watchdogs"), - )) - } else { - None - } + val socksProxy_opt = parseSocks5ProxyParams(config) val publicTorAddress_opt = if (config.getBoolean("tor.publish-onion-address")) torAddress_opt else None diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/Client.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/Client.scala index 6fdc36afdb..9e8742c968 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/io/Client.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/Client.scala @@ -16,8 +16,6 @@ package fr.acinq.eclair.io -import java.net.InetSocketAddress - import akka.actor.{Props, _} import akka.event.Logging.MDC import akka.io.Tcp.SO.KeepAlive @@ -28,14 +26,16 @@ import fr.acinq.eclair.Logs.LogCategory import fr.acinq.eclair.crypto.Noise.KeyPair import fr.acinq.eclair.tor.Socks5Connection.{Socks5Connect, Socks5Connected, Socks5Error} import fr.acinq.eclair.tor.{Socks5Connection, Socks5ProxyParams} +import fr.acinq.eclair.wire.protocol._ +import java.net.InetSocketAddress import scala.concurrent.duration._ /** * Created by PM on 27/10/2015. * */ -class Client(keyPair: KeyPair, socks5ProxyParams_opt: Option[Socks5ProxyParams], peerConnectionConf: PeerConnection.Conf, switchboard: ActorRef, router: ActorRef, remoteAddress: InetSocketAddress, remoteNodeId: PublicKey, origin_opt: Option[ActorRef], isPersistent: Boolean) extends Actor with DiagnosticActorLogging { +class Client(keyPair: KeyPair, socks5ProxyParams_opt: Option[Socks5ProxyParams], peerConnectionConf: PeerConnection.Conf, switchboard: ActorRef, router: ActorRef, remoteNodeAddress: NodeAddress, remoteNodeId: PublicKey, origin_opt: Option[ActorRef], isPersistent: Boolean) extends Actor with DiagnosticActorLogging { import context.system @@ -44,7 +44,14 @@ class Client(keyPair: KeyPair, socks5ProxyParams_opt: Option[Socks5ProxyParams], def receive: Receive = { case Symbol("connect") => - val (peerOrProxyAddress, proxyParams_opt) = socks5ProxyParams_opt.map(proxyParams => (proxyParams, Socks5ProxyParams.proxyAddress(remoteAddress, proxyParams))) match { + // note that there is no resolution here, it's either plain ip addresses, or unresolved tor hostnames + val remoteAddress = remoteNodeAddress match { + case addr: IPv4 => new InetSocketAddress(addr.ipv4, addr.port) + case addr: IPv6 => new InetSocketAddress(addr.ipv6, addr.port) + case addr: Tor2 => InetSocketAddress.createUnresolved(addr.host, addr.port) + case addr: Tor3 => InetSocketAddress.createUnresolved(addr.host, addr.port) + } + val (peerOrProxyAddress, proxyParams_opt) = socks5ProxyParams_opt.map(proxyParams => (proxyParams, Socks5ProxyParams.proxyAddress(remoteNodeAddress, proxyParams))) match { case Some((proxyParams, Some(proxyAddress))) => log.info(s"connecting to SOCKS5 proxy ${str(proxyAddress)}") (proxyAddress, Some(proxyParams)) @@ -53,14 +60,14 @@ class Client(keyPair: KeyPair, socks5ProxyParams_opt: Option[Socks5ProxyParams], (remoteAddress, None) } IO(Tcp) ! Tcp.Connect(peerOrProxyAddress, timeout = Some(20 seconds), options = KeepAlive(true) :: Nil, pullMode = true) - context become connecting(proxyParams_opt) + context become connecting(proxyParams_opt, remoteAddress) } - def connecting(proxyParams: Option[Socks5ProxyParams]): Receive = { + def connecting(proxyParams: Option[Socks5ProxyParams], remoteAddress: InetSocketAddress): Receive = { case Tcp.CommandFailed(c: Tcp.Connect) => val peerOrProxyAddress = c.remoteAddress log.info(s"connection failed to ${str(peerOrProxyAddress)}") - origin_opt.foreach(_ ! PeerConnection.ConnectionResult.ConnectionFailed(remoteAddress)) + origin_opt.foreach(_ ! PeerConnection.ConnectionResult.ConnectionFailed(remoteNodeAddress)) context stop self case Tcp.Connected(peerOrProxyAddress, _) => @@ -75,7 +82,7 @@ class Client(keyPair: KeyPair, socks5ProxyParams_opt: Option[Socks5ProxyParams], context become { case Tcp.CommandFailed(_: Socks5Connect) => log.info(s"connection failed to ${str(remoteAddress)} via SOCKS5 ${str(proxyAddress)}") - origin_opt.foreach(_ ! PeerConnection.ConnectionResult.ConnectionFailed(remoteAddress)) + origin_opt.foreach(_ ! PeerConnection.ConnectionResult.ConnectionFailed(remoteNodeAddress)) context stop self case Socks5Connected(_) => log.info(s"connected to ${str(remoteAddress)} via SOCKS5 proxy ${str(proxyAddress)}") @@ -127,13 +134,13 @@ class Client(keyPair: KeyPair, socks5ProxyParams_opt: Option[Socks5ProxyParams], switchboard = switchboard, router = router )) - peerConnection ! PeerConnection.PendingAuth(connection, remoteNodeId_opt = Some(remoteNodeId), address = remoteAddress, origin_opt = origin_opt, isPersistent = isPersistent) + peerConnection ! PeerConnection.PendingAuth(connection, remoteNodeId_opt = Some(remoteNodeId), address = remoteNodeAddress, origin_opt = origin_opt, isPersistent = isPersistent) peerConnection } } object Client { - def props(keyPair: KeyPair, socks5ProxyParams_opt: Option[Socks5ProxyParams], peerConnectionConf: PeerConnection.Conf, switchboard: ActorRef, router: ActorRef, address: InetSocketAddress, remoteNodeId: PublicKey, origin_opt: Option[ActorRef], isPersistent: Boolean): Props = Props(new Client(keyPair, socks5ProxyParams_opt, peerConnectionConf, switchboard, router, address, remoteNodeId, origin_opt, isPersistent)) + def props(keyPair: KeyPair, socks5ProxyParams_opt: Option[Socks5ProxyParams], peerConnectionConf: PeerConnection.Conf, switchboard: ActorRef, router: ActorRef, address: NodeAddress, remoteNodeId: PublicKey, origin_opt: Option[ActorRef], isPersistent: Boolean): Props = Props(new Client(keyPair, socks5ProxyParams_opt, peerConnectionConf, switchboard, router, address, remoteNodeId, origin_opt, isPersistent)) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/ClientSpawner.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/ClientSpawner.scala index efcb6601d5..e570279f2e 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/io/ClientSpawner.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/ClientSpawner.scala @@ -16,8 +16,6 @@ package fr.acinq.eclair.io -import java.net.InetSocketAddress - import akka.actor.{Actor, ActorLogging, ActorRef, DeadLetter, Props} import akka.cluster.Cluster import akka.cluster.pubsub.DistributedPubSub @@ -26,6 +24,7 @@ import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.eclair.crypto.Noise.KeyPair import fr.acinq.eclair.remote.EclairInternalsSerializer.RemoteTypes import fr.acinq.eclair.tor.Socks5ProxyParams +import fr.acinq.eclair.wire.protocol.NodeAddress class ClientSpawner(keyPair: KeyPair, socks5ProxyParams_opt: Option[Socks5ProxyParams], peerConnectionConf: PeerConnection.Conf, switchboard: ActorRef, router: ActorRef) extends Actor with ActorLogging { @@ -57,7 +56,7 @@ class ClientSpawner(keyPair: KeyPair, socks5ProxyParams_opt: Option[Socks5ProxyP log.warning("handling outgoing connection request locally") self forward req case _: DeadLetter => - // we don't care about other dead letters + // we don't care about other dead letters } } @@ -65,8 +64,8 @@ object ClientSpawner { def props(keyPair: KeyPair, socks5ProxyParams_opt: Option[Socks5ProxyParams], peerConnectionConf: PeerConnection.Conf, switchboard: ActorRef, router: ActorRef): Props = Props(new ClientSpawner(keyPair, socks5ProxyParams_opt, peerConnectionConf, switchboard, router)) - case class ConnectionRequest(address: InetSocketAddress, - remoteNodeId: PublicKey, + case class ConnectionRequest(remoteNodeId: PublicKey, + address: NodeAddress, origin: ActorRef, isPersistent: Boolean) extends RemoteTypes } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/NodeURI.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/NodeURI.scala index 6e7736fd28..b5f450a9c5 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/io/NodeURI.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/NodeURI.scala @@ -18,11 +18,12 @@ package fr.acinq.eclair.io import com.google.common.net.HostAndPort import fr.acinq.bitcoin.Crypto.PublicKey +import fr.acinq.eclair.wire.protocol.NodeAddress import scodec.bits.ByteVector import scala.util.{Failure, Success, Try} -case class NodeURI(nodeId: PublicKey, address: HostAndPort) { +case class NodeURI(nodeId: PublicKey, address: NodeAddress) { override def toString: String = s"$nodeId@$address" } @@ -40,10 +41,10 @@ object NodeURI { @throws[IllegalArgumentException] def parse(uri: String): NodeURI = { uri.split("@") match { - case Array(nodeId, address) => (Try(PublicKey(ByteVector.fromValidHex(nodeId))), Try(HostAndPort.fromString(address).withDefaultPort(DEFAULT_PORT))) match { - case (Success(pk), Success(hostAndPort)) => NodeURI(pk, hostAndPort) - case (Failure(_), _) => throw new IllegalArgumentException("Invalid node id") - case (_, Failure(_)) => throw new IllegalArgumentException("Invalid host:port") + case Array(nodeId, address) => (Try(PublicKey(ByteVector.fromValidHex(nodeId))), Try(HostAndPort.fromString(address)).flatMap(hostAndPort => NodeAddress.fromParts(hostAndPort.getHost, hostAndPort.getPortOrDefault(DEFAULT_PORT)))) match { + case (Success(pk), Success(nodeAddress)) => NodeURI(pk, nodeAddress) + case (Failure(t), _) => throw new IllegalArgumentException("Invalid node id", t) + case (_, Failure(t)) => throw new IllegalArgumentException("Invalid host:port", t) } case _ => throw new IllegalArgumentException("Invalid uri, should be nodeId@host:port") } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala index 5aae20924a..8d1f399532 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala @@ -21,7 +21,6 @@ import akka.actor.{Actor, ActorContext, ActorRef, ExtendedActorSystem, FSM, OneF import akka.event.Logging.MDC import akka.event.{BusLogging, DiagnosticLoggingAdapter} import akka.util.Timeout -import com.google.common.net.HostAndPort import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.bitcoin.{ByteVector32, Satoshi, SatoshiLong, Script} import fr.acinq.eclair.Features.Wumbo @@ -42,7 +41,6 @@ import fr.acinq.eclair.wire.protocol import fr.acinq.eclair.wire.protocol.{Error, HasChannelId, HasTemporaryChannelId, LightningMessage, NodeAddress, OnionMessage, RoutingMessage, UnknownMessage, Warning} import scodec.bits.ByteVector -import java.net.InetSocketAddress import scala.concurrent.ExecutionContext /** @@ -331,12 +329,12 @@ class Peer(val nodeParams: NodeParams, remoteNodeId: PublicKey, wallet: OnChainA def gotoConnected(connectionReady: PeerConnection.ConnectionReady, channels: Map[ChannelId, ActorRef]): State = { require(remoteNodeId == connectionReady.remoteNodeId, s"invalid nodeid: $remoteNodeId != ${connectionReady.remoteNodeId}") - log.debug("got authenticated connection to address {}:{}", connectionReady.address.getHostString, connectionReady.address.getPort) + log.debug("got authenticated connection to address {}", connectionReady.address) if (connectionReady.outgoing) { // we store the node address upon successful outgoing connection, so we can reconnect later // any previous address is overwritten - NodeAddress.fromParts(connectionReady.address.getHostString, connectionReady.address.getPort).map(nodeAddress => nodeParams.db.peers.addOrUpdatePeer(remoteNodeId, nodeAddress)) + nodeParams.db.peers.addOrUpdatePeer(remoteNodeId, connectionReady.address) } // let's bring existing/requested channels online @@ -445,7 +443,7 @@ object Peer { } case object Nothing extends Data { override def channels = Map.empty } case class DisconnectedData(channels: Map[FinalChannelId, ActorRef]) extends Data - case class ConnectedData(address: InetSocketAddress, peerConnection: ActorRef, localInit: protocol.Init, remoteInit: protocol.Init, channels: Map[ChannelId, ActorRef]) extends Data { + case class ConnectedData(address: NodeAddress, peerConnection: ActorRef, localInit: protocol.Init, remoteInit: protocol.Init, channels: Map[ChannelId, ActorRef]) extends Data { val connectionInfo: ConnectionInfo = ConnectionInfo(address, peerConnection, localInit, remoteInit) def localFeatures: Features[InitFeature] = localInit.features def remoteFeatures: Features[InitFeature] = remoteInit.features @@ -457,7 +455,7 @@ object Peer { case object CONNECTED extends State case class Init(storedChannels: Set[HasCommitments]) - case class Connect(nodeId: PublicKey, address_opt: Option[HostAndPort], replyTo: ActorRef, isPersistent: Boolean) { + case class Connect(nodeId: PublicKey, address_opt: Option[NodeAddress], replyTo: ActorRef, isPersistent: Boolean) { def uri: Option[NodeURI] = address_opt.map(NodeURI(nodeId, _)) } object Connect { @@ -476,7 +474,7 @@ object Peer { sealed trait PeerInfoResponse { def nodeId: PublicKey } - case class PeerInfo(peer: ActorRef, nodeId: PublicKey, state: State, address: Option[InetSocketAddress], channels: Int) extends PeerInfoResponse + case class PeerInfo(peer: ActorRef, nodeId: PublicKey, state: State, address: Option[NodeAddress], channels: Int) extends PeerInfoResponse case class PeerNotFound(nodeId: PublicKey) extends PeerInfoResponse { override def toString: String = s"peer $nodeId not found" } case class PeerRoutingMessage(peerConnection: ActorRef, remoteNodeId: PublicKey, message: RoutingMessage) extends RemoteTypes diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/PeerConnection.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/PeerConnection.scala index f60c7ad856..b50d589247 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/io/PeerConnection.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/PeerConnection.scala @@ -32,7 +32,6 @@ import fr.acinq.eclair.{FSMDiagnosticActorLogging, Feature, Features, InitFeatur import scodec.Attempt import scodec.bits.ByteVector -import java.net.InetSocketAddress import scala.concurrent.duration._ import scala.util.Random @@ -87,8 +86,7 @@ class PeerConnection(keyPair: KeyPair, conf: PeerConnection.Conf, switchboard: A when(AUTHENTICATING) { case Event(TransportHandler.HandshakeCompleted(remoteNodeId), d: AuthenticatingData) => cancelTimer(AUTH_TIMER) - import d.pendingAuth.address - log.info(s"connection authenticated with $remoteNodeId@${address.getHostString}:${address.getPort} direction=${if (d.pendingAuth.outgoing) "outgoing" else "incoming"}") + log.info(s"connection authenticated (direction=${if (d.pendingAuth.outgoing) "outgoing" else "incoming"})") Metrics.PeerConnectionsConnecting.withTag(Tags.ConnectionState, Tags.ConnectionStates.Authenticated).increment() switchboard ! Authenticated(self, remoteNodeId) goto(BEFORE_INIT) using BeforeInitData(remoteNodeId, d.pendingAuth, d.transport, d.isPersistent) @@ -104,8 +102,8 @@ class PeerConnection(keyPair: KeyPair, conf: PeerConnection.Conf, switchboard: A d.transport ! TransportHandler.Listener(self) Metrics.PeerConnectionsConnecting.withTag(Tags.ConnectionState, Tags.ConnectionStates.Initializing).increment() log.info(s"using features=$localFeatures") - val localInit = IPAddress(d.pendingAuth.address.getAddress, d.pendingAuth.address.getPort) match { - case Some(remoteAddress) if !d.pendingAuth.outgoing && NodeAddress.isPublicIPAddress(remoteAddress) => protocol.Init(localFeatures, TlvStream(InitTlv.Networks(chainHash :: Nil), InitTlv.RemoteAddress(remoteAddress))) + val localInit = d.pendingAuth.address match { + case remoteAddress if !d.pendingAuth.outgoing && NodeAddress.isPublicIPAddress(remoteAddress) => protocol.Init(localFeatures, TlvStream(InitTlv.Networks(chainHash :: Nil), InitTlv.RemoteAddress(remoteAddress))) case _ => protocol.Init(localFeatures, TlvStream(InitTlv.Networks(chainHash :: Nil))) } d.transport ! localInit @@ -120,7 +118,7 @@ class PeerConnection(keyPair: KeyPair, conf: PeerConnection.Conf, switchboard: A d.transport ! TransportHandler.ReadAck(remoteInit) log.info(s"peer is using features=${remoteInit.features}, networks=${remoteInit.networks.mkString(",")}") - remoteInit.remoteAddress_opt.foreach(address => log.info("peer reports that our IP address is {} (public={})", address.socketAddress.toString, NodeAddress.isPublicIPAddress(address))) + remoteInit.remoteAddress_opt.foreach(address => log.info("peer reports that our IP address is {} (public={})", address.toString, NodeAddress.isPublicIPAddress(address))) val featureGraphErr_opt = Features.validateFeatureGraph(remoteInit.features) if (remoteInit.networks.nonEmpty && remoteInit.networks.intersect(d.localInit.networks).isEmpty) { @@ -552,12 +550,12 @@ object PeerConnection { case object INITIALIZING extends State case object CONNECTED extends State - case class PendingAuth(connection: ActorRef, remoteNodeId_opt: Option[PublicKey], address: InetSocketAddress, origin_opt: Option[ActorRef], transport_opt: Option[ActorRef] = None, isPersistent: Boolean) { + case class PendingAuth(connection: ActorRef, remoteNodeId_opt: Option[PublicKey], address: NodeAddress, origin_opt: Option[ActorRef], transport_opt: Option[ActorRef] = None, isPersistent: Boolean) { def outgoing: Boolean = remoteNodeId_opt.isDefined // if this is an outgoing connection, we know the node id in advance } case class Authenticated(peerConnection: ActorRef, remoteNodeId: PublicKey) extends RemoteTypes case class InitializeConnection(peer: ActorRef, chainHash: ByteVector32, features: Features[InitFeature], doSync: Boolean) extends RemoteTypes - case class ConnectionReady(peerConnection: ActorRef, remoteNodeId: PublicKey, address: InetSocketAddress, outgoing: Boolean, localInit: protocol.Init, remoteInit: protocol.Init) extends RemoteTypes + case class ConnectionReady(peerConnection: ActorRef, remoteNodeId: PublicKey, address: NodeAddress, outgoing: Boolean, localInit: protocol.Init, remoteInit: protocol.Init) extends RemoteTypes sealed trait ConnectionResult extends RemoteTypes object ConnectionResult { @@ -569,7 +567,7 @@ object PeerConnection { } case object NoAddressFound extends ConnectionResult.Failure { override def toString: String = "no address found" } - case class ConnectionFailed(address: InetSocketAddress) extends ConnectionResult.Failure { override def toString: String = s"connection failed to $address" } + case class ConnectionFailed(address: NodeAddress) extends ConnectionResult.Failure { override def toString: String = s"connection failed to $address" } case class AuthenticationFailed(reason: String) extends ConnectionResult.Failure { override def toString: String = reason } case class InitializationFailed(reason: String) extends ConnectionResult.Failure { override def toString: String = reason } case class AlreadyConnected(peerConnection: ActorRef, peer: ActorRef) extends ConnectionResult.Failure with HasConnection { override def toString: String = "already connected" } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/PeerEvents.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/PeerEvents.scala index de213c5c55..73332db36f 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/io/PeerEvents.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/PeerEvents.scala @@ -19,13 +19,11 @@ package fr.acinq.eclair.io import akka.actor.ActorRef import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.eclair.wire.protocol -import fr.acinq.eclair.wire.protocol.UnknownMessage - -import java.net.InetSocketAddress +import fr.acinq.eclair.wire.protocol.{NodeAddress, UnknownMessage} sealed trait PeerEvent -case class ConnectionInfo(address: InetSocketAddress, peerConnection: ActorRef, localInit: protocol.Init, remoteInit: protocol.Init) +case class ConnectionInfo(address: NodeAddress, peerConnection: ActorRef, localInit: protocol.Init, remoteInit: protocol.Init) case class PeerConnected(peer: ActorRef, nodeId: PublicKey, connectionInfo: ConnectionInfo) extends PeerEvent diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/ReconnectionTask.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/ReconnectionTask.scala index d976504504..59059e03b2 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/io/ReconnectionTask.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/ReconnectionTask.scala @@ -21,14 +21,12 @@ import akka.cluster.Cluster import akka.cluster.pubsub.DistributedPubSub import akka.cluster.pubsub.DistributedPubSubMediator.Send import akka.event.Logging.MDC -import com.google.common.net.HostAndPort import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.eclair.Logs.LogCategory import fr.acinq.eclair.io.Monitoring.Metrics import fr.acinq.eclair.wire.protocol.{NodeAddress, OnionAddress} import fr.acinq.eclair.{FSMDiagnosticActorLogging, Logs, NodeParams, TimestampMilli} -import java.net.InetSocketAddress import scala.concurrent.duration.{FiniteDuration, _} import scala.util.Random @@ -130,12 +128,11 @@ class ReconnectionTask(nodeParams: NodeParams, remoteNodeId: PublicKey) extends case Event(TickReconnect, _) => stay() - case Event(Peer.Connect(_, hostAndPort_opt, replyTo, isPersistent), _) => + case Event(Peer.Connect(_, address_opt, replyTo, isPersistent), _) => // manual connection requests happen completely independently of the automated reconnection process; // we initiate a connection but don't modify our state. // if we are already connecting/connected, the peer will kill any duplicate connections - hostAndPort_opt - .map(hostAndPort2InetSocketAddress) + address_opt .orElse(getPeerAddressFromDb(nodeParams, remoteNodeId)) match { case Some(address) => connect(address, origin = replyTo, isPersistent) case None => replyTo ! PeerConnection.ConnectionResult.NoAddressFound @@ -148,10 +145,10 @@ class ReconnectionTask(nodeParams: NodeParams, remoteNodeId: PublicKey) extends // activate the extension only on demand, so that tests pass lazy val mediator = DistributedPubSub(context.system).mediator - private def connect(address: InetSocketAddress, origin: ActorRef, isPersistent: Boolean): Unit = { + private def connect(address: NodeAddress, origin: ActorRef, isPersistent: Boolean): Unit = { log.info(s"connecting to $address") - val req = ClientSpawner.ConnectionRequest(address, remoteNodeId, origin, isPersistent) - if (context.system.hasExtension(Cluster) && !address.getHostName.endsWith("onion")) { + val req = ClientSpawner.ConnectionRequest(remoteNodeId, address, origin, isPersistent) + if (context.system.hasExtension(Cluster)) { mediator ! Send(path = "/user/client-spawner", msg = req, localAffinity = false) } else { context.system.eventStream.publish(req) @@ -185,7 +182,7 @@ object ReconnectionTask { sealed trait Data case object Nothing extends Data case class IdleData(previousData: Data, since: TimestampMilli = TimestampMilli.now()) extends Data - case class ConnectingData(to: InetSocketAddress, nextReconnectionDelay: FiniteDuration) extends Data + case class ConnectingData(to: NodeAddress, nextReconnectionDelay: FiniteDuration) extends Data case class WaitingData(nextReconnectionDelay: FiniteDuration) extends Data // @formatter:on @@ -213,13 +210,11 @@ object ReconnectionTask { } } - def getPeerAddressFromDb(nodeParams: NodeParams, remoteNodeId: PublicKey): Option[InetSocketAddress] = { + def getPeerAddressFromDb(nodeParams: NodeParams, remoteNodeId: PublicKey): Option[NodeAddress] = { val nodeAddresses = nodeParams.db.peers.getPeer(remoteNodeId).toSeq ++ nodeParams.db.network.getNode(remoteNodeId).toSeq.flatMap(_.addresses) - selectNodeAddress(nodeParams, nodeAddresses).map(_.socketAddress) + selectNodeAddress(nodeParams, nodeAddresses) } - def hostAndPort2InetSocketAddress(hostAndPort: HostAndPort): InetSocketAddress = new InetSocketAddress(hostAndPort.getHost, hostAndPort.getPort) - /** * This helps prevent peers reconnection loops due to synchronization of reconnection attempts. */ diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/Server.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/Server.scala index b4d3542754..4dcda42af6 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/io/Server.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/Server.scala @@ -17,7 +17,6 @@ package fr.acinq.eclair.io import java.net.InetSocketAddress - import akka.Done import akka.actor.{Actor, ActorRef, DiagnosticActorLogging, Props} import akka.event.Logging.MDC @@ -26,6 +25,7 @@ import akka.io.{IO, Tcp} import fr.acinq.eclair.Logs import fr.acinq.eclair.Logs.LogCategory import fr.acinq.eclair.crypto.Noise.KeyPair +import fr.acinq.eclair.wire.protocol.{IPAddress, NodeAddress} import scala.concurrent.Promise @@ -62,7 +62,7 @@ class Server(keyPair: KeyPair, peerConnectionConf: PeerConnection.Conf, switchbo switchboard = switchboard, router = router )) - peerConnection ! PeerConnection.PendingAuth(connection, remoteNodeId_opt = None, address = remote, origin_opt = None, isPersistent = true) + peerConnection ! PeerConnection.PendingAuth(connection, remoteNodeId_opt = None, address = IPAddress(remote.getAddress, remote.getPort), origin_opt = None, isPersistent = true) listener ! ResumeAccepting(batchSize = 1) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/json/JsonSerializers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/json/JsonSerializers.scala index 6ea58b28e9..7621a7704f 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/json/JsonSerializers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/json/JsonSerializers.scala @@ -331,7 +331,7 @@ object FailureTypeSerializer extends MinimalSerializer({ }) object NodeAddressSerializer extends MinimalSerializer({ - case n: NodeAddress => JString(HostAndPort.fromParts(n.socketAddress.getHostString, n.socketAddress.getPort).toString) + case n: NodeAddress => JString(n.toString) }) // @formatter:off @@ -426,8 +426,8 @@ object OriginSerializer extends MinimalSerializer({ private case class GlobalBalanceJson(total: Btc, onChain: CorrectedOnChainBalance, offChain: OffChainBalance) object GlobalBalanceSerializer extends ConvertClassSerializer[GlobalBalance](b => GlobalBalanceJson(b.total, b.onChain, b.offChain)) -private case class PeerInfoJson(nodeId: PublicKey, state: String, address: Option[InetSocketAddress], channels: Int) -object PeerInfoSerializer extends ConvertClassSerializer[Peer.PeerInfo](peerInfo => PeerInfoJson(peerInfo.nodeId, peerInfo.state.toString, peerInfo.address, peerInfo.channels)) +private case class PeerInfoJson(nodeId: PublicKey, state: String, address: Option[String], channels: Int) +object PeerInfoSerializer extends ConvertClassSerializer[Peer.PeerInfo](peerInfo => PeerInfoJson(peerInfo.nodeId, peerInfo.state.toString, peerInfo.address.map(_.toString), peerInfo.channels)) private[json] case class MessageReceivedJson(pathId: Option[ByteVector], encodedReplyPath: Option[String], replyPath: Option[BlindedRoute], unknownTlvs: Map[String, ByteVector]) object OnionMessageReceivedSerializer extends ConvertClassSerializer[OnionMessages.ReceiveMessage](m => MessageReceivedJson(m.pathId, m.finalPayload.replyPath.map(route => blindedRouteCodec.encode(route.blindedRoute).require.bytes.toHex), m.finalPayload.replyPath.map(_.blindedRoute), m.finalPayload.records.unknown.map(tlv => tlv.tag.toString -> tlv.value).toMap)) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/remote/EclairInternalsSerializer.scala b/eclair-core/src/main/scala/fr/acinq/eclair/remote/EclairInternalsSerializer.scala index 854ecd1ada..8acaf528f0 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/remote/EclairInternalsSerializer.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/remote/EclairInternalsSerializer.scala @@ -25,7 +25,7 @@ import fr.acinq.eclair.io.Switchboard.RouterPeerConf import fr.acinq.eclair.io.{ClientSpawner, Peer, PeerConnection, Switchboard} import fr.acinq.eclair.payment.relay.Relayer.RelayFees import fr.acinq.eclair.router.Graph.{HeuristicsConstants, WeightRatios} -import fr.acinq.eclair.router.Router.{GossipDecision, MultiPartParams, PathFindingConf, RouterConf, SearchBoundaries, SendChannelQuery} +import fr.acinq.eclair.router.Router._ import fr.acinq.eclair.router._ import fr.acinq.eclair.wire.protocol.CommonCodecs._ import fr.acinq.eclair.wire.protocol.LightningMessageCodecs._ @@ -35,7 +35,6 @@ import fr.acinq.eclair.{CltvExpiryDelta, Feature, Features, InitFeature} import scodec._ import scodec.codecs._ -import java.net.{InetAddress, InetSocketAddress} import scala.concurrent.duration._ class EclairInternalsSerializer(val system: ExtendedActorSystem) extends ScodecSerializer(43, EclairInternalsSerializer.codec(system)) @@ -88,7 +87,7 @@ object EclairInternalsSerializer { val routerConfCodec: Codec[RouterConf] = ( ("watchSpentWindow" | finiteDurationCodec) :: - ("channelExcludeDuration" | finiteDurationCodec) :: + ("channelExcludeDuration" | finiteDurationCodec) :: ("routerBroadcastInterval" | finiteDurationCodec) :: ("requestNodeAnnouncements" | bool(8)) :: ("encodingType" | discriminated[EncodingType].by(uint8) @@ -131,15 +130,9 @@ object EclairInternalsSerializer { (path: String) => system.provider.resolveActorRef(path), (actor: ActorRef) => Serialization.serializedActorPath(actor)) - val inetAddressCodec: Codec[InetAddress] = discriminated[InetAddress].by(uint8) - .typecase(0, ipv4address) - .typecase(1, ipv6address) - - val inetSocketAddressCodec: Codec[InetSocketAddress] = (inetAddressCodec ~ uint16).xmap({ case (addr, port) => new InetSocketAddress(addr, port) }, socketAddr => (socketAddr.getAddress, socketAddr.getPort)) - def connectionRequestCodec(system: ExtendedActorSystem): Codec[ClientSpawner.ConnectionRequest] = ( - ("address" | inetSocketAddressCodec) :: - ("remoteNodeId" | publicKey) :: + ("remoteNodeId" | publicKey) :: + ("address" | nodeaddress) :: ("origin" | actorRefCodec(system)) :: ("isPersistent" | bool8)).as[ClientSpawner.ConnectionRequest] @@ -152,7 +145,7 @@ object EclairInternalsSerializer { def connectionReadyCodec(system: ExtendedActorSystem): Codec[PeerConnection.ConnectionReady] = ( ("peerConnection" | actorRefCodec(system)) :: ("remoteNodeId" | publicKey) :: - ("address" | inetSocketAddressCodec) :: + ("address" | nodeaddress) :: ("outgoing" | bool(8)) :: ("localInit" | lengthPrefixedInitCodec) :: ("remoteInit" | lengthPrefixedInitCodec)).as[PeerConnection.ConnectionReady] @@ -184,7 +177,7 @@ object EclairInternalsSerializer { .typecase(11, initializeConnectionCodec(system)) .typecase(12, connectionReadyCodec(system)) .typecase(13, provide(PeerConnection.ConnectionResult.NoAddressFound)) - .typecase(14, inetSocketAddressCodec.as[PeerConnection.ConnectionResult.ConnectionFailed]) + .typecase(14, nodeaddress.as[PeerConnection.ConnectionResult.ConnectionFailed]) .typecase(15, variableSizeBytes(uint16, utf8).as[PeerConnection.ConnectionResult.AuthenticationFailed]) .typecase(16, variableSizeBytes(uint16, utf8).as[PeerConnection.ConnectionResult.InitializationFailed]) .typecase(17, (actorRefCodec(system) :: actorRefCodec(system)).as[PeerConnection.ConnectionResult.AlreadyConnected]) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/tor/Socks5Connection.scala b/eclair-core/src/main/scala/fr/acinq/eclair/tor/Socks5Connection.scala index 7fa28b1118..0f27c104e7 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/tor/Socks5Connection.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/tor/Socks5Connection.scala @@ -231,12 +231,13 @@ object Socks5ProxyParams { ) - def proxyAddress(socketAddress: InetSocketAddress, proxyParams: Socks5ProxyParams): Option[InetSocketAddress] = - NodeAddress.fromParts(socketAddress.getHostString, socketAddress.getPort).toOption collect { - case _: IPv4 if proxyParams.useForIPv4 => proxyParams.address - case _: IPv6 if proxyParams.useForIPv6 => proxyParams.address - case _: Tor2 if proxyParams.useForTor => proxyParams.address - case _: Tor3 if proxyParams.useForTor => proxyParams.address + def proxyAddress(address: NodeAddress, proxyParams: Socks5ProxyParams): Option[InetSocketAddress] = + address match { + case _: IPv4 if proxyParams.useForIPv4 => Some(proxyParams.address) + case _: IPv6 if proxyParams.useForIPv6 => Some(proxyParams.address) + case _: Tor2 if proxyParams.useForTor => Some(proxyParams.address) + case _: Tor3 if proxyParams.useForTor => Some(proxyParams.address) + case _ => None } def proxyCredentials(proxyParams: Socks5ProxyParams): Option[Socks5Connection.Credentials] = diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageTypes.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageTypes.scala index 6a60124138..7db79d5f3a 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageTypes.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageTypes.scala @@ -17,14 +17,15 @@ package fr.acinq.eclair.wire.protocol import com.google.common.base.Charsets +import com.google.common.net.InetAddresses import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} import fr.acinq.bitcoin.{ByteVector32, ByteVector64, Satoshi} import fr.acinq.eclair.blockchain.fee.FeeratePerKw import fr.acinq.eclair.channel.{ChannelFlags, ChannelType} -import fr.acinq.eclair.{BlockHeight, CltvExpiry, CltvExpiryDelta, Feature, Features, InitFeature, MilliSatoshi, NodeFeature, ShortChannelId, TimestampSecond, UInt64} +import fr.acinq.eclair.{BlockHeight, CltvExpiry, CltvExpiryDelta, Feature, Features, InitFeature, MilliSatoshi, ShortChannelId, TimestampSecond, UInt64} import scodec.bits.ByteVector -import java.net.{Inet4Address, Inet6Address, InetAddress, InetSocketAddress} +import java.net.{Inet4Address, Inet6Address, InetAddress} import java.nio.charset.StandardCharsets import scala.util.Try @@ -214,7 +215,7 @@ case class Color(r: Byte, g: Byte, b: Byte) { } // @formatter:off -sealed trait NodeAddress { def socketAddress: InetSocketAddress } +sealed trait NodeAddress { def host: String; def port: Int; override def toString: String = s"$host:$port" } sealed trait OnionAddress extends NodeAddress sealed trait IPAddress extends NodeAddress // @formatter:on @@ -232,7 +233,7 @@ object NodeAddress { host match { case _ if host.endsWith(".onion") && host.length == 22 => Tor2(host.dropRight(6), port) case _ if host.endsWith(".onion") && host.length == 62 => Tor3(host.dropRight(6), port) - case _ => IPAddress(InetAddress.getByName(host), port).get + case _ => IPAddress(InetAddress.getByName(host), port) } } @@ -248,18 +249,17 @@ object NodeAddress { } object IPAddress { - def apply(inetAddress: InetAddress, port: Int): Option[IPAddress] = inetAddress match { - case address: Inet4Address => Some(IPv4(address, port)) - case address: Inet6Address => Some(IPv6(address, port)) - case _ => None + def apply(inetAddress: InetAddress, port: Int): IPAddress = inetAddress match { + case address: Inet4Address => IPv4(address, port) + case address: Inet6Address => IPv6(address, port) } } // @formatter:off -case class IPv4(ipv4: Inet4Address, port: Int) extends IPAddress { override def socketAddress = new InetSocketAddress(ipv4, port) } -case class IPv6(ipv6: Inet6Address, port: Int) extends IPAddress { override def socketAddress = new InetSocketAddress(ipv6, port) } -case class Tor2(tor2: String, port: Int) extends OnionAddress { override def socketAddress = InetSocketAddress.createUnresolved(tor2 + ".onion", port) } -case class Tor3(tor3: String, port: Int) extends OnionAddress { override def socketAddress = InetSocketAddress.createUnresolved(tor3 + ".onion", port) } +case class IPv4(ipv4: Inet4Address, port: Int) extends IPAddress { override def host: String = InetAddresses.toUriString(ipv4) } +case class IPv6(ipv6: Inet6Address, port: Int) extends IPAddress { override def host: String = InetAddresses.toUriString(ipv6) } +case class Tor2(tor2: String, port: Int) extends OnionAddress { override def host: String = tor2 + ".onion" } +case class Tor3(tor3: String, port: Int) extends OnionAddress { override def host: String = tor3 + ".onion" } // @formatter:on case class NodeAnnouncement(signature: ByteVector64, diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/PackageSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/PackageSpec.scala index fb5c8960a2..9927d7f818 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/PackageSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/PackageSpec.scala @@ -16,6 +16,7 @@ package fr.acinq.eclair +import com.google.common.net.InetAddresses import fr.acinq.bitcoin.Crypto.PrivateKey import fr.acinq.bitcoin.{Base58, Base58Check, Bech32, Block, ByteVector32, Crypto, Script} import org.scalatest.funsuite.AnyFunSuite diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/ChannelIntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/ChannelIntegrationSpec.scala index 22c50677e5..046fac8a91 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/ChannelIntegrationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/ChannelIntegrationSpec.scala @@ -19,7 +19,6 @@ package fr.acinq.eclair.integration import akka.actor.ActorRef import akka.pattern.pipe import akka.testkit.TestProbe -import com.google.common.net.HostAndPort import com.typesafe.config.ConfigFactory import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.bitcoin.{Base58, Base58Check, Bech32, Block, BtcDouble, ByteVector32, Crypto, OP_0, OP_CHECKSIG, OP_DUP, OP_EQUAL, OP_EQUALVERIFY, OP_HASH160, OP_PUSHDATA, OutPoint, SatoshiLong, Script, ScriptFlags, Transaction} @@ -35,7 +34,7 @@ import fr.acinq.eclair.payment.send.PaymentInitiator.SendPaymentToNode import fr.acinq.eclair.router.Router import fr.acinq.eclair.transactions.Transactions.{AnchorOutputsCommitmentFormat, TxOwner} import fr.acinq.eclair.transactions.{Scripts, Transactions} -import fr.acinq.eclair.wire.protocol.{ChannelAnnouncement, ChannelUpdate, PermanentChannelFailure, UpdateAddHtlc} +import fr.acinq.eclair.wire.protocol._ import fr.acinq.eclair.{MilliSatoshi, MilliSatoshiLong, randomBytes32} import org.json4s.JsonAST.{JString, JValue} import scodec.bits.ByteVector @@ -525,7 +524,7 @@ class StandardChannelIntegrationSpec extends ChannelIntegrationSpec { // reconnection sender.send(fundee.switchboard, Peer.Connect( nodeId = funder.nodeParams.nodeId, - address_opt = Some(HostAndPort.fromParts(funder.nodeParams.publicAddresses.head.socketAddress.getHostString, funder.nodeParams.publicAddresses.head.socketAddress.getPort)), + address_opt = funder.nodeParams.publicAddresses.headOption, sender.ref, isPersistent = true )) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/IntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/IntegrationSpec.scala index 49656e2ef7..aba35a9b18 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/IntegrationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/IntegrationSpec.scala @@ -18,7 +18,6 @@ package fr.acinq.eclair.integration import akka.actor.ActorSystem import akka.testkit.{TestKit, TestProbe} -import com.google.common.net.HostAndPort import com.typesafe.config.{Config, ConfigFactory} import fr.acinq.bitcoin.Satoshi import fr.acinq.eclair.Features._ @@ -29,6 +28,7 @@ import fr.acinq.eclair.payment.relay.Relayer.RelayFees import fr.acinq.eclair.router.Graph.WeightRatios import fr.acinq.eclair.router.RouteCalculation.ROUTE_MAX_LENGTH import fr.acinq.eclair.router.Router.{MultiPartParams, PathFindingConf, SearchBoundaries, NORMAL => _, State => _} +import fr.acinq.eclair.wire.protocol.NodeAddress import fr.acinq.eclair.{BlockHeight, CltvExpiryDelta, Kit, MilliSatoshi, MilliSatoshiLong, Setup, TestKitBaseClass} import grizzled.slf4j.Logging import org.json4s.{DefaultFormats, Formats} @@ -156,10 +156,9 @@ abstract class IntegrationSpec extends TestKitBaseClass with BitcoindService wit def connect(node1: Kit, node2: Kit): Unit = { val sender = TestProbe() - val address = node2.nodeParams.publicAddresses.head sender.send(node1.switchboard, Peer.Connect( nodeId = node2.nodeParams.nodeId, - address_opt = Some(HostAndPort.fromParts(address.socketAddress.getHostString, address.socketAddress.getPort)), + address_opt = node2.nodeParams.publicAddresses.headOption, sender.ref, isPersistent = true )) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/io/NodeURISpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/io/NodeURISpec.scala index d9ac3df090..1093ec5f34 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/io/NodeURISpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/io/NodeURISpec.scala @@ -22,125 +22,59 @@ import org.scalatest.funsuite.AnyFunSuite class NodeURISpec extends AnyFunSuite { val PUBKEY = "03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134" - val SHORT_PUB_KEY = "03933884aaf1d6b108397e5efe5c86bcf2d8ca" - val NOT_HEXA_PUB_KEY = "03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fcghijklmn" val IPV4_ENDURANCE = "34.250.234.192" val NAME_ENDURANCE = "endurance.acinq.co" val IPV6 = "[2405:204:66a9:536c:873f:dc4a:f055:a298]" - val IPV6_NO_BRACKETS = "2001:db8:a0b:12f0::1" - val IPV6_PREFIX = "[2001:db8:a0b:12f0::1/64]" val IPV6_ZONE_IDENTIFIER = "[2001:db8:a0b:12f0::1%eth0]" test("default port") { assert(NodeURI.DEFAULT_PORT == 9735) } - // ---------- IPV4 - - test("parse NodeURI with IPV4 and port") { - val uri = s"$PUBKEY@$IPV4_ENDURANCE:9737" - val nodeUri = NodeURI.parse(uri) - assert(nodeUri.nodeId.toString() == PUBKEY) - assert(nodeUri.address.getPort == 9737) - } - - test("parse NodeURI with IPV4 and NO port") { - val uri = s"$PUBKEY@$IPV4_ENDURANCE" - val nodeUri = NodeURI.parse(uri) - assert(nodeUri.address.getPort == NodeURI.DEFAULT_PORT) - } - - test("parse NodeURI with named host and port") { - val uri = s"$PUBKEY@$IPV4_ENDURANCE:9737" - val nodeUri = NodeURI.parse(uri) - assert(nodeUri.nodeId.toString() == PUBKEY) - assert(nodeUri.address.getPort == 9737) - } - - // ---------- IPV6 / regular with brackets - - test("parse NodeURI with IPV6 with brackets and port") { - val uri = s"$PUBKEY@$IPV6:9737" - val nodeUri = NodeURI.parse(uri) - assert(nodeUri.address.getPort == 9737) - } - - test("parse NodeURI with IPV6 with brackets and NO port") { - val uri = s"$PUBKEY@$IPV6" - val nodeUri = NodeURI.parse(uri) - assert(nodeUri.address.getPort == NodeURI.DEFAULT_PORT) - } - - // ---------- IPV6 / regular without brackets - - test("fail to parse NodeURI with IPV6 without brackets and port, and use default port") { - // this can not be parsed because we can not tell what the port is (brackets are required) and the port is the default - val uri = s"$PUBKEY@$IPV6_NO_BRACKETS:9737" - val nodeUri = NodeURI.parse(uri) - assert(nodeUri.address.getPort == NodeURI.DEFAULT_PORT) - } - - test("parse NodeURI with IPV6 without brackets and NO port") { - val uri = s"$PUBKEY@$IPV6_NO_BRACKETS" - val nodeUri = NodeURI.parse(uri) - assert(nodeUri.address.getPort == NodeURI.DEFAULT_PORT) - } - - // ---------- IPV6 / prefix - - test("parse NodeURI with IPV6 with prefix and port") { - val uri = s"$PUBKEY@$IPV6_PREFIX:9737" - val nodeUri = NodeURI.parse(uri) - assert(nodeUri.address.getPort == 9737) - } - - test("parse NodeURI with IPV6 with prefix and NO port") { - val uri = s"$PUBKEY@$IPV6_PREFIX" - val nodeUri = NodeURI.parse(uri) - assert(nodeUri.address.getPort == NodeURI.DEFAULT_PORT) - } - - // ---------- IPV6 / zone identifier - - test("parse NodeURI with IPV6 with a zone identifier and port") { - val uri = s"$PUBKEY@$IPV6_ZONE_IDENTIFIER:9737" - val nodeUri = NodeURI.parse(uri) - assert(nodeUri.address.getPort == 9737) - } - - test("parse NodeURI with IPV6 with a zone identifier and NO port") { - val uri = s"$PUBKEY@$IPV6_ZONE_IDENTIFIER" - val nodeUri = NodeURI.parse(uri) - assert(nodeUri.address.getPort == NodeURI.DEFAULT_PORT) - } - - // ---------- fail if public key is not valid - - test("parsing should fail if the public key is not correct") { - intercept[IllegalArgumentException](NodeURI.parse(s"$SHORT_PUB_KEY@$IPV4_ENDURANCE")) - intercept[IllegalArgumentException](NodeURI.parse(s"$NOT_HEXA_PUB_KEY@$IPV4_ENDURANCE")) + test("NodeURI parsing success") { + case class TestCase(uri: String, formattedAddr: String, port: Int) + + val testCases = List( + TestCase(s"$PUBKEY@$IPV4_ENDURANCE:9737", IPV4_ENDURANCE, 9737), + TestCase(s"$PUBKEY@$IPV4_ENDURANCE", IPV4_ENDURANCE, 9735), + TestCase(s"$PUBKEY@$NAME_ENDURANCE:9737", "13.248.222.197", 9737), + TestCase(s"$PUBKEY@$NAME_ENDURANCE", "13.248.222.197", 9735), + TestCase(s"$PUBKEY@$IPV6:9737", "[2405:204:66a9:536c:873f:dc4a:f055:a298]", 9737), + TestCase(s"$PUBKEY@$IPV6", "[2405:204:66a9:536c:873f:dc4a:f055:a298]", 9735), + TestCase(s"$PUBKEY@$IPV6_ZONE_IDENTIFIER:9737", "[2001:db8:a0b:12f0::1]", 9737), + TestCase(s"$PUBKEY@$IPV6_ZONE_IDENTIFIER", "[2001:db8:a0b:12f0::1]", 9735), + ) + + for (testCase <- testCases) { + val nodeUri = NodeURI.parse(testCase.uri) + assert(nodeUri.nodeId.toString() == PUBKEY) + assert(nodeUri.address.host == testCase.formattedAddr) + assert(nodeUri.address.port == testCase.port) + assert(nodeUri.toString === s"$PUBKEY@${testCase.formattedAddr}:${testCase.port}") + } } - // ---------- fail if host:port is not valid - - test("parsing should fail if host:port is not valid") { - intercept[IllegalArgumentException](NodeURI.parse(s"$SHORT_PUB_KEY@1.2.3.4:abcd")) - intercept[IllegalArgumentException](NodeURI.parse(s"$SHORT_PUB_KEY@1.2.3.4:999999999999999999999")) - } - - test("parsing should fail if the uri is malformed") { - intercept[IllegalArgumentException](NodeURI.parse("03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134@")) - intercept[IllegalArgumentException](NodeURI.parse("03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134@123.45@654321")) - intercept[IllegalArgumentException](NodeURI.parse("loremipsum")) - intercept[IllegalArgumentException](NodeURI.parse(IPV6)) - intercept[IllegalArgumentException](NodeURI.parse(IPV4_ENDURANCE)) - intercept[IllegalArgumentException](NodeURI.parse(PUBKEY)) - intercept[IllegalArgumentException](NodeURI.parse("")) - intercept[IllegalArgumentException](NodeURI.parse("@")) - intercept[IllegalArgumentException](NodeURI.parse(":")) + test("NodeURI parsing failure") { + val testCases = List( + s"03933884aaf1d6b108397e5efe5c86bcf2d8ca@$IPV4_ENDURANCE", + s"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fcghijklmn@$IPV4_ENDURANCE", + s"$PUBKEY@1.2.3.4:abcd", + s"$PUBKEY@1.2.3.4:999999999999999999999", + "03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134@", + "03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134@123.45@654321", + "loremipsum", + IPV6, + IPV4_ENDURANCE, + PUBKEY, + "", + "@", + ":", + ) + for (testCase <- testCases) { + intercept[IllegalArgumentException](NodeURI.parse(testCase)) + } } - } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerConnectionSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerConnectionSpec.scala index af9ba3e16b..a0a54e77bb 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerConnectionSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerConnectionSpec.scala @@ -42,7 +42,7 @@ class PeerConnectionSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wi def ipv4FromInet4(address: InetSocketAddress): IPv4 = IPv4.apply(address.getAddress.asInstanceOf[Inet4Address], address.getPort) - val address = new InetSocketAddress("localhost", 42000) + val address = NodeAddress.fromParts("localhost", 42000).get val fakeIPAddress = NodeAddress.fromParts("1.2.3.4", 42000).get // this map will store private keys so that we can sign new announcements at will val pub2priv: mutable.Map[PublicKey, PrivateKey] = mutable.HashMap.empty @@ -96,7 +96,7 @@ class PeerConnectionSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wi test("send incoming connection's remote address in init") { f => import f._ val probe = TestProbe() - val incomingConnection = PeerConnection.PendingAuth(connection.ref, None, fakeIPAddress.socketAddress, origin_opt = None, transport_opt = Some(transport.ref), isPersistent = true) + val incomingConnection = PeerConnection.PendingAuth(connection.ref, None, fakeIPAddress, origin_opt = None, transport_opt = Some(transport.ref), isPersistent = true) assert(!incomingConnection.outgoing) probe.send(peerConnection, incomingConnection) transport.send(peerConnection, TransportHandler.HandshakeCompleted(remoteNodeId)) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala index 58863d1f3f..3555ab0fab 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala @@ -20,7 +20,6 @@ import akka.actor.Status.Failure import akka.actor.typed.scaladsl.adapter.ClassicActorRefOps import akka.actor.{ActorContext, ActorRef, FSM, PoisonPill, Status} import akka.testkit.{TestFSMRef, TestProbe} -import com.google.common.net.HostAndPort import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.bitcoin.{Block, Btc, SatoshiLong, Script} import fr.acinq.eclair.FeatureSupport.{Mandatory, Optional} @@ -90,7 +89,7 @@ class PeerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Paralle // let's simulate a connection switchboard.send(peer, Peer.Init(channels)) val localInit = protocol.Init(peer.underlyingActor.nodeParams.features.initFeatures()) - switchboard.send(peer, PeerConnection.ConnectionReady(peerConnection.ref, remoteNodeId, fakeIPAddress.socketAddress, outgoing = true, localInit, remoteInit)) + switchboard.send(peer, PeerConnection.ConnectionReady(peerConnection.ref, remoteNodeId, fakeIPAddress, outgoing = true, localInit, remoteInit)) val probe = TestProbe() probe.send(peer, Peer.GetPeerInfo(Some(probe.ref.toTyped))) val peerInfo = probe.expectMsgType[Peer.PeerInfo] @@ -104,7 +103,7 @@ class PeerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Paralle val probe = TestProbe() connect(remoteNodeId, peer, peerConnection, switchboard, channels = Set(ChannelCodecsSpec.normal)) probe.send(peer, Peer.GetPeerInfo(None)) - probe.expectMsg(PeerInfo(peer, remoteNodeId, Peer.CONNECTED, Some(fakeIPAddress.socketAddress), 1)) + probe.expectMsg(PeerInfo(peer, remoteNodeId, Peer.CONNECTED, Some(fakeIPAddress), 1)) } test("fail to connect if no address provided or found") { f => @@ -124,12 +123,12 @@ class PeerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Paralle // we create a dummy tcp server and update bob's announcement to point to it val (mockServer, serverAddress) = createMockServer() - val mockAddress = HostAndPort.fromParts(serverAddress.getHostName, serverAddress.getPort) + val mockAddress_opt = NodeAddress.fromParts(serverAddress.getHostName, serverAddress.getPort).toOption val probe = TestProbe() probe.send(peer, Peer.Init(Set.empty)) // we have auto-reconnect=false so we need to manually tell the peer to reconnect - probe.send(peer, Peer.Connect(remoteNodeId, Some(mockAddress), probe.ref, isPersistent = true)) + probe.send(peer, Peer.Connect(remoteNodeId, mockAddress_opt, probe.ref, isPersistent = true)) // assert our mock server got an incoming connection (the client was spawned with the address from node_announcement) awaitCond(mockServer.accept() != null, max = 30 seconds, interval = 1 second) @@ -224,14 +223,14 @@ class PeerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Paralle (inputReconnected.localInit, inputReconnected.remoteInit) } - peerConnection2.send(peer, PeerConnection.ConnectionReady(peerConnection2.ref, remoteNodeId, fakeIPAddress.socketAddress, outgoing = false, localInit, remoteInit)) + peerConnection2.send(peer, PeerConnection.ConnectionReady(peerConnection2.ref, remoteNodeId, fakeIPAddress, outgoing = false, localInit, remoteInit)) // peer should kill previous connection peerConnection1.expectMsg(PeerConnection.Kill(PeerConnection.KillReason.ConnectionReplaced)) channel.expectMsg(INPUT_DISCONNECTED) channel.expectMsg(INPUT_RECONNECTED(peerConnection2.ref, localInit, remoteInit)) awaitCond(peer.stateData.asInstanceOf[Peer.ConnectedData].peerConnection === peerConnection2.ref) - peerConnection3.send(peer, PeerConnection.ConnectionReady(peerConnection3.ref, remoteNodeId, fakeIPAddress.socketAddress, outgoing = false, localInit, remoteInit)) + peerConnection3.send(peer, PeerConnection.ConnectionReady(peerConnection3.ref, remoteNodeId, fakeIPAddress, outgoing = false, localInit, remoteInit)) // peer should kill previous connection peerConnection2.expectMsg(PeerConnection.Kill(PeerConnection.KillReason.ConnectionReplaced)) channel.expectMsg(INPUT_DISCONNECTED) @@ -258,7 +257,7 @@ class PeerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Paralle // we simulate a success val dummyInit = protocol.Init(peer.underlyingActor.nodeParams.features.initFeatures()) - probe.send(peer, PeerConnection.ConnectionReady(peerConnection.ref, remoteNodeId, fakeIPAddress.socketAddress, outgoing = true, dummyInit, dummyInit)) + probe.send(peer, PeerConnection.ConnectionReady(peerConnection.ref, remoteNodeId, fakeIPAddress, outgoing = true, dummyInit, dummyInit)) // we make sure that the reconnection task has done a full circle monitor.expectMsg(FSM.Transition(reconnectionTask, ReconnectionTask.CONNECTING, ReconnectionTask.IDLE)) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/io/ReconnectionTaskSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/io/ReconnectionTaskSpec.scala index d00562c2b7..db798a2654 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/io/ReconnectionTaskSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/io/ReconnectionTaskSpec.scala @@ -38,7 +38,7 @@ class ReconnectionTaskSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike private val PeerNothingData = Peer.Nothing private val PeerDisconnectedData = Peer.DisconnectedData(channels) - private val PeerConnectedData = Peer.ConnectedData(fakeIPAddress.socketAddress, system.deadLetters, null, null, channels.map { case (k: ChannelId, v) => (k, v) }) + private val PeerConnectedData = Peer.ConnectedData(fakeIPAddress, system.deadLetters, null, null, channels.map { case (k: ChannelId, v) => (k, v) }) case class FixtureParam(nodeParams: NodeParams, remoteNodeId: PublicKey, reconnectionTask: TestFSMRef[ReconnectionTask.State, ReconnectionTask.Data, ReconnectionTask], monitor: TestProbe) @@ -101,7 +101,7 @@ class ReconnectionTaskSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike peer.send(reconnectionTask, Peer.Transition(PeerNothingData, PeerDisconnectedData)) val TransitionWithData(ReconnectionTask.IDLE, ReconnectionTask.WAITING, _, _) = monitor.expectMsgType[TransitionWithData] val TransitionWithData(ReconnectionTask.WAITING, ReconnectionTask.CONNECTING, _, connectingData: ReconnectionTask.ConnectingData) = monitor.expectMsgType[TransitionWithData] - assert(connectingData.to === fakeIPAddress.socketAddress) + assert(connectingData.to === fakeIPAddress) val expectedNextReconnectionDelayInterval = (nodeParams.maxReconnectInterval.toSeconds / 2) to nodeParams.maxReconnectInterval.toSeconds assert(expectedNextReconnectionDelayInterval contains connectingData.nextReconnectionDelay.toSeconds) // we only reconnect once } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/json/JsonSerializersSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/json/JsonSerializersSpec.scala index 5699b5ef2c..e359c19fb3 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/json/JsonSerializersSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/json/JsonSerializersSpec.scala @@ -90,13 +90,15 @@ class JsonSerializersSpec extends AnyFunSuite with Matchers { } test("NodeAddress serialization") { - val ipv4 = NodeAddress.fromParts("10.0.0.1", 8888).get - val ipv6LocalHost = NodeAddress.fromParts(InetAddress.getByAddress(Array(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1)).getHostAddress, 9735).get + val ipv4 = IPAddress(InetAddress.getByName("10.0.0.1"), 8888) + val ipv6 = IPAddress(InetAddress.getByName("[2405:204:66a9:536c:873f:dc4a:f055:a298]"), 9737) + val ipv6LocalHost = IPAddress(InetAddress.getByAddress(Array(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1)), 9735) val tor2 = Tor2("aaaqeayeaudaocaj", 7777) val tor3 = Tor3("aaaqeayeaudaocajbifqydiob4ibceqtcqkrmfyydenbwha5dypsaijc", 9999) JsonSerializers.serialization.write(ipv4)(org.json4s.DefaultFormats + NodeAddressSerializer) shouldBe s""""10.0.0.1:8888"""" - JsonSerializers.serialization.write(ipv6LocalHost)(org.json4s.DefaultFormats + NodeAddressSerializer) shouldBe s""""[0:0:0:0:0:0:0:1]:9735"""" + JsonSerializers.serialization.write(ipv6)(org.json4s.DefaultFormats + NodeAddressSerializer) shouldBe s""""[2405:204:66a9:536c:873f:dc4a:f055:a298]:9737"""" + JsonSerializers.serialization.write(ipv6LocalHost)(org.json4s.DefaultFormats + NodeAddressSerializer) shouldBe s""""[::1]:9735"""" JsonSerializers.serialization.write(tor2)(org.json4s.DefaultFormats + NodeAddressSerializer) shouldBe s""""aaaqeayeaudaocaj.onion:7777"""" JsonSerializers.serialization.write(tor3)(org.json4s.DefaultFormats + NodeAddressSerializer) shouldBe s""""aaaqeayeaudaocajbifqydiob4ibceqtcqkrmfyydenbwha5dypsaijc.onion:9999"""" } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/tor/Socks5ConnectionSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/tor/Socks5ConnectionSpec.scala index a89ee1d21f..a35cfb2b1f 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/tor/Socks5ConnectionSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/tor/Socks5ConnectionSpec.scala @@ -16,8 +16,9 @@ package fr.acinq.eclair.tor -import java.net.InetSocketAddress +import fr.acinq.eclair.wire.protocol.NodeAddress +import java.net.InetSocketAddress import org.scalatest.funsuite.AnyFunSuite /** @@ -30,27 +31,27 @@ class Socks5ConnectionSpec extends AnyFunSuite { val proxyAddress = new InetSocketAddress(9050) assert(Socks5ProxyParams.proxyAddress( - socketAddress = new InetSocketAddress("1.2.3.4", 9735), + address = NodeAddress.fromParts("1.2.3.4", 9735).get, proxyParams = Socks5ProxyParams(address = proxyAddress, credentials_opt = None, randomizeCredentials = false, useForIPv4 = true, useForIPv6 = true, useForTor = true, useForWatchdogs = true)).contains(proxyAddress)) assert(Socks5ProxyParams.proxyAddress( - socketAddress = new InetSocketAddress("1.2.3.4", 9735), + address = NodeAddress.fromParts("1.2.3.4", 9735).get, proxyParams = Socks5ProxyParams(address = proxyAddress, credentials_opt = None, randomizeCredentials = false, useForIPv4 = false, useForIPv6 = true, useForTor = true, useForWatchdogs = true)).isEmpty) assert(Socks5ProxyParams.proxyAddress( - socketAddress = new InetSocketAddress("[fc92:97a3:e057:b290:abd8:9bd6:135d:7e7]", 9735), + address = NodeAddress.fromParts("[fc92:97a3:e057:b290:abd8:9bd6:135d:7e7]", 9735).get, proxyParams = Socks5ProxyParams(address = proxyAddress, credentials_opt = None, randomizeCredentials = false, useForIPv4 = true, useForIPv6 = true, useForTor = true, useForWatchdogs = true)).contains(proxyAddress)) assert(Socks5ProxyParams.proxyAddress( - socketAddress = new InetSocketAddress("[fc92:97a3:e057:b290:abd8:9bd6:135d:7e7]", 9735), + address = NodeAddress.fromParts("[fc92:97a3:e057:b290:abd8:9bd6:135d:7e7]", 9735).get, proxyParams = Socks5ProxyParams(address = proxyAddress, credentials_opt = None, randomizeCredentials = false, useForIPv4 = true, useForIPv6 = false, useForTor = true, useForWatchdogs = true)).isEmpty) assert(Socks5ProxyParams.proxyAddress( - socketAddress = new InetSocketAddress("iq7zhmhck54vcax2vlrdcavq2m32wao7ekh6jyeglmnuuvv3js57r4id.onion", 9735), + address = NodeAddress.fromParts("iq7zhmhck54vcax2vlrdcavq2m32wao7ekh6jyeglmnuuvv3js57r4id.onion", 9735).get, proxyParams = Socks5ProxyParams(address = proxyAddress, credentials_opt = None, randomizeCredentials = false, useForIPv4 = true, useForIPv6 = true, useForTor = true, useForWatchdogs = true)).contains(proxyAddress)) assert(Socks5ProxyParams.proxyAddress( - socketAddress = new InetSocketAddress("iq7zhmhck54vcax2vlrdcavq2m32wao7ekh6jyeglmnuuvv3js57r4id.onion", 9735), + address = NodeAddress.fromParts("iq7zhmhck54vcax2vlrdcavq2m32wao7ekh6jyeglmnuuvv3js57r4id.onion", 9735).get, proxyParams = Socks5ProxyParams(address = proxyAddress, credentials_opt = None, randomizeCredentials = false, useForIPv4 = true, useForIPv6 = true, useForTor = false, useForWatchdogs = true)).isEmpty) } diff --git a/eclair-front/src/main/scala/fr/acinq/eclair/FrontSetup.scala b/eclair-front/src/main/scala/fr/acinq/eclair/FrontSetup.scala index 23736eddd2..3b43e3604f 100644 --- a/eclair-front/src/main/scala/fr/acinq/eclair/FrontSetup.scala +++ b/eclair-front/src/main/scala/fr/acinq/eclair/FrontSetup.scala @@ -85,6 +85,8 @@ class FrontSetup(datadir: File)(implicit system: ActorSystem) extends Logging { config.getString("server.binding-ip"), config.getInt("server.port")) + val socks5ProxyParams_opt = NodeParams.parseSocks5ProxyParams(config) + def bootstrap: Future[Unit] = { val frontJoinedCluster = Promise[Done]() @@ -112,7 +114,7 @@ class FrontSetup(datadir: File)(implicit system: ActorSystem) extends Logging { frontRouter = system.actorOf(SimpleSupervisor.props(FrontRouter.props(routerConf, remoteRouter, Some(frontRouterInitialized)), "front-router", SupervisorStrategy.Resume)) _ <- frontRouterInitialized.future - clientSpawner = system.actorOf(Props(new ClientSpawner(keyPair, None, peerConnectionConf, remoteSwitchboard, frontRouter)), name = "client-spawner") + clientSpawner = system.actorOf(Props(new ClientSpawner(keyPair, socks5ProxyParams_opt, peerConnectionConf, remoteSwitchboard, frontRouter)), name = "client-spawner") server = system.actorOf(SimpleSupervisor.props(Server.props(keyPair, peerConnectionConf, remoteSwitchboard, frontRouter, serverBindingAddress, Some(tcpBound)), "server", SupervisorStrategy.Restart)) } yield () diff --git a/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Node.scala b/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Node.scala index da5cf1a23c..28cf82ed24 100644 --- a/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Node.scala +++ b/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Node.scala @@ -17,11 +17,11 @@ package fr.acinq.eclair.api.handlers import akka.http.scaladsl.server.Route -import com.google.common.net.HostAndPort import fr.acinq.eclair.api.Service import fr.acinq.eclair.api.directives.EclairDirectives import fr.acinq.eclair.api.serde.FormParamExtractors._ import fr.acinq.eclair.io.NodeURI +import fr.acinq.eclair.wire.protocol.NodeAddress trait Node { this: Service with EclairDirectives => @@ -38,7 +38,7 @@ trait Node { } ~ formFields(nodeIdFormParam, "host".as[String], "port".as[Int].?) { (nodeId, host, port_opt) => complete { eclairApi.connect( - Left(NodeURI(nodeId, HostAndPort.fromParts(host, port_opt.getOrElse(NodeURI.DEFAULT_PORT)))) + Left(NodeURI(nodeId, NodeAddress.fromParts(host, port_opt.getOrElse(NodeURI.DEFAULT_PORT)).get)) ) } } ~ formFields(nodeIdFormParam) { nodeId => diff --git a/eclair-node/src/test/resources/api/getinfo b/eclair-node/src/test/resources/api/getinfo index f0264a9768..c6a823114e 100644 --- a/eclair-node/src/test/resources/api/getinfo +++ b/eclair-node/src/test/resources/api/getinfo @@ -1 +1 @@ -{"version":"1.0.0-SNAPSHOT-e3f1ec0","nodeId":"03af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d0","alias":"alice","color":"#000102","features":{"activated":{"option_data_loss_protect":"mandatory","gossip_queries_ex":"optional"},"unknown":[]},"chainHash":"06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f","network":"regtest","blockHeight":9999,"publicAddresses":["localhost:9731"],"instanceId":"01234567-0123-4567-89ab-0123456789ab"} \ No newline at end of file +{"version":"1.0.0-SNAPSHOT-e3f1ec0","nodeId":"03af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d0","alias":"alice","color":"#000102","features":{"activated":{"option_data_loss_protect":"mandatory","gossip_queries_ex":"optional"},"unknown":[]},"chainHash":"06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f","network":"regtest","blockHeight":9999,"publicAddresses":["127.0.0.1:9731"],"instanceId":"01234567-0123-4567-89ab-0123456789ab"} \ No newline at end of file diff --git a/eclair-node/src/test/resources/api/peers b/eclair-node/src/test/resources/api/peers index 3e12eddaa8..5fd9b2a1b8 100644 --- a/eclair-node/src/test/resources/api/peers +++ b/eclair-node/src/test/resources/api/peers @@ -1 +1 @@ -[{"nodeId":"03af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d0","state":"CONNECTED","address":"localhost:9731","channels":1},{"nodeId":"039dc0e0b1d25905e44fdf6f8e89755a5e219685840d0bc1d28d3308f9628a3585","state":"DISCONNECTED","channels":1}] \ No newline at end of file +[{"nodeId":"03af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d0","state":"CONNECTED","address":"127.0.0.1:9731","channels":1},{"nodeId":"039dc0e0b1d25905e44fdf6f8e89755a5e219685840d0bc1d28d3308f9628a3585","state":"DISCONNECTED","channels":1}] \ No newline at end of file diff --git a/eclair-node/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala b/eclair-node/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala index 8b02196a9f..a7e7002921 100644 --- a/eclair-node/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala +++ b/eclair-node/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala @@ -167,7 +167,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM ActorRef.noSender, nodeId = aliceNodeId, state = Peer.CONNECTED, - address = Some(NodeAddress.fromParts("localhost", 9731).get.socketAddress), + address = Some(NodeAddress.fromParts("localhost", 9731).get), channels = 1), PeerInfo( ActorRef.noSender, diff --git a/pom.xml b/pom.xml index 72af6be938..5c8aeeb79d 100644 --- a/pom.xml +++ b/pom.xml @@ -73,7 +73,7 @@ 10.2.7 3.4.1 0.19 - 24.0-android + 31.1-jre 2.4.6 From 0e88440e6a215ce33da019b60cbb714131fbdbff Mon Sep 17 00:00:00 2001 From: Thomas HUET <81159533+thomash-acinq@users.noreply.github.com> Date: Mon, 28 Mar 2022 10:25:04 +0200 Subject: [PATCH 042/121] Make trampoline pay the right fee (#2206) When using multipart payments, the path-finding request sent to the router does not match the actual payment, it is only a request to route a fraction of the amount but the fee budget is still the one for the full payment. As a consequence the router will return routes that will not satisfy the fee budget. The fee budget is checked one last time before relaying the payment but ignoring the fee of the first channel (which should not be ignored for trampoline relays). I fix this last check. --- .../send/MultiPartPaymentLifecycle.scala | 12 +++---- .../eclair/router/RouteCalculation.scala | 6 ++-- .../scala/fr/acinq/eclair/router/Router.scala | 6 ++-- .../integration/PaymentIntegrationSpec.scala | 32 ++++++++++++------- .../MultiPartPaymentLifecycleSpec.scala | 16 +++++----- .../eclair/router/RouteCalculationSpec.scala | 18 +++++++++-- 6 files changed, 57 insertions(+), 33 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/MultiPartPaymentLifecycle.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/MultiPartPaymentLifecycle.scala index f16f8309b6..d591dae7a2 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/MultiPartPaymentLifecycle.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/MultiPartPaymentLifecycle.scala @@ -72,7 +72,7 @@ class MultiPartPaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, case Event(RouteResponse(routes), d: PaymentProgress) => log.info("{} routes found (attempt={}/{})", routes.length, d.request.maxAttempts - d.remainingAttempts + 1, d.request.maxAttempts) // We may have already succeeded sending parts of the payment and only need to take care of the rest. - val (toSend, maxFee) = remainingToSend(d.request, d.pending.values) + val (toSend, maxFee) = remainingToSend(d.request, d.pending.values, d.request.routeParams.includeLocalChannelCost) if (routes.map(_.amount).sum == toSend) { val childPayments = routes.map(route => (UUID.randomUUID(), route)).toMap childPayments.foreach { case (childId, route) => spawnChildPaymentFsm(childId) ! createChildPayment(self, route, d.request) } @@ -92,7 +92,7 @@ class MultiPartPaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, // Channels are mostly ignored for temporary reasons, likely because they didn't have enough balance to forward // the payment. When we're retrying an MPP split, it may make sense to retry those ignored channels because with // a different split, they may have enough balance to forward the payment. - val (toSend, maxFee) = remainingToSend(d.request, d.pending.values) + val (toSend, maxFee) = remainingToSend(d.request, d.pending.values, d.request.routeParams.includeLocalChannelCost) if (d.ignore.channels.nonEmpty) { log.debug("retry sending {} with maximum fee {} without ignoring channels ({})", toSend, maxFee, d.ignore.channels.map(_.shortChannelId).mkString(",")) val routeParams = d.request.routeParams.copy(randomize = true) // we randomize route selection when we retry @@ -141,7 +141,7 @@ class MultiPartPaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, val ignore1 = PaymentFailure.updateIgnored(pf.failures, d.ignore) val assistedRoutes1 = PaymentFailure.updateRoutingHints(pf.failures, d.request.assistedRoutes) val stillPending = d.pending - pf.id - val (toSend, maxFee) = remainingToSend(d.request, stillPending.values) + val (toSend, maxFee) = remainingToSend(d.request, stillPending.values, d.request.routeParams.includeLocalChannelCost) log.debug("child payment failed, retry sending {} with maximum fee {}", toSend, maxFee) val routeParams = d.request.routeParams.copy(randomize = true) // we randomize route selection when we retry val d1 = d.copy(pending = stillPending, ignore = ignore1, failures = d.failures ++ pf.failures, request = d.request.copy(assistedRoutes = assistedRoutes1)) @@ -416,10 +416,10 @@ object MultiPartPaymentLifecycle { case _ => false } - private def remainingToSend(request: SendMultiPartPayment, pending: Iterable[Route]): (MilliSatoshi, MilliSatoshi) = { + private def remainingToSend(request: SendMultiPartPayment, pending: Iterable[Route], includeLocalChannelCost: Boolean): (MilliSatoshi, MilliSatoshi) = { val sentAmount = pending.map(_.amount).sum - val sentFees = pending.map(_.fee).sum - (request.totalAmount - sentAmount, request.routeParams.copy(randomize = false).getMaxFee(request.totalAmount) - sentFees) + val sentFees = pending.map(_.fee(includeLocalChannelCost)).sum + (request.totalAmount - sentAmount, request.routeParams.getMaxFee(request.totalAmount) - sentFees) } } \ No newline at end of file diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/RouteCalculation.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/RouteCalculation.scala index 88537387b7..7c6b973fdf 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/RouteCalculation.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/RouteCalculation.scala @@ -348,7 +348,7 @@ object RouteCalculation { case Right(routes) => // We use these shortest paths to find a set of non-conflicting HTLCs that send the total amount. split(amount, mutable.Queue(routes: _*), initializeUsedCapacity(pendingHtlcs), routeParams1) match { - case Right(routes) if validateMultiPartRoute(amount, maxFee, routes) => Right(routes) + case Right(routes) if validateMultiPartRoute(amount, maxFee, routes, routeParams.includeLocalChannelCost) => Right(routes) case _ => Left(RouteNotFound) } case Left(ex) => Left(ex) @@ -416,9 +416,9 @@ object RouteCalculation { } } - private def validateMultiPartRoute(amount: MilliSatoshi, maxFee: MilliSatoshi, routes: Seq[Route]): Boolean = { + private def validateMultiPartRoute(amount: MilliSatoshi, maxFee: MilliSatoshi, routes: Seq[Route], includeLocalChannelCost: Boolean): Boolean = { val amountOk = routes.map(_.amount).sum == amount - val feeOk = routes.map(_.fee).sum <= maxFee + val feeOk = routes.map(_.fee(includeLocalChannelCost)).sum <= maxFee amountOk && feeOk } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala index c5b2695da3..5bc3006f48 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala @@ -473,8 +473,10 @@ object Router { require(hops.nonEmpty, "route cannot be empty") val length = hops.length - lazy val fee: MilliSatoshi = { - val amountToSend = hops.drop(1).reverse.foldLeft(amount) { case (amount1, hop) => amount1 + hop.fee(amount1) } + + def fee(includeLocalChannelCost: Boolean): MilliSatoshi = { + val hopsToPay = if (includeLocalChannelCost) hops else hops.drop(1) + val amountToSend = hopsToPay.reverse.foldLeft(amount) { case (amount1, hop) => amount1 + hop.fee(amount1) } amountToSend - amount } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/PaymentIntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/PaymentIntegrationSpec.scala index c351c4153e..8dded7ef29 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/PaymentIntegrationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/PaymentIntegrationSpec.scala @@ -73,6 +73,8 @@ class PaymentIntegrationSpec extends IntegrationSpec { // A---B ------- C ==== D // \ / \ // '--E--' '--F + // + // All channels have fees 1 sat + 200 millionths, except for G that have fees 1010 msat + 102 millionths val sender = TestProbe() val eventListener = TestProbe() @@ -465,8 +467,10 @@ class PaymentIntegrationSpec extends IntegrationSpec { assert(invoice.features.hasFeature(Features.BasicMultiPartPayment)) assert(invoice.features.hasFeature(Features.TrampolinePayment)) + // The best route from G is G -> C -> F which has a fee of 1210091 msat + // The first attempt should fail, but the second one should succeed. - val attempts = (1000 msat, CltvExpiryDelta(42)) :: (1000000 msat, CltvExpiryDelta(288)) :: Nil + val attempts = (1210000 msat, CltvExpiryDelta(42)) :: (1210100 msat, CltvExpiryDelta(288)) :: Nil val payment = SendTrampolinePayment(amount, invoice, nodes("G").nodeParams.nodeId, attempts, routeParams = integrationTestRouteParams) sender.send(nodes("B").paymentInitiator, payment) val paymentId = sender.expectMsgType[UUID] @@ -475,7 +479,7 @@ class PaymentIntegrationSpec extends IntegrationSpec { assert(paymentSent.paymentHash === invoice.paymentHash, paymentSent) assert(paymentSent.recipientNodeId === nodes("F").nodeParams.nodeId, paymentSent) assert(paymentSent.recipientAmount === amount, paymentSent) - assert(paymentSent.feesPaid === 1000000.msat, paymentSent) + assert(paymentSent.feesPaid === 1210100.msat, paymentSent) assert(paymentSent.nonTrampolineFees === 0.msat, paymentSent) awaitCond(nodes("F").nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).exists(_.status.isInstanceOf[IncomingPaymentStatus.Received])) @@ -488,14 +492,14 @@ class PaymentIntegrationSpec extends IntegrationSpec { }) val relayed = nodes("G").nodeParams.db.audit.listRelayed(start, TimestampMilli.now()).filter(_.paymentHash == invoice.paymentHash).head assert(relayed.amountIn - relayed.amountOut > 0.msat, relayed) - assert(relayed.amountIn - relayed.amountOut < 1000000.msat, relayed) + assert(relayed.amountIn - relayed.amountOut < 1210100.msat, relayed) val outgoingSuccess = nodes("B").nodeParams.db.payments.listOutgoingPayments(paymentId).filter(p => p.status.isInstanceOf[OutgoingPaymentStatus.Succeeded]) outgoingSuccess.collect { case p@OutgoingPayment(_, _, _, _, _, _, _, recipientNodeId, _, _, OutgoingPaymentStatus.Succeeded(_, _, route, _)) => assert(recipientNodeId === nodes("F").nodeParams.nodeId, p) assert(route.lastOption === Some(HopSummary(nodes("G").nodeParams.nodeId, nodes("F").nodeParams.nodeId)), p) } - assert(outgoingSuccess.map(_.amount).sum === amount + 1000000.msat, outgoingSuccess) + assert(outgoingSuccess.map(_.amount).sum === amount + 1210100.msat, outgoingSuccess) } test("send a trampoline payment D->B (via trampoline C)") { @@ -509,14 +513,18 @@ class PaymentIntegrationSpec extends IntegrationSpec { assert(invoice.features.hasFeature(Features.TrampolinePayment)) assert(invoice.paymentMetadata.nonEmpty) - val payment = SendTrampolinePayment(amount, invoice, nodes("C").nodeParams.nodeId, Seq((350000 msat, CltvExpiryDelta(288))), routeParams = integrationTestRouteParams) + // The direct route C -> B does not have enough capacity, the payment will be split between + // C -> B which would have a fee of 501000 if it could route the whole payment + // C -> G -> B which would have a fee of 757061 if it was used to route the whole payment + // The actual fee needed will be between these two values. + val payment = SendTrampolinePayment(amount, invoice, nodes("C").nodeParams.nodeId, Seq((750000 msat, CltvExpiryDelta(288))), routeParams = integrationTestRouteParams) sender.send(nodes("D").paymentInitiator, payment) val paymentId = sender.expectMsgType[UUID] val paymentSent = sender.expectMsgType[PaymentSent](max = 30 seconds) assert(paymentSent.id === paymentId, paymentSent) assert(paymentSent.paymentHash === invoice.paymentHash, paymentSent) assert(paymentSent.recipientAmount === amount, paymentSent) - assert(paymentSent.feesPaid === 350000.msat, paymentSent) + assert(paymentSent.feesPaid === 750000.msat, paymentSent) assert(paymentSent.nonTrampolineFees === 0.msat, paymentSent) awaitCond(nodes("B").nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).exists(_.status.isInstanceOf[IncomingPaymentStatus.Received])) @@ -530,14 +538,14 @@ class PaymentIntegrationSpec extends IntegrationSpec { }) val relayed = nodes("C").nodeParams.db.audit.listRelayed(start, TimestampMilli.now()).filter(_.paymentHash == invoice.paymentHash).head assert(relayed.amountIn - relayed.amountOut > 0.msat, relayed) - assert(relayed.amountIn - relayed.amountOut < 350000.msat, relayed) + assert(relayed.amountIn - relayed.amountOut < 750000.msat, relayed) val outgoingSuccess = nodes("D").nodeParams.db.payments.listOutgoingPayments(paymentId).filter(p => p.status.isInstanceOf[OutgoingPaymentStatus.Succeeded]) outgoingSuccess.collect { case p@OutgoingPayment(_, _, _, _, _, _, _, recipientNodeId, _, _, OutgoingPaymentStatus.Succeeded(_, _, route, _)) => assert(recipientNodeId === nodes("B").nodeParams.nodeId, p) assert(route.lastOption === Some(HopSummary(nodes("C").nodeParams.nodeId, nodes("B").nodeParams.nodeId)), p) } - assert(outgoingSuccess.map(_.amount).sum === amount + 350000.msat, outgoingSuccess) + assert(outgoingSuccess.map(_.amount).sum === amount + 750000.msat, outgoingSuccess) awaitCond(nodes("D").nodeParams.db.audit.listSent(start, TimestampMilli.now()).nonEmpty) val sent = nodes("D").nodeParams.db.audit.listSent(start, TimestampMilli.now()) @@ -561,14 +569,14 @@ class PaymentIntegrationSpec extends IntegrationSpec { assert(!invoice.features.hasFeature(Features.TrampolinePayment)) assert(invoice.paymentMetadata.nonEmpty) - val payment = SendTrampolinePayment(amount, invoice, nodes("C").nodeParams.nodeId, Seq((1000000 msat, CltvExpiryDelta(432))), routeParams = integrationTestRouteParams) + val payment = SendTrampolinePayment(amount, invoice, nodes("C").nodeParams.nodeId, Seq((1500000 msat, CltvExpiryDelta(432))), routeParams = integrationTestRouteParams) sender.send(nodes("F").paymentInitiator, payment) val paymentId = sender.expectMsgType[UUID] val paymentSent = sender.expectMsgType[PaymentSent](max = 30 seconds) assert(paymentSent.id === paymentId, paymentSent) assert(paymentSent.paymentHash === invoice.paymentHash, paymentSent) assert(paymentSent.recipientAmount === amount, paymentSent) - assert(paymentSent.trampolineFees === 1000000.msat, paymentSent) + assert(paymentSent.trampolineFees === 1500000.msat, paymentSent) awaitCond(nodes("A").nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).exists(_.status.isInstanceOf[IncomingPaymentStatus.Received])) val Some(IncomingPayment(_, _, _, _, IncomingPaymentStatus.Received(receivedAmount, _))) = nodes("A").nodeParams.db.payments.getIncomingPayment(invoice.paymentHash) @@ -581,14 +589,14 @@ class PaymentIntegrationSpec extends IntegrationSpec { }) val relayed = nodes("C").nodeParams.db.audit.listRelayed(start, TimestampMilli.now()).filter(_.paymentHash == invoice.paymentHash).head assert(relayed.amountIn - relayed.amountOut > 0.msat, relayed) - assert(relayed.amountIn - relayed.amountOut < 1000000.msat, relayed) + assert(relayed.amountIn - relayed.amountOut < 1500000.msat, relayed) val outgoingSuccess = nodes("F").nodeParams.db.payments.listOutgoingPayments(paymentId).filter(p => p.status.isInstanceOf[OutgoingPaymentStatus.Succeeded]) outgoingSuccess.collect { case p@OutgoingPayment(_, _, _, _, _, _, _, recipientNodeId, _, _, OutgoingPaymentStatus.Succeeded(_, _, route, _)) => assert(recipientNodeId === nodes("A").nodeParams.nodeId, p) assert(route.lastOption === Some(HopSummary(nodes("C").nodeParams.nodeId, nodes("A").nodeParams.nodeId)), p) } - assert(outgoingSuccess.map(_.amount).sum === amount + 1000000.msat, outgoingSuccess) + assert(outgoingSuccess.map(_.amount).sum === amount + 1500000.msat, outgoingSuccess) } test("send a trampoline payment B->D (temporary local failure at trampoline)") { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartPaymentLifecycleSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartPaymentLifecycleSpec.scala index af0348de9f..3b706db01d 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartPaymentLifecycleSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartPaymentLifecycleSpec.scala @@ -219,7 +219,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS childPayFsm.send(payFsm, PaymentFailed(failedId1, paymentHash, Seq(RemoteFailure(failedRoute1.amount, failedRoute1.hops, Sphinx.DecryptedFailurePacket(b, TemporaryNodeFailure))))) // When we retry, we ignore the failing node and we let the router know about the remaining pending route. - router.expectMsg(RouteRequest(nodeParams.nodeId, e, failedRoute1.amount, maxFee - failedRoute1.fee, ignore = Ignore(Set(b), Set.empty), pendingPayments = Seq(failedRoute2), allowMultiPart = true, routeParams = routeParams.copy(randomize = true), paymentContext = Some(cfg.paymentContext))) + router.expectMsg(RouteRequest(nodeParams.nodeId, e, failedRoute1.amount, maxFee - failedRoute1.fee(false), ignore = Ignore(Set(b), Set.empty), pendingPayments = Seq(failedRoute2), allowMultiPart = true, routeParams = routeParams.copy(randomize = true), paymentContext = Some(cfg.paymentContext))) // The second part fails while we're still waiting for new routes. childPayFsm.send(payFsm, PaymentFailed(failedId2, paymentHash, Seq(RemoteFailure(failedRoute2.amount, failedRoute2.hops, Sphinx.DecryptedFailurePacket(b, TemporaryNodeFailure))))) // We receive a response to our first request, but it's now obsolete: we re-sent a new route request that takes into @@ -289,7 +289,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS // of the amount to send. val expectedRouteRequest = RouteRequest( nodeParams.nodeId, e, - failedRoute.amount, maxFee - failedRoute.fee, + failedRoute.amount, maxFee - failedRoute.fee(false), ignore = Ignore(Set.empty, Set(ChannelDesc(channelId_ab_1, a, b))), pendingPayments = Seq(pendingRoute), allowMultiPart = true, @@ -525,7 +525,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS val result = fulfillPendingPayments(f, 1) assert(result.amountWithFees < finalAmount) // we got the preimage without paying the full amount - assert(result.nonTrampolineFees === successRoute.fee) // we paid the fee for only one of the partial payments + assert(result.nonTrampolineFees === successRoute.fee(false)) // we paid the fee for only one of the partial payments assert(result.parts.length === 1 && result.parts.head.id === successId) } @@ -544,7 +544,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS awaitCond(payFsm.stateName === PAYMENT_ABORTED) sender.watch(payFsm) - childPayFsm.send(payFsm, PaymentSent(cfg.id, paymentHash, paymentPreimage, finalAmount, e, Seq(PaymentSent.PartialPayment(successId, successRoute.amount, successRoute.fee, randomBytes32(), Some(successRoute.hops))))) + childPayFsm.send(payFsm, PaymentSent(cfg.id, paymentHash, paymentPreimage, finalAmount, e, Seq(PaymentSent.PartialPayment(successId, successRoute.amount, successRoute.fee(false), randomBytes32(), Some(successRoute.hops))))) sender.expectMsg(PreimageReceived(paymentHash, paymentPreimage)) val result = sender.expectMsgType[PaymentSent] assert(result.id === cfg.id) @@ -554,7 +554,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS assert(result.recipientAmount === finalAmount) assert(result.recipientNodeId === finalRecipient) assert(result.amountWithFees < finalAmount) // we got the preimage without paying the full amount - assert(result.nonTrampolineFees === successRoute.fee) // we paid the fee for only one of the partial payments + assert(result.nonTrampolineFees === successRoute.fee(false)) // we paid the fee for only one of the partial payments sender.expectTerminated(payFsm) sender.expectNoMessage(100 millis) @@ -573,7 +573,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS childPayFsm.expectMsgType[SendPaymentToRoute] val (childId, route) :: (failedId, failedRoute) :: Nil = payFsm.stateData.asInstanceOf[PaymentProgress].pending.toSeq - childPayFsm.send(payFsm, PaymentSent(cfg.id, paymentHash, paymentPreimage, finalAmount, e, Seq(PaymentSent.PartialPayment(childId, route.amount, route.fee, randomBytes32(), Some(route.hops))))) + childPayFsm.send(payFsm, PaymentSent(cfg.id, paymentHash, paymentPreimage, finalAmount, e, Seq(PaymentSent.PartialPayment(childId, route.amount, route.fee(false), randomBytes32(), Some(route.hops))))) sender.expectMsg(PreimageReceived(paymentHash, paymentPreimage)) awaitCond(payFsm.stateName === PAYMENT_SUCCEEDED) @@ -582,7 +582,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS val result = sender.expectMsgType[PaymentSent] assert(result.parts.length === 1 && result.parts.head.id === childId) assert(result.amountWithFees < finalAmount) // we got the preimage without paying the full amount - assert(result.nonTrampolineFees === route.fee) // we paid the fee for only one of the partial payments + assert(result.nonTrampolineFees === route.fee(false)) // we paid the fee for only one of the partial payments sender.expectTerminated(payFsm) sender.expectNoMessage(100 millis) @@ -598,7 +598,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS assert(pending.size === childCount) val partialPayments = pending.map { - case (childId, route) => PaymentSent.PartialPayment(childId, route.amount, route.fee, randomBytes32(), Some(route.hops)) + case (childId, route) => PaymentSent.PartialPayment(childId, route.amount, route.fee(false), randomBytes32(), Some(route.hops)) } partialPayments.foreach(pp => childPayFsm.send(payFsm, PaymentSent(cfg.id, paymentHash, paymentPreimage, finalAmount, e, Seq(pp)))) sender.expectMsg(PreimageReceived(paymentHash, paymentPreimage)) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/RouteCalculationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/RouteCalculationSpec.scala index cf88cbc1e4..8e68a86e20 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/RouteCalculationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/RouteCalculationSpec.scala @@ -1725,7 +1725,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { val Success(routes) = findRoute(g, a, b, DEFAULT_AMOUNT_MSAT, 100000000 msat, numRoutes = 10, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(routes.distinct.length == 10) - val fees = routes.map(_.fee) + val fees = routes.map(_.fee(false)) assert(fees.forall(_ == fees.head)) } @@ -1870,6 +1870,20 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { val route :: Nil = routes assert(route2Ids(route) === recentChannelId :: Nil) } + + test("trampoline relay with direct channel to target") { + val amount = 100_000_000 msat + val g = DirectedGraph(List(makeEdge(1L, a, b, 1000 msat, 1000, capacity = 100_000_000 sat))) + + { + val routeParams = DEFAULT_ROUTE_PARAMS.copy(includeLocalChannelCost = true, boundaries = SearchBoundaries(100_999 msat, 0.0, 6, CltvExpiryDelta(576))) + assert(findMultiPartRoute(g, a, b, amount, 100_999 msat, Set.empty, Set.empty, Set.empty, Nil, routeParams = routeParams, currentBlockHeight = BlockHeight(400000)) === Failure(RouteNotFound)) + } + { + val routeParams = DEFAULT_ROUTE_PARAMS.copy(includeLocalChannelCost = true, boundaries = SearchBoundaries(101_000 msat, 0.0, 6, CltvExpiryDelta(576))) + assert(findMultiPartRoute(g, a, b, amount, 101_000 msat, Set.empty, Set.empty, Set.empty, Nil, routeParams = routeParams, currentBlockHeight = BlockHeight(400000)).isSuccess) + } + } } object RouteCalculationSpec { @@ -1942,7 +1956,7 @@ object RouteCalculationSpec { def checkRouteAmounts(routes: Seq[Route], totalAmount: MilliSatoshi, maxFee: MilliSatoshi): Unit = { assert(routes.map(_.amount).sum == totalAmount, routes) - assert(routes.map(_.fee).sum <= maxFee, routes) + assert(routes.map(_.fee(false)).sum <= maxFee, routes) } } From d13522480598be29cffe99b6ff7b453bb9403a3b Mon Sep 17 00:00:00 2001 From: Bastien Teinturier <31281497+t-bast@users.noreply.github.com> Date: Mon, 28 Mar 2022 17:00:10 +0200 Subject: [PATCH 043/121] Refactor channel into separate traits (#2217) Refactor the channel state machine into several separate traits. This lets us split its logic into multiple files while keeping a single actor. It also lets us isolate some parts of the state machines as if they were small state machines: we apply this to the channel opening flow which will let us add a new flow for dual funding. This can also be applied to the closing mechanism in the future. --- .../scala/fr/acinq/eclair/NodeParams.scala | 5 +- .../main/scala/fr/acinq/eclair/Setup.scala | 3 +- .../fr/acinq/eclair/channel/Commitments.scala | 1 + .../fr/acinq/eclair/channel/Helpers.scala | 3 +- .../eclair/channel/{ => fsm}/Channel.scala | 843 +----------------- .../channel/fsm/ChannelOpenSingleFunder.scala | 401 +++++++++ .../eclair/channel/fsm/CommonHandlers.scala | 102 +++ .../eclair/channel/fsm/ErrorHandlers.scala | 362 ++++++++ .../eclair/channel/fsm/FundingHandlers.scala | 123 +++ .../main/scala/fr/acinq/eclair/io/Peer.scala | 1 + .../payment/send/PaymentInitiator.scala | 2 +- .../scala/fr/acinq/eclair/TestConstants.scala | 2 +- .../scala/fr/acinq/eclair/TestUtils.scala | 2 +- .../eclair/channel/ChannelDataSpec.scala | 1 + .../fr/acinq/eclair/channel/FuzzySpec.scala | 1 + .../fr/acinq/eclair/channel/HelpersSpec.scala | 1 + .../fr/acinq/eclair/channel/RestoreSpec.scala | 1 + .../publish/ReplaceableTxPublisherSpec.scala | 1 + .../ChannelStateTestsHelperMethods.scala | 1 + .../a/WaitForAcceptChannelStateSpec.scala | 3 +- .../a/WaitForOpenChannelStateSpec.scala | 1 + .../b/WaitForFundingCreatedStateSpec.scala | 1 + .../b/WaitForFundingInternalStateSpec.scala | 3 +- .../b/WaitForFundingSignedStateSpec.scala | 3 +- .../c/WaitForFundingConfirmedStateSpec.scala | 3 +- .../c/WaitForFundingLockedStateSpec.scala | 1 + .../channel/states/e/NormalStateSpec.scala | 3 +- .../channel/states/e/OfflineStateSpec.scala | 1 + .../states/g/NegotiatingStateSpec.scala | 1 + .../channel/states/h/ClosingStateSpec.scala | 3 +- .../integration/PaymentIntegrationSpec.scala | 2 +- .../PerformanceIntegrationSpec.scala | 2 +- .../interop/rustytests/RustyTestsSpec.scala | 1 + .../scala/fr/acinq/eclair/io/PeerSpec.scala | 1 + .../eclair/payment/PaymentInitiatorSpec.scala | 2 +- .../eclair/payment/PaymentLifecycleSpec.scala | 1 + .../eclair/payment/PaymentPacketSpec.scala | 1 + .../internal/channel/ChannelCodecsSpec.scala | 1 + 38 files changed, 1051 insertions(+), 839 deletions(-) rename eclair-core/src/main/scala/fr/acinq/eclair/channel/{ => fsm}/Channel.scala (68%) create mode 100644 eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenSingleFunder.scala create mode 100644 eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/CommonHandlers.scala create mode 100644 eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ErrorHandlers.scala create mode 100644 eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/FundingHandlers.scala diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala index 0ec0fabc3b..30793c2283 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala @@ -21,8 +21,9 @@ import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.bitcoin.{Block, ByteVector32, Crypto, Satoshi} import fr.acinq.eclair.Setup.Seeds import fr.acinq.eclair.blockchain.fee._ -import fr.acinq.eclair.channel.Channel.{ChannelConf, UnhandledExceptionStrategy} -import fr.acinq.eclair.channel.{Channel, ChannelFlags} +import fr.acinq.eclair.channel.ChannelFlags +import fr.acinq.eclair.channel.fsm.Channel +import fr.acinq.eclair.channel.fsm.Channel.{ChannelConf, UnhandledExceptionStrategy} import fr.acinq.eclair.crypto.Noise.KeyPair import fr.acinq.eclair.crypto.keymanager.{ChannelKeyManager, NodeKeyManager} import fr.acinq.eclair.db._ diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala index 1f49f6fa67..6e331040f0 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala @@ -30,7 +30,8 @@ import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher import fr.acinq.eclair.blockchain.bitcoind.rpc.{BasicBitcoinJsonRPCClient, BatchingBitcoinJsonRPCClient, BitcoinCoreClient, BitcoinJsonRPCAuthMethod} import fr.acinq.eclair.blockchain.bitcoind.zmq.ZMQActor import fr.acinq.eclair.blockchain.fee._ -import fr.acinq.eclair.channel.{Channel, Register} +import fr.acinq.eclair.channel.Register +import fr.acinq.eclair.channel.fsm.Channel import fr.acinq.eclair.crypto.WeakEntropyPool import fr.acinq.eclair.crypto.keymanager.{LocalChannelKeyManager, LocalNodeKeyManager} import fr.acinq.eclair.db.Databases.FileBackup diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala index 1b49b9a18e..6bfdf2def5 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala @@ -23,6 +23,7 @@ import fr.acinq.eclair._ import fr.acinq.eclair.blockchain.fee.{FeeratePerKw, OnChainFeeConf} import fr.acinq.eclair.channel.Helpers.Closing import fr.acinq.eclair.channel.Monitoring.Metrics +import fr.acinq.eclair.channel.fsm.Channel import fr.acinq.eclair.crypto.keymanager.ChannelKeyManager import fr.acinq.eclair.crypto.{Generators, ShaChain} import fr.acinq.eclair.payment.OutgoingPaymentPacket diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala index 1af44e59db..2bf5dac908 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala @@ -23,7 +23,8 @@ import fr.acinq.bitcoin._ import fr.acinq.eclair._ import fr.acinq.eclair.blockchain.OnChainAddressGenerator import fr.acinq.eclair.blockchain.fee.{FeeEstimator, FeeTargets, FeeratePerKw} -import fr.acinq.eclair.channel.Channel.{ChannelConf, REFRESH_CHANNEL_UPDATE_INTERVAL} +import fr.acinq.eclair.channel.fsm.Channel +import fr.acinq.eclair.channel.fsm.Channel.{ChannelConf, REFRESH_CHANNEL_UPDATE_INTERVAL} import fr.acinq.eclair.crypto.Generators import fr.acinq.eclair.crypto.keymanager.ChannelKeyManager import fr.acinq.eclair.db.ChannelsDb diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala similarity index 68% rename from eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala rename to eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala index 50e56829fe..02716a1d1b 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala @@ -14,17 +14,15 @@ * limitations under the License. */ -package fr.acinq.eclair.channel +package fr.acinq.eclair.channel.fsm import akka.actor.typed.scaladsl.Behaviors -import akka.actor.typed.scaladsl.adapter.{ClassicActorContextOps, TypedActorRefOps, actorRefAdapter} -import akka.actor.{Actor, ActorContext, ActorRef, FSM, OneForOneStrategy, PossiblyHarmful, Props, Status, SupervisorStrategy, typed} +import akka.actor.typed.scaladsl.adapter.{ClassicActorContextOps, actorRefAdapter} +import akka.actor.{Actor, ActorContext, ActorRef, FSM, OneForOneStrategy, PossiblyHarmful, Props, SupervisorStrategy, typed} import akka.event.Logging.MDC -import akka.pattern.pipe import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} -import fr.acinq.bitcoin.{ByteVector32, OutPoint, Satoshi, SatoshiLong, Script, ScriptFlags, Transaction} +import fr.acinq.bitcoin.{ByteVector32, Satoshi, SatoshiLong, Transaction} import fr.acinq.eclair.Logs.LogCategory -import fr.acinq.eclair.NotificationsLogger.NotifyNodeOperator import fr.acinq.eclair._ import fr.acinq.eclair.blockchain.OnChainWallet.MakeFundingTxResponse import fr.acinq.eclair.blockchain._ @@ -33,12 +31,12 @@ import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._ import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinCoreClient import fr.acinq.eclair.channel.Commitments.PostRevocationAction import fr.acinq.eclair.channel.Helpers.Syncing.SyncResult -import fr.acinq.eclair.channel.Helpers.{Closing, Funding, Syncing, getRelayFees} +import fr.acinq.eclair.channel.Helpers.{Closing, Syncing, getRelayFees} import fr.acinq.eclair.channel.Monitoring.Metrics.ProcessMessage import fr.acinq.eclair.channel.Monitoring.{Metrics, Tags} import fr.acinq.eclair.channel.publish.TxPublisher -import fr.acinq.eclair.channel.publish.TxPublisher.{PublishFinalTx, PublishReplaceableTx, PublishTx, SetChannelId} -import fr.acinq.eclair.crypto.ShaChain +import fr.acinq.eclair.channel.publish.TxPublisher.{PublishFinalTx, SetChannelId} +import fr.acinq.eclair.channel._ import fr.acinq.eclair.crypto.keymanager.ChannelKeyManager import fr.acinq.eclair.db.DbEventHandler.ChannelEvent.EventType import fr.acinq.eclair.db.PendingCommandsDb @@ -46,16 +44,15 @@ import fr.acinq.eclair.io.Peer import fr.acinq.eclair.payment.relay.Relayer import fr.acinq.eclair.payment.{Bolt11Invoice, PaymentSettlingOnChain} import fr.acinq.eclair.router.Announcements -import fr.acinq.eclair.transactions.Transactions.{ClosingTx, TxOwner} +import fr.acinq.eclair.transactions.Transactions.ClosingTx import fr.acinq.eclair.transactions._ import fr.acinq.eclair.wire.protocol._ import scodec.bits.ByteVector -import java.sql.SQLException import scala.collection.immutable.Queue import scala.concurrent.ExecutionContext import scala.concurrent.duration._ -import scala.util.{Failure, Random, Success, Try} +import scala.util.Random /** * Created by PM on 20/08/2015. @@ -163,26 +160,32 @@ object Channel { } -class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, remoteNodeId: PublicKey, blockchain: typed.ActorRef[ZmqWatcher.Command], relayer: ActorRef, txPublisherFactory: Channel.TxPublisherFactory, origin_opt: Option[ActorRef] = None)(implicit ec: ExecutionContext = ExecutionContext.Implicits.global) extends FSM[ChannelState, ChannelData] with FSMDiagnosticActorLogging[ChannelState, ChannelData] { +class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val remoteNodeId: PublicKey, val blockchain: typed.ActorRef[ZmqWatcher.Command], val relayer: ActorRef, val txPublisherFactory: Channel.TxPublisherFactory, val origin_opt: Option[ActorRef] = None)(implicit val ec: ExecutionContext = ExecutionContext.Implicits.global) + extends FSM[ChannelState, ChannelData] + with FSMDiagnosticActorLogging[ChannelState, ChannelData] + with ChannelOpenSingleFunder + with CommonHandlers + with FundingHandlers + with ErrorHandlers { import Channel._ - private val keyManager: ChannelKeyManager = nodeParams.channelKeyManager + val keyManager: ChannelKeyManager = nodeParams.channelKeyManager // we pass these to helpers classes so that they have the logging context implicit def implicitLog: akka.event.DiagnosticLoggingAdapter = diagLog // we assume that the peer is the channel's parent - private val peer = context.parent + val peer = context.parent // noinspection ActorMutableStateInspection // the last active connection we are aware of; note that the peer manages connections and asynchronously notifies // the channel, which means that if we get disconnected, the previous active connection will die and some messages will // be sent to dead letters, before the channel gets notified of the disconnection; knowing that this will happen, we // choose to not make this an Option (that would be None before the first connection), and instead embrace the fact // that the active connection may point to dead letters at all time - private var activeConnection = context.system.deadLetters + var activeConnection = context.system.deadLetters - private val txPublisher = txPublisherFactory.spawnTxPublisher(context, remoteNodeId) + val txPublisher = txPublisherFactory.spawnTxPublisher(context, remoteNodeId) // this will be used to detect htlc timeouts context.system.eventStream.subscribe(self, classOf[CurrentBlockHeight]) @@ -362,356 +365,6 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, remo goto(CLOSED) }) - when(WAIT_FOR_OPEN_CHANNEL)(handleExceptions { - case Event(open: OpenChannel, d@DATA_WAIT_FOR_OPEN_CHANNEL(INPUT_INIT_FUNDEE(_, localParams, _, remoteInit, channelConfig, channelType))) => - Helpers.validateParamsFundee(nodeParams, channelType, localParams.initFeatures, open, remoteNodeId, remoteInit.features) match { - case Left(t) => handleLocalError(t, d, Some(open)) - case Right((channelFeatures, remoteShutdownScript)) => - context.system.eventStream.publish(ChannelCreated(self, peer, remoteNodeId, isFunder = false, open.temporaryChannelId, open.feeratePerKw, None)) - val fundingPubkey = keyManager.fundingPublicKey(localParams.fundingKeyPath).publicKey - val channelKeyPath = keyManager.keyPath(localParams, channelConfig) - val minimumDepth = Helpers.minDepthForFunding(nodeParams.channelConf, open.fundingSatoshis) - // In order to allow TLV extensions and keep backwards-compatibility, we include an empty upfront_shutdown_script if this feature is not used. - // See https://github.com/lightningnetwork/lightning-rfc/pull/714. - val localShutdownScript = if (Features.canUseFeature(localParams.initFeatures, remoteInit.features, Features.UpfrontShutdownScript)) localParams.defaultFinalScriptPubKey else ByteVector.empty - val accept = AcceptChannel(temporaryChannelId = open.temporaryChannelId, - dustLimitSatoshis = localParams.dustLimit, - maxHtlcValueInFlightMsat = localParams.maxHtlcValueInFlightMsat, - channelReserveSatoshis = localParams.channelReserve, - minimumDepth = minimumDepth, - htlcMinimumMsat = localParams.htlcMinimum, - toSelfDelay = localParams.toSelfDelay, - maxAcceptedHtlcs = localParams.maxAcceptedHtlcs, - fundingPubkey = fundingPubkey, - revocationBasepoint = keyManager.revocationPoint(channelKeyPath).publicKey, - paymentBasepoint = localParams.walletStaticPaymentBasepoint.getOrElse(keyManager.paymentPoint(channelKeyPath).publicKey), - delayedPaymentBasepoint = keyManager.delayedPaymentPoint(channelKeyPath).publicKey, - htlcBasepoint = keyManager.htlcPoint(channelKeyPath).publicKey, - firstPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, 0), - tlvStream = TlvStream( - ChannelTlv.UpfrontShutdownScriptTlv(localShutdownScript), - ChannelTlv.ChannelTypeTlv(channelType) - )) - val remoteParams = RemoteParams( - nodeId = remoteNodeId, - dustLimit = open.dustLimitSatoshis, - maxHtlcValueInFlightMsat = open.maxHtlcValueInFlightMsat, - channelReserve = open.channelReserveSatoshis, // remote requires local to keep this much satoshis as direct payment - htlcMinimum = open.htlcMinimumMsat, - toSelfDelay = open.toSelfDelay, - maxAcceptedHtlcs = open.maxAcceptedHtlcs, - fundingPubKey = open.fundingPubkey, - revocationBasepoint = open.revocationBasepoint, - paymentBasepoint = open.paymentBasepoint, - delayedPaymentBasepoint = open.delayedPaymentBasepoint, - htlcBasepoint = open.htlcBasepoint, - initFeatures = remoteInit.features, - shutdownScript = remoteShutdownScript) - log.debug("remote params: {}", remoteParams) - goto(WAIT_FOR_FUNDING_CREATED) using DATA_WAIT_FOR_FUNDING_CREATED(open.temporaryChannelId, localParams, remoteParams, open.fundingSatoshis, open.pushMsat, open.feeratePerKw, open.firstPerCommitmentPoint, open.channelFlags, channelConfig, channelFeatures, accept) sending accept - } - - case Event(c: CloseCommand, d) => handleFastClose(c, d.channelId) - - case Event(e: Error, d: DATA_WAIT_FOR_OPEN_CHANNEL) => handleRemoteError(e, d) - - case Event(INPUT_DISCONNECTED, _) => goto(CLOSED) - }) - - when(WAIT_FOR_ACCEPT_CHANNEL)(handleExceptions { - case Event(accept: AcceptChannel, d@DATA_WAIT_FOR_ACCEPT_CHANNEL(INPUT_INIT_FUNDER(temporaryChannelId, fundingSatoshis, pushMsat, initialFeeratePerKw, fundingTxFeeratePerKw, localParams, _, remoteInit, _, channelConfig, channelType), open)) => - Helpers.validateParamsFunder(nodeParams, channelType, localParams.initFeatures, remoteInit.features, open, accept) match { - case Left(t) => - channelOpenReplyToUser(Left(LocalError(t))) - handleLocalError(t, d, Some(accept)) - case Right((channelFeatures, remoteShutdownScript)) => - val remoteParams = RemoteParams( - nodeId = remoteNodeId, - dustLimit = accept.dustLimitSatoshis, - maxHtlcValueInFlightMsat = accept.maxHtlcValueInFlightMsat, - channelReserve = accept.channelReserveSatoshis, // remote requires local to keep this much satoshis as direct payment - htlcMinimum = accept.htlcMinimumMsat, - toSelfDelay = accept.toSelfDelay, - maxAcceptedHtlcs = accept.maxAcceptedHtlcs, - fundingPubKey = accept.fundingPubkey, - revocationBasepoint = accept.revocationBasepoint, - paymentBasepoint = accept.paymentBasepoint, - delayedPaymentBasepoint = accept.delayedPaymentBasepoint, - htlcBasepoint = accept.htlcBasepoint, - initFeatures = remoteInit.features, - shutdownScript = remoteShutdownScript) - log.debug("remote params: {}", remoteParams) - val localFundingPubkey = keyManager.fundingPublicKey(localParams.fundingKeyPath) - val fundingPubkeyScript = Script.write(Script.pay2wsh(Scripts.multiSig2of2(localFundingPubkey.publicKey, remoteParams.fundingPubKey))) - wallet.makeFundingTx(fundingPubkeyScript, fundingSatoshis, fundingTxFeeratePerKw).pipeTo(self) - goto(WAIT_FOR_FUNDING_INTERNAL) using DATA_WAIT_FOR_FUNDING_INTERNAL(temporaryChannelId, localParams, remoteParams, fundingSatoshis, pushMsat, initialFeeratePerKw, accept.firstPerCommitmentPoint, channelConfig, channelFeatures, open) - } - - case Event(c: CloseCommand, d: DATA_WAIT_FOR_ACCEPT_CHANNEL) => - channelOpenReplyToUser(Right(ChannelOpenResponse.ChannelClosed(d.lastSent.temporaryChannelId))) - handleFastClose(c, d.lastSent.temporaryChannelId) - - case Event(e: Error, d: DATA_WAIT_FOR_ACCEPT_CHANNEL) => - channelOpenReplyToUser(Left(RemoteError(e))) - handleRemoteError(e, d) - - case Event(INPUT_DISCONNECTED, _) => - channelOpenReplyToUser(Left(LocalError(new RuntimeException("disconnected")))) - goto(CLOSED) - - case Event(TickChannelOpenTimeout, _) => - channelOpenReplyToUser(Left(LocalError(new RuntimeException("open channel cancelled, took too long")))) - goto(CLOSED) - }) - - when(WAIT_FOR_FUNDING_INTERNAL)(handleExceptions { - case Event(MakeFundingTxResponse(fundingTx, fundingTxOutputIndex, fundingTxFee), d@DATA_WAIT_FOR_FUNDING_INTERNAL(temporaryChannelId, localParams, remoteParams, fundingAmount, pushMsat, initialFeeratePerKw, remoteFirstPerCommitmentPoint, channelConfig, channelFeatures, open)) => - // let's create the first commitment tx that spends the yet uncommitted funding tx - Funding.makeFirstCommitTxs(keyManager, channelConfig, channelFeatures, temporaryChannelId, localParams, remoteParams, fundingAmount, pushMsat, initialFeeratePerKw, fundingTx.hash, fundingTxOutputIndex, remoteFirstPerCommitmentPoint) match { - case Left(ex) => handleLocalError(ex, d, None) - case Right((localSpec, localCommitTx, remoteSpec, remoteCommitTx)) => - require(fundingTx.txOut(fundingTxOutputIndex).publicKeyScript == localCommitTx.input.txOut.publicKeyScript, s"pubkey script mismatch!") - val localSigOfRemoteTx = keyManager.sign(remoteCommitTx, keyManager.fundingPublicKey(localParams.fundingKeyPath), TxOwner.Remote, channelFeatures.commitmentFormat) - // signature of their initial commitment tx that pays remote pushMsat - val fundingCreated = FundingCreated( - temporaryChannelId = temporaryChannelId, - fundingTxid = fundingTx.hash, - fundingOutputIndex = fundingTxOutputIndex, - signature = localSigOfRemoteTx - ) - val channelId = toLongId(fundingTx.hash, fundingTxOutputIndex) - peer ! ChannelIdAssigned(self, remoteNodeId, temporaryChannelId, channelId) // we notify the peer asap so it knows how to route messages - txPublisher ! SetChannelId(remoteNodeId, channelId) - context.system.eventStream.publish(ChannelIdAssigned(self, remoteNodeId, temporaryChannelId, channelId)) - // NB: we don't send a ChannelSignatureSent for the first commit - goto(WAIT_FOR_FUNDING_SIGNED) using DATA_WAIT_FOR_FUNDING_SIGNED(channelId, localParams, remoteParams, fundingTx, fundingTxFee, localSpec, localCommitTx, RemoteCommit(0, remoteSpec, remoteCommitTx.tx.txid, remoteFirstPerCommitmentPoint), open.channelFlags, channelConfig, channelFeatures, fundingCreated) sending fundingCreated - } - - case Event(Status.Failure(t), d: DATA_WAIT_FOR_FUNDING_INTERNAL) => - log.error(t, s"wallet returned error: ") - channelOpenReplyToUser(Left(LocalError(t))) - handleLocalError(ChannelFundingError(d.temporaryChannelId), d, None) // we use a generic exception and don't send the internal error to the peer - - case Event(c: CloseCommand, d: DATA_WAIT_FOR_FUNDING_INTERNAL) => - channelOpenReplyToUser(Right(ChannelOpenResponse.ChannelClosed(d.temporaryChannelId))) - handleFastClose(c, d.temporaryChannelId) - - case Event(e: Error, d: DATA_WAIT_FOR_FUNDING_INTERNAL) => - channelOpenReplyToUser(Left(RemoteError(e))) - handleRemoteError(e, d) - - case Event(INPUT_DISCONNECTED, _) => - channelOpenReplyToUser(Left(LocalError(new RuntimeException("disconnected")))) - goto(CLOSED) - - case Event(TickChannelOpenTimeout, _) => - channelOpenReplyToUser(Left(LocalError(new RuntimeException("open channel cancelled, took too long")))) - goto(CLOSED) - }) - - when(WAIT_FOR_FUNDING_CREATED)(handleExceptions { - case Event(FundingCreated(_, fundingTxHash, fundingTxOutputIndex, remoteSig, _), d@DATA_WAIT_FOR_FUNDING_CREATED(temporaryChannelId, localParams, remoteParams, fundingAmount, pushMsat, initialFeeratePerKw, remoteFirstPerCommitmentPoint, channelFlags, channelConfig, channelFeatures, _)) => - // they fund the channel with their funding tx, so the money is theirs (but we are paid pushMsat) - Funding.makeFirstCommitTxs(keyManager, channelConfig, channelFeatures, temporaryChannelId, localParams, remoteParams, fundingAmount, pushMsat, initialFeeratePerKw, fundingTxHash, fundingTxOutputIndex, remoteFirstPerCommitmentPoint) match { - case Left(ex) => handleLocalError(ex, d, None) - case Right((localSpec, localCommitTx, remoteSpec, remoteCommitTx)) => - // check remote signature validity - val fundingPubKey = keyManager.fundingPublicKey(localParams.fundingKeyPath) - val localSigOfLocalTx = keyManager.sign(localCommitTx, fundingPubKey, TxOwner.Local, channelFeatures.commitmentFormat) - val signedLocalCommitTx = Transactions.addSigs(localCommitTx, fundingPubKey.publicKey, remoteParams.fundingPubKey, localSigOfLocalTx, remoteSig) - Transactions.checkSpendable(signedLocalCommitTx) match { - case Failure(_) => handleLocalError(InvalidCommitmentSignature(temporaryChannelId, signedLocalCommitTx.tx), d, None) - case Success(_) => - val localSigOfRemoteTx = keyManager.sign(remoteCommitTx, fundingPubKey, TxOwner.Remote, channelFeatures.commitmentFormat) - val channelId = toLongId(fundingTxHash, fundingTxOutputIndex) - // watch the funding tx transaction - val commitInput = localCommitTx.input - val fundingSigned = FundingSigned( - channelId = channelId, - signature = localSigOfRemoteTx - ) - val commitments = Commitments(channelId, channelConfig, channelFeatures, localParams, remoteParams, channelFlags, - LocalCommit(0, localSpec, CommitTxAndRemoteSig(localCommitTx, remoteSig), htlcTxsAndRemoteSigs = Nil), RemoteCommit(0, remoteSpec, remoteCommitTx.tx.txid, remoteFirstPerCommitmentPoint), - LocalChanges(Nil, Nil, Nil), RemoteChanges(Nil, Nil, Nil), - localNextHtlcId = 0L, remoteNextHtlcId = 0L, - originChannels = Map.empty, - remoteNextCommitInfo = Right(randomKey().publicKey), // we will receive their next per-commitment point in the next message, so we temporarily put a random byte array, - commitInput, ShaChain.init) - peer ! ChannelIdAssigned(self, remoteNodeId, temporaryChannelId, channelId) // we notify the peer asap so it knows how to route messages - txPublisher ! SetChannelId(remoteNodeId, channelId) - context.system.eventStream.publish(ChannelIdAssigned(self, remoteNodeId, temporaryChannelId, channelId)) - context.system.eventStream.publish(ChannelSignatureReceived(self, commitments)) - // NB: we don't send a ChannelSignatureSent for the first commit - log.info(s"waiting for them to publish the funding tx for channelId=$channelId fundingTxid=${commitInput.outPoint.txid}") - watchFundingTx(commitments) - val fundingMinDepth = Helpers.minDepthForFunding(nodeParams.channelConf, fundingAmount) - blockchain ! WatchFundingConfirmed(self, commitInput.outPoint.txid, fundingMinDepth) - goto(WAIT_FOR_FUNDING_CONFIRMED) using DATA_WAIT_FOR_FUNDING_CONFIRMED(commitments, None, nodeParams.currentBlockHeight, None, Right(fundingSigned)) storing() sending fundingSigned - } - } - - case Event(c: CloseCommand, d: DATA_WAIT_FOR_FUNDING_CREATED) => - channelOpenReplyToUser(Right(ChannelOpenResponse.ChannelClosed(d.temporaryChannelId))) - handleFastClose(c, d.temporaryChannelId) - - case Event(e: Error, d: DATA_WAIT_FOR_FUNDING_CREATED) => handleRemoteError(e, d) - - case Event(INPUT_DISCONNECTED, _) => goto(CLOSED) - }) - - when(WAIT_FOR_FUNDING_SIGNED)(handleExceptions { - case Event(msg@FundingSigned(_, remoteSig, _), d@DATA_WAIT_FOR_FUNDING_SIGNED(channelId, localParams, remoteParams, fundingTx, fundingTxFee, localSpec, localCommitTx, remoteCommit, channelFlags, channelConfig, channelFeatures, fundingCreated)) => - // we make sure that their sig checks out and that our first commit tx is spendable - val fundingPubKey = keyManager.fundingPublicKey(localParams.fundingKeyPath) - val localSigOfLocalTx = keyManager.sign(localCommitTx, fundingPubKey, TxOwner.Local, channelFeatures.commitmentFormat) - val signedLocalCommitTx = Transactions.addSigs(localCommitTx, fundingPubKey.publicKey, remoteParams.fundingPubKey, localSigOfLocalTx, remoteSig) - Transactions.checkSpendable(signedLocalCommitTx) match { - case Failure(cause) => - // we rollback the funding tx, it will never be published - wallet.rollback(fundingTx) - channelOpenReplyToUser(Left(LocalError(cause))) - handleLocalError(InvalidCommitmentSignature(channelId, signedLocalCommitTx.tx), d, Some(msg)) - case Success(_) => - val commitInput = localCommitTx.input - val commitments = Commitments(channelId, channelConfig, channelFeatures, localParams, remoteParams, channelFlags, - LocalCommit(0, localSpec, CommitTxAndRemoteSig(localCommitTx, remoteSig), htlcTxsAndRemoteSigs = Nil), remoteCommit, - LocalChanges(Nil, Nil, Nil), RemoteChanges(Nil, Nil, Nil), - localNextHtlcId = 0L, remoteNextHtlcId = 0L, - originChannels = Map.empty, - remoteNextCommitInfo = Right(randomKey().publicKey), // we will receive their next per-commitment point in the next message, so we temporarily put a random byte array - commitInput, ShaChain.init) - val blockHeight = nodeParams.currentBlockHeight - context.system.eventStream.publish(ChannelSignatureReceived(self, commitments)) - log.info(s"publishing funding tx for channelId=$channelId fundingTxid=${commitInput.outPoint.txid}") - watchFundingTx(commitments) - blockchain ! WatchFundingConfirmed(self, commitInput.outPoint.txid, nodeParams.channelConf.minDepthBlocks) - log.info(s"committing txid=${fundingTx.txid}") - - // we will publish the funding tx only after the channel state has been written to disk because we want to - // make sure we first persist the commitment that returns back the funds to us in case of problem - def publishFundingTx(): Unit = { - wallet.commit(fundingTx).onComplete { - case Success(true) => - context.system.eventStream.publish(TransactionPublished(commitments.channelId, remoteNodeId, fundingTx, fundingTxFee, "funding")) - channelOpenReplyToUser(Right(ChannelOpenResponse.ChannelOpened(channelId))) - case Success(false) => - channelOpenReplyToUser(Left(LocalError(new RuntimeException("couldn't publish funding tx")))) - self ! BITCOIN_FUNDING_PUBLISH_FAILED // fail-fast: this should be returned only when we are really sure the tx has *not* been published - case Failure(t) => - channelOpenReplyToUser(Left(LocalError(t))) - log.error(t, s"error while committing funding tx: ") // tx may still have been published, can't fail-fast - } - } - - goto(WAIT_FOR_FUNDING_CONFIRMED) using DATA_WAIT_FOR_FUNDING_CONFIRMED(commitments, Some(fundingTx), blockHeight, None, Left(fundingCreated)) storing() calling publishFundingTx() - } - - case Event(c: CloseCommand, d: DATA_WAIT_FOR_FUNDING_SIGNED) => - // we rollback the funding tx, it will never be published - wallet.rollback(d.fundingTx) - channelOpenReplyToUser(Right(ChannelOpenResponse.ChannelClosed(d.channelId))) - handleFastClose(c, d.channelId) - - case Event(e: Error, d: DATA_WAIT_FOR_FUNDING_SIGNED) => - // we rollback the funding tx, it will never be published - wallet.rollback(d.fundingTx) - channelOpenReplyToUser(Left(RemoteError(e))) - handleRemoteError(e, d) - - case Event(INPUT_DISCONNECTED, d: DATA_WAIT_FOR_FUNDING_SIGNED) => - // we rollback the funding tx, it will never be published - wallet.rollback(d.fundingTx) - channelOpenReplyToUser(Left(LocalError(new RuntimeException("disconnected")))) - goto(CLOSED) - - case Event(TickChannelOpenTimeout, d: DATA_WAIT_FOR_FUNDING_SIGNED) => - // we rollback the funding tx, it will never be published - wallet.rollback(d.fundingTx) - channelOpenReplyToUser(Left(LocalError(new RuntimeException("open channel cancelled, took too long")))) - goto(CLOSED) - }) - - when(WAIT_FOR_FUNDING_CONFIRMED)(handleExceptions { - case Event(msg: FundingLocked, d: DATA_WAIT_FOR_FUNDING_CONFIRMED) => - log.info(s"received their FundingLocked, deferring message") - stay() using d.copy(deferred = Some(msg)) // no need to store, they will re-send if we get disconnected - - case Event(WatchFundingConfirmedTriggered(blockHeight, txIndex, fundingTx), d@DATA_WAIT_FOR_FUNDING_CONFIRMED(commitments, _, _, deferred, _)) => - Try(Transaction.correctlySpends(commitments.fullySignedLocalCommitTx(keyManager).tx, Seq(fundingTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)) match { - case Success(_) => - log.info(s"channelId=${commitments.channelId} was confirmed at blockHeight=$blockHeight txIndex=$txIndex") - blockchain ! WatchFundingLost(self, commitments.commitInput.outPoint.txid, nodeParams.channelConf.minDepthBlocks) - if (!d.commitments.localParams.isFunder) context.system.eventStream.publish(TransactionPublished(commitments.channelId, remoteNodeId, fundingTx, 0 sat, "funding")) - context.system.eventStream.publish(TransactionConfirmed(commitments.channelId, remoteNodeId, fundingTx)) - val channelKeyPath = keyManager.keyPath(d.commitments.localParams, commitments.channelConfig) - val nextPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, 1) - val fundingLocked = FundingLocked(commitments.channelId, nextPerCommitmentPoint) - deferred.foreach(self ! _) - // this is the temporary channel id that we will use in our channel_update message, the goal is to be able to use our channel - // as soon as it reaches NORMAL state, and before it is announced on the network - // (this id might be updated when the funding tx gets deeply buried, if there was a reorg in the meantime) - val shortChannelId = ShortChannelId(blockHeight, txIndex, commitments.commitInput.outPoint.index.toInt) - goto(WAIT_FOR_FUNDING_LOCKED) using DATA_WAIT_FOR_FUNDING_LOCKED(commitments, shortChannelId, fundingLocked) storing() sending fundingLocked - case Failure(t) => - log.error(t, s"rejecting channel with invalid funding tx: ${fundingTx.bin}") - goto(CLOSED) - } - - case Event(remoteAnnSigs: AnnouncementSignatures, d: DATA_WAIT_FOR_FUNDING_CONFIRMED) if d.commitments.announceChannel => - log.debug("received remote announcement signatures, delaying") - // we may receive their announcement sigs before our watcher notifies us that the channel has reached min_conf (especially during testing when blocks are generated in bulk) - // note: no need to persist their message, in case of disconnection they will resend it - context.system.scheduler.scheduleOnce(2 seconds, self, remoteAnnSigs) - stay() - - case Event(getTxResponse: GetTxWithMetaResponse, d: DATA_WAIT_FOR_FUNDING_CONFIRMED) if getTxResponse.txid == d.commitments.commitInput.outPoint.txid => handleGetFundingTx(getTxResponse, d.waitingSince, d.fundingTx) - - case Event(BITCOIN_FUNDING_PUBLISH_FAILED, d: DATA_WAIT_FOR_FUNDING_CONFIRMED) => handleFundingPublishFailed(d) - - case Event(ProcessCurrentBlockHeight(c), d: DATA_WAIT_FOR_FUNDING_CONFIRMED) => d.fundingTx match { - case Some(_) => stay() // we are funder, we're still waiting for the funding tx to be confirmed - case None if c.blockHeight - d.waitingSince > FUNDING_TIMEOUT_FUNDEE => - log.warning(s"funding tx hasn't been published in ${c.blockHeight - d.waitingSince} blocks") - self ! BITCOIN_FUNDING_TIMEOUT - stay() - case None => stay() // let's wait longer - } - - case Event(BITCOIN_FUNDING_TIMEOUT, d: DATA_WAIT_FOR_FUNDING_CONFIRMED) => handleFundingTimeout(d) - - case Event(WatchFundingSpentTriggered(tx), d: DATA_WAIT_FOR_FUNDING_CONFIRMED) if tx.txid == d.commitments.remoteCommit.txid => handleRemoteSpentCurrent(tx, d) - - case Event(WatchFundingSpentTriggered(tx), d: DATA_WAIT_FOR_FUNDING_CONFIRMED) => handleInformationLeak(tx, d) - - case Event(e: Error, d: DATA_WAIT_FOR_FUNDING_CONFIRMED) => handleRemoteError(e, d) - }) - - when(WAIT_FOR_FUNDING_LOCKED)(handleExceptions { - case Event(FundingLocked(_, nextPerCommitmentPoint, _), d@DATA_WAIT_FOR_FUNDING_LOCKED(commitments, shortChannelId, _)) => - // used to get the final shortChannelId, used in announcements (if minDepth >= ANNOUNCEMENTS_MINCONF this event will fire instantly) - blockchain ! WatchFundingDeeplyBuried(self, commitments.commitInput.outPoint.txid, ANNOUNCEMENTS_MINCONF) - context.system.eventStream.publish(ShortChannelIdAssigned(self, commitments.channelId, shortChannelId, None)) - // we create a channel_update early so that we can use it to send payments through this channel, but it won't be propagated to other nodes since the channel is not yet announced - val fees = getRelayFees(nodeParams, remoteNodeId, commitments) - val initialChannelUpdate = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, shortChannelId, nodeParams.channelConf.expiryDelta, d.commitments.remoteParams.htlcMinimum, fees.feeBase, fees.feeProportionalMillionths, commitments.capacity.toMilliSatoshi, enable = Helpers.aboveReserve(d.commitments)) - // we need to periodically re-send channel updates, otherwise channel will be considered stale and get pruned by network - context.system.scheduler.scheduleWithFixedDelay(initialDelay = REFRESH_CHANNEL_UPDATE_INTERVAL, delay = REFRESH_CHANNEL_UPDATE_INTERVAL, receiver = self, message = BroadcastChannelUpdate(PeriodicRefresh)) - goto(NORMAL) using DATA_NORMAL(commitments.copy(remoteNextCommitInfo = Right(nextPerCommitmentPoint)), shortChannelId, buried = false, None, initialChannelUpdate, None, None, None) storing() - - case Event(remoteAnnSigs: AnnouncementSignatures, d: DATA_WAIT_FOR_FUNDING_LOCKED) if d.commitments.announceChannel => - log.debug("received remote announcement signatures, delaying") - // we may receive their announcement sigs before our watcher notifies us that the channel has reached min_conf (especially during testing when blocks are generated in bulk) - // note: no need to persist their message, in case of disconnection they will resend it - context.system.scheduler.scheduleOnce(2 seconds, self, remoteAnnSigs) - stay() - - case Event(WatchFundingSpentTriggered(tx), d: DATA_WAIT_FOR_FUNDING_LOCKED) if tx.txid == d.commitments.remoteCommit.txid => handleRemoteSpentCurrent(tx, d) - - case Event(WatchFundingSpentTriggered(tx), d: DATA_WAIT_FOR_FUNDING_LOCKED) => handleInformationLeak(tx, d) - - case Event(e: Error, d: DATA_WAIT_FOR_FUNDING_LOCKED) => handleRemoteError(e, d) - }) - /* 888b d888 d8888 8888888 888b 888 888 .d88888b. .d88888b. 8888888b. 8888b d8888 d88888 888 8888b 888 888 d88P" "Y88b d88P" "Y88b 888 Y88b @@ -2072,18 +1725,6 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, remo 888 888 d88P 888 888 Y888 8888888P" 88888888 8888888888 888 T88b "Y8888P" */ - /** - * This function is used to return feedback to user at channel opening - */ - private def channelOpenReplyToUser(message: Either[ChannelOpenError, ChannelOpenResponse]): Unit = { - val m = message match { - case Left(LocalError(t)) => Status.Failure(t) - case Left(RemoteError(e)) => Status.Failure(new RuntimeException(s"peer sent error: ascii='${e.toAscii}' bin=${e.data.toHex}")) - case Right(s) => s - } - origin_opt.foreach(_ ! m) - } - private def handleCurrentFeerate(c: CurrentFeerates, d: HasCommitments) = { val networkFeeratePerKw = nodeParams.onChainFeeConf.getCommitmentFeerate(remoteNodeId, d.commitments.channelType, d.commitments.capacity, Some(c)) val currentFeeratePerKw = d.commitments.localCommit.spec.commitTxFeerate @@ -2128,12 +1769,6 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, remo } } - private def handleFastClose(c: CloseCommand, channelId: ByteVector32) = { - val replyTo = if (c.replyTo == ActorRef.noSender) sender() else c.replyTo - replyTo ! RES_SUCCESS(c, channelId) - goto(CLOSED) - } - private def handleCommandSuccess(c: channel.Command, newData: ChannelData) = { val replyTo_opt = c match { case hasOptionalReplyTo: HasOptionalReplyToCommand => hasOptionalReplyTo.replyTo_opt @@ -2164,74 +1799,6 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, remo stay() } - private def watchFundingTx(commitments: Commitments, additionalKnownSpendingTxs: Set[ByteVector32] = Set.empty): Unit = { - // TODO: should we wait for an acknowledgment from the watcher? - val knownSpendingTxs = Set(commitments.localCommit.commitTxAndRemoteSig.commitTx.tx.txid, commitments.remoteCommit.txid) ++ commitments.remoteNextCommitInfo.left.toSeq.map(_.nextRemoteCommit.txid).toSet ++ additionalKnownSpendingTxs - blockchain ! WatchFundingSpent(self, commitments.commitInput.outPoint.txid, commitments.commitInput.outPoint.index.toInt, knownSpendingTxs) - // TODO: implement this? (not needed if we use a reasonable min_depth) - //blockchain ! WatchLost(self, commitments.commitInput.outPoint.txid, nodeParams.channelConf.minDepthBlocks, BITCOIN_FUNDING_LOST) - } - - /** - * When we are funder, we use this function to detect when our funding tx has been double-spent (by another transaction - * that we made for some reason). If the funding tx has been double spent we can forget about the channel. - */ - private def checkDoubleSpent(fundingTx: Transaction): Unit = { - log.debug(s"checking status of funding tx txid=${fundingTx.txid}") - wallet.doubleSpent(fundingTx).onComplete { - case Success(true) => - log.warning(s"funding tx has been double spent! fundingTxid=${fundingTx.txid} fundingTx=$fundingTx") - self ! BITCOIN_FUNDING_PUBLISH_FAILED - case Success(false) => () - case Failure(t) => log.error(t, s"error while testing status of funding tx fundingTxid=${fundingTx.txid}: ") - } - } - - private def handleGetFundingTx(getTxResponse: GetTxWithMetaResponse, waitingSince: BlockHeight, fundingTx_opt: Option[Transaction]) = { - import getTxResponse._ - tx_opt match { - case Some(_) => () // the funding tx exists, nothing to do - case None => - fundingTx_opt match { - case Some(fundingTx) => - // if we are funder, we never give up - // we cannot correctly set the fee, but it was correctly set when we initially published the transaction - log.info(s"republishing the funding tx...") - txPublisher ! PublishFinalTx(fundingTx, fundingTx.txIn.head.outPoint, "funding", 0 sat, None) - // we also check if the funding tx has been double-spent - checkDoubleSpent(fundingTx) - context.system.scheduler.scheduleOnce(1 day, blockchain.toClassic, GetTxWithMeta(self, txid)) - case None if (nodeParams.currentBlockHeight - waitingSince) > FUNDING_TIMEOUT_FUNDEE => - // if we are fundee, we give up after some time - log.warning(s"funding tx hasn't been published in ${nodeParams.currentBlockHeight - waitingSince} blocks") - self ! BITCOIN_FUNDING_TIMEOUT - case None => - // let's wait a little longer - log.info(s"funding tx still hasn't been published in ${nodeParams.currentBlockHeight - waitingSince} blocks, will wait ${FUNDING_TIMEOUT_FUNDEE - (nodeParams.currentBlockHeight - waitingSince)} more blocks...") - context.system.scheduler.scheduleOnce(1 day, blockchain.toClassic, GetTxWithMeta(self, txid)) - } - } - stay() - } - - private def handleFundingPublishFailed(d: HasCommitments) = { - log.error(s"failed to publish funding tx") - val exc = ChannelFundingError(d.channelId) - val error = Error(d.channelId, exc.getMessage) - // NB: we don't use the handleLocalError handler because it would result in the commit tx being published, which we don't want: - // implementation *guarantees* that in case of BITCOIN_FUNDING_PUBLISH_FAILED, the funding tx hasn't and will never be published, so we can close the channel right away - context.system.eventStream.publish(ChannelErrorOccurred(self, stateData.channelId, remoteNodeId, stateData, LocalError(exc), isFatal = true)) - goto(CLOSED) sending error - } - - private def handleFundingTimeout(d: HasCommitments) = { - log.warning(s"funding tx hasn't been confirmed in time, cancelling channel delay=$FUNDING_TIMEOUT_FUNDEE") - val exc = FundingTxTimedout(d.channelId) - val error = Error(d.channelId, exc.getMessage) - context.system.eventStream.publish(ChannelErrorOccurred(self, stateData.channelId, remoteNodeId, stateData, LocalError(exc), isFatal = true)) - goto(CLOSED) sending error - } - private def handleRevocationTimeout(revocationTimeout: RevocationTimeout, d: HasCommitments) = { d.commitments.remoteNextCommitInfo match { case Left(waitingForRevocation) if revocationTimeout.remoteCommitNumber + 1 == waitingForRevocation.nextRemoteCommit.index => @@ -2332,74 +1899,6 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, remo } } - private def handleLocalError(cause: Throwable, d: ChannelData, msg: Option[Any]) = { - cause match { - case _: ForcedLocalCommit => - log.warning(s"force-closing channel at user request") - case _ if msg.exists(_.isInstanceOf[OpenChannel]) || msg.exists(_.isInstanceOf[AcceptChannel]) => - // invalid remote channel parameters are logged as warning - log.warning(s"${cause.getMessage} while processing msg=${msg.getOrElse("n/a").getClass.getSimpleName} in state=$stateName") - case _: ChannelException => - log.error(s"${cause.getMessage} while processing msg=${msg.getOrElse("n/a").getClass.getSimpleName} in state=$stateName") - case _ => - // unhandled error: we dump the channel data, and print the stack trace - log.error(cause, s"msg=${msg.getOrElse("n/a")} stateData=$stateData:") - } - - val error = Error(d.channelId, cause.getMessage) - context.system.eventStream.publish(ChannelErrorOccurred(self, stateData.channelId, remoteNodeId, stateData, LocalError(cause), isFatal = true)) - - d match { - case dd: HasCommitments if Closing.nothingAtStake(dd) => goto(CLOSED) - case negotiating@DATA_NEGOTIATING(_, _, _, _, Some(bestUnpublishedClosingTx)) => - log.info(s"we have a valid closing tx, publishing it instead of our commitment: closingTxId=${bestUnpublishedClosingTx.tx.txid}") - // if we were in the process of closing and already received a closing sig from the counterparty, it's always better to use that - handleMutualClose(bestUnpublishedClosingTx, Left(negotiating)) - case dd: HasCommitments => - cause match { - case _: ChannelException => - // known channel exception: we force close using our current commitment - spendLocalCurrent(dd) sending error - case _ => - // unhandled exception: we apply the configured strategy - nodeParams.channelConf.unhandledExceptionStrategy match { - case UnhandledExceptionStrategy.LocalClose => - spendLocalCurrent(dd) sending error - case UnhandledExceptionStrategy.Stop => - log.error("unhandled exception: standard procedure would be to force-close the channel, but eclair has been configured to halt instead.") - NotificationsLogger.logFatalError( - s"""stopping node as configured strategy to unhandled exceptions for nodeId=$remoteNodeId channelId=${d.channelId} - | - |Eclair has been configured to shut down when an unhandled exception happens, instead of requesting a - |force-close from the peer. This gives the operator a chance of avoiding an unnecessary mass force-close - |of channels that may be caused by a bug in Eclair, or issues like running out of disk space, etc. - | - |You should get in touch with Eclair developers and provide logs of your node for analysis. - |""".stripMargin, cause) - sys.exit(1) - stop(FSM.Shutdown) - } - } - case _ => goto(CLOSED) sending error // when there is no commitment yet, we just send an error to our peer and go to CLOSED state - } - } - - private def handleRemoteError(e: Error, d: ChannelData) = { - // see BOLT 1: only print out data verbatim if is composed of printable ASCII characters - log.error(s"peer sent error: ascii='${e.toAscii}' bin=${e.data.toHex}") - context.system.eventStream.publish(ChannelErrorOccurred(self, stateData.channelId, remoteNodeId, stateData, RemoteError(e), isFatal = true)) - - d match { - case _: DATA_CLOSING => stay() // nothing to do, there is already a spending tx published - case negotiating@DATA_NEGOTIATING(_, _, _, _, Some(bestUnpublishedClosingTx)) => - // if we were in the process of closing and already received a closing sig from the counterparty, it's always better to use that - handleMutualClose(bestUnpublishedClosingTx, Left(negotiating)) - case d: DATA_WAIT_FOR_FUNDING_CONFIRMED if Closing.nothingAtStake(d) => goto(CLOSED) // the channel was never used and the funding tx may be double-spent - case hasCommitments: HasCommitments => spendLocalCurrent(hasCommitments) // NB: we publish the commitment even if we have nothing at stake (in a dataloss situation our peer will send us an error just for that) - case _ => goto(CLOSED) // when there is no commitment yet, we just go to CLOSED state in case an error occurs - } - } - /** * Return full information about a known closing tx. */ @@ -2411,308 +1910,6 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, remo proposedTx_opt.get.unsignedTx.copy(tx = tx) } - private def handleMutualClose(closingTx: ClosingTx, d: Either[DATA_NEGOTIATING, DATA_CLOSING]) = { - log.info(s"closing tx published: closingTxId=${closingTx.tx.txid}") - val nextData = d match { - case Left(negotiating) => DATA_CLOSING(negotiating.commitments, fundingTx = None, waitingSince = nodeParams.currentBlockHeight, negotiating.closingTxProposed.flatten.map(_.unsignedTx), mutualClosePublished = closingTx :: Nil) - case Right(closing) => closing.copy(mutualClosePublished = closing.mutualClosePublished :+ closingTx) - } - goto(CLOSING) using nextData storing() calling doPublish(closingTx, nextData.commitments.localParams.isFunder) - } - - private def doPublish(closingTx: ClosingTx, isFunder: Boolean): Unit = { - // the funder pays the fee - val fee = if (isFunder) closingTx.fee else 0.sat - txPublisher ! PublishFinalTx(closingTx, fee, None) - blockchain ! WatchTxConfirmed(self, closingTx.tx.txid, nodeParams.channelConf.minDepthBlocks) - } - - private def spendLocalCurrent(d: HasCommitments) = { - val outdatedCommitment = d match { - case _: DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT => true - case closing: DATA_CLOSING if closing.futureRemoteCommitPublished.isDefined => true - case _ => false - } - if (outdatedCommitment) { - log.warning("we have an outdated commitment: will not publish our local tx") - stay() - } else { - val commitTx = d.commitments.fullySignedLocalCommitTx(keyManager).tx - val localCommitPublished = Closing.LocalClose.claimCommitTxOutputs(keyManager, d.commitments, commitTx, nodeParams.currentBlockHeight, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) - val nextData = d match { - case closing: DATA_CLOSING => closing.copy(localCommitPublished = Some(localCommitPublished)) - case negotiating: DATA_NEGOTIATING => DATA_CLOSING(d.commitments, fundingTx = None, waitingSince = nodeParams.currentBlockHeight, negotiating.closingTxProposed.flatten.map(_.unsignedTx), localCommitPublished = Some(localCommitPublished)) - case waitForFundingConfirmed: DATA_WAIT_FOR_FUNDING_CONFIRMED => DATA_CLOSING(d.commitments, fundingTx = waitForFundingConfirmed.fundingTx, waitingSince = nodeParams.currentBlockHeight, mutualCloseProposed = Nil, localCommitPublished = Some(localCommitPublished)) - case _ => DATA_CLOSING(d.commitments, fundingTx = None, waitingSince = nodeParams.currentBlockHeight, mutualCloseProposed = Nil, localCommitPublished = Some(localCommitPublished)) - } - goto(CLOSING) using nextData storing() calling doPublish(localCommitPublished, d.commitments) - } - } - - /** - * This helper method will publish txs only if they haven't yet reached minDepth - */ - private def publishIfNeeded(txs: Iterable[PublishTx], irrevocablySpent: Map[OutPoint, Transaction]): Unit = { - val (skip, process) = txs.partition(publishTx => Closing.inputAlreadySpent(publishTx.input, irrevocablySpent)) - process.foreach { publishTx => txPublisher ! publishTx } - skip.foreach(publishTx => log.info("no need to republish tx spending {}:{}, it has already been confirmed", publishTx.input.txid, publishTx.input.index)) - } - - /** - * This helper method will watch txs only if they haven't yet reached minDepth - */ - private def watchConfirmedIfNeeded(txs: Iterable[Transaction], irrevocablySpent: Map[OutPoint, Transaction]): Unit = { - val (skip, process) = txs.partition(Closing.inputsAlreadySpent(_, irrevocablySpent)) - process.foreach(tx => blockchain ! WatchTxConfirmed(self, tx.txid, nodeParams.channelConf.minDepthBlocks)) - skip.foreach(tx => log.info(s"no need to watch txid=${tx.txid}, it has already been confirmed")) - } - - /** - * This helper method will watch txs only if the utxo they spend hasn't already been irrevocably spent - * - * @param parentTx transaction which outputs will be watched - * @param outputs outputs that will be watched. They must be a subset of the outputs of the `parentTx` - */ - private def watchSpentIfNeeded(parentTx: Transaction, outputs: Iterable[OutPoint], irrevocablySpent: Map[OutPoint, Transaction]): Unit = { - outputs.foreach { output => - require(output.txid == parentTx.txid && output.index < parentTx.txOut.size, s"output doesn't belong to the given parentTx: output=${output.txid}:${output.index} (expected txid=${parentTx.txid} index < ${parentTx.txOut.size})") - } - val (skip, process) = outputs.partition(irrevocablySpent.contains) - process.foreach(output => blockchain ! WatchOutputSpent(self, parentTx.txid, output.index.toInt, Set.empty)) - skip.foreach(output => log.info(s"no need to watch output=${output.txid}:${output.index}, it has already been spent by txid=${irrevocablySpent.get(output).map(_.txid)}")) - } - - private def doPublish(localCommitPublished: LocalCommitPublished, commitments: Commitments): Unit = { - import localCommitPublished._ - - val commitInput = commitments.commitInput.outPoint - val isFunder = commitments.localParams.isFunder - val publishQueue = commitments.commitmentFormat match { - case Transactions.DefaultCommitmentFormat => - val redeemableHtlcTxs = htlcTxs.values.flatten.map(tx => PublishFinalTx(tx, tx.fee, Some(commitTx.txid))) - List(PublishFinalTx(commitTx, commitInput, "commit-tx", Closing.commitTxFee(commitments.commitInput, commitTx, isFunder), None)) ++ (claimMainDelayedOutputTx.map(tx => PublishFinalTx(tx, tx.fee, None)) ++ redeemableHtlcTxs ++ claimHtlcDelayedTxs.map(tx => PublishFinalTx(tx, tx.fee, None))) - case _: Transactions.AnchorOutputsCommitmentFormat => - val redeemableHtlcTxs = htlcTxs.values.flatten.map(tx => PublishReplaceableTx(tx, commitments)) - val claimLocalAnchor = claimAnchorTxs.collect { case tx: Transactions.ClaimLocalAnchorOutputTx => PublishReplaceableTx(tx, commitments) } - List(PublishFinalTx(commitTx, commitInput, "commit-tx", Closing.commitTxFee(commitments.commitInput, commitTx, isFunder), None)) ++ claimLocalAnchor ++ claimMainDelayedOutputTx.map(tx => PublishFinalTx(tx, tx.fee, None)) ++ redeemableHtlcTxs ++ claimHtlcDelayedTxs.map(tx => PublishFinalTx(tx, tx.fee, None)) - } - publishIfNeeded(publishQueue, irrevocablySpent) - - // we watch: - // - the commitment tx itself, so that we can handle the case where we don't have any outputs - // - 'final txs' that send funds to our wallet and that spend outputs that only us control - val watchConfirmedQueue = List(commitTx) ++ claimMainDelayedOutputTx.map(_.tx) ++ claimHtlcDelayedTxs.map(_.tx) - watchConfirmedIfNeeded(watchConfirmedQueue, irrevocablySpent) - - // we watch outputs of the commitment tx that both parties may spend - // we also watch our local anchor: this ensures that we will correctly detect when it's confirmed and count its fees - // in the audit DB, even if we restart before confirmation - val watchSpentQueue = htlcTxs.keys ++ claimAnchorTxs.collect { case tx: Transactions.ClaimLocalAnchorOutputTx => tx.input.outPoint } - watchSpentIfNeeded(commitTx, watchSpentQueue, irrevocablySpent) - } - - private def handleRemoteSpentCurrent(commitTx: Transaction, d: HasCommitments) = { - log.warning(s"they published their current commit in txid=${commitTx.txid}") - require(commitTx.txid == d.commitments.remoteCommit.txid, "txid mismatch") - - context.system.eventStream.publish(TransactionPublished(d.channelId, remoteNodeId, commitTx, Closing.commitTxFee(d.commitments.commitInput, commitTx, d.commitments.localParams.isFunder), "remote-commit")) - val remoteCommitPublished = Closing.RemoteClose.claimCommitTxOutputs(keyManager, d.commitments, d.commitments.remoteCommit, commitTx, nodeParams.currentBlockHeight, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) - val nextData = d match { - case closing: DATA_CLOSING => closing.copy(remoteCommitPublished = Some(remoteCommitPublished)) - case negotiating: DATA_NEGOTIATING => DATA_CLOSING(d.commitments, fundingTx = None, waitingSince = nodeParams.currentBlockHeight, negotiating.closingTxProposed.flatten.map(_.unsignedTx), remoteCommitPublished = Some(remoteCommitPublished)) - case waitForFundingConfirmed: DATA_WAIT_FOR_FUNDING_CONFIRMED => DATA_CLOSING(d.commitments, fundingTx = waitForFundingConfirmed.fundingTx, waitingSince = nodeParams.currentBlockHeight, mutualCloseProposed = Nil, remoteCommitPublished = Some(remoteCommitPublished)) - case _ => DATA_CLOSING(d.commitments, fundingTx = None, waitingSince = nodeParams.currentBlockHeight, mutualCloseProposed = Nil, remoteCommitPublished = Some(remoteCommitPublished)) - } - goto(CLOSING) using nextData storing() calling doPublish(remoteCommitPublished, d.commitments) - } - - private def handleRemoteSpentFuture(commitTx: Transaction, d: DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT) = { - log.warning(s"they published their future commit (because we asked them to) in txid=${commitTx.txid}") - context.system.eventStream.publish(TransactionPublished(d.channelId, remoteNodeId, commitTx, Closing.commitTxFee(d.commitments.commitInput, commitTx, d.commitments.localParams.isFunder), "future-remote-commit")) - val remotePerCommitmentPoint = d.remoteChannelReestablish.myCurrentPerCommitmentPoint - val remoteCommitPublished = RemoteCommitPublished( - commitTx = commitTx, - claimMainOutputTx = Closing.RemoteClose.claimMainOutput(keyManager, d.commitments, remotePerCommitmentPoint, commitTx, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets), - claimHtlcTxs = Map.empty, - claimAnchorTxs = List.empty, - irrevocablySpent = Map.empty) - val nextData = DATA_CLOSING(d.commitments, fundingTx = None, waitingSince = nodeParams.currentBlockHeight, Nil, futureRemoteCommitPublished = Some(remoteCommitPublished)) - goto(CLOSING) using nextData storing() calling doPublish(remoteCommitPublished, d.commitments) - } - - private def handleRemoteSpentNext(commitTx: Transaction, d: HasCommitments) = { - log.warning(s"they published their next commit in txid=${commitTx.txid}") - require(d.commitments.remoteNextCommitInfo.isLeft, "next remote commit must be defined") - val Left(waitingForRevocation) = d.commitments.remoteNextCommitInfo - val remoteCommit = waitingForRevocation.nextRemoteCommit - require(commitTx.txid == remoteCommit.txid, "txid mismatch") - - context.system.eventStream.publish(TransactionPublished(d.channelId, remoteNodeId, commitTx, Closing.commitTxFee(d.commitments.commitInput, commitTx, d.commitments.localParams.isFunder), "next-remote-commit")) - val remoteCommitPublished = Closing.RemoteClose.claimCommitTxOutputs(keyManager, d.commitments, remoteCommit, commitTx, nodeParams.currentBlockHeight, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) - val nextData = d match { - case closing: DATA_CLOSING => closing.copy(nextRemoteCommitPublished = Some(remoteCommitPublished)) - case negotiating: DATA_NEGOTIATING => DATA_CLOSING(d.commitments, fundingTx = None, waitingSince = nodeParams.currentBlockHeight, negotiating.closingTxProposed.flatten.map(_.unsignedTx), nextRemoteCommitPublished = Some(remoteCommitPublished)) - // NB: if there is a next commitment, we can't be in DATA_WAIT_FOR_FUNDING_CONFIRMED so we don't have the case where fundingTx is defined - case _ => DATA_CLOSING(d.commitments, fundingTx = None, waitingSince = nodeParams.currentBlockHeight, mutualCloseProposed = Nil, nextRemoteCommitPublished = Some(remoteCommitPublished)) - } - goto(CLOSING) using nextData storing() calling doPublish(remoteCommitPublished, d.commitments) - } - - private def doPublish(remoteCommitPublished: RemoteCommitPublished, commitments: Commitments): Unit = { - import remoteCommitPublished._ - - val redeemableHtlcTxs = claimHtlcTxs.values.flatten.map(tx => PublishReplaceableTx(tx, commitments)) - val publishQueue = claimMainOutputTx.map(tx => PublishFinalTx(tx, tx.fee, None)).toSeq ++ redeemableHtlcTxs - publishIfNeeded(publishQueue, irrevocablySpent) - - // we watch: - // - the commitment tx itself, so that we can handle the case where we don't have any outputs - // - 'final txs' that send funds to our wallet and that spend outputs that only us control - val watchConfirmedQueue = List(commitTx) ++ claimMainOutputTx.map(_.tx) - watchConfirmedIfNeeded(watchConfirmedQueue, irrevocablySpent) - - // we watch outputs of the commitment tx that both parties may spend - val watchSpentQueue = claimHtlcTxs.keys - watchSpentIfNeeded(commitTx, watchSpentQueue, irrevocablySpent) - } - - private def handleRemoteSpentOther(tx: Transaction, d: HasCommitments) = { - log.warning(s"funding tx spent in txid=${tx.txid}") - Closing.RevokedClose.claimCommitTxOutputs(keyManager, d.commitments, tx, nodeParams.db.channels, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) match { - case Some(revokedCommitPublished) => - log.warning(s"txid=${tx.txid} was a revoked commitment, publishing the penalty tx") - context.system.eventStream.publish(TransactionPublished(d.channelId, remoteNodeId, tx, Closing.commitTxFee(d.commitments.commitInput, tx, d.commitments.localParams.isFunder), "revoked-commit")) - val exc = FundingTxSpent(d.channelId, tx) - val error = Error(d.channelId, exc.getMessage) - - val nextData = d match { - case closing: DATA_CLOSING => closing.copy(revokedCommitPublished = closing.revokedCommitPublished :+ revokedCommitPublished) - case negotiating: DATA_NEGOTIATING => DATA_CLOSING(d.commitments, fundingTx = None, waitingSince = nodeParams.currentBlockHeight, negotiating.closingTxProposed.flatten.map(_.unsignedTx), revokedCommitPublished = revokedCommitPublished :: Nil) - // NB: if there is a revoked commitment, we can't be in DATA_WAIT_FOR_FUNDING_CONFIRMED so we don't have the case where fundingTx is defined - case _ => DATA_CLOSING(d.commitments, fundingTx = None, waitingSince = nodeParams.currentBlockHeight, mutualCloseProposed = Nil, revokedCommitPublished = revokedCommitPublished :: Nil) - } - goto(CLOSING) using nextData storing() calling doPublish(revokedCommitPublished) sending error - case None => - // the published tx was neither their current commitment nor a revoked one - log.error(s"couldn't identify txid=${tx.txid}, something very bad is going on!!!") - context.system.eventStream.publish(NotifyNodeOperator(NotificationsLogger.Error, s"funding tx ${d.commitments.commitInput.outPoint.txid} of channel ${d.channelId} was spent by an unknown transaction, indicating that your DB has lost data or your node has been breached: please contact the dev team.")) - goto(ERR_INFORMATION_LEAK) - } - } - - private def doPublish(revokedCommitPublished: RevokedCommitPublished): Unit = { - import revokedCommitPublished._ - - val publishQueue = (claimMainOutputTx ++ mainPenaltyTx ++ htlcPenaltyTxs ++ claimHtlcDelayedPenaltyTxs).map(tx => PublishFinalTx(tx, tx.fee, None)) - publishIfNeeded(publishQueue, irrevocablySpent) - - // we watch: - // - the commitment tx itself, so that we can handle the case where we don't have any outputs - // - 'final txs' that send funds to our wallet and that spend outputs that only us control - val watchConfirmedQueue = List(commitTx) ++ claimMainOutputTx.map(_.tx) - watchConfirmedIfNeeded(watchConfirmedQueue, irrevocablySpent) - - // we watch outputs of the commitment tx that both parties may spend - val watchSpentQueue = (mainPenaltyTx ++ htlcPenaltyTxs).map(_.input.outPoint) - watchSpentIfNeeded(commitTx, watchSpentQueue, irrevocablySpent) - } - - private def handleInformationLeak(tx: Transaction, d: HasCommitments) = { - // this is never supposed to happen !! - log.error(s"our funding tx ${d.commitments.commitInput.outPoint.txid} was spent by txid=${tx.txid}!!") - context.system.eventStream.publish(NotifyNodeOperator(NotificationsLogger.Error, s"funding tx ${d.commitments.commitInput.outPoint.txid} of channel ${d.channelId} was spent by an unknown transaction, indicating that your DB has lost data or your node has been breached: please contact the dev team.")) - val exc = FundingTxSpent(d.channelId, tx) - val error = Error(d.channelId, exc.getMessage) - - // let's try to spend our current local tx - val commitTx = d.commitments.fullySignedLocalCommitTx(keyManager).tx - val localCommitPublished = Closing.LocalClose.claimCommitTxOutputs(keyManager, d.commitments, commitTx, nodeParams.currentBlockHeight, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) - - goto(ERR_INFORMATION_LEAK) calling doPublish(localCommitPublished, d.commitments) sending error - } - - private def handleOutdatedCommitment(channelReestablish: ChannelReestablish, d: HasCommitments) = { - val exc = PleasePublishYourCommitment(d.channelId) - val error = Error(d.channelId, exc.getMessage) - goto(WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT) using DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT(d.commitments, channelReestablish) storing() sending error - } - - /** - * This helper function runs the state's default event handlers, and react to exceptions by unilaterally closing the channel - */ - private def handleExceptions(s: StateFunction): StateFunction = { - case event if s.isDefinedAt(event) => - try { - s(event) - } catch { - case t: SQLException => - log.error(t, "fatal database error\n") - NotificationsLogger.logFatalError("eclair is shutting down because of a fatal database error", t) - sys.exit(1) - case t: Throwable => handleLocalError(t, event.stateData, None) - } - } - - implicit private def state2mystate(state: FSM.State[ChannelState, ChannelData]): MyState = MyState(state) - - case class MyState(state: FSM.State[ChannelState, ChannelData]) { - - def storing(unused: Unit = ()): FSM.State[ChannelState, ChannelData] = { - state.stateData match { - case d: HasCommitments => - log.debug("updating database record for channelId={}", d.channelId) - nodeParams.db.channels.addOrUpdateChannel(d) - context.system.eventStream.publish(ChannelPersisted(self, remoteNodeId, d.channelId, d)) - state - case _ => - log.error(s"can't store data=${state.stateData} in state=${state.stateName}") - state - } - } - - def sending(msgs: Seq[LightningMessage]): FSM.State[ChannelState, ChannelData] = { - msgs.foreach(sending) - state - } - - def sending(msg: LightningMessage): FSM.State[ChannelState, ChannelData] = { - send(msg) - state - } - - /** - * This method allows performing actions during the transition, e.g. after a call to [[MyState.storing]]. This is - * particularly useful to publish transactions only after we are sure that the state has been persisted. - */ - def calling(f: => Unit): FSM.State[ChannelState, ChannelData] = { - f - state - } - - /** - * We don't acknowledge htlc commands immediately, because we send them to the channel as soon as possible, and they - * may not yet have been written to the database. - * - * @param cmd fail/fulfill command that has been processed - */ - def acking(channelId: ByteVector32, cmd: HtlcSettlementCommand): FSM.State[ChannelState, ChannelData] = { - log.debug("scheduling acknowledgement of cmd id={}", cmd.id) - context.system.scheduler.scheduleOnce(10 seconds)(PendingCommandsDb.ackSettlementCommand(nodeParams.db.pendingCommands, channelId, cmd))(context.system.dispatcher) - state - } - - def acking(updates: List[UpdateMessage]): FSM.State[ChannelState, ChannelData] = { - log.debug("scheduling acknowledgement of cmds ids={}", updates.collect { case s: HtlcSettlementMessage => s.id }.mkString(",")) - context.system.scheduler.scheduleOnce(10 seconds)(PendingCommandsDb.ackSettlementCommands(nodeParams.db.pendingCommands, updates))(context.system.dispatcher) - state - } - - } - - private def send(msg: LightningMessage): Unit = { - peer ! Peer.OutgoingMessage(msg, activeConnection) - } - override def mdc(currentMessage: Any): MDC = { val category_opt = LogCategory(currentMessage) val id = currentMessage match { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenSingleFunder.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenSingleFunder.scala new file mode 100644 index 0000000000..dfbac9ad8c --- /dev/null +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenSingleFunder.scala @@ -0,0 +1,401 @@ +/* + * 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.channel.fsm + +import akka.actor.Status +import akka.actor.typed.scaladsl.adapter.actorRefAdapter +import akka.pattern.pipe +import fr.acinq.bitcoin.{SatoshiLong, Script, ScriptFlags, Transaction} +import fr.acinq.eclair.blockchain.OnChainWallet.MakeFundingTxResponse +import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._ +import fr.acinq.eclair.channel.Helpers.{Funding, getRelayFees} +import fr.acinq.eclair.channel._ +import fr.acinq.eclair.channel.fsm.Channel._ +import fr.acinq.eclair.channel.publish.TxPublisher.SetChannelId +import fr.acinq.eclair.crypto.ShaChain +import fr.acinq.eclair.router.Announcements +import fr.acinq.eclair.transactions.Transactions.TxOwner +import fr.acinq.eclair.transactions.{Scripts, Transactions} +import fr.acinq.eclair.wire.protocol.{AcceptChannel, AnnouncementSignatures, ChannelTlv, Error, FundingCreated, FundingLocked, FundingSigned, OpenChannel, TlvStream} +import fr.acinq.eclair.{Features, ShortChannelId, ToMilliSatoshiConversion, randomKey, toLongId} +import scodec.bits.ByteVector + +import scala.concurrent.duration.DurationInt +import scala.util.{Failure, Success, Try} + +/** + * Created by t-bast on 28/03/2022. + */ + +/** + * This trait contains the state machine for the single-funder channel funding flow. + */ +trait ChannelOpenSingleFunder extends FundingHandlers with ErrorHandlers { + + this: Channel => + + when(WAIT_FOR_OPEN_CHANNEL)(handleExceptions { + case Event(open: OpenChannel, d@DATA_WAIT_FOR_OPEN_CHANNEL(INPUT_INIT_FUNDEE(_, localParams, _, remoteInit, channelConfig, channelType))) => + Helpers.validateParamsFundee(nodeParams, channelType, localParams.initFeatures, open, remoteNodeId, remoteInit.features) match { + case Left(t) => handleLocalError(t, d, Some(open)) + case Right((channelFeatures, remoteShutdownScript)) => + context.system.eventStream.publish(ChannelCreated(self, peer, remoteNodeId, isFunder = false, open.temporaryChannelId, open.feeratePerKw, None)) + val fundingPubkey = keyManager.fundingPublicKey(localParams.fundingKeyPath).publicKey + val channelKeyPath = keyManager.keyPath(localParams, channelConfig) + val minimumDepth = Helpers.minDepthForFunding(nodeParams.channelConf, open.fundingSatoshis) + // In order to allow TLV extensions and keep backwards-compatibility, we include an empty upfront_shutdown_script if this feature is not used. + // See https://github.com/lightningnetwork/lightning-rfc/pull/714. + val localShutdownScript = if (Features.canUseFeature(localParams.initFeatures, remoteInit.features, Features.UpfrontShutdownScript)) localParams.defaultFinalScriptPubKey else ByteVector.empty + val accept = AcceptChannel(temporaryChannelId = open.temporaryChannelId, + dustLimitSatoshis = localParams.dustLimit, + maxHtlcValueInFlightMsat = localParams.maxHtlcValueInFlightMsat, + channelReserveSatoshis = localParams.channelReserve, + minimumDepth = minimumDepth, + htlcMinimumMsat = localParams.htlcMinimum, + toSelfDelay = localParams.toSelfDelay, + maxAcceptedHtlcs = localParams.maxAcceptedHtlcs, + fundingPubkey = fundingPubkey, + revocationBasepoint = keyManager.revocationPoint(channelKeyPath).publicKey, + paymentBasepoint = localParams.walletStaticPaymentBasepoint.getOrElse(keyManager.paymentPoint(channelKeyPath).publicKey), + delayedPaymentBasepoint = keyManager.delayedPaymentPoint(channelKeyPath).publicKey, + htlcBasepoint = keyManager.htlcPoint(channelKeyPath).publicKey, + firstPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, 0), + tlvStream = TlvStream( + ChannelTlv.UpfrontShutdownScriptTlv(localShutdownScript), + ChannelTlv.ChannelTypeTlv(channelType) + )) + val remoteParams = RemoteParams( + nodeId = remoteNodeId, + dustLimit = open.dustLimitSatoshis, + maxHtlcValueInFlightMsat = open.maxHtlcValueInFlightMsat, + channelReserve = open.channelReserveSatoshis, // remote requires local to keep this much satoshis as direct payment + htlcMinimum = open.htlcMinimumMsat, + toSelfDelay = open.toSelfDelay, + maxAcceptedHtlcs = open.maxAcceptedHtlcs, + fundingPubKey = open.fundingPubkey, + revocationBasepoint = open.revocationBasepoint, + paymentBasepoint = open.paymentBasepoint, + delayedPaymentBasepoint = open.delayedPaymentBasepoint, + htlcBasepoint = open.htlcBasepoint, + initFeatures = remoteInit.features, + shutdownScript = remoteShutdownScript) + log.debug("remote params: {}", remoteParams) + goto(WAIT_FOR_FUNDING_CREATED) using DATA_WAIT_FOR_FUNDING_CREATED(open.temporaryChannelId, localParams, remoteParams, open.fundingSatoshis, open.pushMsat, open.feeratePerKw, open.firstPerCommitmentPoint, open.channelFlags, channelConfig, channelFeatures, accept) sending accept + } + + case Event(c: CloseCommand, d) => handleFastClose(c, d.channelId) + + case Event(e: Error, d: DATA_WAIT_FOR_OPEN_CHANNEL) => handleRemoteError(e, d) + + case Event(INPUT_DISCONNECTED, _) => goto(CLOSED) + }) + + when(WAIT_FOR_ACCEPT_CHANNEL)(handleExceptions { + case Event(accept: AcceptChannel, d@DATA_WAIT_FOR_ACCEPT_CHANNEL(INPUT_INIT_FUNDER(temporaryChannelId, fundingSatoshis, pushMsat, initialFeeratePerKw, fundingTxFeeratePerKw, localParams, _, remoteInit, _, channelConfig, channelType), open)) => + Helpers.validateParamsFunder(nodeParams, channelType, localParams.initFeatures, remoteInit.features, open, accept) match { + case Left(t) => + channelOpenReplyToUser(Left(LocalError(t))) + handleLocalError(t, d, Some(accept)) + case Right((channelFeatures, remoteShutdownScript)) => + val remoteParams = RemoteParams( + nodeId = remoteNodeId, + dustLimit = accept.dustLimitSatoshis, + maxHtlcValueInFlightMsat = accept.maxHtlcValueInFlightMsat, + channelReserve = accept.channelReserveSatoshis, // remote requires local to keep this much satoshis as direct payment + htlcMinimum = accept.htlcMinimumMsat, + toSelfDelay = accept.toSelfDelay, + maxAcceptedHtlcs = accept.maxAcceptedHtlcs, + fundingPubKey = accept.fundingPubkey, + revocationBasepoint = accept.revocationBasepoint, + paymentBasepoint = accept.paymentBasepoint, + delayedPaymentBasepoint = accept.delayedPaymentBasepoint, + htlcBasepoint = accept.htlcBasepoint, + initFeatures = remoteInit.features, + shutdownScript = remoteShutdownScript) + log.debug("remote params: {}", remoteParams) + val localFundingPubkey = keyManager.fundingPublicKey(localParams.fundingKeyPath) + val fundingPubkeyScript = Script.write(Script.pay2wsh(Scripts.multiSig2of2(localFundingPubkey.publicKey, remoteParams.fundingPubKey))) + wallet.makeFundingTx(fundingPubkeyScript, fundingSatoshis, fundingTxFeeratePerKw).pipeTo(self) + goto(WAIT_FOR_FUNDING_INTERNAL) using DATA_WAIT_FOR_FUNDING_INTERNAL(temporaryChannelId, localParams, remoteParams, fundingSatoshis, pushMsat, initialFeeratePerKw, accept.firstPerCommitmentPoint, channelConfig, channelFeatures, open) + } + + case Event(c: CloseCommand, d: DATA_WAIT_FOR_ACCEPT_CHANNEL) => + channelOpenReplyToUser(Right(ChannelOpenResponse.ChannelClosed(d.lastSent.temporaryChannelId))) + handleFastClose(c, d.lastSent.temporaryChannelId) + + case Event(e: Error, d: DATA_WAIT_FOR_ACCEPT_CHANNEL) => + channelOpenReplyToUser(Left(RemoteError(e))) + handleRemoteError(e, d) + + case Event(INPUT_DISCONNECTED, _) => + channelOpenReplyToUser(Left(LocalError(new RuntimeException("disconnected")))) + goto(CLOSED) + + case Event(TickChannelOpenTimeout, _) => + channelOpenReplyToUser(Left(LocalError(new RuntimeException("open channel cancelled, took too long")))) + goto(CLOSED) + }) + + when(WAIT_FOR_FUNDING_INTERNAL)(handleExceptions { + case Event(MakeFundingTxResponse(fundingTx, fundingTxOutputIndex, fundingTxFee), d@DATA_WAIT_FOR_FUNDING_INTERNAL(temporaryChannelId, localParams, remoteParams, fundingAmount, pushMsat, initialFeeratePerKw, remoteFirstPerCommitmentPoint, channelConfig, channelFeatures, open)) => + // let's create the first commitment tx that spends the yet uncommitted funding tx + Funding.makeFirstCommitTxs(keyManager, channelConfig, channelFeatures, temporaryChannelId, localParams, remoteParams, fundingAmount, pushMsat, initialFeeratePerKw, fundingTx.hash, fundingTxOutputIndex, remoteFirstPerCommitmentPoint) match { + case Left(ex) => handleLocalError(ex, d, None) + case Right((localSpec, localCommitTx, remoteSpec, remoteCommitTx)) => + require(fundingTx.txOut(fundingTxOutputIndex).publicKeyScript == localCommitTx.input.txOut.publicKeyScript, s"pubkey script mismatch!") + val localSigOfRemoteTx = keyManager.sign(remoteCommitTx, keyManager.fundingPublicKey(localParams.fundingKeyPath), TxOwner.Remote, channelFeatures.commitmentFormat) + // signature of their initial commitment tx that pays remote pushMsat + val fundingCreated = FundingCreated( + temporaryChannelId = temporaryChannelId, + fundingTxid = fundingTx.hash, + fundingOutputIndex = fundingTxOutputIndex, + signature = localSigOfRemoteTx + ) + val channelId = toLongId(fundingTx.hash, fundingTxOutputIndex) + peer ! ChannelIdAssigned(self, remoteNodeId, temporaryChannelId, channelId) // we notify the peer asap so it knows how to route messages + txPublisher ! SetChannelId(remoteNodeId, channelId) + context.system.eventStream.publish(ChannelIdAssigned(self, remoteNodeId, temporaryChannelId, channelId)) + // NB: we don't send a ChannelSignatureSent for the first commit + goto(WAIT_FOR_FUNDING_SIGNED) using DATA_WAIT_FOR_FUNDING_SIGNED(channelId, localParams, remoteParams, fundingTx, fundingTxFee, localSpec, localCommitTx, RemoteCommit(0, remoteSpec, remoteCommitTx.tx.txid, remoteFirstPerCommitmentPoint), open.channelFlags, channelConfig, channelFeatures, fundingCreated) sending fundingCreated + } + + case Event(Status.Failure(t), d: DATA_WAIT_FOR_FUNDING_INTERNAL) => + log.error(t, s"wallet returned error: ") + channelOpenReplyToUser(Left(LocalError(t))) + handleLocalError(ChannelFundingError(d.temporaryChannelId), d, None) // we use a generic exception and don't send the internal error to the peer + + case Event(c: CloseCommand, d: DATA_WAIT_FOR_FUNDING_INTERNAL) => + channelOpenReplyToUser(Right(ChannelOpenResponse.ChannelClosed(d.temporaryChannelId))) + handleFastClose(c, d.temporaryChannelId) + + case Event(e: Error, d: DATA_WAIT_FOR_FUNDING_INTERNAL) => + channelOpenReplyToUser(Left(RemoteError(e))) + handleRemoteError(e, d) + + case Event(INPUT_DISCONNECTED, _) => + channelOpenReplyToUser(Left(LocalError(new RuntimeException("disconnected")))) + goto(CLOSED) + + case Event(TickChannelOpenTimeout, _) => + channelOpenReplyToUser(Left(LocalError(new RuntimeException("open channel cancelled, took too long")))) + goto(CLOSED) + }) + + when(WAIT_FOR_FUNDING_CREATED)(handleExceptions { + case Event(FundingCreated(_, fundingTxHash, fundingTxOutputIndex, remoteSig, _), d@DATA_WAIT_FOR_FUNDING_CREATED(temporaryChannelId, localParams, remoteParams, fundingAmount, pushMsat, initialFeeratePerKw, remoteFirstPerCommitmentPoint, channelFlags, channelConfig, channelFeatures, _)) => + // they fund the channel with their funding tx, so the money is theirs (but we are paid pushMsat) + Funding.makeFirstCommitTxs(keyManager, channelConfig, channelFeatures, temporaryChannelId, localParams, remoteParams, fundingAmount, pushMsat, initialFeeratePerKw, fundingTxHash, fundingTxOutputIndex, remoteFirstPerCommitmentPoint) match { + case Left(ex) => handleLocalError(ex, d, None) + case Right((localSpec, localCommitTx, remoteSpec, remoteCommitTx)) => + // check remote signature validity + val fundingPubKey = keyManager.fundingPublicKey(localParams.fundingKeyPath) + val localSigOfLocalTx = keyManager.sign(localCommitTx, fundingPubKey, TxOwner.Local, channelFeatures.commitmentFormat) + val signedLocalCommitTx = Transactions.addSigs(localCommitTx, fundingPubKey.publicKey, remoteParams.fundingPubKey, localSigOfLocalTx, remoteSig) + Transactions.checkSpendable(signedLocalCommitTx) match { + case Failure(_) => handleLocalError(InvalidCommitmentSignature(temporaryChannelId, signedLocalCommitTx.tx), d, None) + case Success(_) => + val localSigOfRemoteTx = keyManager.sign(remoteCommitTx, fundingPubKey, TxOwner.Remote, channelFeatures.commitmentFormat) + val channelId = toLongId(fundingTxHash, fundingTxOutputIndex) + // watch the funding tx transaction + val commitInput = localCommitTx.input + val fundingSigned = FundingSigned( + channelId = channelId, + signature = localSigOfRemoteTx + ) + val commitments = Commitments(channelId, channelConfig, channelFeatures, localParams, remoteParams, channelFlags, + LocalCommit(0, localSpec, CommitTxAndRemoteSig(localCommitTx, remoteSig), htlcTxsAndRemoteSigs = Nil), RemoteCommit(0, remoteSpec, remoteCommitTx.tx.txid, remoteFirstPerCommitmentPoint), + LocalChanges(Nil, Nil, Nil), RemoteChanges(Nil, Nil, Nil), + localNextHtlcId = 0L, remoteNextHtlcId = 0L, + originChannels = Map.empty, + remoteNextCommitInfo = Right(randomKey().publicKey), // we will receive their next per-commitment point in the next message, so we temporarily put a random byte array, + commitInput, ShaChain.init) + peer ! ChannelIdAssigned(self, remoteNodeId, temporaryChannelId, channelId) // we notify the peer asap so it knows how to route messages + txPublisher ! SetChannelId(remoteNodeId, channelId) + context.system.eventStream.publish(ChannelIdAssigned(self, remoteNodeId, temporaryChannelId, channelId)) + context.system.eventStream.publish(ChannelSignatureReceived(self, commitments)) + // NB: we don't send a ChannelSignatureSent for the first commit + log.info(s"waiting for them to publish the funding tx for channelId=$channelId fundingTxid=${commitInput.outPoint.txid}") + watchFundingTx(commitments) + val fundingMinDepth = Helpers.minDepthForFunding(nodeParams.channelConf, fundingAmount) + blockchain ! WatchFundingConfirmed(self, commitInput.outPoint.txid, fundingMinDepth) + goto(WAIT_FOR_FUNDING_CONFIRMED) using DATA_WAIT_FOR_FUNDING_CONFIRMED(commitments, None, nodeParams.currentBlockHeight, None, Right(fundingSigned)) storing() sending fundingSigned + } + } + + case Event(c: CloseCommand, d: DATA_WAIT_FOR_FUNDING_CREATED) => + channelOpenReplyToUser(Right(ChannelOpenResponse.ChannelClosed(d.temporaryChannelId))) + handleFastClose(c, d.temporaryChannelId) + + case Event(e: Error, d: DATA_WAIT_FOR_FUNDING_CREATED) => handleRemoteError(e, d) + + case Event(INPUT_DISCONNECTED, _) => goto(CLOSED) + }) + + when(WAIT_FOR_FUNDING_SIGNED)(handleExceptions { + case Event(msg@FundingSigned(_, remoteSig, _), d@DATA_WAIT_FOR_FUNDING_SIGNED(channelId, localParams, remoteParams, fundingTx, fundingTxFee, localSpec, localCommitTx, remoteCommit, channelFlags, channelConfig, channelFeatures, fundingCreated)) => + // we make sure that their sig checks out and that our first commit tx is spendable + val fundingPubKey = keyManager.fundingPublicKey(localParams.fundingKeyPath) + val localSigOfLocalTx = keyManager.sign(localCommitTx, fundingPubKey, TxOwner.Local, channelFeatures.commitmentFormat) + val signedLocalCommitTx = Transactions.addSigs(localCommitTx, fundingPubKey.publicKey, remoteParams.fundingPubKey, localSigOfLocalTx, remoteSig) + Transactions.checkSpendable(signedLocalCommitTx) match { + case Failure(cause) => + // we rollback the funding tx, it will never be published + wallet.rollback(fundingTx) + channelOpenReplyToUser(Left(LocalError(cause))) + handleLocalError(InvalidCommitmentSignature(channelId, signedLocalCommitTx.tx), d, Some(msg)) + case Success(_) => + val commitInput = localCommitTx.input + val commitments = Commitments(channelId, channelConfig, channelFeatures, localParams, remoteParams, channelFlags, + LocalCommit(0, localSpec, CommitTxAndRemoteSig(localCommitTx, remoteSig), htlcTxsAndRemoteSigs = Nil), remoteCommit, + LocalChanges(Nil, Nil, Nil), RemoteChanges(Nil, Nil, Nil), + localNextHtlcId = 0L, remoteNextHtlcId = 0L, + originChannels = Map.empty, + remoteNextCommitInfo = Right(randomKey().publicKey), // we will receive their next per-commitment point in the next message, so we temporarily put a random byte array + commitInput, ShaChain.init) + val blockHeight = nodeParams.currentBlockHeight + context.system.eventStream.publish(ChannelSignatureReceived(self, commitments)) + log.info(s"publishing funding tx for channelId=$channelId fundingTxid=${commitInput.outPoint.txid}") + watchFundingTx(commitments) + blockchain ! WatchFundingConfirmed(self, commitInput.outPoint.txid, nodeParams.channelConf.minDepthBlocks) + log.info(s"committing txid=${fundingTx.txid}") + + // we will publish the funding tx only after the channel state has been written to disk because we want to + // make sure we first persist the commitment that returns back the funds to us in case of problem + def publishFundingTx(): Unit = { + wallet.commit(fundingTx).onComplete { + case Success(true) => + context.system.eventStream.publish(TransactionPublished(commitments.channelId, remoteNodeId, fundingTx, fundingTxFee, "funding")) + channelOpenReplyToUser(Right(ChannelOpenResponse.ChannelOpened(channelId))) + case Success(false) => + channelOpenReplyToUser(Left(LocalError(new RuntimeException("couldn't publish funding tx")))) + self ! BITCOIN_FUNDING_PUBLISH_FAILED // fail-fast: this should be returned only when we are really sure the tx has *not* been published + case Failure(t) => + channelOpenReplyToUser(Left(LocalError(t))) + log.error(t, s"error while committing funding tx: ") // tx may still have been published, can't fail-fast + } + } + + goto(WAIT_FOR_FUNDING_CONFIRMED) using DATA_WAIT_FOR_FUNDING_CONFIRMED(commitments, Some(fundingTx), blockHeight, None, Left(fundingCreated)) storing() calling publishFundingTx() + } + + case Event(c: CloseCommand, d: DATA_WAIT_FOR_FUNDING_SIGNED) => + // we rollback the funding tx, it will never be published + wallet.rollback(d.fundingTx) + channelOpenReplyToUser(Right(ChannelOpenResponse.ChannelClosed(d.channelId))) + handleFastClose(c, d.channelId) + + case Event(e: Error, d: DATA_WAIT_FOR_FUNDING_SIGNED) => + // we rollback the funding tx, it will never be published + wallet.rollback(d.fundingTx) + channelOpenReplyToUser(Left(RemoteError(e))) + handleRemoteError(e, d) + + case Event(INPUT_DISCONNECTED, d: DATA_WAIT_FOR_FUNDING_SIGNED) => + // we rollback the funding tx, it will never be published + wallet.rollback(d.fundingTx) + channelOpenReplyToUser(Left(LocalError(new RuntimeException("disconnected")))) + goto(CLOSED) + + case Event(TickChannelOpenTimeout, d: DATA_WAIT_FOR_FUNDING_SIGNED) => + // we rollback the funding tx, it will never be published + wallet.rollback(d.fundingTx) + channelOpenReplyToUser(Left(LocalError(new RuntimeException("open channel cancelled, took too long")))) + goto(CLOSED) + }) + + when(WAIT_FOR_FUNDING_CONFIRMED)(handleExceptions { + case Event(msg: FundingLocked, d: DATA_WAIT_FOR_FUNDING_CONFIRMED) => + log.info(s"received their FundingLocked, deferring message") + stay() using d.copy(deferred = Some(msg)) // no need to store, they will re-send if we get disconnected + + case Event(WatchFundingConfirmedTriggered(blockHeight, txIndex, fundingTx), d@DATA_WAIT_FOR_FUNDING_CONFIRMED(commitments, _, _, deferred, _)) => + Try(Transaction.correctlySpends(commitments.fullySignedLocalCommitTx(keyManager).tx, Seq(fundingTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)) match { + case Success(_) => + log.info(s"channelId=${commitments.channelId} was confirmed at blockHeight=$blockHeight txIndex=$txIndex") + blockchain ! WatchFundingLost(self, commitments.commitInput.outPoint.txid, nodeParams.channelConf.minDepthBlocks) + if (!d.commitments.localParams.isFunder) context.system.eventStream.publish(TransactionPublished(commitments.channelId, remoteNodeId, fundingTx, 0 sat, "funding")) + context.system.eventStream.publish(TransactionConfirmed(commitments.channelId, remoteNodeId, fundingTx)) + val channelKeyPath = keyManager.keyPath(d.commitments.localParams, commitments.channelConfig) + val nextPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, 1) + val fundingLocked = FundingLocked(commitments.channelId, nextPerCommitmentPoint) + deferred.foreach(self ! _) + // this is the temporary channel id that we will use in our channel_update message, the goal is to be able to use our channel + // as soon as it reaches NORMAL state, and before it is announced on the network + // (this id might be updated when the funding tx gets deeply buried, if there was a reorg in the meantime) + val shortChannelId = ShortChannelId(blockHeight, txIndex, commitments.commitInput.outPoint.index.toInt) + goto(WAIT_FOR_FUNDING_LOCKED) using DATA_WAIT_FOR_FUNDING_LOCKED(commitments, shortChannelId, fundingLocked) storing() sending fundingLocked + case Failure(t) => + log.error(t, s"rejecting channel with invalid funding tx: ${fundingTx.bin}") + goto(CLOSED) + } + + case Event(remoteAnnSigs: AnnouncementSignatures, d: DATA_WAIT_FOR_FUNDING_CONFIRMED) if d.commitments.announceChannel => + log.debug("received remote announcement signatures, delaying") + // we may receive their announcement sigs before our watcher notifies us that the channel has reached min_conf (especially during testing when blocks are generated in bulk) + // note: no need to persist their message, in case of disconnection they will resend it + context.system.scheduler.scheduleOnce(2 seconds, self, remoteAnnSigs) + stay() + + case Event(getTxResponse: GetTxWithMetaResponse, d: DATA_WAIT_FOR_FUNDING_CONFIRMED) if getTxResponse.txid == d.commitments.commitInput.outPoint.txid => handleGetFundingTx(getTxResponse, d.waitingSince, d.fundingTx) + + case Event(BITCOIN_FUNDING_PUBLISH_FAILED, d: DATA_WAIT_FOR_FUNDING_CONFIRMED) => handleFundingPublishFailed(d) + + case Event(ProcessCurrentBlockHeight(c), d: DATA_WAIT_FOR_FUNDING_CONFIRMED) => d.fundingTx match { + case Some(_) => stay() // we are funder, we're still waiting for the funding tx to be confirmed + case None if c.blockHeight - d.waitingSince > FUNDING_TIMEOUT_FUNDEE => + log.warning(s"funding tx hasn't been published in ${c.blockHeight - d.waitingSince} blocks") + self ! BITCOIN_FUNDING_TIMEOUT + stay() + case None => stay() // let's wait longer + } + + case Event(BITCOIN_FUNDING_TIMEOUT, d: DATA_WAIT_FOR_FUNDING_CONFIRMED) => handleFundingTimeout(d) + + case Event(WatchFundingSpentTriggered(tx), d: DATA_WAIT_FOR_FUNDING_CONFIRMED) if tx.txid == d.commitments.remoteCommit.txid => handleRemoteSpentCurrent(tx, d) + + case Event(WatchFundingSpentTriggered(tx), d: DATA_WAIT_FOR_FUNDING_CONFIRMED) => handleInformationLeak(tx, d) + + case Event(e: Error, d: DATA_WAIT_FOR_FUNDING_CONFIRMED) => handleRemoteError(e, d) + }) + + when(WAIT_FOR_FUNDING_LOCKED)(handleExceptions { + case Event(FundingLocked(_, nextPerCommitmentPoint, _), d@DATA_WAIT_FOR_FUNDING_LOCKED(commitments, shortChannelId, _)) => + // used to get the final shortChannelId, used in announcements (if minDepth >= ANNOUNCEMENTS_MINCONF this event will fire instantly) + blockchain ! WatchFundingDeeplyBuried(self, commitments.commitInput.outPoint.txid, ANNOUNCEMENTS_MINCONF) + context.system.eventStream.publish(ShortChannelIdAssigned(self, commitments.channelId, shortChannelId, None)) + // we create a channel_update early so that we can use it to send payments through this channel, but it won't be propagated to other nodes since the channel is not yet announced + val fees = getRelayFees(nodeParams, remoteNodeId, commitments) + val initialChannelUpdate = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, shortChannelId, nodeParams.channelConf.expiryDelta, d.commitments.remoteParams.htlcMinimum, fees.feeBase, fees.feeProportionalMillionths, commitments.capacity.toMilliSatoshi, enable = Helpers.aboveReserve(d.commitments)) + // we need to periodically re-send channel updates, otherwise channel will be considered stale and get pruned by network + context.system.scheduler.scheduleWithFixedDelay(initialDelay = REFRESH_CHANNEL_UPDATE_INTERVAL, delay = REFRESH_CHANNEL_UPDATE_INTERVAL, receiver = self, message = BroadcastChannelUpdate(PeriodicRefresh)) + goto(NORMAL) using DATA_NORMAL(commitments.copy(remoteNextCommitInfo = Right(nextPerCommitmentPoint)), shortChannelId, buried = false, None, initialChannelUpdate, None, None, None) storing() + + case Event(remoteAnnSigs: AnnouncementSignatures, d: DATA_WAIT_FOR_FUNDING_LOCKED) if d.commitments.announceChannel => + log.debug("received remote announcement signatures, delaying") + // we may receive their announcement sigs before our watcher notifies us that the channel has reached min_conf (especially during testing when blocks are generated in bulk) + // note: no need to persist their message, in case of disconnection they will resend it + context.system.scheduler.scheduleOnce(2 seconds, self, remoteAnnSigs) + stay() + + case Event(WatchFundingSpentTriggered(tx), d: DATA_WAIT_FOR_FUNDING_LOCKED) if tx.txid == d.commitments.remoteCommit.txid => handleRemoteSpentCurrent(tx, d) + + case Event(WatchFundingSpentTriggered(tx), d: DATA_WAIT_FOR_FUNDING_LOCKED) => handleInformationLeak(tx, d) + + case Event(e: Error, d: DATA_WAIT_FOR_FUNDING_LOCKED) => handleRemoteError(e, d) + }) + +} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/CommonHandlers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/CommonHandlers.scala new file mode 100644 index 0000000000..87e9416c5a --- /dev/null +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/CommonHandlers.scala @@ -0,0 +1,102 @@ +/* + * 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.channel.fsm + +import akka.actor.FSM +import fr.acinq.bitcoin.ByteVector32 +import fr.acinq.eclair.channel._ +import fr.acinq.eclair.db.PendingCommandsDb +import fr.acinq.eclair.io.Peer +import fr.acinq.eclair.wire.protocol.{HtlcSettlementMessage, LightningMessage, UpdateMessage} + +import scala.concurrent.duration.DurationInt + +/** + * Created by t-bast on 28/03/2022. + */ + +/** + * This trait contains utility functions for basic channel tasks. + */ +trait CommonHandlers { + + this: Channel => + + def send(msg: LightningMessage): Unit = { + peer ! Peer.OutgoingMessage(msg, activeConnection) + } + + implicit def state2mystate(state: FSM.State[ChannelState, ChannelData]): MyState = MyState(state) + + /** + * We wrap the FSM state to add some utility functions that can be called on state transitions. + */ + case class MyState(state: FSM.State[ChannelState, ChannelData]) { + + def storing(unused: Unit = ()): FSM.State[ChannelState, ChannelData] = { + state.stateData match { + case d: HasCommitments => + log.debug("updating database record for channelId={}", d.channelId) + nodeParams.db.channels.addOrUpdateChannel(d) + context.system.eventStream.publish(ChannelPersisted(self, remoteNodeId, d.channelId, d)) + state + case _ => + log.error(s"can't store data=${state.stateData} in state=${state.stateName}") + state + } + } + + def sending(msgs: Seq[LightningMessage]): FSM.State[ChannelState, ChannelData] = { + msgs.foreach(sending) + state + } + + def sending(msg: LightningMessage): FSM.State[ChannelState, ChannelData] = { + send(msg) + state + } + + /** + * This method allows performing actions during the transition, e.g. after a call to [[MyState.storing]]. This is + * particularly useful to publish transactions only after we are sure that the state has been persisted. + */ + def calling(f: => Unit): FSM.State[ChannelState, ChannelData] = { + f + state + } + + /** + * We don't acknowledge htlc commands immediately, because we send them to the channel as soon as possible, and they + * may not yet have been written to the database. + * + * @param cmd fail/fulfill command that has been processed + */ + def acking(channelId: ByteVector32, cmd: HtlcSettlementCommand): FSM.State[ChannelState, ChannelData] = { + log.debug("scheduling acknowledgement of cmd id={}", cmd.id) + context.system.scheduler.scheduleOnce(10 seconds)(PendingCommandsDb.ackSettlementCommand(nodeParams.db.pendingCommands, channelId, cmd))(context.system.dispatcher) + state + } + + def acking(updates: List[UpdateMessage]): FSM.State[ChannelState, ChannelData] = { + log.debug("scheduling acknowledgement of cmds ids={}", updates.collect { case s: HtlcSettlementMessage => s.id }.mkString(",")) + context.system.scheduler.scheduleOnce(10 seconds)(PendingCommandsDb.ackSettlementCommands(nodeParams.db.pendingCommands, updates))(context.system.dispatcher) + state + } + + } + +} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ErrorHandlers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ErrorHandlers.scala new file mode 100644 index 0000000000..e9f9477df7 --- /dev/null +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ErrorHandlers.scala @@ -0,0 +1,362 @@ +/* + * 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.channel.fsm + +import akka.actor.typed.scaladsl.adapter.actorRefAdapter +import akka.actor.{ActorRef, FSM} +import fr.acinq.bitcoin.{ByteVector32, OutPoint, SatoshiLong, Transaction} +import fr.acinq.eclair.NotificationsLogger +import fr.acinq.eclair.NotificationsLogger.NotifyNodeOperator +import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.{WatchOutputSpent, WatchTxConfirmed} +import fr.acinq.eclair.channel.Helpers.Closing +import fr.acinq.eclair.channel._ +import fr.acinq.eclair.channel.fsm.Channel.UnhandledExceptionStrategy +import fr.acinq.eclair.channel.publish.TxPublisher.{PublishFinalTx, PublishReplaceableTx, PublishTx} +import fr.acinq.eclair.transactions.Transactions +import fr.acinq.eclair.transactions.Transactions.ClosingTx +import fr.acinq.eclair.wire.protocol.{AcceptChannel, ChannelReestablish, Error, OpenChannel} + +import java.sql.SQLException + +/** + * Created by t-bast on 28/03/2022. + */ + +/** + * This trait contains handlers for error scenarios (channel closing, force-closing, unhandled, exceptions, etc). + */ +trait ErrorHandlers extends CommonHandlers { + + this: Channel => + + def handleFastClose(c: CloseCommand, channelId: ByteVector32) = { + val replyTo = if (c.replyTo == ActorRef.noSender) sender() else c.replyTo + replyTo ! RES_SUCCESS(c, channelId) + goto(CLOSED) + } + + def handleMutualClose(closingTx: ClosingTx, d: Either[DATA_NEGOTIATING, DATA_CLOSING]) = { + log.info(s"closing tx published: closingTxId=${closingTx.tx.txid}") + val nextData = d match { + case Left(negotiating) => DATA_CLOSING(negotiating.commitments, fundingTx = None, waitingSince = nodeParams.currentBlockHeight, negotiating.closingTxProposed.flatten.map(_.unsignedTx), mutualClosePublished = closingTx :: Nil) + case Right(closing) => closing.copy(mutualClosePublished = closing.mutualClosePublished :+ closingTx) + } + goto(CLOSING) using nextData storing() calling doPublish(closingTx, nextData.commitments.localParams.isFunder) + } + + def doPublish(closingTx: ClosingTx, isFunder: Boolean): Unit = { + // the funder pays the fee + val fee = if (isFunder) closingTx.fee else 0.sat + txPublisher ! PublishFinalTx(closingTx, fee, None) + blockchain ! WatchTxConfirmed(self, closingTx.tx.txid, nodeParams.channelConf.minDepthBlocks) + } + + def handleLocalError(cause: Throwable, d: ChannelData, msg: Option[Any]) = { + cause match { + case _: ForcedLocalCommit => + log.warning(s"force-closing channel at user request") + case _ if msg.exists(_.isInstanceOf[OpenChannel]) || msg.exists(_.isInstanceOf[AcceptChannel]) => + // invalid remote channel parameters are logged as warning + log.warning(s"${cause.getMessage} while processing msg=${msg.getOrElse("n/a").getClass.getSimpleName} in state=$stateName") + case _: ChannelException => + log.error(s"${cause.getMessage} while processing msg=${msg.getOrElse("n/a").getClass.getSimpleName} in state=$stateName") + case _ => + // unhandled error: we dump the channel data, and print the stack trace + log.error(cause, s"msg=${msg.getOrElse("n/a")} stateData=$stateData:") + } + + val error = Error(d.channelId, cause.getMessage) + context.system.eventStream.publish(ChannelErrorOccurred(self, stateData.channelId, remoteNodeId, stateData, LocalError(cause), isFatal = true)) + + d match { + case dd: HasCommitments if Closing.nothingAtStake(dd) => goto(CLOSED) + case negotiating@DATA_NEGOTIATING(_, _, _, _, Some(bestUnpublishedClosingTx)) => + log.info(s"we have a valid closing tx, publishing it instead of our commitment: closingTxId=${bestUnpublishedClosingTx.tx.txid}") + // if we were in the process of closing and already received a closing sig from the counterparty, it's always better to use that + handleMutualClose(bestUnpublishedClosingTx, Left(negotiating)) + case dd: HasCommitments => + cause match { + case _: ChannelException => + // known channel exception: we force close using our current commitment + spendLocalCurrent(dd) sending error + case _ => + // unhandled exception: we apply the configured strategy + nodeParams.channelConf.unhandledExceptionStrategy match { + case UnhandledExceptionStrategy.LocalClose => + spendLocalCurrent(dd) sending error + case UnhandledExceptionStrategy.Stop => + log.error("unhandled exception: standard procedure would be to force-close the channel, but eclair has been configured to halt instead.") + NotificationsLogger.logFatalError( + s"""stopping node as configured strategy to unhandled exceptions for nodeId=$remoteNodeId channelId=${d.channelId} + | + |Eclair has been configured to shut down when an unhandled exception happens, instead of requesting a + |force-close from the peer. This gives the operator a chance of avoiding an unnecessary mass force-close + |of channels that may be caused by a bug in Eclair, or issues like running out of disk space, etc. + | + |You should get in touch with Eclair developers and provide logs of your node for analysis. + |""".stripMargin, cause) + sys.exit(1) + stop(FSM.Shutdown) + } + } + case _ => goto(CLOSED) sending error // when there is no commitment yet, we just send an error to our peer and go to CLOSED state + } + } + + def handleRemoteError(e: Error, d: ChannelData) = { + // see BOLT 1: only print out data verbatim if is composed of printable ASCII characters + log.error(s"peer sent error: ascii='${e.toAscii}' bin=${e.data.toHex}") + context.system.eventStream.publish(ChannelErrorOccurred(self, stateData.channelId, remoteNodeId, stateData, RemoteError(e), isFatal = true)) + + d match { + case _: DATA_CLOSING => stay() // nothing to do, there is already a spending tx published + case negotiating@DATA_NEGOTIATING(_, _, _, _, Some(bestUnpublishedClosingTx)) => + // if we were in the process of closing and already received a closing sig from the counterparty, it's always better to use that + handleMutualClose(bestUnpublishedClosingTx, Left(negotiating)) + case d: DATA_WAIT_FOR_FUNDING_CONFIRMED if Closing.nothingAtStake(d) => goto(CLOSED) // the channel was never used and the funding tx may be double-spent + case hasCommitments: HasCommitments => spendLocalCurrent(hasCommitments) // NB: we publish the commitment even if we have nothing at stake (in a dataloss situation our peer will send us an error just for that) + case _ => goto(CLOSED) // when there is no commitment yet, we just go to CLOSED state in case an error occurs + } + } + + /** + * This helper method will publish txs only if they haven't yet reached minDepth + */ + private def publishIfNeeded(txs: Iterable[PublishTx], irrevocablySpent: Map[OutPoint, Transaction]): Unit = { + val (skip, process) = txs.partition(publishTx => Closing.inputAlreadySpent(publishTx.input, irrevocablySpent)) + process.foreach { publishTx => txPublisher ! publishTx } + skip.foreach(publishTx => log.info("no need to republish tx spending {}:{}, it has already been confirmed", publishTx.input.txid, publishTx.input.index)) + } + + /** + * This helper method will watch txs only if they haven't yet reached minDepth + */ + private def watchConfirmedIfNeeded(txs: Iterable[Transaction], irrevocablySpent: Map[OutPoint, Transaction]): Unit = { + val (skip, process) = txs.partition(Closing.inputsAlreadySpent(_, irrevocablySpent)) + process.foreach(tx => blockchain ! WatchTxConfirmed(self, tx.txid, nodeParams.channelConf.minDepthBlocks)) + skip.foreach(tx => log.info(s"no need to watch txid=${tx.txid}, it has already been confirmed")) + } + + /** + * This helper method will watch txs only if the utxo they spend hasn't already been irrevocably spent + * + * @param parentTx transaction which outputs will be watched + * @param outputs outputs that will be watched. They must be a subset of the outputs of the `parentTx` + */ + private def watchSpentIfNeeded(parentTx: Transaction, outputs: Iterable[OutPoint], irrevocablySpent: Map[OutPoint, Transaction]): Unit = { + outputs.foreach { output => + require(output.txid == parentTx.txid && output.index < parentTx.txOut.size, s"output doesn't belong to the given parentTx: output=${output.txid}:${output.index} (expected txid=${parentTx.txid} index < ${parentTx.txOut.size})") + } + val (skip, process) = outputs.partition(irrevocablySpent.contains) + process.foreach(output => blockchain ! WatchOutputSpent(self, parentTx.txid, output.index.toInt, Set.empty)) + skip.foreach(output => log.info(s"no need to watch output=${output.txid}:${output.index}, it has already been spent by txid=${irrevocablySpent.get(output).map(_.txid)}")) + } + + def spendLocalCurrent(d: HasCommitments) = { + val outdatedCommitment = d match { + case _: DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT => true + case closing: DATA_CLOSING if closing.futureRemoteCommitPublished.isDefined => true + case _ => false + } + if (outdatedCommitment) { + log.warning("we have an outdated commitment: will not publish our local tx") + stay() + } else { + val commitTx = d.commitments.fullySignedLocalCommitTx(keyManager).tx + val localCommitPublished = Closing.LocalClose.claimCommitTxOutputs(keyManager, d.commitments, commitTx, nodeParams.currentBlockHeight, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) + val nextData = d match { + case closing: DATA_CLOSING => closing.copy(localCommitPublished = Some(localCommitPublished)) + case negotiating: DATA_NEGOTIATING => DATA_CLOSING(d.commitments, fundingTx = None, waitingSince = nodeParams.currentBlockHeight, negotiating.closingTxProposed.flatten.map(_.unsignedTx), localCommitPublished = Some(localCommitPublished)) + case waitForFundingConfirmed: DATA_WAIT_FOR_FUNDING_CONFIRMED => DATA_CLOSING(d.commitments, fundingTx = waitForFundingConfirmed.fundingTx, waitingSince = nodeParams.currentBlockHeight, mutualCloseProposed = Nil, localCommitPublished = Some(localCommitPublished)) + case _ => DATA_CLOSING(d.commitments, fundingTx = None, waitingSince = nodeParams.currentBlockHeight, mutualCloseProposed = Nil, localCommitPublished = Some(localCommitPublished)) + } + goto(CLOSING) using nextData storing() calling doPublish(localCommitPublished, d.commitments) + } + } + + def doPublish(localCommitPublished: LocalCommitPublished, commitments: Commitments): Unit = { + import localCommitPublished._ + + val commitInput = commitments.commitInput.outPoint + val isFunder = commitments.localParams.isFunder + val publishQueue = commitments.commitmentFormat match { + case Transactions.DefaultCommitmentFormat => + val redeemableHtlcTxs = htlcTxs.values.flatten.map(tx => PublishFinalTx(tx, tx.fee, Some(commitTx.txid))) + List(PublishFinalTx(commitTx, commitInput, "commit-tx", Closing.commitTxFee(commitments.commitInput, commitTx, isFunder), None)) ++ (claimMainDelayedOutputTx.map(tx => PublishFinalTx(tx, tx.fee, None)) ++ redeemableHtlcTxs ++ claimHtlcDelayedTxs.map(tx => PublishFinalTx(tx, tx.fee, None))) + case _: Transactions.AnchorOutputsCommitmentFormat => + val redeemableHtlcTxs = htlcTxs.values.flatten.map(tx => PublishReplaceableTx(tx, commitments)) + val claimLocalAnchor = claimAnchorTxs.collect { case tx: Transactions.ClaimLocalAnchorOutputTx => PublishReplaceableTx(tx, commitments) } + List(PublishFinalTx(commitTx, commitInput, "commit-tx", Closing.commitTxFee(commitments.commitInput, commitTx, isFunder), None)) ++ claimLocalAnchor ++ claimMainDelayedOutputTx.map(tx => PublishFinalTx(tx, tx.fee, None)) ++ redeemableHtlcTxs ++ claimHtlcDelayedTxs.map(tx => PublishFinalTx(tx, tx.fee, None)) + } + publishIfNeeded(publishQueue, irrevocablySpent) + + // we watch: + // - the commitment tx itself, so that we can handle the case where we don't have any outputs + // - 'final txs' that send funds to our wallet and that spend outputs that only us control + val watchConfirmedQueue = List(commitTx) ++ claimMainDelayedOutputTx.map(_.tx) ++ claimHtlcDelayedTxs.map(_.tx) + watchConfirmedIfNeeded(watchConfirmedQueue, irrevocablySpent) + + // we watch outputs of the commitment tx that both parties may spend + // we also watch our local anchor: this ensures that we will correctly detect when it's confirmed and count its fees + // in the audit DB, even if we restart before confirmation + val watchSpentQueue = htlcTxs.keys ++ claimAnchorTxs.collect { case tx: Transactions.ClaimLocalAnchorOutputTx => tx.input.outPoint } + watchSpentIfNeeded(commitTx, watchSpentQueue, irrevocablySpent) + } + + def handleRemoteSpentCurrent(commitTx: Transaction, d: HasCommitments) = { + log.warning(s"they published their current commit in txid=${commitTx.txid}") + require(commitTx.txid == d.commitments.remoteCommit.txid, "txid mismatch") + + context.system.eventStream.publish(TransactionPublished(d.channelId, remoteNodeId, commitTx, Closing.commitTxFee(d.commitments.commitInput, commitTx, d.commitments.localParams.isFunder), "remote-commit")) + val remoteCommitPublished = Closing.RemoteClose.claimCommitTxOutputs(keyManager, d.commitments, d.commitments.remoteCommit, commitTx, nodeParams.currentBlockHeight, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) + val nextData = d match { + case closing: DATA_CLOSING => closing.copy(remoteCommitPublished = Some(remoteCommitPublished)) + case negotiating: DATA_NEGOTIATING => DATA_CLOSING(d.commitments, fundingTx = None, waitingSince = nodeParams.currentBlockHeight, negotiating.closingTxProposed.flatten.map(_.unsignedTx), remoteCommitPublished = Some(remoteCommitPublished)) + case waitForFundingConfirmed: DATA_WAIT_FOR_FUNDING_CONFIRMED => DATA_CLOSING(d.commitments, fundingTx = waitForFundingConfirmed.fundingTx, waitingSince = nodeParams.currentBlockHeight, mutualCloseProposed = Nil, remoteCommitPublished = Some(remoteCommitPublished)) + case _ => DATA_CLOSING(d.commitments, fundingTx = None, waitingSince = nodeParams.currentBlockHeight, mutualCloseProposed = Nil, remoteCommitPublished = Some(remoteCommitPublished)) + } + goto(CLOSING) using nextData storing() calling doPublish(remoteCommitPublished, d.commitments) + } + + def handleRemoteSpentFuture(commitTx: Transaction, d: DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT) = { + log.warning(s"they published their future commit (because we asked them to) in txid=${commitTx.txid}") + context.system.eventStream.publish(TransactionPublished(d.channelId, remoteNodeId, commitTx, Closing.commitTxFee(d.commitments.commitInput, commitTx, d.commitments.localParams.isFunder), "future-remote-commit")) + val remotePerCommitmentPoint = d.remoteChannelReestablish.myCurrentPerCommitmentPoint + val remoteCommitPublished = RemoteCommitPublished( + commitTx = commitTx, + claimMainOutputTx = Closing.RemoteClose.claimMainOutput(keyManager, d.commitments, remotePerCommitmentPoint, commitTx, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets), + claimHtlcTxs = Map.empty, + claimAnchorTxs = List.empty, + irrevocablySpent = Map.empty) + val nextData = DATA_CLOSING(d.commitments, fundingTx = None, waitingSince = nodeParams.currentBlockHeight, Nil, futureRemoteCommitPublished = Some(remoteCommitPublished)) + goto(CLOSING) using nextData storing() calling doPublish(remoteCommitPublished, d.commitments) + } + + def handleRemoteSpentNext(commitTx: Transaction, d: HasCommitments) = { + log.warning(s"they published their next commit in txid=${commitTx.txid}") + require(d.commitments.remoteNextCommitInfo.isLeft, "next remote commit must be defined") + val Left(waitingForRevocation) = d.commitments.remoteNextCommitInfo + val remoteCommit = waitingForRevocation.nextRemoteCommit + require(commitTx.txid == remoteCommit.txid, "txid mismatch") + + context.system.eventStream.publish(TransactionPublished(d.channelId, remoteNodeId, commitTx, Closing.commitTxFee(d.commitments.commitInput, commitTx, d.commitments.localParams.isFunder), "next-remote-commit")) + val remoteCommitPublished = Closing.RemoteClose.claimCommitTxOutputs(keyManager, d.commitments, remoteCommit, commitTx, nodeParams.currentBlockHeight, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) + val nextData = d match { + case closing: DATA_CLOSING => closing.copy(nextRemoteCommitPublished = Some(remoteCommitPublished)) + case negotiating: DATA_NEGOTIATING => DATA_CLOSING(d.commitments, fundingTx = None, waitingSince = nodeParams.currentBlockHeight, negotiating.closingTxProposed.flatten.map(_.unsignedTx), nextRemoteCommitPublished = Some(remoteCommitPublished)) + // NB: if there is a next commitment, we can't be in DATA_WAIT_FOR_FUNDING_CONFIRMED so we don't have the case where fundingTx is defined + case _ => DATA_CLOSING(d.commitments, fundingTx = None, waitingSince = nodeParams.currentBlockHeight, mutualCloseProposed = Nil, nextRemoteCommitPublished = Some(remoteCommitPublished)) + } + goto(CLOSING) using nextData storing() calling doPublish(remoteCommitPublished, d.commitments) + } + + def doPublish(remoteCommitPublished: RemoteCommitPublished, commitments: Commitments): Unit = { + import remoteCommitPublished._ + + val redeemableHtlcTxs = claimHtlcTxs.values.flatten.map(tx => PublishReplaceableTx(tx, commitments)) + val publishQueue = claimMainOutputTx.map(tx => PublishFinalTx(tx, tx.fee, None)).toSeq ++ redeemableHtlcTxs + publishIfNeeded(publishQueue, irrevocablySpent) + + // we watch: + // - the commitment tx itself, so that we can handle the case where we don't have any outputs + // - 'final txs' that send funds to our wallet and that spend outputs that only us control + val watchConfirmedQueue = List(commitTx) ++ claimMainOutputTx.map(_.tx) + watchConfirmedIfNeeded(watchConfirmedQueue, irrevocablySpent) + + // we watch outputs of the commitment tx that both parties may spend + val watchSpentQueue = claimHtlcTxs.keys + watchSpentIfNeeded(commitTx, watchSpentQueue, irrevocablySpent) + } + + def handleRemoteSpentOther(tx: Transaction, d: HasCommitments) = { + log.warning(s"funding tx spent in txid=${tx.txid}") + Closing.RevokedClose.claimCommitTxOutputs(keyManager, d.commitments, tx, nodeParams.db.channels, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) match { + case Some(revokedCommitPublished) => + log.warning(s"txid=${tx.txid} was a revoked commitment, publishing the penalty tx") + context.system.eventStream.publish(TransactionPublished(d.channelId, remoteNodeId, tx, Closing.commitTxFee(d.commitments.commitInput, tx, d.commitments.localParams.isFunder), "revoked-commit")) + val exc = FundingTxSpent(d.channelId, tx) + val error = Error(d.channelId, exc.getMessage) + + val nextData = d match { + case closing: DATA_CLOSING => closing.copy(revokedCommitPublished = closing.revokedCommitPublished :+ revokedCommitPublished) + case negotiating: DATA_NEGOTIATING => DATA_CLOSING(d.commitments, fundingTx = None, waitingSince = nodeParams.currentBlockHeight, negotiating.closingTxProposed.flatten.map(_.unsignedTx), revokedCommitPublished = revokedCommitPublished :: Nil) + // NB: if there is a revoked commitment, we can't be in DATA_WAIT_FOR_FUNDING_CONFIRMED so we don't have the case where fundingTx is defined + case _ => DATA_CLOSING(d.commitments, fundingTx = None, waitingSince = nodeParams.currentBlockHeight, mutualCloseProposed = Nil, revokedCommitPublished = revokedCommitPublished :: Nil) + } + goto(CLOSING) using nextData storing() calling doPublish(revokedCommitPublished) sending error + case None => + // the published tx was neither their current commitment nor a revoked one + log.error(s"couldn't identify txid=${tx.txid}, something very bad is going on!!!") + context.system.eventStream.publish(NotifyNodeOperator(NotificationsLogger.Error, s"funding tx ${d.commitments.commitInput.outPoint.txid} of channel ${d.channelId} was spent by an unknown transaction, indicating that your DB has lost data or your node has been breached: please contact the dev team.")) + goto(ERR_INFORMATION_LEAK) + } + } + + def doPublish(revokedCommitPublished: RevokedCommitPublished): Unit = { + import revokedCommitPublished._ + + val publishQueue = (claimMainOutputTx ++ mainPenaltyTx ++ htlcPenaltyTxs ++ claimHtlcDelayedPenaltyTxs).map(tx => PublishFinalTx(tx, tx.fee, None)) + publishIfNeeded(publishQueue, irrevocablySpent) + + // we watch: + // - the commitment tx itself, so that we can handle the case where we don't have any outputs + // - 'final txs' that send funds to our wallet and that spend outputs that only us control + val watchConfirmedQueue = List(commitTx) ++ claimMainOutputTx.map(_.tx) + watchConfirmedIfNeeded(watchConfirmedQueue, irrevocablySpent) + + // we watch outputs of the commitment tx that both parties may spend + val watchSpentQueue = (mainPenaltyTx ++ htlcPenaltyTxs).map(_.input.outPoint) + watchSpentIfNeeded(commitTx, watchSpentQueue, irrevocablySpent) + } + + def handleInformationLeak(tx: Transaction, d: HasCommitments) = { + // this is never supposed to happen !! + log.error(s"our funding tx ${d.commitments.commitInput.outPoint.txid} was spent by txid=${tx.txid}!!") + context.system.eventStream.publish(NotifyNodeOperator(NotificationsLogger.Error, s"funding tx ${d.commitments.commitInput.outPoint.txid} of channel ${d.channelId} was spent by an unknown transaction, indicating that your DB has lost data or your node has been breached: please contact the dev team.")) + val exc = FundingTxSpent(d.channelId, tx) + val error = Error(d.channelId, exc.getMessage) + + // let's try to spend our current local tx + val commitTx = d.commitments.fullySignedLocalCommitTx(keyManager).tx + val localCommitPublished = Closing.LocalClose.claimCommitTxOutputs(keyManager, d.commitments, commitTx, nodeParams.currentBlockHeight, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) + + goto(ERR_INFORMATION_LEAK) calling doPublish(localCommitPublished, d.commitments) sending error + } + + def handleOutdatedCommitment(channelReestablish: ChannelReestablish, d: HasCommitments) = { + val exc = PleasePublishYourCommitment(d.channelId) + val error = Error(d.channelId, exc.getMessage) + goto(WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT) using DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT(d.commitments, channelReestablish) storing() sending error + } + + /** + * This helper function runs the state's default event handlers, and react to exceptions by unilaterally closing the channel + */ + def handleExceptions(s: StateFunction): StateFunction = { + case event if s.isDefinedAt(event) => + try { + s(event) + } catch { + case t: SQLException => + log.error(t, "fatal database error\n") + NotificationsLogger.logFatalError("eclair is shutting down because of a fatal database error", t) + sys.exit(1) + case t: Throwable => handleLocalError(t, event.stateData, None) + } + } + +} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/FundingHandlers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/FundingHandlers.scala new file mode 100644 index 0000000000..e918b0d8be --- /dev/null +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/FundingHandlers.scala @@ -0,0 +1,123 @@ +/* + * 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.channel.fsm + +import akka.actor.Status +import akka.actor.typed.scaladsl.adapter.{TypedActorRefOps, actorRefAdapter} +import fr.acinq.bitcoin.{ByteVector32, SatoshiLong, Transaction} +import fr.acinq.eclair.BlockHeight +import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.{GetTxWithMeta, GetTxWithMetaResponse, WatchFundingSpent} +import fr.acinq.eclair.channel._ +import fr.acinq.eclair.channel.fsm.Channel.{BITCOIN_FUNDING_PUBLISH_FAILED, BITCOIN_FUNDING_TIMEOUT, FUNDING_TIMEOUT_FUNDEE} +import fr.acinq.eclair.channel.publish.TxPublisher.PublishFinalTx +import fr.acinq.eclair.wire.protocol.Error + +import scala.concurrent.duration.DurationInt +import scala.util.{Failure, Success} + +/** + * Created by t-bast on 28/03/2022. + */ + +/** + * This trait contains handlers related to funding channel transactions. + */ +trait FundingHandlers extends CommonHandlers { + + this: Channel => + + /** + * This function is used to return feedback to user at channel opening + */ + def channelOpenReplyToUser(message: Either[ChannelOpenError, ChannelOpenResponse]): Unit = { + val m = message match { + case Left(LocalError(t)) => Status.Failure(t) + case Left(RemoteError(e)) => Status.Failure(new RuntimeException(s"peer sent error: ascii='${e.toAscii}' bin=${e.data.toHex}")) + case Right(s) => s + } + origin_opt.foreach(_ ! m) + } + + def watchFundingTx(commitments: Commitments, additionalKnownSpendingTxs: Set[ByteVector32] = Set.empty): Unit = { + // TODO: should we wait for an acknowledgment from the watcher? + val knownSpendingTxs = Set(commitments.localCommit.commitTxAndRemoteSig.commitTx.tx.txid, commitments.remoteCommit.txid) ++ commitments.remoteNextCommitInfo.left.toSeq.map(_.nextRemoteCommit.txid).toSet ++ additionalKnownSpendingTxs + blockchain ! WatchFundingSpent(self, commitments.commitInput.outPoint.txid, commitments.commitInput.outPoint.index.toInt, knownSpendingTxs) + // TODO: implement this? (not needed if we use a reasonable min_depth) + //blockchain ! WatchLost(self, commitments.commitInput.outPoint.txid, nodeParams.channelConf.minDepthBlocks, BITCOIN_FUNDING_LOST) + } + + /** + * When we are funder, we use this function to detect when our funding tx has been double-spent (by another transaction + * that we made for some reason). If the funding tx has been double spent we can forget about the channel. + */ + def checkDoubleSpent(fundingTx: Transaction): Unit = { + log.debug(s"checking status of funding tx txid=${fundingTx.txid}") + wallet.doubleSpent(fundingTx).onComplete { + case Success(true) => + log.warning(s"funding tx has been double spent! fundingTxid=${fundingTx.txid} fundingTx=$fundingTx") + self ! BITCOIN_FUNDING_PUBLISH_FAILED + case Success(false) => () + case Failure(t) => log.error(t, s"error while testing status of funding tx fundingTxid=${fundingTx.txid}: ") + } + } + + def handleGetFundingTx(getTxResponse: GetTxWithMetaResponse, waitingSince: BlockHeight, fundingTx_opt: Option[Transaction]) = { + import getTxResponse._ + tx_opt match { + case Some(_) => () // the funding tx exists, nothing to do + case None => + fundingTx_opt match { + case Some(fundingTx) => + // if we are funder, we never give up + // we cannot correctly set the fee, but it was correctly set when we initially published the transaction + log.info(s"republishing the funding tx...") + txPublisher ! PublishFinalTx(fundingTx, fundingTx.txIn.head.outPoint, "funding", 0 sat, None) + // we also check if the funding tx has been double-spent + checkDoubleSpent(fundingTx) + context.system.scheduler.scheduleOnce(1 day, blockchain.toClassic, GetTxWithMeta(self, txid)) + case None if (nodeParams.currentBlockHeight - waitingSince) > FUNDING_TIMEOUT_FUNDEE => + // if we are fundee, we give up after some time + log.warning(s"funding tx hasn't been published in ${nodeParams.currentBlockHeight - waitingSince} blocks") + self ! BITCOIN_FUNDING_TIMEOUT + case None => + // let's wait a little longer + log.info(s"funding tx still hasn't been published in ${nodeParams.currentBlockHeight - waitingSince} blocks, will wait ${FUNDING_TIMEOUT_FUNDEE - (nodeParams.currentBlockHeight - waitingSince)} more blocks...") + context.system.scheduler.scheduleOnce(1 day, blockchain.toClassic, GetTxWithMeta(self, txid)) + } + } + stay() + } + + def handleFundingPublishFailed(d: HasCommitments) = { + log.error(s"failed to publish funding tx") + val exc = ChannelFundingError(d.channelId) + val error = Error(d.channelId, exc.getMessage) + // NB: we don't use the handleLocalError handler because it would result in the commit tx being published, which we don't want: + // implementation *guarantees* that in case of BITCOIN_FUNDING_PUBLISH_FAILED, the funding tx hasn't and will never be published, so we can close the channel right away + context.system.eventStream.publish(ChannelErrorOccurred(self, stateData.channelId, remoteNodeId, stateData, LocalError(exc), isFatal = true)) + goto(CLOSED) sending error + } + + def handleFundingTimeout(d: HasCommitments) = { + log.warning(s"funding tx hasn't been confirmed in time, cancelling channel delay=$FUNDING_TIMEOUT_FUNDEE") + val exc = FundingTxTimedout(d.channelId) + val error = Error(d.channelId, exc.getMessage) + context.system.eventStream.publish(ChannelErrorOccurred(self, stateData.channelId, remoteNodeId, stateData, LocalError(exc), isFatal = true)) + goto(CLOSED) sending error + } + +} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala index 8d1f399532..f37be95bab 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala @@ -31,6 +31,7 @@ import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher import fr.acinq.eclair.blockchain.fee.FeeratePerKw import fr.acinq.eclair.blockchain.{OnChainAddressGenerator, OnChainChannelFunder} import fr.acinq.eclair.channel._ +import fr.acinq.eclair.channel.fsm.Channel import fr.acinq.eclair.io.MessageRelay.Status import fr.acinq.eclair.io.Monitoring.Metrics import fr.acinq.eclair.io.PeerConnection.KillReason diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentInitiator.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentInitiator.scala index cb84ba7cbf..94d8f98771 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentInitiator.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentInitiator.scala @@ -20,7 +20,7 @@ import akka.actor.{Actor, ActorContext, ActorLogging, ActorRef, Props} import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.bitcoin.{ByteVector32, Crypto} import fr.acinq.eclair.Features.BasicMultiPartPayment -import fr.acinq.eclair.channel.Channel +import fr.acinq.eclair.channel.fsm.Channel import fr.acinq.eclair.crypto.Sphinx import fr.acinq.eclair.payment.Bolt11Invoice.ExtraHop import fr.acinq.eclair.payment.OutgoingPaymentPacket.Upstream diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala b/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala index ea9aba1c32..2519875061 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala @@ -20,7 +20,7 @@ import fr.acinq.bitcoin.{Block, ByteVector32, Satoshi, SatoshiLong, Script} import fr.acinq.eclair.FeatureSupport.{Mandatory, Optional} import fr.acinq.eclair.Features._ import fr.acinq.eclair.blockchain.fee._ -import fr.acinq.eclair.channel.Channel.{ChannelConf, UnhandledExceptionStrategy} +import fr.acinq.eclair.channel.fsm.Channel.{ChannelConf, UnhandledExceptionStrategy} import fr.acinq.eclair.channel.{ChannelFlags, LocalParams} import fr.acinq.eclair.crypto.keymanager.{LocalChannelKeyManager, LocalNodeKeyManager} import fr.acinq.eclair.io.MessageRelay.RelayAll diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/TestUtils.scala b/eclair-core/src/test/scala/fr/acinq/eclair/TestUtils.scala index ede577b057..966ebab908 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/TestUtils.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/TestUtils.scala @@ -19,7 +19,7 @@ package fr.acinq.eclair import akka.actor.ActorRef import akka.event.DiagnosticLoggingAdapter import akka.testkit.{TestActor, TestProbe} -import fr.acinq.eclair.channel.Channel +import fr.acinq.eclair.channel.fsm.Channel import fr.acinq.eclair.io.Peer import fr.acinq.eclair.wire.protocol.LightningMessage diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/ChannelDataSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/ChannelDataSpec.scala index 58da425c64..6aac08cfc1 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/ChannelDataSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/ChannelDataSpec.scala @@ -20,6 +20,7 @@ import akka.testkit.{TestFSMRef, TestProbe} import fr.acinq.bitcoin.{ByteVector32, OutPoint, SatoshiLong, Transaction, TxIn, TxOut} import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.WatchFundingSpentTriggered import fr.acinq.eclair.channel.Helpers.Closing +import fr.acinq.eclair.channel.fsm.Channel import fr.acinq.eclair.channel.states.ChannelStateTestsHelperMethods import fr.acinq.eclair.transactions.Transactions._ import fr.acinq.eclair.wire.protocol.{CommitSig, RevokeAndAck, UnknownNextPeer, UpdateAddHtlc} diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/FuzzySpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/FuzzySpec.scala index a968c65f25..84a21817c4 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/FuzzySpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/FuzzySpec.scala @@ -25,6 +25,7 @@ import fr.acinq.eclair.TestConstants.{Alice, Bob} import fr.acinq.eclair._ import fr.acinq.eclair.blockchain.DummyOnChainWallet import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._ +import fr.acinq.eclair.channel.fsm.Channel import fr.acinq.eclair.channel.publish.TxPublisher import fr.acinq.eclair.channel.states.ChannelStateTestsBase import fr.acinq.eclair.channel.states.ChannelStateTestsHelperMethods.FakeTxPublisherFactory diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/HelpersSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/HelpersSpec.scala index bbe15df698..20f59543a8 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/HelpersSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/HelpersSpec.scala @@ -23,6 +23,7 @@ import fr.acinq.eclair.TestConstants.Alice.nodeParams import fr.acinq.eclair.TestUtils.NoLoggingDiagnostics import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.WatchFundingSpentTriggered import fr.acinq.eclair.channel.Helpers.Closing +import fr.acinq.eclair.channel.fsm.Channel import fr.acinq.eclair.channel.states.{ChannelStateTestsHelperMethods, ChannelStateTestsTags} import fr.acinq.eclair.transactions.Transactions._ import fr.acinq.eclair.wire.protocol.UpdateAddHtlc diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/RestoreSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/RestoreSpec.scala index 2a318adc93..ad29c6cf72 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/RestoreSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/RestoreSpec.scala @@ -9,6 +9,7 @@ import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.bitcoin._ import fr.acinq.eclair.TestConstants.{Alice, Bob} import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.WatchFundingSpentTriggered +import fr.acinq.eclair.channel.fsm.Channel import fr.acinq.eclair.channel.states.ChannelStateTestsBase import fr.acinq.eclair.channel.states.ChannelStateTestsHelperMethods.FakeTxPublisherFactory import fr.acinq.eclair.crypto.Generators diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPublisherSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPublisherSpec.scala index 44b6d5949a..53c4734cfd 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPublisherSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPublisherSpec.scala @@ -30,6 +30,7 @@ import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinCoreClient.MempoolTx import fr.acinq.eclair.blockchain.bitcoind.rpc.{BitcoinCoreClient, BitcoinJsonRPCClient} import fr.acinq.eclair.blockchain.fee.{FeeratePerKw, FeeratesPerKw} import fr.acinq.eclair.channel._ +import fr.acinq.eclair.channel.fsm.Channel import fr.acinq.eclair.channel.publish.ReplaceableTxPublisher.{Publish, Stop, UpdateConfirmationTarget} import fr.acinq.eclair.channel.publish.TxPublisher.TxRejectedReason._ import fr.acinq.eclair.channel.publish.TxPublisher._ diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala index 7c59abfbcd..afe24d937d 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala @@ -28,6 +28,7 @@ import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._ import fr.acinq.eclair.blockchain.fee.{FeeTargets, FeeratePerKw} import fr.acinq.eclair.blockchain.{DummyOnChainWallet, OnChainWallet} import fr.acinq.eclair.channel._ +import fr.acinq.eclair.channel.fsm.Channel import fr.acinq.eclair.channel.publish.TxPublisher import fr.acinq.eclair.channel.publish.TxPublisher.PublishReplaceableTx import fr.acinq.eclair.channel.states.ChannelStateTestsHelperMethods.FakeTxPublisherFactory diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForAcceptChannelStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForAcceptChannelStateSpec.scala index 911d41a17a..f01284802d 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForAcceptChannelStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForAcceptChannelStateSpec.scala @@ -21,8 +21,9 @@ import akka.testkit.{TestFSMRef, TestProbe} import fr.acinq.bitcoin.{Block, Btc, ByteVector32, SatoshiLong} import fr.acinq.eclair.TestConstants.{Alice, Bob} import fr.acinq.eclair.blockchain.NoOpOnChainWallet -import fr.acinq.eclair.channel.Channel.TickChannelOpenTimeout import fr.acinq.eclair.channel._ +import fr.acinq.eclair.channel.fsm.Channel +import fr.acinq.eclair.channel.fsm.Channel.TickChannelOpenTimeout import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsTags} import fr.acinq.eclair.wire.protocol.{AcceptChannel, ChannelTlv, Error, Init, OpenChannel, TlvStream} import fr.acinq.eclair.{CltvExpiryDelta, FeatureSupport, Features, TestConstants, TestKitBaseClass} diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForOpenChannelStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForOpenChannelStateSpec.scala index f376f8a3ac..3e7856e90c 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForOpenChannelStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForOpenChannelStateSpec.scala @@ -21,6 +21,7 @@ import fr.acinq.bitcoin.{Block, Btc, ByteVector32, SatoshiLong} import fr.acinq.eclair.TestConstants.{Alice, Bob} import fr.acinq.eclair.blockchain.fee.FeeratePerKw import fr.acinq.eclair.channel._ +import fr.acinq.eclair.channel.fsm.Channel import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsTags} import fr.acinq.eclair.wire.protocol.{AcceptChannel, ChannelTlv, Error, Init, OpenChannel, TlvStream} import fr.acinq.eclair.{CltvExpiryDelta, MilliSatoshiLong, TestConstants, TestKitBaseClass, ToMilliSatoshiConversion} diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingCreatedStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingCreatedStateSpec.scala index 6a55fff7c3..d49396d94b 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingCreatedStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingCreatedStateSpec.scala @@ -22,6 +22,7 @@ import fr.acinq.bitcoin.{Btc, ByteVector32, SatoshiLong} import fr.acinq.eclair.TestConstants.{Alice, Bob} import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._ import fr.acinq.eclair.channel._ +import fr.acinq.eclair.channel.fsm.Channel import fr.acinq.eclair.channel.publish.TxPublisher import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsTags} import fr.acinq.eclair.transactions.Transactions diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingInternalStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingInternalStateSpec.scala index 6c6116e99d..c475c44699 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingInternalStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingInternalStateSpec.scala @@ -20,8 +20,9 @@ import akka.actor.Status import akka.testkit.{TestFSMRef, TestProbe} import fr.acinq.bitcoin.ByteVector32 import fr.acinq.eclair.blockchain.NoOpOnChainWallet -import fr.acinq.eclair.channel.Channel.TickChannelOpenTimeout import fr.acinq.eclair.channel._ +import fr.acinq.eclair.channel.fsm.Channel +import fr.acinq.eclair.channel.fsm.Channel.TickChannelOpenTimeout import fr.acinq.eclair.channel.states.ChannelStateTestsBase import fr.acinq.eclair.wire.protocol._ import fr.acinq.eclair.{TestConstants, TestKitBaseClass} diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingSignedStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingSignedStateSpec.scala index b58481e05a..38ee091e06 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingSignedStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingSignedStateSpec.scala @@ -22,8 +22,9 @@ import fr.acinq.bitcoin.{Btc, ByteVector32, ByteVector64, SatoshiLong} import fr.acinq.eclair.TestConstants.{Alice, Bob} import fr.acinq.eclair.blockchain.DummyOnChainWallet import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._ -import fr.acinq.eclair.channel.Channel.TickChannelOpenTimeout import fr.acinq.eclair.channel._ +import fr.acinq.eclair.channel.fsm.Channel +import fr.acinq.eclair.channel.fsm.Channel.TickChannelOpenTimeout import fr.acinq.eclair.channel.publish.TxPublisher import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsTags} import fr.acinq.eclair.wire.protocol.{AcceptChannel, Error, FundingCreated, FundingSigned, Init, OpenChannel} diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingConfirmedStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingConfirmedStateSpec.scala index ff1d7b44d5..19d3e67608 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingConfirmedStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingConfirmedStateSpec.scala @@ -20,8 +20,9 @@ import akka.testkit.{TestFSMRef, TestProbe} import fr.acinq.bitcoin.{ByteVector32, SatoshiLong, Script, Transaction} import fr.acinq.eclair.blockchain.CurrentBlockHeight import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._ -import fr.acinq.eclair.channel.Channel.{BITCOIN_FUNDING_PUBLISH_FAILED, BITCOIN_FUNDING_TIMEOUT} import fr.acinq.eclair.channel._ +import fr.acinq.eclair.channel.fsm.Channel +import fr.acinq.eclair.channel.fsm.Channel.{BITCOIN_FUNDING_PUBLISH_FAILED, BITCOIN_FUNDING_TIMEOUT} import fr.acinq.eclair.channel.publish.TxPublisher import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsTags} import fr.acinq.eclair.transactions.Scripts.multiSig2of2 diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingLockedStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingLockedStateSpec.scala index 76dcae683c..97f074e7d7 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingLockedStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingLockedStateSpec.scala @@ -20,6 +20,7 @@ import akka.testkit.{TestFSMRef, TestProbe} import fr.acinq.bitcoin.{ByteVector32, Transaction} import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._ import fr.acinq.eclair.channel._ +import fr.acinq.eclair.channel.fsm.Channel import fr.acinq.eclair.channel.publish.TxPublisher import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsTags} import fr.acinq.eclair.payment.relay.Relayer.RelayFees diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala index c63d9ade71..9c6dbcc89d 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala @@ -27,8 +27,9 @@ import fr.acinq.eclair._ import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._ import fr.acinq.eclair.blockchain.fee.{FeeratePerByte, FeeratePerKw, FeeratesPerKw} import fr.acinq.eclair.blockchain.{CurrentBlockHeight, CurrentFeerates} -import fr.acinq.eclair.channel.Channel._ import fr.acinq.eclair.channel._ +import fr.acinq.eclair.channel.fsm.Channel +import fr.acinq.eclair.channel.fsm.Channel._ import fr.acinq.eclair.channel.publish.TxPublisher.{PublishFinalTx, PublishReplaceableTx, PublishTx} import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsTags} import fr.acinq.eclair.crypto.Sphinx diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/OfflineStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/OfflineStateSpec.scala index ad22801989..8b6b31d481 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/OfflineStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/OfflineStateSpec.scala @@ -25,6 +25,7 @@ import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._ import fr.acinq.eclair.blockchain.fee.FeeratesPerKw import fr.acinq.eclair.blockchain.{CurrentBlockHeight, CurrentFeerates} import fr.acinq.eclair.channel._ +import fr.acinq.eclair.channel.fsm.Channel import fr.acinq.eclair.channel.publish.TxPublisher.{PublishFinalTx, PublishTx} import fr.acinq.eclair.channel.states.ChannelStateTestsBase import fr.acinq.eclair.transactions.Transactions.HtlcSuccessTx diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/g/NegotiatingStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/g/NegotiatingStateSpec.scala index cf5c402107..ab305b3329 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/g/NegotiatingStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/g/NegotiatingStateSpec.scala @@ -22,6 +22,7 @@ import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._ import fr.acinq.eclair.blockchain.fee.{FeeratePerKw, FeeratesPerKw} import fr.acinq.eclair.channel.Helpers.Closing import fr.acinq.eclair.channel._ +import fr.acinq.eclair.channel.fsm.Channel import fr.acinq.eclair.channel.publish.TxPublisher.{PublishFinalTx, PublishTx} import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsTags} import fr.acinq.eclair.transactions.Transactions diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala index e058741993..ea376baf36 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala @@ -21,8 +21,9 @@ import fr.acinq.bitcoin.Crypto.PrivateKey import fr.acinq.bitcoin.{ByteVector32, ByteVector64, Crypto, OutPoint, SatoshiLong, Script, ScriptFlags, Transaction, TxIn, TxOut} import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._ import fr.acinq.eclair.blockchain.fee.{FeeratePerKw, FeeratesPerKw} -import fr.acinq.eclair.channel.Channel.{BITCOIN_FUNDING_PUBLISH_FAILED, BITCOIN_FUNDING_TIMEOUT} import fr.acinq.eclair.channel._ +import fr.acinq.eclair.channel.fsm.Channel +import fr.acinq.eclair.channel.fsm.Channel.{BITCOIN_FUNDING_PUBLISH_FAILED, BITCOIN_FUNDING_TIMEOUT} import fr.acinq.eclair.channel.publish.TxPublisher.{PublishFinalTx, PublishReplaceableTx, PublishTx, SetChannelId} import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsTags} import fr.acinq.eclair.payment._ diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/PaymentIntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/PaymentIntegrationSpec.scala index 8dded7ef29..e9c4e53f47 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/PaymentIntegrationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/PaymentIntegrationSpec.scala @@ -26,8 +26,8 @@ import fr.acinq.eclair.blockchain.bitcoind.BitcoindService.BitcoinReq import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.{Watch, WatchFundingConfirmed} import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinCoreClient -import fr.acinq.eclair.channel.Channel.{BroadcastChannelUpdate, PeriodicRefresh} import fr.acinq.eclair.channel._ +import fr.acinq.eclair.channel.fsm.Channel.{BroadcastChannelUpdate, PeriodicRefresh} import fr.acinq.eclair.crypto.Sphinx.DecryptedFailurePacket import fr.acinq.eclair.crypto.TransportHandler import fr.acinq.eclair.db._ diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/PerformanceIntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/PerformanceIntegrationSpec.scala index 516743a068..37f582382a 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/PerformanceIntegrationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/PerformanceIntegrationSpec.scala @@ -16,11 +16,11 @@ package fr.acinq.eclair.integration -import akka.actor.ActorRef import akka.testkit.TestProbe import com.typesafe.config.ConfigFactory import fr.acinq.bitcoin.SatoshiLong import fr.acinq.eclair.channel._ +import fr.acinq.eclair.channel.fsm.Channel import fr.acinq.eclair.payment._ import fr.acinq.eclair.payment.receive.MultiPartHandler.ReceivePayment import fr.acinq.eclair.payment.send.MultiPartPaymentLifecycle.PreimageReceived diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/interop/rustytests/RustyTestsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/interop/rustytests/RustyTestsSpec.scala index bf4015238b..5727045f09 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/interop/rustytests/RustyTestsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/interop/rustytests/RustyTestsSpec.scala @@ -25,6 +25,7 @@ import fr.acinq.eclair.blockchain.DummyOnChainWallet import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._ import fr.acinq.eclair.blockchain.fee.{FeeratePerKw, FeeratesPerKw} import fr.acinq.eclair.channel._ +import fr.acinq.eclair.channel.fsm.Channel import fr.acinq.eclair.channel.publish.TxPublisher import fr.acinq.eclair.channel.states.ChannelStateTestsHelperMethods.FakeTxPublisherFactory import fr.acinq.eclair.payment.receive.{ForwardHandler, PaymentHandler} diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala index 3555ab0fab..a0aca047c6 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala @@ -30,6 +30,7 @@ import fr.acinq.eclair.blockchain.DummyOnChainWallet import fr.acinq.eclair.blockchain.fee.{FeeratePerKw, FeeratesPerKw} import fr.acinq.eclair.channel.ChannelTypes.UnsupportedChannelType import fr.acinq.eclair.channel._ +import fr.acinq.eclair.channel.fsm.Channel import fr.acinq.eclair.channel.states.ChannelStateTestsTags import fr.acinq.eclair.io.Peer._ import fr.acinq.eclair.message.OnionMessages.{Recipient, buildMessage} diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentInitiatorSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentInitiatorSpec.scala index 710ab371cb..6e052ffe97 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentInitiatorSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentInitiatorSpec.scala @@ -22,7 +22,7 @@ import fr.acinq.bitcoin.Block import fr.acinq.eclair.FeatureSupport.{Mandatory, Optional} import fr.acinq.eclair.Features._ import fr.acinq.eclair.UInt64.Conversions._ -import fr.acinq.eclair.channel.Channel +import fr.acinq.eclair.channel.fsm.Channel import fr.acinq.eclair.crypto.Sphinx import fr.acinq.eclair.payment.Bolt11Invoice.ExtraHop import fr.acinq.eclair.payment.OutgoingPaymentPacket.Upstream diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala index d2dabd413e..126fd8898c 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala @@ -26,6 +26,7 @@ import fr.acinq.eclair._ import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.{UtxoStatus, ValidateRequest, ValidateResult, WatchExternalChannelSpent} import fr.acinq.eclair.channel.Register.{ForwardShortId, ForwardShortIdFailure} import fr.acinq.eclair.channel._ +import fr.acinq.eclair.channel.fsm.Channel import fr.acinq.eclair.crypto.Sphinx import fr.acinq.eclair.db.{OutgoingPayment, OutgoingPaymentStatus, PaymentType} import fr.acinq.eclair.io.Peer.PeerRoutingMessage diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentPacketSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentPacketSpec.scala index 8aabb5a086..cb9b69b185 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentPacketSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentPacketSpec.scala @@ -23,6 +23,7 @@ import fr.acinq.bitcoin.{Block, ByteVector32, Crypto, DeterministicWallet, OutPo import fr.acinq.eclair.FeatureSupport.{Mandatory, Optional} import fr.acinq.eclair.Features._ import fr.acinq.eclair.channel._ +import fr.acinq.eclair.channel.fsm.Channel import fr.acinq.eclair.crypto.Sphinx import fr.acinq.eclair.payment.IncomingPaymentPacket.{ChannelRelayPacket, FinalPacket, NodeRelayPacket, decrypt} import fr.acinq.eclair.payment.OutgoingPaymentPacket._ diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/ChannelCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/ChannelCodecsSpec.scala index d9f093c086..0caee48832 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/ChannelCodecsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/ChannelCodecsSpec.scala @@ -22,6 +22,7 @@ import fr.acinq.eclair._ import fr.acinq.eclair.blockchain.fee.FeeratePerKw import fr.acinq.eclair.channel.Helpers.Funding import fr.acinq.eclair.channel._ +import fr.acinq.eclair.channel.fsm.Channel import fr.acinq.eclair.crypto.ShaChain import fr.acinq.eclair.crypto.keymanager.{LocalChannelKeyManager, LocalNodeKeyManager} import fr.acinq.eclair.json.JsonSerializers From 2872d876d03ac6930d5d9465a9d5e7e45fbcf175 Mon Sep 17 00:00:00 2001 From: Bastien Teinturier <31281497+t-bast@users.noreply.github.com> Date: Tue, 29 Mar 2022 10:42:14 +0200 Subject: [PATCH 044/121] Fix latest bitcoin build (#2218) Update build dependencies to match bitcoind master. --- .github/workflows/latest-bitcoind.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/latest-bitcoind.yml b/.github/workflows/latest-bitcoind.yml index 5c498d3680..c768af0f68 100644 --- a/.github/workflows/latest-bitcoind.yml +++ b/.github/workflows/latest-bitcoind.yml @@ -21,7 +21,7 @@ jobs: path: bitcoin - name: Install bitcoind dependencies - run: sudo apt-get install build-essential libtool autotools-dev automake pkg-config bsdmainutils python3 libssl-dev libevent-dev libboost-system-dev libboost-filesystem-dev libboost-chrono-dev libboost-test-dev libboost-thread-dev libminiupnpc-dev libzmq3-dev libqt5gui5 libqt5core5a libqt5dbus5 qttools5-dev qttools5-dev-tools libprotobuf-dev protobuf-compiler git libsqlite3-dev ccache + run: sudo apt-get install build-essential libtool autotools-dev automake pkg-config bsdmainutils python3 libevent-dev libboost-dev libminiupnpc-dev libnatpmp-dev libzmq3-dev libsqlite3-dev systemtap-sdt-dev working-directory: ./bitcoin - name: Autogen bitcoind From 9358e5e1f5d5fbbf19edcb8891693822c0d4f7bf Mon Sep 17 00:00:00 2001 From: Richard Myers Date: Tue, 29 Mar 2022 11:10:42 +0200 Subject: [PATCH 045/121] Add `channelbalances` API call (#2196) The `channelbalances` API call retrieves information about the balances of all local channels, not just those with usable outgoing balances that are enabled for sending. This change also adds the `isEnabled` attribute to the json results for both the new `channelbalances` and old `usablebalances` API calls. --- docs/release-notes/eclair-vnext.md | 2 +- eclair-core/eclair-cli | 1 + .../main/scala/fr/acinq/eclair/Eclair.scala | 16 ++++---- .../acinq/eclair/payment/relay/Relayer.scala | 7 ++-- .../fr/acinq/eclair/EclairImplSpec.scala | 20 +++++++--- .../integration/PaymentIntegrationSpec.scala | 4 +- .../payment/relay/ChannelRelayerSpec.scala | 4 +- .../acinq/eclair/api/handlers/Channel.scala | 6 ++- .../src/test/resources/api/channelbalances | 1 + .../src/test/resources/api/usablebalances | 2 +- .../fr/acinq/eclair/api/ApiServiceSpec.scala | 39 ++++++++++++++----- 11 files changed, 71 insertions(+), 31 deletions(-) create mode 100644 eclair-node/src/test/resources/api/channelbalances diff --git a/docs/release-notes/eclair-vnext.md b/docs/release-notes/eclair-vnext.md index 97a737e03b..9cef7f9d6a 100644 --- a/docs/release-notes/eclair-vnext.md +++ b/docs/release-notes/eclair-vnext.md @@ -8,7 +8,7 @@ ### API changes - +- `channelbalances` Retrieves information about the balances of all local channels. (#2196) ### Miscellaneous improvements and bug fixes diff --git a/eclair-core/eclair-cli b/eclair-core/eclair-cli index 4b6e320164..553df5a977 100755 --- a/eclair-core/eclair-cli +++ b/eclair-core/eclair-cli @@ -44,6 +44,7 @@ and COMMAND is one of the available commands: - allchannels - allupdates - channelstats + - channelbalances === Fees === - networkfees diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala index 4ca1fb1cea..958bb234be 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala @@ -19,9 +19,6 @@ package fr.acinq.eclair import akka.actor.ActorRef import akka.actor.typed.scaladsl.AskPattern.Askable import akka.actor.typed.scaladsl.adapter.ClassicSchedulerOps -import akka.actor.{ActorRef, typed} -import akka.actor.typed.scaladsl.AskPattern.{Askable, schedulerFromActorSystem} -import akka.actor.typed.scaladsl.adapter.{ClassicActorSystemOps, ClassicSchedulerOps} import akka.pattern._ import akka.util.Timeout import com.softwaremill.quicklens.ModifyPimp @@ -43,7 +40,7 @@ import fr.acinq.eclair.io._ import fr.acinq.eclair.message.{OnionMessages, Postman} import fr.acinq.eclair.payment._ import fr.acinq.eclair.payment.receive.MultiPartHandler.ReceivePayment -import fr.acinq.eclair.payment.relay.Relayer.{GetOutgoingChannels, OutgoingChannels, RelayFees, UsableBalance} +import fr.acinq.eclair.payment.relay.Relayer.{ChannelBalance, GetOutgoingChannels, OutgoingChannels, RelayFees} import fr.acinq.eclair.payment.send.MultiPartPaymentLifecycle.PreimageReceived import fr.acinq.eclair.payment.send.PaymentInitiator._ import fr.acinq.eclair.router.Router @@ -147,7 +144,9 @@ trait Eclair { def getInfo()(implicit timeout: Timeout): Future[GetInfoResponse] - def usableBalances()(implicit timeout: Timeout): Future[Iterable[UsableBalance]] + def usableBalances()(implicit timeout: Timeout): Future[Iterable[ChannelBalance]] + + def channelBalances()(implicit timeout: Timeout): Future[Iterable[ChannelBalance]] def onChainBalance(): Future[OnChainBalance] @@ -485,8 +484,11 @@ class EclairImpl(appKit: Kit) extends Eclair with Logging { instanceId = appKit.nodeParams.instanceId.toString) ) - override def usableBalances()(implicit timeout: Timeout): Future[Iterable[UsableBalance]] = - (appKit.relayer ? GetOutgoingChannels()).mapTo[OutgoingChannels].map(_.channels.map(_.toUsableBalance)) + override def usableBalances()(implicit timeout: Timeout): Future[Iterable[ChannelBalance]] = + (appKit.relayer ? GetOutgoingChannels()).mapTo[OutgoingChannels].map(_.channels.map(_.toChannelBalance)) + + override def channelBalances()(implicit timeout: Timeout): Future[Iterable[ChannelBalance]] = + (appKit.relayer ? GetOutgoingChannels(enabledOnly = false)).mapTo[OutgoingChannels].map(_.channels.map(_.toChannelBalance)) override def globalBalance()(implicit timeout: Timeout): Future[GlobalBalance] = { for { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/Relayer.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/Relayer.scala index b95982e74e..d69dd2945e 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/Relayer.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/Relayer.scala @@ -132,7 +132,7 @@ object Relayer extends Logging { } case class RelayForward(add: UpdateAddHtlc) - case class UsableBalance(remoteNodeId: PublicKey, shortChannelId: ShortChannelId, canSend: MilliSatoshi, canReceive: MilliSatoshi, isPublic: Boolean) + case class ChannelBalance(remoteNodeId: PublicKey, shortChannelId: ShortChannelId, canSend: MilliSatoshi, canReceive: MilliSatoshi, isPublic: Boolean, isEnabled: Boolean) /** * Get the list of local outgoing channels. @@ -141,12 +141,13 @@ object Relayer extends Logging { */ case class GetOutgoingChannels(enabledOnly: Boolean = true) case class OutgoingChannel(nextNodeId: PublicKey, channelUpdate: ChannelUpdate, prevChannelUpdate: Option[ChannelUpdate], commitments: AbstractCommitments) { - def toUsableBalance: UsableBalance = UsableBalance( + def toChannelBalance: ChannelBalance = ChannelBalance( remoteNodeId = nextNodeId, shortChannelId = channelUpdate.shortChannelId, canSend = commitments.availableBalanceForSend, canReceive = commitments.availableBalanceForReceive, - isPublic = commitments.announceChannel) + isPublic = commitments.announceChannel, + isEnabled = channelUpdate.channelFlags.isEnabled) } case class OutgoingChannels(channels: Seq[OutgoingChannel]) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/EclairImplSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/EclairImplSpec.scala index bdd2efe22b..2b6155a867 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/EclairImplSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/EclairImplSpec.scala @@ -30,13 +30,12 @@ import fr.acinq.eclair.blockchain.fee.{FeeratePerByte, FeeratePerKw} import fr.acinq.eclair.channel._ import fr.acinq.eclair.db._ import fr.acinq.eclair.io.Peer.OpenChannel -import fr.acinq.eclair.payment.Bolt11Invoice import fr.acinq.eclair.payment.Bolt11Invoice.ExtraHop import fr.acinq.eclair.payment.receive.MultiPartHandler.ReceivePayment import fr.acinq.eclair.payment.receive.PaymentHandler -import fr.acinq.eclair.payment.relay.Relayer.RelayFees +import fr.acinq.eclair.payment.relay.Relayer.{GetOutgoingChannels, RelayFees} import fr.acinq.eclair.payment.send.PaymentInitiator._ -import fr.acinq.eclair.payment.{PaymentFailed, Invoice} +import fr.acinq.eclair.payment.{Bolt11Invoice, Invoice, PaymentFailed} import fr.acinq.eclair.router.RouteCalculationSpec.makeUpdateShort import fr.acinq.eclair.router.Router.{PredefinedNodeRoute, PublicChannel} import fr.acinq.eclair.router.{Announcements, Router} @@ -56,7 +55,7 @@ import scala.concurrent.duration._ class EclairImplSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with IdiomaticMockito with ParallelTestExecution { implicit val timeout: Timeout = Timeout(30 seconds) - case class FixtureParam(register: TestProbe, router: TestProbe, paymentInitiator: TestProbe, switchboard: TestProbe, paymentHandler: TestProbe, sender: TestProbe, kit: Kit) + case class FixtureParam(register: TestProbe, relayer: TestProbe, router: TestProbe, paymentInitiator: TestProbe, switchboard: TestProbe, paymentHandler: TestProbe, sender: TestProbe, kit: Kit) override def withFixture(test: OneArgTest): Outcome = { val watcher = TestProbe() @@ -86,7 +85,7 @@ class EclairImplSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with I postman.ref.toTyped, new DummyOnChainWallet() ) - withFixture(test.toNoArgTest(FixtureParam(register, router, paymentInitiator, switchboard, paymentHandler, TestProbe(), kit))) + withFixture(test.toNoArgTest(FixtureParam(register, relayer, router, paymentInitiator, switchboard, paymentHandler, TestProbe(), kit))) } test("convert fee rate properly") { f => @@ -611,4 +610,15 @@ class EclairImplSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with I peersDb.addOrUpdateRelayFees(b, RelayFees(999 msat, 1234)).wasCalled(once) } + test("channelBalances asks for all channels, usableBalances only for enabled ones") { f => + import f._ + + val eclair = new EclairImpl(kit) + + eclair.channelBalances().pipeTo(sender.ref) + relayer.expectMsg(GetOutgoingChannels(enabledOnly=false)) + eclair.usableBalances().pipeTo(sender.ref) + relayer.expectMsg(GetOutgoingChannels()) + } + } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/PaymentIntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/PaymentIntegrationSpec.scala index e9c4e53f47..851c4f8a17 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/PaymentIntegrationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/PaymentIntegrationSpec.scala @@ -699,8 +699,8 @@ class PaymentIntegrationSpec extends IntegrationSpec { val channels1 = sender.expectMsgType[Relayer.OutgoingChannels] val channels2 = sender.expectMsgType[Relayer.OutgoingChannels] - logger.info(channels1.channels.map(_.toUsableBalance)) - logger.info(channels2.channels.map(_.toUsableBalance)) + logger.info(channels1.channels.map(_.toChannelBalance)) + logger.info(channels2.channels.map(_.toChannelBalance)) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/ChannelRelayerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/ChannelRelayerSpec.scala index 6875f18a70..3ecf496028 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/ChannelRelayerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/ChannelRelayerSpec.scala @@ -463,9 +463,9 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a val channels1 = getOutgoingChannels(true) assert(channels1.size === 2) assert(channels1.head.channelUpdate === channelUpdate_ab) - assert(channels1.head.toUsableBalance === Relayer.UsableBalance(a, channelUpdate_ab.shortChannelId, 0 msat, 300000 msat, isPublic = false)) + assert(channels1.head.toChannelBalance === Relayer.ChannelBalance(a, channelUpdate_ab.shortChannelId, 0 msat, 300000 msat, isPublic = false, isEnabled = true)) assert(channels1.last.channelUpdate === channelUpdate_bc) - assert(channels1.last.toUsableBalance === Relayer.UsableBalance(c, channelUpdate_bc.shortChannelId, 400000 msat, 0 msat, isPublic = false)) + assert(channels1.last.toChannelBalance === Relayer.ChannelBalance(c, channelUpdate_bc.shortChannelId, 400000 msat, 0 msat, isPublic = false, isEnabled = true)) channelRelayer ! WrappedAvailableBalanceChanged(AvailableBalanceChanged(null, channelId_bc, channelUpdate_bc.shortChannelId, makeCommitments(channelId_bc, 200000 msat, 500000 msat))) val channels2 = getOutgoingChannels(true) diff --git a/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Channel.scala b/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Channel.scala index 340e0845da..6a8691e947 100644 --- a/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Channel.scala +++ b/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Channel.scala @@ -106,6 +106,10 @@ trait Channel { } } - val channelRoutes: Route = open ~ close ~ forceClose ~ channel ~ channels ~ allChannels ~ allUpdates ~ channelStats + val channelBalances: Route = postRequest("channelbalances") { implicit t => + complete(eclairApi.channelBalances()) + } + + val channelRoutes: Route = open ~ close ~ forceClose ~ channel ~ channels ~ allChannels ~ allUpdates ~ channelStats ~ channelBalances } diff --git a/eclair-node/src/test/resources/api/channelbalances b/eclair-node/src/test/resources/api/channelbalances new file mode 100644 index 0000000000..962b4c89aa --- /dev/null +++ b/eclair-node/src/test/resources/api/channelbalances @@ -0,0 +1 @@ +[{"remoteNodeId":"03af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d0","shortChannelId":"0x0x1","canSend":100000000,"canReceive":20000000,"isPublic":true,"isEnabled":true},{"remoteNodeId":"03af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d0","shortChannelId":"0x0x2","canSend":0,"canReceive":30000000,"isPublic":false,"isEnabled":false}] \ No newline at end of file diff --git a/eclair-node/src/test/resources/api/usablebalances b/eclair-node/src/test/resources/api/usablebalances index c1ef6b4920..5613aaf151 100644 --- a/eclair-node/src/test/resources/api/usablebalances +++ b/eclair-node/src/test/resources/api/usablebalances @@ -1 +1 @@ -[{"remoteNodeId":"03af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d0","shortChannelId":"0x0x1","canSend":100000000,"canReceive":20000000,"isPublic":true},{"remoteNodeId":"03af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d0","shortChannelId":"0x0x2","canSend":400000000,"canReceive":30000000,"isPublic":false}] \ No newline at end of file +[{"remoteNodeId":"03af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d0","shortChannelId":"0x0x1","canSend":100000000,"canReceive":20000000,"isPublic":true,"isEnabled":true},{"remoteNodeId":"03af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d0","shortChannelId":"0x0x2","canSend":400000000,"canReceive":30000000,"isPublic":false,"isEnabled":true}] \ No newline at end of file diff --git a/eclair-node/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala b/eclair-node/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala index a7e7002921..ff44e2f097 100644 --- a/eclair-node/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala +++ b/eclair-node/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala @@ -42,12 +42,13 @@ import fr.acinq.eclair.io.Peer.PeerInfo import fr.acinq.eclair.io.{NodeURI, Peer} import fr.acinq.eclair.message.OnionMessages import fr.acinq.eclair.payment._ -import fr.acinq.eclair.payment.relay.Relayer.UsableBalance +import fr.acinq.eclair.payment.relay.Relayer.ChannelBalance import fr.acinq.eclair.payment.send.MultiPartPaymentLifecycle.PreimageReceived import fr.acinq.eclair.payment.send.PaymentInitiator.SendPaymentToRouteResponse import fr.acinq.eclair.router.Router import fr.acinq.eclair.router.Router.PredefinedNodeRoute import fr.acinq.eclair.wire.protocol._ +import org.json4s.{Formats, Serialization} import org.mockito.scalatest.IdiomaticMockito import org.scalatest.funsuite.AnyFunSuite import org.scalatest.matchers.should.Matchers @@ -62,12 +63,12 @@ import scala.util.Try class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticMockito with Matchers { - implicit val formats = JsonSupport.formats - implicit val serialization = JsonSupport.serialization - implicit val routeTestTimeout = RouteTestTimeout(3 seconds) + implicit val formats: Formats = JsonSupport.formats + implicit val serialization: Serialization = JsonSupport.serialization + implicit val routeTestTimeout: RouteTestTimeout = RouteTestTimeout(3 seconds) - val aliceNodeId = PublicKey(hex"03af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d0") - val bobNodeId = PublicKey(hex"039dc0e0b1d25905e44fdf6f8e89755a5e219685840d0bc1d28d3308f9628a3585") + val aliceNodeId: PublicKey = PublicKey(hex"03af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d0") + val bobNodeId: PublicKey = PublicKey(hex"039dc0e0b1d25905e44fdf6f8e89755a5e219685840d0bc1d28d3308f9628a3585") object PluginApi extends RouteProvider { override def route(directives: EclairDirectives): Route = { @@ -200,11 +201,11 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM } } - test("'usablebalances' asks relayer for current usable balances") { + test("'usablebalances' returns expected balance json only for enabled channels") { val eclair = mock[Eclair] eclair.usableBalances()(any[Timeout]) returns Future.successful(List( - UsableBalance(aliceNodeId, ShortChannelId(1), 100000000 msat, 20000000 msat, isPublic = true), - UsableBalance(aliceNodeId, ShortChannelId(2), 400000000 msat, 30000000 msat, isPublic = false) + ChannelBalance(aliceNodeId, ShortChannelId(1), 100000000 msat, 20000000 msat, isPublic = true, isEnabled = true), + ChannelBalance(aliceNodeId, ShortChannelId(2), 400000000 msat, 30000000 msat, isPublic = false, isEnabled = true) )) val mockService = mockApi(eclair) @@ -220,6 +221,26 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM } } + test("'channelbalances' returns expected balance json for all channels") { + val eclair = mock[Eclair] + eclair.channelBalances()(any[Timeout]) returns Future.successful(List( + ChannelBalance(aliceNodeId, ShortChannelId(1), 100000000 msat, 20000000 msat, isPublic = true, isEnabled = true), + ChannelBalance(aliceNodeId, ShortChannelId(2), 0 msat, 30000000 msat, isPublic = false, isEnabled = false) + )) + + val mockService = mockApi(eclair) + Post("/channelbalances") ~> + addCredentials(BasicHttpCredentials("", mockApi().password)) ~> + Route.seal(mockService.channelBalances) ~> + check { + assert(handled) + assert(status == OK) + val response = entityAs[String] + eclair.channelBalances()(any[Timeout]).wasCalled(once) + matchTestJson("channelbalances", response) + } + } + test("'getinfo' response should include this node ID") { val eclair = mock[Eclair] val mockService = new MockService(eclair) From 6f31ed2dc99399783d2aef842cdb5fa19a327ede Mon Sep 17 00:00:00 2001 From: Richard Myers Date: Tue, 29 Mar 2022 12:12:13 +0200 Subject: [PATCH 046/121] Add release notes for new min-funding settings and invoice purger (#2210) --- docs/release-notes/eclair-vnext.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/docs/release-notes/eclair-vnext.md b/docs/release-notes/eclair-vnext.md index 9cef7f9d6a..286ac71557 100644 --- a/docs/release-notes/eclair-vnext.md +++ b/docs/release-notes/eclair-vnext.md @@ -19,6 +19,24 @@ By default, eclair will still accept the old fee for 10 minutes, you can change If you want a specific fee update to ignore this delay, you can update the fee twice to make eclair forget about the previous fee. +#### New minimum funding setting for private channels + +New settings have been added to independently control the minimum funding required to open public and private channels to your node. + +The `eclair.channel.min-funding-satoshis` setting has been deprecated and replaced with the following two new settings and defaults: + +* `eclair.channel.min-public-funding-satoshis = 100000` +* `eclair.channel.min-private-funding-satoshis = 100000` + +If your configuration file changes `eclair.channel.min-funding-satoshis` then you should replace it with both of these new settings. + +#### Expired incoming invoices now purged if unpaid + +Expired incoming invoices that are unpaid will be searched for and purged from the database when Eclair starts up. Thereafter searches for expired unpaid invoices to purge will run once every 24 hours. You can disable this feature, or change the search interval with two new settings: + +* `eclair.purge-expired-invoices.enabled = true +* `eclair.purge-expired-invoices.interval = 24 hours` + ## Verifying signatures You will need `gpg` and our release signing key 7A73FE77DE2C4027. Note that you can get it: From 162768274e5e40e67381482fbe37ce0e797a0f65 Mon Sep 17 00:00:00 2001 From: Thomas HUET <81159533+thomash-acinq@users.noreply.github.com> Date: Thu, 31 Mar 2022 14:19:05 +0200 Subject: [PATCH 047/121] Remove ipv6 zone identifier test (#2220) The eth0 interface does not exist on some machines which causes the tests to fail. --- .../src/test/scala/fr/acinq/eclair/io/NodeURISpec.scala | 3 --- 1 file changed, 3 deletions(-) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/io/NodeURISpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/io/NodeURISpec.scala index 1093ec5f34..2fb1a4ade9 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/io/NodeURISpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/io/NodeURISpec.scala @@ -26,7 +26,6 @@ class NodeURISpec extends AnyFunSuite { val IPV4_ENDURANCE = "34.250.234.192" val NAME_ENDURANCE = "endurance.acinq.co" val IPV6 = "[2405:204:66a9:536c:873f:dc4a:f055:a298]" - val IPV6_ZONE_IDENTIFIER = "[2001:db8:a0b:12f0::1%eth0]" test("default port") { assert(NodeURI.DEFAULT_PORT == 9735) @@ -42,8 +41,6 @@ class NodeURISpec extends AnyFunSuite { TestCase(s"$PUBKEY@$NAME_ENDURANCE", "13.248.222.197", 9735), TestCase(s"$PUBKEY@$IPV6:9737", "[2405:204:66a9:536c:873f:dc4a:f055:a298]", 9737), TestCase(s"$PUBKEY@$IPV6", "[2405:204:66a9:536c:873f:dc4a:f055:a298]", 9735), - TestCase(s"$PUBKEY@$IPV6_ZONE_IDENTIFIER:9737", "[2001:db8:a0b:12f0::1]", 9737), - TestCase(s"$PUBKEY@$IPV6_ZONE_IDENTIFIER", "[2001:db8:a0b:12f0::1]", 9735), ) for (testCase <- testCases) { From 7883bf621dd6add1ab25b23d43cb8b47154acad8 Mon Sep 17 00:00:00 2001 From: Fabrice Drouin Date: Tue, 5 Apr 2022 17:04:33 +0200 Subject: [PATCH 048/121] Use bitcoin-kmp through bitcoin-lib (#2038) We use a version of bitcoin-lib that uses bitcon-kmp under the hood. Its API is the same as before, but its package is now fr.acinq.bitcoin.scalacompat --- eclair-core/pom.xml | 6 ++++ .../main/scala/fr/acinq/eclair/Eclair.scala | 4 +-- .../src/main/scala/fr/acinq/eclair/Logs.scala | 4 +-- .../scala/fr/acinq/eclair/MilliSatoshi.scala | 2 +- .../scala/fr/acinq/eclair/NodeParams.scala | 4 +-- .../scala/fr/acinq/eclair/PluginParams.scala | 4 +-- .../main/scala/fr/acinq/eclair/Setup.scala | 2 +- .../acinq/eclair/balance/BalanceActor.scala | 2 +- .../eclair/balance/ChannelsListener.scala | 2 +- .../acinq/eclair/balance/CheckBalance.scala | 2 +- .../eclair/blockchain/BlockchainEvents.scala | 2 +- .../eclair/blockchain/OnChainWallet.scala | 4 +-- .../blockchain/bitcoind/ZmqWatcher.scala | 2 +- .../rpc/BasicBitcoinJsonRPCClient.scala | 14 +++----- .../bitcoind/rpc/BitcoinCoreClient.scala | 20 +++++++---- .../blockchain/bitcoind/zmq/ZMQActor.scala | 2 +- .../fee/BitcoinCoreFeeProvider.scala | 2 +- .../blockchain/fee/FallbackFeeProvider.scala | 2 +- .../eclair/blockchain/fee/FeeEstimator.scala | 4 +-- .../eclair/blockchain/fee/FeeProvider.scala | 2 +- .../blockchain/watchdogs/ExplorerApi.scala | 9 +++-- .../blockchain/watchdogs/HeadersOverDns.scala | 3 +- .../fr/acinq/eclair/channel/ChannelData.scala | 4 +-- .../acinq/eclair/channel/ChannelEvents.scala | 4 +-- .../eclair/channel/ChannelExceptions.scala | 4 +-- .../fr/acinq/eclair/channel/Commitments.scala | 4 +-- .../acinq/eclair/channel/DustExposure.scala | 2 +- .../fr/acinq/eclair/channel/Helpers.scala | 7 ++-- .../fr/acinq/eclair/channel/Register.scala | 4 +-- .../fr/acinq/eclair/channel/fsm/Channel.scala | 4 +-- .../channel/fsm/ChannelOpenSingleFunder.scala | 3 +- .../eclair/channel/fsm/CommonHandlers.scala | 2 +- .../eclair/channel/fsm/ErrorHandlers.scala | 2 +- .../eclair/channel/fsm/FundingHandlers.scala | 2 +- .../channel/publish/MempoolTxMonitor.scala | 2 +- .../channel/publish/ReplaceableTxFunder.scala | 2 +- .../publish/ReplaceableTxPrePublisher.scala | 2 +- .../publish/ReplaceableTxPublisher.scala | 2 +- .../eclair/channel/publish/TxPublisher.scala | 4 +-- .../channel/publish/TxTimeLocksMonitor.scala | 2 +- .../eclair/crypto/ChaCha20Poly1305.scala | 2 +- .../fr/acinq/eclair/crypto/Generators.scala | 4 +-- .../scala/fr/acinq/eclair/crypto/Mac.scala | 2 +- .../scala/fr/acinq/eclair/crypto/Noise.scala | 9 ++--- .../scala/fr/acinq/eclair/crypto/Random.scala | 2 +- .../fr/acinq/eclair/crypto/ShaChain.scala | 2 +- .../scala/fr/acinq/eclair/crypto/Sphinx.scala | 6 ++-- .../eclair/crypto/TransportHandler.scala | 4 +-- .../acinq/eclair/crypto/WeakEntropyPool.scala | 4 +-- .../crypto/keymanager/ChannelKeyManager.scala | 6 ++-- .../keymanager/LocalChannelKeyManager.scala | 8 ++--- .../keymanager/LocalNodeKeyManager.scala | 6 ++-- .../crypto/keymanager/NodeKeyManager.scala | 4 +-- .../scala/fr/acinq/eclair/db/AuditDb.scala | 4 +-- .../scala/fr/acinq/eclair/db/ChannelsDb.scala | 2 +- .../fr/acinq/eclair/db/DbEventHandler.scala | 4 +-- .../fr/acinq/eclair/db/DualDatabases.scala | 2 +- .../scala/fr/acinq/eclair/db/NetworkDb.scala | 4 +-- .../scala/fr/acinq/eclair/db/PaymentsDb.scala | 4 +-- .../scala/fr/acinq/eclair/db/PeersDb.scala | 2 +- .../acinq/eclair/db/PendingCommandsDb.scala | 2 +- .../fr/acinq/eclair/db/jdbc/JdbcUtils.scala | 2 +- .../fr/acinq/eclair/db/pg/PgAuditDb.scala | 4 +-- .../fr/acinq/eclair/db/pg/PgChannelsDb.scala | 2 +- .../fr/acinq/eclair/db/pg/PgNetworkDb.scala | 2 +- .../fr/acinq/eclair/db/pg/PgPaymentsDb.scala | 4 +-- .../fr/acinq/eclair/db/pg/PgPeersDb.scala | 4 +-- .../eclair/db/pg/PgPendingCommandsDb.scala | 2 +- .../eclair/db/sqlite/SqliteAuditDb.scala | 4 +-- .../eclair/db/sqlite/SqliteChannelsDb.scala | 2 +- .../eclair/db/sqlite/SqliteFeeratesDb.scala | 2 +- .../eclair/db/sqlite/SqliteNetworkDb.scala | 2 +- .../eclair/db/sqlite/SqlitePaymentsDb.scala | 4 +-- .../eclair/db/sqlite/SqlitePeersDb.scala | 4 +-- .../db/sqlite/SqlitePendingCommandsDb.scala | 2 +- .../scala/fr/acinq/eclair/io/Client.scala | 2 +- .../fr/acinq/eclair/io/ClientSpawner.scala | 2 +- .../fr/acinq/eclair/io/MessageRelay.scala | 4 +-- .../scala/fr/acinq/eclair/io/NodeURI.scala | 2 +- .../main/scala/fr/acinq/eclair/io/Peer.scala | 4 +-- .../fr/acinq/eclair/io/PeerConnection.scala | 4 +-- .../scala/fr/acinq/eclair/io/PeerEvents.scala | 2 +- .../fr/acinq/eclair/io/ReconnectionTask.scala | 2 +- .../fr/acinq/eclair/io/Switchboard.scala | 4 +-- .../acinq/eclair/json/JsonSerializers.scala | 14 ++++++-- .../acinq/eclair/message/OnionMessages.scala | 4 +-- .../fr/acinq/eclair/message/Postman.scala | 4 +-- .../main/scala/fr/acinq/eclair/package.scala | 20 ++++++++--- .../acinq/eclair/payment/Bolt11Invoice.scala | 33 +++++++++++-------- .../fr/acinq/eclair/payment/Invoice.scala | 4 +-- .../fr/acinq/eclair/payment/Monitoring.scala | 2 +- .../acinq/eclair/payment/PaymentEvents.scala | 4 +-- .../acinq/eclair/payment/PaymentPacket.scala | 4 +-- .../payment/receive/MultiPartHandler.scala | 2 +- .../payment/receive/MultiPartPaymentFSM.scala | 2 +- .../eclair/payment/relay/ChannelRelay.scala | 2 +- .../eclair/payment/relay/ChannelRelayer.scala | 2 +- .../eclair/payment/relay/NodeRelay.scala | 2 +- .../eclair/payment/relay/NodeRelayer.scala | 2 +- .../relay/PostRestartHtlcCleaner.scala | 4 +-- .../acinq/eclair/payment/relay/Relayer.scala | 2 +- .../acinq/eclair/payment/send/Autoprobe.scala | 2 +- .../send/MultiPartPaymentLifecycle.scala | 4 +-- .../payment/send/PaymentInitiator.scala | 4 +-- .../payment/send/PaymentLifecycle.scala | 4 +-- .../remote/EclairInternalsSerializer.scala | 2 +- .../acinq/eclair/router/Announcements.scala | 4 +-- .../scala/fr/acinq/eclair/router/Graph.scala | 4 +-- .../fr/acinq/eclair/router/Monitoring.scala | 2 +- .../acinq/eclair/router/NetworkEvents.scala | 4 +-- .../eclair/router/RouteCalculation.scala | 4 +-- .../scala/fr/acinq/eclair/router/Router.scala | 4 +-- .../scala/fr/acinq/eclair/router/Sync.scala | 4 +-- .../fr/acinq/eclair/router/Validation.scala | 4 +-- .../eclair/transactions/CommitmentSpec.scala | 2 +- .../acinq/eclair/transactions/Scripts.scala | 15 +++++---- .../eclair/transactions/Transactions.scala | 8 +++-- .../channel/version0/ChannelCodecs0.scala | 13 +++++--- .../channel/version0/ChannelTypes0.scala | 4 +-- .../channel/version1/ChannelCodecs1.scala | 12 ++++--- .../channel/version2/ChannelCodecs2.scala | 12 ++++--- .../channel/version3/ChannelCodecs3.scala | 12 ++++--- .../eclair/wire/protocol/ChannelTlv.scala | 2 +- .../eclair/wire/protocol/CommonCodecs.scala | 4 +-- .../eclair/wire/protocol/FailureMessage.scala | 2 +- .../wire/protocol/LightningMessageTypes.scala | 4 +-- .../eclair/wire/protocol/MessageOnion.scala | 2 +- .../eclair/wire/protocol/OnionRouting.scala | 2 +- .../eclair/wire/protocol/PaymentOnion.scala | 4 +-- .../eclair/wire/protocol/RouteBlinding.scala | 2 +- .../wire/protocol/SetupAndControlTlv.scala | 2 +- .../fr/acinq/eclair/MilliSatoshiTest.java | 2 +- .../fr/acinq/eclair/EclairImplSpec.scala | 4 +-- .../fr/acinq/eclair/MilliSatoshiSpec.scala | 2 +- .../scala/fr/acinq/eclair/PackageSpec.scala | 8 +++-- .../scala/fr/acinq/eclair/StartupSpec.scala | 22 ++++++------- .../acinq/eclair/TestBitcoinCoreClient.scala | 2 +- .../scala/fr/acinq/eclair/TestConstants.scala | 2 +- .../eclair/balance/CheckBalanceSpec.scala | 2 +- .../blockchain/DummyOnChainWallet.scala | 7 ++-- .../acinq/eclair/blockchain/WatcherSpec.scala | 11 ++++--- .../bitcoind/BitcoinCoreClientSpec.scala | 18 +++++----- .../blockchain/bitcoind/BitcoindService.scala | 4 +-- .../blockchain/bitcoind/ZmqWatcherSpec.scala | 2 +- .../fee/BitcoinCoreFeeProviderSpec.scala | 2 +- .../blockchain/fee/DbFeeProviderSpec.scala | 2 +- .../fee/FallbackFeeProviderSpec.scala | 2 +- .../blockchain/fee/FeeEstimatorSpec.scala | 2 +- .../blockchain/fee/FeeProviderSpec.scala | 2 +- .../fee/SmoothFeeProviderSpec.scala | 2 +- .../watchdogs/BlockchainWatchdogSpec.scala | 2 +- .../watchdogs/ExplorerApiSpec.scala | 2 +- .../watchdogs/HeadersOverDnsSpec.scala | 3 +- .../eclair/channel/ChannelDataSpec.scala | 2 +- .../eclair/channel/CommitmentsSpec.scala | 4 +-- .../eclair/channel/DustExposureSpec.scala | 2 +- .../fr/acinq/eclair/channel/FuzzySpec.scala | 4 +-- .../fr/acinq/eclair/channel/HelpersSpec.scala | 2 +- .../acinq/eclair/channel/RegisterSpec.scala | 4 +-- .../fr/acinq/eclair/channel/RestoreSpec.scala | 8 +++-- .../publish/FinalTxPublisherSpec.scala | 2 +- .../publish/MempoolTxMonitorSpec.scala | 4 +-- .../publish/ReplaceableTxFunderSpec.scala | 2 +- .../publish/ReplaceableTxPublisherSpec.scala | 2 +- .../channel/publish/TxPublisherSpec.scala | 2 +- .../publish/TxTimeLocksMonitorSpec.scala | 2 +- .../ChannelStateTestsHelperMethods.scala | 5 +-- .../a/WaitForAcceptChannelStateSpec.scala | 2 +- .../a/WaitForOpenChannelStateSpec.scala | 2 +- .../b/WaitForFundingCreatedStateSpec.scala | 2 +- .../b/WaitForFundingInternalStateSpec.scala | 2 +- .../b/WaitForFundingSignedStateSpec.scala | 2 +- .../c/WaitForFundingConfirmedStateSpec.scala | 2 +- .../c/WaitForFundingLockedStateSpec.scala | 2 +- .../channel/states/e/NormalStateSpec.scala | 5 +-- .../channel/states/e/OfflineStateSpec.scala | 5 +-- .../channel/states/f/ShutdownStateSpec.scala | 5 +-- .../states/g/NegotiatingStateSpec.scala | 2 +- .../channel/states/h/ClosingStateSpec.scala | 5 +-- .../acinq/eclair/crypto/GeneratorsSpec.scala | 4 +-- .../fr/acinq/eclair/crypto/MacSpec.scala | 2 +- .../fr/acinq/eclair/crypto/RandomSpec.scala | 2 +- .../fr/acinq/eclair/crypto/ShaChainSpec.scala | 2 +- .../fr/acinq/eclair/crypto/SphinxSpec.scala | 4 +-- .../LocalChannelKeyManagerSpec.scala | 6 ++-- .../keymanager/LocalNodeKeyManagerSpec.scala | 6 ++-- .../fr/acinq/eclair/db/AuditDbSpec.scala | 4 +-- .../fr/acinq/eclair/db/ChannelsDbSpec.scala | 2 +- .../fr/acinq/eclair/db/NetworkDbSpec.scala | 4 +-- .../fr/acinq/eclair/db/PaymentsDbSpec.scala | 4 +-- .../fr/acinq/eclair/db/PeersDbSpec.scala | 2 +- .../eclair/db/PendingCommandsDbSpec.scala | 2 +- .../eclair/db/SqliteFeeratesDbSpec.scala | 2 +- .../integration/ChannelIntegrationSpec.scala | 13 ++++---- .../eclair/integration/IntegrationSpec.scala | 2 +- .../integration/MessageIntegrationSpec.scala | 3 +- .../integration/PaymentIntegrationSpec.scala | 4 +-- .../PerformanceIntegrationSpec.scala | 2 +- .../interop/rustytests/RustyTestsSpec.scala | 2 +- .../rustytests/SynchronizationPipe.scala | 2 +- .../fr/acinq/eclair/io/MessageRelaySpec.scala | 2 +- .../acinq/eclair/io/PeerConnectionSpec.scala | 4 +-- .../scala/fr/acinq/eclair/io/PeerSpec.scala | 4 +-- .../eclair/io/ReconnectionTaskSpec.scala | 2 +- .../fr/acinq/eclair/io/SwitchboardSpec.scala | 4 +-- .../eclair/json/JsonSerializersSpec.scala | 8 ++--- .../eclair/message/OnionMessagesSpec.scala | 4 +-- .../eclair/payment/Bolt11InvoiceSpec.scala | 4 +-- .../eclair/payment/MultiPartHandlerSpec.scala | 2 +- .../payment/MultiPartPaymentFSMSpec.scala | 2 +- .../MultiPartPaymentLifecycleSpec.scala | 2 +- .../eclair/payment/PaymentInitiatorSpec.scala | 2 +- .../eclair/payment/PaymentLifecycleSpec.scala | 6 ++-- .../eclair/payment/PaymentPacketSpec.scala | 6 ++-- .../payment/PostRestartHtlcCleanerSpec.scala | 4 +-- .../payment/receive/InvoicePurgerSpec.scala | 2 +- .../payment/relay/ChannelRelayerSpec.scala | 4 +-- .../payment/relay/NodeRelayerSpec.scala | 2 +- .../eclair/payment/relay/RelayerSpec.scala | 2 +- .../AnnouncementsBatchValidationSpec.scala | 4 +-- .../eclair/router/AnnouncementsSpec.scala | 4 +-- .../acinq/eclair/router/BaseRouterSpec.scala | 6 ++-- .../router/ChannelRangeQueriesSpec.scala | 2 +- .../fr/acinq/eclair/router/GraphSpec.scala | 4 +-- .../eclair/router/RouteCalculationSpec.scala | 4 +-- .../fr/acinq/eclair/router/RouterSpec.scala | 6 ++-- .../acinq/eclair/router/RoutingSyncSpec.scala | 4 +-- .../transactions/CommitmentSpecSpec.scala | 2 +- .../eclair/transactions/TestVectorsSpec.scala | 5 +-- .../transactions/TransactionsSpec.scala | 7 ++-- .../internal/channel/ChannelCodecsSpec.scala | 4 +-- .../channel/version0/ChannelCodecs0Spec.scala | 2 +- .../channel/version1/ChannelCodecs1Spec.scala | 6 ++-- .../channel/version2/ChannelCodecs2Spec.scala | 2 +- .../channel/version3/ChannelCodecs3Spec.scala | 2 +- .../wire/protocol/CommonCodecsSpec.scala | 4 +-- .../protocol/ExtendedQueriesCodecsSpec.scala | 2 +- .../protocol/FailureMessageCodecsSpec.scala | 2 +- .../protocol/LightningMessageCodecsSpec.scala | 4 +-- .../protocol/MessageOnionCodecsSpec.scala | 4 +-- .../wire/protocol/PaymentOnionSpec.scala | 4 +-- .../wire/protocol/RouteBlindingSpec.scala | 2 +- .../eclair/wire/protocol/TlvCodecsSpec.scala | 2 +- .../scala/fr/acinq/eclair/FrontSetup.scala | 2 +- .../fr/acinq/eclair/router/FrontRouter.scala | 4 +-- .../acinq/eclair/router/FrontRouterSpec.scala | 6 ++-- .../api/directives/ExtraDirectives.scala | 4 +-- .../acinq/eclair/api/handlers/Channel.scala | 2 +- .../acinq/eclair/api/handlers/Invoice.scala | 2 +- .../acinq/eclair/api/handlers/Message.scala | 2 +- .../acinq/eclair/api/handlers/OnChain.scala | 2 +- .../eclair/api/handlers/PathFinding.scala | 2 +- .../acinq/eclair/api/handlers/Payment.scala | 4 +-- .../api/serde/FormParamExtractors.scala | 4 +-- .../fr/acinq/eclair/api/ApiServiceSpec.scala | 4 +-- pom.xml | 2 +- 256 files changed, 546 insertions(+), 466 deletions(-) diff --git a/eclair-core/pom.xml b/eclair-core/pom.xml index 6df076a33b..676b1c7b11 100644 --- a/eclair-core/pom.xml +++ b/eclair-core/pom.xml @@ -163,6 +163,12 @@ com.softwaremill.sttp.client3 okhttp-backend_${scala.version.short} ${sttp.version} + + + org.jetbrains.kotlin + kotlin-stdlib + + diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala index 958bb234be..c8c9767330 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala @@ -22,8 +22,8 @@ import akka.actor.typed.scaladsl.adapter.ClassicSchedulerOps import akka.pattern._ import akka.util.Timeout import com.softwaremill.quicklens.ModifyPimp -import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.bitcoin.{ByteVector32, ByteVector64, Crypto, Satoshi} +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, Crypto, Satoshi} import fr.acinq.eclair.ApiTypes.ChannelNotFound import fr.acinq.eclair.balance.CheckBalance.GlobalBalance import fr.acinq.eclair.balance.{BalanceActor, ChannelsListener} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Logs.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Logs.scala index 93d94c199f..4a045b61c3 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Logs.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Logs.scala @@ -21,8 +21,8 @@ import akka.actor.typed.eventstream.EventStream import akka.actor.typed.scaladsl.Behaviors import akka.event.DiagnosticLoggingAdapter import akka.io.Tcp -import fr.acinq.bitcoin.ByteVector32 -import fr.acinq.bitcoin.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.ByteVector32 +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.ValidateResult import fr.acinq.eclair.channel.{LocalChannelDown, LocalChannelUpdate} import fr.acinq.eclair.crypto.TransportHandler.HandshakeCompleted diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/MilliSatoshi.scala b/eclair-core/src/main/scala/fr/acinq/eclair/MilliSatoshi.scala index f2e78ce589..ccf1d12b47 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/MilliSatoshi.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/MilliSatoshi.scala @@ -16,7 +16,7 @@ package fr.acinq.eclair -import fr.acinq.bitcoin.{Btc, BtcAmount, MilliBtc, Satoshi, btc2satoshi, millibtc2satoshi} +import fr.acinq.bitcoin.scalacompat.{Btc, BtcAmount, MilliBtc, Satoshi, btc2satoshi, millibtc2satoshi} /** * Created by t-bast on 22/08/2019. diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala index 30793c2283..ae01b21058 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala @@ -17,8 +17,8 @@ package fr.acinq.eclair import com.typesafe.config.{Config, ConfigFactory, ConfigValueType} -import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.bitcoin.{Block, ByteVector32, Crypto, Satoshi} +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, Crypto, Satoshi} import fr.acinq.eclair.Setup.Seeds import fr.acinq.eclair.blockchain.fee._ import fr.acinq.eclair.channel.ChannelFlags diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/PluginParams.scala b/eclair-core/src/main/scala/fr/acinq/eclair/PluginParams.scala index 785c54e27d..feedf810e2 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/PluginParams.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/PluginParams.scala @@ -17,8 +17,8 @@ package fr.acinq.eclair import akka.event.LoggingAdapter -import fr.acinq.bitcoin.ByteVector32 -import fr.acinq.bitcoin.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.ByteVector32 +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.eclair.channel.Origin import fr.acinq.eclair.payment.relay.PostRestartHtlcCleaner.IncomingHtlc diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala index 6e331040f0..7bde159236 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala @@ -22,7 +22,7 @@ import akka.actor.typed.scaladsl.adapter.{ClassicActorRefOps, ClassicActorSystem import akka.actor.{ActorRef, ActorSystem, Props, SupervisorStrategy, typed} import akka.pattern.after import akka.util.Timeout -import fr.acinq.bitcoin.{Block, ByteVector32, Satoshi} +import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, Satoshi} import fr.acinq.eclair.Setup.Seeds import fr.acinq.eclair.balance.{BalanceActor, ChannelsListener} import fr.acinq.eclair.blockchain._ diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/balance/BalanceActor.scala b/eclair-core/src/main/scala/fr/acinq/eclair/balance/BalanceActor.scala index b46390d75a..49b60e0da2 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/balance/BalanceActor.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/balance/BalanceActor.scala @@ -3,7 +3,7 @@ package fr.acinq.eclair.balance import akka.actor.typed.eventstream.EventStream import akka.actor.typed.scaladsl.{ActorContext, Behaviors} import akka.actor.typed.{ActorRef, Behavior} -import fr.acinq.bitcoin.{ByteVector32, SatoshiLong} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, SatoshiLong} import fr.acinq.eclair.NotificationsLogger import fr.acinq.eclair.NotificationsLogger.NotifyNodeOperator import fr.acinq.eclair.balance.BalanceActor._ diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/balance/ChannelsListener.scala b/eclair-core/src/main/scala/fr/acinq/eclair/balance/ChannelsListener.scala index 7e3a9dbd2a..4051781fb2 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/balance/ChannelsListener.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/balance/ChannelsListener.scala @@ -6,7 +6,7 @@ import akka.actor.typed.Behavior import akka.actor.typed.eventstream.EventStream import akka.actor.typed.scaladsl.adapter.ClassicActorRefOps import akka.actor.typed.scaladsl.{ActorContext, Behaviors} -import fr.acinq.bitcoin.ByteVector32 +import fr.acinq.bitcoin.scalacompat.ByteVector32 import fr.acinq.eclair.balance.ChannelsListener._ import fr.acinq.eclair.channel.Helpers.Closing import fr.acinq.eclair.channel.{ChannelPersisted, ChannelRestored, HasCommitments} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/balance/CheckBalance.scala b/eclair-core/src/main/scala/fr/acinq/eclair/balance/CheckBalance.scala index dd76256f2c..0a5269d78b 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/balance/CheckBalance.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/balance/CheckBalance.scala @@ -1,7 +1,7 @@ package fr.acinq.eclair.balance import com.softwaremill.quicklens._ -import fr.acinq.bitcoin.{Btc, ByteVector32, Satoshi, SatoshiLong} +import fr.acinq.bitcoin.scalacompat.{Btc, ByteVector32, Satoshi, SatoshiLong} import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinCoreClient import fr.acinq.eclair.channel.Helpers.Closing import fr.acinq.eclair.channel.Helpers.Closing.{CurrentRemoteClose, LocalClose, NextRemoteClose, RemoteClose} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/BlockchainEvents.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/BlockchainEvents.scala index 55d1b54c46..03d815948b 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/BlockchainEvents.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/BlockchainEvents.scala @@ -16,7 +16,7 @@ package fr.acinq.eclair.blockchain -import fr.acinq.bitcoin.{ByteVector32, Transaction} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, Transaction} import fr.acinq.eclair.BlockHeight import fr.acinq.eclair.blockchain.fee.FeeratesPerKw diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/OnChainWallet.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/OnChainWallet.scala index 9a9df7532b..3f62f45009 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/OnChainWallet.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/OnChainWallet.scala @@ -16,8 +16,8 @@ package fr.acinq.eclair.blockchain -import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.bitcoin.{Satoshi, Transaction} +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.{Satoshi, Transaction} import fr.acinq.eclair.blockchain.fee.FeeratePerKw import scodec.bits.ByteVector diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/ZmqWatcher.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/ZmqWatcher.scala index c995f57260..536f205539 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/ZmqWatcher.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/ZmqWatcher.scala @@ -19,7 +19,7 @@ package fr.acinq.eclair.blockchain.bitcoind import akka.actor.typed.eventstream.EventStream import akka.actor.typed.scaladsl.{ActorContext, Behaviors, TimerScheduler} import akka.actor.typed.{ActorRef, Behavior, SupervisorStrategy} -import fr.acinq.bitcoin._ +import fr.acinq.bitcoin.scalacompat._ import fr.acinq.eclair.blockchain.Monitoring.Metrics import fr.acinq.eclair.blockchain._ import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinCoreClient diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/rpc/BasicBitcoinJsonRPCClient.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/rpc/BasicBitcoinJsonRPCClient.scala index 2c7b30dce8..8e3e41fc13 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/rpc/BasicBitcoinJsonRPCClient.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/rpc/BasicBitcoinJsonRPCClient.scala @@ -16,12 +16,12 @@ package fr.acinq.eclair.blockchain.bitcoind.rpc -import fr.acinq.bitcoin.ByteVector32 import fr.acinq.eclair.KamonExt import fr.acinq.eclair.blockchain.Monitoring.{Metrics, Tags} -import org.json4s.JsonAST.{JString, JValue} +import fr.acinq.eclair.json.{ByteVector32KmpSerializer, ByteVector32Serializer} +import org.json4s.DefaultFormats +import org.json4s.JsonAST.JValue import org.json4s.jackson.Serialization -import org.json4s.{CustomSerializer, DefaultFormats} import sttp.client3._ import sttp.client3.json4s._ import sttp.model.StatusCode @@ -34,14 +34,8 @@ import scala.util.{Failure, Success, Try} class BasicBitcoinJsonRPCClient(rpcAuthMethod: BitcoinJsonRPCAuthMethod, host: String = "127.0.0.1", port: Int = 8332, ssl: Boolean = false, wallet: Option[String] = None)(implicit sb: SttpBackend[Future, _]) extends BitcoinJsonRPCClient { - // necessary to properly serialize ByteVector32 into String readable by bitcoind - object ByteVector32Serializer extends CustomSerializer[ByteVector32](_ => ( { - null - }, { - case x: ByteVector32 => JString(x.toHex) - })) + implicit val formats = DefaultFormats.withBigDecimal + ByteVector32Serializer + ByteVector32KmpSerializer - implicit val formats = DefaultFormats.withBigDecimal + ByteVector32Serializer private val scheme = if (ssl) "https" else "http" private val serviceUri = wallet match { case Some(name) => uri"$scheme://$host:$port/wallet/$name" diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/rpc/BitcoinCoreClient.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/rpc/BitcoinCoreClient.scala index 753a092883..018fd52a03 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/rpc/BitcoinCoreClient.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/rpc/BitcoinCoreClient.scala @@ -16,8 +16,9 @@ package fr.acinq.eclair.blockchain.bitcoind.rpc -import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.bitcoin._ +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey +import fr.acinq.bitcoin.{Bech32, Block} +import fr.acinq.bitcoin.scalacompat._ import fr.acinq.eclair.ShortChannelId.coordinates import fr.acinq.eclair.blockchain.OnChainWallet import fr.acinq.eclair.blockchain.OnChainWallet.{MakeFundingTxResponse, OnChainBalance} @@ -32,6 +33,7 @@ import org.json4s.JsonAST._ import scodec.bits.ByteVector import scala.concurrent.{ExecutionContext, Future} +import scala.jdk.CollectionConverters.ListHasAsScala import scala.util.{Failure, Success, Try} /** @@ -130,7 +132,11 @@ class BitcoinCoreClient(val rpcClient: BitcoinJsonRPCClient) extends OnChainWall * @param outputIndex index of the transaction output that has been spent. * @return the transaction spending the given output. */ - def lookForSpendingTx(blockhash_opt: Option[ByteVector32], txid: ByteVector32, outputIndex: Int)(implicit ec: ExecutionContext): Future[Transaction] = + def lookForSpendingTx(blockhash_opt: Option[ByteVector32], txid: ByteVector32, outputIndex: Int)(implicit ec: ExecutionContext): Future[Transaction] = { + lookForSpendingTx(blockhash_opt.map(KotlinUtils.scala2kmp), KotlinUtils.scala2kmp(txid), outputIndex) + } + + def lookForSpendingTx(blockhash_opt: Option[fr.acinq.bitcoin.ByteVector32], txid: fr.acinq.bitcoin.ByteVector32, outputIndex: Int)(implicit ec: ExecutionContext): Future[Transaction] = for { blockhash <- blockhash_opt match { case Some(b) => Future.successful(b) @@ -138,10 +144,10 @@ class BitcoinCoreClient(val rpcClient: BitcoinJsonRPCClient) extends OnChainWall } // with a verbosity of 0, getblock returns the raw serialized block block <- rpcClient.invoke("getblock", blockhash, 0).collect { case JString(b) => Block.read(b) } - prevblockhash = block.header.hashPreviousBlock.reverse - res <- block.tx.find(tx => tx.txIn.exists(i => i.outPoint.txid == txid && i.outPoint.index == outputIndex)) match { + prevblockhash = block.header.hashPreviousBlock.reversed() + res <- block.tx.asScala.find(tx => tx.txIn.asScala.exists(i => i.outPoint.txid == txid && i.outPoint.index == outputIndex)) match { case None => lookForSpendingTx(Some(prevblockhash), txid, outputIndex) - case Some(tx) => Future.successful(tx) + case Some(tx) => Future.successful(KotlinUtils.kmp2scala(tx)) } } yield res @@ -329,7 +335,7 @@ class BitcoinCoreClient(val rpcClient: BitcoinJsonRPCClient) extends OnChainWall def getChangeAddress()(implicit ec: ExecutionContext): Future[ByteVector] = { rpcClient.invoke("getrawchangeaddress", "bech32").collect { case JString(changeAddress) => - val (_, _, pubkeyHash) = Bech32.decodeWitnessAddress(changeAddress) + val pubkeyHash = ByteVector.view(Bech32.decodeWitnessAddress(changeAddress).getThird) pubkeyHash } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/zmq/ZMQActor.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/zmq/ZMQActor.scala index cb34b2d74c..fe8384d6e9 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/zmq/ZMQActor.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/zmq/ZMQActor.scala @@ -18,7 +18,7 @@ package fr.acinq.eclair.blockchain.bitcoind.zmq import akka.Done import akka.actor.{Actor, ActorLogging} -import fr.acinq.bitcoin.{ByteVector32, Transaction} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, Transaction} import fr.acinq.eclair.blockchain.{NewBlock, NewTransaction} import org.zeromq.ZMQ.Event import org.zeromq.{SocketType, ZContext, ZMQ, ZMsg} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/BitcoinCoreFeeProvider.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/BitcoinCoreFeeProvider.scala index 409ed49371..d26e177b30 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/BitcoinCoreFeeProvider.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/BitcoinCoreFeeProvider.scala @@ -16,7 +16,7 @@ package fr.acinq.eclair.blockchain.fee -import fr.acinq.bitcoin._ +import fr.acinq.bitcoin.scalacompat._ import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinJsonRPCClient import org.json4s.DefaultFormats import org.json4s.JsonAST._ diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/FallbackFeeProvider.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/FallbackFeeProvider.scala index 80b064bd6a..dfadb39f69 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/FallbackFeeProvider.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/FallbackFeeProvider.scala @@ -16,7 +16,7 @@ package fr.acinq.eclair.blockchain.fee -import fr.acinq.bitcoin.SatoshiLong +import fr.acinq.bitcoin.scalacompat.SatoshiLong import grizzled.slf4j.Logging import scala.concurrent.{ExecutionContext, Future} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/FeeEstimator.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/FeeEstimator.scala index 25f436d9fd..07d58d7c12 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/FeeEstimator.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/FeeEstimator.scala @@ -16,8 +16,8 @@ package fr.acinq.eclair.blockchain.fee -import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.bitcoin.Satoshi +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.Satoshi import fr.acinq.eclair.blockchain.CurrentFeerates import fr.acinq.eclair.channel.{ChannelTypes, SupportedChannelType} import fr.acinq.eclair.transactions.Transactions diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/FeeProvider.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/FeeProvider.scala index 32e2b2cee5..dc887a559f 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/FeeProvider.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/FeeProvider.scala @@ -16,7 +16,7 @@ package fr.acinq.eclair.blockchain.fee -import fr.acinq.bitcoin.{Satoshi, SatoshiLong} +import fr.acinq.bitcoin.scalacompat.{Satoshi, SatoshiLong} import scala.concurrent.Future diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/watchdogs/ExplorerApi.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/watchdogs/ExplorerApi.scala index bc3b7a7fcc..25acb0b94c 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/watchdogs/ExplorerApi.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/watchdogs/ExplorerApi.scala @@ -20,7 +20,8 @@ import akka.actor.ActorSystem import akka.actor.typed.scaladsl.{ActorContext, Behaviors} import akka.actor.typed.{ActorRef, Behavior} import akka.pattern.after -import fr.acinq.bitcoin.{Block, BlockHeader, ByteVector32} +import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32} +import fr.acinq.bitcoin.BlockHeader import fr.acinq.eclair.blockchain.watchdogs.BlockchainWatchdog.{BlockHeaderAt, LatestHeaders} import fr.acinq.eclair.blockchain.watchdogs.Monitoring.{Metrics, Tags} import fr.acinq.eclair.tor.Socks5ProxyParams @@ -48,6 +49,8 @@ object ExplorerApi { implicit val formats: DefaultFormats = DefaultFormats implicit val serialization: Serialization = Serialization + import fr.acinq.bitcoin.scalacompat.KotlinUtils.scala2kmp + sealed trait Explorer { // @formatter:off /** Explorer friendly-name. */ @@ -163,7 +166,7 @@ object ExplorerApi { val JInt(nonce) = block \ "nonce" val previousBlockHash = (block \ "prev_block").extractOpt[String].map(ByteVector32.fromValidHex(_).reverse).getOrElse(ByteVector32.Zeroes) val merkleRoot = (block \ "mrkl_root").extractOpt[String].map(ByteVector32.fromValidHex(_).reverse).getOrElse(ByteVector32.Zeroes) - val header = BlockHeader(version.toLong, previousBlockHash, merkleRoot, OffsetDateTime.parse(time).toEpochSecond, bits.toLong, nonce.toLong) + val header = new BlockHeader(version.toLong, previousBlockHash, merkleRoot, OffsetDateTime.parse(time).toEpochSecond, bits.toLong, nonce.toLong) Seq(BlockHeaderAt(BlockHeight(height.toLong), header)) }) } yield header @@ -192,7 +195,7 @@ object ExplorerApi { val JInt(nonce) = block \ "nonce" val previousBlockHash = (block \ "previousblockhash").extractOpt[String].map(ByteVector32.fromValidHex(_).reverse).getOrElse(ByteVector32.Zeroes) val merkleRoot = (block \ "merkle_root").extractOpt[String].map(ByteVector32.fromValidHex(_).reverse).getOrElse(ByteVector32.Zeroes) - val header = BlockHeader(version.toLong, previousBlockHash, merkleRoot, time.toLong, bits.toLong, nonce.toLong) + val header = new BlockHeader(version.toLong, previousBlockHash, merkleRoot, time.toLong, bits.toLong, nonce.toLong) BlockHeaderAt(BlockHeight(height.toLong), header) })) .map(headers => LatestHeaders(currentBlockHeight, headers.filter(_.blockHeight >= currentBlockHeight).toSet, name)) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/watchdogs/HeadersOverDns.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/watchdogs/HeadersOverDns.scala index 744a8b7d80..d65ab84670 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/watchdogs/HeadersOverDns.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/watchdogs/HeadersOverDns.scala @@ -22,7 +22,8 @@ import akka.actor.typed.scaladsl.adapter.TypedActorRefOps import akka.actor.typed.{ActorRef, Behavior} import akka.io.dns.{AAAARecord, DnsProtocol} import akka.io.{Dns, IO} -import fr.acinq.bitcoin.{Block, BlockHeader, ByteVector32} +import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32} +import fr.acinq.bitcoin.BlockHeader import fr.acinq.eclair.BlockHeight import fr.acinq.eclair.blockchain.watchdogs.BlockchainWatchdog.BlockHeaderAt import fr.acinq.eclair.blockchain.watchdogs.Monitoring.{Metrics, Tags} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala index b9f33aabfd..ca4f6f3b03 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala @@ -17,8 +17,8 @@ package fr.acinq.eclair.channel import akka.actor.{ActorRef, PossiblyHarmful} -import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.bitcoin.{ByteVector32, DeterministicWallet, OutPoint, Satoshi, Transaction} +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.{ByteVector32, DeterministicWallet, OutPoint, Satoshi, Transaction} import fr.acinq.eclair.blockchain.fee.FeeratePerKw import fr.acinq.eclair.payment.OutgoingPaymentPacket.Upstream import fr.acinq.eclair.transactions.CommitmentSpec diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelEvents.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelEvents.scala index 073ab32e6e..f49958df04 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelEvents.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelEvents.scala @@ -17,8 +17,8 @@ package fr.acinq.eclair.channel import akka.actor.ActorRef -import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.bitcoin.{ByteVector32, Satoshi, Transaction} +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.{ByteVector32, Satoshi, Transaction} import fr.acinq.eclair.{BlockHeight, ShortChannelId} import fr.acinq.eclair.blockchain.fee.FeeratePerKw import fr.acinq.eclair.channel.Helpers.Closing.ClosingType diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelExceptions.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelExceptions.scala index 790baafe0e..624a5a731b 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelExceptions.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelExceptions.scala @@ -16,8 +16,8 @@ package fr.acinq.eclair.channel -import fr.acinq.bitcoin.Crypto.PrivateKey -import fr.acinq.bitcoin.{ByteVector32, Satoshi, Transaction} +import fr.acinq.bitcoin.scalacompat.Crypto.PrivateKey +import fr.acinq.bitcoin.scalacompat.{ByteVector32, Satoshi, Transaction} import fr.acinq.eclair.blockchain.fee.FeeratePerKw import fr.acinq.eclair.wire.protocol.{AnnouncementSignatures, Error, UpdateAddHtlc} import fr.acinq.eclair.{BlockHeight, CltvExpiry, CltvExpiryDelta, MilliSatoshi, UInt64} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala index 6bfdf2def5..08a97a77a1 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala @@ -17,8 +17,8 @@ package fr.acinq.eclair.channel import akka.event.LoggingAdapter -import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey, sha256} -import fr.acinq.bitcoin.{ByteVector32, ByteVector64, Crypto, Satoshi, SatoshiLong} +import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey, sha256} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, Crypto, Satoshi, SatoshiLong} import fr.acinq.eclair._ import fr.acinq.eclair.blockchain.fee.{FeeratePerKw, OnChainFeeConf} import fr.acinq.eclair.channel.Helpers.Closing diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/DustExposure.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/DustExposure.scala index 84c68182b3..2279e87ec1 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/DustExposure.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/DustExposure.scala @@ -16,7 +16,7 @@ package fr.acinq.eclair.channel -import fr.acinq.bitcoin.{Satoshi, SatoshiLong} +import fr.acinq.bitcoin.scalacompat.{Satoshi, SatoshiLong} import fr.acinq.eclair.MilliSatoshi import fr.acinq.eclair.blockchain.fee.{FeeratePerByte, FeeratePerKw} import fr.acinq.eclair.transactions.Transactions.CommitmentFormat diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala index 2bf5dac908..f31e24743d 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala @@ -17,9 +17,10 @@ package fr.acinq.eclair.channel import akka.event.{DiagnosticLoggingAdapter, LoggingAdapter} -import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey, sha256} -import fr.acinq.bitcoin.Script._ -import fr.acinq.bitcoin._ +import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey, sha256} +import fr.acinq.bitcoin.scalacompat.Script._ +import fr.acinq.bitcoin.scalacompat._ +import fr.acinq.bitcoin.ScriptFlags import fr.acinq.eclair._ import fr.acinq.eclair.blockchain.OnChainAddressGenerator import fr.acinq.eclair.blockchain.fee.{FeeEstimator, FeeTargets, FeeratePerKw} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Register.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Register.scala index c3b465c840..8e949012dc 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Register.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Register.scala @@ -17,8 +17,8 @@ package fr.acinq.eclair.channel import akka.actor.{Actor, ActorLogging, ActorRef, Terminated} -import fr.acinq.bitcoin.ByteVector32 -import fr.acinq.bitcoin.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.ByteVector32 +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.eclair.ShortChannelId import fr.acinq.eclair.channel.Register._ diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala index 02716a1d1b..5852e4d086 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala @@ -20,8 +20,8 @@ import akka.actor.typed.scaladsl.Behaviors import akka.actor.typed.scaladsl.adapter.{ClassicActorContextOps, actorRefAdapter} import akka.actor.{Actor, ActorContext, ActorRef, FSM, OneForOneStrategy, PossiblyHarmful, Props, SupervisorStrategy, typed} import akka.event.Logging.MDC -import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} -import fr.acinq.bitcoin.{ByteVector32, Satoshi, SatoshiLong, Transaction} +import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, Satoshi, SatoshiLong, Transaction} import fr.acinq.eclair.Logs.LogCategory import fr.acinq.eclair._ import fr.acinq.eclair.blockchain.OnChainWallet.MakeFundingTxResponse diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenSingleFunder.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenSingleFunder.scala index dfbac9ad8c..12cc7a27e9 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenSingleFunder.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenSingleFunder.scala @@ -19,7 +19,8 @@ package fr.acinq.eclair.channel.fsm import akka.actor.Status import akka.actor.typed.scaladsl.adapter.actorRefAdapter import akka.pattern.pipe -import fr.acinq.bitcoin.{SatoshiLong, Script, ScriptFlags, Transaction} +import fr.acinq.bitcoin.ScriptFlags +import fr.acinq.bitcoin.scalacompat.{SatoshiLong, Script, Transaction} import fr.acinq.eclair.blockchain.OnChainWallet.MakeFundingTxResponse import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._ import fr.acinq.eclair.channel.Helpers.{Funding, getRelayFees} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/CommonHandlers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/CommonHandlers.scala index 87e9416c5a..5c199d33d9 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/CommonHandlers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/CommonHandlers.scala @@ -17,7 +17,7 @@ package fr.acinq.eclair.channel.fsm import akka.actor.FSM -import fr.acinq.bitcoin.ByteVector32 +import fr.acinq.bitcoin.scalacompat.ByteVector32 import fr.acinq.eclair.channel._ import fr.acinq.eclair.db.PendingCommandsDb import fr.acinq.eclair.io.Peer diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ErrorHandlers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ErrorHandlers.scala index e9f9477df7..b03158d1cd 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ErrorHandlers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ErrorHandlers.scala @@ -18,7 +18,7 @@ package fr.acinq.eclair.channel.fsm import akka.actor.typed.scaladsl.adapter.actorRefAdapter import akka.actor.{ActorRef, FSM} -import fr.acinq.bitcoin.{ByteVector32, OutPoint, SatoshiLong, Transaction} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, OutPoint, SatoshiLong, Transaction} import fr.acinq.eclair.NotificationsLogger import fr.acinq.eclair.NotificationsLogger.NotifyNodeOperator import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.{WatchOutputSpent, WatchTxConfirmed} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/FundingHandlers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/FundingHandlers.scala index e918b0d8be..2b3eb92b02 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/FundingHandlers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/FundingHandlers.scala @@ -18,7 +18,7 @@ package fr.acinq.eclair.channel.fsm import akka.actor.Status import akka.actor.typed.scaladsl.adapter.{TypedActorRefOps, actorRefAdapter} -import fr.acinq.bitcoin.{ByteVector32, SatoshiLong, Transaction} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, SatoshiLong, Transaction} import fr.acinq.eclair.BlockHeight import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.{GetTxWithMeta, GetTxWithMetaResponse, WatchFundingSpent} import fr.acinq.eclair.channel._ diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/MempoolTxMonitor.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/MempoolTxMonitor.scala index 031d1089aa..5ba47b7799 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/MempoolTxMonitor.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/MempoolTxMonitor.scala @@ -19,7 +19,7 @@ package fr.acinq.eclair.channel.publish import akka.actor.typed.eventstream.EventStream import akka.actor.typed.scaladsl.{ActorContext, Behaviors, TimerScheduler} import akka.actor.typed.{ActorRef, Behavior} -import fr.acinq.bitcoin.{ByteVector32, OutPoint, Satoshi, Transaction} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, OutPoint, Satoshi, Transaction} import fr.acinq.eclair.blockchain.CurrentBlockHeight import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinCoreClient import fr.acinq.eclair.channel.publish.TxPublisher.{TxPublishContext, TxRejectedReason} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/ReplaceableTxFunder.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/ReplaceableTxFunder.scala index 8a75ef4972..7edffd673b 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/ReplaceableTxFunder.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/ReplaceableTxFunder.scala @@ -19,7 +19,7 @@ package fr.acinq.eclair.channel.publish import akka.actor.typed.eventstream.EventStream import akka.actor.typed.scaladsl.{ActorContext, Behaviors} import akka.actor.typed.{ActorRef, Behavior} -import fr.acinq.bitcoin.{ByteVector32, OutPoint, Satoshi, Script, Transaction, TxOut} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, OutPoint, Satoshi, Script, Transaction, TxOut} import fr.acinq.eclair.NotificationsLogger.NotifyNodeOperator import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinCoreClient import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinCoreClient.FundTransactionOptions diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPrePublisher.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPrePublisher.scala index 43d8c667c0..66fcb3fe82 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPrePublisher.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPrePublisher.scala @@ -18,7 +18,7 @@ package fr.acinq.eclair.channel.publish import akka.actor.typed.scaladsl.{ActorContext, Behaviors} import akka.actor.typed.{ActorRef, Behavior} -import fr.acinq.bitcoin.{ByteVector32, ByteVector64, Crypto, Transaction} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, Crypto, Transaction} import fr.acinq.eclair.NodeParams import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinCoreClient import fr.acinq.eclair.channel.publish.TxPublisher.TxPublishContext diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPublisher.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPublisher.scala index 95a55320f1..3368783710 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPublisher.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPublisher.scala @@ -18,7 +18,7 @@ package fr.acinq.eclair.channel.publish import akka.actor.typed.scaladsl.{ActorContext, Behaviors, TimerScheduler} import akka.actor.typed.{ActorRef, Behavior} -import fr.acinq.bitcoin.{OutPoint, SatoshiLong, Transaction} +import fr.acinq.bitcoin.scalacompat.{OutPoint, SatoshiLong, Transaction} import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinCoreClient import fr.acinq.eclair.blockchain.fee.{FeeEstimator, FeeratePerKw} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/TxPublisher.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/TxPublisher.scala index 499b057f7b..043f1387cf 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/TxPublisher.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/TxPublisher.scala @@ -19,8 +19,8 @@ package fr.acinq.eclair.channel.publish import akka.actor.typed.eventstream.EventStream import akka.actor.typed.scaladsl.{ActorContext, Behaviors, TimerScheduler} import akka.actor.typed.{ActorRef, Behavior} -import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.bitcoin.{ByteVector32, OutPoint, Satoshi, Transaction} +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.{ByteVector32, OutPoint, Satoshi, Transaction} import fr.acinq.eclair.blockchain.CurrentBlockHeight import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinCoreClient diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/TxTimeLocksMonitor.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/TxTimeLocksMonitor.scala index c009834a52..f46c1addba 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/TxTimeLocksMonitor.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/TxTimeLocksMonitor.scala @@ -19,7 +19,7 @@ package fr.acinq.eclair.channel.publish import akka.actor.typed.eventstream.EventStream import akka.actor.typed.scaladsl.{ActorContext, Behaviors, TimerScheduler} import akka.actor.typed.{ActorRef, Behavior} -import fr.acinq.bitcoin.{ByteVector32, Transaction} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, Transaction} import fr.acinq.eclair.blockchain.CurrentBlockHeight import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.{WatchParentTxConfirmed, WatchParentTxConfirmedTriggered} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/ChaCha20Poly1305.scala b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/ChaCha20Poly1305.scala index ecdca6e407..2f3f0425bf 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/ChaCha20Poly1305.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/ChaCha20Poly1305.scala @@ -16,7 +16,7 @@ package fr.acinq.eclair.crypto -import fr.acinq.bitcoin.{ByteVector32, Protocol} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, Protocol} import fr.acinq.eclair.crypto.ChaCha20Poly1305.{DecryptionError, EncryptionError, InvalidCounter} import grizzled.slf4j.Logging import org.bouncycastle.crypto.engines.ChaCha7539Engine diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Generators.scala b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Generators.scala index ae14fbdabd..c5dafa7bcd 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Generators.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Generators.scala @@ -16,8 +16,8 @@ package fr.acinq.eclair.crypto -import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} -import fr.acinq.bitcoin.{ByteVector32, Crypto} +import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, Crypto} import scodec.bits.ByteVector /** diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Mac.scala b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Mac.scala index 011a2b9efc..cc95240791 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Mac.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Mac.scala @@ -16,7 +16,7 @@ package fr.acinq.eclair.crypto -import fr.acinq.bitcoin.ByteVector32 +import fr.acinq.bitcoin.scalacompat.ByteVector32 import org.bouncycastle.crypto.digests.SHA256Digest import org.bouncycastle.crypto.macs.HMac import org.bouncycastle.crypto.params.KeyParameter diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Noise.scala b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Noise.scala index 0539127122..6ba0fb8dd6 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Noise.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Noise.scala @@ -16,8 +16,8 @@ package fr.acinq.eclair.crypto -import fr.acinq.bitcoin.Crypto.PrivateKey -import fr.acinq.bitcoin.{Crypto, Protocol} +import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} +import fr.acinq.bitcoin.scalacompat.{Crypto, Protocol} import fr.acinq.eclair.randomBytes import grizzled.slf4j.Logging import org.bouncycastle.crypto.digests.SHA256Digest @@ -64,10 +64,7 @@ object Noise { * @return sha256(publicKey * keyPair.priv in compressed format) */ override def dh(keyPair: KeyPair, publicKey: ByteVector): ByteVector = { - val point = Crypto.curve.getCurve.decodePoint(publicKey.toArray) - val scalar = new BigInteger(1, keyPair.priv.take(32).toArray) - val point1 = point.multiply(scalar).normalize() - Crypto.sha256(ByteVector.view(point1.getEncoded(true))) + Crypto.ecdh(PrivateKey(keyPair.priv), PublicKey(publicKey)) } override def dhLen: Int = 32 diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Random.scala b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Random.scala index 8159d5a10d..08a2464171 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Random.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Random.scala @@ -16,7 +16,7 @@ package fr.acinq.eclair.crypto -import fr.acinq.bitcoin.Protocol +import fr.acinq.bitcoin.scalacompat.Protocol import org.bouncycastle.crypto.digests.SHA256Digest import org.bouncycastle.crypto.engines.ChaCha7539Engine import org.bouncycastle.crypto.params.{KeyParameter, ParametersWithIV} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/ShaChain.scala b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/ShaChain.scala index 4091c9c08e..44dff50dad 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/ShaChain.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/ShaChain.scala @@ -16,7 +16,7 @@ package fr.acinq.eclair.crypto -import fr.acinq.bitcoin._ +import fr.acinq.bitcoin.scalacompat._ import fr.acinq.eclair.wire.protocol.CommonCodecs import scodec.Codec diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Sphinx.scala b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Sphinx.scala index bd5ec740e9..ed55f2071b 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Sphinx.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Sphinx.scala @@ -16,8 +16,8 @@ package fr.acinq.eclair.crypto -import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} -import fr.acinq.bitcoin.{ByteVector32, Crypto} +import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, Crypto} import fr.acinq.eclair.crypto.Monitoring.{Metrics, Tags} import fr.acinq.eclair.wire.protocol._ import grizzled.slf4j.Logging @@ -68,7 +68,7 @@ object Sphinx extends Logging { * @return a tuple (ephemeral public keys, shared secrets). */ def computeEphemeralPublicKeysAndSharedSecrets(sessionKey: PrivateKey, publicKeys: Seq[PublicKey]): (Seq[PublicKey], Seq[ByteVector32]) = { - val ephemeralPublicKey0 = blind(PublicKey(Crypto.curve.getG), sessionKey.value) + val ephemeralPublicKey0 = blind(PublicKey(fr.acinq.bitcoin.PublicKey.Generator), sessionKey.value) val secret0 = computeSharedSecret(publicKeys.head, sessionKey) val blindingFactor0 = computeBlindingFactor(ephemeralPublicKey0, secret0) computeEphemeralPublicKeysAndSharedSecrets(sessionKey, publicKeys.tail, Seq(ephemeralPublicKey0), Seq(blindingFactor0), Seq(secret0)) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/TransportHandler.scala b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/TransportHandler.scala index 1834908a93..fe836fd088 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/TransportHandler.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/TransportHandler.scala @@ -21,8 +21,8 @@ import akka.event.Logging.MDC import akka.event._ import akka.io.Tcp import akka.util.ByteString -import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.bitcoin.Protocol +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.Protocol import fr.acinq.eclair.Logs.LogCategory import fr.acinq.eclair.crypto.ChaCha20Poly1305.ChaCha20Poly1305Error import fr.acinq.eclair.crypto.Noise._ diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/WeakEntropyPool.scala b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/WeakEntropyPool.scala index 69a686ea57..3f0c53d070 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/WeakEntropyPool.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/WeakEntropyPool.scala @@ -19,8 +19,8 @@ package fr.acinq.eclair.crypto import akka.actor.typed.Behavior import akka.actor.typed.eventstream.EventStream import akka.actor.typed.scaladsl.Behaviors -import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.bitcoin.{ByteVector32, ByteVector64, Crypto} +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, Crypto} import fr.acinq.eclair.TimestampMilli import fr.acinq.eclair.blockchain.NewBlock import fr.acinq.eclair.channel.ChannelSignatureReceived diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/ChannelKeyManager.scala b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/ChannelKeyManager.scala index b1979d3a19..b592f28683 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/ChannelKeyManager.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/ChannelKeyManager.scala @@ -16,9 +16,9 @@ package fr.acinq.eclair.crypto.keymanager -import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} -import fr.acinq.bitcoin.DeterministicWallet.ExtendedPublicKey -import fr.acinq.bitcoin.{ByteVector64, Crypto, DeterministicWallet, Protocol} +import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} +import fr.acinq.bitcoin.scalacompat.DeterministicWallet.ExtendedPublicKey +import fr.acinq.bitcoin.scalacompat.{ByteVector64, Crypto, DeterministicWallet, Protocol} import fr.acinq.eclair.channel.{ChannelConfig, LocalParams} import fr.acinq.eclair.transactions.Transactions.{CommitmentFormat, TransactionWithInputInfo, TxOwner} import scodec.bits.ByteVector diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/LocalChannelKeyManager.scala b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/LocalChannelKeyManager.scala index 9891884b65..03da9f7157 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/LocalChannelKeyManager.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/LocalChannelKeyManager.scala @@ -17,9 +17,9 @@ package fr.acinq.eclair.crypto.keymanager import com.google.common.cache.{CacheBuilder, CacheLoader, LoadingCache} -import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} -import fr.acinq.bitcoin.DeterministicWallet.{derivePrivateKey, _} -import fr.acinq.bitcoin.{Block, ByteVector32, ByteVector64, Crypto, DeterministicWallet} +import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} +import fr.acinq.bitcoin.scalacompat.DeterministicWallet.{derivePrivateKey, _} +import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, ByteVector64, Crypto, DeterministicWallet} import fr.acinq.eclair.crypto.Generators import fr.acinq.eclair.crypto.Monitoring.{Metrics, Tags} import fr.acinq.eclair.router.Announcements @@ -58,7 +58,7 @@ class LocalChannelKeyManager(seed: ByteVector, chainHash: ByteVector32) extends override def load(keyPath: KeyPath): ExtendedPublicKey = publicKey(privateKeys.get(keyPath)) }) - private def internalKeyPath(channelKeyPath: DeterministicWallet.KeyPath, index: Long): List[Long] = (LocalChannelKeyManager.keyBasePath(chainHash) ++ channelKeyPath.path) :+ index + private def internalKeyPath(channelKeyPath: DeterministicWallet.KeyPath, index: Long): KeyPath = KeyPath((LocalChannelKeyManager.keyBasePath(chainHash) ++ channelKeyPath.path) :+ index) private def fundingPrivateKey(channelKeyPath: DeterministicWallet.KeyPath): ExtendedPrivateKey = privateKeys.get(internalKeyPath(channelKeyPath, hardened(0))) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/LocalNodeKeyManager.scala b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/LocalNodeKeyManager.scala index 41750578b5..3ffd713798 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/LocalNodeKeyManager.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/LocalNodeKeyManager.scala @@ -16,9 +16,9 @@ package fr.acinq.eclair.crypto.keymanager -import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} -import fr.acinq.bitcoin.DeterministicWallet.ExtendedPrivateKey -import fr.acinq.bitcoin.{Block, ByteVector32, ByteVector64, Crypto, DeterministicWallet} +import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} +import fr.acinq.bitcoin.scalacompat.DeterministicWallet.ExtendedPrivateKey +import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, ByteVector64, Crypto, DeterministicWallet} import fr.acinq.eclair.router.Announcements import grizzled.slf4j.Logging import scodec.bits.ByteVector diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/NodeKeyManager.scala b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/NodeKeyManager.scala index 3b478aa09d..078b2bfb8c 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/NodeKeyManager.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/NodeKeyManager.scala @@ -1,7 +1,7 @@ package fr.acinq.eclair.crypto.keymanager -import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} -import fr.acinq.bitcoin.{ByteVector32, ByteVector64, DeterministicWallet} +import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, DeterministicWallet} import scodec.bits.ByteVector trait NodeKeyManager { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/AuditDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/AuditDb.scala index 6645430e6c..8ad124ca4b 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/AuditDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/AuditDb.scala @@ -16,8 +16,8 @@ package fr.acinq.eclair.db -import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.bitcoin.{ByteVector32, Satoshi} +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.{ByteVector32, Satoshi} import fr.acinq.eclair.{TimestampMilli, TimestampSecond} import fr.acinq.eclair.channel._ import fr.acinq.eclair.db.AuditDb.{NetworkFee, Stats} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/ChannelsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/ChannelsDb.scala index b676b71a24..585191cf63 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/ChannelsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/ChannelsDb.scala @@ -16,7 +16,7 @@ package fr.acinq.eclair.db -import fr.acinq.bitcoin.ByteVector32 +import fr.acinq.bitcoin.scalacompat.ByteVector32 import fr.acinq.eclair.CltvExpiry import fr.acinq.eclair.channel.HasCommitments import fr.acinq.eclair.db.DbEventHandler.ChannelEvent diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/DbEventHandler.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/DbEventHandler.scala index 13a9c1c8f2..60293b3b81 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/DbEventHandler.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/DbEventHandler.scala @@ -18,8 +18,8 @@ package fr.acinq.eclair.db import akka.actor.{Actor, DiagnosticActorLogging, Props} import akka.event.Logging.MDC -import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.bitcoin.{ByteVector32, Satoshi} +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.{ByteVector32, Satoshi} import fr.acinq.eclair.channel.Helpers.Closing._ import fr.acinq.eclair.channel.Monitoring.{Metrics => ChannelMetrics, Tags => ChannelTags} import fr.acinq.eclair.channel._ diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/DualDatabases.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/DualDatabases.scala index acd3dfae28..512d166355 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/DualDatabases.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/DualDatabases.scala @@ -1,7 +1,7 @@ package fr.acinq.eclair.db import com.google.common.util.concurrent.ThreadFactoryBuilder -import fr.acinq.bitcoin.{ByteVector32, Crypto, Satoshi} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, Crypto, Satoshi} import fr.acinq.eclair.channel._ import fr.acinq.eclair.db.Databases.{FileBackup, PostgresDatabases, SqliteDatabases} import fr.acinq.eclair.db.DbEventHandler.ChannelEvent diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/NetworkDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/NetworkDb.scala index b50815a024..35a071dad6 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/NetworkDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/NetworkDb.scala @@ -16,8 +16,8 @@ package fr.acinq.eclair.db -import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.bitcoin.{ByteVector32, Satoshi} +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.{ByteVector32, Satoshi} import fr.acinq.eclair.ShortChannelId import fr.acinq.eclair.router.Router.PublicChannel import fr.acinq.eclair.wire.protocol.{ChannelAnnouncement, ChannelUpdate, NodeAnnouncement} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala index 38cb89dc44..325382558f 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala @@ -16,8 +16,8 @@ package fr.acinq.eclair.db -import fr.acinq.bitcoin.ByteVector32 -import fr.acinq.bitcoin.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.ByteVector32 +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.eclair.payment._ import fr.acinq.eclair.router.Router.{ChannelHop, Hop, NodeHop} import fr.acinq.eclair.{MilliSatoshi, ShortChannelId, TimestampMilli} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/PeersDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/PeersDb.scala index ab979881fa..5a98a454d4 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/PeersDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/PeersDb.scala @@ -16,7 +16,7 @@ package fr.acinq.eclair.db -import fr.acinq.bitcoin.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.eclair.payment.relay.Relayer.RelayFees import fr.acinq.eclair.wire.protocol.NodeAddress diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/PendingCommandsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/PendingCommandsDb.scala index 47bf04f163..84eee2c10b 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/PendingCommandsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/PendingCommandsDb.scala @@ -18,7 +18,7 @@ package fr.acinq.eclair.db import akka.actor.ActorRef import akka.event.LoggingAdapter -import fr.acinq.bitcoin.ByteVector32 +import fr.acinq.bitcoin.scalacompat.ByteVector32 import fr.acinq.eclair.channel._ import fr.acinq.eclair.wire.protocol.{UpdateFailHtlc, UpdateFailMalformedHtlc, UpdateFulfillHtlc, UpdateMessage} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/jdbc/JdbcUtils.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/jdbc/JdbcUtils.scala index c6771c856b..b3da5d4595 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/jdbc/JdbcUtils.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/jdbc/JdbcUtils.scala @@ -16,7 +16,7 @@ package fr.acinq.eclair.db.jdbc -import fr.acinq.bitcoin.ByteVector32 +import fr.acinq.bitcoin.scalacompat.ByteVector32 import fr.acinq.eclair.MilliSatoshi import grizzled.slf4j.Logger import org.sqlite.SQLiteConnection diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgAuditDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgAuditDb.scala index 0230714b1d..9d12c7fdc0 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgAuditDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgAuditDb.scala @@ -16,8 +16,8 @@ package fr.acinq.eclair.db.pg -import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.bitcoin.{ByteVector32, Satoshi, SatoshiLong} +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.{ByteVector32, Satoshi, SatoshiLong} import fr.acinq.eclair.channel._ import fr.acinq.eclair.db.AuditDb.{NetworkFee, Stats} import fr.acinq.eclair.db.DbEventHandler.ChannelEvent diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgChannelsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgChannelsDb.scala index 1d6499a9c9..0317057add 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgChannelsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgChannelsDb.scala @@ -17,7 +17,7 @@ package fr.acinq.eclair.db.pg import com.zaxxer.hikari.util.IsolationLevel -import fr.acinq.bitcoin.ByteVector32 +import fr.acinq.bitcoin.scalacompat.ByteVector32 import fr.acinq.eclair.CltvExpiry import fr.acinq.eclair.channel.HasCommitments import fr.acinq.eclair.db.ChannelsDb diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgNetworkDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgNetworkDb.scala index 426f6bb1c8..b2322dd05a 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgNetworkDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgNetworkDb.scala @@ -16,7 +16,7 @@ package fr.acinq.eclair.db.pg -import fr.acinq.bitcoin.{ByteVector32, Crypto, Satoshi} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, Crypto, Satoshi} import fr.acinq.eclair.ShortChannelId import fr.acinq.eclair.db.Monitoring.Metrics.withMetrics import fr.acinq.eclair.db.Monitoring.Tags.DbBackends diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgPaymentsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgPaymentsDb.scala index cf7ff97f68..ed3c2d6be0 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgPaymentsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgPaymentsDb.scala @@ -16,8 +16,8 @@ package fr.acinq.eclair.db.pg -import fr.acinq.bitcoin.ByteVector32 -import fr.acinq.bitcoin.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.ByteVector32 +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.eclair.db.Monitoring.Metrics.withMetrics import fr.acinq.eclair.db.Monitoring.Tags.DbBackends import fr.acinq.eclair.db.PaymentsDb.{decodeFailures, decodeRoute, encodeFailures, encodeRoute} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgPeersDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgPeersDb.scala index 9fddafb70e..6154afa345 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgPeersDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgPeersDb.scala @@ -16,8 +16,8 @@ package fr.acinq.eclair.db.pg -import fr.acinq.bitcoin.Crypto -import fr.acinq.bitcoin.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.Crypto +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.eclair.MilliSatoshi import fr.acinq.eclair.db.Monitoring.Metrics.withMetrics import fr.acinq.eclair.db.Monitoring.Tags.DbBackends diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgPendingCommandsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgPendingCommandsDb.scala index 80b9179c42..7ac17b01f3 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgPendingCommandsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgPendingCommandsDb.scala @@ -17,7 +17,7 @@ package fr.acinq.eclair.db.pg -import fr.acinq.bitcoin.ByteVector32 +import fr.acinq.bitcoin.scalacompat.ByteVector32 import fr.acinq.eclair.channel.HtlcSettlementCommand import fr.acinq.eclair.db.Monitoring.Metrics.withMetrics import fr.acinq.eclair.db.Monitoring.Tags.DbBackends diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteAuditDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteAuditDb.scala index ed2f403241..0fba4b971d 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteAuditDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteAuditDb.scala @@ -16,8 +16,8 @@ package fr.acinq.eclair.db.sqlite -import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} -import fr.acinq.bitcoin.{ByteVector32, Satoshi, SatoshiLong} +import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, Satoshi, SatoshiLong} import fr.acinq.eclair.channel._ import fr.acinq.eclair.db.AuditDb.{NetworkFee, Stats} import fr.acinq.eclair.db.DbEventHandler.ChannelEvent diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteChannelsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteChannelsDb.scala index b7dfcad1ea..ea61a527e7 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteChannelsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteChannelsDb.scala @@ -16,7 +16,7 @@ package fr.acinq.eclair.db.sqlite -import fr.acinq.bitcoin.ByteVector32 +import fr.acinq.bitcoin.scalacompat.ByteVector32 import fr.acinq.eclair.{CltvExpiry, TimestampMilli} import fr.acinq.eclair.channel.HasCommitments import fr.acinq.eclair.db.ChannelsDb diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteFeeratesDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteFeeratesDb.scala index 3717fa5bea..48d648aa42 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteFeeratesDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteFeeratesDb.scala @@ -16,7 +16,7 @@ package fr.acinq.eclair.db.sqlite -import fr.acinq.bitcoin.Satoshi +import fr.acinq.bitcoin.scalacompat.Satoshi import fr.acinq.eclair.TimestampMilli import fr.acinq.eclair.blockchain.fee.{FeeratePerKB, FeeratesPerKB} import fr.acinq.eclair.db.FeeratesDb diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteNetworkDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteNetworkDb.scala index 3da17af57e..f5b8cbdc63 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteNetworkDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteNetworkDb.scala @@ -16,7 +16,7 @@ package fr.acinq.eclair.db.sqlite -import fr.acinq.bitcoin.{ByteVector32, Crypto, Satoshi} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, Crypto, Satoshi} import fr.acinq.eclair.ShortChannelId import fr.acinq.eclair.db.Monitoring.Metrics.withMetrics import fr.acinq.eclair.db.Monitoring.Tags.DbBackends diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala index aad5e3a129..b3d216a3a2 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala @@ -16,8 +16,8 @@ package fr.acinq.eclair.db.sqlite -import fr.acinq.bitcoin.ByteVector32 -import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} +import fr.acinq.bitcoin.scalacompat.ByteVector32 +import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} import fr.acinq.eclair.db.Monitoring.Metrics.withMetrics import fr.acinq.eclair.db.Monitoring.Tags.DbBackends import fr.acinq.eclair.db.PaymentsDb.{decodeFailures, decodeRoute, encodeFailures, encodeRoute} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePeersDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePeersDb.scala index 04bfcb2bea..e0e887d7ca 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePeersDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePeersDb.scala @@ -16,8 +16,8 @@ package fr.acinq.eclair.db.sqlite -import fr.acinq.bitcoin.Crypto -import fr.acinq.bitcoin.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.Crypto +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.eclair.MilliSatoshi import fr.acinq.eclair.db.Monitoring.Metrics.withMetrics import fr.acinq.eclair.db.Monitoring.Tags.DbBackends diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePendingCommandsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePendingCommandsDb.scala index 46b6ea80f4..9c2c16ca39 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePendingCommandsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePendingCommandsDb.scala @@ -16,7 +16,7 @@ package fr.acinq.eclair.db.sqlite -import fr.acinq.bitcoin.ByteVector32 +import fr.acinq.bitcoin.scalacompat.ByteVector32 import fr.acinq.eclair.channel.HtlcSettlementCommand import fr.acinq.eclair.db.Monitoring.Metrics.withMetrics import fr.acinq.eclair.db.Monitoring.Tags.DbBackends diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/Client.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/Client.scala index 9e8742c968..3219d6f76f 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/io/Client.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/Client.scala @@ -20,7 +20,7 @@ import akka.actor.{Props, _} import akka.event.Logging.MDC import akka.io.Tcp.SO.KeepAlive import akka.io.{IO, Tcp} -import fr.acinq.bitcoin.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.eclair.Logs import fr.acinq.eclair.Logs.LogCategory import fr.acinq.eclair.crypto.Noise.KeyPair diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/ClientSpawner.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/ClientSpawner.scala index e570279f2e..677cc6ca6c 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/io/ClientSpawner.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/ClientSpawner.scala @@ -20,7 +20,7 @@ import akka.actor.{Actor, ActorLogging, ActorRef, DeadLetter, Props} import akka.cluster.Cluster import akka.cluster.pubsub.DistributedPubSub import akka.cluster.pubsub.DistributedPubSubMediator.Put -import fr.acinq.bitcoin.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.eclair.crypto.Noise.KeyPair import fr.acinq.eclair.remote.EclairInternalsSerializer.RemoteTypes import fr.acinq.eclair.tor.Socks5ProxyParams diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/MessageRelay.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/MessageRelay.scala index d20ba0cdc2..188f090396 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/io/MessageRelay.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/MessageRelay.scala @@ -20,8 +20,8 @@ import akka.actor.typed.Behavior import akka.actor.typed.scaladsl.Behaviors import akka.actor.typed.scaladsl.adapter.TypedActorRefOps import akka.actor.{ActorRef, typed} -import fr.acinq.bitcoin.ByteVector32 -import fr.acinq.bitcoin.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.ByteVector32 +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.eclair.io.Peer.{PeerInfo, PeerInfoResponse} import fr.acinq.eclair.io.Switchboard.GetPeerInfo import fr.acinq.eclair.wire.protocol.OnionMessage diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/NodeURI.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/NodeURI.scala index b5f450a9c5..cb332da292 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/io/NodeURI.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/NodeURI.scala @@ -17,7 +17,7 @@ package fr.acinq.eclair.io import com.google.common.net.HostAndPort -import fr.acinq.bitcoin.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.eclair.wire.protocol.NodeAddress import scodec.bits.ByteVector diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala index f37be95bab..67136eac53 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala @@ -21,8 +21,8 @@ import akka.actor.{Actor, ActorContext, ActorRef, ExtendedActorSystem, FSM, OneF import akka.event.Logging.MDC import akka.event.{BusLogging, DiagnosticLoggingAdapter} import akka.util.Timeout -import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.bitcoin.{ByteVector32, Satoshi, SatoshiLong, Script} +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.{ByteVector32, Satoshi, SatoshiLong, Script} import fr.acinq.eclair.Features.Wumbo import fr.acinq.eclair.Logs.LogCategory import fr.acinq.eclair.NotificationsLogger.NotifyNodeOperator diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/PeerConnection.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/PeerConnection.scala index b50d589247..e437604647 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/io/PeerConnection.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/PeerConnection.scala @@ -18,8 +18,8 @@ package fr.acinq.eclair.io import akka.actor.{ActorRef, FSM, OneForOneStrategy, PoisonPill, Props, SupervisorStrategy, Terminated} import akka.event.Logging.MDC -import fr.acinq.bitcoin.ByteVector32 -import fr.acinq.bitcoin.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.ByteVector32 +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.eclair.Logs.LogCategory import fr.acinq.eclair.crypto.Noise.KeyPair import fr.acinq.eclair.crypto.TransportHandler diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/PeerEvents.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/PeerEvents.scala index 73332db36f..7c03d495d0 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/io/PeerEvents.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/PeerEvents.scala @@ -17,7 +17,7 @@ package fr.acinq.eclair.io import akka.actor.ActorRef -import fr.acinq.bitcoin.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.eclair.wire.protocol import fr.acinq.eclair.wire.protocol.{NodeAddress, UnknownMessage} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/ReconnectionTask.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/ReconnectionTask.scala index 59059e03b2..8946afeb54 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/io/ReconnectionTask.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/ReconnectionTask.scala @@ -21,7 +21,7 @@ import akka.cluster.Cluster import akka.cluster.pubsub.DistributedPubSub import akka.cluster.pubsub.DistributedPubSubMediator.Send import akka.event.Logging.MDC -import fr.acinq.bitcoin.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.eclair.Logs.LogCategory import fr.acinq.eclair.io.Monitoring.Metrics import fr.acinq.eclair.wire.protocol.{NodeAddress, OnionAddress} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/Switchboard.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/Switchboard.scala index d3f958db69..a3ec6355bd 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/io/Switchboard.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/Switchboard.scala @@ -19,8 +19,8 @@ package fr.acinq.eclair.io import akka.actor.typed.scaladsl.Behaviors import akka.actor.typed.scaladsl.adapter.ClassicActorContextOps import akka.actor.{Actor, ActorContext, ActorLogging, ActorRef, OneForOneStrategy, Props, Status, SupervisorStrategy, typed} -import fr.acinq.bitcoin.ByteVector32 -import fr.acinq.bitcoin.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.ByteVector32 +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.eclair.NodeParams import fr.acinq.eclair.blockchain.OnChainAddressGenerator import fr.acinq.eclair.channel.Helpers.Closing diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/json/JsonSerializers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/json/JsonSerializers.scala index 7621a7704f..b296f5cdb2 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/json/JsonSerializers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/json/JsonSerializers.scala @@ -17,8 +17,9 @@ package fr.acinq.eclair.json import com.google.common.net.HostAndPort -import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} -import fr.acinq.bitcoin.{Btc, ByteVector32, ByteVector64, OutPoint, Satoshi, Transaction} +import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} +import fr.acinq.bitcoin.scalacompat.DeterministicWallet.KeyPath +import fr.acinq.bitcoin.scalacompat.{Btc, ByteVector32, ByteVector64, OutPoint, Satoshi, Transaction} import fr.acinq.eclair.balance.CheckBalance.{CorrectedOnChainBalance, GlobalBalance, OffChainBalance} import fr.acinq.eclair.blockchain.fee.FeeratePerKw import fr.acinq.eclair.channel._ @@ -106,6 +107,10 @@ object ByteVector32KeySerializer extends MinimalKeySerializer({ case x: ByteVector32 => x.toHex }) +object ByteVector32KmpSerializer extends MinimalSerializer({ + case x: fr.acinq.bitcoin.ByteVector32 => JString(x.toHex) +}) + object ByteVector64Serializer extends MinimalSerializer({ case x: ByteVector64 => JString(x.toHex) }) @@ -206,6 +211,10 @@ object TransactionSerializer extends MinimalSerializer({ )) }) +object KeyPathSerializer extends MinimalSerializer({ + case x: KeyPath => JObject(JField("path", JArray(x.path.map(x => JLong(x)).toList))) +}) + object TransactionWithInputInfoSerializer extends MinimalSerializer({ case x: HtlcSuccessTx => JObject(List( JField("txid", JString(x.tx.txid.toHex)), @@ -521,6 +530,7 @@ object JsonSerializers { PrivateKeySerializer + TransactionSerializer + TransactionWithInputInfoSerializer + + KeyPathSerializer + InetSocketAddressSerializer + OutPointSerializer + OutPointKeySerializer + diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/message/OnionMessages.scala b/eclair-core/src/main/scala/fr/acinq/eclair/message/OnionMessages.scala index 417b49631d..72e6ac298f 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/message/OnionMessages.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/message/OnionMessages.scala @@ -16,8 +16,8 @@ package fr.acinq.eclair.message -import fr.acinq.bitcoin.ByteVector32 -import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} +import fr.acinq.bitcoin.scalacompat.ByteVector32 +import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} import fr.acinq.eclair.crypto.Sphinx import fr.acinq.eclair.io.MessageRelay.RelayPolicy import fr.acinq.eclair.wire.protocol.MessageOnion.{BlindedFinalPayload, BlindedRelayPayload, FinalPayload, RelayPayload} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/message/Postman.scala b/eclair-core/src/main/scala/fr/acinq/eclair/message/Postman.scala index 2bb92857b0..5a5cff13ba 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/message/Postman.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/message/Postman.scala @@ -19,8 +19,8 @@ package fr.acinq.eclair.message import akka.actor.typed.eventstream.EventStream import akka.actor.typed.scaladsl.Behaviors import akka.actor.typed.{ActorRef, Behavior} -import fr.acinq.bitcoin.ByteVector32 -import fr.acinq.bitcoin.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.ByteVector32 +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.eclair.io.{MessageRelay, Switchboard} import fr.acinq.eclair.message.OnionMessages.ReceiveMessage import fr.acinq.eclair.randomBytes32 diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/package.scala b/eclair-core/src/main/scala/fr/acinq/eclair/package.scala index a60205ce68..4a2cbb3490 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/package.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/package.scala @@ -16,8 +16,9 @@ package fr.acinq -import fr.acinq.bitcoin.Crypto.PrivateKey -import fr.acinq.bitcoin._ +import fr.acinq.bitcoin.{Base58, Base58Check, Bech32} +import fr.acinq.bitcoin.scalacompat.Crypto.PrivateKey +import fr.acinq.bitcoin.scalacompat._ import fr.acinq.eclair.crypto.StrongRandom import fr.acinq.eclair.payment.relay.Relayer.RelayFees import fr.acinq.eclair.wire.protocol.ChannelUpdate @@ -82,14 +83,25 @@ package object eclair { * @return the public key script that matches the input address. */ def addressToPublicKeyScript(address: String, chainHash: ByteVector32): Seq[ScriptElt] = { - Try(Base58Check.decode(address)) match { + + def decodeBase58(input: String): (Byte, ByteVector) = { + val decoded = Base58Check.decode(input) + (decoded.getFirst.byteValue(), ByteVector.view(decoded.getSecond)) + } + + def decodeBech32(input: String): (String, Byte, ByteVector) = { + val decoded = Bech32.decodeWitnessAddress(input) + (decoded.getFirst, decoded.getSecond.byteValue(), ByteVector.view(decoded.getThird)) + } + + Try(decodeBase58(address)) match { case Success((Base58.Prefix.PubkeyAddressTestnet, pubKeyHash)) if chainHash == Block.TestnetGenesisBlock.hash || chainHash == Block.RegtestGenesisBlock.hash => Script.pay2pkh(pubKeyHash) case Success((Base58.Prefix.PubkeyAddress, pubKeyHash)) if chainHash == Block.LivenetGenesisBlock.hash => Script.pay2pkh(pubKeyHash) case Success((Base58.Prefix.ScriptAddressTestnet, scriptHash)) if chainHash == Block.TestnetGenesisBlock.hash || chainHash == Block.RegtestGenesisBlock.hash => OP_HASH160 :: OP_PUSHDATA(scriptHash) :: OP_EQUAL :: Nil case Success((Base58.Prefix.ScriptAddress, scriptHash)) if chainHash == Block.LivenetGenesisBlock.hash => OP_HASH160 :: OP_PUSHDATA(scriptHash) :: OP_EQUAL :: Nil case Success(_) => throw new IllegalArgumentException("base58 address does not match our blockchain") case Failure(base58error) => - Try(Bech32.decodeWitnessAddress(address)) match { + Try(decodeBech32(address)) match { case Success((_, version, _)) if version != 0.toByte => throw new IllegalArgumentException(s"invalid version $version in bech32 address") case Success((_, _, bin)) if bin.length != 20 && bin.length != 32 => throw new IllegalArgumentException("hash length in bech32 address must be either 20 or 32 bytes") case Success(("bc", _, bin)) if chainHash == Block.LivenetGenesisBlock.hash => OP_0 :: OP_PUSHDATA(bin) :: Nil diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Bolt11Invoice.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Bolt11Invoice.scala index 0274948faa..6e2e0630c5 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Bolt11Invoice.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Bolt11Invoice.scala @@ -16,8 +16,9 @@ package fr.acinq.eclair.payment -import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} -import fr.acinq.bitcoin.{Base58, Base58Check, Bech32, Block, ByteVector32, ByteVector64, Crypto} +import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} +import fr.acinq.bitcoin.scalacompat.{ Block, ByteVector32, ByteVector64, Crypto} +import fr.acinq.bitcoin.{Base58, Base58Check, Bech32} import fr.acinq.eclair.{CltvExpiryDelta, Feature, FeatureSupport, Features, InvoiceFeature, MilliSatoshi, MilliSatoshiLong, ShortChannelId, TimestampSecond, randomBytes32} import scodec.bits.{BitVector, ByteOrdering, ByteVector} import scodec.codecs.{list, ubyte} @@ -120,7 +121,7 @@ case class Bolt11Invoice(prefix: String, amount_opt: Option[MilliSatoshi], creat val hrp = s"${prefix}$hramount" val data = Codecs.bolt11DataCodec.encode(Bolt11Data(createdAt, tags, signature)).require val int5s = eight2fiveCodec.decode(data).require.value - Bech32.encode(hrp, int5s.toArray) + Bech32.encode(hrp, int5s.toArray, Bech32.Encoding.Bech32) } } @@ -261,7 +262,10 @@ object Bolt11Invoice { } def fromBase58Address(address: String): FallbackAddress = { - val (prefix, hash) = Base58Check.decode(address) + val (prefix, hash) = { + val decoded = Base58Check.decode(address) + (decoded.getFirst.byteValue(), ByteVector.view(decoded.getSecond)) + } prefix match { case Base58.Prefix.PubkeyAddress => FallbackAddress(17.toByte, hash) case Base58.Prefix.PubkeyAddressTestnet => FallbackAddress(17.toByte, hash) @@ -271,20 +275,23 @@ object Bolt11Invoice { } def fromBech32Address(address: String): FallbackAddress = { - val (_, version, hash) = Bech32.decodeWitnessAddress(address) + val (_, version, hash) = { + val decoded = Bech32.decodeWitnessAddress(address) + (decoded.getFirst, decoded.getSecond, ByteVector.view(decoded.getThird)) + } FallbackAddress(version, hash) } def toAddress(f: FallbackAddress, prefix: String): String = { import f.data f.version match { - case 17 if prefix == "lnbc" => Base58Check.encode(Base58.Prefix.PubkeyAddress, data) - case 18 if prefix == "lnbc" => Base58Check.encode(Base58.Prefix.ScriptAddress, data) - case 17 if prefix == "lntb" || prefix == "lnbcrt" => Base58Check.encode(Base58.Prefix.PubkeyAddressTestnet, data) - case 18 if prefix == "lntb" || prefix == "lnbcrt" => Base58Check.encode(Base58.Prefix.ScriptAddressTestnet, data) - case version if prefix == "lnbc" => Bech32.encodeWitnessAddress("bc", version, data) - case version if prefix == "lntb" => Bech32.encodeWitnessAddress("tb", version, data) - case version if prefix == "lnbcrt" => Bech32.encodeWitnessAddress("bcrt", version, data) + case 17 if prefix == "lnbc" => Base58Check.encode(Base58.Prefix.PubkeyAddress, data.toArray) + case 18 if prefix == "lnbc" => Base58Check.encode(Base58.Prefix.ScriptAddress, data.toArray) + case 17 if prefix == "lntb" || prefix == "lnbcrt" => Base58Check.encode(Base58.Prefix.PubkeyAddressTestnet, data.toArray) + case 18 if prefix == "lntb" || prefix == "lnbcrt" => Base58Check.encode(Base58.Prefix.ScriptAddressTestnet, data.toArray) + case version if prefix == "lnbc" => Bech32.encodeWitnessAddress("bc", version, data.toArray) + case version if prefix == "lntb" => Bech32.encodeWitnessAddress("tb", version, data.toArray) + case version if prefix == "lnbcrt" => Bech32.encodeWitnessAddress("bcrt", version, data.toArray) } } } @@ -509,7 +516,7 @@ object Bolt11Invoice { */ def fromString(input: String): Bolt11Invoice = { // used only for data validation - Bech32.decode(input) + Bech32.decode(input, false) val lowercaseInput = input.toLowerCase val separatorIndex = lowercaseInput.lastIndexOf('1') val hrp = lowercaseInput.take(separatorIndex) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Invoice.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Invoice.scala index bd2ebdfbe5..eebe76e4ee 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Invoice.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Invoice.scala @@ -16,8 +16,8 @@ package fr.acinq.eclair.payment -import fr.acinq.bitcoin.ByteVector32 -import fr.acinq.bitcoin.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.ByteVector32 +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.eclair.{CltvExpiryDelta, Features, InvoiceFeature, MilliSatoshi, TimestampSecond} import scodec.bits.ByteVector diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Monitoring.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Monitoring.scala index 3772216e5e..13a86cd511 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Monitoring.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Monitoring.scala @@ -16,7 +16,7 @@ package fr.acinq.eclair.payment -import fr.acinq.bitcoin.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.eclair.MilliSatoshi import fr.acinq.eclair.channel.CMD_FAIL_HTLC import kamon.Kamon diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentEvents.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentEvents.scala index 85ad38af4b..64ec8c6a82 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentEvents.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentEvents.scala @@ -16,8 +16,8 @@ package fr.acinq.eclair.payment -import fr.acinq.bitcoin.ByteVector32 -import fr.acinq.bitcoin.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.ByteVector32 +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.eclair.crypto.Sphinx import fr.acinq.eclair.payment.Bolt11Invoice.ExtraHop import fr.acinq.eclair.payment.send.PaymentInitiator.SendPaymentConfig diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentPacket.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentPacket.scala index 9294dfe12f..38094ab888 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentPacket.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentPacket.scala @@ -18,8 +18,8 @@ package fr.acinq.eclair.payment import akka.actor.ActorRef import akka.event.LoggingAdapter -import fr.acinq.bitcoin.ByteVector32 -import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} +import fr.acinq.bitcoin.scalacompat.ByteVector32 +import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} import fr.acinq.eclair.channel.{CMD_ADD_HTLC, CMD_FAIL_HTLC, CannotExtractSharedSecret, Origin} import fr.acinq.eclair.crypto.Sphinx import fr.acinq.eclair.router.Router.{ChannelHop, Hop, NodeHop} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/MultiPartHandler.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/MultiPartHandler.scala index 9d699ed335..b461ec4f22 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/MultiPartHandler.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/MultiPartHandler.scala @@ -22,7 +22,7 @@ import akka.actor.typed.scaladsl.Behaviors import akka.actor.typed.scaladsl.adapter.ClassicActorContextOps import akka.actor.{ActorContext, ActorRef, PoisonPill, Status} import akka.event.{DiagnosticLoggingAdapter, LoggingAdapter} -import fr.acinq.bitcoin.{ByteVector32, Crypto} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, Crypto} import fr.acinq.eclair.channel.{CMD_FAIL_HTLC, CMD_FULFILL_HTLC, RES_SUCCESS} import fr.acinq.eclair.db._ import fr.acinq.eclair.payment.Monitoring.{Metrics, Tags} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/MultiPartPaymentFSM.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/MultiPartPaymentFSM.scala index e6babcabfa..4d5fca27f5 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/MultiPartPaymentFSM.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/MultiPartPaymentFSM.scala @@ -18,7 +18,7 @@ package fr.acinq.eclair.payment.receive import akka.actor.{ActorRef, Props} import akka.event.Logging.MDC -import fr.acinq.bitcoin.ByteVector32 +import fr.acinq.bitcoin.scalacompat.ByteVector32 import fr.acinq.eclair.payment.Monitoring.{Metrics, Tags} import fr.acinq.eclair.wire.protocol import fr.acinq.eclair.wire.protocol.{FailureMessage, IncorrectOrUnknownPaymentDetails, UpdateAddHtlc} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/ChannelRelay.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/ChannelRelay.scala index 830eeb52f5..11c3e9dd17 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/ChannelRelay.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/ChannelRelay.scala @@ -21,7 +21,7 @@ import akka.actor.typed.Behavior import akka.actor.typed.eventstream.EventStream import akka.actor.typed.scaladsl.adapter.TypedActorRefOps import akka.actor.typed.scaladsl.{ActorContext, Behaviors} -import fr.acinq.bitcoin.ByteVector32 +import fr.acinq.bitcoin.scalacompat.ByteVector32 import fr.acinq.eclair.channel._ import fr.acinq.eclair.db.PendingCommandsDb import fr.acinq.eclair.payment.Monitoring.{Metrics, Tags} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/ChannelRelayer.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/ChannelRelayer.scala index c8809eb199..14abc832bf 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/ChannelRelayer.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/ChannelRelayer.scala @@ -20,7 +20,7 @@ import akka.actor.ActorRef import akka.actor.typed.Behavior import akka.actor.typed.eventstream.EventStream import akka.actor.typed.scaladsl.Behaviors -import fr.acinq.bitcoin.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.eclair.channel._ import fr.acinq.eclair.payment.IncomingPaymentPacket import fr.acinq.eclair.{Logs, NodeParams, ShortChannelId} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/NodeRelay.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/NodeRelay.scala index f957e4148a..4937f1b642 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/NodeRelay.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/NodeRelay.scala @@ -22,7 +22,7 @@ import akka.actor.typed.eventstream.EventStream import akka.actor.typed.scaladsl.adapter.{TypedActorContextOps, TypedActorRefOps} import akka.actor.typed.scaladsl.{ActorContext, Behaviors} import com.softwaremill.quicklens.ModifyPimp -import fr.acinq.bitcoin.ByteVector32 +import fr.acinq.bitcoin.scalacompat.ByteVector32 import fr.acinq.eclair.channel.{CMD_FAIL_HTLC, CMD_FULFILL_HTLC} import fr.acinq.eclair.db.PendingCommandsDb import fr.acinq.eclair.payment.IncomingPaymentPacket.NodeRelayPacket diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/NodeRelayer.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/NodeRelayer.scala index cecd1cf5cb..20ae577674 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/NodeRelayer.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/NodeRelayer.scala @@ -18,7 +18,7 @@ package fr.acinq.eclair.payment.relay import akka.actor.typed.scaladsl.Behaviors import akka.actor.typed.{ActorRef, Behavior} -import fr.acinq.bitcoin.ByteVector32 +import fr.acinq.bitcoin.scalacompat.ByteVector32 import fr.acinq.eclair.payment._ import fr.acinq.eclair.{Logs, NodeParams} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/PostRestartHtlcCleaner.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/PostRestartHtlcCleaner.scala index 0e7e766d7e..6588c1e3b5 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/PostRestartHtlcCleaner.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/PostRestartHtlcCleaner.scala @@ -19,8 +19,8 @@ package fr.acinq.eclair.payment.relay import akka.Done import akka.actor.{Actor, ActorLogging, ActorRef, Props} import akka.event.LoggingAdapter -import fr.acinq.bitcoin.ByteVector32 -import fr.acinq.bitcoin.Crypto.PrivateKey +import fr.acinq.bitcoin.scalacompat.ByteVector32 +import fr.acinq.bitcoin.scalacompat.Crypto.PrivateKey import fr.acinq.eclair.channel.Helpers.Closing import fr.acinq.eclair.channel._ import fr.acinq.eclair.db._ diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/Relayer.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/Relayer.scala index d69dd2945e..d47d77d09a 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/Relayer.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/Relayer.scala @@ -23,7 +23,7 @@ import akka.actor.typed.scaladsl.adapter.ClassicActorContextOps import akka.actor.{Actor, ActorRef, DiagnosticActorLogging, Props, typed} import akka.event.Logging.MDC import akka.event.LoggingAdapter -import fr.acinq.bitcoin.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.eclair.channel._ import fr.acinq.eclair.db.PendingCommandsDb import fr.acinq.eclair.payment._ diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/Autoprobe.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/Autoprobe.scala index 02ae55a3c6..6250534eb9 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/Autoprobe.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/Autoprobe.scala @@ -17,7 +17,7 @@ package fr.acinq.eclair.payment.send import akka.actor.{Actor, ActorLogging, ActorRef, Props} -import fr.acinq.bitcoin.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.eclair.crypto.Sphinx.DecryptedFailurePacket import fr.acinq.eclair.payment.{PaymentEvent, PaymentFailed, Bolt11Invoice, Invoice, RemoteFailure} import fr.acinq.eclair.router.Router diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/MultiPartPaymentLifecycle.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/MultiPartPaymentLifecycle.scala index d591dae7a2..66d8c00dbf 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/MultiPartPaymentLifecycle.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/MultiPartPaymentLifecycle.scala @@ -18,8 +18,8 @@ package fr.acinq.eclair.payment.send import akka.actor.{ActorRef, FSM, Props, Status} import akka.event.Logging.MDC -import fr.acinq.bitcoin.ByteVector32 -import fr.acinq.bitcoin.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.ByteVector32 +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.eclair.channel.{HtlcOverriddenByLocalCommit, HtlcsTimedoutDownstream, HtlcsWillTimeoutUpstream} import fr.acinq.eclair.db.{OutgoingPayment, OutgoingPaymentStatus, PaymentType} import fr.acinq.eclair.payment.Bolt11Invoice.ExtraHop diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentInitiator.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentInitiator.scala index 94d8f98771..d7f6f8381b 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentInitiator.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentInitiator.scala @@ -17,8 +17,8 @@ package fr.acinq.eclair.payment.send import akka.actor.{Actor, ActorContext, ActorLogging, ActorRef, Props} -import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.bitcoin.{ByteVector32, Crypto} +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.{ByteVector32, Crypto} import fr.acinq.eclair.Features.BasicMultiPartPayment import fr.acinq.eclair.channel.fsm.Channel import fr.acinq.eclair.crypto.Sphinx diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentLifecycle.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentLifecycle.scala index 62f1afdf26..249b2e9616 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentLifecycle.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentLifecycle.scala @@ -18,8 +18,8 @@ package fr.acinq.eclair.payment.send import akka.actor.{ActorRef, FSM, Props, Status} import akka.event.Logging.MDC -import fr.acinq.bitcoin.ByteVector32 -import fr.acinq.bitcoin.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.ByteVector32 +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.eclair._ import fr.acinq.eclair.channel._ import fr.acinq.eclair.crypto.{Sphinx, TransportHandler} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/remote/EclairInternalsSerializer.scala b/eclair-core/src/main/scala/fr/acinq/eclair/remote/EclairInternalsSerializer.scala index 8acaf528f0..590c4a1848 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/remote/EclairInternalsSerializer.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/remote/EclairInternalsSerializer.scala @@ -18,7 +18,7 @@ package fr.acinq.eclair.remote import akka.actor.{ActorRef, ExtendedActorSystem} import akka.serialization.Serialization -import fr.acinq.bitcoin.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.eclair.crypto.TransportHandler import fr.acinq.eclair.io.Peer.PeerRoutingMessage import fr.acinq.eclair.io.Switchboard.RouterPeerConf diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/Announcements.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/Announcements.scala index 74116ba3ca..83b8930183 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/Announcements.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/Announcements.scala @@ -16,8 +16,8 @@ package fr.acinq.eclair.router -import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey, sha256, verifySignature} -import fr.acinq.bitcoin.{ByteVector32, ByteVector64, Crypto, LexicographicalOrdering} +import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey, sha256, verifySignature} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, Crypto, LexicographicalOrdering} import fr.acinq.eclair.wire.protocol._ import fr.acinq.eclair.{CltvExpiryDelta, Feature, Features, MilliSatoshi, NodeFeature, ShortChannelId, TimestampSecond, TimestampSecondLong, serializationResult} import scodec.bits.ByteVector diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/Graph.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/Graph.scala index 70ac131143..f433e28822 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/Graph.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/Graph.scala @@ -16,8 +16,8 @@ package fr.acinq.eclair.router -import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.bitcoin.{Btc, ByteVector32, MilliBtc, Satoshi} +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.{Btc, ByteVector32, MilliBtc, Satoshi} import fr.acinq.eclair._ import fr.acinq.eclair.payment.relay.Relayer.RelayFees import fr.acinq.eclair.router.Graph.GraphStructure.{DirectedGraph, GraphEdge} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/Monitoring.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/Monitoring.scala index e4c2d2f740..bd0e76b068 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/Monitoring.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/Monitoring.scala @@ -16,7 +16,7 @@ package fr.acinq.eclair.router -import fr.acinq.bitcoin.{BtcDouble, MilliBtcDouble, SatoshiLong} +import fr.acinq.bitcoin.scalacompat.{BtcDouble, MilliBtcDouble, SatoshiLong} import fr.acinq.eclair.router.Router.GossipDecision import fr.acinq.eclair.wire.protocol.ChannelUpdate import fr.acinq.eclair.{MilliSatoshi, getSimpleClassName} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/NetworkEvents.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/NetworkEvents.scala index 50f68cfdff..8d781dc7b9 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/NetworkEvents.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/NetworkEvents.scala @@ -16,8 +16,8 @@ package fr.acinq.eclair.router -import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.bitcoin.Satoshi +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.Satoshi import fr.acinq.eclair.ShortChannelId import fr.acinq.eclair.remote.EclairInternalsSerializer.RemoteTypes import fr.acinq.eclair.wire.protocol.{ChannelAnnouncement, ChannelUpdate, NodeAnnouncement} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/RouteCalculation.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/RouteCalculation.scala index 7c6b973fdf..34ab89493d 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/RouteCalculation.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/RouteCalculation.scala @@ -19,8 +19,8 @@ package fr.acinq.eclair.router import akka.actor.{ActorContext, ActorRef, Status} import akka.event.DiagnosticLoggingAdapter import com.softwaremill.quicklens.ModifyPimp -import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.bitcoin.{ByteVector32, ByteVector64, Satoshi, SatoshiLong} +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, Satoshi, SatoshiLong} import fr.acinq.eclair.Logs.LogCategory import fr.acinq.eclair._ import fr.acinq.eclair.payment.Bolt11Invoice.ExtraHop diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala index 5bc3006f48..20745ab0e7 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala @@ -21,8 +21,8 @@ import akka.actor.typed.scaladsl.adapter.actorRefAdapter import akka.actor.{Actor, ActorLogging, ActorRef, Props, Terminated, typed} import akka.event.DiagnosticLoggingAdapter import akka.event.Logging.MDC -import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.bitcoin.{ByteVector32, Satoshi} +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.{ByteVector32, Satoshi} import fr.acinq.eclair.Logs.LogCategory import fr.acinq.eclair._ import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/Sync.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/Sync.scala index cefb61c69a..d1c7f1b990 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/Sync.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/Sync.scala @@ -18,8 +18,8 @@ package fr.acinq.eclair.router import akka.actor.{ActorContext, ActorRef} import akka.event.LoggingAdapter -import fr.acinq.bitcoin.ByteVector32 -import fr.acinq.bitcoin.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.ByteVector32 +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.eclair.crypto.TransportHandler import fr.acinq.eclair.router.Monitoring.{Metrics, Tags} import fr.acinq.eclair.router.Router._ diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/Validation.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/Validation.scala index 66b007c176..5f4d66c73c 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/Validation.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/Validation.scala @@ -19,8 +19,8 @@ package fr.acinq.eclair.router import akka.actor.typed.scaladsl.adapter.actorRefAdapter import akka.actor.{ActorContext, ActorRef, typed} import akka.event.{DiagnosticLoggingAdapter, LoggingAdapter} -import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.bitcoin.Script.{pay2wsh, write} +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.Script.{pay2wsh, write} import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.{UtxoStatus, ValidateRequest, ValidateResult, WatchExternalChannelSpent} import fr.acinq.eclair.channel.{AvailableBalanceChanged, LocalChannelDown, LocalChannelUpdate} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/transactions/CommitmentSpec.scala b/eclair-core/src/main/scala/fr/acinq/eclair/transactions/CommitmentSpec.scala index 414897a7cd..20dd718266 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/transactions/CommitmentSpec.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/transactions/CommitmentSpec.scala @@ -16,7 +16,7 @@ package fr.acinq.eclair.transactions -import fr.acinq.bitcoin.SatoshiLong +import fr.acinq.bitcoin.scalacompat.SatoshiLong import fr.acinq.eclair.MilliSatoshi import fr.acinq.eclair.blockchain.fee.FeeratePerKw import fr.acinq.eclair.transactions.Transactions.{CommitmentFormat, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Scripts.scala b/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Scripts.scala index 9cecd2c059..73213b0210 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Scripts.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Scripts.scala @@ -16,9 +16,12 @@ package fr.acinq.eclair.transactions -import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.bitcoin.Script._ -import fr.acinq.bitcoin._ +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.Script._ +import fr.acinq.bitcoin.scalacompat._ +import fr.acinq.bitcoin.SigHash._ +import fr.acinq.bitcoin.TxIn.{SEQUENCE_LOCKTIME_DISABLE_FLAG, SEQUENCE_LOCKTIME_TYPE_FLAG, SEQUENCE_LOCKTIME_MASK} +import fr.acinq.bitcoin.Script.LockTimeThreshold import fr.acinq.eclair.transactions.Transactions.{AnchorOutputsCommitmentFormat, CommitmentFormat, DefaultCommitmentFormat} import fr.acinq.eclair.{BlockHeight, CltvExpiry, CltvExpiryDelta} import scodec.bits.ByteVector @@ -94,11 +97,11 @@ object Scripts { } private def sequenceToBlockHeight(sequence: Long): Long = { - if ((sequence & TxIn.SEQUENCE_LOCKTIME_DISABLE_FLAG) != 0) { + if ((sequence & SEQUENCE_LOCKTIME_DISABLE_FLAG) != 0) { 0 } else { - require((sequence & TxIn.SEQUENCE_LOCKTIME_TYPE_FLAG) == 0, "CSV timeout must use block heights, not block times") - sequence & TxIn.SEQUENCE_LOCKTIME_MASK + require((sequence & SEQUENCE_LOCKTIME_TYPE_FLAG) == 0, "CSV timeout must use block heights, not block times") + sequence & SEQUENCE_LOCKTIME_MASK } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Transactions.scala b/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Transactions.scala index d7e6f13ace..806a5d55d6 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Transactions.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Transactions.scala @@ -16,10 +16,12 @@ package fr.acinq.eclair.transactions -import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey, ripemd160} -import fr.acinq.bitcoin.Script._ +import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey, ripemd160} +import fr.acinq.bitcoin.scalacompat.Script._ +import fr.acinq.bitcoin.scalacompat._ +import fr.acinq.bitcoin.SigHash._ import fr.acinq.bitcoin.SigVersion._ -import fr.acinq.bitcoin._ +import fr.acinq.bitcoin.ScriptFlags import fr.acinq.eclair._ import fr.acinq.eclair.blockchain.fee.FeeratePerKw import fr.acinq.eclair.transactions.CommitmentOutput._ diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelCodecs0.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelCodecs0.scala index 26232fbf1e..20f6828666 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelCodecs0.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelCodecs0.scala @@ -16,8 +16,9 @@ package fr.acinq.eclair.wire.internal.channel.version0 -import fr.acinq.bitcoin.DeterministicWallet.{ExtendedPrivateKey, KeyPath} -import fr.acinq.bitcoin.{ByteVector32, ByteVector64, Crypto, OutPoint, Transaction, TxOut} +import fr.acinq.bitcoin.scalacompat.DeterministicWallet.{ExtendedPrivateKey, KeyPath} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, Crypto, OutPoint, Transaction, TxOut} +import fr.acinq.eclair.TimestampSecond import fr.acinq.eclair.channel._ import fr.acinq.eclair.crypto.ShaChain import fr.acinq.eclair.transactions.Transactions._ @@ -30,6 +31,7 @@ import fr.acinq.eclair.{BlockHeight, Features, InitFeature, TimestampSecond} import scodec.Codec import scodec.bits.{BitVector, ByteVector} import scodec.codecs._ +import shapeless.{::, HNil} import java.util.UUID @@ -42,14 +44,17 @@ private[channel] object ChannelCodecs0 { private[version0] object Codecs { - val keyPathCodec: Codec[KeyPath] = ("path" | listOfN(uint16, uint32)).xmap[KeyPath](l => new KeyPath(l), keyPath => keyPath.path.toList).as[KeyPath].decodeOnly + val keyPathCodec: Codec[KeyPath] = ("path" | listOfN(uint16, uint32)).xmap[KeyPath](l => KeyPath(l), keyPath => keyPath.path.toList).as[KeyPath].decodeOnly val extendedPrivateKeyCodec: Codec[ExtendedPrivateKey] = ( ("secretkeybytes" | bytes32) :: ("chaincode" | bytes32) :: ("depth" | uint16) :: ("path" | keyPathCodec) :: - ("parent" | int64)).as[ExtendedPrivateKey].decodeOnly + ("parent" | int64)).xmap( + { case a :: b :: c :: d :: e :: HNil => ExtendedPrivateKey(a, b, c, d, e) }, + { exp: ExtendedPrivateKey => exp.secretkeybytes :: exp.chaincode :: exp.depth :: exp.path :: exp.parent :: HNil } + ).decodeOnly val channelVersionCodec: Codec[ChannelTypes0.ChannelVersion] = discriminatorWithDefault[ChannelTypes0.ChannelVersion]( discriminator = discriminated[ChannelTypes0.ChannelVersion].by(byte) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelTypes0.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelTypes0.scala index 24ab351616..785c869137 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelTypes0.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelTypes0.scala @@ -17,8 +17,8 @@ package fr.acinq.eclair.wire.internal.channel.version0 import com.softwaremill.quicklens._ -import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.bitcoin.{ByteVector32, ByteVector64, Crypto, OP_CHECKMULTISIG, OP_PUSHDATA, OutPoint, Satoshi, Script, ScriptWitness, Transaction, TxOut} +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, Crypto, OP_CHECKMULTISIG, OP_PUSHDATA, OutPoint, Satoshi, Script, ScriptWitness, Transaction, TxOut} import fr.acinq.eclair.channel._ import fr.acinq.eclair.crypto.ShaChain import fr.acinq.eclair.transactions.CommitmentSpec diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version1/ChannelCodecs1.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version1/ChannelCodecs1.scala index e667286531..98acb81016 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version1/ChannelCodecs1.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version1/ChannelCodecs1.scala @@ -16,8 +16,8 @@ package fr.acinq.eclair.wire.internal.channel.version1 -import fr.acinq.bitcoin.DeterministicWallet.{ExtendedPrivateKey, KeyPath} -import fr.acinq.bitcoin.{ByteVector32, OutPoint, Transaction, TxOut} +import fr.acinq.bitcoin.scalacompat.DeterministicWallet.{ExtendedPrivateKey, KeyPath} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, OutPoint, Transaction, TxOut} import fr.acinq.eclair.channel._ import fr.acinq.eclair.crypto.ShaChain import fr.acinq.eclair.transactions.Transactions._ @@ -31,19 +31,23 @@ import fr.acinq.eclair.{BlockHeight, Features, InitFeature} import scodec.bits.ByteVector import scodec.codecs._ import scodec.{Attempt, Codec} +import shapeless.{::, HNil} private[channel] object ChannelCodecs1 { private[version1] object Codecs { - val keyPathCodec: Codec[KeyPath] = ("path" | listOfN(uint16, uint32)).xmap[KeyPath](l => new KeyPath(l), keyPath => keyPath.path.toList).as[KeyPath] + val keyPathCodec: Codec[KeyPath] = ("path" | listOfN(uint16, uint32)).xmap[KeyPath](l => KeyPath(l), keyPath => keyPath.path.toList).as[KeyPath] val extendedPrivateKeyCodec: Codec[ExtendedPrivateKey] = ( ("secretkeybytes" | bytes32) :: ("chaincode" | bytes32) :: ("depth" | uint16) :: ("path" | keyPathCodec) :: - ("parent" | int64)).as[ExtendedPrivateKey] + ("parent" | int64)).xmap( + { case a :: b :: c :: d :: e :: HNil => ExtendedPrivateKey(a, b, c, d, e) }, + { exp: ExtendedPrivateKey => exp.secretkeybytes :: exp.chaincode :: exp.depth :: exp.path :: exp.parent :: HNil } + ) val channelVersionCodec: Codec[ChannelTypes0.ChannelVersion] = bits(ChannelTypes0.ChannelVersion.LENGTH_BITS).as[ChannelTypes0.ChannelVersion] diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version2/ChannelCodecs2.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version2/ChannelCodecs2.scala index 1b267a2ec8..99e59338e5 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version2/ChannelCodecs2.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version2/ChannelCodecs2.scala @@ -16,8 +16,8 @@ package fr.acinq.eclair.wire.internal.channel.version2 -import fr.acinq.bitcoin.DeterministicWallet.{ExtendedPrivateKey, KeyPath} -import fr.acinq.bitcoin.{OutPoint, Transaction, TxOut} +import fr.acinq.bitcoin.scalacompat.DeterministicWallet.{ExtendedPrivateKey, KeyPath} +import fr.acinq.bitcoin.scalacompat.{OutPoint, Transaction, TxOut} import fr.acinq.eclair.channel._ import fr.acinq.eclair.crypto.ShaChain import fr.acinq.eclair.transactions.Transactions._ @@ -31,19 +31,23 @@ import fr.acinq.eclair.{BlockHeight, Features, InitFeature} import scodec.bits.ByteVector import scodec.codecs._ import scodec.{Attempt, Codec} +import shapeless.{::, HNil} private[channel] object ChannelCodecs2 { private[version2] object Codecs { - val keyPathCodec: Codec[KeyPath] = ("path" | listOfN(uint16, uint32)).xmap[KeyPath](l => new KeyPath(l), keyPath => keyPath.path.toList).as[KeyPath] + val keyPathCodec: Codec[KeyPath] = ("path" | listOfN(uint16, uint32)).xmap[KeyPath](l => KeyPath(l), keyPath => keyPath.path.toList).as[KeyPath] val extendedPrivateKeyCodec: Codec[ExtendedPrivateKey] = ( ("secretkeybytes" | bytes32) :: ("chaincode" | bytes32) :: ("depth" | uint16) :: ("path" | keyPathCodec) :: - ("parent" | int64)).as[ExtendedPrivateKey] + ("parent" | int64)).xmap( + { case a :: b :: c :: d :: e :: HNil => ExtendedPrivateKey(a, b, c, d, e) }, + { exp: ExtendedPrivateKey => exp.secretkeybytes :: exp.chaincode :: exp.depth :: exp.path :: exp.parent :: HNil } + ) val channelVersionCodec: Codec[ChannelTypes0.ChannelVersion] = bits(ChannelTypes0.ChannelVersion.LENGTH_BITS).as[ChannelTypes0.ChannelVersion] diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3.scala index 633ba5d32a..bfd51eab6a 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3.scala @@ -16,8 +16,8 @@ package fr.acinq.eclair.wire.internal.channel.version3 -import fr.acinq.bitcoin.DeterministicWallet.{ExtendedPrivateKey, KeyPath} -import fr.acinq.bitcoin.{OutPoint, Transaction, TxOut} +import fr.acinq.bitcoin.scalacompat.DeterministicWallet.{ExtendedPrivateKey, KeyPath} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, OutPoint, Transaction, TxOut} import fr.acinq.eclair.channel._ import fr.acinq.eclair.crypto.ShaChain import fr.acinq.eclair.transactions.Transactions._ @@ -29,19 +29,23 @@ import fr.acinq.eclair.{BlockHeight, FeatureSupport, Features, InitFeature} import scodec.bits.{BitVector, ByteVector} import scodec.codecs._ import scodec.{Attempt, Codec} +import shapeless.{::, HNil} private[channel] object ChannelCodecs3 { private[version3] object Codecs { - val keyPathCodec: Codec[KeyPath] = ("path" | listOfN(uint16, uint32)).xmap[KeyPath](l => new KeyPath(l), keyPath => keyPath.path.toList).as[KeyPath] + val keyPathCodec: Codec[KeyPath] = ("path" | listOfN(uint16, uint32)).xmap[KeyPath](l => KeyPath(l), keyPath => keyPath.path.toList).as[KeyPath] val extendedPrivateKeyCodec: Codec[ExtendedPrivateKey] = ( ("secretkeybytes" | bytes32) :: ("chaincode" | bytes32) :: ("depth" | uint16) :: ("path" | keyPathCodec) :: - ("parent" | int64)).as[ExtendedPrivateKey] + ("parent" | int64)).xmap( + { case a :: b :: c :: d :: e :: HNil => ExtendedPrivateKey(a, b, c, d, e) }, + { exp: ExtendedPrivateKey => exp.secretkeybytes :: exp.chaincode :: exp.depth :: exp.path :: exp.parent :: HNil } + ) val channelConfigCodec: Codec[ChannelConfig] = lengthDelimited(bytes).xmap(b => { val activated: Set[ChannelConfigOption] = b.bits.toIndexedSeq.reverse.zipWithIndex.collect { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/ChannelTlv.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/ChannelTlv.scala index 32569b164a..7cf67e0671 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/ChannelTlv.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/ChannelTlv.scala @@ -16,7 +16,7 @@ package fr.acinq.eclair.wire.protocol -import fr.acinq.bitcoin.Satoshi +import fr.acinq.bitcoin.scalacompat.Satoshi import fr.acinq.eclair.channel.{ChannelType, ChannelTypes} import fr.acinq.eclair.wire.protocol.CommonCodecs._ import fr.acinq.eclair.wire.protocol.TlvCodecs.tlvStream diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/CommonCodecs.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/CommonCodecs.scala index 13cb0a41c7..e6c1095fa3 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/CommonCodecs.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/CommonCodecs.scala @@ -16,8 +16,8 @@ package fr.acinq.eclair.wire.protocol -import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} -import fr.acinq.bitcoin.{ByteVector32, ByteVector64, Satoshi} +import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, Satoshi} import fr.acinq.eclair.blockchain.fee.FeeratePerKw import fr.acinq.eclair.channel.ChannelFlags import fr.acinq.eclair.crypto.Mac32 diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/FailureMessage.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/FailureMessage.scala index 15b72b5231..07969828bb 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/FailureMessage.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/FailureMessage.scala @@ -16,7 +16,7 @@ package fr.acinq.eclair.wire.protocol -import fr.acinq.bitcoin.ByteVector32 +import fr.acinq.bitcoin.scalacompat.ByteVector32 import fr.acinq.eclair.crypto.Mac32 import fr.acinq.eclair.wire.protocol.CommonCodecs._ import fr.acinq.eclair.wire.protocol.FailureMessageCodecs.failureMessageCodec diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageTypes.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageTypes.scala index 7db79d5f3a..9880e02781 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageTypes.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageTypes.scala @@ -18,8 +18,8 @@ package fr.acinq.eclair.wire.protocol import com.google.common.base.Charsets import com.google.common.net.InetAddresses -import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} -import fr.acinq.bitcoin.{ByteVector32, ByteVector64, Satoshi} +import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, Satoshi} import fr.acinq.eclair.blockchain.fee.FeeratePerKw import fr.acinq.eclair.channel.{ChannelFlags, ChannelType} import fr.acinq.eclair.{BlockHeight, CltvExpiry, CltvExpiryDelta, Feature, Features, InitFeature, MilliSatoshi, ShortChannelId, TimestampSecond, UInt64} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/MessageOnion.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/MessageOnion.scala index f76c310cf3..2eae728d1c 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/MessageOnion.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/MessageOnion.scala @@ -16,7 +16,7 @@ package fr.acinq.eclair.wire.protocol -import fr.acinq.bitcoin.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.eclair.UInt64 import fr.acinq.eclair.crypto.Sphinx.RouteBlinding.{BlindedNode, BlindedRoute} import fr.acinq.eclair.wire.protocol.OnionRoutingCodecs.{ForbiddenTlv, MissingRequiredTlv} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/OnionRouting.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/OnionRouting.scala index ed40d2f0ca..9cfdf679f5 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/OnionRouting.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/OnionRouting.scala @@ -16,7 +16,7 @@ package fr.acinq.eclair.wire.protocol -import fr.acinq.bitcoin.ByteVector32 +import fr.acinq.bitcoin.scalacompat.ByteVector32 import fr.acinq.eclair.UInt64 import fr.acinq.eclair.wire.protocol.CommonCodecs.bytes32 import scodec.bits.ByteVector diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/PaymentOnion.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/PaymentOnion.scala index bc156dd271..ded53e0362 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/PaymentOnion.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/PaymentOnion.scala @@ -16,8 +16,8 @@ package fr.acinq.eclair.wire.protocol -import fr.acinq.bitcoin.ByteVector32 -import fr.acinq.bitcoin.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.ByteVector32 +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.eclair.payment.{Bolt11Invoice, Invoice} import fr.acinq.eclair.wire.protocol.CommonCodecs._ import fr.acinq.eclair.wire.protocol.OnionRoutingCodecs.MissingRequiredTlv diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/RouteBlinding.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/RouteBlinding.scala index 546383ea72..8244416afc 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/RouteBlinding.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/RouteBlinding.scala @@ -16,7 +16,7 @@ package fr.acinq.eclair.wire.protocol -import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} +import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} import fr.acinq.eclair.crypto.Sphinx import fr.acinq.eclair.{ShortChannelId, UInt64} import scodec.bits.ByteVector diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/SetupAndControlTlv.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/SetupAndControlTlv.scala index 881077fdbf..c1c04307b4 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/SetupAndControlTlv.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/SetupAndControlTlv.scala @@ -16,7 +16,7 @@ package fr.acinq.eclair.wire.protocol -import fr.acinq.bitcoin.ByteVector32 +import fr.acinq.bitcoin.scalacompat.ByteVector32 import fr.acinq.eclair.UInt64 import fr.acinq.eclair.wire.protocol.CommonCodecs._ import fr.acinq.eclair.wire.protocol.TlvCodecs.tlvStream diff --git a/eclair-core/src/test/java/fr/acinq/eclair/MilliSatoshiTest.java b/eclair-core/src/test/java/fr/acinq/eclair/MilliSatoshiTest.java index e48c72cf9e..388c8d5774 100644 --- a/eclair-core/src/test/java/fr/acinq/eclair/MilliSatoshiTest.java +++ b/eclair-core/src/test/java/fr/acinq/eclair/MilliSatoshiTest.java @@ -16,7 +16,7 @@ package fr.acinq.eclair; -import fr.acinq.bitcoin.Satoshi; +import fr.acinq.bitcoin.scalacompat.Satoshi; /** * This class is a compile-time check that we are able to compile Java code that uses MilliSatoshi utilities. diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/EclairImplSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/EclairImplSpec.scala index 2b6155a867..a36188e8f8 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/EclairImplSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/EclairImplSpec.scala @@ -21,8 +21,8 @@ import akka.actor.typed.scaladsl.adapter.{ClassicActorRefOps, actorRefAdapter} import akka.pattern.pipe import akka.testkit.TestProbe import akka.util.Timeout -import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} -import fr.acinq.bitcoin.{Block, ByteVector32, ByteVector64, Crypto, SatoshiLong} +import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} +import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, ByteVector64, Crypto, SatoshiLong} import fr.acinq.eclair.ApiTypes.{ChannelIdentifier, ChannelNotFound} import fr.acinq.eclair.TestConstants._ import fr.acinq.eclair.blockchain.DummyOnChainWallet diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/MilliSatoshiSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/MilliSatoshiSpec.scala index 89c50e5266..09c7f76e1c 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/MilliSatoshiSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/MilliSatoshiSpec.scala @@ -16,7 +16,7 @@ package fr.acinq.eclair -import fr.acinq.bitcoin.{Satoshi, SatoshiLong} +import fr.acinq.bitcoin.scalacompat.{Satoshi, SatoshiLong} import org.scalatest.funsuite.AnyFunSuite /** diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/PackageSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/PackageSpec.scala index 9927d7f818..ef19218dbb 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/PackageSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/PackageSpec.scala @@ -17,8 +17,9 @@ package fr.acinq.eclair import com.google.common.net.InetAddresses -import fr.acinq.bitcoin.Crypto.PrivateKey -import fr.acinq.bitcoin.{Base58, Base58Check, Bech32, Block, ByteVector32, Crypto, Script} +import fr.acinq.bitcoin.{Base58, Base58Check, Bech32} +import fr.acinq.bitcoin.scalacompat.Crypto.PrivateKey +import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, Crypto, Script} import org.scalatest.funsuite.AnyFunSuite import scodec.bits._ @@ -29,6 +30,9 @@ import scala.util.Try */ class PackageSpec extends AnyFunSuite { + implicit def byteVector2array(input: ByteVector): Array[Byte] = input.toArray + + implicit def byteVector322array(input: ByteVector32): Array[Byte] = input.toArray test("compute long channel id") { val data = ((hex"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", 0, hex"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF") :: diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/StartupSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/StartupSpec.scala index 601d8288f8..fb7a86121f 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/StartupSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/StartupSpec.scala @@ -17,8 +17,8 @@ package fr.acinq.eclair import com.typesafe.config.{Config, ConfigFactory} -import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.bitcoin.{Block, SatoshiLong} +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.{Block, SatoshiLong} import fr.acinq.eclair.FeatureSupport.{Mandatory, Optional} import fr.acinq.eclair.Features._ import fr.acinq.eclair.blockchain.fee.{DustTolerance, FeeratePerByte, FeeratePerKw, FeerateTolerance} @@ -168,7 +168,7 @@ class StartupSpec extends AnyFunSuite { """ | override-init-features = [ // optional per-node features | { - | nodeid = "02aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + | nodeid = "031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f", | features { | var_onion_optin = mandatory | payment_secret = mandatory @@ -181,7 +181,7 @@ class StartupSpec extends AnyFunSuite { ) val nodeParams = makeNodeParamsWithDefaults(perNodeConf.withFallback(defaultConf)) - val perNodeFeatures = nodeParams.initFeaturesFor(PublicKey(ByteVector.fromValidHex("02aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"))) + val perNodeFeatures = nodeParams.initFeaturesFor(PublicKey(ByteVector.fromValidHex("031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f"))) assert(perNodeFeatures === Features(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory, BasicMultiPartPayment -> Mandatory, ChannelType -> Optional)) } @@ -190,7 +190,7 @@ class StartupSpec extends AnyFunSuite { """ | override-init-features = [ // optional per-node features | { - | nodeid = "02aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + | nodeid = "031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f", | features { | var_onion_optin = mandatory | payment_secret = mandatory @@ -199,7 +199,7 @@ class StartupSpec extends AnyFunSuite { | } | }, | { - | nodeid = "02bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + | nodeid = "024d4b6cd1361032ca9bd2aeb9d900aa4d45d9ead80ac9423374c451a7254d0766", | features { | var_onion_optin = mandatory | payment_secret = mandatory @@ -219,7 +219,7 @@ class StartupSpec extends AnyFunSuite { """ | on-chain-fees.override-feerate-tolerance = [ | { - | nodeid = "02aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + | nodeid = "031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f", | feerate-tolerance { | ratio-low = 0.1 | ratio-high = 15.0 @@ -231,7 +231,7 @@ class StartupSpec extends AnyFunSuite { | } | }, | { - | nodeid = "02bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + | nodeid = "03462779ad4aad39514614751a71085f2f10e1c7a593e4e030efb5b8721ce55b0b", | feerate-tolerance { | ratio-low = 0.75 | ratio-high = 5.0 @@ -247,9 +247,9 @@ class StartupSpec extends AnyFunSuite { ) val nodeParams = makeNodeParamsWithDefaults(perNodeConf.withFallback(defaultConf)) - assert(nodeParams.onChainFeeConf.feerateToleranceFor(PublicKey(hex"02aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")) === FeerateTolerance(0.1, 15.0, FeeratePerKw(FeeratePerByte(15 sat)), DustTolerance(25_000 sat, closeOnUpdateFeeOverflow = true))) - assert(nodeParams.onChainFeeConf.feerateToleranceFor(PublicKey(hex"02bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb")) === FeerateTolerance(0.75, 5.0, FeeratePerKw(FeeratePerByte(5 sat)), DustTolerance(40_000 sat, closeOnUpdateFeeOverflow = false))) - assert(nodeParams.onChainFeeConf.feerateToleranceFor(PublicKey(hex"02cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc")) === FeerateTolerance(0.5, 10.0, FeeratePerKw(FeeratePerByte(10 sat)), DustTolerance(50_000 sat, closeOnUpdateFeeOverflow = false))) + assert(nodeParams.onChainFeeConf.feerateToleranceFor(PublicKey(hex"031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f")) === FeerateTolerance(0.1, 15.0, FeeratePerKw(FeeratePerByte(15 sat)), DustTolerance(25_000 sat, closeOnUpdateFeeOverflow = true))) + assert(nodeParams.onChainFeeConf.feerateToleranceFor(PublicKey(hex"03462779ad4aad39514614751a71085f2f10e1c7a593e4e030efb5b8721ce55b0b")) === FeerateTolerance(0.75, 5.0, FeeratePerKw(FeeratePerByte(5 sat)), DustTolerance(40_000 sat, closeOnUpdateFeeOverflow = false))) + assert(nodeParams.onChainFeeConf.feerateToleranceFor(PublicKey(hex"0362c0a046dacce86ddd0343c6d3c7c79c2208ba0d9c9cf24a6d046d21d21f90f7")) === FeerateTolerance(0.5, 10.0, FeeratePerKw(FeeratePerByte(10 sat)), DustTolerance(50_000 sat, closeOnUpdateFeeOverflow = false))) } test("NodeParams should fail if htlc-minimum-msat is set to 0") { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/TestBitcoinCoreClient.scala b/eclair-core/src/test/scala/fr/acinq/eclair/TestBitcoinCoreClient.scala index ab9482360f..25d7ee41bd 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/TestBitcoinCoreClient.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/TestBitcoinCoreClient.scala @@ -17,7 +17,7 @@ package fr.acinq.eclair import akka.actor.ActorSystem -import fr.acinq.bitcoin.{ByteVector32, Transaction} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, Transaction} import fr.acinq.eclair.blockchain._ import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinJsonRPCAuthMethod.UserPassword import fr.acinq.eclair.blockchain.bitcoind.rpc.{BasicBitcoinJsonRPCClient, BitcoinCoreClient} diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala b/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala index 2519875061..55fc4fd585 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala @@ -16,7 +16,7 @@ package fr.acinq.eclair -import fr.acinq.bitcoin.{Block, ByteVector32, Satoshi, SatoshiLong, Script} +import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, Satoshi, SatoshiLong, Script} import fr.acinq.eclair.FeatureSupport.{Mandatory, Optional} import fr.acinq.eclair.Features._ import fr.acinq.eclair.blockchain.fee._ diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/balance/CheckBalanceSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/balance/CheckBalanceSpec.scala index df616175f8..4eee5c59bd 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/balance/CheckBalanceSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/balance/CheckBalanceSpec.scala @@ -2,7 +2,7 @@ package fr.acinq.eclair.balance import akka.pattern.pipe import akka.testkit.TestProbe -import fr.acinq.bitcoin.{ByteVector32, SatoshiLong} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, SatoshiLong} import fr.acinq.eclair.balance.CheckBalance.{ClosingBalance, MainAndHtlcBalance, OffChainBalance, PossiblyPublishedMainAndHtlcBalance, PossiblyPublishedMainBalance} import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.{apply => _, _} import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinCoreClient diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/DummyOnChainWallet.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/DummyOnChainWallet.scala index 74f4cbef3f..9cbebadc3a 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/DummyOnChainWallet.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/DummyOnChainWallet.scala @@ -16,8 +16,9 @@ package fr.acinq.eclair.blockchain -import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.bitcoin.{ByteVector32, Crypto, OutPoint, Satoshi, SatoshiLong, Transaction, TxIn, TxOut} +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.{ByteVector32, Crypto, OutPoint, Satoshi, SatoshiLong, Transaction, TxIn, TxOut} +import fr.acinq.bitcoin.TxIn.SEQUENCE_FINAL import fr.acinq.eclair.blockchain.OnChainWallet.{MakeFundingTxResponse, OnChainBalance} import fr.acinq.eclair.blockchain.fee.FeeratePerKw import scodec.bits._ @@ -80,7 +81,7 @@ object DummyOnChainWallet { def makeDummyFundingTx(pubkeyScript: ByteVector, amount: Satoshi): MakeFundingTxResponse = { val fundingTx = Transaction(version = 2, - txIn = TxIn(OutPoint(ByteVector32(ByteVector.fill(32)(1)), 42), signatureScript = Nil, sequence = TxIn.SEQUENCE_FINAL) :: Nil, + txIn = TxIn(OutPoint(ByteVector32(ByteVector.fill(32)(1)), 42), signatureScript = Nil, sequence = SEQUENCE_FINAL) :: Nil, txOut = TxOut(amount, pubkeyScript) :: Nil, lockTime = 0) MakeFundingTxResponse(fundingTx, 0, 420 sat) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/WatcherSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/WatcherSpec.scala index e1ae26cb89..b0fdea68c7 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/WatcherSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/WatcherSpec.scala @@ -16,8 +16,11 @@ package fr.acinq.eclair.blockchain -import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} -import fr.acinq.bitcoin.{OutPoint, SIGHASH_ALL, Satoshi, SatoshiLong, Script, ScriptFlags, ScriptWitness, SigVersion, Transaction, TxIn, TxOut} +import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} +import fr.acinq.bitcoin.scalacompat.{OutPoint, Satoshi, SatoshiLong, Script, ScriptWitness, Transaction, TxIn, TxOut} +import fr.acinq.bitcoin.{SigVersion, ScriptFlags} +import fr.acinq.bitcoin.SigHash.SIGHASH_ALL +import fr.acinq.bitcoin.TxIn.SEQUENCE_FINAL /** * Created by PM on 27/01/2017. @@ -79,9 +82,9 @@ object WatcherSpec { */ def createUnspentTxChain(tx: Transaction, priv: PrivateKey): (Transaction, Transaction) = { // tx1 spends tx - val tx1 = createSpendP2WPKH(tx, priv, priv.publicKey, 10000 sat, TxIn.SEQUENCE_FINAL, 0) + val tx1 = createSpendP2WPKH(tx, priv, priv.publicKey, 10000 sat, SEQUENCE_FINAL, 0) // and tx2 spends tx1 - val tx2 = createSpendP2WPKH(tx1, priv, priv.publicKey, 10000 sat, TxIn.SEQUENCE_FINAL, 0) + val tx2 = createSpendP2WPKH(tx1, priv, priv.publicKey, 10000 sat, SEQUENCE_FINAL, 0) (tx1, tx2) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreClientSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreClientSpec.scala index 0a07224cf8..1a664fb395 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreClientSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreClientSpec.scala @@ -19,9 +19,9 @@ package fr.acinq.eclair.blockchain.bitcoind import akka.actor.Status.Failure import akka.pattern.pipe import akka.testkit.TestProbe -import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.bitcoin.SigVersion.SIGVERSION_WITNESS_V0 -import fr.acinq.bitcoin.{Block, BtcDouble, ByteVector32, MilliBtcDouble, OutPoint, SIGHASH_ALL, Satoshi, SatoshiLong, Script, ScriptFlags, ScriptWitness, Transaction, TxIn, TxOut} +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.{Block, BtcDouble, ByteVector32, MilliBtcDouble, OutPoint, Satoshi, SatoshiLong, Script, ScriptWitness, Transaction, TxIn, TxOut} +import fr.acinq.bitcoin import fr.acinq.eclair.blockchain.OnChainWallet.{MakeFundingTxResponse, OnChainBalance} import fr.acinq.eclair.blockchain.bitcoind.BitcoindService.BitcoinReq import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinCoreClient._ @@ -84,7 +84,7 @@ class BitcoinCoreClientSpec extends TestKitBaseClass with BitcoindService with A assert(fundTxResponse.amountIn > 0.sat) assert(fundTxResponse.fee > 0.sat) fundTxResponse.tx.txIn.foreach(txIn => assert(txIn.signatureScript.isEmpty && txIn.witness.isNull)) - fundTxResponse.tx.txIn.foreach(txIn => assert(txIn.sequence === TxIn.SEQUENCE_FINAL - 2)) + fundTxResponse.tx.txIn.foreach(txIn => assert(txIn.sequence === bitcoin.TxIn.SEQUENCE_FINAL - 2)) bitcoinClient.signTransaction(fundTxResponse.tx, Nil).pipeTo(sender.ref) val signTxResponse = sender.expectMsgType[SignTransactionResponse] @@ -126,7 +126,7 @@ class BitcoinCoreClientSpec extends TestKitBaseClass with BitcoindService with A assert(fundTxResponse.changePosition === Some(1)) assert(!Set(230000 sat, 410000 sat).contains(fundTxResponse.tx.txOut(1).amount)) assert(Set(230000 sat, 410000 sat) === Set(fundTxResponse.tx.txOut.head.amount, fundTxResponse.tx.txOut.last.amount)) - fundTxResponse.tx.txIn.foreach(txIn => assert(txIn.sequence === TxIn.SEQUENCE_FINAL - 1)) + fundTxResponse.tx.txIn.foreach(txIn => assert(txIn.sequence === bitcoin.TxIn.SEQUENCE_FINAL - 1)) } } @@ -407,13 +407,13 @@ class BitcoinCoreClientSpec extends TestKitBaseClass with BitcoindService with A signTxResponse1.tx.txIn.tail.foreach(walletTxIn => assert(walletTxIn.witness.stack.nonEmpty)) // if the non-wallet inputs are signed, bitcoind signs the remaining wallet inputs. - val nonWalletSig = Transaction.signInput(txWithNonWalletInput, 0, Script.pay2pkh(nonWalletKey.publicKey), SIGHASH_ALL, txToRemote.txOut.head.amount, SIGVERSION_WITNESS_V0, nonWalletKey) + val nonWalletSig = Transaction.signInput(txWithNonWalletInput, 0, Script.pay2pkh(nonWalletKey.publicKey), bitcoin.SigHash.SIGHASH_ALL, txToRemote.txOut.head.amount, bitcoin.SigVersion.SIGVERSION_WITNESS_V0, nonWalletKey) val nonWalletWitness = ScriptWitness(Seq(nonWalletSig, nonWalletKey.publicKey.value)) val txWithSignedNonWalletInput = txWithNonWalletInput.updateWitness(0, nonWalletWitness) bitcoinClient.signTransaction(txWithSignedNonWalletInput, Nil).pipeTo(sender.ref) val signTxResponse2 = sender.expectMsgType[SignTransactionResponse] assert(signTxResponse2.complete) - Transaction.correctlySpends(signTxResponse2.tx, txToRemote +: walletInputTxs, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + Transaction.correctlySpends(signTxResponse2.tx, txToRemote +: walletInputTxs, bitcoin.ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) } { // bitcoind does not sign inputs that have already been confirmed. @@ -441,7 +441,7 @@ class BitcoinCoreClientSpec extends TestKitBaseClass with BitcoindService with A bitcoinClient.fundTransaction(Transaction(2, Nil, Seq(TxOut(350000 sat, Script.pay2wpkh(randomKey().publicKey))), 0), opts).pipeTo(sender.ref) val fundTxResponse = sender.expectMsgType[FundTransactionResponse] val txWithUnconfirmedInput = fundTxResponse.tx.copy(txIn = TxIn(OutPoint(unconfirmedTx, 0), ByteVector.empty, 0) +: fundTxResponse.tx.txIn) - val nonWalletSig = Transaction.signInput(txWithUnconfirmedInput, 0, Script.pay2pkh(nonWalletKey.publicKey), SIGHASH_ALL, unconfirmedTx.txOut.head.amount, SIGVERSION_WITNESS_V0, nonWalletKey) + val nonWalletSig = Transaction.signInput(txWithUnconfirmedInput, 0, Script.pay2pkh(nonWalletKey.publicKey), bitcoin.SigHash.SIGHASH_ALL, unconfirmedTx.txOut.head.amount, bitcoin.SigVersion.SIGVERSION_WITNESS_V0, nonWalletKey) val nonWalletWitness = ScriptWitness(Seq(nonWalletSig, nonWalletKey.publicKey.value)) val txWithSignedUnconfirmedInput = txWithUnconfirmedInput.updateWitness(0, nonWalletWitness) val previousTx = PreviousTx(Transactions.InputInfo(OutPoint(unconfirmedTx.txid, 0), unconfirmedTx.txOut.head, Script.pay2pkh(nonWalletKey.publicKey)), nonWalletWitness) @@ -481,7 +481,7 @@ class BitcoinCoreClientSpec extends TestKitBaseClass with BitcoindService with A bitcoinrpcclient.invoke("createrawtransaction", Array(Map("txid" -> tx.txid.toHex, "vout" -> pos)), Map(address -> 5.999)).pipeTo(sender.ref) val JString(unsignedTxStr) = sender.expectMsgType[JValue] val unsignedTx = Transaction.read(unsignedTxStr) - val sig = Transaction.signInput(unsignedTx, 0, Script.pay2pkh(priv.publicKey), SIGHASH_ALL, 6.btc.toSatoshi, SIGVERSION_WITNESS_V0, priv) + val sig = Transaction.signInput(unsignedTx, 0, Script.pay2pkh(priv.publicKey), bitcoin.SigHash.SIGHASH_ALL, 6.btc.toSatoshi, bitcoin.SigVersion.SIGVERSION_WITNESS_V0, priv) unsignedTx.updateWitness(0, Script.witnessPay2wpkh(priv.publicKey, sig)) } bitcoinClient.publishTransaction(spendingTx).pipeTo(sender.ref) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoindService.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoindService.scala index 1135514485..d51266e49c 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoindService.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoindService.scala @@ -19,8 +19,8 @@ package fr.acinq.eclair.blockchain.bitcoind import akka.actor.{Actor, ActorRef, ActorSystem, Props} import akka.pattern.pipe import akka.testkit.{TestKitBase, TestProbe} -import fr.acinq.bitcoin.Crypto.PrivateKey -import fr.acinq.bitcoin.{Block, Btc, BtcAmount, ByteVector32, MilliBtc, OutPoint, Satoshi, Transaction, computeP2WpkhAddress} +import fr.acinq.bitcoin.scalacompat.Crypto.PrivateKey +import fr.acinq.bitcoin.scalacompat.{Block, Btc, BtcAmount, ByteVector32, MilliBtc, OutPoint, Satoshi, Transaction, computeP2WpkhAddress} import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinJsonRPCAuthMethod.{SafeCookie, UserPassword} import fr.acinq.eclair.blockchain.bitcoind.rpc.{BasicBitcoinJsonRPCClient, BitcoinJsonRPCAuthMethod, BitcoinJsonRPCClient} import fr.acinq.eclair.integration.IntegrationSpec diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/ZmqWatcherSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/ZmqWatcherSpec.scala index 892301371e..ff42206b52 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/ZmqWatcherSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/ZmqWatcherSpec.scala @@ -21,7 +21,7 @@ import akka.actor.typed.scaladsl.adapter.{ClassicActorSystemOps, TypedActorRefOp import akka.actor.{ActorRef, Props, typed} import akka.pattern.pipe import akka.testkit.TestProbe -import fr.acinq.bitcoin.{Block, Btc, MilliBtcDouble, OutPoint, SatoshiLong, Script, Transaction, TxOut} +import fr.acinq.bitcoin.scalacompat.{Block, Btc, MilliBtcDouble, OutPoint, SatoshiLong, Script, Transaction, TxOut} import fr.acinq.eclair.blockchain.WatcherSpec._ import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._ import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinCoreClient diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/BitcoinCoreFeeProviderSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/BitcoinCoreFeeProviderSpec.scala index 71142a332d..732f7abe76 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/BitcoinCoreFeeProviderSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/BitcoinCoreFeeProviderSpec.scala @@ -18,7 +18,7 @@ package fr.acinq.eclair.blockchain.fee import akka.pattern.pipe import akka.testkit.TestProbe -import fr.acinq.bitcoin._ +import fr.acinq.bitcoin.scalacompat._ import fr.acinq.eclair.TestKitBaseClass import fr.acinq.eclair.blockchain.bitcoind.BitcoindService import fr.acinq.eclair.blockchain.bitcoind.rpc.BasicBitcoinJsonRPCClient diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/DbFeeProviderSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/DbFeeProviderSpec.scala index 4315b44f59..c650bc9a17 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/DbFeeProviderSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/DbFeeProviderSpec.scala @@ -17,7 +17,7 @@ package fr.acinq.eclair.blockchain.fee import akka.util.Timeout -import fr.acinq.bitcoin.SatoshiLong +import fr.acinq.bitcoin.scalacompat.SatoshiLong import fr.acinq.eclair.TestDatabases import fr.acinq.eclair.db.sqlite.SqliteFeeratesDb import org.scalatest.funsuite.AnyFunSuite diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/FallbackFeeProviderSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/FallbackFeeProviderSpec.scala index 00cc29c7df..4969ddcb19 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/FallbackFeeProviderSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/FallbackFeeProviderSpec.scala @@ -16,7 +16,7 @@ package fr.acinq.eclair.blockchain.fee -import fr.acinq.bitcoin.SatoshiLong +import fr.acinq.bitcoin.scalacompat.SatoshiLong import org.scalatest.funsuite.AnyFunSuite import scala.concurrent.duration._ diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/FeeEstimatorSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/FeeEstimatorSpec.scala index 138ad1c8e5..9ab3f7380a 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/FeeEstimatorSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/FeeEstimatorSpec.scala @@ -16,7 +16,7 @@ package fr.acinq.eclair.blockchain.fee -import fr.acinq.bitcoin.SatoshiLong +import fr.acinq.bitcoin.scalacompat.SatoshiLong import fr.acinq.eclair.blockchain.CurrentFeerates import fr.acinq.eclair.channel.ChannelTypes import fr.acinq.eclair.{TestFeeEstimator, randomKey} diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/FeeProviderSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/FeeProviderSpec.scala index be9c0e16a8..efeda2cee2 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/FeeProviderSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/FeeProviderSpec.scala @@ -16,7 +16,7 @@ package fr.acinq.eclair.blockchain.fee -import fr.acinq.bitcoin.SatoshiLong +import fr.acinq.bitcoin.scalacompat.SatoshiLong import fr.acinq.eclair.blockchain.fee.FeeratePerKw.MinimumFeeratePerKw import org.scalatest.funsuite.AnyFunSuite diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/SmoothFeeProviderSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/SmoothFeeProviderSpec.scala index 0267b5b747..05160c8c98 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/SmoothFeeProviderSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/SmoothFeeProviderSpec.scala @@ -16,7 +16,7 @@ package fr.acinq.eclair.blockchain.fee -import fr.acinq.bitcoin.SatoshiLong +import fr.acinq.bitcoin.scalacompat.SatoshiLong import org.scalatest.funsuite.AnyFunSuite import scala.concurrent.ExecutionContext.Implicits.global diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/watchdogs/BlockchainWatchdogSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/watchdogs/BlockchainWatchdogSpec.scala index 68f71c0efb..24b73080d3 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/watchdogs/BlockchainWatchdogSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/watchdogs/BlockchainWatchdogSpec.scala @@ -19,7 +19,7 @@ package fr.acinq.eclair.blockchain.watchdogs import akka.actor.testkit.typed.scaladsl.{ScalaTestWithActorTestKit, TestProbe} import akka.actor.typed.eventstream.EventStream import com.typesafe.config.ConfigFactory -import fr.acinq.bitcoin.Block +import fr.acinq.bitcoin.scalacompat.Block import fr.acinq.eclair.blockchain.watchdogs.BlockchainWatchdog.{DangerousBlocksSkew, WrappedCurrentBlockHeight} import fr.acinq.eclair.tor.Socks5ProxyParams import fr.acinq.eclair.{BlockHeight, NodeParams, TestConstants, TestTags} diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/watchdogs/ExplorerApiSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/watchdogs/ExplorerApiSpec.scala index 2d47ca8e8a..8c1328bcb3 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/watchdogs/ExplorerApiSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/watchdogs/ExplorerApiSpec.scala @@ -18,7 +18,7 @@ package fr.acinq.eclair.blockchain.watchdogs import akka.actor.testkit.typed.scaladsl.ScalaTestWithActorTestKit import com.typesafe.config.ConfigFactory -import fr.acinq.bitcoin.Block +import fr.acinq.bitcoin.scalacompat.Block import fr.acinq.eclair.blockchain.watchdogs.BlockchainWatchdog.LatestHeaders import fr.acinq.eclair.blockchain.watchdogs.ExplorerApi.{BlockcypherExplorer, BlockstreamExplorer, CheckLatestHeaders, MempoolSpaceExplorer} import fr.acinq.eclair.{BlockHeight, TestTags} diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/watchdogs/HeadersOverDnsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/watchdogs/HeadersOverDnsSpec.scala index 410ae07bc5..27cd7f45df 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/watchdogs/HeadersOverDnsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/watchdogs/HeadersOverDnsSpec.scala @@ -18,7 +18,8 @@ package fr.acinq.eclair.blockchain.watchdogs import akka.actor.testkit.typed.scaladsl.ScalaTestWithActorTestKit import com.typesafe.config.ConfigFactory -import fr.acinq.bitcoin.{Block, BlockHeader} +import fr.acinq.bitcoin.scalacompat.Block +import fr.acinq.bitcoin.BlockHeader import fr.acinq.eclair.blockchain.watchdogs.BlockchainWatchdog.{BlockHeaderAt, LatestHeaders} import fr.acinq.eclair.blockchain.watchdogs.HeadersOverDns.CheckLatestHeaders import fr.acinq.eclair.{BlockHeight, TestTags} diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/ChannelDataSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/ChannelDataSpec.scala index 6aac08cfc1..1b24852f85 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/ChannelDataSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/ChannelDataSpec.scala @@ -17,7 +17,7 @@ package fr.acinq.eclair.channel import akka.testkit.{TestFSMRef, TestProbe} -import fr.acinq.bitcoin.{ByteVector32, OutPoint, SatoshiLong, Transaction, TxIn, TxOut} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, OutPoint, SatoshiLong, Transaction, TxIn, TxOut} import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.WatchFundingSpentTriggered import fr.acinq.eclair.channel.Helpers.Closing import fr.acinq.eclair.channel.fsm.Channel diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/CommitmentsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/CommitmentsSpec.scala index 3407b31375..23ad1c6edc 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/CommitmentsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/CommitmentsSpec.scala @@ -16,8 +16,8 @@ package fr.acinq.eclair.channel -import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.bitcoin.{ByteVector64, DeterministicWallet, Satoshi, SatoshiLong, Transaction} +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.{ByteVector64, DeterministicWallet, Satoshi, SatoshiLong, Transaction} import fr.acinq.eclair.blockchain.fee._ import fr.acinq.eclair.channel.Commitments._ import fr.acinq.eclair.channel.Helpers.Funding diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/DustExposureSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/DustExposureSpec.scala index 6dbff89b48..0bb2fda155 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/DustExposureSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/DustExposureSpec.scala @@ -16,7 +16,7 @@ package fr.acinq.eclair.channel -import fr.acinq.bitcoin.{ByteVector32, SatoshiLong} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, SatoshiLong} import fr.acinq.eclair.blockchain.fee.{FeeratePerByte, FeeratePerKw} import fr.acinq.eclair.transactions._ import fr.acinq.eclair.wire.protocol.UpdateAddHtlc diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/FuzzySpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/FuzzySpec.scala index 84a21817c4..fa4daca90f 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/FuzzySpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/FuzzySpec.scala @@ -19,8 +19,8 @@ package fr.acinq.eclair.channel import akka.actor.typed.scaladsl.adapter.actorRefAdapter import akka.actor.{Actor, ActorLogging, ActorRef, Props} import akka.testkit.{TestFSMRef, TestProbe} -import fr.acinq.bitcoin.ByteVector32 -import fr.acinq.bitcoin.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.ByteVector32 +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.eclair.TestConstants.{Alice, Bob} import fr.acinq.eclair._ import fr.acinq.eclair.blockchain.DummyOnChainWallet diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/HelpersSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/HelpersSpec.scala index 20f59543a8..38b1fb6fe6 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/HelpersSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/HelpersSpec.scala @@ -18,7 +18,7 @@ package fr.acinq.eclair.channel import akka.testkit.{TestFSMRef, TestProbe} import com.softwaremill.quicklens.ModifyPimp -import fr.acinq.bitcoin._ +import fr.acinq.bitcoin.scalacompat._ import fr.acinq.eclair.TestConstants.Alice.nodeParams import fr.acinq.eclair.TestUtils.NoLoggingDiagnostics import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.WatchFundingSpentTriggered diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/RegisterSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/RegisterSpec.scala index b32ec0d9bd..c274b1a14e 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/RegisterSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/RegisterSpec.scala @@ -4,8 +4,8 @@ import fr.acinq.eclair._ import akka.actor.{ActorRef, Props} import akka.testkit.TestProbe -import fr.acinq.bitcoin.ByteVector32 -import fr.acinq.bitcoin.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.ByteVector32 +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import org.scalatest.funsuite.AnyFunSuiteLike import org.scalatest.ParallelTestExecution diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/RestoreSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/RestoreSpec.scala index ad29c6cf72..a749e0ea0b 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/RestoreSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/RestoreSpec.scala @@ -5,8 +5,10 @@ import akka.actor.typed.scaladsl.adapter.actorRefAdapter import akka.testkit import akka.testkit.{TestActor, TestFSMRef, TestProbe} import com.softwaremill.quicklens.ModifyPimp -import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.bitcoin._ +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat._ +import fr.acinq.bitcoin.ScriptFlags +import fr.acinq.bitcoin import fr.acinq.eclair.TestConstants.{Alice, Bob} import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.WatchFundingSpentTriggered import fr.acinq.eclair.channel.fsm.Channel @@ -117,7 +119,7 @@ class RestoreSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Chan // spend our output val tx = Transaction(version = 2, - txIn = TxIn(OutPoint(bobCommitTx, bobCommitTx.txOut.indexOf(ourOutput)), sequence = TxIn.SEQUENCE_FINAL, signatureScript = Nil) :: Nil, + txIn = TxIn(OutPoint(bobCommitTx, bobCommitTx.txOut.indexOf(ourOutput)), sequence = bitcoin.TxIn.SEQUENCE_FINAL, signatureScript = Nil) :: Nil, txOut = TxOut(Satoshi(1000), Script.pay2pkh(fr.acinq.eclair.randomKey().publicKey)) :: Nil, lockTime = 0) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/FinalTxPublisherSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/FinalTxPublisherSpec.scala index f007ac7d5f..a605788918 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/FinalTxPublisherSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/FinalTxPublisherSpec.scala @@ -20,7 +20,7 @@ import akka.actor.typed.ActorRef import akka.actor.typed.scaladsl.adapter.{ClassicActorSystemOps, TypedActorRefOps, actorRefAdapter} import akka.pattern.pipe import akka.testkit.TestProbe -import fr.acinq.bitcoin.{ByteVector32, SatoshiLong, Transaction} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, SatoshiLong, Transaction} import fr.acinq.eclair.blockchain.CurrentBlockHeight import fr.acinq.eclair.blockchain.WatcherSpec.createSpendP2WPKH import fr.acinq.eclair.blockchain.bitcoind.BitcoindService diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/MempoolTxMonitorSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/MempoolTxMonitorSpec.scala index 5533916a1c..05351805ab 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/MempoolTxMonitorSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/MempoolTxMonitorSpec.scala @@ -20,8 +20,8 @@ import akka.actor.typed.ActorRef import akka.actor.typed.scaladsl.adapter.{ClassicActorSystemOps, TypedActorRefOps, actorRefAdapter} import akka.pattern.pipe import akka.testkit.TestProbe -import fr.acinq.bitcoin.Crypto.PrivateKey -import fr.acinq.bitcoin.{ByteVector32, OutPoint, SatoshiLong, Transaction, TxIn} +import fr.acinq.bitcoin.scalacompat.Crypto.PrivateKey +import fr.acinq.bitcoin.scalacompat.{ByteVector32, OutPoint, SatoshiLong, Transaction, TxIn} import fr.acinq.eclair.blockchain.CurrentBlockHeight import fr.acinq.eclair.blockchain.WatcherSpec.{createSpendManyP2WPKH, createSpendP2WPKH} import fr.acinq.eclair.blockchain.bitcoind.BitcoindService diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/ReplaceableTxFunderSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/ReplaceableTxFunderSpec.scala index d44f10dd28..2315b2924f 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/ReplaceableTxFunderSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/ReplaceableTxFunderSpec.scala @@ -16,7 +16,7 @@ package fr.acinq.eclair.channel.publish -import fr.acinq.bitcoin.{ByteVector32, Crypto, OutPoint, SatoshiLong, Script, Transaction, TxIn, TxOut} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, Crypto, OutPoint, SatoshiLong, Script, Transaction, TxIn, TxOut} import fr.acinq.eclair.blockchain.fee.FeeratePerKw import fr.acinq.eclair.channel.Helpers.Funding import fr.acinq.eclair.channel.publish.ReplaceableTxFunder.AdjustPreviousTxOutputResult.{AddWalletInputs, TxOutputAdjusted} diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPublisherSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPublisherSpec.scala index 53c4734cfd..cf1a130cdd 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPublisherSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPublisherSpec.scala @@ -21,7 +21,7 @@ import akka.actor.typed.scaladsl.adapter.{ClassicActorSystemOps, actorRefAdapter import akka.pattern.pipe import akka.testkit.{TestFSMRef, TestProbe} import com.softwaremill.quicklens.ModifyPimp -import fr.acinq.bitcoin.{BtcAmount, ByteVector32, MilliBtcDouble, OutPoint, SatoshiLong, Transaction, TxOut} +import fr.acinq.bitcoin.scalacompat.{BtcAmount, ByteVector32, MilliBtcDouble, OutPoint, SatoshiLong, Transaction, TxOut} import fr.acinq.eclair.NotificationsLogger.NotifyNodeOperator import fr.acinq.eclair.blockchain.CurrentBlockHeight import fr.acinq.eclair.blockchain.bitcoind.BitcoindService diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/TxPublisherSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/TxPublisherSpec.scala index 7e81c6242b..f4c58a4446 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/TxPublisherSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/TxPublisherSpec.scala @@ -20,7 +20,7 @@ import akka.actor.typed.ActorRef import akka.actor.typed.scaladsl.ActorContext import akka.actor.typed.scaladsl.adapter.{ClassicActorSystemOps, TypedActorRefOps, actorRefAdapter} import akka.testkit.TestProbe -import fr.acinq.bitcoin.{OutPoint, SatoshiLong, Transaction, TxIn, TxOut} +import fr.acinq.bitcoin.scalacompat.{OutPoint, SatoshiLong, Transaction, TxIn, TxOut} import fr.acinq.eclair.blockchain.CurrentBlockHeight import fr.acinq.eclair.channel.publish import fr.acinq.eclair.channel.publish.TxPublisher.TxRejectedReason._ diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/TxTimeLocksMonitorSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/TxTimeLocksMonitorSpec.scala index 3f0e55e671..eafbc13294 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/TxTimeLocksMonitorSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/TxTimeLocksMonitorSpec.scala @@ -19,7 +19,7 @@ package fr.acinq.eclair.channel.publish import akka.actor.typed.ActorRef import akka.actor.typed.scaladsl.adapter.{ClassicActorSystemOps, actorRefAdapter} import akka.testkit.TestProbe -import fr.acinq.bitcoin.{OutPoint, SatoshiLong, Script, Transaction, TxIn, TxOut} +import fr.acinq.bitcoin.scalacompat.{OutPoint, SatoshiLong, Script, Transaction, TxIn, TxOut} import fr.acinq.eclair.blockchain.CurrentBlockHeight import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.{WatchParentTxConfirmed, WatchParentTxConfirmedTriggered} import fr.acinq.eclair.channel.publish.TxPublisher.TxPublishContext diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala index afe24d937d..cd98aed03e 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala @@ -20,8 +20,9 @@ import akka.actor.typed.scaladsl.adapter.actorRefAdapter import akka.actor.{ActorContext, ActorRef} import akka.testkit.{TestFSMRef, TestKitBase, TestProbe} import com.softwaremill.quicklens.ModifyPimp -import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.bitcoin.{ByteVector32, Crypto, SatoshiLong, ScriptFlags, Transaction} +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.{ByteVector32, Crypto, SatoshiLong, Transaction} +import fr.acinq.bitcoin.ScriptFlags import fr.acinq.eclair.TestConstants.{Alice, Bob} import fr.acinq.eclair._ import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._ diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForAcceptChannelStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForAcceptChannelStateSpec.scala index f01284802d..541cfd753a 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForAcceptChannelStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForAcceptChannelStateSpec.scala @@ -18,7 +18,7 @@ package fr.acinq.eclair.channel.states.a import akka.actor.Status import akka.testkit.{TestFSMRef, TestProbe} -import fr.acinq.bitcoin.{Block, Btc, ByteVector32, SatoshiLong} +import fr.acinq.bitcoin.scalacompat.{Block, Btc, ByteVector32, SatoshiLong} import fr.acinq.eclair.TestConstants.{Alice, Bob} import fr.acinq.eclair.blockchain.NoOpOnChainWallet import fr.acinq.eclair.channel._ diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForOpenChannelStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForOpenChannelStateSpec.scala index 3e7856e90c..0291fbfc34 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForOpenChannelStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForOpenChannelStateSpec.scala @@ -17,7 +17,7 @@ package fr.acinq.eclair.channel.states.a import akka.testkit.{TestFSMRef, TestProbe} -import fr.acinq.bitcoin.{Block, Btc, ByteVector32, SatoshiLong} +import fr.acinq.bitcoin.scalacompat.{Block, Btc, ByteVector32, SatoshiLong} import fr.acinq.eclair.TestConstants.{Alice, Bob} import fr.acinq.eclair.blockchain.fee.FeeratePerKw import fr.acinq.eclair.channel._ diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingCreatedStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingCreatedStateSpec.scala index d49396d94b..21ec61710e 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingCreatedStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingCreatedStateSpec.scala @@ -18,7 +18,7 @@ package fr.acinq.eclair.channel.states.b import akka.actor.ActorRef import akka.testkit.{TestFSMRef, TestProbe} -import fr.acinq.bitcoin.{Btc, ByteVector32, SatoshiLong} +import fr.acinq.bitcoin.scalacompat.{Btc, ByteVector32, SatoshiLong} import fr.acinq.eclair.TestConstants.{Alice, Bob} import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._ import fr.acinq.eclair.channel._ diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingInternalStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingInternalStateSpec.scala index c475c44699..7fa5845ec6 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingInternalStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingInternalStateSpec.scala @@ -18,7 +18,7 @@ package fr.acinq.eclair.channel.states.b import akka.actor.Status import akka.testkit.{TestFSMRef, TestProbe} -import fr.acinq.bitcoin.ByteVector32 +import fr.acinq.bitcoin.scalacompat.ByteVector32 import fr.acinq.eclair.blockchain.NoOpOnChainWallet import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel.fsm.Channel diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingSignedStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingSignedStateSpec.scala index 38ee091e06..b03b8d00e3 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingSignedStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingSignedStateSpec.scala @@ -18,7 +18,7 @@ package fr.acinq.eclair.channel.states.b import akka.actor.Status import akka.testkit.{TestFSMRef, TestProbe} -import fr.acinq.bitcoin.{Btc, ByteVector32, ByteVector64, SatoshiLong} +import fr.acinq.bitcoin.scalacompat.{Btc, ByteVector32, ByteVector64, SatoshiLong} import fr.acinq.eclair.TestConstants.{Alice, Bob} import fr.acinq.eclair.blockchain.DummyOnChainWallet import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._ diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingConfirmedStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingConfirmedStateSpec.scala index 19d3e67608..8b7a8c7480 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingConfirmedStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingConfirmedStateSpec.scala @@ -17,7 +17,7 @@ package fr.acinq.eclair.channel.states.c import akka.testkit.{TestFSMRef, TestProbe} -import fr.acinq.bitcoin.{ByteVector32, SatoshiLong, Script, Transaction} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, SatoshiLong, Script, Transaction} import fr.acinq.eclair.blockchain.CurrentBlockHeight import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._ import fr.acinq.eclair.channel._ diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingLockedStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingLockedStateSpec.scala index 97f074e7d7..a1bb4b4098 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingLockedStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingLockedStateSpec.scala @@ -17,7 +17,7 @@ package fr.acinq.eclair.channel.states.c import akka.testkit.{TestFSMRef, TestProbe} -import fr.acinq.bitcoin.{ByteVector32, Transaction} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, Transaction} import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._ import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel.fsm.Channel diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala index 9c6dbcc89d..dc2c406abb 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala @@ -18,8 +18,9 @@ package fr.acinq.eclair.channel.states.e import akka.actor.ActorRef import akka.testkit.TestProbe -import fr.acinq.bitcoin.Crypto.PrivateKey -import fr.acinq.bitcoin.{ByteVector32, ByteVector64, Crypto, OutPoint, SatoshiLong, Script, ScriptFlags, Transaction} +import fr.acinq.bitcoin.scalacompat.Crypto.PrivateKey +import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, Crypto, OutPoint, SatoshiLong, Script, Transaction} +import fr.acinq.bitcoin.ScriptFlags import fr.acinq.eclair.Features.StaticRemoteKey import fr.acinq.eclair.TestConstants.{Alice, Bob} import fr.acinq.eclair.UInt64.Conversions._ diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/OfflineStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/OfflineStateSpec.scala index 8b6b31d481..0a57a6e8ad 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/OfflineStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/OfflineStateSpec.scala @@ -18,8 +18,9 @@ package fr.acinq.eclair.channel.states.e import akka.actor.ActorRef import akka.testkit.{TestFSMRef, TestProbe} -import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} -import fr.acinq.bitcoin.{ByteVector32, ScriptFlags, Transaction} +import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, Transaction} +import fr.acinq.bitcoin.ScriptFlags import fr.acinq.eclair.TestConstants.{Alice, Bob} import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._ import fr.acinq.eclair.blockchain.fee.FeeratesPerKw diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/f/ShutdownStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/f/ShutdownStateSpec.scala index 134357387c..f7bd209b63 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/f/ShutdownStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/f/ShutdownStateSpec.scala @@ -17,8 +17,9 @@ package fr.acinq.eclair.channel.states.f import akka.testkit.TestProbe -import fr.acinq.bitcoin.Crypto.PrivateKey -import fr.acinq.bitcoin.{ByteVector32, ByteVector64, Crypto, SatoshiLong, ScriptFlags, Transaction} +import fr.acinq.bitcoin.scalacompat.Crypto.PrivateKey +import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, Crypto, SatoshiLong, Transaction} +import fr.acinq.bitcoin.ScriptFlags import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._ import fr.acinq.eclair.blockchain.fee.{FeeratePerKw, FeeratesPerKw} import fr.acinq.eclair.blockchain.{CurrentBlockHeight, CurrentFeerates} diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/g/NegotiatingStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/g/NegotiatingStateSpec.scala index ab305b3329..f7231d81d5 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/g/NegotiatingStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/g/NegotiatingStateSpec.scala @@ -17,7 +17,7 @@ package fr.acinq.eclair.channel.states.g import akka.testkit.TestProbe -import fr.acinq.bitcoin.{ByteVector32, ByteVector64, Satoshi, SatoshiLong} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, Satoshi, SatoshiLong} import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._ import fr.acinq.eclair.blockchain.fee.{FeeratePerKw, FeeratesPerKw} import fr.acinq.eclair.channel.Helpers.Closing diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala index ea376baf36..1ae8baa91b 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala @@ -17,8 +17,9 @@ package fr.acinq.eclair.channel.states.h import akka.testkit.{TestFSMRef, TestProbe} -import fr.acinq.bitcoin.Crypto.PrivateKey -import fr.acinq.bitcoin.{ByteVector32, ByteVector64, Crypto, OutPoint, SatoshiLong, Script, ScriptFlags, Transaction, TxIn, TxOut} +import fr.acinq.bitcoin.scalacompat.Crypto.PrivateKey +import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, Crypto, OutPoint, SatoshiLong, Script, Transaction, TxIn, TxOut} +import fr.acinq.bitcoin.ScriptFlags import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._ import fr.acinq.eclair.blockchain.fee.{FeeratePerKw, FeeratesPerKw} import fr.acinq.eclair.channel._ diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/GeneratorsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/GeneratorsSpec.scala index 0112c5a566..760b9ff414 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/GeneratorsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/GeneratorsSpec.scala @@ -16,8 +16,8 @@ package fr.acinq.eclair.crypto -import fr.acinq.bitcoin.ByteVector32 -import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} +import fr.acinq.bitcoin.scalacompat.ByteVector32 +import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} import org.scalatest.funsuite.AnyFunSuite import scodec.bits._ diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/MacSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/MacSpec.scala index 3daa3dfe90..a390d64187 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/MacSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/MacSpec.scala @@ -16,7 +16,7 @@ package fr.acinq.eclair.crypto -import fr.acinq.bitcoin.ByteVector32 +import fr.acinq.bitcoin.scalacompat.ByteVector32 import org.scalatest.funsuite.AnyFunSuite import scodec.bits.HexStringSyntax diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/RandomSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/RandomSpec.scala index 29c4917c59..4e6b357f86 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/RandomSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/RandomSpec.scala @@ -16,7 +16,7 @@ package fr.acinq.eclair.crypto -import fr.acinq.bitcoin.ByteVector32 +import fr.acinq.bitcoin.scalacompat.ByteVector32 import fr.acinq.eclair.crypto.RandomSpec.entropyScore import org.bouncycastle.crypto.engines.ChaCha7539Engine import org.bouncycastle.crypto.params.{KeyParameter, ParametersWithIV} diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/ShaChainSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/ShaChainSpec.scala index 3c16846cc8..90b84d9347 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/ShaChainSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/ShaChainSpec.scala @@ -16,7 +16,7 @@ package fr.acinq.eclair.crypto -import fr.acinq.bitcoin.ByteVector32 +import fr.acinq.bitcoin.scalacompat.ByteVector32 import org.scalatest.funsuite.AnyFunSuite import scodec.bits._ diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/SphinxSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/SphinxSpec.scala index 6b6f468199..637983a906 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/SphinxSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/SphinxSpec.scala @@ -16,8 +16,8 @@ package fr.acinq.eclair.crypto -import fr.acinq.bitcoin.ByteVector32 -import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} +import fr.acinq.bitcoin.scalacompat.ByteVector32 +import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} import fr.acinq.eclair.crypto.Sphinx.RouteBlinding.BlindedRoute import fr.acinq.eclair.wire.protocol import fr.acinq.eclair.wire.protocol._ diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/keymanager/LocalChannelKeyManagerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/keymanager/LocalChannelKeyManagerSpec.scala index a3b51a684e..510ea16c87 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/keymanager/LocalChannelKeyManagerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/keymanager/LocalChannelKeyManagerSpec.scala @@ -18,9 +18,9 @@ package fr.acinq.eclair.crypto.keymanager import java.io.File import java.nio.file.Files -import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} -import fr.acinq.bitcoin.DeterministicWallet.KeyPath -import fr.acinq.bitcoin.{Block, ByteVector32, DeterministicWallet} +import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} +import fr.acinq.bitcoin.scalacompat.DeterministicWallet.KeyPath +import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, DeterministicWallet} import fr.acinq.eclair.Setup.Seeds import fr.acinq.eclair.channel.ChannelConfig import fr.acinq.eclair.crypto.ShaChain diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/keymanager/LocalNodeKeyManagerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/keymanager/LocalNodeKeyManagerSpec.scala index f6d810324e..d3277cff65 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/keymanager/LocalNodeKeyManagerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/keymanager/LocalNodeKeyManagerSpec.scala @@ -19,9 +19,9 @@ package fr.acinq.eclair.crypto.keymanager import java.io.File import java.nio.file.Files -import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.bitcoin.DeterministicWallet.KeyPath -import fr.acinq.bitcoin.{Block, ByteVector32, Crypto} +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.DeterministicWallet.KeyPath +import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, Crypto} import fr.acinq.eclair.Setup.Seeds import fr.acinq.eclair.{NodeParams, TestUtils} import org.scalatest.funsuite.AnyFunSuite diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/AuditDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/AuditDbSpec.scala index d61f98d641..8261aa986b 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/AuditDbSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/AuditDbSpec.scala @@ -16,8 +16,8 @@ package fr.acinq.eclair.db -import fr.acinq.bitcoin.Crypto.PrivateKey -import fr.acinq.bitcoin.{ByteVector32, SatoshiLong, Script, Transaction, TxOut} +import fr.acinq.bitcoin.scalacompat.Crypto.PrivateKey +import fr.acinq.bitcoin.scalacompat.{ByteVector32, SatoshiLong, Script, Transaction, TxOut} import fr.acinq.eclair.TestDatabases.{TestPgDatabases, TestSqliteDatabases, migrationCheck} import fr.acinq.eclair._ import fr.acinq.eclair.channel.Helpers.Closing.MutualClose diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/ChannelsDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/ChannelsDbSpec.scala index 111c1e417d..d8f519eb3a 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/ChannelsDbSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/ChannelsDbSpec.scala @@ -17,7 +17,7 @@ package fr.acinq.eclair.db import com.softwaremill.quicklens._ -import fr.acinq.bitcoin.ByteVector32 +import fr.acinq.bitcoin.scalacompat.ByteVector32 import fr.acinq.eclair.TestDatabases.{TestPgDatabases, TestSqliteDatabases, migrationCheck} import fr.acinq.eclair.db.ChannelsDbSpec.{getPgTimestamp, getTimestamp, testCases} import fr.acinq.eclair.db.DbEventHandler.ChannelEvent diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/NetworkDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/NetworkDbSpec.scala index 6a32942112..bdc551e04b 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/NetworkDbSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/NetworkDbSpec.scala @@ -16,8 +16,8 @@ package fr.acinq.eclair.db -import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} -import fr.acinq.bitcoin.{Block, ByteVector32, ByteVector64, Crypto, Satoshi, SatoshiLong} +import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} +import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, ByteVector64, Crypto, Satoshi, SatoshiLong} import fr.acinq.eclair.FeatureSupport.Optional import fr.acinq.eclair.Features.VariableLengthOnion import fr.acinq.eclair.TestDatabases._ diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/PaymentsDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/PaymentsDbSpec.scala index ad8e5778e5..8294d15fe0 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/PaymentsDbSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/PaymentsDbSpec.scala @@ -16,8 +16,8 @@ package fr.acinq.eclair.db -import fr.acinq.bitcoin.Crypto.PrivateKey -import fr.acinq.bitcoin.{Block, ByteVector32, Crypto} +import fr.acinq.bitcoin.scalacompat.Crypto.PrivateKey +import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, Crypto} import fr.acinq.eclair.TestDatabases.{TestPgDatabases, TestSqliteDatabases, forAllDbs, migrationCheck} import fr.acinq.eclair.crypto.Sphinx import fr.acinq.eclair.db.jdbc.JdbcUtils.{setVersion, using} diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/PeersDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/PeersDbSpec.scala index b1865e3431..1b0661b71f 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/PeersDbSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/PeersDbSpec.scala @@ -16,7 +16,7 @@ package fr.acinq.eclair.db -import fr.acinq.bitcoin.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.eclair.TestDatabases.{TestPgDatabases, TestSqliteDatabases} import fr.acinq.eclair.db.pg.PgPeersDb import fr.acinq.eclair.db.sqlite.SqlitePeersDb diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/PendingCommandsDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/PendingCommandsDbSpec.scala index b0b7d4530f..e69551879f 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/PendingCommandsDbSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/PendingCommandsDbSpec.scala @@ -16,7 +16,7 @@ package fr.acinq.eclair.db -import fr.acinq.bitcoin.ByteVector32 +import fr.acinq.bitcoin.scalacompat.ByteVector32 import fr.acinq.eclair.TestDatabases.{TestPgDatabases, TestSqliteDatabases} import fr.acinq.eclair.channel.{CMD_FAIL_HTLC, CMD_FAIL_MALFORMED_HTLC, CMD_FULFILL_HTLC, HtlcSettlementCommand} import fr.acinq.eclair.db.pg.PgPendingCommandsDb diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteFeeratesDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteFeeratesDbSpec.scala index e280b622c4..284fec4100 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteFeeratesDbSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteFeeratesDbSpec.scala @@ -16,7 +16,7 @@ package fr.acinq.eclair.db -import fr.acinq.bitcoin.SatoshiLong +import fr.acinq.bitcoin.scalacompat.SatoshiLong import fr.acinq.eclair._ import fr.acinq.eclair.blockchain.fee.{FeeratePerKB, FeeratesPerKB} import fr.acinq.eclair.db.pg.PgUtils.setVersion diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/ChannelIntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/ChannelIntegrationSpec.scala index 046fac8a91..33d6641be4 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/ChannelIntegrationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/ChannelIntegrationSpec.scala @@ -20,8 +20,9 @@ import akka.actor.ActorRef import akka.pattern.pipe import akka.testkit.TestProbe import com.typesafe.config.ConfigFactory -import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.bitcoin.{Base58, Base58Check, Bech32, Block, BtcDouble, ByteVector32, Crypto, OP_0, OP_CHECKSIG, OP_DUP, OP_EQUAL, OP_EQUALVERIFY, OP_HASH160, OP_PUSHDATA, OutPoint, SatoshiLong, Script, ScriptFlags, Transaction} +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.{Block, BtcDouble, ByteVector32, Crypto, OP_0, OP_CHECKSIG, OP_DUP, OP_EQUAL, OP_EQUALVERIFY, OP_HASH160, OP_PUSHDATA, OutPoint, SatoshiLong, Script, Transaction} +import fr.acinq.bitcoin.{Base58, Base58Check, Bech32, ScriptFlags} import fr.acinq.eclair.blockchain.bitcoind.BitcoindService.BitcoinReq import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinCoreClient import fr.acinq.eclair.channel._ @@ -67,11 +68,11 @@ abstract class ChannelIntegrationSpec extends IntegrationSpec { */ def scriptPubKeyToAddress(scriptPubKey: ByteVector): String = Script.parse(scriptPubKey) match { case OP_DUP :: OP_HASH160 :: OP_PUSHDATA(pubKeyHash, _) :: OP_EQUALVERIFY :: OP_CHECKSIG :: Nil => - Base58Check.encode(Base58.Prefix.PubkeyAddressTestnet, pubKeyHash) + Base58Check.encode(Base58.Prefix.PubkeyAddressTestnet, pubKeyHash.toArray) case OP_HASH160 :: OP_PUSHDATA(scriptHash, _) :: OP_EQUAL :: Nil => - Base58Check.encode(Base58.Prefix.ScriptAddressTestnet, scriptHash) - case OP_0 :: OP_PUSHDATA(pubKeyHash, _) :: Nil if pubKeyHash.length == 20 => Bech32.encodeWitnessAddress("bcrt", 0, pubKeyHash) - case OP_0 :: OP_PUSHDATA(scriptHash, _) :: Nil if scriptHash.length == 32 => Bech32.encodeWitnessAddress("bcrt", 0, scriptHash) + Base58Check.encode(Base58.Prefix.ScriptAddressTestnet, scriptHash.toArray) + case OP_0 :: OP_PUSHDATA(pubKeyHash, _) :: Nil if pubKeyHash.length == 20 => Bech32.encodeWitnessAddress("bcrt", 0, pubKeyHash.toArray) + case OP_0 :: OP_PUSHDATA(scriptHash, _) :: Nil if scriptHash.length == 32 => Bech32.encodeWitnessAddress("bcrt", 0, scriptHash.toArray) case _ => ??? } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/IntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/IntegrationSpec.scala index aba35a9b18..0b540986b5 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/IntegrationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/IntegrationSpec.scala @@ -19,7 +19,7 @@ package fr.acinq.eclair.integration import akka.actor.ActorSystem import akka.testkit.{TestKit, TestProbe} import com.typesafe.config.{Config, ConfigFactory} -import fr.acinq.bitcoin.Satoshi +import fr.acinq.bitcoin.scalacompat.Satoshi import fr.acinq.eclair.Features._ import fr.acinq.eclair.blockchain.bitcoind.BitcoindService import fr.acinq.eclair.channel._ diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/MessageIntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/MessageIntegrationSpec.scala index 6dd4603ec3..39cb766b86 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/MessageIntegrationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/MessageIntegrationSpec.scala @@ -22,7 +22,8 @@ import akka.pattern.pipe import akka.testkit.TestProbe import akka.util.Timeout import com.typesafe.config.ConfigFactory -import fr.acinq.bitcoin.{ByteVector32, Satoshi, Transaction} +import fr.acinq.bitcoin.Transaction +import fr.acinq.bitcoin.scalacompat.{ByteVector32, Satoshi} import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.{Watch, WatchFundingConfirmed} import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinCoreClient diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/PaymentIntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/PaymentIntegrationSpec.scala index 851c4f8a17..5c20e6e695 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/PaymentIntegrationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/PaymentIntegrationSpec.scala @@ -20,8 +20,8 @@ import akka.actor.ActorRef import akka.actor.typed.scaladsl.adapter.actorRefAdapter import akka.testkit.TestProbe import com.typesafe.config.ConfigFactory -import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} -import fr.acinq.bitcoin.{Block, ByteVector32, Crypto, SatoshiLong} +import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} +import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, Crypto, SatoshiLong} import fr.acinq.eclair.blockchain.bitcoind.BitcoindService.BitcoinReq import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.{Watch, WatchFundingConfirmed} diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/PerformanceIntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/PerformanceIntegrationSpec.scala index 37f582382a..0072313230 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/PerformanceIntegrationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/PerformanceIntegrationSpec.scala @@ -18,7 +18,7 @@ package fr.acinq.eclair.integration import akka.testkit.TestProbe import com.typesafe.config.ConfigFactory -import fr.acinq.bitcoin.SatoshiLong +import fr.acinq.bitcoin.scalacompat.SatoshiLong import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel.fsm.Channel import fr.acinq.eclair.payment._ diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/interop/rustytests/RustyTestsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/interop/rustytests/RustyTestsSpec.scala index 5727045f09..9e36e63243 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/interop/rustytests/RustyTestsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/interop/rustytests/RustyTestsSpec.scala @@ -19,7 +19,7 @@ package fr.acinq.eclair.interop.rustytests import akka.actor.typed.scaladsl.adapter.actorRefAdapter import akka.actor.{ActorRef, Props} import akka.testkit.{TestFSMRef, TestKit, TestProbe} -import fr.acinq.bitcoin.{ByteVector32, SatoshiLong} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, SatoshiLong} import fr.acinq.eclair.TestConstants.{Alice, Bob} import fr.acinq.eclair.blockchain.DummyOnChainWallet import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._ diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/interop/rustytests/SynchronizationPipe.scala b/eclair-core/src/test/scala/fr/acinq/eclair/interop/rustytests/SynchronizationPipe.scala index 6056097a9f..17ea91aefa 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/interop/rustytests/SynchronizationPipe.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/interop/rustytests/SynchronizationPipe.scala @@ -21,7 +21,7 @@ import java.util.UUID import java.util.concurrent.CountDownLatch import akka.actor.{Actor, ActorLogging, ActorRef, Stash} -import fr.acinq.bitcoin.ByteVector32 +import fr.acinq.bitcoin.scalacompat.ByteVector32 import fr.acinq.eclair.channel._ import fr.acinq.eclair.transactions.{IncomingHtlc, OutgoingHtlc} import fr.acinq.eclair.{CltvExpiry, MilliSatoshi, TestConstants, TestUtils} diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/io/MessageRelaySpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/io/MessageRelaySpec.scala index bced90e3ac..e5104e37a3 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/io/MessageRelaySpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/io/MessageRelaySpec.scala @@ -21,7 +21,7 @@ import akka.actor.typed.ActorRef import akka.actor.typed.scaladsl.adapter.TypedActorRefOps import akka.testkit.TestProbe import com.typesafe.config.ConfigFactory -import fr.acinq.bitcoin.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.eclair.TestConstants.{Alice, Bob} import fr.acinq.eclair.io.MessageRelay._ import fr.acinq.eclair.io.Peer.{PeerInfo, PeerNotFound} diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerConnectionSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerConnectionSpec.scala index a0a54e77bb..a6ee7d08c9 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerConnectionSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerConnectionSpec.scala @@ -18,8 +18,8 @@ package fr.acinq.eclair.io import akka.actor.PoisonPill import akka.testkit.{TestFSMRef, TestProbe} -import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} -import fr.acinq.bitcoin.{Block, ByteVector32} +import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} +import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32} import fr.acinq.eclair.FeatureSupport.{Mandatory, Optional} import fr.acinq.eclair.Features.{BasicMultiPartPayment, ChannelRangeQueries, PaymentSecret, VariableLengthOnion} import fr.acinq.eclair.TestConstants._ diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala index a0aca047c6..1fa21cf749 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala @@ -20,8 +20,8 @@ import akka.actor.Status.Failure import akka.actor.typed.scaladsl.adapter.ClassicActorRefOps import akka.actor.{ActorContext, ActorRef, FSM, PoisonPill, Status} import akka.testkit.{TestFSMRef, TestProbe} -import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.bitcoin.{Block, Btc, SatoshiLong, Script} +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.{Block, Btc, SatoshiLong, Script} import fr.acinq.eclair.FeatureSupport.{Mandatory, Optional} import fr.acinq.eclair.Features._ import fr.acinq.eclair.TestConstants._ diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/io/ReconnectionTaskSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/io/ReconnectionTaskSpec.scala index db798a2654..cd0bd0ca1a 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/io/ReconnectionTaskSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/io/ReconnectionTaskSpec.scala @@ -17,7 +17,7 @@ package fr.acinq.eclair.io import akka.testkit.{TestFSMRef, TestProbe} -import fr.acinq.bitcoin.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.eclair._ import fr.acinq.eclair.io.Peer.ChannelId import fr.acinq.eclair.io.ReconnectionTask.WaitingData diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/io/SwitchboardSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/io/SwitchboardSpec.scala index d452e3e379..c1f87f42c6 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/io/SwitchboardSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/io/SwitchboardSpec.scala @@ -3,8 +3,8 @@ package fr.acinq.eclair.io import akka.actor.typed.scaladsl.adapter.ClassicActorRefOps import akka.actor.{Actor, ActorContext, ActorRef, Props, Status} import akka.testkit.{TestActorRef, TestProbe} -import fr.acinq.bitcoin.ByteVector64 -import fr.acinq.bitcoin.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.ByteVector64 +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.eclair.TestConstants._ import fr.acinq.eclair.channel.ChannelIdAssigned import fr.acinq.eclair.io.Switchboard._ diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/json/JsonSerializersSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/json/JsonSerializersSpec.scala index e359c19fb3..c88ffd2a43 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/json/JsonSerializersSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/json/JsonSerializersSpec.scala @@ -17,8 +17,8 @@ package fr.acinq.eclair.json import akka.actor.ActorRef -import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.bitcoin.{Btc, ByteVector32, OutPoint, Satoshi, SatoshiLong, Transaction, TxOut} +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.{Btc, ByteVector32, OutPoint, Satoshi, SatoshiLong, Transaction, TxOut} import fr.acinq.eclair._ import fr.acinq.eclair.balance.CheckBalance import fr.acinq.eclair.balance.CheckBalance.{ClosingBalance, GlobalBalance, MainAndHtlcBalance, PossiblyPublishedMainAndHtlcBalance, PossiblyPublishedMainBalance} @@ -51,7 +51,7 @@ class JsonSerializersSpec extends AnyFunSuite with Matchers { val error = intercept[org.json4s.MappingException] { JsonSerializers.serialization.write(map)(org.json4s.DefaultFormats) } - assert(error.msg.contains("Do not know how to serialize key of type class fr.acinq.bitcoin.OutPoint.")) + assert(error.msg.contains("Do not know how to serialize key of type class fr.acinq.bitcoin.scalacompat.OutPoint. Consider implementing a CustomKeySerializer.")) // but it works with our custom key serializer val json = JsonSerializers.serialization.write(map)(org.json4s.DefaultFormats + ByteVectorSerializer + OutPointKeySerializer) @@ -70,7 +70,7 @@ class JsonSerializersSpec extends AnyFunSuite with Matchers { val error = intercept[org.json4s.MappingException] { JsonSerializers.serialization.write(map)(org.json4s.DefaultFormats) } - assert(error.msg.contains("Do not know how to serialize key of type class fr.acinq.bitcoin.OutPoint.")) + assert(error.msg.contains("Do not know how to serialize key of type class fr.acinq.bitcoin.scalacompat.OutPoint. Consider implementing a CustomKeySerializer.")) // but it works with our custom key serializer val json = JsonSerializers.serialization.write(map)(org.json4s.DefaultFormats + TransactionSerializer + OutPointKeySerializer) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/message/OnionMessagesSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/message/OnionMessagesSpec.scala index 3f6e263dab..606572f74f 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/message/OnionMessagesSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/message/OnionMessagesSpec.scala @@ -16,8 +16,8 @@ package fr.acinq.eclair.message -import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} -import fr.acinq.bitcoin.{ByteVector32, Crypto} +import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, Crypto} import fr.acinq.eclair.crypto.Sphinx import fr.acinq.eclair.crypto.Sphinx.PacketAndSecrets import fr.acinq.eclair.message.OnionMessages._ diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/Bolt11InvoiceSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/Bolt11InvoiceSpec.scala index 75375396f3..707b5c42af 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/Bolt11InvoiceSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/Bolt11InvoiceSpec.scala @@ -16,8 +16,8 @@ package fr.acinq.eclair.payment -import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} -import fr.acinq.bitcoin.{Block, BtcDouble, ByteVector32, Crypto, MilliBtcDouble, SatoshiLong} +import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} +import fr.acinq.bitcoin.scalacompat.{Block, BtcDouble, ByteVector32, Crypto, MilliBtcDouble, SatoshiLong} import fr.acinq.eclair.FeatureSupport.{Mandatory, Optional} import fr.acinq.eclair.Features.{PaymentMetadata, PaymentSecret, _} import fr.acinq.eclair.payment.Bolt11Invoice._ diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartHandlerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartHandlerSpec.scala index ff99fa5b6f..778164b2e9 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartHandlerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartHandlerSpec.scala @@ -19,7 +19,7 @@ package fr.acinq.eclair.payment import akka.actor.ActorRef import akka.actor.Status.Failure import akka.testkit.{TestActorRef, TestProbe} -import fr.acinq.bitcoin.{ByteVector32, Crypto} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, Crypto} import fr.acinq.eclair.FeatureSupport.{Mandatory, Optional} import fr.acinq.eclair.Features._ import fr.acinq.eclair.TestConstants.Alice diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartPaymentFSMSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartPaymentFSMSpec.scala index 5092339d1b..e66d61754f 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartPaymentFSMSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartPaymentFSMSpec.scala @@ -18,7 +18,7 @@ package fr.acinq.eclair.payment import akka.actor.FSM.{CurrentState, SubscribeTransitionCallBack, Transition} import akka.testkit.{TestActorRef, TestProbe} -import fr.acinq.bitcoin.ByteVector32 +import fr.acinq.bitcoin.scalacompat.ByteVector32 import fr.acinq.eclair.payment.receive.MultiPartPaymentFSM import fr.acinq.eclair.payment.receive.MultiPartPaymentFSM._ import fr.acinq.eclair.wire.protocol diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartPaymentLifecycleSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartPaymentLifecycleSpec.scala index 3b706db01d..75f7dea40d 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartPaymentLifecycleSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartPaymentLifecycleSpec.scala @@ -18,7 +18,7 @@ package fr.acinq.eclair.payment import akka.actor.{ActorContext, ActorRef, Status} import akka.testkit.{TestFSMRef, TestProbe} -import fr.acinq.bitcoin.{Block, ByteVector32, Crypto, SatoshiLong} +import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, Crypto, SatoshiLong} import fr.acinq.eclair._ import fr.acinq.eclair.channel.{ChannelUnavailable, HtlcsTimedoutDownstream, RemoteCannotAffordFeesForNewHtlc} import fr.acinq.eclair.crypto.Sphinx diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentInitiatorSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentInitiatorSpec.scala index 6e052ffe97..61a2a3a224 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentInitiatorSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentInitiatorSpec.scala @@ -18,7 +18,7 @@ package fr.acinq.eclair.payment import akka.actor.{ActorContext, ActorRef} import akka.testkit.{TestActorRef, TestProbe} -import fr.acinq.bitcoin.Block +import fr.acinq.bitcoin.scalacompat.Block import fr.acinq.eclair.FeatureSupport.{Mandatory, Optional} import fr.acinq.eclair.Features._ import fr.acinq.eclair.UInt64.Conversions._ diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala index 126fd8898c..1ae69f9230 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala @@ -19,9 +19,9 @@ package fr.acinq.eclair.payment import akka.actor.ActorRef import akka.actor.FSM.{CurrentState, SubscribeTransitionCallBack, Transition} import akka.testkit.{TestFSMRef, TestProbe} -import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.bitcoin.Script.{pay2wsh, write} -import fr.acinq.bitcoin.{Block, ByteVector32, Crypto, SatoshiLong, Transaction, TxOut} +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.Script.{pay2wsh, write} +import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, Crypto, SatoshiLong, Transaction, TxOut} import fr.acinq.eclair._ import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.{UtxoStatus, ValidateRequest, ValidateResult, WatchExternalChannelSpent} import fr.acinq.eclair.channel.Register.{ForwardShortId, ForwardShortIdFailure} diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentPacketSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentPacketSpec.scala index cb9b69b185..a2ec5c1343 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentPacketSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentPacketSpec.scala @@ -17,9 +17,9 @@ package fr.acinq.eclair.payment import akka.actor.ActorRef -import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.bitcoin.DeterministicWallet.ExtendedPrivateKey -import fr.acinq.bitcoin.{Block, ByteVector32, Crypto, DeterministicWallet, OutPoint, Satoshi, SatoshiLong, TxOut} +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.DeterministicWallet.ExtendedPrivateKey +import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, Crypto, DeterministicWallet, OutPoint, Satoshi, SatoshiLong, TxOut} import fr.acinq.eclair.FeatureSupport.{Mandatory, Optional} import fr.acinq.eclair.Features._ import fr.acinq.eclair.channel._ diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PostRestartHtlcCleanerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PostRestartHtlcCleanerSpec.scala index f16d3353ee..47d7cb023d 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PostRestartHtlcCleanerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PostRestartHtlcCleanerSpec.scala @@ -20,8 +20,8 @@ import akka.Done import akka.actor.ActorRef import akka.event.LoggingAdapter import akka.testkit.TestProbe -import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.bitcoin.{Block, ByteVector32, Crypto, OutPoint, Satoshi, SatoshiLong, Script, Transaction, TxIn, TxOut} +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, Crypto, OutPoint, Satoshi, SatoshiLong, Script, Transaction, TxIn, TxOut} import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.WatchTxConfirmedTriggered import fr.acinq.eclair.channel.Helpers.Closing import fr.acinq.eclair.channel._ diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/receive/InvoicePurgerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/receive/InvoicePurgerSpec.scala index e89ffd3139..24b0bbaabb 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/receive/InvoicePurgerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/receive/InvoicePurgerSpec.scala @@ -19,7 +19,7 @@ package fr.acinq.eclair.payment.receive import akka.actor.testkit.typed.scaladsl.ScalaTestWithActorTestKit import akka.actor.typed.eventstream.EventStream import com.typesafe.config.ConfigFactory -import fr.acinq.bitcoin.Block +import fr.acinq.bitcoin.scalacompat.Block import fr.acinq.eclair.TestDatabases.TestSqliteDatabases import fr.acinq.eclair.db.{IncomingPayment, IncomingPaymentStatus, PaymentType, PaymentsDbSpec} import fr.acinq.eclair.payment.Bolt11Invoice diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/ChannelRelayerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/ChannelRelayerSpec.scala index 3ecf496028..268953a09a 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/ChannelRelayerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/ChannelRelayerSpec.scala @@ -21,8 +21,8 @@ import akka.actor.typed import akka.actor.typed.eventstream.EventStream import akka.actor.typed.scaladsl.adapter.TypedActorRefOps import com.typesafe.config.ConfigFactory -import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.bitcoin.{Block, ByteVector32, ByteVector64, Crypto, Satoshi, SatoshiLong} +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, ByteVector64, Crypto, Satoshi, SatoshiLong} import fr.acinq.eclair.TestConstants.emptyOnionPacket import fr.acinq.eclair.blockchain.fee.FeeratePerKw import fr.acinq.eclair.channel._ diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/NodeRelayerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/NodeRelayerSpec.scala index a71dc8266d..f851e4627e 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/NodeRelayerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/NodeRelayerSpec.scala @@ -23,7 +23,7 @@ import akka.actor.typed.eventstream.EventStream import akka.actor.typed.scaladsl.ActorContext import akka.actor.typed.scaladsl.adapter._ import com.typesafe.config.ConfigFactory -import fr.acinq.bitcoin.{Block, ByteVector32, Crypto} +import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, Crypto} import fr.acinq.eclair.FeatureSupport.{Mandatory, Optional} import fr.acinq.eclair.Features.{BasicMultiPartPayment, PaymentSecret, VariableLengthOnion} import fr.acinq.eclair.channel.{CMD_FAIL_HTLC, CMD_FULFILL_HTLC, Register} diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/RelayerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/RelayerSpec.scala index 98b7e019a4..66cb6852e9 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/RelayerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/RelayerSpec.scala @@ -22,7 +22,7 @@ import akka.actor.typed.eventstream.EventStream import akka.actor.typed.scaladsl.Behaviors import akka.actor.typed.scaladsl.adapter.{TypedActorContextOps, TypedActorRefOps} import com.typesafe.config.ConfigFactory -import fr.acinq.bitcoin.ByteVector32 +import fr.acinq.bitcoin.scalacompat.ByteVector32 import fr.acinq.eclair.channel._ import fr.acinq.eclair.crypto.Sphinx import fr.acinq.eclair.payment.IncomingPaymentPacket.FinalPacket diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/AnnouncementsBatchValidationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/AnnouncementsBatchValidationSpec.scala index 6b5b826f63..b87f78b3f6 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/AnnouncementsBatchValidationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/AnnouncementsBatchValidationSpec.scala @@ -20,8 +20,8 @@ import akka.actor.ActorSystem import akka.pattern.pipe import akka.testkit.TestProbe import sttp.client3.okhttp.OkHttpFutureBackend -import fr.acinq.bitcoin.Crypto.PrivateKey -import fr.acinq.bitcoin.{Block, Satoshi, SatoshiLong, Script, Transaction} +import fr.acinq.bitcoin.scalacompat.Crypto.PrivateKey +import fr.acinq.bitcoin.scalacompat.{Block, Satoshi, SatoshiLong, Script, Transaction} import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.ValidateResult import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinJsonRPCAuthMethod.UserPassword import fr.acinq.eclair.blockchain.bitcoind.rpc.{BasicBitcoinJsonRPCClient, BitcoinCoreClient} diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/AnnouncementsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/AnnouncementsSpec.scala index 55ef00a669..b92e5f022e 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/AnnouncementsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/AnnouncementsSpec.scala @@ -16,8 +16,8 @@ package fr.acinq.eclair.router -import fr.acinq.bitcoin.Block -import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} +import fr.acinq.bitcoin.scalacompat.Block +import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} import fr.acinq.eclair.TestConstants.Alice import fr.acinq.eclair._ import fr.acinq.eclair.router.Announcements._ diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/BaseRouterSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/BaseRouterSpec.scala index ba738d4954..90b8afac45 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/BaseRouterSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/BaseRouterSpec.scala @@ -19,9 +19,9 @@ package fr.acinq.eclair.router import akka.actor.ActorRef import akka.actor.typed.scaladsl.adapter.actorRefAdapter import akka.testkit.TestProbe -import fr.acinq.bitcoin.Crypto.PrivateKey -import fr.acinq.bitcoin.Script.{pay2wsh, write} -import fr.acinq.bitcoin.{Block, ByteVector32, SatoshiLong, Transaction, TxOut} +import fr.acinq.bitcoin.scalacompat.Crypto.PrivateKey +import fr.acinq.bitcoin.scalacompat.Script.{pay2wsh, write} +import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, SatoshiLong, Transaction, TxOut} import fr.acinq.eclair.TestConstants.Alice import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.{UtxoStatus, ValidateRequest, ValidateResult, WatchExternalChannelSpent} import fr.acinq.eclair.channel.{CommitmentsSpec, LocalChannelUpdate} diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRangeQueriesSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRangeQueriesSpec.scala index f44d9936d6..9320739342 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRangeQueriesSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRangeQueriesSpec.scala @@ -16,7 +16,7 @@ package fr.acinq.eclair.router -import fr.acinq.bitcoin.{Block, ByteVector32, SatoshiLong} +import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, SatoshiLong} import fr.acinq.eclair.router.Router.{ChannelMeta, PublicChannel} import fr.acinq.eclair.router.Sync._ import fr.acinq.eclair.wire.protocol.QueryChannelRangeTlv.QueryFlags diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/GraphSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/GraphSpec.scala index b3dafaefe0..503d07a4f4 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/GraphSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/GraphSpec.scala @@ -16,8 +16,8 @@ package fr.acinq.eclair.router -import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.bitcoin.SatoshiLong +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.SatoshiLong import fr.acinq.eclair.payment.relay.Relayer.RelayFees import fr.acinq.eclair.router.Graph.GraphStructure.{DirectedGraph, GraphEdge} import fr.acinq.eclair.router.Graph.{HeuristicsConstants, yenKshortestPaths} diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/RouteCalculationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/RouteCalculationSpec.scala index 8e68a86e20..ec06855fce 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/RouteCalculationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/RouteCalculationSpec.scala @@ -17,8 +17,8 @@ package fr.acinq.eclair.router import com.softwaremill.quicklens.ModifyPimp -import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.bitcoin.{Block, ByteVector32, ByteVector64, Satoshi, SatoshiLong} +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, ByteVector64, Satoshi, SatoshiLong} import fr.acinq.eclair.payment.Bolt11Invoice.ExtraHop import fr.acinq.eclair.payment.relay.Relayer.RelayFees import fr.acinq.eclair.router.Graph.GraphStructure.DirectedGraph.graphEdgeToHop diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/RouterSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/RouterSpec.scala index 4c69ac9673..a5fc1bb368 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/RouterSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/RouterSpec.scala @@ -19,9 +19,9 @@ package fr.acinq.eclair.router import akka.actor.Status import akka.actor.Status.Failure import akka.testkit.TestProbe -import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.bitcoin.Script.{pay2wsh, write} -import fr.acinq.bitcoin.{Block, SatoshiLong, Transaction, TxOut} +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.Script.{pay2wsh, write} +import fr.acinq.bitcoin.scalacompat.{Block, SatoshiLong, Transaction, TxOut} import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._ import fr.acinq.eclair.channel.{AvailableBalanceChanged, CommitmentsSpec, LocalChannelUpdate} import fr.acinq.eclair.crypto.TransportHandler diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncSpec.scala index 5000e221e8..b2e9dc3d4d 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncSpec.scala @@ -19,8 +19,8 @@ package fr.acinq.eclair.router import akka.actor.typed.scaladsl.adapter.actorRefAdapter import akka.actor.{Actor, Props} import akka.testkit.{TestFSMRef, TestProbe} -import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} -import fr.acinq.bitcoin.{Block, ByteVector32, Satoshi, Script, Transaction, TxIn, TxOut} +import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} +import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, Satoshi, Script, Transaction, TxIn, TxOut} import fr.acinq.eclair.TestConstants.{Alice, Bob} import fr.acinq.eclair._ import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.{UtxoStatus, ValidateRequest, ValidateResult} diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/transactions/CommitmentSpecSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/transactions/CommitmentSpecSpec.scala index de93b261c0..443e3ecdbf 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/transactions/CommitmentSpecSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/transactions/CommitmentSpecSpec.scala @@ -16,7 +16,7 @@ package fr.acinq.eclair.transactions -import fr.acinq.bitcoin.{ByteVector32, Crypto, SatoshiLong} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, Crypto, SatoshiLong} import fr.acinq.eclair.blockchain.fee.FeeratePerKw import fr.acinq.eclair.wire.protocol.{UpdateAddHtlc, UpdateFailHtlc, UpdateFulfillHtlc} import fr.acinq.eclair.{CltvExpiry, MilliSatoshi, MilliSatoshiLong, TestConstants, randomBytes32} diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TestVectorsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TestVectorsSpec.scala index 67e7fe4242..ef7c6f042d 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TestVectorsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TestVectorsSpec.scala @@ -16,8 +16,9 @@ package fr.acinq.eclair.transactions -import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} -import fr.acinq.bitcoin.{ByteVector32, Crypto, SatoshiLong, Script, ScriptFlags, Transaction} +import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, Crypto, SatoshiLong, Script, Transaction} +import fr.acinq.bitcoin.ScriptFlags import fr.acinq.eclair.blockchain.fee.FeeratePerKw import fr.acinq.eclair.channel.ChannelFeatures import fr.acinq.eclair.channel.Helpers.Funding diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TransactionsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TransactionsSpec.scala index 130153a1fa..27e7bd4cec 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TransactionsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TransactionsSpec.scala @@ -16,9 +16,10 @@ package fr.acinq.eclair.transactions -import fr.acinq.bitcoin.Crypto.{PrivateKey, ripemd160, sha256} -import fr.acinq.bitcoin.Script.{pay2wpkh, pay2wsh, write} -import fr.acinq.bitcoin.{Btc, ByteVector32, Crypto, MilliBtc, MilliBtcDouble, OutPoint, Protocol, SIGHASH_ALL, SIGHASH_ANYONECANPAY, SIGHASH_NONE, SIGHASH_SINGLE, Satoshi, SatoshiLong, Script, ScriptWitness, Transaction, TxIn, TxOut, millibtc2satoshi} +import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, ripemd160, sha256} +import fr.acinq.bitcoin.scalacompat.Script.{pay2wpkh, pay2wsh, write} +import fr.acinq.bitcoin.scalacompat.{Btc, ByteVector32, Crypto, MilliBtc, MilliBtcDouble, OutPoint, Protocol, Satoshi, SatoshiLong, Script, ScriptWitness, Transaction, TxIn, TxOut, millibtc2satoshi} +import fr.acinq.bitcoin.SigHash._ import fr.acinq.eclair._ import fr.acinq.eclair.blockchain.fee.FeeratePerKw import fr.acinq.eclair.channel.Helpers.Funding diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/ChannelCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/ChannelCodecsSpec.scala index 0caee48832..06b1749447 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/ChannelCodecsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/ChannelCodecsSpec.scala @@ -16,8 +16,8 @@ package fr.acinq.eclair.wire.internal.channel -import fr.acinq.bitcoin.Crypto.PrivateKey -import fr.acinq.bitcoin.{Block, ByteVector32, ByteVector64, Crypto, DeterministicWallet, Satoshi, SatoshiLong, Transaction, TxIn} +import fr.acinq.bitcoin.scalacompat.Crypto.PrivateKey +import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, ByteVector64, Crypto, DeterministicWallet, Satoshi, SatoshiLong, Transaction, TxIn} import fr.acinq.eclair._ import fr.acinq.eclair.blockchain.fee.FeeratePerKw import fr.acinq.eclair.channel.Helpers.Funding diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelCodecs0Spec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelCodecs0Spec.scala index c488ee882b..82c086eed4 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelCodecs0Spec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelCodecs0Spec.scala @@ -1,6 +1,6 @@ package fr.acinq.eclair.wire.internal.channel.version0 -import fr.acinq.bitcoin.ByteVector32 +import fr.acinq.bitcoin.scalacompat.ByteVector32 import fr.acinq.eclair.transactions.{IncomingHtlc, OutgoingHtlc} import fr.acinq.eclair.wire.internal.channel.version0.ChannelCodecs0.Codecs._ import fr.acinq.eclair.wire.internal.channel.version0.ChannelTypes0.ChannelVersion diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version1/ChannelCodecs1Spec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version1/ChannelCodecs1Spec.scala index f6f5497b16..b06e283794 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version1/ChannelCodecs1Spec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version1/ChannelCodecs1Spec.scala @@ -2,9 +2,9 @@ package fr.acinq.eclair.wire.internal.channel.version1 import akka.actor.ActorSystem import akka.testkit.TestProbe -import fr.acinq.bitcoin.Crypto.PrivateKey -import fr.acinq.bitcoin.DeterministicWallet.KeyPath -import fr.acinq.bitcoin.{DeterministicWallet, OutPoint, Satoshi, SatoshiLong, Script} +import fr.acinq.bitcoin.scalacompat.Crypto.PrivateKey +import fr.acinq.bitcoin.scalacompat.DeterministicWallet.KeyPath +import fr.acinq.bitcoin.scalacompat.{DeterministicWallet, OutPoint, Satoshi, SatoshiLong, Script} import fr.acinq.eclair.blockchain.fee.FeeratePerKw import fr.acinq.eclair.channel.{LocalParams, Origin, RemoteParams} import fr.acinq.eclair.transactions.{CommitmentSpec, DirectedHtlc, IncomingHtlc, OutgoingHtlc} diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version2/ChannelCodecs2Spec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version2/ChannelCodecs2Spec.scala index 4fddb6e55c..13100f6299 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version2/ChannelCodecs2Spec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version2/ChannelCodecs2Spec.scala @@ -1,6 +1,6 @@ package fr.acinq.eclair.wire.internal.channel.version2 -import fr.acinq.bitcoin.{ByteVector64, OutPoint, Transaction} +import fr.acinq.bitcoin.scalacompat.{ByteVector64, OutPoint, Transaction} import fr.acinq.eclair.channel.{ChannelConfig, ChannelFeatures, HtlcTxAndRemoteSig} import fr.acinq.eclair.wire.internal.channel.version2.ChannelCodecs2.Codecs._ import fr.acinq.eclair.wire.internal.channel.version2.ChannelCodecs2.stateDataCodec diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3Spec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3Spec.scala index b6fc95aa4e..832ebe667a 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3Spec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3Spec.scala @@ -15,7 +15,7 @@ */ package fr.acinq.eclair.wire.internal.channel.version3 -import fr.acinq.bitcoin.{ByteVector32, Satoshi} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, Satoshi} import fr.acinq.eclair.FeatureSupport.{Mandatory, Optional} import fr.acinq.eclair.Features.{ChannelRangeQueries, PaymentSecret, VariableLengthOnion} import fr.acinq.eclair.channel._ diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/CommonCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/CommonCodecsSpec.scala index 215d738ca9..dbc6b4e11e 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/CommonCodecsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/CommonCodecsSpec.scala @@ -17,8 +17,8 @@ package fr.acinq.eclair.wire.protocol import com.google.common.net.InetAddresses -import fr.acinq.bitcoin.Crypto.PrivateKey -import fr.acinq.bitcoin._ +import fr.acinq.bitcoin.scalacompat.Crypto.PrivateKey +import fr.acinq.bitcoin.scalacompat._ import fr.acinq.eclair.blockchain.fee.FeeratePerKw import fr.acinq.eclair.channel.ChannelFlags import fr.acinq.eclair.crypto.Hmac256 diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/ExtendedQueriesCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/ExtendedQueriesCodecsSpec.scala index 558b776c86..342ae09b36 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/ExtendedQueriesCodecsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/ExtendedQueriesCodecsSpec.scala @@ -16,7 +16,7 @@ package fr.acinq.eclair.wire.protocol -import fr.acinq.bitcoin.{Block, ByteVector32, ByteVector64} +import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, ByteVector64} import fr.acinq.eclair.router.Sync import fr.acinq.eclair.wire.protocol.LightningMessageCodecs._ import fr.acinq.eclair.wire.protocol.ReplyChannelRangeTlv._ diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/FailureMessageCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/FailureMessageCodecsSpec.scala index 466b68ef18..70f13991de 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/FailureMessageCodecsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/FailureMessageCodecsSpec.scala @@ -16,7 +16,7 @@ package fr.acinq.eclair.wire.protocol -import fr.acinq.bitcoin.{Block, ByteVector32, ByteVector64} +import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, ByteVector64} import fr.acinq.eclair.crypto.Hmac256 import fr.acinq.eclair.wire.protocol.FailureMessageCodecs._ import fr.acinq.eclair.{BlockHeight, CltvExpiry, CltvExpiryDelta, MilliSatoshi, MilliSatoshiLong, ShortChannelId, TimestampSecond, TimestampSecondLong, UInt64, randomBytes32, randomBytes64} diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecsSpec.scala index 759aee0dab..389141e7b8 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecsSpec.scala @@ -16,8 +16,8 @@ package fr.acinq.eclair.wire.protocol -import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} -import fr.acinq.bitcoin.{Block, ByteVector32, ByteVector64, SatoshiLong} +import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} +import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, ByteVector64, SatoshiLong} import fr.acinq.eclair.FeatureSupport.Optional import fr.acinq.eclair.Features.DataLossProtect import fr.acinq.eclair._ diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/MessageOnionCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/MessageOnionCodecsSpec.scala index 3c88f2adda..ae4c9fc98c 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/MessageOnionCodecsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/MessageOnionCodecsSpec.scala @@ -1,7 +1,7 @@ package fr.acinq.eclair.wire.protocol -import fr.acinq.bitcoin.ByteVector32 -import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} +import fr.acinq.bitcoin.scalacompat.ByteVector32 +import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} import fr.acinq.eclair.crypto.Sphinx.RouteBlinding import fr.acinq.eclair.wire.protocol.MessageOnion._ import fr.acinq.eclair.wire.protocol.MessageOnionCodecs._ diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/PaymentOnionSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/PaymentOnionSpec.scala index 353b892adb..94e83962ea 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/PaymentOnionSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/PaymentOnionSpec.scala @@ -16,8 +16,8 @@ package fr.acinq.eclair.wire.protocol -import fr.acinq.bitcoin.ByteVector32 -import fr.acinq.bitcoin.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.ByteVector32 +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.eclair.UInt64.Conversions._ import fr.acinq.eclair.payment.Bolt11Invoice.ExtraHop import fr.acinq.eclair.wire.protocol.OnionPaymentPayloadTlv._ diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/RouteBlindingSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/RouteBlindingSpec.scala index 67eaea0dd7..5bbbc114ea 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/RouteBlindingSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/RouteBlindingSpec.scala @@ -1,6 +1,6 @@ package fr.acinq.eclair.wire.protocol -import fr.acinq.bitcoin.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.eclair.crypto.Sphinx import fr.acinq.eclair.wire.protocol.RouteBlindingEncryptedDataTlv._ import fr.acinq.eclair.{ShortChannelId, UInt64, randomKey} diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/TlvCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/TlvCodecsSpec.scala index df4b75264a..c718ab26ff 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/TlvCodecsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/TlvCodecsSpec.scala @@ -16,7 +16,7 @@ package fr.acinq.eclair.wire.protocol -import fr.acinq.bitcoin.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.eclair.UInt64.Conversions._ import fr.acinq.eclair.wire.protocol.CommonCodecs.{publicKey, shortchannelid, uint64, varint} import fr.acinq.eclair.wire.protocol.TlvCodecs._ diff --git a/eclair-front/src/main/scala/fr/acinq/eclair/FrontSetup.scala b/eclair-front/src/main/scala/fr/acinq/eclair/FrontSetup.scala index 3b43e3604f..90d8e57b31 100644 --- a/eclair-front/src/main/scala/fr/acinq/eclair/FrontSetup.scala +++ b/eclair-front/src/main/scala/fr/acinq/eclair/FrontSetup.scala @@ -24,7 +24,7 @@ import akka.pattern.ask import akka.util.Timeout import com.amazonaws.services.secretsmanager.AWSSecretsManagerClient import com.amazonaws.services.secretsmanager.model.GetSecretValueRequest -import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} +import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} import fr.acinq.eclair.crypto.Noise.KeyPair import fr.acinq.eclair.crypto.keymanager.LocalNodeKeyManager import fr.acinq.eclair.io.Switchboard.{GetRouterPeerConf, RouterPeerConf} diff --git a/eclair-front/src/main/scala/fr/acinq/eclair/router/FrontRouter.scala b/eclair-front/src/main/scala/fr/acinq/eclair/router/FrontRouter.scala index a3a7ea1d5b..aed2186f54 100644 --- a/eclair-front/src/main/scala/fr/acinq/eclair/router/FrontRouter.scala +++ b/eclair-front/src/main/scala/fr/acinq/eclair/router/FrontRouter.scala @@ -20,8 +20,8 @@ import akka.Done import akka.actor.{ActorRef, Props} import akka.event.Logging.MDC import akka.event.LoggingAdapter -import fr.acinq.bitcoin.ByteVector32 -import fr.acinq.bitcoin.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.ByteVector32 +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.eclair.Logs.LogCategory import fr.acinq.eclair.crypto.TransportHandler import fr.acinq.eclair.io.Peer.PeerRoutingMessage diff --git a/eclair-front/src/test/scala/fr/acinq/eclair/router/FrontRouterSpec.scala b/eclair-front/src/test/scala/fr/acinq/eclair/router/FrontRouterSpec.scala index 51eaaa3d9d..d05ea1f218 100644 --- a/eclair-front/src/test/scala/fr/acinq/eclair/router/FrontRouterSpec.scala +++ b/eclair-front/src/test/scala/fr/acinq/eclair/router/FrontRouterSpec.scala @@ -19,9 +19,9 @@ package fr.acinq.eclair.router import akka.actor.ActorSystem import akka.actor.typed.scaladsl.adapter.actorRefAdapter import akka.testkit.{TestKit, TestProbe} -import fr.acinq.bitcoin.Crypto.PrivateKey -import fr.acinq.bitcoin.Script.{pay2wsh, write} -import fr.acinq.bitcoin.{Block, SatoshiLong, Transaction, TxOut} +import fr.acinq.bitcoin.scalacompat.Crypto.PrivateKey +import fr.acinq.bitcoin.scalacompat.Script.{pay2wsh, write} +import fr.acinq.bitcoin.scalacompat.{Block, SatoshiLong, Transaction, TxOut} import fr.acinq.eclair.TestConstants.Alice import fr.acinq.eclair._ import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.{UtxoStatus, ValidateRequest, ValidateResult} diff --git a/eclair-node/src/main/scala/fr/acinq/eclair/api/directives/ExtraDirectives.scala b/eclair-node/src/main/scala/fr/acinq/eclair/api/directives/ExtraDirectives.scala index b56baf6f93..e7889d02ac 100644 --- a/eclair-node/src/main/scala/fr/acinq/eclair/api/directives/ExtraDirectives.scala +++ b/eclair-node/src/main/scala/fr/acinq/eclair/api/directives/ExtraDirectives.scala @@ -21,8 +21,8 @@ import akka.http.scaladsl.marshalling.ToResponseMarshaller import akka.http.scaladsl.model.StatusCodes.NotFound import akka.http.scaladsl.model.{ContentTypes, HttpResponse} import akka.http.scaladsl.server.{Directive1, Directives, MalformedFormFieldRejection, Route} -import fr.acinq.bitcoin.ByteVector32 -import fr.acinq.bitcoin.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.ByteVector32 +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.eclair.ApiTypes.ChannelIdentifier import fr.acinq.eclair.api.serde.FormParamExtractors._ import fr.acinq.eclair.api.serde.JsonSupport._ diff --git a/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Channel.scala b/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Channel.scala index 6a8691e947..61fece11a3 100644 --- a/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Channel.scala +++ b/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Channel.scala @@ -18,7 +18,7 @@ package fr.acinq.eclair.api.handlers import akka.http.scaladsl.server.{MalformedFormFieldRejection, Route} import akka.util.Timeout -import fr.acinq.bitcoin.{Satoshi, Script} +import fr.acinq.bitcoin.scalacompat.{Satoshi, Script} import fr.acinq.eclair.MilliSatoshi import fr.acinq.eclair.api.Service import fr.acinq.eclair.api.directives.EclairDirectives diff --git a/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Invoice.scala b/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Invoice.scala index 94dd48e31c..884a3fa3ef 100644 --- a/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Invoice.scala +++ b/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Invoice.scala @@ -17,7 +17,7 @@ package fr.acinq.eclair.api.handlers import akka.http.scaladsl.server.Route -import fr.acinq.bitcoin.ByteVector32 +import fr.acinq.bitcoin.scalacompat.ByteVector32 import fr.acinq.eclair.api.Service import fr.acinq.eclair.api.directives.EclairDirectives import fr.acinq.eclair.api.serde.FormParamExtractors._ diff --git a/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Message.scala b/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Message.scala index 975a882608..1f9db32194 100644 --- a/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Message.scala +++ b/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Message.scala @@ -17,7 +17,7 @@ package fr.acinq.eclair.api.handlers import akka.http.scaladsl.server.{MalformedFormFieldRejection, Route} -import fr.acinq.bitcoin.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.eclair.api.Service import fr.acinq.eclair.api.directives.EclairDirectives import fr.acinq.eclair.api.serde.FormParamExtractors._ diff --git a/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/OnChain.scala b/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/OnChain.scala index ee6fad9487..7074b49bfd 100644 --- a/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/OnChain.scala +++ b/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/OnChain.scala @@ -17,7 +17,7 @@ package fr.acinq.eclair.api.handlers import akka.http.scaladsl.server.Route -import fr.acinq.bitcoin.Satoshi +import fr.acinq.bitcoin.scalacompat.Satoshi import fr.acinq.eclair.api.Service import fr.acinq.eclair.api.directives.EclairDirectives import fr.acinq.eclair.api.serde.FormParamExtractors._ diff --git a/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/PathFinding.scala b/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/PathFinding.scala index f1c230c947..163db4fed1 100644 --- a/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/PathFinding.scala +++ b/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/PathFinding.scala @@ -17,7 +17,7 @@ package fr.acinq.eclair.api.handlers import akka.http.scaladsl.server.{MalformedFormFieldRejection, Route} -import fr.acinq.bitcoin.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.eclair.api.Service import fr.acinq.eclair.api.directives.{EclairDirectives, RouteFormat} import fr.acinq.eclair.api.serde.FormParamExtractors._ diff --git a/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Payment.scala b/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Payment.scala index 912d6f15f7..f6b68fbded 100644 --- a/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Payment.scala +++ b/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Payment.scala @@ -17,8 +17,8 @@ package fr.acinq.eclair.api.handlers import akka.http.scaladsl.server.{MalformedFormFieldRejection, Route} -import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.bitcoin.{ByteVector32, Satoshi} +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.{ByteVector32, Satoshi} import fr.acinq.eclair.api.Service import fr.acinq.eclair.api.directives.EclairDirectives import fr.acinq.eclair.api.serde.FormParamExtractors.{pubkeyListUnmarshaller, _} diff --git a/eclair-node/src/main/scala/fr/acinq/eclair/api/serde/FormParamExtractors.scala b/eclair-node/src/main/scala/fr/acinq/eclair/api/serde/FormParamExtractors.scala index 8096b3b3a1..b02bd3178a 100644 --- a/eclair-node/src/main/scala/fr/acinq/eclair/api/serde/FormParamExtractors.scala +++ b/eclair-node/src/main/scala/fr/acinq/eclair/api/serde/FormParamExtractors.scala @@ -18,8 +18,8 @@ package fr.acinq.eclair.api.serde import akka.http.scaladsl.unmarshalling.Unmarshaller import akka.util.Timeout -import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.bitcoin.{ByteVector32, Satoshi} +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.{ByteVector32, Satoshi} import fr.acinq.eclair.api.directives.RouteFormat import fr.acinq.eclair.api.serde.JsonSupport._ import fr.acinq.eclair.blockchain.fee.FeeratePerByte diff --git a/eclair-node/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala b/eclair-node/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala index ff44e2f097..f79260a986 100644 --- a/eclair-node/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala +++ b/eclair-node/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala @@ -24,8 +24,8 @@ import akka.http.scaladsl.server.Route import akka.http.scaladsl.testkit.{RouteTestTimeout, ScalatestRouteTest, WSProbe} import akka.util.Timeout import de.heikoseeberger.akkahttpjson4s.Json4sSupport -import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} -import fr.acinq.bitcoin.{Block, ByteVector32, ByteVector64, SatoshiLong} +import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} +import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, ByteVector64, SatoshiLong} import fr.acinq.eclair.ApiTypes.ChannelIdentifier import fr.acinq.eclair.FeatureSupport.{Mandatory, Optional} import fr.acinq.eclair.Features.{ChannelRangeQueriesExtended, DataLossProtect} diff --git a/pom.xml b/pom.xml index 5c8aeeb79d..4db741eaa4 100644 --- a/pom.xml +++ b/pom.xml @@ -72,7 +72,7 @@ 2.6.18 10.2.7 3.4.1 - 0.19 + 0.22 31.1-jre 2.4.6 From 36ed788d392e53a95f707c33dd0930871c72936a Mon Sep 17 00:00:00 2001 From: Bastien Teinturier <31281497+t-bast@users.noreply.github.com> Date: Thu, 7 Apr 2022 11:02:59 +0200 Subject: [PATCH 049/121] Update experimental trampoline feature bit (#2219) The `option_scid_alias` feature (https://github.com/lightning/bolts/pull/910) is going to clash with the feature bit we use for trampoline payments. Fortunately we've never advertized it, so we can simply update it to a much higher value for now (until the final version of trampoline is specified). --- eclair-core/src/main/resources/reference.conf | 2 +- .../main/scala/fr/acinq/eclair/Features.scala | 23 +++++++++++-------- .../payment/receive/MultiPartHandler.scala | 2 +- .../payment/send/PaymentInitiator.scala | 4 ++-- .../integration/PaymentIntegrationSpec.scala | 10 ++++---- .../eclair/payment/Bolt11InvoiceSpec.scala | 16 ++++++------- .../eclair/payment/MultiPartHandlerSpec.scala | 8 +++---- .../eclair/payment/PaymentInitiatorSpec.scala | 2 +- 8 files changed, 35 insertions(+), 32 deletions(-) diff --git a/eclair-core/src/main/resources/reference.conf b/eclair-core/src/main/resources/reference.conf index 6096b75edc..ac83a875e7 100644 --- a/eclair-core/src/main/resources/reference.conf +++ b/eclair-core/src/main/resources/reference.conf @@ -57,7 +57,7 @@ eclair { option_onion_messages = optional option_channel_type = optional option_payment_metadata = optional - trampoline_payment = disabled + trampoline_payment_prototype = disabled keysend = disabled } override-init-features = [ // optional per-node features diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala index 268b53b7c2..504b0ef707 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala @@ -226,19 +226,22 @@ object Features { val mandatory = 48 } - // TODO: @t-bast: update feature bits once spec-ed (currently reserved here: https://github.com/lightningnetwork/lightning-rfc/issues/605) - // We're not advertising these bits yet in our announcements, clients have to assume support. - // This is why we haven't added them yet to `areSupported`. - case object TrampolinePayment extends Feature with InitFeature with NodeFeature with InvoiceFeature { - val rfcName = "trampoline_payment" - val mandatory = 50 - } - case object KeySend extends Feature with NodeFeature { val rfcName = "keysend" val mandatory = 54 } + // TODO: @t-bast: update feature bits once spec-ed (currently reserved here: https://github.com/lightningnetwork/lightning-rfc/issues/605) + // We're not advertising these bits yet in our announcements, clients have to assume support. + // This is why we haven't added them yet to `areSupported`. + // The version of trampoline enabled by this feature bit does not match the latest spec PR: once the spec is accepted, + // we will introduce a new version of trampoline that will work in parallel to this legacy one, until we can safely + // deprecate it. + case object TrampolinePaymentPrototype extends Feature with InitFeature with NodeFeature with InvoiceFeature { + val rfcName = "trampoline_payment_prototype" + val mandatory = 148 + } + val knownFeatures: Set[Feature] = Set( DataLossProtect, InitialRoutingSync, @@ -256,7 +259,7 @@ object Features { OnionMessages, ChannelType, PaymentMetadata, - TrampolinePayment, + TrampolinePaymentPrototype, KeySend ) @@ -269,7 +272,7 @@ object Features { BasicMultiPartPayment -> (PaymentSecret :: Nil), AnchorOutputs -> (StaticRemoteKey :: Nil), AnchorOutputsZeroFeeHtlcTx -> (StaticRemoteKey :: Nil), - TrampolinePayment -> (PaymentSecret :: Nil), + TrampolinePaymentPrototype -> (PaymentSecret :: Nil), KeySend -> (VariableLengthOnion :: Nil) ) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/MultiPartHandler.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/MultiPartHandler.scala index b461ec4f22..f2bc05cf55 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/MultiPartHandler.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/MultiPartHandler.scala @@ -232,7 +232,7 @@ object MultiPartHandler { val expirySeconds = expirySeconds_opt.getOrElse(nodeParams.invoiceExpiry.toSeconds) val paymentMetadata = hex"2a" val invoiceFeatures = if (nodeParams.enableTrampolinePayment) { - Features[InvoiceFeature](nodeParams.features.invoiceFeatures().activated + (Features.TrampolinePayment -> FeatureSupport.Optional)) + Features[InvoiceFeature](nodeParams.features.invoiceFeatures().activated + (Features.TrampolinePaymentPrototype -> FeatureSupport.Optional)) } else { nodeParams.features.invoiceFeatures() } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentInitiator.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentInitiator.scala index d7f6f8381b..0fc74bfc26 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentInitiator.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentInitiator.scala @@ -85,7 +85,7 @@ class PaymentInitiator(nodeParams: NodeParams, outgoingPaymentFactory: PaymentIn r.trampolineAttempts match { case Nil => sender() ! PaymentFailed(paymentId, r.paymentHash, LocalFailure(r.recipientAmount, Nil, TrampolineFeesMissing) :: Nil) - case _ if !r.invoice.features.hasFeature(Features.TrampolinePayment) && r.invoice.amount_opt.isEmpty => + case _ if !r.invoice.features.hasFeature(Features.TrampolinePaymentPrototype) && r.invoice.amount_opt.isEmpty => sender() ! PaymentFailed(paymentId, r.paymentHash, LocalFailure(r.recipientAmount, Nil, TrampolineLegacyAmountLessInvoice) :: Nil) case (trampolineFees, trampolineExpiryDelta) :: remainingAttempts => log.info(s"sending trampoline payment with trampoline fees=$trampolineFees and expiry delta=$trampolineExpiryDelta") @@ -203,7 +203,7 @@ class PaymentInitiator(nodeParams: NodeParams, outgoingPaymentFactory: PaymentIn PaymentOnion.createSinglePartPayload(r.recipientAmount, r.finalExpiry(nodeParams.currentBlockHeight), r.invoice.paymentSecret.get, r.invoice.paymentMetadata) } // We assume that the trampoline node supports multi-part payments (it should). - val trampolinePacket_opt = if (r.invoice.features.hasFeature(Features.TrampolinePayment)) { + val trampolinePacket_opt = if (r.invoice.features.hasFeature(Features.TrampolinePaymentPrototype)) { OutgoingPaymentPacket.buildTrampolinePacket(r.paymentHash, trampolineRoute, finalPayload) } else { OutgoingPaymentPacket.buildTrampolineToLegacyPacket(r.invoice, trampolineRoute, finalPayload) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/PaymentIntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/PaymentIntegrationSpec.scala index 5c20e6e695..a81f02ac00 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/PaymentIntegrationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/PaymentIntegrationSpec.scala @@ -465,7 +465,7 @@ class PaymentIntegrationSpec extends IntegrationSpec { sender.send(nodes("F").paymentHandler, ReceivePayment(Some(amount), Left("like trampoline much?"))) val invoice = sender.expectMsgType[Invoice] assert(invoice.features.hasFeature(Features.BasicMultiPartPayment)) - assert(invoice.features.hasFeature(Features.TrampolinePayment)) + assert(invoice.features.hasFeature(Features.TrampolinePaymentPrototype)) // The best route from G is G -> C -> F which has a fee of 1210091 msat @@ -510,7 +510,7 @@ class PaymentIntegrationSpec extends IntegrationSpec { sender.send(nodes("B").paymentHandler, ReceivePayment(Some(amount), Left("trampoline-MPP is so #reckless"))) val invoice = sender.expectMsgType[Invoice] assert(invoice.features.hasFeature(Features.BasicMultiPartPayment)) - assert(invoice.features.hasFeature(Features.TrampolinePayment)) + assert(invoice.features.hasFeature(Features.TrampolinePaymentPrototype)) assert(invoice.paymentMetadata.nonEmpty) // The direct route C -> B does not have enough capacity, the payment will be split between @@ -566,7 +566,7 @@ class PaymentIntegrationSpec extends IntegrationSpec { sender.send(nodes("A").paymentHandler, ReceivePayment(Some(amount), Left("trampoline to non-trampoline is so #vintage"), extraHops = routingHints)) val invoice = sender.expectMsgType[Invoice] assert(invoice.features.hasFeature(Features.BasicMultiPartPayment)) - assert(!invoice.features.hasFeature(Features.TrampolinePayment)) + assert(!invoice.features.hasFeature(Features.TrampolinePaymentPrototype)) assert(invoice.paymentMetadata.nonEmpty) val payment = SendTrampolinePayment(amount, invoice, nodes("C").nodeParams.nodeId, Seq((1500000 msat, CltvExpiryDelta(432))), routeParams = integrationTestRouteParams) @@ -614,7 +614,7 @@ class PaymentIntegrationSpec extends IntegrationSpec { sender.send(nodes("D").paymentHandler, ReceivePayment(Some(amount), Left("I iz Satoshi"))) val invoice = sender.expectMsgType[Invoice] assert(invoice.features.hasFeature(Features.BasicMultiPartPayment)) - assert(invoice.features.hasFeature(Features.TrampolinePayment)) + assert(invoice.features.hasFeature(Features.TrampolinePaymentPrototype)) val payment = SendTrampolinePayment(amount, invoice, nodes("C").nodeParams.nodeId, Seq((250000 msat, CltvExpiryDelta(288))), routeParams = integrationTestRouteParams) sender.send(nodes("B").paymentInitiator, payment) @@ -635,7 +635,7 @@ class PaymentIntegrationSpec extends IntegrationSpec { sender.send(nodes("D").paymentHandler, ReceivePayment(Some(amount), Left("I iz not Satoshi"))) val invoice = sender.expectMsgType[Invoice] assert(invoice.features.hasFeature(Features.BasicMultiPartPayment)) - assert(invoice.features.hasFeature(Features.TrampolinePayment)) + assert(invoice.features.hasFeature(Features.TrampolinePaymentPrototype)) val payment = SendTrampolinePayment(amount, invoice, nodes("B").nodeParams.nodeId, Seq((450000 msat, CltvExpiryDelta(288))), routeParams = integrationTestRouteParams) sender.send(nodes("A").paymentInitiator, payment) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/Bolt11InvoiceSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/Bolt11InvoiceSpec.scala index 707b5c42af..e1bb6d543d 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/Bolt11InvoiceSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/Bolt11InvoiceSpec.scala @@ -309,7 +309,7 @@ class Bolt11InvoiceSpec extends AnyFunSuite { assert(features2bits(invoice.features) === bin"1000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000100000000") assert(!invoice.features.hasFeature(BasicMultiPartPayment)) assert(invoice.features.hasFeature(PaymentSecret, Some(Mandatory))) - assert(!invoice.features.hasFeature(TrampolinePayment)) + assert(!invoice.features.hasFeature(TrampolinePaymentPrototype)) assert(TestConstants.Alice.nodeParams.features.invoiceFeatures().areSupported(invoice.features)) assert(invoice.sign(priv).toString === ref.toLowerCase) } @@ -329,7 +329,7 @@ class Bolt11InvoiceSpec extends AnyFunSuite { assert(features2bits(invoice.features) === bin"000011000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000100000000") assert(!invoice.features.hasFeature(BasicMultiPartPayment)) assert(invoice.features.hasFeature(PaymentSecret, Some(Mandatory))) - assert(!invoice.features.hasFeature(TrampolinePayment)) + assert(!invoice.features.hasFeature(TrampolinePaymentPrototype)) assert(!TestConstants.Alice.nodeParams.features.invoiceFeatures().areSupported(invoice.features)) assert(invoice.sign(priv).toString === ref) } @@ -534,18 +534,18 @@ class Bolt11InvoiceSpec extends AnyFunSuite { test("trampoline") { val invoice = Bolt11Invoice(Block.LivenetGenesisBlock.hash, Some(123 msat), ByteVector32.One, priv, Left("Some invoice"), CltvExpiryDelta(18)) - assert(!invoice.features.hasFeature(TrampolinePayment)) + assert(!invoice.features.hasFeature(TrampolinePaymentPrototype)) - val pr1 = Bolt11Invoice(Block.LivenetGenesisBlock.hash, Some(123 msat), ByteVector32.One, priv, Left("Some invoice"), CltvExpiryDelta(18), features = Features(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory, TrampolinePayment -> Optional)) + val pr1 = Bolt11Invoice(Block.LivenetGenesisBlock.hash, Some(123 msat), ByteVector32.One, priv, Left("Some invoice"), CltvExpiryDelta(18), features = Features(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory, TrampolinePaymentPrototype -> Optional)) assert(!pr1.features.hasFeature(BasicMultiPartPayment)) - assert(pr1.features.hasFeature(TrampolinePayment)) + assert(pr1.features.hasFeature(TrampolinePaymentPrototype)) - val pr2 = Bolt11Invoice(Block.LivenetGenesisBlock.hash, Some(123 msat), ByteVector32.One, priv, Left("Some invoice"), CltvExpiryDelta(18), features = Features(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory, BasicMultiPartPayment -> Optional, TrampolinePayment -> Optional)) + val pr2 = Bolt11Invoice(Block.LivenetGenesisBlock.hash, Some(123 msat), ByteVector32.One, priv, Left("Some invoice"), CltvExpiryDelta(18), features = Features(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory, BasicMultiPartPayment -> Optional, TrampolinePaymentPrototype -> Optional)) assert(pr2.features.hasFeature(BasicMultiPartPayment)) - assert(pr2.features.hasFeature(TrampolinePayment)) + assert(pr2.features.hasFeature(TrampolinePaymentPrototype)) val pr3 = Bolt11Invoice.fromString("lnbc40n1pw9qjvwpp5qq3w2ln6krepcslqszkrsfzwy49y0407hvks30ec6pu9s07jur3sdpstfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqenwdencqzysxqrrss7ju0s4dwx6w8a95a9p2xc5vudl09gjl0w2n02sjrvffde632nxwh2l4w35nqepj4j5njhh4z65wyfc724yj6dn9wajvajfn5j7em6wsq2elakl") - assert(!pr3.features.hasFeature(TrampolinePayment)) + assert(!pr3.features.hasFeature(TrampolinePaymentPrototype)) } test("nonreg") { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartHandlerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartHandlerSpec.scala index 778164b2e9..8409805683 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartHandlerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartHandlerSpec.scala @@ -190,28 +190,28 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike sender.send(handler, ReceivePayment(Some(42 msat), Left("1 coffee"))) val invoice = sender.expectMsgType[Invoice] assert(!invoice.features.hasFeature(Features.BasicMultiPartPayment)) - assert(!invoice.features.hasFeature(Features.TrampolinePayment)) + assert(!invoice.features.hasFeature(Features.TrampolinePaymentPrototype)) } { val handler = TestActorRef[PaymentHandler](PaymentHandler.props(Alice.nodeParams.copy(enableTrampolinePayment = false, features = featuresWithMpp), TestProbe().ref)) sender.send(handler, ReceivePayment(Some(42 msat), Left("1 coffee"))) val invoice = sender.expectMsgType[Invoice] assert(invoice.features.hasFeature(Features.BasicMultiPartPayment)) - assert(!invoice.features.hasFeature(Features.TrampolinePayment)) + assert(!invoice.features.hasFeature(Features.TrampolinePaymentPrototype)) } { val handler = TestActorRef[PaymentHandler](PaymentHandler.props(Alice.nodeParams.copy(enableTrampolinePayment = true, features = featuresWithoutMpp), TestProbe().ref)) sender.send(handler, ReceivePayment(Some(42 msat), Left("1 coffee"))) val invoice = sender.expectMsgType[Invoice] assert(!invoice.features.hasFeature(Features.BasicMultiPartPayment)) - assert(invoice.features.hasFeature(Features.TrampolinePayment)) + assert(invoice.features.hasFeature(Features.TrampolinePaymentPrototype)) } { val handler = TestActorRef[PaymentHandler](PaymentHandler.props(Alice.nodeParams.copy(enableTrampolinePayment = true, features = featuresWithMpp), TestProbe().ref)) sender.send(handler, ReceivePayment(Some(42 msat), Left("1 coffee"))) val invoice = sender.expectMsgType[Invoice] assert(invoice.features.hasFeature(Features.BasicMultiPartPayment)) - assert(invoice.features.hasFeature(Features.TrampolinePayment)) + assert(invoice.features.hasFeature(Features.TrampolinePaymentPrototype)) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentInitiatorSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentInitiatorSpec.scala index 61a2a3a224..19ee4ff7ad 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentInitiatorSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentInitiatorSpec.scala @@ -68,7 +68,7 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory, BasicMultiPartPayment -> Optional, - TrampolinePayment -> Optional, + TrampolinePaymentPrototype -> Optional, ) case class FakePaymentFactory(payFsm: TestProbe, multiPartPayFsm: TestProbe) extends PaymentInitiator.MultiPartPaymentFactory { From 446a780989f2d326df2511b92e1fb33cac8bc723 Mon Sep 17 00:00:00 2001 From: Bastien Teinturier <31281497+t-bast@users.noreply.github.com> Date: Thu, 7 Apr 2022 16:15:34 +0200 Subject: [PATCH 050/121] Refactor channel transitions (#2228) * Rename HasCommitments * Introduce a TransientChannelData trait, which lets us have complete pattern matching on channel data * Remove channel data from the ChannelErrorOccured event * A few fixes in Channel.scala * Refactor channel transition handlers --- .../main/scala/fr/acinq/eclair/Eclair.scala | 14 +- .../acinq/eclair/balance/BalanceActor.scala | 4 +- .../eclair/balance/ChannelsListener.scala | 8 +- .../acinq/eclair/balance/CheckBalance.scala | 4 +- .../fr/acinq/eclair/channel/ChannelData.scala | 67 ++++---- .../acinq/eclair/channel/ChannelEvents.scala | 6 +- .../fr/acinq/eclair/channel/Helpers.scala | 10 +- .../fr/acinq/eclair/channel/fsm/Channel.scala | 162 +++++++----------- .../channel/fsm/ChannelOpenSingleFunder.scala | 23 +++ .../eclair/channel/fsm/CommonHandlers.scala | 4 +- .../eclair/channel/fsm/ErrorHandlers.scala | 26 +-- .../eclair/channel/fsm/FundingHandlers.scala | 8 +- .../scala/fr/acinq/eclair/db/ChannelsDb.scala | 8 +- .../fr/acinq/eclair/db/DualDatabases.scala | 11 +- .../db/migration/CompareChannelsDb.scala | 14 +- .../db/migration/MigrateChannelsDb.scala | 4 +- .../fr/acinq/eclair/db/pg/PgChannelsDb.scala | 28 +-- .../eclair/db/sqlite/SqliteChannelsDb.scala | 30 ++-- .../main/scala/fr/acinq/eclair/io/Peer.scala | 2 +- .../fr/acinq/eclair/io/Switchboard.scala | 2 +- .../relay/PostRestartHtlcCleaner.scala | 6 +- .../wire/internal/channel/ChannelCodecs.scala | 12 +- .../channel/version0/ChannelCodecs0.scala | 2 +- .../channel/version1/ChannelCodecs1.scala | 2 +- .../channel/version2/ChannelCodecs2.scala | 2 +- .../channel/version3/ChannelCodecs3.scala | 2 +- .../fr/acinq/eclair/EclairImplSpec.scala | 38 ++-- .../scala/fr/acinq/eclair/TestDatabases.scala | 10 +- .../eclair/balance/CheckBalanceSpec.scala | 4 +- .../fr/acinq/eclair/channel/RestoreSpec.scala | 2 +- .../ChannelStateTestsHelperMethods.scala | 30 ++-- .../channel/states/e/NormalStateSpec.scala | 6 +- .../channel/states/e/OfflineStateSpec.scala | 6 +- .../states/g/NegotiatingStateSpec.scala | 8 +- .../channel/states/h/ClosingStateSpec.scala | 2 +- .../fr/acinq/eclair/db/AuditDbSpec.scala | 12 +- .../fr/acinq/eclair/db/ChannelsDbSpec.scala | 4 +- .../integration/ChannelIntegrationSpec.scala | 84 ++++----- .../integration/PaymentIntegrationSpec.scala | 8 +- .../rustytests/SynchronizationPipe.scala | 4 +- .../scala/fr/acinq/eclair/io/PeerSpec.scala | 2 +- .../eclair/json/JsonSerializersSpec.scala | 10 +- .../internal/channel/ChannelCodecsSpec.scala | 42 ++--- .../channel/version2/ChannelCodecs2Spec.scala | 14 +- .../channel/version3/ChannelCodecs3Spec.scala | 14 +- 45 files changed, 376 insertions(+), 385 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala index c8c9767330..6874f8c357 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala @@ -94,9 +94,9 @@ trait Eclair { def updateRelayFee(nodes: List[PublicKey], feeBase: MilliSatoshi, feeProportionalMillionths: Long)(implicit timeout: Timeout): Future[Map[ApiTypes.ChannelIdentifier, Either[Throwable, CommandResponse[CMD_UPDATE_RELAY_FEE]]]] - def channelsInfo(toRemoteNode_opt: Option[PublicKey])(implicit timeout: Timeout): Future[Iterable[RES_GETINFO]] + def channelsInfo(toRemoteNode_opt: Option[PublicKey])(implicit timeout: Timeout): Future[Iterable[RES_GET_CHANNEL_INFO]] - def channelInfo(channel: ApiTypes.ChannelIdentifier)(implicit timeout: Timeout): Future[CommandResponse[CMD_GETINFO]] + def channelInfo(channel: ApiTypes.ChannelIdentifier)(implicit timeout: Timeout): Future[CommandResponse[CMD_GET_CHANNEL_INFO]] def peers()(implicit timeout: Timeout): Future[Iterable[PeerInfo]] @@ -216,7 +216,7 @@ class EclairImpl(appKit: Kit) extends Eclair with Logging { .map(_.filter(n => nodeIds_opt.forall(_.contains(n.nodeId)))) } - override def channelsInfo(toRemoteNode_opt: Option[PublicKey])(implicit timeout: Timeout): Future[Iterable[RES_GETINFO]] = { + override def channelsInfo(toRemoteNode_opt: Option[PublicKey])(implicit timeout: Timeout): Future[Iterable[RES_GET_CHANNEL_INFO]] = { val futureResponse = toRemoteNode_opt match { case Some(pk) => (appKit.register ? Symbol("channelsTo")).mapTo[Map[ByteVector32, PublicKey]].map(_.filter(_._2 == pk).keys) case None => (appKit.register ? Symbol("channels")).mapTo[Map[ByteVector32, ActorRef]].map(_.keys) @@ -224,14 +224,14 @@ class EclairImpl(appKit: Kit) extends Eclair with Logging { for { channelIds <- futureResponse - channels <- Future.sequence(channelIds.map(channelId => sendToChannel[CMD_GETINFO, CommandResponse[CMD_GETINFO]](Left(channelId), CMD_GETINFO(ActorRef.noSender)))) + channels <- Future.sequence(channelIds.map(channelId => sendToChannel[CMD_GET_CHANNEL_INFO, CommandResponse[CMD_GET_CHANNEL_INFO]](Left(channelId), CMD_GET_CHANNEL_INFO(ActorRef.noSender)))) } yield channels.collect { - case properResponse: RES_GETINFO => properResponse + case properResponse: RES_GET_CHANNEL_INFO => properResponse } } - override def channelInfo(channel: ApiTypes.ChannelIdentifier)(implicit timeout: Timeout): Future[CommandResponse[CMD_GETINFO]] = { - sendToChannel[CMD_GETINFO, CommandResponse[CMD_GETINFO]](channel, CMD_GETINFO(ActorRef.noSender)) + override def channelInfo(channel: ApiTypes.ChannelIdentifier)(implicit timeout: Timeout): Future[CommandResponse[CMD_GET_CHANNEL_INFO]] = { + sendToChannel[CMD_GET_CHANNEL_INFO, CommandResponse[CMD_GET_CHANNEL_INFO]](channel, CMD_GET_CHANNEL_INFO(ActorRef.noSender)) } override def allChannels()(implicit timeout: Timeout): Future[Iterable[ChannelDesc]] = { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/balance/BalanceActor.scala b/eclair-core/src/main/scala/fr/acinq/eclair/balance/BalanceActor.scala index 49b60e0da2..966ae3024d 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/balance/BalanceActor.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/balance/BalanceActor.scala @@ -11,7 +11,7 @@ import fr.acinq.eclair.balance.CheckBalance.GlobalBalance import fr.acinq.eclair.balance.Monitoring.{Metrics, Tags} import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinCoreClient import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinCoreClient.Utxo -import fr.acinq.eclair.channel.HasCommitments +import fr.acinq.eclair.channel.PersistentChannelData import fr.acinq.eclair.db.Databases import grizzled.slf4j.Logger import org.json4s.JsonAST.JInt @@ -25,7 +25,7 @@ object BalanceActor { // @formatter:off sealed trait Command private final case object TickBalance extends Command - final case class GetGlobalBalance(replyTo: ActorRef[Try[GlobalBalance]], channels: Map[ByteVector32, HasCommitments]) extends Command + final case class GetGlobalBalance(replyTo: ActorRef[Try[GlobalBalance]], channels: Map[ByteVector32, PersistentChannelData]) extends Command private final case class WrappedChannels(wrapped: ChannelsListener.GetChannelsResponse) extends Command private final case class WrappedGlobalBalanceWithChannels(wrapped: Try[GlobalBalance], channelsCount: Int) extends Command private final case class WrappedUtxoInfo(wrapped: Try[UtxoInfo]) extends Command diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/balance/ChannelsListener.scala b/eclair-core/src/main/scala/fr/acinq/eclair/balance/ChannelsListener.scala index 4051781fb2..64a41c3724 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/balance/ChannelsListener.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/balance/ChannelsListener.scala @@ -9,7 +9,7 @@ import akka.actor.typed.scaladsl.{ActorContext, Behaviors} import fr.acinq.bitcoin.scalacompat.ByteVector32 import fr.acinq.eclair.balance.ChannelsListener._ import fr.acinq.eclair.channel.Helpers.Closing -import fr.acinq.eclair.channel.{ChannelPersisted, ChannelRestored, HasCommitments} +import fr.acinq.eclair.channel.{ChannelPersisted, ChannelRestored, PersistentChannelData} import scala.concurrent.Promise import scala.concurrent.duration.DurationInt @@ -18,14 +18,14 @@ object ChannelsListener { // @formatter:off sealed trait Command - private final case class ChannelData(channelId: ByteVector32, channel: akka.actor.ActorRef, data: HasCommitments) extends Command + private final case class ChannelData(channelId: ByteVector32, channel: akka.actor.ActorRef, data: PersistentChannelData) extends Command private final case class ChannelDied(channelId: ByteVector32) extends Command final case class GetChannels(replyTo: typed.ActorRef[GetChannelsResponse]) extends Command final case object SendDummyEvent extends Command final case object DummyEvent extends Command // @formatter:on - case class GetChannelsResponse(channels: Map[ByteVector32, HasCommitments]) + case class GetChannelsResponse(channels: Map[ByteVector32, PersistentChannelData]) def apply(ready: Promise[Done]): Behavior[Command] = Behaviors.setup { context => @@ -55,7 +55,7 @@ private class ChannelsListener(context: ActorContext[Command]) { private val log = context.log - def running(channels: Map[ByteVector32, HasCommitments]): Behavior[Command] = + def running(channels: Map[ByteVector32, PersistentChannelData]): Behavior[Command] = Behaviors.receiveMessage { case ChannelData(channelId, channel, data) => Closing.isClosed(data, additionalConfirmedTx_opt = None) match { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/balance/CheckBalance.scala b/eclair-core/src/main/scala/fr/acinq/eclair/balance/CheckBalance.scala index 0a5269d78b..8dd65b359c 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/balance/CheckBalance.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/balance/CheckBalance.scala @@ -197,7 +197,7 @@ object CheckBalance { * - In the other cases, we simply take our local amount * - TODO?: we disregard anchor outputs */ - def computeOffChainBalance(channels: Iterable[HasCommitments], knownPreimages: Set[(ByteVector32, Long)]): OffChainBalance = { + def computeOffChainBalance(channels: Iterable[PersistentChannelData], knownPreimages: Set[(ByteVector32, Long)]): OffChainBalance = { channels .foldLeft(OffChainBalance()) { case (r, d: DATA_WAIT_FOR_FUNDING_CONFIRMED) => r.modify(_.waitForFundingConfirmed).using(updateMainBalance(d.commitments.localCommit)) @@ -293,7 +293,7 @@ object CheckBalance { val total: Btc = onChain.total + offChain.total } - def computeGlobalBalance(channels: Map[ByteVector32, HasCommitments], db: Databases, bitcoinClient: BitcoinCoreClient)(implicit ec: ExecutionContext): Future[GlobalBalance] = for { + def computeGlobalBalance(channels: Map[ByteVector32, PersistentChannelData], db: Databases, bitcoinClient: BitcoinCoreClient)(implicit ec: ExecutionContext): Future[GlobalBalance] = for { onChain <- CheckBalance.computeOnChainBalance(bitcoinClient) knownPreimages = db.pendingCommands.listSettlementCommands().collect { case (channelId, cmd: CMD_FULFILL_HTLC) => (channelId, cmd.id) }.toSet offChainRaw = CheckBalance.computeOffChainBalance(channels.values, knownPreimages) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala index ca4f6f3b03..cf7c9db348 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala @@ -62,7 +62,6 @@ case object CLOSED extends ChannelState case object OFFLINE extends ChannelState case object SYNCING extends ChannelState case object WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT extends ChannelState -case object ERR_FUNDING_LOST extends ChannelState case object ERR_INFORMATION_LEAK extends ChannelState /* @@ -96,7 +95,7 @@ case class INPUT_INIT_FUNDEE(temporaryChannelId: ByteVector32, case object INPUT_CLOSE_COMPLETE_TIMEOUT // when requesting a mutual close, we wait for as much as this timeout, then unilateral close case object INPUT_DISCONNECTED case class INPUT_RECONNECTED(remote: ActorRef, localInit: Init, remoteInit: Init) -case class INPUT_RESTORED(data: HasCommitments) +case class INPUT_RESTORED(data: PersistentChannelData) /* .d8888b. .d88888b. 888b d888 888b d888 d8888 888b 888 8888888b. .d8888b. @@ -179,9 +178,9 @@ sealed trait CloseCommand extends HasReplyToCommand final case class CMD_CLOSE(replyTo: ActorRef, scriptPubKey: Option[ByteVector], feerates: Option[ClosingFeerates]) extends CloseCommand final case class CMD_FORCECLOSE(replyTo: ActorRef) extends CloseCommand final case class CMD_UPDATE_RELAY_FEE(replyTo: ActorRef, feeBase: MilliSatoshi, feeProportionalMillionths: Long, cltvExpiryDelta_opt: Option[CltvExpiryDelta]) extends HasReplyToCommand -final case class CMD_GETSTATE(replyTo: ActorRef) extends HasReplyToCommand -final case class CMD_GETSTATEDATA(replyTo: ActorRef) extends HasReplyToCommand -final case class CMD_GETINFO(replyTo: ActorRef)extends HasReplyToCommand +final case class CMD_GET_CHANNEL_STATE(replyTo: ActorRef) extends HasReplyToCommand +final case class CMD_GET_CHANNEL_DATA(replyTo: ActorRef) extends HasReplyToCommand +final case class CMD_GET_CHANNEL_INFO(replyTo: ActorRef)extends HasReplyToCommand /* 88888888b. 8888888888 .d8888b. 88888888b. ,ad8888ba, 888b 88 .d8888b. 8888888888 .d8888b. @@ -225,9 +224,9 @@ object HtlcResult { final case class RES_ADD_SETTLED[+O <: Origin, +R <: HtlcResult](origin: O, htlc: UpdateAddHtlc, result: R) extends CommandSuccess[CMD_ADD_HTLC] /** other specific responses */ -final case class RES_GETSTATE[+S <: ChannelState](state: S) extends CommandSuccess[CMD_GETSTATE] -final case class RES_GETSTATEDATA[+D <: ChannelData](data: D) extends CommandSuccess[CMD_GETSTATEDATA] -final case class RES_GETINFO(nodeId: PublicKey, channelId: ByteVector32, state: ChannelState, data: ChannelData) extends CommandSuccess[CMD_GETINFO] +final case class RES_GET_CHANNEL_STATE(state: ChannelState) extends CommandSuccess[CMD_GET_CHANNEL_STATE] +final case class RES_GET_CHANNEL_DATA[+D <: ChannelData](data: D) extends CommandSuccess[CMD_GET_CHANNEL_DATA] +final case class RES_GET_CHANNEL_INFO(nodeId: PublicKey, channelId: ByteVector32, state: ChannelState, data: ChannelData) extends CommandSuccess[CMD_GET_CHANNEL_INFO] /** * Those are not response to [[Command]], but to [[fr.acinq.eclair.io.Peer.OpenChannel]] @@ -252,19 +251,6 @@ object ChannelOpenResponse { 8888888P" d88P 888 888 d88P 888 */ -sealed trait ChannelData extends PossiblyHarmful { - def channelId: ByteVector32 -} - -case object Nothing extends ChannelData { - val channelId: ByteVector32 = ByteVector32.Zeroes -} - -sealed trait HasCommitments extends ChannelData { - val channelId: ByteVector32 = commitments.channelId - def commitments: Commitments -} - case class ClosingTxProposed(unsignedTx: ClosingTx, localClosingSigned: ClosingSigned) sealed trait CommitPublished { @@ -375,10 +361,25 @@ case class RevokedCommitPublished(commitTx: Transaction, claimMainOutputTx: Opti } } -final case class DATA_WAIT_FOR_OPEN_CHANNEL(initFundee: INPUT_INIT_FUNDEE) extends ChannelData { +sealed trait ChannelData extends PossiblyHarmful { + def channelId: ByteVector32 +} + +sealed trait TransientChannelData extends ChannelData + +case object Nothing extends TransientChannelData { + val channelId: ByteVector32 = ByteVector32.Zeroes +} + +sealed trait PersistentChannelData extends ChannelData { + val channelId: ByteVector32 = commitments.channelId + def commitments: Commitments +} + +final case class DATA_WAIT_FOR_OPEN_CHANNEL(initFundee: INPUT_INIT_FUNDEE) extends TransientChannelData { val channelId: ByteVector32 = initFundee.temporaryChannelId } -final case class DATA_WAIT_FOR_ACCEPT_CHANNEL(initFunder: INPUT_INIT_FUNDER, lastSent: OpenChannel) extends ChannelData { +final case class DATA_WAIT_FOR_ACCEPT_CHANNEL(initFunder: INPUT_INIT_FUNDER, lastSent: OpenChannel) extends TransientChannelData { val channelId: ByteVector32 = initFunder.temporaryChannelId } final case class DATA_WAIT_FOR_FUNDING_INTERNAL(temporaryChannelId: ByteVector32, @@ -390,7 +391,7 @@ final case class DATA_WAIT_FOR_FUNDING_INTERNAL(temporaryChannelId: ByteVector32 remoteFirstPerCommitmentPoint: PublicKey, channelConfig: ChannelConfig, channelFeatures: ChannelFeatures, - lastSent: OpenChannel) extends ChannelData { + lastSent: OpenChannel) extends TransientChannelData { val channelId: ByteVector32 = temporaryChannelId } final case class DATA_WAIT_FOR_FUNDING_CREATED(temporaryChannelId: ByteVector32, @@ -403,7 +404,7 @@ final case class DATA_WAIT_FOR_FUNDING_CREATED(temporaryChannelId: ByteVector32, channelFlags: ChannelFlags, channelConfig: ChannelConfig, channelFeatures: ChannelFeatures, - lastSent: AcceptChannel) extends ChannelData { + lastSent: AcceptChannel) extends TransientChannelData { val channelId: ByteVector32 = temporaryChannelId } final case class DATA_WAIT_FOR_FUNDING_SIGNED(channelId: ByteVector32, @@ -417,13 +418,13 @@ final case class DATA_WAIT_FOR_FUNDING_SIGNED(channelId: ByteVector32, channelFlags: ChannelFlags, channelConfig: ChannelConfig, channelFeatures: ChannelFeatures, - lastSent: FundingCreated) extends ChannelData + lastSent: FundingCreated) extends TransientChannelData final case class DATA_WAIT_FOR_FUNDING_CONFIRMED(commitments: Commitments, fundingTx: Option[Transaction], waitingSince: BlockHeight, // how long have we been waiting for the funding tx to confirm deferred: Option[FundingLocked], - lastSent: Either[FundingCreated, FundingSigned]) extends ChannelData with HasCommitments -final case class DATA_WAIT_FOR_FUNDING_LOCKED(commitments: Commitments, shortChannelId: ShortChannelId, lastSent: FundingLocked) extends ChannelData with HasCommitments + lastSent: Either[FundingCreated, FundingSigned]) extends PersistentChannelData +final case class DATA_WAIT_FOR_FUNDING_LOCKED(commitments: Commitments, shortChannelId: ShortChannelId, lastSent: FundingLocked) extends PersistentChannelData final case class DATA_NORMAL(commitments: Commitments, shortChannelId: ShortChannelId, buried: Boolean, @@ -431,12 +432,12 @@ final case class DATA_NORMAL(commitments: Commitments, channelUpdate: ChannelUpdate, localShutdown: Option[Shutdown], remoteShutdown: Option[Shutdown], - closingFeerates: Option[ClosingFeerates]) extends ChannelData with HasCommitments -final case class DATA_SHUTDOWN(commitments: Commitments, localShutdown: Shutdown, remoteShutdown: Shutdown, closingFeerates: Option[ClosingFeerates]) extends ChannelData with HasCommitments + closingFeerates: Option[ClosingFeerates]) extends PersistentChannelData +final case class DATA_SHUTDOWN(commitments: Commitments, localShutdown: Shutdown, remoteShutdown: Shutdown, closingFeerates: Option[ClosingFeerates]) extends PersistentChannelData final case class DATA_NEGOTIATING(commitments: Commitments, localShutdown: Shutdown, remoteShutdown: Shutdown, closingTxProposed: List[List[ClosingTxProposed]], // one list for every negotiation (there can be several in case of disconnection) - bestUnpublishedClosingTx_opt: Option[ClosingTx]) extends ChannelData with HasCommitments { + bestUnpublishedClosingTx_opt: Option[ClosingTx]) extends PersistentChannelData { require(closingTxProposed.nonEmpty, "there must always be a list for the current negotiation") require(!commitments.localParams.isFunder || closingTxProposed.forall(_.nonEmpty), "funder must have at least one closing signature for every negotiation attempt because it initiates the closing") } @@ -449,12 +450,12 @@ final case class DATA_CLOSING(commitments: Commitments, remoteCommitPublished: Option[RemoteCommitPublished] = None, nextRemoteCommitPublished: Option[RemoteCommitPublished] = None, futureRemoteCommitPublished: Option[RemoteCommitPublished] = None, - revokedCommitPublished: List[RevokedCommitPublished] = Nil) extends ChannelData with HasCommitments { + revokedCommitPublished: List[RevokedCommitPublished] = Nil) extends PersistentChannelData { val spendingTxs: List[Transaction] = mutualClosePublished.map(_.tx) ::: localCommitPublished.map(_.commitTx).toList ::: remoteCommitPublished.map(_.commitTx).toList ::: nextRemoteCommitPublished.map(_.commitTx).toList ::: futureRemoteCommitPublished.map(_.commitTx).toList ::: revokedCommitPublished.map(_.commitTx) require(spendingTxs.nonEmpty, "there must be at least one tx published in this state") } -final case class DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT(commitments: Commitments, remoteChannelReestablish: ChannelReestablish) extends ChannelData with HasCommitments +final case class DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT(commitments: Commitments, remoteChannelReestablish: ChannelReestablish) extends PersistentChannelData /** * @param initFeatures current connection features, or last features used if the channel is disconnected. Note that these diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelEvents.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelEvents.scala index f49958df04..1e5f71afea 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelEvents.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelEvents.scala @@ -40,7 +40,7 @@ trait AbstractChannelRestored extends ChannelEvent { val remoteNodeId: PublicKey } -case class ChannelRestored(channel: ActorRef, channelId: ByteVector32, peer: ActorRef, remoteNodeId: PublicKey, data: HasCommitments) extends AbstractChannelRestored +case class ChannelRestored(channel: ActorRef, channelId: ByteVector32, peer: ActorRef, remoteNodeId: PublicKey, data: PersistentChannelData) extends AbstractChannelRestored case class ChannelIdAssigned(channel: ActorRef, remoteNodeId: PublicKey, temporaryChannelId: ByteVector32, channelId: ByteVector32) extends ChannelEvent @@ -58,7 +58,7 @@ case class ChannelSignatureSent(channel: ActorRef, commitments: Commitments) ext case class ChannelSignatureReceived(channel: ActorRef, commitments: Commitments) extends ChannelEvent -case class ChannelErrorOccurred(channel: ActorRef, channelId: ByteVector32, remoteNodeId: PublicKey, data: ChannelData, error: ChannelOpenError, isFatal: Boolean) extends ChannelEvent +case class ChannelErrorOccurred(channel: ActorRef, channelId: ByteVector32, remoteNodeId: PublicKey, error: ChannelOpenError, isFatal: Boolean) extends ChannelEvent // NB: the fee should be set to 0 when we're not paying it. case class TransactionPublished(channelId: ByteVector32, remoteNodeId: PublicKey, tx: Transaction, miningFee: Satoshi, desc: String) extends ChannelEvent @@ -68,7 +68,7 @@ case class TransactionConfirmed(channelId: ByteVector32, remoteNodeId: PublicKey // NB: this event is only sent when the channel is available. case class AvailableBalanceChanged(channel: ActorRef, channelId: ByteVector32, shortChannelId: ShortChannelId, commitments: AbstractCommitments) extends ChannelEvent -case class ChannelPersisted(channel: ActorRef, remoteNodeId: PublicKey, channelId: ByteVector32, data: HasCommitments) extends ChannelEvent +case class ChannelPersisted(channel: ActorRef, remoteNodeId: PublicKey, channelId: ByteVector32, data: PersistentChannelData) extends ChannelEvent case class LocalCommitConfirmed(channel: ActorRef, remoteNodeId: PublicKey, channelId: ByteVector32, refundAtBlock: BlockHeight) extends ChannelEvent diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala index f31e24743d..3ff8c60375 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala @@ -50,7 +50,7 @@ object Helpers { /** * We update local/global features at reconnection */ - def updateFeatures(data: HasCommitments, localInit: Init, remoteInit: Init): HasCommitments = { + def updateFeatures(data: PersistentChannelData, localInit: Init, remoteInit: Init): PersistentChannelData = { val commitments1 = data.commitments.copy( localParams = data.commitments.localParams.copy(initFeatures = localInit.features), remoteParams = data.commitments.remoteParams.copy(initFeatures = remoteInit.features)) @@ -335,7 +335,7 @@ object Helpers { /** * Check whether we are in sync with our peer. */ - def checkSync(keyManager: ChannelKeyManager, d: HasCommitments, remoteChannelReestablish: ChannelReestablish): SyncResult = { + def checkSync(keyManager: ChannelKeyManager, d: PersistentChannelData, remoteChannelReestablish: ChannelReestablish): SyncResult = { // This is done in two steps: // - step 1: we check our local commitment @@ -343,7 +343,7 @@ object Helpers { // step 2 depends on step 1 because we need to preserve ordering between commit_sig and revocation // step 2: we check the remote commitment - def checkRemoteCommit(d: HasCommitments, remoteChannelReestablish: ChannelReestablish, retransmitRevocation_opt: Option[RevokeAndAck]): SyncResult = { + def checkRemoteCommit(d: PersistentChannelData, remoteChannelReestablish: ChannelReestablish, retransmitRevocation_opt: Option[RevokeAndAck]): SyncResult = { d.commitments.remoteNextCommitInfo match { case Left(waitingForRevocation) if remoteChannelReestablish.nextLocalCommitmentNumber == waitingForRevocation.nextRemoteCommit.index => // we just sent a new commit_sig but they didn't receive it @@ -441,7 +441,7 @@ object Helpers { * * @return true if channel was never open, or got closed immediately, had never any htlcs and local never had a positive balance */ - def nothingAtStake(data: HasCommitments): Boolean = + def nothingAtStake(data: PersistentChannelData): Boolean = data.commitments.localCommit.index == 0 && data.commitments.localCommit.spec.toLocal == 0.msat && data.commitments.remoteCommit.index == 0 && @@ -483,7 +483,7 @@ object Helpers { * because we don't store the closing tx in the channel state * @return the channel closing type, if applicable */ - def isClosed(data: HasCommitments, additionalConfirmedTx_opt: Option[Transaction]): Option[ClosingType] = data match { + def isClosed(data: PersistentChannelData, additionalConfirmedTx_opt: Option[Transaction]): Option[ClosingType] = data match { case closing: DATA_CLOSING if additionalConfirmedTx_opt.exists(closing.mutualClosePublished.map(_.tx).contains) => val closingTx = closing.mutualClosePublished.find(_.tx.txid == additionalConfirmedTx_opt.get.txid).get Some(MutualClose(closingTx)) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala index 5852e4d086..988d5106ee 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala @@ -34,9 +34,9 @@ import fr.acinq.eclair.channel.Helpers.Syncing.SyncResult import fr.acinq.eclair.channel.Helpers.{Closing, Syncing, getRelayFees} import fr.acinq.eclair.channel.Monitoring.Metrics.ProcessMessage import fr.acinq.eclair.channel.Monitoring.{Metrics, Tags} +import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel.publish.TxPublisher import fr.acinq.eclair.channel.publish.TxPublisher.{PublishFinalTx, SetChannelId} -import fr.acinq.eclair.channel._ import fr.acinq.eclair.crypto.keymanager.ChannelKeyManager import fr.acinq.eclair.db.DbEventHandler.ChannelEvent.EventType import fr.acinq.eclair.db.PendingCommandsDb @@ -205,30 +205,6 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val 8888888 888 Y888 8888888 888 */ - /* - NEW - FUNDER FUNDEE - | | - | open_channel |WAIT_FOR_OPEN_CHANNEL - |------------------------------->| - WAIT_FOR_ACCEPT_CHANNEL| | - | accept_channel | - |<-------------------------------| - | |WAIT_FOR_FUNDING_CREATED - | funding_created | - |------------------------------->| - WAIT_FOR_FUNDING_SIGNED| | - | funding_signed | - |<-------------------------------| - WAIT_FOR_FUNDING_LOCKED| |WAIT_FOR_FUNDING_LOCKED - | funding_locked funding_locked | - |--------------- ---------------| - | \/ | - | /\ | - |<-------------- -------------->| - NORMAL| |NORMAL - */ - startWith(WAIT_FOR_INIT_INTERNAL, Nothing) when(WAIT_FOR_INIT_INTERNAL)(handleExceptions { @@ -355,14 +331,6 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val watchFundingTx(data.commitments) goto(OFFLINE) using data } - - case Event(c: CloseCommand, d) => - channelOpenReplyToUser(Right(ChannelOpenResponse.ChannelClosed(d.channelId))) - handleFastClose(c, d.channelId) - - case Event(TickChannelOpenTimeout, _) => - channelOpenReplyToUser(Left(LocalError(new RuntimeException("open channel cancelled, took too long")))) - goto(CLOSED) }) /* @@ -1254,10 +1222,10 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val when(CLOSED)(handleExceptions { case Event(Symbol("shutdown"), _) => stateData match { - case d: HasCommitments => + case d: PersistentChannelData => log.info(s"deleting database record for channelId=${d.channelId}") nodeParams.db.channels.removeChannel(d.channelId) - case _ => + case _: TransientChannelData => // nothing was stored in the DB } log.info("shutting down") stop(FSM.Normal) @@ -1281,7 +1249,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val val d1 = Helpers.updateFeatures(d, localInit, remoteInit) goto(WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT) using d1 sending error - case Event(INPUT_RECONNECTED(r, localInit, remoteInit), d: HasCommitments) => + case Event(INPUT_RECONNECTED(r, localInit, remoteInit), d: PersistentChannelData) => activeConnection = r val yourLastPerCommitmentSecret = d.commitments.remotePerCommitmentSecrets.lastIndex.flatMap(d.commitments.remotePerCommitmentSecrets.getHash).getOrElse(ByteVector32.Zeroes) @@ -1304,9 +1272,9 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val // note: this can only happen if state is NORMAL or SHUTDOWN // -> in NEGOTIATING there are no more htlcs // -> in CLOSING we either have mutual closed (so no more htlcs), or already have unilaterally closed (so no action required), and we can't be in OFFLINE state anyway - case Event(ProcessCurrentBlockHeight(c), d: HasCommitments) => handleNewBlock(c, d) + case Event(ProcessCurrentBlockHeight(c), d: PersistentChannelData) => handleNewBlock(c, d) - case Event(c: CurrentFeerates, d: HasCommitments) => handleCurrentFeerateDisconnected(c, d) + case Event(c: CurrentFeerates, d: PersistentChannelData) => handleCurrentFeerateDisconnected(c, d) case Event(c: CMD_ADD_HTLC, d: DATA_NORMAL) => handleAddDisconnected(c, d) @@ -1326,13 +1294,13 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val case Event(WatchFundingSpentTriggered(tx), d: DATA_NEGOTIATING) if d.closingTxProposed.flatten.exists(_.unsignedTx.tx.txid == tx.txid) => handleMutualClose(getMutualClosePublished(tx, d.closingTxProposed), Left(d)) - case Event(WatchFundingSpentTriggered(tx), d: HasCommitments) if tx.txid == d.commitments.remoteCommit.txid => handleRemoteSpentCurrent(tx, d) + case Event(WatchFundingSpentTriggered(tx), d: PersistentChannelData) if tx.txid == d.commitments.remoteCommit.txid => handleRemoteSpentCurrent(tx, d) - case Event(WatchFundingSpentTriggered(tx), d: HasCommitments) if d.commitments.remoteNextCommitInfo.left.toOption.exists(_.nextRemoteCommit.txid == tx.txid) => handleRemoteSpentNext(tx, d) + case Event(WatchFundingSpentTriggered(tx), d: PersistentChannelData) if d.commitments.remoteNextCommitInfo.left.toOption.exists(_.nextRemoteCommit.txid == tx.txid) => handleRemoteSpentNext(tx, d) case Event(WatchFundingSpentTriggered(tx), d: DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT) => handleRemoteSpentFuture(tx, d) - case Event(WatchFundingSpentTriggered(tx), d: HasCommitments) => handleRemoteSpentOther(tx, d) + case Event(WatchFundingSpentTriggered(tx), d: PersistentChannelData) => handleRemoteSpentOther(tx, d) }) @@ -1444,6 +1412,8 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val case Event(c: CMD_ADD_HTLC, d: DATA_NORMAL) => handleAddDisconnected(c, d) + case Event(c: CMD_UPDATE_RELAY_FEE, d: DATA_NORMAL) => handleUpdateRelayFeeDisconnected(c, d) + case Event(channelReestablish: ChannelReestablish, d: DATA_SHUTDOWN) => Syncing.checkSync(keyManager, d, channelReestablish) match { case syncFailure: SyncResult.Failure => @@ -1488,9 +1458,9 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val context.system.scheduler.scheduleOnce(5 seconds, self, remoteAnnSigs) stay() sending Warning(d.channelId, "spec violation: you sent announcement_signatures before channel_reestablish") - case Event(ProcessCurrentBlockHeight(c), d: HasCommitments) => handleNewBlock(c, d) + case Event(ProcessCurrentBlockHeight(c), d: PersistentChannelData) => handleNewBlock(c, d) - case Event(c: CurrentFeerates, d: HasCommitments) => handleCurrentFeerateDisconnected(c, d) + case Event(c: CurrentFeerates, d: PersistentChannelData) => handleCurrentFeerateDisconnected(c, d) case Event(getTxResponse: GetTxWithMetaResponse, d: DATA_WAIT_FOR_FUNDING_CONFIRMED) if getTxResponse.txid == d.commitments.commitInput.outPoint.txid => handleGetFundingTx(getTxResponse, d.waitingSince, d.fundingTx) @@ -1506,13 +1476,13 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val case Event(WatchFundingSpentTriggered(tx), d: DATA_NEGOTIATING) if d.closingTxProposed.flatten.exists(_.unsignedTx.tx.txid == tx.txid) => handleMutualClose(getMutualClosePublished(tx, d.closingTxProposed), Left(d)) - case Event(WatchFundingSpentTriggered(tx), d: HasCommitments) if tx.txid == d.commitments.remoteCommit.txid => handleRemoteSpentCurrent(tx, d) + case Event(WatchFundingSpentTriggered(tx), d: PersistentChannelData) if tx.txid == d.commitments.remoteCommit.txid => handleRemoteSpentCurrent(tx, d) - case Event(WatchFundingSpentTriggered(tx), d: HasCommitments) if d.commitments.remoteNextCommitInfo.left.toOption.exists(_.nextRemoteCommit.txid == tx.txid) => handleRemoteSpentNext(tx, d) + case Event(WatchFundingSpentTriggered(tx), d: PersistentChannelData) if d.commitments.remoteNextCommitInfo.left.toOption.exists(_.nextRemoteCommit.txid == tx.txid) => handleRemoteSpentNext(tx, d) - case Event(WatchFundingSpentTriggered(tx), d: HasCommitments) => handleRemoteSpentOther(tx, d) + case Event(WatchFundingSpentTriggered(tx), d: PersistentChannelData) => handleRemoteSpentOther(tx, d) - case Event(e: Error, d: HasCommitments) => handleRemoteError(e, d) + case Event(e: Error, d: PersistentChannelData) => handleRemoteError(e, d) }) when(WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT)(handleExceptions { @@ -1525,30 +1495,26 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val when(ERR_INFORMATION_LEAK)(errorStateHandler) - when(ERR_FUNDING_LOST)(errorStateHandler) - whenUnhandled { case Event(INPUT_DISCONNECTED, _) => goto(OFFLINE) - case Event(WatchFundingLostTriggered(_), _) => goto(ERR_FUNDING_LOST) - - case Event(c: CMD_GETSTATE, _) => + case Event(c: CMD_GET_CHANNEL_STATE, _) => val replyTo = if (c.replyTo == ActorRef.noSender) sender() else c.replyTo - replyTo ! RES_GETSTATE(stateName) + replyTo ! RES_GET_CHANNEL_STATE(stateName) stay() - case Event(c: CMD_GETSTATEDATA, _) => + case Event(c: CMD_GET_CHANNEL_DATA, _) => val replyTo = if (c.replyTo == ActorRef.noSender) sender() else c.replyTo - replyTo ! RES_GETSTATEDATA(stateData) + replyTo ! RES_GET_CHANNEL_DATA(stateData) stay() - case Event(c: CMD_GETINFO, _) => + case Event(c: CMD_GET_CHANNEL_INFO, _) => val replyTo = if (c.replyTo == ActorRef.noSender) sender() else c.replyTo - replyTo ! RES_GETINFO(remoteNodeId, stateData.channelId, stateName, stateData) + replyTo ! RES_GET_CHANNEL_INFO(remoteNodeId, stateData.channelId, stateName, stateData) stay() - case Event(c: CMD_ADD_HTLC, d: HasCommitments) => + case Event(c: CMD_ADD_HTLC, d: PersistentChannelData) => log.info(s"rejecting htlc request in state=$stateName") val error = ChannelUnavailable(d.channelId) handleAddHtlcCommandError(c, error, None) // we don't provide a channel_update: this will be a permanent channel failure @@ -1557,12 +1523,13 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val case Event(c: CMD_FORCECLOSE, d) => d match { - case data: HasCommitments => + case data: PersistentChannelData => val replyTo = if (c.replyTo == ActorRef.noSender) sender() else c.replyTo replyTo ! RES_SUCCESS(c, data.channelId) val failure = ForcedLocalCommit(data.channelId) handleLocalError(failure, data, Some(c)) - case _ => handleCommandError(CommandUnavailableInThisState(d.channelId, "forceclose", stateName), c) + case _: TransientChannelData => + handleCommandError(CommandUnavailableInThisState(d.channelId, "forceclose", stateName), c) } // In states where we don't explicitly handle this command, we won't broadcast a new channel update immediately, @@ -1596,12 +1563,12 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val case Event("disconnecting", _) => stay() // funding tx was confirmed in time, let's just ignore this - case Event(BITCOIN_FUNDING_TIMEOUT, _: HasCommitments) => stay() + case Event(BITCOIN_FUNDING_TIMEOUT, _: PersistentChannelData) => stay() // peer doesn't cancel the timer case Event(TickChannelOpenTimeout, _) => stay() - case Event(WatchFundingSpentTriggered(tx), d: HasCommitments) if tx.txid == d.commitments.localCommit.commitTxAndRemoteSig.commitTx.tx.txid => + case Event(WatchFundingSpentTriggered(tx), d: PersistentChannelData) if tx.txid == d.commitments.localCommit.commitTxAndRemoteSig.commitTx.tx.txid => log.warning(s"processing local commit spent in catch-all handler") spendLocalCurrent(d) } @@ -1611,8 +1578,8 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val case state -> nextState => if (state != nextState) { val commitments_opt = nextStateData match { - case hasCommitments: HasCommitments => Some(hasCommitments.commitments) - case _ => None + case d: PersistentChannelData => Some(d.commitments) + case _: TransientChannelData => None } context.system.eventStream.publish(ChannelStateChanged(self, nextStateData.channelId, peer, remoteNodeId, state, nextState, commitments_opt)) } @@ -1629,43 +1596,44 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val // if channel is private, we send the channel_update directly to remote // they need it "to learn the other end's forwarding parameters" (BOLT 7) (state, nextState, stateData, nextStateData) match { - case (_, _, d1: DATA_NORMAL, d2: DATA_NORMAL) if !d1.commitments.announceChannel && !d1.buried && d2.buried => + case (NORMAL, NORMAL, d1: DATA_NORMAL, d2: DATA_NORMAL) if !d1.commitments.announceChannel && !d1.buried && d2.buried => // for a private channel, when the tx was just buried we need to send the channel_update to our peer (even if it didn't change) send(d2.channelUpdate) case (SYNCING, NORMAL, d1: DATA_NORMAL, d2: DATA_NORMAL) if !d1.commitments.announceChannel && d2.buried => // otherwise if we're coming back online, we rebroadcast the latest channel_update // this makes sure that if the channel_update was missed, we have a chance to re-send it send(d2.channelUpdate) - case (_, _, d1: DATA_NORMAL, d2: DATA_NORMAL) if !d1.commitments.announceChannel && d1.channelUpdate != d2.channelUpdate && d2.buried => + case (NORMAL, NORMAL, d1: DATA_NORMAL, d2: DATA_NORMAL) if !d1.commitments.announceChannel && d1.channelUpdate != d2.channelUpdate && d2.buried => // otherwise, we only send it when it is different, and tx is already buried send(d2.channelUpdate) case _ => () } - (state, nextState, stateData, nextStateData) match { - // ORDER MATTERS! - case (WAIT_FOR_INIT_INTERNAL, OFFLINE, _, normal: DATA_NORMAL) => - Logs.withMdc(diagLog)(Logs.mdc(category_opt = Some(Logs.LogCategory.CONNECTION))) { - log.debug("re-emitting channel_update={} enabled={} ", normal.channelUpdate, normal.channelUpdate.channelFlags.isEnabled) - } - context.system.eventStream.publish(LocalChannelUpdate(self, normal.commitments.channelId, normal.shortChannelId, normal.commitments.remoteParams.nodeId, normal.channelAnnouncement, normal.channelUpdate, normal.commitments)) - case (_, _, d1: DATA_NORMAL, d2: DATA_NORMAL) if d1.channelUpdate == d2.channelUpdate && d1.channelAnnouncement == d2.channelAnnouncement => - // don't do anything if neither the channel_update nor the channel_announcement didn't change - () - case (WAIT_FOR_FUNDING_LOCKED | NORMAL | OFFLINE | SYNCING, NORMAL | OFFLINE, _, normal: DATA_NORMAL) => - // when we do WAIT_FOR_FUNDING_LOCKED->NORMAL or NORMAL->NORMAL or SYNCING->NORMAL or NORMAL->OFFLINE, we send out the new channel_update (most of the time it will just be to enable/disable the channel) - log.info("emitting channel_update={} enabled={} ", normal.channelUpdate, normal.channelUpdate.channelFlags.isEnabled) - context.system.eventStream.publish(LocalChannelUpdate(self, normal.commitments.channelId, normal.shortChannelId, normal.commitments.remoteParams.nodeId, normal.channelAnnouncement, normal.channelUpdate, normal.commitments)) - case (_, _, _: DATA_NORMAL, _: DATA_NORMAL) => - // in any other case (e.g. OFFLINE->SYNCING) we do nothing - () - case (_, _, normal: DATA_NORMAL, _) => - // when we finally leave the NORMAL state (or OFFLINE with NORMAL data) to go to SHUTDOWN/NEGOTIATING/CLOSING/ERR*, we advertise the fact that channel can't be used for payments anymore - // if the channel is private we don't really need to tell the counterparty because it is already aware that the channel is being closed - context.system.eventStream.publish(LocalChannelDown(self, normal.commitments.channelId, normal.shortChannelId, normal.commitments.remoteParams.nodeId)) - case _ => () + sealed trait EmitLocalChannelEvent + case class EmitLocalChannelUpdate(d: DATA_NORMAL) extends EmitLocalChannelEvent + case class EmitLocalChannelDown(d: DATA_NORMAL) extends EmitLocalChannelEvent + + val emitEvent_opt: Option[EmitLocalChannelEvent] = (state, nextState, stateData, nextStateData) match { + case (WAIT_FOR_INIT_INTERNAL, OFFLINE, _, d: DATA_NORMAL) => Some(EmitLocalChannelUpdate(d)) + case (WAIT_FOR_FUNDING_LOCKED, NORMAL, _, d: DATA_NORMAL) => Some(EmitLocalChannelUpdate(d)) + case (NORMAL, NORMAL, d1: DATA_NORMAL, d2: DATA_NORMAL) if d1.channelUpdate != d2.channelUpdate || d1.channelAnnouncement != d2.channelAnnouncement => Some(EmitLocalChannelUpdate(d2)) + case (SYNCING, NORMAL, d1: DATA_NORMAL, d2: DATA_NORMAL) if d1.channelUpdate != d2.channelUpdate || d1.channelAnnouncement != d2.channelAnnouncement => Some(EmitLocalChannelUpdate(d2)) + case (NORMAL, OFFLINE, d1: DATA_NORMAL, d2: DATA_NORMAL) if d1.channelUpdate != d2.channelUpdate || d1.channelAnnouncement != d2.channelAnnouncement => Some(EmitLocalChannelUpdate(d2)) + case (OFFLINE, OFFLINE, d1: DATA_NORMAL, d2: DATA_NORMAL) if d1.channelUpdate != d2.channelUpdate || d1.channelAnnouncement != d2.channelAnnouncement => Some(EmitLocalChannelUpdate(d2)) + // When a channel that could previously be used to relay payments starts closing, we advertise the fact that this channel can't be used for payments anymore + // If the channel is private we don't really need to tell the counterparty because it is already aware that the channel is being closed + case (NORMAL | SYNCING | OFFLINE, SHUTDOWN | NEGOTIATING | CLOSING | CLOSED | ERR_INFORMATION_LEAK | WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT, d: DATA_NORMAL, _) => Some(EmitLocalChannelDown(d)) + case _ => None + } + emitEvent_opt.foreach { + case EmitLocalChannelUpdate(d) => + log.info("emitting channel_update={} enabled={} ", d.channelUpdate, d.channelUpdate.channelFlags.isEnabled) + context.system.eventStream.publish(LocalChannelUpdate(self, d.channelId, d.shortChannelId, d.commitments.remoteParams.nodeId, d.channelAnnouncement, d.channelUpdate, d.commitments)) + case EmitLocalChannelDown(d) => + context.system.eventStream.publish(LocalChannelDown(self, d.channelId, d.shortChannelId, d.commitments.remoteParams.nodeId)) } + // When we change our channel update parameters (e.g. relay fees), we want to advertise it to other actors. (stateData, nextStateData) match { // NORMAL->NORMAL, NORMAL->OFFLINE, SYNCING->NORMAL case (d1: DATA_NORMAL, d2: DATA_NORMAL) => maybeEmitChannelUpdateChangedEvent(newUpdate = d2.channelUpdate, oldUpdate_opt = Some(d1.channelUpdate), d2) @@ -1685,7 +1653,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val /** Check pending settlement commands */ onTransition { case _ -> CLOSING => - PendingCommandsDb.getSettlementCommands(nodeParams.db.pendingCommands, nextStateData.asInstanceOf[HasCommitments].channelId) match { + PendingCommandsDb.getSettlementCommands(nodeParams.db.pendingCommands, nextStateData.channelId) match { case Nil => log.debug("nothing to replay") case cmds => @@ -1693,7 +1661,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val cmds.foreach(self ! _) // they all have commit = false } case SYNCING -> (NORMAL | SHUTDOWN) => - PendingCommandsDb.getSettlementCommands(nodeParams.db.pendingCommands, nextStateData.asInstanceOf[HasCommitments].channelId) match { + PendingCommandsDb.getSettlementCommands(nodeParams.db.pendingCommands, nextStateData.channelId) match { case Nil => log.debug("nothing to replay") case cmds => @@ -1725,7 +1693,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val 888 888 d88P 888 888 Y888 8888888P" 88888888 8888888888 888 T88b "Y8888P" */ - private def handleCurrentFeerate(c: CurrentFeerates, d: HasCommitments) = { + private def handleCurrentFeerate(c: CurrentFeerates, d: PersistentChannelData) = { val networkFeeratePerKw = nodeParams.onChainFeeConf.getCommitmentFeerate(remoteNodeId, d.commitments.channelType, d.commitments.capacity, Some(c)) val currentFeeratePerKw = d.commitments.localCommit.spec.commitTxFeerate val shouldUpdateFee = d.commitments.localParams.isFunder && nodeParams.onChainFeeConf.shouldUpdateFee(currentFeeratePerKw, networkFeeratePerKw) @@ -1749,7 +1717,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val * @param d the channel commtiments * @return */ - private def handleCurrentFeerateDisconnected(c: CurrentFeerates, d: HasCommitments) = { + private def handleCurrentFeerateDisconnected(c: CurrentFeerates, d: PersistentChannelData) = { val networkFeeratePerKw = nodeParams.onChainFeeConf.getCommitmentFeerate(remoteNodeId, d.commitments.channelType, d.commitments.capacity, Some(c)) val currentFeeratePerKw = d.commitments.localCommit.spec.commitTxFeerate // if the network fees are too high we risk to not be able to confirm our current commitment @@ -1784,7 +1752,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val log.warning(s"${cause.getMessage} while processing cmd=${c.getClass.getSimpleName} in state=$stateName") val replyTo = if (c.replyTo == ActorRef.noSender) sender() else c.replyTo replyTo ! RES_ADD_FAILED(c, cause, channelUpdate) - context.system.eventStream.publish(ChannelErrorOccurred(self, stateData.channelId, remoteNodeId, stateData, LocalError(cause), isFatal = false)) + context.system.eventStream.publish(ChannelErrorOccurred(self, stateData.channelId, remoteNodeId, LocalError(cause), isFatal = false)) stay() } @@ -1795,11 +1763,11 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val case hasReplyTo: HasReplyToCommand => if (hasReplyTo.replyTo == ActorRef.noSender) Some(sender()) else Some(hasReplyTo.replyTo) } replyTo_opt.foreach(replyTo => replyTo ! RES_FAILURE(c, cause)) - context.system.eventStream.publish(ChannelErrorOccurred(self, stateData.channelId, remoteNodeId, stateData, LocalError(cause), isFatal = false)) + context.system.eventStream.publish(ChannelErrorOccurred(self, stateData.channelId, remoteNodeId, LocalError(cause), isFatal = false)) stay() } - private def handleRevocationTimeout(revocationTimeout: RevocationTimeout, d: HasCommitments) = { + private def handleRevocationTimeout(revocationTimeout: RevocationTimeout, d: PersistentChannelData) = { d.commitments.remoteNextCommitInfo match { case Left(waitingForRevocation) if revocationTimeout.remoteCommitNumber + 1 == waitingForRevocation.nextRemoteCommit.index => log.warning(s"waited for too long for a revocation to remoteCommitNumber=${revocationTimeout.remoteCommitNumber}, disconnecting") @@ -1841,7 +1809,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val stay() using d.copy(channelUpdate = channelUpdate1) storing() } - private def handleSyncFailure(channelReestablish: ChannelReestablish, syncFailure: SyncResult.Failure, d: HasCommitments) = { + private def handleSyncFailure(channelReestablish: ChannelReestablish, syncFailure: SyncResult.Failure, d: PersistentChannelData) = { syncFailure match { case res: SyncResult.LocalLateProven => log.error(s"counterparty proved that we have an outdated (revoked) local commitment!!! ourLocalCommitmentNumber=${res.ourLocalCommitmentNumber} theirRemoteCommitmentNumber=${res.theirRemoteCommitmentNumber}") @@ -1870,7 +1838,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val } } - private def handleNewBlock(c: CurrentBlockHeight, d: HasCommitments) = { + private def handleNewBlock(c: CurrentBlockHeight, d: PersistentChannelData) = { val timedOutOutgoing = d.commitments.timedOutOutgoingHtlcs(c.blockHeight) val almostTimedOutIncoming = d.commitments.almostTimedOutIncomingHtlcs(c.blockHeight, nodeParams.channelConf.fulfillSafetyBeforeTimeout) if (timedOutOutgoing.nonEmpty) { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenSingleFunder.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenSingleFunder.scala index 12cc7a27e9..6bf5fa5389 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenSingleFunder.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenSingleFunder.scala @@ -49,6 +49,29 @@ trait ChannelOpenSingleFunder extends FundingHandlers with ErrorHandlers { this: Channel => + /* + FUNDER FUNDEE + | | + | open_channel |WAIT_FOR_OPEN_CHANNEL + |------------------------------->| + WAIT_FOR_ACCEPT_CHANNEL| | + | accept_channel | + |<-------------------------------| + | |WAIT_FOR_FUNDING_CREATED + | funding_created | + |------------------------------->| + WAIT_FOR_FUNDING_SIGNED| | + | funding_signed | + |<-------------------------------| + WAIT_FOR_FUNDING_LOCKED| |WAIT_FOR_FUNDING_LOCKED + | funding_locked funding_locked | + |--------------- ---------------| + | \/ | + | /\ | + |<-------------- -------------->| + NORMAL| |NORMAL + */ + when(WAIT_FOR_OPEN_CHANNEL)(handleExceptions { case Event(open: OpenChannel, d@DATA_WAIT_FOR_OPEN_CHANNEL(INPUT_INIT_FUNDEE(_, localParams, _, remoteInit, channelConfig, channelType))) => Helpers.validateParamsFundee(nodeParams, channelType, localParams.initFeatures, open, remoteNodeId, remoteInit.features) match { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/CommonHandlers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/CommonHandlers.scala index 5c199d33d9..3fd98cd9a3 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/CommonHandlers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/CommonHandlers.scala @@ -49,12 +49,12 @@ trait CommonHandlers { def storing(unused: Unit = ()): FSM.State[ChannelState, ChannelData] = { state.stateData match { - case d: HasCommitments => + case d: PersistentChannelData => log.debug("updating database record for channelId={}", d.channelId) nodeParams.db.channels.addOrUpdateChannel(d) context.system.eventStream.publish(ChannelPersisted(self, remoteNodeId, d.channelId, d)) state - case _ => + case _: TransientChannelData => log.error(s"can't store data=${state.stateData} in state=${state.stateName}") state } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ErrorHandlers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ErrorHandlers.scala index b03158d1cd..1856a67ab3 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ErrorHandlers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ErrorHandlers.scala @@ -80,15 +80,15 @@ trait ErrorHandlers extends CommonHandlers { } val error = Error(d.channelId, cause.getMessage) - context.system.eventStream.publish(ChannelErrorOccurred(self, stateData.channelId, remoteNodeId, stateData, LocalError(cause), isFatal = true)) + context.system.eventStream.publish(ChannelErrorOccurred(self, stateData.channelId, remoteNodeId, LocalError(cause), isFatal = true)) d match { - case dd: HasCommitments if Closing.nothingAtStake(dd) => goto(CLOSED) + case dd: PersistentChannelData if Closing.nothingAtStake(dd) => goto(CLOSED) case negotiating@DATA_NEGOTIATING(_, _, _, _, Some(bestUnpublishedClosingTx)) => log.info(s"we have a valid closing tx, publishing it instead of our commitment: closingTxId=${bestUnpublishedClosingTx.tx.txid}") // if we were in the process of closing and already received a closing sig from the counterparty, it's always better to use that handleMutualClose(bestUnpublishedClosingTx, Left(negotiating)) - case dd: HasCommitments => + case dd: PersistentChannelData => cause match { case _: ChannelException => // known channel exception: we force close using our current commitment @@ -113,14 +113,14 @@ trait ErrorHandlers extends CommonHandlers { stop(FSM.Shutdown) } } - case _ => goto(CLOSED) sending error // when there is no commitment yet, we just send an error to our peer and go to CLOSED state + case _: TransientChannelData => goto(CLOSED) sending error // when there is no commitment yet, we just send an error to our peer and go to CLOSED state } } def handleRemoteError(e: Error, d: ChannelData) = { // see BOLT 1: only print out data verbatim if is composed of printable ASCII characters log.error(s"peer sent error: ascii='${e.toAscii}' bin=${e.data.toHex}") - context.system.eventStream.publish(ChannelErrorOccurred(self, stateData.channelId, remoteNodeId, stateData, RemoteError(e), isFatal = true)) + context.system.eventStream.publish(ChannelErrorOccurred(self, stateData.channelId, remoteNodeId, RemoteError(e), isFatal = true)) d match { case _: DATA_CLOSING => stay() // nothing to do, there is already a spending tx published @@ -128,8 +128,8 @@ trait ErrorHandlers extends CommonHandlers { // if we were in the process of closing and already received a closing sig from the counterparty, it's always better to use that handleMutualClose(bestUnpublishedClosingTx, Left(negotiating)) case d: DATA_WAIT_FOR_FUNDING_CONFIRMED if Closing.nothingAtStake(d) => goto(CLOSED) // the channel was never used and the funding tx may be double-spent - case hasCommitments: HasCommitments => spendLocalCurrent(hasCommitments) // NB: we publish the commitment even if we have nothing at stake (in a dataloss situation our peer will send us an error just for that) - case _ => goto(CLOSED) // when there is no commitment yet, we just go to CLOSED state in case an error occurs + case hasCommitments: PersistentChannelData => spendLocalCurrent(hasCommitments) // NB: we publish the commitment even if we have nothing at stake (in a dataloss situation our peer will send us an error just for that) + case _: TransientChannelData => goto(CLOSED) // when there is no commitment yet, we just go to CLOSED state in case an error occurs } } @@ -166,7 +166,7 @@ trait ErrorHandlers extends CommonHandlers { skip.foreach(output => log.info(s"no need to watch output=${output.txid}:${output.index}, it has already been spent by txid=${irrevocablySpent.get(output).map(_.txid)}")) } - def spendLocalCurrent(d: HasCommitments) = { + def spendLocalCurrent(d: PersistentChannelData) = { val outdatedCommitment = d match { case _: DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT => true case closing: DATA_CLOSING if closing.futureRemoteCommitPublished.isDefined => true @@ -217,7 +217,7 @@ trait ErrorHandlers extends CommonHandlers { watchSpentIfNeeded(commitTx, watchSpentQueue, irrevocablySpent) } - def handleRemoteSpentCurrent(commitTx: Transaction, d: HasCommitments) = { + def handleRemoteSpentCurrent(commitTx: Transaction, d: PersistentChannelData) = { log.warning(s"they published their current commit in txid=${commitTx.txid}") require(commitTx.txid == d.commitments.remoteCommit.txid, "txid mismatch") @@ -246,7 +246,7 @@ trait ErrorHandlers extends CommonHandlers { goto(CLOSING) using nextData storing() calling doPublish(remoteCommitPublished, d.commitments) } - def handleRemoteSpentNext(commitTx: Transaction, d: HasCommitments) = { + def handleRemoteSpentNext(commitTx: Transaction, d: PersistentChannelData) = { log.warning(s"they published their next commit in txid=${commitTx.txid}") require(d.commitments.remoteNextCommitInfo.isLeft, "next remote commit must be defined") val Left(waitingForRevocation) = d.commitments.remoteNextCommitInfo @@ -282,7 +282,7 @@ trait ErrorHandlers extends CommonHandlers { watchSpentIfNeeded(commitTx, watchSpentQueue, irrevocablySpent) } - def handleRemoteSpentOther(tx: Transaction, d: HasCommitments) = { + def handleRemoteSpentOther(tx: Transaction, d: PersistentChannelData) = { log.warning(s"funding tx spent in txid=${tx.txid}") Closing.RevokedClose.claimCommitTxOutputs(keyManager, d.commitments, tx, nodeParams.db.channels, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) match { case Some(revokedCommitPublished) => @@ -323,7 +323,7 @@ trait ErrorHandlers extends CommonHandlers { watchSpentIfNeeded(commitTx, watchSpentQueue, irrevocablySpent) } - def handleInformationLeak(tx: Transaction, d: HasCommitments) = { + def handleInformationLeak(tx: Transaction, d: PersistentChannelData) = { // this is never supposed to happen !! log.error(s"our funding tx ${d.commitments.commitInput.outPoint.txid} was spent by txid=${tx.txid}!!") context.system.eventStream.publish(NotifyNodeOperator(NotificationsLogger.Error, s"funding tx ${d.commitments.commitInput.outPoint.txid} of channel ${d.channelId} was spent by an unknown transaction, indicating that your DB has lost data or your node has been breached: please contact the dev team.")) @@ -337,7 +337,7 @@ trait ErrorHandlers extends CommonHandlers { goto(ERR_INFORMATION_LEAK) calling doPublish(localCommitPublished, d.commitments) sending error } - def handleOutdatedCommitment(channelReestablish: ChannelReestablish, d: HasCommitments) = { + def handleOutdatedCommitment(channelReestablish: ChannelReestablish, d: PersistentChannelData) = { val exc = PleasePublishYourCommitment(d.channelId) val error = Error(d.channelId, exc.getMessage) goto(WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT) using DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT(d.commitments, channelReestablish) storing() sending error diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/FundingHandlers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/FundingHandlers.scala index 2b3eb92b02..f478859c2f 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/FundingHandlers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/FundingHandlers.scala @@ -102,21 +102,21 @@ trait FundingHandlers extends CommonHandlers { stay() } - def handleFundingPublishFailed(d: HasCommitments) = { + def handleFundingPublishFailed(d: PersistentChannelData) = { log.error(s"failed to publish funding tx") val exc = ChannelFundingError(d.channelId) val error = Error(d.channelId, exc.getMessage) // NB: we don't use the handleLocalError handler because it would result in the commit tx being published, which we don't want: // implementation *guarantees* that in case of BITCOIN_FUNDING_PUBLISH_FAILED, the funding tx hasn't and will never be published, so we can close the channel right away - context.system.eventStream.publish(ChannelErrorOccurred(self, stateData.channelId, remoteNodeId, stateData, LocalError(exc), isFatal = true)) + context.system.eventStream.publish(ChannelErrorOccurred(self, stateData.channelId, remoteNodeId, LocalError(exc), isFatal = true)) goto(CLOSED) sending error } - def handleFundingTimeout(d: HasCommitments) = { + def handleFundingTimeout(d: PersistentChannelData) = { log.warning(s"funding tx hasn't been confirmed in time, cancelling channel delay=$FUNDING_TIMEOUT_FUNDEE") val exc = FundingTxTimedout(d.channelId) val error = Error(d.channelId, exc.getMessage) - context.system.eventStream.publish(ChannelErrorOccurred(self, stateData.channelId, remoteNodeId, stateData, LocalError(exc), isFatal = true)) + context.system.eventStream.publish(ChannelErrorOccurred(self, stateData.channelId, remoteNodeId, LocalError(exc), isFatal = true)) goto(CLOSED) sending error } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/ChannelsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/ChannelsDb.scala index 585191cf63..621862caed 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/ChannelsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/ChannelsDb.scala @@ -18,22 +18,22 @@ package fr.acinq.eclair.db import fr.acinq.bitcoin.scalacompat.ByteVector32 import fr.acinq.eclair.CltvExpiry -import fr.acinq.eclair.channel.HasCommitments +import fr.acinq.eclair.channel.PersistentChannelData import fr.acinq.eclair.db.DbEventHandler.ChannelEvent import java.io.Closeable trait ChannelsDb extends Closeable { - def addOrUpdateChannel(state: HasCommitments): Unit + def addOrUpdateChannel(data: PersistentChannelData): Unit - def getChannel(channelId: ByteVector32): Option[HasCommitments] + def getChannel(channelId: ByteVector32): Option[PersistentChannelData] def updateChannelMeta(channelId: ByteVector32, event: ChannelEvent.EventType): Unit def removeChannel(channelId: ByteVector32): Unit - def listLocalChannels(): Seq[HasCommitments] + def listLocalChannels(): Seq[PersistentChannelData] def addHtlcInfo(channelId: ByteVector32, commitmentNumber: Long, paymentHash: ByteVector32, cltvExpiry: CltvExpiry): Unit diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/DualDatabases.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/DualDatabases.scala index 512d166355..3d64cd57a0 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/DualDatabases.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/DualDatabases.scala @@ -6,7 +6,6 @@ import fr.acinq.eclair.channel._ import fr.acinq.eclair.db.Databases.{FileBackup, PostgresDatabases, SqliteDatabases} import fr.acinq.eclair.db.DbEventHandler.ChannelEvent import fr.acinq.eclair.db.DualDatabases.runAsync -import fr.acinq.eclair.io.Peer import fr.acinq.eclair.payment._ import fr.acinq.eclair.payment.relay.Relayer.RelayFees import fr.acinq.eclair.router.Router @@ -224,12 +223,12 @@ case class DualChannelsDb(primary: ChannelsDb, secondary: ChannelsDb) extends Ch private implicit val ec: ExecutionContext = ExecutionContext.fromExecutor(Executors.newSingleThreadExecutor(new ThreadFactoryBuilder().setNameFormat("db-channels").build())) - override def addOrUpdateChannel(state: HasCommitments): Unit = { - runAsync(secondary.addOrUpdateChannel(state)) - primary.addOrUpdateChannel(state) + override def addOrUpdateChannel(data: PersistentChannelData): Unit = { + runAsync(secondary.addOrUpdateChannel(data)) + primary.addOrUpdateChannel(data) } - override def getChannel(channelId: ByteVector32): Option[HasCommitments] = { + override def getChannel(channelId: ByteVector32): Option[PersistentChannelData] = { runAsync(secondary.getChannel(channelId)) primary.getChannel(channelId) } @@ -244,7 +243,7 @@ case class DualChannelsDb(primary: ChannelsDb, secondary: ChannelsDb) extends Ch primary.removeChannel(channelId) } - override def listLocalChannels(): Seq[HasCommitments] = { + override def listLocalChannels(): Seq[PersistentChannelData] = { runAsync(secondary.listLocalChannels()) primary.listLocalChannels() } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/CompareChannelsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/CompareChannelsDb.scala index 7f166a4d36..3905503b0f 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/CompareChannelsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/CompareChannelsDb.scala @@ -3,7 +3,7 @@ package fr.acinq.eclair.db.migration import fr.acinq.eclair.BlockHeight import fr.acinq.eclair.channel.{DATA_CLOSING, DATA_WAIT_FOR_FUNDING_CONFIRMED} import fr.acinq.eclair.db.migration.CompareDb._ -import fr.acinq.eclair.wire.internal.channel.ChannelCodecs.stateDataCodec +import fr.acinq.eclair.wire.internal.channel.ChannelCodecs.channelDataCodec import scodec.bits.ByteVector import java.sql.{Connection, ResultSet} @@ -16,9 +16,9 @@ object CompareChannelsDb { def hash1(rs: ResultSet): ByteVector = { val data = ByteVector(rs.getBytes("data")) - val data_modified = stateDataCodec.decode(data.bits).require.value match { - case c: DATA_WAIT_FOR_FUNDING_CONFIRMED => stateDataCodec.encode(c.copy(waitingSince = BlockHeight(0))).require.toByteVector - case c: DATA_CLOSING => stateDataCodec.encode(c.copy(waitingSince = BlockHeight(0))).require.toByteVector + val data_modified = channelDataCodec.decode(data.bits).require.value match { + case c: DATA_WAIT_FOR_FUNDING_CONFIRMED => channelDataCodec.encode(c.copy(waitingSince = BlockHeight(0))).require.toByteVector + case c: DATA_CLOSING => channelDataCodec.encode(c.copy(waitingSince = BlockHeight(0))).require.toByteVector case _ => data } bytes(rs, "channel_id") ++ @@ -33,9 +33,9 @@ object CompareChannelsDb { def hash2(rs: ResultSet): ByteVector = { val data = ByteVector(rs.getBytes("data")) - val data_modified = stateDataCodec.decode(data.bits).require.value match { - case c: DATA_WAIT_FOR_FUNDING_CONFIRMED => stateDataCodec.encode(c.copy(waitingSince = BlockHeight(0))).require.toByteVector - case c: DATA_CLOSING => stateDataCodec.encode(c.copy(waitingSince = BlockHeight(0))).require.toByteVector + val data_modified = channelDataCodec.decode(data.bits).require.value match { + case c: DATA_WAIT_FOR_FUNDING_CONFIRMED => channelDataCodec.encode(c.copy(waitingSince = BlockHeight(0))).require.toByteVector + case c: DATA_CLOSING => channelDataCodec.encode(c.copy(waitingSince = BlockHeight(0))).require.toByteVector case _ => data } hex(rs, "channel_id") ++ diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/MigrateChannelsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/MigrateChannelsDb.scala index 0758b1efe5..37055d465b 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/MigrateChannelsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/MigrateChannelsDb.scala @@ -2,7 +2,7 @@ package fr.acinq.eclair.db.migration import fr.acinq.eclair.db.jdbc.JdbcUtils.ExtendedResultSet._ import fr.acinq.eclair.db.migration.MigrateDb.{checkVersions, migrateTable} -import fr.acinq.eclair.wire.internal.channel.ChannelCodecs.stateDataCodec +import fr.acinq.eclair.wire.internal.channel.ChannelCodecs.channelDataCodec import scodec.bits.BitVector import java.sql.{Connection, PreparedStatement, ResultSet, Timestamp} @@ -19,7 +19,7 @@ object MigrateChannelsDb { def migrate(rs: ResultSet, insertStatement: PreparedStatement): Unit = { insertStatement.setString(1, rs.getByteVector32("channel_id").toHex) insertStatement.setBytes(2, rs.getBytes("data")) - val state = stateDataCodec.decode(BitVector(rs.getBytes("data"))).require.value + val state = channelDataCodec.decode(BitVector(rs.getBytes("data"))).require.value val json = serialization.writePretty(state) insertStatement.setString(3, json) insertStatement.setBoolean(4, rs.getBoolean("is_closed")) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgChannelsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgChannelsDb.scala index 0317057add..6dc38e4e13 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgChannelsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgChannelsDb.scala @@ -19,13 +19,13 @@ package fr.acinq.eclair.db.pg import com.zaxxer.hikari.util.IsolationLevel import fr.acinq.bitcoin.scalacompat.ByteVector32 import fr.acinq.eclair.CltvExpiry -import fr.acinq.eclair.channel.HasCommitments +import fr.acinq.eclair.channel.PersistentChannelData import fr.acinq.eclair.db.ChannelsDb import fr.acinq.eclair.db.DbEventHandler.ChannelEvent import fr.acinq.eclair.db.Monitoring.Metrics.withMetrics import fr.acinq.eclair.db.Monitoring.Tags.DbBackends import fr.acinq.eclair.db.pg.PgUtils.PgLock -import fr.acinq.eclair.wire.internal.channel.ChannelCodecs.stateDataCodec +import fr.acinq.eclair.wire.internal.channel.ChannelCodecs.channelDataCodec import grizzled.slf4j.Logging import scodec.bits.BitVector @@ -89,8 +89,8 @@ class PgChannelsDb(implicit ds: DataSource, lock: PgLock) extends ChannelsDb wit (rs, statement) => { // This forces a re-serialization of the channel data with latest codecs, because as of codecs v3 we don't // store local commitment signatures anymore, and we want to clean up existing data - val state = stateDataCodec.decode(BitVector(rs.getBytes("data"))).require.value - val data = stateDataCodec.encode(state).require.toByteArray + val state = channelDataCodec.decode(BitVector(rs.getBytes("data"))).require.value + val data = channelDataCodec.encode(state).require.toByteArray val json = serialization.writePretty(state) statement.setBytes(1, data) statement.setString(2, json) @@ -140,7 +140,7 @@ class PgChannelsDb(implicit ds: DataSource, lock: PgLock) extends ChannelsDb wit table, s"UPDATE $table SET json=?::JSONB WHERE channel_id=?", (rs, statement) => { - val state = stateDataCodec.decode(BitVector(rs.getBytes("data"))).require.value + val state = channelDataCodec.decode(BitVector(rs.getBytes("data"))).require.value val json = serialization.writePretty(state) statement.setString(1, json) statement.setString(2, state.channelId.toHex) @@ -148,9 +148,9 @@ class PgChannelsDb(implicit ds: DataSource, lock: PgLock) extends ChannelsDb wit )(logger) } - override def addOrUpdateChannel(state: HasCommitments): Unit = withMetrics("channels/add-or-update-channel", DbBackends.Postgres) { + override def addOrUpdateChannel(data: PersistentChannelData): Unit = withMetrics("channels/add-or-update-channel", DbBackends.Postgres) { withLock { pg => - val data = stateDataCodec.encode(state).require.toByteArray + val encoded = channelDataCodec.encode(data).require.toByteArray using(pg.prepareStatement( """ | INSERT INTO local.channels (channel_id, data, json, is_closed) @@ -158,19 +158,19 @@ class PgChannelsDb(implicit ds: DataSource, lock: PgLock) extends ChannelsDb wit | ON CONFLICT (channel_id) | DO UPDATE SET data = EXCLUDED.data, json = EXCLUDED.json ; | """.stripMargin)) { statement => - statement.setString(1, state.channelId.toHex) - statement.setBytes(2, data) - statement.setString(3, serialization.writePretty(state)) + statement.setString(1, data.channelId.toHex) + statement.setBytes(2, encoded) + statement.setString(3, serialization.writePretty(data)) statement.executeUpdate() } } } - override def getChannel(channelId: ByteVector32): Option[HasCommitments] = withMetrics("channels/get-channel", DbBackends.Postgres) { + override def getChannel(channelId: ByteVector32): Option[PersistentChannelData] = withMetrics("channels/get-channel", DbBackends.Postgres) { withLock { pg => using(pg.prepareStatement("SELECT data FROM local.channels WHERE channel_id=? AND is_closed=FALSE")) { statement => statement.setString(1, channelId.toHex) - statement.executeQuery.mapCodec(stateDataCodec).lastOption + statement.executeQuery.mapCodec(channelDataCodec).lastOption } } } @@ -219,11 +219,11 @@ class PgChannelsDb(implicit ds: DataSource, lock: PgLock) extends ChannelsDb wit } } - override def listLocalChannels(): Seq[HasCommitments] = withMetrics("channels/list-local-channels", DbBackends.Postgres) { + override def listLocalChannels(): Seq[PersistentChannelData] = withMetrics("channels/list-local-channels", DbBackends.Postgres) { withLock { pg => using(pg.createStatement) { statement => statement.executeQuery("SELECT data FROM local.channels WHERE is_closed=FALSE") - .mapCodec(stateDataCodec).toSeq + .mapCodec(channelDataCodec).toSeq } } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteChannelsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteChannelsDb.scala index ea61a527e7..3cb8df57b3 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteChannelsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteChannelsDb.scala @@ -17,13 +17,13 @@ package fr.acinq.eclair.db.sqlite import fr.acinq.bitcoin.scalacompat.ByteVector32 -import fr.acinq.eclair.{CltvExpiry, TimestampMilli} -import fr.acinq.eclair.channel.HasCommitments +import fr.acinq.eclair.channel.PersistentChannelData import fr.acinq.eclair.db.ChannelsDb import fr.acinq.eclair.db.DbEventHandler.ChannelEvent import fr.acinq.eclair.db.Monitoring.Metrics.withMetrics import fr.acinq.eclair.db.Monitoring.Tags.DbBackends -import fr.acinq.eclair.wire.internal.channel.ChannelCodecs.stateDataCodec +import fr.acinq.eclair.wire.internal.channel.ChannelCodecs.channelDataCodec +import fr.acinq.eclair.{CltvExpiry, TimestampMilli} import grizzled.slf4j.Logging import scodec.bits.BitVector @@ -70,8 +70,8 @@ class SqliteChannelsDb(val sqlite: Connection) extends ChannelsDb with Logging { (rs, statement) => { // This forces a re-serialization of the channel data with latest codecs, because as of codecs v3 we don't // store local commitment signatures anymore, and we want to clean up existing data - val state = stateDataCodec.decode(BitVector(rs.getBytes("data"))).require.value - val data = stateDataCodec.encode(state).require.toByteArray + val state = channelDataCodec.decode(BitVector(rs.getBytes("data"))).require.value + val data = channelDataCodec.encode(state).require.toByteArray statement.setBytes(1, data) statement.setBytes(2, state.channelId.toArray) } @@ -100,25 +100,25 @@ class SqliteChannelsDb(val sqlite: Connection) extends ChannelsDb with Logging { setVersion(statement, DB_NAME, CURRENT_VERSION) } - override def addOrUpdateChannel(state: HasCommitments): Unit = withMetrics("channels/add-or-update-channel", DbBackends.Sqlite) { - val data = stateDataCodec.encode(state).require.toByteArray + override def addOrUpdateChannel(data: PersistentChannelData): Unit = withMetrics("channels/add-or-update-channel", DbBackends.Sqlite) { + val encoded = channelDataCodec.encode(data).require.toByteArray using(sqlite.prepareStatement("UPDATE local_channels SET data=? WHERE channel_id=?")) { update => - update.setBytes(1, data) - update.setBytes(2, state.channelId.toArray) + update.setBytes(1, encoded) + update.setBytes(2, data.channelId.toArray) if (update.executeUpdate() == 0) { using(sqlite.prepareStatement("INSERT INTO local_channels (channel_id, data, is_closed) VALUES (?, ?, 0)")) { statement => - statement.setBytes(1, state.channelId.toArray) - statement.setBytes(2, data) + statement.setBytes(1, data.channelId.toArray) + statement.setBytes(2, encoded) statement.executeUpdate() } } } } - override def getChannel(channelId: ByteVector32): Option[HasCommitments] = withMetrics("channels/get-channel", DbBackends.Sqlite) { + override def getChannel(channelId: ByteVector32): Option[PersistentChannelData] = withMetrics("channels/get-channel", DbBackends.Sqlite) { using(sqlite.prepareStatement("SELECT data FROM local_channels WHERE channel_id=? AND is_closed=0")) { statement => statement.setBytes(1, channelId.toArray) - statement.executeQuery.mapCodec(stateDataCodec).lastOption + statement.executeQuery.mapCodec(channelDataCodec).lastOption } } @@ -162,10 +162,10 @@ class SqliteChannelsDb(val sqlite: Connection) extends ChannelsDb with Logging { } } - override def listLocalChannels(): Seq[HasCommitments] = withMetrics("channels/list-local-channels", DbBackends.Sqlite) { + override def listLocalChannels(): Seq[PersistentChannelData] = withMetrics("channels/list-local-channels", DbBackends.Sqlite) { using(sqlite.createStatement) { statement => statement.executeQuery("SELECT data FROM local_channels WHERE is_closed=0") - .mapCodec(stateDataCodec).toSeq + .mapCodec(channelDataCodec).toSeq } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala index 67136eac53..0c0ef910c7 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala @@ -455,7 +455,7 @@ object Peer { case object DISCONNECTED extends State case object CONNECTED extends State - case class Init(storedChannels: Set[HasCommitments]) + case class Init(storedChannels: Set[PersistentChannelData]) case class Connect(nodeId: PublicKey, address_opt: Option[NodeAddress], replyTo: ActorRef, isPersistent: Boolean) { def uri: Option[NodeURI] = address_opt.map(NodeURI(nodeId, _)) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/Switchboard.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/Switchboard.scala index a3ec6355bd..640f484177 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/io/Switchboard.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/Switchboard.scala @@ -128,7 +128,7 @@ class Switchboard(nodeParams: NodeParams, peerFactory: Switchboard.PeerFactory) def createPeer(remoteNodeId: PublicKey): ActorRef = peerFactory.spawn(context, remoteNodeId) - def createOrGetPeer(remoteNodeId: PublicKey, offlineChannels: Set[HasCommitments]): ActorRef = { + def createOrGetPeer(remoteNodeId: PublicKey, offlineChannels: Set[PersistentChannelData]): ActorRef = { getPeer(remoteNodeId) match { case Some(peer) => peer case None => diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/PostRestartHtlcCleaner.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/PostRestartHtlcCleaner.scala index 6588c1e3b5..f40b4593b8 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/PostRestartHtlcCleaner.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/PostRestartHtlcCleaner.scala @@ -324,7 +324,7 @@ object PostRestartHtlcCleaner { } /** @return incoming HTLCs that have been *cross-signed* (that potentially have been relayed). */ - private def getIncomingHtlcs(channels: Seq[HasCommitments], paymentsDb: IncomingPaymentsDb, privateKey: PrivateKey)(implicit log: LoggingAdapter): Seq[IncomingHtlc] = { + private def getIncomingHtlcs(channels: Seq[PersistentChannelData], paymentsDb: IncomingPaymentsDb, privateKey: PrivateKey)(implicit log: LoggingAdapter): Seq[IncomingHtlc] = { // We are interested in incoming HTLCs, that have been *cross-signed* (otherwise they wouldn't have been relayed). // They signed it first, so the HTLC will first appear in our commitment tx, and later on in their commitment when // we subsequently sign it. That's why we need to look in *their* commitment with direction=OUT. @@ -356,7 +356,7 @@ object PostRestartHtlcCleaner { .toMap /** @return pending outgoing HTLCs, grouped by their upstream origin. */ - private def getHtlcsRelayedOut(channels: Seq[HasCommitments], htlcsIn: Seq[IncomingHtlc])(implicit log: LoggingAdapter): Map[Origin, Set[(ByteVector32, Long)]] = { + private def getHtlcsRelayedOut(channels: Seq[PersistentChannelData], htlcsIn: Seq[IncomingHtlc])(implicit log: LoggingAdapter): Map[Origin, Set[(ByteVector32, Long)]] = { val htlcsOut = channels .flatMap { c => // Filter out HTLCs that will never reach the blockchain or have already been timed-out on-chain. @@ -398,7 +398,7 @@ object PostRestartHtlcCleaner { * and before it has effectively been removed. Such closed channels will automatically be removed once the channel is * restored. */ - private def listLocalChannels(channelsDb: ChannelsDb): Seq[HasCommitments] = + private def listLocalChannels(channelsDb: ChannelsDb): Seq[PersistentChannelData] = channelsDb.listLocalChannels().filterNot(c => Closing.isClosed(c, None).isDefined) /** diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/ChannelCodecs.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/ChannelCodecs.scala index 6a8fb44bfb..d0b116072b 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/ChannelCodecs.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/ChannelCodecs.scala @@ -16,7 +16,7 @@ package fr.acinq.eclair.wire.internal.channel -import fr.acinq.eclair.channel.HasCommitments +import fr.acinq.eclair.channel.PersistentChannelData import fr.acinq.eclair.wire.internal.channel.version0.ChannelCodecs0 import fr.acinq.eclair.wire.internal.channel.version1.ChannelCodecs1 import fr.acinq.eclair.wire.internal.channel.version2.ChannelCodecs2 @@ -65,10 +65,10 @@ object ChannelCodecs extends Logging { * * More info here: https://github.com/scodec/scodec/issues/122 */ - val stateDataCodec: Codec[HasCommitments] = discriminated[HasCommitments].by(byte) - .typecase(3, ChannelCodecs3.stateDataCodec) - .typecase(2, ChannelCodecs2.stateDataCodec.decodeOnly) - .typecase(1, ChannelCodecs1.stateDataCodec.decodeOnly) - .typecase(0, ChannelCodecs0.stateDataCodec.decodeOnly) + val channelDataCodec: Codec[PersistentChannelData] = discriminated[PersistentChannelData].by(byte) + .typecase(3, ChannelCodecs3.channelDataCodec) + .typecase(2, ChannelCodecs2.channelDataCodec.decodeOnly) + .typecase(1, ChannelCodecs1.channelDataCodec.decodeOnly) + .typecase(0, ChannelCodecs0.channelDataCodec.decodeOnly) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelCodecs0.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelCodecs0.scala index 20f6828666..fe817ddf86 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelCodecs0.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelCodecs0.scala @@ -454,7 +454,7 @@ private[channel] object ChannelCodecs0 { } // Order matters! - val stateDataCodec: Codec[HasCommitments] = discriminated[HasCommitments].by(uint16) + val channelDataCodec: Codec[PersistentChannelData] = discriminated[PersistentChannelData].by(uint16) .typecase(0x10, Codecs.DATA_NORMAL_Codec) .typecase(0x09, Codecs.DATA_CLOSING_Codec) .typecase(0x08, Codecs.DATA_WAIT_FOR_FUNDING_CONFIRMED_Codec) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version1/ChannelCodecs1.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version1/ChannelCodecs1.scala index 98acb81016..857dc5af38 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version1/ChannelCodecs1.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version1/ChannelCodecs1.scala @@ -286,7 +286,7 @@ private[channel] object ChannelCodecs1 { } // Order matters! - val stateDataCodec: Codec[HasCommitments] = discriminated[HasCommitments].by(uint16) + val channelDataCodec: Codec[PersistentChannelData] = discriminated[PersistentChannelData].by(uint16) .typecase(0x20, Codecs.DATA_WAIT_FOR_FUNDING_CONFIRMED_Codec) .typecase(0x21, Codecs.DATA_WAIT_FOR_FUNDING_LOCKED_Codec) .typecase(0x22, Codecs.DATA_NORMAL_Codec) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version2/ChannelCodecs2.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version2/ChannelCodecs2.scala index 99e59338e5..ff4853a7ee 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version2/ChannelCodecs2.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version2/ChannelCodecs2.scala @@ -320,7 +320,7 @@ private[channel] object ChannelCodecs2 { ("remoteChannelReestablish" | channelReestablishCodec)).as[DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT] } - val stateDataCodec: Codec[HasCommitments] = discriminated[HasCommitments].by(uint16) + val channelDataCodec: Codec[PersistentChannelData] = discriminated[PersistentChannelData].by(uint16) .typecase(0x00, Codecs.DATA_WAIT_FOR_FUNDING_CONFIRMED_Codec) .typecase(0x01, Codecs.DATA_WAIT_FOR_FUNDING_LOCKED_Codec) .typecase(0x02, Codecs.DATA_NORMAL_Codec) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3.scala index bfd51eab6a..c7686b6af1 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3.scala @@ -386,7 +386,7 @@ private[channel] object ChannelCodecs3 { } // Order matters! - val stateDataCodec: Codec[HasCommitments] = discriminated[HasCommitments].by(uint16) + val channelDataCodec: Codec[PersistentChannelData] = discriminated[PersistentChannelData].by(uint16) .typecase(0x08, Codecs.DATA_SHUTDOWN_Codec) .typecase(0x07, Codecs.DATA_NORMAL_Codec) .typecase(0x06, Codecs.DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT_Codec) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/EclairImplSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/EclairImplSpec.scala index a36188e8f8..f0af4df86f 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/EclairImplSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/EclairImplSpec.scala @@ -429,17 +429,17 @@ class EclairImplSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with I register.expectMsg(Symbol("channels")) register.reply(map) - val c1 = register.expectMsgType[Register.Forward[CMD_GETINFO]] - register.reply(RES_GETINFO(map(c1.channelId), c1.channelId, NORMAL, ChannelCodecsSpec.normal)) - register.expectMsgType[Register.Forward[CMD_GETINFO]] - register.reply(RES_FAILURE(CMD_GETINFO(ActorRef.noSender), new IllegalArgumentException("Non-standard channel"))) - val c3 = register.expectMsgType[Register.Forward[CMD_GETINFO]] - register.reply(RES_GETINFO(map(c3.channelId), c3.channelId, NORMAL, ChannelCodecsSpec.normal)) + val c1 = register.expectMsgType[Register.Forward[CMD_GET_CHANNEL_INFO]] + register.reply(RES_GET_CHANNEL_INFO(map(c1.channelId), c1.channelId, NORMAL, ChannelCodecsSpec.normal)) + register.expectMsgType[Register.Forward[CMD_GET_CHANNEL_INFO]] + register.reply(RES_FAILURE(CMD_GET_CHANNEL_INFO(ActorRef.noSender), new IllegalArgumentException("Non-standard channel"))) + val c3 = register.expectMsgType[Register.Forward[CMD_GET_CHANNEL_INFO]] + register.reply(RES_GET_CHANNEL_INFO(map(c3.channelId), c3.channelId, NORMAL, ChannelCodecsSpec.normal)) register.expectNoMessage() - assert(sender.expectMsgType[Iterable[RES_GETINFO]].toSet === Set( - RES_GETINFO(a, a1, NORMAL, ChannelCodecsSpec.normal), - RES_GETINFO(b, b1, NORMAL, ChannelCodecsSpec.normal), + assert(sender.expectMsgType[Iterable[RES_GET_CHANNEL_INFO]].toSet === Set( + RES_GET_CHANNEL_INFO(a, a1, NORMAL, ChannelCodecsSpec.normal), + RES_GET_CHANNEL_INFO(b, b1, NORMAL, ChannelCodecsSpec.normal), )) } @@ -460,15 +460,15 @@ class EclairImplSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with I register.expectMsg(Symbol("channelsTo")) register.reply(channels2Nodes) - val c1 = register.expectMsgType[Register.Forward[CMD_GETINFO]] - register.reply(RES_GETINFO(channels2Nodes(c1.channelId), c1.channelId, NORMAL, ChannelCodecsSpec.normal)) - val c2 = register.expectMsgType[Register.Forward[CMD_GETINFO]] - register.reply(RES_GETINFO(channels2Nodes(c2.channelId), c2.channelId, NORMAL, ChannelCodecsSpec.normal)) + val c1 = register.expectMsgType[Register.Forward[CMD_GET_CHANNEL_INFO]] + register.reply(RES_GET_CHANNEL_INFO(channels2Nodes(c1.channelId), c1.channelId, NORMAL, ChannelCodecsSpec.normal)) + val c2 = register.expectMsgType[Register.Forward[CMD_GET_CHANNEL_INFO]] + register.reply(RES_GET_CHANNEL_INFO(channels2Nodes(c2.channelId), c2.channelId, NORMAL, ChannelCodecsSpec.normal)) register.expectNoMessage() - assert(sender.expectMsgType[Iterable[RES_GETINFO]].toSet === Set( - RES_GETINFO(a, a1, NORMAL, ChannelCodecsSpec.normal), - RES_GETINFO(a, a2, NORMAL, ChannelCodecsSpec.normal), + assert(sender.expectMsgType[Iterable[RES_GET_CHANNEL_INFO]].toSet === Set( + RES_GET_CHANNEL_INFO(a, a1, NORMAL, ChannelCodecsSpec.normal), + RES_GET_CHANNEL_INFO(a, a2, NORMAL, ChannelCodecsSpec.normal), )) } @@ -486,11 +486,11 @@ class EclairImplSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with I eclair.channelInfo(Left(a2)).pipeTo(sender.ref) - val c1 = register.expectMsgType[Register.Forward[CMD_GETINFO]] - register.reply(RES_GETINFO(channels2Nodes(c1.channelId), c1.channelId, NORMAL, ChannelCodecsSpec.normal)) + val c1 = register.expectMsgType[Register.Forward[CMD_GET_CHANNEL_INFO]] + register.reply(RES_GET_CHANNEL_INFO(channels2Nodes(c1.channelId), c1.channelId, NORMAL, ChannelCodecsSpec.normal)) register.expectNoMessage() - sender.expectMsg(RES_GETINFO(a, a2, NORMAL, ChannelCodecsSpec.normal)) + sender.expectMsg(RES_GET_CHANNEL_INFO(a, a2, NORMAL, ChannelCodecsSpec.normal)) } test("get sent payment info") { f => diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/TestDatabases.scala b/eclair-core/src/test/scala/fr/acinq/eclair/TestDatabases.scala index e626c06f7b..7b813fee70 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/TestDatabases.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/TestDatabases.scala @@ -56,7 +56,7 @@ object TestDatabases { * @param innerDb actual database instance */ class SqliteChannelsDbWithValidation(innerDb: SqliteChannelsDb) extends SqliteChannelsDb(innerDb.sqlite) { - override def addOrUpdateChannel(state: HasCommitments): Unit = { + override def addOrUpdateChannel(data: PersistentChannelData): Unit = { def freeze1(input: Origin): Origin = input match { case h: Origin.LocalHot => Origin.LocalCold(h.id) @@ -68,7 +68,7 @@ object TestDatabases { // payment origins are always "cold" when deserialized, so to compare a "live" channel state against a state that has been // serialized and deserialized we need to turn "hot" payments into cold ones - def freeze3(input: HasCommitments): HasCommitments = input match { + def freeze3(input: PersistentChannelData): PersistentChannelData = input match { case d: DATA_WAIT_FOR_FUNDING_CONFIRMED => d.copy(commitments = freeze2(d.commitments)) case d: DATA_WAIT_FOR_FUNDING_LOCKED => d.copy(commitments = freeze2(d.commitments)) case d: DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT => d.copy(commitments = freeze2(d.commitments)) @@ -78,9 +78,9 @@ object TestDatabases { case d: DATA_SHUTDOWN => d.copy(commitments = freeze2(d.commitments)) } - super.addOrUpdateChannel(state) - val check = super.getChannel(state.channelId) - val frozen = freeze3(state) + super.addOrUpdateChannel(data) + val check = super.getChannel(data.channelId) + val frozen = freeze3(data) require(check.contains(frozen), s"serialization/deserialization check failed, $check != $frozen") } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/balance/CheckBalanceSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/balance/CheckBalanceSpec.scala index 4eee5c59bd..a7c8305da9 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/balance/CheckBalanceSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/balance/CheckBalanceSpec.scala @@ -12,7 +12,7 @@ import fr.acinq.eclair.channel.states.ChannelStateTestsBase import fr.acinq.eclair.channel.{CLOSING, CMD_SIGN, DATA_CLOSING, DATA_NORMAL} import fr.acinq.eclair.db.jdbc.JdbcUtils.ExtendedResultSet._ import fr.acinq.eclair.db.pg.PgUtils.using -import fr.acinq.eclair.wire.internal.channel.ChannelCodecs.stateDataCodec +import fr.acinq.eclair.wire.internal.channel.ChannelCodecs.channelDataCodec import fr.acinq.eclair.wire.protocol.{CommitSig, Error, RevokeAndAck} import fr.acinq.eclair.{BlockHeight, MilliSatoshiLong, TestConstants, TestKitBaseClass, ToMilliSatoshiConversion, randomBytes32} import org.scalatest.Outcome @@ -230,7 +230,7 @@ class CheckBalanceSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val sqlite = DriverManager.getConnection(s"jdbc:sqlite:$dbFile", sqliteConfig.toProperties) val channels = using(sqlite.createStatement) { statement => statement.executeQuery("SELECT data FROM local_channels WHERE is_closed=0") - .mapCodec(stateDataCodec) + .mapCodec(channelDataCodec) } val knownPreimages: Set[(ByteVector32, Long)] = using(sqlite.prepareStatement("SELECT channel_id, htlc_id FROM pending_relay")) { statement => val rs = statement.executeQuery() diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/RestoreSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/RestoreSpec.scala index a749e0ea0b..92f133cada 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/RestoreSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/RestoreSpec.scala @@ -50,7 +50,7 @@ class RestoreSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Chan val sender = TestProbe() // we start by storing the current state - val oldStateData = alice.stateData.asInstanceOf[HasCommitments] + val oldStateData = alice.stateData.asInstanceOf[PersistentChannelData] // then we add an htlc and sign it addHtlc(250000000 msat, alice, bob, alice2bob, bob2alice) sender.send(alice, CMD_SIGN()) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala index cd98aed03e..31444f0cad 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala @@ -273,7 +273,7 @@ trait ChannelStateTestsHelperMethods extends TestKitBase { s ! cmdAdd val htlc = s2r.expectMsgType[UpdateAddHtlc] s2r.forward(r) - awaitCond(r.stateData.asInstanceOf[HasCommitments].commitments.remoteChanges.proposed.contains(htlc)) + awaitCond(r.stateData.asInstanceOf[PersistentChannelData].commitments.remoteChanges.proposed.contains(htlc)) htlc } @@ -281,21 +281,21 @@ trait ChannelStateTestsHelperMethods extends TestKitBase { s ! CMD_FULFILL_HTLC(id, preimage) val fulfill = s2r.expectMsgType[UpdateFulfillHtlc] s2r.forward(r) - awaitCond(r.stateData.asInstanceOf[HasCommitments].commitments.remoteChanges.proposed.contains(fulfill)) + awaitCond(r.stateData.asInstanceOf[PersistentChannelData].commitments.remoteChanges.proposed.contains(fulfill)) } def failHtlc(id: Long, s: TestFSMRef[ChannelState, ChannelData, Channel], r: TestFSMRef[ChannelState, ChannelData, Channel], s2r: TestProbe, r2s: TestProbe): Unit = { s ! CMD_FAIL_HTLC(id, Right(TemporaryNodeFailure)) val fail = s2r.expectMsgType[UpdateFailHtlc] s2r.forward(r) - awaitCond(r.stateData.asInstanceOf[HasCommitments].commitments.remoteChanges.proposed.contains(fail)) + awaitCond(r.stateData.asInstanceOf[PersistentChannelData].commitments.remoteChanges.proposed.contains(fail)) } def crossSign(s: TestFSMRef[ChannelState, ChannelData, Channel], r: TestFSMRef[ChannelState, ChannelData, Channel], s2r: TestProbe, r2s: TestProbe): Unit = { val sender = TestProbe() - val sCommitIndex = s.stateData.asInstanceOf[HasCommitments].commitments.localCommit.index - val rCommitIndex = r.stateData.asInstanceOf[HasCommitments].commitments.localCommit.index - val rHasChanges = Commitments.localHasChanges(r.stateData.asInstanceOf[HasCommitments].commitments) + val sCommitIndex = s.stateData.asInstanceOf[PersistentChannelData].commitments.localCommit.index + val rCommitIndex = r.stateData.asInstanceOf[PersistentChannelData].commitments.localCommit.index + val rHasChanges = Commitments.localHasChanges(r.stateData.asInstanceOf[PersistentChannelData].commitments) s ! CMD_SIGN(Some(sender.ref)) sender.expectMsgType[RES_SUCCESS[CMD_SIGN]] s2r.expectMsgType[CommitSig] @@ -311,15 +311,15 @@ trait ChannelStateTestsHelperMethods extends TestKitBase { s2r.forward(r) r2s.expectMsgType[RevokeAndAck] r2s.forward(s) - awaitCond(s.stateData.asInstanceOf[HasCommitments].commitments.localCommit.index == sCommitIndex + 1) - awaitCond(s.stateData.asInstanceOf[HasCommitments].commitments.remoteCommit.index == sCommitIndex + 2) - awaitCond(r.stateData.asInstanceOf[HasCommitments].commitments.localCommit.index == rCommitIndex + 2) - awaitCond(r.stateData.asInstanceOf[HasCommitments].commitments.remoteCommit.index == rCommitIndex + 1) + awaitCond(s.stateData.asInstanceOf[PersistentChannelData].commitments.localCommit.index == sCommitIndex + 1) + awaitCond(s.stateData.asInstanceOf[PersistentChannelData].commitments.remoteCommit.index == sCommitIndex + 2) + awaitCond(r.stateData.asInstanceOf[PersistentChannelData].commitments.localCommit.index == rCommitIndex + 2) + awaitCond(r.stateData.asInstanceOf[PersistentChannelData].commitments.remoteCommit.index == rCommitIndex + 1) } else { - awaitCond(s.stateData.asInstanceOf[HasCommitments].commitments.localCommit.index == sCommitIndex + 1) - awaitCond(s.stateData.asInstanceOf[HasCommitments].commitments.remoteCommit.index == sCommitIndex + 1) - awaitCond(r.stateData.asInstanceOf[HasCommitments].commitments.localCommit.index == rCommitIndex + 1) - awaitCond(r.stateData.asInstanceOf[HasCommitments].commitments.remoteCommit.index == rCommitIndex + 1) + awaitCond(s.stateData.asInstanceOf[PersistentChannelData].commitments.localCommit.index == sCommitIndex + 1) + awaitCond(s.stateData.asInstanceOf[PersistentChannelData].commitments.remoteCommit.index == sCommitIndex + 1) + awaitCond(r.stateData.asInstanceOf[PersistentChannelData].commitments.localCommit.index == rCommitIndex + 1) + awaitCond(r.stateData.asInstanceOf[PersistentChannelData].commitments.remoteCommit.index == rCommitIndex + 1) } } @@ -335,7 +335,7 @@ trait ChannelStateTestsHelperMethods extends TestKitBase { r2s.forward(s) s2r.expectMsgType[RevokeAndAck] s2r.forward(r) - awaitCond(s.stateData.asInstanceOf[HasCommitments].commitments.localCommit.spec.commitTxFeerate == feerate) + awaitCond(s.stateData.asInstanceOf[PersistentChannelData].commitments.localCommit.spec.commitTxFeerate == feerate) } def mutualClose(s: TestFSMRef[ChannelState, ChannelData, Channel], r: TestFSMRef[ChannelState, ChannelData, Channel], s2r: TestProbe, r2s: TestProbe, s2blockchain: TestProbe, r2blockchain: TestProbe): Unit = { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala index dc2c406abb..a7f5c7b736 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala @@ -2750,7 +2750,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with bob2alice.expectMsgType[CommitSig] bob ! CurrentBlockHeight(htlc.cltvExpiry.blockHeight - Bob.nodeParams.channelConf.fulfillSafetyBeforeTimeout.toInt) - val ChannelErrorOccurred(_, _, _, _, LocalError(err), isFatal) = listener.expectMsgType[ChannelErrorOccurred] + val ChannelErrorOccurred(_, _, _, LocalError(err), isFatal) = listener.expectMsgType[ChannelErrorOccurred] assert(isFatal) assert(err.isInstanceOf[HtlcsWillTimeoutUpstream]) @@ -2783,7 +2783,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with bob2alice.expectNoMessage(500 millis) bob ! CurrentBlockHeight(htlc.cltvExpiry.blockHeight - Bob.nodeParams.channelConf.fulfillSafetyBeforeTimeout.toInt) - val ChannelErrorOccurred(_, _, _, _, LocalError(err), isFatal) = listener.expectMsgType[ChannelErrorOccurred] + val ChannelErrorOccurred(_, _, _, LocalError(err), isFatal) = listener.expectMsgType[ChannelErrorOccurred] assert(isFatal) assert(err.isInstanceOf[HtlcsWillTimeoutUpstream]) @@ -2820,7 +2820,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice2bob.forward(bob) bob ! CurrentBlockHeight(htlc.cltvExpiry.blockHeight - Bob.nodeParams.channelConf.fulfillSafetyBeforeTimeout.toInt) - val ChannelErrorOccurred(_, _, _, _, LocalError(err), isFatal) = listener.expectMsgType[ChannelErrorOccurred] + val ChannelErrorOccurred(_, _, _, LocalError(err), isFatal) = listener.expectMsgType[ChannelErrorOccurred] assert(isFatal) assert(err.isInstanceOf[HtlcsWillTimeoutUpstream]) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/OfflineStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/OfflineStateSpec.scala index 0a57a6e8ad..c467c5d6ca 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/OfflineStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/OfflineStateSpec.scala @@ -540,7 +540,7 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with bob.underlyingActor.nodeParams.db.pendingCommands.addSettlementCommand(initialState.channelId, CMD_FULFILL_HTLC(htlc.id, r, commit = true)) bob ! CurrentBlockHeight(htlc.cltvExpiry.blockHeight - bob.underlyingActor.nodeParams.channelConf.fulfillSafetyBeforeTimeout.toInt) - val ChannelErrorOccurred(_, _, _, _, LocalError(err), isFatal) = listener.expectMsgType[ChannelErrorOccurred] + val ChannelErrorOccurred(_, _, _, LocalError(err), isFatal) = listener.expectMsgType[ChannelErrorOccurred] assert(isFatal) assert(err.isInstanceOf[HtlcsWillTimeoutUpstream]) @@ -787,12 +787,12 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice ! INPUT_RECONNECTED(alice2bob.ref, aliceInit, bobInit) bob ! INPUT_RECONNECTED(bob2alice.ref, bobInit, aliceInit) - val aliceCommitments = alice.stateData.asInstanceOf[HasCommitments].commitments + val aliceCommitments = alice.stateData.asInstanceOf[PersistentChannelData].commitments val aliceCurrentPerCommitmentPoint = TestConstants.Alice.channelKeyManager.commitmentPoint( TestConstants.Alice.channelKeyManager.keyPath(aliceCommitments.localParams, aliceCommitments.channelConfig), aliceCommitments.localCommit.index) - val bobCommitments = bob.stateData.asInstanceOf[HasCommitments].commitments + val bobCommitments = bob.stateData.asInstanceOf[PersistentChannelData].commitments val bobCurrentPerCommitmentPoint = TestConstants.Bob.channelKeyManager.commitmentPoint( TestConstants.Bob.channelKeyManager.keyPath(bobCommitments.localParams, bobCommitments.channelConfig), bobCommitments.localCommit.index) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/g/NegotiatingStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/g/NegotiatingStateSpec.scala index f7231d81d5..075e3274c5 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/g/NegotiatingStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/g/NegotiatingStateSpec.scala @@ -62,11 +62,11 @@ class NegotiatingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike val bobShutdown = bob2alice.expectMsgType[Shutdown] bob2alice.forward(alice, bobShutdown) awaitCond(alice.stateName == NEGOTIATING) - if (alice.stateData.asInstanceOf[HasCommitments].commitments.channelFeatures.hasFeature(Features.UpfrontShutdownScript)) { + if (alice.stateData.asInstanceOf[PersistentChannelData].commitments.channelFeatures.hasFeature(Features.UpfrontShutdownScript)) { assert(aliceShutdown.scriptPubKey == alice.stateData.asInstanceOf[DATA_NEGOTIATING].commitments.localParams.defaultFinalScriptPubKey) } awaitCond(bob.stateName == NEGOTIATING) - if (bob.stateData.asInstanceOf[HasCommitments].commitments.channelFeatures.hasFeature(Features.UpfrontShutdownScript)) { + if (bob.stateData.asInstanceOf[PersistentChannelData].commitments.channelFeatures.hasFeature(Features.UpfrontShutdownScript)) { assert(bobShutdown.scriptPubKey == bob.stateData.asInstanceOf[DATA_NEGOTIATING].commitments.localParams.defaultFinalScriptPubKey) } } @@ -81,11 +81,11 @@ class NegotiatingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike val aliceShutdown = alice2bob.expectMsgType[Shutdown] alice2bob.forward(bob, aliceShutdown) awaitCond(alice.stateName == NEGOTIATING) - if (alice.stateData.asInstanceOf[HasCommitments].commitments.channelFeatures.hasFeature(Features.UpfrontShutdownScript)) { + if (alice.stateData.asInstanceOf[PersistentChannelData].commitments.channelFeatures.hasFeature(Features.UpfrontShutdownScript)) { assert(aliceShutdown.scriptPubKey == alice.stateData.asInstanceOf[DATA_NEGOTIATING].commitments.localParams.defaultFinalScriptPubKey) } awaitCond(bob.stateName == NEGOTIATING) - if (bob.stateData.asInstanceOf[HasCommitments].commitments.channelFeatures.hasFeature(Features.UpfrontShutdownScript)) { + if (bob.stateData.asInstanceOf[PersistentChannelData].commitments.channelFeatures.hasFeature(Features.UpfrontShutdownScript)) { assert(bobShutdown.scriptPubKey == bob.stateData.asInstanceOf[DATA_NEGOTIATING].commitments.localParams.defaultFinalScriptPubKey) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala index 1ae8baa91b..933cd79311 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala @@ -1613,7 +1613,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with import f._ mutualClose(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain) val initialState = alice.stateData.asInstanceOf[DATA_CLOSING] - val bobCommitments = bob.stateData.asInstanceOf[HasCommitments].commitments + val bobCommitments = bob.stateData.asInstanceOf[PersistentChannelData].commitments val bobCurrentPerCommitmentPoint = TestConstants.Bob.channelKeyManager.commitmentPoint( TestConstants.Bob.channelKeyManager.keyPath(bobCommitments.localParams, bobCommitments.channelConfig), bobCommitments.localCommit.index) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/AuditDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/AuditDbSpec.scala index 8261aa986b..cd57224c02 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/AuditDbSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/AuditDbSpec.scala @@ -77,8 +77,8 @@ class AuditDbSpec extends AnyFunSuite { val pp6 = PaymentSent.PartialPayment(UUID.randomUUID(), 42000 msat, 1000 msat, randomBytes32(), None, timestamp = TimestampMilli.now() + 10.minutes) val e6 = PaymentSent(UUID.randomUUID(), randomBytes32(), randomBytes32(), 42000 msat, randomKey().publicKey, pp6 :: Nil) val e7 = ChannelEvent(randomBytes32(), randomKey().publicKey, 456123000 sat, isFunder = true, isPrivate = false, ChannelEvent.EventType.Closed(MutualClose(null))) - val e8 = ChannelErrorOccurred(null, randomBytes32(), randomKey().publicKey, null, LocalError(new RuntimeException("oops")), isFatal = true) - val e9 = ChannelErrorOccurred(null, randomBytes32(), randomKey().publicKey, null, RemoteError(Error(randomBytes32(), "remote oops")), isFatal = true) + val e8 = ChannelErrorOccurred(null, randomBytes32(), randomKey().publicKey, LocalError(new RuntimeException("oops")), isFatal = true) + val e9 = ChannelErrorOccurred(null, randomBytes32(), randomKey().publicKey, RemoteError(Error(randomBytes32(), "remote oops")), isFatal = true) val e10 = TrampolinePaymentRelayed(randomBytes32(), Seq(PaymentRelayed.Part(20000 msat, randomBytes32()), PaymentRelayed.Part(22000 msat, randomBytes32())), Seq(PaymentRelayed.Part(10000 msat, randomBytes32()), PaymentRelayed.Part(12000 msat, randomBytes32()), PaymentRelayed.Part(15000 msat, randomBytes32())), randomKey().publicKey, 30000 msat) val multiPartPaymentHash = randomBytes32() val now = TimestampMilli.now() @@ -208,8 +208,8 @@ class AuditDbSpec extends AnyFunSuite { val pp1 = PaymentSent.PartialPayment(UUID.randomUUID(), 42001 msat, 1001 msat, randomBytes32(), None) val pp2 = PaymentSent.PartialPayment(UUID.randomUUID(), 42002 msat, 1002 msat, randomBytes32(), None) val ps1 = PaymentSent(UUID.randomUUID(), randomBytes32(), randomBytes32(), 84003 msat, PrivateKey(ByteVector32.One).publicKey, pp1 :: pp2 :: Nil) - val e1 = ChannelErrorOccurred(null, randomBytes32(), randomKey().publicKey, null, LocalError(new RuntimeException("oops")), isFatal = true) - val e2 = ChannelErrorOccurred(null, randomBytes32(), randomKey().publicKey, null, RemoteError(Error(randomBytes32(), "remote oops")), isFatal = true) + val e1 = ChannelErrorOccurred(null, randomBytes32(), randomKey().publicKey, LocalError(new RuntimeException("oops")), isFatal = true) + val e2 = ChannelErrorOccurred(null, randomBytes32(), randomKey().publicKey, RemoteError(Error(randomBytes32(), "remote oops")), isFatal = true) migrationCheck( dbs = dbs, @@ -270,8 +270,8 @@ class AuditDbSpec extends AnyFunSuite { test("migrate sqlite audit database v2 -> current") { val dbs = TestSqliteDatabases() - val e1 = ChannelErrorOccurred(null, randomBytes32(), randomKey().publicKey, null, LocalError(new RuntimeException("oops")), isFatal = true) - val e2 = ChannelErrorOccurred(null, randomBytes32(), randomKey().publicKey, null, RemoteError(Error(randomBytes32(), "remote oops")), isFatal = true) + val e1 = ChannelErrorOccurred(null, randomBytes32(), randomKey().publicKey, LocalError(new RuntimeException("oops")), isFatal = true) + val e2 = ChannelErrorOccurred(null, randomBytes32(), randomKey().publicKey, RemoteError(Error(randomBytes32(), "remote oops")), isFatal = true) migrationCheck( dbs = dbs, diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/ChannelsDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/ChannelsDbSpec.scala index d8f519eb3a..871adad0d6 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/ChannelsDbSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/ChannelsDbSpec.scala @@ -26,7 +26,7 @@ import fr.acinq.eclair.db.pg.PgUtils.{getVersion, setVersion} import fr.acinq.eclair.db.pg.{PgChannelsDb, PgUtils} import fr.acinq.eclair.db.sqlite.SqliteChannelsDb import fr.acinq.eclair.db.sqlite.SqliteUtils.ExtendedResultSet._ -import fr.acinq.eclair.wire.internal.channel.ChannelCodecs.stateDataCodec +import fr.acinq.eclair.wire.internal.channel.ChannelCodecs.channelDataCodec import fr.acinq.eclair.wire.internal.channel.ChannelCodecsSpec import fr.acinq.eclair.{CltvExpiry, ShortChannelId, TestDatabases, randomBytes32} import org.scalatest.funsuite.AnyFunSuite @@ -362,7 +362,7 @@ object ChannelsDbSpec { val testCases: Seq[TestCase] = for (_ <- 0 until 10) yield { val channelId = randomBytes32() - val data = stateDataCodec.encode(ChannelCodecsSpec.normal.modify(_.commitments.channelId).setTo(channelId)).require.bytes + val data = channelDataCodec.encode(ChannelCodecsSpec.normal.modify(_.commitments.channelId).setTo(channelId)).require.bytes TestCase( channelId = channelId, data = data, diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/ChannelIntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/ChannelIntegrationSpec.scala index 33d6641be4..c04f621937 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/ChannelIntegrationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/ChannelIntegrationSpec.scala @@ -152,12 +152,12 @@ abstract class ChannelIntegrationSpec extends IntegrationSpec { // F gets the htlc val htlc = htlcReceiver.expectMsgType[IncomingPaymentPacket.FinalPacket](max = 60 seconds).add // now that we have the channel id, we retrieve channels default final addresses - sender.send(nodes("C").register, Register.Forward(sender.ref, htlc.channelId, CMD_GETSTATEDATA(ActorRef.noSender))) - val dataC = sender.expectMsgType[RES_GETSTATEDATA[DATA_NORMAL]].data + sender.send(nodes("C").register, Register.Forward(sender.ref, htlc.channelId, CMD_GET_CHANNEL_DATA(ActorRef.noSender))) + val dataC = sender.expectMsgType[RES_GET_CHANNEL_DATA[DATA_NORMAL]].data assert(dataC.commitments.commitmentFormat === commitmentFormat) val finalAddressC = scriptPubKeyToAddress(dataC.commitments.localParams.defaultFinalScriptPubKey) - sender.send(nodes("F").register, Register.Forward(sender.ref, htlc.channelId, CMD_GETSTATEDATA(ActorRef.noSender))) - val dataF = sender.expectMsgType[RES_GETSTATEDATA[DATA_NORMAL]].data + sender.send(nodes("F").register, Register.Forward(sender.ref, htlc.channelId, CMD_GET_CHANNEL_DATA(ActorRef.noSender))) + val dataF = sender.expectMsgType[RES_GET_CHANNEL_DATA[DATA_NORMAL]].data assert(dataF.commitments.commitmentFormat === commitmentFormat) val finalAddressF = scriptPubKeyToAddress(dataF.commitments.localParams.defaultFinalScriptPubKey) ForceCloseFixture(sender, paymentSender, stateListenerC, stateListenerF, paymentId, htlc, preimage, minerAddress, finalAddressC, finalAddressF) @@ -251,8 +251,8 @@ abstract class ChannelIntegrationSpec extends IntegrationSpec { generateBlocks((htlc.cltvExpiry.blockHeight - getBlockHeight()).toInt, Some(minerAddress)) awaitCond(stateListenerC.expectMsgType[ChannelStateChanged](max = 60 seconds).currentState == CLOSING, max = 60 seconds) awaitCond(stateListenerF.expectMsgType[ChannelStateChanged](max = 60 seconds).currentState == CLOSING, max = 60 seconds) - sender.send(nodes("C").register, Register.Forward(sender.ref, htlc.channelId, CMD_GETSTATEDATA(ActorRef.noSender))) - val Some(localCommit) = sender.expectMsgType[RES_GETSTATEDATA[DATA_CLOSING]].data.localCommitPublished + sender.send(nodes("C").register, Register.Forward(sender.ref, htlc.channelId, CMD_GET_CHANNEL_DATA(ActorRef.noSender))) + val Some(localCommit) = sender.expectMsgType[RES_GET_CHANNEL_DATA[DATA_CLOSING]].data.localCommitPublished // we wait until the commit tx has been broadcast val bitcoinClient = new BitcoinCoreClient(bitcoinrpcclient) waitForTxBroadcastOrConfirmed(localCommit.commitTx.txid, bitcoinClient, sender) @@ -299,14 +299,14 @@ abstract class ChannelIntegrationSpec extends IntegrationSpec { sender.expectMsgType[RES_SUCCESS[CMD_FORCECLOSE]] // we wait for C to detect the unilateral close awaitCond({ - sender.send(nodes("C").register, Register.Forward(sender.ref, htlc.channelId, CMD_GETSTATEDATA(ActorRef.noSender))) - sender.expectMsgType[RES_GETSTATEDATA[ChannelData]].data match { + sender.send(nodes("C").register, Register.Forward(sender.ref, htlc.channelId, CMD_GET_CHANNEL_DATA(ActorRef.noSender))) + sender.expectMsgType[RES_GET_CHANNEL_DATA[ChannelData]].data match { case d: DATA_CLOSING if d.remoteCommitPublished.nonEmpty => true case _ => false } }, max = 30 seconds, interval = 1 second) - sender.send(nodes("C").register, Register.Forward(sender.ref, htlc.channelId, CMD_GETSTATEDATA(ActorRef.noSender))) - val Some(remoteCommit) = sender.expectMsgType[RES_GETSTATEDATA[DATA_CLOSING]].data.remoteCommitPublished + sender.send(nodes("C").register, Register.Forward(sender.ref, htlc.channelId, CMD_GET_CHANNEL_DATA(ActorRef.noSender))) + val Some(remoteCommit) = sender.expectMsgType[RES_GET_CHANNEL_DATA[DATA_CLOSING]].data.remoteCommitPublished // we generate enough blocks to make the htlc timeout generateBlocks((htlc.cltvExpiry.blockHeight - getBlockHeight()).toInt, Some(minerAddress)) // we wait until the claim-htlc-timeout has been broadcast @@ -422,8 +422,8 @@ abstract class ChannelIntegrationSpec extends IntegrationSpec { // we then generate blocks to make htlcs timeout (nothing will happen in the channel because all of them have already been fulfilled) generateBlocks(40) // we retrieve C's default final address - sender.send(nodes("C").register, Register.Forward(sender.ref, commitmentsF.channelId, CMD_GETSTATEDATA(ActorRef.noSender))) - val finalAddressC = scriptPubKeyToAddress(sender.expectMsgType[RES_GETSTATEDATA[DATA_NORMAL]].data.commitments.localParams.defaultFinalScriptPubKey) + sender.send(nodes("C").register, Register.Forward(sender.ref, commitmentsF.channelId, CMD_GET_CHANNEL_DATA(ActorRef.noSender))) + val finalAddressC = scriptPubKeyToAddress(sender.expectMsgType[RES_GET_CHANNEL_DATA[DATA_NORMAL]].data.commitments.localParams.defaultFinalScriptPubKey) // we prepare the revoked transactions F will publish val keyManagerF = nodes("F").nodeParams.channelKeyManager val channelKeyPathF = keyManagerF.keyPath(commitmentsF.localParams, commitmentsF.channelConfig) @@ -491,32 +491,32 @@ class StandardChannelIntegrationSpec extends ChannelIntegrationSpec { sender.send(fundee.register, Symbol("channels")) val Some((_, fundeeChannel)) = sender.expectMsgType[Map[ByteVector32, ActorRef]].find(_._1 == tempChannelId) - sender.send(fundeeChannel, CMD_GETSTATEDATA(ActorRef.noSender)) - val channelId = sender.expectMsgType[RES_GETSTATEDATA[HasCommitments]].data.channelId + sender.send(fundeeChannel, CMD_GET_CHANNEL_DATA(ActorRef.noSender)) + val channelId = sender.expectMsgType[RES_GET_CHANNEL_DATA[PersistentChannelData]].data.channelId awaitCond({ - funder.register ! Register.Forward(sender.ref, channelId, CMD_GETSTATE(ActorRef.noSender)) - sender.expectMsgType[RES_GETSTATE[_]].state == WAIT_FOR_FUNDING_LOCKED + funder.register ! Register.Forward(sender.ref, channelId, CMD_GET_CHANNEL_STATE(ActorRef.noSender)) + sender.expectMsgType[RES_GET_CHANNEL_STATE].state == WAIT_FOR_FUNDING_LOCKED }) generateBlocks(6) // after 8 blocks the fundee is still waiting for more confirmations - fundee.register ! Register.Forward(sender.ref, channelId, CMD_GETSTATE(ActorRef.noSender)) - assert(sender.expectMsgType[RES_GETSTATE[_]].state == WAIT_FOR_FUNDING_CONFIRMED) + fundee.register ! Register.Forward(sender.ref, channelId, CMD_GET_CHANNEL_STATE(ActorRef.noSender)) + assert(sender.expectMsgType[RES_GET_CHANNEL_STATE].state == WAIT_FOR_FUNDING_CONFIRMED) // after 8 blocks the funder is still waiting for funding_locked from the fundee - funder.register ! Register.Forward(sender.ref, channelId, CMD_GETSTATE(ActorRef.noSender)) - assert(sender.expectMsgType[RES_GETSTATE[_]].state == WAIT_FOR_FUNDING_LOCKED) + funder.register ! Register.Forward(sender.ref, channelId, CMD_GET_CHANNEL_STATE(ActorRef.noSender)) + assert(sender.expectMsgType[RES_GET_CHANNEL_STATE].state == WAIT_FOR_FUNDING_LOCKED) // simulate a disconnection sender.send(funder.switchboard, Peer.Disconnect(fundee.nodeParams.nodeId)) assert(sender.expectMsgType[String] == "disconnecting") awaitCond({ - fundee.register ! Register.Forward(sender.ref, channelId, CMD_GETSTATE(ActorRef.noSender)) - val fundeeState = sender.expectMsgType[RES_GETSTATE[_]].state - funder.register ! Register.Forward(sender.ref, channelId, CMD_GETSTATE(ActorRef.noSender)) - val funderState = sender.expectMsgType[RES_GETSTATE[_]].state + fundee.register ! Register.Forward(sender.ref, channelId, CMD_GET_CHANNEL_STATE(ActorRef.noSender)) + val fundeeState = sender.expectMsgType[RES_GET_CHANNEL_STATE].state + funder.register ! Register.Forward(sender.ref, channelId, CMD_GET_CHANNEL_STATE(ActorRef.noSender)) + val funderState = sender.expectMsgType[RES_GET_CHANNEL_STATE].state fundeeState == OFFLINE && funderState == OFFLINE }) @@ -531,10 +531,10 @@ class StandardChannelIntegrationSpec extends ChannelIntegrationSpec { )) sender.expectMsgType[PeerConnection.ConnectionResult.HasConnection](30 seconds) - fundee.register ! Register.Forward(sender.ref, channelId, CMD_GETSTATE(ActorRef.noSender)) - val fundeeState = sender.expectMsgType[RES_GETSTATE[ChannelState]].state - funder.register ! Register.Forward(sender.ref, channelId, CMD_GETSTATE(ActorRef.noSender)) - val funderState = sender.expectMsgType[RES_GETSTATE[ChannelState]].state + fundee.register ! Register.Forward(sender.ref, channelId, CMD_GET_CHANNEL_STATE(ActorRef.noSender)) + val fundeeState = sender.expectMsgType[RES_GET_CHANNEL_STATE].state + funder.register ! Register.Forward(sender.ref, channelId, CMD_GET_CHANNEL_STATE(ActorRef.noSender)) + val funderState = sender.expectMsgType[RES_GET_CHANNEL_STATE].state fundeeState == WAIT_FOR_FUNDING_CONFIRMED && funderState == WAIT_FOR_FUNDING_LOCKED }, max = 30 seconds, interval = 10 seconds) @@ -542,10 +542,10 @@ class StandardChannelIntegrationSpec extends ChannelIntegrationSpec { generateBlocks(5) awaitCond({ - fundee.register ! Register.Forward(sender.ref, channelId, CMD_GETSTATE(ActorRef.noSender)) - val fundeeState = sender.expectMsgType[RES_GETSTATE[ChannelState]].state - funder.register ! Register.Forward(sender.ref, channelId, CMD_GETSTATE(ActorRef.noSender)) - val funderState = sender.expectMsgType[RES_GETSTATE[ChannelState]].state + fundee.register ! Register.Forward(sender.ref, channelId, CMD_GET_CHANNEL_STATE(ActorRef.noSender)) + val fundeeState = sender.expectMsgType[RES_GET_CHANNEL_STATE].state + funder.register ! Register.Forward(sender.ref, channelId, CMD_GET_CHANNEL_STATE(ActorRef.noSender)) + val funderState = sender.expectMsgType[RES_GET_CHANNEL_STATE].state fundeeState == NORMAL && funderState == NORMAL }) @@ -555,12 +555,12 @@ class StandardChannelIntegrationSpec extends ChannelIntegrationSpec { funder.system.eventStream.subscribe(stateListener.ref, classOf[ChannelStateChanged]) // close that wumbo channel - sender.send(funder.register, Register.Forward(sender.ref, channelId, CMD_GETSTATEDATA(ActorRef.noSender))) - val commitmentsC = sender.expectMsgType[RES_GETSTATEDATA[DATA_NORMAL]].data.commitments + sender.send(funder.register, Register.Forward(sender.ref, channelId, CMD_GET_CHANNEL_DATA(ActorRef.noSender))) + val commitmentsC = sender.expectMsgType[RES_GET_CHANNEL_DATA[DATA_NORMAL]].data.commitments val finalPubKeyScriptC = commitmentsC.localParams.defaultFinalScriptPubKey val fundingOutpoint = commitmentsC.commitInput.outPoint - sender.send(fundee.register, Register.Forward(sender.ref, channelId, CMD_GETSTATEDATA(ActorRef.noSender))) - val finalPubKeyScriptF = sender.expectMsgType[RES_GETSTATEDATA[DATA_NORMAL]].data.commitments.localParams.defaultFinalScriptPubKey + sender.send(fundee.register, Register.Forward(sender.ref, channelId, CMD_GET_CHANNEL_DATA(ActorRef.noSender))) + val finalPubKeyScriptF = sender.expectMsgType[RES_GET_CHANNEL_DATA[DATA_NORMAL]].data.commitments.localParams.defaultFinalScriptPubKey fundee.register ! Register.Forward(sender.ref, channelId, CMD_CLOSE(sender.ref, Some(finalPubKeyScriptF), None)) sender.expectMsgType[RES_SUCCESS[CMD_CLOSE]] @@ -667,8 +667,8 @@ abstract class AnchorChannelIntegrationSpec extends ChannelIntegrationSpec { // retrieve the channelId of C <--> F val Some(channelId) = sender.expectMsgType[Map[ByteVector32, PublicKey]].find(_._2 == nodes("C").nodeParams.nodeId).map(_._1) - sender.send(nodes("F").register, Register.Forward(sender.ref, channelId, CMD_GETSTATEDATA(ActorRef.noSender))) - val initialStateDataF = sender.expectMsgType[RES_GETSTATEDATA[DATA_NORMAL]].data + sender.send(nodes("F").register, Register.Forward(sender.ref, channelId, CMD_GET_CHANNEL_DATA(ActorRef.noSender))) + val initialStateDataF = sender.expectMsgType[RES_GET_CHANNEL_DATA[DATA_NORMAL]].data assert(initialStateDataF.commitments.channelType === expectedChannelType) val initialCommitmentIndex = initialStateDataF.commitments.localCommit.index @@ -692,13 +692,13 @@ abstract class AnchorChannelIntegrationSpec extends ChannelIntegrationSpec { // we make sure the htlc has been removed from F's commitment before we force-close awaitCond({ - sender.send(nodes("F").register, Register.Forward(sender.ref, channelId, CMD_GETSTATEDATA(ActorRef.noSender))) - val stateDataF = sender.expectMsgType[RES_GETSTATEDATA[DATA_NORMAL]].data + sender.send(nodes("F").register, Register.Forward(sender.ref, channelId, CMD_GET_CHANNEL_DATA(ActorRef.noSender))) + val stateDataF = sender.expectMsgType[RES_GET_CHANNEL_DATA[DATA_NORMAL]].data stateDataF.commitments.localCommit.spec.htlcs.isEmpty }, max = 20 seconds, interval = 1 second) - sender.send(nodes("F").register, Register.Forward(sender.ref, channelId, CMD_GETSTATEDATA(ActorRef.noSender))) - val stateDataF = sender.expectMsgType[RES_GETSTATEDATA[DATA_NORMAL]].data + sender.send(nodes("F").register, Register.Forward(sender.ref, channelId, CMD_GET_CHANNEL_DATA(ActorRef.noSender))) + val stateDataF = sender.expectMsgType[RES_GET_CHANNEL_DATA[DATA_NORMAL]].data val commitmentIndex = stateDataF.commitments.localCommit.index val commitTx = stateDataF.commitments.localCommit.commitTxAndRemoteSig.commitTx.tx val Some(toRemoteOutCNew) = commitTx.txOut.find(_.publicKeyScript == Script.write(toRemoteAddress)) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/PaymentIntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/PaymentIntegrationSpec.scala index a81f02ac00..246f9b4ea8 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/PaymentIntegrationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/PaymentIntegrationSpec.scala @@ -180,8 +180,8 @@ class PaymentIntegrationSpec extends IntegrationSpec { sender.send(nodes("B").router, Router.GetChannels) val shortIdBC = sender.expectMsgType[Iterable[ChannelAnnouncement]].find(c => Set(c.nodeId1, c.nodeId2) == Set(nodes("B").nodeParams.nodeId, nodes("C").nodeParams.nodeId)).get.shortChannelId // we also need the full commitment - nodes("B").register ! Register.ForwardShortId(sender.ref, shortIdBC, CMD_GETINFO(ActorRef.noSender)) - val commitmentBC = sender.expectMsgType[RES_GETINFO].data.asInstanceOf[DATA_NORMAL].commitments + nodes("B").register ! Register.ForwardShortId(sender.ref, shortIdBC, CMD_GET_CHANNEL_INFO(ActorRef.noSender)) + val commitmentBC = sender.expectMsgType[RES_GET_CHANNEL_INFO].data.asInstanceOf[DATA_NORMAL].commitments // we then forge a new channel_update for B-C... val channelUpdateBC = Announcements.makeChannelUpdate(Block.RegtestGenesisBlock.hash, nodes("B").nodeParams.privateKey, nodes("C").nodeParams.nodeId, shortIdBC, nodes("B").nodeParams.channelConf.expiryDelta + 1, nodes("C").nodeParams.channelConf.htlcMinimum, nodes("B").nodeParams.relayParams.publicChannelFees.feeBase, nodes("B").nodeParams.relayParams.publicChannelFees.feeProportionalMillionths, 500000000 msat) // ...and notify B's relayer @@ -212,8 +212,8 @@ class PaymentIntegrationSpec extends IntegrationSpec { // first let's wait 3 seconds to make sure the timestamp of the new channel_update will be strictly greater than the former sender.expectNoMessage(3 seconds) nodes("B").register ! Register.ForwardShortId(sender.ref, shortIdBC, BroadcastChannelUpdate(PeriodicRefresh)) - nodes("B").register ! Register.ForwardShortId(sender.ref, shortIdBC, CMD_GETINFO(ActorRef.noSender)) - val channelUpdateBC_new = sender.expectMsgType[RES_GETINFO].data.asInstanceOf[DATA_NORMAL].channelUpdate + nodes("B").register ! Register.ForwardShortId(sender.ref, shortIdBC, CMD_GET_CHANNEL_INFO(ActorRef.noSender)) + val channelUpdateBC_new = sender.expectMsgType[RES_GET_CHANNEL_INFO].data.asInstanceOf[DATA_NORMAL].channelUpdate logger.info(s"channelUpdateBC=$channelUpdateBC") logger.info(s"channelUpdateBC_new=$channelUpdateBC_new") assert(channelUpdateBC_new.timestamp > channelUpdateBC.timestamp) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/interop/rustytests/SynchronizationPipe.scala b/eclair-core/src/test/scala/fr/acinq/eclair/interop/rustytests/SynchronizationPipe.scala index 17ea91aefa..4fba26eb41 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/interop/rustytests/SynchronizationPipe.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/interop/rustytests/SynchronizationPipe.scala @@ -81,7 +81,7 @@ class SynchronizationPipe(latch: CountDownLatch) extends Actor with ActorLogging fout.newLine() exec(rest, a, b) case dump(x) :: rest => - resolve(x) ! CMD_GETSTATEDATA(ActorRef.noSender) + resolve(x) ! CMD_GET_CHANNEL_DATA(ActorRef.noSender) context.become(wait(a, b, script)) case "" :: rest => exec(rest, a, b) @@ -126,7 +126,7 @@ class SynchronizationPipe(latch: CountDownLatch) extends Actor with ActorLogging a forward msg unstashAll() exec(script.drop(1), a, b) - case RES_GETSTATEDATA(d: DATA_NORMAL) if script.head.endsWith(":dump") => + case RES_GET_CHANNEL_DATA(d: DATA_NORMAL) if script.head.endsWith(":dump") => def rtrim(s: String) = s.replaceAll("\\s+$", "") import d.commitments._ val l = List( diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala index 1fa21cf749..8f998f0a34 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala @@ -86,7 +86,7 @@ class PeerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Paralle withFixture(test.toNoArgTest(FixtureParam(aliceParams, remoteNodeId, peer, peerConnection, channel, switchboard))) } - def connect(remoteNodeId: PublicKey, peer: TestFSMRef[Peer.State, Peer.Data, Peer], peerConnection: TestProbe, switchboard: TestProbe, channels: Set[HasCommitments] = Set.empty, remoteInit: protocol.Init = protocol.Init(Bob.nodeParams.features.initFeatures())): Unit = { + def connect(remoteNodeId: PublicKey, peer: TestFSMRef[Peer.State, Peer.Data, Peer], peerConnection: TestProbe, switchboard: TestProbe, channels: Set[PersistentChannelData] = Set.empty, remoteInit: protocol.Init = protocol.Init(Bob.nodeParams.features.initFeatures())): Unit = { // let's simulate a connection switchboard.send(peer, Peer.Init(channels)) val localInit = protocol.Init(peer.underlyingActor.nodeParams.features.initFeatures()) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/json/JsonSerializersSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/json/JsonSerializersSpec.scala index c88ffd2a43..6cdb9554cf 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/json/JsonSerializersSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/json/JsonSerializersSpec.scala @@ -276,11 +276,11 @@ class JsonSerializersSpec extends AnyFunSuite with Matchers { val dataNegotiating = hex"0100240000000703af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d0000928544434bbbda1da0790cf138ef4b3881f5cec34b933ab77ffd57a7e12992bf780000001000000000000044c0000000008f0d1800000000000002710000000000000000000900064ff1600140cbd801be794f9854b38981ee859e3a000ad104c0000186b0200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000229a82039dc0e0b1d25905e44fdf6f8e89755a5e219685840d0bc1d28d3308f9628a358500000000000003e8ffffffffffffffff0000000000004e2000000000000003e80090001e0234ea57c6a0d7308e0479811f0315df88650da7a8af626fb6ceb9a6b3e1bf068e032f5bf3637d4efa39b50a7bad5e3f6d32663a1cc207a166155f00f8b9a9f621cc026b8899cafac94bcfee408aecfec60e86a30082b0ea587d8a7ab6b2b309fc66110210996c2725e4129dad191f6c6d9ba8c35ab58df4d340051d7025d366423aa15703bf59f021a7431277a31a53b2101bd3ff5730adefeae596020e58c9099728b7f000000003229a820000000000000000000000000009c4000000002faf0800000000000bebc2002424d4fd09fbcbde0363fda5d43c8bc68cea396aa1edd3d48d118ec27bb30fd1d5000000002b40420f0000000000220020552fd9e112e447f57c95b896d384461ee713c8108894ced2ea1272e0d354aab74752210234ea57c6a0d7308e0479811f0315df88650da7a8af626fb6ceb9a6b3e1bf068e2103d45fea036aa6817a71387c3976790beeb4705739d4cf0271007043854dde877552aefd01bc0200000000010124d4fd09fbcbde0363fda5d43c8bc68cea396aa1edd3d48d118ec27bb30fd1d50000000000c089a180044a010000000000002200207ba66732d8b8e8863cbdcad193c4602b54df24f9af202a977e3a6315e85072704a01000000000000220020f25aee697436b14ebdc0bdab0b3a0c339c98e49cd6a8b84fbfb84c7c5557aad1400d030000000000220020a659aeaecad3be965ed5a498adac881967d2d3ed2a773019da1b26337c7d0d1372270c000000000022002095c2df1035ebb714a728a166d88332e1569b3da5c035037df1bc07e227b7e1ec040047304402205837856dc4bc8f4a679015460580c38a29899986f6cca09afe3525097dcef36702201b2abd6fdbfa890e247cbe6a096bb01f3d47901a552f7f5e5c6ecb6c80a07d1701483045022100abfa35999608ea0c993418f7d432c7f9710b993f4eb45e17fbb95e10c6899e190220542593d102382a92a11eac65bef87a75742ee4447abebb38fc47c4d27d74f9a2014752210234ea57c6a0d7308e0479811f0315df88650da7a8af626fb6ceb9a6b3e1bf068e2103d45fea036aa6817a71387c3976790beeb4705739d4cf0271007043854dde877552ae68075c20000000000000000000000000000009c4000000000bebc200000000002faf080054b02aad4844030f2e1c3c61f95b34df8d14c0fdbddbe5d56090667016ca8979033d0000a1e29b94b3517a04106dc7bb74f4f091a2ee419d889ecf8434bcfdf127000000000000000000000000000000000000000000000000000000000000ff03228da9a93e02211af77afa576b94f57d747099099f5352298d8b3c4dbc7215c62424d4fd09fbcbde0363fda5d43c8bc68cea396aa1edd3d48d118ec27bb30fd1d5000000002b40420f0000000000220020552fd9e112e447f57c95b896d384461ee713c8108894ced2ea1272e0d354aab74752210234ea57c6a0d7308e0479811f0315df88650da7a8af626fb6ceb9a6b3e1bf068e2103d45fea036aa6817a71387c3976790beeb4705739d4cf0271007043854dde877552ae00000024d4fd09fbcbde0363fda5d43c8bc68cea396aa1edd3d48d118ec27bb30fd1d53824d4fd09fbcbde0363fda5d43c8bc68cea396aa1edd3d48d118ec27bb30fd1d5001600140cbd801be794f9854b38981ee859e3a000ad104c3824d4fd09fbcbde0363fda5d43c8bc68cea396aa1edd3d48d118ec27bb30fd1d50016001458043e6e40996eb9d3c77752a81398115ec43d5900010002db71020000000124d4fd09fbcbde0363fda5d43c8bc68cea396aa1edd3d48d118ec27bb30fd1d50000000000ffffffff02400d03000000000016001458043e6e40996eb9d3c77752a81398115ec43d59ac1a0c00000000001600140cbd801be794f9854b38981ee859e3a000ad104c000000006824d4fd09fbcbde0363fda5d43c8bc68cea396aa1edd3d48d118ec27bb30fd1d50000000000001a54fa574848f29f143c52f2717510e2a8831afaf7752217ce3452c7e88d24eb14905191e04229fc6433aef69bc33f9f2f6eb3c841fee628f7c583b9efe5a149aa47db71020000000124d4fd09fbcbde0363fda5d43c8bc68cea396aa1edd3d48d118ec27bb30fd1d50000000000ffffffff02400d03000000000016001458043e6e40996eb9d3c77752a81398115ec43d59f81d0c00000000001600140cbd801be794f9854b38981ee859e3a000ad104c000000006824d4fd09fbcbde0363fda5d43c8bc68cea396aa1edd3d48d118ec27bb30fd1d50000000000001708fc4039310f093081478d723e70b6bb1f17aa925472ccef2f79057aafce602726039f88ed9a0eb0841c61c39a5c224516288a81a53256693588f5af97e9e76c88fffd014e0200000000010124d4fd09fbcbde0363fda5d43c8bc68cea396aa1edd3d48d118ec27bb30fd1d50000000000ffffffff02400d03000000000016001458043e6e40996eb9d3c77752a81398115ec43d5942210c00000000001600140cbd801be794f9854b38981ee859e3a000ad104c0400483045022100ced76c39d6c63a52374b17103eba46ac6d21b5f5e427d48ffcd5e505a26477b90220588f30b94c0938632bcee8cfe9bc185325b2edde88c85fb4d226eda215abf26b01473044022042741816de3767e7b54fc1cdf94b6f0072203daf76c01ba5abb120801965413b02205237d5f3978b2332aa14ce65501fa784f51ea4e97030d44536f52f188f768074014752210234ea57c6a0d7308e0479811f0315df88650da7a8af626fb6ceb9a6b3e1bf068e2103d45fea036aa6817a71387c3976790beeb4705739d4cf0271007043854dde877552ae00000000" val dataClosingLocal = hex"0100250000000103af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d0000948d8efaf118cae9f142433a624b41a9ef7e09327fa45083ebd7694e1a61c429980000001000000000000044c0000000008f0d1800000000000002710000000000000000000900064ff16001401396601573e7aef81f91a89fc4b4b56feb7a35e0000186b0200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000028a82039dc0e0b1d25905e44fdf6f8e89755a5e219685840d0bc1d28d3308f9628a358500000000000003e8ffffffffffffffff0000000000004e2000000000000003e80090001e03174f3de90105ae02d88ccc1a7ed4bf0d116f27ef6010f1ec308273c1010ae3dc02fc17b2289973a29fd374ee24671d5d9643465a11bd725bac1d640b56493bd8f702b5460ae3379bc8a92d2aa2488f3e55a73d8909695e677beddd61a94d3655490f0357a1a37c3ae33b2a94bca0b6ae541036c22feeca86860c7c5501153ccfffd585028fb43c64d53c5d891bb0af3b7ed41acf3ebb0de59599b3e7836a8401645cb6e000000003028a82000000000000000007000500fd05aa0a007eacf337b3a80941f2defd530cf824f13cd263ba2ca194e7170072ce28e8000000000000000400000000017d784077c3e8caa53f1a775a5e89b1a54488aff835593d5e2b7355179136bbdd10a86c00061b1000020b7604aeb7cb080b12de62f75a292bed3134b4a3de764bd032fb049e2c2a1d3942cc11b9b12619d42cda3c7eb63da952644a3f355bdf772d77f1fc2a0b36bf116cdacfcda3b77974536cc3f3d0f8209dba69408073cbe0f039eba220fcf3b0b96d5875bb71acdc999fd9feeab68a0b63629efe0dc91e92dcf93b3c49285c981746a68ead7a96a9c1ce21284a2796b0051e6b7cf36a264d545063c054454b87cc3f9ddad5350382585de18cdc4e65e02484695f541b8fc082c87ea4a56aabc84fd1b7c1cd40e3517ae811f9ff921fba672e0191c88a2681ef8ea1300eeb6c1d8fccdb5ab05bf80b2a5c3ed5c018752cc841305edae02149389bc5e58ea5a59a1c59755c7ab5f35e3740c9f6218d227370363a7e386b214f5b2e771ea8b94a2a5e640d8a715d57756665f4a659f4ef6488f9ceab5e22d523ee52da368701d799fcb3459a31e6fb03de91ffd9b608656b172945015da3e5000f7f6c3dc0b4e102ba5f101bab5c2ceab8021d071bf89cf9d65d2439fe9a9dea4291eccb43a431904313655391ace4b82bf5da6270eb5bc12a60a69e7cf96b75a699db5a374713558eff7101bb2d8471e0c4ad0e202fd10aa46098bd5565c1d410e695fdd799c866090f1a7aa002305388acb68c8d82d588c1a66df5fb12653686bc4cf487a3484c0c23578a428de3df57539ed11227f2648bc36261c7f3f4f13e18d238dc856b8dd8f07de9fb73663d3a0026a03d9ee9f807e2a03c9546d2537a1f30a1f06767622d8bacdf4ab3b0f245a7b4f482baa60080fce3e7e15d2e086c670d6ec11d907e3da593977c8c25d620ed40cf82dd503c25f1a5f3e8a5ddb65ae7117600794844252e1448d17849028bd5df960a5fddf56e48cf082cfbc5c29666bca6af335c06f8240912a311e6c07946eeea9ef57e46b3e48b95c82cbee3830bf2b55d5e0223b9480fe072729a8af4c6c8c2f86038cb8a2e02405ce5f834c87b241fcb9963f008c273c64bc1f7289cc3a0c5eea7ea1a6dc7bc6228a97978a2ddd7dc49303c5beb15b36c8044f656716cad0a62d1e8c381db46f007be7f8096f72d3a8f08d338d9f2bbc43c0244215df229824a9ad34b655d840b38c5061462d54358be1c184b51d3a805beeea78047f544b15cc333d591f3d8cb60cbdd78e58ed47d21739762957ed6365fea14d7ba8368371fe4333be4f5ae289ff444c9f377a072a3a4173bd2680c6d6074743908d9c4f09aa30d064c7866a7a34303fce53deba6a597a24c211d209815bbff97736ba9fc79e390b36b2343d2590c8fd6db3e54206580eb7a53b9de09a42cfaa2cffb00ea90dbd07f1d357e69ca96fbba4efefc4e07b1a1a174a9a8a5568c0ec9a9487add3890a20391eebd69a56c4dd1a7c7112766b05d29fcfdd0f60d8c3f7287ec070201b4d0200e3ebbf47a97c230a7d2f05b25bec59cfd125f6cc15529f13fc624ebfd387327119434b66e61a8446e9fd9e14f4e0a42cee83f4600b195a06f4f0e55d67239c55256db69c3eb5218f9d214e0d9ca0992dee76630b55a96136a251a84711096475028e326782112f2afd64c3795f438ea623754cb0ea5e6a0d4e1ded7ee633f1eb8bf71b791646c8953e54a07242a178c02baf8fc68e7c1d3e6982b265802f50353b7f1eb0d8f33c9aed1b18432ecd04d10f951473a0e7d7e3e88a3cfca46ad1b11c5abca59bab0c8e880519753b9f4a9ab4f7a756d58a389dc3bdb25940a71a03899125b08efd7ec85b3cf0c1577014a81bf63b2144617cf259383a41af2d50a7ebb7d45a8968e3e3edfb1f4a45368ba380d172f82e641eb16949e192a3c2a37117df8e22fb7a4e7ed425f9951b392bc59184db81899ed2270844446a29aaed5cb3839c392d51636a24b737a3100417a849104c08606b4644a785080b67a92dfe888cd987100fd05aa0a007eacf337b3a80941f2defd530cf824f13cd263ba2ca194e7170072ce28e800000000000000070000000001312d0077c3e8caa53f1a775a5e89b1a54488aff835593d5e2b7355179136bbdd10a86c00061b1100030159105c3e04b59668b1516a793628be01d018e0de5972a7e6094a8b29761bd07a1654dc17ab21c8f940806cd4ccfd904db9594a8170f310b1e2d95e0e8b0baab7f8484c2c66a91b7f4274ec9e5cc3651ef2ee3a0177281548905a3c7d03c4ddf2bf8b736a940d69f2a5e6beaea33d92ce189a96803654a52b0d3da0cbdbbe0725e64dc4a6633f33318de0f6ca50b9f2a79ac68fdfe3a6f6c01a5215dc3bdfb56a780dc17968c5afd3270c50d4f7e1f540364e184d460e8b8479719788023ed029ad1439d0a0c85570b7a3f50ff70a82bd9ff9c04d6d8154046a30a3606ce25c2fbda28c863109cfaa95b7fe4f96622c715422ed1a8e99ce5bdecadb7c39798822115215e3914409d5de1e2c95a6c375c530b27fd0263d02167eb28628b945280a668f88c607bd74e3aac0ac704dfda9e796a76e6a856aee502b41c6f3932b6a89425484d580b6abc232cb12530b38b5c20cbbdcb0357210c3ad09a3ad7a56353dbbd6087f7cbccbae1f656ad06d370e4ec5a7906c9841a1f36f24ba6eab5c70b5d66bc8aecb60d9feb4576ef33b23456332483f01fcd0e01477789448ddc20fa207f496db56203fdaf53f6d64d97153d8e8c8c89c821f526f4b376e929d7598cf368b826313eb0ac5e435097b3f4acef26cfb30e63ab70622ff1ed04f9a1c02074bdb2d302d932cffbba4f833ab95912272ec87d8c05a8c5f1b2b101fb78a75cf9d0ab6e751163f27018f08c2fae76c7a6a417cf5cae2cdbd867115d9c1f162e83c3fc4f52202d3471212c3ae6eb39f882aa322609958dfbc8ca3f3a2eb051ef1b43d452b54c2352e8c9d184cbbd46ea936161067893164a72a34f6e2fd63c94353e524c9db699d15cef86f970bcf6a92dc367152f9740e84b2c292dbb338e4f21e6a47171b5a5cec0f4026906b528ad2c115866fb9e807faa200b600637abd774148f7637789946a82ec5c8e35692470c0c0f12a6a39a0c5dddf73635f2dbbb82d9ea56d8313a25cfd879ee024ee5144817b1f96945f34632cf0a5a3dd3ebcb8b42bb6c4b1c3d2a7fc6f817c97ec6282a3981a8d9e3d7070c178f7df847fcec8013b0a0d07a4f8c13c829209172022e0d724caf5a01219575f1d5a26f50f53d72ef8b596ae2263d2c2eeb822166178d56eba9ee9a9c60adb4fe3a7be89044d34288c7bd59cbf6e88e01f971b687200fccdc68d5a21f8430a8e5654a17f05773b2254eb301a5624c4aadbd3db8641cfe866ff72424a891f5e0487a48c204870c932ed9895427ce9a3d0a183fc40b7bbea3617b321f35cbba2c2180908e9bea2e07e1e5d8d5de9846994b78a313b8828eca935f4bdf9ca861326cca20776c6a4df35eeee257899467973941e6e8c567054480a7d8077e6e61ab480562ad53ad8127874a7fa4f747ccb2601cfa6e8c43e89f678dd38e4df80adef7bf07149edd32bf0e9775f3478870da6af26709a5bebbac77018050a07f1a8ffe4539c2df9de1dfe18c9bf0aa640178b0912bd9631acebdc2f2f993039b6cfa414d5cfcd0c363a178443e954db312efcd4463ee7cfe0d8a1eabe74d47692653f996cdd9b93c9fad41c5b16aa91a7bd5b267578a537e45d2af048ee0ce09ea41c72020d61509e5e91575022e7d5956b8e680f229909642366d5a16a17f021f41ca3d13df75ac3d3c8c772c2ba042680ba81466f8d05c07e19b1cde5a4d27ba6b820a63fffc5a42234e18109f128cb1e3524d621e38855b5c11cfbe7edf25dc3cd470308971b655b3381f946531d89ed23ad24f6a3cacd022fb9e5eb1d5a188a5d0936d0dfe3c7c424dba5f8892b2adda87a171d73bb5c5b8f809812689794ee3a5a7dc7d80f9c84bdad2fb7f89b2a45dc25836a42c8757eaddabcab7f82e1e51502cdcd3c4745009c55fd70b1c8f1a748e71e2b96b312f71f21cb00fd05aa0a007eacf337b3a80941f2defd530cf824f13cd263ba2ca194e7170072ce28e80000000000000006000000000010c87c77c3e8caa53f1a775a5e89b1a54488aff835593d5e2b7355179136bbdd10a86c00061b100002a9a1e8cbe48ae54ff2f32eb81ac70966e623abd77532a8cef6c3dab174da1e2879020c2238504e525d2d3fae9adb39044e8e6d0c394ae9e6ff1f01ae21e354d0bd1a5970e77015dbb6a635157b6465c32aee891c26ff8942ccfd24b612e4df241c55fc34dc1a5a613d431b813e14d0ad78023314f93c38f2bba6a6ea6e642d59a547f97b306ae0d072944c62cdec15933ffd85dafd9cabf278d9b8121db56915075baa049ac0d47df8a32b6c54a9935fbc1284fe8af288d3dd72870c8faf77e34a20970bf44b47bda06f2b8bfc931252456760ba1fa58669a3ec443737b429095db43f5bdf8e5d7f2d2f9c7318cfe012213ad35d54751539605337143de28756a146779a41308dbf94be4c9290422cb79ba768c340bc91012488afc3b388fbc253bdb9a35b1f4e39307c89f9ec3bb58e5da660013a741c1ab6c0cb22a3c94db0bf6d207599a527f18831530179880ef00950886378abcb99d2d1502b874669fb8f0a596ce3909e314057d7d1f668ca80e23c46f86127a71a729021623038d23cba281ed4967ea654470d4fdb31b297def0d22f5897f58d28af0cd86287fa51722af2682fe259987f56a9fe74af94138bb520ed12bd86453cc44f31f00e337647997b56c8bf40e3e9233e42b4c4ffe2ccc438417c754e04c95a2e7f357eb238020a0b235b8757a40996b6dab0e09e526f891f1773153151d99f3f65e62f0db6d4d94fb18293ea2f13e8c900d575cc0800969ef1e6903e6958c02dac84a8e863bcb8226bd3c6bab1038b628661333449d801b8b739bb08e09daf59c969691306e9e09c68508c5a558708a9b138fb841906a1967ea6290198be18ea3941a72a95b1b55e38403bda57d391cfdb86db844a0d9b4762a550cb1d7010de80fd6507d1e2dc7830ccd49784f8e80a3c87d9301339572b0347db9336bb7b1ede0fd1e187ca94aba942fe2b5c230f7dd1d3d0376aa882136b112883999227e2be17e9d681fb813acb70807b47e99bac127ca67c2d9619049783f8a9089d3fea4275585533683308ab94f3f02f577fb840ffae279813439fcb30eb5874329c48160768c2ca6b5d6368775989b96a056f22f7dddfbcd1b22a7fb74bb2d1b36f3e94914b82709a9351e1be88364a17a36b2a92ef61bb0520fefb802393b9e9effd6d54741ab9c6dbf28911bc45b334d9c2a6d76d94ecc2a19a3115d0f2b5e5ddb1f29fc9db0b3a21651afc2f93d321189621d8b59ab07e8d4e348f4bdbdb25b91d19b313850afddf92300c7fcebd9b266792745a24a505c56a3f98428ec70b65b0396a46836cabe49024d382eb1c8227e2df4da21385960f3d3801fb63d73ae69a4fcb6aca7018e874e8bf44f73c5e845ce81b2c643862c0344e3f4e50f4e6cb51b53a6c90ea545bc897dd73e2d2b3250711ed2a666413ae85b22e848735e8a6306e3b80f3677c8f40f85bf2f457f7764343288530a9c58b160de657df482af5eb0f291a50071d207cf46969643c113a6b15354b0b590885fbf4a7136939463ef66e013cb794ac6ab0fa887352817827ffc420bd8d1ee0b6a04d6819eadc1d96517c2edd8fe028101cd9842e407a44a8eb6bbda3fc5c54997fa43aaf340ed8ab6f75e3b588dd906ebd9deba889711b3895bd7df494f042e68cd78d7e75c237bd22b90e49e5571e169487508dfea22bfe2c83f21ec1e91f7d9331e5ac4ba6c52c1183afb1e083f3d0c42af6cf41e82e9151b5717b934ba6e4f52757d27a7c8139c523f977cb9c7f29e4a19cf41a1288c60a9ee10f58189351a51925422276e2da9ce3a4ca6944b767601e686745e88b7902e0e110dd66189dec74e3abbc76a4045d5051a6b76c91b2b20fc2ab1ca7d5d63f25d2c83f5d8ab31034e3ca42c4af1ed6017f5b10c2f44a52d78bf858aa7c148b6f07156ee365cd463d3c00fd05aa0a007eacf337b3a80941f2defd530cf824f13cd263ba2ca194e7170072ce28e800000000000000030000000001c9c38077c3e8caa53f1a775a5e89b1a54488aff835593d5e2b7355179136bbdd10a86c00061b1000031a12d4e5b7253515c5822879f6f1738224e451910cacebbb545aa637e2a0f631a151e02f206b6397055534bd6a0b7c7990d269d153d3886fc7e51fd4e09c5357a96694911e9912135e3f9ccec1058be709a743ee44f07feebb0fc6a3078bec155a1b9f67afaac6914debcc890fd86150d790f8de1b7168d4501064dacfd16c19f61a8c13c7658df92d6e5ad79b1b251a96db66bfc498e8e4114f98b9b36bc41663c9d2d15a280fdae479c14e52e148b0f86abbe4f9ef78570e193585b5aa30e53d54352eba5ad54d238f0d756b54aca9889fb21c57b6c639a6713a4f655d5dde92391185a9d8c929bca42c3bc9ba5f65b059dcbab02e89cf2e95fe1b1fb291aa1c2aeb8b57ab24f92414fc296a37b683a1d47155acb5210ffb6a170642e0f1cf7a4704861099b23c0294f367f28bf6fb6f839ba3e958ba6954ef630f90d014b687e63decc26c395601a885583ee0f1a86de5d60069664e2b93a7e936579e934a084b3e34850a1c26dc5fe7a8479278e6000f5025552b0d43ec186cd01a4d6ae748ba9289d39046c3568982aac9593a3496d3eec561b538a9b0d9ed91a4560f504174bb70725fc1c9bc354afe11d1e8aab3389774d619fdff96ea096c8283c2555e9d3169c8dd543bc74eb9f3dd4f17105563922aa18243f7135429b7a71368b44975e07443fb6f5eae089b88dc2d729d87e303104b8a7172902cdb8500ce9cd2086f7a7f6ce683942caae21ed05596c46d8f87f98eadb034c472a3f6b9d7a83d41fa70b44beb4288973cee486adc191a9aad1d1b7cebac6225aec67eed51c347fa5c145e183454b2a514a5044808eea8a9b28b24e39b115e2faa75f04d370285684857129e14e6726bb23aa0ff9323e9687578446868705f3137289c49357ff4acb134f6f3f7dc7efe6b6a332cd2fbbef0579493c4368b70815ff13abc1e3cb08c3931d8b7c99bb8b5e89fccda77a6d51fee07754332500517b75bd26bc66c81cf9f0e8a34e01c700b86590fa1cc7682a6d20c10d325d735dabc43db87ae2700e4966d8be90e408aca19ba1327ab209d4a574095e37285880c31cd631aabafa6b6c31b6db80b989ec57141ab35a5d7e32445f153da3832b223831bcaa8d1cedf6cf9d8e2dc121fd1b8a744481690be8668d6185113853ad1e86fbfe5a1b216208d3300532b0c80bc751d805260f1e6b014fe8f0b904a54560baa1b9f9d81479024b9d722fa577d9708eeb5b84c5005766f2e2c622689e1d750dc16ddd74450fedec590cb49014d4cba6dff6d2aa2c15b71067c8c02128e587f6a09556ee2e8e6b1b6bb8380ea6e79f43fb6e427fa66f0b616b84261b18f07d3607eaf4b932f38fd7267c742c503862458a1d1f01b239e94f39bcce1864564ce2f021fd43673964e77d3616bf8405b4c770996fa48d312458b29be5c05a6941ce5b3b2ff4ff32ad784ea47a664fbc2bec22fb6faecf928539773cd5c0972d9e6a10762a66190c3b5cc9b77d3546ec02d355aeec68ca9af34b2f8c44071b6d9b608f17e24ec3ac185cd9fab9b9bfdfb7873e748796af1b87820707752798534334b0e79bf108d60ef61a2c3cbdffc663b54f2ef407ae43749d31681e3da13ab3beb1f4ac2053cc7286bd7a3192ca412d1692ddcb1fc33131c19bc312d72b1616b91110fc15a8f33b2429eb0fd07bb034405bb879e54cf2f4331c2fbafc69cca8c2ff5a762c7f139edbe123735e63490a17f309ec895077731e5f6d6a53e513f1a3c15eaf72db4605d65eaf5cdc830155f5b9bf4e96a3644dd0ae9ab1f09778fdcd763668507d1c49663dc4a64def5f24577d2b1b34113f6a2a92999bcaf649536e431b8c2c30491cb54fe84b7e087c7fdd1e9b92dfaa31651138e7602602a45d331528ebd79b450ea237b195e80a04a2261a00fdd918cd0b0116f9d00fd05aa0a007eacf337b3a80941f2defd530cf824f13cd263ba2ca194e7170072ce28e800000000000000050000000001c9c38077c3e8caa53f1a775a5e89b1a54488aff835593d5e2b7355179136bbdd10a86c00061b100003d4b11a6510316810f0248ea0530220e3255171fd5fce2eee5d0bff33d694904b77c108bd2c322ebc2c3231e1f78618ab9ba83f81fcbbc7278f4d0cf17edb4342ccf40ba133606f6597f474b0dde594a6f8af078aadcfc940d5fcccd32793a1266190f06e55aec9b95b9f2e552ee8d5e4a662a7cdddb20599b14e3105699f2618395ca9d80ca232f5a96e48e27a09b24b3c28b38b42bf63464dbfc0d233c635d1379784581f5ff2f2fa2c6dff4cf8c12454745c695ea394d0fc477f4907e82e636e347306ee70730f61aa75ddbb10cf023b28a5e1f7dfed48470bf4b3180e343ea7f578a0d8f33505aa36b3875226b668376db40b3171f5a2eef326b0a416fd27cd7c7936e71cc9822421d9e317db5fa31de68f4be412666c3ef4c699acbadf94d2bfd1f32ad1f962da04965d20d783047a415f5f6a59117422dc26c5f05a5ef0180081e9b7771277ae8520647e833c2dc80149baea35700c35aadce8c4c81ef6e3fc3ec55e3fe3b9f846710fd648668ca3ba05f6b7cf5eddc9b5a7e73f2ac4d1f53864765cf70f55128ec53692bf26656da9468ea93b7030063a2021ad57cf3ab1df019c21118f6f9cec64def026910242fc4809d2224210460d63d02f2d6475b371b97907cc3c1e72f56ce8285cc08fd4e37f439f39b63667c045f4b5dc5f982cdc8b32fa0392ce38dccfa6a79f5bcaccd2bdaae666404947fd8b6d8ad7e16ae1307d7b24cc15d712814d1311bdf2155ee28da446ddd2bd0e782c225a8a379073314ec486aa2a9dc54c4edc2e2dea278f39ee23b0d7c2f5df7a20ffe5d77541d1c42a94a17e977763f15dd889c939104b691553ec8c99d5c87a9513553a7384e3a45fac0193b77fd8b7a2c5bf291a6954c767addeca4c91a04946688491c56818bb9de51d2e2dfc2c0b2e8f3e929ba04d9b3f457935f8c797dd770cd6558b6238ba0706a500bd9aaa17565a8a8b8abb41e4717f312d9b449de7dcd5317e1f6ba71b428f5e96857fc9acaef6298d4e994746a45534715af1326c79836f0f3a7ae3aaa41c90f172ec8544d13086237420207fddbae7d349a2871a62f465a6c57bb54e623c36067f41f33282a8548e5047d3d2da4c83483811ee3559bd7479216631281aeee1152f1fb876beb57b51c9cbf002ce9e6c81129a119120e24789958bb1de20448f01692ea4b955099b808af3c4dc7ad6652a9eff075f91fd3bbf54ec7d450c8000005894542b810974b36f6a66125e047dc491aa376b70aef9fd65379c0668937a86e0c12772520a151df018c031b931b2e0c0a25419f10080fa9010ddee60b4861692ed00bef79ab5cae1a4d5baf62552a111845c3969538bed014baac2169b2c6e847b3123a7aff9761736ba9e2f5f4d0f94852c921599615f91bbbdeb230ddf834305c9c70490307c300952b8e61e5255b31cd09f2ca7cdbb4c4e76798a78c0a8709d86c7dfd5dc008af596308a36e0d28c4ba60d68151858e740ccbcc068b0acb3fb81bea92e4e979ac39057ccb19c8603658efc62f6222621cf956b377b7ff0fea770908f04600c2720b03371a62ef941159fcc015248ba326f31d1bc82e4d94c186d4f6c39e1f55600221a2cdf85b560b78dc5fb3e74069021e9062e2e7c6173716ca70dc843cf02070253d31ddb9c4b569df4fe243b78a8300788104b575d641108510bafea96a25bbea6b8f6474fa1886de9d525a4fbdb480afc4ed96482ff2aa8e64142c285b11c87ffef811b5026b3a2587a91aff83c6825c4d0634ccc7aab85e795c11491f3e0883568c23e67efcd4c26a5db3ca0b5c3bbded3c6e355738319c1a087d8da87db4da52d4c5214d272cd1631b2843cbd6b015e52eb51a8bb759d529ae6ab7b397aa8963f2074c9278fa8499de47dc9152bb8726de26f5bbf24e9ca7e329353ca2da6363b4e00002710000000000598cd44000000002faf0800240a007eacf337b3a80941f2defd530cf824f13cd263ba2ca194e7170072ce28e8000000002b40420f0000000000220020e2eb1e4df160f9e11d4d06a6075034b47e9d3e1c3d21d0feab64905d2e6ab17b47522103174f3de90105ae02d88ccc1a7ed4bf0d116f27ef6010f1ec308273c1010ae3dc2103548b338f50bdae124711cadef7a82ce832bff61826f13b031677793abb8c674c52aefd0205020000000001010a007eacf337b3a80941f2defd530cf824f13cd263ba2ca194e7170072ce28e80000000000a1c4a78006204e0000000000002200204c81bcdcc2c4f01d3c045348a155c9249ddba4d48c16ccf33c1d8b6e34614180a8610000000000002200204c81bcdcc2c4f01d3c045348a155c9249ddba4d48c16ccf33c1d8b6e3461418030750000000000002200204c81bcdcc2c4f01d3c045348a155c9249ddba4d48c16ccf33c1d8b6e3461418030750000000000002200204c81bcdcc2c4f01d3c045348a155c9249ddba4d48c16ccf33c1d8b6e34614180a437010000000000220020680a82e78074810847ca530cd74fabdbd2e34beaf0fdd2bc832ddfcd2669876c00350c0000000000160014e1fdef3169db3728018a906129e3913dd841ab0f04004730440220639ddfeb4cfd418c28418208226f5f6be762b65bb242968695ac1574d1fae8e702207bd3b3f60f12f171a37e50b7126c709a4cb87e417efdfe520707962d393bd28c01473044022058407ee97260aa8020eea7036f348ec4959fbd8a820e8628244705138803e00e02207d53e7fec4e01bbc04a0495e8c31210c0c6c2542e38d6c9ae677b78bd568cedb0147522103174f3de90105ae02d88ccc1a7ed4bf0d116f27ef6010f1ec308273c1010ae3dc2103548b338f50bdae124711cadef7a82ce832bff61826f13b031677793abb8c674c52ae340169200004000324efa7874aa335cd976b8209145d670a6dba17471f6a4f47dacaa432dd972d801c000000002b204e0000000000002200204c81bcdcc2c4f01d3c045348a155c9249ddba4d48c16ccf33c1d8b6e346141808576a9144a2fe35d15fdacd3b3cba332b5879b6acfb2e79f8763ac67210375099d4e25580f8d2c59718d7319e9d26c7119f9476ca3a623fb638843d3c8617c820120876475527c210375f5c7f3e5f1da49febb149cb9dc97098402628f84b79b586d5d516c17e6327852ae67a914950b69c651378982bec084b296e6dd23a33c41b588ac68685e0200000001efa7874aa335cd976b8209145d670a6dba17471f6a4f47dacaa432dd972d801c000000000000000000013a34000000000000220020680a82e78074810847ca530cd74fabdbd2e34beaf0fdd2bc832ddfcd2669876c111b06004093d252d2f22cecee6c2b604a1190120db574f0ba33b49ac826229b95071230bd7a4baa7a1704813804ba9b7d0e2aef3932a7d7d096862c1636736ad3296a46f5401f096356d49cb9c0d1dad9a01530aa892aa75cdcb626f42ef09b734f39d0a2650d6274377598b29f3f3eae1281a8b61d5cb9853b478cc973b8035af5d60c26a0000324efa7874aa335cd976b8209145d670a6dba17471f6a4f47dacaa432dd972d801c010000002ba8610000000000002200204c81bcdcc2c4f01d3c045348a155c9249ddba4d48c16ccf33c1d8b6e346141808576a9144a2fe35d15fdacd3b3cba332b5879b6acfb2e79f8763ac67210375099d4e25580f8d2c59718d7319e9d26c7119f9476ca3a623fb638843d3c8617c820120876475527c210375f5c7f3e5f1da49febb149cb9dc97098402628f84b79b586d5d516c17e6327852ae67a914950b69c651378982bec084b296e6dd23a33c41b588ac68685e0200000001efa7874aa335cd976b8209145d670a6dba17471f6a4f47dacaa432dd972d801c01000000000000000001c247000000000000220020680a82e78074810847ca530cd74fabdbd2e34beaf0fdd2bc832ddfcd2669876c101b060040774ca3fbbee6dc29db89dbb2bdb977afdf31727548aa6060e9e23a79090bf658474a5aa2de53dae36371ec535e13e4291c9fbd1942e7cc812ff4ca4c9d84483c4088605547607e6c8b1b24a671ed340700ad396a8bf613fa9e4646c0b11c2aeff708e4448ba70fe48903e3034ace605d8651091fcd6f80fddb57d2e2b09da6765d000324efa7874aa335cd976b8209145d670a6dba17471f6a4f47dacaa432dd972d801c020000002b30750000000000002200204c81bcdcc2c4f01d3c045348a155c9249ddba4d48c16ccf33c1d8b6e346141808576a9144a2fe35d15fdacd3b3cba332b5879b6acfb2e79f8763ac67210375099d4e25580f8d2c59718d7319e9d26c7119f9476ca3a623fb638843d3c8617c820120876475527c210375f5c7f3e5f1da49febb149cb9dc97098402628f84b79b586d5d516c17e6327852ae67a914950b69c651378982bec084b296e6dd23a33c41b588ac68685e0200000001efa7874aa335cd976b8209145d670a6dba17471f6a4f47dacaa432dd972d801c020000000000000000014a5b000000000000220020680a82e78074810847ca530cd74fabdbd2e34beaf0fdd2bc832ddfcd2669876c101b0600404ec5ef779e7b0db022d9da0986bee62eeb0372337f3974e0bfa8ecbddec7a3ed47930ce5ca6796d3cde21dbf9e0678f4d0af5d84889c24e2b960786d141b900a402c736579e487f2ceedb15546a438818e9a13d3b9f9232d4ff418855f9112795e2bce08fb52f8737268b616158eeb06e36abdc83942396fb407d3e603ed1edd7b000324efa7874aa335cd976b8209145d670a6dba17471f6a4f47dacaa432dd972d801c030000002b30750000000000002200204c81bcdcc2c4f01d3c045348a155c9249ddba4d48c16ccf33c1d8b6e346141808576a9144a2fe35d15fdacd3b3cba332b5879b6acfb2e79f8763ac67210375099d4e25580f8d2c59718d7319e9d26c7119f9476ca3a623fb638843d3c8617c820120876475527c210375f5c7f3e5f1da49febb149cb9dc97098402628f84b79b586d5d516c17e6327852ae67a914950b69c651378982bec084b296e6dd23a33c41b588ac68685e0200000001efa7874aa335cd976b8209145d670a6dba17471f6a4f47dacaa432dd972d801c030000000000000000014a5b000000000000220020680a82e78074810847ca530cd74fabdbd2e34beaf0fdd2bc832ddfcd2669876c101b060040ff0a3853c07d426ac93a8b772017a41f324558851ec69bcd65ed1cd81490e0f11c3c01574fe196df6311cf21fbb78abd7d40659479ea080c7ccb3c5fd53027df404f63e06d948b7890c8a3fb1ae568e88de8cd4ef3364c092bcdeb006c40558b082a5fc572f9f02f5d2a0870101ff7d900da184fb19b5cd410ba0779bc55533c4a00000000000000070005fffd05aa0a007eacf337b3a80941f2defd530cf824f13cd263ba2ca194e7170072ce28e800000000000000030000000001c9c38077c3e8caa53f1a775a5e89b1a54488aff835593d5e2b7355179136bbdd10a86c00061b1000031a12d4e5b7253515c5822879f6f1738224e451910cacebbb545aa637e2a0f631a151e02f206b6397055534bd6a0b7c7990d269d153d3886fc7e51fd4e09c5357a96694911e9912135e3f9ccec1058be709a743ee44f07feebb0fc6a3078bec155a1b9f67afaac6914debcc890fd86150d790f8de1b7168d4501064dacfd16c19f61a8c13c7658df92d6e5ad79b1b251a96db66bfc498e8e4114f98b9b36bc41663c9d2d15a280fdae479c14e52e148b0f86abbe4f9ef78570e193585b5aa30e53d54352eba5ad54d238f0d756b54aca9889fb21c57b6c639a6713a4f655d5dde92391185a9d8c929bca42c3bc9ba5f65b059dcbab02e89cf2e95fe1b1fb291aa1c2aeb8b57ab24f92414fc296a37b683a1d47155acb5210ffb6a170642e0f1cf7a4704861099b23c0294f367f28bf6fb6f839ba3e958ba6954ef630f90d014b687e63decc26c395601a885583ee0f1a86de5d60069664e2b93a7e936579e934a084b3e34850a1c26dc5fe7a8479278e6000f5025552b0d43ec186cd01a4d6ae748ba9289d39046c3568982aac9593a3496d3eec561b538a9b0d9ed91a4560f504174bb70725fc1c9bc354afe11d1e8aab3389774d619fdff96ea096c8283c2555e9d3169c8dd543bc74eb9f3dd4f17105563922aa18243f7135429b7a71368b44975e07443fb6f5eae089b88dc2d729d87e303104b8a7172902cdb8500ce9cd2086f7a7f6ce683942caae21ed05596c46d8f87f98eadb034c472a3f6b9d7a83d41fa70b44beb4288973cee486adc191a9aad1d1b7cebac6225aec67eed51c347fa5c145e183454b2a514a5044808eea8a9b28b24e39b115e2faa75f04d370285684857129e14e6726bb23aa0ff9323e9687578446868705f3137289c49357ff4acb134f6f3f7dc7efe6b6a332cd2fbbef0579493c4368b70815ff13abc1e3cb08c3931d8b7c99bb8b5e89fccda77a6d51fee07754332500517b75bd26bc66c81cf9f0e8a34e01c700b86590fa1cc7682a6d20c10d325d735dabc43db87ae2700e4966d8be90e408aca19ba1327ab209d4a574095e37285880c31cd631aabafa6b6c31b6db80b989ec57141ab35a5d7e32445f153da3832b223831bcaa8d1cedf6cf9d8e2dc121fd1b8a744481690be8668d6185113853ad1e86fbfe5a1b216208d3300532b0c80bc751d805260f1e6b014fe8f0b904a54560baa1b9f9d81479024b9d722fa577d9708eeb5b84c5005766f2e2c622689e1d750dc16ddd74450fedec590cb49014d4cba6dff6d2aa2c15b71067c8c02128e587f6a09556ee2e8e6b1b6bb8380ea6e79f43fb6e427fa66f0b616b84261b18f07d3607eaf4b932f38fd7267c742c503862458a1d1f01b239e94f39bcce1864564ce2f021fd43673964e77d3616bf8405b4c770996fa48d312458b29be5c05a6941ce5b3b2ff4ff32ad784ea47a664fbc2bec22fb6faecf928539773cd5c0972d9e6a10762a66190c3b5cc9b77d3546ec02d355aeec68ca9af34b2f8c44071b6d9b608f17e24ec3ac185cd9fab9b9bfdfb7873e748796af1b87820707752798534334b0e79bf108d60ef61a2c3cbdffc663b54f2ef407ae43749d31681e3da13ab3beb1f4ac2053cc7286bd7a3192ca412d1692ddcb1fc33131c19bc312d72b1616b91110fc15a8f33b2429eb0fd07bb034405bb879e54cf2f4331c2fbafc69cca8c2ff5a762c7f139edbe123735e63490a17f309ec895077731e5f6d6a53e513f1a3c15eaf72db4605d65eaf5cdc830155f5b9bf4e96a3644dd0ae9ab1f09778fdcd763668507d1c49663dc4a64def5f24577d2b1b34113f6a2a92999bcaf649536e431b8c2c30491cb54fe84b7e087c7fdd1e9b92dfaa31651138e7602602a45d331528ebd79b450ea237b195e80a04a2261a00fdd918cd0b0116f9dfffd05aa0a007eacf337b3a80941f2defd530cf824f13cd263ba2ca194e7170072ce28e800000000000000050000000001c9c38077c3e8caa53f1a775a5e89b1a54488aff835593d5e2b7355179136bbdd10a86c00061b100003d4b11a6510316810f0248ea0530220e3255171fd5fce2eee5d0bff33d694904b77c108bd2c322ebc2c3231e1f78618ab9ba83f81fcbbc7278f4d0cf17edb4342ccf40ba133606f6597f474b0dde594a6f8af078aadcfc940d5fcccd32793a1266190f06e55aec9b95b9f2e552ee8d5e4a662a7cdddb20599b14e3105699f2618395ca9d80ca232f5a96e48e27a09b24b3c28b38b42bf63464dbfc0d233c635d1379784581f5ff2f2fa2c6dff4cf8c12454745c695ea394d0fc477f4907e82e636e347306ee70730f61aa75ddbb10cf023b28a5e1f7dfed48470bf4b3180e343ea7f578a0d8f33505aa36b3875226b668376db40b3171f5a2eef326b0a416fd27cd7c7936e71cc9822421d9e317db5fa31de68f4be412666c3ef4c699acbadf94d2bfd1f32ad1f962da04965d20d783047a415f5f6a59117422dc26c5f05a5ef0180081e9b7771277ae8520647e833c2dc80149baea35700c35aadce8c4c81ef6e3fc3ec55e3fe3b9f846710fd648668ca3ba05f6b7cf5eddc9b5a7e73f2ac4d1f53864765cf70f55128ec53692bf26656da9468ea93b7030063a2021ad57cf3ab1df019c21118f6f9cec64def026910242fc4809d2224210460d63d02f2d6475b371b97907cc3c1e72f56ce8285cc08fd4e37f439f39b63667c045f4b5dc5f982cdc8b32fa0392ce38dccfa6a79f5bcaccd2bdaae666404947fd8b6d8ad7e16ae1307d7b24cc15d712814d1311bdf2155ee28da446ddd2bd0e782c225a8a379073314ec486aa2a9dc54c4edc2e2dea278f39ee23b0d7c2f5df7a20ffe5d77541d1c42a94a17e977763f15dd889c939104b691553ec8c99d5c87a9513553a7384e3a45fac0193b77fd8b7a2c5bf291a6954c767addeca4c91a04946688491c56818bb9de51d2e2dfc2c0b2e8f3e929ba04d9b3f457935f8c797dd770cd6558b6238ba0706a500bd9aaa17565a8a8b8abb41e4717f312d9b449de7dcd5317e1f6ba71b428f5e96857fc9acaef6298d4e994746a45534715af1326c79836f0f3a7ae3aaa41c90f172ec8544d13086237420207fddbae7d349a2871a62f465a6c57bb54e623c36067f41f33282a8548e5047d3d2da4c83483811ee3559bd7479216631281aeee1152f1fb876beb57b51c9cbf002ce9e6c81129a119120e24789958bb1de20448f01692ea4b955099b808af3c4dc7ad6652a9eff075f91fd3bbf54ec7d450c8000005894542b810974b36f6a66125e047dc491aa376b70aef9fd65379c0668937a86e0c12772520a151df018c031b931b2e0c0a25419f10080fa9010ddee60b4861692ed00bef79ab5cae1a4d5baf62552a111845c3969538bed014baac2169b2c6e847b3123a7aff9761736ba9e2f5f4d0f94852c921599615f91bbbdeb230ddf834305c9c70490307c300952b8e61e5255b31cd09f2ca7cdbb4c4e76798a78c0a8709d86c7dfd5dc008af596308a36e0d28c4ba60d68151858e740ccbcc068b0acb3fb81bea92e4e979ac39057ccb19c8603658efc62f6222621cf956b377b7ff0fea770908f04600c2720b03371a62ef941159fcc015248ba326f31d1bc82e4d94c186d4f6c39e1f55600221a2cdf85b560b78dc5fb3e74069021e9062e2e7c6173716ca70dc843cf02070253d31ddb9c4b569df4fe243b78a8300788104b575d641108510bafea96a25bbea6b8f6474fa1886de9d525a4fbdb480afc4ed96482ff2aa8e64142c285b11c87ffef811b5026b3a2587a91aff83c6825c4d0634ccc7aab85e795c11491f3e0883568c23e67efcd4c26a5db3ca0b5c3bbded3c6e355738319c1a087d8da87db4da52d4c5214d272cd1631b2843cbd6b015e52eb51a8bb759d529ae6ab7b397aa8963f2074c9278fa8499de47dc9152bb8726de26f5bbf24e9ca7e329353ca2da6363b4efffd05aa0a007eacf337b3a80941f2defd530cf824f13cd263ba2ca194e7170072ce28e8000000000000000400000000017d784077c3e8caa53f1a775a5e89b1a54488aff835593d5e2b7355179136bbdd10a86c00061b1000020b7604aeb7cb080b12de62f75a292bed3134b4a3de764bd032fb049e2c2a1d3942cc11b9b12619d42cda3c7eb63da952644a3f355bdf772d77f1fc2a0b36bf116cdacfcda3b77974536cc3f3d0f8209dba69408073cbe0f039eba220fcf3b0b96d5875bb71acdc999fd9feeab68a0b63629efe0dc91e92dcf93b3c49285c981746a68ead7a96a9c1ce21284a2796b0051e6b7cf36a264d545063c054454b87cc3f9ddad5350382585de18cdc4e65e02484695f541b8fc082c87ea4a56aabc84fd1b7c1cd40e3517ae811f9ff921fba672e0191c88a2681ef8ea1300eeb6c1d8fccdb5ab05bf80b2a5c3ed5c018752cc841305edae02149389bc5e58ea5a59a1c59755c7ab5f35e3740c9f6218d227370363a7e386b214f5b2e771ea8b94a2a5e640d8a715d57756665f4a659f4ef6488f9ceab5e22d523ee52da368701d799fcb3459a31e6fb03de91ffd9b608656b172945015da3e5000f7f6c3dc0b4e102ba5f101bab5c2ceab8021d071bf89cf9d65d2439fe9a9dea4291eccb43a431904313655391ace4b82bf5da6270eb5bc12a60a69e7cf96b75a699db5a374713558eff7101bb2d8471e0c4ad0e202fd10aa46098bd5565c1d410e695fdd799c866090f1a7aa002305388acb68c8d82d588c1a66df5fb12653686bc4cf487a3484c0c23578a428de3df57539ed11227f2648bc36261c7f3f4f13e18d238dc856b8dd8f07de9fb73663d3a0026a03d9ee9f807e2a03c9546d2537a1f30a1f06767622d8bacdf4ab3b0f245a7b4f482baa60080fce3e7e15d2e086c670d6ec11d907e3da593977c8c25d620ed40cf82dd503c25f1a5f3e8a5ddb65ae7117600794844252e1448d17849028bd5df960a5fddf56e48cf082cfbc5c29666bca6af335c06f8240912a311e6c07946eeea9ef57e46b3e48b95c82cbee3830bf2b55d5e0223b9480fe072729a8af4c6c8c2f86038cb8a2e02405ce5f834c87b241fcb9963f008c273c64bc1f7289cc3a0c5eea7ea1a6dc7bc6228a97978a2ddd7dc49303c5beb15b36c8044f656716cad0a62d1e8c381db46f007be7f8096f72d3a8f08d338d9f2bbc43c0244215df229824a9ad34b655d840b38c5061462d54358be1c184b51d3a805beeea78047f544b15cc333d591f3d8cb60cbdd78e58ed47d21739762957ed6365fea14d7ba8368371fe4333be4f5ae289ff444c9f377a072a3a4173bd2680c6d6074743908d9c4f09aa30d064c7866a7a34303fce53deba6a597a24c211d209815bbff97736ba9fc79e390b36b2343d2590c8fd6db3e54206580eb7a53b9de09a42cfaa2cffb00ea90dbd07f1d357e69ca96fbba4efefc4e07b1a1a174a9a8a5568c0ec9a9487add3890a20391eebd69a56c4dd1a7c7112766b05d29fcfdd0f60d8c3f7287ec070201b4d0200e3ebbf47a97c230a7d2f05b25bec59cfd125f6cc15529f13fc624ebfd387327119434b66e61a8446e9fd9e14f4e0a42cee83f4600b195a06f4f0e55d67239c55256db69c3eb5218f9d214e0d9ca0992dee76630b55a96136a251a84711096475028e326782112f2afd64c3795f438ea623754cb0ea5e6a0d4e1ded7ee633f1eb8bf71b791646c8953e54a07242a178c02baf8fc68e7c1d3e6982b265802f50353b7f1eb0d8f33c9aed1b18432ecd04d10f951473a0e7d7e3e88a3cfca46ad1b11c5abca59bab0c8e880519753b9f4a9ab4f7a756d58a389dc3bdb25940a71a03899125b08efd7ec85b3cf0c1577014a81bf63b2144617cf259383a41af2d50a7ebb7d45a8968e3e3edfb1f4a45368ba380d172f82e641eb16949e192a3c2a37117df8e22fb7a4e7ed425f9951b392bc59184db81899ed2270844446a29aaed5cb3839c392d51636a24b737a3100417a849104c08606b4644a785080b67a92dfe888cd9871fffd05aa0a007eacf337b3a80941f2defd530cf824f13cd263ba2ca194e7170072ce28e80000000000000006000000000010c87c77c3e8caa53f1a775a5e89b1a54488aff835593d5e2b7355179136bbdd10a86c00061b100002a9a1e8cbe48ae54ff2f32eb81ac70966e623abd77532a8cef6c3dab174da1e2879020c2238504e525d2d3fae9adb39044e8e6d0c394ae9e6ff1f01ae21e354d0bd1a5970e77015dbb6a635157b6465c32aee891c26ff8942ccfd24b612e4df241c55fc34dc1a5a613d431b813e14d0ad78023314f93c38f2bba6a6ea6e642d59a547f97b306ae0d072944c62cdec15933ffd85dafd9cabf278d9b8121db56915075baa049ac0d47df8a32b6c54a9935fbc1284fe8af288d3dd72870c8faf77e34a20970bf44b47bda06f2b8bfc931252456760ba1fa58669a3ec443737b429095db43f5bdf8e5d7f2d2f9c7318cfe012213ad35d54751539605337143de28756a146779a41308dbf94be4c9290422cb79ba768c340bc91012488afc3b388fbc253bdb9a35b1f4e39307c89f9ec3bb58e5da660013a741c1ab6c0cb22a3c94db0bf6d207599a527f18831530179880ef00950886378abcb99d2d1502b874669fb8f0a596ce3909e314057d7d1f668ca80e23c46f86127a71a729021623038d23cba281ed4967ea654470d4fdb31b297def0d22f5897f58d28af0cd86287fa51722af2682fe259987f56a9fe74af94138bb520ed12bd86453cc44f31f00e337647997b56c8bf40e3e9233e42b4c4ffe2ccc438417c754e04c95a2e7f357eb238020a0b235b8757a40996b6dab0e09e526f891f1773153151d99f3f65e62f0db6d4d94fb18293ea2f13e8c900d575cc0800969ef1e6903e6958c02dac84a8e863bcb8226bd3c6bab1038b628661333449d801b8b739bb08e09daf59c969691306e9e09c68508c5a558708a9b138fb841906a1967ea6290198be18ea3941a72a95b1b55e38403bda57d391cfdb86db844a0d9b4762a550cb1d7010de80fd6507d1e2dc7830ccd49784f8e80a3c87d9301339572b0347db9336bb7b1ede0fd1e187ca94aba942fe2b5c230f7dd1d3d0376aa882136b112883999227e2be17e9d681fb813acb70807b47e99bac127ca67c2d9619049783f8a9089d3fea4275585533683308ab94f3f02f577fb840ffae279813439fcb30eb5874329c48160768c2ca6b5d6368775989b96a056f22f7dddfbcd1b22a7fb74bb2d1b36f3e94914b82709a9351e1be88364a17a36b2a92ef61bb0520fefb802393b9e9effd6d54741ab9c6dbf28911bc45b334d9c2a6d76d94ecc2a19a3115d0f2b5e5ddb1f29fc9db0b3a21651afc2f93d321189621d8b59ab07e8d4e348f4bdbdb25b91d19b313850afddf92300c7fcebd9b266792745a24a505c56a3f98428ec70b65b0396a46836cabe49024d382eb1c8227e2df4da21385960f3d3801fb63d73ae69a4fcb6aca7018e874e8bf44f73c5e845ce81b2c643862c0344e3f4e50f4e6cb51b53a6c90ea545bc897dd73e2d2b3250711ed2a666413ae85b22e848735e8a6306e3b80f3677c8f40f85bf2f457f7764343288530a9c58b160de657df482af5eb0f291a50071d207cf46969643c113a6b15354b0b590885fbf4a7136939463ef66e013cb794ac6ab0fa887352817827ffc420bd8d1ee0b6a04d6819eadc1d96517c2edd8fe028101cd9842e407a44a8eb6bbda3fc5c54997fa43aaf340ed8ab6f75e3b588dd906ebd9deba889711b3895bd7df494f042e68cd78d7e75c237bd22b90e49e5571e169487508dfea22bfe2c83f21ec1e91f7d9331e5ac4ba6c52c1183afb1e083f3d0c42af6cf41e82e9151b5717b934ba6e4f52757d27a7c8139c523f977cb9c7f29e4a19cf41a1288c60a9ee10f58189351a51925422276e2da9ce3a4ca6944b767601e686745e88b7902e0e110dd66189dec74e3abbc76a4045d5051a6b76c91b2b20fc2ab1ca7d5d63f25d2c83f5d8ab31034e3ca42c4af1ed6017f5b10c2f44a52d78bf858aa7c148b6f07156ee365cd463d3cfffd05aa0a007eacf337b3a80941f2defd530cf824f13cd263ba2ca194e7170072ce28e800000000000000070000000001312d0077c3e8caa53f1a775a5e89b1a54488aff835593d5e2b7355179136bbdd10a86c00061b1100030159105c3e04b59668b1516a793628be01d018e0de5972a7e6094a8b29761bd07a1654dc17ab21c8f940806cd4ccfd904db9594a8170f310b1e2d95e0e8b0baab7f8484c2c66a91b7f4274ec9e5cc3651ef2ee3a0177281548905a3c7d03c4ddf2bf8b736a940d69f2a5e6beaea33d92ce189a96803654a52b0d3da0cbdbbe0725e64dc4a6633f33318de0f6ca50b9f2a79ac68fdfe3a6f6c01a5215dc3bdfb56a780dc17968c5afd3270c50d4f7e1f540364e184d460e8b8479719788023ed029ad1439d0a0c85570b7a3f50ff70a82bd9ff9c04d6d8154046a30a3606ce25c2fbda28c863109cfaa95b7fe4f96622c715422ed1a8e99ce5bdecadb7c39798822115215e3914409d5de1e2c95a6c375c530b27fd0263d02167eb28628b945280a668f88c607bd74e3aac0ac704dfda9e796a76e6a856aee502b41c6f3932b6a89425484d580b6abc232cb12530b38b5c20cbbdcb0357210c3ad09a3ad7a56353dbbd6087f7cbccbae1f656ad06d370e4ec5a7906c9841a1f36f24ba6eab5c70b5d66bc8aecb60d9feb4576ef33b23456332483f01fcd0e01477789448ddc20fa207f496db56203fdaf53f6d64d97153d8e8c8c89c821f526f4b376e929d7598cf368b826313eb0ac5e435097b3f4acef26cfb30e63ab70622ff1ed04f9a1c02074bdb2d302d932cffbba4f833ab95912272ec87d8c05a8c5f1b2b101fb78a75cf9d0ab6e751163f27018f08c2fae76c7a6a417cf5cae2cdbd867115d9c1f162e83c3fc4f52202d3471212c3ae6eb39f882aa322609958dfbc8ca3f3a2eb051ef1b43d452b54c2352e8c9d184cbbd46ea936161067893164a72a34f6e2fd63c94353e524c9db699d15cef86f970bcf6a92dc367152f9740e84b2c292dbb338e4f21e6a47171b5a5cec0f4026906b528ad2c115866fb9e807faa200b600637abd774148f7637789946a82ec5c8e35692470c0c0f12a6a39a0c5dddf73635f2dbbb82d9ea56d8313a25cfd879ee024ee5144817b1f96945f34632cf0a5a3dd3ebcb8b42bb6c4b1c3d2a7fc6f817c97ec6282a3981a8d9e3d7070c178f7df847fcec8013b0a0d07a4f8c13c829209172022e0d724caf5a01219575f1d5a26f50f53d72ef8b596ae2263d2c2eeb822166178d56eba9ee9a9c60adb4fe3a7be89044d34288c7bd59cbf6e88e01f971b687200fccdc68d5a21f8430a8e5654a17f05773b2254eb301a5624c4aadbd3db8641cfe866ff72424a891f5e0487a48c204870c932ed9895427ce9a3d0a183fc40b7bbea3617b321f35cbba2c2180908e9bea2e07e1e5d8d5de9846994b78a313b8828eca935f4bdf9ca861326cca20776c6a4df35eeee257899467973941e6e8c567054480a7d8077e6e61ab480562ad53ad8127874a7fa4f747ccb2601cfa6e8c43e89f678dd38e4df80adef7bf07149edd32bf0e9775f3478870da6af26709a5bebbac77018050a07f1a8ffe4539c2df9de1dfe18c9bf0aa640178b0912bd9631acebdc2f2f993039b6cfa414d5cfcd0c363a178443e954db312efcd4463ee7cfe0d8a1eabe74d47692653f996cdd9b93c9fad41c5b16aa91a7bd5b267578a537e45d2af048ee0ce09ea41c72020d61509e5e91575022e7d5956b8e680f229909642366d5a16a17f021f41ca3d13df75ac3d3c8c772c2ba042680ba81466f8d05c07e19b1cde5a4d27ba6b820a63fffc5a42234e18109f128cb1e3524d621e38855b5c11cfbe7edf25dc3cd470308971b655b3381f946531d89ed23ad24f6a3cacd022fb9e5eb1d5a188a5d0936d0dfe3c7c424dba5f8892b2adda87a171d73bb5c5b8f809812689794ee3a5a7dc7d80f9c84bdad2fb7f89b2a45dc25836a42c8757eaddabcab7f82e1e51502cdcd3c4745009c55fd70b1c8f1a748e71e2b96b312f71f21cb00002710000000002faf0800000000000598cd446da1a38c05425e396afcb5fbb37ea2a9db15c56121e2fae11ec9f06da63917610371ece811f4d34713b42d65c38a775487c71c193a52df6ac46d6a013164681dd200000000000000000000000000000000000000080000000000000000000500000000000000050003d6ec06b2431d4e1f9c36a610af90d6c40000000000000006000317def351ac5745148ba3275aacb2b68f000000000000000700035d913d0e286346e1a50fe900f618b18600000000000000030003c35487d4d1e24e38bb4f923885d19b44000000000000000400030c7e6ecdbccf41baab2f7d95ca9e5e9bff03d32570a2783f1792d20fbe141f337d5dded9509c875e13288fe140400f36560e240a007eacf337b3a80941f2defd530cf824f13cd263ba2ca194e7170072ce28e8000000002b40420f0000000000220020e2eb1e4df160f9e11d4d06a6075034b47e9d3e1c3d21d0feab64905d2e6ab17b47522103174f3de90105ae02d88ccc1a7ed4bf0d116f27ef6010f1ec308273c1010ae3dc2103548b338f50bdae124711cadef7a82ce832bff61826f13b031677793abb8c674c52ae0003003e0000fffffffffffc008030b0e83d8813791baf507a3c85c55d81cf80a50fd9f08504260743bb8033212800fc0003ffffffffffe80101460f774080d0e1bad18203f9af025451bfb4b8de611116890dd1fa45dd0f8e9802000007ffffffffffc80104c31735de0c70dc61744d1123646e49d871b662ae090c568a3ba7666fe370ce0c0003ffffffffffe40a007eacf337b3a80941f2defd530cf824f13cd263ba2ca194e7170072ce28e8000000000000061a8000000000fffd0205020000000001010a007eacf337b3a80941f2defd530cf824f13cd263ba2ca194e7170072ce28e80000000000a1c4a78006204e0000000000002200204c81bcdcc2c4f01d3c045348a155c9249ddba4d48c16ccf33c1d8b6e34614180a8610000000000002200204c81bcdcc2c4f01d3c045348a155c9249ddba4d48c16ccf33c1d8b6e3461418030750000000000002200204c81bcdcc2c4f01d3c045348a155c9249ddba4d48c16ccf33c1d8b6e3461418030750000000000002200204c81bcdcc2c4f01d3c045348a155c9249ddba4d48c16ccf33c1d8b6e34614180a437010000000000220020680a82e78074810847ca530cd74fabdbd2e34beaf0fdd2bc832ddfcd2669876c00350c0000000000160014e1fdef3169db3728018a906129e3913dd841ab0f04004730440220639ddfeb4cfd418c28418208226f5f6be762b65bb242968695ac1574d1fae8e702207bd3b3f60f12f171a37e50b7126c709a4cb87e417efdfe520707962d393bd28c01473044022058407ee97260aa8020eea7036f348ec4959fbd8a820e8628244705138803e00e02207d53e7fec4e01bbc04a0495e8c31210c0c6c2542e38d6c9ae677b78bd568cedb0147522103174f3de90105ae02d88ccc1a7ed4bf0d116f27ef6010f1ec308273c1010ae3dc2103548b338f50bdae124711cadef7a82ce832bff61826f13b031677793abb8c674c52ae34016920ffed02000000000101efa7874aa335cd976b8209145d670a6dba17471f6a4f47dacaa432dd972d801c04000000009000000001c62401000000000016001401396601573e7aef81f91a89fc4b4b56feb7a35e03483045022100c787b74c895285a11e04a680a50cef980d74a1ea53998bc2abaa2f7e13b6131b02204e0a8216f2efe89880feac989f9b0cfbf6aea1a61444d453d5547060aa65cc5901004d632103d6a391d54b9683da5c4c1ab1720739757422861b7ca98da226118061b40c412e67029000b2752102f2b77c73154588dfa6301b6c69062c0a713467996542a836f7a2ce6a239ba68868ac0000000000000004fd017a02000000000101efa7874aa335cd976b8209145d670a6dba17471f6a4f47dacaa432dd972d801c000000000000000000013a34000000000000220020680a82e78074810847ca530cd74fabdbd2e34beaf0fdd2bc832ddfcd2669876c050047304402201f096356d49cb9c0d1dad9a01530aa892aa75cdcb626f42ef09b734f39d0a26502200d6274377598b29f3f3eae1281a8b61d5cb9853b478cc973b8035af5d60c26a00148304502210093d252d2f22cecee6c2b604a1190120db574f0ba33b49ac826229b95071230bd02207a4baa7a1704813804ba9b7d0e2aef3932a7d7d096862c1636736ad3296a46f501008576a9144a2fe35d15fdacd3b3cba332b5879b6acfb2e79f8763ac67210375099d4e25580f8d2c59718d7319e9d26c7119f9476ca3a623fb638843d3c8617c820120876475527c210375f5c7f3e5f1da49febb149cb9dc97098402628f84b79b586d5d516c17e6327852ae67a914950b69c651378982bec084b296e6dd23a33c41b588ac6868111b0600fd017a02000000000101efa7874aa335cd976b8209145d670a6dba17471f6a4f47dacaa432dd972d801c01000000000000000001c247000000000000220020680a82e78074810847ca530cd74fabdbd2e34beaf0fdd2bc832ddfcd2669876c050048304502210088605547607e6c8b1b24a671ed340700ad396a8bf613fa9e4646c0b11c2aeff7022008e4448ba70fe48903e3034ace605d8651091fcd6f80fddb57d2e2b09da6765d014730440220774ca3fbbee6dc29db89dbb2bdb977afdf31727548aa6060e9e23a79090bf6580220474a5aa2de53dae36371ec535e13e4291c9fbd1942e7cc812ff4ca4c9d84483c01008576a9144a2fe35d15fdacd3b3cba332b5879b6acfb2e79f8763ac67210375099d4e25580f8d2c59718d7319e9d26c7119f9476ca3a623fb638843d3c8617c820120876475527c210375f5c7f3e5f1da49febb149cb9dc97098402628f84b79b586d5d516c17e6327852ae67a914950b69c651378982bec084b296e6dd23a33c41b588ac6868101b0600fd017902000000000101efa7874aa335cd976b8209145d670a6dba17471f6a4f47dacaa432dd972d801c020000000000000000014a5b000000000000220020680a82e78074810847ca530cd74fabdbd2e34beaf0fdd2bc832ddfcd2669876c050047304402202c736579e487f2ceedb15546a438818e9a13d3b9f9232d4ff418855f9112795e02202bce08fb52f8737268b616158eeb06e36abdc83942396fb407d3e603ed1edd7b0147304402204ec5ef779e7b0db022d9da0986bee62eeb0372337f3974e0bfa8ecbddec7a3ed022047930ce5ca6796d3cde21dbf9e0678f4d0af5d84889c24e2b960786d141b900a01008576a9144a2fe35d15fdacd3b3cba332b5879b6acfb2e79f8763ac67210375099d4e25580f8d2c59718d7319e9d26c7119f9476ca3a623fb638843d3c8617c820120876475527c210375f5c7f3e5f1da49febb149cb9dc97098402628f84b79b586d5d516c17e6327852ae67a914950b69c651378982bec084b296e6dd23a33c41b588ac6868101b0600fd017a02000000000101efa7874aa335cd976b8209145d670a6dba17471f6a4f47dacaa432dd972d801c030000000000000000014a5b000000000000220020680a82e78074810847ca530cd74fabdbd2e34beaf0fdd2bc832ddfcd2669876c050047304402204f63e06d948b7890c8a3fb1ae568e88de8cd4ef3364c092bcdeb006c40558b0802202a5fc572f9f02f5d2a0870101ff7d900da184fb19b5cd410ba0779bc55533c4a01483045022100ff0a3853c07d426ac93a8b772017a41f324558851ec69bcd65ed1cd81490e0f102201c3c01574fe196df6311cf21fbb78abd7d40659479ea080c7ccb3c5fd53027df01008576a9144a2fe35d15fdacd3b3cba332b5879b6acfb2e79f8763ac67210375099d4e25580f8d2c59718d7319e9d26c7119f9476ca3a623fb638843d3c8617c820120876475527c210375f5c7f3e5f1da49febb149cb9dc97098402628f84b79b586d5d516c17e6327852ae67a914950b69c651378982bec084b296e6dd23a33c41b588ac6868101b06000004ec02000000000101ea542e3a509a29b8d2afac84f21bc8f4e100df901b35c9fc19a331c588683519000000000090000000015c2100000000000016001401396601573e7aef81f91a89fc4b4b56feb7a35e034730440220359f29d38879b19665015a2b85bd60e4e3949292f75d2294d5a545aa392dd1fd022036aa2b42bdedd9deca319eece3e8a5670fe89e78231e1025b9fc1079a931398d01004d632103d6a391d54b9683da5c4c1ab1720739757422861b7ca98da226118061b40c412e67029000b2752102f2b77c73154588dfa6301b6c69062c0a713467996542a836f7a2ce6a239ba68868ac00000000ed02000000000101374c80d9d5213e7bf0ea94c3a8085866a4d533590d89f68e829162d03d431a2600000000009000000001e43400000000000016001401396601573e7aef81f91a89fc4b4b56feb7a35e03483045022100d486e826ecfddcc474b719cb69e641efe97b4795b8ea4bb9729237788a3d35ed022051b097591bfc53dc855303672e17ad8488de9f2bcca670995060f331a7dfe55e01004d632103d6a391d54b9683da5c4c1ab1720739757422861b7ca98da226118061b40c412e67029000b2752102f2b77c73154588dfa6301b6c69062c0a713467996542a836f7a2ce6a239ba68868ac00000000ed02000000000101a436af3ff69624fd11b543c0b8d451190bcb7891a7259832cb64445571dc3003000000000090000000016c4800000000000016001401396601573e7aef81f91a89fc4b4b56feb7a35e03483045022100c203f2093d4b182ba5b78a471f806acf13c4d44f7191068abaf2b7a6742f427e02200fabe2a24e947f467a48fca7bba3ed68387ce46c32533366d99a52cefb0e3ad801004d632103d6a391d54b9683da5c4c1ab1720739757422861b7ca98da226118061b40c412e67029000b2752102f2b77c73154588dfa6301b6c69062c0a713467996542a836f7a2ce6a239ba68868ac00000000ec020000000001013798507505fc7de865bccaad655dc03b3899bba86e139dfbc543a2a2f9b4b842000000000090000000016c4800000000000016001401396601573e7aef81f91a89fc4b4b56feb7a35e034730440220724b0e5a340e80de26f3908ae510a34c93b5632dbd539c1a155856cad5375ac402203de9139fbaadd86a11168c612b75f974a6143e68c564de77c8b350f2978ff2f901004d632103d6a391d54b9683da5c4c1ab1720739757422861b7ca98da226118061b40c412e67029000b2752102f2b77c73154588dfa6301b6c69062c0a713467996542a836f7a2ce6a239ba68868ac0000000000000000000000" - assert(JsonSerializers.serialization.write(ChannelCodecs.stateDataCodec.decode(dataNormal.bits).require.value)(JsonSerializers.formats).contains(""""type":"DATA_NORMAL"""")) - assert(JsonSerializers.serialization.write(ChannelCodecs.stateDataCodec.decode(dataWaitForFundingConfirmed.bits).require.value)(JsonSerializers.formats).contains(""""type":"DATA_WAIT_FOR_FUNDING_CONFIRMED"""")) - assert(JsonSerializers.serialization.write(ChannelCodecs.stateDataCodec.decode(dataShutdown.bits).require.value)(JsonSerializers.formats).contains(""""type":"DATA_SHUTDOWN"""")) - assert(JsonSerializers.serialization.write(ChannelCodecs.stateDataCodec.decode(dataNegotiating.bits).require.value)(JsonSerializers.formats).contains(""""type":"DATA_NEGOTIATING"""")) - assert(JsonSerializers.serialization.write(ChannelCodecs.stateDataCodec.decode(dataClosingLocal.bits).require.value)(JsonSerializers.formats).contains(""""type":"DATA_CLOSING"""")) + assert(JsonSerializers.serialization.write(ChannelCodecs.channelDataCodec.decode(dataNormal.bits).require.value)(JsonSerializers.formats).contains(""""type":"DATA_NORMAL"""")) + assert(JsonSerializers.serialization.write(ChannelCodecs.channelDataCodec.decode(dataWaitForFundingConfirmed.bits).require.value)(JsonSerializers.formats).contains(""""type":"DATA_WAIT_FOR_FUNDING_CONFIRMED"""")) + assert(JsonSerializers.serialization.write(ChannelCodecs.channelDataCodec.decode(dataShutdown.bits).require.value)(JsonSerializers.formats).contains(""""type":"DATA_SHUTDOWN"""")) + assert(JsonSerializers.serialization.write(ChannelCodecs.channelDataCodec.decode(dataNegotiating.bits).require.value)(JsonSerializers.formats).contains(""""type":"DATA_NEGOTIATING"""")) + assert(JsonSerializers.serialization.write(ChannelCodecs.channelDataCodec.decode(dataClosingLocal.bits).require.value)(JsonSerializers.formats).contains(""""type":"DATA_CLOSING"""")) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/ChannelCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/ChannelCodecsSpec.scala index 06b1749447..5e2259cbf2 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/ChannelCodecsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/ChannelCodecsSpec.scala @@ -48,7 +48,7 @@ class ChannelCodecsSpec extends AnyFunSuite { test("nonreg for der/bin64 signatures") { val bin = ByteVector.fromValidHex(Source.fromInputStream(getClass.getResourceAsStream("/normal_data_htlcs.bin")).mkString) - val c = ChannelCodecs.stateDataCodec.decode(bin.toBitVector).require.value + val c = ChannelCodecs.channelDataCodec.decode(bin.toBitVector).require.value val ref = Seq( hex"304502210097fcda40b22916b5d61badedf6126658c2b5927d5002cc2c3e5f88a78ba5f45b02204a74bcf8827d894cab153fc051f39d8e2aeb660162a6a05797f7140587a6133301", @@ -86,15 +86,15 @@ class ChannelCodecsSpec extends AnyFunSuite { // currently version=0 and discriminator type=1 assert(bin_old.startsWith(hex"000001")) // let's decode the old data (this will use the old codec that provides default values for new fields) - val data_new = stateDataCodec.decode(bin_old.toBitVector).require.value + val data_new = channelDataCodec.decode(bin_old.toBitVector).require.value assert(data_new.asInstanceOf[DATA_WAIT_FOR_FUNDING_CONFIRMED].fundingTx === None) assert(TimestampSecond.now().toLong - data_new.asInstanceOf[DATA_WAIT_FOR_FUNDING_CONFIRMED].waitingSince.toLong < 3600) // we just set this to current time // and re-encode it with the new codec - val bin_new = ByteVector(stateDataCodec.encode(data_new).require.toByteVector.toArray) + val bin_new = ByteVector(channelDataCodec.encode(data_new).require.toByteVector.toArray) // data should now be encoded under the new format assert(bin_new.startsWith(hex"030000")) // now let's decode it again - val data_new2 = stateDataCodec.decode(bin_new.toBitVector).require.value + val data_new2 = channelDataCodec.decode(bin_new.toBitVector).require.value // data should match perfectly assert(data_new === data_new2) } @@ -120,13 +120,13 @@ class ChannelCodecsSpec extends AnyFunSuite { // check that this data has been encoded with the old 0x03 codec assert(oldbin.startsWith(hex"000003")) // we decode with compat codec - val oldnormal = stateDataCodec.decode(oldbin.bits).require.value + val oldnormal = channelDataCodec.decode(oldbin.bits).require.value // and we encode with new codec - val newbin = stateDataCodec.encode(oldnormal).require.bytes + val newbin = channelDataCodec.encode(oldnormal).require.bytes // make sure that encoding used the new codec assert(newbin.startsWith(hex"030007")) // make sure that round-trip yields the same data - val newnormal = stateDataCodec.decode(newbin.bits).require.value + val newnormal = channelDataCodec.decode(newbin.bits).require.value assert(newnormal === oldnormal) } } @@ -144,11 +144,11 @@ class ChannelCodecsSpec extends AnyFunSuite { refs.foreach { case (oldbin, refjson) => // we decode with compat codec - val oldnormal = stateDataCodec.decode(oldbin.bits).require.value + val oldnormal = channelDataCodec.decode(oldbin.bits).require.value // we then encode with new codec - val newbin = stateDataCodec.encode(oldnormal).require.bytes + val newbin = channelDataCodec.encode(oldnormal).require.bytes // and we decode with the new codec - val newnormal = stateDataCodec.decode(newbin.bits).require.value + val newnormal = channelDataCodec.decode(newbin.bits).require.value // finally we check that the actual data is the same as before (we just remove the new json field) val oldjson = Serialization.write(oldnormal)(JsonSerializers.formats) val newjson = Serialization.write(newnormal)(JsonSerializers.formats) @@ -171,41 +171,41 @@ class ChannelCodecsSpec extends AnyFunSuite { // check that this data has been encoded with the old codec assert(oldBin.startsWith(hex"0100")) // we decode with the new codec - val decoded1 = stateDataCodec.decode(oldBin.bits).require.value + val decoded1 = channelDataCodec.decode(oldBin.bits).require.value // and we encode with the new codec - val newBin = stateDataCodec.encode(decoded1).require.bytes + val newBin = channelDataCodec.encode(decoded1).require.bytes // make sure that encoding used the new codec assert(newBin.startsWith(hex"0300")) // make sure that round-trip yields the same data - val decoded2 = stateDataCodec.decode(newBin.bits).require.value + val decoded2 = channelDataCodec.decode(newBin.bits).require.value assert(decoded1 === decoded2) }) - val negotiating = stateDataCodec.decode(dataNegotiating.bits).require.value.asInstanceOf[DATA_NEGOTIATING] + val negotiating = channelDataCodec.decode(dataNegotiating.bits).require.value.asInstanceOf[DATA_NEGOTIATING] assert(negotiating.bestUnpublishedClosingTx_opt.nonEmpty) negotiating.bestUnpublishedClosingTx_opt.foreach(tx => assert(tx.toLocalOutput === None)) assert(negotiating.closingTxProposed.flatten.nonEmpty) negotiating.closingTxProposed.flatten.foreach(tx => assert(tx.unsignedTx.toLocalOutput === None)) - val normal = stateDataCodec.decode(dataNormal.bits).require.value.asInstanceOf[DATA_NORMAL] + val normal = channelDataCodec.decode(dataNormal.bits).require.value.asInstanceOf[DATA_NORMAL] assert(normal.commitments.localCommit.htlcTxsAndRemoteSigs.nonEmpty) normal.commitments.localCommit.htlcTxsAndRemoteSigs.foreach(tx => assert(tx.htlcTx.htlcId === 0)) - val closingLocal = stateDataCodec.decode(dataClosingLocal.bits).require.value.asInstanceOf[DATA_CLOSING] + val closingLocal = channelDataCodec.decode(dataClosingLocal.bits).require.value.asInstanceOf[DATA_CLOSING] assert(closingLocal.localCommitPublished.nonEmpty) assert(closingLocal.localCommitPublished.get.commitTx.txOut.size === 6) assert(closingLocal.localCommitPublished.get.htlcTxs.size === 4) assert(closingLocal.localCommitPublished.get.claimHtlcDelayedTxs.size === 4) assert(closingLocal.localCommitPublished.get.irrevocablySpent.isEmpty) - val closingRemote = stateDataCodec.decode(dataClosingRemote.bits).require.value.asInstanceOf[DATA_CLOSING] + val closingRemote = channelDataCodec.decode(dataClosingRemote.bits).require.value.asInstanceOf[DATA_CLOSING] assert(closingRemote.remoteCommitPublished.nonEmpty) assert(closingRemote.remoteCommitPublished.get.commitTx.txOut.size === 7) assert(closingRemote.remoteCommitPublished.get.commitTx.txOut.count(_.amount === AnchorOutputsCommitmentFormat.anchorAmount) === 2) assert(closingRemote.remoteCommitPublished.get.claimHtlcTxs.size === 3) assert(closingRemote.remoteCommitPublished.get.irrevocablySpent.isEmpty) - val closingRevoked = stateDataCodec.decode(dataClosingRevoked.bits).require.value.asInstanceOf[DATA_CLOSING] + val closingRevoked = channelDataCodec.decode(dataClosingRevoked.bits).require.value.asInstanceOf[DATA_CLOSING] assert(closingRevoked.revokedCommitPublished.size === 1) assert(closingRevoked.revokedCommitPublished.head.commitTx.txOut.size === 6) assert(closingRevoked.revokedCommitPublished.head.htlcPenaltyTxs.size === 4) @@ -231,11 +231,11 @@ class ChannelCodecsSpec extends AnyFunSuite { oldbins.foreach { oldbin => // we decode with compat codec - val oldnormal = stateDataCodec.decode(oldbin.bits).require.value + val oldnormal = channelDataCodec.decode(oldbin.bits).require.value // and we encode with new codec - val newbin = stateDataCodec.encode(oldnormal).require.bytes + val newbin = channelDataCodec.encode(oldnormal).require.bytes // make sure that round-trip yields the same data - val newnormal = stateDataCodec.decode(newbin.bits).require.value + val newnormal = channelDataCodec.decode(newbin.bits).require.value assert(newnormal === oldnormal) // make sure that we have stripped sigs from the transactions assert(newnormal.commitments.localCommit.commitTxAndRemoteSig.commitTx.tx.txIn.forall(_.witness.stack.isEmpty)) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version2/ChannelCodecs2Spec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version2/ChannelCodecs2Spec.scala index 13100f6299..9072ec4de7 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version2/ChannelCodecs2Spec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version2/ChannelCodecs2Spec.scala @@ -3,7 +3,7 @@ package fr.acinq.eclair.wire.internal.channel.version2 import fr.acinq.bitcoin.scalacompat.{ByteVector64, OutPoint, Transaction} import fr.acinq.eclair.channel.{ChannelConfig, ChannelFeatures, HtlcTxAndRemoteSig} import fr.acinq.eclair.wire.internal.channel.version2.ChannelCodecs2.Codecs._ -import fr.acinq.eclair.wire.internal.channel.version2.ChannelCodecs2.stateDataCodec +import fr.acinq.eclair.wire.internal.channel.version2.ChannelCodecs2.channelDataCodec import fr.acinq.eclair.{FeatureSupport, Features, randomBytes32} import org.scalatest.funsuite.AnyFunSuite import scodec.bits.HexStringSyntax @@ -24,7 +24,7 @@ class ChannelCodecs2Spec extends AnyFunSuite { } test("remove signatures from commitment txs") { - val commitments = stateDataCodec.decode(dataNormal.bits).require.value.commitments + val commitments = channelDataCodec.decode(dataNormal.bits).require.value.commitments commitments.localCommit.commitTxAndRemoteSig.commitTx.tx.txIn.foreach(txIn => assert(txIn.witness.isNull)) assert(commitments.localCommit.htlcTxsAndRemoteSigs.nonEmpty) commitments.localCommit.htlcTxsAndRemoteSigs.foreach { @@ -37,32 +37,32 @@ class ChannelCodecs2Spec extends AnyFunSuite { test("split channel version into channel config and channel features") { { // Standard channel - val commitments = stateDataCodec.decode(dataNormal.bits).require.value.commitments + val commitments = channelDataCodec.decode(dataNormal.bits).require.value.commitments assert(commitments.channelConfig === ChannelConfig.standard) assert(commitments.channelFeatures === ChannelFeatures()) } { val staticRemoteKeyChannel = hex"000200000003039dc0e0b1d25905e44fdf6f8e89755a5e219685840d0bc1d28d3308f9628a358500092eaee758da6a5beab04167c5b889732a98ec556cf92689da41744b10159ace488000000000000000000003e8ffffffffffffffff0000000000004e2000000000000003e80090001e001600140a46fc26a4ef7b50aaf16759d2f0ca8b014fd0a6028feba10d0eafd0fad8fe20e6d9206e6bd30242826de05c63f459a00aced24b120000000302698203af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d0000000000000044c000000001dcd6500000000000000271000000000000000000090006402a87d8f1844a3809284885b41cfc9713a561780b2b784a7a027851f68aaa838730328e2fd423323abcd86b15335ff44679068acd4db71b8052f63a2a7f0b47432bd028feba10d0eafd0fad8fe20e6d9206e6bd30242826de05c63f459a00aced24b12025ee0984b83efe2369c23c3754e89d19905f8094a86486de3b241cf8735208292031163dfd4eee879b9a333290cf10e52eb010d1da45bd44efee6fab16d1035120c0000186b02000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000269820000000000000000010001fffd05aab125c87884677613fe191e097b3806667482e0ff7668c64bca637300fe02307200000000000000000000000002faf080a2fbf30338f78b739d2b0a0a806506d6877408a995791901c43f6ef98d8a7a2e00061b10000223bb38126f1131ad95b37e0b30adf0ae315f66f8cf7460afccba393fa848d036073b6577f39b0e54043691e2eec74f46b86584ffab7aa01d15e6a7ff5ff9d7c60d095c7952a19e263c267f94fbe0208fdd212b304fa8d3df1379017d184fbcce7ffd5175997774c9de7ef2f3733c7056a22888c9f302e4a9bac35f581d4f571a7ef29e6cf3b1d8a7cc05cd155a047ae498a18fccb6589338242d91e00493a86b2b78810de20644f0f065399894854bd7347927a0ce8f9c1102c9c7227ffba4b71c4325f301933397aa8f54a28d3844c71021d19b72795141af019c9a0abae3e2cc29033b7f8be8465ac8406d4c1d61d0bc3c87dee2faac4d649fc166917c81a1e237c91d5c8091f696a72241c67047cfcec893bb41e97753b3d362695bc31f776cd4a744bf09f2e428dc189fa4838f34d130d5fd4fc3d79bc7546d30d543ca99818a8c39f6c17e74ff45976c95fd6de261d68e2c5fc585d28fa9ae5abb1db475f24d16d9cb0562a589c6dec07f3b81a37b4c7f2c4642df1fabf5d7343e038706e786d2cea71f76663f57a88751287db3f8e9a7a896990374daf72eb776ee67c9ec61c1a10f164688ecbc88caa086e24daa79808899e3c3f6870ed326f5253fc038147774fa8795351f44cbadc53b67743822b89f27f0c03b2b0f4e4c774598db681cadde0e4e28f1f4edbbc8ec68649fe05e3c8cdbc46b07011c9b81b9cb3ad92ec08ddf8209a501ce064ee0bd9befce196c0c559cffcb3b0c447922ad50fbceb288037ec36abb17d93deebd40e83d8c197248b14ee12dcfe72e46f0826ef1e6e94700ad6428df77b54eb8ca767af1fd84507adc52beef32f48e3968c24d990d04cd8b35c87e270a04e2e8fc9d0ab6a6398c2168986321cc0dc297f8e0e4887226db20958e934bc35151de72d896511e3b850d548402a0e1f32fb01d5050a8ff9d7ba6818600a2dafa1c4b3ca11c0d091796190c05a1edc8146d7ef534d67bb39f76f37279dff6be5e053b7315c3f3406e9240c6b618b18545fccb46d668c007a1b640634c663bbd27c5d00a48f5c3702bac6202b55cce4212920be059ecf708db85f07c0fcc0f480fcd06aef1128fc491b2b384d89ce15120c81bd65c225d679f8cd38fb1b0ddb7d8edd3455506fa3406578f43ba4caab00593b6623b7b69c863409f0350b8b8783b9ee2ada9b9f410ab309527b7f67758f543261f52695da19e597f9457483d188e8a2bd7248a6ee5f2d01cccacd2561239959f0ce80cbb83503891a8b4e3ab88d3a02de5f8c784c0700e8bef68445615f25dd3d5baea34bc8e694a7282b054f99fa2b150af00ec62c2fd2227a23e0c0a36090b5986271f964e969cc9313ef42c3b7a9ff9e608342c73da023019e983ee6b6f630e12a02ae79802bfbee10ba20199895d7f1302d2c3b8d7ba6ad08cbb27265f9bb709bf6225a81bdabf7fc5fe9ee4794b00181038051cae0a52c08d8a7750ee46c99b7dd6cd452c52a347f12873dc697786b89ddd13e2d584aa3244cdbdb7809bc7a42728d756485070ff84aaba81b1506a7b20d2acfef11df724ee78e8b0f92376676d1e01aed7ad8d84919b8296684dafc152680b0f964db5a1b7ee4093f453fe24750c6857252e4befef3df2d648e32cc20c1ef48fd5865c242951cd359f92985680f0d626602f164b6285971a194a4baf4eb1d9a3e88841269b3e84a96e6784407f869bca5db2bb2d31e784c35e210dcaf5d3441edea9a4a348e3ead6c9b920e1ff8f1c76c41d564827cd3db006e741a8dbb5db9bdbe13033faac7e6b676bc1cf88677d10676ddca3e187caeeac52b69212503f7acca29a49c699d68f0d982c4c41806c8d9bbbc4e7d2d77a1a0655c73cfcd21b93d95e70d2bad6c306dc172b67120335bd2d03878b3d9e5967c54dd567d307c15e305b5200002710000000000bebc200000000002cb4178024b125c87884677613fe191e097b3806667482e0ff7668c64bca637300fe023072000000002b40420f000000000022002090750411c84891017ca3ab8e6212a198dbf957143d6753f7b48264c7faf45cf44752210257f2ca56049071462716385481d04d3c7c859d7e9b3f31c019b126aa0eeed1e82102a87d8f1844a3809284885b41cfc9713a561780b2b784a7a027851f68aaa8387352aefd018602000000000101b125c87884677613fe191e097b3806667482e0ff7668c64bca637300fe023072000000000036a2ab800350c300000000000022002042ee833f99ec58f28a3ffc13033c29d4e02f72f6169e0e8e039ad9323e912e30400d030000000000220020981c45edf62e238461337b25d5fd7de72a305e7f4c7755a693f877b1a564831cb04e0b0000000000160014761879f7b274ce995f87150a02e75cc0c037e8e30400483045022100dc11d7761cdf7c708ea2aaaed409a4d24697bb07948704e77b7cbe911d0268fd02204a81721e85bb1ff6cb34741ed5c5ee89c4ea8f5bb1515792625d32043682c14301483045022100af2d760500664483ea81ccaa3bc925c5587cd3cd4358ee27110f62929903c49602201ce1a97498227eaef1ab5c04da2eb9ea8114abe6af1bd35494d9cbb0e64da3e7014752210257f2ca56049071462716385481d04d3c7c859d7e9b3f31c019b126aa0eeed1e82102a87d8f1844a3809284885b41cfc9713a561780b2b784a7a027851f68aaa8387352aead678920000101241bfa1a637efc49b76b41be6ec36c47c546f69b1646a11e8420957a1dbf731487000000002b50c300000000000022002042ee833f99ec58f28a3ffc13033c29d4e02f72f6169e0e8e039ad9323e912e308b76a91420fe4794a7f993489c0477c442fed3be2c9294678763ac67210230714b6385889be70b5299603fbc508a01517a3b754d03e33e5bcf125104adc57c8201208763a914e5a9c4cf993e5053ca47ddd2a194a3c522c1fdc588527c2103bff704aae937cd0b98704a4791140cab835c5bd2406897104602c881352869ae52ae677503101b06b175ac68685e02000000011bfa1a637efc49b76b41be6ec36c47c546f69b1646a11e8420957a1dbf73148700000000000000000001daa7000000000000220020981c45edf62e238461337b25d5fd7de72a305e7f4c7755a693f877b1a564831c00000000a2fbf30338f78b739d2b0a0a806506d6877408a995791901c43f6ef98d8a7a2e000000000000000040c5c678fda30c1e23bbb68093b1d9a34169304ff3d36bfef74157f129df8d01e31ddcd8dd04f3ac9521ac13b7fed5d7997ce0c72c942054711d924ea197c7ab7640cd0045f41ffd38fa637cfc09679c628b29a8273223db1150e262d5159cf5d7852542c4ad7334f67d08ca835cd5b16b1df96544c454dff121856685471d2255300000000000000001000100fd05aab125c87884677613fe191e097b3806667482e0ff7668c64bca637300fe02307200000000000000000000000002faf080a2fbf30338f78b739d2b0a0a806506d6877408a995791901c43f6ef98d8a7a2e00061b10000223bb38126f1131ad95b37e0b30adf0ae315f66f8cf7460afccba393fa848d036073b6577f39b0e54043691e2eec74f46b86584ffab7aa01d15e6a7ff5ff9d7c60d095c7952a19e263c267f94fbe0208fdd212b304fa8d3df1379017d184fbcce7ffd5175997774c9de7ef2f3733c7056a22888c9f302e4a9bac35f581d4f571a7ef29e6cf3b1d8a7cc05cd155a047ae498a18fccb6589338242d91e00493a86b2b78810de20644f0f065399894854bd7347927a0ce8f9c1102c9c7227ffba4b71c4325f301933397aa8f54a28d3844c71021d19b72795141af019c9a0abae3e2cc29033b7f8be8465ac8406d4c1d61d0bc3c87dee2faac4d649fc166917c81a1e237c91d5c8091f696a72241c67047cfcec893bb41e97753b3d362695bc31f776cd4a744bf09f2e428dc189fa4838f34d130d5fd4fc3d79bc7546d30d543ca99818a8c39f6c17e74ff45976c95fd6de261d68e2c5fc585d28fa9ae5abb1db475f24d16d9cb0562a589c6dec07f3b81a37b4c7f2c4642df1fabf5d7343e038706e786d2cea71f76663f57a88751287db3f8e9a7a896990374daf72eb776ee67c9ec61c1a10f164688ecbc88caa086e24daa79808899e3c3f6870ed326f5253fc038147774fa8795351f44cbadc53b67743822b89f27f0c03b2b0f4e4c774598db681cadde0e4e28f1f4edbbc8ec68649fe05e3c8cdbc46b07011c9b81b9cb3ad92ec08ddf8209a501ce064ee0bd9befce196c0c559cffcb3b0c447922ad50fbceb288037ec36abb17d93deebd40e83d8c197248b14ee12dcfe72e46f0826ef1e6e94700ad6428df77b54eb8ca767af1fd84507adc52beef32f48e3968c24d990d04cd8b35c87e270a04e2e8fc9d0ab6a6398c2168986321cc0dc297f8e0e4887226db20958e934bc35151de72d896511e3b850d548402a0e1f32fb01d5050a8ff9d7ba6818600a2dafa1c4b3ca11c0d091796190c05a1edc8146d7ef534d67bb39f76f37279dff6be5e053b7315c3f3406e9240c6b618b18545fccb46d668c007a1b640634c663bbd27c5d00a48f5c3702bac6202b55cce4212920be059ecf708db85f07c0fcc0f480fcd06aef1128fc491b2b384d89ce15120c81bd65c225d679f8cd38fb1b0ddb7d8edd3455506fa3406578f43ba4caab00593b6623b7b69c863409f0350b8b8783b9ee2ada9b9f410ab309527b7f67758f543261f52695da19e597f9457483d188e8a2bd7248a6ee5f2d01cccacd2561239959f0ce80cbb83503891a8b4e3ab88d3a02de5f8c784c0700e8bef68445615f25dd3d5baea34bc8e694a7282b054f99fa2b150af00ec62c2fd2227a23e0c0a36090b5986271f964e969cc9313ef42c3b7a9ff9e608342c73da023019e983ee6b6f630e12a02ae79802bfbee10ba20199895d7f1302d2c3b8d7ba6ad08cbb27265f9bb709bf6225a81bdabf7fc5fe9ee4794b00181038051cae0a52c08d8a7750ee46c99b7dd6cd452c52a347f12873dc697786b89ddd13e2d584aa3244cdbdb7809bc7a42728d756485070ff84aaba81b1506a7b20d2acfef11df724ee78e8b0f92376676d1e01aed7ad8d84919b8296684dafc152680b0f964db5a1b7ee4093f453fe24750c6857252e4befef3df2d648e32cc20c1ef48fd5865c242951cd359f92985680f0d626602f164b6285971a194a4baf4eb1d9a3e88841269b3e84a96e6784407f869bca5db2bb2d31e784c35e210dcaf5d3441edea9a4a348e3ead6c9b920e1ff8f1c76c41d564827cd3db006e741a8dbb5db9bdbe13033faac7e6b676bc1cf88677d10676ddca3e187caeeac52b69212503f7acca29a49c699d68f0d982c4c41806c8d9bbbc4e7d2d77a1a0655c73cfcd21b93d95e70d2bad6c306dc172b67120335bd2d03878b3d9e5967c54dd567d307c15e305b5200002710000000002cb41780000000000bebc200b5c42b5c67eb6043b2776c0e9a8bd91d8ffdf7280b81dce825558ad5258746fb030d9b4186ad4f32aae9ca90701dc66e8df3f7b73e1f9590c0f6cd690aae8ad097000000000000000000000000000000000000000000000000000000010000ff0250b3cac5267f9448efcb8c2a89944f8bb4e4e7f1a3ae72d4961dfe187105b3f324b125c87884677613fe191e097b3806667482e0ff7668c64bca637300fe023072000000002b40420f000000000022002090750411c84891017ca3ab8e6212a198dbf957143d6753f7b48264c7faf45cf44752210257f2ca56049071462716385481d04d3c7c859d7e9b3f31c019b126aa0eeed1e82102a87d8f1844a3809284885b41cfc9713a561780b2b784a7a027851f68aaa8387352ae000100400000ffffffffffff0020b0e394cbe4d79e47a73926b89d17d8835e8f042d7adb244a84b6c30496c6355f80007fffffffffff80b125c87884677613fe191e097b3806667482e0ff7668c64bca637300fe023072061a8000002a00000000888bc37730827b0ccb54f13689babb8d211a6cbbec55113caad14a97f6bfce7d85313c9cefa398d15213abf841e3c9a8b88f62fa3fc742a6c746619d3e76aa71fb06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f061a8000002a000060e32b8d010000900000000000000000000854d00000000a000000003b9aca000000" - val commitments = stateDataCodec.decode(staticRemoteKeyChannel.bits).require.value.commitments + val commitments = channelDataCodec.decode(staticRemoteKeyChannel.bits).require.value.commitments assert(commitments.channelConfig === ChannelConfig.standard) assert(commitments.channelFeatures === ChannelFeatures(Features.StaticRemoteKey)) } { val anchorOutputsChannel = hex"00020000000703af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d000094b82816a3f15ab3e324ddf6f0f5a116cef8c3200a353240db684337d2dcaa81e80000001000000000000044c000000001dcd65000000000000002710000000000000000000900064ff160014f09f6f93de60c6af417ed31d52c2c883b6113f260000186b0200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000225982039dc0e0b1d25905e44fdf6f8e89755a5e219685840d0bc1d28d3308f9628a358500000000000003e8ffffffffffffffff0000000000004e2000000000000003e80090001e02e9cd12509fbc345c10e2b2e10427ae43eabbf0788bda8118b34344e5576d5d5002eb112dd8d61a02bf3434d5417e1020ed2d951e75bd1e14615d473dd5f1991b4402ca8b6891d6a53aea035259fa0d57a1e672d3cc59a2c696a96247d3557ee726cd030f2aa0ca0ae0e00d6da847d897cf7288ae286dd12037f06250937dc7b37fe0b5028d59e68c5799980a98b120acb891dabfd091e80c6596b0c2a7de57a298d48512000000032259820000000000000000000000000009c4000000002faf0800000000000bebc2002432e16e6e875d1f02a9512943ba2eb9309f4715390b52e4965dd6e39a2989ae63000000002b40420f00000000002200204de81ee2e9850dabdb9c364a12cc2dd75dd20c21d4ca0a809a2e6af3caeeaf9647522102e9cd12509fbc345c10e2b2e10427ae43eabbf0788bda8118b34344e5576d5d502103683425bf640d5e4338553ca6f6afea948ff2bf378eed1b3e9497a3f18f477d5352aefd01bc0200000000010132e16e6e875d1f02a9512943ba2eb9309f4715390b52e4965dd6e39a2989ae63000000000089273f80044a010000000000002200202102815a0b14d50a2a8c028cd47413e743f02ad5460151bd4ae5930639cbbd484a010000000000002200204e81bd6470c45c349068c2b0ad544934b718ef49c1615f2a0b0cd8766400b562400d0300000000002200208eadab92c95168438acfb73f2d3d92f56d6fc30d3f79fc4527f4e77df88b8add72270c0000000000220020895c0fef7efb7aa0e4805381434363b30386d31f6cb621abb0ca4a52b269fbc304004830450221009f09a584b1af9ba86f1618aad1ca9817324f97c3d3789b2490c3b9435c7bf1dd02205e4dcca3d7937f6b80a0ed39084e4ea9e9c531b80698ebd0c116fd9319a0db1201473044022001e4104ec25ba9bc43bf3f623d6e98f371434442b134255667ddeedd9cd9cd1b02201f057d2c15ee4e5d62a73f1db262846ca7ad35deaed2496abdb5b16cd91c5cb80147522102e9cd12509fbc345c10e2b2e10427ae43eabbf0788bda8118b34344e5576d5d502103683425bf640d5e4338553ca6f6afea948ff2bf378eed1b3e9497a3f18f477d5352ae25899520000000000000000000000000000009c4000000000bebc200000000002faf0800acdb0bf90ed7350d429c2d70fce66300ac9b27d8dbb3bceb5bd2cc8bd443938f03f54d00601f1b95e82836540d760ab6b38bc6fc1701df5d5e3b410f1936212d54000000000000000000000000000000000000000000000000000000000000ff03658b1e02efd8464688f87a052518691ff4d818a7c36addf4c6520ae07b7e20912432e16e6e875d1f02a9512943ba2eb9309f4715390b52e4965dd6e39a2989ae63000000002b40420f00000000002200204de81ee2e9850dabdb9c364a12cc2dd75dd20c21d4ca0a809a2e6af3caeeaf9647522102e9cd12509fbc345c10e2b2e10427ae43eabbf0788bda8118b34344e5576d5d502103683425bf640d5e4338553ca6f6afea948ff2bf378eed1b3e9497a3f18f477d5352ae00000032e16e6e875d1f02a9512943ba2eb9309f4715390b52e4965dd6e39a2989ae63061a8000002a000000008833a9275befbedbf928e743a71c9476f46528baf6e832ad25478c2607e4108dea67657c51db7344032627c855518179dc7a94b59f306faf9a3b4eef7684d5d73106226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f061a8000002a000060e32a590101009000000000000003e8000854d00000000a000000003b9aca000000" - val commitments = stateDataCodec.decode(anchorOutputsChannel.bits).require.value.commitments + val commitments = channelDataCodec.decode(anchorOutputsChannel.bits).require.value.commitments assert(commitments.channelConfig === ChannelConfig.standard) assert(commitments.channelFeatures === ChannelFeatures(Features.StaticRemoteKey, Features.AnchorOutputs)) } { val wumboChannel = hex"00000000000103af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d00009b3b6da18b7e5e43405415a43e61abd77edf4d300c131af17a955950837d7db4980000001000000000000044c000000001dcd65000000000000002710000000000000000000900064ff1600142946bdbb3141be9a40ed8133ef159ee0022176e90000186b02000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a4982039dc0e0b1d25905e44fdf6f8e89755a5e219685840d0bc1d28d3308f9628a358500000000000003e8ffffffffffffffff0000000000004e2000000000000003e80090001e03a6be2613a9ff761a769b077abb43d76c5f06944fc32010891156179bf8b81607025c24f0c3572631ea09dade913ccbb1cd8a55585fc152cc2165bf4f43af702c8b0306c241c667eefee031639052c4a35c4ae076ab56b49f76f7e12f63dfb7bb32a503fdbff1ca92c99c09a9c0eac3cfe1ca35deb658d5fdb815fe7a240df25da5e20f02c47d9e4e1fb501c81933cfc4d3ba7f0e8203003cda2683569f1f67aac2b6e20d000000030a4982000000000000000000000000002710000000745e66c600000000000bebc2002464fc06b43e94d9ce0a5a0e1ef13f63ba78e56d920e7d606d0c6e7a631c48c32c000000002b0065cd1d00000000220020d1b88cc50b09abce945ce6703f30dae3d71ec626be2201925db9379a3e8b981047522103044d393b0da6bcd92030f3131d0780b0d6fa017a0ff45b7fed708903c2950ced2103a6be2613a9ff761a769b077abb43d76c5f06944fc32010891156179bf8b8160752aefd01590200000000010164fc06b43e94d9ce0a5a0e1ef13f63ba78e56d920e7d606d0c6e7a631c48c32c000000000018e2678002400d030000000000160014cf3c9d08ec27d3ffefbf7e2640d3773e11a60ceb783bca1d00000000220020153e514ade07f347db9ed32cf6b2096d066d48e0cf8c504590cd1155de68be2204004730440220056ac83d5b2eb9eb9714e85294d080a9b64f73916d8206b15533613b4303ed3d022044da40ba433e27ddb8743c75acc0f6b050c346bc9d241ab3a1b65051820ce06a0147304402207683c10f33682e2389e5f02dd34090bbe4d786633d49e1777d5ec2ab71a7924902206d99dceb74ff809237ae8e06c118b38047e2d4a49f9956c538bedca47cb4b1da0147522103044d393b0da6bcd92030f3131d0780b0d6fa017a0ff45b7fed708903c2950ced2103a6be2613a9ff761a769b077abb43d76c5f06944fc32010891156179bf8b8160752aef5c4602000000000000000000000000000002710000000000bebc200000000745e66c6000bdebd6cf39db4c2ce1a675281b0a714e525525149032ba048d75c89e5a9b355021a87b53e12e4b36287635cdf887991a732c2fd56b12f5377d755b8b890795db6000000000000000000000000000000000000000000000000000000000000ff032c5f941f78a00105b995fba7f3c340afe53e287a60361135386ae1448d6f3d632464fc06b43e94d9ce0a5a0e1ef13f63ba78e56d920e7d606d0c6e7a631c48c32c000000002b0065cd1d00000000220020d1b88cc50b09abce945ce6703f30dae3d71ec626be2201925db9379a3e8b981047522103044d393b0da6bcd92030f3131d0780b0d6fa017a0ff45b7fed708903c2950ced2103a6be2613a9ff761a769b077abb43d76c5f06944fc32010891156179bf8b8160752ae00000064fc06b43e94d9ce0a5a0e1ef13f63ba78e56d920e7d606d0c6e7a631c48c32cff5e020000000101010101010101010101010101010101010101010101010101010101010101012a00000000ffffffff010065cd1d00000000220020d1b88cc50b09abce945ce6703f30dae3d71ec626be2201925db9379a3e8b9810000000000000000060e32bf9000082000000000000000000000000000000000000000000000000000000000000000064fc06b43e94d9ce0a5a0e1ef13f63ba78e56d920e7d606d0c6e7a631c48c32c0000f4957823b0a6b76697d7c545bcca66abec8522a0f6350a722e45f3f2830f7f824c84efed8daffa04bb0822ce1a08fb2de77dd20c23a91cced7a3966808956644" - val commitments = stateDataCodec.decode(wumboChannel.bits).require.value.commitments + val commitments = channelDataCodec.decode(wumboChannel.bits).require.value.commitments assert(commitments.channelConfig === ChannelConfig.standard) assert(commitments.channelFeatures === ChannelFeatures(Features.Wumbo)) } } test("ensure remote shutdown script is not set") { - val commitments = stateDataCodec.decode(dataNormal.bits).require.value.commitments + val commitments = channelDataCodec.decode(dataNormal.bits).require.value.commitments assert(commitments.remoteParams.shutdownScript === None) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3Spec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3Spec.scala index 832ebe667a..36ab4756aa 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3Spec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3Spec.scala @@ -22,7 +22,7 @@ import fr.acinq.eclair.channel._ import fr.acinq.eclair.transactions.Transactions._ import fr.acinq.eclair.wire.internal.channel.ChannelCodecsSpec.normal import fr.acinq.eclair.wire.internal.channel.version3.ChannelCodecs3.Codecs._ -import fr.acinq.eclair.wire.internal.channel.version3.ChannelCodecs3.stateDataCodec +import fr.acinq.eclair.wire.internal.channel.version3.ChannelCodecs3.channelDataCodec import fr.acinq.eclair.{BlockHeight, CltvExpiryDelta, Features, MilliSatoshi, UInt64, randomKey} import org.scalatest.funsuite.AnyFunSuite import scodec.bits.{ByteVector, HexStringSyntax} @@ -105,23 +105,23 @@ class ChannelCodecs3Spec extends AnyFunSuite { test("backward compatibility DATA_NORMAL_COMPAT_02_Codec") { val oldBin = hex"00022aed498450b3eb2f6aafedd40640b54efc60ae681da9e6a35299b8b6a6125a7301010003af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d00009d2b17f27b3938b2b50ec713df1b1ae5fd3d23010c9e2e22385f13a168c6acf2c80000001000000000000044c000000001dcd65000000000000002710000000000000000000900064ff1600149d706d0fa71a0b6aa0f3fa400bee18102b45c8170000186b0200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000024982039dc0e0b1d25905e44fdf6f8e89755a5e219685840d0bc1d28d3308f9628a358500000000000003e8ffffffffffffffff0000000000004e2000000000000003e80090001e03fb08866066d5f6f539314220fc31a8e7fdf6ccd12ccde150acc59bac5bb971e003f399d17a9e2fb13a52373d1631807c3161250b2774642fffa629858ad0831f68020b4ecc52820a93f6da40a60d7859307edc737347054d82592959ed1cd0b02e9c03db348203bfd939779bfdd825bc807e904225c3348fa5e3bb57d38ac4b9f40f850284c0df55fbfc2212cbd18cf8ab0eb5283b4b883350ff07e81988e01ae2bb71e20000000302498200000000000000000000000000002710000000002faf0800000000000bebc200242aed498450b3eb2f6aafedd40640b54efc60ae681da9e6a35299b8b6a6125a73000000002b40420f00000000002200203305e10ec90f004675285e9b0371a663ead7abe6caba8c6d5739ace6c9eab4534752210390d6a42ff78a21b41560f75359f0f8a9edaaf0ddcf1a6609130f0d5f234463662103fb08866066d5f6f539314220fc31a8e7fdf6ccd12ccde150acc59bac5bb971e052ae7d02000000012aed498450b3eb2f6aafedd40640b54efc60ae681da9e6a35299b8b6a6125a7300000000000a1e418002400d0300000000001600146862a069e7038573a81f5627ae7d0c6ee5cd0acfb8180c0000000000220020a5f137a8049afcac9f0451b0f31677a81b5443f1c04c1910b37b0d2b8aa4ca0084a4eb2096e400507ac49b57910c914cff8338b8bc57884983541ee1b6919ece7431f4030b09a5fcd87b35682dea0d8faa394e47cc31af897cdf3e6ff502b086cfac37c700000000000000000000000000002710000000000bebc200000000002faf080028131acadf245e7d95d3d7a7f1ac0c0411ead7957ab00d310696b0b5d7d14ac8020597a38e090850030f255fb2781a53713faf7ba81c44de931a63a78efd9908ef000000000000000000000000000000000000000000000000000000000000ff035878c87f6ed100476648193e10a1462bfb55cea3ec5a8f4fbd0fe7304979094b242aed498450b3eb2f6aafedd40640b54efc60ae681da9e6a35299b8b6a6125a73000000002b40420f00000000002200203305e10ec90f004675285e9b0371a663ead7abe6caba8c6d5739ace6c9eab4534752210390d6a42ff78a21b41560f75359f0f8a9edaaf0ddcf1a6609130f0d5f234463662103fb08866066d5f6f539314220fc31a8e7fdf6ccd12ccde150acc59bac5bb971e052ae000000061a8000002a0000000088ec7ae2af533c270809b48f9a0b5a9650df9961a2177e04d7e9929ab319fcd0d150e3ded703b8b4a3f5faff0f2fedf22a6729760deefdea4feed868e4e3cdcf1b06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f061a8000002a000061163fb60101009000000000000003e8000858b800000014000000003b9aca000000" - val decoded1 = stateDataCodec.decode(oldBin.bits).require.value + val decoded1 = channelDataCodec.decode(oldBin.bits).require.value assert(decoded1.asInstanceOf[DATA_NORMAL].closingFeerates === None) - val newBin = stateDataCodec.encode(decoded1).require.bytes + val newBin = channelDataCodec.encode(decoded1).require.bytes // make sure that encoding used the new codec assert(newBin.startsWith(hex"0007")) - val decoded2 = stateDataCodec.decode(newBin.bits).require.value + val decoded2 = channelDataCodec.decode(newBin.bits).require.value assert(decoded1 === decoded2) } test("backward compatibility DATA_SHUTDOWN_COMPAT_03_Codec") { val oldBin = hex"0003342b8fd35a1f3384c6db37723ecc766b672f61b4b0f2e7d5d81cf1e451b584d301010003af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d00009f2408e50889ac3f6c19a007e25752bf143ab0d920cedb9f1c7b7b6b2270c1fe080000001000000000000044c000000001dcd65000000000000002710000000000000000000900064ff160014d0cd80743458194e5f86d9a74592765442fd15bb0000186b0200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000024982039dc0e0b1d25905e44fdf6f8e89755a5e219685840d0bc1d28d3308f9628a358500000000000003e8ffffffffffffffff0000000000004e2000000000000003e80090001e03d1a9a2a574585f60abaaa33826488f9eb8d53ca76693bfa69b42ee2bc8ebe9c50223dc803cceb3edff5deacc9965002cc366b719331e776abf76ee680e4afbf46a0315880bf27110fa57f5b75607bc32a098c209fe8a5a2a30120b2a246fd3fcd62e025dc435be4cb790e5ab12705fb3c0b717ddd9f4d0234fdb557358ecefc6a4e97a0238e3723a9d60dac60c2673ec836e2a909ba9fc888fcbd1cd2ad57ff0b10c5c360000000302498200000000000000000001000200fd05aa342b8fd35a1f3384c6db37723ecc766b672f61b4b0f2e7d5d81cf1e451b584d300000000000000000000000011e1a300d91ec390338376481ff3aea604bf2106cf380d78e946e6a4fc0ad72dc0dec51b00061b100002909a15094ba406ca280bce032eab4d25eae4a9e8b51ff65574b6de2033a292cb1bc6ef576207f4b585273048502efad58af480f08dabcbff1a9642eb3b08811eb4417ac14bf6e5e3883d55ba3b7fdaf27e5ebac16ce63d183b2521867f6cedcf55692f1490da9bc655e38281a26150c44d56e02013faf1cdb2457bec4830e67c10ff0d731798d9e383cc3ce9ba21bddd8971e1d206c54c22317d75411b73abb08c9af348cca2aa2f17232b008aaef4bedb98c426041d3698924d089b59c639396ba38d2ba6d59ea2b0ee8ec9f548deced719e9aae73854d8b6c10c78890999ae09b912a8ae98d3513119bfa0ffdd3037cf5690122020f2c0d01b282ad0cbe345bd31cf7e55d8e78c61abfa62f97d2a095e2256c6569d3e6b5652b365d935d16451f2cf7df04e99e675d9eb649b583fc5d810dae3860001e667b9b78733b3ed6ca5058e6806ca4eb493c5c9a53b0ad78b9ea2091d6e94130afb1d79ce8e274a93d748af9d4ee7ae5b1c06cf1f65507bc79db22c30ae6df93a16c279e4beaf50a0e4e87449e9c830fe9b3877b7332b0632743d4e6267ffa404a35d74d79c5d724bdc9aaaa0423a157fb269b916b0022fd52bb0b2702e6abcc7d9d39d7f69a09a9814b3110cc72574fed031bbc0fd5a03fcee1181076369e76c183b2833385a38dd10773fe3228cb18aa731674a574d310a76052cf89c80077aa89380fcfdeda00508f79261c1e1886240de3bbc343eb84ea7879de24f7dbd0b1527201131bcaf5621e49c69ceec36280f9db6d27161c1676c9b11597d4bd8f10032a964fc1d1afcea594237f3408b184b16eba952778252bc6debc30e6e595c7ebbc2af0c00456ca771d024a56d9f074ca5a7f78f1ef107aa42c2a6426f8dbb2ea8962e2128cfc15161512d2e923e2879635c8ba80bc17f92d09abfae4e08a7f34decc1acd4aa4cc08181017bf248321558a8c4d1a45b92b96274dd9f336eb45d8af6d13147be3a250c17106ec6729e0cffc1af5255352296477dea870426aa82e7af3cc07156b65f8d98b482985618b69b99ee3871240db751923dbeddea6e36f5c3f0599300973fe064ccf4f15cadb7372aaab5b300674f5bf28ce905ab411f5605fba7075ed4a6b08dca3ed5867dc32e491c64e7f09ff4095081e0731cbfc53fa42c33e79bd1a26d8e9f8c5b546b07bd1b300f1f357919e955bded1f987401f4da1a453282b6978ff3bfd13b280f6253dd968cb4e7741a15faf7b4c70dbd9eab6461ca3d3a576410213bc4e144a63685488afeeb7857af4ef83e73c88b17798616a9ccbd603d61ec8f4dbbdd5fea5ca748cf40c38fec50d4eebf1e5b57fd32500baa892da24349598c3125162b2ea0ea049ea9aacdfa848f41fdf7f9eb983776bac58ddf762d131196ab18774349a5eca2fc5b51b60d7aceae1321c1decd462f9c7dbbd820399f71a3c3ccbb30a29f76cd8e94243e400c4dc6e6d32b6f94155a21eb89b6b4c0fd1660bd98509d11b3a45fb257ae1e3c5ebae7bae038200c0f7f2d4ee744c3f2d9007d6fb282b0254ccd71eae5b4c4996b52edb2f2f4120356096c81e42168ee43d98bd630b0af01c270db3fb680cd931d388e1c1260126707548ed1f27006a64db7885ac7500ce020f6349a1abf59fb3ca4bc5bdcbd19b7a0cfe82aeea62f0373d5c16002cd9adcb3ef5e0c862e2fda972530b399b0f6365f4edbc36a32bd1766763104aca34450f278418211d62ff3fff2d0de7f516028011e5681ee892288835a3ec6d0d353f8bc7168086a0eed30d5e81d8a51f7debbb2290dc1a1745e0fc2c08778d11ccb78dc7c98a8b4a4b28fd8decda3a2063ad73c23c112d609b6903613c5ad8e178c90fed2ebd10b176b17c9c2dfc630ef744ac289b437ad229b77a393c6b05c10f36ba2f80482ed6e26b3d9c34d2200fd05aa342b8fd35a1f3384c6db37723ecc766b672f61b4b0f2e7d5d81cf1e451b584d30000000000000001000000000bebc20092ebfcedc573c52ba840ff42bb322419ed390dc845c2fe80636b6b82cf41210000061b100002f211b0d97e6c7f7fb800dbb8d1c7536ad7ef284c40a85fc7316b771bb525c91f7e1d4bba90df0cf0d12c19906e4ae0a1b83954632b59f5f499dedaeba19d75c3bc244ccb57089abeaf29c7d73d67ae822aea2ed4d85d27dc65042589e8172aa3b66d928d7e35f027d96bf23e656701bdf13651e25bed745fef014fdec3faaba4795638299975d3cdc8d2c5b19e2e411e6e4f7b3cede7e269c8902306d25b1027e99736489fde667f4eaf5726349851986ff15a9e967385e080079a14c0a9c3001bbce5da720dcf739e67801cfe2a565b24f5515f8d8889a6d2dc0b51fae4facb72904ba8c1949550332cd8b9095655da72a9815537a80efd204d497af47fd18ef0f6d0639424eb81dab0e34f228aed9406d23773874b4c41d9a21dcaf632419e459baaca5f338f0f9718c89fb72108788d32aeb07e2eb343c45891958779e75fe0039eddaf7e5c4aa8ac2aee9928c68dc529d415780d8e14adcecc91b142b238e7657ef1dc6b6e9843bb5fbe8d04564307e515d88b34105b90b3fa9ed91025297a373507cba86160ba5b7b569948c95fd483b28b8970bebd5e17fcad76c6ed64615cc2d7297994483d94c010a210ad3602b7f359d7d1391b97968f9eaa70de83ea1168fdb05190a6e9fc7c984213da3e2a6672d09704d8e29e2aa9349c9ae44eca3dc0522c769887cbaa0926c5bad64eaf7c22399f7abcc01505d921cce15f4613406956715fe3030ab5c74506d1acc30649b596931316a8e210a368d758d7c1f406f5888499f8317a9ba629dfe85453b4413107796bde15636e6f628078863c0009db7d97cfadc2bfdcdfed237debac33dc525f25865663bc7a812099102fe9c65c8a37b179337141c8d76f365824b0db54b0b18d6c88b21406a1e35061fb4f5ef8a280f9c2ac2ada9eea8cb365d346c5e9eaa56728584aa71c4296a1c11d76f03351a91a030ae6df1da008a5d0f92207b8ca22f335b5a347707ef2500119597d5b92c559f477d8405f1d0dfd790c99ca5a7b4aeac232224ea8461ed07636931e65fee7b063d0ef49f471c04e2fd45b2fff3fb6923314e26045259640335c417ed14a0799553dda3eff5065ec5606303c42309221881c7dea17227da4003a6e396e308ca77da3b4b743a1c2771252c2fa8caad98c9985f9157dbcdd355d3a70b2edeca9682e4bba3e7da24fdd472017b89103e61ab9a4bcc9ab75bd28f7fad29327bf710b3b4759bb76983b003a0907794ad73194d14a70769ce6731b0ae21d565b53780275b6ac42c650d47f570331c7ac57fc75390d3b232eece8277a7c8453b5f28206ff1026879981f2ac83a0d4760554401e671a110de6c03cc104927c55de7254311680af4fb5d36b2199e1348f6fa50877d6cb163dd5f70381f2418f20d145d8ed5df2714cdcda59a4362b0c544ee1af01ac41bc15729951b1e53f3a135fe9011d3e938b08c50d53da259e2ab19b97a47b96d6b7fd559704ac9d1d8a223cbb88c9f77fa563c0c66524c260f0ff54471acae87e71c0507b14edf02bd42993966eea6b2f5d36db12b7fec7a63b80a13651822867db1f046081ad2cf3676b88fa2ef700b8c865a7dedcdee739795853584b74c3f59868daf79e81bf70afed8ea1ce689d67090a32dfe351fd0bdaf60b3903d7ff7c149134a95b2ddf1d63e0f89dfe0b710accf2ca3c37647973532338c59d32e562d7c55573277c1fab09d57a452f4214131178683533fbfe7726276903aacd579d36c6679187b1d22dda32fc5302b91244840a54c258e4d416deb0d20f96b214c335608db6b725175d8517f35f9d6b466c751a1253b20051863a02f2e0c073830cb7de27c96c4ccb196e079edc817ebdf70972b917b6a07c6a431567b99c6072b102e5103a22908683f60bb4fb72538ae3a3768de85f9fc682e67c6aea0e000027100000000011e1a300000000000bebc20024342b8fd35a1f3384c6db37723ecc766b672f61b4b0f2e7d5d81cf1e451b584d3000000002b40420f000000000022002091a342be9a9171a40f83766fbf34a0be03b79675dd20c14e13d91651f7f8273e47522103a1bd05e59ffa63f010cee2ff6848730c6f718da57aef6a5df381683d409a815e2103d1a9a2a574585f60abaaa33826488f9eb8d53ca76693bfa69b42ee2bc8ebe9c552aed30200000001342b8fd35a1f3384c6db37723ecc766b672f61b4b0f2e7d5d81cf1e451b584d300000000007d30f88004400d030000000000160014dab249c7d3b8d826d45244f609bb90f72e691daa400d030000000000220020d24765dcd888e08581c2add26ddfaa6f3ef06b56578373c59915bcc25aa0c6ee286a04000000000022002054d7877901aedaf54d84b20a4233107c2fdab9ff637106d44097e69a813d394ee093040000000000220020039e4d3b414381fb8fda640b122e604b20aa6f64e87607dd9354cdc636cb483d1b17b4207914784cfd82df8f93f15defff862cede27811ce4c59df98e86fd57e5b5105921e842a02a47ee6ddb313b37bc0b6706fe3011e2dd2d82066c85ab11a8d7b4e47000202241ad33e0af0717b31e19b50fd6b0469a42807eec1a080ad29cd56d04a4de54313010000002b400d030000000000220020d24765dcd888e08581c2add26ddfaa6f3ef06b56578373c59915bcc25aa0c6ee8576a91474e49ec772064db47a555b032479b20992beeefe8763ac672102cd66c9b36fdd627754f492f15bc2387f44fd70bc4c9ccdb08df37e25250a5b197c820120876475527c2103d80a13db0dffe34a9b1ff383306a52aed0a40a99ce1c34b113ca7467327f608452ae67a9142a784c1850f79298eab62c5c7709a5d468f5522788ac68685e02000000011ad33e0af0717b31e19b50fd6b0469a42807eec1a080ad29cd56d04a4de54313010000000000000000015af302000000000022002054d7877901aedaf54d84b20a4233107c2fdab9ff637106d44097e69a813d394e101b06000000000000000001f8c46e9fa497a4f9f4fdf422840d264f67178defb43320b83c1d9825a6c152995775652ba3689025f4841f165acbd16732ad88629429b3811d71c7b03adf254402241ad33e0af0717b31e19b50fd6b0469a42807eec1a080ad29cd56d04a4de54313030000002be093040000000000220020039e4d3b414381fb8fda640b122e604b20aa6f64e87607dd9354cdc636cb483d8576a91474e49ec772064db47a555b032479b20992beeefe8763ac672102cd66c9b36fdd627754f492f15bc2387f44fd70bc4c9ccdb08df37e25250a5b197c820120876475527c2103d80a13db0dffe34a9b1ff383306a52aed0a40a99ce1c34b113ca7467327f608452ae67a91448d436723875e511f3a4fc540ca63c91637e926388ac68685e02000000011ad33e0af0717b31e19b50fd6b0469a42807eec1a080ad29cd56d04a4de5431303000000000000000001fa7904000000000022002054d7877901aedaf54d84b20a4233107c2fdab9ff637106d44097e69a813d394e101b06000000000000000000ce718c1999b28ddd1207bcdd6789541ec2f405f775efb096bd896bb237b7f5c17543684f99cb53efeade77bc386997e7b5361418c510ed442700cee38c36f18900000000000000010002fffd05aa342b8fd35a1f3384c6db37723ecc766b672f61b4b0f2e7d5d81cf1e451b584d300000000000000000000000011e1a300d91ec390338376481ff3aea604bf2106cf380d78e946e6a4fc0ad72dc0dec51b00061b100002909a15094ba406ca280bce032eab4d25eae4a9e8b51ff65574b6de2033a292cb1bc6ef576207f4b585273048502efad58af480f08dabcbff1a9642eb3b08811eb4417ac14bf6e5e3883d55ba3b7fdaf27e5ebac16ce63d183b2521867f6cedcf55692f1490da9bc655e38281a26150c44d56e02013faf1cdb2457bec4830e67c10ff0d731798d9e383cc3ce9ba21bddd8971e1d206c54c22317d75411b73abb08c9af348cca2aa2f17232b008aaef4bedb98c426041d3698924d089b59c639396ba38d2ba6d59ea2b0ee8ec9f548deced719e9aae73854d8b6c10c78890999ae09b912a8ae98d3513119bfa0ffdd3037cf5690122020f2c0d01b282ad0cbe345bd31cf7e55d8e78c61abfa62f97d2a095e2256c6569d3e6b5652b365d935d16451f2cf7df04e99e675d9eb649b583fc5d810dae3860001e667b9b78733b3ed6ca5058e6806ca4eb493c5c9a53b0ad78b9ea2091d6e94130afb1d79ce8e274a93d748af9d4ee7ae5b1c06cf1f65507bc79db22c30ae6df93a16c279e4beaf50a0e4e87449e9c830fe9b3877b7332b0632743d4e6267ffa404a35d74d79c5d724bdc9aaaa0423a157fb269b916b0022fd52bb0b2702e6abcc7d9d39d7f69a09a9814b3110cc72574fed031bbc0fd5a03fcee1181076369e76c183b2833385a38dd10773fe3228cb18aa731674a574d310a76052cf89c80077aa89380fcfdeda00508f79261c1e1886240de3bbc343eb84ea7879de24f7dbd0b1527201131bcaf5621e49c69ceec36280f9db6d27161c1676c9b11597d4bd8f10032a964fc1d1afcea594237f3408b184b16eba952778252bc6debc30e6e595c7ebbc2af0c00456ca771d024a56d9f074ca5a7f78f1ef107aa42c2a6426f8dbb2ea8962e2128cfc15161512d2e923e2879635c8ba80bc17f92d09abfae4e08a7f34decc1acd4aa4cc08181017bf248321558a8c4d1a45b92b96274dd9f336eb45d8af6d13147be3a250c17106ec6729e0cffc1af5255352296477dea870426aa82e7af3cc07156b65f8d98b482985618b69b99ee3871240db751923dbeddea6e36f5c3f0599300973fe064ccf4f15cadb7372aaab5b300674f5bf28ce905ab411f5605fba7075ed4a6b08dca3ed5867dc32e491c64e7f09ff4095081e0731cbfc53fa42c33e79bd1a26d8e9f8c5b546b07bd1b300f1f357919e955bded1f987401f4da1a453282b6978ff3bfd13b280f6253dd968cb4e7741a15faf7b4c70dbd9eab6461ca3d3a576410213bc4e144a63685488afeeb7857af4ef83e73c88b17798616a9ccbd603d61ec8f4dbbdd5fea5ca748cf40c38fec50d4eebf1e5b57fd32500baa892da24349598c3125162b2ea0ea049ea9aacdfa848f41fdf7f9eb983776bac58ddf762d131196ab18774349a5eca2fc5b51b60d7aceae1321c1decd462f9c7dbbd820399f71a3c3ccbb30a29f76cd8e94243e400c4dc6e6d32b6f94155a21eb89b6b4c0fd1660bd98509d11b3a45fb257ae1e3c5ebae7bae038200c0f7f2d4ee744c3f2d9007d6fb282b0254ccd71eae5b4c4996b52edb2f2f4120356096c81e42168ee43d98bd630b0af01c270db3fb680cd931d388e1c1260126707548ed1f27006a64db7885ac7500ce020f6349a1abf59fb3ca4bc5bdcbd19b7a0cfe82aeea62f0373d5c16002cd9adcb3ef5e0c862e2fda972530b399b0f6365f4edbc36a32bd1766763104aca34450f278418211d62ff3fff2d0de7f516028011e5681ee892288835a3ec6d0d353f8bc7168086a0eed30d5e81d8a51f7debbb2290dc1a1745e0fc2c08778d11ccb78dc7c98a8b4a4b28fd8decda3a2063ad73c23c112d609b6903613c5ad8e178c90fed2ebd10b176b17c9c2dfc630ef744ac289b437ad229b77a393c6b05c10f36ba2f80482ed6e26b3d9c34d22fffd05aa342b8fd35a1f3384c6db37723ecc766b672f61b4b0f2e7d5d81cf1e451b584d30000000000000001000000000bebc20092ebfcedc573c52ba840ff42bb322419ed390dc845c2fe80636b6b82cf41210000061b100002f211b0d97e6c7f7fb800dbb8d1c7536ad7ef284c40a85fc7316b771bb525c91f7e1d4bba90df0cf0d12c19906e4ae0a1b83954632b59f5f499dedaeba19d75c3bc244ccb57089abeaf29c7d73d67ae822aea2ed4d85d27dc65042589e8172aa3b66d928d7e35f027d96bf23e656701bdf13651e25bed745fef014fdec3faaba4795638299975d3cdc8d2c5b19e2e411e6e4f7b3cede7e269c8902306d25b1027e99736489fde667f4eaf5726349851986ff15a9e967385e080079a14c0a9c3001bbce5da720dcf739e67801cfe2a565b24f5515f8d8889a6d2dc0b51fae4facb72904ba8c1949550332cd8b9095655da72a9815537a80efd204d497af47fd18ef0f6d0639424eb81dab0e34f228aed9406d23773874b4c41d9a21dcaf632419e459baaca5f338f0f9718c89fb72108788d32aeb07e2eb343c45891958779e75fe0039eddaf7e5c4aa8ac2aee9928c68dc529d415780d8e14adcecc91b142b238e7657ef1dc6b6e9843bb5fbe8d04564307e515d88b34105b90b3fa9ed91025297a373507cba86160ba5b7b569948c95fd483b28b8970bebd5e17fcad76c6ed64615cc2d7297994483d94c010a210ad3602b7f359d7d1391b97968f9eaa70de83ea1168fdb05190a6e9fc7c984213da3e2a6672d09704d8e29e2aa9349c9ae44eca3dc0522c769887cbaa0926c5bad64eaf7c22399f7abcc01505d921cce15f4613406956715fe3030ab5c74506d1acc30649b596931316a8e210a368d758d7c1f406f5888499f8317a9ba629dfe85453b4413107796bde15636e6f628078863c0009db7d97cfadc2bfdcdfed237debac33dc525f25865663bc7a812099102fe9c65c8a37b179337141c8d76f365824b0db54b0b18d6c88b21406a1e35061fb4f5ef8a280f9c2ac2ada9eea8cb365d346c5e9eaa56728584aa71c4296a1c11d76f03351a91a030ae6df1da008a5d0f92207b8ca22f335b5a347707ef2500119597d5b92c559f477d8405f1d0dfd790c99ca5a7b4aeac232224ea8461ed07636931e65fee7b063d0ef49f471c04e2fd45b2fff3fb6923314e26045259640335c417ed14a0799553dda3eff5065ec5606303c42309221881c7dea17227da4003a6e396e308ca77da3b4b743a1c2771252c2fa8caad98c9985f9157dbcdd355d3a70b2edeca9682e4bba3e7da24fdd472017b89103e61ab9a4bcc9ab75bd28f7fad29327bf710b3b4759bb76983b003a0907794ad73194d14a70769ce6731b0ae21d565b53780275b6ac42c650d47f570331c7ac57fc75390d3b232eece8277a7c8453b5f28206ff1026879981f2ac83a0d4760554401e671a110de6c03cc104927c55de7254311680af4fb5d36b2199e1348f6fa50877d6cb163dd5f70381f2418f20d145d8ed5df2714cdcda59a4362b0c544ee1af01ac41bc15729951b1e53f3a135fe9011d3e938b08c50d53da259e2ab19b97a47b96d6b7fd559704ac9d1d8a223cbb88c9f77fa563c0c66524c260f0ff54471acae87e71c0507b14edf02bd42993966eea6b2f5d36db12b7fec7a63b80a13651822867db1f046081ad2cf3676b88fa2ef700b8c865a7dedcdee739795853584b74c3f59868daf79e81bf70afed8ea1ce689d67090a32dfe351fd0bdaf60b3903d7ff7c149134a95b2ddf1d63e0f89dfe0b710accf2ca3c37647973532338c59d32e562d7c55573277c1fab09d57a452f4214131178683533fbfe7726276903aacd579d36c6679187b1d22dda32fc5302b91244840a54c258e4d416deb0d20f96b214c335608db6b725175d8517f35f9d6b466c751a1253b20051863a02f2e0c073830cb7de27c96c4ccb196e079edc817ebdf70972b917b6a07c6a431567b99c6072b102e5103a22908683f60bb4fb72538ae3a3768de85f9fc682e67c6aea0e00002710000000000bebc2000000000011e1a300306acd0b21100f755a6d193a819042fde1b4c3839598e730905dfa7e6ef699220240a9cccdb58d887b42ccadeee5704030acd52bd9c7759ba01f47f0995531ca360000000000000000000000000000000000000002000000000000000000020000000000000000000334c7b350311d40b3b422e07f3b7759b400000000000000010003e3dd409b342046e7b410cc380c9a09c2ff03cdeeaf422678a9eac407da8f811a402718083016f88d5404f5508054fe25acb824342b8fd35a1f3384c6db37723ecc766b672f61b4b0f2e7d5d81cf1e451b584d3000000002b40420f000000000022002091a342be9a9171a40f83766fbf34a0be03b79675dd20c14e13d91651f7f8273e47522103a1bd05e59ffa63f010cee2ff6848730c6f718da57aef6a5df381683d409a815e2103d1a9a2a574585f60abaaa33826488f9eb8d53ca76693bfa69b42ee2bc8ebe9c552ae000100400000ffffffffffff00204cec06f1f1496c01cd4840cddff19b47c24b7daa478bcaea862c6d11fcc9fff180007fffffffffff8038342b8fd35a1f3384c6db37723ecc766b672f61b4b0f2e7d5d81cf1e451b584d300160014d0cd80743458194e5f86d9a74592765442fd15bb38342b8fd35a1f3384c6db37723ecc766b672f61b4b0f2e7d5d81cf1e451b584d30016001483366b494906052362a88e967304296937926cd1" - val decoded1 = stateDataCodec.decode(oldBin.bits).require.value + val decoded1 = channelDataCodec.decode(oldBin.bits).require.value assert(decoded1.asInstanceOf[DATA_SHUTDOWN].closingFeerates === None) - val newBin = stateDataCodec.encode(decoded1).require.bytes + val newBin = channelDataCodec.encode(decoded1).require.bytes // make sure that encoding used the new codec assert(newBin.startsWith(hex"0008")) - val decoded2 = stateDataCodec.decode(newBin.bits).require.value + val decoded2 = channelDataCodec.decode(newBin.bits).require.value assert(decoded1 === decoded2) } From ddb52d82ea211d97eff9af42ac57308e9002b149 Mon Sep 17 00:00:00 2001 From: Fabrice Drouin Date: Thu, 7 Apr 2022 17:43:19 +0200 Subject: [PATCH 051/121] Check that channel seed has not been modified (#2226) * Check that channel seed has not been modified On startup, we check that for each of our channels the public key derived from the channel seed matches the key that was used when the channel was funded. * Rename DBCompatChecker to DBChecker and move channel seed check there --- ...{DBCompatChecker.scala => DBChecker.scala} | 31 +++++++++++-------- .../main/scala/fr/acinq/eclair/Setup.scala | 8 +++-- .../fr/acinq/eclair/channel/Commitments.scala | 9 +++++- .../eclair/channel/CommitmentsSpec.scala | 8 ++++- 4 files changed, 38 insertions(+), 18 deletions(-) rename eclair-core/src/main/scala/fr/acinq/eclair/{DBCompatChecker.scala => DBChecker.scala} (56%) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/DBCompatChecker.scala b/eclair-core/src/main/scala/fr/acinq/eclair/DBChecker.scala similarity index 56% rename from eclair-core/src/main/scala/fr/acinq/eclair/DBCompatChecker.scala rename to eclair-core/src/main/scala/fr/acinq/eclair/DBChecker.scala index 9a3b7ef8e3..4119a1e393 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/DBCompatChecker.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/DBChecker.scala @@ -16,31 +16,36 @@ package fr.acinq.eclair +import fr.acinq.eclair.channel.Commitments import grizzled.slf4j.Logging import scala.util.{Failure, Success, Try} -object DBCompatChecker extends Logging { +object DBChecker extends Logging { /** - * Tests if the channels data in the DB are compatible with the current version of eclair; throws an exception if incompatible. - * - * @param nodeParams - */ - def checkDBCompatibility(nodeParams: NodeParams): Unit = + * Tests if the channels data in the DB is valid (and throws an exception if not): + * - it is compatible with the current version of eclair + * - channel keys can be re-generated from the channel seed + * + * @param nodeParams node parameters + */ + def checkChannelsDB(nodeParams: NodeParams): Unit = { Try(nodeParams.db.channels.listLocalChannels()) match { - case Success(_) => {} + case Success(channels) => + channels.foreach(data => if (!Commitments.validateSeed(data.commitments, nodeParams.channelKeyManager)) throw InvalidChannelSeedException(data.channelId)) case Failure(_) => throw IncompatibleDBException } + } /** - * Tests if the network database is readable. - * - * @param nodeParams - */ - def checkNetworkDBCompatibility(nodeParams: NodeParams): Unit = + * Tests if the network database is readable. + * + * @param nodeParams + */ + def checkNetworkDB(nodeParams: NodeParams): Unit = Try(nodeParams.db.network.listChannels(), nodeParams.db.network.listNodes()) match { - case Success(_) => {} + case Success(_) => () case Failure(_) => throw IncompatibleNetworkDBException } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala index 7bde159236..446032024f 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala @@ -139,8 +139,8 @@ class Setup(val datadir: File, val serverBindingAddress = new InetSocketAddress(config.getString("server.binding-ip"), config.getInt("server.port")) // early checks - DBCompatChecker.checkDBCompatibility(nodeParams) - DBCompatChecker.checkNetworkDBCompatibility(nodeParams) + DBChecker.checkChannelsDB(nodeParams) + DBChecker.checkNetworkDB(nodeParams) PortChecker.checkAvailable(serverBindingAddress) logger.info(s"nodeid=${nodeParams.nodeId} alias=${nodeParams.alias}") @@ -417,4 +417,6 @@ case object EmptyAPIPasswordException extends RuntimeException("must set a passw case object IncompatibleDBException extends RuntimeException("database is not compatible with this version of eclair") -case object IncompatibleNetworkDBException extends RuntimeException("network database is not compatible with this version of eclair") \ No newline at end of file +case object IncompatibleNetworkDBException extends RuntimeException("network database is not compatible with this version of eclair") + +case class InvalidChannelSeedException(channelId: ByteVector32) extends RuntimeException(s"channel seed has been modified for channel $channelId") \ No newline at end of file diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala index 08a97a77a1..f41dfdbbc9 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala @@ -18,7 +18,7 @@ package fr.acinq.eclair.channel import akka.event.LoggingAdapter import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey, sha256} -import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, Crypto, Satoshi, SatoshiLong} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, Crypto, Satoshi, SatoshiLong, Script} import fr.acinq.eclair._ import fr.acinq.eclair.blockchain.fee.{FeeratePerKw, OnChainFeeConf} import fr.acinq.eclair.channel.Helpers.Closing @@ -294,6 +294,13 @@ case class Commitments(channelId: ByteVector32, object Commitments { + def validateSeed(commitments: Commitments, keyManager: ChannelKeyManager): Boolean = { + val localFundingKey = keyManager.fundingPublicKey(commitments.localParams.fundingKeyPath).publicKey + val remoteFundingKey = commitments.remoteParams.fundingPubKey + val fundingScript = Script.write(Scripts.multiSig2of2(localFundingKey, remoteFundingKey)) + commitments.commitInput.redeemScript == fundingScript + } + /** * Add a change to our proposed change list. * diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/CommitmentsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/CommitmentsSpec.scala index 23ad1c6edc..25ebee2aad 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/CommitmentsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/CommitmentsSpec.scala @@ -17,12 +17,13 @@ package fr.acinq.eclair.channel import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey -import fr.acinq.bitcoin.scalacompat.{ByteVector64, DeterministicWallet, Satoshi, SatoshiLong, Transaction} +import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, ByteVector64, DeterministicWallet, Satoshi, SatoshiLong, Transaction} import fr.acinq.eclair.blockchain.fee._ import fr.acinq.eclair.channel.Commitments._ import fr.acinq.eclair.channel.Helpers.Funding import fr.acinq.eclair.channel.states.ChannelStateTestsBase import fr.acinq.eclair.crypto.ShaChain +import fr.acinq.eclair.crypto.keymanager.LocalChannelKeyManager import fr.acinq.eclair.transactions.CommitmentSpec import fr.acinq.eclair.transactions.Transactions.CommitTx import fr.acinq.eclair.wire.protocol.{IncorrectOrUnknownPaymentDetails, UpdateAddHtlc} @@ -469,6 +470,11 @@ class CommitmentsSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with } } + test("check if channel seed has been modified") { f => + val commitments = f.alice.stateData.asInstanceOf[DATA_NORMAL].commitments + assert(Commitments.validateSeed(commitments, TestConstants.Alice.channelKeyManager)) + assert(!Commitments.validateSeed(commitments, new LocalChannelKeyManager(ByteVector32.fromValidHex("42" * 32), Block.RegtestGenesisBlock.hash))) + } } object CommitmentsSpec { From 7cf2e209ec4a6d604ea1819bde3b2368d69ece3d Mon Sep 17 00:00:00 2001 From: Bastien Teinturier <31281497+t-bast@users.noreply.github.com> Date: Mon, 11 Apr 2022 11:43:22 +0200 Subject: [PATCH 052/121] Remove Phoenix workaround for PaymentSecret dependency (#2230) When Phoenix was based on a branch of eclair, we needed a workaround for invoice backwards-compatibility. Now that Phoenix uses lightning-kmp instead of eclair, we can remove that work-around. --- .../src/main/scala/fr/acinq/eclair/Features.scala | 4 +--- .../src/test/scala/fr/acinq/eclair/FeaturesSpec.scala | 10 +++++----- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala index 504b0ef707..4ee710d505 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala @@ -266,9 +266,7 @@ object Features { // Features may depend on other features, as specified in Bolt 9. private val featuresDependency = Map( ChannelRangeQueriesExtended -> (ChannelRangeQueries :: Nil), - // This dependency requirement was added to the spec after the Phoenix release, which means Phoenix users have "invalid" - // invoices in their payment history. We choose to treat such invoices as valid; this is a harmless spec violation. - // PaymentSecret -> (VariableLengthOnion :: Nil), + PaymentSecret -> (VariableLengthOnion :: Nil), BasicMultiPartPayment -> (PaymentSecret :: Nil), AnchorOutputs -> (StaticRemoteKey :: Nil), AnchorOutputsZeroFeeHtlcTx -> (StaticRemoteKey :: Nil), diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/FeaturesSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/FeaturesSpec.scala index 9915faf482..e3ef702c97 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/FeaturesSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/FeaturesSpec.scala @@ -70,15 +70,15 @@ class FeaturesSpec extends AnyFunSuite { bin"000000000000010000000000" -> false, bin"000000000000100010000000" -> true, bin"000000000000100001000000" -> true, - // payment_secret depends on var_onion_optin, but we allow not setting it to be compatible with Phoenix - bin"000000001000000000000000" -> true, - bin"000000000100000000000000" -> true, + // payment_secret depends on var_onion_optin + bin"000000001000000000000000" -> false, + bin"000000000100000000000000" -> false, bin"000000000100001000000000" -> true, // basic_mpp depends on payment_secret bin"000000100000000000000000" -> false, bin"000000010000000000000000" -> false, - bin"000000101000000000000000" -> true, // we allow not setting var_onion_optin - bin"000000011000000000000000" -> true, // we allow not setting var_onion_optin + bin"000000101000000100000000" -> true, + bin"000000011000000100000000" -> true, bin"000000011000001000000000" -> true, bin"000000100100000100000000" -> true, // option_anchor_outputs depends on option_static_remotekey From 787c51acc27ea23745b4496c5f8ebae81483ba2b Mon Sep 17 00:00:00 2001 From: Fabrice Drouin Date: Wed, 13 Apr 2022 09:48:12 +0200 Subject: [PATCH 053/121] Add a "stop" API method (#2233) * Add a "stop" API method This API call was added for certain uses cases where killing the process was impractical but internally it just calls `sys.exit()`. Eclair is designed to shutdown cleanly when its process is killed and this is still the recommended way of stopping it. --- docs/release-notes/eclair-vnext.md | 1 + eclair-core/eclair-cli | 1 + .../src/main/scala/fr/acinq/eclair/Eclair.scala | 10 ++++++++++ .../scala/fr/acinq/eclair/api/handlers/Node.scala | 6 +++++- .../scala/fr/acinq/eclair/api/ApiServiceSpec.scala | 14 ++++++++++++++ 5 files changed, 31 insertions(+), 1 deletion(-) diff --git a/docs/release-notes/eclair-vnext.md b/docs/release-notes/eclair-vnext.md index 286ac71557..97bd4cc0e1 100644 --- a/docs/release-notes/eclair-vnext.md +++ b/docs/release-notes/eclair-vnext.md @@ -9,6 +9,7 @@ ### API changes - `channelbalances` Retrieves information about the balances of all local channels. (#2196) +- `stop` Stops eclair. Please note that the recommended way of stopping eclair is simply to kill its process (#2233) ### Miscellaneous improvements and bug fixes diff --git a/eclair-core/eclair-cli b/eclair-core/eclair-cli index 553df5a977..22be3fe5f5 100755 --- a/eclair-core/eclair-cli +++ b/eclair-core/eclair-cli @@ -34,6 +34,7 @@ and COMMAND is one of the available commands: - disconnect - peers - audit + - stop === Channel === - open diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala index 6874f8c357..0d6be5469e 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala @@ -159,6 +159,8 @@ trait Eclair { def verifyMessage(message: ByteVector, recoverableSignature: ByteVector): VerifiedMessage def sendOnionMessage(intermediateNodes: Seq[PublicKey], destination: Either[PublicKey, Sphinx.RouteBlinding.BlindedRoute], replyPath: Option[Seq[PublicKey]], userCustomContent: ByteVector)(implicit timeout: Timeout): Future[SendOnionMessageResponse] + + def stop(): Future[Unit] } class EclairImpl(appKit: Kit) extends Eclair with Logging { @@ -559,4 +561,12 @@ class EclairImpl(appKit: Kit) extends Eclair with Logging { case Attempt.Failure(cause) => Future.successful(SendOnionMessageResponse(sent = false, failureMessage = Some(s"the `content` field is invalid, it must contain encoded tlvs: ${cause.message}"), response = None)) } } + + override def stop(): Future[Unit] = { + // README: do not make this smarter or more complex ! + // eclair can simply and cleanly be stopped by killing its process without fear of losing data, payments, ... and it should remain this way. + logger.info("stopping eclair") + sys.exit(0) + Future.successful(()) + } } diff --git a/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Node.scala b/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Node.scala index 28cf82ed24..a3a835015b 100644 --- a/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Node.scala +++ b/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Node.scala @@ -62,5 +62,9 @@ trait Node { } } - val nodeRoutes: Route = getInfo ~ connect ~ disconnect ~ peers ~ audit + val stop: Route = postRequest("stop") { implicit t => + complete(eclairApi.stop()) + } + + val nodeRoutes: Route = getInfo ~ connect ~ disconnect ~ peers ~ audit ~ stop } diff --git a/eclair-node/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala b/eclair-node/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala index f79260a986..fcef0b94e9 100644 --- a/eclair-node/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala +++ b/eclair-node/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala @@ -1185,6 +1185,20 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM } } + test("stop eclair") { + val eclair = mock[Eclair] + val mockService = new MockService(eclair) + eclair.stop() returns Future.successful(()) + + Post("/stop") ~> + addCredentials(BasicHttpCredentials("", mockApi().password)) ~> + Route.seal(mockService.stop) ~> + check { + assert(handled) + assert(status == OK) + } + } + private def matchTestJson(apiName: String, response: String) = { val resource = getClass.getResourceAsStream(s"/api/$apiName") val expectedResponse = Try(Source.fromInputStream(resource).mkString).getOrElse { From c7c515a0edd84fb9fcd6d8680bd7d2660983e891 Mon Sep 17 00:00:00 2001 From: Thomas HUET <81159533+thomash-acinq@users.noreply.github.com> Date: Wed, 13 Apr 2022 11:26:03 +0200 Subject: [PATCH 054/121] Add Bolt12 types and codecs (#2145) Add types for representing offers and Bolt12 invoices (lightning/bolts#798) --- .../main/scala/fr/acinq/eclair/Features.scala | 2 + .../fr/acinq/eclair/db/pg/PgPaymentsDb.scala | 7 +- .../eclair/db/sqlite/SqlitePaymentsDb.scala | 6 +- .../acinq/eclair/payment/Bolt11Invoice.scala | 15 +- .../acinq/eclair/payment/Bolt12Invoice.scala | 183 +++++++++ .../fr/acinq/eclair/payment/Invoice.scala | 9 +- .../acinq/eclair/transactions/Scripts.scala | 2 +- .../eclair/wire/protocol/MessageOnion.scala | 27 ++ .../eclair/wire/protocol/OfferCodecs.scala | 222 ++++++++++ .../acinq/eclair/wire/protocol/Offers.scala | 387 ++++++++++++++++++ .../eclair/wire/protocol/TlvCodecs.scala | 8 +- .../eclair/json/JsonSerializersSpec.scala | 6 +- .../eclair/payment/Bolt11InvoiceSpec.scala | 85 ++-- .../eclair/payment/Bolt12InvoiceSpec.scala | 349 ++++++++++++++++ .../eclair/wire/protocol/OffersSpec.scala | 283 +++++++++++++ .../api/serde/FormParamExtractors.scala | 2 +- .../fr/acinq/eclair/api/ApiServiceSpec.scala | 8 +- pom.xml | 2 +- 18 files changed, 1532 insertions(+), 71 deletions(-) create mode 100644 eclair-core/src/main/scala/fr/acinq/eclair/payment/Bolt12Invoice.scala create mode 100644 eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/OfferCodecs.scala create mode 100644 eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/Offers.scala create mode 100644 eclair-core/src/test/scala/fr/acinq/eclair/payment/Bolt12InvoiceSpec.scala create mode 100644 eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/OffersSpec.scala diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala index 4ee710d505..1e8b2593f9 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala @@ -62,6 +62,8 @@ case class UnknownFeature(bitIndex: Int) case class Features[T <: Feature](activated: Map[T, FeatureSupport], unknown: Set[UnknownFeature] = Set.empty) { + def isEmpty: Boolean = activated.isEmpty && unknown.isEmpty + def hasFeature(feature: T, support: Option[FeatureSupport] = None): Boolean = support match { case Some(s) => activated.get(feature).contains(s) case None => activated.contains(feature) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgPaymentsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgPaymentsDb.scala index ed3c2d6be0..3d16ac937d 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgPaymentsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgPaymentsDb.scala @@ -23,7 +23,7 @@ import fr.acinq.eclair.db.Monitoring.Tags.DbBackends import fr.acinq.eclair.db.PaymentsDb.{decodeFailures, decodeRoute, encodeFailures, encodeRoute} import fr.acinq.eclair.db._ import fr.acinq.eclair.db.pg.PgUtils.PgLock -import fr.acinq.eclair.payment.{Bolt11Invoice, PaymentFailed, Invoice, PaymentSent} +import fr.acinq.eclair.payment.{Bolt11Invoice, Invoice, PaymentFailed, PaymentSent} import fr.acinq.eclair.{MilliSatoshi, TimestampMilli, TimestampMilliLong} import grizzled.slf4j.Logging import scodec.bits.BitVector @@ -32,7 +32,6 @@ import java.sql.{Connection, ResultSet, Statement, Timestamp} import java.time.Instant import java.util.UUID import javax.sql.DataSource -import scala.concurrent.duration.DurationLong import scala.util.{Failure, Success, Try} object PgPaymentsDb { @@ -157,7 +156,7 @@ class PgPaymentsDb(implicit ds: DataSource, lock: PgLock) extends PaymentsDb wit MilliSatoshi(rs.getLong("recipient_amount_msat")), PublicKey(rs.getByteVectorFromHex("recipient_node_id")), TimestampMilli(rs.getTimestamp("created_at").getTime), - rs.getStringNullable("payment_request").map(Invoice.fromString), + rs.getStringNullable("payment_request").map(Invoice.fromString(_).get), status ) } @@ -249,7 +248,7 @@ class PgPaymentsDb(implicit ds: DataSource, lock: PgLock) extends PaymentsDb wit private def parseIncomingPayment(rs: ResultSet): IncomingPayment = { val invoice = rs.getString("payment_request") IncomingPayment( - Bolt11Invoice.fromString(invoice), + Bolt11Invoice.fromString(invoice).get, rs.getByteVector32FromHex("payment_preimage"), rs.getString("payment_type"), TimestampMilli.fromSqlTimestamp(rs.getTimestamp("created_at")), diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala index b3d216a3a2..fa5fd22f7c 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala @@ -23,7 +23,7 @@ import fr.acinq.eclair.db.Monitoring.Tags.DbBackends import fr.acinq.eclair.db.PaymentsDb.{decodeFailures, decodeRoute, encodeFailures, encodeRoute} import fr.acinq.eclair.db._ import fr.acinq.eclair.db.sqlite.SqliteUtils._ -import fr.acinq.eclair.payment.{Bolt11Invoice, PaymentFailed, Invoice, PaymentSent} +import fr.acinq.eclair.payment.{Bolt11Invoice, Invoice, PaymentFailed, PaymentSent} import fr.acinq.eclair.{MilliSatoshi, TimestampMilli, TimestampMilliLong} import grizzled.slf4j.Logging import scodec.bits.BitVector @@ -178,7 +178,7 @@ class SqlitePaymentsDb(val sqlite: Connection) extends PaymentsDb with Logging { MilliSatoshi(rs.getLong("recipient_amount_msat")), PublicKey(rs.getByteVector("recipient_node_id")), TimestampMilli(rs.getLong("created_at")), - rs.getStringNullable("payment_request").map(Invoice.fromString), + rs.getStringNullable("payment_request").map(Invoice.fromString(_).get), status ) } @@ -256,7 +256,7 @@ class SqlitePaymentsDb(val sqlite: Connection) extends PaymentsDb with Logging { private def parseIncomingPayment(rs: ResultSet): IncomingPayment = { val invoice = rs.getString("payment_request") IncomingPayment( - Bolt11Invoice.fromString(invoice), + Bolt11Invoice.fromString(invoice).get, rs.getByteVector32("payment_preimage"), rs.getString("payment_type"), TimestampMilli(rs.getLong("created_at")), diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Bolt11Invoice.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Bolt11Invoice.scala index 6e2e0630c5..2bcab282c4 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Bolt11Invoice.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Bolt11Invoice.scala @@ -17,7 +17,7 @@ package fr.acinq.eclair.payment import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} -import fr.acinq.bitcoin.scalacompat.{ Block, ByteVector32, ByteVector64, Crypto} +import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, ByteVector64, Crypto} import fr.acinq.bitcoin.{Base58, Base58Check, Bech32} import fr.acinq.eclair.{CltvExpiryDelta, Feature, FeatureSupport, Features, InvoiceFeature, MilliSatoshi, MilliSatoshiLong, ShortChannelId, TimestampSecond, randomBytes32} import scodec.bits.{BitVector, ByteOrdering, ByteVector} @@ -30,7 +30,7 @@ import scala.util.{Failure, Success, Try} /** * Lightning Bolt 11 invoice - * see https://github.com/lightningnetwork/lightning-rfc/blob/master/11-payment-encoding.md + * see https://github.com/lightning/bolts/blob/master/11-payment-encoding.md * * @param prefix currency prefix; lnbc for bitcoin, lntb for bitcoin testnet * @param amount_opt amount to pay (empty string means no amount is specified) @@ -46,8 +46,11 @@ case class Bolt11Invoice(prefix: String, amount_opt: Option[MilliSatoshi], creat amount_opt.foreach(a => require(a > 0.msat, s"amount is not valid")) require(tags.collect { case _: Bolt11Invoice.PaymentHash => }.size == 1, "there must be exactly one payment hash tag") require(tags.collect { case Bolt11Invoice.Description(_) | Bolt11Invoice.DescriptionHash(_) => }.size == 1, "there must be exactly one description tag or one description hash tag") - private val featuresErr = Features.validateFeatureGraph(features) - require(featuresErr.isEmpty, featuresErr.map(_.message)) + + { + val featuresErr = Features.validateFeatureGraph(features) + require(featuresErr.isEmpty, featuresErr.map(_.message)) + } if (features.hasFeature(Features.PaymentSecret)) { require(tags.collect { case _: Bolt11Invoice.PaymentSecret => }.size == 1, "there must be exactly one payment secret tag when feature bit is set") } @@ -508,13 +511,13 @@ object Bolt11Invoice { // TODO: could be optimized by preallocating the resulting buffer def string2Bits(data: String): BitVector = data.map(charToint5).foldLeft(BitVector.empty)(_ ++ _) - val eight2fiveCodec: Codec[List[Byte]] = list(ubyte(5)) + val eight2fiveCodec: Codec[List[java.lang.Byte]] = list(ubyte(5).xmap[java.lang.Byte](b => b, b => b)) /** * @param input bech32-encoded invoice * @return a Bolt11 invoice */ - def fromString(input: String): Bolt11Invoice = { + def fromString(input: String): Try[Bolt11Invoice] = Try { // used only for data validation Bech32.decode(input, false) val lowercaseInput = input.toLowerCase diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Bolt12Invoice.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Bolt12Invoice.scala new file mode 100644 index 0000000000..5d62cf3451 --- /dev/null +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Bolt12Invoice.scala @@ -0,0 +1,183 @@ +/* + * 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.payment + +import fr.acinq.bitcoin.Bech32 +import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} +import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, ByteVector64, Crypto} +import fr.acinq.eclair.crypto.Sphinx.RouteBlinding +import fr.acinq.eclair.payment.Bolt11Invoice.ExtraHop +import fr.acinq.eclair.wire.protocol.OfferCodecs.{invoiceCodec, invoiceTlvCodec} +import fr.acinq.eclair.wire.protocol.Offers._ +import fr.acinq.eclair.wire.protocol.{Offers, TlvStream} +import fr.acinq.eclair.{CltvExpiryDelta, Features, InvoiceFeature, MilliSatoshi, TimestampSecond} +import scodec.bits.ByteVector + +import java.util.concurrent.TimeUnit +import scala.concurrent.duration.FiniteDuration +import scala.util.Try + +/** + * Lightning Bolt 12 invoice + * see https://github.com/lightning/bolts/blob/master/12-offer-encoding.md + */ +case class Bolt12Invoice(records: TlvStream[InvoiceTlv], nodeId_opt: Option[PublicKey]) extends Invoice { + + import Bolt12Invoice._ + + require(records.get[Amount].nonEmpty, "bolt 12 invoices must provide an amount") + require(records.get[NodeId].nonEmpty, "bolt 12 invoices must provide a node id") + require(records.get[PaymentHash].nonEmpty, "bolt 12 invoices must provide a payment hash") + require(records.get[Description].nonEmpty, "bolt 12 invoices must provide a description") + require(records.get[CreatedAt].nonEmpty, "bolt 12 invoices must provide a creation timestamp") + require(records.get[Signature].nonEmpty, "bolt 12 invoices must provide a signature") + + val amount: MilliSatoshi = records.get[Amount].map(_.amount).get + + override val amount_opt: Option[MilliSatoshi] = Some(amount) + + override val nodeId: Crypto.PublicKey = nodeId_opt.getOrElse(records.get[NodeId].get.nodeId1) + + override val paymentHash: ByteVector32 = records.get[PaymentHash].get.hash + + override val paymentSecret: Option[ByteVector32] = None + + override val paymentMetadata: Option[ByteVector] = None + + override val description: Either[String, ByteVector32] = Left(records.get[Description].get.description) + + override val routingInfo: Seq[Seq[ExtraHop]] = Seq.empty + + override val createdAt: TimestampSecond = records.get[CreatedAt].get.timestamp + + override val relativeExpiry: FiniteDuration = FiniteDuration(records.get[RelativeExpiry].map(_.seconds).getOrElse(DEFAULT_EXPIRY_SECONDS), TimeUnit.SECONDS) + + override val minFinalCltvExpiryDelta: CltvExpiryDelta = records.get[Cltv].map(_.minFinalCltvExpiry).getOrElse(DEFAULT_MIN_FINAL_EXPIRY_DELTA) + + override val features: Features[InvoiceFeature] = records.get[FeaturesTlv].map(_.features.invoiceFeatures()).getOrElse(Features.empty) + + override def toString: String = { + val data = invoiceCodec.encode(this).require.bytes + Bech32.encodeBytes(hrp, data.toArray, Bech32.Encoding.Beck32WithoutChecksum) + } + + val chain: ByteVector32 = records.get[Chain].map(_.hash).getOrElse(Block.LivenetGenesisBlock.hash) + + val offerId: Option[ByteVector32] = records.get[OfferId].map(_.offerId) + + val blindedPaths: Option[Seq[RouteBlinding.BlindedRoute]] = records.get[Paths].map(_.paths) + + val issuer: Option[String] = records.get[Issuer].map(_.issuer) + + val quantity: Option[Long] = records.get[Quantity].map(_.quantity) + + val refundFor: Option[ByteVector32] = records.get[RefundFor].map(_.refundedPaymentHash) + + val payerKey: Option[ByteVector32] = records.get[PayerKey].map(_.publicKey) + + val payerNote: Option[String] = records.get[PayerNote].map(_.note) + + val payerInfo: Option[ByteVector] = records.get[PayerInfo].map(_.info) + + val fallbacks: Option[Seq[FallbackAddress]] = records.get[Fallbacks].map(_.addresses) + + val refundSignature: Option[ByteVector64] = records.get[RefundSignature].map(_.signature) + + val replaceInvoice: Option[ByteVector32] = records.get[ReplaceInvoice].map(_.paymentHash) + + val signature: ByteVector64 = records.get[Signature].get.signature + + // It is assumed that the request is valid for this offer. + def isValidFor(offer: Offer, request: InvoiceRequest): Boolean = { + Offers.xOnlyPublicKey(nodeId) == offer.nodeIdXOnly && + checkSignature() && + offerId.contains(request.offerId) && + request.chain == chain && + !isExpired() && + request.amount.contains(amount) && + quantity == request.quantity_opt && + payerKey.contains(request.payerKey) && + payerInfo == request.payerInfo && + // Bolt 12: MUST reject the invoice if payer_note is set, and was unset or not equal to the field in the invoice_request. + payerNote.forall(request.payerNote.contains(_)) && + description.swap.exists(_.startsWith(offer.description)) && + issuer == offer.issuer && + request.features.areSupported(features) + } + + def checkRefundSignature(): Boolean = { + (refundSignature, refundFor, payerKey) match { + case (Some(sig), Some(hash), Some(key)) => verifySchnorr(signatureTag("payer_signature"), hash, sig, key) + case _ => false + } + } + + def checkSignature(): Boolean = { + verifySchnorr(signatureTag("signature"), rootHash(Offers.removeSignature(records), invoiceTlvCodec), signature, Offers.xOnlyPublicKey(nodeId)) + } + + def withNodeId(nodeId: PublicKey): Bolt12Invoice = Bolt12Invoice(records, Some(nodeId)) +} + +object Bolt12Invoice { + val hrp = "lni" + val DEFAULT_EXPIRY_SECONDS: Long = 7200 + val DEFAULT_MIN_FINAL_EXPIRY_DELTA: CltvExpiryDelta = CltvExpiryDelta(18) + + /** + * Creates an invoice for a given offer and invoice request. + * + * @param offer the offer this invoice corresponds to + * @param request the request this invoice responds to + * @param preimage the preimage to use for the payment + * @param nodeKey the key that was used to generate the offer, may be different from our public nodeId if we're hiding behind a blinded route + * @param features invoice features + */ + def apply(offer: Offer, request: InvoiceRequest, preimage: ByteVector32, nodeKey: PrivateKey, features: Features[InvoiceFeature]): Bolt12Invoice = { + require(request.amount.nonEmpty || offer.amount.nonEmpty) + val tlvs: Seq[InvoiceTlv] = Seq( + Some(Chain(request.chain)), + Some(CreatedAt(TimestampSecond.now())), + Some(PaymentHash(Crypto.sha256(preimage))), + Some(OfferId(offer.offerId)), + Some(NodeId(nodeKey.publicKey)), + Some(Amount(request.amount.orElse(offer.amount.map(_ * request.quantity)).get)), + Some(Description(offer.description)), + request.quantity_opt.map(Quantity), + Some(PayerKey(request.payerKey)), + request.payerInfo.map(PayerInfo), + request.payerNote.map(PayerNote), + request.replaceInvoice.map(ReplaceInvoice), + offer.issuer.map(Issuer), + Some(FeaturesTlv(features.unscoped())) + ).flatten + val signature = signSchnorr(signatureTag("signature"), rootHash(TlvStream(tlvs), invoiceTlvCodec), nodeKey) + Bolt12Invoice(TlvStream(tlvs :+ Signature(signature)), Some(nodeKey.publicKey)) + } + + def signatureTag(fieldName: String): String = "lightning" + "invoice" + fieldName + + def fromString(input: String): Try[Bolt12Invoice] = Try { + val triple = Bech32.decodeBytes(input.toLowerCase, true) + val prefix = triple.getFirst + val encoded = triple.getSecond + val encoding = triple.getThird + require(prefix == hrp) + require(encoding == Bech32.Encoding.Beck32WithoutChecksum) + invoiceCodec.decode(ByteVector(encoded).bits).require.value + } +} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Invoice.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Invoice.scala index eebe76e4ee..6060904ae4 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Invoice.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Invoice.scala @@ -22,6 +22,7 @@ import fr.acinq.eclair.{CltvExpiryDelta, Features, InvoiceFeature, MilliSatoshi, import scodec.bits.ByteVector import scala.concurrent.duration.FiniteDuration +import scala.util.Try trait Invoice { val amount_opt: Option[MilliSatoshi] @@ -52,7 +53,11 @@ trait Invoice { } object Invoice { - def fromString(input: String): Invoice = { - Bolt11Invoice.fromString(input) + def fromString(input: String): Try[Invoice] = { + if (input.toLowerCase.startsWith("lni")) { + Bolt12Invoice.fromString(input) + } else { + Bolt11Invoice.fromString(input) + } } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Scripts.scala b/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Scripts.scala index 73213b0210..fdccd4c0a1 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Scripts.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Scripts.scala @@ -72,7 +72,7 @@ object Scripts { def encodeNumber(n: Long): ScriptElt = n match { case 0 => OP_0 case -1 => OP_1NEGATE - case x if x >= 1 && x <= 16 => ScriptElt.code2elt((ScriptElt.elt2code(OP_1) + x - 1).toInt) + case x if x >= 1 && x <= 16 => ScriptElt.code2elt((ScriptElt.elt2code(OP_1) + x - 1).toInt).get case _ => OP_PUSHDATA(Script.encodeNumber(n)) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/MessageOnion.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/MessageOnion.scala index 2eae728d1c..037e42446c 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/MessageOnion.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/MessageOnion.scala @@ -19,6 +19,8 @@ package fr.acinq.eclair.wire.protocol import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.eclair.UInt64 import fr.acinq.eclair.crypto.Sphinx.RouteBlinding.{BlindedNode, BlindedRoute} +import fr.acinq.eclair.payment.Bolt12Invoice +import fr.acinq.eclair.wire.protocol.OfferCodecs.{invoiceCodec, invoiceErrorCodec, invoiceRequestCodec} import fr.acinq.eclair.wire.protocol.OnionRoutingCodecs.{ForbiddenTlv, MissingRequiredTlv} import scodec.bits.ByteVector @@ -40,6 +42,24 @@ object OnionMessagePayloadTlv { */ case class EncryptedData(data: ByteVector) extends OnionMessagePayloadTlv + /** + * In order to pay a Bolt 12 offer, we must send an onion message to request an invoice corresponding to that offer. + * The creator of the offer will send us an invoice back through our blinded reply path. + */ + case class InvoiceRequest(request: Offers.InvoiceRequest) extends OnionMessagePayloadTlv + + /** + * When receiving an invoice request, we must send an onion message back containing an invoice corresponding to the + * requested offer (if it was an offer we published). + */ + case class Invoice(invoice: Bolt12Invoice) extends OnionMessagePayloadTlv + + /** + * This message may be used when we receive an invalid invoice or invoice request. + * It contains information helping senders figure out why their message was invalid. + */ + case class InvoiceError(error: Offers.InvoiceError) extends OnionMessagePayloadTlv + } object MessageOnion { @@ -62,6 +82,9 @@ object MessageOnion { case class FinalPayload(records: TlvStream[OnionMessagePayloadTlv]) extends PerHopPayload { val replyPath: Option[OnionMessagePayloadTlv.ReplyPath] = records.get[OnionMessagePayloadTlv.ReplyPath] val encryptedData: ByteVector = records.get[OnionMessagePayloadTlv.EncryptedData].get.data + val invoiceRequest: Option[OnionMessagePayloadTlv.InvoiceRequest] = records.get[OnionMessagePayloadTlv.InvoiceRequest] + val invoice: Option[OnionMessagePayloadTlv.Invoice] = records.get[OnionMessagePayloadTlv.Invoice] + val invoiceError: Option[OnionMessagePayloadTlv.InvoiceError] = records.get[OnionMessagePayloadTlv.InvoiceError] } /** Content of the encrypted data of a final node's per-hop payload. */ @@ -90,6 +113,10 @@ object MessageOnionCodecs { private val onionTlvCodec = discriminated[OnionMessagePayloadTlv].by(varint) .typecase(UInt64(2), replyPathCodec) .typecase(UInt64(4), encryptedDataCodec) + .typecase(UInt64(64), variableSizeBytesLong(varintoverflow, invoiceRequestCodec.as[InvoiceRequest])) + .typecase(UInt64(66), variableSizeBytesLong(varintoverflow, invoiceCodec.as[Invoice])) + .typecase(UInt64(68), variableSizeBytesLong(varintoverflow, invoiceErrorCodec.as[InvoiceError])) + val perHopPayloadCodec: Codec[TlvStream[OnionMessagePayloadTlv]] = TlvCodecs.lengthPrefixedTlvStream[OnionMessagePayloadTlv](onionTlvCodec).complete diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/OfferCodecs.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/OfferCodecs.scala new file mode 100644 index 0000000000..81bb9d5c58 --- /dev/null +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/OfferCodecs.scala @@ -0,0 +1,222 @@ +/* + * 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.wire.protocol + +import fr.acinq.bitcoin.scalacompat.ByteVector32 +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey +import fr.acinq.eclair.crypto.Sphinx.RouteBlinding.{BlindedNode, BlindedRoute} +import fr.acinq.eclair.payment.Bolt12Invoice +import fr.acinq.eclair.wire.protocol.CommonCodecs._ +import fr.acinq.eclair.wire.protocol.Offers._ +import fr.acinq.eclair.wire.protocol.OnionRoutingCodecs.MissingRequiredTlv +import fr.acinq.eclair.wire.protocol.TlvCodecs.{tmillisatoshi, tu32, tu64overflow} +import fr.acinq.eclair.{CltvExpiryDelta, Feature, Features, TimestampSecond, UInt64} +import scodec.codecs._ +import scodec.{Attempt, Codec} + +object OfferCodecs { + private val chains: Codec[Chains] = variableSizeBytesLong(varintoverflow, list(bytes32)).xmap[Seq[ByteVector32]](_.toSeq, _.toList).as[Chains] + + private val currency: Codec[Currency] = variableSizeBytesLong(varintoverflow, utf8).as[Currency] + + private val amount: Codec[Amount] = variableSizeBytesLong(varintoverflow, tmillisatoshi).as[Amount] + + private val description: Codec[Description] = variableSizeBytesLong(varintoverflow, utf8).as[Description] + + private val features: Codec[FeaturesTlv] = variableSizeBytesLong(varintoverflow, bytes).xmap[Features[Feature]](Features(_), _.toByteVector).as[FeaturesTlv] + + private val absoluteExpiry: Codec[AbsoluteExpiry] = variableSizeBytesLong(varintoverflow, tu64overflow).as[TimestampSecond].as[AbsoluteExpiry] + + private val blindedNodeCodec: Codec[BlindedNode] = (("nodeId" | publicKey) :: ("encryptedData" | variableSizeBytes(uint16, bytes))).as[BlindedNode] + + private val pathCodec: Codec[BlindedRoute] = (("firstNodeId" | publicKey) :: ("blinding" | publicKey) :: ("path" | listOfN(uint8, blindedNodeCodec).xmap[Seq[BlindedNode]](_.toSeq, _.toList))).as[BlindedRoute] + + private val paths: Codec[Paths] = variableSizeBytesLong(varintoverflow, list(pathCodec)).xmap[Seq[BlindedRoute]](_.toSeq, _.toList).as[Paths] + + private val issuer: Codec[Issuer] = variableSizeBytesLong(varintoverflow, utf8).as[Issuer] + + private val quantityMin: Codec[QuantityMin] = variableSizeBytesLong(varintoverflow, tu64overflow).as[QuantityMin] + + private val quantityMax: Codec[QuantityMax] = variableSizeBytesLong(varintoverflow, tu64overflow).as[QuantityMax] + + private val nodeId: Codec[NodeId] = variableSizeBytesLong(varintoverflow, bytes32).as[NodeId] + + private val sendInvoice: Codec[SendInvoice] = variableSizeBytesLong(varintoverflow, provide(SendInvoice())) + + private val refundFor: Codec[RefundFor] = variableSizeBytesLong(varintoverflow, bytes32).as[RefundFor] + + private val signature: Codec[Signature] = variableSizeBytesLong(varintoverflow, bytes64).as[Signature] + + val offerTlvCodec: Codec[TlvStream[OfferTlv]] = TlvCodecs.tlvStream[OfferTlv](discriminated[OfferTlv].by(varint) + .typecase(UInt64(2), chains) + .typecase(UInt64(6), currency) + .typecase(UInt64(8), amount) + .typecase(UInt64(10), description) + .typecase(UInt64(12), features) + .typecase(UInt64(14), absoluteExpiry) + .typecase(UInt64(16), paths) + .typecase(UInt64(20), issuer) + .typecase(UInt64(22), quantityMin) + .typecase(UInt64(24), quantityMax) + .typecase(UInt64(30), nodeId) + .typecase(UInt64(34), refundFor) + .typecase(UInt64(54), sendInvoice) + .typecase(UInt64(240), signature)).complete + + val offerCodec: Codec[Offer] = offerTlvCodec.narrow({ tlvs => + if (tlvs.get[Description].isEmpty) { + Attempt.failure(MissingRequiredTlv(UInt64(10))) + } + if (tlvs.get[NodeId].isEmpty) { + Attempt.failure(MissingRequiredTlv(UInt64(30))) + } + Attempt.successful(Offer(tlvs)) + }, { + case Offer(tlvs) => tlvs + }) + + private val chain: Codec[Chain] = variableSizeBytesLong(varintoverflow, bytes32).as[Chain] + + private val offerId: Codec[OfferId] = variableSizeBytesLong(varintoverflow, bytes32).as[OfferId] + + private val quantity: Codec[Quantity] = variableSizeBytesLong(varintoverflow, tu64overflow).as[Quantity] + + private val payerKey: Codec[PayerKey] = variableSizeBytesLong(varintoverflow, bytes32).as[PayerKey] + + private val payerNote: Codec[PayerNote] = variableSizeBytesLong(varintoverflow, utf8).as[PayerNote] + + private val payerInfo: Codec[PayerInfo] = variableSizeBytesLong(varintoverflow, bytes).as[PayerInfo] + + private val replaceInvoice: Codec[ReplaceInvoice] = variableSizeBytesLong(varintoverflow, bytes32).as[ReplaceInvoice] + + val invoiceRequestTlvCodec: Codec[TlvStream[InvoiceRequestTlv]] = TlvCodecs.tlvStream[InvoiceRequestTlv](discriminated[InvoiceRequestTlv].by(varint) + .typecase(UInt64(3), chain) + .typecase(UInt64(4), offerId) + .typecase(UInt64(8), amount) + .typecase(UInt64(12), features) + .typecase(UInt64(32), quantity) + .typecase(UInt64(38), payerKey) + .typecase(UInt64(39), payerNote) + .typecase(UInt64(50), payerInfo) + .typecase(UInt64(56), replaceInvoice) + .typecase(UInt64(240), signature)).complete + + val invoiceRequestCodec: Codec[InvoiceRequest] = invoiceRequestTlvCodec.narrow({ tlvs => + if (tlvs.get[OfferId].isEmpty) { + Attempt.failure(MissingRequiredTlv(UInt64(4))) + } else if (tlvs.get[PayerKey].isEmpty) { + Attempt.failure(MissingRequiredTlv(UInt64(38))) + } else if (tlvs.get[Signature].isEmpty) { + Attempt.failure(MissingRequiredTlv(UInt64(240))) + } else { + Attempt.successful(InvoiceRequest(tlvs)) + } + }, { + case InvoiceRequest(tlvs) => tlvs + }) + + private val paymentInfo: Codec[PaymentInfo] = (("fee_base_msat" | millisatoshi32) :: + ("fee_proportional_millionths" | tu32) :: + ("cltv_expiry_delta" | cltvExpiryDelta)).as[PaymentInfo] + + private val paymentPathsInfo: Codec[PaymentPathsInfo] = variableSizeBytesLong(varintoverflow, list(paymentInfo)).xmap[Seq[PaymentInfo]](_.toSeq, _.toList).as[PaymentPathsInfo] + + private val paymentConstraints: Codec[PaymentConstraints] = (("max_cltv_expiry" | cltvExpiry) :: + ("htlc_minimum_msat" | millisatoshi) :: + ("allowed_features" | bytes.xmap[Features[Feature]](Features(_), _.toByteVector))).as[PaymentConstraints] + + private val paymentPathsConstraints: Codec[PaymentPathsConstraints] = variableSizeBytesLong(varintoverflow, list(paymentConstraints)).xmap[Seq[PaymentConstraints]](_.toSeq, _.toList).as[PaymentPathsConstraints] + + private val createdAt: Codec[CreatedAt] = variableSizeBytesLong(varintoverflow, tu64overflow).as[TimestampSecond].as[CreatedAt] + + private val paymentHash: Codec[PaymentHash] = variableSizeBytesLong(varintoverflow, bytes32).as[PaymentHash] + + private val relativeExpiry: Codec[RelativeExpiry] = variableSizeBytesLong(varintoverflow, tu32).as[RelativeExpiry] + + private val cltv: Codec[Cltv] = variableSizeBytesLong(varintoverflow, uint16).as[CltvExpiryDelta].as[Cltv] + + private val fallbackAddress: Codec[FallbackAddress] = variableSizeBytesLong(varintoverflow, + ("version" | byte) :: + ("address" | variableSizeBytes(uint16, bytes))).as[FallbackAddress] + + private val fallbacks: Codec[Fallbacks] = variableSizeBytesLong(varintoverflow, list(fallbackAddress)).xmap[Seq[FallbackAddress]](_.toSeq, _.toList).as[Fallbacks] + + private val refundSignature: Codec[RefundSignature] = variableSizeBytesLong(varintoverflow, bytes64).as[RefundSignature] + + val invoiceTlvCodec: Codec[TlvStream[InvoiceTlv]] = TlvCodecs.tlvStream[InvoiceTlv](discriminated[InvoiceTlv].by(varint) + .typecase(UInt64(3), chain) + .typecase(UInt64(4), offerId) + .typecase(UInt64(8), amount) + .typecase(UInt64(10), description) + .typecase(UInt64(12), features) + // TODO: the spec for payment paths is not final, adjust codecs if changes are made to he spec. + .typecase(UInt64(16), paths) + .typecase(UInt64(18), paymentPathsInfo) + .typecase(UInt64(19), paymentPathsConstraints) + .typecase(UInt64(20), issuer) + .typecase(UInt64(30), nodeId) + .typecase(UInt64(32), quantity) + .typecase(UInt64(34), refundFor) + .typecase(UInt64(38), payerKey) + .typecase(UInt64(39), payerNote) + .typecase(UInt64(40), createdAt) + .typecase(UInt64(42), paymentHash) + .typecase(UInt64(44), relativeExpiry) + .typecase(UInt64(46), cltv) + .typecase(UInt64(48), fallbacks) + .typecase(UInt64(50), payerInfo) + .typecase(UInt64(52), refundSignature) + .typecase(UInt64(56), replaceInvoice) + .typecase(UInt64(240), signature) + ).complete + + val invoiceCodec: Codec[Bolt12Invoice] = invoiceTlvCodec.narrow({ tlvs => + if (tlvs.get[Amount].isEmpty) { + Attempt.failure(MissingRequiredTlv(UInt64(8))) + } else if (tlvs.get[Description].isEmpty) { + Attempt.failure(MissingRequiredTlv(UInt64(10))) + } else if (tlvs.get[NodeId].isEmpty) { + Attempt.failure(MissingRequiredTlv(UInt64(30))) + } else if (tlvs.get[CreatedAt].isEmpty) { + Attempt.failure(MissingRequiredTlv(UInt64(40))) + } else if (tlvs.get[PaymentHash].isEmpty) { + Attempt.failure(MissingRequiredTlv(UInt64(42))) + } else if (tlvs.get[Signature].isEmpty) { + Attempt.failure(MissingRequiredTlv(UInt64(240))) + } else { + Attempt.successful(Bolt12Invoice(tlvs, None)) + } + }, { + case Bolt12Invoice(tlvs, _) => tlvs + }) + + val invoiceErrorTlvCodec: Codec[TlvStream[InvoiceErrorTlv]] = TlvCodecs.tlvStream[InvoiceErrorTlv](discriminated[InvoiceErrorTlv].by(varint) + .typecase(UInt64(1), variableSizeBytesLong(varintoverflow, tu64overflow).as[ErroneousField]) + .typecase(UInt64(3), variableSizeBytesLong(varintoverflow, bytes).as[SuggestedValue]) + .typecase(UInt64(5), variableSizeBytesLong(varintoverflow, utf8).as[Error]) + ).complete + + val invoiceErrorCodec: Codec[InvoiceError] = invoiceErrorTlvCodec.narrow({ tlvs => + if (tlvs.get[Error].isEmpty) { + Attempt.failure(MissingRequiredTlv(UInt64(5))) + } else { + Attempt.successful(InvoiceError(tlvs)) + } + }, { + case InvoiceError(tlvs) => tlvs + }) +} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/Offers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/Offers.scala new file mode 100644 index 0000000000..99e8e79e25 --- /dev/null +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/Offers.scala @@ -0,0 +1,387 @@ +/* + * 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.wire.protocol + +import fr.acinq.bitcoin.Bech32 +import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} +import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, ByteVector64, Crypto, LexicographicalOrdering} +import fr.acinq.eclair.crypto.Sphinx.RouteBlinding.BlindedRoute +import fr.acinq.eclair.message.OnionMessages +import fr.acinq.eclair.wire.protocol.OfferCodecs._ +import fr.acinq.eclair.wire.protocol.TlvCodecs.genericTlv +import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, Feature, Features, InvoiceFeature, MilliSatoshi, TimestampSecond} +import fr.acinq.secp256k1.Secp256k1JvmKt +import scodec.Codec +import scodec.bits.ByteVector +import scodec.codecs.vector + +import scala.util.Try + +/** + * Lightning Bolt 12 offers + * see https://github.com/lightning/bolts/blob/master/12-offer-encoding.md + */ +object Offers { + + sealed trait Bolt12Tlv extends Tlv + + sealed trait OfferTlv extends Bolt12Tlv + + sealed trait InvoiceRequestTlv extends Bolt12Tlv + + sealed trait InvoiceTlv extends Bolt12Tlv + + sealed trait InvoiceErrorTlv extends Bolt12Tlv + + case class Chains(chains: Seq[ByteVector32]) extends OfferTlv + + case class Currency(iso4217: String) extends OfferTlv + + case class Amount(amount: MilliSatoshi) extends OfferTlv with InvoiceRequestTlv with InvoiceTlv + + case class Description(description: String) extends OfferTlv with InvoiceTlv + + case class FeaturesTlv(features: Features[Feature]) extends OfferTlv with InvoiceRequestTlv with InvoiceTlv + + case class AbsoluteExpiry(absoluteExpiry: TimestampSecond) extends OfferTlv + + case class Paths(paths: Seq[BlindedRoute]) extends OfferTlv with InvoiceTlv + + case class PaymentInfo(feeBase: MilliSatoshi, feeProportionalMillionths: Long, cltvExpiryDelta: CltvExpiryDelta) + case class PaymentPathsInfo(paymentInfo: Seq[PaymentInfo]) extends InvoiceTlv + + case class PaymentConstraints(maxCltvExpiry: CltvExpiry, minHtlc: MilliSatoshi, allowedFeatures: Features[Feature]) + case class PaymentPathsConstraints(paymentConstraints: Seq[PaymentConstraints]) extends InvoiceTlv + + case class Issuer(issuer: String) extends OfferTlv with InvoiceTlv + + case class QuantityMin(min: Long) extends OfferTlv + + case class QuantityMax(max: Long) extends OfferTlv + + case class NodeId(xonly: ByteVector32) extends OfferTlv with InvoiceTlv { + val nodeId1: PublicKey = PublicKey(2 +: xonly) + val nodeId2: PublicKey = PublicKey(3 +: xonly) + } + + object NodeId { + def apply(publicKey: PublicKey): NodeId = NodeId(xOnlyPublicKey(publicKey)) + } + + case class SendInvoice() extends OfferTlv with InvoiceTlv + + case class RefundFor(refundedPaymentHash: ByteVector32) extends OfferTlv with InvoiceTlv + + case class Signature(signature: ByteVector64) extends OfferTlv with InvoiceRequestTlv with InvoiceTlv + + case class Chain(hash: ByteVector32) extends InvoiceRequestTlv with InvoiceTlv + + case class OfferId(offerId: ByteVector32) extends InvoiceRequestTlv with InvoiceTlv + + case class Quantity(quantity: Long) extends InvoiceRequestTlv with InvoiceTlv + + case class PayerKey(publicKey: ByteVector32) extends InvoiceRequestTlv with InvoiceTlv + + object PayerKey { + def apply(publicKey: PublicKey): PayerKey = PayerKey(xOnlyPublicKey(publicKey)) + } + + case class PayerNote(note: String) extends InvoiceRequestTlv with InvoiceTlv + + case class PayerInfo(info: ByteVector) extends InvoiceRequestTlv with InvoiceTlv + + case class ReplaceInvoice(paymentHash: ByteVector32) extends InvoiceRequestTlv with InvoiceTlv + + case class CreatedAt(timestamp: TimestampSecond) extends InvoiceTlv + + case class PaymentHash(hash: ByteVector32) extends InvoiceTlv + + case class RelativeExpiry(seconds: Long) extends InvoiceTlv + + case class Cltv(minFinalCltvExpiry: CltvExpiryDelta) extends InvoiceTlv + + case class FallbackAddress(version: Byte, value: ByteVector) + + case class Fallbacks(addresses: Seq[FallbackAddress]) extends InvoiceTlv + + case class RefundSignature(signature: ByteVector64) extends InvoiceTlv + + case class ErroneousField(tag: Long) extends InvoiceErrorTlv + + case class SuggestedValue(value: ByteVector) extends InvoiceErrorTlv + + case class Error(message: String) extends InvoiceErrorTlv + + case class Offer(records: TlvStream[OfferTlv]) { + + require(records.get[NodeId].nonEmpty, "bolt 12 offers must provide a node id") + require(records.get[Description].nonEmpty, "bolt 12 offers must provide a description") + + val offerId: ByteVector32 = rootHash(removeSignature(records), offerTlvCodec) + + val chains: Seq[ByteVector32] = records.get[Chains].map(_.chains).getOrElse(Seq(Block.LivenetGenesisBlock.hash)) + + val currency: Option[String] = records.get[Currency].map(_.iso4217) + + val amount: Option[MilliSatoshi] = currency match { + case Some(_) => None // TODO: add exchange rates + case None => records.get[Amount].map(_.amount) + } + + val description: String = records.get[Description].get.description + + val features: Features[InvoiceFeature] = records.get[FeaturesTlv].map(_.features.invoiceFeatures()).getOrElse(Features.empty) + + val expiry: Option[TimestampSecond] = records.get[AbsoluteExpiry].map(_.absoluteExpiry) + + val issuer: Option[String] = records.get[Issuer].map(_.issuer) + + val quantityMin: Option[Long] = records.get[QuantityMin].map(_.min) + val quantityMax: Option[Long] = records.get[QuantityMax].map(_.max) + + val nodeIdXOnly: ByteVector32 = records.get[NodeId].get.xonly + + val sendInvoice: Boolean = records.get[SendInvoice].nonEmpty + + val refundFor: Option[ByteVector32] = records.get[RefundFor].map(_.refundedPaymentHash) + + val signature: Option[ByteVector64] = records.get[Signature].map(_.signature) + + val contact: Seq[OnionMessages.Destination] = + records.get[Paths].flatMap(_.paths.headOption).map(OnionMessages.BlindedPath).map(Seq(_)) + .getOrElse(Seq(PublicKey(2.toByte +: nodeIdXOnly), PublicKey(3.toByte +: nodeIdXOnly)).map(nodeId => OnionMessages.Recipient(nodeId, None, None))) + + def sign(key: PrivateKey): Offer = { + val sig = signSchnorr(Offer.signatureTag, rootHash(records, offerTlvCodec), key) + Offer(TlvStream[OfferTlv](records.records ++ Seq(Signature(sig)), records.unknown)) + } + + def checkSignature(): Boolean = { + signature match { + case Some(sig) => verifySchnorr(Offer.signatureTag, rootHash(removeSignature(records), offerTlvCodec), sig, nodeIdXOnly) + case None => false + } + } + + def encode(): String = { + val data = offerCodec.encode(this).require.bytes + Bech32.encodeBytes(Offer.hrp, data.toArray, Bech32.Encoding.Beck32WithoutChecksum) + } + + override def toString: String = encode() + } + + object Offer { + val hrp = "lno" + val signatureTag: String = "lightning" + "offer" + "signature" + + /** + * @param amount_opt amount if it can be determined at offer creation time. + * @param description description of the offer. + * @param nodeId the nodeId to use for this offer, which should be different from our public nodeId if we're hiding behind a blinded route. + * @param features invoice features. + * @param chain chain on which the offer is valid. + */ + def apply(amount_opt: Option[MilliSatoshi], description: String, nodeId: PublicKey, features: Features[InvoiceFeature], chain: ByteVector32): Offer = { + val tlvs: Seq[OfferTlv] = Seq( + if (chain != Block.LivenetGenesisBlock.hash) Some(Chains(Seq(chain))) else None, + amount_opt.map(Amount), + Some(Description(description)), + Some(NodeId(nodeId)), + if (!features.isEmpty) Some(FeaturesTlv(features.unscoped())) else None, + ).flatten + Offer(TlvStream(tlvs)) + } + + def decode(s: String): Try[Offer] = Try { + val triple = Bech32.decodeBytes(s.toLowerCase, true) + val prefix = triple.getFirst + val encoded = triple.getSecond + val encoding = triple.getThird + require(prefix == hrp) + require(encoding == Bech32.Encoding.Beck32WithoutChecksum) + offerCodec.decode(ByteVector(encoded).bits).require.value + } + } + + case class InvoiceRequest(records: TlvStream[InvoiceRequestTlv]) { + + require(records.get[OfferId].nonEmpty, "bolt 12 invoice requests must provide an offer id") + require(records.get[PayerKey].nonEmpty, "bolt 12 invoice requests must provide a payer key") + require(records.get[Signature].nonEmpty, "bolt 12 invoice requests must provide a payer signature") + + val chain: ByteVector32 = records.get[Chain].map(_.hash).getOrElse(Block.LivenetGenesisBlock.hash) + + val offerId: ByteVector32 = records.get[OfferId].map(_.offerId).get + + val amount: Option[MilliSatoshi] = records.get[Amount].map(_.amount) + + val features: Features[InvoiceFeature] = records.get[FeaturesTlv].map(_.features.invoiceFeatures()).getOrElse(Features.empty) + + val quantity_opt: Option[Long] = records.get[Quantity].map(_.quantity) + + val quantity: Long = quantity_opt.getOrElse(1) + + val payerKey: ByteVector32 = records.get[PayerKey].get.publicKey + + val payerNote: Option[String] = records.get[PayerNote].map(_.note) + + val payerInfo: Option[ByteVector] = records.get[PayerInfo].map(_.info) + + val replaceInvoice: Option[ByteVector32] = records.get[ReplaceInvoice].map(_.paymentHash) + + val payerSignature: ByteVector64 = records.get[Signature].get.signature + + def isValidFor(offer: Offer): Boolean = { + val amountOk = offer.amount match { + case Some(offerAmount) => + val baseInvoiceAmount = offerAmount * quantity + amount.forall(baseInvoiceAmount <= _) + case None => amount.nonEmpty + } + amountOk && + offer.offerId == offerId && + offer.chains.contains(chain) && + offer.quantityMin.forall(min => quantity_opt.nonEmpty && min <= quantity) && + offer.quantityMax.forall(max => quantity_opt.nonEmpty && quantity <= max) && + quantity_opt.forall(_ => offer.quantityMin.nonEmpty || offer.quantityMax.nonEmpty) && + offer.features.areSupported(features) && + checkSignature() + } + + def checkSignature(): Boolean = { + verifySchnorr(InvoiceRequest.signatureTag, rootHash(removeSignature(records), invoiceRequestTlvCodec), payerSignature, payerKey) + } + + def encode(): String = { + val data = invoiceRequestCodec.encode(this).require.bytes + Bech32.encodeBytes(InvoiceRequest.hrp, data.toArray, Bech32.Encoding.Beck32WithoutChecksum) + } + + override def toString: String = encode() + } + + object InvoiceRequest { + val hrp = "lnr" + val signatureTag: String = "lightning" + "invoice_request" + "payer_signature" + + /** + * Create a request to fetch an invoice for a given offer. + * + * @param offer Bolt 12 offer. + * @param amount amount that we want to pay. + * @param quantity quantity of items we're buying. + * @param features invoice features. + * @param payerKey private key identifying the payer: this lets us prove we're the ones who paid the invoice. + * @param chain chain we want to use to pay this offer. + */ + def apply(offer: Offer, amount: MilliSatoshi, quantity: Long, features: Features[InvoiceFeature], payerKey: PrivateKey, chain: ByteVector32): InvoiceRequest = { + require(offer.chains contains chain) + require(quantity == 1 || offer.quantityMin.nonEmpty || offer.quantityMax.nonEmpty) + val tlvs: Seq[InvoiceRequestTlv] = Seq( + Some(Chain(chain)), + Some(OfferId(offer.offerId)), + Some(Amount(amount)), + if (offer.quantityMin.nonEmpty || offer.quantityMax.nonEmpty) Some(Quantity(quantity)) else None, + Some(PayerKey(payerKey.publicKey)), + Some(FeaturesTlv(features.unscoped())) + ).flatten + val signature = signSchnorr(InvoiceRequest.signatureTag, rootHash(TlvStream(tlvs), invoiceRequestTlvCodec), payerKey) + InvoiceRequest(TlvStream(tlvs :+ Signature(signature))) + } + + def decode(s: String): Try[InvoiceRequest] = Try { + val triple = Bech32.decodeBytes(s.toLowerCase, true) + val prefix = triple.getFirst + val encoded = triple.getSecond + val encoding = triple.getThird + require(prefix == hrp) + require(encoding == Bech32.Encoding.Beck32WithoutChecksum) + invoiceRequestCodec.decode(ByteVector(encoded).bits).require.value + } + } + + case class InvoiceError(records: TlvStream[InvoiceErrorTlv]) { + require(records.get[Error].nonEmpty, "bolt 12 invoice errors must provide an explanatory string") + } + + def rootHash[T <: Tlv](tlvs: TlvStream[T], codec: Codec[TlvStream[T]]): ByteVector32 = { + // Encoding tlvs is always safe, unless we have a bug in our codecs, so we can call `.require` here. + val encoded = codec.encode(tlvs).require + // Decoding tlvs that we just encoded is safe as well. + // This encoding/decoding step ensures that the resulting tlvs are ordered. + val genericTlvs = vector(genericTlv).decode(encoded).require.value + val nonceKey = ByteVector("LnAll".getBytes) ++ encoded.bytes + + def previousPowerOfTwo(n: Int): Int = { + var p = 1 + while (p < n) { + p = p << 1 + } + p >> 1 + } + + def merkleTree(i: Int, j: Int): ByteVector32 = { + val (a, b) = if (j - i == 1) { + val tlv = genericTlv.encode(genericTlvs(i)).require.bytes + (hash(ByteVector("LnLeaf".getBytes), tlv), hash(nonceKey, tlv)) + } else { + val k = i + previousPowerOfTwo(j - i) + (merkleTree(i, k), merkleTree(k, j)) + } + if (LexicographicalOrdering.isLessThan(a, b)) { + hash(ByteVector("LnBranch".getBytes), a ++ b) + } else { + hash(ByteVector("LnBranch".getBytes), b ++ a) + } + } + + merkleTree(0, genericTlvs.length) + } + + private def hash(tag: String, msg: ByteVector): ByteVector32 = { + ByteVector.encodeAscii(tag) match { + case Right(bytes) => hash(bytes, msg) + case Left(e) => throw e // NB: the tags we use are hard-coded, so we know they're always ASCII + } + } + + private def hash(tag: ByteVector, msg: ByteVector): ByteVector32 = { + val tagHash = Crypto.sha256(tag) + Crypto.sha256(tagHash ++ tagHash ++ msg) + } + + def signSchnorr(tag: String, msg: ByteVector32, key: PrivateKey): ByteVector64 = { + val h = hash(tag, msg) + // NB: we don't add auxiliary random data to keep signatures deterministic. + ByteVector64(ByteVector(Secp256k1JvmKt.getSecpk256k1.signSchnorr(h.toArray, key.value.toArray, null))) + } + + def verifySchnorr(tag: String, msg: ByteVector32, signature: ByteVector64, publicKey: ByteVector32): Boolean = { + val h = hash(tag, msg) + Secp256k1JvmKt.getSecpk256k1.verifySchnorr(signature.toArray, h.toArray, publicKey.toArray) + } + + def xOnlyPublicKey(publicKey: PublicKey): ByteVector32 = ByteVector32(publicKey.value.drop(1)) + + /** We often need to remove the signature field to compute the merkle root. */ + def removeSignature[T <: Bolt12Tlv](records: TlvStream[T]): TlvStream[T] = { + TlvStream(records.records.filter { case _: Signature => false case _ => true }, records.unknown) + } + +} + diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/TlvCodecs.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/TlvCodecs.scala index 1797eb225d..932ad7f1e0 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/TlvCodecs.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/TlvCodecs.scala @@ -104,7 +104,7 @@ object TlvCodecs { /** Length-prefixed truncated uint16 (1 to 3 bytes unsigned integer). */ val ltu16: Codec[Int] = variableSizeBytes(uint8, tu16) - private def validateGenericTlv(g: GenericTlv): Attempt[GenericTlv] = { + private def validateUnknownTlv(g: GenericTlv): Attempt[GenericTlv] = { if (g.tag < TLV_TYPE_HIGH_RANGE && g.tag.toBigInt % 2 == 0) { Attempt.Failure(Err("unknown even tlv type")) } else { @@ -112,7 +112,9 @@ object TlvCodecs { } } - val genericTlv: Codec[GenericTlv] = (("tag" | varint) :: variableSizeBytesLong(varintoverflow, bytes)).as[GenericTlv].exmap(validateGenericTlv, validateGenericTlv) + val genericTlv: Codec[GenericTlv] = (("tag" | varint) :: variableSizeBytesLong(varintoverflow, bytes)).as[GenericTlv] + + private val unknownTlv = genericTlv.exmap(validateUnknownTlv, validateUnknownTlv) private def tag[T <: Tlv](codec: DiscriminatorCodec[T, UInt64], record: Either[GenericTlv, T]): UInt64 = record match { case Left(generic) => generic.tag @@ -140,7 +142,7 @@ object TlvCodecs { * @param codec codec used for the tlv records contained in the stream. * @tparam T stream namespace. */ - def tlvStream[T <: Tlv](codec: DiscriminatorCodec[T, UInt64]): Codec[TlvStream[T]] = list(discriminatorFallback(genericTlv, codec)).exmap( + def tlvStream[T <: Tlv](codec: DiscriminatorCodec[T, UInt64]): Codec[TlvStream[T]] = list(discriminatorFallback(unknownTlv, codec)).exmap( records => validateStream(codec, records), (stream: TlvStream[T]) => { val records = (stream.records.map(Right(_)) ++ stream.unknown.map(Left(_))).toList diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/json/JsonSerializersSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/json/JsonSerializersSpec.scala index 6cdb9554cf..1b3708bd4c 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/json/JsonSerializersSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/json/JsonSerializersSpec.scala @@ -171,19 +171,19 @@ class JsonSerializersSpec extends AnyFunSuite with Matchers { test("Bolt 11 invoice") { val ref = "lnbcrt50n1p0fm9cdpp5al3wvsfkc6p7fxy89eu8gm4aww9mseu9syrcqtpa4mvx42qelkwqdq9v9ekgxqrrss9qypqsqsp5wl2t45v0hj4lgud0zjxcnjccd29ts0p2kh4vpw75vnhyyzyjtjtqarpvqg33asgh3z5ghfuvhvtf39xtnu9e7aqczpgxa9quwsxkd9rnwmx06pve9awgeewxqh90dqgrhzgsqc09ek6uejr93z8puafm6gsqgrk0hy" - val pr = Invoice.fromString(ref) + val pr = Invoice.fromString(ref).get JsonSerializers.serialization.write(pr)(JsonSerializers.formats) shouldBe """{"prefix":"lnbcrt","timestamp":1587386125,"nodeId":"03b207771ddba774e318970e9972da2491ff8e54f777ad0528b6526773730248a0","serialized":"lnbcrt50n1p0fm9cdpp5al3wvsfkc6p7fxy89eu8gm4aww9mseu9syrcqtpa4mvx42qelkwqdq9v9ekgxqrrss9qypqsqsp5wl2t45v0hj4lgud0zjxcnjccd29ts0p2kh4vpw75vnhyyzyjtjtqarpvqg33asgh3z5ghfuvhvtf39xtnu9e7aqczpgxa9quwsxkd9rnwmx06pve9awgeewxqh90dqgrhzgsqc09ek6uejr93z8puafm6gsqgrk0hy","description":"asd","paymentHash":"efe2e64136c683e498872e78746ebd738bb867858107802c3daed86aa819fd9c","expiry":3600,"amount":5000,"features":{"activated":{"var_onion_optin":"optional","payment_secret":"optional"},"unknown":[]},"routingInfo":[]}""" } test("Bolt 11 invoice with routing hints") { val ref = "lntb1pst2q8xpp5qysan6j5xeq97tytxf7pfr0n75na8rztqhh03glmlgsqsyuqzgnqdqqxqrrss9qy9qsqsp5qq67gcxrn2drj5p0lc6p8wgdpqwxnc2h4s9kra5489q0fqsvhumsrzjqfqnj4upt5z6hdludky9vgk4ehzmwu2dk9rcevzczw5ywstehq79c83xr5qqqkqqqqqqqqlgqqqqqeqqjqrzjqwfn3p9278ttzzpe0e00uhyxhned3j5d9acqak5emwfpflp8z2cng838tqqqqxgqqqqqqqlgqqqqqeqqjqkxs4223x2r6sat65asfp0k2pze2rswe9np9vq08waqvsp832ffgymzgx8hgzejasesfxwcw6jj93azwq9klwuzmef3llns3n95pztgqpawp7an" - val pr = Invoice.fromString(ref) + val pr = Invoice.fromString(ref).get JsonSerializers.serialization.write(pr)(JsonSerializers.formats) shouldBe """{"prefix":"lntb","timestamp":1622474982,"nodeId":"03e89e4c3d41dc5332c2fb6cc66d12bfb9257ba681945a242f27a08d5ad210d891","serialized":"lntb1pst2q8xpp5qysan6j5xeq97tytxf7pfr0n75na8rztqhh03glmlgsqsyuqzgnqdqqxqrrss9qy9qsqsp5qq67gcxrn2drj5p0lc6p8wgdpqwxnc2h4s9kra5489q0fqsvhumsrzjqfqnj4upt5z6hdludky9vgk4ehzmwu2dk9rcevzczw5ywstehq79c83xr5qqqkqqqqqqqqlgqqqqqeqqjqrzjqwfn3p9278ttzzpe0e00uhyxhned3j5d9acqak5emwfpflp8z2cng838tqqqqxgqqqqqqqlgqqqqqeqqjqkxs4223x2r6sat65asfp0k2pze2rswe9np9vq08waqvsp832ffgymzgx8hgzejasesfxwcw6jj93azwq9klwuzmef3llns3n95pztgqpawp7an","description":"","paymentHash":"0121d9ea5436405f2c8b327c148df3f527d38c4b05eef8a3fbfa200813801226","expiry":3600,"features":{"activated":{"var_onion_optin":"optional","payment_secret":"optional","basic_mpp":"optional"},"unknown":[]},"routingInfo":[[{"nodeId":"02413957815d05abb7fc6d885622d5cdc5b7714db1478cb05813a8474179b83c5c","shortChannelId":"1975837x88x0","feeBase":1000,"feeProportionalMillionths":100,"cltvExpiryDelta":144}],[{"nodeId":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","shortChannelId":"1976152x25x0","feeBase":1000,"feeProportionalMillionths":100,"cltvExpiryDelta":144}]]}""" } test("Bolt 11 invoice with metadata") { val ref = "lnbc10m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdp9wpshjmt9de6zqmt9w3skgct5vysxjmnnd9jx2mq8q8a04uqnp4q0n326hr8v9zprg8gsvezcch06gfaqqhde2aj730yg0durunfhv66sp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9q2gqqqqqqsgqy9gw6ymamd20jumvdgpfphkhp8fzhhdhycw36egcmla5vlrtrmhs9t7psfy3hkkdqzm9eq64fjg558znccds5nhsfmxveha5xe0dykgpspdha0" - val pr = Invoice.fromString(ref) + val pr = Invoice.fromString(ref).get JsonSerializers.serialization.write(pr)(JsonSerializers.formats) shouldBe """{"prefix":"lnbc","timestamp":1496314658,"nodeId":"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad","serialized":"lnbc10m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdp9wpshjmt9de6zqmt9w3skgct5vysxjmnnd9jx2mq8q8a04uqnp4q0n326hr8v9zprg8gsvezcch06gfaqqhde2aj730yg0durunfhv66sp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9q2gqqqqqqsgqy9gw6ymamd20jumvdgpfphkhp8fzhhdhycw36egcmla5vlrtrmhs9t7psfy3hkkdqzm9eq64fjg558znccds5nhsfmxveha5xe0dykgpspdha0","description":"payment metadata inside","paymentHash":"0001020304050607080900010203040506070809000102030405060708090102","paymentMetadata":"01fafaf0","amount":1000000000,"features":{"activated":{"var_onion_optin":"mandatory","payment_secret":"mandatory","option_payment_metadata":"mandatory"},"unknown":[]},"routingInfo":[]}""" } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/Bolt11InvoiceSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/Bolt11InvoiceSpec.scala index e1bb6d543d..bcb9adda8b 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/Bolt11InvoiceSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/Bolt11InvoiceSpec.scala @@ -21,7 +21,7 @@ import fr.acinq.bitcoin.scalacompat.{Block, BtcDouble, ByteVector32, Crypto, Mil import fr.acinq.eclair.FeatureSupport.{Mandatory, Optional} import fr.acinq.eclair.Features.{PaymentMetadata, PaymentSecret, _} import fr.acinq.eclair.payment.Bolt11Invoice._ -import fr.acinq.eclair.{CltvExpiryDelta, FeatureSupport, Features, Feature, MilliSatoshi, MilliSatoshiLong, ShortChannelId, TestConstants, TimestampSecond, TimestampSecondLong, ToMilliSatoshiConversion, UnknownFeature, randomBytes32} +import fr.acinq.eclair.{CltvExpiryDelta, Feature, FeatureSupport, Features, MilliSatoshi, MilliSatoshiLong, ShortChannelId, TestConstants, TimestampSecond, TimestampSecondLong, ToMilliSatoshiConversion, UnknownFeature, randomBytes32} import org.scalatest.funsuite.AnyFunSuite import scodec.DecodeResult import scodec.bits._ @@ -43,18 +43,18 @@ class Bolt11InvoiceSpec extends AnyFunSuite { // Copy of Bolt11Invoice.apply that doesn't strip unknown features def createInvoiceUnsafe(chainHash: ByteVector32, - amount: Option[MilliSatoshi], - paymentHash: ByteVector32, - privateKey: PrivateKey, - description: Either[String, ByteVector32], - minFinalCltvExpiryDelta: CltvExpiryDelta, - fallbackAddress: Option[String] = None, - expirySeconds: Option[Long] = None, - extraHops: List[List[ExtraHop]] = Nil, - timestamp: TimestampSecond = TimestampSecond.now(), - paymentSecret: ByteVector32 = randomBytes32(), - paymentMetadata: Option[ByteVector] = None, - features: Features[Feature] = defaultFeatures.unscoped()): Bolt11Invoice = { + amount: Option[MilliSatoshi], + paymentHash: ByteVector32, + privateKey: PrivateKey, + description: Either[String, ByteVector32], + minFinalCltvExpiryDelta: CltvExpiryDelta, + fallbackAddress: Option[String] = None, + expirySeconds: Option[Long] = None, + extraHops: List[List[ExtraHop]] = Nil, + timestamp: TimestampSecond = TimestampSecond.now(), + paymentSecret: ByteVector32 = randomBytes32(), + paymentMetadata: Option[ByteVector] = None, + features: Features[Feature] = defaultFeatures.unscoped()): Bolt11Invoice = { require(features.hasFeature(Features.PaymentSecret, Some(FeatureSupport.Mandatory)), "invoices must require a payment secret") val prefix = prefixes(chainHash) val tags = { @@ -132,7 +132,7 @@ class Bolt11InvoiceSpec extends AnyFunSuite { test("Please make a donation of any amount using payment_hash 0001020304050607080900010203040506070809000102030405060708090102 to me @03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad") { val ref = "lnbc1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdpl2pkx2ctnv5sxxmmwwd5kgetjypeh2ursdae8g6twvus8g6rfwvs8qun0dfjkxaq9qrsgq357wnc5r2ueh7ck6q93dj32dlqnls087fxdwk8qakdyafkq3yap9us6v52vjjsrvywa6rt52cm9r9zqt8r2t7mlcwspyetp5h2tztugp9lfyql" - val invoice = Bolt11Invoice.fromString(ref) + val Success(invoice) = Bolt11Invoice.fromString(ref) assert(invoice.prefix == "lnbc") assert(invoice.amount_opt.isEmpty) assert(invoice.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102") @@ -148,7 +148,7 @@ class Bolt11InvoiceSpec extends AnyFunSuite { test("Please send $3 for a cup of coffee to the same peer, within 1 minute") { val ref = "lnbc2500u1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpu9qrsgquk0rl77nj30yxdy8j9vdx85fkpmdla2087ne0xh8nhedh8w27kyke0lp53ut353s06fv3qfegext0eh0ymjpf39tuven09sam30g4vgpfna3rh" - val invoice = Bolt11Invoice.fromString(ref) + val Success(invoice) = Bolt11Invoice.fromString(ref) assert(invoice.prefix == "lnbc") assert(invoice.amount_opt === Some(250000000 msat)) assert(invoice.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102") @@ -163,7 +163,7 @@ class Bolt11InvoiceSpec extends AnyFunSuite { test("Please send 0.0025 BTC for a cup of nonsense (ナンセンス 1杯) to the same peer, within one minute") { val ref = "lnbc2500u1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdpquwpc4curk03c9wlrswe78q4eyqc7d8d0xqzpu9qrsgqhtjpauu9ur7fw2thcl4y9vfvh4m9wlfyz2gem29g5ghe2aak2pm3ps8fdhtceqsaagty2vph7utlgj48u0ged6a337aewvraedendscp573dxr" - val invoice = Bolt11Invoice.fromString(ref) + val Success(invoice) = Bolt11Invoice.fromString(ref) assert(invoice.prefix == "lnbc") assert(invoice.amount_opt === Some(250000000 msat)) assert(invoice.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102") @@ -178,7 +178,7 @@ class Bolt11InvoiceSpec extends AnyFunSuite { test("Now send $24 for an entire list of things (hashed)") { val ref = "lnbc20m1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqs9qrsgq7ea976txfraylvgzuxs8kgcw23ezlrszfnh8r6qtfpr6cxga50aj6txm9rxrydzd06dfeawfk6swupvz4erwnyutnjq7x39ymw6j38gp7ynn44" - val invoice = Bolt11Invoice.fromString(ref) + val Success(invoice) = Bolt11Invoice.fromString(ref) assert(invoice.prefix == "lnbc") assert(invoice.amount_opt === Some(2000000000 msat)) assert(invoice.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102") @@ -193,7 +193,7 @@ class Bolt11InvoiceSpec extends AnyFunSuite { test("The same, on testnet, with a fallback address mk2QpYatsKicvFVuTAQLBryyccRXMUaGHP") { val ref = "lntb20m1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygshp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqfpp3x9et2e20v6pu37c5d9vax37wxq72un989qrsgqdj545axuxtnfemtpwkc45hx9d2ft7x04mt8q7y6t0k2dge9e7h8kpy9p34ytyslj3yu569aalz2xdk8xkd7ltxqld94u8h2esmsmacgpghe9k8" - val invoice = Bolt11Invoice.fromString(ref) + val Success(invoice) = Bolt11Invoice.fromString(ref) assert(invoice.prefix == "lntb") assert(invoice.amount_opt === Some(2000000000 msat)) assert(invoice.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102") @@ -208,7 +208,7 @@ class Bolt11InvoiceSpec extends AnyFunSuite { test("On mainnet, with fallback address 1RustyRX2oai4EYYDpQGWvEL62BBGqN9T with extra routing info to go via nodes 029e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255 then 039e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255") { val ref = "lnbc20m1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqsfpp3qjmp7lwpagxun9pygexvgpjdc4jdj85fr9yq20q82gphp2nflc7jtzrcazrra7wwgzxqc8u7754cdlpfrmccae92qgzqvzq2ps8pqqqqqqpqqqqq9qqqvpeuqafqxu92d8lr6fvg0r5gv0heeeqgcrqlnm6jhphu9y00rrhy4grqszsvpcgpy9qqqqqqgqqqqq7qqzq9qrsgqdfjcdk6w3ak5pca9hwfwfh63zrrz06wwfya0ydlzpgzxkn5xagsqz7x9j4jwe7yj7vaf2k9lqsdk45kts2fd0fkr28am0u4w95tt2nsq76cqw0" - val invoice = Bolt11Invoice.fromString(ref) + val Success(invoice) = Bolt11Invoice.fromString(ref) assert(invoice.prefix == "lnbc") assert(invoice.amount_opt === Some(2000000000 msat)) assert(invoice.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102") @@ -227,7 +227,7 @@ class Bolt11InvoiceSpec extends AnyFunSuite { test("On mainnet, with fallback (p2sh) address 3EktnHQD7RiAE6uzMj2ZifT9YgRrkSgzQX") { val ref = "lnbc20m1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygshp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqfppj3a24vwu6r8ejrss3axul8rxldph2q7z99qrsgqz6qsgww34xlatfj6e3sngrwfy3ytkt29d2qttr8qz2mnedfqysuqypgqex4haa2h8fx3wnypranf3pdwyluftwe680jjcfp438u82xqphf75ym" - val invoice = Bolt11Invoice.fromString(ref) + val Success(invoice) = Bolt11Invoice.fromString(ref) assert(invoice.prefix == "lnbc") assert(invoice.amount_opt === Some(2000000000 msat)) assert(invoice.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102") @@ -242,7 +242,7 @@ class Bolt11InvoiceSpec extends AnyFunSuite { test("On mainnet, with fallback (p2wpkh) address bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4") { val ref = "lnbc20m1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygshp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqfppqw508d6qejxtdg4y5r3zarvary0c5xw7k9qrsgqt29a0wturnys2hhxpner2e3plp6jyj8qx7548zr2z7ptgjjc7hljm98xhjym0dg52sdrvqamxdezkmqg4gdrvwwnf0kv2jdfnl4xatsqmrnsse" - val invoice = Bolt11Invoice.fromString(ref) + val Success(invoice) = Bolt11Invoice.fromString(ref) assert(invoice.prefix == "lnbc") assert(invoice.amount_opt === Some(2000000000 msat)) assert(invoice.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102") @@ -257,7 +257,7 @@ class Bolt11InvoiceSpec extends AnyFunSuite { test("On mainnet, with fallback (p2wsh) address bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3") { val ref = "lnbc20m1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygshp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqfp4qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q9qrsgq9vlvyj8cqvq6ggvpwd53jncp9nwc47xlrsnenq2zp70fq83qlgesn4u3uyf4tesfkkwwfg3qs54qe426hp3tz7z6sweqdjg05axsrjqp9yrrwc" - val invoice = Bolt11Invoice.fromString(ref) + val Success(invoice) = Bolt11Invoice.fromString(ref) assert(invoice.prefix == "lnbc") assert(invoice.amount_opt === Some(2000000000 msat)) assert(invoice.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102") @@ -273,7 +273,7 @@ class Bolt11InvoiceSpec extends AnyFunSuite { test("On mainnet, with fallback (p2wsh) address bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3 and a minimum htlc cltv expiry of 12") { val ref = "lnbc20m1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygscqpvpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqsfp4qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q9qrsgq999fraffdzl6c8j7qd325dfurcq7vl0mfkdpdvve9fy3hy4lw0x9j3zcj2qdh5e5pyrp6cncvmxrhchgey64culwmjtw9wym74xm6xqqevh9r0" - val invoice = Bolt11Invoice.fromString(ref) + val Success(invoice) = Bolt11Invoice.fromString(ref) assert(invoice.prefix == "lnbc") assert(invoice.amount_opt === Some(2000000000 msat)) assert(invoice.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102") @@ -298,7 +298,7 @@ class Bolt11InvoiceSpec extends AnyFunSuite { ) for (ref <- refs) { - val invoice = Bolt11Invoice.fromString(ref) + val Success(invoice) = Bolt11Invoice.fromString(ref) assert(invoice.prefix === "lnbc") assert(invoice.amount_opt === Some(2500000000L msat)) assert(invoice.paymentHash.bytes === hex"0001020304050607080900010203040506070809000102030405060708090102") @@ -317,7 +317,7 @@ class Bolt11InvoiceSpec extends AnyFunSuite { test("On mainnet, please send $30 for coffee beans to the same peer, which supports features 8, 14, 99 and 100, using secret 0x1111111111111111111111111111111111111111111111111111111111111111") { val ref = "lnbc25m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5vdhkven9v5sxyetpdeessp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9q4psqqqqqqqqqqqqqqqqsgqtqyx5vggfcsll4wu246hz02kp85x4katwsk9639we5n5yngc3yhqkm35jnjw4len8vrnqnf5ejh0mzj9n3vz2px97evektfm2l6wqccp3y7372" - val invoice = Bolt11Invoice.fromString(ref) + val Success(invoice) = Bolt11Invoice.fromString(ref) assert(invoice.prefix === "lnbc") assert(invoice.amount_opt === Some(2500000000L msat)) assert(invoice.paymentHash.bytes === hex"0001020304050607080900010203040506070809000102030405060708090102") @@ -336,7 +336,7 @@ class Bolt11InvoiceSpec extends AnyFunSuite { test("On mainnet, please send 0.00967878534 BTC for a list of items within one week, amount in pico-BTC") { val ref = "lnbc9678785340p1pwmna7lpp5gc3xfm08u9qy06djf8dfflhugl6p7lgza6dsjxq454gxhj9t7a0sd8dgfkx7cmtwd68yetpd5s9xar0wfjn5gpc8qhrsdfq24f5ggrxdaezqsnvda3kkum5wfjkzmfqf3jkgem9wgsyuctwdus9xgrcyqcjcgpzgfskx6eqf9hzqnteypzxz7fzypfhg6trddjhygrcyqezcgpzfysywmm5ypxxjemgw3hxjmn8yptk7untd9hxwg3q2d6xjcmtv4ezq7pqxgsxzmnyyqcjqmt0wfjjq6t5v4khxsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygsxqyjw5qcqp2rzjq0gxwkzc8w6323m55m4jyxcjwmy7stt9hwkwe2qxmy8zpsgg7jcuwz87fcqqeuqqqyqqqqlgqqqqn3qq9q9qrsgqrvgkpnmps664wgkp43l22qsgdw4ve24aca4nymnxddlnp8vh9v2sdxlu5ywdxefsfvm0fq3sesf08uf6q9a2ke0hc9j6z6wlxg5z5kqpu2v9wz" - val invoice = Bolt11Invoice.fromString(ref) + val Success(invoice) = Bolt11Invoice.fromString(ref) assert(invoice.prefix === "lnbc") assert(invoice.amount_opt === Some(967878534 msat)) assert(invoice.paymentHash.bytes === hex"462264ede7e14047e9b249da94fefc47f41f7d02ee9b091815a5506bc8abf75f") @@ -354,7 +354,7 @@ class Bolt11InvoiceSpec extends AnyFunSuite { test("On mainnet, please send 0.01 BTC with payment metadata 0x01fafaf0") { val ref = "lnbc10m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdp9wpshjmt9de6zqmt9w3skgct5vysxjmnnd9jx2mq8q8a04uqsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9q2gqqqqqqsgq7hf8he7ecf7n4ffphs6awl9t6676rrclv9ckg3d3ncn7fct63p6s365duk5wrk202cfy3aj5xnnp5gs3vrdvruverwwq7yzhkf5a3xqpd05wjc" - val invoice = Bolt11Invoice.fromString(ref) + val Success(invoice) = Bolt11Invoice.fromString(ref) assert(invoice.prefix == "lnbc") assert(invoice.amount_opt === Some(1000000000 msat)) assert(invoice.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102") @@ -386,7 +386,7 @@ class Bolt11InvoiceSpec extends AnyFunSuite { "lnbc2500000001p1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpusp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9qrsgq0lzc236j96a95uv0m3umg28gclm5lqxtqqwk32uuk4k6673k6n5kfvx3d2h8s295fad45fdhmusm8sjudfhlf6dcsxmfvkeywmjdkxcp99202x" ) for (ref <- refs) { - assertThrows[Exception](Bolt11Invoice.fromString(ref)) + assert(Bolt11Invoice.fromString(ref).isFailure) } } @@ -403,7 +403,7 @@ class Bolt11InvoiceSpec extends AnyFunSuite { val invoice = Bolt11Invoice(chainHash = Block.LivenetGenesisBlock.hash, amount = Some(123 msat), paymentHash = ByteVector32(ByteVector.fill(32)(1)), privateKey = priv, description = Left("Some invoice"), minFinalCltvExpiryDelta = CltvExpiryDelta(18), expirySeconds = Some(123456), timestamp = 12345 unixsec) assert(invoice.minFinalCltvExpiryDelta === CltvExpiryDelta(18)) val serialized = invoice.toString - val pr1 = Bolt11Invoice.fromString(serialized) + val Success(pr1) = Bolt11Invoice.fromString(serialized) assert(invoice == pr1) } @@ -421,7 +421,7 @@ class Bolt11InvoiceSpec extends AnyFunSuite { signature = ByteVector.empty).sign(priv) val serialized = invoice.toString - val pr1 = Bolt11Invoice.fromString(serialized) + val Success(pr1) = Bolt11Invoice.fromString(serialized) val Some(_) = pr1.tags.collectFirst { case u: UnknownTag21 => u } } @@ -451,12 +451,12 @@ class Bolt11InvoiceSpec extends AnyFunSuite { test("accept uppercase invoices") { val input = "lntb1500n1pwxx94fsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygspp5q3xzmwuvxpkyhz6pvg3fcfxz0259kgh367qazj62af9rs0pw07dsdpa2fjkzep6yp58garswvaz7tmvd9nksarwd9hxw6n0w4kx2tnrdakj7grfwvs8wcqzysxqr23swwl9egjej7rvvt9zdxrtpy8xuu6cckdwajfccmtz7n90ea34k3j595w77pt69s5dx5a46f4k4w5avtvjkc4l4rm8n4xmk7fe3pms3pspdd032j" - assert(Bolt11Invoice.fromString(input.toUpperCase()).toString == input) + assert(Bolt11Invoice.fromString(input.toUpperCase()).get.toString == input) } test("Pay 1 BTC without multiplier") { val ref = "lnbc1000m1pdkmqhusp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygspp5n2ees808r98m0rh4472yyth0c5fptzcxmexcjznrzmq8xald0cgqdqsf4ujqarfwqsxymmccqp2pv37ezvhth477nu0yhhjlcry372eef57qmldhreqnr0kx82jkupp3n7nw42u3kdyyjskdr8jhjy2vugr3skdmy8ersft36969xplkxsp2v7c58" - val invoice = Bolt11Invoice.fromString(ref) + val Success(invoice) = Bolt11Invoice.fromString(ref) assert(invoice.amount_opt === Some(100000000000L msat)) assert(features2bits(invoice.features) === BitVector.empty) } @@ -487,7 +487,7 @@ class Bolt11InvoiceSpec extends AnyFunSuite { for ((features, res) <- featureBits) { val invoice = createInvoiceUnsafe(Block.LivenetGenesisBlock.hash, Some(123 msat), ByteVector32.One, priv, Left("Some invoice"), CltvExpiryDelta(18), features = features) assert(Result(invoice.features.hasFeature(BasicMultiPartPayment), invoice.features.hasFeature(PaymentSecret, Some(Mandatory)), nodeParams.features.invoiceFeatures().areSupported(invoice.features)) === res) - assert(Bolt11Invoice.fromString(invoice.toString) === invoice) + assert(Bolt11Invoice.fromString(invoice.toString).get === invoice) } } @@ -514,17 +514,15 @@ class Bolt11InvoiceSpec extends AnyFunSuite { assert(invoice.features === Features(PaymentSecret -> Mandatory, VariableLengthOnion -> Mandatory)) assert(invoice.features.hasFeature(PaymentSecret, Some(Mandatory))) - val pr1 = Bolt11Invoice.fromString(invoice.toString) + val Success(pr1) = Bolt11Invoice.fromString(invoice.toString) assert(pr1.paymentSecret === invoice.paymentSecret) - val pr2 = Bolt11Invoice.fromString("lnbc40n1pw9qjvwpp5qq3w2ln6krepcslqszkrsfzwy49y0407hvks30ec6pu9s07jur3sdpstfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqenwdencqzysxqrrss7ju0s4dwx6w8a95a9p2xc5vudl09gjl0w2n02sjrvffde632nxwh2l4w35nqepj4j5njhh4z65wyfc724yj6dn9wajvajfn5j7em6wsq2elakl") + val Success(pr2) = Bolt11Invoice.fromString("lnbc40n1pw9qjvwpp5qq3w2ln6krepcslqszkrsfzwy49y0407hvks30ec6pu9s07jur3sdpstfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqenwdencqzysxqrrss7ju0s4dwx6w8a95a9p2xc5vudl09gjl0w2n02sjrvffde632nxwh2l4w35nqepj4j5njhh4z65wyfc724yj6dn9wajvajfn5j7em6wsq2elakl") assert(!pr2.features.hasFeature(PaymentSecret, Some(Mandatory))) assert(pr2.paymentSecret === None) // An invoice that sets the payment secret feature bit must provide a payment secret. - assertThrows[IllegalArgumentException]( - Bolt11Invoice.fromString("lnbc1230p1pwljzn3pp5qyqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqdq52dhk6efqd9h8vmmfvdjs9qypqsqylvwhf7xlpy6xpecsnpcjjuuslmzzgeyv90mh7k7vs88k2dkxgrkt75qyfjv5ckygw206re7spga5zfd4agtdvtktxh5pkjzhn9dq2cqz9upw7") - ) + assert(Bolt11Invoice.fromString("lnbc1230p1pwljzn3pp5qyqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqdq52dhk6efqd9h8vmmfvdjs9qypqsqylvwhf7xlpy6xpecsnpcjjuuslmzzgeyv90mh7k7vs88k2dkxgrkt75qyfjv5ckygw206re7spga5zfd4agtdvtktxh5pkjzhn9dq2cqz9upw7").isFailure) // A multi-part invoice must use a payment secret. assertThrows[IllegalArgumentException]( @@ -544,7 +542,7 @@ class Bolt11InvoiceSpec extends AnyFunSuite { assert(pr2.features.hasFeature(BasicMultiPartPayment)) assert(pr2.features.hasFeature(TrampolinePaymentPrototype)) - val pr3 = Bolt11Invoice.fromString("lnbc40n1pw9qjvwpp5qq3w2ln6krepcslqszkrsfzwy49y0407hvks30ec6pu9s07jur3sdpstfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqenwdencqzysxqrrss7ju0s4dwx6w8a95a9p2xc5vudl09gjl0w2n02sjrvffde632nxwh2l4w35nqepj4j5njhh4z65wyfc724yj6dn9wajvajfn5j7em6wsq2elakl") + val Success(pr3) = Bolt11Invoice.fromString("lnbc40n1pw9qjvwpp5qq3w2ln6krepcslqszkrsfzwy49y0407hvks30ec6pu9s07jur3sdpstfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqenwdencqzysxqrrss7ju0s4dwx6w8a95a9p2xc5vudl09gjl0w2n02sjrvffde632nxwh2l4w35nqepj4j5njhh4z65wyfc724yj6dn9wajvajfn5j7em6wsq2elakl") assert(!pr3.features.hasFeature(TrampolinePaymentPrototype)) } @@ -616,16 +614,17 @@ class Bolt11InvoiceSpec extends AnyFunSuite { ) for ((req, nodeId) <- requests) { - assert(Bolt11Invoice.fromString(req).nodeId === nodeId) - assert(Bolt11Invoice.fromString(req).toString === req) + val Success(invoice) = Bolt11Invoice.fromString(req) + assert(invoice.nodeId === nodeId) + assert(invoice.toString === req) } } - test("no unknown feature in invoice"){ + test("no unknown feature in invoice") { assert(TestConstants.Alice.nodeParams.features.invoiceFeatures().unknown.nonEmpty) val invoice = Bolt11Invoice(Block.LivenetGenesisBlock.hash, Some(123 msat), ByteVector32.One, priv, Left("Some invoice"), CltvExpiryDelta(18), features = TestConstants.Alice.nodeParams.features.invoiceFeatures()) assert(invoice.features === Features(PaymentSecret -> Mandatory, BasicMultiPartPayment -> Optional, PaymentMetadata -> Optional, VariableLengthOnion -> Mandatory)) - assert(Bolt11Invoice.fromString(invoice.toString) === invoice) + assert(Bolt11Invoice.fromString(invoice.toString).get === invoice) } test("Invoices can't have high features"){ diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/Bolt12InvoiceSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/Bolt12InvoiceSpec.scala new file mode 100644 index 0000000000..4808e268af --- /dev/null +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/Bolt12InvoiceSpec.scala @@ -0,0 +1,349 @@ +/* + * 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.payment + +import fr.acinq.bitcoin.scalacompat.Crypto.PrivateKey +import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, Crypto} +import fr.acinq.eclair.FeatureSupport.{Mandatory, Optional} +import fr.acinq.eclair.Features.{BasicMultiPartPayment, VariableLengthOnion} +import fr.acinq.eclair.payment.Bolt12Invoice.signatureTag +import fr.acinq.eclair.wire.protocol.OfferCodecs.{invoiceRequestTlvCodec, invoiceTlvCodec} +import fr.acinq.eclair.wire.protocol.Offers._ +import fr.acinq.eclair.wire.protocol.{GenericTlv, Offers, TlvStream} +import fr.acinq.eclair.{CltvExpiryDelta, Feature, FeatureSupport, Features, MilliSatoshiLong, TimestampSecond, TimestampSecondLong, UInt64, randomBytes32, randomBytes64, randomKey} +import org.scalatest.funsuite.AnyFunSuite +import scodec.bits._ + +import scala.concurrent.duration.DurationInt +import scala.util.Success + +class Bolt12InvoiceSpec extends AnyFunSuite { + + def signInvoice(invoice: Bolt12Invoice, key: PrivateKey): Bolt12Invoice = { + val tlvs = Offers.removeSignature(invoice.records) + val signature = signSchnorr(Bolt12Invoice.signatureTag("signature"), rootHash(tlvs, invoiceTlvCodec), key) + val signedInvoice = Bolt12Invoice(tlvs.copy(records = tlvs.records ++ Seq(Signature(signature))), None) + assert(signedInvoice.checkSignature()) + signedInvoice + } + + test("check invoice signature") { + val (nodeKey, payerKey, chain) = (randomKey(), randomKey(), randomBytes32()) + val offer = Offer(Some(10000 msat), "test offer", nodeKey.publicKey, Features.empty, chain) + val request = InvoiceRequest(offer, 11000 msat, 1, Features.empty, payerKey, chain) + val invoice = Bolt12Invoice(offer, request, randomBytes32(), nodeKey, Features.empty) + assert(invoice.isValidFor(offer, request)) + assert(invoice.checkSignature()) + assert(!invoice.checkRefundSignature()) + assert(Bolt12Invoice.fromString(invoice.toString).get.toString === invoice.toString) + // changing signature makes check fail + val withInvalidSignature = Bolt12Invoice(TlvStream(invoice.records.records.map { case Signature(_) => Signature(randomBytes64()) case x => x }, invoice.records.unknown), None) + assert(!withInvalidSignature.checkSignature()) + assert(!withInvalidSignature.isValidFor(offer, request)) + assert(!withInvalidSignature.checkRefundSignature()) + // changing fields makes the signature invalid + val withModifiedUnknownTlv = Bolt12Invoice(invoice.records.copy(unknown = Seq(GenericTlv(UInt64(7), hex"ade4"))), None) + assert(!withModifiedUnknownTlv.checkSignature()) + assert(!withModifiedUnknownTlv.isValidFor(offer, request)) + assert(!withModifiedUnknownTlv.checkRefundSignature()) + val withModifiedAmount = Bolt12Invoice(TlvStream(invoice.records.records.map { case Amount(amount) => Amount(amount + 100.msat) case x => x }, invoice.records.unknown), None) + assert(!withModifiedAmount.checkSignature()) + assert(!withModifiedAmount.isValidFor(offer, request)) + assert(!withModifiedAmount.checkRefundSignature()) + } + + test("check that invoice matches offer") { + val (nodeKey, payerKey, chain) = (randomKey(), randomKey(), randomBytes32()) + val offer = Offer(Some(10000 msat), "test offer", nodeKey.publicKey, Features.empty, chain) + val request = InvoiceRequest(offer, 11000 msat, 1, Features.empty, payerKey, chain) + val invoice = Bolt12Invoice(offer, request, randomBytes32(), nodeKey, Features.empty) + assert(invoice.isValidFor(offer, request)) + assert(!invoice.isValidFor(Offer(None, "test offer", randomKey().publicKey, Features.empty, chain), request)) + // amount must match the offer + val withOtherAmount = signInvoice(Bolt12Invoice(TlvStream(invoice.records.records.map { case Amount(_) => Amount(9000 msat) case x => x }.toSeq), None), nodeKey) + assert(!withOtherAmount.isValidFor(offer, request)) + // description must match the offer, may have appended info + val withOtherDescription = signInvoice(Bolt12Invoice(TlvStream(invoice.records.records.map { case Description(_) => Description("other description") case x => x }.toSeq), None), nodeKey) + assert(!withOtherDescription.isValidFor(offer, request)) + val withExtendedDescription = signInvoice(Bolt12Invoice(TlvStream(invoice.records.records.map { case Description(_) => Description("test offer + more") case x => x }.toSeq), None), nodeKey) + assert(withExtendedDescription.isValidFor(offer, request)) + // nodeId must match the offer + val otherNodeKey = randomKey() + val withOtherNodeId = signInvoice(Bolt12Invoice(TlvStream(invoice.records.records.map { case NodeId(_) => NodeId(otherNodeKey.publicKey) case x => x }.toSeq), None), otherNodeKey) + assert(!withOtherNodeId.isValidFor(offer, request)) + // offerId must match the offer + val withOtherOfferId = signInvoice(Bolt12Invoice(TlvStream(invoice.records.records.map { case OfferId(_) => OfferId(randomBytes32()) case x => x }.toSeq), None), nodeKey) + assert(!withOtherOfferId.isValidFor(offer, request)) + // issuer must match the offer + val withOtherIssuer = signInvoice(Bolt12Invoice(TlvStream(invoice.records.records ++ Seq(Issuer("spongebob"))), None), nodeKey) + assert(!withOtherIssuer.isValidFor(offer, request)) + } + + test("check that invoice matches invoice request") { + val (nodeKey, payerKey, chain) = (randomKey(), randomKey(), randomBytes32()) + val offer = Offer(Some(15000 msat), "test offer", nodeKey.publicKey, Features(VariableLengthOnion -> Mandatory), chain) + val request = InvoiceRequest(offer, 15000 msat, 1, Features(VariableLengthOnion -> Mandatory), payerKey, chain) + assert(request.quantity_opt === None) // when paying for a single item, the quantity field must not be present + val invoice = Bolt12Invoice(offer, request, randomBytes32(), nodeKey, Features(VariableLengthOnion -> Mandatory, BasicMultiPartPayment -> Optional)) + assert(invoice.isValidFor(offer, request)) + val withInvalidFeatures = signInvoice(Bolt12Invoice(TlvStream(invoice.records.records.map { case FeaturesTlv(_) => FeaturesTlv(Features(VariableLengthOnion -> Mandatory, BasicMultiPartPayment -> Mandatory)) case x => x }.toSeq), None), nodeKey) + assert(!withInvalidFeatures.isValidFor(offer, request)) + val withAmountTooBig = signInvoice(Bolt12Invoice(TlvStream(invoice.records.records.map { case Amount(_) => Amount(20000 msat) case x => x }.toSeq), None), nodeKey) + assert(!withAmountTooBig.isValidFor(offer, request)) + val withQuantity = signInvoice(Bolt12Invoice(TlvStream(invoice.records.records.toSeq :+ Quantity(2)), None), nodeKey) + assert(!withQuantity.isValidFor(offer, request)) + val withOtherPayerKey = signInvoice(Bolt12Invoice(TlvStream(invoice.records.records.map { case PayerKey(_) => PayerKey(randomBytes32()) case x => x }.toSeq), None), nodeKey) + assert(!withOtherPayerKey.isValidFor(offer, request)) + val withPayerNote = signInvoice(Bolt12Invoice(TlvStream(invoice.records.records.toSeq :+ PayerNote("I am Batman")), None), nodeKey) + assert(!withPayerNote.isValidFor(offer, request)) + val withPayerInfo = signInvoice(Bolt12Invoice(TlvStream(invoice.records.records.toSeq :+ PayerInfo(hex"010203040506")), None), nodeKey) + assert(!withPayerInfo.isValidFor(offer, request)) + // Invoice request with more details about the payer. + val requestWithPayerDetails = { + val tlvs: Seq[InvoiceRequestTlv] = Seq( + OfferId(offer.offerId), + Amount(15000 msat), + PayerKey(payerKey.publicKey), + PayerInfo(hex"010203040506"), + PayerNote("I am Batman"), + FeaturesTlv(Features(VariableLengthOnion -> Mandatory)) + ) + val signature = signSchnorr(InvoiceRequest.signatureTag, rootHash(TlvStream(tlvs), invoiceRequestTlvCodec), payerKey) + InvoiceRequest(TlvStream(tlvs :+ Signature(signature))) + } + val withPayerDetails = Bolt12Invoice(offer, requestWithPayerDetails, randomBytes32(), nodeKey, Features.empty) + assert(withPayerDetails.isValidFor(offer, requestWithPayerDetails)) + assert(!withPayerDetails.isValidFor(offer, request)) + val withOtherPayerInfo = signInvoice(Bolt12Invoice(TlvStream(withPayerDetails.records.records.map { case PayerInfo(_) => PayerInfo(hex"deadbeef") case x => x }.toSeq), None), nodeKey) + assert(!withOtherPayerInfo.isValidFor(offer, requestWithPayerDetails)) + assert(!withOtherPayerInfo.isValidFor(offer, request)) + val withOtherPayerNote = signInvoice(Bolt12Invoice(TlvStream(withPayerDetails.records.records.map { case PayerNote(_) => PayerNote("Or am I Bruce Wayne?") case x => x }.toSeq), None), nodeKey) + assert(!withOtherPayerNote.isValidFor(offer, requestWithPayerDetails)) + assert(!withOtherPayerNote.isValidFor(offer, request)) + } + + test("check invoice expiry") { + val (nodeKey, payerKey, chain) = (randomKey(), randomKey(), randomBytes32()) + val offer = Offer(Some(5000 msat), "test offer", nodeKey.publicKey, Features.empty, chain) + val request = InvoiceRequest(offer, 5000 msat, 1, Features.empty, payerKey, chain) + val invoice = Bolt12Invoice(offer, request, randomBytes32(), nodeKey, Features.empty) + assert(!invoice.isExpired()) + assert(invoice.isValidFor(offer, request)) + val expiredInvoice1 = signInvoice(Bolt12Invoice(TlvStream(invoice.records.records.map { case CreatedAt(_) => CreatedAt(0 unixsec) case x => x }), None), nodeKey) + assert(expiredInvoice1.isExpired()) + assert(!expiredInvoice1.isValidFor(offer, request)) // when an invoice is expired, we mark it as invalid as well + val expiredInvoice2 = signInvoice(Bolt12Invoice(TlvStream(invoice.records.records.map { case CreatedAt(_) => CreatedAt(TimestampSecond.now() - 2000) case x => x } ++ Seq(RelativeExpiry(1800))), None), nodeKey) + assert(expiredInvoice2.isExpired()) + assert(!expiredInvoice2.isValidFor(offer, request)) // when an invoice is expired, we mark it as invalid as well + } + + test("check chain compatibility") { + val amount = 5000 msat + val (nodeKey, payerKey) = (randomKey(), randomKey()) + val (chain1, chain2) = (randomBytes32(), randomBytes32()) + val offerBtc = Offer(Some(amount), "bitcoin offer", nodeKey.publicKey, Features.empty, Block.LivenetGenesisBlock.hash) + val requestBtc = InvoiceRequest(offerBtc, amount, 1, Features.empty, payerKey, Block.LivenetGenesisBlock.hash) + val invoiceImplicitBtc = { + val tlvs: Seq[InvoiceTlv] = Seq( + CreatedAt(TimestampSecond.now()), + PaymentHash(Crypto.sha256(randomBytes32())), + OfferId(offerBtc.offerId), + NodeId(nodeKey.publicKey), + Amount(amount), + Description(offerBtc.description), + PayerKey(payerKey.publicKey) + ) + val signature = signSchnorr(signatureTag("signature"), rootHash(TlvStream(tlvs), invoiceTlvCodec), nodeKey) + Bolt12Invoice(TlvStream(tlvs :+ Signature(signature)), None) + } + assert(invoiceImplicitBtc.isValidFor(offerBtc, requestBtc)) + val invoiceExplicitBtc = { + val tlvs: Seq[InvoiceTlv] = Seq( + Chain(Block.LivenetGenesisBlock.hash), + CreatedAt(TimestampSecond.now()), + PaymentHash(Crypto.sha256(randomBytes32())), + OfferId(offerBtc.offerId), + NodeId(nodeKey.publicKey), + Amount(amount), + Description(offerBtc.description), + PayerKey(payerKey.publicKey) + ) + val signature = signSchnorr(signatureTag("signature"), rootHash(TlvStream(tlvs), invoiceTlvCodec), nodeKey) + Bolt12Invoice(TlvStream(tlvs :+ Signature(signature)), None) + } + assert(invoiceExplicitBtc.isValidFor(offerBtc, requestBtc)) + val invoiceOtherChain = { + val tlvs: Seq[InvoiceTlv] = Seq( + Chain(chain1), + CreatedAt(TimestampSecond.now()), + PaymentHash(Crypto.sha256(randomBytes32())), + OfferId(offerBtc.offerId), + NodeId(nodeKey.publicKey), + Amount(amount), + Description(offerBtc.description), + PayerKey(payerKey.publicKey) + ) + val signature = signSchnorr(signatureTag("signature"), rootHash(TlvStream(tlvs), invoiceTlvCodec), nodeKey) + Bolt12Invoice(TlvStream(tlvs :+ Signature(signature)), None) + } + assert(!invoiceOtherChain.isValidFor(offerBtc, requestBtc)) + val offerOtherChains = Offer(TlvStream(Seq(Chains(Seq(chain1, chain2)), Amount(amount), Description("testnets offer"), NodeId(nodeKey.publicKey)))) + val requestOtherChains = InvoiceRequest(offerOtherChains, amount, 1, Features.empty, payerKey, chain1) + val invoiceOtherChains = { + val tlvs: Seq[InvoiceTlv] = Seq( + Chain(chain1), + CreatedAt(TimestampSecond.now()), + PaymentHash(Crypto.sha256(randomBytes32())), + OfferId(offerOtherChains.offerId), + NodeId(nodeKey.publicKey), + Amount(amount), + Description(offerOtherChains.description), + PayerKey(payerKey.publicKey) + ) + val signature = signSchnorr(signatureTag("signature"), rootHash(TlvStream(tlvs), invoiceTlvCodec), nodeKey) + Bolt12Invoice(TlvStream(tlvs :+ Signature(signature)), None) + } + assert(invoiceOtherChains.isValidFor(offerOtherChains, requestOtherChains)) + val invoiceInvalidOtherChain = { + val tlvs: Seq[InvoiceTlv] = Seq( + Chain(chain2), + CreatedAt(TimestampSecond.now()), + PaymentHash(Crypto.sha256(randomBytes32())), + OfferId(offerOtherChains.offerId), + NodeId(nodeKey.publicKey), + Amount(amount), + Description(offerOtherChains.description), + PayerKey(payerKey.publicKey) + ) + val signature = signSchnorr(signatureTag("signature"), rootHash(TlvStream(tlvs), invoiceTlvCodec), nodeKey) + Bolt12Invoice(TlvStream(tlvs :+ Signature(signature)), None) + } + assert(!invoiceInvalidOtherChain.isValidFor(offerOtherChains, requestOtherChains)) + val invoiceMissingChain = signInvoice(Bolt12Invoice(TlvStream(invoiceOtherChains.records.records.filter { case Chain(_) => false case _ => true }), None), nodeKey) + assert(!invoiceMissingChain.isValidFor(offerOtherChains, requestOtherChains)) + } + + test("decode invoice") { + val nodeKey = PrivateKey(hex"c6a75116a91dc5ff741b079c32c8ce7544656b98f047fb0c0fa011bfb2bb3c05") + val payerKey = PrivateKey(hex"7dd30ec116470c5f7f00af2c7e84968e28cdb43083b33ee832decbe73ec07f1a") + val Success(offer) = Offer.decode("lno1qgsyxjtl6luzd9t3pr62xr7eemp6awnejusgf6gw45q75vcfqqqqqqqgqvqcdgq2pd3xzumfvvsx7enxv4epug9kku8f4e9nuef5lv59yrkdc24t5mtrym62cg085w5wtqkp0rsuly") + assert(offer.amount === Some(100_000 msat)) + assert(offer.nodeIdXOnly === xOnlyPublicKey(nodeKey.publicKey)) + assert(offer.chains === Seq(Block.TestnetGenesisBlock.hash)) + val request = InvoiceRequest(offer, 100_000 msat, 1, Features.empty, payerKey, Block.TestnetGenesisBlock.hash) + val Success(invoice) = Bolt12Invoice.fromString("lni1qvsyxjtl6luzd9t3pr62xr7eemp6awnejusgf6gw45q75vcfqqqqqqqyyp53zuupqkwxpmdq0tjg58ntat5ujpejlvyn92r0l5xzh4wru8e5zzqrqxr2qzstvfshx6tryphkven9wgxqq83qk6msaxhyk0n9xnajs5swehp24wndvvn0ftppu7363evzc9uwrnujvg95tuyy05nqkcdetsaljgq4u6789jllc54qrpjrzzn3c38dj3tscu5qgcs2y4lj5gqlvq50uu7sce478j3j0l599nxfs6svx2cfefgn4a0675893wtzuckqfwlcrcq0qspa9zynlpdk9zzechehkemgaksklylxhr7yfjfx6h696th327nm4nsf52xzq0ukchx69g00c4vvk6kzc5jyklneyy05l9tef7a5jcjn5") + assert(!invoice.isExpired()) + assert(invoice.isValidFor(offer, request)) + } + + test("decode invoice with quantity") { + val nodeKey = PrivateKey(hex"c6a75116a91dc5ff741b079c32c8ce7544656b98f047fb0c0fa011bfb2bb3c05") + val payerKey = PrivateKey(hex"94c7a21a11efa16c5f73b093dc136d9525e2ff40ea7a958c43c1f6004bf6a676") + val Success(offer) = Offer.decode("lno1pqpzwyq2pf382mrtyphkven9wgtqzqgcqy9pug9kku8f4e9nuef5lv59yrkdc24t5mtrym62cg085w5wtqkp0rsuly") + assert(offer.amount === Some(10_000 msat)) + assert(offer.nodeIdXOnly === xOnlyPublicKey(nodeKey.publicKey)) + assert(offer.chains === Seq(Block.LivenetGenesisBlock.hash)) + val request = InvoiceRequest(offer, 50_000 msat, 5, Features.empty, payerKey, Block.LivenetGenesisBlock.hash) + val Success(invoice) = Bolt12Invoice.fromString("lni1qss8u47nw2lsgml7fy4jaqwph9f8cl83zfrrhxccvh6076avqzzzv4qgqtp4qzs2vf6kc6eqdanxvetjpsqpug9kku8f4e9nuef5lv59yrkdc24t5mtrym62cg085w5wtqkp0rsulysqzpfxyrat02l8wtgtwuc4h5hw6dxhn0hcpdrtu3dpejfjdlw9h4j3nppxc2qyvg9z0lf2yq7wl9ygd6td4cj7whp3ye4cfxrtu7zq4r2mc0mcdspk3duzv7d0stqyh0upuq8sgr44r7aaluwqfw8pkd9f3cgk7ae2l8rkexznhegr0p7w4mlhvfkvlnr5k2lnw0hhsf6ckys3sst7kng5p7m2pxlvdxl3tan809vkk75j") + assert(!invoice.isExpired()) + assert(invoice.amount === 50_000.msat) + assert(invoice.quantity === Some(5)) + assert(invoice.isValidFor(offer, request)) + } + + test("decode invalid invoice") { + val testCases = Seq( + // Missing amount. + "lni1qssqkquyqnwldm8mjekcm7ztejf0dzhwyvh95l5fz06ztnpfm0sgc3c2pf6x2um5yphkven9wgxqq83q2hfdphr3r8x07ej0z0swprnll58z4jlw36wye7kw63ssm95ru8qjvgxj40x4favsm7uue24lhleg7gng2r69g2plgwxm8xmpuw7wmnyh455qgcs29g3j5g8vpnxqvnzwu0sgqc20hdllxzr4qnpu0zge6drn8p3galht8f62uncyqyrhddpcla8pprdxwdkmjmutya0kvnrpeqjqa75sr02wff0le52ydckr8ww09gg4jyvxyma903fhh6v8t4edftg7vw6qz7h0tz20p5wq", + // Missing node id. + "lni1qssqkquyqnwldm8mjekcm7ztejf0dzhwyvh95l5fz06ztnpfm0sgc3cgqgfcszs2w3jhxapqdanxvetjpsqzvgxj40x4favsm7uue24lhleg7gng2r69g2plgwxm8xmpuw7wmnyh455qgcs29g3j5g8vpnxqvnzwu0sgqc20hdllxzr4qnpu0zge6drn8p3galht8f62uncypc7vg95clf4z8fklzua72nhavtjp5qp0u7pemgpecypn6q809qr5733c9y0thf6fdnegxleeupddgzgsyszrktay6cjv9cld3wzlw5pq", + // Missing payment hash. + "lni1qssqkquyqnwldm8mjekcm7ztejf0dzhwyvh95l5fz06ztnpfm0sgc3cgqgfcszs2w3jhxapqdanxvetjpsqpugz46tgdcugeenlkvncnursgullapc4vhm5wn3x04nk5vyxedqlpcynzp54te420tyxlh8x240al728jy6zs732zs06r3keekc0rhnkue9ad9qzxyz32y0cyqhfql7g2dp8w0s5xc0ccgelq4hrnkgmxxltvdq95rqzpf4e68j60h6dysm3evhnu4rwtrqp3dnekmk9sxklw267axtj6zangxtnfx2pq", + // Missing description. + "lni1qssqkquyqnwldm8mjekcm7ztejf0dzhwyvh95l5fz06ztnpfm0sgc3cgqgfcsrqqrcs9t5ksm3c3nn8lve838c8q3ell6r32e0hga8zvlt8dgcgdj6p7rsfxyrf2hn257kgdlwwv42lmlu50yf59paz59ql58rdnnds7808dejt662qyvg9z5ge2yrkqenqxf38w8cyqv98mkllnpp6sfs783yvax3ensc5wlm4n5a9wfuzqjraxg7gnskte8m9lzn2j5r55a4n3nhhfnflzd5953tau0h2auztf9und4psz6p34wx4vsjxwyvc33lezqvm208ntdczqneylzznt0cg", + // Missing creation date. + "lni1qssqkquyqnwldm8mjekcm7ztejf0dzhwyvh95l5fz06ztnpfm0sgc3cgqgfcszs2w3jhxapqdanxvetjpsqpugz46tgdcugeenlkvncnursgullapc4vhm5wn3x04nk5vyxedqlpcynzp54te420tyxlh8x240al728jy6zs732zs06r3keekc0rhnkue9ad9gswcrxvqexyaclqsps5lwml7vy82pxrc7y3n568xwrz3mlwkwn54e8sgp22vcpqylq4zcxep5faeysey8ucu6wrfgffphlt457rd9f7tlpnyltlqz0yd9eqjcehyu3zwvear2e4ksx32t3qtek9gucrephdmsk0", + // Missing signature. + "lni1qssqkquyqnwldm8mjekcm7ztejf0dzhwyvh95l5fz06ztnpfm0sgc3cgqgfcszs2w3jhxapqdanxvetjpsqpugz46tgdcugeenlkvncnursgullapc4vhm5wn3x04nk5vyxedqlpcynzp54te420tyxlh8x240al728jy6zs732zs06r3keekc0rhnkue9ad9qzxyz32yv4zpmqvesrycnhruzqxznam0lessagyc0rcjxwnguecv280a6e6wjhy", + ) + for (testCase <- testCases) { + assert(Bolt12Invoice.fromString(testCase).isFailure, testCase) + } + } + + test("encode/decode invoice with many fields") { + val chain = Block.TestnetGenesisBlock.hash + val offerId = ByteVector32.fromValidHex("8bc5978de5d625c90136dfa896a8a02cef33c5457027684687e3f98e0cfca4f0") + val amount = 123456 msat + val description = "invoice with many fields" + val features = Features[Feature](Features.VariableLengthOnion -> FeatureSupport.Mandatory) + val issuer = "acinq.co" + val nodeKey = PrivateKey(hex"998cf8ecab46f949bb960813b79d3317cabf4193452a211795cd8af1b9a25d90") + val quantity = 57 + val payerKey = ByteVector32.fromValidHex("8faadd71b1f78b16265e5b061b9d2b88891012dc7ad38626eeaaa2a271615a65") + val payerNote = "I'm the king" + val payerInfo = hex"a9eb6e526eac59cd9b89fb20" + val createdAt = TimestampSecond(1654654654L) + val paymentHash = ByteVector32.fromValidHex("51951d4c53c904035f0b293dc9df1c0e7967213430ae07a5f3e134cd33325341") + val relativeExpiry = 3600 + val cltv = CltvExpiryDelta(123) + val fallbacks = Seq(FallbackAddress(4, hex"123d56f8"), FallbackAddress(6, hex"eb3adc68945ef601")) + val replaceInvoice = ByteVector32.fromValidHex("71ad033e5f42068225608770fa7672505449425db543a1f9c23bf03657aa37c1") + val tlvs = TlvStream[InvoiceTlv](Seq( + Chain(chain), + OfferId(offerId), + Amount(amount), + Description(description), + FeaturesTlv(features), + Issuer(issuer), + NodeId(nodeKey.publicKey), + Quantity(quantity), + PayerKey(payerKey), + PayerNote(payerNote), + PayerInfo(payerInfo), + CreatedAt(createdAt), + PaymentHash(paymentHash), + RelativeExpiry(relativeExpiry), + Cltv(cltv), + Fallbacks(fallbacks), + ReplaceInvoice(replaceInvoice) + ), Seq(GenericTlv(UInt64(311), hex"010203"), GenericTlv(UInt64(313), hex""))) + val signature = signSchnorr(Bolt12Invoice.signatureTag("signature"), rootHash(tlvs, invoiceTlvCodec), nodeKey) + val invoice = Bolt12Invoice(tlvs.copy(records = tlvs.records ++ Seq(Signature(signature))), None) + assert(invoice.toString === "lni1qvsyxjtl6luzd9t3pr62xr7eemp6awnejusgf6gw45q75vcfqqqqqqqyyz9ut9uduhtztjgpxm06394g5qkw7v79g4czw6zxsl3lnrsvljj0qzqrq83yqzscd9h8vmmfvdjjqamfw35zqmtpdeujqenfv4kxgucvqgqsq9qgv93kjmn39e3k783qc3nnudswp67znrydjtv7ta56c9cpc0nmjmv7rszs568gqdz3w77zqqfeycsgl2kawxcl0zckye09kpsmn54c3zgsztw845uxymh24g4zw9s45ef8p3yjwmfqw35x2grtd9hxw2qyv2sqd032ypge282v20ysgq6lpv5nmjwlrs88jeepxsc2upa970snfnfnxff5ztqzpcgzuqsq0vcpgpcyqqzpy02klq9svqqgavadc6y5tmmqzvsv484ku5nw43vumxuflvsrsgr345pnuh6zq6pz2cy8wra8vujs23y5yhd4gwslns3m7qm9023hc8cyq6knwqxzve9r5kpufq3szuhn9f437cj05az5kqnsl9wefhfnwzenf5z68qh5jj48rmku97u0gzdm2wlkuwrylpvqfttdtw972cwdteal6qfhqvqsyqlaqyusq") + val Success(codedDecoded) = Bolt12Invoice.fromString(invoice.toString) + assert(codedDecoded.chain === chain) + assert(codedDecoded.offerId === Some(offerId)) + assert(codedDecoded.amount === amount) + assert(codedDecoded.description === Left(description)) + assert(codedDecoded.features === features) + assert(codedDecoded.issuer === Some(issuer)) + assert(codedDecoded.nodeId.value.drop(1) === nodeKey.publicKey.value.drop(1)) + assert(codedDecoded.quantity === Some(quantity)) + assert(codedDecoded.payerKey === Some(payerKey)) + assert(codedDecoded.payerNote === Some(payerNote)) + assert(codedDecoded.payerInfo === Some(payerInfo)) + assert(codedDecoded.createdAt === createdAt) + assert(codedDecoded.paymentHash === paymentHash) + assert(codedDecoded.relativeExpiry === relativeExpiry.seconds) + assert(codedDecoded.minFinalCltvExpiryDelta === cltv) + assert(codedDecoded.fallbacks === Some(fallbacks)) + assert(codedDecoded.replaceInvoice === Some(replaceInvoice)) + assert(codedDecoded.records.unknown.toSet === Set(GenericTlv(UInt64(311), hex"010203"), GenericTlv(UInt64(313), hex""))) + } + +} diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/OffersSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/OffersSpec.scala new file mode 100644 index 0000000000..c282d943bc --- /dev/null +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/OffersSpec.scala @@ -0,0 +1,283 @@ +/* + * 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.wire.protocol + +import fr.acinq.bitcoin.scalacompat.Crypto.PrivateKey +import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32} +import fr.acinq.eclair.FeatureSupport.{Mandatory, Optional} +import fr.acinq.eclair.Features.{BasicMultiPartPayment, VariableLengthOnion} +import fr.acinq.eclair.wire.protocol.OfferCodecs.invoiceRequestTlvCodec +import fr.acinq.eclair.wire.protocol.Offers._ +import fr.acinq.eclair.{Features, MilliSatoshiLong, randomBytes32, randomKey} +import org.scalatest.funsuite.AnyFunSuite +import scodec.bits.{ByteVector, HexStringSyntax} + +import scala.util.Success + +class OffersSpec extends AnyFunSuite { + val nodeId = ByteVector32(hex"4b9a1fa8e006f1e3937f65f66c408e6da8e1ca728ea43222a7381df1cc449605") + + test("sign and check offer") { + val key = randomKey() + val offer = Offer(Some(100_000 msat), "test offer", key.publicKey, Features(VariableLengthOnion -> Mandatory), Block.LivenetGenesisBlock.hash) + assert(offer.signature.isEmpty) + val signedOffer = offer.sign(key) + assert(signedOffer.checkSignature()) + } + + test("invoice request is signed") { + val sellerKey = randomKey() + val offer = Offer(Some(100_000 msat), "test offer", sellerKey.publicKey, Features.empty, Block.LivenetGenesisBlock.hash) + val payerKey = randomKey() + val request = InvoiceRequest(offer, 100_000 msat, 1, Features.empty, payerKey, Block.LivenetGenesisBlock.hash) + assert(request.checkSignature()) + } + + test("basic offer") { + val encoded = "lno1pg257enxv4ezqcneype82um50ynhxgrwdajx283qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczs" + val offer = Offer.decode(encoded).get + assert(offer.amount.isEmpty) + assert(offer.signature.isEmpty) + assert(offer.description === "Offer by rusty's node") + assert(offer.nodeIdXOnly === nodeId) + } + + test("basic signed offer") { + val encodedSigned = "lno1pg257enxv4ezqcneype82um50ynhxgrwdajx283qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqs85ck65ycmkdk92smwt9zuewdzfe7v4aavvaz5kgv9mkk63v3s0ge0f099kssh3yc95qztx504hu92hnx8ctzhtt08pgk0texz0509tk" + val Success(signedOffer) = Offer.decode(encodedSigned) + assert(signedOffer.checkSignature()) + assert(signedOffer.amount.isEmpty) + assert(signedOffer.description === "Offer by rusty's node") + assert(signedOffer.nodeIdXOnly === nodeId) + } + + test("offer with amount and quantity") { + val encoded = "lno1pqqnyzsmx5cx6umpwssx6atvw35j6ut4v9h8g6t50ysx7enxv4epgrmjw4ehgcm0wfczucm0d5hxzagkqyq3ugztng063cqx783exlm97ekyprnd4rsu5u5w5sez9fecrhcuc3ykq5" + val Success(offer) = Offer.decode(encoded) + assert(offer.amount === Some(50 msat)) + assert(offer.signature.isEmpty) + assert(offer.description === "50msat multi-quantity offer") + assert(offer.nodeIdXOnly === nodeId) + assert(offer.issuer === Some("rustcorp.com.au")) + assert(offer.quantityMin === Some(1)) + } + + test("signed offer with amount and quantity") { + val encodedSigned = "lno1pqqnyzsmx5cx6umpwssx6atvw35j6ut4v9h8g6t50ysx7enxv4epgrmjw4ehgcm0wfczucm0d5hxzagkqyq3ugztng063cqx783exlm97ekyprnd4rsu5u5w5sez9fecrhcuc3ykqhcypjju7unu05vav8yvhn27lztf46k9gqlga8uvu4uq62kpuywnu6me8srgh2q7puczukr8arectaapfl5d4rd6uc7st7tnqf0ttx39n40s" + val Success(signedOffer) = Offer.decode(encodedSigned) + assert(signedOffer.checkSignature()) + assert(signedOffer.amount === Some(50 msat)) + assert(signedOffer.description === "50msat multi-quantity offer") + assert(signedOffer.nodeIdXOnly === nodeId) + assert(signedOffer.issuer === Some("rustcorp.com.au")) + assert(signedOffer.quantityMin === Some(1)) + } + + test("decode invalid offer") { + val testCases = Seq( + "lno1pgxx7enxv4e8xgrjda3kkgg", // missing node id + "lno1rcsdhss957tylk58rmly849jupnmzs52ydhxhl8fgz7994xkf2hnwhg", // missing description + ) + for (testCase <- testCases) { + assert(Offer.decode(testCase).isFailure) + } + } + + def signInvoiceRequest(request: InvoiceRequest, key: PrivateKey): InvoiceRequest = { + val tlvs = removeSignature(request.records) + val signature = signSchnorr(InvoiceRequest.signatureTag, rootHash(tlvs, invoiceRequestTlvCodec), key) + val signedRequest = InvoiceRequest(tlvs.copy(records = tlvs.records ++ Seq(Signature(signature)))) + assert(signedRequest.checkSignature()) + signedRequest + } + + test("check that invoice request matches offer") { + val offer = Offer(Some(2500 msat), "basic offer", randomKey().publicKey, Features.empty, Block.LivenetGenesisBlock.hash) + val payerKey = randomKey() + val request = InvoiceRequest(offer, 2500 msat, 1, Features.empty, payerKey, Block.LivenetGenesisBlock.hash) + assert(request.isValidFor(offer)) + val biggerAmount = signInvoiceRequest(request.copy(records = TlvStream(request.records.records.map { case Amount(_) => Amount(3000 msat) case x => x })), payerKey) + assert(biggerAmount.isValidFor(offer)) + val lowerAmount = signInvoiceRequest(request.copy(records = TlvStream(request.records.records.map { case Amount(_) => Amount(2000 msat) case x => x })), payerKey) + assert(!lowerAmount.isValidFor(offer)) + val otherOfferId = signInvoiceRequest(request.copy(records = TlvStream(request.records.records.map { case OfferId(_) => OfferId(randomBytes32()) case x => x })), payerKey) + assert(!otherOfferId.isValidFor(offer)) + val withQuantity = signInvoiceRequest(request.copy(records = TlvStream(request.records.records ++ Seq(Quantity(1)))), payerKey) + assert(!withQuantity.isValidFor(offer)) + } + + test("check that invoice request matches offer (with features)") { + val offer = Offer(Some(2500 msat), "offer with features", randomKey().publicKey, Features(VariableLengthOnion -> Optional), Block.LivenetGenesisBlock.hash) + val payerKey = randomKey() + val request = InvoiceRequest(offer, 2500 msat, 1, Features(VariableLengthOnion -> Mandatory, BasicMultiPartPayment -> Optional), payerKey, Block.LivenetGenesisBlock.hash) + assert(request.isValidFor(offer)) + val withoutFeatures = InvoiceRequest(offer, 2500 msat, 1, Features.empty, payerKey, Block.LivenetGenesisBlock.hash) + assert(withoutFeatures.isValidFor(offer)) + val otherFeatures = InvoiceRequest(offer, 2500 msat, 1, Features(VariableLengthOnion -> Mandatory, BasicMultiPartPayment -> Mandatory), payerKey, Block.LivenetGenesisBlock.hash) + assert(!otherFeatures.isValidFor(offer)) + } + + test("check that invoice request matches offer (without amount)") { + val offer = Offer(None, "offer without amount", randomKey().publicKey, Features.empty, Block.LivenetGenesisBlock.hash) + val payerKey = randomKey() + val request = InvoiceRequest(offer, 500 msat, 1, Features.empty, payerKey, Block.LivenetGenesisBlock.hash) + assert(request.isValidFor(offer)) + val withoutAmount = signInvoiceRequest(request.copy(records = TlvStream(request.records.records.filter { case Amount(_) => false case _ => true })), payerKey) + assert(!withoutAmount.isValidFor(offer)) + } + + test("check that invoice request matches offer (chain compatibility)") { + { + val offer = Offer(TlvStream(Seq(Amount(100 msat), Description("offer without chains"), NodeId(randomKey().publicKey)))) + val payerKey = randomKey() + val request = { + val tlvs: Seq[InvoiceRequestTlv] = Seq( + OfferId(offer.offerId), + Amount(100 msat), + PayerKey(payerKey.publicKey), + FeaturesTlv(Features.empty) + ) + val signature = signSchnorr(InvoiceRequest.signatureTag, rootHash(TlvStream(tlvs), invoiceRequestTlvCodec), payerKey) + InvoiceRequest(TlvStream(tlvs :+ Signature(signature))) + } + assert(request.isValidFor(offer)) + val withDefaultChain = signInvoiceRequest(request.copy(records = TlvStream(request.records.records ++ Seq(Chain(Block.LivenetGenesisBlock.hash)))), payerKey) + assert(withDefaultChain.isValidFor(offer)) + val otherChain = signInvoiceRequest(request.copy(records = TlvStream(request.records.records ++ Seq(Chain(Block.TestnetGenesisBlock.hash)))), payerKey) + assert(!otherChain.isValidFor(offer)) + } + { + val (chain1, chain2) = (randomBytes32(), randomBytes32()) + val offer = Offer(TlvStream(Seq(Chains(Seq(chain1, chain2)), Amount(100 msat), Description("offer with chains"), NodeId(randomKey().publicKey)))) + val payerKey = randomKey() + val request1 = InvoiceRequest(offer, 100 msat, 1, Features.empty, payerKey, chain1) + assert(request1.isValidFor(offer)) + val request2 = InvoiceRequest(offer, 100 msat, 1, Features.empty, payerKey, chain2) + assert(request2.isValidFor(offer)) + val noChain = signInvoiceRequest(request1.copy(records = TlvStream(request1.records.records.filter { case Chain(_) => false case _ => true })), payerKey) + assert(!noChain.isValidFor(offer)) + val otherChain = signInvoiceRequest(request1.copy(records = TlvStream(request1.records.records.map { case Chain(_) => Chain(Block.LivenetGenesisBlock.hash) case x => x })), payerKey) + assert(!otherChain.isValidFor(offer)) + } + } + + test("check that invoice request matches offer (multiple items)") { + val offer = Offer(TlvStream( + Amount(500 msat), + Description("offer for multiple items"), + NodeId(randomKey().publicKey), + QuantityMin(3), + QuantityMax(10), + )) + val payerKey = randomKey() + val request = InvoiceRequest(offer, 1600 msat, 3, Features.empty, payerKey, Block.LivenetGenesisBlock.hash) + assert(request.records.get[Quantity].nonEmpty) + assert(request.isValidFor(offer)) + val invalidAmount = InvoiceRequest(offer, 2400 msat, 5, Features.empty, payerKey, Block.LivenetGenesisBlock.hash) + assert(!invalidAmount.isValidFor(offer)) + val tooFewItems = InvoiceRequest(offer, 1000 msat, 2, Features.empty, payerKey, Block.LivenetGenesisBlock.hash) + assert(!tooFewItems.isValidFor(offer)) + val tooManyItems = InvoiceRequest(offer, 5500 msat, 11, Features.empty, payerKey, Block.LivenetGenesisBlock.hash) + assert(!tooManyItems.isValidFor(offer)) + } + + test("decode invoice request") { + val encoded = "lnr1qvsxlc5vp2m0rvmjcxn2y34wv0m5lyc7sdj7zksgn35dvxgqqqqqqqqyypz8xu3xwsqpar9dd26lgrrvc7s63ljt0pgh6ag2utv5udez7n2mjzqzz47qcqczqgqzqqgzycsv2tmjgzc5l546aldq699wj9pdusvfred97l352p4aa862vqvzw5p8pdyjqctdyppxzardv9hrypx74klwluzqd0rqgeew2uhuagttuv6aqwklvm0xmlg52lfnagzw8ygt0wrtnv2tsx69m6tgug7njaw5ypa5fn369n9yzc87v02rqccj9h04dxf3nzc" + val Success(request) = InvoiceRequest.decode(encoded) + assert(request.amount === Some(5500 msat)) + assert(request.offerId === ByteVector32(hex"4473722674001e8cad6ab5f40c6cc7a1a8fe4b78517d750ae2d94e3722f4d5b9")) + assert(request.quantity === 2) + assert(request.features === Features(VariableLengthOnion -> Optional, BasicMultiPartPayment -> Optional)) + assert(request.records.get[Chain].nonEmpty) + assert(request.chain === Block.LivenetGenesisBlock.hash) + assert(request.payerKey === ByteVector32(hex"c52f7240b14fd2baefda0d14ae9142de41891e5a5f7e34506bde9f4a60182750")) + assert(request.payerInfo === Some(hex"deadbeef")) + assert(request.payerNote === Some("I am Batman")) + assert(request.encode() === encoded) + } + + test("decode invalid invoice request") { + val testCases = Seq( + // Missing offer id. + "lnr1pqpp8zqvqqnzqq7pw52tqj6pj2mar5cgkmnt9xe3tj40nxc3pp95xml2e8v432ny7pq957u2v4r5cjxfmxtwk9qfu99hftq2ek48pz6c2ywynajha03ut4ffjf34htxxxp668dqd9jwvz2eal6up5mjfe4ad8ndccrtpkkke0g", + // Missing payer key. + "lnr1qss0h356hn94473j5yls8q3w4gkzu9j8rrach3hgms4ks8aumsx29vsgqgfcsrqq7pq957u2v4r5cjxfmxtwk9qfu99hftq2ek48pz6c2ywynajha03ut4ffjf34htxxxp668dqd9jwvz2eal6up5mjfe4ad8ndccrtpkkke0g", + // Missing signature. + "lnr1qss0h356hn94473j5yls8q3w4gkzu9j8rrach3hgms4ks8aumsx29vsgqgfcsrqqycsq8st4zjcyksvjklgaxz9ku6efkv2u4tuekyggfdpkl6kfm9v25eq", + ) + for (testCase <- testCases) { + assert(InvoiceRequest.decode(testCase).isFailure) + } + } + + test("compute merkle tree root") { + import scodec.Codec + import scodec.codecs.list + + case class TestCase(tlvs: ByteVector, count: Int, expected: ByteVector32) + + val testCases = Seq( + // Official test vectors. + TestCase(hex"010203e8", 1, ByteVector32(hex"aa0aa0f694c85492ac459c1de9831a37682985f5e840ecc9b1e28eece7dc5236")), + TestCase(hex"010203e8 02080000010000020003", 2, ByteVector32(hex"013b756ed73554cbc4dd3d90f363cb7cba6d8a279465a21c464e582b173ff502")), + TestCase(hex"010203e8 02080000010000020003 03310266e4598d1d3c415f572a8488830b60f7e744ed9235eb0b1ba93283b315c0351800000000000000010000000000000002", 3, ByteVector32(hex"016fcda3b6f9ca30b35936877ca591fa101365a761a1453cfd9436777d593656")), + TestCase(hex"0603555344 080203e8 0a0f313055534420657665727920646179 141072757374792e6f7a6c6162732e6f7267 1a020101 1e204b9a1fa8e006f1e3937f65f66c408e6da8e1ca728ea43222a7381df1cc449605", 6, ByteVector32(hex"7cef68df49fd9222bed0138ca5603da06464b2f523ea773bc4edcb6bd07966e7")), + // Additional test vectors. + TestCase(hex"010100", 1, ByteVector32(hex"c8112b235945b06a11995bf69956a93ff0403c28de35bd33b4714da1b6239ebb")), + TestCase(hex"010100 020100", 2, ByteVector32(hex"8271d606bea3ef49e59d610585317edfc6c53d8d1afd763731919d9a7d70a7d9")), + TestCase(hex"010100 020100 030100", 3, ByteVector32(hex"c7eff290817749d87eede061d5335559e8211769e651a2ee5c5e7d2ddd655236")), + TestCase(hex"010100 020100 030100 040100", 4, ByteVector32(hex"57883ef2f1e8df4a23e6f0a2e3acda2ed0b11e00ef2d39fe1caa2d71d7273c37")), + TestCase(hex"010100 020100 030100 040100 050100", 5, ByteVector32(hex"85b74f254eced46c525a5369c52f86f249a41f6f6ccb3c918ffe4025ea22d8b6")), + TestCase(hex"010100 020100 030100 040100 050100 060100", 6, ByteVector32(hex"6cf27da8a67b7cb199dd1824017cb008bd22bf1d57273a8c4544c5408275dc2d")), + TestCase(hex"010100 020100 030100 040100 050100 060100 070100", 7, ByteVector32(hex"2a038f022b51b1b969563679a22eb167ef603d5b2cb2d0dbe86dc4d2f48d8c6e")), + TestCase(hex"010100 020100 030100 040100 050100 060100 070100 080100", 8, ByteVector32(hex"8ddbe97f6ed2e2a4a43e828e350f9cb6679b7d5f16837273cf0e6f7da342fa19")), + TestCase(hex"010100 020100 030100 040100 050100 060100 070100 080100 090100", 9, ByteVector32(hex"8050bed857ff7929a24251e3a517fc14f46fb0f02e6719cb9d53087f7f047f6d")), + TestCase(hex"010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100", 10, ByteVector32(hex"e22aa818e746ff9613e6cccc99ebce257d93c35736b168b6b478d6f3762f56ce")), + TestCase(hex"010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100", 11, ByteVector32(hex"626e3159cec72534155ccf691a84ab68da89e6cd679a118c70a28fd1f1bb10cc")), + TestCase(hex"010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100 0c0100", 12, ByteVector32(hex"f659da1c839d99a2c6b104d179ee44ffe3a9eaa55831de3c612c8c293c27401b")), + TestCase(hex"010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100 0c0100 0d0100", 13, ByteVector32(hex"c165756a94718d19e66ff7b581347699009a9e17805e16cb6ba94c034c7dc757")), + TestCase(hex"010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100 0c0100 0d0100 0e0100", 14, ByteVector32(hex"573b85bbceacbf1b189412858ac6573e923bbf0c9cfdc37d37757996f6086208")), + TestCase(hex"010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100 0c0100 0d0100 0e0100 0f0100", 15, ByteVector32(hex"84a3088fe74b82ee82a9415d48fdfad8dc6a286fec8e6fcdcefcf0bc02f3256e")), + TestCase(hex"010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100 0c0100 0d0100 0e0100 0f0100 100100", 16, ByteVector32(hex"a686f116fce33e43fa875fec2253c71694a0324e1ce7640ed1070b0cc3a14cc1")), + TestCase(hex"010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100 0c0100 0d0100 0e0100 0f0100 100100 110100", 17, ByteVector32(hex"fbee87d6726c8b67a8d2e2bff92b13d0b1d9188f9d42af2d3afefceaafa6f3e5")), + TestCase(hex"010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100 0c0100 0d0100 0e0100 0f0100 100100 110100 120100", 18, ByteVector32(hex"5004f619c426b01e57c480a84d5dcdc3a70b4bf575ec573be60c3a75ed978b72")), + TestCase(hex"010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100 0c0100 0d0100 0e0100 0f0100 100100 110100 120100 130100", 19, ByteVector32(hex"6f0a5e59f1fa5dc6c12ed3bbe0eb91c818b22a8d011d5a2160462c59e6158a58")), + TestCase(hex"010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100 0c0100 0d0100 0e0100 0f0100 100100 110100 120100 130100 140100", 20, ByteVector32(hex"e43f00c262e4578c5ed4413ab340e79cb8b258241b7c52550b7307f7b9c4d645")), + TestCase(hex"010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100 0c0100 0d0100 0e0100 0f0100 100100 110100 120100 130100 140100 150100", 21, ByteVector32(hex"e46776637883bae1a62cbfb621c310c13e6c522092954e08d74c08328d53f035")), + TestCase(hex"010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100 0c0100 0d0100 0e0100 0f0100 100100 110100 120100 130100 140100 150100 160100", 22, ByteVector32(hex"813ebe9f07638005abbe270f11ae2749a5b9b0c5cf89a305598303a38f5f2da5")), + TestCase(hex"010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100 0c0100 0d0100 0e0100 0f0100 100100 110100 120100 130100 140100 150100 160100 170100", 23, ByteVector32(hex"fdd7b779192dcbadb5695303e2bcee0fc175428278bdbfa4b4445251df6c9450")), + TestCase(hex"010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100 0c0100 0d0100 0e0100 0f0100 100100 110100 120100 130100 140100 150100 160100 170100 180100", 24, ByteVector32(hex"33c92b7820742d094548328ee3bfdf29bf3fe1f971171dcd2a6da0f185dceddb")), + TestCase(hex"010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100 0c0100 0d0100 0e0100 0f0100 100100 110100 120100 130100 140100 150100 160100 170100 180100 190100", 25, ByteVector32(hex"888da09f5ba1b8e431b3ab1db62fca94c0cbbec6b145012d9308d20f68571ff2")), + TestCase(hex"010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100 0c0100 0d0100 0e0100 0f0100 100100 110100 120100 130100 140100 150100 160100 170100 180100 190100 1a0100", 26, ByteVector32(hex"a87cdc040109b855d81f13af4a6f57cdb7e31252eeb83bc03518fdd6dd81ec18")), + TestCase(hex"010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100 0c0100 0d0100 0e0100 0f0100 100100 110100 120100 130100 140100 150100 160100 170100 180100 190100 1a0100 1b0100", 27, ByteVector32(hex"9829715a0d8cbb5c080de53704f274aa4da3590e8338d57ce99ab491d7a44e76")), + TestCase(hex"010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100 0c0100 0d0100 0e0100 0f0100 100100 110100 120100 130100 140100 150100 160100 170100 180100 190100 1a0100 1b0100 1c0100", 28, ByteVector32(hex"ce8bac7c3d10b528d59d5f391bf36bb6acd65b4bb3cbd0a769488e3b451b2c26")), + TestCase(hex"010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100 0c0100 0d0100 0e0100 0f0100 100100 110100 120100 130100 140100 150100 160100 170100 180100 190100 1a0100 1b0100 1c0100 1d0100", 29, ByteVector32(hex"88d29ac3e4ae8761058af4b1baaa873ec4f76822166f8dfc2888bcbb51212130")), + TestCase(hex"010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100 0c0100 0d0100 0e0100 0f0100 100100 110100 120100 130100 140100 150100 160100 170100 180100 190100 1a0100 1b0100 1c0100 1d0100 1e0100", 30, ByteVector32(hex"b013259fe32c6eaf88d2b3b2d01350e5505bcc0fcdcdc7c360e5644fe827424d")), + TestCase(hex"010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100 0c0100 0d0100 0e0100 0f0100 100100 110100 120100 130100 140100 150100 160100 170100 180100 190100 1a0100 1b0100 1c0100 1d0100 1e0100 1f0100", 31, ByteVector32(hex"1c60489269d312c2ea94c637936e38a968d2900cab6c5544db091aa8b3bb5176")), + TestCase(hex"010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100 0c0100 0d0100 0e0100 0f0100 100100 110100 120100 130100 140100 150100 160100 170100 180100 190100 1a0100 1b0100 1c0100 1d0100 1e0100 1f0100 200100", 32, ByteVector32(hex"e0d88bd7685ffd55e0de4e45e190e7e6bf1ecc0a7d1a32fbdaa6b1b27e8bc37b")), + ) + testCases.foreach { + case TestCase(tlvStream, tlvCount, expectedRoot) => + val genericTlvStream: Codec[TlvStream[GenericTlv]] = list(TlvCodecs.genericTlv).xmap(tlvs => TlvStream(tlvs), tlvs => tlvs.records.toList) + val tlvs = genericTlvStream.decode(tlvStream.bits).require.value + assert(tlvs.records.size === tlvCount) + val root = Offers.rootHash(tlvs, genericTlvStream) + assert(root === expectedRoot) + } + } + +} diff --git a/eclair-node/src/main/scala/fr/acinq/eclair/api/serde/FormParamExtractors.scala b/eclair-node/src/main/scala/fr/acinq/eclair/api/serde/FormParamExtractors.scala index b02bd3178a..4843701bd6 100644 --- a/eclair-node/src/main/scala/fr/acinq/eclair/api/serde/FormParamExtractors.scala +++ b/eclair-node/src/main/scala/fr/acinq/eclair/api/serde/FormParamExtractors.scala @@ -44,7 +44,7 @@ object FormParamExtractors { implicit val sha256HashesUnmarshaller: Unmarshaller[String, List[ByteVector32]] = listUnmarshaller(bin => ByteVector32.fromValidHex(bin)) - implicit val bolt11Unmarshaller: Unmarshaller[String, Bolt11Invoice] = Unmarshaller.strict { rawRequest => Bolt11Invoice.fromString(rawRequest) } + implicit val bolt11Unmarshaller: Unmarshaller[String, Bolt11Invoice] = Unmarshaller.strict { rawRequest => Bolt11Invoice.fromString(rawRequest).get } implicit val shortChannelIdUnmarshaller: Unmarshaller[String, ShortChannelId] = Unmarshaller.strict { str => ShortChannelId(str) } diff --git a/eclair-node/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala b/eclair-node/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala index fcef0b94e9..fdd2a4e045 100644 --- a/eclair-node/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala +++ b/eclair-node/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala @@ -824,7 +824,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM test("'getreceivedinfo' 2") { val invoice = "lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpuaztrnwngzn3kdzw5hydlzf03qdgm2hdq27cqv3agm2awhz5se903vruatfhq77w3ls4evs3ch9zw97j25emudupq63nyw24cg27h2rspfj9srp" - val defaultPayment = IncomingPayment(Bolt11Invoice.fromString(invoice), ByteVector32.One, PaymentType.Standard, 42 unixms, IncomingPaymentStatus.Pending) + val defaultPayment = IncomingPayment(Bolt11Invoice.fromString(invoice).get, ByteVector32.One, PaymentType.Standard, 42 unixms, IncomingPaymentStatus.Pending) val eclair = mock[Eclair] val pending = randomBytes32() eclair.receivedInfo(pending)(any) returns Future.successful(Some(defaultPayment)) @@ -844,7 +844,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM test("'getreceivedinfo' 3") { val invoice = "lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpuaztrnwngzn3kdzw5hydlzf03qdgm2hdq27cqv3agm2awhz5se903vruatfhq77w3ls4evs3ch9zw97j25emudupq63nyw24cg27h2rspfj9srp" - val defaultPayment = IncomingPayment(Bolt11Invoice.fromString(invoice), ByteVector32.One, PaymentType.Standard, 42 unixms, IncomingPaymentStatus.Pending) + val defaultPayment = IncomingPayment(Bolt11Invoice.fromString(invoice).get, ByteVector32.One, PaymentType.Standard, 42 unixms, IncomingPaymentStatus.Pending) val eclair = mock[Eclair] val expired = randomBytes32() eclair.receivedInfo(expired)(any) returns Future.successful(Some(defaultPayment.copy(status = IncomingPaymentStatus.Expired))) @@ -864,7 +864,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM test("'getreceivedinfo' 4") { val invoice = "lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpuaztrnwngzn3kdzw5hydlzf03qdgm2hdq27cqv3agm2awhz5se903vruatfhq77w3ls4evs3ch9zw97j25emudupq63nyw24cg27h2rspfj9srp" - val defaultPayment = IncomingPayment(Bolt11Invoice.fromString(invoice), ByteVector32.One, PaymentType.Standard, 42 unixms, IncomingPaymentStatus.Pending) + val defaultPayment = IncomingPayment(Bolt11Invoice.fromString(invoice).get, ByteVector32.One, PaymentType.Standard, 42 unixms, IncomingPaymentStatus.Pending) val eclair = mock[Eclair] val received = randomBytes32() eclair.receivedInfo(received)(any) returns Future.successful(Some(defaultPayment.copy(status = IncomingPaymentStatus.Received(42 msat, TimestampMilli(1633439543777L))))) @@ -989,7 +989,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM test("'findroute' method response should support nodeId, shortChannelId and full formats") { val serializedInvoice = "lnbc12580n1pw2ywztpp554ganw404sh4yjkwnysgn3wjcxfcq7gtx53gxczkjr9nlpc3hzvqdq2wpskwctddyxqr4rqrzjqwryaup9lh50kkranzgcdnn2fgvx390wgj5jd07rwr3vxeje0glc7z9rtvqqwngqqqqqqqlgqqqqqeqqjqrrt8smgjvfj7sg38dwtr9kc9gg3era9k3t2hvq3cup0jvsrtrxuplevqgfhd3rzvhulgcxj97yjuj8gdx8mllwj4wzjd8gdjhpz3lpqqvk2plh" - val invoice = Invoice.fromString(serializedInvoice) + val invoice = Invoice.fromString(serializedInvoice).get val mockChannelUpdate1 = ChannelUpdate( signature = ByteVector64.fromValidHex("92cf3f12e161391986eb2cd7106ddab41a23c734f8f1ed120fb64f4b91f98f690ecf930388e62965f8aefbf1adafcd25a572669a125396dcfb83615208754679"), diff --git a/pom.xml b/pom.xml index 4db741eaa4..52490a781a 100644 --- a/pom.xml +++ b/pom.xml @@ -72,7 +72,7 @@ 2.6.18 10.2.7 3.4.1 - 0.22 + 0.23 31.1-jre 2.4.6 From 86145a7470316f1e09901b729ed575039d1e32e4 Mon Sep 17 00:00:00 2001 From: Thomas HUET <81159533+thomash-acinq@users.noreply.github.com> Date: Wed, 13 Apr 2022 15:46:28 +0200 Subject: [PATCH 055/121] Remove unused PerHopPayloadFormat traits (#2235) --- .../eclair/wire/protocol/PaymentOnion.scala | 25 ++++++------------- 1 file changed, 7 insertions(+), 18 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/PaymentOnion.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/PaymentOnion.scala index ded53e0362..73f90b5df7 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/PaymentOnion.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/PaymentOnion.scala @@ -199,20 +199,9 @@ object PaymentOnion { * / \ \ \ * RelayLegacyPayload ChannelRelayTlvPayload NodeRelayPayload FinalTlvPayload * - * We also introduce additional traits to separate payloads based on their encoding (PerHopPayloadFormat) and on the - * type of onion packet they can be used with (PacketType). + * We also introduce additional traits to separate payloads based on the type of onion packet they can be used with (PacketType). */ - sealed trait PerHopPayloadFormat - - /** Legacy fixed-size 65-bytes onion payload. */ - sealed trait LegacyFormat extends PerHopPayloadFormat - - /** Variable-length onion payload with optional additional tlv records. */ - sealed trait TlvFormat extends PerHopPayloadFormat { - def records: TlvStream[OnionPaymentPayloadTlv] - } - /** Payment onion packet type. */ sealed trait PacketType @@ -229,7 +218,7 @@ object PaymentOnion { sealed trait PerHopPayload /** Per-hop payload for an intermediate node. */ - sealed trait RelayPayload extends PerHopPayload with PerHopPayloadFormat { + sealed trait RelayPayload extends PerHopPayload { /** Amount to forward to the next node. */ val amountToForward: MilliSatoshi /** CLTV value to use for the HTLC offered to the next node. */ @@ -242,7 +231,7 @@ object PaymentOnion { } /** Per-hop payload for a final node. */ - sealed trait FinalPayload extends PerHopPayload with PerHopPayloadFormat with TrampolinePacket with PaymentPacket { + sealed trait FinalPayload extends PerHopPayload with TrampolinePacket with PaymentPacket { val amount: MilliSatoshi val expiry: CltvExpiry val paymentSecret: ByteVector32 @@ -251,9 +240,9 @@ object PaymentOnion { val paymentMetadata: Option[ByteVector] } - case class RelayLegacyPayload(outgoingChannelId: ShortChannelId, amountToForward: MilliSatoshi, outgoingCltv: CltvExpiry) extends ChannelRelayPayload with LegacyFormat + case class RelayLegacyPayload(outgoingChannelId: ShortChannelId, amountToForward: MilliSatoshi, outgoingCltv: CltvExpiry) extends ChannelRelayPayload - case class ChannelRelayTlvPayload(records: TlvStream[OnionPaymentPayloadTlv]) extends ChannelRelayPayload with TlvFormat { + case class ChannelRelayTlvPayload(records: TlvStream[OnionPaymentPayloadTlv]) extends ChannelRelayPayload { override val amountToForward = records.get[AmountToForward].get.amount override val outgoingCltv = records.get[OutgoingCltv].get.cltv override val outgoingChannelId = records.get[OutgoingChannelId].get.shortChannelId @@ -264,7 +253,7 @@ object PaymentOnion { ChannelRelayTlvPayload(TlvStream(OnionPaymentPayloadTlv.AmountToForward(amountToForward), OnionPaymentPayloadTlv.OutgoingCltv(outgoingCltv), OnionPaymentPayloadTlv.OutgoingChannelId(outgoingChannelId))) } - case class NodeRelayPayload(records: TlvStream[OnionPaymentPayloadTlv]) extends RelayPayload with TlvFormat with TrampolinePacket { + case class NodeRelayPayload(records: TlvStream[OnionPaymentPayloadTlv]) extends RelayPayload with TrampolinePacket { val amountToForward = records.get[AmountToForward].get.amount val outgoingCltv = records.get[OutgoingCltv].get.cltv val outgoingNodeId = records.get[OutgoingNodeId].get.nodeId @@ -279,7 +268,7 @@ object PaymentOnion { val invoiceRoutingInfo = records.get[InvoiceRoutingInfo].map(_.extraHops) } - case class FinalTlvPayload(records: TlvStream[OnionPaymentPayloadTlv]) extends FinalPayload with TlvFormat { + case class FinalTlvPayload(records: TlvStream[OnionPaymentPayloadTlv]) extends FinalPayload { override val amount = records.get[AmountToForward].get.amount override val expiry = records.get[OutgoingCltv].get.cltv override val paymentSecret = records.get[PaymentData].get.secret From 9a31dfabea03bddf70e20628ae643efc78c873e7 Mon Sep 17 00:00:00 2001 From: Bastien Teinturier <31281497+t-bast@users.noreply.github.com> Date: Thu, 14 Apr 2022 11:23:42 +0200 Subject: [PATCH 056/121] Rename channel funder to initiator (#2236) With dual funding, both peers may be funders, but only one of them is the initiator. This is just a dumb renaming, there is not logic change. --- eclair-core/src/main/resources/reference.conf | 4 +- .../fr/acinq/eclair/channel/ChannelData.scala | 6 +- .../acinq/eclair/channel/ChannelEvents.scala | 2 +- .../eclair/channel/ChannelExceptions.scala | 2 +- .../fr/acinq/eclair/channel/Commitments.scala | 68 +++++++++---------- .../fr/acinq/eclair/channel/Helpers.scala | 22 +++--- .../fr/acinq/eclair/channel/fsm/Channel.scala | 50 +++++++------- .../channel/fsm/ChannelOpenSingleFunder.scala | 4 +- .../eclair/channel/fsm/ErrorHandlers.scala | 22 +++--- .../crypto/keymanager/ChannelKeyManager.scala | 4 +- .../keymanager/LocalChannelKeyManager.scala | 4 +- .../fr/acinq/eclair/db/DbEventHandler.scala | 6 +- .../fr/acinq/eclair/db/pg/PgAuditDb.scala | 2 +- .../eclair/db/sqlite/SqliteAuditDb.scala | 2 +- .../main/scala/fr/acinq/eclair/io/Peer.scala | 14 ++-- .../acinq/eclair/json/JsonSerializers.scala | 2 +- .../eclair/transactions/Transactions.scala | 30 ++++---- .../channel/version0/ChannelCodecs0.scala | 2 +- .../channel/version1/ChannelCodecs1.scala | 2 +- .../channel/version2/ChannelCodecs2.scala | 2 +- .../channel/version3/ChannelCodecs3.scala | 2 +- .../scala/fr/acinq/eclair/TestConstants.scala | 7 +- .../eclair/channel/CommitmentsSpec.scala | 30 ++++---- .../channel/states/e/NormalStateSpec.scala | 2 +- .../channel/states/f/ShutdownStateSpec.scala | 2 +- .../LocalChannelKeyManagerSpec.scala | 12 ++-- .../fr/acinq/eclair/db/AuditDbSpec.scala | 2 +- .../eclair/payment/PaymentPacketSpec.scala | 2 +- .../eclair/transactions/TestVectorsSpec.scala | 8 +-- .../transactions/TransactionsSpec.scala | 54 +++++++-------- .../internal/channel/ChannelCodecsSpec.scala | 8 +-- .../channel/version1/ChannelCodecs1Spec.scala | 2 +- .../fr/acinq/eclair/api/ApiServiceSpec.scala | 4 +- 33 files changed, 193 insertions(+), 192 deletions(-) diff --git a/eclair-core/src/main/resources/reference.conf b/eclair-core/src/main/resources/reference.conf index ac83a875e7..11c5c85181 100644 --- a/eclair-core/src/main/resources/reference.conf +++ b/eclair-core/src/main/resources/reference.conf @@ -197,8 +197,8 @@ eclair { close-on-offline-feerate-mismatch = true // do not change this unless you know what you are doing - // funder will send an UpdateFee message if the difference between current commitment fee and actual current network fee is greater - // than this ratio. + // the channel initiator will send an UpdateFee message if the difference between current commitment fee and actual + // current network fee is greater than this ratio. update-fee-min-diff-ratio = 0.1 } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala index cf7c9db348..6a1ffd5bea 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala @@ -439,10 +439,10 @@ final case class DATA_NEGOTIATING(commitments: Commitments, closingTxProposed: List[List[ClosingTxProposed]], // one list for every negotiation (there can be several in case of disconnection) bestUnpublishedClosingTx_opt: Option[ClosingTx]) extends PersistentChannelData { require(closingTxProposed.nonEmpty, "there must always be a list for the current negotiation") - require(!commitments.localParams.isFunder || closingTxProposed.forall(_.nonEmpty), "funder must have at least one closing signature for every negotiation attempt because it initiates the closing") + require(!commitments.localParams.isInitiator || closingTxProposed.forall(_.nonEmpty), "initiator must have at least one closing signature for every negotiation attempt because it initiates the closing") } final case class DATA_CLOSING(commitments: Commitments, - fundingTx: Option[Transaction], // this will be non-empty if we are funder and we got in closing while waiting for our own tx to be published + fundingTx: Option[Transaction], // this will be non-empty if we are the initiator and we got in closing while waiting for our own tx to be published waitingSince: BlockHeight, // how long since we initiated the closing mutualCloseProposed: List[ClosingTx], // all exchanged closing sigs are flattened, we use this only to keep track of what publishable tx they have mutualClosePublished: List[ClosingTx] = Nil, @@ -470,7 +470,7 @@ case class LocalParams(nodeId: PublicKey, htlcMinimum: MilliSatoshi, toSelfDelay: CltvExpiryDelta, maxAcceptedHtlcs: Int, - isFunder: Boolean, + isInitiator: Boolean, defaultFinalScriptPubKey: ByteVector, walletStaticPaymentBasepoint: Option[PublicKey], initFeatures: Features[InitFeature]) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelEvents.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelEvents.scala index 1e5f71afea..671de2a3f0 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelEvents.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelEvents.scala @@ -30,7 +30,7 @@ import fr.acinq.eclair.wire.protocol.{ChannelAnnouncement, ChannelUpdate} trait ChannelEvent -case class ChannelCreated(channel: ActorRef, peer: ActorRef, remoteNodeId: PublicKey, isFunder: Boolean, temporaryChannelId: ByteVector32, initialFeeratePerKw: FeeratePerKw, fundingTxFeeratePerKw: Option[FeeratePerKw]) extends ChannelEvent +case class ChannelCreated(channel: ActorRef, peer: ActorRef, remoteNodeId: PublicKey, isInitiator: Boolean, temporaryChannelId: ByteVector32, initialFeeratePerKw: FeeratePerKw, fundingTxFeeratePerKw: Option[FeeratePerKw]) extends ChannelEvent // This trait can be used by non-standard channels to inject themselves into Register actor and thus make them usable for routing trait AbstractChannelRestored extends ChannelEvent { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelExceptions.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelExceptions.scala index 624a5a731b..222726ed80 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelExceptions.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelExceptions.scala @@ -86,7 +86,7 @@ case class RemoteCannotAffordFeesForNewHtlc (override val channelId: Byte case class InvalidHtlcPreimage (override val channelId: ByteVector32, id: Long) extends ChannelException(channelId, s"invalid htlc preimage for htlc id=$id") case class UnknownHtlcId (override val channelId: ByteVector32, id: Long) extends ChannelException(channelId, s"unknown htlc id=$id") case class CannotExtractSharedSecret (override val channelId: ByteVector32, htlc: UpdateAddHtlc) extends ChannelException(channelId, s"can't extract shared secret: paymentHash=${htlc.paymentHash} onion=${htlc.onionRoutingPacket}") -case class FundeeCannotSendUpdateFee (override val channelId: ByteVector32) extends ChannelException(channelId, s"only the funder should send update_fee messages") +case class NonInitiatorCannotSendUpdateFee (override val channelId: ByteVector32) extends ChannelException(channelId, s"only the initiator should send update_fee messages") case class CannotAffordFees (override val channelId: ByteVector32, missing: Satoshi, reserve: Satoshi, fees: Satoshi) extends ChannelException(channelId, s"can't pay the fee: missing=$missing reserve=$reserve fees=$fees") case class CannotSignWithoutChanges (override val channelId: ByteVector32) extends ChannelException(channelId, "cannot sign when there are no changes") case class CannotSignBeforeRevocation (override val channelId: ByteVector32) extends ChannelException(channelId, "cannot sign until next revocation hash is received") diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala index f41dfdbbc9..1e03c67679 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala @@ -211,8 +211,8 @@ case class Commitments(channelId: ByteVector32, val capacity: Satoshi = commitInput.txOut.amount - // NB: when computing availableBalanceForSend and availableBalanceForReceive, the funder keeps an extra buffer on top - // of its usual channel reserve to avoid getting channels stuck in case the on-chain feerate increases (see + // NB: when computing availableBalanceForSend and availableBalanceForReceive, the initiator keeps an extra buffer on + // top of its usual channel reserve to avoid getting channels stuck in case the on-chain feerate increases (see // https://github.com/lightningnetwork/lightning-rfc/issues/728 for details). // // This extra buffer (which we call "funder fee buffer") is calculated as follows: @@ -242,10 +242,10 @@ case class Commitments(channelId: ByteVector32, val remoteCommit1 = remoteNextCommitInfo.left.toOption.map(_.nextRemoteCommit).getOrElse(remoteCommit) val reduced = CommitmentSpec.reduce(remoteCommit1.spec, remoteChanges.acked, localChanges.proposed) val balanceNoFees = (reduced.toRemote - remoteParams.channelReserve).max(0 msat) - if (localParams.isFunder) { - // The funder always pays the on-chain fees, so we must subtract that from the amount we can send. + if (localParams.isInitiator) { + // The initiator always pays the on-chain fees, so we must subtract that from the amount we can send. val commitFees = commitTxTotalCostMsat(remoteParams.dustLimit, reduced, commitmentFormat) - // the funder needs to keep a "funder fee buffer" (see explanation above) + // the initiator needs to keep a "funder fee buffer" (see explanation above) val funderFeeBuffer = commitTxTotalCostMsat(remoteParams.dustLimit, reduced.copy(commitTxFeerate = reduced.commitTxFeerate * 2), commitmentFormat) + htlcOutputFee(reduced.commitTxFeerate * 2, commitmentFormat) val amountToReserve = commitFees.max(funderFeeBuffer) if (balanceNoFees - amountToReserve < offeredHtlcTrimThreshold(remoteParams.dustLimit, reduced, commitmentFormat)) { @@ -260,7 +260,7 @@ case class Commitments(channelId: ByteVector32, (balanceNoFees - amountToReserve1).max(0 msat) } } else { - // The fundee doesn't pay on-chain fees. + // The non-initiator doesn't pay on-chain fees. balanceNoFees } } @@ -268,13 +268,13 @@ case class Commitments(channelId: ByteVector32, lazy val availableBalanceForReceive: MilliSatoshi = { val reduced = CommitmentSpec.reduce(localCommit.spec, localChanges.acked, remoteChanges.proposed) val balanceNoFees = (reduced.toRemote - localParams.channelReserve).max(0 msat) - if (localParams.isFunder) { - // The fundee doesn't pay on-chain fees so we don't take those into account when receiving. + if (localParams.isInitiator) { + // The non-initiator doesn't pay on-chain fees so we don't take those into account when receiving. balanceNoFees } else { - // The funder always pays the on-chain fees, so we must subtract that from the amount we can receive. + // The initiator always pays the on-chain fees, so we must subtract that from the amount we can receive. val commitFees = commitTxTotalCostMsat(localParams.dustLimit, reduced, commitmentFormat) - // we expected the funder to keep a "funder fee buffer" (see explanation above) + // we expected the initiator to keep a "funder fee buffer" (see explanation above) val funderFeeBuffer = commitTxTotalCostMsat(localParams.dustLimit, reduced.copy(commitTxFeerate = reduced.commitTxFeerate * 2), commitmentFormat) + htlcOutputFee(reduced.commitTxFeerate * 2, commitmentFormat) val amountToReserve = commitFees.max(funderFeeBuffer) if (balanceNoFees - amountToReserve < receivedHtlcTrimThreshold(localParams.dustLimit, reduced, commitmentFormat)) { @@ -365,20 +365,20 @@ object Commitments { // the HTLC we are about to create is outgoing, but from their point of view it is incoming val outgoingHtlcs = reduced.htlcs.collect(incoming) - // note that the funder pays the fee, so if sender != funder, both sides will have to afford this payment + // note that the initiator pays the fee, so if sender != initiator, both sides will have to afford this payment val fees = commitTxTotalCost(commitments1.remoteParams.dustLimit, reduced, commitments.commitmentFormat) - // the funder needs to keep an extra buffer to be able to handle a x2 feerate increase and an additional htlc to avoid + // the initiator needs to keep an extra buffer to be able to handle a x2 feerate increase and an additional htlc to avoid // getting the channel stuck (see https://github.com/lightningnetwork/lightning-rfc/issues/728). val funderFeeBuffer = commitTxTotalCostMsat(commitments1.remoteParams.dustLimit, reduced.copy(commitTxFeerate = reduced.commitTxFeerate * 2), commitments.commitmentFormat) + htlcOutputFee(reduced.commitTxFeerate * 2, commitments.commitmentFormat) // NB: increasing the feerate can actually remove htlcs from the commit tx (if they fall below the trim threshold) // which may result in a lower commit tx fee; this is why we take the max of the two. - val missingForSender = reduced.toRemote - commitments1.remoteParams.channelReserve - (if (commitments1.localParams.isFunder) fees.max(funderFeeBuffer.truncateToSatoshi) else 0.sat) - val missingForReceiver = reduced.toLocal - commitments1.localParams.channelReserve - (if (commitments1.localParams.isFunder) 0.sat else fees) + val missingForSender = reduced.toRemote - commitments1.remoteParams.channelReserve - (if (commitments1.localParams.isInitiator) fees.max(funderFeeBuffer.truncateToSatoshi) else 0.sat) + val missingForReceiver = reduced.toLocal - commitments1.localParams.channelReserve - (if (commitments1.localParams.isInitiator) 0.sat else fees) if (missingForSender < 0.msat) { - return Left(InsufficientFunds(commitments.channelId, amount = cmd.amount, missing = -missingForSender.truncateToSatoshi, reserve = commitments1.remoteParams.channelReserve, fees = if (commitments1.localParams.isFunder) fees else 0.sat)) + return Left(InsufficientFunds(commitments.channelId, amount = cmd.amount, missing = -missingForSender.truncateToSatoshi, reserve = commitments1.remoteParams.channelReserve, fees = if (commitments1.localParams.isInitiator) fees else 0.sat)) } else if (missingForReceiver < 0.msat) { - if (commitments.localParams.isFunder) { - // receiver is fundee; it is ok if it can't maintain its channel_reserve for now, as long as its balance is increasing, which is the case if it is receiving a payment + if (commitments.localParams.isInitiator) { + // receiver is not the channel initiator; it is ok if it can't maintain its channel_reserve for now, as long as its balance is increasing, which is the case if it is receiving a payment } else { return Left(RemoteCannotAffordFeesForNewHtlc(commitments.channelId, amount = cmd.amount, missing = -missingForReceiver.truncateToSatoshi, reserve = commitments1.remoteParams.channelReserve, fees = fees)) } @@ -437,19 +437,19 @@ object Commitments { val reduced = CommitmentSpec.reduce(commitments1.localCommit.spec, commitments1.localChanges.acked, commitments1.remoteChanges.proposed) val incomingHtlcs = reduced.htlcs.collect(incoming) - // note that the funder pays the fee, so if sender != funder, both sides will have to afford this payment + // note that the initiator pays the fee, so if sender != initiator, both sides will have to afford this payment val fees = commitTxTotalCost(commitments1.remoteParams.dustLimit, reduced, commitments.commitmentFormat) - // NB: we don't enforce the funderFeeReserve (see sendAdd) because it would confuse a remote funder that doesn't have this mitigation in place + // NB: we don't enforce the funderFeeReserve (see sendAdd) because it would confuse a remote initiator that doesn't have this mitigation in place // We could enforce it once we're confident a large portion of the network implements it. - val missingForSender = reduced.toRemote - commitments1.localParams.channelReserve - (if (commitments1.localParams.isFunder) 0.sat else fees) - val missingForReceiver = reduced.toLocal - commitments1.remoteParams.channelReserve - (if (commitments1.localParams.isFunder) fees else 0.sat) + val missingForSender = reduced.toRemote - commitments1.localParams.channelReserve - (if (commitments1.localParams.isInitiator) 0.sat else fees) + val missingForReceiver = reduced.toLocal - commitments1.remoteParams.channelReserve - (if (commitments1.localParams.isInitiator) fees else 0.sat) if (missingForSender < 0.sat) { - return Left(InsufficientFunds(commitments.channelId, amount = add.amountMsat, missing = -missingForSender.truncateToSatoshi, reserve = commitments1.localParams.channelReserve, fees = if (commitments1.localParams.isFunder) 0.sat else fees)) + return Left(InsufficientFunds(commitments.channelId, amount = add.amountMsat, missing = -missingForSender.truncateToSatoshi, reserve = commitments1.localParams.channelReserve, fees = if (commitments1.localParams.isInitiator) 0.sat else fees)) } else if (missingForReceiver < 0.sat) { - if (commitments.localParams.isFunder) { + if (commitments.localParams.isInitiator) { return Left(CannotAffordFees(commitments.channelId, missing = -missingForReceiver.truncateToSatoshi, reserve = commitments1.remoteParams.channelReserve, fees = fees)) } else { - // receiver is fundee; it is ok if it can't maintain its channel_reserve for now, as long as its balance is increasing, which is the case if it is receiving a payment + // receiver is not the channel initiator; it is ok if it can't maintain its channel_reserve for now, as long as its balance is increasing, which is the case if it is receiving a payment } } @@ -546,8 +546,8 @@ object Commitments { } def sendFee(commitments: Commitments, cmd: CMD_UPDATE_FEE, feeConf: OnChainFeeConf): Either[ChannelException, (Commitments, UpdateFee)] = { - if (!commitments.localParams.isFunder) { - Left(FundeeCannotSendUpdateFee(commitments.channelId)) + if (!commitments.localParams.isInitiator) { + Left(NonInitiatorCannotSendUpdateFee(commitments.channelId)) } else { // let's compute the current commitment *as seen by them* with this change taken into account val fee = UpdateFee(commitments.channelId, cmd.feeratePerKw) @@ -556,7 +556,7 @@ object Commitments { val reduced = CommitmentSpec.reduce(commitments1.remoteCommit.spec, commitments1.remoteChanges.acked, commitments1.localChanges.proposed) // a node cannot spend pending incoming htlcs, and need to keep funds above the reserve required by the counterparty, after paying the fee - // we look from remote's point of view, so if local is funder remote doesn't pay the fees + // we look from remote's point of view, so if local is initiator remote doesn't pay the fees val fees = commitTxTotalCost(commitments1.remoteParams.dustLimit, reduced, commitments.commitmentFormat) val missing = reduced.toRemote.truncateToSatoshi - commitments1.remoteParams.channelReserve - fees if (missing < 0.sat) { @@ -585,8 +585,8 @@ object Commitments { } def receiveFee(commitments: Commitments, fee: UpdateFee, feeConf: OnChainFeeConf)(implicit log: LoggingAdapter): Either[ChannelException, Commitments] = { - if (commitments.localParams.isFunder) { - Left(FundeeCannotSendUpdateFee(commitments.channelId)) + if (commitments.localParams.isInitiator) { + Left(NonInitiatorCannotSendUpdateFee(commitments.channelId)) } else if (fee.feeratePerKw < FeeratePerKw.MinimumFeeratePerKw) { Left(FeerateTooSmall(commitments.channelId, remoteFeeratePerKw = fee.feeratePerKw)) } else { @@ -596,7 +596,7 @@ object Commitments { if (feeConf.feerateToleranceFor(commitments.remoteNodeId).isFeeDiffTooHigh(commitments.channelType, localFeeratePerKw, fee.feeratePerKw) && commitments.hasPendingOrProposedHtlcs) { Left(FeerateTooDifferent(commitments.channelId, localFeeratePerKw = localFeeratePerKw, remoteFeeratePerKw = fee.feeratePerKw)) } else { - // NB: we check that the funder can afford this new fee even if spec allows to do it at next signature + // NB: we check that the initiator can afford this new fee even if spec allows to do it at next signature // It is easier to do it here because under certain (race) conditions spec allows a lower-than-normal fee to be paid, // and it would be tricky to check if the conditions are met at signing // (it also means that we need to check the fee of the initial commitment tx somewhere) @@ -852,8 +852,8 @@ object Commitments { val remoteHtlcPubkey = Generators.derivePubKey(remoteParams.htlcBasepoint, localPerCommitmentPoint) val localRevocationPubkey = Generators.revocationPubKey(remoteParams.revocationBasepoint, localPerCommitmentPoint) val localPaymentBasepoint = localParams.walletStaticPaymentBasepoint.getOrElse(keyManager.paymentPoint(channelKeyPath).publicKey) - val outputs = makeCommitTxOutputs(localParams.isFunder, localParams.dustLimit, localRevocationPubkey, remoteParams.toSelfDelay, localDelayedPaymentPubkey, remotePaymentPubkey, localHtlcPubkey, remoteHtlcPubkey, localFundingPubkey, remoteParams.fundingPubKey, spec, channelFeatures.commitmentFormat) - val commitTx = Transactions.makeCommitTx(commitmentInput, commitTxNumber, localPaymentBasepoint, remoteParams.paymentBasepoint, localParams.isFunder, outputs) + val outputs = makeCommitTxOutputs(localParams.isInitiator, localParams.dustLimit, localRevocationPubkey, remoteParams.toSelfDelay, localDelayedPaymentPubkey, remotePaymentPubkey, localHtlcPubkey, remoteHtlcPubkey, localFundingPubkey, remoteParams.fundingPubKey, spec, channelFeatures.commitmentFormat) + val commitTx = Transactions.makeCommitTx(commitmentInput, commitTxNumber, localPaymentBasepoint, remoteParams.paymentBasepoint, localParams.isInitiator, outputs) val htlcTxs = Transactions.makeHtlcTxs(commitTx.tx, localParams.dustLimit, localRevocationPubkey, remoteParams.toSelfDelay, localDelayedPaymentPubkey, spec.htlcTxFeerate(channelFeatures.commitmentFormat), outputs, channelFeatures.commitmentFormat) (commitTx, htlcTxs) } @@ -879,8 +879,8 @@ object Commitments { val remoteDelayedPaymentPubkey = Generators.derivePubKey(remoteParams.delayedPaymentBasepoint, remotePerCommitmentPoint) val remoteHtlcPubkey = Generators.derivePubKey(remoteParams.htlcBasepoint, remotePerCommitmentPoint) val remoteRevocationPubkey = Generators.revocationPubKey(keyManager.revocationPoint(channelKeyPath).publicKey, remotePerCommitmentPoint) - val outputs = makeCommitTxOutputs(!localParams.isFunder, remoteParams.dustLimit, remoteRevocationPubkey, localParams.toSelfDelay, remoteDelayedPaymentPubkey, localPaymentPubkey, remoteHtlcPubkey, localHtlcPubkey, remoteParams.fundingPubKey, localFundingPubkey, spec, channelFeatures.commitmentFormat) - val commitTx = Transactions.makeCommitTx(commitmentInput, commitTxNumber, remoteParams.paymentBasepoint, localPaymentBasepoint, !localParams.isFunder, outputs) + val outputs = makeCommitTxOutputs(!localParams.isInitiator, remoteParams.dustLimit, remoteRevocationPubkey, localParams.toSelfDelay, remoteDelayedPaymentPubkey, localPaymentPubkey, remoteHtlcPubkey, localHtlcPubkey, remoteParams.fundingPubKey, localFundingPubkey, spec, channelFeatures.commitmentFormat) + val commitTx = Transactions.makeCommitTx(commitmentInput, commitTxNumber, remoteParams.paymentBasepoint, localPaymentBasepoint, !localParams.isInitiator, outputs) val htlcTxs = Transactions.makeHtlcTxs(commitTx.tx, remoteParams.dustLimit, remoteRevocationPubkey, localParams.toSelfDelay, remoteDelayedPaymentPubkey, spec.htlcTxFeerate(channelFeatures.commitmentFormat), outputs, channelFeatures.commitmentFormat) (commitTx, htlcTxs) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala index 3ff8c60375..b6c6f249fe 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala @@ -290,14 +290,14 @@ object Helpers { * @return (localSpec, localTx, remoteSpec, remoteTx, fundingTxOutput) */ def makeFirstCommitTxs(keyManager: ChannelKeyManager, channelConfig: ChannelConfig, channelFeatures: ChannelFeatures, temporaryChannelId: ByteVector32, localParams: LocalParams, remoteParams: RemoteParams, fundingAmount: Satoshi, pushMsat: MilliSatoshi, initialFeeratePerKw: FeeratePerKw, fundingTxHash: ByteVector32, fundingTxOutputIndex: Int, remoteFirstPerCommitmentPoint: PublicKey): Either[ChannelException, (CommitmentSpec, CommitTx, CommitmentSpec, CommitTx)] = { - val toLocalMsat = if (localParams.isFunder) fundingAmount.toMilliSatoshi - pushMsat else pushMsat - val toRemoteMsat = if (localParams.isFunder) pushMsat else fundingAmount.toMilliSatoshi - pushMsat + val toLocalMsat = if (localParams.isInitiator) fundingAmount.toMilliSatoshi - pushMsat else pushMsat + val toRemoteMsat = if (localParams.isInitiator) pushMsat else fundingAmount.toMilliSatoshi - pushMsat val localSpec = CommitmentSpec(Set.empty[DirectedHtlc], initialFeeratePerKw, toLocal = toLocalMsat, toRemote = toRemoteMsat) val remoteSpec = CommitmentSpec(Set.empty[DirectedHtlc], initialFeeratePerKw, toLocal = toRemoteMsat, toRemote = toLocalMsat) - if (!localParams.isFunder) { - // they are funder, therefore they pay the fee: we need to make sure they can afford it! + if (!localParams.isInitiator) { + // they initiated the channel open, therefore they pay the fee: we need to make sure they can afford it! val toRemoteMsat = remoteSpec.toLocal val fees = commitTxTotalCost(remoteParams.dustLimit, remoteSpec, channelFeatures.commitmentFormat) val missing = toRemoteMsat.truncateToSatoshi - localParams.channelReserve - fees @@ -522,7 +522,7 @@ object Helpers { // this is just to estimate the weight, it depends on size of the pubkey scripts val actualLocalScript = if (channelFeatures.hasFeature(Features.UpfrontShutdownScript)) localParams.defaultFinalScriptPubKey else localScriptPubkey val actualRemoteScript = if (channelFeatures.hasFeature(Features.UpfrontShutdownScript)) remoteParams.shutdownScript.getOrElse(remoteScriptPubkey) else remoteScriptPubkey - val dummyClosingTx = Transactions.makeClosingTx(commitInput, actualLocalScript, actualRemoteScript, localParams.isFunder, Satoshi(0), Satoshi(0), localCommit.spec) + val dummyClosingTx = Transactions.makeClosingTx(commitInput, actualLocalScript, actualRemoteScript, localParams.isInitiator, Satoshi(0), Satoshi(0), localCommit.spec) val closingWeight = Transaction.weight(Transactions.addSigs(dummyClosingTx, dummyPublicKey, remoteParams.fundingPubKey, Transactions.PlaceHolderSig, Transactions.PlaceHolderSig).tx) log.info(s"using feerates=$feerates for initial closing tx") feerates.computeFees(closingWeight) @@ -561,7 +561,7 @@ object Helpers { require(isValidFinalScriptPubkey(actualRemoteScript, allowAnySegwit), "invalid remoteScriptPubkey") log.debug("making closing tx with closing fee={} and commitments:\n{}", closingFees.preferred, Commitments.specs2String(commitments)) val dustLimit = localParams.dustLimit.max(remoteParams.dustLimit) - val closingTx = Transactions.makeClosingTx(commitInput, actualLocalScript, actualRemoteScript, localParams.isFunder, dustLimit, closingFees.preferred, localCommit.spec) + val closingTx = Transactions.makeClosingTx(commitInput, actualLocalScript, actualRemoteScript, localParams.isInitiator, dustLimit, closingFees.preferred, localCommit.spec) val localClosingSig = keyManager.sign(closingTx, keyManager.fundingPublicKey(commitments.localParams.fundingKeyPath), TxOwner.Local, commitmentFormat) val closingSigned = ClosingSigned(channelId, closingFees.preferred, localClosingSig, TlvStream(ClosingSignedTlv.FeeRange(closingFees.min, closingFees.max))) log.info(s"signed closing txid=${closingTx.tx.txid} with closing fee=${closingSigned.feeSatoshis}") @@ -626,10 +626,10 @@ object Helpers { } /** Compute the fee paid by a commitment transaction. */ - def commitTxFee(commitInput: InputInfo, commitTx: Transaction, isFunder: Boolean): Satoshi = { + def commitTxFee(commitInput: InputInfo, commitTx: Transaction, isInitiator: Boolean): Satoshi = { require(commitTx.txIn.size == 1, "transaction must have only one input") require(commitTx.txIn.exists(txIn => txIn.outPoint == commitInput.outPoint), "transaction must spend the funding output") - if (isFunder) commitInput.txOut.amount - commitTx.txOut.map(_.amount).sum else 0 sat + if (isInitiator) commitInput.txOut.amount - commitTx.txOut.map(_.amount).sum else 0 sat } object LocalClose { @@ -835,7 +835,7 @@ object Helpers { val remoteRevocationPubkey = Generators.revocationPubKey(keyManager.revocationPoint(channelKeyPath).publicKey, remoteCommit.remotePerCommitmentPoint) val remoteDelayedPaymentPubkey = Generators.derivePubKey(commitments.remoteParams.delayedPaymentBasepoint, remoteCommit.remotePerCommitmentPoint) val localPaymentPubkey = Generators.derivePubKey(keyManager.paymentPoint(channelKeyPath).publicKey, remoteCommit.remotePerCommitmentPoint) - val outputs = makeCommitTxOutputs(!commitments.localParams.isFunder, commitments.remoteParams.dustLimit, remoteRevocationPubkey, commitments.localParams.toSelfDelay, remoteDelayedPaymentPubkey, localPaymentPubkey, remoteHtlcPubkey, localHtlcPubkey, commitments.remoteParams.fundingPubKey, localFundingPubkey, remoteCommit.spec, commitments.commitmentFormat) + val outputs = makeCommitTxOutputs(!commitments.localParams.isInitiator, commitments.remoteParams.dustLimit, remoteRevocationPubkey, commitments.localParams.toSelfDelay, remoteDelayedPaymentPubkey, localPaymentPubkey, remoteHtlcPubkey, localHtlcPubkey, commitments.remoteParams.fundingPubKey, localFundingPubkey, remoteCommit.spec, commitments.commitmentFormat) // we need to use a rather high fee for htlc-claim because we compete with the counterparty val feeratePerKwHtlc = feeEstimator.getFeeratePerKw(target = 2) @@ -901,7 +901,7 @@ object Helpers { val obscuredTxNumber = Transactions.decodeTxNumber(commitTx.txIn.head.sequence, commitTx.lockTime) val localPaymentPoint = localParams.walletStaticPaymentBasepoint.getOrElse(keyManager.paymentPoint(channelKeyPath).publicKey) // this tx has been published by remote, so we need to invert local/remote params - val txNumber = Transactions.obscuredCommitTxNumber(obscuredTxNumber, !localParams.isFunder, remoteParams.paymentBasepoint, localPaymentPoint) + val txNumber = Transactions.obscuredCommitTxNumber(obscuredTxNumber, !localParams.isInitiator, remoteParams.paymentBasepoint, localPaymentPoint) require(txNumber <= 0xffffffffffffL, "txNumber must be lesser than 48 bits long") log.warning(s"a revoked commit has been published with txnumber=$txNumber") // now we know what commit number this tx is referring to, we can derive the commitment point from the shachain @@ -1005,7 +1005,7 @@ object Helpers { val channelKeyPath = keyManager.keyPath(localParams, channelConfig) val localPaymentPoint = localParams.walletStaticPaymentBasepoint.getOrElse(keyManager.paymentPoint(channelKeyPath).publicKey) // this tx has been published by remote, so we need to invert local/remote params - val txNumber = Transactions.obscuredCommitTxNumber(obscuredTxNumber, !localParams.isFunder, remoteParams.paymentBasepoint, localPaymentPoint) + val txNumber = Transactions.obscuredCommitTxNumber(obscuredTxNumber, !localParams.isInitiator, remoteParams.paymentBasepoint, localPaymentPoint) // now we know what commit number this tx is referring to, we can derive the commitment point from the shachain remotePerCommitmentSecrets.getHash(0xFFFFFFFFFFFFL - txNumber) .map(d => PrivateKey(d)) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala index 988d5106ee..7b7547a5be 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala @@ -120,7 +120,7 @@ object Channel { // since BOLT 1.1, there is a max value for the refund delay of the main commitment tx val MAX_TO_SELF_DELAY: CltvExpiryDelta = CltvExpiryDelta(2016) - // as a fundee, we will wait that many blocks for the funding tx to confirm (funder will rely on the funding tx being double-spent) + // as a non-initiator, we will wait that many blocks for the funding tx to confirm (initiator will rely on the funding tx being double-spent) val FUNDING_TIMEOUT_FUNDEE = 2016 // pruning occurs if no new update has been received in two weeks (BOLT 7) @@ -209,7 +209,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val when(WAIT_FOR_INIT_INTERNAL)(handleExceptions { case Event(initFunder@INPUT_INIT_FUNDER(temporaryChannelId, fundingSatoshis, pushMsat, initialFeeratePerKw, fundingTxFeeratePerKw, localParams, remote, remoteInit, channelFlags, channelConfig, channelType), Nothing) => - context.system.eventStream.publish(ChannelCreated(self, peer, remoteNodeId, isFunder = true, temporaryChannelId, initialFeeratePerKw, Some(fundingTxFeeratePerKw))) + context.system.eventStream.publish(ChannelCreated(self, peer, remoteNodeId, isInitiator = true, temporaryChannelId, initialFeeratePerKw, Some(fundingTxFeeratePerKw))) activeConnection = remote txPublisher ! SetChannelId(remoteNodeId, temporaryChannelId) val fundingPubKey = keyManager.fundingPublicKey(localParams.fundingKeyPath).publicKey @@ -241,7 +241,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val )) goto(WAIT_FOR_ACCEPT_CHANNEL) using DATA_WAIT_FOR_ACCEPT_CHANNEL(initFunder, open) sending open - case Event(inputFundee@INPUT_INIT_FUNDEE(_, localParams, remote, _, _, _), Nothing) if !localParams.isFunder => + case Event(inputFundee@INPUT_INIT_FUNDEE(_, localParams, remote, _, _, _), Nothing) if !localParams.isInitiator => activeConnection = remote txPublisher ! SetChannelId(remoteNodeId, inputFundee.temporaryChannelId) goto(WAIT_FOR_OPEN_CHANNEL) using DATA_WAIT_FOR_OPEN_CHANNEL(inputFundee) @@ -256,7 +256,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val log.info("we have nothing at stake, going straight to CLOSED") goto(CLOSED) using closing case closing: DATA_CLOSING => - val isFunder = closing.commitments.localParams.isFunder + val isInitiator = closing.commitments.localParams.isInitiator // we don't put back the WatchSpent if the commitment tx has already been published and the spending tx already reached mindepth val closingType_opt = Closing.isClosingTypeAlreadyKnown(closing) log.info(s"channel is closing (closingType=${closingType_opt.map(c => EventType.Closed(c).label).getOrElse("UnknownYet")})") @@ -265,7 +265,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val // - there is no need to attempt to publish transactions for other type of closes closingType_opt match { case Some(c: Closing.MutualClose) => - doPublish(c.tx, isFunder) + doPublish(c.tx, isInitiator) case Some(c: Closing.LocalClose) => doPublish(c.localCommitPublished, closing.commitments) case Some(c: Closing.RemoteClose) => @@ -277,7 +277,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val case None => // in all other cases we need to be ready for any type of closing watchFundingTx(data.commitments, closing.spendingTxs.map(_.txid).toSet) - closing.mutualClosePublished.foreach(mcp => doPublish(mcp, isFunder)) + closing.mutualClosePublished.foreach(mcp => doPublish(mcp, isInitiator)) closing.localCommitPublished.foreach(lcp => doPublish(lcp, closing.commitments)) closing.remoteCommitPublished.foreach(rcp => doPublish(rcp, closing.commitments)) closing.nextRemoteCommitPublished.foreach(rcp => doPublish(rcp, closing.commitments)) @@ -594,12 +594,12 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val // are there pending signed changes on either side? we need to have received their last revocation! if (d.commitments.hasNoPendingHtlcsOrFeeUpdate) { // there are no pending signed changes, let's go directly to NEGOTIATING - if (d.commitments.localParams.isFunder) { - // we are funder, need to initiate the negotiation by sending the first closing_signed + if (d.commitments.localParams.isInitiator) { + // we are the channel initiator, need to initiate the negotiation by sending the first closing_signed val (closingTx, closingSigned) = Closing.MutualClose.makeFirstClosingTx(keyManager, d.commitments, localShutdown.scriptPubKey, remoteShutdownScript, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets, d.closingFeerates) goto(NEGOTIATING) using DATA_NEGOTIATING(d.commitments, localShutdown, remoteShutdown, List(List(ClosingTxProposed(closingTx, closingSigned))), bestUnpublishedClosingTx_opt = None) storing() sending sendList :+ closingSigned } else { - // we are fundee, will wait for their closing_signed + // we are not the channel initiator, will wait for their closing_signed goto(NEGOTIATING) using DATA_NEGOTIATING(d.commitments, localShutdown, remoteShutdown, closingTxProposed = List(List()), bestUnpublishedClosingTx_opt = None) storing() sending sendList } } else { @@ -834,12 +834,12 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val log.debug("received a new sig:\n{}", Commitments.specs2String(commitments1)) context.system.eventStream.publish(ChannelSignatureReceived(self, commitments1)) if (commitments1.hasNoPendingHtlcsOrFeeUpdate) { - if (d.commitments.localParams.isFunder) { - // we are funder, need to initiate the negotiation by sending the first closing_signed + if (d.commitments.localParams.isInitiator) { + // we are the channel initiator, need to initiate the negotiation by sending the first closing_signed val (closingTx, closingSigned) = Closing.MutualClose.makeFirstClosingTx(keyManager, commitments1, localShutdown.scriptPubKey, remoteShutdown.scriptPubKey, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets, closingFeerates) goto(NEGOTIATING) using DATA_NEGOTIATING(commitments1, localShutdown, remoteShutdown, List(List(ClosingTxProposed(closingTx, closingSigned))), bestUnpublishedClosingTx_opt = None) storing() sending revocation :: closingSigned :: Nil } else { - // we are fundee, will wait for their closing_signed + // we are not the channel initiator, will wait for their closing_signed goto(NEGOTIATING) using DATA_NEGOTIATING(commitments1, localShutdown, remoteShutdown, closingTxProposed = List(List()), bestUnpublishedClosingTx_opt = None) storing() sending revocation } } else { @@ -874,12 +874,12 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val } if (commitments1.hasNoPendingHtlcsOrFeeUpdate) { log.debug("switching to NEGOTIATING spec:\n{}", Commitments.specs2String(commitments1)) - if (d.commitments.localParams.isFunder) { - // we are funder, need to initiate the negotiation by sending the first closing_signed + if (d.commitments.localParams.isInitiator) { + // we are the channel initiator, need to initiate the negotiation by sending the first closing_signed val (closingTx, closingSigned) = Closing.MutualClose.makeFirstClosingTx(keyManager, commitments1, localShutdown.scriptPubKey, remoteShutdown.scriptPubKey, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets, closingFeerates) goto(NEGOTIATING) using DATA_NEGOTIATING(commitments1, localShutdown, remoteShutdown, List(List(ClosingTxProposed(closingTx, closingSigned))), bestUnpublishedClosingTx_opt = None) storing() sending closingSigned } else { - // we are fundee, will wait for their closing_signed + // we are not the channel initiator, will wait for their closing_signed goto(NEGOTIATING) using DATA_NEGOTIATING(commitments1, localShutdown, remoteShutdown, closingTxProposed = List(List()), bestUnpublishedClosingTx_opt = None) storing() } } else { @@ -953,8 +953,8 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val handleMutualClose(signedClosingTx, Left(d.copy(bestUnpublishedClosingTx_opt = Some(signedClosingTx)))) sending closingSignedRemoteFees } else { c.feeRange_opt match { - case Some(ClosingSignedTlv.FeeRange(minFee, maxFee)) if !d.commitments.localParams.isFunder => - // if we are fundee and they proposed a fee range, we pick a value in that range and they should accept it without further negotiation + case Some(ClosingSignedTlv.FeeRange(minFee, maxFee)) if !d.commitments.localParams.isInitiator => + // if we are not the channel initiator and they proposed a fee range, we pick a value in that range and they should accept it without further negotiation // we don't care much about the closing fee since they're paying it (not us) and we can use CPFP if we want to speed up confirmation val localClosingFees = Closing.MutualClose.firstClosingFee(d.commitments, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) if (maxFee < localClosingFees.min) { @@ -982,7 +982,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val case _ => val lastLocalClosingFee_opt = lastLocalClosingSigned_opt.map(_.localClosingSigned.feeSatoshis) val (closingTx, closingSigned) = { - // if we are fundee and we were waiting for them to send their first closing_signed, we don't have a lastLocalClosingFee, so we compute a firstClosingFee + // if we are not the channel initiator and we were waiting for them to send their first closing_signed, we don't have a lastLocalClosingFee, so we compute a firstClosingFee val localClosingFees = Closing.MutualClose.firstClosingFee(d.commitments, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) val nextPreferredFee = Closing.MutualClose.nextClosingFee(lastLocalClosingFee_opt.getOrElse(localClosingFees.preferred), remoteClosingFee) Closing.MutualClose.makeClosingTx(keyManager, d.commitments, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey, localClosingFees.copy(preferred = nextPreferredFee)) @@ -1306,10 +1306,10 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val when(SYNCING)(handleExceptions { case Event(_: ChannelReestablish, d: DATA_WAIT_FOR_FUNDING_CONFIRMED) => - val minDepth = if (d.commitments.localParams.isFunder) { + val minDepth = if (d.commitments.localParams.isInitiator) { nodeParams.channelConf.minDepthBlocks } else { - // when we're fundee we scale the min_depth confirmations depending on the funding amount + // when we're not the channel initiator we scale the min_depth confirmations depending on the funding amount Helpers.minDepthForFunding(nodeParams.channelConf, d.commitments.commitInput.txOut.amount) } // we put back the watch (operation is idempotent) because the event may have been fired while we were in OFFLINE @@ -1399,7 +1399,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val // only briefly connects and then disconnects, we may never have the opportunity to send our `update_fee`, so // we send it (if needed) when reconnected. val shutdownInProgress = d.localShutdown.nonEmpty || d.remoteShutdown.nonEmpty - if (d.commitments.localParams.isFunder && !shutdownInProgress) { + if (d.commitments.localParams.isInitiator && !shutdownInProgress) { val currentFeeratePerKw = d.commitments.localCommit.spec.commitTxFeerate val networkFeeratePerKw = nodeParams.onChainFeeConf.getCommitmentFeerate(remoteNodeId, d.commitments.channelType, d.commitments.capacity, None) if (nodeParams.onChainFeeConf.shouldUpdateFee(currentFeeratePerKw, networkFeeratePerKw)) { @@ -1427,9 +1427,9 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val case Event(_: ChannelReestablish, d: DATA_NEGOTIATING) => // BOLT 2: A node if it has sent a previous shutdown MUST retransmit shutdown. - // negotiation restarts from the beginning, and is initialized by the funder + // negotiation restarts from the beginning, and is initialized by the channel initiator // note: in any case we still need to keep all previously sent closing_signed, because they may publish one of them - if (d.commitments.localParams.isFunder) { + if (d.commitments.localParams.isInitiator) { // we could use the last closing_signed we sent, but network fees may have changed while we were offline so it is better to restart from scratch val (closingTx, closingSigned) = Closing.MutualClose.makeFirstClosingTx(keyManager, d.commitments, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets, None) val closingTxProposed1 = d.closingTxProposed :+ List(ClosingTxProposed(closingTx, closingSigned)) @@ -1696,8 +1696,8 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val private def handleCurrentFeerate(c: CurrentFeerates, d: PersistentChannelData) = { val networkFeeratePerKw = nodeParams.onChainFeeConf.getCommitmentFeerate(remoteNodeId, d.commitments.channelType, d.commitments.capacity, Some(c)) val currentFeeratePerKw = d.commitments.localCommit.spec.commitTxFeerate - val shouldUpdateFee = d.commitments.localParams.isFunder && nodeParams.onChainFeeConf.shouldUpdateFee(currentFeeratePerKw, networkFeeratePerKw) - val shouldClose = !d.commitments.localParams.isFunder && + val shouldUpdateFee = d.commitments.localParams.isInitiator && nodeParams.onChainFeeConf.shouldUpdateFee(currentFeeratePerKw, networkFeeratePerKw) + val shouldClose = !d.commitments.localParams.isInitiator && nodeParams.onChainFeeConf.feerateToleranceFor(d.commitments.remoteNodeId).isFeeDiffTooHigh(d.commitments.channelType, networkFeeratePerKw, currentFeeratePerKw) && d.commitments.hasPendingOrProposedHtlcs // we close only if we have HTLCs potentially at risk if (shouldUpdateFee) { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenSingleFunder.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenSingleFunder.scala index 6bf5fa5389..932f8016a1 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenSingleFunder.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenSingleFunder.scala @@ -77,7 +77,7 @@ trait ChannelOpenSingleFunder extends FundingHandlers with ErrorHandlers { Helpers.validateParamsFundee(nodeParams, channelType, localParams.initFeatures, open, remoteNodeId, remoteInit.features) match { case Left(t) => handleLocalError(t, d, Some(open)) case Right((channelFeatures, remoteShutdownScript)) => - context.system.eventStream.publish(ChannelCreated(self, peer, remoteNodeId, isFunder = false, open.temporaryChannelId, open.feeratePerKw, None)) + context.system.eventStream.publish(ChannelCreated(self, peer, remoteNodeId, isInitiator = false, open.temporaryChannelId, open.feeratePerKw, None)) val fundingPubkey = keyManager.fundingPublicKey(localParams.fundingKeyPath).publicKey val channelKeyPath = keyManager.keyPath(localParams, channelConfig) val minimumDepth = Helpers.minDepthForFunding(nodeParams.channelConf, open.fundingSatoshis) @@ -351,7 +351,7 @@ trait ChannelOpenSingleFunder extends FundingHandlers with ErrorHandlers { case Success(_) => log.info(s"channelId=${commitments.channelId} was confirmed at blockHeight=$blockHeight txIndex=$txIndex") blockchain ! WatchFundingLost(self, commitments.commitInput.outPoint.txid, nodeParams.channelConf.minDepthBlocks) - if (!d.commitments.localParams.isFunder) context.system.eventStream.publish(TransactionPublished(commitments.channelId, remoteNodeId, fundingTx, 0 sat, "funding")) + if (!d.commitments.localParams.isInitiator) context.system.eventStream.publish(TransactionPublished(commitments.channelId, remoteNodeId, fundingTx, 0 sat, "funding")) context.system.eventStream.publish(TransactionConfirmed(commitments.channelId, remoteNodeId, fundingTx)) val channelKeyPath = keyManager.keyPath(d.commitments.localParams, commitments.channelConfig) val nextPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, 1) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ErrorHandlers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ErrorHandlers.scala index 1856a67ab3..096f3bd610 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ErrorHandlers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ErrorHandlers.scala @@ -55,12 +55,12 @@ trait ErrorHandlers extends CommonHandlers { case Left(negotiating) => DATA_CLOSING(negotiating.commitments, fundingTx = None, waitingSince = nodeParams.currentBlockHeight, negotiating.closingTxProposed.flatten.map(_.unsignedTx), mutualClosePublished = closingTx :: Nil) case Right(closing) => closing.copy(mutualClosePublished = closing.mutualClosePublished :+ closingTx) } - goto(CLOSING) using nextData storing() calling doPublish(closingTx, nextData.commitments.localParams.isFunder) + goto(CLOSING) using nextData storing() calling doPublish(closingTx, nextData.commitments.localParams.isInitiator) } - def doPublish(closingTx: ClosingTx, isFunder: Boolean): Unit = { - // the funder pays the fee - val fee = if (isFunder) closingTx.fee else 0.sat + def doPublish(closingTx: ClosingTx, isInitiator: Boolean): Unit = { + // the initiator pays the fee + val fee = if (isInitiator) closingTx.fee else 0.sat txPublisher ! PublishFinalTx(closingTx, fee, None) blockchain ! WatchTxConfirmed(self, closingTx.tx.txid, nodeParams.channelConf.minDepthBlocks) } @@ -192,15 +192,15 @@ trait ErrorHandlers extends CommonHandlers { import localCommitPublished._ val commitInput = commitments.commitInput.outPoint - val isFunder = commitments.localParams.isFunder + val isInitiator = commitments.localParams.isInitiator val publishQueue = commitments.commitmentFormat match { case Transactions.DefaultCommitmentFormat => val redeemableHtlcTxs = htlcTxs.values.flatten.map(tx => PublishFinalTx(tx, tx.fee, Some(commitTx.txid))) - List(PublishFinalTx(commitTx, commitInput, "commit-tx", Closing.commitTxFee(commitments.commitInput, commitTx, isFunder), None)) ++ (claimMainDelayedOutputTx.map(tx => PublishFinalTx(tx, tx.fee, None)) ++ redeemableHtlcTxs ++ claimHtlcDelayedTxs.map(tx => PublishFinalTx(tx, tx.fee, None))) + List(PublishFinalTx(commitTx, commitInput, "commit-tx", Closing.commitTxFee(commitments.commitInput, commitTx, isInitiator), None)) ++ (claimMainDelayedOutputTx.map(tx => PublishFinalTx(tx, tx.fee, None)) ++ redeemableHtlcTxs ++ claimHtlcDelayedTxs.map(tx => PublishFinalTx(tx, tx.fee, None))) case _: Transactions.AnchorOutputsCommitmentFormat => val redeemableHtlcTxs = htlcTxs.values.flatten.map(tx => PublishReplaceableTx(tx, commitments)) val claimLocalAnchor = claimAnchorTxs.collect { case tx: Transactions.ClaimLocalAnchorOutputTx => PublishReplaceableTx(tx, commitments) } - List(PublishFinalTx(commitTx, commitInput, "commit-tx", Closing.commitTxFee(commitments.commitInput, commitTx, isFunder), None)) ++ claimLocalAnchor ++ claimMainDelayedOutputTx.map(tx => PublishFinalTx(tx, tx.fee, None)) ++ redeemableHtlcTxs ++ claimHtlcDelayedTxs.map(tx => PublishFinalTx(tx, tx.fee, None)) + List(PublishFinalTx(commitTx, commitInput, "commit-tx", Closing.commitTxFee(commitments.commitInput, commitTx, isInitiator), None)) ++ claimLocalAnchor ++ claimMainDelayedOutputTx.map(tx => PublishFinalTx(tx, tx.fee, None)) ++ redeemableHtlcTxs ++ claimHtlcDelayedTxs.map(tx => PublishFinalTx(tx, tx.fee, None)) } publishIfNeeded(publishQueue, irrevocablySpent) @@ -221,7 +221,7 @@ trait ErrorHandlers extends CommonHandlers { log.warning(s"they published their current commit in txid=${commitTx.txid}") require(commitTx.txid == d.commitments.remoteCommit.txid, "txid mismatch") - context.system.eventStream.publish(TransactionPublished(d.channelId, remoteNodeId, commitTx, Closing.commitTxFee(d.commitments.commitInput, commitTx, d.commitments.localParams.isFunder), "remote-commit")) + context.system.eventStream.publish(TransactionPublished(d.channelId, remoteNodeId, commitTx, Closing.commitTxFee(d.commitments.commitInput, commitTx, d.commitments.localParams.isInitiator), "remote-commit")) val remoteCommitPublished = Closing.RemoteClose.claimCommitTxOutputs(keyManager, d.commitments, d.commitments.remoteCommit, commitTx, nodeParams.currentBlockHeight, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) val nextData = d match { case closing: DATA_CLOSING => closing.copy(remoteCommitPublished = Some(remoteCommitPublished)) @@ -234,7 +234,7 @@ trait ErrorHandlers extends CommonHandlers { def handleRemoteSpentFuture(commitTx: Transaction, d: DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT) = { log.warning(s"they published their future commit (because we asked them to) in txid=${commitTx.txid}") - context.system.eventStream.publish(TransactionPublished(d.channelId, remoteNodeId, commitTx, Closing.commitTxFee(d.commitments.commitInput, commitTx, d.commitments.localParams.isFunder), "future-remote-commit")) + context.system.eventStream.publish(TransactionPublished(d.channelId, remoteNodeId, commitTx, Closing.commitTxFee(d.commitments.commitInput, commitTx, d.commitments.localParams.isInitiator), "future-remote-commit")) val remotePerCommitmentPoint = d.remoteChannelReestablish.myCurrentPerCommitmentPoint val remoteCommitPublished = RemoteCommitPublished( commitTx = commitTx, @@ -253,7 +253,7 @@ trait ErrorHandlers extends CommonHandlers { val remoteCommit = waitingForRevocation.nextRemoteCommit require(commitTx.txid == remoteCommit.txid, "txid mismatch") - context.system.eventStream.publish(TransactionPublished(d.channelId, remoteNodeId, commitTx, Closing.commitTxFee(d.commitments.commitInput, commitTx, d.commitments.localParams.isFunder), "next-remote-commit")) + context.system.eventStream.publish(TransactionPublished(d.channelId, remoteNodeId, commitTx, Closing.commitTxFee(d.commitments.commitInput, commitTx, d.commitments.localParams.isInitiator), "next-remote-commit")) val remoteCommitPublished = Closing.RemoteClose.claimCommitTxOutputs(keyManager, d.commitments, remoteCommit, commitTx, nodeParams.currentBlockHeight, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) val nextData = d match { case closing: DATA_CLOSING => closing.copy(nextRemoteCommitPublished = Some(remoteCommitPublished)) @@ -287,7 +287,7 @@ trait ErrorHandlers extends CommonHandlers { Closing.RevokedClose.claimCommitTxOutputs(keyManager, d.commitments, tx, nodeParams.db.channels, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) match { case Some(revokedCommitPublished) => log.warning(s"txid=${tx.txid} was a revoked commitment, publishing the penalty tx") - context.system.eventStream.publish(TransactionPublished(d.channelId, remoteNodeId, tx, Closing.commitTxFee(d.commitments.commitInput, tx, d.commitments.localParams.isFunder), "revoked-commit")) + context.system.eventStream.publish(TransactionPublished(d.channelId, remoteNodeId, tx, Closing.commitTxFee(d.commitments.commitInput, tx, d.commitments.localParams.isInitiator), "revoked-commit")) val exc = FundingTxSpent(d.channelId, tx) val error = Error(d.channelId, exc.getMessage) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/ChannelKeyManager.scala b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/ChannelKeyManager.scala index b592f28683..e5e4bfdfb6 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/ChannelKeyManager.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/ChannelKeyManager.scala @@ -52,12 +52,12 @@ trait ChannelKeyManager { } /** - * @param isFunder true if we're funding this channel + * @param isInitiator true if we initiated the channel open * @return a partial key path for a new funding public key. This key path will be extended: * - with a specific "chain" prefix * - with a specific "funding pubkey" suffix */ - def newFundingKeyPath(isFunder: Boolean): DeterministicWallet.KeyPath + def newFundingKeyPath(isInitiator: Boolean): DeterministicWallet.KeyPath /** * @param tx input transaction diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/LocalChannelKeyManager.scala b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/LocalChannelKeyManager.scala index 03da9f7157..826aeb928c 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/LocalChannelKeyManager.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/LocalChannelKeyManager.scala @@ -72,8 +72,8 @@ class LocalChannelKeyManager(seed: ByteVector, chainHash: ByteVector32) extends private def shaSeed(channelKeyPath: DeterministicWallet.KeyPath): ByteVector32 = Crypto.sha256(privateKeys.get(internalKeyPath(channelKeyPath, hardened(5))).privateKey.value :+ 1.toByte) - override def newFundingKeyPath(isFunder: Boolean): KeyPath = { - val last = DeterministicWallet.hardened(if (isFunder) 1 else 0) + override def newFundingKeyPath(isInitiator: Boolean): KeyPath = { + val last = DeterministicWallet.hardened(if (isInitiator) 1 else 0) def next(): Long = randomLong() & 0xFFFFFFFFL diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/DbEventHandler.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/DbEventHandler.scala index 60293b3b81..e112fea7b6 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/DbEventHandler.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/DbEventHandler.scala @@ -111,7 +111,7 @@ class DbEventHandler(nodeParams: NodeParams) extends Actor with DiagnosticActorL case ChannelStateChanged(_, channelId, _, remoteNodeId, WAIT_FOR_FUNDING_LOCKED, NORMAL, Some(commitments: Commitments)) => ChannelMetrics.ChannelLifecycleEvents.withTag(ChannelTags.Event, ChannelTags.Events.Created).increment() val event = ChannelEvent.EventType.Created - auditDb.add(ChannelEvent(channelId, remoteNodeId, commitments.capacity, commitments.localParams.isFunder, !commitments.announceChannel, event)) + auditDb.add(ChannelEvent(channelId, remoteNodeId, commitments.capacity, commitments.localParams.isInitiator, !commitments.announceChannel, event)) channelsDb.updateChannelMeta(channelId, event) case ChannelStateChanged(_, _, _, _, WAIT_FOR_INIT_INTERNAL, _, _) => case ChannelStateChanged(_, channelId, _, _, OFFLINE, SYNCING, _) => @@ -124,7 +124,7 @@ class DbEventHandler(nodeParams: NodeParams) extends Actor with DiagnosticActorL case e: ChannelClosed => ChannelMetrics.ChannelLifecycleEvents.withTag(ChannelTags.Event, ChannelTags.Events.Closed).increment() val event = ChannelEvent.EventType.Closed(e.closingType) - auditDb.add(ChannelEvent(e.channelId, e.commitments.remoteParams.nodeId, e.commitments.commitInput.txOut.amount, e.commitments.localParams.isFunder, !e.commitments.announceChannel, event)) + auditDb.add(ChannelEvent(e.channelId, e.commitments.remoteParams.nodeId, e.commitments.commitInput.txOut.amount, e.commitments.localParams.isInitiator, !e.commitments.announceChannel, event)) channelsDb.updateChannelMeta(e.channelId, event) case u: ChannelUpdateParametersChanged => @@ -152,7 +152,7 @@ object DbEventHandler { def props(nodeParams: NodeParams): Props = Props(new DbEventHandler(nodeParams)) // @formatter:off - case class ChannelEvent(channelId: ByteVector32, remoteNodeId: PublicKey, capacity: Satoshi, isFunder: Boolean, isPrivate: Boolean, event: ChannelEvent.EventType) + case class ChannelEvent(channelId: ByteVector32, remoteNodeId: PublicKey, capacity: Satoshi, isInitiator: Boolean, isPrivate: Boolean, event: ChannelEvent.EventType) object ChannelEvent { sealed trait EventType { def label: String } object EventType { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgAuditDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgAuditDb.scala index 9d12c7fdc0..c1adb13ddd 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgAuditDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgAuditDb.scala @@ -167,7 +167,7 @@ class PgAuditDb(implicit ds: DataSource) extends AuditDb with Logging { statement.setString(1, e.channelId.toHex) statement.setString(2, e.remoteNodeId.value.toHex) statement.setLong(3, e.capacity.toLong) - statement.setBoolean(4, e.isFunder) + statement.setBoolean(4, e.isInitiator) statement.setBoolean(5, e.isPrivate) statement.setString(6, e.event.label) statement.setTimestamp(7, Timestamp.from(Instant.now())) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteAuditDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteAuditDb.scala index 0fba4b971d..b28763ecb8 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteAuditDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteAuditDb.scala @@ -174,7 +174,7 @@ class SqliteAuditDb(val sqlite: Connection) extends AuditDb with Logging { statement.setBytes(1, e.channelId.toArray) statement.setBytes(2, e.remoteNodeId.value.toArray) statement.setLong(3, e.capacity.toLong) - statement.setBoolean(4, e.isFunder) + statement.setBoolean(4, e.isInitiator) statement.setBoolean(5, e.isPrivate) statement.setString(6, e.event.label) statement.setLong(7, TimestampMilli.now().toLong) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala index 0c0ef910c7..72735a7b95 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala @@ -147,7 +147,7 @@ class Peer(val nodeParams: NodeParams, remoteNodeId: PublicKey, wallet: OnChainA val channelConfig = ChannelConfig.standard // If a channel type was provided, we directly use it instead of computing it based on local and remote features. val channelType = c.channelType_opt.getOrElse(ChannelTypes.defaultFromFeatures(d.localFeatures, d.remoteFeatures)) - val (channel, localParams) = createNewChannel(nodeParams, d.localFeatures, channelType, funder = true, c.fundingSatoshis, origin_opt = Some(sender())) + val (channel, localParams) = createNewChannel(nodeParams, d.localFeatures, channelType, isInitiator = true, c.fundingSatoshis, origin_opt = Some(sender())) c.timeout_opt.map(openTimeout => context.system.scheduler.scheduleOnce(openTimeout.duration, channel, Channel.TickChannelOpenTimeout)(context.dispatcher)) val temporaryChannelId = randomBytes32() val channelFeeratePerKw = nodeParams.onChainFeeConf.getCommitmentFeerate(remoteNodeId, channelType, c.fundingSatoshis, None) @@ -174,7 +174,7 @@ class Peer(val nodeParams: NodeParams, remoteNodeId: PublicKey, wallet: OnChainA } chosenChannelType match { case Right(channelType) => - val (channel, localParams) = createNewChannel(nodeParams, d.localFeatures, channelType, funder = false, fundingAmount = msg.fundingSatoshis, origin_opt = None) + val (channel, localParams) = createNewChannel(nodeParams, d.localFeatures, channelType, isInitiator = false, fundingAmount = msg.fundingSatoshis, origin_opt = None) val temporaryChannelId = msg.temporaryChannelId log.info(s"accepting a new channel with type=$channelType temporaryChannelId=$temporaryChannelId localParams=$localParams") channel ! INPUT_INIT_FUNDEE(temporaryChannelId, localParams, d.peerConnection, d.remoteInit, channelConfig, channelType) @@ -356,14 +356,14 @@ class Peer(val nodeParams: NodeParams, remoteNodeId: PublicKey, wallet: OnChainA s(e) } - def createNewChannel(nodeParams: NodeParams, initFeatures: Features[InitFeature], channelType: SupportedChannelType, funder: Boolean, fundingAmount: Satoshi, origin_opt: Option[ActorRef]): (ActorRef, LocalParams) = { + def createNewChannel(nodeParams: NodeParams, initFeatures: Features[InitFeature], channelType: SupportedChannelType, isInitiator: Boolean, fundingAmount: Satoshi, origin_opt: Option[ActorRef]): (ActorRef, LocalParams) = { val (finalScript, walletStaticPaymentBasepoint) = if (channelType.paysDirectlyToWallet) { val walletKey = Helpers.getWalletPaymentBasepoint(wallet)(ExecutionContext.Implicits.global) (Script.write(Script.pay2wpkh(walletKey)), Some(walletKey)) } else { (Helpers.getFinalScriptPubKey(wallet, nodeParams.chainHash)(ExecutionContext.Implicits.global), None) } - val localParams = makeChannelParams(nodeParams, initFeatures, finalScript, walletStaticPaymentBasepoint, funder, fundingAmount) + val localParams = makeChannelParams(nodeParams, initFeatures, finalScript, walletStaticPaymentBasepoint, isInitiator, fundingAmount) val channel = spawnChannel(origin_opt) (channel, localParams) } @@ -500,17 +500,17 @@ object Peer { case class RelayOnionMessage(messageId: ByteVector32, msg: OnionMessage, replyTo_opt: Option[typed.ActorRef[Status]]) // @formatter:on - def makeChannelParams(nodeParams: NodeParams, initFeatures: Features[InitFeature], defaultFinalScriptPubkey: ByteVector, walletStaticPaymentBasepoint: Option[PublicKey], isFunder: Boolean, fundingAmount: Satoshi): LocalParams = { + def makeChannelParams(nodeParams: NodeParams, initFeatures: Features[InitFeature], defaultFinalScriptPubkey: ByteVector, walletStaticPaymentBasepoint: Option[PublicKey], isInitiator: Boolean, fundingAmount: Satoshi): LocalParams = { LocalParams( nodeParams.nodeId, - nodeParams.channelKeyManager.newFundingKeyPath(isFunder), // we make sure that funder and fundee key path end differently + nodeParams.channelKeyManager.newFundingKeyPath(isInitiator), // we make sure that initiator and non-initiator key paths end differently dustLimit = nodeParams.channelConf.dustLimit, maxHtlcValueInFlightMsat = nodeParams.channelConf.maxHtlcValueInFlightMsat, channelReserve = (fundingAmount * nodeParams.channelConf.reserveToFundingRatio).max(nodeParams.channelConf.dustLimit), // BOLT #2: make sure that our reserve is above our dust limit htlcMinimum = nodeParams.channelConf.htlcMinimum, toSelfDelay = nodeParams.channelConf.toRemoteDelay, // we choose their delay maxAcceptedHtlcs = nodeParams.channelConf.maxAcceptedHtlcs, - isFunder = isFunder, + isInitiator = isInitiator, defaultFinalScriptPubKey = defaultFinalScriptPubkey, walletStaticPaymentBasepoint = walletStaticPaymentBasepoint, initFeatures = initFeatures) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/json/JsonSerializers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/json/JsonSerializers.scala index b296f5cdb2..b696715fe7 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/json/JsonSerializers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/json/JsonSerializers.scala @@ -398,7 +398,7 @@ object ChannelEventSerializer extends MinimalSerializer({ case e: ChannelCreated => JObject( JField("type", JString("channel-opened")), JField("remoteNodeId", JString(e.remoteNodeId.toString())), - JField("isFunder", JBool(e.isFunder)), + JField("isInitiator", JBool(e.isInitiator)), JField("temporaryChannelId", JString(e.temporaryChannelId.toHex)), JField("initialFeeratePerKw", JLong(e.initialFeeratePerKw.toLong)), JField("fundingTxFeeratePerKw", e.fundingTxFeeratePerKw.map(f => JLong(f.toLong)).getOrElse(JNothing)) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Transactions.scala b/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Transactions.scala index 806a5d55d6..3256c731f7 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Transactions.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Transactions.scala @@ -275,10 +275,10 @@ object Transactions { * down to Satoshi. */ def commitTxTotalCostMsat(dustLimit: Satoshi, spec: CommitmentSpec, commitmentFormat: CommitmentFormat): MilliSatoshi = { - // The funder pays the on-chain fee by deducing it from its main output. + // The channel initiator pays the on-chain fee by deducing it from its main output. val txFee = commitTxFeeMsat(dustLimit, spec, commitmentFormat) - // When using anchor outputs, the funder pays for *both* anchors all the time, even if only one anchor is present. - // This is not technically a fee (it doesn't go to miners) but it also has to be deduced from the funder's main output. + // When using anchor outputs, the channel initiator pays for *both* anchors all the time, even if only one anchor is present. + // This is not technically a fee (it doesn't go to miners) but it also has to be deduced from the channel initiator's main output. val anchorsCost = commitmentFormat match { case DefaultCommitmentFormat => Satoshi(0) case _: AnchorOutputsCommitmentFormat => AnchorOutputsCommitmentFormat.anchorAmount * 2 @@ -290,14 +290,14 @@ object Transactions { /** * @param commitTxNumber commit tx number - * @param isFunder true if local node is funder + * @param isInitiator true if local node initiated the channel open * @param localPaymentBasePoint local payment base point * @param remotePaymentBasePoint remote payment base point * @return the obscured tx number as defined in BOLT #3 (a 48 bits integer) */ - def obscuredCommitTxNumber(commitTxNumber: Long, isFunder: Boolean, localPaymentBasePoint: PublicKey, remotePaymentBasePoint: PublicKey): Long = { + def obscuredCommitTxNumber(commitTxNumber: Long, isInitiator: Boolean, localPaymentBasePoint: PublicKey, remotePaymentBasePoint: PublicKey): Long = { // from BOLT 3: SHA256(payment-basepoint from open_channel || payment-basepoint from accept_channel) - val h = if (isFunder) { + val h = if (isInitiator) { Crypto.sha256(localPaymentBasePoint.value ++ remotePaymentBasePoint.value) } else { Crypto.sha256(remotePaymentBasePoint.value ++ localPaymentBasePoint.value) @@ -308,14 +308,14 @@ object Transactions { /** * @param commitTx commit tx - * @param isFunder true if local node is funder + * @param isInitiator true if local node initiated the channel open * @param localPaymentBasePoint local payment base point * @param remotePaymentBasePoint remote payment base point * @return the actual commit tx number that was blinded and stored in locktime and sequence fields */ - def getCommitTxNumber(commitTx: Transaction, isFunder: Boolean, localPaymentBasePoint: PublicKey, remotePaymentBasePoint: PublicKey): Long = { + def getCommitTxNumber(commitTx: Transaction, isInitiator: Boolean, localPaymentBasePoint: PublicKey, remotePaymentBasePoint: PublicKey): Long = { require(commitTx.txIn.size == 1, "commitment tx should have 1 input") - val blind = obscuredCommitTxNumber(0, isFunder, localPaymentBasePoint, remotePaymentBasePoint) + val blind = obscuredCommitTxNumber(0, isInitiator, localPaymentBasePoint, remotePaymentBasePoint) val obscured = decodeTxNumber(commitTx.txIn.head.sequence, commitTx.lockTime) obscured ^ blind } @@ -363,7 +363,7 @@ object Transactions { } } - def makeCommitTxOutputs(localIsFunder: Boolean, + def makeCommitTxOutputs(localIsInitiator: Boolean, localDustLimit: Satoshi, localRevocationPubkey: PublicKey, toLocalDelay: CltvExpiryDelta, @@ -389,7 +389,7 @@ object Transactions { val hasHtlcs = outputs.nonEmpty - val (toLocalAmount: Satoshi, toRemoteAmount: Satoshi) = if (localIsFunder) { + val (toLocalAmount: Satoshi, toRemoteAmount: Satoshi) = if (localIsInitiator) { (spec.toLocal.truncateToSatoshi - commitTxTotalCost(localDustLimit, spec, commitmentFormat), spec.toRemote.truncateToSatoshi) } else { (spec.toLocal.truncateToSatoshi, spec.toRemote.truncateToSatoshi - commitTxTotalCost(localDustLimit, spec, commitmentFormat)) @@ -433,9 +433,9 @@ object Transactions { commitTxNumber: Long, localPaymentBasePoint: PublicKey, remotePaymentBasePoint: PublicKey, - localIsFunder: Boolean, + localIsInitiator: Boolean, outputs: CommitmentOutputs): CommitTx = { - val txNumber = obscuredCommitTxNumber(commitTxNumber, localIsFunder, localPaymentBasePoint, remotePaymentBasePoint) + val txNumber = obscuredCommitTxNumber(commitTxNumber, localIsInitiator, localPaymentBasePoint, remotePaymentBasePoint) val (sequence, lockTime) = encodeTxNumber(txNumber) val tx = Transaction( @@ -783,10 +783,10 @@ object Transactions { } } - def makeClosingTx(commitTxInput: InputInfo, localScriptPubKey: ByteVector, remoteScriptPubKey: ByteVector, localIsFunder: Boolean, dustLimit: Satoshi, closingFee: Satoshi, spec: CommitmentSpec): ClosingTx = { + def makeClosingTx(commitTxInput: InputInfo, localScriptPubKey: ByteVector, remoteScriptPubKey: ByteVector, localIsInitiator: Boolean, dustLimit: Satoshi, closingFee: Satoshi, spec: CommitmentSpec): ClosingTx = { require(spec.htlcs.isEmpty, "there shouldn't be any pending htlcs") - val (toLocalAmount: Satoshi, toRemoteAmount: Satoshi) = if (localIsFunder) { + val (toLocalAmount: Satoshi, toRemoteAmount: Satoshi) = if (localIsInitiator) { (spec.toLocal.truncateToSatoshi - closingFee, spec.toRemote.truncateToSatoshi) } else { (spec.toLocal.truncateToSatoshi, spec.toRemote.truncateToSatoshi - closingFee) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelCodecs0.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelCodecs0.scala index fe817ddf86..ee03273f51 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelCodecs0.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelCodecs0.scala @@ -74,7 +74,7 @@ private[channel] object ChannelCodecs0 { ("htlcMinimum" | millisatoshi) :: ("toSelfDelay" | cltvExpiryDelta) :: ("maxAcceptedHtlcs" | uint16) :: - ("isFunder" | bool) :: + ("isInitiator" | bool) :: ("defaultFinalScriptPubKey" | varsizebinarydata) :: ("walletStaticPaymentBasepoint" | optional(provide(channelVersion.paysDirectlyToWallet), publicKey)) :: ("features" | combinedFeaturesCodec)).as[LocalParams].decodeOnly diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version1/ChannelCodecs1.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version1/ChannelCodecs1.scala index 857dc5af38..e1fc5e2518 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version1/ChannelCodecs1.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version1/ChannelCodecs1.scala @@ -60,7 +60,7 @@ private[channel] object ChannelCodecs1 { ("htlcMinimum" | millisatoshi) :: ("toSelfDelay" | cltvExpiryDelta) :: ("maxAcceptedHtlcs" | uint16) :: - ("isFunder" | bool8) :: + ("isInitiator" | bool8) :: ("defaultFinalScriptPubKey" | lengthDelimited(bytes)) :: ("walletStaticPaymentBasepoint" | optional(provide(channelVersion.paysDirectlyToWallet), publicKey)) :: ("features" | combinedFeaturesCodec)).as[LocalParams] diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version2/ChannelCodecs2.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version2/ChannelCodecs2.scala index ff4853a7ee..155c648267 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version2/ChannelCodecs2.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version2/ChannelCodecs2.scala @@ -60,7 +60,7 @@ private[channel] object ChannelCodecs2 { ("htlcMinimum" | millisatoshi) :: ("toSelfDelay" | cltvExpiryDelta) :: ("maxAcceptedHtlcs" | uint16) :: - ("isFunder" | bool8) :: + ("isInitiator" | bool8) :: ("defaultFinalScriptPubKey" | lengthDelimited(bytes)) :: ("walletStaticPaymentBasepoint" | optional(provide(channelVersion.paysDirectlyToWallet), publicKey)) :: ("features" | combinedFeaturesCodec)).as[LocalParams] diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3.scala index c7686b6af1..423e172fef 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3.scala @@ -79,7 +79,7 @@ private[channel] object ChannelCodecs3 { ("htlcMinimum" | millisatoshi) :: ("toSelfDelay" | cltvExpiryDelta) :: ("maxAcceptedHtlcs" | uint16) :: - ("isFunder" | bool8) :: + ("isInitiator" | bool8) :: ("defaultFinalScriptPubKey" | lengthDelimited(bytes)) :: ("walletStaticPaymentBasepoint" | optional(provide(channelFeatures.paysDirectlyToWallet), publicKey)) :: ("features" | combinedFeaturesCodec)).as[LocalParams] diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala b/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala index 55fc4fd585..061359af68 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala @@ -205,7 +205,7 @@ object TestConstants { nodeParams.features.initFeatures(), Script.write(Script.pay2wpkh(randomKey().publicKey)), None, - isFunder = true, + isInitiator = true, fundingSatoshis ).copy( channelReserve = 10000 sat // Bob will need to keep that much satoshis as direct payment @@ -343,8 +343,9 @@ object TestConstants { nodeParams.features.initFeatures(), Script.write(Script.pay2wpkh(randomKey().publicKey)), None, - isFunder = false, - fundingSatoshis).copy( + isInitiator = false, + fundingSatoshis + ).copy( channelReserve = 20000 sat // Alice will need to keep that much satoshis as direct payment ) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/CommitmentsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/CommitmentsSpec.scala index 25ebee2aad..a845529826 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/CommitmentsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/CommitmentsSpec.scala @@ -381,8 +381,8 @@ class CommitmentsSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // See https://github.com/lightningnetwork/lightning-rfc/issues/728 test("funder keeps additional reserve to avoid channel being stuck") { f => - val isFunder = true - val c = CommitmentsSpec.makeCommitments(100000000 msat, 50000000 msat, FeeratePerKw(2500 sat), 546 sat, isFunder) + val isInitiator = true + val c = CommitmentsSpec.makeCommitments(100000000 msat, 50000000 msat, FeeratePerKw(2500 sat), 546 sat, isInitiator) val (_, cmdAdd) = makeCmdAdd(c.availableBalanceForSend, randomKey().publicKey, f.currentBlockHeight) val Right((c1, _)) = sendAdd(c, cmdAdd, f.currentBlockHeight, feeConfNoMismatch) assert(c1.availableBalanceForSend === 0.msat) @@ -396,8 +396,8 @@ class CommitmentsSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with } test("can send availableForSend") { f => - for (isFunder <- Seq(true, false)) { - val c = CommitmentsSpec.makeCommitments(702000000 msat, 52000000 msat, FeeratePerKw(2679 sat), 546 sat, isFunder) + for (isInitiator <- Seq(true, false)) { + val c = CommitmentsSpec.makeCommitments(702000000 msat, 52000000 msat, FeeratePerKw(2679 sat), 546 sat, isInitiator) val (_, cmdAdd) = makeCmdAdd(c.availableBalanceForSend, randomKey().publicKey, f.currentBlockHeight) val result = sendAdd(c, cmdAdd, f.currentBlockHeight, feeConfNoMismatch) assert(result.isRight, result) @@ -405,8 +405,8 @@ class CommitmentsSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with } test("can receive availableForReceive") { f => - for (isFunder <- Seq(true, false)) { - val c = CommitmentsSpec.makeCommitments(31000000 msat, 702000000 msat, FeeratePerKw(2679 sat), 546 sat, isFunder) + for (isInitiator <- Seq(true, false)) { + val c = CommitmentsSpec.makeCommitments(31000000 msat, 702000000 msat, FeeratePerKw(2679 sat), 546 sat, isInitiator) val add = UpdateAddHtlc(randomBytes32(), c.remoteNextHtlcId, c.availableBalanceForReceive, randomBytes32(), CltvExpiry(f.currentBlockHeight), TestConstants.emptyOnionPacket) receiveAdd(c, add, feeConfNoMismatch) } @@ -414,17 +414,17 @@ class CommitmentsSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with test("should always be able to send availableForSend", Tag("fuzzy")) { f => val maxPendingHtlcAmount = 1000000.msat - case class FuzzTest(isFunder: Boolean, pendingHtlcs: Int, feeRatePerKw: FeeratePerKw, dustLimit: Satoshi, toLocal: MilliSatoshi, toRemote: MilliSatoshi) + case class FuzzTest(isInitiator: Boolean, pendingHtlcs: Int, feeRatePerKw: FeeratePerKw, dustLimit: Satoshi, toLocal: MilliSatoshi, toRemote: MilliSatoshi) for (_ <- 1 to 100) { val t = FuzzTest( - isFunder = Random.nextInt(2) == 0, + isInitiator = Random.nextInt(2) == 0, pendingHtlcs = Random.nextInt(10), feeRatePerKw = FeeratePerKw(Random.nextInt(10000).max(1).sat), dustLimit = Random.nextInt(1000).sat, // We make sure both sides have enough to send/receive at least the initial pending HTLCs. toLocal = maxPendingHtlcAmount * 2 * 10 + Random.nextInt(1000000000).msat, toRemote = maxPendingHtlcAmount * 2 * 10 + Random.nextInt(1000000000).msat) - var c = CommitmentsSpec.makeCommitments(t.toLocal, t.toRemote, t.feeRatePerKw, t.dustLimit, t.isFunder) + var c = CommitmentsSpec.makeCommitments(t.toLocal, t.toRemote, t.feeRatePerKw, t.dustLimit, t.isInitiator) // Add some initial HTLCs to the pending list (bigger commit tx). for (_ <- 0 to t.pendingHtlcs) { val amount = Random.nextInt(maxPendingHtlcAmount.toLong.toInt).msat.max(1 msat) @@ -442,17 +442,17 @@ class CommitmentsSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with test("should always be able to receive availableForReceive", Tag("fuzzy")) { f => val maxPendingHtlcAmount = 1000000.msat - case class FuzzTest(isFunder: Boolean, pendingHtlcs: Int, feeRatePerKw: FeeratePerKw, dustLimit: Satoshi, toLocal: MilliSatoshi, toRemote: MilliSatoshi) + case class FuzzTest(isInitiator: Boolean, pendingHtlcs: Int, feeRatePerKw: FeeratePerKw, dustLimit: Satoshi, toLocal: MilliSatoshi, toRemote: MilliSatoshi) for (_ <- 1 to 100) { val t = FuzzTest( - isFunder = Random.nextInt(2) == 0, + isInitiator = Random.nextInt(2) == 0, pendingHtlcs = Random.nextInt(10), feeRatePerKw = FeeratePerKw(Random.nextInt(10000).max(1).sat), dustLimit = Random.nextInt(1000).sat, // We make sure both sides have enough to send/receive at least the initial pending HTLCs. toLocal = maxPendingHtlcAmount * 2 * 10 + Random.nextInt(1000000000).msat, toRemote = maxPendingHtlcAmount * 2 * 10 + Random.nextInt(1000000000).msat) - var c = CommitmentsSpec.makeCommitments(t.toLocal, t.toRemote, t.feeRatePerKw, t.dustLimit, t.isFunder) + var c = CommitmentsSpec.makeCommitments(t.toLocal, t.toRemote, t.feeRatePerKw, t.dustLimit, t.isInitiator) // Add some initial HTLCs to the pending list (bigger commit tx). for (_ <- 0 to t.pendingHtlcs) { val amount = Random.nextInt(maxPendingHtlcAmount.toLong.toInt).msat.max(1 msat) @@ -479,8 +479,8 @@ class CommitmentsSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with object CommitmentsSpec { - def makeCommitments(toLocal: MilliSatoshi, toRemote: MilliSatoshi, feeRatePerKw: FeeratePerKw = FeeratePerKw(0 sat), dustLimit: Satoshi = 0 sat, isFunder: Boolean = true, announceChannel: Boolean = true): Commitments = { - val localParams = LocalParams(randomKey().publicKey, DeterministicWallet.KeyPath(Seq(42L)), dustLimit, UInt64.MaxValue, 0 sat, 1 msat, CltvExpiryDelta(144), 50, isFunder, ByteVector.empty, None, Features.empty) + def makeCommitments(toLocal: MilliSatoshi, toRemote: MilliSatoshi, feeRatePerKw: FeeratePerKw = FeeratePerKw(0 sat), dustLimit: Satoshi = 0 sat, isInitiator: Boolean = true, announceChannel: Boolean = true): Commitments = { + val localParams = LocalParams(randomKey().publicKey, DeterministicWallet.KeyPath(Seq(42L)), dustLimit, UInt64.MaxValue, 0 sat, 1 msat, CltvExpiryDelta(144), 50, isInitiator, ByteVector.empty, None, Features.empty) val remoteParams = RemoteParams(randomKey().publicKey, dustLimit, UInt64.MaxValue, 0 sat, 1 msat, CltvExpiryDelta(144), 50, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, Features.empty, None) val commitmentInput = Funding.makeFundingInputInfo(randomBytes32(), 0, (toLocal + toRemote).truncateToSatoshi, randomKey().publicKey, remoteParams.fundingPubKey) Commitments( @@ -503,7 +503,7 @@ object CommitmentsSpec { } def makeCommitments(toLocal: MilliSatoshi, toRemote: MilliSatoshi, localNodeId: PublicKey, remoteNodeId: PublicKey, announceChannel: Boolean): Commitments = { - val localParams = LocalParams(localNodeId, DeterministicWallet.KeyPath(Seq(42L)), 0 sat, UInt64.MaxValue, 0 sat, 1 msat, CltvExpiryDelta(144), 50, isFunder = true, ByteVector.empty, None, Features.empty) + val localParams = LocalParams(localNodeId, DeterministicWallet.KeyPath(Seq(42L)), 0 sat, UInt64.MaxValue, 0 sat, 1 msat, CltvExpiryDelta(144), 50, isInitiator = true, ByteVector.empty, None, Features.empty) val remoteParams = RemoteParams(remoteNodeId, 0 sat, UInt64.MaxValue, 0 sat, 1 msat, CltvExpiryDelta(144), 50, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, Features.empty, None) val commitmentInput = Funding.makeFundingInputInfo(randomBytes32(), 0, (toLocal + toRemote).truncateToSatoshi, randomKey().publicKey, remoteParams.fundingPubKey) Commitments( diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala index a7f5c7b736..5d3baa9bc3 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala @@ -2050,7 +2050,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val initialState = bob.stateData.asInstanceOf[DATA_NORMAL] val c = CMD_UPDATE_FEE(FeeratePerKw(20000 sat), replyTo_opt = Some(sender.ref)) bob ! c - sender.expectMsg(RES_FAILURE(c, FundeeCannotSendUpdateFee(channelId(bob)))) + sender.expectMsg(RES_FAILURE(c, NonInitiatorCannotSendUpdateFee(channelId(bob)))) assert(initialState == bob.stateData) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/f/ShutdownStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/f/ShutdownStateSpec.scala index f7bd209b63..5843eb05c5 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/f/ShutdownStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/f/ShutdownStateSpec.scala @@ -530,7 +530,7 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit val sender = TestProbe() val initialState = bob.stateData.asInstanceOf[DATA_SHUTDOWN] bob ! CMD_UPDATE_FEE(FeeratePerKw(20000 sat), replyTo_opt = Some(sender.ref)) - sender.expectMsgType[RES_FAILURE[CMD_UPDATE_FEE, FundeeCannotSendUpdateFee]] + sender.expectMsgType[RES_FAILURE[CMD_UPDATE_FEE, NonInitiatorCannotSendUpdateFee]] assert(initialState == bob.stateData) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/keymanager/LocalChannelKeyManagerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/keymanager/LocalChannelKeyManagerSpec.scala index 510ea16c87..57cc1b0df7 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/keymanager/LocalChannelKeyManagerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/keymanager/LocalChannelKeyManagerSpec.scala @@ -55,16 +55,16 @@ class LocalChannelKeyManagerSpec extends AnyFunSuite { assert(keyPath.toString() == "m/1909530642'/1080788911/847211985'/1791010671/1303008749'/34154019'/723973395/767609665") } - def makefundingKeyPath(entropy: ByteVector, isFunder: Boolean): KeyPath = { + def makefundingKeyPath(entropy: ByteVector, isInitiator: Boolean): KeyPath = { val items = for (i <- 0 to 7) yield entropy.drop(i * 4).take(4).toInt(signed = false) & 0xFFFFFFFFL - val last = DeterministicWallet.hardened(if (isFunder) 1L else 0L) + val last = DeterministicWallet.hardened(if (isInitiator) 1L else 0L) KeyPath(items :+ last) } test("test vectors (testnet, funder)") { val seed = ByteVector.fromValidHex("17b086b228025fa8f4416324b6ba2ec36e68570ae2fc3d392520969f2a9d0c1501") val channelKeyManager = new LocalChannelKeyManager(seed, Block.TestnetGenesisBlock.hash) - val fundingKeyPath = makefundingKeyPath(hex"be4fa97c62b9f88437a3be577b31eb48f2165c7bc252194a15ff92d995778cfb", isFunder = true) + val fundingKeyPath = makefundingKeyPath(hex"be4fa97c62b9f88437a3be577b31eb48f2165c7bc252194a15ff92d995778cfb", isInitiator = true) val fundingPub = channelKeyManager.fundingPublicKey(fundingKeyPath) val localParams = TestConstants.Alice.channelParams.copy(fundingKeyPath = fundingKeyPath) @@ -81,7 +81,7 @@ class LocalChannelKeyManagerSpec extends AnyFunSuite { test("test vectors (testnet, fundee)") { val seed = ByteVector.fromValidHex("aeb3e9b5642cd4523e9e09164047f60adb413633549c3c6189192921311894d501") val channelKeyManager = new LocalChannelKeyManager(seed, Block.TestnetGenesisBlock.hash) - val fundingKeyPath = makefundingKeyPath(hex"06535806c1aa73971ec4877a5e2e684fa636136c073810f190b63eefc58ca488", isFunder = false) + val fundingKeyPath = makefundingKeyPath(hex"06535806c1aa73971ec4877a5e2e684fa636136c073810f190b63eefc58ca488", isInitiator = false) val fundingPub = channelKeyManager.fundingPublicKey(fundingKeyPath) val localParams = TestConstants.Alice.channelParams.copy(fundingKeyPath = fundingKeyPath) @@ -98,7 +98,7 @@ class LocalChannelKeyManagerSpec extends AnyFunSuite { test("test vectors (mainnet, funder)") { val seed = ByteVector.fromValidHex("d8d5431487c2b19ee6486aad6c3bdfb99d10b727bade7fa848e2ab7901c15bff01") val channelKeyManager = new LocalChannelKeyManager(seed, Block.LivenetGenesisBlock.hash) - val fundingKeyPath = makefundingKeyPath(hex"ec1c41cd6be2b6e4ef46c1107f6c51fbb2066d7e1f7720bde4715af233ae1322", isFunder = true) + val fundingKeyPath = makefundingKeyPath(hex"ec1c41cd6be2b6e4ef46c1107f6c51fbb2066d7e1f7720bde4715af233ae1322", isInitiator = true) val fundingPub = channelKeyManager.fundingPublicKey(fundingKeyPath) val localParams = TestConstants.Alice.channelParams.copy(fundingKeyPath = fundingKeyPath) @@ -115,7 +115,7 @@ class LocalChannelKeyManagerSpec extends AnyFunSuite { test("test vectors (mainnet, fundee)") { val seed = ByteVector.fromValidHex("4b809dd593b36131c454d60c2f7bdfd49d12ec455e5b657c47a9ca0f5dfc5eef01") val channelKeyManager = new LocalChannelKeyManager(seed, Block.LivenetGenesisBlock.hash) - val fundingKeyPath = makefundingKeyPath(hex"2b4f045be5303d53f9d3a84a1e70c12251168dc29f300cf9cece0ec85cd8182b", isFunder = false) + val fundingKeyPath = makefundingKeyPath(hex"2b4f045be5303d53f9d3a84a1e70c12251168dc29f300cf9cece0ec85cd8182b", isInitiator = false) val fundingPub = channelKeyManager.fundingPublicKey(fundingKeyPath) val localParams = TestConstants.Alice.channelParams.copy(fundingKeyPath = fundingKeyPath) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/AuditDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/AuditDbSpec.scala index cd57224c02..738242a9c7 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/AuditDbSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/AuditDbSpec.scala @@ -76,7 +76,7 @@ class AuditDbSpec extends AnyFunSuite { val e5 = PaymentSent(UUID.randomUUID(), randomBytes32(), randomBytes32(), 84100 msat, randomKey().publicKey, pp5a :: pp5b :: Nil) val pp6 = PaymentSent.PartialPayment(UUID.randomUUID(), 42000 msat, 1000 msat, randomBytes32(), None, timestamp = TimestampMilli.now() + 10.minutes) val e6 = PaymentSent(UUID.randomUUID(), randomBytes32(), randomBytes32(), 42000 msat, randomKey().publicKey, pp6 :: Nil) - val e7 = ChannelEvent(randomBytes32(), randomKey().publicKey, 456123000 sat, isFunder = true, isPrivate = false, ChannelEvent.EventType.Closed(MutualClose(null))) + val e7 = ChannelEvent(randomBytes32(), randomKey().publicKey, 456123000 sat, isInitiator = true, isPrivate = false, ChannelEvent.EventType.Closed(MutualClose(null))) val e8 = ChannelErrorOccurred(null, randomBytes32(), randomKey().publicKey, LocalError(new RuntimeException("oops")), isFatal = true) val e9 = ChannelErrorOccurred(null, randomBytes32(), randomKey().publicKey, RemoteError(Error(randomBytes32(), "remote oops")), isFatal = true) val e10 = TrampolinePaymentRelayed(randomBytes32(), Seq(PaymentRelayed.Part(20000 msat, randomBytes32()), PaymentRelayed.Part(22000 msat, randomBytes32())), Seq(PaymentRelayed.Part(10000 msat, randomBytes32()), PaymentRelayed.Part(12000 msat, randomBytes32()), PaymentRelayed.Part(15000 msat, randomBytes32())), randomKey().publicKey, 30000 msat) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentPacketSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentPacketSpec.scala index a2ec5c1343..41cf058551 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentPacketSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentPacketSpec.scala @@ -368,7 +368,7 @@ object PaymentPacketSpec { } def makeCommitments(channelId: ByteVector32, testAvailableBalanceForSend: MilliSatoshi = 50000000 msat, testAvailableBalanceForReceive: MilliSatoshi = 50000000 msat, testCapacity: Satoshi = 100000 sat): Commitments = { - val params = LocalParams(null, null, null, null, null, null, null, 0, isFunder = true, null, None, null) + val params = LocalParams(null, null, null, null, null, null, null, 0, isInitiator = true, null, None, null) val remoteParams = RemoteParams(randomKey().publicKey, null, null, null, null, null, maxAcceptedHtlcs = 0, null, null, null, null, null, null, None) val commitInput = InputInfo(OutPoint(randomBytes32(), 1), TxOut(testCapacity, Nil), Nil) val channelFlags = ChannelFlags.Private diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TestVectorsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TestVectorsSpec.scala index ef7c6f042d..604680a45e 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TestVectorsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TestVectorsSpec.scala @@ -124,7 +124,7 @@ trait TestVectorsSpec extends AnyFunSuite with Logging { val commitmentInput = Funding.makeFundingInputInfo(fundingTx.hash, 0, fundingAmount, Local.funding_pubkey, Remote.funding_pubkey) - val obscured_tx_number = Transactions.obscuredCommitTxNumber(42, isFunder = true, Local.payment_basepoint, Remote.payment_basepoint) + val obscured_tx_number = Transactions.obscuredCommitTxNumber(42, isInitiator = true, Local.payment_basepoint, Remote.payment_basepoint) assert(obscured_tx_number === (0x2bb038521914L ^ 42L)) logger.info(s"local_payment_basepoint: ${Local.payment_basepoint}") @@ -185,7 +185,7 @@ trait TestVectorsSpec extends AnyFunSuite with Logging { logger.info(s"local_feerate_per_kw: ${spec.commitTxFeerate}") val outputs = Transactions.makeCommitTxOutputs( - localIsFunder = true, + localIsInitiator = true, localDustLimit = Local.dustLimit, localRevocationPubkey = Local.revocation_pubkey, toLocalDelay = Local.toSelfDelay, @@ -204,7 +204,7 @@ trait TestVectorsSpec extends AnyFunSuite with Logging { commitTxNumber = Local.commitTxNumber, localPaymentBasePoint = Local.payment_basepoint, remotePaymentBasePoint = Remote.payment_basepoint, - localIsFunder = true, + localIsInitiator = true, outputs = outputs) val local_sig = Transactions.sign(tx, Local.funding_privkey, TxOwner.Local, commitmentFormat) logger.info(s"# local_signature = ${Scripts.der(local_sig).dropRight(1).toHex}") @@ -227,7 +227,7 @@ trait TestVectorsSpec extends AnyFunSuite with Logging { } }) - assert(Transactions.getCommitTxNumber(commitTx.tx, isFunder = true, Local.payment_basepoint, Remote.payment_basepoint) === Local.commitTxNumber) + assert(Transactions.getCommitTxNumber(commitTx.tx, isInitiator = true, Local.payment_basepoint, Remote.payment_basepoint) === Local.commitTxNumber) Transaction.correctlySpends(commitTx.tx, Seq(fundingTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) logger.info(s"output commit_tx: ${commitTx.tx}") diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TransactionsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TransactionsSpec.scala index 27e7bd4cec..ca5c56aab4 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TransactionsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TransactionsSpec.scala @@ -169,7 +169,7 @@ class TransactionsSpec extends AnyFunSuite with Logging { val paymentPreimage = randomBytes32() val htlc = UpdateAddHtlc(ByteVector32.Zeroes, 0, (20000 * 1000) msat, sha256(paymentPreimage), CltvExpiryDelta(144).toCltvExpiry(blockHeight), TestConstants.emptyOnionPacket) val spec = CommitmentSpec(Set(OutgoingHtlc(htlc)), feeratePerKw, toLocal = 0 msat, toRemote = 0 msat) - val outputs = makeCommitTxOutputs(localIsFunder = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, spec, DefaultCommitmentFormat) + val outputs = makeCommitTxOutputs(localIsInitiator = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, spec, DefaultCommitmentFormat) val pubKeyScript = write(pay2wsh(htlcOffered(localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localRevocationPriv.publicKey, ripemd160(htlc.paymentHash), DefaultCommitmentFormat))) val commitTx = Transaction(version = 0, txIn = Nil, txOut = TxOut(htlc.amountMsat.truncateToSatoshi, pubKeyScript) :: Nil, lockTime = 0) val Right(claimHtlcSuccessTx) = makeClaimHtlcSuccessTx(commitTx, outputs, localDustLimit, remoteHtlcPriv.publicKey, localHtlcPriv.publicKey, localRevocationPriv.publicKey, finalPubKeyScript, htlc, feeratePerKw, DefaultCommitmentFormat) @@ -184,7 +184,7 @@ class TransactionsSpec extends AnyFunSuite with Logging { val paymentPreimage = randomBytes32() val htlc = UpdateAddHtlc(ByteVector32.Zeroes, 0, (20000 * 1000) msat, sha256(paymentPreimage), toLocalDelay.toCltvExpiry(blockHeight), TestConstants.emptyOnionPacket) val spec = CommitmentSpec(Set(IncomingHtlc(htlc)), feeratePerKw, toLocal = 0 msat, toRemote = 0 msat) - val outputs = makeCommitTxOutputs(localIsFunder = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, spec, DefaultCommitmentFormat) + val outputs = makeCommitTxOutputs(localIsInitiator = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, spec, DefaultCommitmentFormat) val pubKeyScript = write(pay2wsh(htlcReceived(localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localRevocationPriv.publicKey, ripemd160(htlc.paymentHash), htlc.cltvExpiry, DefaultCommitmentFormat))) val commitTx = Transaction(version = 0, txIn = Nil, txOut = TxOut(htlc.amountMsat.truncateToSatoshi, pubKeyScript) :: Nil, lockTime = 0) val Right(claimClaimHtlcTimeoutTx) = makeClaimHtlcTimeoutTx(commitTx, outputs, localDustLimit, remoteHtlcPriv.publicKey, localHtlcPriv.publicKey, localRevocationPriv.publicKey, finalPubKeyScript, htlc, feeratePerKw, DefaultCommitmentFormat) @@ -222,31 +222,31 @@ class TransactionsSpec extends AnyFunSuite with Logging { { val toRemoteFundeeBelowDust = spec.copy(toRemote = belowDust) - val outputs = makeCommitTxOutputs(localIsFunder = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, toRemoteFundeeBelowDust, DefaultCommitmentFormat) + val outputs = makeCommitTxOutputs(localIsInitiator = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, toRemoteFundeeBelowDust, DefaultCommitmentFormat) assert(outputs.map(_.commitmentOutput) === Seq(CommitmentOutput.ToLocal)) assert(outputs.head.output.amount.toMilliSatoshi === toRemoteFundeeBelowDust.toLocal - commitFee) } { val toLocalFunderBelowDust = spec.copy(toLocal = belowDustWithFee) - val outputs = makeCommitTxOutputs(localIsFunder = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, toLocalFunderBelowDust, DefaultCommitmentFormat) + val outputs = makeCommitTxOutputs(localIsInitiator = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, toLocalFunderBelowDust, DefaultCommitmentFormat) assert(outputs.map(_.commitmentOutput) === Seq(CommitmentOutput.ToRemote)) assert(outputs.head.output.amount.toMilliSatoshi === toLocalFunderBelowDust.toRemote) } { val toRemoteFunderBelowDust = spec.copy(toRemote = belowDustWithFee) - val outputs = makeCommitTxOutputs(localIsFunder = false, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, toRemoteFunderBelowDust, DefaultCommitmentFormat) + val outputs = makeCommitTxOutputs(localIsInitiator = false, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, toRemoteFunderBelowDust, DefaultCommitmentFormat) assert(outputs.map(_.commitmentOutput) === Seq(CommitmentOutput.ToLocal)) assert(outputs.head.output.amount.toMilliSatoshi === toRemoteFunderBelowDust.toLocal) } { val toLocalFundeeBelowDust = spec.copy(toLocal = belowDust) - val outputs = makeCommitTxOutputs(localIsFunder = false, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, toLocalFundeeBelowDust, DefaultCommitmentFormat) + val outputs = makeCommitTxOutputs(localIsInitiator = false, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, toLocalFundeeBelowDust, DefaultCommitmentFormat) assert(outputs.map(_.commitmentOutput) === Seq(CommitmentOutput.ToRemote)) assert(outputs.head.output.amount.toMilliSatoshi === toLocalFundeeBelowDust.toRemote - commitFee) } { val allBelowDust = spec.copy(toLocal = belowDust, toRemote = belowDust) - val outputs = makeCommitTxOutputs(localIsFunder = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, allBelowDust, DefaultCommitmentFormat) + val outputs = makeCommitTxOutputs(localIsInitiator = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, allBelowDust, DefaultCommitmentFormat) assert(outputs.isEmpty) } } @@ -281,18 +281,18 @@ class TransactionsSpec extends AnyFunSuite with Logging { toLocal = 400.millibtc.toMilliSatoshi, toRemote = 300.millibtc.toMilliSatoshi) - val outputs = makeCommitTxOutputs(localIsFunder = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, spec, DefaultCommitmentFormat) + val outputs = makeCommitTxOutputs(localIsInitiator = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, spec, DefaultCommitmentFormat) val commitTxNumber = 0x404142434445L val commitTx = { - val txInfo = makeCommitTx(commitInput, commitTxNumber, localPaymentPriv.publicKey, remotePaymentPriv.publicKey, localIsFunder = true, outputs) + val txInfo = makeCommitTx(commitInput, commitTxNumber, localPaymentPriv.publicKey, remotePaymentPriv.publicKey, localIsInitiator = true, outputs) val localSig = Transactions.sign(txInfo, localPaymentPriv, TxOwner.Local, DefaultCommitmentFormat) val remoteSig = Transactions.sign(txInfo, remotePaymentPriv, TxOwner.Remote, DefaultCommitmentFormat) Transactions.addSigs(txInfo, localFundingPriv.publicKey, remoteFundingPriv.publicKey, localSig, remoteSig) } { - assert(getCommitTxNumber(commitTx.tx, isFunder = true, localPaymentPriv.publicKey, remotePaymentPriv.publicKey) == commitTxNumber) + assert(getCommitTxNumber(commitTx.tx, isInitiator = true, localPaymentPriv.publicKey, remotePaymentPriv.publicKey) == commitTxNumber) val hash = Crypto.sha256(localPaymentPriv.publicKey.value ++ remotePaymentPriv.publicKey.value) val num = Protocol.uint64(hash.takeRight(8).toArray, ByteOrder.BIG_ENDIAN) & 0xffffffffffffL val check = ((commitTx.tx.txIn.head.sequence & 0xffffff) << 24) | (commitTx.tx.lockTime & 0xffffff) @@ -440,7 +440,7 @@ class TransactionsSpec extends AnyFunSuite with Logging { val belowDustWithFeeAndAnchors = (localDustLimit + commitFeeAndAnchorCost * 0.9).toMilliSatoshi { - val outputs = makeCommitTxOutputs(localIsFunder = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, spec, UnsafeLegacyAnchorOutputsCommitmentFormat) + val outputs = makeCommitTxOutputs(localIsInitiator = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, spec, UnsafeLegacyAnchorOutputsCommitmentFormat) assert(outputs.map(_.commitmentOutput).toSet === Set(CommitmentOutput.ToLocal, CommitmentOutput.ToRemote, CommitmentOutput.ToLocalAnchor, CommitmentOutput.ToRemoteAnchor)) assert(outputs.find(_.commitmentOutput == CommitmentOutput.ToLocalAnchor).get.output.amount === anchorAmount) assert(outputs.find(_.commitmentOutput == CommitmentOutput.ToRemoteAnchor).get.output.amount === anchorAmount) @@ -449,35 +449,35 @@ class TransactionsSpec extends AnyFunSuite with Logging { } { val toRemoteFundeeBelowDust = spec.copy(toRemote = belowDust) - val outputs = makeCommitTxOutputs(localIsFunder = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, toRemoteFundeeBelowDust, UnsafeLegacyAnchorOutputsCommitmentFormat) + val outputs = makeCommitTxOutputs(localIsInitiator = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, toRemoteFundeeBelowDust, UnsafeLegacyAnchorOutputsCommitmentFormat) assert(outputs.map(_.commitmentOutput).toSet === Set(CommitmentOutput.ToLocal, CommitmentOutput.ToLocalAnchor)) assert(outputs.find(_.commitmentOutput == CommitmentOutput.ToLocalAnchor).get.output.amount === anchorAmount) assert(outputs.find(_.commitmentOutput == CommitmentOutput.ToLocal).get.output.amount.toMilliSatoshi === spec.toLocal - commitFeeAndAnchorCost) } { val toLocalFunderBelowDust = spec.copy(toLocal = belowDustWithFeeAndAnchors) - val outputs = makeCommitTxOutputs(localIsFunder = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, toLocalFunderBelowDust, UnsafeLegacyAnchorOutputsCommitmentFormat) + val outputs = makeCommitTxOutputs(localIsInitiator = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, toLocalFunderBelowDust, UnsafeLegacyAnchorOutputsCommitmentFormat) assert(outputs.map(_.commitmentOutput).toSet === Set(CommitmentOutput.ToRemote, CommitmentOutput.ToRemoteAnchor)) assert(outputs.find(_.commitmentOutput == CommitmentOutput.ToRemoteAnchor).get.output.amount === anchorAmount) assert(outputs.find(_.commitmentOutput == CommitmentOutput.ToRemote).get.output.amount.toMilliSatoshi === spec.toRemote) } { val toRemoteFunderBelowDust = spec.copy(toRemote = belowDustWithFeeAndAnchors) - val outputs = makeCommitTxOutputs(localIsFunder = false, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, toRemoteFunderBelowDust, UnsafeLegacyAnchorOutputsCommitmentFormat) + val outputs = makeCommitTxOutputs(localIsInitiator = false, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, toRemoteFunderBelowDust, UnsafeLegacyAnchorOutputsCommitmentFormat) assert(outputs.map(_.commitmentOutput).toSet === Set(CommitmentOutput.ToLocal, CommitmentOutput.ToLocalAnchor)) assert(outputs.find(_.commitmentOutput == CommitmentOutput.ToLocalAnchor).get.output.amount === anchorAmount) assert(outputs.find(_.commitmentOutput == CommitmentOutput.ToLocal).get.output.amount.toMilliSatoshi === spec.toLocal) } { val toLocalFundeeBelowDust = spec.copy(toLocal = belowDust) - val outputs = makeCommitTxOutputs(localIsFunder = false, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, toLocalFundeeBelowDust, UnsafeLegacyAnchorOutputsCommitmentFormat) + val outputs = makeCommitTxOutputs(localIsInitiator = false, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, toLocalFundeeBelowDust, UnsafeLegacyAnchorOutputsCommitmentFormat) assert(outputs.map(_.commitmentOutput).toSet === Set(CommitmentOutput.ToRemote, CommitmentOutput.ToRemoteAnchor)) assert(outputs.find(_.commitmentOutput == CommitmentOutput.ToRemoteAnchor).get.output.amount === anchorAmount) assert(outputs.find(_.commitmentOutput == CommitmentOutput.ToRemote).get.output.amount.toMilliSatoshi === spec.toRemote - commitFeeAndAnchorCost) } { val allBelowDust = spec.copy(toLocal = belowDust, toRemote = belowDust) - val outputs = makeCommitTxOutputs(localIsFunder = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, allBelowDust, UnsafeLegacyAnchorOutputsCommitmentFormat) + val outputs = makeCommitTxOutputs(localIsInitiator = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, allBelowDust, UnsafeLegacyAnchorOutputsCommitmentFormat) assert(outputs.isEmpty) } } @@ -521,8 +521,8 @@ class TransactionsSpec extends AnyFunSuite with Logging { val (commitTx, commitTxOutputs, htlcTimeoutTxs, htlcSuccessTxs) = { val commitTxNumber = 0x404142434445L - val outputs = makeCommitTxOutputs(localIsFunder = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, spec, UnsafeLegacyAnchorOutputsCommitmentFormat) - val txInfo = makeCommitTx(commitInput, commitTxNumber, localPaymentPriv.publicKey, remotePaymentPriv.publicKey, localIsFunder = true, outputs) + val outputs = makeCommitTxOutputs(localIsInitiator = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, spec, UnsafeLegacyAnchorOutputsCommitmentFormat) + val txInfo = makeCommitTx(commitInput, commitTxNumber, localPaymentPriv.publicKey, remotePaymentPriv.publicKey, localIsInitiator = true, outputs) val localSig = Transactions.sign(txInfo, localPaymentPriv, TxOwner.Local, UnsafeLegacyAnchorOutputsCommitmentFormat) val remoteSig = Transactions.sign(txInfo, remotePaymentPriv, TxOwner.Remote, UnsafeLegacyAnchorOutputsCommitmentFormat) val commitTx = Transactions.addSigs(txInfo, localFundingPriv.publicKey, remoteFundingPriv.publicKey, localSig, remoteSig) @@ -538,8 +538,8 @@ class TransactionsSpec extends AnyFunSuite with Logging { assert(htlcSuccessTxs.size == 3) // htlc2a, htlc2b and htlc4 assert(htlcSuccessTxs.map(_.htlcId).toSet === Set(1, 2, 4)) - val zeroFeeOutputs = makeCommitTxOutputs(localIsFunder = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, spec, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) - val zeroFeeCommitTx = makeCommitTx(commitInput, commitTxNumber, localPaymentPriv.publicKey, remotePaymentPriv.publicKey, localIsFunder = true, zeroFeeOutputs) + val zeroFeeOutputs = makeCommitTxOutputs(localIsInitiator = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, spec, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) + val zeroFeeCommitTx = makeCommitTx(commitInput, commitTxNumber, localPaymentPriv.publicKey, remotePaymentPriv.publicKey, localIsInitiator = true, zeroFeeOutputs) val zeroFeeHtlcTxs = makeHtlcTxs(zeroFeeCommitTx.tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, spec.htlcTxFeerate(ZeroFeeHtlcTxAnchorOutputsCommitmentFormat), zeroFeeOutputs, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) assert(zeroFeeHtlcTxs.length === 7) val zeroFeeConfirmationTargets = zeroFeeHtlcTxs.map(tx => tx.htlcId -> tx.confirmBefore.toLong).toMap @@ -776,8 +776,8 @@ class TransactionsSpec extends AnyFunSuite with Logging { val commitTxNumber = 0x404142434446L val (commitTx, outputs, htlcTxs) = { - val outputs = makeCommitTxOutputs(localIsFunder = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, spec, DefaultCommitmentFormat) - val txInfo = makeCommitTx(commitInput, commitTxNumber, localPaymentPriv.publicKey, remotePaymentPriv.publicKey, localIsFunder = true, outputs) + val outputs = makeCommitTxOutputs(localIsInitiator = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, spec, DefaultCommitmentFormat) + val txInfo = makeCommitTx(commitInput, commitTxNumber, localPaymentPriv.publicKey, remotePaymentPriv.publicKey, localIsInitiator = true, outputs) val localSig = Transactions.sign(txInfo, localPaymentPriv, TxOwner.Local, DefaultCommitmentFormat) val remoteSig = Transactions.sign(txInfo, remotePaymentPriv, TxOwner.Remote, DefaultCommitmentFormat) val commitTx = Transactions.addSigs(txInfo, localFundingPriv.publicKey, remoteFundingPriv.publicKey, localSig, remoteSig) @@ -815,7 +815,7 @@ class TransactionsSpec extends AnyFunSuite with Logging { { // Different amounts, both outputs untrimmed, local is funder: val spec = CommitmentSpec(Set.empty, feeratePerKw, 150_000_000 msat, 250_000_000 msat) - val closingTx = makeClosingTx(commitInput, localPubKeyScript, remotePubKeyScript, localIsFunder = true, localDustLimit, 1000 sat, spec) + val closingTx = makeClosingTx(commitInput, localPubKeyScript, remotePubKeyScript, localIsInitiator = true, localDustLimit, 1000 sat, spec) assert(closingTx.tx.txOut.length === 2) assert(closingTx.toLocalOutput !== None) val toLocal = closingTx.toLocalOutput.get @@ -827,7 +827,7 @@ class TransactionsSpec extends AnyFunSuite with Logging { { // Same amounts, both outputs untrimmed, local is fundee: val spec = CommitmentSpec(Set.empty, feeratePerKw, 150_000_000 msat, 150_000_000 msat) - val closingTx = makeClosingTx(commitInput, localPubKeyScript, remotePubKeyScript, localIsFunder = false, localDustLimit, 1000 sat, spec) + val closingTx = makeClosingTx(commitInput, localPubKeyScript, remotePubKeyScript, localIsInitiator = false, localDustLimit, 1000 sat, spec) assert(closingTx.tx.txOut.length === 2) assert(closingTx.toLocalOutput !== None) val toLocal = closingTx.toLocalOutput.get @@ -839,7 +839,7 @@ class TransactionsSpec extends AnyFunSuite with Logging { { // Their output is trimmed: val spec = CommitmentSpec(Set.empty, feeratePerKw, 150_000_000 msat, 1_000 msat) - val closingTx = makeClosingTx(commitInput, localPubKeyScript, remotePubKeyScript, localIsFunder = false, localDustLimit, 1000 sat, spec) + val closingTx = makeClosingTx(commitInput, localPubKeyScript, remotePubKeyScript, localIsInitiator = false, localDustLimit, 1000 sat, spec) assert(closingTx.tx.txOut.length === 1) assert(closingTx.toLocalOutput !== None) val toLocal = closingTx.toLocalOutput.get @@ -850,14 +850,14 @@ class TransactionsSpec extends AnyFunSuite with Logging { { // Our output is trimmed: val spec = CommitmentSpec(Set.empty, feeratePerKw, 50_000 msat, 150_000_000 msat) - val closingTx = makeClosingTx(commitInput, localPubKeyScript, remotePubKeyScript, localIsFunder = true, localDustLimit, 1000 sat, spec) + val closingTx = makeClosingTx(commitInput, localPubKeyScript, remotePubKeyScript, localIsInitiator = true, localDustLimit, 1000 sat, spec) assert(closingTx.tx.txOut.length === 1) assert(closingTx.toLocalOutput === None) } { // Both outputs are trimmed: val spec = CommitmentSpec(Set.empty, feeratePerKw, 50_000 msat, 10_000 msat) - val closingTx = makeClosingTx(commitInput, localPubKeyScript, remotePubKeyScript, localIsFunder = true, localDustLimit, 1000 sat, spec) + val closingTx = makeClosingTx(commitInput, localPubKeyScript, remotePubKeyScript, localIsInitiator = true, localDustLimit, 1000 sat, spec) assert(closingTx.tx.txOut.isEmpty) assert(closingTx.toLocalOutput === None) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/ChannelCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/ChannelCodecsSpec.scala index 5e2259cbf2..4ea55a1aaa 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/ChannelCodecsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/ChannelCodecsSpec.scala @@ -135,11 +135,11 @@ class ChannelCodecsSpec extends AnyFunSuite { // this test makes sure that we actually produce the same objects than previous versions of eclair val refs = Map( hex"00000303933884AAF1D6B108397E5EFE5C86BCF2D8CA8D2F700EDA99DB9214FC2712B134000456E4167E3C0EB8C856C79CA31C97C0AA0000000000000222000000012A05F2000000000000028F5C000000000000000102D0001E000BD48A2402E80B723C42EE3E42938866EC6686ABB7ABF64380000000C501A7F2974C5074E9E10DBB3F0D9B8C40932EC63ABC610FAD7EB6B21C6D081A459B000000000000011E80000001EEFFFE5C00000000000147AE00000000000001F403F000F18146779F781067ED04B4957E14F1C5623AB653039B2B1D49910240848E4E682DB20131AD64F76FAF90CD7DE26892F1BDAB82FB9E02EF6538D82FF4204B5348F02AE081A5388E9474769D69C4F60A763AE0CCDB5228A06281DE64408871A927297FDFD8818B6383985ABD4F0AC22E73791CF3A4D63C592FA2648242D34B8334B1539E823381BB1F1404C37D9C2318F5FC6B1BF7ECF5E6835B779E3BE09BADCF6DF1F51DCFBC80000000C0808000000000000EFD80000000007F00000000061A0A4880000001EDE5F3C3801203B9C79160B5123CADE575E370480B57B0C816EB35DD7B27372EDA858622EB1E808000000015FFFFFF800000000011001029DFB814F6502A68D6F83B6049E3D2948A2080084083750626532FDB437169C20023A9108146779F781067ED04B4957E14F1C5623AB653039B2B1D49910240848E4E682DB21081B30694071254D8B3B9537320C014B8CB1052E5514F5EFC19CF2EB806308D5CF1A95700AD0100000000008083B9C79160B5123CADE575E370480B57B0C816EB35DD7B27372EDA858622EB1E80800000001961B4C001618F8180000000001100102E648BA30998A28C02C2DFD9DDCD0E0BA064DA199C55186485AFAB296B94E704426FFE00000000000B000A67D9B9FAADB91650E0146B1F742E5C16006708890200239822011026A6925C659D006FEB42D639F1E42DD13224EE49AA34E71B612CF96DB66A8CD4011032C22F653C54CC5E41098227427650644266D80DED45B7387AE0FFC10E529C4680A418228110807CB47D9C1A14CB832FB361C398EA672C9542F34A90BAD4288FA6AC5FC9E9845C01101CF71CAE9252D389135D8C606225DCF1E0333CCDF1FAE84B74FC5D3D440C25F880A3A9108146779F781067ED04B4957E14F1C5623AB653039B2B1D49910240848E4E682DB21081B30694071254D8B3B9537320C014B8CB1052E5514F5EFC19CF2EB806308D5CF1A9573D7C531000000000000000000F3180000000007F00000001EDE5F3C380000000061A0A48D64CA627B243AD5915A2E5D0BAD026762028DDF3304992B83A26D6C11735FC5F01ED56D769BDE7F6A068AF1A4BCFDF950321F3A4744B01B1DDC7498677F112AE1A80000000000000000000000000000000000000658000000000000819800040D37301C10C9419287E9A3B704EB6D7F45CC145DD77DCE8A63B0A47C8AB67467D800901DCE3C8B05A891E56F2BAF1B82405ABD8640B759AEEBD939B976D42C311758F40400000000AFFFFFFC00000000008800814EFDC0A7B2815346B7C1DB024F1E94A451040042041BA83132997EDA1B8B4E10011D48840A33BCFBC0833F6825A4ABF0A78E2B11D5B2981CD958EA4C881204247273416D90840D9834A03892A6C59DCA9B990600A5C65882972A8A7AF7E0CE7975C031846AE78D4AB8002000EC0003FFFFFFFF86801076D98A575A4CDFD0E3F44D1BB3CD3BBAF3BD04C38FED439ED90D88DF932A9296801A80007FFFFFFFF4008136A9D5896669E8724C5120FB6B36C241EF3CEF68AE0316161F04A9EE3EAFF36000FC0003FFFFFFFF86780106E4B5CC4155733A2427082907338051A5DA1E7CA6432840A5528ECAFFA3FB628801B80007FFFFFFFF10020CA4E125E9126107745D4354D4187ABCDE323117857A1DCEB7CCF60B2AAFA80C6003A0000FFFFFFFFE1C0080981575FD981A73A848CC0243CB467BF451F6811DAF4D71CAD8CE8B1E96DB190C01000003FFFFFFFF867400814C747E0FD8290BE8A3B8B3F73015A261479A71780CD3A0A9270234E4B394409C00D80003FFFFFFFF90020E1B9C9B10A97F15F5E1BB27FC8AC670DF8DADEAE4EDFAFB23BDD0AC705FDF51600340000FFFFFFFFF0020AD2581F3494A17B0BE3F63516D53F028A204FD3156D8B21AA4E57A8738D2062080007FFFFFFFF0CE83B9C79160B5123CADE575E370480B57B0C816EB35DD7B27372EDA858622EB1E0B8C1E00000B8000FA46CC2C7E9AB4A37C64216CD65C944E6D73998419D1A1AD2827AB6BC85B32280230764E374064EC82A3751E789607E23BEAE93FB0EDDD5E7FA803767079662E80EAEF384E2AFCB68049D9DC246119E77BD2ED4112330760CAB6CD3671CFCE006C584B9C95E0B554261E00154D40806EA694F44751B328A9291BAD124EFD5664280936EC92D27B242737E7E3E83B4704BA367B7DA5108F2F6EDFB1C38EE721A369E77EED71B12090BAEAAAC322C1457E31AB0C4DE5D9351943F10FD747742616A1AABD09F680B37D4105A8872695EE9B97FAB8985FAA9D747D45046229BF265CEEB300A40FE23040C5F335E0515496C58EE47418B72331FCC6F47A31A9B33B8E000008692FFAFF04D2AE211E9461FB39D875D74F32E4109D21D5A03D46612000000002E307800002E0002069FCA5D3141D3A78436ECFC366E31024CBB18EAF1843EB5FADAC871B42069166C0726710955E3AD621072FCBDFCB90D79E5B1951A5EE01DB533B72429F84E2562680519DE7DE0419FB412D255F853C71588EAD94C0E6CAC7526440902123939A0B6C806CC1A501C495362CEE54DCC830052E32C414B95453D7BF0673CBAE018C23573C69C694A8F88483050257A7366B838489731E5776B6FA0F02573401176D3E7FAEEF11E95A671420586631255F51A0EC2CF4D4D9F69D587712070FE1FB9316B71868692FFAFF04D2AE211E9461FB39D875D74F32E4109D21D5A03D46612000000002E307800002E0002BA11BBBA0202012000000000000007D0000007D0000000C800000007CFFFF83000" - -> """{"type":"DATA_NORMAL","commitments":{"channelId":"07738f22c16a24795bcaebc6e09016af61902dd66bbaf64e6e5db50b0c45d63c","channelConfig":[],"channelFeatures":[],"localParams":{"nodeId":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","fundingKeyPath":{"path":[1457788542,1007597768,1455922339,479707306]},"dustLimit":546,"maxHtlcValueInFlightMsat":5000000000,"channelReserve":167772,"htlcMinimum":1,"toSelfDelay":720,"maxAcceptedHtlcs":30,"isFunder":false,"defaultFinalScriptPubKey":"a9144805d016e47885dc7c852710cdd8cd0d576f57ec87","initFeatures":{"activated":{"option_data_loss_protect":"optional","initial_routing_sync":"optional","gossip_queries":"optional"},"unknown":[]}},"remoteParams":{"nodeId":"034fe52e98a0e9d3c21b767e1b371881265d8c7578c21f5afd6d6438da10348b36","dustLimit":573,"maxHtlcValueInFlightMsat":16609443000,"channelReserve":167772,"htlcMinimum":1000,"toSelfDelay":2016,"maxAcceptedHtlcs":483,"fundingPubKey":"028cef3ef020cfda09692afc29e38ac4756ca60736563a93220481091c9cd05b64","revocationBasepoint":"02635ac9eedf5f219afbc4d125e37b5705f73c05deca71b05fe84096a691e055c1","paymentBasepoint":"034a711d28e8ed3ad389ec14ec75c199b6a45140c503bcc88110e3524e52ffbfb1","delayedPaymentBasepoint":"0316c70730b57a9e15845ce6f239e749ac78b25f44c90485a697066962a73d0467","htlcBasepoint":"03763e280986fb384631ebf8d637efd9ebcd06b6ef3c77c1375b9edbe3ea3b9f79","initFeatures":{"activated":{"option_data_loss_protect":"mandatory","gossip_queries":"optional"},"unknown":[]}},"channelFlags":{"announceChannel":true},"localCommit":{"index":7675,"spec":{"htlcs":[],"commitTxFeerate":254,"toLocal":204739729,"toRemote":16572475271},"commitTxAndRemoteSig":{"commitTx":{"txid":"e25a866b79212015e01e155e530fb547abc8276869f8740a9948e52ca231f1e4","tx":"020000000107738f22c16a24795bcaebc6e09016af61902dd66bbaf64e6e5db50b0c45d63d010000000032c3698002c31f0300000000002200205cc91746133145180585bfb3bb9a1c1740c9b43338aa30c90b5f5652d729ce0884dffc0000000000160014cfb373f55b722ca1c028d63ee85cb82c00ce11127af8a620"},"remoteSig":"4d4d24b8cb3a00dfd685ac73e3c85ba26449dc935469ce36c259f2db6cd519a865845eca78a998bc8213044e84eca0c884cdb01bda8b6e70f5c1ff821ca5388d"},"htlcTxsAndRemoteSigs":[]},"remoteCommit":{"index":7779,"spec":{"htlcs":[],"commitTxFeerate":254,"toLocal":16572475271,"toRemote":204739729},"txid":"ac994c4f64875ab22b45cba175a04cec4051bbe660932570744dad822e6bf8be","remotePerCommitmentPoint":"03daadaed37bcfed40d15e34979fbf2a0643e748e8960363bb8e930cefe2255c35"},"localChanges":{"proposed":[],"signed":[],"acked":[]},"remoteChanges":{"proposed":[],"acked":[],"signed":[]},"localNextHtlcId":203,"remoteNextHtlcId":4147,"originChannels":{},"remoteNextCommitInfo":"034dcc0704325064a1fa68edc13adb5fd173051775df73a298ec291f22ad9d19f6","commitInput":{"outPoint":"3dd6450c0bb55d6e4ef6ba6bd62d9061af1690e0c6ebca5b79246ac1228f7307:1","amountSatoshis":16777215},"remotePerCommitmentSecrets":null},"shortChannelId":"1513532x23x1","buried":true,"channelAnnouncement":{"nodeSignature1":"d2366163f4d5a51be3210b66b2e4a2736b9ccc20ce8d0d69413d5b5e42d991401183b271ba032764151ba8f3c4b03f11df5749fd876eeaf3fd401bb383cb3174","nodeSignature2":"075779c27157e5b4024ecee12308cf3bde976a0891983b0655b669b38e7e700362c25ce4af05aaa130f000aa6a04037534a7a23a8d99454948dd689277eab321","bitcoinSignature1":"4049b7649693d92139bf3f1f41da3825d1b3dbed2884797b76fd8e1c77390d1b4f3bf76b8d890485d7555619160a2bf18d58626f2ec9a8ca1f887eba3ba130b5","bitcoinSignature2":"0d55e84fb4059bea082d443934af74dcbfd5c4c2fd54eba3ea2823114df932e7759805207f1182062f99af028aa4b62c7723a0c5b9198fe637a3d18d4d99dc70","features":{"activated":{},"unknown":[]},"chainHash":"43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000","shortChannelId":"1513532x23x1","nodeId1":"034fe52e98a0e9d3c21b767e1b371881265d8c7578c21f5afd6d6438da10348b36","nodeId2":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","bitcoinKey1":"028cef3ef020cfda09692afc29e38ac4756ca60736563a93220481091c9cd05b64","bitcoinKey2":"03660d280e24a9b16772a6e6418029719620a5caa29ebdf8339e5d700c611ab9e3","tlvStream":{"records":[],"unknown":[]}},"channelUpdate":{"signature":"4e34a547c424182812bd39b35c1c244b98f2bbb5b7d07812b9a008bb69f3fd77788f4ad338a102c331892afa8d076167a6a6cfb4eac3b890387f0fdc98b5b8c3","chainHash":"43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000","shortChannelId":"1513532x23x1","timestamp":{"iso":"2019-06-18T12:49:33Z","unix":1560862173},"channelFlags":{"isEnabled":true,"isNode1":false},"cltvExpiryDelta":144,"htlcMinimumMsat":1000,"feeBaseMsat":1000,"feeProportionalMillionths":100,"htlcMaximumMsat":16777215000,"tlvStream":{"records":[],"unknown":[]}}}""", + -> """{"type":"DATA_NORMAL","commitments":{"channelId":"07738f22c16a24795bcaebc6e09016af61902dd66bbaf64e6e5db50b0c45d63c","channelConfig":[],"channelFeatures":[],"localParams":{"nodeId":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","fundingKeyPath":{"path":[1457788542,1007597768,1455922339,479707306]},"dustLimit":546,"maxHtlcValueInFlightMsat":5000000000,"channelReserve":167772,"htlcMinimum":1,"toSelfDelay":720,"maxAcceptedHtlcs":30,"isInitiator":false,"defaultFinalScriptPubKey":"a9144805d016e47885dc7c852710cdd8cd0d576f57ec87","initFeatures":{"activated":{"option_data_loss_protect":"optional","initial_routing_sync":"optional","gossip_queries":"optional"},"unknown":[]}},"remoteParams":{"nodeId":"034fe52e98a0e9d3c21b767e1b371881265d8c7578c21f5afd6d6438da10348b36","dustLimit":573,"maxHtlcValueInFlightMsat":16609443000,"channelReserve":167772,"htlcMinimum":1000,"toSelfDelay":2016,"maxAcceptedHtlcs":483,"fundingPubKey":"028cef3ef020cfda09692afc29e38ac4756ca60736563a93220481091c9cd05b64","revocationBasepoint":"02635ac9eedf5f219afbc4d125e37b5705f73c05deca71b05fe84096a691e055c1","paymentBasepoint":"034a711d28e8ed3ad389ec14ec75c199b6a45140c503bcc88110e3524e52ffbfb1","delayedPaymentBasepoint":"0316c70730b57a9e15845ce6f239e749ac78b25f44c90485a697066962a73d0467","htlcBasepoint":"03763e280986fb384631ebf8d637efd9ebcd06b6ef3c77c1375b9edbe3ea3b9f79","initFeatures":{"activated":{"option_data_loss_protect":"mandatory","gossip_queries":"optional"},"unknown":[]}},"channelFlags":{"announceChannel":true},"localCommit":{"index":7675,"spec":{"htlcs":[],"commitTxFeerate":254,"toLocal":204739729,"toRemote":16572475271},"commitTxAndRemoteSig":{"commitTx":{"txid":"e25a866b79212015e01e155e530fb547abc8276869f8740a9948e52ca231f1e4","tx":"020000000107738f22c16a24795bcaebc6e09016af61902dd66bbaf64e6e5db50b0c45d63d010000000032c3698002c31f0300000000002200205cc91746133145180585bfb3bb9a1c1740c9b43338aa30c90b5f5652d729ce0884dffc0000000000160014cfb373f55b722ca1c028d63ee85cb82c00ce11127af8a620"},"remoteSig":"4d4d24b8cb3a00dfd685ac73e3c85ba26449dc935469ce36c259f2db6cd519a865845eca78a998bc8213044e84eca0c884cdb01bda8b6e70f5c1ff821ca5388d"},"htlcTxsAndRemoteSigs":[]},"remoteCommit":{"index":7779,"spec":{"htlcs":[],"commitTxFeerate":254,"toLocal":16572475271,"toRemote":204739729},"txid":"ac994c4f64875ab22b45cba175a04cec4051bbe660932570744dad822e6bf8be","remotePerCommitmentPoint":"03daadaed37bcfed40d15e34979fbf2a0643e748e8960363bb8e930cefe2255c35"},"localChanges":{"proposed":[],"signed":[],"acked":[]},"remoteChanges":{"proposed":[],"acked":[],"signed":[]},"localNextHtlcId":203,"remoteNextHtlcId":4147,"originChannels":{},"remoteNextCommitInfo":"034dcc0704325064a1fa68edc13adb5fd173051775df73a298ec291f22ad9d19f6","commitInput":{"outPoint":"3dd6450c0bb55d6e4ef6ba6bd62d9061af1690e0c6ebca5b79246ac1228f7307:1","amountSatoshis":16777215},"remotePerCommitmentSecrets":null},"shortChannelId":"1513532x23x1","buried":true,"channelAnnouncement":{"nodeSignature1":"d2366163f4d5a51be3210b66b2e4a2736b9ccc20ce8d0d69413d5b5e42d991401183b271ba032764151ba8f3c4b03f11df5749fd876eeaf3fd401bb383cb3174","nodeSignature2":"075779c27157e5b4024ecee12308cf3bde976a0891983b0655b669b38e7e700362c25ce4af05aaa130f000aa6a04037534a7a23a8d99454948dd689277eab321","bitcoinSignature1":"4049b7649693d92139bf3f1f41da3825d1b3dbed2884797b76fd8e1c77390d1b4f3bf76b8d890485d7555619160a2bf18d58626f2ec9a8ca1f887eba3ba130b5","bitcoinSignature2":"0d55e84fb4059bea082d443934af74dcbfd5c4c2fd54eba3ea2823114df932e7759805207f1182062f99af028aa4b62c7723a0c5b9198fe637a3d18d4d99dc70","features":{"activated":{},"unknown":[]},"chainHash":"43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000","shortChannelId":"1513532x23x1","nodeId1":"034fe52e98a0e9d3c21b767e1b371881265d8c7578c21f5afd6d6438da10348b36","nodeId2":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","bitcoinKey1":"028cef3ef020cfda09692afc29e38ac4756ca60736563a93220481091c9cd05b64","bitcoinKey2":"03660d280e24a9b16772a6e6418029719620a5caa29ebdf8339e5d700c611ab9e3","tlvStream":{"records":[],"unknown":[]}},"channelUpdate":{"signature":"4e34a547c424182812bd39b35c1c244b98f2bbb5b7d07812b9a008bb69f3fd77788f4ad338a102c331892afa8d076167a6a6cfb4eac3b890387f0fdc98b5b8c3","chainHash":"43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000","shortChannelId":"1513532x23x1","timestamp":{"iso":"2019-06-18T12:49:33Z","unix":1560862173},"channelFlags":{"isEnabled":true,"isNode1":false},"cltvExpiryDelta":144,"htlcMinimumMsat":1000,"feeBaseMsat":1000,"feeProportionalMillionths":100,"htlcMaximumMsat":16777215000,"tlvStream":{"records":[],"unknown":[]}}}""", hex"00000303933884AAF1D6B108397E5EFE5C86BCF2D8CA8D2F700EDA99DB9214FC2712B1340004D443ECE9D9C43A11A19B554BAAA6AD150000000000000222000000003B9ACA0000000000000249F000000000000000010090001E800BD48A22F4C80A42CC8BB29A764DBAEFC95674931FBE9A4380000000C50134D4A745996002F219B5FDBA1E045374DF589ECA06ABE23CECAE47343E65EDCF800000000000011E80000001BA90824000000000000124F800000000000001F4038500F1810AE1AF8A1D6F56F80855E26705F191BB07CD4E2434BC5BB1698E7E5880E2266201E8BFEEEEED725775B8116F6F82CF8E87835A5B45B184E56F272AD70D6078118601E06212B8C8F2E25B73EE7974FDCDF007E389B437BBFE238CCC3F3BF7121B6C5E81AA8589D21E9584B24A11F3ABBA5DAD48D121DD63C57A69CD767119C05DA159CB81A649D8CC0E136EB8DFBD2268B69DCA86F8CE4A604235A03D9D37AE7B07FC563F80000000C080800000000000271C000000000177000000002808B14600000001970039BA00123767F0F4F00D5E9FDF24177EF2872343D9F8FAEC65D3048BA575E70E00A0AB08800000000015E070F20000000000110010584241B5FB364208F6E64A80D1166DAD866186B10C015ED0283FF1C308C2105A0023A910810AE1AF8A1D6F56F80855E26705F191BB07CD4E2434BC5BB1698E7E5880E226621081DE8ADFA110DC8A94D8B9E9EF616BAE8598287C8F82AFDF0FC068697D570266FDA95700AD81000000000080B767F0F4F00D5E9FDF24177EF2872343D9F8FAEC65D3048BA575E70E00A0AB0880000000003E7AEDC0011ABE8A00000000001100101A9CE4B6AEF469590BC7BCC51DCEEAE9C86084055A63CC01E443C733FBE400B9B5B16800000000000B000A5E5700106D1A7097E4DE87EBAF1F8F2773842FA482002418228110805E84989A81F51ABD9D11889AE43E68FAD93659DEC019F1B8C0ADBF15A57B118B81101DCC1256F9306439AD3962C043FC47A5179CAAA001CCB23342BE0E8D92E4022780A4182281108074F306DA3751B84EC5FFB155BDCA7B8E02208BBDBC8D4F3327ABA557BF27CD1701102EF4AC8CC92F469DA9642D4D4162BC545F8B34ADE15B7D6F99808AA22B086B0180A3A910810AE1AF8A1D6F56F80855E26705F191BB07CD4E2434BC5BB1698E7E5880E226621081DE8ADFA110DC8A94D8B9E9EF616BAE8598287C8F82AFDF0FC068697D570266FDA9576F8099900000000000000000271C00000000017700000001970039BA000000002808B14648CE00AE97051EE10A3C361263F81A98165CE4AA7BA076933D4266E533585F24815C15DEACF0691332B38ECF23EC39982C5C978C748374A01BA9B30D501EE4F26E8000000000000000000000000000000000001224000000000000004B800040A911C460F1467952E3B99BED072F81BFB4454FF389636DCB399FE6A78113C28580091BB3F87A7806AF4FEF920BBF794391A1ECFC7D7632E98245D2BAF3870050558440000000000AF0387900000000000880082C2120DAFD9B21047B732540688B36D6C330C3588600AF68141FF8E18461082D0011D488408570D7C50EB7AB7C042AF13382F8C8DD83E6A7121A5E2DD8B4C73F2C407113310840EF456FD0886E454A6C5CF4F7B0B5D742CC143E47C157EF87E03434BEAB81337ED4AB8001C00F40003FFFFFFFEC7200403248A1D44DFA3AC9EC237D452C936400CAA86E9517CCCF2A8F77B7493CD70B6A00780001FFFFFFFF63A0041826829646B907A97FBD1455EA8673A12B8E7AA6EA790F7802E955CE3B69DE57E006E0001FFFFFFFF640081E51EB1F91218821E680B50E4B22DF8B094385BD33ACAE36BFC9E8C2F5AD2DA5400EC0003FFFFFFFEC7801047C26AD5435658D063EBCF73A5D0EEFE73ED6B73426246E8DFB3A21D1C4C7465001900007FFFFFFFE0040B115AC58BAAA900195893EA3B2AB408D2AD348AD047E3B6CB15E599625E38608006A0001FFFFFFFF7002033C39A21A38BB61F6FB33623771A9356D8885B7C12C939C770C939EF826286C200360000FFFFFFFFB4008104EF4271064A0973B053727C3E67352D00E25CAEED944F50782449CEAE8F50960001FFFFFFFF6390DD9FC3D3C0357A7F7C905DFBCA1C8D0F67E3EBB1974C122E95D79C380282AC222B21FA0007920001295AA1FB77029F7620A90EF7AE6A6CD31E4588B93264A7ADB76152D535C52E90B9E1B7C2376DABA316A6290F1A9730D4E5E44D0B1CB0EE6A795702E6A6BCDFCDA1A4BFEBFC134AB8847A5187ECE761D75D3CCB904274875680F51984800000000AC87E8001E480002E884D2A8080804800000000000001F4000001F40000003200000001BF08EB000" - -> """{"type":"DATA_NORMAL","commitments":{"channelId":"6ecfe1e9e01abd3fbe482efde50e4687b3f1f5d8cba609174aebce1c01415611","channelConfig":[],"channelFeatures":[],"localParams":{"nodeId":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","fundingKeyPath":{"path":[3561221353,3653515793,2711311691,2863050005]},"dustLimit":546,"maxHtlcValueInFlightMsat":1000000000,"channelReserve":150000,"htlcMinimum":1,"toSelfDelay":144,"maxAcceptedHtlcs":30,"isFunder":true,"defaultFinalScriptPubKey":"a91445e990148599176534ec9b75df92ace9263f7d3487","initFeatures":{"activated":{"option_data_loss_protect":"optional","initial_routing_sync":"optional","gossip_queries":"optional"},"unknown":[]}},"remoteParams":{"nodeId":"0269a94e8b32c005e4336bfb743c08a6e9beb13d940d57c479d95c8e687ccbdb9f","dustLimit":573,"maxHtlcValueInFlightMsat":14850000000,"channelReserve":150000,"htlcMinimum":1000,"toSelfDelay":1802,"maxAcceptedHtlcs":483,"fundingPubKey":"0215c35f143adeadf010abc4ce0be323760f9a9c486978b762d31cfcb101c44cc4","revocationBasepoint":"03d17fdddddae4aeeb7022dedf059f1d0f06b4b68b6309cade4e55ae1ac0f0230c","paymentBasepoint":"03c0c4257191e5c4b6e7dcf2e9fb9be00fc713686f77fc4719987e77ee2436d8bd","delayedPaymentBasepoint":"03550b13a43d2b09649423e75774bb5a91a243bac78af4d39aece23380bb42b397","htlcBasepoint":"034c93b1981c26dd71bf7a44d16d3b950df19c94c0846b407b3a6f5cf60ff8ac7f","initFeatures":{"activated":{"option_data_loss_protect":"mandatory","gossip_queries":"optional"},"unknown":[]}},"channelFlags":{"announceChannel":true},"localCommit":{"index":20024,"spec":{"htlcs":[],"commitTxFeerate":750,"toLocal":1343316620,"toRemote":13656683380},"commitTxAndRemoteSig":{"commitTx":{"txid":"65fe0b1f079fa763448df3ab8d94b1ad7d377c061121376be90b9c0c1bb0cd43","tx":"02000000016ecfe1e9e01abd3fbe482efde50e4687b3f1f5d8cba609174aebce1c0141561100000000007cf5db8002357d1400000000002200203539c96d5de8d2b2178f798a3b9dd5d390c1080ab4c79803c8878e67f7c801736b62d00000000000160014bcae0020da34e12fc9bd0fd75e3f1e4ee7085f49df013320"},"remoteSig":"bd09313503ea357b3a231135c87cd1f5b26cb3bd8033e371815b7e2b4af623173b9824adf260c8735a72c58087f88f4a2f39554003996466857c1d1b25c8044f"},"htlcTxsAndRemoteSigs":[]},"remoteCommit":{"index":20024,"spec":{"htlcs":[],"commitTxFeerate":750,"toLocal":13656683380,"toRemote":1343316620},"txid":"919c015d2e0a3dc214786c24c7f035302cb9c954f740ed267a84cdca66b0be49","remotePerCommitmentPoint":"02b82bbd59e0d22665671d9e47d8733058b92f18e906e9403753661aa03dc9e4dd"},"localChanges":{"proposed":[],"signed":[],"acked":[]},"remoteChanges":{"proposed":[],"acked":[],"signed":[]},"localNextHtlcId":9288,"remoteNextHtlcId":151,"originChannels":{},"remoteNextCommitInfo":"02a4471183c519e54b8ee66fb41cbe06fed1153fce258db72ce67f9a9e044f0a16","commitInput":{"outPoint":"115641011cceeb4a1709a6cbd8f5f1b387460ee5fd2e48be3fbd1ae0e9e1cf6e:0","amountSatoshis":15000000},"remotePerCommitmentSecrets":null},"shortChannelId":"1413373x969x0","buried":true,"channelUpdate":{"signature":"52b543f6ee053eec41521def5cd4d9a63c8b117264c94f5b6ec2a5aa6b8a5d2173c36f846edb57462d4c521e352e61a9cbc89a163961dcd4f2ae05cd4d79bf9b","chainHash":"43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000","shortChannelId":"1413373x969x0","timestamp":{"iso":"2019-06-24T09:39:33Z","unix":1561369173},"channelFlags":{"isEnabled":true,"isNode1":false},"cltvExpiryDelta":144,"htlcMinimumMsat":1000,"feeBaseMsat":1000,"feeProportionalMillionths":100,"htlcMaximumMsat":15000000000,"tlvStream":{"records":[],"unknown":[]}}}""", + -> """{"type":"DATA_NORMAL","commitments":{"channelId":"6ecfe1e9e01abd3fbe482efde50e4687b3f1f5d8cba609174aebce1c01415611","channelConfig":[],"channelFeatures":[],"localParams":{"nodeId":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","fundingKeyPath":{"path":[3561221353,3653515793,2711311691,2863050005]},"dustLimit":546,"maxHtlcValueInFlightMsat":1000000000,"channelReserve":150000,"htlcMinimum":1,"toSelfDelay":144,"maxAcceptedHtlcs":30,"isInitiator":true,"defaultFinalScriptPubKey":"a91445e990148599176534ec9b75df92ace9263f7d3487","initFeatures":{"activated":{"option_data_loss_protect":"optional","initial_routing_sync":"optional","gossip_queries":"optional"},"unknown":[]}},"remoteParams":{"nodeId":"0269a94e8b32c005e4336bfb743c08a6e9beb13d940d57c479d95c8e687ccbdb9f","dustLimit":573,"maxHtlcValueInFlightMsat":14850000000,"channelReserve":150000,"htlcMinimum":1000,"toSelfDelay":1802,"maxAcceptedHtlcs":483,"fundingPubKey":"0215c35f143adeadf010abc4ce0be323760f9a9c486978b762d31cfcb101c44cc4","revocationBasepoint":"03d17fdddddae4aeeb7022dedf059f1d0f06b4b68b6309cade4e55ae1ac0f0230c","paymentBasepoint":"03c0c4257191e5c4b6e7dcf2e9fb9be00fc713686f77fc4719987e77ee2436d8bd","delayedPaymentBasepoint":"03550b13a43d2b09649423e75774bb5a91a243bac78af4d39aece23380bb42b397","htlcBasepoint":"034c93b1981c26dd71bf7a44d16d3b950df19c94c0846b407b3a6f5cf60ff8ac7f","initFeatures":{"activated":{"option_data_loss_protect":"mandatory","gossip_queries":"optional"},"unknown":[]}},"channelFlags":{"announceChannel":true},"localCommit":{"index":20024,"spec":{"htlcs":[],"commitTxFeerate":750,"toLocal":1343316620,"toRemote":13656683380},"commitTxAndRemoteSig":{"commitTx":{"txid":"65fe0b1f079fa763448df3ab8d94b1ad7d377c061121376be90b9c0c1bb0cd43","tx":"02000000016ecfe1e9e01abd3fbe482efde50e4687b3f1f5d8cba609174aebce1c0141561100000000007cf5db8002357d1400000000002200203539c96d5de8d2b2178f798a3b9dd5d390c1080ab4c79803c8878e67f7c801736b62d00000000000160014bcae0020da34e12fc9bd0fd75e3f1e4ee7085f49df013320"},"remoteSig":"bd09313503ea357b3a231135c87cd1f5b26cb3bd8033e371815b7e2b4af623173b9824adf260c8735a72c58087f88f4a2f39554003996466857c1d1b25c8044f"},"htlcTxsAndRemoteSigs":[]},"remoteCommit":{"index":20024,"spec":{"htlcs":[],"commitTxFeerate":750,"toLocal":13656683380,"toRemote":1343316620},"txid":"919c015d2e0a3dc214786c24c7f035302cb9c954f740ed267a84cdca66b0be49","remotePerCommitmentPoint":"02b82bbd59e0d22665671d9e47d8733058b92f18e906e9403753661aa03dc9e4dd"},"localChanges":{"proposed":[],"signed":[],"acked":[]},"remoteChanges":{"proposed":[],"acked":[],"signed":[]},"localNextHtlcId":9288,"remoteNextHtlcId":151,"originChannels":{},"remoteNextCommitInfo":"02a4471183c519e54b8ee66fb41cbe06fed1153fce258db72ce67f9a9e044f0a16","commitInput":{"outPoint":"115641011cceeb4a1709a6cbd8f5f1b387460ee5fd2e48be3fbd1ae0e9e1cf6e:0","amountSatoshis":15000000},"remotePerCommitmentSecrets":null},"shortChannelId":"1413373x969x0","buried":true,"channelUpdate":{"signature":"52b543f6ee053eec41521def5cd4d9a63c8b117264c94f5b6ec2a5aa6b8a5d2173c36f846edb57462d4c521e352e61a9cbc89a163961dcd4f2ae05cd4d79bf9b","chainHash":"43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000","shortChannelId":"1413373x969x0","timestamp":{"iso":"2019-06-24T09:39:33Z","unix":1561369173},"channelFlags":{"isEnabled":true,"isNode1":false},"cltvExpiryDelta":144,"htlcMinimumMsat":1000,"feeBaseMsat":1000,"feeProportionalMillionths":100,"htlcMaximumMsat":15000000000,"tlvStream":{"records":[],"unknown":[]}}}""", hex"0200020000000303933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b13400098c4b989bbdced820a77a7186c2320e7d176a5c8b5c16d6ac2af3889d6bc8bf8080000001000000000000022200000004a817c80000000000000249f0000000000000000102d0001eff1600148061b7fbd2d84ed1884177ea785faecb2080b10302e56c8eca8d4f00df84ac34c23f49c006d57d316b7ada5c346e9d4211e11604b300000004080aa982027455aef8453d92f4706b560b61527cc217ddf14da41770e8ed6607190a1851b8000000000000023d000000037521048000000000000249f00000000000000001070a01e302eff5309b9368340edc6114d738b3590e6969bec4e95d8a080cf185e8b9ce5e4b0343bf4bfbaea5c100f1f2bf1cdf82a0ef97c9a0069a2aec631e7c3084ba929b7503c54e7d5ccfc13f1a6c7a441ffcfac86248574d1bc0fe9773836f4c724ea7b2bd03765aaac2e8fa6dbce7de5143072e9d9d5e96a1fd451d02fe4ff803f413f303f8022f3b055b0d35cde31dec5263a8ed638433e3424a4e197c06d94053985a364a5700000004808a52a1010000000000000004000000001046000000037e11d6000000000000000000245986f644357956c1674f385e0878fb7bb44ab3d57ea9911fab98af8a71e1ad1b000000002bc0e1e40000000000220020690fb50de412adf9b20a7fc6c8fb86f1bfd4ebc1ef8e2d96a5a196560798d944475221023ced05ed1ab328b67477376d68a69ecd0f371a9d5843c6c3be4d31498d516d8d2102eff5309b9368340edc6114d738b3590e6969bec4e95d8a080cf185e8b9ce5e4b52aefd013b020000000001015986f644357956c1674f385e0878fb7bb44ab3d57ea9911fab98af8a71e1ad1b0000000000c2d6178001f8d5e4000000000022002080f1dfe71a865b605593e169677c952aaa1196fc2f541ef7d21c3b1006527b61040047304402207f8c1936d0a50671c993890f887c78c6019abc2a2e8018899dcdc0e891fd2b090220046b56afa2cb7e9470073c238654ecf584bcf5c00b96b91e38335a70e2739ec901483045022100871afd240e20a171b9cba46f20555f848c5850f94ec7da7b33b9eeaf6af6653c0220119cda8cbf5f80986d6a4f0db2590c734d1de399a7060a477b5d94df0183625b01475221023ced05ed1ab328b67477376d68a69ecd0f371a9d5843c6c3be4d31498d516d8d2102eff5309b9368340edc6114d738b3590e6969bec4e95d8a080cf185e8b9ce5e4b52aed7782c20000000000000000000040000000010460000000000000000000000037e11d600b5f2287b2d5edf4df5602a3c287db3b938c3f1a943e40715886db5bd400f95d802e7e1abac1feb54ee3ac2172c9e2231f77765df57664fb44a6dc2e4aa9e6a9a6a000000000000000000000000000000000000000000000000000000000000ff03fd10fe44564e2d7e1550099785c2c1bad32a5ae0feeef6e27f0c108d18b4931d245986f644357956c1674f385e0878fb7bb44ab3d57ea9911fab98af8a71e1ad1b000000002bc0e1e40000000000220020690fb50de412adf9b20a7fc6c8fb86f1bfd4ebc1ef8e2d96a5a196560798d944475221023ced05ed1ab328b67477376d68a69ecd0f371a9d5843c6c3be4d31498d516d8d2102eff5309b9368340edc6114d738b3590e6969bec4e95d8a080cf185e8b9ce5e4b52ae0001003e0000fffffffffffc0080474b8cf7bb98217dd8dc475cb7c057a3465d466728978bbb909d0a05d4ae7bbe0001fffffffffff85986f644357956c1674f385e0878fb7bb44ab3d57ea9911fab98af8a71e1ad1b1eedce0000010000fffffd01ae98d7a81bc1aa92fcfb74ced2213e85e0d92ae8ac622bf294b3551c7c27f6f84f782f3b318e4d0eb2c67ac719a7c65afcf85bf159f6ceea9427be54920134196992f6ed0e059db72105a13ec0e799bb08896cad8b4feb7e9ec7283c309b5f43123af1bd9e913fc2db018edadde8932d6992408f10c1ad020504361972dfa7fef09bbc2b568cef3c8c006f7860106fd5984bcc271ff06c4829db2a665e59b7c0b22c311a340ff2ab9bcb74a50db10ed85503ad2d248d95af8151aca8ef96248e8f84b3075922385fbaf012f057e7ee84ecbc14c84880520b26d6fd22ab5f107db606a906efdcf0f88ffbe32dc6ecc10131e1ff0dc8d68dad89c98562557f00448b000043497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea3309000000001eedce0000010000027455aef8453d92f4706b560b61527cc217ddf14da41770e8ed6607190a1851b803933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b13402eff5309b9368340edc6114d738b3590e6969bec4e95d8a080cf185e8b9ce5e4b023ced05ed1ab328b67477376d68a69ecd0f371a9d5843c6c3be4d31498d516d8d88710d73875607575f3d84bb507dd87cca5b85f0cdac84f4ccecce7af3a55897525a45070fe26c0ea43e9580d4ea4cfa62ee3273e5546911145cba6bbf56e59d8e43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea3309000000001eedce000001000060e6eb14010100900000000000000001000003e800000064000000037e11d6000000" - -> """{"type":"DATA_NORMAL","commitments":{"channelId":"5986f644357956c1674f385e0878fb7bb44ab3d57ea9911fab98af8a71e1ad1b","channelConfig":["funding_pubkey_based_channel_keypath"],"channelFeatures":["option_static_remotekey"],"localParams":{"nodeId":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","fundingKeyPath":{"path":[2353764507,3184449568,2809819526,3258060413,392846475,1545000620,720603293,1808318336,2147483649]},"dustLimit":546,"maxHtlcValueInFlightMsat":20000000000,"channelReserve":150000,"htlcMinimum":1,"toSelfDelay":720,"maxAcceptedHtlcs":30,"isFunder":true,"defaultFinalScriptPubKey":"00148061b7fbd2d84ed1884177ea785faecb2080b103","walletStaticPaymentBasepoint":"02e56c8eca8d4f00df84ac34c23f49c006d57d316b7ada5c346e9d4211e11604b3","initFeatures":{"activated":{"option_support_large_channel":"optional","gossip_queries_ex":"optional","option_data_loss_protect":"optional","var_onion_optin":"mandatory","option_static_remotekey":"optional","payment_secret":"optional","option_shutdown_anysegwit":"optional","basic_mpp":"optional","gossip_queries":"optional"},"unknown":[]}},"remoteParams":{"nodeId":"027455aef8453d92f4706b560b61527cc217ddf14da41770e8ed6607190a1851b8","dustLimit":573,"maxHtlcValueInFlightMsat":14850000000,"channelReserve":150000,"htlcMinimum":1,"toSelfDelay":1802,"maxAcceptedHtlcs":483,"fundingPubKey":"02eff5309b9368340edc6114d738b3590e6969bec4e95d8a080cf185e8b9ce5e4b","revocationBasepoint":"0343bf4bfbaea5c100f1f2bf1cdf82a0ef97c9a0069a2aec631e7c3084ba929b75","paymentBasepoint":"03c54e7d5ccfc13f1a6c7a441ffcfac86248574d1bc0fe9773836f4c724ea7b2bd","delayedPaymentBasepoint":"03765aaac2e8fa6dbce7de5143072e9d9d5e96a1fd451d02fe4ff803f413f303f8","htlcBasepoint":"022f3b055b0d35cde31dec5263a8ed638433e3424a4e197c06d94053985a364a57","initFeatures":{"activated":{"option_upfront_shutdown_script":"optional","payment_secret":"mandatory","option_data_loss_protect":"mandatory","var_onion_optin":"optional","option_static_remotekey":"mandatory","option_support_large_channel":"optional","option_anchors_zero_fee_htlc_tx":"optional","basic_mpp":"optional","gossip_queries":"optional"},"unknown":[31]}},"channelFlags":{"announceChannel":true},"localCommit":{"index":4,"spec":{"htlcs":[],"commitTxFeerate":4166,"toLocal":15000000000,"toRemote":0},"commitTxAndRemoteSig":{"commitTx":{"txid":"fa747ecb6f718c6831cc7148cf8d65c3468d2bb6c202605e2b82d2277491222f","tx":"02000000015986f644357956c1674f385e0878fb7bb44ab3d57ea9911fab98af8a71e1ad1b0000000000c2d6178001f8d5e4000000000022002080f1dfe71a865b605593e169677c952aaa1196fc2f541ef7d21c3b1006527b61d7782c20"},"remoteSig":"871afd240e20a171b9cba46f20555f848c5850f94ec7da7b33b9eeaf6af6653c119cda8cbf5f80986d6a4f0db2590c734d1de399a7060a477b5d94df0183625b"},"htlcTxsAndRemoteSigs":[]},"remoteCommit":{"index":4,"spec":{"htlcs":[],"commitTxFeerate":4166,"toLocal":0,"toRemote":15000000000},"txid":"b5f2287b2d5edf4df5602a3c287db3b938c3f1a943e40715886db5bd400f95d8","remotePerCommitmentPoint":"02e7e1abac1feb54ee3ac2172c9e2231f77765df57664fb44a6dc2e4aa9e6a9a6a"},"localChanges":{"proposed":[],"signed":[],"acked":[]},"remoteChanges":{"proposed":[],"acked":[],"signed":[]},"localNextHtlcId":0,"remoteNextHtlcId":0,"originChannels":{},"remoteNextCommitInfo":"03fd10fe44564e2d7e1550099785c2c1bad32a5ae0feeef6e27f0c108d18b4931d","commitInput":{"outPoint":"1bade1718aaf98ab1f91a97ed5b34ab47bfb78085e384f67c156793544f68659:0","amountSatoshis":15000000},"remotePerCommitmentSecrets":null},"shortChannelId":"2026958x1x0","buried":true,"channelAnnouncement":{"nodeSignature1":"98d7a81bc1aa92fcfb74ced2213e85e0d92ae8ac622bf294b3551c7c27f6f84f782f3b318e4d0eb2c67ac719a7c65afcf85bf159f6ceea9427be549201341969","nodeSignature2":"92f6ed0e059db72105a13ec0e799bb08896cad8b4feb7e9ec7283c309b5f43123af1bd9e913fc2db018edadde8932d6992408f10c1ad020504361972dfa7fef0","bitcoinSignature1":"9bbc2b568cef3c8c006f7860106fd5984bcc271ff06c4829db2a665e59b7c0b22c311a340ff2ab9bcb74a50db10ed85503ad2d248d95af8151aca8ef96248e8f","bitcoinSignature2":"84b3075922385fbaf012f057e7ee84ecbc14c84880520b26d6fd22ab5f107db606a906efdcf0f88ffbe32dc6ecc10131e1ff0dc8d68dad89c98562557f00448b","features":{"activated":{},"unknown":[]},"chainHash":"43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000","shortChannelId":"2026958x1x0","nodeId1":"027455aef8453d92f4706b560b61527cc217ddf14da41770e8ed6607190a1851b8","nodeId2":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","bitcoinKey1":"02eff5309b9368340edc6114d738b3590e6969bec4e95d8a080cf185e8b9ce5e4b","bitcoinKey2":"023ced05ed1ab328b67477376d68a69ecd0f371a9d5843c6c3be4d31498d516d8d","tlvStream":{"records":[],"unknown":[]}},"channelUpdate":{"signature":"710d73875607575f3d84bb507dd87cca5b85f0cdac84f4ccecce7af3a55897525a45070fe26c0ea43e9580d4ea4cfa62ee3273e5546911145cba6bbf56e59d8e","chainHash":"43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000","shortChannelId":"2026958x1x0","timestamp":{"iso":"2021-07-08T12:09:56Z","unix":1625746196},"channelFlags":{"isEnabled":true,"isNode1":false},"cltvExpiryDelta":144,"htlcMinimumMsat":1,"feeBaseMsat":1000,"feeProportionalMillionths":100,"htlcMaximumMsat":15000000000,"tlvStream":{"records":[],"unknown":[]}}}""" + -> """{"type":"DATA_NORMAL","commitments":{"channelId":"5986f644357956c1674f385e0878fb7bb44ab3d57ea9911fab98af8a71e1ad1b","channelConfig":["funding_pubkey_based_channel_keypath"],"channelFeatures":["option_static_remotekey"],"localParams":{"nodeId":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","fundingKeyPath":{"path":[2353764507,3184449568,2809819526,3258060413,392846475,1545000620,720603293,1808318336,2147483649]},"dustLimit":546,"maxHtlcValueInFlightMsat":20000000000,"channelReserve":150000,"htlcMinimum":1,"toSelfDelay":720,"maxAcceptedHtlcs":30,"isInitiator":true,"defaultFinalScriptPubKey":"00148061b7fbd2d84ed1884177ea785faecb2080b103","walletStaticPaymentBasepoint":"02e56c8eca8d4f00df84ac34c23f49c006d57d316b7ada5c346e9d4211e11604b3","initFeatures":{"activated":{"option_support_large_channel":"optional","gossip_queries_ex":"optional","option_data_loss_protect":"optional","var_onion_optin":"mandatory","option_static_remotekey":"optional","payment_secret":"optional","option_shutdown_anysegwit":"optional","basic_mpp":"optional","gossip_queries":"optional"},"unknown":[]}},"remoteParams":{"nodeId":"027455aef8453d92f4706b560b61527cc217ddf14da41770e8ed6607190a1851b8","dustLimit":573,"maxHtlcValueInFlightMsat":14850000000,"channelReserve":150000,"htlcMinimum":1,"toSelfDelay":1802,"maxAcceptedHtlcs":483,"fundingPubKey":"02eff5309b9368340edc6114d738b3590e6969bec4e95d8a080cf185e8b9ce5e4b","revocationBasepoint":"0343bf4bfbaea5c100f1f2bf1cdf82a0ef97c9a0069a2aec631e7c3084ba929b75","paymentBasepoint":"03c54e7d5ccfc13f1a6c7a441ffcfac86248574d1bc0fe9773836f4c724ea7b2bd","delayedPaymentBasepoint":"03765aaac2e8fa6dbce7de5143072e9d9d5e96a1fd451d02fe4ff803f413f303f8","htlcBasepoint":"022f3b055b0d35cde31dec5263a8ed638433e3424a4e197c06d94053985a364a57","initFeatures":{"activated":{"option_upfront_shutdown_script":"optional","payment_secret":"mandatory","option_data_loss_protect":"mandatory","var_onion_optin":"optional","option_static_remotekey":"mandatory","option_support_large_channel":"optional","option_anchors_zero_fee_htlc_tx":"optional","basic_mpp":"optional","gossip_queries":"optional"},"unknown":[31]}},"channelFlags":{"announceChannel":true},"localCommit":{"index":4,"spec":{"htlcs":[],"commitTxFeerate":4166,"toLocal":15000000000,"toRemote":0},"commitTxAndRemoteSig":{"commitTx":{"txid":"fa747ecb6f718c6831cc7148cf8d65c3468d2bb6c202605e2b82d2277491222f","tx":"02000000015986f644357956c1674f385e0878fb7bb44ab3d57ea9911fab98af8a71e1ad1b0000000000c2d6178001f8d5e4000000000022002080f1dfe71a865b605593e169677c952aaa1196fc2f541ef7d21c3b1006527b61d7782c20"},"remoteSig":"871afd240e20a171b9cba46f20555f848c5850f94ec7da7b33b9eeaf6af6653c119cda8cbf5f80986d6a4f0db2590c734d1de399a7060a477b5d94df0183625b"},"htlcTxsAndRemoteSigs":[]},"remoteCommit":{"index":4,"spec":{"htlcs":[],"commitTxFeerate":4166,"toLocal":0,"toRemote":15000000000},"txid":"b5f2287b2d5edf4df5602a3c287db3b938c3f1a943e40715886db5bd400f95d8","remotePerCommitmentPoint":"02e7e1abac1feb54ee3ac2172c9e2231f77765df57664fb44a6dc2e4aa9e6a9a6a"},"localChanges":{"proposed":[],"signed":[],"acked":[]},"remoteChanges":{"proposed":[],"acked":[],"signed":[]},"localNextHtlcId":0,"remoteNextHtlcId":0,"originChannels":{},"remoteNextCommitInfo":"03fd10fe44564e2d7e1550099785c2c1bad32a5ae0feeef6e27f0c108d18b4931d","commitInput":{"outPoint":"1bade1718aaf98ab1f91a97ed5b34ab47bfb78085e384f67c156793544f68659:0","amountSatoshis":15000000},"remotePerCommitmentSecrets":null},"shortChannelId":"2026958x1x0","buried":true,"channelAnnouncement":{"nodeSignature1":"98d7a81bc1aa92fcfb74ced2213e85e0d92ae8ac622bf294b3551c7c27f6f84f782f3b318e4d0eb2c67ac719a7c65afcf85bf159f6ceea9427be549201341969","nodeSignature2":"92f6ed0e059db72105a13ec0e799bb08896cad8b4feb7e9ec7283c309b5f43123af1bd9e913fc2db018edadde8932d6992408f10c1ad020504361972dfa7fef0","bitcoinSignature1":"9bbc2b568cef3c8c006f7860106fd5984bcc271ff06c4829db2a665e59b7c0b22c311a340ff2ab9bcb74a50db10ed85503ad2d248d95af8151aca8ef96248e8f","bitcoinSignature2":"84b3075922385fbaf012f057e7ee84ecbc14c84880520b26d6fd22ab5f107db606a906efdcf0f88ffbe32dc6ecc10131e1ff0dc8d68dad89c98562557f00448b","features":{"activated":{},"unknown":[]},"chainHash":"43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000","shortChannelId":"2026958x1x0","nodeId1":"027455aef8453d92f4706b560b61527cc217ddf14da41770e8ed6607190a1851b8","nodeId2":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","bitcoinKey1":"02eff5309b9368340edc6114d738b3590e6969bec4e95d8a080cf185e8b9ce5e4b","bitcoinKey2":"023ced05ed1ab328b67477376d68a69ecd0f371a9d5843c6c3be4d31498d516d8d","tlvStream":{"records":[],"unknown":[]}},"channelUpdate":{"signature":"710d73875607575f3d84bb507dd87cca5b85f0cdac84f4ccecce7af3a55897525a45070fe26c0ea43e9580d4ea4cfa62ee3273e5546911145cba6bbf56e59d8e","chainHash":"43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000","shortChannelId":"2026958x1x0","timestamp":{"iso":"2021-07-08T12:09:56Z","unix":1625746196},"channelFlags":{"isEnabled":true,"isNode1":false},"cltvExpiryDelta":144,"htlcMinimumMsat":1,"feeBaseMsat":1000,"feeProportionalMillionths":100,"htlcMaximumMsat":15000000000,"tlvStream":{"records":[],"unknown":[]}}}""" ) refs.foreach { case (oldbin, refjson) => @@ -262,7 +262,7 @@ object ChannelCodecsSpec { maxAcceptedHtlcs = 50, defaultFinalScriptPubKey = ByteVector.empty, walletStaticPaymentBasepoint = None, - isFunder = true, + isInitiator = true, initFeatures = Features.empty) val remoteParams: RemoteParams = RemoteParams( diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version1/ChannelCodecs1Spec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version1/ChannelCodecs1Spec.scala index b06e283794..49733b5114 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version1/ChannelCodecs1Spec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version1/ChannelCodecs1Spec.scala @@ -69,7 +69,7 @@ class ChannelCodecs1Spec extends AnyFunSuite { maxAcceptedHtlcs = Random.nextInt(Short.MaxValue), defaultFinalScriptPubKey = Script.write(Script.pay2wpkh(PrivateKey(randomBytes32()).publicKey)), walletStaticPaymentBasepoint = None, - isFunder = Random.nextBoolean(), + isInitiator = Random.nextBoolean(), initFeatures = Features(randomBytes(256)).initFeatures()) val o1 = o.copy(walletStaticPaymentBasepoint = Some(PrivateKey(randomBytes32()).publicKey)) diff --git a/eclair-node/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala b/eclair-node/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala index fdd2a4e045..be381b571f 100644 --- a/eclair-node/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala +++ b/eclair-node/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala @@ -1153,8 +1153,8 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM system.eventStream.publish(pset) wsClient.expectMessage(expectedSerializedPset) - val chcr = ChannelCreated(system.deadLetters, system.deadLetters, bobNodeId, isFunder = true, ByteVector32.One, FeeratePerKw(25 sat), Some(FeeratePerKw(20 sat))) - val expectedSerializedChcr = """{"type":"channel-opened","remoteNodeId":"039dc0e0b1d25905e44fdf6f8e89755a5e219685840d0bc1d28d3308f9628a3585","isFunder":true,"temporaryChannelId":"0100000000000000000000000000000000000000000000000000000000000000","initialFeeratePerKw":25,"fundingTxFeeratePerKw":20}""" + val chcr = ChannelCreated(system.deadLetters, system.deadLetters, bobNodeId, isInitiator = true, ByteVector32.One, FeeratePerKw(25 sat), Some(FeeratePerKw(20 sat))) + val expectedSerializedChcr = """{"type":"channel-opened","remoteNodeId":"039dc0e0b1d25905e44fdf6f8e89755a5e219685840d0bc1d28d3308f9628a3585","isInitiator":true,"temporaryChannelId":"0100000000000000000000000000000000000000000000000000000000000000","initialFeeratePerKw":25,"fundingTxFeeratePerKw":20}""" assert(serialization.write(chcr) === expectedSerializedChcr) system.eventStream.publish(chcr) wsClient.expectMessage(expectedSerializedChcr) From 443266d2b86a1be40e891dd10ad3436c793b5293 Mon Sep 17 00:00:00 2001 From: Bastien Teinturier <31281497+t-bast@users.noreply.github.com> Date: Thu, 14 Apr 2022 17:57:38 +0200 Subject: [PATCH 057/121] Add dual funding codecs and feature bit (#2231) Add dual funding feature bit, but keep it disabled for now. Add dual funding protocol messages and codecs. We don't actually handle these messages yet. When we receive them, they will simply be ignored and log a warning. --- eclair-core/src/main/resources/reference.conf | 1 + .../main/scala/fr/acinq/eclair/Features.scala | 7 ++ .../eclair/wire/protocol/ChannelTlv.scala | 30 ++++- .../eclair/wire/protocol/CommonCodecs.scala | 12 +- .../wire/protocol/InteractiveTxTlv.scala | 99 +++++++++++++++ .../protocol/LightningMessageCodecs.scala | 117 +++++++++++++++++- .../wire/protocol/LightningMessageTypes.scala | 90 +++++++++++++- .../eclair/wire/protocol/TlvCodecs.scala | 4 + .../scala/fr/acinq/eclair/FeaturesSpec.scala | 73 ++++++----- .../eclair/payment/Bolt11InvoiceSpec.scala | 2 +- .../protocol/LightningMessageCodecsSpec.scala | 102 ++++++++++++++- 11 files changed, 499 insertions(+), 38 deletions(-) create mode 100644 eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/InteractiveTxTlv.scala diff --git a/eclair-core/src/main/resources/reference.conf b/eclair-core/src/main/resources/reference.conf index 11c5c85181..295ba8a30b 100644 --- a/eclair-core/src/main/resources/reference.conf +++ b/eclair-core/src/main/resources/reference.conf @@ -54,6 +54,7 @@ eclair { option_anchor_outputs = disabled option_anchors_zero_fee_htlc_tx = optional option_shutdown_anysegwit = optional + option_dual_fund = disabled option_onion_messages = optional option_channel_type = optional option_payment_metadata = optional diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala index 1e8b2593f9..5eed1bffd6 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala @@ -213,6 +213,11 @@ object Features { val mandatory = 26 } + case object DualFunding extends Feature with InitFeature with NodeFeature { + val rfcName = "option_dual_fund" + val mandatory = 28 + } + case object OnionMessages extends Feature with InitFeature with NodeFeature { val rfcName = "option_onion_messages" val mandatory = 38 @@ -258,6 +263,7 @@ object Features { AnchorOutputs, AnchorOutputsZeroFeeHtlcTx, ShutdownAnySegwit, + DualFunding, OnionMessages, ChannelType, PaymentMetadata, @@ -272,6 +278,7 @@ object Features { BasicMultiPartPayment -> (PaymentSecret :: Nil), AnchorOutputs -> (StaticRemoteKey :: Nil), AnchorOutputsZeroFeeHtlcTx -> (StaticRemoteKey :: Nil), + DualFunding -> (AnchorOutputsZeroFeeHtlcTx :: Nil), TrampolinePaymentPrototype -> (PaymentSecret :: Nil), KeySend -> (VariableLengthOnion :: Nil) ) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/ChannelTlv.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/ChannelTlv.scala index 7cf67e0671..f49187f7a6 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/ChannelTlv.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/ChannelTlv.scala @@ -29,17 +29,21 @@ sealed trait OpenChannelTlv extends Tlv sealed trait AcceptChannelTlv extends Tlv +sealed trait OpenDualFundedChannelTlv extends Tlv + +sealed trait AcceptDualFundedChannelTlv extends Tlv + object ChannelTlv { /** Commitment to where the funds will go in case of a mutual close, which remote node will enforce in case we're compromised. */ - case class UpfrontShutdownScriptTlv(script: ByteVector) extends OpenChannelTlv with AcceptChannelTlv { + case class UpfrontShutdownScriptTlv(script: ByteVector) extends OpenChannelTlv with AcceptChannelTlv with OpenDualFundedChannelTlv with AcceptDualFundedChannelTlv { val isEmpty: Boolean = script.isEmpty } val upfrontShutdownScriptCodec: Codec[UpfrontShutdownScriptTlv] = variableSizeBytesLong(varintoverflow, bytes).as[UpfrontShutdownScriptTlv] /** A channel type is a set of even feature bits that represent persistent features which affect channel operations. */ - case class ChannelTypeTlv(channelType: ChannelType) extends OpenChannelTlv with AcceptChannelTlv + case class ChannelTypeTlv(channelType: ChannelType) extends OpenChannelTlv with AcceptChannelTlv with OpenDualFundedChannelTlv with AcceptDualFundedChannelTlv val channelTypeCodec: Codec[ChannelTypeTlv] = variableSizeBytesLong(varintoverflow, bytes).xmap( b => ChannelTypeTlv(ChannelTypes.fromFeatures(Features(b).initFeatures())), @@ -69,6 +73,28 @@ object AcceptChannelTlv { ) } +object OpenDualFundedChannelTlv { + + import ChannelTlv._ + + val openTlvCodec: Codec[TlvStream[OpenDualFundedChannelTlv]] = tlvStream(discriminated[OpenDualFundedChannelTlv].by(varint) + .typecase(UInt64(0), upfrontShutdownScriptCodec) + .typecase(UInt64(1), channelTypeCodec) + ) + +} + +object AcceptDualFundedChannelTlv { + + import ChannelTlv._ + + val acceptTlvCodec: Codec[TlvStream[AcceptDualFundedChannelTlv]] = tlvStream(discriminated[AcceptDualFundedChannelTlv].by(varint) + .typecase(UInt64(0), upfrontShutdownScriptCodec) + .typecase(UInt64(1), channelTypeCodec) + ) + +} + sealed trait FundingCreatedTlv extends Tlv object FundingCreatedTlv { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/CommonCodecs.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/CommonCodecs.scala index e6c1095fa3..7e37e80478 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/CommonCodecs.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/CommonCodecs.scala @@ -17,7 +17,7 @@ package fr.acinq.eclair.wire.protocol import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} -import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, Satoshi} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, Satoshi, Transaction} import fr.acinq.eclair.blockchain.fee.FeeratePerKw import fr.acinq.eclair.channel.ChannelFlags import fr.acinq.eclair.crypto.Mac32 @@ -100,6 +100,9 @@ object CommonCodecs { // It is useful in combination with variableSizeBytesLong to encode/decode TLV lengths because those will always be < 2^63. val varintoverflow: Codec[Long] = varint.narrow(l => if (l <= UInt64(Long.MaxValue)) Attempt.successful(l.toBigInt.toLong) else Attempt.failure(Err(s"overflow for value $l")), l => UInt64(l)) + // This codec can be safely used for values < 2^32 and will fail otherwise. + val smallvarint: Codec[Int] = varint.narrow(l => if (l <= UInt64(Int.MaxValue)) Attempt.successful(l.toBigInt.toInt) else Attempt.failure(Err(s"overflow for value $l")), l => UInt64(l)) + val bytes32: Codec[ByteVector32] = limitedSizeBytes(32, bytesStrict(32).xmap(d => ByteVector32(d), d => d.bytes)) val bytes64: Codec[ByteVector64] = limitedSizeBytes(64, bytesStrict(64).xmap(d => ByteVector64(d), d => d.bytes)) @@ -112,6 +115,11 @@ object CommonCodecs { val channelflags: Codec[ChannelFlags] = (ignore(7) dropLeft bool).as[ChannelFlags] + val extendedChannelFlags: Codec[ChannelFlags] = variableSizeBytesLong(varintoverflow, bytes).xmap( + bin => ChannelFlags(bin.lastOption.exists(_ % 2 == 1)), + flags => if (flags.announceChannel) ByteVector(1) else ByteVector(0) + ) + val ipv4address: Codec[Inet4Address] = bytes(4).xmap(b => InetAddress.getByAddress(b.toArray).asInstanceOf[Inet4Address], a => ByteVector(a.getAddress)) val ipv6address: Codec[Inet6Address] = bytes(16).exmap(b => Attempt.fromTry(Try(Inet6Address.getByAddress(null, b.toArray, null))), a => Attempt.fromTry(Try(ByteVector(a.getAddress)))) @@ -144,6 +152,8 @@ object CommonCodecs { val rgb: Codec[Color] = bytes(3).xmap(buf => Color(buf(0), buf(1), buf(2)), t => ByteVector(t.r, t.g, t.b)) + val txCodec: Codec[Transaction] = bytes.xmap(d => Transaction.read(d.toArray), d => Transaction.write(d)) + def zeropaddedstring(size: Int): Codec[String] = fixedSizeBytes(size, utf8).xmap(s => s.takeWhile(_ != '\u0000'), s => s) /** diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/InteractiveTxTlv.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/InteractiveTxTlv.scala new file mode 100644 index 0000000000..5776eaa5ac --- /dev/null +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/InteractiveTxTlv.scala @@ -0,0 +1,99 @@ +/* + * 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.wire.protocol + +import fr.acinq.bitcoin.scalacompat.Satoshi +import fr.acinq.eclair.UInt64 +import fr.acinq.eclair.wire.protocol.CommonCodecs.{varint, varintoverflow} +import fr.acinq.eclair.wire.protocol.TlvCodecs.{tlvStream, tsatoshi} +import scodec.Codec +import scodec.codecs.{discriminated, variableSizeBytesLong} + +/** + * Created by t-bast on 08/04/2022. + */ + +sealed trait TxAddInputTlv extends Tlv + +object TxAddInputTlv { + val txAddInputTlvCodec: Codec[TlvStream[TxAddInputTlv]] = tlvStream(discriminated[TxAddInputTlv].by(varint)) +} + +sealed trait TxAddOutputTlv extends Tlv + +object TxAddOutputTlv { + val txAddOutputTlvCodec: Codec[TlvStream[TxAddOutputTlv]] = tlvStream(discriminated[TxAddOutputTlv].by(varint)) +} + +sealed trait TxRemoveInputTlv extends Tlv + +object TxRemoveInputTlv { + val txRemoveInputTlvCodec: Codec[TlvStream[TxRemoveInputTlv]] = tlvStream(discriminated[TxRemoveInputTlv].by(varint)) +} + +sealed trait TxRemoveOutputTlv extends Tlv + +object TxRemoveOutputTlv { + val txRemoveOutputTlvCodec: Codec[TlvStream[TxRemoveOutputTlv]] = tlvStream(discriminated[TxRemoveOutputTlv].by(varint)) +} + +sealed trait TxCompleteTlv extends Tlv + +object TxCompleteTlv { + val txCompleteTlvCodec: Codec[TlvStream[TxCompleteTlv]] = tlvStream(discriminated[TxCompleteTlv].by(varint)) +} + +sealed trait TxSignaturesTlv extends Tlv + +object TxSignaturesTlv { + val txSignaturesTlvCodec: Codec[TlvStream[TxSignaturesTlv]] = tlvStream(discriminated[TxSignaturesTlv].by(varint)) +} + +sealed trait TxInitRbfTlv extends Tlv + +sealed trait TxAckRbfTlv extends Tlv + +object TxRbfTlv { + /** Amount that the peer will contribute to the transaction's shared output. */ + case class SharedOutputContributionTlv(amount: Satoshi) extends TxInitRbfTlv with TxAckRbfTlv +} + +object TxInitRbfTlv { + + import TxRbfTlv._ + + val txInitRbfTlvCodec: Codec[TlvStream[TxInitRbfTlv]] = tlvStream(discriminated[TxInitRbfTlv].by(varint) + .typecase(UInt64(0), variableSizeBytesLong(varintoverflow, tsatoshi).as[SharedOutputContributionTlv]) + ) + +} + +object TxAckRbfTlv { + + import TxRbfTlv._ + + val txAckRbfTlvCodec: Codec[TlvStream[TxAckRbfTlv]] = tlvStream(discriminated[TxAckRbfTlv].by(varint) + .typecase(UInt64(0), variableSizeBytesLong(varintoverflow, tsatoshi).as[SharedOutputContributionTlv]) + ) + +} + +sealed trait TxAbortTlv extends Tlv + +object TxAbortTlv { + val txAbortTlvCodec: Codec[TlvStream[TxAbortTlv]] = tlvStream(discriminated[TxAbortTlv].by(varint)) +} \ No newline at end of file diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecs.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecs.scala index 774adac8d0..620c91c3ab 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecs.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecs.scala @@ -16,9 +16,10 @@ package fr.acinq.eclair.wire.protocol +import fr.acinq.bitcoin.scalacompat.ScriptWitness import fr.acinq.eclair.wire.Monitoring.{Metrics, Tags} import fr.acinq.eclair.wire.protocol.CommonCodecs._ -import fr.acinq.eclair.{Feature, Features, InitFeature, KamonExt, NodeFeature} +import fr.acinq.eclair.{Feature, Features, InitFeature, KamonExt} import scodec.bits.{BitVector, ByteVector} import scodec.codecs._ import scodec.{Attempt, Codec} @@ -81,7 +82,7 @@ object LightningMessageCodecs { ("fundingSatoshis" | satoshi) :: ("pushMsat" | millisatoshi) :: ("dustLimitSatoshis" | satoshi) :: - ("maxHtlcValueInFlightMsat" | uint64) :: + ("maxHtlcValueInFlightMsat" | uint64) :: // this is not MilliSatoshi because it can exceed the total amount of MilliSatoshi ("channelReserveSatoshis" | satoshi) :: ("htlcMinimumMsat" | millisatoshi) :: ("feeratePerKw" | feeratePerKw) :: @@ -96,10 +97,31 @@ object LightningMessageCodecs { ("channelFlags" | channelflags) :: ("tlvStream" | OpenChannelTlv.openTlvCodec)).as[OpenChannel] + val openDualFundedChannelCodec: Codec[OpenDualFundedChannel] = ( + ("chainHash" | bytes32) :: + ("temporaryChannelId" | bytes32) :: + ("fundingFeerate" | feeratePerKw) :: + ("commitmentFeerate" | feeratePerKw) :: + ("fundingAmount" | satoshi) :: + ("dustLimit" | satoshi) :: + ("maxHtlcValueInFlightMsat" | uint64) :: // this is not MilliSatoshi because it can exceed the total amount of MilliSatoshi + ("htlcMinimumMsat" | millisatoshi) :: + ("toSelfDelay" | cltvExpiryDelta) :: + ("maxAcceptedHtlcs" | uint16) :: + ("lockTime" | uint32) :: + ("fundingPubkey" | publicKey) :: + ("revocationBasepoint" | publicKey) :: + ("paymentBasepoint" | publicKey) :: + ("delayedPaymentBasepoint" | publicKey) :: + ("htlcBasepoint" | publicKey) :: + ("firstPerCommitmentPoint" | publicKey) :: + ("channelFlags" | extendedChannelFlags) :: + ("tlvStream" | OpenDualFundedChannelTlv.openTlvCodec)).as[OpenDualFundedChannel] + val acceptChannelCodec: Codec[AcceptChannel] = ( ("temporaryChannelId" | bytes32) :: ("dustLimitSatoshis" | satoshi) :: - ("maxHtlcValueInFlightMsat" | uint64) :: + ("maxHtlcValueInFlightMsat" | uint64) :: // this is not MilliSatoshi because it can exceed the total amount of MilliSatoshi ("channelReserveSatoshis" | satoshi) :: ("htlcMinimumMsat" | millisatoshi) :: ("minimumDepth" | uint32) :: @@ -113,6 +135,24 @@ object LightningMessageCodecs { ("firstPerCommitmentPoint" | publicKey) :: ("tlvStream" | AcceptChannelTlv.acceptTlvCodec)).as[AcceptChannel] + val acceptDualFundedChannelCodec: Codec[AcceptDualFundedChannel] = ( + ("temporaryChannelId" | bytes32) :: + ("fundingAmount" | satoshi) :: + ("dustLimit" | satoshi) :: + ("maxHtlcValueInFlightMsat" | uint64) :: // this is not MilliSatoshi because it can exceed the total amount of MilliSatoshi + ("htlcMinimumMsat" | millisatoshi) :: + ("minimumDepth" | uint32) :: + ("toSelfDelay" | cltvExpiryDelta) :: + ("maxAcceptedHtlcs" | uint16) :: + ("fundingPubkey" | publicKey) :: + ("revocationBasepoint" | publicKey) :: + ("paymentBasepoint" | publicKey) :: + ("delayedPaymentBasepoint" | publicKey) :: + ("htlcBasepoint" | publicKey) :: + ("firstPerCommitmentPoint" | publicKey) :: + ("channelFlags" | extendedChannelFlags) :: + ("tlvStream" | AcceptDualFundedChannelTlv.acceptTlvCodec)).as[AcceptDualFundedChannel] + val fundingCreatedCodec: Codec[FundingCreated] = ( ("temporaryChannelId" | bytes32) :: ("fundingTxid" | bytes32) :: @@ -130,6 +170,66 @@ object LightningMessageCodecs { ("nextPerCommitmentPoint" | publicKey) :: ("tlvStream" | FundingLockedTlv.fundingLockedTlvCodec)).as[FundingLocked] + private val scriptSigOptCodec: Codec[Option[ByteVector]] = lengthDelimited(bytes).xmap[Option[ByteVector]]( + b => if (b.isEmpty) None else Some(b), + b => b.getOrElse(ByteVector.empty) + ) + + val txAddInputCodec: Codec[TxAddInput] = ( + ("channelId" | bytes32) :: + ("serialId" | uint64) :: + ("previousTx" | lengthDelimited(txCodec)) :: + ("previousTxOutput" | uint32) :: + ("sequence" | uint32) :: + ("scriptSig" | scriptSigOptCodec) :: + ("tlvStream" | TxAddInputTlv.txAddInputTlvCodec)).as[TxAddInput] + + val txAddOutputCodec: Codec[TxAddOutput] = ( + ("channelId" | bytes32) :: + ("serialId" | uint64) :: + ("amount" | satoshi) :: + ("scriptPubKey" | lengthDelimited(bytes)) :: + ("tlvStream" | TxAddOutputTlv.txAddOutputTlvCodec)).as[TxAddOutput] + + val txRemoveInputCodec: Codec[TxRemoveInput] = ( + ("channelId" | bytes32) :: + ("serialId" | uint64) :: + ("tlvStream" | TxRemoveInputTlv.txRemoveInputTlvCodec)).as[TxRemoveInput] + + val txRemoveOutputCodec: Codec[TxRemoveOutput] = ( + ("channelId" | bytes32) :: + ("serialId" | uint64) :: + ("tlvStream" | TxRemoveOutputTlv.txRemoveOutputTlvCodec)).as[TxRemoveOutput] + + val txCompleteCodec: Codec[TxComplete] = ( + ("channelId" | bytes32) :: + ("tlvStream" | TxCompleteTlv.txCompleteTlvCodec)).as[TxComplete] + + private val witnessElementCodec: Codec[ByteVector] = lengthDelimited(bytes) + private val witnessStackCodec: Codec[ScriptWitness] = listOfN(smallvarint, witnessElementCodec).xmap(s => ScriptWitness(s.toSeq), w => w.stack.toList) + private val witnessesCodec: Codec[Seq[ScriptWitness]] = listOfN(smallvarint, witnessStackCodec).xmap(l => l.toSeq, l => l.toList) + + val txSignaturesCodec: Codec[TxSignatures] = ( + ("channelId" | bytes32) :: + ("txId" | sha256) :: + ("witnesses" | witnessesCodec) :: + ("tlvStream" | TxSignaturesTlv.txSignaturesTlvCodec)).as[TxSignatures] + + val txInitRbfCodec: Codec[TxInitRbf] = ( + ("channelId" | bytes32) :: + ("lockTime" | uint32) :: + ("feerate" | feeratePerKw) :: + ("tlvStream" | TxInitRbfTlv.txInitRbfTlvCodec)).as[TxInitRbf] + + val txAckRbfCodec: Codec[TxAckRbf] = ( + ("channelId" | bytes32) :: + ("tlvStream" | TxAckRbfTlv.txAckRbfTlvCodec)).as[TxAckRbf] + + val txAbortCodec: Codec[TxAbort] = ( + ("channelId" | bytes32) :: + ("data" | lengthDelimited(bytes)) :: + ("tlvStream" | TxAbortTlv.txAbortTlvCodec)).as[TxAbort] + val shutdownCodec: Codec[Shutdown] = ( ("channelId" | bytes32) :: ("scriptPubKey" | varsizebinarydata) :: @@ -351,6 +451,17 @@ object LightningMessageCodecs { .typecase(36, fundingLockedCodec) .typecase(38, shutdownCodec) .typecase(39, closingSignedCodec) + .typecase(64, openDualFundedChannelCodec) + .typecase(65, acceptDualFundedChannelCodec) + .typecase(66, txAddInputCodec) + .typecase(67, txAddOutputCodec) + .typecase(68, txRemoveInputCodec) + .typecase(69, txRemoveOutputCodec) + .typecase(70, txCompleteCodec) + .typecase(71, txSignaturesCodec) + .typecase(72, txInitRbfCodec) + .typecase(73, txAckRbfCodec) + .typecase(74, txAbortCodec) .typecase(128, updateAddHtlcCodec) .typecase(130, updateFulfillHtlcCodec) .typecase(131, updateFailHtlcCodec) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageTypes.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageTypes.scala index 9880e02781..6510657c48 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageTypes.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageTypes.scala @@ -19,7 +19,7 @@ package fr.acinq.eclair.wire.protocol import com.google.common.base.Charsets import com.google.common.net.InetAddresses import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} -import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, Satoshi} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, Satoshi, ScriptWitness, Transaction} import fr.acinq.eclair.blockchain.fee.FeeratePerKw import fr.acinq.eclair.channel.{ChannelFlags, ChannelType} import fr.acinq.eclair.{BlockHeight, CltvExpiry, CltvExpiryDelta, Feature, Features, InitFeature, MilliSatoshi, ShortChannelId, TimestampSecond, UInt64} @@ -37,6 +37,7 @@ import scala.util.Try sealed trait LightningMessage extends Serializable sealed trait SetupMessage extends LightningMessage sealed trait ChannelMessage extends LightningMessage +sealed trait InteractiveTxMessage extends LightningMessage sealed trait HtlcMessage extends LightningMessage sealed trait RoutingMessage extends LightningMessage sealed trait AnnouncementMessage extends RoutingMessage // <- not in the spec @@ -79,6 +80,48 @@ case class Ping(pongLength: Int, data: ByteVector, tlvStream: TlvStream[PingTlv] case class Pong(data: ByteVector, tlvStream: TlvStream[PongTlv] = TlvStream.empty) extends SetupMessage +case class TxAddInput(channelId: ByteVector32, + serialId: UInt64, + previousTx: Transaction, + previousTxOutput: Long, + sequence: Long, + scriptSig_opt: Option[ByteVector], + tlvStream: TlvStream[TxAddInputTlv] = TlvStream.empty) extends InteractiveTxMessage with HasChannelId + +case class TxAddOutput(channelId: ByteVector32, + serialId: UInt64, + amount: Satoshi, + pubkeyScript: ByteVector, + tlvStream: TlvStream[TxAddOutputTlv] = TlvStream.empty) extends InteractiveTxMessage with HasChannelId + +case class TxRemoveInput(channelId: ByteVector32, + serialId: UInt64, + tlvStream: TlvStream[TxRemoveInputTlv] = TlvStream.empty) extends InteractiveTxMessage with HasChannelId + +case class TxRemoveOutput(channelId: ByteVector32, + serialId: UInt64, + tlvStream: TlvStream[TxRemoveOutputTlv] = TlvStream.empty) extends InteractiveTxMessage with HasChannelId + +case class TxComplete(channelId: ByteVector32, + tlvStream: TlvStream[TxCompleteTlv] = TlvStream.empty) extends InteractiveTxMessage with HasChannelId + +case class TxSignatures(channelId: ByteVector32, + txId: ByteVector32, + witnesses: Seq[ScriptWitness], + tlvStream: TlvStream[TxSignaturesTlv] = TlvStream.empty) extends InteractiveTxMessage with HasChannelId + +case class TxInitRbf(channelId: ByteVector32, + lockTime: Long, + feerate: FeeratePerKw, + tlvStream: TlvStream[TxInitRbfTlv] = TlvStream.empty) extends InteractiveTxMessage with HasChannelId + +case class TxAckRbf(channelId: ByteVector32, + tlvStream: TlvStream[TxAckRbfTlv] = TlvStream.empty) extends InteractiveTxMessage with HasChannelId + +case class TxAbort(channelId: ByteVector32, + data: ByteVector, + tlvStream: TlvStream[TxAbortTlv] = TlvStream.empty) extends InteractiveTxMessage with HasChannelId + case class ChannelReestablish(channelId: ByteVector32, nextLocalCommitmentNumber: Long, nextRemoteRevocationNumber: Long, @@ -128,6 +171,51 @@ case class AcceptChannel(temporaryChannelId: ByteVector32, val channelType_opt: Option[ChannelType] = tlvStream.get[ChannelTlv.ChannelTypeTlv].map(_.channelType) } +// NB: this message is named open_channel2 in the specification. +case class OpenDualFundedChannel(chainHash: ByteVector32, + temporaryChannelId: ByteVector32, + fundingFeerate: FeeratePerKw, + commitmentFeerate: FeeratePerKw, + fundingAmount: Satoshi, + dustLimit: Satoshi, + maxHtlcValueInFlightMsat: UInt64, // this is not MilliSatoshi because it can exceed the total amount of MilliSatoshi + htlcMinimum: MilliSatoshi, + toSelfDelay: CltvExpiryDelta, + maxAcceptedHtlcs: Int, + lockTime: Long, + fundingPubkey: PublicKey, + revocationBasepoint: PublicKey, + paymentBasepoint: PublicKey, + delayedPaymentBasepoint: PublicKey, + htlcBasepoint: PublicKey, + firstPerCommitmentPoint: PublicKey, + channelFlags: ChannelFlags, + tlvStream: TlvStream[OpenDualFundedChannelTlv] = TlvStream.empty) extends ChannelMessage with HasTemporaryChannelId with HasChainHash { + val upfrontShutdownScript_opt: Option[ByteVector] = tlvStream.get[ChannelTlv.UpfrontShutdownScriptTlv].map(_.script) + val channelType_opt: Option[ChannelType] = tlvStream.get[ChannelTlv.ChannelTypeTlv].map(_.channelType) +} + +// NB: this message is named accept_channel2 in the specification. +case class AcceptDualFundedChannel(temporaryChannelId: ByteVector32, + fundingAmount: Satoshi, + dustLimit: Satoshi, + maxHtlcValueInFlightMsat: UInt64, // this is not MilliSatoshi because it can exceed the total amount of MilliSatoshi + htlcMinimum: MilliSatoshi, + minimumDepth: Long, + toSelfDelay: CltvExpiryDelta, + maxAcceptedHtlcs: Int, + fundingPubkey: PublicKey, + revocationBasepoint: PublicKey, + paymentBasepoint: PublicKey, + delayedPaymentBasepoint: PublicKey, + htlcBasepoint: PublicKey, + firstPerCommitmentPoint: PublicKey, + channelFlags: ChannelFlags, + tlvStream: TlvStream[AcceptDualFundedChannelTlv] = TlvStream.empty) extends ChannelMessage with HasTemporaryChannelId { + val upfrontShutdownScript_opt: Option[ByteVector] = tlvStream.get[ChannelTlv.UpfrontShutdownScriptTlv].map(_.script) + val channelType_opt: Option[ChannelType] = tlvStream.get[ChannelTlv.ChannelTypeTlv].map(_.channelType) +} + case class FundingCreated(temporaryChannelId: ByteVector32, fundingTxid: ByteVector32, fundingOutputIndex: Int, diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/TlvCodecs.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/TlvCodecs.scala index 932ad7f1e0..42798b4ad0 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/TlvCodecs.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/TlvCodecs.scala @@ -16,6 +16,7 @@ package fr.acinq.eclair.wire.protocol +import fr.acinq.bitcoin.scalacompat.Satoshi import fr.acinq.eclair.UInt64.Conversions._ import fr.acinq.eclair.wire.protocol.CommonCodecs.{minimalvalue, uint64, varint, varintoverflow} import fr.acinq.eclair.{MilliSatoshi, UInt64} @@ -77,6 +78,9 @@ object TlvCodecs { */ val tmillisatoshi: Codec[MilliSatoshi] = tu64overflow.xmap(l => MilliSatoshi(l), m => m.toLong) + /** Truncated satoshi (0 to 8 bytes unsigned). */ + val tsatoshi: Codec[Satoshi] = tu64overflow.xmap(l => Satoshi(l), s => s.toLong) + /** Truncated uint32 (0 to 4 bytes unsigned integer). */ val tu32: Codec[Long] = tu64.exmap({ case i if i > 0xffffffffL => Attempt.Failure(Err("tu32 overflow")) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/FeaturesSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/FeaturesSpec.scala index e3ef702c97..65c83af986 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/FeaturesSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/FeaturesSpec.scala @@ -62,39 +62,54 @@ class FeaturesSpec extends AnyFunSuite { test("features dependencies") { val testCases = Map( - bin" " -> true, - bin" 00000000" -> true, - bin" 01011000" -> true, + bin" " -> true, + bin" 00000000" -> true, + bin" 01011000" -> true, // gossip_queries_ex depend on gossip_queries - bin"000000000000100000000000" -> false, - bin"000000000000010000000000" -> false, - bin"000000000000100010000000" -> true, - bin"000000000000100001000000" -> true, + bin" 000000000000100000000000" -> false, + bin" 000000000000010000000000" -> false, + bin" 000000000000100010000000" -> true, + bin" 000000000000100001000000" -> true, // payment_secret depends on var_onion_optin - bin"000000001000000000000000" -> false, - bin"000000000100000000000000" -> false, - bin"000000000100001000000000" -> true, + bin" 000000001000000000000000" -> false, + bin" 000000000100000000000000" -> false, + bin" 000000000100001000000000" -> true, // basic_mpp depends on payment_secret - bin"000000100000000000000000" -> false, - bin"000000010000000000000000" -> false, - bin"000000101000000100000000" -> true, - bin"000000011000000100000000" -> true, - bin"000000011000001000000000" -> true, - bin"000000100100000100000000" -> true, + bin" 000000100000000000000000" -> false, + bin" 000000010000000000000000" -> false, + bin" 000000101000000100000000" -> true, + bin" 000000011000000100000000" -> true, + bin" 000000011000001000000000" -> true, + bin" 000000100100000100000000" -> true, // option_anchor_outputs depends on option_static_remotekey - bin"001000000000000000000000" -> false, - bin"000100000000000000000000" -> false, - bin"001000000010000000000000" -> true, - bin"001000000001000000000000" -> true, - bin"000100000010000000000000" -> true, - bin"000100000001000000000000" -> true, + bin" 001000000000000000000000" -> false, + bin" 000100000000000000000000" -> false, + bin" 001000000010000000000000" -> true, + bin" 001000000001000000000000" -> true, + bin" 000100000010000000000000" -> true, + bin" 000100000001000000000000" -> true, // option_anchors_zero_fee_htlc_tx depends on option_static_remotekey - bin"100000000000000000000000" -> false, - bin"010000000000000000000000" -> false, - bin"100000000010000000000000" -> true, - bin"100000000001000000000000" -> true, - bin"010000000010000000000000" -> true, - bin"010000000001000000000000" -> true, + bin" 100000000000000000000000" -> false, + bin" 010000000000000000000000" -> false, + bin" 100000000010000000000000" -> true, + bin" 100000000001000000000000" -> true, + bin" 010000000010000000000000" -> true, + bin" 010000000001000000000000" -> true, + // option_dual_fund depends on option_anchors_zero_fee_htlc_tx, which itself depends on option_static_remotekey + bin"00100000000000000000000000000000" -> false, + bin"00010000000000000000000000000000" -> false, + bin"00100000100000000000000000000000" -> false, + bin"00100000010000000000000000000000" -> false, + bin"00010000100000000000000000000000" -> false, + bin"00010000010000000000000000000000" -> false, + bin"00100000100000000010000000000000" -> true, + bin"00100000100000000001000000000000" -> true, + bin"00100000010000000010000000000000" -> true, + bin"00100000010000000001000000000000" -> true, + bin"00010000100000000010000000000000" -> true, + bin"00010000100000000001000000000000" -> true, + bin"00010000010000000010000000000000" -> true, + bin"00010000010000000001000000000000" -> true, ) for ((testCase, valid) <- testCases) { @@ -236,7 +251,7 @@ class FeaturesSpec extends AnyFunSuite { hex"0100" -> Features(VariableLengthOnion -> Mandatory), hex"028a8a" -> Features(DataLossProtect -> Optional, InitialRoutingSync -> Optional, ChannelRangeQueries -> Optional, VariableLengthOnion -> Optional, ChannelRangeQueriesExtended -> Optional, PaymentSecret -> Optional, BasicMultiPartPayment -> Optional), hex"09004200" -> Features(Map(VariableLengthOnion -> Optional, PaymentSecret -> Mandatory, ShutdownAnySegwit -> Optional), Set(UnknownFeature(24))), - hex"52000000" -> Features(Map.empty[Feature, FeatureSupport], Set(UnknownFeature(25), UnknownFeature(28), UnknownFeature(30))) + hex"80010080000000000000000000000000000000000000" -> Features(Map.empty[Feature, FeatureSupport], Set(UnknownFeature(151), UnknownFeature(160), UnknownFeature(175))) ) for ((bin, features) <- testCases) { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/Bolt11InvoiceSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/Bolt11InvoiceSpec.scala index bcb9adda8b..d68870976a 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/Bolt11InvoiceSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/Bolt11InvoiceSpec.scala @@ -480,7 +480,7 @@ class Bolt11InvoiceSpec extends AnyFunSuite { // those are useful for nonreg testing of the areSupported method (which needs to be updated with every new supported mandatory bit) Features(bin" 000001000000000100000100000000") -> Result(allowMultiPart = false, requirePaymentSecret = true, areSupported = false), Features(bin" 000100000000000100000100000000") -> Result(allowMultiPart = false, requirePaymentSecret = true, areSupported = true), - Features(bin"00000010000000000000100000100000000") -> Result(allowMultiPart = false, requirePaymentSecret = true, areSupported = false), + Features(bin"00000010000000000000100000100000000") -> Result(allowMultiPart = false, requirePaymentSecret = true, areSupported = true), Features(bin"00001000000000000000100000100000000") -> Result(allowMultiPart = false, requirePaymentSecret = true, areSupported = false) ) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecsSpec.scala index 389141e7b8..317f744309 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecsSpec.scala @@ -16,8 +16,9 @@ package fr.acinq.eclair.wire.protocol +import com.google.common.base.Charsets import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} -import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, ByteVector64, SatoshiLong} +import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, ByteVector64, SatoshiLong, ScriptWitness, Transaction} import fr.acinq.eclair.FeatureSupport.Optional import fr.acinq.eclair.Features.DataLossProtect import fr.acinq.eclair._ @@ -25,8 +26,10 @@ import fr.acinq.eclair.blockchain.fee.FeeratePerKw import fr.acinq.eclair.channel.{ChannelFlags, ChannelTypes} import fr.acinq.eclair.json.JsonSerializers import fr.acinq.eclair.router.Announcements +import fr.acinq.eclair.wire.protocol.ChannelTlv.{ChannelTypeTlv, UpfrontShutdownScriptTlv} import fr.acinq.eclair.wire.protocol.LightningMessageCodecs._ import fr.acinq.eclair.wire.protocol.ReplyChannelRangeTlv._ +import fr.acinq.eclair.wire.protocol.TxRbfTlv.SharedOutputContributionTlv import org.json4s.jackson.Serialization import org.scalatest.funsuite.AnyFunSuite import scodec.DecodeResult @@ -153,6 +156,43 @@ class LightningMessageCodecsSpec extends AnyFunSuite { assert(bin === bin2) } + test("encode/decode interactive-tx messages") { + val channelId1 = ByteVector32(hex"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") + val channelId2 = ByteVector32(hex"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb") + // This is a random mainnet transaction. + val txBin1 = hex"020000000001014ade359c5deb7c1cde2e94f401854658f97d7fa31c17ce9a831db253120a0a410100000017160014eb9a5bd79194a23d19d6ec473c768fb74f9ed32cffffffff021ca408000000000017a914946118f24bb7b37d5e9e39579e4a411e70f5b6a08763e703000000000017a9143638b2602d11f934c04abc6adb1494f69d1f14af8702473044022059ddd943b399211e4266a349f26b3289979e29f9b067792c6cfa8cc5ae25f44602204d627a5a5b603d0562e7969011fb3d64908af90a3ec7c876eaa9baf61e1958af012102f5188df1da92ed818581c29778047800ed6635788aa09d9469f7d17628f7323300000000" + val tx1 = Transaction.read(txBin1.toArray) + // This is random, longer mainnet transaction. + val txBin2 = hex"0200000000010142180a8812fc79a3da7fb2471eff3e22d7faee990604c2ba7f2fc8dfb15b550a0200000000feffffff030f241800000000001976a9146774040642a78ca3b8b395e70f8391b21ec026fc88ac4a155801000000001600148d2e0b57adcb8869e603fd35b5179caf053361253b1d010000000000160014e032f4f4b9f8611df0d30a20648c190c263bbc33024730440220506005aa347f5b698542cafcb4f1a10250aeb52a609d6fd67ef68f9c1a5d954302206b9bb844343f4012bccd9d08a0f5430afb9549555a3252e499be7df97aae477a012103976d6b3eea3de4b056cd88cdfd50a22daf121e0fb5c6e45ba0f40e1effbd275a00000000" + val tx2 = Transaction.read(txBin2.toArray) + val testCases = Seq( + TxAddInput(channelId1, UInt64(561), tx1, 1, 5, None) -> hex"0042 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000000231 f7 020000000001014ade359c5deb7c1cde2e94f401854658f97d7fa31c17ce9a831db253120a0a410100000017160014eb9a5bd79194a23d19d6ec473c768fb74f9ed32cffffffff021ca408000000000017a914946118f24bb7b37d5e9e39579e4a411e70f5b6a08763e703000000000017a9143638b2602d11f934c04abc6adb1494f69d1f14af8702473044022059ddd943b399211e4266a349f26b3289979e29f9b067792c6cfa8cc5ae25f44602204d627a5a5b603d0562e7969011fb3d64908af90a3ec7c876eaa9baf61e1958af012102f5188df1da92ed818581c29778047800ed6635788aa09d9469f7d17628f7323300000000 00000001 00000005 00", + TxAddInput(channelId2, UInt64(0), tx2, 2, 0, None) -> hex"0042 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 0000000000000000 fd0100 0200000000010142180a8812fc79a3da7fb2471eff3e22d7faee990604c2ba7f2fc8dfb15b550a0200000000feffffff030f241800000000001976a9146774040642a78ca3b8b395e70f8391b21ec026fc88ac4a155801000000001600148d2e0b57adcb8869e603fd35b5179caf053361253b1d010000000000160014e032f4f4b9f8611df0d30a20648c190c263bbc33024730440220506005aa347f5b698542cafcb4f1a10250aeb52a609d6fd67ef68f9c1a5d954302206b9bb844343f4012bccd9d08a0f5430afb9549555a3252e499be7df97aae477a012103976d6b3eea3de4b056cd88cdfd50a22daf121e0fb5c6e45ba0f40e1effbd275a00000000 00000002 00000000 00", + TxAddInput(channelId1, UInt64(561), tx1, 0, 0, Some(hex"00bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb")) -> hex"0042 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000000231 f7 020000000001014ade359c5deb7c1cde2e94f401854658f97d7fa31c17ce9a831db253120a0a410100000017160014eb9a5bd79194a23d19d6ec473c768fb74f9ed32cffffffff021ca408000000000017a914946118f24bb7b37d5e9e39579e4a411e70f5b6a08763e703000000000017a9143638b2602d11f934c04abc6adb1494f69d1f14af8702473044022059ddd943b399211e4266a349f26b3289979e29f9b067792c6cfa8cc5ae25f44602204d627a5a5b603d0562e7969011fb3d64908af90a3ec7c876eaa9baf61e1958af012102f5188df1da92ed818581c29778047800ed6635788aa09d9469f7d17628f7323300000000 00000000 00000000 15 00bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + TxAddOutput(channelId1, UInt64(1105), 2047 sat, hex"00149357014afd0ccd265658c9ae81efa995e771f472") -> hex"0043 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000000451 00000000000007ff 16 00149357014afd0ccd265658c9ae81efa995e771f472", + TxAddOutput(channelId1, UInt64(1105), 2047 sat, hex"00149357014afd0ccd265658c9ae81efa995e771f472", TlvStream(Nil, Seq(GenericTlv(UInt64(301), hex"2a")))) -> hex"0043 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000000451 00000000000007ff 16 00149357014afd0ccd265658c9ae81efa995e771f472 fd012d012a", + TxRemoveInput(channelId2, UInt64(561)) -> hex"0044 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 0000000000000231", + TxRemoveOutput(channelId1, UInt64(1)) -> hex"0045 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000000001", + TxComplete(channelId1) -> hex"0046 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + TxComplete(channelId1, TlvStream(Nil, Seq(GenericTlv(UInt64(231), hex"deadbeef"), GenericTlv(UInt64(507), hex"")))) -> hex"0046 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa e704deadbeef fd01fb00", + TxSignatures(channelId1, tx2.txid, Seq(ScriptWitness(Seq(hex"dead", hex"beef")), ScriptWitness(Seq(hex"", hex"01010101", hex"", hex"02")))) -> hex"0047 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa f169ed4bcb4ca97646845ec063d4deddcbe704f77f1b2c205929195f84a87afc 02 0202dead02beef 04 000401010101000102", + TxSignatures(channelId2, tx1.txid, Nil) -> hex"0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 06f125a8ef64eb5a25826190dc28f15b85dc1adcfc7a178eef393ea325c02e1f 00", + TxInitRbf(channelId1, 8388607, FeeratePerKw(4000 sat)) -> hex"0048 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 007fffff 00000fa0", + TxInitRbf(channelId1, 0, FeeratePerKw(4000 sat), TlvStream(SharedOutputContributionTlv(5000 sat))) -> hex"0048 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00000000 00000fa0 00021388", + TxAckRbf(channelId2) -> hex"0049 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + TxAckRbf(channelId2, TlvStream(SharedOutputContributionTlv(450000 sat))) -> hex"0049 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 000306ddd0", + TxAbort(channelId1, hex"") -> hex"004a aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00", + TxAbort(channelId1, ByteVector.view("internal error".getBytes(Charsets.US_ASCII))) -> hex"004a aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0e 696e7465726e616c206572726f72", + ) + testCases.foreach { case (message, bin) => + val decoded = lightningMessageCodec.decode(bin.bits).require + assert(decoded.remainder.isEmpty) + assert(decoded.value === message) + val encoded = lightningMessageCodec.encode(message).require.bytes + assert(encoded === bin) + } + } + test("encode/decode open_channel") { val defaultOpen = OpenChannel(ByteVector32.Zeroes, ByteVector32.Zeroes, 1 sat, 1 msat, 1 sat, UInt64(1), 1 sat, 1 msat, FeeratePerKw(1 sat), CltvExpiryDelta(1), 1, publicKey(1), point(2), point(3), point(4), point(5), point(6), ChannelFlags.Private) // Legacy encoding that omits the upfront_shutdown_script and trailing tlv stream. @@ -210,6 +250,36 @@ class LightningMessageCodecsSpec extends AnyFunSuite { } } + test("encode/decode open_channel (dual funding)") { + val defaultOpen = OpenDualFundedChannel(ByteVector32.Zeroes, ByteVector32.One, FeeratePerKw(5000 sat), FeeratePerKw(4000 sat), 250_000 sat, 500 sat, UInt64(50_000), 15 msat, CltvExpiryDelta(144), 483, 650_000, publicKey(1), publicKey(2), publicKey(3), publicKey(4), publicKey(5), publicKey(6), ChannelFlags(true)) + val defaultEncoded = hex"0040 0000000000000000000000000000000000000000000000000000000000000000 0100000000000000000000000000000000000000000000000000000000000000 00001388 00000fa0 000000000003d090 00000000000001f4 000000000000c350 000000000000000f 0090 01e3 0009eb10 031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f 024d4b6cd1361032ca9bd2aeb9d900aa4d45d9ead80ac9423374c451a7254d0766 02531fe6068134503d2723133227c867ac8fa6c83c537e9a44c3c5bdbdcb1fe337 03462779ad4aad39514614751a71085f2f10e1c7a593e4e030efb5b8721ce55b0b 0362c0a046dacce86ddd0343c6d3c7c79c2208ba0d9c9cf24a6d046d21d21f90f7 03f006a18d5653c4edf5391ff23a61f03ff83d237e880ee61187fa9f379a028e0a 0101" + val testCases = Seq( + defaultOpen -> defaultEncoded, + defaultOpen.copy(channelFlags = ChannelFlags(false), tlvStream = TlvStream(ChannelTypeTlv(ChannelTypes.AnchorOutputsZeroFeeHtlcTx))) -> (defaultEncoded.dropRight(2) ++ hex"0100" ++ hex"0103401000"), + defaultOpen.copy(tlvStream = TlvStream(UpfrontShutdownScriptTlv(hex"00143adb2d0445c4d491cc7568b10323bd6615a91283"), ChannelTypeTlv(ChannelTypes.AnchorOutputsZeroFeeHtlcTx))) -> (defaultEncoded ++ hex"001600143adb2d0445c4d491cc7568b10323bd6615a91283 0103401000"), + ) + testCases.foreach { case (open, bin) => + val decoded = lightningMessageCodec.decode(bin.bits).require.value + assert(decoded === open) + val encoded = lightningMessageCodec.encode(open).require.bytes + assert(encoded === bin) + } + } + + test("decode open_channel with unknown channel flags (dual funding)") { + val defaultOpen = OpenDualFundedChannel(ByteVector32.Zeroes, ByteVector32.One, FeeratePerKw(5000 sat), FeeratePerKw(4000 sat), 250_000 sat, 500 sat, UInt64(50_000), 15 msat, CltvExpiryDelta(144), 483, 650_000, publicKey(1), publicKey(2), publicKey(3), publicKey(4), publicKey(5), publicKey(6), ChannelFlags(true)) + val defaultEncodedWithoutFlags = hex"0040 0000000000000000000000000000000000000000000000000000000000000000 0100000000000000000000000000000000000000000000000000000000000000 00001388 00000fa0 000000000003d090 00000000000001f4 000000000000c350 000000000000000f 0090 01e3 0009eb10 031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f 024d4b6cd1361032ca9bd2aeb9d900aa4d45d9ead80ac9423374c451a7254d0766 02531fe6068134503d2723133227c867ac8fa6c83c537e9a44c3c5bdbdcb1fe337 03462779ad4aad39514614751a71085f2f10e1c7a593e4e030efb5b8721ce55b0b 0362c0a046dacce86ddd0343c6d3c7c79c2208ba0d9c9cf24a6d046d21d21f90f7 03f006a18d5653c4edf5391ff23a61f03ff83d237e880ee61187fa9f379a028e0a" + val testCases = Seq( + defaultEncodedWithoutFlags ++ hex"00" -> ChannelFlags(false), + defaultEncodedWithoutFlags ++ hex"01a2" -> ChannelFlags(false), + defaultEncodedWithoutFlags ++ hex"037a2a1f" -> ChannelFlags(true), + ) + testCases.foreach { case (bin, flags) => + val decoded = lightningMessageCodec.decode(bin.bits).require.value + assert(decoded === defaultOpen.copy(channelFlags = flags)) + } + } + test("encode/decode accept_channel") { val defaultAccept = AcceptChannel(ByteVector32.Zeroes, 1 sat, UInt64(1), 1 sat, 1 msat, 1, CltvExpiryDelta(1), 1, publicKey(1), point(2), point(3), point(4), point(5), point(6)) // Legacy encoding that omits the upfront_shutdown_script and trailing tlv stream. @@ -236,6 +306,36 @@ class LightningMessageCodecsSpec extends AnyFunSuite { } } + test("encode/decode accept_channel (dual funding)") { + val defaultAccept = AcceptDualFundedChannel(ByteVector32.One, 50_000 sat, 473 sat, UInt64(100_000_000), 1 msat, 6, CltvExpiryDelta(144), 50, publicKey(1), point(2), point(3), point(4), point(5), point(6), ChannelFlags(true)) + val defaultEncoded = hex"0041 0100000000000000000000000000000000000000000000000000000000000000 000000000000c350 00000000000001d9 0000000005f5e100 0000000000000001 00000006 0090 0032 031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f 024d4b6cd1361032ca9bd2aeb9d900aa4d45d9ead80ac9423374c451a7254d0766 02531fe6068134503d2723133227c867ac8fa6c83c537e9a44c3c5bdbdcb1fe337 03462779ad4aad39514614751a71085f2f10e1c7a593e4e030efb5b8721ce55b0b 0362c0a046dacce86ddd0343c6d3c7c79c2208ba0d9c9cf24a6d046d21d21f90f7 03f006a18d5653c4edf5391ff23a61f03ff83d237e880ee61187fa9f379a028e0a 0101" + val testCases = Seq( + defaultAccept -> defaultEncoded, + defaultAccept.copy(channelFlags = ChannelFlags(false), tlvStream = TlvStream(UpfrontShutdownScriptTlv(hex"00143adb2d0445c4d491cc7568b10323bd6615a91283"))) -> (defaultEncoded.dropRight(2) ++ hex"0100" ++ hex"001600143adb2d0445c4d491cc7568b10323bd6615a91283"), + defaultAccept.copy(tlvStream = TlvStream(ChannelTypeTlv(ChannelTypes.StaticRemoteKey))) -> (defaultEncoded ++ hex"01021000"), + ) + testCases.foreach { case (accept, bin) => + val decoded = lightningMessageCodec.decode(bin.bits).require.value + assert(decoded === accept) + val encoded = lightningMessageCodec.encode(accept).require.bytes + assert(encoded === bin) + } + } + + test("decode accept_channel with unknown channel flags (dual funding)") { + val defaultAccept = AcceptDualFundedChannel(ByteVector32.One, 50_000 sat, 473 sat, UInt64(100_000_000), 1 msat, 6, CltvExpiryDelta(144), 50, publicKey(1), point(2), point(3), point(4), point(5), point(6), ChannelFlags(true)) + val defaultEncodedWithoutFlags = hex"0041 0100000000000000000000000000000000000000000000000000000000000000 000000000000c350 00000000000001d9 0000000005f5e100 0000000000000001 00000006 0090 0032 031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f 024d4b6cd1361032ca9bd2aeb9d900aa4d45d9ead80ac9423374c451a7254d0766 02531fe6068134503d2723133227c867ac8fa6c83c537e9a44c3c5bdbdcb1fe337 03462779ad4aad39514614751a71085f2f10e1c7a593e4e030efb5b8721ce55b0b 0362c0a046dacce86ddd0343c6d3c7c79c2208ba0d9c9cf24a6d046d21d21f90f7 03f006a18d5653c4edf5391ff23a61f03ff83d237e880ee61187fa9f379a028e0a" + val testCases = Seq( + defaultEncodedWithoutFlags ++ hex"00" -> ChannelFlags(false), + defaultEncodedWithoutFlags ++ hex"01a2" -> ChannelFlags(false), + defaultEncodedWithoutFlags ++ hex"037a2a1f" -> ChannelFlags(true), + ) + testCases.foreach { case (bin, flags) => + val decoded = lightningMessageCodec.decode(bin.bits).require.value + assert(decoded === defaultAccept.copy(channelFlags = flags)) + } + } + test("encode/decode closing_signed") { val defaultSig = ByteVector64(hex"01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101") val testCases = Seq( From a4ddbc33eafc64cf769cc9af474e714c61faaecb Mon Sep 17 00:00:00 2001 From: Thomas HUET <81159533+thomash-acinq@users.noreply.github.com> Date: Mon, 25 Apr 2022 10:19:26 +0200 Subject: [PATCH 058/121] Give best capacity score to local channels (#2239) In path-finding, we give a better score to channels with higher capacity because they have a higher change to have enough balance to relay the payment. But for local channels we know the balance so we can be sure that the payment can be relayed. --- .../scala/fr/acinq/eclair/router/Graph.scala | 4 +++- .../eclair/router/RouteCalculationSpec.scala | 22 +++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/Graph.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/Graph.scala index f433e28822..aff89f0c14 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/Graph.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/Graph.scala @@ -315,7 +315,9 @@ object Graph { // Every edge is weighted by channel capacity, larger channels add less weight val edgeMaxCapacity = edge.capacity.toMilliSatoshi - val capFactor = 1 - normalize(edgeMaxCapacity.toLong.toDouble, CAPACITY_CHANNEL_LOW.toLong.toDouble, CAPACITY_CHANNEL_HIGH.toLong.toDouble) + val capFactor = + if (edge.balance_opt.isDefined) 0 // If we know the balance of the channel we treat it as if it had the maximum capacity. + else 1 - normalize(edgeMaxCapacity.toLong.toDouble, CAPACITY_CHANNEL_LOW.toLong.toDouble, CAPACITY_CHANNEL_HIGH.toLong.toDouble) // Every edge is weighted by its cltv-delta value, normalized val cltvFactor = normalize(edge.update.cltvExpiryDelta.toInt, CLTV_LOW, CLTV_HIGH) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/RouteCalculationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/RouteCalculationSpec.scala index ec06855fce..b551dbec1c 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/RouteCalculationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/RouteCalculationSpec.scala @@ -1884,6 +1884,28 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { assert(findMultiPartRoute(g, a, b, amount, 101_000 msat, Set.empty, Set.empty, Set.empty, Nil, routeParams = routeParams, currentBlockHeight = BlockHeight(400000)).isSuccess) } } + + test("small local edge with liquidity is better than big remote edge") { + // A === B === C -- D + // \_________/ + val g = DirectedGraph(List( + makeEdge(1L, a, b, 100 msat, 100, minHtlc = 1000 msat, capacity = 100000000 sat, balance_opt = Some(10000000 msat)), + makeEdge(2L, b, c, 100 msat, 100, minHtlc = 1000 msat, capacity = 100000000 sat), + makeEdge(3L, a, c, 100 msat, 100, minHtlc = 1000 msat, capacity = 100 sat, balance_opt = Some(100000 msat)), + makeEdge(4L, c, d, 100 msat, 100, minHtlc = 1000 msat, capacity = 100000000 sat), + )) + + val wr = WeightRatios( + baseFactor = 0, + cltvDeltaFactor = 0, + ageFactor = 0, + capacityFactor = 1, + hopCost = RelayFees(500 msat, 200), + ) + val Success(routes) = findRoute(g, a, d, 50000 msat, 100000000 msat, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.copy(heuristics = Left(wr), includeLocalChannelCost = true), currentBlockHeight = BlockHeight(400000)) + val route :: Nil = routes + assert(route2Ids(route) === 3 :: 4 :: Nil) + } } object RouteCalculationSpec { From f0994099e9bdbb862d1c6eeca93d220357859e27 Mon Sep 17 00:00:00 2001 From: Pierre-Marie Padiou Date: Mon, 25 Apr 2022 10:47:01 +0200 Subject: [PATCH 059/121] Disable invoice purger in tests (#2240) It was creating a race condition in the test `MultiPartHandlerSpec`.`PaymentHandler should reject incoming multi-part payment with an invalid expiry` due the 0-expiry invoice being immediately purged. --- .../src/test/scala/fr/acinq/eclair/TestConstants.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala b/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala index 061359af68..b77d9fa3af 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala @@ -197,7 +197,7 @@ object TestConstants { relayPolicy = RelayAll, timeout = 1 minute ), - purgeInvoicesInterval = Some(24 hours), + purgeInvoicesInterval = None, ) def channelParams: LocalParams = Peer.makeChannelParams( @@ -335,7 +335,7 @@ object TestConstants { relayPolicy = RelayAll, timeout = 1 minute ), - purgeInvoicesInterval = Some(24 hours) + purgeInvoicesInterval = None ) def channelParams: LocalParams = Peer.makeChannelParams( From 56c9e0670ed18577f956f823bbaa90b7a64b7da0 Mon Sep 17 00:00:00 2001 From: Pierre-Marie Padiou Date: Thu, 28 Apr 2022 12:05:29 +0200 Subject: [PATCH 060/121] Only define decode method in legacy codecs (#2241) Follow up to #2038. --- .../wire/internal/channel/version0/ChannelCodecs0.scala | 7 +++---- .../wire/internal/channel/version1/ChannelCodecs1.scala | 7 +++---- .../wire/internal/channel/version2/ChannelCodecs2.scala | 7 +++---- 3 files changed, 9 insertions(+), 12 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelCodecs0.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelCodecs0.scala index ee03273f51..3dce2bf7ba 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelCodecs0.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelCodecs0.scala @@ -51,10 +51,9 @@ private[channel] object ChannelCodecs0 { ("chaincode" | bytes32) :: ("depth" | uint16) :: ("path" | keyPathCodec) :: - ("parent" | int64)).xmap( - { case a :: b :: c :: d :: e :: HNil => ExtendedPrivateKey(a, b, c, d, e) }, - { exp: ExtendedPrivateKey => exp.secretkeybytes :: exp.chaincode :: exp.depth :: exp.path :: exp.parent :: HNil } - ).decodeOnly + ("parent" | int64)) + .map { case a :: b :: c :: d :: e :: HNil => ExtendedPrivateKey(a, b, c, d, e) } + .decodeOnly val channelVersionCodec: Codec[ChannelTypes0.ChannelVersion] = discriminatorWithDefault[ChannelTypes0.ChannelVersion]( discriminator = discriminated[ChannelTypes0.ChannelVersion].by(byte) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version1/ChannelCodecs1.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version1/ChannelCodecs1.scala index e1fc5e2518..4337f43b40 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version1/ChannelCodecs1.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version1/ChannelCodecs1.scala @@ -44,10 +44,9 @@ private[channel] object ChannelCodecs1 { ("chaincode" | bytes32) :: ("depth" | uint16) :: ("path" | keyPathCodec) :: - ("parent" | int64)).xmap( - { case a :: b :: c :: d :: e :: HNil => ExtendedPrivateKey(a, b, c, d, e) }, - { exp: ExtendedPrivateKey => exp.secretkeybytes :: exp.chaincode :: exp.depth :: exp.path :: exp.parent :: HNil } - ) + ("parent" | int64)) + .map { case a :: b :: c :: d :: e :: HNil => ExtendedPrivateKey(a, b, c, d, e) } + .decodeOnly val channelVersionCodec: Codec[ChannelTypes0.ChannelVersion] = bits(ChannelTypes0.ChannelVersion.LENGTH_BITS).as[ChannelTypes0.ChannelVersion] diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version2/ChannelCodecs2.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version2/ChannelCodecs2.scala index 155c648267..0dab10a8fc 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version2/ChannelCodecs2.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version2/ChannelCodecs2.scala @@ -44,10 +44,9 @@ private[channel] object ChannelCodecs2 { ("chaincode" | bytes32) :: ("depth" | uint16) :: ("path" | keyPathCodec) :: - ("parent" | int64)).xmap( - { case a :: b :: c :: d :: e :: HNil => ExtendedPrivateKey(a, b, c, d, e) }, - { exp: ExtendedPrivateKey => exp.secretkeybytes :: exp.chaincode :: exp.depth :: exp.path :: exp.parent :: HNil } - ) + ("parent" | int64)) + .map { case a :: b :: c :: d :: e :: HNil => ExtendedPrivateKey(a, b, c, d, e) } + .decodeOnly val channelVersionCodec: Codec[ChannelTypes0.ChannelVersion] = bits(ChannelTypes0.ChannelVersion.LENGTH_BITS).as[ChannelTypes0.ChannelVersion] From 69589706d3c79f162d43d5620795035c5bfdc26a Mon Sep 17 00:00:00 2001 From: Bastien Teinturier <31281497+t-bast@users.noreply.github.com> Date: Tue, 3 May 2022 10:48:50 +0200 Subject: [PATCH 061/121] Fix flaky MempoolTxMonitor test (#2249) Using the same test probe to generate new blocks and watch the mempool can lead to rare race conditions. --- .../publish/MempoolTxMonitorSpec.scala | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/MempoolTxMonitorSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/MempoolTxMonitorSpec.scala index 05351805ab..f4b53544c0 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/MempoolTxMonitorSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/MempoolTxMonitorSpec.scala @@ -17,7 +17,7 @@ package fr.acinq.eclair.channel.publish import akka.actor.typed.ActorRef -import akka.actor.typed.scaladsl.adapter.{ClassicActorSystemOps, TypedActorRefOps, actorRefAdapter} +import akka.actor.typed.scaladsl.adapter.{ClassicActorSystemOps, actorRefAdapter} import akka.pattern.pipe import akka.testkit.TestProbe import fr.acinq.bitcoin.scalacompat.Crypto.PrivateKey @@ -82,18 +82,18 @@ class MempoolTxMonitorSpec extends TestKitBaseClass with AnyFunSuiteLike with Bi waitTxInMempool(bitcoinClient, tx.txid, probe) // NB: we don't really generate a block, we're testing the case where the tx is still in the mempool. - system.eventStream.publish(CurrentBlockHeight(currentBlockHeight(probe))) - probe.expectMsg(TxInMempool(tx.txid, currentBlockHeight(probe))) + system.eventStream.publish(CurrentBlockHeight(currentBlockHeight())) + probe.expectMsg(TxInMempool(tx.txid, currentBlockHeight())) probe.expectNoMessage(100 millis) assert(TestConstants.Alice.nodeParams.channelConf.minDepthBlocks > 1) generateBlocks(1) - system.eventStream.publish(CurrentBlockHeight(currentBlockHeight(probe))) + system.eventStream.publish(CurrentBlockHeight(currentBlockHeight())) probe.expectMsg(TxRecentlyConfirmed(tx.txid, 1)) probe.expectNoMessage(100 millis) // we wait for more than one confirmation to protect against reorgs generateBlocks(TestConstants.Alice.nodeParams.channelConf.minDepthBlocks - 1) - system.eventStream.publish(CurrentBlockHeight(currentBlockHeight(probe))) + system.eventStream.publish(CurrentBlockHeight(currentBlockHeight())) probe.expectMsg(TxDeeplyBuried(tx)) } @@ -110,7 +110,7 @@ class MempoolTxMonitorSpec extends TestKitBaseClass with AnyFunSuiteLike with Bi waitTxInMempool(bitcoinClient, tx2.txid, probe) generateBlocks(TestConstants.Alice.nodeParams.channelConf.minDepthBlocks) - system.eventStream.publish(CurrentBlockHeight(currentBlockHeight(probe))) + system.eventStream.publish(CurrentBlockHeight(currentBlockHeight())) probe.expectMsg(TxDeeplyBuried(tx2)) } @@ -192,7 +192,7 @@ class MempoolTxMonitorSpec extends TestKitBaseClass with AnyFunSuiteLike with Bi probe.expectMsg(tx2.txid) // When a new block is found, we detect that the transaction has been replaced. - system.eventStream.publish(CurrentBlockHeight(currentBlockHeight(probe))) + system.eventStream.publish(CurrentBlockHeight(currentBlockHeight())) probe.expectMsg(TxRejected(tx1.txid, ConflictingTxUnconfirmed)) } @@ -210,7 +210,7 @@ class MempoolTxMonitorSpec extends TestKitBaseClass with AnyFunSuiteLike with Bi // When a new block is found, we detect that the transaction has been replaced. generateBlocks(1) - system.eventStream.publish(CurrentBlockHeight(currentBlockHeight(probe))) + system.eventStream.publish(CurrentBlockHeight(currentBlockHeight())) probe.expectMsg(TxRejected(tx1.txid, ConflictingTxConfirmed)) } @@ -234,7 +234,7 @@ class MempoolTxMonitorSpec extends TestKitBaseClass with AnyFunSuiteLike with Bi // When a new block is found, we detect that the transaction has been evicted. generateBlocks(1) - system.eventStream.publish(CurrentBlockHeight(currentBlockHeight(probe))) + system.eventStream.publish(CurrentBlockHeight(currentBlockHeight())) probe.expectMsg(TxRejected(tx.txid, InputGone)) } @@ -258,7 +258,7 @@ class MempoolTxMonitorSpec extends TestKitBaseClass with AnyFunSuiteLike with Bi assert(txPublished.desc === "test-tx") generateBlocks(TestConstants.Alice.nodeParams.channelConf.minDepthBlocks) - system.eventStream.publish(CurrentBlockHeight(currentBlockHeight(probe))) + system.eventStream.publish(CurrentBlockHeight(currentBlockHeight())) eventListener.expectMsg(TransactionConfirmed(txPublished.channelId, txPublished.remoteNodeId, tx)) } From 74ef08294a992b646c7b67562aff2d8c5a5fd840 Mon Sep 17 00:00:00 2001 From: Thomas HUET <81159533+thomash-acinq@users.noreply.github.com> Date: Tue, 10 May 2022 10:51:54 +0200 Subject: [PATCH 062/121] Use log probability in path-finding (#2257) Dijkstra's algorithm assumes that each edge has a fixed weight that does not depend on the rest of the path. In our case this assumption is broken when computing a cost for failed payments as it depends on all the edges of the path. We fix this by adding the possibility to use log probabilities which can be added per edge and offer a good approximation as long as the probabilities stay close to 1. Edges still don't have a perfectly fixed weight because the fee of an edge depends on the fees of the previous edges, however since fees are typically low, it shouldn't matter in practice. --- eclair-core/src/main/resources/reference.conf | 19 +++++++--- .../scala/fr/acinq/eclair/NodeParams.scala | 1 + .../remote/EclairInternalsSerializer.scala | 3 +- .../scala/fr/acinq/eclair/router/Graph.scala | 14 +++++--- .../fr/acinq/eclair/router/GraphSpec.scala | 2 +- .../eclair/router/RouteCalculationSpec.scala | 36 ++++++++++++++----- 6 files changed, 55 insertions(+), 20 deletions(-) diff --git a/eclair-core/src/main/resources/reference.conf b/eclair-core/src/main/resources/reference.conf index 295ba8a30b..7284a66ae2 100644 --- a/eclair-core/src/main/resources/reference.conf +++ b/eclair-core/src/main/resources/reference.conf @@ -256,18 +256,27 @@ eclair { channel-age = 0.4 // when computing the weight for a channel, consider its AGE in this proportion channel-capacity = 0.55 // when computing the weight for a channel, consider its CAPACITY in this proportion } + + hop-cost { + // virtual fee for additional hops: how much you are willing to pay to get one less hop in the payment path + fee-base-msat = 500 + fee-proportional-millionths = 200 + } + locked-funds-risk = 1e-8 // msat per msat locked per block. It should be your expected interest rate per block multiplied by the probability that something goes wrong and your funds stay locked. // 1e-8 corresponds to an interest rate of ~5% per year (1e-6 per block) and a probability of 1% that the channel will fail and our funds will be locked. + // virtual fee for failed payments: how much you are willing to pay to get one less failed payment attempt + // ignored if use-ratio = true failure-cost { fee-base-msat = 2000 fee-proportional-millionths = 500 } - hop-cost { - // virtual fee for additional hops: how much you are willing to pay to get one less hop in the payment path - fee-base-msat = 500 - fee-proportional-millionths = 200 - } + // Using a failure cost breaks Dijkstra (the path returned is no longer guaranteed to be shortest one), if + // that's a concern, you can penalize paths with a low success chance by using the logarithm of the probability + // of success. It satisfies Dijkstra's requirements and is a very good approximation for paths with a high + // probability of success, however is penalizes less the paths with a low probability of success. + use-log-probability = false mpp { min-amount-satoshis = 15000 // minimum amount sent via partial HTLCs diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala index ae01b21058..a74a9373c9 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala @@ -365,6 +365,7 @@ object NodeParams extends Logging { lockedFundsRisk = config.getDouble("locked-funds-risk"), failureCost = getRelayFees(config.getConfig("failure-cost")), hopCost = getRelayFees(config.getConfig("hop-cost")), + useLogProbability = config.getBoolean("use-log-probability"), )) }, mpp = MultiPartParams( diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/remote/EclairInternalsSerializer.scala b/eclair-core/src/main/scala/fr/acinq/eclair/remote/EclairInternalsSerializer.scala index 590c4a1848..d343fb6bc3 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/remote/EclairInternalsSerializer.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/remote/EclairInternalsSerializer.scala @@ -67,7 +67,8 @@ object EclairInternalsSerializer { val heuristicsConstantsCodec: Codec[HeuristicsConstants] = ( ("lockedFundsRisk" | double) :: ("failureCost" | relayFeesCodec) :: - ("hopCost" | relayFeesCodec)).as[HeuristicsConstants] + ("hopCost" | relayFeesCodec) :: + ("useLogProbability" | bool(8))).as[HeuristicsConstants] val multiPartParamsCodec: Codec[MultiPartParams] = ( ("minPartAmount" | millisatoshi) :: diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/Graph.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/Graph.scala index aff89f0c14..65e52fdedb 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/Graph.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/Graph.scala @@ -65,7 +65,7 @@ object Graph { * @param failureCost fee for a failed attempt * @param hopCost virtual fee per hop (how much we're willing to pay to make the route one hop shorter) */ - case class HeuristicsConstants(lockedFundsRisk: Double, failureCost: RelayFees, hopCost: RelayFees) + case class HeuristicsConstants(lockedFundsRisk: Double, failureCost: RelayFees, hopCost: RelayFees, useLogProbability: Boolean) case class WeightedNode(key: PublicKey, weight: RichWeight) @@ -329,7 +329,6 @@ object Graph { case Right(heuristicsConstants) => val hopCost = nodeFee(heuristicsConstants.hopCost, prev.amount) val totalHopsCost = prev.virtualFees + hopCost - val riskCost = totalAmount.toLong * totalCltv.toInt * heuristicsConstants.lockedFundsRisk // If the edge was added by the invoice, it is assumed that it can route the payment. // If we know the balance of the channel, then we will check separately that it can relay the payment. val successProbability = if (edge.update.chainHash == ByteVector32.Zeroes || edge.balance_opt.nonEmpty) 1.0 else 1.0 - prev.amount.toLong.toDouble / edge.capacity.toMilliSatoshi.toLong.toDouble @@ -338,8 +337,15 @@ object Graph { } val totalSuccessProbability = prev.successProbability * successProbability val failureCost = nodeFee(heuristicsConstants.failureCost, totalAmount) - val weight = totalFees.toLong + totalHopsCost.toLong + riskCost + failureCost.toLong / totalSuccessProbability - RichWeight(totalAmount, prev.length + 1, totalCltv, totalSuccessProbability, totalFees, totalHopsCost, weight) + if (heuristicsConstants.useLogProbability) { + val riskCost = totalAmount.toLong * cltv.toInt * heuristicsConstants.lockedFundsRisk + val weight = prev.weight + fee.toLong + hopCost.toLong + riskCost - failureCost.toLong * math.log(successProbability) + RichWeight(totalAmount, prev.length + 1, totalCltv, totalSuccessProbability, totalFees, totalHopsCost, weight) + } else { + val totalRiskCost = totalAmount.toLong * totalCltv.toInt * heuristicsConstants.lockedFundsRisk + val weight = totalFees.toLong + totalHopsCost.toLong + totalRiskCost + failureCost.toLong / totalSuccessProbability + RichWeight(totalAmount, prev.length + 1, totalCltv, totalSuccessProbability, totalFees, totalHopsCost, weight) + } } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/GraphSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/GraphSpec.scala index 503d07a4f4..375f14423d 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/GraphSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/GraphSpec.scala @@ -257,7 +257,7 @@ class GraphSpec extends AnyFunSuite { val path :: Nil = yenKshortestPaths(graph, a, e, 100000000 msat, Set.empty, Set.empty, Set.empty, 1, - Right(HeuristicsConstants(1.0E-8, RelayFees(2000 msat, 500), RelayFees(50 msat, 20))), + Right(HeuristicsConstants(1.0E-8, RelayFees(2000 msat, 500), RelayFees(50 msat, 20), useLogProbability = true)), BlockHeight(714930), _ => true, includeLocalChannelCost = true) assert(path.path == Seq(edgeAB, edgeBC, edgeCE)) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/RouteCalculationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/RouteCalculationSpec.scala index b551dbec1c..6ecb23ed5e 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/RouteCalculationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/RouteCalculationSpec.scala @@ -1792,16 +1792,32 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { makeEdge(4L, d, b, 400 msat, 500, capacity = (DEFAULT_AMOUNT_MSAT * 3).truncateToSatoshi), )) - val hc = HeuristicsConstants( - lockedFundsRisk = 0.0, - failureCost = RelayFees(1000 msat, 500), - hopCost = RelayFees(0 msat, 0), - ) - val Success(routes) = findRoute(g, start, b, DEFAULT_AMOUNT_MSAT, 100000000 msat, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.copy(heuristics = Right(hc)), currentBlockHeight = BlockHeight(400000)) - assert(routes.distinct.length == 1) - val route :: Nil = routes - assert(route2Ids(route) === 0 :: 2 :: 3 :: 4 :: Nil) + { + val hc = HeuristicsConstants( + lockedFundsRisk = 0.0, + failureCost = RelayFees(1000 msat, 500), + hopCost = RelayFees(0 msat, 0), + useLogProbability = false, + ) + val Success(routes) = findRoute(g, start, b, DEFAULT_AMOUNT_MSAT, 100000000 msat, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.copy(heuristics = Right(hc)), currentBlockHeight = BlockHeight(400000)) + assert(routes.distinct.length == 1) + val route :: Nil = routes + assert(route2Ids(route) === 0 :: 2 :: 3 :: 4 :: Nil) + } + + { + val hc = HeuristicsConstants( + lockedFundsRisk = 0.0, + failureCost = RelayFees(10000 msat, 1000), + hopCost = RelayFees(0 msat, 0), + useLogProbability = true, + ) + val Success(routes) = findRoute(g, start, b, DEFAULT_AMOUNT_MSAT, 100000000 msat, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.copy(heuristics = Right(hc)), currentBlockHeight = BlockHeight(400000)) + assert(routes.distinct.length == 1) + val route :: Nil = routes + assert(route2Ids(route) === 0 :: 2 :: 3 :: 4 :: Nil) + } } test("no path that can get our funds stuck for too long") { @@ -1822,6 +1838,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { lockedFundsRisk = 1e-7, failureCost = RelayFees(0 msat, 0), hopCost = RelayFees(0 msat, 0), + useLogProbability = true, ) val Success(routes) = findRoute(g, start, b, DEFAULT_AMOUNT_MSAT, 100000000 msat, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.copy(heuristics = Right(hc)), currentBlockHeight = BlockHeight(400000)) assert(routes.distinct.length == 1) @@ -1841,6 +1858,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { lockedFundsRisk = 1e-7, failureCost = RelayFees(0 msat, 0), hopCost = RelayFees(0 msat, 0), + useLogProbability = true, ) val Success(routes) = findRoute(g, a, c, DEFAULT_AMOUNT_MSAT, 100000000 msat, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.copy(heuristics = Right(hc)), currentBlockHeight = BlockHeight(400000)) assert(routes.distinct.length == 1) From ee74f10a8d956196dcc62ad297e83769aa7a2a82 Mon Sep 17 00:00:00 2001 From: Bastien Teinturier <31281497+t-bast@users.noreply.github.com> Date: Wed, 11 May 2022 14:02:21 +0200 Subject: [PATCH 063/121] Stop compressing with zlib (#2244) While zlib provides good compression results for gossip, it's a dependency that had a few important CVEs in the past. Some implementations are reluctant to import it, so we decided to remove it from the specification in https://github.com/lightning/bolts/pull/981 We stop compressing with zlib and only send uncompressed results, while still supporting receiving compressed data. We will remove support for decompression once our monitoring indicates that we stopped receiving compressed data. We also reduce the maximum allowed chunk size. The previous calculation was wrong and could lead to messages bigger than 65kB. --- eclair-core/src/main/resources/reference.conf | 1 - .../src/main/scala/fr/acinq/eclair/NodeParams.scala | 7 +------ .../main/scala/fr/acinq/eclair/router/Router.scala | 5 ++++- .../main/scala/fr/acinq/eclair/router/Sync.scala | 6 +++--- .../eclair/router/ChannelRangeQueriesSpec.scala | 13 ++++++++++++- 5 files changed, 20 insertions(+), 12 deletions(-) diff --git a/eclair-core/src/main/resources/reference.conf b/eclair-core/src/main/resources/reference.conf index 7284a66ae2..bb29d7f0d5 100644 --- a/eclair-core/src/main/resources/reference.conf +++ b/eclair-core/src/main/resources/reference.conf @@ -230,7 +230,6 @@ eclair { sync { request-node-announcements = true // if true we will ask for node announcements when we receive channel ids that we don't know - encoding-type = zlib // encoding for short_channel_ids and timestamps in query channel sync messages; other possible value is "uncompressed" channel-range-chunk-size = 1500 // max number of short_channel_ids (+ timestamps + checksums) in reply_channel_range *do not change this unless you know what you are doing* channel-query-chunk-size = 100 // max number of short_channel_ids in query_short_channel_ids *do not change this unless you know what you are doing* } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala index a74a9373c9..ba73517dbf 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala @@ -385,11 +385,6 @@ object NodeParams extends Logging { case "stop" => UnhandledExceptionStrategy.Stop } - val routerSyncEncodingType = config.getString("router.sync.encoding-type") match { - case "uncompressed" => EncodingType.UNCOMPRESSED - case "zlib" => EncodingType.COMPRESSED_ZLIB - } - val onionMessageRelayPolicy: RelayPolicy = config.getString("onion-messages.relay-policy") match { case "no-relay" => NoRelay case "channels-only" => RelayChannelsOnly @@ -494,7 +489,7 @@ object NodeParams extends Logging { channelExcludeDuration = FiniteDuration(config.getDuration("router.channel-exclude-duration").getSeconds, TimeUnit.SECONDS), routerBroadcastInterval = FiniteDuration(config.getDuration("router.broadcast-interval").getSeconds, TimeUnit.SECONDS), requestNodeAnnouncements = config.getBoolean("router.sync.request-node-announcements"), - encodingType = routerSyncEncodingType, + encodingType = EncodingType.UNCOMPRESSED, channelRangeChunkSize = config.getInt("router.sync.channel-range-chunk-size"), channelQueryChunkSize = config.getInt("router.sync.channel-query-chunk-size"), pathFindingExperimentConf = getPathFindingExperimentConf(config.getConfig("router.path-finding.experiments")) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala index 20745ab0e7..7a6f1fcc4e 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala @@ -304,7 +304,10 @@ object Router { encodingType: EncodingType, channelRangeChunkSize: Int, channelQueryChunkSize: Int, - pathFindingExperimentConf: PathFindingExperimentConf) + pathFindingExperimentConf: PathFindingExperimentConf) { + require(channelRangeChunkSize <= Sync.MAXIMUM_CHUNK_SIZE, "channel range chunk size exceeds the size of a lightning message") + require(channelQueryChunkSize <= Sync.MAXIMUM_CHUNK_SIZE, "channel query chunk size exceeds the size of a lightning message") + } // @formatter:off case class ChannelDesc(shortChannelId: ShortChannelId, a: PublicKey, b: PublicKey) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/Sync.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/Sync.scala index d1c7f1b990..63e65b3dcc 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/Sync.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/Sync.scala @@ -38,9 +38,9 @@ object Sync { // maximum number of ids we can keep in a single chunk and still have an encoded reply that is smaller than 65Kb // please note that: // - this is based on the worst case scenario where peer want timestamps and checksums and the reply is not compressed - // - the maximum number of public channels in a single block so far is less than 300, and the maximum number of tx per block - // almost never exceeds 2800 so this is not a real limitation yet - val MAXIMUM_CHUNK_SIZE = 3200 + // - the maximum number of public channels in a single block so far is less than 300, and the maximum number of tx per + // block almost never exceeds 2800 so this should very rarely be limiting + val MAXIMUM_CHUNK_SIZE = 2700 def handleSendChannelQuery(d: Data, s: SendChannelQuery)(implicit ctx: ActorContext, log: LoggingAdapter): Data = { implicit val sender: ActorRef = ctx.self // necessary to preserve origin when sending messages to other actors diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRangeQueriesSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRangeQueriesSpec.scala index 9320739342..50aea8114d 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRangeQueriesSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRangeQueriesSpec.scala @@ -22,7 +22,7 @@ import fr.acinq.eclair.router.Sync._ import fr.acinq.eclair.wire.protocol.QueryChannelRangeTlv.QueryFlags import fr.acinq.eclair.wire.protocol.QueryShortChannelIdsTlv.QueryFlagType._ import fr.acinq.eclair.wire.protocol.ReplyChannelRangeTlv._ -import fr.acinq.eclair.wire.protocol.{EncodedShortChannelIds, EncodingType, ReplyChannelRange} +import fr.acinq.eclair.wire.protocol.{EncodedShortChannelIds, EncodingType, LightningMessageCodecs, ReplyChannelRange} import fr.acinq.eclair.{BlockHeight, MilliSatoshiLong, ShortChannelId, TimestampSecond, TimestampSecondLong, randomKey} import org.scalatest.funsuite.AnyFunSuite import scodec.bits.ByteVector @@ -356,6 +356,16 @@ class ChannelRangeQueriesSpec extends AnyFunSuite { } } + test("encode maximum size reply_channel_range") { + val scids = (1 to Sync.MAXIMUM_CHUNK_SIZE).map(i => ShortChannelId(i)).toList + val timestamps = (1 to Sync.MAXIMUM_CHUNK_SIZE).map(i => Timestamps(i.unixsec, (i + 1).unixsec)).toList + val checksums = (1 to Sync.MAXIMUM_CHUNK_SIZE).map(i => Checksums(i, i + 1)).toList + val reply = ReplyChannelRange(Block.RegtestGenesisBlock.hash, BlockHeight(0), 100, 0, EncodedShortChannelIds(EncodingType.UNCOMPRESSED, scids), Some(EncodedTimestamps(EncodingType.UNCOMPRESSED, timestamps)), Some(EncodedChecksums(checksums))) + val encoded = LightningMessageCodecs.lightningMessageCodec.encode(reply) + assert(encoded.isSuccessful) + assert(encoded.require.bytes.length <= 0xffff) + } + test("do not encode empty lists as COMPRESSED_ZLIB") { { val reply = buildReplyChannelRange(ShortChannelIdsChunk(BlockHeight(0), 42, Nil), syncComplete = true, Block.RegtestGenesisBlock.hash, EncodingType.COMPRESSED_ZLIB, Some(QueryFlags(QueryFlags.WANT_ALL)), SortedMap()) @@ -374,4 +384,5 @@ class ChannelRangeQueriesSpec extends AnyFunSuite { assert(reply == ReplyChannelRange(Block.RegtestGenesisBlock.hash, BlockHeight(0), 42L, 1, EncodedShortChannelIds(EncodingType.UNCOMPRESSED, Nil), None, None)) } } + } From 1605c0435d579f00f8b134d499a8be01efebd7bb Mon Sep 17 00:00:00 2001 From: Pierre-Marie Padiou Date: Thu, 12 May 2022 17:37:09 +0200 Subject: [PATCH 064/121] Introduce `ChannelRelayParams` in the graph (#2264) We have two sources for channel routing parameters: - channel updates - routing hints in invoices Instead of generating fake `channel_update`s from routing hints, we define a `ChannelRelayParams` that can be built from announcements or routing hints. This is cleaner but also is a first step to decorrelate the identifier of a channel in our graph, from whatever identifies the source. --- .../scala/fr/acinq/eclair/db/PaymentsDb.scala | 2 +- .../acinq/eclair/json/JsonSerializers.scala | 17 ++-- .../main/scala/fr/acinq/eclair/package.scala | 2 - .../acinq/eclair/payment/Bolt11Invoice.scala | 5 +- .../acinq/eclair/payment/PaymentEvents.scala | 4 +- .../acinq/eclair/payment/PaymentPacket.scala | 2 +- .../eclair/payment/relay/ChannelRelay.scala | 4 +- .../payment/send/PaymentLifecycle.scala | 3 +- .../scala/fr/acinq/eclair/router/Graph.scala | 73 +++++++++++------- .../eclair/router/RouteCalculation.scala | 54 +++++-------- .../scala/fr/acinq/eclair/router/Router.scala | 77 ++++++++++++++----- .../fr/acinq/eclair/router/Validation.scala | 28 ++++--- .../wire/protocol/LightningMessageTypes.scala | 3 + .../fr/acinq/eclair/channel/FuzzySpec.scala | 2 +- .../ChannelStateTestsHelperMethods.scala | 2 +- .../channel/states/f/ShutdownStateSpec.scala | 4 +- .../fr/acinq/eclair/db/PaymentsDbSpec.scala | 4 +- .../MultiPartPaymentLifecycleSpec.scala | 30 ++++---- .../eclair/payment/PaymentLifecycleSpec.scala | 22 +++--- .../eclair/payment/PaymentPacketSpec.scala | 33 ++++---- .../payment/PostRestartHtlcCleanerSpec.scala | 3 +- .../eclair/payment/relay/RelayerSpec.scala | 5 +- .../acinq/eclair/router/BaseRouterSpec.scala | 19 +++-- .../fr/acinq/eclair/router/GraphSpec.scala | 2 +- .../eclair/router/RouteCalculationSpec.scala | 45 +++++------ .../fr/acinq/eclair/router/RouterSpec.scala | 35 +++++---- .../src/test/resources/api/findroute-full | 2 +- .../fr/acinq/eclair/api/ApiServiceSpec.scala | 10 ++- 28 files changed, 275 insertions(+), 217 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala index 325382558f..3b520f2257 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala @@ -199,7 +199,7 @@ case class HopSummary(nodeId: PublicKey, nextNodeId: PublicKey, shortChannelId: object HopSummary { def apply(h: Hop): HopSummary = { val shortChannelId = h match { - case ChannelHop(_, _, channelUpdate) => Some(channelUpdate.shortChannelId) + case ch: ChannelHop => Some(ch.shortChannelId) case _: NodeHop => None } HopSummary(h.nodeId, h.nextNodeId, shortChannelId) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/json/JsonSerializers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/json/JsonSerializers.scala index b696715fe7..d6941bfb84 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/json/JsonSerializers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/json/JsonSerializers.scala @@ -31,12 +31,12 @@ import fr.acinq.eclair.io.Peer import fr.acinq.eclair.message.OnionMessages import fr.acinq.eclair.payment.PaymentFailure.PaymentFailedSummary import fr.acinq.eclair.payment._ -import fr.acinq.eclair.router.Router.{ChannelHop, Route} +import fr.acinq.eclair.router.Router.{ChannelHop, ChannelRelayParams, Route} import fr.acinq.eclair.transactions.DirectedHtlc import fr.acinq.eclair.transactions.Transactions._ import fr.acinq.eclair.wire.protocol.MessageOnionCodecs.blindedRouteCodec import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, FeatureSupport, Feature, MilliSatoshi, ShortChannelId, TimestampMilli, TimestampSecond, UInt64, UnknownFeature} +import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, Feature, FeatureSupport, MilliSatoshi, ShortChannelId, TimestampMilli, TimestampSecond, UInt64, UnknownFeature} import org.json4s import org.json4s.JsonAST._ import org.json4s.jackson.Serialization @@ -290,8 +290,9 @@ object ColorSerializer extends MinimalSerializer({ }) // @formatter:off -private case class RouteFullJson(amount: MilliSatoshi, hops: Seq[ChannelHop]) -object RouteFullSerializer extends ConvertClassSerializer[Route](route => RouteFullJson(route.amount, route.hops)) +private case class ChannelHopJson(nodeId: PublicKey, nextNodeId: PublicKey, source: ChannelRelayParams) +private case class RouteFullJson(amount: MilliSatoshi, hops: Seq[ChannelHopJson]) +object RouteFullSerializer extends ConvertClassSerializer[Route](route => RouteFullJson(route.amount, route.hops.map(h => ChannelHopJson(h.nodeId, h.nextNodeId, h.params)))) private case class RouteNodeIdsJson(amount: MilliSatoshi, nodeIds: Seq[PublicKey]) object RouteNodeIdsSerializer extends ConvertClassSerializer[Route](route => { @@ -303,7 +304,7 @@ object RouteNodeIdsSerializer extends ConvertClassSerializer[Route](route => { }) private case class RouteShortChannelIdsJson(amount: MilliSatoshi, shortChannelIds: Seq[ShortChannelId]) -object RouteShortChannelIdsSerializer extends ConvertClassSerializer[Route](route => RouteShortChannelIdsJson(route.amount, route.hops.map(_.lastUpdate.shortChannelId))) +object RouteShortChannelIdsSerializer extends ConvertClassSerializer[Route](route => RouteShortChannelIdsJson(route.amount, route.hops.map(_.shortChannelId))) // @formatter:on // @formatter:off @@ -481,6 +482,11 @@ object CustomTypeHints { classOf[MessageReceivedJson] -> "onion-message-received" )) + val channelSources: CustomTypeHints = CustomTypeHints(Map( + classOf[ChannelRelayParams.FromAnnouncement] -> "announcement", + classOf[ChannelRelayParams.FromHint] -> "hint" + )) + val channelStates: ShortTypeHints = ShortTypeHints( List( classOf[Nothing], @@ -508,6 +514,7 @@ object JsonSerializers { CustomTypeHints.outgoingPaymentStatus + CustomTypeHints.paymentEvent + CustomTypeHints.onionMessageEvent + + CustomTypeHints.channelSources + CustomTypeHints.channelStates + ByteVectorSerializer + ByteVector32Serializer + diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/package.scala b/eclair-core/src/main/scala/fr/acinq/eclair/package.scala index 4a2cbb3490..cf87799532 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/package.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/package.scala @@ -75,8 +75,6 @@ package object eclair { def nodeFee(relayFees: RelayFees, paymentAmount: MilliSatoshi): MilliSatoshi = nodeFee(relayFees.feeBase, relayFees.feeProportionalMillionths, paymentAmount) - def nodeFee(channelUpdate: ChannelUpdate, paymentAmount: MilliSatoshi): MilliSatoshi = nodeFee(channelUpdate.feeBaseMsat, channelUpdate.feeProportionalMillionths, paymentAmount) - /** * @param address base58 of bech32 address * @param chainHash hash of the chain we're on, which will be checked against the input address diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Bolt11Invoice.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Bolt11Invoice.scala index 2bcab282c4..153c2b72d7 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Bolt11Invoice.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Bolt11Invoice.scala @@ -19,6 +19,7 @@ package fr.acinq.eclair.payment import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, ByteVector64, Crypto} import fr.acinq.bitcoin.{Base58, Base58Check, Bech32} +import fr.acinq.eclair.payment.relay.Relayer import fr.acinq.eclair.{CltvExpiryDelta, Feature, FeatureSupport, Features, InvoiceFeature, MilliSatoshi, MilliSatoshiLong, ShortChannelId, TimestampSecond, randomBytes32} import scodec.bits.{BitVector, ByteOrdering, ByteVector} import scodec.codecs.{list, ubyte} @@ -332,7 +333,9 @@ object Bolt11Invoice { * @param feeProportionalMillionths node proportional fee * @param cltvExpiryDelta node cltv expiry delta */ - case class ExtraHop(nodeId: PublicKey, shortChannelId: ShortChannelId, feeBase: MilliSatoshi, feeProportionalMillionths: Long, cltvExpiryDelta: CltvExpiryDelta) + case class ExtraHop(nodeId: PublicKey, shortChannelId: ShortChannelId, feeBase: MilliSatoshi, feeProportionalMillionths: Long, cltvExpiryDelta: CltvExpiryDelta) { + def relayFees: Relayer.RelayFees = Relayer.RelayFees(feeBase = feeBase, feeProportionalMillionths = feeProportionalMillionths) + } /** diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentEvents.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentEvents.scala index 64ec8c6a82..d638722762 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentEvents.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentEvents.scala @@ -180,7 +180,7 @@ object PaymentFailure { /** Ignore the channel outgoing from the given nodeId in the given route. */ private def ignoreNodeOutgoingChannel(nodeId: PublicKey, hops: Seq[Hop], ignore: Ignore): Ignore = { hops.collectFirst { - case hop: ChannelHop if hop.nodeId == nodeId => ChannelDesc(hop.lastUpdate.shortChannelId, hop.nodeId, hop.nextNodeId) + case hop: ChannelHop if hop.nodeId == nodeId => ChannelDesc(hop.shortChannelId, hop.nodeId, hop.nextNodeId) } match { case Some(faultyChannel) => ignore + faultyChannel case None => ignore @@ -219,7 +219,7 @@ object PaymentFailure { ignore ++ blacklist case LocalFailure(_, hops, _) => hops.headOption match { case Some(hop: ChannelHop) => - val faultyChannel = ChannelDesc(hop.lastUpdate.shortChannelId, hop.nodeId, hop.nextNodeId) + val faultyChannel = ChannelDesc(hop.shortChannelId, hop.nodeId, hop.nextNodeId) ignore + faultyChannel case _ => ignore } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentPacket.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentPacket.scala index 38094ab888..36ba5769a8 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentPacket.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentPacket.scala @@ -170,7 +170,7 @@ object OutgoingPaymentPacket { hops.reverse.foldLeft((finalPayload.amount, finalPayload.expiry, Seq[PaymentOnion.PerHopPayload](finalPayload))) { case ((amount, expiry, payloads), hop) => val payload = hop match { - case hop: ChannelHop => PaymentOnion.ChannelRelayTlvPayload(hop.lastUpdate.shortChannelId, amount, expiry) + case hop: ChannelHop => PaymentOnion.ChannelRelayTlvPayload(hop.shortChannelId, amount, expiry) case hop: NodeHop => PaymentOnion.createNodeRelayPayload(amount, expiry, hop.nextNodeId) } (amount + hop.fee(amount), expiry + hop.cltvExpiryDelta, payload +: payloads) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/ChannelRelay.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/ChannelRelay.scala index 11c3e9dd17..818ab5b80b 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/ChannelRelay.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/ChannelRelay.scala @@ -261,10 +261,10 @@ class ChannelRelay private(nodeParams: NodeParams, RelayFailure(CMD_FAIL_HTLC(add.id, Right(AmountBelowMinimum(payload.amountToForward, channelUpdate)), commit = true)) case Some(channelUpdate) if r.expiryDelta < channelUpdate.cltvExpiryDelta => RelayFailure(CMD_FAIL_HTLC(add.id, Right(IncorrectCltvExpiry(payload.outgoingCltv, channelUpdate)), commit = true)) - case Some(channelUpdate) if r.relayFeeMsat < nodeFee(channelUpdate, payload.amountToForward) && + case Some(channelUpdate) if r.relayFeeMsat < nodeFee(channelUpdate.relayFees, payload.amountToForward) && // fees also do not satisfy the previous channel update for `enforcementDelay` seconds after current update (TimestampSecond.now() - channelUpdate.timestamp > nodeParams.relayParams.enforcementDelay || - outgoingChannel_opt.flatMap(_.prevChannelUpdate).forall(c => r.relayFeeMsat < nodeFee(c, payload.amountToForward))) => + outgoingChannel_opt.flatMap(_.prevChannelUpdate).forall(c => r.relayFeeMsat < nodeFee(c.relayFees, payload.amountToForward))) => RelayFailure(CMD_FAIL_HTLC(add.id, Right(FeeInsufficient(add.amountMsat, channelUpdate)), commit = true)) case Some(channelUpdate) => val origin = Origin.ChannelRelayedHot(addResponseAdapter.toClassic, add, payload.amountToForward) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentLifecycle.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentLifecycle.scala index 249b2e9616..b2256c42a0 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentLifecycle.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentLifecycle.scala @@ -78,7 +78,7 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A log.info(s"route found: attempt=${failures.size + 1}/${c.maxAttempts} route=${route.printNodes()} channels=${route.printChannels()}") OutgoingPaymentPacket.buildCommand(self, cfg.upstream, paymentHash, route.hops, c.finalPayload) match { case Success((cmd, sharedSecrets)) => - register ! Register.ForwardShortId(self, route.hops.head.lastUpdate.shortChannelId, cmd) + register ! Register.ForwardShortId(self, route.hops.head.shortChannelId, cmd) goto(WAITING_FOR_PAYMENT_COMPLETE) using WaitingForComplete(c, cmd, failures, sharedSecrets, ignore, route) case Failure(t) => log.warning("cannot send outgoing payment: {}", t.getMessage) @@ -242,6 +242,7 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A * @return updated routing hints if applicable. */ private def handleUpdate(nodeId: PublicKey, failure: Update, data: WaitingForComplete): Seq[Seq[ExtraHop]] = { + // TODO: properly handle updates to channels provided as routing hints in the invoice data.route.getChannelUpdateForNode(nodeId) match { case Some(u) if u.shortChannelId != failure.update.shortChannelId => // it is possible that nodes in the route prefer using a different channel (to the same N+1 node) than the one we requested, that's fine diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/Graph.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/Graph.scala index 65e52fdedb..2cf45c35e2 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/Graph.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/Graph.scala @@ -17,7 +17,7 @@ package fr.acinq.eclair.router import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey -import fr.acinq.bitcoin.scalacompat.{Btc, ByteVector32, MilliBtc, Satoshi} +import fr.acinq.bitcoin.scalacompat.{Btc, MilliBtc, Satoshi, SatoshiLong} import fr.acinq.eclair._ import fr.acinq.eclair.payment.relay.Relayer.RelayFees import fr.acinq.eclair.router.Graph.GraphStructure.{DirectedGraph, GraphEdge} @@ -248,8 +248,8 @@ object Graph { val neighbor = edge.desc.a if (current.weight.amount <= edge.capacity && edge.balance_opt.forall(current.weight.amount <= _) && - edge.update.htlcMaximumMsat.forall(current.weight.amount <= _) && - current.weight.amount >= edge.update.htlcMinimumMsat && + edge.params.htlcMaximum_opt.forall(current.weight.amount <= _) && + current.weight.amount >= edge.params.htlcMinimum && !ignoredEdges.contains(edge.desc) && !ignoredVertices.contains(neighbor)) { // NB: this contains the amount (including fees) that will need to be sent to `neighbor`, but the amount that @@ -302,7 +302,7 @@ object Graph { val totalAmount = if (edge.desc.a == sender && !includeLocalChannelCost) prev.amount else addEdgeFees(edge, prev.amount) val fee = totalAmount - prev.amount val totalFees = prev.fees + fee - val cltv = if (edge.desc.a == sender && !includeLocalChannelCost) CltvExpiryDelta(0) else edge.update.cltvExpiryDelta + val cltv = if (edge.desc.a == sender && !includeLocalChannelCost) CltvExpiryDelta(0) else edge.params.cltvExpiryDelta val totalCltv = prev.cltv + cltv weightRatios match { case Left(weightRatios) => @@ -320,7 +320,7 @@ object Graph { else 1 - normalize(edgeMaxCapacity.toLong.toDouble, CAPACITY_CHANNEL_LOW.toLong.toDouble, CAPACITY_CHANNEL_HIGH.toLong.toDouble) // Every edge is weighted by its cltv-delta value, normalized - val cltvFactor = normalize(edge.update.cltvExpiryDelta.toInt, CLTV_LOW, CLTV_HIGH) + val cltvFactor = normalize(edge.params.cltvExpiryDelta.toInt, CLTV_LOW, CLTV_HIGH) // NB we're guaranteed to have weightRatios and factors > 0 val factor = weightRatios.baseFactor + (cltvFactor * weightRatios.cltvDeltaFactor) + (ageFactor * weightRatios.ageFactor) + (capFactor * weightRatios.capacityFactor) @@ -329,9 +329,8 @@ object Graph { case Right(heuristicsConstants) => val hopCost = nodeFee(heuristicsConstants.hopCost, prev.amount) val totalHopsCost = prev.virtualFees + hopCost - // If the edge was added by the invoice, it is assumed that it can route the payment. // If we know the balance of the channel, then we will check separately that it can relay the payment. - val successProbability = if (edge.update.chainHash == ByteVector32.Zeroes || edge.balance_opt.nonEmpty) 1.0 else 1.0 - prev.amount.toLong.toDouble / edge.capacity.toMilliSatoshi.toLong.toDouble + val successProbability = if (edge.balance_opt.nonEmpty) 1.0 else 1.0 - prev.amount.toLong.toDouble / edge.capacity.toMilliSatoshi.toLong.toDouble if (successProbability < 0) { throw NegativeProbability(edge, prev, heuristicsConstants) } @@ -358,7 +357,7 @@ object Graph { * @return the new amount updated with the necessary fees for this edge */ private def addEdgeFees(edge: GraphEdge, amountToForward: MilliSatoshi): MilliSatoshi = { - amountToForward + nodeFee(edge.update, amountToForward) + amountToForward + edge.params.fee(amountToForward) } /** Validate that all edges along the path can relay the amount with fees. */ @@ -370,8 +369,8 @@ object Graph { case Some(edge) => val canRelayAmount = amount <= edge.capacity && edge.balance_opt.forall(amount <= _) && - edge.update.htlcMaximumMsat.forall(amount <= _) && - edge.update.htlcMinimumMsat <= amount + edge.params.htlcMaximum_opt.forall(amount <= _) && + edge.params.htlcMinimum <= amount if (canRelayAmount) validateReversePath(path.tail, addEdgeFees(edge, amount)) else false } @@ -423,27 +422,49 @@ object Graph { * Representation of an edge of the graph * * @param desc channel description - * @param update channel info + * @param params source of the channel parameters: can be a channel_update or hints from an invoice * @param capacity channel capacity * @param balance_opt (optional) available balance that can be sent through this edge */ - case class GraphEdge(desc: ChannelDesc, update: ChannelUpdate, capacity: Satoshi, balance_opt: Option[MilliSatoshi]) { + case class GraphEdge private(desc: ChannelDesc, params: ChannelRelayParams, capacity: Satoshi, balance_opt: Option[MilliSatoshi]) { def maxHtlcAmount(reservedCapacity: MilliSatoshi): MilliSatoshi = Seq( balance_opt.map(balance => balance - reservedCapacity), - update.htlcMaximumMsat, + params.htlcMaximum_opt, Some(capacity.toMilliSatoshi - reservedCapacity) ).flatten.min.max(0 msat) - def fee(amount: MilliSatoshi): MilliSatoshi = nodeFee(update, amount) + def fee(amount: MilliSatoshi): MilliSatoshi = params.fee(amount) + } + object GraphEdge { + def apply(u: ChannelUpdate, pc: PublicChannel): GraphEdge = GraphEdge( + desc = ChannelDesc(u, pc.ann), + params = ChannelRelayParams.FromAnnouncement(u), + capacity = pc.capacity, + balance_opt = pc.getBalanceSameSideAs(u) + ) + + def apply(u: ChannelUpdate, pc: PrivateChannel): GraphEdge = GraphEdge( + desc = ChannelDesc(u, pc), + params = ChannelRelayParams.FromAnnouncement(u), + capacity = pc.capacity, + balance_opt = pc.getBalanceSameSideAs(u) + ) + + def apply(ac: AssistedChannel): GraphEdge = GraphEdge( + desc = ChannelDesc(ac.shortChannelId, ac.nodeId, ac.nextNodeId), + params = ac.params, + // Bolt 11 routing hints don't include the channel's capacity, so we round up the maximum htlc amount + capacity = ac.params.htlcMaximum.truncateToSatoshi + 1.sat, + // we assume channels provided as hints have enough balance to handle the payment + balance_opt = Some(ac.params.htlcMaximum) + ) } /** A graph data structure that uses an adjacency list, stores the incoming edges of the neighbors */ case class DirectedGraph(private val vertices: Map[PublicKey, List[GraphEdge]]) { - def addEdge(d: ChannelDesc, u: ChannelUpdate, capacity: Satoshi, balance_opt: Option[MilliSatoshi] = None): DirectedGraph = addEdge(GraphEdge(d, u, capacity, balance_opt)) - def addEdges(edges: Iterable[GraphEdge]): DirectedGraph = edges.foldLeft(this)((acc, edge) => acc.addEdge(edge)) /** @@ -595,27 +616,21 @@ object Graph { // add all the vertices and edges in one go channels.values.foreach { channel => - channel.update_1_opt.foreach { u1 => - val desc1 = Router.getDesc(u1, channel.ann) - addDescToMap(desc1, u1, channel.capacity, channel.meta_opt.map(_.balance1)) - } - channel.update_2_opt.foreach { u2 => - val desc2 = Router.getDesc(u2, channel.ann) - addDescToMap(desc2, u2, channel.capacity, channel.meta_opt.map(_.balance2)) - } + channel.update_1_opt.foreach(u1 => addToMap(GraphEdge(u1, channel))) + channel.update_2_opt.foreach(u2 => addToMap(GraphEdge(u2, channel))) } - def addDescToMap(desc: ChannelDesc, u: ChannelUpdate, capacity: Satoshi, balance_opt: Option[MilliSatoshi]): Unit = { - mutableMap.put(desc.b, GraphEdge(desc, u, capacity, balance_opt) +: mutableMap.getOrElse(desc.b, List.empty[GraphEdge])) - if (!mutableMap.contains(desc.a)) { - mutableMap += desc.a -> List.empty[GraphEdge] + def addToMap(edge: GraphEdge): Unit = { + mutableMap.put(edge.desc.b, edge +: mutableMap.getOrElse(edge.desc.b, List.empty[GraphEdge])) + if (!mutableMap.contains(edge.desc.a)) { + mutableMap += edge.desc.a -> List.empty[GraphEdge] } } new DirectedGraph(mutableMap.toMap) } - def graphEdgeToHop(graphEdge: GraphEdge): ChannelHop = ChannelHop(graphEdge.desc.a, graphEdge.desc.b, graphEdge.update) + def graphEdgeToHop(graphEdge: GraphEdge): ChannelHop = ChannelHop(graphEdge.desc.shortChannelId, graphEdge.desc.a, graphEdge.desc.b, graphEdge.params) } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/RouteCalculation.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/RouteCalculation.scala index 34ab89493d..cf51a2cc97 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/RouteCalculation.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/RouteCalculation.scala @@ -47,9 +47,7 @@ object RouteCalculation { implicit val sender: ActorRef = ctx.self // necessary to preserve origin when sending messages to other actors val assistedChannels: Map[ShortChannelId, AssistedChannel] = fr.assistedRoutes.flatMap(toAssistedChannels(_, fr.route.targetNodeId, fr.amount)).toMap - val extraEdges = assistedChannels.values.map(ac => - GraphEdge(ChannelDesc(ac.extraHop.shortChannelId, ac.extraHop.nodeId, ac.nextNodeId), toFakeUpdate(ac.extraHop, ac.htlcMaximum), htlcMaxToCapacity(ac.htlcMaximum), Some(ac.htlcMaximum)) - ).toSet + val extraEdges = assistedChannels.values.map(ac => GraphEdge(ac)).toSet val g = extraEdges.foldLeft(d.graph) { case (g: DirectedGraph, e: GraphEdge) => g.addEdge(e) } fr.route match { @@ -59,33 +57,33 @@ object RouteCalculation { case edges if edges.nonEmpty && edges.forall(_.nonEmpty) => // select the largest edge (using balance when available, otherwise capacity). val selectedEdges = edges.map(es => es.maxBy(e => e.balance_opt.getOrElse(e.capacity.toMilliSatoshi))) - val hops = selectedEdges.map(d => ChannelHop(d.desc.a, d.desc.b, d.update)) + val hops = selectedEdges.map(d => ChannelHop(d.desc.shortChannelId, d.desc.a, d.desc.b, d.params)) ctx.sender() ! RouteResponse(Route(fr.amount, hops) :: Nil) case _ => // some nodes in the supplied route aren't connected in our graph ctx.sender() ! Status.Failure(new IllegalArgumentException("Not all the nodes in the supplied route are connected with public channels")) } - case PredefinedChannelRoute(targetNodeId, channels) => - val (end, hops) = channels.foldLeft((localNodeId, Seq.empty[ChannelHop])) { - case ((start, current), shortChannelId) => - val channelDesc_opt = d.channels.get(shortChannelId).flatMap(c => start match { + case PredefinedChannelRoute(targetNodeId, shortChannelIds) => + val (end, hops) = shortChannelIds.foldLeft((localNodeId, Seq.empty[ChannelHop])) { + case ((currentNode, previousHops), shortChannelId) => + val channelDesc_opt = d.channels.get(shortChannelId).flatMap(c => currentNode match { case c.ann.nodeId1 => Some(ChannelDesc(shortChannelId, c.ann.nodeId1, c.ann.nodeId2)) case c.ann.nodeId2 => Some(ChannelDesc(shortChannelId, c.ann.nodeId2, c.ann.nodeId1)) case _ => None - }).orElse(d.privateChannels.get(shortChannelId).flatMap(c => start match { + }).orElse(d.privateChannels.get(shortChannelId).flatMap(c => currentNode match { case c.nodeId1 => Some(ChannelDesc(shortChannelId, c.nodeId1, c.nodeId2)) case c.nodeId2 => Some(ChannelDesc(shortChannelId, c.nodeId2, c.nodeId1)) case _ => None - })).orElse(assistedChannels.get(shortChannelId).flatMap(c => start match { - case c.extraHop.nodeId => Some(ChannelDesc(shortChannelId, c.extraHop.nodeId, c.nextNodeId)) + })).orElse(assistedChannels.get(shortChannelId).flatMap(c => currentNode match { + case c.nodeId => Some(ChannelDesc(shortChannelId, c.nodeId, c.nextNodeId)) case _ => None })) channelDesc_opt.flatMap(c => g.getEdge(c)) match { - case Some(edge) => (edge.desc.b, current :+ ChannelHop(edge.desc.a, edge.desc.b, edge.update)) - case None => (start, current) + case Some(edge) => (edge.desc.b, previousHops :+ ChannelHop(edge.desc.shortChannelId, edge.desc.a, edge.desc.b, edge.params)) + case None => (currentNode, previousHops) } } - if (end != targetNodeId || hops.length != channels.length) { + if (end != targetNodeId || hops.length != shortChannelIds.length) { ctx.sender() ! Status.Failure(new IllegalArgumentException("The sequence of channels provided cannot be used to build a route to the target node")) } else { ctx.sender() ! RouteResponse(Route(fr.amount, hops) :: Nil) @@ -107,11 +105,9 @@ object RouteCalculation { // we convert extra routing info provided in the invoice to fake channel_update // it takes precedence over all other channel_updates we know val assistedChannels: Map[ShortChannelId, AssistedChannel] = r.assistedRoutes.flatMap(toAssistedChannels(_, r.target, r.amount)) - .filterNot { case (_, ac) => ac.extraHop.nodeId == r.source } // we ignore routing hints for our own channels, we have more accurate information + .filterNot { case (_, ac) => ac.nodeId == r.source } // we ignore routing hints for our own channels, we have more accurate information .toMap - val extraEdges = assistedChannels.values.map(ac => - GraphEdge(ChannelDesc(ac.extraHop.shortChannelId, ac.extraHop.nodeId, ac.nextNodeId), toFakeUpdate(ac.extraHop, ac.htlcMaximum), htlcMaxToCapacity(ac.htlcMaximum), Some(ac.htlcMaximum)) - ).toSet + val extraEdges = assistedChannels.values.map(ac => GraphEdge(ac)).toSet val ignoredEdges = r.ignore.channels ++ d.excludedChannels val params = r.routeParams val routesToFind = if (params.randomize) DEFAULT_ROUTES_COUNT else 1 @@ -149,24 +145,17 @@ object RouteCalculation { } } - private def toFakeUpdate(extraHop: ExtraHop, htlcMaximum: MilliSatoshi): ChannelUpdate = { - // the `direction` bit in flags will not be accurate but it doesn't matter because it is not used - // what matters is that the `disable` bit is 0 so that this update doesn't get filtered out - ChannelUpdate(signature = ByteVector64.Zeroes, chainHash = ByteVector32.Zeroes, extraHop.shortChannelId, TimestampSecond.now(), channelFlags = ChannelUpdate.ChannelFlags(isNode1 = true, isEnabled = true), extraHop.cltvExpiryDelta, htlcMinimumMsat = 0 msat, extraHop.feeBase, extraHop.feeProportionalMillionths, Some(htlcMaximum)) - } - def toAssistedChannels(extraRoute: Seq[ExtraHop], targetNodeId: PublicKey, amount: MilliSatoshi): Map[ShortChannelId, AssistedChannel] = { // BOLT 11: "For each entry, the pubkey is the node ID of the start of the channel", and the last node is the destination // The invoice doesn't explicitly specify the channel's htlcMaximumMsat, but we can safely assume that the channel // should be able to route the payment, so we'll compute an htlcMaximumMsat accordingly. - // We could also get the channel capacity from the blockchain (since we have the shortChannelId) but that's more expensive. // We also need to make sure the channel isn't excluded by our heuristics. val lastChannelCapacity = amount.max(RoutingHeuristics.CAPACITY_CHANNEL_LOW) val nextNodeIds = extraRoute.map(_.nodeId).drop(1) :+ targetNodeId extraRoute.zip(nextNodeIds).reverse.foldLeft((lastChannelCapacity, Map.empty[ShortChannelId, AssistedChannel])) { case ((amount, acs), (extraHop: ExtraHop, nextNodeId)) => - val nextAmount = amount + nodeFee(extraHop.feeBase, extraHop.feeProportionalMillionths, amount) - (nextAmount, acs + (extraHop.shortChannelId -> AssistedChannel(extraHop, nextNodeId, nextAmount))) + val nextAmount = amount + nodeFee(extraHop.relayFees, amount) + (nextAmount, acs + (extraHop.shortChannelId -> AssistedChannel(nextNodeId, Router.ChannelRelayParams.FromHint(extraHop, nextAmount)))) }._2 } @@ -175,9 +164,6 @@ object RouteCalculation { extraRoute.zip(nextNodeIds).map { case (hop, nextNodeId) => ChannelDesc(hop.shortChannelId, hop.nodeId, nextNodeId) } } - /** Bolt 11 routing hints don't include the channel's capacity, so we round up the maximum htlc amount. */ - private def htlcMaxToCapacity(htlcMaximum: MilliSatoshi): Satoshi = htlcMaximum.truncateToSatoshi + 1.sat - /** This method is used after a payment failed, and we want to exclude some nodes that we know are failing */ def getIgnoredChannelDesc(channels: Map[ShortChannelId, PublicChannel], ignoreNodes: Set[PublicKey]): Iterable[ChannelDesc] = { val desc = if (ignoreNodes.isEmpty) { @@ -334,7 +320,7 @@ object RouteCalculation { val directChannels = g.getEdgesBetween(localNodeId, targetNodeId).collect { // We should always have balance information available for local channels. // NB: htlcMinimumMsat is set by our peer and may be 0 msat (even though it's not recommended). - case GraphEdge(_, update, _, Some(balance)) => DirectChannel(balance, balance <= 0.msat || balance < update.htlcMinimumMsat) + case GraphEdge(_, params, _, Some(balance)) => DirectChannel(balance, balance <= 0.msat || balance < params.htlcMinimum) } // If we have direct channels to the target, we can use them all. // We also count empty channels, which allows replacing them with a non-direct route (multiple hops). @@ -388,12 +374,12 @@ object RouteCalculation { /** Compute the maximum amount that we can send through the given route. */ private def computeRouteMaxAmount(route: Seq[GraphEdge], usedCapacity: mutable.Map[ShortChannelId, MilliSatoshi]): Route = { - val firstHopMaxAmount = route.head.maxHtlcAmount(usedCapacity.getOrElse(route.head.update.shortChannelId, 0 msat)) + val firstHopMaxAmount = route.head.maxHtlcAmount(usedCapacity.getOrElse(route.head.desc.shortChannelId, 0 msat)) val amount = route.drop(1).foldLeft(firstHopMaxAmount) { case (amount, edge) => // We compute fees going forward instead of backwards. That means we will slightly overestimate the fees of some // edges, but we will always stay inside the capacity bounds we computed. val amountMinusFees = amount - edge.fee(amount) - val edgeMaxAmount = edge.maxHtlcAmount(usedCapacity.getOrElse(edge.update.shortChannelId, 0 msat)) + val edgeMaxAmount = edge.maxHtlcAmount(usedCapacity.getOrElse(edge.desc.shortChannelId, 0 msat)) amountMinusFees.min(edgeMaxAmount) } Route(amount.max(0 msat), route.map(graphEdgeToHop)) @@ -411,7 +397,7 @@ object RouteCalculation { /** Update used capacity by taking into account an HTLC sent to the given route. */ private def updateUsedCapacity(route: Route, usedCapacity: mutable.Map[ShortChannelId, MilliSatoshi]): Unit = { route.hops.reverse.foldLeft(route.amount) { case (amount, hop) => - usedCapacity.updateWith(hop.lastUpdate.shortChannelId)(previous => Some(amount + previous.getOrElse(0 msat))) + usedCapacity.updateWith(hop.shortChannelId)(previous => Some(amount + previous.getOrElse(0 msat))) amount + hop.fee(amount) } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala index 7a6f1fcc4e..1e002d0872 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala @@ -31,7 +31,9 @@ import fr.acinq.eclair.channel._ import fr.acinq.eclair.crypto.TransportHandler import fr.acinq.eclair.db.NetworkDb import fr.acinq.eclair.io.Peer.PeerRoutingMessage +import fr.acinq.eclair.payment.Bolt11Invoice import fr.acinq.eclair.payment.Bolt11Invoice.ExtraHop +import fr.acinq.eclair.payment.relay.Relayer import fr.acinq.eclair.remote.EclairInternalsSerializer.RemoteTypes import fr.acinq.eclair.router.Graph.GraphStructure.DirectedGraph import fr.acinq.eclair.router.Graph.{HeuristicsConstants, WeightRatios} @@ -310,7 +312,17 @@ object Router { } // @formatter:off - case class ChannelDesc(shortChannelId: ShortChannelId, a: PublicKey, b: PublicKey) + case class ChannelDesc private(shortChannelId: ShortChannelId, a: PublicKey, b: PublicKey) + object ChannelDesc { + def apply(u: ChannelUpdate, ann: ChannelAnnouncement): ChannelDesc = { + // the least significant bit tells us if it is node1 or node2 + if (u.channelFlags.isNode1) ChannelDesc(ann.shortChannelId, ann.nodeId1, ann.nodeId2) else ChannelDesc(ann.shortChannelId, ann.nodeId2, ann.nodeId1) + } + def apply(u: ChannelUpdate, pc: PrivateChannel): ChannelDesc = { + // the least significant bit tells us if it is node1 or node2 + if (u.channelFlags.isNode1) ChannelDesc(u.shortChannelId, pc.nodeId1, pc.nodeId2) else ChannelDesc(u.shortChannelId, pc.nodeId2, pc.nodeId1) + } + } case class ChannelMeta(balance1: MilliSatoshi, balance2: MilliSatoshi) sealed trait ChannelDetails { val capacity: Satoshi @@ -377,7 +389,10 @@ object Router { } // @formatter:on - case class AssistedChannel(extraHop: ExtraHop, nextNodeId: PublicKey, htlcMaximum: MilliSatoshi) + case class AssistedChannel(nextNodeId: PublicKey, params: ChannelRelayParams.FromHint) { + val nodeId: PublicKey = params.extraHop.nodeId + val shortChannelId: ShortChannelId = params.extraHop.shortChannelId + } trait Hop { /** @return the id of the start node. */ @@ -396,17 +411,47 @@ object Router { def cltvExpiryDelta: CltvExpiryDelta } + // @formatter:off + /** Channel routing parameters */ + sealed trait ChannelRelayParams { + def cltvExpiryDelta: CltvExpiryDelta + def relayFees: Relayer.RelayFees + final def fee(amount: MilliSatoshi): MilliSatoshi = nodeFee(relayFees, amount) + def htlcMinimum: MilliSatoshi + def htlcMaximum_opt: Option[MilliSatoshi] + } + + object ChannelRelayParams { + /** We learnt about this channel from a channel_update */ + case class FromAnnouncement(channelUpdate: ChannelUpdate) extends ChannelRelayParams { + override def cltvExpiryDelta: CltvExpiryDelta = channelUpdate.cltvExpiryDelta + override def relayFees: Relayer.RelayFees = channelUpdate.relayFees + override def htlcMinimum: MilliSatoshi = channelUpdate.htlcMinimumMsat + override def htlcMaximum_opt: Option[MilliSatoshi] = channelUpdate.htlcMaximumMsat + } + /** We learnt about this channel from hints in an invoice */ + case class FromHint(extraHop: Bolt11Invoice.ExtraHop, htlcMaximum: MilliSatoshi) extends ChannelRelayParams { + override def cltvExpiryDelta: CltvExpiryDelta = extraHop.cltvExpiryDelta + override def relayFees: Relayer.RelayFees = extraHop.relayFees + override def htlcMinimum: MilliSatoshi = 0 msat + override def htlcMaximum_opt: Option[MilliSatoshi] = Some(htlcMaximum) + } + } + // @formatter:on + /** * A directed hop between two connected nodes using a specific channel. * - * @param nodeId id of the start node. - * @param nextNodeId id of the end node. - * @param lastUpdate last update of the channel used for the hop. + * @param nodeId id of the start node. + * @param nextNodeId id of the end node. + * @param shortChannelId scid that will be used to build the payment onion. + * @param params source for the channel parameters. */ - case class ChannelHop(nodeId: PublicKey, nextNodeId: PublicKey, lastUpdate: ChannelUpdate) extends Hop { - override lazy val cltvExpiryDelta: CltvExpiryDelta = lastUpdate.cltvExpiryDelta - - override def fee(amount: MilliSatoshi): MilliSatoshi = nodeFee(lastUpdate, amount) + case class ChannelHop(shortChannelId: ShortChannelId, nodeId: PublicKey, nextNodeId: PublicKey, params: ChannelRelayParams) extends Hop { + // @formatter:off + override def cltvExpiryDelta: CltvExpiryDelta = params.cltvExpiryDelta + override def fee(amount: MilliSatoshi): MilliSatoshi = params.fee(amount) + // @formatter:on } /** @@ -484,11 +529,11 @@ object Router { } /** This method retrieves the channel update that we used when we built the route. */ - def getChannelUpdateForNode(nodeId: PublicKey): Option[ChannelUpdate] = hops.find(_.nodeId == nodeId).map(_.lastUpdate) + def getChannelUpdateForNode(nodeId: PublicKey): Option[ChannelUpdate] = hops.find(_.nodeId == nodeId).map(_.params).collect { case s: ChannelRelayParams.FromAnnouncement => s.channelUpdate } def printNodes(): String = hops.map(_.nextNodeId).mkString("->") - def printChannels(): String = hops.map(_.lastUpdate.shortChannelId).mkString("->") + def printChannels(): String = hops.map(_.shortChannelId).mkString("->") } @@ -590,16 +635,6 @@ object Router { case object TickPruneStaleChannels // @formatter:on - def getDesc(u: ChannelUpdate, channel: ChannelAnnouncement): ChannelDesc = { - // the least significant bit tells us if it is node1 or node2 - if (u.channelFlags.isNode1) ChannelDesc(u.shortChannelId, channel.nodeId1, channel.nodeId2) else ChannelDesc(u.shortChannelId, channel.nodeId2, channel.nodeId1) - } - - def getDesc(u: ChannelUpdate, pc: PrivateChannel): ChannelDesc = { - // the least significant bit tells us if it is node1 or node2 - if (u.channelFlags.isNode1) ChannelDesc(u.shortChannelId, pc.nodeId1, pc.nodeId2) else ChannelDesc(u.shortChannelId, pc.nodeId2, pc.nodeId1) - } - def isRelatedTo(c: ChannelAnnouncement, nodeId: PublicKey) = nodeId == c.nodeId1 || nodeId == c.nodeId2 def hasChannels(nodeId: PublicKey, channels: Iterable[PublicChannel]): Boolean = channels.exists(c => isRelatedTo(c.ann, nodeId)) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/Validation.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/Validation.scala index 5f4d66c73c..ed0cce7918 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/Validation.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/Validation.scala @@ -26,6 +26,7 @@ import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.{UtxoStatus, ValidateReque import fr.acinq.eclair.channel.{AvailableBalanceChanged, LocalChannelDown, LocalChannelUpdate} import fr.acinq.eclair.crypto.TransportHandler import fr.acinq.eclair.db.NetworkDb +import fr.acinq.eclair.router.Graph.GraphStructure.GraphEdge import fr.acinq.eclair.router.Monitoring.Metrics import fr.acinq.eclair.router.Router._ import fr.acinq.eclair.transactions.Scripts @@ -263,14 +264,13 @@ object Validation { // related channel is already known (note: this means no related channel_update is in the stash) val publicChannel = true val pc = d.channels(u.shortChannelId) - val desc = getDesc(u, pc.ann) if (d.rebroadcast.updates.contains(u)) { log.debug("ignoring {} (pending rebroadcast)", u) sendDecision(origins, GossipDecision.Accepted(u)) val origins1 = d.rebroadcast.updates(u) ++ origins // NB: we update the channels because the balances may have changed even if the channel_update is the same. val pc1 = pc.applyChannelUpdate(update) - val graph1 = d.graph.addEdge(desc, u, pc1.capacity, pc1.getBalanceSameSideAs(u)) + val graph1 = d.graph.addEdge(GraphEdge(u, pc1)) d.copy(rebroadcast = d.rebroadcast.copy(updates = d.rebroadcast.updates + (u -> origins1)), channels = d.channels + (u.shortChannelId -> pc1), graph = graph1) } else if (StaleChannels.isStale(u)) { log.debug("ignoring {} (stale)", u) @@ -283,7 +283,7 @@ object Validation { case Left(_) => // NB: we update the graph because the balances may have changed even if the channel_update is the same. val pc1 = pc.applyChannelUpdate(update) - val graph1 = d.graph.addEdge(desc, u, pc1.capacity, pc1.getBalanceSameSideAs(u)) + val graph1 = d.graph.addEdge(GraphEdge(u, pc1)) d.copy(channels = d.channels + (u.shortChannelId -> pc1), graph = graph1) case Right(_) => d } @@ -301,10 +301,10 @@ object Validation { val pc1 = pc.applyChannelUpdate(update) val graph1 = if (u.channelFlags.isEnabled) { update.left.foreach(_ => log.info("added local shortChannelId={} public={} to the network graph", u.shortChannelId, publicChannel)) - d.graph.addEdge(desc, u, pc1.capacity, pc1.getBalanceSameSideAs(u)) + d.graph.addEdge(GraphEdge(u, pc1)) } else { update.left.foreach(_ => log.info("removed local shortChannelId={} public={} from the network graph", u.shortChannelId, publicChannel)) - d.graph.removeEdge(desc) + d.graph.removeEdge(ChannelDesc(u, pc.ann)) } d.copy(channels = d.channels + (u.shortChannelId -> pc1), rebroadcast = d.rebroadcast.copy(updates = d.rebroadcast.updates + (u -> origins)), graph = graph1) } else { @@ -314,7 +314,7 @@ object Validation { db.updateChannel(u) // we also need to update the graph val pc1 = pc.applyChannelUpdate(update) - val graph1 = d.graph.addEdge(desc, u, pc1.capacity, pc1.getBalanceSameSideAs(u)) + val graph1 = d.graph.addEdge(GraphEdge(u, pc1)) update.left.foreach(_ => log.info("added local shortChannelId={} public={} to the network graph", u.shortChannelId, publicChannel)) d.copy(channels = d.channels + (u.shortChannelId -> pc1), privateChannels = d.privateChannels - u.shortChannelId, rebroadcast = d.rebroadcast.copy(updates = d.rebroadcast.updates + (u -> origins)), graph = graph1) } @@ -331,7 +331,6 @@ object Validation { } else if (d.privateChannels.contains(u.shortChannelId)) { val publicChannel = false val pc = d.privateChannels(u.shortChannelId) - val desc = getDesc(u, pc) if (StaleChannels.isStale(u)) { log.debug("ignoring {} (stale)", u) sendDecision(origins, GossipDecision.Stale(u)) @@ -340,7 +339,7 @@ object Validation { log.debug("ignoring {} (already know same or newer)", u) sendDecision(origins, GossipDecision.Duplicate(u)) d - } else if (!Announcements.checkSig(u, desc.a)) { + } else if (!Announcements.checkSig(u, pc.getNodeIdSameSideAs(u))) { log.warning("bad signature for announcement shortChannelId={} {}", u.shortChannelId, u) sendDecision(origins, GossipDecision.InvalidSignature(u)) d @@ -353,10 +352,10 @@ object Validation { val pc1 = pc.applyChannelUpdate(update) val graph1 = if (u.channelFlags.isEnabled) { update.left.foreach(_ => log.info("added local shortChannelId={} public={} to the network graph", u.shortChannelId, publicChannel)) - d.graph.addEdge(desc, u, pc1.capacity, pc1.getBalanceSameSideAs(u)) + d.graph.addEdge(GraphEdge(u, pc1)) } else { update.left.foreach(_ => log.info("removed local shortChannelId={} public={} from the network graph", u.shortChannelId, publicChannel)) - d.graph.removeEdge(desc) + d.graph.removeEdge(ChannelDesc(u, pc1)) } d.copy(privateChannels = d.privateChannels + (u.shortChannelId -> pc1), graph = graph1) } else { @@ -365,7 +364,7 @@ object Validation { ctx.system.eventStream.publish(ChannelUpdatesReceived(u :: Nil)) // we also need to update the graph val pc1 = pc.applyChannelUpdate(update) - val graph1 = d.graph.addEdge(desc, u, pc1.capacity, pc1.getBalanceSameSideAs(u)) + val graph1 = d.graph.addEdge(GraphEdge(u, pc1)) update.left.foreach(_ => log.info("added local shortChannelId={} public={} to the network graph", u.shortChannelId, publicChannel)) d.copy(privateChannels = d.privateChannels + (u.shortChannelId -> pc1), graph = graph1) } @@ -462,14 +461,13 @@ object Validation { } def handleAvailableBalanceChanged(d: Data, e: AvailableBalanceChanged)(implicit log: LoggingAdapter): Data = { - val desc = ChannelDesc(e.shortChannelId, e.commitments.localNodeId, e.commitments.remoteNodeId) val (publicChannels1, graph1) = d.channels.get(e.shortChannelId) match { case Some(pc) => val pc1 = pc.updateBalances(e.commitments) log.debug("public channel balance updated: {}", pc1) val update_opt = if (e.commitments.localNodeId == pc1.ann.nodeId1) pc1.update_1_opt else pc1.update_2_opt - val graph1 = update_opt.map(u => d.graph.addEdge(desc, u, pc1.capacity, pc1.getBalanceSameSideAs(u))).getOrElse(d.graph) - (d.channels + (e.shortChannelId -> pc1), graph1) + val graph1 = update_opt.map(u => d.graph.addEdge(GraphEdge(u, pc1))).getOrElse(d.graph) + (d.channels + (pc.ann.shortChannelId -> pc1), graph1) case None => (d.channels, d.graph) } @@ -478,7 +476,7 @@ object Validation { val pc1 = pc.updateBalances(e.commitments) log.debug("private channel balance updated: {}", pc1) val update_opt = if (e.commitments.localNodeId == pc1.nodeId1) pc1.update_1_opt else pc1.update_2_opt - val graph2 = update_opt.map(u => graph1.addEdge(desc, u, pc1.capacity, pc1.getBalanceSameSideAs(u))).getOrElse(graph1) + val graph2 = update_opt.map(u => graph1.addEdge(GraphEdge(u, pc1))).getOrElse(graph1) (d.privateChannels + (e.shortChannelId -> pc1), graph2) case None => (d.privateChannels, graph1) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageTypes.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageTypes.scala index 6510657c48..c91e65964a 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageTypes.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageTypes.scala @@ -22,6 +22,7 @@ import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, Satoshi, ScriptWitness, Transaction} import fr.acinq.eclair.blockchain.fee.FeeratePerKw import fr.acinq.eclair.channel.{ChannelFlags, ChannelType} +import fr.acinq.eclair.payment.relay.Relayer import fr.acinq.eclair.{BlockHeight, CltvExpiry, CltvExpiryDelta, Feature, Features, InitFeature, MilliSatoshi, ShortChannelId, TimestampSecond, UInt64} import scodec.bits.ByteVector @@ -374,6 +375,8 @@ case class ChannelUpdate(signature: ByteVector64, def messageFlags: Byte = if (htlcMaximumMsat.isDefined) 1 else 0 def toStringShort: String = s"cltvExpiryDelta=$cltvExpiryDelta,feeBase=$feeBaseMsat,feeProportionalMillionths=$feeProportionalMillionths" + + def relayFees: Relayer.RelayFees = Relayer.RelayFees(feeBase = feeBaseMsat, feeProportionalMillionths = feeProportionalMillionths) } object ChannelUpdate { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/FuzzySpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/FuzzySpec.scala index fa4daca90f..2ca68f370f 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/FuzzySpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/FuzzySpec.scala @@ -123,7 +123,7 @@ class FuzzySpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Channe // allow overpaying (no more than 2 times the required amount) val amount = requiredAmount + Random.nextInt(requiredAmount.toLong.toInt).msat val expiry = (Channel.MIN_CLTV_EXPIRY_DELTA + 1).toCltvExpiry(currentBlockHeight = BlockHeight(400000)) - OutgoingPaymentPacket.buildCommand(self, Upstream.Local(UUID.randomUUID()), paymentHash, ChannelHop(null, dest, null) :: Nil, PaymentOnion.createSinglePartPayload(amount, expiry, paymentSecret, None)).get._1 + OutgoingPaymentPacket.buildCommand(self, Upstream.Local(UUID.randomUUID()), paymentHash, ChannelHop(null, null, dest, null) :: Nil, PaymentOnion.createSinglePartPayload(amount, expiry, paymentSecret, None)).get._1 } def initiatePaymentOrStop(remaining: Int): Unit = diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala index 31444f0cad..fd1703295f 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala @@ -250,7 +250,7 @@ trait ChannelStateTestsHelperMethods extends TestKitBase { def makeCmdAdd(amount: MilliSatoshi, cltvExpiryDelta: CltvExpiryDelta, destination: PublicKey, paymentPreimage: ByteVector32, currentBlockHeight: BlockHeight, upstream: Upstream, replyTo: ActorRef = TestProbe().ref): (ByteVector32, CMD_ADD_HTLC) = { val paymentHash: ByteVector32 = Crypto.sha256(paymentPreimage) val expiry = cltvExpiryDelta.toCltvExpiry(currentBlockHeight) - val cmd = OutgoingPaymentPacket.buildCommand(replyTo, upstream, paymentHash, ChannelHop(null, destination, null) :: Nil, PaymentOnion.createSinglePartPayload(amount, expiry, randomBytes32(), None)).get._1.copy(commit = false) + val cmd = OutgoingPaymentPacket.buildCommand(replyTo, upstream, paymentHash, ChannelHop(null, null, destination, null) :: Nil, PaymentOnion.createSinglePartPayload(amount, expiry, randomBytes32(), None)).get._1.copy(commit = false) (paymentPreimage, cmd) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/f/ShutdownStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/f/ShutdownStateSpec.scala index 5843eb05c5..1c7afd2671 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/f/ShutdownStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/f/ShutdownStateSpec.scala @@ -60,7 +60,7 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit val h1 = Crypto.sha256(r1) val amount1 = 300000000 msat val expiry1 = CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight) - val cmd1 = OutgoingPaymentPacket.buildCommand(sender.ref, Upstream.Local(UUID.randomUUID), h1, ChannelHop(null, TestConstants.Bob.nodeParams.nodeId, null) :: Nil, PaymentOnion.createSinglePartPayload(amount1, expiry1, randomBytes32(), None)).get._1.copy(commit = false) + val cmd1 = OutgoingPaymentPacket.buildCommand(sender.ref, Upstream.Local(UUID.randomUUID), h1, ChannelHop(null, null, TestConstants.Bob.nodeParams.nodeId, null) :: Nil, PaymentOnion.createSinglePartPayload(amount1, expiry1, randomBytes32(), None)).get._1.copy(commit = false) alice ! cmd1 sender.expectMsgType[RES_SUCCESS[CMD_ADD_HTLC]] val htlc1 = alice2bob.expectMsgType[UpdateAddHtlc] @@ -70,7 +70,7 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit val h2 = Crypto.sha256(r2) val amount2 = 200000000 msat val expiry2 = CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight) - val cmd2 = OutgoingPaymentPacket.buildCommand(sender.ref, Upstream.Local(UUID.randomUUID), h2, ChannelHop(null, TestConstants.Bob.nodeParams.nodeId, null) :: Nil, PaymentOnion.createSinglePartPayload(amount2, expiry2, randomBytes32(), None)).get._1.copy(commit = false) + val cmd2 = OutgoingPaymentPacket.buildCommand(sender.ref, Upstream.Local(UUID.randomUUID), h2, ChannelHop(null, null, TestConstants.Bob.nodeParams.nodeId, null) :: Nil, PaymentOnion.createSinglePartPayload(amount2, expiry2, randomBytes32(), None)).get._1.copy(commit = false) alice ! cmd2 sender.expectMsgType[RES_SUCCESS[CMD_ADD_HTLC]] val htlc2 = alice2bob.expectMsgType[UpdateAddHtlc] diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/PaymentsDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/PaymentsDbSpec.scala index 8294d15fe0..7e3a93b85a 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/PaymentsDbSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/PaymentsDbSpec.scala @@ -24,7 +24,7 @@ import fr.acinq.eclair.db.jdbc.JdbcUtils.{setVersion, using} import fr.acinq.eclair.db.pg.PgPaymentsDb import fr.acinq.eclair.db.sqlite.SqlitePaymentsDb import fr.acinq.eclair.payment._ -import fr.acinq.eclair.router.Router.{ChannelHop, NodeHop} +import fr.acinq.eclair.router.Router.{ChannelHop, ChannelRelayParams, NodeHop} import fr.acinq.eclair.wire.protocol.{ChannelUpdate, UnknownNextPeer} import fr.acinq.eclair.{CltvExpiryDelta, MilliSatoshiLong, ShortChannelId, TestDatabases, TimestampMilli, TimestampMilliLong, TimestampSecond, TimestampSecondLong, randomBytes32, randomBytes64, randomKey} import org.scalatest.funsuite.AnyFunSuite @@ -593,7 +593,7 @@ class PaymentsDbSpec extends AnyFunSuite { object PaymentsDbSpec { val (alicePriv, bobPriv, carolPriv, davePriv) = (randomKey(), randomKey(), randomKey(), randomKey()) val (alice, bob, carol, dave) = (alicePriv.publicKey, bobPriv.publicKey, carolPriv.publicKey, davePriv.publicKey) - val hop_ab = ChannelHop(alice, bob, ChannelUpdate(randomBytes64(), randomBytes32(), ShortChannelId(42), 1 unixsec, ChannelUpdate.ChannelFlags.DUMMY, CltvExpiryDelta(12), 1 msat, 1 msat, 1, None)) + val hop_ab = ChannelHop(ShortChannelId(42), alice, bob, ChannelRelayParams.FromAnnouncement(ChannelUpdate(randomBytes64(), randomBytes32(), ShortChannelId(42), 1 unixsec, ChannelUpdate.ChannelFlags.DUMMY, CltvExpiryDelta(12), 1 msat, 1 msat, 1, None))) val hop_bc = NodeHop(bob, carol, CltvExpiryDelta(14), 1 msat) val (preimage1, preimage2, preimage3, preimage4) = (randomBytes32(), randomBytes32(), randomBytes32(), randomBytes32()) val (paymentHash1, paymentHash2, paymentHash3, paymentHash4) = (Crypto.sha256(preimage1), Crypto.sha256(preimage2), Crypto.sha256(preimage3), Crypto.sha256(preimage4)) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartPaymentLifecycleSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartPaymentLifecycleSpec.scala index 75f7dea40d..634f7e42a1 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartPaymentLifecycleSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartPaymentLifecycleSpec.scala @@ -31,6 +31,7 @@ import fr.acinq.eclair.payment.send.PaymentError.RetryExhausted import fr.acinq.eclair.payment.send.PaymentInitiator.SendPaymentConfig import fr.acinq.eclair.payment.send.PaymentLifecycle.SendPaymentToRoute import fr.acinq.eclair.payment.send.{MultiPartPaymentLifecycle, PaymentInitiator} +import fr.acinq.eclair.router.BaseRouterSpec.channelHopFromUpdate import fr.acinq.eclair.router.Graph.WeightRatios import fr.acinq.eclair.router.Router._ import fr.acinq.eclair.router.{Announcements, RouteNotFound} @@ -317,7 +318,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS import f._ // The B -> E channel is private and provided in the invoice routing hints. - val routingHint = ExtraHop(b, hop_be.lastUpdate.shortChannelId, hop_be.lastUpdate.feeBaseMsat, hop_be.lastUpdate.feeProportionalMillionths, hop_be.lastUpdate.cltvExpiryDelta) + val routingHint = ExtraHop(b, hop_be.shortChannelId, hop_be.params.relayFees.feeBase, hop_be.params.relayFees.feeProportionalMillionths, hop_be.params.cltvExpiryDelta) val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount, expiry, 3, None, routeParams = routeParams, assistedRoutes = List(List(routingHint))) sender.send(payFsm, payment) assert(router.expectMsgType[RouteRequest].assistedRoutes.head.head === routingHint) @@ -327,7 +328,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS childPayFsm.expectNoMessage(100 millis) // B changed his fees and expiry after the invoice was issued. - val channelUpdate = hop_be.lastUpdate.copy(feeBaseMsat = 250 msat, feeProportionalMillionths = 150, cltvExpiryDelta = CltvExpiryDelta(24)) + val channelUpdate = hop_be.params.asInstanceOf[ChannelRelayParams.FromAnnouncement].channelUpdate.copy(feeBaseMsat = 250 msat, feeProportionalMillionths = 150, cltvExpiryDelta = CltvExpiryDelta(24)) val childId = payFsm.stateData.asInstanceOf[PaymentProgress].pending.keys.head childPayFsm.send(payFsm, PaymentFailed(childId, paymentHash, Seq(RemoteFailure(route.amount, route.hops, Sphinx.DecryptedFailurePacket(b, FeeInsufficient(finalAmount, channelUpdate)))))) // We update the routing hints accordingly before requesting a new route. @@ -338,7 +339,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS import f._ // The B -> E channel is private and provided in the invoice routing hints. - val routingHint = ExtraHop(b, hop_be.lastUpdate.shortChannelId, hop_be.lastUpdate.feeBaseMsat, hop_be.lastUpdate.feeProportionalMillionths, hop_be.lastUpdate.cltvExpiryDelta) + val routingHint = ExtraHop(b, hop_be.shortChannelId, hop_be.params.relayFees.feeBase, hop_be.params.relayFees.feeProportionalMillionths, hop_be.params.cltvExpiryDelta) val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount, expiry, 3, None, routeParams = routeParams, assistedRoutes = List(List(routingHint))) sender.send(payFsm, payment) assert(router.expectMsgType[RouteRequest].assistedRoutes.head.head === routingHint) @@ -349,13 +350,14 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS // B doesn't have enough liquidity on this channel. // NB: we need a channel update with a valid signature, otherwise we'll ignore the node instead of this specific channel. - val channelUpdate = Announcements.makeChannelUpdate(hop_be.lastUpdate.chainHash, priv_b, e, hop_be.lastUpdate.shortChannelId, hop_be.lastUpdate.cltvExpiryDelta, hop_be.lastUpdate.htlcMinimumMsat, hop_be.lastUpdate.feeBaseMsat, hop_be.lastUpdate.feeProportionalMillionths, hop_be.lastUpdate.htlcMaximumMsat.get) + val channelUpdateBE = hop_be.params.asInstanceOf[ChannelRelayParams.FromAnnouncement].channelUpdate + val channelUpdateBE1 = Announcements.makeChannelUpdate(channelUpdateBE.chainHash, priv_b, e, channelUpdateBE.shortChannelId, channelUpdateBE.cltvExpiryDelta, channelUpdateBE.htlcMinimumMsat, channelUpdateBE.feeBaseMsat, channelUpdateBE.feeProportionalMillionths, channelUpdateBE.htlcMaximumMsat.get) val childId = payFsm.stateData.asInstanceOf[PaymentProgress].pending.keys.head - childPayFsm.send(payFsm, PaymentFailed(childId, paymentHash, Seq(RemoteFailure(route.amount, route.hops, Sphinx.DecryptedFailurePacket(b, TemporaryChannelFailure(channelUpdate)))))) + childPayFsm.send(payFsm, PaymentFailed(childId, paymentHash, Seq(RemoteFailure(route.amount, route.hops, Sphinx.DecryptedFailurePacket(b, TemporaryChannelFailure(channelUpdateBE1)))))) // We update the routing hints accordingly before requesting a new route and ignore the channel. val routeRequest = router.expectMsgType[RouteRequest] assert(routeRequest.assistedRoutes.head.head === routingHint) - assert(routeRequest.ignore.channels.map(_.shortChannelId) === Set(channelUpdate.shortChannelId)) + assert(routeRequest.ignore.channels.map(_.shortChannelId) === Set(channelUpdateBE1.shortChannelId)) } test("update routing hints") { _ => @@ -698,13 +700,13 @@ object MultiPartPaymentLifecycleSpec { val channelUpdate_ad = defaultChannelUpdate.copy(shortChannelId = channelId_ad) val channelUpdate_de = defaultChannelUpdate.copy(shortChannelId = channelId_de) - val hop_ab_1 = ChannelHop(a, b, channelUpdate_ab_1) - val hop_ab_2 = ChannelHop(a, b, channelUpdate_ab_2) - val hop_be = ChannelHop(b, e, channelUpdate_be) - val hop_ac_1 = ChannelHop(a, c, channelUpdate_ac_1) - val hop_ac_2 = ChannelHop(a, c, channelUpdate_ac_2) - val hop_ce = ChannelHop(c, e, channelUpdate_ce) - val hop_ad = ChannelHop(a, d, channelUpdate_ad) - val hop_de = ChannelHop(d, e, channelUpdate_de) + val hop_ab_1 = channelHopFromUpdate(a, b, channelUpdate_ab_1) + val hop_ab_2 = channelHopFromUpdate(a, b, channelUpdate_ab_2) + val hop_be = channelHopFromUpdate(b, e, channelUpdate_be) + val hop_ac_1 = channelHopFromUpdate(a, c, channelUpdate_ac_1) + val hop_ac_2 = channelHopFromUpdate(a, c, channelUpdate_ac_2) + val hop_ce = channelHopFromUpdate(c, e, channelUpdate_ce) + val hop_ad = channelHopFromUpdate(a, d, channelUpdate_ad) + val hop_de = channelHopFromUpdate(d, e, channelUpdate_de) } \ No newline at end of file diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala index 1ae69f9230..144368adb7 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala @@ -38,7 +38,7 @@ import fr.acinq.eclair.payment.send.PaymentInitiator.SendPaymentConfig import fr.acinq.eclair.payment.send.PaymentLifecycle import fr.acinq.eclair.payment.send.PaymentLifecycle._ import fr.acinq.eclair.router.Announcements.makeChannelUpdate -import fr.acinq.eclair.router.BaseRouterSpec.channelAnnouncement +import fr.acinq.eclair.router.BaseRouterSpec.{channelAnnouncement, channelHopFromUpdate} import fr.acinq.eclair.router.Graph.WeightRatios import fr.acinq.eclair.router.Router._ import fr.acinq.eclair.router._ @@ -103,7 +103,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { import cfg._ // pre-computed route going from A to D - val route = Route(defaultAmountMsat, ChannelHop(a, b, update_ab) :: ChannelHop(b, c, update_bc) :: ChannelHop(c, d, update_cd) :: Nil) + val route = Route(defaultAmountMsat, channelHopFromUpdate(a, b, update_ab) :: channelHopFromUpdate(b, c, update_bc) :: channelHopFromUpdate(c, d, update_cd) :: Nil) val request = SendPaymentToRoute(sender.ref, Right(route), PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata)) sender.send(paymentFSM, request) routerForwarder.expectNoMessage(100 millis) // we don't need the router, we have the pre-computed route @@ -693,7 +693,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val chan_bh = channelAnnouncement(channelId_bh, priv_b, priv_h, priv_funding_b, priv_funding_h) val channelUpdate_bh = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_b, h, channelId_bh, CltvExpiryDelta(9), htlcMinimumMsat = 0 msat, feeBaseMsat = 0 msat, feeProportionalMillionths = 0, htlcMaximumMsat = 500000000 msat) val channelUpdate_hb = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_h, b, channelId_bh, CltvExpiryDelta(9), htlcMinimumMsat = 0 msat, feeBaseMsat = 10 msat, feeProportionalMillionths = 8, htlcMaximumMsat = 500000000 msat) - assert(Router.getDesc(channelUpdate_bh, chan_bh) === ChannelDesc(channelId_bh, priv_b.publicKey, priv_h.publicKey)) + assert(ChannelDesc(channelUpdate_bh, chan_bh) === ChannelDesc(channelId_bh, priv_b.publicKey, priv_h.publicKey)) val peerConnection = TestProbe() router ! PeerRoutingMessage(peerConnection.ref, remoteNodeId, chan_bh) router ! PeerRoutingMessage(peerConnection.ref, remoteNodeId, channelUpdate_bh) @@ -736,21 +736,21 @@ class PaymentLifecycleSpec extends BaseRouterSpec { test("filter errors properly") { _ => val failures = Seq( LocalFailure(defaultAmountMsat, Nil, RouteNotFound), - RemoteFailure(defaultAmountMsat, ChannelHop(a, b, update_ab) :: Nil, Sphinx.DecryptedFailurePacket(a, TemporaryNodeFailure)), - LocalFailure(defaultAmountMsat, ChannelHop(a, b, update_ab) :: Nil, ChannelUnavailable(ByteVector32.Zeroes)), + RemoteFailure(defaultAmountMsat, channelHopFromUpdate(a, b, update_ab) :: Nil, Sphinx.DecryptedFailurePacket(a, TemporaryNodeFailure)), + LocalFailure(defaultAmountMsat, channelHopFromUpdate(a, b, update_ab) :: Nil, ChannelUnavailable(ByteVector32.Zeroes)), LocalFailure(defaultAmountMsat, Nil, RouteNotFound) ) val filtered = PaymentFailure.transformForUser(failures) val expected = Seq( LocalFailure(defaultAmountMsat, Nil, RouteNotFound), - RemoteFailure(defaultAmountMsat, ChannelHop(a, b, update_ab) :: Nil, Sphinx.DecryptedFailurePacket(a, TemporaryNodeFailure)), - LocalFailure(defaultAmountMsat, ChannelHop(a, b, update_ab) :: Nil, ChannelUnavailable(ByteVector32.Zeroes)) + RemoteFailure(defaultAmountMsat, channelHopFromUpdate(a, b, update_ab) :: Nil, Sphinx.DecryptedFailurePacket(a, TemporaryNodeFailure)), + LocalFailure(defaultAmountMsat, channelHopFromUpdate(a, b, update_ab) :: Nil, ChannelUnavailable(ByteVector32.Zeroes)) ) assert(filtered === expected) } test("ignore failed nodes/channels") { _ => - val route_abcd = ChannelHop(a, b, update_ab) :: ChannelHop(b, c, update_bc) :: ChannelHop(c, d, update_cd) :: Nil + val route_abcd = channelHopFromUpdate(a, b, update_ab) :: channelHopFromUpdate(b, c, update_bc) :: channelHopFromUpdate(c, d, update_cd) :: Nil val testCases = Seq( // local failures -> ignore first channel if there is one (LocalFailure(defaultAmountMsat, Nil, RouteNotFound), Set.empty, Set.empty), @@ -765,8 +765,8 @@ class PaymentLifecycleSpec extends BaseRouterSpec { (RemoteFailure(defaultAmountMsat, route_abcd, Sphinx.DecryptedFailurePacket(c, UnknownNextPeer)), Set.empty, Set(ChannelDesc(channelId_cd, c, d))), (RemoteFailure(defaultAmountMsat, route_abcd, Sphinx.DecryptedFailurePacket(b, FeeInsufficient(100 msat, update_bc))), Set.empty, Set.empty), // unreadable remote failures -> blacklist all nodes except our direct peer and the final recipient - (UnreadableRemoteFailure(defaultAmountMsat, ChannelHop(a, b, update_ab) :: Nil), Set.empty, Set.empty), - (UnreadableRemoteFailure(defaultAmountMsat, ChannelHop(a, b, update_ab) :: ChannelHop(b, c, update_bc) :: ChannelHop(c, d, update_cd) :: ChannelHop(d, e, null) :: Nil), Set(c, d), Set.empty) + (UnreadableRemoteFailure(defaultAmountMsat, channelHopFromUpdate(a, b, update_ab) :: Nil), Set.empty, Set.empty), + (UnreadableRemoteFailure(defaultAmountMsat, channelHopFromUpdate(a, b, update_ab) :: channelHopFromUpdate(b, c, update_bc) :: channelHopFromUpdate(c, d, update_cd) :: ChannelHop(ShortChannelId(5656986L), d, e, null) :: Nil), Set(c, d), Set.empty) ) for ((failure, expectedNodes, expectedChannels) <- testCases) { @@ -810,7 +810,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { import cfg._ // pre-computed route going from A to D - val route = Route(defaultAmountMsat, ChannelHop(a, b, update_ab) :: ChannelHop(b, c, update_bc) :: ChannelHop(c, d, update_cd) :: Nil) + val route = Route(defaultAmountMsat, channelHopFromUpdate(a, b, update_ab) :: channelHopFromUpdate(b, c, update_bc) :: channelHopFromUpdate(c, d, update_cd) :: Nil) val request = SendPaymentToRoute(sender.ref, Right(route), PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata)) sender.send(paymentFSM, request) routerForwarder.expectNoMessage(100 millis) // we don't need the router, we have the pre-computed route diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentPacketSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentPacketSpec.scala index 41cf058551..2e041a6d84 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentPacketSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentPacketSpec.scala @@ -27,6 +27,7 @@ import fr.acinq.eclair.channel.fsm.Channel import fr.acinq.eclair.crypto.Sphinx import fr.acinq.eclair.payment.IncomingPaymentPacket.{ChannelRelayPacket, FinalPacket, NodeRelayPacket, decrypt} import fr.acinq.eclair.payment.OutgoingPaymentPacket._ +import fr.acinq.eclair.router.BaseRouterSpec.channelHopFromUpdate import fr.acinq.eclair.router.Router.{ChannelHop, NodeHop} import fr.acinq.eclair.transactions.Transactions.InputInfo import fr.acinq.eclair.wire.protocol.OnionPaymentPayloadTlv.{AmountToForward, OutgoingCltv, PaymentData} @@ -179,7 +180,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { assert(inner_c.paymentMetadata === None) // c forwards the trampoline payment to d. - val Success((amount_d, expiry_d, onion_d)) = buildPaymentPacket(paymentHash, ChannelHop(c, d, channelUpdate_cd) :: Nil, PaymentOnion.createTrampolinePayload(amount_cd, amount_cd, expiry_cd, randomBytes32(), packet_d)) + val Success((amount_d, expiry_d, onion_d)) = buildPaymentPacket(paymentHash, channelHopFromUpdate(c, d, channelUpdate_cd) :: Nil, PaymentOnion.createTrampolinePayload(amount_cd, amount_cd, expiry_cd, randomBytes32(), packet_d)) assert(amount_d === amount_cd) assert(expiry_d === expiry_cd) val add_d = UpdateAddHtlc(randomBytes32(), 3, amount_d, paymentHash, expiry_d, onion_d.packet) @@ -197,7 +198,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { assert(inner_d.paymentMetadata === None) // d forwards the trampoline payment to e. - val Success((amount_e, expiry_e, onion_e)) = buildPaymentPacket(paymentHash, ChannelHop(d, e, channelUpdate_de) :: Nil, PaymentOnion.createTrampolinePayload(amount_de, amount_de, expiry_de, randomBytes32(), packet_e)) + val Success((amount_e, expiry_e, onion_e)) = buildPaymentPacket(paymentHash, channelHopFromUpdate(d, e, channelUpdate_de) :: Nil, PaymentOnion.createTrampolinePayload(amount_de, amount_de, expiry_de, randomBytes32(), packet_e)) assert(amount_e === amount_de) assert(expiry_e === expiry_de) val add_e = UpdateAddHtlc(randomBytes32(), 4, amount_e, paymentHash, expiry_e, onion_e.packet) @@ -240,7 +241,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { assert(inner_c.paymentSecret === None) // c forwards the trampoline payment to d. - val Success((amount_d, expiry_d, onion_d)) = buildPaymentPacket(paymentHash, ChannelHop(c, d, channelUpdate_cd) :: Nil, PaymentOnion.createTrampolinePayload(amount_cd, amount_cd, expiry_cd, randomBytes32(), packet_d)) + val Success((amount_d, expiry_d, onion_d)) = buildPaymentPacket(paymentHash, channelHopFromUpdate(c, d, channelUpdate_cd) :: Nil, PaymentOnion.createTrampolinePayload(amount_cd, amount_cd, expiry_cd, randomBytes32(), packet_d)) assert(amount_d === amount_cd) assert(expiry_d === expiry_cd) val add_d = UpdateAddHtlc(randomBytes32(), 3, amount_d, paymentHash, expiry_d, onion_d.packet) @@ -309,11 +310,11 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { val Right(ChannelRelayPacket(_, _, packet_c)) = decrypt(UpdateAddHtlc(randomBytes32(), 1, firstAmount, paymentHash, firstExpiry, onion.packet), priv_b.privateKey) val Right(NodeRelayPacket(_, _, _, packet_d)) = decrypt(UpdateAddHtlc(randomBytes32(), 2, amount_bc, paymentHash, expiry_bc, packet_c), priv_c.privateKey) // c forwards the trampoline payment to d. - val Success((amount_d, expiry_d, onion_d)) = buildPaymentPacket(paymentHash, ChannelHop(c, d, channelUpdate_cd) :: Nil, PaymentOnion.createTrampolinePayload(amount_cd, amount_cd, expiry_cd, randomBytes32(), packet_d)) + val Success((amount_d, expiry_d, onion_d)) = buildPaymentPacket(paymentHash, channelHopFromUpdate(c, d, channelUpdate_cd) :: Nil, PaymentOnion.createTrampolinePayload(amount_cd, amount_cd, expiry_cd, randomBytes32(), packet_d)) val Right(NodeRelayPacket(_, _, _, packet_e)) = decrypt(UpdateAddHtlc(randomBytes32(), 3, amount_d, paymentHash, expiry_d, onion_d.packet), priv_d.privateKey) // d forwards an invalid amount to e (the outer total amount doesn't match the inner amount). val invalidTotalAmount = amount_de + 100.msat - val Success((amount_e, expiry_e, onion_e)) = buildPaymentPacket(paymentHash, ChannelHop(d, e, channelUpdate_de) :: Nil, PaymentOnion.createTrampolinePayload(amount_de, invalidTotalAmount, expiry_de, randomBytes32(), packet_e)) + val Success((amount_e, expiry_e, onion_e)) = buildPaymentPacket(paymentHash, channelHopFromUpdate(d, e, channelUpdate_de) :: Nil, PaymentOnion.createTrampolinePayload(amount_de, invalidTotalAmount, expiry_de, randomBytes32(), packet_e)) val Left(failure) = decrypt(UpdateAddHtlc(randomBytes32(), 4, amount_e, paymentHash, expiry_e, onion_e.packet), priv_e.privateKey) assert(failure === FinalIncorrectHtlcAmount(invalidTotalAmount)) } @@ -324,11 +325,11 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { val Right(ChannelRelayPacket(_, _, packet_c)) = decrypt(UpdateAddHtlc(randomBytes32(), 1, firstAmount, paymentHash, firstExpiry, onion.packet), priv_b.privateKey) val Right(NodeRelayPacket(_, _, _, packet_d)) = decrypt(UpdateAddHtlc(randomBytes32(), 2, amount_bc, paymentHash, expiry_bc, packet_c), priv_c.privateKey) // c forwards the trampoline payment to d. - val Success((amount_d, expiry_d, onion_d)) = buildPaymentPacket(paymentHash, ChannelHop(c, d, channelUpdate_cd) :: Nil, PaymentOnion.createTrampolinePayload(amount_cd, amount_cd, expiry_cd, randomBytes32(), packet_d)) + val Success((amount_d, expiry_d, onion_d)) = buildPaymentPacket(paymentHash, channelHopFromUpdate(c, d, channelUpdate_cd) :: Nil, PaymentOnion.createTrampolinePayload(amount_cd, amount_cd, expiry_cd, randomBytes32(), packet_d)) val Right(NodeRelayPacket(_, _, _, packet_e)) = decrypt(UpdateAddHtlc(randomBytes32(), 3, amount_d, paymentHash, expiry_d, onion_d.packet), priv_d.privateKey) // d forwards an invalid expiry to e (the outer expiry doesn't match the inner expiry). val invalidExpiry = expiry_de - CltvExpiryDelta(12) - val Success((amount_e, expiry_e, onion_e)) = buildPaymentPacket(paymentHash, ChannelHop(d, e, channelUpdate_de) :: Nil, PaymentOnion.createTrampolinePayload(amount_de, amount_de, invalidExpiry, randomBytes32(), packet_e)) + val Success((amount_e, expiry_e, onion_e)) = buildPaymentPacket(paymentHash, channelHopFromUpdate(d, e, channelUpdate_de) :: Nil, PaymentOnion.createTrampolinePayload(amount_de, amount_de, invalidExpiry, randomBytes32(), packet_e)) val Left(failure) = decrypt(UpdateAddHtlc(randomBytes32(), 4, amount_e, paymentHash, expiry_e, onion_e.packet), priv_e.privateKey) assert(failure === FinalIncorrectCltvExpiry(invalidExpiry)) } @@ -392,10 +393,10 @@ object PaymentPacketSpec { // simple route a -> b -> c -> d -> e val hops = - ChannelHop(a, b, channelUpdate_ab) :: - ChannelHop(b, c, channelUpdate_bc) :: - ChannelHop(c, d, channelUpdate_cd) :: - ChannelHop(d, e, channelUpdate_de) :: Nil + channelHopFromUpdate(a, b, channelUpdate_ab) :: + channelHopFromUpdate(b, c, channelUpdate_bc) :: + channelHopFromUpdate(c, d, channelUpdate_cd) :: + channelHopFromUpdate(d, e, channelUpdate_de) :: Nil val finalAmount = 42000000 msat val currentBlockCount = 400000 @@ -407,15 +408,15 @@ object PaymentPacketSpec { val expiry_de = finalExpiry val amount_de = finalAmount - val fee_d = nodeFee(channelUpdate_de, amount_de) + val fee_d = nodeFee(channelUpdate_de.relayFees, amount_de) val expiry_cd = expiry_de + channelUpdate_de.cltvExpiryDelta val amount_cd = amount_de + fee_d - val fee_c = nodeFee(channelUpdate_cd, amount_cd) + val fee_c = nodeFee(channelUpdate_cd.relayFees, amount_cd) val expiry_bc = expiry_cd + channelUpdate_cd.cltvExpiryDelta val amount_bc = amount_cd + fee_c - val fee_b = nodeFee(channelUpdate_bc, amount_bc) + val fee_b = nodeFee(channelUpdate_bc.relayFees, amount_bc) val expiry_ab = expiry_bc + channelUpdate_bc.cltvExpiryDelta val amount_ab = amount_bc + fee_b @@ -431,7 +432,7 @@ object PaymentPacketSpec { NodeHop(d, e, channelUpdate_de.cltvExpiryDelta, fee_d) :: Nil val trampolineChannelHops = - ChannelHop(a, b, channelUpdate_ab) :: - ChannelHop(b, c, channelUpdate_bc) :: Nil + channelHopFromUpdate(a, b, channelUpdate_ab) :: + channelHopFromUpdate(b, c, channelUpdate_bc) :: Nil } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PostRestartHtlcCleanerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PostRestartHtlcCleanerSpec.scala index 47d7cb023d..611eadc840 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PostRestartHtlcCleanerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PostRestartHtlcCleanerSpec.scala @@ -30,6 +30,7 @@ import fr.acinq.eclair.db.{OutgoingPayment, OutgoingPaymentStatus, PaymentType} import fr.acinq.eclair.payment.OutgoingPaymentPacket.{Upstream, buildCommand} import fr.acinq.eclair.payment.PaymentPacketSpec._ import fr.acinq.eclair.payment.relay.{PostRestartHtlcCleaner, Relayer} +import fr.acinq.eclair.router.BaseRouterSpec.channelHopFromUpdate import fr.acinq.eclair.router.Router.ChannelHop import fr.acinq.eclair.transactions.Transactions.{ClaimRemoteDelayedOutputTx, InputInfo} import fr.acinq.eclair.transactions.{DirectedHtlc, IncomingHtlc, OutgoingHtlc} @@ -729,7 +730,7 @@ object PostRestartHtlcCleanerSpec { def buildHtlcOut(htlcId: Long, channelId: ByteVector32, paymentHash: ByteVector32): DirectedHtlc = OutgoingHtlc(buildHtlc(htlcId, channelId, paymentHash)) def buildFinalHtlc(htlcId: Long, channelId: ByteVector32, paymentHash: ByteVector32): DirectedHtlc = { - val Success((cmd, _)) = buildCommand(ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, ChannelHop(a, TestConstants.Bob.nodeParams.nodeId, channelUpdate_ab) :: Nil, PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, randomBytes32(), None)) + val Success((cmd, _)) = buildCommand(ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, channelHopFromUpdate(a, TestConstants.Bob.nodeParams.nodeId, channelUpdate_ab) :: Nil, PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, randomBytes32(), None)) IncomingHtlc(UpdateAddHtlc(channelId, htlcId, cmd.amount, cmd.paymentHash, cmd.cltvExpiry, cmd.onion)) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/RelayerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/RelayerSpec.scala index 66cb6852e9..29ad5083fe 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/RelayerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/RelayerSpec.scala @@ -30,6 +30,7 @@ import fr.acinq.eclair.payment.OutgoingPaymentPacket.{Upstream, buildCommand} import fr.acinq.eclair.payment.PaymentPacketSpec._ import fr.acinq.eclair.payment.relay.Relayer._ import fr.acinq.eclair.payment.{OutgoingPaymentPacket, PaymentPacketSpec} +import fr.acinq.eclair.router.BaseRouterSpec.channelHopFromUpdate import fr.acinq.eclair.router.Router.{ChannelHop, NodeHop} import fr.acinq.eclair.wire.protocol._ import fr.acinq.eclair.{NodeParams, TestConstants, randomBytes32, _} @@ -118,7 +119,7 @@ class RelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("applicat val Success((trampolineAmount, trampolineExpiry, trampolineOnion)) = OutgoingPaymentPacket.buildTrampolinePacket(paymentHash, trampolineHops, PaymentOnion.createMultiPartPayload(finalAmount, totalAmount, finalExpiry, paymentSecret, None)) assert(trampolineAmount === finalAmount) assert(trampolineExpiry === finalExpiry) - val Success((cmd, _)) = buildCommand(ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, ChannelHop(a, b, channelUpdate_ab) :: Nil, PaymentOnion.createTrampolinePayload(trampolineAmount, trampolineAmount, trampolineExpiry, randomBytes32(), trampolineOnion.packet)) + val Success((cmd, _)) = buildCommand(ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, channelHopFromUpdate(a, b, channelUpdate_ab) :: Nil, PaymentOnion.createTrampolinePayload(trampolineAmount, trampolineAmount, trampolineExpiry, randomBytes32(), trampolineOnion.packet)) assert(cmd.amount === finalAmount) assert(cmd.cltvExpiry === finalExpiry) val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amount, cmd.paymentHash, cmd.cltvExpiry, cmd.onion) @@ -161,7 +162,7 @@ class RelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("applicat // we use this to build a valid trampoline onion inside a normal onion val trampolineHops = NodeHop(a, b, channelUpdate_ab.cltvExpiryDelta, 0 msat) :: NodeHop(b, c, channelUpdate_bc.cltvExpiryDelta, fee_b) :: Nil val Success((trampolineAmount, trampolineExpiry, trampolineOnion)) = OutgoingPaymentPacket.buildTrampolinePacket(paymentHash, trampolineHops, PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, paymentSecret, None)) - val Success((cmd, _)) = buildCommand(ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, ChannelHop(a, b, channelUpdate_ab) :: Nil, PaymentOnion.createTrampolinePayload(trampolineAmount, trampolineAmount, trampolineExpiry, randomBytes32(), trampolineOnion.packet)) + val Success((cmd, _)) = buildCommand(ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, channelHopFromUpdate(a, b, channelUpdate_ab) :: Nil, PaymentOnion.createTrampolinePayload(trampolineAmount, trampolineAmount, trampolineExpiry, randomBytes32(), trampolineOnion.packet)) // and then manually build an htlc val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amount, cmd.paymentHash, cmd.cltvExpiry, cmd.onion) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/BaseRouterSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/BaseRouterSpec.scala index 90b8afac45..a2ce74b18f 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/BaseRouterSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/BaseRouterSpec.scala @@ -19,7 +19,7 @@ package fr.acinq.eclair.router import akka.actor.ActorRef import akka.actor.typed.scaladsl.adapter.actorRefAdapter import akka.testkit.TestProbe -import fr.acinq.bitcoin.scalacompat.Crypto.PrivateKey +import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} import fr.acinq.bitcoin.scalacompat.Script.{pay2wsh, write} import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, SatoshiLong, Transaction, TxOut} import fr.acinq.eclair.TestConstants.Alice @@ -106,13 +106,13 @@ abstract class BaseRouterSpec extends TestKitBaseClass with FixtureAnyFunSuiteLi // and e --(4)--> f (we are a) within(30 seconds) { // first we make sure that we correctly resolve channelId+direction to nodeId - assert(Router.getDesc(update_ab, chan_ab) === ChannelDesc(chan_ab.shortChannelId, a, b)) - assert(Router.getDesc(update_bc, chan_bc) === ChannelDesc(chan_bc.shortChannelId, b, c)) - assert(Router.getDesc(update_cd, chan_cd) === ChannelDesc(chan_cd.shortChannelId, c, d)) - assert(Router.getDesc(update_ef, chan_ef) === ChannelDesc(chan_ef.shortChannelId, e, f)) - assert(Router.getDesc(update_ag_private, PrivateChannel(a, g, None, None, ChannelMeta(1000 msat, 2000 msat))) === ChannelDesc(channelId_ag_private, a, g)) - assert(Router.getDesc(update_ag_private, PrivateChannel(g, a, None, None, ChannelMeta(2000 msat, 1000 msat))) === ChannelDesc(channelId_ag_private, a, g)) - assert(Router.getDesc(update_gh, chan_gh) === ChannelDesc(chan_gh.shortChannelId, g, h)) + assert(ChannelDesc(update_ab, chan_ab) === ChannelDesc(chan_ab.shortChannelId, a, b)) + assert(ChannelDesc(update_bc, chan_bc) === ChannelDesc(chan_bc.shortChannelId, b, c)) + assert(ChannelDesc(update_cd, chan_cd) === ChannelDesc(chan_cd.shortChannelId, c, d)) + assert(ChannelDesc(update_ef, chan_ef) === ChannelDesc(chan_ef.shortChannelId, e, f)) + assert(ChannelDesc(update_ag_private, PrivateChannel(a, g, None, None, ChannelMeta(1000 msat, 2000 msat))) === ChannelDesc(channelId_ag_private, a, g)) + assert(ChannelDesc(update_ag_private, PrivateChannel(g, a, None, None, ChannelMeta(2000 msat, 1000 msat))) === ChannelDesc(channelId_ag_private, a, g)) + assert(ChannelDesc(update_gh, chan_gh) === ChannelDesc(chan_gh.shortChannelId, g, h)) // let's set up the router val sender = TestProbe() @@ -221,4 +221,7 @@ object BaseRouterSpec { val funding2_sig = Announcements.signChannelAnnouncement(witness, funding2_priv) makeChannelAnnouncement(Block.RegtestGenesisBlock.hash, channelId, node1_priv.publicKey, node2_priv.publicKey, funding1_priv.publicKey, funding2_priv.publicKey, node1_sig, node2_sig, funding1_sig, funding2_sig) } + + def channelHopFromUpdate(nodeId: PublicKey, nextNodeId: PublicKey, channelUpdate: ChannelUpdate): ChannelHop = + ChannelHop(channelUpdate.shortChannelId, nodeId, nextNodeId, ChannelRelayParams.FromAnnouncement(channelUpdate)) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/GraphSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/GraphSpec.scala index 375f14423d..9cac18440f 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/GraphSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/GraphSpec.scala @@ -193,7 +193,7 @@ class GraphSpec extends AnyFunSuite { assert(mutatedGraph2.edgesOf(a).size == 3) // A --> B , A --> B , A --> D assert(mutatedGraph2.getEdgesBetween(a, b).size === 2) - assert(mutatedGraph2.getEdge(edgeForTheSameChannel).get.update.feeBaseMsat === 30.msat) + assert(mutatedGraph2.getEdge(edgeForTheSameChannel).get.params.relayFees.feeBase === 30.msat) } test("remove a vertex with incoming edges and check those edges are removed too") { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/RouteCalculationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/RouteCalculationSpec.scala index 6ecb23ed5e..43816d2074 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/RouteCalculationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/RouteCalculationSpec.scala @@ -21,6 +21,7 @@ import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, ByteVector64, Satoshi, SatoshiLong} import fr.acinq.eclair.payment.Bolt11Invoice.ExtraHop import fr.acinq.eclair.payment.relay.Relayer.RelayFees +import fr.acinq.eclair.router.BaseRouterSpec.channelHopFromUpdate import fr.acinq.eclair.router.Graph.GraphStructure.DirectedGraph.graphEdgeToHop import fr.acinq.eclair.router.Graph.GraphStructure.{DirectedGraph, GraphEdge} import fr.acinq.eclair.router.Graph.{HeuristicsConstants, RichWeight, WeightRatios} @@ -427,19 +428,19 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { val ued = ChannelUpdate(DUMMY_SIG, Block.RegtestGenesisBlock.hash, ShortChannelId(4L), 1 unixsec, ChannelUpdate.ChannelFlags(isNode1 = false, isEnabled = false), CltvExpiryDelta(1), 49 msat, 2507 msat, 147, None) val edges = Seq( - GraphEdge(ChannelDesc(ShortChannelId(1L), a, b), uab, DEFAULT_CAPACITY, None), - GraphEdge(ChannelDesc(ShortChannelId(1L), b, a), uba, DEFAULT_CAPACITY, None), - GraphEdge(ChannelDesc(ShortChannelId(2L), b, c), ubc, DEFAULT_CAPACITY, None), - GraphEdge(ChannelDesc(ShortChannelId(2L), c, b), ucb, DEFAULT_CAPACITY, None), - GraphEdge(ChannelDesc(ShortChannelId(3L), c, d), ucd, DEFAULT_CAPACITY, None), - GraphEdge(ChannelDesc(ShortChannelId(3L), d, c), udc, DEFAULT_CAPACITY, None), - GraphEdge(ChannelDesc(ShortChannelId(4L), d, e), ude, DEFAULT_CAPACITY, None), - GraphEdge(ChannelDesc(ShortChannelId(4L), e, d), ued, DEFAULT_CAPACITY, None) + GraphEdge(ChannelDesc(ShortChannelId(1L), a, b), ChannelRelayParams.FromAnnouncement(uab), DEFAULT_CAPACITY, None), + GraphEdge(ChannelDesc(ShortChannelId(1L), b, a), ChannelRelayParams.FromAnnouncement(uba), DEFAULT_CAPACITY, None), + GraphEdge(ChannelDesc(ShortChannelId(2L), b, c), ChannelRelayParams.FromAnnouncement(ubc), DEFAULT_CAPACITY, None), + GraphEdge(ChannelDesc(ShortChannelId(2L), c, b), ChannelRelayParams.FromAnnouncement(ucb), DEFAULT_CAPACITY, None), + GraphEdge(ChannelDesc(ShortChannelId(3L), c, d), ChannelRelayParams.FromAnnouncement(ucd), DEFAULT_CAPACITY, None), + GraphEdge(ChannelDesc(ShortChannelId(3L), d, c), ChannelRelayParams.FromAnnouncement(udc), DEFAULT_CAPACITY, None), + GraphEdge(ChannelDesc(ShortChannelId(4L), d, e), ChannelRelayParams.FromAnnouncement(ude), DEFAULT_CAPACITY, None), + GraphEdge(ChannelDesc(ShortChannelId(4L), e, d), ChannelRelayParams.FromAnnouncement(ued), DEFAULT_CAPACITY, None) ) val g = DirectedGraph(edges) val Success(route :: Nil) = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) - assert(route.hops === ChannelHop(a, b, uab) :: ChannelHop(b, c, ubc) :: ChannelHop(c, d, ucd) :: ChannelHop(d, e, ude) :: Nil) + assert(route.hops === channelHopFromUpdate(a, b, uab) :: channelHopFromUpdate(b, c, ubc) :: channelHopFromUpdate(c, d, ucd) :: channelHopFromUpdate(d, e, ude) :: Nil) } test("convert extra hops to assisted channels") { @@ -458,10 +459,10 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { val amount = 90000 sat // below RoutingHeuristics.CAPACITY_CHANNEL_LOW val assistedChannels = toAssistedChannels(extraHops, e, amount.toMilliSatoshi) - assert(assistedChannels(extraHop4.shortChannelId) === AssistedChannel(extraHop4, e, 100050.sat.toMilliSatoshi)) - assert(assistedChannels(extraHop3.shortChannelId) === AssistedChannel(extraHop3, d, 100200.sat.toMilliSatoshi)) - assert(assistedChannels(extraHop2.shortChannelId) === AssistedChannel(extraHop2, c, 100400.sat.toMilliSatoshi)) - assert(assistedChannels(extraHop1.shortChannelId) === AssistedChannel(extraHop1, b, 101416.sat.toMilliSatoshi)) + assert(assistedChannels(extraHop4.shortChannelId) === AssistedChannel(e, ChannelRelayParams.FromHint(extraHop4, 100050.sat.toMilliSatoshi))) + assert(assistedChannels(extraHop3.shortChannelId) === AssistedChannel(d, ChannelRelayParams.FromHint(extraHop3, 100200.sat.toMilliSatoshi))) + assert(assistedChannels(extraHop2.shortChannelId) === AssistedChannel(c, ChannelRelayParams.FromHint(extraHop2, 100400.sat.toMilliSatoshi))) + assert(assistedChannels(extraHop1.shortChannelId) === AssistedChannel(b, ChannelRelayParams.FromHint(extraHop1, 101416.sat.toMilliSatoshi))) } test("blacklist routes") { @@ -526,12 +527,12 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { val Success(route1 :: Nil) = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(route2Ids(route1) === 1 :: 2 :: 3 :: 4 :: Nil) - assert(route1.hops(1).lastUpdate.feeBaseMsat === 10.msat) + assert(route1.hops(1).params.relayFees.feeBase === 10.msat) val extraGraphEdges = Set(makeEdge(2L, b, c, 5 msat, 5)) val Success(route2 :: Nil) = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, extraEdges = extraGraphEdges, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(route2Ids(route2) === 1 :: 2 :: 3 :: 4 :: Nil) - assert(route2.hops(1).lastUpdate.feeBaseMsat === 5.msat) + assert(route2.hops(1).params.relayFees.feeBase === 5.msat) } test("compute ignored channels") { @@ -565,7 +566,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { ) val publicChannels = channels.map { case (shortChannelId, announcement) => - val update = edges.find(_.desc.shortChannelId == shortChannelId).get.update + val ChannelRelayParams.FromAnnouncement(update) = edges.find(_.desc.shortChannelId == shortChannelId).get.params val (update_1_opt, update_2_opt) = if (update.channelFlags.isNode1) (Some(update), None) else (None, Some(update)) val pc = PublicChannel(announcement, ByteVector32.Zeroes, Satoshi(1000), update_1_opt, update_2_opt, None) (shortChannelId, pc) @@ -1094,7 +1095,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { val amount = 50000 msat // These pending HTLCs will have already been taken into account in the edge's `balance_opt` field: findMultiPartRoute // should ignore this information. - val pendingHtlcs = Seq(Route(10000 msat, ChannelHop(a, b, edge_ab_1.update) :: Nil), Route(5000 msat, ChannelHop(a, b, edge_ab_2.update) :: Nil)) + val pendingHtlcs = Seq(Route(10000 msat, graphEdgeToHop(edge_ab_1) :: Nil), Route(5000 msat, graphEdgeToHop(edge_ab_2) :: Nil)) val Success(routes) = findMultiPartRoute(g, a, b, amount, 1 msat, pendingHtlcs = pendingHtlcs, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(routes.forall(_.length == 1), routes) checkRouteAmounts(routes, amount, 0 msat) @@ -1566,7 +1567,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { makeEdge(6L, d, e, 50 msat, 0, minHtlc = 100 msat, capacity = 25 sat), )) - val pendingHtlcs = Seq(Route(5000 msat, ChannelHop(a, b, edge_ab.update) :: ChannelHop(b, e, edge_be.update) :: Nil)) + val pendingHtlcs = Seq(Route(5000 msat, graphEdgeToHop(edge_ab) :: graphEdgeToHop(edge_be) :: Nil)) val Success(routes) = findMultiPartRoute(g, a, e, amount, maxFee, pendingHtlcs = pendingHtlcs, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(routes.forall(_.length == 2), routes) checkRouteAmounts(routes, amount, maxFee) @@ -1961,7 +1962,7 @@ object RouteCalculationSpec { capacity: Satoshi = DEFAULT_CAPACITY, balance_opt: Option[MilliSatoshi] = None): GraphEdge = { val update = makeUpdateShort(ShortChannelId(shortChannelId), nodeId1, nodeId2, feeBase, feeProportionalMillionth, minHtlc, maxHtlc, cltvDelta) - GraphEdge(ChannelDesc(ShortChannelId(shortChannelId), nodeId1, nodeId2), update, capacity, balance_opt) + GraphEdge(ChannelDesc(ShortChannelId(shortChannelId), nodeId1, nodeId2), ChannelRelayParams.FromAnnouncement(update), capacity, balance_opt) } def makeUpdateShort(shortChannelId: ShortChannelId, nodeId1: PublicKey, nodeId2: PublicKey, feeBase: MilliSatoshi, feeProportionalMillionth: Int, minHtlc: MilliSatoshi = DEFAULT_AMOUNT_MSAT, maxHtlc: Option[MilliSatoshi] = None, cltvDelta: CltvExpiryDelta = CltvExpiryDelta(0), timestamp: TimestampSecond = 0 unixsec): ChannelUpdate = @@ -1978,19 +1979,19 @@ object RouteCalculationSpec { htlcMaximumMsat = maxHtlc ) - def hops2Ids(hops: Seq[ChannelHop]): Seq[Long] = hops.map(hop => hop.lastUpdate.shortChannelId.toLong) + def hops2Ids(hops: Seq[ChannelHop]): Seq[Long] = hops.map(hop => hop.shortChannelId.toLong) def route2Ids(route: Route): Seq[Long] = hops2Ids(route.hops) def routes2Ids(routes: Seq[Route]): Set[Seq[Long]] = routes.map(route2Ids).toSet - def route2Edges(route: Route): Seq[GraphEdge] = route.hops.map(hop => GraphEdge(ChannelDesc(hop.lastUpdate.shortChannelId, hop.nodeId, hop.nextNodeId), hop.lastUpdate, 0 sat, None)) + def route2Edges(route: Route): Seq[GraphEdge] = route.hops.map(hop => GraphEdge(ChannelDesc(hop.shortChannelId, hop.nodeId, hop.nextNodeId), hop.params, 0 sat, None)) def route2Nodes(route: Route): Seq[(PublicKey, PublicKey)] = route.hops.map(hop => (hop.nodeId, hop.nextNodeId)) def checkIgnoredChannels(routes: Seq[Route], shortChannelIds: Long*): Unit = { shortChannelIds.foreach(shortChannelId => routes.foreach(route => { - assert(route.hops.forall(_.lastUpdate.shortChannelId.toLong != shortChannelId), route) + assert(route.hops.forall(_.shortChannelId.toLong != shortChannelId), route) })) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/RouterSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/RouterSpec.scala index a5fc1bb368..b67eb961a3 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/RouterSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/RouterSpec.scala @@ -29,11 +29,12 @@ import fr.acinq.eclair.io.Peer.PeerRoutingMessage import fr.acinq.eclair.payment.Bolt11Invoice.ExtraHop import fr.acinq.eclair.router.Announcements.{makeChannelUpdate, makeNodeAnnouncement} import fr.acinq.eclair.router.BaseRouterSpec.channelAnnouncement +import fr.acinq.eclair.router.Graph.RoutingHeuristics import fr.acinq.eclair.router.RouteCalculationSpec.{DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, DEFAULT_ROUTE_PARAMS} import fr.acinq.eclair.router.Router._ import fr.acinq.eclair.transactions.Scripts import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{BlockHeight, CltvExpiryDelta, Features, MilliSatoshi, MilliSatoshiLong, ShortChannelId, TestConstants, TimestampSecond, randomKey} +import fr.acinq.eclair.{BlockHeight, CltvExpiryDelta, Features, MilliSatoshi, MilliSatoshiLong, ShortChannelId, TestConstants, TimestampSecond, nodeFee, randomKey} import scodec.bits._ import scala.concurrent.duration._ @@ -479,7 +480,7 @@ class RouterSpec extends BaseRouterSpec { // the route hasn't changed (nodes are the same) assert(response.routes.head.hops.map(_.nodeId) === preComputedRoute.nodes.dropRight(1)) assert(response.routes.head.hops.map(_.nextNodeId) === preComputedRoute.nodes.drop(1)) - assert(response.routes.head.hops.map(_.lastUpdate) === Seq(update_ab, update_bc, update_cd)) + assert(response.routes.head.hops.map(_.params) === Seq(ChannelRelayParams.FromAnnouncement(update_ab), ChannelRelayParams.FromAnnouncement(update_bc), ChannelRelayParams.FromAnnouncement(update_cd))) } test("given a pre-defined channels route add the proper channel updates") { fixture => @@ -493,7 +494,7 @@ class RouterSpec extends BaseRouterSpec { // the route hasn't changed (nodes are the same) assert(response.routes.head.hops.map(_.nodeId) === Seq(a, b, c)) assert(response.routes.head.hops.map(_.nextNodeId) === Seq(b, c, d)) - assert(response.routes.head.hops.map(_.lastUpdate) === Seq(update_ab, update_bc, update_cd)) + assert(response.routes.head.hops.map(_.params) === Seq(ChannelRelayParams.FromAnnouncement(update_ab), ChannelRelayParams.FromAnnouncement(update_bc), ChannelRelayParams.FromAnnouncement(update_cd))) } test("given a pre-defined private channels route add the proper channel updates") { fixture => @@ -506,7 +507,7 @@ class RouterSpec extends BaseRouterSpec { val response = sender.expectMsgType[RouteResponse] assert(response.routes.length === 1) val route = response.routes.head - assert(route.hops.map(_.lastUpdate) === Seq(update_ag_private)) + assert(route.hops.map(_.params) === Seq(ChannelRelayParams.FromAnnouncement(update_ag_private))) assert(route.hops.head.nodeId === a) assert(route.hops.head.nextNodeId === g) } @@ -518,7 +519,7 @@ class RouterSpec extends BaseRouterSpec { val route = response.routes.head assert(route.hops.map(_.nodeId) === Seq(a, g)) assert(route.hops.map(_.nextNodeId) === Seq(g, h)) - assert(route.hops.map(_.lastUpdate) === Seq(update_ag_private, update_gh)) + assert(route.hops.map(_.params) === Seq(ChannelRelayParams.FromAnnouncement(update_ag_private), ChannelRelayParams.FromAnnouncement(update_gh))) } } @@ -530,32 +531,32 @@ class RouterSpec extends BaseRouterSpec { { val invoiceRoutingHint = ExtraHop(b, ShortChannelId(BlockHeight(420000), 516, 1105), 10 msat, 150, CltvExpiryDelta(96)) val preComputedRoute = PredefinedChannelRoute(targetNodeId, Seq(channelId_ab, invoiceRoutingHint.shortChannelId)) - sender.send(router, FinalizeRoute(10000 msat, preComputedRoute, assistedRoutes = Seq(Seq(invoiceRoutingHint)))) + val amount = 10_000.msat + // the amount affects the way we estimate the channel capacity of the hinted channel + assert(amount < RoutingHeuristics.CAPACITY_CHANNEL_LOW) + sender.send(router, FinalizeRoute(amount, preComputedRoute, assistedRoutes = Seq(Seq(invoiceRoutingHint)))) val response = sender.expectMsgType[RouteResponse] assert(response.routes.length === 1) val route = response.routes.head assert(route.hops.map(_.nodeId) === Seq(a, b)) assert(route.hops.map(_.nextNodeId) === Seq(b, targetNodeId)) - assert(route.hops.head.lastUpdate === update_ab) - assert(route.hops.last.lastUpdate.shortChannelId === invoiceRoutingHint.shortChannelId) - assert(route.hops.last.lastUpdate.feeBaseMsat === invoiceRoutingHint.feeBase) - assert(route.hops.last.lastUpdate.feeProportionalMillionths === invoiceRoutingHint.feeProportionalMillionths) - assert(route.hops.last.lastUpdate.cltvExpiryDelta === invoiceRoutingHint.cltvExpiryDelta) + assert(route.hops.head.params === ChannelRelayParams.FromAnnouncement(update_ab)) + assert(route.hops.last.params === ChannelRelayParams.FromHint(invoiceRoutingHint, RoutingHeuristics.CAPACITY_CHANNEL_LOW + nodeFee(invoiceRoutingHint.feeBase, invoiceRoutingHint.feeProportionalMillionths, RoutingHeuristics.CAPACITY_CHANNEL_LOW))) } { val invoiceRoutingHint = ExtraHop(h, ShortChannelId(BlockHeight(420000), 516, 1105), 10 msat, 150, CltvExpiryDelta(96)) val preComputedRoute = PredefinedChannelRoute(targetNodeId, Seq(channelId_ag_private, channelId_gh, invoiceRoutingHint.shortChannelId)) - sender.send(router, FinalizeRoute(10000 msat, preComputedRoute, assistedRoutes = Seq(Seq(invoiceRoutingHint)))) + val amount = RoutingHeuristics.CAPACITY_CHANNEL_LOW * 2 + // the amount affects the way we estimate the channel capacity of the hinted channel + assert(amount > RoutingHeuristics.CAPACITY_CHANNEL_LOW) + sender.send(router, FinalizeRoute(amount, preComputedRoute, assistedRoutes = Seq(Seq(invoiceRoutingHint)))) val response = sender.expectMsgType[RouteResponse] assert(response.routes.length === 1) val route = response.routes.head assert(route.hops.map(_.nodeId) === Seq(a, g, h)) assert(route.hops.map(_.nextNodeId) === Seq(g, h, targetNodeId)) - assert(route.hops.map(_.lastUpdate).dropRight(1) === Seq(update_ag_private, update_gh)) - assert(route.hops.last.lastUpdate.shortChannelId === invoiceRoutingHint.shortChannelId) - assert(route.hops.last.lastUpdate.feeBaseMsat === invoiceRoutingHint.feeBase) - assert(route.hops.last.lastUpdate.feeProportionalMillionths === invoiceRoutingHint.feeProportionalMillionths) - assert(route.hops.last.lastUpdate.cltvExpiryDelta === invoiceRoutingHint.cltvExpiryDelta) + assert(route.hops.map(_.params).dropRight(1) === Seq(ChannelRelayParams.FromAnnouncement(update_ag_private), ChannelRelayParams.FromAnnouncement(update_gh))) + assert(route.hops.last.params === ChannelRelayParams.FromHint(invoiceRoutingHint, amount + nodeFee(invoiceRoutingHint.feeBase, invoiceRoutingHint.feeProportionalMillionths, amount))) } } diff --git a/eclair-node/src/test/resources/api/findroute-full b/eclair-node/src/test/resources/api/findroute-full index d41d006dd2..dff011caa1 100644 --- a/eclair-node/src/test/resources/api/findroute-full +++ b/eclair-node/src/test/resources/api/findroute-full @@ -1 +1 @@ -{"routes":[{"amount":456,"hops":[{"nodeId":"03007e67dc5a8fd2b2ef21cb310ab6359ddb51f3f86a8b79b8b1e23bc3a6ea150a","nextNodeId":"026105f6cb4862810be989385d16f04b0f748f6f2a14040338b1a534d45b4be1c1","lastUpdate":{"signature":"92cf3f12e161391986eb2cd7106ddab41a23c734f8f1ed120fb64f4b91f98f690ecf930388e62965f8aefbf1adafcd25a572669a125396dcfb83615208754679","chainHash":"024b7b3626554c44dcc2454ee3812458bfa68d9fced466edfab470844cb7ffe2","shortChannelId":"1x2x3","timestamp":{"iso":"1970-01-01T00:00:00Z","unix":0},"channelFlags":{"isEnabled":true,"isNode1":true},"cltvExpiryDelta":0,"htlcMinimumMsat":1,"feeBaseMsat":1,"feeProportionalMillionths":1,"tlvStream":{"records":[],"unknown":[]}}},{"nodeId":"026105f6cb4862810be989385d16f04b0f748f6f2a14040338b1a534d45b4be1c1","nextNodeId":"038cfa2b5857843ee90cff91b06f692c0d8fe201921ee6387aee901d64f43699f0","lastUpdate":{"signature":"92cf3f12e161391986eb2cd7106ddab41a23c734f8f1ed120fb64f4b91f98f690ecf930388e62965f8aefbf1adafcd25a572669a125396dcfb83615208754679","chainHash":"024b7b3626554c44dcc2454ee3812458bfa68d9fced466edfab470844cb7ffe2","shortChannelId":"1x2x4","timestamp":{"iso":"1970-01-01T00:00:00Z","unix":0},"channelFlags":{"isEnabled":true,"isNode1":true},"cltvExpiryDelta":0,"htlcMinimumMsat":1,"feeBaseMsat":1,"feeProportionalMillionths":1,"tlvStream":{"records":[],"unknown":[]}}},{"nodeId":"038cfa2b5857843ee90cff91b06f692c0d8fe201921ee6387aee901d64f43699f0","nextNodeId":"02be60276e294c6921240daae33a361d214d02578656df0e74c61a09c3196e51df","lastUpdate":{"signature":"92cf3f12e161391986eb2cd7106ddab41a23c734f8f1ed120fb64f4b91f98f690ecf930388e62965f8aefbf1adafcd25a572669a125396dcfb83615208754679","chainHash":"024b7b3626554c44dcc2454ee3812458bfa68d9fced466edfab470844cb7ffe2","shortChannelId":"1x2x5","timestamp":{"iso":"1970-01-01T00:00:00Z","unix":0},"channelFlags":{"isEnabled":true,"isNode1":true},"cltvExpiryDelta":0,"htlcMinimumMsat":1,"feeBaseMsat":1,"feeProportionalMillionths":1,"tlvStream":{"records":[],"unknown":[]}}}]}]} \ No newline at end of file +{"routes":[{"amount":456,"hops":[{"nodeId":"03007e67dc5a8fd2b2ef21cb310ab6359ddb51f3f86a8b79b8b1e23bc3a6ea150a","nextNodeId":"026105f6cb4862810be989385d16f04b0f748f6f2a14040338b1a534d45b4be1c1","source":{"type":"announcement","channelUpdate":{"signature":"92cf3f12e161391986eb2cd7106ddab41a23c734f8f1ed120fb64f4b91f98f690ecf930388e62965f8aefbf1adafcd25a572669a125396dcfb83615208754679","chainHash":"024b7b3626554c44dcc2454ee3812458bfa68d9fced466edfab470844cb7ffe2","shortChannelId":"1x2x3","timestamp":{"iso":"1970-01-01T00:00:00Z","unix":0},"channelFlags":{"isEnabled":true,"isNode1":true},"cltvExpiryDelta":0,"htlcMinimumMsat":1,"feeBaseMsat":1,"feeProportionalMillionths":1,"tlvStream":{"records":[],"unknown":[]}}}},{"nodeId":"026105f6cb4862810be989385d16f04b0f748f6f2a14040338b1a534d45b4be1c1","nextNodeId":"038cfa2b5857843ee90cff91b06f692c0d8fe201921ee6387aee901d64f43699f0","source":{"type":"announcement","channelUpdate":{"signature":"92cf3f12e161391986eb2cd7106ddab41a23c734f8f1ed120fb64f4b91f98f690ecf930388e62965f8aefbf1adafcd25a572669a125396dcfb83615208754679","chainHash":"024b7b3626554c44dcc2454ee3812458bfa68d9fced466edfab470844cb7ffe2","shortChannelId":"1x2x4","timestamp":{"iso":"1970-01-01T00:00:00Z","unix":0},"channelFlags":{"isEnabled":true,"isNode1":true},"cltvExpiryDelta":0,"htlcMinimumMsat":1,"feeBaseMsat":1,"feeProportionalMillionths":1,"tlvStream":{"records":[],"unknown":[]}}}},{"nodeId":"038cfa2b5857843ee90cff91b06f692c0d8fe201921ee6387aee901d64f43699f0","nextNodeId":"02be60276e294c6921240daae33a361d214d02578656df0e74c61a09c3196e51df","source":{"type":"announcement","channelUpdate":{"signature":"92cf3f12e161391986eb2cd7106ddab41a23c734f8f1ed120fb64f4b91f98f690ecf930388e62965f8aefbf1adafcd25a572669a125396dcfb83615208754679","chainHash":"024b7b3626554c44dcc2454ee3812458bfa68d9fced466edfab470844cb7ffe2","shortChannelId":"1x2x5","timestamp":{"iso":"1970-01-01T00:00:00Z","unix":0},"channelFlags":{"isEnabled":true,"isNode1":true},"cltvExpiryDelta":0,"htlcMinimumMsat":1,"feeBaseMsat":1,"feeProportionalMillionths":1,"tlvStream":{"records":[],"unknown":[]}}}}]}]} \ No newline at end of file diff --git a/eclair-node/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala b/eclair-node/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala index be381b571f..e54c2944b4 100644 --- a/eclair-node/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala +++ b/eclair-node/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala @@ -46,7 +46,7 @@ import fr.acinq.eclair.payment.relay.Relayer.ChannelBalance import fr.acinq.eclair.payment.send.MultiPartPaymentLifecycle.PreimageReceived import fr.acinq.eclair.payment.send.PaymentInitiator.SendPaymentToRouteResponse import fr.acinq.eclair.router.Router -import fr.acinq.eclair.router.Router.PredefinedNodeRoute +import fr.acinq.eclair.router.Router.{ChannelRelayParams, PredefinedNodeRoute} import fr.acinq.eclair.wire.protocol._ import org.json4s.{Formats, Serialization} import org.mockito.scalatest.IdiomaticMockito @@ -1003,10 +1003,12 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM feeProportionalMillionths = 1, htlcMaximumMsat = None ) + val mockChannelUpdate2 = mockChannelUpdate1.copy(shortChannelId = ShortChannelId(BlockHeight(1), 2, 4)) + val mockChannelUpdate3 = mockChannelUpdate1.copy(shortChannelId = ShortChannelId(BlockHeight(1), 2, 5)) - val mockHop1 = Router.ChannelHop(PublicKey.fromBin(ByteVector.fromValidHex("03007e67dc5a8fd2b2ef21cb310ab6359ddb51f3f86a8b79b8b1e23bc3a6ea150a")), PublicKey.fromBin(ByteVector.fromValidHex("026105f6cb4862810be989385d16f04b0f748f6f2a14040338b1a534d45b4be1c1")), mockChannelUpdate1) - val mockHop2 = Router.ChannelHop(mockHop1.nextNodeId, PublicKey.fromBin(ByteVector.fromValidHex("038cfa2b5857843ee90cff91b06f692c0d8fe201921ee6387aee901d64f43699f0")), mockChannelUpdate1.copy(shortChannelId = ShortChannelId(BlockHeight(1), 2, 4))) - val mockHop3 = Router.ChannelHop(mockHop2.nextNodeId, PublicKey.fromBin(ByteVector.fromValidHex("02be60276e294c6921240daae33a361d214d02578656df0e74c61a09c3196e51df")), mockChannelUpdate1.copy(shortChannelId = ShortChannelId(BlockHeight(1), 2, 5))) + val mockHop1 = Router.ChannelHop(mockChannelUpdate1.shortChannelId, PublicKey.fromBin(ByteVector.fromValidHex("03007e67dc5a8fd2b2ef21cb310ab6359ddb51f3f86a8b79b8b1e23bc3a6ea150a")), PublicKey.fromBin(ByteVector.fromValidHex("026105f6cb4862810be989385d16f04b0f748f6f2a14040338b1a534d45b4be1c1")), ChannelRelayParams.FromAnnouncement(mockChannelUpdate1)) + val mockHop2 = Router.ChannelHop(mockChannelUpdate2.shortChannelId, mockHop1.nextNodeId, PublicKey.fromBin(ByteVector.fromValidHex("038cfa2b5857843ee90cff91b06f692c0d8fe201921ee6387aee901d64f43699f0")), ChannelRelayParams.FromAnnouncement(mockChannelUpdate2)) + val mockHop3 = Router.ChannelHop(mockChannelUpdate3.shortChannelId, mockHop2.nextNodeId, PublicKey.fromBin(ByteVector.fromValidHex("02be60276e294c6921240daae33a361d214d02578656df0e74c61a09c3196e51df")), ChannelRelayParams.FromAnnouncement(mockChannelUpdate3)) val mockHops = Seq(mockHop1, mockHop2, mockHop3) val eclair = mock[Eclair] From 95213bcb1fc065beaec18294d4af1ea5a69bced6 Mon Sep 17 00:00:00 2001 From: Pierre-Marie Padiou Date: Fri, 13 May 2022 14:39:57 +0200 Subject: [PATCH 065/121] Remove unused `LocalChannel` class in the router (#2265) Also rename `ChannelDetails` to `KnownChannel`, and some cleanup in tests. --- .../scala/fr/acinq/eclair/router/Router.scala | 42 ++---- .../fr/acinq/eclair/channel/FuzzySpec.scala | 24 ++-- .../fr/acinq/eclair/channel/RestoreSpec.scala | 6 +- .../ChannelStateTestsHelperMethods.scala | 14 +- .../channel/states/e/NormalStateSpec.scala | 60 ++++---- .../channel/states/f/ShutdownStateSpec.scala | 14 +- .../channel/states/h/ClosingStateSpec.scala | 136 +++++++++--------- .../integration/PaymentIntegrationSpec.scala | 6 +- .../eclair/payment/PaymentLifecycleSpec.scala | 66 ++++----- .../acinq/eclair/router/BaseRouterSpec.scala | 58 ++++---- .../fr/acinq/eclair/router/RouterSpec.scala | 84 +++++------ 11 files changed, 238 insertions(+), 272 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala index 1e002d0872..2e2ecdcde1 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala @@ -174,12 +174,6 @@ class Router(val nodeParams: NodeParams, watcher: typed.ActorRef[ZmqWatcher.Comm sender() ! d.nodes.values stay() - case Event(GetLocalChannels, d) => - val scids = d.graph.getIncomingEdgesOf(nodeParams.nodeId).map(_.desc.shortChannelId) - val localChannels = scids.flatMap(scid => d.channels.get(scid).orElse(d.privateChannels.get(scid)).map(c => LocalChannel(nodeParams.nodeId, scid, c))) - sender() ! localChannels - stay() - case Event(GetChannels, d) => sender() ! d.channels.values.map(_.ann) stay() @@ -324,16 +318,16 @@ object Router { } } case class ChannelMeta(balance1: MilliSatoshi, balance2: MilliSatoshi) - sealed trait ChannelDetails { + sealed trait KnownChannel { val capacity: Satoshi def getNodeIdSameSideAs(u: ChannelUpdate): PublicKey def getChannelUpdateSameSideAs(u: ChannelUpdate): Option[ChannelUpdate] def getBalanceSameSideAs(u: ChannelUpdate): Option[MilliSatoshi] - def updateChannelUpdateSameSideAs(u: ChannelUpdate): ChannelDetails - def updateBalances(commitments: AbstractCommitments): ChannelDetails - def applyChannelUpdate(update: Either[LocalChannelUpdate, RemoteChannelUpdate]): ChannelDetails + def updateChannelUpdateSameSideAs(u: ChannelUpdate): KnownChannel + def updateBalances(commitments: AbstractCommitments): KnownChannel + def applyChannelUpdate(update: Either[LocalChannelUpdate, RemoteChannelUpdate]): KnownChannel } - case class PublicChannel(ann: ChannelAnnouncement, fundingTxid: ByteVector32, capacity: Satoshi, update_1_opt: Option[ChannelUpdate], update_2_opt: Option[ChannelUpdate], meta_opt: Option[ChannelMeta]) extends ChannelDetails { + case class PublicChannel(ann: ChannelAnnouncement, fundingTxid: ByteVector32, capacity: Satoshi, update_1_opt: Option[ChannelUpdate], update_2_opt: Option[ChannelUpdate], meta_opt: Option[ChannelMeta]) extends KnownChannel { update_1_opt.foreach(u => assert(u.channelFlags.isNode1)) update_2_opt.foreach(u => assert(!u.channelFlags.isNode1)) @@ -351,7 +345,7 @@ object Router { case Right(rcu) => updateChannelUpdateSameSideAs(rcu.channelUpdate) } } - case class PrivateChannel(localNodeId: PublicKey, remoteNodeId: PublicKey, update_1_opt: Option[ChannelUpdate], update_2_opt: Option[ChannelUpdate], meta: ChannelMeta) extends ChannelDetails { + case class PrivateChannel(localNodeId: PublicKey, remoteNodeId: PublicKey, update_1_opt: Option[ChannelUpdate], update_2_opt: Option[ChannelUpdate], meta: ChannelMeta) extends KnownChannel { val (nodeId1, nodeId2) = if (Announcements.isNode1(localNodeId, remoteNodeId)) (localNodeId, remoteNodeId) else (remoteNodeId, localNodeId) val capacity: Satoshi = (meta.balance1 + meta.balance2).truncateToSatoshi @@ -368,24 +362,14 @@ object Router { case Left(lcu) => updateChannelUpdateSameSideAs(lcu.channelUpdate).updateBalances(lcu.commitments) case Right(rcu) => updateChannelUpdateSameSideAs(rcu.channelUpdate) } - } - case class LocalChannel(localNodeId: PublicKey, shortChannelId: ShortChannelId, channel: ChannelDetails) { - val isPrivate: Boolean = channel match { - case _: PrivateChannel => true - case _ => false - } - val capacity: Satoshi = channel.capacity - val remoteNodeId: PublicKey = channel match { - case c: PrivateChannel => c.remoteNodeId - case c: PublicChannel => if (c.ann.nodeId1 == localNodeId) c.ann.nodeId2 else c.ann.nodeId1 - } - /** Our remote peer's channel_update: this is what must be used in invoice routing hints. */ - val remoteUpdate: Option[ChannelUpdate] = channel match { - case c: PrivateChannel => if (remoteNodeId == c.nodeId1) c.update_1_opt else c.update_2_opt - case c: PublicChannel => if (remoteNodeId == c.ann.nodeId1) c.update_1_opt else c.update_2_opt - } /** Create an invoice routing hint from that channel. Note that if the channel is private, the invoice will leak its existence. */ - def toExtraHop: Option[ExtraHop] = remoteUpdate.map(u => ExtraHop(remoteNodeId, u.shortChannelId, u.feeBaseMsat, u.feeProportionalMillionths, u.cltvExpiryDelta)) + def toIncomingExtraHop: Option[ExtraHop] = { + // we want the incoming channel_update + val remoteUpdate_opt = if (localNodeId == nodeId1) update_2_opt else update_1_opt + remoteUpdate_opt.map { remoteUpdate => + ExtraHop(remoteNodeId, remoteUpdate.shortChannelId, remoteUpdate.feeBaseMsat, remoteUpdate.feeProportionalMillionths, remoteUpdate.cltvExpiryDelta) + } + } } // @formatter:on diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/FuzzySpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/FuzzySpec.scala index 2ca68f370f..e4cefd5cd9 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/FuzzySpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/FuzzySpec.scala @@ -52,7 +52,7 @@ import scala.util.Random class FuzzySpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with ChannelStateTestsBase with Logging { - case class FixtureParam(alice: TestFSMRef[ChannelState, ChannelData, Channel], bob: TestFSMRef[ChannelState, ChannelData, Channel], pipe: ActorRef, relayerA: ActorRef, relayerB: ActorRef, paymentHandlerA: ActorRef, paymentHandlerB: ActorRef) + case class FixtureParam(alice: TestFSMRef[ChannelState, ChannelData, Channel], bob: TestFSMRef[ChannelState, ChannelData, Channel], pipe: ActorRef, alice2relayer: ActorRef, bob2relayer: ActorRef, paymentHandlerA: ActorRef, paymentHandlerB: ActorRef) override def withFixture(test: OneArgTest): Outcome = { val fuzzy = test.tags.contains("fuzzy") @@ -65,20 +65,20 @@ class FuzzySpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Channe TestUtils.forwardOutgoingToPipe(bobPeer, pipe) val alice2blockchain = TestProbe() val bob2blockchain = TestProbe() - val registerA = system.actorOf(Props(new TestRegister())) - val registerB = system.actorOf(Props(new TestRegister())) - val paymentHandlerA = system.actorOf(Props(new PaymentHandler(aliceParams, registerA))) - val paymentHandlerB = system.actorOf(Props(new PaymentHandler(bobParams, registerB))) - val relayerA = system.actorOf(Relayer.props(aliceParams, TestProbe().ref, registerA, paymentHandlerA)) - val relayerB = system.actorOf(Relayer.props(bobParams, TestProbe().ref, registerB, paymentHandlerB)) + val aliceRegister = system.actorOf(Props(new TestRegister())) + val bobRegister = system.actorOf(Props(new TestRegister())) + val alicePaymentHandler = system.actorOf(Props(new PaymentHandler(aliceParams, aliceRegister))) + val bobPaymentHandler = system.actorOf(Props(new PaymentHandler(bobParams, bobRegister))) + val aliceRelayer = system.actorOf(Relayer.props(aliceParams, TestProbe().ref, aliceRegister, alicePaymentHandler)) + val bobRelayer = system.actorOf(Relayer.props(bobParams, TestProbe().ref, bobRegister, bobPaymentHandler)) val wallet = new DummyOnChainWallet() - val alice: TestFSMRef[ChannelState, ChannelData, Channel] = TestFSMRef(new Channel(aliceParams, wallet, bobParams.nodeId, alice2blockchain.ref, relayerA, FakeTxPublisherFactory(alice2blockchain)), alicePeer.ref) - val bob: TestFSMRef[ChannelState, ChannelData, Channel] = TestFSMRef(new Channel(bobParams, wallet, aliceParams.nodeId, bob2blockchain.ref, relayerB, FakeTxPublisherFactory(bob2blockchain)), bobPeer.ref) + val alice: TestFSMRef[ChannelState, ChannelData, Channel] = TestFSMRef(new Channel(aliceParams, wallet, bobParams.nodeId, alice2blockchain.ref, aliceRelayer, FakeTxPublisherFactory(alice2blockchain)), alicePeer.ref) + val bob: TestFSMRef[ChannelState, ChannelData, Channel] = TestFSMRef(new Channel(bobParams, wallet, aliceParams.nodeId, bob2blockchain.ref, bobRelayer, FakeTxPublisherFactory(bob2blockchain)), bobPeer.ref) within(30 seconds) { val aliceInit = Init(Alice.channelParams.initFeatures) val bobInit = Init(Bob.channelParams.initFeatures) - registerA ! alice - registerB ! bob + aliceRegister ! alice + bobRegister ! bob // no announcements alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, Alice.channelParams, pipe, bobInit, channelFlags = ChannelFlags.Private, ChannelConfig.standard, ChannelTypes.Standard) alice2blockchain.expectMsgType[TxPublisher.SetChannelId] @@ -100,7 +100,7 @@ class FuzzySpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Channe awaitCond(alice.stateName == NORMAL, 1 minute) awaitCond(bob.stateName == NORMAL, 1 minute) } - withFixture(test.toNoArgTest(FixtureParam(alice, bob, pipe, relayerA, relayerB, paymentHandlerA, paymentHandlerB))) + withFixture(test.toNoArgTest(FixtureParam(alice, bob, pipe, aliceRelayer, bobRelayer, alicePaymentHandler, bobPaymentHandler))) } class TestRegister() extends Actor with ActorLogging { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/RestoreSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/RestoreSpec.scala index 92f133cada..08cc58de8f 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/RestoreSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/RestoreSpec.scala @@ -70,7 +70,7 @@ class RestoreSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Chan alice.stop() // we restart Alice - val newAlice: TestFSMRef[ChannelState, ChannelData, Channel] = TestFSMRef(new Channel(Alice.nodeParams, wallet, Bob.nodeParams.nodeId, alice2blockchain.ref, relayerA.ref, FakeTxPublisherFactory(alice2blockchain)), alicePeer.ref) + val newAlice: TestFSMRef[ChannelState, ChannelData, Channel] = TestFSMRef(new Channel(Alice.nodeParams, wallet, Bob.nodeParams.nodeId, alice2blockchain.ref, alice2relayer.ref, FakeTxPublisherFactory(alice2blockchain)), alicePeer.ref) newAlice ! INPUT_RESTORED(oldStateData) // then we reconnect them @@ -170,7 +170,7 @@ class RestoreSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Chan alice.stop() // we restart Alice - val newAlice: TestFSMRef[ChannelState, ChannelData, Channel] = TestFSMRef(new Channel(Alice.nodeParams, wallet, Bob.nodeParams.nodeId, alice2blockchain.ref, relayerA.ref, FakeTxPublisherFactory(alice2blockchain)), alicePeer.ref) + val newAlice: TestFSMRef[ChannelState, ChannelData, Channel] = TestFSMRef(new Channel(Alice.nodeParams, wallet, Bob.nodeParams.nodeId, alice2blockchain.ref, alice2relayer.ref, FakeTxPublisherFactory(alice2blockchain)), alicePeer.ref) newAlice ! INPUT_RESTORED(oldStateData) newAlice ! INPUT_RECONNECTED(alice2bob.ref, aliceInit, bobInit) @@ -219,7 +219,7 @@ class RestoreSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Chan .modify(_.channelConf.expiryDelta).setTo(CltvExpiryDelta(147)), ) foreach { newConfig => - val newAlice: TestFSMRef[ChannelState, ChannelData, Channel] = TestFSMRef(new Channel(newConfig, wallet, Bob.nodeParams.nodeId, alice2blockchain.ref, relayerA.ref, FakeTxPublisherFactory(alice2blockchain)), alicePeer.ref) + val newAlice: TestFSMRef[ChannelState, ChannelData, Channel] = TestFSMRef(new Channel(newConfig, wallet, Bob.nodeParams.nodeId, alice2blockchain.ref, alice2relayer.ref, FakeTxPublisherFactory(alice2blockchain)), alicePeer.ref) newAlice ! INPUT_RESTORED(oldStateData) val u1 = channelUpdateListener.expectMsgType[ChannelUpdateParametersChanged] diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala index fd1703295f..d7364eba51 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala @@ -97,8 +97,8 @@ trait ChannelStateTestsHelperMethods extends TestKitBase { alice2blockchain: TestProbe, bob2blockchain: TestProbe, router: TestProbe, - relayerA: TestProbe, - relayerB: TestProbe, + alice2relayer: TestProbe, + bob2relayer: TestProbe, channelUpdateListener: TestProbe, wallet: OnChainWallet, alicePeer: TestProbe, @@ -116,8 +116,8 @@ trait ChannelStateTestsHelperMethods extends TestKitBase { TestUtils.forwardOutgoingToPipe(bobPeer, bob2alice.ref) val alice2blockchain = TestProbe() val bob2blockchain = TestProbe() - val relayerA = TestProbe() - val relayerB = TestProbe() + val alice2relayer = TestProbe() + val bob2relayer = TestProbe() val channelUpdateListener = TestProbe() system.eventStream.subscribe(channelUpdateListener.ref, classOf[LocalChannelUpdate]) system.eventStream.subscribe(channelUpdateListener.ref, classOf[LocalChannelDown]) @@ -132,9 +132,9 @@ trait ChannelStateTestsHelperMethods extends TestKitBase { .modify(_.channelConf.dustLimit).setToIf(tags.contains(ChannelStateTestsTags.HighDustLimitDifferenceBobAlice))(5000 sat) .modify(_.channelConf.maxRemoteDustLimit).setToIf(tags.contains(ChannelStateTestsTags.HighDustLimitDifferenceAliceBob))(10000 sat) .modify(_.channelConf.maxRemoteDustLimit).setToIf(tags.contains(ChannelStateTestsTags.HighDustLimitDifferenceBobAlice))(10000 sat) - val alice: TestFSMRef[ChannelState, ChannelData, Channel] = TestFSMRef(new Channel(finalNodeParamsA, wallet, finalNodeParamsB.nodeId, alice2blockchain.ref, relayerA.ref, FakeTxPublisherFactory(alice2blockchain), origin_opt = Some(aliceOrigin.ref)), alicePeer.ref) - val bob: TestFSMRef[ChannelState, ChannelData, Channel] = TestFSMRef(new Channel(finalNodeParamsB, wallet, finalNodeParamsA.nodeId, bob2blockchain.ref, relayerB.ref, FakeTxPublisherFactory(bob2blockchain)), bobPeer.ref) - SetupFixture(alice, bob, aliceOrigin, alice2bob, bob2alice, alice2blockchain, bob2blockchain, router, relayerA, relayerB, channelUpdateListener, wallet, alicePeer, bobPeer) + val alice: TestFSMRef[ChannelState, ChannelData, Channel] = TestFSMRef(new Channel(finalNodeParamsA, wallet, finalNodeParamsB.nodeId, alice2blockchain.ref, alice2relayer.ref, FakeTxPublisherFactory(alice2blockchain), origin_opt = Some(aliceOrigin.ref)), alicePeer.ref) + val bob: TestFSMRef[ChannelState, ChannelData, Channel] = TestFSMRef(new Channel(finalNodeParamsB, wallet, finalNodeParamsA.nodeId, bob2blockchain.ref, bob2relayer.ref, FakeTxPublisherFactory(bob2blockchain)), bobPeer.ref) + SetupFixture(alice, bob, aliceOrigin, alice2bob, bob2alice, alice2blockchain, bob2blockchain, router, alice2relayer, bob2relayer, channelUpdateListener, wallet, alicePeer, bobPeer) } def computeFeatures(setup: SetupFixture, tags: Set[String]): (LocalParams, LocalParams, SupportedChannelType) = { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala index 5d3baa9bc3..59981ba15b 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala @@ -583,7 +583,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with bob ! htlc awaitCond(bob.stateData == initialData.copy(commitments = initialData.commitments.copy(remoteChanges = initialData.commitments.remoteChanges.copy(proposed = initialData.commitments.remoteChanges.proposed :+ htlc), remoteNextHtlcId = 1))) // bob won't forward the add before it is cross-signed - relayerB.expectNoMessage() + bob2relayer.expectNoMessage() } test("recv UpdateAddHtlc (unexpected id)") { f => @@ -1169,14 +1169,14 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with bob2alice.forward(alice) // at this point bob still hasn't forwarded the htlc downstream - relayerB.expectNoMessage() + bob2relayer.expectNoMessage() // actual test begins alice2bob.expectMsgType[RevokeAndAck] alice2bob.forward(bob) awaitCond(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.remoteNextCommitInfo.isRight) // now bob will forward the htlc downstream - val forward = relayerB.expectMsgType[RelayForward] + val forward = bob2relayer.expectMsgType[RelayForward] assert(forward.add === htlc) } @@ -1273,12 +1273,12 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with crossSign(bob, alice, bob2alice, alice2bob) // Alice forwards HTLCs that fit in the dust exposure. - relayerA.expectMsgAllOf( + alice2relayer.expectMsgAllOf( RelayForward(nonDust), RelayForward(almostTrimmed), RelayForward(trimmed2), ) - relayerA.expectNoMessage(100 millis) + alice2relayer.expectNoMessage(100 millis) // And instantly fails the others. val failedHtlcs = Seq( alice2bob.expectMsgType[UpdateFailHtlc], @@ -1299,8 +1299,8 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with addHtlc(4000.sat.toMilliSatoshi, bob, alice, bob2alice, alice2bob) addHtlc(6000.sat.toMilliSatoshi, bob, alice, bob2alice, alice2bob) crossSign(bob, alice, bob2alice, alice2bob) - relayerA.expectMsgType[RelayForward] - relayerA.expectMsgType[RelayForward] + alice2relayer.expectMsgType[RelayForward] + alice2relayer.expectMsgType[RelayForward] // Alice sends HTLCs to Bob that add 10 000 sat to the dust exposure but doesn't sign them yet. addHtlc(6500.sat.toMilliSatoshi, alice, bob, alice2bob, bob2alice) @@ -1321,8 +1321,8 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with bob2alice.forward(alice) // Alice forwards HTLCs that fit in the dust exposure and instantly fails the others. - relayerA.expectMsg(RelayForward(acceptedHtlc)) - relayerA.expectNoMessage(100 millis) + alice2relayer.expectMsg(RelayForward(acceptedHtlc)) + alice2relayer.expectNoMessage(100 millis) assert(alice2bob.expectMsgType[UpdateFailHtlc].id === rejectedHtlc.id) alice2bob.expectMsgType[CommitSig] alice2bob.expectNoMessage(100 millis) @@ -1336,7 +1336,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // Bob sends HTLCs to Alice that add 10 500 sat to the dust exposure. (1 to 10).foreach(_ => addHtlc(1050.sat.toMilliSatoshi, bob, alice, bob2alice, alice2bob)) crossSign(bob, alice, bob2alice, alice2bob) - (1 to 10).foreach(_ => relayerA.expectMsgType[RelayForward]) + (1 to 10).foreach(_ => alice2relayer.expectMsgType[RelayForward]) // Alice sends HTLCs to Bob that add 10 500 sat to the dust exposure but doesn't sign them yet. (1 to 10).foreach(_ => addHtlc(1050.sat.toMilliSatoshi, alice, bob, alice2bob, bob2alice)) @@ -1355,8 +1355,8 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with bob2alice.forward(alice) // Alice forwards HTLCs that fit in the dust exposure and instantly fails the others. - (1 to 3).foreach(_ => relayerA.expectMsgType[RelayForward]) - relayerA.expectNoMessage(100 millis) + (1 to 3).foreach(_ => alice2relayer.expectMsgType[RelayForward]) + alice2relayer.expectNoMessage(100 millis) (1 to 5).foreach(_ => alice2bob.expectMsgType[UpdateFailHtlc]) alice2bob.expectMsgType[CommitSig] alice2bob.expectNoMessage(100 millis) @@ -1407,13 +1407,13 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice2bob.expectMsgType[CommitSig] alice2bob.forward(bob) // alice still hasn't forwarded the fail because it is not yet cross-signed - relayerA.expectNoMessage() + alice2relayer.expectNoMessage() // actual test begins bob2alice.expectMsgType[RevokeAndAck] bob2alice.forward(alice) // alice will forward the fail upstream - val forward = relayerA.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.RemoteFail]] + val forward = alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.RemoteFail]] assert(forward.result.fail === fail) assert(forward.htlc === htlc) } @@ -1433,13 +1433,13 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice2bob.expectMsgType[CommitSig] alice2bob.forward(bob) // alice still hasn't forwarded the fail because it is not yet cross-signed - relayerA.expectNoMessage() + alice2relayer.expectNoMessage() // actual test begins bob2alice.expectMsgType[RevokeAndAck] bob2alice.forward(alice) // alice will forward the fail upstream - val forward = relayerA.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.RemoteFailMalformed]] + val forward = alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.RemoteFailMalformed]] assert(forward.result.fail === fail) assert(forward.htlc === htlc) } @@ -1605,7 +1605,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with awaitCond(alice.stateData == initialState.copy( commitments = initialState.commitments.copy(remoteChanges = initialState.commitments.remoteChanges.copy(initialState.commitments.remoteChanges.proposed :+ fulfill)))) // alice immediately propagates the fulfill upstream - val forward = relayerA.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.RemoteFulfill]] + val forward = alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.RemoteFulfill]] assert(forward.result.fulfill === fulfill) assert(forward.htlc === htlc) } @@ -1661,7 +1661,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with import f._ val (_, htlc) = addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) - relayerB.expectMsgType[RelayForward] + bob2relayer.expectMsgType[RelayForward] val tx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx // actual test begins @@ -1809,7 +1809,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with awaitCond(alice.stateData == initialState.copy( commitments = initialState.commitments.copy(remoteChanges = initialState.commitments.remoteChanges.copy(initialState.commitments.remoteChanges.proposed :+ fail)))) // alice won't forward the fail before it is cross-signed - relayerA.expectNoMessage() + alice2relayer.expectNoMessage() } test("recv UpdateFailHtlc") { @@ -1843,7 +1843,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with awaitCond(alice.stateData == initialState.copy( commitments = initialState.commitments.copy(remoteChanges = initialState.commitments.remoteChanges.copy(initialState.commitments.remoteChanges.proposed :+ fail)))) // alice won't forward the fail before it is cross-signed - relayerA.expectNoMessage() + alice2relayer.expectNoMessage() bob ! CMD_SIGN() val sig = bob2alice.expectMsgType[CommitSig] @@ -2307,7 +2307,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(localUpdate.channelUpdate.feeBaseMsat == newFeeBaseMsat) assert(localUpdate.channelUpdate.feeProportionalMillionths == newFeeProportionalMillionth) assert(localUpdate.channelUpdate.cltvExpiryDelta == newCltvExpiryDelta) - relayerA.expectNoMessage(1 seconds) + alice2relayer.expectNoMessage(1 seconds) } def testCmdClose(f: FixtureParam, script_opt: Option[ByteVector]): Unit = { @@ -2480,7 +2480,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // actual test starts here alice ! CMD_FORCECLOSE(sender.ref) sender.expectMsgType[RES_SUCCESS[CMD_FORCECLOSE]] - val addSettled = relayerA.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.ChannelFailureBeforeSigned.type]] + val addSettled = alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.ChannelFailureBeforeSigned.type]] assert(addSettled.htlc == htlc1) } @@ -3007,7 +3007,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // bob publishes his current commit tx val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx alice ! WatchFundingSpentTriggered(bobCommitTx) - val addSettled = relayerA.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.ChannelFailureBeforeSigned.type]] + val addSettled = alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.ChannelFailureBeforeSigned.type]] assert(addSettled.htlc == htlc1) } @@ -3096,7 +3096,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // bob publishes his current commit tx val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx alice ! WatchFundingSpentTriggered(bobCommitTx) - val addSettled = relayerA.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.ChannelFailureBeforeSigned.type]] + val addSettled = alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.ChannelFailureBeforeSigned.type]] assert(addSettled.htlc == htlc2) } @@ -3229,7 +3229,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // bob publishes his current commit tx alice ! WatchFundingSpentTriggered(bobRevokedCommitTx) - val addSettled = relayerA.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.ChannelFailureBeforeSigned.type]] + val addSettled = alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.ChannelFailureBeforeSigned.type]] assert(addSettled.htlc == htlc3) } @@ -3396,7 +3396,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // actual test starts here alice ! Error(ByteVector32.Zeroes, "oops") - val addSettled = relayerA.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.ChannelFailureBeforeSigned.type]] + val addSettled = alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.ChannelFailureBeforeSigned.type]] assert(addSettled.htlc == htlc1) } @@ -3554,10 +3554,10 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // actual test starts here Thread.sleep(1100) alice ! INPUT_DISCONNECTED - val addSettled1 = relayerA.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult]] + val addSettled1 = alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult]] assert(addSettled1.htlc == htlc1) assert(addSettled1.result.isInstanceOf[HtlcResult.DisconnectedBeforeSigned]) - val addSettled2 = relayerA.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult]] + val addSettled2 = alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult]] assert(addSettled2.htlc == htlc2) assert(addSettled2.result.isInstanceOf[HtlcResult.DisconnectedBeforeSigned]) assert(!channelUpdateListener.expectMsgType[LocalChannelUpdate].channelUpdate.channelFlags.isEnabled) @@ -3601,8 +3601,8 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // actual test starts here Thread.sleep(1100) alice ! INPUT_DISCONNECTED - assert(relayerA.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.DisconnectedBeforeSigned]].htlc.paymentHash === htlc1.paymentHash) - assert(relayerA.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.DisconnectedBeforeSigned]].htlc.paymentHash === htlc2.paymentHash) + assert(alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.DisconnectedBeforeSigned]].htlc.paymentHash === htlc1.paymentHash) + assert(alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.DisconnectedBeforeSigned]].htlc.paymentHash === htlc2.paymentHash) val update2a = channelUpdateListener.expectMsgType[LocalChannelUpdate] assert(update1a.channelUpdate.timestamp < update2a.channelUpdate.timestamp) assert(!update2a.channelUpdate.channelFlags.isEnabled) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/f/ShutdownStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/f/ShutdownStateSpec.scala index 1c7afd2671..4ba4bebb71 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/f/ShutdownStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/f/ShutdownStateSpec.scala @@ -87,8 +87,8 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit bob2alice.forward(alice) alice2bob.expectMsgType[RevokeAndAck] alice2bob.forward(bob) - relayerB.expectMsgType[RelayForward] - relayerB.expectMsgType[RelayForward] + bob2relayer.expectMsgType[RelayForward] + bob2relayer.expectMsgType[RelayForward] // alice initiates a closing alice ! CMD_CLOSE(sender.ref, None, None) alice2bob.expectMsgType[Shutdown] @@ -480,13 +480,13 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit alice2bob.expectMsgType[CommitSig] alice2bob.forward(bob) // alice still hasn't forwarded the fail because it is not yet cross-signed - relayerA.expectNoMessage() + alice2relayer.expectNoMessage() // actual test begins bob2alice.expectMsgType[RevokeAndAck] bob2alice.forward(alice) // alice will forward the fail upstream - val forward = relayerA.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.RemoteFail]] + val forward = alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.RemoteFail]] assert(forward.result.fail === fail) } @@ -503,13 +503,13 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit alice2bob.expectMsgType[CommitSig] alice2bob.forward(bob) // alice still hasn't forwarded the fail because it is not yet cross-signed - relayerA.expectNoMessage() + alice2relayer.expectNoMessage() // actual test begins bob2alice.expectMsgType[RevokeAndAck] bob2alice.forward(alice) // alice will forward the fail upstream - val forward = relayerA.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.RemoteFailMalformed]] + val forward = alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.RemoteFailMalformed]] assert(forward.result.fail === fail) } @@ -601,7 +601,7 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit val newFeeProportionalMillionth = TestConstants.Alice.nodeParams.relayParams.publicChannelFees.feeProportionalMillionths * 2 alice ! CMD_UPDATE_RELAY_FEE(sender.ref, newFeeBaseMsat, newFeeProportionalMillionth, cltvExpiryDelta_opt = None) sender.expectMsgType[RES_SUCCESS[CMD_UPDATE_RELAY_FEE]] - relayerA.expectNoMessage(1 seconds) + alice2relayer.expectNoMessage(1 seconds) } test("recv CurrentBlockCount (no htlc timed out)") { f => diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala index 933cd79311..6fcaca1652 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala @@ -45,7 +45,7 @@ import scala.concurrent.duration._ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with ChannelStateTestsBase { - case class FixtureParam(alice: TestFSMRef[ChannelState, ChannelData, Channel], bob: TestFSMRef[ChannelState, ChannelData, Channel], alice2bob: TestProbe, bob2alice: TestProbe, alice2blockchain: TestProbe, bob2blockchain: TestProbe, relayerA: TestProbe, relayerB: TestProbe, channelUpdateListener: TestProbe, txListener: TestProbe, bobCommitTxs: List[CommitTxAndRemoteSig]) + case class FixtureParam(alice: TestFSMRef[ChannelState, ChannelData, Channel], bob: TestFSMRef[ChannelState, ChannelData, Channel], alice2bob: TestProbe, bob2alice: TestProbe, alice2blockchain: TestProbe, bob2blockchain: TestProbe, alice2relayer: TestProbe, bob2relayer: TestProbe, channelUpdateListener: TestProbe, txListener: TestProbe, bobCommitTxs: List[CommitTxAndRemoteSig]) override def withFixture(test: OneArgTest): Outcome = { val setup = init() @@ -91,7 +91,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with awaitCond(bob.stateName == WAIT_FOR_FUNDING_CONFIRMED) system.eventStream.subscribe(eventListener.ref, classOf[TransactionPublished]) system.eventStream.subscribe(eventListener.ref, classOf[TransactionConfirmed]) - withFixture(test.toNoArgTest(FixtureParam(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain, relayerA, relayerB, channelUpdateListener, eventListener, Nil))) + withFixture(test.toNoArgTest(FixtureParam(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain, alice2relayer, bob2relayer, channelUpdateListener, eventListener, Nil))) } } else { within(30 seconds) { @@ -101,11 +101,11 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val bobCommitTxs: List[CommitTxAndRemoteSig] = (for (amt <- List(100000000 msat, 200000000 msat, 300000000 msat)) yield { val (r, htlc) = addHtlc(amt, alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) - relayerB.expectMsgType[RelayForward] + bob2relayer.expectMsgType[RelayForward] val bobCommitTx1 = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.commitTxAndRemoteSig fulfillHtlc(htlc.id, r, bob, alice, bob2alice, alice2bob) // alice forwards the fulfill upstream - relayerA.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.Fulfill]] + alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.Fulfill]] crossSign(bob, alice, bob2alice, alice2bob) // bob confirms that it has forwarded the fulfill to alice awaitCond(bob.underlyingActor.nodeParams.db.pendingCommands.listSettlementCommands(htlc.channelId).isEmpty) @@ -115,7 +115,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with awaitCond(alice.stateName == NORMAL) awaitCond(bob.stateName == NORMAL) - withFixture(test.toNoArgTest(FixtureParam(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain, relayerA, relayerB, channelUpdateListener, eventListener, bobCommitTxs))) + withFixture(test.toNoArgTest(FixtureParam(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain, alice2relayer, bob2relayer, channelUpdateListener, eventListener, bobCommitTxs))) } } } @@ -337,7 +337,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // alice sends an htlc to bob val (ra1, htlca1) = addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) - relayerB.expectMsgType[RelayForward] + bob2relayer.expectMsgType[RelayForward] localClose(alice, alice2blockchain) val initialState = alice.stateData.asInstanceOf[DATA_CLOSING] assert(initialState.localCommitPublished.isDefined) @@ -348,14 +348,14 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // scenario 1: bob claims the htlc output from the commit tx using its preimage val claimHtlcSuccessFromCommitTx = Transaction(version = 0, txIn = TxIn(outPoint = OutPoint(randomBytes32(), 0), signatureScript = ByteVector.empty, sequence = 0, witness = Scripts.witnessClaimHtlcSuccessFromCommitTx(Transactions.PlaceHolderSig, ra1, ByteVector.fill(130)(33))) :: Nil, txOut = Nil, lockTime = 0) alice ! WatchOutputSpentTriggered(claimHtlcSuccessFromCommitTx) - val fulfill1 = relayerA.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFulfill]] + val fulfill1 = alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFulfill]] assert(fulfill1.htlc === htlca1) assert(fulfill1.result.paymentPreimage === ra1) // scenario 2: bob claims the htlc output from his own commit tx using its preimage (let's assume both parties had published their commitment tx) val claimHtlcSuccessTx = Transaction(version = 0, txIn = TxIn(outPoint = OutPoint(randomBytes32(), 0), signatureScript = ByteVector.empty, sequence = 0, witness = Scripts.witnessHtlcSuccess(Transactions.PlaceHolderSig, Transactions.PlaceHolderSig, ra1, ByteVector.fill(130)(33), Transactions.DefaultCommitmentFormat)) :: Nil, txOut = Nil, lockTime = 0) alice ! WatchOutputSpentTriggered(claimHtlcSuccessTx) - val fulfill2 = relayerA.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFulfill]] + val fulfill2 = alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFulfill]] assert(fulfill2.htlc === htlca1) assert(fulfill2.result.paymentPreimage === ra1) @@ -391,13 +391,13 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(listener.expectMsgType[LocalCommitConfirmed].refundAtBlock == BlockHeight(42) + bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localParams.toSelfDelay.toInt) assert(listener.expectMsgType[PaymentSettlingOnChain].paymentHash == htlca1.paymentHash) // htlcs below dust will never reach the chain, once the commit tx is confirmed we can consider them failed - assert(relayerA.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc === htlca2) - relayerA.expectNoMessage(100 millis) + assert(alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc === htlca2) + alice2relayer.expectNoMessage(100 millis) alice ! WatchTxConfirmedTriggered(BlockHeight(200), 0, closingState.claimMainDelayedOutputTx.get.tx) alice ! WatchTxConfirmedTriggered(BlockHeight(201), 0, htlcTimeoutTx) assert(alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.get.irrevocablySpent.values.toSet === Set(closingState.commitTx, closingState.claimMainDelayedOutputTx.get.tx, htlcTimeoutTx)) - assert(relayerA.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc === htlca1) - relayerA.expectNoMessage(100 millis) + assert(alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc === htlca1) + alice2relayer.expectNoMessage(100 millis) // We claim the htlc-delayed output now that the HTLC tx has been confirmed. val claimHtlcDelayedTx = alice2blockchain.expectMsgType[PublishFinalTx] @@ -444,22 +444,22 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // if commit tx and htlc-timeout txs end up in the same block, we may receive the htlc-timeout confirmation before the commit tx confirmation alice ! WatchTxConfirmedTriggered(BlockHeight(42), 0, htlcTimeoutTxs(0)) - val forwardedFail1 = relayerA.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc - relayerA.expectNoMessage(250 millis) + val forwardedFail1 = alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc + alice2relayer.expectNoMessage(250 millis) alice ! WatchTxConfirmedTriggered(BlockHeight(42), 1, closingState.commitTx) - assert(relayerA.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc === dust) - relayerA.expectNoMessage(250 millis) + assert(alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc === dust) + alice2relayer.expectNoMessage(250 millis) alice ! WatchTxConfirmedTriggered(BlockHeight(200), 0, closingState.claimMainDelayedOutputTx.get.tx) alice ! WatchTxConfirmedTriggered(BlockHeight(202), 0, htlcTimeoutTxs(1)) - val forwardedFail2 = relayerA.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc - relayerA.expectNoMessage(250 millis) + val forwardedFail2 = alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc + alice2relayer.expectNoMessage(250 millis) alice ! WatchTxConfirmedTriggered(BlockHeight(202), 1, htlcTimeoutTxs(2)) - val forwardedFail3 = relayerA.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc - relayerA.expectNoMessage(250 millis) + val forwardedFail3 = alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc + alice2relayer.expectNoMessage(250 millis) alice ! WatchTxConfirmedTriggered(BlockHeight(203), 0, htlcTimeoutTxs(3)) - val forwardedFail4 = relayerA.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc + val forwardedFail4 = alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc assert(Set(forwardedFail1, forwardedFail2, forwardedFail3, forwardedFail4) === Set(htlca1, htlca2, htlca3, htlca4)) - relayerA.expectNoMessage(250 millis) + alice2relayer.expectNoMessage(250 millis) awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.get.claimHtlcDelayedTxs.length === 4) val claimHtlcDelayedTxs = alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.get.claimHtlcDelayedTxs @@ -491,10 +491,10 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, aliceCommitTx) // so she fails it val origin = alice.stateData.asInstanceOf[DATA_CLOSING].commitments.originChannels(htlc.id) - relayerA.expectMsg(RES_ADD_SETTLED(origin, htlc, HtlcResult.OnChainFail(HtlcOverriddenByLocalCommit(channelId(alice), htlc)))) + alice2relayer.expectMsg(RES_ADD_SETTLED(origin, htlc, HtlcResult.OnChainFail(HtlcOverriddenByLocalCommit(channelId(alice), htlc)))) // the htlc will not settle on chain listener.expectNoMessage(2 seconds) - relayerA.expectNoMessage(100 millis) + alice2relayer.expectNoMessage(100 millis) } test("recv WatchTxConfirmedTriggered (local commit with fulfill only signed by local)") { f => @@ -502,7 +502,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // bob sends an htlc val (r, htlc) = addHtlc(110000000 msat, bob, alice, bob2alice, alice2bob) crossSign(bob, alice, bob2alice, alice2bob) - relayerA.expectMsgType[RelayForward] + alice2relayer.expectMsgType[RelayForward] val aliceCommitTx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx assert(aliceCommitTx.txOut.size === 3) // 2 main outputs + 1 htlc @@ -546,10 +546,10 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, closingState.commitTx) // so she fails it val origin = alice.stateData.asInstanceOf[DATA_CLOSING].commitments.originChannels(htlc.id) - relayerA.expectMsg(RES_ADD_SETTLED(origin, htlc, HtlcResult.OnChainFail(HtlcOverriddenByLocalCommit(channelId(alice), htlc)))) + alice2relayer.expectMsg(RES_ADD_SETTLED(origin, htlc, HtlcResult.OnChainFail(HtlcOverriddenByLocalCommit(channelId(alice), htlc)))) // the htlc will not settle on chain listener.expectNoMessage(2 seconds) - relayerA.expectNoMessage(100 millis) + alice2relayer.expectNoMessage(100 millis) } test("recv INPUT_RESTORED (local commit)") { f => @@ -620,7 +620,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, bobCommitTx) // so she fails it val origin = alice.stateData.asInstanceOf[DATA_CLOSING].commitments.originChannels(htlc.id) - relayerA.expectMsg(RES_ADD_SETTLED(origin, htlc, HtlcResult.OnChainFail(HtlcOverriddenByLocalCommit(channelId(alice), htlc)))) + alice2relayer.expectMsg(RES_ADD_SETTLED(origin, htlc, HtlcResult.OnChainFail(HtlcOverriddenByLocalCommit(channelId(alice), htlc)))) // the htlc will not settle on chain listener.expectNoMessage(2 seconds) } @@ -725,17 +725,17 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice ! WatchTxConfirmedTriggered(BlockHeight(42), 0, bobCommitTx) alice ! WatchTxConfirmedTriggered(BlockHeight(45), 0, closingState.claimMainOutputTx.get.tx) - relayerA.expectNoMessage(100 millis) + alice2relayer.expectNoMessage(100 millis) alice ! WatchTxConfirmedTriggered(BlockHeight(201), 0, claimHtlcTimeoutTxs(0)) - val forwardedFail1 = relayerA.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc - relayerA.expectNoMessage(250 millis) + val forwardedFail1 = alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc + alice2relayer.expectNoMessage(250 millis) alice ! WatchTxConfirmedTriggered(BlockHeight(202), 0, claimHtlcTimeoutTxs(1)) - val forwardedFail2 = relayerA.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc - relayerA.expectNoMessage(250 millis) + val forwardedFail2 = alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc + alice2relayer.expectNoMessage(250 millis) alice ! WatchTxConfirmedTriggered(BlockHeight(203), 1, claimHtlcTimeoutTxs(2)) - val forwardedFail3 = relayerA.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc + val forwardedFail3 = alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc assert(Set(forwardedFail1, forwardedFail2, forwardedFail3) === Set(htlca1, htlca2, htlca3)) - relayerA.expectNoMessage(250 millis) + alice2relayer.expectNoMessage(250 millis) awaitCond(alice.stateName == CLOSED) } @@ -756,7 +756,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // An HTLC Bob -> Alice is cross-signed that will be fulfilled later. val (r1, htlc1) = addHtlc(110000000 msat, CltvExpiryDelta(48), bob, alice, bob2alice, alice2bob) crossSign(bob, alice, bob2alice, alice2bob) - relayerA.expectMsgType[RelayForward] + alice2relayer.expectMsgType[RelayForward] // An HTLC Alice -> Bob is only signed by Alice: Bob has two spendable commit tx. val (_, htlc2) = addHtlc(95000000 msat, CltvExpiryDelta(144), alice, bob, alice2bob, bob2alice) @@ -791,13 +791,13 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, bobCommitTx) // The second htlc was not included in the commit tx published on-chain, so we can consider it failed - assert(relayerA.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc === htlc2) + assert(alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc === htlc2) alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, closingState.claimMainOutputTx.get.tx) alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, claimHtlcSuccessTx) assert(alice.stateData.asInstanceOf[DATA_CLOSING].remoteCommitPublished.get.irrevocablySpent.values.toSet === Set(bobCommitTx, closingState.claimMainOutputTx.get.tx, claimHtlcSuccessTx)) awaitCond(alice.stateName == CLOSED) alice2blockchain.expectNoMessage(100 millis) - relayerA.expectNoMessage(100 millis) + alice2relayer.expectNoMessage(100 millis) } test("recv INPUT_RESTORED (remote commit)") { f => @@ -868,17 +868,17 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice ! WatchTxConfirmedTriggered(BlockHeight(42), 0, bobCommitTx) assert(txListener.expectMsgType[TransactionConfirmed].tx === bobCommitTx) alice ! WatchTxConfirmedTriggered(BlockHeight(45), 0, closingState.claimMainOutputTx.get.tx) - relayerA.expectNoMessage(100 millis) + alice2relayer.expectNoMessage(100 millis) alice ! WatchTxConfirmedTriggered(BlockHeight(201), 0, claimHtlcTimeoutTxs(0)) - val forwardedFail1 = relayerA.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc - relayerA.expectNoMessage(250 millis) + val forwardedFail1 = alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc + alice2relayer.expectNoMessage(250 millis) alice ! WatchTxConfirmedTriggered(BlockHeight(202), 0, claimHtlcTimeoutTxs(1)) - val forwardedFail2 = relayerA.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc - relayerA.expectNoMessage(250 millis) + val forwardedFail2 = alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc + alice2relayer.expectNoMessage(250 millis) alice ! WatchTxConfirmedTriggered(BlockHeight(203), 1, claimHtlcTimeoutTxs(2)) - val forwardedFail3 = relayerA.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc + val forwardedFail3 = alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc assert(Set(forwardedFail1, forwardedFail2, forwardedFail3) === htlcs) - relayerA.expectNoMessage(250 millis) + alice2relayer.expectNoMessage(250 millis) awaitCond(alice.stateName == CLOSED) } @@ -888,17 +888,17 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val claimHtlcTimeoutTxs = getClaimHtlcTimeoutTxs(closingState).map(_.tx) alice ! WatchTxConfirmedTriggered(BlockHeight(42), 0, bobCommitTx) assert(closingState.claimMainOutputTx.isEmpty) // with static_remotekey we don't claim out main output - relayerA.expectNoMessage(100 millis) + alice2relayer.expectNoMessage(100 millis) alice ! WatchTxConfirmedTriggered(BlockHeight(201), 0, claimHtlcTimeoutTxs(0)) - val forwardedFail1 = relayerA.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc - relayerA.expectNoMessage(250 millis) + val forwardedFail1 = alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc + alice2relayer.expectNoMessage(250 millis) alice ! WatchTxConfirmedTriggered(BlockHeight(202), 0, claimHtlcTimeoutTxs(1)) - val forwardedFail2 = relayerA.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc - relayerA.expectNoMessage(250 millis) + val forwardedFail2 = alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc + alice2relayer.expectNoMessage(250 millis) alice ! WatchTxConfirmedTriggered(BlockHeight(203), 1, claimHtlcTimeoutTxs(2)) - val forwardedFail3 = relayerA.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc + val forwardedFail3 = alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc assert(Set(forwardedFail1, forwardedFail2, forwardedFail3) === htlcs) - relayerA.expectNoMessage(250 millis) + alice2relayer.expectNoMessage(250 millis) awaitCond(alice.stateName == CLOSED) } @@ -908,17 +908,17 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val claimHtlcTimeoutTxs = getClaimHtlcTimeoutTxs(closingState).map(_.tx) alice ! WatchTxConfirmedTriggered(BlockHeight(42), 0, bobCommitTx) alice ! WatchTxConfirmedTriggered(BlockHeight(45), 0, closingState.claimMainOutputTx.get.tx) - relayerA.expectNoMessage(100 millis) + alice2relayer.expectNoMessage(100 millis) alice ! WatchTxConfirmedTriggered(BlockHeight(201), 0, claimHtlcTimeoutTxs(0)) - val forwardedFail1 = relayerA.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc - relayerA.expectNoMessage(250 millis) + val forwardedFail1 = alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc + alice2relayer.expectNoMessage(250 millis) alice ! WatchTxConfirmedTriggered(BlockHeight(202), 0, claimHtlcTimeoutTxs(1)) - val forwardedFail2 = relayerA.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc - relayerA.expectNoMessage(250 millis) + val forwardedFail2 = alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc + alice2relayer.expectNoMessage(250 millis) alice ! WatchTxConfirmedTriggered(BlockHeight(203), 1, claimHtlcTimeoutTxs(2)) - val forwardedFail3 = relayerA.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc + val forwardedFail3 = alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc assert(Set(forwardedFail1, forwardedFail2, forwardedFail3) === htlcs) - relayerA.expectNoMessage(250 millis) + alice2relayer.expectNoMessage(250 millis) awaitCond(alice.stateName == CLOSED) } @@ -927,7 +927,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // An HTLC Bob -> Alice is cross-signed that will be fulfilled later. val (r1, htlc1) = addHtlc(110000000 msat, CltvExpiryDelta(64), bob, alice, bob2alice, alice2bob) crossSign(bob, alice, bob2alice, alice2bob) - relayerA.expectMsgType[RelayForward] + alice2relayer.expectMsgType[RelayForward] // An HTLC Alice -> Bob is only signed by Alice: Bob has two spendable commit tx. val (_, htlc2) = addHtlc(95000000 msat, CltvExpiryDelta(32), alice, bob, alice2bob, bob2alice) @@ -970,10 +970,10 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, closingState.claimMainOutputTx.get.tx) alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, claimHtlcSuccessTx) alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, claimHtlcTimeoutTx) - assert(relayerA.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc === htlc2) + assert(alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc === htlc2) awaitCond(alice.stateName == CLOSED) alice2blockchain.expectNoMessage(100 millis) - relayerA.expectNoMessage(100 millis) + alice2relayer.expectNoMessage(100 millis) } test("recv INPUT_RESTORED (next remote commit, anchor outputs zero fee htlc txs)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => @@ -1579,8 +1579,8 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with } // alice's first htlc has been failed - assert(relayerA.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.Fail]].htlc === htlcs1.head) - relayerA.expectNoMessage(1 second) + assert(alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.Fail]].htlc === htlcs1.head) + alice2relayer.expectNoMessage(1 second) // bob publishes one of his revoked txs which quickly confirms alice ! WatchFundingSpentTriggered(bobRevokedTx) @@ -1589,12 +1589,12 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // alice should fail all pending htlcs val htlcFails = Seq( - relayerA.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]], - relayerA.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]], - relayerA.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]] + alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]], + alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]], + alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]] ).map(_.htlc).toSet assert(htlcFails === Set(htlcs1(1), htlcs2(0), htlcs2(1))) - relayerA.expectNoMessage(1 second) + alice2relayer.expectNoMessage(1 second) } test("recv WatchTxConfirmedTriggered (one revoked tx, pending htlcs)") { f => diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/PaymentIntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/PaymentIntegrationSpec.scala index 246f9b4ea8..1e85d07c88 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/PaymentIntegrationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/PaymentIntegrationSpec.scala @@ -558,9 +558,9 @@ class PaymentIntegrationSpec extends IntegrationSpec { val start = TimestampMilli.now() val (sender, eventListener) = (TestProbe(), TestProbe()) nodes("A").system.eventStream.subscribe(eventListener.ref, classOf[PaymentMetadataReceived]) - sender.send(nodes("B").relayer, Relayer.GetOutgoingChannels()) - val channelUpdate_ba = sender.expectMsgType[Relayer.OutgoingChannels].channels.filter(c => c.nextNodeId == nodes("A").nodeParams.nodeId).head.channelUpdate - val routingHints = List(List(ExtraHop(nodes("B").nodeParams.nodeId, channelUpdate_ba.shortChannelId, channelUpdate_ba.feeBaseMsat, channelUpdate_ba.feeProportionalMillionths, channelUpdate_ba.cltvExpiryDelta))) + + sender.send(nodes("A").router, Router.GetRouterData) + val routingHints = List(sender.expectMsgType[Router.Data].privateChannels.head._2.toIncomingExtraHop.toList) val amount = 3000000000L.msat sender.send(nodes("A").paymentHandler, ReceivePayment(Some(amount), Left("trampoline to non-trampoline is so #vintage"), extraHops = routingHints)) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala index 144368adb7..c0fedefb3a 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala @@ -294,7 +294,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val WaitingForComplete(_, cmd1, Nil, _, ignore1, route) = paymentFSM.stateData assert(ignore1.nodes.isEmpty) - register.expectMsg(ForwardShortId(paymentFSM, channelId_ab, cmd1)) + register.expectMsg(ForwardShortId(paymentFSM, scid_ab, cmd1)) sender.send(paymentFSM, addCompleted(HtlcResult.RemoteFail(UpdateFailHtlc(ByteVector32.Zeroes, 0, randomBytes32())))) // unparsable message // then the payment lifecycle will ask for a new route excluding all intermediate nodes @@ -306,7 +306,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val WaitingForComplete(_, cmd2, _, _, ignore2, _) = paymentFSM.stateData assert(ignore2.nodes === Set(c)) // and reply a 2nd time with an unparsable failure - register.expectMsg(ForwardShortId(paymentFSM, channelId_ab, cmd2)) + register.expectMsg(ForwardShortId(paymentFSM, scid_ab, cmd2)) sender.send(paymentFSM, addCompleted(HtlcResult.RemoteFail(UpdateFailHtlc(ByteVector32.Zeroes, 0, defaultPaymentHash)))) // unparsable message // we allow 2 tries, so we send a 2nd request to the router @@ -334,11 +334,11 @@ class PaymentLifecycleSpec extends BaseRouterSpec { awaitCond(paymentFSM.stateName == WAITING_FOR_PAYMENT_COMPLETE) val WaitingForComplete(_, cmd1, Nil, _, _, _) = paymentFSM.stateData - register.expectMsg(ForwardShortId(paymentFSM, channelId_ab, cmd1)) + register.expectMsg(ForwardShortId(paymentFSM, scid_ab, cmd1)) sender.send(paymentFSM, RES_ADD_FAILED(cmd1, ChannelUnavailable(ByteVector32.Zeroes), None)) // then the payment lifecycle will ask for a new route excluding the channel - routerForwarder.expectMsg(defaultRouteRequest(nodeParams.nodeId, d, cfg).copy(ignore = Ignore(Set.empty, Set(ChannelDesc(channelId_ab, a, b))))) + routerForwarder.expectMsg(defaultRouteRequest(nodeParams.nodeId, d, cfg).copy(ignore = Ignore(Set.empty, Set(ChannelDesc(scid_ab, a, b))))) awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending)) // payment is still pending because the error is recoverable } @@ -358,7 +358,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { register.send(paymentFSM, ForwardShortIdFailure(fwd)) // then the payment lifecycle will ask for a new route excluding the channel - routerForwarder.expectMsg(defaultRouteRequest(nodeParams.nodeId, d, cfg).copy(ignore = Ignore(Set.empty, Set(ChannelDesc(channelId_ab, a, b))))) + routerForwarder.expectMsg(defaultRouteRequest(nodeParams.nodeId, d, cfg).copy(ignore = Ignore(Set.empty, Set(ChannelDesc(scid_ab, a, b))))) awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending)) // payment is still pending because the error is recoverable } @@ -377,11 +377,11 @@ class PaymentLifecycleSpec extends BaseRouterSpec { awaitCond(paymentFSM.stateName == WAITING_FOR_PAYMENT_COMPLETE) val WaitingForComplete(_, cmd1, Nil, _, _, _) = paymentFSM.stateData - register.expectMsg(ForwardShortId(paymentFSM, channelId_ab, cmd1)) + register.expectMsg(ForwardShortId(paymentFSM, scid_ab, cmd1)) sender.send(paymentFSM, addCompleted(HtlcResult.RemoteFailMalformed(UpdateFailMalformedHtlc(ByteVector32.Zeroes, 0, randomBytes32(), FailureMessageCodecs.BADONION)))) // then the payment lifecycle will ask for a new route excluding the channel - routerForwarder.expectMsg(defaultRouteRequest(a, d, cfg).copy(ignore = Ignore(Set.empty, Set(ChannelDesc(channelId_ab, a, b))))) + routerForwarder.expectMsg(defaultRouteRequest(a, d, cfg).copy(ignore = Ignore(Set.empty, Set(ChannelDesc(scid_ab, a, b))))) awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending)) } @@ -400,7 +400,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { awaitCond(paymentFSM.stateName == WAITING_FOR_PAYMENT_COMPLETE) val WaitingForComplete(_, cmd1, Nil, _, _, _) = paymentFSM.stateData - register.expectMsg(ForwardShortId(paymentFSM, channelId_ab, cmd1)) + register.expectMsg(ForwardShortId(paymentFSM, scid_ab, cmd1)) sender.send(paymentFSM, addCompleted(HtlcResult.OnChainFail(HtlcsTimedoutDownstream(randomBytes32(), Set.empty)))) // this error is fatal @@ -423,12 +423,12 @@ class PaymentLifecycleSpec extends BaseRouterSpec { awaitCond(paymentFSM.stateName == WAITING_FOR_PAYMENT_COMPLETE) val WaitingForComplete(_, cmd1, Nil, _, _, _) = paymentFSM.stateData - register.expectMsg(ForwardShortId(paymentFSM, channelId_ab, cmd1)) + register.expectMsg(ForwardShortId(paymentFSM, scid_ab, cmd1)) val update_bc_disabled = update_bc.copy(channelFlags = ChannelUpdate.ChannelFlags(isNode1 = true, isEnabled = false)) sender.send(paymentFSM, addCompleted(HtlcResult.DisconnectedBeforeSigned(update_bc_disabled))) // then the payment lifecycle will ask for a new route excluding the channel - routerForwarder.expectMsg(defaultRouteRequest(a, d, cfg).copy(ignore = Ignore(Set.empty, Set(ChannelDesc(channelId_ab, a, b))))) + routerForwarder.expectMsg(defaultRouteRequest(a, d, cfg).copy(ignore = Ignore(Set.empty, Set(ChannelDesc(scid_ab, a, b))))) awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending)) } @@ -445,7 +445,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { awaitCond(paymentFSM.stateName == WAITING_FOR_PAYMENT_COMPLETE) val WaitingForComplete(_, cmd1, Nil, sharedSecrets1, _, route) = paymentFSM.stateData - register.expectMsg(ForwardShortId(paymentFSM, channelId_ab, cmd1)) + register.expectMsg(ForwardShortId(paymentFSM, scid_ab, cmd1)) val failure = TemporaryChannelFailure(update_bc) sender.send(paymentFSM, addCompleted(HtlcResult.RemoteFail(UpdateFailHtlc(ByteVector32.Zeroes, 0, Sphinx.FailurePacket.create(sharedSecrets1.head._1, failure))))) @@ -475,10 +475,10 @@ class PaymentLifecycleSpec extends BaseRouterSpec { routerForwarder.forward(routerFixture.router) awaitCond(paymentFSM.stateName == WAITING_FOR_PAYMENT_COMPLETE) val WaitingForComplete(_, cmd1, Nil, sharedSecrets1, _, route1) = paymentFSM.stateData - register.expectMsg(ForwardShortId(paymentFSM, channelId_ab, cmd1)) + register.expectMsg(ForwardShortId(paymentFSM, scid_ab, cmd1)) // we change the cltv expiry - val channelUpdate_bc_modified = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_b, c, channelId_bc, CltvExpiryDelta(42), htlcMinimumMsat = update_bc.htlcMinimumMsat, feeBaseMsat = update_bc.feeBaseMsat, feeProportionalMillionths = update_bc.feeProportionalMillionths, htlcMaximumMsat = update_bc.htlcMaximumMsat.get) + val channelUpdate_bc_modified = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_b, c, scid_bc, CltvExpiryDelta(42), htlcMinimumMsat = update_bc.htlcMinimumMsat, feeBaseMsat = update_bc.feeBaseMsat, feeProportionalMillionths = update_bc.feeProportionalMillionths, htlcMaximumMsat = update_bc.htlcMaximumMsat.get) val failure = IncorrectCltvExpiry(CltvExpiry(5), channelUpdate_bc_modified) // and node replies with a failure containing a new channel update sender.send(paymentFSM, addCompleted(HtlcResult.RemoteFail(UpdateFailHtlc(ByteVector32.Zeroes, 0, Sphinx.FailurePacket.create(sharedSecrets1.head._1, failure))))) @@ -492,10 +492,10 @@ class PaymentLifecycleSpec extends BaseRouterSpec { // router answers with a new route, taking into account the new update awaitCond(paymentFSM.stateName == WAITING_FOR_PAYMENT_COMPLETE) val WaitingForComplete(_, cmd2, _, sharedSecrets2, _, route2) = paymentFSM.stateData - register.expectMsg(ForwardShortId(paymentFSM, channelId_ab, cmd2)) + register.expectMsg(ForwardShortId(paymentFSM, scid_ab, cmd2)) // we change the cltv expiry one more time - val channelUpdate_bc_modified_2 = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_b, c, channelId_bc, CltvExpiryDelta(43), htlcMinimumMsat = update_bc.htlcMinimumMsat, feeBaseMsat = update_bc.feeBaseMsat, feeProportionalMillionths = update_bc.feeProportionalMillionths, htlcMaximumMsat = update_bc.htlcMaximumMsat.get) + val channelUpdate_bc_modified_2 = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_b, c, scid_bc, CltvExpiryDelta(43), htlcMinimumMsat = update_bc.htlcMinimumMsat, feeBaseMsat = update_bc.feeBaseMsat, feeProportionalMillionths = update_bc.feeProportionalMillionths, htlcMaximumMsat = update_bc.htlcMaximumMsat.get) val failure2 = IncorrectCltvExpiry(CltvExpiry(5), channelUpdate_bc_modified_2) // and node replies with a failure containing a new channel update sender.send(paymentFSM, addCompleted(HtlcResult.RemoteFail(UpdateFailHtlc(ByteVector32.Zeroes, 0, Sphinx.FailurePacket.create(sharedSecrets2.head._1, failure2))))) @@ -524,7 +524,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { routerForwarder.forward(routerFixture.router) awaitCond(paymentFSM.stateName == WAITING_FOR_PAYMENT_COMPLETE) val WaitingForComplete(_, cmd1, Nil, sharedSecrets1, _, _) = paymentFSM.stateData - register.expectMsg(ForwardShortId(paymentFSM, channelId_ab, cmd1)) + register.expectMsg(ForwardShortId(paymentFSM, scid_ab, cmd1)) // the node replies with a temporary failure containing the same update as the one we already have (likely a balance issue) val failure = TemporaryChannelFailure(update_bc) @@ -544,8 +544,8 @@ class PaymentLifecycleSpec extends BaseRouterSpec { // we build an assisted route for channel bc and cd val assistedRoutes = Seq(Seq( - ExtraHop(b, channelId_bc, update_bc.feeBaseMsat, update_bc.feeProportionalMillionths, update_bc.cltvExpiryDelta), - ExtraHop(c, channelId_cd, update_cd.feeBaseMsat, update_cd.feeProportionalMillionths, update_cd.cltvExpiryDelta) + ExtraHop(b, scid_bc, update_bc.feeBaseMsat, update_bc.feeProportionalMillionths, update_bc.cltvExpiryDelta), + ExtraHop(c, scid_cd, update_cd.feeBaseMsat, update_cd.feeProportionalMillionths, update_cd.cltvExpiryDelta) )) val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata), 5, assistedRoutes = assistedRoutes, routeParams = defaultRouteParams) @@ -557,10 +557,10 @@ class PaymentLifecycleSpec extends BaseRouterSpec { routerForwarder.forward(routerFixture.router) awaitCond(paymentFSM.stateName == WAITING_FOR_PAYMENT_COMPLETE) val WaitingForComplete(_, cmd1, Nil, sharedSecrets1, _, _) = paymentFSM.stateData - register.expectMsg(ForwardShortId(paymentFSM, channelId_ab, cmd1)) + register.expectMsg(ForwardShortId(paymentFSM, scid_ab, cmd1)) // we change the cltv expiry - val channelUpdate_bc_modified = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_b, c, channelId_bc, CltvExpiryDelta(42), htlcMinimumMsat = update_bc.htlcMinimumMsat, feeBaseMsat = update_bc.feeBaseMsat, feeProportionalMillionths = update_bc.feeProportionalMillionths, htlcMaximumMsat = update_bc.htlcMaximumMsat.get) + val channelUpdate_bc_modified = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_b, c, scid_bc, CltvExpiryDelta(42), htlcMinimumMsat = update_bc.htlcMinimumMsat, feeBaseMsat = update_bc.feeBaseMsat, feeProportionalMillionths = update_bc.feeProportionalMillionths, htlcMaximumMsat = update_bc.htlcMaximumMsat.get) val failure = IncorrectCltvExpiry(CltvExpiry(5), channelUpdate_bc_modified) // and node replies with a failure containing a new channel update sender.send(paymentFSM, addCompleted(HtlcResult.RemoteFail(UpdateFailHtlc(ByteVector32.Zeroes, 0, Sphinx.FailurePacket.create(sharedSecrets1.head._1, failure))))) @@ -569,8 +569,8 @@ class PaymentLifecycleSpec extends BaseRouterSpec { routerForwarder.expectMsg(channelUpdate_bc_modified) awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending)) // 1 failure but not final, the payment is still PENDING val assistedRoutes1 = Seq(Seq( - ExtraHop(b, channelId_bc, update_bc.feeBaseMsat, update_bc.feeProportionalMillionths, channelUpdate_bc_modified.cltvExpiryDelta), - ExtraHop(c, channelId_cd, update_cd.feeBaseMsat, update_cd.feeProportionalMillionths, update_cd.cltvExpiryDelta) + ExtraHop(b, scid_bc, update_bc.feeBaseMsat, update_bc.feeProportionalMillionths, channelUpdate_bc_modified.cltvExpiryDelta), + ExtraHop(c, scid_cd, update_cd.feeBaseMsat, update_cd.feeProportionalMillionths, update_cd.cltvExpiryDelta) )) routerForwarder.expectMsg(defaultRouteRequest(nodeParams.nodeId, d, cfg).copy(assistedRoutes = assistedRoutes1)) routerForwarder.forward(routerFixture.router) @@ -578,7 +578,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { // router answers with a new route, taking into account the new update awaitCond(paymentFSM.stateName == WAITING_FOR_PAYMENT_COMPLETE) val WaitingForComplete(_, cmd2, _, _, _, _) = paymentFSM.stateData - register.expectMsg(ForwardShortId(paymentFSM, channelId_ab, cmd2)) + register.expectMsg(ForwardShortId(paymentFSM, scid_ab, cmd2)) assert(cmd2.cltvExpiry > cmd1.cltvExpiry) } @@ -588,7 +588,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { import cfg._ // we build an assisted route for channel cd - val assistedRoutes = Seq(Seq(ExtraHop(c, channelId_cd, update_cd.feeBaseMsat, update_cd.feeProportionalMillionths, update_cd.cltvExpiryDelta))) + val assistedRoutes = Seq(Seq(ExtraHop(c, scid_cd, update_cd.feeBaseMsat, update_cd.feeProportionalMillionths, update_cd.cltvExpiryDelta))) val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata), 1, assistedRoutes = assistedRoutes, routeParams = defaultRouteParams) sender.send(paymentFSM, request) awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending)) @@ -597,10 +597,10 @@ class PaymentLifecycleSpec extends BaseRouterSpec { routerForwarder.forward(routerFixture.router) awaitCond(paymentFSM.stateName == WAITING_FOR_PAYMENT_COMPLETE) val WaitingForComplete(_, cmd1, Nil, sharedSecrets1, _, _) = paymentFSM.stateData - register.expectMsg(ForwardShortId(paymentFSM, channelId_ab, cmd1)) + register.expectMsg(ForwardShortId(paymentFSM, scid_ab, cmd1)) // we disable the channel - val channelUpdate_cd_disabled = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_c, d, channelId_cd, CltvExpiryDelta(42), update_cd.htlcMinimumMsat, update_cd.feeBaseMsat, update_cd.feeProportionalMillionths, update_cd.htlcMaximumMsat.get, enable = false) + val channelUpdate_cd_disabled = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_c, d, scid_cd, CltvExpiryDelta(42), update_cd.htlcMinimumMsat, update_cd.feeBaseMsat, update_cd.feeProportionalMillionths, update_cd.htlcMaximumMsat.get, enable = false) val failure = ChannelDisabled(channelUpdate_cd_disabled.messageFlags, channelUpdate_cd_disabled.channelFlags, channelUpdate_cd_disabled) val failureOnion = Sphinx.FailurePacket.wrap(Sphinx.FailurePacket.create(sharedSecrets1(1)._1, failure), sharedSecrets1.head._1) sender.send(paymentFSM, addCompleted(HtlcResult.RemoteFail(UpdateFailHtlc(ByteVector32.Zeroes, 0, failureOnion)))) @@ -624,12 +624,12 @@ class PaymentLifecycleSpec extends BaseRouterSpec { awaitCond(paymentFSM.stateName == WAITING_FOR_PAYMENT_COMPLETE) val WaitingForComplete(_, cmd1, Nil, sharedSecrets1, _, route1) = paymentFSM.stateData - register.expectMsg(ForwardShortId(paymentFSM, channelId_ab, cmd1)) + register.expectMsg(ForwardShortId(paymentFSM, scid_ab, cmd1)) sender.send(paymentFSM, addCompleted(HtlcResult.RemoteFail(UpdateFailHtlc(ByteVector32.Zeroes, 0, Sphinx.FailurePacket.create(sharedSecrets1.head._1, failure))))) // payment lifecycle forwards the embedded channelUpdate to the router awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE) - routerForwarder.expectMsg(defaultRouteRequest(nodeParams.nodeId, d, cfg).copy(ignore = Ignore(Set.empty, Set(ChannelDesc(channelId_bc, b, c))))) + routerForwarder.expectMsg(defaultRouteRequest(nodeParams.nodeId, d, cfg).copy(ignore = Ignore(Set.empty, Set(ChannelDesc(scid_bc, b, c))))) routerForwarder.forward(router) // we allow 2 tries, so we send a 2nd request to the router, which won't find another route @@ -755,14 +755,14 @@ class PaymentLifecycleSpec extends BaseRouterSpec { // local failures -> ignore first channel if there is one (LocalFailure(defaultAmountMsat, Nil, RouteNotFound), Set.empty, Set.empty), (LocalFailure(defaultAmountMsat, NodeHop(a, b, CltvExpiryDelta(144), 0 msat) :: NodeHop(b, c, CltvExpiryDelta(144), 0 msat) :: Nil, RouteNotFound), Set.empty, Set.empty), - (LocalFailure(defaultAmountMsat, route_abcd, new RuntimeException("fatal")), Set.empty, Set(ChannelDesc(channelId_ab, a, b))), + (LocalFailure(defaultAmountMsat, route_abcd, new RuntimeException("fatal")), Set.empty, Set(ChannelDesc(scid_ab, a, b))), // remote failure from final recipient -> all intermediate nodes behaved correctly (RemoteFailure(defaultAmountMsat, route_abcd, Sphinx.DecryptedFailurePacket(d, IncorrectOrUnknownPaymentDetails(100 msat, BlockHeight(42)))), Set.empty, Set.empty), // remote failures from intermediate nodes -> depending on the failure, ignore either the failing node or its outgoing channel (RemoteFailure(defaultAmountMsat, route_abcd, Sphinx.DecryptedFailurePacket(b, PermanentNodeFailure)), Set(b), Set.empty), (RemoteFailure(defaultAmountMsat, route_abcd, Sphinx.DecryptedFailurePacket(c, TemporaryNodeFailure)), Set(c), Set.empty), - (RemoteFailure(defaultAmountMsat, route_abcd, Sphinx.DecryptedFailurePacket(b, PermanentChannelFailure)), Set.empty, Set(ChannelDesc(channelId_bc, b, c))), - (RemoteFailure(defaultAmountMsat, route_abcd, Sphinx.DecryptedFailurePacket(c, UnknownNextPeer)), Set.empty, Set(ChannelDesc(channelId_cd, c, d))), + (RemoteFailure(defaultAmountMsat, route_abcd, Sphinx.DecryptedFailurePacket(b, PermanentChannelFailure)), Set.empty, Set(ChannelDesc(scid_bc, b, c))), + (RemoteFailure(defaultAmountMsat, route_abcd, Sphinx.DecryptedFailurePacket(c, UnknownNextPeer)), Set.empty, Set(ChannelDesc(scid_cd, c, d))), (RemoteFailure(defaultAmountMsat, route_abcd, Sphinx.DecryptedFailurePacket(b, FeeInsufficient(100 msat, update_bc))), Set.empty, Set.empty), // unreadable remote failures -> blacklist all nodes except our direct peer and the final recipient (UnreadableRemoteFailure(defaultAmountMsat, channelHopFromUpdate(a, b, update_ab) :: Nil), Set.empty, Set.empty), @@ -782,7 +782,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { ) val ignore = PaymentFailure.updateIgnored(failures, Ignore.empty) assert(ignore.nodes === Set(c)) - assert(ignore.channels === Set(ChannelDesc(channelId_ab, a, b), ChannelDesc(channelId_bc, b, c))) + assert(ignore.channels === Set(ChannelDesc(scid_ab, a, b), ChannelDesc(scid_bc, b, c))) } test("disable database and events") { routerFixture => @@ -823,7 +823,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { assert(outgoing.copy(createdAt = 0 unixms) === OutgoingPayment(id, parentId, Some(defaultExternalId), defaultPaymentHash, PaymentType.Standard, defaultAmountMsat, defaultAmountMsat, d, 0 unixms, Some(defaultInvoice), OutgoingPaymentStatus.Pending)) // we change the cltv expiry - val channelUpdate_bc_modified = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_b, c, channelId_bc, CltvExpiryDelta(42), htlcMinimumMsat = update_bc.htlcMinimumMsat, feeBaseMsat = update_bc.feeBaseMsat, feeProportionalMillionths = update_bc.feeProportionalMillionths, htlcMaximumMsat = update_bc.htlcMaximumMsat.get) + val channelUpdate_bc_modified = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_b, c, scid_bc, CltvExpiryDelta(42), htlcMinimumMsat = update_bc.htlcMinimumMsat, feeBaseMsat = update_bc.feeBaseMsat, feeProportionalMillionths = update_bc.feeProportionalMillionths, htlcMaximumMsat = update_bc.htlcMaximumMsat.get) val failure = IncorrectCltvExpiry(CltvExpiry(5), channelUpdate_bc_modified) // and node replies with a failure containing a new channel update sender.send(paymentFSM, addCompleted(HtlcResult.RemoteFail(UpdateFailHtlc(ByteVector32.Zeroes, 0, Sphinx.FailurePacket.create(sharedSecrets1.head._1, failure))))) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/BaseRouterSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/BaseRouterSpec.scala index a2ce74b18f..3c2c2cff05 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/BaseRouterSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/BaseRouterSpec.scala @@ -73,31 +73,31 @@ abstract class BaseRouterSpec extends TestKitBaseClass with FixtureAnyFunSuiteLi val node_g = makeNodeAnnouncement(priv_g, "node-G", Color(30, 10, -50), Nil, Features.empty) val node_h = makeNodeAnnouncement(priv_h, "node-H", Color(30, 10, -50), Nil, Features.empty) - val channelId_ab = ShortChannelId(BlockHeight(420000), 1, 0) - val channelId_bc = ShortChannelId(BlockHeight(420000), 2, 0) - val channelId_cd = ShortChannelId(BlockHeight(420000), 3, 0) - val channelId_ef = ShortChannelId(BlockHeight(420000), 4, 0) - val channelId_ag_private = ShortChannelId(BlockHeight(420000), 5, 0) - val channelId_gh = ShortChannelId(BlockHeight(420000), 6, 0) - - val chan_ab = channelAnnouncement(channelId_ab, priv_a, priv_b, priv_funding_a, priv_funding_b) - val chan_bc = channelAnnouncement(channelId_bc, priv_b, priv_c, priv_funding_b, priv_funding_c) - val chan_cd = channelAnnouncement(channelId_cd, priv_c, priv_d, priv_funding_c, priv_funding_d) - val chan_ef = channelAnnouncement(channelId_ef, priv_e, priv_f, priv_funding_e, priv_funding_f) - val chan_gh = channelAnnouncement(channelId_gh, priv_g, priv_h, priv_funding_g, priv_funding_h) - - val update_ab = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_a, b, channelId_ab, CltvExpiryDelta(7), htlcMinimumMsat = 0 msat, feeBaseMsat = 10 msat, feeProportionalMillionths = 10, htlcMaximumMsat = htlcMaximum) - val update_ba = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_b, a, channelId_ab, CltvExpiryDelta(7), htlcMinimumMsat = 0 msat, feeBaseMsat = 10 msat, feeProportionalMillionths = 10, htlcMaximumMsat = htlcMaximum) - val update_bc = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_b, c, channelId_bc, CltvExpiryDelta(5), htlcMinimumMsat = 0 msat, feeBaseMsat = 10 msat, feeProportionalMillionths = 1, htlcMaximumMsat = htlcMaximum) - val update_cb = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_c, b, channelId_bc, CltvExpiryDelta(5), htlcMinimumMsat = 0 msat, feeBaseMsat = 10 msat, feeProportionalMillionths = 1, htlcMaximumMsat = htlcMaximum) - val update_cd = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_c, d, channelId_cd, CltvExpiryDelta(3), htlcMinimumMsat = 0 msat, feeBaseMsat = 10 msat, feeProportionalMillionths = 4, htlcMaximumMsat = htlcMaximum) - val update_dc = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_d, c, channelId_cd, CltvExpiryDelta(3), htlcMinimumMsat = 0 msat, feeBaseMsat = 10 msat, feeProportionalMillionths = 4, htlcMaximumMsat = htlcMaximum) - val update_ef = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_e, f, channelId_ef, CltvExpiryDelta(9), htlcMinimumMsat = 0 msat, feeBaseMsat = 10 msat, feeProportionalMillionths = 8, htlcMaximumMsat = htlcMaximum) - val update_fe = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_f, e, channelId_ef, CltvExpiryDelta(9), htlcMinimumMsat = 0 msat, feeBaseMsat = 10 msat, feeProportionalMillionths = 8, htlcMaximumMsat = htlcMaximum) - val update_ag_private = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_a, g, channelId_ag_private, CltvExpiryDelta(7), htlcMinimumMsat = 0 msat, feeBaseMsat = 10 msat, feeProportionalMillionths = 10, htlcMaximumMsat = htlcMaximum) - val update_ga_private = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_g, a, channelId_ag_private, CltvExpiryDelta(7), htlcMinimumMsat = 0 msat, feeBaseMsat = 10 msat, feeProportionalMillionths = 10, htlcMaximumMsat = htlcMaximum) - val update_gh = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_g, h, channelId_gh, CltvExpiryDelta(7), htlcMinimumMsat = 0 msat, feeBaseMsat = 10 msat, feeProportionalMillionths = 10, htlcMaximumMsat = htlcMaximum) - val update_hg = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_h, g, channelId_gh, CltvExpiryDelta(7), htlcMinimumMsat = 0 msat, feeBaseMsat = 10 msat, feeProportionalMillionths = 10, htlcMaximumMsat = htlcMaximum) + val scid_ab = ShortChannelId(BlockHeight(420000), 1, 0) + val scid_bc = ShortChannelId(BlockHeight(420000), 2, 0) + val scid_cd = ShortChannelId(BlockHeight(420000), 3, 0) + val scid_ef = ShortChannelId(BlockHeight(420000), 4, 0) + val scid_ag_private = ShortChannelId(BlockHeight(420000), 5, 0) + val scid_gh = ShortChannelId(BlockHeight(420000), 6, 0) + + val chan_ab = channelAnnouncement(scid_ab, priv_a, priv_b, priv_funding_a, priv_funding_b) + val chan_bc = channelAnnouncement(scid_bc, priv_b, priv_c, priv_funding_b, priv_funding_c) + val chan_cd = channelAnnouncement(scid_cd, priv_c, priv_d, priv_funding_c, priv_funding_d) + val chan_ef = channelAnnouncement(scid_ef, priv_e, priv_f, priv_funding_e, priv_funding_f) + val chan_gh = channelAnnouncement(scid_gh, priv_g, priv_h, priv_funding_g, priv_funding_h) + + val update_ab = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_a, b, scid_ab, CltvExpiryDelta(7), htlcMinimumMsat = 0 msat, feeBaseMsat = 10 msat, feeProportionalMillionths = 10, htlcMaximumMsat = htlcMaximum) + val update_ba = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_b, a, scid_ab, CltvExpiryDelta(7), htlcMinimumMsat = 0 msat, feeBaseMsat = 10 msat, feeProportionalMillionths = 10, htlcMaximumMsat = htlcMaximum) + val update_bc = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_b, c, scid_bc, CltvExpiryDelta(5), htlcMinimumMsat = 0 msat, feeBaseMsat = 10 msat, feeProportionalMillionths = 1, htlcMaximumMsat = htlcMaximum) + val update_cb = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_c, b, scid_bc, CltvExpiryDelta(5), htlcMinimumMsat = 0 msat, feeBaseMsat = 10 msat, feeProportionalMillionths = 1, htlcMaximumMsat = htlcMaximum) + val update_cd = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_c, d, scid_cd, CltvExpiryDelta(3), htlcMinimumMsat = 0 msat, feeBaseMsat = 10 msat, feeProportionalMillionths = 4, htlcMaximumMsat = htlcMaximum) + val update_dc = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_d, c, scid_cd, CltvExpiryDelta(3), htlcMinimumMsat = 0 msat, feeBaseMsat = 10 msat, feeProportionalMillionths = 4, htlcMaximumMsat = htlcMaximum) + val update_ef = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_e, f, scid_ef, CltvExpiryDelta(9), htlcMinimumMsat = 0 msat, feeBaseMsat = 10 msat, feeProportionalMillionths = 8, htlcMaximumMsat = htlcMaximum) + val update_fe = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_f, e, scid_ef, CltvExpiryDelta(9), htlcMinimumMsat = 0 msat, feeBaseMsat = 10 msat, feeProportionalMillionths = 8, htlcMaximumMsat = htlcMaximum) + val update_ag_private = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_a, g, scid_ag_private, CltvExpiryDelta(7), htlcMinimumMsat = 0 msat, feeBaseMsat = 10 msat, feeProportionalMillionths = 10, htlcMaximumMsat = htlcMaximum) + val update_ga_private = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_g, a, scid_ag_private, CltvExpiryDelta(7), htlcMinimumMsat = 0 msat, feeBaseMsat = 10 msat, feeProportionalMillionths = 10, htlcMaximumMsat = htlcMaximum) + val update_gh = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_g, h, scid_gh, CltvExpiryDelta(7), htlcMinimumMsat = 0 msat, feeBaseMsat = 10 msat, feeProportionalMillionths = 10, htlcMaximumMsat = htlcMaximum) + val update_hg = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_h, g, scid_gh, CltvExpiryDelta(7), htlcMinimumMsat = 0 msat, feeBaseMsat = 10 msat, feeProportionalMillionths = 10, htlcMaximumMsat = htlcMaximum) override def withFixture(test: OneArgTest): Outcome = { // the network will be a --(1)--> b ---(2)--> c --(3)--> d @@ -110,8 +110,8 @@ abstract class BaseRouterSpec extends TestKitBaseClass with FixtureAnyFunSuiteLi assert(ChannelDesc(update_bc, chan_bc) === ChannelDesc(chan_bc.shortChannelId, b, c)) assert(ChannelDesc(update_cd, chan_cd) === ChannelDesc(chan_cd.shortChannelId, c, d)) assert(ChannelDesc(update_ef, chan_ef) === ChannelDesc(chan_ef.shortChannelId, e, f)) - assert(ChannelDesc(update_ag_private, PrivateChannel(a, g, None, None, ChannelMeta(1000 msat, 2000 msat))) === ChannelDesc(channelId_ag_private, a, g)) - assert(ChannelDesc(update_ag_private, PrivateChannel(g, a, None, None, ChannelMeta(2000 msat, 1000 msat))) === ChannelDesc(channelId_ag_private, a, g)) + assert(ChannelDesc(update_ag_private, PrivateChannel(a, g, None, None, ChannelMeta(1000 msat, 2000 msat))) === ChannelDesc(scid_ag_private, a, g)) + assert(ChannelDesc(update_ag_private, PrivateChannel(g, a, None, None, ChannelMeta(2000 msat, 1000 msat))) === ChannelDesc(scid_ag_private, a, g)) assert(ChannelDesc(update_gh, chan_gh) === ChannelDesc(chan_gh.shortChannelId, g, h)) // let's set up the router @@ -150,7 +150,7 @@ abstract class BaseRouterSpec extends TestKitBaseClass with FixtureAnyFunSuiteLi peerConnection.send(router, PeerRoutingMessage(peerConnection.ref, remoteNodeId, update_gh)) peerConnection.send(router, PeerRoutingMessage(peerConnection.ref, remoteNodeId, update_hg)) // then private channels - sender.send(router, LocalChannelUpdate(sender.ref, randomBytes32(), channelId_ag_private, g, None, update_ag_private, CommitmentsSpec.makeCommitments(30000000 msat, 8000000 msat, a, g, announceChannel = false))) + sender.send(router, LocalChannelUpdate(sender.ref, randomBytes32(), scid_ag_private, g, None, update_ag_private, CommitmentsSpec.makeCommitments(30000000 msat, 8000000 msat, a, g, announceChannel = false))) // watcher receives the get tx requests assert(watcher.expectMsgType[ValidateRequest].ann === chan_ab) assert(watcher.expectMsgType[ValidateRequest].ann === chan_bc) @@ -171,7 +171,7 @@ abstract class BaseRouterSpec extends TestKitBaseClass with FixtureAnyFunSuiteLi watcher.expectMsgType[WatchExternalChannelSpent].shortChannelId, watcher.expectMsgType[WatchExternalChannelSpent].shortChannelId, ) - assert(watchedShortChannelIds === Set(channelId_ab, channelId_bc, channelId_cd, channelId_ef, channelId_gh)) + assert(watchedShortChannelIds === Set(scid_ab, scid_bc, scid_cd, scid_ef, scid_gh)) // all messages are acked peerConnection.expectMsgAllOf( GossipDecision.Accepted(chan_ab), diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/RouterSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/RouterSpec.scala index b67eb961a3..6afaebf073 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/RouterSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/RouterSpec.scala @@ -262,20 +262,20 @@ class RouterSpec extends BaseRouterSpec { val eventListener = TestProbe() system.eventStream.subscribe(eventListener.ref, classOf[NetworkEvent]) - router ! WatchExternalChannelSpentTriggered(channelId_ab) - eventListener.expectMsg(ChannelLost(channelId_ab)) + router ! WatchExternalChannelSpentTriggered(scid_ab) + eventListener.expectMsg(ChannelLost(scid_ab)) // a doesn't have any channels, b still has one with c eventListener.expectMsg(NodeLost(a)) eventListener.expectNoMessage(200 milliseconds) - router ! WatchExternalChannelSpentTriggered(channelId_cd) - eventListener.expectMsg(ChannelLost(channelId_cd)) + router ! WatchExternalChannelSpentTriggered(scid_cd) + eventListener.expectMsg(ChannelLost(scid_cd)) // d doesn't have any channels, c still has one with b eventListener.expectMsg(NodeLost(d)) eventListener.expectNoMessage(200 milliseconds) - router ! WatchExternalChannelSpentTriggered(channelId_bc) - eventListener.expectMsg(ChannelLost(channelId_bc)) + router ! WatchExternalChannelSpentTriggered(scid_bc) + eventListener.expectMsg(ChannelLost(scid_bc)) // now b and c do not have any channels eventListener.expectMsgAllOf(NodeLost(b), NodeLost(c)) eventListener.expectNoMessage(200 milliseconds) @@ -373,7 +373,7 @@ class RouterSpec extends BaseRouterSpec { assert(res.routes.head.hops.map(_.nodeId).toList === a :: b :: c :: Nil) assert(res.routes.head.hops.last.nextNodeId === d) - val channelUpdate_cd1 = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_c, d, channelId_cd, CltvExpiryDelta(3), 0 msat, 153000 msat, 4, htlcMaximum, enable = false) + val channelUpdate_cd1 = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_c, d, scid_cd, CltvExpiryDelta(3), 0 msat, 153000 msat, 4, htlcMaximum, enable = false) peerConnection.send(router, PeerRoutingMessage(peerConnection.ref, remoteNodeId, channelUpdate_cd1)) peerConnection.expectMsg(TransportHandler.ReadAck(channelUpdate_cd1)) sender.send(router, RouteRequest(a, d, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, routeParams = DEFAULT_ROUTE_PARAMS)) @@ -388,8 +388,8 @@ class RouterSpec extends BaseRouterSpec { assert(res.routes.head.hops.map(_.nodeId).toList === a :: g :: Nil) assert(res.routes.head.hops.last.nextNodeId === h) - val channelUpdate_ag1 = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_a, g, channelId_ag_private, CltvExpiryDelta(7), 0 msat, 10 msat, 10, htlcMaximum, enable = false) - sender.send(router, LocalChannelUpdate(sender.ref, null, channelId_ag_private, g, None, channelUpdate_ag1, CommitmentsSpec.makeCommitments(10000 msat, 15000 msat, a, g, announceChannel = false))) + val channelUpdate_ag1 = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_a, g, scid_ag_private, CltvExpiryDelta(7), 0 msat, 10 msat, 10, htlcMaximum, enable = false) + sender.send(router, LocalChannelUpdate(sender.ref, null, scid_ag_private, g, None, channelUpdate_ag1, CommitmentsSpec.makeCommitments(10000 msat, 15000 msat, a, g, announceChannel = false))) sender.send(router, RouteRequest(a, h, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, routeParams = DEFAULT_ROUTE_PARAMS)) sender.expectMsg(Failure(RouteNotFound)) } @@ -410,7 +410,7 @@ class RouterSpec extends BaseRouterSpec { sender.send(router, RouteRequest(a, b, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, routeParams = DEFAULT_ROUTE_PARAMS)) sender.expectMsgType[RouteResponse] val commitments1 = CommitmentsSpec.makeCommitments(10000000 msat, 20000000 msat, a, b, announceChannel = true) - sender.send(router, LocalChannelUpdate(sender.ref, null, channelId_ab, b, Some(chan_ab), update_ab, commitments1)) + sender.send(router, LocalChannelUpdate(sender.ref, null, scid_ab, b, Some(chan_ab), update_ab, commitments1)) sender.send(router, RouteRequest(a, b, 12000000 msat, Long.MaxValue.msat, routeParams = DEFAULT_ROUTE_PARAMS)) sender.expectMsg(Failure(BalanceTooLow)) sender.send(router, RouteRequest(a, b, 12000000 msat, Long.MaxValue.msat, allowMultiPart = true, routeParams = DEFAULT_ROUTE_PARAMS)) @@ -426,7 +426,7 @@ class RouterSpec extends BaseRouterSpec { val sender = TestProbe() sender.send(router, RouteRequest(a, d, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, routeParams = DEFAULT_ROUTE_PARAMS)) sender.expectMsgType[RouteResponse] - val bc = ChannelDesc(channelId_bc, b, c) + val bc = ChannelDesc(scid_bc, b, c) // let's exclude channel b->c sender.send(router, ExcludeChannel(bc)) sender.send(router, RouteRequest(a, d, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, routeParams = DEFAULT_ROUTE_PARAMS)) @@ -451,24 +451,6 @@ class RouterSpec extends BaseRouterSpec { state.channels.foreach(c => assert(c.capacity === publicChannelCapacity)) } - test("send local channels") { fixture => - import fixture._ - // We need a channel update from our private remote peer, otherwise we can't create invoice routing information. - val peerConnection = TestProbe() - peerConnection.send(router, PeerRoutingMessage(peerConnection.ref, g, update_ga_private)) - val sender = TestProbe() - sender.send(router, GetLocalChannels) - val localChannels = sender.expectMsgType[Seq[LocalChannel]] - assert(localChannels.size === 2) - assert(localChannels.map(_.remoteNodeId).toSet === Set(b, g)) - assert(localChannels.exists(_.isPrivate)) // a ---> g - assert(localChannels.exists(!_.isPrivate)) // a ---> b - assert(localChannels.flatMap(_.toExtraHop).toSet === Set( - ExtraHop(b, channelId_ab, update_ba.feeBaseMsat, update_ba.feeProportionalMillionths, update_ba.cltvExpiryDelta), - ExtraHop(g, channelId_ag_private, update_ga_private.feeBaseMsat, update_ga_private.feeProportionalMillionths, update_ga_private.cltvExpiryDelta) - )) - } - test("given a pre-defined nodes route add the proper channel updates") { fixture => import fixture._ @@ -487,7 +469,7 @@ class RouterSpec extends BaseRouterSpec { import fixture._ val sender = TestProbe() - val preComputedRoute = PredefinedChannelRoute(d, Seq(channelId_ab, channelId_bc, channelId_cd)) + val preComputedRoute = PredefinedChannelRoute(d, Seq(scid_ab, scid_bc, scid_cd)) sender.send(router, FinalizeRoute(10000 msat, preComputedRoute)) val response = sender.expectMsgType[RouteResponse] @@ -502,7 +484,7 @@ class RouterSpec extends BaseRouterSpec { val sender = TestProbe() { - val preComputedRoute = PredefinedChannelRoute(g, Seq(channelId_ag_private)) + val preComputedRoute = PredefinedChannelRoute(g, Seq(scid_ag_private)) sender.send(router, FinalizeRoute(10000 msat, preComputedRoute)) val response = sender.expectMsgType[RouteResponse] assert(response.routes.length === 1) @@ -512,7 +494,7 @@ class RouterSpec extends BaseRouterSpec { assert(route.hops.head.nextNodeId === g) } { - val preComputedRoute = PredefinedChannelRoute(h, Seq(channelId_ag_private, channelId_gh)) + val preComputedRoute = PredefinedChannelRoute(h, Seq(scid_ag_private, scid_gh)) sender.send(router, FinalizeRoute(10000 msat, preComputedRoute)) val response = sender.expectMsgType[RouteResponse] assert(response.routes.length === 1) @@ -530,7 +512,7 @@ class RouterSpec extends BaseRouterSpec { { val invoiceRoutingHint = ExtraHop(b, ShortChannelId(BlockHeight(420000), 516, 1105), 10 msat, 150, CltvExpiryDelta(96)) - val preComputedRoute = PredefinedChannelRoute(targetNodeId, Seq(channelId_ab, invoiceRoutingHint.shortChannelId)) + val preComputedRoute = PredefinedChannelRoute(targetNodeId, Seq(scid_ab, invoiceRoutingHint.shortChannelId)) val amount = 10_000.msat // the amount affects the way we estimate the channel capacity of the hinted channel assert(amount < RoutingHeuristics.CAPACITY_CHANNEL_LOW) @@ -545,7 +527,7 @@ class RouterSpec extends BaseRouterSpec { } { val invoiceRoutingHint = ExtraHop(h, ShortChannelId(BlockHeight(420000), 516, 1105), 10 msat, 150, CltvExpiryDelta(96)) - val preComputedRoute = PredefinedChannelRoute(targetNodeId, Seq(channelId_ag_private, channelId_gh, invoiceRoutingHint.shortChannelId)) + val preComputedRoute = PredefinedChannelRoute(targetNodeId, Seq(scid_ag_private, scid_gh, invoiceRoutingHint.shortChannelId)) val amount = RoutingHeuristics.CAPACITY_CHANNEL_LOW * 2 // the amount affects the way we estimate the channel capacity of the hinted channel assert(amount > RoutingHeuristics.CAPACITY_CHANNEL_LOW) @@ -565,22 +547,22 @@ class RouterSpec extends BaseRouterSpec { val sender = TestProbe() { - val preComputedRoute = PredefinedChannelRoute(d, Seq(channelId_ab, channelId_cd)) + val preComputedRoute = PredefinedChannelRoute(d, Seq(scid_ab, scid_cd)) sender.send(router, FinalizeRoute(10000 msat, preComputedRoute)) sender.expectMsgType[Status.Failure] } { - val preComputedRoute = PredefinedChannelRoute(d, Seq(channelId_ab, channelId_bc)) + val preComputedRoute = PredefinedChannelRoute(d, Seq(scid_ab, scid_bc)) sender.send(router, FinalizeRoute(10000 msat, preComputedRoute)) sender.expectMsgType[Status.Failure] } { - val preComputedRoute = PredefinedChannelRoute(d, Seq(channelId_bc, channelId_cd)) + val preComputedRoute = PredefinedChannelRoute(d, Seq(scid_bc, scid_cd)) sender.send(router, FinalizeRoute(10000 msat, preComputedRoute)) sender.expectMsgType[Status.Failure] } { - val preComputedRoute = PredefinedChannelRoute(d, Seq(channelId_ab, ShortChannelId(1105), channelId_cd)) + val preComputedRoute = PredefinedChannelRoute(d, Seq(scid_ab, ShortChannelId(1105), scid_cd)) sender.send(router, FinalizeRoute(10000 msat, preComputedRoute)) sender.expectMsgType[Status.Failure] } @@ -629,15 +611,15 @@ class RouterSpec extends BaseRouterSpec { // When the local channel comes back online, it will send a LocalChannelUpdate to the router. val balances = Set[Option[MilliSatoshi]](Some(10000 msat), Some(15000 msat)) val commitments = CommitmentsSpec.makeCommitments(10000 msat, 15000 msat, a, b, announceChannel = true) - sender.send(router, LocalChannelUpdate(sender.ref, null, channelId_ab, b, Some(chan_ab), update_ab, commitments)) + sender.send(router, LocalChannelUpdate(sender.ref, null, scid_ab, b, Some(chan_ab), update_ab, commitments)) sender.send(router, GetRoutingState) val channel_ab = sender.expectMsgType[RoutingState].channels.find(_.ann == chan_ab).get assert(Set(channel_ab.meta_opt.map(_.balance1), channel_ab.meta_opt.map(_.balance2)) === balances) // And the graph should be updated too. sender.send(router, Router.GetRouterData) val g = sender.expectMsgType[Data].graph - val edge_ab = g.getEdge(ChannelDesc(channelId_ab, a, b)).get - val edge_ba = g.getEdge(ChannelDesc(channelId_ab, b, a)).get + val edge_ab = g.getEdge(ChannelDesc(scid_ab, a, b)).get + val edge_ba = g.getEdge(ChannelDesc(scid_ab, b, a)).get assert(edge_ab.capacity == channel_ab.capacity && edge_ba.capacity == channel_ab.capacity) assert(balances.contains(edge_ab.balance_opt)) assert(edge_ba.balance_opt === None) @@ -652,15 +634,15 @@ class RouterSpec extends BaseRouterSpec { // Then we update the balance without changing the contents of the channel update; the graph should still be updated. val balances = Set[Option[MilliSatoshi]](Some(11000 msat), Some(14000 msat)) val commitments = CommitmentsSpec.makeCommitments(11000 msat, 14000 msat, a, b, announceChannel = true) - sender.send(router, LocalChannelUpdate(sender.ref, null, channelId_ab, b, Some(chan_ab), update_ab, commitments)) + sender.send(router, LocalChannelUpdate(sender.ref, null, scid_ab, b, Some(chan_ab), update_ab, commitments)) sender.send(router, GetRoutingState) val channel_ab = sender.expectMsgType[RoutingState].channels.find(_.ann == chan_ab).get assert(Set(channel_ab.meta_opt.map(_.balance1), channel_ab.meta_opt.map(_.balance2)) === balances) // And the graph should be updated too. sender.send(router, Router.GetRouterData) val g = sender.expectMsgType[Data].graph - val edge_ab = g.getEdge(ChannelDesc(channelId_ab, a, b)).get - val edge_ba = g.getEdge(ChannelDesc(channelId_ab, b, a)).get + val edge_ab = g.getEdge(ChannelDesc(scid_ab, a, b)).get + val edge_ba = g.getEdge(ChannelDesc(scid_ab, b, a)).get assert(edge_ab.capacity == channel_ab.capacity && edge_ba.capacity == channel_ab.capacity) assert(balances.contains(edge_ab.balance_opt)) assert(edge_ba.balance_opt === None) @@ -670,15 +652,15 @@ class RouterSpec extends BaseRouterSpec { // When HTLCs are relayed through the channel, balance changes are sent to the router. val balances = Set[Option[MilliSatoshi]](Some(12000 msat), Some(13000 msat)) val commitments = CommitmentsSpec.makeCommitments(12000 msat, 13000 msat, a, b, announceChannel = true) - sender.send(router, AvailableBalanceChanged(sender.ref, null, channelId_ab, commitments)) + sender.send(router, AvailableBalanceChanged(sender.ref, null, scid_ab, commitments)) sender.send(router, GetRoutingState) val channel_ab = sender.expectMsgType[RoutingState].channels.find(_.ann == chan_ab).get assert(Set(channel_ab.meta_opt.map(_.balance1), channel_ab.meta_opt.map(_.balance2)) === balances) // And the graph should be updated too. sender.send(router, Router.GetRouterData) val g = sender.expectMsgType[Data].graph - val edge_ab = g.getEdge(ChannelDesc(channelId_ab, a, b)).get - val edge_ba = g.getEdge(ChannelDesc(channelId_ab, b, a)).get + val edge_ab = g.getEdge(ChannelDesc(scid_ab, a, b)).get + val edge_ba = g.getEdge(ChannelDesc(scid_ab, b, a)).get assert(edge_ab.capacity == channel_ab.capacity && edge_ba.capacity == channel_ab.capacity) assert(balances.contains(edge_ab.balance_opt)) assert(edge_ba.balance_opt === None) @@ -688,13 +670,13 @@ class RouterSpec extends BaseRouterSpec { // Private channels should also update the graph when HTLCs are relayed through them. val balances = Set(33000000 msat, 5000000 msat) val commitments = CommitmentsSpec.makeCommitments(33000000 msat, 5000000 msat, a, g, announceChannel = false) - sender.send(router, AvailableBalanceChanged(sender.ref, null, channelId_ag_private, commitments)) + sender.send(router, AvailableBalanceChanged(sender.ref, null, scid_ag_private, commitments)) sender.send(router, Router.GetRouterData) val data = sender.expectMsgType[Data] - val channel_ag = data.privateChannels(channelId_ag_private) + val channel_ag = data.privateChannels(scid_ag_private) assert(Set(channel_ag.meta.balance1, channel_ag.meta.balance2) === balances) // And the graph should be updated too. - val edge_ag = data.graph.getEdge(ChannelDesc(channelId_ag_private, a, g)).get + val edge_ag = data.graph.getEdge(ChannelDesc(scid_ag_private, a, g)).get assert(edge_ag.capacity == channel_ag.capacity) assert(edge_ag.balance_opt === Some(33000000 msat)) } @@ -729,7 +711,7 @@ class RouterSpec extends BaseRouterSpec { }, max = 30 seconds) // new announcements - val update_ab_2 = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_a, b, channelId_ab, CltvExpiryDelta(7), htlcMinimumMsat = 0 msat, feeBaseMsat = 10 msat, feeProportionalMillionths = 10, htlcMaximumMsat = htlcMaximum, timestamp = update_ab.timestamp + 1) + val update_ab_2 = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_a, b, scid_ab, CltvExpiryDelta(7), htlcMinimumMsat = 0 msat, feeBaseMsat = 10 msat, feeProportionalMillionths = 10, htlcMaximumMsat = htlcMaximum, timestamp = update_ab.timestamp + 1) val peerConnection = TestProbe() router ! PeerRoutingMessage(peerConnection.ref, remoteNodeId, update_ab_2) sender.fishForMessage() { From b691e876c589ab3fecd284e6599eb7673ec425c4 Mon Sep 17 00:00:00 2001 From: Bastien Teinturier <31281497+t-bast@users.noreply.github.com> Date: Fri, 13 May 2022 15:57:11 +0200 Subject: [PATCH 066/121] Fix flaky replaceable tx publisher tests (#2262) There was a race condition when testing fee-bumping: we could emit the new block event before the underlying actor registered to this event type. This is fixed by emitting the `TransactionPublished` event after registering to the new block events, and waiting for that in tests before publishing a new block event. --- .../channel/publish/MempoolTxMonitor.scala | 2 +- .../publish/ReplaceableTxPublisherSpec.scala | 76 +++++++++++++++++-- 2 files changed, 70 insertions(+), 8 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/MempoolTxMonitor.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/MempoolTxMonitor.scala index 5ba47b7799..270ee49db6 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/MempoolTxMonitor.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/MempoolTxMonitor.scala @@ -101,7 +101,6 @@ private class MempoolTxMonitor(nodeParams: NodeParams, Behaviors.receiveMessagePartial { case PublishOk => log.debug("txid={} was successfully published, waiting for confirmation...", cmd.tx.txid) - context.system.eventStream ! EventStream.Publish(TransactionPublished(txPublishContext.channelId_opt.getOrElse(ByteVector32.Zeroes), txPublishContext.remoteNodeId, cmd.tx, cmd.fee, cmd.desc)) waitForConfirmation() case PublishFailed(reason) if reason.getMessage.contains("rejecting replacement") => log.info("could not publish tx: a conflicting mempool transaction is already in the mempool") @@ -134,6 +133,7 @@ private class MempoolTxMonitor(nodeParams: NodeParams, def waitForConfirmation(): Behavior[Command] = { val messageAdapter = context.messageAdapter[CurrentBlockHeight](cbc => WrappedCurrentBlockHeight(cbc.blockHeight)) context.system.eventStream ! EventStream.Subscribe(messageAdapter) + context.system.eventStream ! EventStream.Publish(TransactionPublished(txPublishContext.channelId_opt.getOrElse(ByteVector32.Zeroes), txPublishContext.remoteNodeId, cmd.tx, cmd.fee, cmd.desc)) Behaviors.receiveMessagePartial { case WrappedCurrentBlockHeight(currentBlockHeight) => timers.startSingleTimer(CheckTxConfirmationsKey, CheckTxConfirmations(currentBlockHeight), (1 + Random.nextLong(nodeParams.channelConf.maxTxPublishRetryDelay.toMillis)).millis) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPublisherSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPublisherSpec.scala index cf1a130cdd..42f45797e6 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPublisherSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPublisherSpec.scala @@ -450,21 +450,28 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w wallet.publishTransaction(commitTx.tx).pipeTo(probe.ref) probe.expectMsg(commitTx.tx.txid) + val listener = TestProbe() + system.eventStream.subscribe(listener.ref, classOf[TransactionPublished]) + val oldFeerate = FeeratePerKw(3000 sat) setFeerate(oldFeerate) publisher ! Publish(probe.ref, anchorTx) // wait for the commit tx and anchor tx to be published + val anchorTxId1 = listener.expectMsgType[TransactionPublished].tx.txid val mempoolTxs1 = getMempoolTxs(2) assert(mempoolTxs1.map(_.txid).contains(commitTx.tx.txid)) val mempoolAnchorTx1 = mempoolTxs1.filter(_.txid != commitTx.tx.txid).head + assert(mempoolAnchorTx1.txid === anchorTxId1) // A new block is found, and the feerate has increased for our block target, so we bump the fees. val newFeerate = FeeratePerKw(5000 sat) setFeerate(newFeerate, blockTarget = 12) system.eventStream.publish(CurrentBlockHeight(aliceBlockHeight() + 5)) - awaitCond(!isInMempool(mempoolAnchorTx1.txid), interval = 200 millis, max = 30 seconds) + val anchorTxId2 = listener.expectMsgType[TransactionPublished].tx.txid + assert(!isInMempool(mempoolAnchorTx1.txid)) val mempoolTxs2 = getMempoolTxs(2) val mempoolAnchorTx2 = mempoolTxs2.filter(_.txid != commitTx.tx.txid).head + assert(mempoolAnchorTx2.txid === anchorTxId2) assert(mempoolAnchorTx1.fees < mempoolAnchorTx2.fees) val targetFee = Transactions.weight2fee(newFeerate, mempoolTxs2.map(_.weight).sum.toInt) @@ -481,20 +488,27 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w wallet.publishTransaction(commitTx.tx).pipeTo(probe.ref) probe.expectMsg(commitTx.tx.txid) + val listener = TestProbe() + system.eventStream.subscribe(listener.ref, classOf[TransactionPublished]) + // The feerate is (much) higher for higher block targets val targetFeerate = FeeratePerKw(75_000 sat) setFeerate(FeeratePerKw(3000 sat)) setFeerate(targetFeerate, blockTarget = 6) publisher ! Publish(probe.ref, anchorTx) // wait for the commit tx and anchor tx to be published + val anchorTxId1 = listener.expectMsgType[TransactionPublished].tx.txid val mempoolTxs1 = getMempoolTxs(2) assert(mempoolTxs1.map(_.txid).contains(commitTx.tx.txid)) val anchorTx1 = getMempool().filter(_.txid != commitTx.tx.txid).head + assert(anchorTx1.txid === anchorTxId1) // A new block is found, and the feerate has increased for our block target, so we bump the fees. system.eventStream.publish(CurrentBlockHeight(aliceBlockHeight() + 15)) - awaitCond(!isInMempool(anchorTx1.txid), interval = 200 millis, max = 30 seconds) + val anchorTxId2 = listener.expectMsgType[TransactionPublished].tx.txid + assert(!isInMempool(anchorTx1.txid)) val anchorTx2 = getMempool().filter(_.txid != commitTx.tx.txid).head + assert(anchorTx2.txid === anchorTxId2) // We used different inputs to be able to bump to the desired feerate. assert(anchorTx1.txIn.map(_.outPoint).toSet != anchorTx2.txIn.map(_.outPoint).toSet) @@ -578,15 +592,20 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w wallet.publishTransaction(commitTx.tx).pipeTo(probe.ref) probe.expectMsg(commitTx.tx.txid) + val listener = TestProbe() + system.eventStream.subscribe(listener.ref, classOf[TransactionPublished]) + val feerateLow = FeeratePerKw(3000 sat) val feerateHigh = FeeratePerKw(5000 sat) setFeerate(feerateLow) setFeerate(feerateHigh, blockTarget = 6) // With the initial confirmation target, this will use the low feerate. publisher ! Publish(probe.ref, anchorTx) + val anchorTxId1 = listener.expectMsgType[TransactionPublished].tx.txid val mempoolTxs1 = getMempoolTxs(2) assert(mempoolTxs1.map(_.txid).contains(commitTx.tx.txid)) val mempoolAnchorTx1 = mempoolTxs1.filter(_.txid != commitTx.tx.txid).head + assert(mempoolAnchorTx1.txid === anchorTxId1) val targetFee1 = Transactions.weight2fee(feerateLow, mempoolTxs1.map(_.weight).sum.toInt) val actualFee1 = mempoolTxs1.map(_.fees).sum assert(targetFee1 * 0.9 <= actualFee1 && actualFee1 <= targetFee1 * 1.1, s"actualFee=$actualFee1 targetFee=$targetFee1") @@ -595,9 +614,11 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w // We should now use the high feerate, which corresponds to that new target. publisher ! UpdateConfirmationTarget(aliceBlockHeight() + 15) system.eventStream.publish(CurrentBlockHeight(aliceBlockHeight())) + val anchorTxId2 = listener.expectMsgType[TransactionPublished].tx.txid awaitCond(!isInMempool(mempoolAnchorTx1.txid), interval = 200 millis, max = 30 seconds) val mempoolTxs2 = getMempoolTxs(2) val mempoolAnchorTx2 = mempoolTxs2.filter(_.txid != commitTx.tx.txid).head + assert(mempoolAnchorTx2.txid === anchorTxId2) assert(mempoolAnchorTx1.fees < mempoolAnchorTx2.fees) val targetFee2 = Transactions.weight2fee(feerateHigh, mempoolTxs2.map(_.weight).sum.toInt) @@ -916,20 +937,27 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w setFeerate(initialFeerate) val (commitTx, htlcSuccess, _) = closeChannelWithHtlcs(f, aliceBlockHeight() + 30) + val listener = TestProbe() + system.eventStream.subscribe(listener.ref, classOf[TransactionPublished]) + val htlcSuccessPublisher = createPublisher() htlcSuccessPublisher ! Publish(probe.ref, htlcSuccess) val w = alice2blockchain.expectMsgType[WatchParentTxConfirmed] w.replyTo ! WatchParentTxConfirmedTriggered(aliceBlockHeight(), 0, commitTx) + val htlcSuccessTxId1 = listener.expectMsgType[TransactionPublished].tx.txid val htlcSuccessTx1 = getMempoolTxs(1).head val htlcSuccessInputs1 = getMempool().head.txIn.map(_.outPoint).toSet + assert(htlcSuccessTx1.txid === htlcSuccessTxId1) // New blocks are found, which makes us aim for a more aggressive block target, so we bump the fees. val targetFeerate = FeeratePerKw(25_000 sat) setFeerate(targetFeerate, blockTarget = 6) system.eventStream.publish(CurrentBlockHeight(aliceBlockHeight() + 15)) - awaitCond(!isInMempool(htlcSuccessTx1.txid), interval = 200 millis, max = 30 seconds) + val htlcSuccessTxId2 = listener.expectMsgType[TransactionPublished].tx.txid + assert(!isInMempool(htlcSuccessTx1.txid)) val htlcSuccessTx2 = getMempoolTxs(1).head val htlcSuccessInputs2 = getMempool().head.txIn.map(_.outPoint).toSet + assert(htlcSuccessTx2.txid === htlcSuccessTxId2) assert(htlcSuccessTx1.fees < htlcSuccessTx2.fees) assert(htlcSuccessInputs1 === htlcSuccessInputs2) val htlcSuccessTargetFee = Transactions.weight2fee(targetFeerate, htlcSuccessTx2.weight.toInt) @@ -945,20 +973,27 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w setFeerate(initialFeerate) val (commitTx, htlcSuccess, _) = closeChannelWithHtlcs(f, aliceBlockHeight() + 15) + val listener = TestProbe() + system.eventStream.subscribe(listener.ref, classOf[TransactionPublished]) + val htlcSuccessPublisher = createPublisher() htlcSuccessPublisher ! Publish(probe.ref, htlcSuccess) val w = alice2blockchain.expectMsgType[WatchParentTxConfirmed] w.replyTo ! WatchParentTxConfirmedTriggered(aliceBlockHeight(), 0, commitTx) + val htlcSuccessTxId1 = listener.expectMsgType[TransactionPublished].tx.txid val htlcSuccessTx1 = getMempoolTxs(1).head val htlcSuccessInputs1 = getMempool().head.txIn.map(_.outPoint).toSet + assert(htlcSuccessTx1.txid === htlcSuccessTxId1) // New blocks are found, which makes us aim for a more aggressive block target, so we bump the fees. val targetFeerate = FeeratePerKw(75_000 sat) setFeerate(targetFeerate, blockTarget = 2) system.eventStream.publish(CurrentBlockHeight(aliceBlockHeight() + 10)) + val htlcSuccessTxId2 = listener.expectMsgType[TransactionPublished].tx.txid awaitCond(!isInMempool(htlcSuccessTx1.txid), interval = 200 millis, max = 30 seconds) val htlcSuccessTx2 = getMempoolTxs(1).head val htlcSuccessInputs2 = getMempool().head.txIn.map(_.outPoint).toSet + assert(htlcSuccessTx2.txid === htlcSuccessTxId2) assert(htlcSuccessTx1.fees < htlcSuccessTx2.fees) assert(htlcSuccessInputs1 !== htlcSuccessInputs2) val htlcSuccessTargetFee = Transactions.weight2fee(targetFeerate, htlcSuccessTx2.weight.toInt) @@ -974,17 +1009,24 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w setFeerate(initialFeerate) val (commitTx, htlcSuccess, _) = closeChannelWithHtlcs(f, aliceBlockHeight() + 6) + val listener = TestProbe() + system.eventStream.subscribe(listener.ref, classOf[TransactionPublished]) + val htlcSuccessPublisher = createPublisher() htlcSuccessPublisher ! Publish(probe.ref, htlcSuccess) val w = alice2blockchain.expectMsgType[WatchParentTxConfirmed] w.replyTo ! WatchParentTxConfirmedTriggered(aliceBlockHeight(), 0, commitTx) + val htlcSuccessTxId = listener.expectMsgType[TransactionPublished].tx.txid var htlcSuccessTx = getMempoolTxs(1).head + assert(htlcSuccessTx.txid === htlcSuccessTxId) // We are only 6 blocks away from the confirmation target, so we bump the fees at each new block. (1 to 3).foreach(i => { system.eventStream.publish(CurrentBlockHeight(aliceBlockHeight() + i)) - awaitCond(!isInMempool(htlcSuccessTx.txid), interval = 200 millis, max = 30 seconds) + val bumpedHtlcSuccessTxId = listener.expectMsgType[TransactionPublished].tx.txid + assert(!isInMempool(htlcSuccessTx.txid)) val bumpedHtlcSuccessTx = getMempoolTxs(1).head + assert(bumpedHtlcSuccessTx.txid === bumpedHtlcSuccessTxId) assert(htlcSuccessTx.fees < bumpedHtlcSuccessTx.fees) htlcSuccessTx = bumpedHtlcSuccessTx }) @@ -1000,20 +1042,27 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w // The confirmation target for htlc-timeout corresponds to their CLTV: we should claim them asap once the htlc has timed out. val (commitTx, _, htlcTimeout) = closeChannelWithHtlcs(f, aliceBlockHeight() + 144) + val listener = TestProbe() + system.eventStream.subscribe(listener.ref, classOf[TransactionPublished]) + val htlcTimeoutPublisher = createPublisher() htlcTimeoutPublisher ! Publish(probe.ref, htlcTimeout) generateBlocks(144) system.eventStream.publish(CurrentBlockHeight(currentBlockHeight(probe))) val w = alice2blockchain.expectMsgType[WatchParentTxConfirmed] w.replyTo ! WatchParentTxConfirmedTriggered(currentBlockHeight(probe), 0, commitTx) + val htlcTimeoutTxId1 = listener.expectMsgType[TransactionPublished].tx.txid val htlcTimeoutTx1 = getMempoolTxs(1).head val htlcTimeoutInputs1 = getMempool().head.txIn.map(_.outPoint).toSet + assert(htlcTimeoutTx1.txid === htlcTimeoutTxId1) // A new block is found, and we've already reached the confirmation target, so we bump the fees. system.eventStream.publish(CurrentBlockHeight(aliceBlockHeight() + 145)) - awaitCond(!isInMempool(htlcTimeoutTx1.txid), interval = 200 millis, max = 30 seconds) + val htlcTimeoutTxId2 = listener.expectMsgType[TransactionPublished].tx.txid + assert(!isInMempool(htlcTimeoutTx1.txid)) val htlcTimeoutTx2 = getMempoolTxs(1).head val htlcTimeoutInputs2 = getMempool().head.txIn.map(_.outPoint).toSet + assert(htlcTimeoutTx2.txid === htlcTimeoutTxId2) assert(htlcTimeoutTx1.fees < htlcTimeoutTx2.fees) assert(htlcTimeoutInputs1 === htlcTimeoutInputs2) // Once the confirmation target is reach, we should raise the feerate by at least 20% at every block. @@ -1345,16 +1394,22 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w val (remoteCommitTx, claimHtlcSuccess, claimHtlcTimeout) = remoteCloseChannelWithHtlcs(f, aliceBlockHeight() + 144) + val listener = TestProbe() + system.eventStream.subscribe(listener.ref, classOf[TransactionPublished]) + // The Claim-HTLC-success tx will be immediately published. setFeerate(initialFeerate) val claimHtlcSuccessPublisher = createPublisher() claimHtlcSuccessPublisher ! Publish(probe.ref, claimHtlcSuccess) val claimHtlcSuccessTx1 = getMempoolTxs(1).head + assert(listener.expectMsgType[TransactionPublished].tx.txid === claimHtlcSuccessTx1.txid) setFeerate(targetFeerate) system.eventStream.publish(CurrentBlockHeight(aliceBlockHeight() + 5)) - awaitCond(!isInMempool(claimHtlcSuccessTx1.txid), interval = 200 millis, max = 30 seconds) + val claimHtlcSuccessTxId2 = listener.expectMsgType[TransactionPublished].tx.txid + assert(!isInMempool(claimHtlcSuccessTx1.txid)) val claimHtlcSuccessTx2 = getMempoolTxs(1).head + assert(claimHtlcSuccessTx2.txid === claimHtlcSuccessTxId2) assert(claimHtlcSuccessTx1.fees < claimHtlcSuccessTx2.fees) val targetHtlcSuccessFee = Transactions.weight2fee(targetFeerate, claimHtlcSuccessTx2.weight.toInt) assert(targetHtlcSuccessFee * 0.9 <= claimHtlcSuccessTx2.fees && claimHtlcSuccessTx2.fees <= targetHtlcSuccessFee * 1.1, s"actualFee=${claimHtlcSuccessTx2.fees} targetFee=$targetHtlcSuccessFee") @@ -1371,11 +1426,14 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w system.eventStream.publish(CurrentBlockHeight(aliceBlockHeight() + 144)) assert(probe.expectMsgType[TxConfirmed].tx.txid === finalHtlcSuccessTx.txid) // the claim-htlc-success is now confirmed val claimHtlcTimeoutTx1 = getMempoolTxs(1).head + assert(listener.expectMsgType[TransactionPublished].tx.txid === claimHtlcTimeoutTx1.txid) setFeerate(targetFeerate) system.eventStream.publish(CurrentBlockHeight(aliceBlockHeight() + 145)) - awaitCond(!isInMempool(claimHtlcTimeoutTx1.txid), interval = 200 millis, max = 30 seconds) + val claimHtlcTimeoutTxId2 = listener.expectMsgType[TransactionPublished].tx.txid + assert(!isInMempool(claimHtlcTimeoutTx1.txid)) val claimHtlcTimeoutTx2 = getMempoolTxs(1).head + assert(claimHtlcTimeoutTx2.txid === claimHtlcTimeoutTxId2) assert(claimHtlcTimeoutTx1.fees < claimHtlcTimeoutTx2.fees) val targetHtlcTimeoutFee = Transactions.weight2fee(targetFeerate, claimHtlcTimeoutTx2.weight.toInt) assert(targetHtlcTimeoutFee * 0.9 <= claimHtlcTimeoutTx2.fees && claimHtlcTimeoutTx2.fees <= targetHtlcTimeoutFee * 1.1, s"actualFee=${claimHtlcTimeoutTx2.fees} targetFee=$targetHtlcTimeoutFee") @@ -1392,12 +1450,16 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w val (remoteCommitTx, claimHtlcSuccess, _) = remoteCloseChannelWithHtlcs(f, aliceBlockHeight() + 300) + val listener = TestProbe() + system.eventStream.subscribe(listener.ref, classOf[TransactionPublished]) + setFeerate(FeeratePerKw(5_000 sat)) val claimHtlcSuccessPublisher = createPublisher() claimHtlcSuccessPublisher ! Publish(probe.ref, claimHtlcSuccess) val w1 = alice2blockchain.expectMsgType[WatchParentTxConfirmed] w1.replyTo ! WatchParentTxConfirmedTriggered(currentBlockHeight(probe), 0, remoteCommitTx) val claimHtlcSuccessTx = getMempoolTxs(1).head + assert(listener.expectMsgType[TransactionPublished].tx.txid === claimHtlcSuccessTx.txid) // New blocks are found and the feerate is higher, but the htlc would become dust, so we don't bump the fees. setFeerate(FeeratePerKw(50_000 sat)) From 10eb9e932f9c0de06cc8926230d8ad4e2d1d9e2c Mon Sep 17 00:00:00 2001 From: Bastien Teinturier <31281497+t-bast@users.noreply.github.com> Date: Mon, 16 May 2022 14:56:05 +0200 Subject: [PATCH 067/121] Improve wallet double-spend detection (#2258) Our mechanism to detect double-spending wasn't correctly taking into account unconfirmed inputs. This was only used in single-funder scenarios so it could only be an issue in rare edge cases, where it would not lead to any loss of funds as we keep commit tx data in our DB even for closed channels. --- .../bitcoind/rpc/BitcoinCoreClient.scala | 50 +++++++-- .../bitcoind/BitcoinCoreClientSpec.scala | 100 ++++++++++++++---- 2 files changed, 123 insertions(+), 27 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/rpc/BitcoinCoreClient.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/rpc/BitcoinCoreClient.scala index 018fd52a03..896b1005fe 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/rpc/BitcoinCoreClient.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/rpc/BitcoinCoreClient.scala @@ -17,8 +17,8 @@ package fr.acinq.eclair.blockchain.bitcoind.rpc import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey -import fr.acinq.bitcoin.{Bech32, Block} import fr.acinq.bitcoin.scalacompat._ +import fr.acinq.bitcoin.{Bech32, Block} import fr.acinq.eclair.ShortChannelId.coordinates import fr.acinq.eclair.blockchain.OnChainWallet import fr.acinq.eclair.blockchain.OnChainWallet.{MakeFundingTxResponse, OnChainBalance} @@ -95,11 +95,41 @@ class BitcoinCoreClient(val rpcClient: BitcoinJsonRPCClient) extends OnChainWall index = txs.indexOf(JString(txid.toHex)) } yield (BlockHeight(height.toInt), index) + /** + * Return true if this output can potentially be spent. + * + * Note that if this function returns false, that doesn't mean the output cannot be spent. The output could be unknown + * (not in the blockchain nor in the mempool) but could reappear later and be spendable at that point. If you want to + * ensure that an output is not spendable anymore, you should use [[isTransactionOutputSpent]]. + */ def isTransactionOutputSpendable(txid: ByteVector32, outputIndex: Int, includeMempool: Boolean)(implicit ec: ExecutionContext): Future[Boolean] = for { json <- rpcClient.invoke("gettxout", txid, outputIndex, includeMempool) } yield json != JNull + /** + * Return true if this output has already been spent by a confirmed transaction. + * Note that a reorg may invalidate the result of this function and make a spent output spendable again. + */ + def isTransactionOutputSpent(txid: ByteVector32, outputIndex: Int)(implicit ec: ExecutionContext): Future[Boolean] = { + getTxConfirmations(txid).flatMap { + case Some(confirmations) if confirmations > 0 => + // There is an important limitation when using isTransactionOutputSpendable: if it returns false, it can mean a + // few different things: + // - the input has been spent + // - the input is coming from an unconfirmed transaction (in the mempool) but can be unspent + // - the input is unknown (it may come from an unconfirmed transaction that we don't have in our mempool) + // + // The only way to make sure that our output has been spent is to verify that it is coming from a confirmed + // transaction and that it has been spent by another confirmed transaction. We want to ignore the mempool to + // only consider spending transactions that have been confirmed. + isTransactionOutputSpendable(txid, outputIndex, includeMempool = false).map(r => !r) + case _ => + // If the output itself isn't in the blockchain, it cannot be spent by a confirmed transaction. + Future.successful(false) + } + } + def doubleSpent(tx: Transaction)(implicit ec: ExecutionContext): Future[Boolean] = for { exists <- getTransaction(tx.txid) @@ -111,16 +141,20 @@ class BitcoinCoreClient(val rpcClient: BitcoinJsonRPCClient) extends OnChainWall false // won't be reached case _ => false } - doublespent <- if (exists) { - // if the tx is in the blockchain, it can't have been double-spent + doubleSpent <- if (exists) { + // if the tx is in the blockchain or in the mempool, it can't have been double-spent Future.successful(false) } else { - // if the tx wasn't in the blockchain and one of its inputs has been spent, it is double-spent - // NB: we don't look in the mempool, so it means that we will only consider that the tx has been double-spent if - // the overriding transaction has been confirmed - Future.sequence(tx.txIn.map(txIn => isTransactionOutputSpendable(txIn.outPoint.txid, txIn.outPoint.index.toInt, includeMempool = false))).map(_.exists(_ == false)) + // The only way to make sure that our transaction has been double-spent is to find an input that is coming from + // a confirmed transaction and that it has been spent by another confirmed transaction. + // + // Note that if our transaction only had unconfirmed inputs and the transactions creating those inputs have + // themselves been double-spent, we will never be able to consider our transaction double-spent. With the + // information we have, these unknown inputs could eventually reappear and the transaction could be broadcast + // again. + Future.sequence(tx.txIn.map(txIn => isTransactionOutputSpent(txIn.outPoint.txid, txIn.outPoint.index.toInt))).map(_.exists(_ == true)) } - } yield doublespent + } yield doubleSpent /** * Iterate over blocks to find the transaction that has spent a given output. diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreClientSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreClientSpec.scala index 1a664fb395..fa53885f6e 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreClientSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreClientSpec.scala @@ -19,10 +19,11 @@ package fr.acinq.eclair.blockchain.bitcoind import akka.actor.Status.Failure import akka.pattern.pipe import akka.testkit.TestProbe +import fr.acinq.bitcoin import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.bitcoin.scalacompat.{Block, BtcDouble, ByteVector32, MilliBtcDouble, OutPoint, Satoshi, SatoshiLong, Script, ScriptWitness, Transaction, TxIn, TxOut} -import fr.acinq.bitcoin import fr.acinq.eclair.blockchain.OnChainWallet.{MakeFundingTxResponse, OnChainBalance} +import fr.acinq.eclair.blockchain.WatcherSpec.{createSpendManyP2WPKH, createSpendP2WPKH} import fr.acinq.eclair.blockchain.bitcoind.BitcoindService.BitcoinReq import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinCoreClient._ import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinJsonRPCAuthMethod.UserPassword @@ -655,25 +656,18 @@ class BitcoinCoreClientSpec extends TestKitBaseClass with BitcoindService with A val bitcoinClient = new BitcoinCoreClient(bitcoinrpcclient) // first let's create a tx - val address = "n2YKngjUp139nkjKvZGnfLRN6HzzYxJsje" - bitcoinrpcclient.invoke("createrawtransaction", Array.empty, Map(address -> 6)).pipeTo(sender.ref) - val JString(noinputTx1) = sender.expectMsgType[JString] - bitcoinrpcclient.invoke("fundrawtransaction", noinputTx1).pipeTo(sender.ref) - val json = sender.expectMsgType[JValue] - val JString(unsignedtx1) = json \ "hex" - bitcoinrpcclient.invoke("signrawtransactionwithwallet", unsignedtx1).pipeTo(sender.ref) - val JString(signedTx1) = sender.expectMsgType[JValue] \ "hex" - val tx1 = Transaction.read(signedTx1) + val noInputTx1 = Transaction(2, Nil, Seq(TxOut(500_000 sat, Script.pay2wpkh(randomKey().publicKey))), 0) + bitcoinClient.fundTransaction(noInputTx1, FundTransactionOptions(FeeratePerKw(2500 sat))).pipeTo(sender.ref) + val unsignedTx1 = sender.expectMsgType[FundTransactionResponse].tx + bitcoinClient.signTransaction(unsignedTx1).pipeTo(sender.ref) + val tx1 = sender.expectMsgType[SignTransactionResponse].tx // let's then generate another tx that double spends the first one - val inputs = tx1.txIn.map(txIn => Map("txid" -> txIn.outPoint.txid.toString, "vout" -> txIn.outPoint.index)).toArray - bitcoinrpcclient.invoke("createrawtransaction", inputs, Map(address -> tx1.txOut.map(_.amount).sum.toLong * 1.0 / 1e8)).pipeTo(sender.ref) - val JString(unsignedtx2) = sender.expectMsgType[JValue] - bitcoinrpcclient.invoke("signrawtransactionwithwallet", unsignedtx2).pipeTo(sender.ref) - val JString(signedTx2) = sender.expectMsgType[JValue] \ "hex" - val tx2 = Transaction.read(signedTx2) - - // tx1/tx2 haven't been published, so tx1 isn't double spent + val unsignedTx2 = tx1.copy(txOut = Seq(TxOut(tx1.txOut.map(_.amount).sum, Script.pay2wpkh(randomKey().publicKey)))) + bitcoinClient.signTransaction(unsignedTx2).pipeTo(sender.ref) + val tx2 = sender.expectMsgType[SignTransactionResponse].tx + + // tx1/tx2 haven't been published, so tx1 isn't double-spent bitcoinClient.doubleSpent(tx1).pipeTo(sender.ref) sender.expectMsg(false) // let's publish tx2 @@ -682,11 +676,79 @@ class BitcoinCoreClientSpec extends TestKitBaseClass with BitcoindService with A // tx2 hasn't been confirmed so tx1 is still not considered double-spent bitcoinClient.doubleSpent(tx1).pipeTo(sender.ref) sender.expectMsg(false) + // tx2 isn't considered double-spent either + bitcoinClient.doubleSpent(tx2).pipeTo(sender.ref) + sender.expectMsg(false) // let's confirm tx2 generateBlocks(1) - // this time tx1 has been double spent + // this time tx1 has been double-spent bitcoinClient.doubleSpent(tx1).pipeTo(sender.ref) sender.expectMsg(true) + // and tx2 isn't considered double-spent since it's confirmed + bitcoinClient.doubleSpent(tx2).pipeTo(sender.ref) + sender.expectMsg(false) + } + + test("detect if tx has been double-spent (with unconfirmed inputs)") { + val sender = TestProbe() + val bitcoinClient = new BitcoinCoreClient(bitcoinrpcclient) + val priv = randomKey() + + // Let's create one confirmed and one unconfirmed utxo. + val (confirmedParentTx, unconfirmedParentTx) = { + val txs = Seq(400_000 sat, 500_000 sat).map(amount => { + val noInputTx = Transaction(2, Nil, Seq(TxOut(amount, Script.pay2wpkh(priv.publicKey))), 0) + bitcoinClient.fundTransaction(noInputTx, FundTransactionOptions(FeeratePerKw(2500 sat), lockUtxos = true)).pipeTo(sender.ref) + val unsignedTx = sender.expectMsgType[FundTransactionResponse].tx + bitcoinClient.signTransaction(unsignedTx).pipeTo(sender.ref) + sender.expectMsgType[SignTransactionResponse].tx + }) + bitcoinClient.publishTransaction(txs.head).pipeTo(sender.ref) + sender.expectMsg(txs.head.txid) + generateBlocks(1) + bitcoinClient.publishTransaction(txs.last).pipeTo(sender.ref) + sender.expectMsg(txs.last.txid) + (txs.head, txs.last) + } + + // Let's spend those unconfirmed utxos. + val childTx = createSpendManyP2WPKH(Seq(confirmedParentTx, unconfirmedParentTx), priv, priv.publicKey, 500 sat, 0, 0) + // The tx hasn't been published, so it isn't double-spent. + bitcoinClient.doubleSpent(childTx).pipeTo(sender.ref) + sender.expectMsg(false) + // We publish the tx and verify it isn't double-spent. + bitcoinClient.publishTransaction(childTx).pipeTo(sender.ref) + sender.expectMsg(childTx.txid) + bitcoinClient.doubleSpent(childTx).pipeTo(sender.ref) + sender.expectMsg(false) + + // We double-spend the unconfirmed parent, which evicts our child transaction. + { + val previousAmountOut = unconfirmedParentTx.txOut.map(_.amount).sum + val unsignedTx = unconfirmedParentTx.copy(txOut = Seq(TxOut(previousAmountOut - 50_000.sat, Script.pay2wpkh(randomKey().publicKey)))) + bitcoinClient.signTransaction(unsignedTx).pipeTo(sender.ref) + val signedTx = sender.expectMsgType[SignTransactionResponse].tx + bitcoinClient.publishTransaction(signedTx).pipeTo(sender.ref) + sender.expectMsg(signedTx.txid) + } + + // We can't know whether the child transaction is double-spent or not, as its unconfirmed input is now unknown: it's + // not in the blockchain nor in the mempool. This unknown input may reappear in the future and the tx could then be + // published again. + bitcoinClient.doubleSpent(childTx).pipeTo(sender.ref) + sender.expectMsg(false) + + // We double-spend the confirmed input. + val spendingTx = createSpendP2WPKH(confirmedParentTx, priv, priv.publicKey, 600 sat, 0, 0) + bitcoinClient.publishTransaction(spendingTx).pipeTo(sender.ref) + sender.expectMsg(spendingTx.txid) + // While the spending transaction is unconfirmed, we don't consider our transaction double-spent. + bitcoinClient.doubleSpent(childTx).pipeTo(sender.ref) + sender.expectMsg(false) + // Once the spending transaction confirms, we know that our transaction is double-spent. + generateBlocks(1) + bitcoinClient.doubleSpent(childTx).pipeTo(sender.ref) + sender.expectMsg(true) } test("find spending transaction of a given output") { From 8e2fc7acd35ef9c9c1e6318913f37ae225828c5a Mon Sep 17 00:00:00 2001 From: Pierre-Marie Padiou Date: Tue, 17 May 2022 10:03:49 +0200 Subject: [PATCH 068/121] Run channel state tests sequentially (#2271) From scalatest's `ParallelTestExecution` doc: > ScalaTest's normal approach for running suites of tests in parallel is to run different suites in parallel, but the tests of any one suite sequentially. > [...] > To make it easier for users to write tests that run in parallel, this trait runs each test in its own instance of the class. Running each test in its own instance enables tests to use the same instance vars and mutable objects referenced from instance variables without needing to synchronize. Although ScalaTest provides functional approaches to factoring out common test code that can help avoid such issues, running each test in its own instance is an insurance policy that makes running tests in parallel easier and less error prone. This means that for each single test of the `channel.states` package, we instantiate one actor system, which contains two thread pools. With default settings, that's a minimum of 2*8 threads per individual test. That's already pretty bad, and with 65cf238 (#2270) we add a factor of 3 on top of that, which makes us go past the OS limits on github CI. setup | peak thread count | run time -------|---------------------|---- baseline | 5447 | 5m 44s sequential | 1927 | 5m 9s (*) (*) It's actually so bad, that tests run actually faster without parallelization! --- CONTRIBUTING.md | 8 ++++++++ .../channel/states/ChannelStateTestsHelperMethods.scala | 6 +++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 70687fd2c7..96aa2fb1d9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -147,6 +147,14 @@ If your contribution is adding a new dependency, please detail: Contributions that add new dependencies may take longer to approve because a detailed audit of the dependency may be required. +### Testing + +Your code should be tested. We use ScalaTest as a testing framework. + +ScalaTest's approach is to parallelize on test suites rather than individual tests, therefore it is +recommended to keep the execution time of each test suite under one minute and split tests across +multiple smaller suites if needed. + ### IntelliJ Tips If you're using [IntelliJ](https://www.jetbrains.com/idea/), here are some useful commands: diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala index d7364eba51..cbffd9cc3a 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala @@ -20,9 +20,9 @@ import akka.actor.typed.scaladsl.adapter.actorRefAdapter import akka.actor.{ActorContext, ActorRef} import akka.testkit.{TestFSMRef, TestKitBase, TestProbe} import com.softwaremill.quicklens.ModifyPimp +import fr.acinq.bitcoin.ScriptFlags import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.bitcoin.scalacompat.{ByteVector32, Crypto, SatoshiLong, Transaction} -import fr.acinq.bitcoin.ScriptFlags import fr.acinq.eclair.TestConstants.{Alice, Bob} import fr.acinq.eclair._ import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._ @@ -39,7 +39,7 @@ import fr.acinq.eclair.router.Router.ChannelHop import fr.acinq.eclair.transactions.Transactions import fr.acinq.eclair.transactions.Transactions._ import fr.acinq.eclair.wire.protocol._ -import org.scalatest.{FixtureTestSuite, ParallelTestExecution} +import org.scalatest.FixtureTestSuite import java.util.UUID import scala.concurrent.duration._ @@ -47,7 +47,7 @@ import scala.concurrent.duration._ /** * Created by PM on 23/08/2016. */ -trait ChannelStateTestsBase extends ChannelStateTestsHelperMethods with FixtureTestSuite with ParallelTestExecution { +trait ChannelStateTestsBase extends ChannelStateTestsHelperMethods with FixtureTestSuite { implicit class ChannelWithTestFeeConf(a: TestFSMRef[ChannelState, ChannelData, Channel]) { // @formatter:off From 81571b95e697fc748e565ad6ee6e27c9a03e753d Mon Sep 17 00:00:00 2001 From: Thomas HUET <81159533+thomash-acinq@users.noreply.github.com> Date: Wed, 18 May 2022 16:47:31 +0200 Subject: [PATCH 069/121] Fix flaky test PeerConnectionSpec (#2277) In tests we can sleep for at least X seconds but because of other tests running in parallel, we can't get an upper bound on the sleeping time. This makes some tests that depend on time unfeasible. Fixes #2175 #2276 --- .../fr/acinq/eclair/io/PeerConnectionSpec.scala | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerConnectionSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerConnectionSpec.scala index a6ee7d08c9..117d9032c1 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerConnectionSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerConnectionSpec.scala @@ -403,22 +403,16 @@ class PeerConnectionSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wi probe.expectMsg(()) } - test("keep using transient connection") { f => + test("keep transient connection open for a short while") { f => import f._ connect(nodeParams, remoteNodeId, switchboard, router, connection, transport, peerConnection, peer, isPersistent = false) val probe = TestProbe() val (_, message) = buildMessage(randomKey(), randomKey(), Nil, Recipient(remoteNodeId, None), Nil) - probe.send(peerConnection, message) probe watch peerConnection - sleep(900 millis) - assert(peerConnection.stateName === PeerConnection.CONNECTED) - probe.send(peerConnection, message) - sleep(900 millis) - assert(peerConnection.stateName === PeerConnection.CONNECTED) probe.send(peerConnection, message) - sleep(900 millis) + // The connection is still open for a short while. assert(peerConnection.stateName === PeerConnection.CONNECTED) - sleep(200 millis) + sleep(1 second) probe.expectTerminated(peerConnection, max = Duration.Zero) } From bb7703aa5d513e4697e0fd610d289365335baa23 Mon Sep 17 00:00:00 2001 From: Pierre-Marie Padiou Date: Thu, 19 May 2022 09:39:58 +0200 Subject: [PATCH 070/121] Add simple integration test between `Channel` and `Router` (#2270) In this PR we add a basic integration test between `Channel` and `Router` that checks the proper handling of local announcements. * disable router rebroadcast in tests * use separate ActorSystem for alice and bob in tests * add a simple channel-router integration test * fix bug found in new channel-router test The new test was failing, due to a bug. When a local channel graduates from private to public, we do not copy existing known `channel_update`s. Current implementation guarantees that we will process our local `channel_update` immediately, but what about our peer? * fix rebroadcast for local announcements We fix a second bug where gossip origin wasn't properly set for local announcements * increase ram for tests to 2G * improve debuggability of integration tests --- .../scala/fr/acinq/eclair/router/Router.scala | 2 +- .../fr/acinq/eclair/router/Validation.scala | 50 ++++--- .../scala/fr/acinq/eclair/TestConstants.scala | 4 +- .../ChannelStateTestsHelperMethods.scala | 29 +++- .../b/WaitForFundingSignedStateSpec.scala | 2 +- .../c/WaitForFundingConfirmedStateSpec.scala | 10 +- .../channel/states/e/NormalStateSpec.scala | 12 +- .../channel/states/e/OfflineStateSpec.scala | 2 +- .../channel/states/h/ClosingStateSpec.scala | 22 +-- .../integration/PaymentIntegrationSpec.scala | 28 ++-- .../acinq/eclair/router/BaseRouterSpec.scala | 1 - .../router/ChannelRouterIntegrationSpec.scala | 125 ++++++++++++++++++ .../acinq/eclair/router/FrontRouterSpec.scala | 53 +++++--- pom.xml | 2 +- 14 files changed, 257 insertions(+), 85 deletions(-) create mode 100644 eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRouterIntegrationSpec.scala diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala index 2e2ecdcde1..9cca8e5dac 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala @@ -604,7 +604,7 @@ object Router { channels: SortedMap[ShortChannelId, PublicChannel], stash: Stash, rebroadcast: Rebroadcast, - awaiting: Map[ChannelAnnouncement, Seq[RemoteGossip]], // note: this is a seq because we want to preserve order: first actor is the one who we need to send a tcp-ack when validation is done + awaiting: Map[ChannelAnnouncement, Seq[GossipOrigin]], // note: this is a seq because we want to preserve order: first actor is the one who we need to send a tcp-ack when validation is done privateChannels: Map[ShortChannelId, PrivateChannel], excludedChannels: Set[ChannelDesc], // those channels are temporarily excluded from route calculation, because their node returned a TemporaryChannelFailure graph: DirectedGraph, diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/Validation.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/Validation.scala index ed0cce7918..2607a04651 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/Validation.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/Validation.scala @@ -19,6 +19,7 @@ package fr.acinq.eclair.router import akka.actor.typed.scaladsl.adapter.actorRefAdapter import akka.actor.{ActorContext, ActorRef, typed} import akka.event.{DiagnosticLoggingAdapter, LoggingAdapter} +import com.softwaremill.quicklens.ModifyPimp import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.bitcoin.scalacompat.Script.{pay2wsh, write} import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher @@ -81,17 +82,20 @@ object Validation { implicit val sender: ActorRef = ctx.self // necessary to preserve origin when sending messages to other actors import nodeParams.db.{network => db} import r.c - d0.awaiting.get(c) match { - case Some(origin +: _) => origin.peerConnection ! TransportHandler.ReadAck(c) // now we can acknowledge the message, we only need to do it for the first peer that sent us the announcement + // now we can acknowledge the message, we only need to do it for the first peer that sent us the announcement + // (the other ones have already been acknowledged as duplicates) + d0.awaiting.getOrElse(c, Seq.empty).headOption match { + case Some(origin: RemoteGossip) => origin.peerConnection ! TransportHandler.ReadAck(c) + case Some(LocalGossip) => () // there is nothing to ack if it was a local gossip case _ => () } - val remoteOrigins_opt = d0.awaiting.get(c) - Logs.withMdc(log)(Logs.mdc(remoteNodeId_opt = remoteOrigins_opt.flatMap(_.headOption).map(_.nodeId))) { // in the MDC we use the node id that sent us the announcement first + val remoteOrigins = d0.awaiting.getOrElse(c, Set.empty).collect { case rg: RemoteGossip => rg } + Logs.withMdc(log)(Logs.mdc(remoteNodeId_opt = remoteOrigins.headOption.map(_.nodeId))) { // in the MDC we use the node id that sent us the announcement first log.debug("got validation result for shortChannelId={} (awaiting={} stash.nodes={} stash.updates={})", c.shortChannelId, d0.awaiting.size, d0.stash.nodes.size, d0.stash.updates.size) val publicChannel_opt = r match { case ValidateResult(c, Left(t)) => log.warning("validation failure for shortChannelId={} reason={}", c.shortChannelId, t.getMessage) - remoteOrigins_opt.foreach(_.foreach(o => sendDecision(o.peerConnection, GossipDecision.ValidationFailure(c)))) + remoteOrigins.foreach(o => sendDecision(o.peerConnection, GossipDecision.ValidationFailure(c))) None case ValidateResult(c, Right((tx, UtxoStatus.Unspent))) => val TxCoordinates(_, _, outputIndex) = ShortChannelId.coordinates(c.shortChannelId) @@ -103,12 +107,12 @@ object Validation { } if (fundingOutputIsInvalid) { log.error(s"invalid script for shortChannelId={}: txid={} does not have script=$fundingOutputScript at outputIndex=$outputIndex ann={}", c.shortChannelId, tx.txid, c) - remoteOrigins_opt.foreach(_.foreach(o => sendDecision(o.peerConnection, GossipDecision.InvalidAnnouncement(c)))) + remoteOrigins.foreach(o => sendDecision(o.peerConnection, GossipDecision.InvalidAnnouncement(c))) None } else { watcher ! WatchExternalChannelSpent(ctx.self, tx.txid, outputIndex, c.shortChannelId) log.debug("added channel channelId={}", c.shortChannelId) - remoteOrigins_opt.foreach(_.foreach(o => sendDecision(o.peerConnection, GossipDecision.Accepted(c)))) + remoteOrigins.foreach(o => sendDecision(o.peerConnection, GossipDecision.Accepted(c))) val capacity = tx.txOut(outputIndex).amount ctx.system.eventStream.publish(ChannelsDiscovered(SingleChannelDiscovered(c, capacity, None, None) :: Nil)) db.addChannel(c, tx.txid, capacity) @@ -118,24 +122,29 @@ object Validation { val nodeAnn = Announcements.makeNodeAnnouncement(nodeParams.privateKey, nodeParams.alias, nodeParams.color, nodeParams.publicAddresses, nodeParams.features.nodeAnnouncementFeatures()) ctx.self ! nodeAnn } - // public channels that haven't yet been announced are considered as private channels - val channelMeta_opt = d0.privateChannels.get(c.shortChannelId).map(_.meta) - Some(PublicChannel(c, tx.txid, capacity, None, None, channelMeta_opt)) + // maybe this previously was a local unannounced channel + val privateChannel_opt = d0.privateChannels.get(c.shortChannelId) + Some(PublicChannel(c, + tx.txid, + capacity, + update_1_opt = privateChannel_opt.flatMap(_.update_1_opt), + update_2_opt = privateChannel_opt.flatMap(_.update_2_opt), + meta_opt = privateChannel_opt.map(_.meta))) } case ValidateResult(c, Right((tx, fundingTxStatus: UtxoStatus.Spent))) => if (fundingTxStatus.spendingTxConfirmed) { log.debug("ignoring shortChannelId={} tx={} (funding tx already spent and spending tx is confirmed)", c.shortChannelId, tx.txid) // the funding tx has been spent by a transaction that is now confirmed: peer shouldn't send us those - remoteOrigins_opt.foreach(_.foreach(o => sendDecision(o.peerConnection, GossipDecision.ChannelClosed(c)))) + remoteOrigins.foreach(o => sendDecision(o.peerConnection, GossipDecision.ChannelClosed(c))) } else { log.debug("ignoring shortChannelId={} tx={} (funding tx already spent but spending tx isn't confirmed)", c.shortChannelId, tx.txid) - remoteOrigins_opt.foreach(_.foreach(o => sendDecision(o.peerConnection, GossipDecision.ChannelClosing(c)))) + remoteOrigins.foreach(o => sendDecision(o.peerConnection, GossipDecision.ChannelClosing(c))) } // there may be a record if we have just restarted db.removeChannel(c.shortChannelId) None } - // we also reprocess node and channel_update announcements related to channels that were just analyzed + // we also reprocess node and channel_update announcements related to the channel that was just analyzed val reprocessUpdates = d0.stash.updates.view.filterKeys(u => u.shortChannelId == c.shortChannelId) val reprocessNodes = d0.stash.nodes.view.filterKeys(n => isRelatedTo(c, n.nodeId)) // and we remove the reprocessed messages from the stash @@ -145,12 +154,17 @@ object Validation { publicChannel_opt match { case Some(pc) => - // note: if the channel is graduating from private to public, the implementation (in the LocalChannelUpdate handler) guarantees that we will process a new channel_update - // right after the channel_announcement, channel_updates will be moved from private to public at that time + // those updates are only defined if this was a previously an unannounced local channel, we broadcast them if they use the real scid + val updates1 = (pc.update_1_opt.toSet ++ pc.update_2_opt.toSet) + .map(u => u -> (if (pc.getNodeIdSameSideAs(u) == nodeParams.nodeId) Set[GossipOrigin](LocalGossip) else Set.empty[GossipOrigin])) + .toMap val d1 = d0.copy( channels = d0.channels + (c.shortChannelId -> pc), - privateChannels = d0.privateChannels - c.shortChannelId, // we remove fake announcements that we may have made before - rebroadcast = d0.rebroadcast.copy(channels = d0.rebroadcast.channels + (c -> d0.awaiting.getOrElse(c, Nil).toSet)), // we also add the newly validated channels to the rebroadcast queue + privateChannels = d0.privateChannels - c.shortChannelId, // we remove the corresponding unannounced channel that we may have until now + rebroadcast = d0.rebroadcast.copy( + channels = d0.rebroadcast.channels + (c -> d0.awaiting.getOrElse(c, Nil).toSet), // we rebroadcast the channel to our peers + updates = d0.rebroadcast.updates ++ updates1 + ), // we also add the newly validated channels to the rebroadcast queue stash = stash1, awaiting = awaiting1) // we only reprocess updates and nodes if validation succeeded @@ -419,7 +433,7 @@ object Validation { case Some(c) => // channel wasn't announced but here is the announcement, we will process it *before* the channel_update watcher ! ValidateRequest(ctx.self, c) - val d1 = d.copy(awaiting = d.awaiting + (c -> Nil)) // no origin + val d1 = d.copy(awaiting = d.awaiting + (c -> Seq(LocalGossip))) // no origin // maybe the local channel was pruned (can happen if we were disconnected for more than 2 weeks) db.removeFromPruned(c.shortChannelId) handleChannelUpdate(d1, db, routerConf, Left(lcu)) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala b/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala index b77d9fa3af..ad38f3e92c 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala @@ -161,7 +161,7 @@ object TestConstants { routerConf = RouterConf( watchSpentWindow = 1 second, channelExcludeDuration = 60 seconds, - routerBroadcastInterval = 5 seconds, + routerBroadcastInterval = 1 day, // "disables" rebroadcast requestNodeAnnouncements = true, encodingType = EncodingType.COMPRESSED_ZLIB, channelRangeChunkSize = 20, @@ -299,7 +299,7 @@ object TestConstants { routerConf = RouterConf( watchSpentWindow = 1 second, channelExcludeDuration = 60 seconds, - routerBroadcastInterval = 5 seconds, + routerBroadcastInterval = 1 day, // "disables" rebroadcast requestNodeAnnouncements = true, encodingType = EncodingType.UNCOMPRESSED, channelRangeChunkSize = 20, diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala index cbffd9cc3a..663d639939 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala @@ -17,8 +17,8 @@ package fr.acinq.eclair.channel.states import akka.actor.typed.scaladsl.adapter.actorRefAdapter -import akka.actor.{ActorContext, ActorRef} -import akka.testkit.{TestFSMRef, TestKitBase, TestProbe} +import akka.actor.{ActorContext, ActorRef, ActorSystem} +import akka.testkit.{TestFSMRef, TestKit, TestKitBase, TestProbe} import com.softwaremill.quicklens.ModifyPimp import fr.acinq.bitcoin.ScriptFlags import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey @@ -106,6 +106,12 @@ trait ChannelStateTestsHelperMethods extends TestKitBase { def currentBlockHeight: BlockHeight = alice.underlyingActor.nodeParams.currentBlockHeight } + val systemA: ActorSystem = ActorSystem("system-alice") + val systemB: ActorSystem = ActorSystem("system-bob") + + system.registerOnTermination(TestKit.shutdownActorSystem(systemA)) + system.registerOnTermination(TestKit.shutdownActorSystem(systemB)) + def init(nodeParamsA: NodeParams = TestConstants.Alice.nodeParams, nodeParamsB: NodeParams = TestConstants.Bob.nodeParams, wallet: OnChainWallet = new DummyOnChainWallet(), tags: Set[String] = Set.empty): SetupFixture = { val aliceOrigin = TestProbe() val alice2bob = TestProbe() @@ -119,8 +125,10 @@ trait ChannelStateTestsHelperMethods extends TestKitBase { val alice2relayer = TestProbe() val bob2relayer = TestProbe() val channelUpdateListener = TestProbe() - system.eventStream.subscribe(channelUpdateListener.ref, classOf[LocalChannelUpdate]) - system.eventStream.subscribe(channelUpdateListener.ref, classOf[LocalChannelDown]) + systemA.eventStream.subscribe(channelUpdateListener.ref, classOf[LocalChannelUpdate]) + systemA.eventStream.subscribe(channelUpdateListener.ref, classOf[LocalChannelDown]) + systemB.eventStream.subscribe(channelUpdateListener.ref, classOf[LocalChannelUpdate]) + systemB.eventStream.subscribe(channelUpdateListener.ref, classOf[LocalChannelDown]) val router = TestProbe() val finalNodeParamsA = nodeParamsA .modify(_.channelConf.dustLimit).setToIf(tags.contains(ChannelStateTestsTags.HighDustLimitDifferenceAliceBob))(5000 sat) @@ -132,8 +140,14 @@ trait ChannelStateTestsHelperMethods extends TestKitBase { .modify(_.channelConf.dustLimit).setToIf(tags.contains(ChannelStateTestsTags.HighDustLimitDifferenceBobAlice))(5000 sat) .modify(_.channelConf.maxRemoteDustLimit).setToIf(tags.contains(ChannelStateTestsTags.HighDustLimitDifferenceAliceBob))(10000 sat) .modify(_.channelConf.maxRemoteDustLimit).setToIf(tags.contains(ChannelStateTestsTags.HighDustLimitDifferenceBobAlice))(10000 sat) - val alice: TestFSMRef[ChannelState, ChannelData, Channel] = TestFSMRef(new Channel(finalNodeParamsA, wallet, finalNodeParamsB.nodeId, alice2blockchain.ref, alice2relayer.ref, FakeTxPublisherFactory(alice2blockchain), origin_opt = Some(aliceOrigin.ref)), alicePeer.ref) - val bob: TestFSMRef[ChannelState, ChannelData, Channel] = TestFSMRef(new Channel(finalNodeParamsB, wallet, finalNodeParamsA.nodeId, bob2blockchain.ref, bob2relayer.ref, FakeTxPublisherFactory(bob2blockchain)), bobPeer.ref) + val alice: TestFSMRef[ChannelState, ChannelData, Channel] = { + implicit val system: ActorSystem = systemA + TestFSMRef(new Channel(finalNodeParamsA, wallet, finalNodeParamsB.nodeId, alice2blockchain.ref, alice2relayer.ref, FakeTxPublisherFactory(alice2blockchain), origin_opt = Some(aliceOrigin.ref)), alicePeer.ref) + } + val bob: TestFSMRef[ChannelState, ChannelData, Channel] = { + implicit val system: ActorSystem = systemB + TestFSMRef(new Channel(finalNodeParamsB, wallet, finalNodeParamsA.nodeId, bob2blockchain.ref, bob2relayer.ref, FakeTxPublisherFactory(bob2blockchain)), bobPeer.ref) + } SetupFixture(alice, bob, aliceOrigin, alice2bob, bob2alice, alice2blockchain, bob2blockchain, router, alice2relayer, bob2relayer, channelUpdateListener, wallet, alicePeer, bobPeer) } @@ -179,7 +193,7 @@ trait ChannelStateTestsHelperMethods extends TestKitBase { (aliceParams, bobParams, channelType) } - def reachNormal(setup: SetupFixture, tags: Set[String] = Set.empty): Unit = { + def reachNormal(setup: SetupFixture, tags: Set[String] = Set.empty): Transaction = { import setup._ @@ -231,6 +245,7 @@ trait ChannelStateTestsHelperMethods extends TestKitBase { // x2 because alice and bob share the same relayer channelUpdateListener.expectMsgType[LocalChannelUpdate] channelUpdateListener.expectMsgType[LocalChannelUpdate] + fundingTx } def localOrigin(replyTo: ActorRef): Origin.LocalHot = Origin.LocalHot(replyTo, UUID.randomUUID()) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingSignedStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingSignedStateSpec.scala index b03b8d00e3..2a8a475072 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingSignedStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingSignedStateSpec.scala @@ -82,7 +82,7 @@ class WaitForFundingSignedStateSpec extends TestKitBaseClass with FixtureAnyFunS test("recv FundingSigned with valid signature") { f => import f._ val listener = TestProbe() - system.eventStream.subscribe(listener.ref, classOf[TransactionPublished]) + alice.underlying.system.eventStream.subscribe(listener.ref, classOf[TransactionPublished]) bob2alice.expectMsgType[FundingSigned] bob2alice.forward(alice) awaitCond(alice.stateName == WAIT_FOR_FUNDING_CONFIRMED) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingConfirmedStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingConfirmedStateSpec.scala index 8b7a8c7480..3bb70833cb 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingConfirmedStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingConfirmedStateSpec.scala @@ -54,7 +54,7 @@ class WaitForFundingConfirmedStateSpec extends TestKitBaseClass with FixtureAnyF within(30 seconds) { val listener = TestProbe() - system.eventStream.subscribe(listener.ref, classOf[TransactionPublished]) + alice.underlying.system.eventStream.subscribe(listener.ref, classOf[TransactionPublished]) alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, ChannelFlags.Private, channelConfig, channelType) alice2blockchain.expectMsgType[TxPublisher.SetChannelId] bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType) @@ -83,8 +83,8 @@ class WaitForFundingConfirmedStateSpec extends TestKitBaseClass with FixtureAnyF import f._ // we create a new listener that registers after alice has published the funding tx val listener = TestProbe() - system.eventStream.subscribe(listener.ref, classOf[TransactionPublished]) - system.eventStream.subscribe(listener.ref, classOf[TransactionConfirmed]) + bob.underlying.system.eventStream.subscribe(listener.ref, classOf[TransactionPublished]) + bob.underlying.system.eventStream.subscribe(listener.ref, classOf[TransactionConfirmed]) // make bob send a FundingLocked msg val fundingTx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CONFIRMED].fundingTx.get bob ! WatchFundingConfirmedTriggered(BlockHeight(42000), 42, fundingTx) @@ -102,8 +102,8 @@ class WaitForFundingConfirmedStateSpec extends TestKitBaseClass with FixtureAnyF import f._ // we create a new listener that registers after alice has published the funding tx val listener = TestProbe() - system.eventStream.subscribe(listener.ref, classOf[TransactionPublished]) - system.eventStream.subscribe(listener.ref, classOf[TransactionConfirmed]) + alice.underlying.system.eventStream.subscribe(listener.ref, classOf[TransactionPublished]) + alice.underlying.system.eventStream.subscribe(listener.ref, classOf[TransactionConfirmed]) val fundingTx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CONFIRMED].fundingTx.get alice ! WatchFundingConfirmedTriggered(BlockHeight(42000), 42, fundingTx) assert(listener.expectMsgType[TransactionConfirmed].tx === fundingTx) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala index 59981ba15b..75d30245ac 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala @@ -74,7 +74,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] val sender = TestProbe() val listener = TestProbe() - system.eventStream.subscribe(listener.ref, classOf[AvailableBalanceChanged]) + alice.underlying.system.eventStream.subscribe(listener.ref, classOf[AvailableBalanceChanged]) val h = randomBytes32() val add = CMD_ADD_HTLC(sender.ref, 50000000 msat, h, CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, localOrigin(sender.ref)) alice ! add @@ -881,7 +881,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with bob2alice.expectMsgType[UpdateFulfillHtlc] // we listen to channel_update events val listener = TestProbe() - system.eventStream.subscribe(listener.ref, classOf[LocalChannelUpdate]) + bob.underlying.system.eventStream.subscribe(listener.ref, classOf[LocalChannelUpdate]) // actual test starts here // when signing the fulfill, bob will have its main output go above reserve in alice's commitment tx @@ -896,7 +896,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with test("recv CMD_SIGN (after CMD_UPDATE_FEE)") { f => import f._ val listener = TestProbe() - system.eventStream.subscribe(listener.ref, classOf[AvailableBalanceChanged]) + alice.underlying.system.eventStream.subscribe(listener.ref, classOf[AvailableBalanceChanged]) alice ! CMD_UPDATE_FEE(FeeratePerKw(654564 sat)) alice2bob.expectMsgType[UpdateFee] alice ! CMD_SIGN() @@ -2734,7 +2734,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with crossSign(alice, bob, alice2bob, bob2alice) val listener = TestProbe() - system.eventStream.subscribe(listener.ref, classOf[ChannelErrorOccurred]) + bob.underlying.system.eventStream.subscribe(listener.ref, classOf[ChannelErrorOccurred]) // actual test begins: // * Bob receives the HTLC pre-image and wants to fulfill @@ -2767,7 +2767,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with crossSign(alice, bob, alice2bob, bob2alice) val listener = TestProbe() - system.eventStream.subscribe(listener.ref, classOf[ChannelErrorOccurred]) + bob.underlying.system.eventStream.subscribe(listener.ref, classOf[ChannelErrorOccurred]) // actual test begins: // * Bob receives the HTLC pre-image and wants to fulfill but doesn't sign @@ -2800,7 +2800,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with crossSign(alice, bob, alice2bob, bob2alice) val listener = TestProbe() - system.eventStream.subscribe(listener.ref, classOf[ChannelErrorOccurred]) + bob.underlying.system.eventStream.subscribe(listener.ref, classOf[ChannelErrorOccurred]) // actual test begins: // * Bob receives the HTLC pre-image and wants to fulfill diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/OfflineStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/OfflineStateSpec.scala index c467c5d6ca..de429c16fe 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/OfflineStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/OfflineStateSpec.scala @@ -527,7 +527,7 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with crossSign(alice, bob, alice2bob, bob2alice) val listener = TestProbe() - system.eventStream.subscribe(listener.ref, classOf[ChannelErrorOccurred]) + bob.underlying.system.eventStream.subscribe(listener.ref, classOf[ChannelErrorOccurred]) val initialState = bob.stateData.asInstanceOf[DATA_NORMAL] val initialCommitTx = initialState.commitments.localCommit.commitTxAndRemoteSig.commitTx.tx diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala index 6fcaca1652..1d1440ebac 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala @@ -89,15 +89,19 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with bob2blockchain.expectMsgType[WatchFundingConfirmed] awaitCond(alice.stateName == WAIT_FOR_FUNDING_CONFIRMED) awaitCond(bob.stateName == WAIT_FOR_FUNDING_CONFIRMED) - system.eventStream.subscribe(eventListener.ref, classOf[TransactionPublished]) - system.eventStream.subscribe(eventListener.ref, classOf[TransactionConfirmed]) + alice.underlying.system.eventStream.subscribe(eventListener.ref, classOf[TransactionPublished]) + alice.underlying.system.eventStream.subscribe(eventListener.ref, classOf[TransactionConfirmed]) + bob.underlying.system.eventStream.subscribe(eventListener.ref, classOf[TransactionPublished]) + bob.underlying.system.eventStream.subscribe(eventListener.ref, classOf[TransactionConfirmed]) withFixture(test.toNoArgTest(FixtureParam(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain, alice2relayer, bob2relayer, channelUpdateListener, eventListener, Nil))) } } else { within(30 seconds) { reachNormal(setup, test.tags) - system.eventStream.subscribe(eventListener.ref, classOf[TransactionPublished]) - system.eventStream.subscribe(eventListener.ref, classOf[TransactionConfirmed]) + alice.underlying.system.eventStream.subscribe(eventListener.ref, classOf[TransactionPublished]) + alice.underlying.system.eventStream.subscribe(eventListener.ref, classOf[TransactionConfirmed]) + bob.underlying.system.eventStream.subscribe(eventListener.ref, classOf[TransactionPublished]) + bob.underlying.system.eventStream.subscribe(eventListener.ref, classOf[TransactionConfirmed]) val bobCommitTxs: List[CommitTxAndRemoteSig] = (for (amt <- List(100000000 msat, 200000000 msat, 300000000 msat)) yield { val (r, htlc) = addHtlc(amt, alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) @@ -368,8 +372,8 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.channelFeatures === channelFeatures) val listener = TestProbe() - system.eventStream.subscribe(listener.ref, classOf[LocalCommitConfirmed]) - system.eventStream.subscribe(listener.ref, classOf[PaymentSettlingOnChain]) + alice.underlying.system.eventStream.subscribe(listener.ref, classOf[LocalCommitConfirmed]) + alice.underlying.system.eventStream.subscribe(listener.ref, classOf[PaymentSettlingOnChain]) // alice sends an htlc to bob val (_, htlca1) = addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) @@ -473,7 +477,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with test("recv WatchTxConfirmedTriggered (local commit with htlcs only signed by local)") { f => import f._ val listener = TestProbe() - system.eventStream.subscribe(listener.ref, classOf[PaymentSettlingOnChain]) + alice.underlying.system.eventStream.subscribe(listener.ref, classOf[PaymentSettlingOnChain]) val aliceCommitTx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx // alice sends an htlc val (_, htlc) = addHtlc(4200000 msat, alice, bob, alice2bob, bob2alice) @@ -522,7 +526,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with test("recv WatchTxConfirmedTriggered (local commit with fail not acked by remote)") { f => import f._ val listener = TestProbe() - system.eventStream.subscribe(listener.ref, classOf[PaymentSettlingOnChain]) + alice.underlying.system.eventStream.subscribe(listener.ref, classOf[PaymentSettlingOnChain]) val (_, htlc) = addHtlc(25000000 msat, alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) failHtlc(htlc.id, bob, alice, bob2alice, alice2bob) @@ -603,7 +607,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with test("recv WatchTxConfirmedTriggered (remote commit with htlcs only signed by local in next remote commit)") { f => import f._ val listener = TestProbe() - system.eventStream.subscribe(listener.ref, classOf[PaymentSettlingOnChain]) + alice.underlying.system.eventStream.subscribe(listener.ref, classOf[PaymentSettlingOnChain]) val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx // alice sends an htlc val (_, htlc) = addHtlc(4200000 msat, alice, bob, alice2bob, bob2alice) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/PaymentIntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/PaymentIntegrationSpec.scala index 1e85d07c88..ff3b80b4b0 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/PaymentIntegrationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/PaymentIntegrationSpec.scala @@ -117,19 +117,21 @@ class PaymentIntegrationSpec extends IntegrationSpec { def awaitAnnouncements(subset: Map[String, Kit], nodes: Int, channels: Int, updates: Int): Unit = { val sender = TestProbe() subset.foreach { - case (_, setup) => - awaitCond({ - sender.send(setup.router, Router.GetNodes) - sender.expectMsgType[Iterable[NodeAnnouncement]].size == nodes - }, max = 60 seconds, interval = 1 second) - awaitCond({ - sender.send(setup.router, Router.GetChannels) - sender.expectMsgType[Iterable[ChannelAnnouncement]].size == channels - }, max = 60 seconds, interval = 1 second) - awaitCond({ - sender.send(setup.router, Router.GetChannelUpdates) - sender.expectMsgType[Iterable[ChannelUpdate]].size == updates - }, max = 60 seconds, interval = 1 second) + case (node, setup) => + withClue(node) { + awaitAssert({ + sender.send(setup.router, Router.GetNodes) + assert(sender.expectMsgType[Iterable[NodeAnnouncement]].size == nodes) + }, max = 10 seconds, interval = 1 second) + awaitAssert({ + sender.send(setup.router, Router.GetChannels) + sender.expectMsgType[Iterable[ChannelAnnouncement]].size == channels + }, max = 10 seconds, interval = 1 second) + awaitAssert({ + sender.send(setup.router, Router.GetChannelUpdates) + sender.expectMsgType[Iterable[ChannelUpdate]].size == updates + }, max = 10 seconds, interval = 1 second) + } } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/BaseRouterSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/BaseRouterSpec.scala index 3c2c2cff05..12e4b3cf27 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/BaseRouterSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/BaseRouterSpec.scala @@ -122,7 +122,6 @@ abstract class BaseRouterSpec extends TestKitBaseClass with FixtureAnyFunSuiteLi import com.softwaremill.quicklens._ val nodeParams = Alice.nodeParams .modify(_.nodeKeyManager).setTo(testNodeKeyManager) - .modify(_.routerConf.routerBroadcastInterval).setTo(1 day) // "disable" auto rebroadcast val router = system.actorOf(Router.props(nodeParams, watcher.ref)) // we announce channels peerConnection.send(router, PeerRoutingMessage(peerConnection.ref, remoteNodeId, chan_ab)) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRouterIntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRouterIntegrationSpec.scala new file mode 100644 index 0000000000..ad98627e6f --- /dev/null +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRouterIntegrationSpec.scala @@ -0,0 +1,125 @@ +package fr.acinq.eclair.router + +import akka.actor.ActorSystem +import akka.testkit.{TestFSMRef, TestProbe} +import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher +import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.WatchFundingDeeplyBuriedTriggered +import fr.acinq.eclair.channel.DATA_NORMAL +import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsTags} +import fr.acinq.eclair.io.Peer.PeerRoutingMessage +import fr.acinq.eclair.router.Router.{GossipOrigin, LocalGossip} +import fr.acinq.eclair.wire.protocol.{AnnouncementSignatures, ChannelUpdate} +import fr.acinq.eclair.{BlockHeight, TestKitBaseClass} +import org.scalatest.funsuite.FixtureAnyFunSuiteLike +import org.scalatest.{Outcome, Tag} + +/** + * This test checks the integration between Channel and Router (events, etc.) + */ +class ChannelRouterIntegrationSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with ChannelStateTestsBase { + + case class FixtureParam(router: TestFSMRef[Router.State, Router.Data, Router], rebroadcastListener: TestProbe, channels: SetupFixture, testTags: Set[String]) + + implicit val log: akka.event.LoggingAdapter = akka.event.NoLogging + + override def withFixture(test: OneArgTest): Outcome = { + val channels = init(tags = test.tags) + val rebroadcastListener = TestProbe() + val router: TestFSMRef[Router.State, Router.Data, Router] = { + // we use alice's actor system so we share the same event stream + implicit val system: ActorSystem = channels.alice.underlying.system + system.eventStream.subscribe(rebroadcastListener.ref, classOf[Router.Rebroadcast]) + TestFSMRef(new Router(channels.alice.underlyingActor.nodeParams, channels.alice.underlyingActor.blockchain, initialized = None)) + } + withFixture(test.toNoArgTest(FixtureParam(router, rebroadcastListener, channels, test.tags))) + } + + test("private local channel") { f => + import f._ + + reachNormal(channels, testTags) + + awaitAssert(router.stateData.privateChannels.size === 1) + + { + // only the local channel_update is known (bob won't send his before the channel is deeply buried) + val pc = router.stateData.privateChannels.values.head + assert(pc.update_1_opt.isDefined ^ pc.update_2_opt.isDefined) + } + + val peerConnection = TestProbe() + // bob hasn't yet sent his channel_update but we can get it by looking at its internal data + val bobChannelUpdate = channels.bob.stateData.asInstanceOf[DATA_NORMAL].channelUpdate + router ! PeerRoutingMessage(peerConnection.ref, channels.bob.underlyingActor.nodeParams.nodeId, bobChannelUpdate) + + awaitAssert { + val pc = router.stateData.privateChannels.values.head + // both channel_updates are known + pc.update_1_opt.isDefined && pc.update_2_opt.isDefined + } + + // manual rebroadcast + router ! Router.TickBroadcast + rebroadcastListener.expectNoMessage() + + } + + test("public local channel", Tag(ChannelStateTestsTags.ChannelsPublic)) { f => + import f._ + + val fundingTx = reachNormal(channels, testTags) + + awaitAssert(router.stateData.privateChannels.size === 1) + + { + val pc = router.stateData.privateChannels.values.head + // only the local channel_update is known + assert(pc.update_1_opt.isDefined ^ pc.update_2_opt.isDefined) + } + + val peerConnection = TestProbe() + // alice and bob haven't yet sent their channel_updates but we can get them by looking at their internal data + val aliceChannelUpdate = channels.alice.stateData.asInstanceOf[DATA_NORMAL].channelUpdate + val bobChannelUpdate = channels.bob.stateData.asInstanceOf[DATA_NORMAL].channelUpdate + router ! PeerRoutingMessage(peerConnection.ref, channels.bob.underlyingActor.nodeParams.nodeId, bobChannelUpdate) + + awaitAssert { + val pc = router.stateData.privateChannels.values.head + // both channel_updates are known + pc.update_1_opt.isDefined && pc.update_2_opt.isDefined + } + + // funding tx reaches 6 blocks, announcements are exchanged + channels.alice ! WatchFundingDeeplyBuriedTriggered(BlockHeight(400000), 42, null) + channels.alice2bob.expectMsgType[AnnouncementSignatures] + channels.alice2bob.forward(channels.bob) + + channels.bob ! WatchFundingDeeplyBuriedTriggered(BlockHeight(400000), 42, null) + channels.bob2alice.expectMsgType[AnnouncementSignatures] + channels.bob2alice.forward(channels.alice) + + // router gets notified and attempts to validate the local channel + val vr = channels.alice2blockchain.expectMsgType[ZmqWatcher.ValidateRequest] + vr.replyTo ! ZmqWatcher.ValidateResult(vr.ann, Right((fundingTx, ZmqWatcher.UtxoStatus.Unspent))) + + awaitAssert { + router.stateData.privateChannels.isEmpty && router.stateData.channels.size == 1 + } + + awaitAssert { + val pc = router.stateData.channels.values.head + // both channel updates are preserved + pc.update_1_opt.isDefined && pc.update_2_opt.isDefined + } + + // manual rebroadcast + router ! Router.TickBroadcast + rebroadcastListener.expectMsg(Router.Rebroadcast( + channels = Map(vr.ann -> Set[GossipOrigin](LocalGossip)), + updates = Map(aliceChannelUpdate -> Set[GossipOrigin](LocalGossip), bobChannelUpdate -> Set.empty[GossipOrigin]), // broadcast the channel_updates (they were previously unannounced) + nodes = Map(router.underlyingActor.stateData.nodes.values.head -> Set[GossipOrigin](LocalGossip)), // new node_announcement + )) + + } + +} diff --git a/eclair-front/src/test/scala/fr/acinq/eclair/router/FrontRouterSpec.scala b/eclair-front/src/test/scala/fr/acinq/eclair/router/FrontRouterSpec.scala index d05ea1f218..404d3a2c7f 100644 --- a/eclair-front/src/test/scala/fr/acinq/eclair/router/FrontRouterSpec.scala +++ b/eclair-front/src/test/scala/fr/acinq/eclair/router/FrontRouterSpec.scala @@ -18,7 +18,7 @@ package fr.acinq.eclair.router import akka.actor.ActorSystem import akka.actor.typed.scaladsl.adapter.actorRefAdapter -import akka.testkit.{TestKit, TestProbe} +import akka.testkit.{TestFSMRef, TestKit, TestProbe} import fr.acinq.bitcoin.scalacompat.Crypto.PrivateKey import fr.acinq.bitcoin.scalacompat.Script.{pay2wsh, write} import fr.acinq.bitcoin.scalacompat.{Block, SatoshiLong, Transaction, TxOut} @@ -32,9 +32,6 @@ import fr.acinq.eclair.router.Router._ import fr.acinq.eclair.transactions.Scripts import fr.acinq.eclair.wire.protocol.Color import org.scalatest.funsuite.AnyFunSuiteLike -import scodec.bits._ - -import scala.concurrent.duration._ class FrontRouterSpec extends TestKit(ActorSystem("test")) with AnyFunSuiteLike { @@ -131,12 +128,14 @@ class FrontRouterSpec extends TestKit(ActorSystem("test")) with AnyFunSuiteLike peerConnection1b.expectMsg(GossipDecision.Accepted(chan_ab)) peerConnection2a.expectMsg(GossipDecision.Accepted(chan_ab)) - // we have to wait 2 times the broadcast interval because there is an additional per-peer delay - val maxBroadcastDelay = 2 * nodeParams.routerConf.routerBroadcastInterval + 1.second - peerConnection1a.expectMsg(maxBroadcastDelay, Rebroadcast(channels = Map(chan_ab -> Set(origin1a, origin1b)), updates = Map.empty, nodes = Map.empty)) - peerConnection1b.expectMsg(maxBroadcastDelay, Rebroadcast(channels = Map(chan_ab -> Set(origin1a, origin1b)), updates = Map.empty, nodes = Map.empty)) - peerConnection2a.expectMsg(maxBroadcastDelay, Rebroadcast(channels = Map(chan_ab -> Set(origin2a)), updates = Map.empty, nodes = Map.empty)) - peerConnection3a.expectMsg(maxBroadcastDelay, Rebroadcast(channels = Map(chan_ab -> Set.empty), updates = Map.empty, nodes = Map.empty)) + // manual rebroadcast + front1 ! Router.TickBroadcast + peerConnection1a.expectMsg(Rebroadcast(channels = Map(chan_ab -> Set(origin1a, origin1b)), updates = Map.empty, nodes = Map.empty)) + peerConnection1b.expectMsg(Rebroadcast(channels = Map(chan_ab -> Set(origin1a, origin1b)), updates = Map.empty, nodes = Map.empty)) + front2 ! Router.TickBroadcast + peerConnection2a.expectMsg(Rebroadcast(channels = Map(chan_ab -> Set(origin2a)), updates = Map.empty, nodes = Map.empty)) + front3 ! Router.TickBroadcast + peerConnection3a.expectMsg(Rebroadcast(channels = Map(chan_ab -> Set.empty), updates = Map.empty, nodes = Map.empty)) } test("aggregate gossip") { @@ -149,9 +148,18 @@ class FrontRouterSpec extends TestKit(ActorSystem("test")) with AnyFunSuiteLike val system2 = ActorSystem("front-system-2") val system3 = ActorSystem("front-system-3") - val front1 = system1.actorOf(FrontRouter.props(nodeParams.routerConf, router)) - val front2 = system2.actorOf(FrontRouter.props(nodeParams.routerConf, router)) - val front3 = system3.actorOf(FrontRouter.props(nodeParams.routerConf, router)) + val front1 = { + implicit val system: ActorSystem = system1 + TestFSMRef[FrontRouter.State, FrontRouter.Data, FrontRouter](new FrontRouter(nodeParams.routerConf, router)) + } + val front2 = { + implicit val system: ActorSystem = system2 + TestFSMRef[FrontRouter.State, FrontRouter.Data, FrontRouter](new FrontRouter(nodeParams.routerConf, router)) + } + val front3 = { + implicit val system: ActorSystem = system3 + TestFSMRef[FrontRouter.State, FrontRouter.Data, FrontRouter](new FrontRouter(nodeParams.routerConf, router)) + } val peerConnection1a = TestProbe("peerconn-1a") val peerConnection1b = TestProbe("peerconn-1b") @@ -182,7 +190,6 @@ class FrontRouterSpec extends TestKit(ActorSystem("test")) with AnyFunSuiteLike peerConnection3a.expectMsg(TransportHandler.ReadAck(channelUpdate_bc)) peerConnection3a.expectMsg(GossipDecision.NoRelatedChannel(channelUpdate_bc)) - watcher.send(router, ValidateResult(chan_ab, Right((Transaction(version = 0, txIn = Nil, txOut = TxOut(1000000 sat, write(pay2wsh(Scripts.multiSig2of2(funding_a, funding_b)))) :: Nil, lockTime = 0), UtxoStatus.Unspent)))) peerConnection1a.expectMsg(TransportHandler.ReadAck(chan_ab)) @@ -207,12 +214,18 @@ class FrontRouterSpec extends TestKit(ActorSystem("test")) with AnyFunSuiteLike peerConnection3a.expectMsg(TransportHandler.ReadAck(ann_b)) peerConnection3a.expectMsg(GossipDecision.Accepted(ann_b)) - // we have to wait 2 times the broadcast interval because there is an additional per-peer delay - val maxBroadcastDelay = 2 * nodeParams.routerConf.routerBroadcastInterval + 1.second - peerConnection1a.expectMsg(maxBroadcastDelay, Rebroadcast(channels = Map(chan_ab -> Set(origin1a, origin1b)), updates = Map(channelUpdate_ab -> Set(origin1b), channelUpdate_ba -> Set.empty), nodes = Map(ann_a -> Set.empty, ann_b -> Set.empty))) - peerConnection1b.expectMsg(maxBroadcastDelay, Rebroadcast(channels = Map(chan_ab -> Set(origin1a, origin1b)), updates = Map(channelUpdate_ab -> Set(origin1b), channelUpdate_ba -> Set.empty), nodes = Map(ann_a -> Set.empty, ann_b -> Set.empty))) - peerConnection2a.expectMsg(maxBroadcastDelay, Rebroadcast(channels = Map(chan_ab -> Set(origin2a)), updates = Map(channelUpdate_ab -> Set.empty, channelUpdate_ba -> Set.empty), nodes = Map(ann_a -> Set.empty, ann_b -> Set.empty))) - peerConnection3a.expectMsg(maxBroadcastDelay, Rebroadcast(channels = Map(chan_ab -> Set.empty), updates = Map(channelUpdate_ab -> Set.empty, channelUpdate_ba -> Set(origin3a)), nodes = Map(ann_a -> Set(origin3a), ann_b -> Set(origin3a)))) + awaitCond(front1.stateData.nodes.size == 2) + awaitCond(front2.stateData.nodes.size == 2) + awaitCond(front3.stateData.nodes.size == 2) + + // manual rebroadcast + front1 ! Router.TickBroadcast + peerConnection1a.expectMsg(Rebroadcast(channels = Map(chan_ab -> Set(origin1a, origin1b)), updates = Map(channelUpdate_ab -> Set(origin1b), channelUpdate_ba -> Set.empty), nodes = Map(ann_a -> Set.empty, ann_b -> Set.empty))) + peerConnection1b.expectMsg(Rebroadcast(channels = Map(chan_ab -> Set(origin1a, origin1b)), updates = Map(channelUpdate_ab -> Set(origin1b), channelUpdate_ba -> Set.empty), nodes = Map(ann_a -> Set.empty, ann_b -> Set.empty))) + front2 ! Router.TickBroadcast + peerConnection2a.expectMsg(Rebroadcast(channels = Map(chan_ab -> Set(origin2a)), updates = Map(channelUpdate_ab -> Set.empty, channelUpdate_ba -> Set.empty), nodes = Map(ann_a -> Set.empty, ann_b -> Set.empty))) + front3 ! Router.TickBroadcast + peerConnection3a.expectMsg(Rebroadcast(channels = Map(chan_ab -> Set.empty), updates = Map(channelUpdate_ab -> Set.empty, channelUpdate_ba -> Set(origin3a)), nodes = Map(ann_a -> Set(origin3a), ann_b -> Set(origin3a)))) } test("do not forward duplicate gossip") { diff --git a/pom.xml b/pom.xml index 52490a781a..f6272e3e76 100644 --- a/pom.xml +++ b/pom.xml @@ -235,7 +235,7 @@ ${project.build.directory} - -Xmx1024m -Dfile.encoding=UTF-8 + -Xmx2048m -Dfile.encoding=UTF-8 From 03097b0d42b29346535c87480c9e990bcdc83203 Mon Sep 17 00:00:00 2001 From: Bastien Teinturier <31281497+t-bast@users.noreply.github.com> Date: Thu, 19 May 2022 09:43:08 +0200 Subject: [PATCH 071/121] Add `localChannelReserve` and `remoteChannelReserve` (#2237) This is easier to use than having to decide which params we should look into (local or remote). It will also be easier to integrate with dual funding. Rename initialFeeratePerKw: this name was very confusing. This feerate only applies to the commit tx, so we make that explicit. --- .../fr/acinq/eclair/channel/ChannelData.scala | 16 ++--- .../acinq/eclair/channel/ChannelEvents.scala | 2 +- .../fr/acinq/eclair/channel/Commitments.scala | 34 ++++++----- .../fr/acinq/eclair/channel/Helpers.scala | 17 +++--- .../fr/acinq/eclair/channel/fsm/Channel.scala | 8 +-- .../channel/fsm/ChannelOpenSingleFunder.scala | 20 +++---- .../main/scala/fr/acinq/eclair/io/Peer.scala | 2 +- .../acinq/eclair/json/JsonSerializers.scala | 4 +- .../channel/version0/ChannelCodecs0.scala | 7 +-- .../channel/version1/ChannelCodecs1.scala | 6 +- .../channel/version2/ChannelCodecs2.scala | 6 +- .../channel/version3/ChannelCodecs3.scala | 12 ++-- .../scala/fr/acinq/eclair/TestConstants.scala | 4 +- .../eclair/channel/CommitmentsSpec.scala | 8 +-- .../ChannelStateTestsHelperMethods.scala | 6 +- .../a/WaitForAcceptChannelStateSpec.scala | 4 +- .../a/WaitForOpenChannelStateSpec.scala | 4 +- .../b/WaitForFundingCreatedStateSpec.scala | 2 +- .../channel/states/e/NormalStateSpec.scala | 2 +- .../scala/fr/acinq/eclair/io/PeerSpec.scala | 8 +-- .../eclair/payment/PaymentPacketSpec.scala | 4 +- .../internal/channel/ChannelCodecsSpec.scala | 10 ++-- .../channel/version1/ChannelCodecs1Spec.scala | 10 ++-- .../channel/version3/ChannelCodecs3Spec.scala | 59 +++++++++++++++++-- .../fr/acinq/eclair/api/ApiServiceSpec.scala | 2 +- 25 files changed, 157 insertions(+), 100 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala index 6a1ffd5bea..b77a4c1797 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala @@ -18,7 +18,7 @@ package fr.acinq.eclair.channel import akka.actor.{ActorRef, PossiblyHarmful} import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey -import fr.acinq.bitcoin.scalacompat.{ByteVector32, DeterministicWallet, OutPoint, Satoshi, Transaction} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, DeterministicWallet, OutPoint, Satoshi, SatoshiLong, Transaction} import fr.acinq.eclair.blockchain.fee.FeeratePerKw import fr.acinq.eclair.payment.OutgoingPaymentPacket.Upstream import fr.acinq.eclair.transactions.CommitmentSpec @@ -78,8 +78,8 @@ case object ERR_INFORMATION_LEAK extends ChannelState case class INPUT_INIT_FUNDER(temporaryChannelId: ByteVector32, fundingAmount: Satoshi, pushAmount: MilliSatoshi, - initialFeeratePerKw: FeeratePerKw, - fundingTxFeeratePerKw: FeeratePerKw, + commitTxFeerate: FeeratePerKw, + fundingTxFeerate: FeeratePerKw, localParams: LocalParams, remote: ActorRef, remoteInit: Init, @@ -196,7 +196,7 @@ final case class CMD_GET_CHANNEL_INFO(replyTo: ActorRef)extends HasReplyToComman /** response to [[Command]] requests */ sealed trait CommandResponse[+C <: Command] sealed trait CommandSuccess[+C <: Command] extends CommandResponse[C] -sealed trait CommandFailure[+C <: Command, +T <: Throwable] extends CommandResponse[C] { def t: Throwable } +sealed trait CommandFailure[+C <: Command, +T <: Throwable] extends CommandResponse[C] { def t: T } /** generic responses */ final case class RES_SUCCESS[+C <: Command](cmd: C, channelId: ByteVector32) extends CommandSuccess[C] @@ -387,7 +387,7 @@ final case class DATA_WAIT_FOR_FUNDING_INTERNAL(temporaryChannelId: ByteVector32 remoteParams: RemoteParams, fundingAmount: Satoshi, pushAmount: MilliSatoshi, - initialFeeratePerKw: FeeratePerKw, + commitTxFeerate: FeeratePerKw, remoteFirstPerCommitmentPoint: PublicKey, channelConfig: ChannelConfig, channelFeatures: ChannelFeatures, @@ -399,7 +399,7 @@ final case class DATA_WAIT_FOR_FUNDING_CREATED(temporaryChannelId: ByteVector32, remoteParams: RemoteParams, fundingAmount: Satoshi, pushAmount: MilliSatoshi, - initialFeeratePerKw: FeeratePerKw, + commitTxFeerate: FeeratePerKw, remoteFirstPerCommitmentPoint: PublicKey, channelFlags: ChannelFlags, channelConfig: ChannelConfig, @@ -466,7 +466,7 @@ case class LocalParams(nodeId: PublicKey, fundingKeyPath: DeterministicWallet.KeyPath, dustLimit: Satoshi, maxHtlcValueInFlightMsat: UInt64, // this is not MilliSatoshi because it can exceed the total amount of MilliSatoshi - channelReserve: Satoshi, + requestedChannelReserve_opt: Option[Satoshi], htlcMinimum: MilliSatoshi, toSelfDelay: CltvExpiryDelta, maxAcceptedHtlcs: Int, @@ -481,7 +481,7 @@ case class LocalParams(nodeId: PublicKey, case class RemoteParams(nodeId: PublicKey, dustLimit: Satoshi, maxHtlcValueInFlightMsat: UInt64, // this is not MilliSatoshi because it can exceed the total amount of MilliSatoshi - channelReserve: Satoshi, + requestedChannelReserve_opt: Option[Satoshi], htlcMinimum: MilliSatoshi, toSelfDelay: CltvExpiryDelta, maxAcceptedHtlcs: Int, diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelEvents.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelEvents.scala index 671de2a3f0..236acffead 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelEvents.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelEvents.scala @@ -30,7 +30,7 @@ import fr.acinq.eclair.wire.protocol.{ChannelAnnouncement, ChannelUpdate} trait ChannelEvent -case class ChannelCreated(channel: ActorRef, peer: ActorRef, remoteNodeId: PublicKey, isInitiator: Boolean, temporaryChannelId: ByteVector32, initialFeeratePerKw: FeeratePerKw, fundingTxFeeratePerKw: Option[FeeratePerKw]) extends ChannelEvent +case class ChannelCreated(channel: ActorRef, peer: ActorRef, remoteNodeId: PublicKey, isInitiator: Boolean, temporaryChannelId: ByteVector32, commitTxFeerate: FeeratePerKw, fundingTxFeerate: Option[FeeratePerKw]) extends ChannelEvent // This trait can be used by non-standard channels to inject themselves into Register actor and thus make them usable for routing trait AbstractChannelRestored extends ChannelEvent { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala index 1e03c67679..00d0b26989 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala @@ -211,6 +211,12 @@ case class Commitments(channelId: ByteVector32, val capacity: Satoshi = commitInput.txOut.amount + /** Channel reserve that applies to our funds. */ + val localChannelReserve: Satoshi = remoteParams.requestedChannelReserve_opt.getOrElse(0 sat) + + /** Channel reserve that applies to our peer's funds. */ + val remoteChannelReserve: Satoshi = localParams.requestedChannelReserve_opt.getOrElse(0 sat) + // NB: when computing availableBalanceForSend and availableBalanceForReceive, the initiator keeps an extra buffer on // top of its usual channel reserve to avoid getting channels stuck in case the on-chain feerate increases (see // https://github.com/lightningnetwork/lightning-rfc/issues/728 for details). @@ -241,7 +247,7 @@ case class Commitments(channelId: ByteVector32, // we need to base the next current commitment on the last sig we sent, even if we didn't yet receive their revocation val remoteCommit1 = remoteNextCommitInfo.left.toOption.map(_.nextRemoteCommit).getOrElse(remoteCommit) val reduced = CommitmentSpec.reduce(remoteCommit1.spec, remoteChanges.acked, localChanges.proposed) - val balanceNoFees = (reduced.toRemote - remoteParams.channelReserve).max(0 msat) + val balanceNoFees = (reduced.toRemote - localChannelReserve).max(0 msat) if (localParams.isInitiator) { // The initiator always pays the on-chain fees, so we must subtract that from the amount we can send. val commitFees = commitTxTotalCostMsat(remoteParams.dustLimit, reduced, commitmentFormat) @@ -267,7 +273,7 @@ case class Commitments(channelId: ByteVector32, lazy val availableBalanceForReceive: MilliSatoshi = { val reduced = CommitmentSpec.reduce(localCommit.spec, localChanges.acked, remoteChanges.proposed) - val balanceNoFees = (reduced.toRemote - localParams.channelReserve).max(0 msat) + val balanceNoFees = (reduced.toRemote - remoteChannelReserve).max(0 msat) if (localParams.isInitiator) { // The non-initiator doesn't pay on-chain fees so we don't take those into account when receiving. balanceNoFees @@ -372,15 +378,15 @@ object Commitments { val funderFeeBuffer = commitTxTotalCostMsat(commitments1.remoteParams.dustLimit, reduced.copy(commitTxFeerate = reduced.commitTxFeerate * 2), commitments.commitmentFormat) + htlcOutputFee(reduced.commitTxFeerate * 2, commitments.commitmentFormat) // NB: increasing the feerate can actually remove htlcs from the commit tx (if they fall below the trim threshold) // which may result in a lower commit tx fee; this is why we take the max of the two. - val missingForSender = reduced.toRemote - commitments1.remoteParams.channelReserve - (if (commitments1.localParams.isInitiator) fees.max(funderFeeBuffer.truncateToSatoshi) else 0.sat) - val missingForReceiver = reduced.toLocal - commitments1.localParams.channelReserve - (if (commitments1.localParams.isInitiator) 0.sat else fees) + val missingForSender = reduced.toRemote - commitments1.localChannelReserve - (if (commitments1.localParams.isInitiator) fees.max(funderFeeBuffer.truncateToSatoshi) else 0.sat) + val missingForReceiver = reduced.toLocal - commitments1.remoteChannelReserve - (if (commitments1.localParams.isInitiator) 0.sat else fees) if (missingForSender < 0.msat) { - return Left(InsufficientFunds(commitments.channelId, amount = cmd.amount, missing = -missingForSender.truncateToSatoshi, reserve = commitments1.remoteParams.channelReserve, fees = if (commitments1.localParams.isInitiator) fees else 0.sat)) + return Left(InsufficientFunds(commitments.channelId, amount = cmd.amount, missing = -missingForSender.truncateToSatoshi, reserve = commitments1.localChannelReserve, fees = if (commitments1.localParams.isInitiator) fees else 0.sat)) } else if (missingForReceiver < 0.msat) { if (commitments.localParams.isInitiator) { // receiver is not the channel initiator; it is ok if it can't maintain its channel_reserve for now, as long as its balance is increasing, which is the case if it is receiving a payment } else { - return Left(RemoteCannotAffordFeesForNewHtlc(commitments.channelId, amount = cmd.amount, missing = -missingForReceiver.truncateToSatoshi, reserve = commitments1.remoteParams.channelReserve, fees = fees)) + return Left(RemoteCannotAffordFeesForNewHtlc(commitments.channelId, amount = cmd.amount, missing = -missingForReceiver.truncateToSatoshi, reserve = commitments1.remoteChannelReserve, fees = fees)) } } @@ -441,13 +447,13 @@ object Commitments { val fees = commitTxTotalCost(commitments1.remoteParams.dustLimit, reduced, commitments.commitmentFormat) // NB: we don't enforce the funderFeeReserve (see sendAdd) because it would confuse a remote initiator that doesn't have this mitigation in place // We could enforce it once we're confident a large portion of the network implements it. - val missingForSender = reduced.toRemote - commitments1.localParams.channelReserve - (if (commitments1.localParams.isInitiator) 0.sat else fees) - val missingForReceiver = reduced.toLocal - commitments1.remoteParams.channelReserve - (if (commitments1.localParams.isInitiator) fees else 0.sat) + val missingForSender = reduced.toRemote - commitments1.remoteChannelReserve - (if (commitments1.localParams.isInitiator) 0.sat else fees) + val missingForReceiver = reduced.toLocal - commitments1.localChannelReserve - (if (commitments1.localParams.isInitiator) fees else 0.sat) if (missingForSender < 0.sat) { - return Left(InsufficientFunds(commitments.channelId, amount = add.amountMsat, missing = -missingForSender.truncateToSatoshi, reserve = commitments1.localParams.channelReserve, fees = if (commitments1.localParams.isInitiator) 0.sat else fees)) + return Left(InsufficientFunds(commitments.channelId, amount = add.amountMsat, missing = -missingForSender.truncateToSatoshi, reserve = commitments1.remoteChannelReserve, fees = if (commitments1.localParams.isInitiator) 0.sat else fees)) } else if (missingForReceiver < 0.sat) { if (commitments.localParams.isInitiator) { - return Left(CannotAffordFees(commitments.channelId, missing = -missingForReceiver.truncateToSatoshi, reserve = commitments1.remoteParams.channelReserve, fees = fees)) + return Left(CannotAffordFees(commitments.channelId, missing = -missingForReceiver.truncateToSatoshi, reserve = commitments1.localChannelReserve, fees = fees)) } else { // receiver is not the channel initiator; it is ok if it can't maintain its channel_reserve for now, as long as its balance is increasing, which is the case if it is receiving a payment } @@ -558,9 +564,9 @@ object Commitments { // a node cannot spend pending incoming htlcs, and need to keep funds above the reserve required by the counterparty, after paying the fee // we look from remote's point of view, so if local is initiator remote doesn't pay the fees val fees = commitTxTotalCost(commitments1.remoteParams.dustLimit, reduced, commitments.commitmentFormat) - val missing = reduced.toRemote.truncateToSatoshi - commitments1.remoteParams.channelReserve - fees + val missing = reduced.toRemote.truncateToSatoshi - commitments1.localChannelReserve - fees if (missing < 0.sat) { - return Left(CannotAffordFees(commitments.channelId, missing = -missing, reserve = commitments1.remoteParams.channelReserve, fees = fees)) + return Left(CannotAffordFees(commitments.channelId, missing = -missing, reserve = commitments1.localChannelReserve, fees = fees)) } // if we would overflow our dust exposure with the new feerate, we avoid sending this fee update @@ -608,9 +614,9 @@ object Commitments { // a node cannot spend pending incoming htlcs, and need to keep funds above the reserve required by the counterparty, after paying the fee val fees = commitTxTotalCost(commitments1.localParams.dustLimit, reduced, commitments.commitmentFormat) - val missing = reduced.toRemote.truncateToSatoshi - commitments1.localParams.channelReserve - fees + val missing = reduced.toRemote.truncateToSatoshi - commitments1.remoteChannelReserve - fees if (missing < 0.sat) { - return Left(CannotAffordFees(commitments.channelId, missing = -missing, reserve = commitments1.localParams.channelReserve, fees = fees)) + return Left(CannotAffordFees(commitments.channelId, missing = -missing, reserve = commitments1.remoteChannelReserve, fees = fees)) } // if we would overflow our dust exposure with the new feerate, we reject this fee update diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala index b6c6f249fe..bde048e81b 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala @@ -17,10 +17,10 @@ package fr.acinq.eclair.channel import akka.event.{DiagnosticLoggingAdapter, LoggingAdapter} +import fr.acinq.bitcoin.ScriptFlags import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey, sha256} import fr.acinq.bitcoin.scalacompat.Script._ import fr.acinq.bitcoin.scalacompat._ -import fr.acinq.bitcoin.ScriptFlags import fr.acinq.eclair._ import fr.acinq.eclair.blockchain.OnChainAddressGenerator import fr.acinq.eclair.blockchain.fee.{FeeEstimator, FeeTargets, FeeratePerKw} @@ -254,8 +254,8 @@ object Helpers { } val toRemoteSatoshis = remoteCommit.spec.toRemote.truncateToSatoshi // NB: this is an approximation (we don't take network fees into account) - val result = toRemoteSatoshis > commitments.remoteParams.channelReserve - log.debug(s"toRemoteSatoshis=$toRemoteSatoshis reserve=${commitments.remoteParams.channelReserve} aboveReserve=$result for remoteCommitNumber=${remoteCommit.index}") + val result = toRemoteSatoshis > commitments.localChannelReserve + log.debug(s"toRemoteSatoshis=$toRemoteSatoshis reserve=${commitments.localChannelReserve} aboveReserve=$result for remoteCommitNumber=${remoteCommit.index}") result } @@ -289,20 +289,21 @@ object Helpers { * * @return (localSpec, localTx, remoteSpec, remoteTx, fundingTxOutput) */ - def makeFirstCommitTxs(keyManager: ChannelKeyManager, channelConfig: ChannelConfig, channelFeatures: ChannelFeatures, temporaryChannelId: ByteVector32, localParams: LocalParams, remoteParams: RemoteParams, fundingAmount: Satoshi, pushMsat: MilliSatoshi, initialFeeratePerKw: FeeratePerKw, fundingTxHash: ByteVector32, fundingTxOutputIndex: Int, remoteFirstPerCommitmentPoint: PublicKey): Either[ChannelException, (CommitmentSpec, CommitTx, CommitmentSpec, CommitTx)] = { + def makeFirstCommitTxs(keyManager: ChannelKeyManager, channelConfig: ChannelConfig, channelFeatures: ChannelFeatures, temporaryChannelId: ByteVector32, localParams: LocalParams, remoteParams: RemoteParams, fundingAmount: Satoshi, pushMsat: MilliSatoshi, commitTxFeerate: FeeratePerKw, fundingTxHash: ByteVector32, fundingTxOutputIndex: Int, remoteFirstPerCommitmentPoint: PublicKey): Either[ChannelException, (CommitmentSpec, CommitTx, CommitmentSpec, CommitTx)] = { val toLocalMsat = if (localParams.isInitiator) fundingAmount.toMilliSatoshi - pushMsat else pushMsat val toRemoteMsat = if (localParams.isInitiator) pushMsat else fundingAmount.toMilliSatoshi - pushMsat - val localSpec = CommitmentSpec(Set.empty[DirectedHtlc], initialFeeratePerKw, toLocal = toLocalMsat, toRemote = toRemoteMsat) - val remoteSpec = CommitmentSpec(Set.empty[DirectedHtlc], initialFeeratePerKw, toLocal = toRemoteMsat, toRemote = toLocalMsat) + val localSpec = CommitmentSpec(Set.empty[DirectedHtlc], commitTxFeerate, toLocal = toLocalMsat, toRemote = toRemoteMsat) + val remoteSpec = CommitmentSpec(Set.empty[DirectedHtlc], commitTxFeerate, toLocal = toRemoteMsat, toRemote = toLocalMsat) if (!localParams.isInitiator) { // they initiated the channel open, therefore they pay the fee: we need to make sure they can afford it! val toRemoteMsat = remoteSpec.toLocal val fees = commitTxTotalCost(remoteParams.dustLimit, remoteSpec, channelFeatures.commitmentFormat) - val missing = toRemoteMsat.truncateToSatoshi - localParams.channelReserve - fees + val reserve = localParams.requestedChannelReserve_opt.getOrElse(0 sat) + val missing = toRemoteMsat.truncateToSatoshi - reserve - fees if (missing < Satoshi(0)) { - return Left(CannotAffordFees(temporaryChannelId, missing = -missing, reserve = localParams.channelReserve, fees = fees)) + return Left(CannotAffordFees(temporaryChannelId, missing = -missing, reserve = reserve, fees = fees)) } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala index 7b7547a5be..c63762c96c 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala @@ -208,8 +208,8 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val startWith(WAIT_FOR_INIT_INTERNAL, Nothing) when(WAIT_FOR_INIT_INTERNAL)(handleExceptions { - case Event(initFunder@INPUT_INIT_FUNDER(temporaryChannelId, fundingSatoshis, pushMsat, initialFeeratePerKw, fundingTxFeeratePerKw, localParams, remote, remoteInit, channelFlags, channelConfig, channelType), Nothing) => - context.system.eventStream.publish(ChannelCreated(self, peer, remoteNodeId, isInitiator = true, temporaryChannelId, initialFeeratePerKw, Some(fundingTxFeeratePerKw))) + case Event(initFunder@INPUT_INIT_FUNDER(temporaryChannelId, fundingSatoshis, pushMsat, commitTxFeerate, fundingTxFeerate, localParams, remote, remoteInit, channelFlags, channelConfig, channelType), Nothing) => + context.system.eventStream.publish(ChannelCreated(self, peer, remoteNodeId, isInitiator = true, temporaryChannelId, commitTxFeerate, Some(fundingTxFeerate))) activeConnection = remote txPublisher ! SetChannelId(remoteNodeId, temporaryChannelId) val fundingPubKey = keyManager.fundingPublicKey(localParams.fundingKeyPath).publicKey @@ -223,9 +223,9 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val pushMsat = pushMsat, dustLimitSatoshis = localParams.dustLimit, maxHtlcValueInFlightMsat = localParams.maxHtlcValueInFlightMsat, - channelReserveSatoshis = localParams.channelReserve, + channelReserveSatoshis = localParams.requestedChannelReserve_opt.getOrElse(0 sat), htlcMinimumMsat = localParams.htlcMinimum, - feeratePerKw = initialFeeratePerKw, + feeratePerKw = commitTxFeerate, toSelfDelay = localParams.toSelfDelay, maxAcceptedHtlcs = localParams.maxAcceptedHtlcs, fundingPubkey = fundingPubKey, diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenSingleFunder.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenSingleFunder.scala index 932f8016a1..97f71b18fc 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenSingleFunder.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenSingleFunder.scala @@ -87,7 +87,7 @@ trait ChannelOpenSingleFunder extends FundingHandlers with ErrorHandlers { val accept = AcceptChannel(temporaryChannelId = open.temporaryChannelId, dustLimitSatoshis = localParams.dustLimit, maxHtlcValueInFlightMsat = localParams.maxHtlcValueInFlightMsat, - channelReserveSatoshis = localParams.channelReserve, + channelReserveSatoshis = localParams.requestedChannelReserve_opt.getOrElse(0 sat), minimumDepth = minimumDepth, htlcMinimumMsat = localParams.htlcMinimum, toSelfDelay = localParams.toSelfDelay, @@ -106,7 +106,7 @@ trait ChannelOpenSingleFunder extends FundingHandlers with ErrorHandlers { nodeId = remoteNodeId, dustLimit = open.dustLimitSatoshis, maxHtlcValueInFlightMsat = open.maxHtlcValueInFlightMsat, - channelReserve = open.channelReserveSatoshis, // remote requires local to keep this much satoshis as direct payment + requestedChannelReserve_opt = Some(open.channelReserveSatoshis), // our peer requires us to always have at least that much satoshis in our balance htlcMinimum = open.htlcMinimumMsat, toSelfDelay = open.toSelfDelay, maxAcceptedHtlcs = open.maxAcceptedHtlcs, @@ -129,7 +129,7 @@ trait ChannelOpenSingleFunder extends FundingHandlers with ErrorHandlers { }) when(WAIT_FOR_ACCEPT_CHANNEL)(handleExceptions { - case Event(accept: AcceptChannel, d@DATA_WAIT_FOR_ACCEPT_CHANNEL(INPUT_INIT_FUNDER(temporaryChannelId, fundingSatoshis, pushMsat, initialFeeratePerKw, fundingTxFeeratePerKw, localParams, _, remoteInit, _, channelConfig, channelType), open)) => + case Event(accept: AcceptChannel, d@DATA_WAIT_FOR_ACCEPT_CHANNEL(INPUT_INIT_FUNDER(temporaryChannelId, fundingSatoshis, pushMsat, commitTxFeerate, fundingTxFeerate, localParams, _, remoteInit, _, channelConfig, channelType), open)) => Helpers.validateParamsFunder(nodeParams, channelType, localParams.initFeatures, remoteInit.features, open, accept) match { case Left(t) => channelOpenReplyToUser(Left(LocalError(t))) @@ -139,7 +139,7 @@ trait ChannelOpenSingleFunder extends FundingHandlers with ErrorHandlers { nodeId = remoteNodeId, dustLimit = accept.dustLimitSatoshis, maxHtlcValueInFlightMsat = accept.maxHtlcValueInFlightMsat, - channelReserve = accept.channelReserveSatoshis, // remote requires local to keep this much satoshis as direct payment + requestedChannelReserve_opt = Some(accept.channelReserveSatoshis), // our peer requires us to always have at least that much satoshis in our balance htlcMinimum = accept.htlcMinimumMsat, toSelfDelay = accept.toSelfDelay, maxAcceptedHtlcs = accept.maxAcceptedHtlcs, @@ -153,8 +153,8 @@ trait ChannelOpenSingleFunder extends FundingHandlers with ErrorHandlers { log.debug("remote params: {}", remoteParams) val localFundingPubkey = keyManager.fundingPublicKey(localParams.fundingKeyPath) val fundingPubkeyScript = Script.write(Script.pay2wsh(Scripts.multiSig2of2(localFundingPubkey.publicKey, remoteParams.fundingPubKey))) - wallet.makeFundingTx(fundingPubkeyScript, fundingSatoshis, fundingTxFeeratePerKw).pipeTo(self) - goto(WAIT_FOR_FUNDING_INTERNAL) using DATA_WAIT_FOR_FUNDING_INTERNAL(temporaryChannelId, localParams, remoteParams, fundingSatoshis, pushMsat, initialFeeratePerKw, accept.firstPerCommitmentPoint, channelConfig, channelFeatures, open) + wallet.makeFundingTx(fundingPubkeyScript, fundingSatoshis, fundingTxFeerate).pipeTo(self) + goto(WAIT_FOR_FUNDING_INTERNAL) using DATA_WAIT_FOR_FUNDING_INTERNAL(temporaryChannelId, localParams, remoteParams, fundingSatoshis, pushMsat, commitTxFeerate, accept.firstPerCommitmentPoint, channelConfig, channelFeatures, open) } case Event(c: CloseCommand, d: DATA_WAIT_FOR_ACCEPT_CHANNEL) => @@ -175,9 +175,9 @@ trait ChannelOpenSingleFunder extends FundingHandlers with ErrorHandlers { }) when(WAIT_FOR_FUNDING_INTERNAL)(handleExceptions { - case Event(MakeFundingTxResponse(fundingTx, fundingTxOutputIndex, fundingTxFee), d@DATA_WAIT_FOR_FUNDING_INTERNAL(temporaryChannelId, localParams, remoteParams, fundingAmount, pushMsat, initialFeeratePerKw, remoteFirstPerCommitmentPoint, channelConfig, channelFeatures, open)) => + case Event(MakeFundingTxResponse(fundingTx, fundingTxOutputIndex, fundingTxFee), d@DATA_WAIT_FOR_FUNDING_INTERNAL(temporaryChannelId, localParams, remoteParams, fundingAmount, pushMsat, commitTxFeerate, remoteFirstPerCommitmentPoint, channelConfig, channelFeatures, open)) => // let's create the first commitment tx that spends the yet uncommitted funding tx - Funding.makeFirstCommitTxs(keyManager, channelConfig, channelFeatures, temporaryChannelId, localParams, remoteParams, fundingAmount, pushMsat, initialFeeratePerKw, fundingTx.hash, fundingTxOutputIndex, remoteFirstPerCommitmentPoint) match { + Funding.makeFirstCommitTxs(keyManager, channelConfig, channelFeatures, temporaryChannelId, localParams, remoteParams, fundingAmount, pushMsat, commitTxFeerate, fundingTx.hash, fundingTxOutputIndex, remoteFirstPerCommitmentPoint) match { case Left(ex) => handleLocalError(ex, d, None) case Right((localSpec, localCommitTx, remoteSpec, remoteCommitTx)) => require(fundingTx.txOut(fundingTxOutputIndex).publicKeyScript == localCommitTx.input.txOut.publicKeyScript, s"pubkey script mismatch!") @@ -220,9 +220,9 @@ trait ChannelOpenSingleFunder extends FundingHandlers with ErrorHandlers { }) when(WAIT_FOR_FUNDING_CREATED)(handleExceptions { - case Event(FundingCreated(_, fundingTxHash, fundingTxOutputIndex, remoteSig, _), d@DATA_WAIT_FOR_FUNDING_CREATED(temporaryChannelId, localParams, remoteParams, fundingAmount, pushMsat, initialFeeratePerKw, remoteFirstPerCommitmentPoint, channelFlags, channelConfig, channelFeatures, _)) => + case Event(FundingCreated(_, fundingTxHash, fundingTxOutputIndex, remoteSig, _), d@DATA_WAIT_FOR_FUNDING_CREATED(temporaryChannelId, localParams, remoteParams, fundingAmount, pushMsat, commitTxFeerate, remoteFirstPerCommitmentPoint, channelFlags, channelConfig, channelFeatures, _)) => // they fund the channel with their funding tx, so the money is theirs (but we are paid pushMsat) - Funding.makeFirstCommitTxs(keyManager, channelConfig, channelFeatures, temporaryChannelId, localParams, remoteParams, fundingAmount, pushMsat, initialFeeratePerKw, fundingTxHash, fundingTxOutputIndex, remoteFirstPerCommitmentPoint) match { + Funding.makeFirstCommitTxs(keyManager, channelConfig, channelFeatures, temporaryChannelId, localParams, remoteParams, fundingAmount, pushMsat, commitTxFeerate, fundingTxHash, fundingTxOutputIndex, remoteFirstPerCommitmentPoint) match { case Left(ex) => handleLocalError(ex, d, None) case Right((localSpec, localCommitTx, remoteSpec, remoteCommitTx)) => // check remote signature validity diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala index 72735a7b95..e6da8cf17e 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala @@ -506,7 +506,7 @@ object Peer { nodeParams.channelKeyManager.newFundingKeyPath(isInitiator), // we make sure that initiator and non-initiator key paths end differently dustLimit = nodeParams.channelConf.dustLimit, maxHtlcValueInFlightMsat = nodeParams.channelConf.maxHtlcValueInFlightMsat, - channelReserve = (fundingAmount * nodeParams.channelConf.reserveToFundingRatio).max(nodeParams.channelConf.dustLimit), // BOLT #2: make sure that our reserve is above our dust limit + requestedChannelReserve_opt = Some((fundingAmount * nodeParams.channelConf.reserveToFundingRatio).max(nodeParams.channelConf.dustLimit)), // BOLT #2: make sure that our reserve is above our dust limit htlcMinimum = nodeParams.channelConf.htlcMinimum, toSelfDelay = nodeParams.channelConf.toRemoteDelay, // we choose their delay maxAcceptedHtlcs = nodeParams.channelConf.maxAcceptedHtlcs, diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/json/JsonSerializers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/json/JsonSerializers.scala index d6941bfb84..5c44a25968 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/json/JsonSerializers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/json/JsonSerializers.scala @@ -401,8 +401,8 @@ object ChannelEventSerializer extends MinimalSerializer({ JField("remoteNodeId", JString(e.remoteNodeId.toString())), JField("isInitiator", JBool(e.isInitiator)), JField("temporaryChannelId", JString(e.temporaryChannelId.toHex)), - JField("initialFeeratePerKw", JLong(e.initialFeeratePerKw.toLong)), - JField("fundingTxFeeratePerKw", e.fundingTxFeeratePerKw.map(f => JLong(f.toLong)).getOrElse(JNothing)) + JField("commitTxFeeratePerKw", JLong(e.commitTxFeerate.toLong)), + JField("fundingTxFeeratePerKw", e.fundingTxFeerate.map(f => JLong(f.toLong)).getOrElse(JNothing)) ) case e: ChannelStateChanged => JObject( JField("type", JString("channel-state-changed")), diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelCodecs0.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelCodecs0.scala index 3dce2bf7ba..fa23ee1f42 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelCodecs0.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelCodecs0.scala @@ -18,7 +18,7 @@ package fr.acinq.eclair.wire.internal.channel.version0 import fr.acinq.bitcoin.scalacompat.DeterministicWallet.{ExtendedPrivateKey, KeyPath} import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, Crypto, OutPoint, Transaction, TxOut} -import fr.acinq.eclair.TimestampSecond +import fr.acinq.eclair.{BlockHeight, TimestampSecond} import fr.acinq.eclair.channel._ import fr.acinq.eclair.crypto.ShaChain import fr.acinq.eclair.transactions.Transactions._ @@ -27,7 +27,6 @@ import fr.acinq.eclair.wire.internal.channel.version0.ChannelTypes0.{HtlcTxAndSi import fr.acinq.eclair.wire.protocol.CommonCodecs._ import fr.acinq.eclair.wire.protocol.LightningMessageCodecs.{channelAnnouncementCodec, channelUpdateCodec, combinedFeaturesCodec} import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{BlockHeight, Features, InitFeature, TimestampSecond} import scodec.Codec import scodec.bits.{BitVector, ByteVector} import scodec.codecs._ @@ -69,7 +68,7 @@ private[channel] object ChannelCodecs0 { ("channelPath" | keyPathCodec) :: ("dustLimit" | satoshi) :: ("maxHtlcValueInFlightMsat" | uint64) :: - ("channelReserve" | satoshi) :: + ("channelReserve" | conditional(included = true, satoshi)) :: ("htlcMinimum" | millisatoshi) :: ("toSelfDelay" | cltvExpiryDelta) :: ("maxAcceptedHtlcs" | uint16) :: @@ -82,7 +81,7 @@ private[channel] object ChannelCodecs0 { ("nodeId" | publicKey) :: ("dustLimit" | satoshi) :: ("maxHtlcValueInFlightMsat" | uint64) :: - ("channelReserve" | satoshi) :: + ("channelReserve" | conditional(included = true, satoshi)) :: ("htlcMinimum" | millisatoshi) :: ("toSelfDelay" | cltvExpiryDelta) :: ("maxAcceptedHtlcs" | uint16) :: diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version1/ChannelCodecs1.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version1/ChannelCodecs1.scala index 4337f43b40..d4173bef30 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version1/ChannelCodecs1.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version1/ChannelCodecs1.scala @@ -18,6 +18,7 @@ package fr.acinq.eclair.wire.internal.channel.version1 import fr.acinq.bitcoin.scalacompat.DeterministicWallet.{ExtendedPrivateKey, KeyPath} import fr.acinq.bitcoin.scalacompat.{ByteVector32, OutPoint, Transaction, TxOut} +import fr.acinq.eclair.BlockHeight import fr.acinq.eclair.channel._ import fr.acinq.eclair.crypto.ShaChain import fr.acinq.eclair.transactions.Transactions._ @@ -27,7 +28,6 @@ import fr.acinq.eclair.wire.internal.channel.version0.ChannelTypes0.{HtlcTxAndSi import fr.acinq.eclair.wire.protocol.CommonCodecs._ import fr.acinq.eclair.wire.protocol.LightningMessageCodecs._ import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{BlockHeight, Features, InitFeature} import scodec.bits.ByteVector import scodec.codecs._ import scodec.{Attempt, Codec} @@ -55,7 +55,7 @@ private[channel] object ChannelCodecs1 { ("channelPath" | keyPathCodec) :: ("dustLimit" | satoshi) :: ("maxHtlcValueInFlightMsat" | uint64) :: - ("channelReserve" | satoshi) :: + ("channelReserve" | conditional(included = true, satoshi)) :: ("htlcMinimum" | millisatoshi) :: ("toSelfDelay" | cltvExpiryDelta) :: ("maxAcceptedHtlcs" | uint16) :: @@ -68,7 +68,7 @@ private[channel] object ChannelCodecs1 { ("nodeId" | publicKey) :: ("dustLimit" | satoshi) :: ("maxHtlcValueInFlightMsat" | uint64) :: - ("channelReserve" | satoshi) :: + ("channelReserve" | conditional(included = true, satoshi)) :: ("htlcMinimum" | millisatoshi) :: ("toSelfDelay" | cltvExpiryDelta) :: ("maxAcceptedHtlcs" | uint16) :: diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version2/ChannelCodecs2.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version2/ChannelCodecs2.scala index 0dab10a8fc..4001e6e3bb 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version2/ChannelCodecs2.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version2/ChannelCodecs2.scala @@ -18,6 +18,7 @@ package fr.acinq.eclair.wire.internal.channel.version2 import fr.acinq.bitcoin.scalacompat.DeterministicWallet.{ExtendedPrivateKey, KeyPath} import fr.acinq.bitcoin.scalacompat.{OutPoint, Transaction, TxOut} +import fr.acinq.eclair.BlockHeight import fr.acinq.eclair.channel._ import fr.acinq.eclair.crypto.ShaChain import fr.acinq.eclair.transactions.Transactions._ @@ -27,7 +28,6 @@ import fr.acinq.eclair.wire.internal.channel.version0.ChannelTypes0.{HtlcTxAndSi import fr.acinq.eclair.wire.protocol.CommonCodecs._ import fr.acinq.eclair.wire.protocol.LightningMessageCodecs._ import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{BlockHeight, Features, InitFeature} import scodec.bits.ByteVector import scodec.codecs._ import scodec.{Attempt, Codec} @@ -55,7 +55,7 @@ private[channel] object ChannelCodecs2 { ("channelPath" | keyPathCodec) :: ("dustLimit" | satoshi) :: ("maxHtlcValueInFlightMsat" | uint64) :: - ("channelReserve" | satoshi) :: + ("channelReserve" | conditional(included = true, satoshi)) :: ("htlcMinimum" | millisatoshi) :: ("toSelfDelay" | cltvExpiryDelta) :: ("maxAcceptedHtlcs" | uint16) :: @@ -68,7 +68,7 @@ private[channel] object ChannelCodecs2 { ("nodeId" | publicKey) :: ("dustLimit" | satoshi) :: ("maxHtlcValueInFlightMsat" | uint64) :: - ("channelReserve" | satoshi) :: + ("channelReserve" | conditional(included = true, satoshi)) :: ("htlcMinimum" | millisatoshi) :: ("toSelfDelay" | cltvExpiryDelta) :: ("maxAcceptedHtlcs" | uint16) :: diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3.scala index 423e172fef..2f18d60117 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3.scala @@ -17,7 +17,7 @@ package fr.acinq.eclair.wire.internal.channel.version3 import fr.acinq.bitcoin.scalacompat.DeterministicWallet.{ExtendedPrivateKey, KeyPath} -import fr.acinq.bitcoin.scalacompat.{ByteVector32, OutPoint, Transaction, TxOut} +import fr.acinq.bitcoin.scalacompat.{OutPoint, Satoshi, Transaction, TxOut} import fr.acinq.eclair.channel._ import fr.acinq.eclair.crypto.ShaChain import fr.acinq.eclair.transactions.Transactions._ @@ -25,7 +25,7 @@ import fr.acinq.eclair.transactions.{CommitmentSpec, DirectedHtlc, IncomingHtlc, import fr.acinq.eclair.wire.protocol.CommonCodecs._ import fr.acinq.eclair.wire.protocol.LightningMessageCodecs._ import fr.acinq.eclair.wire.protocol.UpdateMessage -import fr.acinq.eclair.{BlockHeight, FeatureSupport, Features, InitFeature} +import fr.acinq.eclair.{BlockHeight, FeatureSupport, Features} import scodec.bits.{BitVector, ByteVector} import scodec.codecs._ import scodec.{Attempt, Codec} @@ -75,7 +75,7 @@ private[channel] object ChannelCodecs3 { ("channelPath" | keyPathCodec) :: ("dustLimit" | satoshi) :: ("maxHtlcValueInFlightMsat" | uint64) :: - ("channelReserve" | satoshi) :: + ("channelReserve" | conditional(!channelFeatures.hasFeature(Features.DualFunding), satoshi)) :: ("htlcMinimum" | millisatoshi) :: ("toSelfDelay" | cltvExpiryDelta) :: ("maxAcceptedHtlcs" | uint16) :: @@ -84,11 +84,11 @@ private[channel] object ChannelCodecs3 { ("walletStaticPaymentBasepoint" | optional(provide(channelFeatures.paysDirectlyToWallet), publicKey)) :: ("features" | combinedFeaturesCodec)).as[LocalParams] - val remoteParamsCodec: Codec[RemoteParams] = ( + def remoteParamsCodec(channelFeatures: ChannelFeatures): Codec[RemoteParams] = ( ("nodeId" | publicKey) :: ("dustLimit" | satoshi) :: ("maxHtlcValueInFlightMsat" | uint64) :: - ("channelReserve" | satoshi) :: + ("channelReserve" | conditional(!channelFeatures.hasFeature(Features.DualFunding), satoshi)) :: ("htlcMinimum" | millisatoshi) :: ("toSelfDelay" | cltvExpiryDelta) :: ("maxAcceptedHtlcs" | uint16) :: @@ -269,7 +269,7 @@ private[channel] object ChannelCodecs3 { ("channelConfig" | channelConfigCodec) :: (("channelFeatures" | channelFeaturesCodec) >>:~ { channelFeatures => ("localParams" | localParamsCodec(channelFeatures)) :: - ("remoteParams" | remoteParamsCodec) :: + ("remoteParams" | remoteParamsCodec(channelFeatures)) :: ("channelFlags" | channelflags) :: ("localCommit" | localCommitCodec) :: ("remoteCommit" | remoteCommitCodec) :: diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala b/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala index ad38f3e92c..7add1eccac 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala @@ -208,7 +208,7 @@ object TestConstants { isInitiator = true, fundingSatoshis ).copy( - channelReserve = 10000 sat // Bob will need to keep that much satoshis as direct payment + requestedChannelReserve_opt = Some(10_000 sat) // Bob will need to keep that much satoshis in his balance ) } @@ -346,7 +346,7 @@ object TestConstants { isInitiator = false, fundingSatoshis ).copy( - channelReserve = 20000 sat // Alice will need to keep that much satoshis as direct payment + requestedChannelReserve_opt = Some(20_000 sat) // Alice will need to keep that much satoshis in her balance ) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/CommitmentsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/CommitmentsSpec.scala index a845529826..1673db2ebe 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/CommitmentsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/CommitmentsSpec.scala @@ -480,8 +480,8 @@ class CommitmentsSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with object CommitmentsSpec { def makeCommitments(toLocal: MilliSatoshi, toRemote: MilliSatoshi, feeRatePerKw: FeeratePerKw = FeeratePerKw(0 sat), dustLimit: Satoshi = 0 sat, isInitiator: Boolean = true, announceChannel: Boolean = true): Commitments = { - val localParams = LocalParams(randomKey().publicKey, DeterministicWallet.KeyPath(Seq(42L)), dustLimit, UInt64.MaxValue, 0 sat, 1 msat, CltvExpiryDelta(144), 50, isInitiator, ByteVector.empty, None, Features.empty) - val remoteParams = RemoteParams(randomKey().publicKey, dustLimit, UInt64.MaxValue, 0 sat, 1 msat, CltvExpiryDelta(144), 50, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, Features.empty, None) + val localParams = LocalParams(randomKey().publicKey, DeterministicWallet.KeyPath(Seq(42L)), dustLimit, UInt64.MaxValue, None, 1 msat, CltvExpiryDelta(144), 50, isInitiator, ByteVector.empty, None, Features.empty) + val remoteParams = RemoteParams(randomKey().publicKey, dustLimit, UInt64.MaxValue, None, 1 msat, CltvExpiryDelta(144), 50, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, Features.empty, None) val commitmentInput = Funding.makeFundingInputInfo(randomBytes32(), 0, (toLocal + toRemote).truncateToSatoshi, randomKey().publicKey, remoteParams.fundingPubKey) Commitments( channelId = randomBytes32(), @@ -503,8 +503,8 @@ object CommitmentsSpec { } def makeCommitments(toLocal: MilliSatoshi, toRemote: MilliSatoshi, localNodeId: PublicKey, remoteNodeId: PublicKey, announceChannel: Boolean): Commitments = { - val localParams = LocalParams(localNodeId, DeterministicWallet.KeyPath(Seq(42L)), 0 sat, UInt64.MaxValue, 0 sat, 1 msat, CltvExpiryDelta(144), 50, isInitiator = true, ByteVector.empty, None, Features.empty) - val remoteParams = RemoteParams(remoteNodeId, 0 sat, UInt64.MaxValue, 0 sat, 1 msat, CltvExpiryDelta(144), 50, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, Features.empty, None) + val localParams = LocalParams(localNodeId, DeterministicWallet.KeyPath(Seq(42L)), 0 sat, UInt64.MaxValue, None, 1 msat, CltvExpiryDelta(144), 50, isInitiator = true, ByteVector.empty, None, Features.empty) + val remoteParams = RemoteParams(remoteNodeId, 0 sat, UInt64.MaxValue, None, 1 msat, CltvExpiryDelta(144), 50, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, Features.empty, None) val commitmentInput = Funding.makeFundingInputInfo(randomBytes32(), 0, (toLocal + toRemote).truncateToSatoshi, randomKey().publicKey, remoteParams.fundingPubKey) Commitments( channelId = randomBytes32(), diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala index 663d639939..d011406706 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala @@ -200,7 +200,7 @@ trait ChannelStateTestsHelperMethods extends TestKitBase { val channelConfig = ChannelConfig.standard val (aliceParams, bobParams, channelType) = computeFeatures(setup, tags) val channelFlags = ChannelFlags(announceChannel = tags.contains(ChannelStateTestsTags.ChannelsPublic)) - val initialFeeratePerKw = if (tags.contains(ChannelStateTestsTags.AnchorOutputs) || tags.contains(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) TestConstants.anchorOutputsFeeratePerKw else TestConstants.feeratePerKw + val commitTxFeerate = if (tags.contains(ChannelStateTestsTags.AnchorOutputs) || tags.contains(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) TestConstants.anchorOutputsFeeratePerKw else TestConstants.feeratePerKw val (fundingSatoshis, pushMsat) = if (tags.contains(ChannelStateTestsTags.NoPushMsat)) { (TestConstants.fundingSatoshis, 0.msat) } else { @@ -209,7 +209,7 @@ trait ChannelStateTestsHelperMethods extends TestKitBase { val aliceInit = Init(aliceParams.initFeatures) val bobInit = Init(bobParams.initFeatures) - alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, fundingSatoshis, pushMsat, initialFeeratePerKw, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType) + alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, fundingSatoshis, pushMsat, commitTxFeerate, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType) assert(alice2blockchain.expectMsgType[TxPublisher.SetChannelId].channelId === ByteVector32.Zeroes) bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType) assert(bob2blockchain.expectMsgType[TxPublisher.SetChannelId].channelId === ByteVector32.Zeroes) @@ -241,7 +241,7 @@ trait ChannelStateTestsHelperMethods extends TestKitBase { bob2blockchain.expectMsgType[WatchFundingDeeplyBuried] awaitCond(alice.stateName == NORMAL) awaitCond(bob.stateName == NORMAL) - assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.availableBalanceForSend == (pushMsat - aliceParams.channelReserve).max(0 msat)) + assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.availableBalanceForSend == (pushMsat - aliceParams.requestedChannelReserve_opt.getOrElse(0 sat)).max(0 msat)) // x2 because alice and bob share the same relayer channelUpdateListener.expectMsgType[LocalChannelUpdate] channelUpdateListener.expectMsgType[LocalChannelUpdate] diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForAcceptChannelStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForAcceptChannelStateSpec.scala index 541cfd753a..11f6dc52ba 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForAcceptChannelStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForAcceptChannelStateSpec.scala @@ -58,12 +58,12 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS val channelConfig = ChannelConfig.standard val (aliceParams, bobParams, defaultChannelType) = computeFeatures(setup, test.tags) val channelType = if (test.tags.contains("standard-channel-type")) ChannelTypes.Standard else defaultChannelType - val initialFeeratePerKw = if (channelType == ChannelTypes.AnchorOutputs || channelType == ChannelTypes.AnchorOutputsZeroFeeHtlcTx) TestConstants.anchorOutputsFeeratePerKw else TestConstants.feeratePerKw + val commitTxFeerate = if (channelType == ChannelTypes.AnchorOutputs || channelType == ChannelTypes.AnchorOutputsZeroFeeHtlcTx) TestConstants.anchorOutputsFeeratePerKw else TestConstants.feeratePerKw val aliceInit = Init(aliceParams.initFeatures) val bobInit = Init(bobParams.initFeatures) within(30 seconds) { val fundingAmount = if (test.tags.contains(ChannelStateTestsTags.Wumbo)) Btc(5).toSatoshi else TestConstants.fundingSatoshis - alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, fundingAmount, TestConstants.pushMsat, initialFeeratePerKw, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, ChannelFlags.Private, channelConfig, channelType) + alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, fundingAmount, TestConstants.pushMsat, commitTxFeerate, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, ChannelFlags.Private, channelConfig, channelType) bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType) alice2bob.expectMsgType[OpenChannel] alice2bob.forward(bob) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForOpenChannelStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForOpenChannelStateSpec.scala index 0291fbfc34..84ea97e2ce 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForOpenChannelStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForOpenChannelStateSpec.scala @@ -52,11 +52,11 @@ class WaitForOpenChannelStateSpec extends TestKitBaseClass with FixtureAnyFunSui val channelConfig = ChannelConfig.standard val (aliceParams, bobParams, defaultChannelType) = computeFeatures(setup, test.tags) val channelType = if (test.tags.contains("standard-channel-type")) ChannelTypes.Standard else defaultChannelType - val initialFeeratePerKw = if (channelType == ChannelTypes.AnchorOutputs || channelType == ChannelTypes.AnchorOutputsZeroFeeHtlcTx) TestConstants.anchorOutputsFeeratePerKw else TestConstants.feeratePerKw + val commitTxFeerate = if (channelType == ChannelTypes.AnchorOutputs || channelType == ChannelTypes.AnchorOutputsZeroFeeHtlcTx) TestConstants.anchorOutputsFeeratePerKw else TestConstants.feeratePerKw val aliceInit = Init(aliceParams.initFeatures) val bobInit = Init(bobParams.initFeatures) within(30 seconds) { - alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, initialFeeratePerKw, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, ChannelFlags.Private, channelConfig, channelType) + alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, commitTxFeerate, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, ChannelFlags.Private, channelConfig, channelType) bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType) awaitCond(bob.stateName == WAIT_FOR_OPEN_CHANNEL) withFixture(test.toNoArgTest(FixtureParam(alice, bob, alice2bob, bob2alice, bob2blockchain))) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingCreatedStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingCreatedStateSpec.scala index 21ec61710e..3c8232860c 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingCreatedStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingCreatedStateSpec.scala @@ -106,7 +106,7 @@ class WaitForFundingCreatedStateSpec extends TestKitBaseClass with FixtureAnyFun import f._ val fees = Transactions.weight2fee(TestConstants.feeratePerKw, Transactions.DefaultCommitmentFormat.commitWeight) val bobParams = bob.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CREATED].localParams - val reserve = bobParams.channelReserve + val reserve = bobParams.requestedChannelReserve_opt.get val missing = 100.sat - fees - reserve val fundingCreated = alice2bob.expectMsgType[FundingCreated] alice2bob.forward(bob) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala index 75d30245ac..d0bc69f39c 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala @@ -257,7 +257,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // but this one will dip alice below her reserve: we must wait for the previous HTLCs to settle before sending any more val failedAdd = CMD_ADD_HTLC(sender.ref, 11000000 msat, randomBytes32(), CltvExpiry(400144), TestConstants.emptyOnionPacket, localOrigin(sender.ref)) bob ! failedAdd - val error = RemoteCannotAffordFeesForNewHtlc(channelId(bob), failedAdd.amount, missing = 1360 sat, 10000 sat, 22720 sat) + val error = RemoteCannotAffordFeesForNewHtlc(channelId(bob), failedAdd.amount, missing = 1360 sat, 20000 sat, 22720 sat) sender.expectMsg(RES_ADD_FAILED(failedAdd, error, Some(bob.stateData.asInstanceOf[DATA_NORMAL].channelUpdate))) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala index 8f998f0a34..f5387b4c77 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala @@ -416,8 +416,8 @@ class PeerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Paralle val init = channel.expectMsgType[INPUT_INIT_FUNDER] assert(init.channelType === ChannelTypes.AnchorOutputs) assert(init.fundingAmount === 15000.sat) - assert(init.initialFeeratePerKw === TestConstants.anchorOutputsFeeratePerKw) - assert(init.fundingTxFeeratePerKw === feeEstimator.getFeeratePerKw(nodeParams.onChainFeeConf.feeTargets.fundingBlockTarget)) + assert(init.commitTxFeerate === TestConstants.anchorOutputsFeeratePerKw) + assert(init.fundingTxFeerate === feeEstimator.getFeeratePerKw(nodeParams.onChainFeeConf.feeTargets.fundingBlockTarget)) } test("use correct on-chain fee rates when spawning a channel (anchor outputs zero fee htlc)", Tag("anchor_outputs_zero_fee_htlc_tx")) { f => @@ -434,8 +434,8 @@ class PeerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Paralle val init = channel.expectMsgType[INPUT_INIT_FUNDER] assert(init.channelType === ChannelTypes.AnchorOutputsZeroFeeHtlcTx) assert(init.fundingAmount === 15000.sat) - assert(init.initialFeeratePerKw === TestConstants.anchorOutputsFeeratePerKw) - assert(init.fundingTxFeeratePerKw === feeEstimator.getFeeratePerKw(nodeParams.onChainFeeConf.feeTargets.fundingBlockTarget)) + assert(init.commitTxFeerate === TestConstants.anchorOutputsFeeratePerKw) + assert(init.fundingTxFeerate === feeEstimator.getFeeratePerKw(nodeParams.onChainFeeConf.feeTargets.fundingBlockTarget)) } test("use correct final script if option_static_remotekey is negotiated", Tag("static_remotekey")) { f => diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentPacketSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentPacketSpec.scala index 2e041a6d84..3193df2d3e 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentPacketSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentPacketSpec.scala @@ -369,8 +369,8 @@ object PaymentPacketSpec { } def makeCommitments(channelId: ByteVector32, testAvailableBalanceForSend: MilliSatoshi = 50000000 msat, testAvailableBalanceForReceive: MilliSatoshi = 50000000 msat, testCapacity: Satoshi = 100000 sat): Commitments = { - val params = LocalParams(null, null, null, null, null, null, null, 0, isInitiator = true, null, None, null) - val remoteParams = RemoteParams(randomKey().publicKey, null, null, null, null, null, maxAcceptedHtlcs = 0, null, null, null, null, null, null, None) + val params = LocalParams(null, null, null, null, None, null, null, 0, isInitiator = true, null, None, null) + val remoteParams = RemoteParams(randomKey().publicKey, null, null, None, null, null, maxAcceptedHtlcs = 0, null, null, null, null, null, null, None) val commitInput = InputInfo(OutPoint(randomBytes32(), 1), TxOut(testCapacity, Nil), Nil) val channelFlags = ChannelFlags.Private new Commitments(channelId, ChannelConfig.standard, ChannelFeatures(), params, remoteParams, channelFlags, null, null, null, null, 0, 0, Map.empty, null, commitInput, null) { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/ChannelCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/ChannelCodecsSpec.scala index 4ea55a1aaa..1112ea9903 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/ChannelCodecsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/ChannelCodecsSpec.scala @@ -135,11 +135,11 @@ class ChannelCodecsSpec extends AnyFunSuite { // this test makes sure that we actually produce the same objects than previous versions of eclair val refs = Map( hex"00000303933884AAF1D6B108397E5EFE5C86BCF2D8CA8D2F700EDA99DB9214FC2712B134000456E4167E3C0EB8C856C79CA31C97C0AA0000000000000222000000012A05F2000000000000028F5C000000000000000102D0001E000BD48A2402E80B723C42EE3E42938866EC6686ABB7ABF64380000000C501A7F2974C5074E9E10DBB3F0D9B8C40932EC63ABC610FAD7EB6B21C6D081A459B000000000000011E80000001EEFFFE5C00000000000147AE00000000000001F403F000F18146779F781067ED04B4957E14F1C5623AB653039B2B1D49910240848E4E682DB20131AD64F76FAF90CD7DE26892F1BDAB82FB9E02EF6538D82FF4204B5348F02AE081A5388E9474769D69C4F60A763AE0CCDB5228A06281DE64408871A927297FDFD8818B6383985ABD4F0AC22E73791CF3A4D63C592FA2648242D34B8334B1539E823381BB1F1404C37D9C2318F5FC6B1BF7ECF5E6835B779E3BE09BADCF6DF1F51DCFBC80000000C0808000000000000EFD80000000007F00000000061A0A4880000001EDE5F3C3801203B9C79160B5123CADE575E370480B57B0C816EB35DD7B27372EDA858622EB1E808000000015FFFFFF800000000011001029DFB814F6502A68D6F83B6049E3D2948A2080084083750626532FDB437169C20023A9108146779F781067ED04B4957E14F1C5623AB653039B2B1D49910240848E4E682DB21081B30694071254D8B3B9537320C014B8CB1052E5514F5EFC19CF2EB806308D5CF1A95700AD0100000000008083B9C79160B5123CADE575E370480B57B0C816EB35DD7B27372EDA858622EB1E80800000001961B4C001618F8180000000001100102E648BA30998A28C02C2DFD9DDCD0E0BA064DA199C55186485AFAB296B94E704426FFE00000000000B000A67D9B9FAADB91650E0146B1F742E5C16006708890200239822011026A6925C659D006FEB42D639F1E42DD13224EE49AA34E71B612CF96DB66A8CD4011032C22F653C54CC5E41098227427650644266D80DED45B7387AE0FFC10E529C4680A418228110807CB47D9C1A14CB832FB361C398EA672C9542F34A90BAD4288FA6AC5FC9E9845C01101CF71CAE9252D389135D8C606225DCF1E0333CCDF1FAE84B74FC5D3D440C25F880A3A9108146779F781067ED04B4957E14F1C5623AB653039B2B1D49910240848E4E682DB21081B30694071254D8B3B9537320C014B8CB1052E5514F5EFC19CF2EB806308D5CF1A9573D7C531000000000000000000F3180000000007F00000001EDE5F3C380000000061A0A48D64CA627B243AD5915A2E5D0BAD026762028DDF3304992B83A26D6C11735FC5F01ED56D769BDE7F6A068AF1A4BCFDF950321F3A4744B01B1DDC7498677F112AE1A80000000000000000000000000000000000000658000000000000819800040D37301C10C9419287E9A3B704EB6D7F45CC145DD77DCE8A63B0A47C8AB67467D800901DCE3C8B05A891E56F2BAF1B82405ABD8640B759AEEBD939B976D42C311758F40400000000AFFFFFFC00000000008800814EFDC0A7B2815346B7C1DB024F1E94A451040042041BA83132997EDA1B8B4E10011D48840A33BCFBC0833F6825A4ABF0A78E2B11D5B2981CD958EA4C881204247273416D90840D9834A03892A6C59DCA9B990600A5C65882972A8A7AF7E0CE7975C031846AE78D4AB8002000EC0003FFFFFFFF86801076D98A575A4CDFD0E3F44D1BB3CD3BBAF3BD04C38FED439ED90D88DF932A9296801A80007FFFFFFFF4008136A9D5896669E8724C5120FB6B36C241EF3CEF68AE0316161F04A9EE3EAFF36000FC0003FFFFFFFF86780106E4B5CC4155733A2427082907338051A5DA1E7CA6432840A5528ECAFFA3FB628801B80007FFFFFFFF10020CA4E125E9126107745D4354D4187ABCDE323117857A1DCEB7CCF60B2AAFA80C6003A0000FFFFFFFFE1C0080981575FD981A73A848CC0243CB467BF451F6811DAF4D71CAD8CE8B1E96DB190C01000003FFFFFFFF867400814C747E0FD8290BE8A3B8B3F73015A261479A71780CD3A0A9270234E4B394409C00D80003FFFFFFFF90020E1B9C9B10A97F15F5E1BB27FC8AC670DF8DADEAE4EDFAFB23BDD0AC705FDF51600340000FFFFFFFFF0020AD2581F3494A17B0BE3F63516D53F028A204FD3156D8B21AA4E57A8738D2062080007FFFFFFFF0CE83B9C79160B5123CADE575E370480B57B0C816EB35DD7B27372EDA858622EB1E0B8C1E00000B8000FA46CC2C7E9AB4A37C64216CD65C944E6D73998419D1A1AD2827AB6BC85B32280230764E374064EC82A3751E789607E23BEAE93FB0EDDD5E7FA803767079662E80EAEF384E2AFCB68049D9DC246119E77BD2ED4112330760CAB6CD3671CFCE006C584B9C95E0B554261E00154D40806EA694F44751B328A9291BAD124EFD5664280936EC92D27B242737E7E3E83B4704BA367B7DA5108F2F6EDFB1C38EE721A369E77EED71B12090BAEAAAC322C1457E31AB0C4DE5D9351943F10FD747742616A1AABD09F680B37D4105A8872695EE9B97FAB8985FAA9D747D45046229BF265CEEB300A40FE23040C5F335E0515496C58EE47418B72331FCC6F47A31A9B33B8E000008692FFAFF04D2AE211E9461FB39D875D74F32E4109D21D5A03D46612000000002E307800002E0002069FCA5D3141D3A78436ECFC366E31024CBB18EAF1843EB5FADAC871B42069166C0726710955E3AD621072FCBDFCB90D79E5B1951A5EE01DB533B72429F84E2562680519DE7DE0419FB412D255F853C71588EAD94C0E6CAC7526440902123939A0B6C806CC1A501C495362CEE54DCC830052E32C414B95453D7BF0673CBAE018C23573C69C694A8F88483050257A7366B838489731E5776B6FA0F02573401176D3E7FAEEF11E95A671420586631255F51A0EC2CF4D4D9F69D587712070FE1FB9316B71868692FFAFF04D2AE211E9461FB39D875D74F32E4109D21D5A03D46612000000002E307800002E0002BA11BBBA0202012000000000000007D0000007D0000000C800000007CFFFF83000" - -> """{"type":"DATA_NORMAL","commitments":{"channelId":"07738f22c16a24795bcaebc6e09016af61902dd66bbaf64e6e5db50b0c45d63c","channelConfig":[],"channelFeatures":[],"localParams":{"nodeId":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","fundingKeyPath":{"path":[1457788542,1007597768,1455922339,479707306]},"dustLimit":546,"maxHtlcValueInFlightMsat":5000000000,"channelReserve":167772,"htlcMinimum":1,"toSelfDelay":720,"maxAcceptedHtlcs":30,"isInitiator":false,"defaultFinalScriptPubKey":"a9144805d016e47885dc7c852710cdd8cd0d576f57ec87","initFeatures":{"activated":{"option_data_loss_protect":"optional","initial_routing_sync":"optional","gossip_queries":"optional"},"unknown":[]}},"remoteParams":{"nodeId":"034fe52e98a0e9d3c21b767e1b371881265d8c7578c21f5afd6d6438da10348b36","dustLimit":573,"maxHtlcValueInFlightMsat":16609443000,"channelReserve":167772,"htlcMinimum":1000,"toSelfDelay":2016,"maxAcceptedHtlcs":483,"fundingPubKey":"028cef3ef020cfda09692afc29e38ac4756ca60736563a93220481091c9cd05b64","revocationBasepoint":"02635ac9eedf5f219afbc4d125e37b5705f73c05deca71b05fe84096a691e055c1","paymentBasepoint":"034a711d28e8ed3ad389ec14ec75c199b6a45140c503bcc88110e3524e52ffbfb1","delayedPaymentBasepoint":"0316c70730b57a9e15845ce6f239e749ac78b25f44c90485a697066962a73d0467","htlcBasepoint":"03763e280986fb384631ebf8d637efd9ebcd06b6ef3c77c1375b9edbe3ea3b9f79","initFeatures":{"activated":{"option_data_loss_protect":"mandatory","gossip_queries":"optional"},"unknown":[]}},"channelFlags":{"announceChannel":true},"localCommit":{"index":7675,"spec":{"htlcs":[],"commitTxFeerate":254,"toLocal":204739729,"toRemote":16572475271},"commitTxAndRemoteSig":{"commitTx":{"txid":"e25a866b79212015e01e155e530fb547abc8276869f8740a9948e52ca231f1e4","tx":"020000000107738f22c16a24795bcaebc6e09016af61902dd66bbaf64e6e5db50b0c45d63d010000000032c3698002c31f0300000000002200205cc91746133145180585bfb3bb9a1c1740c9b43338aa30c90b5f5652d729ce0884dffc0000000000160014cfb373f55b722ca1c028d63ee85cb82c00ce11127af8a620"},"remoteSig":"4d4d24b8cb3a00dfd685ac73e3c85ba26449dc935469ce36c259f2db6cd519a865845eca78a998bc8213044e84eca0c884cdb01bda8b6e70f5c1ff821ca5388d"},"htlcTxsAndRemoteSigs":[]},"remoteCommit":{"index":7779,"spec":{"htlcs":[],"commitTxFeerate":254,"toLocal":16572475271,"toRemote":204739729},"txid":"ac994c4f64875ab22b45cba175a04cec4051bbe660932570744dad822e6bf8be","remotePerCommitmentPoint":"03daadaed37bcfed40d15e34979fbf2a0643e748e8960363bb8e930cefe2255c35"},"localChanges":{"proposed":[],"signed":[],"acked":[]},"remoteChanges":{"proposed":[],"acked":[],"signed":[]},"localNextHtlcId":203,"remoteNextHtlcId":4147,"originChannels":{},"remoteNextCommitInfo":"034dcc0704325064a1fa68edc13adb5fd173051775df73a298ec291f22ad9d19f6","commitInput":{"outPoint":"3dd6450c0bb55d6e4ef6ba6bd62d9061af1690e0c6ebca5b79246ac1228f7307:1","amountSatoshis":16777215},"remotePerCommitmentSecrets":null},"shortChannelId":"1513532x23x1","buried":true,"channelAnnouncement":{"nodeSignature1":"d2366163f4d5a51be3210b66b2e4a2736b9ccc20ce8d0d69413d5b5e42d991401183b271ba032764151ba8f3c4b03f11df5749fd876eeaf3fd401bb383cb3174","nodeSignature2":"075779c27157e5b4024ecee12308cf3bde976a0891983b0655b669b38e7e700362c25ce4af05aaa130f000aa6a04037534a7a23a8d99454948dd689277eab321","bitcoinSignature1":"4049b7649693d92139bf3f1f41da3825d1b3dbed2884797b76fd8e1c77390d1b4f3bf76b8d890485d7555619160a2bf18d58626f2ec9a8ca1f887eba3ba130b5","bitcoinSignature2":"0d55e84fb4059bea082d443934af74dcbfd5c4c2fd54eba3ea2823114df932e7759805207f1182062f99af028aa4b62c7723a0c5b9198fe637a3d18d4d99dc70","features":{"activated":{},"unknown":[]},"chainHash":"43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000","shortChannelId":"1513532x23x1","nodeId1":"034fe52e98a0e9d3c21b767e1b371881265d8c7578c21f5afd6d6438da10348b36","nodeId2":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","bitcoinKey1":"028cef3ef020cfda09692afc29e38ac4756ca60736563a93220481091c9cd05b64","bitcoinKey2":"03660d280e24a9b16772a6e6418029719620a5caa29ebdf8339e5d700c611ab9e3","tlvStream":{"records":[],"unknown":[]}},"channelUpdate":{"signature":"4e34a547c424182812bd39b35c1c244b98f2bbb5b7d07812b9a008bb69f3fd77788f4ad338a102c331892afa8d076167a6a6cfb4eac3b890387f0fdc98b5b8c3","chainHash":"43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000","shortChannelId":"1513532x23x1","timestamp":{"iso":"2019-06-18T12:49:33Z","unix":1560862173},"channelFlags":{"isEnabled":true,"isNode1":false},"cltvExpiryDelta":144,"htlcMinimumMsat":1000,"feeBaseMsat":1000,"feeProportionalMillionths":100,"htlcMaximumMsat":16777215000,"tlvStream":{"records":[],"unknown":[]}}}""", + -> """{"type":"DATA_NORMAL","commitments":{"channelId":"07738f22c16a24795bcaebc6e09016af61902dd66bbaf64e6e5db50b0c45d63c","channelConfig":[],"channelFeatures":[],"localParams":{"nodeId":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","fundingKeyPath":{"path":[1457788542,1007597768,1455922339,479707306]},"dustLimit":546,"maxHtlcValueInFlightMsat":5000000000,"requestedChannelReserve_opt":167772,"htlcMinimum":1,"toSelfDelay":720,"maxAcceptedHtlcs":30,"isInitiator":false,"defaultFinalScriptPubKey":"a9144805d016e47885dc7c852710cdd8cd0d576f57ec87","initFeatures":{"activated":{"option_data_loss_protect":"optional","initial_routing_sync":"optional","gossip_queries":"optional"},"unknown":[]}},"remoteParams":{"nodeId":"034fe52e98a0e9d3c21b767e1b371881265d8c7578c21f5afd6d6438da10348b36","dustLimit":573,"maxHtlcValueInFlightMsat":16609443000,"requestedChannelReserve_opt":167772,"htlcMinimum":1000,"toSelfDelay":2016,"maxAcceptedHtlcs":483,"fundingPubKey":"028cef3ef020cfda09692afc29e38ac4756ca60736563a93220481091c9cd05b64","revocationBasepoint":"02635ac9eedf5f219afbc4d125e37b5705f73c05deca71b05fe84096a691e055c1","paymentBasepoint":"034a711d28e8ed3ad389ec14ec75c199b6a45140c503bcc88110e3524e52ffbfb1","delayedPaymentBasepoint":"0316c70730b57a9e15845ce6f239e749ac78b25f44c90485a697066962a73d0467","htlcBasepoint":"03763e280986fb384631ebf8d637efd9ebcd06b6ef3c77c1375b9edbe3ea3b9f79","initFeatures":{"activated":{"option_data_loss_protect":"mandatory","gossip_queries":"optional"},"unknown":[]}},"channelFlags":{"announceChannel":true},"localCommit":{"index":7675,"spec":{"htlcs":[],"commitTxFeerate":254,"toLocal":204739729,"toRemote":16572475271},"commitTxAndRemoteSig":{"commitTx":{"txid":"e25a866b79212015e01e155e530fb547abc8276869f8740a9948e52ca231f1e4","tx":"020000000107738f22c16a24795bcaebc6e09016af61902dd66bbaf64e6e5db50b0c45d63d010000000032c3698002c31f0300000000002200205cc91746133145180585bfb3bb9a1c1740c9b43338aa30c90b5f5652d729ce0884dffc0000000000160014cfb373f55b722ca1c028d63ee85cb82c00ce11127af8a620"},"remoteSig":"4d4d24b8cb3a00dfd685ac73e3c85ba26449dc935469ce36c259f2db6cd519a865845eca78a998bc8213044e84eca0c884cdb01bda8b6e70f5c1ff821ca5388d"},"htlcTxsAndRemoteSigs":[]},"remoteCommit":{"index":7779,"spec":{"htlcs":[],"commitTxFeerate":254,"toLocal":16572475271,"toRemote":204739729},"txid":"ac994c4f64875ab22b45cba175a04cec4051bbe660932570744dad822e6bf8be","remotePerCommitmentPoint":"03daadaed37bcfed40d15e34979fbf2a0643e748e8960363bb8e930cefe2255c35"},"localChanges":{"proposed":[],"signed":[],"acked":[]},"remoteChanges":{"proposed":[],"acked":[],"signed":[]},"localNextHtlcId":203,"remoteNextHtlcId":4147,"originChannels":{},"remoteNextCommitInfo":"034dcc0704325064a1fa68edc13adb5fd173051775df73a298ec291f22ad9d19f6","commitInput":{"outPoint":"3dd6450c0bb55d6e4ef6ba6bd62d9061af1690e0c6ebca5b79246ac1228f7307:1","amountSatoshis":16777215},"remotePerCommitmentSecrets":null},"shortChannelId":"1513532x23x1","buried":true,"channelAnnouncement":{"nodeSignature1":"d2366163f4d5a51be3210b66b2e4a2736b9ccc20ce8d0d69413d5b5e42d991401183b271ba032764151ba8f3c4b03f11df5749fd876eeaf3fd401bb383cb3174","nodeSignature2":"075779c27157e5b4024ecee12308cf3bde976a0891983b0655b669b38e7e700362c25ce4af05aaa130f000aa6a04037534a7a23a8d99454948dd689277eab321","bitcoinSignature1":"4049b7649693d92139bf3f1f41da3825d1b3dbed2884797b76fd8e1c77390d1b4f3bf76b8d890485d7555619160a2bf18d58626f2ec9a8ca1f887eba3ba130b5","bitcoinSignature2":"0d55e84fb4059bea082d443934af74dcbfd5c4c2fd54eba3ea2823114df932e7759805207f1182062f99af028aa4b62c7723a0c5b9198fe637a3d18d4d99dc70","features":{"activated":{},"unknown":[]},"chainHash":"43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000","shortChannelId":"1513532x23x1","nodeId1":"034fe52e98a0e9d3c21b767e1b371881265d8c7578c21f5afd6d6438da10348b36","nodeId2":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","bitcoinKey1":"028cef3ef020cfda09692afc29e38ac4756ca60736563a93220481091c9cd05b64","bitcoinKey2":"03660d280e24a9b16772a6e6418029719620a5caa29ebdf8339e5d700c611ab9e3","tlvStream":{"records":[],"unknown":[]}},"channelUpdate":{"signature":"4e34a547c424182812bd39b35c1c244b98f2bbb5b7d07812b9a008bb69f3fd77788f4ad338a102c331892afa8d076167a6a6cfb4eac3b890387f0fdc98b5b8c3","chainHash":"43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000","shortChannelId":"1513532x23x1","timestamp":{"iso":"2019-06-18T12:49:33Z","unix":1560862173},"channelFlags":{"isEnabled":true,"isNode1":false},"cltvExpiryDelta":144,"htlcMinimumMsat":1000,"feeBaseMsat":1000,"feeProportionalMillionths":100,"htlcMaximumMsat":16777215000,"tlvStream":{"records":[],"unknown":[]}}}""", hex"00000303933884AAF1D6B108397E5EFE5C86BCF2D8CA8D2F700EDA99DB9214FC2712B1340004D443ECE9D9C43A11A19B554BAAA6AD150000000000000222000000003B9ACA0000000000000249F000000000000000010090001E800BD48A22F4C80A42CC8BB29A764DBAEFC95674931FBE9A4380000000C50134D4A745996002F219B5FDBA1E045374DF589ECA06ABE23CECAE47343E65EDCF800000000000011E80000001BA90824000000000000124F800000000000001F4038500F1810AE1AF8A1D6F56F80855E26705F191BB07CD4E2434BC5BB1698E7E5880E2266201E8BFEEEEED725775B8116F6F82CF8E87835A5B45B184E56F272AD70D6078118601E06212B8C8F2E25B73EE7974FDCDF007E389B437BBFE238CCC3F3BF7121B6C5E81AA8589D21E9584B24A11F3ABBA5DAD48D121DD63C57A69CD767119C05DA159CB81A649D8CC0E136EB8DFBD2268B69DCA86F8CE4A604235A03D9D37AE7B07FC563F80000000C080800000000000271C000000000177000000002808B14600000001970039BA00123767F0F4F00D5E9FDF24177EF2872343D9F8FAEC65D3048BA575E70E00A0AB08800000000015E070F20000000000110010584241B5FB364208F6E64A80D1166DAD866186B10C015ED0283FF1C308C2105A0023A910810AE1AF8A1D6F56F80855E26705F191BB07CD4E2434BC5BB1698E7E5880E226621081DE8ADFA110DC8A94D8B9E9EF616BAE8598287C8F82AFDF0FC068697D570266FDA95700AD81000000000080B767F0F4F00D5E9FDF24177EF2872343D9F8FAEC65D3048BA575E70E00A0AB0880000000003E7AEDC0011ABE8A00000000001100101A9CE4B6AEF469590BC7BCC51DCEEAE9C86084055A63CC01E443C733FBE400B9B5B16800000000000B000A5E5700106D1A7097E4DE87EBAF1F8F2773842FA482002418228110805E84989A81F51ABD9D11889AE43E68FAD93659DEC019F1B8C0ADBF15A57B118B81101DCC1256F9306439AD3962C043FC47A5179CAAA001CCB23342BE0E8D92E4022780A4182281108074F306DA3751B84EC5FFB155BDCA7B8E02208BBDBC8D4F3327ABA557BF27CD1701102EF4AC8CC92F469DA9642D4D4162BC545F8B34ADE15B7D6F99808AA22B086B0180A3A910810AE1AF8A1D6F56F80855E26705F191BB07CD4E2434BC5BB1698E7E5880E226621081DE8ADFA110DC8A94D8B9E9EF616BAE8598287C8F82AFDF0FC068697D570266FDA9576F8099900000000000000000271C00000000017700000001970039BA000000002808B14648CE00AE97051EE10A3C361263F81A98165CE4AA7BA076933D4266E533585F24815C15DEACF0691332B38ECF23EC39982C5C978C748374A01BA9B30D501EE4F26E8000000000000000000000000000000000001224000000000000004B800040A911C460F1467952E3B99BED072F81BFB4454FF389636DCB399FE6A78113C28580091BB3F87A7806AF4FEF920BBF794391A1ECFC7D7632E98245D2BAF3870050558440000000000AF0387900000000000880082C2120DAFD9B21047B732540688B36D6C330C3588600AF68141FF8E18461082D0011D488408570D7C50EB7AB7C042AF13382F8C8DD83E6A7121A5E2DD8B4C73F2C407113310840EF456FD0886E454A6C5CF4F7B0B5D742CC143E47C157EF87E03434BEAB81337ED4AB8001C00F40003FFFFFFFEC7200403248A1D44DFA3AC9EC237D452C936400CAA86E9517CCCF2A8F77B7493CD70B6A00780001FFFFFFFF63A0041826829646B907A97FBD1455EA8673A12B8E7AA6EA790F7802E955CE3B69DE57E006E0001FFFFFFFF640081E51EB1F91218821E680B50E4B22DF8B094385BD33ACAE36BFC9E8C2F5AD2DA5400EC0003FFFFFFFEC7801047C26AD5435658D063EBCF73A5D0EEFE73ED6B73426246E8DFB3A21D1C4C7465001900007FFFFFFFE0040B115AC58BAAA900195893EA3B2AB408D2AD348AD047E3B6CB15E599625E38608006A0001FFFFFFFF7002033C39A21A38BB61F6FB33623771A9356D8885B7C12C939C770C939EF826286C200360000FFFFFFFFB4008104EF4271064A0973B053727C3E67352D00E25CAEED944F50782449CEAE8F50960001FFFFFFFF6390DD9FC3D3C0357A7F7C905DFBCA1C8D0F67E3EBB1974C122E95D79C380282AC222B21FA0007920001295AA1FB77029F7620A90EF7AE6A6CD31E4588B93264A7ADB76152D535C52E90B9E1B7C2376DABA316A6290F1A9730D4E5E44D0B1CB0EE6A795702E6A6BCDFCDA1A4BFEBFC134AB8847A5187ECE761D75D3CCB904274875680F51984800000000AC87E8001E480002E884D2A8080804800000000000001F4000001F40000003200000001BF08EB000" - -> """{"type":"DATA_NORMAL","commitments":{"channelId":"6ecfe1e9e01abd3fbe482efde50e4687b3f1f5d8cba609174aebce1c01415611","channelConfig":[],"channelFeatures":[],"localParams":{"nodeId":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","fundingKeyPath":{"path":[3561221353,3653515793,2711311691,2863050005]},"dustLimit":546,"maxHtlcValueInFlightMsat":1000000000,"channelReserve":150000,"htlcMinimum":1,"toSelfDelay":144,"maxAcceptedHtlcs":30,"isInitiator":true,"defaultFinalScriptPubKey":"a91445e990148599176534ec9b75df92ace9263f7d3487","initFeatures":{"activated":{"option_data_loss_protect":"optional","initial_routing_sync":"optional","gossip_queries":"optional"},"unknown":[]}},"remoteParams":{"nodeId":"0269a94e8b32c005e4336bfb743c08a6e9beb13d940d57c479d95c8e687ccbdb9f","dustLimit":573,"maxHtlcValueInFlightMsat":14850000000,"channelReserve":150000,"htlcMinimum":1000,"toSelfDelay":1802,"maxAcceptedHtlcs":483,"fundingPubKey":"0215c35f143adeadf010abc4ce0be323760f9a9c486978b762d31cfcb101c44cc4","revocationBasepoint":"03d17fdddddae4aeeb7022dedf059f1d0f06b4b68b6309cade4e55ae1ac0f0230c","paymentBasepoint":"03c0c4257191e5c4b6e7dcf2e9fb9be00fc713686f77fc4719987e77ee2436d8bd","delayedPaymentBasepoint":"03550b13a43d2b09649423e75774bb5a91a243bac78af4d39aece23380bb42b397","htlcBasepoint":"034c93b1981c26dd71bf7a44d16d3b950df19c94c0846b407b3a6f5cf60ff8ac7f","initFeatures":{"activated":{"option_data_loss_protect":"mandatory","gossip_queries":"optional"},"unknown":[]}},"channelFlags":{"announceChannel":true},"localCommit":{"index":20024,"spec":{"htlcs":[],"commitTxFeerate":750,"toLocal":1343316620,"toRemote":13656683380},"commitTxAndRemoteSig":{"commitTx":{"txid":"65fe0b1f079fa763448df3ab8d94b1ad7d377c061121376be90b9c0c1bb0cd43","tx":"02000000016ecfe1e9e01abd3fbe482efde50e4687b3f1f5d8cba609174aebce1c0141561100000000007cf5db8002357d1400000000002200203539c96d5de8d2b2178f798a3b9dd5d390c1080ab4c79803c8878e67f7c801736b62d00000000000160014bcae0020da34e12fc9bd0fd75e3f1e4ee7085f49df013320"},"remoteSig":"bd09313503ea357b3a231135c87cd1f5b26cb3bd8033e371815b7e2b4af623173b9824adf260c8735a72c58087f88f4a2f39554003996466857c1d1b25c8044f"},"htlcTxsAndRemoteSigs":[]},"remoteCommit":{"index":20024,"spec":{"htlcs":[],"commitTxFeerate":750,"toLocal":13656683380,"toRemote":1343316620},"txid":"919c015d2e0a3dc214786c24c7f035302cb9c954f740ed267a84cdca66b0be49","remotePerCommitmentPoint":"02b82bbd59e0d22665671d9e47d8733058b92f18e906e9403753661aa03dc9e4dd"},"localChanges":{"proposed":[],"signed":[],"acked":[]},"remoteChanges":{"proposed":[],"acked":[],"signed":[]},"localNextHtlcId":9288,"remoteNextHtlcId":151,"originChannels":{},"remoteNextCommitInfo":"02a4471183c519e54b8ee66fb41cbe06fed1153fce258db72ce67f9a9e044f0a16","commitInput":{"outPoint":"115641011cceeb4a1709a6cbd8f5f1b387460ee5fd2e48be3fbd1ae0e9e1cf6e:0","amountSatoshis":15000000},"remotePerCommitmentSecrets":null},"shortChannelId":"1413373x969x0","buried":true,"channelUpdate":{"signature":"52b543f6ee053eec41521def5cd4d9a63c8b117264c94f5b6ec2a5aa6b8a5d2173c36f846edb57462d4c521e352e61a9cbc89a163961dcd4f2ae05cd4d79bf9b","chainHash":"43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000","shortChannelId":"1413373x969x0","timestamp":{"iso":"2019-06-24T09:39:33Z","unix":1561369173},"channelFlags":{"isEnabled":true,"isNode1":false},"cltvExpiryDelta":144,"htlcMinimumMsat":1000,"feeBaseMsat":1000,"feeProportionalMillionths":100,"htlcMaximumMsat":15000000000,"tlvStream":{"records":[],"unknown":[]}}}""", + -> """{"type":"DATA_NORMAL","commitments":{"channelId":"6ecfe1e9e01abd3fbe482efde50e4687b3f1f5d8cba609174aebce1c01415611","channelConfig":[],"channelFeatures":[],"localParams":{"nodeId":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","fundingKeyPath":{"path":[3561221353,3653515793,2711311691,2863050005]},"dustLimit":546,"maxHtlcValueInFlightMsat":1000000000,"requestedChannelReserve_opt":150000,"htlcMinimum":1,"toSelfDelay":144,"maxAcceptedHtlcs":30,"isInitiator":true,"defaultFinalScriptPubKey":"a91445e990148599176534ec9b75df92ace9263f7d3487","initFeatures":{"activated":{"option_data_loss_protect":"optional","initial_routing_sync":"optional","gossip_queries":"optional"},"unknown":[]}},"remoteParams":{"nodeId":"0269a94e8b32c005e4336bfb743c08a6e9beb13d940d57c479d95c8e687ccbdb9f","dustLimit":573,"maxHtlcValueInFlightMsat":14850000000,"requestedChannelReserve_opt":150000,"htlcMinimum":1000,"toSelfDelay":1802,"maxAcceptedHtlcs":483,"fundingPubKey":"0215c35f143adeadf010abc4ce0be323760f9a9c486978b762d31cfcb101c44cc4","revocationBasepoint":"03d17fdddddae4aeeb7022dedf059f1d0f06b4b68b6309cade4e55ae1ac0f0230c","paymentBasepoint":"03c0c4257191e5c4b6e7dcf2e9fb9be00fc713686f77fc4719987e77ee2436d8bd","delayedPaymentBasepoint":"03550b13a43d2b09649423e75774bb5a91a243bac78af4d39aece23380bb42b397","htlcBasepoint":"034c93b1981c26dd71bf7a44d16d3b950df19c94c0846b407b3a6f5cf60ff8ac7f","initFeatures":{"activated":{"option_data_loss_protect":"mandatory","gossip_queries":"optional"},"unknown":[]}},"channelFlags":{"announceChannel":true},"localCommit":{"index":20024,"spec":{"htlcs":[],"commitTxFeerate":750,"toLocal":1343316620,"toRemote":13656683380},"commitTxAndRemoteSig":{"commitTx":{"txid":"65fe0b1f079fa763448df3ab8d94b1ad7d377c061121376be90b9c0c1bb0cd43","tx":"02000000016ecfe1e9e01abd3fbe482efde50e4687b3f1f5d8cba609174aebce1c0141561100000000007cf5db8002357d1400000000002200203539c96d5de8d2b2178f798a3b9dd5d390c1080ab4c79803c8878e67f7c801736b62d00000000000160014bcae0020da34e12fc9bd0fd75e3f1e4ee7085f49df013320"},"remoteSig":"bd09313503ea357b3a231135c87cd1f5b26cb3bd8033e371815b7e2b4af623173b9824adf260c8735a72c58087f88f4a2f39554003996466857c1d1b25c8044f"},"htlcTxsAndRemoteSigs":[]},"remoteCommit":{"index":20024,"spec":{"htlcs":[],"commitTxFeerate":750,"toLocal":13656683380,"toRemote":1343316620},"txid":"919c015d2e0a3dc214786c24c7f035302cb9c954f740ed267a84cdca66b0be49","remotePerCommitmentPoint":"02b82bbd59e0d22665671d9e47d8733058b92f18e906e9403753661aa03dc9e4dd"},"localChanges":{"proposed":[],"signed":[],"acked":[]},"remoteChanges":{"proposed":[],"acked":[],"signed":[]},"localNextHtlcId":9288,"remoteNextHtlcId":151,"originChannels":{},"remoteNextCommitInfo":"02a4471183c519e54b8ee66fb41cbe06fed1153fce258db72ce67f9a9e044f0a16","commitInput":{"outPoint":"115641011cceeb4a1709a6cbd8f5f1b387460ee5fd2e48be3fbd1ae0e9e1cf6e:0","amountSatoshis":15000000},"remotePerCommitmentSecrets":null},"shortChannelId":"1413373x969x0","buried":true,"channelUpdate":{"signature":"52b543f6ee053eec41521def5cd4d9a63c8b117264c94f5b6ec2a5aa6b8a5d2173c36f846edb57462d4c521e352e61a9cbc89a163961dcd4f2ae05cd4d79bf9b","chainHash":"43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000","shortChannelId":"1413373x969x0","timestamp":{"iso":"2019-06-24T09:39:33Z","unix":1561369173},"channelFlags":{"isEnabled":true,"isNode1":false},"cltvExpiryDelta":144,"htlcMinimumMsat":1000,"feeBaseMsat":1000,"feeProportionalMillionths":100,"htlcMaximumMsat":15000000000,"tlvStream":{"records":[],"unknown":[]}}}""", hex"0200020000000303933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b13400098c4b989bbdced820a77a7186c2320e7d176a5c8b5c16d6ac2af3889d6bc8bf8080000001000000000000022200000004a817c80000000000000249f0000000000000000102d0001eff1600148061b7fbd2d84ed1884177ea785faecb2080b10302e56c8eca8d4f00df84ac34c23f49c006d57d316b7ada5c346e9d4211e11604b300000004080aa982027455aef8453d92f4706b560b61527cc217ddf14da41770e8ed6607190a1851b8000000000000023d000000037521048000000000000249f00000000000000001070a01e302eff5309b9368340edc6114d738b3590e6969bec4e95d8a080cf185e8b9ce5e4b0343bf4bfbaea5c100f1f2bf1cdf82a0ef97c9a0069a2aec631e7c3084ba929b7503c54e7d5ccfc13f1a6c7a441ffcfac86248574d1bc0fe9773836f4c724ea7b2bd03765aaac2e8fa6dbce7de5143072e9d9d5e96a1fd451d02fe4ff803f413f303f8022f3b055b0d35cde31dec5263a8ed638433e3424a4e197c06d94053985a364a5700000004808a52a1010000000000000004000000001046000000037e11d6000000000000000000245986f644357956c1674f385e0878fb7bb44ab3d57ea9911fab98af8a71e1ad1b000000002bc0e1e40000000000220020690fb50de412adf9b20a7fc6c8fb86f1bfd4ebc1ef8e2d96a5a196560798d944475221023ced05ed1ab328b67477376d68a69ecd0f371a9d5843c6c3be4d31498d516d8d2102eff5309b9368340edc6114d738b3590e6969bec4e95d8a080cf185e8b9ce5e4b52aefd013b020000000001015986f644357956c1674f385e0878fb7bb44ab3d57ea9911fab98af8a71e1ad1b0000000000c2d6178001f8d5e4000000000022002080f1dfe71a865b605593e169677c952aaa1196fc2f541ef7d21c3b1006527b61040047304402207f8c1936d0a50671c993890f887c78c6019abc2a2e8018899dcdc0e891fd2b090220046b56afa2cb7e9470073c238654ecf584bcf5c00b96b91e38335a70e2739ec901483045022100871afd240e20a171b9cba46f20555f848c5850f94ec7da7b33b9eeaf6af6653c0220119cda8cbf5f80986d6a4f0db2590c734d1de399a7060a477b5d94df0183625b01475221023ced05ed1ab328b67477376d68a69ecd0f371a9d5843c6c3be4d31498d516d8d2102eff5309b9368340edc6114d738b3590e6969bec4e95d8a080cf185e8b9ce5e4b52aed7782c20000000000000000000040000000010460000000000000000000000037e11d600b5f2287b2d5edf4df5602a3c287db3b938c3f1a943e40715886db5bd400f95d802e7e1abac1feb54ee3ac2172c9e2231f77765df57664fb44a6dc2e4aa9e6a9a6a000000000000000000000000000000000000000000000000000000000000ff03fd10fe44564e2d7e1550099785c2c1bad32a5ae0feeef6e27f0c108d18b4931d245986f644357956c1674f385e0878fb7bb44ab3d57ea9911fab98af8a71e1ad1b000000002bc0e1e40000000000220020690fb50de412adf9b20a7fc6c8fb86f1bfd4ebc1ef8e2d96a5a196560798d944475221023ced05ed1ab328b67477376d68a69ecd0f371a9d5843c6c3be4d31498d516d8d2102eff5309b9368340edc6114d738b3590e6969bec4e95d8a080cf185e8b9ce5e4b52ae0001003e0000fffffffffffc0080474b8cf7bb98217dd8dc475cb7c057a3465d466728978bbb909d0a05d4ae7bbe0001fffffffffff85986f644357956c1674f385e0878fb7bb44ab3d57ea9911fab98af8a71e1ad1b1eedce0000010000fffffd01ae98d7a81bc1aa92fcfb74ced2213e85e0d92ae8ac622bf294b3551c7c27f6f84f782f3b318e4d0eb2c67ac719a7c65afcf85bf159f6ceea9427be54920134196992f6ed0e059db72105a13ec0e799bb08896cad8b4feb7e9ec7283c309b5f43123af1bd9e913fc2db018edadde8932d6992408f10c1ad020504361972dfa7fef09bbc2b568cef3c8c006f7860106fd5984bcc271ff06c4829db2a665e59b7c0b22c311a340ff2ab9bcb74a50db10ed85503ad2d248d95af8151aca8ef96248e8f84b3075922385fbaf012f057e7ee84ecbc14c84880520b26d6fd22ab5f107db606a906efdcf0f88ffbe32dc6ecc10131e1ff0dc8d68dad89c98562557f00448b000043497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea3309000000001eedce0000010000027455aef8453d92f4706b560b61527cc217ddf14da41770e8ed6607190a1851b803933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b13402eff5309b9368340edc6114d738b3590e6969bec4e95d8a080cf185e8b9ce5e4b023ced05ed1ab328b67477376d68a69ecd0f371a9d5843c6c3be4d31498d516d8d88710d73875607575f3d84bb507dd87cca5b85f0cdac84f4ccecce7af3a55897525a45070fe26c0ea43e9580d4ea4cfa62ee3273e5546911145cba6bbf56e59d8e43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea3309000000001eedce000001000060e6eb14010100900000000000000001000003e800000064000000037e11d6000000" - -> """{"type":"DATA_NORMAL","commitments":{"channelId":"5986f644357956c1674f385e0878fb7bb44ab3d57ea9911fab98af8a71e1ad1b","channelConfig":["funding_pubkey_based_channel_keypath"],"channelFeatures":["option_static_remotekey"],"localParams":{"nodeId":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","fundingKeyPath":{"path":[2353764507,3184449568,2809819526,3258060413,392846475,1545000620,720603293,1808318336,2147483649]},"dustLimit":546,"maxHtlcValueInFlightMsat":20000000000,"channelReserve":150000,"htlcMinimum":1,"toSelfDelay":720,"maxAcceptedHtlcs":30,"isInitiator":true,"defaultFinalScriptPubKey":"00148061b7fbd2d84ed1884177ea785faecb2080b103","walletStaticPaymentBasepoint":"02e56c8eca8d4f00df84ac34c23f49c006d57d316b7ada5c346e9d4211e11604b3","initFeatures":{"activated":{"option_support_large_channel":"optional","gossip_queries_ex":"optional","option_data_loss_protect":"optional","var_onion_optin":"mandatory","option_static_remotekey":"optional","payment_secret":"optional","option_shutdown_anysegwit":"optional","basic_mpp":"optional","gossip_queries":"optional"},"unknown":[]}},"remoteParams":{"nodeId":"027455aef8453d92f4706b560b61527cc217ddf14da41770e8ed6607190a1851b8","dustLimit":573,"maxHtlcValueInFlightMsat":14850000000,"channelReserve":150000,"htlcMinimum":1,"toSelfDelay":1802,"maxAcceptedHtlcs":483,"fundingPubKey":"02eff5309b9368340edc6114d738b3590e6969bec4e95d8a080cf185e8b9ce5e4b","revocationBasepoint":"0343bf4bfbaea5c100f1f2bf1cdf82a0ef97c9a0069a2aec631e7c3084ba929b75","paymentBasepoint":"03c54e7d5ccfc13f1a6c7a441ffcfac86248574d1bc0fe9773836f4c724ea7b2bd","delayedPaymentBasepoint":"03765aaac2e8fa6dbce7de5143072e9d9d5e96a1fd451d02fe4ff803f413f303f8","htlcBasepoint":"022f3b055b0d35cde31dec5263a8ed638433e3424a4e197c06d94053985a364a57","initFeatures":{"activated":{"option_upfront_shutdown_script":"optional","payment_secret":"mandatory","option_data_loss_protect":"mandatory","var_onion_optin":"optional","option_static_remotekey":"mandatory","option_support_large_channel":"optional","option_anchors_zero_fee_htlc_tx":"optional","basic_mpp":"optional","gossip_queries":"optional"},"unknown":[31]}},"channelFlags":{"announceChannel":true},"localCommit":{"index":4,"spec":{"htlcs":[],"commitTxFeerate":4166,"toLocal":15000000000,"toRemote":0},"commitTxAndRemoteSig":{"commitTx":{"txid":"fa747ecb6f718c6831cc7148cf8d65c3468d2bb6c202605e2b82d2277491222f","tx":"02000000015986f644357956c1674f385e0878fb7bb44ab3d57ea9911fab98af8a71e1ad1b0000000000c2d6178001f8d5e4000000000022002080f1dfe71a865b605593e169677c952aaa1196fc2f541ef7d21c3b1006527b61d7782c20"},"remoteSig":"871afd240e20a171b9cba46f20555f848c5850f94ec7da7b33b9eeaf6af6653c119cda8cbf5f80986d6a4f0db2590c734d1de399a7060a477b5d94df0183625b"},"htlcTxsAndRemoteSigs":[]},"remoteCommit":{"index":4,"spec":{"htlcs":[],"commitTxFeerate":4166,"toLocal":0,"toRemote":15000000000},"txid":"b5f2287b2d5edf4df5602a3c287db3b938c3f1a943e40715886db5bd400f95d8","remotePerCommitmentPoint":"02e7e1abac1feb54ee3ac2172c9e2231f77765df57664fb44a6dc2e4aa9e6a9a6a"},"localChanges":{"proposed":[],"signed":[],"acked":[]},"remoteChanges":{"proposed":[],"acked":[],"signed":[]},"localNextHtlcId":0,"remoteNextHtlcId":0,"originChannels":{},"remoteNextCommitInfo":"03fd10fe44564e2d7e1550099785c2c1bad32a5ae0feeef6e27f0c108d18b4931d","commitInput":{"outPoint":"1bade1718aaf98ab1f91a97ed5b34ab47bfb78085e384f67c156793544f68659:0","amountSatoshis":15000000},"remotePerCommitmentSecrets":null},"shortChannelId":"2026958x1x0","buried":true,"channelAnnouncement":{"nodeSignature1":"98d7a81bc1aa92fcfb74ced2213e85e0d92ae8ac622bf294b3551c7c27f6f84f782f3b318e4d0eb2c67ac719a7c65afcf85bf159f6ceea9427be549201341969","nodeSignature2":"92f6ed0e059db72105a13ec0e799bb08896cad8b4feb7e9ec7283c309b5f43123af1bd9e913fc2db018edadde8932d6992408f10c1ad020504361972dfa7fef0","bitcoinSignature1":"9bbc2b568cef3c8c006f7860106fd5984bcc271ff06c4829db2a665e59b7c0b22c311a340ff2ab9bcb74a50db10ed85503ad2d248d95af8151aca8ef96248e8f","bitcoinSignature2":"84b3075922385fbaf012f057e7ee84ecbc14c84880520b26d6fd22ab5f107db606a906efdcf0f88ffbe32dc6ecc10131e1ff0dc8d68dad89c98562557f00448b","features":{"activated":{},"unknown":[]},"chainHash":"43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000","shortChannelId":"2026958x1x0","nodeId1":"027455aef8453d92f4706b560b61527cc217ddf14da41770e8ed6607190a1851b8","nodeId2":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","bitcoinKey1":"02eff5309b9368340edc6114d738b3590e6969bec4e95d8a080cf185e8b9ce5e4b","bitcoinKey2":"023ced05ed1ab328b67477376d68a69ecd0f371a9d5843c6c3be4d31498d516d8d","tlvStream":{"records":[],"unknown":[]}},"channelUpdate":{"signature":"710d73875607575f3d84bb507dd87cca5b85f0cdac84f4ccecce7af3a55897525a45070fe26c0ea43e9580d4ea4cfa62ee3273e5546911145cba6bbf56e59d8e","chainHash":"43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000","shortChannelId":"2026958x1x0","timestamp":{"iso":"2021-07-08T12:09:56Z","unix":1625746196},"channelFlags":{"isEnabled":true,"isNode1":false},"cltvExpiryDelta":144,"htlcMinimumMsat":1,"feeBaseMsat":1000,"feeProportionalMillionths":100,"htlcMaximumMsat":15000000000,"tlvStream":{"records":[],"unknown":[]}}}""" + -> """{"type":"DATA_NORMAL","commitments":{"channelId":"5986f644357956c1674f385e0878fb7bb44ab3d57ea9911fab98af8a71e1ad1b","channelConfig":["funding_pubkey_based_channel_keypath"],"channelFeatures":["option_static_remotekey"],"localParams":{"nodeId":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","fundingKeyPath":{"path":[2353764507,3184449568,2809819526,3258060413,392846475,1545000620,720603293,1808318336,2147483649]},"dustLimit":546,"maxHtlcValueInFlightMsat":20000000000,"requestedChannelReserve_opt":150000,"htlcMinimum":1,"toSelfDelay":720,"maxAcceptedHtlcs":30,"isInitiator":true,"defaultFinalScriptPubKey":"00148061b7fbd2d84ed1884177ea785faecb2080b103","walletStaticPaymentBasepoint":"02e56c8eca8d4f00df84ac34c23f49c006d57d316b7ada5c346e9d4211e11604b3","initFeatures":{"activated":{"option_support_large_channel":"optional","gossip_queries_ex":"optional","option_data_loss_protect":"optional","var_onion_optin":"mandatory","option_static_remotekey":"optional","payment_secret":"optional","option_shutdown_anysegwit":"optional","basic_mpp":"optional","gossip_queries":"optional"},"unknown":[]}},"remoteParams":{"nodeId":"027455aef8453d92f4706b560b61527cc217ddf14da41770e8ed6607190a1851b8","dustLimit":573,"maxHtlcValueInFlightMsat":14850000000,"requestedChannelReserve_opt":150000,"htlcMinimum":1,"toSelfDelay":1802,"maxAcceptedHtlcs":483,"fundingPubKey":"02eff5309b9368340edc6114d738b3590e6969bec4e95d8a080cf185e8b9ce5e4b","revocationBasepoint":"0343bf4bfbaea5c100f1f2bf1cdf82a0ef97c9a0069a2aec631e7c3084ba929b75","paymentBasepoint":"03c54e7d5ccfc13f1a6c7a441ffcfac86248574d1bc0fe9773836f4c724ea7b2bd","delayedPaymentBasepoint":"03765aaac2e8fa6dbce7de5143072e9d9d5e96a1fd451d02fe4ff803f413f303f8","htlcBasepoint":"022f3b055b0d35cde31dec5263a8ed638433e3424a4e197c06d94053985a364a57","initFeatures":{"activated":{"option_upfront_shutdown_script":"optional","payment_secret":"mandatory","option_data_loss_protect":"mandatory","var_onion_optin":"optional","option_static_remotekey":"mandatory","option_support_large_channel":"optional","option_anchors_zero_fee_htlc_tx":"optional","basic_mpp":"optional","gossip_queries":"optional"},"unknown":[31]}},"channelFlags":{"announceChannel":true},"localCommit":{"index":4,"spec":{"htlcs":[],"commitTxFeerate":4166,"toLocal":15000000000,"toRemote":0},"commitTxAndRemoteSig":{"commitTx":{"txid":"fa747ecb6f718c6831cc7148cf8d65c3468d2bb6c202605e2b82d2277491222f","tx":"02000000015986f644357956c1674f385e0878fb7bb44ab3d57ea9911fab98af8a71e1ad1b0000000000c2d6178001f8d5e4000000000022002080f1dfe71a865b605593e169677c952aaa1196fc2f541ef7d21c3b1006527b61d7782c20"},"remoteSig":"871afd240e20a171b9cba46f20555f848c5850f94ec7da7b33b9eeaf6af6653c119cda8cbf5f80986d6a4f0db2590c734d1de399a7060a477b5d94df0183625b"},"htlcTxsAndRemoteSigs":[]},"remoteCommit":{"index":4,"spec":{"htlcs":[],"commitTxFeerate":4166,"toLocal":0,"toRemote":15000000000},"txid":"b5f2287b2d5edf4df5602a3c287db3b938c3f1a943e40715886db5bd400f95d8","remotePerCommitmentPoint":"02e7e1abac1feb54ee3ac2172c9e2231f77765df57664fb44a6dc2e4aa9e6a9a6a"},"localChanges":{"proposed":[],"signed":[],"acked":[]},"remoteChanges":{"proposed":[],"acked":[],"signed":[]},"localNextHtlcId":0,"remoteNextHtlcId":0,"originChannels":{},"remoteNextCommitInfo":"03fd10fe44564e2d7e1550099785c2c1bad32a5ae0feeef6e27f0c108d18b4931d","commitInput":{"outPoint":"1bade1718aaf98ab1f91a97ed5b34ab47bfb78085e384f67c156793544f68659:0","amountSatoshis":15000000},"remotePerCommitmentSecrets":null},"shortChannelId":"2026958x1x0","buried":true,"channelAnnouncement":{"nodeSignature1":"98d7a81bc1aa92fcfb74ced2213e85e0d92ae8ac622bf294b3551c7c27f6f84f782f3b318e4d0eb2c67ac719a7c65afcf85bf159f6ceea9427be549201341969","nodeSignature2":"92f6ed0e059db72105a13ec0e799bb08896cad8b4feb7e9ec7283c309b5f43123af1bd9e913fc2db018edadde8932d6992408f10c1ad020504361972dfa7fef0","bitcoinSignature1":"9bbc2b568cef3c8c006f7860106fd5984bcc271ff06c4829db2a665e59b7c0b22c311a340ff2ab9bcb74a50db10ed85503ad2d248d95af8151aca8ef96248e8f","bitcoinSignature2":"84b3075922385fbaf012f057e7ee84ecbc14c84880520b26d6fd22ab5f107db606a906efdcf0f88ffbe32dc6ecc10131e1ff0dc8d68dad89c98562557f00448b","features":{"activated":{},"unknown":[]},"chainHash":"43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000","shortChannelId":"2026958x1x0","nodeId1":"027455aef8453d92f4706b560b61527cc217ddf14da41770e8ed6607190a1851b8","nodeId2":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","bitcoinKey1":"02eff5309b9368340edc6114d738b3590e6969bec4e95d8a080cf185e8b9ce5e4b","bitcoinKey2":"023ced05ed1ab328b67477376d68a69ecd0f371a9d5843c6c3be4d31498d516d8d","tlvStream":{"records":[],"unknown":[]}},"channelUpdate":{"signature":"710d73875607575f3d84bb507dd87cca5b85f0cdac84f4ccecce7af3a55897525a45070fe26c0ea43e9580d4ea4cfa62ee3273e5546911145cba6bbf56e59d8e","chainHash":"43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000","shortChannelId":"2026958x1x0","timestamp":{"iso":"2021-07-08T12:09:56Z","unix":1625746196},"channelFlags":{"isEnabled":true,"isNode1":false},"cltvExpiryDelta":144,"htlcMinimumMsat":1,"feeBaseMsat":1000,"feeProportionalMillionths":100,"htlcMaximumMsat":15000000000,"tlvStream":{"records":[],"unknown":[]}}}""" ) refs.foreach { case (oldbin, refjson) => @@ -256,7 +256,7 @@ object ChannelCodecsSpec { fundingKeyPath = DeterministicWallet.KeyPath(Seq(42L)), dustLimit = Satoshi(546), maxHtlcValueInFlightMsat = UInt64(50000000), - channelReserve = 10000 sat, + requestedChannelReserve_opt = Some(10000 sat), htlcMinimum = 10000 msat, toSelfDelay = CltvExpiryDelta(144), maxAcceptedHtlcs = 50, @@ -269,7 +269,7 @@ object ChannelCodecsSpec { nodeId = randomKey().publicKey, dustLimit = 546 sat, maxHtlcValueInFlightMsat = UInt64(5000000), - channelReserve = 10000 sat, + requestedChannelReserve_opt = Some(10000 sat), htlcMinimum = 5000 msat, toSelfDelay = CltvExpiryDelta(144), maxAcceptedHtlcs = 50, diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version1/ChannelCodecs1Spec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version1/ChannelCodecs1Spec.scala index 49733b5114..6add1bdf43 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version1/ChannelCodecs1Spec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version1/ChannelCodecs1Spec.scala @@ -51,7 +51,7 @@ class ChannelCodecs1Spec extends AnyFunSuite { assert(channelVersionCodec.encode(ChannelVersion.ANCHOR_OUTPUTS) === Attempt.successful(hex"00000007".bits)) } - test("encode/decode localparams") { + test("encode/decode local params") { def roundtrip(localParams: LocalParams, codec: Codec[LocalParams]) = { val encoded = codec.encode(localParams).require val decoded = codec.decode(encoded).require @@ -63,7 +63,7 @@ class ChannelCodecs1Spec extends AnyFunSuite { fundingKeyPath = DeterministicWallet.KeyPath(Seq(42L)), dustLimit = Satoshi(Random.nextInt(Int.MaxValue)), maxHtlcValueInFlightMsat = UInt64(Random.nextInt(Int.MaxValue)), - channelReserve = Satoshi(Random.nextInt(Int.MaxValue)), + requestedChannelReserve_opt = Some(Satoshi(Random.nextInt(Int.MaxValue))), htlcMinimum = MilliSatoshi(Random.nextInt(Int.MaxValue)), toSelfDelay = CltvExpiryDelta(Random.nextInt(Short.MaxValue)), maxAcceptedHtlcs = Random.nextInt(Short.MaxValue), @@ -78,12 +78,12 @@ class ChannelCodecs1Spec extends AnyFunSuite { roundtrip(o, localParamsCodec(ChannelVersion.ANCHOR_OUTPUTS)) } - test("encode/decode remoteparams") { + test("encode/decode remote params") { val o = RemoteParams( nodeId = randomKey().publicKey, dustLimit = Satoshi(Random.nextInt(Int.MaxValue)), maxHtlcValueInFlightMsat = UInt64(Random.nextInt(Int.MaxValue)), - channelReserve = Satoshi(Random.nextInt(Int.MaxValue)), + requestedChannelReserve_opt = Some(Satoshi(Random.nextInt(Int.MaxValue))), htlcMinimum = MilliSatoshi(Random.nextInt(Int.MaxValue)), toSelfDelay = CltvExpiryDelta(Random.nextInt(Short.MaxValue)), maxAcceptedHtlcs = Random.nextInt(Short.MaxValue), @@ -98,7 +98,7 @@ class ChannelCodecs1Spec extends AnyFunSuite { val decoded = remoteParamsCodec.decodeValue(encoded).require assert(o === decoded) - // Backwards-compatibility: decode remoteparams with global features. + // Backwards-compatibility: decode remote params with global features. val withGlobalFeatures = hex"03c70c3b813815a8b79f41622b6f2c343fa24d94fb35fa7110bbb3d4d59cd9612e0000000059844cbc000000001b1524ea000000001503cbac000000006b75d3272e38777e029fa4e94066163024177311de7ba1befec2e48b473c387bbcee1484bf276a54460215e3dfb8e6f262222c5f343f5e38c5c9a43d2594c7f06dd7ac1a4326c665dd050347aba4d56d7007a7dcf03594423dccba9ed700d11e665d261594e1154203df31020d457ee336ba6eeb328d00f1b8bd8bfefb8a4dcd5af6db4c438b7ec5106c7edc0380df17e1beb0f238e51a39122ac4c6fb57f3c4f5b7bc9432f991b1ef4a8af3570002020000018a" val withGlobalFeaturesDecoded = remoteParamsCodec.decode(withGlobalFeatures.bits).require.value assert(withGlobalFeaturesDecoded.initFeatures.toByteVector === hex"028a") diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3Spec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3Spec.scala index 36ab4756aa..4f2f3a634d 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3Spec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3Spec.scala @@ -15,7 +15,7 @@ */ package fr.acinq.eclair.wire.internal.channel.version3 -import fr.acinq.bitcoin.scalacompat.{ByteVector32, Satoshi} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, DeterministicWallet, Satoshi} import fr.acinq.eclair.FeatureSupport.{Mandatory, Optional} import fr.acinq.eclair.Features.{ChannelRangeQueries, PaymentSecret, VariableLengthOnion} import fr.acinq.eclair.channel._ @@ -27,6 +27,8 @@ import fr.acinq.eclair.{BlockHeight, CltvExpiryDelta, Features, MilliSatoshi, UI import org.scalatest.funsuite.AnyFunSuite import scodec.bits.{ByteVector, HexStringSyntax} +import scala.util.Random + class ChannelCodecs3Spec extends AnyFunSuite { test("basic serialization test (NORMAL)") { @@ -77,11 +79,12 @@ class ChannelCodecs3Spec extends AnyFunSuite { } test("encode/decode optional shutdown script") { + val codec = remoteParamsCodec(ChannelFeatures()) val remoteParams = RemoteParams( randomKey().publicKey, Satoshi(600), UInt64(123456L), - Satoshi(300), + Some(Satoshi(300)), MilliSatoshi(1000), CltvExpiryDelta(42), 42, @@ -92,9 +95,9 @@ class ChannelCodecs3Spec extends AnyFunSuite { randomKey().publicKey, Features(ChannelRangeQueries -> Optional, VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory), None) - assert(remoteParamsCodec.decodeValue(remoteParamsCodec.encode(remoteParams).require).require === remoteParams) + assert(codec.decodeValue(codec.encode(remoteParams).require).require === remoteParams) val remoteParams1 = remoteParams.copy(shutdownScript = Some(ByteVector.fromValidHex("deadbeef"))) - assert(remoteParamsCodec.decodeValue(remoteParamsCodec.encode(remoteParams1).require).require === remoteParams1) + assert(codec.decodeValue(codec.encode(remoteParams1).require).require === remoteParams1) val dataWithoutRemoteShutdownScript = normal.copy(commitments = normal.commitments.copy(remoteParams = remoteParams)) assert(DATA_NORMAL_Codec.decode(DATA_NORMAL_Codec.encode(dataWithoutRemoteShutdownScript).require).require.value === dataWithoutRemoteShutdownScript) @@ -103,6 +106,54 @@ class ChannelCodecs3Spec extends AnyFunSuite { assert(DATA_NORMAL_Codec.decode(DATA_NORMAL_Codec.encode(dataWithRemoteShutdownScript).require).require.value === dataWithRemoteShutdownScript) } + test("encode/decode optional channel reserve") { + val localParams = LocalParams( + randomKey().publicKey, + DeterministicWallet.KeyPath(Seq(42L)), + Satoshi(660), + UInt64(500000), + Some(Satoshi(15000)), + MilliSatoshi(1000), + CltvExpiryDelta(36), + 50, + Random.nextBoolean(), + hex"deadbeef", + None, + Features().initFeatures()) + val remoteParams = RemoteParams( + randomKey().publicKey, + Satoshi(500), + UInt64(100000), + Some(Satoshi(30000)), + MilliSatoshi(1500), + CltvExpiryDelta(144), + 10, + randomKey().publicKey, + randomKey().publicKey, + randomKey().publicKey, + randomKey().publicKey, + randomKey().publicKey, + Features(), + None) + + { + val localCodec = localParamsCodec(ChannelFeatures()) + val remoteCodec = remoteParamsCodec(ChannelFeatures()) + val decodedLocalParams = localCodec.decode(localCodec.encode(localParams).require).require.value + val decodedRemoteParams = remoteCodec.decode(remoteCodec.encode(remoteParams).require).require.value + assert(decodedLocalParams === localParams) + assert(decodedRemoteParams === remoteParams) + } + { + val localCodec = localParamsCodec(ChannelFeatures(Features.DualFunding)) + val remoteCodec = remoteParamsCodec(ChannelFeatures(Features.DualFunding)) + val decodedLocalParams = localCodec.decode(localCodec.encode(localParams).require).require.value + val decodedRemoteParams = remoteCodec.decode(remoteCodec.encode(remoteParams).require).require.value + assert(decodedLocalParams === localParams.copy(requestedChannelReserve_opt = None)) + assert(decodedRemoteParams === remoteParams.copy(requestedChannelReserve_opt = None)) + } + } + test("backward compatibility DATA_NORMAL_COMPAT_02_Codec") { val oldBin = hex"00022aed498450b3eb2f6aafedd40640b54efc60ae681da9e6a35299b8b6a6125a7301010003af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d00009d2b17f27b3938b2b50ec713df1b1ae5fd3d23010c9e2e22385f13a168c6acf2c80000001000000000000044c000000001dcd65000000000000002710000000000000000000900064ff1600149d706d0fa71a0b6aa0f3fa400bee18102b45c8170000186b0200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000024982039dc0e0b1d25905e44fdf6f8e89755a5e219685840d0bc1d28d3308f9628a358500000000000003e8ffffffffffffffff0000000000004e2000000000000003e80090001e03fb08866066d5f6f539314220fc31a8e7fdf6ccd12ccde150acc59bac5bb971e003f399d17a9e2fb13a52373d1631807c3161250b2774642fffa629858ad0831f68020b4ecc52820a93f6da40a60d7859307edc737347054d82592959ed1cd0b02e9c03db348203bfd939779bfdd825bc807e904225c3348fa5e3bb57d38ac4b9f40f850284c0df55fbfc2212cbd18cf8ab0eb5283b4b883350ff07e81988e01ae2bb71e20000000302498200000000000000000000000000002710000000002faf0800000000000bebc200242aed498450b3eb2f6aafedd40640b54efc60ae681da9e6a35299b8b6a6125a73000000002b40420f00000000002200203305e10ec90f004675285e9b0371a663ead7abe6caba8c6d5739ace6c9eab4534752210390d6a42ff78a21b41560f75359f0f8a9edaaf0ddcf1a6609130f0d5f234463662103fb08866066d5f6f539314220fc31a8e7fdf6ccd12ccde150acc59bac5bb971e052ae7d02000000012aed498450b3eb2f6aafedd40640b54efc60ae681da9e6a35299b8b6a6125a7300000000000a1e418002400d0300000000001600146862a069e7038573a81f5627ae7d0c6ee5cd0acfb8180c0000000000220020a5f137a8049afcac9f0451b0f31677a81b5443f1c04c1910b37b0d2b8aa4ca0084a4eb2096e400507ac49b57910c914cff8338b8bc57884983541ee1b6919ece7431f4030b09a5fcd87b35682dea0d8faa394e47cc31af897cdf3e6ff502b086cfac37c700000000000000000000000000002710000000000bebc200000000002faf080028131acadf245e7d95d3d7a7f1ac0c0411ead7957ab00d310696b0b5d7d14ac8020597a38e090850030f255fb2781a53713faf7ba81c44de931a63a78efd9908ef000000000000000000000000000000000000000000000000000000000000ff035878c87f6ed100476648193e10a1462bfb55cea3ec5a8f4fbd0fe7304979094b242aed498450b3eb2f6aafedd40640b54efc60ae681da9e6a35299b8b6a6125a73000000002b40420f00000000002200203305e10ec90f004675285e9b0371a663ead7abe6caba8c6d5739ace6c9eab4534752210390d6a42ff78a21b41560f75359f0f8a9edaaf0ddcf1a6609130f0d5f234463662103fb08866066d5f6f539314220fc31a8e7fdf6ccd12ccde150acc59bac5bb971e052ae000000061a8000002a0000000088ec7ae2af533c270809b48f9a0b5a9650df9961a2177e04d7e9929ab319fcd0d150e3ded703b8b4a3f5faff0f2fedf22a6729760deefdea4feed868e4e3cdcf1b06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f061a8000002a000061163fb60101009000000000000003e8000858b800000014000000003b9aca000000" val decoded1 = channelDataCodec.decode(oldBin.bits).require.value diff --git a/eclair-node/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala b/eclair-node/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala index e54c2944b4..9913651d85 100644 --- a/eclair-node/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala +++ b/eclair-node/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala @@ -1156,7 +1156,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM wsClient.expectMessage(expectedSerializedPset) val chcr = ChannelCreated(system.deadLetters, system.deadLetters, bobNodeId, isInitiator = true, ByteVector32.One, FeeratePerKw(25 sat), Some(FeeratePerKw(20 sat))) - val expectedSerializedChcr = """{"type":"channel-opened","remoteNodeId":"039dc0e0b1d25905e44fdf6f8e89755a5e219685840d0bc1d28d3308f9628a3585","isInitiator":true,"temporaryChannelId":"0100000000000000000000000000000000000000000000000000000000000000","initialFeeratePerKw":25,"fundingTxFeeratePerKw":20}""" + val expectedSerializedChcr = """{"type":"channel-opened","remoteNodeId":"039dc0e0b1d25905e44fdf6f8e89755a5e219685840d0bc1d28d3308f9628a3585","isInitiator":true,"temporaryChannelId":"0100000000000000000000000000000000000000000000000000000000000000","commitTxFeeratePerKw":25,"fundingTxFeeratePerKw":20}""" assert(serialization.write(chcr) === expectedSerializedChcr) system.eventStream.publish(chcr) wsClient.expectMessage(expectedSerializedChcr) From 1e8791c56ec4f6b3fc7ed5d4790463f7e61e6687 Mon Sep 17 00:00:00 2001 From: Bastien Teinturier <31281497+t-bast@users.noreply.github.com> Date: Thu, 19 May 2022 10:40:03 +0200 Subject: [PATCH 072/121] Update dual funding codecs (#2245) * Use u16 instead of varint for lengths * Revert channel flags extension * Remove support for non-native segwit inputs --- .../eclair/wire/protocol/CommonCodecs.scala | 8 ---- .../protocol/LightningMessageCodecs.scala | 21 +++------ .../wire/protocol/LightningMessageTypes.scala | 2 - .../protocol/LightningMessageCodecsSpec.scala | 45 +++++++------------ 4 files changed, 22 insertions(+), 54 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/CommonCodecs.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/CommonCodecs.scala index 7e37e80478..78ad89f00a 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/CommonCodecs.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/CommonCodecs.scala @@ -100,9 +100,6 @@ object CommonCodecs { // It is useful in combination with variableSizeBytesLong to encode/decode TLV lengths because those will always be < 2^63. val varintoverflow: Codec[Long] = varint.narrow(l => if (l <= UInt64(Long.MaxValue)) Attempt.successful(l.toBigInt.toLong) else Attempt.failure(Err(s"overflow for value $l")), l => UInt64(l)) - // This codec can be safely used for values < 2^32 and will fail otherwise. - val smallvarint: Codec[Int] = varint.narrow(l => if (l <= UInt64(Int.MaxValue)) Attempt.successful(l.toBigInt.toInt) else Attempt.failure(Err(s"overflow for value $l")), l => UInt64(l)) - val bytes32: Codec[ByteVector32] = limitedSizeBytes(32, bytesStrict(32).xmap(d => ByteVector32(d), d => d.bytes)) val bytes64: Codec[ByteVector64] = limitedSizeBytes(64, bytesStrict(64).xmap(d => ByteVector64(d), d => d.bytes)) @@ -115,11 +112,6 @@ object CommonCodecs { val channelflags: Codec[ChannelFlags] = (ignore(7) dropLeft bool).as[ChannelFlags] - val extendedChannelFlags: Codec[ChannelFlags] = variableSizeBytesLong(varintoverflow, bytes).xmap( - bin => ChannelFlags(bin.lastOption.exists(_ % 2 == 1)), - flags => if (flags.announceChannel) ByteVector(1) else ByteVector(0) - ) - val ipv4address: Codec[Inet4Address] = bytes(4).xmap(b => InetAddress.getByAddress(b.toArray).asInstanceOf[Inet4Address], a => ByteVector(a.getAddress)) val ipv6address: Codec[Inet6Address] = bytes(16).exmap(b => Attempt.fromTry(Try(Inet6Address.getByAddress(null, b.toArray, null))), a => Attempt.fromTry(Try(ByteVector(a.getAddress)))) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecs.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecs.scala index 620c91c3ab..770b5c0839 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecs.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecs.scala @@ -115,7 +115,7 @@ object LightningMessageCodecs { ("delayedPaymentBasepoint" | publicKey) :: ("htlcBasepoint" | publicKey) :: ("firstPerCommitmentPoint" | publicKey) :: - ("channelFlags" | extendedChannelFlags) :: + ("channelFlags" | channelflags) :: ("tlvStream" | OpenDualFundedChannelTlv.openTlvCodec)).as[OpenDualFundedChannel] val acceptChannelCodec: Codec[AcceptChannel] = ( @@ -150,7 +150,6 @@ object LightningMessageCodecs { ("delayedPaymentBasepoint" | publicKey) :: ("htlcBasepoint" | publicKey) :: ("firstPerCommitmentPoint" | publicKey) :: - ("channelFlags" | extendedChannelFlags) :: ("tlvStream" | AcceptDualFundedChannelTlv.acceptTlvCodec)).as[AcceptDualFundedChannel] val fundingCreatedCodec: Codec[FundingCreated] = ( @@ -170,25 +169,19 @@ object LightningMessageCodecs { ("nextPerCommitmentPoint" | publicKey) :: ("tlvStream" | FundingLockedTlv.fundingLockedTlvCodec)).as[FundingLocked] - private val scriptSigOptCodec: Codec[Option[ByteVector]] = lengthDelimited(bytes).xmap[Option[ByteVector]]( - b => if (b.isEmpty) None else Some(b), - b => b.getOrElse(ByteVector.empty) - ) - val txAddInputCodec: Codec[TxAddInput] = ( ("channelId" | bytes32) :: ("serialId" | uint64) :: - ("previousTx" | lengthDelimited(txCodec)) :: + ("previousTx" | variableSizeBytes(uint16, txCodec)) :: ("previousTxOutput" | uint32) :: ("sequence" | uint32) :: - ("scriptSig" | scriptSigOptCodec) :: ("tlvStream" | TxAddInputTlv.txAddInputTlvCodec)).as[TxAddInput] val txAddOutputCodec: Codec[TxAddOutput] = ( ("channelId" | bytes32) :: ("serialId" | uint64) :: ("amount" | satoshi) :: - ("scriptPubKey" | lengthDelimited(bytes)) :: + ("scriptPubKey" | variableSizeBytes(uint16, bytes)) :: ("tlvStream" | TxAddOutputTlv.txAddOutputTlvCodec)).as[TxAddOutput] val txRemoveInputCodec: Codec[TxRemoveInput] = ( @@ -205,9 +198,9 @@ object LightningMessageCodecs { ("channelId" | bytes32) :: ("tlvStream" | TxCompleteTlv.txCompleteTlvCodec)).as[TxComplete] - private val witnessElementCodec: Codec[ByteVector] = lengthDelimited(bytes) - private val witnessStackCodec: Codec[ScriptWitness] = listOfN(smallvarint, witnessElementCodec).xmap(s => ScriptWitness(s.toSeq), w => w.stack.toList) - private val witnessesCodec: Codec[Seq[ScriptWitness]] = listOfN(smallvarint, witnessStackCodec).xmap(l => l.toSeq, l => l.toList) + private val witnessElementCodec: Codec[ByteVector] = variableSizeBytes(uint16, bytes) + private val witnessStackCodec: Codec[ScriptWitness] = listOfN(uint16, witnessElementCodec).xmap(s => ScriptWitness(s.toSeq), w => w.stack.toList) + private val witnessesCodec: Codec[Seq[ScriptWitness]] = listOfN(uint16, witnessStackCodec).xmap(l => l.toSeq, l => l.toList) val txSignaturesCodec: Codec[TxSignatures] = ( ("channelId" | bytes32) :: @@ -227,7 +220,7 @@ object LightningMessageCodecs { val txAbortCodec: Codec[TxAbort] = ( ("channelId" | bytes32) :: - ("data" | lengthDelimited(bytes)) :: + ("data" | variableSizeBytes(uint16, bytes)) :: ("tlvStream" | TxAbortTlv.txAbortTlvCodec)).as[TxAbort] val shutdownCodec: Codec[Shutdown] = ( diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageTypes.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageTypes.scala index c91e65964a..c614990f12 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageTypes.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageTypes.scala @@ -86,7 +86,6 @@ case class TxAddInput(channelId: ByteVector32, previousTx: Transaction, previousTxOutput: Long, sequence: Long, - scriptSig_opt: Option[ByteVector], tlvStream: TlvStream[TxAddInputTlv] = TlvStream.empty) extends InteractiveTxMessage with HasChannelId case class TxAddOutput(channelId: ByteVector32, @@ -211,7 +210,6 @@ case class AcceptDualFundedChannel(temporaryChannelId: ByteVector32, delayedPaymentBasepoint: PublicKey, htlcBasepoint: PublicKey, firstPerCommitmentPoint: PublicKey, - channelFlags: ChannelFlags, tlvStream: TlvStream[AcceptDualFundedChannelTlv] = TlvStream.empty) extends ChannelMessage with HasTemporaryChannelId { val upfrontShutdownScript_opt: Option[ByteVector] = tlvStream.get[ChannelTlv.UpfrontShutdownScriptTlv].map(_.script) val channelType_opt: Option[ChannelType] = tlvStream.get[ChannelTlv.ChannelTypeTlv].map(_.channelType) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecsSpec.scala index 317f744309..76b2f9cf4b 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecsSpec.scala @@ -166,23 +166,23 @@ class LightningMessageCodecsSpec extends AnyFunSuite { val txBin2 = hex"0200000000010142180a8812fc79a3da7fb2471eff3e22d7faee990604c2ba7f2fc8dfb15b550a0200000000feffffff030f241800000000001976a9146774040642a78ca3b8b395e70f8391b21ec026fc88ac4a155801000000001600148d2e0b57adcb8869e603fd35b5179caf053361253b1d010000000000160014e032f4f4b9f8611df0d30a20648c190c263bbc33024730440220506005aa347f5b698542cafcb4f1a10250aeb52a609d6fd67ef68f9c1a5d954302206b9bb844343f4012bccd9d08a0f5430afb9549555a3252e499be7df97aae477a012103976d6b3eea3de4b056cd88cdfd50a22daf121e0fb5c6e45ba0f40e1effbd275a00000000" val tx2 = Transaction.read(txBin2.toArray) val testCases = Seq( - TxAddInput(channelId1, UInt64(561), tx1, 1, 5, None) -> hex"0042 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000000231 f7 020000000001014ade359c5deb7c1cde2e94f401854658f97d7fa31c17ce9a831db253120a0a410100000017160014eb9a5bd79194a23d19d6ec473c768fb74f9ed32cffffffff021ca408000000000017a914946118f24bb7b37d5e9e39579e4a411e70f5b6a08763e703000000000017a9143638b2602d11f934c04abc6adb1494f69d1f14af8702473044022059ddd943b399211e4266a349f26b3289979e29f9b067792c6cfa8cc5ae25f44602204d627a5a5b603d0562e7969011fb3d64908af90a3ec7c876eaa9baf61e1958af012102f5188df1da92ed818581c29778047800ed6635788aa09d9469f7d17628f7323300000000 00000001 00000005 00", - TxAddInput(channelId2, UInt64(0), tx2, 2, 0, None) -> hex"0042 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 0000000000000000 fd0100 0200000000010142180a8812fc79a3da7fb2471eff3e22d7faee990604c2ba7f2fc8dfb15b550a0200000000feffffff030f241800000000001976a9146774040642a78ca3b8b395e70f8391b21ec026fc88ac4a155801000000001600148d2e0b57adcb8869e603fd35b5179caf053361253b1d010000000000160014e032f4f4b9f8611df0d30a20648c190c263bbc33024730440220506005aa347f5b698542cafcb4f1a10250aeb52a609d6fd67ef68f9c1a5d954302206b9bb844343f4012bccd9d08a0f5430afb9549555a3252e499be7df97aae477a012103976d6b3eea3de4b056cd88cdfd50a22daf121e0fb5c6e45ba0f40e1effbd275a00000000 00000002 00000000 00", - TxAddInput(channelId1, UInt64(561), tx1, 0, 0, Some(hex"00bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb")) -> hex"0042 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000000231 f7 020000000001014ade359c5deb7c1cde2e94f401854658f97d7fa31c17ce9a831db253120a0a410100000017160014eb9a5bd79194a23d19d6ec473c768fb74f9ed32cffffffff021ca408000000000017a914946118f24bb7b37d5e9e39579e4a411e70f5b6a08763e703000000000017a9143638b2602d11f934c04abc6adb1494f69d1f14af8702473044022059ddd943b399211e4266a349f26b3289979e29f9b067792c6cfa8cc5ae25f44602204d627a5a5b603d0562e7969011fb3d64908af90a3ec7c876eaa9baf61e1958af012102f5188df1da92ed818581c29778047800ed6635788aa09d9469f7d17628f7323300000000 00000000 00000000 15 00bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", - TxAddOutput(channelId1, UInt64(1105), 2047 sat, hex"00149357014afd0ccd265658c9ae81efa995e771f472") -> hex"0043 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000000451 00000000000007ff 16 00149357014afd0ccd265658c9ae81efa995e771f472", - TxAddOutput(channelId1, UInt64(1105), 2047 sat, hex"00149357014afd0ccd265658c9ae81efa995e771f472", TlvStream(Nil, Seq(GenericTlv(UInt64(301), hex"2a")))) -> hex"0043 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000000451 00000000000007ff 16 00149357014afd0ccd265658c9ae81efa995e771f472 fd012d012a", + TxAddInput(channelId1, UInt64(561), tx1, 1, 5) -> hex"0042 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000000231 00f7 020000000001014ade359c5deb7c1cde2e94f401854658f97d7fa31c17ce9a831db253120a0a410100000017160014eb9a5bd79194a23d19d6ec473c768fb74f9ed32cffffffff021ca408000000000017a914946118f24bb7b37d5e9e39579e4a411e70f5b6a08763e703000000000017a9143638b2602d11f934c04abc6adb1494f69d1f14af8702473044022059ddd943b399211e4266a349f26b3289979e29f9b067792c6cfa8cc5ae25f44602204d627a5a5b603d0562e7969011fb3d64908af90a3ec7c876eaa9baf61e1958af012102f5188df1da92ed818581c29778047800ed6635788aa09d9469f7d17628f7323300000000 00000001 00000005", + TxAddInput(channelId2, UInt64(0), tx2, 2, 0) -> hex"0042 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 0000000000000000 0100 0200000000010142180a8812fc79a3da7fb2471eff3e22d7faee990604c2ba7f2fc8dfb15b550a0200000000feffffff030f241800000000001976a9146774040642a78ca3b8b395e70f8391b21ec026fc88ac4a155801000000001600148d2e0b57adcb8869e603fd35b5179caf053361253b1d010000000000160014e032f4f4b9f8611df0d30a20648c190c263bbc33024730440220506005aa347f5b698542cafcb4f1a10250aeb52a609d6fd67ef68f9c1a5d954302206b9bb844343f4012bccd9d08a0f5430afb9549555a3252e499be7df97aae477a012103976d6b3eea3de4b056cd88cdfd50a22daf121e0fb5c6e45ba0f40e1effbd275a00000000 00000002 00000000", + TxAddInput(channelId1, UInt64(561), tx1, 0, 0) -> hex"0042 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000000231 00f7 020000000001014ade359c5deb7c1cde2e94f401854658f97d7fa31c17ce9a831db253120a0a410100000017160014eb9a5bd79194a23d19d6ec473c768fb74f9ed32cffffffff021ca408000000000017a914946118f24bb7b37d5e9e39579e4a411e70f5b6a08763e703000000000017a9143638b2602d11f934c04abc6adb1494f69d1f14af8702473044022059ddd943b399211e4266a349f26b3289979e29f9b067792c6cfa8cc5ae25f44602204d627a5a5b603d0562e7969011fb3d64908af90a3ec7c876eaa9baf61e1958af012102f5188df1da92ed818581c29778047800ed6635788aa09d9469f7d17628f7323300000000 00000000 00000000", + TxAddOutput(channelId1, UInt64(1105), 2047 sat, hex"00149357014afd0ccd265658c9ae81efa995e771f472") -> hex"0043 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000000451 00000000000007ff 0016 00149357014afd0ccd265658c9ae81efa995e771f472", + TxAddOutput(channelId1, UInt64(1105), 2047 sat, hex"00149357014afd0ccd265658c9ae81efa995e771f472", TlvStream(Nil, Seq(GenericTlv(UInt64(301), hex"2a")))) -> hex"0043 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000000451 00000000000007ff 0016 00149357014afd0ccd265658c9ae81efa995e771f472 fd012d012a", TxRemoveInput(channelId2, UInt64(561)) -> hex"0044 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 0000000000000231", TxRemoveOutput(channelId1, UInt64(1)) -> hex"0045 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000000001", TxComplete(channelId1) -> hex"0046 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", TxComplete(channelId1, TlvStream(Nil, Seq(GenericTlv(UInt64(231), hex"deadbeef"), GenericTlv(UInt64(507), hex"")))) -> hex"0046 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa e704deadbeef fd01fb00", - TxSignatures(channelId1, tx2.txid, Seq(ScriptWitness(Seq(hex"dead", hex"beef")), ScriptWitness(Seq(hex"", hex"01010101", hex"", hex"02")))) -> hex"0047 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa f169ed4bcb4ca97646845ec063d4deddcbe704f77f1b2c205929195f84a87afc 02 0202dead02beef 04 000401010101000102", - TxSignatures(channelId2, tx1.txid, Nil) -> hex"0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 06f125a8ef64eb5a25826190dc28f15b85dc1adcfc7a178eef393ea325c02e1f 00", + TxSignatures(channelId1, tx2.txid, Seq(ScriptWitness(Seq(hex"dead", hex"beef")), ScriptWitness(Seq(hex"", hex"01010101", hex"", hex"02")))) -> hex"0047 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa f169ed4bcb4ca97646845ec063d4deddcbe704f77f1b2c205929195f84a87afc 0002 00020002dead0002beef 0004 00000004010101010000000102", + TxSignatures(channelId2, tx1.txid, Nil) -> hex"0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 06f125a8ef64eb5a25826190dc28f15b85dc1adcfc7a178eef393ea325c02e1f 0000", TxInitRbf(channelId1, 8388607, FeeratePerKw(4000 sat)) -> hex"0048 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 007fffff 00000fa0", TxInitRbf(channelId1, 0, FeeratePerKw(4000 sat), TlvStream(SharedOutputContributionTlv(5000 sat))) -> hex"0048 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00000000 00000fa0 00021388", TxAckRbf(channelId2) -> hex"0049 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", TxAckRbf(channelId2, TlvStream(SharedOutputContributionTlv(450000 sat))) -> hex"0049 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 000306ddd0", - TxAbort(channelId1, hex"") -> hex"004a aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00", - TxAbort(channelId1, ByteVector.view("internal error".getBytes(Charsets.US_ASCII))) -> hex"004a aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0e 696e7465726e616c206572726f72", + TxAbort(channelId1, hex"") -> hex"004a aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000", + TxAbort(channelId1, ByteVector.view("internal error".getBytes(Charsets.US_ASCII))) -> hex"004a aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 000e 696e7465726e616c206572726f72", ) testCases.foreach { case (message, bin) => val decoded = lightningMessageCodec.decode(bin.bits).require @@ -252,10 +252,10 @@ class LightningMessageCodecsSpec extends AnyFunSuite { test("encode/decode open_channel (dual funding)") { val defaultOpen = OpenDualFundedChannel(ByteVector32.Zeroes, ByteVector32.One, FeeratePerKw(5000 sat), FeeratePerKw(4000 sat), 250_000 sat, 500 sat, UInt64(50_000), 15 msat, CltvExpiryDelta(144), 483, 650_000, publicKey(1), publicKey(2), publicKey(3), publicKey(4), publicKey(5), publicKey(6), ChannelFlags(true)) - val defaultEncoded = hex"0040 0000000000000000000000000000000000000000000000000000000000000000 0100000000000000000000000000000000000000000000000000000000000000 00001388 00000fa0 000000000003d090 00000000000001f4 000000000000c350 000000000000000f 0090 01e3 0009eb10 031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f 024d4b6cd1361032ca9bd2aeb9d900aa4d45d9ead80ac9423374c451a7254d0766 02531fe6068134503d2723133227c867ac8fa6c83c537e9a44c3c5bdbdcb1fe337 03462779ad4aad39514614751a71085f2f10e1c7a593e4e030efb5b8721ce55b0b 0362c0a046dacce86ddd0343c6d3c7c79c2208ba0d9c9cf24a6d046d21d21f90f7 03f006a18d5653c4edf5391ff23a61f03ff83d237e880ee61187fa9f379a028e0a 0101" + val defaultEncoded = hex"0040 0000000000000000000000000000000000000000000000000000000000000000 0100000000000000000000000000000000000000000000000000000000000000 00001388 00000fa0 000000000003d090 00000000000001f4 000000000000c350 000000000000000f 0090 01e3 0009eb10 031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f 024d4b6cd1361032ca9bd2aeb9d900aa4d45d9ead80ac9423374c451a7254d0766 02531fe6068134503d2723133227c867ac8fa6c83c537e9a44c3c5bdbdcb1fe337 03462779ad4aad39514614751a71085f2f10e1c7a593e4e030efb5b8721ce55b0b 0362c0a046dacce86ddd0343c6d3c7c79c2208ba0d9c9cf24a6d046d21d21f90f7 03f006a18d5653c4edf5391ff23a61f03ff83d237e880ee61187fa9f379a028e0a 01" val testCases = Seq( defaultOpen -> defaultEncoded, - defaultOpen.copy(channelFlags = ChannelFlags(false), tlvStream = TlvStream(ChannelTypeTlv(ChannelTypes.AnchorOutputsZeroFeeHtlcTx))) -> (defaultEncoded.dropRight(2) ++ hex"0100" ++ hex"0103401000"), + defaultOpen.copy(tlvStream = TlvStream(ChannelTypeTlv(ChannelTypes.AnchorOutputsZeroFeeHtlcTx))) -> (defaultEncoded ++ hex"0103401000"), defaultOpen.copy(tlvStream = TlvStream(UpfrontShutdownScriptTlv(hex"00143adb2d0445c4d491cc7568b10323bd6615a91283"), ChannelTypeTlv(ChannelTypes.AnchorOutputsZeroFeeHtlcTx))) -> (defaultEncoded ++ hex"001600143adb2d0445c4d491cc7568b10323bd6615a91283 0103401000"), ) testCases.foreach { case (open, bin) => @@ -271,8 +271,8 @@ class LightningMessageCodecsSpec extends AnyFunSuite { val defaultEncodedWithoutFlags = hex"0040 0000000000000000000000000000000000000000000000000000000000000000 0100000000000000000000000000000000000000000000000000000000000000 00001388 00000fa0 000000000003d090 00000000000001f4 000000000000c350 000000000000000f 0090 01e3 0009eb10 031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f 024d4b6cd1361032ca9bd2aeb9d900aa4d45d9ead80ac9423374c451a7254d0766 02531fe6068134503d2723133227c867ac8fa6c83c537e9a44c3c5bdbdcb1fe337 03462779ad4aad39514614751a71085f2f10e1c7a593e4e030efb5b8721ce55b0b 0362c0a046dacce86ddd0343c6d3c7c79c2208ba0d9c9cf24a6d046d21d21f90f7 03f006a18d5653c4edf5391ff23a61f03ff83d237e880ee61187fa9f379a028e0a" val testCases = Seq( defaultEncodedWithoutFlags ++ hex"00" -> ChannelFlags(false), - defaultEncodedWithoutFlags ++ hex"01a2" -> ChannelFlags(false), - defaultEncodedWithoutFlags ++ hex"037a2a1f" -> ChannelFlags(true), + defaultEncodedWithoutFlags ++ hex"a2" -> ChannelFlags(false), + defaultEncodedWithoutFlags ++ hex"ff" -> ChannelFlags(true), ) testCases.foreach { case (bin, flags) => val decoded = lightningMessageCodec.decode(bin.bits).require.value @@ -307,11 +307,10 @@ class LightningMessageCodecsSpec extends AnyFunSuite { } test("encode/decode accept_channel (dual funding)") { - val defaultAccept = AcceptDualFundedChannel(ByteVector32.One, 50_000 sat, 473 sat, UInt64(100_000_000), 1 msat, 6, CltvExpiryDelta(144), 50, publicKey(1), point(2), point(3), point(4), point(5), point(6), ChannelFlags(true)) - val defaultEncoded = hex"0041 0100000000000000000000000000000000000000000000000000000000000000 000000000000c350 00000000000001d9 0000000005f5e100 0000000000000001 00000006 0090 0032 031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f 024d4b6cd1361032ca9bd2aeb9d900aa4d45d9ead80ac9423374c451a7254d0766 02531fe6068134503d2723133227c867ac8fa6c83c537e9a44c3c5bdbdcb1fe337 03462779ad4aad39514614751a71085f2f10e1c7a593e4e030efb5b8721ce55b0b 0362c0a046dacce86ddd0343c6d3c7c79c2208ba0d9c9cf24a6d046d21d21f90f7 03f006a18d5653c4edf5391ff23a61f03ff83d237e880ee61187fa9f379a028e0a 0101" + val defaultAccept = AcceptDualFundedChannel(ByteVector32.One, 50_000 sat, 473 sat, UInt64(100_000_000), 1 msat, 6, CltvExpiryDelta(144), 50, publicKey(1), point(2), point(3), point(4), point(5), point(6)) + val defaultEncoded = hex"0041 0100000000000000000000000000000000000000000000000000000000000000 000000000000c350 00000000000001d9 0000000005f5e100 0000000000000001 00000006 0090 0032 031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f 024d4b6cd1361032ca9bd2aeb9d900aa4d45d9ead80ac9423374c451a7254d0766 02531fe6068134503d2723133227c867ac8fa6c83c537e9a44c3c5bdbdcb1fe337 03462779ad4aad39514614751a71085f2f10e1c7a593e4e030efb5b8721ce55b0b 0362c0a046dacce86ddd0343c6d3c7c79c2208ba0d9c9cf24a6d046d21d21f90f7 03f006a18d5653c4edf5391ff23a61f03ff83d237e880ee61187fa9f379a028e0a" val testCases = Seq( defaultAccept -> defaultEncoded, - defaultAccept.copy(channelFlags = ChannelFlags(false), tlvStream = TlvStream(UpfrontShutdownScriptTlv(hex"00143adb2d0445c4d491cc7568b10323bd6615a91283"))) -> (defaultEncoded.dropRight(2) ++ hex"0100" ++ hex"001600143adb2d0445c4d491cc7568b10323bd6615a91283"), defaultAccept.copy(tlvStream = TlvStream(ChannelTypeTlv(ChannelTypes.StaticRemoteKey))) -> (defaultEncoded ++ hex"01021000"), ) testCases.foreach { case (accept, bin) => @@ -322,20 +321,6 @@ class LightningMessageCodecsSpec extends AnyFunSuite { } } - test("decode accept_channel with unknown channel flags (dual funding)") { - val defaultAccept = AcceptDualFundedChannel(ByteVector32.One, 50_000 sat, 473 sat, UInt64(100_000_000), 1 msat, 6, CltvExpiryDelta(144), 50, publicKey(1), point(2), point(3), point(4), point(5), point(6), ChannelFlags(true)) - val defaultEncodedWithoutFlags = hex"0041 0100000000000000000000000000000000000000000000000000000000000000 000000000000c350 00000000000001d9 0000000005f5e100 0000000000000001 00000006 0090 0032 031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f 024d4b6cd1361032ca9bd2aeb9d900aa4d45d9ead80ac9423374c451a7254d0766 02531fe6068134503d2723133227c867ac8fa6c83c537e9a44c3c5bdbdcb1fe337 03462779ad4aad39514614751a71085f2f10e1c7a593e4e030efb5b8721ce55b0b 0362c0a046dacce86ddd0343c6d3c7c79c2208ba0d9c9cf24a6d046d21d21f90f7 03f006a18d5653c4edf5391ff23a61f03ff83d237e880ee61187fa9f379a028e0a" - val testCases = Seq( - defaultEncodedWithoutFlags ++ hex"00" -> ChannelFlags(false), - defaultEncodedWithoutFlags ++ hex"01a2" -> ChannelFlags(false), - defaultEncodedWithoutFlags ++ hex"037a2a1f" -> ChannelFlags(true), - ) - testCases.foreach { case (bin, flags) => - val decoded = lightningMessageCodec.decode(bin.bits).require.value - assert(decoded === defaultAccept.copy(channelFlags = flags)) - } - } - test("encode/decode closing_signed") { val defaultSig = ByteVector64(hex"01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101") val testCases = Seq( From 6fa02a0ec18264d082dc6148fafb1ac22315aef6 Mon Sep 17 00:00:00 2001 From: Pierre-Marie Padiou Date: Thu, 19 May 2022 10:55:47 +0200 Subject: [PATCH 073/121] Use long id instead of short id in `ChannelRelay` (#2221) This is a preparation for https://github.com/lightning/bolts/pull/910. The relaying logic is now based on the `channelId` instead of `shortChannelId`. We used to consider `shortChannelId` as a unique identifier for the channel, but it's not true anymore: there will be multiple aliases per channel. --- .../eclair/payment/relay/ChannelRelay.scala | 149 +++++++++--------- .../eclair/payment/relay/ChannelRelayer.scala | 68 ++++---- .../acinq/eclair/payment/relay/Relayer.scala | 2 + .../payment/relay/ChannelRelayerSpec.scala | 56 ++++--- .../eclair/payment/relay/RelayerSpec.scala | 2 +- 5 files changed, 138 insertions(+), 139 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/ChannelRelay.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/ChannelRelay.scala index 818ab5b80b..bcec91b1d0 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/ChannelRelay.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/ChannelRelay.scala @@ -28,7 +28,7 @@ import fr.acinq.eclair.payment.Monitoring.{Metrics, Tags} import fr.acinq.eclair.payment.relay.Relayer.OutgoingChannel import fr.acinq.eclair.payment.{ChannelPaymentRelayed, IncomingPaymentPacket} import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{Logs, NodeParams, ShortChannelId, TimestampSecond, channel, nodeFee} +import fr.acinq.eclair.{Logs, NodeParams, TimestampSecond, channel, nodeFee} import java.util.UUID @@ -37,17 +37,17 @@ object ChannelRelay { // @formatter:off sealed trait Command private case object DoRelay extends Command - private case class WrappedForwardShortIdFailure(failure: Register.ForwardShortIdFailure[CMD_ADD_HTLC]) extends Command + private case class WrappedForwardFailure(failure: Register.ForwardFailure[CMD_ADD_HTLC]) extends Command private case class WrappedAddResponse(res: CommandResponse[CMD_ADD_HTLC]) extends Command // @formatter:on // @formatter:off sealed trait RelayResult case class RelayFailure(cmdFail: CMD_FAIL_HTLC) extends RelayResult - case class RelaySuccess(shortChannelId: ShortChannelId, cmdAdd: CMD_ADD_HTLC) extends RelayResult + case class RelaySuccess(selectedChannelId: ByteVector32, cmdAdd: CMD_ADD_HTLC) extends RelayResult // @formatter:on - def apply(nodeParams: NodeParams, register: ActorRef, channels: Map[ShortChannelId, Relayer.OutgoingChannel], relayId: UUID, r: IncomingPaymentPacket.ChannelRelayPacket): Behavior[Command] = + def apply(nodeParams: NodeParams, register: ActorRef, channels: Map[ByteVector32, Relayer.OutgoingChannel], relayId: UUID, r: IncomingPaymentPacket.ChannelRelayPacket): Behavior[Command] = Behaviors.setup { context => Behaviors.withMdc(Logs.mdc( category_opt = Some(Logs.LogCategory.PAYMENT), @@ -92,16 +92,16 @@ object ChannelRelay { */ class ChannelRelay private(nodeParams: NodeParams, register: ActorRef, - channels: Map[ShortChannelId, Relayer.OutgoingChannel], + channels: Map[ByteVector32, Relayer.OutgoingChannel], r: IncomingPaymentPacket.ChannelRelayPacket, context: ActorContext[ChannelRelay.Command]) { import ChannelRelay._ - private val forwardShortIdAdapter = context.messageAdapter[Register.ForwardShortIdFailure[CMD_ADD_HTLC]](WrappedForwardShortIdFailure) + private val forwardFailureAdapter = context.messageAdapter[Register.ForwardFailure[CMD_ADD_HTLC]](WrappedForwardFailure) private val addResponseAdapter = context.messageAdapter[CommandResponse[CMD_ADD_HTLC]](WrappedAddResponse) - private case class PreviouslyTried(shortChannelId: ShortChannelId, failure: RES_ADD_FAILED[ChannelException]) + private case class PreviouslyTried(channelId: ByteVector32, failure: RES_ADD_FAILED[ChannelException]) def relay(previousFailures: Seq[PreviouslyTried]): Behavior[Command] = { Behaviors.receiveMessagePartial { @@ -115,18 +115,18 @@ class ChannelRelay private(nodeParams: NodeParams, Metrics.recordPaymentRelayFailed(Tags.FailureType(cmdFail), Tags.RelayType.Channel) context.log.info(s"rejecting htlc reason=${cmdFail.reason}") safeSendAndStop(r.add.channelId, cmdFail) - case RelaySuccess(selectedShortChannelId, cmdAdd) => - context.log.info(s"forwarding htlc to shortChannelId=$selectedShortChannelId") - register ! Register.ForwardShortId(forwardShortIdAdapter.toClassic, selectedShortChannelId, cmdAdd) - waitForAddResponse(selectedShortChannelId, previousFailures) + case RelaySuccess(selectedChannelId, cmdAdd) => + context.log.info(s"forwarding htlc to channelId=$selectedChannelId") + register ! Register.Forward(forwardFailureAdapter.toClassic, selectedChannelId, cmdAdd) + waitForAddResponse(selectedChannelId, previousFailures) } } } - def waitForAddResponse(selectedShortChannelId: ShortChannelId, previousFailures: Seq[PreviouslyTried]): Behavior[Command] = + def waitForAddResponse(selectedChannelId: ByteVector32, previousFailures: Seq[PreviouslyTried]): Behavior[Command] = Behaviors.receiveMessagePartial { - case WrappedForwardShortIdFailure(Register.ForwardShortIdFailure(Register.ForwardShortId(_, shortChannelId, CMD_ADD_HTLC(_, _, _, _, _, o: Origin.ChannelRelayedHot, _)))) => - context.log.warn(s"couldn't resolve downstream channel $shortChannelId, failing htlc #${o.add.id}") + case WrappedForwardFailure(Register.ForwardFailure(Register.Forward(_, channelId, CMD_ADD_HTLC(_, _, _, _, _, o: Origin.ChannelRelayedHot, _)))) => + context.log.warn(s"couldn't resolve downstream channel $channelId, failing htlc #${o.add.id}") val cmdFail = CMD_FAIL_HTLC(o.add.id, Right(UnknownNextPeer), commit = true) Metrics.recordPaymentRelayFailed(Tags.FailureType(cmdFail), Tags.RelayType.Channel) safeSendAndStop(o.add.channelId, cmdFail) @@ -134,7 +134,7 @@ class ChannelRelay private(nodeParams: NodeParams, case WrappedAddResponse(addFailed@RES_ADD_FAILED(CMD_ADD_HTLC(_, _, _, _, _, _: Origin.ChannelRelayedHot, _), _, _)) => context.log.info("attempt failed with reason={}", addFailed.t.getClass.getSimpleName) context.self ! DoRelay - relay(previousFailures :+ PreviouslyTried(selectedShortChannelId, addFailed)) + relay(previousFailures :+ PreviouslyTried(selectedChannelId, addFailed)) case WrappedAddResponse(_: RES_SUCCESS[_]) => context.log.debug("sent htlc to the downstream channel") @@ -170,14 +170,13 @@ class ChannelRelay private(nodeParams: NodeParams, * - a CMD_ADD_HTLC to propagate downstream */ def handleRelay(previousFailures: Seq[PreviouslyTried]): RelayResult = { - val alreadyTried = previousFailures.map(_.shortChannelId) - selectPreferredChannel(alreadyTried) - .flatMap(selectedShortChannelId => channels.get(selectedShortChannelId)) match { + val alreadyTried = previousFailures.map(_.channelId) + selectPreferredChannel(alreadyTried) match { case None if previousFailures.nonEmpty => // no more channels to try val error = previousFailures // we return the error for the initially requested channel if it exists - .find(_.shortChannelId == r.payload.outgoingChannelId) + .find(failure => requestedChannelId_opt.contains(failure.channelId)) // otherwise we return the error for the first channel tried .getOrElse(previousFailures.head) .failure @@ -190,58 +189,64 @@ class ChannelRelay private(nodeParams: NodeParams, /** all the channels point to the same next node, we take the first one */ private val nextNodeId_opt = channels.headOption.map(_._2.nextNodeId) + /** channel id explicitly requested in the onion payload */ + private val requestedChannelId_opt = channels.find(_._2.channelUpdate.shortChannelId == r.payload.outgoingChannelId).map(_._1) + /** * Select a channel to the same node to relay the payment to, that has the lowest capacity and balance and is * compatible in terms of fees, expiry_delta, etc. * * If no suitable channel is found we default to the originally requested channel. */ - def selectPreferredChannel(alreadyTried: Seq[ShortChannelId]): Option[ShortChannelId] = { + def selectPreferredChannel(alreadyTried: Seq[ByteVector32]): Option[OutgoingChannel] = { val requestedShortChannelId = r.payload.outgoingChannelId context.log.debug("selecting next channel with requestedShortChannelId={}", requestedShortChannelId) - nextNodeId_opt match { - case Some(_) => - // we then filter out channels that we have already tried - val candidateChannels: Map[ShortChannelId, OutgoingChannel] = channels -- alreadyTried - // and we filter again to keep the ones that are compatible with this payment (mainly fees, expiry delta) - candidateChannels - .map { case (shortChannelId, channelInfo) => - val relayResult = relayOrFail(Some(channelInfo)) - context.log.debug(s"candidate channel: shortChannelId=$shortChannelId availableForSend={} capacity={} channelUpdate={} result={}", - channelInfo.commitments.availableBalanceForSend, - channelInfo.commitments.capacity, - channelInfo.channelUpdate, - relayResult match { - case _: RelaySuccess => "success" - case RelayFailure(CMD_FAIL_HTLC(_, Right(failureReason), _, _)) => failureReason - case other => other - }) - (shortChannelId, channelInfo, relayResult) - } - .collect { - // we only keep channels that have enough balance to handle this payment - case (shortChannelId, channelInfo, _: RelaySuccess) if channelInfo.commitments.availableBalanceForSend > r.payload.amountToForward => (shortChannelId, channelInfo.commitments) - } - .toList // needed for ordering - // we want to use the channel with: - // - the lowest available capacity to ensure we keep high-capacity channels for big payments - // - the lowest available balance to increase our incoming liquidity - .sortBy { case (_, commitments) => (commitments.capacity, commitments.availableBalanceForSend) } - .headOption match { - case Some((preferredShortChannelId, commitments)) if preferredShortChannelId != requestedShortChannelId => - context.log.info("replacing requestedShortChannelId={} by preferredShortChannelId={} with availableBalanceMsat={}", requestedShortChannelId, preferredShortChannelId, commitments.availableBalanceForSend) - Some(preferredShortChannelId) - case Some(_) => - context.log.debug("requested short channel id is our preferred channel") - Some(requestedShortChannelId) - case None if !alreadyTried.contains(requestedShortChannelId) => - context.log.debug("no channel seems to work for this payment, we will try to use the requested short channel id") - Some(requestedShortChannelId) - case None => + // we filter out channels that we have already tried + val candidateChannels: Map[ByteVector32, OutgoingChannel] = channels -- alreadyTried + // and we filter again to keep the ones that are compatible with this payment (mainly fees, expiry delta) + candidateChannels + .values + .map { channel => + val relayResult = relayOrFail(Some(channel)) + context.log.debug(s"candidate channel: channelId=${channel.channelId} availableForSend={} capacity={} channelUpdate={} result={}", + channel.commitments.availableBalanceForSend, + channel.commitments.capacity, + channel.channelUpdate, + relayResult match { + case _: RelaySuccess => "success" + case RelayFailure(CMD_FAIL_HTLC(_, Right(failureReason), _, _)) => failureReason + case other => other + }) + (channel, relayResult) + } + .collect { + // we only keep channels that have enough balance to handle this payment + case (channel, _: RelaySuccess) if channel.commitments.availableBalanceForSend > r.payload.amountToForward => channel + } + .toList // needed for ordering + // we want to use the channel with: + // - the lowest available capacity to ensure we keep high-capacity channels for big payments + // - the lowest available balance to increase our incoming liquidity + .sortBy { channel => (channel.commitments.capacity, channel.commitments.availableBalanceForSend) } + .headOption match { + case Some(channel) => + if (requestedChannelId_opt.contains(channel.channelId)) { + context.log.debug("requested short channel id is our preferred channel") + Some(channel) + } else { + context.log.info("replacing requestedShortChannelId={} by preferredShortChannelId={} with availableBalanceMsat={}", requestedShortChannelId, channel.channelUpdate.shortChannelId, channel.commitments.availableBalanceForSend) + Some(channel) + } + case None => + val requestedChannel_opt = requestedChannelId_opt.flatMap(channels.get) + requestedChannel_opt match { + case Some(requestedChannel) if alreadyTried.contains(requestedChannel.channelId) => context.log.debug("no channel seems to work for this payment and we have already tried the requested channel id: giving up") None + case _ => + context.log.debug("no channel seems to work for this payment, we will try to use the one requested") + requestedChannel_opt } - case _ => Some(requestedShortChannelId) // we don't have a channel_update for this short_channel_id } } @@ -252,23 +257,23 @@ class ChannelRelay private(nodeParams: NodeParams, */ def relayOrFail(outgoingChannel_opt: Option[OutgoingChannel]): RelayResult = { import r._ - outgoingChannel_opt.map(_.channelUpdate) match { + outgoingChannel_opt match { case None => RelayFailure(CMD_FAIL_HTLC(add.id, Right(UnknownNextPeer), commit = true)) - case Some(channelUpdate) if !channelUpdate.channelFlags.isEnabled => - RelayFailure(CMD_FAIL_HTLC(add.id, Right(ChannelDisabled(channelUpdate.messageFlags, channelUpdate.channelFlags, channelUpdate)), commit = true)) - case Some(channelUpdate) if payload.amountToForward < channelUpdate.htlcMinimumMsat => - RelayFailure(CMD_FAIL_HTLC(add.id, Right(AmountBelowMinimum(payload.amountToForward, channelUpdate)), commit = true)) - case Some(channelUpdate) if r.expiryDelta < channelUpdate.cltvExpiryDelta => - RelayFailure(CMD_FAIL_HTLC(add.id, Right(IncorrectCltvExpiry(payload.outgoingCltv, channelUpdate)), commit = true)) - case Some(channelUpdate) if r.relayFeeMsat < nodeFee(channelUpdate.relayFees, payload.amountToForward) && + case Some(c) if !c.channelUpdate.channelFlags.isEnabled => + RelayFailure(CMD_FAIL_HTLC(add.id, Right(ChannelDisabled(c.channelUpdate.messageFlags, c.channelUpdate.channelFlags, c.channelUpdate)), commit = true)) + case Some(c) if payload.amountToForward < c.channelUpdate.htlcMinimumMsat => + RelayFailure(CMD_FAIL_HTLC(add.id, Right(AmountBelowMinimum(payload.amountToForward, c.channelUpdate)), commit = true)) + case Some(c) if r.expiryDelta < c.channelUpdate.cltvExpiryDelta => + RelayFailure(CMD_FAIL_HTLC(add.id, Right(IncorrectCltvExpiry(payload.outgoingCltv, c.channelUpdate)), commit = true)) + case Some(c) if r.relayFeeMsat < nodeFee(c.channelUpdate.relayFees, payload.amountToForward) && // fees also do not satisfy the previous channel update for `enforcementDelay` seconds after current update - (TimestampSecond.now() - channelUpdate.timestamp > nodeParams.relayParams.enforcementDelay || + (TimestampSecond.now() - c.channelUpdate.timestamp > nodeParams.relayParams.enforcementDelay || outgoingChannel_opt.flatMap(_.prevChannelUpdate).forall(c => r.relayFeeMsat < nodeFee(c.relayFees, payload.amountToForward))) => - RelayFailure(CMD_FAIL_HTLC(add.id, Right(FeeInsufficient(add.amountMsat, channelUpdate)), commit = true)) - case Some(channelUpdate) => + RelayFailure(CMD_FAIL_HTLC(add.id, Right(FeeInsufficient(add.amountMsat, c.channelUpdate)), commit = true)) + case Some(c) => val origin = Origin.ChannelRelayedHot(addResponseAdapter.toClassic, add, payload.amountToForward) - RelaySuccess(channelUpdate.shortChannelId, CMD_ADD_HTLC(addResponseAdapter.toClassic, payload.amountToForward, add.paymentHash, payload.outgoingCltv, nextPacket, origin, commit = true)) + RelaySuccess(c.channelId, CMD_ADD_HTLC(addResponseAdapter.toClassic, payload.amountToForward, add.paymentHash, payload.outgoingCltv, nextPacket, origin, commit = true)) } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/ChannelRelayer.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/ChannelRelayer.scala index 14abc832bf..f874479681 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/ChannelRelayer.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/ChannelRelayer.scala @@ -20,6 +20,7 @@ import akka.actor.ActorRef import akka.actor.typed.Behavior import akka.actor.typed.eventstream.EventStream import akka.actor.typed.scaladsl.Behaviors +import fr.acinq.bitcoin.scalacompat.ByteVector32 import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.eclair.channel._ import fr.acinq.eclair.payment.IncomingPaymentPacket @@ -53,13 +54,11 @@ object ChannelRelayer { case _ => Map.empty } - private type ChannelUpdates = Map[ShortChannelId, Relayer.OutgoingChannel] - private type NodeChannels = mutable.MultiDict[PublicKey, ShortChannelId] - def apply(nodeParams: NodeParams, register: ActorRef, - channelUpdates: ChannelUpdates = Map.empty, - node2channels: NodeChannels = mutable.MultiDict.empty[PublicKey, ShortChannelId]): Behavior[Command] = + channels: Map[ByteVector32, Relayer.OutgoingChannel] = Map.empty, + scid2channels: Map[ShortChannelId, ByteVector32] = Map.empty, + node2channels: mutable.MultiDict[PublicKey, ByteVector32] = mutable.MultiDict.empty): Behavior[Command] = Behaviors.setup { context => context.system.eventStream ! EventStream.Subscribe(context.messageAdapter[LocalChannelUpdate](WrappedLocalChannelUpdate)) context.system.eventStream ! EventStream.Subscribe(context.messageAdapter[LocalChannelDown](WrappedLocalChannelDown)) @@ -70,61 +69,56 @@ object ChannelRelayer { Behaviors.receiveMessage { case Relay(channelRelayPacket) => val relayId = UUID.randomUUID() - val nextNodeId_opt: Option[PublicKey] = channelUpdates.get(channelRelayPacket.payload.outgoingChannelId) match { - case Some(channel) => Some(channel.nextNodeId) + val nextNodeId_opt: Option[PublicKey] = scid2channels.get(channelRelayPacket.payload.outgoingChannelId) match { + case Some(channelId) => channels.get(channelId).map(_.nextNodeId) case None => None } - val channels: Map[ShortChannelId, Relayer.OutgoingChannel] = nextNodeId_opt match { - case Some(nextNodeId) => node2channels.get(nextNodeId).map(channelUpdates).map(c => c.channelUpdate.shortChannelId -> c).toMap + val nextChannels: Map[ByteVector32, Relayer.OutgoingChannel] = nextNodeId_opt match { + case Some(nextNodeId) => node2channels.get(nextNodeId).flatMap(channels.get).map(c => c.channelId -> c).toMap case None => Map.empty } - context.log.debug(s"spawning a new handler with relayId=$relayId to nextNodeId={} with channels={}", nextNodeId_opt.getOrElse(""), channels.keys.mkString(",")) - context.spawn(ChannelRelay.apply(nodeParams, register, channels, relayId, channelRelayPacket), name = relayId.toString) + context.log.debug(s"spawning a new handler with relayId=$relayId to nextNodeId={} with channels={}", nextNodeId_opt.getOrElse(""), nextChannels.keys.mkString(",")) + context.spawn(ChannelRelay.apply(nodeParams, register, nextChannels, relayId, channelRelayPacket), name = relayId.toString) Behaviors.same case GetOutgoingChannels(replyTo, Relayer.GetOutgoingChannels(enabledOnly)) => - val channels = if (enabledOnly) { - channelUpdates.values.filter(o => o.channelUpdate.channelFlags.isEnabled) + val selected = if (enabledOnly) { + channels.values.filter(o => o.channelUpdate.channelFlags.isEnabled) } else { - channelUpdates.values + channels.values } - replyTo ! Relayer.OutgoingChannels(channels.toSeq) + replyTo ! Relayer.OutgoingChannels(selected.toSeq) Behaviors.same case WrappedLocalChannelUpdate(LocalChannelUpdate(_, channelId, shortChannelId, remoteNodeId, _, channelUpdate, commitments)) => context.log.debug(s"updating local channel info for channelId=$channelId shortChannelId=$shortChannelId remoteNodeId=$remoteNodeId channelUpdate={} commitments={}", channelUpdate, commitments) - val prevChannelUpdate = channelUpdates.get(shortChannelId).map(_.channelUpdate) - val channelUpdates1 = channelUpdates + (channelUpdate.shortChannelId -> Relayer.OutgoingChannel(remoteNodeId, channelUpdate, prevChannelUpdate, commitments)) - val node2channels1 = node2channels.addOne(remoteNodeId, channelUpdate.shortChannelId) - apply(nodeParams, register, channelUpdates1, node2channels1) + val prevChannelUpdate = channels.get(channelId).map(_.channelUpdate) + val channel = Relayer.OutgoingChannel(remoteNodeId, channelUpdate, prevChannelUpdate, commitments) + val channels1 = channels + (channelId -> channel) + val scid2channels1 = scid2channels + (channelUpdate.shortChannelId -> channelId) + val node2channels1 = node2channels.addOne(remoteNodeId, channelId) + apply(nodeParams, register, channels1, scid2channels1, node2channels1) case WrappedLocalChannelDown(LocalChannelDown(_, channelId, shortChannelId, remoteNodeId)) => context.log.debug(s"removed local channel info for channelId=$channelId shortChannelId=$shortChannelId") - val node2channels1 = node2channels.subtractOne(remoteNodeId, shortChannelId) - apply(nodeParams, register, channelUpdates - shortChannelId, node2channels1) + val channels1 = channels - channelId + val scid2Channels1 = scid2channels - shortChannelId + val node2channels1 = node2channels.subtractOne(remoteNodeId, channelId) + apply(nodeParams, register, channels1, scid2Channels1, node2channels1) case WrappedAvailableBalanceChanged(AvailableBalanceChanged(_, channelId, shortChannelId, commitments)) => - val channelUpdates1 = channelUpdates.get(shortChannelId) match { + val channels1 = channels.get(channelId) match { case Some(c: Relayer.OutgoingChannel) => context.log.debug(s"available balance changed for channelId=$channelId shortChannelId=$shortChannelId availableForSend={} availableForReceive={}", commitments.availableBalanceForSend, commitments.availableBalanceForReceive) - channelUpdates + (shortChannelId -> c.copy(commitments = commitments)) - case None => channelUpdates // we only consider the balance if we have the channel_update + channels + (channelId -> c.copy(commitments = commitments)) + case None => channels // we only consider the balance if we have the channel_update } - apply(nodeParams, register, channelUpdates1, node2channels) + apply(nodeParams, register, channels1, scid2channels, node2channels) case WrappedShortChannelIdAssigned(ShortChannelIdAssigned(_, channelId, shortChannelId, previousShortChannelId_opt)) => - val (channelUpdates1, node2channels1) = previousShortChannelId_opt match { - case Some(previousShortChannelId) if previousShortChannelId != shortChannelId => - context.log.debug(s"shortChannelId changed for channelId=$channelId ($previousShortChannelId->$shortChannelId, probably due to chain re-org)") - // We simply remove the old entry: we should receive a LocalChannelUpdate with the new shortChannelId shortly. - val node2channels1 = channelUpdates.get(previousShortChannelId).map(_.nextNodeId) match { - case Some(remoteNodeId) => node2channels.subtractOne(remoteNodeId, previousShortChannelId) - case None => node2channels - } - (channelUpdates - previousShortChannelId, node2channels1) - case _ => (channelUpdates, node2channels) - } - apply(nodeParams, register, channelUpdates1, node2channels1) + context.log.debug(s"added new mapping shortChannelId=$shortChannelId for channelId=$channelId") + val scid2channels1 = scid2channels + (shortChannelId -> channelId) + apply(nodeParams, register, channels, scid2channels1, node2channels) } } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/Relayer.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/Relayer.scala index d47d77d09a..6a95189bb5 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/Relayer.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/Relayer.scala @@ -23,6 +23,7 @@ import akka.actor.typed.scaladsl.adapter.ClassicActorContextOps import akka.actor.{Actor, ActorRef, DiagnosticActorLogging, Props, typed} import akka.event.Logging.MDC import akka.event.LoggingAdapter +import fr.acinq.bitcoin.scalacompat.ByteVector32 import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.eclair.channel._ import fr.acinq.eclair.db.PendingCommandsDb @@ -141,6 +142,7 @@ object Relayer extends Logging { */ case class GetOutgoingChannels(enabledOnly: Boolean = true) case class OutgoingChannel(nextNodeId: PublicKey, channelUpdate: ChannelUpdate, prevChannelUpdate: Option[ChannelUpdate], commitments: AbstractCommitments) { + val channelId: ByteVector32 = commitments.channelId def toChannelBalance: ChannelBalance = ChannelBalance( remoteNodeId = nextNodeId, shortChannelId = channelUpdate.shortChannelId, diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/ChannelRelayerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/ChannelRelayerSpec.scala index 268953a09a..3aba714c92 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/ChannelRelayerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/ChannelRelayerSpec.scala @@ -62,9 +62,9 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a fwd } - def expectFwdAdd(register: TestProbe[Any], shortChannelId: ShortChannelId, outAmount: MilliSatoshi, outExpiry: CltvExpiry): Register.ForwardShortId[CMD_ADD_HTLC] = { - val fwd = register.expectMessageType[Register.ForwardShortId[CMD_ADD_HTLC]] - assert(fwd.shortChannelId === shortChannelId) + def expectFwdAdd(register: TestProbe[Any], channelId: ByteVector32, outAmount: MilliSatoshi, outExpiry: CltvExpiry): Register.Forward[CMD_ADD_HTLC] = { + val fwd = register.expectMessageType[Register.Forward[CMD_ADD_HTLC]] + assert(fwd.channelId === channelId) assert(fwd.message.amount === outAmount) assert(fwd.message.cltvExpiry === outExpiry) assert(fwd.message.origin.isInstanceOf[Origin.ChannelRelayedHot]) @@ -83,7 +83,7 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a channelRelayer ! WrappedLocalChannelUpdate(u) channelRelayer ! Relay(r) - expectFwdAdd(register, shortId1, outgoingAmount, outgoingExpiry) + expectFwdAdd(register, channelIds(shortId1), outgoingAmount, outgoingExpiry) } test("relay an htlc-add with onion tlv payload") { f => @@ -97,7 +97,7 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a channelRelayer ! WrappedLocalChannelUpdate(u) channelRelayer ! Relay(r) - expectFwdAdd(register, shortId1, outgoingAmount, outgoingExpiry) + expectFwdAdd(register, channelIds(shortId1), outgoingAmount, outgoingExpiry) } test("relay an htlc-add with retries") { f => @@ -117,12 +117,12 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a channelRelayer ! Relay(r) // first try - val fwd1 = expectFwdAdd(register, shortId2, outgoingAmount, outgoingExpiry) + val fwd1 = expectFwdAdd(register, channelIds(shortId2), outgoingAmount, outgoingExpiry) // channel returns an error fwd1.message.replyTo ! RES_ADD_FAILED(fwd1.message, HtlcValueTooHighInFlight(channelIds(shortId2), UInt64(1000000000L), 1516977616L msat), Some(u2.channelUpdate)) // second try - val fwd2 = expectFwdAdd(register, shortId1, outgoingAmount, outgoingExpiry) + val fwd2 = expectFwdAdd(register, channelIds(shortId1), outgoingAmount, outgoingExpiry) // failure again fwd1.message.replyTo ! RES_ADD_FAILED(fwd2.message, HtlcValueTooHighInFlight(channelIds(shortId1), UInt64(1000000000L), 1516977616L msat), Some(u1.channelUpdate)) @@ -151,8 +151,8 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a channelRelayer ! WrappedLocalChannelUpdate(u) channelRelayer ! Relay(r) - val fwd = expectFwdAdd(register, shortId1, outgoingAmount, outgoingExpiry) - fwd.replyTo ! Register.ForwardShortIdFailure(fwd) + val fwd = expectFwdAdd(register, channelIds(shortId1), outgoingAmount, outgoingExpiry) + fwd.replyTo ! Register.ForwardFailure(fwd) expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, Right(UnknownNextPeer), commit = true)) } @@ -208,7 +208,7 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a channelRelayer ! WrappedLocalChannelUpdate(u) channelRelayer ! Relay(r) - expectFwdAdd(register, shortId1, payload.amountToForward, payload.outgoingCltv).message + expectFwdAdd(register, channelIds(shortId1), payload.amountToForward, payload.outgoingCltv).message } test("fail to relay an htlc-add (expiry too small)") { f => @@ -248,7 +248,7 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a channelRelayer ! Relay(r) // relay succeeds with current channel update (u1) with lower fees - expectFwdAdd(register, shortId1, outgoingAmount, outgoingExpiry) + expectFwdAdd(register, channelIds(shortId1), outgoingAmount, outgoingExpiry) val u2 = createLocalUpdate(shortId1, timestamp = TimestampSecond.now() - 530) @@ -256,7 +256,7 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a channelRelayer ! Relay(r) // relay succeeds because the current update (u2) with higher fees occurred less than 10 minutes ago - expectFwdAdd(register, shortId1, outgoingAmount, outgoingExpiry) + expectFwdAdd(register, channelIds(shortId1), outgoingAmount, outgoingExpiry) val u3 = createLocalUpdate(shortId1, timestamp = TimestampSecond.now() - 601) @@ -290,7 +290,7 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a testCases.foreach { testCase => channelRelayer ! WrappedLocalChannelUpdate(u) channelRelayer ! Relay(r) - val fwd = expectFwdAdd(register, shortId1, outgoingAmount, outgoingExpiry) + val fwd = expectFwdAdd(register, channelIds(shortId1), outgoingAmount, outgoingExpiry) fwd.message.replyTo ! RES_ADD_FAILED(fwd.message, testCase.exc, Some(testCase.update)) expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, Right(testCase.failure), commit = true)) } @@ -303,7 +303,7 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a def dummyLocalUpdate(shortChannelId: ShortChannelId, remoteNodeId: PublicKey, availableBalanceForSend: MilliSatoshi, capacity: Satoshi) = { val channelId = randomBytes32() val update = Announcements.makeChannelUpdate(Block.RegtestGenesisBlock.hash, randomKey(), remoteNodeId, shortChannelId, CltvExpiryDelta(10), 100 msat, 1000 msat, 100, capacity.toMilliSatoshi) - val commitments = PaymentPacketSpec.makeCommitments(ByteVector32.Zeroes, availableBalanceForSend, testCapacity = capacity) + val commitments = PaymentPacketSpec.makeCommitments(channelId, availableBalanceForSend, testCapacity = capacity) LocalChannelUpdate(null, channelId, shortChannelId, remoteNodeId, None, update, commitments) } @@ -325,16 +325,16 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a val r = createValidIncomingPacket(1000000 msat, CltvExpiry(70), payload) channelRelayer ! Relay(r) // select the channel to the same node, with the lowest capacity and balance but still high enough to handle the payment - val cmd1 = expectFwdAdd(register, ShortChannelId(22223), payload.amountToForward, payload.outgoingCltv).message + val cmd1 = expectFwdAdd(register, channelUpdates(ShortChannelId(22223)).channelId, payload.amountToForward, payload.outgoingCltv).message cmd1.replyTo ! RES_ADD_FAILED(cmd1, ChannelUnavailable(randomBytes32()), None) // select 2nd-to-best channel: higher capacity and balance - val cmd2 = expectFwdAdd(register, ShortChannelId(22222), payload.amountToForward, payload.outgoingCltv).message + val cmd2 = expectFwdAdd(register, channelUpdates(ShortChannelId(22222)).channelId, payload.amountToForward, payload.outgoingCltv).message cmd2.replyTo ! RES_ADD_FAILED(cmd2, TooManyAcceptedHtlcs(randomBytes32(), 42), Some(channelUpdates(ShortChannelId(22222)).channelUpdate)) // select 3rd-to-best channel: same balance but higher capacity - val cmd3 = expectFwdAdd(register, ShortChannelId(12345), payload.amountToForward, payload.outgoingCltv).message + val cmd3 = expectFwdAdd(register, channelUpdates(ShortChannelId(12345)).channelId, payload.amountToForward, payload.outgoingCltv).message cmd3.replyTo ! RES_ADD_FAILED(cmd3, TooManyAcceptedHtlcs(randomBytes32(), 42), Some(channelUpdates(ShortChannelId(12345)).channelUpdate)) // select 4th-to-best channel: same capacity but higher balance - val cmd4 = expectFwdAdd(register, ShortChannelId(11111), payload.amountToForward, payload.outgoingCltv).message + val cmd4 = expectFwdAdd(register, channelUpdates(ShortChannelId(11111)).channelId, payload.amountToForward, payload.outgoingCltv).message cmd4.replyTo ! RES_ADD_FAILED(cmd4, HtlcValueTooHighInFlight(randomBytes32(), UInt64(100000000), 100000000 msat), Some(channelUpdates(ShortChannelId(11111)).channelUpdate)) // all the suitable channels have been tried expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, Right(TemporaryChannelFailure(channelUpdates(ShortChannelId(12345)).channelUpdate)), commit = true)) @@ -344,28 +344,28 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a val payload = RelayLegacyPayload(ShortChannelId(12345), 50000000 msat, CltvExpiry(60)) val r = createValidIncomingPacket(60000000 msat, CltvExpiry(70), payload) channelRelayer ! Relay(r) - expectFwdAdd(register, ShortChannelId(11111), payload.amountToForward, payload.outgoingCltv).message + expectFwdAdd(register, channelUpdates(ShortChannelId(11111)).channelId, payload.amountToForward, payload.outgoingCltv).message } { // lower amount payment val payload = RelayLegacyPayload(ShortChannelId(12345), 1000 msat, CltvExpiry(60)) val r = createValidIncomingPacket(60000000 msat, CltvExpiry(70), payload) channelRelayer ! Relay(r) - expectFwdAdd(register, ShortChannelId(33333), payload.amountToForward, payload.outgoingCltv).message + expectFwdAdd(register, channelUpdates(ShortChannelId(33333)).channelId, payload.amountToForward, payload.outgoingCltv).message } { // payment too high, no suitable channel found, we keep the requested one val payload = RelayLegacyPayload(ShortChannelId(12345), 1000000000 msat, CltvExpiry(60)) val r = createValidIncomingPacket(1010000000 msat, CltvExpiry(70), payload) channelRelayer ! Relay(r) - expectFwdAdd(register, ShortChannelId(12345), payload.amountToForward, payload.outgoingCltv).message + expectFwdAdd(register, channelUpdates(ShortChannelId(12345)).channelId, payload.amountToForward, payload.outgoingCltv).message } { // cltv expiry larger than our requirements val payload = RelayLegacyPayload(ShortChannelId(12345), 998900 msat, CltvExpiry(50)) val r = createValidIncomingPacket(1000000 msat, CltvExpiry(70), payload) channelRelayer ! Relay(r) - expectFwdAdd(register, ShortChannelId(22223), payload.amountToForward, payload.outgoingCltv).message + expectFwdAdd(register, channelUpdates(ShortChannelId(22223)).channelId, payload.amountToForward, payload.outgoingCltv).message } { // cltv expiry too small, no suitable channel found @@ -399,7 +399,7 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a testCases.foreach { testCase => channelRelayer ! WrappedLocalChannelUpdate(u) channelRelayer ! Relay(r) - val fwd = expectFwdAdd(register, shortId1, outgoingAmount, outgoingExpiry) + val fwd = expectFwdAdd(register, channelIds(shortId1), outgoingAmount, outgoingExpiry) fwd.message.replyTo ! RES_SUCCESS(fwd.message, channelId1) fwd.message.origin.replyTo ! RES_ADD_SETTLED(fwd.message.origin, downstream_htlc, testCase.result) expectFwdFail(register, r.add.channelId, testCase.cmd) @@ -428,7 +428,7 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a channelRelayer ! WrappedLocalChannelUpdate(u) channelRelayer ! Relay(r) - val fwd1 = expectFwdAdd(register, shortId1, outgoingAmount, outgoingExpiry) + val fwd1 = expectFwdAdd(register, channelIds(shortId1), outgoingAmount, outgoingExpiry) fwd1.message.replyTo ! RES_SUCCESS(fwd1.message, channelId1) fwd1.message.origin.replyTo ! RES_ADD_SETTLED(fwd1.message.origin, downstream_htlc, testCase.result) @@ -488,14 +488,12 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a // Simulate a chain re-org that changes the shortChannelId: channelRelayer ! WrappedShortChannelIdAssigned(ShortChannelIdAssigned(null, channelId_ab, ShortChannelId(42), Some(channelUpdate_ab.shortChannelId))) - val channels7 = getOutgoingChannels(true) - assert(channels7.isEmpty) // We should receive the updated channel update containing the new shortChannelId: channelRelayer ! WrappedLocalChannelUpdate(LocalChannelUpdate(null, channelId_ab, ShortChannelId(42), a, None, channelUpdate_ab.copy(shortChannelId = ShortChannelId(42)), makeCommitments(channelId_ab, 100000 msat, 200000 msat))) - val channels8 = getOutgoingChannels(true) - assert(channels8.size === 1) - assert(channels8.head.channelUpdate.shortChannelId === ShortChannelId(42)) + val channels7 = getOutgoingChannels(true) + assert(channels7.size === 1) + assert(channels7.head.channelUpdate.shortChannelId === ShortChannelId(42)) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/RelayerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/RelayerSpec.scala index 29ad5083fe..12d477466f 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/RelayerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/RelayerSpec.scala @@ -90,7 +90,7 @@ class RelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("applicat // and then manually build an htlc val add_ab = UpdateAddHtlc(channelId = randomBytes32(), id = 123456, cmd.amount, cmd.paymentHash, cmd.cltvExpiry, cmd.onion) relayer ! RelayForward(add_ab) - register.expectMessageType[Register.ForwardShortId[CMD_ADD_HTLC]] + register.expectMessageType[Register.Forward[CMD_ADD_HTLC]] } test("relay an htlc-add at the final node to the payment handler") { f => From d98da6f44f219831b630924cd2e09595d4db5f4f Mon Sep 17 00:00:00 2001 From: Pierre-Marie Padiou Date: Thu, 19 May 2022 13:31:27 +0200 Subject: [PATCH 074/121] Index unannounced channels on `channelId` instead of `shortChannelId` in the router (#2269) This is the final preparatory step before we can introduce channel aliases, which are arbitrary values of type `ShortChannelId`. We had to move to a stable identifier for private channels. Public channels can keep using the `shortChannelId`, it is stable because it is a "real scid" (and only used after 6 confs) and is useful as an index for channel queries. --- .../eclair/router/RouteCalculation.scala | 28 +- .../scala/fr/acinq/eclair/router/Router.scala | 30 +- .../fr/acinq/eclair/router/Validation.scala | 303 +++++++++--------- .../acinq/eclair/router/BaseRouterSpec.scala | 8 +- .../fr/acinq/eclair/router/RouterSpec.scala | 4 +- 5 files changed, 202 insertions(+), 171 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/RouteCalculation.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/RouteCalculation.scala index cf51a2cc97..58d75a6c4c 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/RouteCalculation.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/RouteCalculation.scala @@ -66,18 +66,22 @@ object RouteCalculation { case PredefinedChannelRoute(targetNodeId, shortChannelIds) => val (end, hops) = shortChannelIds.foldLeft((localNodeId, Seq.empty[ChannelHop])) { case ((currentNode, previousHops), shortChannelId) => - val channelDesc_opt = d.channels.get(shortChannelId).flatMap(c => currentNode match { - case c.ann.nodeId1 => Some(ChannelDesc(shortChannelId, c.ann.nodeId1, c.ann.nodeId2)) - case c.ann.nodeId2 => Some(ChannelDesc(shortChannelId, c.ann.nodeId2, c.ann.nodeId1)) - case _ => None - }).orElse(d.privateChannels.get(shortChannelId).flatMap(c => currentNode match { - case c.nodeId1 => Some(ChannelDesc(shortChannelId, c.nodeId1, c.nodeId2)) - case c.nodeId2 => Some(ChannelDesc(shortChannelId, c.nodeId2, c.nodeId1)) - case _ => None - })).orElse(assistedChannels.get(shortChannelId).flatMap(c => currentNode match { - case c.nodeId => Some(ChannelDesc(shortChannelId, c.nodeId, c.nextNodeId)) - case _ => None - })) + val channelDesc_opt = d.resolve(shortChannelId) match { + case Some(c: PublicChannel) => currentNode match { + case c.nodeId1 => Some(ChannelDesc(shortChannelId, c.nodeId1, c.nodeId2)) + case c.nodeId2 => Some(ChannelDesc(shortChannelId, c.nodeId2, c.nodeId1)) + case _ => None + } + case Some(c: PrivateChannel) => currentNode match { + case c.nodeId1 => Some(ChannelDesc(c.shortChannelId, c.nodeId1, c.nodeId2)) + case c.nodeId2 => Some(ChannelDesc(c.shortChannelId, c.nodeId2, c.nodeId1)) + case _ => None + } + case None => assistedChannels.get(shortChannelId).flatMap(c => currentNode match { + case c.nodeId => Some(ChannelDesc(shortChannelId, c.nodeId, c.nextNodeId)) + case _ => None + }) + } channelDesc_opt.flatMap(c => g.getEdge(c)) match { case Some(edge) => (edge.desc.b, previousHops :+ ChannelHop(edge.desc.shortChannelId, edge.desc.a, edge.desc.b, edge.params)) case None => (currentNode, previousHops) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala index 9cca8e5dac..e1adb6a0a4 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala @@ -24,6 +24,7 @@ import akka.event.Logging.MDC import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.bitcoin.scalacompat.{ByteVector32, Satoshi} import fr.acinq.eclair.Logs.LogCategory +import fr.acinq.eclair.ShortChannelId.outputIndex import fr.acinq.eclair._ import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.{ValidateResult, WatchExternalChannelSpent, WatchExternalChannelSpentTriggered} @@ -102,7 +103,7 @@ class Router(val nodeParams: NodeParams, watcher: typed.ActorRef[ZmqWatcher.Comm log.info(s"initialization completed, ready to process messages") Try(initialized.map(_.success(Done))) - startWith(NORMAL, Data(initNodes, initChannels, Stash(Map.empty, Map.empty), rebroadcast = Rebroadcast(channels = Map.empty, updates = Map.empty, nodes = Map.empty), awaiting = Map.empty, privateChannels = Map.empty, excludedChannels = Set.empty, graph, sync = Map.empty)) + startWith(NORMAL, Data(initNodes, initChannels, Stash(Map.empty, Map.empty), rebroadcast = Rebroadcast(channels = Map.empty, updates = Map.empty, nodes = Map.empty), awaiting = Map.empty, privateChannels = Map.empty, scid2PrivateChannels = Map.empty, excludedChannels = Set.empty, graph, sync = Map.empty)) } when(NORMAL) { @@ -320,6 +321,8 @@ object Router { case class ChannelMeta(balance1: MilliSatoshi, balance2: MilliSatoshi) sealed trait KnownChannel { val capacity: Satoshi + val nodeId1: PublicKey + val nodeId2: PublicKey def getNodeIdSameSideAs(u: ChannelUpdate): PublicKey def getChannelUpdateSameSideAs(u: ChannelUpdate): Option[ChannelUpdate] def getBalanceSameSideAs(u: ChannelUpdate): Option[MilliSatoshi] @@ -331,6 +334,10 @@ object Router { update_1_opt.foreach(u => assert(u.channelFlags.isNode1)) update_2_opt.foreach(u => assert(!u.channelFlags.isNode1)) + val nodeId1: PublicKey = ann.nodeId1 + val nodeId2: PublicKey = ann.nodeId2 + def shortChannelId: ShortChannelId = ann.shortChannelId + def channelId: ByteVector32 = toLongId(fundingTxid.reverse, outputIndex(ann.shortChannelId)) def getNodeIdSameSideAs(u: ChannelUpdate): PublicKey = if (u.channelFlags.isNode1) ann.nodeId1 else ann.nodeId2 def getChannelUpdateSameSideAs(u: ChannelUpdate): Option[ChannelUpdate] = if (u.channelFlags.isNode1) update_1_opt else update_2_opt def getBalanceSameSideAs(u: ChannelUpdate): Option[MilliSatoshi] = if (u.channelFlags.isNode1) meta_opt.map(_.balance1) else meta_opt.map(_.balance2) @@ -345,7 +352,7 @@ object Router { case Right(rcu) => updateChannelUpdateSameSideAs(rcu.channelUpdate) } } - case class PrivateChannel(localNodeId: PublicKey, remoteNodeId: PublicKey, update_1_opt: Option[ChannelUpdate], update_2_opt: Option[ChannelUpdate], meta: ChannelMeta) extends KnownChannel { + case class PrivateChannel(shortChannelId: ShortChannelId, channelId: ByteVector32, localNodeId: PublicKey, remoteNodeId: PublicKey, update_1_opt: Option[ChannelUpdate], update_2_opt: Option[ChannelUpdate], meta: ChannelMeta) extends KnownChannel { val (nodeId1, nodeId2) = if (Announcements.isNode1(localNodeId, remoteNodeId)) (localNodeId, remoteNodeId) else (remoteNodeId, localNodeId) val capacity: Satoshi = (meta.balance1 + meta.balance2).truncateToSatoshi @@ -605,11 +612,26 @@ object Router { stash: Stash, rebroadcast: Rebroadcast, awaiting: Map[ChannelAnnouncement, Seq[GossipOrigin]], // note: this is a seq because we want to preserve order: first actor is the one who we need to send a tcp-ack when validation is done - privateChannels: Map[ShortChannelId, PrivateChannel], + privateChannels: Map[ByteVector32, PrivateChannel], // indexed by channel id + scid2PrivateChannels: Map[ShortChannelId, ByteVector32], // scid to channel_id, only to be used for private channels excludedChannels: Set[ChannelDesc], // those channels are temporarily excluded from route calculation, because their node returned a TemporaryChannelFailure graph: DirectedGraph, sync: Map[PublicKey, Syncing] // keep tracks of channel range queries sent to each peer. If there is an entry in the map, it means that there is an ongoing query for which we have not yet received an 'end' message - ) + ) { + + def resolve(scid: ShortChannelId): Option[KnownChannel] = { + // let's assume this is a real scid + channels.get(scid) match { + case Some(publicChannel) => Some(publicChannel) + case None => + // maybe it's an alias or a real scid + scid2PrivateChannels.get(scid).flatMap(privateChannels.get) match { + case Some(privateChannel) => Some(privateChannel) + case None => None + } + } + } + } // @formatter:off sealed trait State diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/Validation.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/Validation.scala index 2607a04651..a1ca814820 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/Validation.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/Validation.scala @@ -32,7 +32,7 @@ import fr.acinq.eclair.router.Monitoring.Metrics import fr.acinq.eclair.router.Router._ import fr.acinq.eclair.transactions.Scripts import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{Logs, MilliSatoshiLong, NodeParams, ShortChannelId, TxCoordinates} +import fr.acinq.eclair.{Logs, MilliSatoshiLong, NodeParams, ShortChannelId, TxCoordinates, toLongId} object Validation { @@ -123,7 +123,8 @@ object Validation { ctx.self ! nodeAnn } // maybe this previously was a local unannounced channel - val privateChannel_opt = d0.privateChannels.get(c.shortChannelId) + val channelId = toLongId(tx.txid.reverse, outputIndex) + val privateChannel_opt = d0.privateChannels.get(channelId) Some(PublicChannel(c, tx.txid, capacity, @@ -160,7 +161,7 @@ object Validation { .toMap val d1 = d0.copy( channels = d0.channels + (c.shortChannelId -> pc), - privateChannels = d0.privateChannels - c.shortChannelId, // we remove the corresponding unannounced channel that we may have until now + privateChannels = d0.privateChannels - pc.channelId, // we remove the corresponding unannounced channel that we may have until now rebroadcast = d0.rebroadcast.copy( channels = d0.rebroadcast.channels + (c -> d0.awaiting.getOrElse(c, Nil).toSet), // we rebroadcast the channel to our peers updates = d0.rebroadcast.updates ++ updates1 @@ -264,158 +265,157 @@ object Validation { def handleChannelUpdate(d: Data, db: NetworkDb, routerConf: RouterConf, update: Either[LocalChannelUpdate, RemoteChannelUpdate], wasStashed: Boolean = false)(implicit ctx: ActorContext, log: LoggingAdapter): Data = { implicit val sender: ActorRef = ctx.self // necessary to preserve origin when sending messages to other actors - val (u: ChannelUpdate, origins: Set[GossipOrigin]) = update match { - case Left(lcu) => (lcu.channelUpdate, Set(LocalGossip)) + val (pc_opt: Option[KnownChannel], u: ChannelUpdate, origins: Set[GossipOrigin]) = update match { + case Left(lcu) => (d.resolve(lcu.shortChannelId), lcu.channelUpdate, Set(LocalGossip)) case Right(rcu) => rcu.origins.collect { case RemoteGossip(peerConnection, _) if !wasStashed => // stashed changes have already been acknowledged log.debug("received channel update for shortChannelId={}", rcu.channelUpdate.shortChannelId) peerConnection ! TransportHandler.ReadAck(rcu.channelUpdate) } - (rcu.channelUpdate, rcu.origins) + (d.resolve(rcu.channelUpdate.shortChannelId), rcu.channelUpdate, rcu.origins) } - if (d.channels.contains(u.shortChannelId)) { - // related channel is already known (note: this means no related channel_update is in the stash) - val publicChannel = true - val pc = d.channels(u.shortChannelId) - if (d.rebroadcast.updates.contains(u)) { - log.debug("ignoring {} (pending rebroadcast)", u) - sendDecision(origins, GossipDecision.Accepted(u)) - val origins1 = d.rebroadcast.updates(u) ++ origins - // NB: we update the channels because the balances may have changed even if the channel_update is the same. - val pc1 = pc.applyChannelUpdate(update) - val graph1 = d.graph.addEdge(GraphEdge(u, pc1)) - d.copy(rebroadcast = d.rebroadcast.copy(updates = d.rebroadcast.updates + (u -> origins1)), channels = d.channels + (u.shortChannelId -> pc1), graph = graph1) - } else if (StaleChannels.isStale(u)) { - log.debug("ignoring {} (stale)", u) - sendDecision(origins, GossipDecision.Stale(u)) - d - } else if (pc.getChannelUpdateSameSideAs(u).exists(_.timestamp >= u.timestamp)) { - log.debug("ignoring {} (duplicate)", u) - sendDecision(origins, GossipDecision.Duplicate(u)) - update match { - case Left(_) => - // NB: we update the graph because the balances may have changed even if the channel_update is the same. - val pc1 = pc.applyChannelUpdate(update) - val graph1 = d.graph.addEdge(GraphEdge(u, pc1)) - d.copy(channels = d.channels + (u.shortChannelId -> pc1), graph = graph1) - case Right(_) => d - } - } else if (!Announcements.checkSig(u, pc.getNodeIdSameSideAs(u))) { - log.warning("bad signature for announcement shortChannelId={} {}", u.shortChannelId, u) - sendDecision(origins, GossipDecision.InvalidSignature(u)) - d - } else if (pc.getChannelUpdateSameSideAs(u).isDefined) { - log.debug("updated channel_update for shortChannelId={} public={} flags={} {}", u.shortChannelId, publicChannel, u.channelFlags, u) - Metrics.channelUpdateRefreshed(u, pc.getChannelUpdateSameSideAs(u).get, publicChannel) - sendDecision(origins, GossipDecision.Accepted(u)) - ctx.system.eventStream.publish(ChannelUpdatesReceived(u :: Nil)) - db.updateChannel(u) - // update the graph - val pc1 = pc.applyChannelUpdate(update) - val graph1 = if (u.channelFlags.isEnabled) { + pc_opt match { + case Some(pc: PublicChannel) => + // related channel is already known (note: this means no related channel_update is in the stash) + val publicChannel = true + if (d.rebroadcast.updates.contains(u)) { + log.debug("ignoring {} (pending rebroadcast)", u) + sendDecision(origins, GossipDecision.Accepted(u)) + val origins1 = d.rebroadcast.updates(u) ++ origins + // NB: we update the channels because the balances may have changed even if the channel_update is the same. + val pc1 = pc.applyChannelUpdate(update) + val graph1 = d.graph.addEdge(GraphEdge(u, pc1)) + d.copy(rebroadcast = d.rebroadcast.copy(updates = d.rebroadcast.updates + (u -> origins1)), channels = d.channels + (pc.shortChannelId -> pc1), graph = graph1) + } else if (StaleChannels.isStale(u)) { + log.debug("ignoring {} (stale)", u) + sendDecision(origins, GossipDecision.Stale(u)) + d + } else if (pc.getChannelUpdateSameSideAs(u).exists(_.timestamp >= u.timestamp)) { + log.debug("ignoring {} (duplicate)", u) + sendDecision(origins, GossipDecision.Duplicate(u)) + update match { + case Left(_) => + // NB: we update the graph because the balances may have changed even if the channel_update is the same. + val pc1 = pc.applyChannelUpdate(update) + val graph1 = d.graph.addEdge(GraphEdge(u, pc1)) + d.copy(channels = d.channels + (pc.shortChannelId -> pc1), graph = graph1) + case Right(_) => d + } + } else if (!Announcements.checkSig(u, pc.getNodeIdSameSideAs(u))) { + log.warning("bad signature for announcement shortChannelId={} {}", u.shortChannelId, u) + sendDecision(origins, GossipDecision.InvalidSignature(u)) + d + } else if (pc.getChannelUpdateSameSideAs(u).isDefined) { + log.debug("updated channel_update for shortChannelId={} public={} flags={} {}", u.shortChannelId, publicChannel, u.channelFlags, u) + Metrics.channelUpdateRefreshed(u, pc.getChannelUpdateSameSideAs(u).get, publicChannel) + sendDecision(origins, GossipDecision.Accepted(u)) + ctx.system.eventStream.publish(ChannelUpdatesReceived(u :: Nil)) + db.updateChannel(u) + // update the graph + val pc1 = pc.applyChannelUpdate(update) + val graph1 = if (u.channelFlags.isEnabled) { + update.left.foreach(_ => log.info("added local shortChannelId={} public={} to the network graph", u.shortChannelId, publicChannel)) + d.graph.addEdge(GraphEdge(u, pc1)) + } else { + update.left.foreach(_ => log.info("removed local shortChannelId={} public={} from the network graph", u.shortChannelId, publicChannel)) + d.graph.removeEdge(ChannelDesc(u, pc1.ann)) + } + d.copy(channels = d.channels + (pc.shortChannelId -> pc1), rebroadcast = d.rebroadcast.copy(updates = d.rebroadcast.updates + (u -> origins)), graph = graph1) + } else { + log.debug("added channel_update for shortChannelId={} public={} flags={} {}", u.shortChannelId, publicChannel, u.channelFlags, u) + sendDecision(origins, GossipDecision.Accepted(u)) + ctx.system.eventStream.publish(ChannelUpdatesReceived(u :: Nil)) + db.updateChannel(u) + // we also need to update the graph + val pc1 = pc.applyChannelUpdate(update) + val graph1 = d.graph.addEdge(GraphEdge(u, pc1)) update.left.foreach(_ => log.info("added local shortChannelId={} public={} to the network graph", u.shortChannelId, publicChannel)) - d.graph.addEdge(GraphEdge(u, pc1)) + d.copy(channels = d.channels + (pc.shortChannelId -> pc1), privateChannels = d.privateChannels - pc1.channelId, rebroadcast = d.rebroadcast.copy(updates = d.rebroadcast.updates + (u -> origins)), graph = graph1) + } + case Some(pc: PrivateChannel) => + val publicChannel = false + if (StaleChannels.isStale(u)) { + log.debug("ignoring {} (stale)", u) + sendDecision(origins, GossipDecision.Stale(u)) + d + } else if (pc.getChannelUpdateSameSideAs(u).exists(_.timestamp >= u.timestamp)) { + log.debug("ignoring {} (already know same or newer)", u) + sendDecision(origins, GossipDecision.Duplicate(u)) + d + } else if (!Announcements.checkSig(u, pc.getNodeIdSameSideAs(u))) { + log.warning("bad signature for announcement shortChannelId={} {}", u.shortChannelId, u) + sendDecision(origins, GossipDecision.InvalidSignature(u)) + d + } else if (pc.getChannelUpdateSameSideAs(u).isDefined) { + log.debug("updated channel_update for channelId={} public={} flags={} {}", pc.channelId, publicChannel, u.channelFlags, u) + Metrics.channelUpdateRefreshed(u, pc.getChannelUpdateSameSideAs(u).get, publicChannel) + sendDecision(origins, GossipDecision.Accepted(u)) + ctx.system.eventStream.publish(ChannelUpdatesReceived(u :: Nil)) + // we also need to update the graph + val pc1 = pc.applyChannelUpdate(update) + val graph1 = if (u.channelFlags.isEnabled) { + update.left.foreach(_ => log.info("added local channelId={} public={} to the network graph", pc.channelId, publicChannel)) + d.graph.addEdge(GraphEdge(u, pc1)) + } else { + update.left.foreach(_ => log.info("removed local channelId={} public={} from the network graph", pc.channelId, publicChannel)) + d.graph.removeEdge(ChannelDesc(u, pc1)) + } + d.copy(privateChannels = d.privateChannels + (pc.channelId -> pc1), graph = graph1) } else { - update.left.foreach(_ => log.info("removed local shortChannelId={} public={} from the network graph", u.shortChannelId, publicChannel)) - d.graph.removeEdge(ChannelDesc(u, pc.ann)) + log.debug("added channel_update for channelId={} public={} flags={} {}", pc.channelId, publicChannel, u.channelFlags, u) + sendDecision(origins, GossipDecision.Accepted(u)) + ctx.system.eventStream.publish(ChannelUpdatesReceived(u :: Nil)) + // we also need to update the graph + val pc1 = pc.applyChannelUpdate(update) + val graph1 = d.graph.addEdge(GraphEdge(u, pc1)) + update.left.foreach(_ => log.info("added local channelId={} public={} to the network graph", pc.channelId, publicChannel)) + d.copy(privateChannels = d.privateChannels + (pc.channelId -> pc1), graph = graph1) } - d.copy(channels = d.channels + (u.shortChannelId -> pc1), rebroadcast = d.rebroadcast.copy(updates = d.rebroadcast.updates + (u -> origins)), graph = graph1) - } else { - log.debug("added channel_update for shortChannelId={} public={} flags={} {}", u.shortChannelId, publicChannel, u.channelFlags, u) - sendDecision(origins, GossipDecision.Accepted(u)) - ctx.system.eventStream.publish(ChannelUpdatesReceived(u :: Nil)) - db.updateChannel(u) - // we also need to update the graph - val pc1 = pc.applyChannelUpdate(update) - val graph1 = d.graph.addEdge(GraphEdge(u, pc1)) - update.left.foreach(_ => log.info("added local shortChannelId={} public={} to the network graph", u.shortChannelId, publicChannel)) - d.copy(channels = d.channels + (u.shortChannelId -> pc1), privateChannels = d.privateChannels - u.shortChannelId, rebroadcast = d.rebroadcast.copy(updates = d.rebroadcast.updates + (u -> origins)), graph = graph1) - } - } else if (d.awaiting.keys.exists(c => c.shortChannelId == u.shortChannelId)) { - // channel is currently being validated - if (d.stash.updates.contains(u)) { - log.debug("ignoring {} (already stashed)", u) - val origins1 = d.stash.updates(u) ++ origins - d.copy(stash = d.stash.copy(updates = d.stash.updates + (u -> origins1))) - } else { - log.debug("stashing {}", u) - d.copy(stash = d.stash.copy(updates = d.stash.updates + (u -> origins))) - } - } else if (d.privateChannels.contains(u.shortChannelId)) { - val publicChannel = false - val pc = d.privateChannels(u.shortChannelId) - if (StaleChannels.isStale(u)) { - log.debug("ignoring {} (stale)", u) - sendDecision(origins, GossipDecision.Stale(u)) - d - } else if (pc.getChannelUpdateSameSideAs(u).exists(_.timestamp >= u.timestamp)) { - log.debug("ignoring {} (already know same or newer)", u) - sendDecision(origins, GossipDecision.Duplicate(u)) - d - } else if (!Announcements.checkSig(u, pc.getNodeIdSameSideAs(u))) { - log.warning("bad signature for announcement shortChannelId={} {}", u.shortChannelId, u) - sendDecision(origins, GossipDecision.InvalidSignature(u)) - d - } else if (pc.getChannelUpdateSameSideAs(u).isDefined) { - log.debug("updated channel_update for shortChannelId={} public={} flags={} {}", u.shortChannelId, publicChannel, u.channelFlags, u) - Metrics.channelUpdateRefreshed(u, pc.getChannelUpdateSameSideAs(u).get, publicChannel) - sendDecision(origins, GossipDecision.Accepted(u)) - ctx.system.eventStream.publish(ChannelUpdatesReceived(u :: Nil)) - // we also need to update the graph - val pc1 = pc.applyChannelUpdate(update) - val graph1 = if (u.channelFlags.isEnabled) { - update.left.foreach(_ => log.info("added local shortChannelId={} public={} to the network graph", u.shortChannelId, publicChannel)) - d.graph.addEdge(GraphEdge(u, pc1)) + case None if d.awaiting.keys.exists(c => c.shortChannelId == u.shortChannelId) => + // channel is currently being validated + if (d.stash.updates.contains(u)) { + log.debug("ignoring {} (already stashed)", u) + val origins1 = d.stash.updates(u) ++ origins + d.copy(stash = d.stash.copy(updates = d.stash.updates + (u -> origins1))) } else { - update.left.foreach(_ => log.info("removed local shortChannelId={} public={} from the network graph", u.shortChannelId, publicChannel)) - d.graph.removeEdge(ChannelDesc(u, pc1)) + log.debug("stashing {}", u) + d.copy(stash = d.stash.copy(updates = d.stash.updates + (u -> origins))) } - d.copy(privateChannels = d.privateChannels + (u.shortChannelId -> pc1), graph = graph1) - } else { - log.debug("added channel_update for shortChannelId={} public={} flags={} {}", u.shortChannelId, publicChannel, u.channelFlags, u) - sendDecision(origins, GossipDecision.Accepted(u)) - ctx.system.eventStream.publish(ChannelUpdatesReceived(u :: Nil)) - // we also need to update the graph - val pc1 = pc.applyChannelUpdate(update) - val graph1 = d.graph.addEdge(GraphEdge(u, pc1)) - update.left.foreach(_ => log.info("added local shortChannelId={} public={} to the network graph", u.shortChannelId, publicChannel)) - d.copy(privateChannels = d.privateChannels + (u.shortChannelId -> pc1), graph = graph1) - } - } else if (db.isPruned(u.shortChannelId) && !StaleChannels.isStale(u)) { - // the channel was recently pruned, but if we are here, it means that the update is not stale so this is the case - // of a zombie channel coming back from the dead. they probably sent us a channel_announcement right before this update, - // but we ignored it because the channel was in the 'pruned' list. Now that we know that the channel is alive again, - // let's remove the channel from the zombie list and ask the sender to re-send announcements (channel_announcement + updates) - // about that channel. We can ignore this update since we will receive it again - log.info(s"channel shortChannelId=${u.shortChannelId} is back from the dead! requesting announcements about this channel") - sendDecision(origins, GossipDecision.RelatedChannelPruned(u)) - db.removeFromPruned(u.shortChannelId) - // peerConnection_opt will contain a valid peerConnection only when we're handling an update that we received from a peer, not - // when we're sending updates to ourselves - origins head match { - case RemoteGossip(peerConnection, remoteNodeId) => - val query = QueryShortChannelIds(u.chainHash, EncodedShortChannelIds(routerConf.encodingType, List(u.shortChannelId)), TlvStream.empty) - d.sync.get(remoteNodeId) match { - case Some(sync) if sync.started => - // we already have a pending request to that node, let's add this channel to the list and we'll get it later - // TODO: we only request channels with old style channel_query - d.copy(sync = d.sync + (remoteNodeId -> sync.copy(remainingQueries = sync.remainingQueries :+ query, totalQueries = sync.totalQueries + 1))) - case _ => - // otherwise we send the query right away - peerConnection ! query - d.copy(sync = d.sync + (remoteNodeId -> Syncing(remainingQueries = Nil, totalQueries = 1))) - } - case _ => - // we don't know which node this update came from (maybe it was stashed and the channel got pruned in the meantime or some other corner case). - // or we don't have a peerConnection to send our query to. - // anyway, that's not really a big deal because we have removed the channel from the pruned db so next time it shows up we will revalidate it - d - } - } else { - log.debug("ignoring announcement {} (unknown channel)", u) - sendDecision(origins, GossipDecision.NoRelatedChannel(u)) - d + case None if db.isPruned(u.shortChannelId) && !StaleChannels.isStale(u) => + // the channel was recently pruned, but if we are here, it means that the update is not stale so this is the case + // of a zombie channel coming back from the dead. they probably sent us a channel_announcement right before this update, + // but we ignored it because the channel was in the 'pruned' list. Now that we know that the channel is alive again, + // let's remove the channel from the zombie list and ask the sender to re-send announcements (channel_announcement + updates) + // about that channel. We can ignore this update since we will receive it again + log.info(s"channel shortChannelId=${u.shortChannelId} is back from the dead! requesting announcements about this channel") + sendDecision(origins, GossipDecision.RelatedChannelPruned(u)) + db.removeFromPruned(u.shortChannelId) + // peerConnection_opt will contain a valid peerConnection only when we're handling an update that we received from a peer, not + // when we're sending updates to ourselves + origins head match { + case RemoteGossip(peerConnection, remoteNodeId) => + val query = QueryShortChannelIds(u.chainHash, EncodedShortChannelIds(routerConf.encodingType, List(u.shortChannelId)), TlvStream.empty) + d.sync.get(remoteNodeId) match { + case Some(sync) if sync.started => + // we already have a pending request to that node, let's add this channel to the list and we'll get it later + // TODO: we only request channels with old style channel_query + d.copy(sync = d.sync + (remoteNodeId -> sync.copy(remainingQueries = sync.remainingQueries :+ query, totalQueries = sync.totalQueries + 1))) + case _ => + // otherwise we send the query right away + peerConnection ! query + d.copy(sync = d.sync + (remoteNodeId -> Syncing(remainingQueries = Nil, totalQueries = 1))) + } + case _ => + // we don't know which node this update came from (maybe it was stashed and the channel got pruned in the meantime or some other corner case). + // or we don't have a peerConnection to send our query to. + // anyway, that's not really a big deal because we have removed the channel from the pruned db so next time it shows up we will revalidate it + d + } + case None => + log.debug("ignoring announcement {} (unknown channel)", u) + sendDecision(origins, GossipDecision.NoRelatedChannel(u)) + d } } @@ -437,15 +437,18 @@ object Validation { // maybe the local channel was pruned (can happen if we were disconnected for more than 2 weeks) db.removeFromPruned(c.shortChannelId) handleChannelUpdate(d1, db, routerConf, Left(lcu)) - case None if d.privateChannels.contains(lcu.shortChannelId) => + case None if d.privateChannels.contains(lcu.channelId) => // channel isn't announced but we already know about it, we can process the channel_update handleChannelUpdate(d, db, routerConf, Left(lcu)) case None => // channel isn't announced and we never heard of it (maybe it is a private channel or maybe it is a public channel that doesn't yet have 6 confirmations) // let's create a corresponding private channel and process the channel_update log.debug("adding unannounced local channel to remote={} shortChannelId={}", lcu.remoteNodeId, lcu.shortChannelId) - val pc = PrivateChannel(localNodeId, lcu.remoteNodeId, None, None, ChannelMeta(0 msat, 0 msat)).updateBalances(lcu.commitments) - val d1 = d.copy(privateChannels = d.privateChannels + (lcu.shortChannelId -> pc)) + val pc = PrivateChannel(lcu.shortChannelId, lcu.channelId, localNodeId, lcu.remoteNodeId, None, None, ChannelMeta(0 msat, 0 msat)).updateBalances(lcu.commitments) + val d1 = d.copy( + privateChannels = d.privateChannels + (lcu.channelId -> pc), + scid2PrivateChannels = d.scid2PrivateChannels + (lcu.shortChannelId -> lcu.channelId) + ) handleChannelUpdate(d1, db, routerConf, Left(lcu)) } } @@ -458,7 +461,7 @@ object Validation { // the channel was public, we will receive (or have already received) a WatchEventSpentBasic event, that will trigger a clean up of the channel // so let's not do anything here d - } else if (d.privateChannels.contains(shortChannelId)) { + } else if (d.privateChannels.contains(channelId)) { // the channel was private or public-but-not-yet-announced, let's do the clean up log.info("removing private local channel and channel_update for channelId={} shortChannelId={}", channelId, shortChannelId) val desc1 = ChannelDesc(shortChannelId, localNodeId, remoteNodeId) @@ -468,7 +471,7 @@ object Validation { .removeEdge(desc1) .removeEdge(desc2) // and we remove the channel and channel_update from our state - d.copy(privateChannels = d.privateChannels - shortChannelId, graph = graph1) + d.copy(privateChannels = d.privateChannels - channelId, graph = graph1) } else { d } @@ -485,13 +488,13 @@ object Validation { case None => (d.channels, d.graph) } - val (privateChannels1, graph2) = d.privateChannels.get(e.shortChannelId) match { + val (privateChannels1, graph2) = d.privateChannels.get(e.channelId) match { case Some(pc) => val pc1 = pc.updateBalances(e.commitments) log.debug("private channel balance updated: {}", pc1) val update_opt = if (e.commitments.localNodeId == pc1.nodeId1) pc1.update_1_opt else pc1.update_2_opt val graph2 = update_opt.map(u => graph1.addEdge(GraphEdge(u, pc1))).getOrElse(graph1) - (d.privateChannels + (e.shortChannelId -> pc1), graph2) + (d.privateChannels + (e.channelId -> pc1), graph2) case None => (d.privateChannels, graph1) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/BaseRouterSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/BaseRouterSpec.scala index 12e4b3cf27..155df8c7dd 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/BaseRouterSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/BaseRouterSpec.scala @@ -80,6 +80,8 @@ abstract class BaseRouterSpec extends TestKitBaseClass with FixtureAnyFunSuiteLi val scid_ag_private = ShortChannelId(BlockHeight(420000), 5, 0) val scid_gh = ShortChannelId(BlockHeight(420000), 6, 0) + val channelId_ag_private = randomBytes32() + val chan_ab = channelAnnouncement(scid_ab, priv_a, priv_b, priv_funding_a, priv_funding_b) val chan_bc = channelAnnouncement(scid_bc, priv_b, priv_c, priv_funding_b, priv_funding_c) val chan_cd = channelAnnouncement(scid_cd, priv_c, priv_d, priv_funding_c, priv_funding_d) @@ -110,8 +112,8 @@ abstract class BaseRouterSpec extends TestKitBaseClass with FixtureAnyFunSuiteLi assert(ChannelDesc(update_bc, chan_bc) === ChannelDesc(chan_bc.shortChannelId, b, c)) assert(ChannelDesc(update_cd, chan_cd) === ChannelDesc(chan_cd.shortChannelId, c, d)) assert(ChannelDesc(update_ef, chan_ef) === ChannelDesc(chan_ef.shortChannelId, e, f)) - assert(ChannelDesc(update_ag_private, PrivateChannel(a, g, None, None, ChannelMeta(1000 msat, 2000 msat))) === ChannelDesc(scid_ag_private, a, g)) - assert(ChannelDesc(update_ag_private, PrivateChannel(g, a, None, None, ChannelMeta(2000 msat, 1000 msat))) === ChannelDesc(scid_ag_private, a, g)) + assert(ChannelDesc(update_ag_private, PrivateChannel(scid_ag_private, channelId_ag_private, a, g, None, None, ChannelMeta(1000 msat, 2000 msat))) === ChannelDesc(scid_ag_private, a, g)) + assert(ChannelDesc(update_ag_private, PrivateChannel(scid_ag_private, channelId_ag_private, g, a, None, None, ChannelMeta(2000 msat, 1000 msat))) === ChannelDesc(scid_ag_private, a, g)) assert(ChannelDesc(update_gh, chan_gh) === ChannelDesc(chan_gh.shortChannelId, g, h)) // let's set up the router @@ -149,7 +151,7 @@ abstract class BaseRouterSpec extends TestKitBaseClass with FixtureAnyFunSuiteLi peerConnection.send(router, PeerRoutingMessage(peerConnection.ref, remoteNodeId, update_gh)) peerConnection.send(router, PeerRoutingMessage(peerConnection.ref, remoteNodeId, update_hg)) // then private channels - sender.send(router, LocalChannelUpdate(sender.ref, randomBytes32(), scid_ag_private, g, None, update_ag_private, CommitmentsSpec.makeCommitments(30000000 msat, 8000000 msat, a, g, announceChannel = false))) + sender.send(router, LocalChannelUpdate(sender.ref, channelId_ag_private, scid_ag_private, g, None, update_ag_private, CommitmentsSpec.makeCommitments(30000000 msat, 8000000 msat, a, g, announceChannel = false))) // watcher receives the get tx requests assert(watcher.expectMsgType[ValidateRequest].ann === chan_ab) assert(watcher.expectMsgType[ValidateRequest].ann === chan_bc) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/RouterSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/RouterSpec.scala index 6afaebf073..f433335c63 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/RouterSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/RouterSpec.scala @@ -670,10 +670,10 @@ class RouterSpec extends BaseRouterSpec { // Private channels should also update the graph when HTLCs are relayed through them. val balances = Set(33000000 msat, 5000000 msat) val commitments = CommitmentsSpec.makeCommitments(33000000 msat, 5000000 msat, a, g, announceChannel = false) - sender.send(router, AvailableBalanceChanged(sender.ref, null, scid_ag_private, commitments)) + sender.send(router, AvailableBalanceChanged(sender.ref, channelId_ag_private, scid_ag_private, commitments)) sender.send(router, Router.GetRouterData) val data = sender.expectMsgType[Data] - val channel_ag = data.privateChannels(scid_ag_private) + val channel_ag = data.privateChannels(channelId_ag_private) assert(Set(channel_ag.meta.balance1, channel_ag.meta.balance2) === balances) // And the graph should be updated too. val edge_ag = data.graph.getEdge(ChannelDesc(scid_ag_private, a, g)).get From 44e71277a6f1f80d28c1c14cee2f974eb2e1a6db Mon Sep 17 00:00:00 2001 From: Pierre-Marie Padiou Date: Fri, 20 May 2022 15:01:35 +0200 Subject: [PATCH 075/121] (Minor) Postgres: use `write` instead of `writePretty` (#2279) There is no point in formatting the json since the `JSONB` data type that we use in postgres do not preserve formatting. --- .../db/migration/MigrateChannelsDb.scala | 2 +- .../db/migration/MigrateNetworkDb.scala | 8 ++++---- .../fr/acinq/eclair/db/pg/PgChannelsDb.scala | 6 +++--- .../fr/acinq/eclair/db/pg/PgNetworkDb.scala | 20 +++++++++---------- 4 files changed, 18 insertions(+), 18 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/MigrateChannelsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/MigrateChannelsDb.scala index 37055d465b..2b4165daea 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/MigrateChannelsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/MigrateChannelsDb.scala @@ -20,7 +20,7 @@ object MigrateChannelsDb { insertStatement.setString(1, rs.getByteVector32("channel_id").toHex) insertStatement.setBytes(2, rs.getBytes("data")) val state = channelDataCodec.decode(BitVector(rs.getBytes("data"))).require.value - val json = serialization.writePretty(state) + val json = serialization.write(state) insertStatement.setString(3, json) insertStatement.setBoolean(4, rs.getBoolean("is_closed")) insertStatement.setTimestamp(5, rs.getLongNullable("created_timestamp").map(l => Timestamp.from(Instant.ofEpochMilli(l))).orNull) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/MigrateNetworkDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/MigrateNetworkDb.scala index 807e97f185..0f2b4ccdf8 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/MigrateNetworkDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/migration/MigrateNetworkDb.scala @@ -19,7 +19,7 @@ object MigrateNetworkDb { insertStatement.setString(1, rs.getByteVector("node_id").toHex) insertStatement.setBytes(2, rs.getBytes("data")) val state = nodeAnnouncementCodec.decode(BitVector(rs.getBytes("data"))).require.value - val json = serialization.writePretty(state) + val json = serialization.write(state) insertStatement.setString(3, json) } @@ -42,9 +42,9 @@ object MigrateNetworkDb { val ann = channelAnnouncementCodec.decode(rs.getBitVectorOpt("channel_announcement").get).require.value val channel_update_1_opt = rs.getBitVectorOpt("channel_update_1").map(channelUpdateCodec.decode(_).require.value) val channel_update_2_opt = rs.getBitVectorOpt("channel_update_2").map(channelUpdateCodec.decode(_).require.value) - val json = serialization.writePretty(ann) - val u1_json = channel_update_1_opt.map(serialization.writePretty(_)).orNull - val u2_json = channel_update_2_opt.map(serialization.writePretty(_)).orNull + val json = serialization.write(ann) + val u1_json = channel_update_1_opt.map(serialization.write(_)).orNull + val u2_json = channel_update_2_opt.map(serialization.write(_)).orNull insertStatement.setString(7, json) insertStatement.setString(8, u1_json) insertStatement.setString(9, u2_json) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgChannelsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgChannelsDb.scala index 6dc38e4e13..c3c156ce8d 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgChannelsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgChannelsDb.scala @@ -91,7 +91,7 @@ class PgChannelsDb(implicit ds: DataSource, lock: PgLock) extends ChannelsDb wit // store local commitment signatures anymore, and we want to clean up existing data val state = channelDataCodec.decode(BitVector(rs.getBytes("data"))).require.value val data = channelDataCodec.encode(state).require.toByteArray - val json = serialization.writePretty(state) + val json = serialization.write(state) statement.setBytes(1, data) statement.setString(2, json) statement.setString(3, state.channelId.toHex) @@ -141,7 +141,7 @@ class PgChannelsDb(implicit ds: DataSource, lock: PgLock) extends ChannelsDb wit s"UPDATE $table SET json=?::JSONB WHERE channel_id=?", (rs, statement) => { val state = channelDataCodec.decode(BitVector(rs.getBytes("data"))).require.value - val json = serialization.writePretty(state) + val json = serialization.write(state) statement.setString(1, json) statement.setString(2, state.channelId.toHex) } @@ -160,7 +160,7 @@ class PgChannelsDb(implicit ds: DataSource, lock: PgLock) extends ChannelsDb wit | """.stripMargin)) { statement => statement.setString(1, data.channelId.toHex) statement.setBytes(2, encoded) - statement.setString(3, serialization.writePretty(data)) + statement.setString(3, serialization.write(data)) statement.executeUpdate() } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgNetworkDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgNetworkDb.scala index b2322dd05a..d37bf540bd 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgNetworkDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgNetworkDb.scala @@ -92,24 +92,24 @@ class PgNetworkDb(implicit ds: DataSource) extends NetworkDb with Logging { val channelsTable = if (oldTableName) "channels" else "network.public_channels" migrateTable(connection, connection, nodesTable, - s"UPDATE $nodesTable SET json=?::JSON WHERE node_id=?", + s"UPDATE $nodesTable SET json=?::JSONB WHERE node_id=?", (rs, statement) => { val node = nodeAnnouncementCodec.decode(BitVector(rs.getBytes("data"))).require.value - val json = serialization.writePretty(node) + val json = serialization.write(node) statement.setString(1, json) statement.setString(2, node.nodeId.toString()) } )(logger) migrateTable(connection, connection, channelsTable, - s"UPDATE $channelsTable SET channel_announcement_json=?::JSON, channel_update_1_json=?::JSON, channel_update_2_json=?::JSON WHERE short_channel_id=?", + s"UPDATE $channelsTable SET channel_announcement_json=?::JSONB, channel_update_1_json=?::JSONB, channel_update_2_json=?::JSONB WHERE short_channel_id=?", (rs, statement) => { val ann = channelAnnouncementCodec.decode(rs.getBitVectorOpt("channel_announcement").get).require.value val channel_update_1_opt = rs.getBitVectorOpt("channel_update_1").map(channelUpdateCodec.decode(_).require.value) val channel_update_2_opt = rs.getBitVectorOpt("channel_update_2").map(channelUpdateCodec.decode(_).require.value) - val json = serialization.writePretty(ann) - val u1_json = channel_update_1_opt.map(serialization.writePretty(_)).orNull - val u2_json = channel_update_2_opt.map(serialization.writePretty(_)).orNull + val json = serialization.write(ann) + val u1_json = channel_update_1_opt.map(serialization.write(_)).orNull + val u2_json = channel_update_2_opt.map(serialization.write(_)).orNull statement.setString(1, json) statement.setString(2, u1_json) statement.setString(3, u2_json) @@ -124,7 +124,7 @@ class PgNetworkDb(implicit ds: DataSource) extends NetworkDb with Logging { statement => statement.setString(1, n.nodeId.value.toHex) statement.setBytes(2, nodeAnnouncementCodec.encode(n).require.toByteArray) - statement.setString(3, serialization.writePretty(n)) + statement.setString(3, serialization.write(n)) statement.executeUpdate() } } @@ -135,7 +135,7 @@ class PgNetworkDb(implicit ds: DataSource) extends NetworkDb with Logging { using(pg.prepareStatement("UPDATE network.nodes SET data=?, json=?::JSONB WHERE node_id=?")) { statement => statement.setBytes(1, nodeAnnouncementCodec.encode(n).require.toByteArray) - statement.setString(2, serialization.writePretty(n)) + statement.setString(2, serialization.write(n)) statement.setString(3, n.nodeId.value.toHex) statement.executeUpdate() } @@ -180,7 +180,7 @@ class PgNetworkDb(implicit ds: DataSource) extends NetworkDb with Logging { statement.setString(2, txid.toHex) statement.setBytes(3, channelAnnouncementCodec.encode(c).require.toByteArray) statement.setLong(4, capacity.toLong) - statement.setString(5, serialization.writePretty(c)) + statement.setString(5, serialization.write(c)) statement.executeUpdate() } } @@ -192,7 +192,7 @@ class PgNetworkDb(implicit ds: DataSource) extends NetworkDb with Logging { using(pg.prepareStatement(s"UPDATE network.public_channels SET $column=?, ${column}_json=?::JSONB WHERE short_channel_id=?")) { statement => statement.setBytes(1, channelUpdateCodec.encode(u).require.toByteArray) - statement.setString(2, serialization.writePretty(u)) + statement.setString(2, serialization.write(u)) statement.setLong(3, u.shortChannelId.toLong) statement.executeUpdate() } From 9c7ddcd8745bbea9c7113457e1a1f378435347c7 Mon Sep 17 00:00:00 2001 From: Pierre-Marie Padiou Date: Tue, 24 May 2022 10:09:25 +0200 Subject: [PATCH 076/121] Increase closed timeout from 10s to 1min (#2281) This gives more time to rollback the funding tx in case where we moved from `WAIT_FOR_FUNDING_INTERNAL`->`CLOSED`. We could do it differently, with an additional step, or by only sending the "shutdown" symbol after we receive the wallet response for this specific transition, but it would be more complicated. --- .../src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala index c63762c96c..b3baf3d8a6 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala @@ -1586,7 +1586,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val if (nextState == CLOSED) { // channel is closed, scheduling this actor for self destruction - context.system.scheduler.scheduleOnce(10 seconds, self, Symbol("shutdown")) + context.system.scheduler.scheduleOnce(1 minute, self, Symbol("shutdown")) } if (nextState == OFFLINE) { // we can cancel the timer, we are not expecting anything when disconnected From 45f19f6287ae5809f5df6b4248ed148138d076f7 Mon Sep 17 00:00:00 2001 From: Barry G Becker Date: Mon, 30 May 2022 07:50:03 -0700 Subject: [PATCH 077/121] Fix test when run in Intellij (#2288) (#2289) The path needed to be adjusted from "target" to "eclair-core/target". Also fixed a typo in a comment. --- eclair-core/src/main/scala/fr/acinq/eclair/MilliSatoshi.scala | 2 +- eclair-core/src/test/scala/fr/acinq/eclair/TestUtils.scala | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/MilliSatoshi.scala b/eclair-core/src/main/scala/fr/acinq/eclair/MilliSatoshi.scala index ccf1d12b47..c62933cd8d 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/MilliSatoshi.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/MilliSatoshi.scala @@ -23,7 +23,7 @@ import fr.acinq.bitcoin.scalacompat.{Btc, BtcAmount, MilliBtc, Satoshi, btc2sato */ /** - * One MilliSatoshi is a thousand of a Satoshi, the smallest unit usable in bitcoin + * One MilliSatoshi is a thousandth of a Satoshi, the smallest unit usable in bitcoin */ case class MilliSatoshi(private val underlying: Long) extends Ordered[MilliSatoshi] { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/TestUtils.scala b/eclair-core/src/test/scala/fr/acinq/eclair/TestUtils.scala index 966ebab908..4979d559af 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/TestUtils.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/TestUtils.scala @@ -36,7 +36,7 @@ object TestUtils { val BUILD_DIRECTORY = sys .props .get("buildDirectory") // this is defined if we run from maven - .getOrElse(new File(sys.props("user.dir"), "target").getAbsolutePath) // otherwise we probably are in intellij, so we build it manually assuming that user.dir == path to the module + .getOrElse(new File(sys.props("user.dir"), "eclair-core/target").getAbsolutePath) // otherwise we probably are in intellij, so we build it manually assuming that user.dir == path to the module def availablePort: Int = synchronized { var serverSocket: ServerSocket = null From 749d92ba39cc21f90a72aa9915ac62335397ade1 Mon Sep 17 00:00:00 2001 From: Bastien Teinturier <31281497+t-bast@users.noreply.github.com> Date: Tue, 31 May 2022 17:20:59 +0200 Subject: [PATCH 078/121] Add option to disable sending remote addr in init (#2285) When running behind a load balancer that doesn't preserve the client source IP address, it doesn't make sense to include the `remote-address` tlv in our `init` message, it will contain an IP address that is completely unrelated to our peer. Fixes #2256 --- eclair-core/src/main/resources/reference.conf | 3 +++ .../src/main/scala/fr/acinq/eclair/NodeParams.scala | 3 ++- .../src/main/scala/fr/acinq/eclair/io/PeerConnection.scala | 7 ++++--- .../fr/acinq/eclair/remote/EclairInternalsSerializer.scala | 5 +++-- .../src/test/scala/fr/acinq/eclair/TestConstants.scala | 6 ++++-- 5 files changed, 16 insertions(+), 8 deletions(-) diff --git a/eclair-core/src/main/resources/reference.conf b/eclair-core/src/main/resources/reference.conf index bb29d7f0d5..56b1961eba 100644 --- a/eclair-core/src/main/resources/reference.conf +++ b/eclair-core/src/main/resources/reference.conf @@ -209,6 +209,9 @@ eclair { ping-interval = 30 seconds ping-timeout = 20 seconds // will disconnect if peer takes longer than that to respond ping-disconnect = true // disconnect if no answer to our pings + // When enabled, if we receive an incoming connection, we will echo the source IP address in our init message. + // This should be disabled if your node is behind a load balancer that doesn't preserve source IP addresses. + send-remote-address-init = true } auto-reconnect = true diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala index ba73517dbf..7fedf8d21d 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala @@ -482,7 +482,8 @@ object NodeParams extends Logging { pingDisconnect = config.getBoolean("peer-connection.ping-disconnect"), maxRebroadcastDelay = FiniteDuration(config.getDuration("router.broadcast-interval").getSeconds, TimeUnit.SECONDS), // it makes sense to not delay rebroadcast by more than the rebroadcast period killIdleDelay = FiniteDuration(config.getDuration("onion-messages.kill-transient-connection-after").getSeconds, TimeUnit.SECONDS), - maxOnionMessagesPerSecond = config.getInt("onion-messages.max-per-peer-per-second") + maxOnionMessagesPerSecond = config.getInt("onion-messages.max-per-peer-per-second"), + sendRemoteAddressInit = config.getBoolean("peer-connection.send-remote-address-init"), ), routerConf = RouterConf( watchSpentWindow = watchSpentWindow, diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/PeerConnection.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/PeerConnection.scala index e437604647..0ad5da48b8 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/io/PeerConnection.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/PeerConnection.scala @@ -28,7 +28,7 @@ import fr.acinq.eclair.remote.EclairInternalsSerializer.RemoteTypes import fr.acinq.eclair.router.Router._ import fr.acinq.eclair.wire.protocol import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{FSMDiagnosticActorLogging, Feature, Features, InitFeature, Logs, TimestampMilli, TimestampSecond} +import fr.acinq.eclair.{FSMDiagnosticActorLogging, Features, InitFeature, Logs, TimestampMilli, TimestampSecond} import scodec.Attempt import scodec.bits.ByteVector @@ -103,7 +103,7 @@ class PeerConnection(keyPair: KeyPair, conf: PeerConnection.Conf, switchboard: A Metrics.PeerConnectionsConnecting.withTag(Tags.ConnectionState, Tags.ConnectionStates.Initializing).increment() log.info(s"using features=$localFeatures") val localInit = d.pendingAuth.address match { - case remoteAddress if !d.pendingAuth.outgoing && NodeAddress.isPublicIPAddress(remoteAddress) => protocol.Init(localFeatures, TlvStream(InitTlv.Networks(chainHash :: Nil), InitTlv.RemoteAddress(remoteAddress))) + case remoteAddress if !d.pendingAuth.outgoing && conf.sendRemoteAddressInit && NodeAddress.isPublicIPAddress(remoteAddress) => protocol.Init(localFeatures, TlvStream(InitTlv.Networks(chainHash :: Nil), InitTlv.RemoteAddress(remoteAddress))) case _ => protocol.Init(localFeatures, TlvStream(InitTlv.Networks(chainHash :: Nil))) } d.transport ! localInit @@ -528,7 +528,8 @@ object PeerConnection { pingDisconnect: Boolean, maxRebroadcastDelay: FiniteDuration, killIdleDelay: FiniteDuration, - maxOnionMessagesPerSecond: Int) + maxOnionMessagesPerSecond: Int, + sendRemoteAddressInit: Boolean) // @formatter:off diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/remote/EclairInternalsSerializer.scala b/eclair-core/src/main/scala/fr/acinq/eclair/remote/EclairInternalsSerializer.scala index d343fb6bc3..5d0c6a3a8b 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/remote/EclairInternalsSerializer.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/remote/EclairInternalsSerializer.scala @@ -31,7 +31,7 @@ import fr.acinq.eclair.wire.protocol.CommonCodecs._ import fr.acinq.eclair.wire.protocol.LightningMessageCodecs._ import fr.acinq.eclair.wire.protocol.QueryChannelRangeTlv.queryFlagsCodec import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{CltvExpiryDelta, Feature, Features, InitFeature} +import fr.acinq.eclair.{CltvExpiryDelta, Feature, Features} import scodec._ import scodec.codecs._ @@ -108,7 +108,8 @@ object EclairInternalsSerializer { ("pingDisconnect" | bool(8)) :: ("maxRebroadcastDelay" | finiteDurationCodec) :: ("killIdleDelay" | finiteDurationCodec) :: - ("maxOnionMessagesPerSecond" | int32)).as[PeerConnection.Conf] + ("maxOnionMessagesPerSecond" | int32) :: + ("sendRemoteAddressInit" | bool(8))).as[PeerConnection.Conf] val peerConnectionDoSyncCodec: Codec[PeerConnection.DoSync] = bool(8).as[PeerConnection.DoSync] diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala b/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala index 7add1eccac..9c254fa4ce 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala @@ -156,7 +156,8 @@ object TestConstants { pingDisconnect = true, maxRebroadcastDelay = 5 seconds, killIdleDelay = 1 seconds, - maxOnionMessagesPerSecond = 10 + maxOnionMessagesPerSecond = 10, + sendRemoteAddressInit = true, ), routerConf = RouterConf( watchSpentWindow = 1 second, @@ -294,7 +295,8 @@ object TestConstants { pingDisconnect = true, maxRebroadcastDelay = 5 seconds, killIdleDelay = 10 seconds, - maxOnionMessagesPerSecond = 10 + maxOnionMessagesPerSecond = 10, + sendRemoteAddressInit = true, ), routerConf = RouterConf( watchSpentWindow = 1 second, From 088de6f666e05438924838f9cc5d40744b316963 Mon Sep 17 00:00:00 2001 From: Bastien Teinturier <31281497+t-bast@users.noreply.github.com> Date: Wed, 1 Jun 2022 19:21:28 +0200 Subject: [PATCH 079/121] Don't RBF anchor if commit is confirmed (#2283) Even though we use our anchor to CPFP our commit tx, the commit tx could confirm on its own. If that happens, we don't care about the anchor tx so we shouldn't RBF it. Ideally we'd like to cancel it but that's impossible, so we just wait for it to either confirm or drop out of mempools after a while. --- .../channel/publish/MempoolTxMonitor.scala | 11 +++++++++-- .../channel/publish/ReplaceableTxPublisher.scala | 16 ++++++++++++---- .../channel/publish/MempoolTxMonitorSpec.scala | 16 +++++++++++++++- 3 files changed, 36 insertions(+), 7 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/MempoolTxMonitor.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/MempoolTxMonitor.scala index 270ee49db6..5ae49bd53f 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/MempoolTxMonitor.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/MempoolTxMonitor.scala @@ -44,6 +44,7 @@ object MempoolTxMonitor { private case class InputStatus(spentConfirmed: Boolean, spentUnconfirmed: Boolean) extends Command private case class CheckInputFailed(reason: Throwable) extends Command private case class TxConfirmations(confirmations: Int, blockHeight: BlockHeight) extends Command + private case class ParentTxStatus(confirmed: Boolean, blockHeight: BlockHeight) extends Command private case object TxNotFound extends Command private case class GetTxConfirmationsFailed(reason: Throwable) extends Command private case class WrappedCurrentBlockHeight(currentBlockHeight: BlockHeight) extends Command @@ -58,7 +59,7 @@ object MempoolTxMonitor { sealed trait TxResult sealed trait IntermediateTxResult extends TxResult /** The transaction is still unconfirmed and available in the mempool. */ - case class TxInMempool(txid: ByteVector32, blockHeight: BlockHeight) extends IntermediateTxResult + case class TxInMempool(txid: ByteVector32, blockHeight: BlockHeight, parentConfirmed: Boolean) extends IntermediateTxResult /** The transaction is confirmed, but hasn't reached min depth yet, we should wait for more confirmations. */ case class TxRecentlyConfirmed(txid: ByteVector32, confirmations: Int) extends IntermediateTxResult sealed trait FinalTxResult extends TxResult @@ -147,7 +148,10 @@ private class MempoolTxMonitor(nodeParams: NodeParams, Behaviors.same case TxConfirmations(confirmations, currentBlockHeight) => if (confirmations == 0) { - cmd.replyTo ! TxInMempool(cmd.tx.txid, currentBlockHeight) + context.pipeToSelf(bitcoinClient.getTxConfirmations(cmd.input.txid)) { + case Success(parentConfirmations_opt) => ParentTxStatus(parentConfirmations_opt.exists(_ >= 1), currentBlockHeight) + case Failure(reason) => GetTxConfirmationsFailed(reason) + } Behaviors.same } else if (confirmations < nodeParams.channelConf.minDepthBlocks) { log.info("txid={} has {} confirmations, waiting to reach min depth", cmd.tx.txid, confirmations) @@ -158,6 +162,9 @@ private class MempoolTxMonitor(nodeParams: NodeParams, context.system.eventStream ! EventStream.Publish(TransactionConfirmed(txPublishContext.channelId_opt.getOrElse(ByteVector32.Zeroes), txPublishContext.remoteNodeId, cmd.tx)) sendFinalResult(TxDeeplyBuried(cmd.tx), Some(messageAdapter)) } + case ParentTxStatus(confirmed, currentBlockHeight) => + cmd.replyTo ! TxInMempool(cmd.tx.txid, currentBlockHeight, confirmed) + Behaviors.same case TxNotFound => log.warn("txid={} has been evicted from the mempool", cmd.tx.txid) checkInputStatus(cmd.input) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPublisher.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPublisher.scala index 3368783710..ca68a9479a 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPublisher.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPublisher.scala @@ -25,6 +25,7 @@ import fr.acinq.eclair.blockchain.fee.{FeeEstimator, FeeratePerKw} import fr.acinq.eclair.channel.publish.ReplaceableTxFunder.FundedTx import fr.acinq.eclair.channel.publish.ReplaceableTxPrePublisher.{ClaimLocalAnchorWithWitnessData, ReplaceableTxWithWitnessData} import fr.acinq.eclair.channel.publish.TxPublisher.TxPublishContext +import fr.acinq.eclair.transactions.Transactions import fr.acinq.eclair.{BlockHeight, NodeParams} import scala.concurrent.duration.{DurationInt, DurationLong} @@ -195,10 +196,17 @@ private class ReplaceableTxPublisher(nodeParams: NodeParams, Behaviors.receiveMessagePartial { case WrappedTxResult(txResult) => txResult match { - case MempoolTxMonitor.TxInMempool(_, currentBlockHeight) => - context.pipeToSelf(hasEnoughSafeUtxos(nodeParams.onChainFeeConf.feeTargets.safeUtxosThreshold)) { - case Success(isSafe) => CheckUtxosResult(isSafe, currentBlockHeight) - case Failure(_) => CheckUtxosResult(isSafe = false, currentBlockHeight) // if we can't check our utxos, we assume the worst + case MempoolTxMonitor.TxInMempool(_, currentBlockHeight, parentConfirmed) => + val shouldRbf = cmd.txInfo match { + // Our commit tx was confirmed on its own, so there's no need to increase fees on the anchor tx. + case _: Transactions.ClaimLocalAnchorOutputTx if parentConfirmed => false + case _ => true + } + if (shouldRbf) { + context.pipeToSelf(hasEnoughSafeUtxos(nodeParams.onChainFeeConf.feeTargets.safeUtxosThreshold)) { + case Success(isSafe) => CheckUtxosResult(isSafe, currentBlockHeight) + case Failure(_) => CheckUtxosResult(isSafe = false, currentBlockHeight) // if we can't check our utxos, we assume the worst + } } Behaviors.same case MempoolTxMonitor.TxRecentlyConfirmed(_, _) => Behaviors.same // just wait for the tx to be deeply buried diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/MempoolTxMonitorSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/MempoolTxMonitorSpec.scala index f4b53544c0..168b00f185 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/MempoolTxMonitorSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/MempoolTxMonitorSpec.scala @@ -70,6 +70,20 @@ class MempoolTxMonitorSpec extends TestKitBaseClass with AnyFunSuiteLike with Bi awaitCond(getMempool(bitcoinClient, probe).exists(_.txid == txId)) } + test("transaction still in mempool with unconfirmed parent") { + val f = createFixture() + import f._ + + val tx = createSpendP2WPKH(parentTx, priv, priv.publicKey, 1_000 sat, 0, 0) + monitor ! Publish(probe.ref, tx, tx.txIn.head.outPoint, "test-tx", 50 sat) + waitTxInMempool(bitcoinClient, tx.txid, probe) + + // NB: we don't really generate a block, we're testing the case where both txs are still in the mempool. + system.eventStream.publish(CurrentBlockHeight(currentBlockHeight())) + probe.expectMsg(TxInMempool(tx.txid, currentBlockHeight(), parentConfirmed = false)) + probe.expectNoMessage(100 millis) + } + test("transaction confirmed") { val f = createFixture() import f._ @@ -83,7 +97,7 @@ class MempoolTxMonitorSpec extends TestKitBaseClass with AnyFunSuiteLike with Bi // NB: we don't really generate a block, we're testing the case where the tx is still in the mempool. system.eventStream.publish(CurrentBlockHeight(currentBlockHeight())) - probe.expectMsg(TxInMempool(tx.txid, currentBlockHeight())) + probe.expectMsg(TxInMempool(tx.txid, currentBlockHeight(), parentConfirmed = true)) probe.expectNoMessage(100 millis) assert(TestConstants.Alice.nodeParams.channelConf.minDepthBlocks > 1) From ee46c562812660f36524175aef6b993ff7ecfdf9 Mon Sep 17 00:00:00 2001 From: Bastien Teinturier <31281497+t-bast@users.noreply.github.com> Date: Wed, 1 Jun 2022 21:58:42 +0200 Subject: [PATCH 080/121] Remove dependency between dual funding and anchors (#2297) This dependency didn't actually make sense, and we're removing it from the spec as well. --- .../src/main/scala/fr/acinq/eclair/Features.scala | 1 - .../test/scala/fr/acinq/eclair/FeaturesSpec.scala | 15 --------------- 2 files changed, 16 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala index 5eed1bffd6..3138f995ab 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala @@ -278,7 +278,6 @@ object Features { BasicMultiPartPayment -> (PaymentSecret :: Nil), AnchorOutputs -> (StaticRemoteKey :: Nil), AnchorOutputsZeroFeeHtlcTx -> (StaticRemoteKey :: Nil), - DualFunding -> (AnchorOutputsZeroFeeHtlcTx :: Nil), TrampolinePaymentPrototype -> (PaymentSecret :: Nil), KeySend -> (VariableLengthOnion :: Nil) ) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/FeaturesSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/FeaturesSpec.scala index 65c83af986..85c4fc0c80 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/FeaturesSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/FeaturesSpec.scala @@ -95,21 +95,6 @@ class FeaturesSpec extends AnyFunSuite { bin" 100000000001000000000000" -> true, bin" 010000000010000000000000" -> true, bin" 010000000001000000000000" -> true, - // option_dual_fund depends on option_anchors_zero_fee_htlc_tx, which itself depends on option_static_remotekey - bin"00100000000000000000000000000000" -> false, - bin"00010000000000000000000000000000" -> false, - bin"00100000100000000000000000000000" -> false, - bin"00100000010000000000000000000000" -> false, - bin"00010000100000000000000000000000" -> false, - bin"00010000010000000000000000000000" -> false, - bin"00100000100000000010000000000000" -> true, - bin"00100000100000000001000000000000" -> true, - bin"00100000010000000010000000000000" -> true, - bin"00100000010000000001000000000000" -> true, - bin"00010000100000000010000000000000" -> true, - bin"00010000100000000001000000000000" -> true, - bin"00010000010000000010000000000000" -> true, - bin"00010000010000000001000000000000" -> true, ) for ((testCase, valid) <- testCases) { From 9610fe30e308af64fd30d0953ccf2eb0da69b67a Mon Sep 17 00:00:00 2001 From: Pierre-Marie Padiou Date: Fri, 3 Jun 2022 09:58:53 +0200 Subject: [PATCH 081/121] Define a proper base class for fixture tests (#2286) This PR does two main things: - introduce a new `FixtureSpec` base class for tests that involve a fixture. See the scaladoc for more info. - add new simple integration tests in package `integration.basic`. They are based on `MinimalNodeFixture`, which is a full setup for a node with real actors, except the bitcoin part (watcher/wallet) which is mocked. They are much lighter than our previous integration tests, which allow us to keep each test individual, as opposed to having all tests of the same suite depend on each other. We can define more complex fixtures with any number of nodes. Other minor improvements: - update scalatest version - simplify `ChannelStateTestsHelperMethods` - replace all === by == Triple equals allows customizing the equality method, but we never use that. Detailed errors (which is what we were looking for) are provided by mixing in scalatest's `Assertions` and using the regular `assert(x==y)` syntax. The `Assertions` trait is already mixed in by default in all scalatest suites. --- eclair-core/pom.xml | 2 +- .../src/main/scala/fr/acinq/eclair/Logs.scala | 9 +- .../fr/acinq/eclair/channel/fsm/Channel.scala | 2 +- .../main/scala/fr/acinq/eclair/io/Peer.scala | 2 +- .../fr/acinq/eclair/io/PeerConnection.scala | 15 +- .../fr/acinq/eclair/io/ReconnectionTask.scala | 2 +- .../scala/fr/acinq/eclair/router/Router.scala | 11 +- .../src/test/resources/logback-test.xml | 2 +- .../fr/acinq/eclair/CltvExpirySpec.scala | 22 +- .../fr/acinq/eclair/EclairImplSpec.scala | 128 ++--- .../scala/fr/acinq/eclair/FeaturesSpec.scala | 36 +- .../fr/acinq/eclair/MilliSatoshiSpec.scala | 44 +- .../scala/fr/acinq/eclair/PackageSpec.scala | 2 +- .../scala/fr/acinq/eclair/StartupSpec.scala | 16 +- .../scala/fr/acinq/eclair/TestConstants.scala | 6 +- .../scala/fr/acinq/eclair/TestDatabases.scala | 3 +- .../scala/fr/acinq/eclair/UInt64Spec.scala | 24 +- .../eclair/balance/CheckBalanceSpec.scala | 22 +- .../blockchain/DummyOnChainWallet.scala | 10 +- .../bitcoind/BitcoinCoreClientSpec.scala | 70 +-- .../blockchain/bitcoind/ZmqWatcherSpec.scala | 38 +- .../fee/BitcoinCoreFeeProviderSpec.scala | 2 +- .../fee/FallbackFeeProviderSpec.scala | 22 +- .../blockchain/fee/FeeEstimatorSpec.scala | 42 +- .../blockchain/fee/FeeProviderSpec.scala | 28 +- .../watchdogs/BlockchainWatchdogSpec.scala | 28 +- .../watchdogs/ExplorerApiSpec.scala | 4 +- .../eclair/channel/ChannelDataSpec.scala | 64 +-- .../eclair/channel/ChannelFeaturesSpec.scala | 34 +- .../eclair/channel/CommitmentsSpec.scala | 2 +- .../eclair/channel/DustExposureSpec.scala | 38 +- .../fr/acinq/eclair/channel/FuzzySpec.scala | 2 +- .../fr/acinq/eclair/channel/HelpersSpec.scala | 28 +- .../acinq/eclair/channel/RegisterSpec.scala | 2 +- .../fr/acinq/eclair/channel/RestoreSpec.scala | 10 +- .../publish/FinalTxPublisherSpec.scala | 8 +- .../publish/MempoolTxMonitorSpec.scala | 6 +- .../publish/ReplaceableTxFunderSpec.scala | 46 +- .../publish/ReplaceableTxPublisherSpec.scala | 196 ++++---- .../channel/publish/TxPublisherSpec.scala | 38 +- .../publish/TxTimeLocksMonitorSpec.scala | 8 +- .../ChannelStateTestsHelperMethods.scala | 90 ++-- .../a/WaitForAcceptChannelStateSpec.scala | 48 +- .../a/WaitForOpenChannelStateSpec.scala | 46 +- .../b/WaitForFundingCreatedStateSpec.scala | 4 +- .../b/WaitForFundingSignedStateSpec.scala | 6 +- .../c/WaitForFundingConfirmedStateSpec.scala | 22 +- .../c/WaitForFundingLockedStateSpec.scala | 20 +- .../channel/states/e/NormalStateSpec.scala | 446 +++++++++--------- .../channel/states/e/OfflineStateSpec.scala | 38 +- .../channel/states/f/ShutdownStateSpec.scala | 120 ++--- .../states/g/NegotiatingStateSpec.scala | 134 +++--- .../channel/states/h/ClosingStateSpec.scala | 374 +++++++-------- .../eclair/crypto/ChaCha20Poly1305Spec.scala | 4 +- .../fr/acinq/eclair/crypto/RandomSpec.scala | 2 +- .../fr/acinq/eclair/crypto/ShaChainSpec.scala | 2 +- .../fr/acinq/eclair/crypto/SphinxSpec.scala | 130 ++--- .../eclair/crypto/TransportHandlerSpec.scala | 12 +- .../keymanager/LocalNodeKeyManagerSpec.scala | 2 +- .../fr/acinq/eclair/db/AuditDbSpec.scala | 40 +- .../fr/acinq/eclair/db/ChannelsDbSpec.scala | 32 +- .../acinq/eclair/db/DualDatabasesSpec.scala | 4 +- .../fr/acinq/eclair/db/NetworkDbSpec.scala | 36 +- .../fr/acinq/eclair/db/PaymentsDbSpec.scala | 92 ++-- .../fr/acinq/eclair/db/PeersDbSpec.scala | 32 +- .../eclair/db/PendingCommandsDbSpec.scala | 14 +- .../fr/acinq/eclair/db/PgUtilsSpec.scala | 8 +- .../eclair/db/SqliteFeeratesDbSpec.scala | 4 +- .../fr/acinq/eclair/db/SqliteUtilsSpec.scala | 16 +- .../integration/ChannelIntegrationSpec.scala | 38 +- .../integration/MessageIntegrationSpec.scala | 14 +- .../integration/PaymentIntegrationSpec.scala | 134 +++--- .../integration/StartupIntegrationSpec.scala | 6 +- .../basic/ThreeNodesIntegrationSpec.scala | 73 +++ .../basic/TwoNodesIntegrationSpec.scala | 96 ++++ .../basic/fixtures/MinimalNodeFixture.scala | 271 +++++++++++ .../basic/fixtures/ThreeNodesFixture.scala | 31 ++ .../basic/fixtures/TwoNodesFixture.scala | 27 ++ .../interop/rustytests/RustyTestsSpec.scala | 22 +- .../fr/acinq/eclair/io/MessageRelaySpec.scala | 22 +- .../fr/acinq/eclair/io/NodeURISpec.scala | 2 +- .../acinq/eclair/io/PeerConnectionSpec.scala | 20 +- .../scala/fr/acinq/eclair/io/PeerSpec.scala | 66 +-- .../eclair/io/ReconnectionTaskSpec.scala | 26 +- .../fr/acinq/eclair/io/SwitchboardSpec.scala | 20 +- .../eclair/message/OnionMessagesSpec.scala | 8 +- .../fr/acinq/eclair/message/PostmanSpec.scala | 16 +- .../eclair/payment/Bolt11InvoiceSpec.scala | 212 ++++----- .../eclair/payment/Bolt12InvoiceSpec.scala | 58 +-- .../eclair/payment/MultiPartHandlerSpec.scala | 92 ++-- .../payment/MultiPartPaymentFSMSpec.scala | 44 +- .../MultiPartPaymentLifecycleSpec.scala | 140 +++--- .../eclair/payment/PaymentInitiatorSpec.scala | 106 ++--- .../eclair/payment/PaymentLifecycleSpec.scala | 100 ++-- .../eclair/payment/PaymentPacketSpec.scala | 218 ++++----- .../payment/PostRestartHtlcCleanerSpec.scala | 70 +-- .../payment/receive/InvoicePurgerSpec.scala | 14 +- .../payment/relay/ChannelRelayerSpec.scala | 42 +- .../payment/relay/NodeRelayerSpec.scala | 160 +++---- .../eclair/payment/relay/RelayerSpec.scala | 28 +- .../eclair/router/AnnouncementsSpec.scala | 14 +- .../acinq/eclair/router/BaseRouterSpec.scala | 28 +- .../router/ChannelRangeQueriesSpec.scala | 24 +- .../router/ChannelRouterIntegrationSpec.scala | 4 +- .../fr/acinq/eclair/router/GraphSpec.scala | 72 +-- .../eclair/router/RouteCalculationSpec.scala | 256 +++++----- .../fr/acinq/eclair/router/RouterSpec.scala | 106 ++--- .../acinq/eclair/router/RoutingSyncSpec.scala | 62 +-- .../acinq/eclair/testutils/FixtureSpec.scala | 82 ++++ .../eclair/tor/TorProtocolHandlerSpec.scala | 16 +- .../transactions/CommitmentSpecSpec.scala | 22 +- .../eclair/transactions/TestVectorsSpec.scala | 6 +- .../transactions/TransactionsSpec.scala | 152 +++--- .../wire/internal/CommandCodecsSpec.scala | 8 +- .../internal/channel/ChannelCodecsSpec.scala | 46 +- .../channel/version0/ChannelCodecs0Spec.scala | 22 +- .../channel/version1/ChannelCodecs1Spec.scala | 48 +- .../channel/version2/ChannelCodecs2Spec.scala | 20 +- .../channel/version3/ChannelCodecs3Spec.scala | 76 +-- .../wire/protocol/CommonCodecsSpec.scala | 68 +-- .../protocol/ExtendedQueriesCodecsSpec.scala | 24 +- .../protocol/FailureMessageCodecsSpec.scala | 20 +- .../protocol/LightningMessageCodecsSpec.scala | 64 +-- .../protocol/MessageOnionCodecsSpec.scala | 28 +- .../eclair/wire/protocol/OffersSpec.scala | 50 +- .../wire/protocol/PaymentOnionSpec.scala | 88 ++-- .../wire/protocol/RouteBlindingSpec.scala | 6 +- .../eclair/wire/protocol/TlvCodecsSpec.scala | 56 +-- .../acinq/eclair/router/FrontRouterSpec.scala | 4 +- eclair-node/pom.xml | 2 +- .../fr/acinq/eclair/api/ApiServiceSpec.scala | 26 +- pom.xml | 4 +- 132 files changed, 3567 insertions(+), 2965 deletions(-) create mode 100644 eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/ThreeNodesIntegrationSpec.scala create mode 100644 eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/TwoNodesIntegrationSpec.scala create mode 100644 eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/fixtures/MinimalNodeFixture.scala create mode 100644 eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/fixtures/ThreeNodesFixture.scala create mode 100644 eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/fixtures/TwoNodesFixture.scala create mode 100644 eclair-core/src/test/scala/fr/acinq/eclair/testutils/FixtureSpec.scala diff --git a/eclair-core/pom.xml b/eclair-core/pom.xml index 676b1c7b11..86185396fa 100644 --- a/eclair-core/pom.xml +++ b/eclair-core/pom.xml @@ -302,7 +302,7 @@ org.mockito mockito-scala-scalatest_${scala.version.short} - 1.5.9 + 1.17.5 test diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Logs.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Logs.scala index 4a045b61c3..46456d0521 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Logs.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Logs.scala @@ -49,15 +49,18 @@ object Logs { parentPaymentId_opt: Option[UUID] = None, paymentId_opt: Option[UUID] = None, paymentHash_opt: Option[ByteVector32] = None, - txPublishId_opt: Option[UUID] = None): Map[String, String] = + txPublishId_opt: Option[UUID] = None, + nodeAlias_opt: Option[String] = None): Map[String, String] = Seq( + // nb: we preformat MDC values so that there is no white spaces in logs when they are not defined category_opt.map(l => "category" -> s" ${l.category}"), - remoteNodeId_opt.map(n => "nodeId" -> s" n:$n"), // nb: we preformat MDC values so that there is no white spaces in logs when they are not defined + remoteNodeId_opt.map(n => "nodeId" -> s" n:$n"), channelId_opt.map(c => "channelId" -> s" c:$c"), parentPaymentId_opt.map(p => "parentPaymentId" -> s" p:$p"), paymentId_opt.map(i => "paymentId" -> s" i:$i"), paymentHash_opt.map(h => "paymentHash" -> s" h:$h"), - txPublishId_opt.map(t => "txPublishId" -> s" t:$t") + txPublishId_opt.map(t => "txPublishId" -> s" t:$t"), + nodeAlias_opt.map(a => "nodeAlias" -> s" a:$a"), ).flatten.toMap /** diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala index b3baf3d8a6..adc691063d 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala @@ -1884,7 +1884,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val case INPUT_RESTORED(data) => data.channelId case _ => stateData.channelId } - Logs.mdc(category_opt, remoteNodeId_opt = Some(remoteNodeId), channelId_opt = Some(id)) + Logs.mdc(category_opt, remoteNodeId_opt = Some(remoteNodeId), channelId_opt = Some(id), nodeAlias_opt = Some(nodeParams.alias)) } // we let the peer decide what to do diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala index e6da8cf17e..0fa63ef102 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala @@ -410,7 +410,7 @@ class Peer(val nodeParams: NodeParams, remoteNodeId: PublicKey, wallet: OnChainA } override def mdc(currentMessage: Any): MDC = { - Logs.mdc(LogCategory(currentMessage), Some(remoteNodeId), Logs.channelId(currentMessage)) + Logs.mdc(LogCategory(currentMessage), Some(remoteNodeId), Logs.channelId(currentMessage), nodeAlias_opt = Some(nodeParams.alias)) } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/PeerConnection.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/PeerConnection.scala index 0ad5da48b8..6297084eb5 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/io/PeerConnection.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/PeerConnection.scala @@ -16,7 +16,7 @@ package fr.acinq.eclair.io -import akka.actor.{ActorRef, FSM, OneForOneStrategy, PoisonPill, Props, SupervisorStrategy, Terminated} +import akka.actor.{ActorRef, FSM, OneForOneStrategy, PoisonPill, Props, Stash, SupervisorStrategy, Terminated} import akka.event.Logging.MDC import fr.acinq.bitcoin.scalacompat.ByteVector32 import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey @@ -56,7 +56,7 @@ import scala.util.Random * * Created by PM on 11/03/2020. */ -class PeerConnection(keyPair: KeyPair, conf: PeerConnection.Conf, switchboard: ActorRef, router: ActorRef) extends FSMDiagnosticActorLogging[PeerConnection.State, PeerConnection.Data] { +class PeerConnection(keyPair: KeyPair, conf: PeerConnection.Conf, switchboard: ActorRef, router: ActorRef) extends FSMDiagnosticActorLogging[PeerConnection.State, PeerConnection.Data] with Stash { import PeerConnection._ @@ -95,6 +95,11 @@ class PeerConnection(keyPair: KeyPair, conf: PeerConnection.Conf, switchboard: A log.warning(s"authentication timed out after ${conf.authTimeout}") d.pendingAuth.origin_opt.foreach(_ ! ConnectionResult.AuthenticationFailed("authentication timed out")) stop(FSM.Normal) + + case Event(_: protocol.Init, _) => + log.debug("stashing remote init") + stash() + stay() } when(BEFORE_INIT) { @@ -108,7 +113,13 @@ class PeerConnection(keyPair: KeyPair, conf: PeerConnection.Conf, switchboard: A } d.transport ! localInit startSingleTimer(INIT_TIMER, InitTimeout, conf.initTimeout) + unstashAll() // unstash remote init if it already arrived goto(INITIALIZING) using InitializingData(chainHash, d.pendingAuth, d.remoteNodeId, d.transport, peer, localInit, doSync, d.isPersistent) + + case Event(_: protocol.Init, _) => + log.debug("stashing remote init") + stash() + stay() } when(INITIALIZING) { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/ReconnectionTask.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/ReconnectionTask.scala index 8946afeb54..a2c5602904 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/io/ReconnectionTask.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/ReconnectionTask.scala @@ -157,7 +157,7 @@ class ReconnectionTask(nodeParams: NodeParams, remoteNodeId: PublicKey) extends } override def mdc(currentMessage: Any): MDC = { - Logs.mdc(Some(LogCategory.CONNECTION), Some(remoteNodeId)) + Logs.mdc(Some(LogCategory.CONNECTION), Some(remoteNodeId), nodeAlias_opt = Some(nodeParams.alias)) } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala index e1adb6a0a4..42caea67d0 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala @@ -257,12 +257,13 @@ class Router(val nodeParams: NodeParams, watcher: typed.ActorRef[ZmqWatcher.Comm override def mdc(currentMessage: Any): MDC = { val category_opt = LogCategory(currentMessage) - currentMessage match { - case s: SendChannelQuery => Logs.mdc(category_opt, remoteNodeId_opt = Some(s.remoteNodeId)) - case prm: PeerRoutingMessage => Logs.mdc(category_opt, remoteNodeId_opt = Some(prm.remoteNodeId)) - case lcu: LocalChannelUpdate => Logs.mdc(category_opt, remoteNodeId_opt = Some(lcu.remoteNodeId)) - case _ => Logs.mdc(category_opt) + val remoteNodeId_opt = currentMessage match { + case s: SendChannelQuery => Some(s.remoteNodeId) + case prm: PeerRoutingMessage => Some(prm.remoteNodeId) + case lcu: LocalChannelUpdate => Some(lcu.remoteNodeId) + case _ => None } + Logs.mdc(category_opt, remoteNodeId_opt = remoteNodeId_opt, nodeAlias_opt = Some(nodeParams.alias)) } } diff --git a/eclair-core/src/test/resources/logback-test.xml b/eclair-core/src/test/resources/logback-test.xml index e3f0af56c7..91441698aa 100644 --- a/eclair-core/src/test/resources/logback-test.xml +++ b/eclair-core/src/test/resources/logback-test.xml @@ -20,7 +20,7 @@ System.out - %date{HH:mm:ss.SSS} %highlight(%-5level) %replace(%logger{24}){'\$.*',''}%X{category}%X{nodeId}%X{channelId}%X{paymentHash}%.-11X{parentPaymentId}%.-11X{paymentId}%.-11X{txPublishId} - %msg%ex{12}%n + %date{HH:mm:ss.SSS} %highlight(%-5level) %replace(%logger{24}){'\$.*',''}%X{category}%.-9X{nodeId}%.-11X{channelId}%.-11X{paymentHash}%.-11X{parentPaymentId}%.-11X{paymentId}%.-11X{txPublishId}%.-11X{nodeAlias} - %msg%ex{12}%n diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/CltvExpirySpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/CltvExpirySpec.scala index e77446a728..b319a494d2 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/CltvExpirySpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/CltvExpirySpec.scala @@ -27,15 +27,15 @@ class CltvExpirySpec extends AnyFunSuite with ParallelTestExecution { test("cltv expiry delta") { val d = CltvExpiryDelta(561) - assert(d.toInt === 561) + assert(d.toInt == 561) // add - assert(d + 5 === CltvExpiryDelta(566)) - assert(d + CltvExpiryDelta(5) === CltvExpiryDelta(566)) + assert(d + 5 == CltvExpiryDelta(566)) + assert(d + CltvExpiryDelta(5) == CltvExpiryDelta(566)) // subtract - assert(d - CltvExpiryDelta(5) === CltvExpiryDelta(556)) - assert(d - CltvExpiryDelta(562) === CltvExpiryDelta(-1)) + assert(d - CltvExpiryDelta(5) == CltvExpiryDelta(556)) + assert(d - CltvExpiryDelta(562) == CltvExpiryDelta(-1)) // compare assert(d <= CltvExpiryDelta(561)) @@ -44,18 +44,18 @@ class CltvExpirySpec extends AnyFunSuite with ParallelTestExecution { assert(d > CltvExpiryDelta(560)) // convert to cltv expiry - assert(d.toCltvExpiry(currentBlockHeight = BlockHeight(1105)) === CltvExpiry(1666)) - assert(d.toCltvExpiry(currentBlockHeight = BlockHeight(1106)) === CltvExpiry(1667)) + assert(d.toCltvExpiry(currentBlockHeight = BlockHeight(1105)) == CltvExpiry(1666)) + assert(d.toCltvExpiry(currentBlockHeight = BlockHeight(1106)) == CltvExpiry(1667)) } test("cltv expiry") { val e = CltvExpiry(1105) - assert(e.toLong === 1105) + assert(e.toLong == 1105) // add - assert(e + CltvExpiryDelta(561) === CltvExpiry(1666)) - assert(e - CltvExpiryDelta(561) === CltvExpiry(544)) - assert(e - CltvExpiry(561) === CltvExpiryDelta(544)) + assert(e + CltvExpiryDelta(561) == CltvExpiry(1666)) + assert(e - CltvExpiryDelta(561) == CltvExpiry(544)) + assert(e - CltvExpiry(561) == CltvExpiryDelta(544)) // compare assert(e <= CltvExpiry(1105)) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/EclairImplSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/EclairImplSpec.scala index f0af4df86f..9154eb3154 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/EclairImplSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/EclairImplSpec.scala @@ -97,13 +97,13 @@ class EclairImplSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with I // standard conversion eclair.open(nodeId, fundingAmount = 10000000L sat, pushAmount_opt = None, channelType_opt = None, fundingFeeratePerByte_opt = Some(FeeratePerByte(5 sat)), announceChannel_opt = None, openTimeout_opt = None) val open = switchboard.expectMsgType[OpenChannel] - assert(open.fundingTxFeeratePerKw_opt === Some(FeeratePerKw(1250 sat))) + assert(open.fundingTxFeeratePerKw_opt == Some(FeeratePerKw(1250 sat))) // check that minimum fee rate of 253 sat/bw is used eclair.open(nodeId, fundingAmount = 10000000L sat, pushAmount_opt = None, channelType_opt = Some(ChannelTypes.StaticRemoteKey), fundingFeeratePerByte_opt = Some(FeeratePerByte(1 sat)), announceChannel_opt = None, openTimeout_opt = None) val open1 = switchboard.expectMsgType[OpenChannel] - assert(open1.fundingTxFeeratePerKw_opt === Some(FeeratePerKw.MinimumFeeratePerKw)) - assert(open1.channelType_opt === Some(ChannelTypes.StaticRemoteKey)) + assert(open1.fundingTxFeeratePerKw_opt == Some(FeeratePerKw.MinimumFeeratePerKw)) + assert(open1.channelType_opt == Some(ChannelTypes.StaticRemoteKey)) } test("call send with passing correct arguments") { f => @@ -114,12 +114,12 @@ class EclairImplSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with I val invoice0 = Bolt11Invoice(Block.RegtestGenesisBlock.hash, Some(123 msat), ByteVector32.Zeroes, nodePrivKey, Left("description"), CltvExpiryDelta(18)) eclair.send(None, 123 msat, invoice0) val send = paymentInitiator.expectMsgType[SendPaymentToNode] - assert(send.externalId === None) - assert(send.recipientNodeId === nodePrivKey.publicKey) - assert(send.recipientAmount === 123.msat) - assert(send.paymentHash === ByteVector32.Zeroes) - assert(send.invoice === invoice0) - assert(send.assistedRoutes === Seq.empty) + assert(send.externalId == None) + assert(send.recipientNodeId == nodePrivKey.publicKey) + assert(send.recipientAmount == 123.msat) + assert(send.paymentHash == ByteVector32.Zeroes) + assert(send.invoice == invoice0) + assert(send.assistedRoutes == Seq.empty) // with assisted routes val externalId1 = "030bb6a5e0c6b203c7e2180fb78c7ba4bdce46126761d8201b91ddac089cdecc87" @@ -127,33 +127,33 @@ class EclairImplSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with I val invoice1 = Bolt11Invoice(Block.RegtestGenesisBlock.hash, Some(123 msat), ByteVector32.Zeroes, nodePrivKey, Left("description"), CltvExpiryDelta(18), None, None, hints) eclair.send(Some(externalId1), 123 msat, invoice1) val send1 = paymentInitiator.expectMsgType[SendPaymentToNode] - assert(send1.externalId === Some(externalId1)) - assert(send1.recipientNodeId === nodePrivKey.publicKey) - assert(send1.recipientAmount === 123.msat) - assert(send1.paymentHash === ByteVector32.Zeroes) - assert(send1.invoice === invoice1) - assert(send1.assistedRoutes === hints) + assert(send1.externalId == Some(externalId1)) + assert(send1.recipientNodeId == nodePrivKey.publicKey) + assert(send1.recipientAmount == 123.msat) + assert(send1.paymentHash == ByteVector32.Zeroes) + assert(send1.invoice == invoice1) + assert(send1.assistedRoutes == hints) // with finalCltvExpiry val externalId2 = "487da196-a4dc-4b1e-92b4-3e5e905e9f3f" val invoice2 = Bolt11Invoice("lntb", Some(123 msat), TimestampSecond.now(), nodePrivKey.publicKey, List(Bolt11Invoice.MinFinalCltvExpiry(96), Bolt11Invoice.PaymentHash(ByteVector32.Zeroes), Bolt11Invoice.Description("description")), ByteVector.empty) eclair.send(Some(externalId2), 123 msat, invoice2) val send2 = paymentInitiator.expectMsgType[SendPaymentToNode] - assert(send2.externalId === Some(externalId2)) - assert(send2.recipientNodeId === nodePrivKey.publicKey) - assert(send2.recipientAmount === 123.msat) - assert(send2.paymentHash === ByteVector32.Zeroes) - assert(send2.invoice === invoice2) + assert(send2.externalId == Some(externalId2)) + assert(send2.recipientNodeId == nodePrivKey.publicKey) + assert(send2.recipientAmount == 123.msat) + assert(send2.paymentHash == ByteVector32.Zeroes) + assert(send2.invoice == invoice2) // with custom route fees parameters eclair.send(None, 123 msat, invoice0, maxFeeFlat_opt = Some(123 sat), maxFeePct_opt = Some(4.20)) val send3 = paymentInitiator.expectMsgType[SendPaymentToNode] - assert(send3.externalId === None) - assert(send3.recipientNodeId === nodePrivKey.publicKey) - assert(send3.recipientAmount === 123.msat) - assert(send3.paymentHash === ByteVector32.Zeroes) - assert(send3.routeParams.boundaries.maxFeeFlat === 123000.msat) // conversion sat -> msat - assert(send3.routeParams.boundaries.maxFeeProportional === 0.042) + assert(send3.externalId == None) + assert(send3.recipientNodeId == nodePrivKey.publicKey) + assert(send3.recipientAmount == 123.msat) + assert(send3.paymentHash == ByteVector32.Zeroes) + assert(send3.routeParams.boundaries.maxFeeFlat == 123000.msat) // conversion sat -> msat + assert(send3.routeParams.boundaries.maxFeeProportional == 0.042) val invalidExternalId = "Robert'); DROP TABLE received_payments; DROP TABLE sent_payments; DROP TABLE payments;" assertThrows[IllegalArgumentException](Await.result(eclair.send(Some(invalidExternalId), 123 msat, invoice0), 50 millis)) @@ -179,13 +179,13 @@ class EclairImplSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with I eclair.nodes().pipeTo(sender.ref) router.expectMsg(Router.GetNodes) router.reply(allNodes) - assert(sender.expectMsgType[Iterable[NodeAnnouncement]].toSet === allNodes.toSet) + assert(sender.expectMsgType[Iterable[NodeAnnouncement]].toSet == allNodes.toSet) } { eclair.nodes(Some(Set(remoteNodeAnn1.nodeId, remoteNodeAnn2.nodeId))).pipeTo(sender.ref) router.expectMsg(Router.GetNodes) router.reply(allNodes) - assert(sender.expectMsgType[Iterable[NodeAnnouncement]].toSet === Set(remoteNodeAnn1, remoteNodeAnn2)) + assert(sender.expectMsgType[Iterable[NodeAnnouncement]].toSet == Set(remoteNodeAnn1, remoteNodeAnn2)) } { eclair.nodes(Some(Set(randomKey().publicKey))).pipeTo(sender.ref) @@ -230,7 +230,7 @@ class EclairImplSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with I eclair.allUpdates(Some(b)).pipeTo(sender.ref) // ask updates filtered by 'b' router.expectMsg(Router.GetChannelsMap) router.reply(channels) - assert(sender.expectMsgType[Iterable[ChannelUpdate]].map(_.shortChannelId).toSet === Set(ShortChannelId(2))) + assert(sender.expectMsgType[Iterable[ChannelUpdate]].map(_.shortChannelId).toSet == Set(ShortChannelId(2))) } test("close and forceclose should work both with channelId and shortChannelId") { f => @@ -279,9 +279,9 @@ class EclairImplSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with I eclair.receive(Left("some desc"), Some(123 msat), Some(456), Some(fallBackAddressRaw), None) val receive = paymentHandler.expectMsgType[ReceivePayment] - assert(receive.amount_opt === Some(123 msat)) - assert(receive.expirySeconds_opt === Some(456)) - assert(receive.fallbackAddress_opt === Some(fallBackAddressRaw)) + assert(receive.amount_opt == Some(123 msat)) + assert(receive.expirySeconds_opt == Some(456)) + assert(receive.fallbackAddress_opt == Some(fallBackAddressRaw)) // try with wrong address format assertThrows[IllegalArgumentException](eclair.receive(Left("some desc"), Some(123 msat), Some(456), Some("wassa wassa"), None)) @@ -295,7 +295,7 @@ class EclairImplSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with I val paymentPreimage = randomBytes32() eclair.receive(Left("some desc"), None, None, None, Some(paymentPreimage)).pipeTo(sender.ref) - assert(sender.expectMsgType[Invoice].paymentHash === Crypto.sha256(paymentPreimage)) + assert(sender.expectMsgType[Invoice].paymentHash == Crypto.sha256(paymentPreimage)) } test("sendtoroute should pass the parameters correctly") { f => @@ -319,10 +319,10 @@ class EclairImplSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with I eclair.sendWithPreimage(None, nodeId, 12345 msat) val send = paymentInitiator.expectMsgType[SendSpontaneousPayment] - assert(send.externalId === None) - assert(send.recipientNodeId === nodeId) - assert(send.recipientAmount === 12345.msat) - assert(send.paymentHash === Crypto.sha256(send.paymentPreimage)) + assert(send.externalId == None) + assert(send.recipientNodeId == nodeId) + assert(send.recipientAmount == 12345.msat) + assert(send.paymentHash == Crypto.sha256(send.paymentPreimage)) assert(send.userCustomTlvs.isEmpty) } @@ -336,11 +336,11 @@ class EclairImplSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with I eclair.sendWithPreimage(None, nodeId, 12345 msat, paymentPreimage = expectedPaymentPreimage) val send = paymentInitiator.expectMsgType[SendSpontaneousPayment] - assert(send.externalId === None) - assert(send.recipientNodeId === nodeId) - assert(send.recipientAmount === 12345.msat) - assert(send.paymentPreimage === expectedPaymentPreimage) - assert(send.paymentHash === expectedPaymentHash) + assert(send.externalId == None) + assert(send.recipientNodeId == nodeId) + assert(send.recipientAmount == 12345.msat) + assert(send.paymentPreimage == expectedPaymentPreimage) + assert(send.paymentHash == expectedPaymentHash) assert(send.userCustomTlvs.isEmpty) } @@ -353,17 +353,17 @@ class EclairImplSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with I val bytesMsg = ByteVector.fromValidBase64(base64Msg) val signedMessage = eclair.signMessage(bytesMsg) - assert(signedMessage.nodeId === kit.nodeParams.nodeId) - assert(signedMessage.message === base64Msg) + assert(signedMessage.nodeId == kit.nodeParams.nodeId) + assert(signedMessage.message == base64Msg) val verifiedMessage = eclair.verifyMessage(bytesMsg, signedMessage.signature) assert(verifiedMessage.valid) - assert(verifiedMessage.publicKey === kit.nodeParams.nodeId) + assert(verifiedMessage.publicKey == kit.nodeParams.nodeId) val prefix = ByteVector("Lightning Signed Message:".getBytes) val dhash256 = Crypto.hash256(prefix ++ bytesMsg) val expectedDigest = ByteVector32(hex"cbedbc1542fb139e2e10954f1ff9f82e8a1031cc63260636bbc45a90114552ea") - assert(dhash256 === expectedDigest) + assert(dhash256 == expectedDigest) assert(Crypto.verifySignature(dhash256, ByteVector64(signedMessage.signature.tail), kit.nodeParams.nodeId)) } @@ -376,8 +376,8 @@ class EclairImplSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with I val bytesMsg = ByteVector.fromValidBase64(base64Msg) val signedMessage = eclair.signMessage(bytesMsg) - assert(signedMessage.nodeId === kit.nodeParams.nodeId) - assert(signedMessage.message === base64Msg) + assert(signedMessage.nodeId == kit.nodeParams.nodeId) + assert(signedMessage.message == base64Msg) val wrongMsg = ByteVector.fromValidBase64(base64Msg.tail) val verifiedMessage = eclair.verifyMessage(wrongMsg, signedMessage.signature) @@ -404,8 +404,8 @@ class EclairImplSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with I val bytesMsg = ByteVector.fromValidBase64(base64Msg) val signedMessage = eclair.signMessage(bytesMsg) - assert(signedMessage.nodeId === kit.nodeParams.nodeId) - assert(signedMessage.message === base64Msg) + assert(signedMessage.nodeId == kit.nodeParams.nodeId) + assert(signedMessage.message == base64Msg) val invalidSignature = (if (signedMessage.signature.head.toInt == 31) 32 else 31).toByte +: signedMessage.signature.tail val verifiedMessage = eclair.verifyMessage(bytesMsg, invalidSignature) @@ -437,7 +437,7 @@ class EclairImplSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with I register.reply(RES_GET_CHANNEL_INFO(map(c3.channelId), c3.channelId, NORMAL, ChannelCodecsSpec.normal)) register.expectNoMessage() - assert(sender.expectMsgType[Iterable[RES_GET_CHANNEL_INFO]].toSet === Set( + assert(sender.expectMsgType[Iterable[RES_GET_CHANNEL_INFO]].toSet == Set( RES_GET_CHANNEL_INFO(a, a1, NORMAL, ChannelCodecsSpec.normal), RES_GET_CHANNEL_INFO(b, b1, NORMAL, ChannelCodecsSpec.normal), )) @@ -466,7 +466,7 @@ class EclairImplSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with I register.reply(RES_GET_CHANNEL_INFO(channels2Nodes(c2.channelId), c2.channelId, NORMAL, ChannelCodecsSpec.normal)) register.expectNoMessage() - assert(sender.expectMsgType[Iterable[RES_GET_CHANNEL_INFO]].toSet === Set( + assert(sender.expectMsgType[Iterable[RES_GET_CHANNEL_INFO]].toSet == Set( RES_GET_CHANNEL_INFO(a, a1, NORMAL, ChannelCodecsSpec.normal), RES_GET_CHANNEL_INFO(a, a2, NORMAL, ChannelCodecsSpec.normal), )) @@ -519,11 +519,11 @@ class EclairImplSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with I paymentInitiator.expectMsg(GetPayment(Right(spontaneousPayment.paymentHash))) paymentInitiator.reply(PaymentIsPending(pendingPaymentId, spontaneousPayment.paymentHash, PendingSpontaneousPayment(ActorRef.noSender, spontaneousPayment))) val pendingPayment2 = sender.expectMsgType[Seq[OutgoingPayment]] - assert(pendingPayment2.length === 1) - assert(pendingPayment2.head.id === pendingPayment2.head.parentId) - assert(pendingPayment2.head.id === pendingPaymentId) - assert(pendingPayment2.head.paymentHash === spontaneousPayment.paymentHash) - assert(pendingPayment2.head.status === OutgoingPaymentStatus.Pending) + assert(pendingPayment2.length == 1) + assert(pendingPayment2.head.id == pendingPayment2.head.parentId) + assert(pendingPayment2.head.id == pendingPaymentId) + assert(pendingPayment2.head.paymentHash == spontaneousPayment.paymentHash) + assert(pendingPayment2.head.status == OutgoingPaymentStatus.Pending) // A third payment is fully settled in the DB and not being retried. val failedAt = TimestampMilli.now() @@ -540,11 +540,11 @@ class EclairImplSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with I paymentInitiator.expectMsg(GetPayment(Left(failedPayment.parentId))) paymentInitiator.reply(PaymentIsPending(failedPayment.parentId, spontaneousPayment.paymentHash, PendingSpontaneousPayment(ActorRef.noSender, spontaneousPayment))) val pendingPayment3 = sender.expectMsgType[Seq[OutgoingPayment]] - assert(pendingPayment3.length === 2) - assert(pendingPayment3.head.id === failedPayment.parentId) - assert(pendingPayment3.head.paymentHash === failedPayment.paymentHash) - assert(pendingPayment3.head.status === OutgoingPaymentStatus.Pending) - assert(pendingPayment3.last === failedPayment) + assert(pendingPayment3.length == 2) + assert(pendingPayment3.head.id == failedPayment.parentId) + assert(pendingPayment3.head.paymentHash == failedPayment.paymentHash) + assert(pendingPayment3.head.status == OutgoingPaymentStatus.Pending) + assert(pendingPayment3.last == failedPayment) } test("close channels") { f => @@ -563,7 +563,7 @@ class EclairImplSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with I register.reply(RES_SUCCESS(c2.message, c2.channelId)) register.expectNoMessage() - assert(sender.expectMsgType[Map[ChannelIdentifier, Either[Throwable, CommandResponse[CMD_CLOSE]]]] === Map( + assert(sender.expectMsgType[Map[ChannelIdentifier, Either[Throwable, CommandResponse[CMD_CLOSE]]]] == Map( Left(a) -> Right(RES_SUCCESS(CMD_CLOSE(ActorRef.noSender, None, None), a)), Left(b) -> Right(RES_SUCCESS(CMD_CLOSE(ActorRef.noSender, None, None), b)) )) @@ -600,7 +600,7 @@ class EclairImplSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with I register.reply(Register.ForwardFailure(u3)) register.expectNoMessage() - assert(sender.expectMsgType[Map[ChannelIdentifier, Either[Throwable, CommandResponse[CMD_UPDATE_RELAY_FEE]]]] === Map( + assert(sender.expectMsgType[Map[ChannelIdentifier, Either[Throwable, CommandResponse[CMD_UPDATE_RELAY_FEE]]]] == Map( Left(a1) -> Right(RES_SUCCESS(CMD_UPDATE_RELAY_FEE(ActorRef.noSender, 999 msat, 1234, None), a1)), Left(a2) -> Right(RES_FAILURE(CMD_UPDATE_RELAY_FEE(ActorRef.noSender, 999 msat, 1234, None), CommandUnavailableInThisState(a2, "CMD_UPDATE_RELAY_FEE", channel.CLOSING))), Left(b1) -> Left(ChannelNotFound(Left(b1))) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/FeaturesSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/FeaturesSpec.scala index 85c4fc0c80..1bf74af7a4 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/FeaturesSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/FeaturesSpec.scala @@ -99,8 +99,8 @@ class FeaturesSpec extends AnyFunSuite { for ((testCase, valid) <- testCases) { if (valid) { - assert(validateFeatureGraph(Features(testCase)) === None) - assert(validateFeatureGraph(Features(testCase.bytes)) === None) + assert(validateFeatureGraph(Features(testCase)) == None) + assert(validateFeatureGraph(Features(testCase.bytes)) == None) } else { assert(validateFeatureGraph(Features(testCase)).nonEmpty) assert(validateFeatureGraph(Features(testCase.bytes)).nonEmpty) @@ -205,9 +205,9 @@ class FeaturesSpec extends AnyFunSuite { ) for (testCase <- testCases) { - assert(areCompatible(testCase.ours, testCase.theirs) === testCase.compatible, testCase) - assert(testCase.ours.areSupported(testCase.theirs) === testCase.oursSupportTheirs, testCase) - assert(testCase.theirs.areSupported(testCase.ours) === testCase.theirsSupportOurs, testCase) + assert(areCompatible(testCase.ours, testCase.theirs) == testCase.compatible, testCase) + assert(testCase.ours.areSupported(testCase.theirs) == testCase.oursSupportTheirs, testCase) + assert(testCase.theirs.areSupported(testCase.ours) == testCase.theirsSupportOurs, testCase) } } @@ -216,15 +216,15 @@ class FeaturesSpec extends AnyFunSuite { Map(DataLossProtect -> Optional, InitialRoutingSync -> Optional, VariableLengthOnion -> Mandatory, PaymentMetadata -> Optional), Set(UnknownFeature(753), UnknownFeature(852), UnknownFeature(65303)) ) - assert(features.initFeatures() === Features( + assert(features.initFeatures() == Features( Map(DataLossProtect -> Optional, InitialRoutingSync -> Optional, VariableLengthOnion -> Mandatory), Set(UnknownFeature(753), UnknownFeature(852), UnknownFeature(65303)) )) - assert(features.nodeAnnouncementFeatures() === Features( + assert(features.nodeAnnouncementFeatures() == Features( Map(DataLossProtect -> Optional, VariableLengthOnion -> Mandatory), Set(UnknownFeature(753), UnknownFeature(852), UnknownFeature(65303)) )) - assert(features.invoiceFeatures() === Features( + assert(features.invoiceFeatures() == Features( Map(VariableLengthOnion -> Mandatory, PaymentMetadata -> Optional), Set(UnknownFeature(753), UnknownFeature(852), UnknownFeature(65303)) )) @@ -240,11 +240,11 @@ class FeaturesSpec extends AnyFunSuite { ) for ((bin, features) <- testCases) { - assert(features.toByteVector === bin) - assert(Features(bin) === features) + assert(features.toByteVector == bin) + assert(Features(bin) == features) val notMinimallyEncoded = Features(hex"00" ++ bin) - assert(notMinimallyEncoded === features) - assert(notMinimallyEncoded.toByteVector === bin) // features are minimally-encoded when converting to bytes + assert(notMinimallyEncoded == features) + assert(notMinimallyEncoded.toByteVector == bin) // features are minimally-encoded when converting to bytes } } @@ -262,9 +262,9 @@ class FeaturesSpec extends AnyFunSuite { """) val features = fromConfiguration(conf) - assert(features.toByteVector === hex"028a8a") - assert(Features(hex"028a8a") === features) - assert(validateFeatureGraph(features) === None) + assert(features.toByteVector == hex"028a8a") + assert(Features(hex"028a8a") == features) + assert(validateFeatureGraph(features) == None) assert(features.hasFeature(DataLossProtect, Some(Optional))) assert(features.hasFeature(InitialRoutingSync, Some(Optional))) assert(features.hasFeature(ChannelRangeQueries, Some(Optional))) @@ -285,9 +285,9 @@ class FeaturesSpec extends AnyFunSuite { """) val features = fromConfiguration(conf) - assert(features.toByteVector === hex"068a") - assert(Features(hex"068a") === features) - assert(validateFeatureGraph(features) === None) + assert(features.toByteVector == hex"068a") + assert(Features(hex"068a") == features) + assert(validateFeatureGraph(features) == None) assert(features.hasFeature(DataLossProtect, Some(Optional))) assert(features.hasFeature(InitialRoutingSync, Some(Optional))) assert(!features.hasFeature(InitialRoutingSync, Some(Mandatory))) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/MilliSatoshiSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/MilliSatoshiSpec.scala index 09c7f76e1c..6b2133becc 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/MilliSatoshiSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/MilliSatoshiSpec.scala @@ -27,26 +27,26 @@ class MilliSatoshiSpec extends AnyFunSuite { test("millisatoshi numeric operations") { // add - assert(MilliSatoshi(561) + 0.msat === MilliSatoshi(561)) - assert(MilliSatoshi(561) + 0.sat === MilliSatoshi(561)) - assert(MilliSatoshi(561) + 1105.msat === MilliSatoshi(1666)) - assert(MilliSatoshi(2000) + 3.sat === MilliSatoshi(5000)) + assert(MilliSatoshi(561) + 0.msat == MilliSatoshi(561)) + assert(MilliSatoshi(561) + 0.sat == MilliSatoshi(561)) + assert(MilliSatoshi(561) + 1105.msat == MilliSatoshi(1666)) + assert(MilliSatoshi(2000) + 3.sat == MilliSatoshi(5000)) // subtract - assert(MilliSatoshi(561) - 0.msat === MilliSatoshi(561)) - assert(MilliSatoshi(1105) - 561.msat === MilliSatoshi(544)) - assert(561.msat - 1105.msat === -MilliSatoshi(544)) - assert(MilliSatoshi(561) - 1105.msat === -MilliSatoshi(544)) - assert(MilliSatoshi(1105) - 1.sat === MilliSatoshi(105)) + assert(MilliSatoshi(561) - 0.msat == MilliSatoshi(561)) + assert(MilliSatoshi(1105) - 561.msat == MilliSatoshi(544)) + assert(561.msat - 1105.msat == -MilliSatoshi(544)) + assert(MilliSatoshi(561) - 1105.msat == -MilliSatoshi(544)) + assert(MilliSatoshi(1105) - 1.sat == MilliSatoshi(105)) // multiply - assert(MilliSatoshi(561) * 1 === 561.msat) - assert(MilliSatoshi(561) * 2 === 1122.msat) - assert(MilliSatoshi(561) * 2.5 === 1402.msat) + assert(MilliSatoshi(561) * 1 == 561.msat) + assert(MilliSatoshi(561) * 2 == 1122.msat) + assert(MilliSatoshi(561) * 2.5 == 1402.msat) // divide - assert(MilliSatoshi(561) / 1 === MilliSatoshi(561)) - assert(MilliSatoshi(561) / 2 === MilliSatoshi(280)) + assert(MilliSatoshi(561) / 1 == MilliSatoshi(561)) + assert(MilliSatoshi(561) / 2 == MilliSatoshi(280)) // compare assert(MilliSatoshi(561) <= MilliSatoshi(561)) @@ -63,16 +63,16 @@ class MilliSatoshiSpec extends AnyFunSuite { assert(MilliSatoshi(2000) > Satoshi(1)) // maxOf - assert((561 msat).max(1105 msat) === MilliSatoshi(1105)) - assert((1105 msat).max(1 sat) === MilliSatoshi(1105)) - assert((1105 msat).max(2 sat) === MilliSatoshi(2000)) - assert((1 sat).max(2 sat) === Satoshi(2)) + assert((561 msat).max(1105 msat) == MilliSatoshi(1105)) + assert((1105 msat).max(1 sat) == MilliSatoshi(1105)) + assert((1105 msat).max(2 sat) == MilliSatoshi(2000)) + assert((1 sat).max(2 sat) == Satoshi(2)) // minOf - assert((561 msat).min(1105 msat) === MilliSatoshi(561)) - assert((1105 msat).min(1 sat) === MilliSatoshi(1000)) - assert((1105 msat).min(2 sat) === MilliSatoshi(1105)) - assert((1 sat).min(2 sat) === Satoshi(1)) + assert((561 msat).min(1105 msat) == MilliSatoshi(561)) + assert((1105 msat).min(1 sat) == MilliSatoshi(1000)) + assert((1105 msat).min(2 sat) == MilliSatoshi(1105)) + assert((1 sat).min(2 sat) == Satoshi(1)) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/PackageSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/PackageSpec.scala index ef19218dbb..9427149101 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/PackageSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/PackageSpec.scala @@ -41,7 +41,7 @@ class PackageSpec extends AnyFunSuite { (hex"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00F0", 0x0F00, hex"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0FF0") :: Nil) .map(x => (ByteVector32(x._1), x._2, ByteVector32(x._3))) - data.foreach(x => assert(toLongId(ByteVector32(x._1), x._2) === x._3)) + data.foreach(x => assert(toLongId(ByteVector32(x._1), x._2) == x._3)) } test("decode base58 addresses") { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/StartupSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/StartupSpec.scala index fb7a86121f..e4efaa0cec 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/StartupSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/StartupSpec.scala @@ -54,14 +54,14 @@ class StartupSpec extends AnyFunSuite { val threeBytesUTFChar = '\u20AC' // € val baseUkraineAlias = "BitcoinLightningNodeUkraine" - assert(baseUkraineAlias.length === 27) - assert(baseUkraineAlias.getBytes.length === 27) + assert(baseUkraineAlias.length == 27) + assert(baseUkraineAlias.getBytes.length == 27) // we add 2 UTF-8 chars, each is 3-bytes long -> total new length 33 bytes! val goUkraineGo = s"${threeBytesUTFChar}BitcoinLightningNodeUkraine$threeBytesUTFChar" - assert(goUkraineGo.length === 29) - assert(goUkraineGo.getBytes.length === 33) // too long for the alias, should be truncated + assert(goUkraineGo.length == 29) + assert(goUkraineGo.getBytes.length == 33) // too long for the alias, should be truncated val illegalAliasConf = ConfigFactory.parseString(s"node-alias = $goUkraineGo") val conf = illegalAliasConf.withFallback(defaultConf) @@ -182,7 +182,7 @@ class StartupSpec extends AnyFunSuite { val nodeParams = makeNodeParamsWithDefaults(perNodeConf.withFallback(defaultConf)) val perNodeFeatures = nodeParams.initFeaturesFor(PublicKey(ByteVector.fromValidHex("031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f"))) - assert(perNodeFeatures === Features(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory, BasicMultiPartPayment -> Mandatory, ChannelType -> Optional)) + assert(perNodeFeatures == Features(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory, BasicMultiPartPayment -> Mandatory, ChannelType -> Optional)) } test("reject non-init features in node override") { @@ -247,9 +247,9 @@ class StartupSpec extends AnyFunSuite { ) val nodeParams = makeNodeParamsWithDefaults(perNodeConf.withFallback(defaultConf)) - assert(nodeParams.onChainFeeConf.feerateToleranceFor(PublicKey(hex"031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f")) === FeerateTolerance(0.1, 15.0, FeeratePerKw(FeeratePerByte(15 sat)), DustTolerance(25_000 sat, closeOnUpdateFeeOverflow = true))) - assert(nodeParams.onChainFeeConf.feerateToleranceFor(PublicKey(hex"03462779ad4aad39514614751a71085f2f10e1c7a593e4e030efb5b8721ce55b0b")) === FeerateTolerance(0.75, 5.0, FeeratePerKw(FeeratePerByte(5 sat)), DustTolerance(40_000 sat, closeOnUpdateFeeOverflow = false))) - assert(nodeParams.onChainFeeConf.feerateToleranceFor(PublicKey(hex"0362c0a046dacce86ddd0343c6d3c7c79c2208ba0d9c9cf24a6d046d21d21f90f7")) === FeerateTolerance(0.5, 10.0, FeeratePerKw(FeeratePerByte(10 sat)), DustTolerance(50_000 sat, closeOnUpdateFeeOverflow = false))) + assert(nodeParams.onChainFeeConf.feerateToleranceFor(PublicKey(hex"031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f")) == FeerateTolerance(0.1, 15.0, FeeratePerKw(FeeratePerByte(15 sat)), DustTolerance(25_000 sat, closeOnUpdateFeeOverflow = true))) + assert(nodeParams.onChainFeeConf.feerateToleranceFor(PublicKey(hex"03462779ad4aad39514614751a71085f2f10e1c7a593e4e030efb5b8721ce55b0b")) == FeerateTolerance(0.75, 5.0, FeeratePerKw(FeeratePerByte(5 sat)), DustTolerance(40_000 sat, closeOnUpdateFeeOverflow = false))) + assert(nodeParams.onChainFeeConf.feerateToleranceFor(PublicKey(hex"0362c0a046dacce86ddd0343c6d3c7c79c2208ba0d9c9cf24a6d046d21d21f90f7")) == FeerateTolerance(0.5, 10.0, FeeratePerKw(FeeratePerByte(10 sat)), DustTolerance(50_000 sat, closeOnUpdateFeeOverflow = false))) } test("NodeParams should fail if htlc-minimum-msat is set to 0") { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala b/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala index 9c254fa4ce..2bc65f917a 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala @@ -32,7 +32,7 @@ import fr.acinq.eclair.router.PathFindingExperimentConf import fr.acinq.eclair.router.Router.{MultiPartParams, PathFindingConf, RouterConf, SearchBoundaries} import fr.acinq.eclair.wire.protocol.{Color, EncodingType, NodeAddress, OnionRoutingPacket} import org.scalatest.Tag -import scodec.bits.ByteVector +import scodec.bits.{ByteVector, HexStringSyntax} import java.util.UUID import java.util.concurrent.atomic.AtomicLong @@ -71,7 +71,7 @@ object TestConstants { ) object Alice { - val seed: ByteVector32 = ByteVector32(ByteVector.fill(32)(1)) + val seed: ByteVector32 = ByteVector32(hex"b4acd47335b25ab7b84b8c020997b12018592bb4631b868762154d77fa8b93a3") // 02aaaa... val nodeKeyManager = new LocalNodeKeyManager(seed, Block.RegtestGenesisBlock.hash) val channelKeyManager = new LocalChannelKeyManager(seed, Block.RegtestGenesisBlock.hash) @@ -214,7 +214,7 @@ object TestConstants { } object Bob { - val seed: ByteVector32 = ByteVector32(ByteVector.fill(32)(2)) + val seed: ByteVector32 = ByteVector32(hex"7620226fec887b0b2ebe76492e5a3fd3eb0e47cd3773263f6a81b59a704dc492") // 02bbbb... val nodeKeyManager = new LocalNodeKeyManager(seed, Block.RegtestGenesisBlock.hash) val channelKeyManager = new LocalChannelKeyManager(seed, Block.RegtestGenesisBlock.hash) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/TestDatabases.scala b/eclair-core/src/test/scala/fr/acinq/eclair/TestDatabases.scala index 7b813fee70..2930d4c07a 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/TestDatabases.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/TestDatabases.scala @@ -60,8 +60,9 @@ object TestDatabases { def freeze1(input: Origin): Origin = input match { case h: Origin.LocalHot => Origin.LocalCold(h.id) + case h: Origin.ChannelRelayedHot => Origin.ChannelRelayedCold(h.originChannelId, h.originHtlcId, h.amountIn, h.amountOut) case h: Origin.TrampolineRelayedHot => Origin.TrampolineRelayedCold(h.htlcs) - case _ => input + case c: Origin.Cold => c } def freeze2(input: Commitments): Commitments = input.copy(originChannels = input.originChannels.view.mapValues(o => freeze1(o)).toMap) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/UInt64Spec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/UInt64Spec.scala index 54535ed515..d07caab268 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/UInt64Spec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/UInt64Spec.scala @@ -47,21 +47,21 @@ class UInt64Spec extends AnyFunSuite { assert(l1.toString == "9223372036854775808") assert(l1.toBigInt == BigInt("9223372036854775808")) - assert(a.toByteVector === hex"ffffffffffffffff") - assert(a.toString === "18446744073709551615") // 2^64 - 1 - assert(a.toBigInt === BigInt("18446744073709551615")) + assert(a.toByteVector == hex"ffffffffffffffff") + assert(a.toString == "18446744073709551615") // 2^64 - 1 + assert(a.toBigInt == BigInt("18446744073709551615")) - assert(b.toByteVector === hex"fffffffffffffffe") - assert(b.toString === "18446744073709551614") - assert(b.toBigInt === BigInt("18446744073709551614")) + assert(b.toByteVector == hex"fffffffffffffffe") + assert(b.toString == "18446744073709551614") + assert(b.toBigInt == BigInt("18446744073709551614")) - assert(c.toByteVector === hex"00000000000002a") - assert(c.toString === "42") - assert(c.toBigInt === BigInt("42")) + assert(c.toByteVector == hex"00000000000002a") + assert(c.toString == "42") + assert(c.toBigInt == BigInt("42")) - assert(z.toByteVector === hex"000000000000000") - assert(z.toString === "0") - assert(z.toBigInt === BigInt("0")) + assert(z.toByteVector == hex"000000000000000") + assert(z.toString == "0") + assert(z.toBigInt == BigInt("0")) assert(UInt64(hex"ff").toByteVector == hex"0000000000000ff") assert(UInt64(hex"800").toByteVector == hex"000000000000800") diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/balance/CheckBalanceSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/balance/CheckBalanceSpec.scala index a7c8305da9..a16d0e9e38 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/balance/CheckBalanceSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/balance/CheckBalanceSpec.scala @@ -47,7 +47,7 @@ class CheckBalanceSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with addHtlc(10_000_000 msat, alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) - assert(CheckBalance.computeOffChainBalance(Seq(alice.stateData.asInstanceOf[DATA_NORMAL]), knownPreimages = Set.empty).normal === + assert(CheckBalance.computeOffChainBalance(Seq(alice.stateData.asInstanceOf[DATA_NORMAL]), knownPreimages = Set.empty).normal == MainAndHtlcBalance( toLocal = (TestConstants.fundingSatoshis - TestConstants.pushMsat - 30_000_000.msat).truncateToSatoshi, htlcs = 30_000.sat @@ -64,7 +64,7 @@ class CheckBalanceSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with bob ! CMD_SIGN() bob2alice.expectMsgType[CommitSig] - assert(CheckBalance.computeOffChainBalance(Seq(bob.stateData.asInstanceOf[DATA_NORMAL]), knownPreimages = Set.empty).normal === + assert(CheckBalance.computeOffChainBalance(Seq(bob.stateData.asInstanceOf[DATA_NORMAL]), knownPreimages = Set.empty).normal == MainAndHtlcBalance( toLocal = TestConstants.pushMsat.truncateToSatoshi, htlcs = htlc.amountMsat.truncateToSatoshi @@ -97,7 +97,7 @@ class CheckBalanceSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val commitments = alice.stateData.asInstanceOf[DATA_CLOSING].commitments val remoteCommitPublished = alice.stateData.asInstanceOf[DATA_CLOSING].remoteCommitPublished.get val knownPreimages = Set((commitments.channelId, htlcb1.id)) - assert(CheckBalance.computeRemoteCloseBalance(commitments, CurrentRemoteClose(commitments.remoteCommit, remoteCommitPublished), knownPreimages) === + assert(CheckBalance.computeRemoteCloseBalance(commitments, CurrentRemoteClose(commitments.remoteCommit, remoteCommitPublished), knownPreimages) == PossiblyPublishedMainAndHtlcBalance( toLocal = Map(remoteCommitPublished.claimMainOutputTx.get.tx.txid -> remoteCommitPublished.claimMainOutputTx.get.tx.txOut.head.amount), htlcs = claimHtlcTxs.map(claimTx => claimTx.txid -> claimTx.txOut.head.amount.toBtc).toMap, @@ -105,7 +105,7 @@ class CheckBalanceSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with )) // assuming alice gets the preimage for the 2nd htlc val knownPreimages1 = Set((commitments.channelId, htlcb1.id), (commitments.channelId, htlcb2.id)) - assert(CheckBalance.computeRemoteCloseBalance(commitments, CurrentRemoteClose(commitments.remoteCommit, remoteCommitPublished), knownPreimages1) === + assert(CheckBalance.computeRemoteCloseBalance(commitments, CurrentRemoteClose(commitments.remoteCommit, remoteCommitPublished), knownPreimages1) == PossiblyPublishedMainAndHtlcBalance( toLocal = Map(remoteCommitPublished.claimMainOutputTx.get.tx.txid -> remoteCommitPublished.claimMainOutputTx.get.tx.txOut.head.amount), htlcs = claimHtlcTxs.map(claimTx => claimTx.txid -> claimTx.txOut.head.amount.toBtc).toMap, @@ -146,7 +146,7 @@ class CheckBalanceSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val remoteCommitPublished = alice.stateData.asInstanceOf[DATA_CLOSING].nextRemoteCommitPublished.get val knownPreimages = Set((commitments.channelId, htlcb1.id)) val Left(waitingForRevocation) = commitments.remoteNextCommitInfo - assert(CheckBalance.computeRemoteCloseBalance(commitments, CurrentRemoteClose(waitingForRevocation.nextRemoteCommit, remoteCommitPublished), knownPreimages) === + assert(CheckBalance.computeRemoteCloseBalance(commitments, CurrentRemoteClose(waitingForRevocation.nextRemoteCommit, remoteCommitPublished), knownPreimages) == PossiblyPublishedMainAndHtlcBalance( toLocal = Map(remoteCommitPublished.claimMainOutputTx.get.tx.txid -> remoteCommitPublished.claimMainOutputTx.get.tx.txOut.head.amount), htlcs = claimHtlcTxs.map(claimTx => claimTx.txid -> claimTx.txOut.head.amount.toBtc).toMap, @@ -154,7 +154,7 @@ class CheckBalanceSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with )) // assuming alice gets the preimage for the 2nd htlc val knownPreimages1 = Set((commitments.channelId, htlcb1.id), (commitments.channelId, htlcb2.id)) - assert(CheckBalance.computeRemoteCloseBalance(commitments, CurrentRemoteClose(waitingForRevocation.nextRemoteCommit, remoteCommitPublished), knownPreimages1) === + assert(CheckBalance.computeRemoteCloseBalance(commitments, CurrentRemoteClose(waitingForRevocation.nextRemoteCommit, remoteCommitPublished), knownPreimages1) == PossiblyPublishedMainAndHtlcBalance( toLocal = Map(remoteCommitPublished.claimMainOutputTx.get.tx.txid -> remoteCommitPublished.claimMainOutputTx.get.tx.txOut.head.amount), htlcs = claimHtlcTxs.map(claimTx => claimTx.txid -> claimTx.txOut.head.amount.toBtc).toMap, @@ -179,14 +179,14 @@ class CheckBalanceSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // alice publishes her commit tx val aliceCommitTx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx alice ! Error(ByteVector32.Zeroes, "oops") - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid === aliceCommitTx.txid) + assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid == aliceCommitTx.txid) assert(aliceCommitTx.txOut.size == 6) // two main outputs and 4 pending htlcs awaitCond(alice.stateName == CLOSING) assert(alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.isDefined) val commitments = alice.stateData.asInstanceOf[DATA_CLOSING].commitments val localCommitPublished = alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.get val knownPreimages = Set((commitments.channelId, htlcb1.id)) - assert(CheckBalance.computeLocalCloseBalance(commitments, LocalClose(commitments.localCommit, localCommitPublished), knownPreimages) === + assert(CheckBalance.computeLocalCloseBalance(commitments, LocalClose(commitments.localCommit, localCommitPublished), knownPreimages) == PossiblyPublishedMainAndHtlcBalance( toLocal = Map(localCommitPublished.claimMainDelayedOutputTx.get.tx.txid -> localCommitPublished.claimMainDelayedOutputTx.get.tx.txOut.head.amount), htlcs = Map.empty, @@ -207,15 +207,15 @@ class CheckBalanceSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // 3rd-stage txs are published when htlc txs confirm val claimHtlcDelayedTxs = Seq(htlcTx1, htlcTx2, htlcTx3).map { htlcTimeoutTx => alice ! WatchOutputSpentTriggered(htlcTimeoutTx) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === htlcTimeoutTx.txid) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == htlcTimeoutTx.txid) alice ! WatchTxConfirmedTriggered(BlockHeight(2701), 3, htlcTimeoutTx) val claimHtlcDelayedTx = alice2blockchain.expectMsgType[PublishFinalTx].tx - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === claimHtlcDelayedTx.txid) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == claimHtlcDelayedTx.txid) claimHtlcDelayedTx } awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.get.claimHtlcDelayedTxs.length == 3) - assert(CheckBalance.computeLocalCloseBalance(commitments, LocalClose(commitments.localCommit, alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.get), knownPreimages) === + assert(CheckBalance.computeLocalCloseBalance(commitments, LocalClose(commitments.localCommit, alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.get), knownPreimages) == PossiblyPublishedMainAndHtlcBalance( toLocal = Map(localCommitPublished.claimMainDelayedOutputTx.get.tx.txid -> localCommitPublished.claimMainDelayedOutputTx.get.tx.txOut.head.amount), htlcs = claimHtlcDelayedTxs.map(claimTx => claimTx.txid -> claimTx.txOut.head.amount.toBtc).toMap, diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/DummyOnChainWallet.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/DummyOnChainWallet.scala index 9cbebadc3a..5476611fd6 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/DummyOnChainWallet.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/DummyOnChainWallet.scala @@ -16,9 +16,9 @@ package fr.acinq.eclair.blockchain +import fr.acinq.bitcoin.TxIn.SEQUENCE_FINAL import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.bitcoin.scalacompat.{ByteVector32, Crypto, OutPoint, Satoshi, SatoshiLong, Transaction, TxIn, TxOut} -import fr.acinq.bitcoin.TxIn.SEQUENCE_FINAL import fr.acinq.eclair.blockchain.OnChainWallet.{MakeFundingTxResponse, OnChainBalance} import fr.acinq.eclair.blockchain.fee.FeeratePerKw import scodec.bits._ @@ -32,6 +32,7 @@ class DummyOnChainWallet extends OnChainWallet { import DummyOnChainWallet._ + val funded = collection.concurrent.TrieMap.empty[ByteVector32, Transaction] var rolledback = Set.empty[Transaction] override def onChainBalance()(implicit ec: ExecutionContext): Future[OnChainBalance] = Future.successful(OnChainBalance(1105 sat, 561 sat)) @@ -40,8 +41,11 @@ class DummyOnChainWallet extends OnChainWallet { override def getReceivePubkey(receiveAddress: Option[String] = None)(implicit ec: ExecutionContext): Future[Crypto.PublicKey] = Future.successful(dummyReceivePubkey) - override def makeFundingTx(pubkeyScript: ByteVector, amount: Satoshi, feeRatePerKw: FeeratePerKw)(implicit ec: ExecutionContext): Future[MakeFundingTxResponse] = - Future.successful(DummyOnChainWallet.makeDummyFundingTx(pubkeyScript, amount)) + override def makeFundingTx(pubkeyScript: ByteVector, amount: Satoshi, feeRatePerKw: FeeratePerKw)(implicit ec: ExecutionContext): Future[MakeFundingTxResponse] = { + val tx = DummyOnChainWallet.makeDummyFundingTx(pubkeyScript, amount) + funded += (tx.fundingTx.txid -> tx.fundingTx) + Future.successful(tx) + } override def commit(tx: Transaction)(implicit ec: ExecutionContext): Future[Boolean] = Future.successful(true) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreClientSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreClientSpec.scala index fa53885f6e..c37464d932 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreClientSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreClientSpec.scala @@ -85,12 +85,12 @@ class BitcoinCoreClientSpec extends TestKitBaseClass with BitcoindService with A assert(fundTxResponse.amountIn > 0.sat) assert(fundTxResponse.fee > 0.sat) fundTxResponse.tx.txIn.foreach(txIn => assert(txIn.signatureScript.isEmpty && txIn.witness.isNull)) - fundTxResponse.tx.txIn.foreach(txIn => assert(txIn.sequence === bitcoin.TxIn.SEQUENCE_FINAL - 2)) + fundTxResponse.tx.txIn.foreach(txIn => assert(txIn.sequence == bitcoin.TxIn.SEQUENCE_FINAL - 2)) bitcoinClient.signTransaction(fundTxResponse.tx, Nil).pipeTo(sender.ref) val signTxResponse = sender.expectMsgType[SignTransactionResponse] assert(signTxResponse.complete) - assert(signTxResponse.tx.txOut.size === 2) + assert(signTxResponse.tx.txOut.size == 2) bitcoinClient.publishTransaction(signTxResponse.tx).pipeTo(sender.ref) sender.expectMsg(signTxResponse.tx.txid) @@ -123,11 +123,11 @@ class BitcoinCoreClientSpec extends TestKitBaseClass with BitcoindService with A val txManyOutputs = Transaction(2, Nil, TxOut(410000 sat, Script.pay2wpkh(randomKey().publicKey)) :: TxOut(230000 sat, Script.pay2wpkh(randomKey().publicKey)) :: Nil, 0) bitcoinClient.fundTransaction(txManyOutputs, FundTransactionOptions(TestConstants.feeratePerKw, replaceable = false, changePosition = Some(1))).pipeTo(sender.ref) val fundTxResponse = sender.expectMsgType[FundTransactionResponse] - assert(fundTxResponse.tx.txOut.size === 3) - assert(fundTxResponse.changePosition === Some(1)) + assert(fundTxResponse.tx.txOut.size == 3) + assert(fundTxResponse.changePosition == Some(1)) assert(!Set(230000 sat, 410000 sat).contains(fundTxResponse.tx.txOut(1).amount)) - assert(Set(230000 sat, 410000 sat) === Set(fundTxResponse.tx.txOut.head.amount, fundTxResponse.tx.txOut.last.amount)) - fundTxResponse.tx.txIn.foreach(txIn => assert(txIn.sequence === bitcoin.TxIn.SEQUENCE_FINAL - 1)) + assert(Set(230000 sat, 410000 sat) == Set(fundTxResponse.tx.txOut.head.amount, fundTxResponse.tx.txOut.last.amount)) + fundTxResponse.tx.txIn.foreach(txIn => assert(txIn.sequence == bitcoin.TxIn.SEQUENCE_FINAL - 1)) } } @@ -149,11 +149,11 @@ class BitcoinCoreClientSpec extends TestKitBaseClass with BitcoindService with A val sender = TestProbe() val bitcoinClient = new BitcoinCoreClient(rpcClient) bitcoinClient.onChainBalance().pipeTo(sender.ref) - assert(sender.expectMsgType[OnChainBalance] === OnChainBalance(Satoshi(satoshi), Satoshi(satoshi))) + assert(sender.expectMsgType[OnChainBalance] == OnChainBalance(Satoshi(satoshi), Satoshi(satoshi))) bitcoinClient.fundTransaction(txIn, FundTransactionOptions(FeeratePerKw(250 sat))).pipeTo(sender.ref) val fundTxResponse = sender.expectMsgType[FundTransactionResponse] - assert(fundTxResponse.fee === Satoshi(satoshi)) + assert(fundTxResponse.fee == Satoshi(satoshi)) } } @@ -182,7 +182,7 @@ class BitcoinCoreClientSpec extends TestKitBaseClass with BitcoindService with A sender.expectMsgType[MakeFundingTxResponse].fundingTx } - assert(getLocks(sender).size === 4) + assert(getLocks(sender).size == 4) bitcoinClient.commit(fundingTxs(0)).pipeTo(sender.ref) assert(sender.expectMsgType[Boolean]) @@ -294,9 +294,9 @@ class BitcoinCoreClientSpec extends TestKitBaseClass with BitcoindService with A // tx2 replaced tx1 in the mempool bitcoinClient.getMempool().pipeTo(sender.ref) val mempoolTxs = sender.expectMsgType[Seq[Transaction]] - assert(mempoolTxs.length === 1) - assert(mempoolTxs.head.txid === tx2.txid) - assert(tx2.txIn.map(_.outPoint).intersect(tx1.txIn.map(_.outPoint)).length === 1) + assert(mempoolTxs.length == 1) + assert(mempoolTxs.head.txid == tx2.txid) + assert(tx2.txIn.map(_.outPoint).intersect(tx1.txIn.map(_.outPoint)).length == 1) } test("unlock transaction inputs if publishing fails") { @@ -540,7 +540,7 @@ class BitcoinCoreClientSpec extends TestKitBaseClass with BitcoindService with A bitcoinClient.onChainBalance().pipeTo(sender.ref) val initialBalance = sender.expectMsgType[OnChainBalance] - assert(initialBalance.unconfirmed === 0.sat) + assert(initialBalance.unconfirmed == 0.sat) assert(initialBalance.confirmed > 50.btc.toSatoshi) val address = "n2YKngjUp139nkjKvZGnfLRN6HzzYxJsje" @@ -550,10 +550,10 @@ class BitcoinCoreClientSpec extends TestKitBaseClass with BitcoindService with A bitcoinClient.listTransactions(25, 0).pipeTo(sender.ref) val Some(tx1) = sender.expectMsgType[List[WalletTx]].collectFirst { case tx if tx.txid == txid => tx } - assert(tx1.address === address) - assert(tx1.amount === -amount) + assert(tx1.address == address) + assert(tx1.amount == -amount) assert(tx1.fees < 0.sat) - assert(tx1.confirmations === 0) + assert(tx1.confirmations == 0) bitcoinClient.onChainBalance().pipeTo(sender.ref) // NB: we use + because these amounts are already negative @@ -562,10 +562,10 @@ class BitcoinCoreClientSpec extends TestKitBaseClass with BitcoindService with A generateBlocks(1) bitcoinClient.listTransactions(25, 0).pipeTo(sender.ref) val Some(tx2) = sender.expectMsgType[List[WalletTx]].collectFirst { case tx if tx.txid == txid => tx } - assert(tx2.address === address) - assert(tx2.amount === -amount) + assert(tx2.address == address) + assert(tx2.amount == -amount) assert(tx2.fees < 0.sat) - assert(tx2.confirmations === 1) + assert(tx2.confirmations == 1) } test("get mempool transaction") { @@ -591,26 +591,26 @@ class BitcoinCoreClientSpec extends TestKitBaseClass with BitcoindService with A bitcoinClient.getMempoolTx(tx1.txid).pipeTo(sender.ref) val mempoolTx1 = sender.expectMsgType[MempoolTx] - assert(mempoolTx1.ancestorCount === 0) - assert(mempoolTx1.descendantCount === 2) - assert(mempoolTx1.fees === mempoolTx1.ancestorFees) - assert(mempoolTx1.descendantFees === mempoolTx1.fees + 12500.sat) + assert(mempoolTx1.ancestorCount == 0) + assert(mempoolTx1.descendantCount == 2) + assert(mempoolTx1.fees == mempoolTx1.ancestorFees) + assert(mempoolTx1.descendantFees == mempoolTx1.fees + 12500.sat) bitcoinClient.getMempoolTx(tx2.txid).pipeTo(sender.ref) val mempoolTx2 = sender.expectMsgType[MempoolTx] - assert(mempoolTx2.ancestorCount === 1) - assert(mempoolTx2.descendantCount === 1) - assert(mempoolTx2.fees === 5000.sat) - assert(mempoolTx2.descendantFees === 12500.sat) - assert(mempoolTx2.ancestorFees === mempoolTx1.fees + 5000.sat) + assert(mempoolTx2.ancestorCount == 1) + assert(mempoolTx2.descendantCount == 1) + assert(mempoolTx2.fees == 5000.sat) + assert(mempoolTx2.descendantFees == 12500.sat) + assert(mempoolTx2.ancestorFees == mempoolTx1.fees + 5000.sat) bitcoinClient.getMempoolTx(tx3.txid).pipeTo(sender.ref) val mempoolTx3 = sender.expectMsgType[MempoolTx] - assert(mempoolTx3.ancestorCount === 2) - assert(mempoolTx3.descendantCount === 0) - assert(mempoolTx3.fees === 7500.sat) - assert(mempoolTx3.descendantFees === mempoolTx3.fees) - assert(mempoolTx3.ancestorFees === mempoolTx1.fees + 12500.sat) + assert(mempoolTx3.ancestorCount == 2) + assert(mempoolTx3.descendantCount == 0) + assert(mempoolTx3.fees == 7500.sat) + assert(mempoolTx3.descendantFees == mempoolTx3.fees) + assert(mempoolTx3.ancestorFees == mempoolTx1.fees + 12500.sat) } test("abandon transaction") { @@ -781,7 +781,7 @@ class BitcoinCoreClientSpec extends TestKitBaseClass with BitcoindService with A generateBlocks(1) bitcoinClient.getBlockHeight().pipeTo(sender.ref) val blockHeight1 = sender.expectMsgType[BlockHeight] - assert(blockHeight1 === blockHeight + 1) + assert(blockHeight1 == blockHeight + 1) bitcoinClient.getTxConfirmations(tx1.txid).pipeTo(sender.ref) sender.expectMsg(Some(1)) bitcoinClient.isTransactionOutputSpendable(tx1.txid, 0, includeMempool = false).pipeTo(sender.ref) @@ -803,7 +803,7 @@ class BitcoinCoreClientSpec extends TestKitBaseClass with BitcoindService with A bitcoinClient.getReceivePubkey(receiveAddress = Some(address)).pipeTo(sender.ref) val receiveKey = sender.expectMsgType[PublicKey] - assert(addressToPublicKeyScript(address, Block.RegtestGenesisBlock.hash) === Script.pay2wpkh(receiveKey)) + assert(addressToPublicKeyScript(address, Block.RegtestGenesisBlock.hash) == Script.pay2wpkh(receiveKey)) } } \ No newline at end of file diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/ZmqWatcherSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/ZmqWatcherSpec.scala index ff42206b52..b521692e20 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/ZmqWatcherSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/ZmqWatcherSpec.scala @@ -95,7 +95,7 @@ class ZmqWatcherSpec extends TestKitBaseClass with AnyFunSuiteLike with Bitcoind restartBitcoind(probe) generateBlocks(1) val block2 = listener.expectMsgType[CurrentBlockHeight] - assert(block2.blockHeight === block1.blockHeight + 1) + assert(block2.blockHeight == block1.blockHeight + 1) listener.expectNoMessage(100 millis) }) } @@ -141,17 +141,17 @@ class ZmqWatcherSpec extends TestKitBaseClass with AnyFunSuiteLike with Bitcoind // When the watcher starts, it broadcasts the current height. val block1 = listener.expectMsgType[CurrentBlockHeight] - assert(blockHeight.get() === block1.blockHeight.toLong) + assert(blockHeight.get() == block1.blockHeight.toLong) listener.expectNoMessage(100 millis) generateBlocks(1) - assert(listener.expectMsgType[CurrentBlockHeight].blockHeight === block1.blockHeight + 1) - assert(blockHeight.get() === block1.blockHeight.toLong + 1) + assert(listener.expectMsgType[CurrentBlockHeight].blockHeight == block1.blockHeight + 1) + assert(blockHeight.get() == block1.blockHeight.toLong + 1) listener.expectNoMessage(100 millis) generateBlocks(5) - assert(listener.expectMsgType[CurrentBlockHeight].blockHeight === block1.blockHeight + 6) - assert(blockHeight.get() === block1.blockHeight.toLong + 6) + assert(listener.expectMsgType[CurrentBlockHeight].blockHeight == block1.blockHeight + 6) + assert(blockHeight.get() == block1.blockHeight.toLong + 6) listener.expectNoMessage(100 millis) }) } @@ -170,17 +170,17 @@ class ZmqWatcherSpec extends TestKitBaseClass with AnyFunSuiteLike with Bitcoind listener.expectNoMessage(1 second) watcher ! ListWatches(listener.ref) - assert(listener.expectMsgType[Set[Watch[_]]].size === 2) + assert(listener.expectMsgType[Set[Watch[_]]].size == 2) generateBlocks(1) - assert(listener.expectMsgType[WatchFundingConfirmedTriggered].tx.txid === tx.txid) + assert(listener.expectMsgType[WatchFundingConfirmedTriggered].tx.txid == tx.txid) listener.expectNoMessage(1 second) watcher ! ListWatches(listener.ref) - assert(listener.expectMsgType[Set[Watch[_]]].size === 1) + assert(listener.expectMsgType[Set[Watch[_]]].size == 1) generateBlocks(3) - assert(listener.expectMsgType[WatchFundingDeeplyBuriedTriggered].tx.txid === tx.txid) + assert(listener.expectMsgType[WatchFundingDeeplyBuriedTriggered].tx.txid == tx.txid) listener.expectNoMessage(1 second) watcher ! ListWatches(listener.ref) @@ -188,11 +188,11 @@ class ZmqWatcherSpec extends TestKitBaseClass with AnyFunSuiteLike with Bitcoind // If we try to watch a transaction that has already been confirmed, we should immediately receive a WatchEventConfirmed. watcher ! WatchFundingConfirmed(listener.ref, tx.txid, 1) - assert(listener.expectMsgType[WatchFundingConfirmedTriggered].tx.txid === tx.txid) + assert(listener.expectMsgType[WatchFundingConfirmedTriggered].tx.txid == tx.txid) watcher ! WatchFundingConfirmed(listener.ref, tx.txid, 2) - assert(listener.expectMsgType[WatchFundingConfirmedTriggered].tx.txid === tx.txid) + assert(listener.expectMsgType[WatchFundingConfirmedTriggered].tx.txid == tx.txid) watcher ! WatchFundingDeeplyBuried(listener.ref, tx.txid, 4) - assert(listener.expectMsgType[WatchFundingDeeplyBuriedTriggered].tx.txid === tx.txid) + assert(listener.expectMsgType[WatchFundingDeeplyBuriedTriggered].tx.txid == tx.txid) listener.expectNoMessage(1 second) watcher ! ListWatches(listener.ref) @@ -215,7 +215,7 @@ class ZmqWatcherSpec extends TestKitBaseClass with AnyFunSuiteLike with Bitcoind listener.expectNoMessage(1 second) watcher ! ListWatches(listener.ref) - assert(listener.expectMsgType[Set[Watch[_]]].size === 2) + assert(listener.expectMsgType[Set[Watch[_]]].size == 2) bitcoinClient.publishTransaction(tx1).pipeTo(probe.ref) probe.expectMsg(tx1.txid) @@ -234,7 +234,7 @@ class ZmqWatcherSpec extends TestKitBaseClass with AnyFunSuiteLike with Bitcoind watcher ! ListWatches(listener.ref) val watches1 = listener.expectMsgType[Set[Watch[_]]] - assert(watches1.size === 1) + assert(watches1.size == 1) assert(watches1.forall(_.isInstanceOf[WatchFundingSpent])) // Let's submit tx2, and set a watch after it has been confirmed this time. @@ -248,7 +248,7 @@ class ZmqWatcherSpec extends TestKitBaseClass with AnyFunSuiteLike with Bitcoind watcher ! ListWatches(listener.ref) val watches2 = listener.expectMsgType[Set[Watch[_]]] - assert(watches2.size === 1) + assert(watches2.size == 1) assert(watches2.forall(_.isInstanceOf[WatchFundingSpent])) watcher ! StopWatching(listener.ref) @@ -302,7 +302,7 @@ class ZmqWatcherSpec extends TestKitBaseClass with AnyFunSuiteLike with Bitcoind generateBlocks(1) probe.expectMsg(WatchFundingSpentTriggered(tx2)) // tx2 is confirmed which triggers WatchEventSpent again generateBlocks(1) - assert(probe.expectMsgType[WatchFundingConfirmedTriggered].tx === tx1) // tx1 now has 3 confirmations + assert(probe.expectMsgType[WatchFundingConfirmedTriggered].tx == tx1) // tx1 now has 3 confirmations }) } @@ -350,7 +350,7 @@ class ZmqWatcherSpec extends TestKitBaseClass with AnyFunSuiteLike with Bitcoind watcher ! WatchOutputSpent(actor1.ref, txid, 1, Set.empty) watcher ! ListWatches(actor1.ref) val watches1 = actor1.expectMsgType[Set[Watch[_]]] - assert(watches1.size === 6) + assert(watches1.size == 6) watcher ! WatchFundingConfirmed(actor2.ref, txid, 2) watcher ! WatchFundingDeeplyBuried(actor2.ref, txid, 3) @@ -359,7 +359,7 @@ class ZmqWatcherSpec extends TestKitBaseClass with AnyFunSuiteLike with Bitcoind watcher ! WatchOutputSpent(actor2.ref, txid, 1, Set.empty) watcher ! ListWatches(actor2.ref) val watches2 = actor2.expectMsgType[Set[Watch[_]]] - assert(watches2.size === 11) + assert(watches2.size == 11) assert(watches1.forall(w => watches2.contains(w))) watcher ! StopWatching(actor2.ref) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/BitcoinCoreFeeProviderSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/BitcoinCoreFeeProviderSpec.scala index 732f7abe76..090519e9b4 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/BitcoinCoreFeeProviderSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/BitcoinCoreFeeProviderSpec.scala @@ -117,7 +117,7 @@ class BitcoinCoreFeeProviderSpec extends TestKitBaseClass with BitcoindService w regtestProvider.mempoolMinFee().pipeTo(sender.ref) val mempoolMinFee = sender.expectMsgType[FeeratePerKB] // The regtest provider doesn't have any transaction in its mempool, so it defaults to the min_relay_fee. - assert(mempoolMinFee.feerate.toLong === FeeratePerKw.MinimumRelayFeeRate) + assert(mempoolMinFee.feerate.toLong == FeeratePerKw.MinimumRelayFeeRate) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/FallbackFeeProviderSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/FallbackFeeProviderSpec.scala index 4969ddcb19..0112b2ecf9 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/FallbackFeeProviderSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/FallbackFeeProviderSpec.scala @@ -55,19 +55,19 @@ class FallbackFeeProviderSpec extends AnyFunSuite { val fallbackFeeProvider = new FallbackFeeProvider(provider0 :: provider1 :: provider3 :: provider5 :: provider7 :: Nil, FeeratePerByte(1 sat)) - assert(await(fallbackFeeProvider.getFeerates) === provider1.feeratesPerKB) + assert(await(fallbackFeeProvider.getFeerates) == provider1.feeratesPerKB) - assert(await(fallbackFeeProvider.getFeerates) === provider3.feeratesPerKB) - assert(await(fallbackFeeProvider.getFeerates) === provider3.feeratesPerKB) - assert(await(fallbackFeeProvider.getFeerates) === provider3.feeratesPerKB) + assert(await(fallbackFeeProvider.getFeerates) == provider3.feeratesPerKB) + assert(await(fallbackFeeProvider.getFeerates) == provider3.feeratesPerKB) + assert(await(fallbackFeeProvider.getFeerates) == provider3.feeratesPerKB) - assert(await(fallbackFeeProvider.getFeerates) === provider5.feeratesPerKB) - assert(await(fallbackFeeProvider.getFeerates) === provider5.feeratesPerKB) - assert(await(fallbackFeeProvider.getFeerates) === provider5.feeratesPerKB) - assert(await(fallbackFeeProvider.getFeerates) === provider5.feeratesPerKB) - assert(await(fallbackFeeProvider.getFeerates) === provider5.feeratesPerKB) + assert(await(fallbackFeeProvider.getFeerates) == provider5.feeratesPerKB) + assert(await(fallbackFeeProvider.getFeerates) == provider5.feeratesPerKB) + assert(await(fallbackFeeProvider.getFeerates) == provider5.feeratesPerKB) + assert(await(fallbackFeeProvider.getFeerates) == provider5.feeratesPerKB) + assert(await(fallbackFeeProvider.getFeerates) == provider5.feeratesPerKB) - assert(await(fallbackFeeProvider.getFeerates) === provider7.feeratesPerKB) + assert(await(fallbackFeeProvider.getFeerates) == provider7.feeratesPerKB) } test("ensure minimum feerate") { @@ -75,7 +75,7 @@ class FallbackFeeProviderSpec extends AnyFunSuite { val minFeeratePerByte = FeeratePerByte(2 sat) val minFeeratePerKB = FeeratePerKB(minFeeratePerByte) val fallbackFeeProvider = new FallbackFeeProvider(constantFeeProvider :: Nil, minFeeratePerByte) - assert(await(fallbackFeeProvider.getFeerates) === FeeratesPerKB(FeeratePerKB(64000 sat), FeeratePerKB(32000 sat), FeeratePerKB(16000 sat), FeeratePerKB(8000 sat), FeeratePerKB(4000 sat), minFeeratePerKB, minFeeratePerKB, minFeeratePerKB, minFeeratePerKB)) + assert(await(fallbackFeeProvider.getFeerates) == FeeratesPerKB(FeeratePerKB(64000 sat), FeeratePerKB(32000 sat), FeeratePerKB(16000 sat), FeeratePerKB(8000 sat), FeeratePerKB(4000 sat), minFeeratePerKB, minFeeratePerKB, minFeeratePerKB, minFeeratePerKB)) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/FeeEstimatorSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/FeeEstimatorSpec.scala index 9ab3f7380a..3e0567eb4a 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/FeeEstimatorSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/FeeEstimatorSpec.scala @@ -41,10 +41,10 @@ class FeeEstimatorSpec extends AnyFunSuite { val feeConf = OnChainFeeConf(FeeTargets(1, 2, 6, 1, 1, 1), feeEstimator, closeOnOfflineMismatch = true, updateFeeMinDiffRatio = 0.1, defaultFeerateTolerance, Map.empty) feeEstimator.setFeerate(FeeratesPerKw.single(FeeratePerKw(10000 sat)).copy(blocks_2 = FeeratePerKw(5000 sat))) - assert(feeConf.getCommitmentFeerate(randomKey().publicKey, channelType, 100000 sat, None) === FeeratePerKw(5000 sat)) + assert(feeConf.getCommitmentFeerate(randomKey().publicKey, channelType, 100000 sat, None) == FeeratePerKw(5000 sat)) val currentFeerates = CurrentFeerates(FeeratesPerKw.single(FeeratePerKw(10000 sat)).copy(blocks_2 = FeeratePerKw(4000 sat))) - assert(feeConf.getCommitmentFeerate(randomKey().publicKey, channelType, 100000 sat, Some(currentFeerates)) === FeeratePerKw(4000 sat)) + assert(feeConf.getCommitmentFeerate(randomKey().publicKey, channelType, 100000 sat, Some(currentFeerates)) == FeeratePerKw(4000 sat)) } test("get commitment feerate (anchor outputs)") { @@ -56,35 +56,35 @@ class FeeEstimatorSpec extends AnyFunSuite { val feeConf = OnChainFeeConf(FeeTargets(1, 2, 6, 1, 1, 1), feeEstimator, closeOnOfflineMismatch = true, updateFeeMinDiffRatio = 0.1, defaultFeerateTolerance, Map(overrideNodeId -> defaultFeerateTolerance.copy(anchorOutputMaxCommitFeerate = overrideMaxCommitFeerate))) feeEstimator.setFeerate(FeeratesPerKw.single(FeeratePerKw(10000 sat)).copy(blocks_2 = defaultMaxCommitFeerate / 2, mempoolMinFee = FeeratePerKw(250 sat))) - assert(feeConf.getCommitmentFeerate(defaultNodeId, ChannelTypes.AnchorOutputs, 100000 sat, None) === defaultMaxCommitFeerate / 2) - assert(feeConf.getCommitmentFeerate(defaultNodeId, ChannelTypes.AnchorOutputsZeroFeeHtlcTx, 100000 sat, None) === defaultMaxCommitFeerate / 2) + assert(feeConf.getCommitmentFeerate(defaultNodeId, ChannelTypes.AnchorOutputs, 100000 sat, None) == defaultMaxCommitFeerate / 2) + assert(feeConf.getCommitmentFeerate(defaultNodeId, ChannelTypes.AnchorOutputsZeroFeeHtlcTx, 100000 sat, None) == defaultMaxCommitFeerate / 2) feeEstimator.setFeerate(FeeratesPerKw.single(FeeratePerKw(10000 sat)).copy(blocks_2 = defaultMaxCommitFeerate * 2, mempoolMinFee = FeeratePerKw(250 sat))) - assert(feeConf.getCommitmentFeerate(defaultNodeId, ChannelTypes.AnchorOutputs, 100000 sat, None) === defaultMaxCommitFeerate) - assert(feeConf.getCommitmentFeerate(defaultNodeId, ChannelTypes.AnchorOutputsZeroFeeHtlcTx, 100000 sat, None) === defaultMaxCommitFeerate) - assert(feeConf.getCommitmentFeerate(overrideNodeId, ChannelTypes.AnchorOutputs, 100000 sat, None) === overrideMaxCommitFeerate) - assert(feeConf.getCommitmentFeerate(overrideNodeId, ChannelTypes.AnchorOutputsZeroFeeHtlcTx, 100000 sat, None) === overrideMaxCommitFeerate) + assert(feeConf.getCommitmentFeerate(defaultNodeId, ChannelTypes.AnchorOutputs, 100000 sat, None) == defaultMaxCommitFeerate) + assert(feeConf.getCommitmentFeerate(defaultNodeId, ChannelTypes.AnchorOutputsZeroFeeHtlcTx, 100000 sat, None) == defaultMaxCommitFeerate) + assert(feeConf.getCommitmentFeerate(overrideNodeId, ChannelTypes.AnchorOutputs, 100000 sat, None) == overrideMaxCommitFeerate) + assert(feeConf.getCommitmentFeerate(overrideNodeId, ChannelTypes.AnchorOutputsZeroFeeHtlcTx, 100000 sat, None) == overrideMaxCommitFeerate) val currentFeerates1 = CurrentFeerates(FeeratesPerKw.single(FeeratePerKw(10000 sat)).copy(blocks_2 = defaultMaxCommitFeerate / 2, mempoolMinFee = FeeratePerKw(250 sat))) - assert(feeConf.getCommitmentFeerate(defaultNodeId, ChannelTypes.AnchorOutputs, 100000 sat, Some(currentFeerates1)) === defaultMaxCommitFeerate / 2) - assert(feeConf.getCommitmentFeerate(defaultNodeId, ChannelTypes.AnchorOutputsZeroFeeHtlcTx, 100000 sat, Some(currentFeerates1)) === defaultMaxCommitFeerate / 2) + assert(feeConf.getCommitmentFeerate(defaultNodeId, ChannelTypes.AnchorOutputs, 100000 sat, Some(currentFeerates1)) == defaultMaxCommitFeerate / 2) + assert(feeConf.getCommitmentFeerate(defaultNodeId, ChannelTypes.AnchorOutputsZeroFeeHtlcTx, 100000 sat, Some(currentFeerates1)) == defaultMaxCommitFeerate / 2) val currentFeerates2 = CurrentFeerates(FeeratesPerKw.single(FeeratePerKw(10000 sat)).copy(blocks_2 = defaultMaxCommitFeerate * 1.5, mempoolMinFee = FeeratePerKw(250 sat))) feeEstimator.setFeerate(FeeratesPerKw.single(FeeratePerKw(10000 sat)).copy(blocks_2 = defaultMaxCommitFeerate / 2, mempoolMinFee = FeeratePerKw(250 sat))) - assert(feeConf.getCommitmentFeerate(defaultNodeId, ChannelTypes.AnchorOutputs, 100000 sat, Some(currentFeerates2)) === defaultMaxCommitFeerate) - assert(feeConf.getCommitmentFeerate(defaultNodeId, ChannelTypes.AnchorOutputsZeroFeeHtlcTx, 100000 sat, Some(currentFeerates2)) === defaultMaxCommitFeerate) + assert(feeConf.getCommitmentFeerate(defaultNodeId, ChannelTypes.AnchorOutputs, 100000 sat, Some(currentFeerates2)) == defaultMaxCommitFeerate) + assert(feeConf.getCommitmentFeerate(defaultNodeId, ChannelTypes.AnchorOutputsZeroFeeHtlcTx, 100000 sat, Some(currentFeerates2)) == defaultMaxCommitFeerate) val highFeerates = CurrentFeerates(FeeratesPerKw.single(FeeratePerKw(25000 sat)).copy(mempoolMinFee = FeeratePerKw(10000 sat))) - assert(feeConf.getCommitmentFeerate(defaultNodeId, ChannelTypes.AnchorOutputs, 100000 sat, Some(highFeerates)) === FeeratePerKw(10000 sat) * 1.25) - assert(feeConf.getCommitmentFeerate(defaultNodeId, ChannelTypes.AnchorOutputsZeroFeeHtlcTx, 100000 sat, Some(highFeerates)) === FeeratePerKw(10000 sat) * 1.25) - assert(feeConf.getCommitmentFeerate(overrideNodeId, ChannelTypes.AnchorOutputs, 100000 sat, Some(highFeerates)) === FeeratePerKw(10000 sat) * 1.25) - assert(feeConf.getCommitmentFeerate(overrideNodeId, ChannelTypes.AnchorOutputsZeroFeeHtlcTx, 100000 sat, Some(highFeerates)) === FeeratePerKw(10000 sat) * 1.25) + assert(feeConf.getCommitmentFeerate(defaultNodeId, ChannelTypes.AnchorOutputs, 100000 sat, Some(highFeerates)) == FeeratePerKw(10000 sat) * 1.25) + assert(feeConf.getCommitmentFeerate(defaultNodeId, ChannelTypes.AnchorOutputsZeroFeeHtlcTx, 100000 sat, Some(highFeerates)) == FeeratePerKw(10000 sat) * 1.25) + assert(feeConf.getCommitmentFeerate(overrideNodeId, ChannelTypes.AnchorOutputs, 100000 sat, Some(highFeerates)) == FeeratePerKw(10000 sat) * 1.25) + assert(feeConf.getCommitmentFeerate(overrideNodeId, ChannelTypes.AnchorOutputsZeroFeeHtlcTx, 100000 sat, Some(highFeerates)) == FeeratePerKw(10000 sat) * 1.25) feeEstimator.setFeerate(FeeratesPerKw.single(FeeratePerKw(25000 sat)).copy(mempoolMinFee = FeeratePerKw(10000 sat))) - assert(feeConf.getCommitmentFeerate(defaultNodeId, ChannelTypes.AnchorOutputs, 100000 sat, None) === FeeratePerKw(10000 sat) * 1.25) - assert(feeConf.getCommitmentFeerate(defaultNodeId, ChannelTypes.AnchorOutputsZeroFeeHtlcTx, 100000 sat, None) === FeeratePerKw(10000 sat) * 1.25) - assert(feeConf.getCommitmentFeerate(overrideNodeId, ChannelTypes.AnchorOutputs, 100000 sat, None) === FeeratePerKw(10000 sat) * 1.25) - assert(feeConf.getCommitmentFeerate(overrideNodeId, ChannelTypes.AnchorOutputsZeroFeeHtlcTx, 100000 sat, None) === FeeratePerKw(10000 sat) * 1.25) + assert(feeConf.getCommitmentFeerate(defaultNodeId, ChannelTypes.AnchorOutputs, 100000 sat, None) == FeeratePerKw(10000 sat) * 1.25) + assert(feeConf.getCommitmentFeerate(defaultNodeId, ChannelTypes.AnchorOutputsZeroFeeHtlcTx, 100000 sat, None) == FeeratePerKw(10000 sat) * 1.25) + assert(feeConf.getCommitmentFeerate(overrideNodeId, ChannelTypes.AnchorOutputs, 100000 sat, None) == FeeratePerKw(10000 sat) * 1.25) + assert(feeConf.getCommitmentFeerate(overrideNodeId, ChannelTypes.AnchorOutputsZeroFeeHtlcTx, 100000 sat, None) == FeeratePerKw(10000 sat) * 1.25) } test("fee difference too high") { @@ -102,7 +102,7 @@ class FeeEstimatorSpec extends AnyFunSuite { (FeeratePerKw(250 sat), FeeratePerKw(1500 sat), true), ) testCases.foreach { case (networkFeerate, proposedFeerate, expected) => - assert(tolerance.isFeeDiffTooHigh(channelType, networkFeerate, proposedFeerate) === expected) + assert(tolerance.isFeeDiffTooHigh(channelType, networkFeerate, proposedFeerate) == expected) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/FeeProviderSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/FeeProviderSpec.scala index efeda2cee2..73c4d4548f 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/FeeProviderSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/FeeProviderSpec.scala @@ -23,17 +23,17 @@ import org.scalatest.funsuite.AnyFunSuite class FeeProviderSpec extends AnyFunSuite { test("convert fee rates") { - assert(FeeratePerByte(FeeratePerKw(2000 sat)) === FeeratePerByte(8 sat)) - assert(FeeratePerKB(FeeratePerByte(10 sat)) === FeeratePerKB(10000 sat)) - assert(FeeratePerKB(FeeratePerKw(25 sat)) === FeeratePerKB(100 sat)) - assert(FeeratePerKw(FeeratePerKB(10000 sat)) === FeeratePerKw(2500 sat)) - assert(FeeratePerKw(FeeratePerByte(10 sat)) === FeeratePerKw(2500 sat)) + assert(FeeratePerByte(FeeratePerKw(2000 sat)) == FeeratePerByte(8 sat)) + assert(FeeratePerKB(FeeratePerByte(10 sat)) == FeeratePerKB(10000 sat)) + assert(FeeratePerKB(FeeratePerKw(25 sat)) == FeeratePerKB(100 sat)) + assert(FeeratePerKw(FeeratePerKB(10000 sat)) == FeeratePerKw(2500 sat)) + assert(FeeratePerKw(FeeratePerByte(10 sat)) == FeeratePerKw(2500 sat)) } test("enforce a minimum feerate-per-kw") { - assert(FeeratePerKw(FeeratePerKB(1000 sat)) === MinimumFeeratePerKw) - assert(FeeratePerKw(FeeratePerKB(500 sat)) === MinimumFeeratePerKw) - assert(FeeratePerKw(FeeratePerByte(1 sat)) === MinimumFeeratePerKw) + assert(FeeratePerKw(FeeratePerKB(1000 sat)) == MinimumFeeratePerKw) + assert(FeeratePerKw(FeeratePerKB(500 sat)) == MinimumFeeratePerKw) + assert(FeeratePerKw(FeeratePerByte(1 sat)) == MinimumFeeratePerKw) } test("compare feerates") { @@ -48,12 +48,12 @@ class FeeProviderSpec extends AnyFunSuite { } test("numeric operations") { - assert(FeeratePerKw(100 sat) * 0.4 === FeeratePerKw(40 sat)) - assert(FeeratePerKw(501 sat) * 0.5 === FeeratePerKw(250 sat)) - assert(FeeratePerKw(5 sat) * 5 === FeeratePerKw(25 sat)) - assert(FeeratePerKw(100 sat) / 10 === FeeratePerKw(10 sat)) - assert(FeeratePerKw(101 sat) / 10 === FeeratePerKw(10 sat)) - assert(FeeratePerKw(100 sat) + FeeratePerKw(50 sat) === FeeratePerKw(150 sat)) + assert(FeeratePerKw(100 sat) * 0.4 == FeeratePerKw(40 sat)) + assert(FeeratePerKw(501 sat) * 0.5 == FeeratePerKw(250 sat)) + assert(FeeratePerKw(5 sat) * 5 == FeeratePerKw(25 sat)) + assert(FeeratePerKw(100 sat) / 10 == FeeratePerKw(10 sat)) + assert(FeeratePerKw(101 sat) / 10 == FeeratePerKw(10 sat)) + assert(FeeratePerKw(100 sat) + FeeratePerKw(50 sat) == FeeratePerKw(150 sat)) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/watchdogs/BlockchainWatchdogSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/watchdogs/BlockchainWatchdogSpec.scala index 24b73080d3..819579ce96 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/watchdogs/BlockchainWatchdogSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/watchdogs/BlockchainWatchdogSpec.scala @@ -53,8 +53,8 @@ class BlockchainWatchdogSpec extends ScalaTestWithActorTestKit(ConfigFactory.loa // eventListener.expectMessageType[DangerousBlocksSkew] ) eventListener.expectNoMessage(100 millis) - // assert(events.map(_.recentHeaders.source).toSet === Set("bitcoinheaders.net", "blockcypher.com", "blockstream.info", "mempool.space")) - assert(events.map(_.recentHeaders.source).toSet === Set("bitcoinheaders.net", "blockstream.info", "mempool.space")) + // assert(events.map(_.recentHeaders.source).toSet == Set("bitcoinheaders.net", "blockcypher.com", "blockstream.info", "mempool.space")) + assert(events.map(_.recentHeaders.source).toSet == Set("bitcoinheaders.net", "blockstream.info", "mempool.space")) testKit.stop(watchdog) } @@ -70,8 +70,8 @@ class BlockchainWatchdogSpec extends ScalaTestWithActorTestKit(ConfigFactory.loa // eventListener.expectMessageType[DangerousBlocksSkew] ) eventListener.expectNoMessage(100 millis) - // assert(events.map(_.recentHeaders.source).toSet === Set("blockcypher.com", "blockstream.info", "mempool.space")) - assert(events.map(_.recentHeaders.source).toSet === Set("blockstream.info", "mempool.space")) + // assert(events.map(_.recentHeaders.source).toSet == Set("blockcypher.com", "blockstream.info", "mempool.space")) + assert(events.map(_.recentHeaders.source).toSet == Set("blockstream.info", "mempool.space")) testKit.stop(watchdog) } @@ -82,21 +82,21 @@ class BlockchainWatchdogSpec extends ScalaTestWithActorTestKit(ConfigFactory.loa val watchdog = testKit.spawn(BlockchainWatchdog(nodeParamsTestnet, 1 second, blockTimeout)) watchdog ! WrappedCurrentBlockHeight(BlockHeight(500000)) - assert(eventListener.expectMessageType[DangerousBlocksSkew].recentHeaders.currentBlockHeight === BlockHeight(500000)) - assert(eventListener.expectMessageType[DangerousBlocksSkew].recentHeaders.currentBlockHeight === BlockHeight(500000)) - // assert(eventListener.expectMessageType[DangerousBlocksSkew].recentHeaders.currentBlockHeight === BlockHeight(500000)) + assert(eventListener.expectMessageType[DangerousBlocksSkew].recentHeaders.currentBlockHeight == BlockHeight(500000)) + assert(eventListener.expectMessageType[DangerousBlocksSkew].recentHeaders.currentBlockHeight == BlockHeight(500000)) + // assert(eventListener.expectMessageType[DangerousBlocksSkew].recentHeaders.currentBlockHeight == BlockHeight(500000)) eventListener.expectNoMessage(100 millis) // If we don't receive blocks, we check blockchain sources. - assert(eventListener.expectMessageType[DangerousBlocksSkew].recentHeaders.currentBlockHeight === BlockHeight(500000)) - assert(eventListener.expectMessageType[DangerousBlocksSkew].recentHeaders.currentBlockHeight === BlockHeight(500000)) - // assert(eventListener.expectMessageType[DangerousBlocksSkew].recentHeaders.currentBlockHeight === BlockHeight(500000)) + assert(eventListener.expectMessageType[DangerousBlocksSkew].recentHeaders.currentBlockHeight == BlockHeight(500000)) + assert(eventListener.expectMessageType[DangerousBlocksSkew].recentHeaders.currentBlockHeight == BlockHeight(500000)) + // assert(eventListener.expectMessageType[DangerousBlocksSkew].recentHeaders.currentBlockHeight == BlockHeight(500000)) eventListener.expectNoMessage(100 millis) // And we keep checking blockchain sources until we receive a block. - assert(eventListener.expectMessageType[DangerousBlocksSkew].recentHeaders.currentBlockHeight === BlockHeight(500000)) - assert(eventListener.expectMessageType[DangerousBlocksSkew].recentHeaders.currentBlockHeight === BlockHeight(500000)) - // assert(eventListener.expectMessageType[DangerousBlocksSkew].recentHeaders.currentBlockHeight === BlockHeight(500000)) + assert(eventListener.expectMessageType[DangerousBlocksSkew].recentHeaders.currentBlockHeight == BlockHeight(500000)) + assert(eventListener.expectMessageType[DangerousBlocksSkew].recentHeaders.currentBlockHeight == BlockHeight(500000)) + // assert(eventListener.expectMessageType[DangerousBlocksSkew].recentHeaders.currentBlockHeight == BlockHeight(500000)) eventListener.expectNoMessage(100 millis) } @@ -123,7 +123,7 @@ class BlockchainWatchdogSpec extends ScalaTestWithActorTestKit(ConfigFactory.loa // eventListener.expectMessageType[DangerousBlocksSkew] ) eventListener.expectNoMessage(100 millis) - assert(events.map(_.recentHeaders.source).toSet === Set("blockstream.info", "mempool.space")) + assert(events.map(_.recentHeaders.source).toSet == Set("blockstream.info", "mempool.space")) testKit.stop(watchdog) } else { cancel("Tor daemon is not up and running") diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/watchdogs/ExplorerApiSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/watchdogs/ExplorerApiSpec.scala index 8c1328bcb3..846292dc1b 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/watchdogs/ExplorerApiSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/watchdogs/ExplorerApiSpec.scala @@ -36,10 +36,10 @@ class ExplorerApiSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl val sender = testKit.createTestProbe[LatestHeaders]() api ! CheckLatestHeaders(sender.ref) val latestHeaders = sender.expectMessageType[LatestHeaders] - assert(latestHeaders.currentBlockHeight === BlockHeight(630450)) + assert(latestHeaders.currentBlockHeight == BlockHeight(630450)) assert(latestHeaders.blockHeaders.nonEmpty) assert(latestHeaders.blockHeaders.forall(_.blockHeight > BlockHeight(630450))) - assert(latestHeaders.source === explorer.name) + assert(latestHeaders.source == explorer.name) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/ChannelDataSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/ChannelDataSpec.scala index 1b24852f85..e86490470a 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/ChannelDataSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/ChannelDataSpec.scala @@ -21,14 +21,14 @@ import fr.acinq.bitcoin.scalacompat.{ByteVector32, OutPoint, SatoshiLong, Transa import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.WatchFundingSpentTriggered import fr.acinq.eclair.channel.Helpers.Closing import fr.acinq.eclair.channel.fsm.Channel -import fr.acinq.eclair.channel.states.ChannelStateTestsHelperMethods +import fr.acinq.eclair.channel.states.ChannelStateTestsBase import fr.acinq.eclair.transactions.Transactions._ import fr.acinq.eclair.wire.protocol.{CommitSig, RevokeAndAck, UnknownNextPeer, UpdateAddHtlc} import fr.acinq.eclair.{MilliSatoshiLong, NodeParams, TestKitBaseClass} import org.scalatest.funsuite.AnyFunSuiteLike import scodec.bits.ByteVector -class ChannelDataSpec extends TestKitBaseClass with AnyFunSuiteLike with ChannelStateTestsHelperMethods { +class ChannelDataSpec extends TestKitBaseClass with AnyFunSuiteLike with ChannelStateTestsBase { implicit val log: akka.event.LoggingAdapter = akka.event.NoLogging @@ -82,15 +82,15 @@ class ChannelDataSpec extends TestKitBaseClass with AnyFunSuiteLike with Channel val aliceClosing = alice.stateData.asInstanceOf[DATA_CLOSING] assert(aliceClosing.localCommitPublished.nonEmpty) val lcp = aliceClosing.localCommitPublished.get - assert(lcp.commitTx.txOut.length === 6) + assert(lcp.commitTx.txOut.length == 6) assert(lcp.claimMainDelayedOutputTx.nonEmpty) - assert(lcp.htlcTxs.size === 4) // we have one entry for each non-dust htlc + assert(lcp.htlcTxs.size == 4) // we have one entry for each non-dust htlc val htlcTimeoutTxs = getHtlcTimeoutTxs(lcp) - assert(htlcTimeoutTxs.length === 2) + assert(htlcTimeoutTxs.length == 2) val htlcSuccessTxs = getHtlcSuccessTxs(lcp) - assert(htlcSuccessTxs.length === 1) // we only have the preimage for 1 of the 2 non-dust htlcs + assert(htlcSuccessTxs.length == 1) // we only have the preimage for 1 of the 2 non-dust htlcs val remainingHtlcOutpoint = lcp.htlcTxs.collect { case (outpoint, None) => outpoint }.head - assert(lcp.claimHtlcDelayedTxs.length === 0) // we will publish 3rd-stage txs once htlc txs confirm + assert(lcp.claimHtlcDelayedTxs.length == 0) // we will publish 3rd-stage txs once htlc txs confirm assert(!lcp.isConfirmed) assert(!lcp.isDone) @@ -122,7 +122,7 @@ class ChannelDataSpec extends TestKitBaseClass with AnyFunSuiteLike with Channel Closing.updateLocalCommitPublished(current1, tx) } assert(!lcp3.isDone) - assert(lcp3.claimHtlcDelayedTxs.length === 3) + assert(lcp3.claimHtlcDelayedTxs.length == 3) val lcp4 = lcp3.claimHtlcDelayedTxs.map(_.tx).foldLeft(lcp3) { case (current, tx) => Closing.updateLocalCommitPublished(current, tx) @@ -145,7 +145,7 @@ class ChannelDataSpec extends TestKitBaseClass with AnyFunSuiteLike with Channel Closing.updateLocalCommitPublished(current1, tx) } assert(!lcp3.isDone) - assert(lcp3.claimHtlcDelayedTxs.length === 3) + assert(lcp3.claimHtlcDelayedTxs.length == 3) val lcp4 = lcp3.claimHtlcDelayedTxs.map(_.tx).foldLeft(lcp3) { case (current, tx) => Closing.updateLocalCommitPublished(current, tx) @@ -157,11 +157,11 @@ class ChannelDataSpec extends TestKitBaseClass with AnyFunSuiteLike with Channel val aliceClosing1 = alice.stateData.asInstanceOf[DATA_CLOSING] val lcp5 = aliceClosing1.localCommitPublished.get.copy(irrevocablySpent = lcp4.irrevocablySpent, claimHtlcDelayedTxs = lcp4.claimHtlcDelayedTxs) assert(lcp5.htlcTxs(remainingHtlcOutpoint) !== None) - assert(lcp5.claimHtlcDelayedTxs.length === 3) + assert(lcp5.claimHtlcDelayedTxs.length == 3) val newHtlcSuccessTx = lcp5.htlcTxs(remainingHtlcOutpoint).get.tx val (lcp6, Some(newClaimHtlcDelayedTx)) = Closing.LocalClose.claimHtlcDelayedOutput(lcp5, nodeParams.channelKeyManager, aliceClosing.commitments, newHtlcSuccessTx, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) - assert(lcp6.claimHtlcDelayedTxs.length === 4) + assert(lcp6.claimHtlcDelayedTxs.length == 4) val lcp7 = Closing.updateLocalCommitPublished(lcp6, newHtlcSuccessTx) assert(!lcp7.isDone) @@ -180,16 +180,16 @@ class ChannelDataSpec extends TestKitBaseClass with AnyFunSuiteLike with Channel val (current1, _) = Closing.LocalClose.claimHtlcDelayedOutput(current, nodeParams.channelKeyManager, aliceClosing.commitments, tx, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) Closing.updateLocalCommitPublished(current1, tx) } - assert(lcp3.claimHtlcDelayedTxs.length === 1) + assert(lcp3.claimHtlcDelayedTxs.length == 1) assert(!lcp3.isDone) val lcp4 = Closing.updateLocalCommitPublished(lcp3, lcp3.claimHtlcDelayedTxs.head.tx) assert(!lcp4.isDone) val remainingHtlcTimeoutTxs = htlcTimeoutTxs.filter(_.input.outPoint != remoteHtlcSuccess.input.outPoint) - assert(remainingHtlcTimeoutTxs.length === 1) + assert(remainingHtlcTimeoutTxs.length == 1) val (lcp5, Some(remainingClaimHtlcTx)) = Closing.LocalClose.claimHtlcDelayedOutput(lcp4, nodeParams.channelKeyManager, aliceClosing.commitments, remainingHtlcTimeoutTxs.head.tx, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) - assert(lcp5.claimHtlcDelayedTxs.length === 2) + assert(lcp5.claimHtlcDelayedTxs.length == 2) val lcp6 = (remainingHtlcTimeoutTxs.map(_.tx) ++ Seq(remainingClaimHtlcTx.tx)).foldLeft(lcp5) { case (current, tx) => Closing.updateLocalCommitPublished(current, tx) @@ -211,7 +211,7 @@ class ChannelDataSpec extends TestKitBaseClass with AnyFunSuiteLike with Channel Closing.updateLocalCommitPublished(current1, tx) } assert(!lcp3.isDone) - assert(lcp3.claimHtlcDelayedTxs.length === 2) + assert(lcp3.claimHtlcDelayedTxs.length == 2) val lcp4 = lcp3.claimHtlcDelayedTxs.map(_.tx).foldLeft(lcp3) { case (current, tx) => Closing.updateLocalCommitPublished(current, tx) @@ -219,7 +219,7 @@ class ChannelDataSpec extends TestKitBaseClass with AnyFunSuiteLike with Channel assert(!lcp4.isDone) val remoteHtlcTimeoutTxs = getClaimHtlcTimeoutTxs(rcp).map(_.tx) - assert(remoteHtlcTimeoutTxs.length === 2) + assert(remoteHtlcTimeoutTxs.length == 2) val lcp5 = Closing.updateLocalCommitPublished(lcp4, remoteHtlcTimeoutTxs.head) assert(!lcp5.isDone) @@ -238,7 +238,7 @@ class ChannelDataSpec extends TestKitBaseClass with AnyFunSuiteLike with Channel } assert(!lcp3.isDone) - assert(lcp3.claimHtlcDelayedTxs.length === 3) + assert(lcp3.claimHtlcDelayedTxs.length == 3) val lcp4 = lcp3.claimHtlcDelayedTxs.map(_.tx).foldLeft(lcp3) { case (current, tx) => Closing.updateLocalCommitPublished(current, tx) @@ -246,14 +246,14 @@ class ChannelDataSpec extends TestKitBaseClass with AnyFunSuiteLike with Channel assert(!lcp4.isDone) // at this point the pending incoming htlc is waiting for a preimage - assert(lcp4.htlcTxs(remainingHtlcOutpoint) === None) + assert(lcp4.htlcTxs(remainingHtlcOutpoint) == None) alice ! CMD_FAIL_HTLC(1, Right(UnknownNextPeer), replyTo_opt = Some(probe.ref)) probe.expectMsgType[CommandSuccess[CMD_FAIL_HTLC]] val aliceClosing1 = alice.stateData.asInstanceOf[DATA_CLOSING] val lcp5 = aliceClosing1.localCommitPublished.get.copy(irrevocablySpent = lcp4.irrevocablySpent, claimHtlcDelayedTxs = lcp4.claimHtlcDelayedTxs) assert(!lcp5.htlcTxs.contains(remainingHtlcOutpoint)) - assert(lcp5.claimHtlcDelayedTxs.length === 3) + assert(lcp5.claimHtlcDelayedTxs.length == 3) assert(lcp5.isDone) } @@ -267,13 +267,13 @@ class ChannelDataSpec extends TestKitBaseClass with AnyFunSuiteLike with Channel val bobClosing = bob.stateData.asInstanceOf[DATA_CLOSING] assert(bobClosing.remoteCommitPublished.nonEmpty) val rcp = bobClosing.remoteCommitPublished.get - assert(rcp.commitTx.txOut.length === 6) + assert(rcp.commitTx.txOut.length == 6) assert(rcp.claimMainOutputTx.nonEmpty) - assert(rcp.claimHtlcTxs.size === 4) // we have one entry for each non-dust htlc + assert(rcp.claimHtlcTxs.size == 4) // we have one entry for each non-dust htlc val claimHtlcTimeoutTxs = getClaimHtlcTimeoutTxs(rcp) - assert(claimHtlcTimeoutTxs.length === 2) + assert(claimHtlcTimeoutTxs.length == 2) val claimHtlcSuccessTxs = getClaimHtlcSuccessTxs(rcp) - assert(claimHtlcSuccessTxs.length === 1) // we only have the preimage for 1 of the 2 non-dust htlcs + assert(claimHtlcSuccessTxs.length == 1) // we only have the preimage for 1 of the 2 non-dust htlcs val remainingHtlcOutpoint = rcp.claimHtlcTxs.collect { case (outpoint, None) => outpoint }.head assert(!rcp.isConfirmed) assert(!rcp.isDone) @@ -342,7 +342,7 @@ class ChannelDataSpec extends TestKitBaseClass with AnyFunSuiteLike with Channel assert(!rcp3.isDone) val remainingClaimHtlcTimeoutTx = claimHtlcTimeoutTxs.filter(_.input.outPoint != remoteHtlcSuccess.input.outPoint) - assert(remainingClaimHtlcTimeoutTx.length === 1) + assert(remainingClaimHtlcTimeoutTx.length == 1) val rcp4 = Closing.updateRemoteCommitPublished(rcp3, remainingClaimHtlcTimeoutTx.head.tx) assert(!rcp4.isDone) @@ -383,7 +383,7 @@ class ChannelDataSpec extends TestKitBaseClass with AnyFunSuiteLike with Channel val bobClosing1 = bob.stateData.asInstanceOf[DATA_CLOSING] val rcp4 = bobClosing1.remoteCommitPublished.get.copy(irrevocablySpent = rcp3.irrevocablySpent) assert(!rcp4.claimHtlcTxs.contains(remainingHtlcOutpoint)) - assert(rcp4.claimHtlcTxs.size === 3) + assert(rcp4.claimHtlcTxs.size == 3) assert(getClaimHtlcSuccessTxs(rcp4).size == 1) assert(getClaimHtlcTimeoutTxs(rcp4).size == 2) @@ -431,13 +431,13 @@ class ChannelDataSpec extends TestKitBaseClass with AnyFunSuiteLike with Channel val bobClosing = bob.stateData.asInstanceOf[DATA_CLOSING] assert(bobClosing.nextRemoteCommitPublished.nonEmpty) val rcp = bobClosing.nextRemoteCommitPublished.get - assert(rcp.commitTx.txOut.length === 6) + assert(rcp.commitTx.txOut.length == 6) assert(rcp.claimMainOutputTx.nonEmpty) - assert(rcp.claimHtlcTxs.size === 4) // we have one entry for each non-dust htlc + assert(rcp.claimHtlcTxs.size == 4) // we have one entry for each non-dust htlc val claimHtlcTimeoutTxs = getClaimHtlcTimeoutTxs(rcp) - assert(claimHtlcTimeoutTxs.length === 2) + assert(claimHtlcTimeoutTxs.length == 2) val claimHtlcSuccessTxs = getClaimHtlcSuccessTxs(rcp) - assert(claimHtlcSuccessTxs.length === 1) // we only have the preimage for 1 of the 2 non-dust htlcs + assert(claimHtlcSuccessTxs.length == 1) // we only have the preimage for 1 of the 2 non-dust htlcs val remainingHtlcOutpoint = rcp.claimHtlcTxs.collect { case (outpoint, None) => outpoint }.head assert(!rcp.isConfirmed) assert(!rcp.isDone) @@ -504,7 +504,7 @@ class ChannelDataSpec extends TestKitBaseClass with AnyFunSuiteLike with Channel assert(!rcp3.isDone) val remainingClaimHtlcTimeoutTx = claimHtlcTimeoutTxs.filter(_.input.outPoint != remoteHtlcSuccess.input.outPoint) - assert(remainingClaimHtlcTimeoutTx.length === 1) + assert(remainingClaimHtlcTimeoutTx.length == 1) val rcp4 = Closing.updateRemoteCommitPublished(rcp3, remainingClaimHtlcTimeoutTx.head.tx) assert(!rcp4.isDone) @@ -552,11 +552,11 @@ class ChannelDataSpec extends TestKitBaseClass with AnyFunSuiteLike with Channel alice ! WatchFundingSpentTriggered(revokedCommitTx) awaitCond(alice.stateName == CLOSING) val aliceClosing = alice.stateData.asInstanceOf[DATA_CLOSING] - assert(aliceClosing.revokedCommitPublished.length === 1) + assert(aliceClosing.revokedCommitPublished.length == 1) val rvk = aliceClosing.revokedCommitPublished.head assert(rvk.claimMainOutputTx.nonEmpty) assert(rvk.mainPenaltyTx.nonEmpty) - assert(rvk.htlcPenaltyTxs.length === 4) + assert(rvk.htlcPenaltyTxs.length == 4) assert(rvk.claimHtlcDelayedPenaltyTxs.isEmpty) assert(!rvk.isDone) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/ChannelFeaturesSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/ChannelFeaturesSpec.scala index 0981d3bac5..8a8eb9d1f1 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/ChannelFeaturesSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/ChannelFeaturesSpec.scala @@ -18,12 +18,12 @@ package fr.acinq.eclair.channel import fr.acinq.eclair.FeatureSupport._ import fr.acinq.eclair.Features._ -import fr.acinq.eclair.channel.states.ChannelStateTestsHelperMethods +import fr.acinq.eclair.channel.states.ChannelStateTestsBase import fr.acinq.eclair.transactions.Transactions import fr.acinq.eclair.{Features, InitFeature, NodeFeature, TestKitBaseClass} import org.scalatest.funsuite.AnyFunSuiteLike -class ChannelFeaturesSpec extends TestKitBaseClass with AnyFunSuiteLike with ChannelStateTestsHelperMethods { +class ChannelFeaturesSpec extends TestKitBaseClass with AnyFunSuiteLike with ChannelStateTestsBase { test("channel features determines commitment format") { val standardChannel = ChannelFeatures() @@ -33,24 +33,24 @@ class ChannelFeaturesSpec extends TestKitBaseClass with AnyFunSuiteLike with Cha assert(!standardChannel.hasFeature(Features.StaticRemoteKey)) assert(!standardChannel.hasFeature(Features.AnchorOutputs)) - assert(standardChannel.commitmentFormat === Transactions.DefaultCommitmentFormat) + assert(standardChannel.commitmentFormat == Transactions.DefaultCommitmentFormat) assert(!standardChannel.paysDirectlyToWallet) assert(staticRemoteKeyChannel.hasFeature(Features.StaticRemoteKey)) assert(!staticRemoteKeyChannel.hasFeature(Features.AnchorOutputs)) - assert(staticRemoteKeyChannel.commitmentFormat === Transactions.DefaultCommitmentFormat) + assert(staticRemoteKeyChannel.commitmentFormat == Transactions.DefaultCommitmentFormat) assert(staticRemoteKeyChannel.paysDirectlyToWallet) assert(anchorOutputsChannel.hasFeature(Features.StaticRemoteKey)) assert(anchorOutputsChannel.hasFeature(Features.AnchorOutputs)) assert(!anchorOutputsChannel.hasFeature(Features.AnchorOutputsZeroFeeHtlcTx)) - assert(anchorOutputsChannel.commitmentFormat === Transactions.UnsafeLegacyAnchorOutputsCommitmentFormat) + assert(anchorOutputsChannel.commitmentFormat == Transactions.UnsafeLegacyAnchorOutputsCommitmentFormat) assert(!anchorOutputsChannel.paysDirectlyToWallet) assert(anchorOutputsZeroFeeHtlcsChannel.hasFeature(Features.StaticRemoteKey)) assert(anchorOutputsZeroFeeHtlcsChannel.hasFeature(Features.AnchorOutputsZeroFeeHtlcTx)) assert(!anchorOutputsZeroFeeHtlcsChannel.hasFeature(Features.AnchorOutputs)) - assert(anchorOutputsZeroFeeHtlcsChannel.commitmentFormat === Transactions.ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) + assert(anchorOutputsZeroFeeHtlcsChannel.commitmentFormat == Transactions.ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) assert(!anchorOutputsZeroFeeHtlcsChannel.paysDirectlyToWallet) } @@ -75,7 +75,7 @@ class ChannelFeaturesSpec extends TestKitBaseClass with AnyFunSuiteLike with Cha ) for (testCase <- testCases) { - assert(ChannelTypes.defaultFromFeatures(testCase.localFeatures, testCase.remoteFeatures) === testCase.expectedChannelType) + assert(ChannelTypes.defaultFromFeatures(testCase.localFeatures, testCase.remoteFeatures) == testCase.expectedChannelType) } } @@ -89,7 +89,7 @@ class ChannelFeaturesSpec extends TestKitBaseClass with AnyFunSuiteLike with Cha TestCase(Features(StaticRemoteKey -> Mandatory, AnchorOutputsZeroFeeHtlcTx -> Mandatory), ChannelTypes.AnchorOutputsZeroFeeHtlcTx), ) for (testCase <- validChannelTypes) { - assert(ChannelTypes.fromFeatures(testCase.features) === testCase.expectedChannelType) + assert(ChannelTypes.fromFeatures(testCase.features) == testCase.expectedChannelType) } val invalidChannelTypes: Seq[Features[InitFeature]] = Seq( @@ -107,20 +107,20 @@ class ChannelFeaturesSpec extends TestKitBaseClass with AnyFunSuiteLike with Cha Features(StaticRemoteKey -> Mandatory, AnchorOutputs -> Mandatory, Wumbo -> Optional), ) for (features <- invalidChannelTypes) { - assert(ChannelTypes.fromFeatures(features) === ChannelTypes.UnsupportedChannelType(features)) + assert(ChannelTypes.fromFeatures(features) == ChannelTypes.UnsupportedChannelType(features)) } } test("enrich channel type with other permanent channel features") { assert(ChannelFeatures(ChannelTypes.Standard, Features[InitFeature](Wumbo -> Optional), Features.empty[InitFeature]).features.isEmpty) - assert(ChannelFeatures(ChannelTypes.Standard, Features[InitFeature](Wumbo -> Optional), Features[InitFeature](Wumbo -> Optional)).features === Set(Wumbo)) - assert(ChannelFeatures(ChannelTypes.Standard, Features[InitFeature](Wumbo -> Mandatory), Features[InitFeature](Wumbo -> Optional)).features === Set(Wumbo)) - assert(ChannelFeatures(ChannelTypes.StaticRemoteKey, Features[InitFeature](Wumbo -> Optional), Features.empty[InitFeature]).features === Set(StaticRemoteKey)) - assert(ChannelFeatures(ChannelTypes.StaticRemoteKey, Features[InitFeature](Wumbo -> Optional), Features[InitFeature](Wumbo -> Optional)).features === Set(StaticRemoteKey, Wumbo)) - assert(ChannelFeatures(ChannelTypes.AnchorOutputs, Features.empty[InitFeature], Features[InitFeature](Wumbo -> Optional)).features === Set(StaticRemoteKey, AnchorOutputs)) - assert(ChannelFeatures(ChannelTypes.AnchorOutputs, Features[InitFeature](Wumbo -> Optional), Features[InitFeature](Wumbo -> Mandatory)).features === Set(StaticRemoteKey, AnchorOutputs, Wumbo)) - assert(ChannelFeatures(ChannelTypes.AnchorOutputsZeroFeeHtlcTx, Features.empty[InitFeature], Features[InitFeature](Wumbo -> Optional)).features === Set(StaticRemoteKey, AnchorOutputsZeroFeeHtlcTx)) - assert(ChannelFeatures(ChannelTypes.AnchorOutputsZeroFeeHtlcTx, Features[InitFeature](Wumbo -> Optional), Features[InitFeature](Wumbo -> Mandatory)).features === Set(StaticRemoteKey, AnchorOutputsZeroFeeHtlcTx, Wumbo)) + assert(ChannelFeatures(ChannelTypes.Standard, Features[InitFeature](Wumbo -> Optional), Features[InitFeature](Wumbo -> Optional)).features == Set(Wumbo)) + assert(ChannelFeatures(ChannelTypes.Standard, Features[InitFeature](Wumbo -> Mandatory), Features[InitFeature](Wumbo -> Optional)).features == Set(Wumbo)) + assert(ChannelFeatures(ChannelTypes.StaticRemoteKey, Features[InitFeature](Wumbo -> Optional), Features.empty[InitFeature]).features == Set(StaticRemoteKey)) + assert(ChannelFeatures(ChannelTypes.StaticRemoteKey, Features[InitFeature](Wumbo -> Optional), Features[InitFeature](Wumbo -> Optional)).features == Set(StaticRemoteKey, Wumbo)) + assert(ChannelFeatures(ChannelTypes.AnchorOutputs, Features.empty[InitFeature], Features[InitFeature](Wumbo -> Optional)).features == Set(StaticRemoteKey, AnchorOutputs)) + assert(ChannelFeatures(ChannelTypes.AnchorOutputs, Features[InitFeature](Wumbo -> Optional), Features[InitFeature](Wumbo -> Mandatory)).features == Set(StaticRemoteKey, AnchorOutputs, Wumbo)) + assert(ChannelFeatures(ChannelTypes.AnchorOutputsZeroFeeHtlcTx, Features.empty[InitFeature], Features[InitFeature](Wumbo -> Optional)).features == Set(StaticRemoteKey, AnchorOutputsZeroFeeHtlcTx)) + assert(ChannelFeatures(ChannelTypes.AnchorOutputsZeroFeeHtlcTx, Features[InitFeature](Wumbo -> Optional), Features[InitFeature](Wumbo -> Mandatory)).features == Set(StaticRemoteKey, AnchorOutputsZeroFeeHtlcTx, Wumbo)) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/CommitmentsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/CommitmentsSpec.scala index 1673db2ebe..d18261043f 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/CommitmentsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/CommitmentsSpec.scala @@ -385,7 +385,7 @@ class CommitmentsSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val c = CommitmentsSpec.makeCommitments(100000000 msat, 50000000 msat, FeeratePerKw(2500 sat), 546 sat, isInitiator) val (_, cmdAdd) = makeCmdAdd(c.availableBalanceForSend, randomKey().publicKey, f.currentBlockHeight) val Right((c1, _)) = sendAdd(c, cmdAdd, f.currentBlockHeight, feeConfNoMismatch) - assert(c1.availableBalanceForSend === 0.msat) + assert(c1.availableBalanceForSend == 0.msat) // We should be able to handle a fee increase. val Right((c2, _)) = sendFee(c1, CMD_UPDATE_FEE(FeeratePerKw(3000 sat)), feeConfNoMismatch) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/DustExposureSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/DustExposureSpec.scala index 0bb2fda155..c277653adc 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/DustExposureSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/DustExposureSpec.scala @@ -42,18 +42,18 @@ class DustExposureSpec extends AnyFunSuiteLike { OutgoingHtlc(createHtlc(3, 500.sat.toMilliSatoshi)), ) val spec = CommitmentSpec(htlcs, FeeratePerKw(FeeratePerByte(50 sat)), 50000 msat, 75000 msat) - assert(DustExposure.computeExposure(spec, 450 sat, Transactions.ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) === 898.sat.toMilliSatoshi) - assert(DustExposure.computeExposure(spec, 500 sat, Transactions.ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) === 2796.sat.toMilliSatoshi) - assert(DustExposure.computeExposure(spec, 500 sat, Transactions.UnsafeLegacyAnchorOutputsCommitmentFormat) === 3796.sat.toMilliSatoshi) + assert(DustExposure.computeExposure(spec, 450 sat, Transactions.ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) == 898.sat.toMilliSatoshi) + assert(DustExposure.computeExposure(spec, 500 sat, Transactions.ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) == 2796.sat.toMilliSatoshi) + assert(DustExposure.computeExposure(spec, 500 sat, Transactions.UnsafeLegacyAnchorOutputsCommitmentFormat) == 3796.sat.toMilliSatoshi) } { // Low feerate: buffer adds 10 sat/byte val dustLimit = 500.sat val feerate = FeeratePerKw(FeeratePerByte(10 sat)) - assert(Transactions.receivedHtlcTrimThreshold(dustLimit, feerate, Transactions.DefaultCommitmentFormat) === 2257.sat) - assert(Transactions.offeredHtlcTrimThreshold(dustLimit, feerate, Transactions.DefaultCommitmentFormat) === 2157.sat) - assert(Transactions.receivedHtlcTrimThreshold(dustLimit, feerate * 2, Transactions.DefaultCommitmentFormat) === 4015.sat) - assert(Transactions.offeredHtlcTrimThreshold(dustLimit, feerate * 2, Transactions.DefaultCommitmentFormat) === 3815.sat) + assert(Transactions.receivedHtlcTrimThreshold(dustLimit, feerate, Transactions.DefaultCommitmentFormat) == 2257.sat) + assert(Transactions.offeredHtlcTrimThreshold(dustLimit, feerate, Transactions.DefaultCommitmentFormat) == 2157.sat) + assert(Transactions.receivedHtlcTrimThreshold(dustLimit, feerate * 2, Transactions.DefaultCommitmentFormat) == 4015.sat) + assert(Transactions.offeredHtlcTrimThreshold(dustLimit, feerate * 2, Transactions.DefaultCommitmentFormat) == 3815.sat) val htlcs = Set[DirectedHtlc]( // Below the dust limit. IncomingHtlc(createHtlc(0, 450.sat.toMilliSatoshi)), @@ -70,8 +70,8 @@ class DustExposureSpec extends AnyFunSuiteLike { ) val spec = CommitmentSpec(htlcs, feerate, 50000 msat, 75000 msat) val expected = 450.sat + 450.sat + 2250.sat + 2150.sat + 4010.sat + 3810.sat - assert(DustExposure.computeExposure(spec, dustLimit, Transactions.DefaultCommitmentFormat) === expected.toMilliSatoshi) - assert(DustExposure.computeExposure(spec, feerate * 2, dustLimit, Transactions.DefaultCommitmentFormat) === DustExposure.computeExposure(spec, dustLimit, Transactions.DefaultCommitmentFormat)) + assert(DustExposure.computeExposure(spec, dustLimit, Transactions.DefaultCommitmentFormat) == expected.toMilliSatoshi) + assert(DustExposure.computeExposure(spec, feerate * 2, dustLimit, Transactions.DefaultCommitmentFormat) == DustExposure.computeExposure(spec, dustLimit, Transactions.DefaultCommitmentFormat)) assert(DustExposure.contributesToDustExposure(IncomingHtlc(createHtlc(4, 4010.sat.toMilliSatoshi)), spec, dustLimit, Transactions.DefaultCommitmentFormat)) assert(DustExposure.contributesToDustExposure(OutgoingHtlc(createHtlc(4, 3810.sat.toMilliSatoshi)), spec, dustLimit, Transactions.DefaultCommitmentFormat)) assert(!DustExposure.contributesToDustExposure(IncomingHtlc(createHtlc(5, 4020.sat.toMilliSatoshi)), spec, dustLimit, Transactions.DefaultCommitmentFormat)) @@ -81,10 +81,10 @@ class DustExposureSpec extends AnyFunSuiteLike { // High feerate: buffer adds 25% val dustLimit = 1000.sat val feerate = FeeratePerKw(FeeratePerByte(80 sat)) - assert(Transactions.receivedHtlcTrimThreshold(dustLimit, feerate, Transactions.UnsafeLegacyAnchorOutputsCommitmentFormat) === 15120.sat) - assert(Transactions.offeredHtlcTrimThreshold(dustLimit, feerate, Transactions.UnsafeLegacyAnchorOutputsCommitmentFormat) === 14320.sat) - assert(Transactions.receivedHtlcTrimThreshold(dustLimit, feerate * 1.25, Transactions.UnsafeLegacyAnchorOutputsCommitmentFormat) === 18650.sat) - assert(Transactions.offeredHtlcTrimThreshold(dustLimit, feerate * 1.25, Transactions.UnsafeLegacyAnchorOutputsCommitmentFormat) === 17650.sat) + assert(Transactions.receivedHtlcTrimThreshold(dustLimit, feerate, Transactions.UnsafeLegacyAnchorOutputsCommitmentFormat) == 15120.sat) + assert(Transactions.offeredHtlcTrimThreshold(dustLimit, feerate, Transactions.UnsafeLegacyAnchorOutputsCommitmentFormat) == 14320.sat) + assert(Transactions.receivedHtlcTrimThreshold(dustLimit, feerate * 1.25, Transactions.UnsafeLegacyAnchorOutputsCommitmentFormat) == 18650.sat) + assert(Transactions.offeredHtlcTrimThreshold(dustLimit, feerate * 1.25, Transactions.UnsafeLegacyAnchorOutputsCommitmentFormat) == 17650.sat) val htlcs = Set[DirectedHtlc]( // Below the dust limit. IncomingHtlc(createHtlc(0, 900.sat.toMilliSatoshi)), @@ -101,8 +101,8 @@ class DustExposureSpec extends AnyFunSuiteLike { ) val spec = CommitmentSpec(htlcs, feerate, 50000 msat, 75000 msat) val expected = 900.sat + 900.sat + 15000.sat + 14000.sat + 18000.sat + 17000.sat - assert(DustExposure.computeExposure(spec, dustLimit, Transactions.UnsafeLegacyAnchorOutputsCommitmentFormat) === expected.toMilliSatoshi) - assert(DustExposure.computeExposure(spec, feerate * 1.25, dustLimit, Transactions.DefaultCommitmentFormat) === DustExposure.computeExposure(spec, dustLimit, Transactions.DefaultCommitmentFormat)) + assert(DustExposure.computeExposure(spec, dustLimit, Transactions.UnsafeLegacyAnchorOutputsCommitmentFormat) == expected.toMilliSatoshi) + assert(DustExposure.computeExposure(spec, feerate * 1.25, dustLimit, Transactions.DefaultCommitmentFormat) == DustExposure.computeExposure(spec, dustLimit, Transactions.DefaultCommitmentFormat)) assert(DustExposure.contributesToDustExposure(IncomingHtlc(createHtlc(4, 18000.sat.toMilliSatoshi)), spec, dustLimit, Transactions.UnsafeLegacyAnchorOutputsCommitmentFormat)) assert(DustExposure.contributesToDustExposure(OutgoingHtlc(createHtlc(4, 17000.sat.toMilliSatoshi)), spec, dustLimit, Transactions.UnsafeLegacyAnchorOutputsCommitmentFormat)) assert(!DustExposure.contributesToDustExposure(IncomingHtlc(createHtlc(5, 19000.sat.toMilliSatoshi)), spec, dustLimit, Transactions.UnsafeLegacyAnchorOutputsCommitmentFormat)) @@ -113,7 +113,7 @@ class DustExposureSpec extends AnyFunSuiteLike { test("filter incoming htlcs before forwarding") { val dustLimit = 1000.sat val initialSpec = CommitmentSpec(Set.empty, FeeratePerKw(10000 sat), 0 msat, 0 msat) - assert(DustExposure.computeExposure(initialSpec, dustLimit, Transactions.DefaultCommitmentFormat) === 0.msat) + assert(DustExposure.computeExposure(initialSpec, dustLimit, Transactions.DefaultCommitmentFormat) == 0.msat) assert(DustExposure.contributesToDustExposure(IncomingHtlc(createHtlc(0, 9000.sat.toMilliSatoshi)), initialSpec, dustLimit, Transactions.DefaultCommitmentFormat)) assert(DustExposure.contributesToDustExposure(OutgoingHtlc(createHtlc(0, 9000.sat.toMilliSatoshi)), initialSpec, dustLimit, Transactions.DefaultCommitmentFormat)) // NB: HTLC-success transactions are bigger than HTLC-timeout transactions: that means incoming htlcs have a higher @@ -128,7 +128,7 @@ class DustExposureSpec extends AnyFunSuiteLike { OutgoingHtlc(createHtlc(3, 9500.sat.toMilliSatoshi)), IncomingHtlc(createHtlc(4, 9500.sat.toMilliSatoshi)), )) - assert(DustExposure.computeExposure(updatedSpec, dustLimit, Transactions.DefaultCommitmentFormat) === 18500.sat.toMilliSatoshi) + assert(DustExposure.computeExposure(updatedSpec, dustLimit, Transactions.DefaultCommitmentFormat) == 18500.sat.toMilliSatoshi) val receivedHtlcs = Seq( createHtlc(5, 9500.sat.toMilliSatoshi), @@ -139,8 +139,8 @@ class DustExposureSpec extends AnyFunSuiteLike { createHtlc(10, 50000.sat.toMilliSatoshi), ) val (accepted, rejected) = DustExposure.filterBeforeForward(25000 sat, updatedSpec, dustLimit, 10000.sat.toMilliSatoshi, initialSpec, dustLimit, 15000.sat.toMilliSatoshi, receivedHtlcs, Transactions.DefaultCommitmentFormat) - assert(accepted.map(_.id).toSet === Set(5, 6, 8, 10)) - assert(rejected.map(_.id).toSet === Set(7, 9)) + assert(accepted.map(_.id).toSet == Set(5, 6, 8, 10)) + assert(rejected.map(_.id).toSet == Set(7, 9)) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/FuzzySpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/FuzzySpec.scala index e4cefd5cd9..987b686a7c 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/FuzzySpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/FuzzySpec.scala @@ -28,7 +28,7 @@ import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._ import fr.acinq.eclair.channel.fsm.Channel import fr.acinq.eclair.channel.publish.TxPublisher import fr.acinq.eclair.channel.states.ChannelStateTestsBase -import fr.acinq.eclair.channel.states.ChannelStateTestsHelperMethods.FakeTxPublisherFactory +import fr.acinq.eclair.channel.states.ChannelStateTestsBase.FakeTxPublisherFactory import fr.acinq.eclair.payment.OutgoingPaymentPacket.Upstream import fr.acinq.eclair.payment._ import fr.acinq.eclair.payment.receive.MultiPartHandler.ReceivePayment diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/HelpersSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/HelpersSpec.scala index 38b1fb6fe6..47de53b8d3 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/HelpersSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/HelpersSpec.scala @@ -24,7 +24,7 @@ import fr.acinq.eclair.TestUtils.NoLoggingDiagnostics import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.WatchFundingSpentTriggered import fr.acinq.eclair.channel.Helpers.Closing import fr.acinq.eclair.channel.fsm.Channel -import fr.acinq.eclair.channel.states.{ChannelStateTestsHelperMethods, ChannelStateTestsTags} +import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsTags} import fr.acinq.eclair.transactions.Transactions._ import fr.acinq.eclair.wire.protocol.UpdateAddHtlc import fr.acinq.eclair.{BlockHeight, MilliSatoshiLong, TestKitBaseClass, TimestampSecond, TimestampSecondLong} @@ -35,7 +35,7 @@ import scodec.bits.HexStringSyntax import java.util.UUID import scala.concurrent.duration._ -class HelpersSpec extends TestKitBaseClass with AnyFunSuiteLike with ChannelStateTestsHelperMethods { +class HelpersSpec extends TestKitBaseClass with AnyFunSuiteLike with ChannelStateTestsBase { implicit val log: akka.event.LoggingAdapter = akka.event.NoLogging @@ -98,18 +98,18 @@ class HelpersSpec extends TestKitBaseClass with AnyFunSuiteLike with ChannelStat awaitCond(bob.stateName == CLOSING) val lcp = alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.get - assert(lcp.htlcTxs.size === 6) + assert(lcp.htlcTxs.size == 6) val htlcTimeoutTxs = getHtlcTimeoutTxs(lcp) - assert(htlcTimeoutTxs.length === 3) + assert(htlcTimeoutTxs.length == 3) val htlcSuccessTxs = getHtlcSuccessTxs(lcp) - assert(htlcSuccessTxs.length === 1) + assert(htlcSuccessTxs.length == 1) val rcp = bob.stateData.asInstanceOf[DATA_CLOSING].remoteCommitPublished.get - assert(rcp.claimHtlcTxs.size === 6) + assert(rcp.claimHtlcTxs.size == 6) val claimHtlcTimeoutTxs = getClaimHtlcTimeoutTxs(rcp) - assert(claimHtlcTimeoutTxs.length === 3) + assert(claimHtlcTimeoutTxs.length == 3) val claimHtlcSuccessTxs = getClaimHtlcSuccessTxs(rcp) - assert(claimHtlcSuccessTxs.length === 1) + assert(claimHtlcSuccessTxs.length == 1) Fixture(alice, lcp, Set(htlca1a, htlca1b, htlca2), bob, rcp, Set(htlcb1a, htlcb1b, htlcb2), probe) } @@ -197,24 +197,24 @@ class HelpersSpec extends TestKitBaseClass with AnyFunSuiteLike with ChannelStat val aliceTimedOutHtlcs = htlcTimeoutTxs.map(htlcTimeout => { val timedOutHtlcs = Closing.trimmedOrTimedOutHtlcs(commitmentFormat, localCommit, localCommitPublished, dustLimit, htlcTimeout.tx) - assert(timedOutHtlcs.size === 1) + assert(timedOutHtlcs.size == 1) timedOutHtlcs.head }) - assert(aliceTimedOutHtlcs.toSet === aliceHtlcs) + assert(aliceTimedOutHtlcs.toSet == aliceHtlcs) val bobTimedOutHtlcs = claimHtlcTimeoutTxs.map(claimHtlcTimeout => { val timedOutHtlcs = Closing.trimmedOrTimedOutHtlcs(commitmentFormat, remoteCommit, remoteCommitPublished, dustLimit, claimHtlcTimeout.tx) - assert(timedOutHtlcs.size === 1) + assert(timedOutHtlcs.size == 1) timedOutHtlcs.head }) - assert(bobTimedOutHtlcs.toSet === bobHtlcs) + assert(bobTimedOutHtlcs.toSet == bobHtlcs) val bobTimedOutHtlcs2 = claimHtlcTimeoutTxsModifiedFees.map(claimHtlcTimeout => { val timedOutHtlcs = Closing.trimmedOrTimedOutHtlcs(commitmentFormat, remoteCommit, remoteCommitPublished, dustLimit, claimHtlcTimeout.tx) - assert(timedOutHtlcs.size === 1) + assert(timedOutHtlcs.size == 1) timedOutHtlcs.head }) - assert(bobTimedOutHtlcs2.toSet === bobHtlcs) + assert(bobTimedOutHtlcs2.toSet == bobHtlcs) htlcSuccessTxs.foreach(htlcSuccess => assert(Closing.trimmedOrTimedOutHtlcs(commitmentFormat, localCommit, localCommitPublished, dustLimit, htlcSuccess.tx).isEmpty)) htlcSuccessTxs.foreach(htlcSuccess => assert(Closing.trimmedOrTimedOutHtlcs(commitmentFormat, remoteCommit, remoteCommitPublished, dustLimit, htlcSuccess.tx).isEmpty)) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/RegisterSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/RegisterSpec.scala index c274b1a14e..c6a9b6aaac 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/RegisterSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/RegisterSpec.scala @@ -19,6 +19,6 @@ class RegisterSpec extends TestKitBaseClass with AnyFunSuiteLike with ParallelTe val customRestoredEvent = CustomChannelRestored(TestProbe().ref, randomBytes32(), TestProbe().ref, randomKey().publicKey) registerRef ! customRestoredEvent sender.send(registerRef, Symbol("channels")) - sender.expectMsgType[Map[ByteVector32, ActorRef]] === Map(customRestoredEvent.channelId -> customRestoredEvent.channel) + sender.expectMsgType[Map[ByteVector32, ActorRef]] == Map(customRestoredEvent.channelId -> customRestoredEvent.channel) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/RestoreSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/RestoreSpec.scala index 08cc58de8f..687cf9a917 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/RestoreSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/RestoreSpec.scala @@ -13,7 +13,7 @@ import fr.acinq.eclair.TestConstants.{Alice, Bob} import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.WatchFundingSpentTriggered import fr.acinq.eclair.channel.fsm.Channel import fr.acinq.eclair.channel.states.ChannelStateTestsBase -import fr.acinq.eclair.channel.states.ChannelStateTestsHelperMethods.FakeTxPublisherFactory +import fr.acinq.eclair.channel.states.ChannelStateTestsBase.FakeTxPublisherFactory import fr.acinq.eclair.crypto.Generators import fr.acinq.eclair.crypto.keymanager.ChannelKeyManager import fr.acinq.eclair.router.Announcements @@ -85,7 +85,7 @@ class RestoreSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Chan bob2alice.forward(newAlice) // ... and ask bob to publish its current commitment val error = alice2bob.expectMsgType[Error] - assert(new String(error.data.toArray) === PleasePublishYourCommitment(channelId(newAlice)).getMessage) + assert(new String(error.data.toArray) == PleasePublishYourCommitment(channelId(newAlice)).getMessage) // alice now waits for bob to publish its commitment awaitCond(newAlice.stateName == WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT) @@ -224,9 +224,9 @@ class RestoreSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Chan val u1 = channelUpdateListener.expectMsgType[ChannelUpdateParametersChanged] assert(!Announcements.areSameIgnoreFlags(u1.channelUpdate, oldStateData.channelUpdate)) - assert(u1.channelUpdate.feeBaseMsat === newConfig.relayParams.privateChannelFees.feeBase) - assert(u1.channelUpdate.feeProportionalMillionths === newConfig.relayParams.privateChannelFees.feeProportionalMillionths) - assert(u1.channelUpdate.cltvExpiryDelta === newConfig.channelConf.expiryDelta) + assert(u1.channelUpdate.feeBaseMsat == newConfig.relayParams.privateChannelFees.feeBase) + assert(u1.channelUpdate.feeProportionalMillionths == newConfig.relayParams.privateChannelFees.feeProportionalMillionths) + assert(u1.channelUpdate.cltvExpiryDelta == newConfig.channelConf.expiryDelta) newAlice ! INPUT_RECONNECTED(alice2bob.ref, aliceInit, bobInit) bob ! INPUT_RECONNECTED(bob2alice.ref, bobInit, aliceInit) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/FinalTxPublisherSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/FinalTxPublisherSpec.scala index a605788918..fb4df104f1 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/FinalTxPublisherSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/FinalTxPublisherSpec.scala @@ -83,8 +83,8 @@ class FinalTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike with Bi publisher ! Publish(probe.ref, cmd) val w = watcher.expectMsgType[WatchParentTxConfirmed] - assert(w.txId === parentTx.txid) - assert(w.minDepth === 5) + assert(w.txId == parentTx.txid) + assert(w.minDepth == 5) createBlocks(5, probe) w.replyTo ! WatchParentTxConfirmedTriggered(currentBlockHeight(probe), 0, parentTx) @@ -143,8 +143,8 @@ class FinalTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike with Bi createBlocks(1, probe) val response = probe.expectMsgType[TxRejected] - assert(response.cmd === cmd) - assert(response.reason === ConflictingTxConfirmed) + assert(response.cmd == cmd) + assert(response.reason == ConflictingTxConfirmed) // The actor should stop when requested: probe.watch(publisher.toClassic) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/MempoolTxMonitorSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/MempoolTxMonitorSpec.scala index 168b00f185..e197f5bc92 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/MempoolTxMonitorSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/MempoolTxMonitorSpec.scala @@ -267,9 +267,9 @@ class MempoolTxMonitorSpec extends TestKitBaseClass with AnyFunSuiteLike with Bi monitor ! Publish(probe.ref, tx, tx.txIn.head.outPoint, "test-tx", 15 sat) waitTxInMempool(bitcoinClient, tx.txid, probe) val txPublished = eventListener.expectMsgType[TransactionPublished] - assert(txPublished.tx === tx) - assert(txPublished.miningFee === 15.sat) - assert(txPublished.desc === "test-tx") + assert(txPublished.tx == tx) + assert(txPublished.miningFee == 15.sat) + assert(txPublished.desc == "test-tx") generateBlocks(TestConstants.Alice.nodeParams.channelConf.minDepthBlocks) system.eventStream.publish(CurrentBlockHeight(currentBlockHeight())) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/ReplaceableTxFunderSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/ReplaceableTxFunderSpec.scala index 2315b2924f..16aa7226a0 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/ReplaceableTxFunderSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/ReplaceableTxFunderSpec.scala @@ -68,8 +68,8 @@ class ReplaceableTxFunderSpec extends TestKitBaseClass with AnyFunSuiteLike { txOut = TxOut(amountOut, Script.pay2wpkh(PlaceHolderPubKey)) :: Nil, ))) val adjustedTx = adjustAnchorOutputChange(unsignedTx, commitTx.tx, amountIn, commitFeerate, targetFeerate, dustLimit) - assert(adjustedTx.txInfo.tx.txIn.size === unsignedTx.txInfo.tx.txIn.size) - assert(adjustedTx.txInfo.tx.txOut.size === 1) + assert(adjustedTx.txInfo.tx.txIn.size == unsignedTx.txInfo.tx.txIn.size) + assert(adjustedTx.txInfo.tx.txOut.size == 1) assert(adjustedTx.txInfo.tx.txOut.head.amount >= dustLimit) if (adjustedTx.txInfo.tx.txOut.head.amount > dustLimit) { // Simulate tx signing to check final feerate. @@ -133,8 +133,8 @@ class ReplaceableTxFunderSpec extends TestKitBaseClass with AnyFunSuiteLike { for (unsignedTx <- Seq(unsignedHtlcSuccessTx, unsignedHtlcTimeoutTx)) { val totalAmountIn = unsignedTx.txInfo.input.txOut.amount + walletAmountIn val adjustedTx = adjustHtlcTxChange(unsignedTx, totalAmountIn, targetFeerate, dustLimit, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) - assert(adjustedTx.txInfo.tx.txIn.size === unsignedTx.txInfo.tx.txIn.size) - assert(adjustedTx.txInfo.tx.txOut.size === 1 || adjustedTx.txInfo.tx.txOut.size === 2) + assert(adjustedTx.txInfo.tx.txIn.size == unsignedTx.txInfo.tx.txIn.size) + assert(adjustedTx.txInfo.tx.txOut.size == 1 || adjustedTx.txInfo.tx.txOut.size == 2) if (adjustedTx.txInfo.tx.txOut.size == 2) { // Simulate tx signing to check final feerate. val signedTx = { @@ -184,8 +184,8 @@ class ReplaceableTxFunderSpec extends TestKitBaseClass with AnyFunSuiteLike { adjustClaimHtlcTxOutput(claimHtlc, targetFeerate, dustLimit) match { case Left(_) => assert(targetFeerate >= FeeratePerKw(7000 sat)) case Right(updatedClaimHtlc) => - assert(updatedClaimHtlc.txInfo.tx.txIn.length === 1) - assert(updatedClaimHtlc.txInfo.tx.txOut.length === 1) + assert(updatedClaimHtlc.txInfo.tx.txIn.length == 1) + assert(updatedClaimHtlc.txInfo.tx.txOut.length == 1) assert(updatedClaimHtlc.txInfo.tx.txOut.head.amount < previousAmount) previousAmount = updatedClaimHtlc.txInfo.tx.txOut.head.amount val signedTx = updatedClaimHtlc match { @@ -223,16 +223,16 @@ class ReplaceableTxFunderSpec extends TestKitBaseClass with AnyFunSuiteLike { // We can handle a small feerate update by lowering the change output. val TxOutputAdjusted(feerateUpdate1) = adjustPreviousTxOutput(FundedTx(previousAnchorTx, 12000 sat, FeeratePerKw(2500 sat)), FeeratePerKw(5000 sat), commitments) - assert(feerateUpdate1.txInfo.tx.txIn === previousAnchorTx.txInfo.tx.txIn) - assert(feerateUpdate1.txInfo.tx.txOut.length === 1) + assert(feerateUpdate1.txInfo.tx.txIn == previousAnchorTx.txInfo.tx.txIn) + assert(feerateUpdate1.txInfo.tx.txOut.length == 1) val TxOutputAdjusted(feerateUpdate2) = adjustPreviousTxOutput(FundedTx(previousAnchorTx, 12000 sat, FeeratePerKw(2500 sat)), FeeratePerKw(6000 sat), commitments) - assert(feerateUpdate2.txInfo.tx.txIn === previousAnchorTx.txInfo.tx.txIn) - assert(feerateUpdate2.txInfo.tx.txOut.length === 1) + assert(feerateUpdate2.txInfo.tx.txIn == previousAnchorTx.txInfo.tx.txIn) + assert(feerateUpdate2.txInfo.tx.txOut.length == 1) assert(feerateUpdate2.txInfo.tx.txOut.head.amount < feerateUpdate1.txInfo.tx.txOut.head.amount) // But if the feerate increase is too large, we must add new wallet inputs. val AddWalletInputs(previousTx) = adjustPreviousTxOutput(FundedTx(previousAnchorTx, 12000 sat, FeeratePerKw(2500 sat)), FeeratePerKw(10000 sat), commitments) - assert(previousTx === previousAnchorTx) + assert(previousTx == previousAnchorTx) } test("adjust previous htlc transaction outputs", Tag("fuzzy")) { @@ -259,19 +259,19 @@ class ReplaceableTxFunderSpec extends TestKitBaseClass with AnyFunSuiteLike { // We can handle a small feerate update by lowering the change output. val TxOutputAdjusted(feerateUpdate1) = adjustPreviousTxOutput(FundedTx(previousTx, 15000 sat, FeeratePerKw(2500 sat)), FeeratePerKw(5000 sat), commitments) - assert(feerateUpdate1.txInfo.tx.txIn === previousTx.txInfo.tx.txIn) - assert(feerateUpdate1.txInfo.tx.txOut.length === 2) - assert(feerateUpdate1.txInfo.tx.txOut.head === previousTx.txInfo.tx.txOut.head) + assert(feerateUpdate1.txInfo.tx.txIn == previousTx.txInfo.tx.txIn) + assert(feerateUpdate1.txInfo.tx.txOut.length == 2) + assert(feerateUpdate1.txInfo.tx.txOut.head == previousTx.txInfo.tx.txOut.head) val TxOutputAdjusted(feerateUpdate2) = adjustPreviousTxOutput(FundedTx(previousTx, 15000 sat, FeeratePerKw(2500 sat)), FeeratePerKw(6000 sat), commitments) - assert(feerateUpdate2.txInfo.tx.txIn === previousTx.txInfo.tx.txIn) - assert(feerateUpdate2.txInfo.tx.txOut.length === 2) - assert(feerateUpdate2.txInfo.tx.txOut.head === previousTx.txInfo.tx.txOut.head) + assert(feerateUpdate2.txInfo.tx.txIn == previousTx.txInfo.tx.txIn) + assert(feerateUpdate2.txInfo.tx.txOut.length == 2) + assert(feerateUpdate2.txInfo.tx.txOut.head == previousTx.txInfo.tx.txOut.head) assert(feerateUpdate2.txInfo.tx.txOut.last.amount < feerateUpdate1.txInfo.tx.txOut.last.amount) // If the previous funding attempt didn't add a change output, we must add new wallet inputs. val previousTxNoChange = previousTx.updateTx(previousTx.txInfo.tx.copy(txOut = Seq(previousTx.txInfo.tx.txOut.head))) val AddWalletInputs(tx) = adjustPreviousTxOutput(FundedTx(previousTxNoChange, 25000 sat, FeeratePerKw(2500 sat)), FeeratePerKw(5000 sat), commitments) - assert(tx === previousTxNoChange) + assert(tx == previousTxNoChange) for (_ <- 1 to 100) { val amountIn = Random.nextInt(25_000_000).sat @@ -283,11 +283,11 @@ class ReplaceableTxFunderSpec extends TestKitBaseClass with AnyFunSuiteLike { val targetFeerate = FeeratePerKw(2500 sat) + FeeratePerKw(Random.nextInt(20000).sat) adjustPreviousTxOutput(FundedTx(fuzzyPreviousTx, amountIn, FeeratePerKw(2500 sat)), targetFeerate, commitments) match { case AdjustPreviousTxOutputResult.Skip(_) => // nothing do check - case AddWalletInputs(tx) => assert(tx === fuzzyPreviousTx) + case AddWalletInputs(tx) => assert(tx == fuzzyPreviousTx) case TxOutputAdjusted(updatedTx) => - assert(updatedTx.txInfo.tx.txIn === fuzzyPreviousTx.txInfo.tx.txIn) + assert(updatedTx.txInfo.tx.txIn == fuzzyPreviousTx.txInfo.tx.txIn) assert(Set(1, 2).contains(updatedTx.txInfo.tx.txOut.length)) - assert(updatedTx.txInfo.tx.txOut.head === fuzzyPreviousTx.txInfo.tx.txOut.head) + assert(updatedTx.txInfo.tx.txOut.head == fuzzyPreviousTx.txInfo.tx.txOut.head) assert(updatedTx.txInfo.tx.txOut.last.amount >= 600.sat) } } @@ -308,8 +308,8 @@ class ReplaceableTxFunderSpec extends TestKitBaseClass with AnyFunSuiteLike { case AdjustPreviousTxOutputResult.Skip(_) => assert(targetFeerate >= FeeratePerKw(10000 sat)) case AddWalletInputs(_) => fail("shouldn't add wallet inputs to claim-htlc-tx") case TxOutputAdjusted(updatedTx) => - assert(updatedTx.txInfo.tx.txIn === claimHtlc.txInfo.tx.txIn) - assert(updatedTx.txInfo.tx.txOut.length === 1) + assert(updatedTx.txInfo.tx.txIn == claimHtlc.txInfo.tx.txIn) + assert(updatedTx.txInfo.tx.txOut.length == 1) assert(updatedTx.txInfo.tx.txOut.head.amount < previousAmount) previousAmount = updatedTx.txInfo.tx.txOut.head.amount } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPublisherSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPublisherSpec.scala index 42f45797e6..3d136c5ae7 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPublisherSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPublisherSpec.scala @@ -34,7 +34,7 @@ import fr.acinq.eclair.channel.fsm.Channel import fr.acinq.eclair.channel.publish.ReplaceableTxPublisher.{Publish, Stop, UpdateConfirmationTarget} import fr.acinq.eclair.channel.publish.TxPublisher.TxRejectedReason._ import fr.acinq.eclair.channel.publish.TxPublisher._ -import fr.acinq.eclair.channel.states.{ChannelStateTestsHelperMethods, ChannelStateTestsTags} +import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsTags} import fr.acinq.eclair.transactions.Transactions import fr.acinq.eclair.transactions.Transactions._ import fr.acinq.eclair.{BlockHeight, MilliSatoshiLong, NodeParams, NotificationsLogger, TestConstants, TestFeeEstimator, TestKitBaseClass, randomBytes32, randomKey} @@ -46,7 +46,7 @@ import java.util.concurrent.atomic.AtomicLong import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.duration.DurationInt -class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike with BitcoindService with ChannelStateTestsHelperMethods with BeforeAndAfterAll { +class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike with BitcoindService with ChannelStateTestsBase with BeforeAndAfterAll { override def beforeAll(): Unit = { startBitcoind() @@ -160,7 +160,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w val publishCommitTx = alice2blockchain.expectMsg(PublishFinalTx(commitTx, commitTx.fee, None)) // Forward the anchor tx to the publisher. val publishAnchor = alice2blockchain.expectMsgType[PublishReplaceableTx] - assert(publishAnchor.txInfo.input.outPoint.txid === commitTx.tx.txid) + assert(publishAnchor.txInfo.input.outPoint.txid == commitTx.tx.txid) assert(publishAnchor.txInfo.isInstanceOf[ClaimLocalAnchorOutputTx]) val anchorTx = publishAnchor.txInfo.asInstanceOf[ClaimLocalAnchorOutputTx].copy(confirmBefore = overrideCommitTarget) @@ -177,8 +177,8 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w publisher ! Publish(probe.ref, anchorTx) val result = probe.expectMsgType[TxRejected] - assert(result.cmd === anchorTx) - assert(result.reason === TxSkipped(retryNextBlock = true)) + assert(result.cmd == anchorTx) + assert(result.reason == TxSkipped(retryNextBlock = true)) } } @@ -194,8 +194,8 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w setFeerate(FeeratePerKw(10_000 sat)) publisher ! Publish(probe.ref, anchorTx) val result = probe.expectMsgType[TxRejected] - assert(result.cmd === anchorTx) - assert(result.reason === TxSkipped(retryNextBlock = false)) + assert(result.cmd == anchorTx) + assert(result.reason == TxSkipped(retryNextBlock = false)) } } @@ -212,8 +212,8 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w publisher ! Publish(probe.ref, anchorTx) val result = probe.expectMsgType[TxRejected] - assert(result.cmd === anchorTx) - assert(result.reason === TxSkipped(retryNextBlock = false)) + assert(result.cmd == anchorTx) + assert(result.reason == TxSkipped(retryNextBlock = false)) } } @@ -230,8 +230,8 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w setFeerate(FeeratePerKw(10_000 sat)) publisher ! Publish(probe.ref, anchorTx) val result = probe.expectMsgType[TxRejected] - assert(result.cmd === anchorTx) - assert(result.reason === TxSkipped(retryNextBlock = false)) + assert(result.cmd == anchorTx) + assert(result.reason == TxSkipped(retryNextBlock = false)) } } @@ -247,10 +247,10 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w setFeerate(FeeratePerKw(10_000 sat)) publisher ! Publish(probe.ref, anchorTx) val result = probe.expectMsgType[TxRejected] - assert(result.cmd === anchorTx) + assert(result.cmd == anchorTx) // When the remote commit tx is still unconfirmed, we want to retry in case it is evicted from the mempool and our // commit is then published. - assert(result.reason === TxSkipped(retryNextBlock = true)) + assert(result.reason == TxSkipped(retryNextBlock = true)) } } @@ -259,7 +259,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w import f._ val remoteCommit = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.fullySignedLocalCommitTx(bob.underlyingActor.nodeParams.channelKeyManager) - assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.spec.commitTxFeerate === FeeratePerKw(2500 sat)) + assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.spec.commitTxFeerate == FeeratePerKw(2500 sat)) // We lower the feerate to make it easy to replace our commit tx by theirs in the mempool. val lowFeerate = FeeratePerKw(500 sat) @@ -278,15 +278,15 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w system.eventStream.publish(CurrentBlockHeight(currentBlockHeight(probe))) val result = probe.expectMsgType[TxRejected] - assert(result.cmd === anchorTx) - assert(result.reason === InputGone) + assert(result.cmd == anchorTx) + assert(result.reason == InputGone) // Since our wallet input is gone, we will retry and discover that a commit tx has been confirmed. val publisher2 = createPublisher() publisher2 ! Publish(probe.ref, anchorTx) val result2 = probe.expectMsgType[TxRejected] - assert(result2.cmd === anchorTx) - assert(result2.reason === TxSkipped(retryNextBlock = false)) + assert(result2.cmd == anchorTx) + assert(result2.reason == TxSkipped(retryNextBlock = false)) } } @@ -300,9 +300,9 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w val cmd = anchorTx.copy(commitments = anchorTx.commitments.copy(commitInput = InputInfo(OutPoint(randomBytes32(), 1), TxOut(0 sat, Nil), Nil))) publisher ! Publish(probe.ref, cmd) val result = probe.expectMsgType[TxRejected] - assert(result.cmd === cmd) + assert(result.cmd == cmd) // We should keep retrying until the funding transaction is available. - assert(result.reason === TxSkipped(retryNextBlock = true)) + assert(result.reason == TxSkipped(retryNextBlock = true)) } } @@ -318,10 +318,10 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w setFeerate(FeeratePerKw(25_000 sat)) publisher ! Publish(probe.ref, anchorTx) val result = probe.expectMsgType[TxRejected] - assert(result.cmd === anchorTx) + assert(result.cmd == anchorTx) // When the remote commit tx is still unconfirmed, we want to retry in case it is evicted from the mempool and our // commit is then published. - assert(result.reason === CouldNotFund) + assert(result.reason == CouldNotFund) } } @@ -332,7 +332,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w val (commitTx, anchorTx) = closeChannelWithoutHtlcs(f, aliceBlockHeight() + 30) wallet.publishTransaction(commitTx.tx).pipeTo(probe.ref) probe.expectMsg(commitTx.tx.txid) - assert(getMempool().length === 1) + assert(getMempool().length == 1) val targetFeerate = FeeratePerKw(3000 sat) // NB: we try to get transactions confirmed *before* their confirmation target, so we aim for a more aggressive block target what's provided. @@ -349,7 +349,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w generateBlocks(5) system.eventStream.publish(CurrentBlockHeight(currentBlockHeight(probe))) val result = probe.expectMsgType[TxConfirmed] - assert(result.cmd === anchorTx) + assert(result.cmd == anchorTx) assert(result.tx.txIn.map(_.outPoint.txid).contains(commitTx.tx.txid)) assert(mempoolTxs.map(_.txid).contains(result.tx.txid)) } @@ -377,7 +377,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w generateBlocks(5) system.eventStream.publish(CurrentBlockHeight(currentBlockHeight(probe))) val result = probe.expectMsgType[TxConfirmed] - assert(result.cmd === anchorTx) + assert(result.cmd == anchorTx) assert(result.tx.txIn.map(_.outPoint.txid).contains(commitTx.tx.txid)) assert(mempoolTxs.map(_.txid).contains(result.tx.txid)) } @@ -412,7 +412,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w generateBlocks(5) system.eventStream.publish(CurrentBlockHeight(currentBlockHeight(probe))) val result = probe.expectMsgType[TxConfirmed] - assert(result.cmd === anchorTx) + assert(result.cmd == anchorTx) assert(result.tx.txIn.map(_.outPoint.txid).contains(commitTx.tx.txid)) assert(result.tx.txIn.length > 2) // we added more than 1 wallet input assert(mempoolTxs.map(_.txid).contains(result.tx.txid)) @@ -438,7 +438,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w system.eventStream.publish(CurrentBlockHeight(aliceBlockHeight() + 5)) probe.expectNoMessage(500 millis) val mempoolTxs2 = getMempool() - assert(mempoolTxs.map(_.txid).toSet === mempoolTxs2.map(_.txid).toSet) + assert(mempoolTxs.map(_.txid).toSet == mempoolTxs2.map(_.txid).toSet) } } @@ -461,7 +461,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w val mempoolTxs1 = getMempoolTxs(2) assert(mempoolTxs1.map(_.txid).contains(commitTx.tx.txid)) val mempoolAnchorTx1 = mempoolTxs1.filter(_.txid != commitTx.tx.txid).head - assert(mempoolAnchorTx1.txid === anchorTxId1) + assert(mempoolAnchorTx1.txid == anchorTxId1) // A new block is found, and the feerate has increased for our block target, so we bump the fees. val newFeerate = FeeratePerKw(5000 sat) @@ -471,7 +471,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w assert(!isInMempool(mempoolAnchorTx1.txid)) val mempoolTxs2 = getMempoolTxs(2) val mempoolAnchorTx2 = mempoolTxs2.filter(_.txid != commitTx.tx.txid).head - assert(mempoolAnchorTx2.txid === anchorTxId2) + assert(mempoolAnchorTx2.txid == anchorTxId2) assert(mempoolAnchorTx1.fees < mempoolAnchorTx2.fees) val targetFee = Transactions.weight2fee(newFeerate, mempoolTxs2.map(_.weight).sum.toInt) @@ -501,14 +501,14 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w val mempoolTxs1 = getMempoolTxs(2) assert(mempoolTxs1.map(_.txid).contains(commitTx.tx.txid)) val anchorTx1 = getMempool().filter(_.txid != commitTx.tx.txid).head - assert(anchorTx1.txid === anchorTxId1) + assert(anchorTx1.txid == anchorTxId1) // A new block is found, and the feerate has increased for our block target, so we bump the fees. system.eventStream.publish(CurrentBlockHeight(aliceBlockHeight() + 15)) val anchorTxId2 = listener.expectMsgType[TransactionPublished].tx.txid assert(!isInMempool(anchorTx1.txid)) val anchorTx2 = getMempool().filter(_.txid != commitTx.tx.txid).head - assert(anchorTx2.txid === anchorTxId2) + assert(anchorTx2.txid == anchorTxId2) // We used different inputs to be able to bump to the desired feerate. assert(anchorTx1.txIn.map(_.outPoint).toSet != anchorTx2.txIn.map(_.outPoint).toSet) @@ -549,7 +549,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w case _ => false } val mempoolTxs2 = getMempool() - assert(mempoolTxs1.map(_.txid).toSet === mempoolTxs2.map(_.txid).toSet) + assert(mempoolTxs1.map(_.txid).toSet == mempoolTxs2.map(_.txid).toSet) } } @@ -580,7 +580,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w system.eventStream.publish(CurrentBlockHeight(aliceBlockHeight() + 15)) probe.expectMsgType[NotifyNodeOperator] val mempoolTxs2 = getMempool() - assert(mempoolTxs1.map(_.txid).toSet + walletTx.txid === mempoolTxs2.map(_.txid).toSet) + assert(mempoolTxs1.map(_.txid).toSet + walletTx.txid == mempoolTxs2.map(_.txid).toSet) } } @@ -605,7 +605,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w val mempoolTxs1 = getMempoolTxs(2) assert(mempoolTxs1.map(_.txid).contains(commitTx.tx.txid)) val mempoolAnchorTx1 = mempoolTxs1.filter(_.txid != commitTx.tx.txid).head - assert(mempoolAnchorTx1.txid === anchorTxId1) + assert(mempoolAnchorTx1.txid == anchorTxId1) val targetFee1 = Transactions.weight2fee(feerateLow, mempoolTxs1.map(_.weight).sum.toInt) val actualFee1 = mempoolTxs1.map(_.fees).sum assert(targetFee1 * 0.9 <= actualFee1 && actualFee1 <= targetFee1 * 1.1, s"actualFee=$actualFee1 targetFee=$targetFee1") @@ -618,7 +618,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w awaitCond(!isInMempool(mempoolAnchorTx1.txid), interval = 200 millis, max = 30 seconds) val mempoolTxs2 = getMempoolTxs(2) val mempoolAnchorTx2 = mempoolTxs2.filter(_.txid != commitTx.tx.txid).head - assert(mempoolAnchorTx2.txid === anchorTxId2) + assert(mempoolAnchorTx2.txid == anchorTxId2) assert(mempoolAnchorTx1.fees < mempoolAnchorTx2.fees) val targetFee2 = Transactions.weight2fee(feerateHigh, mempoolTxs2.map(_.weight).sum.toInt) @@ -645,7 +645,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w val publisher2 = createPublisher() publisher2 ! Publish(probe.ref, anchorTx) val result = probe.expectMsgType[TxRejected] - assert(result.reason === ConflictingTxUnconfirmed) + assert(result.reason == ConflictingTxUnconfirmed) getMempoolTxs(2) // the previous anchor tx and the commit tx are still in the mempool // our parent will stop us when receiving the TxRejected message. @@ -655,7 +655,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w // the first publishing attempt succeeds generateBlocks(5) system.eventStream.publish(CurrentBlockHeight(currentBlockHeight(probe))) - assert(probe.expectMsgType[TxConfirmed].cmd === anchorTx) + assert(probe.expectMsgType[TxConfirmed].cmd == anchorTx) } } @@ -712,15 +712,15 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w val htlcSuccessPublisher = createPublisher() htlcSuccessPublisher ! Publish(probe.ref, htlcSuccess) val result1 = probe.expectMsgType[TxRejected] - assert(result1.cmd === htlcSuccess) - assert(result1.reason === ConflictingTxConfirmed) + assert(result1.cmd == htlcSuccess) + assert(result1.reason == ConflictingTxConfirmed) htlcSuccessPublisher ! Stop val htlcTimeoutPublisher = createPublisher() htlcTimeoutPublisher ! Publish(probe.ref, htlcTimeout) val result2 = probe.expectMsgType[TxRejected] - assert(result2.cmd === htlcTimeout) - assert(result2.reason === ConflictingTxConfirmed) + assert(result2.cmd == htlcTimeout) + assert(result2.reason == ConflictingTxConfirmed) htlcTimeoutPublisher ! Stop } } @@ -738,7 +738,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w // Force-close channel and verify txs sent to watcher. val commitTx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.fullySignedLocalCommitTx(alice.underlyingActor.nodeParams.channelKeyManager) - assert(commitTx.tx.txOut.size === 6) + assert(commitTx.tx.txOut.size == 6) probe.send(alice, CMD_FORCECLOSE(probe.ref)) probe.expectMsgType[CommandSuccess[CMD_FORCECLOSE]] @@ -779,8 +779,8 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w w.replyTo ! WatchParentTxConfirmedTriggered(currentBlockHeight(probe), 0, commitTx) val result = probe.expectMsgType[TxRejected] - assert(result.cmd === htlcSuccess) - assert(result.reason === CouldNotFund) + assert(result.cmd == htlcSuccess) + assert(result.reason == CouldNotFund) htlcSuccessPublisher ! Stop } } @@ -799,7 +799,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w generateBlocks(4) system.eventStream.publish(CurrentBlockHeight(currentBlockHeight(probe))) val htlcSuccessResult = probe.expectMsgType[TxConfirmed] - assert(htlcSuccessResult.cmd === htlcSuccess) + assert(htlcSuccessResult.cmd == htlcSuccess) assert(htlcSuccessResult.tx.txIn.map(_.outPoint.txid).contains(commitTx.txid)) htlcSuccessPublisher ! Stop htlcSuccessResult.tx @@ -828,7 +828,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w generateBlocks(4) system.eventStream.publish(CurrentBlockHeight(currentBlockHeight(probe))) val htlcTimeoutResult = probe.expectMsgType[TxConfirmed] - assert(htlcTimeoutResult.cmd === htlcTimeout) + assert(htlcTimeoutResult.cmd == htlcTimeout) assert(htlcTimeoutResult.tx.txIn.map(_.outPoint.txid).contains(commitTx.txid)) htlcTimeoutPublisher ! Stop htlcTimeoutResult.tx @@ -843,10 +843,10 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w setFeerate(currentFeerate) val htlcSuccessTx = testPublishHtlcSuccess(f, commitTx, htlcSuccess, currentFeerate) assert(htlcSuccess.txInfo.fee > 0.sat) - assert(htlcSuccessTx.txIn.length === 1) + assert(htlcSuccessTx.txIn.length == 1) val htlcTimeoutTx = testPublishHtlcTimeout(f, commitTx, htlcTimeout, currentFeerate) assert(htlcTimeout.txInfo.fee > 0.sat) - assert(htlcTimeoutTx.txIn.length === 1) + assert(htlcTimeoutTx.txIn.length == 1) } } @@ -873,10 +873,10 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w val (commitTx, htlcSuccess, htlcTimeout) = closeChannelWithHtlcs(f, aliceBlockHeight() + 30) // NB: we try to get transactions confirmed *before* their confirmation target, so we aim for a more aggressive block target than what's provided. setFeerate(targetFeerate, blockTarget = 12) - assert(htlcSuccess.txInfo.fee === 0.sat) + assert(htlcSuccess.txInfo.fee == 0.sat) val htlcSuccessTx = testPublishHtlcSuccess(f, commitTx, htlcSuccess, targetFeerate) assert(htlcSuccessTx.txIn.length > 1) - assert(htlcTimeout.txInfo.fee === 0.sat) + assert(htlcTimeout.txInfo.fee == 0.sat) val htlcTimeoutTx = testPublishHtlcTimeout(f, commitTx, htlcTimeout, targetFeerate) assert(htlcTimeoutTx.txIn.length > 1) } @@ -890,10 +890,10 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w val targetFeerate = commitFeerate / 2 val (commitTx, htlcSuccess, htlcTimeout) = closeChannelWithHtlcs(f, aliceBlockHeight() + 30) setFeerate(targetFeerate) - assert(htlcSuccess.txInfo.fee === 0.sat) + assert(htlcSuccess.txInfo.fee == 0.sat) val htlcSuccessTx = testPublishHtlcSuccess(f, commitTx, htlcSuccess, targetFeerate) assert(htlcSuccessTx.txIn.length > 1) - assert(htlcTimeout.txInfo.fee === 0.sat) + assert(htlcTimeout.txInfo.fee == 0.sat) val htlcTimeoutTx = testPublishHtlcTimeout(f, commitTx, htlcTimeout, targetFeerate) assert(htlcTimeoutTx.txIn.length > 1) } @@ -947,7 +947,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w val htlcSuccessTxId1 = listener.expectMsgType[TransactionPublished].tx.txid val htlcSuccessTx1 = getMempoolTxs(1).head val htlcSuccessInputs1 = getMempool().head.txIn.map(_.outPoint).toSet - assert(htlcSuccessTx1.txid === htlcSuccessTxId1) + assert(htlcSuccessTx1.txid == htlcSuccessTxId1) // New blocks are found, which makes us aim for a more aggressive block target, so we bump the fees. val targetFeerate = FeeratePerKw(25_000 sat) @@ -957,9 +957,9 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w assert(!isInMempool(htlcSuccessTx1.txid)) val htlcSuccessTx2 = getMempoolTxs(1).head val htlcSuccessInputs2 = getMempool().head.txIn.map(_.outPoint).toSet - assert(htlcSuccessTx2.txid === htlcSuccessTxId2) + assert(htlcSuccessTx2.txid == htlcSuccessTxId2) assert(htlcSuccessTx1.fees < htlcSuccessTx2.fees) - assert(htlcSuccessInputs1 === htlcSuccessInputs2) + assert(htlcSuccessInputs1 == htlcSuccessInputs2) val htlcSuccessTargetFee = Transactions.weight2fee(targetFeerate, htlcSuccessTx2.weight.toInt) assert(htlcSuccessTargetFee * 0.9 <= htlcSuccessTx2.fees && htlcSuccessTx2.fees <= htlcSuccessTargetFee * 1.4, s"actualFee=${htlcSuccessTx2.fees} targetFee=$htlcSuccessTargetFee") } @@ -983,7 +983,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w val htlcSuccessTxId1 = listener.expectMsgType[TransactionPublished].tx.txid val htlcSuccessTx1 = getMempoolTxs(1).head val htlcSuccessInputs1 = getMempool().head.txIn.map(_.outPoint).toSet - assert(htlcSuccessTx1.txid === htlcSuccessTxId1) + assert(htlcSuccessTx1.txid == htlcSuccessTxId1) // New blocks are found, which makes us aim for a more aggressive block target, so we bump the fees. val targetFeerate = FeeratePerKw(75_000 sat) @@ -993,7 +993,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w awaitCond(!isInMempool(htlcSuccessTx1.txid), interval = 200 millis, max = 30 seconds) val htlcSuccessTx2 = getMempoolTxs(1).head val htlcSuccessInputs2 = getMempool().head.txIn.map(_.outPoint).toSet - assert(htlcSuccessTx2.txid === htlcSuccessTxId2) + assert(htlcSuccessTx2.txid == htlcSuccessTxId2) assert(htlcSuccessTx1.fees < htlcSuccessTx2.fees) assert(htlcSuccessInputs1 !== htlcSuccessInputs2) val htlcSuccessTargetFee = Transactions.weight2fee(targetFeerate, htlcSuccessTx2.weight.toInt) @@ -1018,7 +1018,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w w.replyTo ! WatchParentTxConfirmedTriggered(aliceBlockHeight(), 0, commitTx) val htlcSuccessTxId = listener.expectMsgType[TransactionPublished].tx.txid var htlcSuccessTx = getMempoolTxs(1).head - assert(htlcSuccessTx.txid === htlcSuccessTxId) + assert(htlcSuccessTx.txid == htlcSuccessTxId) // We are only 6 blocks away from the confirmation target, so we bump the fees at each new block. (1 to 3).foreach(i => { @@ -1026,7 +1026,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w val bumpedHtlcSuccessTxId = listener.expectMsgType[TransactionPublished].tx.txid assert(!isInMempool(htlcSuccessTx.txid)) val bumpedHtlcSuccessTx = getMempoolTxs(1).head - assert(bumpedHtlcSuccessTx.txid === bumpedHtlcSuccessTxId) + assert(bumpedHtlcSuccessTx.txid == bumpedHtlcSuccessTxId) assert(htlcSuccessTx.fees < bumpedHtlcSuccessTx.fees) htlcSuccessTx = bumpedHtlcSuccessTx }) @@ -1054,7 +1054,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w val htlcTimeoutTxId1 = listener.expectMsgType[TransactionPublished].tx.txid val htlcTimeoutTx1 = getMempoolTxs(1).head val htlcTimeoutInputs1 = getMempool().head.txIn.map(_.outPoint).toSet - assert(htlcTimeoutTx1.txid === htlcTimeoutTxId1) + assert(htlcTimeoutTx1.txid == htlcTimeoutTxId1) // A new block is found, and we've already reached the confirmation target, so we bump the fees. system.eventStream.publish(CurrentBlockHeight(aliceBlockHeight() + 145)) @@ -1062,9 +1062,9 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w assert(!isInMempool(htlcTimeoutTx1.txid)) val htlcTimeoutTx2 = getMempoolTxs(1).head val htlcTimeoutInputs2 = getMempool().head.txIn.map(_.outPoint).toSet - assert(htlcTimeoutTx2.txid === htlcTimeoutTxId2) + assert(htlcTimeoutTx2.txid == htlcTimeoutTxId2) assert(htlcTimeoutTx1.fees < htlcTimeoutTx2.fees) - assert(htlcTimeoutInputs1 === htlcTimeoutInputs2) + assert(htlcTimeoutInputs1 == htlcTimeoutInputs2) // Once the confirmation target is reach, we should raise the feerate by at least 20% at every block. val htlcTimeoutTargetFee = Transactions.weight2fee(feerate * 1.2, htlcTimeoutTx2.weight.toInt) assert(htlcTimeoutTargetFee * 0.9 <= htlcTimeoutTx2.fees && htlcTimeoutTx2.fees <= htlcTimeoutTargetFee * 1.1, s"actualFee=${htlcTimeoutTx2.fees} targetFee=$htlcTimeoutTargetFee") @@ -1113,7 +1113,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w val w2 = alice2blockchain.expectMsgType[WatchParentTxConfirmed] w2.replyTo ! WatchParentTxConfirmedTriggered(currentBlockHeight(probe), 0, commitTx) val result = probe.expectMsgType[TxRejected] - assert(result.reason === ConflictingTxUnconfirmed) + assert(result.reason == ConflictingTxUnconfirmed) getMempoolTxs(1) // the previous htlc-success tx is still in the mempool // our parent will stop us when receiving the TxRejected message. @@ -1123,7 +1123,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w // the first publishing attempt succeeds generateBlocks(5) system.eventStream.publish(CurrentBlockHeight(currentBlockHeight(probe))) - assert(probe.expectMsgType[TxConfirmed].cmd === htlcSuccess) + assert(probe.expectMsgType[TxConfirmed].cmd == htlcSuccess) publisher1 ! Stop } } @@ -1160,7 +1160,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w // Force-close channel. val localCommitTx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.fullySignedLocalCommitTx(alice.underlyingActor.nodeParams.channelKeyManager) val remoteCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.fullySignedLocalCommitTx(bob.underlyingActor.nodeParams.channelKeyManager) - assert(remoteCommitTx.tx.txOut.size === 6) + assert(remoteCommitTx.tx.txOut.size == 6) probe.send(alice, WatchFundingSpentTriggered(remoteCommitTx.tx)) alice2blockchain.expectMsgType[PublishFinalTx] // claim main output val claimHtlcTimeout = alice2blockchain.expectMsgType[PublishReplaceableTx] @@ -1178,15 +1178,15 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w val claimHtlcSuccessPublisher = createPublisher() claimHtlcSuccessPublisher ! Publish(probe.ref, claimHtlcSuccess) val result1 = probe.expectMsgType[TxRejected] - assert(result1.cmd === claimHtlcSuccess) - assert(result1.reason === ConflictingTxConfirmed) + assert(result1.cmd == claimHtlcSuccess) + assert(result1.reason == ConflictingTxConfirmed) claimHtlcSuccessPublisher ! Stop val claimHtlcTimeoutPublisher = createPublisher() claimHtlcTimeoutPublisher ! Publish(probe.ref, claimHtlcTimeout) val result2 = probe.expectMsgType[TxRejected] - assert(result2.cmd === claimHtlcTimeout) - assert(result2.reason === ConflictingTxConfirmed) + assert(result2.cmd == claimHtlcTimeout) + assert(result2.reason == ConflictingTxConfirmed) claimHtlcTimeoutPublisher ! Stop } } @@ -1205,9 +1205,9 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w // Force-close channel and verify txs sent to watcher. val remoteCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.fullySignedLocalCommitTx(bob.underlyingActor.nodeParams.channelKeyManager) if (bob.stateData.asInstanceOf[DATA_NORMAL].commitments.commitmentFormat == DefaultCommitmentFormat) { - assert(remoteCommitTx.tx.txOut.size === 4) + assert(remoteCommitTx.tx.txOut.size == 4) } else { - assert(remoteCommitTx.tx.txOut.size === 6) + assert(remoteCommitTx.tx.txOut.size == 6) } probe.send(alice, WatchFundingSpentTriggered(remoteCommitTx.tx)) @@ -1247,7 +1247,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w generateBlocks(4) system.eventStream.publish(CurrentBlockHeight(currentBlockHeight(probe))) val claimHtlcSuccessResult = probe.expectMsgType[TxConfirmed] - assert(claimHtlcSuccessResult.cmd === claimHtlcSuccess) + assert(claimHtlcSuccessResult.cmd == claimHtlcSuccess) assert(claimHtlcSuccessResult.tx.txIn.map(_.outPoint.txid).contains(remoteCommitTx.txid)) claimHtlcSuccessPublisher ! Stop claimHtlcSuccessResult.tx @@ -1276,7 +1276,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w generateBlocks(4) system.eventStream.publish(CurrentBlockHeight(currentBlockHeight(probe))) val claimHtlcTimeoutResult = probe.expectMsgType[TxConfirmed] - assert(claimHtlcTimeoutResult.cmd === claimHtlcTimeout) + assert(claimHtlcTimeoutResult.cmd == claimHtlcTimeout) assert(claimHtlcTimeoutResult.tx.txIn.map(_.outPoint.txid).contains(remoteCommitTx.txid)) claimHtlcTimeoutPublisher ! Stop claimHtlcTimeoutResult.tx @@ -1290,10 +1290,10 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w val (remoteCommitTx, claimHtlcSuccess, claimHtlcTimeout) = remoteCloseChannelWithHtlcs(f, aliceBlockHeight() + 50) val claimHtlcSuccessTx = testPublishClaimHtlcSuccess(f, remoteCommitTx, claimHtlcSuccess, currentFeerate) assert(claimHtlcSuccess.txInfo.fee > 0.sat) - assert(claimHtlcSuccessTx.txIn.length === 1) + assert(claimHtlcSuccessTx.txIn.length == 1) val claimHtlcTimeoutTx = testPublishClaimHtlcTimeout(f, remoteCommitTx, claimHtlcTimeout, currentFeerate) assert(claimHtlcTimeout.txInfo.fee > 0.sat) - assert(claimHtlcTimeoutTx.txIn.length === 1) + assert(claimHtlcTimeoutTx.txIn.length == 1) } } @@ -1306,12 +1306,12 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w // NB: we try to get transactions confirmed *before* their confirmation target, so we aim for a more aggressive block target than what's provided. setFeerate(targetFeerate, blockTarget = 12) val claimHtlcSuccessTx = testPublishClaimHtlcSuccess(f, remoteCommitTx, claimHtlcSuccess, targetFeerate) - assert(claimHtlcSuccessTx.txIn.length === 1) - assert(claimHtlcSuccessTx.txOut.length === 1) + assert(claimHtlcSuccessTx.txIn.length == 1) + assert(claimHtlcSuccessTx.txOut.length == 1) assert(claimHtlcSuccessTx.txOut.head.amount < claimHtlcSuccess.txInfo.tx.txOut.head.amount) val claimHtlcTimeoutTx = testPublishClaimHtlcTimeout(f, remoteCommitTx, claimHtlcTimeout, targetFeerate) - assert(claimHtlcTimeoutTx.txIn.length === 1) - assert(claimHtlcTimeoutTx.txOut.length === 1) + assert(claimHtlcTimeoutTx.txIn.length == 1) + assert(claimHtlcTimeoutTx.txOut.length == 1) assert(claimHtlcTimeoutTx.txOut.head.amount < claimHtlcTimeout.txInfo.tx.txOut.head.amount) } } @@ -1333,7 +1333,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w generateBlocks(4) system.eventStream.publish(CurrentBlockHeight(currentBlockHeight(probe))) val claimHtlcSuccessResult = probe.expectMsgType[TxConfirmed] - assert(claimHtlcSuccessResult.cmd === claimHtlcSuccess) + assert(claimHtlcSuccessResult.cmd == claimHtlcSuccess) assert(claimHtlcSuccessResult.tx.txIn.map(_.outPoint.txid).contains(remoteCommitTx.txid)) claimHtlcSuccessPublisher ! Stop @@ -1350,7 +1350,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w generateBlocks(4) system.eventStream.publish(CurrentBlockHeight(currentBlockHeight(probe))) val claimHtlcTimeoutResult = probe.expectMsgType[TxConfirmed] - assert(claimHtlcTimeoutResult.cmd === claimHtlcTimeout) + assert(claimHtlcTimeoutResult.cmd == claimHtlcTimeout) assert(claimHtlcTimeoutResult.tx.txIn.map(_.outPoint.txid).contains(remoteCommitTx.txid)) claimHtlcTimeoutPublisher ! Stop } @@ -1368,8 +1368,8 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w val w1 = alice2blockchain.expectMsgType[WatchParentTxConfirmed] w1.replyTo ! WatchParentTxConfirmedTriggered(currentBlockHeight(probe), 0, remoteCommitTx) val result1 = probe.expectMsgType[TxRejected] - assert(result1.cmd === claimHtlcSuccess) - assert(result1.reason === TxSkipped(retryNextBlock = true)) + assert(result1.cmd == claimHtlcSuccess) + assert(result1.reason == TxSkipped(retryNextBlock = true)) claimHtlcSuccessPublisher ! Stop val claimHtlcTimeoutPublisher = createPublisher() @@ -1379,8 +1379,8 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w val w2 = alice2blockchain.expectMsgType[WatchParentTxConfirmed] w2.replyTo ! WatchParentTxConfirmedTriggered(currentBlockHeight(probe), 0, remoteCommitTx) val result2 = probe.expectMsgType[TxRejected] - assert(result2.cmd === claimHtlcTimeout) - assert(result2.reason === TxSkipped(retryNextBlock = true)) + assert(result2.cmd == claimHtlcTimeout) + assert(result2.reason == TxSkipped(retryNextBlock = true)) claimHtlcTimeoutPublisher ! Stop } } @@ -1402,21 +1402,21 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w val claimHtlcSuccessPublisher = createPublisher() claimHtlcSuccessPublisher ! Publish(probe.ref, claimHtlcSuccess) val claimHtlcSuccessTx1 = getMempoolTxs(1).head - assert(listener.expectMsgType[TransactionPublished].tx.txid === claimHtlcSuccessTx1.txid) + assert(listener.expectMsgType[TransactionPublished].tx.txid == claimHtlcSuccessTx1.txid) setFeerate(targetFeerate) system.eventStream.publish(CurrentBlockHeight(aliceBlockHeight() + 5)) val claimHtlcSuccessTxId2 = listener.expectMsgType[TransactionPublished].tx.txid assert(!isInMempool(claimHtlcSuccessTx1.txid)) val claimHtlcSuccessTx2 = getMempoolTxs(1).head - assert(claimHtlcSuccessTx2.txid === claimHtlcSuccessTxId2) + assert(claimHtlcSuccessTx2.txid == claimHtlcSuccessTxId2) assert(claimHtlcSuccessTx1.fees < claimHtlcSuccessTx2.fees) val targetHtlcSuccessFee = Transactions.weight2fee(targetFeerate, claimHtlcSuccessTx2.weight.toInt) assert(targetHtlcSuccessFee * 0.9 <= claimHtlcSuccessTx2.fees && claimHtlcSuccessTx2.fees <= targetHtlcSuccessFee * 1.1, s"actualFee=${claimHtlcSuccessTx2.fees} targetFee=$targetHtlcSuccessFee") val finalHtlcSuccessTx = getMempool().head - assert(finalHtlcSuccessTx.txIn.length === 1) - assert(finalHtlcSuccessTx.txOut.length === 1) - assert(finalHtlcSuccessTx.txIn.head.outPoint.txid === remoteCommitTx.txid) + assert(finalHtlcSuccessTx.txIn.length == 1) + assert(finalHtlcSuccessTx.txOut.length == 1) + assert(finalHtlcSuccessTx.txIn.head.outPoint.txid == remoteCommitTx.txid) // The Claim-HTLC-timeout will be published after the timeout. setFeerate(initialFeerate) @@ -1424,23 +1424,23 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w claimHtlcTimeoutPublisher ! Publish(probe.ref, claimHtlcTimeout) generateBlocks(144) system.eventStream.publish(CurrentBlockHeight(aliceBlockHeight() + 144)) - assert(probe.expectMsgType[TxConfirmed].tx.txid === finalHtlcSuccessTx.txid) // the claim-htlc-success is now confirmed + assert(probe.expectMsgType[TxConfirmed].tx.txid == finalHtlcSuccessTx.txid) // the claim-htlc-success is now confirmed val claimHtlcTimeoutTx1 = getMempoolTxs(1).head - assert(listener.expectMsgType[TransactionPublished].tx.txid === claimHtlcTimeoutTx1.txid) + assert(listener.expectMsgType[TransactionPublished].tx.txid == claimHtlcTimeoutTx1.txid) setFeerate(targetFeerate) system.eventStream.publish(CurrentBlockHeight(aliceBlockHeight() + 145)) val claimHtlcTimeoutTxId2 = listener.expectMsgType[TransactionPublished].tx.txid assert(!isInMempool(claimHtlcTimeoutTx1.txid)) val claimHtlcTimeoutTx2 = getMempoolTxs(1).head - assert(claimHtlcTimeoutTx2.txid === claimHtlcTimeoutTxId2) + assert(claimHtlcTimeoutTx2.txid == claimHtlcTimeoutTxId2) assert(claimHtlcTimeoutTx1.fees < claimHtlcTimeoutTx2.fees) val targetHtlcTimeoutFee = Transactions.weight2fee(targetFeerate, claimHtlcTimeoutTx2.weight.toInt) assert(targetHtlcTimeoutFee * 0.9 <= claimHtlcTimeoutTx2.fees && claimHtlcTimeoutTx2.fees <= targetHtlcTimeoutFee * 1.1, s"actualFee=${claimHtlcTimeoutTx2.fees} targetFee=$targetHtlcTimeoutFee") val finalHtlcTimeoutTx = getMempool().head - assert(finalHtlcTimeoutTx.txIn.length === 1) - assert(finalHtlcTimeoutTx.txOut.length === 1) - assert(finalHtlcTimeoutTx.txIn.head.outPoint.txid === remoteCommitTx.txid) + assert(finalHtlcTimeoutTx.txIn.length == 1) + assert(finalHtlcTimeoutTx.txOut.length == 1) + assert(finalHtlcTimeoutTx.txIn.head.outPoint.txid == remoteCommitTx.txid) } } @@ -1459,14 +1459,14 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w val w1 = alice2blockchain.expectMsgType[WatchParentTxConfirmed] w1.replyTo ! WatchParentTxConfirmedTriggered(currentBlockHeight(probe), 0, remoteCommitTx) val claimHtlcSuccessTx = getMempoolTxs(1).head - assert(listener.expectMsgType[TransactionPublished].tx.txid === claimHtlcSuccessTx.txid) + assert(listener.expectMsgType[TransactionPublished].tx.txid == claimHtlcSuccessTx.txid) // New blocks are found and the feerate is higher, but the htlc would become dust, so we don't bump the fees. setFeerate(FeeratePerKw(50_000 sat)) system.eventStream.publish(CurrentBlockHeight(aliceBlockHeight() + 5)) probe.expectNoMessage(500 millis) val mempoolTxs = getMempool() - assert(mempoolTxs.map(_.txid).toSet === Set(claimHtlcSuccessTx.txid)) + assert(mempoolTxs.map(_.txid).toSet == Set(claimHtlcSuccessTx.txid)) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/TxPublisherSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/TxPublisherSpec.scala index f4c58a4446..fdd48f7443 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/TxPublisherSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/TxPublisherSpec.scala @@ -75,7 +75,7 @@ class TxPublisherSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike { val cmd = PublishFinalTx(tx, tx.txIn.head.outPoint, "final-tx", 5 sat, None) txPublisher ! cmd val child = factory.expectMsgType[FinalTxPublisherSpawned].actor - assert(child.expectMsgType[FinalTxPublisher.Publish].cmd === cmd) + assert(child.expectMsgType[FinalTxPublisher.Publish].cmd == cmd) } test("publish final tx duplicate") { f => @@ -107,7 +107,7 @@ class TxPublisherSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike { txPublisher ! cmd val child = factory.expectMsgType[ReplaceableTxPublisherSpawned].actor val p = child.expectMsgType[ReplaceableTxPublisher.Publish] - assert(p.cmd === cmd) + assert(p.cmd == cmd) } test("publish replaceable tx duplicate") { f => @@ -119,7 +119,7 @@ class TxPublisherSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike { val cmd = PublishReplaceableTx(anchorTx, null) txPublisher ! cmd val child = factory.expectMsgType[ReplaceableTxPublisherSpawned].actor - assert(child.expectMsgType[ReplaceableTxPublisher.Publish].cmd === cmd) + assert(child.expectMsgType[ReplaceableTxPublisher.Publish].cmd == cmd) // We ignore duplicates that don't use a more aggressive confirmation target: txPublisher ! PublishReplaceableTx(anchorTx, null) @@ -192,7 +192,7 @@ class TxPublisherSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike { // We automatically retry the failed attempt with new wallet inputs: val attempt3 = factory.expectMsgType[ReplaceableTxPublisherSpawned] - assert(attempt3.actor.expectMsgType[ReplaceableTxPublisher.Publish].cmd === cmd2) + assert(attempt3.actor.expectMsgType[ReplaceableTxPublisher.Publish].cmd == cmd2) } test("publishing attempt fails (main input gone)") { f => @@ -212,7 +212,7 @@ class TxPublisherSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike { factory.expectNoMessage(100 millis) system.eventStream.publish(CurrentBlockHeight(BlockHeight(8200))) val attempt2 = factory.expectMsgType[FinalTxPublisherSpawned] - assert(attempt2.actor.expectMsgType[FinalTxPublisher.Publish].cmd === cmd) + assert(attempt2.actor.expectMsgType[FinalTxPublisher.Publish].cmd == cmd) } test("publishing attempt fails (not enough funds)") { f => @@ -233,7 +233,7 @@ class TxPublisherSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike { factory.expectNoMessage(100 millis) system.eventStream.publish(CurrentBlockHeight(BlockHeight(8200))) val attempt2 = factory.expectMsgType[ReplaceableTxPublisherSpawned] - assert(attempt2.actor.expectMsgType[ReplaceableTxPublisher.Publish].cmd === cmd) + assert(attempt2.actor.expectMsgType[ReplaceableTxPublisher.Publish].cmd == cmd) } test("publishing attempt fails (transaction skipped)") { f => @@ -259,7 +259,7 @@ class TxPublisherSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike { system.eventStream.publish(CurrentBlockHeight(BlockHeight(8200))) val attempt3 = factory.expectMsgType[FinalTxPublisherSpawned] - assert(attempt3.actor.expectMsgType[publish.FinalTxPublisher.Publish].cmd === cmd2) + assert(attempt3.actor.expectMsgType[publish.FinalTxPublisher.Publish].cmd == cmd2) factory.expectNoMessage(100 millis) } @@ -297,7 +297,7 @@ class TxPublisherSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike { // We retry when a new block is found: system.eventStream.publish(CurrentBlockHeight(nodeParams.currentBlockHeight + 1)) val attempt2 = factory.expectMsgType[ReplaceableTxPublisherSpawned] - assert(attempt2.actor.expectMsgType[ReplaceableTxPublisher.Publish].cmd === cmd) + assert(attempt2.actor.expectMsgType[ReplaceableTxPublisher.Publish].cmd == cmd) } test("publishing attempt fails (confirmed conflicting transaction)") { f => @@ -339,9 +339,9 @@ class TxPublisherSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike { // No attempts. val attempts = PublishAttempts.empty assert(attempts.isEmpty) - assert(attempts.count === 0) + assert(attempts.count == 0) assert(attempts.attempts.isEmpty) - assert(attempts.remove(UUID.randomUUID()) === (Nil, attempts)) + assert(attempts.remove(UUID.randomUUID()) == (Nil, attempts)) } { // Only final attempts. @@ -349,19 +349,19 @@ class TxPublisherSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike { val attempt2 = FinalAttempt(UUID.randomUUID(), null, null) val attempts = PublishAttempts.empty.add(attempt1).add(attempt2) assert(!attempts.isEmpty) - assert(attempts.count === 2) + assert(attempts.count == 2) assert(attempts.replaceableAttempt_opt.isEmpty) - assert(attempts.remove(UUID.randomUUID()) === (Nil, attempts)) - assert(attempts.remove(attempt1.id) === (Seq(attempt1), PublishAttempts(Seq(attempt2), None))) + assert(attempts.remove(UUID.randomUUID()) == (Nil, attempts)) + assert(attempts.remove(attempt1.id) == (Seq(attempt1), PublishAttempts(Seq(attempt2), None))) } { // Only replaceable attempts. val attempt = ReplaceableAttempt(UUID.randomUUID(), null, BlockHeight(0), null) val attempts = PublishAttempts(Nil, Some(attempt)) assert(!attempts.isEmpty) - assert(attempts.count === 1) - assert(attempts.remove(UUID.randomUUID()) === (Nil, attempts)) - assert(attempts.remove(attempt.id) === (Seq(attempt), PublishAttempts.empty)) + assert(attempts.count == 1) + assert(attempts.remove(UUID.randomUUID()) == (Nil, attempts)) + assert(attempts.remove(attempt.id) == (Seq(attempt), PublishAttempts.empty)) } { // Mix of final and replaceable attempts with the same id. @@ -370,9 +370,9 @@ class TxPublisherSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike { val attempt3 = FinalAttempt(UUID.randomUUID(), null, null) val attempts = PublishAttempts(Seq(attempt2), Some(attempt1)).add(attempt3) assert(!attempts.isEmpty) - assert(attempts.count === 3) - assert(attempts.remove(attempt3.id) === (Seq(attempt3), PublishAttempts(Seq(attempt2), Some(attempt1)))) - assert(attempts.remove(attempt1.id) === (Seq(attempt2, attempt1), PublishAttempts(Seq(attempt3), None))) + assert(attempts.count == 3) + assert(attempts.remove(attempt3.id) == (Seq(attempt3), PublishAttempts(Seq(attempt2), Some(attempt1)))) + assert(attempts.remove(attempt1.id) == (Seq(attempt2, attempt1), PublishAttempts(Seq(attempt3), None))) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/TxTimeLocksMonitorSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/TxTimeLocksMonitorSpec.scala index eafbc13294..0795a5dad1 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/TxTimeLocksMonitorSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/TxTimeLocksMonitorSpec.scala @@ -67,8 +67,8 @@ class TxTimeLocksMonitorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik monitor ! CheckTx(probe.ref, tx, "relative-delay") val w = watcher.expectMsgType[WatchParentTxConfirmed] - assert(w.txId === parentTx.txid) - assert(w.minDepth === 3) + assert(w.txId == parentTx.txid) + assert(w.minDepth == 3) probe.expectNoMessage(100 millis) w.replyTo ! WatchParentTxConfirmedTriggered(BlockHeight(651), 0, parentTx) @@ -92,7 +92,7 @@ class TxTimeLocksMonitorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik val w1 = watcher.expectMsgType[WatchParentTxConfirmed] val w2 = watcher.expectMsgType[WatchParentTxConfirmed] watcher.expectNoMessage(100 millis) - assert(Seq(w1, w2).map(w => (w.txId, w.minDepth)).toSet === Set((parentTx1.txid, 3), (parentTx2.txid, 1))) + assert(Seq(w1, w2).map(w => (w.txId, w.minDepth)).toSet == Set((parentTx1.txid, 3), (parentTx2.txid, 1))) probe.expectNoMessage(100 millis) w1.replyTo ! WatchParentTxConfirmedTriggered(BlockHeight(651), 0, parentTx1) @@ -121,7 +121,7 @@ class TxTimeLocksMonitorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik val w1 = watcher.expectMsgType[WatchParentTxConfirmed] val w2 = watcher.expectMsgType[WatchParentTxConfirmed] watcher.expectNoMessage(100 millis) - assert(Seq(w1, w2).map(w => (w.txId, w.minDepth)).toSet === Set((parentTx1.txid, 3), (parentTx2.txid, 6))) + assert(Seq(w1, w2).map(w => (w.txId, w.minDepth)).toSet == Set((parentTx1.txid, 3), (parentTx2.txid, 6))) probe.expectNoMessage(100 millis) w1.replyTo ! WatchParentTxConfirmedTriggered(BlockHeight(651), 0, parentTx1) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala index d011406706..c470e466d1 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala @@ -18,7 +18,7 @@ package fr.acinq.eclair.channel.states import akka.actor.typed.scaladsl.adapter.actorRefAdapter import akka.actor.{ActorContext, ActorRef, ActorSystem} -import akka.testkit.{TestFSMRef, TestKit, TestKitBase, TestProbe} +import akka.testkit.{TestFSMRef, TestKit, TestProbe} import com.softwaremill.quicklens.ModifyPimp import fr.acinq.bitcoin.ScriptFlags import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey @@ -32,32 +32,19 @@ import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel.fsm.Channel import fr.acinq.eclair.channel.publish.TxPublisher import fr.acinq.eclair.channel.publish.TxPublisher.PublishReplaceableTx -import fr.acinq.eclair.channel.states.ChannelStateTestsHelperMethods.FakeTxPublisherFactory +import fr.acinq.eclair.channel.states.ChannelStateTestsBase.FakeTxPublisherFactory import fr.acinq.eclair.payment.OutgoingPaymentPacket import fr.acinq.eclair.payment.OutgoingPaymentPacket.Upstream import fr.acinq.eclair.router.Router.ChannelHop import fr.acinq.eclair.transactions.Transactions import fr.acinq.eclair.transactions.Transactions._ import fr.acinq.eclair.wire.protocol._ -import org.scalatest.FixtureTestSuite +import org.scalatest.Assertions +import org.scalatest.concurrent.Eventually import java.util.UUID import scala.concurrent.duration._ -/** - * Created by PM on 23/08/2016. - */ -trait ChannelStateTestsBase extends ChannelStateTestsHelperMethods with FixtureTestSuite { - - implicit class ChannelWithTestFeeConf(a: TestFSMRef[ChannelState, ChannelData, Channel]) { - // @formatter:off - def feeEstimator: TestFeeEstimator = a.underlyingActor.nodeParams.onChainFeeConf.feeEstimator.asInstanceOf[TestFeeEstimator] - def feeTargets: FeeTargets = a.underlyingActor.nodeParams.onChainFeeConf.feeTargets - // @formatter:on - } - -} - object ChannelStateTestsTags { /** If set, channels will use option_support_large_channel. */ val Wumbo = "wumbo" @@ -87,7 +74,7 @@ object ChannelStateTestsTags { val ChannelType = "option_channel_type" } -trait ChannelStateTestsHelperMethods extends TestKitBase { +trait ChannelStateTestsBase extends Assertions with Eventually { case class SetupFixture(alice: TestFSMRef[ChannelState, ChannelData, Channel], bob: TestFSMRef[ChannelState, ChannelData, Channel], @@ -106,6 +93,14 @@ trait ChannelStateTestsHelperMethods extends TestKitBase { def currentBlockHeight: BlockHeight = alice.underlyingActor.nodeParams.currentBlockHeight } + implicit class ChannelWithTestFeeConf(a: TestFSMRef[ChannelState, ChannelData, Channel]) { + // @formatter:off + def feeEstimator: TestFeeEstimator = a.underlyingActor.nodeParams.onChainFeeConf.feeEstimator.asInstanceOf[TestFeeEstimator] + def feeTargets: FeeTargets = a.underlyingActor.nodeParams.onChainFeeConf.feeTargets + // @formatter:on + } + + implicit val system: ActorSystem val systemA: ActorSystem = ActorSystem("system-alice") val systemB: ActorSystem = ActorSystem("system-bob") @@ -210,9 +205,9 @@ trait ChannelStateTestsHelperMethods extends TestKitBase { val aliceInit = Init(aliceParams.initFeatures) val bobInit = Init(bobParams.initFeatures) alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, fundingSatoshis, pushMsat, commitTxFeerate, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType) - assert(alice2blockchain.expectMsgType[TxPublisher.SetChannelId].channelId === ByteVector32.Zeroes) + assert(alice2blockchain.expectMsgType[TxPublisher.SetChannelId].channelId == ByteVector32.Zeroes) bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType) - assert(bob2blockchain.expectMsgType[TxPublisher.SetChannelId].channelId === ByteVector32.Zeroes) + assert(bob2blockchain.expectMsgType[TxPublisher.SetChannelId].channelId == ByteVector32.Zeroes) alice2bob.expectMsgType[OpenChannel] alice2bob.forward(bob) bob2alice.expectMsgType[AcceptChannel] @@ -227,7 +222,8 @@ trait ChannelStateTestsHelperMethods extends TestKitBase { assert(bob2blockchain.expectMsgType[TxPublisher.SetChannelId].channelId != ByteVector32.Zeroes) bob2blockchain.expectMsgType[WatchFundingSpent] bob2blockchain.expectMsgType[WatchFundingConfirmed] - awaitCond(alice.stateName == WAIT_FOR_FUNDING_CONFIRMED) + + eventually(assert(alice.stateName == WAIT_FOR_FUNDING_CONFIRMED)) val fundingTx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CONFIRMED].fundingTx.get alice ! WatchFundingConfirmedTriggered(BlockHeight(400000), 42, fundingTx) bob ! WatchFundingConfirmedTriggered(BlockHeight(400000), 42, fundingTx) @@ -239,8 +235,8 @@ trait ChannelStateTestsHelperMethods extends TestKitBase { bob2alice.forward(alice) alice2blockchain.expectMsgType[WatchFundingDeeplyBuried] bob2blockchain.expectMsgType[WatchFundingDeeplyBuried] - awaitCond(alice.stateName == NORMAL) - awaitCond(bob.stateName == NORMAL) + eventually(assert(alice.stateName == NORMAL)) + eventually(assert(bob.stateName == NORMAL)) assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.availableBalanceForSend == (pushMsat - aliceParams.requestedChannelReserve_opt.getOrElse(0 sat)).max(0 msat)) // x2 because alice and bob share the same relayer channelUpdateListener.expectMsgType[LocalChannelUpdate] @@ -288,7 +284,7 @@ trait ChannelStateTestsHelperMethods extends TestKitBase { s ! cmdAdd val htlc = s2r.expectMsgType[UpdateAddHtlc] s2r.forward(r) - awaitCond(r.stateData.asInstanceOf[PersistentChannelData].commitments.remoteChanges.proposed.contains(htlc)) + eventually(assert(r.stateData.asInstanceOf[PersistentChannelData].commitments.remoteChanges.proposed.contains(htlc))) htlc } @@ -296,14 +292,14 @@ trait ChannelStateTestsHelperMethods extends TestKitBase { s ! CMD_FULFILL_HTLC(id, preimage) val fulfill = s2r.expectMsgType[UpdateFulfillHtlc] s2r.forward(r) - awaitCond(r.stateData.asInstanceOf[PersistentChannelData].commitments.remoteChanges.proposed.contains(fulfill)) + eventually(assert(r.stateData.asInstanceOf[PersistentChannelData].commitments.remoteChanges.proposed.contains(fulfill))) } def failHtlc(id: Long, s: TestFSMRef[ChannelState, ChannelData, Channel], r: TestFSMRef[ChannelState, ChannelData, Channel], s2r: TestProbe, r2s: TestProbe): Unit = { s ! CMD_FAIL_HTLC(id, Right(TemporaryNodeFailure)) val fail = s2r.expectMsgType[UpdateFailHtlc] s2r.forward(r) - awaitCond(r.stateData.asInstanceOf[PersistentChannelData].commitments.remoteChanges.proposed.contains(fail)) + eventually(assert(r.stateData.asInstanceOf[PersistentChannelData].commitments.remoteChanges.proposed.contains(fail))) } def crossSign(s: TestFSMRef[ChannelState, ChannelData, Channel], r: TestFSMRef[ChannelState, ChannelData, Channel], s2r: TestProbe, r2s: TestProbe): Unit = { @@ -326,15 +322,19 @@ trait ChannelStateTestsHelperMethods extends TestKitBase { s2r.forward(r) r2s.expectMsgType[RevokeAndAck] r2s.forward(s) - awaitCond(s.stateData.asInstanceOf[PersistentChannelData].commitments.localCommit.index == sCommitIndex + 1) - awaitCond(s.stateData.asInstanceOf[PersistentChannelData].commitments.remoteCommit.index == sCommitIndex + 2) - awaitCond(r.stateData.asInstanceOf[PersistentChannelData].commitments.localCommit.index == rCommitIndex + 2) - awaitCond(r.stateData.asInstanceOf[PersistentChannelData].commitments.remoteCommit.index == rCommitIndex + 1) + eventually { + assert(s.stateData.asInstanceOf[PersistentChannelData].commitments.localCommit.index == sCommitIndex + 1) + assert(s.stateData.asInstanceOf[PersistentChannelData].commitments.remoteCommit.index == sCommitIndex + 2) + assert(r.stateData.asInstanceOf[PersistentChannelData].commitments.localCommit.index == rCommitIndex + 2) + assert(r.stateData.asInstanceOf[PersistentChannelData].commitments.remoteCommit.index == rCommitIndex + 1) + } } else { - awaitCond(s.stateData.asInstanceOf[PersistentChannelData].commitments.localCommit.index == sCommitIndex + 1) - awaitCond(s.stateData.asInstanceOf[PersistentChannelData].commitments.remoteCommit.index == sCommitIndex + 1) - awaitCond(r.stateData.asInstanceOf[PersistentChannelData].commitments.localCommit.index == rCommitIndex + 1) - awaitCond(r.stateData.asInstanceOf[PersistentChannelData].commitments.remoteCommit.index == rCommitIndex + 1) + eventually { + assert(s.stateData.asInstanceOf[PersistentChannelData].commitments.localCommit.index == sCommitIndex + 1) + assert(s.stateData.asInstanceOf[PersistentChannelData].commitments.remoteCommit.index == sCommitIndex + 1) + assert(r.stateData.asInstanceOf[PersistentChannelData].commitments.localCommit.index == rCommitIndex + 1) + assert(r.stateData.asInstanceOf[PersistentChannelData].commitments.remoteCommit.index == rCommitIndex + 1) + } } } @@ -350,7 +350,7 @@ trait ChannelStateTestsHelperMethods extends TestKitBase { r2s.forward(s) s2r.expectMsgType[RevokeAndAck] s2r.forward(r) - awaitCond(s.stateData.asInstanceOf[PersistentChannelData].commitments.localCommit.spec.commitTxFeerate == feerate) + eventually(assert(s.stateData.asInstanceOf[PersistentChannelData].commitments.localCommit.spec.commitTxFeerate == feerate)) } def mutualClose(s: TestFSMRef[ChannelState, ChannelData, Channel], r: TestFSMRef[ChannelState, ChannelData, Channel], s2r: TestProbe, r2s: TestProbe, s2blockchain: TestProbe, r2blockchain: TestProbe): Unit = { @@ -373,8 +373,10 @@ trait ChannelStateTestsHelperMethods extends TestKitBase { s2blockchain.expectMsgType[WatchTxConfirmed] r2blockchain.expectMsgType[TxPublisher.PublishTx] r2blockchain.expectMsgType[WatchTxConfirmed] - awaitCond(s.stateName == CLOSING) - awaitCond(r.stateName == CLOSING) + eventually { + assert(s.stateName == CLOSING) + assert(r.stateName == CLOSING) + } // both nodes are now in CLOSING state with a mutual close tx pending for confirmation } @@ -387,7 +389,7 @@ trait ChannelStateTestsHelperMethods extends TestKitBase { val commitTx = localCommit.commitTxAndRemoteSig.commitTx.tx s ! Error(ByteVector32.Zeroes, "oops") - awaitCond(s.stateName == CLOSING) + eventually(assert(s.stateName == CLOSING)) val closingState = s.stateData.asInstanceOf[DATA_CLOSING] assert(closingState.localCommitPublished.isDefined) val localCommitPublished = closingState.localCommitPublished.get @@ -415,8 +417,8 @@ trait ChannelStateTestsHelperMethods extends TestKitBase { } // we watch the confirmation of the "final" transactions that send funds to our wallets (main delayed output and 2nd stage htlc transactions) - assert(s2blockchain.expectMsgType[WatchTxConfirmed].txId === commitTx.txid) - localCommitPublished.claimMainDelayedOutputTx.foreach(claimMain => assert(s2blockchain.expectMsgType[WatchTxConfirmed].txId === claimMain.tx.txid)) + assert(s2blockchain.expectMsgType[WatchTxConfirmed].txId == commitTx.txid) + localCommitPublished.claimMainDelayedOutputTx.foreach(claimMain => assert(s2blockchain.expectMsgType[WatchTxConfirmed].txId == claimMain.tx.txid)) // we watch outputs of the commitment tx that both parties may spend and anchor outputs val watchedOutputIndexes = localCommitPublished.htlcTxs.keySet.map(_.index) ++ localCommitPublished.claimAnchorTxs.collect { case tx: ClaimLocalAnchorOutputTx => tx.input.outPoint.index } @@ -432,7 +434,7 @@ trait ChannelStateTestsHelperMethods extends TestKitBase { def remoteClose(rCommitTx: Transaction, s: TestFSMRef[ChannelState, ChannelData, Channel], s2blockchain: TestProbe): RemoteCommitPublished = { // we make s believe r unilaterally closed the channel s ! WatchFundingSpentTriggered(rCommitTx) - awaitCond(s.stateName == CLOSING) + eventually(assert(s.stateName == CLOSING)) val closingData = s.stateData.asInstanceOf[DATA_CLOSING] val remoteCommitPublished_opt = closingData.remoteCommitPublished.orElse(closingData.nextRemoteCommitPublished).orElse(closingData.futureRemoteCommitPublished) assert(remoteCommitPublished_opt.isDefined) @@ -451,8 +453,8 @@ trait ChannelStateTestsHelperMethods extends TestKitBase { assert(publishedClaimHtlcTxs.map(_.input).toSet == claimHtlcTxs.map(_.input.outPoint).toSet) // we watch the confirmation of the "final" transactions that send funds to our wallets (main delayed output and 2nd stage htlc transactions) - assert(s2blockchain.expectMsgType[WatchTxConfirmed].txId === rCommitTx.txid) - remoteCommitPublished.claimMainOutputTx.foreach(claimMain => assert(s2blockchain.expectMsgType[WatchTxConfirmed].txId === claimMain.tx.txid)) + assert(s2blockchain.expectMsgType[WatchTxConfirmed].txId == rCommitTx.txid) + remoteCommitPublished.claimMainOutputTx.foreach(claimMain => assert(s2blockchain.expectMsgType[WatchTxConfirmed].txId == claimMain.tx.txid)) // we watch outputs of the commitment tx that both parties may spend val htlcOutputIndexes = remoteCommitPublished.claimHtlcTxs.keySet.map(_.index) @@ -477,7 +479,7 @@ trait ChannelStateTestsHelperMethods extends TestKitBase { } -object ChannelStateTestsHelperMethods { +object ChannelStateTestsBase { case class FakeTxPublisherFactory(txPublisher: TestProbe) extends Channel.TxPublisherFactory { override def spawnTxPublisher(context: ActorContext, remoteNodeId: PublicKey): akka.actor.typed.ActorRef[TxPublisher.Command] = txPublisher.ref diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForAcceptChannelStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForAcceptChannelStateSpec.scala index 11f6dc52ba..0fa87ba812 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForAcceptChannelStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForAcceptChannelStateSpec.scala @@ -76,8 +76,8 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS import f._ val accept = bob2alice.expectMsgType[AcceptChannel] // Since https://github.com/lightningnetwork/lightning-rfc/pull/714 we must include an empty upfront_shutdown_script. - assert(accept.upfrontShutdownScript_opt === Some(ByteVector.empty)) - assert(accept.channelType_opt === Some(ChannelTypes.Standard)) + assert(accept.upfrontShutdownScript_opt == Some(ByteVector.empty)) + assert(accept.channelType_opt == Some(ChannelTypes.Standard)) bob2alice.forward(alice) awaitCond(alice.stateName == WAIT_FOR_FUNDING_INTERNAL) aliceOrigin.expectNoMessage() @@ -86,39 +86,39 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS test("recv AcceptChannel (anchor outputs)", Tag(ChannelStateTestsTags.AnchorOutputs)) { f => import f._ val accept = bob2alice.expectMsgType[AcceptChannel] - assert(accept.channelType_opt === Some(ChannelTypes.AnchorOutputs)) + assert(accept.channelType_opt == Some(ChannelTypes.AnchorOutputs)) bob2alice.forward(alice) awaitCond(alice.stateName == WAIT_FOR_FUNDING_INTERNAL) - assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_INTERNAL].channelFeatures.channelType === ChannelTypes.AnchorOutputs) + assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_INTERNAL].channelFeatures.channelType == ChannelTypes.AnchorOutputs) aliceOrigin.expectNoMessage() } test("recv AcceptChannel (anchor outputs zero fee htlc txs)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => import f._ val accept = bob2alice.expectMsgType[AcceptChannel] - assert(accept.channelType_opt === Some(ChannelTypes.AnchorOutputsZeroFeeHtlcTx)) + assert(accept.channelType_opt == Some(ChannelTypes.AnchorOutputsZeroFeeHtlcTx)) bob2alice.forward(alice) awaitCond(alice.stateName == WAIT_FOR_FUNDING_INTERNAL) - assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_INTERNAL].channelFeatures.channelType === ChannelTypes.AnchorOutputsZeroFeeHtlcTx) + assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_INTERNAL].channelFeatures.channelType == ChannelTypes.AnchorOutputsZeroFeeHtlcTx) aliceOrigin.expectNoMessage() } test("recv AcceptChannel (channel type not set)", Tag(ChannelStateTestsTags.AnchorOutputs)) { f => import f._ val accept = bob2alice.expectMsgType[AcceptChannel] - assert(accept.channelType_opt === Some(ChannelTypes.AnchorOutputs)) + assert(accept.channelType_opt == Some(ChannelTypes.AnchorOutputs)) // Alice explicitly asked for an anchor output channel. Bob doesn't support explicit channel type negotiation but // they both activated anchor outputs so it is the default choice anyway. bob2alice.forward(alice, accept.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScriptTlv(ByteVector.empty)))) awaitCond(alice.stateName == WAIT_FOR_FUNDING_INTERNAL) - assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_INTERNAL].channelFeatures.channelType === ChannelTypes.AnchorOutputs) + assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_INTERNAL].channelFeatures.channelType == ChannelTypes.AnchorOutputs) aliceOrigin.expectNoMessage() } test("recv AcceptChannel (channel type not set but feature bit set)", Tag(ChannelStateTestsTags.ChannelType), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => import f._ val accept = bob2alice.expectMsgType[AcceptChannel] - assert(accept.channelType_opt === Some(ChannelTypes.AnchorOutputsZeroFeeHtlcTx)) + assert(accept.channelType_opt == Some(ChannelTypes.AnchorOutputsZeroFeeHtlcTx)) bob2alice.forward(alice, accept.copy(tlvStream = TlvStream.empty)) alice2bob.expectMsg(Error(accept.temporaryChannelId, "option_channel_type was negotiated but channel_type is missing")) awaitCond(alice.stateName == CLOSED) @@ -129,17 +129,17 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS import f._ val accept = bob2alice.expectMsgType[AcceptChannel] // Alice asked for a standard channel whereas they both support anchor outputs. - assert(accept.channelType_opt === Some(ChannelTypes.Standard)) + assert(accept.channelType_opt == Some(ChannelTypes.Standard)) bob2alice.forward(alice, accept) awaitCond(alice.stateName == WAIT_FOR_FUNDING_INTERNAL) - assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_INTERNAL].channelFeatures.channelType === ChannelTypes.Standard) + assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_INTERNAL].channelFeatures.channelType == ChannelTypes.Standard) aliceOrigin.expectNoMessage() } test("recv AcceptChannel (non-default channel type not set)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs), Tag("standard-channel-type")) { f => import f._ val accept = bob2alice.expectMsgType[AcceptChannel] - assert(accept.channelType_opt === Some(ChannelTypes.Standard)) + assert(accept.channelType_opt == Some(ChannelTypes.Standard)) // Alice asked for a standard channel whereas they both support anchor outputs. Bob doesn't support explicit channel // type negotiation so Alice needs to abort because the channel types won't match. bob2alice.forward(alice, accept.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScriptTlv(ByteVector.empty)))) @@ -159,20 +159,20 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.anchorOutputsFeeratePerKw, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, Init(bobParams.initFeatures), ChannelFlags.Private, channelConfig, ChannelTypes.AnchorOutputs) bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, bobParams, bob2alice.ref, Init(bobParams.initFeatures), channelConfig, ChannelTypes.AnchorOutputs) val open = alice2bob.expectMsgType[OpenChannel] - assert(open.channelType_opt === Some(ChannelTypes.AnchorOutputs)) + assert(open.channelType_opt == Some(ChannelTypes.AnchorOutputs)) alice2bob.forward(bob, open) val accept = bob2alice.expectMsgType[AcceptChannel] - assert(accept.channelType_opt === Some(ChannelTypes.AnchorOutputs)) + assert(accept.channelType_opt == Some(ChannelTypes.AnchorOutputs)) bob2alice.forward(alice, accept) awaitCond(alice.stateName == WAIT_FOR_FUNDING_INTERNAL) - assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_INTERNAL].channelFeatures.channelType === ChannelTypes.AnchorOutputs) + assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_INTERNAL].channelFeatures.channelType == ChannelTypes.AnchorOutputs) aliceOrigin.expectNoMessage() } test("recv AcceptChannel (invalid channel type)") { f => import f._ val accept = bob2alice.expectMsgType[AcceptChannel] - assert(accept.channelType_opt === Some(ChannelTypes.Standard)) + assert(accept.channelType_opt == Some(ChannelTypes.Standard)) val invalidAccept = accept.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScriptTlv(ByteVector.empty), ChannelTlv.ChannelTypeTlv(ChannelTypes.AnchorOutputs))) bob2alice.forward(alice, invalidAccept) alice2bob.expectMsg(Error(accept.temporaryChannelId, "invalid channel_type=anchor_outputs, expected channel_type=standard")) @@ -187,7 +187,7 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS val invalidMaxAcceptedHtlcs = 484 alice ! accept.copy(maxAcceptedHtlcs = invalidMaxAcceptedHtlcs) val error = alice2bob.expectMsgType[Error] - assert(error === Error(accept.temporaryChannelId, InvalidMaxAcceptedHtlcs(accept.temporaryChannelId, invalidMaxAcceptedHtlcs, Channel.MAX_ACCEPTED_HTLCS).getMessage)) + assert(error == Error(accept.temporaryChannelId, InvalidMaxAcceptedHtlcs(accept.temporaryChannelId, invalidMaxAcceptedHtlcs, Channel.MAX_ACCEPTED_HTLCS).getMessage)) awaitCond(alice.stateName == CLOSED) aliceOrigin.expectMsgType[Status.Failure] } @@ -199,7 +199,7 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS val lowDustLimitSatoshis = 353.sat alice ! accept.copy(dustLimitSatoshis = lowDustLimitSatoshis) val error = alice2bob.expectMsgType[Error] - assert(error === Error(accept.temporaryChannelId, DustLimitTooSmall(accept.temporaryChannelId, lowDustLimitSatoshis, Channel.MIN_DUST_LIMIT).getMessage)) + assert(error == Error(accept.temporaryChannelId, DustLimitTooSmall(accept.temporaryChannelId, lowDustLimitSatoshis, Channel.MIN_DUST_LIMIT).getMessage)) awaitCond(alice.stateName == CLOSED) aliceOrigin.expectMsgType[Status.Failure] } @@ -210,7 +210,7 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS val highDustLimitSatoshis = 2000.sat alice ! accept.copy(dustLimitSatoshis = highDustLimitSatoshis) val error = alice2bob.expectMsgType[Error] - assert(error === Error(accept.temporaryChannelId, DustLimitTooLarge(accept.temporaryChannelId, highDustLimitSatoshis, Alice.nodeParams.channelConf.maxRemoteDustLimit).getMessage)) + assert(error == Error(accept.temporaryChannelId, DustLimitTooLarge(accept.temporaryChannelId, highDustLimitSatoshis, Alice.nodeParams.channelConf.maxRemoteDustLimit).getMessage)) awaitCond(alice.stateName == CLOSED) aliceOrigin.expectMsgType[Status.Failure] } @@ -221,7 +221,7 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS val delayTooHigh = CltvExpiryDelta(10000) alice ! accept.copy(toSelfDelay = delayTooHigh) val error = alice2bob.expectMsgType[Error] - assert(error === Error(accept.temporaryChannelId, ToSelfDelayTooHigh(accept.temporaryChannelId, delayTooHigh, Alice.nodeParams.channelConf.maxToLocalDelay).getMessage)) + assert(error == Error(accept.temporaryChannelId, ToSelfDelayTooHigh(accept.temporaryChannelId, delayTooHigh, Alice.nodeParams.channelConf.maxToLocalDelay).getMessage)) awaitCond(alice.stateName == CLOSED) aliceOrigin.expectMsgType[Status.Failure] } @@ -233,7 +233,7 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS val reserveTooHigh = TestConstants.fundingSatoshis * 0.3 alice ! accept.copy(channelReserveSatoshis = reserveTooHigh) val error = alice2bob.expectMsgType[Error] - assert(error === Error(accept.temporaryChannelId, ChannelReserveTooHigh(accept.temporaryChannelId, reserveTooHigh, 0.3, 0.05).getMessage)) + assert(error == Error(accept.temporaryChannelId, ChannelReserveTooHigh(accept.temporaryChannelId, reserveTooHigh, 0.3, 0.05).getMessage)) awaitCond(alice.stateName == CLOSED) aliceOrigin.expectMsgType[Status.Failure] } @@ -244,7 +244,7 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS val reserveTooSmall = accept.dustLimitSatoshis - 1.sat alice ! accept.copy(channelReserveSatoshis = reserveTooSmall) val error = alice2bob.expectMsgType[Error] - assert(error === Error(accept.temporaryChannelId, DustLimitTooLarge(accept.temporaryChannelId, accept.dustLimitSatoshis, reserveTooSmall).getMessage)) + assert(error == Error(accept.temporaryChannelId, DustLimitTooLarge(accept.temporaryChannelId, accept.dustLimitSatoshis, reserveTooSmall).getMessage)) awaitCond(alice.stateName == CLOSED) aliceOrigin.expectMsgType[Status.Failure] } @@ -256,7 +256,7 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS val reserveTooSmall = open.dustLimitSatoshis - 1.sat alice ! accept.copy(channelReserveSatoshis = reserveTooSmall) val error = alice2bob.expectMsgType[Error] - assert(error === Error(accept.temporaryChannelId, ChannelReserveBelowOurDustLimit(accept.temporaryChannelId, reserveTooSmall, open.dustLimitSatoshis).getMessage)) + assert(error == Error(accept.temporaryChannelId, ChannelReserveBelowOurDustLimit(accept.temporaryChannelId, reserveTooSmall, open.dustLimitSatoshis).getMessage)) awaitCond(alice.stateName == CLOSED) aliceOrigin.expectMsgType[Status.Failure] } @@ -268,7 +268,7 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS val dustTooBig = open.channelReserveSatoshis + 1.sat alice ! accept.copy(dustLimitSatoshis = dustTooBig) val error = alice2bob.expectMsgType[Error] - assert(error === Error(accept.temporaryChannelId, DustLimitAboveOurChannelReserve(accept.temporaryChannelId, dustTooBig, open.channelReserveSatoshis).getMessage)) + assert(error == Error(accept.temporaryChannelId, DustLimitAboveOurChannelReserve(accept.temporaryChannelId, dustTooBig, open.channelReserveSatoshis).getMessage)) awaitCond(alice.stateName == CLOSED) aliceOrigin.expectMsgType[Status.Failure] } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForOpenChannelStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForOpenChannelStateSpec.scala index 84ea97e2ce..f4220136fd 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForOpenChannelStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForOpenChannelStateSpec.scala @@ -67,39 +67,39 @@ class WaitForOpenChannelStateSpec extends TestKitBaseClass with FixtureAnyFunSui import f._ val open = alice2bob.expectMsgType[OpenChannel] // Since https://github.com/lightningnetwork/lightning-rfc/pull/714 we must include an empty upfront_shutdown_script. - assert(open.upfrontShutdownScript_opt === Some(ByteVector.empty)) + assert(open.upfrontShutdownScript_opt == Some(ByteVector.empty)) // We always send a channel type, even for standard channels. - assert(open.channelType_opt === Some(ChannelTypes.Standard)) + assert(open.channelType_opt == Some(ChannelTypes.Standard)) alice2bob.forward(bob) awaitCond(bob.stateName == WAIT_FOR_FUNDING_CREATED) - assert(bob.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CREATED].channelFeatures.channelType === ChannelTypes.Standard) + assert(bob.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CREATED].channelFeatures.channelType == ChannelTypes.Standard) } test("recv OpenChannel (anchor outputs)", Tag(ChannelStateTestsTags.AnchorOutputs)) { f => import f._ val open = alice2bob.expectMsgType[OpenChannel] - assert(open.channelType_opt === Some(ChannelTypes.AnchorOutputs)) + assert(open.channelType_opt == Some(ChannelTypes.AnchorOutputs)) alice2bob.forward(bob) awaitCond(bob.stateName == WAIT_FOR_FUNDING_CREATED) - assert(bob.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CREATED].channelFeatures.channelType === ChannelTypes.AnchorOutputs) + assert(bob.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CREATED].channelFeatures.channelType == ChannelTypes.AnchorOutputs) } test("recv OpenChannel (anchor outputs zero fee htlc txs)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => import f._ val open = alice2bob.expectMsgType[OpenChannel] - assert(open.channelType_opt === Some(ChannelTypes.AnchorOutputsZeroFeeHtlcTx)) + assert(open.channelType_opt == Some(ChannelTypes.AnchorOutputsZeroFeeHtlcTx)) alice2bob.forward(bob) awaitCond(bob.stateName == WAIT_FOR_FUNDING_CREATED) - assert(bob.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CREATED].channelFeatures.channelType === ChannelTypes.AnchorOutputsZeroFeeHtlcTx) + assert(bob.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CREATED].channelFeatures.channelType == ChannelTypes.AnchorOutputsZeroFeeHtlcTx) } test("recv OpenChannel (non-default channel type)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs), Tag("standard-channel-type")) { f => import f._ val open = alice2bob.expectMsgType[OpenChannel] - assert(open.channelType_opt === Some(ChannelTypes.Standard)) + assert(open.channelType_opt == Some(ChannelTypes.Standard)) alice2bob.forward(bob) awaitCond(bob.stateName == WAIT_FOR_FUNDING_CREATED) - assert(bob.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CREATED].channelFeatures.channelType === ChannelTypes.Standard) + assert(bob.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CREATED].channelFeatures.channelType == ChannelTypes.Standard) } test("recv OpenChannel (invalid chain)") { f => @@ -109,7 +109,7 @@ class WaitForOpenChannelStateSpec extends TestKitBaseClass with FixtureAnyFunSui val livenetChainHash = Block.LivenetGenesisBlock.hash bob ! open.copy(chainHash = livenetChainHash) val error = bob2alice.expectMsgType[Error] - assert(error === Error(open.temporaryChannelId, InvalidChainHash(open.temporaryChannelId, Block.RegtestGenesisBlock.hash, livenetChainHash).getMessage)) + assert(error == Error(open.temporaryChannelId, InvalidChainHash(open.temporaryChannelId, Block.RegtestGenesisBlock.hash, livenetChainHash).getMessage)) awaitCond(bob.stateName == CLOSED) } @@ -120,7 +120,7 @@ class WaitForOpenChannelStateSpec extends TestKitBaseClass with FixtureAnyFunSui val announceChannel = true bob ! open.copy(fundingSatoshis = lowFunding, channelFlags = ChannelFlags(announceChannel)) val error = bob2alice.expectMsgType[Error] - assert(error === Error(open.temporaryChannelId, InvalidFundingAmount(open.temporaryChannelId, lowFunding, Bob.nodeParams.channelConf.minFundingSatoshis(announceChannel), Bob.nodeParams.channelConf.maxFundingSatoshis).getMessage)) + assert(error == Error(open.temporaryChannelId, InvalidFundingAmount(open.temporaryChannelId, lowFunding, Bob.nodeParams.channelConf.minFundingSatoshis(announceChannel), Bob.nodeParams.channelConf.maxFundingSatoshis).getMessage)) awaitCond(bob.stateName == CLOSED) } @@ -131,7 +131,7 @@ class WaitForOpenChannelStateSpec extends TestKitBaseClass with FixtureAnyFunSui val announceChannel = false bob ! open.copy(fundingSatoshis = lowFunding, channelFlags = ChannelFlags(announceChannel)) val error = bob2alice.expectMsgType[Error] - assert(error === Error(open.temporaryChannelId, InvalidFundingAmount(open.temporaryChannelId, lowFunding, Bob.nodeParams.channelConf.minFundingSatoshis(announceChannel), Bob.nodeParams.channelConf.maxFundingSatoshis).getMessage)) + assert(error == Error(open.temporaryChannelId, InvalidFundingAmount(open.temporaryChannelId, lowFunding, Bob.nodeParams.channelConf.minFundingSatoshis(announceChannel), Bob.nodeParams.channelConf.maxFundingSatoshis).getMessage)) awaitCond(bob.stateName == CLOSED) } @@ -141,7 +141,7 @@ class WaitForOpenChannelStateSpec extends TestKitBaseClass with FixtureAnyFunSui val highFundingMsat = 100000000.sat bob ! open.copy(fundingSatoshis = highFundingMsat) val error = bob2alice.expectMsgType[Error] - assert(error === Error(open.temporaryChannelId, InvalidFundingAmount(open.temporaryChannelId, highFundingMsat, Bob.nodeParams.channelConf.minFundingSatoshis(open.channelFlags.announceChannel), Bob.nodeParams.channelConf.maxFundingSatoshis).getMessage)) + assert(error == Error(open.temporaryChannelId, InvalidFundingAmount(open.temporaryChannelId, highFundingMsat, Bob.nodeParams.channelConf.minFundingSatoshis(open.channelFlags.announceChannel), Bob.nodeParams.channelConf.maxFundingSatoshis).getMessage)) awaitCond(bob.stateName == CLOSED) } @@ -151,7 +151,7 @@ class WaitForOpenChannelStateSpec extends TestKitBaseClass with FixtureAnyFunSui val highFundingSat = Bob.nodeParams.channelConf.maxFundingSatoshis + Btc(1) bob ! open.copy(fundingSatoshis = highFundingSat) val error = bob2alice.expectMsgType[Error] - assert(error === Error(open.temporaryChannelId, InvalidFundingAmount(open.temporaryChannelId, highFundingSat, Bob.nodeParams.channelConf.minFundingSatoshis(open.channelFlags.announceChannel), Bob.nodeParams.channelConf.maxFundingSatoshis).getMessage)) + assert(error == Error(open.temporaryChannelId, InvalidFundingAmount(open.temporaryChannelId, highFundingSat, Bob.nodeParams.channelConf.minFundingSatoshis(open.channelFlags.announceChannel), Bob.nodeParams.channelConf.maxFundingSatoshis).getMessage)) } test("recv OpenChannel (invalid max accepted htlcs)") { f => @@ -160,7 +160,7 @@ class WaitForOpenChannelStateSpec extends TestKitBaseClass with FixtureAnyFunSui val invalidMaxAcceptedHtlcs = Channel.MAX_ACCEPTED_HTLCS + 1 bob ! open.copy(maxAcceptedHtlcs = invalidMaxAcceptedHtlcs) val error = bob2alice.expectMsgType[Error] - assert(error === Error(open.temporaryChannelId, InvalidMaxAcceptedHtlcs(open.temporaryChannelId, invalidMaxAcceptedHtlcs, Channel.MAX_ACCEPTED_HTLCS).getMessage)) + assert(error == Error(open.temporaryChannelId, InvalidMaxAcceptedHtlcs(open.temporaryChannelId, invalidMaxAcceptedHtlcs, Channel.MAX_ACCEPTED_HTLCS).getMessage)) awaitCond(bob.stateName == CLOSED) } @@ -170,7 +170,7 @@ class WaitForOpenChannelStateSpec extends TestKitBaseClass with FixtureAnyFunSui val invalidPushMsat = 100000000000L.msat bob ! open.copy(pushMsat = invalidPushMsat) val error = bob2alice.expectMsgType[Error] - assert(error === Error(open.temporaryChannelId, InvalidPushAmount(open.temporaryChannelId, invalidPushMsat, open.fundingSatoshis.toMilliSatoshi).getMessage)) + assert(error == Error(open.temporaryChannelId, InvalidPushAmount(open.temporaryChannelId, invalidPushMsat, open.fundingSatoshis.toMilliSatoshi).getMessage)) awaitCond(bob.stateName == CLOSED) } @@ -180,7 +180,7 @@ class WaitForOpenChannelStateSpec extends TestKitBaseClass with FixtureAnyFunSui val delayTooHigh = CltvExpiryDelta(10000) bob ! open.copy(toSelfDelay = delayTooHigh) val error = bob2alice.expectMsgType[Error] - assert(error === Error(open.temporaryChannelId, ToSelfDelayTooHigh(open.temporaryChannelId, delayTooHigh, Alice.nodeParams.channelConf.maxToLocalDelay).getMessage)) + assert(error == Error(open.temporaryChannelId, ToSelfDelayTooHigh(open.temporaryChannelId, delayTooHigh, Alice.nodeParams.channelConf.maxToLocalDelay).getMessage)) awaitCond(bob.stateName == CLOSED) } @@ -191,7 +191,7 @@ class WaitForOpenChannelStateSpec extends TestKitBaseClass with FixtureAnyFunSui val reserveTooHigh = TestConstants.fundingSatoshis * 0.3 bob ! open.copy(channelReserveSatoshis = reserveTooHigh) val error = bob2alice.expectMsgType[Error] - assert(error === Error(open.temporaryChannelId, ChannelReserveTooHigh(open.temporaryChannelId, reserveTooHigh, 0.3, 0.05).getMessage)) + assert(error == Error(open.temporaryChannelId, ChannelReserveTooHigh(open.temporaryChannelId, reserveTooHigh, 0.3, 0.05).getMessage)) awaitCond(bob.stateName == CLOSED) } @@ -203,7 +203,7 @@ class WaitForOpenChannelStateSpec extends TestKitBaseClass with FixtureAnyFunSui bob ! open.copy(feeratePerKw = tinyFee) val error = bob2alice.expectMsgType[Error] // we check that the error uses the temporary channel id - assert(error === Error(open.temporaryChannelId, "local/remote feerates are too different: remoteFeeratePerKw=253 localFeeratePerKw=10000")) + assert(error == Error(open.temporaryChannelId, "local/remote feerates are too different: remoteFeeratePerKw=253 localFeeratePerKw=10000")) awaitCond(bob.stateName == CLOSED) } @@ -215,7 +215,7 @@ class WaitForOpenChannelStateSpec extends TestKitBaseClass with FixtureAnyFunSui bob ! open.copy(feeratePerKw = tinyFee) val error = bob2alice.expectMsgType[Error] // we check that the error uses the temporary channel id - assert(error === Error(open.temporaryChannelId, "remote fee rate is too small: remoteFeeratePerKw=252")) + assert(error == Error(open.temporaryChannelId, "remote fee rate is too small: remoteFeeratePerKw=252")) awaitCond(bob.stateName == CLOSED) } @@ -226,7 +226,7 @@ class WaitForOpenChannelStateSpec extends TestKitBaseClass with FixtureAnyFunSui bob ! open.copy(channelReserveSatoshis = reserveTooSmall) val error = bob2alice.expectMsgType[Error] // we check that the error uses the temporary channel id - assert(error === Error(open.temporaryChannelId, DustLimitTooLarge(open.temporaryChannelId, open.dustLimitSatoshis, reserveTooSmall).getMessage)) + assert(error == Error(open.temporaryChannelId, DustLimitTooLarge(open.temporaryChannelId, open.dustLimitSatoshis, reserveTooSmall).getMessage)) awaitCond(bob.stateName == CLOSED) } @@ -236,7 +236,7 @@ class WaitForOpenChannelStateSpec extends TestKitBaseClass with FixtureAnyFunSui val dustLimitTooHigh = 2000.sat bob ! open.copy(dustLimitSatoshis = dustLimitTooHigh) val error = bob2alice.expectMsgType[Error] - assert(error === Error(open.temporaryChannelId, DustLimitTooLarge(open.temporaryChannelId, dustLimitTooHigh, Bob.nodeParams.channelConf.maxRemoteDustLimit).getMessage)) + assert(error == Error(open.temporaryChannelId, DustLimitTooLarge(open.temporaryChannelId, dustLimitTooHigh, Bob.nodeParams.channelConf.maxRemoteDustLimit).getMessage)) awaitCond(bob.stateName == CLOSED) } @@ -248,7 +248,7 @@ class WaitForOpenChannelStateSpec extends TestKitBaseClass with FixtureAnyFunSui bob ! open.copy(fundingSatoshis = fundingSatoshis, pushMsat = pushMsat) val error = bob2alice.expectMsgType[Error] // we check that the error uses the temporary channel id - assert(error === Error(open.temporaryChannelId, ChannelReserveNotMet(open.temporaryChannelId, pushMsat, (open.channelReserveSatoshis - 1.sat).toMilliSatoshi, open.channelReserveSatoshis).getMessage)) + assert(error == Error(open.temporaryChannelId, ChannelReserveNotMet(open.temporaryChannelId, pushMsat, (open.channelReserveSatoshis - 1.sat).toMilliSatoshi, open.channelReserveSatoshis).getMessage)) awaitCond(bob.stateName == CLOSED) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingCreatedStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingCreatedStateSpec.scala index 3c8232860c..84bb3b43e1 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingCreatedStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingCreatedStateSpec.scala @@ -86,7 +86,7 @@ class WaitForFundingCreatedStateSpec extends TestKitBaseClass with FixtureAnyFun bob2blockchain.expectMsgType[TxPublisher.SetChannelId] bob2blockchain.expectMsgType[WatchFundingSpent] val watchConfirmed = bob2blockchain.expectMsgType[WatchFundingConfirmed] - assert(watchConfirmed.minDepth === Alice.nodeParams.channelConf.minDepthBlocks) + assert(watchConfirmed.minDepth == Alice.nodeParams.channelConf.minDepthBlocks) } test("recv FundingCreated (wumbo)", Tag(ChannelStateTestsTags.Wumbo)) { f => @@ -111,7 +111,7 @@ class WaitForFundingCreatedStateSpec extends TestKitBaseClass with FixtureAnyFun val fundingCreated = alice2bob.expectMsgType[FundingCreated] alice2bob.forward(bob) val error = bob2alice.expectMsgType[Error] - assert(error === Error(fundingCreated.temporaryChannelId, s"can't pay the fee: missing=${-missing} reserve=$reserve fees=$fees")) + assert(error == Error(fundingCreated.temporaryChannelId, s"can't pay the fee: missing=${-missing} reserve=$reserve fees=$fees")) awaitCond(bob.stateName == CLOSED) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingSignedStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingSignedStateSpec.scala index 2a8a475072..9596b3c33b 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingSignedStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingSignedStateSpec.scala @@ -88,10 +88,10 @@ class WaitForFundingSignedStateSpec extends TestKitBaseClass with FixtureAnyFunS awaitCond(alice.stateName == WAIT_FOR_FUNDING_CONFIRMED) val fundingTxId = alice2blockchain.expectMsgType[WatchFundingSpent].txId val txPublished = listener.expectMsgType[TransactionPublished] - assert(txPublished.tx.txid === fundingTxId) + assert(txPublished.tx.txid == fundingTxId) assert(txPublished.miningFee > 0.sat) val watchConfirmed = alice2blockchain.expectMsgType[WatchFundingConfirmed] - assert(watchConfirmed.minDepth === Alice.nodeParams.channelConf.minDepthBlocks) + assert(watchConfirmed.minDepth == Alice.nodeParams.channelConf.minDepthBlocks) aliceOrigin.expectMsgType[ChannelOpenResponse.ChannelOpened] } @@ -103,7 +103,7 @@ class WaitForFundingSignedStateSpec extends TestKitBaseClass with FixtureAnyFunS alice2blockchain.expectMsgType[WatchFundingSpent] val watchConfirmed = alice2blockchain.expectMsgType[WatchFundingConfirmed] // when we are funder, we keep our regular min depth even for wumbo channels - assert(watchConfirmed.minDepth === Alice.nodeParams.channelConf.minDepthBlocks) + assert(watchConfirmed.minDepth == Alice.nodeParams.channelConf.minDepthBlocks) aliceOrigin.expectMsgType[ChannelOpenResponse.ChannelOpened] } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingConfirmedStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingConfirmedStateSpec.scala index 3bb70833cb..149f4d9255 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingConfirmedStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingConfirmedStateSpec.scala @@ -89,9 +89,9 @@ class WaitForFundingConfirmedStateSpec extends TestKitBaseClass with FixtureAnyF val fundingTx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CONFIRMED].fundingTx.get bob ! WatchFundingConfirmedTriggered(BlockHeight(42000), 42, fundingTx) val txPublished = listener.expectMsgType[TransactionPublished] - assert(txPublished.tx === fundingTx) - assert(txPublished.miningFee === 0.sat) // bob is fundee - assert(listener.expectMsgType[TransactionConfirmed].tx === fundingTx) + assert(txPublished.tx == fundingTx) + assert(txPublished.miningFee == 0.sat) // bob is fundee + assert(listener.expectMsgType[TransactionConfirmed].tx == fundingTx) val msg = bob2alice.expectMsgType[FundingLocked] bob2alice.forward(alice) awaitCond(alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CONFIRMED].deferred.contains(msg)) @@ -106,7 +106,7 @@ class WaitForFundingConfirmedStateSpec extends TestKitBaseClass with FixtureAnyF alice.underlying.system.eventStream.subscribe(listener.ref, classOf[TransactionConfirmed]) val fundingTx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CONFIRMED].fundingTx.get alice ! WatchFundingConfirmedTriggered(BlockHeight(42000), 42, fundingTx) - assert(listener.expectMsgType[TransactionConfirmed].tx === fundingTx) + assert(listener.expectMsgType[TransactionConfirmed].tx == fundingTx) awaitCond(alice.stateName == WAIT_FOR_FUNDING_LOCKED) alice2blockchain.expectMsgType[WatchFundingLost] alice2bob.expectMsgType[FundingLocked] @@ -182,7 +182,7 @@ class WaitForFundingConfirmedStateSpec extends TestKitBaseClass with FixtureAnyF awaitCond(bob.stateName == OFFLINE) // We reset the waiting period to the current block height when starting up after updating eclair. val currentBlockHeight = bob.underlyingActor.nodeParams.currentBlockHeight - assert(bob.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CONFIRMED].waitingSince === currentBlockHeight) + assert(bob.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CONFIRMED].waitingSince == currentBlockHeight) } test("recv WatchFundingSpentTriggered (remote commit)") { f => @@ -191,7 +191,7 @@ class WaitForFundingConfirmedStateSpec extends TestKitBaseClass with FixtureAnyF val tx = bob.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CONFIRMED].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx alice ! WatchFundingSpentTriggered(tx) alice2blockchain.expectMsgType[TxPublisher.PublishTx] - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === tx.txid) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == tx.txid) awaitCond(alice.stateName == CLOSING) } @@ -200,7 +200,7 @@ class WaitForFundingConfirmedStateSpec extends TestKitBaseClass with FixtureAnyF val tx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CONFIRMED].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx alice ! WatchFundingSpentTriggered(Transaction(0, Nil, Nil, 0)) alice2bob.expectMsgType[Error] - assert(alice2blockchain.expectMsgType[TxPublisher.PublishFinalTx].tx.txid === tx.txid) + assert(alice2blockchain.expectMsgType[TxPublisher.PublishFinalTx].tx.txid == tx.txid) awaitCond(alice.stateName == ERR_INFORMATION_LEAK) } @@ -209,9 +209,9 @@ class WaitForFundingConfirmedStateSpec extends TestKitBaseClass with FixtureAnyF val tx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CONFIRMED].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx alice ! Error(ByteVector32.Zeroes, "oops") awaitCond(alice.stateName == CLOSING) - assert(alice2blockchain.expectMsgType[TxPublisher.PublishFinalTx].tx.txid === tx.txid) + assert(alice2blockchain.expectMsgType[TxPublisher.PublishFinalTx].tx.txid == tx.txid) alice2blockchain.expectMsgType[TxPublisher.PublishTx] // claim-main-delayed - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === tx.txid) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == tx.txid) } test("recv Error (nothing at stake)", Tag(ChannelStateTestsTags.NoPushMsat)) { f => @@ -235,9 +235,9 @@ class WaitForFundingConfirmedStateSpec extends TestKitBaseClass with FixtureAnyF val tx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CONFIRMED].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx alice ! CMD_FORCECLOSE(sender.ref) awaitCond(alice.stateName == CLOSING) - assert(alice2blockchain.expectMsgType[TxPublisher.PublishFinalTx].tx.txid === tx.txid) + assert(alice2blockchain.expectMsgType[TxPublisher.PublishFinalTx].tx.txid == tx.txid) alice2blockchain.expectMsgType[TxPublisher.PublishTx] // claim-main-delayed - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === tx.txid) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == tx.txid) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingLockedStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingLockedStateSpec.scala index a1bb4b4098..01345af1e2 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingLockedStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingLockedStateSpec.scala @@ -89,8 +89,8 @@ class WaitForFundingLockedStateSpec extends TestKitBaseClass with FixtureAnyFunS bob2alice.forward(alice) awaitCond(alice.stateName == NORMAL) val initialChannelUpdate = alice.stateData.asInstanceOf[DATA_NORMAL].channelUpdate - assert(initialChannelUpdate.feeBaseMsat === relayFees.feeBase) - assert(initialChannelUpdate.feeProportionalMillionths === relayFees.feeProportionalMillionths) + assert(initialChannelUpdate.feeBaseMsat == relayFees.feeBase) + assert(initialChannelUpdate.feeProportionalMillionths == relayFees.feeProportionalMillionths) bob2alice.expectNoMessage(200 millis) } @@ -100,7 +100,7 @@ class WaitForFundingLockedStateSpec extends TestKitBaseClass with FixtureAnyFunS val tx = bob.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_LOCKED].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx alice ! WatchFundingSpentTriggered(tx) alice2blockchain.expectMsgType[TxPublisher.PublishTx] - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === tx.txid) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == tx.txid) awaitCond(alice.stateName == CLOSING) } @@ -109,7 +109,7 @@ class WaitForFundingLockedStateSpec extends TestKitBaseClass with FixtureAnyFunS val tx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_LOCKED].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx alice ! WatchFundingSpentTriggered(Transaction(0, Nil, Nil, 0)) alice2bob.expectMsgType[Error] - assert(alice2blockchain.expectMsgType[TxPublisher.PublishFinalTx].tx.txid === tx.txid) + assert(alice2blockchain.expectMsgType[TxPublisher.PublishFinalTx].tx.txid == tx.txid) alice2blockchain.expectMsgType[TxPublisher.PublishTx] awaitCond(alice.stateName == ERR_INFORMATION_LEAK) } @@ -119,9 +119,9 @@ class WaitForFundingLockedStateSpec extends TestKitBaseClass with FixtureAnyFunS val tx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_LOCKED].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx alice ! Error(ByteVector32.Zeroes, "oops") awaitCond(alice.stateName == CLOSING) - assert(alice2blockchain.expectMsgType[TxPublisher.PublishFinalTx].tx.txid === tx.txid) + assert(alice2blockchain.expectMsgType[TxPublisher.PublishFinalTx].tx.txid == tx.txid) alice2blockchain.expectMsgType[TxPublisher.PublishTx] - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === tx.txid) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == tx.txid) } test("recv Error (nothing at stake)", Tag(ChannelStateTestsTags.NoPushMsat)) { f => @@ -129,8 +129,8 @@ class WaitForFundingLockedStateSpec extends TestKitBaseClass with FixtureAnyFunS val tx = bob.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_LOCKED].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx bob ! Error(ByteVector32.Zeroes, "funding double-spent") awaitCond(bob.stateName == CLOSING) - assert(bob2blockchain.expectMsgType[TxPublisher.PublishFinalTx].tx.txid === tx.txid) - assert(bob2blockchain.expectMsgType[WatchTxConfirmed].txId === tx.txid) + assert(bob2blockchain.expectMsgType[TxPublisher.PublishFinalTx].tx.txid == tx.txid) + assert(bob2blockchain.expectMsgType[WatchTxConfirmed].txId == tx.txid) } test("recv CMD_CLOSE") { f => @@ -147,8 +147,8 @@ class WaitForFundingLockedStateSpec extends TestKitBaseClass with FixtureAnyFunS val tx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_LOCKED].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx alice ! CMD_FORCECLOSE(sender.ref) awaitCond(alice.stateName == CLOSING) - assert(alice2blockchain.expectMsgType[TxPublisher.PublishFinalTx].tx.txid === tx.txid) + assert(alice2blockchain.expectMsgType[TxPublisher.PublishFinalTx].tx.txid == tx.txid) alice2blockchain.expectMsgType[TxPublisher.PublishTx] - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === tx.txid) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == tx.txid) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala index d0bc69f39c..bafc54d154 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala @@ -183,7 +183,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with import f._ val sender = TestProbe() // Alice has a minimum set to 0 msat (which should be invalid, but may mislead Bob into relaying 0-value HTLCs which is forbidden by the spec). - assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localParams.htlcMinimum === 0.msat) + assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localParams.htlcMinimum == 0.msat) val initialState = bob.stateData.asInstanceOf[DATA_NORMAL] val add = CMD_ADD_HTLC(sender.ref, 0 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, localOrigin(sender.ref)) bob ! add @@ -244,7 +244,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val sender = TestProbe() addHtlc(758640000 msat, alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) - assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.availableBalanceForSend === 0.msat) + assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.availableBalanceForSend == 0.msat) // actual test begins // at this point alice has the minimal amount to sustain a channel @@ -302,8 +302,8 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with import f._ val sender = TestProbe() val initialState = bob.stateData.asInstanceOf[DATA_NORMAL] - assert(initialState.commitments.localParams.maxHtlcValueInFlightMsat === UInt64.MaxValue) - assert(initialState.commitments.remoteParams.maxHtlcValueInFlightMsat === UInt64(150000000)) + assert(initialState.commitments.localParams.maxHtlcValueInFlightMsat == UInt64.MaxValue) + assert(initialState.commitments.remoteParams.maxHtlcValueInFlightMsat == UInt64(150000000)) val add = CMD_ADD_HTLC(sender.ref, 151000000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, localOrigin(sender.ref)) bob ! add val error = HtlcValueTooHighInFlight(channelId(bob), maximum = 150000000, actual = 151000000 msat) @@ -315,8 +315,8 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with import f._ val sender = TestProbe() val initialState = bob.stateData.asInstanceOf[DATA_NORMAL] - assert(initialState.commitments.localParams.maxHtlcValueInFlightMsat === UInt64.MaxValue) - assert(initialState.commitments.remoteParams.maxHtlcValueInFlightMsat === UInt64(150000000)) + assert(initialState.commitments.localParams.maxHtlcValueInFlightMsat == UInt64.MaxValue) + assert(initialState.commitments.remoteParams.maxHtlcValueInFlightMsat == UInt64(150000000)) val add = CMD_ADD_HTLC(sender.ref, 75500000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, localOrigin(sender.ref)) bob ! add sender.expectMsgType[RES_SUCCESS[CMD_ADD_HTLC]] @@ -332,8 +332,8 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with import f._ val sender = TestProbe() val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] - assert(initialState.commitments.localParams.maxHtlcValueInFlightMsat === UInt64(150000000)) - assert(initialState.commitments.remoteParams.maxHtlcValueInFlightMsat === UInt64.MaxValue) + assert(initialState.commitments.localParams.maxHtlcValueInFlightMsat == UInt64(150000000)) + assert(initialState.commitments.remoteParams.maxHtlcValueInFlightMsat == UInt64.MaxValue) val add = CMD_ADD_HTLC(sender.ref, 151000000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, localOrigin(sender.ref)) alice ! add val error = HtlcValueTooHighInFlight(channelId(alice), maximum = 150000000, actual = 151000000 msat) @@ -345,8 +345,8 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with import f._ val sender = TestProbe() val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] - assert(initialState.commitments.localParams.maxAcceptedHtlcs === 100) - assert(initialState.commitments.remoteParams.maxAcceptedHtlcs === 30) // Bob accepts a maximum of 30 htlcs + assert(initialState.commitments.localParams.maxAcceptedHtlcs == 100) + assert(initialState.commitments.remoteParams.maxAcceptedHtlcs == 30) // Bob accepts a maximum of 30 htlcs for (_ <- 0 until 30) { alice ! CMD_ADD_HTLC(sender.ref, 10000000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, localOrigin(sender.ref)) sender.expectMsgType[RES_SUCCESS[CMD_ADD_HTLC]] @@ -363,8 +363,8 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with import f._ val sender = TestProbe() val initialState = bob.stateData.asInstanceOf[DATA_NORMAL] - assert(initialState.commitments.localParams.maxAcceptedHtlcs === 30) // Bob accepts a maximum of 30 htlcs - assert(initialState.commitments.remoteParams.maxAcceptedHtlcs === 100) // Alice accepts more, but Bob will stop at 30 HTLCs + assert(initialState.commitments.localParams.maxAcceptedHtlcs == 30) // Bob accepts a maximum of 30 htlcs + assert(initialState.commitments.remoteParams.maxAcceptedHtlcs == 100) // Alice accepts more, but Bob will stop at 30 HTLCs for (_ <- 0 until 30) { bob ! CMD_ADD_HTLC(sender.ref, 500000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, localOrigin(sender.ref)) sender.expectMsgType[RES_SUCCESS[CMD_ADD_HTLC]] @@ -382,11 +382,11 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val sender = TestProbe() val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] val aliceCommitments = initialState.commitments - assert(alice.underlyingActor.nodeParams.onChainFeeConf.feerateToleranceFor(bob.underlyingActor.nodeParams.nodeId).dustTolerance.maxExposure === 25_000.sat) - assert(Transactions.offeredHtlcTrimThreshold(aliceCommitments.localParams.dustLimit, aliceCommitments.localCommit.spec, aliceCommitments.commitmentFormat) === 7730.sat) - assert(Transactions.receivedHtlcTrimThreshold(aliceCommitments.localParams.dustLimit, aliceCommitments.localCommit.spec, aliceCommitments.commitmentFormat) === 8130.sat) - assert(Transactions.offeredHtlcTrimThreshold(aliceCommitments.remoteParams.dustLimit, aliceCommitments.localCommit.spec, aliceCommitments.commitmentFormat) === 7630.sat) - assert(Transactions.receivedHtlcTrimThreshold(aliceCommitments.remoteParams.dustLimit, aliceCommitments.localCommit.spec, aliceCommitments.commitmentFormat) === 8030.sat) + assert(alice.underlyingActor.nodeParams.onChainFeeConf.feerateToleranceFor(bob.underlyingActor.nodeParams.nodeId).dustTolerance.maxExposure == 25_000.sat) + assert(Transactions.offeredHtlcTrimThreshold(aliceCommitments.localParams.dustLimit, aliceCommitments.localCommit.spec, aliceCommitments.commitmentFormat) == 7730.sat) + assert(Transactions.receivedHtlcTrimThreshold(aliceCommitments.localParams.dustLimit, aliceCommitments.localCommit.spec, aliceCommitments.commitmentFormat) == 8130.sat) + assert(Transactions.offeredHtlcTrimThreshold(aliceCommitments.remoteParams.dustLimit, aliceCommitments.localCommit.spec, aliceCommitments.commitmentFormat) == 7630.sat) + assert(Transactions.receivedHtlcTrimThreshold(aliceCommitments.remoteParams.dustLimit, aliceCommitments.localCommit.spec, aliceCommitments.commitmentFormat) == 8030.sat) // Alice sends HTLCs to Bob that add 10 000 sat to the dust exposure: addHtlc(500.sat.toMilliSatoshi, alice, bob, alice2bob, bob2alice) // dust htlc @@ -423,7 +423,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with import f._ val sender = TestProbe() val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] - assert(alice.underlyingActor.nodeParams.onChainFeeConf.feerateToleranceFor(bob.underlyingActor.nodeParams.nodeId).dustTolerance.maxExposure === 25_000.sat) + assert(alice.underlyingActor.nodeParams.onChainFeeConf.feerateToleranceFor(bob.underlyingActor.nodeParams.nodeId).dustTolerance.maxExposure == 25_000.sat) // Alice sends HTLCs to Bob that add 20 000 sat to the dust exposure. // She signs them but Bob doesn't answer yet. @@ -449,9 +449,9 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with import f._ val sender = TestProbe() val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] - assert(alice.underlyingActor.nodeParams.onChainFeeConf.feerateToleranceFor(bob.underlyingActor.nodeParams.nodeId).dustTolerance.maxExposure === 25_000.sat) - assert(alice.underlyingActor.nodeParams.channelConf.dustLimit === 1100.sat) - assert(bob.underlyingActor.nodeParams.channelConf.dustLimit === 1000.sat) + assert(alice.underlyingActor.nodeParams.onChainFeeConf.feerateToleranceFor(bob.underlyingActor.nodeParams.nodeId).dustTolerance.maxExposure == 25_000.sat) + assert(alice.underlyingActor.nodeParams.channelConf.dustLimit == 1100.sat) + assert(bob.underlyingActor.nodeParams.channelConf.dustLimit == 1000.sat) // Alice sends HTLCs to Bob that add 21 000 sat to the dust exposure. // She signs them but Bob doesn't answer yet. @@ -473,9 +473,9 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with import f._ val sender = TestProbe() val initialState = bob.stateData.asInstanceOf[DATA_NORMAL] - assert(bob.underlyingActor.nodeParams.onChainFeeConf.feerateToleranceFor(alice.underlyingActor.nodeParams.nodeId).dustTolerance.maxExposure === 30_000.sat) - assert(alice.underlyingActor.nodeParams.channelConf.dustLimit === 1100.sat) - assert(bob.underlyingActor.nodeParams.channelConf.dustLimit === 1000.sat) + assert(bob.underlyingActor.nodeParams.onChainFeeConf.feerateToleranceFor(alice.underlyingActor.nodeParams.nodeId).dustTolerance.maxExposure == 30_000.sat) + assert(alice.underlyingActor.nodeParams.channelConf.dustLimit == 1100.sat) + assert(bob.underlyingActor.nodeParams.channelConf.dustLimit == 1000.sat) // Bob sends HTLCs to Alice that add 21 000 sat to the dust exposure. // He signs them but Alice doesn't answer yet. @@ -596,9 +596,9 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with bob ! htlc.copy(id = 3) bob ! htlc.copy(id = 42) val error = bob2alice.expectMsgType[Error] - assert(new String(error.data.toArray) === UnexpectedHtlcId(channelId(bob), expected = 4, actual = 42).getMessage) + assert(new String(error.data.toArray) == UnexpectedHtlcId(channelId(bob), expected = 4, actual = 42).getMessage) awaitCond(bob.stateName == CLOSING) - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid === tx.txid) + assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == tx.txid) bob2blockchain.expectMsgType[PublishTx] bob2blockchain.expectMsgType[WatchTxConfirmed] } @@ -609,11 +609,11 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val htlc = UpdateAddHtlc(ByteVector32.Zeroes, 0, 150 msat, randomBytes32(), cltvExpiry = CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket) alice2bob.forward(bob, htlc) val error = bob2alice.expectMsgType[Error] - assert(new String(error.data.toArray) === HtlcValueTooSmall(channelId(bob), minimum = 1000 msat, actual = 150 msat).getMessage) + assert(new String(error.data.toArray) == HtlcValueTooSmall(channelId(bob), minimum = 1000 msat, actual = 150 msat).getMessage) awaitCond(bob.stateName == CLOSING) // channel should be advertised as down - assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId === bob.stateData.asInstanceOf[DATA_CLOSING].channelId) - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid === tx.txid) + assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId == bob.stateData.asInstanceOf[DATA_CLOSING].channelId) + assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == tx.txid) bob2blockchain.expectMsgType[PublishTx] bob2blockchain.expectMsgType[WatchTxConfirmed] } @@ -624,11 +624,11 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val htlc = UpdateAddHtlc(ByteVector32.Zeroes, 0, MilliSatoshi(Long.MaxValue), randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket) alice2bob.forward(bob, htlc) val error = bob2alice.expectMsgType[Error] - assert(new String(error.data.toArray) === InsufficientFunds(channelId(bob), amount = MilliSatoshi(Long.MaxValue), missing = 9223372036083735L sat, reserve = 20000 sat, fees = 8960 sat).getMessage) + assert(new String(error.data.toArray) == InsufficientFunds(channelId(bob), amount = MilliSatoshi(Long.MaxValue), missing = 9223372036083735L sat, reserve = 20000 sat, fees = 8960 sat).getMessage) awaitCond(bob.stateName == CLOSING) // channel should be advertised as down - assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId === bob.stateData.asInstanceOf[DATA_CLOSING].channelId) - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid === tx.txid) + assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId == bob.stateData.asInstanceOf[DATA_CLOSING].channelId) + assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == tx.txid) bob2blockchain.expectMsgType[PublishTx] bob2blockchain.expectMsgType[WatchTxConfirmed] } @@ -640,11 +640,11 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice2bob.forward(bob, UpdateAddHtlc(ByteVector32.Zeroes, 1, 300000000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket)) alice2bob.forward(bob, UpdateAddHtlc(ByteVector32.Zeroes, 2, 100000000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket)) val error = bob2alice.expectMsgType[Error] - assert(new String(error.data.toArray) === InsufficientFunds(channelId(bob), amount = 100000000 msat, missing = 24760 sat, reserve = 20000 sat, fees = 4760 sat).getMessage) + assert(new String(error.data.toArray) == InsufficientFunds(channelId(bob), amount = 100000000 msat, missing = 24760 sat, reserve = 20000 sat, fees = 4760 sat).getMessage) awaitCond(bob.stateName == CLOSING) // channel should be advertised as down - assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId === bob.stateData.asInstanceOf[DATA_CLOSING].channelId) - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid === tx.txid) + assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId == bob.stateData.asInstanceOf[DATA_CLOSING].channelId) + assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == tx.txid) } test("recv UpdateAddHtlc (insufficient funds w/ pending htlcs 1/2)") { f => @@ -655,11 +655,11 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice2bob.forward(bob, UpdateAddHtlc(ByteVector32.Zeroes, 2, 167600000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket)) alice2bob.forward(bob, UpdateAddHtlc(ByteVector32.Zeroes, 3, 10000000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket)) val error = bob2alice.expectMsgType[Error] - assert(new String(error.data.toArray) === InsufficientFunds(channelId(bob), amount = 10000000 msat, missing = 11720 sat, reserve = 20000 sat, fees = 14120 sat).getMessage) + assert(new String(error.data.toArray) == InsufficientFunds(channelId(bob), amount = 10000000 msat, missing = 11720 sat, reserve = 20000 sat, fees = 14120 sat).getMessage) awaitCond(bob.stateName == CLOSING) // channel should be advertised as down - assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId === bob.stateData.asInstanceOf[DATA_CLOSING].channelId) - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid === tx.txid) + assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId == bob.stateData.asInstanceOf[DATA_CLOSING].channelId) + assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == tx.txid) bob2blockchain.expectMsgType[PublishTx] bob2blockchain.expectMsgType[WatchTxConfirmed] } @@ -671,11 +671,11 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice2bob.forward(bob, UpdateAddHtlc(ByteVector32.Zeroes, 1, 300000000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket)) alice2bob.forward(bob, UpdateAddHtlc(ByteVector32.Zeroes, 2, 500000000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket)) val error = bob2alice.expectMsgType[Error] - assert(new String(error.data.toArray) === InsufficientFunds(channelId(bob), amount = 500000000 msat, missing = 332400 sat, reserve = 20000 sat, fees = 12400 sat).getMessage) + assert(new String(error.data.toArray) == InsufficientFunds(channelId(bob), amount = 500000000 msat, missing = 332400 sat, reserve = 20000 sat, fees = 12400 sat).getMessage) awaitCond(bob.stateName == CLOSING) // channel should be advertised as down - assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId === bob.stateData.asInstanceOf[DATA_CLOSING].channelId) - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid === tx.txid) + assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId == bob.stateData.asInstanceOf[DATA_CLOSING].channelId) + assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == tx.txid) bob2blockchain.expectMsgType[PublishTx] bob2blockchain.expectMsgType[WatchTxConfirmed] } @@ -685,11 +685,11 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val tx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx alice2bob.forward(alice, UpdateAddHtlc(ByteVector32.Zeroes, 0, 151000000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket)) val error = alice2bob.expectMsgType[Error] - assert(new String(error.data.toArray) === HtlcValueTooHighInFlight(channelId(alice), maximum = 150000000, actual = 151000000 msat).getMessage) + assert(new String(error.data.toArray) == HtlcValueTooHighInFlight(channelId(alice), maximum = 150000000, actual = 151000000 msat).getMessage) awaitCond(alice.stateName == CLOSING) // channel should be advertised as down - assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId === alice.stateData.asInstanceOf[DATA_CLOSING].channelId) - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid === tx.txid) + assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId == alice.stateData.asInstanceOf[DATA_CLOSING].channelId) + assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid == tx.txid) alice2blockchain.expectMsgType[PublishTx] alice2blockchain.expectMsgType[WatchTxConfirmed] } @@ -703,11 +703,11 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with } alice2bob.forward(bob, UpdateAddHtlc(ByteVector32.Zeroes, 30, 1000000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket)) val error = bob2alice.expectMsgType[Error] - assert(new String(error.data.toArray) === TooManyAcceptedHtlcs(channelId(bob), maximum = 30).getMessage) + assert(new String(error.data.toArray) == TooManyAcceptedHtlcs(channelId(bob), maximum = 30).getMessage) awaitCond(bob.stateName == CLOSING) // channel should be advertised as down - assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId === bob.stateData.asInstanceOf[DATA_CLOSING].channelId) - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid === tx.txid) + assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId == bob.stateData.asInstanceOf[DATA_CLOSING].channelId) + assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == tx.txid) bob2blockchain.expectMsgType[PublishTx] bob2blockchain.expectMsgType[WatchTxConfirmed] } @@ -795,8 +795,8 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // actual test starts here crossSign(alice, bob, alice2bob, bob2alice) // depending on who starts signing first, there will be one or two commitments because both sides have changes - assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.index === 2) - assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.index === 3) + assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.index == 2) + assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.index == 3) assert(alice.underlyingActor.nodeParams.db.channels.listHtlcInfos(alice.stateData.asInstanceOf[DATA_NORMAL].channelId, 1).size == 0) assert(alice.underlyingActor.nodeParams.db.channels.listHtlcInfos(alice.stateData.asInstanceOf[DATA_NORMAL].channelId, 2).size == 2) assert(alice.underlyingActor.nodeParams.db.channels.listHtlcInfos(alice.stateData.asInstanceOf[DATA_NORMAL].channelId, 3).size == 4) @@ -824,7 +824,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with awaitCond(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.htlcTxsAndRemoteSigs.size == htlcCount) val htlcTxs = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.htlcTxsAndRemoteSigs val amounts = htlcTxs.map(_.htlcTx.tx.txOut.head.amount.toLong) - assert(amounts === amounts.sorted) + assert(amounts == amounts.sorted) } test("recv CMD_SIGN (no changes)") { f => @@ -844,12 +844,12 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice2bob.expectMsgType[CommitSig] awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.remoteNextCommitInfo.isLeft) val waitForRevocation = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.remoteNextCommitInfo.left.toOption.get - assert(waitForRevocation.reSignAsap === false) + assert(waitForRevocation.reSignAsap == false) // actual test starts here alice ! CMD_SIGN() sender.expectNoMessage(300 millis) - assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.remoteNextCommitInfo === Left(waitForRevocation)) + assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.remoteNextCommitInfo == Left(waitForRevocation)) } test("recv CMD_SIGN (while waiting for RevokeAndAck (with pending changes)") { f => @@ -861,13 +861,13 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice2bob.expectMsgType[CommitSig] awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.remoteNextCommitInfo.isLeft) val waitForRevocation = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.remoteNextCommitInfo.left.toOption.get - assert(waitForRevocation.reSignAsap === false) + assert(waitForRevocation.reSignAsap == false) // actual test starts here addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) alice ! CMD_SIGN() sender.expectNoMessage(300 millis) - assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.remoteNextCommitInfo === Left(waitForRevocation.copy(reSignAsap = true))) + assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.remoteNextCommitInfo == Left(waitForRevocation.copy(reSignAsap = true))) } test("recv CMD_SIGN (going above reserve)", Tag(ChannelStateTestsTags.NoPushMsat)) { f => @@ -890,7 +890,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // it should update its channel_update awaitCond(bob.stateData.asInstanceOf[DATA_NORMAL].channelUpdate.channelFlags.isEnabled) // and broadcast it - assert(listener.expectMsgType[LocalChannelUpdate].channelUpdate === bob.stateData.asInstanceOf[DATA_NORMAL].channelUpdate) + assert(listener.expectMsgType[LocalChannelUpdate].channelUpdate == bob.stateData.asInstanceOf[DATA_NORMAL].channelUpdate) } test("recv CMD_SIGN (after CMD_UPDATE_FEE)") { f => @@ -960,14 +960,14 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice ! CMD_SIGN() val aliceCommitSig = alice2bob.expectMsgType[CommitSig] - assert(aliceCommitSig.htlcSignatures.length === 3) + assert(aliceCommitSig.htlcSignatures.length == 3) alice2bob.forward(bob, aliceCommitSig) bob2alice.expectMsgType[RevokeAndAck] bob2alice.forward(alice) // actual test begins val bobCommitSig = bob2alice.expectMsgType[CommitSig] - assert(bobCommitSig.htlcSignatures.length === 5) + assert(bobCommitSig.htlcSignatures.length == 5) bob2alice.forward(alice, bobCommitSig) awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.index == 1) @@ -987,14 +987,14 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice ! CMD_SIGN() val aliceCommitSig = alice2bob.expectMsgType[CommitSig] - assert(aliceCommitSig.htlcSignatures.length === 2) + assert(aliceCommitSig.htlcSignatures.length == 2) alice2bob.forward(bob, aliceCommitSig) bob2alice.expectMsgType[RevokeAndAck] bob2alice.forward(alice) // actual test begins val bobCommitSig = bob2alice.expectMsgType[CommitSig] - assert(bobCommitSig.htlcSignatures.length === 3) + assert(bobCommitSig.htlcSignatures.length == 3) bob2alice.forward(alice, bobCommitSig) awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.index == 1) @@ -1014,14 +1014,14 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice ! CMD_SIGN() val aliceCommitSig = alice2bob.expectMsgType[CommitSig] - assert(aliceCommitSig.htlcSignatures.length === 3) + assert(aliceCommitSig.htlcSignatures.length == 3) alice2bob.forward(bob, aliceCommitSig) bob2alice.expectMsgType[RevokeAndAck] bob2alice.forward(alice) // actual test begins val bobCommitSig = bob2alice.expectMsgType[CommitSig] - assert(bobCommitSig.htlcSignatures.length === 5) + assert(bobCommitSig.htlcSignatures.length == 5) bob2alice.forward(alice, bobCommitSig) awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.index == 1) @@ -1036,7 +1036,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // actual test begins (note that channel sends a CMD_SIGN to itself when it receives RevokeAndAck and there are changes) val updateFee = alice2bob.expectMsgType[UpdateFee] - assert(updateFee.feeratePerKw === TestConstants.feeratePerKw + FeeratePerKw(1000 sat)) + assert(updateFee.feeratePerKw == TestConstants.feeratePerKw + FeeratePerKw(1000 sat)) alice2bob.forward(bob) alice2bob.expectMsgType[CommitSig] alice2bob.forward(bob) @@ -1079,8 +1079,8 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(new String(error.data.toArray).startsWith("cannot sign when there are no changes")) awaitCond(bob.stateName == CLOSING) // channel should be advertised as down - assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId === bob.stateData.asInstanceOf[DATA_CLOSING].channelId) - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid === tx.txid) + assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId == bob.stateData.asInstanceOf[DATA_CLOSING].channelId) + assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == tx.txid) bob2blockchain.expectMsgType[PublishTx] bob2blockchain.expectMsgType[WatchTxConfirmed] } @@ -1095,7 +1095,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val error = bob2alice.expectMsgType[Error] assert(new String(error.data.toArray).startsWith("invalid commitment signature")) awaitCond(bob.stateName == CLOSING) - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid === tx.txid) + assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == tx.txid) bob2blockchain.expectMsgType[PublishTx] bob2blockchain.expectMsgType[WatchTxConfirmed] } @@ -1113,8 +1113,8 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val badCommitSig = commitSig.copy(htlcSignatures = commitSig.htlcSignatures ::: commitSig.htlcSignatures) bob ! badCommitSig val error = bob2alice.expectMsgType[Error] - assert(new String(error.data.toArray) === HtlcSigCountMismatch(channelId(bob), expected = 1, actual = 2).getMessage) - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid === tx.txid) + assert(new String(error.data.toArray) == HtlcSigCountMismatch(channelId(bob), expected = 1, actual = 2).getMessage) + assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == tx.txid) bob2blockchain.expectMsgType[PublishTx] bob2blockchain.expectMsgType[WatchTxConfirmed] } @@ -1133,7 +1133,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with bob ! badCommitSig val error = bob2alice.expectMsgType[Error] assert(new String(error.data.toArray).startsWith("invalid htlc signature")) - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid === tx.txid) + assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == tx.txid) bob2blockchain.expectMsgType[PublishTx] bob2blockchain.expectMsgType[WatchTxConfirmed] } @@ -1177,7 +1177,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with awaitCond(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.remoteNextCommitInfo.isRight) // now bob will forward the htlc downstream val forward = bob2relayer.expectMsgType[RelayForward] - assert(forward.add === htlc) + assert(forward.add == htlc) } test("recv RevokeAndAck (multiple htlcs in both directions)") { f => @@ -1221,7 +1221,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) alice ! CMD_SIGN() sender.expectNoMessage(300 millis) - assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.remoteNextCommitInfo.left.toOption.get.reSignAsap === true) + assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.remoteNextCommitInfo.left.toOption.get.reSignAsap == true) // actual test starts here bob2alice.expectMsgType[RevokeAndAck] @@ -1244,8 +1244,8 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice2bob.expectMsgType[Error] awaitCond(alice.stateName == CLOSING) // channel should be advertised as down - assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId === alice.stateData.asInstanceOf[DATA_CLOSING].channelId) - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid === tx.txid) + assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId == alice.stateData.asInstanceOf[DATA_CLOSING].channelId) + assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid == tx.txid) alice2blockchain.expectMsgType[PublishTx] alice2blockchain.expectMsgType[WatchTxConfirmed] } @@ -1253,9 +1253,9 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with test("recv RevokeAndAck (over max dust htlc exposure)") { f => import f._ val aliceCommitments = alice.stateData.asInstanceOf[DATA_NORMAL].commitments - assert(alice.underlyingActor.nodeParams.onChainFeeConf.feerateToleranceFor(bob.underlyingActor.nodeParams.nodeId).dustTolerance.maxExposure === 25_000.sat) - assert(Transactions.offeredHtlcTrimThreshold(aliceCommitments.localParams.dustLimit, aliceCommitments.localCommit.spec, aliceCommitments.commitmentFormat) === 7730.sat) - assert(Transactions.receivedHtlcTrimThreshold(aliceCommitments.remoteParams.dustLimit, aliceCommitments.localCommit.spec, aliceCommitments.commitmentFormat) === 8030.sat) + assert(alice.underlyingActor.nodeParams.onChainFeeConf.feerateToleranceFor(bob.underlyingActor.nodeParams.nodeId).dustTolerance.maxExposure == 25_000.sat) + assert(Transactions.offeredHtlcTrimThreshold(aliceCommitments.localParams.dustLimit, aliceCommitments.localCommit.spec, aliceCommitments.commitmentFormat) == 7730.sat) + assert(Transactions.receivedHtlcTrimThreshold(aliceCommitments.remoteParams.dustLimit, aliceCommitments.localCommit.spec, aliceCommitments.commitmentFormat) == 8030.sat) // Alice sends HTLCs to Bob that add 10 000 sat to the dust exposure: addHtlc(500.sat.toMilliSatoshi, alice, bob, alice2bob, bob2alice) // dust htlc @@ -1285,7 +1285,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice2bob.expectMsgType[UpdateFailHtlc], alice2bob.expectMsgType[UpdateFailHtlc] ) - assert(failedHtlcs.map(_.id).toSet === Set(dust1.id, dust2.id, trimmed1.id)) + assert(failedHtlcs.map(_.id).toSet == Set(dust1.id, dust2.id, trimmed1.id)) alice2bob.expectMsgType[CommitSig] alice2bob.expectNoMessage(100 millis) } @@ -1293,7 +1293,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with test("recv RevokeAndAck (over max dust htlc exposure with pending local changes)") { f => import f._ val sender = TestProbe() - assert(alice.underlyingActor.nodeParams.onChainFeeConf.feerateToleranceFor(bob.underlyingActor.nodeParams.nodeId).dustTolerance.maxExposure === 25_000.sat) + assert(alice.underlyingActor.nodeParams.onChainFeeConf.feerateToleranceFor(bob.underlyingActor.nodeParams.nodeId).dustTolerance.maxExposure == 25_000.sat) // Bob sends HTLCs to Alice that add 10 000 sat to the dust exposure. addHtlc(4000.sat.toMilliSatoshi, bob, alice, bob2alice, alice2bob) @@ -1323,7 +1323,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // Alice forwards HTLCs that fit in the dust exposure and instantly fails the others. alice2relayer.expectMsg(RelayForward(acceptedHtlc)) alice2relayer.expectNoMessage(100 millis) - assert(alice2bob.expectMsgType[UpdateFailHtlc].id === rejectedHtlc.id) + assert(alice2bob.expectMsgType[UpdateFailHtlc].id == rejectedHtlc.id) alice2bob.expectMsgType[CommitSig] alice2bob.expectNoMessage(100 millis) } @@ -1331,7 +1331,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with def testRevokeAndAckDustOverflowSingleCommit(f: FixtureParam): Unit = { import f._ val sender = TestProbe() - assert(alice.underlyingActor.nodeParams.onChainFeeConf.feerateToleranceFor(bob.underlyingActor.nodeParams.nodeId).dustTolerance.maxExposure === 25_000.sat) + assert(alice.underlyingActor.nodeParams.onChainFeeConf.feerateToleranceFor(bob.underlyingActor.nodeParams.nodeId).dustTolerance.maxExposure == 25_000.sat) // Bob sends HTLCs to Alice that add 10 500 sat to the dust exposure. (1 to 10).foreach(_ => addHtlc(1050.sat.toMilliSatoshi, bob, alice, bob2alice, alice2bob)) @@ -1365,16 +1365,16 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with test("recv RevokeAndAck (over max dust htlc exposure in local commit only with pending local changes)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs), Tag(ChannelStateTestsTags.HighDustLimitDifferenceAliceBob)) { f => import f._ val sender = TestProbe() - assert(alice.underlyingActor.nodeParams.channelConf.dustLimit === 5000.sat) - assert(bob.underlyingActor.nodeParams.channelConf.dustLimit === 1000.sat) + assert(alice.underlyingActor.nodeParams.channelConf.dustLimit == 5000.sat) + assert(bob.underlyingActor.nodeParams.channelConf.dustLimit == 1000.sat) testRevokeAndAckDustOverflowSingleCommit(f) } test("recv RevokeAndAck (over max dust htlc exposure in remote commit only with pending local changes)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs), Tag(ChannelStateTestsTags.HighDustLimitDifferenceBobAlice)) { f => import f._ val sender = TestProbe() - assert(alice.underlyingActor.nodeParams.channelConf.dustLimit === 1000.sat) - assert(bob.underlyingActor.nodeParams.channelConf.dustLimit === 5000.sat) + assert(alice.underlyingActor.nodeParams.channelConf.dustLimit == 1000.sat) + assert(bob.underlyingActor.nodeParams.channelConf.dustLimit == 5000.sat) testRevokeAndAckDustOverflowSingleCommit(f) } @@ -1386,8 +1386,8 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice2bob.expectMsgType[Error] awaitCond(alice.stateName == CLOSING) // channel should be advertised as down - assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId === alice.stateData.asInstanceOf[DATA_CLOSING].channelId) - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid === tx.txid) + assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId == alice.stateData.asInstanceOf[DATA_CLOSING].channelId) + assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid == tx.txid) alice2blockchain.expectMsgType[PublishTx] alice2blockchain.expectMsgType[WatchTxConfirmed] } @@ -1414,8 +1414,8 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with bob2alice.forward(alice) // alice will forward the fail upstream val forward = alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.RemoteFail]] - assert(forward.result.fail === fail) - assert(forward.htlc === htlc) + assert(forward.result.fail == fail) + assert(forward.htlc == htlc) } test("recv RevokeAndAck (forward UpdateFailMalformedHtlc)") { f => @@ -1440,8 +1440,8 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with bob2alice.forward(alice) // alice will forward the fail upstream val forward = alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.RemoteFailMalformed]] - assert(forward.result.fail === fail) - assert(forward.htlc === htlc) + assert(forward.result.fail == fail) + assert(forward.htlc == htlc) } def testRevokeAndAckHtlcStaticRemoteKey(f: FixtureParam): Unit = { @@ -1606,8 +1606,8 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with commitments = initialState.commitments.copy(remoteChanges = initialState.commitments.remoteChanges.copy(initialState.commitments.remoteChanges.proposed :+ fulfill)))) // alice immediately propagates the fulfill upstream val forward = alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.RemoteFulfill]] - assert(forward.result.fulfill === fulfill) - assert(forward.htlc === htlc) + assert(forward.result.fulfill == fulfill) + assert(forward.htlc == htlc) } test("recv UpdateFulfillHtlc") { @@ -1638,8 +1638,8 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice2bob.expectMsgType[Error] awaitCond(alice.stateName == CLOSING) // channel should be advertised as down - assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId === alice.stateData.asInstanceOf[DATA_CLOSING].channelId) - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid === tx.txid) + assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId == alice.stateData.asInstanceOf[DATA_CLOSING].channelId) + assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid == tx.txid) alice2blockchain.expectMsgType[PublishTx] alice2blockchain.expectMsgType[WatchTxConfirmed] } @@ -1651,8 +1651,8 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice2bob.expectMsgType[Error] awaitCond(alice.stateName == CLOSING) // channel should be advertised as down - assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId === alice.stateData.asInstanceOf[DATA_CLOSING].channelId) - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid === tx.txid) + assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId == alice.stateData.asInstanceOf[DATA_CLOSING].channelId) + assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid == tx.txid) alice2blockchain.expectMsgType[PublishTx] alice2blockchain.expectMsgType[WatchTxConfirmed] } @@ -1669,8 +1669,8 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice2bob.expectMsgType[Error] awaitCond(alice.stateName == CLOSING) // channel should be advertised as down - assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId === alice.stateData.asInstanceOf[DATA_CLOSING].channelId) - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid === tx.txid) + assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId == alice.stateData.asInstanceOf[DATA_CLOSING].channelId) + assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid == tx.txid) alice2blockchain.expectMsgType[PublishTx] // main delayed alice2blockchain.expectMsgType[PublishTx] // htlc timeout alice2blockchain.expectMsgType[WatchTxConfirmed] @@ -1685,7 +1685,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val initialState = bob.stateData.asInstanceOf[DATA_NORMAL] val cmd = CMD_FAIL_HTLC(htlc.id, Right(PermanentChannelFailure)) val Right(fail) = OutgoingPaymentPacket.buildHtlcFailure(Bob.nodeParams.privateKey, cmd, htlc) - assert(fail.id === htlc.id) + assert(fail.id == htlc.id) bob ! cmd bob2alice.expectMsg(fail) awaitCond(bob.stateData == initialState.copy( @@ -1865,9 +1865,9 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val fail = UpdateFailMalformedHtlc(ByteVector32.Zeroes, htlc.id, Sphinx.hash(htlc.onionRoutingPacket), 42) alice ! fail val error = alice2bob.expectMsgType[Error] - assert(new String(error.data.toArray) === InvalidFailureCode(ByteVector32.Zeroes).getMessage) + assert(new String(error.data.toArray) == InvalidFailureCode(ByteVector32.Zeroes).getMessage) awaitCond(alice.stateName == CLOSING) - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid === tx.txid) // commit tx + assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid == tx.txid) // commit tx alice2blockchain.expectMsgType[PublishTx] // main delayed alice2blockchain.expectMsgType[PublishTx] // htlc timeout alice2blockchain.expectMsgType[WatchTxConfirmed] @@ -1885,8 +1885,8 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice2bob.expectMsgType[Error] awaitCond(alice.stateName == CLOSING) // channel should be advertised as down - assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId === alice.stateData.asInstanceOf[DATA_CLOSING].channelId) - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid === tx.txid) + assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId == alice.stateData.asInstanceOf[DATA_CLOSING].channelId) + assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid == tx.txid) alice2blockchain.expectMsgType[PublishTx] alice2blockchain.expectMsgType[WatchTxConfirmed] } @@ -1898,8 +1898,8 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice2bob.expectMsgType[Error] awaitCond(alice.stateName == CLOSING) // channel should be advertised as down - assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId === alice.stateData.asInstanceOf[DATA_CLOSING].channelId) - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid === tx.txid) + assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId == alice.stateData.asInstanceOf[DATA_CLOSING].channelId) + assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid == tx.txid) alice2blockchain.expectMsgType[PublishTx] alice2blockchain.expectMsgType[WatchTxConfirmed] } @@ -1911,9 +1911,9 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // Bob receives a failure with a completely invalid onion error (missing mac) bob ! CMD_FAIL_HTLC(htlc.id, Left(ByteVector.fill(260)(42))) val fail = bob2alice.expectMsgType[UpdateFailHtlc] - assert(fail.id === htlc.id) + assert(fail.id == htlc.id) // We should rectify the packet length before forwarding upstream. - assert(fail.reason.length === Sphinx.FailurePacket.PacketLength) + assert(fail.reason.length == Sphinx.FailurePacket.PacketLength) } private def testCmdUpdateFee(f: FixtureParam): Unit = { @@ -1942,8 +1942,8 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with addHtlc(14000.sat.toMilliSatoshi, alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) val aliceCommitments = alice.stateData.asInstanceOf[DATA_NORMAL].commitments - assert(DustExposure.computeExposure(aliceCommitments.localCommit.spec, aliceCommitments.localParams.dustLimit, aliceCommitments.commitmentFormat) === 0.msat) - assert(DustExposure.computeExposure(aliceCommitments.remoteCommit.spec, aliceCommitments.remoteParams.dustLimit, aliceCommitments.commitmentFormat) === 0.msat) + assert(DustExposure.computeExposure(aliceCommitments.localCommit.spec, aliceCommitments.localParams.dustLimit, aliceCommitments.commitmentFormat) == 0.msat) + assert(DustExposure.computeExposure(aliceCommitments.remoteCommit.spec, aliceCommitments.remoteParams.dustLimit, aliceCommitments.commitmentFormat) == 0.msat) // A large feerate increase would make these HTLCs overflow alice's dust exposure, so she rejects it: val sender = TestProbe() @@ -1955,7 +1955,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with test("recv CMD_UPDATE_FEE (over max dust htlc exposure with pending local changes)") { f => import f._ val sender = TestProbe() - assert(alice.underlyingActor.nodeParams.onChainFeeConf.feerateToleranceFor(bob.underlyingActor.nodeParams.nodeId).dustTolerance.maxExposure === 25_000.sat) + assert(alice.underlyingActor.nodeParams.onChainFeeConf.feerateToleranceFor(bob.underlyingActor.nodeParams.nodeId).dustTolerance.maxExposure == 25_000.sat) // Alice sends an HTLC to Bob that is not included in the dust exposure at the current feerate. // She signs them but Bob doesn't answer yet. @@ -1967,8 +1967,8 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // Alice sends another HTLC to Bob that is not included in the dust exposure at the current feerate. addHtlc(14000.sat.toMilliSatoshi, alice, bob, alice2bob, bob2alice) val aliceCommitments = alice.stateData.asInstanceOf[DATA_NORMAL].commitments - assert(DustExposure.computeExposure(aliceCommitments.localCommit.spec, aliceCommitments.localParams.dustLimit, aliceCommitments.commitmentFormat) === 0.msat) - assert(DustExposure.computeExposure(aliceCommitments.remoteCommit.spec, aliceCommitments.remoteParams.dustLimit, aliceCommitments.commitmentFormat) === 0.msat) + assert(DustExposure.computeExposure(aliceCommitments.localCommit.spec, aliceCommitments.localParams.dustLimit, aliceCommitments.commitmentFormat) == 0.msat) + assert(DustExposure.computeExposure(aliceCommitments.remoteCommit.spec, aliceCommitments.remoteParams.dustLimit, aliceCommitments.commitmentFormat) == 0.msat) // A large feerate increase would make these HTLCs overflow alice's dust exposure, so she rejects it: val cmd = CMD_UPDATE_FEE(FeeratePerKw(20000 sat), replyTo_opt = Some(sender.ref)) @@ -1986,21 +1986,21 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with updateFee(initialFeerate, alice, bob, alice2bob, bob2alice) val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] val aliceCommitments = initialState.commitments - assert(alice.underlyingActor.nodeParams.onChainFeeConf.feerateToleranceFor(bob.underlyingActor.nodeParams.nodeId).dustTolerance.maxExposure === 25_000.sat) + assert(alice.underlyingActor.nodeParams.onChainFeeConf.feerateToleranceFor(bob.underlyingActor.nodeParams.nodeId).dustTolerance.maxExposure == 25_000.sat) val higherDustLimit = Seq(aliceCommitments.localParams.dustLimit, aliceCommitments.remoteParams.dustLimit).max val lowerDustLimit = Seq(aliceCommitments.localParams.dustLimit, aliceCommitments.remoteParams.dustLimit).min // We have the following dust thresholds at the current feerate - assert(Transactions.offeredHtlcTrimThreshold(higherDustLimit, aliceCommitments.localCommit.spec.copy(commitTxFeerate = DustExposure.feerateForDustExposure(initialFeerate)), aliceCommitments.commitmentFormat) === 6989.sat) - assert(Transactions.receivedHtlcTrimThreshold(higherDustLimit, aliceCommitments.localCommit.spec.copy(commitTxFeerate = DustExposure.feerateForDustExposure(initialFeerate)), aliceCommitments.commitmentFormat) === 7109.sat) - assert(Transactions.offeredHtlcTrimThreshold(lowerDustLimit, aliceCommitments.localCommit.spec.copy(commitTxFeerate = DustExposure.feerateForDustExposure(initialFeerate)), aliceCommitments.commitmentFormat) === 2989.sat) - assert(Transactions.receivedHtlcTrimThreshold(lowerDustLimit, aliceCommitments.localCommit.spec.copy(commitTxFeerate = DustExposure.feerateForDustExposure(initialFeerate)), aliceCommitments.commitmentFormat) === 3109.sat) + assert(Transactions.offeredHtlcTrimThreshold(higherDustLimit, aliceCommitments.localCommit.spec.copy(commitTxFeerate = DustExposure.feerateForDustExposure(initialFeerate)), aliceCommitments.commitmentFormat) == 6989.sat) + assert(Transactions.receivedHtlcTrimThreshold(higherDustLimit, aliceCommitments.localCommit.spec.copy(commitTxFeerate = DustExposure.feerateForDustExposure(initialFeerate)), aliceCommitments.commitmentFormat) == 7109.sat) + assert(Transactions.offeredHtlcTrimThreshold(lowerDustLimit, aliceCommitments.localCommit.spec.copy(commitTxFeerate = DustExposure.feerateForDustExposure(initialFeerate)), aliceCommitments.commitmentFormat) == 2989.sat) + assert(Transactions.receivedHtlcTrimThreshold(lowerDustLimit, aliceCommitments.localCommit.spec.copy(commitTxFeerate = DustExposure.feerateForDustExposure(initialFeerate)), aliceCommitments.commitmentFormat) == 3109.sat) // And the following thresholds after the feerate update // NB: we apply the real feerate when sending update_fee, not the one adjusted for dust val updatedFeerate = FeeratePerKw(4000 sat) - assert(Transactions.offeredHtlcTrimThreshold(higherDustLimit, aliceCommitments.localCommit.spec.copy(commitTxFeerate = updatedFeerate), aliceCommitments.commitmentFormat) === 7652.sat) - assert(Transactions.receivedHtlcTrimThreshold(higherDustLimit, aliceCommitments.localCommit.spec.copy(commitTxFeerate = updatedFeerate), aliceCommitments.commitmentFormat) === 7812.sat) - assert(Transactions.offeredHtlcTrimThreshold(lowerDustLimit, aliceCommitments.localCommit.spec.copy(commitTxFeerate = updatedFeerate), aliceCommitments.commitmentFormat) === 3652.sat) - assert(Transactions.receivedHtlcTrimThreshold(lowerDustLimit, aliceCommitments.localCommit.spec.copy(commitTxFeerate = updatedFeerate), aliceCommitments.commitmentFormat) === 3812.sat) + assert(Transactions.offeredHtlcTrimThreshold(higherDustLimit, aliceCommitments.localCommit.spec.copy(commitTxFeerate = updatedFeerate), aliceCommitments.commitmentFormat) == 7652.sat) + assert(Transactions.receivedHtlcTrimThreshold(higherDustLimit, aliceCommitments.localCommit.spec.copy(commitTxFeerate = updatedFeerate), aliceCommitments.commitmentFormat) == 7812.sat) + assert(Transactions.offeredHtlcTrimThreshold(lowerDustLimit, aliceCommitments.localCommit.spec.copy(commitTxFeerate = updatedFeerate), aliceCommitments.commitmentFormat) == 3652.sat) + assert(Transactions.receivedHtlcTrimThreshold(lowerDustLimit, aliceCommitments.localCommit.spec.copy(commitTxFeerate = updatedFeerate), aliceCommitments.commitmentFormat) == 3812.sat) // Alice send HTLCs to Bob that are not included in the dust exposure at the current feerate. // She signs them but Bob doesn't answer yet. @@ -2065,7 +2065,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with test("recv UpdateFee (anchor outputs)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => import f._ val initialData = bob.stateData.asInstanceOf[DATA_NORMAL] - assert(initialData.commitments.localCommit.spec.commitTxFeerate === TestConstants.anchorOutputsFeeratePerKw) + assert(initialData.commitments.localCommit.spec.commitTxFeerate == TestConstants.anchorOutputsFeeratePerKw) val fee = UpdateFee(ByteVector32.Zeroes, TestConstants.anchorOutputsFeeratePerKw * 0.8) bob ! fee awaitCond(bob.stateData == initialData.copy(commitments = initialData.commitments.copy(remoteChanges = initialData.commitments.remoteChanges.copy(proposed = initialData.commitments.remoteChanges.proposed :+ fee), remoteNextHtlcId = 0))) @@ -2088,8 +2088,8 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice2bob.expectMsgType[Error] awaitCond(alice.stateName == CLOSING) // channel should be advertised as down - assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId === alice.stateData.asInstanceOf[DATA_CLOSING].channelId) - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid === tx.txid) + assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId == alice.stateData.asInstanceOf[DATA_CLOSING].channelId) + assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid == tx.txid) alice2blockchain.expectMsgType[PublishTx] alice2blockchain.expectMsgType[WatchTxConfirmed] } @@ -2102,11 +2102,11 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with bob.feeEstimator.setFeerate(FeeratesPerKw.single(fee.feeratePerKw)) bob ! fee val error = bob2alice.expectMsgType[Error] - assert(new String(error.data.toArray) === CannotAffordFees(channelId(bob), missing = 71620000L sat, reserve = 20000L sat, fees = 72400000L sat).getMessage) + assert(new String(error.data.toArray) == CannotAffordFees(channelId(bob), missing = 71620000L sat, reserve = 20000L sat, fees = 72400000L sat).getMessage) awaitCond(bob.stateName == CLOSING) // channel should be advertised as down - assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId === bob.stateData.asInstanceOf[DATA_CLOSING].channelId) - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid === tx.txid) // commit tx + assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId == bob.stateData.asInstanceOf[DATA_CLOSING].channelId) + assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == tx.txid) // commit tx //bob2blockchain.expectMsgType[PublishTx] // main delayed (removed because of the high fees) bob2blockchain.expectMsgType[WatchTxConfirmed] } @@ -2117,11 +2117,11 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // This feerate is just above the threshold: (800000 (alice balance) - 20000 (reserve) - 660 (anchors)) / 1124 (commit tx weight) = 693363 bob ! UpdateFee(ByteVector32.Zeroes, FeeratePerKw(693364 sat)) val error = bob2alice.expectMsgType[Error] - assert(new String(error.data.toArray) === CannotAffordFees(channelId(bob), missing = 1 sat, reserve = 20000 sat, fees = 780001 sat).getMessage) + assert(new String(error.data.toArray) == CannotAffordFees(channelId(bob), missing = 1 sat, reserve = 20000 sat, fees = 780001 sat).getMessage) awaitCond(bob.stateName == CLOSING) // channel should be advertised as down - assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId === bob.stateData.asInstanceOf[DATA_CLOSING].channelId) - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid === tx.txid) // commit tx + assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId == bob.stateData.asInstanceOf[DATA_CLOSING].channelId) + assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == tx.txid) // commit tx } test("recv UpdateFee (local/remote feerates are too different)") { f => @@ -2129,7 +2129,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val initialState = bob.stateData.asInstanceOf[DATA_NORMAL] val commitTx = initialState.commitments.localCommit.commitTxAndRemoteSig.commitTx.tx - assert(initialState.commitments.localCommit.spec.commitTxFeerate === TestConstants.feeratePerKw) + assert(initialState.commitments.localCommit.spec.commitTxFeerate == TestConstants.feeratePerKw) alice2bob.send(bob, UpdateFee(ByteVector32.Zeroes, TestConstants.feeratePerKw * 3)) bob2alice.expectNoMessage(250 millis) // we don't close because the commitment doesn't contain any HTLC @@ -2139,8 +2139,8 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(new String(error.data.toArray).contains("local/remote feerates are too different")) awaitCond(bob.stateName == CLOSING) // channel should be advertised as down - assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId === bob.stateData.asInstanceOf[DATA_CLOSING].channelId) - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid === commitTx.txid) + assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId == bob.stateData.asInstanceOf[DATA_CLOSING].channelId) + assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == commitTx.txid) bob2blockchain.expectMsgType[PublishTx] bob2blockchain.expectMsgType[WatchTxConfirmed] } @@ -2149,7 +2149,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with import f._ val initialState = bob.stateData.asInstanceOf[DATA_NORMAL] - assert(initialState.commitments.localCommit.spec.commitTxFeerate === TestConstants.anchorOutputsFeeratePerKw) + assert(initialState.commitments.localCommit.spec.commitTxFeerate == TestConstants.anchorOutputsFeeratePerKw) val add = UpdateAddHtlc(ByteVector32.Zeroes, 0, 2500000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket) alice2bob.send(bob, add) val fee = UpdateFee(initialState.channelId, TestConstants.anchorOutputsFeeratePerKw * 3) @@ -2162,7 +2162,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with import f._ val initialState = bob.stateData.asInstanceOf[DATA_NORMAL] - assert(initialState.commitments.localCommit.spec.commitTxFeerate === TestConstants.anchorOutputsFeeratePerKw) + assert(initialState.commitments.localCommit.spec.commitTxFeerate == TestConstants.anchorOutputsFeeratePerKw) val add = UpdateAddHtlc(ByteVector32.Zeroes, 0, 2500000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket) alice2bob.send(bob, add) val fee = UpdateFee(initialState.channelId, FeeratePerKw(FeeratePerByte(2 sat))) @@ -2179,11 +2179,11 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(bobCommitments.localCommit.spec.commitTxFeerate == expectedFeeratePerKw) bob ! UpdateFee(ByteVector32.Zeroes, FeeratePerKw(252 sat)) val error = bob2alice.expectMsgType[Error] - assert(new String(error.data.toArray) === "remote fee rate is too small: remoteFeeratePerKw=252") + assert(new String(error.data.toArray) == "remote fee rate is too small: remoteFeeratePerKw=252") awaitCond(bob.stateName == CLOSING) // channel should be advertised as down - assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId === bob.stateData.asInstanceOf[DATA_CLOSING].channelId) - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid === tx.txid) + assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId == bob.stateData.asInstanceOf[DATA_CLOSING].channelId) + assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == tx.txid) bob2blockchain.expectMsgType[PublishTx] bob2blockchain.expectMsgType[WatchTxConfirmed] } @@ -2197,23 +2197,23 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with addHtlc(14000.sat.toMilliSatoshi, alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) val bobCommitments = bob.stateData.asInstanceOf[DATA_NORMAL].commitments - assert(DustExposure.computeExposure(bobCommitments.localCommit.spec, bobCommitments.localParams.dustLimit, bobCommitments.commitmentFormat) === 0.msat) - assert(DustExposure.computeExposure(bobCommitments.remoteCommit.spec, bobCommitments.remoteParams.dustLimit, bobCommitments.commitmentFormat) === 0.msat) + assert(DustExposure.computeExposure(bobCommitments.localCommit.spec, bobCommitments.localParams.dustLimit, bobCommitments.commitmentFormat) == 0.msat) + assert(DustExposure.computeExposure(bobCommitments.remoteCommit.spec, bobCommitments.remoteParams.dustLimit, bobCommitments.commitmentFormat) == 0.msat) val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx // A large feerate increase would make these HTLCs overflow Bob's dust exposure, so he force-closes: bob.feeEstimator.setFeerate(FeeratesPerKw.single(FeeratePerKw(20000 sat))) bob ! UpdateFee(channelId(bob), FeeratePerKw(20000 sat)) val error = bob2alice.expectMsgType[Error] - assert(new String(error.data.toArray) === LocalDustHtlcExposureTooHigh(channelId(bob), 30000 sat, 40500000 msat).getMessage) - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid === tx.txid) + assert(new String(error.data.toArray) == LocalDustHtlcExposureTooHigh(channelId(bob), 30000 sat, 40500000 msat).getMessage) + assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == tx.txid) awaitCond(bob.stateName == CLOSING) } test("recv UpdateFee (over max dust htlc exposure with pending local changes)") { f => import f._ val sender = TestProbe() - assert(bob.underlyingActor.nodeParams.onChainFeeConf.feerateToleranceFor(alice.underlyingActor.nodeParams.nodeId).dustTolerance.maxExposure === 30_000.sat) + assert(bob.underlyingActor.nodeParams.onChainFeeConf.feerateToleranceFor(alice.underlyingActor.nodeParams.nodeId).dustTolerance.maxExposure == 30_000.sat) // Bob sends HTLCs to Alice that are not included in the dust exposure at the current feerate. // He signs them but Alice doesn't answer yet. @@ -2226,16 +2226,16 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // Bob sends another HTLC to Alice that is not included in the dust exposure at the current feerate. addHtlc(14000.sat.toMilliSatoshi, bob, alice, bob2alice, alice2bob) val bobCommitments = bob.stateData.asInstanceOf[DATA_NORMAL].commitments - assert(DustExposure.computeExposure(bobCommitments.localCommit.spec, bobCommitments.localParams.dustLimit, bobCommitments.commitmentFormat) === 0.msat) - assert(DustExposure.computeExposure(bobCommitments.remoteCommit.spec, bobCommitments.remoteParams.dustLimit, bobCommitments.commitmentFormat) === 0.msat) + assert(DustExposure.computeExposure(bobCommitments.localCommit.spec, bobCommitments.localParams.dustLimit, bobCommitments.commitmentFormat) == 0.msat) + assert(DustExposure.computeExposure(bobCommitments.remoteCommit.spec, bobCommitments.remoteParams.dustLimit, bobCommitments.commitmentFormat) == 0.msat) // A large feerate increase would make these HTLCs overflow Bob's dust exposure, so he force-close: val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx bob.feeEstimator.setFeerate(FeeratesPerKw.single(FeeratePerKw(20000 sat))) bob ! UpdateFee(channelId(bob), FeeratePerKw(20000 sat)) val error = bob2alice.expectMsgType[Error] - assert(new String(error.data.toArray) === LocalDustHtlcExposureTooHigh(channelId(bob), 30000 sat, 40500000 msat).getMessage) - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid === tx.txid) + assert(new String(error.data.toArray) == LocalDustHtlcExposureTooHigh(channelId(bob), 30000 sat, 40500000 msat).getMessage) + assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == tx.txid) awaitCond(bob.stateName == CLOSING) } @@ -2249,21 +2249,21 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with updateFee(initialFeerate, alice, bob, alice2bob, bob2alice) val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] val aliceCommitments = initialState.commitments - assert(alice.underlyingActor.nodeParams.onChainFeeConf.feerateToleranceFor(bob.underlyingActor.nodeParams.nodeId).dustTolerance.maxExposure === 25_000.sat) + assert(alice.underlyingActor.nodeParams.onChainFeeConf.feerateToleranceFor(bob.underlyingActor.nodeParams.nodeId).dustTolerance.maxExposure == 25_000.sat) val higherDustLimit = Seq(aliceCommitments.localParams.dustLimit, aliceCommitments.remoteParams.dustLimit).max val lowerDustLimit = Seq(aliceCommitments.localParams.dustLimit, aliceCommitments.remoteParams.dustLimit).min // We have the following dust thresholds at the current feerate - assert(Transactions.offeredHtlcTrimThreshold(higherDustLimit, aliceCommitments.localCommit.spec.copy(commitTxFeerate = DustExposure.feerateForDustExposure(initialFeerate)), aliceCommitments.commitmentFormat) === 6989.sat) - assert(Transactions.receivedHtlcTrimThreshold(higherDustLimit, aliceCommitments.localCommit.spec.copy(commitTxFeerate = DustExposure.feerateForDustExposure(initialFeerate)), aliceCommitments.commitmentFormat) === 7109.sat) - assert(Transactions.offeredHtlcTrimThreshold(lowerDustLimit, aliceCommitments.localCommit.spec.copy(commitTxFeerate = DustExposure.feerateForDustExposure(initialFeerate)), aliceCommitments.commitmentFormat) === 2989.sat) - assert(Transactions.receivedHtlcTrimThreshold(lowerDustLimit, aliceCommitments.localCommit.spec.copy(commitTxFeerate = DustExposure.feerateForDustExposure(initialFeerate)), aliceCommitments.commitmentFormat) === 3109.sat) + assert(Transactions.offeredHtlcTrimThreshold(higherDustLimit, aliceCommitments.localCommit.spec.copy(commitTxFeerate = DustExposure.feerateForDustExposure(initialFeerate)), aliceCommitments.commitmentFormat) == 6989.sat) + assert(Transactions.receivedHtlcTrimThreshold(higherDustLimit, aliceCommitments.localCommit.spec.copy(commitTxFeerate = DustExposure.feerateForDustExposure(initialFeerate)), aliceCommitments.commitmentFormat) == 7109.sat) + assert(Transactions.offeredHtlcTrimThreshold(lowerDustLimit, aliceCommitments.localCommit.spec.copy(commitTxFeerate = DustExposure.feerateForDustExposure(initialFeerate)), aliceCommitments.commitmentFormat) == 2989.sat) + assert(Transactions.receivedHtlcTrimThreshold(lowerDustLimit, aliceCommitments.localCommit.spec.copy(commitTxFeerate = DustExposure.feerateForDustExposure(initialFeerate)), aliceCommitments.commitmentFormat) == 3109.sat) // And the following thresholds after the feerate update // NB: we apply the real feerate when sending update_fee, not the one adjusted for dust val updatedFeerate = FeeratePerKw(4000 sat) - assert(Transactions.offeredHtlcTrimThreshold(higherDustLimit, aliceCommitments.localCommit.spec.copy(commitTxFeerate = updatedFeerate), aliceCommitments.commitmentFormat) === 7652.sat) - assert(Transactions.receivedHtlcTrimThreshold(higherDustLimit, aliceCommitments.localCommit.spec.copy(commitTxFeerate = updatedFeerate), aliceCommitments.commitmentFormat) === 7812.sat) - assert(Transactions.offeredHtlcTrimThreshold(lowerDustLimit, aliceCommitments.localCommit.spec.copy(commitTxFeerate = updatedFeerate), aliceCommitments.commitmentFormat) === 3652.sat) - assert(Transactions.receivedHtlcTrimThreshold(lowerDustLimit, aliceCommitments.localCommit.spec.copy(commitTxFeerate = updatedFeerate), aliceCommitments.commitmentFormat) === 3812.sat) + assert(Transactions.offeredHtlcTrimThreshold(higherDustLimit, aliceCommitments.localCommit.spec.copy(commitTxFeerate = updatedFeerate), aliceCommitments.commitmentFormat) == 7652.sat) + assert(Transactions.receivedHtlcTrimThreshold(higherDustLimit, aliceCommitments.localCommit.spec.copy(commitTxFeerate = updatedFeerate), aliceCommitments.commitmentFormat) == 7812.sat) + assert(Transactions.offeredHtlcTrimThreshold(lowerDustLimit, aliceCommitments.localCommit.spec.copy(commitTxFeerate = updatedFeerate), aliceCommitments.commitmentFormat) == 3652.sat) + assert(Transactions.receivedHtlcTrimThreshold(lowerDustLimit, aliceCommitments.localCommit.spec.copy(commitTxFeerate = updatedFeerate), aliceCommitments.commitmentFormat) == 3812.sat) // Bob send HTLCs to Alice that are not included in the dust exposure at the current feerate. // He signs them but Alice doesn't answer yet. @@ -2281,8 +2281,8 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with bob ! UpdateFee(channelId(bob), updatedFeerate) val error = bob2alice.expectMsgType[Error] // NB: we don't need to distinguish local and remote, the error message is exactly the same. - assert(new String(error.data.toArray) === LocalDustHtlcExposureTooHigh(channelId(bob), 30000 sat, 37000000 msat).getMessage) - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid === tx.txid) + assert(new String(error.data.toArray) == LocalDustHtlcExposureTooHigh(channelId(bob), 30000 sat, 37000000 msat).getMessage) + assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == tx.txid) awaitCond(bob.stateName == CLOSING) } @@ -2317,7 +2317,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice ! CMD_CLOSE(sender.ref, script_opt, None) sender.expectMsgType[RES_SUCCESS[CMD_CLOSE]] val shutdown = alice2bob.expectMsgType[Shutdown] - script_opt.foreach(script => assert(script === shutdown.scriptPubKey)) + script_opt.foreach(script => assert(script == shutdown.scriptPubKey)) awaitCond(alice.stateName == NORMAL) awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].localShutdown.isDefined) } @@ -2492,7 +2492,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice2bob.expectMsgType[ClosingSigned] awaitCond(alice.stateName == NEGOTIATING) // channel should be advertised as down - assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId === alice.stateData.asInstanceOf[DATA_NEGOTIATING].channelId) + assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId == alice.stateData.asInstanceOf[DATA_NEGOTIATING].channelId) } test("recv Shutdown (no pending htlcs)") { f => @@ -2525,7 +2525,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice2bob.expectMsgType[Shutdown] awaitCond(alice.stateName == SHUTDOWN) // channel should be advertised as down - assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId === alice.stateData.asInstanceOf[DATA_SHUTDOWN].channelId) + assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId == alice.stateData.asInstanceOf[DATA_SHUTDOWN].channelId) } test("recv Shutdown (with unacked received htlcs)") { f => @@ -2721,11 +2721,11 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] val aliceCommitTx = initialState.commitments.localCommit.commitTxAndRemoteSig.commitTx.tx alice ! CurrentBlockHeight(BlockHeight(400145)) - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid === aliceCommitTx.txid) + assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid == aliceCommitTx.txid) alice2blockchain.expectMsgType[PublishTx] // main delayed alice2blockchain.expectMsgType[PublishTx] // htlc timeout - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === aliceCommitTx.txid) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == aliceCommitTx.txid) } test("recv CurrentBlockCount (fulfilled signed htlc ignored by upstream peer)") { f => @@ -2754,10 +2754,10 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(isFatal) assert(err.isInstanceOf[HtlcsWillTimeoutUpstream]) - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid === initialCommitTx.txid) + assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == initialCommitTx.txid) bob2blockchain.expectMsgType[PublishTx] // main delayed - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txOut === htlcSuccessTx.txOut) - assert(bob2blockchain.expectMsgType[WatchTxConfirmed].txId === initialCommitTx.txid) + assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txOut == htlcSuccessTx.txOut) + assert(bob2blockchain.expectMsgType[WatchTxConfirmed].txId == initialCommitTx.txid) alice2blockchain.expectNoMessage(500 millis) } @@ -2787,10 +2787,10 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(isFatal) assert(err.isInstanceOf[HtlcsWillTimeoutUpstream]) - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid === initialCommitTx.txid) + assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == initialCommitTx.txid) bob2blockchain.expectMsgType[PublishTx] // main delayed - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txOut === htlcSuccessTx.txOut) - assert(bob2blockchain.expectMsgType[WatchTxConfirmed].txId === initialCommitTx.txid) + assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txOut == htlcSuccessTx.txOut) + assert(bob2blockchain.expectMsgType[WatchTxConfirmed].txId == initialCommitTx.txid) alice2blockchain.expectNoMessage(500 millis) } @@ -2824,10 +2824,10 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(isFatal) assert(err.isInstanceOf[HtlcsWillTimeoutUpstream]) - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid === initialCommitTx.txid) + assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == initialCommitTx.txid) bob2blockchain.expectMsgType[PublishTx] // main delayed - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txOut === htlcSuccessTx.txOut) - assert(bob2blockchain.expectMsgType[WatchTxConfirmed].txId === initialCommitTx.txid) + assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txOut == htlcSuccessTx.txOut) + assert(bob2blockchain.expectMsgType[WatchTxConfirmed].txId == initialCommitTx.txid) alice2blockchain.expectNoMessage(500 millis) } @@ -2842,7 +2842,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with test("recv CurrentFeerate (when funder, triggers an UpdateFee, anchor outputs)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => import f._ val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] - assert(initialState.commitments.localCommit.spec.commitTxFeerate === TestConstants.anchorOutputsFeeratePerKw) + assert(initialState.commitments.localCommit.spec.commitTxFeerate == TestConstants.anchorOutputsFeeratePerKw) alice ! CurrentFeerates(FeeratesPerKw.single(TestConstants.anchorOutputsFeeratePerKw / 2).copy(mempoolMinFee = FeeratePerKw(250 sat))) alice2bob.expectMsg(UpdateFee(initialState.commitments.channelId, TestConstants.anchorOutputsFeeratePerKw / 2)) alice2bob.expectMsgType[CommitSig] @@ -2861,7 +2861,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with test("recv CurrentFeerate (when funder, doesn't trigger an UpdateFee, anchor outputs)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => import f._ val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] - assert(initialState.commitments.localCommit.spec.commitTxFeerate === TestConstants.anchorOutputsFeeratePerKw) + assert(initialState.commitments.localCommit.spec.commitTxFeerate == TestConstants.anchorOutputsFeeratePerKw) alice ! CurrentFeerates(FeeratesPerKw.single(TestConstants.anchorOutputsFeeratePerKw * 2).copy(mempoolMinFee = FeeratePerKw(250 sat))) alice2bob.expectNoMessage(500 millis) } @@ -2900,14 +2900,14 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice2bob.forward(bob) addHtlc(10000000 msat, alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) - assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.spec.commitTxFeerate === TestConstants.anchorOutputsFeeratePerKw / 2) + assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.spec.commitTxFeerate == TestConstants.anchorOutputsFeeratePerKw / 2) // The network fees spike, but Bob doesn't close the channel because we're using anchor outputs. bob.feeEstimator.setFeerate(FeeratesPerKw.single(TestConstants.anchorOutputsFeeratePerKw * 2)) val event = CurrentFeerates(FeeratesPerKw.single(TestConstants.anchorOutputsFeeratePerKw * 2)) bob ! event bob2alice.expectNoMessage(250 millis) - assert(bob.stateName === NORMAL) + assert(bob.stateName == NORMAL) } test("recv CurrentFeerate (when fundee, commit-fee/network-fee are very different, without HTLCs)") { f => @@ -2967,14 +2967,14 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with }).sum // at best we have a little less than 450 000 + 250 000 + 100 000 + 50 000 = 850 000 (because fees) val amountClaimed = claimMain.txOut.head.amount + htlcAmountClaimed - assert(amountClaimed === 814880.sat) + assert(amountClaimed == 814880.sat) // alice sets the confirmation targets to the HTLC expiry - assert(claimHtlcTxs.collect { case PublishReplaceableTx(tx: ClaimHtlcSuccessTx, _) => (tx.htlcId, tx.confirmBefore.toLong) }.toMap === Map(htlcb1.id -> htlcb1.cltvExpiry.toLong)) - assert(claimHtlcTxs.collect { case PublishReplaceableTx(tx: ClaimHtlcTimeoutTx, _) => (tx.htlcId, tx.confirmBefore.toLong) }.toMap === Map(htlca1.id -> htlca1.cltvExpiry.toLong, htlca2.id -> htlca2.cltvExpiry.toLong)) + assert(claimHtlcTxs.collect { case PublishReplaceableTx(tx: ClaimHtlcSuccessTx, _) => (tx.htlcId, tx.confirmBefore.toLong) }.toMap == Map(htlcb1.id -> htlcb1.cltvExpiry.toLong)) + assert(claimHtlcTxs.collect { case PublishReplaceableTx(tx: ClaimHtlcTimeoutTx, _) => (tx.htlcId, tx.confirmBefore.toLong) }.toMap == Map(htlca1.id -> htlca1.cltvExpiry.toLong, htlca2.id -> htlca2.cltvExpiry.toLong)) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === bobCommitTx.txid) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === claimMain.txid) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == bobCommitTx.txid) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == claimMain.txid) alice2blockchain.expectMsgType[WatchOutputSpent] // htlc 1 alice2blockchain.expectMsgType[WatchOutputSpent] // htlc 2 alice2blockchain.expectMsgType[WatchOutputSpent] // htlc 3 @@ -2984,7 +2984,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with awaitCond(alice.stateName == CLOSING) assert(alice.stateData.asInstanceOf[DATA_CLOSING].remoteCommitPublished.isDefined) val rcp = alice.stateData.asInstanceOf[DATA_CLOSING].remoteCommitPublished.get - assert(rcp.claimHtlcTxs.size === 4) + assert(rcp.claimHtlcTxs.size == 4) assert(getClaimHtlcSuccessTxs(rcp).length == 1) assert(getClaimHtlcTimeoutTxs(rcp).length == 2) @@ -3058,13 +3058,13 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with }).sum // at best we have a little less than 500 000 + 250 000 + 100 000 = 850 000 (because fees) val amountClaimed = claimMain.txOut.head.amount + htlcAmountClaimed - assert(amountClaimed === 822310.sat) + assert(amountClaimed == 822310.sat) // alice sets the confirmation targets to the HTLC expiry - assert(claimHtlcTxs.collect { case PublishReplaceableTx(tx: ClaimHtlcTimeoutTx, _) => (tx.htlcId, tx.confirmBefore.toLong) }.toMap === Map(htlca1.id -> htlca1.cltvExpiry.toLong, htlca2.id -> htlca2.cltvExpiry.toLong)) + assert(claimHtlcTxs.collect { case PublishReplaceableTx(tx: ClaimHtlcTimeoutTx, _) => (tx.htlcId, tx.confirmBefore.toLong) }.toMap == Map(htlca1.id -> htlca1.cltvExpiry.toLong, htlca2.id -> htlca2.cltvExpiry.toLong)) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === bobCommitTx.txid) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === claimMain.txid) // claim-main + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == bobCommitTx.txid) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == claimMain.txid) // claim-main alice2blockchain.expectMsgType[WatchOutputSpent] // htlc 1 alice2blockchain.expectMsgType[WatchOutputSpent] // htlc 2 alice2blockchain.expectMsgType[WatchOutputSpent] // htlc 3 @@ -3073,8 +3073,8 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with awaitCond(alice.stateName == CLOSING) assert(alice.stateData.asInstanceOf[DATA_CLOSING].nextRemoteCommitPublished.isDefined) val rcp = alice.stateData.asInstanceOf[DATA_CLOSING].nextRemoteCommitPublished.get - assert(getClaimHtlcSuccessTxs(rcp).length === 0) - assert(getClaimHtlcTimeoutTxs(rcp).length === 2) + assert(getClaimHtlcSuccessTxs(rcp).length == 0) + assert(getClaimHtlcTimeoutTxs(rcp).length == 2) } test("recv WatchFundingSpentTriggered (their *next* commit w/ pending unsigned htlcs)") { f => @@ -3133,11 +3133,11 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val mainTx = alice2blockchain.expectMsgType[PublishFinalTx].tx val mainPenaltyTx = alice2blockchain.expectMsgType[PublishFinalTx].tx val htlcPenaltyTxs = for (_ <- 0 until 4) yield alice2blockchain.expectMsgType[PublishFinalTx].tx - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === revokedTx.txid) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === mainTx.txid) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == revokedTx.txid) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == mainTx.txid) alice2blockchain.expectMsgType[WatchOutputSpent] // main-penalty // let's make sure that htlc-penalty txs each spend a different output - assert(htlcPenaltyTxs.map(_.txIn.head.outPoint.index).toSet.size === htlcPenaltyTxs.size) + assert(htlcPenaltyTxs.map(_.txIn.head.outPoint.index).toSet.size == htlcPenaltyTxs.size) htlcPenaltyTxs.foreach(_ => alice2blockchain.expectMsgType[WatchOutputSpent]) alice2blockchain.expectNoMessage(1 second) @@ -3146,12 +3146,12 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with htlcPenaltyTxs.foreach(htlcPenaltyTx => Transaction.correctlySpends(htlcPenaltyTx, Seq(revokedTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)) // two main outputs are 760 000 and 200 000 - assert(mainTx.txOut.head.amount === 741500.sat) - assert(mainPenaltyTx.txOut.head.amount === 195160.sat) - assert(htlcPenaltyTxs(0).txOut.head.amount === 4540.sat) - assert(htlcPenaltyTxs(1).txOut.head.amount === 4540.sat) - assert(htlcPenaltyTxs(2).txOut.head.amount === 4540.sat) - assert(htlcPenaltyTxs(3).txOut.head.amount === 4540.sat) + assert(mainTx.txOut.head.amount == 741500.sat) + assert(mainPenaltyTx.txOut.head.amount == 195160.sat) + assert(htlcPenaltyTxs(0).txOut.head.amount == 4540.sat) + assert(htlcPenaltyTxs(1).txOut.head.amount == 4540.sat) + assert(htlcPenaltyTxs(2).txOut.head.amount == 4540.sat) + assert(htlcPenaltyTxs(3).txOut.head.amount == 4540.sat) awaitCond(alice.stateName == CLOSING) assert(alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.size == 1) @@ -3198,9 +3198,9 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val mainPenaltyTx = alice2blockchain.expectMsgType[PublishFinalTx].tx val htlcPenaltyTxs = for (_ <- 0 until 2) yield alice2blockchain.expectMsgType[PublishFinalTx].tx // let's make sure that htlc-penalty txs each spend a different output - assert(htlcPenaltyTxs.map(_.txIn.head.outPoint.index).toSet.size === htlcPenaltyTxs.size) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === revokedTx.txid) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === mainTx.txid) + assert(htlcPenaltyTxs.map(_.txIn.head.outPoint.index).toSet.size == htlcPenaltyTxs.size) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == revokedTx.txid) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == mainTx.txid) alice2blockchain.expectMsgType[WatchOutputSpent] // main-penalty htlcPenaltyTxs.foreach(_ => alice2blockchain.expectMsgType[WatchOutputSpent]) alice2blockchain.expectNoMessage(1 second) @@ -3258,15 +3258,15 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // an error occurs and alice publishes her commit tx val aliceCommitTx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx alice ! Error(ByteVector32.Zeroes, "oops") - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid === aliceCommitTx.txid) + assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid == aliceCommitTx.txid) assert(aliceCommitTx.txOut.size == 6) // two main outputs and 4 pending htlcs awaitCond(alice.stateName == CLOSING) assert(alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.isDefined) val localCommitPublished = alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.get assert(localCommitPublished.commitTx.txid == aliceCommitTx.txid) - assert(localCommitPublished.htlcTxs.size === 4) - assert(getHtlcSuccessTxs(localCommitPublished).length === 1) - assert(getHtlcTimeoutTxs(localCommitPublished).length === 2) + assert(localCommitPublished.htlcTxs.size == 4) + assert(getHtlcSuccessTxs(localCommitPublished).length == 1) + assert(getHtlcTimeoutTxs(localCommitPublished).length == 2) assert(localCommitPublished.claimHtlcDelayedTxs.isEmpty) // alice can only claim 3 out of 4 htlcs, she can't do anything regarding the htlc sent by bob for which she does not have the preimage @@ -3281,8 +3281,8 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // the main delayed output and htlc txs spend the commitment transaction Seq(claimMain, htlcTx1, htlcTx2, htlcTx3).foreach(tx => Transaction.correctlySpends(tx, aliceCommitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === aliceCommitTx.txid) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === claimMain.txid) // main-delayed + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == aliceCommitTx.txid) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == claimMain.txid) // main-delayed alice2blockchain.expectMsgType[WatchOutputSpent] // htlc 1 alice2blockchain.expectMsgType[WatchOutputSpent] // htlc 2 alice2blockchain.expectMsgType[WatchOutputSpent] // htlc 3 @@ -3292,11 +3292,11 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // 3rd-stage txs are published when htlc txs confirm Seq(htlcTx1, htlcTx2, htlcTx3).foreach { htlcTimeoutTx => alice ! WatchOutputSpentTriggered(htlcTimeoutTx) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === htlcTimeoutTx.txid) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == htlcTimeoutTx.txid) alice ! WatchTxConfirmedTriggered(BlockHeight(2701), 3, htlcTimeoutTx) val claimHtlcDelayedTx = alice2blockchain.expectMsgType[PublishFinalTx].tx Transaction.correctlySpends(claimHtlcDelayedTx, htlcTimeoutTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === claimHtlcDelayedTx.txid) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == claimHtlcDelayedTx.txid) } awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.get.claimHtlcDelayedTxs.length == 3) alice2blockchain.expectNoMessage(1 second) @@ -3316,12 +3316,12 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // an error occurs and alice publishes her commit tx val aliceCommitTx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx alice ! Error(ByteVector32.Zeroes, "oops") - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid === aliceCommitTx.txid) + assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid == aliceCommitTx.txid) assert(aliceCommitTx.txOut.size == 8) // two main outputs, two anchors and 4 pending htlcs awaitCond(alice.stateName == CLOSING) val localAnchor = alice2blockchain.expectMsgType[PublishReplaceableTx] - assert(localAnchor.txInfo.confirmBefore.toLong === htlca1.cltvExpiry.toLong) // the target is set to match the first htlc that expires + assert(localAnchor.txInfo.confirmBefore.toLong == htlca1.cltvExpiry.toLong) // the target is set to match the first htlc that expires val claimMain = alice2blockchain.expectMsgType[PublishFinalTx] // alice can only claim 3 out of 4 htlcs, she can't do anything regarding the htlc sent by bob for which she does not have the preimage val htlcConfirmationTargets = Seq( @@ -3329,10 +3329,10 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice2blockchain.expectMsgType[PublishReplaceableTx], // htlc 2 alice2blockchain.expectMsgType[PublishReplaceableTx], // htlc 3 ).map(p => p.txInfo.asInstanceOf[HtlcTx].htlcId -> p.txInfo.confirmBefore.toLong).toMap - assert(htlcConfirmationTargets === Map(htlcb1.id -> htlcb1.cltvExpiry.toLong, htlca1.id -> htlca1.cltvExpiry.toLong, htlca2.id -> htlca2.cltvExpiry.toLong)) + assert(htlcConfirmationTargets == Map(htlcb1.id -> htlcb1.cltvExpiry.toLong, htlca1.id -> htlca1.cltvExpiry.toLong, htlca2.id -> htlca2.cltvExpiry.toLong)) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === aliceCommitTx.txid) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === claimMain.tx.txid) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == aliceCommitTx.txid) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == claimMain.tx.txid) val watchedOutputs = Seq( alice2blockchain.expectMsgType[WatchOutputSpent], // htlc 1 alice2blockchain.expectMsgType[WatchOutputSpent], // htlc 2 @@ -3341,7 +3341,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice2blockchain.expectMsgType[WatchOutputSpent], // local anchor ).map(w => OutPoint(w.txId.reverse, w.outputIndex)).toSet val localCommitPublished = alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.get - assert(watchedOutputs === localCommitPublished.htlcTxs.keySet + localAnchor.txInfo.input.outPoint) + assert(watchedOutputs == localCommitPublished.htlcTxs.keySet + localAnchor.txInfo.input.outPoint) alice2blockchain.expectNoMessage(1 second) } @@ -3351,7 +3351,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // an error occurs and alice publishes her commit tx val aliceCommitTx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx alice ! Error(ByteVector32.Zeroes, "oops") - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid === aliceCommitTx.txid) + assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid == aliceCommitTx.txid) assert(aliceCommitTx.txOut.size == 4) // two main outputs and two anchors awaitCond(alice.stateName == CLOSING) @@ -3359,11 +3359,11 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val blockTargets = alice.underlyingActor.nodeParams.onChainFeeConf.feeTargets val localAnchor = alice2blockchain.expectMsgType[PublishReplaceableTx] // When there are no pending HTLCs, there is no rush to get the commit tx confirmed - assert(localAnchor.txInfo.confirmBefore === currentBlockHeight + blockTargets.commitmentWithoutHtlcsBlockTarget) + assert(localAnchor.txInfo.confirmBefore == currentBlockHeight + blockTargets.commitmentWithoutHtlcsBlockTarget) val claimMain = alice2blockchain.expectMsgType[PublishFinalTx] - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === aliceCommitTx.txid) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === claimMain.tx.txid) - assert(alice2blockchain.expectMsgType[WatchOutputSpent].outputIndex === localAnchor.input.index) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == aliceCommitTx.txid) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == claimMain.tx.txid) + assert(alice2blockchain.expectMsgType[WatchOutputSpent].outputIndex == localAnchor.input.index) alice2blockchain.expectNoMessage(1 second) } @@ -3376,7 +3376,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // an error occurs and alice publishes her commit tx val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx bob ! Error(ByteVector32.Zeroes, "oops") - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid === bobCommitTx.txid) + assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == bobCommitTx.txid) assert(bobCommitTx.txOut.size == 1) // only one main output alice2blockchain.expectNoMessage(1 second) @@ -3458,7 +3458,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val normal = alice.stateData.asInstanceOf[DATA_NORMAL] normal.shortChannelId == annSigsA.shortChannelId && normal.buried && normal.channelAnnouncement.contains(channelAnn) && normal.channelUpdate.shortChannelId == annSigsA.shortChannelId }) - assert(channelUpdateListener.expectMsgType[LocalChannelUpdate].channelAnnouncement_opt === Some(channelAnn)) + assert(channelUpdateListener.expectMsgType[LocalChannelUpdate].channelAnnouncement_opt == Some(channelAnn)) } test("recv AnnouncementSignatures (re-send)", Tag(ChannelStateTestsTags.ChannelsPublic)) { f => @@ -3471,7 +3471,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with import initialState.commitments.{localParams, remoteParams} val channelAnn = Announcements.makeChannelAnnouncement(Alice.nodeParams.chainHash, annSigsA.shortChannelId, Alice.nodeParams.nodeId, remoteParams.nodeId, Alice.channelKeyManager.fundingPublicKey(localParams.fundingKeyPath).publicKey, remoteParams.fundingPubKey, annSigsA.nodeSignature, annSigsB.nodeSignature, annSigsA.bitcoinSignature, annSigsB.bitcoinSignature) bob2alice.forward(alice) - awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].channelAnnouncement === Some(channelAnn)) + awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].channelAnnouncement == Some(channelAnn)) // actual test starts here // simulate bob re-sending its sigs @@ -3601,8 +3601,8 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // actual test starts here Thread.sleep(1100) alice ! INPUT_DISCONNECTED - assert(alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.DisconnectedBeforeSigned]].htlc.paymentHash === htlc1.paymentHash) - assert(alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.DisconnectedBeforeSigned]].htlc.paymentHash === htlc2.paymentHash) + assert(alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.DisconnectedBeforeSigned]].htlc.paymentHash == htlc1.paymentHash) + assert(alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.DisconnectedBeforeSigned]].htlc.paymentHash == htlc2.paymentHash) val update2a = channelUpdateListener.expectMsgType[LocalChannelUpdate] assert(update1a.channelUpdate.timestamp < update2a.channelUpdate.timestamp) assert(!update2a.channelUpdate.channelFlags.isEnabled) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/OfflineStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/OfflineStateSpec.scala index de429c16fe..cca34ac7d0 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/OfflineStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/OfflineStateSpec.scala @@ -256,11 +256,11 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with reconnect(alice, bob, alice2bob, bob2alice) val reestablishA = alice2bob.expectMsgType[ChannelReestablish] - assert(reestablishA.nextLocalCommitmentNumber === 4) - assert(reestablishA.nextRemoteRevocationNumber === 3) + assert(reestablishA.nextLocalCommitmentNumber == 4) + assert(reestablishA.nextRemoteRevocationNumber == 3) val reestablishB = bob2alice.expectMsgType[ChannelReestablish] - assert(reestablishB.nextLocalCommitmentNumber === 5) - assert(reestablishB.nextRemoteRevocationNumber === 3) + assert(reestablishB.nextLocalCommitmentNumber == 5) + assert(reestablishB.nextRemoteRevocationNumber == 3) bob2alice.forward(alice, reestablishB) // alice does not re-send messages bob already received @@ -275,8 +275,8 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice2bob.expectMsgType[RevokeAndAck] alice2bob.forward(bob) - assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.index === 4) - assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.index === 4) + assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.index == 4) + assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.index == 4) } test("reconnect with an outdated commitment") { f => @@ -313,7 +313,7 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with bob2alice.forward(alice, reestablishB) // ... and ask bob to publish its current commitment val error = alice2bob.expectMsgType[Error] - assert(error === Error(channelId(alice), PleasePublishYourCommitment(channelId(alice)).getMessage)) + assert(error == Error(channelId(alice), PleasePublishYourCommitment(channelId(alice)).getMessage)) alice2bob.forward(bob) // alice now waits for bob to publish its commitment @@ -370,7 +370,7 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with bob2alice.forward(alice, reestablishB) // alice asks bob to publish its current commitment val error = alice2bob.expectMsgType[Error] - assert(error === Error(channelId(alice), PleasePublishYourCommitment(channelId(alice)).getMessage)) + assert(error == Error(channelId(alice), PleasePublishYourCommitment(channelId(alice)).getMessage)) alice2bob.forward(bob) // alice now waits for bob to publish its commitment @@ -400,10 +400,10 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // alice then finds out bob is lying bob2alice.send(alice, invalidReestablish) val error = alice2bob.expectMsgType[Error] - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid === aliceCommitTx.txid) + assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid == aliceCommitTx.txid) val claimMainOutput = alice2blockchain.expectMsgType[PublishFinalTx].tx Transaction.correctlySpends(claimMainOutput, aliceCommitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - assert(error === Error(channelId(alice), InvalidRevokedCommitProof(channelId(alice), 0, 42, invalidReestablish.yourLastPerCommitmentSecret).getMessage)) + assert(error == Error(channelId(alice), InvalidRevokedCommitProof(channelId(alice), 0, 42, invalidReestablish.yourLastPerCommitmentSecret).getMessage)) } test("change relay fee while offline") { f => @@ -434,8 +434,8 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // then alice reaches NORMAL state, and after a delay she broadcasts the channel_update val channelUpdate = channelUpdateListener.expectMsgType[LocalChannelUpdate](20 seconds).channelUpdate - assert(channelUpdate.feeBaseMsat === 4200.msat) - assert(channelUpdate.feeProportionalMillionths === 123456) + assert(channelUpdate.feeBaseMsat == 4200.msat) + assert(channelUpdate.feeProportionalMillionths == 123456) assert(channelUpdate.channelFlags.isEnabled) // no more messages @@ -544,16 +544,16 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(isFatal) assert(err.isInstanceOf[HtlcsWillTimeoutUpstream]) - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid === initialCommitTx.txid) + assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == initialCommitTx.txid) bob2blockchain.expectMsgType[PublishTx] // main delayed - assert(bob2blockchain.expectMsgType[WatchTxConfirmed].txId === initialCommitTx.txid) + assert(bob2blockchain.expectMsgType[WatchTxConfirmed].txId == initialCommitTx.txid) bob2blockchain.expectMsgType[WatchTxConfirmed] // main delayed bob2blockchain.expectMsgType[WatchOutputSpent] // htlc - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid === initialCommitTx.txid) + assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == initialCommitTx.txid) bob2blockchain.expectMsgType[PublishTx] // main delayed - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txOut === htlcSuccessTx.txOut) - assert(bob2blockchain.expectMsgType[WatchTxConfirmed].txId === initialCommitTx.txid) + assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txOut == htlcSuccessTx.txOut) + assert(bob2blockchain.expectMsgType[WatchTxConfirmed].txId == initialCommitTx.txid) bob2blockchain.expectMsgType[WatchTxConfirmed] // main delayed bob2blockchain.expectMsgType[WatchOutputSpent] // htlc bob2blockchain.expectNoMessage(500 millis) @@ -607,7 +607,7 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // alice is funder alice ! CurrentFeerates(networkFeerate) if (shouldClose) { - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid === aliceCommitTx.txid) + assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid == aliceCommitTx.txid) } else { alice2blockchain.expectNoMessage() } @@ -716,7 +716,7 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // bob is fundee bob ! CurrentFeerates(networkFeerate) if (shouldClose) { - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid === bobCommitTx.txid) + assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == bobCommitTx.txid) } else { bob2blockchain.expectNoMessage() } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/f/ShutdownStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/f/ShutdownStateSpec.scala index 4ba4bebb71..a7e5bb68a8 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/f/ShutdownStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/f/ShutdownStateSpec.scala @@ -185,7 +185,7 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit alice ! fulfill alice2bob.expectMsgType[Error] awaitCond(alice.stateName == CLOSING) - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid === tx.txid) // commit tx + assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid == tx.txid) // commit tx alice2blockchain.expectMsgType[PublishTx] // main delayed alice2blockchain.expectMsgType[PublishTx] // htlc timeout 1 alice2blockchain.expectMsgType[PublishTx] // htlc timeout 2 @@ -198,7 +198,7 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit alice ! UpdateFulfillHtlc(ByteVector32.Zeroes, 42, ByteVector32.Zeroes) alice2bob.expectMsgType[Error] awaitCond(alice.stateName == CLOSING) - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid === tx.txid) // commit tx + assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid == tx.txid) // commit tx alice2blockchain.expectMsgType[PublishTx] // main delayed alice2blockchain.expectMsgType[PublishTx] // htlc timeout 1 alice2blockchain.expectMsgType[PublishTx] // htlc timeout 2 @@ -289,7 +289,7 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit alice ! UpdateFailHtlc(ByteVector32.Zeroes, 42, ByteVector.fill(152)(0)) alice2bob.expectMsgType[Error] awaitCond(alice.stateName == CLOSING) - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid === tx.txid) // commit tx + assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid == tx.txid) // commit tx alice2blockchain.expectMsgType[PublishTx] // main delayed alice2blockchain.expectMsgType[PublishTx] // htlc timeout 1 alice2blockchain.expectMsgType[PublishTx] // htlc timeout 2 @@ -310,9 +310,9 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit val fail = UpdateFailMalformedHtlc(ByteVector32.Zeroes, 1, Crypto.sha256(ByteVector.empty), 42) alice ! fail val error = alice2bob.expectMsgType[Error] - assert(new String(error.data.toArray) === InvalidFailureCode(ByteVector32.Zeroes).getMessage) + assert(new String(error.data.toArray) == InvalidFailureCode(ByteVector32.Zeroes).getMessage) awaitCond(alice.stateName == CLOSING) - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid === tx.txid) // commit tx + assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid == tx.txid) // commit tx alice2blockchain.expectMsgType[PublishTx] // main delayed alice2blockchain.expectMsgType[PublishTx] // htlc timeout 1 alice2blockchain.expectMsgType[PublishTx] // htlc timeout 2 @@ -354,12 +354,12 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit bob2alice.expectMsgType[CommitSig] awaitCond(bob.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.remoteNextCommitInfo.isLeft) val waitForRevocation = bob.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.remoteNextCommitInfo.left.toOption.get - assert(waitForRevocation.reSignAsap === false) + assert(waitForRevocation.reSignAsap == false) // actual test starts here bob ! CMD_SIGN(replyTo_opt = Some(sender.ref)) sender.expectNoMessage(300 millis) - assert(bob.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.remoteNextCommitInfo === Left(waitForRevocation)) + assert(bob.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.remoteNextCommitInfo == Left(waitForRevocation)) } test("recv CommitSig") { f => @@ -380,7 +380,7 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit bob ! CommitSig(ByteVector32.Zeroes, ByteVector64.Zeroes, Nil) bob2alice.expectMsgType[Error] awaitCond(bob.stateName == CLOSING) - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid === tx.txid) // commit tx + assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == tx.txid) // commit tx bob2blockchain.expectMsgType[PublishTx] // main delayed bob2blockchain.expectMsgType[WatchTxConfirmed] } @@ -391,7 +391,7 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit bob ! CommitSig(ByteVector32.Zeroes, ByteVector64.Zeroes, Nil) bob2alice.expectMsgType[Error] awaitCond(bob.stateName == CLOSING) - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid === tx.txid) // commit tx + assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == tx.txid) // commit tx bob2blockchain.expectMsgType[PublishTx] // main delayed bob2blockchain.expectMsgType[WatchTxConfirmed] } @@ -447,7 +447,7 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit bob ! RevokeAndAck(ByteVector32.Zeroes, PrivateKey(randomBytes32()), PrivateKey(randomBytes32()).publicKey) bob2alice.expectMsgType[Error] awaitCond(bob.stateName == CLOSING) - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid === tx.txid) // commit tx + assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == tx.txid) // commit tx bob2blockchain.expectMsgType[PublishTx] // main delayed bob2blockchain.expectMsgType[PublishTx] // htlc success bob2blockchain.expectMsgType[WatchTxConfirmed] @@ -460,7 +460,7 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit alice ! RevokeAndAck(ByteVector32.Zeroes, PrivateKey(randomBytes32()), PrivateKey(randomBytes32()).publicKey) alice2bob.expectMsgType[Error] awaitCond(alice.stateName == CLOSING) - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid === tx.txid) // commit tx + assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid == tx.txid) // commit tx alice2blockchain.expectMsgType[PublishTx] // main delayed alice2blockchain.expectMsgType[PublishTx] // htlc timeout 1 alice2blockchain.expectMsgType[PublishTx] // htlc timeout 2 @@ -487,7 +487,7 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit bob2alice.forward(alice) // alice will forward the fail upstream val forward = alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.RemoteFail]] - assert(forward.result.fail === fail) + assert(forward.result.fail == fail) } test("recv RevokeAndAck (forward UpdateFailMalformedHtlc)") { f => @@ -510,7 +510,7 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit bob2alice.forward(alice) // alice will forward the fail upstream val forward = alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.RemoteFailMalformed]] - assert(forward.result.fail === fail) + assert(forward.result.fail == fail) } test("recv CMD_UPDATE_FEE") { f => @@ -548,7 +548,7 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit alice ! UpdateFee(ByteVector32.Zeroes, FeeratePerKw(12000 sat)) alice2bob.expectMsgType[Error] awaitCond(alice.stateName == CLOSING) - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid === tx.txid) // commit tx + assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid == tx.txid) // commit tx alice2blockchain.expectMsgType[PublishTx] // main delayed alice2blockchain.expectMsgType[PublishTx] // htlc timeout 1 alice2blockchain.expectMsgType[PublishTx] // htlc timeout 2 @@ -563,9 +563,9 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit bob.feeEstimator.setFeerate(FeeratesPerKw.single(fee.feeratePerKw)) bob ! fee val error = bob2alice.expectMsgType[Error] - assert(new String(error.data.toArray) === CannotAffordFees(channelId(bob), missing = 72120000L sat, reserve = 20000L sat, fees = 72400000L sat).getMessage) + assert(new String(error.data.toArray) == CannotAffordFees(channelId(bob), missing = 72120000L sat, reserve = 20000L sat, fees = 72400000L sat).getMessage) awaitCond(bob.stateName == CLOSING) - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid === tx.txid) // commit tx + assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == tx.txid) // commit tx //bob2blockchain.expectMsgType[PublishTx] // main delayed (removed because of the high fees) bob2blockchain.expectMsgType[WatchTxConfirmed] } @@ -575,9 +575,9 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit val tx = bob.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx bob ! UpdateFee(ByteVector32.Zeroes, FeeratePerKw(65000 sat)) val error = bob2alice.expectMsgType[Error] - assert(new String(error.data.toArray) === "local/remote feerates are too different: remoteFeeratePerKw=65000 localFeeratePerKw=10000") + assert(new String(error.data.toArray) == "local/remote feerates are too different: remoteFeeratePerKw=65000 localFeeratePerKw=10000") awaitCond(bob.stateName == CLOSING) - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid === tx.txid) // commit tx + assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == tx.txid) // commit tx bob2blockchain.expectMsgType[PublishTx] // main delayed bob2blockchain.expectMsgType[WatchTxConfirmed] } @@ -587,9 +587,9 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit val tx = bob.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx bob ! UpdateFee(ByteVector32.Zeroes, FeeratePerKw(252 sat)) val error = bob2alice.expectMsgType[Error] - assert(new String(error.data.toArray) === "remote fee rate is too small: remoteFeeratePerKw=252") + assert(new String(error.data.toArray) == "remote fee rate is too small: remoteFeeratePerKw=252") awaitCond(bob.stateName == CLOSING) - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid === tx.txid) // commit tx + assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == tx.txid) // commit tx bob2blockchain.expectMsgType[PublishTx] // main delayed bob2blockchain.expectMsgType[WatchTxConfirmed] } @@ -616,11 +616,11 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit val initialState = alice.stateData.asInstanceOf[DATA_SHUTDOWN] val aliceCommitTx = initialState.commitments.localCommit.commitTxAndRemoteSig.commitTx.tx alice ! CurrentBlockHeight(BlockHeight(400145)) - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid === aliceCommitTx.txid) // commit tx + assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid == aliceCommitTx.txid) // commit tx alice2blockchain.expectMsgType[PublishTx] // main delayed alice2blockchain.expectMsgType[PublishTx] // htlc timeout 1 alice2blockchain.expectMsgType[PublishTx] // htlc timeout 2 - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === aliceCommitTx.txid) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == aliceCommitTx.txid) } test("recv CurrentFeerate (when funder, triggers an UpdateFee)") { f => @@ -634,7 +634,7 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit test("recv CurrentFeerate (when funder, triggers an UpdateFee, anchor outputs)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => import f._ val initialState = alice.stateData.asInstanceOf[DATA_SHUTDOWN] - assert(initialState.commitments.localCommit.spec.commitTxFeerate === TestConstants.anchorOutputsFeeratePerKw) + assert(initialState.commitments.localCommit.spec.commitTxFeerate == TestConstants.anchorOutputsFeeratePerKw) alice ! CurrentFeerates(FeeratesPerKw.single(TestConstants.anchorOutputsFeeratePerKw / 2).copy(mempoolMinFee = FeeratePerKw(250 sat))) alice2bob.expectMsg(UpdateFee(initialState.commitments.channelId, TestConstants.anchorOutputsFeeratePerKw / 2)) } @@ -649,7 +649,7 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit test("recv CurrentFeerate (when funder, doesn't trigger an UpdateFee, anchor outputs)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => import f._ val initialState = alice.stateData.asInstanceOf[DATA_SHUTDOWN] - assert(initialState.commitments.localCommit.spec.commitTxFeerate === TestConstants.anchorOutputsFeeratePerKw) + assert(initialState.commitments.localCommit.spec.commitTxFeerate == TestConstants.anchorOutputsFeeratePerKw) alice ! CurrentFeerates(FeeratesPerKw.single(TestConstants.anchorOutputsFeeratePerKw * 2).copy(mempoolMinFee = FeeratePerKw(250 sat))) alice2bob.expectNoMessage(500 millis) } @@ -691,10 +691,10 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit }).sum // htlc will timeout and be eventually refunded so we have a little less than fundingSatoshis - pushMsat = 1000000 - 200000 = 800000 (because fees) val amountClaimed = htlcAmountClaimed + claimMain.txOut.head.amount - assert(amountClaimed === 774040.sat) + assert(amountClaimed == 774040.sat) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === bobCommitTx.txid) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === claimMain.txid) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == bobCommitTx.txid) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == claimMain.txid) alice2blockchain.expectMsgType[WatchOutputSpent] alice2blockchain.expectMsgType[WatchOutputSpent] alice2blockchain.expectNoMessage(1 second) @@ -702,9 +702,9 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit awaitCond(alice.stateName == CLOSING) assert(alice.stateData.asInstanceOf[DATA_CLOSING].remoteCommitPublished.isDefined) val rcp = alice.stateData.asInstanceOf[DATA_CLOSING].remoteCommitPublished.get - assert(rcp.claimHtlcTxs.size === 2) - assert(getClaimHtlcSuccessTxs(rcp).length === 0) - assert(getClaimHtlcTimeoutTxs(rcp).length === 2) + assert(rcp.claimHtlcTxs.size == 2) + assert(getClaimHtlcSuccessTxs(rcp).length == 0) + assert(getClaimHtlcTimeoutTxs(rcp).length == 2) } test("recv WatchFundingSpentTriggered (their next commit)") { f => @@ -741,19 +741,19 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit claimTx.txOut.head.amount }).sum // htlc will timeout and be eventually refunded so we have a little less than fundingSatoshis - pushMsat - htlc1 = 1000000 - 200000 - 300 000 = 500000 (because fees) - assert(amountClaimed === 481210.sat) + assert(amountClaimed == 481210.sat) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === bobCommitTx.txid) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === claimTxs(0).txid) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == bobCommitTx.txid) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == claimTxs(0).txid) alice2blockchain.expectMsgType[WatchOutputSpent] alice2blockchain.expectNoMessage(1 second) awaitCond(alice.stateName == CLOSING) assert(alice.stateData.asInstanceOf[DATA_CLOSING].nextRemoteCommitPublished.isDefined) val rcp = alice.stateData.asInstanceOf[DATA_CLOSING].nextRemoteCommitPublished.get - assert(rcp.claimHtlcTxs.size === 1) - assert(getClaimHtlcSuccessTxs(rcp).length === 0) - assert(getClaimHtlcTimeoutTxs(rcp).length === 1) + assert(rcp.claimHtlcTxs.size == 1) + assert(getClaimHtlcSuccessTxs(rcp).length == 0) + assert(getClaimHtlcTimeoutTxs(rcp).length == 1) } test("recv WatchFundingSpentTriggered (revoked tx)") { f => @@ -776,8 +776,8 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit val mainPenaltyTx = alice2blockchain.expectMsgType[PublishFinalTx].tx val htlc1PenaltyTx = alice2blockchain.expectMsgType[PublishFinalTx].tx val htlc2PenaltyTx = alice2blockchain.expectMsgType[PublishFinalTx].tx - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === revokedTx.txid) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === mainTx.txid) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == revokedTx.txid) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == mainTx.txid) alice2blockchain.expectMsgType[WatchOutputSpent] // main-penalty alice2blockchain.expectMsgType[WatchOutputSpent] // htlc1-penalty alice2blockchain.expectMsgType[WatchOutputSpent] // htlc2-penalty @@ -789,10 +789,10 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit Transaction.correctlySpends(htlc2PenaltyTx, Seq(revokedTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) // two main outputs are 300 000 and 200 000, htlcs are 300 000 and 200 000 - assert(mainTx.txOut.head.amount === 284940.sat) - assert(mainPenaltyTx.txOut.head.amount === 195160.sat) - assert(htlc1PenaltyTx.txOut.head.amount === 194540.sat) - assert(htlc2PenaltyTx.txOut.head.amount === 294540.sat) + assert(mainTx.txOut.head.amount == 284940.sat) + assert(mainPenaltyTx.txOut.head.amount == 195160.sat) + assert(htlc1PenaltyTx.txOut.head.amount == 194540.sat) + assert(htlc2PenaltyTx.txOut.head.amount == 294540.sat) awaitCond(alice.stateName == CLOSING) assert(alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.size == 1) @@ -801,13 +801,13 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit test("recv WatchFundingSpentTriggered (revoked tx with updated commitment)") { f => import f._ val initialCommitTx = bob.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx - assert(initialCommitTx.txOut.size === 4) // two main outputs + 2 htlc + assert(initialCommitTx.txOut.size == 4) // two main outputs + 2 htlc // bob fulfills one of the pending htlc (commitment update while in shutdown state) fulfillHtlc(0, r1, bob, alice, bob2alice, alice2bob) crossSign(bob, alice, bob2alice, alice2bob) val revokedTx = bob.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx - assert(revokedTx.txOut.size === 3) // two main outputs + 1 htlc + assert(revokedTx.txOut.size == 3) // two main outputs + 1 htlc // bob fulfills the second pending htlc (and revokes the previous commitment) fulfillHtlc(1, r2, bob, alice, bob2alice, alice2bob) @@ -821,8 +821,8 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit val mainTx = alice2blockchain.expectMsgType[PublishFinalTx].tx val mainPenaltyTx = alice2blockchain.expectMsgType[PublishFinalTx].tx val htlcPenaltyTx = alice2blockchain.expectMsgType[PublishFinalTx].tx - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === revokedTx.txid) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === mainTx.txid) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == revokedTx.txid) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == mainTx.txid) alice2blockchain.expectMsgType[WatchOutputSpent] // main-penalty alice2blockchain.expectMsgType[WatchOutputSpent] // htlc-penalty alice2blockchain.expectNoMessage(1 second) @@ -832,9 +832,9 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit Transaction.correctlySpends(htlcPenaltyTx, Seq(revokedTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) // two main outputs are 300 000 and 200 000, htlcs are 300 000 and 200 000 - assert(mainTx.txOut(0).amount === 286660.sat) - assert(mainPenaltyTx.txOut(0).amount === 495160.sat) - assert(htlcPenaltyTx.txOut(0).amount === 194540.sat) + assert(mainTx.txOut(0).amount == 286660.sat) + assert(mainPenaltyTx.txOut(0).amount == 495160.sat) + assert(htlcPenaltyTx.txOut(0).amount == 194540.sat) awaitCond(alice.stateName == CLOSING) assert(alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.size == 1) @@ -853,7 +853,7 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit val closingFeerates1 = ClosingFeerates(FeeratePerKw(500 sat), FeeratePerKw(250 sat), FeeratePerKw(2500 sat)) alice ! CMD_CLOSE(sender.ref, None, Some(closingFeerates1)) sender.expectMsgType[RES_SUCCESS[CMD_CLOSE]] - assert(alice.stateData.asInstanceOf[DATA_SHUTDOWN].closingFeerates === Some(closingFeerates1)) + assert(alice.stateData.asInstanceOf[DATA_SHUTDOWN].closingFeerates == Some(closingFeerates1)) val closingScript = alice.stateData.asInstanceOf[DATA_SHUTDOWN].localShutdown.scriptPubKey val closingFeerates2 = ClosingFeerates(FeeratePerKw(600 sat), FeeratePerKw(300 sat), FeeratePerKw(2500 sat)) @@ -861,7 +861,7 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit sender.expectMsgType[RES_FAILURE[CMD_CLOSE, ClosingAlreadyInProgress]] alice ! CMD_CLOSE(sender.ref, Some(closingScript), Some(closingFeerates2)) sender.expectMsgType[RES_SUCCESS[CMD_CLOSE]] - assert(alice.stateData.asInstanceOf[DATA_SHUTDOWN].closingFeerates === Some(closingFeerates2)) + assert(alice.stateData.asInstanceOf[DATA_SHUTDOWN].closingFeerates == Some(closingFeerates2)) } test("recv CMD_FORCECLOSE") { f => @@ -873,19 +873,19 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit val sender = TestProbe() alice ! CMD_FORCECLOSE(sender.ref) sender.expectMsgType[RES_SUCCESS[CMD_FORCECLOSE]] - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid === aliceCommitTx.txid) + assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid == aliceCommitTx.txid) awaitCond(alice.stateName == CLOSING) assert(alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.isDefined) val lcp = alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.get - assert(lcp.htlcTxs.size === 2) + assert(lcp.htlcTxs.size == 2) assert(lcp.claimHtlcDelayedTxs.isEmpty) // 3rd-stage txs will be published once htlc txs confirm val claimMain = alice2blockchain.expectMsgType[PublishFinalTx].tx val htlc1 = alice2blockchain.expectMsgType[PublishFinalTx].tx val htlc2 = alice2blockchain.expectMsgType[PublishFinalTx].tx Seq(claimMain, htlc1, htlc2).foreach(tx => Transaction.correctlySpends(tx, aliceCommitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === aliceCommitTx.txid) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === claimMain.txid) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == aliceCommitTx.txid) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == claimMain.txid) alice2blockchain.expectMsgType[WatchOutputSpent] alice2blockchain.expectMsgType[WatchOutputSpent] alice2blockchain.expectNoMessage(1 second) @@ -893,11 +893,11 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit // 3rd-stage txs are published when htlc txs confirm Seq(htlc1, htlc2).foreach(htlcTimeoutTx => { alice ! WatchOutputSpentTriggered(htlcTimeoutTx) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === htlcTimeoutTx.txid) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == htlcTimeoutTx.txid) alice ! WatchTxConfirmedTriggered(BlockHeight(2701), 3, htlcTimeoutTx) val claimHtlcDelayedTx = alice2blockchain.expectMsgType[PublishFinalTx].tx Transaction.correctlySpends(claimHtlcDelayedTx, htlcTimeoutTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === claimHtlcDelayedTx.txid) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == claimHtlcDelayedTx.txid) }) awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.get.claimHtlcDelayedTxs.length == 2) alice2blockchain.expectNoMessage(1 second) @@ -907,7 +907,7 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit import f._ val aliceCommitTx = alice.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx alice ! Error(ByteVector32.Zeroes, "oops") - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid === aliceCommitTx.txid) + assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid == aliceCommitTx.txid) assert(aliceCommitTx.txOut.size == 4) // two main outputs and two htlcs awaitCond(alice.stateName == CLOSING) assert(alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.isDefined) @@ -919,8 +919,8 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit val claimTxs = for (_ <- 0 until 3) yield alice2blockchain.expectMsgType[PublishFinalTx].tx // the main delayed output and htlc txs spend the commitment transaction claimTxs.foreach(tx => Transaction.correctlySpends(tx, aliceCommitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === aliceCommitTx.txid) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === claimTxs(0).txid) // main-delayed + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == aliceCommitTx.txid) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == claimTxs(0).txid) // main-delayed alice2blockchain.expectMsgType[WatchOutputSpent] alice2blockchain.expectMsgType[WatchOutputSpent] alice2blockchain.expectNoMessage(1 second) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/g/NegotiatingStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/g/NegotiatingStateSpec.scala index 075e3274c5..bf5acc288a 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/g/NegotiatingStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/g/NegotiatingStateSpec.scala @@ -123,19 +123,19 @@ class NegotiatingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike // alice is funder so she initiates the negotiation val aliceCloseSig1 = alice2bob.expectMsgType[ClosingSigned] - assert(aliceCloseSig1.feeSatoshis === 3370.sat) // matches a feerate of 5000 sat/kw + assert(aliceCloseSig1.feeSatoshis == 3370.sat) // matches a feerate of 5000 sat/kw assert(aliceCloseSig1.feeRange_opt.nonEmpty) assert(aliceCloseSig1.feeRange_opt.get.min < aliceCloseSig1.feeSatoshis) assert(aliceCloseSig1.feeSatoshis < aliceCloseSig1.feeRange_opt.get.max) - assert(alice.stateData.asInstanceOf[DATA_NEGOTIATING].closingTxProposed.length === 1) - assert(alice.stateData.asInstanceOf[DATA_NEGOTIATING].closingTxProposed.last.length === 1) + assert(alice.stateData.asInstanceOf[DATA_NEGOTIATING].closingTxProposed.length == 1) + assert(alice.stateData.asInstanceOf[DATA_NEGOTIATING].closingTxProposed.last.length == 1) assert(alice.stateData.asInstanceOf[DATA_NEGOTIATING].bestUnpublishedClosingTx_opt.isEmpty) if (alice.stateData.asInstanceOf[DATA_NEGOTIATING].commitments.channelFeatures.hasFeature(Features.UpfrontShutdownScript)) { // check that the closing tx uses Alice and Bob's default closing scripts val closingTx = alice.stateData.asInstanceOf[DATA_NEGOTIATING].closingTxProposed.last.head.unsignedTx.tx val expectedLocalScript = alice.stateData.asInstanceOf[DATA_NEGOTIATING].commitments.localParams.defaultFinalScriptPubKey val expectedRemoteScript = bob.stateData.asInstanceOf[DATA_NEGOTIATING].commitments.localParams.defaultFinalScriptPubKey - assert(closingTx.txOut.map(_.publicKeyScript).toSet === Set(expectedLocalScript, expectedRemoteScript)) + assert(closingTx.txOut.map(_.publicKeyScript).toSet == Set(expectedLocalScript, expectedRemoteScript)) } alice2bob.forward(bob) // bob answers with a counter proposition in alice's fee range @@ -148,19 +148,19 @@ class NegotiatingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike bob2alice.forward(alice) // alice accepts this proposition val aliceCloseSig2 = alice2bob.expectMsgType[ClosingSigned] - assert(aliceCloseSig2.feeSatoshis === bobCloseSig1.feeSatoshis) + assert(aliceCloseSig2.feeSatoshis == bobCloseSig1.feeSatoshis) alice2bob.forward(bob) assert(alice.stateName == CLOSING) assert(bob.stateName == CLOSING) val mutualCloseTx = alice2blockchain.expectMsgType[PublishFinalTx].tx - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx === mutualCloseTx) - assert(mutualCloseTx.txOut.length === 2) // NB: in the anchor outputs case, anchors are removed from the closing tx + assert(bob2blockchain.expectMsgType[PublishFinalTx].tx == mutualCloseTx) + assert(mutualCloseTx.txOut.length == 2) // NB: in the anchor outputs case, anchors are removed from the closing tx assert(aliceCloseSig2.feeSatoshis > Transactions.weight2fee(TestConstants.anchorOutputsFeeratePerKw, mutualCloseTx.weight())) // NB: closing fee is allowed to be higher than commit tx fee when using anchor outputs - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === mutualCloseTx.txid) - assert(bob2blockchain.expectMsgType[WatchTxConfirmed].txId === mutualCloseTx.txid) - assert(alice.stateData.asInstanceOf[DATA_CLOSING].mutualClosePublished.map(_.tx) === List(mutualCloseTx)) - assert(bob.stateData.asInstanceOf[DATA_CLOSING].mutualClosePublished.map(_.tx) === List(mutualCloseTx)) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == mutualCloseTx.txid) + assert(bob2blockchain.expectMsgType[WatchTxConfirmed].txId == mutualCloseTx.txid) + assert(alice.stateData.asInstanceOf[DATA_CLOSING].mutualClosePublished.map(_.tx) == List(mutualCloseTx)) + assert(bob.stateData.asInstanceOf[DATA_CLOSING].mutualClosePublished.map(_.tx) == List(mutualCloseTx)) } test("recv ClosingSigned (theirCloseFee != ourCloseFee)") { f => @@ -187,7 +187,7 @@ class NegotiatingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike val aliceCloseSig = alice2bob.expectMsgType[ClosingSigned] alice2bob.forward(bob) val bobCloseSig = bob2alice.expectMsgType[ClosingSigned] - assert(bobCloseSig.feeSatoshis === aliceCloseSig.feeSatoshis) + assert(bobCloseSig.feeSatoshis == aliceCloseSig.feeSatoshis) } test("recv ClosingSigned (theirMaxCloseFee < ourCloseFee)") { f => @@ -198,7 +198,7 @@ class NegotiatingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike val aliceCloseSig = alice2bob.expectMsgType[ClosingSigned] alice2bob.forward(bob) val bobCloseSig = bob2alice.expectMsgType[ClosingSigned] - assert(bobCloseSig.feeSatoshis === aliceCloseSig.feeRange_opt.get.max) + assert(bobCloseSig.feeSatoshis == aliceCloseSig.feeRange_opt.get.max) } private def testClosingSignedSameFees(f: FixtureParam, bobInitiates: Boolean = false): Unit = { @@ -216,16 +216,16 @@ class NegotiatingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike // alice is funder so she initiates the negotiation val aliceCloseSig1 = alice2bob.expectMsgType[ClosingSigned] - assert(aliceCloseSig1.feeSatoshis === 3370.sat) // matches a feerate of 5 000 sat/kw + assert(aliceCloseSig1.feeSatoshis == 3370.sat) // matches a feerate of 5 000 sat/kw assert(aliceCloseSig1.feeRange_opt.nonEmpty) alice2bob.forward(bob) // bob agrees with that proposal val bobCloseSig1 = bob2alice.expectMsgType[ClosingSigned] - assert(bobCloseSig1.feeSatoshis === aliceCloseSig1.feeSatoshis) + assert(bobCloseSig1.feeSatoshis == aliceCloseSig1.feeSatoshis) val mutualCloseTx = bob2blockchain.expectMsgType[PublishFinalTx].tx - assert(mutualCloseTx.txOut.length === 2) // NB: in the anchor outputs case, anchors are removed from the closing tx + assert(mutualCloseTx.txOut.length == 2) // NB: in the anchor outputs case, anchors are removed from the closing tx bob2alice.forward(alice) - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx === mutualCloseTx) + assert(alice2blockchain.expectMsgType[PublishFinalTx].tx == mutualCloseTx) assert(alice.stateName == CLOSING) assert(bob.stateName == CLOSING) } @@ -253,20 +253,20 @@ class NegotiatingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike aliceClose(f, Some(ClosingFeerates(FeeratePerKw(2500 sat), FeeratePerKw(2000 sat), FeeratePerKw(3000 sat)))) // alice initiates the negotiation with a very low feerate val aliceCloseSig = alice2bob.expectMsgType[ClosingSigned] - assert(aliceCloseSig.feeSatoshis === 1685.sat) - assert(aliceCloseSig.feeRange_opt === Some(FeeRange(1348 sat, 2022 sat))) + assert(aliceCloseSig.feeSatoshis == 1685.sat) + assert(aliceCloseSig.feeRange_opt == Some(FeeRange(1348 sat, 2022 sat))) alice2bob.forward(bob) // bob chooses alice's highest fee val bobCloseSig = bob2alice.expectMsgType[ClosingSigned] - assert(bobCloseSig.feeSatoshis === 2022.sat) + assert(bobCloseSig.feeSatoshis == 2022.sat) bob2alice.forward(alice) // alice accepts this proposition - assert(alice2bob.expectMsgType[ClosingSigned].feeSatoshis === 2022.sat) + assert(alice2bob.expectMsgType[ClosingSigned].feeSatoshis == 2022.sat) alice2bob.forward(bob) val mutualCloseTx = alice2blockchain.expectMsgType[PublishFinalTx].tx - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx === mutualCloseTx) - awaitCond(alice.stateName === CLOSING) - awaitCond(bob.stateName === CLOSING) + assert(bob2blockchain.expectMsgType[PublishFinalTx].tx == mutualCloseTx) + awaitCond(alice.stateName == CLOSING) + awaitCond(bob.stateName == CLOSING) } test("override on-chain fee estimator (fundee)") { f => @@ -276,16 +276,16 @@ class NegotiatingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike bobClose(f, Some(ClosingFeerates(FeeratePerKw(2500 sat), FeeratePerKw(2000 sat), FeeratePerKw(3000 sat)))) // alice is funder, so bob's override will simply be ignored val aliceCloseSig = alice2bob.expectMsgType[ClosingSigned] - assert(aliceCloseSig.feeSatoshis === 6740.sat) // matches a feerate of 10000 sat/kw + assert(aliceCloseSig.feeSatoshis == 6740.sat) // matches a feerate of 10000 sat/kw alice2bob.forward(bob) // bob directly agrees because their fee estimator matches val bobCloseSig = bob2alice.expectMsgType[ClosingSigned] - assert(aliceCloseSig.feeSatoshis === bobCloseSig.feeSatoshis) + assert(aliceCloseSig.feeSatoshis == bobCloseSig.feeSatoshis) bob2alice.forward(alice) val mutualCloseTx = alice2blockchain.expectMsgType[PublishFinalTx].tx - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx === mutualCloseTx) - awaitCond(alice.stateName === CLOSING) - awaitCond(bob.stateName === CLOSING) + assert(bob2blockchain.expectMsgType[PublishFinalTx].tx == mutualCloseTx) + awaitCond(alice.stateName == CLOSING) + awaitCond(bob.stateName == CLOSING) } test("recv ClosingSigned (nothing at stake)", Tag(ChannelStateTestsTags.NoPushMsat)) { f => @@ -296,9 +296,9 @@ class NegotiatingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike val aliceCloseFee = alice2bob.expectMsgType[ClosingSigned].feeSatoshis alice2bob.forward(bob) val bobCloseFee = bob2alice.expectMsgType[ClosingSigned].feeSatoshis - assert(aliceCloseFee === bobCloseFee) + assert(aliceCloseFee == bobCloseFee) bob2blockchain.expectMsgType[PublishTx] - awaitCond(bob.stateName === CLOSING) + awaitCond(bob.stateName == CLOSING) } private def makeLegacyClosingSigned(f: FixtureParam, closingFee: Satoshi): (ClosingSigned, ClosingSigned) = { @@ -320,9 +320,9 @@ class NegotiatingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike aliceClose(f) val aliceClosing1 = alice2bob.expectMsgType[ClosingSigned] val Some(FeeRange(_, maxFee)) = aliceClosing1.feeRange_opt - assert(aliceClosing1.feeSatoshis === 674.sat) - assert(maxFee === 1348.sat) - assert(alice.stateData.asInstanceOf[DATA_NEGOTIATING].closingTxProposed.last.length === 1) + assert(aliceClosing1.feeSatoshis == 674.sat) + assert(maxFee == 1348.sat) + assert(alice.stateData.asInstanceOf[DATA_NEGOTIATING].closingTxProposed.last.length == 1) assert(alice.stateData.asInstanceOf[DATA_NEGOTIATING].bestUnpublishedClosingTx_opt.isEmpty) // bob makes a proposal outside our fee range val (_, bobClosing1) = makeLegacyClosingSigned(f, 2500 sat) @@ -330,27 +330,27 @@ class NegotiatingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike val aliceClosing2 = alice2bob.expectMsgType[ClosingSigned] assert(aliceClosing1.feeSatoshis < aliceClosing2.feeSatoshis) assert(aliceClosing2.feeSatoshis < 1600.sat) - assert(alice.stateData.asInstanceOf[DATA_NEGOTIATING].closingTxProposed.last.length === 2) + assert(alice.stateData.asInstanceOf[DATA_NEGOTIATING].closingTxProposed.last.length == 2) assert(alice.stateData.asInstanceOf[DATA_NEGOTIATING].bestUnpublishedClosingTx_opt.nonEmpty) val (_, bobClosing2) = makeLegacyClosingSigned(f, 2000 sat) bob2alice.send(alice, bobClosing2) val aliceClosing3 = alice2bob.expectMsgType[ClosingSigned] assert(aliceClosing2.feeSatoshis < aliceClosing3.feeSatoshis) assert(aliceClosing3.feeSatoshis < 1800.sat) - assert(alice.stateData.asInstanceOf[DATA_NEGOTIATING].closingTxProposed.last.length === 3) + assert(alice.stateData.asInstanceOf[DATA_NEGOTIATING].closingTxProposed.last.length == 3) assert(alice.stateData.asInstanceOf[DATA_NEGOTIATING].bestUnpublishedClosingTx_opt.nonEmpty) val (_, bobClosing3) = makeLegacyClosingSigned(f, 1800 sat) bob2alice.send(alice, bobClosing3) val aliceClosing4 = alice2bob.expectMsgType[ClosingSigned] assert(aliceClosing3.feeSatoshis < aliceClosing4.feeSatoshis) assert(aliceClosing4.feeSatoshis < 1800.sat) - assert(alice.stateData.asInstanceOf[DATA_NEGOTIATING].closingTxProposed.last.length === 4) + assert(alice.stateData.asInstanceOf[DATA_NEGOTIATING].closingTxProposed.last.length == 4) assert(alice.stateData.asInstanceOf[DATA_NEGOTIATING].bestUnpublishedClosingTx_opt.nonEmpty) val (_, bobClosing4) = makeLegacyClosingSigned(f, aliceClosing4.feeSatoshis) bob2alice.send(alice, bobClosing4) - awaitCond(alice.stateName === CLOSING) - assert(alice.stateData.asInstanceOf[DATA_CLOSING].mutualClosePublished.length === 1) - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx === alice.stateData.asInstanceOf[DATA_CLOSING].mutualClosePublished.head.tx) + awaitCond(alice.stateName == CLOSING) + assert(alice.stateData.asInstanceOf[DATA_CLOSING].mutualClosePublished.length == 1) + assert(alice2blockchain.expectMsgType[PublishFinalTx].tx == alice.stateData.asInstanceOf[DATA_CLOSING].mutualClosePublished.head.tx) } test("recv ClosingSigned (other side ignores our fee range, fundee)") { f => @@ -362,34 +362,34 @@ class NegotiatingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike alice2bob.send(bob, aliceClosing1) val bobClosing1 = bob2alice.expectMsgType[ClosingSigned] assert(3000.sat < bobClosing1.feeSatoshis) - assert(bob.stateData.asInstanceOf[DATA_NEGOTIATING].closingTxProposed.last.length === 1) + assert(bob.stateData.asInstanceOf[DATA_NEGOTIATING].closingTxProposed.last.length == 1) assert(bob.stateData.asInstanceOf[DATA_NEGOTIATING].bestUnpublishedClosingTx_opt.nonEmpty) val (aliceClosing2, _) = makeLegacyClosingSigned(f, 750 sat) alice2bob.send(bob, aliceClosing2) val bobClosing2 = bob2alice.expectMsgType[ClosingSigned] assert(bobClosing2.feeSatoshis < bobClosing1.feeSatoshis) assert(2000.sat < bobClosing2.feeSatoshis) - assert(bob.stateData.asInstanceOf[DATA_NEGOTIATING].closingTxProposed.last.length === 2) + assert(bob.stateData.asInstanceOf[DATA_NEGOTIATING].closingTxProposed.last.length == 2) assert(bob.stateData.asInstanceOf[DATA_NEGOTIATING].bestUnpublishedClosingTx_opt.nonEmpty) val (aliceClosing3, _) = makeLegacyClosingSigned(f, 1000 sat) alice2bob.send(bob, aliceClosing3) val bobClosing3 = bob2alice.expectMsgType[ClosingSigned] assert(bobClosing3.feeSatoshis < bobClosing2.feeSatoshis) assert(1500.sat < bobClosing3.feeSatoshis) - assert(bob.stateData.asInstanceOf[DATA_NEGOTIATING].closingTxProposed.last.length === 3) + assert(bob.stateData.asInstanceOf[DATA_NEGOTIATING].closingTxProposed.last.length == 3) assert(bob.stateData.asInstanceOf[DATA_NEGOTIATING].bestUnpublishedClosingTx_opt.nonEmpty) val (aliceClosing4, _) = makeLegacyClosingSigned(f, 1300 sat) alice2bob.send(bob, aliceClosing4) val bobClosing4 = bob2alice.expectMsgType[ClosingSigned] assert(bobClosing4.feeSatoshis < bobClosing3.feeSatoshis) assert(1300.sat < bobClosing4.feeSatoshis) - assert(bob.stateData.asInstanceOf[DATA_NEGOTIATING].closingTxProposed.last.length === 4) + assert(bob.stateData.asInstanceOf[DATA_NEGOTIATING].closingTxProposed.last.length == 4) assert(bob.stateData.asInstanceOf[DATA_NEGOTIATING].bestUnpublishedClosingTx_opt.nonEmpty) val (aliceClosing5, _) = makeLegacyClosingSigned(f, bobClosing4.feeSatoshis) alice2bob.send(bob, aliceClosing5) - awaitCond(bob.stateName === CLOSING) - assert(bob.stateData.asInstanceOf[DATA_CLOSING].mutualClosePublished.length === 1) - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx === bob.stateData.asInstanceOf[DATA_CLOSING].mutualClosePublished.head.tx) + awaitCond(bob.stateName == CLOSING) + assert(bob.stateData.asInstanceOf[DATA_CLOSING].mutualClosePublished.length == 1) + assert(bob2blockchain.expectMsgType[PublishFinalTx].tx == bob.stateData.asInstanceOf[DATA_CLOSING].mutualClosePublished.head.tx) } test("recv ClosingSigned (other side ignores our fee range, max iterations reached)") { f => @@ -403,9 +403,9 @@ class NegotiatingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike val (_, bobClosing) = makeLegacyClosingSigned(f, bobNextFee) bob2alice.send(alice, bobClosing) } - awaitCond(alice.stateName === CLOSING) - assert(alice.stateData.asInstanceOf[DATA_CLOSING].mutualClosePublished.length === 1) - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx === alice.stateData.asInstanceOf[DATA_CLOSING].mutualClosePublished.head.tx) + awaitCond(alice.stateName == CLOSING) + assert(alice.stateData.asInstanceOf[DATA_CLOSING].mutualClosePublished.length == 1) + assert(alice2blockchain.expectMsgType[PublishFinalTx].tx == alice.stateData.asInstanceOf[DATA_CLOSING].mutualClosePublished.head.tx) } test("recv ClosingSigned (fee too low, fundee)") { f => @@ -429,7 +429,7 @@ class NegotiatingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike alice2bob.forward(bob, aliceCloseSig.copy(feeSatoshis = 99000 sat)) // sig doesn't matter, it is checked later val error = bob2alice.expectMsgType[Error] assert(new String(error.data.toArray).startsWith("invalid close fee: fee_satoshis=99000 sat")) - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid === tx.txid) + assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == tx.txid) bob2blockchain.expectMsgType[PublishTx] bob2blockchain.expectMsgType[WatchTxConfirmed] } @@ -442,7 +442,7 @@ class NegotiatingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike bob ! aliceCloseSig.copy(signature = ByteVector64.Zeroes) val error = bob2alice.expectMsgType[Error] assert(new String(error.data.toArray).startsWith("invalid close signature")) - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid === tx.txid) + assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == tx.txid) bob2blockchain.expectMsgType[PublishTx] bob2blockchain.expectMsgType[WatchTxConfirmed] } @@ -454,12 +454,12 @@ class NegotiatingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike alice2bob.forward(bob, aliceCloseSig) // at this point alice and bob agree on closing fees, but alice has not yet received the final signature whereas bob has // bob publishes the mutual close and alice is notified that the funding tx has been spent - assert(alice.stateName === NEGOTIATING) + assert(alice.stateName == NEGOTIATING) val mutualCloseTx = bob2blockchain.expectMsgType[PublishFinalTx].tx - assert(bob2blockchain.expectMsgType[WatchTxConfirmed].txId === mutualCloseTx.txid) + assert(bob2blockchain.expectMsgType[WatchTxConfirmed].txId == mutualCloseTx.txid) alice ! WatchFundingSpentTriggered(mutualCloseTx) - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx === mutualCloseTx) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === mutualCloseTx.txid) + assert(alice2blockchain.expectMsgType[PublishFinalTx].tx == mutualCloseTx) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == mutualCloseTx.txid) alice2blockchain.expectNoMessage(100 millis) assert(alice.stateName == CLOSING) } @@ -482,10 +482,10 @@ class NegotiatingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike assert(firstMutualCloseTx.tx.txid !== latestMutualCloseTx.tx.txid) // at this point bob will receive a new signature, but he decides instead to publish the first mutual close alice ! WatchFundingSpentTriggered(firstMutualCloseTx.tx) - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx === firstMutualCloseTx.tx) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === firstMutualCloseTx.tx.txid) + assert(alice2blockchain.expectMsgType[PublishFinalTx].tx == firstMutualCloseTx.tx) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == firstMutualCloseTx.tx.txid) alice2blockchain.expectNoMessage(100 millis) - assert(alice.stateName === CLOSING) + assert(alice.stateName == CLOSING) } test("recv WatchFundingSpentTriggered (self mutual close)") { f => @@ -498,11 +498,11 @@ class NegotiatingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike bob2alice.expectMsgType[ClosingSigned] // at this point bob has received a mutual close signature from alice, but doesn't yet agree on the fee // bob's mutual close is published from the outside of the actor - assert(bob.stateName === NEGOTIATING) + assert(bob.stateName == NEGOTIATING) val mutualCloseTx = bob.stateData.asInstanceOf[DATA_NEGOTIATING].bestUnpublishedClosingTx_opt.get.tx bob ! WatchFundingSpentTriggered(mutualCloseTx) - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx === mutualCloseTx) - assert(bob2blockchain.expectMsgType[WatchTxConfirmed].txId === mutualCloseTx.txid) + assert(bob2blockchain.expectMsgType[PublishFinalTx].tx == mutualCloseTx) + assert(bob2blockchain.expectMsgType[WatchTxConfirmed].txId == mutualCloseTx.txid) bob2blockchain.expectNoMessage(100 millis) assert(bob.stateName == CLOSING) } @@ -542,9 +542,9 @@ class NegotiatingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike val aliceClosing3 = alice2bob.expectMsgType[ClosingSigned] alice2bob.forward(bob, aliceClosing3) val mutualCloseTx = alice2blockchain.expectMsgType[PublishFinalTx].tx - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx === mutualCloseTx) - awaitCond(alice.stateName === CLOSING) - awaitCond(bob.stateName === CLOSING) + assert(bob2blockchain.expectMsgType[PublishFinalTx].tx == mutualCloseTx) + awaitCond(alice.stateName == CLOSING) + awaitCond(bob.stateName == CLOSING) } test("recv Error") { f => @@ -554,9 +554,9 @@ class NegotiatingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike val tx = alice.stateData.asInstanceOf[DATA_NEGOTIATING].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx alice ! Error(ByteVector32.Zeroes, "oops") awaitCond(alice.stateName == CLOSING) - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid === tx.txid) + assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid == tx.txid) alice2blockchain.expectMsgType[PublishTx] - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === tx.txid) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == tx.txid) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala index 1d1440ebac..52052b9e67 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala @@ -186,7 +186,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // test starts here alice ! GetTxWithMetaResponse(fundingTx.txid, None, TimestampSecond.now()) alice2bob.expectNoMessage(200 millis) - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx === fundingTx) // we republish the funding tx + assert(alice2blockchain.expectMsgType[PublishFinalTx].tx == fundingTx) // we republish the funding tx assert(alice.stateName == CLOSING) // the above expectNoMsg will make us wait, so this checks that we are still in CLOSING } @@ -277,7 +277,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with def testMutualCloseBeforeConverge(f: FixtureParam, channelFeatures: ChannelFeatures): Unit = { import f._ val sender = TestProbe() - assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.channelFeatures === channelFeatures) + assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.channelFeatures == channelFeatures) bob.feeEstimator.setFeerate(FeeratesPerKw.single(FeeratePerKw(2500 sat)).copy(mempoolMinFee = FeeratePerKw(250 sat), blocks_1008 = FeeratePerKw(250 sat))) // alice initiates a closing with a low fee alice ! CMD_CLOSE(sender.ref, None, Some(ClosingFeerates(FeeratePerKw(500 sat), FeeratePerKw(250 sat), FeeratePerKw(1000 sat)))) @@ -294,13 +294,13 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // let's make bob publish this closing tx bob ! Error(ByteVector32.Zeroes, "") awaitCond(bob.stateName == CLOSING) - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx === mutualCloseTx.tx) - assert(mutualCloseTx === bob.stateData.asInstanceOf[DATA_CLOSING].mutualClosePublished.last) + assert(bob2blockchain.expectMsgType[PublishFinalTx].tx == mutualCloseTx.tx) + assert(mutualCloseTx == bob.stateData.asInstanceOf[DATA_CLOSING].mutualClosePublished.last) // actual test starts here bob ! WatchFundingSpentTriggered(mutualCloseTx.tx) bob ! WatchTxConfirmedTriggered(BlockHeight(0), 0, mutualCloseTx.tx) - assert(txListener.expectMsgType[TransactionConfirmed].tx === mutualCloseTx.tx) + assert(txListener.expectMsgType[TransactionConfirmed].tx == mutualCloseTx.tx) awaitCond(bob.stateName == CLOSED) } @@ -353,15 +353,15 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val claimHtlcSuccessFromCommitTx = Transaction(version = 0, txIn = TxIn(outPoint = OutPoint(randomBytes32(), 0), signatureScript = ByteVector.empty, sequence = 0, witness = Scripts.witnessClaimHtlcSuccessFromCommitTx(Transactions.PlaceHolderSig, ra1, ByteVector.fill(130)(33))) :: Nil, txOut = Nil, lockTime = 0) alice ! WatchOutputSpentTriggered(claimHtlcSuccessFromCommitTx) val fulfill1 = alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFulfill]] - assert(fulfill1.htlc === htlca1) - assert(fulfill1.result.paymentPreimage === ra1) + assert(fulfill1.htlc == htlca1) + assert(fulfill1.result.paymentPreimage == ra1) // scenario 2: bob claims the htlc output from his own commit tx using its preimage (let's assume both parties had published their commitment tx) val claimHtlcSuccessTx = Transaction(version = 0, txIn = TxIn(outPoint = OutPoint(randomBytes32(), 0), signatureScript = ByteVector.empty, sequence = 0, witness = Scripts.witnessHtlcSuccess(Transactions.PlaceHolderSig, Transactions.PlaceHolderSig, ra1, ByteVector.fill(130)(33), Transactions.DefaultCommitmentFormat)) :: Nil, txOut = Nil, lockTime = 0) alice ! WatchOutputSpentTriggered(claimHtlcSuccessTx) val fulfill2 = alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFulfill]] - assert(fulfill2.htlc === htlca1) - assert(fulfill2.result.paymentPreimage === ra1) + assert(fulfill2.htlc == htlca1) + assert(fulfill2.result.paymentPreimage == ra1) assert(alice.stateData == initialState) // this was a no-op } @@ -369,7 +369,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with def testLocalCommitTxConfirmed(f: FixtureParam, channelFeatures: ChannelFeatures): Unit = { import f._ - assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.channelFeatures === channelFeatures) + assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.channelFeatures == channelFeatures) val listener = TestProbe() alice.underlying.system.eventStream.subscribe(listener.ref, classOf[LocalCommitConfirmed]) @@ -385,28 +385,28 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // actual test starts here assert(closingState.claimMainDelayedOutputTx.isDefined) - assert(closingState.htlcTxs.size === 1) + assert(closingState.htlcTxs.size == 1) assert(getHtlcSuccessTxs(closingState).isEmpty) - assert(getHtlcTimeoutTxs(closingState).length === 1) + assert(getHtlcTimeoutTxs(closingState).length == 1) val htlcTimeoutTx = getHtlcTimeoutTxs(closingState).head.tx - assert(closingState.claimHtlcDelayedTxs.length === 0) + assert(closingState.claimHtlcDelayedTxs.length == 0) alice ! WatchTxConfirmedTriggered(BlockHeight(42), 0, closingState.commitTx) - assert(txListener.expectMsgType[TransactionConfirmed].tx === closingState.commitTx) + assert(txListener.expectMsgType[TransactionConfirmed].tx == closingState.commitTx) assert(listener.expectMsgType[LocalCommitConfirmed].refundAtBlock == BlockHeight(42) + bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localParams.toSelfDelay.toInt) assert(listener.expectMsgType[PaymentSettlingOnChain].paymentHash == htlca1.paymentHash) // htlcs below dust will never reach the chain, once the commit tx is confirmed we can consider them failed - assert(alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc === htlca2) + assert(alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc == htlca2) alice2relayer.expectNoMessage(100 millis) alice ! WatchTxConfirmedTriggered(BlockHeight(200), 0, closingState.claimMainDelayedOutputTx.get.tx) alice ! WatchTxConfirmedTriggered(BlockHeight(201), 0, htlcTimeoutTx) - assert(alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.get.irrevocablySpent.values.toSet === Set(closingState.commitTx, closingState.claimMainDelayedOutputTx.get.tx, htlcTimeoutTx)) - assert(alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc === htlca1) + assert(alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.get.irrevocablySpent.values.toSet == Set(closingState.commitTx, closingState.claimMainDelayedOutputTx.get.tx, htlcTimeoutTx)) + assert(alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc == htlca1) alice2relayer.expectNoMessage(100 millis) // We claim the htlc-delayed output now that the HTLC tx has been confirmed. val claimHtlcDelayedTx = alice2blockchain.expectMsgType[PublishFinalTx] Transaction.correctlySpends(claimHtlcDelayedTx.tx, Seq(htlcTimeoutTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.get.claimHtlcDelayedTxs.length === 1) + awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.get.claimHtlcDelayedTxs.length == 1) alice ! WatchTxConfirmedTriggered(BlockHeight(202), 0, claimHtlcDelayedTx.tx) awaitCond(alice.stateName == CLOSED) @@ -440,18 +440,18 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // actual test starts here assert(closingState.claimMainDelayedOutputTx.isDefined) - assert(closingState.htlcTxs.size === 4) + assert(closingState.htlcTxs.size == 4) assert(getHtlcSuccessTxs(closingState).isEmpty) val htlcTimeoutTxs = getHtlcTimeoutTxs(closingState).map(_.tx) - assert(htlcTimeoutTxs.length === 4) - assert(closingState.claimHtlcDelayedTxs.length === 0) + assert(htlcTimeoutTxs.length == 4) + assert(closingState.claimHtlcDelayedTxs.length == 0) // if commit tx and htlc-timeout txs end up in the same block, we may receive the htlc-timeout confirmation before the commit tx confirmation alice ! WatchTxConfirmedTriggered(BlockHeight(42), 0, htlcTimeoutTxs(0)) val forwardedFail1 = alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc alice2relayer.expectNoMessage(250 millis) alice ! WatchTxConfirmedTriggered(BlockHeight(42), 1, closingState.commitTx) - assert(alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc === dust) + assert(alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc == dust) alice2relayer.expectNoMessage(250 millis) alice ! WatchTxConfirmedTriggered(BlockHeight(200), 0, closingState.claimMainDelayedOutputTx.get.tx) alice ! WatchTxConfirmedTriggered(BlockHeight(202), 0, htlcTimeoutTxs(1)) @@ -462,10 +462,10 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice2relayer.expectNoMessage(250 millis) alice ! WatchTxConfirmedTriggered(BlockHeight(203), 0, htlcTimeoutTxs(3)) val forwardedFail4 = alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc - assert(Set(forwardedFail1, forwardedFail2, forwardedFail3, forwardedFail4) === Set(htlca1, htlca2, htlca3, htlca4)) + assert(Set(forwardedFail1, forwardedFail2, forwardedFail3, forwardedFail4) == Set(htlca1, htlca2, htlca3, htlca4)) alice2relayer.expectNoMessage(250 millis) - awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.get.claimHtlcDelayedTxs.length === 4) + awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.get.claimHtlcDelayedTxs.length == 4) val claimHtlcDelayedTxs = alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.get.claimHtlcDelayedTxs alice ! WatchTxConfirmedTriggered(BlockHeight(203), 0, claimHtlcDelayedTxs(0).tx) alice ! WatchTxConfirmedTriggered(BlockHeight(203), 1, claimHtlcDelayedTxs(1).tx) @@ -508,7 +508,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with crossSign(bob, alice, bob2alice, alice2bob) alice2relayer.expectMsgType[RelayForward] val aliceCommitTx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx - assert(aliceCommitTx.txOut.size === 3) // 2 main outputs + 1 htlc + assert(aliceCommitTx.txOut.size == 3) // 2 main outputs + 1 htlc // alice fulfills the HTLC but bob doesn't receive the signature alice ! CMD_FULFILL_HTLC(htlc.id, r, commit = true) @@ -518,9 +518,9 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // note that bob doesn't receive the new sig! // then we make alice unilaterally close the channel val closingState = localClose(alice, alice2blockchain) - assert(closingState.commitTx.txid === aliceCommitTx.txid) + assert(closingState.commitTx.txid == aliceCommitTx.txid) assert(getHtlcTimeoutTxs(closingState).isEmpty) - assert(getHtlcSuccessTxs(closingState).length === 1) + assert(getHtlcSuccessTxs(closingState).length == 1) } test("recv WatchTxConfirmedTriggered (local commit with fail not acked by remote)") { f => @@ -541,7 +541,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // note that alice doesn't receive the last revocation // then we make alice unilaterally close the channel val closingState = localClose(alice, alice2blockchain) - assert(closingState.commitTx.txOut.length === 2) // htlc has been removed + assert(closingState.commitTx.txOut.length == 2) // htlc has been removed // actual test starts here channelUpdateListener.expectMsgType[LocalChannelDown] @@ -575,12 +575,12 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // the commit tx hasn't been confirmed yet, so we watch the funding output first alice2blockchain.expectMsgType[WatchFundingSpent] // then we should re-publish unconfirmed transactions - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx === closingState.commitTx) - closingState.claimMainDelayedOutputTx.foreach(claimMain => assert(alice2blockchain.expectMsgType[PublishFinalTx].tx === claimMain.tx)) - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx === htlcTimeoutTx.tx) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === closingState.commitTx.txid) - closingState.claimMainDelayedOutputTx.foreach(claimMain => assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === claimMain.tx.txid)) - assert(alice2blockchain.expectMsgType[WatchOutputSpent].outputIndex === htlcTimeoutTx.input.outPoint.index) + assert(alice2blockchain.expectMsgType[PublishFinalTx].tx == closingState.commitTx) + closingState.claimMainDelayedOutputTx.foreach(claimMain => assert(alice2blockchain.expectMsgType[PublishFinalTx].tx == claimMain.tx)) + assert(alice2blockchain.expectMsgType[PublishFinalTx].tx == htlcTimeoutTx.tx) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == closingState.commitTx.txid) + closingState.claimMainDelayedOutputTx.foreach(claimMain => assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == claimMain.tx.txid)) + assert(alice2blockchain.expectMsgType[WatchOutputSpent].outputIndex == htlcTimeoutTx.input.outPoint.index) // the htlc transaction confirms, so we publish a 3rd-stage transaction alice ! WatchTxConfirmedTriggered(BlockHeight(2701), 1, closingState.commitTx) @@ -588,8 +588,8 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.get.claimHtlcDelayedTxs.nonEmpty) val beforeSecondRestart = alice.stateData.asInstanceOf[DATA_CLOSING] val claimHtlcTimeoutTx = beforeSecondRestart.localCommitPublished.get.claimHtlcDelayedTxs.head - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx === claimHtlcTimeoutTx.tx) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === claimHtlcTimeoutTx.tx.txid) + assert(alice2blockchain.expectMsgType[PublishFinalTx].tx == claimHtlcTimeoutTx.tx) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == claimHtlcTimeoutTx.tx.txid) // simulate another node restart alice.setState(WAIT_FOR_INIT_INTERNAL, Nothing) @@ -598,10 +598,10 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with awaitCond(alice.stateName == CLOSING) // we should re-publish unconfirmed transactions - closingState.claimMainDelayedOutputTx.foreach(claimMain => assert(alice2blockchain.expectMsgType[PublishFinalTx].tx === claimMain.tx)) - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx === claimHtlcTimeoutTx.tx) - closingState.claimMainDelayedOutputTx.foreach(claimMain => assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === claimMain.tx.txid)) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === claimHtlcTimeoutTx.tx.txid) + closingState.claimMainDelayedOutputTx.foreach(claimMain => assert(alice2blockchain.expectMsgType[PublishFinalTx].tx == claimMain.tx)) + assert(alice2blockchain.expectMsgType[PublishFinalTx].tx == claimHtlcTimeoutTx.tx) + closingState.claimMainDelayedOutputTx.foreach(claimMain => assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == claimMain.tx.txid)) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == claimHtlcTimeoutTx.tx.txid) } test("recv WatchTxConfirmedTriggered (remote commit with htlcs only signed by local in next remote commit)") { f => @@ -641,7 +641,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(closingState.claimHtlcTxs.isEmpty) assert(alice.stateData.asInstanceOf[DATA_CLOSING].copy(remoteCommitPublished = None) == initialState) val txPublished = txListener.expectMsgType[TransactionPublished] - assert(txPublished.tx === bobCommitTx) + assert(txPublished.tx == bobCommitTx) assert(txPublished.miningFee > 0.sat) // alice is funder, she pays the fee for the remote commit } @@ -649,7 +649,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with import f._ mutualClose(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain) val initialState = alice.stateData.asInstanceOf[DATA_CLOSING] - assert(initialState.commitments.channelFeatures === ChannelFeatures()) + assert(initialState.commitments.channelFeatures == ChannelFeatures()) // bob publishes his last current commit tx, the one it had when entering NEGOTIATING state val bobCommitTx = bobCommitTxs.last.commitTx.tx assert(bobCommitTx.txOut.size == 2) // two main outputs @@ -662,14 +662,14 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with txListener.expectMsgType[TransactionPublished] alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, bobCommitTx) alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, closingState.claimMainOutputTx.get.tx) - assert(txListener.expectMsgType[TransactionConfirmed].tx === bobCommitTx) + assert(txListener.expectMsgType[TransactionConfirmed].tx == bobCommitTx) awaitCond(alice.stateName == CLOSED) } test("recv WatchTxConfirmedTriggered (remote commit, option_static_remotekey)", Tag(ChannelStateTestsTags.StaticRemoteKey)) { f => import f._ mutualClose(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain) - assert(alice.stateData.asInstanceOf[DATA_CLOSING].commitments.channelFeatures === ChannelFeatures(Features.StaticRemoteKey)) + assert(alice.stateData.asInstanceOf[DATA_CLOSING].commitments.channelFeatures == ChannelFeatures(Features.StaticRemoteKey)) // bob publishes his last current commit tx, the one it had when entering NEGOTIATING state val bobCommitTx = bobCommitTxs.last.commitTx.tx assert(bobCommitTx.txOut.size == 2) // two main outputs @@ -687,7 +687,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with import f._ mutualClose(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain) val initialState = alice.stateData.asInstanceOf[DATA_CLOSING] - assert(initialState.commitments.channelFeatures === ChannelFeatures(Features.StaticRemoteKey, Features.AnchorOutputsZeroFeeHtlcTx)) + assert(initialState.commitments.channelFeatures == ChannelFeatures(Features.StaticRemoteKey, Features.AnchorOutputsZeroFeeHtlcTx)) // bob publishes his last current commit tx, the one it had when entering NEGOTIATING state val bobCommitTx = bobCommitTxs.last.commitTx.tx assert(bobCommitTx.txOut.size == 4) // two main outputs + two anchors @@ -705,7 +705,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with def testRemoteCommitTxWithHtlcsConfirmed(f: FixtureParam, channelFeatures: ChannelFeatures): Unit = { import f._ - assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.channelFeatures === channelFeatures) + assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.channelFeatures == channelFeatures) // alice sends a first htlc to bob val (ra1, htlca1) = addHtlc(15000000 msat, alice, bob, alice2bob, bob2alice) @@ -719,13 +719,13 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // Bob publishes the latest commit tx. val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx channelFeatures.commitmentFormat match { - case _: AnchorOutputsCommitmentFormat => assert(bobCommitTx.txOut.length === 7) // two main outputs + two anchors + 3 HTLCs - case DefaultCommitmentFormat => assert(bobCommitTx.txOut.length === 5) // two main outputs + 3 HTLCs + case _: AnchorOutputsCommitmentFormat => assert(bobCommitTx.txOut.length == 7) // two main outputs + two anchors + 3 HTLCs + case DefaultCommitmentFormat => assert(bobCommitTx.txOut.length == 5) // two main outputs + 3 HTLCs } val closingState = remoteClose(bobCommitTx, alice, alice2blockchain) - assert(closingState.claimHtlcTxs.size === 3) + assert(closingState.claimHtlcTxs.size == 3) val claimHtlcTimeoutTxs = getClaimHtlcTimeoutTxs(closingState).map(_.tx) - assert(claimHtlcTimeoutTxs.length === 3) + assert(claimHtlcTimeoutTxs.length == 3) alice ! WatchTxConfirmedTriggered(BlockHeight(42), 0, bobCommitTx) alice ! WatchTxConfirmedTriggered(BlockHeight(45), 0, closingState.claimMainOutputTx.get.tx) @@ -738,7 +738,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice2relayer.expectNoMessage(250 millis) alice ! WatchTxConfirmedTriggered(BlockHeight(203), 1, claimHtlcTimeoutTxs(2)) val forwardedFail3 = alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc - assert(Set(forwardedFail1, forwardedFail2, forwardedFail3) === Set(htlca1, htlca2, htlca3)) + assert(Set(forwardedFail1, forwardedFail2, forwardedFail3) == Set(htlca1, htlca2, htlca3)) alice2relayer.expectNoMessage(250 millis) awaitCond(alice.stateName == CLOSED) } @@ -769,36 +769,36 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // Now Bob publishes the first commit tx (force-close). val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx - assert(bobCommitTx.txOut.length === 3) // two main outputs + 1 HTLC + assert(bobCommitTx.txOut.length == 3) // two main outputs + 1 HTLC val closingState = remoteClose(bobCommitTx, alice, alice2blockchain) assert(closingState.claimMainOutputTx.nonEmpty) - assert(closingState.claimHtlcTxs.size === 1) + assert(closingState.claimHtlcTxs.size == 1) assert(getClaimHtlcSuccessTxs(closingState).isEmpty) // we don't have the preimage to claim the htlc-success yet assert(getClaimHtlcTimeoutTxs(closingState).isEmpty) // Alice receives the preimage for the first HTLC from downstream; she can now claim the corresponding HTLC output. alice ! CMD_FULFILL_HTLC(htlc1.id, r1, commit = true) - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx === closingState.claimMainOutputTx.get.tx) + assert(alice2blockchain.expectMsgType[PublishFinalTx].tx == closingState.claimMainOutputTx.get.tx) val claimHtlcSuccessTx = getClaimHtlcSuccessTxs(alice.stateData.asInstanceOf[DATA_CLOSING].remoteCommitPublished.get).head.tx Transaction.correctlySpends(claimHtlcSuccessTx, bobCommitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) val publishHtlcSuccessTx = alice2blockchain.expectMsgType[PublishReplaceableTx] - assert(publishHtlcSuccessTx.txInfo.tx === claimHtlcSuccessTx) - assert(publishHtlcSuccessTx.txInfo.confirmBefore.toLong === htlc1.cltvExpiry.toLong) + assert(publishHtlcSuccessTx.txInfo.tx == claimHtlcSuccessTx) + assert(publishHtlcSuccessTx.txInfo.confirmBefore.toLong == htlc1.cltvExpiry.toLong) // Alice resets watches on all relevant transactions. - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === bobCommitTx.txid) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === closingState.claimMainOutputTx.get.tx.txid) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == bobCommitTx.txid) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == closingState.claimMainOutputTx.get.tx.txid) val watchHtlcSuccess = alice2blockchain.expectMsgType[WatchOutputSpent] - assert(watchHtlcSuccess.txId === bobCommitTx.txid) - assert(watchHtlcSuccess.outputIndex === claimHtlcSuccessTx.txIn.head.outPoint.index) + assert(watchHtlcSuccess.txId == bobCommitTx.txid) + assert(watchHtlcSuccess.outputIndex == claimHtlcSuccessTx.txIn.head.outPoint.index) alice2blockchain.expectNoMessage(100 millis) alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, bobCommitTx) // The second htlc was not included in the commit tx published on-chain, so we can consider it failed - assert(alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc === htlc2) + assert(alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc == htlc2) alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, closingState.claimMainOutputTx.get.tx) alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, claimHtlcSuccessTx) - assert(alice.stateData.asInstanceOf[DATA_CLOSING].remoteCommitPublished.get.irrevocablySpent.values.toSet === Set(bobCommitTx, closingState.claimMainOutputTx.get.tx, claimHtlcSuccessTx)) + assert(alice.stateData.asInstanceOf[DATA_CLOSING].remoteCommitPublished.get.irrevocablySpent.values.toSet == Set(bobCommitTx, closingState.claimMainOutputTx.get.tx, claimHtlcSuccessTx)) awaitCond(alice.stateName == CLOSED) alice2blockchain.expectNoMessage(100 millis) alice2relayer.expectNoMessage(100 millis) @@ -823,18 +823,18 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with awaitCond(alice.stateName == CLOSING) // we should re-publish unconfirmed transactions - closingState.claimMainOutputTx.foreach(claimMain => assert(alice2blockchain.expectMsgType[PublishFinalTx].tx === claimMain.tx)) + closingState.claimMainOutputTx.foreach(claimMain => assert(alice2blockchain.expectMsgType[PublishFinalTx].tx == claimMain.tx)) val publishHtlcTimeoutTx = alice2blockchain.expectMsgType[PublishReplaceableTx] - assert(publishHtlcTimeoutTx.txInfo.tx === htlcTimeoutTx.tx) - assert(publishHtlcTimeoutTx.txInfo.confirmBefore.toLong === htlca.cltvExpiry.toLong) - closingState.claimMainOutputTx.foreach(claimMain => assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === claimMain.tx.txid)) - assert(alice2blockchain.expectMsgType[WatchOutputSpent].outputIndex === htlcTimeoutTx.input.outPoint.index) + assert(publishHtlcTimeoutTx.txInfo.tx == htlcTimeoutTx.tx) + assert(publishHtlcTimeoutTx.txInfo.confirmBefore.toLong == htlca.cltvExpiry.toLong) + closingState.claimMainOutputTx.foreach(claimMain => assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == claimMain.tx.txid)) + assert(alice2blockchain.expectMsgType[WatchOutputSpent].outputIndex == htlcTimeoutTx.input.outPoint.index) } private def testNextRemoteCommitTxConfirmed(f: FixtureParam, channelFeatures: ChannelFeatures): (Transaction, RemoteCommitPublished, Set[UpdateAddHtlc]) = { import f._ - assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.channelFeatures === channelFeatures) + assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.channelFeatures == channelFeatures) // alice sends a first htlc to bob val (ra1, htlca1) = addHtlc(15000000 msat, alice, bob, alice2bob, bob2alice) @@ -854,11 +854,11 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // Bob publishes the next commit tx. val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx channelFeatures.commitmentFormat match { - case _: AnchorOutputsCommitmentFormat => assert(bobCommitTx.txOut.length === 7) // two main outputs + two anchors + 3 HTLCs - case DefaultCommitmentFormat => assert(bobCommitTx.txOut.length === 5) // two main outputs + 3 HTLCs + case _: AnchorOutputsCommitmentFormat => assert(bobCommitTx.txOut.length == 7) // two main outputs + two anchors + 3 HTLCs + case DefaultCommitmentFormat => assert(bobCommitTx.txOut.length == 5) // two main outputs + 3 HTLCs } val closingState = remoteClose(bobCommitTx, alice, alice2blockchain) - assert(getClaimHtlcTimeoutTxs(closingState).length === 3) + assert(getClaimHtlcTimeoutTxs(closingState).length == 3) (bobCommitTx, closingState, Set(htlca1, htlca2, htlca3)) } @@ -866,11 +866,11 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with import f._ val (bobCommitTx, closingState, htlcs) = testNextRemoteCommitTxConfirmed(f, ChannelFeatures()) val txPublished = txListener.expectMsgType[TransactionPublished] - assert(txPublished.tx === bobCommitTx) + assert(txPublished.tx == bobCommitTx) assert(txPublished.miningFee > 0.sat) // alice is funder, she pays the fee for the remote commit val claimHtlcTimeoutTxs = getClaimHtlcTimeoutTxs(closingState).map(_.tx) alice ! WatchTxConfirmedTriggered(BlockHeight(42), 0, bobCommitTx) - assert(txListener.expectMsgType[TransactionConfirmed].tx === bobCommitTx) + assert(txListener.expectMsgType[TransactionConfirmed].tx == bobCommitTx) alice ! WatchTxConfirmedTriggered(BlockHeight(45), 0, closingState.claimMainOutputTx.get.tx) alice2relayer.expectNoMessage(100 millis) alice ! WatchTxConfirmedTriggered(BlockHeight(201), 0, claimHtlcTimeoutTxs(0)) @@ -881,7 +881,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice2relayer.expectNoMessage(250 millis) alice ! WatchTxConfirmedTriggered(BlockHeight(203), 1, claimHtlcTimeoutTxs(2)) val forwardedFail3 = alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc - assert(Set(forwardedFail1, forwardedFail2, forwardedFail3) === htlcs) + assert(Set(forwardedFail1, forwardedFail2, forwardedFail3) == htlcs) alice2relayer.expectNoMessage(250 millis) awaitCond(alice.stateName == CLOSED) } @@ -901,7 +901,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice2relayer.expectNoMessage(250 millis) alice ! WatchTxConfirmedTriggered(BlockHeight(203), 1, claimHtlcTimeoutTxs(2)) val forwardedFail3 = alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc - assert(Set(forwardedFail1, forwardedFail2, forwardedFail3) === htlcs) + assert(Set(forwardedFail1, forwardedFail2, forwardedFail3) == htlcs) alice2relayer.expectNoMessage(250 millis) awaitCond(alice.stateName == CLOSED) } @@ -921,7 +921,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice2relayer.expectNoMessage(250 millis) alice ! WatchTxConfirmedTriggered(BlockHeight(203), 1, claimHtlcTimeoutTxs(2)) val forwardedFail3 = alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc - assert(Set(forwardedFail1, forwardedFail2, forwardedFail3) === htlcs) + assert(Set(forwardedFail1, forwardedFail2, forwardedFail3) == htlcs) alice2relayer.expectNoMessage(250 millis) awaitCond(alice.stateName == CLOSED) } @@ -943,38 +943,38 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // Now Bob publishes the next commit tx (force-close). val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx - assert(bobCommitTx.txOut.length === 4) // two main outputs + 2 HTLCs + assert(bobCommitTx.txOut.length == 4) // two main outputs + 2 HTLCs val closingState = remoteClose(bobCommitTx, alice, alice2blockchain) assert(closingState.claimMainOutputTx.nonEmpty) - assert(closingState.claimHtlcTxs.size === 2) + assert(closingState.claimHtlcTxs.size == 2) assert(getClaimHtlcSuccessTxs(closingState).isEmpty) // we don't have the preimage to claim the htlc-success yet - assert(getClaimHtlcTimeoutTxs(closingState).length === 1) + assert(getClaimHtlcTimeoutTxs(closingState).length == 1) val claimHtlcTimeoutTx = getClaimHtlcTimeoutTxs(closingState).head.tx // Alice receives the preimage for the first HTLC from downstream; she can now claim the corresponding HTLC output. alice ! CMD_FULFILL_HTLC(htlc1.id, r1, commit = true) - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx === closingState.claimMainOutputTx.get.tx) + assert(alice2blockchain.expectMsgType[PublishFinalTx].tx == closingState.claimMainOutputTx.get.tx) val claimHtlcSuccessTx = getClaimHtlcSuccessTxs(alice.stateData.asInstanceOf[DATA_CLOSING].nextRemoteCommitPublished.get).head.tx Transaction.correctlySpends(claimHtlcSuccessTx, bobCommitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) val publishHtlcSuccessTx = alice2blockchain.expectMsgType[PublishReplaceableTx] - assert(publishHtlcSuccessTx.txInfo.tx === claimHtlcSuccessTx) - assert(publishHtlcSuccessTx.txInfo.confirmBefore.toLong === htlc1.cltvExpiry.toLong) + assert(publishHtlcSuccessTx.txInfo.tx == claimHtlcSuccessTx) + assert(publishHtlcSuccessTx.txInfo.confirmBefore.toLong == htlc1.cltvExpiry.toLong) val publishHtlcTimeoutTx = alice2blockchain.expectMsgType[PublishReplaceableTx] - assert(publishHtlcTimeoutTx.txInfo.tx === claimHtlcTimeoutTx) - assert(publishHtlcTimeoutTx.txInfo.confirmBefore.toLong === htlc2.cltvExpiry.toLong) + assert(publishHtlcTimeoutTx.txInfo.tx == claimHtlcTimeoutTx) + assert(publishHtlcTimeoutTx.txInfo.confirmBefore.toLong == htlc2.cltvExpiry.toLong) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === bobCommitTx.txid) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === closingState.claimMainOutputTx.get.tx.txid) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == bobCommitTx.txid) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == closingState.claimMainOutputTx.get.tx.txid) val watchHtlcs = alice2blockchain.expectMsgType[WatchOutputSpent] :: alice2blockchain.expectMsgType[WatchOutputSpent] :: Nil - watchHtlcs.foreach(ws => assert(ws.txId === bobCommitTx.txid)) - assert(watchHtlcs.map(_.outputIndex).toSet === Set(claimHtlcSuccessTx, claimHtlcTimeoutTx).map(_.txIn.head.outPoint.index)) + watchHtlcs.foreach(ws => assert(ws.txId == bobCommitTx.txid)) + assert(watchHtlcs.map(_.outputIndex).toSet == Set(claimHtlcSuccessTx, claimHtlcTimeoutTx).map(_.txIn.head.outPoint.index)) alice2blockchain.expectNoMessage(100 millis) alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, bobCommitTx) alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, closingState.claimMainOutputTx.get.tx) alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, claimHtlcSuccessTx) alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, claimHtlcTimeoutTx) - assert(alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc === htlc2) + assert(alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc == htlc2) awaitCond(alice.stateName == CLOSED) alice2blockchain.expectNoMessage(100 millis) alice2relayer.expectNoMessage(100 millis) @@ -996,17 +996,17 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // the commit tx hasn't been confirmed yet, so we watch the funding output first alice2blockchain.expectMsgType[WatchFundingSpent] // then we should re-publish unconfirmed transactions - closingState.claimMainOutputTx.foreach(claimMain => assert(alice2blockchain.expectMsgType[PublishFinalTx].tx === claimMain.tx)) - claimHtlcTimeoutTxs.foreach(claimHtlcTimeout => assert(alice2blockchain.expectMsgType[PublishReplaceableTx].txInfo.tx === claimHtlcTimeout.tx)) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === bobCommitTx.txid) - closingState.claimMainOutputTx.foreach(claimMain => assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === claimMain.tx.txid)) - claimHtlcTimeoutTxs.foreach(claimHtlcTimeout => assert(alice2blockchain.expectMsgType[WatchOutputSpent].outputIndex === claimHtlcTimeout.input.outPoint.index)) + closingState.claimMainOutputTx.foreach(claimMain => assert(alice2blockchain.expectMsgType[PublishFinalTx].tx == claimMain.tx)) + claimHtlcTimeoutTxs.foreach(claimHtlcTimeout => assert(alice2blockchain.expectMsgType[PublishReplaceableTx].txInfo.tx == claimHtlcTimeout.tx)) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == bobCommitTx.txid) + closingState.claimMainOutputTx.foreach(claimMain => assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == claimMain.tx.txid)) + claimHtlcTimeoutTxs.foreach(claimHtlcTimeout => assert(alice2blockchain.expectMsgType[WatchOutputSpent].outputIndex == claimHtlcTimeout.input.outPoint.index)) } private def testFutureRemoteCommitTxConfirmed(f: FixtureParam, channelFeatures: ChannelFeatures): Transaction = { import f._ val oldStateData = alice.stateData - assert(oldStateData.asInstanceOf[DATA_NORMAL].commitments.channelFeatures === channelFeatures) + assert(oldStateData.asInstanceOf[DATA_NORMAL].commitments.channelFeatures == channelFeatures) // This HTLC will be fulfilled. val (ra1, htlca1) = addHtlc(25000000 msat, alice, bob, alice2bob, bob2alice) // These 2 HTLCs should timeout on-chain, but since alice lost data, she won't be able to claim them. @@ -1035,14 +1035,14 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with bob2alice.forward(alice) // ... and ask bob to publish its current commitment val error = alice2bob.expectMsgType[Error] - assert(new String(error.data.toArray) === PleasePublishYourCommitment(channelId(alice)).getMessage) + assert(new String(error.data.toArray) == PleasePublishYourCommitment(channelId(alice)).getMessage) // alice now waits for bob to publish its commitment awaitCond(alice.stateName == WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT) // bob is nice and publishes its commitment val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx channelFeatures.commitmentFormat match { - case _: AnchorOutputsCommitmentFormat => assert(bobCommitTx.txOut.length === 6) // two main outputs + two anchors + 2 HTLCs - case DefaultCommitmentFormat => assert(bobCommitTx.txOut.length === 4) // two main outputs + 2 HTLCs + case _: AnchorOutputsCommitmentFormat => assert(bobCommitTx.txOut.length == 6) // two main outputs + two anchors + 2 HTLCs + case DefaultCommitmentFormat => assert(bobCommitTx.txOut.length == 4) // two main outputs + 2 HTLCs } alice ! WatchFundingSpentTriggered(bobCommitTx) bobCommitTx @@ -1052,19 +1052,19 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with import f._ val bobCommitTx = testFutureRemoteCommitTxConfirmed(f, ChannelFeatures()) val txPublished = txListener.expectMsgType[TransactionPublished] - assert(txPublished.tx === bobCommitTx) + assert(txPublished.tx == bobCommitTx) assert(txPublished.miningFee > 0.sat) // alice is funder, she pays the fee for the remote commit // alice is able to claim its main output val claimMainTx = alice2blockchain.expectMsgType[PublishFinalTx].tx Transaction.correctlySpends(claimMainTx, bobCommitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === bobCommitTx.txid) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == bobCommitTx.txid) awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].futureRemoteCommitPublished.isDefined) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === claimMainTx.txid) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == claimMainTx.txid) alice2blockchain.expectNoMessage(250 millis) // alice ignores the htlc-timeout // actual test starts here alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, bobCommitTx) - assert(txListener.expectMsgType[TransactionConfirmed].tx === bobCommitTx) + assert(txListener.expectMsgType[TransactionConfirmed].tx == bobCommitTx) alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, claimMainTx) awaitCond(alice.stateName == CLOSED) } @@ -1085,9 +1085,9 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // alice is able to claim its main output val claimMainTx = alice2blockchain.expectMsgType[PublishFinalTx].tx Transaction.correctlySpends(claimMainTx, bobCommitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === bobCommitTx.txid) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == bobCommitTx.txid) awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].futureRemoteCommitPublished.isDefined) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === claimMainTx.txid) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == claimMainTx.txid) alice2blockchain.expectNoMessage(250 millis) // alice ignores the htlc-timeout // actual test starts here @@ -1110,8 +1110,8 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // then we should claim our main output val claimMainTx = alice2blockchain.expectMsgType[PublishFinalTx].tx Transaction.correctlySpends(claimMainTx, bobCommitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === bobCommitTx.txid) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === claimMainTx.txid) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == bobCommitTx.txid) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == claimMainTx.txid) } case class RevokedCloseFixture(bobRevokedTxs: Seq[LocalCommit], htlcsAlice: Seq[(UpdateAddHtlc, ByteVector32)], htlcsBob: Seq[(UpdateAddHtlc, ByteVector32)]) @@ -1122,8 +1122,8 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // Bob's first commit tx doesn't contain any htlc val localCommit1 = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit channelFeatures.commitmentFormat match { - case _: AnchorOutputsCommitmentFormat => assert(localCommit1.commitTxAndRemoteSig.commitTx.tx.txOut.size === 4) // 2 main outputs + 2 anchors - case DefaultCommitmentFormat => assert(localCommit1.commitTxAndRemoteSig.commitTx.tx.txOut.size === 2) // 2 main outputs + case _: AnchorOutputsCommitmentFormat => assert(localCommit1.commitTxAndRemoteSig.commitTx.tx.txOut.size == 4) // 2 main outputs + 2 anchors + case DefaultCommitmentFormat => assert(localCommit1.commitTxAndRemoteSig.commitTx.tx.txOut.size == 2) // 2 main outputs } // Bob's second commit tx contains 1 incoming htlc and 1 outgoing htlc @@ -1138,8 +1138,8 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx.txOut.size == localCommit2.commitTxAndRemoteSig.commitTx.tx.txOut.size) channelFeatures.commitmentFormat match { - case _: AnchorOutputsCommitmentFormat => assert(localCommit2.commitTxAndRemoteSig.commitTx.tx.txOut.size === 6) - case DefaultCommitmentFormat => assert(localCommit2.commitTxAndRemoteSig.commitTx.tx.txOut.size === 4) + case _: AnchorOutputsCommitmentFormat => assert(localCommit2.commitTxAndRemoteSig.commitTx.tx.txOut.size == 6) + case DefaultCommitmentFormat => assert(localCommit2.commitTxAndRemoteSig.commitTx.tx.txOut.size == 4) } // Bob's third commit tx contains 2 incoming htlcs and 2 outgoing htlcs @@ -1154,8 +1154,8 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx.txOut.size == localCommit3.commitTxAndRemoteSig.commitTx.tx.txOut.size) channelFeatures.commitmentFormat match { - case _: AnchorOutputsCommitmentFormat => assert(localCommit3.commitTxAndRemoteSig.commitTx.tx.txOut.size === 8) - case DefaultCommitmentFormat => assert(localCommit3.commitTxAndRemoteSig.commitTx.tx.txOut.size === 6) + case _: AnchorOutputsCommitmentFormat => assert(localCommit3.commitTxAndRemoteSig.commitTx.tx.txOut.size == 8) + case DefaultCommitmentFormat => assert(localCommit3.commitTxAndRemoteSig.commitTx.tx.txOut.size == 6) } // Bob's fourth commit tx doesn't contain any htlc @@ -1168,8 +1168,8 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx.txOut.size == localCommit4.commitTxAndRemoteSig.commitTx.tx.txOut.size) channelFeatures.commitmentFormat match { - case _: AnchorOutputsCommitmentFormat => assert(localCommit4.commitTxAndRemoteSig.commitTx.tx.txOut.size === 4) - case DefaultCommitmentFormat => assert(localCommit4.commitTxAndRemoteSig.commitTx.tx.txOut.size === 2) + case _: AnchorOutputsCommitmentFormat => assert(localCommit4.commitTxAndRemoteSig.commitTx.tx.txOut.size == 4) + case DefaultCommitmentFormat => assert(localCommit4.commitTxAndRemoteSig.commitTx.tx.txOut.size == 2) } RevokedCloseFixture(Seq(localCommit1, localCommit2, localCommit3, localCommit4), Seq(htlcAlice1, htlcAlice2), Seq(htlcBob1, htlcBob2)) @@ -1179,7 +1179,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with import f._ val revokedCloseFixture = prepareRevokedClose(f, channelFeatures) - assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.channelFeatures === channelFeatures) + assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.channelFeatures == channelFeatures) // bob publishes one of his revoked txs val bobRevokedTx = revokedCloseFixture.bobRevokedTxs(1).commitTxAndRemoteSig.commitTx.tx @@ -1188,46 +1188,46 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with awaitCond(alice.stateData.isInstanceOf[DATA_CLOSING]) awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.size == 1) val rvk = alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.head - assert(rvk.commitTx === bobRevokedTx) + assert(rvk.commitTx == bobRevokedTx) if (!channelFeatures.paysDirectlyToWallet) { assert(rvk.claimMainOutputTx.nonEmpty) } assert(rvk.mainPenaltyTx.nonEmpty) - assert(rvk.htlcPenaltyTxs.size === 2) + assert(rvk.htlcPenaltyTxs.size == 2) assert(rvk.claimHtlcDelayedPenaltyTxs.isEmpty) val penaltyTxs = rvk.claimMainOutputTx.toList ++ rvk.mainPenaltyTx.toList ++ rvk.htlcPenaltyTxs // alice publishes the penalty txs if (!channelFeatures.paysDirectlyToWallet) { - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx === rvk.claimMainOutputTx.get.tx) + assert(alice2blockchain.expectMsgType[PublishFinalTx].tx == rvk.claimMainOutputTx.get.tx) } - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx === rvk.mainPenaltyTx.get.tx) - assert(Set(alice2blockchain.expectMsgType[PublishFinalTx].tx, alice2blockchain.expectMsgType[PublishFinalTx].tx) === rvk.htlcPenaltyTxs.map(_.tx).toSet) + assert(alice2blockchain.expectMsgType[PublishFinalTx].tx == rvk.mainPenaltyTx.get.tx) + assert(Set(alice2blockchain.expectMsgType[PublishFinalTx].tx, alice2blockchain.expectMsgType[PublishFinalTx].tx) == rvk.htlcPenaltyTxs.map(_.tx).toSet) for (penaltyTx <- penaltyTxs) { Transaction.correctlySpends(penaltyTx.tx, bobRevokedTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) } // alice spends all outpoints of the revoked tx, except her main output when it goes directly to our wallet val spentOutpoints = penaltyTxs.flatMap(_.tx.txIn.map(_.outPoint)).toSet - assert(spentOutpoints.forall(_.txid === bobRevokedTx.txid)) + assert(spentOutpoints.forall(_.txid == bobRevokedTx.txid)) if (channelFeatures.commitmentFormat.isInstanceOf[AnchorOutputsCommitmentFormat]) { - assert(spentOutpoints.size === bobRevokedTx.txOut.size - 2) // we don't claim the anchors + assert(spentOutpoints.size == bobRevokedTx.txOut.size - 2) // we don't claim the anchors } else if (channelFeatures.paysDirectlyToWallet) { - assert(spentOutpoints.size === bobRevokedTx.txOut.size - 1) // we don't claim our main output, it directly goes to our wallet + assert(spentOutpoints.size == bobRevokedTx.txOut.size - 1) // we don't claim our main output, it directly goes to our wallet } else { - assert(spentOutpoints.size === bobRevokedTx.txOut.size) + assert(spentOutpoints.size == bobRevokedTx.txOut.size) } // alice watches confirmation for the outputs only her can claim - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === bobRevokedTx.txid) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == bobRevokedTx.txid) if (!channelFeatures.paysDirectlyToWallet) { - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === rvk.claimMainOutputTx.get.tx.txid) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == rvk.claimMainOutputTx.get.tx.txid) } // alice watches outputs that can be spent by both parties val watchedOutpoints = Seq(alice2blockchain.expectMsgType[WatchOutputSpent], alice2blockchain.expectMsgType[WatchOutputSpent], alice2blockchain.expectMsgType[WatchOutputSpent]).map(_.outputIndex).toSet - assert(watchedOutpoints === (rvk.mainPenaltyTx.get :: rvk.htlcPenaltyTxs).map(_.input.outPoint.index).toSet) + assert(watchedOutpoints == (rvk.mainPenaltyTx.get :: rvk.htlcPenaltyTxs).map(_.input.outPoint.index).toSet) alice2blockchain.expectNoMessage(1 second) (bobRevokedTx, rvk) @@ -1238,20 +1238,20 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val (bobRevokedTx, rvk) = setupFundingSpentRevokedTx(f, channelFeatures) val txPublished = txListener.expectMsgType[TransactionPublished] - assert(txPublished.tx === bobRevokedTx) + assert(txPublished.tx == bobRevokedTx) assert(txPublished.miningFee > 0.sat) // alice is funder, she pays the fee for the revoked commit // once all txs are confirmed, alice can move to the closed state alice ! WatchTxConfirmedTriggered(BlockHeight(100), 3, bobRevokedTx) - assert(txListener.expectMsgType[TransactionConfirmed].tx === bobRevokedTx) + assert(txListener.expectMsgType[TransactionConfirmed].tx == bobRevokedTx) alice ! WatchTxConfirmedTriggered(BlockHeight(110), 1, rvk.mainPenaltyTx.get.tx) if (!channelFeatures.paysDirectlyToWallet) { alice ! WatchTxConfirmedTriggered(BlockHeight(110), 2, rvk.claimMainOutputTx.get.tx) } alice ! WatchTxConfirmedTriggered(BlockHeight(115), 0, rvk.htlcPenaltyTxs(0).tx) - assert(alice.stateName === CLOSING) + assert(alice.stateName == CLOSING) alice ! WatchTxConfirmedTriggered(BlockHeight(115), 2, rvk.htlcPenaltyTxs(1).tx) - awaitCond(alice.stateName === CLOSED) + awaitCond(alice.stateName == CLOSED) } test("recv WatchFundingSpentTriggered (one revoked tx)") { f => @@ -1273,13 +1273,13 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with test("recv WatchFundingSpentTriggered (multiple revoked tx)") { f => import f._ val revokedCloseFixture = prepareRevokedClose(f, ChannelFeatures()) - assert(revokedCloseFixture.bobRevokedTxs.map(_.commitTxAndRemoteSig.commitTx.tx.txid).toSet.size === revokedCloseFixture.bobRevokedTxs.size) // all commit txs are distinct + assert(revokedCloseFixture.bobRevokedTxs.map(_.commitTxAndRemoteSig.commitTx.tx.txid).toSet.size == revokedCloseFixture.bobRevokedTxs.size) // all commit txs are distinct def broadcastBobRevokedTx(revokedTx: Transaction, htlcCount: Int, revokedCount: Int): RevokedCommitPublished = { alice ! WatchFundingSpentTriggered(revokedTx) awaitCond(alice.stateData.isInstanceOf[DATA_CLOSING]) awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.size == revokedCount) - assert(alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.last.commitTx === revokedTx) + assert(alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.last.commitTx == revokedTx) // alice publishes penalty txs val claimMain = alice2blockchain.expectMsgType[PublishFinalTx].tx @@ -1288,13 +1288,13 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with (claimMain +: mainPenalty +: htlcPenaltyTxs).foreach(tx => Transaction.correctlySpends(tx, revokedTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)) // alice watches confirmation for the outputs only her can claim - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === revokedTx.txid) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === claimMain.txid) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == revokedTx.txid) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == claimMain.txid) // alice watches outputs that can be spent by both parties - assert(alice2blockchain.expectMsgType[WatchOutputSpent].outputIndex === mainPenalty.txIn.head.outPoint.index) + assert(alice2blockchain.expectMsgType[WatchOutputSpent].outputIndex == mainPenalty.txIn.head.outPoint.index) val htlcOutpoints = (1 to htlcCount).map(_ => alice2blockchain.expectMsgType[WatchOutputSpent].outputIndex).toSet - assert(htlcOutpoints === htlcPenaltyTxs.flatMap(_.txIn.map(_.outPoint.index)).toSet) + assert(htlcOutpoints == htlcPenaltyTxs.flatMap(_.txIn.map(_.outPoint.index)).toSet) alice2blockchain.expectNoMessage(1 second) alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.last @@ -1313,9 +1313,9 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice ! WatchTxConfirmedTriggered(BlockHeight(100), 2, rvk2.claimMainOutputTx.get.tx) alice ! WatchTxConfirmedTriggered(BlockHeight(100), 3, rvk2.commitTx) alice ! WatchTxConfirmedTriggered(BlockHeight(115), 0, rvk2.htlcPenaltyTxs(0).tx) - assert(alice.stateName === CLOSING) + assert(alice.stateName == CLOSING) alice ! WatchTxConfirmedTriggered(BlockHeight(115), 2, rvk2.htlcPenaltyTxs(1).tx) - awaitCond(alice.stateName === CLOSED) + awaitCond(alice.stateName == CLOSED) } def testInputRestoredRevokedTx(f: FixtureParam, channelFeatures: ChannelFeatures): Unit = { @@ -1333,13 +1333,13 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // the commit tx hasn't been confirmed yet, so we watch the funding output first alice2blockchain.expectMsgType[WatchFundingSpent] // then we should re-publish unconfirmed transactions - rvk.claimMainOutputTx.foreach(claimMain => assert(alice2blockchain.expectMsgType[PublishFinalTx].tx === claimMain.tx)) - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx === rvk.mainPenaltyTx.get.tx) - rvk.htlcPenaltyTxs.foreach(htlcPenalty => assert(alice2blockchain.expectMsgType[PublishFinalTx].tx === htlcPenalty.tx)) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === bobRevokedTx.txid) - rvk.claimMainOutputTx.foreach(claimMain => assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === claimMain.tx.txid)) - assert(alice2blockchain.expectMsgType[WatchOutputSpent].outputIndex === rvk.mainPenaltyTx.get.input.outPoint.index) - rvk.htlcPenaltyTxs.foreach(htlcPenalty => assert(alice2blockchain.expectMsgType[WatchOutputSpent].outputIndex === htlcPenalty.input.outPoint.index)) + rvk.claimMainOutputTx.foreach(claimMain => assert(alice2blockchain.expectMsgType[PublishFinalTx].tx == claimMain.tx)) + assert(alice2blockchain.expectMsgType[PublishFinalTx].tx == rvk.mainPenaltyTx.get.tx) + rvk.htlcPenaltyTxs.foreach(htlcPenalty => assert(alice2blockchain.expectMsgType[PublishFinalTx].tx == htlcPenalty.tx)) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == bobRevokedTx.txid) + rvk.claimMainOutputTx.foreach(claimMain => assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == claimMain.tx.txid)) + assert(alice2blockchain.expectMsgType[WatchOutputSpent].outputIndex == rvk.mainPenaltyTx.get.input.outPoint.index) + rvk.htlcPenaltyTxs.foreach(htlcPenalty => assert(alice2blockchain.expectMsgType[WatchOutputSpent].outputIndex == htlcPenalty.input.outPoint.index)) } test("recv INPUT_RESTORED (one revoked tx)") { f => @@ -1357,7 +1357,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with def testOutputSpentRevokedTx(f: FixtureParam, channelFeatures: ChannelFeatures): Unit = { import f._ val revokedCloseFixture = prepareRevokedClose(f, channelFeatures) - assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.channelFeatures === channelFeatures) + assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.channelFeatures == channelFeatures) val commitmentFormat = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.commitmentFormat // bob publishes one of his revoked txs @@ -1367,22 +1367,22 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with awaitCond(alice.stateData.isInstanceOf[DATA_CLOSING]) awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.size == 1) val rvk = alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.head - assert(rvk.commitTx === bobRevokedCommit.commitTxAndRemoteSig.commitTx.tx) + assert(rvk.commitTx == bobRevokedCommit.commitTxAndRemoteSig.commitTx.tx) if (channelFeatures.paysDirectlyToWallet) { assert(rvk.claimMainOutputTx.isEmpty) } else { assert(rvk.claimMainOutputTx.nonEmpty) } assert(rvk.mainPenaltyTx.nonEmpty) - assert(rvk.htlcPenaltyTxs.size === 4) + assert(rvk.htlcPenaltyTxs.size == 4) assert(rvk.claimHtlcDelayedPenaltyTxs.isEmpty) // alice publishes the penalty txs and watches outputs val claimTxsCount = if (channelFeatures.paysDirectlyToWallet) 5 else 6 // 2 main outputs and 4 htlcs (1 to claimTxsCount).foreach(_ => alice2blockchain.expectMsgType[PublishTx]) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === rvk.commitTx.txid) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == rvk.commitTx.txid) if (!channelFeatures.paysDirectlyToWallet) { - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === rvk.claimMainOutputTx.get.tx.txid) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == rvk.claimMainOutputTx.get.tx.txid) } (1 to 5).foreach(_ => alice2blockchain.expectMsgType[WatchOutputSpent]) // main output penalty and 4 htlc penalties alice2blockchain.expectNoMessage(1 second) @@ -1392,7 +1392,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val (failedHtlc, _) = revokedCloseFixture.htlcsBob.last val bobHtlcSuccessTx1 = bobRevokedCommit.htlcTxsAndRemoteSigs.collectFirst { case HtlcTxAndRemoteSig(txInfo: HtlcSuccessTx, _) if txInfo.htlcId == fulfilledHtlc.id => - assert(fulfilledHtlc.paymentHash === txInfo.paymentHash) + assert(fulfilledHtlc.paymentHash == txInfo.paymentHash) Transactions.addSigs(txInfo, ByteVector64.Zeroes, ByteVector64.Zeroes, preimage, commitmentFormat) }.get val bobHtlcTimeoutTx = bobRevokedCommit.htlcTxsAndRemoteSigs.collectFirst { @@ -1400,29 +1400,29 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Transactions.addSigs(txInfo, ByteVector64.Zeroes, ByteVector64.Zeroes, commitmentFormat) }.get val bobOutpoints = Seq(bobHtlcSuccessTx1, bobHtlcTimeoutTx).map(_.input.outPoint).toSet - assert(bobOutpoints.size === 2) + assert(bobOutpoints.size == 2) // alice reacts by publishing penalty txs that spend bob's htlc transactions alice ! WatchOutputSpentTriggered(bobHtlcSuccessTx1.tx) awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.head.claimHtlcDelayedPenaltyTxs.size == 1) val claimHtlcSuccessPenalty1 = alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.head.claimHtlcDelayedPenaltyTxs.last Transaction.correctlySpends(claimHtlcSuccessPenalty1.tx, bobHtlcSuccessTx1.tx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === bobHtlcSuccessTx1.tx.txid) - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx === claimHtlcSuccessPenalty1.tx) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == bobHtlcSuccessTx1.tx.txid) + assert(alice2blockchain.expectMsgType[PublishFinalTx].tx == claimHtlcSuccessPenalty1.tx) val watchSpent1 = alice2blockchain.expectMsgType[WatchOutputSpent] - assert(watchSpent1.txId === bobHtlcSuccessTx1.tx.txid) - assert(watchSpent1.outputIndex === claimHtlcSuccessPenalty1.input.outPoint.index) + assert(watchSpent1.txId == bobHtlcSuccessTx1.tx.txid) + assert(watchSpent1.outputIndex == claimHtlcSuccessPenalty1.input.outPoint.index) alice2blockchain.expectNoMessage(1 second) alice ! WatchOutputSpentTriggered(bobHtlcTimeoutTx.tx) awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.head.claimHtlcDelayedPenaltyTxs.size == 2) val claimHtlcTimeoutPenalty = alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.head.claimHtlcDelayedPenaltyTxs.last Transaction.correctlySpends(claimHtlcTimeoutPenalty.tx, bobHtlcTimeoutTx.tx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === bobHtlcTimeoutTx.tx.txid) - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx === claimHtlcTimeoutPenalty.tx) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == bobHtlcTimeoutTx.tx.txid) + assert(alice2blockchain.expectMsgType[PublishFinalTx].tx == claimHtlcTimeoutPenalty.tx) val watchSpent2 = alice2blockchain.expectMsgType[WatchOutputSpent] - assert(watchSpent2.txId === bobHtlcTimeoutTx.tx.txid) - assert(watchSpent2.outputIndex === claimHtlcTimeoutPenalty.input.outPoint.index) + assert(watchSpent2.txId == bobHtlcTimeoutTx.tx.txid) + assert(watchSpent2.outputIndex == claimHtlcTimeoutPenalty.input.outPoint.index) alice2blockchain.expectNoMessage(1 second) // bob RBFs his htlc-success with a different transaction @@ -1433,16 +1433,16 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val claimHtlcSuccessPenalty2 = alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.head.claimHtlcDelayedPenaltyTxs.last assert(claimHtlcSuccessPenalty1.tx.txid != claimHtlcSuccessPenalty2.tx.txid) Transaction.correctlySpends(claimHtlcSuccessPenalty2.tx, bobHtlcSuccessTx2 :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === bobHtlcSuccessTx2.txid) - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx === claimHtlcSuccessPenalty2.tx) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == bobHtlcSuccessTx2.txid) + assert(alice2blockchain.expectMsgType[PublishFinalTx].tx == claimHtlcSuccessPenalty2.tx) val watchSpent3 = alice2blockchain.expectMsgType[WatchOutputSpent] - assert(watchSpent3.txId === bobHtlcSuccessTx2.txid) - assert(watchSpent3.outputIndex === claimHtlcSuccessPenalty2.input.outPoint.index) + assert(watchSpent3.txId == bobHtlcSuccessTx2.txid) + assert(watchSpent3.outputIndex == claimHtlcSuccessPenalty2.input.outPoint.index) alice2blockchain.expectNoMessage(1 second) // transactions confirm: alice can move to the closed state val remainingHtlcPenaltyTxs = rvk.htlcPenaltyTxs.filterNot(htlcPenalty => bobOutpoints.contains(htlcPenalty.input.outPoint)) - assert(remainingHtlcPenaltyTxs.size === 2) + assert(remainingHtlcPenaltyTxs.size == 2) alice ! WatchTxConfirmedTriggered(BlockHeight(100), 3, rvk.commitTx) alice ! WatchTxConfirmedTriggered(BlockHeight(110), 0, rvk.mainPenaltyTx.get.tx) if (!channelFeatures.paysDirectlyToWallet) { @@ -1452,11 +1452,11 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice ! WatchTxConfirmedTriggered(BlockHeight(115), 2, remainingHtlcPenaltyTxs.last.tx) alice ! WatchTxConfirmedTriggered(BlockHeight(115), 0, bobHtlcTimeoutTx.tx) alice ! WatchTxConfirmedTriggered(BlockHeight(115), 1, bobHtlcSuccessTx2) - assert(alice.stateName === CLOSING) + assert(alice.stateName == CLOSING) alice ! WatchTxConfirmedTriggered(BlockHeight(120), 0, claimHtlcTimeoutPenalty.tx) alice ! WatchTxConfirmedTriggered(BlockHeight(121), 0, claimHtlcSuccessPenalty2.tx) - awaitCond(alice.stateName === CLOSED) + awaitCond(alice.stateName == CLOSED) } test("recv WatchOutputSpentTriggered (one revoked tx, counterparty published htlc-success tx)") { f => @@ -1484,17 +1484,17 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val commitmentFormat = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.commitmentFormat alice ! WatchFundingSpentTriggered(bobRevokedCommit.commitTxAndRemoteSig.commitTx.tx) awaitCond(alice.stateData.isInstanceOf[DATA_CLOSING]) - assert(alice.stateData.asInstanceOf[DATA_CLOSING].commitments.commitmentFormat === ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) + assert(alice.stateData.asInstanceOf[DATA_CLOSING].commitments.commitmentFormat == ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.size == 1) val rvk = alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.head - assert(rvk.commitTx === bobRevokedCommit.commitTxAndRemoteSig.commitTx.tx) - assert(rvk.htlcPenaltyTxs.size === 4) + assert(rvk.commitTx == bobRevokedCommit.commitTxAndRemoteSig.commitTx.tx) + assert(rvk.htlcPenaltyTxs.size == 4) assert(rvk.claimHtlcDelayedPenaltyTxs.isEmpty) // alice publishes the penalty txs and watches outputs (1 to 6).foreach(_ => alice2blockchain.expectMsgType[PublishTx]) // 2 main outputs and 4 htlcs - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === rvk.commitTx.txid) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === rvk.claimMainOutputTx.get.tx.txid) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == rvk.commitTx.txid) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == rvk.claimMainOutputTx.get.tx.txid) (1 to 5).foreach(_ => alice2blockchain.expectMsgType[WatchOutputSpent]) // main output penalty and 4 htlc penalties alice2blockchain.expectNoMessage(1 second) @@ -1503,12 +1503,12 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val bobHtlcTxs = bobRevokedCommit.htlcTxsAndRemoteSigs.collect { case HtlcTxAndRemoteSig(txInfo: HtlcSuccessTx, _) => val preimage = revokedCloseFixture.htlcsAlice.collectFirst { case (add, preimage) if add.id == txInfo.htlcId => preimage }.get - assert(Crypto.sha256(preimage) === txInfo.paymentHash) + assert(Crypto.sha256(preimage) == txInfo.paymentHash) Transactions.addSigs(txInfo, ByteVector64.Zeroes, ByteVector64.Zeroes, preimage, commitmentFormat) case HtlcTxAndRemoteSig(txInfo: HtlcTimeoutTx, _) => Transactions.addSigs(txInfo, ByteVector64.Zeroes, ByteVector64.Zeroes, commitmentFormat) } - assert(bobHtlcTxs.map(_.input.outPoint).size === 4) + assert(bobHtlcTxs.map(_.input.outPoint).size == 4) val bobHtlcTx = Transaction( 2, Seq( @@ -1533,34 +1533,34 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.head.claimHtlcDelayedPenaltyTxs.size == 4) val claimHtlcDelayedPenaltyTxs = alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.head.claimHtlcDelayedPenaltyTxs val spentOutpoints = Set(OutPoint(bobHtlcTx, 1), OutPoint(bobHtlcTx, 2), OutPoint(bobHtlcTx, 3), OutPoint(bobHtlcTx, 4)) - assert(claimHtlcDelayedPenaltyTxs.map(_.input.outPoint).toSet === spentOutpoints) + assert(claimHtlcDelayedPenaltyTxs.map(_.input.outPoint).toSet == spentOutpoints) claimHtlcDelayedPenaltyTxs.foreach(claimHtlcPenalty => Transaction.correctlySpends(claimHtlcPenalty.tx, bobHtlcTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === bobHtlcTx.txid) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == bobHtlcTx.txid) val publishedPenaltyTxs = Set( alice2blockchain.expectMsgType[PublishFinalTx], alice2blockchain.expectMsgType[PublishFinalTx], alice2blockchain.expectMsgType[PublishFinalTx], alice2blockchain.expectMsgType[PublishFinalTx] ) - assert(publishedPenaltyTxs.map(_.tx) === claimHtlcDelayedPenaltyTxs.map(_.tx).toSet) + assert(publishedPenaltyTxs.map(_.tx) == claimHtlcDelayedPenaltyTxs.map(_.tx).toSet) val watchedOutpoints = Seq( alice2blockchain.expectMsgType[WatchOutputSpent], alice2blockchain.expectMsgType[WatchOutputSpent], alice2blockchain.expectMsgType[WatchOutputSpent], alice2blockchain.expectMsgType[WatchOutputSpent] ).map(w => OutPoint(w.txId.reverse, w.outputIndex)).toSet - assert(watchedOutpoints === spentOutpoints) + assert(watchedOutpoints == spentOutpoints) alice2blockchain.expectNoMessage(1 second) } private def testRevokedTxConfirmed(f: FixtureParam, channelFeatures: ChannelFeatures): Unit = { import f._ - assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.channelFeatures === channelFeatures) + assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.channelFeatures == channelFeatures) val initOutputCount = channelFeatures.commitmentFormat match { case _: AnchorOutputsCommitmentFormat => 4 case DefaultCommitmentFormat => 2 } - assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx.txOut.size === initOutputCount) + assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx.txOut.size == initOutputCount) // bob's second commit tx contains 2 incoming htlcs val (bobRevokedTx, htlcs1) = { @@ -1568,7 +1568,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val (_, htlc2) = addHtlc(20000000 msat, alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx - assert(bobCommitTx.txOut.size === initOutputCount + 2) + assert(bobCommitTx.txOut.size == initOutputCount + 2) (bobCommitTx, Seq(htlc1, htlc2)) } @@ -1578,18 +1578,18 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val (_, htlc4) = addHtlc(18000000 msat, alice, bob, alice2bob, bob2alice) failHtlc(htlcs1.head.id, bob, alice, bob2alice, alice2bob) crossSign(bob, alice, bob2alice, alice2bob) - assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx.txOut.size === initOutputCount + 3) + assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx.txOut.size == initOutputCount + 3) Seq(htlc3, htlc4) } // alice's first htlc has been failed - assert(alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.Fail]].htlc === htlcs1.head) + assert(alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.Fail]].htlc == htlcs1.head) alice2relayer.expectNoMessage(1 second) // bob publishes one of his revoked txs which quickly confirms alice ! WatchFundingSpentTriggered(bobRevokedTx) alice ! WatchTxConfirmedTriggered(BlockHeight(100), 1, bobRevokedTx) - awaitCond(alice.stateName === CLOSING) + awaitCond(alice.stateName == CLOSING) // alice should fail all pending htlcs val htlcFails = Seq( @@ -1597,7 +1597,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]], alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]] ).map(_.htlc).toSet - assert(htlcFails === Set(htlcs1(1), htlcs2(0), htlcs2(1))) + assert(htlcFails == Set(htlcs1(1), htlcs2(0), htlcs2(1))) alice2relayer.expectNoMessage(1 second) } @@ -1625,7 +1625,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice ! ChannelReestablish(channelId(bob), 42, 42, PrivateKey(ByteVector32.Zeroes), bobCurrentPerCommitmentPoint) val error = alice2bob.expectMsgType[Error] - assert(new String(error.data.toArray) === FundingTxSpent(channelId(alice), initialState.spendingTxs.head).getMessage) + assert(new String(error.data.toArray) == FundingTxSpent(channelId(alice), initialState.spendingTxs.head).getMessage) } test("recv CMD_CLOSE") { f => diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/ChaCha20Poly1305Spec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/ChaCha20Poly1305Spec.scala index 84da3119e0..de613699fe 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/ChaCha20Poly1305Spec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/ChaCha20Poly1305Spec.scala @@ -26,7 +26,7 @@ class ChaCha20Poly1305Spec extends AnyFunSuite { val key: ByteVector = hex"85d6be7857556d337f4452fe42d506a80103808afb0db2fd4abff6af4149f51b" val data: ByteVector = ByteVector.view("Cryptographic Forum Research Group".getBytes("UTF-8")) val mac = Poly1305.mac(key, data) - assert(mac === hex"a8061dc1305136c6c22b8baf0c0127a9") + assert(mac == hex"a8061dc1305136c6c22b8baf0c0127a9") } test("Poly1305 #2") { @@ -34,7 +34,7 @@ class ChaCha20Poly1305Spec extends AnyFunSuite { val key: ByteVector = ByteVector.view("this is 32-byte key for Poly1305".getBytes()) val data: ByteVector = ByteVector.view("Hello world!".getBytes("UTF-8")) val mac = Poly1305.mac(key, data) - assert(mac === hex"a6f745008f81c916a20dcc74eef2b2f0") + assert(mac == hex"a6f745008f81c916a20dcc74eef2b2f0") } test("Chacha20Poly1305 IETF RFC7539 tests") { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/RandomSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/RandomSpec.scala index 4e6b357f86..a5f5d240d1 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/RandomSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/RandomSpec.scala @@ -28,7 +28,7 @@ class RandomSpec extends AnyFunSuiteLike { test("random long generation") { for (rng <- Seq(new WeakRandom(), new StrongRandom())) { val randomNumbers = (1 to 1000).map(_ => rng.nextLong()) - assert(randomNumbers.toSet.size === 1000) + assert(randomNumbers.toSet.size == 1000) val entropy = randomNumbers.foldLeft(0.0) { case (current, next) => current + entropyScore(next) } / 1000 assert(entropy >= 0.98) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/ShaChainSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/ShaChainSpec.scala index 90b84d9347..c4ad17b189 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/ShaChainSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/ShaChainSpec.scala @@ -78,7 +78,7 @@ class ShaChainSpec extends AnyFunSuite { test("generate and receive hashes") { val result: List[ByteVector32] = (for (i <- 0 until 50) yield ShaChain.shaChainFromSeed(seed, 0xFFFFFFFFFFFFFFFFL - i)).toList - assert(result === expected) + assert(result == expected) var receiver = ShaChain.empty for (i <- 0 until 1000) { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/SphinxSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/SphinxSpec.scala index 637983a906..b8a9aeb54a 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/SphinxSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/SphinxSpec.scala @@ -93,7 +93,7 @@ class SphinxSpec extends AnyFunSuite { ) for ((expected, payload) <- testCases) { - assert(peekPayloadLength(payload) === expected) + assert(peekPayloadLength(payload) == expected) } } @@ -110,7 +110,7 @@ class SphinxSpec extends AnyFunSuite { ) for ((expected, packet) <- testCases) { - assert(packet.isLastPacket === expected) + assert(packet.isLastPacket == expected) } } @@ -129,7 +129,7 @@ class SphinxSpec extends AnyFunSuite { for ((packet, expected) <- badOnions zip expected) { val Left(onionErr) = peel(privKeys.head, associatedData, packet) - assert(onionErr === expected) + assert(onionErr == expected) } } @@ -244,22 +244,22 @@ class SphinxSpec extends AnyFunSuite { val expected = DecryptedFailurePacket(publicKeys.head, InvalidOnionKey(ByteVector32.One)) val packet1 = FailurePacket.create(sharedSecrets.head, expected.failureMessage) - assert(packet1.length === FailurePacket.PacketLength) + assert(packet1.length == FailurePacket.PacketLength) val Success(decrypted1) = FailurePacket.decrypt(packet1, Seq(0).map(i => (sharedSecrets(i), publicKeys(i)))) - assert(decrypted1 === expected) + assert(decrypted1 == expected) val packet2 = FailurePacket.wrap(packet1, sharedSecrets(1)) - assert(packet2.length === FailurePacket.PacketLength) + assert(packet2.length == FailurePacket.PacketLength) val Success(decrypted2) = FailurePacket.decrypt(packet2, Seq(1, 0).map(i => (sharedSecrets(i), publicKeys(i)))) - assert(decrypted2 === expected) + assert(decrypted2 == expected) val packet3 = FailurePacket.wrap(packet2, sharedSecrets(2)) - assert(packet3.length === FailurePacket.PacketLength) + assert(packet3.length == FailurePacket.PacketLength) val Success(decrypted3) = FailurePacket.decrypt(packet3, Seq(2, 1, 0).map(i => (sharedSecrets(i), publicKeys(i)))) - assert(decrypted3 === expected) + assert(decrypted3 == expected) } test("decrypt invalid failure message") { @@ -304,24 +304,24 @@ class SphinxSpec extends AnyFunSuite { // node #4 want to reply with an error message val error = FailurePacket.create(sharedSecret4, TemporaryNodeFailure) - assert(error === hex"a5e6bd0c74cb347f10cce367f949098f2457d14c046fd8a22cb96efb30b0fdcda8cb9168b50f2fd45edd73c1b0c8b33002df376801ff58aaa94000bf8a86f92620f343baef38a580102395ae3abf9128d1047a0736ff9b83d456740ebbb4aeb3aa9737f18fb4afb4aa074fb26c4d702f42968888550a3bded8c05247e045b866baef0499f079fdaeef6538f31d44deafffdfd3afa2fb4ca9082b8f1c465371a9894dd8c243fb4847e004f5256b3e90e2edde4c9fb3082ddfe4d1e734cacd96ef0706bf63c9984e22dc98851bcccd1c3494351feb458c9c6af41c0044bea3c47552b1d992ae542b17a2d0bba1a096c78d169034ecb55b6e3a7263c26017f033031228833c1daefc0dedb8cf7c3e37c9c37ebfe42f3225c326e8bcfd338804c145b16e34e4") + assert(error == hex"a5e6bd0c74cb347f10cce367f949098f2457d14c046fd8a22cb96efb30b0fdcda8cb9168b50f2fd45edd73c1b0c8b33002df376801ff58aaa94000bf8a86f92620f343baef38a580102395ae3abf9128d1047a0736ff9b83d456740ebbb4aeb3aa9737f18fb4afb4aa074fb26c4d702f42968888550a3bded8c05247e045b866baef0499f079fdaeef6538f31d44deafffdfd3afa2fb4ca9082b8f1c465371a9894dd8c243fb4847e004f5256b3e90e2edde4c9fb3082ddfe4d1e734cacd96ef0706bf63c9984e22dc98851bcccd1c3494351feb458c9c6af41c0044bea3c47552b1d992ae542b17a2d0bba1a096c78d169034ecb55b6e3a7263c26017f033031228833c1daefc0dedb8cf7c3e37c9c37ebfe42f3225c326e8bcfd338804c145b16e34e4") // error sent back to 3, 2, 1 and 0 val error1 = FailurePacket.wrap(error, sharedSecret3) - assert(error1 === hex"c49a1ce81680f78f5f2000cda36268de34a3f0a0662f55b4e837c83a8773c22aa081bab1616a0011585323930fa5b9fae0c85770a2279ff59ec427ad1bbff9001c0cd1497004bd2a0f68b50704cf6d6a4bf3c8b6a0833399a24b3456961ba00736785112594f65b6b2d44d9f5ea4e49b5e1ec2af978cbe31c67114440ac51a62081df0ed46d4a3df295da0b0fe25c0115019f03f15ec86fabb4c852f83449e812f141a9395b3f70b766ebbd4ec2fae2b6955bd8f32684c15abfe8fd3a6261e52650e8807a92158d9f1463261a925e4bfba44bd20b166d532f0017185c3a6ac7957adefe45559e3072c8dc35abeba835a8cb01a71a15c736911126f27d46a36168ca5ef7dccd4e2886212602b181463e0dd30185c96348f9743a02aca8ec27c0b90dca270") + assert(error1 == hex"c49a1ce81680f78f5f2000cda36268de34a3f0a0662f55b4e837c83a8773c22aa081bab1616a0011585323930fa5b9fae0c85770a2279ff59ec427ad1bbff9001c0cd1497004bd2a0f68b50704cf6d6a4bf3c8b6a0833399a24b3456961ba00736785112594f65b6b2d44d9f5ea4e49b5e1ec2af978cbe31c67114440ac51a62081df0ed46d4a3df295da0b0fe25c0115019f03f15ec86fabb4c852f83449e812f141a9395b3f70b766ebbd4ec2fae2b6955bd8f32684c15abfe8fd3a6261e52650e8807a92158d9f1463261a925e4bfba44bd20b166d532f0017185c3a6ac7957adefe45559e3072c8dc35abeba835a8cb01a71a15c736911126f27d46a36168ca5ef7dccd4e2886212602b181463e0dd30185c96348f9743a02aca8ec27c0b90dca270") val error2 = FailurePacket.wrap(error1, sharedSecret2) - assert(error2 === hex"a5d3e8634cfe78b2307d87c6d90be6fe7855b4f2cc9b1dfb19e92e4b79103f61ff9ac25f412ddfb7466e74f81b3e545563cdd8f5524dae873de61d7bdfccd496af2584930d2b566b4f8d3881f8c043df92224f38cf094cfc09d92655989531524593ec6d6caec1863bdfaa79229b5020acc034cd6deeea1021c50586947b9b8e6faa83b81fbfa6133c0af5d6b07c017f7158fa94f0d206baf12dda6b68f785b773b360fd0497e16cc402d779c8d48d0fa6315536ef0660f3f4e1865f5b38ea49c7da4fd959de4e83ff3ab686f059a45c65ba2af4a6a79166aa0f496bf04d06987b6d2ea205bdb0d347718b9aeff5b61dfff344993a275b79717cd815b6ad4c0beb568c4ac9c36ff1c315ec1119a1993c4b61e6eaa0375e0aaf738ac691abd3263bf937e3") + assert(error2 == hex"a5d3e8634cfe78b2307d87c6d90be6fe7855b4f2cc9b1dfb19e92e4b79103f61ff9ac25f412ddfb7466e74f81b3e545563cdd8f5524dae873de61d7bdfccd496af2584930d2b566b4f8d3881f8c043df92224f38cf094cfc09d92655989531524593ec6d6caec1863bdfaa79229b5020acc034cd6deeea1021c50586947b9b8e6faa83b81fbfa6133c0af5d6b07c017f7158fa94f0d206baf12dda6b68f785b773b360fd0497e16cc402d779c8d48d0fa6315536ef0660f3f4e1865f5b38ea49c7da4fd959de4e83ff3ab686f059a45c65ba2af4a6a79166aa0f496bf04d06987b6d2ea205bdb0d347718b9aeff5b61dfff344993a275b79717cd815b6ad4c0beb568c4ac9c36ff1c315ec1119a1993c4b61e6eaa0375e0aaf738ac691abd3263bf937e3") val error3 = FailurePacket.wrap(error2, sharedSecret1) - assert(error3 === hex"aac3200c4968f56b21f53e5e374e3a2383ad2b1b6501bbcc45abc31e59b26881b7dfadbb56ec8dae8857add94e6702fb4c3a4de22e2e669e1ed926b04447fc73034bb730f4932acd62727b75348a648a1128744657ca6a4e713b9b646c3ca66cac02cdab44dd3439890ef3aaf61708714f7375349b8da541b2548d452d84de7084bb95b3ac2345201d624d31f4d52078aa0fa05a88b4e20202bd2b86ac5b52919ea305a8949de95e935eed0319cf3cf19ebea61d76ba92532497fcdc9411d06bcd4275094d0a4a3c5d3a945e43305a5a9256e333e1f64dbca5fcd4e03a39b9012d197506e06f29339dfee3331995b21615337ae060233d39befea925cc262873e0530408e6990f1cbd233a150ef7b004ff6166c70c68d9f8c853c1abca640b8660db2921") + assert(error3 == hex"aac3200c4968f56b21f53e5e374e3a2383ad2b1b6501bbcc45abc31e59b26881b7dfadbb56ec8dae8857add94e6702fb4c3a4de22e2e669e1ed926b04447fc73034bb730f4932acd62727b75348a648a1128744657ca6a4e713b9b646c3ca66cac02cdab44dd3439890ef3aaf61708714f7375349b8da541b2548d452d84de7084bb95b3ac2345201d624d31f4d52078aa0fa05a88b4e20202bd2b86ac5b52919ea305a8949de95e935eed0319cf3cf19ebea61d76ba92532497fcdc9411d06bcd4275094d0a4a3c5d3a945e43305a5a9256e333e1f64dbca5fcd4e03a39b9012d197506e06f29339dfee3331995b21615337ae060233d39befea925cc262873e0530408e6990f1cbd233a150ef7b004ff6166c70c68d9f8c853c1abca640b8660db2921") val error4 = FailurePacket.wrap(error3, sharedSecret0) - assert(error4 === hex"9c5add3963fc7f6ed7f148623c84134b5647e1306419dbe2174e523fa9e2fbed3a06a19f899145610741c83ad40b7712aefaddec8c6baf7325d92ea4ca4d1df8bce517f7e54554608bf2bd8071a4f52a7a2f7ffbb1413edad81eeea5785aa9d990f2865dc23b4bc3c301a94eec4eabebca66be5cf638f693ec256aec514620cc28ee4a94bd9565bc4d4962b9d3641d4278fb319ed2b84de5b665f307a2db0f7fbb757366067d88c50f7e829138fde4f78d39b5b5802f1b92a8a820865af5cc79f9f30bc3f461c66af95d13e5e1f0381c184572a91dee1c849048a647a1158cf884064deddbf1b0b88dfe2f791428d0ba0f6fb2f04e14081f69165ae66d9297c118f0907705c9c4954a199bae0bb96fad763d690e7daa6cfda59ba7f2c8d11448b604d12d") + assert(error4 == hex"9c5add3963fc7f6ed7f148623c84134b5647e1306419dbe2174e523fa9e2fbed3a06a19f899145610741c83ad40b7712aefaddec8c6baf7325d92ea4ca4d1df8bce517f7e54554608bf2bd8071a4f52a7a2f7ffbb1413edad81eeea5785aa9d990f2865dc23b4bc3c301a94eec4eabebca66be5cf638f693ec256aec514620cc28ee4a94bd9565bc4d4962b9d3641d4278fb319ed2b84de5b665f307a2db0f7fbb757366067d88c50f7e829138fde4f78d39b5b5802f1b92a8a820865af5cc79f9f30bc3f461c66af95d13e5e1f0381c184572a91dee1c849048a647a1158cf884064deddbf1b0b88dfe2f791428d0ba0f6fb2f04e14081f69165ae66d9297c118f0907705c9c4954a199bae0bb96fad763d690e7daa6cfda59ba7f2c8d11448b604d12d") // origin parses error packet and can see that it comes from node #4 val Success(DecryptedFailurePacket(pubkey, failure)) = FailurePacket.decrypt(error4, sharedSecrets) - assert(pubkey === publicKeys(4)) - assert(failure === TemporaryNodeFailure) + assert(pubkey == publicKeys(4)) + assert(failure == TemporaryNodeFailure) } } @@ -335,7 +335,7 @@ class SphinxSpec extends AnyFunSuite { for (error <- errors) { val wrapped = FailurePacket.wrap(error, sharedSecret) - assert(wrapped.length === FailurePacket.PacketLength) + assert(wrapped.length == FailurePacket.PacketLength) } } @@ -367,34 +367,34 @@ class SphinxSpec extends AnyFunSuite { // origin parses error packet and can see that it comes from node #2 val Success(DecryptedFailurePacket(pubkey, failure)) = FailurePacket.decrypt(error2, sharedSecrets) - assert(pubkey === publicKeys(2)) - assert(failure === InvalidRealm) + assert(pubkey == publicKeys(2)) + assert(failure == InvalidRealm) } } test("create blinded route (reference test vector)") { val sessionKey = PrivateKey(hex"0101010101010101010101010101010101010101010101010101010101010101") val blindedRoute = RouteBlinding.create(sessionKey, publicKeys, routeBlindingPayloads) - assert(blindedRoute.introductionNode.publicKey === publicKeys(0)) - assert(blindedRoute.introductionNodeId === publicKeys(0)) - assert(blindedRoute.introductionNode.blindedPublicKey === PublicKey(hex"02ec68ed555f5d18b12fe0e2208563c3566032967cf11dc29b20c345449f9a50a2")) - assert(blindedRoute.introductionNode.blindingEphemeralKey === PublicKey(hex"031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f")) - assert(blindedRoute.introductionNode.encryptedPayload === hex"af4fbf67bd52520bdfab6a88cd4e7f22ffad08d8b153b17ff303f93fdb4712") - assert(blindedRoute.blindedNodeIds === Seq( + assert(blindedRoute.introductionNode.publicKey == publicKeys(0)) + assert(blindedRoute.introductionNodeId == publicKeys(0)) + assert(blindedRoute.introductionNode.blindedPublicKey == PublicKey(hex"02ec68ed555f5d18b12fe0e2208563c3566032967cf11dc29b20c345449f9a50a2")) + assert(blindedRoute.introductionNode.blindingEphemeralKey == PublicKey(hex"031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f")) + assert(blindedRoute.introductionNode.encryptedPayload == hex"af4fbf67bd52520bdfab6a88cd4e7f22ffad08d8b153b17ff303f93fdb4712") + assert(blindedRoute.blindedNodeIds == Seq( PublicKey(hex"02ec68ed555f5d18b12fe0e2208563c3566032967cf11dc29b20c345449f9a50a2"), PublicKey(hex"022b09d77fb3374ee3ed9d2153e15e9962944ad1690327cbb0a9acb7d90f168763"), PublicKey(hex"03d9f889364dc5a173460a2a6cc565b4ca78931792115dd6ef82c0e18ced837372"), PublicKey(hex"03bfddd2253b42fe12edd37f9071a3883830ed61a4bc347eeac63421629cf032b5"), PublicKey(hex"03a8588bc4a0a2f0d2fb8d5c0f8d062fb4d78bfba24a85d0ddeb4fd35dd3b34110"), )) - assert(blindedRoute.subsequentNodes.map(_.blindedPublicKey) === Seq( + assert(blindedRoute.subsequentNodes.map(_.blindedPublicKey) == Seq( PublicKey(hex"022b09d77fb3374ee3ed9d2153e15e9962944ad1690327cbb0a9acb7d90f168763"), PublicKey(hex"03d9f889364dc5a173460a2a6cc565b4ca78931792115dd6ef82c0e18ced837372"), PublicKey(hex"03bfddd2253b42fe12edd37f9071a3883830ed61a4bc347eeac63421629cf032b5"), PublicKey(hex"03a8588bc4a0a2f0d2fb8d5c0f8d062fb4d78bfba24a85d0ddeb4fd35dd3b34110"), )) - assert(blindedRoute.encryptedPayloads === blindedRoute.introductionNode.encryptedPayload +: blindedRoute.subsequentNodes.map(_.encryptedPayload)) - assert(blindedRoute.subsequentNodes.map(_.encryptedPayload) === Seq( + assert(blindedRoute.encryptedPayloads == blindedRoute.introductionNode.encryptedPayload +: blindedRoute.subsequentNodes.map(_.encryptedPayload)) + assert(blindedRoute.subsequentNodes.map(_.encryptedPayload) == Seq( hex"146c9694ead7de2a54fc43e8bb927bfc377dda7ed5a2e36b327b739e368aa602e43e07e14bfb81d66e1e295f848b6f15ee6483005abb830f4ef08a9da6", hex"8ad7d5d448f15208417a1840f82274101b3c254c24b1b49fd676fd0c4293c9aa66ed51da52579e934a869f016f213044d1b13b63bf586e9c9832106b59", hex"52a45a884542d180e76fe84fc13e71a01f65d943ff89aed29b94644a91b037b9143cfda8f1ff25ba61c37108a5ae57d9ddc5ab688ee8b2f9f6bd94522c", @@ -403,31 +403,31 @@ class SphinxSpec extends AnyFunSuite { // The introduction point can decrypt its encrypted payload and obtain the next ephemeral public key. val Success((payload0, ephKey1)) = RouteBlinding.decryptPayload(privKeys(0), blindedRoute.introductionNode.blindingEphemeralKey, blindedRoute.encryptedPayloads(0)) - assert(payload0 === routeBlindingPayloads(0)) - assert(ephKey1 === PublicKey(hex"035cb4c003d58e16cc9207270b3596c2be3309eca64c36b208c946bbb599bfcad0")) + assert(payload0 == routeBlindingPayloads(0)) + assert(ephKey1 == PublicKey(hex"035cb4c003d58e16cc9207270b3596c2be3309eca64c36b208c946bbb599bfcad0")) // The next node can derive the private key used to unwrap the onion and decrypt its encrypted payload. - assert(RouteBlinding.derivePrivateKey(privKeys(1), ephKey1).publicKey === blindedRoute.blindedNodeIds(1)) + assert(RouteBlinding.derivePrivateKey(privKeys(1), ephKey1).publicKey == blindedRoute.blindedNodeIds(1)) val Success((payload1, ephKey2)) = RouteBlinding.decryptPayload(privKeys(1), ephKey1, blindedRoute.encryptedPayloads(1)) - assert(payload1 === routeBlindingPayloads(1)) - assert(ephKey2 === PublicKey(hex"02e105bc01a7af07074a1b0b1d9a112a1d89c6cd87cc4e2b6ba3a824731d9508bd")) + assert(payload1 == routeBlindingPayloads(1)) + assert(ephKey2 == PublicKey(hex"02e105bc01a7af07074a1b0b1d9a112a1d89c6cd87cc4e2b6ba3a824731d9508bd")) // The next node can derive the private key used to unwrap the onion and decrypt its encrypted payload. - assert(RouteBlinding.derivePrivateKey(privKeys(2), ephKey2).publicKey === blindedRoute.blindedNodeIds(2)) + assert(RouteBlinding.derivePrivateKey(privKeys(2), ephKey2).publicKey == blindedRoute.blindedNodeIds(2)) val Success((payload2, ephKey3)) = RouteBlinding.decryptPayload(privKeys(2), ephKey2, blindedRoute.encryptedPayloads(2)) - assert(payload2 === routeBlindingPayloads(2)) - assert(ephKey3 === PublicKey(hex"0349164db5398925ef234002e62d2834da115b8eafc73436fab98ed12266e797cc")) + assert(payload2 == routeBlindingPayloads(2)) + assert(ephKey3 == PublicKey(hex"0349164db5398925ef234002e62d2834da115b8eafc73436fab98ed12266e797cc")) // The next node can derive the private key used to unwrap the onion and decrypt its encrypted payload. - assert(RouteBlinding.derivePrivateKey(privKeys(3), ephKey3).publicKey === blindedRoute.blindedNodeIds(3)) + assert(RouteBlinding.derivePrivateKey(privKeys(3), ephKey3).publicKey == blindedRoute.blindedNodeIds(3)) val Success((payload3, ephKey4)) = RouteBlinding.decryptPayload(privKeys(3), ephKey3, blindedRoute.encryptedPayloads(3)) - assert(payload3 === routeBlindingPayloads(3)) - assert(ephKey4 === PublicKey(hex"020a6d1951916adcac22125063f62c35b3686f36e5db2f77073f3d35b19c7a118a")) + assert(payload3 == routeBlindingPayloads(3)) + assert(ephKey4 == PublicKey(hex"020a6d1951916adcac22125063f62c35b3686f36e5db2f77073f3d35b19c7a118a")) // The last node can derive the private key used to unwrap the onion and decrypt its encrypted payload. - assert(RouteBlinding.derivePrivateKey(privKeys(4), ephKey4).publicKey === blindedRoute.blindedNodeIds(4)) + assert(RouteBlinding.derivePrivateKey(privKeys(4), ephKey4).publicKey == blindedRoute.blindedNodeIds(4)) val Success((payload4, _)) = RouteBlinding.decryptPayload(privKeys(4), ephKey4, blindedRoute.encryptedPayloads(4)) - assert(payload4 === routeBlindingPayloads(4)) + assert(payload4 == routeBlindingPayloads(4)) } test("concatenate blinded routes (reference test vector)") { @@ -440,7 +440,7 @@ class SphinxSpec extends AnyFunSuite { hex"010f000000000000000000000000000000 061000112233445566778899aabbccddeeff" ) val blindedRoute = RouteBlinding.create(sessionKey, publicKeys.drop(2), payloads) - assert(blindedRoute.blindingKey === PublicKey(hex"031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f")) + assert(blindedRoute.blindingKey == PublicKey(hex"031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f")) (blindedRoute.blindingKey, blindedRoute, payloads) } // The sender also wants to use route blinding to reach the introduction point. @@ -454,15 +454,15 @@ class SphinxSpec extends AnyFunSuite { (RouteBlinding.create(sessionKey, publicKeys.take(2), payloads), payloads) } val blindedRoute = BlindedRoute(publicKeys(0), blindedRouteStart.blindingKey, blindedRouteStart.blindedNodes ++ blindedRouteEnd.blindedNodes) - assert(blindedRoute.blindingKey === PublicKey(hex"024d4b6cd1361032ca9bd2aeb9d900aa4d45d9ead80ac9423374c451a7254d0766")) - assert(blindedRoute.blindedNodeIds === Seq( + assert(blindedRoute.blindingKey == PublicKey(hex"024d4b6cd1361032ca9bd2aeb9d900aa4d45d9ead80ac9423374c451a7254d0766")) + assert(blindedRoute.blindedNodeIds == Seq( PublicKey(hex"0303176d13958a8a59d59517a6223e12cf291ba5f65c8011efcdca0a52c3850abc"), PublicKey(hex"03adbdd3c0fb69641e96de2d5ac923ffc0910d3ed4dfe2314609fae61a71df4da2"), PublicKey(hex"021026e6369e42b7f6d723c0c56a3e0b4d67111f07685bd03e9fa6d93ac6bb6dbe"), PublicKey(hex"02ba3db3fe7f1ed28c4d82f28cf358373cbf3241a16aba265b1b6fb26f094c0c7f"), PublicKey(hex"0379d4ca14cb19e2f7bcb217d36267e3d03b027bc4228923967f5b2e32cbb763c1"), )) - assert(blindedRoute.encryptedPayloads === Seq( + assert(blindedRoute.encryptedPayloads == Seq( hex"31da0d438752ed0f19ccd970a386ead7155fd187becd4e1770d561dffdb03d3568dac746dde98725f146582cb040207e8b6c070e28d707564a4dd9fb53f9274ad69d09add393b509a2fa42df5055d7c8aeda5881d5aa", hex"d9dfa92f898dc8e37b73c944aa4205f225337b2edde67623e775c79e2bcf395dc205004aa07fdc65712afa5c2687aff9bb3d5e6af7c89cc94f23f962a27844ce7629773f9413ebcf131dbc35818410df207f29b013b0", hex"30015dcdcbce70bdcd0125be8ccd541b101d95bcb049ccfc737f91c98cc139cb6f16354ec5a38e77eca769c2245ac4467524d6", @@ -472,35 +472,35 @@ class SphinxSpec extends AnyFunSuite { // The introduction point can decrypt its encrypted payload and obtain the next ephemeral public key. val Success((payload0, ephKey1)) = RouteBlinding.decryptPayload(privKeys(0), blindedRoute.blindingKey, blindedRoute.encryptedPayloads(0)) - assert(payload0 === payloadsStart(0)) - assert(ephKey1 === PublicKey(hex"02be4b436dbc6cfa43d7d5652bc630ffdaf0dac93e6682db7950828506055ad1a7")) + assert(payload0 == payloadsStart(0)) + assert(ephKey1 == PublicKey(hex"02be4b436dbc6cfa43d7d5652bc630ffdaf0dac93e6682db7950828506055ad1a7")) // The next node can derive the private key used to unwrap the onion and decrypt its encrypted payload. - assert(RouteBlinding.derivePrivateKey(privKeys(1), ephKey1).publicKey === blindedRoute.blindedNodeIds(1)) + assert(RouteBlinding.derivePrivateKey(privKeys(1), ephKey1).publicKey == blindedRoute.blindedNodeIds(1)) val Success((payload1, ephKey2)) = RouteBlinding.decryptPayload(privKeys(1), ephKey1, blindedRoute.encryptedPayloads(1)) - assert(payload1 === payloadsStart(1)) - assert(ephKey2 === PublicKey(hex"03fb82254d740754efddc3318674f4e26cefcb8dec42a3910c08c64d19f25e50b7")) + assert(payload1 == payloadsStart(1)) + assert(ephKey2 == PublicKey(hex"03fb82254d740754efddc3318674f4e26cefcb8dec42a3910c08c64d19f25e50b7")) // NB: this node finds a blinding override and will transmit that instead of ephKey2 to the next node. assert(payload1.containsSlice(blindingOverride.value)) // The next node must be given the blinding override to derive the private key used to unwrap the onion and decrypt its encrypted payload. assert(RouteBlinding.decryptPayload(privKeys(2), ephKey2, blindedRoute.encryptedPayloads(2)).isFailure) - assert(RouteBlinding.derivePrivateKey(privKeys(2), blindingOverride).publicKey === blindedRoute.blindedNodeIds(2)) + assert(RouteBlinding.derivePrivateKey(privKeys(2), blindingOverride).publicKey == blindedRoute.blindedNodeIds(2)) val Success((payload2, ephKey3)) = RouteBlinding.decryptPayload(privKeys(2), blindingOverride, blindedRoute.encryptedPayloads(2)) - assert(payload2 === payloadsEnd(0)) - assert(ephKey3 === PublicKey(hex"03932f4ab7605e8c046b5677becd4d61fdfdc8b9d10f1e9c3080ced0d64fd76931")) + assert(payload2 == payloadsEnd(0)) + assert(ephKey3 == PublicKey(hex"03932f4ab7605e8c046b5677becd4d61fdfdc8b9d10f1e9c3080ced0d64fd76931")) // The next node can derive the private key used to unwrap the onion and decrypt its encrypted payload. - assert(RouteBlinding.derivePrivateKey(privKeys(3), ephKey3).publicKey === blindedRoute.blindedNodeIds(3)) + assert(RouteBlinding.derivePrivateKey(privKeys(3), ephKey3).publicKey == blindedRoute.blindedNodeIds(3)) val Success((payload3, ephKey4)) = RouteBlinding.decryptPayload(privKeys(3), ephKey3, blindedRoute.encryptedPayloads(3)) - assert(payload3 === payloadsEnd(1)) - assert(ephKey4 === PublicKey(hex"037bceb365470d24f8204c622e1b7959c6beeb774c634640de6c8401079159fc58")) + assert(payload3 == payloadsEnd(1)) + assert(ephKey4 == PublicKey(hex"037bceb365470d24f8204c622e1b7959c6beeb774c634640de6c8401079159fc58")) // The last node can derive the private key used to unwrap the onion and decrypt its encrypted payload. - assert(RouteBlinding.derivePrivateKey(privKeys(4), ephKey4).publicKey === blindedRoute.blindedNodeIds(4)) + assert(RouteBlinding.derivePrivateKey(privKeys(4), ephKey4).publicKey == blindedRoute.blindedNodeIds(4)) val Success((payload4, ephKey5)) = RouteBlinding.decryptPayload(privKeys(4), ephKey4, blindedRoute.encryptedPayloads(4)) - assert(payload4 === payloadsEnd(2)) - assert(ephKey5 === PublicKey(hex"0339ddfa85a2155fb27e94742885fad85696e54920aa148cb86e00bcb8ee346bd4")) + assert(payload4 == payloadsEnd(2)) + assert(ephKey5 == PublicKey(hex"0339ddfa85a2155fb27e94742885fad85696e54920aa148cb86e00bcb8ee346bd4")) } test("invalid blinded route") { @@ -523,7 +523,7 @@ class SphinxSpec extends AnyFunSuite { // The sender obtains this information (e.g. from a Bolt11 invoice) and prepends two normal hops to reach the introduction node. val nodeIds = publicKeys.take(2) ++ Seq(blindedRoute.introductionNodeId) ++ blindedRoute.subsequentNodes.map(_.blindedPublicKey) - assert(blindedRoute.encryptedPayloads === Seq( + assert(blindedRoute.encryptedPayloads == Seq( hex"36285ee1c0b289eeedf05c9ab66a7b669d92bd3729082c1c42e443d2775b3be7de74b1c64e0cebd0e3a8cddeff2e9acb1ddb62cbb73166723cae905938", hex"14cd98e3f4f29cc7af8624250c5bd14fb5a69be8235748909e754ef43b09b1b424b1bba062d801f8648f28fb1101b9a56dcb1f69d5e1ba7fe584f6a6be", hex"fe7862b65ac8e1c2a319ba558513d97dc237132b22ce4f7439983545e37164d792dc6925a3c7cde855ac824871bd455f2859298456da3ade87d884080d", @@ -552,11 +552,11 @@ class SphinxSpec extends AnyFunSuite { // However it contains a blinding point and encrypted data, which it can decrypt to discover the next node. val Right(DecryptedPacket(payload2, nextPacket2, sharedSecret2)) = peel(privKeys(2), associatedData, nextPacket1) val tlvs2 = PaymentOnionCodecs.tlvPerHopPayloadCodec.decode(payload2.bits).require.value - assert(tlvs2.get[OnionPaymentPayloadTlv.BlindingPoint].map(_.publicKey) === Some(blindingEphemeralKey0)) + assert(tlvs2.get[OnionPaymentPayloadTlv.BlindingPoint].map(_.publicKey) == Some(blindingEphemeralKey0)) assert(tlvs2.get[OnionPaymentPayloadTlv.EncryptedRecipientData].nonEmpty) val Success((recipientTlvs2, blindingEphemeralKey1)) = RouteBlindingEncryptedDataCodecs.decode(privKeys(2), blindingEphemeralKey0, tlvs2.get[OnionPaymentPayloadTlv.EncryptedRecipientData].get.data) - assert(recipientTlvs2.get[RouteBlindingEncryptedDataTlv.OutgoingChannelId].map(_.shortChannelId) === Some(ShortChannelId(1105))) - assert(recipientTlvs2.get[RouteBlindingEncryptedDataTlv.OutgoingNodeId].map(_.nodeId) === Some(publicKeys(3))) + assert(recipientTlvs2.get[RouteBlindingEncryptedDataTlv.OutgoingChannelId].map(_.shortChannelId) == Some(ShortChannelId(1105))) + assert(recipientTlvs2.get[RouteBlindingEncryptedDataTlv.OutgoingNodeId].map(_.nodeId) == Some(publicKeys(3))) // The fourth hop is a blinded hop. // It receives the blinding key from the previous node (e.g. in a tlv field in update_add_htlc) which it can use to @@ -567,7 +567,7 @@ class SphinxSpec extends AnyFunSuite { val tlvs3 = PaymentOnionCodecs.tlvPerHopPayloadCodec.decode(payload3.bits).require.value assert(tlvs3.get[OnionPaymentPayloadTlv.EncryptedRecipientData].nonEmpty) val Success((recipientTlvs3, blindingEphemeralKey2)) = RouteBlindingEncryptedDataCodecs.decode(privKeys(3), blindingEphemeralKey1, tlvs3.get[OnionPaymentPayloadTlv.EncryptedRecipientData].get.data) - assert(recipientTlvs3.get[RouteBlindingEncryptedDataTlv.OutgoingNodeId].map(_.nodeId) === Some(publicKeys(4))) + assert(recipientTlvs3.get[RouteBlindingEncryptedDataTlv.OutgoingNodeId].map(_.nodeId) == Some(publicKeys(4))) // The fifth hop is the blinded recipient. // It receives the blinding key from the previous node (e.g. in a tlv field in update_add_htlc) which it can use to @@ -577,7 +577,7 @@ class SphinxSpec extends AnyFunSuite { val tlvs4 = PaymentOnionCodecs.tlvPerHopPayloadCodec.decode(payload4.bits).require.value assert(tlvs4.get[OnionPaymentPayloadTlv.EncryptedRecipientData].nonEmpty) val Success((recipientTlvs4, _)) = RouteBlindingEncryptedDataCodecs.decode(privKeys(4), blindingEphemeralKey2, tlvs4.get[OnionPaymentPayloadTlv.EncryptedRecipientData].get.data) - assert(recipientTlvs4.get[RouteBlindingEncryptedDataTlv.PathId].map(_.data) === associatedData.map(_.bytes)) + assert(recipientTlvs4.get[RouteBlindingEncryptedDataTlv.PathId].map(_.data) == associatedData.map(_.bytes)) assert(Seq(payload0, payload1, payload2, payload3, payload4) == payloads) assert(Seq(sharedSecret0, sharedSecret1, sharedSecret2, sharedSecret3, sharedSecret4) == sharedSecrets.map(_._1)) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/TransportHandlerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/TransportHandlerSpec.scala index 2e915fdd4a..bc7d35797c 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/TransportHandlerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/TransportHandlerSpec.scala @@ -187,12 +187,12 @@ class TransportHandlerSpec extends TestKitBaseClass with AnyFunSuiteLike with Be } val ciphertexts = loop(Encryptor(enc), 1002) - assert(ciphertexts(0) === hex"cf2b30ddf0cf3f80e7c35a6e6730b59fe802473180f396d88a8fb0db8cbcf25d2f214cf9ea1d95") - assert(ciphertexts(1) === hex"72887022101f0b6753e0c7de21657d35a4cb2a1f5cde2650528bbc8f837d0f0d7ad833b1a256a1") - assert(ciphertexts(500) === hex"178cb9d7387190fa34db9c2d50027d21793c9bc2d40b1e14dcf30ebeeeb220f48364f7a4c68bf8") - assert(ciphertexts(501) === hex"1b186c57d44eb6de4c057c49940d79bb838a145cb528d6e8fd26dbe50a60ca2c104b56b60e45bd") - assert(ciphertexts(1000) === hex"4a2f3cc3b5e78ddb83dcb426d9863d9d9a723b0337c89dd0b005d89f8d3c05c52b76b29b740f09") - assert(ciphertexts(1001) === hex"2ecd8c8a5629d0d02ab457a0fdd0f7b90a192cd46be5ecb6ca570bfc5e268338b1a16cf4ef2d36") + assert(ciphertexts(0) == hex"cf2b30ddf0cf3f80e7c35a6e6730b59fe802473180f396d88a8fb0db8cbcf25d2f214cf9ea1d95") + assert(ciphertexts(1) == hex"72887022101f0b6753e0c7de21657d35a4cb2a1f5cde2650528bbc8f837d0f0d7ad833b1a256a1") + assert(ciphertexts(500) == hex"178cb9d7387190fa34db9c2d50027d21793c9bc2d40b1e14dcf30ebeeeb220f48364f7a4c68bf8") + assert(ciphertexts(501) == hex"1b186c57d44eb6de4c057c49940d79bb838a145cb528d6e8fd26dbe50a60ca2c104b56b60e45bd") + assert(ciphertexts(1000) == hex"4a2f3cc3b5e78ddb83dcb426d9863d9d9a723b0337c89dd0b005d89f8d3c05c52b76b29b740f09") + assert(ciphertexts(1001) == hex"2ecd8c8a5629d0d02ab457a0fdd0f7b90a192cd46be5ecb6ca570bfc5e268338b1a16cf4ef2d36") } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/keymanager/LocalNodeKeyManagerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/keymanager/LocalNodeKeyManagerSpec.scala index d3277cff65..3b33f54809 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/keymanager/LocalNodeKeyManagerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/keymanager/LocalNodeKeyManagerSpec.scala @@ -69,7 +69,7 @@ class LocalNodeKeyManagerSpec extends AnyFunSuite { val (signature, recid) = testKeyManager.signDigest(digest) val recoveredPubkey = Crypto.recoverPublicKey(signature, digest, recid) - assert(recoveredPubkey === testKeyManager.nodeId) + assert(recoveredPubkey == testKeyManager.nodeId) assert(Crypto.verifySignature(digest, signature, testKeyManager.nodeId)) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/AuditDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/AuditDbSpec.scala index 738242a9c7..b3316ab38e 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/AuditDbSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/AuditDbSpec.scala @@ -100,12 +100,12 @@ class AuditDbSpec extends AnyFunSuite { db.add(e11) db.add(e12) - assert(db.listSent(from = TimestampMilli(0L), to = TimestampMilli.now() + 15.minute).toSet === Set(e1, e5, e6)) - assert(db.listSent(from = TimestampMilli(100000L), to = TimestampMilli.now() + 1.minute).toList === List(e1)) - assert(db.listReceived(from = TimestampMilli(0L), to = TimestampMilli.now() + 1.minute).toList === List(e2)) - assert(db.listRelayed(from = TimestampMilli(0L), to = TimestampMilli.now() + 1.minute).toList === List(e3, e10, e11, e12)) - assert(db.listNetworkFees(from = TimestampMilli(0L), to = TimestampMilli.now() + 1.minute).size === 1) - assert(db.listNetworkFees(from = TimestampMilli(0L), to = TimestampMilli.now() + 1.minute).head.txType === "mutual") + assert(db.listSent(from = TimestampMilli(0L), to = TimestampMilli.now() + 15.minute).toSet == Set(e1, e5, e6)) + assert(db.listSent(from = TimestampMilli(100000L), to = TimestampMilli.now() + 1.minute).toList == List(e1)) + assert(db.listReceived(from = TimestampMilli(0L), to = TimestampMilli.now() + 1.minute).toList == List(e2)) + assert(db.listRelayed(from = TimestampMilli(0L), to = TimestampMilli.now() + 1.minute).toList == List(e3, e10, e11, e12)) + assert(db.listNetworkFees(from = TimestampMilli(0L), to = TimestampMilli.now() + 1.minute).size == 1) + assert(db.listNetworkFees(from = TimestampMilli(0L), to = TimestampMilli.now() + 1.minute).head.txType == "mutual") } } @@ -147,7 +147,7 @@ class AuditDbSpec extends AnyFunSuite { db.add(TransactionConfirmed(c4, n4, Transaction(0, Seq.empty, Seq(TxOut(2500 sat, hex"ffffff")), 0))) // doesn't match a published tx // NB: we only count a relay fee for the outgoing channel, no the incoming one. - assert(db.stats(0 unixms, TimestampMilli.now() + 1.milli).toSet === Set( + assert(db.stats(0 unixms, TimestampMilli.now() + 1.milli).toSet == Set( Stats(channelId = c1, direction = "IN", avgPaymentAmount = 0 sat, paymentCount = 0, relayFee = 0 sat, networkFee = 0 sat), Stats(channelId = c1, direction = "OUT", avgPaymentAmount = 42 sat, paymentCount = 3, relayFee = 4 sat, networkFee = 0 sat), Stats(channelId = c2, direction = "IN", avgPaymentAmount = 0 sat, paymentCount = 0, relayFee = 0 sat, networkFee = 500 sat), @@ -248,7 +248,7 @@ class AuditDbSpec extends AnyFunSuite { targetVersion = SqliteAuditDb.CURRENT_VERSION, postCheck = connection => { // existing rows in the 'sent' table will use id=00000000-0000-0000-0000-000000000000 as default - assert(dbs.audit.listSent(0 unixms, TimestampMilli.now() + 1.minute) === Seq(ps.copy(id = ZERO_UUID, parts = Seq(ps.parts.head.copy(id = ZERO_UUID))))) + assert(dbs.audit.listSent(0 unixms, TimestampMilli.now() + 1.minute) == Seq(ps.copy(id = ZERO_UUID, parts = Seq(ps.parts.head.copy(id = ZERO_UUID))))) val postMigrationDb = new SqliteAuditDb(connection) @@ -262,7 +262,7 @@ class AuditDbSpec extends AnyFunSuite { // the old record will have the UNKNOWN_UUID but the new ones will have their actual id val expected = Seq(ps.copy(id = ZERO_UUID, parts = Seq(ps.parts.head.copy(id = ZERO_UUID))), ps1) - assert(postMigrationDb.listSent(0 unixms, TimestampMilli.now() + 1.minute) === expected) + assert(postMigrationDb.listSent(0 unixms, TimestampMilli.now() + 1.minute) == expected) } ) } @@ -380,11 +380,11 @@ class AuditDbSpec extends AnyFunSuite { using(connection.createStatement()) { statement => assert(getVersion(statement, "audit").contains(SqliteAuditDb.CURRENT_VERSION)) } - assert(migratedDb.listSent(50 unixms, 150 unixms).toSet === Set( + assert(migratedDb.listSent(50 unixms, 150 unixms).toSet == Set( ps1.copy(id = pp1.id, recipientAmount = pp1.amount, parts = pp1 :: Nil), ps1.copy(id = pp2.id, recipientAmount = pp2.amount, parts = pp2 :: Nil) )) - assert(migratedDb.listRelayed(100 unixms, 120 unixms) === Seq(relayed1, relayed2)) + assert(migratedDb.listRelayed(100 unixms, 120 unixms) == Seq(relayed1, relayed2)) val postMigrationDb = new SqliteAuditDb(connection) using(connection.createStatement()) { statement => @@ -396,9 +396,9 @@ class AuditDbSpec extends AnyFunSuite { )) val relayed3 = TrampolinePaymentRelayed(randomBytes32(), Seq(PaymentRelayed.Part(450 msat, randomBytes32()), PaymentRelayed.Part(500 msat, randomBytes32())), Seq(PaymentRelayed.Part(800 msat, randomBytes32())), randomKey().publicKey, 700 msat, 150 unixms) postMigrationDb.add(ps2) - assert(postMigrationDb.listSent(155 unixms, 200 unixms) === Seq(ps2)) + assert(postMigrationDb.listSent(155 unixms, 200 unixms) == Seq(ps2)) postMigrationDb.add(relayed3) - assert(postMigrationDb.listRelayed(100 unixms, 160 unixms) === Seq(relayed1, relayed2, relayed3)) + assert(postMigrationDb.listRelayed(100 unixms, 160 unixms) == Seq(relayed1, relayed2, relayed3)) } ) } @@ -479,7 +479,7 @@ class AuditDbSpec extends AnyFunSuite { postCheck = connection => { val migratedDb = dbs.audit - assert(migratedDb.listRelayed(100 unixms, 120 unixms) === Seq(relayed1, relayed2)) + assert(migratedDb.listRelayed(100 unixms, 120 unixms) == Seq(relayed1, relayed2)) val postMigrationDb = new PgAuditDb()(dbs.datasource) using(connection.createStatement()) { statement => @@ -487,7 +487,7 @@ class AuditDbSpec extends AnyFunSuite { } val relayed3 = TrampolinePaymentRelayed(randomBytes32(), Seq(PaymentRelayed.Part(450 msat, randomBytes32()), PaymentRelayed.Part(500 msat, randomBytes32())), Seq(PaymentRelayed.Part(800 msat, randomBytes32())), randomKey().publicKey, 700 msat, 150 unixms) postMigrationDb.add(relayed3) - assert(postMigrationDb.listRelayed(100 unixms, 160 unixms) === Seq(relayed1, relayed2, relayed3)) + assert(postMigrationDb.listRelayed(100 unixms, 160 unixms) == Seq(relayed1, relayed2, relayed3)) } ) case dbs: TestSqliteDatabases => @@ -562,7 +562,7 @@ class AuditDbSpec extends AnyFunSuite { using(connection.createStatement()) { statement => assert(getVersion(statement, "audit").contains(SqliteAuditDb.CURRENT_VERSION)) } - assert(migratedDb.listRelayed(100 unixms, 120 unixms) === Seq(relayed1, relayed2)) + assert(migratedDb.listRelayed(100 unixms, 120 unixms) == Seq(relayed1, relayed2)) val postMigrationDb = new SqliteAuditDb(connection) using(connection.createStatement()) { statement => @@ -570,7 +570,7 @@ class AuditDbSpec extends AnyFunSuite { } val relayed3 = TrampolinePaymentRelayed(randomBytes32(), Seq(PaymentRelayed.Part(450 msat, randomBytes32()), PaymentRelayed.Part(500 msat, randomBytes32())), Seq(PaymentRelayed.Part(800 msat, randomBytes32())), randomKey().publicKey, 700 msat, 150 unixms) postMigrationDb.add(relayed3) - assert(postMigrationDb.listRelayed(100 unixms, 160 unixms) === Seq(relayed1, relayed2, relayed3)) + assert(postMigrationDb.listRelayed(100 unixms, 160 unixms) == Seq(relayed1, relayed2, relayed3)) } ) } @@ -640,7 +640,7 @@ class AuditDbSpec extends AnyFunSuite { postCheck = connection => { val migratedDb = dbs.audit using(connection.createStatement()) { statement => assert(getVersion(statement, "audit").contains(PgAuditDb.CURRENT_VERSION)) } - assert(migratedDb.listNetworkFees(0 unixms, 700 unixms) === networkFees) + assert(migratedDb.listNetworkFees(0 unixms, 700 unixms) == networkFees) } ) case dbs: TestSqliteDatabases => @@ -698,7 +698,7 @@ class AuditDbSpec extends AnyFunSuite { postCheck = connection => { val migratedDb = dbs.audit using(connection.createStatement()) { statement => assert(getVersion(statement, "audit").contains(SqliteAuditDb.CURRENT_VERSION)) } - assert(migratedDb.listNetworkFees(0 unixms, 700 unixms) === networkFees) + assert(migratedDb.listNetworkFees(0 unixms, 700 unixms) == networkFees) } ) } @@ -744,7 +744,7 @@ class AuditDbSpec extends AnyFunSuite { statement.executeUpdate() } - assert(db.listRelayed(0 unixms, 40 unixms) === Nil) + assert(db.listRelayed(0 unixms, 40 unixms) == Nil) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/ChannelsDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/ChannelsDbSpec.scala index 871adad0d6..5d4c5dede7 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/ChannelsDbSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/ChannelsDbSpec.scala @@ -70,16 +70,16 @@ class ChannelsDbSpec extends AnyFunSuite { intercept[SQLException](db.addHtlcInfo(channel1.channelId, commitNumber, paymentHash1, cltvExpiry1)) // no related channel - assert(db.listLocalChannels().toSet === Set.empty) + assert(db.listLocalChannels().toSet == Set.empty) db.addOrUpdateChannel(channel1) db.addOrUpdateChannel(channel1) - assert(db.listLocalChannels() === List(channel1)) + assert(db.listLocalChannels() == List(channel1)) db.addOrUpdateChannel(channel2a) - assert(db.listLocalChannels() === List(channel1, channel2a)) + assert(db.listLocalChannels() == List(channel1, channel2a)) assert(db.getChannel(channel1.channelId).contains(channel1)) assert(db.getChannel(channel2a.channelId).contains(channel2a)) db.addOrUpdateChannel(channel2b) - assert(db.listLocalChannels() === List(channel1, channel2b)) + assert(db.listLocalChannels() == List(channel1, channel2b)) assert(db.getChannel(channel2b.channelId).contains(channel2b)) assert(db.listHtlcInfos(channel1.channelId, commitNumber).toList == Nil) @@ -90,11 +90,11 @@ class ChannelsDbSpec extends AnyFunSuite { db.removeChannel(channel1.channelId) assert(db.getChannel(channel1.channelId).isEmpty) - assert(db.listLocalChannels() === List(channel2b)) + assert(db.listLocalChannels() == List(channel2b)) assert(db.listHtlcInfos(channel1.channelId, commitNumber).toList == Nil) db.removeChannel(channel2b.channelId) assert(db.getChannel(channel2b.channelId).isEmpty) - assert(db.listLocalChannels() === Nil) + assert(db.listLocalChannels() == Nil) } } @@ -195,11 +195,11 @@ class ChannelsDbSpec extends AnyFunSuite { using(sqlite.createStatement()) { statement => assert(getVersion(statement, "channels").contains(targetVersion)) } - assert(db.listLocalChannels().size === testCases.size) + assert(db.listLocalChannels().size == testCases.size) for (testCase <- testCases) { db.updateChannelMeta(testCase.channelId, ChannelEvent.EventType.Created) // this call must not fail for (commitmentNumber <- testCase.commitmentNumbers) { - assert(db.listHtlcInfos(testCase.channelId, commitmentNumber).size === testCase.commitmentNumbers.count(_ == commitmentNumber)) + assert(db.listHtlcInfos(testCase.channelId, commitmentNumber).size == testCase.commitmentNumbers.count(_ == commitmentNumber)) } } } @@ -207,11 +207,11 @@ class ChannelsDbSpec extends AnyFunSuite { test("migrate channel database v2 -> current") { def postCheck(channelsDb: ChannelsDb): Unit = { - assert(channelsDb.listLocalChannels().size === testCases.filterNot(_.isClosed).size) + assert(channelsDb.listLocalChannels().size == testCases.filterNot(_.isClosed).size) for (testCase <- testCases.filterNot(_.isClosed)) { channelsDb.updateChannelMeta(testCase.channelId, ChannelEvent.EventType.Created) // this call must not fail for (commitmentNumber <- testCase.commitmentNumbers) { - assert(channelsDb.listHtlcInfos(testCase.channelId, commitmentNumber).size === testCase.commitmentNumbers.count(_ == commitmentNumber)) + assert(channelsDb.listHtlcInfos(testCase.channelId, commitmentNumber).size == testCase.commitmentNumbers.count(_ == commitmentNumber)) } } } @@ -320,13 +320,13 @@ class ChannelsDbSpec extends AnyFunSuite { dbName = PgChannelsDb.DB_NAME, targetVersion = PgChannelsDb.CURRENT_VERSION, postCheck = connection => { - assert(dbs.channels.listLocalChannels().size === testCases.filterNot(_.isClosed).size) + assert(dbs.channels.listLocalChannels().size == testCases.filterNot(_.isClosed).size) testCases.foreach { testCase => - assert(getPgTimestamp(connection, testCase.channelId, "created_timestamp") === testCase.createdTimestamp) - assert(getPgTimestamp(connection, testCase.channelId, "last_payment_sent_timestamp") === testCase.lastPaymentSentTimestamp) - assert(getPgTimestamp(connection, testCase.channelId, "last_payment_received_timestamp") === testCase.lastPaymentReceivedTimestamp) - assert(getPgTimestamp(connection, testCase.channelId, "last_connected_timestamp") === testCase.lastConnectedTimestamp) - assert(getPgTimestamp(connection, testCase.channelId, "closed_timestamp") === testCase.closedTimestamp) + assert(getPgTimestamp(connection, testCase.channelId, "created_timestamp") == testCase.createdTimestamp) + assert(getPgTimestamp(connection, testCase.channelId, "last_payment_sent_timestamp") == testCase.lastPaymentSentTimestamp) + assert(getPgTimestamp(connection, testCase.channelId, "last_payment_received_timestamp") == testCase.lastPaymentReceivedTimestamp) + assert(getPgTimestamp(connection, testCase.channelId, "last_connected_timestamp") == testCase.lastConnectedTimestamp) + assert(getPgTimestamp(connection, testCase.channelId, "closed_timestamp") == testCase.closedTimestamp) } } ) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/DualDatabasesSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/DualDatabasesSpec.scala index a9ae922f23..a4b81d94ea 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/DualDatabasesSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/DualDatabasesSpec.scala @@ -25,7 +25,7 @@ class DualDatabasesSpec extends TestKitBaseClass with AnyFunSuiteLike { db.channels.addOrUpdateChannel(ChannelCodecsSpec.normal) assert(db.primary.channels.listLocalChannels().nonEmpty) - awaitCond(db.primary.channels.listLocalChannels() === db.secondary.channels.listLocalChannels()) + awaitCond(db.primary.channels.listLocalChannels() == db.secondary.channels.listLocalChannels()) } test("postgres primary") { @@ -33,7 +33,7 @@ class DualDatabasesSpec extends TestKitBaseClass with AnyFunSuiteLike { db.channels.addOrUpdateChannel(ChannelCodecsSpec.normal) assert(db.primary.channels.listLocalChannels().nonEmpty) - awaitCond(db.primary.channels.listLocalChannels() === db.secondary.channels.listLocalChannels()) + awaitCond(db.primary.channels.listLocalChannels() == db.secondary.channels.listLocalChannels()) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/NetworkDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/NetworkDbSpec.scala index bdc551e04b..a1e39298f9 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/NetworkDbSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/NetworkDbSpec.scala @@ -60,17 +60,17 @@ class NetworkDbSpec extends AnyFunSuite { val node_3 = Announcements.makeNodeAnnouncement(randomKey(), "node-charlie", Color(100.toByte, 200.toByte, 300.toByte), NodeAddress.fromParts("192.168.1.42", 42000).get :: Nil, Features(VariableLengthOnion -> Optional)) val node_4 = Announcements.makeNodeAnnouncement(randomKey(), "node-charlie", Color(100.toByte, 200.toByte, 300.toByte), Tor2("aaaqeayeaudaocaj", 42000) :: Nil, Features.empty) - assert(db.listNodes().toSet === Set.empty) + assert(db.listNodes().toSet == Set.empty) db.addNode(node_1) db.addNode(node_1) // duplicate is ignored - assert(db.getNode(node_1.nodeId) === Some(node_1)) - assert(db.listNodes().size === 1) + assert(db.getNode(node_1.nodeId) == Some(node_1)) + assert(db.listNodes().size == 1) db.addNode(node_2) db.addNode(node_3) db.addNode(node_4) - assert(db.listNodes().toSet === Set(node_1, node_2, node_3, node_4)) + assert(db.listNodes().toSet == Set(node_1, node_2, node_3, node_4)) db.removeNode(node_2.nodeId) - assert(db.listNodes().toSet === Set(node_1, node_3, node_4)) + assert(db.listNodes().toSet == Set(node_1, node_3, node_4)) db.updateNode(node_1) assert(node_4.addresses == List(Tor2("aaaqeayeaudaocaj", 42000))) @@ -84,7 +84,7 @@ class NetworkDbSpec extends AnyFunSuite { val c = Announcements.makeChannelAnnouncement(Block.RegtestGenesisBlock.hash, ShortChannelId(42), randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, sig, sig, sig, sig) val txid = ByteVector32.fromValidHex("0001" * 16) db.addChannel(c, txid, Satoshi(42)) - assert(db.listChannels() === SortedMap(c.shortChannelId -> PublicChannel(c, txid, Satoshi(42), None, None, None))) + assert(db.listChannels() == SortedMap(c.shortChannelId -> PublicChannel(c, txid, Satoshi(42), None, None, None))) } } @@ -113,18 +113,18 @@ class NetworkDbSpec extends AnyFunSuite { val txid_3 = randomBytes32() val capacity = 10000 sat - assert(db.listChannels().toSet === Set.empty) + assert(db.listChannels().toSet == Set.empty) db.addChannel(channel_1, txid_1, capacity) db.addChannel(channel_1, txid_1, capacity) // duplicate is ignored - assert(db.listChannels().size === 1) + assert(db.listChannels().size == 1) db.addChannel(channel_2, txid_2, capacity) db.addChannel(channel_3, txid_3, capacity) - assert(db.listChannels() === SortedMap( + assert(db.listChannels() == SortedMap( channel_1.shortChannelId -> PublicChannel(channel_1, txid_1, capacity, None, None, None), channel_2.shortChannelId -> PublicChannel(channel_2, txid_2, capacity, None, None, None), channel_3.shortChannelId -> PublicChannel(channel_3, txid_3, capacity, None, None, None))) db.removeChannel(channel_2.shortChannelId) - assert(db.listChannels() === SortedMap( + assert(db.listChannels() == SortedMap( channel_1.shortChannelId -> PublicChannel(channel_1, txid_1, capacity, None, None, None), channel_3.shortChannelId -> PublicChannel(channel_3, txid_3, capacity, None, None, None))) @@ -136,11 +136,11 @@ class NetworkDbSpec extends AnyFunSuite { db.updateChannel(channel_update_1) // duplicate is ignored db.updateChannel(channel_update_2) db.updateChannel(channel_update_3) - assert(db.listChannels() === SortedMap( + assert(db.listChannels() == SortedMap( channel_1.shortChannelId -> PublicChannel(channel_1, txid_1, capacity, Some(channel_update_1), Some(channel_update_2), None), channel_3.shortChannelId -> PublicChannel(channel_3, txid_3, capacity, Some(channel_update_3), None, None))) db.removeChannel(channel_3.shortChannelId) - assert(db.listChannels() === SortedMap( + assert(db.listChannels() == SortedMap( channel_1.shortChannelId -> PublicChannel(channel_1, txid_1, capacity, Some(channel_update_1), Some(channel_update_2), None))) } @@ -210,11 +210,11 @@ class NetworkDbSpec extends AnyFunSuite { val txid = randomBytes32() channels.foreach(ca => db.addChannel(ca, txid, capacity)) updates.foreach(u => db.updateChannel(u)) - assert(db.listChannels().keySet === channels.map(_.shortChannelId).toSet) + assert(db.listChannels().keySet == channels.map(_.shortChannelId).toSet) val toDelete = channels.map(_.shortChannelId).take(1 + Random.nextInt(2500)) db.removeChannels(toDelete) - assert(db.listChannels().keySet === (channels.map(_.shortChannelId).toSet -- toDelete)) + assert(db.listChannels().keySet == (channels.map(_.shortChannelId).toSet -- toDelete)) } } @@ -279,9 +279,9 @@ class NetworkDbSpec extends AnyFunSuite { dbName = SqliteNetworkDb.DB_NAME, targetVersion = SqliteNetworkDb.CURRENT_VERSION, postCheck = _ => { - assert(dbs.network.listNodes().toSet === nodeTestCases.map(_.node).toSet) + assert(dbs.network.listNodes().toSet == nodeTestCases.map(_.node).toSet) // NB: channel updates are not migrated - assert(dbs.network.listChannels().values.toSet === channelTestCases.map(tc => PublicChannel(tc.channel, tc.txid, tc.capacity, None, None, None)).toSet) + assert(dbs.network.listChannels().values.toSet == channelTestCases.map(tc => PublicChannel(tc.channel, tc.txid, tc.capacity, None, None, None)).toSet) } ) } @@ -319,9 +319,9 @@ class NetworkDbSpec extends AnyFunSuite { dbName = PgNetworkDb.DB_NAME, targetVersion = PgNetworkDb.CURRENT_VERSION, postCheck = _ => { - assert(dbs.network.listNodes().toSet === nodeTestCases.map(_.node).toSet) + assert(dbs.network.listNodes().toSet == nodeTestCases.map(_.node).toSet) // NB: channel updates are not migrated - assert(dbs.network.listChannels().values.toSet === channelTestCases.map(tc => PublicChannel(tc.channel, tc.txid, tc.capacity, tc.update_1_opt, tc.update_2_opt, None)).toSet) + assert(dbs.network.listChannels().values.toSet == channelTestCases.map(tc => PublicChannel(tc.channel, tc.txid, tc.capacity, tc.update_1_opt, tc.update_2_opt, None)).toSet) } ) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/PaymentsDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/PaymentsDbSpec.scala index 7e3a93b85a..9a0511430a 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/PaymentsDbSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/PaymentsDbSpec.scala @@ -85,8 +85,8 @@ class PaymentsDbSpec extends AnyFunSuite { db.addIncomingPayment(i1, preimage1) db.receiveIncomingPayment(i1.paymentHash, 550 msat, 1100 unixms) - assert(db.listIncomingPayments(1 unixms, 1500 unixms) === Seq(pr1)) - assert(db.listOutgoingPayments(1 unixms, 1500 unixms) === Seq(ps1)) + assert(db.listIncomingPayments(1 unixms, 1500 unixms) == Seq(pr1)) + assert(db.listOutgoingPayments(1 unixms, 1500 unixms) == Seq(ps1)) } ) @@ -184,9 +184,9 @@ class PaymentsDbSpec extends AnyFunSuite { postCheck = _ => { val db = dbs.db.payments - assert(db.getIncomingPayment(i1.paymentHash) === Some(pr1)) - assert(db.getIncomingPayment(i2.paymentHash) === Some(pr2)) - assert(db.listOutgoingPayments(1 unixms, 2000 unixms) === Seq(ps1, ps2, ps3)) + assert(db.getIncomingPayment(i1.paymentHash) == Some(pr1)) + assert(db.getIncomingPayment(i2.paymentHash) == Some(pr2)) + assert(db.listOutgoingPayments(1 unixms, 2000 unixms) == Seq(ps1, ps2, ps3)) val i3 = Bolt11Invoice(Block.TestnetGenesisBlock.hash, Some(561 msat), paymentHash3, alicePriv, Left("invoice #3"), CltvExpiryDelta(18), expirySeconds = Some(30)) val pr3 = IncomingPayment(i3, preimage3, PaymentType.Standard, i3.createdAt.toTimestampMilli, IncomingPaymentStatus.Pending) @@ -201,9 +201,9 @@ class PaymentsDbSpec extends AnyFunSuite { db.addOutgoingPayment(ps6.copy(status = OutgoingPaymentStatus.Pending)) db.updateOutgoingPayment(PaymentFailed(ps6.id, ps6.paymentHash, Nil, 1300 unixms)) - assert(db.listOutgoingPayments(1 unixms, 2000 unixms) === Seq(ps1, ps2, ps3, ps4, ps5, ps6)) - assert(db.listIncomingPayments(1 unixms, TimestampMilli.now()) === Seq(pr1, pr2, pr3)) - assert(db.listExpiredIncomingPayments(1 unixms, 2000 unixms) === Seq(pr2)) + assert(db.listOutgoingPayments(1 unixms, 2000 unixms) == Seq(ps1, ps2, ps3, ps4, ps5, ps6)) + assert(db.listIncomingPayments(1 unixms, TimestampMilli.now()) == Seq(pr1, pr2, pr3)) + assert(db.listExpiredIncomingPayments(1 unixms, 2000 unixms) == Seq(pr2)) }) } @@ -284,8 +284,8 @@ class PaymentsDbSpec extends AnyFunSuite { targetVersion = SqlitePaymentsDb.CURRENT_VERSION, postCheck = _ => { val db = dbs.db.payments - assert(db.getOutgoingPayment(id1) === Some(ps1)) - assert(db.listOutgoingPayments(parentId) === Seq(ps2, ps3)) + assert(db.getOutgoingPayment(id1) == Some(ps1)) + assert(db.listOutgoingPayments(parentId) == Seq(ps2, ps3)) } ) } @@ -378,18 +378,18 @@ class PaymentsDbSpec extends AnyFunSuite { postCheck = _ => { val db = dbs.db.payments - assert(db.getIncomingPayment(i1.paymentHash) === Some(pr1)) - assert(db.getIncomingPayment(i2.paymentHash) === Some(pr2)) - assert(db.listIncomingPayments(TimestampMilli(Instant.parse("2020-01-01T00:00:00.00Z").toEpochMilli), TimestampMilli(Instant.parse("2100-12-31T23:59:59.00Z").toEpochMilli)) === Seq(pr2, pr1)) - assert(db.listIncomingPayments(TimestampMilli(Instant.parse("2020-01-01T00:00:00.00Z").toEpochMilli), TimestampMilli(Instant.parse("2020-12-31T23:59:59.00Z").toEpochMilli)) === Seq(pr2)) - assert(db.listIncomingPayments(TimestampMilli(Instant.parse("2010-01-01T00:00:00.00Z").toEpochMilli), TimestampMilli(Instant.parse("2011-12-31T23:59:59.00Z").toEpochMilli)) === Seq.empty) - assert(db.listExpiredIncomingPayments(TimestampMilli(Instant.parse("2020-01-01T00:00:00.00Z").toEpochMilli), TimestampMilli(Instant.parse("2100-12-31T23:59:59.00Z").toEpochMilli)) === Seq(pr2)) - assert(db.listExpiredIncomingPayments(TimestampMilli(Instant.parse("2020-01-01T00:00:00.00Z").toEpochMilli), TimestampMilli(Instant.parse("2020-12-31T23:59:59.00Z").toEpochMilli)) === Seq(pr2)) - assert(db.listExpiredIncomingPayments(TimestampMilli(Instant.parse("2010-01-01T00:00:00.00Z").toEpochMilli), TimestampMilli(Instant.parse("2011-12-31T23:59:59.00Z").toEpochMilli)) === Seq.empty) - - assert(db.listOutgoingPayments(TimestampMilli(Instant.parse("2020-01-01T00:00:00.00Z").toEpochMilli), TimestampMilli(Instant.parse("2021-12-31T23:59:59.00Z").toEpochMilli)) === Seq(ps2, ps1, ps3)) - assert(db.listOutgoingPayments(TimestampMilli(Instant.parse("2010-01-01T00:00:00.00Z").toEpochMilli), TimestampMilli(Instant.parse("2021-01-15T23:59:59.00Z").toEpochMilli)) === Seq(ps2, ps1)) - assert(db.listOutgoingPayments(TimestampMilli(Instant.parse("2010-01-01T00:00:00.00Z").toEpochMilli), TimestampMilli(Instant.parse("2011-12-31T23:59:59.00Z").toEpochMilli)) === Seq.empty) + assert(db.getIncomingPayment(i1.paymentHash) == Some(pr1)) + assert(db.getIncomingPayment(i2.paymentHash) == Some(pr2)) + assert(db.listIncomingPayments(TimestampMilli(Instant.parse("2020-01-01T00:00:00.00Z").toEpochMilli), TimestampMilli(Instant.parse("2100-12-31T23:59:59.00Z").toEpochMilli)) == Seq(pr2, pr1)) + assert(db.listIncomingPayments(TimestampMilli(Instant.parse("2020-01-01T00:00:00.00Z").toEpochMilli), TimestampMilli(Instant.parse("2020-12-31T23:59:59.00Z").toEpochMilli)) == Seq(pr2)) + assert(db.listIncomingPayments(TimestampMilli(Instant.parse("2010-01-01T00:00:00.00Z").toEpochMilli), TimestampMilli(Instant.parse("2011-12-31T23:59:59.00Z").toEpochMilli)) == Seq.empty) + assert(db.listExpiredIncomingPayments(TimestampMilli(Instant.parse("2020-01-01T00:00:00.00Z").toEpochMilli), TimestampMilli(Instant.parse("2100-12-31T23:59:59.00Z").toEpochMilli)) == Seq(pr2)) + assert(db.listExpiredIncomingPayments(TimestampMilli(Instant.parse("2020-01-01T00:00:00.00Z").toEpochMilli), TimestampMilli(Instant.parse("2020-12-31T23:59:59.00Z").toEpochMilli)) == Seq(pr2)) + assert(db.listExpiredIncomingPayments(TimestampMilli(Instant.parse("2010-01-01T00:00:00.00Z").toEpochMilli), TimestampMilli(Instant.parse("2011-12-31T23:59:59.00Z").toEpochMilli)) == Seq.empty) + + assert(db.listOutgoingPayments(TimestampMilli(Instant.parse("2020-01-01T00:00:00.00Z").toEpochMilli), TimestampMilli(Instant.parse("2021-12-31T23:59:59.00Z").toEpochMilli)) == Seq(ps2, ps1, ps3)) + assert(db.listOutgoingPayments(TimestampMilli(Instant.parse("2010-01-01T00:00:00.00Z").toEpochMilli), TimestampMilli(Instant.parse("2021-01-15T23:59:59.00Z").toEpochMilli)) == Seq(ps2, ps1)) + assert(db.listOutgoingPayments(TimestampMilli(Instant.parse("2010-01-01T00:00:00.00Z").toEpochMilli), TimestampMilli(Instant.parse("2011-12-31T23:59:59.00Z").toEpochMilli)) == Seq.empty) } ) } @@ -427,29 +427,29 @@ class PaymentsDbSpec extends AnyFunSuite { db.addIncomingPayment(paidInvoice1, payment1.paymentPreimage) db.addIncomingPayment(paidInvoice2, payment2.paymentPreimage) - assert(db.getIncomingPayment(pendingInvoice1.paymentHash) === Some(pendingPayment1)) - assert(db.getIncomingPayment(expiredInvoice2.paymentHash) === Some(expiredPayment2)) - assert(db.getIncomingPayment(paidInvoice1.paymentHash) === Some(payment1.copy(status = IncomingPaymentStatus.Pending))) + assert(db.getIncomingPayment(pendingInvoice1.paymentHash) == Some(pendingPayment1)) + assert(db.getIncomingPayment(expiredInvoice2.paymentHash) == Some(expiredPayment2)) + assert(db.getIncomingPayment(paidInvoice1.paymentHash) == Some(payment1.copy(status = IncomingPaymentStatus.Pending))) val now = TimestampMilli.now() - assert(db.listIncomingPayments(0 unixms, now) === Seq(expiredPayment1, expiredPayment2, pendingPayment1, pendingPayment2, payment1.copy(status = IncomingPaymentStatus.Pending), payment2.copy(status = IncomingPaymentStatus.Pending))) - assert(db.listExpiredIncomingPayments(0 unixms, now) === Seq(expiredPayment1, expiredPayment2)) - assert(db.listReceivedIncomingPayments(0 unixms, now) === Nil) - assert(db.listPendingIncomingPayments(0 unixms, now) === Seq(pendingPayment1, pendingPayment2, payment1.copy(status = IncomingPaymentStatus.Pending), payment2.copy(status = IncomingPaymentStatus.Pending))) + assert(db.listIncomingPayments(0 unixms, now) == Seq(expiredPayment1, expiredPayment2, pendingPayment1, pendingPayment2, payment1.copy(status = IncomingPaymentStatus.Pending), payment2.copy(status = IncomingPaymentStatus.Pending))) + assert(db.listExpiredIncomingPayments(0 unixms, now) == Seq(expiredPayment1, expiredPayment2)) + assert(db.listReceivedIncomingPayments(0 unixms, now) == Nil) + assert(db.listPendingIncomingPayments(0 unixms, now) == Seq(pendingPayment1, pendingPayment2, payment1.copy(status = IncomingPaymentStatus.Pending), payment2.copy(status = IncomingPaymentStatus.Pending))) db.receiveIncomingPayment(paidInvoice1.paymentHash, 461 msat, receivedAt1) db.receiveIncomingPayment(paidInvoice1.paymentHash, 100 msat, receivedAt2) // adding another payment to this invoice should sum db.receiveIncomingPayment(paidInvoice2.paymentHash, 1111 msat, receivedAt2) - assert(db.getIncomingPayment(paidInvoice1.paymentHash) === Some(payment1)) + assert(db.getIncomingPayment(paidInvoice1.paymentHash) == Some(payment1)) - assert(db.listIncomingPayments(0 unixms, now) === Seq(expiredPayment1, expiredPayment2, pendingPayment1, pendingPayment2, payment1, payment2)) - assert(db.listIncomingPayments(now - 60.seconds, now) === Seq(pendingPayment1, pendingPayment2, payment1, payment2)) - assert(db.listPendingIncomingPayments(0 unixms, now) === Seq(pendingPayment1, pendingPayment2)) - assert(db.listReceivedIncomingPayments(0 unixms, now) === Seq(payment1, payment2)) + assert(db.listIncomingPayments(0 unixms, now) == Seq(expiredPayment1, expiredPayment2, pendingPayment1, pendingPayment2, payment1, payment2)) + assert(db.listIncomingPayments(now - 60.seconds, now) == Seq(pendingPayment1, pendingPayment2, payment1, payment2)) + assert(db.listPendingIncomingPayments(0 unixms, now) == Seq(pendingPayment1, pendingPayment2)) + assert(db.listReceivedIncomingPayments(0 unixms, now) == Seq(payment1, payment2)) assert(db.removeIncomingPayment(paidInvoice1.paymentHash).isFailure) - db.removeIncomingPayment(paidInvoice1.paymentHash).failed.foreach(e => assert(e.getMessage === "Cannot remove a received incoming payment")) + db.removeIncomingPayment(paidInvoice1.paymentHash).failed.foreach(e => assert(e.getMessage == "Cannot remove a received incoming payment")) assert(db.removeIncomingPayment(pendingPayment1.invoice.paymentHash).isSuccess) assert(db.removeIncomingPayment(pendingPayment1.invoice.paymentHash).isSuccess) // idempotent assert(db.removeIncomingPayment(expiredPayment1.invoice.paymentHash).isSuccess) @@ -476,12 +476,12 @@ class PaymentsDbSpec extends AnyFunSuite { assert(db.listOutgoingPayments(1 unixms, 300 unixms).toList == Seq(s1, s2)) assert(db.listOutgoingPayments(1 unixms, 150 unixms).toList == Seq(s1)) assert(db.listOutgoingPayments(150 unixms, 250 unixms).toList == Seq(s2)) - assert(db.getOutgoingPayment(s1.id) === Some(s1)) - assert(db.getOutgoingPayment(UUID.randomUUID()) === None) - assert(db.listOutgoingPayments(s2.paymentHash) === Seq(s1, s2)) - assert(db.listOutgoingPayments(s1.id) === Nil) - assert(db.listOutgoingPayments(parentId) === Seq(s1, s2)) - assert(db.listOutgoingPayments(ByteVector32.Zeroes) === Nil) + assert(db.getOutgoingPayment(s1.id) == Some(s1)) + assert(db.getOutgoingPayment(UUID.randomUUID()) == None) + assert(db.listOutgoingPayments(s2.paymentHash) == Seq(s1, s2)) + assert(db.listOutgoingPayments(s1.id) == Nil) + assert(db.listOutgoingPayments(parentId) == Seq(s1, s2)) + assert(db.listOutgoingPayments(ByteVector32.Zeroes) == Nil) val s3 = s2.copy(id = UUID.randomUUID(), amount = 789 msat, createdAt = 300 unixms) val s4 = s2.copy(id = UUID.randomUUID(), paymentType = PaymentType.Standard, createdAt = 301 unixms) @@ -490,10 +490,10 @@ class PaymentsDbSpec extends AnyFunSuite { db.updateOutgoingPayment(PaymentFailed(s3.id, s3.paymentHash, Nil, 310 unixms)) val ss3 = s3.copy(status = OutgoingPaymentStatus.Failed(Nil, 310 unixms)) - assert(db.getOutgoingPayment(s3.id) === Some(ss3)) + assert(db.getOutgoingPayment(s3.id) == Some(ss3)) db.updateOutgoingPayment(PaymentFailed(s4.id, s4.paymentHash, Seq(LocalFailure(s4.amount, Seq(hop_ab), new RuntimeException("woops")), RemoteFailure(s4.amount, Seq(hop_ab, hop_bc), Sphinx.DecryptedFailurePacket(carol, UnknownNextPeer))), 320 unixms)) val ss4 = s4.copy(status = OutgoingPaymentStatus.Failed(Seq(FailureSummary(FailureType.LOCAL, "woops", List(HopSummary(alice, bob, Some(ShortChannelId(42)))), Some(alice)), FailureSummary(FailureType.REMOTE, "processing node does not know the next peer in the route", List(HopSummary(alice, bob, Some(ShortChannelId(42))), HopSummary(bob, carol, None)), Some(carol))), 320 unixms)) - assert(db.getOutgoingPayment(s4.id) === Some(ss4)) + assert(db.getOutgoingPayment(s4.id) == Some(ss4)) // can't update again once it's in a final state assertThrows[IllegalArgumentException](db.updateOutgoingPayment(PaymentSent(parentId, s3.paymentHash, preimage1, s3.recipientAmount, s3.recipientNodeId, Seq(PaymentSent.PartialPayment(s3.id, s3.amount, 42 msat, randomBytes32(), None))))) @@ -505,9 +505,9 @@ class PaymentsDbSpec extends AnyFunSuite { val ss1 = s1.copy(status = OutgoingPaymentStatus.Succeeded(preimage1, 15 msat, Nil, 400 unixms)) val ss2 = s2.copy(status = OutgoingPaymentStatus.Succeeded(preimage1, 20 msat, Seq(HopSummary(alice, bob, Some(ShortChannelId(42))), HopSummary(bob, carol, None)), 410 unixms)) db.updateOutgoingPayment(paymentSent) - assert(db.getOutgoingPayment(s1.id) === Some(ss1)) - assert(db.getOutgoingPayment(s2.id) === Some(ss2)) - assert(db.listOutgoingPayments(parentId) === Seq(ss1, ss2, ss3, ss4)) + assert(db.getOutgoingPayment(s1.id) == Some(ss1)) + assert(db.getOutgoingPayment(s2.id) == Some(ss2)) + assert(db.listOutgoingPayments(parentId) == Seq(ss1, ss2, ss3, ss4)) // can't update again once it's in a final state assertThrows[IllegalArgumentException](db.updateOutgoingPayment(PaymentFailed(s1.id, s1.paymentHash, Nil))) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/PeersDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/PeersDbSpec.scala index 1b0661b71f..848b946f04 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/PeersDbSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/PeersDbSpec.scala @@ -56,20 +56,20 @@ class PeersDbSpec extends AnyFunSuite { val peer_2 = TestCase(randomKey().publicKey, Tor2("z4zif3fy7fe7bpg3", 4231)) val peer_3 = TestCase(randomKey().publicKey, Tor3("mrl2d3ilhctt2vw4qzvmz3etzjvpnc6dczliq5chrxetthgbuczuggyd", 4231)) - assert(db.listPeers().toSet === Set.empty) + assert(db.listPeers().toSet == Set.empty) db.addOrUpdatePeer(peer_1.nodeId, peer_1.nodeAddress) - assert(db.getPeer(peer_1.nodeId) === Some(peer_1.nodeAddress)) - assert(db.getPeer(peer_2.nodeId) === None) + assert(db.getPeer(peer_1.nodeId) == Some(peer_1.nodeAddress)) + assert(db.getPeer(peer_2.nodeId) == None) db.addOrUpdatePeer(peer_1.nodeId, peer_1.nodeAddress) // duplicate is ignored - assert(db.listPeers().size === 1) + assert(db.listPeers().size == 1) db.addOrUpdatePeer(peer_2.nodeId, peer_2.nodeAddress) db.addOrUpdatePeer(peer_3.nodeId, peer_3.nodeAddress) - assert(db.listPeers().map(p => TestCase(p._1, p._2)).toSet === Set(peer_1, peer_2, peer_3)) + assert(db.listPeers().map(p => TestCase(p._1, p._2)).toSet == Set(peer_1, peer_2, peer_3)) db.removePeer(peer_2.nodeId) - assert(db.listPeers().map(p => TestCase(p._1, p._2)).toSet === Set(peer_1, peer_3)) + assert(db.listPeers().map(p => TestCase(p._1, p._2)).toSet == Set(peer_1, peer_3)) db.addOrUpdatePeer(peer_1_bis.nodeId, peer_1_bis.nodeAddress) - assert(db.getPeer(peer_1.nodeId) === Some(peer_1_bis.nodeAddress)) - assert(db.listPeers().map(p => TestCase(p._1, p._2)).toSet === Set(peer_1_bis, peer_3)) + assert(db.getPeer(peer_1.nodeId) == Some(peer_1_bis.nodeAddress)) + assert(db.listPeers().map(p => TestCase(p._1, p._2)).toSet == Set(peer_1_bis, peer_3)) } } @@ -93,17 +93,17 @@ class PeersDbSpec extends AnyFunSuite { val a = randomKey().publicKey val b = randomKey().publicKey - assert(db.getRelayFees(a) === None) - assert(db.getRelayFees(b) === None) + assert(db.getRelayFees(a) == None) + assert(db.getRelayFees(b) == None) db.addOrUpdateRelayFees(a, RelayFees(1 msat, 123)) - assert(db.getRelayFees(a) === Some(RelayFees(1 msat, 123))) - assert(db.getRelayFees(b) === None) + assert(db.getRelayFees(a) == Some(RelayFees(1 msat, 123))) + assert(db.getRelayFees(b) == None) db.addOrUpdateRelayFees(a, RelayFees(2 msat, 456)) - assert(db.getRelayFees(a) === Some(RelayFees(2 msat, 456))) - assert(db.getRelayFees(b) === None) + assert(db.getRelayFees(a) == Some(RelayFees(2 msat, 456))) + assert(db.getRelayFees(b) == None) db.addOrUpdateRelayFees(b, RelayFees(3 msat, 789)) - assert(db.getRelayFees(a) === Some(RelayFees(2 msat, 456))) - assert(db.getRelayFees(b) === Some(RelayFees(3 msat, 789))) + assert(db.getRelayFees(a) == Some(RelayFees(2 msat, 456))) + assert(db.getRelayFees(b) == Some(RelayFees(3 msat, 789))) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/PendingCommandsDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/PendingCommandsDbSpec.scala index e69551879f..d2ec57a89f 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/PendingCommandsDbSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/PendingCommandsDbSpec.scala @@ -59,7 +59,7 @@ class PendingCommandsDbSpec extends AnyFunSuite { val msg3 = CMD_FAIL_HTLC(3, Left(randomBytes32())) val msg4 = CMD_FAIL_MALFORMED_HTLC(4, randomBytes32(), FailureMessageCodecs.BADONION) - assert(db.listSettlementCommands(channelId1).toSet === Set.empty) + assert(db.listSettlementCommands(channelId1).toSet == Set.empty) db.addSettlementCommand(channelId1, msg0) db.addSettlementCommand(channelId1, msg0) // duplicate db.addSettlementCommand(channelId1, msg1) @@ -68,11 +68,11 @@ class PendingCommandsDbSpec extends AnyFunSuite { db.addSettlementCommand(channelId1, msg4) db.addSettlementCommand(channelId2, msg0) // same messages but for different channel db.addSettlementCommand(channelId2, msg1) - assert(db.listSettlementCommands(channelId1).toSet === Set(msg0, msg1, msg2, msg3, msg4)) - assert(db.listSettlementCommands(channelId2).toSet === Set(msg0, msg1)) - assert(db.listSettlementCommands().toSet === Set((channelId1, msg0), (channelId1, msg1), (channelId1, msg2), (channelId1, msg3), (channelId1, msg4), (channelId2, msg0), (channelId2, msg1))) + assert(db.listSettlementCommands(channelId1).toSet == Set(msg0, msg1, msg2, msg3, msg4)) + assert(db.listSettlementCommands(channelId2).toSet == Set(msg0, msg1)) + assert(db.listSettlementCommands().toSet == Set((channelId1, msg0), (channelId1, msg1), (channelId1, msg2), (channelId1, msg3), (channelId1, msg4), (channelId2, msg0), (channelId2, msg1))) db.removeSettlementCommand(channelId1, msg1.id) - assert(db.listSettlementCommands().toSet === Set((channelId1, msg0), (channelId1, msg2), (channelId1, msg3), (channelId1, msg4), (channelId2, msg0), (channelId2, msg1))) + assert(db.listSettlementCommands().toSet == Set((channelId1, msg0), (channelId1, msg2), (channelId1, msg3), (channelId1, msg4), (channelId2, msg0), (channelId2, msg1))) } } @@ -98,7 +98,7 @@ class PendingCommandsDbSpec extends AnyFunSuite { dbName = PgPendingCommandsDb.DB_NAME, targetVersion = PgPendingCommandsDb.CURRENT_VERSION, postCheck = _ => - assert(dbs.pendingCommands.listSettlementCommands().toSet === testCases.map(tc => tc.channelId -> tc.cmd)) + assert(dbs.pendingCommands.listSettlementCommands().toSet == testCases.map(tc => tc.channelId -> tc.cmd)) ) case dbs: TestSqliteDatabases => migrationCheck( @@ -120,7 +120,7 @@ class PendingCommandsDbSpec extends AnyFunSuite { dbName = SqlitePendingCommandsDb.DB_NAME, targetVersion = SqlitePendingCommandsDb.CURRENT_VERSION, postCheck = _ => - assert(dbs.pendingCommands.listSettlementCommands().toSet === testCases.map(tc => tc.channelId -> tc.cmd)) + assert(dbs.pendingCommands.listSettlementCommands().toSet == testCases.map(tc => tc.channelId -> tc.cmd)) ) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/PgUtilsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/PgUtilsSpec.scala index a11283f4a9..a7f8b7f39b 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/PgUtilsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/PgUtilsSpec.scala @@ -38,7 +38,7 @@ class PgUtilsSpec extends TestKitBaseClass with AnyFunSuiteLike with Eventually intercept[LockFailureHandler.LockException] { // this will fail because the database is already locked for a different instance id Databases.postgres(config, UUID.randomUUID(), datadir, None, LockFailureHandler.logAndThrow) - }.lockFailure === LockFailure.AlreadyLocked(instanceId1)) + }.lockFailure == LockFailure.AlreadyLocked(instanceId1)) // we can renew the lease at will db1.obtainExclusiveLock() @@ -49,7 +49,7 @@ class PgUtilsSpec extends TestKitBaseClass with AnyFunSuiteLike with Eventually intercept[LockFailureHandler.LockException] { // this will fail because the database is already locked for a different instance id Databases.postgres(config, UUID.randomUUID(), datadir, None, LockFailureHandler.logAndThrow) - }.lockFailure === LockFailure.AlreadyLocked(instanceId1)) + }.lockFailure == LockFailure.AlreadyLocked(instanceId1)) // we close the first connection db1.dataSource.close() @@ -69,7 +69,7 @@ class PgUtilsSpec extends TestKitBaseClass with AnyFunSuiteLike with Eventually assert(intercept[LockFailureHandler.LockException] { // this will fail because even if we have acquired the table lock, the previous lease still hasn't expired Databases.postgres(config, UUID.randomUUID(), datadir, None, LockFailureHandler.logAndThrow) - }.lockFailure === LockFailure.AlreadyLocked(instanceId2)) + }.lockFailure == LockFailure.AlreadyLocked(instanceId2)) pg.close() } @@ -261,7 +261,7 @@ class PgUtilsSpec extends TestKitBaseClass with AnyFunSuiteLike with Eventually using(pg.getPostgresDatabase.getConnection.createStatement()) { statement => val rs = statement.executeQuery("SELECT bar FROM foo") - assert(rs.map(_.getInt("bar")).toSet === Set(10, 20, 30)) + assert(rs.map(_.getInt("bar")).toSet == Set(10, 20, 30)) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteFeeratesDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteFeeratesDbSpec.scala index 284fec4100..a5dae743d7 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteFeeratesDbSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteFeeratesDbSpec.scala @@ -86,9 +86,9 @@ class SqliteFeeratesDbSpec extends AnyFunSuite { } // When migrating, we simply copy the estimate for blocks 144 to blocks 1008. - assert(migratedDb.getFeerates() === Some(feerate.copy(blocks_1008 = feerate.blocks_144, mempoolMinFee = feerate.blocks_144))) + assert(migratedDb.getFeerates() == Some(feerate.copy(blocks_1008 = feerate.blocks_144, mempoolMinFee = feerate.blocks_144))) migratedDb.addOrUpdateFeerates(feerate) - assert(migratedDb.getFeerates() === Some(feerate)) + assert(migratedDb.getFeerates() == Some(feerate)) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteUtilsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteUtilsSpec.scala index 93ed45f2e2..6af0aea6b0 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteUtilsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteUtilsSpec.scala @@ -40,9 +40,9 @@ class SqliteUtilsSpec extends AnyFunSuite { using(conn.createStatement()) { statement => val results = statement.executeQuery("SELECT * FROM utils_test ORDER BY id") assert(results.next()) - assert(results.getLong("id") === 1) + assert(results.getLong("id") == 1) assert(results.next()) - assert(results.getLong("id") === 2) + assert(results.getLong("id") == 2) assert(!results.next()) } @@ -54,9 +54,9 @@ class SqliteUtilsSpec extends AnyFunSuite { using(conn.createStatement()) { statement => val results = statement.executeQuery("SELECT * FROM utils_test ORDER BY id") assert(results.next()) - assert(results.getLong("id") === 1) + assert(results.getLong("id") == 1) assert(results.next()) - assert(results.getLong("id") === 2) + assert(results.getLong("id") == 2) assert(!results.next()) } @@ -68,13 +68,13 @@ class SqliteUtilsSpec extends AnyFunSuite { using(conn.createStatement()) { statement => val results = statement.executeQuery("SELECT * FROM utils_test ORDER BY id") assert(results.next()) - assert(results.getLong("id") === 1) + assert(results.getLong("id") == 1) assert(results.next()) - assert(results.getLong("id") === 2) + assert(results.getLong("id") == 2) assert(results.next()) - assert(results.getLong("id") === 3) + assert(results.getLong("id") == 3) assert(results.next()) - assert(results.getLong("id") === 4) + assert(results.getLong("id") == 4) assert(!results.next()) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/ChannelIntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/ChannelIntegrationSpec.scala index c04f621937..c778b391ab 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/ChannelIntegrationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/ChannelIntegrationSpec.scala @@ -154,11 +154,11 @@ abstract class ChannelIntegrationSpec extends IntegrationSpec { // now that we have the channel id, we retrieve channels default final addresses sender.send(nodes("C").register, Register.Forward(sender.ref, htlc.channelId, CMD_GET_CHANNEL_DATA(ActorRef.noSender))) val dataC = sender.expectMsgType[RES_GET_CHANNEL_DATA[DATA_NORMAL]].data - assert(dataC.commitments.commitmentFormat === commitmentFormat) + assert(dataC.commitments.commitmentFormat == commitmentFormat) val finalAddressC = scriptPubKeyToAddress(dataC.commitments.localParams.defaultFinalScriptPubKey) sender.send(nodes("F").register, Register.Forward(sender.ref, htlc.channelId, CMD_GET_CHANNEL_DATA(ActorRef.noSender))) val dataF = sender.expectMsgType[RES_GET_CHANNEL_DATA[DATA_NORMAL]].data - assert(dataF.commitments.commitmentFormat === commitmentFormat) + assert(dataF.commitments.commitmentFormat == commitmentFormat) val finalAddressF = scriptPubKeyToAddress(dataF.commitments.localParams.defaultFinalScriptPubKey) ForceCloseFixture(sender, paymentSender, stateListenerC, stateListenerF, paymentId, htlc, preimage, minerAddress, finalAddressC, finalAddressF) } @@ -259,16 +259,16 @@ abstract class ChannelIntegrationSpec extends IntegrationSpec { // we generate a few blocks to get the commit tx confirmed generateBlocks(3, Some(minerAddress)) // we wait until the htlc-timeout has been broadcast - assert(localCommit.htlcTxs.size === 1) + assert(localCommit.htlcTxs.size == 1) waitForOutputSpent(localCommit.htlcTxs.keys.head, bitcoinClient, sender) // we generate more blocks for the htlc-timeout to reach enough confirmations generateBlocks(3, Some(minerAddress)) // this will fail the htlc val failed = paymentSender.expectMsgType[PaymentFailed](max = 60 seconds) assert(failed.id == paymentId) - assert(failed.paymentHash === htlc.paymentHash) + assert(failed.paymentHash == htlc.paymentHash) assert(failed.failures.nonEmpty) - assert(failed.failures.head.asInstanceOf[RemoteFailure].e === DecryptedFailurePacket(nodes("C").nodeParams.nodeId, PermanentChannelFailure)) + assert(failed.failures.head.asInstanceOf[RemoteFailure].e == DecryptedFailurePacket(nodes("C").nodeParams.nodeId, PermanentChannelFailure)) // we then generate enough blocks to confirm all delayed transactions generateBlocks(25, Some(minerAddress)) // C should have 2 recv transactions: its main output and the htlc timeout @@ -311,16 +311,16 @@ abstract class ChannelIntegrationSpec extends IntegrationSpec { generateBlocks((htlc.cltvExpiry.blockHeight - getBlockHeight()).toInt, Some(minerAddress)) // we wait until the claim-htlc-timeout has been broadcast val bitcoinClient = new BitcoinCoreClient(bitcoinrpcclient) - assert(remoteCommit.claimHtlcTxs.size === 1) + assert(remoteCommit.claimHtlcTxs.size == 1) waitForOutputSpent(remoteCommit.claimHtlcTxs.keys.head, bitcoinClient, sender) // and we generate blocks for the claim-htlc-timeout to reach enough confirmations generateBlocks(3, Some(minerAddress)) // this will fail the htlc val failed = paymentSender.expectMsgType[PaymentFailed](max = 60 seconds) assert(failed.id == paymentId) - assert(failed.paymentHash === htlc.paymentHash) + assert(failed.paymentHash == htlc.paymentHash) assert(failed.failures.nonEmpty) - assert(failed.failures.head.asInstanceOf[RemoteFailure].e === DecryptedFailurePacket(nodes("C").nodeParams.nodeId, PermanentChannelFailure)) + assert(failed.failures.head.asInstanceOf[RemoteFailure].e == DecryptedFailurePacket(nodes("C").nodeParams.nodeId, PermanentChannelFailure)) // we then generate enough blocks to confirm all delayed transactions generateBlocks(25, Some(minerAddress)) // C should have 2 recv transactions: its main output and the htlc timeout @@ -391,17 +391,17 @@ abstract class ChannelIntegrationSpec extends IntegrationSpec { forwardHandlerC.forward(buffer.ref) val commitmentsF = sigListener.expectMsgType[ChannelSignatureReceived].commitments sigListener.expectNoMessage(1 second) - assert(commitmentsF.commitmentFormat === commitmentFormat) + assert(commitmentsF.commitmentFormat == commitmentFormat) // in this commitment, both parties should have a main output, there are four pending htlcs and anchor outputs if applicable val localCommitF = commitmentsF.localCommit commitmentFormat match { - case Transactions.DefaultCommitmentFormat => assert(localCommitF.commitTxAndRemoteSig.commitTx.tx.txOut.size === 6) - case _: Transactions.AnchorOutputsCommitmentFormat => assert(localCommitF.commitTxAndRemoteSig.commitTx.tx.txOut.size === 8) + case Transactions.DefaultCommitmentFormat => assert(localCommitF.commitTxAndRemoteSig.commitTx.tx.txOut.size == 6) + case _: Transactions.AnchorOutputsCommitmentFormat => assert(localCommitF.commitTxAndRemoteSig.commitTx.tx.txOut.size == 8) } val htlcTimeoutTxs = localCommitF.htlcTxsAndRemoteSigs.collect { case h@HtlcTxAndRemoteSig(_: Transactions.HtlcTimeoutTx, _) => h } val htlcSuccessTxs = localCommitF.htlcTxsAndRemoteSigs.collect { case h@HtlcTxAndRemoteSig(_: Transactions.HtlcSuccessTx, _) => h } - assert(htlcTimeoutTxs.size === 2) - assert(htlcSuccessTxs.size === 2) + assert(htlcTimeoutTxs.size == 2) + assert(htlcSuccessTxs.size == 2) // we fulfill htlcs to get the preimages buffer.expectMsgType[IncomingPaymentPacket.FinalPacket] buffer.forward(paymentHandlerF) @@ -570,14 +570,14 @@ class StandardChannelIntegrationSpec extends ChannelIntegrationSpec { val bitcoinClient = new BitcoinCoreClient(bitcoinrpcclient) awaitCond({ bitcoinClient.getMempool().pipeTo(sender.ref) - sender.expectMsgType[Seq[Transaction]].exists(_.txIn.head.outPoint.txid === fundingOutpoint.txid) + sender.expectMsgType[Seq[Transaction]].exists(_.txIn.head.outPoint.txid == fundingOutpoint.txid) }, max = 20 seconds, interval = 1 second) generateBlocks(3) awaitCond(stateListener.expectMsgType[ChannelStateChanged](max = 60 seconds).currentState == CLOSED, max = 60 seconds) bitcoinClient.lookForSpendingTx(None, fundingOutpoint.txid, fundingOutpoint.index.toInt).pipeTo(sender.ref) val closingTx = sender.expectMsgType[Transaction] - assert(closingTx.txOut.map(_.publicKeyScript).toSet === Set(finalPubKeyScriptC, finalPubKeyScriptF)) + assert(closingTx.txOut.map(_.publicKeyScript).toSet == Set(finalPubKeyScriptC, finalPubKeyScriptF)) awaitAnnouncements(1) } @@ -645,7 +645,7 @@ abstract class AnchorChannelIntegrationSpec extends ChannelIntegrationSpec { val stateEvent = eventListener.expectMsgType[ChannelStateChanged](max = 60 seconds) if (stateEvent.currentState == NORMAL) { assert(stateEvent.commitments_opt.nonEmpty) - assert(stateEvent.commitments_opt.get.asInstanceOf[Commitments].channelType === expectedChannelType) + assert(stateEvent.commitments_opt.get.asInstanceOf[Commitments].channelType == expectedChannelType) count = count + 1 } } @@ -669,7 +669,7 @@ abstract class AnchorChannelIntegrationSpec extends ChannelIntegrationSpec { sender.send(nodes("F").register, Register.Forward(sender.ref, channelId, CMD_GET_CHANNEL_DATA(ActorRef.noSender))) val initialStateDataF = sender.expectMsgType[RES_GET_CHANNEL_DATA[DATA_NORMAL]].data - assert(initialStateDataF.commitments.channelType === expectedChannelType) + assert(initialStateDataF.commitments.channelType == expectedChannelType) val initialCommitmentIndex = initialStateDataF.commitments.localCommit.index // the 'to remote' address is a simple script spending to the remote payment basepoint with a 1-block CSV delay @@ -688,7 +688,7 @@ abstract class AnchorChannelIntegrationSpec extends ChannelIntegrationSpec { val paymentId = sender.expectMsgType[UUID] val ps = sender.expectMsgType[PaymentSent](60 seconds) assert(ps.id == paymentId) - assert(Crypto.sha256(ps.paymentPreimage) === invoice.paymentHash) + assert(Crypto.sha256(ps.paymentPreimage) == invoice.paymentHash) // we make sure the htlc has been removed from F's commitment before we force-close awaitCond({ @@ -734,7 +734,7 @@ abstract class AnchorChannelIntegrationSpec extends ChannelIntegrationSpec { val mainOutputC = OutPoint(commitTx, commitTx.txOut.indexWhere(_.publicKeyScript == toRemoteOutC.publicKeyScript)) awaitCond({ bitcoinClient.getMempool().pipeTo(sender.ref) - sender.expectMsgType[Seq[Transaction]].exists(_.txIn.head.outPoint === mainOutputC) + sender.expectMsgType[Seq[Transaction]].exists(_.txIn.head.outPoint == mainOutputC) }, max = 20 seconds, interval = 1 second) // get the claim-remote-output confirmed, then the channel can go to the CLOSED state diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/MessageIntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/MessageIntegrationSpec.scala index 39cb766b86..836470bf9b 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/MessageIntegrationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/MessageIntegrationSpec.scala @@ -86,7 +86,7 @@ class MessageIntegrationSpec extends IntegrationSpec { val res = probe.expectMsgType[SendOnionMessageResponse] assert(res.failureMessage.isEmpty) assert(res.response.nonEmpty) - assert(res.response.get.unknownTlvs("29") === hex"ab") + assert(res.response.get.unknownTlvs("29") == hex"ab") } test("reply timeout") { @@ -138,7 +138,7 @@ class MessageIntegrationSpec extends IntegrationSpec { assert(probe.expectMsgType[SendOnionMessageResponse].sent) val r = eventListener.expectMsgType[OnionMessages.ReceiveMessage](max = 60 seconds) - assert(r.finalPayload.records.unknown.toSet === Set(GenericTlv(UInt64(113), hex"010203"), GenericTlv(UInt64(117), hex"0102"))) + assert(r.finalPayload.records.unknown.toSet == Set(GenericTlv(UInt64(113), hex"010203"), GenericTlv(UInt64(117), hex"0102"))) } test("relay with channels-only and missing channel") { @@ -231,7 +231,7 @@ class MessageIntegrationSpec extends IntegrationSpec { assert(probe.expectMsgType[SendOnionMessageResponse].sent) val r = eventListener.expectMsgType[OnionMessages.ReceiveMessage](max = 60 seconds) - assert(r.finalPayload.records.unknown.toSet === Set(GenericTlv(UInt64(113), hex"010203"), GenericTlv(UInt64(117), hex"0102"))) + assert(r.finalPayload.records.unknown.toSet == Set(GenericTlv(UInt64(113), hex"010203"), GenericTlv(UInt64(117), hex"0102"))) } test("channel relay with no-relay") { @@ -266,10 +266,10 @@ class MessageIntegrationSpec extends IntegrationSpec { val probe = TestProbe() probe.send(nodes("B").register, Symbol("channels")) val channelsB = probe.expectMsgType[Map[ByteVector32, ActorRef]] - assert(channelsB.size === 3) + assert(channelsB.size == 3) probe.send(nodes("D").register, Symbol("channels")) val channelsD = probe.expectMsgType[Map[ByteVector32, ActorRef]] - assert(channelsD.size === 3) + assert(channelsD.size == 3) channelsB.foreach { case (channelId, channel) => if (!channelsD.contains(channelId)) { @@ -307,8 +307,8 @@ class MessageIntegrationSpec extends IntegrationSpec { assert(probe.expectMsgType[SendOnionMessageResponse].sent) val r = eventListener.expectMsgType[OnionMessages.ReceiveMessage](max = 60 seconds) - assert(r.pathId === None) - assert(r.finalPayload.records.unknown.toSet === Set(GenericTlv(UInt64(115), hex""))) + assert(r.pathId == None) + assert(r.finalPayload.records.unknown.toSet == Set(GenericTlv(UInt64(115), hex""))) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/PaymentIntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/PaymentIntegrationSpec.scala index ff3b80b4b0..976dfcbbef 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/PaymentIntegrationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/PaymentIntegrationSpec.scala @@ -153,7 +153,7 @@ class PaymentIntegrationSpec extends IntegrationSpec { val routingState = sender.expectMsgType[Router.RoutingState] val publicChannels = routingState.channels.filter(pc => Set(pc.ann.nodeId1, pc.ann.nodeId2).contains(nodeId)) assert(publicChannels.nonEmpty) - publicChannels.foreach(pc => assert(pc.meta_opt.map(m => m.balance1 > 0.msat || m.balance2 > 0.msat) === Some(true), pc)) + publicChannels.foreach(pc => assert(pc.meta_opt.map(m => m.balance1 > 0.msat || m.balance2 > 0.msat) == Some(true), pc)) } test("send an HTLC A->D") { @@ -171,7 +171,7 @@ class PaymentIntegrationSpec extends IntegrationSpec { val paymentId = sender.expectMsgType[UUID] val ps = sender.expectMsgType[PaymentSent] assert(ps.id == paymentId) - assert(Crypto.sha256(ps.paymentPreimage) === invoice.paymentHash) + assert(Crypto.sha256(ps.paymentPreimage) == invoice.paymentHash) eventListener.expectMsg(PaymentMetadataReceived(invoice.paymentHash, invoice.paymentMetadata.get)) } @@ -199,7 +199,7 @@ class PaymentIntegrationSpec extends IntegrationSpec { val paymentId = sender.expectMsgType[UUID] val ps = sender.expectMsgType[PaymentSent] assert(ps.id == paymentId) - assert(Crypto.sha256(ps.paymentPreimage) === invoice.paymentHash) + assert(Crypto.sha256(ps.paymentPreimage) == invoice.paymentHash) def updateFor(n: PublicKey, pc: PublicChannel): Option[ChannelUpdate] = if (n == pc.ann.nodeId1) pc.update_1_opt else if (n == pc.ann.nodeId2) pc.update_2_opt else throw new IllegalArgumentException("this node is unrelated to this channel") @@ -252,9 +252,9 @@ class PaymentIntegrationSpec extends IntegrationSpec { val paymentId = sender.expectMsgType[UUID] val failed = sender.expectMsgType[PaymentFailed] assert(failed.id == paymentId) - assert(failed.paymentHash === invoice.paymentHash) - assert(failed.failures.size === 1) - assert(failed.failures.head.asInstanceOf[RemoteFailure].e === DecryptedFailurePacket(nodes("D").nodeParams.nodeId, IncorrectOrUnknownPaymentDetails(amount, getBlockHeight()))) + assert(failed.paymentHash == invoice.paymentHash) + assert(failed.failures.size == 1) + assert(failed.failures.head.asInstanceOf[RemoteFailure].e == DecryptedFailurePacket(nodes("D").nodeParams.nodeId, IncorrectOrUnknownPaymentDetails(amount, getBlockHeight()))) } test("send an HTLC A->D with a lower amount than requested") { @@ -272,9 +272,9 @@ class PaymentIntegrationSpec extends IntegrationSpec { val paymentId = sender.expectMsgType[UUID] val failed = sender.expectMsgType[PaymentFailed] assert(failed.id == paymentId) - assert(failed.paymentHash === invoice.paymentHash) - assert(failed.failures.size === 1) - assert(failed.failures.head.asInstanceOf[RemoteFailure].e === DecryptedFailurePacket(nodes("D").nodeParams.nodeId, IncorrectOrUnknownPaymentDetails(100000000 msat, getBlockHeight()))) + assert(failed.paymentHash == invoice.paymentHash) + assert(failed.failures.size == 1) + assert(failed.failures.head.asInstanceOf[RemoteFailure].e == DecryptedFailurePacket(nodes("D").nodeParams.nodeId, IncorrectOrUnknownPaymentDetails(100000000 msat, getBlockHeight()))) } test("send an HTLC A->D with too much overpayment") { @@ -292,9 +292,9 @@ class PaymentIntegrationSpec extends IntegrationSpec { val paymentId = sender.expectMsgType[UUID] val failed = sender.expectMsgType[PaymentFailed] assert(paymentId == failed.id) - assert(failed.paymentHash === invoice.paymentHash) - assert(failed.failures.size === 1) - assert(failed.failures.head.asInstanceOf[RemoteFailure].e === DecryptedFailurePacket(nodes("D").nodeParams.nodeId, IncorrectOrUnknownPaymentDetails(600000000 msat, getBlockHeight()))) + assert(failed.paymentHash == invoice.paymentHash) + assert(failed.failures.size == 1) + assert(failed.failures.head.asInstanceOf[RemoteFailure].e == DecryptedFailurePacket(nodes("D").nodeParams.nodeId, IncorrectOrUnknownPaymentDetails(600000000 msat, getBlockHeight()))) } test("send an HTLC A->D with a reasonable overpayment") { @@ -350,30 +350,30 @@ class PaymentIntegrationSpec extends IntegrationSpec { sender.send(nodes("B").paymentInitiator, SendPaymentToNode(amount, invoice, maxAttempts = 5, routeParams = integrationTestRouteParams)) val paymentId = sender.expectMsgType[UUID] val paymentSent = sender.expectMsgType[PaymentSent](max = 30 seconds) - assert(paymentSent.id === paymentId, paymentSent) - assert(paymentSent.paymentHash === invoice.paymentHash, paymentSent) + assert(paymentSent.id == paymentId, paymentSent) + assert(paymentSent.paymentHash == invoice.paymentHash, paymentSent) assert(paymentSent.parts.length > 1, paymentSent) - assert(paymentSent.recipientNodeId === nodes("D").nodeParams.nodeId, paymentSent) - assert(paymentSent.recipientAmount === amount, paymentSent) + assert(paymentSent.recipientNodeId == nodes("D").nodeParams.nodeId, paymentSent) + assert(paymentSent.recipientAmount == amount, paymentSent) assert(paymentSent.feesPaid > 0.msat, paymentSent) assert(paymentSent.parts.forall(p => p.id != paymentSent.id), paymentSent) assert(paymentSent.parts.forall(p => p.route.isDefined), paymentSent) val paymentParts = nodes("B").nodeParams.db.payments.listOutgoingPayments(paymentId).filter(_.status.isInstanceOf[OutgoingPaymentStatus.Succeeded]) assert(paymentParts.length == paymentSent.parts.length, paymentParts) - assert(paymentParts.map(_.amount).sum === amount, paymentParts) + assert(paymentParts.map(_.amount).sum == amount, paymentParts) assert(paymentParts.forall(p => p.parentId == paymentId), paymentParts) assert(paymentParts.forall(p => p.parentId != p.id), paymentParts) assert(paymentParts.forall(p => p.status.asInstanceOf[OutgoingPaymentStatus.Succeeded].feesPaid > 0.msat), paymentParts) awaitCond(nodes("B").nodeParams.db.audit.listSent(start, TimestampMilli.now()).nonEmpty) val sent = nodes("B").nodeParams.db.audit.listSent(start, TimestampMilli.now()) - assert(sent.length === 1, sent) - assert(sent.head.copy(parts = sent.head.parts.sortBy(_.timestamp)) === paymentSent.copy(parts = paymentSent.parts.map(_.copy(route = None)).sortBy(_.timestamp)), sent) + assert(sent.length == 1, sent) + assert(sent.head.copy(parts = sent.head.parts.sortBy(_.timestamp)) == paymentSent.copy(parts = paymentSent.parts.map(_.copy(route = None)).sortBy(_.timestamp)), sent) awaitCond(nodes("D").nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).exists(_.status.isInstanceOf[IncomingPaymentStatus.Received])) val Some(IncomingPayment(_, _, _, _, IncomingPaymentStatus.Received(receivedAmount, _))) = nodes("D").nodeParams.db.payments.getIncomingPayment(invoice.paymentHash) - assert(receivedAmount === amount) + assert(receivedAmount == amount) } // NB: this test may take 20 seconds to complete (multi-part payment timeout). @@ -393,11 +393,11 @@ class PaymentIntegrationSpec extends IntegrationSpec { sender.send(nodes("B").paymentInitiator, SendPaymentToNode(amount, invoice, maxAttempts = 1, routeParams = integrationTestRouteParams)) val paymentId = sender.expectMsgType[UUID] val paymentFailed = sender.expectMsgType[PaymentFailed](max = 30 seconds) - assert(paymentFailed.id === paymentId, paymentFailed) - assert(paymentFailed.paymentHash === invoice.paymentHash, paymentFailed) + assert(paymentFailed.id == paymentId, paymentFailed) + assert(paymentFailed.paymentHash == invoice.paymentHash, paymentFailed) assert(paymentFailed.failures.length > 1, paymentFailed) - assert(nodes("D").nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).get.status === IncomingPaymentStatus.Pending) + assert(nodes("D").nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).get.status == IncomingPaymentStatus.Pending) sender.send(nodes("B").relayer, Relayer.GetOutgoingChannels()) val canSend2 = sender.expectMsgType[Relayer.OutgoingChannels].channels.map(_.commitments.availableBalanceForSend).sum @@ -416,21 +416,21 @@ class PaymentIntegrationSpec extends IntegrationSpec { sender.send(nodes("D").paymentInitiator, SendPaymentToNode(amount, invoice, maxAttempts = 3, routeParams = integrationTestRouteParams)) val paymentId = sender.expectMsgType[UUID] val paymentSent = sender.expectMsgType[PaymentSent](max = 30 seconds) - assert(paymentSent.id === paymentId, paymentSent) - assert(paymentSent.paymentHash === invoice.paymentHash, paymentSent) + assert(paymentSent.id == paymentId, paymentSent) + assert(paymentSent.paymentHash == invoice.paymentHash, paymentSent) assert(paymentSent.parts.length > 1, paymentSent) - assert(paymentSent.recipientAmount === amount, paymentSent) - assert(paymentSent.feesPaid === 0.msat, paymentSent) // no fees when using direct channels + assert(paymentSent.recipientAmount == amount, paymentSent) + assert(paymentSent.feesPaid == 0.msat, paymentSent) // no fees when using direct channels val paymentParts = nodes("D").nodeParams.db.payments.listOutgoingPayments(paymentId).filter(_.status.isInstanceOf[OutgoingPaymentStatus.Succeeded]) - assert(paymentParts.map(_.amount).sum === amount, paymentParts) + assert(paymentParts.map(_.amount).sum == amount, paymentParts) assert(paymentParts.forall(p => p.parentId == paymentId), paymentParts) assert(paymentParts.forall(p => p.parentId != p.id), paymentParts) assert(paymentParts.forall(p => p.status.asInstanceOf[OutgoingPaymentStatus.Succeeded].feesPaid == 0.msat), paymentParts) awaitCond(nodes("C").nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).exists(_.status.isInstanceOf[IncomingPaymentStatus.Received])) val Some(IncomingPayment(_, _, _, _, IncomingPaymentStatus.Received(receivedAmount, _))) = nodes("C").nodeParams.db.payments.getIncomingPayment(invoice.paymentHash) - assert(receivedAmount === amount) + assert(receivedAmount == amount) } test("send a multi-part payment D->C greater than balance D->C (temporary local failure)") { @@ -448,11 +448,11 @@ class PaymentIntegrationSpec extends IntegrationSpec { sender.send(nodes("D").paymentInitiator, SendPaymentToNode(amount, invoice, maxAttempts = 1, routeParams = integrationTestRouteParams)) val paymentId = sender.expectMsgType[UUID] val paymentFailed = sender.expectMsgType[PaymentFailed](max = 30 seconds) - assert(paymentFailed.id === paymentId, paymentFailed) - assert(paymentFailed.paymentHash === invoice.paymentHash, paymentFailed) + assert(paymentFailed.id == paymentId, paymentFailed) + assert(paymentFailed.paymentHash == invoice.paymentHash, paymentFailed) val incoming = nodes("C").nodeParams.db.payments.getIncomingPayment(invoice.paymentHash) - assert(incoming.get.status === IncomingPaymentStatus.Pending, incoming) + assert(incoming.get.status == IncomingPaymentStatus.Pending, incoming) sender.send(nodes("D").relayer, Relayer.GetOutgoingChannels()) val canSend2 = sender.expectMsgType[Relayer.OutgoingChannels].channels.map(_.commitments.availableBalanceForSend).sum @@ -477,16 +477,16 @@ class PaymentIntegrationSpec extends IntegrationSpec { sender.send(nodes("B").paymentInitiator, payment) val paymentId = sender.expectMsgType[UUID] val paymentSent = sender.expectMsgType[PaymentSent](max = 30 seconds) - assert(paymentSent.id === paymentId, paymentSent) - assert(paymentSent.paymentHash === invoice.paymentHash, paymentSent) - assert(paymentSent.recipientNodeId === nodes("F").nodeParams.nodeId, paymentSent) - assert(paymentSent.recipientAmount === amount, paymentSent) - assert(paymentSent.feesPaid === 1210100.msat, paymentSent) - assert(paymentSent.nonTrampolineFees === 0.msat, paymentSent) + assert(paymentSent.id == paymentId, paymentSent) + assert(paymentSent.paymentHash == invoice.paymentHash, paymentSent) + assert(paymentSent.recipientNodeId == nodes("F").nodeParams.nodeId, paymentSent) + assert(paymentSent.recipientAmount == amount, paymentSent) + assert(paymentSent.feesPaid == 1210100.msat, paymentSent) + assert(paymentSent.nonTrampolineFees == 0.msat, paymentSent) awaitCond(nodes("F").nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).exists(_.status.isInstanceOf[IncomingPaymentStatus.Received])) val Some(IncomingPayment(_, _, _, _, IncomingPaymentStatus.Received(receivedAmount, _))) = nodes("F").nodeParams.db.payments.getIncomingPayment(invoice.paymentHash) - assert(receivedAmount === amount) + assert(receivedAmount == amount) awaitCond({ val relayed = nodes("G").nodeParams.db.audit.listRelayed(start, TimestampMilli.now()).filter(_.paymentHash == invoice.paymentHash) @@ -498,10 +498,10 @@ class PaymentIntegrationSpec extends IntegrationSpec { val outgoingSuccess = nodes("B").nodeParams.db.payments.listOutgoingPayments(paymentId).filter(p => p.status.isInstanceOf[OutgoingPaymentStatus.Succeeded]) outgoingSuccess.collect { case p@OutgoingPayment(_, _, _, _, _, _, _, recipientNodeId, _, _, OutgoingPaymentStatus.Succeeded(_, _, route, _)) => - assert(recipientNodeId === nodes("F").nodeParams.nodeId, p) - assert(route.lastOption === Some(HopSummary(nodes("G").nodeParams.nodeId, nodes("F").nodeParams.nodeId)), p) + assert(recipientNodeId == nodes("F").nodeParams.nodeId, p) + assert(route.lastOption == Some(HopSummary(nodes("G").nodeParams.nodeId, nodes("F").nodeParams.nodeId)), p) } - assert(outgoingSuccess.map(_.amount).sum === amount + 1210100.msat, outgoingSuccess) + assert(outgoingSuccess.map(_.amount).sum == amount + 1210100.msat, outgoingSuccess) } test("send a trampoline payment D->B (via trampoline C)") { @@ -523,15 +523,15 @@ class PaymentIntegrationSpec extends IntegrationSpec { sender.send(nodes("D").paymentInitiator, payment) val paymentId = sender.expectMsgType[UUID] val paymentSent = sender.expectMsgType[PaymentSent](max = 30 seconds) - assert(paymentSent.id === paymentId, paymentSent) - assert(paymentSent.paymentHash === invoice.paymentHash, paymentSent) - assert(paymentSent.recipientAmount === amount, paymentSent) - assert(paymentSent.feesPaid === 750000.msat, paymentSent) - assert(paymentSent.nonTrampolineFees === 0.msat, paymentSent) + assert(paymentSent.id == paymentId, paymentSent) + assert(paymentSent.paymentHash == invoice.paymentHash, paymentSent) + assert(paymentSent.recipientAmount == amount, paymentSent) + assert(paymentSent.feesPaid == 750000.msat, paymentSent) + assert(paymentSent.nonTrampolineFees == 0.msat, paymentSent) awaitCond(nodes("B").nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).exists(_.status.isInstanceOf[IncomingPaymentStatus.Received])) val Some(IncomingPayment(_, _, _, _, IncomingPaymentStatus.Received(receivedAmount, _))) = nodes("B").nodeParams.db.payments.getIncomingPayment(invoice.paymentHash) - assert(receivedAmount === amount) + assert(receivedAmount == amount) eventListener.expectMsg(PaymentMetadataReceived(invoice.paymentHash, invoice.paymentMetadata.get)) awaitCond({ @@ -544,15 +544,15 @@ class PaymentIntegrationSpec extends IntegrationSpec { val outgoingSuccess = nodes("D").nodeParams.db.payments.listOutgoingPayments(paymentId).filter(p => p.status.isInstanceOf[OutgoingPaymentStatus.Succeeded]) outgoingSuccess.collect { case p@OutgoingPayment(_, _, _, _, _, _, _, recipientNodeId, _, _, OutgoingPaymentStatus.Succeeded(_, _, route, _)) => - assert(recipientNodeId === nodes("B").nodeParams.nodeId, p) - assert(route.lastOption === Some(HopSummary(nodes("C").nodeParams.nodeId, nodes("B").nodeParams.nodeId)), p) + assert(recipientNodeId == nodes("B").nodeParams.nodeId, p) + assert(route.lastOption == Some(HopSummary(nodes("C").nodeParams.nodeId, nodes("B").nodeParams.nodeId)), p) } - assert(outgoingSuccess.map(_.amount).sum === amount + 750000.msat, outgoingSuccess) + assert(outgoingSuccess.map(_.amount).sum == amount + 750000.msat, outgoingSuccess) awaitCond(nodes("D").nodeParams.db.audit.listSent(start, TimestampMilli.now()).nonEmpty) val sent = nodes("D").nodeParams.db.audit.listSent(start, TimestampMilli.now()) - assert(sent.length === 1, sent) - assert(sent.head.copy(parts = sent.head.parts.sortBy(_.timestamp)) === paymentSent.copy(parts = paymentSent.parts.map(_.copy(route = None)).sortBy(_.timestamp)), sent) + assert(sent.length == 1, sent) + assert(sent.head.copy(parts = sent.head.parts.sortBy(_.timestamp)) == paymentSent.copy(parts = paymentSent.parts.map(_.copy(route = None)).sortBy(_.timestamp)), sent) } test("send a trampoline payment F1->A (via trampoline C, non-trampoline recipient)") { @@ -575,14 +575,14 @@ class PaymentIntegrationSpec extends IntegrationSpec { sender.send(nodes("F").paymentInitiator, payment) val paymentId = sender.expectMsgType[UUID] val paymentSent = sender.expectMsgType[PaymentSent](max = 30 seconds) - assert(paymentSent.id === paymentId, paymentSent) - assert(paymentSent.paymentHash === invoice.paymentHash, paymentSent) - assert(paymentSent.recipientAmount === amount, paymentSent) - assert(paymentSent.trampolineFees === 1500000.msat, paymentSent) + assert(paymentSent.id == paymentId, paymentSent) + assert(paymentSent.paymentHash == invoice.paymentHash, paymentSent) + assert(paymentSent.recipientAmount == amount, paymentSent) + assert(paymentSent.trampolineFees == 1500000.msat, paymentSent) awaitCond(nodes("A").nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).exists(_.status.isInstanceOf[IncomingPaymentStatus.Received])) val Some(IncomingPayment(_, _, _, _, IncomingPaymentStatus.Received(receivedAmount, _))) = nodes("A").nodeParams.db.payments.getIncomingPayment(invoice.paymentHash) - assert(receivedAmount === amount) + assert(receivedAmount == amount) eventListener.expectMsg(PaymentMetadataReceived(invoice.paymentHash, invoice.paymentMetadata.get)) awaitCond({ @@ -595,10 +595,10 @@ class PaymentIntegrationSpec extends IntegrationSpec { val outgoingSuccess = nodes("F").nodeParams.db.payments.listOutgoingPayments(paymentId).filter(p => p.status.isInstanceOf[OutgoingPaymentStatus.Succeeded]) outgoingSuccess.collect { case p@OutgoingPayment(_, _, _, _, _, _, _, recipientNodeId, _, _, OutgoingPaymentStatus.Succeeded(_, _, route, _)) => - assert(recipientNodeId === nodes("A").nodeParams.nodeId, p) - assert(route.lastOption === Some(HopSummary(nodes("C").nodeParams.nodeId, nodes("A").nodeParams.nodeId)), p) + assert(recipientNodeId == nodes("A").nodeParams.nodeId, p) + assert(route.lastOption == Some(HopSummary(nodes("C").nodeParams.nodeId, nodes("A").nodeParams.nodeId)), p) } - assert(outgoingSuccess.map(_.amount).sum === amount + 1500000.msat, outgoingSuccess) + assert(outgoingSuccess.map(_.amount).sum == amount + 1500000.msat, outgoingSuccess) } test("send a trampoline payment B->D (temporary local failure at trampoline)") { @@ -622,10 +622,10 @@ class PaymentIntegrationSpec extends IntegrationSpec { sender.send(nodes("B").paymentInitiator, payment) val paymentId = sender.expectMsgType[UUID] val paymentFailed = sender.expectMsgType[PaymentFailed](max = 30 seconds) - assert(paymentFailed.id === paymentId, paymentFailed) - assert(paymentFailed.paymentHash === invoice.paymentHash, paymentFailed) + assert(paymentFailed.id == paymentId, paymentFailed) + assert(paymentFailed.paymentHash == invoice.paymentHash, paymentFailed) - assert(nodes("D").nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).get.status === IncomingPaymentStatus.Pending) + assert(nodes("D").nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).get.status == IncomingPaymentStatus.Pending) val outgoingPayments = nodes("B").nodeParams.db.payments.listOutgoingPayments(paymentId) assert(outgoingPayments.nonEmpty, outgoingPayments) assert(outgoingPayments.forall(p => p.status.isInstanceOf[OutgoingPaymentStatus.Failed]), outgoingPayments) @@ -643,10 +643,10 @@ class PaymentIntegrationSpec extends IntegrationSpec { sender.send(nodes("A").paymentInitiator, payment) val paymentId = sender.expectMsgType[UUID] val paymentFailed = sender.expectMsgType[PaymentFailed](max = 30 seconds) - assert(paymentFailed.id === paymentId, paymentFailed) - assert(paymentFailed.paymentHash === invoice.paymentHash, paymentFailed) + assert(paymentFailed.id == paymentId, paymentFailed) + assert(paymentFailed.paymentHash == invoice.paymentHash, paymentFailed) - assert(nodes("D").nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).get.status === IncomingPaymentStatus.Pending) + assert(nodes("D").nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).get.status == IncomingPaymentStatus.Pending) val outgoingPayments = nodes("A").nodeParams.db.payments.listOutgoingPayments(paymentId) assert(outgoingPayments.nonEmpty, outgoingPayments) assert(outgoingPayments.forall(p => p.status.isInstanceOf[OutgoingPaymentStatus.Failed]), outgoingPayments) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/StartupIntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/StartupIntegrationSpec.scala index 1aeddcad3f..2d13c8b4c5 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/StartupIntegrationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/StartupIntegrationSpec.scala @@ -41,7 +41,7 @@ class StartupIntegrationSpec extends IntegrationSpec { val thrown = intercept[BitcoinDefaultWalletException] { instantiateEclairNode("C", ConfigFactory.parseMap(Map("eclair.bitcoind.wallet" -> "", "eclair.server.port" -> TestUtils.availablePort).asJava).withFallback(withDefaultCommitment).withFallback(commonConfig)) } - assert(thrown === BitcoinDefaultWalletException(List(defaultWallet, ""))) + assert(thrown == BitcoinDefaultWalletException(List(defaultWallet, ""))) } test("explicit bitcoind wallet configured and two wallets loaded") { @@ -58,7 +58,7 @@ class StartupIntegrationSpec extends IntegrationSpec { val thrown = intercept[BitcoinWalletNotLoadedException] { instantiateEclairNode("E", ConfigFactory.parseMap(Map("eclair.bitcoind.wallet" -> "notloaded", "eclair.server.port" -> TestUtils.availablePort).asJava).withFallback(withDefaultCommitment).withFallback(commonConfig)) } - assert(thrown === BitcoinWalletNotLoadedException("notloaded", List(defaultWallet, ""))) + assert(thrown == BitcoinWalletNotLoadedException("notloaded", List(defaultWallet, ""))) } test("bitcoind started with wallets disabled") { @@ -66,6 +66,6 @@ class StartupIntegrationSpec extends IntegrationSpec { val thrown = intercept[BitcoinWalletDisabledException] { instantiateEclairNode("F", ConfigFactory.load().getConfig("eclair").withFallback(withDefaultCommitment).withFallback(commonConfig)) } - assert(thrown === BitcoinWalletDisabledException(e = JsonRPCError(Error(-32601, "Method not found")))) + assert(thrown == BitcoinWalletDisabledException(e = JsonRPCError(Error(-32601, "Method not found")))) } } \ No newline at end of file diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/ThreeNodesIntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/ThreeNodesIntegrationSpec.scala new file mode 100644 index 0000000000..f6c2eedcc4 --- /dev/null +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/ThreeNodesIntegrationSpec.scala @@ -0,0 +1,73 @@ +package fr.acinq.eclair.integration.basic + +import fr.acinq.bitcoin.scalacompat.{ByteVector32, SatoshiLong} +import fr.acinq.eclair.ShortChannelId.txIndex +import fr.acinq.eclair.integration.basic.fixtures.ThreeNodesFixture +import fr.acinq.eclair.testutils.FixtureSpec +import fr.acinq.eclair.{BlockHeight, MilliSatoshiLong} +import org.scalatest.TestData +import org.scalatest.concurrent.IntegrationPatience +import scodec.bits.HexStringSyntax + + +/** + * This test checks the integration between Channel and Router (events, etc.) + */ +class ThreeNodesIntegrationSpec extends FixtureSpec with IntegrationPatience { + + type FixtureParam = ThreeNodesFixture + + import fr.acinq.eclair.integration.basic.fixtures.MinimalNodeFixture._ + + 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")) + val bobParams = nodeParamsFor("bob", ByteVector32(hex"7620226fec887b0b2ebe76492e5a3fd3eb0e47cd3773263f6a81b59a704dc492")) + val carolParams = nodeParamsFor("carol", ByteVector32(hex"ebd5a5d3abfb3ef73731eb3418d918f247445183180522674666db98a66411cc")) + ThreeNodesFixture(aliceParams, bobParams, carolParams) + } + + override def cleanupFixture(fixture: FixtureParam): Unit = { + fixture.cleanup() + } + + test("connect alice->bob and bob->carol, pay alice->carol") { f => + import f._ + connect(alice, bob) + connect(bob, carol) + + val channelIdAB = openChannel(alice, bob, 100_000 sat).channelId + val channelIdBC = openChannel(bob, carol, 100_000 sat).channelId + + val fundingTxAB = fundingTx(alice, channelIdAB) + val fundingTxBC = fundingTx(bob, channelIdBC) + + val shortIdAB = confirmChannel(alice, bob, channelIdAB, BlockHeight(420_000), 21) + val shortIdBC = confirmChannel(bob, carol, channelIdBC, BlockHeight(420_001), 22) + + val fundingTxs = Map( + shortIdAB -> fundingTxAB, + shortIdBC -> fundingTxBC + ) + + // auto-validate channel announcements + alice.watcher.setAutoPilot(autoValidatePublicChannels(fundingTxs)) + bob.watcher.setAutoPilot(autoValidatePublicChannels(fundingTxs)) + + confirmChannelDeep(alice, bob, channelIdAB, shortIdAB.blockHeight, txIndex(shortIdAB)) + confirmChannelDeep(bob, carol, channelIdBC, shortIdBC.blockHeight, txIndex(shortIdBC)) + + // alice now knows about bob-carol + eventually { + val routerData = getRouterData(alice) + //prettyPrint(routerData, alice, bob, carol) + assert(routerData.channels.size == 2) // 2 channels + assert(routerData.channels.values.flatMap(c => c.update_1_opt.toSeq ++ c.update_2_opt.toSeq).size == 3) // only 3 channel_updates because c->b is disabled (all funds on b) + } + + sendPayment(alice, carol, 100_000 msat) + } + +} + + diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/TwoNodesIntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/TwoNodesIntegrationSpec.scala new file mode 100644 index 0000000000..cbeb0e4fa6 --- /dev/null +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/TwoNodesIntegrationSpec.scala @@ -0,0 +1,96 @@ +package fr.acinq.eclair.integration.basic + +import fr.acinq.bitcoin.scalacompat.{ByteVector32, SatoshiLong} +import fr.acinq.eclair.channel.{DATA_NORMAL, NORMAL} +import fr.acinq.eclair.integration.basic.fixtures.TwoNodesFixture +import fr.acinq.eclair.testutils.FixtureSpec +import fr.acinq.eclair.{BlockHeight, MilliSatoshiLong} +import org.scalatest.TestData +import org.scalatest.concurrent.IntegrationPatience +import scodec.bits.HexStringSyntax + + +/** + * This test checks the integration between Channel and Router (events, etc.) + */ +class TwoNodesIntegrationSpec extends FixtureSpec with IntegrationPatience { + + type FixtureParam = TwoNodesFixture + + import fr.acinq.eclair.integration.basic.fixtures.MinimalNodeFixture._ + + 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")) + val bobParams = nodeParamsFor("bob", ByteVector32(hex"7620226fec887b0b2ebe76492e5a3fd3eb0e47cd3773263f6a81b59a704dc492")) + TwoNodesFixture(aliceParams, bobParams) + } + + override def cleanupFixture(fixture: FixtureParam): Unit = { + fixture.cleanup() + } + + test("connect alice to bob") { f => + import f._ + connect(alice, bob) + } + + test("open a channel alice-bob") { f => + import f._ + connect(alice, bob) + val channelId = openChannel(alice, bob, 100_000 sat).channelId + confirmChannel(alice, bob, channelId, BlockHeight(420_000), 21) + assert(getChannelState(alice, channelId) == NORMAL) + assert(getChannelState(bob, channelId) == NORMAL) + } + + test("open a channel alice-bob (autoconfirm)") { f => + import f._ + alice.watcher.setAutoPilot(autoConfirmLocalChannels(alice.wallet.funded)) + bob.watcher.setAutoPilot(autoConfirmLocalChannels(alice.wallet.funded)) + connect(alice, bob) + val channelId = openChannel(alice, bob, 100_000 sat).channelId + eventually { + assert(getChannelState(alice, channelId) == NORMAL) + assert(getChannelState(bob, channelId) == NORMAL) + } + } + + test("open a channel alice-bob and confirm deeply") { f => + import f._ + connect(alice, bob) + val channelId = openChannel(alice, bob, 100_000 sat).channelId + confirmChannel(alice, bob, channelId, BlockHeight(420_000), 21) + confirmChannelDeep(alice, bob, channelId, BlockHeight(420_000), 21) + assert(getChannelData(alice, channelId).asInstanceOf[DATA_NORMAL].buried) + assert(getChannelData(bob, channelId).asInstanceOf[DATA_NORMAL].buried) + } + + test("open a channel alice-bob and confirm deeply (autoconfirm)") { f => + import f._ + alice.watcher.setAutoPilot(autoConfirmLocalChannels(alice.wallet.funded)) + bob.watcher.setAutoPilot(autoConfirmLocalChannels(alice.wallet.funded)) + connect(alice, bob) + val channelId = openChannel(alice, bob, 100_000 sat).channelId + eventually { + assert(getChannelData(alice, channelId).asInstanceOf[DATA_NORMAL].buried) + assert(getChannelData(bob, channelId).asInstanceOf[DATA_NORMAL].buried) + } + } + + test("open a channel alice-bob and make a payment alice->bob") { f => + import f._ + connect(alice, bob) + val channelId = openChannel(alice, bob, 100_000 sat).channelId + confirmChannel(alice, bob, channelId, BlockHeight(420_000), 21) + + eventually { + getRouterData(alice).privateChannels.size == 1 + } + + sendPayment(alice, bob, 10_000 msat) + } + +} + + diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/fixtures/MinimalNodeFixture.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/fixtures/MinimalNodeFixture.scala new file mode 100644 index 0000000000..3316c5cb3d --- /dev/null +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/fixtures/MinimalNodeFixture.scala @@ -0,0 +1,271 @@ +package fr.acinq.eclair.integration.basic.fixtures + +import akka.actor.typed.scaladsl.adapter.ClassicActorRefOps +import akka.actor.{ActorRef, ActorSystem, Props} +import akka.testkit.{TestActor, TestProbe} +import com.softwaremill.quicklens.ModifyPimp +import com.typesafe.config.ConfigFactory +import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, Satoshi, Transaction} +import fr.acinq.eclair.blockchain.DummyOnChainWallet +import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher +import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.{WatchFundingConfirmed, WatchFundingConfirmedTriggered, WatchFundingDeeplyBuried, WatchFundingDeeplyBuriedTriggered} +import fr.acinq.eclair.channel.ChannelOpenResponse.ChannelOpened +import fr.acinq.eclair.channel._ +import fr.acinq.eclair.channel.fsm.Channel +import fr.acinq.eclair.crypto.TransportHandler +import fr.acinq.eclair.crypto.keymanager.{LocalChannelKeyManager, LocalNodeKeyManager} +import fr.acinq.eclair.io.PeerConnection.ConnectionResult +import fr.acinq.eclair.io.{Peer, PeerConnection, Switchboard} +import fr.acinq.eclair.payment.receive.{MultiPartHandler, PaymentHandler} +import fr.acinq.eclair.payment.relay.Relayer +import fr.acinq.eclair.payment.send.PaymentInitiator +import fr.acinq.eclair.payment.{Bolt11Invoice, PaymentSent} +import fr.acinq.eclair.router.Router +import fr.acinq.eclair.wire.protocol.IPAddress +import fr.acinq.eclair.{BlockHeight, MilliSatoshi, MilliSatoshiLong, NodeParams, ShortChannelId, TestBitcoinCoreClient, TestDatabases, TestFeeEstimator} +import org.scalatest.Assertions + +import java.net.InetAddress +import java.util.UUID +import java.util.concurrent.atomic.AtomicLong +import scala.concurrent.duration.DurationInt +import scala.util.{Random, Try} + + +/** + * A minimal node setup, with real actors. + * + * Only the bitcoin watcher and wallet are mocked. + */ +case class MinimalNodeFixture private(nodeParams: NodeParams, + system: ActorSystem, + register: ActorRef, + router: ActorRef, + relayer: ActorRef, + switchboard: ActorRef, + paymentInitiator: ActorRef, + paymentHandler: ActorRef, + watcher: TestProbe, + wallet: DummyOnChainWallet) + +object MinimalNodeFixture extends Assertions { + + def nodeParamsFor(alias: String, seed: ByteVector32): NodeParams = { + NodeParams.makeNodeParams( + config = ConfigFactory.load().getConfig("eclair"), + instanceId = UUID.randomUUID(), + nodeKeyManager = new LocalNodeKeyManager(seed, Block.RegtestGenesisBlock.hash), + channelKeyManager = new LocalChannelKeyManager(seed, Block.RegtestGenesisBlock.hash), + torAddress_opt = None, + database = TestDatabases.inMemoryDb(), + blockHeight = new AtomicLong(400_000), + feeEstimator = new TestFeeEstimator + ).modify(_.alias).setTo(alias) + .modify(_.chainHash).setTo(Block.RegtestGenesisBlock.hash) + .modify(_.routerConf.routerBroadcastInterval).setTo(1 second) + .modify(_.peerConnectionConf.maxRebroadcastDelay).setTo(1 second) + } + + def apply(nodeParams: NodeParams): MinimalNodeFixture = { + implicit val system: ActorSystem = ActorSystem(s"system-${nodeParams.alias}") + val bitcoinClient = new TestBitcoinCoreClient() + val wallet = new DummyOnChainWallet() + val watcher = TestProbe("watcher") + val watcherTyped = watcher.ref.toTyped[ZmqWatcher.Command] + val register = system.actorOf(Props(new Register), "register") + val router = system.actorOf(Router.props(nodeParams, watcherTyped), "router") + val paymentHandler = system.actorOf(PaymentHandler.props(nodeParams, register), "payment-handler") + val relayer = system.actorOf(Relayer.props(nodeParams, router, register, paymentHandler), "relayer") + val txPublisherFactory = Channel.SimpleTxPublisherFactory(nodeParams, watcherTyped, bitcoinClient) + val channelFactory = Peer.SimpleChannelFactory(nodeParams, watcherTyped, relayer, wallet, txPublisherFactory) + val peerFactory = Switchboard.SimplePeerFactory(nodeParams, wallet, channelFactory) + val switchboard = system.actorOf(Switchboard.props(nodeParams, peerFactory), "switchboard") + val paymentFactory = PaymentInitiator.SimplePaymentFactory(nodeParams, router, register) + val paymentInitiator = system.actorOf(PaymentInitiator.props(nodeParams, paymentFactory), "payment-initiator") + MinimalNodeFixture( + nodeParams, + system, + register = register, + router = router, + relayer = relayer, + switchboard = switchboard, + paymentInitiator = paymentInitiator, + paymentHandler = paymentHandler, + watcher = watcher, + wallet = wallet + ) + } + + def connect(node1: MinimalNodeFixture, node2: MinimalNodeFixture)(implicit system: ActorSystem): ConnectionResult.Connected = { + val sender = TestProbe("sender") + + val connection1 = TestProbe("connection-1")(node1.system) + val transport1 = TestProbe("transport-1")(node1.system) + + val connection2 = TestProbe("connection-2")(node2.system) + val transport2 = TestProbe("transport-2")(node2.system) + + val peerConnection1 = node1.system.actorOf(PeerConnection.props(node1.nodeParams.keyPair, node1.nodeParams.peerConnectionConf, node1.switchboard, node1.router), s"peer-connection-${Random.nextLong()}") + val peerConnection2 = node2.system.actorOf(PeerConnection.props(node2.nodeParams.keyPair, node2.nodeParams.peerConnectionConf, node2.switchboard, node2.router), s"peer-connection-${Random.nextLong()}") + + transport1.setAutoPilot { (_: ActorRef, msg: Any) => + msg match { + case _: TransportHandler.Listener => TestActor.KeepRunning + case _: TransportHandler.ReadAck => TestActor.KeepRunning + case _ => + peerConnection2.tell(msg, transport2.ref) + TestActor.KeepRunning + } + } + + transport2.setAutoPilot { (_: ActorRef, msg: Any) => + msg match { + case _: TransportHandler.Listener => TestActor.KeepRunning + case _: TransportHandler.ReadAck => TestActor.KeepRunning + case _ => + peerConnection1.tell(msg, transport1.ref) + TestActor.KeepRunning + } + } + + val pendingAuth1 = PeerConnection.PendingAuth(connection1.ref, Some(node2.nodeParams.nodeId), IPAddress(InetAddress.getLoopbackAddress, 65432), origin_opt = Some(sender.ref), transport_opt = Some(transport1.ref), isPersistent = false) + peerConnection1 ! pendingAuth1 + + val pendingAuth2 = PeerConnection.PendingAuth(connection2.ref, None, IPAddress(InetAddress.getLoopbackAddress, 65432), origin_opt = None, transport_opt = Some(transport2.ref), isPersistent = false) + peerConnection2 ! pendingAuth2 + + peerConnection1 ! TransportHandler.HandshakeCompleted(node2.nodeParams.nodeId) + peerConnection2 ! TransportHandler.HandshakeCompleted(node1.nodeParams.nodeId) + + sender.expectMsgType[ConnectionResult.Connected] + } + + def openChannel(node1: MinimalNodeFixture, node2: MinimalNodeFixture, funding: Satoshi)(implicit system: ActorSystem): ChannelOpened = { + val sender = TestProbe("sender") + sender.send(node1.switchboard, Peer.OpenChannel(node2.nodeParams.nodeId, funding, 0L msat, None, None, None, None)) + sender.expectMsgType[ChannelOpened] + } + + def fundingTx(node: MinimalNodeFixture, channelId: ByteVector32)(implicit system: ActorSystem): Transaction = { + val fundingTxid = getChannelData(node, channelId).asInstanceOf[PersistentChannelData].commitments.commitInput.outPoint.txid + node.wallet.funded(fundingTxid) + } + + def confirmChannel(node1: MinimalNodeFixture, node2: MinimalNodeFixture, channelId: ByteVector32, blockHeight: BlockHeight, txIndex: Int)(implicit system: ActorSystem): ShortChannelId = { + assert(getChannelState(node1, channelId) == WAIT_FOR_FUNDING_CONFIRMED) + val data1Before = getChannelData(node1, channelId).asInstanceOf[DATA_WAIT_FOR_FUNDING_CONFIRMED] + val fundingTx = data1Before.fundingTx.get + + val watch1 = node1.watcher.fishForMessage() { case w: WatchFundingConfirmed if w.txId == fundingTx.txid => true; case _ => false }.asInstanceOf[WatchFundingConfirmed] + val watch2 = node2.watcher.fishForMessage() { case w: WatchFundingConfirmed if w.txId == fundingTx.txid => true; case _ => false }.asInstanceOf[WatchFundingConfirmed] + + watch1.replyTo ! WatchFundingConfirmedTriggered(blockHeight, txIndex, fundingTx) + watch2.replyTo ! WatchFundingConfirmedTriggered(blockHeight, txIndex, fundingTx) + + waitReady(node1, channelId) + waitReady(node2, channelId) + + val data1After = getChannelData(node1, channelId).asInstanceOf[DATA_NORMAL] + val data2After = getChannelData(node2, channelId).asInstanceOf[DATA_NORMAL] + assert(data1After.shortChannelId == data2After.shortChannelId) + assert(!data1After.buried && !data2After.buried) + + data1After.shortChannelId + } + + def confirmChannelDeep(node1: MinimalNodeFixture, node2: MinimalNodeFixture, channelId: ByteVector32, blockHeight: BlockHeight, txIndex: Int)(implicit system: ActorSystem): ShortChannelId = { + assert(getChannelState(node1, channelId) == NORMAL) + val data1Before = getChannelData(node1, channelId).asInstanceOf[DATA_NORMAL] + val fundingTxid = data1Before.commitments.commitInput.outPoint.txid + val fundingTx = node1.wallet.funded(fundingTxid) + + val watch1 = node1.watcher.fishForMessage() { case w: WatchFundingDeeplyBuried if w.txId == fundingTx.txid => true; case _ => false }.asInstanceOf[WatchFundingDeeplyBuried] + val watch2 = node2.watcher.fishForMessage() { case w: WatchFundingDeeplyBuried if w.txId == fundingTx.txid => true; case _ => false }.asInstanceOf[WatchFundingDeeplyBuried] + + watch1.replyTo ! WatchFundingDeeplyBuriedTriggered(blockHeight, txIndex, fundingTx) + watch2.replyTo ! WatchFundingDeeplyBuriedTriggered(blockHeight, txIndex, fundingTx) + + waitReady(node1, channelId) + waitReady(node2, channelId) + + val data1After = getChannelData(node1, channelId).asInstanceOf[DATA_NORMAL] + val data2After = getChannelData(node2, channelId).asInstanceOf[DATA_NORMAL] + assert(data1After.shortChannelId == data2After.shortChannelId) + assert(data1After.buried && data2After.buried) + + data1After.shortChannelId + } + + /** Utility method to make sure that the channel has processed all previous messages */ + def waitReady(node: MinimalNodeFixture, channelId: ByteVector32)(implicit system: ActorSystem): Unit = { + getChannelState(node, channelId) + } + + def getChannelState(node: MinimalNodeFixture, channelId: ByteVector32)(implicit system: ActorSystem): ChannelState = { + val sender = TestProbe("sender") + node.register ! Register.Forward(sender.ref, channelId, CMD_GET_CHANNEL_STATE(sender.ref)) + sender.expectMsgType[RES_GET_CHANNEL_STATE].state + } + + def getChannelData(node: MinimalNodeFixture, channelId: ByteVector32)(implicit system: ActorSystem): ChannelData = { + val sender = TestProbe("sender") + node.register ! Register.Forward(sender.ref, channelId, CMD_GET_CHANNEL_DATA(sender.ref)) + sender.expectMsgType[RES_GET_CHANNEL_DATA[ChannelData]].data + } + + def getRouterData(node: MinimalNodeFixture)(implicit system: ActorSystem): Router.Data = { + val sender = TestProbe("sender") + sender.send(node.router, Router.GetRouterData) + sender.expectMsgType[Router.Data] + } + + private def deterministicShortId(txId: ByteVector32): (BlockHeight, Int) = { + val blockHeight = txId.take(3).toInt(signed = false) + val txIndex = txId.takeRight(2).toInt(signed = false) + (BlockHeight(blockHeight), txIndex) + } + + /** An autopilot method for the watcher, that handled funding confirmation requests from the channel */ + def autoConfirmLocalChannels(fundingTxs: collection.concurrent.Map[ByteVector32, Transaction]): TestActor.AutoPilot = (_, msg) => msg match { + case watch: ZmqWatcher.WatchFundingConfirmed => + val (blockHeight, txIndex) = deterministicShortId(watch.txId) + watch.replyTo ! ZmqWatcher.WatchFundingConfirmedTriggered(blockHeight, txIndex, fundingTxs(watch.txId)) + TestActor.KeepRunning + case watch: ZmqWatcher.WatchFundingDeeplyBuried => + val (blockHeight, txIndex) = deterministicShortId(watch.txId) + watch.replyTo ! ZmqWatcher.WatchFundingDeeplyBuriedTriggered(blockHeight, txIndex, fundingTxs(watch.txId)) + TestActor.KeepRunning + case _ => TestActor.KeepRunning + } + + /** An autopilot method for the watcher, that handles channel validation requests from the router */ + def autoValidatePublicChannels(fundingTxs: Map[ShortChannelId, Transaction]): TestActor.AutoPilot = (_, msg: Any) => msg match { + case vr: ZmqWatcher.ValidateRequest => + val res = Try(fundingTxs(vr.ann.shortChannelId), ZmqWatcher.UtxoStatus.Unspent).toEither + vr.replyTo ! ZmqWatcher.ValidateResult(vr.ann, res) + TestActor.KeepRunning + case _ => TestActor.KeepRunning + } + + def sendPayment(node1: MinimalNodeFixture, node2: MinimalNodeFixture, amount: MilliSatoshi)(implicit system: ActorSystem): PaymentSent = { + val sender = TestProbe("sender") + sender.send(node2.paymentHandler, MultiPartHandler.ReceivePayment(Some(amount), Left("test payment"))) + val invoice = sender.expectMsgType[Bolt11Invoice] + + val routeParams = node1.nodeParams.routerConf.pathFindingExperimentConf.experiments.values.head.getDefaultRouteParams + sender.send(node1.paymentInitiator, PaymentInitiator.SendPaymentToNode(amount, invoice, maxAttempts = 1, routeParams = routeParams, blockUntilComplete = true)) + sender.expectMsgType[PaymentSent] + } + + def prettyPrint(routerData: Router.Data, nodes: MinimalNodeFixture*): Unit = { + val nodeId2Alias = nodes.map(n => n.nodeParams.nodeId -> n.nodeParams.alias).toMap + .withDefault(nodeId => throw new RuntimeException(s"cannot resolve nodeId=$nodeId, make sure you have provided all node fixtures")) + routerData.channels.values.foreach { channel => + val name = Seq(channel.nodeId1, channel.nodeId2).map(nodeId2Alias).sorted.mkString("-") + val u1 = channel.update_1_opt.map(_ => s"${nodeId2Alias(channel.nodeId1)}=yes").getOrElse(s"${nodeId2Alias(channel.nodeId1)}=no") + val u2 = channel.update_2_opt.map(_ => s"${nodeId2Alias(channel.nodeId2)}=yes").getOrElse(s"${nodeId2Alias(channel.nodeId2)}=no") + println(s"$name : $u1 $u2") + } + } + +} \ No newline at end of file diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/fixtures/ThreeNodesFixture.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/fixtures/ThreeNodesFixture.scala new file mode 100644 index 0000000000..c2062d168f --- /dev/null +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/fixtures/ThreeNodesFixture.scala @@ -0,0 +1,31 @@ +package fr.acinq.eclair.integration.basic.fixtures + +import akka.actor.ActorSystem +import akka.testkit.TestKit +import fr.acinq.eclair.NodeParams + + +case class ThreeNodesFixture(system: ActorSystem, + alice: MinimalNodeFixture, + bob: MinimalNodeFixture, + carol: MinimalNodeFixture) { + implicit val implicitSystem: ActorSystem = system + + def cleanup(): Unit = { + TestKit.shutdownActorSystem(alice.system) + TestKit.shutdownActorSystem(bob.system) + TestKit.shutdownActorSystem(carol.system) + TestKit.shutdownActorSystem(system) + } +} + +object ThreeNodesFixture { + def apply(aliceParams: NodeParams, bobParams: NodeParams, carolParams: NodeParams): ThreeNodesFixture = { + ThreeNodesFixture( + system = ActorSystem("system-test"), + alice = MinimalNodeFixture(aliceParams), + bob = MinimalNodeFixture(bobParams), + carol = MinimalNodeFixture(carolParams), + ) + } +} \ No newline at end of file diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/fixtures/TwoNodesFixture.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/fixtures/TwoNodesFixture.scala new file mode 100644 index 0000000000..4e9bc88287 --- /dev/null +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/fixtures/TwoNodesFixture.scala @@ -0,0 +1,27 @@ +package fr.acinq.eclair.integration.basic.fixtures + +import akka.actor.ActorSystem +import akka.testkit.TestKit +import fr.acinq.eclair.NodeParams + +case class TwoNodesFixture(system: ActorSystem, + alice: MinimalNodeFixture, + bob: MinimalNodeFixture) { + implicit val implicitSystem: ActorSystem = system + + def cleanup(): Unit = { + TestKit.shutdownActorSystem(alice.system) + TestKit.shutdownActorSystem(bob.system) + TestKit.shutdownActorSystem(system) + } +} + +object TwoNodesFixture { + def apply(aliceParams: NodeParams, bobParams: NodeParams): TwoNodesFixture = { + TwoNodesFixture( + system = ActorSystem("system-test"), + alice = MinimalNodeFixture(aliceParams), + bob = MinimalNodeFixture(bobParams) + ) + } +} diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/interop/rustytests/RustyTestsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/interop/rustytests/RustyTestsSpec.scala index 9e36e63243..fa6b642e34 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/interop/rustytests/RustyTestsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/interop/rustytests/RustyTestsSpec.scala @@ -27,7 +27,7 @@ import fr.acinq.eclair.blockchain.fee.{FeeratePerKw, FeeratesPerKw} import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel.fsm.Channel import fr.acinq.eclair.channel.publish.TxPublisher -import fr.acinq.eclair.channel.states.ChannelStateTestsHelperMethods.FakeTxPublisherFactory +import fr.acinq.eclair.channel.states.ChannelStateTestsBase.FakeTxPublisherFactory import fr.acinq.eclair.payment.receive.{ForwardHandler, PaymentHandler} import fr.acinq.eclair.wire.protocol.Init import fr.acinq.eclair.{BlockHeight, MilliSatoshiLong, TestFeeEstimator, TestKitBaseClass, TestUtils} @@ -107,15 +107,15 @@ class RustyTestsSpec extends TestKitBaseClass with Matchers with FixtureAnyFunSu TestKit.shutdownActorSystem(system) } - test("01-offer1") { f => assert(f.ref === f.res) } - test("02-offer2") { f => assert(f.ref === f.res) } - test("03-fulfill1") { f => assert(f.ref === f.res) } - // test("04-two-commits-onedir") { f => assert(f.ref === f.res) } DOES NOT PASS : we now automatically sign back when we receive a revocation and have acked changes - // test("05-two-commits-in-flight") { f => assert(f.ref === f.res)} DOES NOT PASS : cannot send two commit in a row (without having first revocation) - test("10-offers-crossover") { f => assert(f.ref === f.res) } - test("11-commits-crossover") { f => assert(f.ref === f.res) } - /*test("13-fee") { f => assert(f.ref === f.res)} - test("14-fee-twice") { f => assert(f.ref === f.res)} - test("15-fee-twice-back-to-back") { f => assert(f.ref === f.res)}*/ + test("01-offer1") { f => assert(f.ref == f.res) } + test("02-offer2") { f => assert(f.ref == f.res) } + test("03-fulfill1") { f => assert(f.ref == f.res) } + // test("04-two-commits-onedir") { f => assert(f.ref == f.res) } DOES NOT PASS : we now automatically sign back when we receive a revocation and have acked changes + // test("05-two-commits-in-flight") { f => assert(f.ref == f.res)} DOES NOT PASS : cannot send two commit in a row (without having first revocation) + test("10-offers-crossover") { f => assert(f.ref == f.res) } + test("11-commits-crossover") { f => assert(f.ref == f.res) } + /*test("13-fee") { f => assert(f.ref == f.res)} + test("14-fee-twice") { f => assert(f.ref == f.res)} + test("15-fee-twice-back-to-back") { f => assert(f.ref == f.res)}*/ } \ No newline at end of file diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/io/MessageRelaySpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/io/MessageRelaySpec.scala index e5104e37a3..202a4fe44f 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/io/MessageRelaySpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/io/MessageRelaySpec.scala @@ -61,9 +61,9 @@ class MessageRelaySpec extends ScalaTestWithActorTestKit(ConfigFactory.load("app relay ! RelayMessage(messageId, switchboard.ref, randomKey().publicKey, bobId, message, RelayAll, None) val connectToNextPeer = switchboard.expectMsgType[Peer.Connect] - assert(connectToNextPeer.nodeId === bobId) + assert(connectToNextPeer.nodeId == bobId) connectToNextPeer.replyTo ! PeerConnection.ConnectionResult.Connected(peerConnection.ref.toClassic, peer.ref.toClassic) - assert(peer.expectMessageType[Peer.RelayOnionMessage].msg === message) + assert(peer.expectMessageType[Peer.RelayOnionMessage].msg == message) } test("relay with existing peer") { f => @@ -74,9 +74,9 @@ class MessageRelaySpec extends ScalaTestWithActorTestKit(ConfigFactory.load("app relay ! RelayMessage(messageId, switchboard.ref, randomKey().publicKey, bobId, message, RelayAll, None) val connectToNextPeer = switchboard.expectMsgType[Peer.Connect] - assert(connectToNextPeer.nodeId === bobId) + assert(connectToNextPeer.nodeId == bobId) connectToNextPeer.replyTo ! PeerConnection.ConnectionResult.AlreadyConnected(peerConnection.ref.toClassic, peer.ref.toClassic) - assert(peer.expectMessageType[Peer.RelayOnionMessage].msg === message) + assert(peer.expectMessageType[Peer.RelayOnionMessage].msg == message) } test("can't open new connection") { f => @@ -87,7 +87,7 @@ class MessageRelaySpec extends ScalaTestWithActorTestKit(ConfigFactory.load("app relay ! RelayMessage(messageId, switchboard.ref, randomKey().publicKey, bobId, message, RelayAll, Some(probe.ref)) val connectToNextPeer = switchboard.expectMsgType[Peer.Connect] - assert(connectToNextPeer.nodeId === bobId) + assert(connectToNextPeer.nodeId == bobId) connectToNextPeer.replyTo ! PeerConnection.ConnectionResult.NoAddressFound probe.expectMessage(ConnectionFailure(messageId, PeerConnection.ConnectionResult.NoAddressFound)) } @@ -101,7 +101,7 @@ class MessageRelaySpec extends ScalaTestWithActorTestKit(ConfigFactory.load("app relay ! RelayMessage(messageId, switchboard.ref, previousNodeId, bobId, message, RelayChannelsOnly, Some(probe.ref)) val getPeerInfo = switchboard.expectMsgType[GetPeerInfo] - assert(getPeerInfo.remoteNodeId === previousNodeId) + assert(getPeerInfo.remoteNodeId == previousNodeId) getPeerInfo.replyTo ! PeerInfo(peer.ref.toClassic, previousNodeId, Peer.CONNECTED, None, 0) probe.expectMessage(AgainstPolicy(messageId, RelayChannelsOnly)) @@ -117,11 +117,11 @@ class MessageRelaySpec extends ScalaTestWithActorTestKit(ConfigFactory.load("app relay ! RelayMessage(messageId, switchboard.ref, previousNodeId, bobId, message, RelayChannelsOnly, Some(probe.ref)) val getPeerInfo1 = switchboard.expectMsgType[GetPeerInfo] - assert(getPeerInfo1.remoteNodeId === previousNodeId) + assert(getPeerInfo1.remoteNodeId == previousNodeId) getPeerInfo1.replyTo ! PeerInfo(peer.ref.toClassic, previousNodeId, Peer.CONNECTED, None, 1) val getPeerInfo2 = switchboard.expectMsgType[GetPeerInfo] - assert(getPeerInfo2.remoteNodeId === bobId) + assert(getPeerInfo2.remoteNodeId == bobId) getPeerInfo2.replyTo ! PeerNotFound(bobId) probe.expectMessage(AgainstPolicy(messageId, RelayChannelsOnly)) @@ -137,14 +137,14 @@ class MessageRelaySpec extends ScalaTestWithActorTestKit(ConfigFactory.load("app relay ! RelayMessage(messageId, switchboard.ref, previousNodeId, bobId, message, RelayChannelsOnly, None) val getPeerInfo1 = switchboard.expectMsgType[GetPeerInfo] - assert(getPeerInfo1.remoteNodeId === previousNodeId) + assert(getPeerInfo1.remoteNodeId == previousNodeId) getPeerInfo1.replyTo ! PeerInfo(TestProbe()(system.classicSystem).ref, previousNodeId, Peer.CONNECTED, None, 1) val getPeerInfo2 = switchboard.expectMsgType[GetPeerInfo] - assert(getPeerInfo2.remoteNodeId === bobId) + assert(getPeerInfo2.remoteNodeId == bobId) getPeerInfo2.replyTo ! PeerInfo(peer.ref.toClassic, bobId, Peer.CONNECTED, None, 2) - assert(peer.expectMessageType[Peer.RelayOnionMessage].msg === message) + assert(peer.expectMessageType[Peer.RelayOnionMessage].msg == message) } test("no relay") { f => diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/io/NodeURISpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/io/NodeURISpec.scala index 2fb1a4ade9..949e24bdad 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/io/NodeURISpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/io/NodeURISpec.scala @@ -48,7 +48,7 @@ class NodeURISpec extends AnyFunSuite { assert(nodeUri.nodeId.toString() == PUBKEY) assert(nodeUri.address.host == testCase.formattedAddr) assert(nodeUri.address.port == testCase.port) - assert(nodeUri.toString === s"$PUBKEY@${testCase.formattedAddr}:${testCase.port}") + assert(nodeUri.toString == s"$PUBKEY@${testCase.formattedAddr}:${testCase.port}") } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerConnectionSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerConnectionSpec.scala index 117d9032c1..6333ae77ab 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerConnectionSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerConnectionSpec.scala @@ -76,7 +76,7 @@ class PeerConnectionSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wi probe.send(peerConnection, PeerConnection.InitializeConnection(peer.ref, aliceParams.chainHash, aliceParams.features.initFeatures(), doSync)) transport.expectMsgType[TransportHandler.Listener] val localInit = transport.expectMsgType[protocol.Init] - assert(localInit.networks === List(Block.RegtestGenesisBlock.hash)) + assert(localInit.networks == List(Block.RegtestGenesisBlock.hash)) transport.send(peerConnection, remoteInit) transport.expectMsgType[TransportHandler.ReadAck] if (doSync) { @@ -85,7 +85,7 @@ class PeerConnectionSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wi router.expectNoMessage(1 second) } peer.expectMsg(PeerConnection.ConnectionReady(peerConnection, remoteNodeId, address, outgoing = true, localInit, remoteInit)) - assert(peerConnection.stateName === PeerConnection.CONNECTED) + assert(peerConnection.stateName == PeerConnection.CONNECTED) } test("establish connection") { f => @@ -104,7 +104,7 @@ class PeerConnectionSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wi probe.send(peerConnection, PeerConnection.InitializeConnection(peer.ref, nodeParams.chainHash, nodeParams.features.initFeatures(), doSync = false)) transport.expectMsgType[TransportHandler.Listener] val localInit = transport.expectMsgType[protocol.Init] - assert(localInit.remoteAddress_opt === Some(fakeIPAddress)) + assert(localInit.remoteAddress_opt == Some(fakeIPAddress)) } test("handle connection closed during authentication") { f => @@ -215,7 +215,7 @@ class PeerConnectionSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wi val ping = Ping(42, randomBytes(127)) transport.send(peerConnection, ping) transport.expectMsg(TransportHandler.ReadAck(ping)) - assert(transport.expectMsgType[Pong].data.size === ping.pongLength) + assert(transport.expectMsgType[Pong].data.size == ping.pongLength) } test("send a ping if no message after init") { f => @@ -246,7 +246,7 @@ class PeerConnectionSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wi val ping = Ping(Int.MaxValue, randomBytes(127)) transport.send(peerConnection, ping) transport.expectMsg(TransportHandler.ReadAck(ping)) - assert(transport.expectMsgType[Warning].channelId === Peer.CHANNELID_ZERO) + assert(transport.expectMsgType[Warning].channelId == Peer.CHANNELID_ZERO) transport.expectNoMessage() } @@ -376,14 +376,14 @@ class PeerConnectionSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wi router.send(peerConnection, GossipDecision.InvalidAnnouncement(channels(0))) // peer will return a connection-wide error, including the hex-encoded representation of the bad message val warn1 = transport.expectMsgType[Warning] - assert(warn1.channelId === Peer.CHANNELID_ZERO) + assert(warn1.channelId == Peer.CHANNELID_ZERO) assert(new String(warn1.data.toArray).startsWith("invalid announcement, couldn't verify channel")) // let's assume that one of the sigs were invalid router.send(peerConnection, GossipDecision.InvalidSignature(channels(0))) // peer will return a connection-wide error, including the hex-encoded representation of the bad message val warn2 = transport.expectMsgType[Warning] - assert(warn2.channelId === Peer.CHANNELID_ZERO) + assert(warn2.channelId == Peer.CHANNELID_ZERO) assert(new String(warn2.data.toArray).startsWith("invalid announcement sig")) } @@ -411,7 +411,7 @@ class PeerConnectionSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wi probe watch peerConnection probe.send(peerConnection, message) // The connection is still open for a short while. - assert(peerConnection.stateName === PeerConnection.CONNECTED) + assert(peerConnection.stateName == PeerConnection.CONNECTED) sleep(1 second) probe.expectTerminated(peerConnection, max = Duration.Zero) } @@ -422,7 +422,7 @@ class PeerConnectionSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wi val probe = TestProbe() val (_, message) = buildMessage(randomKey(), randomKey(), Nil, Recipient(remoteNodeId, None), Nil) probe.send(peerConnection, message) - assert(peerConnection.stateName === PeerConnection.CONNECTED) + assert(peerConnection.stateName == PeerConnection.CONNECTED) probe.send(peerConnection, FundingLocked(ByteVector32(hex"0000000000000000000000000000000000000000000000000000000000000000"), randomKey().publicKey)) peerConnection.stateData match { case d: PeerConnection.ConnectedData => assert(d.isPersistent) @@ -481,7 +481,7 @@ class PeerConnectionSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wi ) for ((address, expected) <- testCases) { val isPublicIP = NodeAddress.isPublicIPAddress(address) - assert(isPublicIP === expected) + assert(isPublicIP == expected) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala index f5387b4c77..e557c5b3e0 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala @@ -55,7 +55,7 @@ class PeerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Paralle case class FakeChannelFactory(channel: TestProbe) extends ChannelFactory { override def spawn(context: ActorContext, remoteNodeId: PublicKey, origin_opt: Option[ActorRef]): ActorRef = { - assert(remoteNodeId === Bob.nodeParams.nodeId) + assert(remoteNodeId == Bob.nodeParams.nodeId) channel.ref } } @@ -94,9 +94,9 @@ class PeerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Paralle val probe = TestProbe() probe.send(peer, Peer.GetPeerInfo(Some(probe.ref.toTyped))) val peerInfo = probe.expectMsgType[Peer.PeerInfo] - assert(peerInfo.peer === peer) - assert(peerInfo.nodeId === remoteNodeId) - assert(peerInfo.state === Peer.CONNECTED) + assert(peerInfo.peer == peer) + assert(peerInfo.nodeId == remoteNodeId) + assert(peerInfo.state == Peer.CONNECTED) } test("restore existing channels") { f => @@ -188,7 +188,7 @@ class PeerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Paralle connect(remoteNodeId, peer, peerConnection, switchboard, channels = Set(ChannelCodecsSpec.normal)) probe.send(peer, Peer.GetPeerInfo(Some(probe.ref.toTyped))) - assert(probe.expectMsgType[Peer.PeerInfo].state === Peer.CONNECTED) + assert(probe.expectMsgType[Peer.PeerInfo].state == Peer.CONNECTED) probe.send(peer, Peer.Disconnect(f.remoteNodeId)) probe.expectMsg("disconnecting") @@ -202,11 +202,11 @@ class PeerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Paralle awaitCond { probe.send(peer, Peer.GetPeerInfo(None)) - probe.expectMsgType[Peer.PeerInfo].state === Peer.DISCONNECTED + probe.expectMsgType[Peer.PeerInfo].state == Peer.DISCONNECTED } probe.send(peer, Peer.Disconnect(f.remoteNodeId)) - assert(probe.expectMsgType[Status.Failure].cause.getMessage === "not connected") + assert(probe.expectMsgType[Status.Failure].cause.getMessage == "not connected") } test("handle new connection in state CONNECTED") { f => @@ -220,7 +220,7 @@ class PeerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Paralle channel.expectMsg(INPUT_RESTORED(ChannelCodecsSpec.normal)) val (localInit, remoteInit) = { val inputReconnected = channel.expectMsgType[INPUT_RECONNECTED] - assert(inputReconnected.remote === peerConnection1.ref) + assert(inputReconnected.remote == peerConnection1.ref) (inputReconnected.localInit, inputReconnected.remoteInit) } @@ -229,14 +229,14 @@ class PeerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Paralle peerConnection1.expectMsg(PeerConnection.Kill(PeerConnection.KillReason.ConnectionReplaced)) channel.expectMsg(INPUT_DISCONNECTED) channel.expectMsg(INPUT_RECONNECTED(peerConnection2.ref, localInit, remoteInit)) - awaitCond(peer.stateData.asInstanceOf[Peer.ConnectedData].peerConnection === peerConnection2.ref) + awaitCond(peer.stateData.asInstanceOf[Peer.ConnectedData].peerConnection == peerConnection2.ref) peerConnection3.send(peer, PeerConnection.ConnectionReady(peerConnection3.ref, remoteNodeId, fakeIPAddress, outgoing = false, localInit, remoteInit)) // peer should kill previous connection peerConnection2.expectMsg(PeerConnection.Kill(PeerConnection.KillReason.ConnectionReplaced)) channel.expectMsg(INPUT_DISCONNECTED) channel.expectMsg(INPUT_RECONNECTED(peerConnection3.ref, localInit, remoteInit)) - awaitCond(peer.stateData.asInstanceOf[Peer.ConnectedData].peerConnection === peerConnection3.ref) + awaitCond(peer.stateData.asInstanceOf[Peer.ConnectedData].peerConnection == peerConnection3.ref) } test("send state transitions to child reconnection actor", Tag("auto_reconnect"), Tag("with_node_announcement")) { f => @@ -275,14 +275,14 @@ class PeerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Paralle val open = createOpenChannelMessage() peerConnection.send(peer, open) awaitCond(peer.stateData.channels.nonEmpty) - assert(channel.expectMsgType[INPUT_INIT_FUNDEE].temporaryChannelId === open.temporaryChannelId) + assert(channel.expectMsgType[INPUT_INIT_FUNDEE].temporaryChannelId == open.temporaryChannelId) channel.expectMsg(open) // open_channel messages with the same temporary channel id should simply be ignored peerConnection.send(peer, open.copy(fundingSatoshis = 100000 sat, fundingPubkey = randomKey().publicKey)) channel.expectNoMessage(100 millis) peerConnection.expectNoMessage(100 millis) - assert(peer.stateData.channels.size === 1) + assert(peer.stateData.channels.size == 1) } test("don't spawn a wumbo channel if wumbo feature isn't enabled") { f => @@ -379,7 +379,7 @@ class PeerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Paralle val open = createOpenChannelMessage(TlvStream[OpenChannelTlv](ChannelTlv.ChannelTypeTlv(ChannelTypes.Standard))) peerConnection.send(peer, open) awaitCond(peer.stateData.channels.nonEmpty) - assert(channel.expectMsgType[INPUT_INIT_FUNDEE].channelType === ChannelTypes.Standard) + assert(channel.expectMsgType[INPUT_INIT_FUNDEE].channelType == ChannelTypes.Standard) channel.expectMsg(open) } @@ -391,15 +391,15 @@ class PeerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Paralle assert(peer.stateData.channels.isEmpty) probe.send(peer, Peer.OpenChannel(remoteNodeId, 15000 sat, 0 msat, None, None, None, None)) - assert(channel.expectMsgType[INPUT_INIT_FUNDER].channelType === ChannelTypes.StaticRemoteKey) + assert(channel.expectMsgType[INPUT_INIT_FUNDER].channelType == ChannelTypes.StaticRemoteKey) // We can create channels that don't use the features we have enabled. probe.send(peer, Peer.OpenChannel(remoteNodeId, 15000 sat, 0 msat, Some(ChannelTypes.Standard), None, None, None)) - assert(channel.expectMsgType[INPUT_INIT_FUNDER].channelType === ChannelTypes.Standard) + assert(channel.expectMsgType[INPUT_INIT_FUNDER].channelType == ChannelTypes.Standard) // We can create channels that use features that we haven't enabled. probe.send(peer, Peer.OpenChannel(remoteNodeId, 15000 sat, 0 msat, Some(ChannelTypes.AnchorOutputs), None, None, None)) - assert(channel.expectMsgType[INPUT_INIT_FUNDER].channelType === ChannelTypes.AnchorOutputs) + assert(channel.expectMsgType[INPUT_INIT_FUNDER].channelType == ChannelTypes.AnchorOutputs) } test("use correct on-chain fee rates when spawning a channel (anchor outputs)", Tag("anchor_outputs")) { f => @@ -414,10 +414,10 @@ class PeerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Paralle feeEstimator.setFeerate(FeeratesPerKw.single(TestConstants.anchorOutputsFeeratePerKw * 2).copy(mempoolMinFee = FeeratePerKw(250 sat))) probe.send(peer, Peer.OpenChannel(remoteNodeId, 15000 sat, 0 msat, None, None, None, None)) val init = channel.expectMsgType[INPUT_INIT_FUNDER] - assert(init.channelType === ChannelTypes.AnchorOutputs) - assert(init.fundingAmount === 15000.sat) - assert(init.commitTxFeerate === TestConstants.anchorOutputsFeeratePerKw) - assert(init.fundingTxFeerate === feeEstimator.getFeeratePerKw(nodeParams.onChainFeeConf.feeTargets.fundingBlockTarget)) + assert(init.channelType == ChannelTypes.AnchorOutputs) + assert(init.fundingAmount == 15000.sat) + assert(init.commitTxFeerate == TestConstants.anchorOutputsFeeratePerKw) + assert(init.fundingTxFeerate == feeEstimator.getFeeratePerKw(nodeParams.onChainFeeConf.feeTargets.fundingBlockTarget)) } test("use correct on-chain fee rates when spawning a channel (anchor outputs zero fee htlc)", Tag("anchor_outputs_zero_fee_htlc_tx")) { f => @@ -432,10 +432,10 @@ class PeerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Paralle feeEstimator.setFeerate(FeeratesPerKw.single(TestConstants.anchorOutputsFeeratePerKw * 2).copy(mempoolMinFee = FeeratePerKw(250 sat))) probe.send(peer, Peer.OpenChannel(remoteNodeId, 15000 sat, 0 msat, None, None, None, None)) val init = channel.expectMsgType[INPUT_INIT_FUNDER] - assert(init.channelType === ChannelTypes.AnchorOutputsZeroFeeHtlcTx) - assert(init.fundingAmount === 15000.sat) - assert(init.commitTxFeerate === TestConstants.anchorOutputsFeeratePerKw) - assert(init.fundingTxFeerate === feeEstimator.getFeeratePerKw(nodeParams.onChainFeeConf.feeTargets.fundingBlockTarget)) + assert(init.channelType == ChannelTypes.AnchorOutputsZeroFeeHtlcTx) + assert(init.fundingAmount == 15000.sat) + assert(init.commitTxFeerate == TestConstants.anchorOutputsFeeratePerKw) + assert(init.fundingTxFeerate == feeEstimator.getFeeratePerKw(nodeParams.onChainFeeConf.feeTargets.fundingBlockTarget)) } test("use correct final script if option_static_remotekey is negotiated", Tag("static_remotekey")) { f => @@ -445,9 +445,9 @@ class PeerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Paralle connect(remoteNodeId, peer, peerConnection, switchboard, remoteInit = protocol.Init(Features(StaticRemoteKey -> Mandatory))) probe.send(peer, Peer.OpenChannel(remoteNodeId, 24000 sat, 0 msat, None, None, None, None)) val init = channel.expectMsgType[INPUT_INIT_FUNDER] - assert(init.channelType === ChannelTypes.StaticRemoteKey) + assert(init.channelType == ChannelTypes.StaticRemoteKey) assert(init.localParams.walletStaticPaymentBasepoint.isDefined) - assert(init.localParams.defaultFinalScriptPubKey === Script.write(Script.pay2wpkh(init.localParams.walletStaticPaymentBasepoint.get))) + assert(init.localParams.defaultFinalScriptPubKey == Script.write(Script.pay2wpkh(init.localParams.walletStaticPaymentBasepoint.get))) } test("set origin_opt when spawning a channel") { f => @@ -456,7 +456,7 @@ class PeerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Paralle val probe = TestProbe() val channelFactory = new ChannelFactory { override def spawn(context: ActorContext, remoteNodeId: PublicKey, origin_opt: Option[ActorRef]): ActorRef = { - assert(origin_opt === Some(probe.ref)) + assert(origin_opt == Some(probe.ref)) channel.ref } } @@ -464,8 +464,8 @@ class PeerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Paralle connect(remoteNodeId, peer, peerConnection, switchboard) probe.send(peer, Peer.OpenChannel(remoteNodeId, 15000 sat, 100 msat, None, None, None, None)) val init = channel.expectMsgType[INPUT_INIT_FUNDER] - assert(init.fundingAmount === 15000.sat) - assert(init.pushAmount === 100.msat) + assert(init.fundingAmount == 15000.sat) + assert(init.pushAmount == 100.msat) } test("handle final channelId assigned in state DISCONNECTED") { f => @@ -475,13 +475,13 @@ class PeerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Paralle peer ! ConnectionDown(peerConnection.ref) probe.send(peer, Peer.GetPeerInfo(Some(probe.ref.toTyped))) val peerInfo1 = probe.expectMsgType[Peer.PeerInfo] - assert(peerInfo1.state === Peer.DISCONNECTED) - assert(peerInfo1.channels === 1) + assert(peerInfo1.state == Peer.DISCONNECTED) + assert(peerInfo1.channels == 1) peer ! ChannelIdAssigned(probe.ref, remoteNodeId, randomBytes32(), randomBytes32()) probe.send(peer, Peer.GetPeerInfo(Some(probe.ref.toTyped))) val peerInfo2 = probe.expectMsgType[Peer.PeerInfo] - assert(peerInfo2.state === Peer.DISCONNECTED) - assert(peerInfo2.channels === 2) + assert(peerInfo2.state == Peer.DISCONNECTED) + assert(peerInfo2.channels == 2) } test("notify when last channel is closed") { f => diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/io/ReconnectionTaskSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/io/ReconnectionTaskSpec.scala index cd0bd0ca1a..fec76a7a6e 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/io/ReconnectionTaskSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/io/ReconnectionTaskSpec.scala @@ -101,7 +101,7 @@ class ReconnectionTaskSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike peer.send(reconnectionTask, Peer.Transition(PeerNothingData, PeerDisconnectedData)) val TransitionWithData(ReconnectionTask.IDLE, ReconnectionTask.WAITING, _, _) = monitor.expectMsgType[TransitionWithData] val TransitionWithData(ReconnectionTask.WAITING, ReconnectionTask.CONNECTING, _, connectingData: ReconnectionTask.ConnectingData) = monitor.expectMsgType[TransitionWithData] - assert(connectingData.to === fakeIPAddress) + assert(connectingData.to == fakeIPAddress) val expectedNextReconnectionDelayInterval = (nodeParams.maxReconnectInterval.toSeconds / 2) to nodeParams.maxReconnectInterval.toSeconds assert(expectedNextReconnectionDelayInterval contains connectingData.nextReconnectionDelay.toSeconds) // we only reconnect once } @@ -134,13 +134,13 @@ class ReconnectionTaskSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike val TransitionWithData(ReconnectionTask.WAITING, ReconnectionTask.CONNECTING, _, _) = monitor.expectMsgType[TransitionWithData] val TransitionWithData(ReconnectionTask.CONNECTING, ReconnectionTask.WAITING, _, waitingData1: WaitingData) = monitor.expectMsgType[TransitionWithData](60 seconds) - assert(waitingData1.nextReconnectionDelay === (waitingData0.nextReconnectionDelay * 2)) + assert(waitingData1.nextReconnectionDelay == (waitingData0.nextReconnectionDelay * 2)) probe.send(reconnectionTask, ReconnectionTask.TickReconnect) val TransitionWithData(ReconnectionTask.WAITING, ReconnectionTask.CONNECTING, _, _) = monitor.expectMsgType[TransitionWithData] val TransitionWithData(ReconnectionTask.CONNECTING, ReconnectionTask.WAITING, _, waitingData2: WaitingData) = monitor.expectMsgType[TransitionWithData](60 seconds) - assert(waitingData2.nextReconnectionDelay === (waitingData0.nextReconnectionDelay * 4)) + assert(waitingData2.nextReconnectionDelay == (waitingData0.nextReconnectionDelay * 4)) probe.send(reconnectionTask, ReconnectionTask.TickReconnect) val TransitionWithData(ReconnectionTask.WAITING, ReconnectionTask.CONNECTING, _, _) = monitor.expectMsgType[TransitionWithData] @@ -153,7 +153,7 @@ class ReconnectionTaskSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike // the auto reconnect kicks off again, but this time we pick up the reconnect delay where we left it val TransitionWithData(ReconnectionTask.IDLE, ReconnectionTask.WAITING, _, waitingData3: WaitingData) = monitor.expectMsgType[TransitionWithData] - assert(waitingData3.nextReconnectionDelay === (waitingData0.nextReconnectionDelay * 8)) + assert(waitingData3.nextReconnectionDelay == (waitingData0.nextReconnectionDelay * 8)) } test("all kind of connection failures should be caught by the reconnection task", Tag("auto_reconnect")) { f => @@ -235,9 +235,9 @@ class ReconnectionTaskSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike { // tor not supported: always return clearnet addresses nodeParams.socksProxy_opt returns None - assert(ReconnectionTask.selectNodeAddress(nodeParams, List(clearnet)) === Some(clearnet)) - assert(ReconnectionTask.selectNodeAddress(nodeParams, List(tor)) === None) - assert(ReconnectionTask.selectNodeAddress(nodeParams, List(clearnet, tor)) === Some(clearnet)) + assert(ReconnectionTask.selectNodeAddress(nodeParams, List(clearnet)) == Some(clearnet)) + assert(ReconnectionTask.selectNodeAddress(nodeParams, List(tor)) == None) + assert(ReconnectionTask.selectNodeAddress(nodeParams, List(clearnet, tor)) == Some(clearnet)) } { @@ -247,9 +247,9 @@ class ReconnectionTaskSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike socksParams.useForIPv4 returns false socksParams.useForIPv6 returns false nodeParams.socksProxy_opt returns Some(socksParams) - assert(ReconnectionTask.selectNodeAddress(nodeParams, List(clearnet)) === Some(clearnet)) - assert(ReconnectionTask.selectNodeAddress(nodeParams, List(tor)) === Some(tor)) - assert(ReconnectionTask.selectNodeAddress(nodeParams, List(clearnet, tor)) === Some(clearnet)) + assert(ReconnectionTask.selectNodeAddress(nodeParams, List(clearnet)) == Some(clearnet)) + assert(ReconnectionTask.selectNodeAddress(nodeParams, List(tor)) == Some(tor)) + assert(ReconnectionTask.selectNodeAddress(nodeParams, List(clearnet, tor)) == Some(clearnet)) } { @@ -259,9 +259,9 @@ class ReconnectionTaskSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike socksParams.useForIPv4 returns true socksParams.useForIPv6 returns true nodeParams.socksProxy_opt returns Some(socksParams) - assert(ReconnectionTask.selectNodeAddress(nodeParams, List(clearnet)) === Some(clearnet)) - assert(ReconnectionTask.selectNodeAddress(nodeParams, List(tor)) === Some(tor)) - assert(ReconnectionTask.selectNodeAddress(nodeParams, List(clearnet, tor)) === Some(tor)) + assert(ReconnectionTask.selectNodeAddress(nodeParams, List(clearnet)) == Some(clearnet)) + assert(ReconnectionTask.selectNodeAddress(nodeParams, List(tor)) == Some(tor)) + assert(ReconnectionTask.selectNodeAddress(nodeParams, List(clearnet, tor)) == Some(tor)) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/io/SwitchboardSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/io/SwitchboardSpec.scala index c1f87f42c6..c20f33814b 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/io/SwitchboardSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/io/SwitchboardSpec.scala @@ -42,8 +42,8 @@ class SwitchboardSpec extends TestKitBaseClass with AnyFunSuiteLike { probe.expectMsg(remoteNodeId) peer.expectMsg(Peer.Init(Set.empty)) val connect = peer.expectMsgType[Peer.Connect] - assert(connect.nodeId === remoteNodeId) - assert(connect.address_opt === None) + assert(connect.nodeId == remoteNodeId) + assert(connect.address_opt == None) } test("disconnect from peers") { @@ -58,7 +58,7 @@ class SwitchboardSpec extends TestKitBaseClass with AnyFunSuiteLike { val unknownNodeId = randomKey().publicKey probe.send(switchboard, Peer.Disconnect(unknownNodeId)) - assert(probe.expectMsgType[Status.Failure].cause.getMessage === s"peer $unknownNodeId not found") + assert(probe.expectMsgType[Status.Failure].cause.getMessage == s"peer $unknownNodeId not found") probe.send(switchboard, Peer.Disconnect(remoteNodeId)) peer.expectMsg(Peer.Disconnect(remoteNodeId)) } @@ -68,9 +68,9 @@ class SwitchboardSpec extends TestKitBaseClass with AnyFunSuiteLike { val switchboard = TestActorRef(new Switchboard(nodeParams, FakePeerFactory(probe, peer))) switchboard ! PeerConnection.Authenticated(peerConnection.ref, remoteNodeId) val initConnection = peerConnection.expectMsgType[PeerConnection.InitializeConnection] - assert(initConnection.chainHash === nodeParams.chainHash) - assert(initConnection.features === expectedFeatures) - assert(initConnection.doSync === expectedSync) + assert(initConnection.chainHash == nodeParams.chainHash) + assert(initConnection.features == expectedFeatures) + assert(initConnection.doSync == expectedSync) } test("sync if no whitelist is defined and peer has channels") { @@ -90,16 +90,16 @@ class SwitchboardSpec extends TestKitBaseClass with AnyFunSuiteLike { switchboard ! ChannelIdAssigned(TestProbe().ref, remoteNodeId, randomBytes32(), randomBytes32()) switchboard ! PeerConnection.Authenticated(peerConnection.ref, remoteNodeId) val initConnection1 = peerConnection.expectMsgType[PeerConnection.InitializeConnection] - assert(initConnection1.chainHash === nodeParams.chainHash) - assert(initConnection1.features === nodeParams.features.initFeatures()) + assert(initConnection1.chainHash == nodeParams.chainHash) + assert(initConnection1.features == nodeParams.features.initFeatures()) assert(initConnection1.doSync) // We don't have channels with our peer, so we won't trigger a sync when connecting. switchboard ! LastChannelClosed(peer.ref, remoteNodeId) switchboard ! PeerConnection.Authenticated(peerConnection.ref, remoteNodeId) val initConnection2 = peerConnection.expectMsgType[PeerConnection.InitializeConnection] - assert(initConnection2.chainHash === nodeParams.chainHash) - assert(initConnection2.features === nodeParams.features.initFeatures()) + assert(initConnection2.chainHash == nodeParams.chainHash) + assert(initConnection2.features == nodeParams.features.initFeatures()) assert(!initConnection2.doSync) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/message/OnionMessagesSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/message/OnionMessagesSpec.scala index 606572f74f..84f9968245 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/message/OnionMessagesSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/message/OnionMessagesSpec.scala @@ -224,11 +224,11 @@ class OnionMessagesSpec extends AnyFunSuite { val blindingOverride = randomKey() val destination = randomKey() val replyPath = buildRoute(blindingOverride, IntermediateNode(destination.publicKey) :: Nil, Recipient(destination.publicKey, pathId = Some(hex"01234567"))) - assert(replyPath.blindingKey === blindingOverride.publicKey) - assert(replyPath.introductionNodeId === destination.publicKey) + assert(replyPath.blindingKey == blindingOverride.publicKey) + assert(replyPath.introductionNodeId == destination.publicKey) val (nextNodeId, message) = buildMessage(sessionKey, blindingSecret, Nil, BlindedPath(replyPath), Nil) - assert(nextNodeId === destination.publicKey) - assert(message.blindingKey === blindingOverride.publicKey) // blindingSecret was not used as the replyPath was used as is + assert(nextNodeId == destination.publicKey) + assert(message.blindingKey == blindingOverride.publicKey) // blindingSecret was not used as the replyPath was used as is process(destination, message) match { case ReceiveMessage(_, Some(pathId)) if pathId == hex"01234567" => () diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/message/PostmanSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/message/PostmanSpec.scala index 2bd30db61a..66297b5535 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/message/PostmanSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/message/PostmanSpec.scala @@ -62,8 +62,8 @@ class PostmanSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("applicat postman ! SendMessage(recipient, messageExpectingReply, Some(pathId), messageRecipient.ref, 100 millis) val RelayMessage(messageId, _, nextNodeId, message, _, _) = switchboard.expectMessageType[RelayMessage] - assert(nextNodeId === recipient) - assert(message === messageExpectingReply) + assert(nextNodeId == recipient) + assert(message == messageExpectingReply) postman ! SendingStatus(Sent(messageId)) testKit.system.eventStream ! EventStream.Publish(ReceiveMessage(payload, Some(pathId))) testKit.system.eventStream ! EventStream.Publish(ReceiveMessage(payload, Some(pathId))) @@ -84,8 +84,8 @@ class PostmanSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("applicat postman ! SendMessage(recipient, messageExpectingReply, Some(pathId), messageRecipient.ref, 100 millis) val RelayMessage(messageId, _, nextNodeId, message, _, _) = switchboard.expectMessageType[RelayMessage] - assert(nextNodeId === recipient) - assert(message === messageExpectingReply) + assert(nextNodeId == recipient) + assert(message == messageExpectingReply) postman ! SendingStatus(Disconnected(messageId)) messageRecipient.expectMessage(SendingStatus(Disconnected(messageId))) @@ -105,8 +105,8 @@ class PostmanSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("applicat postman ! SendMessage(recipient, messageExpectingReply, Some(pathId), messageRecipient.ref, 1 millis) val RelayMessage(messageId, _, nextNodeId, message, _, _) = switchboard.expectMessageType[RelayMessage] - assert(nextNodeId === recipient) - assert(message === messageExpectingReply) + assert(nextNodeId == recipient) + assert(message == messageExpectingReply) postman ! SendingStatus(Sent(messageId)) messageRecipient.expectMessage(NoReply) @@ -125,8 +125,8 @@ class PostmanSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("applicat postman ! SendMessage(recipient, messageExpectingReply, None, messageRecipient.ref, 100 millis) val RelayMessage(messageId, _, nextNodeId, message, _, _) = switchboard.expectMessageType[RelayMessage] - assert(nextNodeId === recipient) - assert(message === messageExpectingReply) + assert(nextNodeId == recipient) + assert(message == messageExpectingReply) postman ! SendingStatus(Sent(messageId)) messageRecipient.expectMessage(SendingStatus(Sent(messageId))) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/Bolt11InvoiceSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/Bolt11InvoiceSpec.scala index d68870976a..3c50b23fe9 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/Bolt11InvoiceSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/Bolt11InvoiceSpec.scala @@ -82,37 +82,37 @@ class Bolt11InvoiceSpec extends AnyFunSuite { } test("check minimal unit is used") { - assert('p' === Amount.unit(1 msat)) - assert('p' === Amount.unit(99 msat)) - assert('n' === Amount.unit(100 msat)) - assert('p' === Amount.unit(101 msat)) - assert('n' === Amount.unit((1 sat).toMilliSatoshi)) - assert('u' === Amount.unit((100 sat).toMilliSatoshi)) - assert('n' === Amount.unit((101 sat).toMilliSatoshi)) - assert('u' === Amount.unit((1155400 sat).toMilliSatoshi)) - assert('m' === Amount.unit((1 millibtc).toMilliSatoshi)) - assert('m' === Amount.unit((10 millibtc).toMilliSatoshi)) - assert('m' === Amount.unit((1 btc).toMilliSatoshi)) + assert('p' == Amount.unit(1 msat)) + assert('p' == Amount.unit(99 msat)) + assert('n' == Amount.unit(100 msat)) + assert('p' == Amount.unit(101 msat)) + assert('n' == Amount.unit((1 sat).toMilliSatoshi)) + assert('u' == Amount.unit((100 sat).toMilliSatoshi)) + assert('n' == Amount.unit((101 sat).toMilliSatoshi)) + assert('u' == Amount.unit((1155400 sat).toMilliSatoshi)) + assert('m' == Amount.unit((1 millibtc).toMilliSatoshi)) + assert('m' == Amount.unit((10 millibtc).toMilliSatoshi)) + assert('m' == Amount.unit((1 btc).toMilliSatoshi)) } test("decode empty amount") { - assert(Amount.decode("") === Success(None)) - assert(Amount.decode("0") === Success(None)) - assert(Amount.decode("0p") === Success(None)) - assert(Amount.decode("0n") === Success(None)) - assert(Amount.decode("0u") === Success(None)) - assert(Amount.decode("0m") === Success(None)) + assert(Amount.decode("") == Success(None)) + assert(Amount.decode("0") == Success(None)) + assert(Amount.decode("0p") == Success(None)) + assert(Amount.decode("0n") == Success(None)) + assert(Amount.decode("0u") == Success(None)) + assert(Amount.decode("0m") == Success(None)) } test("check that we can still decode non-minimal amount encoding") { - assert(Amount.decode("1000u") === Success(Some(100000000 msat))) - assert(Amount.decode("1000000n") === Success(Some(100000000 msat))) - assert(Amount.decode("1000000000p") === Success(Some(100000000 msat))) + assert(Amount.decode("1000u") == Success(Some(100000000 msat))) + assert(Amount.decode("1000000n") == Success(Some(100000000 msat))) + assert(Amount.decode("1000000000p") == Success(Some(100000000 msat))) } test("data string -> bitvector") { - assert(string2Bits("p") === bin"00001") - assert(string2Bits("pz") === bin"0000100010") + assert(string2Bits("p") == bin"00001") + assert(string2Bits("pz") == bin"0000100010") } test("minimal length long, left-padded to be multiple of 5") { @@ -136,13 +136,13 @@ class Bolt11InvoiceSpec extends AnyFunSuite { assert(invoice.prefix == "lnbc") assert(invoice.amount_opt.isEmpty) assert(invoice.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102") - assert(invoice.paymentSecret.map(_.bytes) === Some(hex"1111111111111111111111111111111111111111111111111111111111111111")) - assert(invoice.features === Features(Features.VariableLengthOnion -> Mandatory, Features.PaymentSecret -> Mandatory)) + assert(invoice.paymentSecret.map(_.bytes) == Some(hex"1111111111111111111111111111111111111111111111111111111111111111")) + assert(invoice.features == Features(Features.VariableLengthOnion -> Mandatory, Features.PaymentSecret -> Mandatory)) assert(invoice.createdAt == TimestampSecond(1496314658L)) assert(invoice.nodeId == PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) assert(invoice.description == Left("Please consider supporting this project")) - assert(invoice.fallbackAddress() === None) - assert(invoice.tags.size === 4) + assert(invoice.fallbackAddress() == None) + assert(invoice.tags.size == 4) assert(invoice.sign(priv).toString == ref) } @@ -150,14 +150,14 @@ class Bolt11InvoiceSpec extends AnyFunSuite { val ref = "lnbc2500u1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpu9qrsgquk0rl77nj30yxdy8j9vdx85fkpmdla2087ne0xh8nhedh8w27kyke0lp53ut353s06fv3qfegext0eh0ymjpf39tuven09sam30g4vgpfna3rh" val Success(invoice) = Bolt11Invoice.fromString(ref) assert(invoice.prefix == "lnbc") - assert(invoice.amount_opt === Some(250000000 msat)) + assert(invoice.amount_opt == Some(250000000 msat)) assert(invoice.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102") - assert(invoice.features === Features(Features.VariableLengthOnion -> Mandatory, Features.PaymentSecret -> Mandatory)) + assert(invoice.features == Features(Features.VariableLengthOnion -> Mandatory, Features.PaymentSecret -> Mandatory)) assert(invoice.createdAt == TimestampSecond(1496314658L)) assert(invoice.nodeId == PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) assert(invoice.description == Left("1 cup coffee")) - assert(invoice.fallbackAddress() === None) - assert(invoice.tags.size === 5) + assert(invoice.fallbackAddress() == None) + assert(invoice.tags.size == 5) assert(invoice.sign(priv).toString == ref) } @@ -165,14 +165,14 @@ class Bolt11InvoiceSpec extends AnyFunSuite { val ref = "lnbc2500u1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdpquwpc4curk03c9wlrswe78q4eyqc7d8d0xqzpu9qrsgqhtjpauu9ur7fw2thcl4y9vfvh4m9wlfyz2gem29g5ghe2aak2pm3ps8fdhtceqsaagty2vph7utlgj48u0ged6a337aewvraedendscp573dxr" val Success(invoice) = Bolt11Invoice.fromString(ref) assert(invoice.prefix == "lnbc") - assert(invoice.amount_opt === Some(250000000 msat)) + assert(invoice.amount_opt == Some(250000000 msat)) assert(invoice.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102") - assert(invoice.features === Features(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory)) + assert(invoice.features == Features(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory)) assert(invoice.createdAt == TimestampSecond(1496314658L)) assert(invoice.nodeId == PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) assert(invoice.description == Left("ナンセンス 1杯")) - assert(invoice.fallbackAddress() === None) - assert(invoice.tags.size === 5) + assert(invoice.fallbackAddress() == None) + assert(invoice.tags.size == 5) assert(invoice.sign(priv).toString == ref) } @@ -180,14 +180,14 @@ class Bolt11InvoiceSpec extends AnyFunSuite { val ref = "lnbc20m1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqs9qrsgq7ea976txfraylvgzuxs8kgcw23ezlrszfnh8r6qtfpr6cxga50aj6txm9rxrydzd06dfeawfk6swupvz4erwnyutnjq7x39ymw6j38gp7ynn44" val Success(invoice) = Bolt11Invoice.fromString(ref) assert(invoice.prefix == "lnbc") - assert(invoice.amount_opt === Some(2000000000 msat)) + assert(invoice.amount_opt == Some(2000000000 msat)) assert(invoice.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102") - assert(invoice.features === Features(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory)) + assert(invoice.features == Features(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory)) assert(invoice.createdAt == TimestampSecond(1496314658L)) assert(invoice.nodeId == PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) assert(invoice.description == Right(Crypto.sha256(ByteVector("One piece of chocolate cake, one icecream cone, one pickle, one slice of swiss cheese, one slice of salami, one lollypop, one piece of cherry pie, one sausage, one cupcake, and one slice of watermelon".getBytes)))) - assert(invoice.fallbackAddress() === None) - assert(invoice.tags.size === 4) + assert(invoice.fallbackAddress() == None) + assert(invoice.tags.size == 4) assert(invoice.sign(priv).toString == ref) } @@ -195,13 +195,13 @@ class Bolt11InvoiceSpec extends AnyFunSuite { val ref = "lntb20m1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygshp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqfpp3x9et2e20v6pu37c5d9vax37wxq72un989qrsgqdj545axuxtnfemtpwkc45hx9d2ft7x04mt8q7y6t0k2dge9e7h8kpy9p34ytyslj3yu569aalz2xdk8xkd7ltxqld94u8h2esmsmacgpghe9k8" val Success(invoice) = Bolt11Invoice.fromString(ref) assert(invoice.prefix == "lntb") - assert(invoice.amount_opt === Some(2000000000 msat)) + assert(invoice.amount_opt == Some(2000000000 msat)) assert(invoice.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102") - assert(invoice.features === Features(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory)) + assert(invoice.features == Features(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory)) assert(invoice.createdAt == TimestampSecond(1496314658L)) assert(invoice.nodeId == PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) assert(invoice.description == Right(Crypto.sha256(ByteVector.view("One piece of chocolate cake, one icecream cone, one pickle, one slice of swiss cheese, one slice of salami, one lollypop, one piece of cherry pie, one sausage, one cupcake, and one slice of watermelon".getBytes)))) - assert(invoice.fallbackAddress() === Some("mk2QpYatsKicvFVuTAQLBryyccRXMUaGHP")) + assert(invoice.fallbackAddress() == Some("mk2QpYatsKicvFVuTAQLBryyccRXMUaGHP")) assert(invoice.tags.size == 5) assert(invoice.sign(priv).toString == ref) } @@ -210,14 +210,14 @@ class Bolt11InvoiceSpec extends AnyFunSuite { val ref = "lnbc20m1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqsfpp3qjmp7lwpagxun9pygexvgpjdc4jdj85fr9yq20q82gphp2nflc7jtzrcazrra7wwgzxqc8u7754cdlpfrmccae92qgzqvzq2ps8pqqqqqqpqqqqq9qqqvpeuqafqxu92d8lr6fvg0r5gv0heeeqgcrqlnm6jhphu9y00rrhy4grqszsvpcgpy9qqqqqqgqqqqq7qqzq9qrsgqdfjcdk6w3ak5pca9hwfwfh63zrrz06wwfya0ydlzpgzxkn5xagsqz7x9j4jwe7yj7vaf2k9lqsdk45kts2fd0fkr28am0u4w95tt2nsq76cqw0" val Success(invoice) = Bolt11Invoice.fromString(ref) assert(invoice.prefix == "lnbc") - assert(invoice.amount_opt === Some(2000000000 msat)) + assert(invoice.amount_opt == Some(2000000000 msat)) assert(invoice.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102") - assert(invoice.features === Features(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory)) + assert(invoice.features == Features(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory)) assert(invoice.createdAt == TimestampSecond(1496314658L)) assert(invoice.nodeId == PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) assert(invoice.description == Right(Crypto.sha256(ByteVector.view("One piece of chocolate cake, one icecream cone, one pickle, one slice of swiss cheese, one slice of salami, one lollypop, one piece of cherry pie, one sausage, one cupcake, and one slice of watermelon".getBytes)))) - assert(invoice.fallbackAddress() === Some("1RustyRX2oai4EYYDpQGWvEL62BBGqN9T")) - assert(invoice.routingInfo === List(List( + assert(invoice.fallbackAddress() == Some("1RustyRX2oai4EYYDpQGWvEL62BBGqN9T")) + assert(invoice.routingInfo == List(List( ExtraHop(PublicKey(hex"029e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255"), ShortChannelId("66051x263430x1800"), 1 msat, 20, CltvExpiryDelta(3)), ExtraHop(PublicKey(hex"039e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255"), ShortChannelId("197637x395016x2314"), 2 msat, 30, CltvExpiryDelta(4)) ))) @@ -229,13 +229,13 @@ class Bolt11InvoiceSpec extends AnyFunSuite { val ref = "lnbc20m1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygshp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqfppj3a24vwu6r8ejrss3axul8rxldph2q7z99qrsgqz6qsgww34xlatfj6e3sngrwfy3ytkt29d2qttr8qz2mnedfqysuqypgqex4haa2h8fx3wnypranf3pdwyluftwe680jjcfp438u82xqphf75ym" val Success(invoice) = Bolt11Invoice.fromString(ref) assert(invoice.prefix == "lnbc") - assert(invoice.amount_opt === Some(2000000000 msat)) + assert(invoice.amount_opt == Some(2000000000 msat)) assert(invoice.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102") - assert(invoice.features === Features(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory)) + assert(invoice.features == Features(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory)) assert(invoice.createdAt == TimestampSecond(1496314658L)) assert(invoice.nodeId == PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) assert(invoice.description == Right(Crypto.sha256(ByteVector.view("One piece of chocolate cake, one icecream cone, one pickle, one slice of swiss cheese, one slice of salami, one lollypop, one piece of cherry pie, one sausage, one cupcake, and one slice of watermelon".getBytes)))) - assert(invoice.fallbackAddress() === Some("3EktnHQD7RiAE6uzMj2ZifT9YgRrkSgzQX")) + assert(invoice.fallbackAddress() == Some("3EktnHQD7RiAE6uzMj2ZifT9YgRrkSgzQX")) assert(invoice.tags.size == 5) assert(invoice.sign(priv).toString == ref) } @@ -244,13 +244,13 @@ class Bolt11InvoiceSpec extends AnyFunSuite { val ref = "lnbc20m1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygshp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqfppqw508d6qejxtdg4y5r3zarvary0c5xw7k9qrsgqt29a0wturnys2hhxpner2e3plp6jyj8qx7548zr2z7ptgjjc7hljm98xhjym0dg52sdrvqamxdezkmqg4gdrvwwnf0kv2jdfnl4xatsqmrnsse" val Success(invoice) = Bolt11Invoice.fromString(ref) assert(invoice.prefix == "lnbc") - assert(invoice.amount_opt === Some(2000000000 msat)) + assert(invoice.amount_opt == Some(2000000000 msat)) assert(invoice.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102") - assert(invoice.features === Features(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory)) + assert(invoice.features == Features(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory)) assert(invoice.createdAt == TimestampSecond(1496314658L)) assert(invoice.nodeId == PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) assert(invoice.description == Right(Crypto.sha256(ByteVector.view("One piece of chocolate cake, one icecream cone, one pickle, one slice of swiss cheese, one slice of salami, one lollypop, one piece of cherry pie, one sausage, one cupcake, and one slice of watermelon".getBytes)))) - assert(invoice.fallbackAddress() === Some("bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4")) + assert(invoice.fallbackAddress() == Some("bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4")) assert(invoice.tags.size == 5) assert(invoice.sign(priv).toString == ref) } @@ -259,14 +259,14 @@ class Bolt11InvoiceSpec extends AnyFunSuite { val ref = "lnbc20m1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygshp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqfp4qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q9qrsgq9vlvyj8cqvq6ggvpwd53jncp9nwc47xlrsnenq2zp70fq83qlgesn4u3uyf4tesfkkwwfg3qs54qe426hp3tz7z6sweqdjg05axsrjqp9yrrwc" val Success(invoice) = Bolt11Invoice.fromString(ref) assert(invoice.prefix == "lnbc") - assert(invoice.amount_opt === Some(2000000000 msat)) + assert(invoice.amount_opt == Some(2000000000 msat)) assert(invoice.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102") - assert(invoice.features === Features(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory)) + assert(invoice.features == Features(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory)) assert(!invoice.features.hasFeature(BasicMultiPartPayment)) assert(invoice.createdAt == TimestampSecond(1496314658L)) assert(invoice.nodeId == PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) assert(invoice.description == Right(Crypto.sha256(ByteVector.view("One piece of chocolate cake, one icecream cone, one pickle, one slice of swiss cheese, one slice of salami, one lollypop, one piece of cherry pie, one sausage, one cupcake, and one slice of watermelon".getBytes)))) - assert(invoice.fallbackAddress() === Some("bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3")) + assert(invoice.fallbackAddress() == Some("bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3")) assert(invoice.tags.size == 5) assert(invoice.sign(priv).toString == ref) } @@ -275,15 +275,15 @@ class Bolt11InvoiceSpec extends AnyFunSuite { val ref = "lnbc20m1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygscqpvpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqsfp4qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q9qrsgq999fraffdzl6c8j7qd325dfurcq7vl0mfkdpdvve9fy3hy4lw0x9j3zcj2qdh5e5pyrp6cncvmxrhchgey64culwmjtw9wym74xm6xqqevh9r0" val Success(invoice) = Bolt11Invoice.fromString(ref) assert(invoice.prefix == "lnbc") - assert(invoice.amount_opt === Some(2000000000 msat)) + assert(invoice.amount_opt == Some(2000000000 msat)) assert(invoice.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102") - assert(invoice.features === Features(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory)) + assert(invoice.features == Features(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory)) assert(!invoice.features.hasFeature(BasicMultiPartPayment)) assert(invoice.createdAt == TimestampSecond(1496314658L)) assert(invoice.nodeId == PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) assert(invoice.description == Right(Crypto.sha256(ByteVector.view("One piece of chocolate cake, one icecream cone, one pickle, one slice of swiss cheese, one slice of salami, one lollypop, one piece of cherry pie, one sausage, one cupcake, and one slice of watermelon".getBytes)))) - assert(invoice.fallbackAddress() === Some("bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3")) - assert(invoice.minFinalCltvExpiryDelta === CltvExpiryDelta(12)) + assert(invoice.fallbackAddress() == Some("bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3")) + assert(invoice.minFinalCltvExpiryDelta == CltvExpiryDelta(12)) assert(invoice.tags.size == 6) assert(invoice.sign(priv).toString == ref) } @@ -299,71 +299,71 @@ class Bolt11InvoiceSpec extends AnyFunSuite { for (ref <- refs) { val Success(invoice) = Bolt11Invoice.fromString(ref) - assert(invoice.prefix === "lnbc") - assert(invoice.amount_opt === Some(2500000000L msat)) - assert(invoice.paymentHash.bytes === hex"0001020304050607080900010203040506070809000102030405060708090102") - assert(invoice.paymentSecret === Some(ByteVector32(hex"1111111111111111111111111111111111111111111111111111111111111111"))) - assert(invoice.createdAt === TimestampSecond(1496314658L)) - assert(invoice.nodeId === PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) - assert(invoice.description === Left("coffee beans")) - assert(features2bits(invoice.features) === bin"1000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000100000000") + assert(invoice.prefix == "lnbc") + assert(invoice.amount_opt == Some(2500000000L msat)) + assert(invoice.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102") + assert(invoice.paymentSecret == Some(ByteVector32(hex"1111111111111111111111111111111111111111111111111111111111111111"))) + assert(invoice.createdAt == TimestampSecond(1496314658L)) + assert(invoice.nodeId == PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) + assert(invoice.description == Left("coffee beans")) + assert(features2bits(invoice.features) == bin"1000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000100000000") assert(!invoice.features.hasFeature(BasicMultiPartPayment)) assert(invoice.features.hasFeature(PaymentSecret, Some(Mandatory))) assert(!invoice.features.hasFeature(TrampolinePaymentPrototype)) assert(TestConstants.Alice.nodeParams.features.invoiceFeatures().areSupported(invoice.features)) - assert(invoice.sign(priv).toString === ref.toLowerCase) + assert(invoice.sign(priv).toString == ref.toLowerCase) } } test("On mainnet, please send $30 for coffee beans to the same peer, which supports features 8, 14, 99 and 100, using secret 0x1111111111111111111111111111111111111111111111111111111111111111") { val ref = "lnbc25m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5vdhkven9v5sxyetpdeessp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9q4psqqqqqqqqqqqqqqqqsgqtqyx5vggfcsll4wu246hz02kp85x4katwsk9639we5n5yngc3yhqkm35jnjw4len8vrnqnf5ejh0mzj9n3vz2px97evektfm2l6wqccp3y7372" val Success(invoice) = Bolt11Invoice.fromString(ref) - assert(invoice.prefix === "lnbc") - assert(invoice.amount_opt === Some(2500000000L msat)) - assert(invoice.paymentHash.bytes === hex"0001020304050607080900010203040506070809000102030405060708090102") - assert(invoice.paymentSecret === Some(ByteVector32(hex"1111111111111111111111111111111111111111111111111111111111111111"))) - assert(invoice.createdAt === TimestampSecond(1496314658L)) - assert(invoice.nodeId === PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) - assert(invoice.description === Left("coffee beans")) + assert(invoice.prefix == "lnbc") + assert(invoice.amount_opt == Some(2500000000L msat)) + assert(invoice.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102") + assert(invoice.paymentSecret == Some(ByteVector32(hex"1111111111111111111111111111111111111111111111111111111111111111"))) + assert(invoice.createdAt == TimestampSecond(1496314658L)) + assert(invoice.nodeId == PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) + assert(invoice.description == Left("coffee beans")) assert(invoice.fallbackAddress().isEmpty) - assert(features2bits(invoice.features) === bin"000011000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000100000000") + assert(features2bits(invoice.features) == bin"000011000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000100000000") assert(!invoice.features.hasFeature(BasicMultiPartPayment)) assert(invoice.features.hasFeature(PaymentSecret, Some(Mandatory))) assert(!invoice.features.hasFeature(TrampolinePaymentPrototype)) assert(!TestConstants.Alice.nodeParams.features.invoiceFeatures().areSupported(invoice.features)) - assert(invoice.sign(priv).toString === ref) + assert(invoice.sign(priv).toString == ref) } test("On mainnet, please send 0.00967878534 BTC for a list of items within one week, amount in pico-BTC") { val ref = "lnbc9678785340p1pwmna7lpp5gc3xfm08u9qy06djf8dfflhugl6p7lgza6dsjxq454gxhj9t7a0sd8dgfkx7cmtwd68yetpd5s9xar0wfjn5gpc8qhrsdfq24f5ggrxdaezqsnvda3kkum5wfjkzmfqf3jkgem9wgsyuctwdus9xgrcyqcjcgpzgfskx6eqf9hzqnteypzxz7fzypfhg6trddjhygrcyqezcgpzfysywmm5ypxxjemgw3hxjmn8yptk7untd9hxwg3q2d6xjcmtv4ezq7pqxgsxzmnyyqcjqmt0wfjjq6t5v4khxsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygsxqyjw5qcqp2rzjq0gxwkzc8w6323m55m4jyxcjwmy7stt9hwkwe2qxmy8zpsgg7jcuwz87fcqqeuqqqyqqqqlgqqqqn3qq9q9qrsgqrvgkpnmps664wgkp43l22qsgdw4ve24aca4nymnxddlnp8vh9v2sdxlu5ywdxefsfvm0fq3sesf08uf6q9a2ke0hc9j6z6wlxg5z5kqpu2v9wz" val Success(invoice) = Bolt11Invoice.fromString(ref) - assert(invoice.prefix === "lnbc") - assert(invoice.amount_opt === Some(967878534 msat)) - assert(invoice.paymentHash.bytes === hex"462264ede7e14047e9b249da94fefc47f41f7d02ee9b091815a5506bc8abf75f") - assert(invoice.features === Features(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory)) + assert(invoice.prefix == "lnbc") + assert(invoice.amount_opt == Some(967878534 msat)) + assert(invoice.paymentHash.bytes == hex"462264ede7e14047e9b249da94fefc47f41f7d02ee9b091815a5506bc8abf75f") + assert(invoice.features == Features(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory)) assert(TestConstants.Alice.nodeParams.features.invoiceFeatures().areSupported(invoice.features)) - assert(invoice.createdAt === TimestampSecond(1572468703L)) - assert(invoice.nodeId === PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) - assert(invoice.description === Left("Blockstream Store: 88.85 USD for Blockstream Ledger Nano S x 1, \"Back In My Day\" Sticker x 2, \"I Got Lightning Working\" Sticker x 2 and 1 more items")) + assert(invoice.createdAt == TimestampSecond(1572468703L)) + assert(invoice.nodeId == PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) + assert(invoice.description == Left("Blockstream Store: 88.85 USD for Blockstream Ledger Nano S x 1, \"Back In My Day\" Sticker x 2, \"I Got Lightning Working\" Sticker x 2 and 1 more items")) assert(invoice.fallbackAddress().isEmpty) - assert(invoice.relativeExpiry === 604800.seconds) - assert(invoice.minFinalCltvExpiryDelta === CltvExpiryDelta(10)) - assert(invoice.routingInfo === Seq(Seq(ExtraHop(PublicKey(hex"03d06758583bb5154774a6eb221b1276c9e82d65bbaceca806d90e20c108f4b1c7"), ShortChannelId("589390x3312x1"), 1000 msat, 2500, CltvExpiryDelta(40))))) - assert(invoice.sign(priv).toString === ref) + assert(invoice.relativeExpiry == 604800.seconds) + assert(invoice.minFinalCltvExpiryDelta == CltvExpiryDelta(10)) + assert(invoice.routingInfo == Seq(Seq(ExtraHop(PublicKey(hex"03d06758583bb5154774a6eb221b1276c9e82d65bbaceca806d90e20c108f4b1c7"), ShortChannelId("589390x3312x1"), 1000 msat, 2500, CltvExpiryDelta(40))))) + assert(invoice.sign(priv).toString == ref) } test("On mainnet, please send 0.01 BTC with payment metadata 0x01fafaf0") { val ref = "lnbc10m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdp9wpshjmt9de6zqmt9w3skgct5vysxjmnnd9jx2mq8q8a04uqsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9q2gqqqqqqsgq7hf8he7ecf7n4ffphs6awl9t6676rrclv9ckg3d3ncn7fct63p6s365duk5wrk202cfy3aj5xnnp5gs3vrdvruverwwq7yzhkf5a3xqpd05wjc" val Success(invoice) = Bolt11Invoice.fromString(ref) assert(invoice.prefix == "lnbc") - assert(invoice.amount_opt === Some(1000000000 msat)) + assert(invoice.amount_opt == Some(1000000000 msat)) assert(invoice.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102") - assert(invoice.features === Features(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory, PaymentMetadata -> Mandatory)) + assert(invoice.features == Features(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory, PaymentMetadata -> Mandatory)) assert(invoice.createdAt == TimestampSecond(1496314658L)) assert(invoice.nodeId == PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) - assert(invoice.paymentSecret === Some(ByteVector32(hex"1111111111111111111111111111111111111111111111111111111111111111"))) + assert(invoice.paymentSecret == Some(ByteVector32(hex"1111111111111111111111111111111111111111111111111111111111111111"))) assert(invoice.description == Left("payment metadata inside")) - assert(invoice.paymentMetadata === Some(hex"01fafaf0")) + assert(invoice.paymentMetadata == Some(hex"01fafaf0")) assert(invoice.tags.size == 5) assert(invoice.sign(priv).toString == ref) } @@ -401,7 +401,7 @@ class Bolt11InvoiceSpec extends AnyFunSuite { assert(field1 == field) val invoice = Bolt11Invoice(chainHash = Block.LivenetGenesisBlock.hash, amount = Some(123 msat), paymentHash = ByteVector32(ByteVector.fill(32)(1)), privateKey = priv, description = Left("Some invoice"), minFinalCltvExpiryDelta = CltvExpiryDelta(18), expirySeconds = Some(123456), timestamp = 12345 unixsec) - assert(invoice.minFinalCltvExpiryDelta === CltvExpiryDelta(18)) + assert(invoice.minFinalCltvExpiryDelta == CltvExpiryDelta(18)) val serialized = invoice.toString val Success(pr1) = Bolt11Invoice.fromString(serialized) assert(invoice == pr1) @@ -443,9 +443,9 @@ class Bolt11InvoiceSpec extends AnyFunSuite { for ((input, value) <- inputs) { val data = string2Bits(input) val decoded = Codecs.taggedFieldCodec.decode(data).require.value - assert(decoded === value) + assert(decoded == value) val encoded = Codecs.taggedFieldCodec.encode(value).require - assert(encoded === data) + assert(encoded == data) } } @@ -457,8 +457,8 @@ class Bolt11InvoiceSpec extends AnyFunSuite { test("Pay 1 BTC without multiplier") { val ref = "lnbc1000m1pdkmqhusp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygspp5n2ees808r98m0rh4472yyth0c5fptzcxmexcjznrzmq8xald0cgqdqsf4ujqarfwqsxymmccqp2pv37ezvhth477nu0yhhjlcry372eef57qmldhreqnr0kx82jkupp3n7nw42u3kdyyjskdr8jhjy2vugr3skdmy8ersft36969xplkxsp2v7c58" val Success(invoice) = Bolt11Invoice.fromString(ref) - assert(invoice.amount_opt === Some(100000000000L msat)) - assert(features2bits(invoice.features) === BitVector.empty) + assert(invoice.amount_opt == Some(100000000000L msat)) + assert(features2bits(invoice.features) == BitVector.empty) } test("supported invoice features") { @@ -486,8 +486,8 @@ class Bolt11InvoiceSpec extends AnyFunSuite { for ((features, res) <- featureBits) { val invoice = createInvoiceUnsafe(Block.LivenetGenesisBlock.hash, Some(123 msat), ByteVector32.One, priv, Left("Some invoice"), CltvExpiryDelta(18), features = features) - assert(Result(invoice.features.hasFeature(BasicMultiPartPayment), invoice.features.hasFeature(PaymentSecret, Some(Mandatory)), nodeParams.features.invoiceFeatures().areSupported(invoice.features)) === res) - assert(Bolt11Invoice.fromString(invoice.toString).get === invoice) + assert(Result(invoice.features.hasFeature(BasicMultiPartPayment), invoice.features.hasFeature(PaymentSecret, Some(Mandatory)), nodeParams.features.invoiceFeatures().areSupported(invoice.features)) == res) + assert(Bolt11Invoice.fromString(invoice.toString).get == invoice) } } @@ -504,22 +504,22 @@ class Bolt11InvoiceSpec extends AnyFunSuite { ) for ((bitmask, featureBytes) <- testCases) { - assert(Features(bitmask).toByteVector === featureBytes) + assert(Features(bitmask).toByteVector == featureBytes) } } test("payment secret") { val invoice = Bolt11Invoice(Block.LivenetGenesisBlock.hash, Some(123 msat), ByteVector32.One, priv, Left("Some invoice"), CltvExpiryDelta(18)) assert(invoice.paymentSecret.isDefined) - assert(invoice.features === Features(PaymentSecret -> Mandatory, VariableLengthOnion -> Mandatory)) + assert(invoice.features == Features(PaymentSecret -> Mandatory, VariableLengthOnion -> Mandatory)) assert(invoice.features.hasFeature(PaymentSecret, Some(Mandatory))) val Success(pr1) = Bolt11Invoice.fromString(invoice.toString) - assert(pr1.paymentSecret === invoice.paymentSecret) + assert(pr1.paymentSecret == invoice.paymentSecret) val Success(pr2) = Bolt11Invoice.fromString("lnbc40n1pw9qjvwpp5qq3w2ln6krepcslqszkrsfzwy49y0407hvks30ec6pu9s07jur3sdpstfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqenwdencqzysxqrrss7ju0s4dwx6w8a95a9p2xc5vudl09gjl0w2n02sjrvffde632nxwh2l4w35nqepj4j5njhh4z65wyfc724yj6dn9wajvajfn5j7em6wsq2elakl") assert(!pr2.features.hasFeature(PaymentSecret, Some(Mandatory))) - assert(pr2.paymentSecret === None) + assert(pr2.paymentSecret == None) // An invoice that sets the payment secret feature bit must provide a payment secret. assert(Bolt11Invoice.fromString("lnbc1230p1pwljzn3pp5qyqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqdq52dhk6efqd9h8vmmfvdjs9qypqsqylvwhf7xlpy6xpecsnpcjjuuslmzzgeyv90mh7k7vs88k2dkxgrkt75qyfjv5ckygw206re7spga5zfd4agtdvtktxh5pkjzhn9dq2cqz9upw7").isFailure) @@ -615,16 +615,16 @@ class Bolt11InvoiceSpec extends AnyFunSuite { for ((req, nodeId) <- requests) { val Success(invoice) = Bolt11Invoice.fromString(req) - assert(invoice.nodeId === nodeId) - assert(invoice.toString === req) + assert(invoice.nodeId == nodeId) + assert(invoice.toString == req) } } test("no unknown feature in invoice") { assert(TestConstants.Alice.nodeParams.features.invoiceFeatures().unknown.nonEmpty) val invoice = Bolt11Invoice(Block.LivenetGenesisBlock.hash, Some(123 msat), ByteVector32.One, priv, Left("Some invoice"), CltvExpiryDelta(18), features = TestConstants.Alice.nodeParams.features.invoiceFeatures()) - assert(invoice.features === Features(PaymentSecret -> Mandatory, BasicMultiPartPayment -> Optional, PaymentMetadata -> Optional, VariableLengthOnion -> Mandatory)) - assert(Bolt11Invoice.fromString(invoice.toString).get === invoice) + assert(invoice.features == Features(PaymentSecret -> Mandatory, BasicMultiPartPayment -> Optional, PaymentMetadata -> Optional, VariableLengthOnion -> Mandatory)) + assert(Bolt11Invoice.fromString(invoice.toString).get == invoice) } test("Invoices can't have high features"){ diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/Bolt12InvoiceSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/Bolt12InvoiceSpec.scala index 4808e268af..5cfddb4c22 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/Bolt12InvoiceSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/Bolt12InvoiceSpec.scala @@ -49,7 +49,7 @@ class Bolt12InvoiceSpec extends AnyFunSuite { assert(invoice.isValidFor(offer, request)) assert(invoice.checkSignature()) assert(!invoice.checkRefundSignature()) - assert(Bolt12Invoice.fromString(invoice.toString).get.toString === invoice.toString) + assert(Bolt12Invoice.fromString(invoice.toString).get.toString == invoice.toString) // changing signature makes check fail val withInvalidSignature = Bolt12Invoice(TlvStream(invoice.records.records.map { case Signature(_) => Signature(randomBytes64()) case x => x }, invoice.records.unknown), None) assert(!withInvalidSignature.checkSignature()) @@ -97,7 +97,7 @@ class Bolt12InvoiceSpec extends AnyFunSuite { val (nodeKey, payerKey, chain) = (randomKey(), randomKey(), randomBytes32()) val offer = Offer(Some(15000 msat), "test offer", nodeKey.publicKey, Features(VariableLengthOnion -> Mandatory), chain) val request = InvoiceRequest(offer, 15000 msat, 1, Features(VariableLengthOnion -> Mandatory), payerKey, chain) - assert(request.quantity_opt === None) // when paying for a single item, the quantity field must not be present + assert(request.quantity_opt == None) // when paying for a single item, the quantity field must not be present val invoice = Bolt12Invoice(offer, request, randomBytes32(), nodeKey, Features(VariableLengthOnion -> Mandatory, BasicMultiPartPayment -> Optional)) assert(invoice.isValidFor(offer, request)) val withInvalidFeatures = signInvoice(Bolt12Invoice(TlvStream(invoice.records.records.map { case FeaturesTlv(_) => FeaturesTlv(Features(VariableLengthOnion -> Mandatory, BasicMultiPartPayment -> Mandatory)) case x => x }.toSeq), None), nodeKey) @@ -241,9 +241,9 @@ class Bolt12InvoiceSpec extends AnyFunSuite { val nodeKey = PrivateKey(hex"c6a75116a91dc5ff741b079c32c8ce7544656b98f047fb0c0fa011bfb2bb3c05") val payerKey = PrivateKey(hex"7dd30ec116470c5f7f00af2c7e84968e28cdb43083b33ee832decbe73ec07f1a") val Success(offer) = Offer.decode("lno1qgsyxjtl6luzd9t3pr62xr7eemp6awnejusgf6gw45q75vcfqqqqqqqgqvqcdgq2pd3xzumfvvsx7enxv4epug9kku8f4e9nuef5lv59yrkdc24t5mtrym62cg085w5wtqkp0rsuly") - assert(offer.amount === Some(100_000 msat)) - assert(offer.nodeIdXOnly === xOnlyPublicKey(nodeKey.publicKey)) - assert(offer.chains === Seq(Block.TestnetGenesisBlock.hash)) + assert(offer.amount == Some(100_000 msat)) + assert(offer.nodeIdXOnly == xOnlyPublicKey(nodeKey.publicKey)) + assert(offer.chains == Seq(Block.TestnetGenesisBlock.hash)) val request = InvoiceRequest(offer, 100_000 msat, 1, Features.empty, payerKey, Block.TestnetGenesisBlock.hash) val Success(invoice) = Bolt12Invoice.fromString("lni1qvsyxjtl6luzd9t3pr62xr7eemp6awnejusgf6gw45q75vcfqqqqqqqyyp53zuupqkwxpmdq0tjg58ntat5ujpejlvyn92r0l5xzh4wru8e5zzqrqxr2qzstvfshx6tryphkven9wgxqq83qk6msaxhyk0n9xnajs5swehp24wndvvn0ftppu7363evzc9uwrnujvg95tuyy05nqkcdetsaljgq4u6789jllc54qrpjrzzn3c38dj3tscu5qgcs2y4lj5gqlvq50uu7sce478j3j0l599nxfs6svx2cfefgn4a0675893wtzuckqfwlcrcq0qspa9zynlpdk9zzechehkemgaksklylxhr7yfjfx6h696th327nm4nsf52xzq0ukchx69g00c4vvk6kzc5jyklneyy05l9tef7a5jcjn5") assert(!invoice.isExpired()) @@ -254,14 +254,14 @@ class Bolt12InvoiceSpec extends AnyFunSuite { val nodeKey = PrivateKey(hex"c6a75116a91dc5ff741b079c32c8ce7544656b98f047fb0c0fa011bfb2bb3c05") val payerKey = PrivateKey(hex"94c7a21a11efa16c5f73b093dc136d9525e2ff40ea7a958c43c1f6004bf6a676") val Success(offer) = Offer.decode("lno1pqpzwyq2pf382mrtyphkven9wgtqzqgcqy9pug9kku8f4e9nuef5lv59yrkdc24t5mtrym62cg085w5wtqkp0rsuly") - assert(offer.amount === Some(10_000 msat)) - assert(offer.nodeIdXOnly === xOnlyPublicKey(nodeKey.publicKey)) - assert(offer.chains === Seq(Block.LivenetGenesisBlock.hash)) + assert(offer.amount == Some(10_000 msat)) + assert(offer.nodeIdXOnly == xOnlyPublicKey(nodeKey.publicKey)) + assert(offer.chains == Seq(Block.LivenetGenesisBlock.hash)) val request = InvoiceRequest(offer, 50_000 msat, 5, Features.empty, payerKey, Block.LivenetGenesisBlock.hash) val Success(invoice) = Bolt12Invoice.fromString("lni1qss8u47nw2lsgml7fy4jaqwph9f8cl83zfrrhxccvh6076avqzzzv4qgqtp4qzs2vf6kc6eqdanxvetjpsqpug9kku8f4e9nuef5lv59yrkdc24t5mtrym62cg085w5wtqkp0rsulysqzpfxyrat02l8wtgtwuc4h5hw6dxhn0hcpdrtu3dpejfjdlw9h4j3nppxc2qyvg9z0lf2yq7wl9ygd6td4cj7whp3ye4cfxrtu7zq4r2mc0mcdspk3duzv7d0stqyh0upuq8sgr44r7aaluwqfw8pkd9f3cgk7ae2l8rkexznhegr0p7w4mlhvfkvlnr5k2lnw0hhsf6ckys3sst7kng5p7m2pxlvdxl3tan809vkk75j") assert(!invoice.isExpired()) - assert(invoice.amount === 50_000.msat) - assert(invoice.quantity === Some(5)) + assert(invoice.amount == 50_000.msat) + assert(invoice.quantity == Some(5)) assert(invoice.isValidFor(offer, request)) } @@ -324,26 +324,26 @@ class Bolt12InvoiceSpec extends AnyFunSuite { ), Seq(GenericTlv(UInt64(311), hex"010203"), GenericTlv(UInt64(313), hex""))) val signature = signSchnorr(Bolt12Invoice.signatureTag("signature"), rootHash(tlvs, invoiceTlvCodec), nodeKey) val invoice = Bolt12Invoice(tlvs.copy(records = tlvs.records ++ Seq(Signature(signature))), None) - assert(invoice.toString === "lni1qvsyxjtl6luzd9t3pr62xr7eemp6awnejusgf6gw45q75vcfqqqqqqqyyz9ut9uduhtztjgpxm06394g5qkw7v79g4czw6zxsl3lnrsvljj0qzqrq83yqzscd9h8vmmfvdjjqamfw35zqmtpdeujqenfv4kxgucvqgqsq9qgv93kjmn39e3k783qc3nnudswp67znrydjtv7ta56c9cpc0nmjmv7rszs568gqdz3w77zqqfeycsgl2kawxcl0zckye09kpsmn54c3zgsztw845uxymh24g4zw9s45ef8p3yjwmfqw35x2grtd9hxw2qyv2sqd032ypge282v20ysgq6lpv5nmjwlrs88jeepxsc2upa970snfnfnxff5ztqzpcgzuqsq0vcpgpcyqqzpy02klq9svqqgavadc6y5tmmqzvsv484ku5nw43vumxuflvsrsgr345pnuh6zq6pz2cy8wra8vujs23y5yhd4gwslns3m7qm9023hc8cyq6knwqxzve9r5kpufq3szuhn9f437cj05az5kqnsl9wefhfnwzenf5z68qh5jj48rmku97u0gzdm2wlkuwrylpvqfttdtw972cwdteal6qfhqvqsyqlaqyusq") + assert(invoice.toString == "lni1qvsyxjtl6luzd9t3pr62xr7eemp6awnejusgf6gw45q75vcfqqqqqqqyyz9ut9uduhtztjgpxm06394g5qkw7v79g4czw6zxsl3lnrsvljj0qzqrq83yqzscd9h8vmmfvdjjqamfw35zqmtpdeujqenfv4kxgucvqgqsq9qgv93kjmn39e3k783qc3nnudswp67znrydjtv7ta56c9cpc0nmjmv7rszs568gqdz3w77zqqfeycsgl2kawxcl0zckye09kpsmn54c3zgsztw845uxymh24g4zw9s45ef8p3yjwmfqw35x2grtd9hxw2qyv2sqd032ypge282v20ysgq6lpv5nmjwlrs88jeepxsc2upa970snfnfnxff5ztqzpcgzuqsq0vcpgpcyqqzpy02klq9svqqgavadc6y5tmmqzvsv484ku5nw43vumxuflvsrsgr345pnuh6zq6pz2cy8wra8vujs23y5yhd4gwslns3m7qm9023hc8cyq6knwqxzve9r5kpufq3szuhn9f437cj05az5kqnsl9wefhfnwzenf5z68qh5jj48rmku97u0gzdm2wlkuwrylpvqfttdtw972cwdteal6qfhqvqsyqlaqyusq") val Success(codedDecoded) = Bolt12Invoice.fromString(invoice.toString) - assert(codedDecoded.chain === chain) - assert(codedDecoded.offerId === Some(offerId)) - assert(codedDecoded.amount === amount) - assert(codedDecoded.description === Left(description)) - assert(codedDecoded.features === features) - assert(codedDecoded.issuer === Some(issuer)) - assert(codedDecoded.nodeId.value.drop(1) === nodeKey.publicKey.value.drop(1)) - assert(codedDecoded.quantity === Some(quantity)) - assert(codedDecoded.payerKey === Some(payerKey)) - assert(codedDecoded.payerNote === Some(payerNote)) - assert(codedDecoded.payerInfo === Some(payerInfo)) - assert(codedDecoded.createdAt === createdAt) - assert(codedDecoded.paymentHash === paymentHash) - assert(codedDecoded.relativeExpiry === relativeExpiry.seconds) - assert(codedDecoded.minFinalCltvExpiryDelta === cltv) - assert(codedDecoded.fallbacks === Some(fallbacks)) - assert(codedDecoded.replaceInvoice === Some(replaceInvoice)) - assert(codedDecoded.records.unknown.toSet === Set(GenericTlv(UInt64(311), hex"010203"), GenericTlv(UInt64(313), hex""))) + assert(codedDecoded.chain == chain) + assert(codedDecoded.offerId == Some(offerId)) + assert(codedDecoded.amount == amount) + assert(codedDecoded.description == Left(description)) + assert(codedDecoded.features == features) + assert(codedDecoded.issuer == Some(issuer)) + assert(codedDecoded.nodeId.value.drop(1) == nodeKey.publicKey.value.drop(1)) + assert(codedDecoded.quantity == Some(quantity)) + assert(codedDecoded.payerKey == Some(payerKey)) + assert(codedDecoded.payerNote == Some(payerNote)) + assert(codedDecoded.payerInfo == Some(payerInfo)) + assert(codedDecoded.createdAt == createdAt) + assert(codedDecoded.paymentHash == paymentHash) + assert(codedDecoded.relativeExpiry == relativeExpiry.seconds) + assert(codedDecoded.minFinalCltvExpiryDelta == cltv) + assert(codedDecoded.fallbacks == Some(fallbacks)) + assert(codedDecoded.replaceInvoice == Some(replaceInvoice)) + assert(codedDecoded.records.unknown.toSet == Set(GenericTlv(UInt64(311), hex"010203"), GenericTlv(UInt64(313), hex""))) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartHandlerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartHandlerSpec.scala index 8409805683..52495bbec0 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartHandlerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartHandlerSpec.scala @@ -89,19 +89,19 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike val invoice = sender.expectMsgType[Invoice] val incoming = nodeParams.db.payments.getIncomingPayment(invoice.paymentHash) assert(incoming.isDefined) - assert(incoming.get.status === IncomingPaymentStatus.Pending) + assert(incoming.get.status == IncomingPaymentStatus.Pending) assert(!incoming.get.invoice.isExpired()) - assert(Crypto.sha256(incoming.get.paymentPreimage) === invoice.paymentHash) + assert(Crypto.sha256(incoming.get.paymentPreimage) == invoice.paymentHash) val add = UpdateAddHtlc(ByteVector32.One, 0, amountMsat, invoice.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket) sender.send(handlerWithoutMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createSinglePartPayload(add.amountMsat, add.cltvExpiry, invoice.paymentSecret.get, invoice.paymentMetadata))) register.expectMsgType[Register.Forward[CMD_FULFILL_HTLC]] val paymentReceived = eventListener.expectMsgType[PaymentReceived] - assert(paymentReceived.copy(parts = paymentReceived.parts.map(_.copy(timestamp = 0 unixms))) === PaymentReceived(add.paymentHash, PartialPayment(amountMsat, add.channelId, timestamp = 0 unixms) :: Nil)) + assert(paymentReceived.copy(parts = paymentReceived.parts.map(_.copy(timestamp = 0 unixms))) == PaymentReceived(add.paymentHash, PartialPayment(amountMsat, add.channelId, timestamp = 0 unixms) :: Nil)) val received = nodeParams.db.payments.getIncomingPayment(invoice.paymentHash) assert(received.isDefined && received.get.status.isInstanceOf[IncomingPaymentStatus.Received]) - assert(received.get.status.asInstanceOf[IncomingPaymentStatus.Received].copy(receivedAt = 0 unixms) === IncomingPaymentStatus.Received(amountMsat, 0 unixms)) + assert(received.get.status.asInstanceOf[IncomingPaymentStatus.Received].copy(receivedAt = 0 unixms) == IncomingPaymentStatus.Received(amountMsat, 0 unixms)) sender.expectNoMessage(50 millis) } @@ -110,17 +110,17 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike sender.send(handlerWithMpp, ReceivePayment(Some(amountMsat), Left("another coffee with multi-part"))) val invoice = sender.expectMsgType[Invoice] assert(invoice.features.hasFeature(Features.BasicMultiPartPayment)) - assert(nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).get.status === IncomingPaymentStatus.Pending) + assert(nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).get.status == IncomingPaymentStatus.Pending) val add = UpdateAddHtlc(ByteVector32.One, 0, amountMsat, invoice.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket) sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createSinglePartPayload(add.amountMsat, add.cltvExpiry, invoice.paymentSecret.get, invoice.paymentMetadata))) register.expectMsgType[Register.Forward[CMD_FULFILL_HTLC]] val paymentReceived = eventListener.expectMsgType[PaymentReceived] - assert(paymentReceived.copy(parts = paymentReceived.parts.map(_.copy(timestamp = 0 unixms))) === PaymentReceived(add.paymentHash, PartialPayment(amountMsat, add.channelId, timestamp = 0 unixms) :: Nil)) + assert(paymentReceived.copy(parts = paymentReceived.parts.map(_.copy(timestamp = 0 unixms))) == PaymentReceived(add.paymentHash, PartialPayment(amountMsat, add.channelId, timestamp = 0 unixms) :: Nil)) val received = nodeParams.db.payments.getIncomingPayment(invoice.paymentHash) assert(received.isDefined && received.get.status.isInstanceOf[IncomingPaymentStatus.Received]) - assert(received.get.status.asInstanceOf[IncomingPaymentStatus.Received].copy(receivedAt = 0 unixms) === IncomingPaymentStatus.Received(amountMsat, 0 unixms)) + assert(received.get.status.asInstanceOf[IncomingPaymentStatus.Received].copy(receivedAt = 0 unixms) == IncomingPaymentStatus.Received(amountMsat, 0 unixms)) sender.expectNoMessage(50 millis) } @@ -128,13 +128,13 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike { sender.send(handlerWithMpp, ReceivePayment(Some(amountMsat), Left("bad expiry"))) val invoice = sender.expectMsgType[Invoice] - assert(nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).get.status === IncomingPaymentStatus.Pending) + assert(nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).get.status == IncomingPaymentStatus.Pending) val add = UpdateAddHtlc(ByteVector32.One, 0, amountMsat, invoice.paymentHash, CltvExpiryDelta(3).toCltvExpiry(nodeParams.currentBlockHeight), TestConstants.emptyOnionPacket) sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createSinglePartPayload(add.amountMsat, add.cltvExpiry, invoice.paymentSecret.get, invoice.paymentMetadata))) val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message assert(cmd.reason == Right(IncorrectOrUnknownPaymentDetails(amountMsat, nodeParams.currentBlockHeight))) - assert(nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).get.status === IncomingPaymentStatus.Pending) + assert(nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).get.status == IncomingPaymentStatus.Pending) eventListener.expectNoMessage(100 milliseconds) sender.expectNoMessage(50 millis) @@ -173,13 +173,13 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike sender.send(handlerWithMpp, ReceivePayment(Some(42000 msat), Left("1 coffee"))) val pr1 = sender.expectMsgType[Invoice] - assert(pr1.minFinalCltvExpiryDelta === nodeParams.channelConf.minFinalExpiryDelta) - assert(pr1.relativeExpiry === Alice.nodeParams.invoiceExpiry) + assert(pr1.minFinalCltvExpiryDelta == nodeParams.channelConf.minFinalExpiryDelta) + assert(pr1.relativeExpiry == Alice.nodeParams.invoiceExpiry) sender.send(handlerWithMpp, ReceivePayment(Some(42000 msat), Left("1 coffee with custom expiry"), expirySeconds_opt = Some(60))) val pr2 = sender.expectMsgType[Invoice] - assert(pr2.minFinalCltvExpiryDelta === nodeParams.channelConf.minFinalExpiryDelta) - assert(pr2.relativeExpiry === 60.seconds) + assert(pr2.minFinalCltvExpiryDelta == nodeParams.channelConf.minFinalExpiryDelta) + assert(pr2.relativeExpiry == 60.seconds) } test("Invoice generation with trampoline support") { _ => @@ -227,10 +227,10 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike val route_x_t = extraHop_x_t :: Nil sender.send(handlerWithMpp, ReceivePayment(Some(42000 msat), Left("1 coffee with additional routing info"), extraHops = List(route_x_z, route_x_t))) - assert(sender.expectMsgType[Invoice].routingInfo === Seq(route_x_z, route_x_t)) + assert(sender.expectMsgType[Invoice].routingInfo == Seq(route_x_z, route_x_t)) sender.send(handlerWithMpp, ReceivePayment(Some(42000 msat), Left("1 coffee without routing info"))) - assert(sender.expectMsgType[Invoice].routingInfo === Nil) + assert(sender.expectMsgType[Invoice].routingInfo == Nil) } test("PaymentHandler should reject incoming payments if the invoice is expired") { f => @@ -245,7 +245,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike sender.send(handlerWithoutMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createSinglePartPayload(add.amountMsat, add.cltvExpiry, invoice.paymentSecret.get, invoice.paymentMetadata))) register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]] val Some(incoming) = nodeParams.db.payments.getIncomingPayment(invoice.paymentHash) - assert(incoming.invoice.isExpired() && incoming.status === IncomingPaymentStatus.Expired) + assert(incoming.invoice.isExpired() && incoming.status == IncomingPaymentStatus.Expired) } test("PaymentHandler should reject incoming multi-part payment if the invoice is expired") { f => @@ -261,7 +261,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message assert(cmd.reason == Right(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight))) val Some(incoming) = nodeParams.db.payments.getIncomingPayment(invoice.paymentHash) - assert(incoming.invoice.isExpired() && incoming.status === IncomingPaymentStatus.Expired) + assert(incoming.invoice.isExpired() && incoming.status == IncomingPaymentStatus.Expired) } test("PaymentHandler should reject incoming multi-part payment if the invoice does not allow it") { f => @@ -275,7 +275,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike sender.send(handlerWithoutMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createMultiPartPayload(add.amountMsat, 1000 msat, add.cltvExpiry, invoice.paymentSecret.get, invoice.paymentMetadata))) val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message assert(cmd.reason == Right(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight))) - assert(nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).get.status === IncomingPaymentStatus.Pending) + assert(nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).get.status == IncomingPaymentStatus.Pending) } test("PaymentHandler should reject incoming multi-part payment with an invalid expiry") { f => @@ -290,7 +290,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createMultiPartPayload(add.amountMsat, 1000 msat, add.cltvExpiry, invoice.paymentSecret.get, invoice.paymentMetadata))) val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message assert(cmd.reason == Right(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight))) - assert(nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).get.status === IncomingPaymentStatus.Pending) + assert(nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).get.status == IncomingPaymentStatus.Pending) } test("PaymentHandler should reject incoming multi-part payment with an unknown payment hash") { f => @@ -304,7 +304,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createMultiPartPayload(add.amountMsat, 1000 msat, add.cltvExpiry, invoice.paymentSecret.get, invoice.paymentMetadata))) val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message assert(cmd.reason == Right(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight))) - assert(nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).get.status === IncomingPaymentStatus.Pending) + assert(nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).get.status == IncomingPaymentStatus.Pending) } test("PaymentHandler should reject incoming multi-part payment with a total amount too low") { f => @@ -318,7 +318,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createMultiPartPayload(add.amountMsat, 999 msat, add.cltvExpiry, invoice.paymentSecret.get, invoice.paymentMetadata))) val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message assert(cmd.reason == Right(IncorrectOrUnknownPaymentDetails(999 msat, nodeParams.currentBlockHeight))) - assert(nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).get.status === IncomingPaymentStatus.Pending) + assert(nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).get.status == IncomingPaymentStatus.Pending) } test("PaymentHandler should reject incoming multi-part payment with a total amount too high") { f => @@ -332,7 +332,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createMultiPartPayload(add.amountMsat, 2001 msat, add.cltvExpiry, invoice.paymentSecret.get, invoice.paymentMetadata))) val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message assert(cmd.reason == Right(IncorrectOrUnknownPaymentDetails(2001 msat, nodeParams.currentBlockHeight))) - assert(nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).get.status === IncomingPaymentStatus.Pending) + assert(nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).get.status == IncomingPaymentStatus.Pending) } test("PaymentHandler should reject incoming multi-part payment with an invalid payment secret") { f => @@ -347,7 +347,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createMultiPartPayload(add.amountMsat, 1000 msat, add.cltvExpiry, invoice.paymentSecret.get.reverse, invoice.paymentMetadata))) val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message assert(cmd.reason == Right(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight))) - assert(nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).get.status === IncomingPaymentStatus.Pending) + assert(nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).get.status == IncomingPaymentStatus.Pending) } test("PaymentHandler should handle multi-part payment timeout") { f => @@ -372,7 +372,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike } val commands = f.register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]] :: f.register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]] :: Nil - assert(commands.toSet === Set( + assert(commands.toSet == Set( Register.Forward(ActorRef.noSender, ByteVector32.One, CMD_FAIL_HTLC(0, Right(PaymentTimeout), commit = true)), Register.Forward(ActorRef.noSender, ByteVector32.One, CMD_FAIL_HTLC(1, Right(PaymentTimeout), commit = true)) )) @@ -387,7 +387,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike // The payment should still be pending in DB. val Some(incomingPayment) = nodeParams.db.payments.getIncomingPayment(pr1.paymentHash) - assert(incomingPayment.status === IncomingPaymentStatus.Pending) + assert(incomingPayment.status == IncomingPaymentStatus.Pending) } test("PaymentHandler should handle multi-part payment success") { f => @@ -413,10 +413,10 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike ) val paymentReceived = f.eventListener.expectMsgType[PaymentReceived] - assert(paymentReceived.parts.map(_.copy(timestamp = 0 unixms)).toSet === Set(PartialPayment(800 msat, ByteVector32.One, 0 unixms), PartialPayment(200 msat, ByteVector32.Zeroes, 0 unixms))) + assert(paymentReceived.parts.map(_.copy(timestamp = 0 unixms)).toSet == Set(PartialPayment(800 msat, ByteVector32.One, 0 unixms), PartialPayment(200 msat, ByteVector32.Zeroes, 0 unixms))) val received = nodeParams.db.payments.getIncomingPayment(invoice.paymentHash) assert(received.isDefined && received.get.status.isInstanceOf[IncomingPaymentStatus.Received]) - assert(received.get.status.asInstanceOf[IncomingPaymentStatus.Received].amount === 1000.msat) + assert(received.get.status.asInstanceOf[IncomingPaymentStatus.Received].amount == 1000.msat) awaitCond({ f.sender.send(handler, GetPendingPayments) f.sender.expectMsgType[PendingPayments].paymentHashes.isEmpty @@ -425,9 +425,9 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike // Extraneous HTLCs should be fulfilled. f.sender.send(handler, MultiPartPaymentFSM.ExtraPaymentReceived(invoice.paymentHash, HtlcPart(1000 msat, UpdateAddHtlc(ByteVector32.One, 44, 200 msat, invoice.paymentHash, add1.cltvExpiry, add1.onionRoutingPacket)), None)) f.register.expectMsg(Register.Forward(ActorRef.noSender, ByteVector32.One, CMD_FULFILL_HTLC(44, preimage, commit = true))) - assert(f.eventListener.expectMsgType[PaymentReceived].amount === 200.msat) + assert(f.eventListener.expectMsgType[PaymentReceived].amount == 200.msat) val received2 = nodeParams.db.payments.getIncomingPayment(invoice.paymentHash) - assert(received2.get.status.asInstanceOf[IncomingPaymentStatus.Received].amount === 1200.msat) + assert(received2.get.status.asInstanceOf[IncomingPaymentStatus.Received].amount == 1200.msat) f.sender.send(handler, GetPendingPayments) f.sender.expectMsgType[PendingPayments].paymentHashes.isEmpty @@ -463,11 +463,11 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike ) val paymentReceived = f.eventListener.expectMsgType[PaymentReceived] - assert(paymentReceived.paymentHash === invoice.paymentHash) - assert(paymentReceived.parts.map(_.copy(timestamp = 0 unixms)).toSet === Set(PartialPayment(300 msat, ByteVector32.One, 0 unixms), PartialPayment(700 msat, ByteVector32.Zeroes, 0 unixms))) + assert(paymentReceived.paymentHash == invoice.paymentHash) + assert(paymentReceived.parts.map(_.copy(timestamp = 0 unixms)).toSet == Set(PartialPayment(300 msat, ByteVector32.One, 0 unixms), PartialPayment(700 msat, ByteVector32.Zeroes, 0 unixms))) val received = nodeParams.db.payments.getIncomingPayment(invoice.paymentHash) assert(received.isDefined && received.get.status.isInstanceOf[IncomingPaymentStatus.Received]) - assert(received.get.status.asInstanceOf[IncomingPaymentStatus.Received].amount === 1000.msat) + assert(received.get.status.asInstanceOf[IncomingPaymentStatus.Received].amount == 1000.msat) awaitCond({ f.sender.send(handler, GetPendingPayments) f.sender.expectMsgType[PendingPayments].paymentHashes.isEmpty @@ -483,17 +483,17 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike val paymentSecret = randomBytes32() val payload = FinalTlvPayload(TlvStream(Seq(OnionPaymentPayloadTlv.AmountToForward(amountMsat), OnionPaymentPayloadTlv.OutgoingCltv(defaultExpiry), OnionPaymentPayloadTlv.PaymentData(paymentSecret, 0 msat), OnionPaymentPayloadTlv.KeySend(paymentPreimage)))) - assert(nodeParams.db.payments.getIncomingPayment(paymentHash) === None) + assert(nodeParams.db.payments.getIncomingPayment(paymentHash) == None) val add = UpdateAddHtlc(ByteVector32.One, 0, amountMsat, paymentHash, defaultExpiry, TestConstants.emptyOnionPacket) sender.send(handlerWithKeySend, IncomingPaymentPacket.FinalPacket(add, payload)) register.expectMsgType[Register.Forward[CMD_FULFILL_HTLC]] val paymentReceived = eventListener.expectMsgType[PaymentReceived] - assert(paymentReceived.copy(parts = paymentReceived.parts.map(_.copy(timestamp = 0 unixms))) === PaymentReceived(add.paymentHash, PartialPayment(amountMsat, add.channelId, timestamp = 0 unixms) :: Nil)) + assert(paymentReceived.copy(parts = paymentReceived.parts.map(_.copy(timestamp = 0 unixms))) == PaymentReceived(add.paymentHash, PartialPayment(amountMsat, add.channelId, timestamp = 0 unixms) :: Nil)) val received = nodeParams.db.payments.getIncomingPayment(paymentHash) assert(received.isDefined && received.get.status.isInstanceOf[IncomingPaymentStatus.Received]) - assert(received.get.status.asInstanceOf[IncomingPaymentStatus.Received].copy(receivedAt = 0 unixms) === IncomingPaymentStatus.Received(amountMsat, 0 unixms)) + assert(received.get.status.asInstanceOf[IncomingPaymentStatus.Received].copy(receivedAt = 0 unixms) == IncomingPaymentStatus.Received(amountMsat, 0 unixms)) } test("PaymentHandler should reject KeySend payment when feature is disabled") { f => @@ -505,13 +505,13 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike val paymentSecret = randomBytes32() val payload = FinalTlvPayload(TlvStream(Seq(OnionPaymentPayloadTlv.AmountToForward(amountMsat), OnionPaymentPayloadTlv.OutgoingCltv(defaultExpiry), OnionPaymentPayloadTlv.PaymentData(paymentSecret, 0 msat), OnionPaymentPayloadTlv.KeySend(paymentPreimage)))) - assert(nodeParams.db.payments.getIncomingPayment(paymentHash) === None) + assert(nodeParams.db.payments.getIncomingPayment(paymentHash) == None) val add = UpdateAddHtlc(ByteVector32.One, 0, amountMsat, paymentHash, defaultExpiry, TestConstants.emptyOnionPacket) sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, payload)) f.register.expectMsg(Register.Forward(ActorRef.noSender, add.channelId, CMD_FAIL_HTLC(add.id, Right(IncorrectOrUnknownPaymentDetails(42000 msat, nodeParams.currentBlockHeight)), commit = true))) - assert(nodeParams.db.payments.getIncomingPayment(paymentHash) === None) + assert(nodeParams.db.payments.getIncomingPayment(paymentHash) == None) } test("PaymentHandler should reject incoming payments if the invoice doesn't exist") { f => @@ -519,13 +519,13 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike val paymentHash = randomBytes32() val paymentSecret = randomBytes32() - assert(nodeParams.db.payments.getIncomingPayment(paymentHash) === None) + assert(nodeParams.db.payments.getIncomingPayment(paymentHash) == None) val add = UpdateAddHtlc(ByteVector32.One, 0, 1000 msat, paymentHash, defaultExpiry, TestConstants.emptyOnionPacket) sender.send(handlerWithoutMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createSinglePartPayload(add.amountMsat, add.cltvExpiry, paymentSecret, None))) val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message - assert(cmd.id === add.id) - assert(cmd.reason === Right(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight))) + assert(cmd.id == add.id) + assert(cmd.reason == Right(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight))) } test("PaymentHandler should reject incoming multi-part payment if the invoice doesn't exist") { f => @@ -533,13 +533,13 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike val paymentHash = randomBytes32() val paymentSecret = randomBytes32() - assert(nodeParams.db.payments.getIncomingPayment(paymentHash) === None) + assert(nodeParams.db.payments.getIncomingPayment(paymentHash) == None) val add = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, paymentHash, defaultExpiry, TestConstants.emptyOnionPacket) sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createMultiPartPayload(add.amountMsat, 1000 msat, add.cltvExpiry, paymentSecret, Some(hex"012345")))) val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message - assert(cmd.id === add.id) - assert(cmd.reason === Right(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight))) + assert(cmd.id == add.id) + assert(cmd.reason == Right(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight))) } test("PaymentHandler should fail fulfilling incoming payments if the invoice doesn't exist") { f => @@ -547,13 +547,13 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike val paymentPreimage = randomBytes32() val paymentHash = Crypto.sha256(paymentPreimage) - assert(nodeParams.db.payments.getIncomingPayment(paymentHash) === None) + assert(nodeParams.db.payments.getIncomingPayment(paymentHash) == None) val add = UpdateAddHtlc(ByteVector32.One, 0, 1000 msat, paymentHash, defaultExpiry, TestConstants.emptyOnionPacket) val fulfill = DoFulfill(paymentPreimage, MultiPartPaymentFSM.MultiPartPaymentSucceeded(paymentHash, Queue(HtlcPart(1000 msat, add)))) sender.send(handlerWithoutMpp, fulfill) val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message - assert(cmd.id === add.id) - assert(cmd.reason === Right(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight))) + assert(cmd.id == add.id) + assert(cmd.reason == Right(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight))) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartPaymentFSMSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartPaymentFSMSpec.scala index e66d61754f..f2c2a79e40 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartPaymentFSMSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartPaymentFSMSpec.scala @@ -73,9 +73,9 @@ class MultiPartPaymentFSMSpec extends TestKitBaseClass with AnyFunSuiteLike { parts.foreach(p => f.parent.send(f.handler, p)) val fail = f.parent.expectMsgType[MultiPartPaymentFailed] - assert(fail.paymentHash === paymentHash) - assert(fail.failure === protocol.PaymentTimeout) - assert(fail.parts.toSet === parts.toSet) + assert(fail.paymentHash == paymentHash) + assert(fail.failure == protocol.PaymentTimeout) + assert(fail.parts.toSet == parts.toSet) f.parent.expectNoMessage(50 millis) f.eventListener.expectNoMessage(50 millis) @@ -85,14 +85,14 @@ class MultiPartPaymentFSMSpec extends TestKitBaseClass with AnyFunSuiteLike { val f = createFixture(250 millis, 1000 msat) f.parent.send(f.handler, createMultiPartHtlc(1000 msat, 150 msat, 1)) f.parent.send(f.handler, createMultiPartHtlc(1000 msat, 100 msat, 2)) - assert(f.parent.expectMsgType[MultiPartPaymentFailed].parts.length === 2) + assert(f.parent.expectMsgType[MultiPartPaymentFailed].parts.length == 2) val extraPart = createMultiPartHtlc(1000 msat, 300 msat, 3) f.parent.send(f.handler, extraPart) val fail = f.parent.expectMsgType[ExtraPaymentReceived[PaymentPart]] - assert(fail.paymentHash === paymentHash) - assert(fail.failure === Some(protocol.PaymentTimeout)) - assert(fail.payment === extraPart) + assert(fail.paymentHash == paymentHash) + assert(fail.failure == Some(protocol.PaymentTimeout)) + assert(fail.payment == extraPart) f.parent.expectNoMessage(50 millis) f.eventListener.expectNoMessage(50 millis) @@ -103,9 +103,9 @@ class MultiPartPaymentFSMSpec extends TestKitBaseClass with AnyFunSuiteLike { f.parent.send(f.handler, createMultiPartHtlc(1000 msat, 600 msat, 1)) f.parent.send(f.handler, createMultiPartHtlc(1100 msat, 650 msat, 2)) val fail = f.parent.expectMsgType[MultiPartPaymentFailed] - assert(fail.paymentHash === paymentHash) - assert(fail.failure === IncorrectOrUnknownPaymentDetails(1100 msat, f.currentBlockHeight)) - assert(fail.parts.length === 2) + assert(fail.paymentHash == paymentHash) + assert(fail.failure == IncorrectOrUnknownPaymentDetails(1100 msat, f.currentBlockHeight)) + assert(fail.parts.length == 2) f.parent.expectNoMessage(50 millis) f.eventListener.expectNoMessage(50 millis) @@ -121,8 +121,8 @@ class MultiPartPaymentFSMSpec extends TestKitBaseClass with AnyFunSuiteLike { parts.foreach(p => f.parent.send(f.handler, p)) val paymentResult = f.parent.expectMsgType[MultiPartPaymentSucceeded] - assert(paymentResult.paymentHash === paymentHash) - assert(paymentResult.parts.toSet === parts.toSet) + assert(paymentResult.paymentHash == paymentHash) + assert(paymentResult.parts.toSet == parts.toSet) f.parent.expectNoMessage(50 millis) f.eventListener.expectNoMessage(50 millis) } @@ -137,8 +137,8 @@ class MultiPartPaymentFSMSpec extends TestKitBaseClass with AnyFunSuiteLike { parts.foreach(p => f.parent.send(f.handler, p)) val paymentResult = f.parent.expectMsgType[MultiPartPaymentSucceeded] - assert(paymentResult.paymentHash === paymentHash) - assert(paymentResult.parts.toSet === parts.toSet) + assert(paymentResult.paymentHash == paymentHash) + assert(paymentResult.parts.toSet == parts.toSet) f.parent.expectNoMessage(50 millis) f.eventListener.expectNoMessage(50 millis) @@ -150,7 +150,7 @@ class MultiPartPaymentFSMSpec extends TestKitBaseClass with AnyFunSuiteLike { f.parent.send(f.handler, part) val paymentResult = f.parent.expectMsgType[MultiPartPaymentSucceeded] - assert(paymentResult.parts === Seq(part)) + assert(paymentResult.parts == Seq(part)) f.parent.expectNoMessage(50 millis) f.eventListener.expectNoMessage(50 millis) @@ -162,7 +162,7 @@ class MultiPartPaymentFSMSpec extends TestKitBaseClass with AnyFunSuiteLike { f.parent.send(f.handler, part) val paymentResult = f.parent.expectMsgType[MultiPartPaymentSucceeded] - assert(paymentResult.parts === Seq(part)) + assert(paymentResult.parts == Seq(part)) f.parent.expectNoMessage(50 millis) f.eventListener.expectNoMessage(50 millis) @@ -177,14 +177,14 @@ class MultiPartPaymentFSMSpec extends TestKitBaseClass with AnyFunSuiteLike { ) parts.foreach(p => f.parent.send(f.handler, p)) val paymentResult = f.parent.expectMsgType[MultiPartPaymentSucceeded] - assert(paymentResult.parts.toSet === parts.toSet) + assert(paymentResult.parts.toSet == parts.toSet) f.parent.expectNoMessage(50 millis) val extra = createMultiPartHtlc(1000 msat, 300 msat, 3) f.parent.send(f.handler, extra) val extraneousPayment = f.parent.expectMsgType[ExtraPaymentReceived[PaymentPart]] - assert(extraneousPayment.paymentHash === paymentHash) - assert(extraneousPayment.payment === extra) + assert(extraneousPayment.paymentHash == paymentHash) + assert(extraneousPayment.payment == extra) f.parent.expectNoMessage(50 millis) f.eventListener.expectNoMessage(50 millis) @@ -203,8 +203,8 @@ class MultiPartPaymentFSMSpec extends TestKitBaseClass with AnyFunSuiteLike { f.parent.send(f.handler, parts(1)) val paymentResult = f.parent.expectMsgType[MultiPartPaymentSucceeded] - assert(paymentResult.paymentHash === paymentHash) - assert(paymentResult.parts.toSet === parts.toSet) + assert(paymentResult.paymentHash == paymentHash) + assert(paymentResult.parts.toSet == parts.toSet) f.parent.expectNoMessage(50 millis) } @@ -219,7 +219,7 @@ class MultiPartPaymentFSMSpec extends TestKitBaseClass with AnyFunSuiteLike { f.parent.send(f.handler, parts(1)) val paymentResult = f.parent.expectMsgType[MultiPartPaymentSucceeded] - assert(paymentResult.parts.toSet === parts.toSet) + assert(paymentResult.parts.toSet == parts.toSet) f.parent.expectNoMessage(50 millis) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartPaymentLifecycleSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartPaymentLifecycleSpec.scala index 634f7e42a1..8b4591a212 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartPaymentLifecycleSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartPaymentLifecycleSpec.scala @@ -78,27 +78,27 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS test("successful first attempt (single part)") { f => import f._ - assert(payFsm.stateName === WAIT_FOR_PAYMENT_REQUEST) + assert(payFsm.stateName == WAIT_FOR_PAYMENT_REQUEST) val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount, expiry, 1, None, routeParams = routeParams.copy(randomize = true)) sender.send(payFsm, payment) router.expectMsg(RouteRequest(nodeParams.nodeId, e, finalAmount, maxFee, routeParams = routeParams.copy(randomize = false), allowMultiPart = true, paymentContext = Some(cfg.paymentContext))) - assert(payFsm.stateName === WAIT_FOR_ROUTES) + assert(payFsm.stateName == WAIT_FOR_ROUTES) val singleRoute = Route(finalAmount, hop_ab_1 :: hop_be :: Nil) router.send(payFsm, RouteResponse(Seq(singleRoute))) val childPayment = childPayFsm.expectMsgType[SendPaymentToRoute] - assert(childPayment.route === Right(singleRoute)) - assert(childPayment.finalPayload.expiry === expiry) - assert(childPayment.finalPayload.paymentSecret === payment.paymentSecret) - assert(childPayment.finalPayload.amount === finalAmount) - assert(childPayment.finalPayload.totalAmount === finalAmount) - assert(payFsm.stateName === PAYMENT_IN_PROGRESS) + assert(childPayment.route == Right(singleRoute)) + assert(childPayment.finalPayload.expiry == expiry) + assert(childPayment.finalPayload.paymentSecret == payment.paymentSecret) + assert(childPayment.finalPayload.amount == finalAmount) + assert(childPayment.finalPayload.totalAmount == finalAmount) + assert(payFsm.stateName == PAYMENT_IN_PROGRESS) val result = fulfillPendingPayments(f, 1) - assert(result.amountWithFees === finalAmount + 100.msat) - assert(result.trampolineFees === 0.msat) - assert(result.nonTrampolineFees === 100.msat) + assert(result.amountWithFees == finalAmount + 100.msat) + assert(result.trampolineFees == 0.msat) + assert(result.nonTrampolineFees == 100.msat) val metrics = metricsListener.expectMsgType[PathFindingExperimentMetrics] assert(metrics.status == "SUCCESS") @@ -111,12 +111,12 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS test("successful first attempt (multiple parts)") { f => import f._ - assert(payFsm.stateName === WAIT_FOR_PAYMENT_REQUEST) + assert(payFsm.stateName == WAIT_FOR_PAYMENT_REQUEST) val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, 1200000 msat, expiry, 1, Some(hex"012345"), routeParams = routeParams.copy(randomize = false)) sender.send(payFsm, payment) router.expectMsg(RouteRequest(nodeParams.nodeId, e, 1200000 msat, maxFee, routeParams = routeParams.copy(randomize = false), allowMultiPart = true, paymentContext = Some(cfg.paymentContext))) - assert(payFsm.stateName === WAIT_FOR_ROUTES) + assert(payFsm.stateName == WAIT_FOR_ROUTES) val routes = Seq( Route(500000 msat, hop_ab_1 :: hop_be :: Nil), @@ -124,18 +124,18 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS ) router.send(payFsm, RouteResponse(routes)) val childPayments = childPayFsm.expectMsgType[SendPaymentToRoute] :: childPayFsm.expectMsgType[SendPaymentToRoute] :: Nil - assert(childPayments.map(_.route).toSet === routes.map(r => Right(r)).toSet) - assert(childPayments.map(_.finalPayload.expiry).toSet === Set(expiry)) - assert(childPayments.map(_.finalPayload.paymentSecret).toSet === Set(payment.paymentSecret)) - assert(childPayments.map(_.finalPayload.paymentMetadata).toSet === Set(Some(hex"012345"))) - assert(childPayments.map(_.finalPayload.amount).toSet === Set(500000 msat, 700000 msat)) - assert(childPayments.map(_.finalPayload.totalAmount).toSet === Set(1200000 msat)) - assert(payFsm.stateName === PAYMENT_IN_PROGRESS) + assert(childPayments.map(_.route).toSet == routes.map(r => Right(r)).toSet) + assert(childPayments.map(_.finalPayload.expiry).toSet == Set(expiry)) + assert(childPayments.map(_.finalPayload.paymentSecret).toSet == Set(payment.paymentSecret)) + assert(childPayments.map(_.finalPayload.paymentMetadata).toSet == Set(Some(hex"012345"))) + assert(childPayments.map(_.finalPayload.amount).toSet == Set(500000 msat, 700000 msat)) + assert(childPayments.map(_.finalPayload.totalAmount).toSet == Set(1200000 msat)) + assert(payFsm.stateName == PAYMENT_IN_PROGRESS) val result = fulfillPendingPayments(f, 2) - assert(result.amountWithFees === 1200200.msat) - assert(result.trampolineFees === 200000.msat) - assert(result.nonTrampolineFees === 200.msat) + assert(result.amountWithFees == 1200200.msat) + assert(result.trampolineFees == 200000.msat) + assert(result.nonTrampolineFees == 200.msat) val metrics = metricsListener.expectMsgType[PathFindingExperimentMetrics] assert(metrics.status == "SUCCESS") @@ -157,12 +157,12 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS router.send(payFsm, RouteResponse(Seq(Route(500000 msat, hop_ab_1 :: hop_be :: Nil), Route(501000 msat, hop_ac_1 :: hop_ce :: Nil)))) val childPayments = childPayFsm.expectMsgType[SendPaymentToRoute] :: childPayFsm.expectMsgType[SendPaymentToRoute] :: Nil childPayments.map(_.finalPayload.asInstanceOf[PaymentOnion.FinalTlvPayload]).foreach(p => { - assert(p.records.get[OnionPaymentPayloadTlv.TrampolineOnion] === Some(trampolineTlv)) - assert(p.records.unknown.toSeq === Seq(userCustomTlv)) + assert(p.records.get[OnionPaymentPayloadTlv.TrampolineOnion] == Some(trampolineTlv)) + assert(p.records.unknown.toSeq == Seq(userCustomTlv)) }) val result = fulfillPendingPayments(f, 2) - assert(result.trampolineFees === 1000.msat) + assert(result.trampolineFees == 1000.msat) val metrics = metricsListener.expectMsgType[PathFindingExperimentMetrics] assert(metrics.status == "SUCCESS") @@ -193,9 +193,9 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS assert(!payFsm.stateData.asInstanceOf[PaymentProgress].pending.contains(childId)) val result = fulfillPendingPayments(f, 2) - assert(result.amountWithFees === 1000200.msat) - assert(result.trampolineFees === 0.msat) - assert(result.nonTrampolineFees === 200.msat) + assert(result.amountWithFees == 1000200.msat) + assert(result.trampolineFees == 0.msat) + assert(result.nonTrampolineFees == 200.msat) val metrics = metricsListener.expectMsgType[PathFindingExperimentMetrics] assert(metrics.status == "SUCCESS") @@ -236,8 +236,8 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS childPayFsm.expectMsgType[SendPaymentToRoute] val result = fulfillPendingPayments(f, 2) - assert(result.amountWithFees === 1000200.msat) - assert(result.nonTrampolineFees === 200.msat) + assert(result.amountWithFees == 1000200.msat) + assert(result.nonTrampolineFees == 200.msat) val metrics = metricsListener.expectMsgType[PathFindingExperimentMetrics] assert(metrics.status == "SUCCESS") @@ -304,7 +304,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS childPayFsm.expectMsgType[SendPaymentToRoute] val result = fulfillPendingPayments(f, 2) - assert(result.amountWithFees === 1000200.msat) + assert(result.amountWithFees == 1000200.msat) val metrics = metricsListener.expectMsgType[PathFindingExperimentMetrics] assert(metrics.status == "SUCCESS") @@ -321,7 +321,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS val routingHint = ExtraHop(b, hop_be.shortChannelId, hop_be.params.relayFees.feeBase, hop_be.params.relayFees.feeProportionalMillionths, hop_be.params.cltvExpiryDelta) val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount, expiry, 3, None, routeParams = routeParams, assistedRoutes = List(List(routingHint))) sender.send(payFsm, payment) - assert(router.expectMsgType[RouteRequest].assistedRoutes.head.head === routingHint) + assert(router.expectMsgType[RouteRequest].assistedRoutes.head.head == routingHint) val route = Route(finalAmount, hop_ab_1 :: hop_be :: Nil) router.send(payFsm, RouteResponse(Seq(route))) childPayFsm.expectMsgType[SendPaymentToRoute] @@ -332,7 +332,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS val childId = payFsm.stateData.asInstanceOf[PaymentProgress].pending.keys.head childPayFsm.send(payFsm, PaymentFailed(childId, paymentHash, Seq(RemoteFailure(route.amount, route.hops, Sphinx.DecryptedFailurePacket(b, FeeInsufficient(finalAmount, channelUpdate)))))) // We update the routing hints accordingly before requesting a new route. - assert(router.expectMsgType[RouteRequest].assistedRoutes.head.head === ExtraHop(b, channelUpdate.shortChannelId, 250 msat, 150, CltvExpiryDelta(24))) + assert(router.expectMsgType[RouteRequest].assistedRoutes.head.head == ExtraHop(b, channelUpdate.shortChannelId, 250 msat, 150, CltvExpiryDelta(24))) } test("retry with ignored routing hints (temporary channel failure)") { f => @@ -342,7 +342,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS val routingHint = ExtraHop(b, hop_be.shortChannelId, hop_be.params.relayFees.feeBase, hop_be.params.relayFees.feeProportionalMillionths, hop_be.params.cltvExpiryDelta) val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount, expiry, 3, None, routeParams = routeParams, assistedRoutes = List(List(routingHint))) sender.send(payFsm, payment) - assert(router.expectMsgType[RouteRequest].assistedRoutes.head.head === routingHint) + assert(router.expectMsgType[RouteRequest].assistedRoutes.head.head == routingHint) val route = Route(finalAmount, hop_ab_1 :: hop_be :: Nil) router.send(payFsm, RouteResponse(Seq(route))) childPayFsm.expectMsgType[SendPaymentToRoute] @@ -356,8 +356,8 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS childPayFsm.send(payFsm, PaymentFailed(childId, paymentHash, Seq(RemoteFailure(route.amount, route.hops, Sphinx.DecryptedFailurePacket(b, TemporaryChannelFailure(channelUpdateBE1)))))) // We update the routing hints accordingly before requesting a new route and ignore the channel. val routeRequest = router.expectMsgType[RouteRequest] - assert(routeRequest.assistedRoutes.head.head === routingHint) - assert(routeRequest.ignore.channels.map(_.shortChannelId) === Set(channelUpdateBE1.shortChannelId)) + assert(routeRequest.assistedRoutes.head.head == routingHint) + assert(routeRequest.ignore.channels.map(_.shortChannelId) == Set(channelUpdateBE1.shortChannelId)) } test("update routing hints") { _ => @@ -380,7 +380,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS Seq(ExtraHop(a, ShortChannelId(1), 10 msat, 0, CltvExpiryDelta(12)), ExtraHop(b, ShortChannelId(2), 15 msat, 150, CltvExpiryDelta(48))), Seq(ExtraHop(a, ShortChannelId(3), 1 msat, 10, CltvExpiryDelta(144))) ) - assert(routingHints1 === PaymentFailure.updateRoutingHints(failures, routingHints)) + assert(routingHints1 == PaymentFailure.updateRoutingHints(failures, routingHints)) } { val failures = Seq( @@ -393,7 +393,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS Seq(ExtraHop(a, ShortChannelId(1), 23 msat, 23, CltvExpiryDelta(23)), ExtraHop(b, ShortChannelId(2), 21 msat, 21, CltvExpiryDelta(21))), Seq(ExtraHop(a, ShortChannelId(3), 22 msat, 22, CltvExpiryDelta(22))) ) - assert(routingHints1 === PaymentFailure.updateRoutingHints(failures, routingHints)) + assert(routingHints1 == PaymentFailure.updateRoutingHints(failures, routingHints)) } } @@ -437,13 +437,13 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS router.send(payFsm, Status.Failure(RouteNotFound)) val result = sender.expectMsgType[PaymentFailed] - assert(result.id === cfg.id) - assert(result.paymentHash === paymentHash) - assert(result.failures === Seq(LocalFailure(finalAmount, Nil, RouteNotFound))) + assert(result.id == cfg.id) + assert(result.paymentHash == paymentHash) + assert(result.failures == Seq(LocalFailure(finalAmount, Nil, RouteNotFound))) val Some(outgoing) = nodeParams.db.payments.getOutgoingPayment(cfg.id) assert(outgoing.status.isInstanceOf[OutgoingPaymentStatus.Failed]) - assert(outgoing.status.asInstanceOf[OutgoingPaymentStatus.Failed].failures === Seq(FailureSummary(FailureType.LOCAL, RouteNotFound.getMessage, Nil, None))) + assert(outgoing.status.asInstanceOf[OutgoingPaymentStatus.Failed].failures == Seq(FailureSummary(FailureType.LOCAL, RouteNotFound.getMessage, Nil, None))) sender.expectTerminated(payFsm) sender.expectNoMessage(100 millis) @@ -469,7 +469,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS val (failedId, failedRoute) = payFsm.stateData.asInstanceOf[PaymentProgress].pending.head val result = abortAfterFailure(f, PaymentFailed(failedId, paymentHash, Seq(RemoteFailure(failedRoute.amount, failedRoute.hops, Sphinx.DecryptedFailurePacket(e, IncorrectOrUnknownPaymentDetails(600000 msat, BlockHeight(0))))))) - assert(result.failures.length === 1) + assert(result.failures.length == 1) val metrics = metricsListener.expectMsgType[PathFindingExperimentMetrics] assert(metrics.status == "FAILURE") @@ -490,7 +490,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS val (failedId, failedRoute) = payFsm.stateData.asInstanceOf[PaymentProgress].pending.head val result = abortAfterFailure(f, PaymentFailed(failedId, paymentHash, Seq(LocalFailure(failedRoute.amount, failedRoute.hops, HtlcsTimedoutDownstream(channelId = ByteVector32.One, htlcs = Set.empty))))) - assert(result.failures.length === 1) + assert(result.failures.length == 1) } test("abort if recipient sends error during retry") { f => @@ -508,7 +508,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS router.expectMsgType[RouteRequest] val result = abortAfterFailure(f, PaymentFailed(failedId2, paymentHash, Seq(RemoteFailure(failedRoute2.amount, failedRoute2.hops, Sphinx.DecryptedFailurePacket(e, PaymentTimeout))))) - assert(result.failures.length === 2) + assert(result.failures.length == 2) } test("receive partial success after retriable failure (recipient spec violation)") { f => @@ -527,8 +527,8 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS val result = fulfillPendingPayments(f, 1) assert(result.amountWithFees < finalAmount) // we got the preimage without paying the full amount - assert(result.nonTrampolineFees === successRoute.fee(false)) // we paid the fee for only one of the partial payments - assert(result.parts.length === 1 && result.parts.head.id === successId) + assert(result.nonTrampolineFees == successRoute.fee(false)) // we paid the fee for only one of the partial payments + assert(result.parts.length == 1 && result.parts.head.id == successId) } test("receive partial success after abort (recipient spec violation)") { f => @@ -543,20 +543,20 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS val (failedId, failedRoute) :: (successId, successRoute) :: Nil = payFsm.stateData.asInstanceOf[PaymentProgress].pending.toSeq childPayFsm.send(payFsm, PaymentFailed(failedId, paymentHash, Seq(RemoteFailure(failedRoute.amount, failedRoute.hops, Sphinx.DecryptedFailurePacket(e, PaymentTimeout))))) - awaitCond(payFsm.stateName === PAYMENT_ABORTED) + awaitCond(payFsm.stateName == PAYMENT_ABORTED) sender.watch(payFsm) childPayFsm.send(payFsm, PaymentSent(cfg.id, paymentHash, paymentPreimage, finalAmount, e, Seq(PaymentSent.PartialPayment(successId, successRoute.amount, successRoute.fee(false), randomBytes32(), Some(successRoute.hops))))) sender.expectMsg(PreimageReceived(paymentHash, paymentPreimage)) val result = sender.expectMsgType[PaymentSent] - assert(result.id === cfg.id) - assert(result.paymentHash === paymentHash) - assert(result.paymentPreimage === paymentPreimage) - assert(result.parts.length === 1 && result.parts.head.id === successId) - assert(result.recipientAmount === finalAmount) - assert(result.recipientNodeId === finalRecipient) + assert(result.id == cfg.id) + assert(result.paymentHash == paymentHash) + assert(result.paymentPreimage == paymentPreimage) + assert(result.parts.length == 1 && result.parts.head.id == successId) + assert(result.recipientAmount == finalAmount) + assert(result.recipientNodeId == finalRecipient) assert(result.amountWithFees < finalAmount) // we got the preimage without paying the full amount - assert(result.nonTrampolineFees === successRoute.fee(false)) // we paid the fee for only one of the partial payments + assert(result.nonTrampolineFees == successRoute.fee(false)) // we paid the fee for only one of the partial payments sender.expectTerminated(payFsm) sender.expectNoMessage(100 millis) @@ -577,14 +577,14 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS val (childId, route) :: (failedId, failedRoute) :: Nil = payFsm.stateData.asInstanceOf[PaymentProgress].pending.toSeq childPayFsm.send(payFsm, PaymentSent(cfg.id, paymentHash, paymentPreimage, finalAmount, e, Seq(PaymentSent.PartialPayment(childId, route.amount, route.fee(false), randomBytes32(), Some(route.hops))))) sender.expectMsg(PreimageReceived(paymentHash, paymentPreimage)) - awaitCond(payFsm.stateName === PAYMENT_SUCCEEDED) + awaitCond(payFsm.stateName == PAYMENT_SUCCEEDED) sender.watch(payFsm) childPayFsm.send(payFsm, PaymentFailed(failedId, paymentHash, Seq(RemoteFailure(failedRoute.amount, failedRoute.hops, Sphinx.DecryptedFailurePacket(e, PaymentTimeout))))) val result = sender.expectMsgType[PaymentSent] - assert(result.parts.length === 1 && result.parts.head.id === childId) + assert(result.parts.length == 1 && result.parts.head.id == childId) assert(result.amountWithFees < finalAmount) // we got the preimage without paying the full amount - assert(result.nonTrampolineFees === route.fee(false)) // we paid the fee for only one of the partial payments + assert(result.nonTrampolineFees == route.fee(false)) // we paid the fee for only one of the partial payments sender.expectTerminated(payFsm) sender.expectNoMessage(100 millis) @@ -597,7 +597,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS sender.watch(payFsm) val pending = payFsm.stateData.asInstanceOf[PaymentProgress].pending - assert(pending.size === childCount) + assert(pending.size == childCount) val partialPayments = pending.map { case (childId, route) => PaymentSent.PartialPayment(childId, route.amount, route.fee(false), randomBytes32(), Some(route.hops)) @@ -605,12 +605,12 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS partialPayments.foreach(pp => childPayFsm.send(payFsm, PaymentSent(cfg.id, paymentHash, paymentPreimage, finalAmount, e, Seq(pp)))) sender.expectMsg(PreimageReceived(paymentHash, paymentPreimage)) val result = sender.expectMsgType[PaymentSent] - assert(result.id === cfg.id) - assert(result.paymentHash === paymentHash) - assert(result.paymentPreimage === paymentPreimage) - assert(result.parts.toSet === partialPayments.toSet) - assert(result.recipientAmount === finalAmount) - assert(result.recipientNodeId === finalRecipient) + assert(result.id == cfg.id) + assert(result.paymentHash == paymentHash) + assert(result.paymentPreimage == paymentPreimage) + assert(result.parts.toSet == partialPayments.toSet) + assert(result.recipientAmount == finalAmount) + assert(result.recipientNodeId == finalRecipient) sender.expectTerminated(payFsm) sender.expectNoMessage(100 millis) @@ -627,8 +627,8 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS val pending = payFsm.stateData.asInstanceOf[PaymentProgress].pending childPayFsm.send(payFsm, childFailure) // this failure should trigger an abort if (pending.size > 1) { - awaitCond(payFsm.stateName === PAYMENT_ABORTED) - assert(payFsm.stateData.asInstanceOf[PaymentAborted].pending.size === pending.size - 1) + awaitCond(payFsm.stateName == PAYMENT_ABORTED) + assert(payFsm.stateData.asInstanceOf[PaymentAborted].pending.size == pending.size - 1) // Fail all remaining child payments. payFsm.stateData.asInstanceOf[PaymentAborted].pending.foreach(childId => childPayFsm.send(payFsm, PaymentFailed(childId, paymentHash, Seq(RemoteFailure(pending(childId).amount, hop_ab_1 :: hop_be :: Nil, Sphinx.DecryptedFailurePacket(e, PaymentTimeout))))) @@ -636,8 +636,8 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS } val result = sender.expectMsgType[PaymentFailed] - assert(result.id === cfg.id) - assert(result.paymentHash === paymentHash) + assert(result.id == cfg.id) + assert(result.paymentHash == paymentHash) assert(result.failures.nonEmpty) sender.expectTerminated(payFsm) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentInitiatorSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentInitiatorSpec.scala index 19ee4ff7ad..a72817e183 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentInitiatorSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentInitiatorSpec.scala @@ -135,9 +135,9 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike sender.send(initiator, req) val id = sender.expectMsgType[UUID] val fail = sender.expectMsgType[PaymentFailed] - assert(fail.id === id) + assert(fail.id == id) assert(fail.failures.head.isInstanceOf[LocalFailure]) - assert(fail.failures.head.asInstanceOf[LocalFailure].t === UnsupportedFeatures(invoice.features)) + assert(fail.failures.head.asInstanceOf[LocalFailure].t == UnsupportedFeatures(invoice.features)) } test("forward payment with pre-defined route") { f => @@ -170,7 +170,7 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike val finalExpiryDelta = CltvExpiryDelta(24) val invoice = Bolt11Invoice(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, priv_c.privateKey, Left("Some MPP invoice"), finalExpiryDelta, features = featuresWithMpp) val req = SendPaymentToNode(finalAmount, invoice, 1, routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams) - assert(req.finalExpiry(nodeParams.currentBlockHeight) === (finalExpiryDelta + 1).toCltvExpiry(nodeParams.currentBlockHeight)) + assert(req.finalExpiry(nodeParams.currentBlockHeight) == (finalExpiryDelta + 1).toCltvExpiry(nodeParams.currentBlockHeight)) sender.send(initiator, req) val id = sender.expectMsgType[UUID] payFsm.expectMsg(SendPaymentConfig(id, id, None, paymentHash, finalAmount, c, Upstream.Local(id), Some(invoice), storeInDb = true, publishEvent = true, recordPathFindingMetrics = true, Nil)) @@ -222,12 +222,12 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike val payment = sender.expectMsgType[SendPaymentToRouteResponse] payFsm.expectMsg(SendPaymentConfig(payment.paymentId, payment.parentId, None, paymentHash, finalAmount, c, Upstream.Local(payment.paymentId), Some(invoice), storeInDb = true, publishEvent = true, recordPathFindingMetrics = false, Nil)) val msg = payFsm.expectMsgType[PaymentLifecycle.SendPaymentToRoute] - assert(msg.replyTo === initiator) - assert(msg.route === Left(route)) - assert(msg.finalPayload.amount === finalAmount / 2) - assert(msg.finalPayload.expiry === req.finalExpiry(nodeParams.currentBlockHeight)) - assert(msg.finalPayload.paymentSecret === invoice.paymentSecret.get) - assert(msg.finalPayload.totalAmount === finalAmount) + assert(msg.replyTo == initiator) + assert(msg.route == Left(route)) + assert(msg.finalPayload.amount == finalAmount / 2) + assert(msg.finalPayload.expiry == req.finalExpiry(nodeParams.currentBlockHeight)) + assert(msg.finalPayload.paymentSecret == invoice.paymentSecret.get) + assert(msg.finalPayload.totalAmount == finalAmount) sender.send(initiator, GetPayment(Left(payment.paymentId))) sender.expectMsg(PaymentIsPending(payment.paymentId, invoice.paymentHash, PendingPaymentToRoute(sender.ref, req))) @@ -260,33 +260,33 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike val msg = multiPartPayFsm.expectMsgType[SendMultiPartPayment] assert(msg.paymentSecret !== invoice.paymentSecret.get) // we should not leak the invoice secret to the trampoline node - assert(msg.targetNodeId === b) - assert(msg.targetExpiry.toLong === currentBlockCount + 9 + 12 + 1) - assert(msg.totalAmount === finalAmount + trampolineFees) + assert(msg.targetNodeId == b) + assert(msg.targetExpiry.toLong == currentBlockCount + 9 + 12 + 1) + assert(msg.totalAmount == finalAmount + trampolineFees) assert(msg.additionalTlvs.head.isInstanceOf[OnionPaymentPayloadTlv.TrampolineOnion]) - assert(msg.maxAttempts === nodeParams.maxPaymentAttempts) + assert(msg.maxAttempts == nodeParams.maxPaymentAttempts) // Verify that the trampoline node can correctly peel the trampoline onion. val trampolineOnion = msg.additionalTlvs.head.asInstanceOf[OnionPaymentPayloadTlv.TrampolineOnion].packet val Right(decrypted) = Sphinx.peel(priv_b.privateKey, Some(invoice.paymentHash), trampolineOnion) assert(!decrypted.isLastPacket) val trampolinePayload = PaymentOnionCodecs.nodeRelayPerHopPayloadCodec.decode(decrypted.payload.bits).require.value - assert(trampolinePayload.amountToForward === finalAmount) - assert(trampolinePayload.totalAmount === finalAmount) - assert(trampolinePayload.outgoingCltv.toLong === currentBlockCount + 9 + 1) - assert(trampolinePayload.outgoingNodeId === c) - assert(trampolinePayload.paymentSecret === None) // we're not leaking the invoice secret to the trampoline node - assert(trampolinePayload.invoiceRoutingInfo === None) - assert(trampolinePayload.invoiceFeatures === None) + assert(trampolinePayload.amountToForward == finalAmount) + assert(trampolinePayload.totalAmount == finalAmount) + assert(trampolinePayload.outgoingCltv.toLong == currentBlockCount + 9 + 1) + assert(trampolinePayload.outgoingNodeId == c) + assert(trampolinePayload.paymentSecret == None) // we're not leaking the invoice secret to the trampoline node + assert(trampolinePayload.invoiceRoutingInfo == None) + assert(trampolinePayload.invoiceFeatures == None) // Verify that the recipient can correctly peel the trampoline onion. val Right(decrypted1) = Sphinx.peel(priv_c.privateKey, Some(invoice.paymentHash), decrypted.nextPacket) assert(decrypted1.isLastPacket) val finalPayload = PaymentOnionCodecs.finalPerHopPayloadCodec.decode(decrypted1.payload.bits).require.value - assert(finalPayload.amount === finalAmount) - assert(finalPayload.totalAmount === finalAmount) - assert(finalPayload.expiry.toLong === currentBlockCount + 9 + 1) - assert(finalPayload.paymentSecret === invoice.paymentSecret.get) + assert(finalPayload.amount == finalAmount) + assert(finalPayload.totalAmount == finalAmount) + assert(finalPayload.expiry.toLong == currentBlockCount + 9 + 1) + assert(finalPayload.paymentSecret == invoice.paymentSecret.get) } test("forward trampoline to legacy payment") { f => @@ -300,9 +300,9 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike val msg = multiPartPayFsm.expectMsgType[SendMultiPartPayment] assert(msg.paymentSecret !== invoice.paymentSecret.get) // we should not leak the invoice secret to the trampoline node - assert(msg.targetNodeId === b) - assert(msg.targetExpiry.toLong === currentBlockCount + 9 + 12 + 1) - assert(msg.totalAmount === finalAmount + trampolineFees) + assert(msg.targetNodeId == b) + assert(msg.targetExpiry.toLong == currentBlockCount + 9 + 12 + 1) + assert(msg.totalAmount == finalAmount + trampolineFees) assert(msg.additionalTlvs.head.isInstanceOf[OnionPaymentPayloadTlv.TrampolineOnion]) // Verify that the trampoline node can correctly peel the trampoline onion. @@ -310,12 +310,12 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike val Right(decrypted) = Sphinx.peel(priv_b.privateKey, Some(invoice.paymentHash), trampolineOnion) assert(!decrypted.isLastPacket) val trampolinePayload = PaymentOnionCodecs.nodeRelayPerHopPayloadCodec.decode(decrypted.payload.bits).require.value - assert(trampolinePayload.amountToForward === finalAmount) - assert(trampolinePayload.totalAmount === finalAmount) - assert(trampolinePayload.outgoingCltv.toLong === currentBlockCount + 9 + 1) - assert(trampolinePayload.outgoingNodeId === c) - assert(trampolinePayload.paymentSecret === invoice.paymentSecret) - assert(trampolinePayload.invoiceFeatures === Some(hex"4100")) // var_onion_optin, payment_secret + assert(trampolinePayload.amountToForward == finalAmount) + assert(trampolinePayload.totalAmount == finalAmount) + assert(trampolinePayload.outgoingCltv.toLong == currentBlockCount + 9 + 1) + assert(trampolinePayload.outgoingNodeId == c) + assert(trampolinePayload.paymentSecret == invoice.paymentSecret) + assert(trampolinePayload.invoiceFeatures == Some(hex"4100")) // var_onion_optin, payment_secret } test("reject trampoline to legacy payment for 0-value invoice") { f => @@ -328,8 +328,8 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike sender.send(initiator, req) val id = sender.expectMsgType[UUID] val fail = sender.expectMsgType[PaymentFailed] - assert(fail.id === id) - assert(fail.failures === LocalFailure(finalAmount, Nil, PaymentError.TrampolineLegacyAmountLessInvoice) :: Nil) + assert(fail.id == id) + assert(fail.failures == LocalFailure(finalAmount, Nil, PaymentError.TrampolineLegacyAmountLessInvoice) :: Nil) multiPartPayFsm.expectNoMessage(50 millis) payFsm.expectNoMessage(50 millis) @@ -344,9 +344,9 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike sender.send(initiator, req) val id = sender.expectMsgType[UUID] val fail = sender.expectMsgType[PaymentFailed] - assert(fail.id === id) - assert(fail.failures.length === 1) - assert(fail.failures.head.asInstanceOf[LocalFailure].t.getMessage === "requirement failed: packet per-hop payloads cannot exceed 400 bytes") + assert(fail.id == id) + assert(fail.failures.length == 1) + assert(fail.failures.head.asInstanceOf[LocalFailure].t.getMessage == "requirement failed: packet per-hop payloads cannot exceed 400 bytes") } test("retry trampoline payment") { f => @@ -361,7 +361,7 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike assert(!cfg.publishEvent) val msg1 = multiPartPayFsm.expectMsgType[SendMultiPartPayment] - assert(msg1.totalAmount === finalAmount + 21000.msat) + assert(msg1.totalAmount == finalAmount + 21000.msat) sender.send(initiator, GetPayment(Left(id))) sender.expectMsgType[PaymentIsPending] @@ -370,7 +370,7 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike multiPartPayFsm.send(initiator, PaymentFailed(cfg.parentId, invoice.paymentHash, Seq(RemoteFailure(msg1.totalAmount, Nil, Sphinx.DecryptedFailurePacket(b, TrampolineFeeInsufficient))))) multiPartPayFsm.expectMsgType[SendPaymentConfig] val msg2 = multiPartPayFsm.expectMsgType[SendMultiPartPayment] - assert(msg2.totalAmount === finalAmount + 25000.msat) + assert(msg2.totalAmount == finalAmount + 25000.msat) // Simulate success which should publish the event and respond to the original sender. val success = PaymentSent(cfg.parentId, invoice.paymentHash, randomBytes32(), finalAmount, c, Seq(PaymentSent.PartialPayment(UUID.randomUUID(), 1000 msat, 500 msat, randomBytes32(), None))) @@ -396,13 +396,13 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike assert(!cfg.publishEvent) val msg1 = multiPartPayFsm.expectMsgType[SendMultiPartPayment] - assert(msg1.totalAmount === finalAmount + 21000.msat) + assert(msg1.totalAmount == finalAmount + 21000.msat) // Simulate a failure which should trigger a retry. multiPartPayFsm.send(initiator, PaymentFailed(cfg.parentId, invoice.paymentHash, Seq(RemoteFailure(msg1.totalAmount, Nil, Sphinx.DecryptedFailurePacket(b, TrampolineFeeInsufficient))))) multiPartPayFsm.expectMsgType[SendPaymentConfig] val msg2 = multiPartPayFsm.expectMsgType[SendMultiPartPayment] - assert(msg2.totalAmount === finalAmount + 25000.msat) + assert(msg2.totalAmount == finalAmount + 25000.msat) // Simulate a failure that exhausts payment attempts. val failed = PaymentFailed(cfg.parentId, invoice.paymentHash, Seq(RemoteFailure(msg2.totalAmount, Nil, Sphinx.DecryptedFailurePacket(b, TemporaryNodeFailure)))) @@ -423,18 +423,18 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike val cfg = multiPartPayFsm.expectMsgType[SendPaymentConfig] val msg1 = multiPartPayFsm.expectMsgType[SendMultiPartPayment] - assert(msg1.totalAmount === finalAmount + 21000.msat) + assert(msg1.totalAmount == finalAmount + 21000.msat) // Trampoline node couldn't find a route for the given fee. val failed = PaymentFailed(cfg.parentId, invoice.paymentHash, Seq(RemoteFailure(msg1.totalAmount, Nil, Sphinx.DecryptedFailurePacket(b, TrampolineFeeInsufficient)))) multiPartPayFsm.send(initiator, failed) multiPartPayFsm.expectMsgType[SendPaymentConfig] val msg2 = multiPartPayFsm.expectMsgType[SendMultiPartPayment] - assert(msg2.totalAmount === finalAmount + 25000.msat) + assert(msg2.totalAmount == finalAmount + 25000.msat) // Trampoline node couldn't find a route even with the increased fee. multiPartPayFsm.send(initiator, failed) val failure = sender.expectMsgType[PaymentFailed] - assert(failure.failures === Seq(LocalFailure(finalAmount, Seq(NodeHop(nodeParams.nodeId, b, nodeParams.channelConf.expiryDelta, 0 msat), NodeHop(b, c, CltvExpiryDelta(24), 25000 msat)), RouteNotFound))) + assert(failure.failures == Seq(LocalFailure(finalAmount, Seq(NodeHop(nodeParams.nodeId, b, nodeParams.channelConf.expiryDelta, 0 msat), NodeHop(b, c, CltvExpiryDelta(24), 25000 msat)), RouteNotFound))) eventListener.expectMsg(failure) sender.expectNoMessage(100 millis) eventListener.expectNoMessage(100 millis) @@ -451,10 +451,10 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike assert(payment.trampolineSecret.nonEmpty) payFsm.expectMsg(SendPaymentConfig(payment.paymentId, payment.parentId, None, paymentHash, finalAmount, c, Upstream.Local(payment.paymentId), Some(invoice), storeInDb = true, publishEvent = true, recordPathFindingMetrics = false, Seq(NodeHop(b, c, CltvExpiryDelta(0), 0 msat)))) val msg = payFsm.expectMsgType[PaymentLifecycle.SendPaymentToRoute] - assert(msg.route === Left(route)) - assert(msg.finalPayload.amount === finalAmount + trampolineFees) - assert(msg.finalPayload.paymentSecret === payment.trampolineSecret.get) - assert(msg.finalPayload.totalAmount === finalAmount + trampolineFees) + assert(msg.route == Left(route)) + assert(msg.finalPayload.amount == finalAmount + trampolineFees) + assert(msg.finalPayload.paymentSecret == payment.trampolineSecret.get) + assert(msg.finalPayload.totalAmount == finalAmount + trampolineFees) assert(msg.finalPayload.isInstanceOf[PaymentOnion.FinalTlvPayload]) val trampolineOnion = msg.finalPayload.asInstanceOf[PaymentOnion.FinalTlvPayload].records.get[OnionPaymentPayloadTlv.TrampolineOnion] assert(trampolineOnion.nonEmpty) @@ -463,10 +463,10 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike val Right(decrypted) = Sphinx.peel(priv_b.privateKey, Some(invoice.paymentHash), trampolineOnion.get.packet) assert(!decrypted.isLastPacket) val trampolinePayload = PaymentOnionCodecs.nodeRelayPerHopPayloadCodec.decode(decrypted.payload.bits).require.value - assert(trampolinePayload.amountToForward === finalAmount) - assert(trampolinePayload.totalAmount === finalAmount) - assert(trampolinePayload.outgoingNodeId === c) - assert(trampolinePayload.paymentSecret === invoice.paymentSecret) + assert(trampolinePayload.amountToForward == finalAmount) + assert(trampolinePayload.totalAmount == finalAmount) + assert(trampolinePayload.outgoingNodeId == c) + assert(trampolinePayload.paymentSecret == invoice.paymentSecret) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala index c0fedefb3a..f766cdc821 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala @@ -112,12 +112,12 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val Transition(_, WAITING_FOR_ROUTE, WAITING_FOR_PAYMENT_COMPLETE) = monitor.expectMsgClass(classOf[Transition[_]]) awaitCond(nodeParams.db.payments.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.Pending)) val Some(outgoing) = nodeParams.db.payments.getOutgoingPayment(id) - assert(outgoing.copy(createdAt = 0 unixms) === OutgoingPayment(id, parentId, Some(defaultExternalId), defaultPaymentHash, PaymentType.Standard, defaultAmountMsat, defaultAmountMsat, d, 0 unixms, Some(defaultInvoice), OutgoingPaymentStatus.Pending)) + assert(outgoing.copy(createdAt = 0 unixms) == OutgoingPayment(id, parentId, Some(defaultExternalId), defaultPaymentHash, PaymentType.Standard, defaultAmountMsat, defaultAmountMsat, d, 0 unixms, Some(defaultInvoice), OutgoingPaymentStatus.Pending)) sender.send(paymentFSM, addCompleted(HtlcResult.RemoteFulfill(UpdateFulfillHtlc(ByteVector32.Zeroes, 0, defaultPaymentPreimage)))) val ps = sender.expectMsgType[PaymentSent] - assert(ps.id === parentId) - assert(ps.parts.head.route === Some(route.hops)) + assert(ps.id == parentId) + assert(ps.parts.head.route == Some(route.hops)) awaitCond(nodeParams.db.payments.getOutgoingPayment(id).exists(_.status.isInstanceOf[OutgoingPaymentStatus.Succeeded])) metricsListener.expectNoMessage() @@ -140,11 +140,11 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val Transition(_, WAITING_FOR_ROUTE, WAITING_FOR_PAYMENT_COMPLETE) = monitor.expectMsgClass(classOf[Transition[_]]) awaitCond(nodeParams.db.payments.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.Pending)) val Some(outgoing) = nodeParams.db.payments.getOutgoingPayment(id) - assert(outgoing.copy(createdAt = 0 unixms) === OutgoingPayment(id, parentId, Some(defaultExternalId), defaultPaymentHash, PaymentType.Standard, defaultAmountMsat, defaultAmountMsat, d, 0 unixms, Some(defaultInvoice), OutgoingPaymentStatus.Pending)) + assert(outgoing.copy(createdAt = 0 unixms) == OutgoingPayment(id, parentId, Some(defaultExternalId), defaultPaymentHash, PaymentType.Standard, defaultAmountMsat, defaultAmountMsat, d, 0 unixms, Some(defaultInvoice), OutgoingPaymentStatus.Pending)) sender.send(paymentFSM, addCompleted(HtlcResult.RemoteFulfill(UpdateFulfillHtlc(ByteVector32.Zeroes, 0, defaultPaymentPreimage)))) val ps = sender.expectMsgType[PaymentSent] - assert(ps.id === parentId) + assert(ps.id == parentId) awaitCond(nodeParams.db.payments.getOutgoingPayment(id).exists(_.status.isInstanceOf[OutgoingPaymentStatus.Succeeded])) metricsListener.expectNoMessage() @@ -201,7 +201,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { sender.send(paymentFSM, addCompleted(HtlcResult.OnChainFulfill(defaultPaymentPreimage))) val ps = sender.expectMsgType[PaymentSent] - assert(ps.id === parentId) + assert(ps.id == parentId) awaitCond(nodeParams.db.payments.getOutgoingPayment(id).exists(_.status.isInstanceOf[OutgoingPaymentStatus.Succeeded])) metricsListener.expectNoMessage() @@ -216,10 +216,10 @@ class PaymentLifecycleSpec extends BaseRouterSpec { sender.send(paymentFSM, request) val Transition(_, WAITING_FOR_REQUEST, WAITING_FOR_ROUTE) = monitor.expectMsgClass(classOf[Transition[_]]) val routeRequest = routerForwarder.expectMsgType[RouteRequest] - awaitCond(nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending)) + awaitCond(nodeParams.db.payments.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.Pending)) routerForwarder.forward(routerFixture.router, routeRequest) - assert(sender.expectMsgType[PaymentFailed].failures === LocalFailure(defaultAmountMsat, Nil, RouteNotFound) :: Nil) + assert(sender.expectMsgType[PaymentFailed].failures == LocalFailure(defaultAmountMsat, Nil, RouteNotFound) :: Nil) awaitCond(nodeParams.db.payments.getOutgoingPayment(id).exists(_.status.isInstanceOf[OutgoingPaymentStatus.Failed])) val metrics = metricsListener.expectMsgType[PathFindingExperimentMetrics] @@ -273,7 +273,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { routerForwarder.forward(routerFixture.router, routeRequest) val pf = sender.expectMsgType[PaymentFailed] - assert(pf.failures.length === 1) + assert(pf.failures.length == 1) assert(pf.failures.head.isInstanceOf[LocalFailure]) awaitCond(nodeParams.db.payments.getOutgoingPayment(id).exists(_.status.isInstanceOf[OutgoingPaymentStatus.Failed])) } @@ -286,7 +286,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata), 2, routeParams = defaultRouteParams) sender.send(paymentFSM, request) routerForwarder.expectMsg(defaultRouteRequest(a, d, cfg)) - awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending)) + awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.Pending)) val WaitingForRoute(_, Nil, _) = paymentFSM.stateData routerForwarder.forward(routerFixture.router) @@ -304,13 +304,13 @@ class PaymentLifecycleSpec extends BaseRouterSpec { sender.send(paymentFSM, RouteResponse(route :: Nil)) awaitCond(paymentFSM.stateName == WAITING_FOR_PAYMENT_COMPLETE) val WaitingForComplete(_, cmd2, _, _, ignore2, _) = paymentFSM.stateData - assert(ignore2.nodes === Set(c)) + assert(ignore2.nodes == Set(c)) // and reply a 2nd time with an unparsable failure register.expectMsg(ForwardShortId(paymentFSM, scid_ab, cmd2)) sender.send(paymentFSM, addCompleted(HtlcResult.RemoteFail(UpdateFailHtlc(ByteVector32.Zeroes, 0, defaultPaymentHash)))) // unparsable message // we allow 2 tries, so we send a 2nd request to the router - assert(sender.expectMsgType[PaymentFailed].failures === UnreadableRemoteFailure(route.amount, route.hops) :: UnreadableRemoteFailure(route.amount, route.hops) :: Nil) + assert(sender.expectMsgType[PaymentFailed].failures == UnreadableRemoteFailure(route.amount, route.hops) :: UnreadableRemoteFailure(route.amount, route.hops) :: Nil) awaitCond(nodeParams.db.payments.getOutgoingPayment(id).exists(_.status.isInstanceOf[OutgoingPaymentStatus.Failed])) // after last attempt the payment is failed val metrics = metricsListener.expectMsgType[PathFindingExperimentMetrics] @@ -328,7 +328,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata), 2, routeParams = defaultRouteParams) sender.send(paymentFSM, request) - awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending)) + awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.Pending)) routerForwarder.expectMsgType[RouteRequest] routerForwarder.forward(routerFixture.router) awaitCond(paymentFSM.stateName == WAITING_FOR_PAYMENT_COMPLETE) @@ -339,7 +339,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { // then the payment lifecycle will ask for a new route excluding the channel routerForwarder.expectMsg(defaultRouteRequest(nodeParams.nodeId, d, cfg).copy(ignore = Ignore(Set.empty, Set(ChannelDesc(scid_ab, a, b))))) - awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending)) // payment is still pending because the error is recoverable + awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.Pending)) // payment is still pending because the error is recoverable } test("payment failed (register error)") { routerFixture => @@ -349,7 +349,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata), 2, routeParams = defaultRouteParams) sender.send(paymentFSM, request) - awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending)) + awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.Pending)) routerForwarder.expectMsgType[RouteRequest] routerForwarder.forward(routerFixture.router) awaitCond(paymentFSM.stateName == WAITING_FOR_PAYMENT_COMPLETE) @@ -359,7 +359,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { // then the payment lifecycle will ask for a new route excluding the channel routerForwarder.expectMsg(defaultRouteRequest(nodeParams.nodeId, d, cfg).copy(ignore = Ignore(Set.empty, Set(ChannelDesc(scid_ab, a, b))))) - awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending)) // payment is still pending because the error is recoverable + awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.Pending)) // payment is still pending because the error is recoverable } test("payment failed (first hop returns an UpdateFailMalformedHtlc)") { routerFixture => @@ -369,7 +369,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata), 2, routeParams = defaultRouteParams) sender.send(paymentFSM, request) - awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending)) + awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.Pending)) val WaitingForRoute(_, Nil, _) = paymentFSM.stateData routerForwarder.expectMsg(defaultRouteRequest(nodeParams.nodeId, d, cfg)) @@ -382,7 +382,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { // then the payment lifecycle will ask for a new route excluding the channel routerForwarder.expectMsg(defaultRouteRequest(a, d, cfg).copy(ignore = Ignore(Set.empty, Set(ChannelDesc(scid_ab, a, b))))) - awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending)) + awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.Pending)) } test("payment failed (first htlc failed on-chain)") { routerFixture => @@ -392,7 +392,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata), 2, routeParams = defaultRouteParams) sender.send(paymentFSM, request) - awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending)) + awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.Pending)) val WaitingForRoute(_, Nil, _) = paymentFSM.stateData routerForwarder.expectMsg(defaultRouteRequest(nodeParams.nodeId, d, cfg)) @@ -415,7 +415,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata), 2, routeParams = defaultRouteParams) sender.send(paymentFSM, request) - awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending)) + awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.Pending)) val WaitingForRoute(_, Nil, _) = paymentFSM.stateData routerForwarder.expectMsg(defaultRouteRequest(nodeParams.nodeId, d, cfg)) @@ -429,7 +429,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { // then the payment lifecycle will ask for a new route excluding the channel routerForwarder.expectMsg(defaultRouteRequest(a, d, cfg).copy(ignore = Ignore(Set.empty, Set(ChannelDesc(scid_ab, a, b))))) - awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending)) + awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.Pending)) } test("payment failed (TemporaryChannelFailure)") { routerFixture => @@ -458,7 +458,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { routerForwarder.expectMsg(defaultRouteRequest(a, d, cfg).copy(ignore = Ignore(Set.empty, Set(ChannelDesc(update_bc.shortChannelId, b, c))))) routerForwarder.forward(routerFixture.router) // we allow 2 tries, so we send a 2nd request to the router - assert(sender.expectMsgType[PaymentFailed].failures === RemoteFailure(route.amount, route.hops, Sphinx.DecryptedFailurePacket(b, failure)) :: LocalFailure(defaultAmountMsat, Nil, RouteNotFound) :: Nil) + assert(sender.expectMsgType[PaymentFailed].failures == RemoteFailure(route.amount, route.hops, Sphinx.DecryptedFailurePacket(b, failure)) :: LocalFailure(defaultAmountMsat, Nil, RouteNotFound) :: Nil) } test("payment failed (Update)") { routerFixture => @@ -468,7 +468,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata), 5, routeParams = defaultRouteParams) sender.send(paymentFSM, request) - awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending)) + awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.Pending)) val WaitingForRoute(_, Nil, _) = paymentFSM.stateData routerForwarder.expectMsg(defaultRouteRequest(nodeParams.nodeId, d, cfg)) @@ -485,7 +485,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { // payment lifecycle forwards the embedded channelUpdate to the router routerForwarder.expectMsg(channelUpdate_bc_modified) - awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending)) // 1 failure but not final, the payment is still PENDING + awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.Pending)) // 1 failure but not final, the payment is still PENDING routerForwarder.expectMsg(defaultRouteRequest(nodeParams.nodeId, d, cfg)) routerForwarder.forward(routerFixture.router) @@ -510,7 +510,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { routerForwarder.forward(routerFixture.router) // this time the router can't find a route: game over - assert(sender.expectMsgType[PaymentFailed].failures === RemoteFailure(route1.amount, route1.hops, Sphinx.DecryptedFailurePacket(b, failure)) :: RemoteFailure(route2.amount, route2.hops, Sphinx.DecryptedFailurePacket(b, failure2)) :: LocalFailure(defaultAmountMsat, Nil, RouteNotFound) :: Nil) + assert(sender.expectMsgType[PaymentFailed].failures == RemoteFailure(route1.amount, route1.hops, Sphinx.DecryptedFailurePacket(b, failure)) :: RemoteFailure(route2.amount, route2.hops, Sphinx.DecryptedFailurePacket(b, failure2)) :: LocalFailure(defaultAmountMsat, Nil, RouteNotFound) :: Nil) awaitCond(nodeParams.db.payments.getOutgoingPayment(id).exists(_.status.isInstanceOf[OutgoingPaymentStatus.Failed])) } @@ -550,7 +550,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata), 5, assistedRoutes = assistedRoutes, routeParams = defaultRouteParams) sender.send(paymentFSM, request) - awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending)) + awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.Pending)) val WaitingForRoute(_, Nil, _) = paymentFSM.stateData routerForwarder.expectMsg(defaultRouteRequest(nodeParams.nodeId, d, cfg).copy(assistedRoutes = assistedRoutes)) @@ -567,7 +567,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { // payment lifecycle forwards the embedded channelUpdate to the router routerForwarder.expectMsg(channelUpdate_bc_modified) - awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending)) // 1 failure but not final, the payment is still PENDING + awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.Pending)) // 1 failure but not final, the payment is still PENDING val assistedRoutes1 = Seq(Seq( ExtraHop(b, scid_bc, update_bc.feeBaseMsat, update_bc.feeProportionalMillionths, channelUpdate_bc_modified.cltvExpiryDelta), ExtraHop(c, scid_cd, update_cd.feeBaseMsat, update_cd.feeProportionalMillionths, update_cd.cltvExpiryDelta) @@ -591,7 +591,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val assistedRoutes = Seq(Seq(ExtraHop(c, scid_cd, update_cd.feeBaseMsat, update_cd.feeProportionalMillionths, update_cd.cltvExpiryDelta))) val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata), 1, assistedRoutes = assistedRoutes, routeParams = defaultRouteParams) sender.send(paymentFSM, request) - awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending)) + awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.Pending)) routerForwarder.expectMsg(defaultRouteRequest(nodeParams.nodeId, d, cfg).copy(assistedRoutes = assistedRoutes)) routerForwarder.forward(routerFixture.router) @@ -616,7 +616,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata), 2, routeParams = defaultRouteParams) sender.send(paymentFSM, request) - awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending)) + awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.Pending)) val WaitingForRoute(_, Nil, _) = paymentFSM.stateData routerForwarder.expectMsg(defaultRouteRequest(nodeParams.nodeId, d, cfg)) @@ -633,7 +633,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { routerForwarder.forward(router) // we allow 2 tries, so we send a 2nd request to the router, which won't find another route - assert(sender.expectMsgType[PaymentFailed].failures === RemoteFailure(route1.amount, route1.hops, Sphinx.DecryptedFailurePacket(b, failure)) :: LocalFailure(defaultAmountMsat, Nil, RouteNotFound) :: Nil) + assert(sender.expectMsgType[PaymentFailed].failures == RemoteFailure(route1.amount, route1.hops, Sphinx.DecryptedFailurePacket(b, failure)) :: LocalFailure(defaultAmountMsat, Nil, RouteNotFound) :: Nil) awaitCond(nodeParams.db.payments.getOutgoingPayment(id).exists(_.status.isInstanceOf[OutgoingPaymentStatus.Failed])) } @@ -658,18 +658,18 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val Transition(_, WAITING_FOR_REQUEST, WAITING_FOR_ROUTE) = monitor.expectMsgClass(classOf[Transition[_]]) routerForwarder.forward(routerFixture.router) val Transition(_, WAITING_FOR_ROUTE, WAITING_FOR_PAYMENT_COMPLETE) = monitor.expectMsgClass(classOf[Transition[_]]) - awaitCond(nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending)) + awaitCond(nodeParams.db.payments.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.Pending)) val Some(outgoing) = nodeParams.db.payments.getOutgoingPayment(id) - assert(outgoing.copy(createdAt = 0 unixms) === OutgoingPayment(id, parentId, Some(defaultExternalId), defaultPaymentHash, PaymentType.Standard, defaultAmountMsat, defaultAmountMsat, d, 0 unixms, Some(defaultInvoice), OutgoingPaymentStatus.Pending)) + assert(outgoing.copy(createdAt = 0 unixms) == OutgoingPayment(id, parentId, Some(defaultExternalId), defaultPaymentHash, PaymentType.Standard, defaultAmountMsat, defaultAmountMsat, d, 0 unixms, Some(defaultInvoice), OutgoingPaymentStatus.Pending)) sender.send(paymentFSM, addCompleted(HtlcResult.RemoteFulfill(UpdateFulfillHtlc(ByteVector32.Zeroes, 0, defaultPaymentPreimage)))) val ps = eventListener.expectMsgType[PaymentSent] - assert(ps.id === parentId) + assert(ps.id == parentId) assert(ps.feesPaid > 0.msat) - assert(ps.recipientAmount === defaultAmountMsat) - assert(ps.paymentHash === defaultPaymentHash) - assert(ps.paymentPreimage === defaultPaymentPreimage) - assert(ps.parts.head.id === id) + assert(ps.recipientAmount == defaultAmountMsat) + assert(ps.paymentHash == defaultPaymentHash) + assert(ps.paymentPreimage == defaultPaymentPreimage) + assert(ps.parts.head.id == id) awaitCond(nodeParams.db.payments.getOutgoingPayment(id).exists(_.status.isInstanceOf[OutgoingPaymentStatus.Succeeded])) val metrics = metricsListener.expectMsgType[PathFindingExperimentMetrics] @@ -693,12 +693,12 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val chan_bh = channelAnnouncement(channelId_bh, priv_b, priv_h, priv_funding_b, priv_funding_h) val channelUpdate_bh = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_b, h, channelId_bh, CltvExpiryDelta(9), htlcMinimumMsat = 0 msat, feeBaseMsat = 0 msat, feeProportionalMillionths = 0, htlcMaximumMsat = 500000000 msat) val channelUpdate_hb = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_h, b, channelId_bh, CltvExpiryDelta(9), htlcMinimumMsat = 0 msat, feeBaseMsat = 10 msat, feeProportionalMillionths = 8, htlcMaximumMsat = 500000000 msat) - assert(ChannelDesc(channelUpdate_bh, chan_bh) === ChannelDesc(channelId_bh, priv_b.publicKey, priv_h.publicKey)) + assert(ChannelDesc(channelUpdate_bh, chan_bh) == ChannelDesc(channelId_bh, priv_b.publicKey, priv_h.publicKey)) val peerConnection = TestProbe() router ! PeerRoutingMessage(peerConnection.ref, remoteNodeId, chan_bh) router ! PeerRoutingMessage(peerConnection.ref, remoteNodeId, channelUpdate_bh) router ! PeerRoutingMessage(peerConnection.ref, remoteNodeId, channelUpdate_hb) - assert(watcher.expectMsgType[ValidateRequest].ann === chan_bh) + assert(watcher.expectMsgType[ValidateRequest].ann == chan_bh) watcher.send(router, ValidateResult(chan_bh, Right((Transaction(version = 0, txIn = Nil, txOut = TxOut(1000000 sat, write(pay2wsh(Scripts.multiSig2of2(funding_b, funding_h)))) :: Nil, lockTime = 0), UtxoStatus.Unspent)))) watcher.expectMsgType[WatchExternalChannelSpent] @@ -718,12 +718,12 @@ class PaymentLifecycleSpec extends BaseRouterSpec { sender.send(paymentFSM, addCompleted(HtlcResult.OnChainFulfill(defaultPaymentPreimage))) val paymentOK = sender.expectMsgType[PaymentSent] val PaymentSent(_, _, paymentOK.paymentPreimage, finalAmount, _, PartialPayment(_, request.finalPayload.amount, fee, ByteVector32.Zeroes, _, _) :: Nil) = eventListener.expectMsgType[PaymentSent] - assert(finalAmount === defaultAmountMsat) + assert(finalAmount == defaultAmountMsat) // NB: A -> B doesn't pay fees because it's our direct neighbor // NB: B -> H doesn't asks for fees at all - assert(fee === 0.msat) - assert(paymentOK.recipientAmount === request.finalPayload.amount) + assert(fee == 0.msat) + assert(paymentOK.recipientAmount == request.finalPayload.amount) val metrics = metricsListener.expectMsgType[PathFindingExperimentMetrics] assert(metrics.status == "SUCCESS") @@ -746,7 +746,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { RemoteFailure(defaultAmountMsat, channelHopFromUpdate(a, b, update_ab) :: Nil, Sphinx.DecryptedFailurePacket(a, TemporaryNodeFailure)), LocalFailure(defaultAmountMsat, channelHopFromUpdate(a, b, update_ab) :: Nil, ChannelUnavailable(ByteVector32.Zeroes)) ) - assert(filtered === expected) + assert(filtered == expected) } test("ignore failed nodes/channels") { _ => @@ -771,8 +771,8 @@ class PaymentLifecycleSpec extends BaseRouterSpec { for ((failure, expectedNodes, expectedChannels) <- testCases) { val ignore = PaymentFailure.updateIgnored(failure, Ignore.empty) - assert(ignore.nodes === expectedNodes, failure) - assert(ignore.channels === expectedChannels, failure) + assert(ignore.nodes == expectedNodes, failure) + assert(ignore.channels == expectedChannels, failure) } val failures = Seq( @@ -781,8 +781,8 @@ class PaymentLifecycleSpec extends BaseRouterSpec { LocalFailure(defaultAmountMsat, route_abcd, new RuntimeException("fatal")) ) val ignore = PaymentFailure.updateIgnored(failures, Ignore.empty) - assert(ignore.nodes === Set(c)) - assert(ignore.channels === Set(ChannelDesc(scid_ab, a, b), ChannelDesc(scid_bc, b, c))) + assert(ignore.nodes == Set(c)) + assert(ignore.channels == Set(ChannelDesc(scid_ab, a, b), ChannelDesc(scid_bc, b, c))) } test("disable database and events") { routerFixture => @@ -796,11 +796,11 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val Transition(_, WAITING_FOR_REQUEST, WAITING_FOR_ROUTE) = monitor.expectMsgClass(classOf[Transition[_]]) routerForwarder.forward(routerFixture.router) val Transition(_, WAITING_FOR_ROUTE, WAITING_FOR_PAYMENT_COMPLETE) = monitor.expectMsgClass(classOf[Transition[_]]) - assert(nodeParams.db.payments.getOutgoingPayment(id) === None) + assert(nodeParams.db.payments.getOutgoingPayment(id) == None) sender.send(paymentFSM, addCompleted(HtlcResult.RemoteFulfill(UpdateFulfillHtlc(ByteVector32.Zeroes, 0, defaultPaymentPreimage)))) sender.expectMsgType[PaymentSent] - assert(nodeParams.db.payments.getOutgoingPayment(id) === None) + assert(nodeParams.db.payments.getOutgoingPayment(id) == None) eventListener.expectNoMessage(100 millis) } @@ -820,7 +820,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val WaitingForComplete(_, _, Nil, sharedSecrets1, _, _) = paymentFSM.stateData awaitCond(nodeParams.db.payments.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.Pending)) val Some(outgoing) = nodeParams.db.payments.getOutgoingPayment(id) - assert(outgoing.copy(createdAt = 0 unixms) === OutgoingPayment(id, parentId, Some(defaultExternalId), defaultPaymentHash, PaymentType.Standard, defaultAmountMsat, defaultAmountMsat, d, 0 unixms, Some(defaultInvoice), OutgoingPaymentStatus.Pending)) + assert(outgoing.copy(createdAt = 0 unixms) == OutgoingPayment(id, parentId, Some(defaultExternalId), defaultPaymentHash, PaymentType.Standard, defaultAmountMsat, defaultAmountMsat, d, 0 unixms, Some(defaultInvoice), OutgoingPaymentStatus.Pending)) // we change the cltv expiry val channelUpdate_bc_modified = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_b, c, scid_bc, CltvExpiryDelta(42), htlcMinimumMsat = update_bc.htlcMinimumMsat, feeBaseMsat = update_bc.feeBaseMsat, feeProportionalMillionths = update_bc.feeProportionalMillionths, htlcMaximumMsat = update_bc.htlcMaximumMsat.get) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentPacketSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentPacketSpec.scala index 3193df2d3e..491f200789 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentPacketSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentPacketSpec.scala @@ -59,15 +59,15 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { // spec: fee-base-msat + htlc-amount-msat * fee-proportional-millionths / 1000000 val ref = feeBaseMsat + htlcAmountMsat * feeProportionalMillionth / 1000000 val fee = nodeFee(feeBaseMsat, feeProportionalMillionth, htlcAmountMsat) - assert(ref === fee) + assert(ref == fee) } def testBuildOnion(): Unit = { val finalPayload = FinalTlvPayload(TlvStream(AmountToForward(finalAmount), OutgoingCltv(finalExpiry), PaymentData(paymentSecret, 0 msat))) val Success((firstAmount, firstExpiry, onion)) = buildPaymentPacket(paymentHash, hops, finalPayload) - assert(firstAmount === amount_ab) - assert(firstExpiry === expiry_ab) - assert(onion.packet.payload.length === PaymentOnionCodecs.paymentOnionPayloadLength) + assert(firstAmount == amount_ab) + assert(firstExpiry == expiry_ab) + assert(onion.packet.payload.length == PaymentOnionCodecs.paymentOnionPayloadLength) // let's peel the onion testPeelOnion(onion.packet) @@ -76,41 +76,41 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { def testPeelOnion(packet_b: OnionRoutingPacket): Unit = { val add_b = UpdateAddHtlc(randomBytes32(), 0, amount_ab, paymentHash, expiry_ab, packet_b) val Right(relay_b@ChannelRelayPacket(add_b2, payload_b, packet_c)) = decrypt(add_b, priv_b.privateKey) - assert(add_b2 === add_b) - assert(packet_c.payload.length === PaymentOnionCodecs.paymentOnionPayloadLength) - assert(payload_b.amountToForward === amount_bc) - assert(payload_b.outgoingCltv === expiry_bc) - assert(payload_b.outgoingChannelId === channelUpdate_bc.shortChannelId) - assert(relay_b.relayFeeMsat === fee_b) - assert(relay_b.expiryDelta === channelUpdate_bc.cltvExpiryDelta) + assert(add_b2 == add_b) + assert(packet_c.payload.length == PaymentOnionCodecs.paymentOnionPayloadLength) + assert(payload_b.amountToForward == amount_bc) + assert(payload_b.outgoingCltv == expiry_bc) + assert(payload_b.outgoingChannelId == channelUpdate_bc.shortChannelId) + assert(relay_b.relayFeeMsat == fee_b) + assert(relay_b.expiryDelta == channelUpdate_bc.cltvExpiryDelta) val add_c = UpdateAddHtlc(randomBytes32(), 1, amount_bc, paymentHash, expiry_bc, packet_c) val Right(relay_c@ChannelRelayPacket(add_c2, payload_c, packet_d)) = decrypt(add_c, priv_c.privateKey) - assert(add_c2 === add_c) - assert(packet_d.payload.length === PaymentOnionCodecs.paymentOnionPayloadLength) - assert(payload_c.amountToForward === amount_cd) - assert(payload_c.outgoingCltv === expiry_cd) - assert(payload_c.outgoingChannelId === channelUpdate_cd.shortChannelId) - assert(relay_c.relayFeeMsat === fee_c) - assert(relay_c.expiryDelta === channelUpdate_cd.cltvExpiryDelta) + assert(add_c2 == add_c) + assert(packet_d.payload.length == PaymentOnionCodecs.paymentOnionPayloadLength) + assert(payload_c.amountToForward == amount_cd) + assert(payload_c.outgoingCltv == expiry_cd) + assert(payload_c.outgoingChannelId == channelUpdate_cd.shortChannelId) + assert(relay_c.relayFeeMsat == fee_c) + assert(relay_c.expiryDelta == channelUpdate_cd.cltvExpiryDelta) val add_d = UpdateAddHtlc(randomBytes32(), 2, amount_cd, paymentHash, expiry_cd, packet_d) val Right(relay_d@ChannelRelayPacket(add_d2, payload_d, packet_e)) = decrypt(add_d, priv_d.privateKey) - assert(add_d2 === add_d) - assert(packet_e.payload.length === PaymentOnionCodecs.paymentOnionPayloadLength) - assert(payload_d.amountToForward === amount_de) - assert(payload_d.outgoingCltv === expiry_de) - assert(payload_d.outgoingChannelId === channelUpdate_de.shortChannelId) - assert(relay_d.relayFeeMsat === fee_d) - assert(relay_d.expiryDelta === channelUpdate_de.cltvExpiryDelta) + assert(add_d2 == add_d) + assert(packet_e.payload.length == PaymentOnionCodecs.paymentOnionPayloadLength) + assert(payload_d.amountToForward == amount_de) + assert(payload_d.outgoingCltv == expiry_de) + assert(payload_d.outgoingChannelId == channelUpdate_de.shortChannelId) + assert(relay_d.relayFeeMsat == fee_d) + assert(relay_d.expiryDelta == channelUpdate_de.cltvExpiryDelta) val add_e = UpdateAddHtlc(randomBytes32(), 2, amount_de, paymentHash, expiry_de, packet_e) val Right(FinalPacket(add_e2, payload_e)) = decrypt(add_e, priv_e.privateKey) - assert(add_e2 === add_e) - assert(payload_e.amount === finalAmount) - assert(payload_e.totalAmount === finalAmount) - assert(payload_e.expiry === finalExpiry) - assert(payload_e.paymentSecret === paymentSecret) + assert(add_e2 == add_e) + assert(payload_e.amount == finalAmount) + assert(payload_e.totalAmount == finalAmount) + assert(payload_e.expiry == finalExpiry) + assert(payload_e.paymentSecret == paymentSecret) } test("build onion with final payload") { @@ -120,9 +120,9 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { test("build a command including the onion") { val Success((add, _)) = buildCommand(ActorRef.noSender, Upstream.Local(UUID.randomUUID), paymentHash, hops, PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, paymentSecret, None)) assert(add.amount > finalAmount) - assert(add.cltvExpiry === finalExpiry + channelUpdate_de.cltvExpiryDelta + channelUpdate_cd.cltvExpiryDelta + channelUpdate_bc.cltvExpiryDelta) - assert(add.paymentHash === paymentHash) - assert(add.onion.payload.length === PaymentOnionCodecs.paymentOnionPayloadLength) + assert(add.cltvExpiry == finalExpiry + channelUpdate_de.cltvExpiryDelta + channelUpdate_cd.cltvExpiryDelta + channelUpdate_bc.cltvExpiryDelta) + assert(add.paymentHash == paymentHash) + assert(add.onion.payload.length == PaymentOnionCodecs.paymentOnionPayloadLength) // let's peel the onion testPeelOnion(add.onion) @@ -130,20 +130,20 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { test("build a command with no hops") { val Success((add, _)) = buildCommand(ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, hops.take(1), PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, paymentSecret, Some(paymentMetadata))) - assert(add.amount === finalAmount) - assert(add.cltvExpiry === finalExpiry) - assert(add.paymentHash === paymentHash) - assert(add.onion.payload.length === PaymentOnionCodecs.paymentOnionPayloadLength) + assert(add.amount == finalAmount) + assert(add.cltvExpiry == finalExpiry) + assert(add.paymentHash == paymentHash) + assert(add.onion.payload.length == PaymentOnionCodecs.paymentOnionPayloadLength) // let's peel the onion val add_b = UpdateAddHtlc(randomBytes32(), 0, finalAmount, paymentHash, finalExpiry, add.onion) val Right(FinalPacket(add_b2, payload_b)) = decrypt(add_b, priv_b.privateKey) - assert(add_b2 === add_b) - assert(payload_b.amount === finalAmount) - assert(payload_b.totalAmount === finalAmount) - assert(payload_b.expiry === finalExpiry) - assert(payload_b.paymentSecret === paymentSecret) - assert(payload_b.paymentMetadata === Some(paymentMetadata)) + assert(add_b2 == add_b) + assert(payload_b.amount == finalAmount) + assert(payload_b.totalAmount == finalAmount) + assert(payload_b.expiry == finalExpiry) + assert(payload_b.paymentSecret == paymentSecret) + assert(payload_b.paymentMetadata == Some(paymentMetadata)) } test("build a trampoline payment") { @@ -153,58 +153,58 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { // a -> b -> c d e val Success((amount_ac, expiry_ac, trampolineOnion)) = buildTrampolinePacket(paymentHash, trampolineHops, PaymentOnion.createMultiPartPayload(finalAmount, finalAmount * 3, finalExpiry, paymentSecret, Some(hex"010203"))) - assert(amount_ac === amount_bc) - assert(expiry_ac === expiry_bc) + assert(amount_ac == amount_bc) + assert(expiry_ac == expiry_bc) val Success((firstAmount, firstExpiry, onion)) = buildPaymentPacket(paymentHash, trampolineChannelHops, PaymentOnion.createTrampolinePayload(amount_ac, amount_ac, expiry_ac, randomBytes32(), trampolineOnion.packet)) - assert(firstAmount === amount_ab) - assert(firstExpiry === expiry_ab) + assert(firstAmount == amount_ab) + assert(firstExpiry == expiry_ab) val add_b = UpdateAddHtlc(randomBytes32(), 1, firstAmount, paymentHash, firstExpiry, onion.packet) val Right(ChannelRelayPacket(add_b2, payload_b, packet_c)) = decrypt(add_b, priv_b.privateKey) - assert(add_b2 === add_b) - assert(payload_b === ChannelRelayTlvPayload(channelUpdate_bc.shortChannelId, amount_bc, expiry_bc)) + assert(add_b2 == add_b) + assert(payload_b == ChannelRelayTlvPayload(channelUpdate_bc.shortChannelId, amount_bc, expiry_bc)) val add_c = UpdateAddHtlc(randomBytes32(), 2, amount_bc, paymentHash, expiry_bc, packet_c) val Right(NodeRelayPacket(add_c2, outer_c, inner_c, packet_d)) = decrypt(add_c, priv_c.privateKey) - assert(add_c2 === add_c) - assert(outer_c.amount === amount_bc) - assert(outer_c.totalAmount === amount_bc) - assert(outer_c.expiry === expiry_bc) - assert(inner_c.amountToForward === amount_cd) - assert(inner_c.outgoingCltv === expiry_cd) - assert(inner_c.outgoingNodeId === d) - assert(inner_c.invoiceRoutingInfo === None) - assert(inner_c.invoiceFeatures === None) - assert(inner_c.paymentSecret === None) - assert(inner_c.paymentMetadata === None) + assert(add_c2 == add_c) + assert(outer_c.amount == amount_bc) + assert(outer_c.totalAmount == amount_bc) + assert(outer_c.expiry == expiry_bc) + assert(inner_c.amountToForward == amount_cd) + assert(inner_c.outgoingCltv == expiry_cd) + assert(inner_c.outgoingNodeId == d) + assert(inner_c.invoiceRoutingInfo == None) + assert(inner_c.invoiceFeatures == None) + assert(inner_c.paymentSecret == None) + assert(inner_c.paymentMetadata == None) // c forwards the trampoline payment to d. val Success((amount_d, expiry_d, onion_d)) = buildPaymentPacket(paymentHash, channelHopFromUpdate(c, d, channelUpdate_cd) :: Nil, PaymentOnion.createTrampolinePayload(amount_cd, amount_cd, expiry_cd, randomBytes32(), packet_d)) - assert(amount_d === amount_cd) - assert(expiry_d === expiry_cd) + assert(amount_d == amount_cd) + assert(expiry_d == expiry_cd) val add_d = UpdateAddHtlc(randomBytes32(), 3, amount_d, paymentHash, expiry_d, onion_d.packet) val Right(NodeRelayPacket(add_d2, outer_d, inner_d, packet_e)) = decrypt(add_d, priv_d.privateKey) - assert(add_d2 === add_d) - assert(outer_d.amount === amount_cd) - assert(outer_d.totalAmount === amount_cd) - assert(outer_d.expiry === expiry_cd) - assert(inner_d.amountToForward === amount_de) - assert(inner_d.outgoingCltv === expiry_de) - assert(inner_d.outgoingNodeId === e) - assert(inner_d.invoiceRoutingInfo === None) - assert(inner_d.invoiceFeatures === None) - assert(inner_d.paymentSecret === None) - assert(inner_d.paymentMetadata === None) + assert(add_d2 == add_d) + assert(outer_d.amount == amount_cd) + assert(outer_d.totalAmount == amount_cd) + assert(outer_d.expiry == expiry_cd) + assert(inner_d.amountToForward == amount_de) + assert(inner_d.outgoingCltv == expiry_de) + assert(inner_d.outgoingNodeId == e) + assert(inner_d.invoiceRoutingInfo == None) + assert(inner_d.invoiceFeatures == None) + assert(inner_d.paymentSecret == None) + assert(inner_d.paymentMetadata == None) // d forwards the trampoline payment to e. val Success((amount_e, expiry_e, onion_e)) = buildPaymentPacket(paymentHash, channelHopFromUpdate(d, e, channelUpdate_de) :: Nil, PaymentOnion.createTrampolinePayload(amount_de, amount_de, expiry_de, randomBytes32(), packet_e)) - assert(amount_e === amount_de) - assert(expiry_e === expiry_de) + assert(amount_e == amount_de) + assert(expiry_e == expiry_de) val add_e = UpdateAddHtlc(randomBytes32(), 4, amount_e, paymentHash, expiry_e, onion_e.packet) val Right(FinalPacket(add_e2, payload_e)) = decrypt(add_e, priv_e.privateKey) - assert(add_e2 === add_e) - assert(payload_e === FinalTlvPayload(TlvStream(AmountToForward(finalAmount), OutgoingCltv(finalExpiry), PaymentData(paymentSecret, finalAmount * 3), OnionPaymentPayloadTlv.PaymentMetadata(hex"010203")))) + assert(add_e2 == add_e) + assert(payload_e == FinalTlvPayload(TlvStream(AmountToForward(finalAmount), OutgoingCltv(finalExpiry), PaymentData(paymentSecret, finalAmount * 3), OnionPaymentPayloadTlv.PaymentMetadata(hex"010203")))) } test("build a trampoline payment with non-trampoline recipient") { @@ -217,47 +217,47 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { val invoiceFeatures = Features[InvoiceFeature](VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory, BasicMultiPartPayment -> Optional) val invoice = Bolt11Invoice(Block.RegtestGenesisBlock.hash, Some(finalAmount), paymentHash, priv_a.privateKey, Left("#reckless"), CltvExpiryDelta(18), None, None, routingHints, features = invoiceFeatures, paymentMetadata = Some(hex"010203")) val Success((amount_ac, expiry_ac, trampolineOnion)) = buildTrampolineToLegacyPacket(invoice, trampolineHops, PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, invoice.paymentSecret.get, None)) - assert(amount_ac === amount_bc) - assert(expiry_ac === expiry_bc) + assert(amount_ac == amount_bc) + assert(expiry_ac == expiry_bc) val Success((firstAmount, firstExpiry, onion)) = buildPaymentPacket(paymentHash, trampolineChannelHops, PaymentOnion.createTrampolinePayload(amount_ac, amount_ac, expiry_ac, randomBytes32(), trampolineOnion.packet)) - assert(firstAmount === amount_ab) - assert(firstExpiry === expiry_ab) + assert(firstAmount == amount_ab) + assert(firstExpiry == expiry_ab) val add_b = UpdateAddHtlc(randomBytes32(), 1, firstAmount, paymentHash, firstExpiry, onion.packet) val Right(ChannelRelayPacket(_, _, packet_c)) = decrypt(add_b, priv_b.privateKey) val add_c = UpdateAddHtlc(randomBytes32(), 2, amount_bc, paymentHash, expiry_bc, packet_c) val Right(NodeRelayPacket(_, outer_c, inner_c, packet_d)) = decrypt(add_c, priv_c.privateKey) - assert(outer_c.amount === amount_bc) - assert(outer_c.totalAmount === amount_bc) - assert(outer_c.expiry === expiry_bc) + assert(outer_c.amount == amount_bc) + assert(outer_c.totalAmount == amount_bc) + assert(outer_c.expiry == expiry_bc) assert(outer_c.paymentSecret !== invoice.paymentSecret) - assert(inner_c.amountToForward === amount_cd) - assert(inner_c.outgoingCltv === expiry_cd) - assert(inner_c.outgoingNodeId === d) - assert(inner_c.invoiceRoutingInfo === None) - assert(inner_c.invoiceFeatures === None) - assert(inner_c.paymentSecret === None) + assert(inner_c.amountToForward == amount_cd) + assert(inner_c.outgoingCltv == expiry_cd) + assert(inner_c.outgoingNodeId == d) + assert(inner_c.invoiceRoutingInfo == None) + assert(inner_c.invoiceFeatures == None) + assert(inner_c.paymentSecret == None) // c forwards the trampoline payment to d. val Success((amount_d, expiry_d, onion_d)) = buildPaymentPacket(paymentHash, channelHopFromUpdate(c, d, channelUpdate_cd) :: Nil, PaymentOnion.createTrampolinePayload(amount_cd, amount_cd, expiry_cd, randomBytes32(), packet_d)) - assert(amount_d === amount_cd) - assert(expiry_d === expiry_cd) + assert(amount_d == amount_cd) + assert(expiry_d == expiry_cd) val add_d = UpdateAddHtlc(randomBytes32(), 3, amount_d, paymentHash, expiry_d, onion_d.packet) val Right(NodeRelayPacket(_, outer_d, inner_d, _)) = decrypt(add_d, priv_d.privateKey) - assert(outer_d.amount === amount_cd) - assert(outer_d.totalAmount === amount_cd) - assert(outer_d.expiry === expiry_cd) + assert(outer_d.amount == amount_cd) + assert(outer_d.totalAmount == amount_cd) + assert(outer_d.expiry == expiry_cd) assert(outer_d.paymentSecret !== invoice.paymentSecret) - assert(inner_d.amountToForward === finalAmount) - assert(inner_d.outgoingCltv === expiry_de) - assert(inner_d.outgoingNodeId === e) - assert(inner_d.totalAmount === finalAmount) - assert(inner_d.paymentSecret === invoice.paymentSecret) - assert(inner_d.paymentMetadata === Some(hex"010203")) - assert(inner_d.invoiceFeatures === Some(hex"024100")) // var_onion_optin, payment_secret, basic_mpp - assert(inner_d.invoiceRoutingInfo === Some(routingHints)) + assert(inner_d.amountToForward == finalAmount) + assert(inner_d.outgoingCltv == expiry_de) + assert(inner_d.outgoingNodeId == e) + assert(inner_d.totalAmount == finalAmount) + assert(inner_d.paymentSecret == invoice.paymentSecret) + assert(inner_d.paymentMetadata == Some(hex"010203")) + assert(inner_d.invoiceFeatures == Some(hex"024100")) // var_onion_optin, payment_secret, basic_mpp + assert(inner_d.invoiceRoutingInfo == Some(routingHints)) } test("fail to build a trampoline payment when too much invoice data is provided") { @@ -294,14 +294,14 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { val Success((firstAmount, firstExpiry, onion)) = buildPaymentPacket(paymentHash, hops.take(1), PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, paymentSecret, None)) val add = UpdateAddHtlc(randomBytes32(), 1, firstAmount - 100.msat, paymentHash, firstExpiry, onion.packet) val Left(failure) = decrypt(add, priv_b.privateKey) - assert(failure === FinalIncorrectHtlcAmount(firstAmount - 100.msat)) + assert(failure == FinalIncorrectHtlcAmount(firstAmount - 100.msat)) } test("fail to decrypt at the final node when expiry has been modified by next-to-last node") { val Success((firstAmount, firstExpiry, onion)) = buildPaymentPacket(paymentHash, hops.take(1), PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, paymentSecret, None)) val add = UpdateAddHtlc(randomBytes32(), 1, firstAmount, paymentHash, firstExpiry - CltvExpiryDelta(12), onion.packet) val Left(failure) = decrypt(add, priv_b.privateKey) - assert(failure === FinalIncorrectCltvExpiry(firstExpiry - CltvExpiryDelta(12))) + assert(failure == FinalIncorrectCltvExpiry(firstExpiry - CltvExpiryDelta(12))) } test("fail to decrypt at the final trampoline node when amount has been modified by next-to-last trampoline") { @@ -316,7 +316,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { val invalidTotalAmount = amount_de + 100.msat val Success((amount_e, expiry_e, onion_e)) = buildPaymentPacket(paymentHash, channelHopFromUpdate(d, e, channelUpdate_de) :: Nil, PaymentOnion.createTrampolinePayload(amount_de, invalidTotalAmount, expiry_de, randomBytes32(), packet_e)) val Left(failure) = decrypt(UpdateAddHtlc(randomBytes32(), 4, amount_e, paymentHash, expiry_e, onion_e.packet), priv_e.privateKey) - assert(failure === FinalIncorrectHtlcAmount(invalidTotalAmount)) + assert(failure == FinalIncorrectHtlcAmount(invalidTotalAmount)) } test("fail to decrypt at the final trampoline node when expiry has been modified by next-to-last trampoline") { @@ -331,7 +331,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { val invalidExpiry = expiry_de - CltvExpiryDelta(12) val Success((amount_e, expiry_e, onion_e)) = buildPaymentPacket(paymentHash, channelHopFromUpdate(d, e, channelUpdate_de) :: Nil, PaymentOnion.createTrampolinePayload(amount_de, amount_de, invalidExpiry, randomBytes32(), packet_e)) val Left(failure) = decrypt(UpdateAddHtlc(randomBytes32(), 4, amount_e, paymentHash, expiry_e, onion_e.packet), priv_e.privateKey) - assert(failure === FinalIncorrectCltvExpiry(invalidExpiry)) + assert(failure == FinalIncorrectCltvExpiry(invalidExpiry)) } test("fail to decrypt at intermediate trampoline node when amount is invalid") { @@ -340,7 +340,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { val Right(ChannelRelayPacket(_, _, packet_c)) = decrypt(UpdateAddHtlc(randomBytes32(), 1, firstAmount, paymentHash, firstExpiry, onion.packet), priv_b.privateKey) // A trampoline relay is very similar to a final node: it can validate that the HTLC amount matches the onion outer amount. val Left(failure) = decrypt(UpdateAddHtlc(randomBytes32(), 2, amount_bc - 100.msat, paymentHash, expiry_bc, packet_c), priv_c.privateKey) - assert(failure === FinalIncorrectHtlcAmount(amount_bc - 100.msat)) + assert(failure == FinalIncorrectHtlcAmount(amount_bc - 100.msat)) } test("fail to decrypt at intermediate trampoline node when expiry is invalid") { @@ -349,7 +349,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { val Right(ChannelRelayPacket(_, _, packet_c)) = decrypt(UpdateAddHtlc(randomBytes32(), 1, firstAmount, paymentHash, firstExpiry, onion.packet), priv_b.privateKey) // A trampoline relay is very similar to a final node: it can validate that the HTLC expiry matches the onion outer expiry. val Left(failure) = decrypt(UpdateAddHtlc(randomBytes32(), 2, amount_bc, paymentHash, expiry_bc - CltvExpiryDelta(12), packet_c), priv_c.privateKey) - assert(failure === FinalIncorrectCltvExpiry(expiry_bc - CltvExpiryDelta(12))) + assert(failure == FinalIncorrectCltvExpiry(expiry_bc - CltvExpiryDelta(12))) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PostRestartHtlcCleanerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PostRestartHtlcCleanerSpec.scala index 611eadc840..9dfcf00f73 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PostRestartHtlcCleanerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PostRestartHtlcCleanerSpec.scala @@ -25,7 +25,7 @@ import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, Crypto, OutPoint, Sato import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.WatchTxConfirmedTriggered import fr.acinq.eclair.channel.Helpers.Closing import fr.acinq.eclair.channel._ -import fr.acinq.eclair.channel.states.ChannelStateTestsHelperMethods +import fr.acinq.eclair.channel.states.ChannelStateTestsBase import fr.acinq.eclair.db.{OutgoingPayment, OutgoingPaymentStatus, PaymentType} import fr.acinq.eclair.payment.OutgoingPaymentPacket.{Upstream, buildCommand} import fr.acinq.eclair.payment.PaymentPacketSpec._ @@ -50,7 +50,7 @@ import scala.util.Success * Created by t-bast on 21/11/2019. */ -class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with ParallelTestExecution with ChannelStateTestsHelperMethods { +class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with ParallelTestExecution with ChannelStateTestsBase { import PostRestartHtlcCleanerSpec._ @@ -123,19 +123,19 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit // channel 1 goes to NORMAL state: system.eventStream.publish(ChannelStateChanged(channel.ref, channels.head.commitments.channelId, system.deadLetters, a, OFFLINE, NORMAL, Some(channels.head.commitments))) val fails_ab_1 = channel.expectMsgType[CMD_FAIL_HTLC] :: channel.expectMsgType[CMD_FAIL_HTLC] :: Nil - assert(fails_ab_1.toSet === Set(CMD_FAIL_HTLC(1, Right(TemporaryNodeFailure), commit = true), CMD_FAIL_HTLC(4, Right(TemporaryNodeFailure), commit = true))) + assert(fails_ab_1.toSet == Set(CMD_FAIL_HTLC(1, Right(TemporaryNodeFailure), commit = true), CMD_FAIL_HTLC(4, Right(TemporaryNodeFailure), commit = true))) channel.expectNoMessage(100 millis) // channel 2 goes to NORMAL state: system.eventStream.publish(ChannelStateChanged(channel.ref, channels(1).commitments.channelId, system.deadLetters, a, OFFLINE, NORMAL, Some(channels(1).commitments))) val fails_ab_2 = channel.expectMsgType[CMD_FAIL_HTLC] :: channel.expectMsgType[CMD_FAIL_HTLC] :: Nil - assert(fails_ab_2.toSet === Set(CMD_FAIL_HTLC(0, Right(TemporaryNodeFailure), commit = true), CMD_FAIL_HTLC(4, Right(TemporaryNodeFailure), commit = true))) + assert(fails_ab_2.toSet == Set(CMD_FAIL_HTLC(0, Right(TemporaryNodeFailure), commit = true), CMD_FAIL_HTLC(4, Right(TemporaryNodeFailure), commit = true))) channel.expectNoMessage(100 millis) // let's assume that channel 1 was disconnected before having signed the fails, and gets connected again: system.eventStream.publish(ChannelStateChanged(channel.ref, channels.head.channelId, system.deadLetters, a, OFFLINE, NORMAL, Some(channels.head.commitments))) val fails_ab_1_bis = channel.expectMsgType[CMD_FAIL_HTLC] :: channel.expectMsgType[CMD_FAIL_HTLC] :: Nil - assert(fails_ab_1_bis.toSet === Set(CMD_FAIL_HTLC(1, Right(TemporaryNodeFailure), commit = true), CMD_FAIL_HTLC(4, Right(TemporaryNodeFailure), commit = true))) + assert(fails_ab_1_bis.toSet == Set(CMD_FAIL_HTLC(1, Right(TemporaryNodeFailure), commit = true), CMD_FAIL_HTLC(4, Right(TemporaryNodeFailure), commit = true))) channel.expectNoMessage(100 millis) // let's now assume that channel 1 gets reconnected, and it had the time to fail the htlcs: @@ -191,7 +191,7 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit CMD_FAIL_HTLC(7, Right(TemporaryNodeFailure), commit = true) ) val received1 = expected1.map(_ => channel.expectMsgType[Command]) - assert(received1 === expected1) + assert(received1 == expected1) channel.expectNoMessage(100 millis) // channel 2 goes to NORMAL state: @@ -203,13 +203,13 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit CMD_FAIL_HTLC(9, Right(TemporaryNodeFailure), commit = true) ) val received2 = expected2.map(_ => channel.expectMsgType[Command]) - assert(received2 === expected2) + assert(received2 == expected2) channel.expectNoMessage(100 millis) // let's assume that channel 1 was disconnected before having signed the updates, and gets connected again: system.eventStream.publish(ChannelStateChanged(channel.ref, channels.head.channelId, system.deadLetters, a, OFFLINE, NORMAL, Some(channels.head.commitments))) val received3 = expected1.map(_ => channel.expectMsgType[Command]) - assert(received3 === expected1) + assert(received3 == expected1) channel.expectNoMessage(100 millis) // let's now assume that channel 1 gets reconnected, and it had the time to sign the htlcs: @@ -232,20 +232,20 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit sender.send(relayer, testCase.fails(1)) eventListener.expectNoMessage(100 millis) // This is a multi-part payment, the second part is still pending. - assert(nodeParams.db.payments.getOutgoingPayment(testCase.childIds(2)).get.status === OutgoingPaymentStatus.Pending) + assert(nodeParams.db.payments.getOutgoingPayment(testCase.childIds(2)).get.status == OutgoingPaymentStatus.Pending) sender.send(relayer, testCase.fails(2)) val e1 = eventListener.expectMsgType[PaymentFailed] - assert(e1.id === testCase.parentId) - assert(e1.paymentHash === paymentHash2) + assert(e1.id == testCase.parentId) + assert(e1.paymentHash == paymentHash2) assert(nodeParams.db.payments.getOutgoingPayment(testCase.childIds(1)).get.status.isInstanceOf[OutgoingPaymentStatus.Failed]) assert(nodeParams.db.payments.getOutgoingPayment(testCase.childIds(2)).get.status.isInstanceOf[OutgoingPaymentStatus.Failed]) - assert(nodeParams.db.payments.getOutgoingPayment(testCase.childIds.head).get.status === OutgoingPaymentStatus.Pending) + assert(nodeParams.db.payments.getOutgoingPayment(testCase.childIds.head).get.status == OutgoingPaymentStatus.Pending) sender.send(relayer, testCase.fails.head) val e2 = eventListener.expectMsgType[PaymentFailed] - assert(e2.id === testCase.childIds.head) - assert(e2.paymentHash === paymentHash1) + assert(e2.id == testCase.childIds.head) + assert(e2.paymentHash == paymentHash1) assert(nodeParams.db.payments.getOutgoingPayment(testCase.childIds.head).get.status.isInstanceOf[OutgoingPaymentStatus.Failed]) register.expectNoMessage(100 millis) @@ -261,27 +261,27 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit sender.send(relayer, testCase.fulfills(1)) eventListener.expectNoMessage(100 millis) // This is a multi-part payment, the second part is still pending. - assert(nodeParams.db.payments.getOutgoingPayment(testCase.childIds(2)).get.status === OutgoingPaymentStatus.Pending) + assert(nodeParams.db.payments.getOutgoingPayment(testCase.childIds(2)).get.status == OutgoingPaymentStatus.Pending) sender.send(relayer, testCase.fulfills(2)) val e1 = eventListener.expectMsgType[PaymentSent] - assert(e1.id === testCase.parentId) - assert(e1.paymentPreimage === preimage2) - assert(e1.paymentHash === paymentHash2) - assert(e1.parts.length === 2) - assert(e1.amountWithFees === 2834.msat) - assert(e1.recipientAmount === 2500.msat) + assert(e1.id == testCase.parentId) + assert(e1.paymentPreimage == preimage2) + assert(e1.paymentHash == paymentHash2) + assert(e1.parts.length == 2) + assert(e1.amountWithFees == 2834.msat) + assert(e1.recipientAmount == 2500.msat) assert(nodeParams.db.payments.getOutgoingPayment(testCase.childIds(1)).get.status.isInstanceOf[OutgoingPaymentStatus.Succeeded]) assert(nodeParams.db.payments.getOutgoingPayment(testCase.childIds(2)).get.status.isInstanceOf[OutgoingPaymentStatus.Succeeded]) - assert(nodeParams.db.payments.getOutgoingPayment(testCase.childIds.head).get.status === OutgoingPaymentStatus.Pending) + assert(nodeParams.db.payments.getOutgoingPayment(testCase.childIds.head).get.status == OutgoingPaymentStatus.Pending) sender.send(relayer, testCase.fulfills.head) val e2 = eventListener.expectMsgType[PaymentSent] - assert(e2.id === testCase.childIds.head) - assert(e2.paymentPreimage === preimage1) - assert(e2.paymentHash === paymentHash1) - assert(e2.parts.length === 1) - assert(e2.recipientAmount === 561.msat) + assert(e2.id == testCase.childIds.head) + assert(e2.paymentPreimage == preimage1) + assert(e2.paymentHash == paymentHash1) + assert(e2.parts.length == 1) + assert(e2.recipientAmount == 561.msat) assert(nodeParams.db.payments.getOutgoingPayment(testCase.childIds.head).get.status.isInstanceOf[OutgoingPaymentStatus.Succeeded]) register.expectNoMessage(100 millis) @@ -299,8 +299,8 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit val probe = TestProbe() probe.send(postRestart, PostRestartHtlcCleaner.GetBrokenHtlcs) val brokenHtlcs = probe.expectMsgType[PostRestartHtlcCleaner.BrokenHtlcs] - assert(brokenHtlcs.notRelayed.map(htlc => (htlc.add.id, htlc.add.channelId)).toSet === testCase.notRelayed) - assert(brokenHtlcs.relayedOut === Map( + assert(brokenHtlcs.notRelayed.map(htlc => (htlc.add.id, htlc.add.channelId)).toSet == testCase.notRelayed) + assert(brokenHtlcs.relayedOut == Map( testCase.origin_1 -> Set(testCase.downstream_1_1).map(htlc => (htlc.channelId, htlc.id)), testCase.origin_2 -> Set(testCase.downstream_2_1, testCase.downstream_2_2, testCase.downstream_2_3).map(htlc => (htlc.channelId, htlc.id)) )) @@ -383,8 +383,8 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit val closingState = localClose(alice, alice2blockchain) alice ! WatchTxConfirmedTriggered(BlockHeight(42), 0, closingState.commitTx) // All committed htlcs timed out except the last two; one will be fulfilled later and the other will timeout later. - assert(closingState.htlcTxs.size === 4) - assert(getHtlcTimeoutTxs(closingState).length === 4) + assert(closingState.htlcTxs.size == 4) + assert(getHtlcTimeoutTxs(closingState).length == 4) val htlcTxs = getHtlcTimeoutTxs(closingState).sortBy(_.tx.txOut.map(_.amount).sum) htlcTxs.reverse.drop(2).zipWithIndex.foreach { case (htlcTx, i) => alice ! WatchTxConfirmedTriggered(BlockHeight(201), i, htlcTx.tx) @@ -479,13 +479,13 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit nodeParams.db.channels.addOrUpdateChannel(upstreamChannel) nodeParams.db.channels.addOrUpdateChannel(downstreamChannel) - assert(Closing.isClosed(downstreamChannel, None) === None) + assert(Closing.isClosed(downstreamChannel, None) == None) val (_, postRestart) = f.createRelayer(nodeParams) sender.send(postRestart, PostRestartHtlcCleaner.GetBrokenHtlcs) val brokenHtlcs = sender.expectMsgType[PostRestartHtlcCleaner.BrokenHtlcs] assert(brokenHtlcs.relayedOut.isEmpty) - assert(brokenHtlcs.notRelayed === htlc_ab.map(htlc => PostRestartHtlcCleaner.IncomingHtlc(htlc.add, None))) + assert(brokenHtlcs.notRelayed == htlc_ab.map(htlc => PostRestartHtlcCleaner.IncomingHtlc(htlc.add, None))) } test("handle a channel relay htlc-fail") { f => @@ -529,7 +529,7 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit // This downstream HTLC has two upstream HTLCs. sender.send(relayer, buildForwardFail(testCase.downstream_1_1, testCase.origin_1)) val fails = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]] :: register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]] :: Nil - assert(fails.toSet === testCase.origin_1.htlcs.map { + assert(fails.toSet == testCase.origin_1.htlcs.map { case (channelId, htlcId) => Register.Forward(ActorRef.noSender, channelId, CMD_FAIL_HTLC(htlcId, Right(TemporaryNodeFailure), commit = true)) }.toSet) @@ -559,7 +559,7 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit // This downstream HTLC has two upstream HTLCs. sender.send(relayer, buildForwardFulfill(testCase.downstream_1_1, testCase.origin_1, preimage1)) val fulfills = register.expectMsgType[Register.Forward[CMD_FULFILL_HTLC]] :: register.expectMsgType[Register.Forward[CMD_FULFILL_HTLC]] :: Nil - assert(fulfills.toSet === testCase.origin_1.htlcs.map { + assert(fulfills.toSet == testCase.origin_1.htlcs.map { case (channelId, htlcId) => Register.Forward(ActorRef.noSender, channelId, CMD_FULFILL_HTLC(htlcId, preimage1, commit = true)) }.toSet) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/receive/InvoicePurgerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/receive/InvoicePurgerSpec.scala index 24b0bbaabb..e7cf259e7e 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/receive/InvoicePurgerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/receive/InvoicePurgerSpec.scala @@ -60,11 +60,11 @@ class InvoicePurgerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("ap }) val now = TimestampMilli.now() - assert(db.listIncomingPayments(0 unixms, now) === expiredPayments ++ pendingPayments ++ paidPayments) - assert(db.listIncomingPayments(now - 100.days, now) === pendingPayments ++ paidPayments) - assert(db.listPendingIncomingPayments(0 unixms, now) === pendingPayments) - assert(db.listReceivedIncomingPayments(0 unixms, now) === paidPayments) - assert(db.listExpiredIncomingPayments(0 unixms, now) === expiredPayments) + assert(db.listIncomingPayments(0 unixms, now) == expiredPayments ++ pendingPayments ++ paidPayments) + assert(db.listIncomingPayments(now - 100.days, now) == pendingPayments ++ paidPayments) + assert(db.listPendingIncomingPayments(0 unixms, now) == pendingPayments) + assert(db.listReceivedIncomingPayments(0 unixms, now) == paidPayments) + assert(db.listExpiredIncomingPayments(0 unixms, now) == expiredPayments) val probe = testKit.createTestProbe[PurgeEvent]() system.eventStream ! EventStream.Subscribe(probe.ref) @@ -75,7 +75,7 @@ class InvoicePurgerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("ap probe.expectMessage(5 seconds, PurgeCompleted) probe.expectNoMessage() assert(db.listExpiredIncomingPayments(0 unixms, now).isEmpty) - assert(db.listIncomingPayments(0 unixms, now) === pendingPayments ++ paidPayments) + assert(db.listIncomingPayments(0 unixms, now) == pendingPayments ++ paidPayments) testKit.stop(purger) } @@ -122,7 +122,7 @@ class InvoicePurgerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("ap // check that subsequent purge runs do not go back > 15 days probe.expectMessage(10 seconds, PurgeCompleted) probe.expectNoMessage() - assert(db.listExpiredIncomingPayments(0 unixms, TimestampMilli.now()) === Seq(expiredPayment3)) + assert(db.listExpiredIncomingPayments(0 unixms, TimestampMilli.now()) == Seq(expiredPayment3)) testKit.stop(purger) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/ChannelRelayerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/ChannelRelayerSpec.scala index 3aba714c92..b3096820c1 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/ChannelRelayerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/ChannelRelayerSpec.scala @@ -57,19 +57,19 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a def expectFwdFail(register: TestProbe[Any], channelId: ByteVector32, cmd: channel.Command): Register.Forward[channel.Command] = { val fwd = register.expectMessageType[Register.Forward[channel.Command]] - assert(fwd.channelId === channelId) - assert(fwd.message === cmd) + assert(fwd.channelId == channelId) + assert(fwd.message == cmd) fwd } def expectFwdAdd(register: TestProbe[Any], channelId: ByteVector32, outAmount: MilliSatoshi, outExpiry: CltvExpiry): Register.Forward[CMD_ADD_HTLC] = { val fwd = register.expectMessageType[Register.Forward[CMD_ADD_HTLC]] - assert(fwd.channelId === channelId) - assert(fwd.message.amount === outAmount) - assert(fwd.message.cltvExpiry === outExpiry) + assert(fwd.channelId == channelId) + assert(fwd.message.amount == outAmount) + assert(fwd.message.cltvExpiry == outExpiry) assert(fwd.message.origin.isInstanceOf[Origin.ChannelRelayedHot]) val o = fwd.message.origin.asInstanceOf[Origin.ChannelRelayedHot] - assert(o.amountOut === outAmount) + assert(o.amountOut == outAmount) fwd } @@ -433,12 +433,12 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a fwd1.message.origin.replyTo ! RES_ADD_SETTLED(fwd1.message.origin, downstream_htlc, testCase.result) val fwd2 = register.expectMessageType[Register.Forward[CMD_FULFILL_HTLC]] - assert(fwd2.channelId === r.add.channelId) - assert(fwd2.message.id === r.add.id) - assert(fwd2.message.r === paymentPreimage) + assert(fwd2.channelId == r.add.channelId) + assert(fwd2.message.id == r.add.id) + assert(fwd2.message.r == paymentPreimage) val paymentRelayed = eventListener.expectMessageType[ChannelPaymentRelayed] - assert(paymentRelayed.copy(timestamp = 0 unixms) === ChannelPaymentRelayed(r.add.amountMsat, r.payload.amountToForward, r.add.paymentHash, r.add.channelId, channelId1, timestamp = 0 unixms)) + assert(paymentRelayed.copy(timestamp = 0 unixms) == ChannelPaymentRelayed(r.add.amountMsat, r.payload.amountToForward, r.add.paymentHash, r.add.channelId, channelId1, timestamp = 0 unixms)) } } @@ -461,30 +461,30 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a channelRelayer ! WrappedLocalChannelUpdate(LocalChannelUpdate(null, channelId_bc, channelUpdate_bc.shortChannelId, c, None, channelUpdate_bc, makeCommitments(channelId_bc, 400000 msat, -5000 msat))) val channels1 = getOutgoingChannels(true) - assert(channels1.size === 2) - assert(channels1.head.channelUpdate === channelUpdate_ab) - assert(channels1.head.toChannelBalance === Relayer.ChannelBalance(a, channelUpdate_ab.shortChannelId, 0 msat, 300000 msat, isPublic = false, isEnabled = true)) - assert(channels1.last.channelUpdate === channelUpdate_bc) - assert(channels1.last.toChannelBalance === Relayer.ChannelBalance(c, channelUpdate_bc.shortChannelId, 400000 msat, 0 msat, isPublic = false, isEnabled = true)) + assert(channels1.size == 2) + assert(channels1.head.channelUpdate == channelUpdate_ab) + assert(channels1.head.toChannelBalance == Relayer.ChannelBalance(a, channelUpdate_ab.shortChannelId, 0 msat, 300000 msat, isPublic = false, isEnabled = true)) + assert(channels1.last.channelUpdate == channelUpdate_bc) + assert(channels1.last.toChannelBalance == Relayer.ChannelBalance(c, channelUpdate_bc.shortChannelId, 400000 msat, 0 msat, isPublic = false, isEnabled = true)) channelRelayer ! WrappedAvailableBalanceChanged(AvailableBalanceChanged(null, channelId_bc, channelUpdate_bc.shortChannelId, makeCommitments(channelId_bc, 200000 msat, 500000 msat))) val channels2 = getOutgoingChannels(true) - assert(channels2.last.commitments.availableBalanceForReceive === 500000.msat && channels2.last.commitments.availableBalanceForSend === 200000.msat) + assert(channels2.last.commitments.availableBalanceForReceive == 500000.msat && channels2.last.commitments.availableBalanceForSend == 200000.msat) channelRelayer ! WrappedAvailableBalanceChanged(AvailableBalanceChanged(null, channelId_ab, channelUpdate_ab.shortChannelId, makeCommitments(channelId_ab, 100000 msat, 200000 msat))) channelRelayer ! WrappedLocalChannelDown(LocalChannelDown(null, channelId_bc, channelUpdate_bc.shortChannelId, c)) val channels3 = getOutgoingChannels(true) - assert(channels3.size === 1 && channels3.head.commitments.availableBalanceForSend === 100000.msat) + assert(channels3.size == 1 && channels3.head.commitments.availableBalanceForSend == 100000.msat) channelRelayer ! WrappedLocalChannelUpdate(LocalChannelUpdate(null, channelId_ab, channelUpdate_ab.shortChannelId, a, None, channelUpdate_ab.copy(channelFlags = ChannelUpdate.ChannelFlags(isEnabled = false, isNode1 = true)), makeCommitments(channelId_ab, 100000 msat, 200000 msat))) val channels4 = getOutgoingChannels(true) assert(channels4.isEmpty) val channels5 = getOutgoingChannels(false) - assert(channels5.size === 1) + assert(channels5.size == 1) channelRelayer ! WrappedLocalChannelUpdate(LocalChannelUpdate(null, channelId_ab, channelUpdate_ab.shortChannelId, a, None, channelUpdate_ab, makeCommitments(channelId_ab, 100000 msat, 200000 msat))) val channels6 = getOutgoingChannels(true) - assert(channels6.size === 1) + assert(channels6.size == 1) // Simulate a chain re-org that changes the shortChannelId: channelRelayer ! WrappedShortChannelIdAssigned(ShortChannelIdAssigned(null, channelId_ab, ShortChannelId(42), Some(channelUpdate_ab.shortChannelId))) @@ -492,8 +492,8 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a // We should receive the updated channel update containing the new shortChannelId: channelRelayer ! WrappedLocalChannelUpdate(LocalChannelUpdate(null, channelId_ab, ShortChannelId(42), a, None, channelUpdate_ab.copy(shortChannelId = ShortChannelId(42)), makeCommitments(channelId_ab, 100000 msat, 200000 msat))) val channels7 = getOutgoingChannels(true) - assert(channels7.size === 1) - assert(channels7.head.channelUpdate.shortChannelId === ShortChannelId(42)) + assert(channels7.size == 1) + assert(channels7.head.channelUpdate.shortChannelId == ShortChannelId(42)) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/NodeRelayerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/NodeRelayerSpec.scala index f851e4627e..26a76adf64 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/NodeRelayerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/NodeRelayerSpec.scala @@ -101,26 +101,26 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl parentRelayer ! NodeRelayer.Relay(payment1) parentRelayer ! NodeRelayer.GetPendingPayments(probe.ref.toClassic) val pending1 = probe.expectMessageType[Map[PaymentKey, ActorRef[NodeRelay.Command]]] - assert(pending1.keySet === Set(PaymentKey(paymentHash1, paymentSecret1))) + assert(pending1.keySet == Set(PaymentKey(paymentHash1, paymentSecret1))) val (paymentHash2, paymentSecret2) = (randomBytes32(), randomBytes32()) val payment2 = createPartialIncomingPacket(paymentHash2, paymentSecret2) parentRelayer ! NodeRelayer.Relay(payment2) parentRelayer ! NodeRelayer.GetPendingPayments(probe.ref.toClassic) val pending2 = probe.expectMessageType[Map[PaymentKey, ActorRef[NodeRelay.Command]]] - assert(pending2.keySet === Set(PaymentKey(paymentHash1, paymentSecret1), PaymentKey(paymentHash2, paymentSecret2))) + assert(pending2.keySet == Set(PaymentKey(paymentHash1, paymentSecret1), PaymentKey(paymentHash2, paymentSecret2))) val payment3a = createPartialIncomingPacket(paymentHash1, paymentSecret2) parentRelayer ! NodeRelayer.Relay(payment3a) parentRelayer ! NodeRelayer.GetPendingPayments(probe.ref.toClassic) val pending3 = probe.expectMessageType[Map[PaymentKey, ActorRef[NodeRelay.Command]]] - assert(pending3.keySet === Set(PaymentKey(paymentHash1, paymentSecret1), PaymentKey(paymentHash2, paymentSecret2), PaymentKey(paymentHash1, paymentSecret2))) + assert(pending3.keySet == Set(PaymentKey(paymentHash1, paymentSecret1), PaymentKey(paymentHash2, paymentSecret2), PaymentKey(paymentHash1, paymentSecret2))) val payment3b = createPartialIncomingPacket(paymentHash1, paymentSecret2) parentRelayer ! NodeRelayer.Relay(payment3b) parentRelayer ! NodeRelayer.GetPendingPayments(probe.ref.toClassic) val pending4 = probe.expectMessageType[Map[PaymentKey, ActorRef[NodeRelay.Command]]] - assert(pending4.keySet === Set(PaymentKey(paymentHash1, paymentSecret1), PaymentKey(paymentHash2, paymentSecret2), PaymentKey(paymentHash1, paymentSecret2))) + assert(pending4.keySet == Set(PaymentKey(paymentHash1, paymentSecret1), PaymentKey(paymentHash2, paymentSecret2), PaymentKey(paymentHash1, paymentSecret2))) register.expectNoMessage(100 millis) } @@ -169,8 +169,8 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl parentRelayer ! NodeRelayer.Relay(incomingMultiPart.head) parentRelayer ! NodeRelayer.GetPendingPayments(probe.ref.toClassic) val pending1 = probe.expectMessageType[Map[PaymentKey, ActorRef[NodeRelay.Command]]] - assert(pending1.size === 1) - assert(pending1.head._1 === PaymentKey(paymentHash, incomingSecret)) + assert(pending1.size == 1) + assert(pending1.head._1 == PaymentKey(paymentHash, incomingSecret)) parentRelayer ! NodeRelayer.RelayComplete(pending1.head._2, paymentHash, incomingSecret) parentRelayer ! NodeRelayer.GetPendingPayments(probe.ref.toClassic) @@ -179,8 +179,8 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl parentRelayer ! NodeRelayer.Relay(incomingMultiPart.head) parentRelayer ! NodeRelayer.GetPendingPayments(probe.ref.toClassic) val pending2 = probe.expectMessageType[Map[PaymentKey, ActorRef[NodeRelay.Command]]] - assert(pending2.size === 1) - assert(pending2.head._1 === pending1.head._1) + assert(pending2.size == 1) + assert(pending2.head._1 == pending1.head._1) assert(pending2.head._2 !== pending1.head._2) } } @@ -194,9 +194,9 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl // after a while the payment times out incomingMultiPart.dropRight(1).foreach { p => val fwd = register.expectMessageType[Register.Forward[CMD_FAIL_HTLC]](30 seconds) - assert(fwd.channelId === p.add.channelId) + assert(fwd.channelId == p.add.channelId) val failure = Right(PaymentTimeout) - assert(fwd.message === CMD_FAIL_HTLC(p.add.id, failure, commit = true)) + assert(fwd.message == CMD_FAIL_HTLC(p.add.id, failure, commit = true)) } parent.expectMessageType[NodeRelayer.RelayComplete] @@ -219,9 +219,9 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl // the extra payment will be rejected val fwd = register.expectMessageType[Register.Forward[CMD_FAIL_HTLC]] - assert(fwd.channelId === extra.add.channelId) + assert(fwd.channelId == extra.add.channelId) val failure = IncorrectOrUnknownPaymentDetails(extra.add.amountMsat, nodeParams.currentBlockHeight) - assert(fwd.message === CMD_FAIL_HTLC(extra.add.id, Right(failure), commit = true)) + assert(fwd.message == CMD_FAIL_HTLC(extra.add.id, Right(failure), commit = true)) register.expectNoMessage(100 millis) } @@ -247,9 +247,9 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl nodeRelayer ! NodeRelay.Relay(i1) val fwd1 = register.expectMessageType[Register.Forward[CMD_FAIL_HTLC]] - assert(fwd1.channelId === i1.add.channelId) + assert(fwd1.channelId == i1.add.channelId) val failure1 = IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight) - assert(fwd1.message === CMD_FAIL_HTLC(i1.add.id, Right(failure1), commit = true)) + assert(fwd1.message == CMD_FAIL_HTLC(i1.add.id, Right(failure1), commit = true)) // Receive new HTLC with different details, but for the same payment hash. val i2 = IncomingPaymentPacket.NodeRelayPacket( @@ -260,9 +260,9 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl nodeRelayer ! NodeRelay.Relay(i2) val fwd2 = register.expectMessageType[Register.Forward[CMD_FAIL_HTLC]] - assert(fwd1.channelId === i1.add.channelId) + assert(fwd1.channelId == i1.add.channelId) val failure2 = IncorrectOrUnknownPaymentDetails(1500 msat, nodeParams.currentBlockHeight) - assert(fwd2.message === CMD_FAIL_HTLC(i2.add.id, Right(failure2), commit = true)) + assert(fwd2.message == CMD_FAIL_HTLC(i2.add.id, Right(failure2), commit = true)) register.expectNoMessage(100 millis) } @@ -277,8 +277,8 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl nodeRelayer ! NodeRelay.Relay(p) val fwd = register.expectMessageType[Register.Forward[CMD_FAIL_HTLC]] - assert(fwd.channelId === p.add.channelId) - assert(fwd.message === CMD_FAIL_HTLC(p.add.id, Right(TrampolineExpiryTooSoon), commit = true)) + assert(fwd.channelId == p.add.channelId) + assert(fwd.message == CMD_FAIL_HTLC(p.add.id, Right(TrampolineExpiryTooSoon), commit = true)) register.expectNoMessage(100 millis) } @@ -293,8 +293,8 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl nodeRelayer ! NodeRelay.Relay(p) val fwd = register.expectMessageType[Register.Forward[CMD_FAIL_HTLC]] - assert(fwd.channelId === p.add.channelId) - assert(fwd.message === CMD_FAIL_HTLC(p.add.id, Right(TrampolineExpiryTooSoon), commit = true)) + assert(fwd.channelId == p.add.channelId) + assert(fwd.message == CMD_FAIL_HTLC(p.add.id, Right(TrampolineExpiryTooSoon), commit = true)) register.expectNoMessage(100 millis) } @@ -314,8 +314,8 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl p.foreach { p => val fwd = register.expectMessageType[Register.Forward[CMD_FAIL_HTLC]] - assert(fwd.channelId === p.add.channelId) - assert(fwd.message === CMD_FAIL_HTLC(p.add.id, Right(TrampolineExpiryTooSoon), commit = true)) + assert(fwd.channelId == p.add.channelId) + assert(fwd.message == CMD_FAIL_HTLC(p.add.id, Right(TrampolineExpiryTooSoon), commit = true)) } register.expectNoMessage(100 millis) @@ -329,8 +329,8 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl nodeRelayer ! NodeRelay.Relay(p) val fwd = register.expectMessageType[Register.Forward[CMD_FAIL_HTLC]] - assert(fwd.channelId === p.add.channelId) - assert(fwd.message === CMD_FAIL_HTLC(p.add.id, Right(TrampolineFeeInsufficient), commit = true)) + assert(fwd.channelId == p.add.channelId) + assert(fwd.message == CMD_FAIL_HTLC(p.add.id, Right(TrampolineFeeInsufficient), commit = true)) register.expectNoMessage(100 millis) } @@ -347,8 +347,8 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl p.foreach { p => val fwd = register.expectMessageType[Register.Forward[CMD_FAIL_HTLC]] - assert(fwd.channelId === p.add.channelId) - assert(fwd.message === CMD_FAIL_HTLC(p.add.id, Right(TrampolineFeeInsufficient), commit = true)) + assert(fwd.channelId == p.add.channelId) + assert(fwd.message == CMD_FAIL_HTLC(p.add.id, Right(TrampolineFeeInsufficient), commit = true)) } register.expectNoMessage(100 millis) @@ -362,8 +362,8 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl nodeRelayer ! NodeRelay.Relay(p) val fwd = register.expectMessageType[Register.Forward[CMD_FAIL_HTLC]] - assert(fwd.channelId === p.add.channelId) - assert(fwd.message === CMD_FAIL_HTLC(p.add.id, Right(InvalidOnionPayload(UInt64(2), 0)), commit = true)) + assert(fwd.channelId == p.add.channelId) + assert(fwd.message == CMD_FAIL_HTLC(p.add.id, Right(InvalidOnionPayload(UInt64(2), 0)), commit = true)) register.expectNoMessage(100 millis) } @@ -380,8 +380,8 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl p.foreach { p => val fwd = register.expectMessageType[Register.Forward[CMD_FAIL_HTLC]] - assert(fwd.channelId === p.add.channelId) - assert(fwd.message === CMD_FAIL_HTLC(p.add.id, Right(InvalidOnionPayload(UInt64(2), 0)), commit = true)) + assert(fwd.channelId == p.add.channelId) + assert(fwd.message == CMD_FAIL_HTLC(p.add.id, Right(InvalidOnionPayload(UInt64(2), 0)), commit = true)) } register.expectNoMessage(100 millis) @@ -402,8 +402,8 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl nodeRelayerAdapters ! PaymentFailed(relayId, paymentHash, LocalFailure(outgoingAmount, Nil, BalanceTooLow) :: Nil) incomingMultiPart.foreach { p => val fwd = register.expectMessageType[Register.Forward[CMD_FAIL_HTLC]] - assert(fwd.channelId === p.add.channelId) - assert(fwd.message === CMD_FAIL_HTLC(p.add.id, Right(TrampolineFeeInsufficient), commit = true)) + assert(fwd.channelId == p.add.channelId) + assert(fwd.message == CMD_FAIL_HTLC(p.add.id, Right(TrampolineFeeInsufficient), commit = true)) } register.expectNoMessage(100 millis) @@ -427,8 +427,8 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl incoming.foreach { p => val fwd = register.expectMessageType[Register.Forward[CMD_FAIL_HTLC]] - assert(fwd.channelId === p.add.channelId) - assert(fwd.message === CMD_FAIL_HTLC(p.add.id, Right(TemporaryNodeFailure), commit = true)) + assert(fwd.channelId == p.add.channelId) + assert(fwd.message == CMD_FAIL_HTLC(p.add.id, Right(TemporaryNodeFailure), commit = true)) } register.expectNoMessage(100 millis) @@ -450,8 +450,8 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl incomingMultiPart.foreach { p => val fwd = register.expectMessageType[Register.Forward[CMD_FAIL_HTLC]] - assert(fwd.channelId === p.add.channelId) - assert(fwd.message === CMD_FAIL_HTLC(p.add.id, Right(TrampolineFeeInsufficient), commit = true)) + assert(fwd.channelId == p.add.channelId) + assert(fwd.message == CMD_FAIL_HTLC(p.add.id, Right(TrampolineFeeInsufficient), commit = true)) } register.expectNoMessage(100 millis) @@ -472,8 +472,8 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl incomingMultiPart.foreach { p => val fwd = register.expectMessageType[Register.Forward[CMD_FAIL_HTLC]] - assert(fwd.channelId === p.add.channelId) - assert(fwd.message === CMD_FAIL_HTLC(p.add.id, Right(FinalIncorrectHtlcAmount(42 msat)), commit = true)) + assert(fwd.channelId == p.add.channelId) + assert(fwd.message == CMD_FAIL_HTLC(p.add.id, Right(FinalIncorrectHtlcAmount(42 msat)), commit = true)) } register.expectNoMessage(100 millis) @@ -488,9 +488,9 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl val routeRequest = router.expectMessageType[RouteRequest] val routeParams = routeRequest.routeParams - assert(routeParams.boundaries.maxFeeProportional === 0) // should be disabled - assert(routeParams.boundaries.maxFeeFlat === incomingAmount - outgoingAmount) - assert(routeParams.boundaries.maxCltv === incomingSinglePart.add.cltvExpiry - outgoingExpiry) + assert(routeParams.boundaries.maxFeeProportional == 0) // should be disabled + assert(routeParams.boundaries.maxFeeFlat == incomingAmount - outgoingAmount) + assert(routeParams.boundaries.maxCltv == incomingSinglePart.add.cltvExpiry - outgoingExpiry) assert(routeParams.includeLocalChannelCost) } @@ -515,8 +515,8 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl nodeRelayerAdapters ! PreimageReceived(paymentHash, paymentPreimage) incomingMultiPart.foreach { p => val fwd = register.expectMessageType[Register.Forward[CMD_FULFILL_HTLC]] - assert(fwd.channelId === p.add.channelId) - assert(fwd.message === CMD_FULFILL_HTLC(p.add.id, paymentPreimage, commit = true)) + assert(fwd.channelId == p.add.channelId) + assert(fwd.message == CMD_FULFILL_HTLC(p.add.id, paymentPreimage, commit = true)) } // If the payment FSM sends us duplicate preimage events, we should not fulfill a second time upstream. @@ -527,7 +527,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl nodeRelayerAdapters ! createSuccessEvent() val relayEvent = eventListener.expectMessageType[TrampolinePaymentRelayed] validateRelayEvent(relayEvent) - assert(relayEvent.incoming.toSet === incomingMultiPart.map(i => PaymentRelayed.Part(i.add.amountMsat, i.add.channelId)).toSet) + assert(relayEvent.incoming.toSet == incomingMultiPart.map(i => PaymentRelayed.Part(i.add.amountMsat, i.add.channelId)).toSet) assert(relayEvent.outgoing.nonEmpty) parent.expectMessageType[NodeRelayer.RelayComplete] register.expectNoMessage(100 millis) @@ -550,13 +550,13 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl nodeRelayerAdapters ! PreimageReceived(paymentHash, paymentPreimage) val incomingAdd = incomingSinglePart.add val fwd = register.expectMessageType[Register.Forward[CMD_FULFILL_HTLC]] - assert(fwd.channelId === incomingAdd.channelId) - assert(fwd.message === CMD_FULFILL_HTLC(incomingAdd.id, paymentPreimage, commit = true)) + assert(fwd.channelId == incomingAdd.channelId) + assert(fwd.message == CMD_FULFILL_HTLC(incomingAdd.id, paymentPreimage, commit = true)) nodeRelayerAdapters ! createSuccessEvent() val relayEvent = eventListener.expectMessageType[TrampolinePaymentRelayed] validateRelayEvent(relayEvent) - assert(relayEvent.incoming === Seq(PaymentRelayed.Part(incomingSinglePart.add.amountMsat, incomingSinglePart.add.channelId))) + assert(relayEvent.incoming == Seq(PaymentRelayed.Part(incomingSinglePart.add.amountMsat, incomingSinglePart.add.channelId))) assert(relayEvent.outgoing.nonEmpty) parent.expectMessageType[NodeRelayer.RelayComplete] register.expectNoMessage(100 millis) @@ -578,27 +578,27 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl val outgoingCfg = mockPayFSM.expectMessageType[SendPaymentConfig] validateOutgoingCfg(outgoingCfg, Upstream.Trampoline(incomingMultiPart.map(_.add))) val outgoingPayment = mockPayFSM.expectMessageType[SendMultiPartPayment] - assert(outgoingPayment.paymentSecret === invoice.paymentSecret.get) // we should use the provided secret - assert(outgoingPayment.paymentMetadata === invoice.paymentMetadata) // we should use the provided metadata - assert(outgoingPayment.totalAmount === outgoingAmount) - assert(outgoingPayment.targetExpiry === outgoingExpiry) - assert(outgoingPayment.targetNodeId === outgoingNodeId) - assert(outgoingPayment.additionalTlvs === Nil) - assert(outgoingPayment.assistedRoutes === hints) + assert(outgoingPayment.paymentSecret == invoice.paymentSecret.get) // we should use the provided secret + assert(outgoingPayment.paymentMetadata == invoice.paymentMetadata) // we should use the provided metadata + assert(outgoingPayment.totalAmount == outgoingAmount) + assert(outgoingPayment.targetExpiry == outgoingExpiry) + assert(outgoingPayment.targetNodeId == outgoingNodeId) + assert(outgoingPayment.additionalTlvs == Nil) + assert(outgoingPayment.assistedRoutes == hints) // those are adapters for pay-fsm messages val nodeRelayerAdapters = outgoingPayment.replyTo nodeRelayerAdapters ! PreimageReceived(paymentHash, paymentPreimage) incomingMultiPart.foreach { p => val fwd = register.expectMessageType[Register.Forward[CMD_FULFILL_HTLC]] - assert(fwd.channelId === p.add.channelId) - assert(fwd.message === CMD_FULFILL_HTLC(p.add.id, paymentPreimage, commit = true)) + assert(fwd.channelId == p.add.channelId) + assert(fwd.message == CMD_FULFILL_HTLC(p.add.id, paymentPreimage, commit = true)) } nodeRelayerAdapters ! createSuccessEvent() val relayEvent = eventListener.expectMessageType[TrampolinePaymentRelayed] validateRelayEvent(relayEvent) - assert(relayEvent.incoming === incomingMultiPart.map(i => PaymentRelayed.Part(i.add.amountMsat, i.add.channelId))) + assert(relayEvent.incoming == incomingMultiPart.map(i => PaymentRelayed.Part(i.add.amountMsat, i.add.channelId))) assert(relayEvent.outgoing.nonEmpty) parent.expectMessageType[NodeRelayer.RelayComplete] register.expectNoMessage(100 millis) @@ -620,26 +620,26 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl val outgoingCfg = mockPayFSM.expectMessageType[SendPaymentConfig] validateOutgoingCfg(outgoingCfg, Upstream.Trampoline(incomingMultiPart.map(_.add))) val outgoingPayment = mockPayFSM.expectMessageType[SendPaymentToNode] - assert(outgoingPayment.finalPayload.amount === outgoingAmount) - assert(outgoingPayment.finalPayload.expiry === outgoingExpiry) - assert(outgoingPayment.finalPayload.paymentMetadata === invoice.paymentMetadata) // we should use the provided metadata - assert(outgoingPayment.targetNodeId === outgoingNodeId) - assert(outgoingPayment.assistedRoutes === hints) + assert(outgoingPayment.finalPayload.amount == outgoingAmount) + assert(outgoingPayment.finalPayload.expiry == outgoingExpiry) + assert(outgoingPayment.finalPayload.paymentMetadata == invoice.paymentMetadata) // we should use the provided metadata + assert(outgoingPayment.targetNodeId == outgoingNodeId) + assert(outgoingPayment.assistedRoutes == hints) // those are adapters for pay-fsm messages val nodeRelayerAdapters = outgoingPayment.replyTo nodeRelayerAdapters ! PreimageReceived(paymentHash, paymentPreimage) incomingMultiPart.foreach { p => val fwd = register.expectMessageType[Register.Forward[CMD_FULFILL_HTLC]] - assert(fwd.channelId === p.add.channelId) - assert(fwd.message === CMD_FULFILL_HTLC(p.add.id, paymentPreimage, commit = true)) + assert(fwd.channelId == p.add.channelId) + assert(fwd.message == CMD_FULFILL_HTLC(p.add.id, paymentPreimage, commit = true)) } nodeRelayerAdapters ! createSuccessEvent() val relayEvent = eventListener.expectMessageType[TrampolinePaymentRelayed] validateRelayEvent(relayEvent) - assert(relayEvent.incoming === incomingMultiPart.map(i => PaymentRelayed.Part(i.add.amountMsat, i.add.channelId))) - assert(relayEvent.outgoing.length === 1) + assert(relayEvent.incoming == incomingMultiPart.map(i => PaymentRelayed.Part(i.add.amountMsat, i.add.channelId))) + assert(relayEvent.outgoing.length == 1) parent.expectMessageType[NodeRelayer.RelayComplete] register.expectNoMessage(100 millis) } @@ -659,34 +659,34 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl incomingMultiPart.foreach { p => val fwd = register.expectMessageType[Register.Forward[CMD_FAIL_HTLC]] - assert(fwd.channelId === p.add.channelId) - assert(fwd.message === CMD_FAIL_HTLC(p.add.id, Right(InvalidOnionPayload(UInt64(8), 0)), commit = true)) + assert(fwd.channelId == p.add.channelId) + assert(fwd.message == CMD_FAIL_HTLC(p.add.id, Right(InvalidOnionPayload(UInt64(8), 0)), commit = true)) } } def validateOutgoingCfg(outgoingCfg: SendPaymentConfig, upstream: Upstream): Unit = { assert(!outgoingCfg.publishEvent) assert(!outgoingCfg.storeInDb) - assert(outgoingCfg.paymentHash === paymentHash) - assert(outgoingCfg.invoice === None) - assert(outgoingCfg.recipientAmount === outgoingAmount) - assert(outgoingCfg.recipientNodeId === outgoingNodeId) - assert(outgoingCfg.upstream === upstream) + assert(outgoingCfg.paymentHash == paymentHash) + assert(outgoingCfg.invoice == None) + assert(outgoingCfg.recipientAmount == outgoingAmount) + assert(outgoingCfg.recipientNodeId == outgoingNodeId) + assert(outgoingCfg.upstream == upstream) } def validateOutgoingPayment(outgoingPayment: SendMultiPartPayment): Unit = { assert(outgoingPayment.paymentSecret !== incomingSecret) // we should generate a new outgoing secret - assert(outgoingPayment.totalAmount === outgoingAmount) - assert(outgoingPayment.targetExpiry === outgoingExpiry) - assert(outgoingPayment.targetNodeId === outgoingNodeId) - assert(outgoingPayment.additionalTlvs === Seq(OnionPaymentPayloadTlv.TrampolineOnion(nextTrampolinePacket))) - assert(outgoingPayment.assistedRoutes === Nil) + assert(outgoingPayment.totalAmount == outgoingAmount) + assert(outgoingPayment.targetExpiry == outgoingExpiry) + assert(outgoingPayment.targetNodeId == outgoingNodeId) + assert(outgoingPayment.additionalTlvs == Seq(OnionPaymentPayloadTlv.TrampolineOnion(nextTrampolinePacket))) + assert(outgoingPayment.assistedRoutes == Nil) } def validateRelayEvent(e: TrampolinePaymentRelayed): Unit = { - assert(e.amountIn === incomingAmount) + assert(e.amountIn == incomingAmount) assert(e.amountOut >= outgoingAmount) // outgoingAmount + routing fees - assert(e.paymentHash === paymentHash) + assert(e.paymentHash == paymentHash) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/RelayerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/RelayerSpec.scala index 12d477466f..0df90707cb 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/RelayerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/RelayerSpec.scala @@ -102,8 +102,8 @@ class RelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("applicat relayer ! RelayForward(add_ab) val fp = paymentHandler.expectMessageType[FinalPacket] - assert(fp.add === add_ab) - assert(fp.payload === PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, paymentSecret, None)) + assert(fp.add == add_ab) + assert(fp.payload == PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, paymentSecret, None)) register.expectNoMessage(50 millis) } @@ -117,21 +117,21 @@ class RelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("applicat val totalAmount = finalAmount * 3 val trampolineHops = NodeHop(a, b, channelUpdate_ab.cltvExpiryDelta, 0 msat) :: Nil val Success((trampolineAmount, trampolineExpiry, trampolineOnion)) = OutgoingPaymentPacket.buildTrampolinePacket(paymentHash, trampolineHops, PaymentOnion.createMultiPartPayload(finalAmount, totalAmount, finalExpiry, paymentSecret, None)) - assert(trampolineAmount === finalAmount) - assert(trampolineExpiry === finalExpiry) + assert(trampolineAmount == finalAmount) + assert(trampolineExpiry == finalExpiry) val Success((cmd, _)) = buildCommand(ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, channelHopFromUpdate(a, b, channelUpdate_ab) :: Nil, PaymentOnion.createTrampolinePayload(trampolineAmount, trampolineAmount, trampolineExpiry, randomBytes32(), trampolineOnion.packet)) - assert(cmd.amount === finalAmount) - assert(cmd.cltvExpiry === finalExpiry) + assert(cmd.amount == finalAmount) + assert(cmd.cltvExpiry == finalExpiry) val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amount, cmd.paymentHash, cmd.cltvExpiry, cmd.onion) relayer ! RelayForward(add_ab) val fp = paymentHandler.expectMessageType[FinalPacket] - assert(fp.add === add_ab) - assert(fp.payload.amount === finalAmount) - assert(fp.payload.totalAmount === totalAmount) - assert(fp.payload.expiry === finalExpiry) - assert(fp.payload.paymentSecret === paymentSecret) + assert(fp.add == add_ab) + assert(fp.payload.amount == finalAmount) + assert(fp.payload.totalAmount == totalAmount) + assert(fp.payload.expiry == finalExpiry) + assert(fp.payload.paymentSecret == paymentSecret) register.expectNoMessage(50 millis) } @@ -147,9 +147,9 @@ class RelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("applicat relayer ! RelayForward(add_ab) val fail = register.expectMessageType[Register.Forward[CMD_FAIL_MALFORMED_HTLC]].message - assert(fail.id === add_ab.id) + assert(fail.id == add_ab.id) assert(fail.onionHash == Sphinx.hash(add_ab.onionRoutingPacket)) - assert(fail.failureCode === (FailureMessageCodecs.BADONION | FailureMessageCodecs.PERM | 5)) + assert(fail.failureCode == (FailureMessageCodecs.BADONION | FailureMessageCodecs.PERM | 5)) register.expectNoMessage(50 millis) } @@ -170,7 +170,7 @@ class RelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("applicat relayer ! RelayForward(add_ab) val fail = register.expectMessageType[Register.Forward[CMD_FAIL_HTLC]].message - assert(fail.id === add_ab.id) + assert(fail.id == add_ab.id) assert(fail.reason == Right(RequiredNodeFeatureMissing)) register.expectNoMessage(50 millis) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/AnnouncementsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/AnnouncementsSpec.scala index b92e5f022e..b7e4b52f35 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/AnnouncementsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/AnnouncementsSpec.scala @@ -50,7 +50,7 @@ class AnnouncementsSpec extends AnyFunSuite { val bitcoin_b_sig = Announcements.signChannelAnnouncement(witness, bitcoin_b) val ann = makeChannelAnnouncement(Block.RegtestGenesisBlock.hash, ShortChannelId(42L), node_a.publicKey, node_b.publicKey, bitcoin_a.publicKey, bitcoin_b.publicKey, node_a_sig, node_b_sig, bitcoin_a_sig, bitcoin_b_sig) assert(checkSigs(ann)) - assert(checkSigs(ann.copy(nodeId1 = randomKey().publicKey)) === false) + assert(checkSigs(ann.copy(nodeId1 = randomKey().publicKey)) == false) } test("create valid signed node announcement") { @@ -66,7 +66,7 @@ class AnnouncementsSpec extends AnyFunSuite { ) val ann = makeNodeAnnouncement(Alice.nodeParams.privateKey, Alice.nodeParams.alias, Alice.nodeParams.color, Alice.nodeParams.publicAddresses, features.nodeAnnouncementFeatures()) // Features should be filtered to only include node_announcement related features. - assert(ann.features === Features( + assert(ann.features == Features( Features.DataLossProtect -> FeatureSupport.Optional, Features.ChannelRangeQueries -> FeatureSupport.Optional, Features.ChannelRangeQueriesExtended -> FeatureSupport.Optional, @@ -75,7 +75,7 @@ class AnnouncementsSpec extends AnyFunSuite { Features.BasicMultiPartPayment -> FeatureSupport.Optional, )) assert(checkSig(ann)) - assert(checkSig(ann.copy(timestamp = 153 unixsec)) === false) + assert(checkSig(ann.copy(timestamp = 153 unixsec)) == false) } test("sort node announcement addresses") { @@ -87,7 +87,7 @@ class AnnouncementsSpec extends AnyFunSuite { ) val ann = makeNodeAnnouncement(Alice.nodeParams.privateKey, Alice.nodeParams.alias, Alice.nodeParams.color, addresses, Alice.nodeParams.features.nodeAnnouncementFeatures()) assert(checkSig(ann)) - assert(ann.addresses === List( + assert(ann.addresses == List( NodeAddress.fromParts("140.82.121.4", 9735).get, NodeAddress.fromParts("2620:1ec:c11:0:0:0:0:200", 9735).get, NodeAddress.fromParts("hsmithsxurybd7uh.onion", 9735).get, @@ -96,13 +96,13 @@ class AnnouncementsSpec extends AnyFunSuite { } test("nodeParams.nodeId equals nodeParams.privateKey.publicKey") { - assert(Alice.nodeParams.nodeId === Alice.nodeParams.privateKey.publicKey) + assert(Alice.nodeParams.nodeId == Alice.nodeParams.privateKey.publicKey) } test("create valid signed channel update announcement") { val ann = makeChannelUpdate(Block.RegtestGenesisBlock.hash, Alice.nodeParams.privateKey, randomKey().publicKey, ShortChannelId(45561L), Alice.nodeParams.channelConf.expiryDelta, Alice.nodeParams.channelConf.htlcMinimum, Alice.nodeParams.relayParams.publicChannelFees.feeBase, Alice.nodeParams.relayParams.publicChannelFees.feeProportionalMillionths, 500000000 msat) assert(checkSig(ann, Alice.nodeParams.nodeId)) - assert(checkSig(ann, randomKey().publicKey) === false) + assert(checkSig(ann, randomKey().publicKey) == false) } test("check flags") { @@ -135,6 +135,6 @@ class AnnouncementsSpec extends AnyFunSuite { val ann = nodeAnnouncementCodec.decode(encoded.bits).require.value assert(ann.features.hasFeature(Features.PaymentMetadata)) assert(checkSig(ann)) - assert(nodeAnnouncementCodec.encode(ann).require.bytes === encoded) + assert(nodeAnnouncementCodec.encode(ann).require.bytes == encoded) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/BaseRouterSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/BaseRouterSpec.scala index 155df8c7dd..625f814d44 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/BaseRouterSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/BaseRouterSpec.scala @@ -108,13 +108,13 @@ abstract class BaseRouterSpec extends TestKitBaseClass with FixtureAnyFunSuiteLi // and e --(4)--> f (we are a) within(30 seconds) { // first we make sure that we correctly resolve channelId+direction to nodeId - assert(ChannelDesc(update_ab, chan_ab) === ChannelDesc(chan_ab.shortChannelId, a, b)) - assert(ChannelDesc(update_bc, chan_bc) === ChannelDesc(chan_bc.shortChannelId, b, c)) - assert(ChannelDesc(update_cd, chan_cd) === ChannelDesc(chan_cd.shortChannelId, c, d)) - assert(ChannelDesc(update_ef, chan_ef) === ChannelDesc(chan_ef.shortChannelId, e, f)) - assert(ChannelDesc(update_ag_private, PrivateChannel(scid_ag_private, channelId_ag_private, a, g, None, None, ChannelMeta(1000 msat, 2000 msat))) === ChannelDesc(scid_ag_private, a, g)) - assert(ChannelDesc(update_ag_private, PrivateChannel(scid_ag_private, channelId_ag_private, g, a, None, None, ChannelMeta(2000 msat, 1000 msat))) === ChannelDesc(scid_ag_private, a, g)) - assert(ChannelDesc(update_gh, chan_gh) === ChannelDesc(chan_gh.shortChannelId, g, h)) + assert(ChannelDesc(update_ab, chan_ab) == ChannelDesc(chan_ab.shortChannelId, a, b)) + assert(ChannelDesc(update_bc, chan_bc) == ChannelDesc(chan_bc.shortChannelId, b, c)) + assert(ChannelDesc(update_cd, chan_cd) == ChannelDesc(chan_cd.shortChannelId, c, d)) + assert(ChannelDesc(update_ef, chan_ef) == ChannelDesc(chan_ef.shortChannelId, e, f)) + assert(ChannelDesc(update_ag_private, PrivateChannel(scid_ag_private, channelId_ag_private, a, g, None, None, ChannelMeta(1000 msat, 2000 msat))) == ChannelDesc(scid_ag_private, a, g)) + assert(ChannelDesc(update_ag_private, PrivateChannel(scid_ag_private, channelId_ag_private, g, a, None, None, ChannelMeta(2000 msat, 1000 msat))) == ChannelDesc(scid_ag_private, a, g)) + assert(ChannelDesc(update_gh, chan_gh) == ChannelDesc(chan_gh.shortChannelId, g, h)) // let's set up the router val sender = TestProbe() @@ -153,11 +153,11 @@ abstract class BaseRouterSpec extends TestKitBaseClass with FixtureAnyFunSuiteLi // then private channels sender.send(router, LocalChannelUpdate(sender.ref, channelId_ag_private, scid_ag_private, g, None, update_ag_private, CommitmentsSpec.makeCommitments(30000000 msat, 8000000 msat, a, g, announceChannel = false))) // watcher receives the get tx requests - assert(watcher.expectMsgType[ValidateRequest].ann === chan_ab) - assert(watcher.expectMsgType[ValidateRequest].ann === chan_bc) - assert(watcher.expectMsgType[ValidateRequest].ann === chan_cd) - assert(watcher.expectMsgType[ValidateRequest].ann === chan_ef) - assert(watcher.expectMsgType[ValidateRequest].ann === chan_gh) + assert(watcher.expectMsgType[ValidateRequest].ann == chan_ab) + assert(watcher.expectMsgType[ValidateRequest].ann == chan_bc) + assert(watcher.expectMsgType[ValidateRequest].ann == chan_cd) + assert(watcher.expectMsgType[ValidateRequest].ann == chan_ef) + assert(watcher.expectMsgType[ValidateRequest].ann == chan_gh) // and answers with valid scripts watcher.send(router, ValidateResult(chan_ab, Right((Transaction(version = 0, txIn = Nil, txOut = TxOut(publicChannelCapacity, write(pay2wsh(Scripts.multiSig2of2(funding_a, funding_b)))) :: Nil, lockTime = 0), UtxoStatus.Unspent)))) watcher.send(router, ValidateResult(chan_bc, Right((Transaction(version = 0, txIn = Nil, txOut = TxOut(publicChannelCapacity, write(pay2wsh(Scripts.multiSig2of2(funding_b, funding_c)))) :: Nil, lockTime = 0), UtxoStatus.Unspent)))) @@ -172,7 +172,7 @@ abstract class BaseRouterSpec extends TestKitBaseClass with FixtureAnyFunSuiteLi watcher.expectMsgType[WatchExternalChannelSpent].shortChannelId, watcher.expectMsgType[WatchExternalChannelSpent].shortChannelId, ) - assert(watchedShortChannelIds === Set(scid_ab, scid_bc, scid_cd, scid_ef, scid_gh)) + assert(watchedShortChannelIds == Set(scid_ab, scid_bc, scid_cd, scid_ef, scid_gh)) // all messages are acked peerConnection.expectMsgAllOf( GossipDecision.Accepted(chan_ab), @@ -205,7 +205,7 @@ abstract class BaseRouterSpec extends TestKitBaseClass with FixtureAnyFunSuiteLi val channels = sender.expectMsgType[Iterable[ChannelAnnouncement]] sender.send(router, GetChannelUpdates) val updates = sender.expectMsgType[Iterable[ChannelUpdate]] - nodes.size === 8 && channels.size === 5 && updates.size === 11 + nodes.size == 8 && channels.size == 5 && updates.size == 11 }, max = 10 seconds, interval = 1 second) withFixture(test.toNoArgTest(FixtureParam(nodeParams, router, watcher))) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRangeQueriesSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRangeQueriesSpec.scala index 50aea8114d..15cbbc832f 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRangeQueriesSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRangeQueriesSpec.scala @@ -109,25 +109,25 @@ class ChannelRangeQueriesSpec extends AnyFunSuite { assert(getChannelDigestInfo(channels)(ab.shortChannelId) == (Timestamps(now, now), Checksums(1697591108L, 3692323747L))) // no extended info but we know the channel: we ask for the updates - assert(computeFlag(channels)(ab.shortChannelId, None, None, includeNodeAnnouncements = false) === (INCLUDE_CHANNEL_UPDATE_1 | INCLUDE_CHANNEL_UPDATE_2)) - assert(computeFlag(channels)(ab.shortChannelId, None, None, includeNodeAnnouncements = true) === (INCLUDE_CHANNEL_UPDATE_1 | INCLUDE_CHANNEL_UPDATE_2 | INCLUDE_NODE_ANNOUNCEMENT_1 | INCLUDE_NODE_ANNOUNCEMENT_2)) + assert(computeFlag(channels)(ab.shortChannelId, None, None, includeNodeAnnouncements = false) == (INCLUDE_CHANNEL_UPDATE_1 | INCLUDE_CHANNEL_UPDATE_2)) + assert(computeFlag(channels)(ab.shortChannelId, None, None, includeNodeAnnouncements = true) == (INCLUDE_CHANNEL_UPDATE_1 | INCLUDE_CHANNEL_UPDATE_2 | INCLUDE_NODE_ANNOUNCEMENT_1 | INCLUDE_NODE_ANNOUNCEMENT_2)) // same checksums, newer timestamps: we don't ask anything - assert(computeFlag(channels)(ab.shortChannelId, Some(Timestamps(now + 1, now + 1)), Some(Checksums(1697591108L, 3692323747L)), includeNodeAnnouncements = true) === 0) + assert(computeFlag(channels)(ab.shortChannelId, Some(Timestamps(now + 1, now + 1)), Some(Checksums(1697591108L, 3692323747L)), includeNodeAnnouncements = true) == 0) // different checksums, newer timestamps: we ask for the updates - assert(computeFlag(channels)(ab.shortChannelId, Some(Timestamps(now + 1, now)), Some(Checksums(154654604, 3692323747L)), includeNodeAnnouncements = true) === (INCLUDE_CHANNEL_UPDATE_1 | INCLUDE_NODE_ANNOUNCEMENT_1 | INCLUDE_NODE_ANNOUNCEMENT_2)) - assert(computeFlag(channels)(ab.shortChannelId, Some(Timestamps(now, now + 1)), Some(Checksums(1697591108L, 45664546)), includeNodeAnnouncements = true) === (INCLUDE_CHANNEL_UPDATE_2 | INCLUDE_NODE_ANNOUNCEMENT_1 | INCLUDE_NODE_ANNOUNCEMENT_2)) - assert(computeFlag(channels)(ab.shortChannelId, Some(Timestamps(now + 1, now + 1)), Some(Checksums(154654604, 45664546 + 6)), includeNodeAnnouncements = true) === (INCLUDE_CHANNEL_UPDATE_1 | INCLUDE_CHANNEL_UPDATE_2 | INCLUDE_NODE_ANNOUNCEMENT_1 | INCLUDE_NODE_ANNOUNCEMENT_2)) + assert(computeFlag(channels)(ab.shortChannelId, Some(Timestamps(now + 1, now)), Some(Checksums(154654604, 3692323747L)), includeNodeAnnouncements = true) == (INCLUDE_CHANNEL_UPDATE_1 | INCLUDE_NODE_ANNOUNCEMENT_1 | INCLUDE_NODE_ANNOUNCEMENT_2)) + assert(computeFlag(channels)(ab.shortChannelId, Some(Timestamps(now, now + 1)), Some(Checksums(1697591108L, 45664546)), includeNodeAnnouncements = true) == (INCLUDE_CHANNEL_UPDATE_2 | INCLUDE_NODE_ANNOUNCEMENT_1 | INCLUDE_NODE_ANNOUNCEMENT_2)) + assert(computeFlag(channels)(ab.shortChannelId, Some(Timestamps(now + 1, now + 1)), Some(Checksums(154654604, 45664546 + 6)), includeNodeAnnouncements = true) == (INCLUDE_CHANNEL_UPDATE_1 | INCLUDE_CHANNEL_UPDATE_2 | INCLUDE_NODE_ANNOUNCEMENT_1 | INCLUDE_NODE_ANNOUNCEMENT_2)) // different checksums, older timestamps: we don't ask anything - assert(computeFlag(channels)(ab.shortChannelId, Some(Timestamps(now - 1, now)), Some(Checksums(154654604, 3692323747L)), includeNodeAnnouncements = true) === 0) - assert(computeFlag(channels)(ab.shortChannelId, Some(Timestamps(now, now - 1)), Some(Checksums(1697591108L, 45664546)), includeNodeAnnouncements = true) === 0) - assert(computeFlag(channels)(ab.shortChannelId, Some(Timestamps(now - 1, now - 1)), Some(Checksums(154654604, 45664546)), includeNodeAnnouncements = true) === 0) + assert(computeFlag(channels)(ab.shortChannelId, Some(Timestamps(now - 1, now)), Some(Checksums(154654604, 3692323747L)), includeNodeAnnouncements = true) == 0) + assert(computeFlag(channels)(ab.shortChannelId, Some(Timestamps(now, now - 1)), Some(Checksums(1697591108L, 45664546)), includeNodeAnnouncements = true) == 0) + assert(computeFlag(channels)(ab.shortChannelId, Some(Timestamps(now - 1, now - 1)), Some(Checksums(154654604, 45664546)), includeNodeAnnouncements = true) == 0) // missing channel update: we ask for it - assert(computeFlag(channels)(cd.shortChannelId, Some(Timestamps(now, now)), Some(Checksums(3297511804L, 3297511804L)), includeNodeAnnouncements = true) === (INCLUDE_CHANNEL_UPDATE_2 | INCLUDE_NODE_ANNOUNCEMENT_1 | INCLUDE_NODE_ANNOUNCEMENT_2)) + assert(computeFlag(channels)(cd.shortChannelId, Some(Timestamps(now, now)), Some(Checksums(3297511804L, 3297511804L)), includeNodeAnnouncements = true) == (INCLUDE_CHANNEL_UPDATE_2 | INCLUDE_NODE_ANNOUNCEMENT_1 | INCLUDE_NODE_ANNOUNCEMENT_2)) // unknown channel: we ask everything - assert(computeFlag(channels)(ef.shortChannelId, None, None, includeNodeAnnouncements = false) === (INCLUDE_CHANNEL_ANNOUNCEMENT | INCLUDE_CHANNEL_UPDATE_1 | INCLUDE_CHANNEL_UPDATE_2)) - assert(computeFlag(channels)(ef.shortChannelId, None, None, includeNodeAnnouncements = true) === (INCLUDE_CHANNEL_ANNOUNCEMENT | INCLUDE_CHANNEL_UPDATE_1 | INCLUDE_CHANNEL_UPDATE_2 | INCLUDE_NODE_ANNOUNCEMENT_1 | INCLUDE_NODE_ANNOUNCEMENT_2)) + assert(computeFlag(channels)(ef.shortChannelId, None, None, includeNodeAnnouncements = false) == (INCLUDE_CHANNEL_ANNOUNCEMENT | INCLUDE_CHANNEL_UPDATE_1 | INCLUDE_CHANNEL_UPDATE_2)) + assert(computeFlag(channels)(ef.shortChannelId, None, None, includeNodeAnnouncements = true) == (INCLUDE_CHANNEL_ANNOUNCEMENT | INCLUDE_CHANNEL_UPDATE_1 | INCLUDE_CHANNEL_UPDATE_2 | INCLUDE_NODE_ANNOUNCEMENT_1 | INCLUDE_NODE_ANNOUNCEMENT_2)) } def makeShortChannelIds(height: BlockHeight, count: Int): List[ShortChannelId] = { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRouterIntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRouterIntegrationSpec.scala index ad98627e6f..daa0d8ed65 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRouterIntegrationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRouterIntegrationSpec.scala @@ -39,7 +39,7 @@ class ChannelRouterIntegrationSpec extends TestKitBaseClass with FixtureAnyFunSu reachNormal(channels, testTags) - awaitAssert(router.stateData.privateChannels.size === 1) + awaitAssert(router.stateData.privateChannels.size == 1) { // only the local channel_update is known (bob won't send his before the channel is deeply buried) @@ -69,7 +69,7 @@ class ChannelRouterIntegrationSpec extends TestKitBaseClass with FixtureAnyFunSu val fundingTx = reachNormal(channels, testTags) - awaitAssert(router.stateData.privateChannels.size === 1) + awaitAssert(router.stateData.privateChannels.size == 1) { val pc = router.stateData.privateChannels.values.head diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/GraphSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/GraphSpec.scala index 9cac18440f..942ede4076 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/GraphSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/GraphSpec.scala @@ -62,10 +62,10 @@ class GraphSpec extends AnyFunSuite { .addVertex(e) assert(graph.containsVertex(a) && graph.containsVertex(e)) - assert(graph.vertexSet().size === 5) + assert(graph.vertexSet().size == 5) val otherGraph = graph.addVertex(a) // adding the same vertex twice! - assert(otherGraph.vertexSet().size === 5) + assert(otherGraph.vertexSet().size == 5) // add some edges to the graph val edgeAB = makeEdge(1L, a, b, 0 msat, 0) @@ -81,14 +81,14 @@ class GraphSpec extends AnyFunSuite { .addEdge(edgeDC) .addEdge(edgeCE) - assert(graphWithEdges.edgesOf(a).size === 2) - assert(graphWithEdges.edgesOf(b).size === 1) - assert(graphWithEdges.edgesOf(c).size === 1) - assert(graphWithEdges.edgesOf(d).size === 1) - assert(graphWithEdges.edgesOf(e).size === 0) + assert(graphWithEdges.edgesOf(a).size == 2) + assert(graphWithEdges.edgesOf(b).size == 1) + assert(graphWithEdges.edgesOf(c).size == 1) + assert(graphWithEdges.edgesOf(d).size == 1) + assert(graphWithEdges.edgesOf(e).size == 0) val withRemovedEdges = graphWithEdges.removeEdge(edgeAD.desc) - assert(withRemovedEdges.edgesOf(d).size === 1) + assert(withRemovedEdges.edgesOf(d).size == 1) } test("instantiate a graph adding edges only") { @@ -106,10 +106,10 @@ class GraphSpec extends AnyFunSuite { .addEdge(edgeCE) .addEdge(edgeBE) - assert(graph.vertexSet().size === 5) - assert(graph.edgesOf(c).size === 1) - assert(graph.getIncomingEdgesOf(c).size === 2) - assert(graph.edgeSet().size === 6) + assert(graph.vertexSet().size == 5) + assert(graph.edgesOf(c).size == 1) + assert(graph.getIncomingEdgesOf(c).size == 2) + assert(graph.edgeSet().size == 6) } test("containsEdge should return true if the graph contains that edge, false otherwise") { @@ -139,14 +139,14 @@ class GraphSpec extends AnyFunSuite { val edgeAD = makeEdge(3L, a, d, 0 msat, 0) val edgeDC = makeEdge(4L, d, c, 0 msat, 0) - assert(graph.edgeSet().size === 6) + assert(graph.edgeSet().size == 6) assert(graph.containsEdge(edgeBE.desc)) val withRemovedEdge = graph.removeEdge(edgeBE.desc) - assert(withRemovedEdge.edgeSet().size === 5) + assert(withRemovedEdge.edgeSet().size == 5) val withRemovedList = graph.removeEdges(Seq(edgeAD.desc, edgeDC.desc)) - assert(withRemovedList.edgeSet().size === 4) + assert(withRemovedList.edgeSet().size == 4) val withoutAnyIncomingEdgeInE = graph.removeEdges(Seq(edgeBE.desc, edgeCE.desc)) assert(withoutAnyIncomingEdgeInE.containsVertex(e)) @@ -161,19 +161,19 @@ class GraphSpec extends AnyFunSuite { )) val edgesAB = graph.getEdgesBetween(a, b) - assert(edgesAB.size === 1) // there should be an edge a --> b - assert(edgesAB.head.desc.a === a) - assert(edgesAB.head.desc.b === b) + assert(edgesAB.size == 1) // there should be an edge a --> b + assert(edgesAB.head.desc.a == a) + assert(edgesAB.head.desc.b == b) val bIncoming = graph.getIncomingEdgesOf(b) - assert(bIncoming.size === 1) - assert(bIncoming.exists(_.desc.a === a)) // there should be an edge a --> b - assert(bIncoming.exists(_.desc.b === b)) + assert(bIncoming.size == 1) + assert(bIncoming.exists(_.desc.a == a)) // there should be an edge a --> b + assert(bIncoming.exists(_.desc.b == b)) val bOutgoing = graph.edgesOf(b) - assert(bOutgoing.size === 1) - assert(bOutgoing.exists(_.desc.a === b)) - assert(bOutgoing.exists(_.desc.b === c)) + assert(bOutgoing.size == 1) + assert(bOutgoing.exists(_.desc.a == b)) + assert(bOutgoing.exists(_.desc.b == c)) } test("there can be multiple edges between the same vertices") { @@ -192,13 +192,13 @@ class GraphSpec extends AnyFunSuite { val mutatedGraph2 = mutatedGraph.addEdge(edgeForTheSameChannel) assert(mutatedGraph2.edgesOf(a).size == 3) // A --> B , A --> B , A --> D - assert(mutatedGraph2.getEdgesBetween(a, b).size === 2) - assert(mutatedGraph2.getEdge(edgeForTheSameChannel).get.params.relayFees.feeBase === 30.msat) + assert(mutatedGraph2.getEdgesBetween(a, b).size == 2) + assert(mutatedGraph2.getEdge(edgeForTheSameChannel).get.params.relayFees.feeBase == 30.msat) } test("remove a vertex with incoming edges and check those edges are removed too") { val graph = makeTestGraph() - assert(graph.vertexSet().size === 5) + assert(graph.vertexSet().size == 5) assert(graph.containsVertex(e)) assert(graph.containsEdge(descFromNodes(5, c, e))) assert(graph.containsEdge(descFromNodes(6, b, e))) @@ -206,7 +206,7 @@ class GraphSpec extends AnyFunSuite { // E has 2 incoming edges val withoutE = graph.removeVertex(e) - assert(withoutE.vertexSet().size === 4) + assert(withoutE.vertexSet().size == 4) assert(!withoutE.containsVertex(e)) assert(!withoutE.containsEdge(descFromNodes(5, c, e))) assert(!withoutE.containsEdge(descFromNodes(6, b, e))) @@ -219,19 +219,19 @@ class GraphSpec extends AnyFunSuite { val edgeDC = makeEdge(4L, d, c, 0 msat, 0, capacity = 800 sat, balance_opt = Some(50000 msat)) val graph = DirectedGraph(Seq(edgeAB, edgeAD, edgeBC, edgeDC)) - assert(graph.edgesOf(a).toSet === Set(edgeAB, edgeAD)) - assert(graph.getIncomingEdgesOf(a) === Nil) - assert(graph.edgesOf(c) === Nil) - assert(graph.getIncomingEdgesOf(c).toSet === Set(edgeBC, edgeDC)) + assert(graph.edgesOf(a).toSet == Set(edgeAB, edgeAD)) + assert(graph.getIncomingEdgesOf(a) == Nil) + assert(graph.edgesOf(c) == Nil) + assert(graph.getIncomingEdgesOf(c).toSet == Set(edgeBC, edgeDC)) val edgeAB1 = edgeAB.copy(balance_opt = Some(200000 msat)) val edgeBC1 = edgeBC.copy(balance_opt = Some(150000 msat)) val graph1 = graph.addEdge(edgeAB1).addEdge(edgeBC1) - assert(graph1.edgesOf(a).toSet === Set(edgeAB1, edgeAD)) - assert(graph1.getIncomingEdgesOf(a) === Nil) - assert(graph1.edgesOf(c) === Nil) - assert(graph1.getIncomingEdgesOf(c).toSet === Set(edgeBC1, edgeDC)) + assert(graph1.edgesOf(a).toSet == Set(edgeAB1, edgeAD)) + assert(graph1.getIncomingEdgesOf(a) == Nil) + assert(graph1.edgesOf(c) == Nil) + assert(graph1.getIncomingEdgesOf(c).toSet == Set(edgeBC1, edgeDC)) } def descFromNodes(shortChannelId: Long, a: PublicKey, b: PublicKey): ChannelDesc = makeEdge(shortChannelId, a, b, 0 msat, 0).desc diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/RouteCalculationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/RouteCalculationSpec.scala index 43816d2074..a44a9afb14 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/RouteCalculationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/RouteCalculationSpec.scala @@ -57,7 +57,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { )) val Success(route :: Nil) = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) - assert(route2Ids(route) === 1 :: 2 :: 3 :: 4 :: Nil) + assert(route2Ids(route) == 1 :: 2 :: 3 :: 4 :: Nil) } test("check fee against max pct properly") { @@ -78,7 +78,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { )) val Success(route :: Nil) = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, maxFee, numRoutes = 1, routeParams = routeParams, currentBlockHeight = BlockHeight(400000)) - assert(route2Ids(route) === 1 :: 2 :: 3 :: 4 :: Nil) + assert(route2Ids(route) == 1 :: 2 :: 3 :: 4 :: Nil) } test("calculate the shortest path (correct fees)") { @@ -122,9 +122,9 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { val Success(route :: Nil) = findRoute(graph, a, d, amount, maxFee = 7 msat, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) val weightedPath = Graph.pathWeight(a, route2Edges(route), amount, BlockHeight(0), Left(NO_WEIGHT_RATIOS), includeLocalChannelCost = false) - assert(route2Ids(route) === 4 :: 5 :: 6 :: Nil) - assert(weightedPath.length === 3) - assert(weightedPath.amount === expectedCost) + assert(route2Ids(route) == 4 :: 5 :: 6 :: Nil) + assert(weightedPath.length == 3) + assert(weightedPath.amount == expectedCost) // update channel 5 so that it can route the final amount (10000) but not the amount + fees (10002) val graph1 = graph.addEdge(makeEdge(5L, e, f, feeBase = 1 msat, feeProportionalMillionth = 400, minHtlc = 0 msat, maxHtlc = Some(10001 msat))) @@ -132,7 +132,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { val graph3 = graph.addEdge(makeEdge(5L, e, f, feeBase = 1 msat, feeProportionalMillionth = 400, minHtlc = 0 msat, balance_opt = Some(10001 msat))) for (g <- Seq(graph1, graph2, graph3)) { val Success(route1 :: Nil) = findRoute(g, a, d, amount, maxFee = 10 msat, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) - assert(route2Ids(route1) === 1 :: 2 :: 3 :: Nil) + assert(route2Ids(route1) == 1 :: 2 :: 3 :: Nil) } } @@ -146,7 +146,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { )) val Success(route :: Nil) = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) - assert(route2Ids(route) === 2 :: 5 :: Nil) + assert(route2Ids(route) == 2 :: 5 :: Nil) } test("calculate simple route (add and remove edges") { @@ -158,11 +158,11 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { )) val Success(route1 :: Nil) = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) - assert(route2Ids(route1) === 1 :: 2 :: 3 :: 4 :: Nil) + assert(route2Ids(route1) == 1 :: 2 :: 3 :: 4 :: Nil) val graphWithRemovedEdge = g.removeEdge(ChannelDesc(ShortChannelId(3L), c, d)) val route2 = findRoute(graphWithRemovedEdge, a, e, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) - assert(route2 === Failure(RouteNotFound)) + assert(route2 == Failure(RouteNotFound)) } test("calculate the shortest path (hardcoded nodes)") { @@ -181,7 +181,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { )) val Success(route :: Nil) = findRoute(graph, f, i, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) - assert(route2Ids(route) === 4 :: 3 :: Nil) + assert(route2Ids(route) == 4 :: 3 :: Nil) } test("calculate the shortest path (select direct channel)") { @@ -200,8 +200,8 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { )) val Success(route1 :: route2 :: Nil) = findRoute(graph, f, i, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 2, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) - assert(route2Ids(route1) === 4 :: Nil) - assert(route2Ids(route2) === 1 :: 2 :: 3 :: Nil) + assert(route2Ids(route1) == 4 :: Nil) + assert(route2Ids(route2) == 1 :: 2 :: 3 :: Nil) } test("find a route using channels with htlMaximumMsat close to the payment amount") { @@ -220,7 +220,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { )) val Success(route :: Nil) = findRoute(graph, f, i, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) - assert(route2Ids(route) === 1 :: 2 :: 3 :: Nil) + assert(route2Ids(route) == 1 :: 2 :: 3 :: Nil) } test("find a route using channels with htlMinimumMsat close to the payment amount") { @@ -239,7 +239,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { )) val route = findRoute(graph, f, i, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) - assert(route === Failure(RouteNotFound)) + assert(route == Failure(RouteNotFound)) } test("if there are multiple channels between the same node, select the cheapest") { @@ -258,7 +258,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { )) val Success(route :: Nil) = findRoute(graph, f, i, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) - assert(route2Ids(route) === 1 :: 6 :: 3 :: Nil) + assert(route2Ids(route) == 1 :: 6 :: 3 :: Nil) } test("if there are multiple channels between the same node, select one that has enough balance") { @@ -277,7 +277,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { )) val Success(route :: Nil) = findRoute(graph, f, i, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) - assert(route2Ids(route) === 1 :: 2 :: 3 :: Nil) + assert(route2Ids(route) == 1 :: 2 :: 3 :: Nil) } test("calculate longer but cheaper route") { @@ -290,7 +290,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { )) val Success(route :: Nil) = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) - assert(route2Ids(route) === 1 :: 2 :: 3 :: 4 :: Nil) + assert(route2Ids(route) == 1 :: 2 :: 3 :: 4 :: Nil) } test("no local channels") { @@ -300,7 +300,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { )) val route = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) - assert(route === Failure(RouteNotFound)) + assert(route == Failure(RouteNotFound)) } test("route not found") { @@ -311,7 +311,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { )) val route = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) - assert(route === Failure(RouteNotFound)) + assert(route == Failure(RouteNotFound)) } test("route not found (source OR target node not connected)") { @@ -320,8 +320,8 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { makeEdge(4L, c, d, 0 msat, 0) )).addVertex(a).addVertex(e) - assert(findRoute(g, a, d, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) === Failure(RouteNotFound)) - assert(findRoute(g, b, e, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) === Failure(RouteNotFound)) + assert(findRoute(g, a, d, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) == Failure(RouteNotFound)) + assert(findRoute(g, b, e, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) == Failure(RouteNotFound)) } test("route not found (amount too high OR too low)") { @@ -343,8 +343,8 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { val g = DirectedGraph(edgesHi) val g1 = DirectedGraph(edgesLo) - assert(findRoute(g, a, d, highAmount, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) === Failure(RouteNotFound)) - assert(findRoute(g1, a, d, lowAmount, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) === Failure(RouteNotFound)) + assert(findRoute(g, a, d, highAmount, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) == Failure(RouteNotFound)) + assert(findRoute(g1, a, d, lowAmount, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) == Failure(RouteNotFound)) } test("route not found (balance too low)") { @@ -373,7 +373,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { makeEdge(2L, b, c, 1 msat, 2, minHtlc = 10000 msat), makeEdge(3L, c, d, 1 msat, 2, minHtlc = 10000 msat) )) - Seq(g1, g2, g3).foreach(g => assert(findRoute(g, a, d, 15000 msat, 100 msat, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) === Failure(RouteNotFound))) + Seq(g1, g2, g3).foreach(g => assert(findRoute(g, a, d, 15000 msat, 100 msat, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) == Failure(RouteNotFound))) } test("route to self") { @@ -384,7 +384,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { )) val route = findRoute(g, a, a, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) - assert(route === Failure(CannotRouteToSelf)) + assert(route == Failure(CannotRouteToSelf)) } test("route to immediate neighbor") { @@ -396,7 +396,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { )) val Success(route :: Nil) = findRoute(g, a, b, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) - assert(route2Ids(route) === 1 :: Nil) + assert(route2Ids(route) == 1 :: Nil) } test("directed graph") { @@ -409,10 +409,10 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { )) val Success(route1 :: Nil) = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) - assert(route2Ids(route1) === 1 :: 2 :: 3 :: 4 :: Nil) + assert(route2Ids(route1) == 1 :: 2 :: 3 :: 4 :: Nil) val route2 = findRoute(g, e, a, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) - assert(route2 === Failure(RouteNotFound)) + assert(route2 == Failure(RouteNotFound)) } test("calculate route and return metadata") { @@ -440,7 +440,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { val g = DirectedGraph(edges) val Success(route :: Nil) = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) - assert(route.hops === channelHopFromUpdate(a, b, uab) :: channelHopFromUpdate(b, c, ubc) :: channelHopFromUpdate(c, d, ucd) :: channelHopFromUpdate(d, e, ude) :: Nil) + assert(route.hops == channelHopFromUpdate(a, b, uab) :: channelHopFromUpdate(b, c, ubc) :: channelHopFromUpdate(c, d, ucd) :: channelHopFromUpdate(d, e, ude) :: Nil) } test("convert extra hops to assisted channels") { @@ -459,10 +459,10 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { val amount = 90000 sat // below RoutingHeuristics.CAPACITY_CHANNEL_LOW val assistedChannels = toAssistedChannels(extraHops, e, amount.toMilliSatoshi) - assert(assistedChannels(extraHop4.shortChannelId) === AssistedChannel(e, ChannelRelayParams.FromHint(extraHop4, 100050.sat.toMilliSatoshi))) - assert(assistedChannels(extraHop3.shortChannelId) === AssistedChannel(d, ChannelRelayParams.FromHint(extraHop3, 100200.sat.toMilliSatoshi))) - assert(assistedChannels(extraHop2.shortChannelId) === AssistedChannel(c, ChannelRelayParams.FromHint(extraHop2, 100400.sat.toMilliSatoshi))) - assert(assistedChannels(extraHop1.shortChannelId) === AssistedChannel(b, ChannelRelayParams.FromHint(extraHop1, 101416.sat.toMilliSatoshi))) + assert(assistedChannels(extraHop4.shortChannelId) == AssistedChannel(e, ChannelRelayParams.FromHint(extraHop4, 100050.sat.toMilliSatoshi))) + assert(assistedChannels(extraHop3.shortChannelId) == AssistedChannel(d, ChannelRelayParams.FromHint(extraHop3, 100200.sat.toMilliSatoshi))) + assert(assistedChannels(extraHop2.shortChannelId) == AssistedChannel(c, ChannelRelayParams.FromHint(extraHop2, 100400.sat.toMilliSatoshi))) + assert(assistedChannels(extraHop1.shortChannelId) == AssistedChannel(b, ChannelRelayParams.FromHint(extraHop1, 101416.sat.toMilliSatoshi))) } test("blacklist routes") { @@ -474,7 +474,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { )) val route1 = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, ignoredEdges = Set(ChannelDesc(ShortChannelId(3L), c, d)), routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) - assert(route1 === Failure(RouteNotFound)) + assert(route1 == Failure(RouteNotFound)) // verify that we left the graph untouched assert(g.containsEdge(ChannelDesc(ShortChannelId(3), c, d))) @@ -483,7 +483,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { // make sure we can find a route if without the blacklist val Success(route2 :: Nil) = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) - assert(route2Ids(route2) === 1 :: 2 :: 3 :: 4 :: Nil) + assert(route2Ids(route2) == 1 :: 2 :: 3 :: 4 :: Nil) } test("route to a destination that is not in the graph (with assisted routes)") { @@ -494,12 +494,12 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { )) val route = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) - assert(route === Failure(RouteNotFound)) + assert(route == Failure(RouteNotFound)) // now we add the missing edge to reach the destination val extraGraphEdges = Set(makeEdge(4L, d, e, 5 msat, 5)) val Success(route1 :: Nil) = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, extraEdges = extraGraphEdges, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) - assert(route2Ids(route1) === 1 :: 2 :: 3 :: 4 :: Nil) + assert(route2Ids(route1) == 1 :: 2 :: 3 :: 4 :: Nil) } test("route from a source that is not in the graph (with assisted routes)") { @@ -509,12 +509,12 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { )) val route = findRoute(g, a, d, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) - assert(route === Failure(RouteNotFound)) + assert(route == Failure(RouteNotFound)) // now we add the missing starting edge val extraGraphEdges = Set(makeEdge(1L, a, b, 5 msat, 5)) val Success(route1 :: Nil) = findRoute(g, a, d, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, extraEdges = extraGraphEdges, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) - assert(route2Ids(route1) === 1 :: 2 :: 3 :: Nil) + assert(route2Ids(route1) == 1 :: 2 :: 3 :: Nil) } test("verify that extra hops takes precedence over known channels") { @@ -526,13 +526,13 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { )) val Success(route1 :: Nil) = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) - assert(route2Ids(route1) === 1 :: 2 :: 3 :: 4 :: Nil) - assert(route1.hops(1).params.relayFees.feeBase === 10.msat) + assert(route2Ids(route1) == 1 :: 2 :: 3 :: 4 :: Nil) + assert(route1.hops(1).params.relayFees.feeBase == 10.msat) val extraGraphEdges = Set(makeEdge(2L, b, c, 5 msat, 5)) val Success(route2 :: Nil) = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, extraEdges = extraGraphEdges, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) - assert(route2Ids(route2) === 1 :: 2 :: 3 :: 4 :: Nil) - assert(route2.hops(1).params.relayFees.feeBase === 5.msat) + assert(route2Ids(route2) == 1 :: 2 :: 3 :: 4 :: Nil) + assert(route2.hops(1).params.relayFees.feeBase == 5.msat) } test("compute ignored channels") { @@ -588,10 +588,10 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { val g = DirectedGraph(edges) - assert(findRoute(g, nodes(0), nodes(18), DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)).map(r => route2Ids(r.head)) === Success(0 until 18)) - assert(findRoute(g, nodes(0), nodes(19), DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)).map(r => route2Ids(r.head)) === Success(0 until 19)) - assert(findRoute(g, nodes(0), nodes(20), DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)).map(r => route2Ids(r.head)) === Success(0 until 20)) - assert(findRoute(g, nodes(0), nodes(21), DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) === Failure(RouteNotFound)) + assert(findRoute(g, nodes(0), nodes(18), DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)).map(r => route2Ids(r.head)) == Success(0 until 18)) + assert(findRoute(g, nodes(0), nodes(19), DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)).map(r => route2Ids(r.head)) == Success(0 until 19)) + assert(findRoute(g, nodes(0), nodes(20), DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)).map(r => route2Ids(r.head)) == Success(0 until 20)) + assert(findRoute(g, nodes(0), nodes(21), DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) == Failure(RouteNotFound)) } test("ignore cheaper route when it has more than 20 hops") { @@ -607,7 +607,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { val g = DirectedGraph(expensiveShortEdge :: edges) val Success(route :: Nil) = findRoute(g, nodes(0), nodes(49), DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) - assert(route2Ids(route) === 0 :: 1 :: 99 :: 48 :: Nil) + assert(route2Ids(route) == 0 :: 1 :: 99 :: 48 :: Nil) } test("ignore cheaper route when it has more than the requested CLTV") { @@ -622,7 +622,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { )) val Success(route :: Nil) = findRoute(g, a, d, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.modify(_.boundaries.maxCltv).setTo(CltvExpiryDelta(28)), currentBlockHeight = BlockHeight(400000)) - assert(route2Ids(route) === 4 :: 5 :: 6 :: Nil) + assert(route2Ids(route) == 4 :: 5 :: 6 :: Nil) } test("ignore cheaper route when it grows longer than the requested size") { @@ -637,7 +637,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { )) val Success(route :: Nil) = findRoute(g, a, f, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.modify(_.boundaries.maxRouteLength).setTo(3), currentBlockHeight = BlockHeight(400000)) - assert(route2Ids(route) === 1 :: 6 :: Nil) + assert(route2Ids(route) == 1 :: 6 :: Nil) } test("ignore loops") { @@ -650,7 +650,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { )) val Success(route :: Nil) = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) - assert(route2Ids(route) === 1 :: 2 :: 4 :: 5 :: Nil) + assert(route2Ids(route) == 1 :: 2 :: 4 :: 5 :: Nil) } test("ensure the route calculation terminates correctly when selecting 0-fees edges") { @@ -666,7 +666,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { )) val Success(route :: Nil) = findRoute(g, a, d, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) - assert(route2Ids(route) === 1 :: 3 :: 5 :: Nil) + assert(route2Ids(route) == 1 :: 3 :: 5 :: Nil) } // +---+ +---+ +---+ @@ -699,19 +699,19 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { )) val fourShortestPaths = Graph.yenKshortestPaths(g1, d, f, DEFAULT_AMOUNT_MSAT, Set.empty, Set.empty, Set.empty, pathsToFind = 4, Left(NO_WEIGHT_RATIOS), BlockHeight(0), noopBoundaries, includeLocalChannelCost = false) - assert(fourShortestPaths.size === 4) - assert(hops2Ids(fourShortestPaths(0).path.map(graphEdgeToHop)) === 2 :: 5 :: Nil) // D -> E -> F - assert(hops2Ids(fourShortestPaths(1).path.map(graphEdgeToHop)) === 1 :: 3 :: 5 :: Nil) // D -> A -> E -> F - assert(hops2Ids(fourShortestPaths(2).path.map(graphEdgeToHop)) === 2 :: 4 :: 6 :: 7 :: Nil) // D -> E -> B -> C -> F - assert(hops2Ids(fourShortestPaths(3).path.map(graphEdgeToHop)) === 1 :: 3 :: 4 :: 6 :: 7 :: Nil) // D -> A -> E -> B -> C -> F + assert(fourShortestPaths.size == 4) + assert(hops2Ids(fourShortestPaths(0).path.map(graphEdgeToHop)) == 2 :: 5 :: Nil) // D -> E -> F + assert(hops2Ids(fourShortestPaths(1).path.map(graphEdgeToHop)) == 1 :: 3 :: 5 :: Nil) // D -> A -> E -> F + assert(hops2Ids(fourShortestPaths(2).path.map(graphEdgeToHop)) == 2 :: 4 :: 6 :: 7 :: Nil) // D -> E -> B -> C -> F + assert(hops2Ids(fourShortestPaths(3).path.map(graphEdgeToHop)) == 1 :: 3 :: 4 :: 6 :: 7 :: Nil) // D -> A -> E -> B -> C -> F // Update balance D -> A to evict the last path (balance too low) val g2 = g1.addEdge(makeEdge(1L, d, a, 1 msat, 0, balance_opt = Some(DEFAULT_AMOUNT_MSAT + 3.msat))) val threeShortestPaths = Graph.yenKshortestPaths(g2, d, f, DEFAULT_AMOUNT_MSAT, Set.empty, Set.empty, Set.empty, pathsToFind = 4, Left(NO_WEIGHT_RATIOS), BlockHeight(0), noopBoundaries, includeLocalChannelCost = false) - assert(threeShortestPaths.size === 3) - assert(hops2Ids(threeShortestPaths(0).path.map(graphEdgeToHop)) === 2 :: 5 :: Nil) // D -> E -> F - assert(hops2Ids(threeShortestPaths(1).path.map(graphEdgeToHop)) === 1 :: 3 :: 5 :: Nil) // D -> A -> E -> F - assert(hops2Ids(threeShortestPaths(2).path.map(graphEdgeToHop)) === 2 :: 4 :: 6 :: 7 :: Nil) // D -> E -> B -> C -> F + assert(threeShortestPaths.size == 3) + assert(hops2Ids(threeShortestPaths(0).path.map(graphEdgeToHop)) == 2 :: 5 :: Nil) // D -> E -> F + assert(hops2Ids(threeShortestPaths(1).path.map(graphEdgeToHop)) == 1 :: 3 :: 5 :: Nil) // D -> A -> E -> F + assert(hops2Ids(threeShortestPaths(2).path.map(graphEdgeToHop)) == 2 :: 4 :: 6 :: 7 :: Nil) // D -> E -> B -> C -> F } test("find the k shortest path (wikipedia example)") { @@ -738,12 +738,12 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { val twoShortestPaths = Graph.yenKshortestPaths(graph, c, h, DEFAULT_AMOUNT_MSAT, Set.empty, Set.empty, Set.empty, pathsToFind = 2, Left(NO_WEIGHT_RATIOS), BlockHeight(0), noopBoundaries, includeLocalChannelCost = false) - assert(twoShortestPaths.size === 2) + assert(twoShortestPaths.size == 2) val shortest = twoShortestPaths(0) - assert(hops2Ids(shortest.path.map(graphEdgeToHop)) === 10 :: 50 :: 80 :: Nil) // C -> E -> F -> H + assert(hops2Ids(shortest.path.map(graphEdgeToHop)) == 10 :: 50 :: 80 :: Nil) // C -> E -> F -> H val secondShortest = twoShortestPaths(1) - assert(hops2Ids(secondShortest.path.map(graphEdgeToHop)) === 10 :: 60 :: 90 :: Nil) // C -> E -> G -> H + assert(hops2Ids(secondShortest.path.map(graphEdgeToHop)) == 10 :: 60 :: 90 :: Nil) // C -> E -> G -> H } test("terminate looking for k-shortest path if there are no more alternative paths than k, must not consider routes going back on their steps") { @@ -768,9 +768,9 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { // we ask for 3 shortest paths but only 2 can be found val foundPaths = Graph.yenKshortestPaths(graph, a, f, DEFAULT_AMOUNT_MSAT, Set.empty, Set.empty, Set.empty, pathsToFind = 3, Left(NO_WEIGHT_RATIOS), BlockHeight(0), noopBoundaries, includeLocalChannelCost = false) - assert(foundPaths.size === 2) - assert(hops2Ids(foundPaths(0).path.map(graphEdgeToHop)) === 1 :: 2 :: 3 :: Nil) // A -> B -> C -> F - assert(hops2Ids(foundPaths(1).path.map(graphEdgeToHop)) === 1 :: 2 :: 4 :: 5 :: 6 :: Nil) // A -> B -> C -> D -> E -> F + assert(foundPaths.size == 2) + assert(hops2Ids(foundPaths(0).path.map(graphEdgeToHop)) == 1 :: 2 :: 3 :: Nil) // A -> B -> C -> F + assert(hops2Ids(foundPaths(1).path.map(graphEdgeToHop)) == 1 :: 2 :: 4 :: 5 :: 6 :: Nil) // A -> B -> C -> D -> E -> F } test("select a random route below the requested fee") { @@ -779,7 +779,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { .modify(_.boundaries.maxFeeProportional).setTo(0) .modify(_.randomize).setTo(true) val strictFee = strictFeeParams.getMaxFee(DEFAULT_AMOUNT_MSAT) - assert(strictFee === 7.msat) + assert(strictFee == 7.msat) // A -> B -> C -> D has total cost of 10000005 // A -> E -> C -> D has total cost of 10000103 !! @@ -796,12 +796,12 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { for (_ <- 0 to 10) { val Success(routes) = findRoute(g, a, d, DEFAULT_AMOUNT_MSAT, strictFee, numRoutes = 3, routeParams = strictFeeParams, currentBlockHeight = BlockHeight(400000)) - assert(routes.length === 2, routes) + assert(routes.length == 2, routes) val weightedPath = Graph.pathWeight(a, route2Edges(routes.head), DEFAULT_AMOUNT_MSAT, BlockHeight(400000), Left(NO_WEIGHT_RATIOS), includeLocalChannelCost = false) val totalFees = weightedPath.amount - DEFAULT_AMOUNT_MSAT // over the three routes we could only get the 2 cheapest because the third is too expensive (over 7 msat of fees) - assert(totalFees === 5.msat || totalFees === 6.msat) - assert(weightedPath.length === 3) + assert(totalFees == 5.msat || totalFees == 6.msat) + assert(weightedPath.length == 3) } } @@ -823,7 +823,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { )) val Success(routeFeeOptimized :: Nil) = findRoute(g, a, d, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) - assert(route2Nodes(routeFeeOptimized) === (a, b) :: (b, c) :: (c, d) :: Nil) + assert(route2Nodes(routeFeeOptimized) == (a, b) :: (b, c) :: (c, d) :: Nil) val Success(routeCltvOptimized :: Nil) = findRoute(g, a, d, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.copy(heuristics = Left(WeightRatios( baseFactor = 0, @@ -832,7 +832,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { capacityFactor = 0, hopCost = RelayFees(0 msat, 0), ))), currentBlockHeight = BlockHeight(400000)) - assert(route2Nodes(routeCltvOptimized) === (a, e) :: (e, f) :: (f, d) :: Nil) + assert(route2Nodes(routeCltvOptimized) == (a, e) :: (e, f) :: (f, d) :: Nil) val Success(routeCapacityOptimized :: Nil) = findRoute(g, a, d, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.copy(heuristics = Left(WeightRatios( baseFactor = 0, @@ -841,7 +841,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { capacityFactor = 1, hopCost = RelayFees(0 msat, 0), ))), currentBlockHeight = BlockHeight(400000)) - assert(route2Nodes(routeCapacityOptimized) === (a, e) :: (e, c) :: (c, d) :: Nil) + assert(route2Nodes(routeCapacityOptimized) == (a, e) :: (e, c) :: (c, d) :: Nil) } test("prefer going through an older channel if fees and CLTV are the same") { @@ -864,7 +864,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { hopCost = RelayFees(0 msat, 0), ))), currentBlockHeight = currentBlockHeight) - assert(route2Nodes(routeScoreOptimized) === (a, b) :: (b, c) :: (c, d) :: Nil) + assert(route2Nodes(routeScoreOptimized) == (a, b) :: (b, c) :: (c, d) :: Nil) } test("prefer a route with a smaller total CLTV if fees and score are the same") { @@ -885,7 +885,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { hopCost = RelayFees(0 msat, 0), ))), currentBlockHeight = BlockHeight(400000)) - assert(route2Nodes(routeScoreOptimized) === (a, b) :: (b, c) :: (c, d) :: Nil) + assert(route2Nodes(routeScoreOptimized) == (a, b) :: (b, c) :: (c, d) :: Nil) } test("avoid a route that breaks off the max CLTV") { @@ -908,7 +908,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { hopCost = RelayFees(0 msat, 0), ))), currentBlockHeight = BlockHeight(400000)) - assert(route2Nodes(routeScoreOptimized) === (a, e) :: (e, f) :: (f, d) :: Nil) + assert(route2Nodes(routeScoreOptimized) == (a, e) :: (e, f) :: (f, d) :: Nil) } test("cost function is monotonic") { @@ -987,7 +987,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { { val Success(routes) = findMultiPartRoute(g, a, b, amount, 1 msat, routeParams = routeParams, currentBlockHeight = BlockHeight(400000)) - assert(routes.length === 4, routes) + assert(routes.length == 4, routes) assert(routes.forall(_.length == 1), routes) checkRouteAmounts(routes, amount, 0 msat) } @@ -1000,7 +1000,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { { // We set min-part-amount to a value that excludes channels 1 and 4. val failure = findMultiPartRoute(g, a, b, amount, 1 msat, routeParams = routeParams.copy(mpp = MultiPartParams(16500 msat, 3)), currentBlockHeight = BlockHeight(400000)) - assert(failure === Failure(RouteNotFound)) + assert(failure == Failure(RouteNotFound)) } } @@ -1014,9 +1014,9 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { val amount = 25000 msat val Success(routes) = findMultiPartRoute(g, a, b, amount, 1 msat, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) - assert(routes.length === 1, routes) + assert(routes.length == 1, routes) checkRouteAmounts(routes, amount, 0 msat) - assert(route2Ids(routes.head) === 1L :: Nil) + assert(route2Ids(routes.head) == 1L :: Nil) } test("calculate multipart route to neighbor (many channels, some balance unknown)") { @@ -1030,7 +1030,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { val amount = 65000 msat val Success(routes) = findMultiPartRoute(g, a, b, amount, 1 msat, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) - assert(routes.length === 4, routes) + assert(routes.length == 4, routes) assert(routes.forall(_.length == 1), routes) checkRouteAmounts(routes, amount, 0 msat) } @@ -1049,7 +1049,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { { val Success(routes) = findMultiPartRoute(g, a, b, amount, 1 msat, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) - assert(routes.length === 3, routes) + assert(routes.length == 3, routes) assert(routes.forall(_.length == 1), routes) checkIgnoredChannels(routes, 2L) checkRouteAmounts(routes, amount, 0 msat) @@ -1144,7 +1144,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { val amount = 30000 msat val maxFeeTooLow = findMultiPartRoute(g, a, b, amount, 1 msat, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) - assert(maxFeeTooLow === Failure(RouteNotFound)) + assert(maxFeeTooLow == Failure(RouteNotFound)) val Success(routes) = findMultiPartRoute(g, a, b, amount, 20 msat, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(routes.forall(_.length <= 2), routes) @@ -1162,11 +1162,11 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { { val result = findMultiPartRoute(g, a, b, 40000 msat, 1 msat, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) - assert(result === Failure(RouteNotFound)) + assert(result == Failure(RouteNotFound)) } { val result = findMultiPartRoute(g, a, b, 40000 msat, 1 msat, routeParams = DEFAULT_ROUTE_PARAMS.copy(randomize = true), currentBlockHeight = BlockHeight(400000)) - assert(result === Failure(RouteNotFound)) + assert(result == Failure(RouteNotFound)) } } @@ -1179,7 +1179,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { )) val result = findMultiPartRoute(g, a, b, 5000000 msat, 1 msat, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) - assert(result === Failure(RouteNotFound)) + assert(result == Failure(RouteNotFound)) } test("cannot find multipart route to neighbor (restricted htlc_minimum_msat)") { @@ -1191,11 +1191,11 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { { val result = findMultiPartRoute(g, a, b, 10000 msat, 1 msat, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) - assert(result === Failure(RouteNotFound)) + assert(result == Failure(RouteNotFound)) } { val result = findMultiPartRoute(g, a, b, 10000 msat, 1 msat, routeParams = DEFAULT_ROUTE_PARAMS.copy(randomize = true), currentBlockHeight = BlockHeight(400000)) - assert(result === Failure(RouteNotFound)) + assert(result == Failure(RouteNotFound)) } } @@ -1219,14 +1219,14 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { { val Success(routes) = findMultiPartRoute(g, a, e, amount, maxFee, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) checkRouteAmounts(routes, amount, maxFee) - assert(routes2Ids(routes) === Set(Seq(1L, 2L, 3L), Seq(4L, 6L), Seq(5L, 6L))) + assert(routes2Ids(routes) == Set(Seq(1L, 2L, 3L), Seq(4L, 6L), Seq(5L, 6L))) } { // Update A - B with unknown balance, capacity should be used instead. val g1 = g.addEdge(edge_ab.copy(capacity = 15 sat, balance_opt = None)) val Success(routes) = findMultiPartRoute(g1, a, e, amount, maxFee, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) checkRouteAmounts(routes, amount, maxFee) - assert(routes2Ids(routes) === Set(Seq(1L, 2L, 3L), Seq(4L, 6L), Seq(5L, 6L))) + assert(routes2Ids(routes) == Set(Seq(1L, 2L, 3L), Seq(4L, 6L), Seq(5L, 6L))) } { // Randomize routes. @@ -1237,19 +1237,19 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { // Update balance A - B to be too low. val g1 = g.addEdge(edge_ab.copy(balance_opt = Some(2000 msat))) val failure = findMultiPartRoute(g1, a, e, amount, maxFee, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) - assert(failure === Failure(RouteNotFound)) + assert(failure == Failure(RouteNotFound)) } { // Update capacity A - B to be too low. val g1 = g.addEdge(edge_ab.copy(capacity = 5 sat, balance_opt = None)) val failure = findMultiPartRoute(g1, a, e, amount, maxFee, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) - assert(failure === Failure(RouteNotFound)) + assert(failure == Failure(RouteNotFound)) } { // Try to find a route with a maxFee that's too low. val maxFeeTooLow = 100 msat val failure = findMultiPartRoute(g, a, e, amount, maxFeeTooLow, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) - assert(failure === Failure(RouteNotFound)) + assert(failure == Failure(RouteNotFound)) } } @@ -1277,7 +1277,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { // But we don't want to split such tiny amounts. val (amount, maxFee) = (2000 msat, 150 msat) val failure = findMultiPartRoute(g, a, e, amount, maxFee, routeParams = routeParams, currentBlockHeight = BlockHeight(400000)) - assert(failure === Failure(RouteNotFound)) + assert(failure == Failure(RouteNotFound)) } } @@ -1291,8 +1291,8 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { val Success(routes) = findMultiPartRoute(g, a, d, amount, maxFee, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) checkRouteAmounts(routes, amount, maxFee) - assert(routes.length === 1, "payment shouldn't be split when we have one path with enough capacity") - assert(routes2Ids(routes) === Set(Seq(1L, 2L, 3L))) + assert(routes.length == 1, "payment shouldn't be split when we have one path with enough capacity") + assert(routes2Ids(routes) == Set(Seq(1L, 2L, 3L))) } test("calculate multipart route to remote node (single local channel)") { @@ -1316,7 +1316,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { { val Success(routes) = findMultiPartRoute(g, a, f, amount, maxFee, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) checkRouteAmounts(routes, amount, maxFee) - assert(routes2Ids(routes) === Set(Seq(1L, 2L, 3L, 5L), Seq(1L, 4L, 5L), Seq(1L, 6L, 7L))) + assert(routes2Ids(routes) == Set(Seq(1L, 2L, 3L, 5L), Seq(1L, 4L, 5L), Seq(1L, 6L, 7L))) } { // Randomize routes. @@ -1328,25 +1328,25 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { val g1 = g.addEdge(edge_ab.copy(capacity = 500 sat, balance_opt = None)) val Success(routes) = findMultiPartRoute(g1, a, f, amount, maxFee, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) checkRouteAmounts(routes, amount, maxFee) - assert(routes2Ids(routes) === Set(Seq(1L, 2L, 3L, 5L), Seq(1L, 4L, 5L), Seq(1L, 6L, 7L))) + assert(routes2Ids(routes) == Set(Seq(1L, 2L, 3L, 5L), Seq(1L, 4L, 5L), Seq(1L, 6L, 7L))) } { // Update balance A - B to be too low to cover fees. val g1 = g.addEdge(edge_ab.copy(balance_opt = Some(400000 msat))) val failure = findMultiPartRoute(g1, a, f, amount, maxFee, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) - assert(failure === Failure(RouteNotFound)) + assert(failure == Failure(RouteNotFound)) } { // Update capacity A - B to be too low to cover fees. val g1 = g.addEdge(edge_ab.copy(capacity = 400 sat, balance_opt = None)) val failure = findMultiPartRoute(g1, a, f, amount, maxFee, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) - assert(failure === Failure(RouteNotFound)) + assert(failure == Failure(RouteNotFound)) } { // Try to find a route with a maxFee that's too low. val maxFeeTooLow = 100 msat val failure = findMultiPartRoute(g, a, f, amount, maxFeeTooLow, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) - assert(failure === Failure(RouteNotFound)) + assert(failure == Failure(RouteNotFound)) } } @@ -1381,23 +1381,23 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { val routeParams = DEFAULT_ROUTE_PARAMS.copy(randomize = false, mpp = MultiPartParams(50_000 msat, 5)) val Success(routes) = findMultiPartRoute(g, a, d, amount, maxFee, routeParams = routeParams, currentBlockHeight = BlockHeight(400000)) checkRouteAmounts(routes, amount, maxFee) - assert(routes2Ids(routes) === Set(Seq(100L, 101L))) + assert(routes2Ids(routes) == Set(Seq(100L, 101L))) } { val amount = 15_000_000 msat val maxFee = 10_000 msat // this fee is too low to go through the preferred route val routeParams = DEFAULT_ROUTE_PARAMS.copy(randomize = false, mpp = MultiPartParams(50_000 msat, 5)) val failure = findMultiPartRoute(g, a, d, amount, maxFee, routeParams = routeParams, currentBlockHeight = BlockHeight(400000)) - assert(failure === Failure(RouteNotFound)) + assert(failure == Failure(RouteNotFound)) } { val amount = 5_000_000 msat val maxFee = 10_000 msat // this fee is enough to go through the preferred route, but the cheaper ones can handle it val routeParams = DEFAULT_ROUTE_PARAMS.copy(randomize = false, mpp = MultiPartParams(50_000 msat, 5)) val Success(routes) = findMultiPartRoute(g, a, d, amount, maxFee, routeParams = routeParams, currentBlockHeight = BlockHeight(400000)) - assert(routes.length === 5) + assert(routes.length == 5) routes.foreach(route => { - assert(route.length === 2) + assert(route.length == 2) assert(route.amount <= 1_200_000.msat) assert(!route.hops.flatMap(h => Seq(h.nodeId, h.nextNodeId)).contains(c)) }) @@ -1432,7 +1432,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { val ignoredChannels = Set(ChannelDesc(ShortChannelId(2L), b, c)) val Success(routes) = findMultiPartRoute(g, a, f, amount, maxFee, ignoredEdges = ignoredChannels, ignoredVertices = ignoredNodes, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) checkRouteAmounts(routes, amount, maxFee) - assert(routes2Ids(routes) === Set(Seq(8L), Seq(9L, 10L))) + assert(routes2Ids(routes) == Set(Seq(8L), Seq(9L, 10L))) } test("calculate multipart route to remote node (restricted htlc_minimum_msat and htlc_maximum_msat)") { @@ -1461,7 +1461,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { val maxFeeTooLow = 3 msat val failure = findMultiPartRoute(g, a, e, amount, maxFeeTooLow, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) - assert(failure === Failure(RouteNotFound)) + assert(failure == Failure(RouteNotFound)) } test("calculate multipart route to remote node (complex graph)") { @@ -1498,7 +1498,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { { val (amount, maxFee) = (25000 msat, 50 msat) val failure = findMultiPartRoute(g, d, f, amount, maxFee, routeParams = routeParams, currentBlockHeight = BlockHeight(400000)) - assert(failure === Failure(RouteNotFound)) + assert(failure == Failure(RouteNotFound)) } { val (amount, maxFee) = (40000 msat, 100 msat) @@ -1513,7 +1513,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { { val (amount, maxFee) = (40000 msat, 50 msat) val failure = findMultiPartRoute(g, d, f, amount, maxFee, routeParams = routeParams, currentBlockHeight = BlockHeight(400000)) - assert(failure === Failure(RouteNotFound)) + assert(failure == Failure(RouteNotFound)) } } @@ -1545,7 +1545,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { val maxFeeTooLow = 40 msat val failure = findMultiPartRoute(g, a, f, amount, maxFeeTooLow, extraEdges = extraEdges, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) - assert(failure === Failure(RouteNotFound)) + assert(failure == Failure(RouteNotFound)) } test("calculate multipart route to remote node (pending htlcs)") { @@ -1608,7 +1608,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { findMultiPartRoute(g, d, f, amount, maxFee, routeParams = DEFAULT_ROUTE_PARAMS.copy(randomize = true), currentBlockHeight = BlockHeight(400000)) match { case Success(routes) => checkRouteAmounts(routes, amount, maxFee) - case Failure(ex) => assert(ex === RouteNotFound) + case Failure(ex) => assert(ex == RouteNotFound) } } } @@ -1634,8 +1634,8 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { val Success(routes) = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 3, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(routes.length == 2) val route1 :: route2 :: Nil = routes - assert(route2Ids(route1) === 1 :: 5 :: Nil) - assert(route2Ids(route2) === 1 :: 2 :: 3 :: 4 :: Nil) + assert(route2Ids(route1) == 1 :: 5 :: Nil) + assert(route2Ids(route2) == 1 :: 2 :: 3 :: 4 :: Nil) } test("reversed loop trap") { @@ -1659,8 +1659,8 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { val Success(routes) = findRoute(g, e, a, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, numRoutes = 3, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(routes.length == 2) val route1 :: route2 :: Nil = routes - assert(route2Ids(route1) === 5 :: 1 :: Nil) - assert(route2Ids(route2) === 4 :: 3 :: 2 :: 1 :: Nil) + assert(route2Ids(route1) == 5 :: 1 :: Nil) + assert(route2Ids(route2) == 4 :: 3 :: 2 :: 1 :: Nil) } test("k-shortest paths must be distinct") { @@ -1735,7 +1735,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { makeEdge(1L, a, b, 1000 msat, 7000), )) - assert(findRoute(g, a, b, 10000000 msat, 10000 msat, numRoutes = 3, routeParams = DEFAULT_ROUTE_PARAMS.copy(includeLocalChannelCost = true), currentBlockHeight = BlockHeight(400000)) === Failure(RouteNotFound)) + assert(findRoute(g, a, b, 10000000 msat, 10000 msat, numRoutes = 3, routeParams = DEFAULT_ROUTE_PARAMS.copy(includeLocalChannelCost = true), currentBlockHeight = BlockHeight(400000)) == Failure(RouteNotFound)) assert(findRoute(g, a, b, 10000000 msat, 100000 msat, numRoutes = 3, routeParams = DEFAULT_ROUTE_PARAMS.copy(includeLocalChannelCost = true), currentBlockHeight = BlockHeight(400000)).isSuccess) } @@ -1763,19 +1763,19 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { val Success(routes) = findRoute(g, start, b, DEFAULT_AMOUNT_MSAT, 100000000 msat, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = BlockHeight(400000)) assert(routes.distinct.length == 1) val route :: Nil = routes - assert(route2Ids(route) === 0 :: 2 :: 5 :: 6 :: 7 :: 4 :: Nil) + assert(route2Ids(route) == 0 :: 2 :: 5 :: 6 :: 7 :: 4 :: Nil) } { // small base hop cost val Success(routes) = findRoute(g, start, b, DEFAULT_AMOUNT_MSAT, 100000000 msat, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.copy(heuristics = Left(WeightRatios(1, 0, 0, 0, RelayFees(100 msat, 0)))), currentBlockHeight = BlockHeight(400000)) assert(routes.distinct.length == 1) val route :: Nil = routes - assert(route2Ids(route) === 0 :: 2 :: 3 :: 4 :: Nil) + assert(route2Ids(route) == 0 :: 2 :: 3 :: 4 :: Nil) } { // large proportional hop cost val Success(routes) = findRoute(g, start, b, DEFAULT_AMOUNT_MSAT, 100000000 msat, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.copy(heuristics = Left(WeightRatios(1, 0, 0, 0, RelayFees(0 msat, 200)))), currentBlockHeight = BlockHeight(400000)) assert(routes.distinct.length == 1) val route :: Nil = routes - assert(route2Ids(route) === 0 :: 1 :: Nil) + assert(route2Ids(route) == 0 :: 1 :: Nil) } } @@ -1803,7 +1803,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { val Success(routes) = findRoute(g, start, b, DEFAULT_AMOUNT_MSAT, 100000000 msat, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.copy(heuristics = Right(hc)), currentBlockHeight = BlockHeight(400000)) assert(routes.distinct.length == 1) val route :: Nil = routes - assert(route2Ids(route) === 0 :: 2 :: 3 :: 4 :: Nil) + assert(route2Ids(route) == 0 :: 2 :: 3 :: 4 :: Nil) } @@ -1817,7 +1817,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { val Success(routes) = findRoute(g, start, b, DEFAULT_AMOUNT_MSAT, 100000000 msat, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.copy(heuristics = Right(hc)), currentBlockHeight = BlockHeight(400000)) assert(routes.distinct.length == 1) val route :: Nil = routes - assert(route2Ids(route) === 0 :: 2 :: 3 :: 4 :: Nil) + assert(route2Ids(route) == 0 :: 2 :: 3 :: 4 :: Nil) } } @@ -1844,7 +1844,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { val Success(routes) = findRoute(g, start, b, DEFAULT_AMOUNT_MSAT, 100000000 msat, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.copy(heuristics = Right(hc)), currentBlockHeight = BlockHeight(400000)) assert(routes.distinct.length == 1) val route :: Nil = routes - assert(route2Ids(route) === 0 :: 2 :: 3 :: 4 :: Nil) + assert(route2Ids(route) == 0 :: 2 :: 3 :: 4 :: Nil) } test("edge too small to relay payment is ignored") { @@ -1864,7 +1864,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { val Success(routes) = findRoute(g, a, c, DEFAULT_AMOUNT_MSAT, 100000000 msat, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.copy(heuristics = Right(hc)), currentBlockHeight = BlockHeight(400000)) assert(routes.distinct.length == 1) val route :: Nil = routes - assert(route2Ids(route) === 1 :: 2 :: Nil) + assert(route2Ids(route) == 1 :: 2 :: Nil) } test("use direct channel when available") { @@ -1887,7 +1887,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { val Success(routes) = findRoute(g, a, c, DEFAULT_AMOUNT_MSAT, 100_000_000 msat, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.copy(heuristics = Left(wr)), currentBlockHeight = BlockHeight(400000)) assert(routes.distinct.length == 1) val route :: Nil = routes - assert(route2Ids(route) === recentChannelId :: Nil) + assert(route2Ids(route) == recentChannelId :: Nil) } test("trampoline relay with direct channel to target") { @@ -1896,7 +1896,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { { val routeParams = DEFAULT_ROUTE_PARAMS.copy(includeLocalChannelCost = true, boundaries = SearchBoundaries(100_999 msat, 0.0, 6, CltvExpiryDelta(576))) - assert(findMultiPartRoute(g, a, b, amount, 100_999 msat, Set.empty, Set.empty, Set.empty, Nil, routeParams = routeParams, currentBlockHeight = BlockHeight(400000)) === Failure(RouteNotFound)) + assert(findMultiPartRoute(g, a, b, amount, 100_999 msat, Set.empty, Set.empty, Set.empty, Nil, routeParams = routeParams, currentBlockHeight = BlockHeight(400000)) == Failure(RouteNotFound)) } { val routeParams = DEFAULT_ROUTE_PARAMS.copy(includeLocalChannelCost = true, boundaries = SearchBoundaries(101_000 msat, 0.0, 6, CltvExpiryDelta(576))) @@ -1905,7 +1905,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { } test("small local edge with liquidity is better than big remote edge") { - // A === B === C -- D + // A == B == C -- D // \_________/ val g = DirectedGraph(List( makeEdge(1L, a, b, 100 msat, 100, minHtlc = 1000 msat, capacity = 100000000 sat, balance_opt = Some(10000000 msat)), @@ -1923,7 +1923,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { ) val Success(routes) = findRoute(g, a, d, 50000 msat, 100000000 msat, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.copy(heuristics = Left(wr), includeLocalChannelCost = true), currentBlockHeight = BlockHeight(400000)) val route :: Nil = routes - assert(route2Ids(route) === 3 :: 4 :: Nil) + assert(route2Ids(route) == 3 :: 4 :: Nil) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/RouterSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/RouterSpec.scala index f433335c63..d5dfcd1850 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/RouterSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/RouterSpec.scala @@ -59,12 +59,12 @@ class RouterSpec extends BaseRouterSpec { val node_c = makeNodeAnnouncement(priv_c, "node-C", Color(123, 100, -40), Nil, TestConstants.Bob.nodeParams.features.nodeAnnouncementFeatures(), timestamp = TimestampSecond.now() + 1) peerConnection.send(router, PeerRoutingMessage(peerConnection.ref, remoteNodeId, chan_ac)) peerConnection.expectNoMessage(100 millis) // we don't immediately acknowledge the announcement (back pressure) - assert(watcher.expectMsgType[ValidateRequest].ann === chan_ac) + assert(watcher.expectMsgType[ValidateRequest].ann == chan_ac) watcher.send(router, ValidateResult(chan_ac, Right(Transaction(version = 0, txIn = Nil, txOut = TxOut(1000000 sat, write(pay2wsh(Scripts.multiSig2of2(funding_a, funding_c)))) :: Nil, lockTime = 0), UtxoStatus.Unspent))) peerConnection.expectMsg(TransportHandler.ReadAck(chan_ac)) peerConnection.expectMsg(GossipDecision.Accepted(chan_ac)) assert(peerConnection.sender() == router) - assert(watcher.expectMsgType[WatchExternalChannelSpent].shortChannelId === chan_ac.shortChannelId) + assert(watcher.expectMsgType[WatchExternalChannelSpent].shortChannelId == chan_ac.shortChannelId) peerConnection.send(router, PeerRoutingMessage(peerConnection.ref, remoteNodeId, update_ac)) peerConnection.expectMsg(TransportHandler.ReadAck(update_ac)) peerConnection.expectMsg(GossipDecision.Accepted(update_ac)) @@ -89,7 +89,7 @@ class RouterSpec extends BaseRouterSpec { val node_u = makeNodeAnnouncement(priv_u, "node-U", Color(-120, -20, 60), Nil, Features.empty) peerConnection.send(router, PeerRoutingMessage(peerConnection.ref, remoteNodeId, chan_uc)) peerConnection.expectNoMessage(200 millis) // we don't immediately acknowledge the announcement (back pressure) - assert(watcher.expectMsgType[ValidateRequest].ann === chan_uc) + assert(watcher.expectMsgType[ValidateRequest].ann == chan_uc) peerConnection.send(router, PeerRoutingMessage(peerConnection.ref, remoteNodeId, update_uc)) peerConnection.expectMsg(TransportHandler.ReadAck(update_uc)) peerConnection.send(router, PeerRoutingMessage(peerConnection.ref, remoteNodeId, node_u)) @@ -98,7 +98,7 @@ class RouterSpec extends BaseRouterSpec { peerConnection.expectMsg(TransportHandler.ReadAck(chan_uc)) peerConnection.expectMsg(GossipDecision.Accepted(chan_uc)) assert(peerConnection.sender() == router) - assert(watcher.expectMsgType[WatchExternalChannelSpent].shortChannelId === chan_uc.shortChannelId) + assert(watcher.expectMsgType[WatchExternalChannelSpent].shortChannelId == chan_uc.shortChannelId) peerConnection.expectMsg(GossipDecision.Accepted(update_uc)) peerConnection.expectMsg(GossipDecision.Accepted(node_u)) eventListener.expectMsg(ChannelsDiscovered(SingleChannelDiscovered(chan_uc, 2000000 sat, None, None) :: Nil)) @@ -194,7 +194,7 @@ class RouterSpec extends BaseRouterSpec { val update_ay = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_a, priv_y.publicKey, chan_ay.shortChannelId, CltvExpiryDelta(7), 0 msat, 766000 msat, 10, htlcMaximum) val node_y = makeNodeAnnouncement(priv_y, "node-Y", Color(123, 100, -40), Nil, TestConstants.Bob.nodeParams.features.nodeAnnouncementFeatures()) peerConnection.send(router, PeerRoutingMessage(peerConnection.ref, remoteNodeId, chan_ay)) - assert(watcher.expectMsgType[ValidateRequest].ann === chan_ay) + assert(watcher.expectMsgType[ValidateRequest].ann == chan_ay) peerConnection.send(router, PeerRoutingMessage(peerConnection.ref, remoteNodeId, update_ay)) peerConnection.expectMsg(TransportHandler.ReadAck(update_ay)) peerConnection.send(router, PeerRoutingMessage(peerConnection.ref, remoteNodeId, node_y)) @@ -214,7 +214,7 @@ class RouterSpec extends BaseRouterSpec { val priv_x = randomKey() val chan_ax = channelAnnouncement(ShortChannelId(42001), priv_a, priv_x, priv_funding_a, randomKey()) peerConnection.send(router, PeerRoutingMessage(peerConnection.ref, remoteNodeId, chan_ax)) - assert(watcher.expectMsgType[ValidateRequest].ann === chan_ax) + assert(watcher.expectMsgType[ValidateRequest].ann == chan_ax) watcher.send(router, ValidateResult(chan_ax, Left(new RuntimeException("funding tx not found")))) peerConnection.expectMsg(TransportHandler.ReadAck(chan_ax)) peerConnection.expectMsg(GossipDecision.ValidationFailure(chan_ax)) @@ -229,7 +229,7 @@ class RouterSpec extends BaseRouterSpec { val priv_funding_z = randomKey() val chan_az = channelAnnouncement(ShortChannelId(42003), priv_a, priv_z, priv_funding_a, priv_funding_z) peerConnection.send(router, PeerRoutingMessage(peerConnection.ref, remoteNodeId, chan_az)) - assert(watcher.expectMsgType[ValidateRequest].ann === chan_az) + assert(watcher.expectMsgType[ValidateRequest].ann == chan_az) watcher.send(router, ValidateResult(chan_az, Right(Transaction(version = 0, txIn = Nil, txOut = TxOut(1000000 sat, write(pay2wsh(Scripts.multiSig2of2(funding_a, priv_funding_z.publicKey)))) :: Nil, lockTime = 0), UtxoStatus.Spent(spendingTxConfirmed = false)))) peerConnection.expectMsg(TransportHandler.ReadAck(chan_az)) peerConnection.expectMsg(GossipDecision.ChannelClosing(chan_az)) @@ -244,7 +244,7 @@ class RouterSpec extends BaseRouterSpec { val priv_funding_z = randomKey() val chan_az = channelAnnouncement(ShortChannelId(42003), priv_a, priv_z, priv_funding_a, priv_funding_z) peerConnection.send(router, PeerRoutingMessage(peerConnection.ref, remoteNodeId, chan_az)) - assert(watcher.expectMsgType[ValidateRequest].ann === chan_az) + assert(watcher.expectMsgType[ValidateRequest].ann == chan_az) watcher.send(router, ValidateResult(chan_az, Right(Transaction(version = 0, txIn = Nil, txOut = TxOut(1000000 sat, write(pay2wsh(Scripts.multiSig2of2(funding_a, priv_funding_z.publicKey)))) :: Nil, lockTime = 0), UtxoStatus.Spent(spendingTxConfirmed = true)))) peerConnection.expectMsg(TransportHandler.ReadAck(chan_az)) peerConnection.expectMsg(GossipDecision.ChannelClosed(chan_az)) @@ -340,13 +340,13 @@ class RouterSpec extends BaseRouterSpec { val sender = TestProbe() sender.send(router, RouteRequest(a, d, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, routeParams = DEFAULT_ROUTE_PARAMS)) val res = sender.expectMsgType[RouteResponse] - assert(res.routes.head.hops.map(_.nodeId).toList === a :: b :: c :: Nil) - assert(res.routes.head.hops.last.nextNodeId === d) + assert(res.routes.head.hops.map(_.nodeId).toList == a :: b :: c :: Nil) + assert(res.routes.head.hops.last.nextNodeId == d) sender.send(router, RouteRequest(a, h, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, routeParams = DEFAULT_ROUTE_PARAMS)) val res1 = sender.expectMsgType[RouteResponse] - assert(res1.routes.head.hops.map(_.nodeId).toList === a :: g :: Nil) - assert(res1.routes.head.hops.last.nextNodeId === h) + assert(res1.routes.head.hops.map(_.nodeId).toList == a :: g :: Nil) + assert(res1.routes.head.hops.last.nextNodeId == h) } test("route found (with extra routing info)") { fixture => @@ -360,8 +360,8 @@ class RouterSpec extends BaseRouterSpec { val extraHop_yz = ExtraHop(y, ShortChannelId(3), 20 msat, 21, CltvExpiryDelta(22)) sender.send(router, RouteRequest(a, z, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, assistedRoutes = Seq(extraHop_cx :: extraHop_xy :: extraHop_yz :: Nil), routeParams = DEFAULT_ROUTE_PARAMS)) val res = sender.expectMsgType[RouteResponse] - assert(res.routes.head.hops.map(_.nodeId).toList === a :: b :: c :: x :: y :: Nil) - assert(res.routes.head.hops.last.nextNodeId === z) + assert(res.routes.head.hops.map(_.nodeId).toList == a :: b :: c :: x :: y :: Nil) + assert(res.routes.head.hops.last.nextNodeId == z) } test("route not found (channel disabled)") { fixture => @@ -370,8 +370,8 @@ class RouterSpec extends BaseRouterSpec { val peerConnection = TestProbe() sender.send(router, RouteRequest(a, d, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, routeParams = DEFAULT_ROUTE_PARAMS)) val res = sender.expectMsgType[RouteResponse] - assert(res.routes.head.hops.map(_.nodeId).toList === a :: b :: c :: Nil) - assert(res.routes.head.hops.last.nextNodeId === d) + assert(res.routes.head.hops.map(_.nodeId).toList == a :: b :: c :: Nil) + assert(res.routes.head.hops.last.nextNodeId == d) val channelUpdate_cd1 = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_c, d, scid_cd, CltvExpiryDelta(3), 0 msat, 153000 msat, 4, htlcMaximum, enable = false) peerConnection.send(router, PeerRoutingMessage(peerConnection.ref, remoteNodeId, channelUpdate_cd1)) @@ -385,8 +385,8 @@ class RouterSpec extends BaseRouterSpec { val sender = TestProbe() sender.send(router, RouteRequest(a, h, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, routeParams = DEFAULT_ROUTE_PARAMS)) val res = sender.expectMsgType[RouteResponse] - assert(res.routes.head.hops.map(_.nodeId).toList === a :: g :: Nil) - assert(res.routes.head.hops.last.nextNodeId === h) + assert(res.routes.head.hops.map(_.nodeId).toList == a :: g :: Nil) + assert(res.routes.head.hops.last.nextNodeId == h) val channelUpdate_ag1 = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_a, g, scid_ag_private, CltvExpiryDelta(7), 0 msat, 10 msat, 10, htlcMaximum, enable = false) sender.send(router, LocalChannelUpdate(sender.ref, null, scid_ag_private, g, None, channelUpdate_ag1, CommitmentsSpec.makeCommitments(10000 msat, 15000 msat, a, g, announceChannel = false))) @@ -448,7 +448,7 @@ class RouterSpec extends BaseRouterSpec { assert(state.channels.size == 5) assert(state.nodes.size == 8) assert(state.channels.flatMap(c => c.update_1_opt.toSeq ++ c.update_2_opt.toSeq).size == 10) - state.channels.foreach(c => assert(c.capacity === publicChannelCapacity)) + state.channels.foreach(c => assert(c.capacity == publicChannelCapacity)) } test("given a pre-defined nodes route add the proper channel updates") { fixture => @@ -460,9 +460,9 @@ class RouterSpec extends BaseRouterSpec { val response = sender.expectMsgType[RouteResponse] // the route hasn't changed (nodes are the same) - assert(response.routes.head.hops.map(_.nodeId) === preComputedRoute.nodes.dropRight(1)) - assert(response.routes.head.hops.map(_.nextNodeId) === preComputedRoute.nodes.drop(1)) - assert(response.routes.head.hops.map(_.params) === Seq(ChannelRelayParams.FromAnnouncement(update_ab), ChannelRelayParams.FromAnnouncement(update_bc), ChannelRelayParams.FromAnnouncement(update_cd))) + assert(response.routes.head.hops.map(_.nodeId) == preComputedRoute.nodes.dropRight(1)) + assert(response.routes.head.hops.map(_.nextNodeId) == preComputedRoute.nodes.drop(1)) + assert(response.routes.head.hops.map(_.params) == Seq(ChannelRelayParams.FromAnnouncement(update_ab), ChannelRelayParams.FromAnnouncement(update_bc), ChannelRelayParams.FromAnnouncement(update_cd))) } test("given a pre-defined channels route add the proper channel updates") { fixture => @@ -474,9 +474,9 @@ class RouterSpec extends BaseRouterSpec { val response = sender.expectMsgType[RouteResponse] // the route hasn't changed (nodes are the same) - assert(response.routes.head.hops.map(_.nodeId) === Seq(a, b, c)) - assert(response.routes.head.hops.map(_.nextNodeId) === Seq(b, c, d)) - assert(response.routes.head.hops.map(_.params) === Seq(ChannelRelayParams.FromAnnouncement(update_ab), ChannelRelayParams.FromAnnouncement(update_bc), ChannelRelayParams.FromAnnouncement(update_cd))) + assert(response.routes.head.hops.map(_.nodeId) == Seq(a, b, c)) + assert(response.routes.head.hops.map(_.nextNodeId) == Seq(b, c, d)) + assert(response.routes.head.hops.map(_.params) == Seq(ChannelRelayParams.FromAnnouncement(update_ab), ChannelRelayParams.FromAnnouncement(update_bc), ChannelRelayParams.FromAnnouncement(update_cd))) } test("given a pre-defined private channels route add the proper channel updates") { fixture => @@ -487,21 +487,21 @@ class RouterSpec extends BaseRouterSpec { val preComputedRoute = PredefinedChannelRoute(g, Seq(scid_ag_private)) sender.send(router, FinalizeRoute(10000 msat, preComputedRoute)) val response = sender.expectMsgType[RouteResponse] - assert(response.routes.length === 1) + assert(response.routes.length == 1) val route = response.routes.head - assert(route.hops.map(_.params) === Seq(ChannelRelayParams.FromAnnouncement(update_ag_private))) - assert(route.hops.head.nodeId === a) - assert(route.hops.head.nextNodeId === g) + assert(route.hops.map(_.params) == Seq(ChannelRelayParams.FromAnnouncement(update_ag_private))) + assert(route.hops.head.nodeId == a) + assert(route.hops.head.nextNodeId == g) } { val preComputedRoute = PredefinedChannelRoute(h, Seq(scid_ag_private, scid_gh)) sender.send(router, FinalizeRoute(10000 msat, preComputedRoute)) val response = sender.expectMsgType[RouteResponse] - assert(response.routes.length === 1) + assert(response.routes.length == 1) val route = response.routes.head - assert(route.hops.map(_.nodeId) === Seq(a, g)) - assert(route.hops.map(_.nextNodeId) === Seq(g, h)) - assert(route.hops.map(_.params) === Seq(ChannelRelayParams.FromAnnouncement(update_ag_private), ChannelRelayParams.FromAnnouncement(update_gh))) + assert(route.hops.map(_.nodeId) == Seq(a, g)) + assert(route.hops.map(_.nextNodeId) == Seq(g, h)) + assert(route.hops.map(_.params) == Seq(ChannelRelayParams.FromAnnouncement(update_ag_private), ChannelRelayParams.FromAnnouncement(update_gh))) } } @@ -518,12 +518,12 @@ class RouterSpec extends BaseRouterSpec { assert(amount < RoutingHeuristics.CAPACITY_CHANNEL_LOW) sender.send(router, FinalizeRoute(amount, preComputedRoute, assistedRoutes = Seq(Seq(invoiceRoutingHint)))) val response = sender.expectMsgType[RouteResponse] - assert(response.routes.length === 1) + assert(response.routes.length == 1) val route = response.routes.head - assert(route.hops.map(_.nodeId) === Seq(a, b)) - assert(route.hops.map(_.nextNodeId) === Seq(b, targetNodeId)) - assert(route.hops.head.params === ChannelRelayParams.FromAnnouncement(update_ab)) - assert(route.hops.last.params === ChannelRelayParams.FromHint(invoiceRoutingHint, RoutingHeuristics.CAPACITY_CHANNEL_LOW + nodeFee(invoiceRoutingHint.feeBase, invoiceRoutingHint.feeProportionalMillionths, RoutingHeuristics.CAPACITY_CHANNEL_LOW))) + assert(route.hops.map(_.nodeId) == Seq(a, b)) + assert(route.hops.map(_.nextNodeId) == Seq(b, targetNodeId)) + assert(route.hops.head.params == ChannelRelayParams.FromAnnouncement(update_ab)) + assert(route.hops.last.params == ChannelRelayParams.FromHint(invoiceRoutingHint, RoutingHeuristics.CAPACITY_CHANNEL_LOW + nodeFee(invoiceRoutingHint.feeBase, invoiceRoutingHint.feeProportionalMillionths, RoutingHeuristics.CAPACITY_CHANNEL_LOW))) } { val invoiceRoutingHint = ExtraHop(h, ShortChannelId(BlockHeight(420000), 516, 1105), 10 msat, 150, CltvExpiryDelta(96)) @@ -533,12 +533,12 @@ class RouterSpec extends BaseRouterSpec { assert(amount > RoutingHeuristics.CAPACITY_CHANNEL_LOW) sender.send(router, FinalizeRoute(amount, preComputedRoute, assistedRoutes = Seq(Seq(invoiceRoutingHint)))) val response = sender.expectMsgType[RouteResponse] - assert(response.routes.length === 1) + assert(response.routes.length == 1) val route = response.routes.head - assert(route.hops.map(_.nodeId) === Seq(a, g, h)) - assert(route.hops.map(_.nextNodeId) === Seq(g, h, targetNodeId)) - assert(route.hops.map(_.params).dropRight(1) === Seq(ChannelRelayParams.FromAnnouncement(update_ag_private), ChannelRelayParams.FromAnnouncement(update_gh))) - assert(route.hops.last.params === ChannelRelayParams.FromHint(invoiceRoutingHint, amount + nodeFee(invoiceRoutingHint.feeBase, invoiceRoutingHint.feeProportionalMillionths, amount))) + assert(route.hops.map(_.nodeId) == Seq(a, g, h)) + assert(route.hops.map(_.nextNodeId) == Seq(g, h, targetNodeId)) + assert(route.hops.map(_.params).dropRight(1) == Seq(ChannelRelayParams.FromAnnouncement(update_ag_private), ChannelRelayParams.FromAnnouncement(update_gh))) + assert(route.hops.last.params == ChannelRelayParams.FromHint(invoiceRoutingHint, amount + nodeFee(invoiceRoutingHint.feeBase, invoiceRoutingHint.feeProportionalMillionths, amount))) } } @@ -605,7 +605,7 @@ class RouterSpec extends BaseRouterSpec { val sender = TestProbe() sender.send(router, GetRoutingState) val channel_ab = sender.expectMsgType[RoutingState].channels.find(_.ann == chan_ab).get - assert(channel_ab.meta_opt === None) + assert(channel_ab.meta_opt == None) { // When the local channel comes back online, it will send a LocalChannelUpdate to the router. @@ -614,7 +614,7 @@ class RouterSpec extends BaseRouterSpec { sender.send(router, LocalChannelUpdate(sender.ref, null, scid_ab, b, Some(chan_ab), update_ab, commitments)) sender.send(router, GetRoutingState) val channel_ab = sender.expectMsgType[RoutingState].channels.find(_.ann == chan_ab).get - assert(Set(channel_ab.meta_opt.map(_.balance1), channel_ab.meta_opt.map(_.balance2)) === balances) + assert(Set(channel_ab.meta_opt.map(_.balance1), channel_ab.meta_opt.map(_.balance2)) == balances) // And the graph should be updated too. sender.send(router, Router.GetRouterData) val g = sender.expectMsgType[Data].graph @@ -622,7 +622,7 @@ class RouterSpec extends BaseRouterSpec { val edge_ba = g.getEdge(ChannelDesc(scid_ab, b, a)).get assert(edge_ab.capacity == channel_ab.capacity && edge_ba.capacity == channel_ab.capacity) assert(balances.contains(edge_ab.balance_opt)) - assert(edge_ba.balance_opt === None) + assert(edge_ba.balance_opt == None) } { @@ -637,7 +637,7 @@ class RouterSpec extends BaseRouterSpec { sender.send(router, LocalChannelUpdate(sender.ref, null, scid_ab, b, Some(chan_ab), update_ab, commitments)) sender.send(router, GetRoutingState) val channel_ab = sender.expectMsgType[RoutingState].channels.find(_.ann == chan_ab).get - assert(Set(channel_ab.meta_opt.map(_.balance1), channel_ab.meta_opt.map(_.balance2)) === balances) + assert(Set(channel_ab.meta_opt.map(_.balance1), channel_ab.meta_opt.map(_.balance2)) == balances) // And the graph should be updated too. sender.send(router, Router.GetRouterData) val g = sender.expectMsgType[Data].graph @@ -645,7 +645,7 @@ class RouterSpec extends BaseRouterSpec { val edge_ba = g.getEdge(ChannelDesc(scid_ab, b, a)).get assert(edge_ab.capacity == channel_ab.capacity && edge_ba.capacity == channel_ab.capacity) assert(balances.contains(edge_ab.balance_opt)) - assert(edge_ba.balance_opt === None) + assert(edge_ba.balance_opt == None) } { @@ -655,7 +655,7 @@ class RouterSpec extends BaseRouterSpec { sender.send(router, AvailableBalanceChanged(sender.ref, null, scid_ab, commitments)) sender.send(router, GetRoutingState) val channel_ab = sender.expectMsgType[RoutingState].channels.find(_.ann == chan_ab).get - assert(Set(channel_ab.meta_opt.map(_.balance1), channel_ab.meta_opt.map(_.balance2)) === balances) + assert(Set(channel_ab.meta_opt.map(_.balance1), channel_ab.meta_opt.map(_.balance2)) == balances) // And the graph should be updated too. sender.send(router, Router.GetRouterData) val g = sender.expectMsgType[Data].graph @@ -663,7 +663,7 @@ class RouterSpec extends BaseRouterSpec { val edge_ba = g.getEdge(ChannelDesc(scid_ab, b, a)).get assert(edge_ab.capacity == channel_ab.capacity && edge_ba.capacity == channel_ab.capacity) assert(balances.contains(edge_ab.balance_opt)) - assert(edge_ba.balance_opt === None) + assert(edge_ba.balance_opt == None) } { @@ -674,11 +674,11 @@ class RouterSpec extends BaseRouterSpec { sender.send(router, Router.GetRouterData) val data = sender.expectMsgType[Data] val channel_ag = data.privateChannels(channelId_ag_private) - assert(Set(channel_ag.meta.balance1, channel_ag.meta.balance2) === balances) + assert(Set(channel_ag.meta.balance1, channel_ag.meta.balance2) == balances) // And the graph should be updated too. val edge_ag = data.graph.getEdge(ChannelDesc(scid_ag_private, a, g)).get assert(edge_ag.capacity == channel_ag.capacity) - assert(edge_ag.balance_opt === Some(33000000 msat)) + assert(edge_ag.balance_opt == Some(33000000 msat)) } } @@ -702,7 +702,7 @@ class RouterSpec extends BaseRouterSpec { false case RoutingStateStreamingUpToDate => true } - assert(nodes.size === 8 && channels.size === 5 && updates.size === 10) // public channels only + assert(nodes.size == 8 && channels.size == 5 && updates.size == 10) // public channels only // just making sure that we have been subscribed to network events, otherwise there is a possible race condition awaitCond({ diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncSpec.scala index b2e9dc3d4d..5abee91aba 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncSpec.scala @@ -140,9 +140,9 @@ class RoutingSyncSpec extends TestKitBaseClass with AnyFunSuiteLike with Paralle val extendedQueryFlags_opt = None // tell alice to sync with bob - assert(BasicSyncResult(ranges = 1, queries = 0, channels = 0, updates = 0, nodes = 0) === sync(alice, bob, extendedQueryFlags_opt).counts) - awaitCond(alice.stateData.channels === bob.stateData.channels) - awaitCond(alice.stateData.nodes === bob.stateData.nodes) + assert(BasicSyncResult(ranges = 1, queries = 0, channels = 0, updates = 0, nodes = 0) == sync(alice, bob, extendedQueryFlags_opt).counts) + awaitCond(alice.stateData.channels == bob.stateData.channels) + awaitCond(alice.stateData.nodes == bob.stateData.nodes) // add some channels and updates to bob and resync fakeRoutingInfo.take(10).values.foreach { @@ -153,18 +153,18 @@ class RoutingSyncSpec extends TestKitBaseClass with AnyFunSuiteLike with Paralle sender.send(bob, PeerRoutingMessage(sender.ref, charlieId, na1)) sender.send(bob, PeerRoutingMessage(sender.ref, charlieId, na2)) } - awaitCond(bob.stateData.channels.size === 10 && countUpdates(bob.stateData.channels) === 10) - assert(BasicSyncResult(ranges = 1, queries = 2, channels = 10, updates = 10, nodes = 10 * 2) === sync(alice, bob, extendedQueryFlags_opt).counts) - awaitCond(alice.stateData.channels === bob.stateData.channels) + awaitCond(bob.stateData.channels.size == 10 && countUpdates(bob.stateData.channels) == 10) + assert(BasicSyncResult(ranges = 1, queries = 2, channels = 10, updates = 10, nodes = 10 * 2) == sync(alice, bob, extendedQueryFlags_opt).counts) + awaitCond(alice.stateData.channels == bob.stateData.channels) // add some updates to bob and resync fakeRoutingInfo.take(10).values.foreach { case (pc, _, _) => sender.send(bob, PeerRoutingMessage(sender.ref, charlieId, pc.update_2_opt.get)) } - awaitCond(bob.stateData.channels.size === 10 && countUpdates(bob.stateData.channels) === 10 * 2) - assert(BasicSyncResult(ranges = 1, queries = 2, channels = 10, updates = 10 * 2, nodes = 10 * 2) === sync(alice, bob, extendedQueryFlags_opt).counts) - awaitCond(alice.stateData.channels === bob.stateData.channels) + awaitCond(bob.stateData.channels.size == 10 && countUpdates(bob.stateData.channels) == 10 * 2) + assert(BasicSyncResult(ranges = 1, queries = 2, channels = 10, updates = 10 * 2, nodes = 10 * 2) == sync(alice, bob, extendedQueryFlags_opt).counts) + awaitCond(alice.stateData.channels == bob.stateData.channels) // add everything (duplicates will be ignored) fakeRoutingInfo.values.foreach { @@ -175,9 +175,9 @@ class RoutingSyncSpec extends TestKitBaseClass with AnyFunSuiteLike with Paralle sender.send(bob, PeerRoutingMessage(sender.ref, charlieId, na1)) sender.send(bob, PeerRoutingMessage(sender.ref, charlieId, na2)) } - awaitCond(bob.stateData.channels.size === fakeRoutingInfo.size && countUpdates(bob.stateData.channels) === 2 * fakeRoutingInfo.size, max = 60 seconds) - assert(BasicSyncResult(ranges = 3, queries = 13, channels = fakeRoutingInfo.size, updates = 2 * fakeRoutingInfo.size, nodes = 2 * fakeRoutingInfo.size) === sync(alice, bob, extendedQueryFlags_opt).counts) - awaitCond(alice.stateData.channels === bob.stateData.channels, max = 60 seconds) + awaitCond(bob.stateData.channels.size == fakeRoutingInfo.size && countUpdates(bob.stateData.channels) == 2 * fakeRoutingInfo.size, max = 60 seconds) + assert(BasicSyncResult(ranges = 3, queries = 13, channels = fakeRoutingInfo.size, updates = 2 * fakeRoutingInfo.size, nodes = 2 * fakeRoutingInfo.size) == sync(alice, bob, extendedQueryFlags_opt).counts) + awaitCond(alice.stateData.channels == bob.stateData.channels, max = 60 seconds) } def syncWithExtendedQueries(requestNodeAnnouncements: Boolean): Unit = { @@ -189,8 +189,8 @@ class RoutingSyncSpec extends TestKitBaseClass with AnyFunSuiteLike with Paralle val extendedQueryFlags_opt = Some(QueryChannelRangeTlv.QueryFlags(QueryChannelRangeTlv.QueryFlags.WANT_ALL)) // tell alice to sync with bob - assert(BasicSyncResult(ranges = 1, queries = 0, channels = 0, updates = 0, nodes = 0) === sync(alice, bob, extendedQueryFlags_opt).counts) - awaitCond(alice.stateData.channels === bob.stateData.channels) + assert(BasicSyncResult(ranges = 1, queries = 0, channels = 0, updates = 0, nodes = 0) == sync(alice, bob, extendedQueryFlags_opt).counts) + awaitCond(alice.stateData.channels == bob.stateData.channels) // add some channels and updates to bob and resync fakeRoutingInfo.take(10).values.foreach { @@ -201,19 +201,19 @@ class RoutingSyncSpec extends TestKitBaseClass with AnyFunSuiteLike with Paralle sender.send(bob, PeerRoutingMessage(sender.ref, charlieId, na1)) sender.send(bob, PeerRoutingMessage(sender.ref, charlieId, na2)) } - awaitCond(bob.stateData.channels.size === 10 && countUpdates(bob.stateData.channels) === 10) - assert(BasicSyncResult(ranges = 1, queries = 2, channels = 10, updates = 10, nodes = if (requestNodeAnnouncements) 10 * 2 else 0) === sync(alice, bob, extendedQueryFlags_opt).counts) - awaitCond(alice.stateData.channels === bob.stateData.channels, max = 60 seconds) - if (requestNodeAnnouncements) awaitCond(alice.stateData.nodes === bob.stateData.nodes) + awaitCond(bob.stateData.channels.size == 10 && countUpdates(bob.stateData.channels) == 10) + assert(BasicSyncResult(ranges = 1, queries = 2, channels = 10, updates = 10, nodes = if (requestNodeAnnouncements) 10 * 2 else 0) == sync(alice, bob, extendedQueryFlags_opt).counts) + awaitCond(alice.stateData.channels == bob.stateData.channels, max = 60 seconds) + if (requestNodeAnnouncements) awaitCond(alice.stateData.nodes == bob.stateData.nodes) // add some updates to bob and resync fakeRoutingInfo.take(10).values.foreach { case (pc, _, _) => sender.send(bob, PeerRoutingMessage(sender.ref, charlieId, pc.update_2_opt.get)) } - awaitCond(bob.stateData.channels.size === 10 && countUpdates(bob.stateData.channels) === 10 * 2) - assert(BasicSyncResult(ranges = 1, queries = 2, channels = 0, updates = 10, nodes = if (requestNodeAnnouncements) 10 * 2 else 0) === sync(alice, bob, extendedQueryFlags_opt).counts) - awaitCond(alice.stateData.channels === bob.stateData.channels, max = 60 seconds) + awaitCond(bob.stateData.channels.size == 10 && countUpdates(bob.stateData.channels) == 10 * 2) + assert(BasicSyncResult(ranges = 1, queries = 2, channels = 0, updates = 10, nodes = if (requestNodeAnnouncements) 10 * 2 else 0) == sync(alice, bob, extendedQueryFlags_opt).counts) + awaitCond(alice.stateData.channels == bob.stateData.channels, max = 60 seconds) // add everything (duplicates will be ignored) fakeRoutingInfo.values.foreach { @@ -224,9 +224,9 @@ class RoutingSyncSpec extends TestKitBaseClass with AnyFunSuiteLike with Paralle sender.send(bob, PeerRoutingMessage(sender.ref, charlieId, na1)) sender.send(bob, PeerRoutingMessage(sender.ref, charlieId, na2)) } - awaitCond(bob.stateData.channels.size === fakeRoutingInfo.size && countUpdates(bob.stateData.channels) === 2 * fakeRoutingInfo.size, max = 60 seconds) - assert(BasicSyncResult(ranges = 3, queries = 11, channels = fakeRoutingInfo.size - 10, updates = 2 * (fakeRoutingInfo.size - 10), nodes = if (requestNodeAnnouncements) 2 * (fakeRoutingInfo.size - 10) else 0) === sync(alice, bob, extendedQueryFlags_opt).counts) - awaitCond(alice.stateData.channels === bob.stateData.channels, max = 60 seconds) + awaitCond(bob.stateData.channels.size == fakeRoutingInfo.size && countUpdates(bob.stateData.channels) == 2 * fakeRoutingInfo.size, max = 60 seconds) + assert(BasicSyncResult(ranges = 3, queries = 11, channels = fakeRoutingInfo.size - 10, updates = 2 * (fakeRoutingInfo.size - 10), nodes = if (requestNodeAnnouncements) 2 * (fakeRoutingInfo.size - 10) else 0) == sync(alice, bob, extendedQueryFlags_opt).counts) + awaitCond(alice.stateData.channels == bob.stateData.channels, max = 60 seconds) // bump random channel_updates def touchUpdate(shortChannelId: Int, side: Boolean) = { @@ -236,9 +236,9 @@ class RoutingSyncSpec extends TestKitBaseClass with AnyFunSuiteLike with Paralle val bumpedUpdates = (List(0, 3, 7).map(touchUpdate(_, side = true)) ++ List(1, 3, 9).map(touchUpdate(_, side = false))).toSet bumpedUpdates.foreach(c => sender.send(bob, PeerRoutingMessage(sender.ref, charlieId, c))) - assert(BasicSyncResult(ranges = 3, queries = 1, channels = 0, updates = bumpedUpdates.size, nodes = if (requestNodeAnnouncements) 5 * 2 else 0) === sync(alice, bob, extendedQueryFlags_opt).counts) - awaitCond(alice.stateData.channels === bob.stateData.channels, max = 60 seconds) - if (requestNodeAnnouncements) awaitCond(alice.stateData.nodes === bob.stateData.nodes) + assert(BasicSyncResult(ranges = 3, queries = 1, channels = 0, updates = bumpedUpdates.size, nodes = if (requestNodeAnnouncements) 5 * 2 else 0) == sync(alice, bob, extendedQueryFlags_opt).counts) + awaitCond(alice.stateData.channels == bob.stateData.channels, max = 60 seconds) + if (requestNodeAnnouncements) awaitCond(alice.stateData.nodes == bob.stateData.nodes) } test("sync with extended channel queries (don't request node announcements)") { @@ -263,12 +263,12 @@ class RoutingSyncSpec extends TestKitBaseClass with AnyFunSuiteLike with Paralle sender.send(router, SendChannelQuery(params.chainHash, remoteNodeId, sender.ref, replacePrevious = true, None)) val QueryChannelRange(chainHash, firstBlockNum, numberOfBlocks, _) = sender.expectMsgType[QueryChannelRange] sender.expectMsgType[GossipTimestampFilter] - assert(router.stateData.sync.get(remoteNodeId) === Some(Syncing(Nil, 0))) + assert(router.stateData.sync.get(remoteNodeId) == Some(Syncing(Nil, 0))) // ask router to send another channel range query sender.send(router, SendChannelQuery(params.chainHash, remoteNodeId, sender.ref, replacePrevious = false, None)) sender.expectNoMessage(100 millis) // it's a duplicate and should be ignored - assert(router.stateData.sync.get(remoteNodeId) === Some(Syncing(Nil, 0))) + assert(router.stateData.sync.get(remoteNodeId) == Some(Syncing(Nil, 0))) val block1 = ReplyChannelRange(chainHash, firstBlockNum, numberOfBlocks, 1, EncodedShortChannelIds(EncodingType.UNCOMPRESSED, fakeRoutingInfo.take(params.routerConf.channelQueryChunkSize).keys.toList), None, None) @@ -276,7 +276,7 @@ class RoutingSyncSpec extends TestKitBaseClass with AnyFunSuiteLike with Paralle peerConnection.send(router, PeerRoutingMessage(peerConnection.ref, remoteNodeId, block1)) // router should ask for our first block of ids - assert(peerConnection.expectMsgType[QueryShortChannelIds] === QueryShortChannelIds(chainHash, block1.shortChannelIds, TlvStream.empty)) + assert(peerConnection.expectMsgType[QueryShortChannelIds] == QueryShortChannelIds(chainHash, block1.shortChannelIds, TlvStream.empty)) // router should think that it is missing 100 channels, in one request val Some(sync) = router.stateData.sync.get(remoteNodeId) assert(sync.remainingQueries.isEmpty) // the request was sent already @@ -286,7 +286,7 @@ class RoutingSyncSpec extends TestKitBaseClass with AnyFunSuiteLike with Paralle sender.send(router, SendChannelQuery(params.chainHash, remoteNodeId, sender.ref, replacePrevious = true, None)) sender.expectMsgType[QueryChannelRange] sender.expectMsgType[GossipTimestampFilter] - assert(router.stateData.sync.get(remoteNodeId) === Some(Syncing(Nil, 0))) + assert(router.stateData.sync.get(remoteNodeId) == Some(Syncing(Nil, 0))) } test("reject unsolicited sync") { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/testutils/FixtureSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/testutils/FixtureSpec.scala new file mode 100644 index 0000000000..fe140a43c9 --- /dev/null +++ b/eclair-core/src/test/scala/fr/acinq/eclair/testutils/FixtureSpec.scala @@ -0,0 +1,82 @@ +/* + * 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.testutils + +import org.scalatest.concurrent.Eventually +import org.scalatest.funsuite.FixtureAnyFunSuite +import org.scalatest.{Assertions, Outcome, TestData} + +/** + * This is an opinionated base class for tests that need a fixture. It is a very thin layer on top of Scalatest, which + * follows the recommended way of using fixtures, and hides boiler plate code. + * + * Usage: + * {{{ + * class MyTestSpec extends FixtureSpec { + * + * type FixtureParam = MyFixtureClass + * + * override def createFixture(testData: TestData): MyFixtureClass = { + * // will be run for each test + * // build the fixture here + * // customize with testData.tags if appropriate + * ... + * } + * + * override def cleanupFixture(fixture: MyFixtureClass): Unit = { + * fixture.cleanup() // it is a good practice to define a cleanup method in your fixture case class + * } + * + * test("my first test") { f => + * import f._ + * // write your test here + * ... + * } + * } + * }}} + * + * Recommendations: + * - on test parallelization: + * - do not use [[org.scalatest.ParallelTestExecution]], parallelize on test suites instead + * - keep the execution time of each suite under one minute + * - on actor testing: + * - use a normal [[akka.actor.ActorSystem]] and real actors, use [[akka.testkit.TestProbe]] to communicate with them + * - do not use [[akka.testkit.TestKit]] class + * - do not use [[akka.testkit.TestActorRef]] + * + * References: + * - https://hseeberger.github.io/2017/09/13/2017-09-13-how-to-use-akka-testkit/ + * - https://markuseliasson.se/article/scalatest-best-practices/ + */ +abstract class FixtureSpec extends FixtureAnyFunSuite + with Assertions + with Eventually { + + def createFixture(testData: TestData): FixtureParam + + def cleanupFixture(fixture: FixtureParam): Unit + + override final def withFixture(test: OneArgTest): Outcome = { + val fixture = createFixture(test) + try { + withFixture(test.toNoArgTest(fixture)) + } finally { + cleanupFixture(fixture) + } + } + +} diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/tor/TorProtocolHandlerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/tor/TorProtocolHandlerSpec.scala index 9b342051e5..056866728d 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/tor/TorProtocolHandlerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/tor/TorProtocolHandlerSpec.scala @@ -101,9 +101,9 @@ class TorProtocolHandlerSpec extends TestKitBaseClass expectMsg(Some(Tor2("z4zif3fy7fe7bpg3", 9999))) val address = Await.result(promiseOnionAddress.future, 3 seconds) - assert(address === Tor2("z4zif3fy7fe7bpg3", 9999)) + assert(address == Tor2("z4zif3fy7fe7bpg3", 9999)) - assert(readString(PkFilePath) === "RSA1024:private-key") + assert(readString(PkFilePath) == "RSA1024:private-key") } test("happy path v3") { @@ -142,9 +142,9 @@ class TorProtocolHandlerSpec extends TestKitBaseClass expectMsg(Some(Tor3("mrl2d3ilhctt2vw4qzvmz3etzjvpnc6dczliq5chrxetthgbuczuggyd", 9999))) val address = Await.result(promiseOnionAddress.future, 3 seconds) - assert(address === Tor3("mrl2d3ilhctt2vw4qzvmz3etzjvpnc6dczliq5chrxetthgbuczuggyd", 9999)) + assert(address == Tor3("mrl2d3ilhctt2vw4qzvmz3etzjvpnc6dczliq5chrxetthgbuczuggyd", 9999)) - assert(readString(PkFilePath) === "ED25519-V3:private-key") + assert(readString(PkFilePath) == "ED25519-V3:private-key") } test("v2/v3 compatibility check against tor version") { @@ -179,7 +179,7 @@ class TorProtocolHandlerSpec extends TestKitBaseClass assert(intercept[TorException] { Await.result(promiseOnionAddress.future, 3 seconds) - } === TorException("cannot use authentication 'password', supported methods are 'COOKIE,SAFECOOKIE'")) + } == TorException("cannot use authentication 'password', supported methods are 'COOKIE,SAFECOOKIE'")) } test("invalid server hash") { @@ -211,7 +211,7 @@ class TorProtocolHandlerSpec extends TestKitBaseClass assert(intercept[TorException] { Await.result(promiseOnionAddress.future, 3 seconds) - } === TorException("unexpected server hash")) + } == TorException("unexpected server hash")) } @@ -249,7 +249,7 @@ class TorProtocolHandlerSpec extends TestKitBaseClass assert(intercept[TorException] { Await.result(promiseOnionAddress.future, 3 seconds) - } === TorException("server returned error: 515 Authentication failed: Safe cookie response did not match expected value.")) + } == TorException("server returned error: 515 Authentication failed: Safe cookie response did not match expected value.")) } test("ADD_ONION failure") { @@ -295,7 +295,7 @@ class TorProtocolHandlerSpec extends TestKitBaseClass assert(intercept[TorException] { Await.result(promiseOnionAddress.future, 3 seconds) - } === TorException("server returned error: 513 Invalid argument")) + } == TorException("server returned error: 513 Invalid argument")) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/transactions/CommitmentSpecSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/transactions/CommitmentSpecSpec.scala index 443e3ecdbf..6d5a6f8ae6 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/transactions/CommitmentSpecSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/transactions/CommitmentSpecSpec.scala @@ -31,19 +31,19 @@ class CommitmentSpecSpec extends AnyFunSuite { val add1 = UpdateAddHtlc(ByteVector32.Zeroes, 1, (2000 * 1000) msat, H, CltvExpiry(400), TestConstants.emptyOnionPacket) val spec1 = CommitmentSpec.reduce(spec, add1 :: Nil, Nil) - assert(spec1 === spec.copy(htlcs = Set(OutgoingHtlc(add1)), toLocal = 3000000 msat)) + assert(spec1 == spec.copy(htlcs = Set(OutgoingHtlc(add1)), toLocal = 3000000 msat)) val add2 = UpdateAddHtlc(ByteVector32.Zeroes, 2, (1000 * 1000) msat, H, CltvExpiry(400), TestConstants.emptyOnionPacket) val spec2 = CommitmentSpec.reduce(spec1, add2 :: Nil, Nil) - assert(spec2 === spec1.copy(htlcs = Set(OutgoingHtlc(add1), OutgoingHtlc(add2)), toLocal = 2000000 msat)) + assert(spec2 == spec1.copy(htlcs = Set(OutgoingHtlc(add1), OutgoingHtlc(add2)), toLocal = 2000000 msat)) val ful1 = UpdateFulfillHtlc(ByteVector32.Zeroes, add1.id, R) val spec3 = CommitmentSpec.reduce(spec2, Nil, ful1 :: Nil) - assert(spec3 === spec2.copy(htlcs = Set(OutgoingHtlc(add2)), toRemote = 2000000 msat)) + assert(spec3 == spec2.copy(htlcs = Set(OutgoingHtlc(add2)), toRemote = 2000000 msat)) val fail1 = UpdateFailHtlc(ByteVector32.Zeroes, add2.id, R) val spec4 = CommitmentSpec.reduce(spec3, Nil, fail1 :: Nil) - assert(spec4 === spec3.copy(htlcs = Set(), toLocal = 3000000 msat)) + assert(spec4 == spec3.copy(htlcs = Set(), toLocal = 3000000 msat)) } test("add, fulfill and fail htlcs from the receiver side") { @@ -53,26 +53,26 @@ class CommitmentSpecSpec extends AnyFunSuite { val add1 = UpdateAddHtlc(ByteVector32.Zeroes, 1, (2000 * 1000) msat, H, CltvExpiry(400), TestConstants.emptyOnionPacket) val spec1 = CommitmentSpec.reduce(spec, Nil, add1 :: Nil) - assert(spec1 === spec.copy(htlcs = Set(IncomingHtlc(add1)), toRemote = 3000 * 1000 msat)) + assert(spec1 == spec.copy(htlcs = Set(IncomingHtlc(add1)), toRemote = 3000 * 1000 msat)) val add2 = UpdateAddHtlc(ByteVector32.Zeroes, 2, (1000 * 1000) msat, H, CltvExpiry(400), TestConstants.emptyOnionPacket) val spec2 = CommitmentSpec.reduce(spec1, Nil, add2 :: Nil) - assert(spec2 === spec1.copy(htlcs = Set(IncomingHtlc(add1), IncomingHtlc(add2)), toRemote = (2000 * 1000) msat)) + assert(spec2 == spec1.copy(htlcs = Set(IncomingHtlc(add1), IncomingHtlc(add2)), toRemote = (2000 * 1000) msat)) val ful1 = UpdateFulfillHtlc(ByteVector32.Zeroes, add1.id, R) val spec3 = CommitmentSpec.reduce(spec2, ful1 :: Nil, Nil) - assert(spec3 === spec2.copy(htlcs = Set(IncomingHtlc(add2)), toLocal = (2000 * 1000) msat)) + assert(spec3 == spec2.copy(htlcs = Set(IncomingHtlc(add2)), toLocal = (2000 * 1000) msat)) val fail1 = UpdateFailHtlc(ByteVector32.Zeroes, add2.id, R) val spec4 = CommitmentSpec.reduce(spec3, fail1 :: Nil, Nil) - assert(spec4 === spec3.copy(htlcs = Set(), toRemote = (3000 * 1000) msat)) + assert(spec4 == spec3.copy(htlcs = Set(), toRemote = (3000 * 1000) msat)) } test("compute htlc tx feerate based on commitment format") { val spec = CommitmentSpec(htlcs = Set(), commitTxFeerate = FeeratePerKw(2500 sat), toLocal = (5000 * 1000) msat, toRemote = (2500 * 1000) msat) - assert(spec.htlcTxFeerate(Transactions.DefaultCommitmentFormat) === FeeratePerKw(2500 sat)) - assert(spec.htlcTxFeerate(Transactions.UnsafeLegacyAnchorOutputsCommitmentFormat) === FeeratePerKw(2500 sat)) - assert(spec.htlcTxFeerate(Transactions.ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) === FeeratePerKw(0 sat)) + assert(spec.htlcTxFeerate(Transactions.DefaultCommitmentFormat) == FeeratePerKw(2500 sat)) + assert(spec.htlcTxFeerate(Transactions.UnsafeLegacyAnchorOutputsCommitmentFormat) == FeeratePerKw(2500 sat)) + assert(spec.htlcTxFeerate(Transactions.ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) == FeeratePerKw(0 sat)) } def createHtlc(amount: MilliSatoshi): UpdateAddHtlc = { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TestVectorsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TestVectorsSpec.scala index 604680a45e..5761570969 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TestVectorsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TestVectorsSpec.scala @@ -125,7 +125,7 @@ trait TestVectorsSpec extends AnyFunSuite with Logging { val commitmentInput = Funding.makeFundingInputInfo(fundingTx.hash, 0, fundingAmount, Local.funding_pubkey, Remote.funding_pubkey) val obscured_tx_number = Transactions.obscuredCommitTxNumber(42, isInitiator = true, Local.payment_basepoint, Remote.payment_basepoint) - assert(obscured_tx_number === (0x2bb038521914L ^ 42L)) + assert(obscured_tx_number == (0x2bb038521914L ^ 42L)) logger.info(s"local_payment_basepoint: ${Local.payment_basepoint}") logger.info(s"remote_payment_basepoint: ${Remote.payment_basepoint}") @@ -227,7 +227,7 @@ trait TestVectorsSpec extends AnyFunSuite with Logging { } }) - assert(Transactions.getCommitTxNumber(commitTx.tx, isInitiator = true, Local.payment_basepoint, Remote.payment_basepoint) === Local.commitTxNumber) + assert(Transactions.getCommitTxNumber(commitTx.tx, isInitiator = true, Local.payment_basepoint, Remote.payment_basepoint) == Local.commitTxNumber) Transaction.correctlySpends(commitTx.tx, Seq(fundingTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) logger.info(s"output commit_tx: ${commitTx.tx}") @@ -276,7 +276,7 @@ trait TestVectorsSpec extends AnyFunSuite with Logging { val check = (0 to 4).flatMap(i => tests(name).get(s"htlc_success_tx (htlc #$i)").toSeq ++ tests(name).get(s"htlc_timeout_tx (htlc #$i)").toSeq ).toSet.map((tx: String) => Transaction.read(tx)) - assert(htlcTxs.map(_.tx).toSet === check) + assert(htlcTxs.map(_.tx).toSet == check) } test("simple commitment tx with no HTLCs") { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TransactionsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TransactionsSpec.scala index ca5c56aab4..b124808a6c 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TransactionsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TransactionsSpec.scala @@ -72,8 +72,8 @@ class TransactionsSpec extends AnyFunSuite with Logging { parentTxId2 -> 4, parentTxId3 -> 5, ) - assert(expected === Scripts.csvTimeouts(tx)) - assert(BlockHeight(10) === Scripts.cltvTimeout(tx)) + assert(expected == Scripts.csvTimeouts(tx)) + assert(BlockHeight(10) == Scripts.cltvTimeout(tx)) } test("encode/decode sequence and lockTime (one example)") { @@ -106,7 +106,7 @@ class TransactionsSpec extends AnyFunSuite with Logging { ) val spec = CommitmentSpec(htlcs, FeeratePerKw(5000 sat), toLocal = 0 msat, toRemote = 0 msat) val fee = commitTxFeeMsat(546 sat, spec, DefaultCommitmentFormat) - assert(fee === 5340000.msat) + assert(fee == 5340000.msat) } test("check pre-computed transaction weights") { @@ -200,7 +200,7 @@ class TransactionsSpec extends AnyFunSuite with Logging { val commitTx = Transaction(version = 0, txIn = Nil, txOut = TxOut(anchorAmount, pubKeyScript) :: Nil, lockTime = 0) val Right(claimAnchorOutputTx) = makeClaimLocalAnchorOutputTx(commitTx, localFundingPriv.publicKey, BlockHeight(1105)) assert(claimAnchorOutputTx.tx.txOut.isEmpty) - assert(claimAnchorOutputTx.confirmBefore === BlockHeight(1105)) + assert(claimAnchorOutputTx.confirmBefore == BlockHeight(1105)) // we will always add at least one input and one output to be able to set our desired feerate // we use dummy signatures to compute the weight val p2wpkhWitness = ScriptWitness(Seq(Scripts.der(PlaceHolderSig), PlaceHolderPubKey.value)) @@ -209,7 +209,7 @@ class TransactionsSpec extends AnyFunSuite with Logging { txOut = Seq(TxOut(1500 sat, Script.pay2wpkh(randomKey().publicKey))) )) val weight = Transaction.weight(addSigs(claimAnchorOutputTxWithFees, PlaceHolderSig).tx) - assert(weight === 717) + assert(weight == 717) assert(weight >= claimAnchorOutputMinWeight) } } @@ -223,26 +223,26 @@ class TransactionsSpec extends AnyFunSuite with Logging { { val toRemoteFundeeBelowDust = spec.copy(toRemote = belowDust) val outputs = makeCommitTxOutputs(localIsInitiator = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, toRemoteFundeeBelowDust, DefaultCommitmentFormat) - assert(outputs.map(_.commitmentOutput) === Seq(CommitmentOutput.ToLocal)) - assert(outputs.head.output.amount.toMilliSatoshi === toRemoteFundeeBelowDust.toLocal - commitFee) + assert(outputs.map(_.commitmentOutput) == Seq(CommitmentOutput.ToLocal)) + assert(outputs.head.output.amount.toMilliSatoshi == toRemoteFundeeBelowDust.toLocal - commitFee) } { val toLocalFunderBelowDust = spec.copy(toLocal = belowDustWithFee) val outputs = makeCommitTxOutputs(localIsInitiator = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, toLocalFunderBelowDust, DefaultCommitmentFormat) - assert(outputs.map(_.commitmentOutput) === Seq(CommitmentOutput.ToRemote)) - assert(outputs.head.output.amount.toMilliSatoshi === toLocalFunderBelowDust.toRemote) + assert(outputs.map(_.commitmentOutput) == Seq(CommitmentOutput.ToRemote)) + assert(outputs.head.output.amount.toMilliSatoshi == toLocalFunderBelowDust.toRemote) } { val toRemoteFunderBelowDust = spec.copy(toRemote = belowDustWithFee) val outputs = makeCommitTxOutputs(localIsInitiator = false, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, toRemoteFunderBelowDust, DefaultCommitmentFormat) - assert(outputs.map(_.commitmentOutput) === Seq(CommitmentOutput.ToLocal)) - assert(outputs.head.output.amount.toMilliSatoshi === toRemoteFunderBelowDust.toLocal) + assert(outputs.map(_.commitmentOutput) == Seq(CommitmentOutput.ToLocal)) + assert(outputs.head.output.amount.toMilliSatoshi == toRemoteFunderBelowDust.toLocal) } { val toLocalFundeeBelowDust = spec.copy(toLocal = belowDust) val outputs = makeCommitTxOutputs(localIsInitiator = false, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, toLocalFundeeBelowDust, DefaultCommitmentFormat) - assert(outputs.map(_.commitmentOutput) === Seq(CommitmentOutput.ToRemote)) - assert(outputs.head.output.amount.toMilliSatoshi === toLocalFundeeBelowDust.toRemote - commitFee) + assert(outputs.map(_.commitmentOutput) == Seq(CommitmentOutput.ToRemote)) + assert(outputs.head.output.amount.toMilliSatoshi == toLocalFundeeBelowDust.toRemote - commitFee) } { val allBelowDust = spec.copy(toLocal = belowDust, toRemote = belowDust) @@ -300,15 +300,15 @@ class TransactionsSpec extends AnyFunSuite with Logging { } val htlcTxs = makeHtlcTxs(commitTx.tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, spec.htlcTxFeerate(DefaultCommitmentFormat), outputs, DefaultCommitmentFormat) - assert(htlcTxs.length === 4) + assert(htlcTxs.length == 4) val confirmationTargets = htlcTxs.map(tx => tx.htlcId -> tx.confirmBefore.toLong).toMap - assert(confirmationTargets === Map(0 -> 300, 1 -> 310, 2 -> 295, 3 -> 300)) + assert(confirmationTargets == Map(0 -> 300, 1 -> 310, 2 -> 295, 3 -> 300)) val htlcSuccessTxs = htlcTxs.collect { case tx: HtlcSuccessTx => tx } val htlcTimeoutTxs = htlcTxs.collect { case tx: HtlcTimeoutTx => tx } assert(htlcTimeoutTxs.size == 2) // htlc1 and htlc3 - assert(htlcTimeoutTxs.map(_.htlcId).toSet === Set(0, 2)) + assert(htlcTimeoutTxs.map(_.htlcId).toSet == Set(0, 2)) assert(htlcSuccessTxs.size == 2) // htlc2 and htlc4 - assert(htlcSuccessTxs.map(_.htlcId).toSet === Set(1, 3)) + assert(htlcSuccessTxs.map(_.htlcId).toSet == Set(1, 3)) { // either party spends local->remote htlc output with htlc timeout tx @@ -327,7 +327,7 @@ class TransactionsSpec extends AnyFunSuite with Logging { assert(checkSpendable(signedTx).isSuccess) // local can't claim delayed output of htlc3 timeout tx because it is below the dust limit val htlcDelayed1 = makeHtlcDelayedTx(htlcTimeoutTxs(0).tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) - assert(htlcDelayed1 === Left(OutputNotFound)) + assert(htlcDelayed1 == Left(OutputNotFound)) } { // remote spends local->remote htlc1/htlc3 output directly in case of success @@ -357,7 +357,7 @@ class TransactionsSpec extends AnyFunSuite with Logging { assert(checkSpendable(signedTx).isSuccess) // local can't claim delayed output of htlc4 success tx because it is below the dust limit val htlcDelayed1 = makeHtlcDelayedTx(htlcSuccessTxs(0).tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) - assert(htlcDelayed1 === Left(AmountBelowDustLimit)) + assert(htlcDelayed1 == Left(AmountBelowDustLimit)) } { // local spends main delayed output @@ -395,7 +395,7 @@ class TransactionsSpec extends AnyFunSuite with Logging { assert(checkSpendable(signed).isSuccess) // remote can't claim revoked output of htlc3's htlc-timeout tx because it is below the dust limit val claimHtlcDelayedPenaltyTx1 = makeClaimHtlcDelayedOutputPenaltyTxs(htlcTimeoutTxs(0).tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) - assert(claimHtlcDelayedPenaltyTx1 === Seq(Left(AmountBelowDustLimit))) + assert(claimHtlcDelayedPenaltyTx1 == Seq(Left(AmountBelowDustLimit))) } { // remote spends offered HTLC output with revocation key @@ -417,7 +417,7 @@ class TransactionsSpec extends AnyFunSuite with Logging { assert(checkSpendable(signed).isSuccess) // remote can't claim revoked output of htlc4's htlc-success tx because it is below the dust limit val claimHtlcDelayedPenaltyTx1 = makeClaimHtlcDelayedOutputPenaltyTxs(htlcSuccessTxs(0).tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) - assert(claimHtlcDelayedPenaltyTx1 === Seq(Left(AmountBelowDustLimit))) + assert(claimHtlcDelayedPenaltyTx1 == Seq(Left(AmountBelowDustLimit))) } { // remote spends received HTLC output with revocation key @@ -441,39 +441,39 @@ class TransactionsSpec extends AnyFunSuite with Logging { { val outputs = makeCommitTxOutputs(localIsInitiator = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, spec, UnsafeLegacyAnchorOutputsCommitmentFormat) - assert(outputs.map(_.commitmentOutput).toSet === Set(CommitmentOutput.ToLocal, CommitmentOutput.ToRemote, CommitmentOutput.ToLocalAnchor, CommitmentOutput.ToRemoteAnchor)) - assert(outputs.find(_.commitmentOutput == CommitmentOutput.ToLocalAnchor).get.output.amount === anchorAmount) - assert(outputs.find(_.commitmentOutput == CommitmentOutput.ToRemoteAnchor).get.output.amount === anchorAmount) - assert(outputs.find(_.commitmentOutput == CommitmentOutput.ToLocal).get.output.amount.toMilliSatoshi === spec.toLocal - commitFeeAndAnchorCost) - assert(outputs.find(_.commitmentOutput == CommitmentOutput.ToRemote).get.output.amount.toMilliSatoshi === spec.toRemote) + assert(outputs.map(_.commitmentOutput).toSet == Set(CommitmentOutput.ToLocal, CommitmentOutput.ToRemote, CommitmentOutput.ToLocalAnchor, CommitmentOutput.ToRemoteAnchor)) + assert(outputs.find(_.commitmentOutput == CommitmentOutput.ToLocalAnchor).get.output.amount == anchorAmount) + assert(outputs.find(_.commitmentOutput == CommitmentOutput.ToRemoteAnchor).get.output.amount == anchorAmount) + assert(outputs.find(_.commitmentOutput == CommitmentOutput.ToLocal).get.output.amount.toMilliSatoshi == spec.toLocal - commitFeeAndAnchorCost) + assert(outputs.find(_.commitmentOutput == CommitmentOutput.ToRemote).get.output.amount.toMilliSatoshi == spec.toRemote) } { val toRemoteFundeeBelowDust = spec.copy(toRemote = belowDust) val outputs = makeCommitTxOutputs(localIsInitiator = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, toRemoteFundeeBelowDust, UnsafeLegacyAnchorOutputsCommitmentFormat) - assert(outputs.map(_.commitmentOutput).toSet === Set(CommitmentOutput.ToLocal, CommitmentOutput.ToLocalAnchor)) - assert(outputs.find(_.commitmentOutput == CommitmentOutput.ToLocalAnchor).get.output.amount === anchorAmount) - assert(outputs.find(_.commitmentOutput == CommitmentOutput.ToLocal).get.output.amount.toMilliSatoshi === spec.toLocal - commitFeeAndAnchorCost) + assert(outputs.map(_.commitmentOutput).toSet == Set(CommitmentOutput.ToLocal, CommitmentOutput.ToLocalAnchor)) + assert(outputs.find(_.commitmentOutput == CommitmentOutput.ToLocalAnchor).get.output.amount == anchorAmount) + assert(outputs.find(_.commitmentOutput == CommitmentOutput.ToLocal).get.output.amount.toMilliSatoshi == spec.toLocal - commitFeeAndAnchorCost) } { val toLocalFunderBelowDust = spec.copy(toLocal = belowDustWithFeeAndAnchors) val outputs = makeCommitTxOutputs(localIsInitiator = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, toLocalFunderBelowDust, UnsafeLegacyAnchorOutputsCommitmentFormat) - assert(outputs.map(_.commitmentOutput).toSet === Set(CommitmentOutput.ToRemote, CommitmentOutput.ToRemoteAnchor)) - assert(outputs.find(_.commitmentOutput == CommitmentOutput.ToRemoteAnchor).get.output.amount === anchorAmount) - assert(outputs.find(_.commitmentOutput == CommitmentOutput.ToRemote).get.output.amount.toMilliSatoshi === spec.toRemote) + assert(outputs.map(_.commitmentOutput).toSet == Set(CommitmentOutput.ToRemote, CommitmentOutput.ToRemoteAnchor)) + assert(outputs.find(_.commitmentOutput == CommitmentOutput.ToRemoteAnchor).get.output.amount == anchorAmount) + assert(outputs.find(_.commitmentOutput == CommitmentOutput.ToRemote).get.output.amount.toMilliSatoshi == spec.toRemote) } { val toRemoteFunderBelowDust = spec.copy(toRemote = belowDustWithFeeAndAnchors) val outputs = makeCommitTxOutputs(localIsInitiator = false, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, toRemoteFunderBelowDust, UnsafeLegacyAnchorOutputsCommitmentFormat) - assert(outputs.map(_.commitmentOutput).toSet === Set(CommitmentOutput.ToLocal, CommitmentOutput.ToLocalAnchor)) - assert(outputs.find(_.commitmentOutput == CommitmentOutput.ToLocalAnchor).get.output.amount === anchorAmount) - assert(outputs.find(_.commitmentOutput == CommitmentOutput.ToLocal).get.output.amount.toMilliSatoshi === spec.toLocal) + assert(outputs.map(_.commitmentOutput).toSet == Set(CommitmentOutput.ToLocal, CommitmentOutput.ToLocalAnchor)) + assert(outputs.find(_.commitmentOutput == CommitmentOutput.ToLocalAnchor).get.output.amount == anchorAmount) + assert(outputs.find(_.commitmentOutput == CommitmentOutput.ToLocal).get.output.amount.toMilliSatoshi == spec.toLocal) } { val toLocalFundeeBelowDust = spec.copy(toLocal = belowDust) val outputs = makeCommitTxOutputs(localIsInitiator = false, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, toLocalFundeeBelowDust, UnsafeLegacyAnchorOutputsCommitmentFormat) - assert(outputs.map(_.commitmentOutput).toSet === Set(CommitmentOutput.ToRemote, CommitmentOutput.ToRemoteAnchor)) - assert(outputs.find(_.commitmentOutput == CommitmentOutput.ToRemoteAnchor).get.output.amount === anchorAmount) - assert(outputs.find(_.commitmentOutput == CommitmentOutput.ToRemote).get.output.amount.toMilliSatoshi === spec.toRemote - commitFeeAndAnchorCost) + assert(outputs.map(_.commitmentOutput).toSet == Set(CommitmentOutput.ToRemote, CommitmentOutput.ToRemoteAnchor)) + assert(outputs.find(_.commitmentOutput == CommitmentOutput.ToRemoteAnchor).get.output.amount == anchorAmount) + assert(outputs.find(_.commitmentOutput == CommitmentOutput.ToRemote).get.output.amount.toMilliSatoshi == spec.toRemote - commitFeeAndAnchorCost) } { val allBelowDust = spec.copy(toLocal = belowDust, toRemote = belowDust) @@ -528,30 +528,30 @@ class TransactionsSpec extends AnyFunSuite with Logging { val commitTx = Transactions.addSigs(txInfo, localFundingPriv.publicKey, remoteFundingPriv.publicKey, localSig, remoteSig) val htlcTxs = makeHtlcTxs(commitTx.tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, spec.htlcTxFeerate(UnsafeLegacyAnchorOutputsCommitmentFormat), outputs, UnsafeLegacyAnchorOutputsCommitmentFormat) - assert(htlcTxs.length === 5) + assert(htlcTxs.length == 5) val confirmationTargets = htlcTxs.map(tx => tx.htlcId -> tx.confirmBefore.toLong).toMap - assert(confirmationTargets === Map(0 -> 300, 1 -> 310, 2 -> 310, 3 -> 295, 4 -> 300)) + assert(confirmationTargets == Map(0 -> 300, 1 -> 310, 2 -> 310, 3 -> 295, 4 -> 300)) val htlcSuccessTxs = htlcTxs.collect { case tx: HtlcSuccessTx => tx } val htlcTimeoutTxs = htlcTxs.collect { case tx: HtlcTimeoutTx => tx } assert(htlcTimeoutTxs.size == 2) // htlc1 and htlc3 - assert(htlcTimeoutTxs.map(_.htlcId).toSet === Set(0, 3)) + assert(htlcTimeoutTxs.map(_.htlcId).toSet == Set(0, 3)) assert(htlcSuccessTxs.size == 3) // htlc2a, htlc2b and htlc4 - assert(htlcSuccessTxs.map(_.htlcId).toSet === Set(1, 2, 4)) + assert(htlcSuccessTxs.map(_.htlcId).toSet == Set(1, 2, 4)) val zeroFeeOutputs = makeCommitTxOutputs(localIsInitiator = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, spec, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) val zeroFeeCommitTx = makeCommitTx(commitInput, commitTxNumber, localPaymentPriv.publicKey, remotePaymentPriv.publicKey, localIsInitiator = true, zeroFeeOutputs) val zeroFeeHtlcTxs = makeHtlcTxs(zeroFeeCommitTx.tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, spec.htlcTxFeerate(ZeroFeeHtlcTxAnchorOutputsCommitmentFormat), zeroFeeOutputs, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) - assert(zeroFeeHtlcTxs.length === 7) + assert(zeroFeeHtlcTxs.length == 7) val zeroFeeConfirmationTargets = zeroFeeHtlcTxs.map(tx => tx.htlcId -> tx.confirmBefore.toLong).toMap - assert(zeroFeeConfirmationTargets === Map(0 -> 300, 1 -> 310, 2 -> 310, 3 -> 295, 4 -> 300, 7 -> 300, 8 -> 302)) + assert(zeroFeeConfirmationTargets == Map(0 -> 300, 1 -> 310, 2 -> 310, 3 -> 295, 4 -> 300, 7 -> 300, 8 -> 302)) val zeroFeeHtlcSuccessTxs = zeroFeeHtlcTxs.collect { case tx: HtlcSuccessTx => tx } val zeroFeeHtlcTimeoutTxs = zeroFeeHtlcTxs.collect { case tx: HtlcTimeoutTx => tx } - zeroFeeHtlcSuccessTxs.foreach(tx => assert(tx.fee === 0.sat)) - zeroFeeHtlcTimeoutTxs.foreach(tx => assert(tx.fee === 0.sat)) + zeroFeeHtlcSuccessTxs.foreach(tx => assert(tx.fee == 0.sat)) + zeroFeeHtlcTimeoutTxs.foreach(tx => assert(tx.fee == 0.sat)) assert(zeroFeeHtlcTimeoutTxs.size == 3) // htlc1, htlc3 and htlc7 - assert(zeroFeeHtlcTimeoutTxs.map(_.htlcId).toSet === Set(0, 3, 7)) + assert(zeroFeeHtlcTimeoutTxs.map(_.htlcId).toSet == Set(0, 3, 7)) assert(zeroFeeHtlcSuccessTxs.size == 4) // htlc2a, htlc2b, htlc4 and htlc8 - assert(zeroFeeHtlcSuccessTxs.map(_.htlcId).toSet === Set(1, 2, 4, 8)) + assert(zeroFeeHtlcSuccessTxs.map(_.htlcId).toSet == Set(1, 2, 4, 8)) (commitTx, outputs, htlcTimeoutTxs, htlcSuccessTxs) } @@ -566,7 +566,7 @@ class TransactionsSpec extends AnyFunSuite with Logging { { // remote cannot spend main output with default commitment format val Left(failure) = makeClaimP2WPKHOutputTx(commitTx.tx, localDustLimit, remotePaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) - assert(failure === OutputNotFound) + assert(failure == OutputNotFound) } { // remote spends main delayed output @@ -622,7 +622,7 @@ class TransactionsSpec extends AnyFunSuite with Logging { assert(checkSpendable(signedTx).isSuccess) // local can't claim delayed output of htlc3 timeout tx because it is below the dust limit val htlcDelayed1 = makeHtlcDelayedTx(htlcTimeoutTxs(0).tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) - assert(htlcDelayed1 === Left(OutputNotFound)) + assert(htlcDelayed1 == Left(OutputNotFound)) } { // local spends offered htlc with HTLC-success tx @@ -654,7 +654,7 @@ class TransactionsSpec extends AnyFunSuite with Logging { } // local can't claim delayed output of htlc4 success tx because it is below the dust limit val claimHtlcDelayed1 = makeClaimLocalDelayedOutputTx(htlcSuccessTxs(0).tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) - assert(claimHtlcDelayed1 === Left(AmountBelowDustLimit)) + assert(claimHtlcDelayed1 == Left(AmountBelowDustLimit)) } { // remote spends local->remote htlc outputs directly in case of success @@ -673,7 +673,7 @@ class TransactionsSpec extends AnyFunSuite with Logging { assert(checkSpendable(signed).isSuccess) // remote can't claim revoked output of htlc3's htlc-timeout tx because it is below the dust limit val claimHtlcDelayedPenaltyTx1 = makeClaimHtlcDelayedOutputPenaltyTxs(htlcTimeoutTxs(0).tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) - assert(claimHtlcDelayedPenaltyTx1 === Seq(Left(AmountBelowDustLimit))) + assert(claimHtlcDelayedPenaltyTx1 == Seq(Left(AmountBelowDustLimit))) } { // remote spends remote->local htlc output directly in case of timeout @@ -695,7 +695,7 @@ class TransactionsSpec extends AnyFunSuite with Logging { } // remote can't claim revoked output of htlc4's htlc-success tx because it is below the dust limit val claimHtlcDelayedPenaltyTx1 = makeClaimHtlcDelayedOutputPenaltyTxs(htlcSuccessTxs(0).tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) - assert(claimHtlcDelayedPenaltyTx1 === Seq(Left(AmountBelowDustLimit))) + assert(claimHtlcDelayedPenaltyTx1 == Seq(Left(AmountBelowDustLimit))) } { // remote spends all htlc txs aggregated in a single tx @@ -703,13 +703,13 @@ class TransactionsSpec extends AnyFunSuite with Logging { val txOut = htlcTimeoutTxs.flatMap(_.tx.txOut) ++ htlcSuccessTxs.flatMap(_.tx.txOut) val aggregatedHtlcTx = Transaction(2, txIn, txOut, 0) val claimHtlcDelayedPenaltyTxs = makeClaimHtlcDelayedOutputPenaltyTxs(aggregatedHtlcTx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) - assert(claimHtlcDelayedPenaltyTxs.size === 5) + assert(claimHtlcDelayedPenaltyTxs.size == 5) val skipped = claimHtlcDelayedPenaltyTxs.collect { case Left(reason) => reason } - assert(skipped.size === 2) - assert(skipped.toSet === Set(AmountBelowDustLimit)) + assert(skipped.size == 2) + assert(skipped.toSet == Set(AmountBelowDustLimit)) val claimed = claimHtlcDelayedPenaltyTxs.collect { case Right(tx) => tx } - assert(claimed.size === 3) - assert(claimed.map(_.input.outPoint).toSet.size === 3) + assert(claimed.size == 3) + assert(claimed.map(_.input.outPoint).toSet.size == 3) } { // remote spends offered htlc output with revocation key @@ -795,10 +795,10 @@ class TransactionsSpec extends AnyFunSuite with Logging { } // htlc3 and htlc4 are completely identical, their relative order can't be enforced. - assert(htlcTxs.length === 5) + assert(htlcTxs.length == 5) htlcTxs.foreach(tx => assert(tx.isInstanceOf[HtlcTimeoutTx])) val htlcIds = htlcTxs.sortBy(_.input.outPoint.index).map(_.htlcId) - assert(htlcIds === Seq(1, 2, 3, 4, 5) || htlcIds === Seq(1, 2, 4, 3, 5)) + assert(htlcIds == Seq(1, 2, 3, 4, 5) || htlcIds == Seq(1, 2, 4, 3, 5)) assert(htlcOut2.publicKeyScript.toHex < htlcOut3.publicKeyScript.toHex) assert(outputs.find(_.commitmentOutput == OutHtlc(OutgoingHtlc(htlc2))).map(_.output.publicKeyScript).contains(htlcOut2.publicKeyScript)) @@ -816,23 +816,23 @@ class TransactionsSpec extends AnyFunSuite with Logging { // Different amounts, both outputs untrimmed, local is funder: val spec = CommitmentSpec(Set.empty, feeratePerKw, 150_000_000 msat, 250_000_000 msat) val closingTx = makeClosingTx(commitInput, localPubKeyScript, remotePubKeyScript, localIsInitiator = true, localDustLimit, 1000 sat, spec) - assert(closingTx.tx.txOut.length === 2) + assert(closingTx.tx.txOut.length == 2) assert(closingTx.toLocalOutput !== None) val toLocal = closingTx.toLocalOutput.get - assert(toLocal.publicKeyScript === localPubKeyScript) - assert(toLocal.amount === 149_000.sat) // funder pays the fee + assert(toLocal.publicKeyScript == localPubKeyScript) + assert(toLocal.amount == 149_000.sat) // funder pays the fee val toRemoteIndex = (toLocal.index + 1) % 2 - assert(closingTx.tx.txOut(toRemoteIndex.toInt).amount === 250_000.sat) + assert(closingTx.tx.txOut(toRemoteIndex.toInt).amount == 250_000.sat) } { // Same amounts, both outputs untrimmed, local is fundee: val spec = CommitmentSpec(Set.empty, feeratePerKw, 150_000_000 msat, 150_000_000 msat) val closingTx = makeClosingTx(commitInput, localPubKeyScript, remotePubKeyScript, localIsInitiator = false, localDustLimit, 1000 sat, spec) - assert(closingTx.tx.txOut.length === 2) + assert(closingTx.tx.txOut.length == 2) assert(closingTx.toLocalOutput !== None) val toLocal = closingTx.toLocalOutput.get - assert(toLocal.publicKeyScript === localPubKeyScript) - assert(toLocal.amount === 150_000.sat) + assert(toLocal.publicKeyScript == localPubKeyScript) + assert(toLocal.amount == 150_000.sat) val toRemoteIndex = (toLocal.index + 1) % 2 assert(closingTx.tx.txOut(toRemoteIndex.toInt).amount < 150_000.sat) } @@ -840,26 +840,26 @@ class TransactionsSpec extends AnyFunSuite with Logging { // Their output is trimmed: val spec = CommitmentSpec(Set.empty, feeratePerKw, 150_000_000 msat, 1_000 msat) val closingTx = makeClosingTx(commitInput, localPubKeyScript, remotePubKeyScript, localIsInitiator = false, localDustLimit, 1000 sat, spec) - assert(closingTx.tx.txOut.length === 1) + assert(closingTx.tx.txOut.length == 1) assert(closingTx.toLocalOutput !== None) val toLocal = closingTx.toLocalOutput.get - assert(toLocal.publicKeyScript === localPubKeyScript) - assert(toLocal.amount === 150_000.sat) - assert(toLocal.index === 0) + assert(toLocal.publicKeyScript == localPubKeyScript) + assert(toLocal.amount == 150_000.sat) + assert(toLocal.index == 0) } { // Our output is trimmed: val spec = CommitmentSpec(Set.empty, feeratePerKw, 50_000 msat, 150_000_000 msat) val closingTx = makeClosingTx(commitInput, localPubKeyScript, remotePubKeyScript, localIsInitiator = true, localDustLimit, 1000 sat, spec) - assert(closingTx.tx.txOut.length === 1) - assert(closingTx.toLocalOutput === None) + assert(closingTx.tx.txOut.length == 1) + assert(closingTx.toLocalOutput == None) } { // Both outputs are trimmed: val spec = CommitmentSpec(Set.empty, feeratePerKw, 50_000 msat, 10_000 msat) val closingTx = makeClosingTx(commitInput, localPubKeyScript, remotePubKeyScript, localIsInitiator = true, localDustLimit, 1000 sat, spec) assert(closingTx.tx.txOut.isEmpty) - assert(closingTx.toLocalOutput === None) + assert(closingTx.toLocalOutput == None) } } @@ -900,11 +900,11 @@ class TransactionsSpec extends AnyFunSuite with Logging { TestVector(name, CommitmentSpec(htlcs, FeeratePerKw(feerate_per_kw.toLong.sat), MilliSatoshi(to_local_msat.toLong), MilliSatoshi(to_remote_msat.toLong)), Satoshi(fee.toLong)) }).toSeq - assert(tests.size === 30, "there were 15 tests at b201efe0546120c14bf154ce5f4e18da7243fe7a") // simple non-reg to make sure we are not missing tests + assert(tests.size == 30, "there were 15 tests at b201efe0546120c14bf154ce5f4e18da7243fe7a") // simple non-reg to make sure we are not missing tests tests.foreach(test => { logger.info(s"running BOLT 3 test: '${test.name}'") val fee = commitTxTotalCost(dustLimit, test.spec, DefaultCommitmentFormat) - assert(fee === test.expectedFee) + assert(fee == test.expectedFee) }) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/CommandCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/CommandCodecsSpec.scala index d7fcaefc78..592dc1d54a 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/CommandCodecsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/CommandCodecsSpec.scala @@ -44,7 +44,7 @@ class CommandCodecsSpec extends AnyFunSuite { msg => val encoded = CommandCodecs.cmdCodec.encode(msg).require val decoded = CommandCodecs.cmdCodec.decode(encoded).require - assert(msg === decoded.value) + assert(msg == decoded.value) } } @@ -57,14 +57,14 @@ class CommandCodecsSpec extends AnyFunSuite { (("id" | int64) :: ("r" | bytes32) :: ("commit" | provide(false))) - assert(CommandCodecs.cmdFulfillCodec.decode(legacyCmdFulfillCodec.encode(42 :: data32 :: true :: HNil).require).require === + assert(CommandCodecs.cmdFulfillCodec.decode(legacyCmdFulfillCodec.encode(42 :: data32 :: true :: HNil).require).require == DecodeResult(CMD_FULFILL_HTLC(42, data32, commit = false, None), BitVector.empty)) val legacyCmdFailCodec = (("id" | int64) :: ("reason" | either(bool, varsizebinarydata, failureMessageCodec)) :: ("commit" | provide(false))) - assert(CommandCodecs.cmdFailCodec.decode(legacyCmdFailCodec.encode(42 :: Left(data123) :: true :: HNil).require).require === + assert(CommandCodecs.cmdFailCodec.decode(legacyCmdFailCodec.encode(42 :: Left(data123) :: true :: HNil).require).require == DecodeResult(CMD_FAIL_HTLC(42, Left(data123), commit = false, None), BitVector.empty)) val legacyCmdFailMalformedCodec = @@ -72,7 +72,7 @@ class CommandCodecsSpec extends AnyFunSuite { ("onionHash" | bytes32) :: ("failureCode" | uint16) :: ("commit" | provide(false))) - assert(CommandCodecs.cmdFailMalformedCodec.decode(legacyCmdFailMalformedCodec.encode(42 :: data32 :: 456 :: true :: HNil).require).require === + assert(CommandCodecs.cmdFailMalformedCodec.decode(legacyCmdFailMalformedCodec.encode(42 :: data32 :: 456 :: true :: HNil).require).require == DecodeResult(CMD_FAIL_MALFORMED_HTLC(42, data32, 456, commit = false, None), BitVector.empty)) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/ChannelCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/ChannelCodecsSpec.scala index 1112ea9903..fe3e7663f7 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/ChannelCodecsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/ChannelCodecsSpec.scala @@ -71,13 +71,13 @@ class ChannelCodecsSpec extends AnyFunSuite { .htlcTxsAndRemoteSigs .map(data => Scripts.der(data.remoteSig)) - assert(ref === sigs) + assert(ref == sigs) } test("nonreg for channel flags codec") { // make sure that we correctly decode data encoded with the previous standard 'byte' codec - assert(CommonCodecs.channelflags.decode(byte.encode(0).require).require === DecodeResult(ChannelFlags(announceChannel = false), BitVector.empty)) - assert(CommonCodecs.channelflags.decode(byte.encode(1).require).require === DecodeResult(ChannelFlags(announceChannel = true), BitVector.empty)) + assert(CommonCodecs.channelflags.decode(byte.encode(0).require).require == DecodeResult(ChannelFlags(announceChannel = false), BitVector.empty)) + assert(CommonCodecs.channelflags.decode(byte.encode(1).require).require == DecodeResult(ChannelFlags(announceChannel = true), BitVector.empty)) } test("backward compatibility DATA_WAIT_FOR_FUNDING_CONFIRMED_COMPAT_01_Codec") { @@ -87,7 +87,7 @@ class ChannelCodecsSpec extends AnyFunSuite { assert(bin_old.startsWith(hex"000001")) // let's decode the old data (this will use the old codec that provides default values for new fields) val data_new = channelDataCodec.decode(bin_old.toBitVector).require.value - assert(data_new.asInstanceOf[DATA_WAIT_FOR_FUNDING_CONFIRMED].fundingTx === None) + assert(data_new.asInstanceOf[DATA_WAIT_FOR_FUNDING_CONFIRMED].fundingTx == None) assert(TimestampSecond.now().toLong - data_new.asInstanceOf[DATA_WAIT_FOR_FUNDING_CONFIRMED].waitingSince.toLong < 3600) // we just set this to current time // and re-encode it with the new codec val bin_new = ByteVector(channelDataCodec.encode(data_new).require.toByteVector.toArray) @@ -96,7 +96,7 @@ class ChannelCodecsSpec extends AnyFunSuite { // now let's decode it again val data_new2 = channelDataCodec.decode(bin_new.toBitVector).require.value // data should match perfectly - assert(data_new === data_new2) + assert(data_new == data_new2) } test("backward compatibility DATA_NORMAL_COMPAT_03_Codec (roundtrip)") { @@ -127,7 +127,7 @@ class ChannelCodecsSpec extends AnyFunSuite { assert(newbin.startsWith(hex"030007")) // make sure that round-trip yields the same data val newnormal = channelDataCodec.decode(newbin.bits).require.value - assert(newnormal === oldnormal) + assert(newnormal == oldnormal) } } @@ -152,8 +152,8 @@ class ChannelCodecsSpec extends AnyFunSuite { // finally we check that the actual data is the same as before (we just remove the new json field) val oldjson = Serialization.write(oldnormal)(JsonSerializers.formats) val newjson = Serialization.write(newnormal)(JsonSerializers.formats) - assert(oldjson === refjson) - assert(newjson === refjson) + assert(oldjson == refjson) + assert(newjson == refjson) } } @@ -178,38 +178,38 @@ class ChannelCodecsSpec extends AnyFunSuite { assert(newBin.startsWith(hex"0300")) // make sure that round-trip yields the same data val decoded2 = channelDataCodec.decode(newBin.bits).require.value - assert(decoded1 === decoded2) + assert(decoded1 == decoded2) }) val negotiating = channelDataCodec.decode(dataNegotiating.bits).require.value.asInstanceOf[DATA_NEGOTIATING] assert(negotiating.bestUnpublishedClosingTx_opt.nonEmpty) - negotiating.bestUnpublishedClosingTx_opt.foreach(tx => assert(tx.toLocalOutput === None)) + negotiating.bestUnpublishedClosingTx_opt.foreach(tx => assert(tx.toLocalOutput == None)) assert(negotiating.closingTxProposed.flatten.nonEmpty) - negotiating.closingTxProposed.flatten.foreach(tx => assert(tx.unsignedTx.toLocalOutput === None)) + negotiating.closingTxProposed.flatten.foreach(tx => assert(tx.unsignedTx.toLocalOutput == None)) val normal = channelDataCodec.decode(dataNormal.bits).require.value.asInstanceOf[DATA_NORMAL] assert(normal.commitments.localCommit.htlcTxsAndRemoteSigs.nonEmpty) - normal.commitments.localCommit.htlcTxsAndRemoteSigs.foreach(tx => assert(tx.htlcTx.htlcId === 0)) + normal.commitments.localCommit.htlcTxsAndRemoteSigs.foreach(tx => assert(tx.htlcTx.htlcId == 0)) val closingLocal = channelDataCodec.decode(dataClosingLocal.bits).require.value.asInstanceOf[DATA_CLOSING] assert(closingLocal.localCommitPublished.nonEmpty) - assert(closingLocal.localCommitPublished.get.commitTx.txOut.size === 6) - assert(closingLocal.localCommitPublished.get.htlcTxs.size === 4) - assert(closingLocal.localCommitPublished.get.claimHtlcDelayedTxs.size === 4) + assert(closingLocal.localCommitPublished.get.commitTx.txOut.size == 6) + assert(closingLocal.localCommitPublished.get.htlcTxs.size == 4) + assert(closingLocal.localCommitPublished.get.claimHtlcDelayedTxs.size == 4) assert(closingLocal.localCommitPublished.get.irrevocablySpent.isEmpty) val closingRemote = channelDataCodec.decode(dataClosingRemote.bits).require.value.asInstanceOf[DATA_CLOSING] assert(closingRemote.remoteCommitPublished.nonEmpty) - assert(closingRemote.remoteCommitPublished.get.commitTx.txOut.size === 7) - assert(closingRemote.remoteCommitPublished.get.commitTx.txOut.count(_.amount === AnchorOutputsCommitmentFormat.anchorAmount) === 2) - assert(closingRemote.remoteCommitPublished.get.claimHtlcTxs.size === 3) + assert(closingRemote.remoteCommitPublished.get.commitTx.txOut.size == 7) + assert(closingRemote.remoteCommitPublished.get.commitTx.txOut.count(_.amount == AnchorOutputsCommitmentFormat.anchorAmount) == 2) + assert(closingRemote.remoteCommitPublished.get.claimHtlcTxs.size == 3) assert(closingRemote.remoteCommitPublished.get.irrevocablySpent.isEmpty) val closingRevoked = channelDataCodec.decode(dataClosingRevoked.bits).require.value.asInstanceOf[DATA_CLOSING] - assert(closingRevoked.revokedCommitPublished.size === 1) - assert(closingRevoked.revokedCommitPublished.head.commitTx.txOut.size === 6) - assert(closingRevoked.revokedCommitPublished.head.htlcPenaltyTxs.size === 4) - assert(closingRevoked.revokedCommitPublished.head.claimHtlcDelayedPenaltyTxs.size === 2) + assert(closingRevoked.revokedCommitPublished.size == 1) + assert(closingRevoked.revokedCommitPublished.head.commitTx.txOut.size == 6) + assert(closingRevoked.revokedCommitPublished.head.htlcPenaltyTxs.size == 4) + assert(closingRevoked.revokedCommitPublished.head.claimHtlcDelayedPenaltyTxs.size == 2) assert(closingRevoked.revokedCommitPublished.head.irrevocablySpent.isEmpty) } @@ -236,7 +236,7 @@ class ChannelCodecsSpec extends AnyFunSuite { val newbin = channelDataCodec.encode(oldnormal).require.bytes // make sure that round-trip yields the same data val newnormal = channelDataCodec.decode(newbin.bits).require.value - assert(newnormal === oldnormal) + assert(newnormal == oldnormal) // make sure that we have stripped sigs from the transactions assert(newnormal.commitments.localCommit.commitTxAndRemoteSig.commitTx.tx.txIn.forall(_.witness.stack.isEmpty)) assert(newnormal.commitments.localCommit.htlcTxsAndRemoteSigs.forall(_.htlcTx.tx.txIn.forall(_.witness.stack.isEmpty))) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelCodecs0Spec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelCodecs0Spec.scala index 82c086eed4..24fe9f5e89 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelCodecs0Spec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelCodecs0Spec.scala @@ -22,21 +22,21 @@ class ChannelCodecs0Spec extends AnyFunSuite { val current03 = hex"010000000103d5c030835d6a6248b2d1d4cac60813838011b995a66b6f78dcc9fb8b5c40c3f3" val current04 = hex"010000000303d5c030835d6a6248b2d1d4cac60813838011b995a66b6f78dcc9fb8b5c40c3f3" - assert(codec.decode(legacy02.bits) === Attempt.successful(DecodeResult(ChannelVersion.ZEROES, legacy02.bits))) - assert(codec.decode(legacy03.bits) === Attempt.successful(DecodeResult(ChannelVersion.ZEROES, legacy03.bits))) - assert(codec.decode(current02.bits) === Attempt.successful(DecodeResult(ChannelVersion.STANDARD, current02.drop(5).bits))) - assert(codec.decode(current03.bits) === Attempt.successful(DecodeResult(ChannelVersion.STANDARD, current03.drop(5).bits))) - assert(codec.decode(current04.bits) === Attempt.successful(DecodeResult(ChannelVersion.STATIC_REMOTEKEY, current04.drop(5).bits))) - - assert(codec.encode(ChannelVersion.STANDARD) === Attempt.successful(hex"0100000001".bits)) - assert(codec.encode(ChannelVersion.STATIC_REMOTEKEY) === Attempt.successful(hex"0100000003".bits)) + assert(codec.decode(legacy02.bits) == Attempt.successful(DecodeResult(ChannelVersion.ZEROES, legacy02.bits))) + assert(codec.decode(legacy03.bits) == Attempt.successful(DecodeResult(ChannelVersion.ZEROES, legacy03.bits))) + assert(codec.decode(current02.bits) == Attempt.successful(DecodeResult(ChannelVersion.STANDARD, current02.drop(5).bits))) + assert(codec.decode(current03.bits) == Attempt.successful(DecodeResult(ChannelVersion.STANDARD, current03.drop(5).bits))) + assert(codec.decode(current04.bits) == Attempt.successful(DecodeResult(ChannelVersion.STATIC_REMOTEKEY, current04.drop(5).bits))) + + assert(codec.encode(ChannelVersion.STANDARD) == Attempt.successful(hex"0100000001".bits)) + assert(codec.encode(ChannelVersion.STATIC_REMOTEKEY) == Attempt.successful(hex"0100000003".bits)) } test("backward compatibility local params with global features") { // Backwards-compatibility: decode localparams with global features. val withGlobalFeatures = hex"033b1d42aa7c6a1a3502cbcfe4d2787e9f96237465cd1ba675f50cadf0be17092500010000002a0000000026cb536b00000000568a2768000000004f182e8d0000000040dd1d3d10e3040d00422f82d368b09056d1dcb2d67c4e8cae516abbbc8932f2b7d8f93b3be8e8cc6b64bb164563d567189bad0e07e24e821795aaef2dcbb9e5c1ad579961680202b38de5dd5426c524c7523b1fcdcf8c600d47f4b96a6dd48516b8e0006e81c83464b2800db0f3f63ceeb23a81511d159bae9ad07d10c0d144ba2da6f0cff30e7154eb48c908e9000101000001044500" val withGlobalFeaturesDecoded = localParamsCodec(ChannelVersion.STANDARD).decode(withGlobalFeatures.bits).require.value - assert(withGlobalFeaturesDecoded.initFeatures.toByteVector === hex"0a8a") + assert(withGlobalFeaturesDecoded.initFeatures.toByteVector == hex"0a8a") } test("backward compatibility of htlc codec") { @@ -69,8 +69,8 @@ class ChannelCodecs0Spec extends AnyFunSuite { assert(r1 == remaining) assert(r2 == remaining) - assert(codec.encode(h1).require.bytes === encodedHtlc1) - assert(codec.encode(h2).require.bytes === encodedHtlc2) + assert(codec.encode(h1).require.bytes == encodedHtlc1) + assert(codec.encode(h2).require.bytes == encodedHtlc2) } test("tell channel_update length") { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version1/ChannelCodecs1Spec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version1/ChannelCodecs1Spec.scala index 6add1bdf43..545da0fdc5 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version1/ChannelCodecs1Spec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version1/ChannelCodecs1Spec.scala @@ -25,14 +25,14 @@ class ChannelCodecs1Spec extends AnyFunSuite { val keyPath = KeyPath(Seq(0L, 0L, 0L, 0L)) val encoded = keyPathCodec.encode(keyPath).require val decoded = keyPathCodec.decode(encoded).require - assert(keyPath === decoded.value) + assert(keyPath == decoded.value) } test("encode/decode key paths (all 1s)") { val keyPath = KeyPath(Seq(0xffffffffL, 0xffffffffL, 0xffffffffL, 0xffffffffL)) val encoded = keyPathCodec.encode(keyPath).require val decoded = keyPathCodec.decode(encoded).require - assert(keyPath === decoded.value) + assert(keyPath == decoded.value) } test("encode/decode channel version") { @@ -41,21 +41,21 @@ class ChannelCodecs1Spec extends AnyFunSuite { val current04 = hex"0000000303d5c030835d6a6248b2d1d4cac60813838011b995a66b6f78dcc9fb8b5c40c3f3" val current05 = hex"0000000703d5c030835d6a6248b2d1d4cac60813838011b995a66b6f78dcc9fb8b5c40c3f3" - assert(channelVersionCodec.decode(current02.bits) === Attempt.successful(DecodeResult(ChannelVersion.STANDARD, current02.drop(4).bits))) - assert(channelVersionCodec.decode(current03.bits) === Attempt.successful(DecodeResult(ChannelVersion.STANDARD, current03.drop(4).bits))) - assert(channelVersionCodec.decode(current04.bits) === Attempt.successful(DecodeResult(ChannelVersion.STATIC_REMOTEKEY, current04.drop(4).bits))) - assert(channelVersionCodec.decode(current05.bits) === Attempt.successful(DecodeResult(ChannelVersion.ANCHOR_OUTPUTS, current05.drop(4).bits))) + assert(channelVersionCodec.decode(current02.bits) == Attempt.successful(DecodeResult(ChannelVersion.STANDARD, current02.drop(4).bits))) + assert(channelVersionCodec.decode(current03.bits) == Attempt.successful(DecodeResult(ChannelVersion.STANDARD, current03.drop(4).bits))) + assert(channelVersionCodec.decode(current04.bits) == Attempt.successful(DecodeResult(ChannelVersion.STATIC_REMOTEKEY, current04.drop(4).bits))) + assert(channelVersionCodec.decode(current05.bits) == Attempt.successful(DecodeResult(ChannelVersion.ANCHOR_OUTPUTS, current05.drop(4).bits))) - assert(channelVersionCodec.encode(ChannelVersion.STANDARD) === Attempt.successful(hex"00000001".bits)) - assert(channelVersionCodec.encode(ChannelVersion.STATIC_REMOTEKEY) === Attempt.successful(hex"00000003".bits)) - assert(channelVersionCodec.encode(ChannelVersion.ANCHOR_OUTPUTS) === Attempt.successful(hex"00000007".bits)) + assert(channelVersionCodec.encode(ChannelVersion.STANDARD) == Attempt.successful(hex"00000001".bits)) + assert(channelVersionCodec.encode(ChannelVersion.STATIC_REMOTEKEY) == Attempt.successful(hex"00000003".bits)) + assert(channelVersionCodec.encode(ChannelVersion.ANCHOR_OUTPUTS) == Attempt.successful(hex"00000007".bits)) } test("encode/decode local params") { def roundtrip(localParams: LocalParams, codec: Codec[LocalParams]) = { val encoded = codec.encode(localParams).require val decoded = codec.decode(encoded).require - assert(localParams === decoded.value) + assert(localParams == decoded.value) } val o = LocalParams( @@ -96,12 +96,12 @@ class ChannelCodecs1Spec extends AnyFunSuite { shutdownScript = None) val encoded = remoteParamsCodec.encode(o).require val decoded = remoteParamsCodec.decodeValue(encoded).require - assert(o === decoded) + assert(o == decoded) // Backwards-compatibility: decode remote params with global features. val withGlobalFeatures = hex"03c70c3b813815a8b79f41622b6f2c343fa24d94fb35fa7110bbb3d4d59cd9612e0000000059844cbc000000001b1524ea000000001503cbac000000006b75d3272e38777e029fa4e94066163024177311de7ba1befec2e48b473c387bbcee1484bf276a54460215e3dfb8e6f262222c5f343f5e38c5c9a43d2594c7f06dd7ac1a4326c665dd050347aba4d56d7007a7dcf03594423dccba9ed700d11e665d261594e1154203df31020d457ee336ba6eeb328d00f1b8bd8bfefb8a4dcd5af6db4c438b7ec5106c7edc0380df17e1beb0f238e51a39122ac4c6fb57f3c4f5b7bc9432f991b1ef4a8af3570002020000018a" val withGlobalFeaturesDecoded = remoteParamsCodec.decode(withGlobalFeatures.bits).require.value - assert(withGlobalFeaturesDecoded.initFeatures.toByteVector === hex"028a") + assert(withGlobalFeaturesDecoded.initFeatures.toByteVector == hex"028a") } test("encode/decode htlc") { @@ -114,8 +114,8 @@ class ChannelCodecs1Spec extends AnyFunSuite { onionRoutingPacket = TestConstants.emptyOnionPacket) val htlc1 = IncomingHtlc(add) val htlc2 = OutgoingHtlc(add) - assert(htlcCodec.decodeValue(htlcCodec.encode(htlc1).require).require === htlc1) - assert(htlcCodec.decodeValue(htlcCodec.encode(htlc2).require).require === htlc2) + assert(htlcCodec.decodeValue(htlcCodec.encode(htlc1).require).require == htlc1) + assert(htlcCodec.decodeValue(htlcCodec.encode(htlc2).require).require == htlc2) } test("encode/decode commitment spec") { @@ -136,7 +136,7 @@ class ChannelCodecs1Spec extends AnyFunSuite { val htlc1 = IncomingHtlc(add1) val htlc2 = OutgoingHtlc(add2) val htlcs = Set[DirectedHtlc](htlc1, htlc2) - assert(setCodec(htlcCodec).decodeValue(setCodec(htlcCodec).encode(htlcs).require).require === htlcs) + assert(setCodec(htlcCodec).decodeValue(setCodec(htlcCodec).encode(htlcs).require).require == htlcs) val o = CommitmentSpec( htlcs = Set(htlc1, htlc2), commitTxFeerate = FeeratePerKw(Random.nextInt(Int.MaxValue).sat), @@ -145,7 +145,7 @@ class ChannelCodecs1Spec extends AnyFunSuite { ) val encoded = commitmentSpecCodec.encode(o).require val decoded = commitmentSpecCodec.decode(encoded).require - assert(o === decoded.value) + assert(o == decoded.value) } test("encode/decode origin") { @@ -153,14 +153,14 @@ class ChannelCodecs1Spec extends AnyFunSuite { val localHot = Origin.LocalHot(replyTo, UUID.randomUUID()) val localCold = Origin.LocalCold(localHot.id) - assert(originCodec.decodeValue(originCodec.encode(localHot).require).require === localCold) - assert(originCodec.decodeValue(originCodec.encode(localCold).require).require === localCold) + assert(originCodec.decodeValue(originCodec.encode(localHot).require).require == localCold) + assert(originCodec.decodeValue(originCodec.encode(localCold).require).require == localCold) val add = UpdateAddHtlc(randomBytes32(), 4324, 11000000 msat, randomBytes32(), CltvExpiry(400000), TestConstants.emptyOnionPacket) val relayedHot = Origin.ChannelRelayedHot(replyTo, add, 11000000 msat) val relayedCold = Origin.ChannelRelayedCold(add.channelId, add.id, add.amountMsat, relayedHot.amountOut) - assert(originCodec.decodeValue(originCodec.encode(relayedHot).require).require === relayedCold) - assert(originCodec.decodeValue(originCodec.encode(relayedCold).require).require === relayedCold) + assert(originCodec.decodeValue(originCodec.encode(relayedHot).require).require == relayedCold) + assert(originCodec.decodeValue(originCodec.encode(relayedCold).require).require == relayedCold) val adds = Seq( UpdateAddHtlc(randomBytes32(), 1L, 1000 msat, randomBytes32(), CltvExpiry(400000), TestConstants.emptyOnionPacket), @@ -169,8 +169,8 @@ class ChannelCodecs1Spec extends AnyFunSuite { ) val trampolineRelayedHot = Origin.TrampolineRelayedHot(replyTo, adds) val trampolineRelayedCold = Origin.TrampolineRelayedCold(trampolineRelayedHot.htlcs) - assert(originCodec.decodeValue(originCodec.encode(trampolineRelayedHot).require).require === trampolineRelayedCold) - assert(originCodec.decodeValue(originCodec.encode(trampolineRelayedCold).require).require === trampolineRelayedCold) + assert(originCodec.decodeValue(originCodec.encode(trampolineRelayedHot).require).require == trampolineRelayedCold) + assert(originCodec.decodeValue(originCodec.encode(trampolineRelayedCold).require).require == trampolineRelayedCold) } test("encode/decode map of origins") { @@ -184,7 +184,7 @@ class ChannelCodecs1Spec extends AnyFunSuite { -32L -> Origin.ChannelRelayedCold(randomBytes32(), 54, 15000000 msat, 14000000 msat), -54L -> Origin.TrampolineRelayedCold((randomBytes32(), 1L) :: (randomBytes32(), 2L) :: Nil), -4L -> Origin.LocalCold(UUID.randomUUID())) - assert(originsMapCodec.decodeValue(originsMapCodec.encode(map).require).require === map) + assert(originsMapCodec.decodeValue(originsMapCodec.encode(map).require).require == map) } test("encode/decode map of spending txes") { @@ -194,7 +194,7 @@ class ChannelCodecs1Spec extends AnyFunSuite { OutPoint(randomBytes32(), 0) -> randomBytes32(), OutPoint(randomBytes32(), 454513) -> randomBytes32() ) - assert(spentMapCodec.decodeValue(spentMapCodec.encode(map).require).require === map) + assert(spentMapCodec.decodeValue(spentMapCodec.encode(map).require).require == map) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version2/ChannelCodecs2Spec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version2/ChannelCodecs2Spec.scala index 9072ec4de7..ca95bd323e 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version2/ChannelCodecs2Spec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version2/ChannelCodecs2Spec.scala @@ -20,7 +20,7 @@ class ChannelCodecs2Spec extends AnyFunSuite { OutPoint(randomBytes32(), 0) -> Transaction.read("02000000000101b8cefef62ea66f5178b9361b2371be0759cbc8c689bcfa7a8e6746d497ec221a040000000001000000010a060000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e050047304402203c3a699fb80a38112aafd73d6e3a9b7d40bc2c3ed8b7fbc182a20f43b215172202204e71821b984d1af52c4b8e2cd4c572578c12a965866130c2345f61e4c2d3fef48347304402205bcfa92f83c69289a412b0b6dd4f2a0fe0b0fc2d45bd74706e963257a09ea24902203783e47883e60b86240e877fcbf33d50b1742f65bc93b3162d1be26583b367ee012001010101010101010101010101010101010101010101010101010101010101018d76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a9144b6b2e5444c2639cc0fb7bcea5afba3f3cdce23988527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f501b175ac6851b2756800000000"), OutPoint(randomBytes32(), 454513) -> Transaction.read("02000000000101ab84ff284f162cfbfef241f853b47d4368d171f9e2a1445160cd591c4c7d882b00000000000000000001e8030000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500483045022100d9e29616b8f3959f1d3d7f7ce893ffedcdc407717d0de8e37d808c91d3a7c50d022078c3033f6d00095c8720a4bc943c1b45727818c082e4e3ddbc6d3116435b624b014730440220636de5682ef0c5b61f124ec74e8aa2461a69777521d6998295dcea36bc3338110220165285594b23c50b28b82df200234566628a27bcd17f7f14404bd865354eb3ce012000000000000000000000000000000000000000000000000000000000000000008a76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a914b8bcb07f6344b42ab04250c86a6e8b75d3fdbbc688527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f401b175ac686800000000") ) - assert(spentMapCodec.decodeValue(spentMapCodec.encode(map).require).require === map) + assert(spentMapCodec.decodeValue(spentMapCodec.encode(map).require).require == map) } test("remove signatures from commitment txs") { @@ -38,32 +38,32 @@ class ChannelCodecs2Spec extends AnyFunSuite { { // Standard channel val commitments = channelDataCodec.decode(dataNormal.bits).require.value.commitments - assert(commitments.channelConfig === ChannelConfig.standard) - assert(commitments.channelFeatures === ChannelFeatures()) + assert(commitments.channelConfig == ChannelConfig.standard) + assert(commitments.channelFeatures == ChannelFeatures()) } { val staticRemoteKeyChannel = hex"000200000003039dc0e0b1d25905e44fdf6f8e89755a5e219685840d0bc1d28d3308f9628a358500092eaee758da6a5beab04167c5b889732a98ec556cf92689da41744b10159ace488000000000000000000003e8ffffffffffffffff0000000000004e2000000000000003e80090001e001600140a46fc26a4ef7b50aaf16759d2f0ca8b014fd0a6028feba10d0eafd0fad8fe20e6d9206e6bd30242826de05c63f459a00aced24b120000000302698203af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d0000000000000044c000000001dcd6500000000000000271000000000000000000090006402a87d8f1844a3809284885b41cfc9713a561780b2b784a7a027851f68aaa838730328e2fd423323abcd86b15335ff44679068acd4db71b8052f63a2a7f0b47432bd028feba10d0eafd0fad8fe20e6d9206e6bd30242826de05c63f459a00aced24b12025ee0984b83efe2369c23c3754e89d19905f8094a86486de3b241cf8735208292031163dfd4eee879b9a333290cf10e52eb010d1da45bd44efee6fab16d1035120c0000186b02000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000269820000000000000000010001fffd05aab125c87884677613fe191e097b3806667482e0ff7668c64bca637300fe02307200000000000000000000000002faf080a2fbf30338f78b739d2b0a0a806506d6877408a995791901c43f6ef98d8a7a2e00061b10000223bb38126f1131ad95b37e0b30adf0ae315f66f8cf7460afccba393fa848d036073b6577f39b0e54043691e2eec74f46b86584ffab7aa01d15e6a7ff5ff9d7c60d095c7952a19e263c267f94fbe0208fdd212b304fa8d3df1379017d184fbcce7ffd5175997774c9de7ef2f3733c7056a22888c9f302e4a9bac35f581d4f571a7ef29e6cf3b1d8a7cc05cd155a047ae498a18fccb6589338242d91e00493a86b2b78810de20644f0f065399894854bd7347927a0ce8f9c1102c9c7227ffba4b71c4325f301933397aa8f54a28d3844c71021d19b72795141af019c9a0abae3e2cc29033b7f8be8465ac8406d4c1d61d0bc3c87dee2faac4d649fc166917c81a1e237c91d5c8091f696a72241c67047cfcec893bb41e97753b3d362695bc31f776cd4a744bf09f2e428dc189fa4838f34d130d5fd4fc3d79bc7546d30d543ca99818a8c39f6c17e74ff45976c95fd6de261d68e2c5fc585d28fa9ae5abb1db475f24d16d9cb0562a589c6dec07f3b81a37b4c7f2c4642df1fabf5d7343e038706e786d2cea71f76663f57a88751287db3f8e9a7a896990374daf72eb776ee67c9ec61c1a10f164688ecbc88caa086e24daa79808899e3c3f6870ed326f5253fc038147774fa8795351f44cbadc53b67743822b89f27f0c03b2b0f4e4c774598db681cadde0e4e28f1f4edbbc8ec68649fe05e3c8cdbc46b07011c9b81b9cb3ad92ec08ddf8209a501ce064ee0bd9befce196c0c559cffcb3b0c447922ad50fbceb288037ec36abb17d93deebd40e83d8c197248b14ee12dcfe72e46f0826ef1e6e94700ad6428df77b54eb8ca767af1fd84507adc52beef32f48e3968c24d990d04cd8b35c87e270a04e2e8fc9d0ab6a6398c2168986321cc0dc297f8e0e4887226db20958e934bc35151de72d896511e3b850d548402a0e1f32fb01d5050a8ff9d7ba6818600a2dafa1c4b3ca11c0d091796190c05a1edc8146d7ef534d67bb39f76f37279dff6be5e053b7315c3f3406e9240c6b618b18545fccb46d668c007a1b640634c663bbd27c5d00a48f5c3702bac6202b55cce4212920be059ecf708db85f07c0fcc0f480fcd06aef1128fc491b2b384d89ce15120c81bd65c225d679f8cd38fb1b0ddb7d8edd3455506fa3406578f43ba4caab00593b6623b7b69c863409f0350b8b8783b9ee2ada9b9f410ab309527b7f67758f543261f52695da19e597f9457483d188e8a2bd7248a6ee5f2d01cccacd2561239959f0ce80cbb83503891a8b4e3ab88d3a02de5f8c784c0700e8bef68445615f25dd3d5baea34bc8e694a7282b054f99fa2b150af00ec62c2fd2227a23e0c0a36090b5986271f964e969cc9313ef42c3b7a9ff9e608342c73da023019e983ee6b6f630e12a02ae79802bfbee10ba20199895d7f1302d2c3b8d7ba6ad08cbb27265f9bb709bf6225a81bdabf7fc5fe9ee4794b00181038051cae0a52c08d8a7750ee46c99b7dd6cd452c52a347f12873dc697786b89ddd13e2d584aa3244cdbdb7809bc7a42728d756485070ff84aaba81b1506a7b20d2acfef11df724ee78e8b0f92376676d1e01aed7ad8d84919b8296684dafc152680b0f964db5a1b7ee4093f453fe24750c6857252e4befef3df2d648e32cc20c1ef48fd5865c242951cd359f92985680f0d626602f164b6285971a194a4baf4eb1d9a3e88841269b3e84a96e6784407f869bca5db2bb2d31e784c35e210dcaf5d3441edea9a4a348e3ead6c9b920e1ff8f1c76c41d564827cd3db006e741a8dbb5db9bdbe13033faac7e6b676bc1cf88677d10676ddca3e187caeeac52b69212503f7acca29a49c699d68f0d982c4c41806c8d9bbbc4e7d2d77a1a0655c73cfcd21b93d95e70d2bad6c306dc172b67120335bd2d03878b3d9e5967c54dd567d307c15e305b5200002710000000000bebc200000000002cb4178024b125c87884677613fe191e097b3806667482e0ff7668c64bca637300fe023072000000002b40420f000000000022002090750411c84891017ca3ab8e6212a198dbf957143d6753f7b48264c7faf45cf44752210257f2ca56049071462716385481d04d3c7c859d7e9b3f31c019b126aa0eeed1e82102a87d8f1844a3809284885b41cfc9713a561780b2b784a7a027851f68aaa8387352aefd018602000000000101b125c87884677613fe191e097b3806667482e0ff7668c64bca637300fe023072000000000036a2ab800350c300000000000022002042ee833f99ec58f28a3ffc13033c29d4e02f72f6169e0e8e039ad9323e912e30400d030000000000220020981c45edf62e238461337b25d5fd7de72a305e7f4c7755a693f877b1a564831cb04e0b0000000000160014761879f7b274ce995f87150a02e75cc0c037e8e30400483045022100dc11d7761cdf7c708ea2aaaed409a4d24697bb07948704e77b7cbe911d0268fd02204a81721e85bb1ff6cb34741ed5c5ee89c4ea8f5bb1515792625d32043682c14301483045022100af2d760500664483ea81ccaa3bc925c5587cd3cd4358ee27110f62929903c49602201ce1a97498227eaef1ab5c04da2eb9ea8114abe6af1bd35494d9cbb0e64da3e7014752210257f2ca56049071462716385481d04d3c7c859d7e9b3f31c019b126aa0eeed1e82102a87d8f1844a3809284885b41cfc9713a561780b2b784a7a027851f68aaa8387352aead678920000101241bfa1a637efc49b76b41be6ec36c47c546f69b1646a11e8420957a1dbf731487000000002b50c300000000000022002042ee833f99ec58f28a3ffc13033c29d4e02f72f6169e0e8e039ad9323e912e308b76a91420fe4794a7f993489c0477c442fed3be2c9294678763ac67210230714b6385889be70b5299603fbc508a01517a3b754d03e33e5bcf125104adc57c8201208763a914e5a9c4cf993e5053ca47ddd2a194a3c522c1fdc588527c2103bff704aae937cd0b98704a4791140cab835c5bd2406897104602c881352869ae52ae677503101b06b175ac68685e02000000011bfa1a637efc49b76b41be6ec36c47c546f69b1646a11e8420957a1dbf73148700000000000000000001daa7000000000000220020981c45edf62e238461337b25d5fd7de72a305e7f4c7755a693f877b1a564831c00000000a2fbf30338f78b739d2b0a0a806506d6877408a995791901c43f6ef98d8a7a2e000000000000000040c5c678fda30c1e23bbb68093b1d9a34169304ff3d36bfef74157f129df8d01e31ddcd8dd04f3ac9521ac13b7fed5d7997ce0c72c942054711d924ea197c7ab7640cd0045f41ffd38fa637cfc09679c628b29a8273223db1150e262d5159cf5d7852542c4ad7334f67d08ca835cd5b16b1df96544c454dff121856685471d2255300000000000000001000100fd05aab125c87884677613fe191e097b3806667482e0ff7668c64bca637300fe02307200000000000000000000000002faf080a2fbf30338f78b739d2b0a0a806506d6877408a995791901c43f6ef98d8a7a2e00061b10000223bb38126f1131ad95b37e0b30adf0ae315f66f8cf7460afccba393fa848d036073b6577f39b0e54043691e2eec74f46b86584ffab7aa01d15e6a7ff5ff9d7c60d095c7952a19e263c267f94fbe0208fdd212b304fa8d3df1379017d184fbcce7ffd5175997774c9de7ef2f3733c7056a22888c9f302e4a9bac35f581d4f571a7ef29e6cf3b1d8a7cc05cd155a047ae498a18fccb6589338242d91e00493a86b2b78810de20644f0f065399894854bd7347927a0ce8f9c1102c9c7227ffba4b71c4325f301933397aa8f54a28d3844c71021d19b72795141af019c9a0abae3e2cc29033b7f8be8465ac8406d4c1d61d0bc3c87dee2faac4d649fc166917c81a1e237c91d5c8091f696a72241c67047cfcec893bb41e97753b3d362695bc31f776cd4a744bf09f2e428dc189fa4838f34d130d5fd4fc3d79bc7546d30d543ca99818a8c39f6c17e74ff45976c95fd6de261d68e2c5fc585d28fa9ae5abb1db475f24d16d9cb0562a589c6dec07f3b81a37b4c7f2c4642df1fabf5d7343e038706e786d2cea71f76663f57a88751287db3f8e9a7a896990374daf72eb776ee67c9ec61c1a10f164688ecbc88caa086e24daa79808899e3c3f6870ed326f5253fc038147774fa8795351f44cbadc53b67743822b89f27f0c03b2b0f4e4c774598db681cadde0e4e28f1f4edbbc8ec68649fe05e3c8cdbc46b07011c9b81b9cb3ad92ec08ddf8209a501ce064ee0bd9befce196c0c559cffcb3b0c447922ad50fbceb288037ec36abb17d93deebd40e83d8c197248b14ee12dcfe72e46f0826ef1e6e94700ad6428df77b54eb8ca767af1fd84507adc52beef32f48e3968c24d990d04cd8b35c87e270a04e2e8fc9d0ab6a6398c2168986321cc0dc297f8e0e4887226db20958e934bc35151de72d896511e3b850d548402a0e1f32fb01d5050a8ff9d7ba6818600a2dafa1c4b3ca11c0d091796190c05a1edc8146d7ef534d67bb39f76f37279dff6be5e053b7315c3f3406e9240c6b618b18545fccb46d668c007a1b640634c663bbd27c5d00a48f5c3702bac6202b55cce4212920be059ecf708db85f07c0fcc0f480fcd06aef1128fc491b2b384d89ce15120c81bd65c225d679f8cd38fb1b0ddb7d8edd3455506fa3406578f43ba4caab00593b6623b7b69c863409f0350b8b8783b9ee2ada9b9f410ab309527b7f67758f543261f52695da19e597f9457483d188e8a2bd7248a6ee5f2d01cccacd2561239959f0ce80cbb83503891a8b4e3ab88d3a02de5f8c784c0700e8bef68445615f25dd3d5baea34bc8e694a7282b054f99fa2b150af00ec62c2fd2227a23e0c0a36090b5986271f964e969cc9313ef42c3b7a9ff9e608342c73da023019e983ee6b6f630e12a02ae79802bfbee10ba20199895d7f1302d2c3b8d7ba6ad08cbb27265f9bb709bf6225a81bdabf7fc5fe9ee4794b00181038051cae0a52c08d8a7750ee46c99b7dd6cd452c52a347f12873dc697786b89ddd13e2d584aa3244cdbdb7809bc7a42728d756485070ff84aaba81b1506a7b20d2acfef11df724ee78e8b0f92376676d1e01aed7ad8d84919b8296684dafc152680b0f964db5a1b7ee4093f453fe24750c6857252e4befef3df2d648e32cc20c1ef48fd5865c242951cd359f92985680f0d626602f164b6285971a194a4baf4eb1d9a3e88841269b3e84a96e6784407f869bca5db2bb2d31e784c35e210dcaf5d3441edea9a4a348e3ead6c9b920e1ff8f1c76c41d564827cd3db006e741a8dbb5db9bdbe13033faac7e6b676bc1cf88677d10676ddca3e187caeeac52b69212503f7acca29a49c699d68f0d982c4c41806c8d9bbbc4e7d2d77a1a0655c73cfcd21b93d95e70d2bad6c306dc172b67120335bd2d03878b3d9e5967c54dd567d307c15e305b5200002710000000002cb41780000000000bebc200b5c42b5c67eb6043b2776c0e9a8bd91d8ffdf7280b81dce825558ad5258746fb030d9b4186ad4f32aae9ca90701dc66e8df3f7b73e1f9590c0f6cd690aae8ad097000000000000000000000000000000000000000000000000000000010000ff0250b3cac5267f9448efcb8c2a89944f8bb4e4e7f1a3ae72d4961dfe187105b3f324b125c87884677613fe191e097b3806667482e0ff7668c64bca637300fe023072000000002b40420f000000000022002090750411c84891017ca3ab8e6212a198dbf957143d6753f7b48264c7faf45cf44752210257f2ca56049071462716385481d04d3c7c859d7e9b3f31c019b126aa0eeed1e82102a87d8f1844a3809284885b41cfc9713a561780b2b784a7a027851f68aaa8387352ae000100400000ffffffffffff0020b0e394cbe4d79e47a73926b89d17d8835e8f042d7adb244a84b6c30496c6355f80007fffffffffff80b125c87884677613fe191e097b3806667482e0ff7668c64bca637300fe023072061a8000002a00000000888bc37730827b0ccb54f13689babb8d211a6cbbec55113caad14a97f6bfce7d85313c9cefa398d15213abf841e3c9a8b88f62fa3fc742a6c746619d3e76aa71fb06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f061a8000002a000060e32b8d010000900000000000000000000854d00000000a000000003b9aca000000" val commitments = channelDataCodec.decode(staticRemoteKeyChannel.bits).require.value.commitments - assert(commitments.channelConfig === ChannelConfig.standard) - assert(commitments.channelFeatures === ChannelFeatures(Features.StaticRemoteKey)) + assert(commitments.channelConfig == ChannelConfig.standard) + assert(commitments.channelFeatures == ChannelFeatures(Features.StaticRemoteKey)) } { val anchorOutputsChannel = hex"00020000000703af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d000094b82816a3f15ab3e324ddf6f0f5a116cef8c3200a353240db684337d2dcaa81e80000001000000000000044c000000001dcd65000000000000002710000000000000000000900064ff160014f09f6f93de60c6af417ed31d52c2c883b6113f260000186b0200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000225982039dc0e0b1d25905e44fdf6f8e89755a5e219685840d0bc1d28d3308f9628a358500000000000003e8ffffffffffffffff0000000000004e2000000000000003e80090001e02e9cd12509fbc345c10e2b2e10427ae43eabbf0788bda8118b34344e5576d5d5002eb112dd8d61a02bf3434d5417e1020ed2d951e75bd1e14615d473dd5f1991b4402ca8b6891d6a53aea035259fa0d57a1e672d3cc59a2c696a96247d3557ee726cd030f2aa0ca0ae0e00d6da847d897cf7288ae286dd12037f06250937dc7b37fe0b5028d59e68c5799980a98b120acb891dabfd091e80c6596b0c2a7de57a298d48512000000032259820000000000000000000000000009c4000000002faf0800000000000bebc2002432e16e6e875d1f02a9512943ba2eb9309f4715390b52e4965dd6e39a2989ae63000000002b40420f00000000002200204de81ee2e9850dabdb9c364a12cc2dd75dd20c21d4ca0a809a2e6af3caeeaf9647522102e9cd12509fbc345c10e2b2e10427ae43eabbf0788bda8118b34344e5576d5d502103683425bf640d5e4338553ca6f6afea948ff2bf378eed1b3e9497a3f18f477d5352aefd01bc0200000000010132e16e6e875d1f02a9512943ba2eb9309f4715390b52e4965dd6e39a2989ae63000000000089273f80044a010000000000002200202102815a0b14d50a2a8c028cd47413e743f02ad5460151bd4ae5930639cbbd484a010000000000002200204e81bd6470c45c349068c2b0ad544934b718ef49c1615f2a0b0cd8766400b562400d0300000000002200208eadab92c95168438acfb73f2d3d92f56d6fc30d3f79fc4527f4e77df88b8add72270c0000000000220020895c0fef7efb7aa0e4805381434363b30386d31f6cb621abb0ca4a52b269fbc304004830450221009f09a584b1af9ba86f1618aad1ca9817324f97c3d3789b2490c3b9435c7bf1dd02205e4dcca3d7937f6b80a0ed39084e4ea9e9c531b80698ebd0c116fd9319a0db1201473044022001e4104ec25ba9bc43bf3f623d6e98f371434442b134255667ddeedd9cd9cd1b02201f057d2c15ee4e5d62a73f1db262846ca7ad35deaed2496abdb5b16cd91c5cb80147522102e9cd12509fbc345c10e2b2e10427ae43eabbf0788bda8118b34344e5576d5d502103683425bf640d5e4338553ca6f6afea948ff2bf378eed1b3e9497a3f18f477d5352ae25899520000000000000000000000000000009c4000000000bebc200000000002faf0800acdb0bf90ed7350d429c2d70fce66300ac9b27d8dbb3bceb5bd2cc8bd443938f03f54d00601f1b95e82836540d760ab6b38bc6fc1701df5d5e3b410f1936212d54000000000000000000000000000000000000000000000000000000000000ff03658b1e02efd8464688f87a052518691ff4d818a7c36addf4c6520ae07b7e20912432e16e6e875d1f02a9512943ba2eb9309f4715390b52e4965dd6e39a2989ae63000000002b40420f00000000002200204de81ee2e9850dabdb9c364a12cc2dd75dd20c21d4ca0a809a2e6af3caeeaf9647522102e9cd12509fbc345c10e2b2e10427ae43eabbf0788bda8118b34344e5576d5d502103683425bf640d5e4338553ca6f6afea948ff2bf378eed1b3e9497a3f18f477d5352ae00000032e16e6e875d1f02a9512943ba2eb9309f4715390b52e4965dd6e39a2989ae63061a8000002a000000008833a9275befbedbf928e743a71c9476f46528baf6e832ad25478c2607e4108dea67657c51db7344032627c855518179dc7a94b59f306faf9a3b4eef7684d5d73106226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f061a8000002a000060e32a590101009000000000000003e8000854d00000000a000000003b9aca000000" val commitments = channelDataCodec.decode(anchorOutputsChannel.bits).require.value.commitments - assert(commitments.channelConfig === ChannelConfig.standard) - assert(commitments.channelFeatures === ChannelFeatures(Features.StaticRemoteKey, Features.AnchorOutputs)) + assert(commitments.channelConfig == ChannelConfig.standard) + assert(commitments.channelFeatures == ChannelFeatures(Features.StaticRemoteKey, Features.AnchorOutputs)) } { val wumboChannel = hex"00000000000103af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d00009b3b6da18b7e5e43405415a43e61abd77edf4d300c131af17a955950837d7db4980000001000000000000044c000000001dcd65000000000000002710000000000000000000900064ff1600142946bdbb3141be9a40ed8133ef159ee0022176e90000186b02000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a4982039dc0e0b1d25905e44fdf6f8e89755a5e219685840d0bc1d28d3308f9628a358500000000000003e8ffffffffffffffff0000000000004e2000000000000003e80090001e03a6be2613a9ff761a769b077abb43d76c5f06944fc32010891156179bf8b81607025c24f0c3572631ea09dade913ccbb1cd8a55585fc152cc2165bf4f43af702c8b0306c241c667eefee031639052c4a35c4ae076ab56b49f76f7e12f63dfb7bb32a503fdbff1ca92c99c09a9c0eac3cfe1ca35deb658d5fdb815fe7a240df25da5e20f02c47d9e4e1fb501c81933cfc4d3ba7f0e8203003cda2683569f1f67aac2b6e20d000000030a4982000000000000000000000000002710000000745e66c600000000000bebc2002464fc06b43e94d9ce0a5a0e1ef13f63ba78e56d920e7d606d0c6e7a631c48c32c000000002b0065cd1d00000000220020d1b88cc50b09abce945ce6703f30dae3d71ec626be2201925db9379a3e8b981047522103044d393b0da6bcd92030f3131d0780b0d6fa017a0ff45b7fed708903c2950ced2103a6be2613a9ff761a769b077abb43d76c5f06944fc32010891156179bf8b8160752aefd01590200000000010164fc06b43e94d9ce0a5a0e1ef13f63ba78e56d920e7d606d0c6e7a631c48c32c000000000018e2678002400d030000000000160014cf3c9d08ec27d3ffefbf7e2640d3773e11a60ceb783bca1d00000000220020153e514ade07f347db9ed32cf6b2096d066d48e0cf8c504590cd1155de68be2204004730440220056ac83d5b2eb9eb9714e85294d080a9b64f73916d8206b15533613b4303ed3d022044da40ba433e27ddb8743c75acc0f6b050c346bc9d241ab3a1b65051820ce06a0147304402207683c10f33682e2389e5f02dd34090bbe4d786633d49e1777d5ec2ab71a7924902206d99dceb74ff809237ae8e06c118b38047e2d4a49f9956c538bedca47cb4b1da0147522103044d393b0da6bcd92030f3131d0780b0d6fa017a0ff45b7fed708903c2950ced2103a6be2613a9ff761a769b077abb43d76c5f06944fc32010891156179bf8b8160752aef5c4602000000000000000000000000000002710000000000bebc200000000745e66c6000bdebd6cf39db4c2ce1a675281b0a714e525525149032ba048d75c89e5a9b355021a87b53e12e4b36287635cdf887991a732c2fd56b12f5377d755b8b890795db6000000000000000000000000000000000000000000000000000000000000ff032c5f941f78a00105b995fba7f3c340afe53e287a60361135386ae1448d6f3d632464fc06b43e94d9ce0a5a0e1ef13f63ba78e56d920e7d606d0c6e7a631c48c32c000000002b0065cd1d00000000220020d1b88cc50b09abce945ce6703f30dae3d71ec626be2201925db9379a3e8b981047522103044d393b0da6bcd92030f3131d0780b0d6fa017a0ff45b7fed708903c2950ced2103a6be2613a9ff761a769b077abb43d76c5f06944fc32010891156179bf8b8160752ae00000064fc06b43e94d9ce0a5a0e1ef13f63ba78e56d920e7d606d0c6e7a631c48c32cff5e020000000101010101010101010101010101010101010101010101010101010101010101012a00000000ffffffff010065cd1d00000000220020d1b88cc50b09abce945ce6703f30dae3d71ec626be2201925db9379a3e8b9810000000000000000060e32bf9000082000000000000000000000000000000000000000000000000000000000000000064fc06b43e94d9ce0a5a0e1ef13f63ba78e56d920e7d606d0c6e7a631c48c32c0000f4957823b0a6b76697d7c545bcca66abec8522a0f6350a722e45f3f2830f7f824c84efed8daffa04bb0822ce1a08fb2de77dd20c23a91cced7a3966808956644" val commitments = channelDataCodec.decode(wumboChannel.bits).require.value.commitments - assert(commitments.channelConfig === ChannelConfig.standard) - assert(commitments.channelFeatures === ChannelFeatures(Features.Wumbo)) + assert(commitments.channelConfig == ChannelConfig.standard) + assert(commitments.channelFeatures == ChannelFeatures(Features.Wumbo)) } } test("ensure remote shutdown script is not set") { val commitments = channelDataCodec.decode(dataNormal.bits).require.value.commitments - assert(commitments.remoteParams.shutdownScript === None) + assert(commitments.remoteParams.shutdownScript == None) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3Spec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3Spec.scala index 4f2f3a634d..18f8a86de7 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3Spec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3Spec.scala @@ -35,21 +35,21 @@ class ChannelCodecs3Spec extends AnyFunSuite { val data = normal val bin = DATA_NORMAL_Codec.encode(data).require val check = DATA_NORMAL_Codec.decodeValue(bin).require - assert(data.commitments.localCommit.spec === check.commitments.localCommit.spec) - assert(data === check) + assert(data.commitments.localCommit.spec == check.commitments.localCommit.spec) + assert(data == check) } test("encode/decode channel configuration options") { - assert(channelConfigCodec.encode(ChannelConfig(Set.empty[ChannelConfigOption])).require.bytes === hex"00") - assert(channelConfigCodec.decode(hex"00".bits).require.value === ChannelConfig(Set.empty[ChannelConfigOption])) - assert(channelConfigCodec.decode(hex"01f0".bits).require.value === ChannelConfig(Set.empty[ChannelConfigOption])) - assert(channelConfigCodec.decode(hex"020000".bits).require.value === ChannelConfig(Set.empty[ChannelConfigOption])) - - assert(channelConfigCodec.encode(ChannelConfig.standard).require.bytes === hex"0101") - assert(channelConfigCodec.encode(ChannelConfig(ChannelConfig.FundingPubKeyBasedChannelKeyPath)).require.bytes === hex"0101") - assert(channelConfigCodec.decode(hex"0101".bits).require.value === ChannelConfig(ChannelConfig.FundingPubKeyBasedChannelKeyPath)) - assert(channelConfigCodec.decode(hex"01ff".bits).require.value === ChannelConfig(ChannelConfig.FundingPubKeyBasedChannelKeyPath)) - assert(channelConfigCodec.decode(hex"020001".bits).require.value === ChannelConfig(ChannelConfig.FundingPubKeyBasedChannelKeyPath)) + assert(channelConfigCodec.encode(ChannelConfig(Set.empty[ChannelConfigOption])).require.bytes == hex"00") + assert(channelConfigCodec.decode(hex"00".bits).require.value == ChannelConfig(Set.empty[ChannelConfigOption])) + assert(channelConfigCodec.decode(hex"01f0".bits).require.value == ChannelConfig(Set.empty[ChannelConfigOption])) + assert(channelConfigCodec.decode(hex"020000".bits).require.value == ChannelConfig(Set.empty[ChannelConfigOption])) + + assert(channelConfigCodec.encode(ChannelConfig.standard).require.bytes == hex"0101") + assert(channelConfigCodec.encode(ChannelConfig(ChannelConfig.FundingPubKeyBasedChannelKeyPath)).require.bytes == hex"0101") + assert(channelConfigCodec.decode(hex"0101".bits).require.value == ChannelConfig(ChannelConfig.FundingPubKeyBasedChannelKeyPath)) + assert(channelConfigCodec.decode(hex"01ff".bits).require.value == ChannelConfig(ChannelConfig.FundingPubKeyBasedChannelKeyPath)) + assert(channelConfigCodec.decode(hex"020001".bits).require.value == ChannelConfig(ChannelConfig.FundingPubKeyBasedChannelKeyPath)) } test("decode all known channel configuration options") { @@ -75,7 +75,7 @@ class ChannelCodecs3Spec extends AnyFunSuite { assert(declaredOptions.nonEmpty) val encoded = channelConfigCodec.encode(ChannelConfig(declaredOptions)).require val decoded = channelConfigCodec.decode(encoded).require.value - assert(decoded.options === declaredOptions) + assert(decoded.options == declaredOptions) } test("encode/decode optional shutdown script") { @@ -95,15 +95,15 @@ class ChannelCodecs3Spec extends AnyFunSuite { randomKey().publicKey, Features(ChannelRangeQueries -> Optional, VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory), None) - assert(codec.decodeValue(codec.encode(remoteParams).require).require === remoteParams) + assert(codec.decodeValue(codec.encode(remoteParams).require).require == remoteParams) val remoteParams1 = remoteParams.copy(shutdownScript = Some(ByteVector.fromValidHex("deadbeef"))) - assert(codec.decodeValue(codec.encode(remoteParams1).require).require === remoteParams1) + assert(codec.decodeValue(codec.encode(remoteParams1).require).require == remoteParams1) val dataWithoutRemoteShutdownScript = normal.copy(commitments = normal.commitments.copy(remoteParams = remoteParams)) - assert(DATA_NORMAL_Codec.decode(DATA_NORMAL_Codec.encode(dataWithoutRemoteShutdownScript).require).require.value === dataWithoutRemoteShutdownScript) + assert(DATA_NORMAL_Codec.decode(DATA_NORMAL_Codec.encode(dataWithoutRemoteShutdownScript).require).require.value == dataWithoutRemoteShutdownScript) val dataWithRemoteShutdownScript = normal.copy(commitments = normal.commitments.copy(remoteParams = remoteParams1)) - assert(DATA_NORMAL_Codec.decode(DATA_NORMAL_Codec.encode(dataWithRemoteShutdownScript).require).require.value === dataWithRemoteShutdownScript) + assert(DATA_NORMAL_Codec.decode(DATA_NORMAL_Codec.encode(dataWithRemoteShutdownScript).require).require.value == dataWithRemoteShutdownScript) } test("encode/decode optional channel reserve") { @@ -141,39 +141,39 @@ class ChannelCodecs3Spec extends AnyFunSuite { val remoteCodec = remoteParamsCodec(ChannelFeatures()) val decodedLocalParams = localCodec.decode(localCodec.encode(localParams).require).require.value val decodedRemoteParams = remoteCodec.decode(remoteCodec.encode(remoteParams).require).require.value - assert(decodedLocalParams === localParams) - assert(decodedRemoteParams === remoteParams) + assert(decodedLocalParams == localParams) + assert(decodedRemoteParams == remoteParams) } { val localCodec = localParamsCodec(ChannelFeatures(Features.DualFunding)) val remoteCodec = remoteParamsCodec(ChannelFeatures(Features.DualFunding)) val decodedLocalParams = localCodec.decode(localCodec.encode(localParams).require).require.value val decodedRemoteParams = remoteCodec.decode(remoteCodec.encode(remoteParams).require).require.value - assert(decodedLocalParams === localParams.copy(requestedChannelReserve_opt = None)) - assert(decodedRemoteParams === remoteParams.copy(requestedChannelReserve_opt = None)) + assert(decodedLocalParams == localParams.copy(requestedChannelReserve_opt = None)) + assert(decodedRemoteParams == remoteParams.copy(requestedChannelReserve_opt = None)) } } test("backward compatibility DATA_NORMAL_COMPAT_02_Codec") { val oldBin = hex"00022aed498450b3eb2f6aafedd40640b54efc60ae681da9e6a35299b8b6a6125a7301010003af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d00009d2b17f27b3938b2b50ec713df1b1ae5fd3d23010c9e2e22385f13a168c6acf2c80000001000000000000044c000000001dcd65000000000000002710000000000000000000900064ff1600149d706d0fa71a0b6aa0f3fa400bee18102b45c8170000186b0200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000024982039dc0e0b1d25905e44fdf6f8e89755a5e219685840d0bc1d28d3308f9628a358500000000000003e8ffffffffffffffff0000000000004e2000000000000003e80090001e03fb08866066d5f6f539314220fc31a8e7fdf6ccd12ccde150acc59bac5bb971e003f399d17a9e2fb13a52373d1631807c3161250b2774642fffa629858ad0831f68020b4ecc52820a93f6da40a60d7859307edc737347054d82592959ed1cd0b02e9c03db348203bfd939779bfdd825bc807e904225c3348fa5e3bb57d38ac4b9f40f850284c0df55fbfc2212cbd18cf8ab0eb5283b4b883350ff07e81988e01ae2bb71e20000000302498200000000000000000000000000002710000000002faf0800000000000bebc200242aed498450b3eb2f6aafedd40640b54efc60ae681da9e6a35299b8b6a6125a73000000002b40420f00000000002200203305e10ec90f004675285e9b0371a663ead7abe6caba8c6d5739ace6c9eab4534752210390d6a42ff78a21b41560f75359f0f8a9edaaf0ddcf1a6609130f0d5f234463662103fb08866066d5f6f539314220fc31a8e7fdf6ccd12ccde150acc59bac5bb971e052ae7d02000000012aed498450b3eb2f6aafedd40640b54efc60ae681da9e6a35299b8b6a6125a7300000000000a1e418002400d0300000000001600146862a069e7038573a81f5627ae7d0c6ee5cd0acfb8180c0000000000220020a5f137a8049afcac9f0451b0f31677a81b5443f1c04c1910b37b0d2b8aa4ca0084a4eb2096e400507ac49b57910c914cff8338b8bc57884983541ee1b6919ece7431f4030b09a5fcd87b35682dea0d8faa394e47cc31af897cdf3e6ff502b086cfac37c700000000000000000000000000002710000000000bebc200000000002faf080028131acadf245e7d95d3d7a7f1ac0c0411ead7957ab00d310696b0b5d7d14ac8020597a38e090850030f255fb2781a53713faf7ba81c44de931a63a78efd9908ef000000000000000000000000000000000000000000000000000000000000ff035878c87f6ed100476648193e10a1462bfb55cea3ec5a8f4fbd0fe7304979094b242aed498450b3eb2f6aafedd40640b54efc60ae681da9e6a35299b8b6a6125a73000000002b40420f00000000002200203305e10ec90f004675285e9b0371a663ead7abe6caba8c6d5739ace6c9eab4534752210390d6a42ff78a21b41560f75359f0f8a9edaaf0ddcf1a6609130f0d5f234463662103fb08866066d5f6f539314220fc31a8e7fdf6ccd12ccde150acc59bac5bb971e052ae000000061a8000002a0000000088ec7ae2af533c270809b48f9a0b5a9650df9961a2177e04d7e9929ab319fcd0d150e3ded703b8b4a3f5faff0f2fedf22a6729760deefdea4feed868e4e3cdcf1b06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f061a8000002a000061163fb60101009000000000000003e8000858b800000014000000003b9aca000000" val decoded1 = channelDataCodec.decode(oldBin.bits).require.value - assert(decoded1.asInstanceOf[DATA_NORMAL].closingFeerates === None) + assert(decoded1.asInstanceOf[DATA_NORMAL].closingFeerates == None) val newBin = channelDataCodec.encode(decoded1).require.bytes // make sure that encoding used the new codec assert(newBin.startsWith(hex"0007")) val decoded2 = channelDataCodec.decode(newBin.bits).require.value - assert(decoded1 === decoded2) + assert(decoded1 == decoded2) } test("backward compatibility DATA_SHUTDOWN_COMPAT_03_Codec") { val oldBin = hex"0003342b8fd35a1f3384c6db37723ecc766b672f61b4b0f2e7d5d81cf1e451b584d301010003af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d00009f2408e50889ac3f6c19a007e25752bf143ab0d920cedb9f1c7b7b6b2270c1fe080000001000000000000044c000000001dcd65000000000000002710000000000000000000900064ff160014d0cd80743458194e5f86d9a74592765442fd15bb0000186b0200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000024982039dc0e0b1d25905e44fdf6f8e89755a5e219685840d0bc1d28d3308f9628a358500000000000003e8ffffffffffffffff0000000000004e2000000000000003e80090001e03d1a9a2a574585f60abaaa33826488f9eb8d53ca76693bfa69b42ee2bc8ebe9c50223dc803cceb3edff5deacc9965002cc366b719331e776abf76ee680e4afbf46a0315880bf27110fa57f5b75607bc32a098c209fe8a5a2a30120b2a246fd3fcd62e025dc435be4cb790e5ab12705fb3c0b717ddd9f4d0234fdb557358ecefc6a4e97a0238e3723a9d60dac60c2673ec836e2a909ba9fc888fcbd1cd2ad57ff0b10c5c360000000302498200000000000000000001000200fd05aa342b8fd35a1f3384c6db37723ecc766b672f61b4b0f2e7d5d81cf1e451b584d300000000000000000000000011e1a300d91ec390338376481ff3aea604bf2106cf380d78e946e6a4fc0ad72dc0dec51b00061b100002909a15094ba406ca280bce032eab4d25eae4a9e8b51ff65574b6de2033a292cb1bc6ef576207f4b585273048502efad58af480f08dabcbff1a9642eb3b08811eb4417ac14bf6e5e3883d55ba3b7fdaf27e5ebac16ce63d183b2521867f6cedcf55692f1490da9bc655e38281a26150c44d56e02013faf1cdb2457bec4830e67c10ff0d731798d9e383cc3ce9ba21bddd8971e1d206c54c22317d75411b73abb08c9af348cca2aa2f17232b008aaef4bedb98c426041d3698924d089b59c639396ba38d2ba6d59ea2b0ee8ec9f548deced719e9aae73854d8b6c10c78890999ae09b912a8ae98d3513119bfa0ffdd3037cf5690122020f2c0d01b282ad0cbe345bd31cf7e55d8e78c61abfa62f97d2a095e2256c6569d3e6b5652b365d935d16451f2cf7df04e99e675d9eb649b583fc5d810dae3860001e667b9b78733b3ed6ca5058e6806ca4eb493c5c9a53b0ad78b9ea2091d6e94130afb1d79ce8e274a93d748af9d4ee7ae5b1c06cf1f65507bc79db22c30ae6df93a16c279e4beaf50a0e4e87449e9c830fe9b3877b7332b0632743d4e6267ffa404a35d74d79c5d724bdc9aaaa0423a157fb269b916b0022fd52bb0b2702e6abcc7d9d39d7f69a09a9814b3110cc72574fed031bbc0fd5a03fcee1181076369e76c183b2833385a38dd10773fe3228cb18aa731674a574d310a76052cf89c80077aa89380fcfdeda00508f79261c1e1886240de3bbc343eb84ea7879de24f7dbd0b1527201131bcaf5621e49c69ceec36280f9db6d27161c1676c9b11597d4bd8f10032a964fc1d1afcea594237f3408b184b16eba952778252bc6debc30e6e595c7ebbc2af0c00456ca771d024a56d9f074ca5a7f78f1ef107aa42c2a6426f8dbb2ea8962e2128cfc15161512d2e923e2879635c8ba80bc17f92d09abfae4e08a7f34decc1acd4aa4cc08181017bf248321558a8c4d1a45b92b96274dd9f336eb45d8af6d13147be3a250c17106ec6729e0cffc1af5255352296477dea870426aa82e7af3cc07156b65f8d98b482985618b69b99ee3871240db751923dbeddea6e36f5c3f0599300973fe064ccf4f15cadb7372aaab5b300674f5bf28ce905ab411f5605fba7075ed4a6b08dca3ed5867dc32e491c64e7f09ff4095081e0731cbfc53fa42c33e79bd1a26d8e9f8c5b546b07bd1b300f1f357919e955bded1f987401f4da1a453282b6978ff3bfd13b280f6253dd968cb4e7741a15faf7b4c70dbd9eab6461ca3d3a576410213bc4e144a63685488afeeb7857af4ef83e73c88b17798616a9ccbd603d61ec8f4dbbdd5fea5ca748cf40c38fec50d4eebf1e5b57fd32500baa892da24349598c3125162b2ea0ea049ea9aacdfa848f41fdf7f9eb983776bac58ddf762d131196ab18774349a5eca2fc5b51b60d7aceae1321c1decd462f9c7dbbd820399f71a3c3ccbb30a29f76cd8e94243e400c4dc6e6d32b6f94155a21eb89b6b4c0fd1660bd98509d11b3a45fb257ae1e3c5ebae7bae038200c0f7f2d4ee744c3f2d9007d6fb282b0254ccd71eae5b4c4996b52edb2f2f4120356096c81e42168ee43d98bd630b0af01c270db3fb680cd931d388e1c1260126707548ed1f27006a64db7885ac7500ce020f6349a1abf59fb3ca4bc5bdcbd19b7a0cfe82aeea62f0373d5c16002cd9adcb3ef5e0c862e2fda972530b399b0f6365f4edbc36a32bd1766763104aca34450f278418211d62ff3fff2d0de7f516028011e5681ee892288835a3ec6d0d353f8bc7168086a0eed30d5e81d8a51f7debbb2290dc1a1745e0fc2c08778d11ccb78dc7c98a8b4a4b28fd8decda3a2063ad73c23c112d609b6903613c5ad8e178c90fed2ebd10b176b17c9c2dfc630ef744ac289b437ad229b77a393c6b05c10f36ba2f80482ed6e26b3d9c34d2200fd05aa342b8fd35a1f3384c6db37723ecc766b672f61b4b0f2e7d5d81cf1e451b584d30000000000000001000000000bebc20092ebfcedc573c52ba840ff42bb322419ed390dc845c2fe80636b6b82cf41210000061b100002f211b0d97e6c7f7fb800dbb8d1c7536ad7ef284c40a85fc7316b771bb525c91f7e1d4bba90df0cf0d12c19906e4ae0a1b83954632b59f5f499dedaeba19d75c3bc244ccb57089abeaf29c7d73d67ae822aea2ed4d85d27dc65042589e8172aa3b66d928d7e35f027d96bf23e656701bdf13651e25bed745fef014fdec3faaba4795638299975d3cdc8d2c5b19e2e411e6e4f7b3cede7e269c8902306d25b1027e99736489fde667f4eaf5726349851986ff15a9e967385e080079a14c0a9c3001bbce5da720dcf739e67801cfe2a565b24f5515f8d8889a6d2dc0b51fae4facb72904ba8c1949550332cd8b9095655da72a9815537a80efd204d497af47fd18ef0f6d0639424eb81dab0e34f228aed9406d23773874b4c41d9a21dcaf632419e459baaca5f338f0f9718c89fb72108788d32aeb07e2eb343c45891958779e75fe0039eddaf7e5c4aa8ac2aee9928c68dc529d415780d8e14adcecc91b142b238e7657ef1dc6b6e9843bb5fbe8d04564307e515d88b34105b90b3fa9ed91025297a373507cba86160ba5b7b569948c95fd483b28b8970bebd5e17fcad76c6ed64615cc2d7297994483d94c010a210ad3602b7f359d7d1391b97968f9eaa70de83ea1168fdb05190a6e9fc7c984213da3e2a6672d09704d8e29e2aa9349c9ae44eca3dc0522c769887cbaa0926c5bad64eaf7c22399f7abcc01505d921cce15f4613406956715fe3030ab5c74506d1acc30649b596931316a8e210a368d758d7c1f406f5888499f8317a9ba629dfe85453b4413107796bde15636e6f628078863c0009db7d97cfadc2bfdcdfed237debac33dc525f25865663bc7a812099102fe9c65c8a37b179337141c8d76f365824b0db54b0b18d6c88b21406a1e35061fb4f5ef8a280f9c2ac2ada9eea8cb365d346c5e9eaa56728584aa71c4296a1c11d76f03351a91a030ae6df1da008a5d0f92207b8ca22f335b5a347707ef2500119597d5b92c559f477d8405f1d0dfd790c99ca5a7b4aeac232224ea8461ed07636931e65fee7b063d0ef49f471c04e2fd45b2fff3fb6923314e26045259640335c417ed14a0799553dda3eff5065ec5606303c42309221881c7dea17227da4003a6e396e308ca77da3b4b743a1c2771252c2fa8caad98c9985f9157dbcdd355d3a70b2edeca9682e4bba3e7da24fdd472017b89103e61ab9a4bcc9ab75bd28f7fad29327bf710b3b4759bb76983b003a0907794ad73194d14a70769ce6731b0ae21d565b53780275b6ac42c650d47f570331c7ac57fc75390d3b232eece8277a7c8453b5f28206ff1026879981f2ac83a0d4760554401e671a110de6c03cc104927c55de7254311680af4fb5d36b2199e1348f6fa50877d6cb163dd5f70381f2418f20d145d8ed5df2714cdcda59a4362b0c544ee1af01ac41bc15729951b1e53f3a135fe9011d3e938b08c50d53da259e2ab19b97a47b96d6b7fd559704ac9d1d8a223cbb88c9f77fa563c0c66524c260f0ff54471acae87e71c0507b14edf02bd42993966eea6b2f5d36db12b7fec7a63b80a13651822867db1f046081ad2cf3676b88fa2ef700b8c865a7dedcdee739795853584b74c3f59868daf79e81bf70afed8ea1ce689d67090a32dfe351fd0bdaf60b3903d7ff7c149134a95b2ddf1d63e0f89dfe0b710accf2ca3c37647973532338c59d32e562d7c55573277c1fab09d57a452f4214131178683533fbfe7726276903aacd579d36c6679187b1d22dda32fc5302b91244840a54c258e4d416deb0d20f96b214c335608db6b725175d8517f35f9d6b466c751a1253b20051863a02f2e0c073830cb7de27c96c4ccb196e079edc817ebdf70972b917b6a07c6a431567b99c6072b102e5103a22908683f60bb4fb72538ae3a3768de85f9fc682e67c6aea0e000027100000000011e1a300000000000bebc20024342b8fd35a1f3384c6db37723ecc766b672f61b4b0f2e7d5d81cf1e451b584d3000000002b40420f000000000022002091a342be9a9171a40f83766fbf34a0be03b79675dd20c14e13d91651f7f8273e47522103a1bd05e59ffa63f010cee2ff6848730c6f718da57aef6a5df381683d409a815e2103d1a9a2a574585f60abaaa33826488f9eb8d53ca76693bfa69b42ee2bc8ebe9c552aed30200000001342b8fd35a1f3384c6db37723ecc766b672f61b4b0f2e7d5d81cf1e451b584d300000000007d30f88004400d030000000000160014dab249c7d3b8d826d45244f609bb90f72e691daa400d030000000000220020d24765dcd888e08581c2add26ddfaa6f3ef06b56578373c59915bcc25aa0c6ee286a04000000000022002054d7877901aedaf54d84b20a4233107c2fdab9ff637106d44097e69a813d394ee093040000000000220020039e4d3b414381fb8fda640b122e604b20aa6f64e87607dd9354cdc636cb483d1b17b4207914784cfd82df8f93f15defff862cede27811ce4c59df98e86fd57e5b5105921e842a02a47ee6ddb313b37bc0b6706fe3011e2dd2d82066c85ab11a8d7b4e47000202241ad33e0af0717b31e19b50fd6b0469a42807eec1a080ad29cd56d04a4de54313010000002b400d030000000000220020d24765dcd888e08581c2add26ddfaa6f3ef06b56578373c59915bcc25aa0c6ee8576a91474e49ec772064db47a555b032479b20992beeefe8763ac672102cd66c9b36fdd627754f492f15bc2387f44fd70bc4c9ccdb08df37e25250a5b197c820120876475527c2103d80a13db0dffe34a9b1ff383306a52aed0a40a99ce1c34b113ca7467327f608452ae67a9142a784c1850f79298eab62c5c7709a5d468f5522788ac68685e02000000011ad33e0af0717b31e19b50fd6b0469a42807eec1a080ad29cd56d04a4de54313010000000000000000015af302000000000022002054d7877901aedaf54d84b20a4233107c2fdab9ff637106d44097e69a813d394e101b06000000000000000001f8c46e9fa497a4f9f4fdf422840d264f67178defb43320b83c1d9825a6c152995775652ba3689025f4841f165acbd16732ad88629429b3811d71c7b03adf254402241ad33e0af0717b31e19b50fd6b0469a42807eec1a080ad29cd56d04a4de54313030000002be093040000000000220020039e4d3b414381fb8fda640b122e604b20aa6f64e87607dd9354cdc636cb483d8576a91474e49ec772064db47a555b032479b20992beeefe8763ac672102cd66c9b36fdd627754f492f15bc2387f44fd70bc4c9ccdb08df37e25250a5b197c820120876475527c2103d80a13db0dffe34a9b1ff383306a52aed0a40a99ce1c34b113ca7467327f608452ae67a91448d436723875e511f3a4fc540ca63c91637e926388ac68685e02000000011ad33e0af0717b31e19b50fd6b0469a42807eec1a080ad29cd56d04a4de5431303000000000000000001fa7904000000000022002054d7877901aedaf54d84b20a4233107c2fdab9ff637106d44097e69a813d394e101b06000000000000000000ce718c1999b28ddd1207bcdd6789541ec2f405f775efb096bd896bb237b7f5c17543684f99cb53efeade77bc386997e7b5361418c510ed442700cee38c36f18900000000000000010002fffd05aa342b8fd35a1f3384c6db37723ecc766b672f61b4b0f2e7d5d81cf1e451b584d300000000000000000000000011e1a300d91ec390338376481ff3aea604bf2106cf380d78e946e6a4fc0ad72dc0dec51b00061b100002909a15094ba406ca280bce032eab4d25eae4a9e8b51ff65574b6de2033a292cb1bc6ef576207f4b585273048502efad58af480f08dabcbff1a9642eb3b08811eb4417ac14bf6e5e3883d55ba3b7fdaf27e5ebac16ce63d183b2521867f6cedcf55692f1490da9bc655e38281a26150c44d56e02013faf1cdb2457bec4830e67c10ff0d731798d9e383cc3ce9ba21bddd8971e1d206c54c22317d75411b73abb08c9af348cca2aa2f17232b008aaef4bedb98c426041d3698924d089b59c639396ba38d2ba6d59ea2b0ee8ec9f548deced719e9aae73854d8b6c10c78890999ae09b912a8ae98d3513119bfa0ffdd3037cf5690122020f2c0d01b282ad0cbe345bd31cf7e55d8e78c61abfa62f97d2a095e2256c6569d3e6b5652b365d935d16451f2cf7df04e99e675d9eb649b583fc5d810dae3860001e667b9b78733b3ed6ca5058e6806ca4eb493c5c9a53b0ad78b9ea2091d6e94130afb1d79ce8e274a93d748af9d4ee7ae5b1c06cf1f65507bc79db22c30ae6df93a16c279e4beaf50a0e4e87449e9c830fe9b3877b7332b0632743d4e6267ffa404a35d74d79c5d724bdc9aaaa0423a157fb269b916b0022fd52bb0b2702e6abcc7d9d39d7f69a09a9814b3110cc72574fed031bbc0fd5a03fcee1181076369e76c183b2833385a38dd10773fe3228cb18aa731674a574d310a76052cf89c80077aa89380fcfdeda00508f79261c1e1886240de3bbc343eb84ea7879de24f7dbd0b1527201131bcaf5621e49c69ceec36280f9db6d27161c1676c9b11597d4bd8f10032a964fc1d1afcea594237f3408b184b16eba952778252bc6debc30e6e595c7ebbc2af0c00456ca771d024a56d9f074ca5a7f78f1ef107aa42c2a6426f8dbb2ea8962e2128cfc15161512d2e923e2879635c8ba80bc17f92d09abfae4e08a7f34decc1acd4aa4cc08181017bf248321558a8c4d1a45b92b96274dd9f336eb45d8af6d13147be3a250c17106ec6729e0cffc1af5255352296477dea870426aa82e7af3cc07156b65f8d98b482985618b69b99ee3871240db751923dbeddea6e36f5c3f0599300973fe064ccf4f15cadb7372aaab5b300674f5bf28ce905ab411f5605fba7075ed4a6b08dca3ed5867dc32e491c64e7f09ff4095081e0731cbfc53fa42c33e79bd1a26d8e9f8c5b546b07bd1b300f1f357919e955bded1f987401f4da1a453282b6978ff3bfd13b280f6253dd968cb4e7741a15faf7b4c70dbd9eab6461ca3d3a576410213bc4e144a63685488afeeb7857af4ef83e73c88b17798616a9ccbd603d61ec8f4dbbdd5fea5ca748cf40c38fec50d4eebf1e5b57fd32500baa892da24349598c3125162b2ea0ea049ea9aacdfa848f41fdf7f9eb983776bac58ddf762d131196ab18774349a5eca2fc5b51b60d7aceae1321c1decd462f9c7dbbd820399f71a3c3ccbb30a29f76cd8e94243e400c4dc6e6d32b6f94155a21eb89b6b4c0fd1660bd98509d11b3a45fb257ae1e3c5ebae7bae038200c0f7f2d4ee744c3f2d9007d6fb282b0254ccd71eae5b4c4996b52edb2f2f4120356096c81e42168ee43d98bd630b0af01c270db3fb680cd931d388e1c1260126707548ed1f27006a64db7885ac7500ce020f6349a1abf59fb3ca4bc5bdcbd19b7a0cfe82aeea62f0373d5c16002cd9adcb3ef5e0c862e2fda972530b399b0f6365f4edbc36a32bd1766763104aca34450f278418211d62ff3fff2d0de7f516028011e5681ee892288835a3ec6d0d353f8bc7168086a0eed30d5e81d8a51f7debbb2290dc1a1745e0fc2c08778d11ccb78dc7c98a8b4a4b28fd8decda3a2063ad73c23c112d609b6903613c5ad8e178c90fed2ebd10b176b17c9c2dfc630ef744ac289b437ad229b77a393c6b05c10f36ba2f80482ed6e26b3d9c34d22fffd05aa342b8fd35a1f3384c6db37723ecc766b672f61b4b0f2e7d5d81cf1e451b584d30000000000000001000000000bebc20092ebfcedc573c52ba840ff42bb322419ed390dc845c2fe80636b6b82cf41210000061b100002f211b0d97e6c7f7fb800dbb8d1c7536ad7ef284c40a85fc7316b771bb525c91f7e1d4bba90df0cf0d12c19906e4ae0a1b83954632b59f5f499dedaeba19d75c3bc244ccb57089abeaf29c7d73d67ae822aea2ed4d85d27dc65042589e8172aa3b66d928d7e35f027d96bf23e656701bdf13651e25bed745fef014fdec3faaba4795638299975d3cdc8d2c5b19e2e411e6e4f7b3cede7e269c8902306d25b1027e99736489fde667f4eaf5726349851986ff15a9e967385e080079a14c0a9c3001bbce5da720dcf739e67801cfe2a565b24f5515f8d8889a6d2dc0b51fae4facb72904ba8c1949550332cd8b9095655da72a9815537a80efd204d497af47fd18ef0f6d0639424eb81dab0e34f228aed9406d23773874b4c41d9a21dcaf632419e459baaca5f338f0f9718c89fb72108788d32aeb07e2eb343c45891958779e75fe0039eddaf7e5c4aa8ac2aee9928c68dc529d415780d8e14adcecc91b142b238e7657ef1dc6b6e9843bb5fbe8d04564307e515d88b34105b90b3fa9ed91025297a373507cba86160ba5b7b569948c95fd483b28b8970bebd5e17fcad76c6ed64615cc2d7297994483d94c010a210ad3602b7f359d7d1391b97968f9eaa70de83ea1168fdb05190a6e9fc7c984213da3e2a6672d09704d8e29e2aa9349c9ae44eca3dc0522c769887cbaa0926c5bad64eaf7c22399f7abcc01505d921cce15f4613406956715fe3030ab5c74506d1acc30649b596931316a8e210a368d758d7c1f406f5888499f8317a9ba629dfe85453b4413107796bde15636e6f628078863c0009db7d97cfadc2bfdcdfed237debac33dc525f25865663bc7a812099102fe9c65c8a37b179337141c8d76f365824b0db54b0b18d6c88b21406a1e35061fb4f5ef8a280f9c2ac2ada9eea8cb365d346c5e9eaa56728584aa71c4296a1c11d76f03351a91a030ae6df1da008a5d0f92207b8ca22f335b5a347707ef2500119597d5b92c559f477d8405f1d0dfd790c99ca5a7b4aeac232224ea8461ed07636931e65fee7b063d0ef49f471c04e2fd45b2fff3fb6923314e26045259640335c417ed14a0799553dda3eff5065ec5606303c42309221881c7dea17227da4003a6e396e308ca77da3b4b743a1c2771252c2fa8caad98c9985f9157dbcdd355d3a70b2edeca9682e4bba3e7da24fdd472017b89103e61ab9a4bcc9ab75bd28f7fad29327bf710b3b4759bb76983b003a0907794ad73194d14a70769ce6731b0ae21d565b53780275b6ac42c650d47f570331c7ac57fc75390d3b232eece8277a7c8453b5f28206ff1026879981f2ac83a0d4760554401e671a110de6c03cc104927c55de7254311680af4fb5d36b2199e1348f6fa50877d6cb163dd5f70381f2418f20d145d8ed5df2714cdcda59a4362b0c544ee1af01ac41bc15729951b1e53f3a135fe9011d3e938b08c50d53da259e2ab19b97a47b96d6b7fd559704ac9d1d8a223cbb88c9f77fa563c0c66524c260f0ff54471acae87e71c0507b14edf02bd42993966eea6b2f5d36db12b7fec7a63b80a13651822867db1f046081ad2cf3676b88fa2ef700b8c865a7dedcdee739795853584b74c3f59868daf79e81bf70afed8ea1ce689d67090a32dfe351fd0bdaf60b3903d7ff7c149134a95b2ddf1d63e0f89dfe0b710accf2ca3c37647973532338c59d32e562d7c55573277c1fab09d57a452f4214131178683533fbfe7726276903aacd579d36c6679187b1d22dda32fc5302b91244840a54c258e4d416deb0d20f96b214c335608db6b725175d8517f35f9d6b466c751a1253b20051863a02f2e0c073830cb7de27c96c4ccb196e079edc817ebdf70972b917b6a07c6a431567b99c6072b102e5103a22908683f60bb4fb72538ae3a3768de85f9fc682e67c6aea0e00002710000000000bebc2000000000011e1a300306acd0b21100f755a6d193a819042fde1b4c3839598e730905dfa7e6ef699220240a9cccdb58d887b42ccadeee5704030acd52bd9c7759ba01f47f0995531ca360000000000000000000000000000000000000002000000000000000000020000000000000000000334c7b350311d40b3b422e07f3b7759b400000000000000010003e3dd409b342046e7b410cc380c9a09c2ff03cdeeaf422678a9eac407da8f811a402718083016f88d5404f5508054fe25acb824342b8fd35a1f3384c6db37723ecc766b672f61b4b0f2e7d5d81cf1e451b584d3000000002b40420f000000000022002091a342be9a9171a40f83766fbf34a0be03b79675dd20c14e13d91651f7f8273e47522103a1bd05e59ffa63f010cee2ff6848730c6f718da57aef6a5df381683d409a815e2103d1a9a2a574585f60abaaa33826488f9eb8d53ca76693bfa69b42ee2bc8ebe9c552ae000100400000ffffffffffff00204cec06f1f1496c01cd4840cddff19b47c24b7daa478bcaea862c6d11fcc9fff180007fffffffffff8038342b8fd35a1f3384c6db37723ecc766b672f61b4b0f2e7d5d81cf1e451b584d300160014d0cd80743458194e5f86d9a74592765442fd15bb38342b8fd35a1f3384c6db37723ecc766b672f61b4b0f2e7d5d81cf1e451b584d30016001483366b494906052362a88e967304296937926cd1" val decoded1 = channelDataCodec.decode(oldBin.bits).require.value - assert(decoded1.asInstanceOf[DATA_SHUTDOWN].closingFeerates === None) + assert(decoded1.asInstanceOf[DATA_SHUTDOWN].closingFeerates == None) val newBin = channelDataCodec.encode(decoded1).require.bytes // make sure that encoding used the new codec assert(newBin.startsWith(hex"0008")) val decoded2 = channelDataCodec.decode(newBin.bits).require.value - assert(decoded1 === decoded2) + assert(decoded1 == decoded2) } test("backwards compatibility with legacy claim-htlc-success transactions") { @@ -183,7 +183,7 @@ class ChannelCodecs3Spec extends AnyFunSuite { val legacyClaimHtlcSuccess = { val claimHtlcSuccessTx1 = txWithInputInfoCodec.decode(oldTxWithInputInfoCodecBin.bits).require.value val claimHtlcSuccessTx2 = claimHtlcTxCodec.decode(oldClaimHtlcTxBin.bits).require.value - assert(claimHtlcSuccessTx1 === claimHtlcSuccessTx2) + assert(claimHtlcSuccessTx1 == claimHtlcSuccessTx2) assert(claimHtlcSuccessTx1.isInstanceOf[LegacyClaimHtlcSuccessTx]) claimHtlcSuccessTx1.asInstanceOf[LegacyClaimHtlcSuccessTx] } @@ -197,9 +197,9 @@ class ChannelCodecs3Spec extends AnyFunSuite { // And decode new data that contains a payment hash. val decoded1 = txWithInputInfoCodec.decode(txWithInputInfoCodecBin.bits).require.value - assert(claimHtlcSuccess === decoded1) + assert(claimHtlcSuccess == decoded1) val decoded2 = claimHtlcTxCodec.decode(claimHtlcTxBin.bits).require.value - assert(claimHtlcSuccess === decoded2) + assert(claimHtlcSuccess == decoded2) } test("backwards compatibility with transactions missing a confirmation target") { @@ -207,46 +207,46 @@ class ChannelCodecs3Spec extends AnyFunSuite { val oldAnchorTxBin = hex"0011 24bd0be30e31c748c7afdde7d2c527d711fadf88500971a4a1136bca375dba07b8000000002b4a0100000000000022002036c067df8952dbcd5db347e7c152ca3fa4514f2072d27867837b1c2d319a7e01282103cc89f1459b5201cda08e08c6fb7b1968c54e8172c555896da27c6fdc10522ceeac736460b268330200000001bd0be30e31c748c7afdde7d2c527d711fadf88500971a4a1136bca375dba07b80000000000000000000000000000" val oldAnchorTx = txWithInputInfoCodec.decode(oldAnchorTxBin.bits).require.value assert(oldAnchorTx.isInstanceOf[ClaimLocalAnchorOutputTx]) - assert(oldAnchorTx.asInstanceOf[ClaimLocalAnchorOutputTx].confirmBefore === BlockHeight(0)) + assert(oldAnchorTx.asInstanceOf[ClaimLocalAnchorOutputTx].confirmBefore == BlockHeight(0)) val anchorTx = oldAnchorTx.asInstanceOf[ClaimLocalAnchorOutputTx].copy(confirmBefore = BlockHeight(1105)) val anchorTx2 = txWithInputInfoCodec.decode(txWithInputInfoCodec.encode(anchorTx).require).require.value - assert(anchorTx === anchorTx2) + assert(anchorTx == anchorTx2) } { val oldHtlcSuccessTxBin = hex"0002 24f5580de0577271dce09d2de26e19ec58bf2373b0171473291a8f8be9b04fb289000000002bb0ad010000000000220020462cf8912ffc5f27764c109bed188950500011a2837ff8b9c8f9a39cffa395a58b76a91406b0950d9feded82239b3e6c9082308900f389de8763ac672102d65aa07658a7214ff129f91a1a22ade2ea4d1b07cc14b2f85a2842c34240836f7c8201208763a914c461c897e2165c7e44e14850dfcfd68f99127aed88527c21033c8d41cfbe1511a909b63fed68e75a29c3ce30418c39bbb8294c8b36c6a6c16a52ae677503101b06b175ac6868fd01a002000000000101f5580de0577271dce09d2de26e19ec58bf2373b0171473291a8f8be9b04fb289000000000000000000013a920100000000002200208742b16c9fd4e74854dcd84322dd1de06f7993fe627fd2ca0be4b996a936d56b050047304402201b4527c8f420852550af00bbd9149db9b31adcb7e1f127766e75e1e01746df0302202a57bb1e274ed7d3e8dbe5f205de721a23092c1e2ce2135f4750f18f6c0b51b001483045022100b6df309c8e5746a077b1f7c2f299528e164946bd514a5049475af7f5665805da0220392ae877112a3c52f74d190b354b4f5c020da9c1a71a7a08ced0a5363e795a27012017ea8f5afde8f708258d5669e1bbd454e82ddca8c6c480ec5302b4b1e8051d3d8b76a91406b0950d9feded82239b3e6c9082308900f389de8763ac672102d65aa07658a7214ff129f91a1a22ade2ea4d1b07cc14b2f85a2842c34240836f7c8201208763a914c461c897e2165c7e44e14850dfcfd68f99127aed88527c21033c8d41cfbe1511a909b63fed68e75a29c3ce30418c39bbb8294c8b36c6a6c16a52ae677503101b06b175ac686800000000dc7002a387673f17ebaf08545ccec712a9b6914813cdb83b4270932294f20f660000000000000000" val oldHtlcSuccessTx = txWithInputInfoCodec.decode(oldHtlcSuccessTxBin.bits).require.value assert(oldHtlcSuccessTx.isInstanceOf[HtlcSuccessTx]) - assert(oldHtlcSuccessTx.asInstanceOf[HtlcSuccessTx].confirmBefore === BlockHeight(0)) + assert(oldHtlcSuccessTx.asInstanceOf[HtlcSuccessTx].confirmBefore == BlockHeight(0)) val htlcSuccessTx = oldHtlcSuccessTx.asInstanceOf[HtlcSuccessTx].copy(confirmBefore = BlockHeight(1105)) val htlcSuccessTx2 = txWithInputInfoCodec.decode(txWithInputInfoCodec.encode(htlcSuccessTx).require).require.value - assert(htlcSuccessTx === htlcSuccessTx2) + assert(htlcSuccessTx == htlcSuccessTx2) } { val oldHtlcTimeoutTxBin = hex"0003 248f0619a4b2a351977b3e5b0ddd700482e1d697d40deea2dd7356df99345d51d0000000002b50c300000000000022002005ff644937d7f5f32ec194424f551371e8d4bcf2cda3e1096cdd2fe88687fc408576a914c98707b6420ef3454f3bd10d663adcc04452baea8763ac672102fb20469287c8ade948011bd001440d74633d5e1a98574e4783dd38b76509d8f67c820120876475527c210279d3a1c2086a0968404a0160c8f8c6f88c0ce7184022bb7406f98fdb503ea51452ae67a9140550b2b1621e788d795fe3ae308e7dec06a6a1e088ac6868fd0179020000000001018f0619a4b2a351977b3e5b0ddd700482e1d697d40deea2dd7356df99345d51d0000000000000000000016aa9000000000000220020c980a1573ce6dc6a1bb8a1d60ecffdf2f0a7aa983c49786a2ab1ba8f0cd74a76050047304402200d2b631fb0e5a7f406f3de2b36fe3c0ab3337fe98f114dadf38de8f5548e87eb02203ad7c385c7cf62ac17ec15329dee071ee2c479aceb34a14d700dfeba80008574014730440220653b4ff490b03de06a9053aa7ab0b30e85c484c76900c586db65f7308f38ad86022019ac12ffb127e4a17a01dc6db730d3eccea837d2dc85a0275bdea8b393a2f11d01008576a914c98707b6420ef3454f3bd10d663adcc04452baea8763ac672102fb20469287c8ade948011bd001440d74633d5e1a98574e4783dd38b76509d8f67c820120876475527c210279d3a1c2086a0968404a0160c8f8c6f88c0ce7184022bb7406f98fdb503ea51452ae67a9140550b2b1621e788d795fe3ae308e7dec06a6a1e088ac6868101b06000000000000000003" val oldHtlcTimeoutTx = txWithInputInfoCodec.decode(oldHtlcTimeoutTxBin.bits).require.value assert(oldHtlcTimeoutTx.isInstanceOf[HtlcTimeoutTx]) - assert(oldHtlcTimeoutTx.asInstanceOf[HtlcTimeoutTx].confirmBefore === BlockHeight(0)) + assert(oldHtlcTimeoutTx.asInstanceOf[HtlcTimeoutTx].confirmBefore == BlockHeight(0)) val htlcTimeoutTx = oldHtlcTimeoutTx.asInstanceOf[HtlcTimeoutTx].copy(confirmBefore = BlockHeight(1105)) val htlcTimeoutTx2 = txWithInputInfoCodec.decode(txWithInputInfoCodec.encode(htlcTimeoutTx).require).require.value - assert(htlcTimeoutTx === htlcTimeoutTx2) + assert(htlcTimeoutTx == htlcTimeoutTx2) } { val oldClaimHtlcSuccessTxBin = hex"0016 24e75b5236d1cdd482a6f540d5f08b9aa27b74a9ecae6e2622a67110b3ee1b3d89000000002bb0ad01000000000022002063e22369052a2bad9eb124737742690b8d1aba7693869d041da16443e2973e638576a91494957f4639ebc6f8a30e126552aff8429174dfb18763ac672102e1aa04ff55771238012edb958e6e0525af0415a01d52dd8d5f69fb391e586adc7c820120876475527c2102384e785d34b3b1fe35d3c093a750b234fbc79d8316c149e7845929f628a5baa052ae67a9145e3be49c9ace2d9eab11dc5fc29e40cd4148262e88ac6868fd014502000000000101e75b5236d1cdd482a6f540d5f08b9aa27b74a9ecae6e2622a67110b3ee1b3d890000000000000000000162970100000000001600140262586eef1a2c8f47ebc139a2733123d09e315603483045022100898f6b51361f044c8c54468dcb1b9decd75edcc1b69b62e584059b512eb075900220675f8b23aa402bb7d5bfdefbac4074088f82bd34d09b34c651c0dff48e6967930120efb38d645311af59c028cd8a9bf8ee21ff9e7c1a1cff1a0398a0315280247ac38576a91494957f4639ebc6f8a30e126552aff8429174dfb18763ac672102e1aa04ff55771238012edb958e6e0525af0415a01d52dd8d5f69fb391e586adc7c820120876475527c2102384e785d34b3b1fe35d3c093a750b234fbc79d8316c149e7845929f628a5baa052ae67a9145e3be49c9ace2d9eab11dc5fc29e40cd4148262e88ac686800000000ad02394dd18774f6f403f783afb518b6c69a89d531f718b29868ddca3e7905020000000000000000" val oldClaimHtlcSuccessTx = txWithInputInfoCodec.decode(oldClaimHtlcSuccessTxBin.bits).require.value assert(oldClaimHtlcSuccessTx.isInstanceOf[ClaimHtlcSuccessTx]) - assert(oldClaimHtlcSuccessTx.asInstanceOf[ClaimHtlcSuccessTx].confirmBefore === BlockHeight(0)) + assert(oldClaimHtlcSuccessTx.asInstanceOf[ClaimHtlcSuccessTx].confirmBefore == BlockHeight(0)) val claimHtlcSuccessTx = oldClaimHtlcSuccessTx.asInstanceOf[ClaimHtlcSuccessTx].copy(confirmBefore = BlockHeight(1105)) val claimHtlcSuccessTx2 = txWithInputInfoCodec.decode(txWithInputInfoCodec.encode(claimHtlcSuccessTx).require).require.value - assert(claimHtlcSuccessTx === claimHtlcSuccessTx2) + assert(claimHtlcSuccessTx == claimHtlcSuccessTx2) } { val oldClaimHtlcTimeoutTxBin = hex"0005 24df6aa4cd4e8877e4b3a363d6180951036d6f18ef214dabd50a6c05a60077d4d8000000002b50c30000000000002200205db892d76fb358ed508ec09c77b5a184cd9de3aa3e74a025a5c5d3f7adc221f78b76a914e0c241e0656088953f84475cbe5c70ded12e05b58763ac6721028876f3e23b21e07f889f10fc2aa0875b96021359a06d0b7f52a79fc284f6b2837c8201208763a9144ae5c96e8b7495fd35a5ca5681aa7c8f4ab6bc9b88527c2102b4d398ee7a42e87012de5a76832d9ebfa8532ef267490a7f3fe75b2c2e18cc9152ae677503981a06b175ac6868fd012b02000000000101df6aa4cd4e8877e4b3a363d6180951036d6f18ef214dabd50a6c05a60077d4d80000000000000000000106ae0000000000001600142b8e221121004b248f6551a0c6fb8ce219f4997e03483045022100ecdb8179f92c097594844756ac2bd7948f1c44ae74b54dfda86971800e59bf980220125edbe563f24cde15fa1fa25f4eac7cb938a3ace6f9329eb487938005c7621501008b76a914e0c241e0656088953f84475cbe5c70ded12e05b58763ac6721028876f3e23b21e07f889f10fc2aa0875b96021359a06d0b7f52a79fc284f6b2837c8201208763a9144ae5c96e8b7495fd35a5ca5681aa7c8f4ab6bc9b88527c2102b4d398ee7a42e87012de5a76832d9ebfa8532ef267490a7f3fe75b2c2e18cc9152ae677503981a06b175ac6868981a06000000000000000003" val oldClaimHtlcTimeoutTx = txWithInputInfoCodec.decode(oldClaimHtlcTimeoutTxBin.bits).require.value assert(oldClaimHtlcTimeoutTx.isInstanceOf[ClaimHtlcTimeoutTx]) - assert(oldClaimHtlcTimeoutTx.asInstanceOf[ClaimHtlcTimeoutTx].confirmBefore === BlockHeight(0)) + assert(oldClaimHtlcTimeoutTx.asInstanceOf[ClaimHtlcTimeoutTx].confirmBefore == BlockHeight(0)) val claimHtlcTimeoutTx = oldClaimHtlcTimeoutTx.asInstanceOf[ClaimHtlcTimeoutTx].copy(confirmBefore = BlockHeight(1105)) val claimHtlcTimeoutTx2 = txWithInputInfoCodec.decode(txWithInputInfoCodec.encode(claimHtlcTimeoutTx).require).require.value - assert(claimHtlcTimeoutTx === claimHtlcTimeoutTx2) + assert(claimHtlcTimeoutTx == claimHtlcTimeoutTx2) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/CommonCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/CommonCodecsSpec.scala index dbc6b4e11e..feeef78e46 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/CommonCodecsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/CommonCodecsSpec.scala @@ -47,9 +47,9 @@ class CommonCodecsSpec extends AnyFunSuite { for ((uint, ref) <- expected) { val encoded = uint64.encode(uint).require - assert(ref === encoded) + assert(ref == encoded) val decoded = uint64.decode(encoded).require.value - assert(uint === decoded) + assert(uint == decoded) } } @@ -60,7 +60,7 @@ class CommonCodecsSpec extends AnyFunSuite { UInt64(hex"efffffffffffffff"), UInt64(hex"effffffffffffffe") ) - assert(refs.forall(value => uint64.decode(uint64.encode(value).require).require.value === value)) + assert(refs.forall(value => uint64.decode(uint64.encode(value).require).require.value == value)) } test("encode/decode with varint codec") { @@ -78,9 +78,9 @@ class CommonCodecsSpec extends AnyFunSuite { for ((uint, ref) <- expected) { val encoded = varint.encode(uint).require - assert(ref === encoded, ref) + assert(ref == encoded, ref) val decoded = varint.decode(encoded).require.value - assert(uint === decoded, uint) + assert(uint == decoded, uint) } } @@ -121,9 +121,9 @@ class CommonCodecsSpec extends AnyFunSuite { for ((long, ref) <- expected) { val encoded = varintoverflow.encode(long).require - assert(ref === encoded, ref) + assert(ref == encoded, ref) val decoded = varintoverflow.decode(encoded).require.value - assert(long === decoded, long) + assert(long == decoded, long) } } @@ -144,21 +144,21 @@ class CommonCodecsSpec extends AnyFunSuite { bin"00000001" -> ChannelFlags(announceChannel = true), ) testCases.foreach { case (bin, obj) => - assert(channelflags.decode(bin).require === DecodeResult(obj, BitVector.empty)) - assert(channelflags.encode(obj).require === bin) + assert(channelflags.decode(bin).require == DecodeResult(obj, BitVector.empty)) + assert(channelflags.encode(obj).require == bin) } // BOLT 2: The receiving node MUST [...] ignore undefined bits in channel_flags. - assert(channelflags.decode(bin"11111111").require === DecodeResult(ChannelFlags(announceChannel = true), BitVector.empty)) - assert(channelflags.decode(bin"11111110").require === DecodeResult(ChannelFlags(announceChannel = false), BitVector.empty)) + assert(channelflags.decode(bin"11111111").require == DecodeResult(ChannelFlags(announceChannel = true), BitVector.empty)) + assert(channelflags.decode(bin"11111110").require == DecodeResult(ChannelFlags(announceChannel = false), BitVector.empty)) } test("encode/decode with rgb codec") { val color = Color(47.toByte, 255.toByte, 142.toByte) val bin = rgb.encode(color).require - assert(bin === hex"2f ff 8e".toBitVector) + assert(bin == hex"2f ff 8e".toBitVector) val color2 = rgb.decode(bin).require.value - assert(color === color2) + assert(color == color2) } test("encode/decode all kind of IPv6 addresses with ipv6address codec") { @@ -167,7 +167,7 @@ class CommonCodecsSpec extends AnyFunSuite { val bin = hex"00000000000000000000ffffae8a0b08".toBitVector val ipv6 = Inet6Address.getByAddress(null, bin.toByteArray, null) val bin2 = ipv6address.encode(ipv6).require - assert(bin === bin2) + assert(bin == bin2) } { @@ -175,7 +175,7 @@ class CommonCodecsSpec extends AnyFunSuite { val ipv6 = InetAddresses.forString("1080:0:0:0:8:800:200C:417A").asInstanceOf[Inet6Address] val bin = ipv6address.encode(ipv6).require val ipv62 = ipv6address.decode(bin).require.value - assert(ipv6 === ipv62) + assert(ipv6 == ipv62) } } @@ -184,31 +184,31 @@ class CommonCodecsSpec extends AnyFunSuite { val ipv4addr = InetAddress.getByAddress(Array[Byte](192.toByte, 168.toByte, 1.toByte, 42.toByte)).asInstanceOf[Inet4Address] val nodeaddr = IPv4(ipv4addr, 4231) val bin = nodeaddress.encode(nodeaddr).require - assert(bin === hex"01 C0 A8 01 2A 10 87".toBitVector) + assert(bin == hex"01 C0 A8 01 2A 10 87".toBitVector) val nodeaddr2 = nodeaddress.decode(bin).require.value - assert(nodeaddr === nodeaddr2) + assert(nodeaddr == nodeaddr2) } { val ipv6addr = InetAddress.getByAddress(hex"2001 0db8 0000 85a3 0000 0000 ac1f 8001".toArray).asInstanceOf[Inet6Address] val nodeaddr = IPv6(ipv6addr, 4231) val bin = nodeaddress.encode(nodeaddr).require - assert(bin === hex"02 2001 0db8 0000 85a3 0000 0000 ac1f 8001 1087".toBitVector) + assert(bin == hex"02 2001 0db8 0000 85a3 0000 0000 ac1f 8001 1087".toBitVector) val nodeaddr2 = nodeaddress.decode(bin).require.value - assert(nodeaddr === nodeaddr2) + assert(nodeaddr == nodeaddr2) } { val nodeaddr = Tor2("z4zif3fy7fe7bpg3", 4231) val bin = nodeaddress.encode(nodeaddr).require - assert(bin === hex"03 cf3282ecb8f949f0bcdb 1087".toBitVector) + assert(bin == hex"03 cf3282ecb8f949f0bcdb 1087".toBitVector) val nodeaddr2 = nodeaddress.decode(bin).require.value - assert(nodeaddr === nodeaddr2) + assert(nodeaddr == nodeaddr2) } { val nodeaddr = Tor3("mrl2d3ilhctt2vw4qzvmz3etzjvpnc6dczliq5chrxetthgbuczuggyd", 4231) val bin = nodeaddress.encode(nodeaddr).require - assert(bin === hex"04 6457a1ed0b38a73d56dc866accec93ca6af68bc316568874478dc9399cc1a0b3431b03 1087".toBitVector) + assert(bin == hex"04 6457a1ed0b38a73d56dc866accec93ca6af68bc316568874478dc9399cc1a0b3431b03 1087".toBitVector) val nodeaddr2 = nodeaddress.decode(bin).require.value - assert(nodeaddr === nodeaddr2) + assert(nodeaddr == nodeaddr2) } } @@ -229,8 +229,8 @@ class CommonCodecsSpec extends AnyFunSuite { expected_opt match { case Some(expected) => val decoded = bytes32.decode(encoded.bits).require.value - assert(decoded === expected) - assert(expected.bytes === bytes32.encode(decoded).require.bytes) + assert(decoded == expected) + assert(expected.bytes == bytes32.encode(decoded).require.bytes) case None => assert(bytes32.decode(encoded.bits).isFailure) } @@ -255,8 +255,8 @@ class CommonCodecsSpec extends AnyFunSuite { expected_opt match { case Some(expected) => val decoded = bytes64.decode(encoded.bits).require.value - assert(decoded === expected) - assert(expected.bytes === bytes64.encode(decoded).require.bytes) + assert(decoded == expected) + assert(expected.bytes == bytes64.encode(decoded).require.bytes) case None => assert(bytes64.decode(encoded.bits).isFailure) } @@ -285,17 +285,17 @@ class CommonCodecsSpec extends AnyFunSuite { { val alias = "IRATEMONK" val bin = c.encode(alias).require - assert(bin === BitVector(alias.getBytes("UTF-8") ++ Array.fill[Byte](32 - alias.length)(0))) + assert(bin == BitVector(alias.getBytes("UTF-8") ++ Array.fill[Byte](32 - alias.length)(0))) val alias2 = c.decode(bin).require.value - assert(alias === alias2) + assert(alias == alias2) } { val alias = "this-alias-is-exactly-32-B-long." val bin = c.encode(alias).require - assert(bin === BitVector(alias.getBytes("UTF-8") ++ Array.fill[Byte](32 - alias.length)(0))) + assert(bin == BitVector(alias.getBytes("UTF-8") ++ Array.fill[Byte](32 - alias.length)(0))) val alias2 = c.decode(bin).require.value - assert(alias === alias2) + assert(alias == alias2) } { @@ -314,17 +314,17 @@ class CommonCodecsSpec extends AnyFunSuite { for ((codec, expected, bin) <- testCases) { val macCodec = prependmac(codec, mac) val decoded = macCodec.decode(bin.toBitVector).require.value - assert(decoded === expected) + assert(decoded == expected) val encoded = macCodec.encode(expected).require.toByteVector - assert(encoded === bin) + assert(encoded == bin) } } test("backward compatibility on feerate codec") { val value = 123456 val feerate = FeeratePerKw(value sat) - assert(feeratePerKw.decode(uint32.encode(value).require).require === DecodeResult(feerate, BitVector.empty)) + assert(feeratePerKw.decode(uint32.encode(value).require).require == DecodeResult(feerate, BitVector.empty)) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/ExtendedQueriesCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/ExtendedQueriesCodecsSpec.scala index 342ae09b36..b405869f1e 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/ExtendedQueriesCodecsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/ExtendedQueriesCodecsSpec.scala @@ -32,7 +32,7 @@ class ExtendedQueriesCodecsSpec extends AnyFunSuite { val ids = EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(ShortChannelId(142), ShortChannelId(15465), ShortChannelId(4564676))) val encoded = encodedShortChannelIdsCodec.encode(ids).require val decoded = encodedShortChannelIdsCodec.decode(encoded).require.value - assert(decoded === ids) + assert(decoded == ids) } { @@ -40,25 +40,25 @@ class ExtendedQueriesCodecsSpec extends AnyFunSuite { val ids = EncodedShortChannelIds(EncodingType.COMPRESSED_ZLIB, List(ShortChannelId(142), ShortChannelId(15465), ShortChannelId(4564676))) val encoded = encodedShortChannelIdsCodec.encode(ids).require val decoded = encodedShortChannelIdsCodec.decode(encoded).require.value - assert(decoded === ids) + assert(decoded == ids) } { // encode/decode empty list with encoding 'uncompressed' val ids = EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List.empty) val encoded = encodedShortChannelIdsCodec.encode(ids).require - assert(encoded.bytes === hex"00") + assert(encoded.bytes == hex"00") val decoded = encodedShortChannelIdsCodec.decode(encoded).require.value - assert(decoded === ids) + assert(decoded == ids) } { // encode/decode empty list with encoding 'zlib' val ids = EncodedShortChannelIds(EncodingType.COMPRESSED_ZLIB, List.empty) val encoded = encodedShortChannelIdsCodec.encode(ids).require - assert(encoded.bytes === hex"00") // NB: empty list is always encoded with encoding type 'uncompressed' + assert(encoded.bytes == hex"00") // NB: empty list is always encoded with encoding type 'uncompressed' val decoded = encodedShortChannelIdsCodec.decode(encoded).require.value - assert(decoded === EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List.empty)) + assert(decoded == EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List.empty)) } } @@ -70,7 +70,7 @@ class ExtendedQueriesCodecsSpec extends AnyFunSuite { val encoded = queryShortChannelIdsCodec.encode(query_short_channel_id).require val decoded = queryShortChannelIdsCodec.decode(encoded).require.value - assert(decoded === query_short_channel_id) + assert(decoded == query_short_channel_id) } test("encode query_short_channel_ids (with optional data)") { @@ -81,7 +81,7 @@ class ExtendedQueriesCodecsSpec extends AnyFunSuite { val encoded = queryShortChannelIdsCodec.encode(query_short_channel_id).require val decoded = queryShortChannelIdsCodec.decode(encoded).require.value - assert(decoded === query_short_channel_id) + assert(decoded == query_short_channel_id) } test("encode query_short_channel_ids (with optional data including unknown data)") { @@ -96,7 +96,7 @@ class ExtendedQueriesCodecsSpec extends AnyFunSuite { val encoded = queryShortChannelIdsCodec.encode(query_short_channel_id).require val decoded = queryShortChannelIdsCodec.decode(encoded).require.value - assert(decoded === query_short_channel_id) + assert(decoded == query_short_channel_id) } test("encode reply_channel_range (no optional data)") { @@ -109,7 +109,7 @@ class ExtendedQueriesCodecsSpec extends AnyFunSuite { val encoded = replyChannelRangeCodec.encode(replyChannelRange).require val decoded = replyChannelRangeCodec.decode(encoded).require.value - assert(decoded === replyChannelRange) + assert(decoded == replyChannelRange) } test("encode reply_channel_range (with optional timestamps)") { @@ -123,7 +123,7 @@ class ExtendedQueriesCodecsSpec extends AnyFunSuite { val encoded = replyChannelRangeCodec.encode(replyChannelRange).require val decoded = replyChannelRangeCodec.decode(encoded).require.value - assert(decoded === replyChannelRange) + assert(decoded == replyChannelRange) } test("encode reply_channel_range (with optional timestamps, checksums, and unknown data)") { @@ -143,7 +143,7 @@ class ExtendedQueriesCodecsSpec extends AnyFunSuite { val encoded = replyChannelRangeCodec.encode(replyChannelRange).require val decoded = replyChannelRangeCodec.decode(encoded).require.value - assert(decoded === replyChannelRange) + assert(decoded == replyChannelRange) } test("compute checksums correctly (CL test #1)") { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/FailureMessageCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/FailureMessageCodecsSpec.scala index 70f13991de..5a2ebade3f 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/FailureMessageCodecsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/FailureMessageCodecsSpec.scala @@ -53,7 +53,7 @@ class FailureMessageCodecsSpec extends AnyFunSuite { msg => { val encoded = failureMessageCodec.encode(msg).require val decoded = failureMessageCodec.decode(encoded).require - assert(msg === decoded.value) + assert(msg == decoded.value) } } } @@ -74,8 +74,8 @@ class FailureMessageCodecsSpec extends AnyFunSuite { val decoded = failureMessageCodec.decode(bin.bits).require.value assert(decoded.isInstanceOf[FailureMessage]) assert(decoded.isInstanceOf[UnknownFailureMessage]) - assert(decoded.isInstanceOf[Node] === node) - assert(decoded.isInstanceOf[Perm] === perm) + assert(decoded.isInstanceOf[Node] == node) + assert(decoded.isInstanceOf[Perm] == perm) } } @@ -87,7 +87,7 @@ class FailureMessageCodecsSpec extends AnyFunSuite { ) for ((code, message) <- msgs) { - assert(message.code === code) + assert(message.code == code) } } @@ -100,10 +100,10 @@ class FailureMessageCodecsSpec extends AnyFunSuite { for ((expected, bin) <- testCases) { val decoded = codec.decode(bin.toBitVector).require.value - assert(decoded === expected) + assert(decoded == expected) val encoded = codec.encode(expected).require.toByteVector - assert(encoded === bin) + assert(encoded == bin) } } @@ -119,7 +119,7 @@ class FailureMessageCodecsSpec extends AnyFunSuite { ) for ((expected, bin) <- testCases) { - assert(codec.decode(bin.bits).require.value === expected) + assert(codec.decode(bin.bits).require.value == expected) } } @@ -151,10 +151,10 @@ class FailureMessageCodecsSpec extends AnyFunSuite { val ref = TemporaryChannelFailure(ChannelUpdate(ByteVector64(hex"cc3e80149073ed487c76e48e9622bf980f78267b8a34a3f61921f2d8fce6063b08e74f34a073a13f2097337e4915bb4c001f3b5c4d81e9524ed575e1f4578219"), Block.LivenetGenesisBlock.hash, ShortChannelId(0x826050004130000L), 1536275759 unixsec, ChannelUpdate.ChannelFlags(isEnabled = false, isNode1 = false), CltvExpiryDelta(14), 1000 msat, 1 msat, 1, None)) val u = failureMessageCodec.decode(tmp_channel_failure_notype.toBitVector).require.value - assert(u === ref) + assert(u == ref) val bin = ByteVector(failureMessageCodec.encode(u).require.toByteArray) - assert(bin === tmp_channel_failure_withtype) + assert(bin == tmp_channel_failure_withtype) val u2 = failureMessageCodec.decode(bin.toBitVector).require.value - assert(u2 === ref) + assert(u2 == ref) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecsSpec.scala index 76b2f9cf4b..899de909a0 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecsSpec.scala @@ -81,12 +81,12 @@ class LightningMessageCodecsSpec extends AnyFunSuite { for (testCase <- testCases) { if (testCase.valid) { val init = initCodec.decode(testCase.encoded.bits).require.value - assert(init.features.toByteVector === testCase.rawFeatures) - assert(init.networks === testCase.networks) - assert(init.remoteAddress_opt === testCase.address) + assert(init.features.toByteVector == testCase.rawFeatures) + assert(init.networks == testCase.networks) + assert(init.remoteAddress_opt == testCase.address) val encoded = initCodec.encode(init).require - assert(encoded.bytes === testCase.reEncoded.getOrElse(testCase.encoded)) - assert(initCodec.decode(encoded).require.value === init) + assert(encoded.bytes == testCase.reEncoded.getOrElse(testCase.encoded)) + assert(initCodec.decode(encoded).require.value == init) } else { assert(initCodec.decode(testCase.encoded.bits).isFailure) } @@ -102,8 +102,8 @@ class LightningMessageCodecsSpec extends AnyFunSuite { ) for ((warning, expected) <- testCases) { - assert(lightningMessageCodec.encode(warning).require.bytes === expected) - assert(lightningMessageCodec.decode(expected.bits).require.value === warning) + assert(lightningMessageCodec.encode(warning).require.bytes == expected) + assert(lightningMessageCodec.decode(expected.bits).require.value == warning) } } @@ -142,8 +142,8 @@ class LightningMessageCodecsSpec extends AnyFunSuite { ) refs.foreach { case ((bin, remainder), msg) => - assert(lightningMessageCodec.decode(bin.bits ++ remainder.bits).require === DecodeResult(msg, remainder.bits)) - assert(lightningMessageCodec.encode(msg).require === bin.bits) + assert(lightningMessageCodec.decode(bin.bits ++ remainder.bits).require == DecodeResult(msg, remainder.bits)) + assert(lightningMessageCodec.encode(msg).require == bin.bits) } } @@ -153,7 +153,7 @@ class LightningMessageCodecsSpec extends AnyFunSuite { val node = nodeAnnouncementCodec.decode(bin).require.value val bin2 = nodeAnnouncementCodec.encode(node).require - assert(bin === bin2) + assert(bin == bin2) } test("encode/decode interactive-tx messages") { @@ -187,9 +187,9 @@ class LightningMessageCodecsSpec extends AnyFunSuite { testCases.foreach { case (message, bin) => val decoded = lightningMessageCodec.decode(bin.bits).require assert(decoded.remainder.isEmpty) - assert(decoded.value === message) + assert(decoded.value == message) val encoded = lightningMessageCodec.encode(message).require.bytes - assert(encoded === bin) + assert(encoded == bin) } } @@ -226,9 +226,9 @@ class LightningMessageCodecsSpec extends AnyFunSuite { for ((encoded, expected) <- testCases) { val decoded = openChannelCodec.decode(encoded.bits).require.value - assert(decoded === expected) + assert(decoded == expected) val reEncoded = openChannelCodec.encode(decoded).require.bytes - assert(reEncoded === encoded) + assert(reEncoded == encoded) } } @@ -260,9 +260,9 @@ class LightningMessageCodecsSpec extends AnyFunSuite { ) testCases.foreach { case (open, bin) => val decoded = lightningMessageCodec.decode(bin.bits).require.value - assert(decoded === open) + assert(decoded == open) val encoded = lightningMessageCodec.encode(open).require.bytes - assert(encoded === bin) + assert(encoded == bin) } } @@ -276,7 +276,7 @@ class LightningMessageCodecsSpec extends AnyFunSuite { ) testCases.foreach { case (bin, flags) => val decoded = lightningMessageCodec.decode(bin.bits).require.value - assert(decoded === defaultOpen.copy(channelFlags = flags)) + assert(decoded == defaultOpen.copy(channelFlags = flags)) } } @@ -300,9 +300,9 @@ class LightningMessageCodecsSpec extends AnyFunSuite { for ((encoded, expected) <- testCases) { val decoded = acceptChannelCodec.decode(encoded.bits).require.value - assert(decoded === expected) + assert(decoded == expected) val reEncoded = acceptChannelCodec.encode(decoded).require.bytes - assert(reEncoded === encoded) + assert(reEncoded == encoded) } } @@ -315,9 +315,9 @@ class LightningMessageCodecsSpec extends AnyFunSuite { ) testCases.foreach { case (accept, bin) => val decoded = lightningMessageCodec.decode(bin.bits).require.value - assert(decoded === accept) + assert(decoded == accept) val encoded = lightningMessageCodec.encode(accept).require.bytes - assert(encoded === bin) + assert(encoded == bin) } } @@ -334,9 +334,9 @@ class LightningMessageCodecsSpec extends AnyFunSuite { for ((encoded, expected) <- testCases) { val decoded = closingSignedCodec.decode(encoded.bits).require.value - assert(decoded === expected) + assert(decoded == expected) val reEncoded = closingSignedCodec.encode(decoded).require.bytes - assert(reEncoded === encoded) + assert(reEncoded == encoded) } } @@ -389,7 +389,7 @@ class LightningMessageCodecsSpec extends AnyFunSuite { msg => { val encoded = lightningMessageCodecWithFallback.encode(msg).require val decoded = lightningMessageCodecWithFallback.decode(encoded).require - assert(msg === decoded.value) + assert(msg == decoded.value) } } } @@ -401,7 +401,7 @@ class LightningMessageCodecsSpec extends AnyFunSuite { val encoded1 = lightningMessageCodecWithFallback.encode(unknown).require val decoded1 = lightningMessageCodecWithFallback.decode(encoded1).require.value assert(lightningMessageCodec.decode(encoded1).isFailure) - assert(decoded1 === unknown) + assert(decoded1 == unknown) } test("non-reg encoding type") { @@ -462,7 +462,7 @@ class LightningMessageCodecsSpec extends AnyFunSuite { val items = refs.map { case (obj, refbin) => val bin = lightningMessageCodec.encode(obj).require - assert(refbin.bits === bin) + assert(refbin.bits == bin) TestItem(obj, bin.toHex) } @@ -520,11 +520,11 @@ class LightningMessageCodecsSpec extends AnyFunSuite { // this was generated by c-lightning val bin = hex"010258fff7d0e987e2cdd560e3bb5a046b4efe7b26c969c2f51da1dceec7bcb8ae1b634790503d5290c1a6c51d681cf8f4211d27ed33a257dcc1102862571bf1792306226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f0005a100000200005bc75919010100060000000000000001000000010000000a000000003a699d00" val update = lightningMessageCodec.decode(bin.bits).require.value.asInstanceOf[ChannelUpdate] - assert(update === ChannelUpdate(ByteVector64(hex"58fff7d0e987e2cdd560e3bb5a046b4efe7b26c969c2f51da1dceec7bcb8ae1b634790503d5290c1a6c51d681cf8f4211d27ed33a257dcc1102862571bf17923"), ByteVector32(hex"06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f"), ShortChannelId(0x5a10000020000L), 1539791129 unixsec, ChannelUpdate.ChannelFlags(isEnabled = true, isNode1 = false), CltvExpiryDelta(6), 1 msat, 1 msat, 10, Some(980000000 msat))) + assert(update == ChannelUpdate(ByteVector64(hex"58fff7d0e987e2cdd560e3bb5a046b4efe7b26c969c2f51da1dceec7bcb8ae1b634790503d5290c1a6c51d681cf8f4211d27ed33a257dcc1102862571bf17923"), ByteVector32(hex"06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f"), ShortChannelId(0x5a10000020000L), 1539791129 unixsec, ChannelUpdate.ChannelFlags(isEnabled = true, isNode1 = false), CltvExpiryDelta(6), 1 msat, 1 msat, 10, Some(980000000 msat))) val nodeId = PublicKey(hex"03370c9bac836e557eb4f017fe8f9cc047f44db39c1c4e410ff0f7be142b817ae4") assert(Announcements.checkSig(update, nodeId)) val bin2 = ByteVector(lightningMessageCodec.encode(update).require.toByteArray) - assert(bin === bin2) + assert(bin == bin2) } test("non-regression on channel_update") { @@ -549,10 +549,10 @@ class LightningMessageCodecsSpec extends AnyFunSuite { for ((bin, ref) <- bins) { val decoded = channelUpdateCodec.decode(bin.bits).require // not only must decoding succeed, it must also produce the same object - assert(Serialization.write(decoded.value)(JsonSerializers.formats) === ref) + assert(Serialization.write(decoded.value)(JsonSerializers.formats) == ref) assert(decoded.remainder.isEmpty) // encoding must produce the same buffer - assert(channelUpdateCodec.encode(decoded.value).require.bytes === bin) + assert(channelUpdateCodec.encode(decoded.value).require.bytes == bin) } } @@ -565,9 +565,9 @@ class LightningMessageCodecsSpec extends AnyFunSuite { ) for ((bin, ref) <- testCases) { val decoded = channelFlagsCodec.decode(bin).require - assert(decoded.value === ref) + assert(decoded.value == ref) assert(decoded.remainder.isEmpty) - assert(channelFlagsCodec.encode(ref).require === bin) + assert(channelFlagsCodec.encode(ref).require == bin) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/MessageOnionCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/MessageOnionCodecsSpec.scala index ae4c9fc98c..12768df26c 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/MessageOnionCodecsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/MessageOnionCodecsSpec.scala @@ -14,8 +14,8 @@ class MessageOnionCodecsSpec extends AnyFunSuiteLike { test("simple relay payload") { val payload = RelayPayload(TlvStream(EncryptedData(hex"0a336970e870b473ddbc27e3098bfa45bb1aa54f1f637f803d957e6271d8ffeba89da2665d62123763d9b634e30714144a1c165ac9"))) val serialized = hex"3704350a336970e870b473ddbc27e3098bfa45bb1aa54f1f637f803d957e6271d8ffeba89da2665d62123763d9b634e30714144a1c165ac9" - assert(relayPerHopPayloadCodec.encode(payload).require.bytes === serialized) - assert(relayPerHopPayloadCodec.decode(serialized.bits).require.value === payload) + assert(relayPerHopPayloadCodec.encode(payload).require.bytes == serialized) + assert(relayPerHopPayloadCodec.decode(serialized.bits).require.value == payload) } test("empty relay payload") { @@ -31,8 +31,8 @@ class MessageOnionCodecsSpec extends AnyFunSuiteLike { test("simple final payload") { val payload = FinalPayload(TlvStream(EncryptedData(hex""))) val serialized = hex"020400" - assert(finalPerHopPayloadCodec.encode(payload).require.bytes === serialized) - assert(finalPerHopPayloadCodec.decode(serialized.bits).require.value === payload) + assert(finalPerHopPayloadCodec.encode(payload).require.bytes == serialized) + assert(finalPerHopPayloadCodec.decode(serialized.bits).require.value == payload) } test("empty final payload") { @@ -47,34 +47,34 @@ class MessageOnionCodecsSpec extends AnyFunSuiteLike { List(hex"04210324653eac434488002cc06bbfb7f10fe18991e35f9fe4302dbea6d2353dc0ab1c", hex"0421027f31ebc5462c1fdce1b737ecff52d37d75dea43ce11c74d25aa297165faa2007")) val payload = FinalPayload(TlvStream(ReplyPath(blindedRoute), EncryptedData(hex""))) val serialized = hex"f202ee02eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f28368661902c5ee5d5d559475814916957e167b8c257e06532ae6bfcbe4553e4549b9142ec7039dcddf597c0ea5bfe3c4de00630182d26c8f3cb588fa02c8cd19391a110f41a200330840ad82edc7378794e568deb3a836e3b9bc2e4a684412c34dbc5e50159ecf0b9c3844719f8656af9ff283e1eecb503f5e45b302aa42066bc9802597cac8f9f7193b8fd24b8671e3807e9c61dae8b330b695de780033d76f6388daa82694bcc63d43eaac1c5d189722cb84d0edb3b8b7dccb833c886eda7adb483f44498789f4139b2c12a0bfe8436a0400" - assert(finalPerHopPayloadCodec.encode(payload).require.bytes === serialized) - assert(finalPerHopPayloadCodec.decode(serialized.bits).require.value === payload) + assert(finalPerHopPayloadCodec.encode(payload).require.bytes == serialized) + assert(finalPerHopPayloadCodec.decode(serialized.bits).require.value == payload) } test("onion packet can be any size") { { // small onion val onion = OnionRoutingPacket(1, hex"032c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e668680991", hex"0012345679abcdef", ByteVector32(hex"0000111122223333444455556666777788889999aaaabbbbccccddddeeee0000")) val serialized = hex"004a 01 032c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e668680991 0012345679abcdef 0000111122223333444455556666777788889999aaaabbbbccccddddeeee0000" - assert(messageOnionPacketCodec.encode(onion).require.bytes === serialized) - assert(messageOnionPacketCodec.decode(serialized.bits).require.value === onion) + assert(messageOnionPacketCodec.encode(onion).require.bytes == serialized) + assert(messageOnionPacketCodec.decode(serialized.bits).require.value == onion) } { // larger onion val onion = OnionRoutingPacket(2, hex"027f31ebc5462c1fdce1b737ecff52d37d75dea43ce11c74d25aa297165faa2007", hex"0012345679abcdef012345679abcdef012345679abcdef012345679abcdef012345679abcdef", ByteVector32(hex"eeee0000111122223333444455556666777788889999aaaabbbbccccddddeeee")) val serialized = hex"0068 02 027f31ebc5462c1fdce1b737ecff52d37d75dea43ce11c74d25aa297165faa2007 0012345679abcdef012345679abcdef012345679abcdef012345679abcdef012345679abcdef eeee0000111122223333444455556666777788889999aaaabbbbccccddddeeee" - assert(messageOnionPacketCodec.encode(onion).require.bytes === serialized) - assert(messageOnionPacketCodec.decode(serialized.bits).require.value === onion) + assert(messageOnionPacketCodec.encode(onion).require.bytes == serialized) + assert(messageOnionPacketCodec.decode(serialized.bits).require.value == onion) } { // onion with trailing additional bytes val onion = OnionRoutingPacket(0, hex"032c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e668680991", hex"ffffffff", ByteVector32.Zeroes) val serialized = hex"0046 00 032c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e668680991 ffffffff 0000000000000000000000000000000000000000000000000000000000000000 0a01020000030400000000" - assert(messageOnionPacketCodec.encode(onion).require.bytes === serialized.dropRight(11)) - assert(messageOnionPacketCodec.decode(serialized.bits).require.value === onion) + assert(messageOnionPacketCodec.encode(onion).require.bytes == serialized.dropRight(11)) + assert(messageOnionPacketCodec.decode(serialized.bits).require.value == onion) } { // onion with empty payload val onion = OnionRoutingPacket(0, hex"032c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e668680991", hex"", ByteVector32.Zeroes) val serialized = hex"0042 00 032c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e668680991 0000000000000000000000000000000000000000000000000000000000000000" - assert(messageOnionPacketCodec.encode(onion).require.bytes === serialized) - assert(messageOnionPacketCodec.decode(serialized.bits).require.value === onion) + assert(messageOnionPacketCodec.encode(onion).require.bytes == serialized) + assert(messageOnionPacketCodec.decode(serialized.bits).require.value == onion) } { // onion length too big val serialized = hex"0048 00 032c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e668680991 ffffffff 0000000000000000000000000000000000000000000000000000000000000000" diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/OffersSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/OffersSpec.scala index c282d943bc..22e7636dc2 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/OffersSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/OffersSpec.scala @@ -52,8 +52,8 @@ class OffersSpec extends AnyFunSuite { val offer = Offer.decode(encoded).get assert(offer.amount.isEmpty) assert(offer.signature.isEmpty) - assert(offer.description === "Offer by rusty's node") - assert(offer.nodeIdXOnly === nodeId) + assert(offer.description == "Offer by rusty's node") + assert(offer.nodeIdXOnly == nodeId) } test("basic signed offer") { @@ -61,30 +61,30 @@ class OffersSpec extends AnyFunSuite { val Success(signedOffer) = Offer.decode(encodedSigned) assert(signedOffer.checkSignature()) assert(signedOffer.amount.isEmpty) - assert(signedOffer.description === "Offer by rusty's node") - assert(signedOffer.nodeIdXOnly === nodeId) + assert(signedOffer.description == "Offer by rusty's node") + assert(signedOffer.nodeIdXOnly == nodeId) } test("offer with amount and quantity") { val encoded = "lno1pqqnyzsmx5cx6umpwssx6atvw35j6ut4v9h8g6t50ysx7enxv4epgrmjw4ehgcm0wfczucm0d5hxzagkqyq3ugztng063cqx783exlm97ekyprnd4rsu5u5w5sez9fecrhcuc3ykq5" val Success(offer) = Offer.decode(encoded) - assert(offer.amount === Some(50 msat)) + assert(offer.amount == Some(50 msat)) assert(offer.signature.isEmpty) - assert(offer.description === "50msat multi-quantity offer") - assert(offer.nodeIdXOnly === nodeId) - assert(offer.issuer === Some("rustcorp.com.au")) - assert(offer.quantityMin === Some(1)) + assert(offer.description == "50msat multi-quantity offer") + assert(offer.nodeIdXOnly == nodeId) + assert(offer.issuer == Some("rustcorp.com.au")) + assert(offer.quantityMin == Some(1)) } test("signed offer with amount and quantity") { val encodedSigned = "lno1pqqnyzsmx5cx6umpwssx6atvw35j6ut4v9h8g6t50ysx7enxv4epgrmjw4ehgcm0wfczucm0d5hxzagkqyq3ugztng063cqx783exlm97ekyprnd4rsu5u5w5sez9fecrhcuc3ykqhcypjju7unu05vav8yvhn27lztf46k9gqlga8uvu4uq62kpuywnu6me8srgh2q7puczukr8arectaapfl5d4rd6uc7st7tnqf0ttx39n40s" val Success(signedOffer) = Offer.decode(encodedSigned) assert(signedOffer.checkSignature()) - assert(signedOffer.amount === Some(50 msat)) - assert(signedOffer.description === "50msat multi-quantity offer") - assert(signedOffer.nodeIdXOnly === nodeId) - assert(signedOffer.issuer === Some("rustcorp.com.au")) - assert(signedOffer.quantityMin === Some(1)) + assert(signedOffer.amount == Some(50 msat)) + assert(signedOffer.description == "50msat multi-quantity offer") + assert(signedOffer.nodeIdXOnly == nodeId) + assert(signedOffer.issuer == Some("rustcorp.com.au")) + assert(signedOffer.quantityMin == Some(1)) } test("decode invalid offer") { @@ -198,16 +198,16 @@ class OffersSpec extends AnyFunSuite { test("decode invoice request") { val encoded = "lnr1qvsxlc5vp2m0rvmjcxn2y34wv0m5lyc7sdj7zksgn35dvxgqqqqqqqqyypz8xu3xwsqpar9dd26lgrrvc7s63ljt0pgh6ag2utv5udez7n2mjzqzz47qcqczqgqzqqgzycsv2tmjgzc5l546aldq699wj9pdusvfred97l352p4aa862vqvzw5p8pdyjqctdyppxzardv9hrypx74klwluzqd0rqgeew2uhuagttuv6aqwklvm0xmlg52lfnagzw8ygt0wrtnv2tsx69m6tgug7njaw5ypa5fn369n9yzc87v02rqccj9h04dxf3nzc" val Success(request) = InvoiceRequest.decode(encoded) - assert(request.amount === Some(5500 msat)) - assert(request.offerId === ByteVector32(hex"4473722674001e8cad6ab5f40c6cc7a1a8fe4b78517d750ae2d94e3722f4d5b9")) - assert(request.quantity === 2) - assert(request.features === Features(VariableLengthOnion -> Optional, BasicMultiPartPayment -> Optional)) + assert(request.amount == Some(5500 msat)) + assert(request.offerId == ByteVector32(hex"4473722674001e8cad6ab5f40c6cc7a1a8fe4b78517d750ae2d94e3722f4d5b9")) + assert(request.quantity == 2) + assert(request.features == Features(VariableLengthOnion -> Optional, BasicMultiPartPayment -> Optional)) assert(request.records.get[Chain].nonEmpty) - assert(request.chain === Block.LivenetGenesisBlock.hash) - assert(request.payerKey === ByteVector32(hex"c52f7240b14fd2baefda0d14ae9142de41891e5a5f7e34506bde9f4a60182750")) - assert(request.payerInfo === Some(hex"deadbeef")) - assert(request.payerNote === Some("I am Batman")) - assert(request.encode() === encoded) + assert(request.chain == Block.LivenetGenesisBlock.hash) + assert(request.payerKey == ByteVector32(hex"c52f7240b14fd2baefda0d14ae9142de41891e5a5f7e34506bde9f4a60182750")) + assert(request.payerInfo == Some(hex"deadbeef")) + assert(request.payerNote == Some("I am Batman")) + assert(request.encode() == encoded) } test("decode invalid invoice request") { @@ -274,9 +274,9 @@ class OffersSpec extends AnyFunSuite { case TestCase(tlvStream, tlvCount, expectedRoot) => val genericTlvStream: Codec[TlvStream[GenericTlv]] = list(TlvCodecs.genericTlv).xmap(tlvs => TlvStream(tlvs), tlvs => tlvs.records.toList) val tlvs = genericTlvStream.decode(tlvStream.bits).require.value - assert(tlvs.records.size === tlvCount) + assert(tlvs.records.size == tlvCount) val root = Offers.rootHash(tlvs, genericTlvStream) - assert(root === expectedRoot) + assert(root == expectedRoot) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/PaymentOnionSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/PaymentOnionSpec.scala index 94e83962ea..0e597c8a75 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/PaymentOnionSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/PaymentOnionSpec.scala @@ -40,10 +40,10 @@ class PaymentOnionSpec extends AnyFunSuite { val expected = OnionRoutingPacket(0, hex"02eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619", hex"e5f14350c2a76fc232b5e46d421e9615471ab9e0bc887beff8c95fdb878f7b3a71da571226458c510bbadd1276f045c21c520a07d35da256ef75b4367962437b0dd10f7d61ab590531cf08000178a333a347f8b4072e216400406bdf3bf038659793a86cae5f52d32f3438527b47a1cfc54285a8afec3a4c9f3323db0c946f5d4cb2ce721caad69320c3a469a202f3e468c67eaf7a7cda226d0fd32f7b48084dca885d15222e60826d5d971f64172d98e0760154400958f00e86697aa1aa9d41bee8119a1ec866abe044a9ad635778ba61fc0776dc832b39451bd5d35072d2269cf9b040d6ba38b54ec35f81d7fc67678c3be47274f3c4cc472aff005c3469eb3bc140769ed4c7f0218ff8c6c7dd7221d189c65b3b9aaa71a01484b122846c7c7b57e02e679ea8469b70e14fe4f70fee4d87b910cf144be6fe48eef24da475c0b0bcc6565ae82cd3f4e3b24c76eaa5616c6111343306ab35c1fe5ca4a77c0e314ed7dba39d6f1e0de791719c241a939cc493bea2bae1c1e932679ea94d29084278513c77b899cc98059d06a27d171b0dbdf6bee13ddc4fc17a0c4d2827d488436b57baa167544138ca2e64a11b43ac8a06cd0c2fba2d4d900ed2d9205305e2d7383cc98dacb078133de5f6fb6bed2ef26ba92cea28aafc3b9948dd9ae5559e8bd6920b8cea462aa445ca6a95e0e7ba52961b181c79e73bd581821df2b10173727a810c92b83b5ba4a0403eb710d2ca10689a35bec6c3a708e9e92f7d78ff3c5d9989574b00c6736f84c199256e76e19e78f0c98a9d580b4a658c84fc8f2096c2fbea8f5f8c59d0fdacb3be2802ef802abbecb3aba4acaac69a0e965abd8981e9896b1f6ef9d60f7a164b371af869fd0e48073742825e9434fc54da837e120266d53302954843538ea7c6c3dbfb4ff3b2fdbe244437f2a153ccf7bdb4c92aa08102d4f3cff2ae5ef86fab4653595e6a5837fa2f3e29f27a9cde5966843fb847a4a61f1e76c281fe8bb2b0a181d096100db5a1a5ce7a910238251a43ca556712eaadea167fb4d7d75825e440f3ecd782036d7574df8bceacb397abefc5f5254d2722215c53ff54af8299aaaad642c6d72a14d27882d9bbd539e1cc7a527526ba89b8c037ad09120e98ab042d3e8652b31ae0e478516bfaf88efca9f3676ffe99d2819dcaeb7610a626695f53117665d267d3f7abebd6bbd6733f645c72c389f03855bdf1e4b8075b516569b118233a0f0971d24b83113c0b096f5216a207ca99a7cddc81c130923fe3d91e7508c9ac5f2e914ff5dccab9e558566fa14efb34ac98d878580814b94b73acbfde9072f30b881f7f0fff42d4045d1ace6322d86a97d164aa84d93a60498065cc7c20e636f5862dc81531a88c60305a2e59a985be327a6902e4bed986dbf4a0b50c217af0ea7fdf9ab37f9ea1a1aaa72f54cf40154ea9b269f1a7c09f9f43245109431a175d50e2db0132337baa0ef97eed0fcf20489da36b79a1172faccc2f7ded7c60e00694282d93359c4682135642bc81f433574aa8ef0c97b4ade7ca372c5ffc23c7eddd839bab4e0f14d6df15c9dbeab176bec8b5701cf054eb3072f6dadc98f88819042bf10c407516ee58bce33fbe3b3d86a54255e577db4598e30a135361528c101683a5fcde7e8ba53f3456254be8f45fe3a56120ae96ea3773631fcb3873aa3abd91bcff00bd38bd43697a2e789e00da6077482e7b1b1a677b5afae4c54e6cbdf7377b694eb7d7a5b913476a5be923322d3de06060fd5e819635232a2cf4f0731da13b8546d1d6d4f8d75b9fce6c2341a71b0ea6f780df54bfdb0dd5cd9855179f602f9172", ByteVector32(hex"65f21f9190c70217774a6fbaaa7d63ad64199f4664813b955cff954949076dcf")) val decoded = paymentOnionPacketCodec.decode(bin.bits).require.value - assert(decoded === expected) + assert(decoded == expected) val encoded = paymentOnionPacketCodec.encode(decoded).require - assert(encoded.toByteVector === bin) + assert(encoded.toByteVector == bin) } test("encode/decode fixed-size (legacy) relay per-hop payload") { @@ -55,10 +55,10 @@ class PaymentOnionSpec extends AnyFunSuite { for ((expected, bin) <- testCases) { val decoded = channelRelayPerHopPayloadCodec.decode(bin.bits).require.value - assert(decoded === expected) + assert(decoded == expected) val encoded = channelRelayPerHopPayloadCodec.encode(expected).require.bytes - assert(encoded === bin) + assert(encoded == bin) } } @@ -75,7 +75,7 @@ class PaymentOnionSpec extends AnyFunSuite { ) for ((payloadLength, bin) <- testCases) { - assert(payloadLengthDecoder.decode(bin.bits).require.value === payloadLength) + assert(payloadLengthDecoder.decode(bin.bits).require.value == payloadLength) } } @@ -88,13 +88,13 @@ class PaymentOnionSpec extends AnyFunSuite { for ((expected, bin) <- testCases) { val decoded = channelRelayPerHopPayloadCodec.decode(bin.bits).require.value - assert(decoded === ChannelRelayTlvPayload(expected)) - assert(decoded.amountToForward === 561.msat) - assert(decoded.outgoingCltv === CltvExpiry(42)) - assert(decoded.outgoingChannelId === ShortChannelId(1105)) + assert(decoded == ChannelRelayTlvPayload(expected)) + assert(decoded.amountToForward == 561.msat) + assert(decoded.outgoingCltv == CltvExpiry(42)) + assert(decoded.outgoingChannelId == ShortChannelId(1105)) val encoded = channelRelayPerHopPayloadCodec.encode(ChannelRelayTlvPayload(expected)).require.bytes - assert(encoded === bin) + assert(encoded == bin) } } @@ -104,17 +104,17 @@ class PaymentOnionSpec extends AnyFunSuite { val bin = hex"2e 02020231 04012a fe000102322102eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619" val decoded = nodeRelayPerHopPayloadCodec.decode(bin.bits).require.value - assert(decoded === NodeRelayPayload(expected)) - assert(decoded.amountToForward === 561.msat) - assert(decoded.totalAmount === 561.msat) - assert(decoded.outgoingCltv === CltvExpiry(42)) - assert(decoded.outgoingNodeId === nodeId) - assert(decoded.paymentSecret === None) - assert(decoded.invoiceFeatures === None) - assert(decoded.invoiceRoutingInfo === None) + assert(decoded == NodeRelayPayload(expected)) + assert(decoded.amountToForward == 561.msat) + assert(decoded.totalAmount == 561.msat) + assert(decoded.outgoingCltv == CltvExpiry(42)) + assert(decoded.outgoingNodeId == nodeId) + assert(decoded.paymentSecret == None) + assert(decoded.invoiceFeatures == None) + assert(decoded.invoiceRoutingInfo == None) val encoded = nodeRelayPerHopPayloadCodec.encode(NodeRelayPayload(expected)).require.bytes - assert(encoded === bin) + assert(encoded == bin) } test("encode/decode variable-length (tlv) node relay to legacy per-hop payload") { @@ -129,17 +129,17 @@ class PaymentOnionSpec extends AnyFunSuite { val bin = hex"fa 02020231 04012a 0822eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f2836866190451 fe00010231010a fe000102322102eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619 fe000102339b01036d6caac248af96f6afa7f904f550253a0f3ef3f5aa2fe6838a95b216691468e200000000000000010000000a00000064009002025f7117a78150fe2ef97db7cfc83bd57b2e2c0d0dd25eaf467a4a1c2a45ce148600000000000000020000001400000096000c02a051267759c3a149e3e72372f4e0c4054ba597ebfd0eda78a2273023667205ee00000000000000030000001e000000c80018" val decoded = nodeRelayPerHopPayloadCodec.decode(bin.bits).require.value - assert(decoded === NodeRelayPayload(expected)) - assert(decoded.amountToForward === 561.msat) - assert(decoded.totalAmount === 1105.msat) - assert(decoded.paymentSecret === Some(ByteVector32(hex"eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619"))) - assert(decoded.outgoingCltv === CltvExpiry(42)) - assert(decoded.outgoingNodeId === nodeId) - assert(decoded.invoiceFeatures === Some(features)) - assert(decoded.invoiceRoutingInfo === Some(routingHints)) + assert(decoded == NodeRelayPayload(expected)) + assert(decoded.amountToForward == 561.msat) + assert(decoded.totalAmount == 1105.msat) + assert(decoded.paymentSecret == Some(ByteVector32(hex"eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619"))) + assert(decoded.outgoingCltv == CltvExpiry(42)) + assert(decoded.outgoingNodeId == nodeId) + assert(decoded.invoiceFeatures == Some(features)) + assert(decoded.invoiceRoutingInfo == Some(routingHints)) val encoded = nodeRelayPerHopPayloadCodec.encode(NodeRelayPayload(expected)).require.bytes - assert(encoded === bin) + assert(encoded == bin) } test("encode/decode variable-length (tlv) final per-hop payload") { @@ -157,12 +157,12 @@ class PaymentOnionSpec extends AnyFunSuite { for ((expected, bin) <- testCases) { val decoded = finalPerHopPayloadCodec.decode(bin.bits).require.value - assert(decoded === FinalTlvPayload(expected)) - assert(decoded.amount === 561.msat) - assert(decoded.expiry === CltvExpiry(42)) + assert(decoded == FinalTlvPayload(expected)) + assert(decoded.amount == 561.msat) + assert(decoded.expiry == CltvExpiry(42)) val encoded = finalPerHopPayloadCodec.encode(FinalTlvPayload(expected)).require.bytes - assert(encoded === bin) + assert(encoded == bin) } } @@ -171,22 +171,22 @@ class PaymentOnionSpec extends AnyFunSuite { val bin = hex"53 02020231 04012a 0820eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619 ff0000000143c7a0402016c7ec71663784ff100b6eface1e60a97b92ea9d18b8ece5e558586bc7453828" val encoded = finalPerHopPayloadCodec.encode(FinalTlvPayload(tlvs)).require.bytes - assert(encoded === bin) + assert(encoded == bin) assert(finalPerHopPayloadCodec.decode(bin.bits).require.value == FinalTlvPayload(tlvs)) } test("decode multi-part final per-hop payload") { val multiPart = finalPerHopPayloadCodec.decode(hex"2b 02020231 04012a 0822eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f2836866190451".bits).require.value - assert(multiPart.amount === 561.msat) - assert(multiPart.expiry === CltvExpiry(42)) - assert(multiPart.totalAmount === 1105.msat) - assert(multiPart.paymentSecret === ByteVector32(hex"eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619")) + assert(multiPart.amount == 561.msat) + assert(multiPart.expiry == CltvExpiry(42)) + assert(multiPart.totalAmount == 1105.msat) + assert(multiPart.paymentSecret == ByteVector32(hex"eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619")) val multiPartNoTotalAmount = finalPerHopPayloadCodec.decode(hex"29 02020231 04012a 0820eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619".bits).require.value - assert(multiPartNoTotalAmount.amount === 561.msat) - assert(multiPartNoTotalAmount.expiry === CltvExpiry(42)) - assert(multiPartNoTotalAmount.totalAmount === 561.msat) - assert(multiPartNoTotalAmount.paymentSecret === ByteVector32(hex"eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619")) + assert(multiPartNoTotalAmount.amount == 561.msat) + assert(multiPartNoTotalAmount.expiry == CltvExpiry(42)) + assert(multiPartNoTotalAmount.totalAmount == 561.msat) + assert(multiPartNoTotalAmount.paymentSecret == ByteVector32(hex"eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619")) } test("decode variable-length (tlv) relay per-hop payload missing information") { @@ -200,7 +200,7 @@ class PaymentOnionSpec extends AnyFunSuite { val decoded = channelRelayPerHopPayloadCodec.decode(bin.bits) assert(decoded.isFailure) val Attempt.Failure(err: MissingRequiredTlv) = decoded - assert(err.failureMessage === expectedErr) + assert(err.failureMessage == expectedErr) } } @@ -215,7 +215,7 @@ class PaymentOnionSpec extends AnyFunSuite { val decoded = nodeRelayPerHopPayloadCodec.decode(bin.bits) assert(decoded.isFailure) val Attempt.Failure(err: MissingRequiredTlv) = decoded - assert(err.failureMessage === expectedErr) + assert(err.failureMessage == expectedErr) } } @@ -230,7 +230,7 @@ class PaymentOnionSpec extends AnyFunSuite { val decoded = finalPerHopPayloadCodec.decode(bin.bits) assert(decoded.isFailure) val Attempt.Failure(err: MissingRequiredTlv) = decoded - assert(err.failureMessage === expectedErr) + assert(err.failureMessage == expectedErr) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/RouteBlindingSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/RouteBlindingSpec.scala index 5bbbc114ea..3e7f63fe60 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/RouteBlindingSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/RouteBlindingSpec.scala @@ -27,9 +27,9 @@ class RouteBlindingSpec extends AnyFunSuiteLike { for ((encoded, data) <- payloads) { val decoded = RouteBlindingEncryptedDataCodecs.encryptedDataCodec.decode(encoded.bits).require.value - assert(decoded === data) + assert(decoded == data) val reEncoded = RouteBlindingEncryptedDataCodecs.encryptedDataCodec.encode(data).require.bytes - assert(reEncoded === encoded) + assert(reEncoded == encoded) } } @@ -51,7 +51,7 @@ class RouteBlindingSpec extends AnyFunSuiteLike { val Success((decryptedPayload2, blinding3)) = RouteBlindingEncryptedDataCodecs.decode(nodePrivKeys(2), blinding2, blindedRoute.encryptedPayloads(2)) val Success((decryptedPayload3, blinding4)) = RouteBlindingEncryptedDataCodecs.decode(nodePrivKeys(3), blinding3, blindedRoute.encryptedPayloads(3)) val Success((decryptedPayload4, _)) = RouteBlindingEncryptedDataCodecs.decode(nodePrivKeys(4), blinding4, blindedRoute.encryptedPayloads(4)) - assert(Seq(decryptedPayload0, decryptedPayload1, decryptedPayload2, decryptedPayload3, decryptedPayload4) === payloads.map(_._1)) + assert(Seq(decryptedPayload0, decryptedPayload1, decryptedPayload2, decryptedPayload3, decryptedPayload4) == payloads.map(_._1)) } test("decode invalid encrypted route blinding data") { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/TlvCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/TlvCodecsSpec.scala index c718ab26ff..d13a7b5374 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/TlvCodecsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/TlvCodecsSpec.scala @@ -48,13 +48,13 @@ class TlvCodecsSpec extends AnyFunSuite { for ((bin, lengthPrefixedBin, expected) <- testCases) { val decoded = tu16.decode(bin.bits).require.value val decoded1 = ltu16.decode(lengthPrefixedBin.bits).require.value - assert(decoded === expected) - assert(decoded1 === expected) + assert(decoded == expected) + assert(decoded1 == expected) val encoded = tu16.encode(expected).require.bytes val encoded1 = ltu16.encode(expected).require.bytes - assert(encoded === bin) - assert(encoded1 === lengthPrefixedBin) + assert(encoded == bin) + assert(encoded1 == lengthPrefixedBin) } } @@ -77,13 +77,13 @@ class TlvCodecsSpec extends AnyFunSuite { for ((bin, lengthPrefixedBin, expected) <- testCases) { val decoded = tu32.decode(bin.bits).require.value val decoded1 = ltu32.decode(lengthPrefixedBin.bits).require.value - assert(decoded === expected) - assert(decoded1 === expected) + assert(decoded == expected) + assert(decoded1 == expected) val encoded = tu32.encode(expected).require.bytes val encoded1 = ltu32.encode(expected).require.bytes - assert(encoded === bin) - assert(encoded1 === lengthPrefixedBin) + assert(encoded == bin) + assert(encoded1 == lengthPrefixedBin) } } @@ -118,32 +118,32 @@ class TlvCodecsSpec extends AnyFunSuite { for ((bin, lengthPrefixedBin, expected) <- testCases) { val decoded = tu64.decode(bin.bits).require.value val decoded1 = ltu64.decode(lengthPrefixedBin.bits).require.value - assert(decoded === expected) - assert(decoded1 === expected) + assert(decoded == expected) + assert(decoded1 == expected) val encoded = tu64.encode(expected).require.bytes val encoded1 = ltu64.encode(expected).require.bytes - assert(encoded === bin) - assert(encoded1 === lengthPrefixedBin) + assert(encoded == bin) + assert(encoded1 == lengthPrefixedBin) } } test("encode/decode truncated uint64 overflow") { - assert(tu64overflow.encode(Long.MaxValue).require.toByteVector === hex"7fffffffffffffff") - assert(tu64overflow.decode(hex"7fffffffffffffff".bits).require.value === Long.MaxValue) + assert(tu64overflow.encode(Long.MaxValue).require.toByteVector == hex"7fffffffffffffff") + assert(tu64overflow.decode(hex"7fffffffffffffff".bits).require.value == Long.MaxValue) - assert(tu64overflow.encode(42L).require.toByteVector === hex"2a") - assert(tu64overflow.decode(hex"2a".bits).require.value === 42L) + assert(tu64overflow.encode(42L).require.toByteVector == hex"2a") + assert(tu64overflow.decode(hex"2a".bits).require.value == 42L) assert(tu64overflow.encode(-1L).isFailure) assert(tu64overflow.decode(hex"8000000000000000".bits).isFailure) } test("decode length-prefixed truncated uint64 ignores trailing bytes") { - assert(ltu64.decode(hex"00 1234".bits).require.value === UInt64(0)) - assert(ltu64.decode(hex"01 2a ff".bits).require.value === UInt64(42)) - assert(ltu64.decode(hex"02 0451 1234".bits).require.value === UInt64(1105)) - assert(ltu64.decode(hex"03 010000 0000".bits).require.value === UInt64(65536)) + assert(ltu64.decode(hex"00 1234".bits).require.value == UInt64(0)) + assert(ltu64.decode(hex"01 2a ff".bits).require.value == UInt64(42)) + assert(ltu64.decode(hex"02 0451 1234".bits).require.value == UInt64(1105)) + assert(ltu64.decode(hex"03 010000 0000".bits).require.value == UInt64(65536)) } test("decode invalid truncated integers") { @@ -200,9 +200,9 @@ class TlvCodecsSpec extends AnyFunSuite { for ((bin, expected) <- testCases) { val decoded = testTlvStreamCodec.decode(bin.bits).require.value - assert(decoded === expected) + assert(decoded == expected) val encoded = testTlvStreamCodec.encode(expected).require.bytes - assert(encoded === bin) + assert(encoded == bin) } } @@ -275,7 +275,7 @@ class TlvCodecsSpec extends AnyFunSuite { ) for (testCase <- testCases) { - assert(lengthPrefixedTestTlvStreamCodec.encode(lengthPrefixedTestTlvStreamCodec.decode(testCase.bits).require.value).require.bytes === testCase) + assert(lengthPrefixedTestTlvStreamCodec.encode(lengthPrefixedTestTlvStreamCodec.decode(testCase.bits).require.value).require.bytes == testCase) } } @@ -302,8 +302,8 @@ class TlvCodecsSpec extends AnyFunSuite { test("encode unordered tlv stream (codec should sort appropriately)") { val stream = TlvStream[TestTlv](Seq(TestType254(42), TestType1(42)), Seq(GenericTlv(13, hex"2a"), GenericTlv(11, hex"2b"))) - assert(testTlvStreamCodec.encode(stream).require.toByteVector === hex"01012a 0b012b 0d012a fd00fe02002a") - assert(lengthPrefixedTestTlvStreamCodec.encode(stream).require.toByteVector === hex"0f 01012a 0b012b 0d012a fd00fe02002a") + assert(testTlvStreamCodec.encode(stream).require.toByteVector == hex"01012a 0b012b 0d012a fd00fe02002a") + assert(lengthPrefixedTestTlvStreamCodec.encode(stream).require.toByteVector == hex"0f 01012a 0b012b 0d012a fd00fe02002a") } test("encode/decode custom even tlv records") { @@ -334,9 +334,9 @@ class TlvCodecsSpec extends AnyFunSuite { test("get optional TLV field") { val stream = TlvStream[TestTlv](Seq(TestType254(42), TestType1(42)), Seq(GenericTlv(13, hex"2a"), GenericTlv(11, hex"2b"))) - assert(stream.get[TestType254] === Some(TestType254(42))) - assert(stream.get[TestType1] === Some(TestType1(42))) - assert(stream.get[TestType2] === None) + assert(stream.get[TestType254] == Some(TestType254(42))) + assert(stream.get[TestType1] == Some(TestType1(42))) + assert(stream.get[TestType2] == None) } } diff --git a/eclair-front/src/test/scala/fr/acinq/eclair/router/FrontRouterSpec.scala b/eclair-front/src/test/scala/fr/acinq/eclair/router/FrontRouterSpec.scala index 404d3a2c7f..a72d82c11b 100644 --- a/eclair-front/src/test/scala/fr/acinq/eclair/router/FrontRouterSpec.scala +++ b/eclair-front/src/test/scala/fr/acinq/eclair/router/FrontRouterSpec.scala @@ -89,7 +89,7 @@ class FrontRouterSpec extends TestKit(ActorSystem("test")) with AnyFunSuiteLike pipe1.expectMsg(PeerRoutingMessage(front1, origin1a.nodeId, chan_ab)) pipe1.send(router, PeerRoutingMessage(pipe1.ref, origin1a.nodeId, chan_ab)) - assert(watcher.expectMsgType[ValidateRequest].ann === chan_ab) + assert(watcher.expectMsgType[ValidateRequest].ann == chan_ab) peerConnection1b.send(front1, PeerRoutingMessage(peerConnection1b.ref, origin1b.nodeId, chan_ab)) pipe1.expectNoMessage() @@ -177,7 +177,7 @@ class FrontRouterSpec extends TestKit(ActorSystem("test")) with AnyFunSuiteLike val origin3a = RemoteGossip(peerConnection3a.ref, randomKey().publicKey) peerConnection1a.send(front1, PeerRoutingMessage(peerConnection1a.ref, origin1a.nodeId, chan_ab)) - assert(watcher.expectMsgType[ValidateRequest].ann === chan_ab) + assert(watcher.expectMsgType[ValidateRequest].ann == chan_ab) peerConnection1b.send(front1, PeerRoutingMessage(peerConnection1b.ref, origin1b.nodeId, chan_ab)) peerConnection2a.send(front2, PeerRoutingMessage(peerConnection2a.ref, origin2a.nodeId, chan_ab)) diff --git a/eclair-node/pom.xml b/eclair-node/pom.xml index 6f75cdba85..5f6fe8f97b 100644 --- a/eclair-node/pom.xml +++ b/eclair-node/pom.xml @@ -135,7 +135,7 @@ org.mockito mockito-scala-scalatest_${scala.version.short} - 1.5.9 + 1.17.5 test diff --git a/eclair-node/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala b/eclair-node/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala index 9913651d85..de7394b551 100644 --- a/eclair-node/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala +++ b/eclair-node/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala @@ -631,7 +631,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM assert(status == OK) val response = entityAs[String] val expected = """{"paymentHash":"0000000000000000000000000000000000000000000000000000000000000000","paymentPreimage":"0100000000000000000000000000000000000000000000000000000000000000"}""" - assert(response === expected) + assert(response == expected) } val uuid = UUID.fromString("487da196-a4dc-4b1e-92b4-3e5e905e9f3f") @@ -645,7 +645,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM assert(status == OK) val response = entityAs[String] val expected = """{"type":"payment-sent","id":"487da196-a4dc-4b1e-92b4-3e5e905e9f3f","paymentHash":"0000000000000000000000000000000000000000000000000000000000000000","paymentPreimage":"0100000000000000000000000000000000000000000000000000000000000000","recipientAmount":25,"recipientNodeId":"03af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d0","parts":[{"id":"487da196-a4dc-4b1e-92b4-3e5e905e9f3f","amount":21,"feesPaid":1,"toChannelId":"0000000000000000000000000000000000000000000000000000000000000000","timestamp":{"iso":"2019-03-28T14:45:37.711Z","unix":1553784337}}]}""" - assert(response === expected) + assert(response == expected) } val paymentFailed = PaymentFailed(uuid, ByteVector32.Zeroes, failures = Seq.empty, timestamp = TimestampMilli(1553784963659L)) @@ -658,7 +658,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM assert(status == OK) val response = entityAs[String] val expected = """{"type":"payment-failed","id":"487da196-a4dc-4b1e-92b4-3e5e905e9f3f","paymentHash":"0000000000000000000000000000000000000000000000000000000000000000","failures":[],"timestamp":{"iso":"2019-03-28T14:56:03.659Z","unix":1553784963}}""" - assert(response === expected) + assert(response == expected) } } @@ -1121,55 +1121,55 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM check { val pf = PaymentFailed(fixedUUID, ByteVector32.Zeroes, failures = Seq.empty, timestamp = TimestampMilli(1553784963659L)) val expectedSerializedPf = """{"type":"payment-failed","id":"487da196-a4dc-4b1e-92b4-3e5e905e9f3f","paymentHash":"0000000000000000000000000000000000000000000000000000000000000000","failures":[],"timestamp":{"iso":"2019-03-28T14:56:03.659Z","unix":1553784963}}""" - assert(serialization.write(pf) === expectedSerializedPf) + assert(serialization.write(pf) == expectedSerializedPf) system.eventStream.publish(pf) wsClient.expectMessage(expectedSerializedPf) val ps = PaymentSent(fixedUUID, ByteVector32.Zeroes, ByteVector32.One, 25 msat, aliceNodeId, Seq(PaymentSent.PartialPayment(fixedUUID, 21 msat, 1 msat, ByteVector32.Zeroes, None, TimestampMilli(1553784337711L)))) val expectedSerializedPs = """{"type":"payment-sent","id":"487da196-a4dc-4b1e-92b4-3e5e905e9f3f","paymentHash":"0000000000000000000000000000000000000000000000000000000000000000","paymentPreimage":"0100000000000000000000000000000000000000000000000000000000000000","recipientAmount":25,"recipientNodeId":"03af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d0","parts":[{"id":"487da196-a4dc-4b1e-92b4-3e5e905e9f3f","amount":21,"feesPaid":1,"toChannelId":"0000000000000000000000000000000000000000000000000000000000000000","timestamp":{"iso":"2019-03-28T14:45:37.711Z","unix":1553784337}}]}""" - assert(serialization.write(ps) === expectedSerializedPs) + assert(serialization.write(ps) == expectedSerializedPs) system.eventStream.publish(ps) wsClient.expectMessage(expectedSerializedPs) val prel = ChannelPaymentRelayed(21 msat, 20 msat, ByteVector32.Zeroes, ByteVector32.Zeroes, ByteVector32.One, TimestampMilli(1553784963659L)) val expectedSerializedPrel = """{"type":"payment-relayed","amountIn":21,"amountOut":20,"paymentHash":"0000000000000000000000000000000000000000000000000000000000000000","fromChannelId":"0000000000000000000000000000000000000000000000000000000000000000","toChannelId":"0100000000000000000000000000000000000000000000000000000000000000","timestamp":{"iso":"2019-03-28T14:56:03.659Z","unix":1553784963}}""" - assert(serialization.write(prel) === expectedSerializedPrel) + assert(serialization.write(prel) == expectedSerializedPrel) system.eventStream.publish(prel) wsClient.expectMessage(expectedSerializedPrel) val ptrel = TrampolinePaymentRelayed(ByteVector32.Zeroes, Seq(PaymentRelayed.Part(21 msat, ByteVector32.Zeroes)), Seq(PaymentRelayed.Part(8 msat, ByteVector32.Zeroes), PaymentRelayed.Part(10 msat, ByteVector32.One)), bobNodeId, 17 msat, TimestampMilli(1553784963659L)) val expectedSerializedPtrel = """{"type":"trampoline-payment-relayed","paymentHash":"0000000000000000000000000000000000000000000000000000000000000000","incoming":[{"amount":21,"channelId":"0000000000000000000000000000000000000000000000000000000000000000"}],"outgoing":[{"amount":8,"channelId":"0000000000000000000000000000000000000000000000000000000000000000"},{"amount":10,"channelId":"0100000000000000000000000000000000000000000000000000000000000000"}],"nextTrampolineNodeId":"039dc0e0b1d25905e44fdf6f8e89755a5e219685840d0bc1d28d3308f9628a3585","nextTrampolineAmount":17,"timestamp":{"iso":"2019-03-28T14:56:03.659Z","unix":1553784963}}""" - assert(serialization.write(ptrel) === expectedSerializedPtrel) + assert(serialization.write(ptrel) == expectedSerializedPtrel) system.eventStream.publish(ptrel) wsClient.expectMessage(expectedSerializedPtrel) val precv = PaymentReceived(ByteVector32.Zeroes, Seq(PaymentReceived.PartialPayment(21 msat, ByteVector32.Zeroes, TimestampMilli(1553784963659L)))) val expectedSerializedPrecv = """{"type":"payment-received","paymentHash":"0000000000000000000000000000000000000000000000000000000000000000","parts":[{"amount":21,"fromChannelId":"0000000000000000000000000000000000000000000000000000000000000000","timestamp":{"iso":"2019-03-28T14:56:03.659Z","unix":1553784963}}]}""" - assert(serialization.write(precv) === expectedSerializedPrecv) + assert(serialization.write(precv) == expectedSerializedPrecv) system.eventStream.publish(precv) wsClient.expectMessage(expectedSerializedPrecv) val pset = PaymentSettlingOnChain(fixedUUID, 21 msat, ByteVector32.One, timestamp = TimestampMilli(1553785442676L)) val expectedSerializedPset = """{"type":"payment-settling-onchain","id":"487da196-a4dc-4b1e-92b4-3e5e905e9f3f","amount":21,"paymentHash":"0100000000000000000000000000000000000000000000000000000000000000","timestamp":{"iso":"2019-03-28T15:04:02.676Z","unix":1553785442}}""" - assert(serialization.write(pset) === expectedSerializedPset) + assert(serialization.write(pset) == expectedSerializedPset) system.eventStream.publish(pset) wsClient.expectMessage(expectedSerializedPset) val chcr = ChannelCreated(system.deadLetters, system.deadLetters, bobNodeId, isInitiator = true, ByteVector32.One, FeeratePerKw(25 sat), Some(FeeratePerKw(20 sat))) val expectedSerializedChcr = """{"type":"channel-opened","remoteNodeId":"039dc0e0b1d25905e44fdf6f8e89755a5e219685840d0bc1d28d3308f9628a3585","isInitiator":true,"temporaryChannelId":"0100000000000000000000000000000000000000000000000000000000000000","commitTxFeeratePerKw":25,"fundingTxFeeratePerKw":20}""" - assert(serialization.write(chcr) === expectedSerializedChcr) + assert(serialization.write(chcr) == expectedSerializedChcr) system.eventStream.publish(chcr) wsClient.expectMessage(expectedSerializedChcr) val chsc = ChannelStateChanged(system.deadLetters, ByteVector32.One, system.deadLetters, bobNodeId, OFFLINE, NORMAL, null) val expectedSerializedChsc = """{"type":"channel-state-changed","channelId":"0100000000000000000000000000000000000000000000000000000000000000","remoteNodeId":"039dc0e0b1d25905e44fdf6f8e89755a5e219685840d0bc1d28d3308f9628a3585","previousState":"OFFLINE","currentState":"NORMAL"}""" - assert(serialization.write(chsc) === expectedSerializedChsc) + assert(serialization.write(chsc) == expectedSerializedChsc) system.eventStream.publish(chsc) wsClient.expectMessage(expectedSerializedChsc) val chcl = ChannelClosed(system.deadLetters, ByteVector32.One, Closing.NextRemoteClose(null, null), null) val expectedSerializedChcl = """{"type":"channel-closed","channelId":"0100000000000000000000000000000000000000000000000000000000000000","closingType":"NextRemoteClose"}""" - assert(serialization.write(chcl) === expectedSerializedChcl) + assert(serialization.write(chcl) == expectedSerializedChcl) system.eventStream.publish(chcl) wsClient.expectMessage(expectedSerializedChcl) @@ -1181,7 +1181,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM GenericTlv(UInt64(5), hex"1111") ))), Some(hex"2222")) val expectedSerializedMsgrcv = """{"type":"onion-message-received","pathId":"2222","encodedReplyPath":"039dc0e0b1d25905e44fdf6f8e89755a5e219685840d0bc1d28d3308f9628a358502eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619020303f91e620504cde242df38d04599d8b4d4c555149cc742a5f12de452cbdd400013126a26221759247584d704b382a5789f1d8c5a","replyPath":{"introductionNodeId":"039dc0e0b1d25905e44fdf6f8e89755a5e219685840d0bc1d28d3308f9628a3585","blindingKey":"02eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619","blindedNodes":[{"blindedPublicKey":"020303f91e620504cde242df38d04599d8b4d4c555149cc742a5f12de452cbdd40","encryptedPayload":"126a26221759247584d704b382a5789f1d8c5a"}]},"unknownTlvs":{"5":"1111"}}""" - assert(serialization.write(msgrcv) === expectedSerializedMsgrcv) + assert(serialization.write(msgrcv) == expectedSerializedMsgrcv) system.eventStream.publish(msgrcv) wsClient.expectMessage(expectedSerializedMsgrcv) } diff --git a/pom.xml b/pom.xml index f6272e3e76..48f3e25e5f 100644 --- a/pom.xml +++ b/pom.xml @@ -132,7 +132,7 @@ net.alchim31.maven scala-maven-plugin - 3.4.2 + 4.6.1 -feature @@ -274,7 +274,7 @@ org.scalatest scalatest_${scala.version.short} - 3.1.1 + 3.2.12 test From 47c5b95eaa7ee9001c4853cf4e4b0269e685f77d Mon Sep 17 00:00:00 2001 From: rorp Date: Mon, 6 Jun 2022 01:03:44 -0700 Subject: [PATCH 082/121] Drop support for Tor v2 hidden services (#2296) Tor v2 addresses have been officially deprecated by the Tor team and removed from the lightning specification in https://github.com/lightning/bolts/pull/940 --- docs/Tor.md | 16 +--- docs/release-notes/eclair-vnext.md | 7 +- eclair-core/src/main/resources/reference.conf | 1 - .../main/scala/fr/acinq/eclair/Setup.scala | 2 - .../acinq/eclair/tor/TorProtocolHandler.scala | 75 ++++++------------- .../eclair/tor/TorProtocolHandlerSpec.scala | 49 ++++-------- 6 files changed, 45 insertions(+), 105 deletions(-) diff --git a/docs/Tor.md b/docs/Tor.md index 95ffea146d..377e43a610 100644 --- a/docs/Tor.md +++ b/docs/Tor.md @@ -1,5 +1,7 @@ ## How to Use Tor with Eclair +Current supported version of Tor is 0.3.3.6 or higher. + ### Installing Tor on your node #### Linux: @@ -100,21 +102,7 @@ eclair-cli getinfo ``` Eclair saves the Tor endpoint's private key in `~/.eclair/tor.dat`, so that it can recreate the endpoint address after a restart. If you remove the private key Eclair will regenerate the endpoint address. - -There are two possible values for `protocol-version`: - -``` -eclair.tor.protocol-version = "v3" -``` - -value | description ---------|--------------------------------------------------------- - v2 | set up a Tor hidden service version 2 end point - v3 | set up a Tor hidden service version 3 end point (default) -Tor protocol v3 (supported by Tor version 0.3.3.6 and higher) is backwards compatible and supports -both v2 and v3 addresses. - For increased privacy do not advertise your IP address in the `server.public-ips` list, and set your binding IP to `localhost`: ``` eclair.server.binding-ip = "127.0.0.1" diff --git a/docs/release-notes/eclair-vnext.md b/docs/release-notes/eclair-vnext.md index 97bd4cc0e1..31392c7d38 100644 --- a/docs/release-notes/eclair-vnext.md +++ b/docs/release-notes/eclair-vnext.md @@ -4,7 +4,12 @@ ## Major changes - +Dropped support for version 2 of Tor protocol. That means + +- Eclair can't open control connection to Tor daemon version 0.3.3.5 and earlier anymore +- Eclair can't create hidden services for Tor protocol v2 with newer versions of Tor daemon + +IMPORTANT: You'll need to upgrade your Tor daemon if for some reason you still use Tor v0.3.3.5 or earlier before upgrading to this release. ### API changes diff --git a/eclair-core/src/main/resources/reference.conf b/eclair-core/src/main/resources/reference.conf index 56b1961eba..ae606c4774 100644 --- a/eclair-core/src/main/resources/reference.conf +++ b/eclair-core/src/main/resources/reference.conf @@ -315,7 +315,6 @@ eclair { tor { enabled = false - protocol = "v3" // v2, v3 auth = "password" // safecookie, password password = "foobar" // used when auth=password host = "127.0.0.1" diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala index 446032024f..ecbe266f0e 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala @@ -43,7 +43,6 @@ import fr.acinq.eclair.payment.receive.PaymentHandler import fr.acinq.eclair.payment.relay.Relayer import fr.acinq.eclair.payment.send.{Autoprobe, PaymentInitiator} import fr.acinq.eclair.router._ -import fr.acinq.eclair.tor.TorProtocolHandler.OnionServiceVersion import fr.acinq.eclair.tor.{Controller, TorProtocolHandler} import fr.acinq.eclair.wire.protocol.NodeAddress import grizzled.slf4j.Logging @@ -351,7 +350,6 @@ class Setup(val datadir: File, case "safecookie" => TorProtocolHandler.SafeCookie() } val protocolHandlerProps = TorProtocolHandler.props( - version = OnionServiceVersion(config.getString("tor.protocol")), authentication = auth, privateKeyPath = new File(datadir, config.getString("tor.private-key-file")).toPath, virtualPort = config.getInt("server.port"), diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/tor/TorProtocolHandler.scala b/eclair-core/src/main/scala/fr/acinq/eclair/tor/TorProtocolHandler.scala index c6f1584ded..b775280649 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/tor/TorProtocolHandler.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/tor/TorProtocolHandler.scala @@ -16,20 +16,19 @@ package fr.acinq.eclair.tor -import java.nio.file.attribute.PosixFilePermissions -import java.nio.file.{Files, Path, Paths} -import java.util - import akka.actor.{Actor, ActorLogging, ActorRef, Props, Stash} import akka.io.Tcp.Connected import akka.util.ByteString -import fr.acinq.eclair.tor.TorProtocolHandler.{Authentication, OnionServiceVersion} -import fr.acinq.eclair.wire.protocol.{NodeAddress, Tor2, Tor3} -import javax.crypto.Mac -import javax.crypto.spec.SecretKeySpec +import fr.acinq.eclair.tor.TorProtocolHandler.Authentication +import fr.acinq.eclair.wire.protocol.{NodeAddress, Tor3} import scodec.bits.Bases.Alphabets import scodec.bits.ByteVector +import java.nio.file.attribute.PosixFilePermissions +import java.nio.file.{Files, Path, Paths} +import java.util +import javax.crypto.Mac +import javax.crypto.spec.SecretKeySpec import scala.concurrent.Promise import scala.util.Try @@ -40,15 +39,13 @@ case class TorException(private val msg: String) extends RuntimeException(s"Tor * * Specification: https://gitweb.torproject.org/torspec.git/tree/control-spec.txt * - * @param onionServiceVersion v2 or v3 * @param authentication Tor controller auth mechanism (password or safecookie) * @param privateKeyPath path to a file that contains a Tor private key * @param virtualPort port for the public hidden service (typically 9735) * @param targets address of our protected server (format [host:]port), 127.0.0.1:[[virtualPort]] if empty * @param onionAdded a Promise to track creation of the endpoint */ -class TorProtocolHandler(onionServiceVersion: OnionServiceVersion, - authentication: Authentication, +class TorProtocolHandler(authentication: Authentication, privateKeyPath: Path, virtualPort: Int, targets: Seq[String], @@ -74,8 +71,8 @@ class TorProtocolHandler(onionServiceVersion: OnionServiceVersion, val methods: String = res.getOrElse("METHODS", throw TorException("auth methods not found")) val torVersion = unquote(res.getOrElse("Tor", throw TorException("version not found"))) log.info(s"Tor version $torVersion") - if (!OnionServiceVersion.isCompatible(onionServiceVersion, torVersion)) { - throw TorException(s"version $torVersion does not support onion service $onionServiceVersion") + if (!isCompatible(torVersion)) { + throw TorException(s"unsupported Tor version: $torVersion") } if (!Authentication.isCompatible(authentication, methods)) { throw TorException(s"cannot use authentication '$authentication', supported methods are '$methods'") @@ -116,10 +113,7 @@ class TorProtocolHandler(onionServiceVersion: OnionServiceVersion, val res = readResponse(data) if (ok(res)) { val serviceId = processOnionResponse(parseResponse(res)) - address = Some(onionServiceVersion match { - case V2 => Tor2(serviceId, virtualPort) - case V3 => Tor3(serviceId, virtualPort) - }) + address = Some(Tor3(serviceId, virtualPort)) onionAdded.foreach(_.success(address.get)) log.debug("Onion address: {}", address.get) } @@ -151,10 +145,7 @@ class TorProtocolHandler(onionServiceVersion: OnionServiceVersion, if (privateKeyPath.toFile.exists()) { readString(privateKeyPath) } else { - onionServiceVersion match { - case V2 => "NEW:RSA1024" - case V3 => "NEW:ED25519-V3" - } + "NEW:ED25519-V3" } } @@ -190,48 +181,30 @@ class TorProtocolHandler(onionServiceVersion: OnionServiceVersion, } object TorProtocolHandler { - def props(version: OnionServiceVersion, - authentication: Authentication, + def props(authentication: Authentication, privateKeyPath: Path, virtualPort: Int, targets: Seq[String] = Seq(), onionAdded: Option[Promise[NodeAddress]] = None ): Props = - Props(new TorProtocolHandler(version, authentication, privateKeyPath, virtualPort, targets, onionAdded)) + Props(new TorProtocolHandler(authentication, privateKeyPath, virtualPort, targets, onionAdded)) // those are defined in the spec private val ServerKey = ByteVector.view("Tor safe cookie authentication server-to-controller hash".getBytes()) private val ClientKey = ByteVector.view("Tor safe cookie authentication controller-to-server hash".getBytes()) - // @formatter:off - sealed trait OnionServiceVersion - case object V2 extends OnionServiceVersion - case object V3 extends OnionServiceVersion - // @formatter:on - - object OnionServiceVersion { - def apply(s: String): OnionServiceVersion = s match { - case "v2" | "V2" => V2 - case "v3" | "V3" => V3 - case _ => throw TorException(s"unknown protocol version `$s`") - } - - def isCompatible(onionServiceVersion: OnionServiceVersion, torVersion: String): Boolean = - onionServiceVersion match { - case V2 => true - case V3 => torVersion - .split("\\.") - .map(_.split('-').head) // remove non-numeric symbols at the end of the last number (rc, beta, alpha, etc.) - .map(d => Try(d.toInt).getOrElse(0)) - .zipAll(List(0, 3, 3, 6), 0, 0) // min version for v3 is 0.3.3.6 - .foldLeft(Option.empty[Boolean]) { // compare subversion by subversion starting from the left - case (Some(res), _) => Some(res) // we stop the comparison as soon as there is a difference - case (None, (v, vref)) => if (v > vref) Some(true) else if (v < vref) Some(false) else None - } - .getOrElse(true) // if version == 0.3.3.6 then result will be None + private[tor] def isCompatible(torVersion: String): Boolean = + torVersion + .split("\\.") + .map(_.split('-').head) // remove non-numeric symbols at the end of the last number (rc, beta, alpha, etc.) + .map(d => Try(d.toInt).getOrElse(0)) + .zipAll(List(0, 3, 3, 6), 0, 0) // min version for v3 is 0.3.3.6 + .foldLeft(Option.empty[Boolean]) { // compare subversion by subversion starting from the left + case (Some(res), _) => Some(res) // we stop the comparison as soon as there is a difference + case (None, (v, vref)) => if (v > vref) Some(true) else if (v < vref) Some(false) else None } - } + .getOrElse(true) // if version == 0.3.3.6 then result will be None // @formatter:off sealed trait Authentication diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/tor/TorProtocolHandlerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/tor/TorProtocolHandlerSpec.scala index 056866728d..995c529b11 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/tor/TorProtocolHandlerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/tor/TorProtocolHandlerSpec.scala @@ -54,7 +54,6 @@ class TorProtocolHandlerSpec extends TestKitBaseClass val promiseOnionAddress = Promise[NodeAddress]() val protocolHandlerProps = TorProtocolHandler.props( - version = OnionServiceVersion("v2"), authentication = Password(PASSWORD), privateKeyPath = PkFilePath, virtualPort = 9999, @@ -70,7 +69,6 @@ class TorProtocolHandlerSpec extends TestKitBaseClass val promiseOnionAddress = Promise[NodeAddress]() val protocolHandler = TestActorRef(props( - version = OnionServiceVersion("v2"), authentication = Password(PASSWORD), privateKeyPath = PkFilePath, virtualPort = 9999, @@ -86,31 +84,15 @@ class TorProtocolHandlerSpec extends TestKitBaseClass "250 OK\r\n" ) - expectMsg(ByteString(s"""AUTHENTICATE "$PASSWORD"\r\n""")) - protocolHandler ! ByteString( - "250 OK\r\n" - ) - - expectMsg(ByteString("ADD_ONION NEW:RSA1024 Port=9999,9999\r\n")) - protocolHandler ! ByteString( - "250-ServiceID=z4zif3fy7fe7bpg3\r\n" + - "250-PrivateKey=RSA1024:private-key\r\n" + - "250 OK\r\n" - ) - protocolHandler ! GetOnionAddress - expectMsg(Some(Tor2("z4zif3fy7fe7bpg3", 9999))) + awaitCond(promiseOnionAddress.isCompleted) - val address = Await.result(promiseOnionAddress.future, 3 seconds) - assert(address == Tor2("z4zif3fy7fe7bpg3", 9999)) - - assert(readString(PkFilePath) == "RSA1024:private-key") + assertThrows[TorException](Await.result(promiseOnionAddress.future, Duration.Inf)) } test("happy path v3") { val promiseOnionAddress = Promise[NodeAddress]() val protocolHandler = TestActorRef(props( - version = OnionServiceVersion("v3"), authentication = Password(PASSWORD), privateKeyPath = PkFilePath, virtualPort = 9999, @@ -148,20 +130,18 @@ class TorProtocolHandlerSpec extends TestKitBaseClass } test("v2/v3 compatibility check against tor version") { - assert(OnionServiceVersion.isCompatible(V3, "0.3.3.6")) - assert(!OnionServiceVersion.isCompatible(V3, "0.3.3.5")) - assert(OnionServiceVersion.isCompatible(V3, "0.3.3.6-devel")) - assert(OnionServiceVersion.isCompatible(V3, "0.4")) - assert(!OnionServiceVersion.isCompatible(V3, "0.2")) - assert(OnionServiceVersion.isCompatible(V3, "0.5.1.2.3.4")) - + assert(isCompatible("0.3.3.6")) + assert(!isCompatible("0.3.3.5")) + assert(isCompatible("0.3.3.6-devel")) + assert(isCompatible("0.4")) + assert(!isCompatible("0.2")) + assert(isCompatible("0.5.1.2.3.4")) } test("authentication method errors") { val promiseOnionAddress = Promise[NodeAddress]() val protocolHandler = TestActorRef(props( - version = OnionServiceVersion("v2"), authentication = Password(PASSWORD), privateKeyPath = PkFilePath, virtualPort = 9999, @@ -173,7 +153,7 @@ class TorProtocolHandlerSpec extends TestKitBaseClass protocolHandler ! ByteString( "250-PROTOCOLINFO 1\r\n" + "250-AUTH METHODS=COOKIE,SAFECOOKIE COOKIEFILE=\"" + CookieFilePath + "\"\r\n" + - "250-VERSION Tor=\"0.3.3.5\"\r\n" + + "250-VERSION Tor=\"0.3.3.6\"\r\n" + "250 OK\r\n" ) @@ -188,7 +168,6 @@ class TorProtocolHandlerSpec extends TestKitBaseClass Files.write(CookieFilePath, fr.acinq.eclair.randomBytes32().toArray) val protocolHandler = TestActorRef(props( - version = OnionServiceVersion("v2"), authentication = SafeCookie(ClientNonce), privateKeyPath = PkFilePath, virtualPort = 9999, @@ -200,7 +179,7 @@ class TorProtocolHandlerSpec extends TestKitBaseClass protocolHandler ! ByteString( "250-PROTOCOLINFO 1\r\n" + "250-AUTH METHODS=COOKIE,SAFECOOKIE COOKIEFILE=\"" + CookieFilePath + "\"\r\n" + - "250-VERSION Tor=\"0.3.3.5\"\r\n" + + "250-VERSION Tor=\"0.3.3.6\"\r\n" + "250 OK\r\n" ) @@ -221,7 +200,6 @@ class TorProtocolHandlerSpec extends TestKitBaseClass Files.write(CookieFilePath, AuthCookie.toArray) val protocolHandler = TestActorRef(props( - version = OnionServiceVersion("v2"), authentication = SafeCookie(ClientNonce), privateKeyPath = PkFilePath, virtualPort = 9999, @@ -233,7 +211,7 @@ class TorProtocolHandlerSpec extends TestKitBaseClass protocolHandler ! ByteString( "250-PROTOCOLINFO 1\r\n" + "250-AUTH METHODS=COOKIE,SAFECOOKIE COOKIEFILE=\"" + CookieFilePath + "\"\r\n" + - "250-VERSION Tor=\"0.3.3.5\"\r\n" + + "250-VERSION Tor=\"0.3.3.6\"\r\n" + "250 OK\r\n" ) @@ -258,7 +236,6 @@ class TorProtocolHandlerSpec extends TestKitBaseClass Files.write(CookieFilePath, AuthCookie.toArray) val protocolHandler = TestActorRef(props( - version = OnionServiceVersion("v2"), authentication = SafeCookie(ClientNonce), privateKeyPath = PkFilePath, virtualPort = 9999, @@ -270,7 +247,7 @@ class TorProtocolHandlerSpec extends TestKitBaseClass protocolHandler ! ByteString( "250-PROTOCOLINFO 1\r\n" + "250-AUTH METHODS=COOKIE,SAFECOOKIE COOKIEFILE=\"" + CookieFilePath + "\"\r\n" + - "250-VERSION Tor=\"0.3.3.5\"\r\n" + + "250-VERSION Tor=\"0.3.3.6\"\r\n" + "250 OK\r\n" ) @@ -284,7 +261,7 @@ class TorProtocolHandlerSpec extends TestKitBaseClass "250 OK\r\n" ) - expectMsg(ByteString("ADD_ONION NEW:RSA1024 Port=9999,9999\r\n")) + expectMsg(ByteString("ADD_ONION NEW:ED25519-V3 Port=9999,9999\r\n")) protocolHandler ! ByteString( "513 Invalid argument\r\n" ) From ecbec93dfe15a76eef94aa54783d908d2829fb44 Mon Sep 17 00:00:00 2001 From: Bastien Teinturier <31281497+t-bast@users.noreply.github.com> Date: Mon, 6 Jun 2022 13:51:44 +0200 Subject: [PATCH 083/121] Add traits for permanent channel features (#2282) We create new feature traits for permanent channel features, split between features that are part of the channel type and features that are not. --- .../main/scala/fr/acinq/eclair/Features.scala | 24 +++++++++--- .../eclair/channel/ChannelFeatures.scala | 39 ++++++++++--------- .../channel/version0/ChannelTypes0.scala | 6 +-- .../channel/version3/ChannelCodecs3.scala | 6 +-- .../eclair/channel/ChannelFeaturesSpec.scala | 35 ++++++++++------- 5 files changed, 65 insertions(+), 45 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala index 3138f995ab..bb5e5575c9 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala @@ -56,6 +56,18 @@ trait InitFeature extends Feature trait NodeFeature extends Feature /** Feature that should be advertised in invoices. */ trait InvoiceFeature extends Feature +/** + * Feature negotiated when opening a channel that will apply for all of the channel's lifetime. + * This doesn't include features that can be safely activated/deactivated without impacting the channel's operation such + * as option_dataloss_protect or option_shutdown_anysegwit. + */ +trait PermanentChannelFeature extends InitFeature // <- not in the spec +/** + * Permanent channel feature negotiated in the channel type. Those features take precedence over permanent channel + * features negotiated in init messages. For example, if the channel type is option_static_remotekey, then even if + * the option_anchor_outputs feature is supported by both peers, it won't apply to the channel. + */ +trait ChannelTypeFeature extends PermanentChannelFeature // @formatter:on case class UnknownFeature(bitIndex: Int) @@ -158,7 +170,7 @@ object Features { val mandatory = 2 } - case object UpfrontShutdownScript extends Feature with InitFeature with NodeFeature { + case object UpfrontShutdownScript extends Feature with InitFeature with NodeFeature with PermanentChannelFeature { val rfcName = "option_upfront_shutdown_script" val mandatory = 4 } @@ -178,7 +190,7 @@ object Features { val mandatory = 10 } - case object StaticRemoteKey extends Feature with InitFeature with NodeFeature { + case object StaticRemoteKey extends Feature with InitFeature with NodeFeature with ChannelTypeFeature { val rfcName = "option_static_remotekey" val mandatory = 12 } @@ -193,17 +205,17 @@ object Features { val mandatory = 16 } - case object Wumbo extends Feature with InitFeature with NodeFeature { + case object Wumbo extends Feature with InitFeature with NodeFeature with PermanentChannelFeature { val rfcName = "option_support_large_channel" val mandatory = 18 } - case object AnchorOutputs extends Feature with InitFeature with NodeFeature { + case object AnchorOutputs extends Feature with InitFeature with NodeFeature with ChannelTypeFeature { val rfcName = "option_anchor_outputs" val mandatory = 20 } - case object AnchorOutputsZeroFeeHtlcTx extends Feature with InitFeature with NodeFeature { + case object AnchorOutputsZeroFeeHtlcTx extends Feature with InitFeature with NodeFeature with ChannelTypeFeature { val rfcName = "option_anchors_zero_fee_htlc_tx" val mandatory = 22 } @@ -213,7 +225,7 @@ object Features { val mandatory = 26 } - case object DualFunding extends Feature with InitFeature with NodeFeature { + case object DualFunding extends Feature with InitFeature with NodeFeature with PermanentChannelFeature { val rfcName = "option_dual_fund" val mandatory = 28 } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelFeatures.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelFeatures.scala index 9881aa5869..5ef17a4a11 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelFeatures.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelFeatures.scala @@ -17,7 +17,7 @@ package fr.acinq.eclair.channel import fr.acinq.eclair.transactions.Transactions.{CommitmentFormat, DefaultCommitmentFormat, UnsafeLegacyAnchorOutputsCommitmentFormat, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat} -import fr.acinq.eclair.{FeatureSupport, Features, InitFeature, Feature} +import fr.acinq.eclair.{ChannelTypeFeature, FeatureSupport, Features, InitFeature, PermanentChannelFeature} /** * Created by t-bast on 24/06/2021. @@ -28,7 +28,7 @@ import fr.acinq.eclair.{FeatureSupport, Features, InitFeature, Feature} * Even if one of these features is later disabled at the connection level, it will still apply to the channel until the * channel is upgraded or closed. */ -case class ChannelFeatures(features: Set[Feature]) { +case class ChannelFeatures(features: Set[PermanentChannelFeature]) { val channelType: SupportedChannelType = { if (hasFeature(Features.AnchorOutputsZeroFeeHtlcTx)) { @@ -45,7 +45,7 @@ case class ChannelFeatures(features: Set[Feature]) { val paysDirectlyToWallet: Boolean = channelType.paysDirectlyToWallet val commitmentFormat: CommitmentFormat = channelType.commitmentFormat - def hasFeature(feature: Feature): Boolean = features.contains(feature) + def hasFeature(feature: PermanentChannelFeature): Boolean = features.contains(feature) override def toString: String = features.mkString(",") @@ -53,15 +53,16 @@ case class ChannelFeatures(features: Set[Feature]) { object ChannelFeatures { - def apply(features: Feature*): ChannelFeatures = ChannelFeatures(Set.from(features)) + def apply(features: PermanentChannelFeature*): ChannelFeatures = ChannelFeatures(Set.from(features)) /** Enrich the channel type with other permanent features that will be applied to the channel. */ - def apply(channelType: ChannelType, localFeatures: Features[InitFeature], remoteFeatures: Features[InitFeature]): ChannelFeatures = { - // NB: we don't include features that can be safely activated/deactivated without impacting the channel's operation, - // such as option_dataloss_protect or option_shutdown_anysegwit. - val availableFeatures = Seq(Features.Wumbo, Features.UpfrontShutdownScript).filter(f => Features.canUseFeature(localFeatures, remoteFeatures, f)) - val allFeatures = channelType.features.toSeq ++ availableFeatures - ChannelFeatures(allFeatures: _*) + def apply(channelType: SupportedChannelType, localFeatures: Features[InitFeature], remoteFeatures: Features[InitFeature]): ChannelFeatures = { + val additionalPermanentFeatures = Features.knownFeatures.collect { + case _: ChannelTypeFeature => None // channel-type features are negotiated in the channel-type, we ignore them in the init + case f: PermanentChannelFeature if Features.canUseFeature(localFeatures, remoteFeatures, f) => Some(f) // we only consider permanent channel features + }.flatten + val allPermanentFeatures = channelType.features.toSeq ++ additionalPermanentFeatures + ChannelFeatures(allPermanentFeatures: _*) } } @@ -69,10 +70,13 @@ object ChannelFeatures { /** A channel type is a specific set of even feature bits that represent persistent channel features as defined in Bolt 2. */ sealed trait ChannelType { /** Features representing that channel type. */ - def features: Set[InitFeature] + def features: Set[_ <: InitFeature] } sealed trait SupportedChannelType extends ChannelType { + /** Known channel-type features */ + override def features: Set[ChannelTypeFeature] + /** True if our main output in the remote commitment is directly sent (without any delay) to one of our wallet addresses. */ def paysDirectlyToWallet: Boolean @@ -84,25 +88,25 @@ object ChannelTypes { // @formatter:off case object Standard extends SupportedChannelType { - override def features: Set[InitFeature] = Set.empty + override def features: Set[ChannelTypeFeature] = Set.empty override def paysDirectlyToWallet: Boolean = false override def commitmentFormat: CommitmentFormat = DefaultCommitmentFormat override def toString: String = "standard" } case object StaticRemoteKey extends SupportedChannelType { - override def features: Set[InitFeature] = Set(Features.StaticRemoteKey) + override def features: Set[ChannelTypeFeature] = Set(Features.StaticRemoteKey) override def paysDirectlyToWallet: Boolean = true override def commitmentFormat: CommitmentFormat = DefaultCommitmentFormat override def toString: String = "static_remotekey" } case object AnchorOutputs extends SupportedChannelType { - override def features: Set[InitFeature] = Set(Features.StaticRemoteKey, Features.AnchorOutputs) + override def features: Set[ChannelTypeFeature] = Set(Features.StaticRemoteKey, Features.AnchorOutputs) override def paysDirectlyToWallet: Boolean = false override def commitmentFormat: CommitmentFormat = UnsafeLegacyAnchorOutputsCommitmentFormat override def toString: String = "anchor_outputs" } case object AnchorOutputsZeroFeeHtlcTx extends SupportedChannelType { - override def features: Set[InitFeature] = Set(Features.StaticRemoteKey, Features.AnchorOutputsZeroFeeHtlcTx) + override def features: Set[ChannelTypeFeature] = Set(Features.StaticRemoteKey, Features.AnchorOutputsZeroFeeHtlcTx) override def paysDirectlyToWallet: Boolean = false override def commitmentFormat: CommitmentFormat = ZeroFeeHtlcTxAnchorOutputsCommitmentFormat override def toString: String = "anchor_outputs_zero_fee_htlc_tx" @@ -113,14 +117,13 @@ object ChannelTypes { } // @formatter:on - private val features2ChannelType: Map[Features[InitFeature], SupportedChannelType] = Set(Standard, StaticRemoteKey, AnchorOutputs, AnchorOutputsZeroFeeHtlcTx) - .map(channelType => Features[InitFeature](channelType.features.map(_ -> FeatureSupport.Mandatory).toMap) -> channelType) + private val features2ChannelType: Map[Features[_ <: InitFeature], SupportedChannelType] = Set(Standard, StaticRemoteKey, AnchorOutputs, AnchorOutputsZeroFeeHtlcTx) + .map(channelType => Features(channelType.features.map(_ -> FeatureSupport.Mandatory).toMap) -> channelType) .toMap // NB: Bolt 2: features must exactly match in order to identify a channel type. def fromFeatures(features: Features[InitFeature]): ChannelType = features2ChannelType.getOrElse(features, UnsupportedChannelType(features)) - /** Pick the channel type based on local and remote feature bits, as defined by the spec. */ def defaultFromFeatures(localFeatures: Features[InitFeature], remoteFeatures: Features[InitFeature]): SupportedChannelType = { if (Features.canUseFeature(localFeatures, remoteFeatures, Features.AnchorOutputsZeroFeeHtlcTx)) { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelTypes0.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelTypes0.scala index 785c869137..16b15c7bc3 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelTypes0.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelTypes0.scala @@ -23,7 +23,7 @@ import fr.acinq.eclair.channel._ import fr.acinq.eclair.crypto.ShaChain import fr.acinq.eclair.transactions.CommitmentSpec import fr.acinq.eclair.transactions.Transactions._ -import fr.acinq.eclair.{BlockHeight, Features, Feature, channel} +import fr.acinq.eclair.{BlockHeight, ChannelTypeFeature, Features, PermanentChannelFeature, channel} import scodec.bits.BitVector private[channel] object ChannelTypes0 { @@ -196,8 +196,8 @@ private[channel] object ChannelTypes0 { ChannelConfig() } val isWumboChannel = commitInput.txOut.amount > Satoshi(16777215) - val baseChannelFeatures: Set[Feature] = if (isWumboChannel) Set(Features.Wumbo) else Set.empty - val commitmentFeatures: Set[Feature] = if (channelVersion.hasAnchorOutputs) { + val baseChannelFeatures: Set[PermanentChannelFeature] = if (isWumboChannel) Set(Features.Wumbo) else Set.empty + val commitmentFeatures: Set[ChannelTypeFeature] = if (channelVersion.hasAnchorOutputs) { Set(Features.StaticRemoteKey, Features.AnchorOutputs) } else if (channelVersion.hasStaticRemotekey) { Set(Features.StaticRemoteKey) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3.scala index 2f18d60117..be8ebdf99c 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3.scala @@ -17,7 +17,7 @@ package fr.acinq.eclair.wire.internal.channel.version3 import fr.acinq.bitcoin.scalacompat.DeterministicWallet.{ExtendedPrivateKey, KeyPath} -import fr.acinq.bitcoin.scalacompat.{OutPoint, Satoshi, Transaction, TxOut} +import fr.acinq.bitcoin.scalacompat.{OutPoint, Transaction, TxOut} import fr.acinq.eclair.channel._ import fr.acinq.eclair.crypto.ShaChain import fr.acinq.eclair.transactions.Transactions._ @@ -25,7 +25,7 @@ import fr.acinq.eclair.transactions.{CommitmentSpec, DirectedHtlc, IncomingHtlc, import fr.acinq.eclair.wire.protocol.CommonCodecs._ import fr.acinq.eclair.wire.protocol.LightningMessageCodecs._ import fr.acinq.eclair.wire.protocol.UpdateMessage -import fr.acinq.eclair.{BlockHeight, FeatureSupport, Features} +import fr.acinq.eclair.{BlockHeight, FeatureSupport, Features, PermanentChannelFeature} import scodec.bits.{BitVector, ByteVector} import scodec.codecs._ import scodec.{Attempt, Codec} @@ -66,7 +66,7 @@ private[channel] object ChannelCodecs3 { /** We use the same encoding as init features, even if we don't need the distinction between mandatory and optional */ val channelFeaturesCodec: Codec[ChannelFeatures] = lengthDelimited(bytes).xmap( - (b: ByteVector) => ChannelFeatures(Features(b).activated.keySet), // we make no difference between mandatory/optional, both are considered activated + (b: ByteVector) => ChannelFeatures(Features(b).activated.keySet.collect { case f: PermanentChannelFeature => f }), // we make no difference between mandatory/optional, both are considered activated (cf: ChannelFeatures) => Features(cf.features.map(f => f -> FeatureSupport.Mandatory).toMap).toByteVector // we encode features as mandatory, by convention ) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/ChannelFeaturesSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/ChannelFeaturesSpec.scala index 8a8eb9d1f1..ad3a4baeaf 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/ChannelFeaturesSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/ChannelFeaturesSpec.scala @@ -20,7 +20,7 @@ import fr.acinq.eclair.FeatureSupport._ import fr.acinq.eclair.Features._ import fr.acinq.eclair.channel.states.ChannelStateTestsBase import fr.acinq.eclair.transactions.Transactions -import fr.acinq.eclair.{Features, InitFeature, NodeFeature, TestKitBaseClass} +import fr.acinq.eclair.{Feature, Features, InitFeature, TestKitBaseClass} import org.scalatest.funsuite.AnyFunSuiteLike class ChannelFeaturesSpec extends TestKitBaseClass with AnyFunSuiteLike with ChannelStateTestsBase { @@ -75,7 +75,7 @@ class ChannelFeaturesSpec extends TestKitBaseClass with AnyFunSuiteLike with Cha ) for (testCase <- testCases) { - assert(ChannelTypes.defaultFromFeatures(testCase.localFeatures, testCase.remoteFeatures) == testCase.expectedChannelType) + assert(ChannelTypes.defaultFromFeatures(testCase.localFeatures, testCase.remoteFeatures) == testCase.expectedChannelType, s"localFeatures=${testCase.localFeatures} remoteFeatures=${testCase.remoteFeatures}") } } @@ -83,13 +83,13 @@ class ChannelFeaturesSpec extends TestKitBaseClass with AnyFunSuiteLike with Cha case class TestCase(features: Features[InitFeature], expectedChannelType: ChannelType) val validChannelTypes = Seq( - TestCase(Features.empty[InitFeature], ChannelTypes.Standard), + TestCase(Features.empty, ChannelTypes.Standard), TestCase(Features(StaticRemoteKey -> Mandatory), ChannelTypes.StaticRemoteKey), TestCase(Features(StaticRemoteKey -> Mandatory, AnchorOutputs -> Mandatory), ChannelTypes.AnchorOutputs), TestCase(Features(StaticRemoteKey -> Mandatory, AnchorOutputsZeroFeeHtlcTx -> Mandatory), ChannelTypes.AnchorOutputsZeroFeeHtlcTx), ) for (testCase <- validChannelTypes) { - assert(ChannelTypes.fromFeatures(testCase.features) == testCase.expectedChannelType) + assert(ChannelTypes.fromFeatures(testCase.features) == testCase.expectedChannelType, testCase.features) } val invalidChannelTypes: Seq[Features[InitFeature]] = Seq( @@ -107,20 +107,25 @@ class ChannelFeaturesSpec extends TestKitBaseClass with AnyFunSuiteLike with Cha Features(StaticRemoteKey -> Mandatory, AnchorOutputs -> Mandatory, Wumbo -> Optional), ) for (features <- invalidChannelTypes) { - assert(ChannelTypes.fromFeatures(features) == ChannelTypes.UnsupportedChannelType(features)) + assert(ChannelTypes.fromFeatures(features) == ChannelTypes.UnsupportedChannelType(features), features) } } - test("enrich channel type with other permanent channel features") { - assert(ChannelFeatures(ChannelTypes.Standard, Features[InitFeature](Wumbo -> Optional), Features.empty[InitFeature]).features.isEmpty) - assert(ChannelFeatures(ChannelTypes.Standard, Features[InitFeature](Wumbo -> Optional), Features[InitFeature](Wumbo -> Optional)).features == Set(Wumbo)) - assert(ChannelFeatures(ChannelTypes.Standard, Features[InitFeature](Wumbo -> Mandatory), Features[InitFeature](Wumbo -> Optional)).features == Set(Wumbo)) - assert(ChannelFeatures(ChannelTypes.StaticRemoteKey, Features[InitFeature](Wumbo -> Optional), Features.empty[InitFeature]).features == Set(StaticRemoteKey)) - assert(ChannelFeatures(ChannelTypes.StaticRemoteKey, Features[InitFeature](Wumbo -> Optional), Features[InitFeature](Wumbo -> Optional)).features == Set(StaticRemoteKey, Wumbo)) - assert(ChannelFeatures(ChannelTypes.AnchorOutputs, Features.empty[InitFeature], Features[InitFeature](Wumbo -> Optional)).features == Set(StaticRemoteKey, AnchorOutputs)) - assert(ChannelFeatures(ChannelTypes.AnchorOutputs, Features[InitFeature](Wumbo -> Optional), Features[InitFeature](Wumbo -> Mandatory)).features == Set(StaticRemoteKey, AnchorOutputs, Wumbo)) - assert(ChannelFeatures(ChannelTypes.AnchorOutputsZeroFeeHtlcTx, Features.empty[InitFeature], Features[InitFeature](Wumbo -> Optional)).features == Set(StaticRemoteKey, AnchorOutputsZeroFeeHtlcTx)) - assert(ChannelFeatures(ChannelTypes.AnchorOutputsZeroFeeHtlcTx, Features[InitFeature](Wumbo -> Optional), Features[InitFeature](Wumbo -> Mandatory)).features == Set(StaticRemoteKey, AnchorOutputsZeroFeeHtlcTx, Wumbo)) + test("enrich channel type with optional permanent channel features") { + case class TestCase(channelType: SupportedChannelType, localFeatures: Features[InitFeature], remoteFeatures: Features[InitFeature], expected: Set[Feature]) + val testCases = Seq( + TestCase(ChannelTypes.Standard, Features(Wumbo -> Optional), Features.empty, Set.empty), + TestCase(ChannelTypes.Standard, Features(Wumbo -> Optional), Features(Wumbo -> Optional), Set(Wumbo)), + TestCase(ChannelTypes.Standard, Features(Wumbo -> Mandatory), Features(Wumbo -> Optional), Set(Wumbo)), + TestCase(ChannelTypes.StaticRemoteKey, Features(Wumbo -> Optional), Features.empty, Set(StaticRemoteKey)), + TestCase(ChannelTypes.StaticRemoteKey, Features(Wumbo -> Optional), Features(Wumbo -> Optional), Set(StaticRemoteKey, Wumbo)), + TestCase(ChannelTypes.AnchorOutputs, Features.empty, Features(Wumbo -> Optional), Set(StaticRemoteKey, AnchorOutputs)), + TestCase(ChannelTypes.AnchorOutputs, Features(Wumbo -> Optional), Features(Wumbo -> Mandatory), Set(StaticRemoteKey, AnchorOutputs, Wumbo)), + TestCase(ChannelTypes.AnchorOutputsZeroFeeHtlcTx, Features.empty, Features(Wumbo -> Optional), Set(StaticRemoteKey, AnchorOutputsZeroFeeHtlcTx)), + TestCase(ChannelTypes.AnchorOutputsZeroFeeHtlcTx, Features(Wumbo -> Optional), Features(Wumbo -> Mandatory), Set(StaticRemoteKey, AnchorOutputsZeroFeeHtlcTx, Wumbo)), + TestCase(ChannelTypes.AnchorOutputsZeroFeeHtlcTx, Features(DualFunding -> Optional, Wumbo -> Optional), Features(DualFunding -> Optional, Wumbo -> Optional), Set(StaticRemoteKey, AnchorOutputsZeroFeeHtlcTx, Wumbo, DualFunding)), + ) + testCases.foreach(t => assert(ChannelFeatures(t.channelType, t.localFeatures, t.remoteFeatures).features == t.expected, s"channelType=${t.channelType} localFeatures=${t.localFeatures} remoteFeatures=${t.remoteFeatures}")) } } From e08353b2432e6143ad18622689e99472dc9b4e2c Mon Sep 17 00:00:00 2001 From: Pierre-Marie Padiou Date: Wed, 8 Jun 2022 10:39:21 +0200 Subject: [PATCH 084/121] Remove close() in db interfaces (#2303) * Remove close() in db interfaces It shouldn't be the responsibility of individual db classes to close the underlying db connection because they typically share the same db instance (postgres) or db files (sqlite). Closing should be handled in the `Databases` level (which is already the case for postgres. For sqlite, closing was only useful for mobile apps, which now use lightning-kmp. Also removed `DbFeeProvider`, which was only used by mobile apps. * increase github ci build time 20min->30min --- .github/workflows/main.yml | 2 +- .../eclair/blockchain/fee/DbFeeProvider.scala | 33 ----- .../scala/fr/acinq/eclair/db/AuditDb.scala | 6 +- .../scala/fr/acinq/eclair/db/ChannelsDb.scala | 4 +- .../fr/acinq/eclair/db/DualDatabases.scala | 31 ----- .../scala/fr/acinq/eclair/db/FeeratesDb.scala | 34 ----- .../scala/fr/acinq/eclair/db/NetworkDb.scala | 3 +- .../scala/fr/acinq/eclair/db/PaymentsDb.scala | 3 +- .../scala/fr/acinq/eclair/db/PeersDb.scala | 4 +- .../acinq/eclair/db/PendingCommandsDb.scala | 4 +- .../fr/acinq/eclair/db/pg/PgAuditDb.scala | 3 - .../fr/acinq/eclair/db/pg/PgChannelsDb.scala | 2 - .../fr/acinq/eclair/db/pg/PgNetworkDb.scala | 2 - .../fr/acinq/eclair/db/pg/PgPaymentsDb.scala | 3 - .../fr/acinq/eclair/db/pg/PgPeersDb.scala | 2 - .../eclair/db/pg/PgPendingCommandsDb.scala | 2 - .../eclair/db/sqlite/SqliteAuditDb.scala | 4 - .../eclair/db/sqlite/SqliteChannelsDb.scala | 3 - .../eclair/db/sqlite/SqliteFeeratesDb.scala | 118 ------------------ .../eclair/db/sqlite/SqliteNetworkDb.scala | 3 - .../eclair/db/sqlite/SqlitePaymentsDb.scala | 4 - .../eclair/db/sqlite/SqlitePeersDb.scala | 4 - .../db/sqlite/SqlitePendingCommandsDb.scala | 3 - .../blockchain/fee/DbFeeProviderSpec.scala | 43 ------- .../eclair/db/SqliteFeeratesDbSpec.scala | 94 -------------- .../fr/acinq/eclair/db/SqliteUtilsSpec.scala | 4 +- 26 files changed, 10 insertions(+), 408 deletions(-) delete mode 100644 eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/DbFeeProvider.scala delete mode 100644 eclair-core/src/main/scala/fr/acinq/eclair/db/FeeratesDb.scala delete mode 100644 eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteFeeratesDb.scala delete mode 100644 eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/DbFeeProviderSpec.scala delete mode 100644 eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteFeeratesDbSpec.scala diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 3a48b058d8..1191b29621 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -10,7 +10,7 @@ jobs: build: runs-on: ubuntu-latest - timeout-minutes: 20 + timeout-minutes: 30 steps: - uses: actions/checkout@v2 diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/DbFeeProvider.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/DbFeeProvider.scala deleted file mode 100644 index ff9e9f4a32..0000000000 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/DbFeeProvider.scala +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright 2020 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.blockchain.fee - -import fr.acinq.eclair.db.FeeratesDb - -import scala.concurrent.{ExecutionContext, Future} - - -class DbFeeProvider(db: FeeratesDb, provider: FeeProvider)(implicit ec: ExecutionContext) extends FeeProvider { - - /** This method retrieves feerates from the provider, and store results in the database */ - override def getFeerates: Future[FeeratesPerKB] = - provider.getFeerates map { feerates => - db.addOrUpdateFeerates(feerates) - feerates - } - -} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/AuditDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/AuditDb.scala index 8ad124ca4b..2105278cb4 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/AuditDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/AuditDb.scala @@ -18,15 +18,13 @@ package fr.acinq.eclair.db import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.bitcoin.scalacompat.{ByteVector32, Satoshi} -import fr.acinq.eclair.{TimestampMilli, TimestampSecond} +import fr.acinq.eclair.TimestampMilli import fr.acinq.eclair.channel._ import fr.acinq.eclair.db.AuditDb.{NetworkFee, Stats} import fr.acinq.eclair.db.DbEventHandler.ChannelEvent import fr.acinq.eclair.payment.{PathFindingExperimentMetrics, PaymentReceived, PaymentRelayed, PaymentSent} -import java.io.Closeable - -trait AuditDb extends Closeable { +trait AuditDb { def add(channelLifecycle: ChannelEvent): Unit diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/ChannelsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/ChannelsDb.scala index 621862caed..a771f927cb 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/ChannelsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/ChannelsDb.scala @@ -21,9 +21,7 @@ import fr.acinq.eclair.CltvExpiry import fr.acinq.eclair.channel.PersistentChannelData import fr.acinq.eclair.db.DbEventHandler.ChannelEvent -import java.io.Closeable - -trait ChannelsDb extends Closeable { +trait ChannelsDb { def addOrUpdateChannel(data: PersistentChannelData): Unit diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/DualDatabases.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/DualDatabases.scala index 3d64cd57a0..9b9d5056e2 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/DualDatabases.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/DualDatabases.scala @@ -132,11 +132,6 @@ case class DualNetworkDb(primary: NetworkDb, secondary: NetworkDb) extends Netwo runAsync(secondary.isPruned(shortChannelId)) primary.isPruned(shortChannelId) } - - override def close(): Unit = { - runAsync(secondary.close()) - primary.close() - } } case class DualAuditDb(primary: AuditDb, secondary: AuditDb) extends AuditDb { @@ -212,11 +207,6 @@ case class DualAuditDb(primary: AuditDb, secondary: AuditDb) extends AuditDb { runAsync(secondary.stats(from, to)) primary.stats(from, to) } - - override def close(): Unit = { - runAsync(secondary.close()) - primary.close() - } } case class DualChannelsDb(primary: ChannelsDb, secondary: ChannelsDb) extends ChannelsDb { @@ -257,11 +247,6 @@ case class DualChannelsDb(primary: ChannelsDb, secondary: ChannelsDb) extends Ch runAsync(secondary.listHtlcInfos(channelId, commitmentNumber)) primary.listHtlcInfos(channelId, commitmentNumber) } - - override def close(): Unit = { - runAsync(secondary.close()) - primary.close() - } } case class DualPeersDb(primary: PeersDb, secondary: PeersDb) extends PeersDb { @@ -297,11 +282,6 @@ case class DualPeersDb(primary: PeersDb, secondary: PeersDb) extends PeersDb { runAsync(secondary.getRelayFees(nodeId)) primary.getRelayFees(nodeId) } - - override def close(): Unit = { - runAsync(secondary.close()) - primary.close() - } } case class DualPaymentsDb(primary: PaymentsDb, secondary: PaymentsDb) extends PaymentsDb { @@ -313,11 +293,6 @@ case class DualPaymentsDb(primary: PaymentsDb, secondary: PaymentsDb) extends Pa primary.listPaymentsOverview(limit) } - override def close(): Unit = { - runAsync(secondary.close()) - primary.close() - } - override def addIncomingPayment(pr: Bolt11Invoice, preimage: ByteVector32, paymentType: String): Unit = { runAsync(secondary.addIncomingPayment(pr, preimage, paymentType)) primary.addIncomingPayment(pr, preimage, paymentType) @@ -392,7 +367,6 @@ case class DualPaymentsDb(primary: PaymentsDb, secondary: PaymentsDb) extends Pa runAsync(secondary.listOutgoingPayments(from, to)) primary.listOutgoingPayments(from, to) } - } case class DualPendingCommandsDb(primary: PendingCommandsDb, secondary: PendingCommandsDb) extends PendingCommandsDb { @@ -418,9 +392,4 @@ case class DualPendingCommandsDb(primary: PendingCommandsDb, secondary: PendingC runAsync(secondary.listSettlementCommands()) primary.listSettlementCommands() } - - override def close(): Unit = { - runAsync(secondary.close()) - primary.close() - } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/FeeratesDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/FeeratesDb.scala deleted file mode 100644 index f8343eaef7..0000000000 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/FeeratesDb.scala +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright 2020 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.db - -import fr.acinq.eclair.blockchain.fee.FeeratesPerKB - -import java.io.Closeable - -/** - * This database stores the fee rates retrieved by a [[fr.acinq.eclair.blockchain.fee.FeeProvider]]. - */ -trait FeeratesDb extends Closeable { - - /** Insert or update the feerates into the feerates database. */ - def addOrUpdateFeerates(feeratesPerKB: FeeratesPerKB): Unit - - /** Return the (optional) feerates from the feerates database. */ - def getFeerates(): Option[FeeratesPerKB] - -} \ No newline at end of file diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/NetworkDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/NetworkDb.scala index 35a071dad6..80973d1c2c 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/NetworkDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/NetworkDb.scala @@ -22,10 +22,9 @@ import fr.acinq.eclair.ShortChannelId import fr.acinq.eclair.router.Router.PublicChannel import fr.acinq.eclair.wire.protocol.{ChannelAnnouncement, ChannelUpdate, NodeAnnouncement} -import java.io.Closeable import scala.collection.immutable.SortedMap -trait NetworkDb extends Closeable { +trait NetworkDb { def addNode(n: NodeAnnouncement): Unit diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala index 3b520f2257..2e60ea707a 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala @@ -22,11 +22,10 @@ import fr.acinq.eclair.payment._ import fr.acinq.eclair.router.Router.{ChannelHop, Hop, NodeHop} import fr.acinq.eclair.{MilliSatoshi, ShortChannelId, TimestampMilli} -import java.io.Closeable import java.util.UUID import scala.util.Try -trait PaymentsDb extends IncomingPaymentsDb with OutgoingPaymentsDb with PaymentsOverviewDb with Closeable +trait PaymentsDb extends IncomingPaymentsDb with OutgoingPaymentsDb with PaymentsOverviewDb trait IncomingPaymentsDb { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/PeersDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/PeersDb.scala index 5a98a454d4..ea10f348e8 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/PeersDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/PeersDb.scala @@ -20,9 +20,7 @@ import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.eclair.payment.relay.Relayer.RelayFees import fr.acinq.eclair.wire.protocol.NodeAddress -import java.io.Closeable - -trait PeersDb extends Closeable { +trait PeersDb { def addOrUpdatePeer(nodeId: PublicKey, address: NodeAddress): Unit diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/PendingCommandsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/PendingCommandsDb.scala index 84eee2c10b..62b77e8939 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/PendingCommandsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/PendingCommandsDb.scala @@ -22,8 +22,6 @@ import fr.acinq.bitcoin.scalacompat.ByteVector32 import fr.acinq.eclair.channel._ import fr.acinq.eclair.wire.protocol.{UpdateFailHtlc, UpdateFailMalformedHtlc, UpdateFulfillHtlc, UpdateMessage} -import java.io.Closeable - /** * This database stores CMD_FULFILL_HTLC and CMD_FAIL_HTLC that we have received from downstream * (either directly via UpdateFulfillHtlc or by extracting the value from the @@ -36,7 +34,7 @@ import java.io.Closeable * to handle all corner cases. * */ -trait PendingCommandsDb extends Closeable { +trait PendingCommandsDb { def addSettlementCommand(channelId: ByteVector32, cmd: HtlcSettlementCommand): Unit diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgAuditDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgAuditDb.scala index c1adb13ddd..980b459c64 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgAuditDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgAuditDb.scala @@ -474,7 +474,4 @@ class PgAuditDb(implicit ds: DataSource) extends AuditDb with Logging { } }) } - - override def close(): Unit = () - } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgChannelsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgChannelsDb.scala index c3c156ce8d..4b003da8a3 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgChannelsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgChannelsDb.scala @@ -252,6 +252,4 @@ class PgChannelsDb(implicit ds: DataSource, lock: PgLock) extends ChannelsDb wit } } } - - override def close(): Unit = () } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgNetworkDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgNetworkDb.scala index d37bf540bd..afe716350e 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgNetworkDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgNetworkDb.scala @@ -267,6 +267,4 @@ class PgNetworkDb(implicit ds: DataSource) extends NetworkDb with Logging { } } } - - override def close(): Unit = () } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgPaymentsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgPaymentsDb.scala index 3d16ac937d..e82c755a34 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgPaymentsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgPaymentsDb.scala @@ -406,7 +406,4 @@ class PgPaymentsDb(implicit ds: DataSource, lock: PgLock) extends PaymentsDb wit } } } - - override def close(): Unit = () - } \ No newline at end of file diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgPeersDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgPeersDb.scala index 6154afa345..45094dd4a9 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgPeersDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgPeersDb.scala @@ -155,6 +155,4 @@ class PgPeersDb(implicit ds: DataSource, lock: PgLock) extends PeersDb with Logg } } } - - override def close(): Unit = () } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgPendingCommandsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgPendingCommandsDb.scala index 7ac17b01f3..2ebd7d57b3 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgPendingCommandsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgPendingCommandsDb.scala @@ -113,6 +113,4 @@ class PgPendingCommandsDb(implicit ds: DataSource, lock: PgLock) extends Pending } } } - - override def close(): Unit = () } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteAuditDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteAuditDb.scala index b28763ecb8..94fc3b1a5d 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteAuditDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteAuditDb.scala @@ -460,8 +460,4 @@ class SqliteAuditDb(val sqlite: Connection) extends AuditDb with Logging { } }) } - - // used by mobile apps - override def close(): Unit = sqlite.close() - } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteChannelsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteChannelsDb.scala index 3cb8df57b3..c39ee4f68d 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteChannelsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteChannelsDb.scala @@ -188,7 +188,4 @@ class SqliteChannelsDb(val sqlite: Connection) extends ChannelsDb with Logging { .toSeq } } - - // used by mobile apps - override def close(): Unit = sqlite.close() } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteFeeratesDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteFeeratesDb.scala deleted file mode 100644 index 48d648aa42..0000000000 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteFeeratesDb.scala +++ /dev/null @@ -1,118 +0,0 @@ -/* - * Copyright 2020 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.db.sqlite - -import fr.acinq.bitcoin.scalacompat.Satoshi -import fr.acinq.eclair.TimestampMilli -import fr.acinq.eclair.blockchain.fee.{FeeratePerKB, FeeratesPerKB} -import fr.acinq.eclair.db.FeeratesDb -import grizzled.slf4j.Logging - -import java.sql.{Connection, Statement} - -object SqliteFeeratesDb { - val DB_NAME = "feerates" - val CURRENT_VERSION = 2 -} - -class SqliteFeeratesDb(sqlite: Connection) extends FeeratesDb with Logging { - - import SqliteFeeratesDb._ - import SqliteUtils.ExtendedResultSet._ - import SqliteUtils._ - - using(sqlite.createStatement(), inTransaction = true) { statement => - - def migration12(statement: Statement): Unit = { - statement.executeUpdate("ALTER TABLE feerates_per_kb RENAME TO _feerates_per_kb_old") - statement.executeUpdate( - """ - |CREATE TABLE feerates_per_kb ( - |rate_block_1 INTEGER NOT NULL, rate_blocks_2 INTEGER NOT NULL, rate_blocks_6 INTEGER NOT NULL, rate_blocks_12 INTEGER NOT NULL, rate_blocks_36 INTEGER NOT NULL, rate_blocks_72 INTEGER NOT NULL, rate_blocks_144 INTEGER NOT NULL, rate_blocks_1008 INTEGER NOT NULL, - |timestamp INTEGER NOT NULL)""".stripMargin) - statement.executeUpdate("INSERT INTO feerates_per_kb (rate_block_1, rate_blocks_2, rate_blocks_6, rate_blocks_12, rate_blocks_36, rate_blocks_72, rate_blocks_144, rate_blocks_1008, timestamp) SELECT rate_block_1, rate_blocks_2, rate_blocks_6, rate_blocks_12, rate_blocks_36, rate_blocks_72, rate_blocks_144, rate_blocks_144, timestamp FROM _feerates_per_kb_old") - statement.executeUpdate("DROP table _feerates_per_kb_old") - } - - getVersion(statement, DB_NAME) match { - case None => - // Create feerates table. Rates are in kb. - statement.executeUpdate( - """ - |CREATE TABLE feerates_per_kb ( - |rate_block_1 INTEGER NOT NULL, rate_blocks_2 INTEGER NOT NULL, rate_blocks_6 INTEGER NOT NULL, rate_blocks_12 INTEGER NOT NULL, rate_blocks_36 INTEGER NOT NULL, rate_blocks_72 INTEGER NOT NULL, rate_blocks_144 INTEGER NOT NULL, rate_blocks_1008 INTEGER NOT NULL, - |timestamp INTEGER NOT NULL)""".stripMargin) - case Some(v@1) => - logger.warn(s"migrating db $DB_NAME, found version=$v current=$CURRENT_VERSION") - migration12(statement) - case Some(CURRENT_VERSION) => () // table is up-to-date, nothing to do - case Some(unknownVersion) => throw new RuntimeException(s"Unknown version of DB $DB_NAME found, version=$unknownVersion") - } - setVersion(statement, DB_NAME, CURRENT_VERSION) - } - - override def addOrUpdateFeerates(feeratesPerKB: FeeratesPerKB): Unit = { - using(sqlite.prepareStatement("UPDATE feerates_per_kb SET rate_block_1=?, rate_blocks_2=?, rate_blocks_6=?, rate_blocks_12=?, rate_blocks_36=?, rate_blocks_72=?, rate_blocks_144=?, rate_blocks_1008=?, timestamp=?")) { update => - update.setLong(1, feeratesPerKB.block_1.toLong) - update.setLong(2, feeratesPerKB.blocks_2.toLong) - update.setLong(3, feeratesPerKB.blocks_6.toLong) - update.setLong(4, feeratesPerKB.blocks_12.toLong) - update.setLong(5, feeratesPerKB.blocks_36.toLong) - update.setLong(6, feeratesPerKB.blocks_72.toLong) - update.setLong(7, feeratesPerKB.blocks_144.toLong) - update.setLong(8, feeratesPerKB.blocks_1008.toLong) - update.setLong(9, TimestampMilli.now().toLong) - if (update.executeUpdate() == 0) { - using(sqlite.prepareStatement("INSERT INTO feerates_per_kb VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)")) { insert => - insert.setLong(1, feeratesPerKB.block_1.toLong) - insert.setLong(2, feeratesPerKB.blocks_2.toLong) - insert.setLong(3, feeratesPerKB.blocks_6.toLong) - insert.setLong(4, feeratesPerKB.blocks_12.toLong) - insert.setLong(5, feeratesPerKB.blocks_36.toLong) - insert.setLong(6, feeratesPerKB.blocks_72.toLong) - insert.setLong(7, feeratesPerKB.blocks_144.toLong) - insert.setLong(8, feeratesPerKB.blocks_1008.toLong) - insert.setLong(9, TimestampMilli.now().toLong) - insert.executeUpdate() - } - } - } - } - - override def getFeerates(): Option[FeeratesPerKB] = { - using(sqlite.prepareStatement("SELECT rate_block_1, rate_blocks_2, rate_blocks_6, rate_blocks_12, rate_blocks_36, rate_blocks_72, rate_blocks_144, rate_blocks_1008 FROM feerates_per_kb")) { statement => - statement.executeQuery() - .map { rs => - FeeratesPerKB( - // NB: we don't bother storing this value in the DB, because it's unused on mobile. - mempoolMinFee = FeeratePerKB(Satoshi(rs.getLong("rate_blocks_1008"))), - block_1 = FeeratePerKB(Satoshi(rs.getLong("rate_block_1"))), - blocks_2 = FeeratePerKB(Satoshi(rs.getLong("rate_blocks_2"))), - blocks_6 = FeeratePerKB(Satoshi(rs.getLong("rate_blocks_6"))), - blocks_12 = FeeratePerKB(Satoshi(rs.getLong("rate_blocks_12"))), - blocks_36 = FeeratePerKB(Satoshi(rs.getLong("rate_blocks_36"))), - blocks_72 = FeeratePerKB(Satoshi(rs.getLong("rate_blocks_72"))), - blocks_144 = FeeratePerKB(Satoshi(rs.getLong("rate_blocks_144"))), - blocks_1008 = FeeratePerKB(Satoshi(rs.getLong("rate_blocks_1008")))) - } - .headOption - } - } - - // used by mobile apps - override def close(): Unit = sqlite.close() -} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteNetworkDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteNetworkDb.scala index f5b8cbdc63..844e886aae 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteNetworkDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteNetworkDb.scala @@ -176,7 +176,4 @@ class SqliteNetworkDb(val sqlite: Connection) extends NetworkDb with Logging { statement.executeQuery().nonEmpty } } - - // used by mobile apps - override def close(): Unit = sqlite.close() } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala index fa5fd22f7c..7a61795bae 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala @@ -396,10 +396,6 @@ class SqlitePaymentsDb(val sqlite: Connection) extends PaymentsDb with Logging { }.toSeq } } - - // used by mobile apps - override def close(): Unit = sqlite.close() - } object SqlitePaymentsDb { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePeersDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePeersDb.scala index e0e887d7ca..610bb07909 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePeersDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePeersDb.scala @@ -128,8 +128,4 @@ class SqlitePeersDb(val sqlite: Connection) extends PeersDb with Logging { ) } } - - - // used by mobile apps - override def close(): Unit = sqlite.close() } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePendingCommandsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePendingCommandsDb.scala index 9c2c16ca39..351a229e74 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePendingCommandsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePendingCommandsDb.scala @@ -88,7 +88,4 @@ class SqlitePendingCommandsDb(val sqlite: Connection) extends PendingCommandsDb .toSeq } } - - // used by mobile apps - override def close(): Unit = sqlite.close() } \ No newline at end of file diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/DbFeeProviderSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/DbFeeProviderSpec.scala deleted file mode 100644 index c650bc9a17..0000000000 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/DbFeeProviderSpec.scala +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright 2020 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.blockchain.fee - -import akka.util.Timeout -import fr.acinq.bitcoin.scalacompat.SatoshiLong -import fr.acinq.eclair.TestDatabases -import fr.acinq.eclair.db.sqlite.SqliteFeeratesDb -import org.scalatest.funsuite.AnyFunSuite - -import scala.concurrent.Await -import scala.concurrent.ExecutionContext.Implicits.global -import scala.concurrent.duration._ - -class DbFeeProviderSpec extends AnyFunSuite { - - val feerates1: FeeratesPerKB = FeeratesPerKB(FeeratePerKB(800 sat), FeeratePerKB(100 sat), FeeratePerKB(200 sat), FeeratePerKB(300 sat), FeeratePerKB(400 sat), FeeratePerKB(500 sat), FeeratePerKB(600 sat), FeeratePerKB(700 sat), FeeratePerKB(800 sat)) - - test("db fee provider saves feerates in database") { - val sqlite = TestDatabases.sqliteInMemory() - val db = new SqliteFeeratesDb(sqlite) - val provider = new DbFeeProvider(db, new ConstantFeeProvider(feerates1)) - - assert(db.getFeerates().isEmpty) - assert(Await.result(provider.getFeerates, Timeout(30 seconds).duration) == feerates1) - assert(db.getFeerates().get == feerates1) - } - -} diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteFeeratesDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteFeeratesDbSpec.scala deleted file mode 100644 index a5dae743d7..0000000000 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteFeeratesDbSpec.scala +++ /dev/null @@ -1,94 +0,0 @@ -/* - * Copyright 2020 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.db - -import fr.acinq.bitcoin.scalacompat.SatoshiLong -import fr.acinq.eclair._ -import fr.acinq.eclair.blockchain.fee.{FeeratePerKB, FeeratesPerKB} -import fr.acinq.eclair.db.pg.PgUtils.setVersion -import fr.acinq.eclair.db.sqlite.SqliteFeeratesDb -import fr.acinq.eclair.db.sqlite.SqliteUtils.{getVersion, using} -import org.scalatest.funsuite.AnyFunSuite - -class SqliteFeeratesDbSpec extends AnyFunSuite { - - val feerate: FeeratesPerKB = FeeratesPerKB( - mempoolMinFee = FeeratePerKB(10000 sat), - block_1 = FeeratePerKB(150000 sat), - blocks_2 = FeeratePerKB(120000 sat), - blocks_6 = FeeratePerKB(100000 sat), - blocks_12 = FeeratePerKB(90000 sat), - blocks_36 = FeeratePerKB(70000 sat), - blocks_72 = FeeratePerKB(50000 sat), - blocks_144 = FeeratePerKB(20000 sat), - blocks_1008 = FeeratePerKB(10000 sat)) - - test("init database 2 times in a row") { - val sqlite = TestDatabases.sqliteInMemory() - val db1 = new SqliteFeeratesDb(sqlite) - val db2 = new SqliteFeeratesDb(sqlite) - } - - test("add/get feerates") { - val sqlite = TestDatabases.sqliteInMemory() - val db = new SqliteFeeratesDb(sqlite) - - db.addOrUpdateFeerates(feerate) - assert(db.getFeerates().get == feerate) - } - - test("migration v1 -> current") { - val sqlite = TestDatabases.sqliteInMemory() - - using(sqlite.createStatement()) { statement => - statement.executeUpdate( - """ - |CREATE TABLE IF NOT EXISTS feerates_per_kb ( - |rate_block_1 INTEGER NOT NULL, rate_blocks_2 INTEGER NOT NULL, rate_blocks_6 INTEGER NOT NULL, rate_blocks_12 INTEGER NOT NULL, rate_blocks_36 INTEGER NOT NULL, rate_blocks_72 INTEGER NOT NULL, rate_blocks_144 INTEGER NOT NULL, - |timestamp INTEGER NOT NULL)""".stripMargin) - setVersion(statement, "feerates", 1) - } - - using(sqlite.createStatement()) { statement => - assert(getVersion(statement, "feerates").contains(1)) - } - - // Version 1 was missing the 1008 block target. - using(sqlite.prepareStatement("INSERT INTO feerates_per_kb VALUES (?, ?, ?, ?, ?, ?, ?, ?)")) { statement => - statement.setLong(1, feerate.block_1.toLong) - statement.setLong(2, feerate.blocks_2.toLong) - statement.setLong(3, feerate.blocks_6.toLong) - statement.setLong(4, feerate.blocks_12.toLong) - statement.setLong(5, feerate.blocks_36.toLong) - statement.setLong(6, feerate.blocks_72.toLong) - statement.setLong(7, feerate.blocks_144.toLong) - statement.setLong(8, TimestampMilli.now().toLong) - statement.executeUpdate() - } - - val migratedDb = new SqliteFeeratesDb(sqlite) - using(sqlite.createStatement()) { statement => - assert(getVersion(statement, "feerates").contains(SqliteFeeratesDb.CURRENT_VERSION)) - } - - // When migrating, we simply copy the estimate for blocks 144 to blocks 1008. - assert(migratedDb.getFeerates() == Some(feerate.copy(blocks_1008 = feerate.blocks_144, mempoolMinFee = feerate.blocks_144))) - migratedDb.addOrUpdateFeerates(feerate) - assert(migratedDb.getFeerates() == Some(feerate)) - } - -} diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteUtilsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteUtilsSpec.scala index 6af0aea6b0..1f3ec53e4d 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteUtilsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteUtilsSpec.scala @@ -86,13 +86,13 @@ class SqliteUtilsSpec extends AnyFunSuite { // first start : write to file val db1 = Databases.sqlite(datadir, Some(jdbcUrlPath)) - db1.channels.close() + db1.channels.sqlite.close() assert(Files.readString(jdbcUrlPath.toPath).trim == "sqlite") // 2nd start : no-op val db2 = Databases.sqlite(datadir, Some(jdbcUrlPath)) - db2.channels.close() + db2.channels.sqlite.close() // we modify the file Files.writeString(jdbcUrlPath.toPath, "postgres") From c5620b2d5613e40961eca701236a22ec256a9ca2 Mon Sep 17 00:00:00 2001 From: Thomas HUET <81159533+thomash-acinq@users.noreply.github.com> Date: Wed, 8 Jun 2022 11:08:30 +0200 Subject: [PATCH 085/121] Log routing hint when finding a route (#2242) When finding a payment route (typically for routing a trampoline payment) we already log the recipient node id, however it can be a private node hidden behind a routing hint in which case it is more useful to log the public node from the routing hint. This is added to the `audit.path_finding_metrics` and we also add the `payment_hash` so that it can be joined with the `audit.relayed` and `audit.relayed_trampoline` tables. --- .../fr/acinq/eclair/db/pg/PgAuditDb.scala | 23 ++- .../acinq/eclair/payment/PaymentEvents.scala | 6 +- .../send/MultiPartPaymentLifecycle.scala | 3 +- .../payment/send/PaymentLifecycle.scala | 3 +- .../fr/acinq/eclair/db/AuditDbSpec.scala | 131 +++++++++++++++++- 5 files changed, 156 insertions(+), 10 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgAuditDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgAuditDb.scala index 980b459c64..4305217916 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgAuditDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgAuditDb.scala @@ -36,7 +36,7 @@ import javax.sql.DataSource object PgAuditDb { val DB_NAME = "audit" - val CURRENT_VERSION = 10 + val CURRENT_VERSION = 11 } class PgAuditDb(implicit ds: DataSource) extends AuditDb with Logging { @@ -44,6 +44,7 @@ class PgAuditDb(implicit ds: DataSource) extends AuditDb with Logging { import PgUtils._ import ExtendedResultSet._ import PgAuditDb._ + import fr.acinq.eclair.json.JsonSerializers.{formats, serialization} case class RelayedPart(channelId: ByteVector32, amount: MilliSatoshi, direction: String, relayType: String, timestamp: TimestampMilli) @@ -102,6 +103,13 @@ class PgAuditDb(implicit ds: DataSource) extends AuditDb with Logging { statement.executeUpdate("DROP TABLE audit.network_fees") } + def migration1011(statement: Statement): Unit = { + statement.executeUpdate("ALTER TABLE audit.path_finding_metrics ADD COLUMN payment_hash TEXT") + statement.executeUpdate("ALTER TABLE audit.path_finding_metrics ADD COLUMN routing_hints JSONB") + statement.executeUpdate("CREATE INDEX metrics_hash_idx ON audit.path_finding_metrics(payment_hash)") + statement.executeUpdate("CREATE INDEX metrics_recipient_idx ON audit.path_finding_metrics(recipient_node_id)") + } + getVersion(statement, DB_NAME) match { case None => statement.executeUpdate("CREATE SCHEMA audit") @@ -112,7 +120,7 @@ class PgAuditDb(implicit ds: DataSource) extends AuditDb with Logging { statement.executeUpdate("CREATE TABLE audit.relayed_trampoline (payment_hash TEXT NOT NULL, amount_msat BIGINT NOT NULL, next_node_id TEXT NOT NULL, timestamp TIMESTAMP WITH TIME ZONE NOT NULL)") statement.executeUpdate("CREATE TABLE audit.channel_events (channel_id TEXT NOT NULL, node_id TEXT NOT NULL, capacity_sat BIGINT NOT NULL, is_funder BOOLEAN NOT NULL, is_private BOOLEAN NOT NULL, event TEXT NOT NULL, timestamp TIMESTAMP WITH TIME ZONE NOT NULL)") statement.executeUpdate("CREATE TABLE audit.channel_updates (channel_id TEXT NOT NULL, node_id TEXT NOT NULL, fee_base_msat BIGINT NOT NULL, fee_proportional_millionths BIGINT NOT NULL, cltv_expiry_delta BIGINT NOT NULL, htlc_minimum_msat BIGINT NOT NULL, htlc_maximum_msat BIGINT NOT NULL, timestamp TIMESTAMP WITH TIME ZONE NOT NULL)") - statement.executeUpdate("CREATE TABLE audit.path_finding_metrics (amount_msat BIGINT NOT NULL, fees_msat BIGINT NOT NULL, status TEXT NOT NULL, duration_ms BIGINT NOT NULL, timestamp TIMESTAMP WITH TIME ZONE NOT NULL, is_mpp BOOLEAN NOT NULL, experiment_name TEXT NOT NULL, recipient_node_id TEXT NOT NULL)") + statement.executeUpdate("CREATE TABLE audit.path_finding_metrics (amount_msat BIGINT NOT NULL, fees_msat BIGINT NOT NULL, status TEXT NOT NULL, duration_ms BIGINT NOT NULL, timestamp TIMESTAMP WITH TIME ZONE NOT NULL, is_mpp BOOLEAN NOT NULL, experiment_name TEXT NOT NULL, recipient_node_id TEXT NOT NULL, payment_hash TEXT, routing_hints JSONB)") statement.executeUpdate("CREATE TABLE audit.transactions_published (tx_id TEXT NOT NULL PRIMARY KEY, channel_id TEXT NOT NULL, node_id TEXT NOT NULL, mining_fee_sat BIGINT NOT NULL, tx_type TEXT NOT NULL, timestamp TIMESTAMP WITH TIME ZONE NOT NULL)") statement.executeUpdate("CREATE TABLE audit.transactions_confirmed (tx_id TEXT NOT NULL PRIMARY KEY, channel_id TEXT NOT NULL, node_id TEXT NOT NULL, timestamp TIMESTAMP WITH TIME ZONE NOT NULL)") @@ -132,9 +140,11 @@ class PgAuditDb(implicit ds: DataSource) extends AuditDb with Logging { statement.executeUpdate("CREATE INDEX metrics_timestamp_idx ON audit.path_finding_metrics(timestamp)") statement.executeUpdate("CREATE INDEX metrics_mpp_idx ON audit.path_finding_metrics(is_mpp)") statement.executeUpdate("CREATE INDEX metrics_name_idx ON audit.path_finding_metrics(experiment_name)") + statement.executeUpdate("CREATE INDEX metrics_recipient_idx ON audit.path_finding_metrics(recipient_node_id)") + statement.executeUpdate("CREATE INDEX metrics_hash_idx ON audit.path_finding_metrics(payment_hash)") statement.executeUpdate("CREATE INDEX transactions_published_timestamp_idx ON audit.transactions_published(timestamp)") statement.executeUpdate("CREATE INDEX transactions_confirmed_timestamp_idx ON audit.transactions_confirmed(timestamp)") - case Some(v@(4 | 5 | 6 | 7 | 8 | 9)) => + case Some(v@(4 | 5 | 6 | 7 | 8 | 9 | 10)) => logger.warn(s"migrating db $DB_NAME, found version=$v current=$CURRENT_VERSION") if (v < 5) { migration45(statement) @@ -154,6 +164,9 @@ class PgAuditDb(implicit ds: DataSource) extends AuditDb with Logging { if (v < 10) { migration910(statement) } + if (v < 11) { + migration1011(statement) + } case Some(CURRENT_VERSION) => () // table is up-to-date, nothing to do case Some(unknownVersion) => throw new RuntimeException(s"Unknown version of DB $DB_NAME found, version=$unknownVersion") } @@ -305,7 +318,7 @@ class PgAuditDb(implicit ds: DataSource) extends AuditDb with Logging { override def addPathFindingExperimentMetrics(m: PathFindingExperimentMetrics): Unit = withMetrics("audit/add-experiment-metrics", DbBackends.Postgres) { inTransaction { pg => - using(pg.prepareStatement("INSERT INTO audit.path_finding_metrics VALUES (?, ?, ?, ?, ?, ?, ?, ?)")) { statement => + using(pg.prepareStatement("INSERT INTO audit.path_finding_metrics VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?::JSONB)")) { statement => statement.setLong(1, m.amount.toLong) statement.setLong(2, m.fees.toLong) statement.setString(3, m.status) @@ -314,6 +327,8 @@ class PgAuditDb(implicit ds: DataSource) extends AuditDb with Logging { statement.setBoolean(6, m.isMultiPart) statement.setString(7, m.experimentName) statement.setString(8, m.recipientNodeId.value.toHex) + statement.setString(9, m.paymentHash.toHex) + statement.setString(10, m.routingHints_opt.map(serialization.write(_)).orNull) statement.executeUpdate() } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentEvents.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentEvents.scala index d638722762..8d7add9887 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentEvents.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentEvents.scala @@ -251,11 +251,13 @@ object PaymentFailure { } -case class PathFindingExperimentMetrics(amount: MilliSatoshi, +case class PathFindingExperimentMetrics(paymentHash: ByteVector32, + amount: MilliSatoshi, fees: MilliSatoshi, status: String, duration: FiniteDuration, timestamp: TimestampMilli, isMultiPart: Boolean, experimentName: String, - recipientNodeId: PublicKey) + recipientNodeId: PublicKey, + routingHints_opt: Option[Seq[Seq[ExtraHop]]]) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/MultiPartPaymentLifecycle.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/MultiPartPaymentLifecycle.scala index 66d8c00dbf..90537637f9 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/MultiPartPaymentLifecycle.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/MultiPartPaymentLifecycle.scala @@ -264,7 +264,8 @@ class MultiPartPaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, } paymentSent.feesPaid + localFees } - context.system.eventStream.publish(PathFindingExperimentMetrics(cfg.recipientAmount, fees, status, duration, now, isMultiPart = true, request.routeParams.experimentName, cfg.recipientNodeId)) + val hints_opt = cfg.invoice.flatMap(invoice => if (invoice.routingInfo.isEmpty) None else Some(invoice.routingInfo)) + context.system.eventStream.publish(PathFindingExperimentMetrics(cfg.paymentHash, cfg.recipientAmount, fees, status, duration, now, isMultiPart = true, request.routeParams.experimentName, cfg.recipientNodeId, hints_opt)) } Metrics.SentPaymentDuration .withTag(Tags.MultiPart, Tags.MultiPartType.Parent) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentLifecycle.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentLifecycle.scala index b2256c42a0..0735f48353 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentLifecycle.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentLifecycle.scala @@ -334,7 +334,8 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A } request match { case SendPaymentToNode(_, _, _, _, _, routeParams) => - context.system.eventStream.publish(PathFindingExperimentMetrics(request.finalPayload.amount, fees, status, duration, now, isMultiPart = false, routeParams.experimentName, cfg.recipientNodeId)) + val hints_opt = cfg.invoice.flatMap(invoice => if (invoice.routingInfo.isEmpty) None else Some(invoice.routingInfo)) + context.system.eventStream.publish(PathFindingExperimentMetrics(cfg.paymentHash, request.finalPayload.amount, fees, status, duration, now, isMultiPart = false, routeParams.experimentName, cfg.recipientNodeId, hints_opt)) case SendPaymentToRoute(_, _, _, _) => () } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/AuditDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/AuditDbSpec.scala index b3316ab38e..39506f983f 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/AuditDbSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/AuditDbSpec.scala @@ -16,7 +16,7 @@ package fr.acinq.eclair.db -import fr.acinq.bitcoin.scalacompat.Crypto.PrivateKey +import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} import fr.acinq.bitcoin.scalacompat.{ByteVector32, SatoshiLong, Script, Transaction, TxOut} import fr.acinq.eclair.TestDatabases.{TestPgDatabases, TestSqliteDatabases, migrationCheck} import fr.acinq.eclair._ @@ -28,6 +28,7 @@ import fr.acinq.eclair.db.jdbc.JdbcUtils.using import fr.acinq.eclair.db.pg.PgAuditDb import fr.acinq.eclair.db.pg.PgUtils.{getVersion, setVersion} import fr.acinq.eclair.db.sqlite.SqliteAuditDb +import fr.acinq.eclair.payment.Bolt11Invoice.ExtraHop import fr.acinq.eclair.payment._ import fr.acinq.eclair.router.Announcements import fr.acinq.eclair.transactions.Transactions.PlaceHolderPubKey @@ -704,6 +705,96 @@ class AuditDbSpec extends AnyFunSuite { } } + test("migrate postgres audit database v10 -> current") { + forAllDbs { + case dbs: TestPgDatabases => + migrationCheck( + dbs = dbs, + initializeTables = connection => { + // simulate existing v10 db + using(connection.createStatement()) { statement => + statement.executeUpdate("CREATE SCHEMA audit") + + statement.executeUpdate("CREATE TABLE audit.sent (amount_msat BIGINT NOT NULL, fees_msat BIGINT NOT NULL, recipient_amount_msat BIGINT NOT NULL, payment_id TEXT NOT NULL, parent_payment_id TEXT NOT NULL, payment_hash TEXT NOT NULL, payment_preimage TEXT NOT NULL, recipient_node_id TEXT NOT NULL, to_channel_id TEXT NOT NULL, timestamp TIMESTAMP WITH TIME ZONE NOT NULL)") + statement.executeUpdate("CREATE TABLE audit.received (amount_msat BIGINT NOT NULL, payment_hash TEXT NOT NULL, from_channel_id TEXT NOT NULL, timestamp TIMESTAMP WITH TIME ZONE NOT NULL)") + statement.executeUpdate("CREATE TABLE audit.relayed (payment_hash TEXT NOT NULL, amount_msat BIGINT NOT NULL, channel_id TEXT NOT NULL, direction TEXT NOT NULL, relay_type TEXT NOT NULL, timestamp TIMESTAMP WITH TIME ZONE NOT NULL)") + statement.executeUpdate("CREATE TABLE audit.relayed_trampoline (payment_hash TEXT NOT NULL, amount_msat BIGINT NOT NULL, next_node_id TEXT NOT NULL, timestamp TIMESTAMP WITH TIME ZONE NOT NULL)") + statement.executeUpdate("CREATE TABLE audit.channel_events (channel_id TEXT NOT NULL, node_id TEXT NOT NULL, capacity_sat BIGINT NOT NULL, is_funder BOOLEAN NOT NULL, is_private BOOLEAN NOT NULL, event TEXT NOT NULL, timestamp TIMESTAMP WITH TIME ZONE NOT NULL)") + statement.executeUpdate("CREATE TABLE audit.channel_updates (channel_id TEXT NOT NULL, node_id TEXT NOT NULL, fee_base_msat BIGINT NOT NULL, fee_proportional_millionths BIGINT NOT NULL, cltv_expiry_delta BIGINT NOT NULL, htlc_minimum_msat BIGINT NOT NULL, htlc_maximum_msat BIGINT NOT NULL, timestamp TIMESTAMP WITH TIME ZONE NOT NULL)") + statement.executeUpdate("CREATE TABLE audit.path_finding_metrics (amount_msat BIGINT NOT NULL, fees_msat BIGINT NOT NULL, status TEXT NOT NULL, duration_ms BIGINT NOT NULL, timestamp TIMESTAMP WITH TIME ZONE NOT NULL, is_mpp BOOLEAN NOT NULL, experiment_name TEXT NOT NULL, recipient_node_id TEXT NOT NULL)") + statement.executeUpdate("CREATE TABLE audit.transactions_published (tx_id TEXT NOT NULL PRIMARY KEY, channel_id TEXT NOT NULL, node_id TEXT NOT NULL, mining_fee_sat BIGINT NOT NULL, tx_type TEXT NOT NULL, timestamp TIMESTAMP WITH TIME ZONE NOT NULL)") + statement.executeUpdate("CREATE TABLE audit.transactions_confirmed (tx_id TEXT NOT NULL PRIMARY KEY, channel_id TEXT NOT NULL, node_id TEXT NOT NULL, timestamp TIMESTAMP WITH TIME ZONE NOT NULL)") + + statement.executeUpdate("CREATE TABLE audit.channel_errors (channel_id TEXT NOT NULL, node_id TEXT NOT NULL, error_name TEXT NOT NULL, error_message TEXT NOT NULL, is_fatal BOOLEAN NOT NULL, timestamp TIMESTAMP WITH TIME ZONE NOT NULL)") + statement.executeUpdate("CREATE INDEX sent_timestamp_idx ON audit.sent(timestamp)") + statement.executeUpdate("CREATE INDEX received_timestamp_idx ON audit.received(timestamp)") + statement.executeUpdate("CREATE INDEX relayed_timestamp_idx ON audit.relayed(timestamp)") + statement.executeUpdate("CREATE INDEX relayed_payment_hash_idx ON audit.relayed(payment_hash)") + statement.executeUpdate("CREATE INDEX relayed_trampoline_timestamp_idx ON audit.relayed_trampoline(timestamp)") + statement.executeUpdate("CREATE INDEX relayed_trampoline_payment_hash_idx ON audit.relayed_trampoline(payment_hash)") + statement.executeUpdate("CREATE INDEX channel_events_timestamp_idx ON audit.channel_events(timestamp)") + statement.executeUpdate("CREATE INDEX channel_errors_timestamp_idx ON audit.channel_errors(timestamp)") + statement.executeUpdate("CREATE INDEX channel_updates_cid_idx ON audit.channel_updates(channel_id)") + statement.executeUpdate("CREATE INDEX channel_updates_nid_idx ON audit.channel_updates(node_id)") + statement.executeUpdate("CREATE INDEX channel_updates_timestamp_idx ON audit.channel_updates(timestamp)") + statement.executeUpdate("CREATE INDEX metrics_status_idx ON audit.path_finding_metrics(status)") + statement.executeUpdate("CREATE INDEX metrics_timestamp_idx ON audit.path_finding_metrics(timestamp)") + statement.executeUpdate("CREATE INDEX metrics_mpp_idx ON audit.path_finding_metrics(is_mpp)") + statement.executeUpdate("CREATE INDEX metrics_name_idx ON audit.path_finding_metrics(experiment_name)") + statement.executeUpdate("CREATE INDEX transactions_published_timestamp_idx ON audit.transactions_published(timestamp)") + statement.executeUpdate("CREATE INDEX transactions_confirmed_timestamp_idx ON audit.transactions_confirmed(timestamp)") + + setVersion(statement, "audit", 10) + } + + using(connection.prepareStatement("INSERT INTO audit.path_finding_metrics VALUES (?, ?, ?, ?, ?, ?, ?, ?)")) { statement => + statement.setLong(1, 214000) + statement.setLong(2, 345) + statement.setString(3, "FAILURE") + statement.setLong(4, 520) + statement.setTimestamp(5, TimestampSecond(1651053434L).toSqlTimestamp) + statement.setBoolean(6, true) + statement.setString(7, "experiment-a") + statement.setString(8, "03271338633d2d37b285dae4df40b413d8c6c791fbee7797bc5dc70812196d7d5c") + statement.executeUpdate() + } + using(connection.prepareStatement("INSERT INTO audit.path_finding_metrics VALUES (?, ?, ?, ?, ?, ?, ?, ?)")) { statement => + statement.setLong(1, 35000) + statement.setLong(2, 43) + statement.setString(3, "SUCCESS") + statement.setLong(4, 2043) + statement.setTimestamp(5, TimestampSecond(1651054567L).toSqlTimestamp) + statement.setBoolean(6, false) + statement.setString(7, "experiment-b") + statement.setString(8, "030c3f19d742ca294a55c00376b3b355c3c90d61c6b6b39554dbc7ac19b141c14f") + statement.executeUpdate() + } + }, + dbName = PgAuditDb.DB_NAME, + targetVersion = PgAuditDb.CURRENT_VERSION, + postCheck = connection => { + val migratedDb = dbs.audit + using(connection.createStatement()) { statement => assert(getVersion(statement, "audit").contains(PgAuditDb.CURRENT_VERSION)) } + using(connection.prepareStatement(s"SELECT amount_msat, status, experiment_name, recipient_node_id FROM audit.path_finding_metrics ORDER BY timestamp")) { statement => + val result = statement.executeQuery() + assert(result.next()) + assert(result.getLong(1) == 214000) + assert(result.getString(2) == "FAILURE") + assert(result.getString(3) == "experiment-a") + assert(result.getString(4) == "03271338633d2d37b285dae4df40b413d8c6c791fbee7797bc5dc70812196d7d5c") + assert(result.next()) + assert(result.getLong(1) == 35000) + assert(result.getString(2) == "SUCCESS") + assert(result.getString(3) == "experiment-b") + assert(result.getString(4) == "030c3f19d742ca294a55c00376b3b355c3c90d61c6b6b39554dbc7ac19b141c14f") + assert(!result.next()) + } + } + ) + case _: TestSqliteDatabases => () + } + } + test("ignore invalid values in the DB") { forAllDbs { dbs => val db = dbs.audit @@ -760,7 +851,43 @@ class AuditDbSpec extends AnyFunSuite { test("add experiment metrics") { forAllDbs { dbs => - dbs.audit.addPathFindingExperimentMetrics(PathFindingExperimentMetrics(100000000 msat, 3000 msat, status = "SUCCESS", 37 millis, TimestampMilli.now(), isMultiPart = false, "my-test-experiment", randomKey().publicKey)) + val isPg = dbs.isInstanceOf[TestPgDatabases] + val hints = Some(Seq(Seq(ExtraHop( + PublicKey(hex"033f2d90d6ba1f771e4b3586b35cc9f825cfcb7cdd7edaa2bfd63f0cb81b17580e"), + ShortChannelId(1), + 1000 msat, + 100, + CltvExpiryDelta(144) + ), ExtraHop( + PublicKey(hex"02c15a88ff263cec5bf79c315b17b7f2e083f71d62a880e30281faaac0898cb2b7"), + ShortChannelId(2), + 900 msat, + 200, + CltvExpiryDelta(12) + )), Seq(ExtraHop( + PublicKey(hex"026ec3e3438308519a75ca4496822a6c1e229174fbcaadeeb174704c377112c331"), + ShortChannelId(3), + 800 msat, + 300, + CltvExpiryDelta(78) + )))) + dbs.audit.addPathFindingExperimentMetrics(PathFindingExperimentMetrics(randomBytes32(), 100000000 msat, 3000 msat, status = "SUCCESS", 37 millis, TimestampMilli.now(), isMultiPart = false, "my-test-experiment", randomKey().publicKey, hints)) + + val table = if (isPg) "audit.path_finding_metrics" else "path_finding_metrics" + val hint_column = if (isPg) ", routing_hints" else "" + using(dbs.connection.prepareStatement(s"SELECT amount_msat, status, fees_msat, duration_ms, experiment_name $hint_column FROM $table")) { statement => + val result = statement.executeQuery() + assert(result.next()) + assert(result.getLong(1) == 100000000) + assert(result.getString(2) == "SUCCESS") + assert(result.getLong(3) == 3000) + assert(result.getLong(4) == 37) + assert(result.getString(5) == "my-test-experiment") + if (isPg) { + assert(result.getString(6) == "[[{\"nodeId\": \"033f2d90d6ba1f771e4b3586b35cc9f825cfcb7cdd7edaa2bfd63f0cb81b17580e\", \"feeBase\": 1000, \"shortChannelId\": \"0x0x1\", \"cltvExpiryDelta\": 144, \"feeProportionalMillionths\": 100}, {\"nodeId\": \"02c15a88ff263cec5bf79c315b17b7f2e083f71d62a880e30281faaac0898cb2b7\", \"feeBase\": 900, \"shortChannelId\": \"0x0x2\", \"cltvExpiryDelta\": 12, \"feeProportionalMillionths\": 200}], [{\"nodeId\": \"026ec3e3438308519a75ca4496822a6c1e229174fbcaadeeb174704c377112c331\", \"feeBase\": 800, \"shortChannelId\": \"0x0x3\", \"cltvExpiryDelta\": 78, \"feeProportionalMillionths\": 300}]]") + } + assert(!result.next()) + } } } From 340b568d532249fd2d67dd567ad008d3300c83a6 Mon Sep 17 00:00:00 2001 From: Pierre-Marie Padiou Date: Wed, 8 Jun 2022 13:03:33 +0200 Subject: [PATCH 086/121] (Minor) Test nits (#2306) * use zero-args method to skip fixture This is the proper way to skip building the fixture. Using `_` actually creates a fixture and then ignores it. * consistency nit * use list/map instead for test cases * add a custom reporter to highlight slowest tests --- .../eclair/balance/CheckBalanceSpec.scala | 2 +- .../channel/publish/TxPublisherSpec.scala | 2 +- .../ChannelStateTestsHelperMethods.scala | 6 +- .../a/WaitForAcceptChannelStateSpec.scala | 8 +- .../a/WaitForOpenChannelStateSpec.scala | 6 +- .../channel/states/e/NormalStateSpec.scala | 8 +- .../states/g/NegotiatingStateSpec.scala | 4 +- .../acinq/eclair/io/PeerConnectionSpec.scala | 2 +- .../eclair/io/ReconnectionTaskSpec.scala | 2 +- .../eclair/payment/MultiPartHandlerSpec.scala | 2 +- .../MultiPartPaymentLifecycleSpec.scala | 2 +- .../eclair/payment/PaymentLifecycleSpec.scala | 8 +- .../testutils/DurationBenchmarkReporter.scala | 44 ++++++ .../protocol/LightningMessageCodecsSpec.scala | 139 +++++++++--------- 14 files changed, 137 insertions(+), 98 deletions(-) create mode 100644 eclair-core/src/test/scala/fr/acinq/eclair/testutils/DurationBenchmarkReporter.scala diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/balance/CheckBalanceSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/balance/CheckBalanceSpec.scala index a16d0e9e38..2c510a434c 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/balance/CheckBalanceSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/balance/CheckBalanceSpec.scala @@ -245,7 +245,7 @@ class CheckBalanceSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with println(res.total) } - test("tx pruning") { _ => + test("tx pruning") { () => val txids = (for (_ <- 0 until 20) yield randomBytes32()).toList val knownTxids = Set(txids(1), txids(3), txids(4), txids(6), txids(9), txids(12), txids(13)) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/TxPublisherSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/TxPublisherSpec.scala index fdd48f7443..5ad93f2b3f 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/TxPublisherSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/TxPublisherSpec.scala @@ -334,7 +334,7 @@ class TxPublisherSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike { factory.expectNoMessage(100 millis) } - test("update publishing attempts") { _ => + test("update publishing attempts") { () => { // No attempts. val attempts = PublishAttempts.empty diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala index c470e466d1..5caba83160 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala @@ -65,7 +65,7 @@ object ChannelStateTestsTags { /** If set, max-htlc-value-in-flight will be set to a low value for Alice. */ val AliceLowMaxHtlcValueInFlight = "alice_low_max_htlc_value_in_flight" /** If set, channels will use option_upfront_shutdown_script. */ - val OptionUpfrontShutdownScript = "option_upfront_shutdown_script" + val UpfrontShutdownScript = "option_upfront_shutdown_script" /** If set, Alice will have a much higher dust limit than Bob. */ val HighDustLimitDifferenceAliceBob = "high_dust_limit_difference_alice_bob" /** If set, Bob will have a much higher dust limit than Alice. */ @@ -155,7 +155,7 @@ trait ChannelStateTestsBase extends Assertions with Eventually { .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.AnchorOutputs))(_.updated(Features.StaticRemoteKey, FeatureSupport.Optional).updated(Features.AnchorOutputs, FeatureSupport.Optional)) .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs))(_.updated(Features.StaticRemoteKey, FeatureSupport.Optional).updated(Features.AnchorOutputs, FeatureSupport.Optional).updated(Features.AnchorOutputsZeroFeeHtlcTx, FeatureSupport.Optional)) .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.ShutdownAnySegwit))(_.updated(Features.ShutdownAnySegwit, FeatureSupport.Optional)) - .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.OptionUpfrontShutdownScript))(_.updated(Features.UpfrontShutdownScript, FeatureSupport.Optional)) + .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.UpfrontShutdownScript))(_.updated(Features.UpfrontShutdownScript, FeatureSupport.Optional)) .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.ChannelType))(_.updated(Features.ChannelType, FeatureSupport.Optional)) .initFeatures() val bobInitFeatures = Bob.nodeParams.features @@ -164,7 +164,7 @@ trait ChannelStateTestsBase extends Assertions with Eventually { .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.AnchorOutputs))(_.updated(Features.StaticRemoteKey, FeatureSupport.Optional).updated(Features.AnchorOutputs, FeatureSupport.Optional)) .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs))(_.updated(Features.StaticRemoteKey, FeatureSupport.Optional).updated(Features.AnchorOutputs, FeatureSupport.Optional).updated(Features.AnchorOutputsZeroFeeHtlcTx, FeatureSupport.Optional)) .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.ShutdownAnySegwit))(_.updated(Features.ShutdownAnySegwit, FeatureSupport.Optional)) - .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.OptionUpfrontShutdownScript))(_.updated(Features.UpfrontShutdownScript, FeatureSupport.Optional)) + .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.UpfrontShutdownScript))(_.updated(Features.UpfrontShutdownScript, FeatureSupport.Optional)) .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.ChannelType))(_.updated(Features.ChannelType, FeatureSupport.Optional)) .initFeatures() diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForAcceptChannelStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForAcceptChannelStateSpec.scala index 0fa87ba812..040e255f29 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForAcceptChannelStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForAcceptChannelStateSpec.scala @@ -148,7 +148,7 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS aliceOrigin.expectMsgType[Status.Failure] } - test("recv AcceptChannel (anchor outputs channel type without enabling the feature)") { _ => + test("recv AcceptChannel (anchor outputs channel type without enabling the feature)") { () => val setup = init(Alice.nodeParams, Bob.nodeParams, wallet = new NoOpOnChainWallet()) import setup._ @@ -282,7 +282,7 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS aliceOrigin.expectNoMessage() } - test("recv AcceptChannel (upfront shutdown script)", Tag(ChannelStateTestsTags.OptionUpfrontShutdownScript)) { f => + test("recv AcceptChannel (upfront shutdown script)", Tag(ChannelStateTestsTags.UpfrontShutdownScript)) { f => import f._ val accept = bob2alice.expectMsgType[AcceptChannel] assert(accept.upfrontShutdownScript_opt.contains(bob.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CREATED].localParams.defaultFinalScriptPubKey)) @@ -292,7 +292,7 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS aliceOrigin.expectNoMessage() } - test("recv AcceptChannel (empty upfront shutdown script)", Tag(ChannelStateTestsTags.OptionUpfrontShutdownScript)) { f => + test("recv AcceptChannel (empty upfront shutdown script)", Tag(ChannelStateTestsTags.UpfrontShutdownScript)) { f => import f._ val accept = bob2alice.expectMsgType[AcceptChannel] assert(accept.upfrontShutdownScript_opt.contains(bob.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CREATED].localParams.defaultFinalScriptPubKey)) @@ -303,7 +303,7 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS aliceOrigin.expectNoMessage() } - test("recv AcceptChannel (invalid upfront shutdown script)", Tag(ChannelStateTestsTags.OptionUpfrontShutdownScript)) { f => + test("recv AcceptChannel (invalid upfront shutdown script)", Tag(ChannelStateTestsTags.UpfrontShutdownScript)) { f => import f._ val accept = bob2alice.expectMsgType[AcceptChannel] val accept1 = accept.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScriptTlv(ByteVector.fromValidHex("deadbeef")))) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForOpenChannelStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForOpenChannelStateSpec.scala index f4220136fd..b76db60e96 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForOpenChannelStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForOpenChannelStateSpec.scala @@ -261,7 +261,7 @@ class WaitForOpenChannelStateSpec extends TestKitBaseClass with FixtureAnyFunSui awaitCond(bob.stateName == WAIT_FOR_FUNDING_CREATED) } - test("recv OpenChannel (upfront shutdown script)", Tag(ChannelStateTestsTags.OptionUpfrontShutdownScript)) { f => + test("recv OpenChannel (upfront shutdown script)", Tag(ChannelStateTestsTags.UpfrontShutdownScript)) { f => import f._ val open = alice2bob.expectMsgType[OpenChannel] assert(open.upfrontShutdownScript_opt.contains(alice.stateData.asInstanceOf[DATA_WAIT_FOR_ACCEPT_CHANNEL].initFunder.localParams.defaultFinalScriptPubKey)) @@ -270,7 +270,7 @@ class WaitForOpenChannelStateSpec extends TestKitBaseClass with FixtureAnyFunSui assert(bob.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CREATED].remoteParams.shutdownScript == open.upfrontShutdownScript_opt) } - test("recv OpenChannel (empty upfront shutdown script)", Tag(ChannelStateTestsTags.OptionUpfrontShutdownScript)) { f => + test("recv OpenChannel (empty upfront shutdown script)", Tag(ChannelStateTestsTags.UpfrontShutdownScript)) { f => import f._ val open = alice2bob.expectMsgType[OpenChannel] val open1 = open.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScriptTlv(ByteVector.empty))) @@ -279,7 +279,7 @@ class WaitForOpenChannelStateSpec extends TestKitBaseClass with FixtureAnyFunSui assert(bob.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CREATED].remoteParams.shutdownScript.isEmpty) } - test("recv OpenChannel (invalid upfront shutdown script)", Tag(ChannelStateTestsTags.OptionUpfrontShutdownScript)) { f => + test("recv OpenChannel (invalid upfront shutdown script)", Tag(ChannelStateTestsTags.UpfrontShutdownScript)) { f => import f._ val open = alice2bob.expectMsgType[OpenChannel] val open1 = open.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScriptTlv(ByteVector.fromValidHex("deadbeef")))) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala index bafc54d154..2930c973ba 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala @@ -2438,7 +2438,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with awaitCond(alice.stateName == NORMAL) } - test("recv CMD_CLOSE (with a script that does not match our upfront shutdown script)", Tag(ChannelStateTestsTags.OptionUpfrontShutdownScript)) { f => + test("recv CMD_CLOSE (with a script that does not match our upfront shutdown script)", Tag(ChannelStateTestsTags.UpfrontShutdownScript)) { f => import f._ val sender = TestProbe() val shutdownScript = Script.write(Script.pay2wpkh(randomKey().publicKey)) @@ -2446,7 +2446,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with sender.expectMsgType[RES_FAILURE[CMD_CLOSE, InvalidFinalScript]] } - test("recv CMD_CLOSE (with a script that does match our upfront shutdown script)", Tag(ChannelStateTestsTags.OptionUpfrontShutdownScript)) { f => + test("recv CMD_CLOSE (with a script that does match our upfront shutdown script)", Tag(ChannelStateTestsTags.UpfrontShutdownScript)) { f => import f._ val sender = TestProbe() val shutdownScript = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localParams.defaultFinalScriptPubKey @@ -2458,7 +2458,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].localShutdown.isDefined) } - test("recv CMD_CLOSE (upfront shutdown script)", Tag(ChannelStateTestsTags.OptionUpfrontShutdownScript)) { f => + test("recv CMD_CLOSE (upfront shutdown script)", Tag(ChannelStateTestsTags.UpfrontShutdownScript)) { f => import f._ val sender = TestProbe() alice ! CMD_CLOSE(sender.ref, None, None) @@ -2611,7 +2611,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with } } - test("recv Shutdown (with a script that does not match the upfront shutdown script)", Tag(ChannelStateTestsTags.OptionUpfrontShutdownScript)) { f => + test("recv Shutdown (with a script that does not match the upfront shutdown script)", Tag(ChannelStateTestsTags.UpfrontShutdownScript)) { f => import f._ bob ! Shutdown(ByteVector32.Zeroes, Script.write(Script.pay2wpkh(randomKey().publicKey))) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/g/NegotiatingStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/g/NegotiatingStateSpec.scala index bf5acc288a..63893abd7f 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/g/NegotiatingStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/g/NegotiatingStateSpec.scala @@ -175,7 +175,7 @@ class NegotiatingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike testClosingSignedDifferentFees(f) } - test("recv ClosingSigned (theirCloseFee != ourCloseFee, anchor outputs, upfront shutdown scripts)", Tag(ChannelStateTestsTags.AnchorOutputs), Tag(ChannelStateTestsTags.OptionUpfrontShutdownScript)) { f => + test("recv ClosingSigned (theirCloseFee != ourCloseFee, anchor outputs, upfront shutdown scripts)", Tag(ChannelStateTestsTags.AnchorOutputs), Tag(ChannelStateTestsTags.UpfrontShutdownScript)) { f => testClosingSignedDifferentFees(f) } @@ -242,7 +242,7 @@ class NegotiatingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike testClosingSignedSameFees(f) } - test("recv ClosingSigned (theirCloseFee == ourCloseFee, upfront shutdown script)", Tag(ChannelStateTestsTags.OptionUpfrontShutdownScript)) { f => + test("recv ClosingSigned (theirCloseFee == ourCloseFee, upfront shutdown script)", Tag(ChannelStateTestsTags.UpfrontShutdownScript)) { f => testClosingSignedSameFees(f) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerConnectionSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerConnectionSpec.scala index 6333ae77ab..fffe22d180 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerConnectionSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerConnectionSpec.scala @@ -468,7 +468,7 @@ class PeerConnectionSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wi transport.expectMsg(message) } - test("filter private IP addresses") { _ => + test("filter private IP addresses") { () => val testCases = Seq( NodeAddress.fromParts("127.0.0.1", 9735).get -> false, NodeAddress.fromParts("0.0.0.0", 9735).get -> false, diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/io/ReconnectionTaskSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/io/ReconnectionTaskSpec.scala index fec76a7a6e..1e4721819b 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/io/ReconnectionTaskSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/io/ReconnectionTaskSpec.scala @@ -225,7 +225,7 @@ class ReconnectionTaskSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike mockServer.close() } - test("select peer address for reconnection") { _ => + test("select peer address for reconnection") { () => val nodeParams = mock[NodeParams] val clearnet = NodeAddress.fromParts("1.2.3.4", 9735).get val tor = NodeAddress.fromParts("iq7zhmhck54vcax2vlrdcavq2m32wao7ekh6jyeglmnuuvv3js57r4id.onion", 9735).get diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartHandlerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartHandlerSpec.scala index 52495bbec0..a4198eccee 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartHandlerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartHandlerSpec.scala @@ -182,7 +182,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike assert(pr2.relativeExpiry == 60.seconds) } - test("Invoice generation with trampoline support") { _ => + test("Invoice generation with trampoline support") { () => val sender = TestProbe() { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartPaymentLifecycleSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartPaymentLifecycleSpec.scala index 8b4591a212..693d81028e 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartPaymentLifecycleSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartPaymentLifecycleSpec.scala @@ -360,7 +360,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS assert(routeRequest.ignore.channels.map(_.shortChannelId) == Set(channelUpdateBE1.shortChannelId)) } - test("update routing hints") { _ => + test("update routing hints") { () => val routingHints = Seq( Seq(ExtraHop(a, ShortChannelId(1), 10 msat, 0, CltvExpiryDelta(12)), ExtraHop(b, ShortChannelId(2), 0 msat, 100, CltvExpiryDelta(24))), Seq(ExtraHop(a, ShortChannelId(3), 1 msat, 10, CltvExpiryDelta(144))) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala index f766cdc821..e1882d59d5 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala @@ -97,7 +97,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { result) } - test("send to route") { _ => + test("send to route") { () => val payFixture = createPaymentLifecycle(recordMetrics = false) import payFixture._ import cfg._ @@ -733,7 +733,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { metricsListener.expectNoMessage() } - test("filter errors properly") { _ => + test("filter errors properly") { () => val failures = Seq( LocalFailure(defaultAmountMsat, Nil, RouteNotFound), RemoteFailure(defaultAmountMsat, channelHopFromUpdate(a, b, update_ab) :: Nil, Sphinx.DecryptedFailurePacket(a, TemporaryNodeFailure)), @@ -749,7 +749,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { assert(filtered == expected) } - test("ignore failed nodes/channels") { _ => + test("ignore failed nodes/channels") { () => val route_abcd = channelHopFromUpdate(a, b, update_ab) :: channelHopFromUpdate(b, c, update_bc) :: channelHopFromUpdate(c, d, update_cd) :: Nil val testCases = Seq( // local failures -> ignore first channel if there is one @@ -804,7 +804,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { eventListener.expectNoMessage(100 millis) } - test("send to route (no retry on error") { _ => + test("send to route (no retry on error") { () => val payFixture = createPaymentLifecycle() import payFixture._ import cfg._ diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/testutils/DurationBenchmarkReporter.scala b/eclair-core/src/test/scala/fr/acinq/eclair/testutils/DurationBenchmarkReporter.scala new file mode 100644 index 0000000000..3c1817a3ad --- /dev/null +++ b/eclair-core/src/test/scala/fr/acinq/eclair/testutils/DurationBenchmarkReporter.scala @@ -0,0 +1,44 @@ +package fr.acinq.eclair.testutils + +import org.scalatest.Reporter +import org.scalatest.events.{Event, RunCompleted, SuiteCompleted, TestSucceeded} + +/** + * A scalatest reporter that displays the slowest tests and suites. Useful to detect pain points and speed up the + * overall build. + * + * To activate, edit the project's pom.xml and set: + * {{{ + * scalatest-maven-plugin + * ... + * + * false + * fr.acinq.eclair.testutils.DurationBenchmarkReporter + * ... + * + * }}} + * + * Then run the tests as usual. + * + * Note that test parallelization needs to be disabled to have accurate measurement of durations, which will considerably + * slow down the build time. + */ +class DurationBenchmarkReporter() extends Reporter { + + val suites = collection.mutable.Map.empty[String, Long] + val tests = collection.mutable.Map.empty[String, Long] + + override def apply(event: Event): Unit = event match { + case e: TestSucceeded => + e.duration.map(d => tests.addOne(e.testName, d)) + case e: SuiteCompleted => + e.duration.map(d => suites.addOne(e.suiteName, d)) + case _: RunCompleted => + println("top 20 slowest tests:") + tests.toList.sortBy(-_._2).take(20).foreach { case (name, duration) => println(s" ${duration}ms\t${name} ") } + println("top 20 slowest suites:") + suites.toList.sortBy(-_._2).take(20).foreach { case (name, duration) => println(s" ${duration}ms\t${name} ") } + case _ => + () + } +} diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecsSpec.scala index 899de909a0..9893c6b268 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecsSpec.scala @@ -341,49 +341,44 @@ class LightningMessageCodecsSpec extends AnyFunSuite { } test("encode/decode all channel messages") { - val open = OpenChannel(randomBytes32(), randomBytes32(), 3 sat, 4 msat, 5 sat, UInt64(6), 7 sat, 8 msat, FeeratePerKw(9 sat), CltvExpiryDelta(10), 11, publicKey(1), point(2), point(3), point(4), point(5), point(6), ChannelFlags.Private) - val accept = AcceptChannel(randomBytes32(), 3 sat, UInt64(4), 5 sat, 6 msat, 7, CltvExpiryDelta(8), 9, publicKey(1), point(2), point(3), point(4), point(5), point(6)) - val funding_created = FundingCreated(randomBytes32(), bin32(0), 3, randomBytes64()) - val funding_signed = FundingSigned(randomBytes32(), randomBytes64()) - val funding_locked = FundingLocked(randomBytes32(), point(2)) - val update_fee = UpdateFee(randomBytes32(), FeeratePerKw(2 sat)) - val shutdown = Shutdown(randomBytes32(), bin(47, 0)) - val closing_signed = ClosingSigned(randomBytes32(), 2 sat, randomBytes64()) - val update_add_htlc = UpdateAddHtlc(randomBytes32(), 2, 3 msat, bin32(0), CltvExpiry(4), TestConstants.emptyOnionPacket) - val update_fulfill_htlc = UpdateFulfillHtlc(randomBytes32(), 2, bin32(0)) - val update_fail_htlc = UpdateFailHtlc(randomBytes32(), 2, bin(154, 0)) - val update_fail_malformed_htlc = UpdateFailMalformedHtlc(randomBytes32(), 2, randomBytes32(), 1111) - val commit_sig = CommitSig(randomBytes32(), randomBytes64(), randomBytes64() :: randomBytes64() :: randomBytes64() :: Nil) - val revoke_and_ack = RevokeAndAck(randomBytes32(), scalar(0), point(1)) - val channel_announcement = ChannelAnnouncement(randomBytes64(), randomBytes64(), randomBytes64(), randomBytes64(), Features(bin(7, 9)), Block.RegtestGenesisBlock.hash, ShortChannelId(1), randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey) - val node_announcement = NodeAnnouncement(randomBytes64(), Features(DataLossProtect -> Optional), 1 unixsec, randomKey().publicKey, Color(100.toByte, 200.toByte, 300.toByte), "node-alias", IPv4(InetAddress.getByAddress(Array[Byte](192.toByte, 168.toByte, 1.toByte, 42.toByte)).asInstanceOf[Inet4Address], 42000) :: Nil) - val channel_update = ChannelUpdate(randomBytes64(), Block.RegtestGenesisBlock.hash, ShortChannelId(1), 2 unixsec, ChannelUpdate.ChannelFlags.DUMMY, CltvExpiryDelta(3), 4 msat, 5 msat, 6, None) - val announcement_signatures = AnnouncementSignatures(randomBytes32(), ShortChannelId(42), randomBytes64(), randomBytes64()) - val gossip_timestamp_filter = GossipTimestampFilter(Block.RegtestGenesisBlock.blockId, 100000 unixsec, 1500) - val query_short_channel_id = QueryShortChannelIds(Block.RegtestGenesisBlock.blockId, EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(ShortChannelId(142), ShortChannelId(15465), ShortChannelId(4564676))), TlvStream.empty) val unknownTlv = GenericTlv(UInt64(5), ByteVector.fromValidHex("deadbeef")) - val query_channel_range = QueryChannelRange(Block.RegtestGenesisBlock.blockId, - BlockHeight(100000), - 1500, - TlvStream(QueryChannelRangeTlv.QueryFlags(QueryChannelRangeTlv.QueryFlags.WANT_ALL) :: Nil, unknownTlv :: Nil)) - val reply_channel_range = ReplyChannelRange(Block.RegtestGenesisBlock.blockId, - BlockHeight(100000), 1500, 1, - EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(ShortChannelId(142), ShortChannelId(15465), ShortChannelId(4564676))), - TlvStream( - EncodedTimestamps(EncodingType.UNCOMPRESSED, List(Timestamps(1 unixsec, 1 unixsec), Timestamps(2 unixsec, 2 unixsec), Timestamps(3 unixsec, 3 unixsec))) :: EncodedChecksums(List(Checksums(1, 1), Checksums(2, 2), Checksums(3, 3))) :: Nil, - unknownTlv :: Nil) + val msgs = List( + OpenChannel(randomBytes32(), randomBytes32(), 3 sat, 4 msat, 5 sat, UInt64(6), 7 sat, 8 msat, FeeratePerKw(9 sat), CltvExpiryDelta(10), 11, publicKey(1), point(2), point(3), point(4), point(5), point(6), ChannelFlags.Private), + AcceptChannel(randomBytes32(), 3 sat, UInt64(4), 5 sat, 6 msat, 7, CltvExpiryDelta(8), 9, publicKey(1), point(2), point(3), point(4), point(5), point(6)), + FundingCreated(randomBytes32(), bin32(0), 3, randomBytes64()), + FundingSigned(randomBytes32(), randomBytes64()), + FundingLocked(randomBytes32(), point(2)), + UpdateFee(randomBytes32(), FeeratePerKw(2 sat)), + Shutdown(randomBytes32(), bin(47, 0)), + ClosingSigned(randomBytes32(), 2 sat, randomBytes64()), + UpdateAddHtlc(randomBytes32(), 2, 3 msat, bin32(0), CltvExpiry(4), TestConstants.emptyOnionPacket), + UpdateFulfillHtlc(randomBytes32(), 2, bin32(0)), + UpdateFailHtlc(randomBytes32(), 2, bin(154, 0)), + UpdateFailMalformedHtlc(randomBytes32(), 2, randomBytes32(), 1111), + CommitSig(randomBytes32(), randomBytes64(), randomBytes64() :: randomBytes64() :: randomBytes64() :: Nil), + RevokeAndAck(randomBytes32(), scalar(0), point(1)), + ChannelAnnouncement(randomBytes64(), randomBytes64(), randomBytes64(), randomBytes64(), Features(bin(7, 9)), Block.RegtestGenesisBlock.hash, ShortChannelId(1), randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey), + NodeAnnouncement(randomBytes64(), Features(DataLossProtect -> Optional), 1 unixsec, randomKey().publicKey, Color(100.toByte, 200.toByte, 300.toByte), "node-alias", IPv4(InetAddress.getByAddress(Array[Byte](192.toByte, 168.toByte, 1.toByte, 42.toByte)).asInstanceOf[Inet4Address], 42000) :: Nil), + ChannelUpdate(randomBytes64(), Block.RegtestGenesisBlock.hash, ShortChannelId(1), 2 unixsec, ChannelUpdate.ChannelFlags.DUMMY, CltvExpiryDelta(3), 4 msat, 5 msat, 6, None), + AnnouncementSignatures(randomBytes32(), ShortChannelId(42), randomBytes64(), randomBytes64()), + GossipTimestampFilter(Block.RegtestGenesisBlock.blockId, 100000 unixsec, 1500), + QueryShortChannelIds(Block.RegtestGenesisBlock.blockId, EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(ShortChannelId(142), ShortChannelId(15465), ShortChannelId(4564676))), TlvStream.empty), + QueryChannelRange(Block.RegtestGenesisBlock.blockId, + BlockHeight(100000), + 1500, + TlvStream(QueryChannelRangeTlv.QueryFlags(QueryChannelRangeTlv.QueryFlags.WANT_ALL) :: Nil, unknownTlv :: Nil)), + ReplyChannelRange(Block.RegtestGenesisBlock.blockId, + BlockHeight(100000), 1500, 1, + EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(ShortChannelId(142), ShortChannelId(15465), ShortChannelId(4564676))), + TlvStream( + EncodedTimestamps(EncodingType.UNCOMPRESSED, List(Timestamps(1 unixsec, 1 unixsec), Timestamps(2 unixsec, 2 unixsec), Timestamps(3 unixsec, 3 unixsec))) :: EncodedChecksums(List(Checksums(1, 1), Checksums(2, 2), Checksums(3, 3))) :: Nil, + unknownTlv :: Nil) + ), + Ping(100, bin(10, 1)), + Pong(bin(10, 1)), + ChannelReestablish(randomBytes32(), 242842L, 42L, randomKey(), randomKey().publicKey), + UnknownMessage(tag = 60000, data = ByteVector32.One.bytes), ) - val ping = Ping(100, bin(10, 1)) - val pong = Pong(bin(10, 1)) - val channel_reestablish = ChannelReestablish(randomBytes32(), 242842L, 42L, randomKey(), randomKey().publicKey) - - val unknown_message = UnknownMessage(tag = 60000, data = ByteVector32.One.bytes) - - val msgs: List[LightningMessage] = - open :: accept :: funding_created :: funding_signed :: funding_locked :: update_fee :: shutdown :: closing_signed :: - update_add_htlc :: update_fulfill_htlc :: update_fail_htlc :: update_fail_malformed_htlc :: commit_sig :: revoke_and_ack :: - channel_announcement :: node_announcement :: channel_update :: gossip_timestamp_filter :: query_short_channel_id :: query_channel_range :: reply_channel_range :: announcement_signatures :: - ping :: pong :: channel_reestablish :: unknown_message :: Nil msgs.foreach { msg => { @@ -425,39 +420,39 @@ class LightningMessageCodecsSpec extends AnyFunSuite { case class TestItem(msg: Any, hex: String) test("test vectors for extended channel queries ") { - val query_channel_range = QueryChannelRange(Block.RegtestGenesisBlock.blockId, BlockHeight(100000), 1500, TlvStream.empty) - val query_channel_range_timestamps_checksums = QueryChannelRange(Block.RegtestGenesisBlock.blockId, - BlockHeight(35000), - 100, - TlvStream(QueryChannelRangeTlv.QueryFlags(QueryChannelRangeTlv.QueryFlags.WANT_ALL))) - val reply_channel_range = ReplyChannelRange(Block.RegtestGenesisBlock.blockId, BlockHeight(756230), 1500, 1, - EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(ShortChannelId(142), ShortChannelId(15465), ShortChannelId(4564676))), None, None) - val reply_channel_range_zlib = ReplyChannelRange(Block.RegtestGenesisBlock.blockId, BlockHeight(1600), 110, 1, - EncodedShortChannelIds(EncodingType.COMPRESSED_ZLIB, List(ShortChannelId(142), ShortChannelId(15465), ShortChannelId(265462))), None, None) - val reply_channel_range_timestamps_checksums = ReplyChannelRange(Block.RegtestGenesisBlock.blockId, BlockHeight(122334), 1500, 1, - EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(ShortChannelId(12355), ShortChannelId(489686), ShortChannelId(4645313))), - Some(EncodedTimestamps(EncodingType.UNCOMPRESSED, List(Timestamps(164545 unixsec, 948165 unixsec), Timestamps(489645 unixsec, 4786864 unixsec), Timestamps(46456 unixsec, 9788415 unixsec)))), - Some(EncodedChecksums(List(Checksums(1111, 2222), Checksums(3333, 4444), Checksums(5555, 6666))))) - val reply_channel_range_timestamps_checksums_zlib = ReplyChannelRange(Block.RegtestGenesisBlock.blockId, BlockHeight(122334), 1500, 1, - EncodedShortChannelIds(EncodingType.COMPRESSED_ZLIB, List(ShortChannelId(12355), ShortChannelId(489686), ShortChannelId(4645313))), - Some(EncodedTimestamps(EncodingType.COMPRESSED_ZLIB, List(Timestamps(164545 unixsec, 948165 unixsec), Timestamps(489645 unixsec, 4786864 unixsec), Timestamps(46456 unixsec, 9788415 unixsec)))), - Some(EncodedChecksums(List(Checksums(1111, 2222), Checksums(3333, 4444), Checksums(5555, 6666))))) - val query_short_channel_id = QueryShortChannelIds(Block.RegtestGenesisBlock.blockId, EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(ShortChannelId(142), ShortChannelId(15465), ShortChannelId(4564676))), TlvStream.empty) - val query_short_channel_id_zlib = QueryShortChannelIds(Block.RegtestGenesisBlock.blockId, EncodedShortChannelIds(EncodingType.COMPRESSED_ZLIB, List(ShortChannelId(4564), ShortChannelId(178622), ShortChannelId(4564676))), TlvStream.empty) - val query_short_channel_id_flags = QueryShortChannelIds(Block.RegtestGenesisBlock.blockId, EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(ShortChannelId(12232), ShortChannelId(15556), ShortChannelId(4564676))), TlvStream(QueryShortChannelIdsTlv.EncodedQueryFlags(EncodingType.COMPRESSED_ZLIB, List(1, 2, 4)))) - val query_short_channel_id_flags_zlib = QueryShortChannelIds(Block.RegtestGenesisBlock.blockId, EncodedShortChannelIds(EncodingType.COMPRESSED_ZLIB, List(ShortChannelId(14200), ShortChannelId(46645), ShortChannelId(4564676))), TlvStream(QueryShortChannelIdsTlv.EncodedQueryFlags(EncodingType.COMPRESSED_ZLIB, List(1, 2, 4)))) val refs = Map( - query_channel_range -> hex"01070f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206000186a0000005dc", - query_channel_range_timestamps_checksums -> hex"01070f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206000088b800000064010103", - reply_channel_range -> hex"01080f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206000b8a06000005dc01001900000000000000008e0000000000003c69000000000045a6c4", - reply_channel_range_zlib -> hex"01080f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206000006400000006e01001601789c636000833e08659309a65878be010010a9023a", - reply_channel_range_timestamps_checksums -> hex"01080f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e22060001ddde000005dc01001900000000000000304300000000000778d6000000000046e1c1011900000282c1000e77c5000778ad00490ab00000b57800955bff031800000457000008ae00000d050000115c000015b300001a0a", - reply_channel_range_timestamps_checksums_zlib -> hex"01080f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e22060001ddde000005dc01001801789c63600001036730c55e710d4cbb3d3c080017c303b1012201789c63606a3ac8c0577e9481bd622d8327d7060686ad150c53a3ff0300554707db031800000457000008ae00000d050000115c000015b300001a0a", - query_short_channel_id -> hex"01050f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206001900000000000000008e0000000000003c69000000000045a6c4", - query_short_channel_id_zlib -> hex"01050f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206001801789c63600001c12b608a69e73e30edbaec0800203b040e", - query_short_channel_id_flags -> hex"01050f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e22060019000000000000002fc80000000000003cc4000000000045a6c4010c01789c6364620100000e0008", - query_short_channel_id_flags_zlib -> hex"01050f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206001801789c63600001f30a30c5b0cd144cb92e3b020017c6034a010c01789c6364620100000e0008" + QueryChannelRange(Block.RegtestGenesisBlock.blockId, BlockHeight(100000), 1500, TlvStream.empty) -> + hex"01070f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206000186a0000005dc", + QueryChannelRange(Block.RegtestGenesisBlock.blockId, + BlockHeight(35000), + 100, + TlvStream(QueryChannelRangeTlv.QueryFlags(QueryChannelRangeTlv.QueryFlags.WANT_ALL))) -> + hex"01070f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206000088b800000064010103", + ReplyChannelRange(Block.RegtestGenesisBlock.blockId, BlockHeight(756230), 1500, 1, + EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(ShortChannelId(142), ShortChannelId(15465), ShortChannelId(4564676))), None, None) -> + hex"01080f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206000b8a06000005dc01001900000000000000008e0000000000003c69000000000045a6c4", + ReplyChannelRange(Block.RegtestGenesisBlock.blockId, BlockHeight(1600), 110, 1, + EncodedShortChannelIds(EncodingType.COMPRESSED_ZLIB, List(ShortChannelId(142), ShortChannelId(15465), ShortChannelId(265462))), None, None) -> + hex"01080f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206000006400000006e01001601789c636000833e08659309a65878be010010a9023a", + ReplyChannelRange(Block.RegtestGenesisBlock.blockId, BlockHeight(122334), 1500, 1, + EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(ShortChannelId(12355), ShortChannelId(489686), ShortChannelId(4645313))), + Some(EncodedTimestamps(EncodingType.UNCOMPRESSED, List(Timestamps(164545 unixsec, 948165 unixsec), Timestamps(489645 unixsec, 4786864 unixsec), Timestamps(46456 unixsec, 9788415 unixsec)))), + Some(EncodedChecksums(List(Checksums(1111, 2222), Checksums(3333, 4444), Checksums(5555, 6666))))) -> + hex"01080f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e22060001ddde000005dc01001900000000000000304300000000000778d6000000000046e1c1011900000282c1000e77c5000778ad00490ab00000b57800955bff031800000457000008ae00000d050000115c000015b300001a0a", + ReplyChannelRange(Block.RegtestGenesisBlock.blockId, BlockHeight(122334), 1500, 1, + EncodedShortChannelIds(EncodingType.COMPRESSED_ZLIB, List(ShortChannelId(12355), ShortChannelId(489686), ShortChannelId(4645313))), + Some(EncodedTimestamps(EncodingType.COMPRESSED_ZLIB, List(Timestamps(164545 unixsec, 948165 unixsec), Timestamps(489645 unixsec, 4786864 unixsec), Timestamps(46456 unixsec, 9788415 unixsec)))), + Some(EncodedChecksums(List(Checksums(1111, 2222), Checksums(3333, 4444), Checksums(5555, 6666))))) -> + hex"01080f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e22060001ddde000005dc01001801789c63600001036730c55e710d4cbb3d3c080017c303b1012201789c63606a3ac8c0577e9481bd622d8327d7060686ad150c53a3ff0300554707db031800000457000008ae00000d050000115c000015b300001a0a", + QueryShortChannelIds(Block.RegtestGenesisBlock.blockId, EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(ShortChannelId(142), ShortChannelId(15465), ShortChannelId(4564676))), TlvStream.empty) -> + hex"01050f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206001900000000000000008e0000000000003c69000000000045a6c4", + QueryShortChannelIds(Block.RegtestGenesisBlock.blockId, EncodedShortChannelIds(EncodingType.COMPRESSED_ZLIB, List(ShortChannelId(4564), ShortChannelId(178622), ShortChannelId(4564676))), TlvStream.empty) -> + hex"01050f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206001801789c63600001c12b608a69e73e30edbaec0800203b040e", + QueryShortChannelIds(Block.RegtestGenesisBlock.blockId, EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(ShortChannelId(12232), ShortChannelId(15556), ShortChannelId(4564676))), TlvStream(QueryShortChannelIdsTlv.EncodedQueryFlags(EncodingType.COMPRESSED_ZLIB, List(1, 2, 4)))) -> + hex"01050f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e22060019000000000000002fc80000000000003cc4000000000045a6c4010c01789c6364620100000e0008", + QueryShortChannelIds(Block.RegtestGenesisBlock.blockId, EncodedShortChannelIds(EncodingType.COMPRESSED_ZLIB, List(ShortChannelId(14200), ShortChannelId(46645), ShortChannelId(4564676))), TlvStream(QueryShortChannelIdsTlv.EncodedQueryFlags(EncodingType.COMPRESSED_ZLIB, List(1, 2, 4)))) -> + hex"01050f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206001801789c63600001f30a30c5b0cd144cb92e3b020017c6034a010c01789c6364620100000e0008" ) val items = refs.map { case (obj, refbin) => From 682e9bf6a76473ae288f17ba8856e02e962c8b6f Mon Sep 17 00:00:00 2001 From: Bastien Teinturier <31281497+t-bast@users.noreply.github.com> Date: Wed, 8 Jun 2022 13:33:55 +0200 Subject: [PATCH 087/121] Fix minimal node fixture flakyness (#2302) There was a race condition between the `wait_for_funding_locked` state and the `normal` state. --- .../eclair/integration/basic/TwoNodesIntegrationSpec.scala | 1 - .../integration/basic/fixtures/MinimalNodeFixture.scala | 7 +++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/TwoNodesIntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/TwoNodesIntegrationSpec.scala index cbeb0e4fa6..ae2ab79092 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/TwoNodesIntegrationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/TwoNodesIntegrationSpec.scala @@ -9,7 +9,6 @@ import org.scalatest.TestData import org.scalatest.concurrent.IntegrationPatience import scodec.bits.HexStringSyntax - /** * This test checks the integration between Channel and Router (events, etc.) */ diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/fixtures/MinimalNodeFixture.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/fixtures/MinimalNodeFixture.scala index 3316c5cb3d..9962d5972b 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/fixtures/MinimalNodeFixture.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/fixtures/MinimalNodeFixture.scala @@ -24,6 +24,7 @@ import fr.acinq.eclair.router.Router import fr.acinq.eclair.wire.protocol.IPAddress import fr.acinq.eclair.{BlockHeight, MilliSatoshi, MilliSatoshiLong, NodeParams, ShortChannelId, TestBitcoinCoreClient, TestDatabases, TestFeeEstimator} import org.scalatest.Assertions +import org.scalatest.concurrent.Eventually.eventually import java.net.InetAddress import java.util.UUID @@ -162,8 +163,10 @@ object MinimalNodeFixture extends Assertions { watch1.replyTo ! WatchFundingConfirmedTriggered(blockHeight, txIndex, fundingTx) watch2.replyTo ! WatchFundingConfirmedTriggered(blockHeight, txIndex, fundingTx) - waitReady(node1, channelId) - waitReady(node2, channelId) + eventually { + assert(getChannelState(node1, channelId) == NORMAL) + assert(getChannelState(node2, channelId) == NORMAL) + } val data1After = getChannelData(node1, channelId).asInstanceOf[DATA_NORMAL] val data2After = getChannelData(node2, channelId).asInstanceOf[DATA_NORMAL] From 472275573cd4b9576d54a6e06373223877653554 Mon Sep 17 00:00:00 2001 From: Thomas HUET <81159533+thomash-acinq@users.noreply.github.com> Date: Wed, 8 Jun 2022 13:37:19 +0200 Subject: [PATCH 088/121] Estimate balances of remote channels (#2272) We use past payments attempts to estimate the balances of all channels. This can later be used in path-finding to improve the reliability of payments. --- eclair-core/src/main/resources/reference.conf | 1 + .../scala/fr/acinq/eclair/NodeParams.scala | 3 +- .../payment/send/PaymentLifecycle.scala | 21 +- .../remote/EclairInternalsSerializer.scala | 3 +- .../acinq/eclair/router/BalanceEstimate.scala | 331 ++++++++++++++++++ .../fr/acinq/eclair/router/Monitoring.scala | 6 + .../eclair/router/RouteCalculation.scala | 12 +- .../scala/fr/acinq/eclair/router/Router.scala | 44 ++- .../acinq/eclair/router/StaleChannels.scala | 4 +- .../fr/acinq/eclair/router/Validation.scala | 59 ++-- .../scala/fr/acinq/eclair/TestConstants.scala | 6 +- .../eclair/payment/PaymentLifecycleSpec.scala | 29 ++ .../eclair/router/BalanceEstimateSpec.scala | 292 +++++++++++++++ .../fr/acinq/eclair/router/RouterSpec.scala | 8 +- 14 files changed, 767 insertions(+), 52 deletions(-) create mode 100644 eclair-core/src/main/scala/fr/acinq/eclair/router/BalanceEstimate.scala create mode 100644 eclair-core/src/test/scala/fr/acinq/eclair/router/BalanceEstimateSpec.scala diff --git a/eclair-core/src/main/resources/reference.conf b/eclair-core/src/main/resources/reference.conf index ae606c4774..8b6739d551 100644 --- a/eclair-core/src/main/resources/reference.conf +++ b/eclair-core/src/main/resources/reference.conf @@ -230,6 +230,7 @@ eclair { channel-exclude-duration = 60 seconds // when a temporary channel failure is returned, we exclude the channel from our payment routes for this duration broadcast-interval = 60 seconds // see BOLT #7 init-timeout = 5 minutes + balance-estimate-half-life = 1 day // time after which the confidence of the balance estimate is halved sync { request-node-announcements = true // if true we will ask for node announcements when we receive channel ids that we don't know diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala index 7fedf8d21d..155a56c1e1 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala @@ -493,7 +493,8 @@ object NodeParams extends Logging { encodingType = EncodingType.UNCOMPRESSED, channelRangeChunkSize = config.getInt("router.sync.channel-range-chunk-size"), channelQueryChunkSize = config.getInt("router.sync.channel-query-chunk-size"), - pathFindingExperimentConf = getPathFindingExperimentConf(config.getConfig("router.path-finding.experiments")) + pathFindingExperimentConf = getPathFindingExperimentConf(config.getConfig("router.path-finding.experiments")), + balanceEstimateHalfLife = FiniteDuration(config.getDuration("router.balance-estimate-half-life").getSeconds, TimeUnit.SECONDS), ), socksProxy_opt = socksProxy_opt, maxPaymentAttempts = config.getInt("max-payment-attempts"), diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentLifecycle.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentLifecycle.scala index 0735f48353..4a7e80e688 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentLifecycle.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentLifecycle.scala @@ -102,6 +102,7 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A handleLocalFail(d, DisconnectedException, isFatal = false) case Event(RES_ADD_SETTLED(_, htlc, fulfill: HtlcResult.Fulfill), d: WaitingForComplete) => + router ! Router.RouteDidRelay(d.route) Metrics.PaymentAttempt.withTag(Tags.MultiPart, value = false).record(d.failures.size + 1) val p = PartialPayment(id, d.c.finalPayload.amount, d.cmd.amount - d.c.finalPayload.amount, htlc.channelId, Some(cfg.fullRoute(d.route))) myStop(d.c, Right(cfg.createPaymentSent(fulfill.paymentPreimage, p :: Nil))) @@ -166,13 +167,31 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A private def handleRemoteFail(d: WaitingForComplete, fail: UpdateFailHtlc) = { import d._ - (Sphinx.FailurePacket.decrypt(fail.reason, sharedSecrets) match { + ((Sphinx.FailurePacket.decrypt(fail.reason, sharedSecrets) match { case success@Success(e) => Metrics.PaymentError.withTag(Tags.Failure, Tags.FailureType(RemoteFailure(d.c.finalPayload.amount, Nil, e))).increment() success case failure@Failure(_) => Metrics.PaymentError.withTag(Tags.Failure, Tags.FailureType(UnreadableRemoteFailure(d.c.finalPayload.amount, Nil))).increment() failure + }) match { + case res@Success(Sphinx.DecryptedFailurePacket(nodeId, failureMessage)) => + // We have discovered some liquidity information with this payment: we update the router accordingly. + val stoppedRoute = d.route.stopAt(nodeId) + if (stoppedRoute.hops.length > 1) { + router ! Router.RouteCouldRelay(stoppedRoute) + } + failureMessage match { + case TemporaryChannelFailure(update) => + d.route.hops.find(_.nodeId == nodeId) match { + case Some(failingHop) if ChannelRelayParams.areSame(failingHop.params, ChannelRelayParams.FromAnnouncement(update), true) => + router ! Router.ChannelCouldNotRelay(stoppedRoute.amount, failingHop) + case _ => // otherwise the relay parameters may have changed, so it's not necessarily a liquidity issue + } + case _ => // other errors should not be used for liquidity issues + } + res + case res => res }) match { case Success(e@Sphinx.DecryptedFailurePacket(nodeId, failureMessage)) if nodeId == c.targetNodeId => // if destination node returns an error, we fail the payment immediately diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/remote/EclairInternalsSerializer.scala b/eclair-core/src/main/scala/fr/acinq/eclair/remote/EclairInternalsSerializer.scala index 5d0c6a3a8b..d0917fdf4b 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/remote/EclairInternalsSerializer.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/remote/EclairInternalsSerializer.scala @@ -96,7 +96,8 @@ object EclairInternalsSerializer { .typecase(1, provide(EncodingType.COMPRESSED_ZLIB))) :: ("channelRangeChunkSize" | int32) :: ("channelQueryChunkSize" | int32) :: - ("pathFindingExperimentConf" | pathFindingExperimentConfCodec)).as[RouterConf] + ("pathFindingExperimentConf" | pathFindingExperimentConfCodec) :: + ("balanceEstimateHalfLife" | finiteDurationCodec)).as[RouterConf] val overrideFeaturesListCodec: Codec[List[(PublicKey, Features[Feature])]] = listOfN(uint16, publicKey ~ variableSizeBytes(uint16, featuresCodec)) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/BalanceEstimate.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/BalanceEstimate.scala new file mode 100644 index 0000000000..18ea2a810d --- /dev/null +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/BalanceEstimate.scala @@ -0,0 +1,331 @@ +/* + * Copyright 2021 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.router + +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.{Satoshi, SatoshiLong} +import fr.acinq.eclair.router.Graph.GraphStructure.{DirectedGraph, GraphEdge} +import fr.acinq.eclair.router.Router.{ChannelDesc, ChannelHop, Route} +import fr.acinq.eclair.{MilliSatoshi, MilliSatoshiLong, ShortChannelId, TimestampSecond, TimestampSecondLong, ToMilliSatoshiConversion} + +import scala.concurrent.duration.{DurationInt, FiniteDuration} + +/** + * Estimates the balance between a pair of nodes + * + * @param low lower bound on the balance + * @param lowTimestamp time at which the lower bound was known to be correct + * @param high upper bound on the balance + * @param highTimestamp time at which the upper bound was known to be correct + * @param capacities capacities of the channels between these two nodes + * @param halfLife time after which the certainty of the lower/upper bounds is halved + */ +case class BalanceEstimate private(low: MilliSatoshi, + lowTimestamp: TimestampSecond, + high: MilliSatoshi, highTimestamp: TimestampSecond, + capacities: Map[ShortChannelId, Satoshi], + halfLife: FiniteDuration) { + val maxCapacity: Satoshi = capacities.values.maxOption.getOrElse(0 sat) + + /* The goal of this class is to estimate the probability that a given edge can relay the amount that we plan to send + * through it. We model this probability with 3 pieces of linear functions. + * + * Without any information we use the following baseline (x is the amount we're sending and y the probability it can be relayed): + * + * 1 |**** + * | **** + * | **** + * | **** + * | **** + * | **** + * | **** + * | **** + * | **** + * | **** + * | **** + * | **** + * 0 +------------------------------------------------**** + * 0 capacity + * + * If we get the information that the edge can (or can't) relay a given amount (because we tried), then we get a lower + * bound (or upper bound) that we can use and our model becomes: + * + * 1 |*************** + * | |* + * | | * + * | | * + * | | * + * | | * + * | | * + * | | * + * | | * + * | | * + * | | * + * | | * + * 0 +--------------|-----------|************************* + * 0 low high capacity + * + * However this lower bound (or upper bound) is only valid at the moment we got that information. If we wait, the + * information decays and we slowly go back towards our baseline: + * + * 1 |***** + * | ***** + * | ***** + * | |** + * | | * + * | | ** + * | | ** + * | | * + * | | ** + * | | * + * | | ******** + * | | | ********* + * 0 +--------------|-----------|----------------********* + * 0 low high capacity + */ + + /** + * We model the decay with a half-life H: every H units of time, our confidence decreases by half and our estimated + * probability distribution gets closer to the baseline uniform distribution of balances between 0 and totalCapacity. + * + * @param amount the amount that we knew we could send or not send at time t + * @param successProbabilityAtT probability that we could relay amount at time t (usually 0 or 1) + * @param t time at which we knew if we could or couldn't send amount + * @return the probability that we can send amount now + */ + private def decay(amount: MilliSatoshi, successProbabilityAtT: Double, t: TimestampSecond): Double = { + val decayRatio = 1 / math.pow(2, (TimestampSecond.now() - t) / halfLife) + val baseline = 1 - amount.toLong.toDouble / maxCapacity.toMilliSatoshi.toLong + baseline * (1 - decayRatio) + successProbabilityAtT * decayRatio + } + + private def otherSide: BalanceEstimate = + BalanceEstimate(maxCapacity - high, highTimestamp, maxCapacity - low, lowTimestamp, capacities, halfLife) + + /** + * We tried to send the given amount and received a temporary channel failure. We assume that this failure was caused + * by a lack of liquidity: it could also be caused by a violation of max_accepted_htlcs, max_htlc_value_in_flight_msat + * or a spamming protection heuristic by the relaying node, but since we have no way of detecting that, our best + * strategy is to ignore these cases. + */ + def couldNotSend(amount: MilliSatoshi, timestamp: TimestampSecond): BalanceEstimate = { + if (amount <= low) { + // the balance is actually below `low`, we discard our previous lower bound + copy(low = 0 msat, lowTimestamp = timestamp, high = amount, highTimestamp = timestamp) + } else if (amount < high) { + // the balance is between `low` and `high` as we expected, we discard our previous upper bound + copy(high = amount, highTimestamp = timestamp) + } else { + // We already expected not to be able to relay that amount as it is above our upper bound. However if the upper bound + // was old enough that replacing it with the current amount decreases the success probability for `high`, then we + // replace it. + val updated = copy(high = amount, highTimestamp = timestamp) + if (updated.canSend(high) < this.canSend(high)) { + updated + } else { + this + } + } + } + + /** + * We tried to send the given amount, it was correctly relayed but failed afterwards, so we know we should be able to + * send at least this amount again. + */ + def couldSend(amount: MilliSatoshi, timestamp: TimestampSecond): BalanceEstimate = + otherSide.couldNotSend(maxCapacity - amount, timestamp).otherSide + + /** + * We successfully sent the given amount, so we know that some of the liquidity has shifted. + */ + def didSend(amount: MilliSatoshi, timestamp: TimestampSecond): BalanceEstimate = { + val newLow = (low - amount).max(0 msat) + if (capacities.size == 1) { + // Special case for single channel as we expect this case to be quite common and we can easily get more precise bounds. + val newHigh = (high - amount).max(0 msat) + // We could shift everything left by amount without changing the timestamps but we may get more information by + // ignoring the old high if it has decayed too much. We try both and choose the one that gives the lowest + // probability for the new high. + val a = copy(low = newLow, high = newHigh) + val b = copy(low = newLow, high = (maxCapacity - amount).max(0 msat), highTimestamp = timestamp) + if (a.canSend(newHigh) < b.canSend(newHigh)) { + a + } else { + b + } + } else { + copy(low = newLow) + } + } + + /** + * We successfully received the given amount, so we know that some of the liquidity has shifted. + */ + def didReceive(amount: MilliSatoshi, timestamp: TimestampSecond): BalanceEstimate = + otherSide.didSend(amount, timestamp).otherSide + + def addEdge(edge: GraphEdge): BalanceEstimate = copy( + high = high.max(edge.capacity.toMilliSatoshi), + capacities = capacities.updated(edge.desc.shortChannelId, edge.capacity) + ) + + def removeEdge(desc: ChannelDesc): BalanceEstimate = { + val edgeCapacity = capacities.getOrElse(desc.shortChannelId, 0 sat) + val newCapacities = capacities.removed(desc.shortChannelId) + copy( + low = (low - edgeCapacity.toMilliSatoshi).max(0 msat), + high = high.min(newCapacities.values.maxOption.getOrElse(0 sat).toMilliSatoshi), + capacities = newCapacities + ) + } + + /** + * Estimate the probability that we can successfully send `amount` through the channel + * + * We estimate this probability with a piecewise linear function: + * - probability that it can relay a payment of 0 is 1 + * - probability that it can relay a payment of low is decay(low, 1, lowTimestamp) which is close to 1 if lowTimestamp is recent + * - probability that it can relay a payment of high is decay(high, 0, highTimestamp) which is close to 0 if highTimestamp is recent + * - probability that it can relay a payment of maxCapacity is 0 + */ + def canSend(amount: MilliSatoshi): Double = { + val a = amount.toLong.toDouble + val l = low.toLong.toDouble + val h = high.toLong.toDouble + val c = maxCapacity.toMilliSatoshi.toLong.toDouble + + // Success probability at the low and high points + val pLow = decay(low, 1, lowTimestamp) + val pHigh = decay(high, 0, highTimestamp) + + if (amount < low) { + (l - a * (1.0 - pLow)) / l + } else if (amount < high) { + ((h - a) * pLow + (a - l) * pHigh) / (h - l) + } else if (h < c) { + ((c - a) * pHigh) / (c - h) + } else { + 0 + } + } +} + +object BalanceEstimate { + def empty(halfLife: FiniteDuration): BalanceEstimate = BalanceEstimate(0 msat, 0 unixsec, 0 msat, 0 unixsec, Map.empty, halfLife) +} + +/** + * Balance estimates for the whole routing graph. + */ +case class BalancesEstimates(balances: Map[(PublicKey, PublicKey), BalanceEstimate], defaultHalfLife: FiniteDuration) { + private def get(a: PublicKey, b: PublicKey): Option[BalanceEstimate] = balances.get((a, b)) + + def addEdge(edge: GraphEdge): BalancesEstimates = BalancesEstimates( + balances.updatedWith((edge.desc.a, edge.desc.b))(balance => + Some(balance.getOrElse(BalanceEstimate.empty(defaultHalfLife)).addEdge(edge)) + ), + defaultHalfLife + ) + + def removeEdge(desc: ChannelDesc): BalancesEstimates = BalancesEstimates( + balances.updatedWith((desc.a, desc.b)) { + case None => None + case Some(balance) => + val newBalance = balance.removeEdge(desc) + if (newBalance.capacities.nonEmpty) { + Some(newBalance) + } else { + None + } + }, + defaultHalfLife + ) + + def channelCouldSend(hop: ChannelHop, amount: MilliSatoshi): BalancesEstimates = { + get(hop.nodeId, hop.nextNodeId).foreach { balance => + val estimatedProbability = balance.canSend(amount) + Monitoring.Metrics.remoteEdgeRelaySuccess(estimatedProbability) + } + BalancesEstimates(balances.updatedWith((hop.nodeId, hop.nextNodeId))(_.map(_.couldSend(amount, TimestampSecond.now()))), defaultHalfLife) + } + + def channelCouldNotSend(hop: ChannelHop, amount: MilliSatoshi): BalancesEstimates = { + get(hop.nodeId, hop.nextNodeId).foreach { balance => + val estimatedProbability = balance.canSend(amount) + Monitoring.Metrics.remoteEdgeRelayFailure(estimatedProbability) + } + BalancesEstimates(balances.updatedWith((hop.nodeId, hop.nextNodeId))(_.map(_.couldNotSend(amount, TimestampSecond.now()))), defaultHalfLife) + } + + def channelDidSend(hop: ChannelHop, amount: MilliSatoshi): BalancesEstimates = { + get(hop.nodeId, hop.nextNodeId).foreach { balance => + val estimatedProbability = balance.canSend(amount) + Monitoring.Metrics.remoteEdgeRelaySuccess(estimatedProbability) + } + val balances1 = balances.updatedWith((hop.nodeId, hop.nextNodeId))(_.map(_.didSend(amount, TimestampSecond.now()))) + val balances2 = balances1.updatedWith((hop.nextNodeId, hop.nodeId))(_.map(_.didReceive(amount, TimestampSecond.now()))) + BalancesEstimates(balances2, defaultHalfLife) + } + +} + +case class GraphWithBalanceEstimates(graph: DirectedGraph, private val balances: BalancesEstimates) { + def addEdge(edge: GraphEdge): GraphWithBalanceEstimates = GraphWithBalanceEstimates(graph.addEdge(edge), balances.addEdge(edge)) + + def removeEdge(desc: ChannelDesc): GraphWithBalanceEstimates = GraphWithBalanceEstimates(graph.removeEdge(desc), balances.removeEdge(desc)) + + def removeEdges(descList: Iterable[ChannelDesc]): GraphWithBalanceEstimates = GraphWithBalanceEstimates( + graph.removeEdges(descList), + descList.foldLeft(balances)((acc, edge) => acc.removeEdge(edge)), + ) + + def routeCouldRelay(route: Route): GraphWithBalanceEstimates = { + val (balances1, _) = route.hops.foldRight((balances, route.amount)) { + case (hop, (balances, amount)) => + (balances.channelCouldSend(hop, amount), amount + hop.fee(amount)) + } + GraphWithBalanceEstimates(graph, balances1) + } + + def routeDidRelay(route: Route): GraphWithBalanceEstimates = { + val (balances1, _) = route.hops.foldRight((balances, route.amount)) { + case (hop, (balances, amount)) => + (balances.channelDidSend(hop, amount), amount + hop.fee(amount)) + } + GraphWithBalanceEstimates(graph, balances1) + } + + def channelCouldNotSend(hop: ChannelHop, amount: MilliSatoshi): GraphWithBalanceEstimates = { + GraphWithBalanceEstimates(graph, balances.channelCouldNotSend(hop, amount)) + } + + def canSend(amount: MilliSatoshi, edge: GraphEdge): Double = { + balances.balances.get((edge.desc.a, edge.desc.b)) match { + case Some(estimate) => estimate.canSend(amount) + case None => BalanceEstimate.empty(1 hour).addEdge(edge).canSend(amount) + } + } +} + +object GraphWithBalanceEstimates { + def apply(graph: DirectedGraph, defaultHalfLife: FiniteDuration): GraphWithBalanceEstimates = { + val balances = graph.edgeSet().foldLeft(Map.empty[(PublicKey, PublicKey), BalanceEstimate]) { + case (m, edge) => m.updatedWith((edge.desc.a, edge.desc.b))(balance => Some(balance.getOrElse(BalanceEstimate.empty(defaultHalfLife)).addEdge(edge))) + } + GraphWithBalanceEstimates(graph, BalancesEstimates(balances, defaultHalfLife)) + } +} \ No newline at end of file diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/Monitoring.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/Monitoring.scala index bd0e76b068..5daf5383ee 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/Monitoring.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/Monitoring.scala @@ -66,6 +66,12 @@ object Monitoring { case _: GossipDecision.Accepted => GossipResult.withTag("result", "accepted") case rejected: GossipDecision.Rejected => GossipResult.withTag("result", "rejected").withTag("reason", getSimpleClassName(rejected)) } + + private val RelayProbabilityEstimate = Kamon.histogram("router.balance-estimates.remote-edge-relay", "Estimated probability (in percent) that the relay will be successful") + + def remoteEdgeRelaySuccess(estimatedProbability: Double) = RelayProbabilityEstimate.withTag("success", (estimatedProbability * 100).toLong) + + def remoteEdgeRelayFailure(estimatedProbability: Double) = RelayProbabilityEstimate.withTag("failure", (estimatedProbability * 100).toLong) } object Tags { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/RouteCalculation.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/RouteCalculation.scala index 58d75a6c4c..46cd566252 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/RouteCalculation.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/RouteCalculation.scala @@ -20,7 +20,6 @@ import akka.actor.{ActorContext, ActorRef, Status} import akka.event.DiagnosticLoggingAdapter import com.softwaremill.quicklens.ModifyPimp import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey -import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, Satoshi, SatoshiLong} import fr.acinq.eclair.Logs.LogCategory import fr.acinq.eclair._ import fr.acinq.eclair.payment.Bolt11Invoice.ExtraHop @@ -29,7 +28,6 @@ import fr.acinq.eclair.router.Graph.GraphStructure.{DirectedGraph, GraphEdge} import fr.acinq.eclair.router.Graph.{InfiniteLoop, NegativeProbability, RichWeight, RoutingHeuristics} import fr.acinq.eclair.router.Monitoring.{Metrics, Tags} import fr.acinq.eclair.router.Router._ -import fr.acinq.eclair.wire.protocol.ChannelUpdate import kamon.tag.TagSet import scala.annotation.tailrec @@ -48,7 +46,7 @@ object RouteCalculation { val assistedChannels: Map[ShortChannelId, AssistedChannel] = fr.assistedRoutes.flatMap(toAssistedChannels(_, fr.route.targetNodeId, fr.amount)).toMap val extraEdges = assistedChannels.values.map(ac => GraphEdge(ac)).toSet - val g = extraEdges.foldLeft(d.graph) { case (g: DirectedGraph, e: GraphEdge) => g.addEdge(e) } + val g = extraEdges.foldLeft(d.graphWithBalances.graph) { case (g: DirectedGraph, e: GraphEdge) => g.addEdge(e) } fr.route match { case PredefinedNodeRoute(hops) => @@ -118,13 +116,13 @@ object RouteCalculation { log.info(s"finding routes ${r.source}->${r.target} with assistedChannels={} ignoreNodes={} ignoreChannels={} excludedChannels={}", assistedChannels.keys.mkString(","), r.ignore.nodes.map(_.value).mkString(","), r.ignore.channels.mkString(","), d.excludedChannels.mkString(",")) log.info("finding routes with params={}, multiPart={}", params, r.allowMultiPart) - log.info("local channels to recipient: {}", d.graph.getEdgesBetween(r.source, r.target).map(e => s"${e.desc.shortChannelId} (${e.balance_opt}/${e.capacity})").mkString(", ")) + log.info("local channels to recipient: {}", d.graphWithBalances.graph.getEdgesBetween(r.source, r.target).map(e => s"${e.desc.shortChannelId} (${e.balance_opt}/${e.capacity})").mkString(", ")) val tags = TagSet.Empty.withTag(Tags.MultiPart, r.allowMultiPart).withTag(Tags.Amount, Tags.amountBucket(r.amount)) KamonExt.time(Metrics.FindRouteDuration.withTags(tags.withTag(Tags.NumberOfRoutes, routesToFind.toLong))) { val result = if (r.allowMultiPart) { - findMultiPartRoute(d.graph, r.source, r.target, r.amount, r.maxFee, extraEdges, ignoredEdges, r.ignore.nodes, r.pendingPayments, params, currentBlockHeight) + findMultiPartRoute(d.graphWithBalances.graph, r.source, r.target, r.amount, r.maxFee, extraEdges, ignoredEdges, r.ignore.nodes, r.pendingPayments, params, currentBlockHeight) } else { - findRoute(d.graph, r.source, r.target, r.amount, r.maxFee, routesToFind, extraEdges, ignoredEdges, r.ignore.nodes, params, currentBlockHeight) + findRoute(d.graphWithBalances.graph, r.source, r.target, r.amount, r.maxFee, routesToFind, extraEdges, ignoredEdges, r.ignore.nodes, params, currentBlockHeight) } result match { case Success(routes) => @@ -140,7 +138,7 @@ object RouteCalculation { Metrics.FindRouteErrors.withTags(tags.withTag(Tags.Error, "NegativeProbability")).increment() ctx.sender() ! Status.Failure(failure) case Failure(t) => - val failure = if (isNeighborBalanceTooLow(d.graph, r)) BalanceTooLow else t + val failure = if (isNeighborBalanceTooLow(d.graphWithBalances.graph, r)) BalanceTooLow else t Metrics.FindRouteErrors.withTags(tags.withTag(Tags.Error, failure.getClass.getSimpleName)).increment() ctx.sender() ! Status.Failure(failure) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala index 42caea67d0..6580c17831 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala @@ -103,7 +103,17 @@ class Router(val nodeParams: NodeParams, watcher: typed.ActorRef[ZmqWatcher.Comm log.info(s"initialization completed, ready to process messages") Try(initialized.map(_.success(Done))) - startWith(NORMAL, Data(initNodes, initChannels, Stash(Map.empty, Map.empty), rebroadcast = Rebroadcast(channels = Map.empty, updates = Map.empty, nodes = Map.empty), awaiting = Map.empty, privateChannels = Map.empty, scid2PrivateChannels = Map.empty, excludedChannels = Set.empty, graph, sync = Map.empty)) + val data = Data( + initNodes, initChannels, + Stash(Map.empty, Map.empty), + rebroadcast = Rebroadcast(channels = Map.empty, updates = Map.empty, nodes = Map.empty), + awaiting = Map.empty, + privateChannels = Map.empty, + scid2PrivateChannels = Map.empty, + excludedChannels = Set.empty, + graphWithBalances = GraphWithBalanceEstimates(graph, nodeParams.routerConf.balanceEstimateHalfLife), + sync = Map.empty) + startWith(NORMAL, data) } when(NORMAL) { @@ -251,6 +261,14 @@ class Router(val nodeParams: NodeParams, watcher: typed.ActorRef[ZmqWatcher.Comm case Event(PeerRoutingMessage(peerConnection, remoteNodeId, r: ReplyShortChannelIdsEnd), d) => stay() using Sync.handleReplyShortChannelIdsEnd(d, RemoteGossip(peerConnection, remoteNodeId), r) + case Event(RouteCouldRelay(route), d) => + stay() using d.copy(graphWithBalances = d.graphWithBalances.routeCouldRelay(route)) + + case Event(RouteDidRelay(route), d) => + stay() using d.copy(graphWithBalances = d.graphWithBalances.routeDidRelay(route)) + + case Event(ChannelCouldNotRelay(amount, hop), d) => + stay() using d.copy(graphWithBalances = d.graphWithBalances.channelCouldNotSend(hop, amount)) } initialize() @@ -302,7 +320,8 @@ object Router { encodingType: EncodingType, channelRangeChunkSize: Int, channelQueryChunkSize: Int, - pathFindingExperimentConf: PathFindingExperimentConf) { + pathFindingExperimentConf: PathFindingExperimentConf, + balanceEstimateHalfLife: FiniteDuration) { require(channelRangeChunkSize <= Sync.MAXIMUM_CHUNK_SIZE, "channel range chunk size exceeds the size of a lightning message") require(channelQueryChunkSize <= Sync.MAXIMUM_CHUNK_SIZE, "channel query chunk size exceeds the size of a lightning message") } @@ -428,6 +447,11 @@ object Router { override def htlcMinimum: MilliSatoshi = 0 msat override def htlcMaximum_opt: Option[MilliSatoshi] = Some(htlcMaximum) } + + def areSame(a: ChannelRelayParams, b: ChannelRelayParams, ignoreHtlcSize: Boolean = false): Boolean = + a.cltvExpiryDelta == b.cltvExpiryDelta && + a.relayFees == b.relayFees && + (ignoreHtlcSize || (a.htlcMinimum == b.htlcMinimum && a.htlcMaximum_opt == b.htlcMaximum_opt)) } // @formatter:on @@ -527,6 +551,10 @@ object Router { def printChannels(): String = hops.map(_.shortChannelId).mkString("->") + def stopAt(nodeId: PublicKey): Route = { + val amountAtStop = hops.reverse.takeWhile(_.nextNodeId != nodeId).foldLeft(amount) { case (amount1, hop) => amount1 + hop.fee(amount1) } + Route(amountAtStop, hops.takeWhile(_.nodeId != nodeId)) + } } case class RouteResponse(routes: Seq[Route]) { @@ -616,10 +644,9 @@ object Router { privateChannels: Map[ByteVector32, PrivateChannel], // indexed by channel id scid2PrivateChannels: Map[ShortChannelId, ByteVector32], // scid to channel_id, only to be used for private channels excludedChannels: Set[ChannelDesc], // those channels are temporarily excluded from route calculation, because their node returned a TemporaryChannelFailure - graph: DirectedGraph, + graphWithBalances: GraphWithBalanceEstimates, sync: Map[PublicKey, Syncing] // keep tracks of channel range queries sent to each peer. If there is an entry in the map, it means that there is an ongoing query for which we have not yet received an 'end' message ) { - def resolve(scid: ShortChannelId): Option[KnownChannel] = { // let's assume this is a real scid channels.get(scid) match { @@ -645,4 +672,13 @@ object Router { def isRelatedTo(c: ChannelAnnouncement, nodeId: PublicKey) = nodeId == c.nodeId1 || nodeId == c.nodeId2 def hasChannels(nodeId: PublicKey, channels: Iterable[PublicChannel]): Boolean = channels.exists(c => isRelatedTo(c.ann, nodeId)) + + /** We know that this route could relay because we have tried it but the payment was eventually cancelled */ + case class RouteCouldRelay(route: Route) + + /** We have relayed using this route. */ + case class RouteDidRelay(route: Route) + + /** We have tried to relay this amount from this channel and it failed. */ + case class ChannelCouldNotRelay(amount: MilliSatoshi, hop: ChannelHop) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/StaleChannels.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/StaleChannels.scala index 1eff789583..91ce0d13f2 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/StaleChannels.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/StaleChannels.scala @@ -53,14 +53,14 @@ object StaleChannels { staleChannelsToRemove += ChannelDesc(ca.ann.shortChannelId, ca.ann.nodeId2, ca.ann.nodeId1) }) - val graph1 = d.graph.removeEdges(staleChannelsToRemove) + val graphWithBalances1 = d.graphWithBalances.removeEdges(staleChannelsToRemove) staleNodes.foreach { nodeId => log.info("pruning nodeId={} (stale)", nodeId) db.removeNode(nodeId) ctx.system.eventStream.publish(NodeLost(nodeId)) } - d.copy(nodes = d.nodes -- staleNodes, channels = channels1, graph = graph1) + d.copy(nodes = d.nodes -- staleNodes, channels = channels1, graphWithBalances = graphWithBalances1) } def isStale(u: ChannelUpdate): Boolean = isStale(u.timestamp) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/Validation.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/Validation.scala index a1ca814820..a218504a1e 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/Validation.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/Validation.scala @@ -19,7 +19,6 @@ package fr.acinq.eclair.router import akka.actor.typed.scaladsl.adapter.actorRefAdapter import akka.actor.{ActorContext, ActorRef, typed} import akka.event.{DiagnosticLoggingAdapter, LoggingAdapter} -import com.softwaremill.quicklens.ModifyPimp import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.bitcoin.scalacompat.Script.{pay2wsh, write} import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher @@ -195,7 +194,7 @@ object Validation { log.info("pruning shortChannelId={} (spent)", shortChannelId) db.removeChannel(shortChannelId) // NB: this also removes channel updates // we also need to remove updates from the graph - val graph1 = d.graph + val graphWithBalances1 = d.graphWithBalances .removeEdge(ChannelDesc(lostChannel.shortChannelId, lostChannel.nodeId1, lostChannel.nodeId2)) .removeEdge(ChannelDesc(lostChannel.shortChannelId, lostChannel.nodeId2, lostChannel.nodeId1)) @@ -206,7 +205,7 @@ object Validation { db.removeNode(nodeId) ctx.system.eventStream.publish(NodeLost(nodeId)) } - d.copy(nodes = d.nodes -- lostNodes, channels = d.channels - shortChannelId, graph = graph1) + d.copy(nodes = d.nodes -- lostNodes, channels = d.channels - shortChannelId, graphWithBalances = graphWithBalances1) } def handleNodeAnnouncement(d: Data, db: NetworkDb, origins: Set[GossipOrigin], n: NodeAnnouncement, wasStashed: Boolean = false)(implicit ctx: ActorContext, log: LoggingAdapter): Data = { @@ -285,8 +284,8 @@ object Validation { val origins1 = d.rebroadcast.updates(u) ++ origins // NB: we update the channels because the balances may have changed even if the channel_update is the same. val pc1 = pc.applyChannelUpdate(update) - val graph1 = d.graph.addEdge(GraphEdge(u, pc1)) - d.copy(rebroadcast = d.rebroadcast.copy(updates = d.rebroadcast.updates + (u -> origins1)), channels = d.channels + (pc.shortChannelId -> pc1), graph = graph1) + val graphWithBalances1 = d.graphWithBalances.addEdge(GraphEdge(u, pc1)) + d.copy(rebroadcast = d.rebroadcast.copy(updates = d.rebroadcast.updates + (u -> origins1)), channels = d.channels + (pc.shortChannelId -> pc1), graphWithBalances = graphWithBalances1) } else if (StaleChannels.isStale(u)) { log.debug("ignoring {} (stale)", u) sendDecision(origins, GossipDecision.Stale(u)) @@ -298,8 +297,8 @@ object Validation { case Left(_) => // NB: we update the graph because the balances may have changed even if the channel_update is the same. val pc1 = pc.applyChannelUpdate(update) - val graph1 = d.graph.addEdge(GraphEdge(u, pc1)) - d.copy(channels = d.channels + (pc.shortChannelId -> pc1), graph = graph1) + val graphWithBalances1 = d.graphWithBalances.addEdge(GraphEdge(u, pc1)) + d.copy(channels = d.channels + (pc.shortChannelId -> pc1), graphWithBalances = graphWithBalances1) case Right(_) => d } } else if (!Announcements.checkSig(u, pc.getNodeIdSameSideAs(u))) { @@ -314,14 +313,14 @@ object Validation { db.updateChannel(u) // update the graph val pc1 = pc.applyChannelUpdate(update) - val graph1 = if (u.channelFlags.isEnabled) { + val graphWithBalances1 = if (u.channelFlags.isEnabled) { update.left.foreach(_ => log.info("added local shortChannelId={} public={} to the network graph", u.shortChannelId, publicChannel)) - d.graph.addEdge(GraphEdge(u, pc1)) + d.graphWithBalances.addEdge(GraphEdge(u, pc1)) } else { update.left.foreach(_ => log.info("removed local shortChannelId={} public={} from the network graph", u.shortChannelId, publicChannel)) - d.graph.removeEdge(ChannelDesc(u, pc1.ann)) + d.graphWithBalances.removeEdge(ChannelDesc(u, pc1.ann)) } - d.copy(channels = d.channels + (pc.shortChannelId -> pc1), rebroadcast = d.rebroadcast.copy(updates = d.rebroadcast.updates + (u -> origins)), graph = graph1) + d.copy(channels = d.channels + (pc.shortChannelId -> pc1), rebroadcast = d.rebroadcast.copy(updates = d.rebroadcast.updates + (u -> origins)), graphWithBalances = graphWithBalances1) } else { log.debug("added channel_update for shortChannelId={} public={} flags={} {}", u.shortChannelId, publicChannel, u.channelFlags, u) sendDecision(origins, GossipDecision.Accepted(u)) @@ -329,9 +328,9 @@ object Validation { db.updateChannel(u) // we also need to update the graph val pc1 = pc.applyChannelUpdate(update) - val graph1 = d.graph.addEdge(GraphEdge(u, pc1)) + val graphWithBalances1 = d.graphWithBalances.addEdge(GraphEdge(u, pc1)) update.left.foreach(_ => log.info("added local shortChannelId={} public={} to the network graph", u.shortChannelId, publicChannel)) - d.copy(channels = d.channels + (pc.shortChannelId -> pc1), privateChannels = d.privateChannels - pc1.channelId, rebroadcast = d.rebroadcast.copy(updates = d.rebroadcast.updates + (u -> origins)), graph = graph1) + d.copy(channels = d.channels + (pc.shortChannelId -> pc1), privateChannels = d.privateChannels - pc1.channelId, rebroadcast = d.rebroadcast.copy(updates = d.rebroadcast.updates + (u -> origins)), graphWithBalances = graphWithBalances1) } case Some(pc: PrivateChannel) => val publicChannel = false @@ -354,23 +353,23 @@ object Validation { ctx.system.eventStream.publish(ChannelUpdatesReceived(u :: Nil)) // we also need to update the graph val pc1 = pc.applyChannelUpdate(update) - val graph1 = if (u.channelFlags.isEnabled) { + val graphWithBalances1 = if (u.channelFlags.isEnabled) { update.left.foreach(_ => log.info("added local channelId={} public={} to the network graph", pc.channelId, publicChannel)) - d.graph.addEdge(GraphEdge(u, pc1)) + d.graphWithBalances.addEdge(GraphEdge(u, pc1)) } else { update.left.foreach(_ => log.info("removed local channelId={} public={} from the network graph", pc.channelId, publicChannel)) - d.graph.removeEdge(ChannelDesc(u, pc1)) + d.graphWithBalances.removeEdge(ChannelDesc(u, pc1)) } - d.copy(privateChannels = d.privateChannels + (pc.channelId -> pc1), graph = graph1) + d.copy(privateChannels = d.privateChannels + (pc.channelId -> pc1), graphWithBalances = graphWithBalances1) } else { log.debug("added channel_update for channelId={} public={} flags={} {}", pc.channelId, publicChannel, u.channelFlags, u) sendDecision(origins, GossipDecision.Accepted(u)) ctx.system.eventStream.publish(ChannelUpdatesReceived(u :: Nil)) // we also need to update the graph val pc1 = pc.applyChannelUpdate(update) - val graph1 = d.graph.addEdge(GraphEdge(u, pc1)) + val graphWithBalances1 = d.graphWithBalances.addEdge(GraphEdge(u, pc1)) update.left.foreach(_ => log.info("added local channelId={} public={} to the network graph", pc.channelId, publicChannel)) - d.copy(privateChannels = d.privateChannels + (pc.channelId -> pc1), graph = graph1) + d.copy(privateChannels = d.privateChannels + (pc.channelId -> pc1), graphWithBalances = graphWithBalances1) } case None if d.awaiting.keys.exists(c => c.shortChannelId == u.shortChannelId) => // channel is currently being validated @@ -467,38 +466,38 @@ object Validation { val desc1 = ChannelDesc(shortChannelId, localNodeId, remoteNodeId) val desc2 = ChannelDesc(shortChannelId, remoteNodeId, localNodeId) // we remove the corresponding updates from the graph - val graph1 = d.graph + val graphWithBalances1 = d.graphWithBalances .removeEdge(desc1) .removeEdge(desc2) // and we remove the channel and channel_update from our state - d.copy(privateChannels = d.privateChannels - channelId, graph = graph1) + d.copy(privateChannels = d.privateChannels - channelId, graphWithBalances = graphWithBalances1) } else { d } } def handleAvailableBalanceChanged(d: Data, e: AvailableBalanceChanged)(implicit log: LoggingAdapter): Data = { - val (publicChannels1, graph1) = d.channels.get(e.shortChannelId) match { + val (publicChannels1, graphWithBalances1) = d.channels.get(e.shortChannelId) match { case Some(pc) => val pc1 = pc.updateBalances(e.commitments) log.debug("public channel balance updated: {}", pc1) val update_opt = if (e.commitments.localNodeId == pc1.ann.nodeId1) pc1.update_1_opt else pc1.update_2_opt - val graph1 = update_opt.map(u => d.graph.addEdge(GraphEdge(u, pc1))).getOrElse(d.graph) - (d.channels + (pc.ann.shortChannelId -> pc1), graph1) + val graphWithBalances1 = update_opt.map(u => d.graphWithBalances.addEdge(GraphEdge(u, pc1))).getOrElse(d.graphWithBalances) + (d.channels + (pc.ann.shortChannelId -> pc1), graphWithBalances1) case None => - (d.channels, d.graph) + (d.channels, d.graphWithBalances) } - val (privateChannels1, graph2) = d.privateChannels.get(e.channelId) match { + val (privateChannels1, graphWithBalances2) = d.privateChannels.get(e.channelId) match { case Some(pc) => val pc1 = pc.updateBalances(e.commitments) log.debug("private channel balance updated: {}", pc1) val update_opt = if (e.commitments.localNodeId == pc1.nodeId1) pc1.update_1_opt else pc1.update_2_opt - val graph2 = update_opt.map(u => graph1.addEdge(GraphEdge(u, pc1))).getOrElse(graph1) - (d.privateChannels + (e.channelId -> pc1), graph2) + val graphWithBalances2 = update_opt.map(u => graphWithBalances1.addEdge(GraphEdge(u, pc1))).getOrElse(graphWithBalances1) + (d.privateChannels + (e.channelId -> pc1), graphWithBalances2) case None => - (d.privateChannels, graph1) + (d.privateChannels, graphWithBalances1) } - d.copy(channels = publicChannels1, privateChannels = privateChannels1, graph = graph2) + d.copy(channels = publicChannels1, privateChannels = privateChannels1, graphWithBalances = graphWithBalances2) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala b/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala index 2bc65f917a..aedfb336f3 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala @@ -186,7 +186,8 @@ object TestConstants { maxParts = 10, ), experimentName = "alice-test-experiment", - experimentPercentage = 100))) + experimentPercentage = 100))), + balanceEstimateHalfLife = 1 day, ), socksProxy_opt = None, maxPaymentAttempts = 5, @@ -325,7 +326,8 @@ object TestConstants { maxParts = 10, ), experimentName = "bob-test-experiment", - experimentPercentage = 100))) + experimentPercentage = 100))), + balanceEstimateHalfLife = 1 day, ), socksProxy_opt = None, maxPaymentAttempts = 5, diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala index e1882d59d5..c7c010a55c 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala @@ -121,6 +121,8 @@ class PaymentLifecycleSpec extends BaseRouterSpec { awaitCond(nodeParams.db.payments.getOutgoingPayment(id).exists(_.status.isInstanceOf[OutgoingPaymentStatus.Succeeded])) metricsListener.expectNoMessage() + + assert(routerForwarder.expectMsgType[RouteDidRelay].route === route) } test("send to route (node_id only)") { routerFixture => @@ -148,6 +150,8 @@ class PaymentLifecycleSpec extends BaseRouterSpec { awaitCond(nodeParams.db.payments.getOutgoingPayment(id).exists(_.status.isInstanceOf[OutgoingPaymentStatus.Succeeded])) metricsListener.expectNoMessage() + + assert(routerForwarder.expectMsgType[RouteDidRelay].route.hops.map(_.nodeId) === Seq(a, b, c)) } test("send to route (nodes not found in the graph)") { routerFixture => @@ -163,6 +167,8 @@ class PaymentLifecycleSpec extends BaseRouterSpec { assert(failureMessage == "Not all the nodes in the supplied route are connected with public channels") metricsListener.expectNoMessage() + + routerForwarder.expectNoMessage(100 millis) } test("send to route (channels not found in the graph)") { routerFixture => @@ -178,6 +184,8 @@ class PaymentLifecycleSpec extends BaseRouterSpec { assert(failureMessage == "The sequence of channels provided cannot be used to build a route to the target node") metricsListener.expectNoMessage() + + routerForwarder.expectNoMessage(100 millis) } test("send to route (routing hints)") { routerFixture => @@ -205,6 +213,8 @@ class PaymentLifecycleSpec extends BaseRouterSpec { awaitCond(nodeParams.db.payments.getOutgoingPayment(id).exists(_.status.isInstanceOf[OutgoingPaymentStatus.Succeeded])) metricsListener.expectNoMessage() + + assert(routerForwarder.expectMsgType[RouteDidRelay].route.hops.map(_.nodeId) === Seq(a, b, c)) } test("payment failed (route not found)") { routerFixture => @@ -228,6 +238,8 @@ class PaymentLifecycleSpec extends BaseRouterSpec { assert(metrics.amount == defaultAmountMsat) assert(metrics.fees == 4260000.msat) metricsListener.expectNoMessage() + + routerForwarder.expectNoMessage(100 millis) } test("payment failed (route too expensive)") { routerFixture => @@ -258,6 +270,8 @@ class PaymentLifecycleSpec extends BaseRouterSpec { assert(metrics.amount == defaultAmountMsat) assert(metrics.fees == 100.msat) metricsListener.expectNoMessage() + + routerForwarder.expectNoMessage(100 millis) } test("payment failed (cannot build onion)") { routerFixture => @@ -276,6 +290,8 @@ class PaymentLifecycleSpec extends BaseRouterSpec { assert(pf.failures.length == 1) assert(pf.failures.head.isInstanceOf[LocalFailure]) awaitCond(nodeParams.db.payments.getOutgoingPayment(id).exists(_.status.isInstanceOf[OutgoingPaymentStatus.Failed])) + + routerForwarder.expectNoMessage(100 millis) } test("payment failed (unparsable failure)") { routerFixture => @@ -319,6 +335,8 @@ class PaymentLifecycleSpec extends BaseRouterSpec { assert(metrics.amount == defaultAmountMsat) assert(metrics.fees == 4260000.msat) metricsListener.expectNoMessage() + + routerForwarder.expectNoMessage(100 millis) } test("payment failed (local error)") { routerFixture => @@ -450,6 +468,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { sender.send(paymentFSM, addCompleted(HtlcResult.RemoteFail(UpdateFailHtlc(ByteVector32.Zeroes, 0, Sphinx.FailurePacket.create(sharedSecrets1.head._1, failure))))) // payment lifecycle will ask the router to temporarily exclude this channel from its route calculations + assert(routerForwarder.expectMsgType[ChannelCouldNotRelay].hop.shortChannelId == update_bc.shortChannelId) routerForwarder.expectMsg(ExcludeChannel(ChannelDesc(update_bc.shortChannelId, b, c))) routerForwarder.forward(routerFixture.router) // payment lifecycle forwards the embedded channelUpdate to the router @@ -512,6 +531,8 @@ class PaymentLifecycleSpec extends BaseRouterSpec { // this time the router can't find a route: game over assert(sender.expectMsgType[PaymentFailed].failures == RemoteFailure(route1.amount, route1.hops, Sphinx.DecryptedFailurePacket(b, failure)) :: RemoteFailure(route2.amount, route2.hops, Sphinx.DecryptedFailurePacket(b, failure2)) :: LocalFailure(defaultAmountMsat, Nil, RouteNotFound) :: Nil) awaitCond(nodeParams.db.payments.getOutgoingPayment(id).exists(_.status.isInstanceOf[OutgoingPaymentStatus.Failed])) + + routerForwarder.expectNoMessage(100 millis) } test("payment failed (Update in last attempt)") { routerFixture => @@ -530,6 +551,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val failure = TemporaryChannelFailure(update_bc) sender.send(paymentFSM, addCompleted(HtlcResult.RemoteFail(UpdateFailHtlc(ByteVector32.Zeroes, 0, Sphinx.FailurePacket.create(sharedSecrets1.head._1, failure))))) // we should temporarily exclude that channel + assert(routerForwarder.expectMsgType[ChannelCouldNotRelay].hop.shortChannelId == update_bc.shortChannelId) routerForwarder.expectMsg(ExcludeChannel(ChannelDesc(update_bc.shortChannelId, b, c))) routerForwarder.expectMsg(update_bc) @@ -605,6 +627,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val failureOnion = Sphinx.FailurePacket.wrap(Sphinx.FailurePacket.create(sharedSecrets1(1)._1, failure), sharedSecrets1.head._1) sender.send(paymentFSM, addCompleted(HtlcResult.RemoteFail(UpdateFailHtlc(ByteVector32.Zeroes, 0, failureOnion)))) + assert(routerForwarder.expectMsgType[RouteCouldRelay].route.hops.map(_.shortChannelId) == Seq(update_ab, update_bc).map(_.shortChannelId)) routerForwarder.expectMsg(channelUpdate_cd_disabled) routerForwarder.expectMsg(ExcludeChannel(ChannelDesc(update_cd.shortChannelId, c, d))) } @@ -678,6 +701,8 @@ class PaymentLifecycleSpec extends BaseRouterSpec { assert(metrics.amount == defaultAmountMsat) assert(metrics.fees == 730.msat) metricsListener.expectNoMessage() + + assert(routerForwarder.expectMsgType[RouteDidRelay].route.hops.map(_.shortChannelId) == Seq(update_ab, update_bc, update_cd).map(_.shortChannelId)) } test("payment succeeded to a channel with fees=0") { routerFixture => @@ -731,6 +756,8 @@ class PaymentLifecycleSpec extends BaseRouterSpec { assert(metrics.amount == defaultAmountMsat) assert(metrics.fees == 0.msat) metricsListener.expectNoMessage() + + assert(routerForwarder.expectMsgType[RouteDidRelay].route.hops.map(_.shortChannelId) == Seq(update_ab, channelUpdate_bh).map(_.shortChannelId)) } test("filter errors properly") { () => @@ -802,6 +829,8 @@ class PaymentLifecycleSpec extends BaseRouterSpec { sender.expectMsgType[PaymentSent] assert(nodeParams.db.payments.getOutgoingPayment(id) == None) eventListener.expectNoMessage(100 millis) + + assert(routerForwarder.expectMsgType[RouteDidRelay].route.hops.map(_.nextNodeId) == Seq(b, c, d)) } test("send to route (no retry on error") { () => diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/BalanceEstimateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/BalanceEstimateSpec.scala new file mode 100644 index 0000000000..4e14e981b6 --- /dev/null +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/BalanceEstimateSpec.scala @@ -0,0 +1,292 @@ +/* + * Copyright 2021 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.router + +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.{Satoshi, SatoshiLong} +import fr.acinq.eclair.payment.Bolt11Invoice.ExtraHop +import fr.acinq.eclair.router.Graph.GraphStructure.{DirectedGraph, GraphEdge} +import fr.acinq.eclair.router.Router.{ChannelDesc, ChannelRelayParams} +import fr.acinq.eclair.{CltvExpiryDelta, MilliSatoshiLong, ShortChannelId, TimestampSecond, randomKey} +import org.scalactic.Tolerance.convertNumericToPlusOrMinusWrapper +import org.scalatest.funsuite.AnyFunSuite + +import scala.concurrent.duration.DurationInt + +class BalanceEstimateSpec extends AnyFunSuite { + + def isValid(balance: BalanceEstimate): Boolean = { + balance.low >= 0.msat && + balance.low <= balance.high && + balance.high <= balance.maxCapacity + } + + def makeEdge(nodeId1: PublicKey, nodeId2: PublicKey, channelId: Long, capacity: Satoshi): GraphEdge = + GraphEdge( + ChannelDesc(ShortChannelId(channelId), nodeId1, nodeId2), + ChannelRelayParams.FromHint(ExtraHop(nodeId1, ShortChannelId(channelId), 0 msat, 0, CltvExpiryDelta(0)), 0 msat), + capacity, None) + + def makeEdge(channelId: Long, capacity: Satoshi): GraphEdge = + makeEdge(randomKey().publicKey, randomKey().publicKey, channelId, capacity) + + test("no balance information") { + val balance = BalanceEstimate.empty(1 day).addEdge(makeEdge(0, 100 sat)) + assert(isValid(balance)) + assert(balance.canSend(0 msat) === 1.0 +- 0.001) + assert(balance.canSend(1 msat) === 1.0 +- 0.001) + assert(balance.canSend(25000 msat) === 0.75 +- 0.001) + assert(balance.canSend(50000 msat) === 0.5 +- 0.001) + assert(balance.canSend(75000 msat) === 0.25 +- 0.001) + assert(balance.canSend(99999 msat) === 0.0 +- 0.001) + assert(balance.canSend(100000 msat) === 0.0 +- 0.001) + } + + test("add and remove channels") { + val a = makeEdge(0, 200 sat) + val b = makeEdge(1, 100 sat) + val c = makeEdge(2, 800 sat) + val d = makeEdge(3, 120 sat) + val e = makeEdge(4, 190 sat) + val balance = BalanceEstimate.empty(1 day) + .addEdge(a) + .addEdge(b) + .removeEdge(a.desc) + assert(isValid(balance)) + assert(balance.maxCapacity == 100.sat) + val balance1 = balance + .addEdge(c) + .removeEdge(c.desc) + .removeEdge(b.desc) + .addEdge(d) + .addEdge(e) + assert(isValid(balance1)) + assert(balance1.maxCapacity == 190.sat) + val balance2 = balance1 + .removeEdge(d.desc) + .removeEdge(e.desc) + assert(isValid(balance2)) + assert(balance2.maxCapacity == 0.sat) + assert(balance2.capacities.isEmpty) + } + + test("update bounds based on what could then could not be sent (increasing amounts)") { + val now = TimestampSecond.now() + val balance = BalanceEstimate.empty(1 day) + // NB: the number of channels has no impact here + .addEdge(makeEdge(0, 100 sat)) + .addEdge(makeEdge(1, 100 sat)) + .couldSend(24000 msat, now) + .couldNotSend(30000 msat, now) + assert(balance.canSend(0 msat) === 1.0 +- 0.001) + assert(balance.canSend(23999 msat) === 1.0 +- 0.001) + assert(balance.canSend(24000 msat) === 1.0 +- 0.001) + assert(balance.canSend(24001 msat) === 1.0 +- 0.001) + assert(balance.canSend(27000 msat) === 0.5 +- 0.001) + assert(balance.canSend(29999 msat) === 0.0 +- 0.001) + assert(balance.canSend(30000 msat) === 0.0 +- 0.001) + assert(balance.canSend(30001 msat) === 0.0 +- 0.001) + assert(balance.canSend(100000 msat) === 0.0 +- 0.001) + } + + test("update bounds based on what could then could not be sent (decreasing amounts)") { + val now = TimestampSecond.now() + val balance = BalanceEstimate.empty(1 day) + // NB: the number of channels has no impact here + .addEdge(makeEdge(0, 75 sat)) + .addEdge(makeEdge(1, 100 sat)) + .couldSend(26000 msat, now) + .couldNotSend(14000 msat, now) + assert(isValid(balance)) + assert(balance.canSend(0 msat) === 1.0 +- 0.001) + assert(balance.canSend(1 msat) === 1.0 +- 0.001) + assert(balance.canSend(7000 msat) === 0.5 +- 0.001) + assert(balance.canSend(14000 msat) === 0.0 +- 0.001) + assert(balance.canSend(26000 msat) === 0.0 +- 0.001) + assert(balance.canSend(99999 msat) === 0.0 +- 0.001) + assert(balance.canSend(100000 msat) === 0.0 +- 0.001) + } + + test("update bounds based on what could not then could be sent (increasing amounts)") { + val now = TimestampSecond.now() + val balance = BalanceEstimate.empty(1 day) + // NB: the number of channels has no impact here + .addEdge(makeEdge(0, 100 sat)) + .addEdge(makeEdge(1, 50 sat)) + .couldNotSend(26000 msat, now) + .couldSend(30000 msat, now) + assert(isValid(balance)) + assert(balance.canSend(0 msat) === 1.0 +- 0.001) + assert(balance.canSend(1 msat) === 1.0 +- 0.001) + assert(balance.canSend(30000 msat) === 1.0 +- 0.001) + assert(balance.canSend(65000 msat) === 0.5 +- 0.001) + assert(balance.canSend(99999 msat) === 0.0 +- 0.001) + assert(balance.canSend(100000 msat) === 0.0 +- 0.001) + } + + test("update bounds based on what could not then could be sent (decreasing amounts)") { + val now = TimestampSecond.now() + val balance = BalanceEstimate.empty(1 day) + // NB: the number of channels has no impact here + .addEdge(makeEdge(0, 100 sat)) + .addEdge(makeEdge(1, 50 sat)) + .couldNotSend(30000 msat, now) + .couldSend(20000 msat, now) + assert(isValid(balance)) + assert(balance.canSend(0 msat) === 1.0 +- 0.001) + assert(balance.canSend(1 msat) === 1.0 +- 0.001) + assert(balance.canSend(20000 msat) === 1.0 +- 0.001) + assert(balance.canSend(25000 msat) === 0.5 +- 0.001) + assert(balance.canSend(30000 msat) === 0.0 +- 0.001) + assert(balance.canSend(99999 msat) === 0.0 +- 0.001) + assert(balance.canSend(100000 msat) === 0.0 +- 0.001) + } + + test("decay restores baseline bounds") { + val longAgo = TimestampSecond.now() - 30.seconds + val balance = BalanceEstimate.empty(1 second) + .addEdge(makeEdge(0, 100 sat)) + .couldNotSend(32000 msat, longAgo) + .couldSend(28000 msat, longAgo) + assert(isValid(balance)) + assert(balance.canSend(1 msat) === 1.0 +- 0.01) + assert(balance.canSend(33333 msat) === 0.666 +- 0.01) + assert(balance.canSend(66666 msat) === 0.333 +- 0.01) + assert(balance.canSend(99999 msat) === 0.0 +- 0.01) + } + + test("sending on single channel shifts amounts") { + val now = TimestampSecond.now() + val balance = BalanceEstimate.empty(1 day) + .addEdge(makeEdge(0, 100 sat)) + .couldNotSend(80000 msat, now) + .couldSend(50000 msat, now) + assert(isValid(balance)) + assert(balance.canSend(50000 msat) === 1.0 +- 0.001) + assert(balance.canSend(80000 msat) === 0.0 +- 0.001) + val balanceAfterSend = balance.didSend(20000 msat, now) + assert(isValid(balanceAfterSend)) + assert(balanceAfterSend.canSend(30000 msat) === 1.0 +- 0.001) + assert(balanceAfterSend.canSend(45000 msat) === 0.5 +- 0.001) + assert(balanceAfterSend.canSend(60000 msat) === 0.0 +- 0.001) + } + + test("sending on single channel after decay") { + val longAgo = TimestampSecond.now() - 60.seconds + val now = TimestampSecond.now() + val balance = BalanceEstimate.empty(1 second) + .addEdge(makeEdge(0, 100 sat)) + .couldNotSend(80000 msat, longAgo) + .couldSend(50000 msat, longAgo) + .didSend(40000 msat, now) + assert(isValid(balance)) + assert(balance.canSend(0 msat) === 1.0 +- 0.01) + assert(balance.canSend(10000 msat) <= 0.9) + assert(balance.canSend(50000 msat) >= 0.1) + assert(balance.canSend(60000 msat) === 0.0 +- 0.01) + } + + test("sending on parallel channels shifts low only") { + val now = TimestampSecond.now() + val balance = BalanceEstimate.empty(1 day) + .addEdge(makeEdge(0, 100 sat)) + .addEdge(makeEdge(1, 80 sat)) + .couldNotSend(80000 msat, now) + .couldSend(50000 msat, now) + assert(isValid(balance)) + assert(balance.canSend(50000 msat) === 1.0 +- 0.001) + assert(balance.canSend(80000 msat) === 0.0 +- 0.001) + val balanceAfterSend = balance.didSend(20000 msat, now) + assert(isValid(balanceAfterSend)) + assert(balanceAfterSend.canSend(30000 msat) === 1.0 +- 0.001) + assert(balanceAfterSend.canSend(70000 msat) > 0.1) + } + + test("receiving on single channel shifts amounts") { + val now = TimestampSecond.now() + val balance = BalanceEstimate.empty(1 day) + .addEdge(makeEdge(0, 100 sat)) + .couldNotSend(80000 msat, now) + .couldSend(50000 msat, now) + .didReceive(10000 msat, now) + assert(isValid(balance)) + assert(balance.canSend(60000 msat) === 1.0 +- 0.001) + assert(balance.canSend(75000 msat) === 0.5 +- 0.001) + assert(balance.canSend(90000 msat) === 0.0 +- 0.001) + } + + test("receiving on single channel after decay") { + val longAgo = TimestampSecond.now() - 60.seconds + val now = TimestampSecond.now() + val balance = BalanceEstimate.empty(1 second) + .addEdge(makeEdge(0, 100 sat)) + .couldNotSend(80000 msat, longAgo) + .couldSend(50000 msat, longAgo) + .didReceive(10000 msat, now) + assert(isValid(balance)) + assert(balance.canSend(10000 msat) >= 0.9) + assert(balance.canSend(20000 msat) <= 0.9) + assert(balance.canSend(80000 msat) >= 0.1) + assert(balance.canSend(90000 msat) <= 0.1) + } + + test("receiving on parallel channels shifts high only") { + val now = TimestampSecond.now() + val balance = BalanceEstimate.empty(1 day) + .addEdge(makeEdge(0, 100 sat)) + .addEdge(makeEdge(1, 80 sat)) + .couldNotSend(70000 msat, now) + .couldSend(50000 msat, now) + .didReceive(20000 msat, now) + assert(isValid(balance)) + assert(balance.canSend(50000 msat) === 1.0 +- 0.001) + assert(balance.canSend(70000 msat) === 0.5 +- 0.001) + assert(balance.canSend(90000 msat) === 0.0 +- 0.001) + } + + test("baseline from graph") { + val (a, b, c) = (randomKey().publicKey, randomKey().publicKey, randomKey().publicKey) + val g = DirectedGraph(Seq( + makeEdge(a, b, 1, 100 sat), + makeEdge(b, a, 1, 100 sat), + makeEdge(a, b, 2, 110 sat), + makeEdge(b, a, 3, 120 sat), + makeEdge(a, c, 4, 130 sat), + makeEdge(c, a, 4, 130 sat), + makeEdge(a, c, 5, 140 sat), + makeEdge(c, a, 5, 140 sat), + makeEdge(b, c, 6, 150 sat), + )) + + val graphWithBalances = GraphWithBalanceEstimates(g, 1 day) + // NB: it doesn't matter which edge is selected, the balance estimation takes all existing edges into account. + val edge_ab = makeEdge(a, b, 1, 10 sat) + val edge_ba = makeEdge(b, a, 1, 10 sat) + val edge_bc = makeEdge(b, c, 6, 10 sat) + assert(graphWithBalances.canSend(27500 msat, edge_ab) === 0.75 +- 0.01) + assert(graphWithBalances.canSend(55000 msat, edge_ab) === 0.5 +- 0.01) + assert(graphWithBalances.canSend(30000 msat, edge_ba) === 0.75 +- 0.01) + assert(graphWithBalances.canSend(60000 msat, edge_ba) === 0.5 +- 0.01) + assert(graphWithBalances.canSend(75000 msat, edge_bc) === 0.5 +- 0.01) + assert(graphWithBalances.canSend(100000 msat, edge_bc) === 0.33 +- 0.01) + val unknownEdge = makeEdge(42, 40 sat) + assert(graphWithBalances.canSend(10000 msat, unknownEdge) === 0.75 +- 0.01) + assert(graphWithBalances.canSend(20000 msat, unknownEdge) === 0.5 +- 0.01) + assert(graphWithBalances.canSend(30000 msat, unknownEdge) === 0.25 +- 0.01) + } + +} diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/RouterSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/RouterSpec.scala index d5dfcd1850..4c5b9d0eb8 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/RouterSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/RouterSpec.scala @@ -617,7 +617,7 @@ class RouterSpec extends BaseRouterSpec { assert(Set(channel_ab.meta_opt.map(_.balance1), channel_ab.meta_opt.map(_.balance2)) == balances) // And the graph should be updated too. sender.send(router, Router.GetRouterData) - val g = sender.expectMsgType[Data].graph + val g = sender.expectMsgType[Data].graphWithBalances.graph val edge_ab = g.getEdge(ChannelDesc(scid_ab, a, b)).get val edge_ba = g.getEdge(ChannelDesc(scid_ab, b, a)).get assert(edge_ab.capacity == channel_ab.capacity && edge_ba.capacity == channel_ab.capacity) @@ -640,7 +640,7 @@ class RouterSpec extends BaseRouterSpec { assert(Set(channel_ab.meta_opt.map(_.balance1), channel_ab.meta_opt.map(_.balance2)) == balances) // And the graph should be updated too. sender.send(router, Router.GetRouterData) - val g = sender.expectMsgType[Data].graph + val g = sender.expectMsgType[Data].graphWithBalances.graph val edge_ab = g.getEdge(ChannelDesc(scid_ab, a, b)).get val edge_ba = g.getEdge(ChannelDesc(scid_ab, b, a)).get assert(edge_ab.capacity == channel_ab.capacity && edge_ba.capacity == channel_ab.capacity) @@ -658,7 +658,7 @@ class RouterSpec extends BaseRouterSpec { assert(Set(channel_ab.meta_opt.map(_.balance1), channel_ab.meta_opt.map(_.balance2)) == balances) // And the graph should be updated too. sender.send(router, Router.GetRouterData) - val g = sender.expectMsgType[Data].graph + val g = sender.expectMsgType[Data].graphWithBalances.graph val edge_ab = g.getEdge(ChannelDesc(scid_ab, a, b)).get val edge_ba = g.getEdge(ChannelDesc(scid_ab, b, a)).get assert(edge_ab.capacity == channel_ab.capacity && edge_ba.capacity == channel_ab.capacity) @@ -676,7 +676,7 @@ class RouterSpec extends BaseRouterSpec { val channel_ag = data.privateChannels(channelId_ag_private) assert(Set(channel_ag.meta.balance1, channel_ag.meta.balance2) == balances) // And the graph should be updated too. - val edge_ag = data.graph.getEdge(ChannelDesc(scid_ag_private, a, g)).get + val edge_ag = data.graphWithBalances.graph.getEdge(ChannelDesc(scid_ag_private, a, g)).get assert(edge_ag.capacity == channel_ag.capacity) assert(edge_ag.balance_opt == Some(33000000 msat)) } From c4786f3a7b9458ec62251f4ca1455fcc72a13b0c Mon Sep 17 00:00:00 2001 From: Bastien Teinturier <31281497+t-bast@users.noreply.github.com> Date: Wed, 8 Jun 2022 16:33:59 +0200 Subject: [PATCH 089/121] Store disabled channel update when offline (#2307) When a channel is offline and we want to relay an HTLC through it, we emit a new `channel_update` with the disable bit set (unless it was already disabled). We previously didn't persist our internal state with this new `channel_update`, which created the following issue: if eclair is restarted before the channel comes back online, eclair would only try to rebroadcast the previous `channel_update` which has a lower timestamp than the one disabling the channel. That `channel_update` is then rejected by the network, causing the channel to stay disabled until the next refresh, which only happens after 10 days. --- .../main/scala/fr/acinq/eclair/channel/fsm/Channel.scala | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala index adc691063d..3b1b73c223 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala @@ -699,18 +699,17 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val // we cancel the timer that would have made us send the enabled update after reconnection (flappy channel protection) cancelTimer(Reconnected.toString) // if we have pending unsigned htlcs, then we cancel them and generate an update with the disabled flag set, that will be returned to the sender in a temporary channel failure - val d1 = if (d.commitments.localChanges.proposed.collectFirst { case add: UpdateAddHtlc => add }.isDefined) { + if (d.commitments.localChanges.proposed.collectFirst { case add: UpdateAddHtlc => add }.isDefined) { log.debug("updating channel_update announcement (reason=disabled)") val channelUpdate1 = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, d.shortChannelId, d.channelUpdate.cltvExpiryDelta, d.channelUpdate.htlcMinimumMsat, d.channelUpdate.feeBaseMsat, d.channelUpdate.feeProportionalMillionths, d.commitments.capacity.toMilliSatoshi, enable = false) // NB: the htlcs stay() in the commitments.localChange, they will be cleaned up after reconnection d.commitments.localChanges.proposed.collect { case add: UpdateAddHtlc => relayer ! RES_ADD_SETTLED(d.commitments.originChannels(add.id), add, HtlcResult.DisconnectedBeforeSigned(channelUpdate1)) } - d.copy(channelUpdate = channelUpdate1) + goto(OFFLINE) using d.copy(channelUpdate = channelUpdate1) storing() } else { - d + goto(OFFLINE) using d } - goto(OFFLINE) using d1 case Event(e: Error, d: DATA_NORMAL) => handleRemoteError(e, d) @@ -1788,7 +1787,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val // then we update the state and replay the request self forward c // we use goto() to fire transitions - goto(stateName) using d.copy(channelUpdate = channelUpdate1) + goto(stateName) using d.copy(channelUpdate = channelUpdate1) storing() } else { // channel is already disabled, we reply to the request val error = ChannelUnavailable(d.channelId) From 3e3f786000acf4c339784a01ee11273997ad0c27 Mon Sep 17 00:00:00 2001 From: llya Evdokimov Date: Mon, 13 Jun 2022 08:35:19 +0200 Subject: [PATCH 090/121] Allow disabling bitcoind requests batching (#2301) Eclair batches bitcoind request by default for efficiency. However this can create latency and reliability issues when running bitcoind on a remote machine over an unreliable network. In those cases it's better to opt-out of batching. We also make the threshold of missing blocks for blockchain watchdog configurable instead of a hard-coded value of 6. --- eclair-core/src/main/resources/reference.conf | 7 ++++++- .../src/main/scala/fr/acinq/eclair/NodeParams.scala | 2 ++ eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala | 9 ++++++++- .../eclair/blockchain/watchdogs/BlockchainWatchdog.scala | 2 +- .../src/test/scala/fr/acinq/eclair/TestConstants.scala | 2 ++ 5 files changed, 19 insertions(+), 3 deletions(-) diff --git a/eclair-core/src/main/resources/reference.conf b/eclair-core/src/main/resources/reference.conf index 8b6739d551..c44e058c0c 100644 --- a/eclair-core/src/main/resources/reference.conf +++ b/eclair-core/src/main/resources/reference.conf @@ -29,6 +29,9 @@ eclair { wallet = "" zmqblock = "tcp://127.0.0.1:29000" zmqtx = "tcp://127.0.0.1:29000" + // Batching requests saves bandwidth but may slightly degrade latency and reliability. + // You may want to disable this when bitcoin is running on a remote machine with an unreliable network. + batch-requests = true } node-alias = "eclair" @@ -350,7 +353,7 @@ eclair { } safety-checks { // A set of basic checks on data to make sure we use the correct database - // Those checks are disabled by default because they wouldn't pass on a fresh new node with + // Those checks are disabled by default because they would not pass on a fresh new node with // zero channels. You should enable them when you already have channels, so that there is // something to compare to, and the values should be specific to your setup, especially // for local channels. If your operate a busy node, you can reduce max-age.local-channels @@ -391,6 +394,8 @@ eclair { "blockstream.info", "mempool.space" ] + // maximum lag of chain height observed by eclair to blockchain watchdog sources + missing-blocks-threshold = 7 } onion-messages { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala index 155a56c1e1..7a7fdbb066 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala @@ -79,6 +79,7 @@ case class NodeParams(nodeKeyManager: NodeKeyManager, maxPaymentAttempts: Int, enableTrampolinePayment: Boolean, balanceCheckInterval: FiniteDuration, + blockchainWatchdogThreshold: Int, blockchainWatchdogSources: Seq[String], onionMessageConfig: OnionMessageConfig, purgeInvoicesInterval: Option[FiniteDuration]) { @@ -500,6 +501,7 @@ object NodeParams extends Logging { maxPaymentAttempts = config.getInt("max-payment-attempts"), enableTrampolinePayment = config.getBoolean("trampoline-payments-enable"), balanceCheckInterval = FiniteDuration(config.getDuration("balance-check-interval").getSeconds, TimeUnit.SECONDS), + blockchainWatchdogThreshold = config.getInt("blockchain-watchdog.missing-blocks-threshold"), blockchainWatchdogSources = config.getStringList("blockchain-watchdog.sources").asScala.toSeq, onionMessageConfig = OnionMessageConfig( relayPolicy = onionMessageRelayPolicy, diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala index ecbe266f0e..2b21c48c09 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala @@ -157,6 +157,7 @@ class Setup(val datadir: File, } case "password" => BitcoinJsonRPCAuthMethod.UserPassword(config.getString("bitcoind.rpcuser"), config.getString("bitcoind.rpcpassword")) } + val bitcoinClient = new BasicBitcoinJsonRPCClient( rpcAuthMethod = rpcAuthMethod, host = config.getString("bitcoind.host"), @@ -253,7 +254,13 @@ class Setup(val datadir: File, }) _ <- feeratesRetrieved.future - bitcoinClient = new BitcoinCoreClient(new BatchingBitcoinJsonRPCClient(bitcoin)) + bitcoinClient = config.getBoolean("bitcoind.batch-requests") match { + case true => + new BitcoinCoreClient(new BatchingBitcoinJsonRPCClient(bitcoin)) + case _ => + new BitcoinCoreClient(bitcoin) + } + watcher = { system.actorOf(SimpleSupervisor.props(Props(new ZMQActor(config.getString("bitcoind.zmqblock"), ZMQActor.Topics.HashBlock, Some(zmqBlockConnected))), "zmqblock", SupervisorStrategy.Restart)) system.actorOf(SimpleSupervisor.props(Props(new ZMQActor(config.getString("bitcoind.zmqtx"), ZMQActor.Topics.RawTx, Some(zmqTxConnected))), "zmqtx", SupervisorStrategy.Restart)) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/watchdogs/BlockchainWatchdog.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/watchdogs/BlockchainWatchdog.scala index 3309037414..9039d2375e 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/watchdogs/BlockchainWatchdog.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/watchdogs/BlockchainWatchdog.scala @@ -115,7 +115,7 @@ object BlockchainWatchdog { case h if h.isEmpty => 0 case _ => blockHeaders.map(_.blockHeight).max - blockHeight } - if (missingBlocks >= 6) { + if (missingBlocks >= nodeParams.blockchainWatchdogThreshold) { context.log.warn("{}: we are {} blocks late: we may be eclipsed from the bitcoin network", source, missingBlocks) context.system.eventStream ! EventStream.Publish(DangerousBlocksSkew(headers)) context.system.eventStream ! EventStream.Publish(NotifyNodeOperator(NotificationsLogger.Warning, s"we are $missingBlocks late according to $source: we may be eclipsed from the bitcoin network, check your bitcoind node.")) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala b/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala index aedfb336f3..a0429479a7 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala @@ -194,6 +194,7 @@ object TestConstants { enableTrampolinePayment = true, instanceId = UUID.fromString("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"), balanceCheckInterval = 1 hour, + blockchainWatchdogThreshold = 6, blockchainWatchdogSources = blockchainWatchdogSources, onionMessageConfig = OnionMessageConfig( relayPolicy = RelayAll, @@ -334,6 +335,7 @@ object TestConstants { enableTrampolinePayment = true, instanceId = UUID.fromString("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"), balanceCheckInterval = 1 hour, + blockchainWatchdogThreshold = 6, blockchainWatchdogSources = blockchainWatchdogSources, onionMessageConfig = OnionMessageConfig( relayPolicy = RelayAll, From e8c9df4ec48ec1746ffa727d17bb4563c517b25e Mon Sep 17 00:00:00 2001 From: Barry G Becker Date: Mon, 13 Jun 2022 01:59:50 -0700 Subject: [PATCH 091/121] Add more GraphSpec tests (#2292) (#2293) Add test for the wikipedia example of Yen's algorithm --- .../fr/acinq/eclair/router/GraphSpec.scala | 97 ++++++++++++++++++- 1 file changed, 92 insertions(+), 5 deletions(-) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/GraphSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/GraphSpec.scala index 942ede4076..c2e832905b 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/GraphSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/GraphSpec.scala @@ -20,7 +20,7 @@ import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.bitcoin.scalacompat.SatoshiLong import fr.acinq.eclair.payment.relay.Relayer.RelayFees import fr.acinq.eclair.router.Graph.GraphStructure.{DirectedGraph, GraphEdge} -import fr.acinq.eclair.router.Graph.{HeuristicsConstants, yenKshortestPaths} +import fr.acinq.eclair.router.Graph.{HeuristicsConstants, WeightRatios, yenKshortestPaths} import fr.acinq.eclair.router.RouteCalculationSpec._ import fr.acinq.eclair.router.Router.ChannelDesc import fr.acinq.eclair.{BlockHeight, MilliSatoshiLong, ShortChannelId} @@ -29,14 +29,15 @@ import scodec.bits._ class GraphSpec extends AnyFunSuite { - val (a, b, c, d, e, f, g) = ( + val (a, b, c, d, e, f, g, h) = ( PublicKey(hex"02999fa724ec3c244e4da52b4a91ad421dc96c9a810587849cd4b2469313519c73"), //a PublicKey(hex"03f1cb1af20fe9ccda3ea128e27d7c39ee27375c8480f11a87c17197e97541ca6a"), //b PublicKey(hex"0358e32d245ff5f5a3eb14c78c6f69c67cea7846bdf9aeeb7199e8f6fbb0306484"), //c PublicKey(hex"029e059b6780f155f38e83601969919aae631ddf6faed58fe860c72225eb327d7c"), //d PublicKey(hex"02f38f4e37142cc05df44683a83e22dea608cf4691492829ff4cf99888c5ec2d3a"), //e PublicKey(hex"03fc5b91ce2d857f146fd9b986363374ffe04dc143d8bcd6d7664c8873c463cdfc"), //f - PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f") //g + PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f"), //g + PublicKey(hex"03bfddd2253b42fe12edd37f9071a3883830ed61a4bc347eeac63421629cf032b5") //h ) // +---- D -------+ @@ -44,7 +45,7 @@ class GraphSpec extends AnyFunSuite { // A --- B ------ C // | | // +--- E --+ - def makeTestGraph() = + private def makeTestGraph() = DirectedGraph().addEdges(Seq( makeEdge(1L, a, b, 0 msat, 0), makeEdge(2L, b, c, 0 msat, 0), @@ -238,7 +239,7 @@ class GraphSpec extends AnyFunSuite { def edgeFromNodes(shortChannelId: Long, a: PublicKey, b: PublicKey): GraphEdge = makeEdge(shortChannelId, a, b, 0 msat, 0) - test("amount with fees larger than channel capacity") { + test("amount with fees larger than channel capacity for C->D") { /* The channel C -> D is just large enough for the payment to go through but when adding the channel fee it becomes too big. A --> B --> C <-> D @@ -261,4 +262,90 @@ class GraphSpec extends AnyFunSuite { BlockHeight(714930), _ => true, includeLocalChannelCost = true) assert(path.path == Seq(edgeAB, edgeBC, edgeCE)) } + + test("fees less along C->D->E than C->E") { + /* + The channel going through C -> D -> E (fee = 1001 + 1 = 1002) is considered shorter than going + through C -> E (fee = 1003) because the fees are less. + A --> B --> C <-> D + \ / + \ / + E + */ + val edgeAB = makeEdge(1L, a, b, 10001 msat, 0, capacity = 200000 sat) + val edgeBC = makeEdge(2L, b, c, 10000 msat, 0, capacity = 200000 sat) + val edgeCD = makeEdge(3L, c, d, 10001 msat, 0, capacity = 200000 sat) + val edgeDC = makeEdge(4L, d, c, 10 msat, 0, capacity = 200000 sat) + val edgeCE = makeEdge(5L, c, e, 10003 msat, 0, capacity = 200000 sat) + val edgeDE = makeEdge(6L, d, e, 1 msat, 0, capacity = 200000 sat) + val graph = DirectedGraph(Seq(edgeAB, edgeBC, edgeCD, edgeDC, edgeCE, edgeDE)) + + val paths = yenKshortestPaths(graph, a, e, 90000000 msat, + Set.empty, Set.empty, Set.empty, 2, + Left(WeightRatios(1, 0, 0, 0, RelayFees(0 msat, 0))), + BlockHeight(714930), _ => true, includeLocalChannelCost = true) + + assert(paths.length == 2) + assert(paths(0).path == Seq(edgeAB, edgeBC, edgeCD, edgeDE)) + assert(paths(1).path == Seq(edgeAB, edgeBC, edgeCE)) + } + + test("Path C->D->E selected because amount too great for C->E capacity") { + /* + The channel C -> D -> E is a valid path, but C -> E is not. + A --> B --> C <-> D + \ / + \ / + E + */ + val edgeAB = makeEdge(1L, a, b, 10001 msat, 0, capacity = 200000 sat) + val edgeBC = makeEdge(2L, b, c, 10000 msat, 0, capacity = 200000 sat) + val edgeCD = makeEdge(3L, c, d, 20001 msat, 0, capacity = 200000 sat) + val edgeDC = makeEdge(4L, d, c, 10 msat, 0, capacity = 200000 sat) + val edgeCE = makeEdge(5L, c, e, 10003 msat, 0, capacity = 10000 sat) + val edgeDE = makeEdge(6L, d, e, 1 msat, 0, capacity = 200000 sat) + val graph = DirectedGraph(Seq(edgeAB, edgeBC, edgeCD, edgeDC, edgeCE, edgeDE)) + + val paths = yenKshortestPaths(graph, a, e, 90000000 msat, + Set.empty, Set.empty, Set.empty, 2, + Left(WeightRatios(1, 0, 0, 0, RelayFees(0 msat, 0))), + BlockHeight(714930), _ => true, includeLocalChannelCost = true) + + // Even though paths to find is 2, we only find 1 because that is all the valid paths that there are. + assert(paths.length == 1) + assert(paths.head.path == Seq(edgeAB, edgeBC, edgeCD, edgeDE)) + } + + /** + * Find all the shortest paths using the example described in + * https://en.wikipedia.org/wiki/Yen's_algorithm#Example + */ + test("all shortest paths are found") { + // There will be 3 shortest paths. + // Edge capacities are set to be the same so that only feeBase will affect the RichWeight. + // C --> D --> F + // \ ^ / | \ + // \ | / | \ + // \ | / | \ + // E --> G --> H + val edgeCD = makeEdge(1L, c, d, 3 msat, 0, capacity = 100000 sat, minHtlc = 1000 msat) + val edgeDF = makeEdge(2L, d, f, 4 msat, 0, capacity = 100000 sat, minHtlc = 1000 msat) + val edgeCE = makeEdge(3L, c, e, 2 msat, 0, capacity = 100000 sat, minHtlc = 1000 msat) + val edgeED = makeEdge(4L, e, d, 1 msat, 0, capacity = 100000 sat, minHtlc = 1000 msat) + val edgeEF = makeEdge(5L, e, f, 2 msat, 0, capacity = 100000 sat, minHtlc = 1000 msat) + val edgeFG = makeEdge(6L, f, g, 2 msat, 0, capacity = 100000 sat, minHtlc = 1000 msat) + val edgeFH = makeEdge(7L, f, h, 1 msat, 0, capacity = 100000 sat, minHtlc = 1000 msat) + val edgeEG = makeEdge(8L, e, g, 3 msat, 0, capacity = 100000 sat, minHtlc = 1000 msat) + val edgeGH = makeEdge(9L, g, h, 2 msat, 0, capacity = 100000 sat, minHtlc = 1000 msat) + val graph = DirectedGraph(Seq(edgeCD, edgeDF, edgeCE, edgeED, edgeEF, edgeFG, edgeFH, edgeEG, edgeGH)) + + val paths = yenKshortestPaths(graph, c, h, 10000000 msat, + Set.empty, Set.empty, Set.empty, 3, + Left(WeightRatios(1, 0, 0, 0, RelayFees(0 msat, 0))), + BlockHeight(714930), _ => true, includeLocalChannelCost = true) + assert(paths.length == 3) + assert(paths(0).path == Seq(edgeCE, edgeEF, edgeFH)) + assert(paths(1).path == Seq(edgeCE, edgeEG, edgeGH)) + assert(paths(2).path == Seq(edgeCD, edgeDF, edgeFH)) + } } From e5f5cd152e9cb881395afb5a04bbf045c548611a Mon Sep 17 00:00:00 2001 From: Pierre-Marie Padiou Date: Wed, 15 Jun 2022 13:09:54 +0200 Subject: [PATCH 092/121] Add support for zero-conf and scid-alias (#2224) This implements lightning/bolts#910. Co-authored-by: Bastien Teinturier <31281497+t-bast@users.noreply.github.com> --- docs/release-notes/eclair-vnext.md | 111 +++++- eclair-core/src/main/resources/reference.conf | 6 +- .../scala/fr/acinq/eclair/BlockHeight.scala | 2 + .../main/scala/fr/acinq/eclair/Eclair.scala | 20 +- .../main/scala/fr/acinq/eclair/Features.scala | 16 +- .../fr/acinq/eclair/ShortChannelId.scala | 63 +++- .../acinq/eclair/balance/BalanceActor.scala | 2 +- .../acinq/eclair/balance/CheckBalance.scala | 6 +- .../fr/acinq/eclair/balance/Monitoring.scala | 2 +- .../blockchain/bitcoind/ZmqWatcher.scala | 6 +- .../eclair/blockchain/fee/FeeEstimator.scala | 2 +- .../watchdogs/BlockchainWatchdog.scala | 4 +- .../fr/acinq/eclair/channel/ChannelData.scala | 45 ++- .../acinq/eclair/channel/ChannelEvents.scala | 33 +- .../eclair/channel/ChannelFeatures.scala | 35 +- .../fr/acinq/eclair/channel/Commitments.scala | 2 +- .../fr/acinq/eclair/channel/Helpers.scala | 78 +++-- .../fr/acinq/eclair/channel/Register.scala | 11 +- .../fr/acinq/eclair/channel/fsm/Channel.scala | 259 +++++++------- .../channel/fsm/ChannelOpenSingleFunder.scala | 101 +++--- .../eclair/channel/fsm/FundingHandlers.scala | 19 +- .../eclair/crypto/TransportHandler.scala | 2 + .../fr/acinq/eclair/db/DbEventHandler.scala | 2 +- .../fr/acinq/eclair/db/DualDatabases.scala | 7 +- .../scala/fr/acinq/eclair/db/NetworkDb.scala | 7 +- .../fr/acinq/eclair/db/pg/PgNetworkDb.scala | 14 +- .../eclair/db/sqlite/SqliteNetworkDb.scala | 14 +- .../main/scala/fr/acinq/eclair/io/Peer.scala | 16 +- .../acinq/eclair/json/JsonSerializers.scala | 24 +- .../main/scala/fr/acinq/eclair/package.scala | 3 +- .../eclair/payment/relay/ChannelRelay.scala | 3 +- .../eclair/payment/relay/ChannelRelayer.scala | 29 +- .../remote/EclairInternalsSerializer.scala | 2 +- .../acinq/eclair/router/Announcements.scala | 7 +- .../scala/fr/acinq/eclair/router/Graph.scala | 13 +- .../acinq/eclair/router/NetworkEvents.scala | 3 +- .../eclair/router/RouteCalculation.scala | 4 +- .../scala/fr/acinq/eclair/router/Router.scala | 63 ++-- .../acinq/eclair/router/StaleChannels.scala | 2 +- .../scala/fr/acinq/eclair/router/Sync.scala | 31 +- .../fr/acinq/eclair/router/Validation.scala | 270 +++++++++------ .../channel/version0/ChannelCodecs0.scala | 35 +- .../channel/version1/ChannelCodecs1.scala | 22 +- .../channel/version2/ChannelCodecs2.scala | 22 +- .../channel/version3/ChannelCodecs3.scala | 47 ++- .../eclair/wire/protocol/ChannelTlv.scala | 16 +- .../eclair/wire/protocol/CommonCodecs.scala | 21 +- .../eclair/wire/protocol/FailureMessage.scala | 2 + .../protocol/LightningMessageCodecs.scala | 14 +- .../wire/protocol/LightningMessageTypes.scala | 15 +- .../fr/acinq/eclair/EclairImplSpec.scala | 34 +- .../fr/acinq/eclair/ShortChannelIdSpec.scala | 39 ++- .../scala/fr/acinq/eclair/TestDatabases.scala | 2 +- .../scala/fr/acinq/eclair/TestUtils.scala | 25 +- .../bitcoind/BitcoinCoreClientSpec.scala | 2 +- .../blockchain/bitcoind/ZmqWatcherSpec.scala | 14 +- .../blockchain/fee/FeeEstimatorSpec.scala | 20 +- .../eclair/channel/ChannelFeaturesSpec.scala | 52 +-- .../fr/acinq/eclair/channel/HelpersSpec.scala | 17 +- .../fr/acinq/eclair/channel/RestoreSpec.scala | 34 +- .../publish/ReplaceableTxPublisherSpec.scala | 68 ++-- .../ChannelStateTestsHelperMethods.scala | 49 ++- .../a/WaitForAcceptChannelStateSpec.scala | 23 +- .../a/WaitForOpenChannelStateSpec.scala | 30 +- .../b/WaitForFundingCreatedStateSpec.scala | 5 +- .../b/WaitForFundingInternalStateSpec.scala | 5 +- .../b/WaitForFundingSignedStateSpec.scala | 22 +- .../c/WaitForChannelReadyStateSpec.scala | 255 ++++++++++++++ .../c/WaitForFundingConfirmedStateSpec.scala | 92 +++-- .../c/WaitForFundingLockedStateSpec.scala | 154 --------- .../channel/states/e/NormalStateSpec.scala | 165 ++++++--- .../channel/states/e/OfflineStateSpec.scala | 83 +++-- .../channel/states/h/ClosingStateSpec.scala | 5 +- .../fr/acinq/eclair/db/AuditDbSpec.scala | 2 +- .../fr/acinq/eclair/db/ChannelsDbSpec.scala | 5 +- .../fr/acinq/eclair/db/NetworkDbSpec.scala | 16 +- .../integration/ChannelIntegrationSpec.scala | 10 +- .../eclair/integration/IntegrationSpec.scala | 1 - .../integration/MessageIntegrationSpec.scala | 4 +- .../integration/PaymentIntegrationSpec.scala | 32 +- .../basic/ThreeNodesIntegrationSpec.scala | 32 +- .../basic/TwoNodesIntegrationSpec.scala | 18 +- .../basic/ZeroConfActivationSpec.scala | 79 +++++ .../basic/ZeroConfAliasIntegrationSpec.scala | 231 +++++++++++++ .../basic/fixtures/MinimalNodeFixture.scala | 87 +++-- .../acinq/eclair/io/PeerConnectionSpec.scala | 5 +- .../scala/fr/acinq/eclair/io/PeerSpec.scala | 12 +- .../eclair/json/JsonSerializersSpec.scala | 16 +- .../eclair/payment/PaymentLifecycleSpec.scala | 2 +- .../eclair/payment/PaymentPacketSpec.scala | 4 +- .../payment/relay/ChannelRelayerSpec.scala | 324 +++++++++++------- .../eclair/payment/relay/RelayerSpec.scala | 5 +- .../AnnouncementsBatchValidationSpec.scala | 8 +- .../eclair/router/AnnouncementsSpec.scala | 5 +- .../acinq/eclair/router/BaseRouterSpec.scala | 40 ++- .../router/ChannelRangeQueriesSpec.scala | 37 +- .../router/ChannelRouterIntegrationSpec.scala | 236 ++++++++----- .../eclair/router/RouteCalculationSpec.scala | 13 +- .../fr/acinq/eclair/router/RouterSpec.scala | 57 +-- .../acinq/eclair/router/RoutingSyncSpec.scala | 17 +- .../internal/channel/ChannelCodecsSpec.scala | 11 +- .../channel/version3/ChannelCodecs3Spec.scala | 24 +- .../protocol/ExtendedQueriesCodecsSpec.scala | 18 +- .../protocol/LightningMessageCodecsSpec.scala | 47 +-- .../fr/acinq/eclair/router/FrontRouter.scala | 11 +- .../acinq/eclair/router/FrontRouterSpec.scala | 10 +- .../acinq/eclair/api/handlers/Channel.scala | 25 +- .../api/serde/FormParamExtractors.scala | 2 +- .../fr/acinq/eclair/api/ApiServiceSpec.scala | 8 +- pom.xml | 4 +- 110 files changed, 2804 insertions(+), 1382 deletions(-) create mode 100644 eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForChannelReadyStateSpec.scala delete mode 100644 eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingLockedStateSpec.scala create mode 100644 eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/ZeroConfActivationSpec.scala create mode 100644 eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/ZeroConfAliasIntegrationSpec.scala diff --git a/docs/release-notes/eclair-vnext.md b/docs/release-notes/eclair-vnext.md index 31392c7d38..33cdb33d0b 100644 --- a/docs/release-notes/eclair-vnext.md +++ b/docs/release-notes/eclair-vnext.md @@ -4,12 +4,88 @@ ## Major changes -Dropped support for version 2 of Tor protocol. That means +### Add support for channel aliases and zeroconf channels + +:information_source: Those features are only supported for channels of type `AnchorOutputsZeroFeeHtlcTx`, which is the +newest channel type and the one enabled by default. If you are opening a channel with a node that doesn't run Eclair, +make sure they support `option_anchors_zero_fee_htlc_tx`. + +#### Channel aliases + +Channel aliases offer a way to use arbitrary channel identifiers for routing. This feature improves privacy by not +leaking the funding transaction of the channel during payments. + +This feature is enabled by default, but your peer has to support it too, and it is not compatible with public channels. + +#### Zeroconf channels + +Zeroconf channels make it possible to use a newly created channel before the funding tx is confirmed on the blockchain. + +:warning: Zeroconf requires the fundee to trust the funder. For this reason it is disabled by default, and you should +only enable it on a peer-by-peer basis. + +##### Enabling through features + +Below is how to enable zeroconf with a given peer in `eclair.conf`. With this config, your node will _accept_ zeroconf +channels from node `03864e...`. + +```eclair.conf +override-init-features = [ + { + nodeid = "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f", + features = { + // these features need to be enabled + var_onion_optin = mandatory + payment_secret = mandatory + option_channel_type = optional + // dependencies of zeroconf + option_static_remotekey = optional + option_anchors_zero_fee_htlc_tx = optional + option_scid_alias = optional + // enable zeroconf + option_zeroconf = optional + } + } +] +``` + +Note that, as funder, Eclair will happily use an unconfirmed channel if the peer sends an early `channel_ready`, even if +the `option_zeroconf` feature isn't enabled, as long as the peer provides a channel alias. + +##### Enabling through channel type + +You can enable `option_scid_alias` and `option_zeroconf` features by requesting them in the channel type, even if those +options aren't enabled in your features. + +The new channel types variations are: + +- `anchor_outputs_zero_fee_htlc_tx+scid_alias` +- `anchor_outputs_zero_fee_htlc_tx+zeroconf` +- `anchor_outputs_zero_fee_htlc_tx+scid_alias+zeroconf` + +Examples using the command-line interface: + +- open a public zeroconf channel: + +```shell +$ ./eclair-cli open --nodeId=03864e... --fundingSatoshis=100000 --channelType=anchor_outputs_zero_fee_htlc_tx+zeroconf --announceChannel=true +``` + +- open a private zeroconf channel with aliases: + +```shell +$ ./eclair-cli open --nodeId=03864e... --fundingSatoshis=100000 --channelType=anchor_outputs_zero_fee_htlc_tx+scid_alias+zeroconf --announceChannel=false +``` + +### Remove support for Tor v2 + +Dropped support for version 2 of Tor protocol. That means: - Eclair can't open control connection to Tor daemon version 0.3.3.5 and earlier anymore - Eclair can't create hidden services for Tor protocol v2 with newer versions of Tor daemon -IMPORTANT: You'll need to upgrade your Tor daemon if for some reason you still use Tor v0.3.3.5 or earlier before upgrading to this release. +IMPORTANT: You'll need to upgrade your Tor daemon if for some reason you still use Tor v0.3.3.5 or earlier before +upgrading to this release. ### API changes @@ -20,25 +96,33 @@ IMPORTANT: You'll need to upgrade your Tor daemon if for some reason you still u #### Delay enforcement of new channel fees -When updating the relay fees for a channel, eclair can now continue accepting to relay payments using the old fee even if they would be rejected with the new fee. -By default, eclair will still accept the old fee for 10 minutes, you can change it by setting `eclair.relay.fees.enforcement-delay` to a different value. +When updating the relay fees for a channel, eclair can now continue accepting to relay payments using the old fee even +if they would be rejected with the new fee. +By default, eclair will still accept the old fee for 10 minutes, you can change it by +setting `eclair.relay.fees.enforcement-delay` to a different value. -If you want a specific fee update to ignore this delay, you can update the fee twice to make eclair forget about the previous fee. +If you want a specific fee update to ignore this delay, you can update the fee twice to make eclair forget about the +previous fee. #### New minimum funding setting for private channels -New settings have been added to independently control the minimum funding required to open public and private channels to your node. +New settings have been added to independently control the minimum funding required to open public and private channels +to your node. -The `eclair.channel.min-funding-satoshis` setting has been deprecated and replaced with the following two new settings and defaults: +The `eclair.channel.min-funding-satoshis` setting has been deprecated and replaced with the following two new settings +and defaults: * `eclair.channel.min-public-funding-satoshis = 100000` * `eclair.channel.min-private-funding-satoshis = 100000` -If your configuration file changes `eclair.channel.min-funding-satoshis` then you should replace it with both of these new settings. +If your configuration file changes `eclair.channel.min-funding-satoshis` then you should replace it with both of these +new settings. #### Expired incoming invoices now purged if unpaid -Expired incoming invoices that are unpaid will be searched for and purged from the database when Eclair starts up. Thereafter searches for expired unpaid invoices to purge will run once every 24 hours. You can disable this feature, or change the search interval with two new settings: +Expired incoming invoices that are unpaid will be searched for and purged from the database when Eclair starts up. +Thereafter searches for expired unpaid invoices to purge will run once every 24 hours. You can disable this feature, or +change the search interval with two new settings: * `eclair.purge-expired-invoices.enabled = true * `eclair.purge-expired-invoices.interval = 24 hours` @@ -77,13 +161,16 @@ Use the following command to generate the eclair-node package: mvn clean install -DskipTests ``` -That should generate `eclair-node/target/eclair-node--XXXXXXX-bin.zip` with sha256 checksums that match the one we provide and sign in `SHA256SUMS.asc` +That should generate `eclair-node/target/eclair-node--XXXXXXX-bin.zip` with sha256 checksums that match the one +we provide and sign in `SHA256SUMS.asc` -(*) You may be able to build the exact same artefacts with other operating systems or versions of JDK 11, we have not tried everything. +(*) You may be able to build the exact same artefacts with other operating systems or versions of JDK 11, we have not +tried everything. ## Upgrading -This release is fully compatible with previous eclair versions. You don't need to close your channels, just stop eclair, upgrade and restart. +This release is fully compatible with previous eclair versions. You don't need to close your channels, just stop eclair, +upgrade and restart. ## Changelog diff --git a/eclair-core/src/main/resources/reference.conf b/eclair-core/src/main/resources/reference.conf index c44e058c0c..1ee6a20b28 100644 --- a/eclair-core/src/main/resources/reference.conf +++ b/eclair-core/src/main/resources/reference.conf @@ -60,9 +60,13 @@ eclair { option_dual_fund = disabled option_onion_messages = optional option_channel_type = optional + option_scid_alias = optional option_payment_metadata = optional - trampoline_payment_prototype = disabled + // By enabling option_zeroconf, you will be trusting your peers as fundee. You will lose funds if they double spend + // their funding tx. + option_zeroconf = disabled keysend = disabled + trampoline_payment_prototype = disabled } override-init-features = [ // optional per-node features # { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/BlockHeight.scala b/eclair-core/src/main/scala/fr/acinq/eclair/BlockHeight.scala index f00c70f40b..0a22160df0 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/BlockHeight.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/BlockHeight.scala @@ -36,6 +36,8 @@ case class BlockHeight(private val underlying: Long) extends Ordered[BlockHeight def toInt: Int = underlying.toInt def toLong: Long = underlying def toDouble: Double = underlying.toDouble + + override def toString() = underlying.toString // @formatter:on } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala index 0d6be5469e..d162fe7890 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala @@ -182,14 +182,18 @@ class EclairImpl(appKit: Kit) extends Eclair with Logging { override def open(nodeId: PublicKey, fundingAmount: Satoshi, pushAmount_opt: Option[MilliSatoshi], channelType_opt: Option[SupportedChannelType], fundingFeeratePerByte_opt: Option[FeeratePerByte], announceChannel_opt: Option[Boolean], openTimeout_opt: Option[Timeout])(implicit timeout: Timeout): Future[ChannelOpenResponse] = { // we want the open timeout to expire *before* the default ask timeout, otherwise user will get a generic response val openTimeout = openTimeout_opt.getOrElse(Timeout(20 seconds)) - (appKit.switchboard ? Peer.OpenChannel( - remoteNodeId = nodeId, - fundingSatoshis = fundingAmount, - pushMsat = pushAmount_opt.getOrElse(0 msat), - channelType_opt = channelType_opt, - fundingTxFeeratePerKw_opt = fundingFeeratePerByte_opt.map(FeeratePerKw(_)), - channelFlags = announceChannel_opt.map(announceChannel => ChannelFlags(announceChannel = announceChannel)), - timeout_opt = Some(openTimeout))).mapTo[ChannelOpenResponse] + for { + _ <- Future.successful(0) + open = Peer.OpenChannel( + remoteNodeId = nodeId, + fundingSatoshis = fundingAmount, + pushMsat = pushAmount_opt.getOrElse(0 msat), + channelType_opt = channelType_opt, + fundingTxFeeratePerKw_opt = fundingFeeratePerByte_opt.map(FeeratePerKw(_)), + channelFlags = announceChannel_opt.map(announceChannel => ChannelFlags(announceChannel = announceChannel)), + timeout_opt = Some(openTimeout)) + res <- (appKit.switchboard ? open).mapTo[ChannelOpenResponse] + } yield res } override def close(channels: List[ApiTypes.ChannelIdentifier], scriptPubKey_opt: Option[ByteVector], closingFeerates_opt: Option[ClosingFeerates])(implicit timeout: Timeout): Future[Map[ApiTypes.ChannelIdentifier, Either[Throwable, CommandResponse[CMD_CLOSE]]]] = { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala index bb5e5575c9..f6816f4cbe 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala @@ -240,11 +240,21 @@ object Features { val mandatory = 44 } + case object ScidAlias extends Feature with InitFeature with NodeFeature with ChannelTypeFeature { + val rfcName = "option_scid_alias" + val mandatory = 46 + } + case object PaymentMetadata extends Feature with InvoiceFeature { val rfcName = "option_payment_metadata" val mandatory = 48 } + case object ZeroConf extends Feature with InitFeature with NodeFeature with ChannelTypeFeature { + val rfcName = "option_zeroconf" + val mandatory = 50 + } + case object KeySend extends Feature with NodeFeature { val rfcName = "keysend" val mandatory = 54 @@ -278,9 +288,11 @@ object Features { DualFunding, OnionMessages, ChannelType, + ScidAlias, PaymentMetadata, - TrampolinePaymentPrototype, - KeySend + ZeroConf, + KeySend, + TrampolinePaymentPrototype ) // Features may depend on other features, as specified in Bolt 9. diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/ShortChannelId.scala b/eclair-core/src/main/scala/fr/acinq/eclair/ShortChannelId.scala index 44fc67ff6c..2265f220b7 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/ShortChannelId.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/ShortChannelId.scala @@ -16,46 +16,71 @@ package fr.acinq.eclair -/** - * A short channel id uniquely identifies a channel by the coordinates of its funding tx output in the blockchain. - * See BOLT 7: https://github.com/lightningnetwork/lightning-rfc/blob/master/07-routing-gossip.md#requirements - */ -case class ShortChannelId(private val id: Long) extends Ordered[ShortChannelId] { - - def toLong: Long = id - - def blockHeight = ShortChannelId.blockHeight(this) +import fr.acinq.eclair.ShortChannelId.toShortId - override def toString: String = { +// @formatter:off +sealed trait ShortChannelId extends Ordered[ShortChannelId] { + def toLong: Long + // we use an unsigned long comparison here + override def compare(that: ShortChannelId): Int = (this.toLong + Long.MinValue).compareTo(that.toLong + Long.MinValue) + override def hashCode(): Int = toLong.hashCode() + override def equals(obj: Any): Boolean = obj match { + case scid: ShortChannelId => this.toLong.equals(scid.toLong) + case _ => false + } + def toCoordinatesString: String = { val TxCoordinates(blockHeight, txIndex, outputIndex) = ShortChannelId.coordinates(this) s"${blockHeight.toLong}x${txIndex}x$outputIndex" } - - // we use an unsigned long comparison here - override def compare(that: ShortChannelId): Int = (this.id + Long.MinValue).compareTo(that.id + Long.MinValue) + def toHex: String = s"0x${toLong.toHexString}" +} +/** Sometimes we don't know what a scid really is */ +case class UnspecifiedShortChannelId(private val id: Long) extends ShortChannelId { + override def toLong: Long = id + override def toString: String = toCoordinatesString // for backwards compatibility, because ChannelUpdate have an unspecified scid } +case class RealShortChannelId private(private val id: Long) extends ShortChannelId { + override def toLong: Long = id + override def toString: String = toCoordinatesString + def blockHeight: BlockHeight = ShortChannelId.blockHeight(this) + def outputIndex: Int = ShortChannelId.outputIndex(this) +} +case class Alias(private val id: Long) extends ShortChannelId { + override def toLong: Long = id + override def toString: String = toHex +} +// @formatter:on object ShortChannelId { - def apply(s: String): ShortChannelId = s.split("x").toList match { - case blockHeight :: txIndex :: outputIndex :: Nil => ShortChannelId(toShortId(blockHeight.toInt, txIndex.toInt, outputIndex.toInt)) + case blockHeight :: txIndex :: outputIndex :: Nil => UnspecifiedShortChannelId(toShortId(blockHeight.toInt, txIndex.toInt, outputIndex.toInt)) case _ => throw new IllegalArgumentException(s"Invalid short channel id: $s") } - def apply(blockHeight: BlockHeight, txIndex: Int, outputIndex: Int): ShortChannelId = ShortChannelId(toShortId(blockHeight.toInt, txIndex, outputIndex)) + def apply(l: Long): ShortChannelId = UnspecifiedShortChannelId(l) def toShortId(blockHeight: Int, txIndex: Int, outputIndex: Int): Long = ((blockHeight & 0xFFFFFFL) << 40) | ((txIndex & 0xFFFFFFL) << 16) | (outputIndex & 0xFFFFL) + def generateLocalAlias(): Alias = Alias(System.nanoTime()) // TODO: fixme (duplicate, etc.) + @inline - def blockHeight(shortChannelId: ShortChannelId): BlockHeight = BlockHeight((shortChannelId.id >> 40) & 0xFFFFFF) + def blockHeight(shortChannelId: ShortChannelId): BlockHeight = BlockHeight((shortChannelId.toLong >> 40) & 0xFFFFFF) @inline - def txIndex(shortChannelId: ShortChannelId): Int = ((shortChannelId.id >> 16) & 0xFFFFFF).toInt + def txIndex(shortChannelId: ShortChannelId): Int = ((shortChannelId.toLong >> 16) & 0xFFFFFF).toInt @inline - def outputIndex(shortChannelId: ShortChannelId): Int = (shortChannelId.id & 0xFFFF).toInt + def outputIndex(shortChannelId: ShortChannelId): Int = (shortChannelId.toLong & 0xFFFF).toInt def coordinates(shortChannelId: ShortChannelId): TxCoordinates = TxCoordinates(blockHeight(shortChannelId), txIndex(shortChannelId), outputIndex(shortChannelId)) } +/** + * A real short channel id uniquely identifies a channel by the coordinates of its funding tx output in the blockchain. + * See BOLT 7: https://github.com/lightningnetwork/lightning-rfc/blob/master/07-routing-gossip.md#requirements + */ +object RealShortChannelId { + def apply(blockHeight: BlockHeight, txIndex: Int, outputIndex: Int): RealShortChannelId = RealShortChannelId(toShortId(blockHeight.toInt, txIndex, outputIndex)) +} + case class TxCoordinates(blockHeight: BlockHeight, txIndex: Int, outputIndex: Int) \ No newline at end of file diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/balance/BalanceActor.scala b/eclair-core/src/main/scala/fr/acinq/eclair/balance/BalanceActor.scala index 966ae3024d..3924d70f34 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/balance/BalanceActor.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/balance/BalanceActor.scala @@ -105,7 +105,7 @@ private class BalanceActor(context: ActorContext[Command], Metrics.GlobalBalanceDetailed.withTag(Tags.BalanceType, Tags.BalanceTypes.OnchainConfirmed).update(result.onChain.confirmed.toMilliBtc.toDouble) Metrics.GlobalBalanceDetailed.withTag(Tags.BalanceType, Tags.BalanceTypes.OnchainUnconfirmed).update(result.onChain.unconfirmed.toMilliBtc.toDouble) Metrics.GlobalBalanceDetailed.withTag(Tags.BalanceType, Tags.BalanceTypes.Offchain).withTag(Tags.OffchainState, Tags.OffchainStates.waitForFundingConfirmed).update(result.offChain.waitForFundingConfirmed.toMilliBtc.toDouble) - Metrics.GlobalBalanceDetailed.withTag(Tags.BalanceType, Tags.BalanceTypes.Offchain).withTag(Tags.OffchainState, Tags.OffchainStates.waitForFundingLocked).update(result.offChain.waitForFundingLocked.toMilliBtc.toDouble) + Metrics.GlobalBalanceDetailed.withTag(Tags.BalanceType, Tags.BalanceTypes.Offchain).withTag(Tags.OffchainState, Tags.OffchainStates.waitForChannelReady).update(result.offChain.waitForChannelReady.toMilliBtc.toDouble) Metrics.GlobalBalanceDetailed.withTag(Tags.BalanceType, Tags.BalanceTypes.Offchain).withTag(Tags.OffchainState, Tags.OffchainStates.normal).update(result.offChain.normal.total.toMilliBtc.toDouble) Metrics.GlobalBalanceDetailed.withTag(Tags.BalanceType, Tags.BalanceTypes.Offchain).withTag(Tags.OffchainState, Tags.OffchainStates.shutdown).update(result.offChain.shutdown.total.toMilliBtc.toDouble) Metrics.GlobalBalanceDetailed.withTag(Tags.BalanceType, Tags.BalanceTypes.Offchain).withTag(Tags.OffchainState, Tags.OffchainStates.closingLocal).update(result.offChain.closing.localCloseBalance.total.toMilliBtc.toDouble) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/balance/CheckBalance.scala b/eclair-core/src/main/scala/fr/acinq/eclair/balance/CheckBalance.scala index 8dd65b359c..7d08a54d09 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/balance/CheckBalance.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/balance/CheckBalance.scala @@ -77,13 +77,13 @@ object CheckBalance { * The overall balance among all channels in all states. */ case class OffChainBalance(waitForFundingConfirmed: Btc = 0.sat, - waitForFundingLocked: Btc = 0.sat, + waitForChannelReady: Btc = 0.sat, normal: MainAndHtlcBalance = MainAndHtlcBalance(), shutdown: MainAndHtlcBalance = MainAndHtlcBalance(), negotiating: Btc = 0.sat, closing: ClosingBalance = ClosingBalance(), waitForPublishFutureCommitment: Btc = 0.sat) { - val total: Btc = waitForFundingConfirmed + waitForFundingLocked + normal.total + shutdown.total + negotiating + closing.total + waitForPublishFutureCommitment + val total: Btc = waitForFundingConfirmed + waitForChannelReady + normal.total + shutdown.total + negotiating + closing.total + waitForPublishFutureCommitment } def updateMainBalance(localCommit: LocalCommit): Btc => Btc = { v: Btc => @@ -201,7 +201,7 @@ object CheckBalance { channels .foldLeft(OffChainBalance()) { case (r, d: DATA_WAIT_FOR_FUNDING_CONFIRMED) => r.modify(_.waitForFundingConfirmed).using(updateMainBalance(d.commitments.localCommit)) - case (r, d: DATA_WAIT_FOR_FUNDING_LOCKED) => r.modify(_.waitForFundingLocked).using(updateMainBalance(d.commitments.localCommit)) + case (r, d: DATA_WAIT_FOR_CHANNEL_READY) => r.modify(_.waitForChannelReady).using(updateMainBalance(d.commitments.localCommit)) case (r, d: DATA_NORMAL) => r.modify(_.normal).using(updateMainAndHtlcBalance(d.commitments, knownPreimages)) case (r, d: DATA_SHUTDOWN) => r.modify(_.shutdown).using(updateMainAndHtlcBalance(d.commitments, knownPreimages)) case (r, d: DATA_NEGOTIATING) => r.modify(_.negotiating).using(updateMainBalance(d.commitments.localCommit)) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/balance/Monitoring.scala b/eclair-core/src/main/scala/fr/acinq/eclair/balance/Monitoring.scala index cd672f27a4..0fb652c107 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/balance/Monitoring.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/balance/Monitoring.scala @@ -44,7 +44,7 @@ object Monitoring { object OffchainStates { val waitForFundingConfirmed = "waitForFundingConfirmed" - val waitForFundingLocked = "waitForFundingLocked" + val waitForChannelReady = "waitForChannelReady" val normal = "normal" val shutdown = "shutdown" val negotiating = "negotiating" diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/ZmqWatcher.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/ZmqWatcher.scala index 536f205539..c9331c651a 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/ZmqWatcher.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/ZmqWatcher.scala @@ -25,7 +25,7 @@ import fr.acinq.eclair.blockchain._ import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinCoreClient import fr.acinq.eclair.blockchain.watchdogs.BlockchainWatchdog import fr.acinq.eclair.wire.protocol.ChannelAnnouncement -import fr.acinq.eclair.{BlockHeight, KamonExt, NodeParams, ShortChannelId, TimestampSecond} +import fr.acinq.eclair.{BlockHeight, KamonExt, NodeParams, RealShortChannelId, TimestampSecond} import java.util.concurrent.atomic.AtomicLong import scala.concurrent.duration._ @@ -136,8 +136,8 @@ object ZmqWatcher { /** This event is sent when a [[WatchSpentBasic]] condition is met. */ sealed trait WatchSpentBasicTriggered extends WatchTriggered - case class WatchExternalChannelSpent(replyTo: ActorRef[WatchExternalChannelSpentTriggered], txId: ByteVector32, outputIndex: Int, shortChannelId: ShortChannelId) extends WatchSpentBasic[WatchExternalChannelSpentTriggered] - case class WatchExternalChannelSpentTriggered(shortChannelId: ShortChannelId) extends WatchSpentBasicTriggered + case class WatchExternalChannelSpent(replyTo: ActorRef[WatchExternalChannelSpentTriggered], txId: ByteVector32, outputIndex: Int, shortChannelId: RealShortChannelId) extends WatchSpentBasic[WatchExternalChannelSpentTriggered] + case class WatchExternalChannelSpentTriggered(shortChannelId: RealShortChannelId) extends WatchSpentBasicTriggered case class WatchFundingSpent(replyTo: ActorRef[WatchFundingSpentTriggered], txId: ByteVector32, outputIndex: Int, hints: Set[ByteVector32]) extends WatchSpent[WatchFundingSpentTriggered] case class WatchFundingSpentTriggered(spendingTx: Transaction) extends WatchSpentTriggered diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/FeeEstimator.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/FeeEstimator.scala index 07d58d7c12..a01b2fcf75 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/FeeEstimator.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/FeeEstimator.scala @@ -49,7 +49,7 @@ case class FeerateTolerance(ratioLow: Double, ratioHigh: Double, anchorOutputMax channelType match { case ChannelTypes.Standard | ChannelTypes.StaticRemoteKey => proposedFeerate < networkFeerate * ratioLow || networkFeerate * ratioHigh < proposedFeerate - case ChannelTypes.AnchorOutputs | ChannelTypes.AnchorOutputsZeroFeeHtlcTx => + case ChannelTypes.AnchorOutputs | _: ChannelTypes.AnchorOutputsZeroFeeHtlcTx => // when using anchor outputs, we allow any feerate: fees will be set with CPFP and RBF at broadcast time false } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/watchdogs/BlockchainWatchdog.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/watchdogs/BlockchainWatchdog.scala index 9039d2375e..8dd8488076 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/watchdogs/BlockchainWatchdog.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/watchdogs/BlockchainWatchdog.scala @@ -104,10 +104,10 @@ object BlockchainWatchdog { case CheckLatestHeaders(blockHeight) => val id = UUID.randomUUID() if (headersOverDnsEnabled) { - context.spawn(HeadersOverDns(nodeParams.chainHash, blockHeight), s"${HeadersOverDns.Source}-${blockHeight.toLong}-$id") ! HeadersOverDns.CheckLatestHeaders(context.self) + context.spawn(HeadersOverDns(nodeParams.chainHash, blockHeight), s"${HeadersOverDns.Source}-${blockHeight}-$id") ! HeadersOverDns.CheckLatestHeaders(context.self) } explorers.foreach { explorer => - context.spawn(ExplorerApi(nodeParams.chainHash, blockHeight, explorer), s"${explorer.name}-${blockHeight.toLong}-$id") ! ExplorerApi.CheckLatestHeaders(context.self) + context.spawn(ExplorerApi(nodeParams.chainHash, blockHeight, explorer), s"${explorer.name}-${blockHeight}-$id") ! ExplorerApi.CheckLatestHeaders(context.self) } Behaviors.same case headers@LatestHeaders(blockHeight, blockHeaders, source) => diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala index b77a4c1797..538f69d9ac 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala @@ -18,13 +18,13 @@ package fr.acinq.eclair.channel import akka.actor.{ActorRef, PossiblyHarmful} import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey -import fr.acinq.bitcoin.scalacompat.{ByteVector32, DeterministicWallet, OutPoint, Satoshi, SatoshiLong, Transaction} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, DeterministicWallet, OutPoint, Satoshi, Transaction} import fr.acinq.eclair.blockchain.fee.FeeratePerKw import fr.acinq.eclair.payment.OutgoingPaymentPacket.Upstream import fr.acinq.eclair.transactions.CommitmentSpec import fr.acinq.eclair.transactions.Transactions._ -import fr.acinq.eclair.wire.protocol.{AcceptChannel, ChannelAnnouncement, ChannelReestablish, ChannelUpdate, ClosingSigned, FailureMessage, FundingCreated, FundingLocked, FundingSigned, Init, OnionRoutingPacket, OpenChannel, Shutdown, UpdateAddHtlc, UpdateFailHtlc, UpdateFailMalformedHtlc, UpdateFulfillHtlc} -import fr.acinq.eclair.{BlockHeight, CltvExpiry, CltvExpiryDelta, Features, InitFeature, MilliSatoshi, ShortChannelId, UInt64} +import fr.acinq.eclair.wire.protocol.{AcceptChannel, ChannelAnnouncement, ChannelReady, ChannelReestablish, ChannelUpdate, ClosingSigned, FailureMessage, FundingCreated, FundingSigned, Init, OnionRoutingPacket, OpenChannel, Shutdown, UpdateAddHtlc, UpdateFailHtlc, UpdateFailMalformedHtlc, UpdateFulfillHtlc} +import fr.acinq.eclair.{BlockHeight, CltvExpiry, CltvExpiryDelta, Features, InitFeature, Alias, MilliSatoshi, RealShortChannelId, ShortChannelId, UInt64} import scodec.bits.ByteVector import java.util.UUID @@ -53,7 +53,7 @@ case object WAIT_FOR_FUNDING_INTERNAL extends ChannelState case object WAIT_FOR_FUNDING_CREATED extends ChannelState case object WAIT_FOR_FUNDING_SIGNED extends ChannelState case object WAIT_FOR_FUNDING_CONFIRMED extends ChannelState -case object WAIT_FOR_FUNDING_LOCKED extends ChannelState +case object WAIT_FOR_CHANNEL_READY extends ChannelState case object NORMAL extends ChannelState case object SHUTDOWN extends ChannelState case object NEGOTIATING extends ChannelState @@ -85,7 +85,9 @@ case class INPUT_INIT_FUNDER(temporaryChannelId: ByteVector32, remoteInit: Init, channelFlags: ChannelFlags, channelConfig: ChannelConfig, - channelType: SupportedChannelType) + channelType: SupportedChannelType) { + require(!(channelType.features.contains(Features.ScidAlias) && channelFlags.announceChannel), "option_scid_alias is not compatible with public channels") +} case class INPUT_INIT_FUNDEE(temporaryChannelId: ByteVector32, localParams: LocalParams, remote: ActorRef, @@ -422,12 +424,37 @@ final case class DATA_WAIT_FOR_FUNDING_SIGNED(channelId: ByteVector32, final case class DATA_WAIT_FOR_FUNDING_CONFIRMED(commitments: Commitments, fundingTx: Option[Transaction], waitingSince: BlockHeight, // how long have we been waiting for the funding tx to confirm - deferred: Option[FundingLocked], + deferred: Option[ChannelReady], lastSent: Either[FundingCreated, FundingSigned]) extends PersistentChannelData -final case class DATA_WAIT_FOR_FUNDING_LOCKED(commitments: Commitments, shortChannelId: ShortChannelId, lastSent: FundingLocked) extends PersistentChannelData +final case class DATA_WAIT_FOR_CHANNEL_READY(commitments: Commitments, + shortIds: ShortIds, + lastSent: ChannelReady) extends PersistentChannelData + +sealed trait RealScidStatus { def toOption: Option[RealShortChannelId] } +object RealScidStatus { + /** The funding transaction has been confirmed but hasn't reached min_depth, we must be ready for a reorg. */ + case class Temporary(realScid: RealShortChannelId) extends RealScidStatus { override def toOption: Option[RealShortChannelId] = Some(realScid) } + /** The funding transaction has been deeply confirmed. */ + case class Final(realScid: RealShortChannelId) extends RealScidStatus { override def toOption: Option[RealShortChannelId] = Some(realScid) } + /** We don't know the status of the funding transaction. */ + case object Unknown extends RealScidStatus { override def toOption: Option[RealShortChannelId] = None } +} + +/** + * Short identifiers for the channel + * + * @param real the real scid, it may change if a reorg happens before the channel reaches 6 conf + * @param localAlias we must remember the alias that we sent to our peer because we use it to: + * - identify incoming [[ChannelUpdate]] at the connection level + * - route outgoing payments to that channel + * @param remoteAlias_opt we only remember the last alias received from our peer, we use this to generate + * routing hints in [[fr.acinq.eclair.payment.Bolt11Invoice]] + */ +case class ShortIds(real: RealScidStatus, + localAlias: Alias, + remoteAlias_opt: Option[Alias]) final case class DATA_NORMAL(commitments: Commitments, - shortChannelId: ShortChannelId, - buried: Boolean, + shortIds: ShortIds, channelAnnouncement: Option[ChannelAnnouncement], channelUpdate: ChannelUpdate, localShutdown: Option[Shutdown], diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelEvents.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelEvents.scala index 236acffead..b41cffaa9c 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelEvents.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelEvents.scala @@ -19,10 +19,10 @@ package fr.acinq.eclair.channel import akka.actor.ActorRef import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.bitcoin.scalacompat.{ByteVector32, Satoshi, Transaction} -import fr.acinq.eclair.{BlockHeight, ShortChannelId} import fr.acinq.eclair.blockchain.fee.FeeratePerKw import fr.acinq.eclair.channel.Helpers.Closing.ClosingType import fr.acinq.eclair.wire.protocol.{ChannelAnnouncement, ChannelUpdate} +import fr.acinq.eclair.{BlockHeight, Features, ShortChannelId} /** * Created by PM on 17/08/2016. @@ -44,13 +44,32 @@ case class ChannelRestored(channel: ActorRef, channelId: ByteVector32, peer: Act case class ChannelIdAssigned(channel: ActorRef, remoteNodeId: PublicKey, temporaryChannelId: ByteVector32, channelId: ByteVector32) extends ChannelEvent -case class ShortChannelIdAssigned(channel: ActorRef, channelId: ByteVector32, shortChannelId: ShortChannelId, previousShortChannelId: Option[ShortChannelId]) extends ChannelEvent - -case class LocalChannelUpdate(channel: ActorRef, channelId: ByteVector32, shortChannelId: ShortChannelId, remoteNodeId: PublicKey, channelAnnouncement_opt: Option[ChannelAnnouncement], channelUpdate: ChannelUpdate, commitments: AbstractCommitments) extends ChannelEvent +/** + * This event will be sent whenever a new scid is assigned to the channel, be it a real, local alias or remote alias. + */ +case class ShortChannelIdAssigned(channel: ActorRef, channelId: ByteVector32, shortIds: ShortIds, remoteNodeId: PublicKey) extends ChannelEvent + +case class LocalChannelUpdate(channel: ActorRef, channelId: ByteVector32, shortIds: ShortIds, remoteNodeId: PublicKey, channelAnnouncement_opt: Option[ChannelAnnouncement], channelUpdate: ChannelUpdate, commitments: AbstractCommitments) extends ChannelEvent { + /** + * We always include the local alias because we must always be able to route based on it. + * However we only include the real scid if option_scid_alias is disabled, because we otherwise want to hide it. + */ + def scidsForRouting: Seq[ShortChannelId] = { + val canUseRealScid = commitments match { + case c: Commitments => !c.channelFeatures.hasFeature(Features.ScidAlias) + case _ => false + } + if (canUseRealScid) { + shortIds.real.toOption.toSeq :+ shortIds.localAlias + } else { + Seq(shortIds.localAlias) + } + } +} -case class ChannelUpdateParametersChanged(channel: ActorRef, channelId: ByteVector32, shortChannelId: ShortChannelId, remoteNodeId: PublicKey, channelUpdate: ChannelUpdate) extends ChannelEvent +case class ChannelUpdateParametersChanged(channel: ActorRef, channelId: ByteVector32, remoteNodeId: PublicKey, channelUpdate: ChannelUpdate) extends ChannelEvent -case class LocalChannelDown(channel: ActorRef, channelId: ByteVector32, shortChannelId: ShortChannelId, remoteNodeId: PublicKey) extends ChannelEvent +case class LocalChannelDown(channel: ActorRef, channelId: ByteVector32, shortIds: ShortIds, remoteNodeId: PublicKey) extends ChannelEvent case class ChannelStateChanged(channel: ActorRef, channelId: ByteVector32, peer: ActorRef, remoteNodeId: PublicKey, previousState: ChannelState, currentState: ChannelState, commitments_opt: Option[AbstractCommitments]) extends ChannelEvent @@ -66,7 +85,7 @@ case class TransactionPublished(channelId: ByteVector32, remoteNodeId: PublicKey case class TransactionConfirmed(channelId: ByteVector32, remoteNodeId: PublicKey, tx: Transaction) extends ChannelEvent // NB: this event is only sent when the channel is available. -case class AvailableBalanceChanged(channel: ActorRef, channelId: ByteVector32, shortChannelId: ShortChannelId, commitments: AbstractCommitments) extends ChannelEvent +case class AvailableBalanceChanged(channel: ActorRef, channelId: ByteVector32, shortIds: ShortIds, commitments: AbstractCommitments) extends ChannelEvent case class ChannelPersisted(channel: ActorRef, remoteNodeId: PublicKey, channelId: ByteVector32, data: PersistentChannelData) extends ChannelEvent diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelFeatures.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelFeatures.scala index 5ef17a4a11..7bd79bb98e 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelFeatures.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelFeatures.scala @@ -17,6 +17,7 @@ package fr.acinq.eclair.channel import fr.acinq.eclair.transactions.Transactions.{CommitmentFormat, DefaultCommitmentFormat, UnsafeLegacyAnchorOutputsCommitmentFormat, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat} +import fr.acinq.eclair.{Feature, FeatureSupport, Features, InitFeature} import fr.acinq.eclair.{ChannelTypeFeature, FeatureSupport, Features, InitFeature, PermanentChannelFeature} /** @@ -32,7 +33,7 @@ case class ChannelFeatures(features: Set[PermanentChannelFeature]) { val channelType: SupportedChannelType = { if (hasFeature(Features.AnchorOutputsZeroFeeHtlcTx)) { - ChannelTypes.AnchorOutputsZeroFeeHtlcTx + ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = features.contains(Features.ScidAlias), zeroConf = features.contains(Features.ZeroConf)) } else if (hasFeature(Features.AnchorOutputs)) { ChannelTypes.AnchorOutputs } else if (hasFeature(Features.StaticRemoteKey)) { @@ -105,11 +106,16 @@ object ChannelTypes { override def commitmentFormat: CommitmentFormat = UnsafeLegacyAnchorOutputsCommitmentFormat override def toString: String = "anchor_outputs" } - case object AnchorOutputsZeroFeeHtlcTx extends SupportedChannelType { - override def features: Set[ChannelTypeFeature] = Set(Features.StaticRemoteKey, Features.AnchorOutputsZeroFeeHtlcTx) + case class AnchorOutputsZeroFeeHtlcTx(scidAlias: Boolean, zeroConf: Boolean) extends SupportedChannelType { + override def features: Set[ChannelTypeFeature] = Set( + if (scidAlias) Some(Features.ScidAlias) else None, + if (zeroConf) Some(Features.ZeroConf) else None, + Some(Features.StaticRemoteKey), + Some(Features.AnchorOutputsZeroFeeHtlcTx) + ).flatten override def paysDirectlyToWallet: Boolean = false override def commitmentFormat: CommitmentFormat = ZeroFeeHtlcTxAnchorOutputsCommitmentFormat - override def toString: String = "anchor_outputs_zero_fee_htlc_tx" + override def toString: String = s"anchor_outputs_zero_fee_htlc_tx${if (scidAlias) "+scid_alias" else ""}${if (zeroConf) "+zeroconf" else ""}" } case class UnsupportedChannelType(featureBits: Features[InitFeature]) extends ChannelType { override def features: Set[InitFeature] = featureBits.activated.keySet @@ -117,7 +123,14 @@ object ChannelTypes { } // @formatter:on - private val features2ChannelType: Map[Features[_ <: InitFeature], SupportedChannelType] = Set(Standard, StaticRemoteKey, AnchorOutputs, AnchorOutputsZeroFeeHtlcTx) + private val features2ChannelType: Map[Features[_ <: InitFeature], SupportedChannelType] = Set( + Standard, + StaticRemoteKey, + AnchorOutputs, + AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false), + AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = true), + AnchorOutputsZeroFeeHtlcTx(scidAlias = true, zeroConf = false), + AnchorOutputsZeroFeeHtlcTx(scidAlias = true, zeroConf = true)) .map(channelType => Features(channelType.features.map(_ -> FeatureSupport.Mandatory).toMap) -> channelType) .toMap @@ -125,12 +138,14 @@ object ChannelTypes { def fromFeatures(features: Features[InitFeature]): ChannelType = features2ChannelType.getOrElse(features, UnsupportedChannelType(features)) /** Pick the channel type based on local and remote feature bits, as defined by the spec. */ - def defaultFromFeatures(localFeatures: Features[InitFeature], remoteFeatures: Features[InitFeature]): SupportedChannelType = { - if (Features.canUseFeature(localFeatures, remoteFeatures, Features.AnchorOutputsZeroFeeHtlcTx)) { - AnchorOutputsZeroFeeHtlcTx - } else if (Features.canUseFeature(localFeatures, remoteFeatures, Features.AnchorOutputs)) { + def defaultFromFeatures(localFeatures: Features[InitFeature], remoteFeatures: Features[InitFeature], announceChannel: Boolean): SupportedChannelType = { + def canUse(feature: InitFeature) = Features.canUseFeature(localFeatures, remoteFeatures, feature) + + if (canUse(Features.AnchorOutputsZeroFeeHtlcTx)) { + AnchorOutputsZeroFeeHtlcTx(scidAlias = canUse(Features.ScidAlias) && !announceChannel, zeroConf = canUse(Features.ZeroConf)) // alias feature is incompatible with public channel + } else if (canUse(Features.AnchorOutputs)) { AnchorOutputs - } else if (Features.canUseFeature(localFeatures, remoteFeatures, Features.StaticRemoteKey)) { + } else if (canUse(Features.StaticRemoteKey)) { StaticRemoteKey } else { Standard diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala index 00d0b26989..2294a09a6c 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala @@ -915,7 +915,7 @@ object Commitments { case _: CommitSig => s"sig" case _: RevokeAndAck => s"rev" case _: Error => s"err" - case _: FundingLocked => s"funding_locked" + case _: ChannelReady => s"channel_ready" case _ => "???" } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala index bde048e81b..238b9f4a9e 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala @@ -56,7 +56,7 @@ object Helpers { remoteParams = data.commitments.remoteParams.copy(initFeatures = remoteInit.features)) data match { case d: DATA_WAIT_FOR_FUNDING_CONFIRMED => d.copy(commitments = commitments1) - case d: DATA_WAIT_FOR_FUNDING_LOCKED => d.copy(commitments = commitments1) + case d: DATA_WAIT_FOR_CHANNEL_READY => d.copy(commitments = commitments1) case d: DATA_NORMAL => d.copy(commitments = commitments1) case d: DATA_SHUTDOWN => d.copy(commitments = commitments1) case d: DATA_NEGOTIATING => d.copy(commitments = commitments1) @@ -65,22 +65,6 @@ object Helpers { } } - /** - * Returns the number of confirmations needed to safely handle the funding transaction, - * we make sure the cumulative block reward largely exceeds the channel size. - * - * @param fundingSatoshis funding amount of the channel - * @return number of confirmations needed - */ - def minDepthForFunding(channelConf: ChannelConf, fundingSatoshis: Satoshi): Long = fundingSatoshis match { - case funding if funding <= Channel.MAX_FUNDING => channelConf.minDepthBlocks - case funding => - val blockReward = 6.25 // this is true as of ~May 2020, but will be too large after 2024 - val scalingFactor = 15 - val blocksToReachFunding = (((scalingFactor * funding.toBtc.toDouble) / blockReward).ceil + 1).toInt - channelConf.minDepthBlocks.max(blocksToReachFunding) - } - def extractShutdownScript(channelId: ByteVector32, localFeatures: Features[InitFeature], remoteFeatures: Features[InitFeature], upfrontShutdownScript_opt: Option[ByteVector]): Either[ChannelException, Option[ByteVector]] = { val canUseUpfrontShutdownScript = Features.canUseFeature(localFeatures, remoteFeatures, Features.UpfrontShutdownScript) val canUseAnySegwit = Features.canUseFeature(localFeatures, remoteFeatures, Features.ShutdownAnySegwit) @@ -164,10 +148,10 @@ object Helpers { case None if Features.canUseFeature(localFeatures, remoteFeatures, Features.ChannelType) => // Bolt 2: if `option_channel_type` is negotiated: MUST set `channel_type` return Left(MissingChannelType(open.temporaryChannelId)) - case None if channelType != ChannelTypes.defaultFromFeatures(localFeatures, remoteFeatures) => + case None if channelType != ChannelTypes.defaultFromFeatures(localFeatures, remoteFeatures, open.channelFlags.announceChannel) => // If we have overridden the default channel type, but they didn't support explicit channel type negotiation, // we need to abort because they expect a different channel type than what we offered. - return Left(InvalidChannelType(open.temporaryChannelId, channelType, ChannelTypes.defaultFromFeatures(localFeatures, remoteFeatures))) + return Left(InvalidChannelType(open.temporaryChannelId, channelType, ChannelTypes.defaultFromFeatures(localFeatures, remoteFeatures, open.channelFlags.announceChannel))) case _ => // we agree on channel type } @@ -201,6 +185,29 @@ object Helpers { extractShutdownScript(accept.temporaryChannelId, localFeatures, remoteFeatures, accept.upfrontShutdownScript_opt).map(script_opt => (channelFeatures, script_opt)) } + /** + * The general rule is that we use remote_alias for our channel_update until the channel is publicly announced, and + * then we use the real scid. + * + * Private channels are handled like public channels that have not yet been announced, there is no special case. + * + * Decision tree: + * - received remote_alias from peer + * - before channel announcement: use remote_alias + * - after channel announcement: use real scid + * - no remote_alias from peer + * - min_depth > 0: use real scid (may change if reorg between min_depth and 6 conf) + * - min_depth = 0 (zero-conf): spec violation, our peer MUST send an alias when using zero-conf + */ + def scidForChannelUpdate(channelAnnouncement_opt: Option[ChannelAnnouncement], shortIds: ShortIds): ShortChannelId = { + channelAnnouncement_opt.map(_.shortChannelId) // we use the real "final" scid when it is publicly announced + .orElse(shortIds.remoteAlias_opt) // otherwise the remote alias + .orElse(shortIds.real.toOption) // if we don't have a remote alias, we use the real scid (which could change because the funding tx possibly has less than 6 confs here) + .getOrElse(throw new RuntimeException("this is a zero-conf channel and no alias was provided in channel_ready")) // if we don't have a real scid, it means this is a zero-conf channel and our peer must have sent an alias + } + + def scidForChannelUpdate(d: DATA_NORMAL): ShortChannelId = scidForChannelUpdate(d.channelAnnouncement, d.shortIds) + /** * Compute the delay until we need to refresh the channel_update for our channel not to be considered stale by * other nodes. @@ -226,7 +233,7 @@ object Helpers { remoteFeeratePerKw < FeeratePerKw.MinimumFeeratePerKw } - def makeAnnouncementSignatures(nodeParams: NodeParams, commitments: Commitments, shortChannelId: ShortChannelId): AnnouncementSignatures = { + def makeAnnouncementSignatures(nodeParams: NodeParams, commitments: Commitments, shortChannelId: RealShortChannelId): AnnouncementSignatures = { val features = Features.empty[Feature] // empty features for now val fundingPubKey = nodeParams.channelKeyManager.fundingPublicKey(commitments.localParams.fundingKeyPath) val witness = Announcements.generateChannelAnnouncementWitness( @@ -278,6 +285,37 @@ object Helpers { object Funding { + /** + * As funder we trust ourselves to not double spend funding txs: we could always use a zero-confirmation watch, + * but we need a scid to send the initial channel_update and remote may not provide an alias. That's why we always + * wait for one conf, except if the channel has the zero-conf feature (because presumably the peer will send an + * alias in that case). + */ + def minDepthFunder(channelFeatures: ChannelFeatures): Option[Long] = { + if (channelFeatures.hasFeature(Features.ZeroConf)) { + None + } else { + Some(1) + } + } + + /** + * Returns the number of confirmations needed to safely handle the funding transaction, + * we make sure the cumulative block reward largely exceeds the channel size. + * + * @param fundingSatoshis funding amount of the channel + * @return number of confirmations needed, if any + */ + def minDepthFundee(channelConf: ChannelConf, channelFeatures: ChannelFeatures, fundingSatoshis: Satoshi): Option[Long] = fundingSatoshis match { + case _ if channelFeatures.hasFeature(Features.ZeroConf) => None // zero-conf stay zero-conf, whatever the funding amount is + case funding if funding <= Channel.MAX_FUNDING => Some(channelConf.minDepthBlocks) + case funding => + val blockReward = 6.25 // this is true as of ~May 2020, but will be too large after 2024 + val scalingFactor = 15 + val blocksToReachFunding = (((scalingFactor * funding.toBtc.toDouble) / blockReward).ceil + 1).toInt + Some(channelConf.minDepthBlocks.max(blocksToReachFunding)) + } + def makeFundingInputInfo(fundingTxId: ByteVector32, fundingTxOutputIndex: Int, fundingSatoshis: Satoshi, fundingPubkey1: PublicKey, fundingPubkey2: PublicKey): InputInfo = { val fundingScript = multiSig2of2(fundingPubkey1, fundingPubkey2) val fundingTxOut = TxOut(fundingSatoshis, pay2wsh(fundingScript)) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Register.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Register.scala index 8e949012dc..7622bd13f1 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Register.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Register.scala @@ -47,13 +47,16 @@ class Register extends Actor with ActorLogging { case ChannelIdAssigned(channel, remoteNodeId, temporaryChannelId, channelId) => context become main(channels + (channelId -> channel) - temporaryChannelId, shortIds, channelsTo + (channelId -> remoteNodeId) - temporaryChannelId) - case ShortChannelIdAssigned(_, channelId, shortChannelId, _) => - context become main(channels, shortIds + (shortChannelId -> channelId), channelsTo) + case scidAssigned: ShortChannelIdAssigned => + // We map all known scids (real or alias) to the channel_id. The relayer is in charge of deciding whether a real + // scid can be used or not for routing (see option_scid_alias), but the register is neutral. + val m = (scidAssigned.shortIds.real.toOption.toSeq :+ scidAssigned.shortIds.localAlias).map(_ -> scidAssigned.channelId).toMap + context become main(channels, shortIds ++ m, channelsTo) case Terminated(actor) if channels.values.toSet.contains(actor) => val channelId = channels.find(_._2 == actor).get._1 - val shortChannelId = shortIds.find(_._2 == channelId).map(_._1).getOrElse(ShortChannelId(0L)) - context become main(channels - channelId, shortIds - shortChannelId, channelsTo - channelId) + val shortChannelIds = shortIds.collect { case (key, value) if value == channelId => key } + context become main(channels - channelId, shortIds -- shortChannelIds, channelsTo - channelId) case Symbol("channels") => sender() ! channels diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala index 3b1b73c223..1e6fec2f99 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala @@ -31,7 +31,7 @@ import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._ import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinCoreClient import fr.acinq.eclair.channel.Commitments.PostRevocationAction import fr.acinq.eclair.channel.Helpers.Syncing.SyncResult -import fr.acinq.eclair.channel.Helpers.{Closing, Syncing, getRelayFees} +import fr.acinq.eclair.channel.Helpers.{Closing, Syncing, getRelayFees, scidForChannelUpdate} import fr.acinq.eclair.channel.Monitoring.Metrics.ProcessMessage import fr.acinq.eclair.channel.Monitoring.{Metrics, Tags} import fr.acinq.eclair.channel._ @@ -299,7 +299,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val case normal: DATA_NORMAL => watchFundingTx(data.commitments) - context.system.eventStream.publish(ShortChannelIdAssigned(self, normal.channelId, normal.channelUpdate.shortChannelId, None)) + context.system.eventStream.publish(ShortChannelIdAssigned(self, normal.channelId, normal.shortIds, remoteNodeId)) // we check the configuration because the values for channel_update may have changed while eclair was down val fees = getRelayFees(nodeParams, remoteNodeId, data.commitments) @@ -355,7 +355,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val Commitments.sendAdd(d.commitments, c, nodeParams.currentBlockHeight, nodeParams.onChainFeeConf) match { case Right((commitments1, add)) => if (c.commit) self ! CMD_SIGN() - context.system.eventStream.publish(AvailableBalanceChanged(self, d.channelId, d.shortChannelId, commitments1)) + context.system.eventStream.publish(AvailableBalanceChanged(self, d.channelId, d.shortIds, commitments1)) handleCommandSuccess(c, d.copy(commitments = commitments1)) sending add case Left(cause) => handleAddHtlcCommandError(c, cause, Some(d.channelUpdate)) } @@ -370,7 +370,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val Commitments.sendFulfill(d.commitments, c) match { case Right((commitments1, fulfill)) => if (c.commit) self ! CMD_SIGN() - context.system.eventStream.publish(AvailableBalanceChanged(self, d.channelId, d.shortChannelId, commitments1)) + context.system.eventStream.publish(AvailableBalanceChanged(self, d.channelId, d.shortIds, commitments1)) handleCommandSuccess(c, d.copy(commitments = commitments1)) sending fulfill case Left(cause) => // we acknowledge the command right away in case of failure @@ -390,7 +390,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val Commitments.sendFail(d.commitments, c, nodeParams.privateKey) match { case Right((commitments1, fail)) => if (c.commit) self ! CMD_SIGN() - context.system.eventStream.publish(AvailableBalanceChanged(self, d.channelId, d.shortChannelId, commitments1)) + context.system.eventStream.publish(AvailableBalanceChanged(self, d.channelId, d.shortIds, commitments1)) handleCommandSuccess(c, d.copy(commitments = commitments1)) sending fail case Left(cause) => // we acknowledge the command right away in case of failure @@ -401,7 +401,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val Commitments.sendFailMalformed(d.commitments, c) match { case Right((commitments1, fail)) => if (c.commit) self ! CMD_SIGN() - context.system.eventStream.publish(AvailableBalanceChanged(self, d.channelId, d.shortChannelId, commitments1)) + context.system.eventStream.publish(AvailableBalanceChanged(self, d.channelId, d.shortIds, commitments1)) handleCommandSuccess(c, d.copy(commitments = commitments1)) sending fail case Left(cause) => // we acknowledge the command right away in case of failure @@ -424,7 +424,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val Commitments.sendFee(d.commitments, c, nodeParams.onChainFeeConf) match { case Right((commitments1, fee)) => if (c.commit) self ! CMD_SIGN() - context.system.eventStream.publish(AvailableBalanceChanged(self, d.channelId, d.shortChannelId, commitments1)) + context.system.eventStream.publish(AvailableBalanceChanged(self, d.channelId, d.shortIds, commitments1)) handleCommandSuccess(c, d.copy(commitments = commitments1)) sending fee case Left(cause) => handleCommandError(cause, c) } @@ -481,7 +481,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val } if (d.commitments.availableBalanceForSend != commitments1.availableBalanceForSend) { // we send this event only when our balance changes - context.system.eventStream.publish(AvailableBalanceChanged(self, d.channelId, d.shortChannelId, commitments1)) + context.system.eventStream.publish(AvailableBalanceChanged(self, d.channelId, d.shortIds, commitments1)) } context.system.eventStream.publish(ChannelSignatureReceived(self, commitments1)) stay() using d.copy(commitments = commitments1) storing() sending revocation @@ -613,62 +613,76 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val case Event(c: CurrentFeerates, d: DATA_NORMAL) => handleCurrentFeerate(c, d) - case Event(WatchFundingDeeplyBuriedTriggered(blockHeight, txIndex, _), d: DATA_NORMAL) if d.channelAnnouncement.isEmpty => - val shortChannelId = ShortChannelId(blockHeight, txIndex, d.commitments.commitInput.outPoint.index.toInt) - log.info(s"funding tx is deeply buried at blockHeight=$blockHeight txIndex=$txIndex shortChannelId=$shortChannelId") - // if final shortChannelId is different from the one we had before, we need to re-announce it - val channelUpdate = if (shortChannelId != d.shortChannelId) { - log.info(s"short channel id changed, probably due to a chain reorg: old=${d.shortChannelId} new=$shortChannelId") - // we need to re-announce this shortChannelId - context.system.eventStream.publish(ShortChannelIdAssigned(self, d.channelId, shortChannelId, Some(d.shortChannelId))) + case Event(WatchFundingDeeplyBuriedTriggered(blockHeight, txIndex, fundingTx), d: DATA_NORMAL) if d.channelAnnouncement.isEmpty => + val finalRealShortId = RealScidStatus.Final(RealShortChannelId(blockHeight, txIndex, d.commitments.commitInput.outPoint.index.toInt)) + log.info(s"funding tx is deeply buried at blockHeight=$blockHeight txIndex=$txIndex shortChannelId=${finalRealShortId.realScid}") + val shortIds1 = d.shortIds.copy(real = finalRealShortId) + context.system.eventStream.publish(ShortChannelIdAssigned(self, d.channelId, shortIds1, remoteNodeId)) + if (d.shortIds.real == RealScidStatus.Unknown) { + // this is a zero-conf channel and it is the first time we know for sure that the funding tx has been confirmed + context.system.eventStream.publish(TransactionConfirmed(d.channelId, remoteNodeId, fundingTx)) + } + val scidForChannelUpdate = Helpers.scidForChannelUpdate(d.channelAnnouncement, shortIds1) + // if the shortChannelId is different from the one we had before, we need to re-announce it + val channelUpdate1 = if (d.channelUpdate.shortChannelId != scidForChannelUpdate) { + log.info(s"using new scid in channel_update: old=${d.channelUpdate.shortChannelId} new=$scidForChannelUpdate") // we re-announce the channelUpdate for the same reason - Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, shortChannelId, d.channelUpdate.cltvExpiryDelta, d.channelUpdate.htlcMinimumMsat, d.channelUpdate.feeBaseMsat, d.channelUpdate.feeProportionalMillionths, d.commitments.capacity.toMilliSatoshi, enable = Helpers.aboveReserve(d.commitments)) - } else d.channelUpdate - val localAnnSigs_opt = if (d.commitments.announceChannel) { + Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, scidForChannelUpdate, d.channelUpdate.cltvExpiryDelta, d.channelUpdate.htlcMinimumMsat, d.channelUpdate.feeBaseMsat, d.channelUpdate.feeProportionalMillionths, d.commitments.capacity.toMilliSatoshi, enable = Helpers.aboveReserve(d.commitments)) + } else { + d.channelUpdate + } + if (d.commitments.announceChannel) { // if channel is public we need to send our announcement_signatures in order to generate the channel_announcement - Some(Helpers.makeAnnouncementSignatures(nodeParams, d.commitments, shortChannelId)) - } else None - // we use goto() instead of stay() because we want to fire transitions - goto(NORMAL) using d.copy(shortChannelId = shortChannelId, buried = true, channelUpdate = channelUpdate) storing() sending localAnnSigs_opt.toSeq + val localAnnSigs = Helpers.makeAnnouncementSignatures(nodeParams, d.commitments, finalRealShortId.realScid) + // we use goto() instead of stay() because we want to fire transitions + goto(NORMAL) using d.copy(shortIds = shortIds1, channelUpdate = channelUpdate1) storing() sending localAnnSigs + } else { + // we use goto() instead of stay() because we want to fire transitions + goto(NORMAL) using d.copy(shortIds = shortIds1, channelUpdate = channelUpdate1) storing() + } case Event(remoteAnnSigs: AnnouncementSignatures, d: DATA_NORMAL) if d.commitments.announceChannel => // channels are publicly announced if both parties want it (defined as feature bit) - if (d.buried) { - // we are aware that the channel has reached enough confirmations - // we already had sent our announcement_signatures but we don't store them so we need to recompute it - val localAnnSigs = Helpers.makeAnnouncementSignatures(nodeParams, d.commitments, d.shortChannelId) - d.channelAnnouncement match { - case None => - require(d.shortChannelId == remoteAnnSigs.shortChannelId, s"shortChannelId mismatch: local=${d.shortChannelId} remote=${remoteAnnSigs.shortChannelId}") - log.info(s"announcing channelId=${d.channelId} on the network with shortId=${d.shortChannelId}") - import d.commitments.{localParams, remoteParams} - val fundingPubKey = keyManager.fundingPublicKey(localParams.fundingKeyPath) - val channelAnn = Announcements.makeChannelAnnouncement(nodeParams.chainHash, localAnnSigs.shortChannelId, nodeParams.nodeId, remoteParams.nodeId, fundingPubKey.publicKey, remoteParams.fundingPubKey, localAnnSigs.nodeSignature, remoteAnnSigs.nodeSignature, localAnnSigs.bitcoinSignature, remoteAnnSigs.bitcoinSignature) - if (!Announcements.checkSigs(channelAnn)) { - handleLocalError(InvalidAnnouncementSignatures(d.channelId, remoteAnnSigs), d, Some(remoteAnnSigs)) - } else { - // we use goto() instead of stay() because we want to fire transitions - goto(NORMAL) using d.copy(channelAnnouncement = Some(channelAnn)) storing() - } - case Some(_) => - // they have sent their announcement sigs, but we already have a valid channel announcement - // this can happen if our announcement_signatures was lost during a disconnection - // specs says that we "MUST respond to the first announcement_signatures message after reconnection with its own announcement_signatures message" - // current implementation always replies to announcement_signatures, not only the first time - // TODO: we should only be nice once, current behaviour opens way to DOS, but this should be handled higher in the stack anyway - log.info("re-sending our announcement sigs") - stay() sending localAnnSigs - } - } else { - // our watcher didn't notify yet that the tx has reached ANNOUNCEMENTS_MINCONF confirmations, let's delay remote's message - // note: no need to persist their message, in case of disconnection they will resend it - log.debug("received remote announcement signatures, delaying") - context.system.scheduler.scheduleOnce(5 seconds, self, remoteAnnSigs) - stay() + d.shortIds.real match { + case RealScidStatus.Final(realScid) => + // we are aware that the channel has reached enough confirmations + // we already had sent our announcement_signatures but we don't store them so we need to recompute it + val localAnnSigs = Helpers.makeAnnouncementSignatures(nodeParams, d.commitments, realScid) + d.channelAnnouncement match { + case None => + require(localAnnSigs.shortChannelId == remoteAnnSigs.shortChannelId, s"shortChannelId mismatch: local=${localAnnSigs.shortChannelId} remote=${remoteAnnSigs.shortChannelId}") + log.info(s"announcing channelId=${d.channelId} on the network with shortId=${localAnnSigs.shortChannelId}") + import d.commitments.{localParams, remoteParams} + val fundingPubKey = keyManager.fundingPublicKey(localParams.fundingKeyPath) + val channelAnn = Announcements.makeChannelAnnouncement(nodeParams.chainHash, localAnnSigs.shortChannelId, nodeParams.nodeId, remoteParams.nodeId, fundingPubKey.publicKey, remoteParams.fundingPubKey, localAnnSigs.nodeSignature, remoteAnnSigs.nodeSignature, localAnnSigs.bitcoinSignature, remoteAnnSigs.bitcoinSignature) + if (!Announcements.checkSigs(channelAnn)) { + handleLocalError(InvalidAnnouncementSignatures(d.channelId, remoteAnnSigs), d, Some(remoteAnnSigs)) + } else { + // we generate a new channel_update because the scid used may change if we were previously using an alias + val scidForChannelUpdate = Helpers.scidForChannelUpdate(Some(channelAnn), d.shortIds) + val channelUpdate = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, scidForChannelUpdate, d.channelUpdate.cltvExpiryDelta, d.channelUpdate.htlcMinimumMsat, d.channelUpdate.feeBaseMsat, d.channelUpdate.feeProportionalMillionths, d.commitments.capacity.toMilliSatoshi, enable = Helpers.aboveReserve(d.commitments)) + // we use goto() instead of stay() because we want to fire transitions + goto(NORMAL) using d.copy(channelAnnouncement = Some(channelAnn), channelUpdate = channelUpdate) storing() + } + case Some(_) => + // they have sent their announcement sigs, but we already have a valid channel announcement + // this can happen if our announcement_signatures was lost during a disconnection + // specs says that we "MUST respond to the first announcement_signatures message after reconnection with its own announcement_signatures message" + // current implementation always replies to announcement_signatures, not only the first time + // TODO: we should only be nice once, current behaviour opens way to DOS, but this should be handled higher in the stack anyway + log.info("re-sending our announcement sigs") + stay() sending localAnnSigs + } + case _ => + // our watcher didn't notify yet that the tx has reached ANNOUNCEMENTS_MINCONF confirmations, let's delay remote's message + // note: no need to persist their message, in case of disconnection they will resend it + log.debug("received remote announcement signatures, delaying") + context.system.scheduler.scheduleOnce(5 seconds, self, remoteAnnSigs) + stay() } case Event(c: CMD_UPDATE_RELAY_FEE, d: DATA_NORMAL) => - val channelUpdate1 = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, d.shortChannelId, c.cltvExpiryDelta_opt.getOrElse(d.channelUpdate.cltvExpiryDelta), d.channelUpdate.htlcMinimumMsat, c.feeBase, c.feeProportionalMillionths, d.commitments.capacity.toMilliSatoshi, enable = Helpers.aboveReserve(d.commitments)) + val channelUpdate1 = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, scidForChannelUpdate(d), c.cltvExpiryDelta_opt.getOrElse(d.channelUpdate.cltvExpiryDelta), d.channelUpdate.htlcMinimumMsat, c.feeBase, c.feeProportionalMillionths, d.commitments.capacity.toMilliSatoshi, enable = Helpers.aboveReserve(d.commitments)) log.info(s"updating relay fees: prev={} next={}", d.channelUpdate.toStringShort, channelUpdate1.toStringShort) val replyTo = if (c.replyTo == ActorRef.noSender) sender() else c.replyTo replyTo ! RES_SUCCESS(c, d.channelId) @@ -677,7 +691,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val case Event(BroadcastChannelUpdate(reason), d: DATA_NORMAL) => val age = TimestampSecond.now() - d.channelUpdate.timestamp - val channelUpdate1 = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, d.shortChannelId, d.channelUpdate.cltvExpiryDelta, d.channelUpdate.htlcMinimumMsat, d.channelUpdate.feeBaseMsat, d.channelUpdate.feeProportionalMillionths, d.commitments.capacity.toMilliSatoshi, enable = Helpers.aboveReserve(d.commitments)) + val channelUpdate1 = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, scidForChannelUpdate(d), d.channelUpdate.cltvExpiryDelta, d.channelUpdate.htlcMinimumMsat, d.channelUpdate.feeBaseMsat, d.channelUpdate.feeProportionalMillionths, d.commitments.capacity.toMilliSatoshi, enable = Helpers.aboveReserve(d.commitments)) reason match { case Reconnected if d.commitments.announceChannel && Announcements.areSame(channelUpdate1, d.channelUpdate) && age < REFRESH_CHANNEL_UPDATE_INTERVAL => // we already sent an identical channel_update not long ago (flapping protection in case we keep being disconnected/reconnected) @@ -701,7 +715,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val // if we have pending unsigned htlcs, then we cancel them and generate an update with the disabled flag set, that will be returned to the sender in a temporary channel failure if (d.commitments.localChanges.proposed.collectFirst { case add: UpdateAddHtlc => add }.isDefined) { log.debug("updating channel_update announcement (reason=disabled)") - val channelUpdate1 = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, d.shortChannelId, d.channelUpdate.cltvExpiryDelta, d.channelUpdate.htlcMinimumMsat, d.channelUpdate.feeBaseMsat, d.channelUpdate.feeProportionalMillionths, d.commitments.capacity.toMilliSatoshi, enable = false) + val channelUpdate1 = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, scidForChannelUpdate(d), d.channelUpdate.cltvExpiryDelta, d.channelUpdate.htlcMinimumMsat, d.channelUpdate.feeBaseMsat, d.channelUpdate.feeProportionalMillionths, d.commitments.capacity.toMilliSatoshi, enable = false) // NB: the htlcs stay() in the commitments.localChange, they will be cleaned up after reconnection d.commitments.localChanges.proposed.collect { case add: UpdateAddHtlc => relayer ! RES_ADD_SETTLED(d.commitments.originChannels(add.id), add, HtlcResult.DisconnectedBeforeSigned(channelUpdate1)) @@ -713,7 +727,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val case Event(e: Error, d: DATA_NORMAL) => handleRemoteError(e, d) - case Event(_: FundingLocked, _: DATA_NORMAL) => stay() // will happen after a reconnection if no updates were ever committed to the channel + case Event(_: ChannelReady, _: DATA_NORMAL) => stay() // will happen after a reconnection if no updates were ever committed to the channel }) @@ -1306,21 +1320,22 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val when(SYNCING)(handleExceptions { case Event(_: ChannelReestablish, d: DATA_WAIT_FOR_FUNDING_CONFIRMED) => val minDepth = if (d.commitments.localParams.isInitiator) { - nodeParams.channelConf.minDepthBlocks + Helpers.Funding.minDepthFunder(d.commitments.channelFeatures) } else { // when we're not the channel initiator we scale the min_depth confirmations depending on the funding amount - Helpers.minDepthForFunding(nodeParams.channelConf, d.commitments.commitInput.txOut.amount) + Helpers.Funding.minDepthFundee(nodeParams.channelConf, d.commitments.channelFeatures, d.commitments.commitInput.txOut.amount) } // we put back the watch (operation is idempotent) because the event may have been fired while we were in OFFLINE - blockchain ! WatchFundingConfirmed(self, d.commitments.commitInput.outPoint.txid, minDepth) + require(minDepth.nonEmpty, "min_depth must be set since we're waiting for the funding tx to confirm") + blockchain ! WatchFundingConfirmed(self, d.commitments.commitInput.outPoint.txid, minDepth.get) goto(WAIT_FOR_FUNDING_CONFIRMED) - case Event(_: ChannelReestablish, d: DATA_WAIT_FOR_FUNDING_LOCKED) => - log.debug("re-sending fundingLocked") + case Event(_: ChannelReestablish, d: DATA_WAIT_FOR_CHANNEL_READY) => + log.debug("re-sending channelReady") val channelKeyPath = keyManager.keyPath(d.commitments.localParams, d.commitments.channelConfig) val nextPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, 1) - val fundingLocked = FundingLocked(d.commitments.channelId, nextPerCommitmentPoint) - goto(WAIT_FOR_FUNDING_LOCKED) sending fundingLocked + val channelReady = ChannelReady(d.commitments.channelId, nextPerCommitmentPoint) + goto(WAIT_FOR_CHANNEL_READY) sending channelReady case Event(channelReestablish: ChannelReestablish, d: DATA_NORMAL) => Syncing.checkSync(keyManager, d, channelReestablish) match { @@ -1331,12 +1346,12 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val // normal case, our data is up-to-date if (channelReestablish.nextLocalCommitmentNumber == 1 && d.commitments.localCommit.index == 0) { - // If next_local_commitment_number is 1 in both the channel_reestablish it sent and received, then the node MUST retransmit funding_locked, otherwise it MUST NOT - log.debug("re-sending fundingLocked") + // If next_local_commitment_number is 1 in both the channel_reestablish it sent and received, then the node MUST retransmit channel_ready, otherwise it MUST NOT + log.debug("re-sending channelReady") val channelKeyPath = keyManager.keyPath(d.commitments.localParams, d.commitments.channelConfig) val nextPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, 1) - val fundingLocked = FundingLocked(d.commitments.channelId, nextPerCommitmentPoint) - sendQueue = sendQueue :+ fundingLocked + val channelReady = ChannelReady(d.commitments.channelId, nextPerCommitmentPoint) + sendQueue = sendQueue :+ channelReady } // we may need to retransmit updates and/or commit_sig and/or revocation @@ -1364,24 +1379,18 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val sendQueue = sendQueue :+ localShutdown } - if (!d.buried) { - // even if we were just disconnected/reconnected, we need to put back the watch because the event may have been - // fired while we were in OFFLINE (if not, the operation is idempotent anyway) - blockchain ! WatchFundingDeeplyBuried(self, d.commitments.commitInput.outPoint.txid, ANNOUNCEMENTS_MINCONF) - } else { - // channel has been buried enough, should we (re)send our announcement sigs? - d.channelAnnouncement match { - case None if !d.commitments.announceChannel => - // that's a private channel, nothing to do - () - case None => + d.shortIds.real match { + case RealScidStatus.Final(realShortChannelId) => + // should we (re)send our announcement sigs? + if (d.commitments.announceChannel && d.channelAnnouncement.isEmpty) { // BOLT 7: a node SHOULD retransmit the announcement_signatures message if it has not received an announcement_signatures message - val localAnnSigs = Helpers.makeAnnouncementSignatures(nodeParams, d.commitments, d.shortChannelId) + val localAnnSigs = Helpers.makeAnnouncementSignatures(nodeParams, d.commitments, realShortChannelId) sendQueue = sendQueue :+ localAnnSigs - case Some(_) => - // channel was already announced, nothing to do - () - } + } + case _ => + // even if we were just disconnected/reconnected, we need to put back the watch because the event may have been + // fired while we were in OFFLINE (if not, the operation is idempotent anyway) + blockchain ! WatchFundingDeeplyBuried(self, d.commitments.commitInput.outPoint.txid, ANNOUNCEMENTS_MINCONF) } if (d.commitments.announceChannel) { @@ -1440,14 +1449,14 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val } // This handler is a workaround for an issue in lnd: starting with versions 0.10 / 0.11, they sometimes fail to send - // a channel_reestablish when reconnecting a channel that recently got confirmed, and instead send a funding_locked + // a channel_reestablish when reconnecting a channel that recently got confirmed, and instead send a channel_ready // first and then go silent. This is due to a race condition on their side, so we trigger a reconnection, hoping that // we will eventually receive their channel_reestablish. - case Event(_: FundingLocked, d) => - log.warning("received funding_locked before channel_reestablish (known lnd bug): disconnecting...") + case Event(_: ChannelReady, d) => + log.warning("received channel_ready before channel_reestablish (known lnd bug): disconnecting...") // NB: we use a small delay to ensure we've sent our warning before disconnecting. context.system.scheduler.scheduleOnce(2 second, peer, Peer.Disconnect(remoteNodeId)) - stay() sending Warning(d.channelId, "spec violation: you sent funding_locked before channel_reestablish") + stay() sending Warning(d.channelId, "spec violation: you sent channel_ready before channel_reestablish") // This handler is a workaround for an issue in lnd similar to the one above: they sometimes send announcement_signatures // before channel_reestablish, which is a minor spec violation. It doesn't halt the channel, we can simply postpone @@ -1592,52 +1601,54 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val cancelTimer(RevocationTimeout.toString) } - // if channel is private, we send the channel_update directly to remote - // they need it "to learn the other end's forwarding parameters" (BOLT 7) - (state, nextState, stateData, nextStateData) match { - case (NORMAL, NORMAL, d1: DATA_NORMAL, d2: DATA_NORMAL) if !d1.commitments.announceChannel && !d1.buried && d2.buried => - // for a private channel, when the tx was just buried we need to send the channel_update to our peer (even if it didn't change) - send(d2.channelUpdate) - case (SYNCING, NORMAL, d1: DATA_NORMAL, d2: DATA_NORMAL) if !d1.commitments.announceChannel && d2.buried => - // otherwise if we're coming back online, we rebroadcast the latest channel_update - // this makes sure that if the channel_update was missed, we have a chance to re-send it - send(d2.channelUpdate) - case (NORMAL, NORMAL, d1: DATA_NORMAL, d2: DATA_NORMAL) if !d1.commitments.announceChannel && d1.channelUpdate != d2.channelUpdate && d2.buried => - // otherwise, we only send it when it is different, and tx is already buried - send(d2.channelUpdate) - case _ => () - } - sealed trait EmitLocalChannelEvent - case class EmitLocalChannelUpdate(d: DATA_NORMAL) extends EmitLocalChannelEvent + /* + * This event is for: + * - the router: so it knows about this channel to find routes + * - the relayer: so it learns about the channel real scid/alias and can route to it + * - the peer: so they can "learn the other end's forwarding parameters" (BOLT 7) + */ + case class EmitLocalChannelUpdate(reason: String, d: DATA_NORMAL, sendToPeer: Boolean) extends EmitLocalChannelEvent + /* + * When a channel that could previously be used to relay payments starts closing, we advertise the fact that this + * channel can't be used for payments anymore. If the channel is private we don't really need to tell the + * counterparty because it is already aware that the channel is being closed + */ case class EmitLocalChannelDown(d: DATA_NORMAL) extends EmitLocalChannelEvent + // We only send the channel_update directly to the peer if we are connected AND the channel hasn't been announced val emitEvent_opt: Option[EmitLocalChannelEvent] = (state, nextState, stateData, nextStateData) match { - case (WAIT_FOR_INIT_INTERNAL, OFFLINE, _, d: DATA_NORMAL) => Some(EmitLocalChannelUpdate(d)) - case (WAIT_FOR_FUNDING_LOCKED, NORMAL, _, d: DATA_NORMAL) => Some(EmitLocalChannelUpdate(d)) - case (NORMAL, NORMAL, d1: DATA_NORMAL, d2: DATA_NORMAL) if d1.channelUpdate != d2.channelUpdate || d1.channelAnnouncement != d2.channelAnnouncement => Some(EmitLocalChannelUpdate(d2)) - case (SYNCING, NORMAL, d1: DATA_NORMAL, d2: DATA_NORMAL) if d1.channelUpdate != d2.channelUpdate || d1.channelAnnouncement != d2.channelAnnouncement => Some(EmitLocalChannelUpdate(d2)) - case (NORMAL, OFFLINE, d1: DATA_NORMAL, d2: DATA_NORMAL) if d1.channelUpdate != d2.channelUpdate || d1.channelAnnouncement != d2.channelAnnouncement => Some(EmitLocalChannelUpdate(d2)) - case (OFFLINE, OFFLINE, d1: DATA_NORMAL, d2: DATA_NORMAL) if d1.channelUpdate != d2.channelUpdate || d1.channelAnnouncement != d2.channelAnnouncement => Some(EmitLocalChannelUpdate(d2)) - // When a channel that could previously be used to relay payments starts closing, we advertise the fact that this channel can't be used for payments anymore - // If the channel is private we don't really need to tell the counterparty because it is already aware that the channel is being closed + case (WAIT_FOR_INIT_INTERNAL, OFFLINE, _, d: DATA_NORMAL) => Some(EmitLocalChannelUpdate("restore", d, sendToPeer = false)) + case (WAIT_FOR_FUNDING_CONFIRMED, NORMAL, _, d: DATA_NORMAL) => Some(EmitLocalChannelUpdate("initial", d, sendToPeer = true)) + case (WAIT_FOR_CHANNEL_READY, NORMAL, _, d: DATA_NORMAL) => Some(EmitLocalChannelUpdate("initial", d, sendToPeer = true)) + case (NORMAL, NORMAL, d1: DATA_NORMAL, d2: DATA_NORMAL) if d1.channelUpdate != d2.channelUpdate || d1.channelAnnouncement != d2.channelAnnouncement => Some(EmitLocalChannelUpdate("normal->normal", d2, sendToPeer = d2.channelAnnouncement.isEmpty)) + case (SYNCING, NORMAL, d1: DATA_NORMAL, d2: DATA_NORMAL) if d1.channelUpdate != d2.channelUpdate || d1.channelAnnouncement != d2.channelAnnouncement => Some(EmitLocalChannelUpdate("syncing->normal", d2, sendToPeer = d2.channelAnnouncement.isEmpty)) + case (NORMAL, OFFLINE, d1: DATA_NORMAL, d2: DATA_NORMAL) if d1.channelUpdate != d2.channelUpdate || d1.channelAnnouncement != d2.channelAnnouncement => Some(EmitLocalChannelUpdate("normal->offline", d2, sendToPeer = false)) + case (OFFLINE, OFFLINE, d1: DATA_NORMAL, d2: DATA_NORMAL) if d1.channelUpdate != d2.channelUpdate || d1.channelAnnouncement != d2.channelAnnouncement => Some(EmitLocalChannelUpdate("offline->offline", d2, sendToPeer = false)) case (NORMAL | SYNCING | OFFLINE, SHUTDOWN | NEGOTIATING | CLOSING | CLOSED | ERR_INFORMATION_LEAK | WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT, d: DATA_NORMAL, _) => Some(EmitLocalChannelDown(d)) case _ => None } emitEvent_opt.foreach { - case EmitLocalChannelUpdate(d) => - log.info("emitting channel_update={} enabled={} ", d.channelUpdate, d.channelUpdate.channelFlags.isEnabled) - context.system.eventStream.publish(LocalChannelUpdate(self, d.channelId, d.shortChannelId, d.commitments.remoteParams.nodeId, d.channelAnnouncement, d.channelUpdate, d.commitments)) + case EmitLocalChannelUpdate(reason, d, sendToPeer) => + log.info(s"emitting channel update event: reason=$reason enabled=${d.channelUpdate.channelFlags.isEnabled} sendToPeer=$sendToPeer realScid=${d.shortIds.real} channel_update={} channel_announcement={}", d.channelUpdate, d.channelAnnouncement.map(_ => "yes").getOrElse("no")) + val lcu = LocalChannelUpdate(self, d.channelId, d.shortIds, d.commitments.remoteParams.nodeId, d.channelAnnouncement, d.channelUpdate, d.commitments) + context.system.eventStream.publish(lcu) + if (sendToPeer) { + send(d.channelUpdate) + } case EmitLocalChannelDown(d) => - context.system.eventStream.publish(LocalChannelDown(self, d.channelId, d.shortChannelId, d.commitments.remoteParams.nodeId)) + log.debug(s"emitting channel down event") + val lcd = LocalChannelDown(self, d.channelId, d.shortIds, d.commitments.remoteParams.nodeId) + context.system.eventStream.publish(lcd) } // When we change our channel update parameters (e.g. relay fees), we want to advertise it to other actors. (stateData, nextStateData) match { // NORMAL->NORMAL, NORMAL->OFFLINE, SYNCING->NORMAL case (d1: DATA_NORMAL, d2: DATA_NORMAL) => maybeEmitChannelUpdateChangedEvent(newUpdate = d2.channelUpdate, oldUpdate_opt = Some(d1.channelUpdate), d2) - // WAIT_FOR_FUNDING_LOCKED->NORMAL - case (_: DATA_WAIT_FOR_FUNDING_LOCKED, d2: DATA_NORMAL) => maybeEmitChannelUpdateChangedEvent(newUpdate = d2.channelUpdate, oldUpdate_opt = None, d2) + // WAIT_FOR_FUNDING_CONFIRMED->NORMAL, WAIT_FOR_CHANNEL_READY->NORMAL + case (_: DATA_WAIT_FOR_FUNDING_CONFIRMED, d2: DATA_NORMAL) => maybeEmitChannelUpdateChangedEvent(newUpdate = d2.channelUpdate, oldUpdate_opt = None, d2) + case (_: DATA_WAIT_FOR_CHANNEL_READY, d2: DATA_NORMAL) => maybeEmitChannelUpdateChangedEvent(newUpdate = d2.channelUpdate, oldUpdate_opt = None, d2) case _ => () } } @@ -1783,7 +1794,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val if (d.channelUpdate.channelFlags.isEnabled) { // if the channel isn't disabled we generate a new channel_update log.info("updating channel_update announcement (reason=disabled)") - val channelUpdate1 = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, d.shortChannelId, d.channelUpdate.cltvExpiryDelta, d.channelUpdate.htlcMinimumMsat, d.channelUpdate.feeBaseMsat, d.channelUpdate.feeProportionalMillionths, d.commitments.capacity.toMilliSatoshi, enable = false) + val channelUpdate1 = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, scidForChannelUpdate(d), d.channelUpdate.cltvExpiryDelta, d.channelUpdate.htlcMinimumMsat, d.channelUpdate.feeBaseMsat, d.channelUpdate.feeProportionalMillionths, d.commitments.capacity.toMilliSatoshi, enable = false) // then we update the state and replay the request self forward c // we use goto() to fire transitions @@ -1796,7 +1807,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val } private def handleUpdateRelayFeeDisconnected(c: CMD_UPDATE_RELAY_FEE, d: DATA_NORMAL) = { - val channelUpdate1 = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, d.shortChannelId, c.cltvExpiryDelta_opt.getOrElse(d.channelUpdate.cltvExpiryDelta), d.channelUpdate.htlcMinimumMsat, c.feeBase, c.feeProportionalMillionths, d.commitments.capacity.toMilliSatoshi, enable = false) + val channelUpdate1 = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, scidForChannelUpdate(d), c.cltvExpiryDelta_opt.getOrElse(d.channelUpdate.cltvExpiryDelta), d.channelUpdate.htlcMinimumMsat, c.feeBase, c.feeProportionalMillionths, d.commitments.capacity.toMilliSatoshi, enable = false) log.info(s"updating relay fees: prev={} next={}", d.channelUpdate.toStringShort, channelUpdate1.toStringShort) val replyTo = if (c.replyTo == ActorRef.noSender) sender() else c.replyTo replyTo ! RES_SUCCESS(c, d.channelId) @@ -1833,7 +1844,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val private def maybeEmitChannelUpdateChangedEvent(newUpdate: ChannelUpdate, oldUpdate_opt: Option[ChannelUpdate], d: DATA_NORMAL): Unit = { if (oldUpdate_opt.isEmpty || !Announcements.areSameIgnoreFlags(newUpdate, oldUpdate_opt.get)) { - context.system.eventStream.publish(ChannelUpdateParametersChanged(self, d.channelId, newUpdate.shortChannelId, d.commitments.remoteNodeId, newUpdate)) + context.system.eventStream.publish(ChannelUpdateParametersChanged(self, d.channelId, d.commitments.remoteNodeId, newUpdate)) } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenSingleFunder.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenSingleFunder.scala index 97f71b18fc..8966b25364 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenSingleFunder.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenSingleFunder.scala @@ -31,8 +31,8 @@ import fr.acinq.eclair.crypto.ShaChain import fr.acinq.eclair.router.Announcements import fr.acinq.eclair.transactions.Transactions.TxOwner import fr.acinq.eclair.transactions.{Scripts, Transactions} -import fr.acinq.eclair.wire.protocol.{AcceptChannel, AnnouncementSignatures, ChannelTlv, Error, FundingCreated, FundingLocked, FundingSigned, OpenChannel, TlvStream} -import fr.acinq.eclair.{Features, ShortChannelId, ToMilliSatoshiConversion, randomKey, toLongId} +import fr.acinq.eclair.wire.protocol.{AcceptChannel, AnnouncementSignatures, ChannelReady, ChannelTlv, Error, FundingCreated, FundingSigned, OpenChannel, TlvStream} +import fr.acinq.eclair.{Features, RealShortChannelId, ToMilliSatoshiConversion, randomKey, toLongId} import scodec.bits.ByteVector import scala.concurrent.duration.DurationInt @@ -63,8 +63,8 @@ trait ChannelOpenSingleFunder extends FundingHandlers with ErrorHandlers { WAIT_FOR_FUNDING_SIGNED| | | funding_signed | |<-------------------------------| - WAIT_FOR_FUNDING_LOCKED| |WAIT_FOR_FUNDING_LOCKED - | funding_locked funding_locked | + WAIT_FOR_CHANNEL_READY| |WAIT_FOR_CHANNEL_READY + | channel_ready channel_ready | |--------------- ---------------| | \/ | | /\ | @@ -80,7 +80,8 @@ trait ChannelOpenSingleFunder extends FundingHandlers with ErrorHandlers { context.system.eventStream.publish(ChannelCreated(self, peer, remoteNodeId, isInitiator = false, open.temporaryChannelId, open.feeratePerKw, None)) val fundingPubkey = keyManager.fundingPublicKey(localParams.fundingKeyPath).publicKey val channelKeyPath = keyManager.keyPath(localParams, channelConfig) - val minimumDepth = Helpers.minDepthForFunding(nodeParams.channelConf, open.fundingSatoshis) + val minimumDepth = Funding.minDepthFundee(nodeParams.channelConf, channelFeatures, open.fundingSatoshis) + log.info("will use fundingMinDepth={}", minimumDepth) // In order to allow TLV extensions and keep backwards-compatibility, we include an empty upfront_shutdown_script if this feature is not used. // See https://github.com/lightningnetwork/lightning-rfc/pull/714. val localShutdownScript = if (Features.canUseFeature(localParams.initFeatures, remoteInit.features, Features.UpfrontShutdownScript)) localParams.defaultFinalScriptPubKey else ByteVector.empty @@ -88,7 +89,7 @@ trait ChannelOpenSingleFunder extends FundingHandlers with ErrorHandlers { dustLimitSatoshis = localParams.dustLimit, maxHtlcValueInFlightMsat = localParams.maxHtlcValueInFlightMsat, channelReserveSatoshis = localParams.requestedChannelReserve_opt.getOrElse(0 sat), - minimumDepth = minimumDepth, + minimumDepth = minimumDepth.getOrElse(0), htlcMinimumMsat = localParams.htlcMinimum, toSelfDelay = localParams.toSelfDelay, maxAcceptedHtlcs = localParams.maxAcceptedHtlcs, @@ -151,6 +152,7 @@ trait ChannelOpenSingleFunder extends FundingHandlers with ErrorHandlers { initFeatures = remoteInit.features, shutdownScript = remoteShutdownScript) log.debug("remote params: {}", remoteParams) + log.info("remote will use fundingMinDepth={}", accept.minimumDepth) val localFundingPubkey = keyManager.fundingPublicKey(localParams.fundingKeyPath) val fundingPubkeyScript = Script.write(Script.pay2wsh(Scripts.multiSig2of2(localFundingPubkey.publicKey, remoteParams.fundingPubKey))) wallet.makeFundingTx(fundingPubkeyScript, fundingSatoshis, fundingTxFeerate).pipeTo(self) @@ -254,9 +256,14 @@ trait ChannelOpenSingleFunder extends FundingHandlers with ErrorHandlers { // NB: we don't send a ChannelSignatureSent for the first commit log.info(s"waiting for them to publish the funding tx for channelId=$channelId fundingTxid=${commitInput.outPoint.txid}") watchFundingTx(commitments) - val fundingMinDepth = Helpers.minDepthForFunding(nodeParams.channelConf, fundingAmount) - blockchain ! WatchFundingConfirmed(self, commitInput.outPoint.txid, fundingMinDepth) - goto(WAIT_FOR_FUNDING_CONFIRMED) using DATA_WAIT_FOR_FUNDING_CONFIRMED(commitments, None, nodeParams.currentBlockHeight, None, Right(fundingSigned)) storing() sending fundingSigned + Funding.minDepthFundee(nodeParams.channelConf, commitments.channelFeatures, fundingAmount) match { + case Some(fundingMinDepth) => + blockchain ! WatchFundingConfirmed(self, commitInput.outPoint.txid, fundingMinDepth) + goto(WAIT_FOR_FUNDING_CONFIRMED) using DATA_WAIT_FOR_FUNDING_CONFIRMED(commitments, None, nodeParams.currentBlockHeight, None, Right(fundingSigned)) storing() sending fundingSigned + case None => + val (shortIds, channelReady) = acceptFundingTx(commitments, RealScidStatus.Unknown) + goto(WAIT_FOR_CHANNEL_READY) using DATA_WAIT_FOR_CHANNEL_READY(commitments, shortIds, channelReady) storing() sending Seq(fundingSigned, channelReady) + } } } @@ -294,8 +301,6 @@ trait ChannelOpenSingleFunder extends FundingHandlers with ErrorHandlers { context.system.eventStream.publish(ChannelSignatureReceived(self, commitments)) log.info(s"publishing funding tx for channelId=$channelId fundingTxid=${commitInput.outPoint.txid}") watchFundingTx(commitments) - blockchain ! WatchFundingConfirmed(self, commitInput.outPoint.txid, nodeParams.channelConf.minDepthBlocks) - log.info(s"committing txid=${fundingTx.txid}") // we will publish the funding tx only after the channel state has been written to disk because we want to // make sure we first persist the commitment that returns back the funds to us in case of problem @@ -313,7 +318,14 @@ trait ChannelOpenSingleFunder extends FundingHandlers with ErrorHandlers { } } - goto(WAIT_FOR_FUNDING_CONFIRMED) using DATA_WAIT_FOR_FUNDING_CONFIRMED(commitments, Some(fundingTx), blockHeight, None, Left(fundingCreated)) storing() calling publishFundingTx() + Funding.minDepthFunder(commitments.channelFeatures) match { + case Some(fundingMinDepth) => + blockchain ! WatchFundingConfirmed(self, commitInput.outPoint.txid, fundingMinDepth) + goto(WAIT_FOR_FUNDING_CONFIRMED) using DATA_WAIT_FOR_FUNDING_CONFIRMED(commitments, Some(fundingTx), blockHeight, None, Left(fundingCreated)) storing() calling publishFundingTx() + case None => + val (shortIds, channelReady) = acceptFundingTx(commitments, RealScidStatus.Unknown) + goto(WAIT_FOR_CHANNEL_READY) using DATA_WAIT_FOR_CHANNEL_READY(commitments, shortIds, channelReady) storing() sending channelReady calling publishFundingTx() + } } case Event(c: CloseCommand, d: DATA_WAIT_FOR_FUNDING_SIGNED) => @@ -342,26 +354,30 @@ trait ChannelOpenSingleFunder extends FundingHandlers with ErrorHandlers { }) when(WAIT_FOR_FUNDING_CONFIRMED)(handleExceptions { - case Event(msg: FundingLocked, d: DATA_WAIT_FOR_FUNDING_CONFIRMED) => - log.info(s"received their FundingLocked, deferring message") - stay() using d.copy(deferred = Some(msg)) // no need to store, they will re-send if we get disconnected + case Event(remoteChannelReady: ChannelReady, d: DATA_WAIT_FOR_FUNDING_CONFIRMED) => + if (remoteChannelReady.alias_opt.isDefined && d.commitments.localParams.isInitiator) { + log.info("this chanel isn't zero-conf, but we are funder and they sent an early channel_ready with an alias: no need to wait for confirmations") + // No need to emit ShortChannelIdAssigned: we will emit it when handling their channel_ready in WAIT_FOR_CHANNEL_READY + val (shortIds, localChannelReady) = acceptFundingTx(d.commitments, RealScidStatus.Unknown) + self ! remoteChannelReady + // NB: we will receive a WatchFundingConfirmedTriggered later that will simply be ignored + goto(WAIT_FOR_CHANNEL_READY) using DATA_WAIT_FOR_CHANNEL_READY(d.commitments, shortIds, localChannelReady) storing() sending localChannelReady + } else { + log.info("received their channel_ready, deferring message") + stay() using d.copy(deferred = Some(remoteChannelReady)) // no need to store, they will re-send if we get disconnected + } case Event(WatchFundingConfirmedTriggered(blockHeight, txIndex, fundingTx), d@DATA_WAIT_FOR_FUNDING_CONFIRMED(commitments, _, _, deferred, _)) => Try(Transaction.correctlySpends(commitments.fullySignedLocalCommitTx(keyManager).tx, Seq(fundingTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)) match { case Success(_) => - log.info(s"channelId=${commitments.channelId} was confirmed at blockHeight=$blockHeight txIndex=$txIndex") - blockchain ! WatchFundingLost(self, commitments.commitInput.outPoint.txid, nodeParams.channelConf.minDepthBlocks) - if (!d.commitments.localParams.isInitiator) context.system.eventStream.publish(TransactionPublished(commitments.channelId, remoteNodeId, fundingTx, 0 sat, "funding")) - context.system.eventStream.publish(TransactionConfirmed(commitments.channelId, remoteNodeId, fundingTx)) - val channelKeyPath = keyManager.keyPath(d.commitments.localParams, commitments.channelConfig) - val nextPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, 1) - val fundingLocked = FundingLocked(commitments.channelId, nextPerCommitmentPoint) + log.info(s"channelId=${d.channelId} was confirmed at blockHeight=$blockHeight txIndex=$txIndex") + if (!d.commitments.localParams.isInitiator) context.system.eventStream.publish(TransactionPublished(d.channelId, remoteNodeId, fundingTx, 0 sat, "funding")) + context.system.eventStream.publish(TransactionConfirmed(d.channelId, remoteNodeId, fundingTx)) + + val realScidStatus = RealScidStatus.Temporary(RealShortChannelId(blockHeight, txIndex, commitments.commitInput.outPoint.index.toInt)) + val (shortIds, channelReady) = acceptFundingTx(commitments, realScidStatus = realScidStatus) deferred.foreach(self ! _) - // this is the temporary channel id that we will use in our channel_update message, the goal is to be able to use our channel - // as soon as it reaches NORMAL state, and before it is announced on the network - // (this id might be updated when the funding tx gets deeply buried, if there was a reorg in the meantime) - val shortChannelId = ShortChannelId(blockHeight, txIndex, commitments.commitInput.outPoint.index.toInt) - goto(WAIT_FOR_FUNDING_LOCKED) using DATA_WAIT_FOR_FUNDING_LOCKED(commitments, shortChannelId, fundingLocked) storing() sending fundingLocked + goto(WAIT_FOR_CHANNEL_READY) using DATA_WAIT_FOR_CHANNEL_READY(commitments, shortIds, channelReady) storing() sending channelReady case Failure(t) => log.error(t, s"rejecting channel with invalid funding tx: ${fundingTx.bin}") goto(CLOSED) @@ -396,30 +412,37 @@ trait ChannelOpenSingleFunder extends FundingHandlers with ErrorHandlers { case Event(e: Error, d: DATA_WAIT_FOR_FUNDING_CONFIRMED) => handleRemoteError(e, d) }) - when(WAIT_FOR_FUNDING_LOCKED)(handleExceptions { - case Event(FundingLocked(_, nextPerCommitmentPoint, _), d@DATA_WAIT_FOR_FUNDING_LOCKED(commitments, shortChannelId, _)) => - // used to get the final shortChannelId, used in announcements (if minDepth >= ANNOUNCEMENTS_MINCONF this event will fire instantly) - blockchain ! WatchFundingDeeplyBuried(self, commitments.commitInput.outPoint.txid, ANNOUNCEMENTS_MINCONF) - context.system.eventStream.publish(ShortChannelIdAssigned(self, commitments.channelId, shortChannelId, None)) + when(WAIT_FOR_CHANNEL_READY)(handleExceptions { + case Event(channelReady: ChannelReady, d: DATA_WAIT_FOR_CHANNEL_READY) => + val shortIds1 = d.shortIds.copy(remoteAlias_opt = channelReady.alias_opt) + shortIds1.remoteAlias_opt.foreach { remoteAlias => + log.info("received remoteAlias={}", remoteAlias) + context.system.eventStream.publish(ShortChannelIdAssigned(self, d.commitments.channelId, shortIds = shortIds1, remoteNodeId = remoteNodeId)) + } + log.info("shortIds: real={} localAlias={} remoteAlias={}", shortIds1.real.toOption.getOrElse("none"), shortIds1.localAlias, shortIds1.remoteAlias_opt.getOrElse("none")) // we create a channel_update early so that we can use it to send payments through this channel, but it won't be propagated to other nodes since the channel is not yet announced - val fees = getRelayFees(nodeParams, remoteNodeId, commitments) - val initialChannelUpdate = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, shortChannelId, nodeParams.channelConf.expiryDelta, d.commitments.remoteParams.htlcMinimum, fees.feeBase, fees.feeProportionalMillionths, commitments.capacity.toMilliSatoshi, enable = Helpers.aboveReserve(d.commitments)) + val scidForChannelUpdate = Helpers.scidForChannelUpdate(channelAnnouncement_opt = None, shortIds1) + log.info("using shortChannelId={} for initial channel_update", scidForChannelUpdate) + val relayFees = getRelayFees(nodeParams, remoteNodeId, d.commitments) + val initialChannelUpdate = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, scidForChannelUpdate, nodeParams.channelConf.expiryDelta, d.commitments.remoteParams.htlcMinimum, relayFees.feeBase, relayFees.feeProportionalMillionths, d.commitments.capacity.toMilliSatoshi, enable = Helpers.aboveReserve(d.commitments)) // we need to periodically re-send channel updates, otherwise channel will be considered stale and get pruned by network context.system.scheduler.scheduleWithFixedDelay(initialDelay = REFRESH_CHANNEL_UPDATE_INTERVAL, delay = REFRESH_CHANNEL_UPDATE_INTERVAL, receiver = self, message = BroadcastChannelUpdate(PeriodicRefresh)) - goto(NORMAL) using DATA_NORMAL(commitments.copy(remoteNextCommitInfo = Right(nextPerCommitmentPoint)), shortChannelId, buried = false, None, initialChannelUpdate, None, None, None) storing() + // used to get the final shortChannelId, used in announcements (if minDepth >= ANNOUNCEMENTS_MINCONF this event will fire instantly) + blockchain ! WatchFundingDeeplyBuried(self, d.commitments.commitInput.outPoint.txid, ANNOUNCEMENTS_MINCONF) + goto(NORMAL) using DATA_NORMAL(d.commitments.copy(remoteNextCommitInfo = Right(channelReady.nextPerCommitmentPoint)), shortIds1, None, initialChannelUpdate, None, None, None) storing() - case Event(remoteAnnSigs: AnnouncementSignatures, d: DATA_WAIT_FOR_FUNDING_LOCKED) if d.commitments.announceChannel => + case Event(remoteAnnSigs: AnnouncementSignatures, d: DATA_WAIT_FOR_CHANNEL_READY) if d.commitments.announceChannel => log.debug("received remote announcement signatures, delaying") // we may receive their announcement sigs before our watcher notifies us that the channel has reached min_conf (especially during testing when blocks are generated in bulk) // note: no need to persist their message, in case of disconnection they will resend it context.system.scheduler.scheduleOnce(2 seconds, self, remoteAnnSigs) stay() - case Event(WatchFundingSpentTriggered(tx), d: DATA_WAIT_FOR_FUNDING_LOCKED) if tx.txid == d.commitments.remoteCommit.txid => handleRemoteSpentCurrent(tx, d) + case Event(WatchFundingSpentTriggered(tx), d: DATA_WAIT_FOR_CHANNEL_READY) if tx.txid == d.commitments.remoteCommit.txid => handleRemoteSpentCurrent(tx, d) - case Event(WatchFundingSpentTriggered(tx), d: DATA_WAIT_FOR_FUNDING_LOCKED) => handleInformationLeak(tx, d) + case Event(WatchFundingSpentTriggered(tx), d: DATA_WAIT_FOR_CHANNEL_READY) => handleInformationLeak(tx, d) - case Event(e: Error, d: DATA_WAIT_FOR_FUNDING_LOCKED) => handleRemoteError(e, d) + case Event(e: Error, d: DATA_WAIT_FOR_CHANNEL_READY) => handleRemoteError(e, d) }) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/FundingHandlers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/FundingHandlers.scala index f478859c2f..58adea13a7 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/FundingHandlers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/FundingHandlers.scala @@ -19,12 +19,12 @@ package fr.acinq.eclair.channel.fsm import akka.actor.Status import akka.actor.typed.scaladsl.adapter.{TypedActorRefOps, actorRefAdapter} import fr.acinq.bitcoin.scalacompat.{ByteVector32, SatoshiLong, Transaction} -import fr.acinq.eclair.BlockHeight -import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.{GetTxWithMeta, GetTxWithMetaResponse, WatchFundingSpent} +import fr.acinq.eclair.{Alias, BlockHeight, RealShortChannelId, ShortChannelId} +import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.{GetTxWithMeta, GetTxWithMetaResponse, WatchFundingLost, WatchFundingSpent} import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel.fsm.Channel.{BITCOIN_FUNDING_PUBLISH_FAILED, BITCOIN_FUNDING_TIMEOUT, FUNDING_TIMEOUT_FUNDEE} import fr.acinq.eclair.channel.publish.TxPublisher.PublishFinalTx -import fr.acinq.eclair.wire.protocol.Error +import fr.acinq.eclair.wire.protocol.{ChannelReady, ChannelReadyTlv, Error, TlvStream} import scala.concurrent.duration.DurationInt import scala.util.{Failure, Success} @@ -59,6 +59,19 @@ trait FundingHandlers extends CommonHandlers { // TODO: implement this? (not needed if we use a reasonable min_depth) //blockchain ! WatchLost(self, commitments.commitInput.outPoint.txid, nodeParams.channelConf.minDepthBlocks, BITCOIN_FUNDING_LOST) } + + def acceptFundingTx(commitments: Commitments, realScidStatus: RealScidStatus): (ShortIds, ChannelReady) = { + blockchain ! WatchFundingLost(self, commitments.commitInput.outPoint.txid, nodeParams.channelConf.minDepthBlocks) + val channelKeyPath = keyManager.keyPath(commitments.localParams, commitments.channelConfig) + val nextPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, 1) + // the alias will use in our peer's channel_update message, the goal is to be able to use our channel as soon + // as it reaches NORMAL state, and before it is announced on the network + val shortIds = ShortIds(realScidStatus, ShortChannelId.generateLocalAlias(), remoteAlias_opt = None) + context.system.eventStream.publish(ShortChannelIdAssigned(self, commitments.channelId, shortIds, remoteNodeId)) + // we always send our local alias, even if it isn't explicitly supported, that's an optional TLV anyway + val channelReady = ChannelReady(commitments.channelId, nextPerCommitmentPoint, TlvStream(ChannelReadyTlv.ShortChannelIdTlv(shortIds.localAlias))) + (shortIds, channelReady) + } /** * When we are funder, we use this function to detect when our funding tx has been double-spent (by another transaction diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/TransportHandler.scala b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/TransportHandler.scala index fe836fd088..ac6003d0aa 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/TransportHandler.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/TransportHandler.scala @@ -410,6 +410,7 @@ object TransportHandler { case class Encryptor(state: CipherState) { /** * see BOLT #8 + * {{{ * +------------------------------- * |2-byte encrypted message length| * +------------------------------- @@ -425,6 +426,7 @@ object TransportHandler { * | 16-byte MAC of the | * | lightning message | * +------------------------------- + * }}} * * @param plaintext plaintext * @return a (cipherstate, ciphertext) tuple where ciphertext is encrypted according to BOLT #8 diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/DbEventHandler.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/DbEventHandler.scala index e112fea7b6..a1f63f57ca 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/DbEventHandler.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/DbEventHandler.scala @@ -108,7 +108,7 @@ class DbEventHandler(nodeParams: NodeParams) extends Actor with DiagnosticActorL case e: ChannelStateChanged => // NB: order matters! e match { - case ChannelStateChanged(_, channelId, _, remoteNodeId, WAIT_FOR_FUNDING_LOCKED, NORMAL, Some(commitments: Commitments)) => + case ChannelStateChanged(_, channelId, _, remoteNodeId, WAIT_FOR_CHANNEL_READY, NORMAL, Some(commitments: Commitments)) => ChannelMetrics.ChannelLifecycleEvents.withTag(ChannelTags.Event, ChannelTags.Events.Created).increment() val event = ChannelEvent.EventType.Created auditDb.add(ChannelEvent(channelId, remoteNodeId, commitments.capacity, commitments.localParams.isInitiator, !commitments.announceChannel, event)) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/DualDatabases.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/DualDatabases.scala index 9b9d5056e2..1fa137dcf8 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/DualDatabases.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/DualDatabases.scala @@ -2,6 +2,7 @@ package fr.acinq.eclair.db import com.google.common.util.concurrent.ThreadFactoryBuilder import fr.acinq.bitcoin.scalacompat.{ByteVector32, Crypto, Satoshi} +import fr.acinq.eclair.RealShortChannelId import fr.acinq.eclair.channel._ import fr.acinq.eclair.db.Databases.{FileBackup, PostgresDatabases, SqliteDatabases} import fr.acinq.eclair.db.DbEventHandler.ChannelEvent @@ -113,17 +114,17 @@ case class DualNetworkDb(primary: NetworkDb, secondary: NetworkDb) extends Netwo primary.removeChannels(shortChannelIds) } - override def listChannels(): SortedMap[ShortChannelId, Router.PublicChannel] = { + override def listChannels(): SortedMap[RealShortChannelId, Router.PublicChannel] = { runAsync(secondary.listChannels()) primary.listChannels() } - override def addToPruned(shortChannelIds: Iterable[ShortChannelId]): Unit = { + override def addToPruned(shortChannelIds: Iterable[RealShortChannelId]): Unit = { runAsync(secondary.addToPruned(shortChannelIds)) primary.addToPruned(shortChannelIds) } - override def removeFromPruned(shortChannelId: ShortChannelId): Unit = { + override def removeFromPruned(shortChannelId: RealShortChannelId): Unit = { runAsync(secondary.removeFromPruned(shortChannelId)) primary.removeFromPruned(shortChannelId) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/NetworkDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/NetworkDb.scala index 80973d1c2c..ec3031fd0a 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/NetworkDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/NetworkDb.scala @@ -19,6 +19,7 @@ package fr.acinq.eclair.db import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.bitcoin.scalacompat.{ByteVector32, Satoshi} import fr.acinq.eclair.ShortChannelId +import fr.acinq.eclair.RealShortChannelId import fr.acinq.eclair.router.Router.PublicChannel import fr.acinq.eclair.wire.protocol.{ChannelAnnouncement, ChannelUpdate, NodeAnnouncement} @@ -44,11 +45,11 @@ trait NetworkDb { def removeChannels(shortChannelIds: Iterable[ShortChannelId]): Unit - def listChannels(): SortedMap[ShortChannelId, PublicChannel] + def listChannels(): SortedMap[RealShortChannelId, PublicChannel] - def addToPruned(shortChannelIds: Iterable[ShortChannelId]): Unit + def addToPruned(shortChannelIds: Iterable[RealShortChannelId]): Unit - def removeFromPruned(shortChannelId: ShortChannelId): Unit + def removeFromPruned(shortChannelId: RealShortChannelId): Unit def isPruned(shortChannelId: ShortChannelId): Boolean diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgNetworkDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgNetworkDb.scala index afe716350e..c44ca7c892 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgNetworkDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgNetworkDb.scala @@ -18,6 +18,7 @@ package fr.acinq.eclair.db.pg import fr.acinq.bitcoin.scalacompat.{ByteVector32, Crypto, Satoshi} import fr.acinq.eclair.ShortChannelId +import fr.acinq.eclair.RealShortChannelId import fr.acinq.eclair.db.Monitoring.Metrics.withMetrics import fr.acinq.eclair.db.Monitoring.Tags.DbBackends import fr.acinq.eclair.db.NetworkDb @@ -199,11 +200,11 @@ class PgNetworkDb(implicit ds: DataSource) extends NetworkDb with Logging { } } - override def listChannels(): SortedMap[ShortChannelId, PublicChannel] = withMetrics("network/list-channels", DbBackends.Postgres) { + override def listChannels(): SortedMap[RealShortChannelId, PublicChannel] = withMetrics("network/list-channels", DbBackends.Postgres) { inTransaction { pg => using(pg.createStatement()) { statement => statement.executeQuery("SELECT channel_announcement, txid, capacity_sat, channel_update_1, channel_update_2 FROM network.public_channels") - .foldLeft(SortedMap.empty[ShortChannelId, PublicChannel]) { (m, rs) => + .foldLeft(SortedMap.empty[RealShortChannelId, PublicChannel]) { (m, rs) => val ann = channelAnnouncementCodec.decode(rs.getBitVectorOpt("channel_announcement").get).require.value val txId = ByteVector32.fromValidHex(rs.getString("txid")) val capacity = rs.getLong("capacity_sat") @@ -223,12 +224,13 @@ class PgNetworkDb(implicit ds: DataSource) extends NetworkDb with Logging { })")) { statement => shortChannelIds + .map(_.toLong) .grouped(batchSize) .foreach { group => - val padded = group.toArray.padTo(batchSize, ShortChannelId(0L)) + val padded = group.toArray.padTo(batchSize, 0L) for (i <- 0 until batchSize) { - statement.setLong(1 + i, padded(i).toLong) // index for jdbc parameters starts at 1 + statement.setLong(1 + i, padded(i)) // index for jdbc parameters starts at 1 } statement.executeUpdate() } @@ -236,7 +238,7 @@ class PgNetworkDb(implicit ds: DataSource) extends NetworkDb with Logging { } } - override def addToPruned(shortChannelIds: Iterable[ShortChannelId]): Unit = withMetrics("network/add-to-pruned", DbBackends.Postgres) { + override def addToPruned(shortChannelIds: Iterable[RealShortChannelId]): Unit = withMetrics("network/add-to-pruned", DbBackends.Postgres) { inTransaction { pg => using(pg.prepareStatement("INSERT INTO network.pruned_channels VALUES (?) ON CONFLICT DO NOTHING")) { statement => @@ -249,7 +251,7 @@ class PgNetworkDb(implicit ds: DataSource) extends NetworkDb with Logging { } } - override def removeFromPruned(shortChannelId: ShortChannelId): Unit = withMetrics("network/remove-from-pruned", DbBackends.Postgres) { + override def removeFromPruned(shortChannelId: RealShortChannelId): Unit = withMetrics("network/remove-from-pruned", DbBackends.Postgres) { inTransaction { pg => using(pg.prepareStatement(s"DELETE FROM network.pruned_channels WHERE short_channel_id=?")) { statement => diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteNetworkDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteNetworkDb.scala index 844e886aae..b05e743ffb 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteNetworkDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteNetworkDb.scala @@ -18,6 +18,7 @@ package fr.acinq.eclair.db.sqlite import fr.acinq.bitcoin.scalacompat.{ByteVector32, Crypto, Satoshi} import fr.acinq.eclair.ShortChannelId +import fr.acinq.eclair.RealShortChannelId import fr.acinq.eclair.db.Monitoring.Metrics.withMetrics import fr.acinq.eclair.db.Monitoring.Tags.DbBackends import fr.acinq.eclair.db.NetworkDb @@ -124,10 +125,10 @@ class SqliteNetworkDb(val sqlite: Connection) extends NetworkDb with Logging { } } - override def listChannels(): SortedMap[ShortChannelId, PublicChannel] = withMetrics("network/list-channels", DbBackends.Sqlite) { + override def listChannels(): SortedMap[RealShortChannelId, PublicChannel] = withMetrics("network/list-channels", DbBackends.Sqlite) { using(sqlite.createStatement()) { statement => statement.executeQuery("SELECT channel_announcement, txid, capacity_sat, channel_update_1, channel_update_2 FROM channels") - .foldLeft(SortedMap.empty[ShortChannelId, PublicChannel]) { (m, rs) => + .foldLeft(SortedMap.empty[RealShortChannelId, PublicChannel]) { (m, rs) => val ann = channelAnnouncementCodec.decode(rs.getBitVectorOpt("channel_announcement").get).require.value val txId = ByteVector32.fromValidHex(rs.getString("txid")) val capacity = rs.getLong("capacity_sat") @@ -142,18 +143,19 @@ class SqliteNetworkDb(val sqlite: Connection) extends NetworkDb with Logging { val batchSize = 100 using(sqlite.prepareStatement(s"DELETE FROM channels WHERE short_channel_id IN (${List.fill(batchSize)("?").mkString(",")})")) { statement => shortChannelIds + .map(_.toLong) .grouped(batchSize) .foreach { group => - val padded = group.toArray.padTo(batchSize, ShortChannelId(0L)) + val padded = group.toArray.padTo(batchSize, 0L) for (i <- 0 until batchSize) { - statement.setLong(1 + i, padded(i).toLong) // index for jdbc parameters starts at 1 + statement.setLong(1 + i, padded(i)) // index for jdbc parameters starts at 1 } statement.executeUpdate() } } } - override def addToPruned(shortChannelIds: Iterable[ShortChannelId]): Unit = withMetrics("network/add-to-pruned", DbBackends.Sqlite) { + override def addToPruned(shortChannelIds: Iterable[RealShortChannelId]): Unit = withMetrics("network/add-to-pruned", DbBackends.Sqlite) { using(sqlite.prepareStatement("INSERT OR IGNORE INTO pruned VALUES (?)"), inTransaction = true) { statement => shortChannelIds.foreach(shortChannelId => { statement.setLong(1, shortChannelId.toLong) @@ -163,7 +165,7 @@ class SqliteNetworkDb(val sqlite: Connection) extends NetworkDb with Logging { } } - override def removeFromPruned(shortChannelId: ShortChannelId): Unit = withMetrics("network/remove-from-pruned", DbBackends.Sqlite) { + override def removeFromPruned(shortChannelId: RealShortChannelId): Unit = withMetrics("network/remove-from-pruned", DbBackends.Sqlite) { using(sqlite.prepareStatement(s"DELETE FROM pruned WHERE short_channel_id=?")) { statement => statement.setLong(1, shortChannelId.toLong) statement.executeUpdate() diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala index 0fa63ef102..a37270e758 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala @@ -146,14 +146,15 @@ class Peer(val nodeParams: NodeParams, remoteNodeId: PublicKey, wallet: OnChainA } else { val channelConfig = ChannelConfig.standard // If a channel type was provided, we directly use it instead of computing it based on local and remote features. - val channelType = c.channelType_opt.getOrElse(ChannelTypes.defaultFromFeatures(d.localFeatures, d.remoteFeatures)) + val channelFlags = c.channelFlags.getOrElse(nodeParams.channelConf.channelFlags) + val channelType = c.channelType_opt.getOrElse(ChannelTypes.defaultFromFeatures(d.localFeatures, d.remoteFeatures, channelFlags.announceChannel)) val (channel, localParams) = createNewChannel(nodeParams, d.localFeatures, channelType, isInitiator = true, c.fundingSatoshis, origin_opt = Some(sender())) c.timeout_opt.map(openTimeout => context.system.scheduler.scheduleOnce(openTimeout.duration, channel, Channel.TickChannelOpenTimeout)(context.dispatcher)) val temporaryChannelId = randomBytes32() val channelFeeratePerKw = nodeParams.onChainFeeConf.getCommitmentFeerate(remoteNodeId, channelType, c.fundingSatoshis, None) val fundingTxFeeratePerKw = c.fundingTxFeeratePerKw_opt.getOrElse(nodeParams.onChainFeeConf.feeEstimator.getFeeratePerKw(target = nodeParams.onChainFeeConf.feeTargets.fundingBlockTarget)) log.info(s"requesting a new channel with type=$channelType fundingSatoshis=${c.fundingSatoshis}, pushMsat=${c.pushMsat} and fundingFeeratePerByte=${c.fundingTxFeeratePerKw_opt} temporaryChannelId=$temporaryChannelId localParams=$localParams") - channel ! INPUT_INIT_FUNDER(temporaryChannelId, c.fundingSatoshis, c.pushMsat, channelFeeratePerKw, fundingTxFeeratePerKw, localParams, d.peerConnection, d.remoteInit, c.channelFlags.getOrElse(nodeParams.channelConf.channelFlags), channelConfig, channelType) + channel ! INPUT_INIT_FUNDER(temporaryChannelId, c.fundingSatoshis, c.pushMsat, channelFeeratePerKw, fundingTxFeeratePerKw, localParams, d.peerConnection, d.remoteInit, channelFlags, channelConfig, channelType) stay() using d.copy(channels = d.channels + (TemporaryChannelId(temporaryChannelId) -> channel)) } @@ -165,12 +166,12 @@ class Peer(val nodeParams: NodeParams, remoteNodeId: PublicKey, wallet: OnChainA // remote explicitly specifies a channel type: we check whether we want to allow it case Some(remoteChannelType) => ChannelTypes.areCompatible(d.localFeatures, remoteChannelType) match { case Some(acceptedChannelType) => Right(acceptedChannelType) - case None => Left(InvalidChannelType(msg.temporaryChannelId, ChannelTypes.defaultFromFeatures(d.localFeatures, d.remoteFeatures), remoteChannelType)) + case None => Left(InvalidChannelType(msg.temporaryChannelId, ChannelTypes.defaultFromFeatures(d.localFeatures, d.remoteFeatures, msg.channelFlags.announceChannel), remoteChannelType)) } // Bolt 2: if `option_channel_type` is negotiated: MUST set `channel_type` case None if Features.canUseFeature(d.localFeatures, d.remoteFeatures, Features.ChannelType) => Left(MissingChannelType(msg.temporaryChannelId)) // remote doesn't specify a channel type: we use spec-defined defaults - case None => Right(ChannelTypes.defaultFromFeatures(d.localFeatures, d.remoteFeatures)) + case None => Right(ChannelTypes.defaultFromFeatures(d.localFeatures, d.remoteFeatures, msg.channelFlags.announceChannel)) } chosenChannelType match { case Right(channelType) => @@ -465,9 +466,10 @@ object Peer { case class Disconnect(nodeId: PublicKey) extends PossiblyHarmful case class OpenChannel(remoteNodeId: PublicKey, fundingSatoshis: Satoshi, pushMsat: MilliSatoshi, channelType_opt: Option[SupportedChannelType], fundingTxFeeratePerKw_opt: Option[FeeratePerKw], channelFlags: Option[ChannelFlags], timeout_opt: Option[Timeout]) extends PossiblyHarmful { - require(pushMsat <= fundingSatoshis, s"pushMsat must be less or equal to fundingSatoshis") - require(fundingSatoshis >= 0.sat, s"fundingSatoshis must be positive") - require(pushMsat >= 0.msat, s"pushMsat must be positive") + require(!(channelType_opt.exists(_.features.contains(Features.ScidAlias)) && channelFlags.exists(_.announceChannel)), "option_scid_alias is not compatible with public channels") + require(pushMsat <= fundingSatoshis, "pushMsat must be less or equal to fundingSatoshis") + require(fundingSatoshis >= 0.sat, "fundingSatoshis must be positive") + require(pushMsat >= 0.msat, "pushMsat must be positive") fundingTxFeeratePerKw_opt.foreach(feeratePerKw => require(feeratePerKw >= FeeratePerKw.MinimumFeeratePerKw, s"fee rate $feeratePerKw is below minimum ${FeeratePerKw.MinimumFeeratePerKw} rate/kw")) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/json/JsonSerializers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/json/JsonSerializers.scala index 5c44a25968..a03abfb410 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/json/JsonSerializers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/json/JsonSerializers.scala @@ -36,7 +36,7 @@ import fr.acinq.eclair.transactions.DirectedHtlc import fr.acinq.eclair.transactions.Transactions._ import fr.acinq.eclair.wire.protocol.MessageOnionCodecs.blindedRouteCodec import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, Feature, FeatureSupport, MilliSatoshi, ShortChannelId, TimestampMilli, TimestampSecond, UInt64, UnknownFeature} +import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, Feature, FeatureSupport, Alias, MilliSatoshi, ShortChannelId, TimestampMilli, TimestampSecond, UInt64, UnknownFeature, channel} import org.json4s import org.json4s.JsonAST._ import org.json4s.jackson.Serialization @@ -443,10 +443,14 @@ private[json] case class MessageReceivedJson(pathId: Option[ByteVector], encoded object OnionMessageReceivedSerializer extends ConvertClassSerializer[OnionMessages.ReceiveMessage](m => MessageReceivedJson(m.pathId, m.finalPayload.replyPath.map(route => blindedRouteCodec.encode(route.blindedRoute).require.bytes.toHex), m.finalPayload.replyPath.map(_.blindedRoute), m.finalPayload.records.unknown.map(tlv => tlv.tag.toString -> tlv.value).toMap)) // @formatter:on -case class CustomTypeHints(custom: Map[Class[_], String]) extends TypeHints { - val reverse: Map[String, Class[_]] = custom.map(_.swap) +// @formatter:off +/** this is cosmetic, just to not have a '_opt' field in json, which will only appear if the option is defined anyway */ +private case class ShortIdsJson(real: RealScidStatus, localAlias: Alias, remoteAlias: Option[ShortChannelId]) +object ShortIdsSerializer extends ConvertClassSerializer[ShortIds](s => ShortIdsJson(s.real, s.localAlias, s.remoteAlias_opt)) +// @formatter:on - override def typeHintFieldName: String = "type" +case class CustomTypeHints(custom: Map[Class[_], String], override val typeHintFieldName: String = "type") extends TypeHints { + val reverse: Map[String, Class[_]] = custom.map(_.swap) override val hints: List[Class[_]] = custom.keys.toList @@ -495,7 +499,7 @@ object CustomTypeHints { classOf[DATA_WAIT_FOR_FUNDING_INTERNAL], classOf[DATA_WAIT_FOR_FUNDING_CREATED], classOf[DATA_WAIT_FOR_FUNDING_SIGNED], - classOf[DATA_WAIT_FOR_FUNDING_LOCKED], + classOf[DATA_WAIT_FOR_CHANNEL_READY], classOf[DATA_WAIT_FOR_FUNDING_CONFIRMED], classOf[DATA_NORMAL], classOf[DATA_SHUTDOWN], @@ -503,6 +507,12 @@ object CustomTypeHints { classOf[DATA_CLOSING], classOf[DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT] ), typeHintFieldName = "type") + + val realScidStatuses: CustomTypeHints = CustomTypeHints(Map( + classOf[RealScidStatus.Unknown.type] -> "unknown", + classOf[RealScidStatus.Temporary] -> "temporary", + classOf[RealScidStatus.Final] -> "final", + ), typeHintFieldName = "status") } object JsonSerializers { @@ -516,6 +526,7 @@ object JsonSerializers { CustomTypeHints.onionMessageEvent + CustomTypeHints.channelSources + CustomTypeHints.channelStates + + CustomTypeHints.realScidStatuses + ByteVectorSerializer + ByteVector32Serializer + ByteVector64Serializer + @@ -562,6 +573,7 @@ object JsonSerializers { GlobalBalanceSerializer + PeerInfoSerializer + PaymentFailedSummarySerializer + - OnionMessageReceivedSerializer + OnionMessageReceivedSerializer + + ShortIdsSerializer } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/package.scala b/eclair-core/src/main/scala/fr/acinq/eclair/package.scala index cf87799532..8f5852e22b 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/package.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/package.scala @@ -16,12 +16,11 @@ package fr.acinq -import fr.acinq.bitcoin.{Base58, Base58Check, Bech32} import fr.acinq.bitcoin.scalacompat.Crypto.PrivateKey import fr.acinq.bitcoin.scalacompat._ +import fr.acinq.bitcoin.{Base58, Base58Check, Bech32} import fr.acinq.eclair.crypto.StrongRandom import fr.acinq.eclair.payment.relay.Relayer.RelayFees -import fr.acinq.eclair.wire.protocol.ChannelUpdate import scodec.Attempt import scodec.bits.{BitVector, ByteVector} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/ChannelRelay.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/ChannelRelay.scala index bcec91b1d0..3475b97f74 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/ChannelRelay.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/ChannelRelay.scala @@ -52,7 +52,8 @@ object ChannelRelay { Behaviors.withMdc(Logs.mdc( category_opt = Some(Logs.LogCategory.PAYMENT), parentPaymentId_opt = Some(relayId), // for a channel relay, parent payment id = relay id - paymentHash_opt = Some(r.add.paymentHash))) { + paymentHash_opt = Some(r.add.paymentHash), + nodeAlias_opt = Some(nodeParams.alias))) { context.self ! DoRelay new ChannelRelay(nodeParams, register, channels, r, context).relay(Seq.empty) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/ChannelRelayer.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/ChannelRelayer.scala index f874479681..741f508970 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/ChannelRelayer.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/ChannelRelayer.scala @@ -46,11 +46,13 @@ object ChannelRelayer { private[payment] case class WrappedLocalChannelUpdate(localChannelUpdate: LocalChannelUpdate) extends Command private[payment] case class WrappedLocalChannelDown(localChannelDown: LocalChannelDown) extends Command private[payment] case class WrappedAvailableBalanceChanged(availableBalanceChanged: AvailableBalanceChanged) extends Command - private[payment] case class WrappedShortChannelIdAssigned(shortChannelIdAssigned: ShortChannelIdAssigned) extends Command // @formatter:on def mdc: Command => Map[String, String] = { case c: Relay => Logs.mdc(paymentHash_opt = Some(c.channelRelayPacket.add.paymentHash)) + case c: WrappedLocalChannelUpdate => Logs.mdc(channelId_opt = Some(c.localChannelUpdate.channelId)) + case c: WrappedLocalChannelDown => Logs.mdc(channelId_opt = Some(c.localChannelDown.channelId)) + case c: WrappedAvailableBalanceChanged => Logs.mdc(channelId_opt = Some(c.availableBalanceChanged.channelId)) case _ => Map.empty } @@ -63,9 +65,8 @@ object ChannelRelayer { context.system.eventStream ! EventStream.Subscribe(context.messageAdapter[LocalChannelUpdate](WrappedLocalChannelUpdate)) context.system.eventStream ! EventStream.Subscribe(context.messageAdapter[LocalChannelDown](WrappedLocalChannelDown)) context.system.eventStream ! EventStream.Subscribe(context.messageAdapter[AvailableBalanceChanged](WrappedAvailableBalanceChanged)) - context.system.eventStream ! EventStream.Subscribe(context.messageAdapter[ShortChannelIdAssigned](WrappedShortChannelIdAssigned)) context.messageAdapter[IncomingPaymentPacket.ChannelRelayPacket](Relay) - Behaviors.withMdc(Logs.mdc(category_opt = Some(Logs.LogCategory.PAYMENT)), mdc) { + Behaviors.withMdc(Logs.mdc(category_opt = Some(Logs.LogCategory.PAYMENT), nodeAlias_opt = Some(nodeParams.alias)), mdc) { Behaviors.receiveMessage { case Relay(channelRelayPacket) => val relayId = UUID.randomUUID() @@ -90,35 +91,33 @@ object ChannelRelayer { replyTo ! Relayer.OutgoingChannels(selected.toSeq) Behaviors.same - case WrappedLocalChannelUpdate(LocalChannelUpdate(_, channelId, shortChannelId, remoteNodeId, _, channelUpdate, commitments)) => - context.log.debug(s"updating local channel info for channelId=$channelId shortChannelId=$shortChannelId remoteNodeId=$remoteNodeId channelUpdate={} commitments={}", channelUpdate, commitments) + case WrappedLocalChannelUpdate(lcu@LocalChannelUpdate(_, channelId, shortIds, remoteNodeId, _, channelUpdate, commitments)) => + context.log.debug(s"updating local channel info for channelId=$channelId realScid=${shortIds.real} localAlias=${shortIds.localAlias} remoteNodeId=$remoteNodeId channelUpdate={} commitments={}", channelUpdate, commitments) val prevChannelUpdate = channels.get(channelId).map(_.channelUpdate) val channel = Relayer.OutgoingChannel(remoteNodeId, channelUpdate, prevChannelUpdate, commitments) val channels1 = channels + (channelId -> channel) - val scid2channels1 = scid2channels + (channelUpdate.shortChannelId -> channelId) + val mappings = lcu.scidsForRouting.map(_ -> channelId).toMap + context.log.debug("adding mappings={} to channelId={}", mappings.keys.mkString(","), channelId) + val scid2channels1 = scid2channels ++ mappings val node2channels1 = node2channels.addOne(remoteNodeId, channelId) apply(nodeParams, register, channels1, scid2channels1, node2channels1) - case WrappedLocalChannelDown(LocalChannelDown(_, channelId, shortChannelId, remoteNodeId)) => - context.log.debug(s"removed local channel info for channelId=$channelId shortChannelId=$shortChannelId") + case WrappedLocalChannelDown(LocalChannelDown(_, channelId, shortIds, remoteNodeId)) => + context.log.debug(s"removed local channel info for channelId=$channelId localAlias=${shortIds.localAlias}") val channels1 = channels - channelId - val scid2Channels1 = scid2channels - shortChannelId + val scid2Channels1 = scid2channels - shortIds.localAlias -- shortIds.real.toOption val node2channels1 = node2channels.subtractOne(remoteNodeId, channelId) apply(nodeParams, register, channels1, scid2Channels1, node2channels1) - case WrappedAvailableBalanceChanged(AvailableBalanceChanged(_, channelId, shortChannelId, commitments)) => + case WrappedAvailableBalanceChanged(AvailableBalanceChanged(_, channelId, shortIds, commitments)) => val channels1 = channels.get(channelId) match { case Some(c: Relayer.OutgoingChannel) => - context.log.debug(s"available balance changed for channelId=$channelId shortChannelId=$shortChannelId availableForSend={} availableForReceive={}", commitments.availableBalanceForSend, commitments.availableBalanceForReceive) + context.log.debug(s"available balance changed for channelId=$channelId localAlias=${shortIds.localAlias} availableForSend={} availableForReceive={}", commitments.availableBalanceForSend, commitments.availableBalanceForReceive) channels + (channelId -> c.copy(commitments = commitments)) case None => channels // we only consider the balance if we have the channel_update } apply(nodeParams, register, channels1, scid2channels, node2channels) - case WrappedShortChannelIdAssigned(ShortChannelIdAssigned(_, channelId, shortChannelId, previousShortChannelId_opt)) => - context.log.debug(s"added new mapping shortChannelId=$shortChannelId for channelId=$channelId") - val scid2channels1 = scid2channels + (shortChannelId -> channelId) - apply(nodeParams, register, channels, scid2channels1, node2channels) } } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/remote/EclairInternalsSerializer.scala b/eclair-core/src/main/scala/fr/acinq/eclair/remote/EclairInternalsSerializer.scala index d0917fdf4b..d35bb6c37f 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/remote/EclairInternalsSerializer.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/remote/EclairInternalsSerializer.scala @@ -194,7 +194,7 @@ object EclairInternalsSerializer { .typecase(31, lengthPrefixedNodeAnnouncementCodec.as[NodeUpdated]) .typecase(32, publicKey.as[NodeLost]) .typecase(33, iterable(singleChannelDiscoveredCodec).as[ChannelsDiscovered]) - .typecase(34, shortchannelid.as[ChannelLost]) + .typecase(34, realshortchannelid.as[ChannelLost]) .typecase(35, iterable(lengthPrefixedChannelUpdateCodec).as[ChannelUpdatesReceived]) .typecase(36, double.as[SyncProgress]) .typecase(40, lengthPrefixedAnnouncementCodec.as[GossipDecision.Accepted]) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/Announcements.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/Announcements.scala index 83b8930183..da8381840c 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/Announcements.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/Announcements.scala @@ -18,6 +18,7 @@ package fr.acinq.eclair.router import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey, sha256, verifySignature} import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, Crypto, LexicographicalOrdering} +import fr.acinq.eclair.RealShortChannelId import fr.acinq.eclair.wire.protocol._ import fr.acinq.eclair.{CltvExpiryDelta, Feature, Features, MilliSatoshi, NodeFeature, ShortChannelId, TimestampSecond, TimestampSecondLong, serializationResult} import scodec.bits.ByteVector @@ -28,7 +29,7 @@ import shapeless.HNil */ object Announcements { - def channelAnnouncementWitnessEncode(chainHash: ByteVector32, shortChannelId: ShortChannelId, nodeId1: PublicKey, nodeId2: PublicKey, bitcoinKey1: PublicKey, bitcoinKey2: PublicKey, features: Features[Feature], tlvStream: TlvStream[ChannelAnnouncementTlv]): ByteVector = + def channelAnnouncementWitnessEncode(chainHash: ByteVector32, shortChannelId: RealShortChannelId, nodeId1: PublicKey, nodeId2: PublicKey, bitcoinKey1: PublicKey, bitcoinKey2: PublicKey, features: Features[Feature], tlvStream: TlvStream[ChannelAnnouncementTlv]): ByteVector = sha256(sha256(serializationResult(LightningMessageCodecs.channelAnnouncementWitnessCodec.encode(features :: chainHash :: shortChannelId :: nodeId1 :: nodeId2 :: bitcoinKey1 :: bitcoinKey2 :: tlvStream :: HNil)))) def nodeAnnouncementWitnessEncode(timestamp: TimestampSecond, nodeId: PublicKey, rgbColor: Color, alias: String, features: Features[Feature], addresses: List[NodeAddress], tlvStream: TlvStream[NodeAnnouncementTlv]): ByteVector = @@ -37,7 +38,7 @@ object Announcements { def channelUpdateWitnessEncode(chainHash: ByteVector32, shortChannelId: ShortChannelId, timestamp: TimestampSecond, channelFlags: ChannelUpdate.ChannelFlags, cltvExpiryDelta: CltvExpiryDelta, htlcMinimumMsat: MilliSatoshi, feeBaseMsat: MilliSatoshi, feeProportionalMillionths: Long, htlcMaximumMsat: Option[MilliSatoshi], tlvStream: TlvStream[ChannelUpdateTlv]): ByteVector = sha256(sha256(serializationResult(LightningMessageCodecs.channelUpdateWitnessCodec.encode(chainHash :: shortChannelId :: timestamp :: channelFlags :: cltvExpiryDelta :: htlcMinimumMsat :: feeBaseMsat :: feeProportionalMillionths :: htlcMaximumMsat :: tlvStream :: HNil)))) - def generateChannelAnnouncementWitness(chainHash: ByteVector32, shortChannelId: ShortChannelId, localNodeId: PublicKey, remoteNodeId: PublicKey, localFundingKey: PublicKey, remoteFundingKey: PublicKey, features: Features[Feature]): ByteVector = + def generateChannelAnnouncementWitness(chainHash: ByteVector32, shortChannelId: RealShortChannelId, localNodeId: PublicKey, remoteNodeId: PublicKey, localFundingKey: PublicKey, remoteFundingKey: PublicKey, features: Features[Feature]): ByteVector = if (isNode1(localNodeId, remoteNodeId)) { channelAnnouncementWitnessEncode(chainHash, shortChannelId, localNodeId, remoteNodeId, localFundingKey, remoteFundingKey, features, TlvStream.empty) } else { @@ -46,7 +47,7 @@ object Announcements { def signChannelAnnouncement(witness: ByteVector, key: PrivateKey): ByteVector64 = Crypto.sign(witness, key) - def makeChannelAnnouncement(chainHash: ByteVector32, shortChannelId: ShortChannelId, localNodeId: PublicKey, remoteNodeId: PublicKey, localFundingKey: PublicKey, remoteFundingKey: PublicKey, localNodeSignature: ByteVector64, remoteNodeSignature: ByteVector64, localBitcoinSignature: ByteVector64, remoteBitcoinSignature: ByteVector64): ChannelAnnouncement = { + def makeChannelAnnouncement(chainHash: ByteVector32, shortChannelId: RealShortChannelId, localNodeId: PublicKey, remoteNodeId: PublicKey, localFundingKey: PublicKey, remoteFundingKey: PublicKey, localNodeSignature: ByteVector64, remoteNodeSignature: ByteVector64, localBitcoinSignature: ByteVector64, remoteBitcoinSignature: ByteVector64): ChannelAnnouncement = { val (nodeId1, nodeId2, bitcoinKey1, bitcoinKey2, nodeSignature1, nodeSignature2, bitcoinSignature1, bitcoinSignature2) = if (isNode1(localNodeId, remoteNodeId)) { (localNodeId, remoteNodeId, localFundingKey, remoteFundingKey, localNodeSignature, remoteNodeSignature, localBitcoinSignature, remoteBitcoinSignature) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/Graph.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/Graph.scala index 2cf45c35e2..73c4fcca11 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/Graph.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/Graph.scala @@ -18,11 +18,11 @@ package fr.acinq.eclair.router import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.bitcoin.scalacompat.{Btc, MilliBtc, Satoshi, SatoshiLong} -import fr.acinq.eclair._ import fr.acinq.eclair.payment.relay.Relayer.RelayFees import fr.acinq.eclair.router.Graph.GraphStructure.{DirectedGraph, GraphEdge} import fr.acinq.eclair.router.Router._ import fr.acinq.eclair.wire.protocol.ChannelUpdate +import fr.acinq.eclair.{RealShortChannelId, _} import scala.annotation.tailrec import scala.collection.immutable.SortedMap @@ -310,8 +310,13 @@ object Graph { import RoutingHeuristics._ // Every edge is weighted by funding block height where older blocks add less weight. The window considered is 1 year. - val channelBlockHeight = ShortChannelId.coordinates(edge.desc.shortChannelId).blockHeight - val ageFactor = normalize(channelBlockHeight.toDouble, min = (currentBlockHeight - BLOCK_TIME_ONE_YEAR).toDouble, max = currentBlockHeight.toDouble) + val ageFactor = edge.desc.shortChannelId match { + case real: RealShortChannelId => normalize(real.blockHeight.toDouble, min = (currentBlockHeight - BLOCK_TIME_ONE_YEAR).toDouble, max = currentBlockHeight.toDouble) + // for local channels or route hints we don't easily have access to the channel block height, but we want to + // give them the best score anyway + case _: Alias => 1 + case _: UnspecifiedShortChannelId => 1 + } // Every edge is weighted by channel capacity, larger channels add less weight val edgeMaxCapacity = edge.capacity.toMilliSatoshi @@ -610,7 +615,7 @@ object Graph { * * @param channels map of all known public channels in the network. */ - def makeGraph(channels: SortedMap[ShortChannelId, PublicChannel]): DirectedGraph = { + def makeGraph(channels: SortedMap[RealShortChannelId, PublicChannel]): DirectedGraph = { // initialize the map with the appropriate size to avoid resizing during the graph initialization val mutableMap = new mutable.HashMap[PublicKey, List[GraphEdge]](initialCapacity = channels.size + 1, mutable.HashMap.defaultLoadFactor) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/NetworkEvents.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/NetworkEvents.scala index 8d781dc7b9..f960bf8d6b 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/NetworkEvents.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/NetworkEvents.scala @@ -19,6 +19,7 @@ package fr.acinq.eclair.router import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.bitcoin.scalacompat.Satoshi import fr.acinq.eclair.ShortChannelId +import fr.acinq.eclair.RealShortChannelId import fr.acinq.eclair.remote.EclairInternalsSerializer.RemoteTypes import fr.acinq.eclair.wire.protocol.{ChannelAnnouncement, ChannelUpdate, NodeAnnouncement} @@ -37,7 +38,7 @@ case class SingleChannelDiscovered(ann: ChannelAnnouncement, capacity: Satoshi, case class ChannelsDiscovered(c: Iterable[SingleChannelDiscovered]) extends NetworkEvent -case class ChannelLost(shortChannelId: ShortChannelId) extends NetworkEvent +case class ChannelLost(shortChannelId: RealShortChannelId) extends NetworkEvent case class ChannelUpdatesReceived(ann: Iterable[ChannelUpdate]) extends NetworkEvent diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/RouteCalculation.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/RouteCalculation.scala index 46cd566252..200a81ed2e 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/RouteCalculation.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/RouteCalculation.scala @@ -71,8 +71,8 @@ object RouteCalculation { case _ => None } case Some(c: PrivateChannel) => currentNode match { - case c.nodeId1 => Some(ChannelDesc(c.shortChannelId, c.nodeId1, c.nodeId2)) - case c.nodeId2 => Some(ChannelDesc(c.shortChannelId, c.nodeId2, c.nodeId1)) + case c.nodeId1 => Some(ChannelDesc(c.shortIds.localAlias, c.nodeId1, c.nodeId2)) + case c.nodeId2 => Some(ChannelDesc(c.shortIds.localAlias, c.nodeId2, c.nodeId1)) case _ => None } case None => assistedChannels.get(shortChannelId).flatMap(c => currentNode match { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala index 6580c17831..5755151d33 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala @@ -24,7 +24,6 @@ import akka.event.Logging.MDC import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.bitcoin.scalacompat.{ByteVector32, Satoshi} import fr.acinq.eclair.Logs.LogCategory -import fr.acinq.eclair.ShortChannelId.outputIndex import fr.acinq.eclair._ import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.{ValidateResult, WatchExternalChannelSpent, WatchExternalChannelSpentTriggered} @@ -40,7 +39,6 @@ import fr.acinq.eclair.router.Graph.GraphStructure.DirectedGraph import fr.acinq.eclair.router.Graph.{HeuristicsConstants, WeightRatios} import fr.acinq.eclair.router.Monitoring.Metrics import fr.acinq.eclair.wire.protocol._ -import kamon.context.Context import java.util.UUID import scala.collection.immutable.SortedMap @@ -60,6 +58,7 @@ class Router(val nodeParams: NodeParams, watcher: typed.ActorRef[ZmqWatcher.Comm // we pass these to helpers classes so that they have the logging context implicit def implicitLog: DiagnosticLoggingAdapter = diagLog + context.system.eventStream.subscribe(self, classOf[ShortChannelIdAssigned]) context.system.eventStream.subscribe(self, classOf[LocalChannelUpdate]) context.system.eventStream.subscribe(self, classOf[LocalChannelDown]) context.system.eventStream.subscribe(self, classOf[AvailableBalanceChanged]) @@ -229,14 +228,17 @@ class Router(val nodeParams: NodeParams, watcher: typed.ActorRef[ZmqWatcher.Comm case Event(PeerRoutingMessage(peerConnection, remoteNodeId, n: NodeAnnouncement), d: Data) => stay() using Validation.handleNodeAnnouncement(d, nodeParams.db.network, Set(RemoteGossip(peerConnection, remoteNodeId)), n) - case Event(u: ChannelUpdate, d: Data) => + case Event(scia: ShortChannelIdAssigned, d) => + stay() using Validation.handleShortChannelIdAssigned(d, nodeParams.nodeId, scia) + + case Event(u: ChannelUpdate, d: Data) => // from payment lifecycle stay() using Validation.handleChannelUpdate(d, nodeParams.db.network, nodeParams.routerConf, Right(RemoteChannelUpdate(u, Set(LocalGossip)))) - case Event(PeerRoutingMessage(peerConnection, remoteNodeId, u: ChannelUpdate), d) => + case Event(PeerRoutingMessage(peerConnection, remoteNodeId, u: ChannelUpdate), d) => // from network (gossip or peer) stay() using Validation.handleChannelUpdate(d, nodeParams.db.network, nodeParams.routerConf, Right(RemoteChannelUpdate(u, Set(RemoteGossip(peerConnection, remoteNodeId))))) - case Event(lcu: LocalChannelUpdate, d: Data) => - stay() using Validation.handleLocalChannelUpdate(d, nodeParams.db.network, nodeParams.routerConf, nodeParams.nodeId, watcher, lcu) + case Event(lcu: LocalChannelUpdate, d: Data) => // from local channel + stay() using Validation.handleLocalChannelUpdate(d, nodeParams, watcher, lcu) case Event(lcd: LocalChannelDown, d: Data) => stay() using Validation.handleLocalChannelDown(d, nodeParams.nodeId, lcd) @@ -275,21 +277,21 @@ class Router(val nodeParams: NodeParams, watcher: typed.ActorRef[ZmqWatcher.Comm override def mdc(currentMessage: Any): MDC = { val category_opt = LogCategory(currentMessage) - val remoteNodeId_opt = currentMessage match { - case s: SendChannelQuery => Some(s.remoteNodeId) - case prm: PeerRoutingMessage => Some(prm.remoteNodeId) - case lcu: LocalChannelUpdate => Some(lcu.remoteNodeId) - case _ => None + val (remoteNodeId_opt, channelId_opt) = currentMessage match { + case s: SendChannelQuery => (Some(s.remoteNodeId), None) + case prm: PeerRoutingMessage => (Some(prm.remoteNodeId), None) + case sca: ShortChannelIdAssigned => (Some(sca.remoteNodeId), Some(sca.channelId)) + case lcu: LocalChannelUpdate => (Some(lcu.remoteNodeId), Some(lcu.channelId)) + case lcd: LocalChannelDown => (Some(lcd.remoteNodeId), Some(lcd.channelId)) + case abc: AvailableBalanceChanged => (Some(abc.commitments.remoteNodeId), Some(abc.channelId)) + case _ => (None, None) } - Logs.mdc(category_opt, remoteNodeId_opt = remoteNodeId_opt, nodeAlias_opt = Some(nodeParams.alias)) + Logs.mdc(category_opt, remoteNodeId_opt = remoteNodeId_opt, channelId_opt = channelId_opt, nodeAlias_opt = Some(nodeParams.alias)) } } object Router { - val shortChannelIdKey = Context.key[ShortChannelId]("shortChannelId", ShortChannelId(0)) - val remoteNodeIdKey = Context.key[String]("remoteNodeId", "unknown") - def props(nodeParams: NodeParams, watcher: typed.ActorRef[ZmqWatcher.Command], initialized: Option[Promise[Done]] = None) = Props(new Router(nodeParams, watcher, initialized)) case class SearchBoundaries(maxFeeFlat: MilliSatoshi, @@ -335,7 +337,7 @@ object Router { } def apply(u: ChannelUpdate, pc: PrivateChannel): ChannelDesc = { // the least significant bit tells us if it is node1 or node2 - if (u.channelFlags.isNode1) ChannelDesc(u.shortChannelId, pc.nodeId1, pc.nodeId2) else ChannelDesc(u.shortChannelId, pc.nodeId2, pc.nodeId1) + if (u.channelFlags.isNode1) ChannelDesc(pc.shortIds.localAlias, pc.nodeId1, pc.nodeId2) else ChannelDesc(pc.shortIds.localAlias, pc.nodeId2, pc.nodeId1) } } case class ChannelMeta(balance1: MilliSatoshi, balance2: MilliSatoshi) @@ -356,8 +358,8 @@ object Router { val nodeId1: PublicKey = ann.nodeId1 val nodeId2: PublicKey = ann.nodeId2 - def shortChannelId: ShortChannelId = ann.shortChannelId - def channelId: ByteVector32 = toLongId(fundingTxid.reverse, outputIndex(ann.shortChannelId)) + def shortChannelId: RealShortChannelId = ann.shortChannelId + def channelId: ByteVector32 = toLongId(fundingTxid.reverse, ann.shortChannelId.outputIndex) def getNodeIdSameSideAs(u: ChannelUpdate): PublicKey = if (u.channelFlags.isNode1) ann.nodeId1 else ann.nodeId2 def getChannelUpdateSameSideAs(u: ChannelUpdate): Option[ChannelUpdate] = if (u.channelFlags.isNode1) update_1_opt else update_2_opt def getBalanceSameSideAs(u: ChannelUpdate): Option[MilliSatoshi] = if (u.channelFlags.isNode1) meta_opt.map(_.balance1) else meta_opt.map(_.balance2) @@ -372,7 +374,7 @@ object Router { case Right(rcu) => updateChannelUpdateSameSideAs(rcu.channelUpdate) } } - case class PrivateChannel(shortChannelId: ShortChannelId, channelId: ByteVector32, localNodeId: PublicKey, remoteNodeId: PublicKey, update_1_opt: Option[ChannelUpdate], update_2_opt: Option[ChannelUpdate], meta: ChannelMeta) extends KnownChannel { + case class PrivateChannel(channelId: ByteVector32, shortIds: ShortIds, localNodeId: PublicKey, remoteNodeId: PublicKey, update_1_opt: Option[ChannelUpdate], update_2_opt: Option[ChannelUpdate], meta: ChannelMeta) extends KnownChannel { val (nodeId1, nodeId2) = if (Announcements.isNode1(localNodeId, remoteNodeId)) (localNodeId, remoteNodeId) else (remoteNodeId, localNodeId) val capacity: Satoshi = (meta.balance1 + meta.balance2).truncateToSatoshi @@ -393,9 +395,14 @@ object Router { def toIncomingExtraHop: Option[ExtraHop] = { // we want the incoming channel_update val remoteUpdate_opt = if (localNodeId == nodeId1) update_2_opt else update_1_opt - remoteUpdate_opt.map { remoteUpdate => - ExtraHop(remoteNodeId, remoteUpdate.shortChannelId, remoteUpdate.feeBaseMsat, remoteUpdate.feeProportionalMillionths, remoteUpdate.cltvExpiryDelta) + // for incoming payments we preferably use the *remote alias*, otherwise the real scid if we have it + val scid_opt = shortIds.remoteAlias_opt.orElse(shortIds.real.toOption) + // we override the remote update's scid, because it contains either the real scid or our local alias + scid_opt.flatMap { scid => + remoteUpdate_opt.map { remoteUpdate => + ExtraHop(remoteNodeId, scid, remoteUpdate.feeBaseMsat, remoteUpdate.feeProportionalMillionths, remoteUpdate.cltvExpiryDelta) } + } } } // @formatter:on @@ -626,7 +633,7 @@ object Router { case class Rebroadcast(channels: Map[ChannelAnnouncement, Set[GossipOrigin]], updates: Map[ChannelUpdate, Set[GossipOrigin]], nodes: Map[NodeAnnouncement, Set[GossipOrigin]]) // @formatter:on - case class ShortChannelIdAndFlag(shortChannelId: ShortChannelId, flag: Long) + case class ShortChannelIdAndFlag(shortChannelId: RealShortChannelId, flag: Long) /** * @param remainingQueries remaining queries to send, the next one will be popped after we receive a [[ReplyShortChannelIdsEnd]] @@ -637,28 +644,32 @@ object Router { } case class Data(nodes: Map[PublicKey, NodeAnnouncement], - channels: SortedMap[ShortChannelId, PublicChannel], + channels: SortedMap[RealShortChannelId, PublicChannel], stash: Stash, rebroadcast: Rebroadcast, awaiting: Map[ChannelAnnouncement, Seq[GossipOrigin]], // note: this is a seq because we want to preserve order: first actor is the one who we need to send a tcp-ack when validation is done privateChannels: Map[ByteVector32, PrivateChannel], // indexed by channel id - scid2PrivateChannels: Map[ShortChannelId, ByteVector32], // scid to channel_id, only to be used for private channels + scid2PrivateChannels: Map[Long, ByteVector32], // real scid or alias to channel_id, only to be used for private channels excludedChannels: Set[ChannelDesc], // those channels are temporarily excluded from route calculation, because their node returned a TemporaryChannelFailure graphWithBalances: GraphWithBalanceEstimates, sync: Map[PublicKey, Syncing] // keep tracks of channel range queries sent to each peer. If there is an entry in the map, it means that there is an ongoing query for which we have not yet received an 'end' message ) { def resolve(scid: ShortChannelId): Option[KnownChannel] = { // let's assume this is a real scid - channels.get(scid) match { + channels.get(RealShortChannelId(scid.toLong)) match { case Some(publicChannel) => Some(publicChannel) case None => // maybe it's an alias or a real scid - scid2PrivateChannels.get(scid).flatMap(privateChannels.get) match { + scid2PrivateChannels.get(scid.toLong).flatMap(privateChannels.get) match { case Some(privateChannel) => Some(privateChannel) case None => None } } } + + def resolve(channelId: ByteVector32, realScid_opt: Option[RealShortChannelId]): Option[KnownChannel] = { + privateChannels.get(channelId).orElse(realScid_opt.flatMap(channels.get)) + } } // @formatter:off diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/StaleChannels.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/StaleChannels.scala index 91ce0d13f2..8905989562 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/StaleChannels.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/StaleChannels.scala @@ -21,7 +21,7 @@ import akka.event.LoggingAdapter import fr.acinq.eclair.db.NetworkDb import fr.acinq.eclair.router.Router.{ChannelDesc, Data, PublicChannel, hasChannels} import fr.acinq.eclair.wire.protocol.{ChannelAnnouncement, ChannelUpdate} -import fr.acinq.eclair.{BlockHeight, ShortChannelId, TimestampSecond, TxCoordinates} +import fr.acinq.eclair.{BlockHeight, RealShortChannelId, ShortChannelId, TimestampSecond, TxCoordinates} import scala.collection.mutable import scala.concurrent.duration._ diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/Sync.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/Sync.scala index 63e65b3dcc..4eadbb4988 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/Sync.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/Sync.scala @@ -20,6 +20,7 @@ import akka.actor.{ActorContext, ActorRef} import akka.event.LoggingAdapter import fr.acinq.bitcoin.scalacompat.ByteVector32 import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey +import fr.acinq.eclair.RealShortChannelId import fr.acinq.eclair.crypto.TransportHandler import fr.acinq.eclair.router.Monitoring.{Metrics, Tags} import fr.acinq.eclair.router.Router._ @@ -69,13 +70,13 @@ object Sync { } } - def handleQueryChannelRange(channels: SortedMap[ShortChannelId, PublicChannel], routerConf: RouterConf, origin: RemoteGossip, q: QueryChannelRange)(implicit ctx: ActorContext, log: LoggingAdapter): Unit = { + def handleQueryChannelRange(channels: SortedMap[RealShortChannelId, PublicChannel], routerConf: RouterConf, origin: RemoteGossip, q: QueryChannelRange)(implicit ctx: ActorContext, log: LoggingAdapter): Unit = { implicit val sender: ActorRef = ctx.self // necessary to preserve origin when sending messages to other actors ctx.sender() ! TransportHandler.ReadAck(q) Metrics.QueryChannelRange.Blocks.withoutTags().record(q.numberOfBlocks) log.info("received query_channel_range with firstBlockNum={} numberOfBlocks={} extendedQueryFlags_opt={}", q.firstBlock, q.numberOfBlocks, q.tlvStream) // keep channel ids that are in [firstBlockNum, firstBlockNum + numberOfBlocks] - val shortChannelIds: SortedSet[ShortChannelId] = channels.keySet.filter(keep(q.firstBlock, q.numberOfBlocks, _)) + val shortChannelIds: SortedSet[RealShortChannelId] = channels.keySet.filter(keep(q.firstBlock, q.numberOfBlocks, _)) log.info("replying with {} items for range=({}, {})", shortChannelIds.size, q.firstBlock, q.numberOfBlocks) val chunks = split(shortChannelIds, q.firstBlock, q.numberOfBlocks, routerConf.channelRangeChunkSize) Metrics.QueryChannelRange.Replies.withoutTags().record(chunks.size) @@ -107,7 +108,7 @@ object Sync { Metrics.ReplyChannelRange.ShortChannelIds.withTag(Tags.Direction, Tags.Directions.Incoming).record(r.shortChannelIds.array.size) @tailrec - def loop(ids: List[ShortChannelId], timestamps: List[ReplyChannelRangeTlv.Timestamps], checksums: List[ReplyChannelRangeTlv.Checksums], acc: List[ShortChannelIdAndFlag] = List.empty[ShortChannelIdAndFlag]): List[ShortChannelIdAndFlag] = { + def loop(ids: List[RealShortChannelId], timestamps: List[ReplyChannelRangeTlv.Timestamps], checksums: List[ReplyChannelRangeTlv.Checksums], acc: List[ShortChannelIdAndFlag] = List.empty[ShortChannelIdAndFlag]): List[ShortChannelIdAndFlag] = { ids match { case Nil => acc.reverse case head :: tail => @@ -158,7 +159,7 @@ object Sync { } } - def handleQueryShortChannelIds(nodes: Map[PublicKey, NodeAnnouncement], channels: SortedMap[ShortChannelId, PublicChannel], origin: RemoteGossip, q: QueryShortChannelIds)(implicit ctx: ActorContext, log: LoggingAdapter): Unit = { + def handleQueryShortChannelIds(nodes: Map[PublicKey, NodeAnnouncement], channels: SortedMap[RealShortChannelId, PublicChannel], origin: RemoteGossip, q: QueryShortChannelIds)(implicit ctx: ActorContext, log: LoggingAdapter): Unit = { implicit val sender: ActorRef = ctx.self // necessary to preserve origin when sending messages to other actors ctx.sender() ! TransportHandler.ReadAck(q) @@ -218,7 +219,7 @@ object Sync { /** * Filters channels that we want to send to nodes asking for a channel range */ - def keep(firstBlock: BlockHeight, numberOfBlocks: Long, id: ShortChannelId): Boolean = { + def keep(firstBlock: BlockHeight, numberOfBlocks: Long, id: RealShortChannelId): Boolean = { val height = id.blockHeight height >= firstBlock && height < (firstBlock + numberOfBlocks) } @@ -251,8 +252,8 @@ object Sync { } } - def computeFlag(channels: SortedMap[ShortChannelId, PublicChannel])( - shortChannelId: ShortChannelId, + def computeFlag(channels: SortedMap[RealShortChannelId, PublicChannel])( + shortChannelId: RealShortChannelId, theirTimestamps_opt: Option[ReplyChannelRangeTlv.Timestamps], theirChecksums_opt: Option[ReplyChannelRangeTlv.Checksums], includeNodeAnnouncements: Boolean): Long = { @@ -291,8 +292,8 @@ object Sync { * */ def processChannelQuery(nodes: Map[PublicKey, NodeAnnouncement], - channels: SortedMap[ShortChannelId, PublicChannel])( - ids: List[ShortChannelId], + channels: SortedMap[RealShortChannelId, PublicChannel])( + ids: List[RealShortChannelId], flags: List[Long], onChannel: ChannelAnnouncement => Unit, onUpdate: ChannelUpdate => Unit, @@ -302,7 +303,7 @@ object Sync { // we loop over channel ids and query flag. We track node Ids for node announcement // we've already sent to avoid sending them multiple times, as requested by the BOLTs @tailrec - def loop(ids: List[ShortChannelId], flags: List[Long], numca: Int = 0, numcu: Int = 0, nodesSent: Set[PublicKey] = Set.empty[PublicKey]): (Int, Int, Int) = ids match { + def loop(ids: List[RealShortChannelId], flags: List[Long], numca: Int = 0, numcu: Int = 0, nodesSent: Set[PublicKey] = Set.empty[PublicKey]): (Int, Int, Int) = ids match { case Nil => (numca, numcu, nodesSent.size) case head :: tail if !channels.contains(head) => log.warning("received query for shortChannelId={} that we don't have", head) @@ -369,7 +370,7 @@ object Sync { } } - def getChannelDigestInfo(channels: SortedMap[ShortChannelId, PublicChannel])(shortChannelId: ShortChannelId): (ReplyChannelRangeTlv.Timestamps, ReplyChannelRangeTlv.Checksums) = { + def getChannelDigestInfo(channels: SortedMap[RealShortChannelId, PublicChannel])(shortChannelId: RealShortChannelId): (ReplyChannelRangeTlv.Timestamps, ReplyChannelRangeTlv.Checksums) = { val c = channels(shortChannelId) val timestamp1 = c.update_1_opt.map(_.timestamp).getOrElse(0L unixsec) val timestamp2 = c.update_2_opt.map(_.timestamp).getOrElse(0L unixsec) @@ -390,7 +391,7 @@ object Sync { crc32c(data) } - case class ShortChannelIdsChunk(firstBlock: BlockHeight, numBlocks: Long, shortChannelIds: List[ShortChannelId]) { + case class ShortChannelIdsChunk(firstBlock: BlockHeight, numBlocks: Long, shortChannelIds: List[RealShortChannelId]) { /** * @param maximumSize maximum size of the short channel ids list * @return a chunk with at most `maximumSize` ids @@ -419,7 +420,7 @@ object Sync { * returned chunks may still contain more than `channelRangeChunkSize` elements * @return a list of short channel id chunks */ - def split(shortChannelIds: SortedSet[ShortChannelId], firstBlock: BlockHeight, numberOfBlocks: Long, channelRangeChunkSize: Int): List[ShortChannelIdsChunk] = { + def split(shortChannelIds: SortedSet[RealShortChannelId], firstBlock: BlockHeight, numberOfBlocks: Long, channelRangeChunkSize: Int): List[ShortChannelIdsChunk] = { // see BOLT7: MUST encode a short_channel_id for every open channel it knows in blocks first_blocknum to first_blocknum plus number_of_blocks minus one val it = shortChannelIds.iterator.dropWhile(_.blockHeight < firstBlock).takeWhile(_.blockHeight < firstBlock + numberOfBlocks) if (it.isEmpty) { @@ -429,7 +430,7 @@ object Sync { // ids that have the same block height must be grouped in the same chunk // chunk should contain `channelRangeChunkSize` ids @tailrec - def loop(currentChunk: List[ShortChannelId], acc: List[ShortChannelIdsChunk]): List[ShortChannelIdsChunk] = { + def loop(currentChunk: List[RealShortChannelId], acc: List[ShortChannelIdsChunk]): List[ShortChannelIdsChunk] = { if (it.hasNext) { val id = it.next() val currentHeight = currentChunk.head.blockHeight @@ -481,7 +482,7 @@ object Sync { * @param channels channels map * @return a ReplyChannelRange object */ - def buildReplyChannelRange(chunk: ShortChannelIdsChunk, syncComplete: Boolean, chainHash: ByteVector32, defaultEncoding: EncodingType, queryFlags_opt: Option[QueryChannelRangeTlv.QueryFlags], channels: SortedMap[ShortChannelId, PublicChannel]): ReplyChannelRange = { + def buildReplyChannelRange(chunk: ShortChannelIdsChunk, syncComplete: Boolean, chainHash: ByteVector32, defaultEncoding: EncodingType, queryFlags_opt: Option[QueryChannelRangeTlv.QueryFlags], channels: SortedMap[RealShortChannelId, PublicChannel]): ReplyChannelRange = { val encoding = if (chunk.shortChannelIds.isEmpty) EncodingType.UNCOMPRESSED else defaultEncoding val (timestamps, checksums) = queryFlags_opt match { case Some(extension) if extension.wantChecksums | extension.wantTimestamps => diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/Validation.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/Validation.scala index a218504a1e..b173ceb2a7 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/Validation.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/Validation.scala @@ -21,9 +21,11 @@ import akka.actor.{ActorContext, ActorRef, typed} import akka.event.{DiagnosticLoggingAdapter, LoggingAdapter} import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.bitcoin.scalacompat.Script.{pay2wsh, write} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, Satoshi} +import fr.acinq.eclair.ShortChannelId.outputIndex import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.{UtxoStatus, ValidateRequest, ValidateResult, WatchExternalChannelSpent} -import fr.acinq.eclair.channel.{AvailableBalanceChanged, LocalChannelDown, LocalChannelUpdate} +import fr.acinq.eclair.channel._ import fr.acinq.eclair.crypto.TransportHandler import fr.acinq.eclair.db.NetworkDb import fr.acinq.eclair.router.Graph.GraphStructure.GraphEdge @@ -31,7 +33,7 @@ import fr.acinq.eclair.router.Monitoring.Metrics import fr.acinq.eclair.router.Router._ import fr.acinq.eclair.transactions.Scripts import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{Logs, MilliSatoshiLong, NodeParams, ShortChannelId, TxCoordinates, toLongId} +import fr.acinq.eclair.{Logs, MilliSatoshiLong, NodeParams, RealShortChannelId, ShortChannelId, TxCoordinates, toLongId} object Validation { @@ -79,7 +81,6 @@ object Validation { def handleChannelValidationResponse(d0: Data, nodeParams: NodeParams, watcher: typed.ActorRef[ZmqWatcher.Command], r: ValidateResult)(implicit ctx: ActorContext, log: DiagnosticLoggingAdapter): Data = { implicit val sender: ActorRef = ctx.self // necessary to preserve origin when sending messages to other actors - import nodeParams.db.{network => db} import r.c // now we can acknowledge the message, we only need to do it for the first peer that sent us the announcement // (the other ones have already been acknowledged as duplicates) @@ -91,7 +92,7 @@ object Validation { val remoteOrigins = d0.awaiting.getOrElse(c, Set.empty).collect { case rg: RemoteGossip => rg } Logs.withMdc(log)(Logs.mdc(remoteNodeId_opt = remoteOrigins.headOption.map(_.nodeId))) { // in the MDC we use the node id that sent us the announcement first log.debug("got validation result for shortChannelId={} (awaiting={} stash.nodes={} stash.updates={})", c.shortChannelId, d0.awaiting.size, d0.stash.nodes.size, d0.stash.updates.size) - val publicChannel_opt = r match { + val d1_opt = r match { case ValidateResult(c, Left(t)) => log.warning("validation failure for shortChannelId={} reason={}", c.shortChannelId, t.getMessage) remoteOrigins.foreach(o => sendDecision(o.peerConnection, GossipDecision.ValidationFailure(c))) @@ -109,39 +110,22 @@ object Validation { remoteOrigins.foreach(o => sendDecision(o.peerConnection, GossipDecision.InvalidAnnouncement(c))) None } else { - watcher ! WatchExternalChannelSpent(ctx.self, tx.txid, outputIndex, c.shortChannelId) - log.debug("added channel channelId={}", c.shortChannelId) + log.debug("validation successful for shortChannelId={}", c.shortChannelId) remoteOrigins.foreach(o => sendDecision(o.peerConnection, GossipDecision.Accepted(c))) val capacity = tx.txOut(outputIndex).amount - ctx.system.eventStream.publish(ChannelsDiscovered(SingleChannelDiscovered(c, capacity, None, None) :: Nil)) - db.addChannel(c, tx.txid, capacity) - // in case we just validated our first local channel, we announce the local node - if (!d0.nodes.contains(nodeParams.nodeId) && isRelatedTo(c, nodeParams.nodeId)) { - log.info("first local channel validated, announcing local node") - val nodeAnn = Announcements.makeNodeAnnouncement(nodeParams.privateKey, nodeParams.alias, nodeParams.color, nodeParams.publicAddresses, nodeParams.features.nodeAnnouncementFeatures()) - ctx.self ! nodeAnn - } - // maybe this previously was a local unannounced channel - val channelId = toLongId(tx.txid.reverse, outputIndex) - val privateChannel_opt = d0.privateChannels.get(channelId) - Some(PublicChannel(c, - tx.txid, - capacity, - update_1_opt = privateChannel_opt.flatMap(_.update_1_opt), - update_2_opt = privateChannel_opt.flatMap(_.update_2_opt), - meta_opt = privateChannel_opt.map(_.meta))) + Some(addPublicChannel(d0, nodeParams, watcher, c, fundingTxid = tx.txid, capacity = capacity)) } case ValidateResult(c, Right((tx, fundingTxStatus: UtxoStatus.Spent))) => if (fundingTxStatus.spendingTxConfirmed) { - log.debug("ignoring shortChannelId={} tx={} (funding tx already spent and spending tx is confirmed)", c.shortChannelId, tx.txid) + log.debug("ignoring shortChannelId={} txid={} (funding tx already spent and spending tx is confirmed)", c.shortChannelId, tx.txid) // the funding tx has been spent by a transaction that is now confirmed: peer shouldn't send us those remoteOrigins.foreach(o => sendDecision(o.peerConnection, GossipDecision.ChannelClosed(c))) } else { - log.debug("ignoring shortChannelId={} tx={} (funding tx already spent but spending tx isn't confirmed)", c.shortChannelId, tx.txid) + log.debug("ignoring shortChannelId={} txid={} (funding tx already spent but spending tx isn't confirmed)", c.shortChannelId, tx.txid) remoteOrigins.foreach(o => sendDecision(o.peerConnection, GossipDecision.ChannelClosing(c))) } // there may be a record if we have just restarted - db.removeChannel(c.shortChannelId) + nodeParams.db.network.removeChannel(c.shortChannelId) None } // we also reprocess node and channel_update announcements related to the channel that was just analyzed @@ -152,30 +136,19 @@ object Validation { // we remove channel from awaiting map val awaiting1 = d0.awaiting - c - publicChannel_opt match { - case Some(pc) => - // those updates are only defined if this was a previously an unannounced local channel, we broadcast them if they use the real scid - val updates1 = (pc.update_1_opt.toSet ++ pc.update_2_opt.toSet) - .map(u => u -> (if (pc.getNodeIdSameSideAs(u) == nodeParams.nodeId) Set[GossipOrigin](LocalGossip) else Set.empty[GossipOrigin])) - .toMap - val d1 = d0.copy( - channels = d0.channels + (c.shortChannelId -> pc), - privateChannels = d0.privateChannels - pc.channelId, // we remove the corresponding unannounced channel that we may have until now - rebroadcast = d0.rebroadcast.copy( - channels = d0.rebroadcast.channels + (c -> d0.awaiting.getOrElse(c, Nil).toSet), // we rebroadcast the channel to our peers - updates = d0.rebroadcast.updates ++ updates1 - ), // we also add the newly validated channels to the rebroadcast queue - stash = stash1, - awaiting = awaiting1) - // we only reprocess updates and nodes if validation succeeded - val d2 = reprocessUpdates.foldLeft(d1) { + d1_opt match { + case Some(d1) => + val d2 = d1.copy(stash = stash1, awaiting = awaiting1) + // we process channel updates and node announcements if validation succeeded + val d3 = reprocessUpdates.foldLeft(d2) { case (d, (u, origins)) => Validation.handleChannelUpdate(d, nodeParams.db.network, nodeParams.routerConf, Right(RemoteChannelUpdate(u, origins)), wasStashed = true) } - val d3 = reprocessNodes.foldLeft(d2) { + val d4 = reprocessNodes.foldLeft(d3) { case (d, (n, origins)) => Validation.handleNodeAnnouncement(d, nodeParams.db.network, origins, n, wasStashed = true) } - d3 + d4 case None => + // if validation failed we can fast-discard related announcements reprocessUpdates.foreach { case (u, origins) => origins.collect { case o: RemoteGossip => sendDecision(o.peerConnection, GossipDecision.NoRelatedChannel(u)) } } reprocessNodes.foreach { case (n, origins) => origins.collect { case o: RemoteGossip => sendDecision(o.peerConnection, GossipDecision.NoKnownChannel(n)) } } d0.copy(stash = stash1, awaiting = awaiting1) @@ -183,12 +156,74 @@ object Validation { } } - def handleChannelSpent(d: Data, db: NetworkDb, shortChannelId: ShortChannelId)(implicit ctx: ActorContext, log: LoggingAdapter): Data = { + private def addPublicChannel(d: Data, nodeParams: NodeParams, watcher: typed.ActorRef[ZmqWatcher.Command], ann: ChannelAnnouncement, fundingTxid: ByteVector32, capacity: Satoshi)(implicit ctx: ActorContext, log: DiagnosticLoggingAdapter): Data = { + implicit val sender: ActorRef = ctx.self // necessary to preserve origin when sending messages to other actors + val fundingOutputIndex = outputIndex(ann.shortChannelId) + val channelId = toLongId(fundingTxid.reverse, fundingOutputIndex) + watcher ! WatchExternalChannelSpent(ctx.self, fundingTxid, fundingOutputIndex, ann.shortChannelId) + ctx.system.eventStream.publish(ChannelsDiscovered(SingleChannelDiscovered(ann, capacity, None, None) :: Nil)) + nodeParams.db.network.addChannel(ann, fundingTxid, capacity) + // if this is a local channel graduating from private to public, we already have data + val privChan_opt = d.privateChannels.get(channelId) + val pubChan = PublicChannel( + ann = ann, + fundingTxid = fundingTxid, + capacity = capacity, + update_1_opt = privChan_opt.flatMap(_.update_1_opt), + update_2_opt = privChan_opt.flatMap(_.update_2_opt), + meta_opt = privChan_opt.map(_.meta) + ) + log.debug("adding public channel channelId={} realScid={} localChannel={} publicChannel={}", channelId, ann.shortChannelId, privChan_opt.isDefined, pubChan) + // if this is a local channel graduating from private to public, we need to update the graph because the edge + // identifiers change from alias to real scid, and we can also populate the metadata + val graph1 = privChan_opt match { + case Some(privateChannel) => + log.debug("updating the graph for shortChannelId={}", pubChan.shortChannelId) + // mutable variable is simpler here + var graph = d.graphWithBalances + // remove previous private edges + pubChan.update_1_opt.foreach(u => graph = graph.removeEdge(ChannelDesc(u, privateChannel))) + pubChan.update_2_opt.foreach(u => graph = graph.removeEdge(ChannelDesc(u, privateChannel))) + // add new public edges + pubChan.update_1_opt.foreach(u => graph = graph.addEdge(GraphEdge(u, pubChan))) + pubChan.update_2_opt.foreach(u => graph = graph.addEdge(GraphEdge(u, pubChan))) + graph + case None => d.graphWithBalances + } + // those updates are only defined if this was a previously an unannounced local channel, we broadcast them if they use the real scid + val rebroadcastUpdates1 = (pubChan.update_1_opt.toSet ++ pubChan.update_2_opt.toSet) + .filter(_.shortChannelId == pubChan.shortChannelId) + .map(u => u -> (if (pubChan.getNodeIdSameSideAs(u) == nodeParams.nodeId) Set[GossipOrigin](LocalGossip) else Set.empty[GossipOrigin])) + .toMap + val d1 = d.copy( + channels = d.channels + (pubChan.shortChannelId -> pubChan), + // we remove the corresponding unannounced channel that we may have until now + privateChannels = d.privateChannels - pubChan.channelId, + // we also remove the scid -> channelId mappings + scid2PrivateChannels = d.scid2PrivateChannels - pubChan.shortChannelId.toLong -- privChan_opt.map(_.shortIds.localAlias.toLong), + // we also add the newly validated channels to the rebroadcast queue + rebroadcast = d.rebroadcast.copy( + // we rebroadcast the channel to our peers + channels = d.rebroadcast.channels + (pubChan.ann -> d.awaiting.getOrElse(pubChan.ann, if (pubChan.nodeId1 == nodeParams.nodeId || pubChan.nodeId2 == nodeParams.nodeId) Seq(LocalGossip) else Nil).toSet), + // those updates are only defined if the channel was previously an unannounced local channel, we broadcast them + updates = d.rebroadcast.updates ++ rebroadcastUpdates1 + ), + graphWithBalances = graph1 + ) + // in case this was our first local channel, we make a node announcement + if (!d.nodes.contains(nodeParams.nodeId) && isRelatedTo(ann, nodeParams.nodeId)) { + log.info("first local channel validated, announcing local node") + val nodeAnn = Announcements.makeNodeAnnouncement(nodeParams.privateKey, nodeParams.alias, nodeParams.color, nodeParams.publicAddresses, nodeParams.features.nodeAnnouncementFeatures()) + handleNodeAnnouncement(d1, nodeParams.db.network, Set(LocalGossip), nodeAnn) + } else d1 + } + + def handleChannelSpent(d: Data, db: NetworkDb, shortChannelId: RealShortChannelId)(implicit ctx: ActorContext, log: LoggingAdapter): Data = { implicit val sender: ActorRef = ctx.self // necessary to preserve origin when sending messages to other actors val lostChannel = d.channels(shortChannelId).ann log.info("funding tx of channelId={} has been spent", shortChannelId) // we need to remove nodes that aren't tied to any channels anymore - val channels1 = d.channels - lostChannel.shortChannelId + val channels1 = d.channels - shortChannelId val lostNodes = Seq(lostChannel.nodeId1, lostChannel.nodeId2).filterNot(nodeId => hasChannels(nodeId, channels1.values)) // let's clean the db and send the events log.info("pruning shortChannelId={} (spent)", shortChannelId) @@ -205,12 +240,12 @@ object Validation { db.removeNode(nodeId) ctx.system.eventStream.publish(NodeLost(nodeId)) } - d.copy(nodes = d.nodes -- lostNodes, channels = d.channels - shortChannelId, graphWithBalances = graphWithBalances1) + d.copy(nodes = d.nodes -- lostNodes, channels = channels1, graphWithBalances = graphWithBalances1) } def handleNodeAnnouncement(d: Data, db: NetworkDb, origins: Set[GossipOrigin], n: NodeAnnouncement, wasStashed: Boolean = false)(implicit ctx: ActorContext, log: LoggingAdapter): Data = { implicit val sender: ActorRef = ctx.self // necessary to preserve origin when sending messages to other actors - val remoteOrigins = origins flatMap { + val remoteOrigins = origins.flatMap { case r: RemoteGossip if wasStashed => Some(r.peerConnection) case RemoteGossip(peerConnection, _) => @@ -230,7 +265,7 @@ object Validation { remoteOrigins.foreach(sendDecision(_, GossipDecision.Accepted(n))) val origins1 = d.rebroadcast.nodes(n) ++ origins d.copy(rebroadcast = d.rebroadcast.copy(nodes = d.rebroadcast.nodes + (n -> origins1))) - } else if (d.nodes.contains(n.nodeId) && d.nodes(n.nodeId).timestamp >= n.timestamp) { + } else if (d.nodes.get(n.nodeId).exists(_.timestamp >= n.timestamp)) { log.debug("ignoring {} (duplicate)", n) remoteOrigins.foreach(sendDecision(_, GossipDecision.Duplicate(n))) d @@ -265,7 +300,7 @@ object Validation { def handleChannelUpdate(d: Data, db: NetworkDb, routerConf: RouterConf, update: Either[LocalChannelUpdate, RemoteChannelUpdate], wasStashed: Boolean = false)(implicit ctx: ActorContext, log: LoggingAdapter): Data = { implicit val sender: ActorRef = ctx.self // necessary to preserve origin when sending messages to other actors val (pc_opt: Option[KnownChannel], u: ChannelUpdate, origins: Set[GossipOrigin]) = update match { - case Left(lcu) => (d.resolve(lcu.shortChannelId), lcu.channelUpdate, Set(LocalGossip)) + case Left(lcu) => (d.resolve(lcu.channelId, lcu.shortIds.real.toOption), lcu.channelUpdate, Set(LocalGossip)) case Right(rcu) => rcu.origins.collect { case RemoteGossip(peerConnection, _) if !wasStashed => // stashed changes have already been acknowledged @@ -282,15 +317,20 @@ object Validation { log.debug("ignoring {} (pending rebroadcast)", u) sendDecision(origins, GossipDecision.Accepted(u)) val origins1 = d.rebroadcast.updates(u) ++ origins - // NB: we update the channels because the balances may have changed even if the channel_update is the same. - val pc1 = pc.applyChannelUpdate(update) - val graphWithBalances1 = d.graphWithBalances.addEdge(GraphEdge(u, pc1)) - d.copy(rebroadcast = d.rebroadcast.copy(updates = d.rebroadcast.updates + (u -> origins1)), channels = d.channels + (pc.shortChannelId -> pc1), graphWithBalances = graphWithBalances1) + update match { + case Left(_) => + // NB: we update the channels because the balances may have changed even if the channel_update is the same. + val pc1 = pc.applyChannelUpdate(update) + val graphWithBalances1 = d.graphWithBalances.addEdge(GraphEdge(u, pc1)) + d.copy(rebroadcast = d.rebroadcast.copy(updates = d.rebroadcast.updates + (u -> origins1)), channels = d.channels + (pc.shortChannelId -> pc1), graphWithBalances = graphWithBalances1) + case Right(_) => + d.copy(rebroadcast = d.rebroadcast.copy(updates = d.rebroadcast.updates + (u -> origins1))) + } } else if (StaleChannels.isStale(u)) { log.debug("ignoring {} (stale)", u) sendDecision(origins, GossipDecision.Stale(u)) d - } else if (pc.getChannelUpdateSameSideAs(u).exists(_.timestamp >= u.timestamp)) { + } else if (pc.getChannelUpdateSameSideAs(u).exists(previous => previous.timestamp >= u.timestamp && previous.shortChannelId == u.shortChannelId)) { // NB: we also check the id because there could be a switch alias->real scid log.debug("ignoring {} (duplicate)", u) sendDecision(origins, GossipDecision.Duplicate(u)) update match { @@ -330,7 +370,7 @@ object Validation { val pc1 = pc.applyChannelUpdate(update) val graphWithBalances1 = d.graphWithBalances.addEdge(GraphEdge(u, pc1)) update.left.foreach(_ => log.info("added local shortChannelId={} public={} to the network graph", u.shortChannelId, publicChannel)) - d.copy(channels = d.channels + (pc.shortChannelId -> pc1), privateChannels = d.privateChannels - pc1.channelId, rebroadcast = d.rebroadcast.copy(updates = d.rebroadcast.updates + (u -> origins)), graphWithBalances = graphWithBalances1) + d.copy(channels = d.channels + (pc.shortChannelId -> pc1), rebroadcast = d.rebroadcast.copy(updates = d.rebroadcast.updates + (u -> origins)), graphWithBalances = graphWithBalances1) } case Some(pc: PrivateChannel) => val publicChannel = false @@ -338,7 +378,7 @@ object Validation { log.debug("ignoring {} (stale)", u) sendDecision(origins, GossipDecision.Stale(u)) d - } else if (pc.getChannelUpdateSameSideAs(u).exists(_.timestamp >= u.timestamp)) { + } else if (pc.getChannelUpdateSameSideAs(u).exists(previous => previous.timestamp >= u.timestamp && previous.shortChannelId == u.shortChannelId)) { // NB: we also check the id because there could be a switch alias->real scid log.debug("ignoring {} (already know same or newer)", u) sendDecision(origins, GossipDecision.Duplicate(u)) d @@ -382,23 +422,24 @@ object Validation { d.copy(stash = d.stash.copy(updates = d.stash.updates + (u -> origins))) } case None if db.isPruned(u.shortChannelId) && !StaleChannels.isStale(u) => + // only public channels are pruned + val realShortChannelId = RealShortChannelId(u.shortChannelId.toLong) // the channel was recently pruned, but if we are here, it means that the update is not stale so this is the case // of a zombie channel coming back from the dead. they probably sent us a channel_announcement right before this update, // but we ignored it because the channel was in the 'pruned' list. Now that we know that the channel is alive again, // let's remove the channel from the zombie list and ask the sender to re-send announcements (channel_announcement + updates) // about that channel. We can ignore this update since we will receive it again - log.info(s"channel shortChannelId=${u.shortChannelId} is back from the dead! requesting announcements about this channel") + log.info(s"channel shortChannelId=$realShortChannelId is back from the dead! requesting announcements about this channel") sendDecision(origins, GossipDecision.RelatedChannelPruned(u)) - db.removeFromPruned(u.shortChannelId) + db.removeFromPruned(realShortChannelId) // peerConnection_opt will contain a valid peerConnection only when we're handling an update that we received from a peer, not // when we're sending updates to ourselves - origins head match { + origins.head match { case RemoteGossip(peerConnection, remoteNodeId) => - val query = QueryShortChannelIds(u.chainHash, EncodedShortChannelIds(routerConf.encodingType, List(u.shortChannelId)), TlvStream.empty) + val query = QueryShortChannelIds(u.chainHash, EncodedShortChannelIds(routerConf.encodingType, List(realShortChannelId)), TlvStream.empty) d.sync.get(remoteNodeId) match { case Some(sync) if sync.started => // we already have a pending request to that node, let's add this channel to the list and we'll get it later - // TODO: we only request channels with old style channel_query d.copy(sync = d.sync + (remoteNodeId -> sync.copy(remainingQueries = sync.remainingQueries :+ query, totalQueries = sync.totalQueries + 1))) case _ => // otherwise we send the query right away @@ -418,66 +459,97 @@ object Validation { } } - def handleLocalChannelUpdate(d: Data, db: NetworkDb, routerConf: RouterConf, localNodeId: PublicKey, watcher: typed.ActorRef[ZmqWatcher.Command], lcu: LocalChannelUpdate)(implicit ctx: ActorContext, log: LoggingAdapter): Data = { + /** + * We will receive this event before [[LocalChannelUpdate]] or [[ChannelUpdate]] + */ + def handleShortChannelIdAssigned(d: Data, localNodeId: PublicKey, scia: ShortChannelIdAssigned)(implicit ctx: ActorContext, log: LoggingAdapter): Data = { implicit val sender: ActorRef = ctx.self // necessary to preserve origin when sending messages to other actors - d.channels.get(lcu.shortChannelId) match { + // NB: we don't map remote aliases because they are decided by our peer and could overlap with ours + val mappings = scia.shortIds.real.toOption match { + case Some(realScid) => Map(realScid.toLong -> scia.channelId, scia.shortIds.localAlias.toLong -> scia.channelId) + case None => Map(scia.shortIds.localAlias.toLong -> scia.channelId) + } + log.debug("handleShortChannelIdAssigned scia={} mappings={}", scia, mappings) + val d1 = d.copy(scid2PrivateChannels = d.scid2PrivateChannels ++ mappings) + d1.resolve(scia.channelId, scia.shortIds.real.toOption) match { case Some(_) => - // channel has already been announced and router knows about it, we can process the channel_update - handleChannelUpdate(d, db, routerConf, Left(lcu)) + // channel is known, nothing more to do + d1 case None => + // this is a local channel that hasn't yet been announced (maybe it is a private channel or maybe it is a public + // channel that doesn't yet have 6 confirmations), we create a corresponding private channel + val pc = PrivateChannel(scia.channelId, scia.shortIds, localNodeId, scia.remoteNodeId, None, None, ChannelMeta(0 msat, 0 msat)) + log.debug("adding unannounced local channel to remote={} channelId={} localAlias={}", scia.remoteNodeId, scia.channelId, scia.shortIds.localAlias) + d1.copy(privateChannels = d1.privateChannels + (scia.channelId -> pc)) + } + } + + def handleLocalChannelUpdate(d: Data, nodeParams: NodeParams, watcher: typed.ActorRef[ZmqWatcher.Command], lcu: LocalChannelUpdate)(implicit ctx: ActorContext, log: DiagnosticLoggingAdapter): Data = { + implicit val sender: ActorRef = ctx.self // necessary to preserve origin when sending messages to other actors + import nodeParams.db.{network => db} + log.debug("handleLocalChannelUpdate lcu={}", lcu) + d.resolve(lcu.channelId, lcu.shortIds.real.toOption) match { + case Some(publicChannel: PublicChannel) => + // this a known public channel, we can process the channel_update + log.debug("this is a known public channel, processing channel_update publicChannel={}", publicChannel) + handleChannelUpdate(d, db, nodeParams.routerConf, Left(lcu)) + case Some(privateChannel: PrivateChannel) => lcu.channelAnnouncement_opt match { - case Some(c) if d.awaiting.contains(c) => - // channel is currently being verified, we can process the channel_update right away (it will be stashed) - handleChannelUpdate(d, db, routerConf, Left(lcu)) - case Some(c) => - // channel wasn't announced but here is the announcement, we will process it *before* the channel_update - watcher ! ValidateRequest(ctx.self, c) - val d1 = d.copy(awaiting = d.awaiting + (c -> Seq(LocalGossip))) // no origin + case Some(ann) => + log.debug("private channel graduating to public privateChannel={}", privateChannel) + // channel is graduating from private to public + // since this is a local channel, we can trust the announcement, no need to go through the full + // verification process and make calls to bitcoin core + val fundingTxId = lcu.commitments match { + case commitments: Commitments => commitments.commitInput.outPoint.txid + case _ => ByteVector32.Zeroes + } + val d1 = addPublicChannel(d, nodeParams, watcher, ann, fundingTxId, lcu.commitments.capacity) // maybe the local channel was pruned (can happen if we were disconnected for more than 2 weeks) - db.removeFromPruned(c.shortChannelId) - handleChannelUpdate(d1, db, routerConf, Left(lcu)) - case None if d.privateChannels.contains(lcu.channelId) => - // channel isn't announced but we already know about it, we can process the channel_update - handleChannelUpdate(d, db, routerConf, Left(lcu)) + db.removeFromPruned(ann.shortChannelId) + log.debug("processing channel_update") + handleChannelUpdate(d1, db, nodeParams.routerConf, Left(lcu)) case None => - // channel isn't announced and we never heard of it (maybe it is a private channel or maybe it is a public channel that doesn't yet have 6 confirmations) - // let's create a corresponding private channel and process the channel_update - log.debug("adding unannounced local channel to remote={} shortChannelId={}", lcu.remoteNodeId, lcu.shortChannelId) - val pc = PrivateChannel(lcu.shortChannelId, lcu.channelId, localNodeId, lcu.remoteNodeId, None, None, ChannelMeta(0 msat, 0 msat)).updateBalances(lcu.commitments) - val d1 = d.copy( - privateChannels = d.privateChannels + (lcu.channelId -> pc), - scid2PrivateChannels = d.scid2PrivateChannels + (lcu.shortChannelId -> lcu.channelId) - ) - handleChannelUpdate(d1, db, routerConf, Left(lcu)) + log.debug("this is a known private channel, processing channel_update privateChannel={}", privateChannel) + // this a known private channel, we update the short ids (we now may have the remote_alias) and the balances + val pc1 = privateChannel.copy(shortIds = lcu.shortIds).updateBalances(lcu.commitments) + val d1 = d.copy(privateChannels = d.privateChannels + (privateChannel.channelId -> pc1)) + // then we can process the channel_update + handleChannelUpdate(d1, db, nodeParams.routerConf, Left(lcu)) } + case None => + // should never happen, we log a warning and handle the update, it will be rejected since there is no related channel + log.warning("unrecognized local chanel update for channelId={} localAlias={}", lcu.channelId, lcu.shortIds.localAlias) + handleChannelUpdate(d, db, nodeParams.routerConf, Left(lcu)) } } def handleLocalChannelDown(d: Data, localNodeId: PublicKey, lcd: LocalChannelDown)(implicit log: LoggingAdapter): Data = { - import lcd.{channelId, remoteNodeId, shortChannelId} + import lcd.{channelId, remoteNodeId} + log.debug("handleLocalChannelDown lcd={}", lcd) + val scid2PrivateChannels1 = d.scid2PrivateChannels - lcd.shortIds.localAlias.toLong -- lcd.shortIds.real.toOption.map(_.toLong) // a local channel has permanently gone down - if (d.channels.contains(shortChannelId)) { + if (lcd.shortIds.real.toOption.exists(d.channels.contains)) { // the channel was public, we will receive (or have already received) a WatchEventSpentBasic event, that will trigger a clean up of the channel // so let's not do anything here - d - } else if (d.privateChannels.contains(channelId)) { + d.copy(scid2PrivateChannels = scid2PrivateChannels1) + } else if (d.privateChannels.contains(lcd.channelId)) { // the channel was private or public-but-not-yet-announced, let's do the clean up - log.info("removing private local channel and channel_update for channelId={} shortChannelId={}", channelId, shortChannelId) - val desc1 = ChannelDesc(shortChannelId, localNodeId, remoteNodeId) - val desc2 = ChannelDesc(shortChannelId, remoteNodeId, localNodeId) + val localAlias = d.privateChannels(channelId).shortIds.localAlias + log.info("removing private local channel and channel_update for channelId={} localAlias={}", channelId, localAlias) // we remove the corresponding updates from the graph val graphWithBalances1 = d.graphWithBalances - .removeEdge(desc1) - .removeEdge(desc2) + .removeEdge(ChannelDesc(localAlias, localNodeId, remoteNodeId)) + .removeEdge(ChannelDesc(localAlias, remoteNodeId, localNodeId)) // and we remove the channel and channel_update from our state - d.copy(privateChannels = d.privateChannels - channelId, graphWithBalances = graphWithBalances1) + d.copy(privateChannels = d.privateChannels - channelId, scid2PrivateChannels = scid2PrivateChannels1, graphWithBalances = graphWithBalances1) } else { d } } def handleAvailableBalanceChanged(d: Data, e: AvailableBalanceChanged)(implicit log: LoggingAdapter): Data = { - val (publicChannels1, graphWithBalances1) = d.channels.get(e.shortChannelId) match { + val (publicChannels1, graphWithBalances1) = e.shortIds.real.toOption.flatMap(d.channels.get) match { case Some(pc) => val pc1 = pc.updateBalances(e.commitments) log.debug("public channel balance updated: {}", pc1) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelCodecs0.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelCodecs0.scala index fa23ee1f42..b2c491c0fd 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelCodecs0.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelCodecs0.scala @@ -18,7 +18,6 @@ package fr.acinq.eclair.wire.internal.channel.version0 import fr.acinq.bitcoin.scalacompat.DeterministicWallet.{ExtendedPrivateKey, KeyPath} import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, Crypto, OutPoint, Transaction, TxOut} -import fr.acinq.eclair.{BlockHeight, TimestampSecond} import fr.acinq.eclair.channel._ import fr.acinq.eclair.crypto.ShaChain import fr.acinq.eclair.transactions.Transactions._ @@ -27,6 +26,7 @@ import fr.acinq.eclair.wire.internal.channel.version0.ChannelTypes0.{HtlcTxAndSi import fr.acinq.eclair.wire.protocol.CommonCodecs._ import fr.acinq.eclair.wire.protocol.LightningMessageCodecs.{channelAnnouncementCodec, channelUpdateCodec, combinedFeaturesCodec} import fr.acinq.eclair.wire.protocol._ +import fr.acinq.eclair.{BlockHeight, Alias, TimestampSecond} import scodec.Codec import scodec.bits.{BitVector, ByteVector} import scodec.codecs._ @@ -349,30 +349,33 @@ private[channel] object ChannelCodecs0 { ("signature" | bytes64) :: ("tlvStream" | provide(TlvStream.empty[FundingSignedTlv]))).as[FundingSigned] - val fundingLockedCodec: Codec[FundingLocked] = ( + val channelReadyCodec: Codec[ChannelReady] = ( ("channelId" | bytes32) :: ("nextPerCommitmentPoint" | publicKey) :: - ("tlvStream" | provide(TlvStream.empty[FundingLockedTlv]))).as[FundingLocked] + ("tlvStream" | provide(TlvStream.empty[ChannelReadyTlv]))).as[ChannelReady] // this is a decode-only codec compatible with versions 997acee and below, with placeholders for new fields val DATA_WAIT_FOR_FUNDING_CONFIRMED_COMPAT_01_Codec: Codec[DATA_WAIT_FOR_FUNDING_CONFIRMED] = ( ("commitments" | commitmentsCodec) :: ("fundingTx" | provide[Option[Transaction]](None)) :: ("waitingSince" | provide(BlockHeight(TimestampSecond.now().toLong))) :: - ("deferred" | optional(bool, fundingLockedCodec)) :: + ("deferred" | optional(bool, channelReadyCodec)) :: ("lastSent" | either(bool, fundingCreatedCodec, fundingSignedCodec))).as[DATA_WAIT_FOR_FUNDING_CONFIRMED].decodeOnly val DATA_WAIT_FOR_FUNDING_CONFIRMED_Codec: Codec[DATA_WAIT_FOR_FUNDING_CONFIRMED] = ( ("commitments" | commitmentsCodec) :: ("fundingTx" | optional(bool, txCodec)) :: ("waitingSince" | int64.as[BlockHeight]) :: - ("deferred" | optional(bool, fundingLockedCodec)) :: + ("deferred" | optional(bool, channelReadyCodec)) :: ("lastSent" | either(bool, fundingCreatedCodec, fundingSignedCodec))).as[DATA_WAIT_FOR_FUNDING_CONFIRMED].decodeOnly - val DATA_WAIT_FOR_FUNDING_LOCKED_Codec: Codec[DATA_WAIT_FOR_FUNDING_LOCKED] = ( + val DATA_WAIT_FOR_CHANNEL_READY_Codec: Codec[DATA_WAIT_FOR_CHANNEL_READY] = ( ("commitments" | commitmentsCodec) :: - ("shortChannelId" | shortchannelid) :: - ("lastSent" | fundingLockedCodec)).as[DATA_WAIT_FOR_FUNDING_LOCKED].decodeOnly + ("shortChannelId" | realshortchannelid) :: + ("lastSent" | channelReadyCodec)).map { + case commitments :: shortChannelId :: lastSent :: HNil => + DATA_WAIT_FOR_CHANNEL_READY(commitments, shortIds = ShortIds(real = RealScidStatus.Temporary(shortChannelId), localAlias = Alias(shortChannelId.toLong), remoteAlias_opt = None), lastSent = lastSent) + }.decodeOnly val shutdownCodec: Codec[Shutdown] = ( ("channelId" | bytes32) :: @@ -382,23 +385,29 @@ private[channel] object ChannelCodecs0 { // this is a decode-only codec compatible with versions 9afb26e and below val DATA_NORMAL_COMPAT_03_Codec: Codec[DATA_NORMAL] = ( ("commitments" | commitmentsCodec) :: - ("shortChannelId" | shortchannelid) :: + ("shortChannelId" | realshortchannelid) :: ("buried" | bool) :: ("channelAnnouncement" | optional(bool, variableSizeBytes(noUnknownFieldsChannelAnnouncementSizeCodec, channelAnnouncementCodec))) :: ("channelUpdate" | variableSizeBytes(noUnknownFieldsChannelUpdateSizeCodec, channelUpdateCodec)) :: ("localShutdown" | optional(bool, shutdownCodec)) :: ("remoteShutdown" | optional(bool, shutdownCodec)) :: - ("closingFeerates" | provide(Option.empty[ClosingFeerates]))).as[DATA_NORMAL].decodeOnly + ("closingFeerates" | provide(Option.empty[ClosingFeerates]))).map { + case commitments :: shortChannelId :: buried :: channelAnnouncement :: channelUpdate :: localShutdown :: remoteShutdown :: closingFeerates :: HNil => + DATA_NORMAL(commitments, shortIds = ShortIds(real = if (buried) RealScidStatus.Final(shortChannelId) else RealScidStatus.Temporary(shortChannelId), localAlias = Alias(shortChannelId.toLong), remoteAlias_opt = None), channelAnnouncement, channelUpdate, localShutdown, remoteShutdown, closingFeerates) + }.decodeOnly val DATA_NORMAL_Codec: Codec[DATA_NORMAL] = ( ("commitments" | commitmentsCodec) :: - ("shortChannelId" | shortchannelid) :: + ("shortChannelId" | realshortchannelid) :: ("buried" | bool) :: ("channelAnnouncement" | optional(bool, variableSizeBytes(uint16, channelAnnouncementCodec))) :: ("channelUpdate" | variableSizeBytes(uint16, channelUpdateCodec)) :: ("localShutdown" | optional(bool, shutdownCodec)) :: ("remoteShutdown" | optional(bool, shutdownCodec)) :: - ("closingFeerates" | provide(Option.empty[ClosingFeerates]))).as[DATA_NORMAL].decodeOnly + ("closingFeerates" | provide(Option.empty[ClosingFeerates]))).map { + case commitments :: shortChannelId :: buried :: channelAnnouncement :: channelUpdate :: localShutdown :: remoteShutdown :: closingFeerates :: HNil => + DATA_NORMAL(commitments, shortIds = ShortIds(real = if (buried) RealScidStatus.Final(shortChannelId) else RealScidStatus.Temporary(shortChannelId), localAlias = Alias(shortChannelId.toLong), remoteAlias_opt = None), channelAnnouncement, channelUpdate, localShutdown, remoteShutdown, closingFeerates) + }.decodeOnly val DATA_SHUTDOWN_Codec: Codec[DATA_SHUTDOWN] = ( ("commitments" | commitmentsCodec) :: @@ -457,7 +466,7 @@ private[channel] object ChannelCodecs0 { .typecase(0x09, Codecs.DATA_CLOSING_Codec) .typecase(0x08, Codecs.DATA_WAIT_FOR_FUNDING_CONFIRMED_Codec) .typecase(0x01, Codecs.DATA_WAIT_FOR_FUNDING_CONFIRMED_COMPAT_01_Codec) - .typecase(0x02, Codecs.DATA_WAIT_FOR_FUNDING_LOCKED_Codec) + .typecase(0x02, Codecs.DATA_WAIT_FOR_CHANNEL_READY_Codec) .typecase(0x03, Codecs.DATA_NORMAL_COMPAT_03_Codec) .typecase(0x04, Codecs.DATA_SHUTDOWN_Codec) .typecase(0x05, Codecs.DATA_NEGOTIATING_Codec) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version1/ChannelCodecs1.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version1/ChannelCodecs1.scala index d4173bef30..384947d98f 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version1/ChannelCodecs1.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version1/ChannelCodecs1.scala @@ -18,7 +18,7 @@ package fr.acinq.eclair.wire.internal.channel.version1 import fr.acinq.bitcoin.scalacompat.DeterministicWallet.{ExtendedPrivateKey, KeyPath} import fr.acinq.bitcoin.scalacompat.{ByteVector32, OutPoint, Transaction, TxOut} -import fr.acinq.eclair.BlockHeight +import fr.acinq.eclair.{Alias, BlockHeight} import fr.acinq.eclair.channel._ import fr.acinq.eclair.crypto.ShaChain import fr.acinq.eclair.transactions.Transactions._ @@ -236,23 +236,29 @@ private[channel] object ChannelCodecs1 { ("commitments" | commitmentsCodec) :: ("fundingTx" | optional(bool8, txCodec)) :: ("waitingSince" | int64.as[BlockHeight]) :: - ("deferred" | optional(bool8, lengthDelimited(fundingLockedCodec))) :: + ("deferred" | optional(bool8, lengthDelimited(channelReadyCodec))) :: ("lastSent" | either(bool8, lengthDelimited(fundingCreatedCodec), lengthDelimited(fundingSignedCodec)))).as[DATA_WAIT_FOR_FUNDING_CONFIRMED] - val DATA_WAIT_FOR_FUNDING_LOCKED_Codec: Codec[DATA_WAIT_FOR_FUNDING_LOCKED] = ( + val DATA_WAIT_FOR_CHANNEL_READY_Codec: Codec[DATA_WAIT_FOR_CHANNEL_READY] = ( ("commitments" | commitmentsCodec) :: - ("shortChannelId" | shortchannelid) :: - ("lastSent" | lengthDelimited(fundingLockedCodec))).as[DATA_WAIT_FOR_FUNDING_LOCKED] + ("shortChannelId" | realshortchannelid) :: + ("lastSent" | lengthDelimited(channelReadyCodec))).map { + case commitments :: shortChannelId :: lastSent :: HNil => + DATA_WAIT_FOR_CHANNEL_READY(commitments, shortIds = ShortIds(real = RealScidStatus.Temporary(shortChannelId), localAlias = Alias(shortChannelId.toLong), remoteAlias_opt = None), lastSent = lastSent) + }.decodeOnly val DATA_NORMAL_Codec: Codec[DATA_NORMAL] = ( ("commitments" | commitmentsCodec) :: - ("shortChannelId" | shortchannelid) :: + ("shortChannelId" | realshortchannelid) :: ("buried" | bool8) :: ("channelAnnouncement" | optional(bool8, lengthDelimited(channelAnnouncementCodec))) :: ("channelUpdate" | lengthDelimited(channelUpdateCodec)) :: ("localShutdown" | optional(bool8, lengthDelimited(shutdownCodec))) :: ("remoteShutdown" | optional(bool8, lengthDelimited(shutdownCodec))) :: - ("closingFeerates" | provide(Option.empty[ClosingFeerates]))).as[DATA_NORMAL] + ("closingFeerates" | provide(Option.empty[ClosingFeerates]))).map { + case commitments :: shortChannelId :: buried :: channelAnnouncement :: channelUpdate :: localShutdown :: remoteShutdown :: closingFeerates :: HNil => + DATA_NORMAL(commitments, shortIds = ShortIds(real = if (buried) RealScidStatus.Final(shortChannelId) else RealScidStatus.Temporary(shortChannelId), localAlias = Alias(shortChannelId.toLong), remoteAlias_opt = None), channelAnnouncement, channelUpdate, localShutdown, remoteShutdown, closingFeerates) + }.decodeOnly val DATA_SHUTDOWN_Codec: Codec[DATA_SHUTDOWN] = ( ("commitments" | commitmentsCodec) :: @@ -287,7 +293,7 @@ private[channel] object ChannelCodecs1 { // Order matters! val channelDataCodec: Codec[PersistentChannelData] = discriminated[PersistentChannelData].by(uint16) .typecase(0x20, Codecs.DATA_WAIT_FOR_FUNDING_CONFIRMED_Codec) - .typecase(0x21, Codecs.DATA_WAIT_FOR_FUNDING_LOCKED_Codec) + .typecase(0x21, Codecs.DATA_WAIT_FOR_CHANNEL_READY_Codec) .typecase(0x22, Codecs.DATA_NORMAL_Codec) .typecase(0x23, Codecs.DATA_SHUTDOWN_Codec) .typecase(0x24, Codecs.DATA_NEGOTIATING_Codec) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version2/ChannelCodecs2.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version2/ChannelCodecs2.scala index 4001e6e3bb..0e1c29770c 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version2/ChannelCodecs2.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version2/ChannelCodecs2.scala @@ -18,7 +18,7 @@ package fr.acinq.eclair.wire.internal.channel.version2 import fr.acinq.bitcoin.scalacompat.DeterministicWallet.{ExtendedPrivateKey, KeyPath} import fr.acinq.bitcoin.scalacompat.{OutPoint, Transaction, TxOut} -import fr.acinq.eclair.BlockHeight +import fr.acinq.eclair.{Alias, BlockHeight} import fr.acinq.eclair.channel._ import fr.acinq.eclair.crypto.ShaChain import fr.acinq.eclair.transactions.Transactions._ @@ -271,23 +271,29 @@ private[channel] object ChannelCodecs2 { ("commitments" | commitmentsCodec) :: ("fundingTx" | optional(bool8, txCodec)) :: ("waitingSince" | int64.as[BlockHeight]) :: - ("deferred" | optional(bool8, lengthDelimited(fundingLockedCodec))) :: + ("deferred" | optional(bool8, lengthDelimited(channelReadyCodec))) :: ("lastSent" | either(bool8, lengthDelimited(fundingCreatedCodec), lengthDelimited(fundingSignedCodec)))).as[DATA_WAIT_FOR_FUNDING_CONFIRMED] - val DATA_WAIT_FOR_FUNDING_LOCKED_Codec: Codec[DATA_WAIT_FOR_FUNDING_LOCKED] = ( + val DATA_WAIT_FOR_CHANNEL_READY_Codec: Codec[DATA_WAIT_FOR_CHANNEL_READY] = ( ("commitments" | commitmentsCodec) :: - ("shortChannelId" | shortchannelid) :: - ("lastSent" | lengthDelimited(fundingLockedCodec))).as[DATA_WAIT_FOR_FUNDING_LOCKED] + ("shortChannelId" | realshortchannelid) :: + ("lastSent" | lengthDelimited(channelReadyCodec))).map { + case commitments :: shortChannelId :: lastSent :: HNil => + DATA_WAIT_FOR_CHANNEL_READY(commitments, shortIds = ShortIds(real = RealScidStatus.Temporary(shortChannelId), localAlias = Alias(shortChannelId.toLong), remoteAlias_opt = None), lastSent = lastSent) + }.decodeOnly val DATA_NORMAL_Codec: Codec[DATA_NORMAL] = ( ("commitments" | commitmentsCodec) :: - ("shortChannelId" | shortchannelid) :: + ("shortChannelId" | realshortchannelid) :: ("buried" | bool8) :: ("channelAnnouncement" | optional(bool8, lengthDelimited(channelAnnouncementCodec))) :: ("channelUpdate" | lengthDelimited(channelUpdateCodec)) :: ("localShutdown" | optional(bool8, lengthDelimited(shutdownCodec))) :: ("remoteShutdown" | optional(bool8, lengthDelimited(shutdownCodec))) :: - ("closingFeerates" | provide(Option.empty[ClosingFeerates]))).as[DATA_NORMAL] + ("closingFeerates" | provide(Option.empty[ClosingFeerates]))).map { + case commitments :: shortChannelId :: buried :: channelAnnouncement :: channelUpdate :: localShutdown :: remoteShutdown :: closingFeerates :: HNil => + DATA_NORMAL(commitments, shortIds = ShortIds(real = if (buried) RealScidStatus.Final(shortChannelId) else RealScidStatus.Temporary(shortChannelId), localAlias = Alias(shortChannelId.toLong), remoteAlias_opt = None), channelAnnouncement, channelUpdate, localShutdown, remoteShutdown, closingFeerates) + }.decodeOnly val DATA_SHUTDOWN_Codec: Codec[DATA_SHUTDOWN] = ( ("commitments" | commitmentsCodec) :: @@ -321,7 +327,7 @@ private[channel] object ChannelCodecs2 { val channelDataCodec: Codec[PersistentChannelData] = discriminated[PersistentChannelData].by(uint16) .typecase(0x00, Codecs.DATA_WAIT_FOR_FUNDING_CONFIRMED_Codec) - .typecase(0x01, Codecs.DATA_WAIT_FOR_FUNDING_LOCKED_Codec) + .typecase(0x01, Codecs.DATA_WAIT_FOR_CHANNEL_READY_Codec) .typecase(0x02, Codecs.DATA_NORMAL_Codec) .typecase(0x03, Codecs.DATA_SHUTDOWN_Codec) .typecase(0x04, Codecs.DATA_NEGOTIATING_Codec) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3.scala index be8ebdf99c..8dfb7410b8 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3.scala @@ -25,7 +25,7 @@ import fr.acinq.eclair.transactions.{CommitmentSpec, DirectedHtlc, IncomingHtlc, import fr.acinq.eclair.wire.protocol.CommonCodecs._ import fr.acinq.eclair.wire.protocol.LightningMessageCodecs._ import fr.acinq.eclair.wire.protocol.UpdateMessage -import fr.acinq.eclair.{BlockHeight, FeatureSupport, Features, PermanentChannelFeature} +import fr.acinq.eclair.{BlockHeight, FeatureSupport, Features, Alias, PermanentChannelFeature} import scodec.bits.{BitVector, ByteVector} import scodec.codecs._ import scodec.{Attempt, Codec} @@ -320,32 +320,55 @@ private[channel] object ChannelCodecs3 { ("fundingTx" | optional(bool8, txCodec)) :: // TODO: next time we define a new channel codec version, we should use the blockHeight codec here (32 bytes) ("waitingSince" | int64.as[BlockHeight]) :: - ("deferred" | optional(bool8, lengthDelimited(fundingLockedCodec))) :: + ("deferred" | optional(bool8, lengthDelimited(channelReadyCodec))) :: ("lastSent" | either(bool8, lengthDelimited(fundingCreatedCodec), lengthDelimited(fundingSignedCodec)))).as[DATA_WAIT_FOR_FUNDING_CONFIRMED] - val DATA_WAIT_FOR_FUNDING_LOCKED_Codec: Codec[DATA_WAIT_FOR_FUNDING_LOCKED] = ( + val DATA_WAIT_FOR_CHANNEL_READY_COMPAT_01_Codec: Codec[DATA_WAIT_FOR_CHANNEL_READY] = ( ("commitments" | commitmentsCodec) :: - ("shortChannelId" | shortchannelid) :: - ("lastSent" | lengthDelimited(fundingLockedCodec))).as[DATA_WAIT_FOR_FUNDING_LOCKED] + ("shortChannelId" | realshortchannelid) :: + ("lastSent" | lengthDelimited(channelReadyCodec))).map { + case commitments :: shortChannelId :: lastSent :: HNil => + DATA_WAIT_FOR_CHANNEL_READY(commitments, shortIds = ShortIds(real = RealScidStatus.Temporary(shortChannelId), localAlias = Alias(shortChannelId.toLong), remoteAlias_opt = None), lastSent = lastSent) + }.decodeOnly + + val DATA_WAIT_FOR_CHANNEL_READY_Codec: Codec[DATA_WAIT_FOR_CHANNEL_READY] = ( + ("commitments" | commitmentsCodec) :: + ("shortIds" | shortids) :: + ("lastSent" | lengthDelimited(channelReadyCodec))).as[DATA_WAIT_FOR_CHANNEL_READY] val DATA_NORMAL_COMPAT_02_Codec: Codec[DATA_NORMAL] = ( ("commitments" | commitmentsCodec) :: - ("shortChannelId" | shortchannelid) :: + ("shortChannelId" | realshortchannelid) :: ("buried" | bool8) :: ("channelAnnouncement" | optional(bool8, lengthDelimited(channelAnnouncementCodec))) :: ("channelUpdate" | lengthDelimited(channelUpdateCodec)) :: ("localShutdown" | optional(bool8, lengthDelimited(shutdownCodec))) :: ("remoteShutdown" | optional(bool8, lengthDelimited(shutdownCodec))) :: - ("closingFeerates" | provide(Option.empty[ClosingFeerates]))).as[DATA_NORMAL] + ("closingFeerates" | provide(Option.empty[ClosingFeerates]))).map { + case commitments :: shortChannelId :: buried :: channelAnnouncement :: channelUpdate :: localShutdown :: remoteShutdown :: closingFeerates :: HNil => + DATA_NORMAL(commitments, shortIds = ShortIds(real = if (buried) RealScidStatus.Final(shortChannelId) else RealScidStatus.Temporary(shortChannelId), localAlias = Alias(shortChannelId.toLong), remoteAlias_opt = None), channelAnnouncement, channelUpdate, localShutdown, remoteShutdown, closingFeerates) + }.decodeOnly - val DATA_NORMAL_Codec: Codec[DATA_NORMAL] = ( + val DATA_NORMAL_COMPAT_07_Codec: Codec[DATA_NORMAL] = ( ("commitments" | commitmentsCodec) :: - ("shortChannelId" | shortchannelid) :: + ("shortChannelId" | realshortchannelid) :: ("buried" | bool8) :: ("channelAnnouncement" | optional(bool8, lengthDelimited(channelAnnouncementCodec))) :: ("channelUpdate" | lengthDelimited(channelUpdateCodec)) :: ("localShutdown" | optional(bool8, lengthDelimited(shutdownCodec))) :: ("remoteShutdown" | optional(bool8, lengthDelimited(shutdownCodec))) :: + ("closingFeerates" | optional(bool8, closingFeeratesCodec))).map { + case commitments :: shortChannelId :: buried :: channelAnnouncement :: channelUpdate :: localShutdown :: remoteShutdown :: closingFeerates :: HNil => + DATA_NORMAL(commitments, shortIds = ShortIds(real = if (buried) RealScidStatus.Final(shortChannelId) else RealScidStatus.Temporary(shortChannelId), localAlias = Alias(shortChannelId.toLong), remoteAlias_opt = None), channelAnnouncement, channelUpdate, localShutdown, remoteShutdown, closingFeerates) + }.decodeOnly + + val DATA_NORMAL_Codec: Codec[DATA_NORMAL] = ( + ("commitments" | commitmentsCodec) :: + ("shortids" | shortids) :: + ("channelAnnouncement" | optional(bool8, lengthDelimited(channelAnnouncementCodec))) :: + ("channelUpdate" | lengthDelimited(channelUpdateCodec)) :: + ("localShutdown" | optional(bool8, lengthDelimited(shutdownCodec))) :: + ("remoteShutdown" | optional(bool8, lengthDelimited(shutdownCodec))) :: ("closingFeerates" | optional(bool8, closingFeeratesCodec))).as[DATA_NORMAL] val DATA_SHUTDOWN_COMPAT_03_Codec: Codec[DATA_SHUTDOWN] = ( @@ -387,14 +410,16 @@ private[channel] object ChannelCodecs3 { // Order matters! val channelDataCodec: Codec[PersistentChannelData] = discriminated[PersistentChannelData].by(uint16) + .typecase(0x0a, Codecs.DATA_WAIT_FOR_CHANNEL_READY_Codec) + .typecase(0x09, Codecs.DATA_NORMAL_Codec) .typecase(0x08, Codecs.DATA_SHUTDOWN_Codec) - .typecase(0x07, Codecs.DATA_NORMAL_Codec) + .typecase(0x07, Codecs.DATA_NORMAL_COMPAT_07_Codec) .typecase(0x06, Codecs.DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT_Codec) .typecase(0x05, Codecs.DATA_CLOSING_Codec) .typecase(0x04, Codecs.DATA_NEGOTIATING_Codec) .typecase(0x03, Codecs.DATA_SHUTDOWN_COMPAT_03_Codec) .typecase(0x02, Codecs.DATA_NORMAL_COMPAT_02_Codec) - .typecase(0x01, Codecs.DATA_WAIT_FOR_FUNDING_LOCKED_Codec) + .typecase(0x01, Codecs.DATA_WAIT_FOR_CHANNEL_READY_COMPAT_01_Codec) .typecase(0x00, Codecs.DATA_WAIT_FOR_FUNDING_CONFIRMED_Codec) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/ChannelTlv.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/ChannelTlv.scala index f49187f7a6..27a32898f7 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/ChannelTlv.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/ChannelTlv.scala @@ -18,9 +18,10 @@ package fr.acinq.eclair.wire.protocol import fr.acinq.bitcoin.scalacompat.Satoshi import fr.acinq.eclair.channel.{ChannelType, ChannelTypes} +import fr.acinq.eclair.wire.protocol.ChannelTlv.ChannelTypeTlv import fr.acinq.eclair.wire.protocol.CommonCodecs._ import fr.acinq.eclair.wire.protocol.TlvCodecs.tlvStream -import fr.acinq.eclair.{FeatureSupport, Features, UInt64} +import fr.acinq.eclair.{Alias, FeatureSupport, Features, ShortChannelId, UInt64} import scodec.Codec import scodec.bits.ByteVector import scodec.codecs._ @@ -107,10 +108,17 @@ object FundingSignedTlv { val fundingSignedTlvCodec: Codec[TlvStream[FundingSignedTlv]] = tlvStream(discriminated[FundingSignedTlv].by(varint)) } -sealed trait FundingLockedTlv extends Tlv +sealed trait ChannelReadyTlv extends Tlv -object FundingLockedTlv { - val fundingLockedTlvCodec: Codec[TlvStream[FundingLockedTlv]] = tlvStream(discriminated[FundingLockedTlv].by(varint)) +object ChannelReadyTlv { + + case class ShortChannelIdTlv(alias: Alias) extends ChannelReadyTlv + + val channelAliasTlvCodec: Codec[ShortChannelIdTlv] = variableSizeBytesLong(varintoverflow, "alias" | alias).as[ShortChannelIdTlv] + + val channelReadyTlvCodec: Codec[TlvStream[ChannelReadyTlv]] = tlvStream(discriminated[ChannelReadyTlv].by(varint) + .typecase(UInt64(1), channelAliasTlvCodec) + ) } sealed trait ChannelReestablishTlv extends Tlv diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/CommonCodecs.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/CommonCodecs.scala index 78ad89f00a..314fbaf4bf 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/CommonCodecs.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/CommonCodecs.scala @@ -19,9 +19,9 @@ package fr.acinq.eclair.wire.protocol import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, Satoshi, Transaction} import fr.acinq.eclair.blockchain.fee.FeeratePerKw -import fr.acinq.eclair.channel.ChannelFlags +import fr.acinq.eclair.channel.{ChannelFlags, RealScidStatus, ShortIds} import fr.acinq.eclair.crypto.Mac32 -import fr.acinq.eclair.{BlockHeight, CltvExpiry, CltvExpiryDelta, MilliSatoshi, ShortChannelId, TimestampSecond, UInt64} +import fr.acinq.eclair.{Alias, BlockHeight, CltvExpiry, CltvExpiryDelta, MilliSatoshi, RealShortChannelId, ShortChannelId, TimestampSecond, UInt64, UnspecifiedShortChannelId} import org.apache.commons.codec.binary.Base32 import scodec.bits.{BitVector, ByteVector} import scodec.codecs._ @@ -130,7 +130,22 @@ object CommonCodecs { // number of bytes we can just skip to the next field val listofnodeaddresses: Codec[List[NodeAddress]] = variableSizeBytes(uint16, list(nodeaddress)) - val shortchannelid: Codec[ShortChannelId] = int64.xmap(l => ShortChannelId(l), s => s.toLong) + val shortchannelid: Codec[ShortChannelId] = int64.xmap(l => UnspecifiedShortChannelId(l), s => s.toLong) + + val realshortchannelid: Codec[RealShortChannelId] = shortchannelid.narrow[RealShortChannelId](scid => Attempt.successful(RealShortChannelId(scid.toLong)), scid => scid) + + val alias: Codec[Alias] = shortchannelid.narrow[Alias](scid => Attempt.successful(Alias(scid.toLong)), scid => scid) + + val realShortChannelIdStatus: Codec[RealScidStatus] = discriminated[RealScidStatus].by(uint8) + .typecase(0, provide(RealScidStatus.Unknown)) + .typecase(1, realshortchannelid.as[RealScidStatus.Temporary]) + .typecase(2, realshortchannelid.as[RealScidStatus.Final]) + + val shortids: Codec[ShortIds] = ( + ("real" | realShortChannelIdStatus) :: + ("localAlias" | discriminated[Alias].by(uint16).typecase(1, alias)) :: // forward-compatible with listOfN(uint16, aliashortchannelid) in case we want to store a list of local aliases later + ("remoteAlias_opt" | optional(bool8, alias)) + ).as[ShortIds] val privateKey: Codec[PrivateKey] = Codec[PrivateKey]( (priv: PrivateKey) => bytes(32).encode(priv.value), diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/FailureMessage.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/FailureMessage.scala index 07969828bb..1e9612c22a 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/FailureMessage.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/FailureMessage.scala @@ -139,9 +139,11 @@ object FailureMessageCodecs { /** * An onion-encrypted failure from an intermediate node: + * {{{ * +----------------+----------------------------------+-----------------+----------------------+-----+ * | HMAC(32 bytes) | failure message length (2 bytes) | failure message | pad length (2 bytes) | pad | * +----------------+----------------------------------+-----------------+----------------------+-----+ + * }}} * with failure message length + pad length = 256 */ def failureOnionCodec(mac: Mac32): Codec[FailureMessage] = CommonCodecs.prependmac( diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecs.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecs.scala index 770b5c0839..4ffa28885a 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecs.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecs.scala @@ -164,10 +164,10 @@ object LightningMessageCodecs { ("signature" | bytes64) :: ("tlvStream" | FundingSignedTlv.fundingSignedTlvCodec)).as[FundingSigned] - val fundingLockedCodec: Codec[FundingLocked] = ( + val channelReadyCodec: Codec[ChannelReady] = ( ("channelId" | bytes32) :: ("nextPerCommitmentPoint" | publicKey) :: - ("tlvStream" | FundingLockedTlv.fundingLockedTlvCodec)).as[FundingLocked] + ("tlvStream" | ChannelReadyTlv.channelReadyTlvCodec)).as[ChannelReady] val txAddInputCodec: Codec[TxAddInput] = ( ("channelId" | bytes32) :: @@ -281,7 +281,7 @@ object LightningMessageCodecs { val announcementSignaturesCodec: Codec[AnnouncementSignatures] = ( ("channelId" | bytes32) :: - ("shortChannelId" | shortchannelid) :: + ("shortChannelId" | realshortchannelid) :: ("nodeSignature" | bytes64) :: ("bitcoinSignature" | bytes64) :: ("tlvStream" | AnnouncementSignaturesTlv.announcementSignaturesTlvCodec)).as[AnnouncementSignatures] @@ -289,7 +289,7 @@ object LightningMessageCodecs { val channelAnnouncementWitnessCodec = ("features" | featuresCodec) :: ("chainHash" | bytes32) :: - ("shortChannelId" | shortchannelid) :: + ("shortChannelId" | realshortchannelid) :: ("nodeId1" | publicKey) :: ("nodeId2" | publicKey) :: ("bitcoinKey1" | publicKey) :: @@ -368,10 +368,10 @@ object LightningMessageCodecs { .\(0) { case a@EncodedShortChannelIds(_, Nil) => a // empty list is always encoded with encoding type 'uncompressed' for compatibility with other implementations case a@EncodedShortChannelIds(EncodingType.UNCOMPRESSED, _) => a - }((provide[EncodingType](EncodingType.UNCOMPRESSED) :: list(shortchannelid)).as[EncodedShortChannelIds]) + }((provide[EncodingType](EncodingType.UNCOMPRESSED) :: list(realshortchannelid)).as[EncodedShortChannelIds]) .\(1) { case a@EncodedShortChannelIds(EncodingType.COMPRESSED_ZLIB, _) => a - }((provide[EncodingType](EncodingType.COMPRESSED_ZLIB) :: zlib(list(shortchannelid))).as[EncodedShortChannelIds]) + }((provide[EncodingType](EncodingType.COMPRESSED_ZLIB) :: zlib(list(realshortchannelid))).as[EncodedShortChannelIds]) val queryShortChannelIdsCodec: Codec[QueryShortChannelIds] = ( ("chainHash" | bytes32) :: @@ -441,7 +441,7 @@ object LightningMessageCodecs { .typecase(33, acceptChannelCodec) .typecase(34, fundingCreatedCodec) .typecase(35, fundingSignedCodec) - .typecase(36, fundingLockedCodec) + .typecase(36, channelReadyCodec) .typecase(38, shutdownCodec) .typecase(39, closingSignedCodec) .typecase(64, openDualFundedChannelCodec) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageTypes.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageTypes.scala index c614990f12..7d8aa9f3e2 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageTypes.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageTypes.scala @@ -20,10 +20,11 @@ import com.google.common.base.Charsets import com.google.common.net.InetAddresses import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, Satoshi, ScriptWitness, Transaction} +import fr.acinq.eclair.{Alias, BlockHeight, CltvExpiry, CltvExpiryDelta, Feature, Features, InitFeature, MilliSatoshi, RealShortChannelId, ShortChannelId, TimestampSecond, UInt64} import fr.acinq.eclair.blockchain.fee.FeeratePerKw import fr.acinq.eclair.channel.{ChannelFlags, ChannelType} import fr.acinq.eclair.payment.relay.Relayer -import fr.acinq.eclair.{BlockHeight, CltvExpiry, CltvExpiryDelta, Feature, Features, InitFeature, MilliSatoshi, ShortChannelId, TimestampSecond, UInt64} +import fr.acinq.eclair.wire.protocol.ChannelReadyTlv.ShortChannelIdTlv import scodec.bits.ByteVector import java.net.{Inet4Address, Inet6Address, InetAddress} @@ -225,9 +226,11 @@ case class FundingSigned(channelId: ByteVector32, signature: ByteVector64, tlvStream: TlvStream[FundingSignedTlv] = TlvStream.empty) extends ChannelMessage with HasChannelId -case class FundingLocked(channelId: ByteVector32, +case class ChannelReady(channelId: ByteVector32, nextPerCommitmentPoint: PublicKey, - tlvStream: TlvStream[FundingLockedTlv] = TlvStream.empty) extends ChannelMessage with HasChannelId + tlvStream: TlvStream[ChannelReadyTlv] = TlvStream.empty) extends ChannelMessage with HasChannelId { + val alias_opt: Option[Alias] = tlvStream.get[ShortChannelIdTlv].map(_.alias) +} case class Shutdown(channelId: ByteVector32, scriptPubKey: ByteVector, @@ -279,7 +282,7 @@ case class UpdateFee(channelId: ByteVector32, tlvStream: TlvStream[UpdateFeeTlv] = TlvStream.empty) extends ChannelMessage with UpdateMessage with HasChannelId case class AnnouncementSignatures(channelId: ByteVector32, - shortChannelId: ShortChannelId, + shortChannelId: RealShortChannelId, nodeSignature: ByteVector64, bitcoinSignature: ByteVector64, tlvStream: TlvStream[AnnouncementSignaturesTlv] = TlvStream.empty) extends RoutingMessage with HasChannelId @@ -290,7 +293,7 @@ case class ChannelAnnouncement(nodeSignature1: ByteVector64, bitcoinSignature2: ByteVector64, features: Features[Feature], chainHash: ByteVector32, - shortChannelId: ShortChannelId, + shortChannelId: RealShortChannelId, nodeId1: PublicKey, nodeId2: PublicKey, bitcoinKey1: PublicKey, @@ -394,7 +397,7 @@ object EncodingType { } // @formatter:on -case class EncodedShortChannelIds(encoding: EncodingType, array: List[ShortChannelId]) { +case class EncodedShortChannelIds(encoding: EncodingType, array: List[RealShortChannelId]) { /** custom toString because it can get huge in logs */ override def toString: String = s"EncodedShortChannelIds($encoding,${array.headOption.getOrElse("")}->${array.lastOption.getOrElse("")} size=${array.size})" } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/EclairImplSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/EclairImplSpec.scala index 9154eb3154..ed46c2e0d8 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/EclairImplSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/EclairImplSpec.scala @@ -16,8 +16,8 @@ package fr.acinq.eclair -import akka.actor.ActorRef import akka.actor.typed.scaladsl.adapter.{ClassicActorRefOps, actorRefAdapter} +import akka.actor.{ActorRef, Status} import akka.pattern.pipe import akka.testkit.TestProbe import akka.util.Timeout @@ -29,6 +29,7 @@ import fr.acinq.eclair.blockchain.DummyOnChainWallet import fr.acinq.eclair.blockchain.fee.{FeeratePerByte, FeeratePerKw} import fr.acinq.eclair.channel._ import fr.acinq.eclair.db._ +import fr.acinq.eclair.io.Peer import fr.acinq.eclair.io.Peer.OpenChannel import fr.acinq.eclair.payment.Bolt11Invoice.ExtraHop import fr.acinq.eclair.payment.receive.MultiPartHandler.ReceivePayment @@ -207,11 +208,11 @@ class EclairImplSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with I ) val (a, b, c, d, e) = (a_priv.publicKey, b_priv.publicKey, c_priv.publicKey, d_priv.publicKey, e_priv.publicKey) - val ann_ab = Announcements.makeChannelAnnouncement(Block.RegtestGenesisBlock.hash, ShortChannelId(1), a, b, a, b, ByteVector64.Zeroes, ByteVector64.Zeroes, ByteVector64.Zeroes, ByteVector64.Zeroes) - val ann_ae = Announcements.makeChannelAnnouncement(Block.RegtestGenesisBlock.hash, ShortChannelId(4), a, e, a, e, ByteVector64.Zeroes, ByteVector64.Zeroes, ByteVector64.Zeroes, ByteVector64.Zeroes) - val ann_bc = Announcements.makeChannelAnnouncement(Block.RegtestGenesisBlock.hash, ShortChannelId(2), b, c, b, c, ByteVector64.Zeroes, ByteVector64.Zeroes, ByteVector64.Zeroes, ByteVector64.Zeroes) - val ann_cd = Announcements.makeChannelAnnouncement(Block.RegtestGenesisBlock.hash, ShortChannelId(3), c, d, c, d, ByteVector64.Zeroes, ByteVector64.Zeroes, ByteVector64.Zeroes, ByteVector64.Zeroes) - val ann_ec = Announcements.makeChannelAnnouncement(Block.RegtestGenesisBlock.hash, ShortChannelId(7), e, c, e, c, ByteVector64.Zeroes, ByteVector64.Zeroes, ByteVector64.Zeroes, ByteVector64.Zeroes) + val ann_ab = Announcements.makeChannelAnnouncement(Block.RegtestGenesisBlock.hash, RealShortChannelId(1), a, b, a, b, ByteVector64.Zeroes, ByteVector64.Zeroes, ByteVector64.Zeroes, ByteVector64.Zeroes) + val ann_ae = Announcements.makeChannelAnnouncement(Block.RegtestGenesisBlock.hash, RealShortChannelId(4), a, e, a, e, ByteVector64.Zeroes, ByteVector64.Zeroes, ByteVector64.Zeroes, ByteVector64.Zeroes) + val ann_bc = Announcements.makeChannelAnnouncement(Block.RegtestGenesisBlock.hash, RealShortChannelId(2), b, c, b, c, ByteVector64.Zeroes, ByteVector64.Zeroes, ByteVector64.Zeroes, ByteVector64.Zeroes) + val ann_cd = Announcements.makeChannelAnnouncement(Block.RegtestGenesisBlock.hash, RealShortChannelId(3), c, d, c, d, ByteVector64.Zeroes, ByteVector64.Zeroes, ByteVector64.Zeroes, ByteVector64.Zeroes) + val ann_ec = Announcements.makeChannelAnnouncement(Block.RegtestGenesisBlock.hash, RealShortChannelId(7), e, c, e, c, ByteVector64.Zeroes, ByteVector64.Zeroes, ByteVector64.Zeroes, ByteVector64.Zeroes) assert(Announcements.isNode1(a, b)) assert(Announcements.isNode1(b, c)) @@ -233,6 +234,25 @@ class EclairImplSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with I assert(sender.expectMsgType[Iterable[ChannelUpdate]].map(_.shortChannelId).toSet == Set(ShortChannelId(2))) } + test("open with bad arguments") { f => + import f._ + + val eclair = new EclairImpl(kit) + + // option_scid_alias is not compatible with public channels + eclair.open(randomKey().publicKey, 123456 sat, None, Some(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = true, zeroConf = true)), None, announceChannel_opt = Some(true), None).pipeTo(sender.ref) + assert(sender.expectMsgType[Status.Failure].cause.getMessage.contains("option_scid_alias is not compatible with public channels")) + + eclair.open(randomKey().publicKey, 123456 sat, None, Some(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = true, zeroConf = true)), None, announceChannel_opt = Some(false), None).pipeTo(sender.ref) + switchboard.expectMsgType[Peer.OpenChannel] + + eclair.open(randomKey().publicKey, 123456 sat, None, Some(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = true)), None, announceChannel_opt = Some(true), None).pipeTo(sender.ref) + switchboard.expectMsgType[Peer.OpenChannel] + + eclair.open(randomKey().publicKey, 123456 sat, None, Some(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = true)), None, announceChannel_opt = Some(false), None).pipeTo(sender.ref) + switchboard.expectMsgType[Peer.OpenChannel] + } + test("close and forceclose should work both with channelId and shortChannelId") { f => import f._ @@ -616,7 +636,7 @@ class EclairImplSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with I val eclair = new EclairImpl(kit) eclair.channelBalances().pipeTo(sender.ref) - relayer.expectMsg(GetOutgoingChannels(enabledOnly=false)) + relayer.expectMsg(GetOutgoingChannels(enabledOnly = false)) eclair.usableBalances().pipeTo(sender.ref) relayer.expectMsg(GetOutgoingChannels()) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/ShortChannelIdSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/ShortChannelIdSpec.scala index e26254dddf..ae6853e51a 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/ShortChannelIdSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/ShortChannelIdSpec.scala @@ -20,31 +20,29 @@ import org.scalatest.funsuite.AnyFunSuite import scala.util.Try - - class ShortChannelIdSpec extends AnyFunSuite { - test("handle values from 0 to 0xffffffffffff") { + test("handle real short channel ids from 0 to 0xffffffffffff") { val expected = Map( - TxCoordinates(BlockHeight(0), 0, 0) -> ShortChannelId(0), - TxCoordinates(BlockHeight(42000), 27, 3) -> ShortChannelId(0x0000a41000001b0003L), - TxCoordinates(BlockHeight(1258612), 63, 0) -> ShortChannelId(0x13347400003f0000L), - TxCoordinates(BlockHeight(0xffffff), 0x000000, 0xffff) -> ShortChannelId(0xffffff000000ffffL), - TxCoordinates(BlockHeight(0x000000), 0xffffff, 0xffff) -> ShortChannelId(0x000000ffffffffffL), - TxCoordinates(BlockHeight(0xffffff), 0xffffff, 0x0000) -> ShortChannelId(0xffffffffffff0000L), - TxCoordinates(BlockHeight(0xffffff), 0xffffff, 0xffff) -> ShortChannelId(0xffffffffffffffffL) + TxCoordinates(BlockHeight(0), 0, 0) -> RealShortChannelId(0), + TxCoordinates(BlockHeight(42000), 27, 3) -> RealShortChannelId(0x0000a41000001b0003L), + TxCoordinates(BlockHeight(1258612), 63, 0) -> RealShortChannelId(0x13347400003f0000L), + TxCoordinates(BlockHeight(0xffffff), 0x000000, 0xffff) -> RealShortChannelId(0xffffff000000ffffL), + TxCoordinates(BlockHeight(0x000000), 0xffffff, 0xffff) -> RealShortChannelId(0x000000ffffffffffL), + TxCoordinates(BlockHeight(0xffffff), 0xffffff, 0x0000) -> RealShortChannelId(0xffffffffffff0000L), + TxCoordinates(BlockHeight(0xffffff), 0xffffff, 0xffff) -> RealShortChannelId(0xffffffffffffffffL) ) for ((coord, shortChannelId) <- expected) { - assert(shortChannelId == ShortChannelId(coord.blockHeight, coord.txIndex, coord.outputIndex)) + assert(shortChannelId == RealShortChannelId(coord.blockHeight, coord.txIndex, coord.outputIndex)) assert(coord == ShortChannelId.coordinates(shortChannelId)) } } test("human readable format as per spec") { - assert(ShortChannelId(0x0000a41000001b0003L).toString == "42000x27x3") + assert(RealShortChannelId(0x0000a41000001b0003L).toString == "42000x27x3") } - test("parse a short channel it") { + test("parse a short channel id") { assert(ShortChannelId("42000x27x3").toLong == 0x0000a41000001b0003L) } @@ -57,4 +55,19 @@ class ShortChannelIdSpec extends AnyFunSuite { assert(Try(ShortChannelId("42000x27")).isFailure) assert(Try(ShortChannelId("42000x")).isFailure) } + + test("compare different types of short channel ids") { + val id = 123456 + val alias = Alias(id) + val realScid = RealShortChannelId(id) + val scid = ShortChannelId(id) + assert(alias == realScid) + assert(realScid == scid) + val m = Map(alias -> "alias", realScid -> "real", scid -> "unknown") + // all scids are in the same key space + assert(m.size == 1) + // Values outside of the range [0;0xffffffffffff] can be used for aliases. + Seq(-561L, 0xffffffffffffffffL, 0x2affffffffffffffL).foreach(id => assert(Alias(id) == UnspecifiedShortChannelId(id))) + } + } \ No newline at end of file diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/TestDatabases.scala b/eclair-core/src/test/scala/fr/acinq/eclair/TestDatabases.scala index 2930d4c07a..e613e7ab1c 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/TestDatabases.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/TestDatabases.scala @@ -71,7 +71,7 @@ object TestDatabases { // serialized and deserialized we need to turn "hot" payments into cold ones def freeze3(input: PersistentChannelData): PersistentChannelData = input match { case d: DATA_WAIT_FOR_FUNDING_CONFIRMED => d.copy(commitments = freeze2(d.commitments)) - case d: DATA_WAIT_FOR_FUNDING_LOCKED => d.copy(commitments = freeze2(d.commitments)) + case d: DATA_WAIT_FOR_CHANNEL_READY => d.copy(commitments = freeze2(d.commitments)) case d: DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT => d.copy(commitments = freeze2(d.commitments)) case d: DATA_NORMAL => d.copy(commitments = freeze2(d.commitments)) case d: DATA_CLOSING => d.copy(commitments = freeze2(d.commitments)) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/TestUtils.scala b/eclair-core/src/test/scala/fr/acinq/eclair/TestUtils.scala index 4979d559af..c8012d440b 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/TestUtils.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/TestUtils.scala @@ -16,17 +16,20 @@ package fr.acinq.eclair -import akka.actor.ActorRef -import akka.event.DiagnosticLoggingAdapter +import akka.actor.{ActorRef, ActorSystem} +import akka.event.{DiagnosticLoggingAdapter, EventStream} import akka.testkit.{TestActor, TestProbe} import fr.acinq.eclair.channel.fsm.Channel import fr.acinq.eclair.io.Peer import fr.acinq.eclair.wire.protocol.LightningMessage +import org.scalatest.concurrent.Eventually.eventually +import org.scalatest.concurrent.PatienceConfiguration import java.io.File import java.net.ServerSocket import java.nio.file.Files import java.util.UUID +import scala.concurrent.duration.{DurationInt, FiniteDuration} object TestUtils { @@ -89,4 +92,22 @@ object TestUtils { seedFile } + /** + * Subscribing to [[EventStream]] is asynchronous, which can lead to race conditions. + * + * We use a dummy event subscription and poll until we receive a message, and rely on the fact that + * [[EventStream]] is an actor which means that all previous subscriptions have been taken into account. + */ + def waitEventStreamSynced(eventStream: EventStream)(implicit system: ActorSystem): Unit = { + val listener = TestProbe() + case class DummyEvent() + eventStream.subscribe(listener.ref, classOf[DummyEvent]) + eventually { + eventStream.publish(DummyEvent()) + assert(listener.msgAvailable) + } + } + + def waitFor(duration: FiniteDuration): Unit = Thread.sleep(duration.toMillis) + } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreClientSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreClientSpec.scala index c37464d932..e9a8e7c740 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreClientSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreClientSpec.scala @@ -61,7 +61,7 @@ class BitcoinCoreClientSpec extends TestKitBaseClass with BitcoindService with A val bitcoinClient = new BitcoinCoreClient(bitcoinrpcclient) val walletPassword = Random.alphanumeric.take(8).mkString sender.send(bitcoincli, BitcoinReq("encryptwallet", walletPassword)) - sender.expectMsgType[JString] + sender.expectMsgType[JString](60 seconds) restartBitcoind(sender) val pubkeyScript = Script.write(Script.pay2wsh(Scripts.multiSig2of2(randomKey().publicKey, randomKey().publicKey))) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/ZmqWatcherSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/ZmqWatcherSpec.scala index b521692e20..544b9481db 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/ZmqWatcherSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/ZmqWatcherSpec.scala @@ -29,7 +29,7 @@ import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinCoreClient.{FundTransactio import fr.acinq.eclair.blockchain.bitcoind.zmq.ZMQActor import fr.acinq.eclair.blockchain.fee.FeeratePerKw import fr.acinq.eclair.blockchain.{CurrentBlockHeight, NewTransaction} -import fr.acinq.eclair.{BlockHeight, ShortChannelId, TestConstants, TestKitBaseClass, randomBytes32, randomKey} +import fr.acinq.eclair.{BlockHeight, RealShortChannelId, TestConstants, TestKitBaseClass, randomBytes32, randomKey} import grizzled.slf4j.Logging import org.scalatest.BeforeAndAfterAll import org.scalatest.funsuite.AnyFunSuiteLike @@ -108,8 +108,8 @@ class ZmqWatcherSpec extends TestKitBaseClass with AnyFunSuiteLike with Bitcoind val w1 = WatchFundingSpent(TestProbe().ref, txid, outputIndex, hints = Set.empty) val w2 = WatchFundingSpent(TestProbe().ref, txid, outputIndex, hints = Set.empty) - val w3 = WatchExternalChannelSpent(TestProbe().ref, txid, outputIndex, ShortChannelId(1)) - val w4 = WatchExternalChannelSpent(TestProbe().ref, randomBytes32(), 5, ShortChannelId(1)) + val w3 = WatchExternalChannelSpent(TestProbe().ref, txid, outputIndex, RealShortChannelId(1)) + val w4 = WatchExternalChannelSpent(TestProbe().ref, randomBytes32(), 5, RealShortChannelId(1)) val w5 = WatchFundingConfirmed(TestProbe().ref, txid, 3) // we test as if the collection was immutable @@ -210,7 +210,7 @@ class ZmqWatcherSpec extends TestKitBaseClass with AnyFunSuiteLike with Bitcoind val (tx1, tx2) = createUnspentTxChain(tx, priv) val listener = TestProbe() - watcher ! WatchExternalChannelSpent(listener.ref, tx.txid, outputIndex, ShortChannelId(5)) + watcher ! WatchExternalChannelSpent(listener.ref, tx.txid, outputIndex, RealShortChannelId(5)) watcher ! WatchFundingSpent(listener.ref, tx.txid, outputIndex, Set.empty) listener.expectNoMessage(1 second) @@ -221,7 +221,7 @@ class ZmqWatcherSpec extends TestKitBaseClass with AnyFunSuiteLike with Bitcoind probe.expectMsg(tx1.txid) // tx and tx1 aren't confirmed yet, but we trigger the WatchEventSpent when we see tx1 in the mempool. listener.expectMsgAllOf( - WatchExternalChannelSpentTriggered(ShortChannelId(5)), + WatchExternalChannelSpentTriggered(RealShortChannelId(5)), WatchFundingSpentTriggered(tx1) ) // Let's confirm tx and tx1: seeing tx1 in a block should trigger WatchEventSpent again, but not WatchEventSpentBasic @@ -267,8 +267,8 @@ class ZmqWatcherSpec extends TestKitBaseClass with AnyFunSuiteLike with Bitcoind listener.expectMsg(WatchOutputSpentTriggered(tx1)) watcher ! StopWatching(listener.ref) - watcher ! WatchExternalChannelSpent(listener.ref, tx1.txid, 0, ShortChannelId(1)) - listener.expectMsg(WatchExternalChannelSpentTriggered(ShortChannelId(1))) + watcher ! WatchExternalChannelSpent(listener.ref, tx1.txid, 0, RealShortChannelId(1)) + listener.expectMsg(WatchExternalChannelSpentTriggered(RealShortChannelId(1))) watcher ! WatchFundingSpent(listener.ref, tx1.txid, 0, Set.empty) listener.expectMsg(WatchFundingSpentTriggered(tx2)) }) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/FeeEstimatorSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/FeeEstimatorSpec.scala index 3e0567eb4a..4211416a7b 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/FeeEstimatorSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/FeeEstimatorSpec.scala @@ -57,34 +57,34 @@ class FeeEstimatorSpec extends AnyFunSuite { feeEstimator.setFeerate(FeeratesPerKw.single(FeeratePerKw(10000 sat)).copy(blocks_2 = defaultMaxCommitFeerate / 2, mempoolMinFee = FeeratePerKw(250 sat))) assert(feeConf.getCommitmentFeerate(defaultNodeId, ChannelTypes.AnchorOutputs, 100000 sat, None) == defaultMaxCommitFeerate / 2) - assert(feeConf.getCommitmentFeerate(defaultNodeId, ChannelTypes.AnchorOutputsZeroFeeHtlcTx, 100000 sat, None) == defaultMaxCommitFeerate / 2) + assert(feeConf.getCommitmentFeerate(defaultNodeId, ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false), 100000 sat, None) == defaultMaxCommitFeerate / 2) feeEstimator.setFeerate(FeeratesPerKw.single(FeeratePerKw(10000 sat)).copy(blocks_2 = defaultMaxCommitFeerate * 2, mempoolMinFee = FeeratePerKw(250 sat))) assert(feeConf.getCommitmentFeerate(defaultNodeId, ChannelTypes.AnchorOutputs, 100000 sat, None) == defaultMaxCommitFeerate) - assert(feeConf.getCommitmentFeerate(defaultNodeId, ChannelTypes.AnchorOutputsZeroFeeHtlcTx, 100000 sat, None) == defaultMaxCommitFeerate) + assert(feeConf.getCommitmentFeerate(defaultNodeId, ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false), 100000 sat, None) == defaultMaxCommitFeerate) assert(feeConf.getCommitmentFeerate(overrideNodeId, ChannelTypes.AnchorOutputs, 100000 sat, None) == overrideMaxCommitFeerate) - assert(feeConf.getCommitmentFeerate(overrideNodeId, ChannelTypes.AnchorOutputsZeroFeeHtlcTx, 100000 sat, None) == overrideMaxCommitFeerate) + assert(feeConf.getCommitmentFeerate(overrideNodeId, ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false), 100000 sat, None) == overrideMaxCommitFeerate) val currentFeerates1 = CurrentFeerates(FeeratesPerKw.single(FeeratePerKw(10000 sat)).copy(blocks_2 = defaultMaxCommitFeerate / 2, mempoolMinFee = FeeratePerKw(250 sat))) assert(feeConf.getCommitmentFeerate(defaultNodeId, ChannelTypes.AnchorOutputs, 100000 sat, Some(currentFeerates1)) == defaultMaxCommitFeerate / 2) - assert(feeConf.getCommitmentFeerate(defaultNodeId, ChannelTypes.AnchorOutputsZeroFeeHtlcTx, 100000 sat, Some(currentFeerates1)) == defaultMaxCommitFeerate / 2) + assert(feeConf.getCommitmentFeerate(defaultNodeId, ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false), 100000 sat, Some(currentFeerates1)) == defaultMaxCommitFeerate / 2) val currentFeerates2 = CurrentFeerates(FeeratesPerKw.single(FeeratePerKw(10000 sat)).copy(blocks_2 = defaultMaxCommitFeerate * 1.5, mempoolMinFee = FeeratePerKw(250 sat))) feeEstimator.setFeerate(FeeratesPerKw.single(FeeratePerKw(10000 sat)).copy(blocks_2 = defaultMaxCommitFeerate / 2, mempoolMinFee = FeeratePerKw(250 sat))) assert(feeConf.getCommitmentFeerate(defaultNodeId, ChannelTypes.AnchorOutputs, 100000 sat, Some(currentFeerates2)) == defaultMaxCommitFeerate) - assert(feeConf.getCommitmentFeerate(defaultNodeId, ChannelTypes.AnchorOutputsZeroFeeHtlcTx, 100000 sat, Some(currentFeerates2)) == defaultMaxCommitFeerate) + assert(feeConf.getCommitmentFeerate(defaultNodeId, ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false), 100000 sat, Some(currentFeerates2)) == defaultMaxCommitFeerate) val highFeerates = CurrentFeerates(FeeratesPerKw.single(FeeratePerKw(25000 sat)).copy(mempoolMinFee = FeeratePerKw(10000 sat))) assert(feeConf.getCommitmentFeerate(defaultNodeId, ChannelTypes.AnchorOutputs, 100000 sat, Some(highFeerates)) == FeeratePerKw(10000 sat) * 1.25) - assert(feeConf.getCommitmentFeerate(defaultNodeId, ChannelTypes.AnchorOutputsZeroFeeHtlcTx, 100000 sat, Some(highFeerates)) == FeeratePerKw(10000 sat) * 1.25) + assert(feeConf.getCommitmentFeerate(defaultNodeId, ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false), 100000 sat, Some(highFeerates)) == FeeratePerKw(10000 sat) * 1.25) assert(feeConf.getCommitmentFeerate(overrideNodeId, ChannelTypes.AnchorOutputs, 100000 sat, Some(highFeerates)) == FeeratePerKw(10000 sat) * 1.25) - assert(feeConf.getCommitmentFeerate(overrideNodeId, ChannelTypes.AnchorOutputsZeroFeeHtlcTx, 100000 sat, Some(highFeerates)) == FeeratePerKw(10000 sat) * 1.25) + assert(feeConf.getCommitmentFeerate(overrideNodeId, ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false), 100000 sat, Some(highFeerates)) == FeeratePerKw(10000 sat) * 1.25) feeEstimator.setFeerate(FeeratesPerKw.single(FeeratePerKw(25000 sat)).copy(mempoolMinFee = FeeratePerKw(10000 sat))) assert(feeConf.getCommitmentFeerate(defaultNodeId, ChannelTypes.AnchorOutputs, 100000 sat, None) == FeeratePerKw(10000 sat) * 1.25) - assert(feeConf.getCommitmentFeerate(defaultNodeId, ChannelTypes.AnchorOutputsZeroFeeHtlcTx, 100000 sat, None) == FeeratePerKw(10000 sat) * 1.25) + assert(feeConf.getCommitmentFeerate(defaultNodeId, ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false), 100000 sat, None) == FeeratePerKw(10000 sat) * 1.25) assert(feeConf.getCommitmentFeerate(overrideNodeId, ChannelTypes.AnchorOutputs, 100000 sat, None) == FeeratePerKw(10000 sat) * 1.25) - assert(feeConf.getCommitmentFeerate(overrideNodeId, ChannelTypes.AnchorOutputsZeroFeeHtlcTx, 100000 sat, None) == FeeratePerKw(10000 sat) * 1.25) + assert(feeConf.getCommitmentFeerate(overrideNodeId, ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false), 100000 sat, None) == FeeratePerKw(10000 sat) * 1.25) } test("fee difference too high") { @@ -123,7 +123,7 @@ class FeeEstimatorSpec extends AnyFunSuite { ) testCases.foreach { case (networkFeerate, proposedFeerate) => assert(!tolerance.isFeeDiffTooHigh(ChannelTypes.AnchorOutputs, networkFeerate, proposedFeerate)) - assert(!tolerance.isFeeDiffTooHigh(ChannelTypes.AnchorOutputsZeroFeeHtlcTx, networkFeerate, proposedFeerate)) + assert(!tolerance.isFeeDiffTooHigh(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false), networkFeerate, proposedFeerate)) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/ChannelFeaturesSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/ChannelFeaturesSpec.scala index ad3a4baeaf..d992176daa 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/ChannelFeaturesSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/ChannelFeaturesSpec.scala @@ -55,27 +55,34 @@ class ChannelFeaturesSpec extends TestKitBaseClass with AnyFunSuiteLike with Cha } test("pick channel type based on local and remote features") { - case class TestCase(localFeatures: Features[InitFeature], remoteFeatures: Features[InitFeature], expectedChannelType: ChannelType) + case class TestCase(localFeatures: Features[InitFeature], remoteFeatures: Features[InitFeature], announceChannel: Boolean, expectedChannelType: ChannelType) val testCases = Seq( - TestCase(Features.empty, Features.empty, ChannelTypes.Standard), - TestCase(Features(StaticRemoteKey -> Optional), Features.empty, ChannelTypes.Standard), - TestCase(Features.empty, Features(StaticRemoteKey -> Optional), ChannelTypes.Standard), - TestCase(Features.empty, Features(StaticRemoteKey -> Mandatory), ChannelTypes.Standard), - TestCase(Features(StaticRemoteKey -> Optional, Wumbo -> Mandatory), Features(Wumbo -> Mandatory), ChannelTypes.Standard), - TestCase(Features(StaticRemoteKey -> Optional), Features(StaticRemoteKey -> Optional), ChannelTypes.StaticRemoteKey), - TestCase(Features(StaticRemoteKey -> Optional), Features(StaticRemoteKey -> Mandatory), ChannelTypes.StaticRemoteKey), - TestCase(Features(StaticRemoteKey -> Optional, Wumbo -> Optional), Features(StaticRemoteKey -> Mandatory, Wumbo -> Mandatory), ChannelTypes.StaticRemoteKey), - TestCase(Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional), Features(StaticRemoteKey -> Optional), ChannelTypes.StaticRemoteKey), - TestCase(Features(StaticRemoteKey -> Mandatory, AnchorOutputsZeroFeeHtlcTx -> Optional), Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional), ChannelTypes.StaticRemoteKey), - TestCase(Features(StaticRemoteKey -> Mandatory, AnchorOutputs -> Optional), Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional), ChannelTypes.AnchorOutputs), - TestCase(Features(StaticRemoteKey -> Mandatory, AnchorOutputs -> Optional), Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTx -> Optional), ChannelTypes.AnchorOutputs), - TestCase(Features(StaticRemoteKey -> Mandatory, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTx -> Optional), Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTx -> Optional), ChannelTypes.AnchorOutputsZeroFeeHtlcTx), - TestCase(Features(StaticRemoteKey -> Mandatory, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTx -> Optional), Features(StaticRemoteKey -> Optional, AnchorOutputs -> Mandatory, AnchorOutputsZeroFeeHtlcTx -> Optional), ChannelTypes.AnchorOutputsZeroFeeHtlcTx), - TestCase(Features(StaticRemoteKey -> Mandatory, AnchorOutputsZeroFeeHtlcTx -> Optional), Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTx -> Mandatory), ChannelTypes.AnchorOutputsZeroFeeHtlcTx), + TestCase(Features.empty, Features.empty, announceChannel = true, ChannelTypes.Standard), + TestCase(Features(StaticRemoteKey -> Optional), Features.empty, announceChannel = true, ChannelTypes.Standard), + TestCase(Features.empty, Features(StaticRemoteKey -> Optional), announceChannel = true, ChannelTypes.Standard), + TestCase(Features.empty, Features(StaticRemoteKey -> Mandatory), announceChannel = true, ChannelTypes.Standard), + TestCase(Features(StaticRemoteKey -> Optional, Wumbo -> Mandatory), Features(Wumbo -> Mandatory), announceChannel = true, ChannelTypes.Standard), + TestCase(Features(StaticRemoteKey -> Optional), Features(StaticRemoteKey -> Optional), announceChannel = true, ChannelTypes.StaticRemoteKey), + TestCase(Features(StaticRemoteKey -> Optional), Features(StaticRemoteKey -> Mandatory), announceChannel = true, ChannelTypes.StaticRemoteKey), + TestCase(Features(StaticRemoteKey -> Optional, Wumbo -> Optional), Features(StaticRemoteKey -> Mandatory, Wumbo -> Mandatory), announceChannel = true, ChannelTypes.StaticRemoteKey), + TestCase(Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional), Features(StaticRemoteKey -> Optional), announceChannel = true, ChannelTypes.StaticRemoteKey), + TestCase(Features(StaticRemoteKey -> Mandatory, AnchorOutputsZeroFeeHtlcTx -> Optional), Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional), announceChannel = true, ChannelTypes.StaticRemoteKey), + TestCase(Features(StaticRemoteKey -> Mandatory, AnchorOutputs -> Optional), Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional), announceChannel = true, ChannelTypes.AnchorOutputs), + TestCase(Features(StaticRemoteKey -> Mandatory, AnchorOutputs -> Optional), Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTx -> Optional), announceChannel = true, ChannelTypes.AnchorOutputs), + TestCase(Features(StaticRemoteKey -> Mandatory, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTx -> Optional), Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTx -> Optional), announceChannel = true, ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)), + TestCase(Features(StaticRemoteKey -> Mandatory, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTx -> Optional), Features(StaticRemoteKey -> Optional, AnchorOutputs -> Mandatory, AnchorOutputsZeroFeeHtlcTx -> Optional), announceChannel = true, ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)), + TestCase(Features(StaticRemoteKey -> Mandatory, AnchorOutputsZeroFeeHtlcTx -> Optional), Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTx -> Mandatory), announceChannel = true, ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)), + TestCase(Features(StaticRemoteKey -> Mandatory, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTx -> Optional, Features.ScidAlias -> Optional), Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTx -> Optional), announceChannel = true, ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)), + TestCase(Features(StaticRemoteKey -> Mandatory, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTx -> Optional, Features.ScidAlias -> Optional), Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTx -> Optional, Features.ScidAlias -> Optional), announceChannel = true, ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)), + TestCase(Features(StaticRemoteKey -> Mandatory, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTx -> Optional, Features.ScidAlias -> Optional), Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTx -> Optional, Features.ScidAlias -> Optional), announceChannel = false, ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = true, zeroConf = false)), + TestCase(Features(StaticRemoteKey -> Mandatory, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTx -> Optional, Features.ScidAlias -> Optional), Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTx -> Optional, Features.ZeroConf -> Optional), announceChannel = true, ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)), + TestCase(Features(StaticRemoteKey -> Mandatory, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTx -> Optional, Features.ZeroConf -> Optional), Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTx -> Optional, Features.ZeroConf -> Optional), announceChannel = true, ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = true)), + TestCase(Features(StaticRemoteKey -> Mandatory, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTx -> Optional, Features.ScidAlias -> Mandatory, Features.ZeroConf -> Optional), Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTx -> Optional, Features.ScidAlias -> Optional, Features.ZeroConf -> Optional), announceChannel = true, ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = true)), + TestCase(Features(StaticRemoteKey -> Mandatory, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTx -> Optional, Features.ScidAlias -> Mandatory, Features.ZeroConf -> Optional), Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTx -> Optional, Features.ScidAlias -> Optional, Features.ZeroConf -> Optional), announceChannel = false, ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = true, zeroConf = true)), ) for (testCase <- testCases) { - assert(ChannelTypes.defaultFromFeatures(testCase.localFeatures, testCase.remoteFeatures) == testCase.expectedChannelType, s"localFeatures=${testCase.localFeatures} remoteFeatures=${testCase.remoteFeatures}") + assert(ChannelTypes.defaultFromFeatures(testCase.localFeatures, testCase.remoteFeatures, testCase.announceChannel) == testCase.expectedChannelType, s"localFeatures=${testCase.localFeatures} remoteFeatures=${testCase.remoteFeatures}") } } @@ -86,7 +93,10 @@ class ChannelFeaturesSpec extends TestKitBaseClass with AnyFunSuiteLike with Cha TestCase(Features.empty, ChannelTypes.Standard), TestCase(Features(StaticRemoteKey -> Mandatory), ChannelTypes.StaticRemoteKey), TestCase(Features(StaticRemoteKey -> Mandatory, AnchorOutputs -> Mandatory), ChannelTypes.AnchorOutputs), - TestCase(Features(StaticRemoteKey -> Mandatory, AnchorOutputsZeroFeeHtlcTx -> Mandatory), ChannelTypes.AnchorOutputsZeroFeeHtlcTx), + TestCase(Features(StaticRemoteKey -> Mandatory, AnchorOutputsZeroFeeHtlcTx -> Mandatory), ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)), + TestCase(Features(StaticRemoteKey -> Mandatory, AnchorOutputsZeroFeeHtlcTx -> Mandatory, ScidAlias -> Mandatory), ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = true, zeroConf = false)), + TestCase(Features(StaticRemoteKey -> Mandatory, AnchorOutputsZeroFeeHtlcTx -> Mandatory, ZeroConf -> Mandatory), ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = true)), + TestCase(Features(StaticRemoteKey -> Mandatory, AnchorOutputsZeroFeeHtlcTx -> Mandatory, ScidAlias -> Mandatory, ZeroConf -> Mandatory), ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = true, zeroConf = true)), ) for (testCase <- validChannelTypes) { assert(ChannelTypes.fromFeatures(testCase.features) == testCase.expectedChannelType, testCase.features) @@ -121,9 +131,9 @@ class ChannelFeaturesSpec extends TestKitBaseClass with AnyFunSuiteLike with Cha TestCase(ChannelTypes.StaticRemoteKey, Features(Wumbo -> Optional), Features(Wumbo -> Optional), Set(StaticRemoteKey, Wumbo)), TestCase(ChannelTypes.AnchorOutputs, Features.empty, Features(Wumbo -> Optional), Set(StaticRemoteKey, AnchorOutputs)), TestCase(ChannelTypes.AnchorOutputs, Features(Wumbo -> Optional), Features(Wumbo -> Mandatory), Set(StaticRemoteKey, AnchorOutputs, Wumbo)), - TestCase(ChannelTypes.AnchorOutputsZeroFeeHtlcTx, Features.empty, Features(Wumbo -> Optional), Set(StaticRemoteKey, AnchorOutputsZeroFeeHtlcTx)), - TestCase(ChannelTypes.AnchorOutputsZeroFeeHtlcTx, Features(Wumbo -> Optional), Features(Wumbo -> Mandatory), Set(StaticRemoteKey, AnchorOutputsZeroFeeHtlcTx, Wumbo)), - TestCase(ChannelTypes.AnchorOutputsZeroFeeHtlcTx, Features(DualFunding -> Optional, Wumbo -> Optional), Features(DualFunding -> Optional, Wumbo -> Optional), Set(StaticRemoteKey, AnchorOutputsZeroFeeHtlcTx, Wumbo, DualFunding)), + TestCase(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false), Features.empty, Features(Wumbo -> Optional), Set(StaticRemoteKey, AnchorOutputsZeroFeeHtlcTx)), + TestCase(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false), Features(Wumbo -> Optional), Features(Wumbo -> Mandatory), Set(StaticRemoteKey, AnchorOutputsZeroFeeHtlcTx, Wumbo)), + TestCase(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false), Features(DualFunding -> Optional, Wumbo -> Optional), Features(DualFunding -> Optional, Wumbo -> Optional), Set(StaticRemoteKey, AnchorOutputsZeroFeeHtlcTx, Wumbo, DualFunding)), ) testCases.foreach(t => assert(ChannelFeatures(t.channelType, t.localFeatures, t.remoteFeatures).features == t.expected, s"channelType=${t.channelType} localFeatures=${t.localFeatures} remoteFeatures=${t.remoteFeatures}")) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/HelpersSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/HelpersSpec.scala index 47de53b8d3..d521d6c5f8 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/HelpersSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/HelpersSpec.scala @@ -27,7 +27,7 @@ import fr.acinq.eclair.channel.fsm.Channel import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsTags} import fr.acinq.eclair.transactions.Transactions._ import fr.acinq.eclair.wire.protocol.UpdateAddHtlc -import fr.acinq.eclair.{BlockHeight, MilliSatoshiLong, TestKitBaseClass, TimestampSecond, TimestampSecondLong} +import fr.acinq.eclair.{BlockHeight, Features, MilliSatoshiLong, TestKitBaseClass, TimestampSecond, TimestampSecondLong} import org.scalatest.Tag import org.scalatest.funsuite.AnyFunSuiteLike import scodec.bits.HexStringSyntax @@ -40,13 +40,14 @@ class HelpersSpec extends TestKitBaseClass with AnyFunSuiteLike with ChannelStat implicit val log: akka.event.LoggingAdapter = akka.event.NoLogging test("compute the funding tx min depth according to funding amount") { - assert(Helpers.minDepthForFunding(nodeParams.channelConf, Btc(1)) == 4) - assert(Helpers.minDepthForFunding(nodeParams.channelConf.copy(minDepthBlocks = 6), Btc(1)) == 6) // 4 conf would be enough but we use min-depth=6 - assert(Helpers.minDepthForFunding(nodeParams.channelConf, Btc(6.25)) == 16) // we use scaling_factor=15 and a fixed block reward of 6.25BTC - assert(Helpers.minDepthForFunding(nodeParams.channelConf, Btc(12.50)) == 31) - assert(Helpers.minDepthForFunding(nodeParams.channelConf, Btc(12.60)) == 32) - assert(Helpers.minDepthForFunding(nodeParams.channelConf, Btc(30)) == 73) - assert(Helpers.minDepthForFunding(nodeParams.channelConf, Btc(50)) == 121) + assert(Helpers.Funding.minDepthFundee(nodeParams.channelConf, ChannelFeatures(), Btc(1)).contains(4)) + assert(Helpers.Funding.minDepthFundee(nodeParams.channelConf.copy(minDepthBlocks = 6), ChannelFeatures(), Btc(1)).contains(6)) // 4 conf would be enough but we use min-depth=6 + assert(Helpers.Funding.minDepthFundee(nodeParams.channelConf, ChannelFeatures(), Btc(6.25)).contains(16)) // we use scaling_factor=15 and a fixed block reward of 6.25BTC + assert(Helpers.Funding.minDepthFundee(nodeParams.channelConf, ChannelFeatures(), Btc(12.50)).contains(31)) + assert(Helpers.Funding.minDepthFundee(nodeParams.channelConf, ChannelFeatures(), Btc(12.60)).contains(32)) + assert(Helpers.Funding.minDepthFundee(nodeParams.channelConf, ChannelFeatures(), Btc(30)).contains(73)) + assert(Helpers.Funding.minDepthFundee(nodeParams.channelConf, ChannelFeatures(), Btc(50)).contains(121)) + assert(Helpers.Funding.minDepthFundee(nodeParams.channelConf, ChannelFeatures(Features.ZeroConf), Btc(50)).isEmpty) } test("compute refresh delay") { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/RestoreSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/RestoreSpec.scala index 687cf9a917..256d4b90df 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/RestoreSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/RestoreSpec.scala @@ -5,10 +5,10 @@ import akka.actor.typed.scaladsl.adapter.actorRefAdapter import akka.testkit import akka.testkit.{TestActor, TestFSMRef, TestProbe} import com.softwaremill.quicklens.ModifyPimp +import fr.acinq.bitcoin +import fr.acinq.bitcoin.ScriptFlags import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.bitcoin.scalacompat._ -import fr.acinq.bitcoin.ScriptFlags -import fr.acinq.bitcoin import fr.acinq.eclair.TestConstants.{Alice, Bob} import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.WatchFundingSpentTriggered import fr.acinq.eclair.channel.fsm.Channel @@ -19,7 +19,7 @@ import fr.acinq.eclair.crypto.keymanager.ChannelKeyManager import fr.acinq.eclair.router.Announcements import fr.acinq.eclair.transactions.Scripts import fr.acinq.eclair.transactions.Transactions.{ClaimP2WPKHOutputTx, DefaultCommitmentFormat, InputInfo, TxOwner} -import fr.acinq.eclair.wire.protocol.{ChannelReestablish, CommitSig, Error, FundingLocked, Init, RevokeAndAck} +import fr.acinq.eclair.wire.protocol.{ChannelReady, ChannelReestablish, ChannelUpdate, CommitSig, Error, Init, RevokeAndAck} import fr.acinq.eclair.{TestKitBaseClass, _} import org.scalatest.Outcome import org.scalatest.funsuite.FixtureAnyFunSuiteLike @@ -31,19 +31,16 @@ class RestoreSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Chan type FixtureParam = SetupFixture override def withFixture(test: OneArgTest): Outcome = { - val setup = test.tags.contains("disable-offline-mismatch") match { - case false => init() - case true => init(nodeParamsA = Alice.nodeParams.copy(onChainFeeConf = Alice.nodeParams.onChainFeeConf.copy(closeOnOfflineMismatch = false))) - } + val setup = init() within(30 seconds) { reachNormal(setup) withFixture(test.toNoArgTest(setup)) } } - def aliceInit = Init(Alice.nodeParams.features.initFeatures()) + private def aliceInit = Init(Alice.nodeParams.features.initFeatures()) - def bobInit = Init(Bob.nodeParams.features.initFeatures()) + private def bobInit = Init(Bob.nodeParams.features.initFeatures()) test("use funding pubkeys from publish commitment to spend our output") { f => import f._ @@ -136,14 +133,14 @@ class RestoreSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Chan /** We are only interested in channel updates from Alice, we use the channel flag to discriminate */ def aliceChannelUpdateListener(channelUpdateListener: TestProbe): TestProbe = { val aliceListener = TestProbe() - channelUpdateListener.setAutoPilot(new testkit.TestActor.AutoPilot { - override def run(sender: ActorRef, msg: Any): TestActor.AutoPilot = msg match { + channelUpdateListener.setAutoPilot { + (sender: ActorRef, msg: Any) => msg match { case u: ChannelUpdateParametersChanged if u.channelUpdate.channelFlags.isNode1 == Announcements.isNode1(Alice.nodeParams.nodeId, Bob.nodeParams.nodeId) => aliceListener.ref.tell(msg, sender) TestActor.KeepRunning case _ => TestActor.KeepRunning } - }) + } aliceListener } @@ -206,6 +203,10 @@ class RestoreSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Chan // and we terminate Alice alice.stop() + // there should ne no pending messages + alice2bob.expectNoMessage() + bob2alice.expectNoMessage() + // we restart Alice with different configurations Seq( Alice.nodeParams @@ -234,10 +235,15 @@ class RestoreSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Chan bob2alice.expectMsgType[ChannelReestablish] alice2bob.forward(bob) bob2alice.forward(newAlice) - alice2bob.expectMsgType[FundingLocked] - bob2alice.expectMsgType[FundingLocked] + alice2bob.expectMsgType[ChannelReady] + bob2alice.expectMsgType[ChannelReady] alice2bob.forward(bob) bob2alice.forward(newAlice) + alice2bob.expectMsgType[ChannelUpdate] + bob2alice.expectMsgType[ChannelUpdate] + alice2bob.expectNoMessage() + bob2alice.expectNoMessage() + awaitCond(newAlice.stateName == NORMAL) awaitCond(bob.stateName == NORMAL) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPublisherSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPublisherSpec.scala index 3d136c5ae7..1dc458a684 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPublisherSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPublisherSpec.scala @@ -131,7 +131,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w val aliceNodeParams = TestConstants.Alice.nodeParams.copy(blockHeight = blockHeight) val setup = init(aliceNodeParams, TestConstants.Bob.nodeParams.copy(blockHeight = blockHeight), walletClient) val testTags = channelType match { - case ChannelTypes.AnchorOutputsZeroFeeHtlcTx => Set(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs) + case _: ChannelTypes.AnchorOutputsZeroFeeHtlcTx => Set(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs) case ChannelTypes.AnchorOutputs => Set(ChannelStateTestsTags.AnchorOutputs) case ChannelTypes.StaticRemoteKey => Set(ChannelStateTestsTags.StaticRemoteKey) case _ => Set.empty[String] @@ -168,7 +168,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } test("commit tx feerate high enough, not spending anchor output") { - withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx) { f => + withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)) { f => import f._ val commitFeerate = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.spec.commitTxFeerate @@ -183,7 +183,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } test("commit tx confirmed, not spending anchor output") { - withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx) { f => + withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)) { f => import f._ val (commitTx, anchorTx) = closeChannelWithoutHtlcs(f, aliceBlockHeight() + 12) @@ -200,7 +200,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } test("commit tx feerate high enough and commit tx confirmed, not spending anchor output") { - withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx) { f => + withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)) { f => import f._ val commitFeerate = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.spec.commitTxFeerate @@ -218,7 +218,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } test("remote commit tx confirmed, not spending anchor output") { - withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx) { f => + withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)) { f => import f._ val remoteCommit = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.fullySignedLocalCommitTx(bob.underlyingActor.nodeParams.channelKeyManager) @@ -236,7 +236,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } test("remote commit tx published, not spending anchor output") { - withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx) { f => + withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)) { f => import f._ val remoteCommit = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.fullySignedLocalCommitTx(bob.underlyingActor.nodeParams.channelKeyManager) @@ -255,7 +255,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } test("remote commit tx replaces local commit tx, not spending anchor output") { - withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx) { f => + withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)) { f => import f._ val remoteCommit = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.fullySignedLocalCommitTx(bob.underlyingActor.nodeParams.channelKeyManager) @@ -291,7 +291,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } test("funding tx not found, skipping anchor output") { - withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx) { f => + withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)) { f => import f._ val (_, anchorTx) = closeChannelWithoutHtlcs(f, aliceBlockHeight() + 12) @@ -307,7 +307,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } test("not enough funds to increase commit tx feerate") { - withFixture(Seq(10.5 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx) { f => + withFixture(Seq(10.5 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)) { f => import f._ // close channel and wait for the commit tx to be published, anchor will not be published because we don't have enough funds @@ -326,7 +326,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } test("commit tx feerate too low, spending anchor output") { - withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx) { f => + withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)) { f => import f._ val (commitTx, anchorTx) = closeChannelWithoutHtlcs(f, aliceBlockHeight() + 30) @@ -356,7 +356,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } test("commit tx not published, publishing it and spending anchor output") { - withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx) { f => + withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)) { f => import f._ val (commitTx, anchorTx) = closeChannelWithoutHtlcs(f, aliceBlockHeight() + 32) @@ -392,7 +392,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w 22000 sat, 15000 sat ) - withFixture(utxos, ChannelTypes.AnchorOutputsZeroFeeHtlcTx) { f => + withFixture(utxos, ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)) { f => import f._ // NB: we try to get transactions confirmed *before* their confirmation target, so we aim for a more aggressive block target than what's provided. @@ -420,7 +420,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } test("commit tx fees not increased when confirmation target is far and feerate hasn't changed") { - withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx) { f => + withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)) { f => import f._ val (commitTx, anchorTx) = closeChannelWithoutHtlcs(f, aliceBlockHeight() + 30) @@ -443,7 +443,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } test("commit tx not confirming, lowering anchor output amount") { - withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx) { f => + withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)) { f => import f._ val (commitTx, anchorTx) = closeChannelWithoutHtlcs(f, aliceBlockHeight() + 30) @@ -481,7 +481,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } test("commit tx not confirming, adding other wallet inputs") { - withFixture(Seq(10.5 millibtc, 5 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx) { f => + withFixture(Seq(10.5 millibtc, 5 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)) { f => import f._ val (commitTx, anchorTx) = closeChannelWithoutHtlcs(f, aliceBlockHeight() + 30) @@ -520,7 +520,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } test("commit tx not confirming, not enough funds to increase fees") { - withFixture(Seq(10.2 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx) { f => + withFixture(Seq(10.2 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)) { f => import f._ val (commitTx, anchorTx) = closeChannelWithoutHtlcs(f, aliceBlockHeight() + 30) @@ -554,7 +554,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } test("commit tx not confirming, cannot use new unconfirmed inputs to increase fees") { - withFixture(Seq(10.2 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx) { f => + withFixture(Seq(10.2 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)) { f => import f._ val (commitTx, anchorTx) = closeChannelWithoutHtlcs(f, aliceBlockHeight() + 30) @@ -585,7 +585,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } test("commit tx not confirming, updating confirmation target") { - withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx) { f => + withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)) { f => import f._ val (commitTx, anchorTx) = closeChannelWithoutHtlcs(f, aliceBlockHeight() + 30) @@ -628,7 +628,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } test("unlock utxos when anchor tx cannot be published") { - withFixture(Seq(500 millibtc, 200 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx) { f => + withFixture(Seq(500 millibtc, 200 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)) { f => import f._ val targetFeerate = FeeratePerKw(3000 sat) @@ -660,7 +660,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } test("unlock anchor utxos when stopped before completion") { - withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx) { f => + withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)) { f => import f._ val targetFeerate = FeeratePerKw(3000 sat) @@ -679,7 +679,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } test("remote commit tx confirmed, not publishing htlc tx") { - withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx) { f => + withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)) { f => import f._ // Add htlcs in both directions and ensure that preimages are available. @@ -768,7 +768,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } test("not enough funds to increase htlc tx feerate") { - withFixture(Seq(10.5 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx) { f => + withFixture(Seq(10.5 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)) { f => import f._ val (commitTx, htlcSuccess, _) = closeChannelWithHtlcs(f, aliceBlockHeight()) @@ -866,7 +866,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } test("htlc tx feerate zero, adding wallet inputs") { - withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx) { f => + withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)) { f => import f._ val targetFeerate = FeeratePerKw(15_000 sat) @@ -883,7 +883,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } test("htlc tx feerate zero, high commit feerate, adding wallet inputs") { - withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx) { f => + withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)) { f => import f._ val commitFeerate = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.spec.commitTxFeerate @@ -915,7 +915,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w 5200 sat, 5100 sat ) - withFixture(utxos, ChannelTypes.AnchorOutputsZeroFeeHtlcTx) { f => + withFixture(utxos, ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)) { f => import f._ val targetFeerate = FeeratePerKw(8_000 sat) @@ -930,7 +930,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } test("htlc success tx not confirming, lowering output amount") { - withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx) { f => + withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)) { f => import f._ val initialFeerate = FeeratePerKw(15_000 sat) @@ -966,7 +966,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } test("htlc success tx not confirming, adding other wallet inputs") { - withFixture(Seq(10.2 millibtc, 2 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx) { f => + withFixture(Seq(10.2 millibtc, 2 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)) { f => import f._ val initialFeerate = FeeratePerKw(15_000 sat) @@ -1002,7 +1002,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } test("htlc success tx confirmation target reached, increasing fees") { - withFixture(Seq(50 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx) { f => + withFixture(Seq(50 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)) { f => import f._ val initialFeerate = FeeratePerKw(10_000 sat) @@ -1034,7 +1034,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } test("htlc timeout tx not confirming, increasing fees") { - withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx) { f => + withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)) { f => import f._ val feerate = FeeratePerKw(15_000 sat) @@ -1072,7 +1072,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } test("utxos count too low, setting short confirmation target") { - withFixture(Seq(15 millibtc, 10 millibtc, 5 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx) { f => + withFixture(Seq(15 millibtc, 10 millibtc, 5 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)) { f => import f._ val (commitTx, htlcSuccess, _) = closeChannelWithHtlcs(f, aliceBlockHeight() + 144) @@ -1094,7 +1094,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } test("unlock utxos when htlc tx cannot be published") { - withFixture(Seq(500 millibtc, 200 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx) { f => + withFixture(Seq(500 millibtc, 200 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)) { f => import f._ val targetFeerate = FeeratePerKw(5_000 sat) @@ -1129,7 +1129,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } test("unlock htlc utxos when stopped before completion") { - withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx) { f => + withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)) { f => import f._ setFeerate(FeeratePerKw(5_000 sat)) @@ -1146,7 +1146,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } test("local commit tx confirmed, not publishing claim htlc tx") { - withFixture(Seq(11 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx) { f => + withFixture(Seq(11 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)) { f => import f._ // Add htlcs in both directions and ensure that preimages are available. @@ -1283,7 +1283,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } test("claim htlc tx feerate high enough, not changing output amount") { - withFixture(Seq(11 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx) { f => + withFixture(Seq(11 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)) { f => import f._ val currentFeerate = alice.underlyingActor.nodeParams.onChainFeeConf.feeEstimator.getFeeratePerKw(2) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala index 5caba83160..b25046e630 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala @@ -72,6 +72,10 @@ object ChannelStateTestsTags { val HighDustLimitDifferenceBobAlice = "high_dust_limit_difference_bob_alice" /** If set, channels will use option_channel_type. */ val ChannelType = "option_channel_type" + /** If set, channels will use option_zeroconf. */ + val ZeroConf = "zeroconf" + /** If set, channels will use option_scid_alias. */ + val ScidAlias = "scid_alias" } trait ChannelStateTestsBase extends Assertions with Eventually { @@ -146,7 +150,7 @@ trait ChannelStateTestsBase extends Assertions with Eventually { SetupFixture(alice, bob, aliceOrigin, alice2bob, bob2alice, alice2blockchain, bob2blockchain, router, alice2relayer, bob2relayer, channelUpdateListener, wallet, alicePeer, bobPeer) } - def computeFeatures(setup: SetupFixture, tags: Set[String]): (LocalParams, LocalParams, SupportedChannelType) = { + def computeFeatures(setup: SetupFixture, tags: Set[String], channelFlags: ChannelFlags): (LocalParams, LocalParams, SupportedChannelType) = { import setup._ val aliceInitFeatures = Alice.nodeParams.features @@ -157,6 +161,8 @@ trait ChannelStateTestsBase extends Assertions with Eventually { .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.ShutdownAnySegwit))(_.updated(Features.ShutdownAnySegwit, FeatureSupport.Optional)) .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.UpfrontShutdownScript))(_.updated(Features.UpfrontShutdownScript, FeatureSupport.Optional)) .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.ChannelType))(_.updated(Features.ChannelType, FeatureSupport.Optional)) + .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.ZeroConf))(_.updated(Features.ZeroConf, FeatureSupport.Optional)) + .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.ScidAlias))(_.updated(Features.ScidAlias, FeatureSupport.Optional)) .initFeatures() val bobInitFeatures = Bob.nodeParams.features .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.Wumbo))(_.updated(Features.Wumbo, FeatureSupport.Optional)) @@ -166,9 +172,15 @@ trait ChannelStateTestsBase extends Assertions with Eventually { .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.ShutdownAnySegwit))(_.updated(Features.ShutdownAnySegwit, FeatureSupport.Optional)) .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.UpfrontShutdownScript))(_.updated(Features.UpfrontShutdownScript, FeatureSupport.Optional)) .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.ChannelType))(_.updated(Features.ChannelType, FeatureSupport.Optional)) + .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.ZeroConf))(_.updated(Features.ZeroConf, FeatureSupport.Optional)) + .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.ScidAlias))(_.updated(Features.ScidAlias, FeatureSupport.Optional)) .initFeatures() - val channelType = ChannelTypes.defaultFromFeatures(aliceInitFeatures, bobInitFeatures) + val channelType = ChannelTypes.defaultFromFeatures(aliceInitFeatures, bobInitFeatures, channelFlags.announceChannel) + + // those features can only be enabled with AnchorOutputsZeroFeeHtlcTxs, this is to prevent incompatible test configurations + if (tags.contains(ChannelStateTestsTags.ZeroConf)) assert(tags.contains(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs), "invalid test configuration") + if (tags.contains(ChannelStateTestsTags.ScidAlias)) assert(channelType.features.contains(Features.ScidAlias), "invalid test configuration") implicit val ec: scala.concurrent.ExecutionContext = scala.concurrent.ExecutionContext.global val aliceParams = Alice.channelParams @@ -188,13 +200,13 @@ trait ChannelStateTestsBase extends Assertions with Eventually { (aliceParams, bobParams, channelType) } - def reachNormal(setup: SetupFixture, tags: Set[String] = Set.empty): Transaction = { + def reachNormal(setup: SetupFixture, tags: Set[String] = Set.empty, interceptChannelUpdates: Boolean = true): Transaction = { import setup._ val channelConfig = ChannelConfig.standard - val (aliceParams, bobParams, channelType) = computeFeatures(setup, tags) val channelFlags = ChannelFlags(announceChannel = tags.contains(ChannelStateTestsTags.ChannelsPublic)) + val (aliceParams, bobParams, channelType) = computeFeatures(setup, tags, channelFlags) val commitTxFeerate = if (tags.contains(ChannelStateTestsTags.AnchorOutputs) || tags.contains(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) TestConstants.anchorOutputsFeeratePerKw else TestConstants.feeratePerKw val (fundingSatoshis, pushMsat) = if (tags.contains(ChannelStateTestsTags.NoPushMsat)) { (TestConstants.fundingSatoshis, 0.msat) @@ -202,6 +214,9 @@ trait ChannelStateTestsBase extends Assertions with Eventually { (TestConstants.fundingSatoshis, TestConstants.pushMsat) } + val eventListener = TestProbe() + systemA.eventStream.subscribe(eventListener.ref, classOf[TransactionPublished]) + val aliceInit = Init(aliceParams.initFeatures) val bobInit = Init(bobParams.initFeatures) alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, fundingSatoshis, pushMsat, commitTxFeerate, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType) @@ -218,21 +233,29 @@ trait ChannelStateTestsBase extends Assertions with Eventually { bob2alice.forward(alice) assert(alice2blockchain.expectMsgType[TxPublisher.SetChannelId].channelId != ByteVector32.Zeroes) alice2blockchain.expectMsgType[WatchFundingSpent] - alice2blockchain.expectMsgType[WatchFundingConfirmed] assert(bob2blockchain.expectMsgType[TxPublisher.SetChannelId].channelId != ByteVector32.Zeroes) bob2blockchain.expectMsgType[WatchFundingSpent] - bob2blockchain.expectMsgType[WatchFundingConfirmed] - - eventually(assert(alice.stateName == WAIT_FOR_FUNDING_CONFIRMED)) - val fundingTx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CONFIRMED].fundingTx.get - alice ! WatchFundingConfirmedTriggered(BlockHeight(400000), 42, fundingTx) - bob ! WatchFundingConfirmedTriggered(BlockHeight(400000), 42, fundingTx) + val fundingTx = eventListener.expectMsgType[TransactionPublished].tx + if (!channelType.features.contains(Features.ZeroConf)) { + alice2blockchain.expectMsgType[WatchFundingConfirmed] + bob2blockchain.expectMsgType[WatchFundingConfirmed] + eventually(assert(alice.stateName == WAIT_FOR_FUNDING_CONFIRMED)) + alice ! WatchFundingConfirmedTriggered(BlockHeight(400000), 42, fundingTx) + bob ! WatchFundingConfirmedTriggered(BlockHeight(400000), 42, fundingTx) + } + eventually(assert(alice.stateName == WAIT_FOR_CHANNEL_READY)) + eventually(assert(bob.stateName == WAIT_FOR_CHANNEL_READY)) alice2blockchain.expectMsgType[WatchFundingLost] bob2blockchain.expectMsgType[WatchFundingLost] - alice2bob.expectMsgType[FundingLocked] + alice2bob.expectMsgType[ChannelReady] alice2bob.forward(bob) - bob2alice.expectMsgType[FundingLocked] + bob2alice.expectMsgType[ChannelReady] bob2alice.forward(alice) + if (interceptChannelUpdates) { + // we don't forward the channel updates, in reality they would be processed by the router + alice2bob.expectMsgType[ChannelUpdate] + bob2alice.expectMsgType[ChannelUpdate] + } alice2blockchain.expectMsgType[WatchFundingDeeplyBuried] bob2blockchain.expectMsgType[WatchFundingDeeplyBuried] eventually(assert(alice.stateName == NORMAL)) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForAcceptChannelStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForAcceptChannelStateSpec.scala index 040e255f29..3ed6f1429b 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForAcceptChannelStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForAcceptChannelStateSpec.scala @@ -56,14 +56,15 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS import setup._ val channelConfig = ChannelConfig.standard - val (aliceParams, bobParams, defaultChannelType) = computeFeatures(setup, test.tags) + val channelFlags = ChannelFlags.Private + val (aliceParams, bobParams, defaultChannelType) = computeFeatures(setup, test.tags, channelFlags) val channelType = if (test.tags.contains("standard-channel-type")) ChannelTypes.Standard else defaultChannelType - val commitTxFeerate = if (channelType == ChannelTypes.AnchorOutputs || channelType == ChannelTypes.AnchorOutputsZeroFeeHtlcTx) TestConstants.anchorOutputsFeeratePerKw else TestConstants.feeratePerKw + val commitTxFeerate = if (channelType == ChannelTypes.AnchorOutputs || channelType.isInstanceOf[ChannelTypes.AnchorOutputsZeroFeeHtlcTx]) TestConstants.anchorOutputsFeeratePerKw else TestConstants.feeratePerKw val aliceInit = Init(aliceParams.initFeatures) val bobInit = Init(bobParams.initFeatures) within(30 seconds) { val fundingAmount = if (test.tags.contains(ChannelStateTestsTags.Wumbo)) Btc(5).toSatoshi else TestConstants.fundingSatoshis - alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, fundingAmount, TestConstants.pushMsat, commitTxFeerate, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, ChannelFlags.Private, channelConfig, channelType) + alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, fundingAmount, TestConstants.pushMsat, commitTxFeerate, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType) bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType) alice2bob.expectMsgType[OpenChannel] alice2bob.forward(bob) @@ -96,10 +97,20 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS test("recv AcceptChannel (anchor outputs zero fee htlc txs)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => import f._ val accept = bob2alice.expectMsgType[AcceptChannel] - assert(accept.channelType_opt == Some(ChannelTypes.AnchorOutputsZeroFeeHtlcTx)) + assert(accept.channelType_opt == Some(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false))) bob2alice.forward(alice) awaitCond(alice.stateName == WAIT_FOR_FUNDING_INTERNAL) - assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_INTERNAL].channelFeatures.channelType == ChannelTypes.AnchorOutputsZeroFeeHtlcTx) + assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_INTERNAL].channelFeatures.channelType == ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)) + aliceOrigin.expectNoMessage() + } + + test("recv AcceptChannel (anchor outputs zero fee htlc txs and scid alias)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs), Tag(ChannelStateTestsTags.ScidAlias)) { f => + import f._ + val accept = bob2alice.expectMsgType[AcceptChannel] + assert(accept.channelType_opt == Some(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = true, zeroConf = false))) + bob2alice.forward(alice) + awaitCond(alice.stateName == WAIT_FOR_FUNDING_INTERNAL) + assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_INTERNAL].channelFeatures.channelType == ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = true, zeroConf = false)) aliceOrigin.expectNoMessage() } @@ -118,7 +129,7 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS test("recv AcceptChannel (channel type not set but feature bit set)", Tag(ChannelStateTestsTags.ChannelType), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => import f._ val accept = bob2alice.expectMsgType[AcceptChannel] - assert(accept.channelType_opt == Some(ChannelTypes.AnchorOutputsZeroFeeHtlcTx)) + assert(accept.channelType_opt == Some(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false))) bob2alice.forward(alice, accept.copy(tlvStream = TlvStream.empty)) alice2bob.expectMsg(Error(accept.temporaryChannelId, "option_channel_type was negotiated but channel_type is missing")) awaitCond(alice.stateName == CLOSED) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForOpenChannelStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForOpenChannelStateSpec.scala index b76db60e96..13ac83a34c 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForOpenChannelStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForOpenChannelStateSpec.scala @@ -24,7 +24,7 @@ import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel.fsm.Channel import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsTags} import fr.acinq.eclair.wire.protocol.{AcceptChannel, ChannelTlv, Error, Init, OpenChannel, TlvStream} -import fr.acinq.eclair.{CltvExpiryDelta, MilliSatoshiLong, TestConstants, TestKitBaseClass, ToMilliSatoshiConversion} +import fr.acinq.eclair.{CltvExpiryDelta, Features, MilliSatoshiLong, TestConstants, TestKitBaseClass, ToMilliSatoshiConversion} import org.scalatest.funsuite.FixtureAnyFunSuiteLike import org.scalatest.{Outcome, Tag} import scodec.bits.ByteVector @@ -50,13 +50,14 @@ class WaitForOpenChannelStateSpec extends TestKitBaseClass with FixtureAnyFunSui import setup._ val channelConfig = ChannelConfig.standard - val (aliceParams, bobParams, defaultChannelType) = computeFeatures(setup, test.tags) + val channelFlags = ChannelFlags.Private + val (aliceParams, bobParams, defaultChannelType) = computeFeatures(setup, test.tags, channelFlags) val channelType = if (test.tags.contains("standard-channel-type")) ChannelTypes.Standard else defaultChannelType - val commitTxFeerate = if (channelType == ChannelTypes.AnchorOutputs || channelType == ChannelTypes.AnchorOutputsZeroFeeHtlcTx) TestConstants.anchorOutputsFeeratePerKw else TestConstants.feeratePerKw + val commitTxFeerate = if (channelType == ChannelTypes.AnchorOutputs || channelType.isInstanceOf[ChannelTypes.AnchorOutputsZeroFeeHtlcTx]) TestConstants.anchorOutputsFeeratePerKw else TestConstants.feeratePerKw val aliceInit = Init(aliceParams.initFeatures) val bobInit = Init(bobParams.initFeatures) within(30 seconds) { - alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, commitTxFeerate, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, ChannelFlags.Private, channelConfig, channelType) + alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, commitTxFeerate, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType) bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType) awaitCond(bob.stateName == WAIT_FOR_OPEN_CHANNEL) withFixture(test.toNoArgTest(FixtureParam(alice, bob, alice2bob, bob2alice, bob2blockchain))) @@ -87,10 +88,19 @@ class WaitForOpenChannelStateSpec extends TestKitBaseClass with FixtureAnyFunSui test("recv OpenChannel (anchor outputs zero fee htlc txs)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => import f._ val open = alice2bob.expectMsgType[OpenChannel] - assert(open.channelType_opt == Some(ChannelTypes.AnchorOutputsZeroFeeHtlcTx)) + assert(open.channelType_opt == Some(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false))) alice2bob.forward(bob) awaitCond(bob.stateName == WAIT_FOR_FUNDING_CREATED) - assert(bob.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CREATED].channelFeatures.channelType == ChannelTypes.AnchorOutputsZeroFeeHtlcTx) + assert(bob.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CREATED].channelFeatures.channelType == ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)) + } + + test("recv OpenChannel (anchor outputs zero fee htlc txs and scid alias)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs), Tag(ChannelStateTestsTags.ScidAlias)) { f => + import f._ + val open = alice2bob.expectMsgType[OpenChannel] + assert(open.channelType_opt == Some(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = true, zeroConf = false))) + alice2bob.forward(bob) + awaitCond(bob.stateName == WAIT_FOR_FUNDING_CREATED) + assert(bob.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CREATED].channelFeatures.channelType == ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = true, zeroConf = false)) } test("recv OpenChannel (non-default channel type)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs), Tag("standard-channel-type")) { f => @@ -287,6 +297,14 @@ class WaitForOpenChannelStateSpec extends TestKitBaseClass with FixtureAnyFunSui awaitCond(bob.stateName == CLOSED) } + test("recv OpenChannel (zeroconf)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs), Tag(ChannelStateTestsTags.ZeroConf)) { f => + import f._ + val open = alice2bob.expectMsgType[OpenChannel] + alice2bob.forward(bob, open) + val accept = bob2alice.expectMsgType[AcceptChannel] + assert(accept.minimumDepth == 0) + } + test("recv Error") { f => import f._ bob ! Error(ByteVector32.Zeroes, "oops") diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingCreatedStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingCreatedStateSpec.scala index 84bb3b43e1..ff298ee255 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingCreatedStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingCreatedStateSpec.scala @@ -60,11 +60,12 @@ class WaitForFundingCreatedStateSpec extends TestKitBaseClass with FixtureAnyFun import setup._ val channelConfig = ChannelConfig.standard - val (aliceParams, bobParams, channelType) = computeFeatures(setup, test.tags) + val channelFlags = ChannelFlags.Private + val (aliceParams, bobParams, channelType) = computeFeatures(setup, test.tags, channelFlags) val aliceInit = Init(aliceParams.initFeatures) val bobInit = Init(bobParams.initFeatures) within(30 seconds) { - alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, fundingSatoshis, pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, ChannelFlags.Private, channelConfig, channelType) + alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, fundingSatoshis, pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType) alice2blockchain.expectMsgType[TxPublisher.SetChannelId] bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType) bob2blockchain.expectMsgType[TxPublisher.SetChannelId] diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingInternalStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingInternalStateSpec.scala index 7fa5845ec6..23419edfda 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingInternalStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingInternalStateSpec.scala @@ -43,11 +43,12 @@ class WaitForFundingInternalStateSpec extends TestKitBaseClass with FixtureAnyFu val setup = init(wallet = new NoOpOnChainWallet()) import setup._ val channelConfig = ChannelConfig.standard - val (aliceParams, bobParams, channelType) = computeFeatures(setup, test.tags) + val channelFlags = ChannelFlags.Private + val (aliceParams, bobParams, channelType) = computeFeatures(setup, test.tags, channelFlags) val aliceInit = Init(aliceParams.initFeatures) val bobInit = Init(bobParams.initFeatures) within(30 seconds) { - alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, ChannelFlags.Private, channelConfig, channelType) + alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType) bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType) alice2bob.expectMsgType[OpenChannel] alice2bob.forward(bob) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingSignedStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingSignedStateSpec.scala index 9596b3c33b..ddccfb0499 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingSignedStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingSignedStateSpec.scala @@ -59,11 +59,12 @@ class WaitForFundingSignedStateSpec extends TestKitBaseClass with FixtureAnyFunS import setup._ val channelConfig = ChannelConfig.standard - val (aliceParams, bobParams, channelType) = computeFeatures(setup, test.tags) + val channelFlags = ChannelFlags.Private + val (aliceParams, bobParams, channelType) = computeFeatures(setup, test.tags, channelFlags) val aliceInit = Init(aliceParams.initFeatures) val bobInit = Init(bobParams.initFeatures) within(30 seconds) { - alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, fundingSatoshis, pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, ChannelFlags.Private, channelConfig, channelType) + alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, fundingSatoshis, pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType) alice2blockchain.expectMsgType[TxPublisher.SetChannelId] bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType) bob2blockchain.expectMsgType[TxPublisher.SetChannelId] @@ -91,7 +92,19 @@ class WaitForFundingSignedStateSpec extends TestKitBaseClass with FixtureAnyFunS assert(txPublished.tx.txid == fundingTxId) assert(txPublished.miningFee > 0.sat) val watchConfirmed = alice2blockchain.expectMsgType[WatchFundingConfirmed] - assert(watchConfirmed.minDepth == Alice.nodeParams.channelConf.minDepthBlocks) + assert(watchConfirmed.minDepth == 1) // when funder we trust ourselves so we never wait more than 1 block + aliceOrigin.expectMsgType[ChannelOpenResponse.ChannelOpened] + } + + test("recv FundingSigned with valid signature (zero-conf)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs), Tag(ChannelStateTestsTags.ZeroConf)) { f => + import f._ + bob2alice.expectMsgType[FundingSigned] + bob2alice.forward(alice) + awaitCond(alice.stateName == WAIT_FOR_CHANNEL_READY) + // alice doesn't watch for the funding tx to confirm + alice2blockchain.expectMsgType[WatchFundingSpent] + alice2blockchain.expectMsgType[WatchFundingLost] + alice2blockchain.expectNoMessage(100 millis) aliceOrigin.expectMsgType[ChannelOpenResponse.ChannelOpened] } @@ -102,8 +115,7 @@ class WaitForFundingSignedStateSpec extends TestKitBaseClass with FixtureAnyFunS awaitCond(alice.stateName == WAIT_FOR_FUNDING_CONFIRMED) alice2blockchain.expectMsgType[WatchFundingSpent] val watchConfirmed = alice2blockchain.expectMsgType[WatchFundingConfirmed] - // when we are funder, we keep our regular min depth even for wumbo channels - assert(watchConfirmed.minDepth == Alice.nodeParams.channelConf.minDepthBlocks) + assert(watchConfirmed.minDepth == 1) // when funder we trust ourselves so we never wait more than 1 block aliceOrigin.expectMsgType[ChannelOpenResponse.ChannelOpened] } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForChannelReadyStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForChannelReadyStateSpec.scala new file mode 100644 index 0000000000..167c75563d --- /dev/null +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForChannelReadyStateSpec.scala @@ -0,0 +1,255 @@ +/* + * Copyright 2019 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.channel.states.c + +import akka.testkit.{TestFSMRef, TestProbe} +import com.softwaremill.quicklens.ModifyPimp +import fr.acinq.bitcoin.scalacompat.{ByteVector32, Transaction} +import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._ +import fr.acinq.eclair.channel._ +import fr.acinq.eclair.channel.fsm.Channel +import fr.acinq.eclair.channel.publish.TxPublisher +import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsTags} +import fr.acinq.eclair.payment.relay.Relayer.RelayFees +import fr.acinq.eclair.wire.protocol._ +import fr.acinq.eclair.{BlockHeight, MilliSatoshiLong, TestConstants, TestKitBaseClass} +import org.scalatest.funsuite.FixtureAnyFunSuiteLike +import org.scalatest.{Outcome, Tag} + +import scala.concurrent.duration._ + +/** + * Created by PM on 05/07/2016. + */ + +class WaitForChannelReadyStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with ChannelStateTestsBase { + + val relayFees: RelayFees = RelayFees(999 msat, 1234) + + case class FixtureParam(alice: TestFSMRef[ChannelState, ChannelData, Channel], bob: TestFSMRef[ChannelState, ChannelData, Channel], alice2bob: TestProbe, bob2alice: TestProbe, alice2blockchain: TestProbe, bob2blockchain: TestProbe, router: TestProbe) + + override def withFixture(test: OneArgTest): Outcome = { + val setup = init() + import setup._ + val channelConfig = ChannelConfig.standard + val channelFlags = ChannelFlags(announceChannel = test.tags.contains(ChannelStateTestsTags.ChannelsPublic)) + val (aliceParams, bobParams, channelType) = computeFeatures(setup, test.tags, channelFlags) + val pushMsat = if (test.tags.contains(ChannelStateTestsTags.NoPushMsat)) 0.msat else TestConstants.pushMsat + val aliceInit = Init(aliceParams.initFeatures) + val bobInit = Init(bobParams.initFeatures) + + within(30 seconds) { + alice.underlyingActor.nodeParams.db.peers.addOrUpdateRelayFees(bobParams.nodeId, relayFees) + alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType) + alice2blockchain.expectMsgType[TxPublisher.SetChannelId] + bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType) + bob2blockchain.expectMsgType[TxPublisher.SetChannelId] + alice2bob.expectMsgType[OpenChannel] + alice2bob.forward(bob) + bob2alice.expectMsgType[AcceptChannel] + bob2alice.forward(alice) + alice2bob.expectMsgType[FundingCreated] + alice2bob.forward(bob) + bob2alice.expectMsgType[FundingSigned] + bob2alice.forward(alice) + alice2blockchain.expectMsgType[TxPublisher.SetChannelId] + alice2blockchain.expectMsgType[WatchFundingSpent] + bob2blockchain.expectMsgType[TxPublisher.SetChannelId] + bob2blockchain.expectMsgType[WatchFundingSpent] + if (!test.tags.contains(ChannelStateTestsTags.ZeroConf)) { + alice2blockchain.expectMsgType[WatchFundingConfirmed] + bob2blockchain.expectMsgType[WatchFundingConfirmed] + awaitCond(alice.stateName == WAIT_FOR_FUNDING_CONFIRMED) + awaitCond(bob.stateName == WAIT_FOR_FUNDING_CONFIRMED) + val fundingTx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CONFIRMED].fundingTx.get + alice ! WatchFundingConfirmedTriggered(BlockHeight(400000), 42, fundingTx) + bob ! WatchFundingConfirmedTriggered(BlockHeight(400000), 42, fundingTx) + } + alice2blockchain.expectMsgType[WatchFundingLost] + bob2blockchain.expectMsgType[WatchFundingLost] + alice2bob.expectMsgType[ChannelReady] + awaitCond(alice.stateName == WAIT_FOR_CHANNEL_READY) + awaitCond(bob.stateName == WAIT_FOR_CHANNEL_READY) + withFixture(test.toNoArgTest(FixtureParam(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain, router))) + } + } + + test("recv ChannelReady") { f => + import f._ + // we have a real scid at this stage, because this isn't a zero-conf channel + assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_CHANNEL_READY].shortIds.real.isInstanceOf[RealScidStatus.Temporary]) + assert(bob.stateData.asInstanceOf[DATA_WAIT_FOR_CHANNEL_READY].shortIds.real.isInstanceOf[RealScidStatus.Temporary]) + val channelReady = bob2alice.expectMsgType[ChannelReady] + bob2alice.forward(alice) + val initialChannelUpdate = alice2bob.expectMsgType[ChannelUpdate] + assert(initialChannelUpdate == alice.stateData.asInstanceOf[DATA_NORMAL].channelUpdate) + // we have a real scid, but the channel is not announced so alice uses bob's alias + assert(initialChannelUpdate.shortChannelId == channelReady.alias_opt.get) + assert(initialChannelUpdate.feeBaseMsat == relayFees.feeBase) + assert(initialChannelUpdate.feeProportionalMillionths == relayFees.feeProportionalMillionths) + alice2blockchain.expectMsgType[WatchFundingDeeplyBuried] + bob2alice.expectNoMessage(100 millis) + awaitCond(alice.stateName == NORMAL) + } + + test("recv ChannelReady (no alias)") { f => + import f._ + // we have a real scid at this stage, because this isn't a zero-conf channel + val realScid = alice.stateData.asInstanceOf[DATA_WAIT_FOR_CHANNEL_READY].shortIds.real.asInstanceOf[RealScidStatus.Temporary].realScid + val channelReady = bob2alice.expectMsgType[ChannelReady] + val channelReadyNoAlias = channelReady.modify(_.tlvStream.records).using(_.filterNot(_.isInstanceOf[ChannelReadyTlv.ShortChannelIdTlv])) + bob2alice.forward(alice, channelReadyNoAlias) + val initialChannelUpdate = alice2bob.expectMsgType[ChannelUpdate] + assert(initialChannelUpdate == alice.stateData.asInstanceOf[DATA_NORMAL].channelUpdate) + // the channel is not announced but bob didn't send an alias so we use the real scid + assert(initialChannelUpdate.shortChannelId == realScid) + assert(initialChannelUpdate.feeBaseMsat == relayFees.feeBase) + assert(initialChannelUpdate.feeProportionalMillionths == relayFees.feeProportionalMillionths) + alice2blockchain.expectMsgType[WatchFundingDeeplyBuried] + bob2alice.expectNoMessage(100 millis) + awaitCond(alice.stateName == NORMAL) + } + + test("recv ChannelReady (zero-conf)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs), Tag(ChannelStateTestsTags.ZeroConf)) { f => + import f._ + // zero-conf channel: we don't have a real scid + assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_CHANNEL_READY].shortIds.real == RealScidStatus.Unknown) + assert(bob.stateData.asInstanceOf[DATA_WAIT_FOR_CHANNEL_READY].shortIds.real == RealScidStatus.Unknown) + val channelReady = bob2alice.expectMsgType[ChannelReady] + bob2alice.forward(alice) + val initialChannelUpdate = alice2bob.expectMsgType[ChannelUpdate] + assert(initialChannelUpdate == alice.stateData.asInstanceOf[DATA_NORMAL].channelUpdate) + // the channel is not announced so alice uses bob's alias (we have a no real scid anyway) + assert(initialChannelUpdate.shortChannelId == channelReady.alias_opt.get) + assert(initialChannelUpdate.feeBaseMsat == relayFees.feeBase) + assert(initialChannelUpdate.feeProportionalMillionths == relayFees.feeProportionalMillionths) + alice2blockchain.expectMsgType[WatchFundingDeeplyBuried] + bob2alice.expectNoMessage(100 millis) + awaitCond(alice.stateName == NORMAL) + } + + test("recv ChannelReady (zero-conf, no alias)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs), Tag(ChannelStateTestsTags.ZeroConf)) { f => + import f._ + // zero-conf channel: we don't have a real scid + assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_CHANNEL_READY].shortIds.real == RealScidStatus.Unknown) + assert(bob.stateData.asInstanceOf[DATA_WAIT_FOR_CHANNEL_READY].shortIds.real == RealScidStatus.Unknown) + val channelReady = bob2alice.expectMsgType[ChannelReady] + val channelReadyNoAlias = channelReady.modify(_.tlvStream.records).using(_.filterNot(_.isInstanceOf[ChannelReadyTlv.ShortChannelIdTlv])) + bob2alice.forward(alice, channelReadyNoAlias) + awaitCond(alice.stateName == CLOSING) + assert(alice2blockchain.expectMsgType[TxPublisher.PublishFinalTx].desc == "commit-tx") + assert(alice2blockchain.expectMsgType[TxPublisher.PublishTx].desc == "local-anchor") + assert(alice2blockchain.expectMsgType[TxPublisher.PublishFinalTx].desc == "local-main-delayed") + alice2blockchain.expectMsgType[WatchTxConfirmed] + } + + test("recv ChannelReady (public)", Tag(ChannelStateTestsTags.ChannelsPublic)) { f => + import f._ + // we have a real scid at this stage, because this isn't a zero-conf channel + assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_CHANNEL_READY].shortIds.real.isInstanceOf[RealScidStatus.Temporary]) + assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_CHANNEL_READY].commitments.channelFlags.announceChannel) + assert(bob.stateData.asInstanceOf[DATA_WAIT_FOR_CHANNEL_READY].shortIds.real.isInstanceOf[RealScidStatus.Temporary]) + assert(bob.stateData.asInstanceOf[DATA_WAIT_FOR_CHANNEL_READY].commitments.channelFlags.announceChannel) + val channelReady = bob2alice.expectMsgType[ChannelReady] + bob2alice.forward(alice) + val initialChannelUpdate = alice2bob.expectMsgType[ChannelUpdate] + assert(initialChannelUpdate == alice.stateData.asInstanceOf[DATA_NORMAL].channelUpdate) + // we have a real scid, but it is not the final one (less than 6 confirmations) so alice uses bob's alias + assert(initialChannelUpdate.shortChannelId == channelReady.alias_opt.get) + assert(initialChannelUpdate.feeBaseMsat == relayFees.feeBase) + assert(initialChannelUpdate.feeProportionalMillionths == relayFees.feeProportionalMillionths) + alice2blockchain.expectMsgType[WatchFundingDeeplyBuried] + bob2alice.expectNoMessage(100 millis) + awaitCond(alice.stateName == NORMAL) + } + + test("recv ChannelReady (public, zero-conf)", Tag(ChannelStateTestsTags.ChannelsPublic), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs), Tag(ChannelStateTestsTags.ZeroConf)) { f => + import f._ + // zero-conf channel: we don't have a real scid + assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_CHANNEL_READY].shortIds.real == RealScidStatus.Unknown) + assert(bob.stateData.asInstanceOf[DATA_WAIT_FOR_CHANNEL_READY].shortIds.real == RealScidStatus.Unknown) + val channelReady = bob2alice.expectMsgType[ChannelReady] + bob2alice.forward(alice) + val initialChannelUpdate = alice2bob.expectMsgType[ChannelUpdate] + assert(initialChannelUpdate == alice.stateData.asInstanceOf[DATA_NORMAL].channelUpdate) + // the channel is not announced, so alice uses bob's alias (we have a no real scid anyway) + assert(initialChannelUpdate.shortChannelId == channelReady.alias_opt.get) + assert(initialChannelUpdate.feeBaseMsat == relayFees.feeBase) + assert(initialChannelUpdate.feeProportionalMillionths == relayFees.feeProportionalMillionths) + alice2blockchain.expectMsgType[WatchFundingDeeplyBuried] + bob2alice.expectNoMessage(100 millis) + awaitCond(alice.stateName == NORMAL) + } + + test("recv WatchFundingSpentTriggered (remote commit)") { f => + import f._ + // bob publishes his commitment tx + val tx = bob.stateData.asInstanceOf[DATA_WAIT_FOR_CHANNEL_READY].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx + alice ! WatchFundingSpentTriggered(tx) + alice2blockchain.expectMsgType[TxPublisher.PublishTx] + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == tx.txid) + awaitCond(alice.stateName == CLOSING) + } + + test("recv WatchFundingSpentTriggered (other commit)") { f => + import f._ + val tx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_CHANNEL_READY].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx + alice ! WatchFundingSpentTriggered(Transaction(0, Nil, Nil, 0)) + alice2bob.expectMsgType[Error] + assert(alice2blockchain.expectMsgType[TxPublisher.PublishFinalTx].tx.txid == tx.txid) + alice2blockchain.expectMsgType[TxPublisher.PublishTx] + awaitCond(alice.stateName == ERR_INFORMATION_LEAK) + } + + test("recv Error") { f => + import f._ + val tx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_CHANNEL_READY].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx + alice ! Error(ByteVector32.Zeroes, "oops") + awaitCond(alice.stateName == CLOSING) + assert(alice2blockchain.expectMsgType[TxPublisher.PublishFinalTx].tx.txid == tx.txid) + alice2blockchain.expectMsgType[TxPublisher.PublishTx] + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == tx.txid) + } + + test("recv Error (nothing at stake)", Tag(ChannelStateTestsTags.NoPushMsat)) { f => + import f._ + val tx = bob.stateData.asInstanceOf[DATA_WAIT_FOR_CHANNEL_READY].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx + bob ! Error(ByteVector32.Zeroes, "funding double-spent") + awaitCond(bob.stateName == CLOSING) + assert(bob2blockchain.expectMsgType[TxPublisher.PublishFinalTx].tx.txid == tx.txid) + assert(bob2blockchain.expectMsgType[WatchTxConfirmed].txId == tx.txid) + } + + test("recv CMD_CLOSE") { f => + import f._ + val sender = TestProbe() + val c = CMD_CLOSE(sender.ref, None, None) + alice ! c + sender.expectMsg(RES_FAILURE(c, CommandUnavailableInThisState(channelId(alice), "close", WAIT_FOR_CHANNEL_READY))) + } + + test("recv CMD_FORCECLOSE") { f => + import f._ + val sender = TestProbe() + val tx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_CHANNEL_READY].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx + alice ! CMD_FORCECLOSE(sender.ref) + awaitCond(alice.stateName == CLOSING) + assert(alice2blockchain.expectMsgType[TxPublisher.PublishFinalTx].tx.txid == tx.txid) + alice2blockchain.expectMsgType[TxPublisher.PublishTx] + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == tx.txid) + } +} diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingConfirmedStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingConfirmedStateSpec.scala index 149f4d9255..340f7ca24f 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingConfirmedStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingConfirmedStateSpec.scala @@ -26,10 +26,10 @@ import fr.acinq.eclair.channel.fsm.Channel.{BITCOIN_FUNDING_PUBLISH_FAILED, BITC import fr.acinq.eclair.channel.publish.TxPublisher import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsTags} import fr.acinq.eclair.transactions.Scripts.multiSig2of2 -import fr.acinq.eclair.wire.protocol.{AcceptChannel, Error, FundingCreated, FundingLocked, FundingSigned, Init, OpenChannel} +import fr.acinq.eclair.wire.protocol.{AcceptChannel, ChannelReady, Error, FundingCreated, FundingSigned, Init, OpenChannel, TlvStream} import fr.acinq.eclair.{BlockHeight, MilliSatoshiLong, TestConstants, TestKitBaseClass, TimestampSecond, randomKey} -import org.scalatest.{Outcome, Tag} import org.scalatest.funsuite.FixtureAnyFunSuiteLike +import org.scalatest.{Outcome, Tag} import scala.concurrent.duration._ @@ -47,15 +47,16 @@ class WaitForFundingConfirmedStateSpec extends TestKitBaseClass with FixtureAnyF import setup._ val channelConfig = ChannelConfig.standard + val channelFlags = ChannelFlags.Private + val (aliceParams, bobParams, channelType) = computeFeatures(setup, test.tags, channelFlags) val pushMsat = if (test.tags.contains(ChannelStateTestsTags.NoPushMsat)) 0.msat else TestConstants.pushMsat - val (aliceParams, bobParams, channelType) = computeFeatures(setup, test.tags) val aliceInit = Init(aliceParams.initFeatures) val bobInit = Init(bobParams.initFeatures) within(30 seconds) { val listener = TestProbe() alice.underlying.system.eventStream.subscribe(listener.ref, classOf[TransactionPublished]) - alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, ChannelFlags.Private, channelConfig, channelType) + alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType) alice2blockchain.expectMsgType[TxPublisher.SetChannelId] bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType) bob2blockchain.expectMsgType[TxPublisher.SetChannelId] @@ -79,26 +80,55 @@ class WaitForFundingConfirmedStateSpec extends TestKitBaseClass with FixtureAnyF } } - test("recv FundingLocked") { f => + test("recv ChannelReady (funder, with remote alias)") { f => import f._ - // we create a new listener that registers after alice has published the funding tx - val listener = TestProbe() - bob.underlying.system.eventStream.subscribe(listener.ref, classOf[TransactionPublished]) - bob.underlying.system.eventStream.subscribe(listener.ref, classOf[TransactionConfirmed]) - // make bob send a FundingLocked msg + // make bob send a ChannelReady msg val fundingTx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CONFIRMED].fundingTx.get bob ! WatchFundingConfirmedTriggered(BlockHeight(42000), 42, fundingTx) - val txPublished = listener.expectMsgType[TransactionPublished] - assert(txPublished.tx == fundingTx) - assert(txPublished.miningFee == 0.sat) // bob is fundee - assert(listener.expectMsgType[TransactionConfirmed].tx == fundingTx) - val msg = bob2alice.expectMsgType[FundingLocked] + val bobChannelReady = bob2alice.expectMsgType[ChannelReady] + assert(bobChannelReady.alias_opt.isDefined) + // test starts here bob2alice.forward(alice) - awaitCond(alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CONFIRMED].deferred.contains(msg)) - awaitCond(alice.stateName == WAIT_FOR_FUNDING_CONFIRMED) + // alice stops waiting for confirmations since bob is accepting the channel + alice2blockchain.expectMsgType[WatchFundingLost] + alice2blockchain.expectMsgType[WatchFundingDeeplyBuried] + val aliceChannelReady = alice2bob.expectMsgType[ChannelReady] + assert(aliceChannelReady.alias_opt.nonEmpty) + awaitAssert(assert(alice.stateName == NORMAL)) + } + + test("recv ChannelReady (funder, no remote alias)") { f => + import f._ + // make bob send a ChannelReady msg + val fundingTx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CONFIRMED].fundingTx.get + bob ! WatchFundingConfirmedTriggered(BlockHeight(42000), 42, fundingTx) + val channelReadyNoAlias = bob2alice.expectMsgType[ChannelReady].copy(tlvStream = TlvStream.empty) + // test starts here + bob2alice.forward(alice, channelReadyNoAlias) + // alice keeps bob's channel_ready for later processing + eventually { + assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CONFIRMED].deferred.contains(channelReadyNoAlias)) + } + alice2blockchain.expectNoMessage() + } + + test("recv ChannelReady (fundee)") { f => + import f._ + // make alice send a ChannelReady msg + val fundingTx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CONFIRMED].fundingTx.get + alice ! WatchFundingConfirmedTriggered(BlockHeight(42000), 42, fundingTx) + val channelReady = alice2bob.expectMsgType[ChannelReady] + // test starts here + alice2bob.forward(bob) + // alice keeps bob's channel_ready for later processing + eventually { + assert(bob.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CONFIRMED].deferred.contains(channelReady)) + } + // bob is fundee, he doesn't trust alice and won't create a zero-conf watch + bob2blockchain.expectNoMessage() } - test("recv WatchFundingConfirmedTriggered") { f => + test("recv WatchFundingConfirmedTriggered (funder)") { f => import f._ // we create a new listener that registers after alice has published the funding tx val listener = TestProbe() @@ -107,9 +137,31 @@ class WaitForFundingConfirmedStateSpec extends TestKitBaseClass with FixtureAnyF val fundingTx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CONFIRMED].fundingTx.get alice ! WatchFundingConfirmedTriggered(BlockHeight(42000), 42, fundingTx) assert(listener.expectMsgType[TransactionConfirmed].tx == fundingTx) - awaitCond(alice.stateName == WAIT_FOR_FUNDING_LOCKED) + awaitCond(alice.stateName == WAIT_FOR_CHANNEL_READY) alice2blockchain.expectMsgType[WatchFundingLost] - alice2bob.expectMsgType[FundingLocked] + val channelReady = alice2bob.expectMsgType[ChannelReady] + // we always send an alias + assert(channelReady.alias_opt.isDefined) + } + + test("recv WatchFundingConfirmedTriggered (fundee)") { f => + import f._ + // we create a new listener that registers after alice has published the funding tx + val listener = TestProbe() + bob.underlying.system.eventStream.subscribe(listener.ref, classOf[TransactionPublished]) + bob.underlying.system.eventStream.subscribe(listener.ref, classOf[TransactionConfirmed]) + // make bob send a ChannelReady msg + val fundingTx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CONFIRMED].fundingTx.get + bob ! WatchFundingConfirmedTriggered(BlockHeight(42000), 42, fundingTx) + val txPublished = listener.expectMsgType[TransactionPublished] + assert(txPublished.tx == fundingTx) + assert(txPublished.miningFee == 0.sat) // bob is fundee + assert(listener.expectMsgType[TransactionConfirmed].tx == fundingTx) + awaitCond(bob.stateName == WAIT_FOR_CHANNEL_READY) + bob2blockchain.expectMsgType[WatchFundingLost] + val channelReady = bob2alice.expectMsgType[ChannelReady] + // we always send an alias + assert(channelReady.alias_opt.isDefined) } test("recv WatchFundingConfirmedTriggered (bad funding pubkey script)") { f => diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingLockedStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingLockedStateSpec.scala deleted file mode 100644 index 01345af1e2..0000000000 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingLockedStateSpec.scala +++ /dev/null @@ -1,154 +0,0 @@ -/* - * Copyright 2019 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.channel.states.c - -import akka.testkit.{TestFSMRef, TestProbe} -import fr.acinq.bitcoin.scalacompat.{ByteVector32, Transaction} -import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._ -import fr.acinq.eclair.channel._ -import fr.acinq.eclair.channel.fsm.Channel -import fr.acinq.eclair.channel.publish.TxPublisher -import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsTags} -import fr.acinq.eclair.payment.relay.Relayer.RelayFees -import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{BlockHeight, MilliSatoshiLong, TestConstants, TestKitBaseClass} -import org.scalatest.funsuite.FixtureAnyFunSuiteLike -import org.scalatest.{Outcome, Tag} - -import scala.concurrent.duration._ - -/** - * Created by PM on 05/07/2016. - */ - -class WaitForFundingLockedStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with ChannelStateTestsBase { - - val relayFees: RelayFees = RelayFees(999 msat, 1234) - - case class FixtureParam(alice: TestFSMRef[ChannelState, ChannelData, Channel], bob: TestFSMRef[ChannelState, ChannelData, Channel], alice2bob: TestProbe, bob2alice: TestProbe, alice2blockchain: TestProbe, bob2blockchain: TestProbe, router: TestProbe) - - override def withFixture(test: OneArgTest): Outcome = { - val setup = init() - import setup._ - val channelConfig = ChannelConfig.standard - val pushMsat = if (test.tags.contains(ChannelStateTestsTags.NoPushMsat)) 0.msat else TestConstants.pushMsat - val (aliceParams, bobParams, channelType) = computeFeatures(setup, test.tags) - val aliceInit = Init(aliceParams.initFeatures) - val bobInit = Init(bobParams.initFeatures) - - within(30 seconds) { - alice.underlyingActor.nodeParams.db.peers.addOrUpdateRelayFees(bobParams.nodeId, relayFees) - alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, ChannelFlags.Private, channelConfig, channelType) - alice2blockchain.expectMsgType[TxPublisher.SetChannelId] - bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType) - bob2blockchain.expectMsgType[TxPublisher.SetChannelId] - alice2bob.expectMsgType[OpenChannel] - alice2bob.forward(bob) - bob2alice.expectMsgType[AcceptChannel] - bob2alice.forward(alice) - alice2bob.expectMsgType[FundingCreated] - alice2bob.forward(bob) - bob2alice.expectMsgType[FundingSigned] - bob2alice.forward(alice) - alice2blockchain.expectMsgType[TxPublisher.SetChannelId] - alice2blockchain.expectMsgType[WatchFundingSpent] - alice2blockchain.expectMsgType[WatchFundingConfirmed] - bob2blockchain.expectMsgType[TxPublisher.SetChannelId] - bob2blockchain.expectMsgType[WatchFundingSpent] - bob2blockchain.expectMsgType[WatchFundingConfirmed] - awaitCond(alice.stateName == WAIT_FOR_FUNDING_CONFIRMED) - val fundingTx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CONFIRMED].fundingTx.get - alice ! WatchFundingConfirmedTriggered(BlockHeight(400000), 42, fundingTx) - bob ! WatchFundingConfirmedTriggered(BlockHeight(400000), 42, fundingTx) - alice2blockchain.expectMsgType[WatchFundingLost] - bob2blockchain.expectMsgType[WatchFundingLost] - alice2bob.expectMsgType[FundingLocked] - awaitCond(alice.stateName == WAIT_FOR_FUNDING_LOCKED) - awaitCond(bob.stateName == WAIT_FOR_FUNDING_LOCKED) - withFixture(test.toNoArgTest(FixtureParam(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain, router))) - } - } - - test("recv FundingLocked") { f => - import f._ - bob2alice.expectMsgType[FundingLocked] - bob2alice.forward(alice) - awaitCond(alice.stateName == NORMAL) - val initialChannelUpdate = alice.stateData.asInstanceOf[DATA_NORMAL].channelUpdate - assert(initialChannelUpdate.feeBaseMsat == relayFees.feeBase) - assert(initialChannelUpdate.feeProportionalMillionths == relayFees.feeProportionalMillionths) - bob2alice.expectNoMessage(200 millis) - } - - test("recv WatchFundingSpentTriggered (remote commit)") { f => - import f._ - // bob publishes his commitment tx - val tx = bob.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_LOCKED].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx - alice ! WatchFundingSpentTriggered(tx) - alice2blockchain.expectMsgType[TxPublisher.PublishTx] - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == tx.txid) - awaitCond(alice.stateName == CLOSING) - } - - test("recv WatchFundingSpentTriggered (other commit)") { f => - import f._ - val tx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_LOCKED].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx - alice ! WatchFundingSpentTriggered(Transaction(0, Nil, Nil, 0)) - alice2bob.expectMsgType[Error] - assert(alice2blockchain.expectMsgType[TxPublisher.PublishFinalTx].tx.txid == tx.txid) - alice2blockchain.expectMsgType[TxPublisher.PublishTx] - awaitCond(alice.stateName == ERR_INFORMATION_LEAK) - } - - test("recv Error") { f => - import f._ - val tx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_LOCKED].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx - alice ! Error(ByteVector32.Zeroes, "oops") - awaitCond(alice.stateName == CLOSING) - assert(alice2blockchain.expectMsgType[TxPublisher.PublishFinalTx].tx.txid == tx.txid) - alice2blockchain.expectMsgType[TxPublisher.PublishTx] - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == tx.txid) - } - - test("recv Error (nothing at stake)", Tag(ChannelStateTestsTags.NoPushMsat)) { f => - import f._ - val tx = bob.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_LOCKED].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx - bob ! Error(ByteVector32.Zeroes, "funding double-spent") - awaitCond(bob.stateName == CLOSING) - assert(bob2blockchain.expectMsgType[TxPublisher.PublishFinalTx].tx.txid == tx.txid) - assert(bob2blockchain.expectMsgType[WatchTxConfirmed].txId == tx.txid) - } - - test("recv CMD_CLOSE") { f => - import f._ - val sender = TestProbe() - val c = CMD_CLOSE(sender.ref, None, None) - alice ! c - sender.expectMsg(RES_FAILURE(c, CommandUnavailableInThisState(channelId(alice), "close", WAIT_FOR_FUNDING_LOCKED))) - } - - test("recv CMD_FORCECLOSE") { f => - import f._ - val sender = TestProbe() - val tx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_LOCKED].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx - alice ! CMD_FORCECLOSE(sender.ref) - awaitCond(alice.stateName == CLOSING) - assert(alice2blockchain.expectMsgType[TxPublisher.PublishFinalTx].tx.txid == tx.txid) - alice2blockchain.expectMsgType[TxPublisher.PublishTx] - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == tx.txid) - } -} diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala index 2930c973ba..8ff235ff85 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala @@ -18,9 +18,9 @@ package fr.acinq.eclair.channel.states.e import akka.actor.ActorRef import akka.testkit.TestProbe +import fr.acinq.bitcoin.ScriptFlags import fr.acinq.bitcoin.scalacompat.Crypto.PrivateKey import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, Crypto, OutPoint, SatoshiLong, Script, Transaction} -import fr.acinq.bitcoin.ScriptFlags import fr.acinq.eclair.Features.StaticRemoteKey import fr.acinq.eclair.TestConstants.{Alice, Bob} import fr.acinq.eclair.UInt64.Conversions._ @@ -41,7 +41,7 @@ import fr.acinq.eclair.router.Announcements import fr.acinq.eclair.transactions.DirectedHtlc.{incoming, outgoing} import fr.acinq.eclair.transactions.Transactions import fr.acinq.eclair.transactions.Transactions._ -import fr.acinq.eclair.wire.protocol.{AnnouncementSignatures, ChannelUpdate, ClosingSigned, CommitSig, Error, FailureMessageCodecs, PermanentChannelFailure, RevokeAndAck, Shutdown, TemporaryNodeFailure, UpdateAddHtlc, UpdateFailHtlc, UpdateFailMalformedHtlc, UpdateFee, UpdateFulfillHtlc, Warning} +import fr.acinq.eclair.wire.protocol.{AnnouncementSignatures, ClosingSigned, CommitSig, Error, FailureMessageCodecs, PermanentChannelFailure, RevokeAndAck, Shutdown, TemporaryNodeFailure, UpdateAddHtlc, UpdateFailHtlc, UpdateFailMalformedHtlc, UpdateFee, UpdateFulfillHtlc, Warning} import org.scalatest.funsuite.FixtureAnyFunSuiteLike import org.scalatest.{Outcome, Tag} import scodec.bits._ @@ -3400,78 +3400,157 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(addSettled.htlc == htlc1) } - test("recv WatchFundingDeeplyBuriedTriggered", Tag(ChannelStateTestsTags.ChannelsPublic)) { f => + test("recv WatchFundingDeeplyBuriedTriggered (public channel)", Tag(ChannelStateTestsTags.ChannelsPublic)) { f => import f._ - alice ! WatchFundingDeeplyBuriedTriggered(BlockHeight(400000), 42, null) + val realShortChannelId = alice.stateData.asInstanceOf[DATA_NORMAL].shortIds.real.asInstanceOf[RealScidStatus.Temporary].realScid + val bobAlias = bob.stateData.asInstanceOf[DATA_NORMAL].shortIds.localAlias + // existing funding tx coordinates + val TxCoordinates(blockHeight, txIndex, _) = ShortChannelId.coordinates(realShortChannelId) + alice ! WatchFundingDeeplyBuriedTriggered(blockHeight, txIndex, null) val annSigs = alice2bob.expectMsgType[AnnouncementSignatures] - // public channel: we don't send the channel_update directly to the peer - alice2bob.expectNoMessage(1 second) - awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].shortChannelId == annSigs.shortChannelId && alice.stateData.asInstanceOf[DATA_NORMAL].buried) - // we don't re-publish the same channel_update if there was no change - channelUpdateListener.expectNoMessage(1 second) + assert(annSigs.shortChannelId == realShortChannelId) + // alice updates her internal state + awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].shortIds.real == RealScidStatus.Final(realShortChannelId)) + // public channel: alice will update the channel update to use the real scid when she receives her peer's announcement_signatures + alice2bob.expectNoMessage(100 millis) + channelUpdateListener.expectNoMessage(100 millis) + assert(alice.stateData.asInstanceOf[DATA_NORMAL].channelUpdate.shortChannelId == bobAlias) } - test("recv WatchFundingDeeplyBuriedTriggered (short channel id changed)", Tag(ChannelStateTestsTags.ChannelsPublic)) { f => + test("recv WatchFundingDeeplyBuriedTriggered (public channel, zero-conf)", Tag(ChannelStateTestsTags.ChannelsPublic), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs), Tag(ChannelStateTestsTags.ZeroConf)) { f => import f._ - alice ! WatchFundingDeeplyBuriedTriggered(BlockHeight(400001), 22, null) + // in zero-conf channel we don't have a real short channel id when going to NORMAL state + assert(alice.stateData.asInstanceOf[DATA_NORMAL].shortIds.real == RealScidStatus.Unknown) + val bobAlias = bob.stateData.asInstanceOf[DATA_NORMAL].shortIds.localAlias + // funding tx coordinates (unknown before) + val (blockHeight, txIndex) = (BlockHeight(400000), 42) + alice ! WatchFundingDeeplyBuriedTriggered(blockHeight, txIndex, null) + val realShortChannelId = RealShortChannelId(blockHeight, txIndex, alice.stateData.asInstanceOf[DATA_NORMAL].commitments.commitInput.outPoint.index.toInt) val annSigs = alice2bob.expectMsgType[AnnouncementSignatures] - // public channel: we don't send the channel_update directly to the peer - alice2bob.expectNoMessage(1 second) - awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].shortChannelId == annSigs.shortChannelId && alice.stateData.asInstanceOf[DATA_NORMAL].buried) - assert(channelUpdateListener.expectMsgType[LocalChannelUpdate].shortChannelId == alice.stateData.asInstanceOf[DATA_NORMAL].shortChannelId) - channelUpdateListener.expectNoMessage(1 second) + assert(annSigs.shortChannelId == realShortChannelId) + // alice updates her internal state + awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].shortIds.real == RealScidStatus.Final(realShortChannelId)) + // public channel: alice will update the channel update to use the real scid when she receives her peer's announcement_signatures + alice2bob.expectNoMessage(100 millis) + channelUpdateListener.expectNoMessage(100 millis) + assert(alice.stateData.asInstanceOf[DATA_NORMAL].channelUpdate.shortChannelId == bobAlias) + } + + test("recv WatchFundingDeeplyBuriedTriggered (public channel, short channel id changed)", Tag(ChannelStateTestsTags.ChannelsPublic)) { f => + import f._ + val realShortChannelId = alice.stateData.asInstanceOf[DATA_NORMAL].shortIds.real.asInstanceOf[RealScidStatus.Temporary].realScid + val bobAlias = bob.stateData.asInstanceOf[DATA_NORMAL].shortIds.localAlias + // existing funding tx coordinates + val TxCoordinates(blockHeight, txIndex, _) = ShortChannelId.coordinates(realShortChannelId) + // new funding tx coordinates (there was a reorg) + val (blockHeight1, txIndex1) = (blockHeight + 10, txIndex + 10) + alice ! WatchFundingDeeplyBuriedTriggered(blockHeight1, txIndex1, null) + val newRealShortChannelId = RealShortChannelId(blockHeight1, txIndex1, alice.stateData.asInstanceOf[DATA_NORMAL].commitments.commitInput.outPoint.index.toInt) + val annSigs = alice2bob.expectMsgType[AnnouncementSignatures] + assert(annSigs.shortChannelId == newRealShortChannelId) + // update data with real short channel id + awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].shortIds.real == RealScidStatus.Final(newRealShortChannelId)) + // public channel: alice will update the channel update to use the real scid when she receives her peer's announcement_signatures + alice2bob.expectNoMessage(100 millis) + channelUpdateListener.expectNoMessage(100 millis) + assert(alice.stateData.asInstanceOf[DATA_NORMAL].channelUpdate.shortChannelId == bobAlias) } test("recv WatchFundingDeeplyBuriedTriggered (private channel)") { f => import f._ - alice ! WatchFundingDeeplyBuriedTriggered(BlockHeight(400000), 42, null) - // private channel: we send the channel_update directly to the peer - val channelUpdate = alice2bob.expectMsgType[ChannelUpdate] - awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].shortChannelId == channelUpdate.shortChannelId && alice.stateData.asInstanceOf[DATA_NORMAL].buried) - // we don't re-publish the same channel_update if there was no change - channelUpdateListener.expectNoMessage(1 second) + val realShortChannelId = alice.stateData.asInstanceOf[DATA_NORMAL].shortIds.real.asInstanceOf[RealScidStatus.Temporary].realScid + val bobAlias = bob.stateData.asInstanceOf[DATA_NORMAL].shortIds.localAlias + // existing funding tx coordinates + val TxCoordinates(blockHeight, txIndex, _) = ShortChannelId.coordinates(realShortChannelId) + alice ! WatchFundingDeeplyBuriedTriggered(blockHeight, txIndex, null) + // update data with real short channel id + awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].shortIds.real == RealScidStatus.Final(realShortChannelId)) + // private channel: we prefer the remote alias, so there is no change in the channel_update, and we don't send a new one + alice2bob.expectNoMessage(100 millis) + channelUpdateListener.expectNoMessage(100 millis) + assert(alice.stateData.asInstanceOf[DATA_NORMAL].channelUpdate.shortChannelId == bobAlias) + } + + test("recv WatchFundingDeeplyBuriedTriggered (private channel, zero-conf)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs), Tag(ChannelStateTestsTags.ZeroConf)) { f => + import f._ + // we create a new listener that registers after alice has published the funding tx + val listener = TestProbe() + alice.underlying.system.eventStream.subscribe(listener.ref, classOf[TransactionConfirmed]) + // zero-conf channel: the funding tx isn't confirmed + assert(alice.stateData.asInstanceOf[DATA_NORMAL].shortIds.real == RealScidStatus.Unknown) + val bobAlias = bob.stateData.asInstanceOf[DATA_NORMAL].shortIds.localAlias + alice ! WatchFundingDeeplyBuriedTriggered(BlockHeight(42000), 42, null) + val realShortChannelId = RealShortChannelId(BlockHeight(42000), 42, 0) + // update data with real short channel id + awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].shortIds.real == RealScidStatus.Final(realShortChannelId)) + // private channel: we prefer the remote alias, so there is no change in the channel_update, and we don't send a new one + alice2bob.expectNoMessage(100 millis) + channelUpdateListener.expectNoMessage(100 millis) + assert(alice.stateData.asInstanceOf[DATA_NORMAL].channelUpdate.shortChannelId == bobAlias) + // this is the first time we know the funding tx has been confirmed + listener.expectMsgType[TransactionConfirmed] } test("recv WatchFundingDeeplyBuriedTriggered (private channel, short channel id changed)") { f => import f._ - alice ! WatchFundingDeeplyBuriedTriggered(BlockHeight(400001), 22, null) - // private channel: we send the channel_update directly to the peer - val channelUpdate = alice2bob.expectMsgType[ChannelUpdate] - awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].shortChannelId == channelUpdate.shortChannelId && alice.stateData.asInstanceOf[DATA_NORMAL].buried) - // LocalChannelUpdate should not be published - assert(channelUpdateListener.expectMsgType[LocalChannelUpdate].shortChannelId == alice.stateData.asInstanceOf[DATA_NORMAL].shortChannelId) - channelUpdateListener.expectNoMessage(1 second) + val realShortChannelId = alice.stateData.asInstanceOf[DATA_NORMAL].shortIds.real.asInstanceOf[RealScidStatus.Temporary].realScid + val bobAlias = bob.stateData.asInstanceOf[DATA_NORMAL].shortIds.localAlias + // existing funding tx coordinates + val TxCoordinates(blockHeight, txIndex, _) = ShortChannelId.coordinates(realShortChannelId) + // new funding tx coordinates (there was a reorg) + val (blockHeight1, txIndex1) = (blockHeight + 10, txIndex + 10) + alice ! WatchFundingDeeplyBuriedTriggered(blockHeight1, txIndex1, null) + val newRealShortChannelId = RealShortChannelId(blockHeight1, txIndex1, alice.stateData.asInstanceOf[DATA_NORMAL].commitments.commitInput.outPoint.index.toInt) + // update data with real short channel id + awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].shortIds.real == RealScidStatus.Final(newRealShortChannelId)) + // private channel: we prefer the remote alias, so there is no change in the channel_update, and we don't send a new one + alice2bob.expectNoMessage(100 millis) + channelUpdateListener.expectNoMessage(100 millis) + assert(alice.stateData.asInstanceOf[DATA_NORMAL].channelUpdate.shortChannelId == bobAlias) } test("recv AnnouncementSignatures", Tag(ChannelStateTestsTags.ChannelsPublic)) { f => import f._ val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] - alice ! WatchFundingDeeplyBuriedTriggered(BlockHeight(400000), 42, null) + val realShortChannelId = initialState.shortIds.real.asInstanceOf[RealScidStatus.Temporary].realScid + // existing funding tx coordinates + val TxCoordinates(blockHeight, txIndex, _) = ShortChannelId.coordinates(realShortChannelId) + + alice ! WatchFundingDeeplyBuriedTriggered(blockHeight, txIndex, null) val annSigsA = alice2bob.expectMsgType[AnnouncementSignatures] - bob ! WatchFundingDeeplyBuriedTriggered(BlockHeight(400000), 42, null) + bob ! WatchFundingDeeplyBuriedTriggered(blockHeight, txIndex, null) val annSigsB = bob2alice.expectMsgType[AnnouncementSignatures] import initialState.commitments.{localParams, remoteParams} val channelAnn = Announcements.makeChannelAnnouncement(Alice.nodeParams.chainHash, annSigsA.shortChannelId, Alice.nodeParams.nodeId, remoteParams.nodeId, Alice.channelKeyManager.fundingPublicKey(localParams.fundingKeyPath).publicKey, remoteParams.fundingPubKey, annSigsA.nodeSignature, annSigsB.nodeSignature, annSigsA.bitcoinSignature, annSigsB.bitcoinSignature) // actual test starts here - bob2alice.forward(alice) - awaitCond({ + bob2alice.forward(alice, annSigsB) + awaitAssert { val normal = alice.stateData.asInstanceOf[DATA_NORMAL] - normal.shortChannelId == annSigsA.shortChannelId && normal.buried && normal.channelAnnouncement.contains(channelAnn) && normal.channelUpdate.shortChannelId == annSigsA.shortChannelId - }) - assert(channelUpdateListener.expectMsgType[LocalChannelUpdate].channelAnnouncement_opt == Some(channelAnn)) + assert(normal.shortIds.real == RealScidStatus.Final(annSigsA.shortChannelId) && normal.channelAnnouncement.contains(channelAnn) && normal.channelUpdate.shortChannelId == annSigsA.shortChannelId) + } + // we use the real scid instead of remote alias as soon as the channel is announced + val lcu = channelUpdateListener.expectMsgType[LocalChannelUpdate] + assert(lcu.channelUpdate.shortChannelId == realShortChannelId) + assert(lcu.channelAnnouncement_opt.contains(channelAnn)) + // we don't send directly the channel_update to our peer, public announcements are handled by the router + alice2bob.expectNoMessage(100 millis) } test("recv AnnouncementSignatures (re-send)", Tag(ChannelStateTestsTags.ChannelsPublic)) { f => import f._ val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] - alice ! WatchFundingDeeplyBuriedTriggered(BlockHeight(42), 10, null) + val realShortChannelId = initialState.shortIds.real.asInstanceOf[RealScidStatus.Temporary].realScid + // existing funding tx coordinates + val TxCoordinates(blockHeight, txIndex, _) = ShortChannelId.coordinates(realShortChannelId) + + alice ! WatchFundingDeeplyBuriedTriggered(blockHeight, txIndex, null) val annSigsA = alice2bob.expectMsgType[AnnouncementSignatures] - bob ! WatchFundingDeeplyBuriedTriggered(BlockHeight(42), 10, null) + bob ! WatchFundingDeeplyBuriedTriggered(blockHeight, txIndex, null) val annSigsB = bob2alice.expectMsgType[AnnouncementSignatures] import initialState.commitments.{localParams, remoteParams} val channelAnn = Announcements.makeChannelAnnouncement(Alice.nodeParams.chainHash, annSigsA.shortChannelId, Alice.nodeParams.nodeId, remoteParams.nodeId, Alice.channelKeyManager.fundingPublicKey(localParams.fundingKeyPath).publicKey, remoteParams.fundingPubKey, annSigsA.nodeSignature, annSigsB.nodeSignature, annSigsA.bitcoinSignature, annSigsB.bitcoinSignature) bob2alice.forward(alice) - awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].channelAnnouncement == Some(channelAnn)) + awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].channelAnnouncement.contains(channelAnn)) // actual test starts here // simulate bob re-sending its sigs @@ -3527,9 +3606,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with test("recv INPUT_DISCONNECTED") { f => import f._ - alice ! WatchFundingDeeplyBuriedTriggered(BlockHeight(400000), 42, null) - val update1a = alice2bob.expectMsgType[ChannelUpdate] - assert(update1a.channelFlags.isEnabled) + assert(alice.stateData.asInstanceOf[DATA_NORMAL].channelUpdate.channelFlags.isEnabled) // actual test starts here alice ! INPUT_DISCONNECTED @@ -3540,10 +3617,8 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with test("recv INPUT_DISCONNECTED (with pending unsigned htlcs)") { f => import f._ + assert(alice.stateData.asInstanceOf[DATA_NORMAL].channelUpdate.channelFlags.isEnabled) val sender = TestProbe() - alice ! WatchFundingDeeplyBuriedTriggered(BlockHeight(400000), 42, null) - val update1a = alice2bob.expectMsgType[ChannelUpdate] - assert(update1a.channelFlags.isEnabled) val (_, htlc1) = addHtlc(10000 msat, alice, bob, alice2bob, bob2alice, sender.ref) sender.expectMsgType[RES_SUCCESS[CMD_ADD_HTLC]] val (_, htlc2) = addHtlc(10000 msat, alice, bob, alice2bob, bob2alice, sender.ref) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/OfflineStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/OfflineStateSpec.scala index cca34ac7d0..6a66d8a789 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/OfflineStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/OfflineStateSpec.scala @@ -18,9 +18,10 @@ package fr.acinq.eclair.channel.states.e import akka.actor.ActorRef import akka.testkit.{TestFSMRef, TestProbe} +import com.softwaremill.quicklens.ModifyPimp +import fr.acinq.bitcoin.ScriptFlags import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} import fr.acinq.bitcoin.scalacompat.{ByteVector32, Transaction} -import fr.acinq.bitcoin.ScriptFlags import fr.acinq.eclair.TestConstants.{Alice, Bob} import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._ import fr.acinq.eclair.blockchain.fee.FeeratesPerKw @@ -31,7 +32,7 @@ import fr.acinq.eclair.channel.publish.TxPublisher.{PublishFinalTx, PublishTx} import fr.acinq.eclair.channel.states.ChannelStateTestsBase import fr.acinq.eclair.transactions.Transactions.HtlcSuccessTx import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{BlockHeight, CltvExpiry, CltvExpiryDelta, MilliSatoshiLong, TestConstants, TestFeeEstimator, TestKitBaseClass, randomBytes32} +import fr.acinq.eclair.{BlockHeight, CltvExpiry, CltvExpiryDelta, MilliSatoshiLong, TestConstants, TestFeeEstimator, TestKitBaseClass, TestUtils, TimestampMilli, randomBytes32} import org.scalatest.funsuite.FixtureAnyFunSuiteLike import org.scalatest.{Outcome, Tag} @@ -45,14 +46,22 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with type FixtureParam = SetupFixture + /** If set, do not close channel in case of a fee mismatch when disconnected */ + val DisableOfflineMismatch = "disable_offline_mismatch" + /** If set, channel_update will be ignored */ + val IgnoreChannelUpdates = "ignore_channel_updates" + override def withFixture(test: OneArgTest): Outcome = { - val setup = test.tags.contains("disable-offline-mismatch") match { - case false => init() - case true => init(nodeParamsA = Alice.nodeParams.copy(onChainFeeConf = Alice.nodeParams.onChainFeeConf.copy(closeOnOfflineMismatch = false))) - } + val aliceParams = Alice.nodeParams + .modify(_.onChainFeeConf.closeOnOfflineMismatch).setToIf(test.tags.contains(DisableOfflineMismatch))(false) + val setup = init(nodeParamsA = aliceParams) import setup._ within(30 seconds) { reachNormal(setup) + if (test.tags.contains(IgnoreChannelUpdates)) { + setup.alice2bob.ignoreMsg({ case _: ChannelUpdate => true }) + setup.bob2alice.ignoreMsg({ case _: ChannelUpdate => true }) + } awaitCond(alice.stateName == NORMAL) awaitCond(bob.stateName == NORMAL) withFixture(test.toNoArgTest(setup)) @@ -62,7 +71,7 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val aliceInit = Init(TestConstants.Alice.nodeParams.features.initFeatures()) val bobInit = Init(TestConstants.Bob.nodeParams.features.initFeatures()) - test("re-send lost htlc and signature after first commitment") { f => + test("re-send lost htlc and signature after first commitment", Tag(IgnoreChannelUpdates)) { f => import f._ // alice bob // | | @@ -85,9 +94,9 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice2bob.forward(bob, reestablishA) bob2alice.forward(alice, reestablishB) - // both nodes will send the funding_locked message because all updates have been cancelled - alice2bob.expectMsgType[FundingLocked] - bob2alice.expectMsgType[FundingLocked] + // both nodes will send the channel_ready message because all updates have been cancelled + alice2bob.expectMsgType[ChannelReady] + bob2alice.expectMsgType[ChannelReady] // alice will re-send the update and the sig alice2bob.expectMsg(htlc) @@ -115,7 +124,7 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with awaitCond(bob.stateName == NORMAL) } - test("re-send lost revocation") { f => + test("re-send lost revocation", Tag(IgnoreChannelUpdates)) { f => import f._ // alice bob // | | @@ -159,7 +168,7 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with awaitCond(bob.stateName == NORMAL) } - test("re-send lost signature after revocation") { f => + test("re-send lost signature after revocation", Tag(IgnoreChannelUpdates)) { f => import f._ // alice bob // | | @@ -232,7 +241,7 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with awaitCond(bob.stateName == NORMAL) } - test("resume htlc settlement") { f => + test("resume htlc settlement", Tag(IgnoreChannelUpdates)) { f => import f._ // Successfully send a first payment. @@ -279,7 +288,7 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.index == 4) } - test("reconnect with an outdated commitment") { f => + test("reconnect with an outdated commitment", Tag(IgnoreChannelUpdates)) { f => import f._ val (ra1, htlca1) = addHtlc(250000000 msat, alice, bob, alice2bob, bob2alice) @@ -329,7 +338,7 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Transaction.correctlySpends(claimMainOutput, bobCommitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) } - test("reconnect with an outdated commitment (but counterparty can't tell)") { f => + test("reconnect with an outdated commitment (but counterparty can't tell)", Tag(IgnoreChannelUpdates)) { f => import f._ // we start by storing the current state @@ -384,7 +393,7 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Transaction.correctlySpends(claimMainOutput, bobCommitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) } - test("counterparty lies about having a more recent commitment") { f => + test("counterparty lies about having a more recent commitment", Tag(IgnoreChannelUpdates)) { f => import f._ val aliceCommitTx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx @@ -406,7 +415,7 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(error == Error(channelId(alice), InvalidRevokedCommitProof(channelId(alice), 0, 42, invalidReestablish.yourLastPerCommitmentSecret).getMessage)) } - test("change relay fee while offline") { f => + test("change relay fee while offline", Tag(IgnoreChannelUpdates)) { f => import f._ val sender = TestProbe() @@ -461,7 +470,7 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(!update.channelUpdate.channelFlags.isEnabled) } - test("replay pending commands when going back to NORMAL") { f => + test("replay pending commands when going back to NORMAL", Tag(IgnoreChannelUpdates)) { f => import f._ val (r, htlc) = addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) @@ -613,7 +622,7 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with } } - test("handle feerate changes while offline (don't close on mismatch)", Tag("disable-offline-mismatch")) { f => + test("handle feerate changes while offline (don't close on mismatch)", Tag(DisableOfflineMismatch)) { f => import f._ // we only close channels on feerate mismatch if there are HTLCs at risk in the commitment @@ -660,7 +669,7 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with bob2alice.expectMsgType[ChannelReestablish] bob2alice.forward(alice) - alice2bob.expectMsgType[FundingLocked] // since the channel's commitment hasn't been updated, we re-send funding_locked + alice2bob.expectMsgType[ChannelReady] // since the channel's commitment hasn't been updated, we re-send channel_ready if (shouldUpdateFee) { alice2bob.expectMsg(UpdateFee(channelId(alice), networkFeeratePerKw)) } else { @@ -669,11 +678,11 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with } } - test("handle feerate changes while offline (update at reconnection)") { f => + test("handle feerate changes while offline (update at reconnection)", Tag(IgnoreChannelUpdates)) { f => testUpdateFeeOnReconnect(f, shouldUpdateFee = true) } - test("handle feerate changes while offline (shutdown sent, don't update at reconnection)") { f => + test("handle feerate changes while offline (shutdown sent, don't update at reconnection)", Tag(IgnoreChannelUpdates)) { f => import f._ // alice initiates a shutdown @@ -722,20 +731,27 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with } } - test("re-send channel_update at reconnection for private channels") { f => + test("re-send channel_update at reconnection for unannounced channels") { f => import f._ // we simulate a disconnection / reconnection disconnect(alice, bob) + // we wait 1s so the new channel_update doesn't have the same timestamp + TestUtils.waitFor(1 second) reconnect(alice, bob, alice2bob, bob2alice) alice2bob.expectMsgType[ChannelReestablish] bob2alice.expectMsgType[ChannelReestablish] bob2alice.forward(alice) alice2bob.forward(bob) - // at this point the channel isn't deeply buried: channel_update isn't sent again - alice2bob.expectMsgType[FundingLocked] - bob2alice.expectMsgType[FundingLocked] + // alice and bob resend their channel_ready because there hasn't been payments on the channel + alice2bob.expectMsgType[ChannelReady] + bob2alice.expectMsgType[ChannelReady] + + // alice and bob resend their channel update at reconnection (unannounced channel) + alice2bob.expectMsgType[ChannelUpdate] + bob2alice.expectMsgType[ChannelUpdate] + alice2bob.expectNoMessage() bob2alice.expectNoMessage() @@ -745,24 +761,31 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // we simulate a disconnection / reconnection disconnect(alice, bob) + // we wait 1s so the new channel_update doesn't have the same timestamp + TestUtils.waitFor(1 second) reconnect(alice, bob, alice2bob, bob2alice) alice2bob.expectMsgType[ChannelReestablish] bob2alice.expectMsgType[ChannelReestablish] bob2alice.forward(alice) alice2bob.forward(bob) - // at this point the channel still isn't deeply buried: channel_update isn't sent again + // alice and bob resend their channel update at reconnection (unannounced channel) + alice2bob.expectMsgType[ChannelUpdate] + bob2alice.expectMsgType[ChannelUpdate] + alice2bob.expectNoMessage() bob2alice.expectNoMessage() - // funding tx gets 6 confirmations, channel is private so there is no announcement sigs + // funding tx gets 6 confirmations alice ! WatchFundingDeeplyBuriedTriggered(BlockHeight(400000), 42, null) bob ! WatchFundingDeeplyBuriedTriggered(BlockHeight(400000), 42, null) - alice2bob.expectMsgType[ChannelUpdate] - bob2alice.expectMsgType[ChannelUpdate] + // channel is private so there is no announcement sigs + // we use aliases so there is no need to resend a channel_update // we get disconnected again disconnect(alice, bob) + // we wait 1s so the new channel_update doesn't have the same timestamp + TestUtils.waitFor(1 second) reconnect(alice, bob, alice2bob, bob2alice) alice2bob.expectMsgType[ChannelReestablish] bob2alice.expectMsgType[ChannelReestablish] diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala index 52052b9e67..556b453386 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala @@ -66,10 +66,11 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with if (unconfirmedFundingTx) { within(30 seconds) { val channelConfig = ChannelConfig.standard - val (aliceParams, bobParams, channelType) = computeFeatures(setup, test.tags) + val channelFlags = ChannelFlags.Private + val (aliceParams, bobParams, channelType) = computeFeatures(setup, test.tags, channelFlags) val aliceInit = Init(aliceParams.initFeatures) val bobInit = Init(bobParams.initFeatures) - alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, ChannelFlags.Private, channelConfig, channelType) + alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType) alice2blockchain.expectMsgType[SetChannelId] bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType) bob2blockchain.expectMsgType[SetChannelId] diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/AuditDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/AuditDbSpec.scala index 39506f983f..9ef2ca1d01 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/AuditDbSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/AuditDbSpec.scala @@ -845,7 +845,7 @@ class AuditDbSpec extends AnyFunSuite { val scid = ShortChannelId(123) val remoteNodeId = randomKey().publicKey val u = Announcements.makeChannelUpdate(randomBytes32(), randomKey(), remoteNodeId, scid, CltvExpiryDelta(56), 2000 msat, 1000 msat, 999, 1000000000 msat) - dbs.audit.addChannelUpdate(ChannelUpdateParametersChanged(null, channelId, scid, remoteNodeId, u)) + dbs.audit.addChannelUpdate(ChannelUpdateParametersChanged(null, channelId, remoteNodeId, u)) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/ChannelsDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/ChannelsDbSpec.scala index 5d4c5dede7..1a8c6e75d4 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/ChannelsDbSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/ChannelsDbSpec.scala @@ -19,6 +19,7 @@ package fr.acinq.eclair.db import com.softwaremill.quicklens._ import fr.acinq.bitcoin.scalacompat.ByteVector32 import fr.acinq.eclair.TestDatabases.{TestPgDatabases, TestSqliteDatabases, migrationCheck} +import fr.acinq.eclair.channel.RealScidStatus import fr.acinq.eclair.db.ChannelsDbSpec.{getPgTimestamp, getTimestamp, testCases} import fr.acinq.eclair.db.DbEventHandler.ChannelEvent import fr.acinq.eclair.db.jdbc.JdbcUtils.using @@ -28,7 +29,7 @@ import fr.acinq.eclair.db.sqlite.SqliteChannelsDb import fr.acinq.eclair.db.sqlite.SqliteUtils.ExtendedResultSet._ import fr.acinq.eclair.wire.internal.channel.ChannelCodecs.channelDataCodec import fr.acinq.eclair.wire.internal.channel.ChannelCodecsSpec -import fr.acinq.eclair.{CltvExpiry, ShortChannelId, TestDatabases, randomBytes32} +import fr.acinq.eclair.{CltvExpiry, RealShortChannelId, TestDatabases, randomBytes32} import org.scalatest.funsuite.AnyFunSuite import scodec.bits.ByteVector @@ -60,7 +61,7 @@ class ChannelsDbSpec extends AnyFunSuite { val channel1 = ChannelCodecsSpec.normal val channel2a = ChannelCodecsSpec.normal.modify(_.commitments.channelId).setTo(randomBytes32()) - val channel2b = channel2a.modify(_.shortChannelId).setTo(ShortChannelId(189371)) + val channel2b = channel2a.modify(_.shortIds.real).setTo(RealScidStatus.Final(RealShortChannelId(189371))) val commitNumber = 42 val paymentHash1 = ByteVector32.Zeroes diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/NetworkDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/NetworkDbSpec.scala index a1e39298f9..3d56d767fb 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/NetworkDbSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/NetworkDbSpec.scala @@ -29,7 +29,7 @@ import fr.acinq.eclair.router.Announcements import fr.acinq.eclair.router.Router.PublicChannel import fr.acinq.eclair.wire.protocol.LightningMessageCodecs.{channelAnnouncementCodec, channelUpdateCodec, nodeAnnouncementCodec} import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{CltvExpiryDelta, Features, MilliSatoshiLong, ShortChannelId, TestDatabases, randomBytes32, randomKey} +import fr.acinq.eclair.{CltvExpiryDelta, Features, MilliSatoshiLong, RealShortChannelId, ShortChannelId, TestDatabases, randomBytes32, randomKey} import org.scalatest.funsuite.AnyFunSuite import scala.collection.{SortedMap, mutable} @@ -81,7 +81,7 @@ class NetworkDbSpec extends AnyFunSuite { forAllDbs { dbs => val db = dbs.network val sig = ByteVector64.Zeroes - val c = Announcements.makeChannelAnnouncement(Block.RegtestGenesisBlock.hash, ShortChannelId(42), randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, sig, sig, sig, sig) + val c = Announcements.makeChannelAnnouncement(Block.RegtestGenesisBlock.hash, RealShortChannelId(42), randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, sig, sig, sig, sig) val txid = ByteVector32.fromValidHex("0001" * 16) db.addChannel(c, txid, Satoshi(42)) assert(db.listChannels() == SortedMap(c.shortChannelId -> PublicChannel(c, txid, Satoshi(42), None, None, None))) @@ -104,9 +104,9 @@ class NetworkDbSpec extends AnyFunSuite { val b = generatePubkeyHigherThan(a) val c = generatePubkeyHigherThan(b) - val channel_1 = Announcements.makeChannelAnnouncement(Block.RegtestGenesisBlock.hash, ShortChannelId(42), a.publicKey, b.publicKey, randomKey().publicKey, randomKey().publicKey, sig, sig, sig, sig) - val channel_2 = Announcements.makeChannelAnnouncement(Block.RegtestGenesisBlock.hash, ShortChannelId(43), a.publicKey, c.publicKey, randomKey().publicKey, randomKey().publicKey, sig, sig, sig, sig) - val channel_3 = Announcements.makeChannelAnnouncement(Block.RegtestGenesisBlock.hash, ShortChannelId(44), b.publicKey, c.publicKey, randomKey().publicKey, randomKey().publicKey, sig, sig, sig, sig) + val channel_1 = Announcements.makeChannelAnnouncement(Block.RegtestGenesisBlock.hash, RealShortChannelId(42), a.publicKey, b.publicKey, randomKey().publicKey, randomKey().publicKey, sig, sig, sig, sig) + val channel_2 = Announcements.makeChannelAnnouncement(Block.RegtestGenesisBlock.hash, RealShortChannelId(43), a.publicKey, c.publicKey, randomKey().publicKey, randomKey().publicKey, sig, sig, sig, sig) + val channel_3 = Announcements.makeChannelAnnouncement(Block.RegtestGenesisBlock.hash, RealShortChannelId(44), b.publicKey, c.publicKey, randomKey().publicKey, randomKey().publicKey, sig, sig, sig, sig) val txid_1 = randomBytes32() val txid_2 = randomBytes32() @@ -194,7 +194,7 @@ class NetworkDbSpec extends AnyFunSuite { } } - val shortChannelIds: Seq[ShortChannelId] = (42 to (5000 + 42)).map(i => ShortChannelId(i)) + val shortChannelIds: Seq[RealShortChannelId] = (42 to (5000 + 42)).map(i => RealShortChannelId(i)) test("remove many channels") { forAllDbs { dbs => @@ -224,7 +224,7 @@ class NetworkDbSpec extends AnyFunSuite { db.addToPruned(shortChannelIds) shortChannelIds.foreach { id => assert(db.isPruned(id)) } - db.removeFromPruned(ShortChannelId(5)) + db.removeFromPruned(RealShortChannelId(5)) assert(!db.isPruned(ShortChannelId(5))) } } @@ -381,7 +381,7 @@ object NetworkDbSpec { val channelTestCases: Seq[ChannelTestCase] = for (_ <- 0 until 10) yield { val a = randomKey() val b = generatePubkeyHigherThan(a) - val channel = Announcements.makeChannelAnnouncement(Block.RegtestGenesisBlock.hash, ShortChannelId(Random.nextInt(1_000_000)), a.publicKey, a.publicKey, randomKey().publicKey, randomKey().publicKey, ByteVector64.Zeroes, ByteVector64.Zeroes, ByteVector64.Zeroes, ByteVector64.Zeroes) + val channel = Announcements.makeChannelAnnouncement(Block.RegtestGenesisBlock.hash, RealShortChannelId(Random.nextInt(1_000_000)), a.publicKey, a.publicKey, randomKey().publicKey, randomKey().publicKey, ByteVector64.Zeroes, ByteVector64.Zeroes, ByteVector64.Zeroes, ByteVector64.Zeroes) val channel_update_1_opt = if (Random.nextBoolean()) { Some(Announcements.makeChannelUpdate(Block.RegtestGenesisBlock.hash, a, b.publicKey, channel.shortChannelId, CltvExpiryDelta(5), 7000000 msat, 50000 msat, 100, 500000000L msat, Random.nextBoolean())) } else None diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/ChannelIntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/ChannelIntegrationSpec.scala index c778b391ab..ff22a84393 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/ChannelIntegrationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/ChannelIntegrationSpec.scala @@ -495,7 +495,7 @@ class StandardChannelIntegrationSpec extends ChannelIntegrationSpec { val channelId = sender.expectMsgType[RES_GET_CHANNEL_DATA[PersistentChannelData]].data.channelId awaitCond({ funder.register ! Register.Forward(sender.ref, channelId, CMD_GET_CHANNEL_STATE(ActorRef.noSender)) - sender.expectMsgType[RES_GET_CHANNEL_STATE].state == WAIT_FOR_FUNDING_LOCKED + sender.expectMsgType[RES_GET_CHANNEL_STATE].state == WAIT_FOR_CHANNEL_READY }) generateBlocks(6) @@ -506,7 +506,7 @@ class StandardChannelIntegrationSpec extends ChannelIntegrationSpec { // after 8 blocks the funder is still waiting for funding_locked from the fundee funder.register ! Register.Forward(sender.ref, channelId, CMD_GET_CHANNEL_STATE(ActorRef.noSender)) - assert(sender.expectMsgType[RES_GET_CHANNEL_STATE].state == WAIT_FOR_FUNDING_LOCKED) + assert(sender.expectMsgType[RES_GET_CHANNEL_STATE].state == WAIT_FOR_CHANNEL_READY) // simulate a disconnection sender.send(funder.switchboard, Peer.Disconnect(fundee.nodeParams.nodeId)) @@ -520,7 +520,7 @@ class StandardChannelIntegrationSpec extends ChannelIntegrationSpec { fundeeState == OFFLINE && funderState == OFFLINE }) - // reconnect and check the fundee is waiting for more conf, funder is waiting for fundee to send funding_locked + // reconnect and check the fundee is waiting for more conf, funder is waiting for fundee to send channel_ready awaitCond({ // reconnection sender.send(fundee.switchboard, Peer.Connect( @@ -535,7 +535,7 @@ class StandardChannelIntegrationSpec extends ChannelIntegrationSpec { val fundeeState = sender.expectMsgType[RES_GET_CHANNEL_STATE].state funder.register ! Register.Forward(sender.ref, channelId, CMD_GET_CHANNEL_STATE(ActorRef.noSender)) val funderState = sender.expectMsgType[RES_GET_CHANNEL_STATE].state - fundeeState == WAIT_FOR_FUNDING_CONFIRMED && funderState == WAIT_FOR_FUNDING_LOCKED + fundeeState == WAIT_FOR_FUNDING_CONFIRMED && funderState == WAIT_FOR_CHANNEL_READY }, max = 30 seconds, interval = 10 seconds) // 5 extra blocks make it 13, just the amount of confirmations needed @@ -829,7 +829,7 @@ class AnchorOutputZeroFeeHtlcTxsChannelIntegrationSpec extends AnchorChannelInte } test("open channel C <-> F, send payments and close (anchor outputs zero fee htlc txs)") { - testOpenPayClose(ChannelTypes.AnchorOutputsZeroFeeHtlcTx) + testOpenPayClose(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)) } test("propagate a fulfill upstream when a downstream htlc is redeemed on-chain (local commit, anchor outputs zero fee htlc txs)") { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/IntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/IntegrationSpec.scala index 0b540986b5..1e383811e0 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/IntegrationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/IntegrationSpec.scala @@ -28,7 +28,6 @@ import fr.acinq.eclair.payment.relay.Relayer.RelayFees import fr.acinq.eclair.router.Graph.WeightRatios import fr.acinq.eclair.router.RouteCalculation.ROUTE_MAX_LENGTH import fr.acinq.eclair.router.Router.{MultiPartParams, PathFindingConf, SearchBoundaries, NORMAL => _, State => _} -import fr.acinq.eclair.wire.protocol.NodeAddress import fr.acinq.eclair.{BlockHeight, CltvExpiryDelta, Kit, MilliSatoshi, MilliSatoshiLong, Setup, TestKitBaseClass} import grizzled.slf4j.Logging import org.json4s.{DefaultFormats, Formats} diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/MessageIntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/MessageIntegrationSpec.scala index 836470bf9b..6c19262e89 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/MessageIntegrationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/MessageIntegrationSpec.scala @@ -24,6 +24,7 @@ import akka.util.Timeout import com.typesafe.config.ConfigFactory import fr.acinq.bitcoin.Transaction import fr.acinq.bitcoin.scalacompat.{ByteVector32, Satoshi} +import fr.acinq.eclair.TestUtils.waitEventStreamSynced import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.{Watch, WatchFundingConfirmed} import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinCoreClient @@ -303,11 +304,12 @@ class MessageIntegrationSpec extends IntegrationSpec { val probe = TestProbe() val eventListener = TestProbe() nodes("C").system.eventStream.subscribe(eventListener.ref, classOf[OnionMessages.ReceiveMessage]) + waitEventStreamSynced(nodes("C").system.eventStream) alice.sendOnionMessage(nodes("B").nodeParams.nodeId :: Nil, Left(nodes("C").nodeParams.nodeId), None, hex"7300").pipeTo(probe.ref) assert(probe.expectMsgType[SendOnionMessageResponse].sent) val r = eventListener.expectMsgType[OnionMessages.ReceiveMessage](max = 60 seconds) - assert(r.pathId == None) + assert(r.pathId.isEmpty) assert(r.finalPayload.records.unknown.toSet == Set(GenericTlv(UInt64(115), hex""))) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/PaymentIntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/PaymentIntegrationSpec.scala index 976dfcbbef..e0d7bcca0b 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/PaymentIntegrationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/PaymentIntegrationSpec.scala @@ -32,7 +32,6 @@ import fr.acinq.eclair.crypto.Sphinx.DecryptedFailurePacket import fr.acinq.eclair.crypto.TransportHandler import fr.acinq.eclair.db._ import fr.acinq.eclair.io.Peer.PeerRoutingMessage -import fr.acinq.eclair.payment.Bolt11Invoice.ExtraHop import fr.acinq.eclair.payment._ import fr.acinq.eclair.payment.receive.MultiPartHandler.ReceivePayment import fr.acinq.eclair.payment.relay.Relayer @@ -41,7 +40,7 @@ import fr.acinq.eclair.payment.send.PaymentInitiator.{SendPaymentToNode, SendTra import fr.acinq.eclair.router.Graph.WeightRatios import fr.acinq.eclair.router.Router.{GossipDecision, PublicChannel} import fr.acinq.eclair.router.{Announcements, AnnouncementsBatchValidationSpec, Router} -import fr.acinq.eclair.wire.protocol.{ChannelAnnouncement, ChannelUpdate, IncorrectOrUnknownPaymentDetails, NodeAnnouncement} +import fr.acinq.eclair.wire.protocol.{ChannelAnnouncement, ChannelUpdate, IncorrectOrUnknownPaymentDetails} import fr.acinq.eclair.{CltvExpiryDelta, Features, Kit, MilliSatoshiLong, ShortChannelId, TimestampMilli, randomBytes32} import org.json4s.JsonAST.{JString, JValue} import scodec.bits.ByteVector @@ -114,22 +113,19 @@ class PaymentIntegrationSpec extends IntegrationSpec { } } - def awaitAnnouncements(subset: Map[String, Kit], nodes: Int, channels: Int, updates: Int): Unit = { + def awaitAnnouncements(subset: Map[String, Kit], nodes: Int, privateChannels: Int, publicChannels: Int, privateUpdates: Int, publicUpdates: Int): Unit = { val sender = TestProbe() subset.foreach { case (node, setup) => withClue(node) { awaitAssert({ - sender.send(setup.router, Router.GetNodes) - assert(sender.expectMsgType[Iterable[NodeAnnouncement]].size == nodes) - }, max = 10 seconds, interval = 1 second) - awaitAssert({ - sender.send(setup.router, Router.GetChannels) - sender.expectMsgType[Iterable[ChannelAnnouncement]].size == channels - }, max = 10 seconds, interval = 1 second) - awaitAssert({ - sender.send(setup.router, Router.GetChannelUpdates) - sender.expectMsgType[Iterable[ChannelUpdate]].size == updates + sender.send(setup.router, Router.GetRouterData) + val data = sender.expectMsgType[Router.Data] + assert(data.nodes.size == nodes) + assert(data.privateChannels.size == privateChannels) + assert(data.channels.size == publicChannels) + assert(data.privateChannels.values.flatMap(pc => pc.update_1_opt.toSeq ++ pc.update_2_opt.toSeq).size == privateUpdates) + assert(data.channels.values.flatMap(pc => pc.update_1_opt.toSeq ++ pc.update_2_opt.toSeq).size == publicUpdates) }, max = 10 seconds, interval = 1 second) } } @@ -141,8 +137,8 @@ class PaymentIntegrationSpec extends IntegrationSpec { // A requires private channels, as a consequence: // - only A and B know about channel A-B (and there is no channel_announcement) // - A is not announced (no node_announcement) - awaitAnnouncements(nodes.view.filterKeys(key => List("A", "B").contains(key)).toMap, 6, 8, 18) - awaitAnnouncements(nodes.view.filterKeys(key => List("C", "D", "E", "G").contains(key)).toMap, 6, 8, 16) + awaitAnnouncements(nodes.view.filterKeys(key => List("A", "B").contains(key)).toMap, nodes = 6, privateChannels = 1, publicChannels = 8, privateUpdates = 2, publicUpdates = 16) + awaitAnnouncements(nodes.view.filterKeys(key => List("C", "D", "E", "G").contains(key)).toMap, nodes = 6, privateChannels = 0, publicChannels = 8, privateUpdates = 0, publicUpdates = 16) } test("wait for channels balance") { @@ -153,7 +149,7 @@ class PaymentIntegrationSpec extends IntegrationSpec { val routingState = sender.expectMsgType[Router.RoutingState] val publicChannels = routingState.channels.filter(pc => Set(pc.ann.nodeId1, pc.ann.nodeId2).contains(nodeId)) assert(publicChannels.nonEmpty) - publicChannels.foreach(pc => assert(pc.meta_opt.map(m => m.balance1 > 0.msat || m.balance2 > 0.msat) == Some(true), pc)) + publicChannels.foreach(pc => assert(pc.meta_opt.exists(m => m.balance1 > 0.msat || m.balance2 > 0.msat), pc)) } test("send an HTLC A->D") { @@ -183,11 +179,11 @@ class PaymentIntegrationSpec extends IntegrationSpec { val shortIdBC = sender.expectMsgType[Iterable[ChannelAnnouncement]].find(c => Set(c.nodeId1, c.nodeId2) == Set(nodes("B").nodeParams.nodeId, nodes("C").nodeParams.nodeId)).get.shortChannelId // we also need the full commitment nodes("B").register ! Register.ForwardShortId(sender.ref, shortIdBC, CMD_GET_CHANNEL_INFO(ActorRef.noSender)) - val commitmentBC = sender.expectMsgType[RES_GET_CHANNEL_INFO].data.asInstanceOf[DATA_NORMAL].commitments + val normalBC = sender.expectMsgType[RES_GET_CHANNEL_INFO].data.asInstanceOf[DATA_NORMAL] // we then forge a new channel_update for B-C... val channelUpdateBC = Announcements.makeChannelUpdate(Block.RegtestGenesisBlock.hash, nodes("B").nodeParams.privateKey, nodes("C").nodeParams.nodeId, shortIdBC, nodes("B").nodeParams.channelConf.expiryDelta + 1, nodes("C").nodeParams.channelConf.htlcMinimum, nodes("B").nodeParams.relayParams.publicChannelFees.feeBase, nodes("B").nodeParams.relayParams.publicChannelFees.feeProportionalMillionths, 500000000 msat) // ...and notify B's relayer - nodes("B").system.eventStream.publish(LocalChannelUpdate(system.deadLetters, commitmentBC.channelId, shortIdBC, commitmentBC.remoteParams.nodeId, None, channelUpdateBC, commitmentBC)) + nodes("B").system.eventStream.publish(LocalChannelUpdate(system.deadLetters, normalBC.channelId, normalBC.shortIds, normalBC.commitments.remoteParams.nodeId, normalBC.channelAnnouncement, channelUpdateBC, normalBC.commitments)) // we retrieve a payment hash from D val amountMsat = 4200000.msat sender.send(nodes("D").paymentHandler, ReceivePayment(Some(amountMsat), Left("1 coffee"))) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/ThreeNodesIntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/ThreeNodesIntegrationSpec.scala index f6c2eedcc4..a1c8306f80 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/ThreeNodesIntegrationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/ThreeNodesIntegrationSpec.scala @@ -1,10 +1,9 @@ package fr.acinq.eclair.integration.basic import fr.acinq.bitcoin.scalacompat.{ByteVector32, SatoshiLong} -import fr.acinq.eclair.ShortChannelId.txIndex +import fr.acinq.eclair.MilliSatoshiLong import fr.acinq.eclair.integration.basic.fixtures.ThreeNodesFixture import fr.acinq.eclair.testutils.FixtureSpec -import fr.acinq.eclair.{BlockHeight, MilliSatoshiLong} import org.scalatest.TestData import org.scalatest.concurrent.IntegrationPatience import scodec.bits.HexStringSyntax @@ -36,33 +35,20 @@ class ThreeNodesIntegrationSpec extends FixtureSpec with IntegrationPatience { connect(alice, bob) connect(bob, carol) - val channelIdAB = openChannel(alice, bob, 100_000 sat).channelId - val channelIdBC = openChannel(bob, carol, 100_000 sat).channelId + // we put watchers on auto pilot to confirm funding txs + alice.watcher.setAutoPilot(watcherAutopilot(knownFundingTxs(alice, bob, carol))) + bob.watcher.setAutoPilot(watcherAutopilot(knownFundingTxs(alice, bob, carol))) + carol.watcher.setAutoPilot(watcherAutopilot(knownFundingTxs(alice, bob, carol))) - val fundingTxAB = fundingTx(alice, channelIdAB) - val fundingTxBC = fundingTx(bob, channelIdBC) - - val shortIdAB = confirmChannel(alice, bob, channelIdAB, BlockHeight(420_000), 21) - val shortIdBC = confirmChannel(bob, carol, channelIdBC, BlockHeight(420_001), 22) - - val fundingTxs = Map( - shortIdAB -> fundingTxAB, - shortIdBC -> fundingTxBC - ) - - // auto-validate channel announcements - alice.watcher.setAutoPilot(autoValidatePublicChannels(fundingTxs)) - bob.watcher.setAutoPilot(autoValidatePublicChannels(fundingTxs)) - - confirmChannelDeep(alice, bob, channelIdAB, shortIdAB.blockHeight, txIndex(shortIdAB)) - confirmChannelDeep(bob, carol, channelIdBC, shortIdBC.blockHeight, txIndex(shortIdBC)) + openChannel(alice, bob, 100_000 sat).channelId + openChannel(bob, carol, 100_000 sat).channelId // alice now knows about bob-carol eventually { val routerData = getRouterData(alice) - //prettyPrint(routerData, alice, bob, carol) + prettyPrint(routerData, alice, bob, carol) assert(routerData.channels.size == 2) // 2 channels - assert(routerData.channels.values.flatMap(c => c.update_1_opt.toSeq ++ c.update_2_opt.toSeq).size == 3) // only 3 channel_updates because c->b is disabled (all funds on b) + assert(routerData.channels.values.flatMap(c => c.update_1_opt.toSeq ++ c.update_2_opt.toSeq).size == 4) // 2 channel_updates per channel } sendPayment(alice, carol, 100_000 msat) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/TwoNodesIntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/TwoNodesIntegrationSpec.scala index ae2ab79092..89598db9e5 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/TwoNodesIntegrationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/TwoNodesIntegrationSpec.scala @@ -1,7 +1,7 @@ package fr.acinq.eclair.integration.basic import fr.acinq.bitcoin.scalacompat.{ByteVector32, SatoshiLong} -import fr.acinq.eclair.channel.{DATA_NORMAL, NORMAL} +import fr.acinq.eclair.channel.{DATA_NORMAL, NORMAL, RealScidStatus} import fr.acinq.eclair.integration.basic.fixtures.TwoNodesFixture import fr.acinq.eclair.testutils.FixtureSpec import fr.acinq.eclair.{BlockHeight, MilliSatoshiLong} @@ -45,8 +45,8 @@ class TwoNodesIntegrationSpec extends FixtureSpec with IntegrationPatience { test("open a channel alice-bob (autoconfirm)") { f => import f._ - alice.watcher.setAutoPilot(autoConfirmLocalChannels(alice.wallet.funded)) - bob.watcher.setAutoPilot(autoConfirmLocalChannels(alice.wallet.funded)) + alice.watcher.setAutoPilot(watcherAutopilot(knownFundingTxs(alice))) + bob.watcher.setAutoPilot(watcherAutopilot(knownFundingTxs(alice))) connect(alice, bob) val channelId = openChannel(alice, bob, 100_000 sat).channelId eventually { @@ -61,19 +61,19 @@ class TwoNodesIntegrationSpec extends FixtureSpec with IntegrationPatience { val channelId = openChannel(alice, bob, 100_000 sat).channelId confirmChannel(alice, bob, channelId, BlockHeight(420_000), 21) confirmChannelDeep(alice, bob, channelId, BlockHeight(420_000), 21) - assert(getChannelData(alice, channelId).asInstanceOf[DATA_NORMAL].buried) - assert(getChannelData(bob, channelId).asInstanceOf[DATA_NORMAL].buried) + assert(getChannelData(alice, channelId).asInstanceOf[DATA_NORMAL].shortIds.real.isInstanceOf[RealScidStatus.Final]) + assert(getChannelData(bob, channelId).asInstanceOf[DATA_NORMAL].shortIds.real.isInstanceOf[RealScidStatus.Final]) } test("open a channel alice-bob and confirm deeply (autoconfirm)") { f => import f._ - alice.watcher.setAutoPilot(autoConfirmLocalChannels(alice.wallet.funded)) - bob.watcher.setAutoPilot(autoConfirmLocalChannels(alice.wallet.funded)) + alice.watcher.setAutoPilot(watcherAutopilot(knownFundingTxs(alice))) + bob.watcher.setAutoPilot(watcherAutopilot(knownFundingTxs(alice))) connect(alice, bob) val channelId = openChannel(alice, bob, 100_000 sat).channelId eventually { - assert(getChannelData(alice, channelId).asInstanceOf[DATA_NORMAL].buried) - assert(getChannelData(bob, channelId).asInstanceOf[DATA_NORMAL].buried) + assert(getChannelData(alice, channelId).asInstanceOf[DATA_NORMAL].shortIds.real.isInstanceOf[RealScidStatus.Final]) + assert(getChannelData(bob, channelId).asInstanceOf[DATA_NORMAL].shortIds.real.isInstanceOf[RealScidStatus.Final]) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/ZeroConfActivationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/ZeroConfActivationSpec.scala new file mode 100644 index 0000000000..23313dffe9 --- /dev/null +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/ZeroConfActivationSpec.scala @@ -0,0 +1,79 @@ +package fr.acinq.eclair.integration.basic + +import com.softwaremill.quicklens.ModifyPimp +import fr.acinq.bitcoin.scalacompat.{ByteVector32, SatoshiLong} +import fr.acinq.eclair.FeatureSupport.Optional +import fr.acinq.eclair.Features.ZeroConf +import fr.acinq.eclair.channel.ChannelTypes.AnchorOutputsZeroFeeHtlcTx +import fr.acinq.eclair.channel.PersistentChannelData +import fr.acinq.eclair.integration.basic.fixtures.TwoNodesFixture +import fr.acinq.eclair.testutils.FixtureSpec +import org.scalatest.concurrent.IntegrationPatience +import org.scalatest.{Tag, TestData} +import scodec.bits.HexStringSyntax + +/** + * Test the activation of zero-conf option, via features or channel type. + */ +class ZeroConfActivationSpec extends FixtureSpec with IntegrationPatience { + + type FixtureParam = TwoNodesFixture + + val ZeroConfBob = "zero_conf_bob" + + import fr.acinq.eclair.integration.basic.fixtures.MinimalNodeFixture._ + + 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")) + val bobParams = nodeParamsFor("bob", ByteVector32(hex"7620226fec887b0b2ebe76492e5a3fd3eb0e47cd3773263f6a81b59a704dc492")) + .modify(_.features.activated).using(_ - ZeroConf) // we will enable those features on demand + .modify(_.features.activated).usingIf(testData.tags.contains(ZeroConfBob))(_ + (ZeroConf -> Optional)) + TwoNodesFixture(aliceParams, bobParams) + } + + override def cleanupFixture(fixture: FixtureParam): Unit = { + fixture.cleanup() + } + + test("open a channel alice-bob (zero-conf disabled on both sides)") { f => + import f._ + + assert(!alice.nodeParams.features.activated.contains(ZeroConf)) + assert(!bob.nodeParams.features.activated.contains(ZeroConf)) + + connect(alice, bob) + val channelId = openChannel(alice, bob, 100_000 sat).channelId + + assert(!getChannelData(alice, channelId).asInstanceOf[PersistentChannelData].commitments.channelFeatures.hasFeature(ZeroConf)) + assert(!getChannelData(bob, channelId).asInstanceOf[PersistentChannelData].commitments.channelFeatures.hasFeature(ZeroConf)) + } + + test("open a channel alice-bob (zero-conf disabled on both sides, requested via channel type by alice)") { f => + import f._ + + assert(!alice.nodeParams.features.activated.contains(ZeroConf)) + assert(!bob.nodeParams.features.activated.contains(ZeroConf)) + + connect(alice, bob) + val channelType = AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = true) + // bob rejects the channel + intercept[AssertionError] { + openChannel(alice, bob, 100_000 sat, channelType_opt = Some(channelType)).channelId + } + } + + test("open a channel alice-bob (zero-conf enabled on bob, requested via channel type by alice)", Tag(ZeroConfBob)) { f => + import f._ + + assert(!alice.nodeParams.features.activated.contains(ZeroConf)) + assert(bob.nodeParams.features.activated.contains(ZeroConf)) + + connect(alice, bob) + val channelType = AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = true) + val channelId = openChannel(alice, bob, 100_000 sat, channelType_opt = Some(channelType)).channelId + + assert(getChannelData(alice, channelId).asInstanceOf[PersistentChannelData].commitments.channelFeatures.hasFeature(ZeroConf)) + assert(getChannelData(bob, channelId).asInstanceOf[PersistentChannelData].commitments.channelFeatures.hasFeature(ZeroConf)) + } +} \ No newline at end of file diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/ZeroConfAliasIntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/ZeroConfAliasIntegrationSpec.scala new file mode 100644 index 0000000000..71c26c03b0 --- /dev/null +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/ZeroConfAliasIntegrationSpec.scala @@ -0,0 +1,231 @@ +package fr.acinq.eclair.integration.basic + +import com.softwaremill.quicklens._ +import fr.acinq.bitcoin.scalacompat.{ByteVector32, SatoshiLong} +import fr.acinq.eclair.FeatureSupport.{Mandatory, Optional} +import fr.acinq.eclair.Features.{ScidAlias, ZeroConf} +import fr.acinq.eclair.channel.{DATA_NORMAL, RealScidStatus} +import fr.acinq.eclair.integration.basic.fixtures.ThreeNodesFixture +import fr.acinq.eclair.payment.PaymentSent +import fr.acinq.eclair.testutils.FixtureSpec +import fr.acinq.eclair.{MilliSatoshiLong, RealShortChannelId} +import org.scalatest.OptionValues.convertOptionToValuable +import org.scalatest.concurrent.IntegrationPatience +import org.scalatest.{Tag, TestData} +import scodec.bits.HexStringSyntax + +class ZeroConfAliasIntegrationSpec extends FixtureSpec with IntegrationPatience { + + type FixtureParam = ThreeNodesFixture + + val ZeroConfBobCarol = "zeroconf_bob_carol" + val ScidAliasBobCarol = "scid_alias_bob_carol" + val PublicBobCarol = "public_bob_carol" + + import fr.acinq.eclair.integration.basic.fixtures.MinimalNodeFixture._ + + 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")) + val bobParams = nodeParamsFor("bob", ByteVector32(hex"7620226fec887b0b2ebe76492e5a3fd3eb0e47cd3773263f6a81b59a704dc492")) + .modify(_.features.activated).using(_ - ZeroConf - ScidAlias) // we will enable those features on demand + .modify(_.features.activated).usingIf(testData.tags.contains(ZeroConfBobCarol))(_ + (ZeroConf -> Optional)) + .modify(_.features.activated).usingIf(testData.tags.contains(ScidAliasBobCarol))(_ + (ScidAlias -> Optional)) + .modify(_.channelConf.channelFlags.announceChannel).setTo(testData.tags.contains(PublicBobCarol)) + val carolParams = nodeParamsFor("carol", ByteVector32(hex"ebd5a5d3abfb3ef73731eb3418d918f247445183180522674666db98a66411cc")) + .modify(_.features.activated).using(_ - ZeroConf - ScidAlias) // we will enable those features on demand + .modify(_.features.activated).usingIf(testData.tags.contains(ZeroConfBobCarol))(_ + (ZeroConf -> Mandatory)) + .modify(_.features.activated).usingIf(testData.tags.contains(ScidAliasBobCarol))(_ + (ScidAlias -> Mandatory)) + .modify(_.channelConf.channelFlags.announceChannel).setTo(testData.tags.contains(PublicBobCarol)) + ThreeNodesFixture(aliceParams, bobParams, carolParams) + } + + override def cleanupFixture(fixture: FixtureParam): Unit = { + fixture.cleanup() + } + + private def createChannels(f: FixtureParam)(deepConfirm: Boolean): (ByteVector32, ByteVector32) = { + import f._ + + alice.watcher.setAutoPilot(watcherAutopilot(knownFundingTxs(alice, bob, carol), deepConfirm = deepConfirm)) + bob.watcher.setAutoPilot(watcherAutopilot(knownFundingTxs(alice, bob, carol), deepConfirm = deepConfirm)) + carol.watcher.setAutoPilot(watcherAutopilot(knownFundingTxs(alice, bob, carol), deepConfirm = deepConfirm)) + + connect(alice, bob) + connect(bob, carol) + + val channelId_ab = openChannel(alice, bob, 100_000 sat).channelId + val channelId_bc = openChannel(bob, carol, 100_000 sat).channelId + + (channelId_ab, channelId_bc) + } + + private def sendPaymentAliceToCarol(f: FixtureParam, useHint: Boolean = false, overrideHintScid_opt: Option[RealShortChannelId] = None): PaymentSent = { + import f._ + val hint = if (useHint) { + val Some(carolHint) = getRouterData(carol).privateChannels.values.head.toIncomingExtraHop + // due to how node ids are built, bob < carol so carol is always the node 2 + val bobAlias = getRouterData(bob).privateChannels.values.find(_.nodeId2 == carol.nodeParams.nodeId).value.shortIds.localAlias + // the hint is always using the alias + assert(carolHint.shortChannelId == bobAlias) + Seq(carolHint.modify(_.shortChannelId).setToIfDefined(overrideHintScid_opt)) + } else Seq.empty + sendPayment(alice, carol, 100_000 msat, hints = Seq(hint)) + } + + private def internalTest(f: FixtureParam, + deepConfirm: Boolean, + bcPublic: Boolean, + bcZeroConf: Boolean, + bcScidAlias: Boolean, + paymentWorksWithoutHint: Boolean, + paymentWorksWithHint_opt: Option[Boolean], + paymentWorksWithRealScidHint_opt: Option[Boolean]): Unit = { + import f._ + + val (_, channelId_bc) = createChannels(f)(deepConfirm = deepConfirm) + + eventually { + assert(getChannelData(bob, channelId_bc).asInstanceOf[DATA_NORMAL].commitments.channelFeatures.features.contains(ZeroConf) == bcZeroConf) + assert(getChannelData(bob, channelId_bc).asInstanceOf[DATA_NORMAL].commitments.channelFeatures.features.contains(ScidAlias) == bcScidAlias) + assert(getChannelData(bob, channelId_bc).asInstanceOf[DATA_NORMAL].commitments.channelFlags.announceChannel == bcPublic) + if (deepConfirm) { + assert(getChannelData(bob, channelId_bc).asInstanceOf[DATA_NORMAL].shortIds.real.isInstanceOf[RealScidStatus.Final]) + } else if (bcZeroConf) { + assert(getChannelData(bob, channelId_bc).asInstanceOf[DATA_NORMAL].shortIds.real == RealScidStatus.Unknown) + } else { + assert(getChannelData(bob, channelId_bc).asInstanceOf[DATA_NORMAL].shortIds.real.isInstanceOf[RealScidStatus.Temporary]) + } + } + + if (bcPublic && deepConfirm) { + // if channel bob-carol is public, we wait for alice to learn about it + eventually { + val data = getRouterData(alice) + assert(data.channels.size == 2) + assert(data.channels.values.forall(pc => pc.update_1_opt.isDefined && pc.update_2_opt.isDefined)) + } + } + + if (paymentWorksWithoutHint) { + sendPaymentAliceToCarol(f) + } else { + intercept[AssertionError] { + sendPaymentAliceToCarol(f) + } + } + + paymentWorksWithHint_opt match { + case Some(true) => sendPaymentAliceToCarol(f, useHint = true) + case Some(false) => intercept[AssertionError] { + sendPaymentAliceToCarol(f, useHint = true) + } + case None => // skipped + } + + paymentWorksWithRealScidHint_opt match { + // if alice uses the real scid instead of the bob-carol alias, it still works + case Some(true) => sendPaymentAliceToCarol(f, useHint = true, overrideHintScid_opt = Some(getChannelData(bob, channelId_bc).asInstanceOf[DATA_NORMAL].shortIds.real.toOption.value)) + case Some(false) => intercept[AssertionError] { + sendPaymentAliceToCarol(f, useHint = true, overrideHintScid_opt = Some(getChannelData(bob, channelId_bc).asInstanceOf[DATA_NORMAL].shortIds.real.toOption.value)) + } + case None => // skipped + } + } + + test("a->b->c (b-c private)") { f => + import f._ + + internalTest(f, + deepConfirm = true, + bcPublic = false, + bcZeroConf = false, + bcScidAlias = false, + paymentWorksWithoutHint = false, // alice can't find a route to carol because bob-carol isn't announced + paymentWorksWithHint_opt = Some(true), // with a routing hint the payment works (and it will use the alias, even if the feature isn't enabled) + paymentWorksWithRealScidHint_opt = Some(true) // if alice uses the real scid instead of the bob-carol alias, it still works + ) + } + + test("a->b->c (b-c scid-alias private)", Tag(ScidAliasBobCarol)) { f => + import f._ + + internalTest(f, + deepConfirm = true, + bcPublic = false, + bcZeroConf = false, + bcScidAlias = true, + paymentWorksWithoutHint = false, // alice can't find a route to carol because bob-carol isn't announced + paymentWorksWithHint_opt = Some(true), // with a routing hint the payment works + paymentWorksWithRealScidHint_opt = Some(false) // if alice uses the real scid instead of the bob-carol alias, it doesn't work due to option_scid_alias + ) + } + + test("a->b->c (b-c zero-conf unconfirmed private)", Tag(ZeroConfBobCarol)) { f => + import f._ + + internalTest(f, + deepConfirm = false, + bcPublic = false, + bcZeroConf = true, + bcScidAlias = false, + paymentWorksWithoutHint = false, // alice can't find a route to carol because bob-carol isn't announced + paymentWorksWithHint_opt = Some(true), // with a routing hint the payment works + paymentWorksWithRealScidHint_opt = None // there is no real scid for bob-carol yet + ) + } + + test("a->b->c (b-c zero-conf deeply confirmed private)", Tag(ZeroConfBobCarol)) { f => + internalTest(f, + deepConfirm = true, + bcPublic = false, + bcZeroConf = true, + bcScidAlias = false, + paymentWorksWithoutHint = false, // alice can't find a route to carol because bob-carol isn't announced + paymentWorksWithHint_opt = Some(true), // with a routing hint the payment works + // TODO: we should be able to send payments with the real scid in the routing hint, but this currently doesn't work, + // because the ChannelRelayer relies on the the LocalChannelUpdate event to maintain its scid resolution map, and + // the channel doesn't emit a new one when a real scid is assigned, because we use the remote alias for the + // channel_update, not the real scid. So the channel_update remains the same. We used to have the ChannelRelayer + // also listen to ShortChannelIdAssigned event, but it's doesn't seem worth it here. + paymentWorksWithRealScidHint_opt = None + ) + } + + test("a->b->c (b-c zero-conf scid-alias deeply confirmed private)", Tag(ZeroConfBobCarol), Tag(ScidAliasBobCarol)) { f => + internalTest(f, + deepConfirm = true, + bcPublic = false, + bcZeroConf = true, + bcScidAlias = true, + paymentWorksWithoutHint = false, // alice can't find a route to carol because bob-carol isn't announced + paymentWorksWithHint_opt = Some(true), // with a routing hint the payment works + paymentWorksWithRealScidHint_opt = Some(false) // if alice uses the real scid instead of the b-c alias, it doesn't work due to option_scid_alias + ) + } + + test("a->b->c (b-c zero-conf unconfirmed public)", Tag(ZeroConfBobCarol), Tag(PublicBobCarol)) { f => + internalTest(f, + deepConfirm = false, + bcPublic = true, + bcZeroConf = true, + bcScidAlias = false, + paymentWorksWithoutHint = false, // alice can't find a route to carol because bob-carol isn't announced yet + paymentWorksWithHint_opt = Some(true), // with a routing hint the payment works + paymentWorksWithRealScidHint_opt = None // there is no real scid for bob-carol yet + ) + } + + test("a->b->c (b-c zero-conf deeply confirmed public)", Tag(ZeroConfBobCarol), Tag(PublicBobCarol)) { f => + internalTest(f, + deepConfirm = true, + bcPublic = true, + bcZeroConf = true, + bcScidAlias = false, + paymentWorksWithoutHint = true, + paymentWorksWithHint_opt = None, // there is no routing hints for public channels + paymentWorksWithRealScidHint_opt = None // there is no routing hints for public channels + ) + } + +} diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/fixtures/MinimalNodeFixture.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/fixtures/MinimalNodeFixture.scala index 9962d5972b..491a3ea271 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/fixtures/MinimalNodeFixture.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/fixtures/MinimalNodeFixture.scala @@ -6,6 +6,7 @@ import akka.testkit.{TestActor, TestProbe} import com.softwaremill.quicklens.ModifyPimp import com.typesafe.config.ConfigFactory import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, Satoshi, Transaction} +import fr.acinq.eclair.ShortChannelId.txIndex import fr.acinq.eclair.blockchain.DummyOnChainWallet import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.{WatchFundingConfirmed, WatchFundingConfirmedTriggered, WatchFundingDeeplyBuried, WatchFundingDeeplyBuriedTriggered} @@ -16,13 +17,14 @@ import fr.acinq.eclair.crypto.TransportHandler import fr.acinq.eclair.crypto.keymanager.{LocalChannelKeyManager, LocalNodeKeyManager} import fr.acinq.eclair.io.PeerConnection.ConnectionResult import fr.acinq.eclair.io.{Peer, PeerConnection, Switchboard} +import fr.acinq.eclair.payment.Bolt11Invoice.ExtraHop import fr.acinq.eclair.payment.receive.{MultiPartHandler, PaymentHandler} import fr.acinq.eclair.payment.relay.Relayer import fr.acinq.eclair.payment.send.PaymentInitiator import fr.acinq.eclair.payment.{Bolt11Invoice, PaymentSent} import fr.acinq.eclair.router.Router import fr.acinq.eclair.wire.protocol.IPAddress -import fr.acinq.eclair.{BlockHeight, MilliSatoshi, MilliSatoshiLong, NodeParams, ShortChannelId, TestBitcoinCoreClient, TestDatabases, TestFeeEstimator} +import fr.acinq.eclair.{BlockHeight, MilliSatoshi, MilliSatoshiLong, NodeParams, RealShortChannelId, TestBitcoinCoreClient, TestDatabases, TestFeeEstimator} import org.scalatest.Assertions import org.scalatest.concurrent.Eventually.eventually @@ -97,7 +99,13 @@ object MinimalNodeFixture extends Assertions { ) } - def connect(node1: MinimalNodeFixture, node2: MinimalNodeFixture)(implicit system: ActorSystem): ConnectionResult.Connected = { + /** + * Connect node1 to node2, using a real [[PeerConnection]] and a fake transport layer. + * + * @param mutate12 a method to alter messages from node1 to node2 mid-flight for testing purposes + * @param mutate21 a method to alter messages from node2 to node1 mid-flight for testing purposes + */ + def connect(node1: MinimalNodeFixture, node2: MinimalNodeFixture, mutate12: Any => Any = identity, mutate21: Any => Any = identity)(implicit system: ActorSystem): ConnectionResult.Connected = { val sender = TestProbe("sender") val connection1 = TestProbe("connection-1")(node1.system) @@ -114,7 +122,7 @@ object MinimalNodeFixture extends Assertions { case _: TransportHandler.Listener => TestActor.KeepRunning case _: TransportHandler.ReadAck => TestActor.KeepRunning case _ => - peerConnection2.tell(msg, transport2.ref) + peerConnection2.tell(mutate12(msg), transport2.ref) TestActor.KeepRunning } } @@ -124,7 +132,7 @@ object MinimalNodeFixture extends Assertions { case _: TransportHandler.Listener => TestActor.KeepRunning case _: TransportHandler.ReadAck => TestActor.KeepRunning case _ => - peerConnection1.tell(msg, transport1.ref) + peerConnection1.tell(mutate21(msg), transport1.ref) TestActor.KeepRunning } } @@ -141,9 +149,9 @@ object MinimalNodeFixture extends Assertions { sender.expectMsgType[ConnectionResult.Connected] } - def openChannel(node1: MinimalNodeFixture, node2: MinimalNodeFixture, funding: Satoshi)(implicit system: ActorSystem): ChannelOpened = { + def openChannel(node1: MinimalNodeFixture, node2: MinimalNodeFixture, funding: Satoshi, channelType_opt: Option[SupportedChannelType] = None)(implicit system: ActorSystem): ChannelOpened = { val sender = TestProbe("sender") - sender.send(node1.switchboard, Peer.OpenChannel(node2.nodeParams.nodeId, funding, 0L msat, None, None, None, None)) + sender.send(node1.switchboard, Peer.OpenChannel(node2.nodeParams.nodeId, funding, 0L msat, channelType_opt, None, None, None)) sender.expectMsgType[ChannelOpened] } @@ -152,7 +160,7 @@ object MinimalNodeFixture extends Assertions { node.wallet.funded(fundingTxid) } - def confirmChannel(node1: MinimalNodeFixture, node2: MinimalNodeFixture, channelId: ByteVector32, blockHeight: BlockHeight, txIndex: Int)(implicit system: ActorSystem): ShortChannelId = { + def confirmChannel(node1: MinimalNodeFixture, node2: MinimalNodeFixture, channelId: ByteVector32, blockHeight: BlockHeight, txIndex: Int)(implicit system: ActorSystem): Option[RealScidStatus.Temporary] = { assert(getChannelState(node1, channelId) == WAIT_FOR_FUNDING_CONFIRMED) val data1Before = getChannelData(node1, channelId).asInstanceOf[DATA_WAIT_FOR_FUNDING_CONFIRMED] val fundingTx = data1Before.fundingTx.get @@ -170,13 +178,13 @@ object MinimalNodeFixture extends Assertions { val data1After = getChannelData(node1, channelId).asInstanceOf[DATA_NORMAL] val data2After = getChannelData(node2, channelId).asInstanceOf[DATA_NORMAL] - assert(data1After.shortChannelId == data2After.shortChannelId) - assert(!data1After.buried && !data2After.buried) - - data1After.shortChannelId + val realScid1 = data1After.shortIds.real.asInstanceOf[RealScidStatus.Temporary] + val realScid2 = data2After.shortIds.real.asInstanceOf[RealScidStatus.Temporary] + assert(realScid1 == realScid2) + Some(realScid1) } - def confirmChannelDeep(node1: MinimalNodeFixture, node2: MinimalNodeFixture, channelId: ByteVector32, blockHeight: BlockHeight, txIndex: Int)(implicit system: ActorSystem): ShortChannelId = { + def confirmChannelDeep(node1: MinimalNodeFixture, node2: MinimalNodeFixture, channelId: ByteVector32, blockHeight: BlockHeight, txIndex: Int)(implicit system: ActorSystem): RealScidStatus.Final = { assert(getChannelState(node1, channelId) == NORMAL) val data1Before = getChannelData(node1, channelId).asInstanceOf[DATA_NORMAL] val fundingTxid = data1Before.commitments.commitInput.outPoint.txid @@ -193,10 +201,10 @@ object MinimalNodeFixture extends Assertions { val data1After = getChannelData(node1, channelId).asInstanceOf[DATA_NORMAL] val data2After = getChannelData(node2, channelId).asInstanceOf[DATA_NORMAL] - assert(data1After.shortChannelId == data2After.shortChannelId) - assert(data1After.buried && data2After.buried) - - data1After.shortChannelId + val realScid1 = data1After.shortIds.real.asInstanceOf[RealScidStatus.Final] + val realScid2 = data2After.shortIds.real.asInstanceOf[RealScidStatus.Final] + assert(realScid1 == realScid2) + realScid1 } /** Utility method to make sure that the channel has processed all previous messages */ @@ -222,41 +230,54 @@ object MinimalNodeFixture extends Assertions { sender.expectMsgType[Router.Data] } - private def deterministicShortId(txId: ByteVector32): (BlockHeight, Int) = { + /** + * Computes a deterministic [[RealShortChannelId]] based on a txid. We need this so that watchers can verify + * transactions in a independent and stateless fashion, since there is no actual blockchain in those tests. + */ + def deterministicShortId(txId: ByteVector32): RealShortChannelId = { val blockHeight = txId.take(3).toInt(signed = false) val txIndex = txId.takeRight(2).toInt(signed = false) - (BlockHeight(blockHeight), txIndex) + val outputIndex = 0 // funding txs created by the dummy wallet used in tests only have one output + RealShortChannelId(BlockHeight(blockHeight), txIndex, outputIndex) } - /** An autopilot method for the watcher, that handled funding confirmation requests from the channel */ - def autoConfirmLocalChannels(fundingTxs: collection.concurrent.Map[ByteVector32, Transaction]): TestActor.AutoPilot = (_, msg) => msg match { + /** All known funding txs (we don't evaluate immediately because new ones could be created) */ + def knownFundingTxs(nodes: MinimalNodeFixture*): () => Iterable[Transaction] = () => nodes.map(_.wallet.funded.values).reduce(_ ++ _) + + /** + * An autopilot method for the watcher, that handled funding confirmation requests from the channel and channel + * validation requests from the router + */ + def watcherAutopilot(knownFundingTxs: () => Iterable[Transaction], deepConfirm: Boolean = true): TestActor.AutoPilot = (_, msg) => msg match { case watch: ZmqWatcher.WatchFundingConfirmed => - val (blockHeight, txIndex) = deterministicShortId(watch.txId) - watch.replyTo ! ZmqWatcher.WatchFundingConfirmedTriggered(blockHeight, txIndex, fundingTxs(watch.txId)) + val realScid = deterministicShortId(watch.txId) + val fundingTx = knownFundingTxs().find(_.txid == watch.txId) + .getOrElse(throw new RuntimeException(s"unknown fundingTxId=${watch.txId}, known=${knownFundingTxs().map(_.txid).mkString(",")}")) + watch.replyTo ! ZmqWatcher.WatchFundingConfirmedTriggered(realScid.blockHeight, txIndex(realScid), fundingTx) TestActor.KeepRunning - case watch: ZmqWatcher.WatchFundingDeeplyBuried => - val (blockHeight, txIndex) = deterministicShortId(watch.txId) - watch.replyTo ! ZmqWatcher.WatchFundingDeeplyBuriedTriggered(blockHeight, txIndex, fundingTxs(watch.txId)) + case watch: ZmqWatcher.WatchFundingDeeplyBuried if deepConfirm => + val realScid = deterministicShortId(watch.txId) + val fundingTx = knownFundingTxs().find(_.txid == watch.txId).get + watch.replyTo ! ZmqWatcher.WatchFundingDeeplyBuriedTriggered(realScid.blockHeight, txIndex(realScid), fundingTx) TestActor.KeepRunning - case _ => TestActor.KeepRunning - } - - /** An autopilot method for the watcher, that handles channel validation requests from the router */ - def autoValidatePublicChannels(fundingTxs: Map[ShortChannelId, Transaction]): TestActor.AutoPilot = (_, msg: Any) => msg match { case vr: ZmqWatcher.ValidateRequest => - val res = Try(fundingTxs(vr.ann.shortChannelId), ZmqWatcher.UtxoStatus.Unspent).toEither + val res = Try { + val fundingTx = knownFundingTxs().find(tx => deterministicShortId(tx.txid) == vr.ann.shortChannelId) + .getOrElse(throw new RuntimeException(s"unknown realScid=${vr.ann.shortChannelId}, known=${knownFundingTxs().map(tx => deterministicShortId(tx.txid)).mkString(",")}")) + (fundingTx, ZmqWatcher.UtxoStatus.Unspent) + }.toEither vr.replyTo ! ZmqWatcher.ValidateResult(vr.ann, res) TestActor.KeepRunning case _ => TestActor.KeepRunning } - def sendPayment(node1: MinimalNodeFixture, node2: MinimalNodeFixture, amount: MilliSatoshi)(implicit system: ActorSystem): PaymentSent = { + def sendPayment(node1: MinimalNodeFixture, node2: MinimalNodeFixture, amount: MilliSatoshi, hints: Seq[Seq[ExtraHop]] = Seq.empty)(implicit system: ActorSystem): PaymentSent = { val sender = TestProbe("sender") sender.send(node2.paymentHandler, MultiPartHandler.ReceivePayment(Some(amount), Left("test payment"))) val invoice = sender.expectMsgType[Bolt11Invoice] val routeParams = node1.nodeParams.routerConf.pathFindingExperimentConf.experiments.values.head.getDefaultRouteParams - sender.send(node1.paymentInitiator, PaymentInitiator.SendPaymentToNode(amount, invoice, maxAttempts = 1, routeParams = routeParams, blockUntilComplete = true)) + sender.send(node1.paymentInitiator, PaymentInitiator.SendPaymentToNode(amount, invoice, maxAttempts = 1, routeParams = routeParams, assistedRoutes = hints, blockUntilComplete = true)) sender.expectMsgType[PaymentSent] } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerConnectionSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerConnectionSpec.scala index fffe22d180..5985109de1 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerConnectionSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerConnectionSpec.scala @@ -23,6 +23,7 @@ import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32} import fr.acinq.eclair.FeatureSupport.{Mandatory, Optional} import fr.acinq.eclair.Features.{BasicMultiPartPayment, ChannelRangeQueries, PaymentSecret, VariableLengthOnion} import fr.acinq.eclair.TestConstants._ +import fr.acinq.eclair.RealShortChannelId import fr.acinq.eclair._ import fr.acinq.eclair.crypto.TransportHandler import fr.acinq.eclair.message.OnionMessages.{Recipient, buildMessage} @@ -333,7 +334,7 @@ class PeerConnectionSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wi val query = QueryShortChannelIds( Alice.nodeParams.chainHash, - EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(ShortChannelId(42000))), + EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(RealShortChannelId(42000))), TlvStream.empty) // make sure that routing messages go through @@ -423,7 +424,7 @@ class PeerConnectionSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wi val (_, message) = buildMessage(randomKey(), randomKey(), Nil, Recipient(remoteNodeId, None), Nil) probe.send(peerConnection, message) assert(peerConnection.stateName == PeerConnection.CONNECTED) - probe.send(peerConnection, FundingLocked(ByteVector32(hex"0000000000000000000000000000000000000000000000000000000000000000"), randomKey().publicKey)) + probe.send(peerConnection, ChannelReady(ByteVector32(hex"0000000000000000000000000000000000000000000000000000000000000000"), randomKey().publicKey)) peerConnection.stateData match { case d: PeerConnection.ConnectedData => assert(d.isPersistent) case _ => fail() diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala index e557c5b3e0..77d0623c0e 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala @@ -341,7 +341,7 @@ class PeerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Paralle } // They only support anchor outputs with zero fee htlc txs and we don't. { - val open = createOpenChannelMessage(TlvStream[OpenChannelTlv](ChannelTlv.ChannelTypeTlv(ChannelTypes.AnchorOutputsZeroFeeHtlcTx))) + val open = createOpenChannelMessage(TlvStream[OpenChannelTlv](ChannelTlv.ChannelTypeTlv(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)))) peerConnection.send(peer, open) peerConnection.expectMsg(Error(open.temporaryChannelId, "invalid channel_type=anchor_outputs_zero_fee_htlc_tx, expected channel_type=standard")) } @@ -432,7 +432,7 @@ class PeerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Paralle feeEstimator.setFeerate(FeeratesPerKw.single(TestConstants.anchorOutputsFeeratePerKw * 2).copy(mempoolMinFee = FeeratePerKw(250 sat))) probe.send(peer, Peer.OpenChannel(remoteNodeId, 15000 sat, 0 msat, None, None, None, None)) val init = channel.expectMsgType[INPUT_INIT_FUNDER] - assert(init.channelType == ChannelTypes.AnchorOutputsZeroFeeHtlcTx) + assert(init.channelType == ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)) assert(init.fundingAmount == 15000.sat) assert(init.commitTxFeerate == TestConstants.anchorOutputsFeeratePerKw) assert(init.fundingTxFeerate == feeEstimator.getFeeratePerKw(nodeParams.onChainFeeConf.feeTargets.fundingBlockTarget)) @@ -450,6 +450,14 @@ class PeerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Paralle assert(init.localParams.defaultFinalScriptPubKey == Script.write(Script.pay2wpkh(init.localParams.walletStaticPaymentBasepoint.get))) } + test("do not allow option_scid_alias with public channel") { f => + import f._ + + intercept[IllegalArgumentException] { + Peer.OpenChannel(remoteNodeId, 24000 sat, 0 msat, Some(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = true, zeroConf = true)), None, Some(ChannelFlags(announceChannel = true)), None) + } + } + test("set origin_opt when spawning a channel") { f => import f._ diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/json/JsonSerializersSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/json/JsonSerializersSpec.scala index 1b3708bd4c..0249b9a53f 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/json/JsonSerializersSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/json/JsonSerializersSpec.scala @@ -202,7 +202,7 @@ class JsonSerializersSpec extends AnyFunSuite with Matchers { toLocal = Map(ByteVector32(hex"7e3b012534afe0bb8d1f2f09c724b1a10a813ce704e5b9c217ccfdffffff0247") -> Btc(0.1))) )) ) - JsonSerializers.serialization.write(gb)(JsonSerializers.formats) shouldBe """{"total":1.0,"onChain":{"confirmed":0.4,"unconfirmed":0.05},"offChain":{"waitForFundingConfirmed":0.0,"waitForFundingLocked":0.0,"normal":{"toLocal":0.2,"htlcs":0.05},"shutdown":{"toLocal":0.0,"htlcs":0.0},"negotiating":0.0,"closing":{"localCloseBalance":{"toLocal":{"4d176ad844c363bed59edf81962b008faa6194c3b3757ffcd26ba60f95716db2":0.1},"htlcs":{"94b70cec5a98d67d17c6e3de5c7697f8a6cab4f698df91e633ce35efa3574d71":0.03,"a844edd41ce8503864f3c95d89d850b177a09d7d35e950a7d27c14abb63adb13":0.06},"htlcsUnpublished":0.01},"remoteCloseBalance":{"toLocal":{},"htlcs":{},"htlcsUnpublished":0.0},"mutualCloseBalance":{"toLocal":{"7e3b012534afe0bb8d1f2f09c724b1a10a813ce704e5b9c217ccfdffffff0247":0.1}},"unknownCloseBalance":{"toLocal":0.0,"htlcs":0.0}},"waitForPublishFutureCommitment":0.0}}""" + JsonSerializers.serialization.write(gb)(JsonSerializers.formats) shouldBe """{"total":1.0,"onChain":{"confirmed":0.4,"unconfirmed":0.05},"offChain":{"waitForFundingConfirmed":0.0,"waitForChannelReady":0.0,"normal":{"toLocal":0.2,"htlcs":0.05},"shutdown":{"toLocal":0.0,"htlcs":0.0},"negotiating":0.0,"closing":{"localCloseBalance":{"toLocal":{"4d176ad844c363bed59edf81962b008faa6194c3b3757ffcd26ba60f95716db2":0.1},"htlcs":{"94b70cec5a98d67d17c6e3de5c7697f8a6cab4f698df91e633ce35efa3574d71":0.03,"a844edd41ce8503864f3c95d89d850b177a09d7d35e950a7d27c14abb63adb13":0.06},"htlcsUnpublished":0.01},"remoteCloseBalance":{"toLocal":{},"htlcs":{},"htlcsUnpublished":0.0},"mutualCloseBalance":{"toLocal":{"7e3b012534afe0bb8d1f2f09c724b1a10a813ce704e5b9c217ccfdffffff0247":0.1}},"unknownCloseBalance":{"toLocal":0.0,"htlcs":0.0}},"waitForPublishFutureCommitment":0.0}}""" } test("type hints") { @@ -306,6 +306,20 @@ class JsonSerializersSpec extends AnyFunSuite with Matchers { JsonSerializers.serialization.write(map)(JsonSerializers.formats) shouldBe s"""{"e2fc57221cfb1942224082174022f3f70a32005aa209956f9c94c6903f7669ff":"ok","8e3ec6e16436b7dc61b86340192603d05f16d4f8e06c8aaa02fbe2ad63209af3":"cannot execute command=CMD_UPDATE_RELAY_FEE in state=CLOSING","74ca7a86e52d597aa2248cd2ff3b24428ede71345262be7fb31afddfe18dc0d8":"channel 74ca7a86e52d597aa2248cd2ff3b24428ede71345262be7fb31afddfe18dc0d8 not found"}""" } + test("serialize short ids") { + val testCases = Map( + ShortIds(real = RealScidStatus.Unknown, localAlias = Alias(0x4455), remoteAlias_opt = Some(Alias(0x88888888L))) -> + """{"real":{"status":"unknown"},"localAlias":"0x4455","remoteAlias":"0x88888888"}""", + ShortIds(real = RealScidStatus.Temporary(RealShortChannelId(BlockHeight(500000), 42, 1)), localAlias = Alias(0x4455), remoteAlias_opt = None) -> + """{"real":{"status":"temporary","realScid":"500000x42x1"},"localAlias":"0x4455"}""", + ShortIds(real = RealScidStatus.Final(RealShortChannelId(BlockHeight(500000), 42, 1)), localAlias = Alias(0x4455), remoteAlias_opt = None) -> + """{"real":{"status":"final","realScid":"500000x42x1"},"localAlias":"0x4455"}""", + ) + for ((obj, json) <- testCases) { + JsonSerializers.serialization.write(obj)(JsonSerializers.formats) shouldBe json + } + } + /** utility method that strips line breaks in the expected json */ def assertJsonEquals(actual: String, expected: String) = { val cleanedExpected = expected diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala index c7c010a55c..13a45c188b 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala @@ -714,7 +714,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { // | | and b -> h has fees = 0 // +---(5)--> g ---(6)--> h // and e --(4)--> f (we are a) - val channelId_bh = ShortChannelId(BlockHeight(420000), 100, 0) + val channelId_bh = RealShortChannelId(BlockHeight(420000), 100, 0) val chan_bh = channelAnnouncement(channelId_bh, priv_b, priv_h, priv_funding_b, priv_funding_h) val channelUpdate_bh = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_b, h, channelId_bh, CltvExpiryDelta(9), htlcMinimumMsat = 0 msat, feeBaseMsat = 0 msat, feeProportionalMillionths = 0, htlcMaximumMsat = 500000000 msat) val channelUpdate_hb = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_h, b, channelId_bh, CltvExpiryDelta(9), htlcMinimumMsat = 0 msat, feeBaseMsat = 10 msat, feeProportionalMillionths = 8, htlcMaximumMsat = 500000000 msat) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentPacketSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentPacketSpec.scala index 491f200789..1a3edce695 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentPacketSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentPacketSpec.scala @@ -368,12 +368,12 @@ object PaymentPacketSpec { Sphinx.create(sessionKey, packetPayloadLength, nodes, payloadsBin, Some(associatedData)).get.packet } - def makeCommitments(channelId: ByteVector32, testAvailableBalanceForSend: MilliSatoshi = 50000000 msat, testAvailableBalanceForReceive: MilliSatoshi = 50000000 msat, testCapacity: Satoshi = 100000 sat): Commitments = { + def makeCommitments(channelId: ByteVector32, testAvailableBalanceForSend: MilliSatoshi = 50000000 msat, testAvailableBalanceForReceive: MilliSatoshi = 50000000 msat, testCapacity: Satoshi = 100000 sat, channelFeatures: ChannelFeatures = ChannelFeatures()): Commitments = { val params = LocalParams(null, null, null, null, None, null, null, 0, isInitiator = true, null, None, null) val remoteParams = RemoteParams(randomKey().publicKey, null, null, None, null, null, maxAcceptedHtlcs = 0, null, null, null, null, null, null, None) val commitInput = InputInfo(OutPoint(randomBytes32(), 1), TxOut(testCapacity, Nil), Nil) val channelFlags = ChannelFlags.Private - new Commitments(channelId, ChannelConfig.standard, ChannelFeatures(), params, remoteParams, channelFlags, null, null, null, null, 0, 0, Map.empty, null, commitInput, null) { + new Commitments(channelId, ChannelConfig.standard, channelFeatures, params, remoteParams, channelFlags, null, null, null, null, 0, 0, Map.empty, null, commitInput, null) { override lazy val availableBalanceForSend: MilliSatoshi = testAvailableBalanceForSend.max(0 msat) override lazy val availableBalanceForReceive: MilliSatoshi = testAvailableBalanceForReceive.max(0 msat) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/ChannelRelayerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/ChannelRelayerSpec.scala index b3096820c1..b123d35c51 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/ChannelRelayerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/ChannelRelayerSpec.scala @@ -20,10 +20,13 @@ import akka.actor.testkit.typed.scaladsl.{ScalaTestWithActorTestKit, TestProbe} import akka.actor.typed import akka.actor.typed.eventstream.EventStream import akka.actor.typed.scaladsl.adapter.TypedActorRefOps +import com.softwaremill.quicklens.ModifyPimp import com.typesafe.config.ConfigFactory import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, ByteVector64, Crypto, Satoshi, SatoshiLong} +import fr.acinq.eclair.Features.ScidAlias import fr.acinq.eclair.TestConstants.emptyOnionPacket +import fr.acinq.eclair.RealShortChannelId import fr.acinq.eclair.blockchain.fee.FeeratePerKw import fr.acinq.eclair.channel._ import fr.acinq.eclair.payment.IncomingPaymentPacket.ChannelRelayPacket @@ -32,7 +35,7 @@ import fr.acinq.eclair.payment.{ChannelPaymentRelayed, IncomingPaymentPacket, Pa import fr.acinq.eclair.router.Announcements import fr.acinq.eclair.wire.protocol.PaymentOnion.{ChannelRelayPayload, ChannelRelayTlvPayload, RelayLegacyPayload} import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{CltvExpiry, NodeParams, TestConstants, randomBytes32, _} +import fr.acinq.eclair.{CltvExpiry, NodeParams, RealShortChannelId, TestConstants, randomBytes32, _} import org.scalatest.Outcome import org.scalatest.funsuite.FixtureAnyFunSuiteLike import scodec.bits.HexStringSyntax @@ -57,13 +60,14 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a def expectFwdFail(register: TestProbe[Any], channelId: ByteVector32, cmd: channel.Command): Register.Forward[channel.Command] = { val fwd = register.expectMessageType[Register.Forward[channel.Command]] - assert(fwd.channelId == channelId) assert(fwd.message == cmd) + assert(fwd.channelId == channelId) fwd } def expectFwdAdd(register: TestProbe[Any], channelId: ByteVector32, outAmount: MilliSatoshi, outExpiry: CltvExpiry): Register.Forward[CMD_ADD_HTLC] = { val fwd = register.expectMessageType[Register.Forward[CMD_ADD_HTLC]] + assert(fwd.message.isInstanceOf[CMD_ADD_HTLC]) // the line above doesn't check the type due to type erasure assert(fwd.channelId == channelId) assert(fwd.message.amount == outAmount) assert(fwd.message.cltvExpiry == outExpiry) @@ -73,97 +77,161 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a fwd } - test("relay htlc-add") { f => + def basicRelaytest(f: FixtureParam)(relayPayloadScid: ShortChannelId, lcu: LocalChannelUpdate, success: Boolean): Unit = { import f._ - val payload = RelayLegacyPayload(shortId1, outgoingAmount, outgoingExpiry) - val r = createValidIncomingPacket(1100000 msat, CltvExpiry(400100), payload) - val u = createLocalUpdate(shortId1) + val payload = RelayLegacyPayload(relayPayloadScid, outgoingAmount, outgoingExpiry) + val r = createValidIncomingPacket(payload) - channelRelayer ! WrappedLocalChannelUpdate(u) + channelRelayer ! WrappedLocalChannelUpdate(lcu) channelRelayer ! Relay(r) - expectFwdAdd(register, channelIds(shortId1), outgoingAmount, outgoingExpiry) + if (success) { + expectFwdAdd(register, lcu.channelId, outgoingAmount, outgoingExpiry) + } else { + expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, Right(UnknownNextPeer), commit = true)) + } + } + + test("relay with real scid (channel update uses real scid)") { f => + basicRelaytest(f)(relayPayloadScid = realScid1, lcu = createLocalUpdate(channelId1), success = true) + } + + test("relay with real scid (channel update uses remote alias)") { f => + basicRelaytest(f)(relayPayloadScid = realScid1, lcu = createLocalUpdate(channelId1, channelUpdateScid_opt = Some(remoteAlias1)), success = true) + } + + test("relay with local alias (channel update uses real scid)") { f => + basicRelaytest(f)(relayPayloadScid = localAlias1, lcu = createLocalUpdate(channelId1), success = true) + } + + test("relay with local alias (channel update uses remote alias)") { f => + // we use a random value to simulate a remote alias + basicRelaytest(f)(relayPayloadScid = localAlias1, lcu = createLocalUpdate(channelId1, channelUpdateScid_opt = Some(remoteAlias1)), success = true) + } + + test("fail to relay with real scid when option_scid_alias is enabled (channel update uses real scid)") { f => + basicRelaytest(f)(relayPayloadScid = realScid1, lcu = createLocalUpdate(channelId1, optionScidAlias = true), success = false) + } + + test("fail to relay with real scid when option_scid_alias is enabled (channel update uses remote alias)") { f => + basicRelaytest(f)(relayPayloadScid = realScid1, lcu = createLocalUpdate(channelId1, optionScidAlias = true, channelUpdateScid_opt = Some(remoteAlias1)), success = false) } - test("relay an htlc-add with onion tlv payload") { f => + test("relay with local alias when option_scid_alias is enabled (channel update uses real scid)") { f => + basicRelaytest(f)(relayPayloadScid = localAlias1, lcu = createLocalUpdate(channelId1, optionScidAlias = true), success = true) + } + + test("relay with local alias when option_scid_alias is enabled (channel update uses remote alias)") { f => + // we use a random value to simulate a remote alias + basicRelaytest(f)(relayPayloadScid = localAlias1, lcu = createLocalUpdate(channelId1, optionScidAlias = true, channelUpdateScid_opt = Some(remoteAlias1)), success = true) + } + + test("relay with new real scid after reorg") { f => + import f._ + + // initial channel update + val lcu1 = createLocalUpdate(channelId1) + val payload1 = RelayLegacyPayload(realScid1, outgoingAmount, outgoingExpiry) + val r1 = createValidIncomingPacket(payload1) + channelRelayer ! WrappedLocalChannelUpdate(lcu1) + channelRelayer ! Relay(r1) + expectFwdAdd(register, lcu1.channelId, outgoingAmount, outgoingExpiry) + + // reorg happens + val realScid1AfterReorg = RealShortChannelId(111112) + val lcu2 = createLocalUpdate(channelId1).modify(_.shortIds.real).setTo(RealScidStatus.Final(realScid1AfterReorg)) + val payload2 = RelayLegacyPayload(realScid1AfterReorg, outgoingAmount, outgoingExpiry) + val r2 = createValidIncomingPacket(payload2) + channelRelayer ! WrappedLocalChannelUpdate(lcu2) + + // both old and new real scids work + channelRelayer ! Relay(r1) + expectFwdAdd(register, lcu1.channelId, outgoingAmount, outgoingExpiry) + // new real scid works + channelRelayer ! Relay(r2) + expectFwdAdd(register, lcu2.channelId, outgoingAmount, outgoingExpiry) + } + + + test("relay with onion tlv payload") { f => import f._ import fr.acinq.eclair.wire.protocol.OnionPaymentPayloadTlv._ - val payload = ChannelRelayTlvPayload(TlvStream[OnionPaymentPayloadTlv](AmountToForward(outgoingAmount), OutgoingCltv(outgoingExpiry), OutgoingChannelId(shortId1))) - val r = createValidIncomingPacket(1100000 msat, CltvExpiry(400100), payload) - val u = createLocalUpdate(shortId1) + val payload = ChannelRelayTlvPayload(TlvStream[OnionPaymentPayloadTlv](AmountToForward(outgoingAmount), OutgoingCltv(outgoingExpiry), OutgoingChannelId(realScid1))) + val r = createValidIncomingPacket(payload) + val u = createLocalUpdate(channelId1) channelRelayer ! WrappedLocalChannelUpdate(u) channelRelayer ! Relay(r) - expectFwdAdd(register, channelIds(shortId1), outgoingAmount, outgoingExpiry) + expectFwdAdd(register, channelIds(realScid1), outgoingAmount, outgoingExpiry) } - test("relay an htlc-add with retries") { f => + test("relay with retries") { f => import f._ - val payload = RelayLegacyPayload(shortId1, outgoingAmount, outgoingExpiry) - val r = createValidIncomingPacket(1100000 msat, CltvExpiry(400100), payload) + val payload = RelayLegacyPayload(realScid1, outgoingAmount, outgoingExpiry) + val r = createValidIncomingPacket(payload) // we tell the relayer about the first channel - val u1 = createLocalUpdate(shortId1) + val u1 = createLocalUpdate(channelId1) channelRelayer ! WrappedLocalChannelUpdate(u1) // this is another channel, with less balance (it will be preferred) - val u2 = createLocalUpdate(shortId2, 8000000 msat) + val u2 = createLocalUpdate(channelId2, balance = 8000000 msat) channelRelayer ! WrappedLocalChannelUpdate(u2) channelRelayer ! Relay(r) // first try - val fwd1 = expectFwdAdd(register, channelIds(shortId2), outgoingAmount, outgoingExpiry) + val fwd1 = expectFwdAdd(register, channelIds(realScId2), outgoingAmount, outgoingExpiry) // channel returns an error - fwd1.message.replyTo ! RES_ADD_FAILED(fwd1.message, HtlcValueTooHighInFlight(channelIds(shortId2), UInt64(1000000000L), 1516977616L msat), Some(u2.channelUpdate)) + fwd1.message.replyTo ! RES_ADD_FAILED(fwd1.message, HtlcValueTooHighInFlight(channelIds(realScId2), UInt64(1000000000L), 1516977616L msat), Some(u2.channelUpdate)) // second try - val fwd2 = expectFwdAdd(register, channelIds(shortId1), outgoingAmount, outgoingExpiry) + val fwd2 = expectFwdAdd(register, channelIds(realScid1), outgoingAmount, outgoingExpiry) // failure again - fwd1.message.replyTo ! RES_ADD_FAILED(fwd2.message, HtlcValueTooHighInFlight(channelIds(shortId1), UInt64(1000000000L), 1516977616L msat), Some(u1.channelUpdate)) + fwd1.message.replyTo ! RES_ADD_FAILED(fwd2.message, HtlcValueTooHighInFlight(channelIds(realScid1), UInt64(1000000000L), 1516977616L msat), Some(u1.channelUpdate)) // the relayer should give up expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, Right(TemporaryNodeFailure), commit = true)) } - test("fail to relay an htlc-add when we have no channel_update for the next channel") { f => + test("fail to relay when we have no channel_update for the next channel") { f => import f._ - val payload = RelayLegacyPayload(shortId1, outgoingAmount, outgoingExpiry) - val r = createValidIncomingPacket(1100000 msat, CltvExpiry(400100), payload) + val payload = RelayLegacyPayload(realScid1, outgoingAmount, outgoingExpiry) + val r = createValidIncomingPacket(payload) channelRelayer ! Relay(r) expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, Right(UnknownNextPeer), commit = true)) } - test("fail to relay an htlc-add when register returns an error") { f => + test("fail to relay when register returns an error") { f => import f._ - val payload = RelayLegacyPayload(shortId1, outgoingAmount, outgoingExpiry) - val r = createValidIncomingPacket(1100000 msat, CltvExpiry(400100), payload) - val u = createLocalUpdate(shortId1) + val payload = RelayLegacyPayload(realScid1, outgoingAmount, outgoingExpiry) + val r = createValidIncomingPacket(payload) + val u = createLocalUpdate(channelId1) channelRelayer ! WrappedLocalChannelUpdate(u) channelRelayer ! Relay(r) - val fwd = expectFwdAdd(register, channelIds(shortId1), outgoingAmount, outgoingExpiry) + val fwd = expectFwdAdd(register, channelIds(realScid1), outgoingAmount, outgoingExpiry) fwd.replyTo ! Register.ForwardFailure(fwd) expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, Right(UnknownNextPeer), commit = true)) } - test("fail to relay an htlc-add when the channel is advertised as unusable (down)") { f => + test("fail to relay when the channel is advertised as unusable (down)") { f => import f._ - val payload = RelayLegacyPayload(shortId1, outgoingAmount, outgoingExpiry) - val r = createValidIncomingPacket(1100000 msat, CltvExpiry(400100), payload) - val u = createLocalUpdate(shortId1) - val d = LocalChannelDown(null, channelId = channelIds(shortId1), shortId1, outgoingNodeId) + val payload = RelayLegacyPayload(realScid1, outgoingAmount, outgoingExpiry) + val r = createValidIncomingPacket(payload) + val u = createLocalUpdate(channelId1) + val d = LocalChannelDown(null, channelId1, createShortIds(channelId1), outgoingNodeId) channelRelayer ! WrappedLocalChannelUpdate(u) channelRelayer ! WrappedLocalChannelDown(d) @@ -172,12 +240,12 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, Right(UnknownNextPeer), commit = true)) } - test("fail to relay an htlc-add (channel disabled)") { f => + test("fail to relay when channel is disabled") { f => import f._ - val payload = RelayLegacyPayload(shortId1, outgoingAmount, outgoingExpiry) - val r = createValidIncomingPacket(1100000 msat, CltvExpiry(400100), payload) - val u = createLocalUpdate(shortId1, enabled = false) + val payload = RelayLegacyPayload(realScid1, outgoingAmount, outgoingExpiry) + val r = createValidIncomingPacket(payload) + val u = createLocalUpdate(channelId1, enabled = false) channelRelayer ! WrappedLocalChannelUpdate(u) channelRelayer ! Relay(r) @@ -185,12 +253,12 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, Right(ChannelDisabled(u.channelUpdate.messageFlags, u.channelUpdate.channelFlags, u.channelUpdate)), commit = true)) } - test("fail to relay an htlc-add (amount below minimum)") { f => + test("fail to relay when amount is below minimum") { f => import f._ - val payload = RelayLegacyPayload(shortId1, outgoingAmount, outgoingExpiry) - val r = createValidIncomingPacket(1100000 msat, CltvExpiry(400100), payload) - val u = createLocalUpdate(shortId1, htlcMinimum = outgoingAmount + 1.msat) + val payload = RelayLegacyPayload(realScid1, outgoingAmount, outgoingExpiry) + val r = createValidIncomingPacket(payload) + val u = createLocalUpdate(channelId1, htlcMinimum = outgoingAmount + 1.msat) channelRelayer ! WrappedLocalChannelUpdate(u) channelRelayer ! Relay(r) @@ -198,25 +266,25 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, Right(AmountBelowMinimum(outgoingAmount, u.channelUpdate)), commit = true)) } - test("relay an htlc-add (expiry larger than our requirements)") { f => + test("relay when expiry larger than our requirements") { f => import f._ - val payload = RelayLegacyPayload(shortId1, outgoingAmount, outgoingExpiry) - val u = createLocalUpdate(shortId1) - val r = createValidIncomingPacket(1100000 msat, outgoingExpiry + u.channelUpdate.cltvExpiryDelta + CltvExpiryDelta(1), payload) + val payload = RelayLegacyPayload(realScid1, outgoingAmount, outgoingExpiry) + val u = createLocalUpdate(channelId1) + val r = createValidIncomingPacket(payload, expiryIn = outgoingExpiry + u.channelUpdate.cltvExpiryDelta + CltvExpiryDelta(1)) channelRelayer ! WrappedLocalChannelUpdate(u) channelRelayer ! Relay(r) - expectFwdAdd(register, channelIds(shortId1), payload.amountToForward, payload.outgoingCltv).message + expectFwdAdd(register, channelIds(realScid1), payload.amountToForward, payload.outgoingCltv).message } - test("fail to relay an htlc-add (expiry too small)") { f => + test("fail to relay when expiry is too small") { f => import f._ - val payload = RelayLegacyPayload(shortId1, outgoingAmount, outgoingExpiry) - val u = createLocalUpdate(shortId1) - val r = createValidIncomingPacket(1100000 msat, outgoingExpiry + u.channelUpdate.cltvExpiryDelta - CltvExpiryDelta(1), payload) + val payload = RelayLegacyPayload(realScid1, outgoingAmount, outgoingExpiry) + val u = createLocalUpdate(channelId1) + val r = createValidIncomingPacket(payload, expiryIn = outgoingExpiry + u.channelUpdate.cltvExpiryDelta - CltvExpiryDelta(1)) channelRelayer ! WrappedLocalChannelUpdate(u) channelRelayer ! Relay(r) @@ -224,12 +292,12 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, Right(IncorrectCltvExpiry(payload.outgoingCltv, u.channelUpdate)), commit = true)) } - test("fail to relay an htlc-add (fee insufficient)") { f => + test("fail to relay when fee is insufficient") { f => import f._ - val payload = RelayLegacyPayload(shortId1, outgoingAmount, outgoingExpiry) - val r = createValidIncomingPacket(outgoingAmount + 1.msat, CltvExpiry(400100), payload) - val u = createLocalUpdate(shortId1) + val payload = RelayLegacyPayload(realScid1, outgoingAmount, outgoingExpiry) + val r = createValidIncomingPacket(payload, amountIn = outgoingAmount + 1.msat) + val u = createLocalUpdate(channelId1) channelRelayer ! WrappedLocalChannelUpdate(u) channelRelayer ! Relay(r) @@ -237,28 +305,28 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, Right(FeeInsufficient(r.add.amountMsat, u.channelUpdate)), commit = true)) } - test("relay an htlc-add that would fail (fee insufficient) with a recent channel update but succeed with the previous update") { f => + test("relay that would fail (fee insufficient) with a recent channel update but succeed with the previous update") { f => import f._ - val payload = RelayLegacyPayload(shortId1, outgoingAmount, outgoingExpiry) - val r = createValidIncomingPacket(outgoingAmount + 1.msat, CltvExpiry(400100), payload) - val u1 = createLocalUpdate(shortId1, timestamp = TimestampSecond.now(), feeBaseMsat = 1 msat, feeProportionalMillionths = 0) + val payload = RelayLegacyPayload(realScid1, outgoingAmount, outgoingExpiry) + val r = createValidIncomingPacket(payload, amountIn = outgoingAmount + 1.msat) + val u1 = createLocalUpdate(channelId1, timestamp = TimestampSecond.now(), feeBaseMsat = 1 msat, feeProportionalMillionths = 0) channelRelayer ! WrappedLocalChannelUpdate(u1) channelRelayer ! Relay(r) // relay succeeds with current channel update (u1) with lower fees - expectFwdAdd(register, channelIds(shortId1), outgoingAmount, outgoingExpiry) + expectFwdAdd(register, channelIds(realScid1), outgoingAmount, outgoingExpiry) - val u2 = createLocalUpdate(shortId1, timestamp = TimestampSecond.now() - 530) + val u2 = createLocalUpdate(channelId1, timestamp = TimestampSecond.now() - 530) channelRelayer ! WrappedLocalChannelUpdate(u2) channelRelayer ! Relay(r) // relay succeeds because the current update (u2) with higher fees occurred less than 10 minutes ago - expectFwdAdd(register, channelIds(shortId1), outgoingAmount, outgoingExpiry) + expectFwdAdd(register, channelIds(realScid1), outgoingAmount, outgoingExpiry) - val u3 = createLocalUpdate(shortId1, timestamp = TimestampSecond.now() - 601) + val u3 = createLocalUpdate(channelId1, timestamp = TimestampSecond.now() - 601) channelRelayer ! WrappedLocalChannelUpdate(u1) channelRelayer ! WrappedLocalChannelUpdate(u3) @@ -268,14 +336,14 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, Right(FeeInsufficient(r.add.amountMsat, u3.channelUpdate)), commit = true)) } - test("fail to relay an htlc-add (local error)") { f => + test("fail to relay when there is a local error") { f => import f._ - val channelId1 = channelIds(shortId1) - val payload = RelayLegacyPayload(shortId1, outgoingAmount, outgoingExpiry) - val r = createValidIncomingPacket(1100000 msat, CltvExpiry(400100), payload) - val u = createLocalUpdate(shortId1) - val u_disabled = createLocalUpdate(shortId1, enabled = false) + val channelId1 = channelIds(realScid1) + val payload = RelayLegacyPayload(realScid1, outgoingAmount, outgoingExpiry) + val r = createValidIncomingPacket(payload) + val u = createLocalUpdate(channelId1) + val u_disabled = createLocalUpdate(channelId1, enabled = false) case class TestCase(exc: ChannelException, update: ChannelUpdate, failure: FailureMessage) @@ -290,7 +358,7 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a testCases.foreach { testCase => channelRelayer ! WrappedLocalChannelUpdate(u) channelRelayer ! Relay(r) - val fwd = expectFwdAdd(register, channelIds(shortId1), outgoingAmount, outgoingExpiry) + val fwd = expectFwdAdd(register, channelIds(realScid1), outgoingAmount, outgoingExpiry) fwd.message.replyTo ! RES_ADD_FAILED(fwd.message, testCase.exc, Some(testCase.update)) expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, Right(testCase.failure), commit = true)) } @@ -300,29 +368,30 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a import f._ /** This is just a simplified helper function with random values for fields we are not using here */ - def dummyLocalUpdate(shortChannelId: ShortChannelId, remoteNodeId: PublicKey, availableBalanceForSend: MilliSatoshi, capacity: Satoshi) = { + def dummyLocalUpdate(shortChannelId: RealShortChannelId, remoteNodeId: PublicKey, availableBalanceForSend: MilliSatoshi, capacity: Satoshi) = { val channelId = randomBytes32() val update = Announcements.makeChannelUpdate(Block.RegtestGenesisBlock.hash, randomKey(), remoteNodeId, shortChannelId, CltvExpiryDelta(10), 100 msat, 1000 msat, 100, capacity.toMilliSatoshi) val commitments = PaymentPacketSpec.makeCommitments(channelId, availableBalanceForSend, testCapacity = capacity) - LocalChannelUpdate(null, channelId, shortChannelId, remoteNodeId, None, update, commitments) + val shortIds = ShortIds(real = RealScidStatus.Final(shortChannelId), localAlias = ShortChannelId.generateLocalAlias(), remoteAlias_opt = None) + LocalChannelUpdate(null, channelId, shortIds, remoteNodeId, None, update, commitments) } val (a, b) = (randomKey().publicKey, randomKey().publicKey) val channelUpdates = Map( - ShortChannelId(11111) -> dummyLocalUpdate(ShortChannelId(11111), a, 100000000 msat, 200000 sat), - ShortChannelId(12345) -> dummyLocalUpdate(ShortChannelId(12345), a, 10000000 msat, 200000 sat), - ShortChannelId(22222) -> dummyLocalUpdate(ShortChannelId(22222), a, 10000000 msat, 100000 sat), - ShortChannelId(22223) -> dummyLocalUpdate(ShortChannelId(22223), a, 9000000 msat, 50000 sat), - ShortChannelId(33333) -> dummyLocalUpdate(ShortChannelId(33333), a, 100000 msat, 50000 sat), - ShortChannelId(44444) -> dummyLocalUpdate(ShortChannelId(44444), b, 1000000 msat, 10000 sat), + ShortChannelId(11111) -> dummyLocalUpdate(RealShortChannelId(11111), a, 100000000 msat, 200000 sat), + ShortChannelId(12345) -> dummyLocalUpdate(RealShortChannelId(12345), a, 10000000 msat, 200000 sat), + ShortChannelId(22222) -> dummyLocalUpdate(RealShortChannelId(22222), a, 10000000 msat, 100000 sat), + ShortChannelId(22223) -> dummyLocalUpdate(RealShortChannelId(22223), a, 9000000 msat, 50000 sat), + ShortChannelId(33333) -> dummyLocalUpdate(RealShortChannelId(33333), a, 100000 msat, 50000 sat), + ShortChannelId(44444) -> dummyLocalUpdate(RealShortChannelId(44444), b, 1000000 msat, 10000 sat), ) channelUpdates.values.foreach(u => channelRelayer ! WrappedLocalChannelUpdate(u)) { val payload = RelayLegacyPayload(ShortChannelId(12345), 998900 msat, CltvExpiry(60)) - val r = createValidIncomingPacket(1000000 msat, CltvExpiry(70), payload) + val r = createValidIncomingPacket(payload, 1000000 msat, CltvExpiry(70)) channelRelayer ! Relay(r) // select the channel to the same node, with the lowest capacity and balance but still high enough to handle the payment val cmd1 = expectFwdAdd(register, channelUpdates(ShortChannelId(22223)).channelId, payload.amountToForward, payload.outgoingCltv).message @@ -342,35 +411,35 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a { // higher amount payment (have to increased incoming htlc amount for fees to be sufficient) val payload = RelayLegacyPayload(ShortChannelId(12345), 50000000 msat, CltvExpiry(60)) - val r = createValidIncomingPacket(60000000 msat, CltvExpiry(70), payload) + val r = createValidIncomingPacket(payload, 60000000 msat, CltvExpiry(70)) channelRelayer ! Relay(r) expectFwdAdd(register, channelUpdates(ShortChannelId(11111)).channelId, payload.amountToForward, payload.outgoingCltv).message } { // lower amount payment val payload = RelayLegacyPayload(ShortChannelId(12345), 1000 msat, CltvExpiry(60)) - val r = createValidIncomingPacket(60000000 msat, CltvExpiry(70), payload) + val r = createValidIncomingPacket(payload, 60000000 msat, CltvExpiry(70)) channelRelayer ! Relay(r) expectFwdAdd(register, channelUpdates(ShortChannelId(33333)).channelId, payload.amountToForward, payload.outgoingCltv).message } { // payment too high, no suitable channel found, we keep the requested one val payload = RelayLegacyPayload(ShortChannelId(12345), 1000000000 msat, CltvExpiry(60)) - val r = createValidIncomingPacket(1010000000 msat, CltvExpiry(70), payload) + val r = createValidIncomingPacket(payload, 1010000000 msat, CltvExpiry(70)) channelRelayer ! Relay(r) expectFwdAdd(register, channelUpdates(ShortChannelId(12345)).channelId, payload.amountToForward, payload.outgoingCltv).message } { // cltv expiry larger than our requirements val payload = RelayLegacyPayload(ShortChannelId(12345), 998900 msat, CltvExpiry(50)) - val r = createValidIncomingPacket(1000000 msat, CltvExpiry(70), payload) + val r = createValidIncomingPacket(payload, 1000000 msat, CltvExpiry(70)) channelRelayer ! Relay(r) expectFwdAdd(register, channelUpdates(ShortChannelId(22223)).channelId, payload.amountToForward, payload.outgoingCltv).message } { // cltv expiry too small, no suitable channel found val payload = RelayLegacyPayload(ShortChannelId(12345), 998900 msat, CltvExpiry(61)) - val r = createValidIncomingPacket(1000000 msat, CltvExpiry(70), payload) + val r = createValidIncomingPacket(payload, 1000000 msat, CltvExpiry(70)) channelRelayer ! Relay(r) expectFwdFail(register, r.add.channelId, CMD_FAIL_HTLC(r.add.id, Right(IncorrectCltvExpiry(CltvExpiry(61), channelUpdates(ShortChannelId(12345)).channelUpdate)), commit = true)) } @@ -379,11 +448,11 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a test("settlement failure") { f => import f._ - val channelId1 = channelIds(shortId1) - val payload = RelayLegacyPayload(shortId1, outgoingAmount, outgoingExpiry) - val r = createValidIncomingPacket(1100000 msat, CltvExpiry(400100), payload) - val u = createLocalUpdate(shortId1) - val u_disabled = createLocalUpdate(shortId1, enabled = false) + val channelId1 = channelIds(realScid1) + val payload = RelayLegacyPayload(realScid1, outgoingAmount, outgoingExpiry) + val r = createValidIncomingPacket(payload, 1100000 msat, CltvExpiry(400100)) + val u = createLocalUpdate(channelId1) + val u_disabled = createLocalUpdate(channelId1, enabled = false) val downstream_htlc = UpdateAddHtlc(channelId1, 7, outgoingAmount, paymentHash, outgoingExpiry, emptyOnionPacket) case class TestCase(result: HtlcResult, cmd: channel.HtlcSettlementCommand) @@ -399,7 +468,7 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a testCases.foreach { testCase => channelRelayer ! WrappedLocalChannelUpdate(u) channelRelayer ! Relay(r) - val fwd = expectFwdAdd(register, channelIds(shortId1), outgoingAmount, outgoingExpiry) + val fwd = expectFwdAdd(register, channelIds(realScid1), outgoingAmount, outgoingExpiry) fwd.message.replyTo ! RES_SUCCESS(fwd.message, channelId1) fwd.message.origin.replyTo ! RES_ADD_SETTLED(fwd.message.origin, downstream_htlc, testCase.result) expectFwdFail(register, r.add.channelId, testCase.cmd) @@ -411,10 +480,10 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a val eventListener = TestProbe[ChannelPaymentRelayed]() system.eventStream ! EventStream.Subscribe(eventListener.ref) - val channelId1 = channelIds(shortId1) - val payload = RelayLegacyPayload(shortId1, outgoingAmount, outgoingExpiry) - val r = createValidIncomingPacket(1100000 msat, CltvExpiry(400100), payload) - val u = createLocalUpdate(shortId1) + val channelId1 = channelIds(realScid1) + val payload = RelayLegacyPayload(realScid1, outgoingAmount, outgoingExpiry) + val r = createValidIncomingPacket(payload) + val u = createLocalUpdate(channelId1) val downstream_htlc = UpdateAddHtlc(channelId1, 7, outgoingAmount, paymentHash, outgoingExpiry, emptyOnionPacket) case class TestCase(result: HtlcResult) @@ -428,7 +497,7 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a channelRelayer ! WrappedLocalChannelUpdate(u) channelRelayer ! Relay(r) - val fwd1 = expectFwdAdd(register, channelIds(shortId1), outgoingAmount, outgoingExpiry) + val fwd1 = expectFwdAdd(register, channelIds(realScid1), outgoingAmount, outgoingExpiry) fwd1.message.replyTo ! RES_SUCCESS(fwd1.message, channelId1) fwd1.message.origin.replyTo ! RES_ADD_SETTLED(fwd1.message.origin, downstream_htlc, testCase.result) @@ -447,6 +516,8 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a import f._ val channelId_ab = randomBytes32() val channelId_bc = randomBytes32() + val shortIds_ab = ShortIds(RealScidStatus.Final(RealShortChannelId(channelUpdate_ab.shortChannelId.toLong)), ShortChannelId.generateLocalAlias(), remoteAlias_opt = None) + val shortIds_bc = ShortIds(RealScidStatus.Final(RealShortChannelId(channelUpdate_bc.shortChannelId.toLong)), ShortChannelId.generateLocalAlias(), remoteAlias_opt = None) val a = PaymentPacketSpec.a val sender = TestProbe[Relayer.OutgoingChannels]() @@ -457,8 +528,8 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a channels } - channelRelayer ! WrappedLocalChannelUpdate(LocalChannelUpdate(null, channelId_ab, channelUpdate_ab.shortChannelId, a, None, channelUpdate_ab, makeCommitments(channelId_ab, -2000 msat, 300000 msat))) - channelRelayer ! WrappedLocalChannelUpdate(LocalChannelUpdate(null, channelId_bc, channelUpdate_bc.shortChannelId, c, None, channelUpdate_bc, makeCommitments(channelId_bc, 400000 msat, -5000 msat))) + channelRelayer ! WrappedLocalChannelUpdate(LocalChannelUpdate(null, channelId_ab, shortIds_ab, a, None, channelUpdate_ab, makeCommitments(channelId_ab, -2000 msat, 300000 msat))) + channelRelayer ! WrappedLocalChannelUpdate(LocalChannelUpdate(null, channelId_bc, shortIds_bc, c, None, channelUpdate_bc, makeCommitments(channelId_bc, 400000 msat, -5000 msat))) val channels1 = getOutgoingChannels(true) assert(channels1.size == 2) @@ -467,33 +538,24 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a assert(channels1.last.channelUpdate == channelUpdate_bc) assert(channels1.last.toChannelBalance == Relayer.ChannelBalance(c, channelUpdate_bc.shortChannelId, 400000 msat, 0 msat, isPublic = false, isEnabled = true)) - channelRelayer ! WrappedAvailableBalanceChanged(AvailableBalanceChanged(null, channelId_bc, channelUpdate_bc.shortChannelId, makeCommitments(channelId_bc, 200000 msat, 500000 msat))) + channelRelayer ! WrappedAvailableBalanceChanged(AvailableBalanceChanged(null, channelId_bc, shortIds_ab, makeCommitments(channelId_bc, 200000 msat, 500000 msat))) val channels2 = getOutgoingChannels(true) assert(channels2.last.commitments.availableBalanceForReceive == 500000.msat && channels2.last.commitments.availableBalanceForSend == 200000.msat) - channelRelayer ! WrappedAvailableBalanceChanged(AvailableBalanceChanged(null, channelId_ab, channelUpdate_ab.shortChannelId, makeCommitments(channelId_ab, 100000 msat, 200000 msat))) - channelRelayer ! WrappedLocalChannelDown(LocalChannelDown(null, channelId_bc, channelUpdate_bc.shortChannelId, c)) + channelRelayer ! WrappedAvailableBalanceChanged(AvailableBalanceChanged(null, channelId_ab, shortIds_ab, makeCommitments(channelId_ab, 100000 msat, 200000 msat))) + channelRelayer ! WrappedLocalChannelDown(LocalChannelDown(null, channelId_bc, shortIds_ab, c)) val channels3 = getOutgoingChannels(true) assert(channels3.size == 1 && channels3.head.commitments.availableBalanceForSend == 100000.msat) - channelRelayer ! WrappedLocalChannelUpdate(LocalChannelUpdate(null, channelId_ab, channelUpdate_ab.shortChannelId, a, None, channelUpdate_ab.copy(channelFlags = ChannelUpdate.ChannelFlags(isEnabled = false, isNode1 = true)), makeCommitments(channelId_ab, 100000 msat, 200000 msat))) + channelRelayer ! WrappedLocalChannelUpdate(LocalChannelUpdate(null, channelId_ab, shortIds_ab, a, None, channelUpdate_ab.copy(channelFlags = ChannelUpdate.ChannelFlags(isEnabled = false, isNode1 = true)), makeCommitments(channelId_ab, 100000 msat, 200000 msat))) val channels4 = getOutgoingChannels(true) assert(channels4.isEmpty) val channels5 = getOutgoingChannels(false) assert(channels5.size == 1) - channelRelayer ! WrappedLocalChannelUpdate(LocalChannelUpdate(null, channelId_ab, channelUpdate_ab.shortChannelId, a, None, channelUpdate_ab, makeCommitments(channelId_ab, 100000 msat, 200000 msat))) + channelRelayer ! WrappedLocalChannelUpdate(LocalChannelUpdate(null, channelId_ab, shortIds_ab, a, None, channelUpdate_ab, makeCommitments(channelId_ab, 100000 msat, 200000 msat))) val channels6 = getOutgoingChannels(true) assert(channels6.size == 1) - - // Simulate a chain re-org that changes the shortChannelId: - channelRelayer ! WrappedShortChannelIdAssigned(ShortChannelIdAssigned(null, channelId_ab, ShortChannelId(42), Some(channelUpdate_ab.shortChannelId))) - - // We should receive the updated channel update containing the new shortChannelId: - channelRelayer ! WrappedLocalChannelUpdate(LocalChannelUpdate(null, channelId_ab, ShortChannelId(42), a, None, channelUpdate_ab.copy(shortChannelId = ShortChannelId(42)), makeCommitments(channelId_ab, 100000 msat, 200000 msat))) - val channels7 = getOutgoingChannels(true) - assert(channels7.size == 1) - assert(channels7.head.channelUpdate.shortChannelId == ShortChannelId(42)) } } @@ -506,23 +568,41 @@ object ChannelRelayerSpec { val outgoingExpiry: CltvExpiry = CltvExpiry(400000) val outgoingNodeId: PublicKey = randomKey().publicKey - val shortId1: ShortChannelId = ShortChannelId(111111) - val shortId2: ShortChannelId = ShortChannelId(222222) + val realScid1: RealShortChannelId = RealShortChannelId(111111) + val realScId2: RealShortChannelId = RealShortChannelId(222222) + + val localAlias1: Alias = Alias(111000) + val localAlias2: Alias = Alias(222000) + + val remoteAlias1: ShortChannelId = ShortChannelId(111999) + + val channelId1: ByteVector32 = randomBytes32() + val channelId2: ByteVector32 = randomBytes32() val channelIds = Map( - shortId1 -> randomBytes32(), - shortId2 -> randomBytes32() + realScid1 -> channelId1, + realScId2 -> channelId2, + localAlias1 -> channelId1, + localAlias2 -> channelId2, ) - def createValidIncomingPacket(amountIn: MilliSatoshi, expiryIn: CltvExpiry, payload: ChannelRelayPayload): IncomingPaymentPacket.ChannelRelayPacket = { + def createValidIncomingPacket(payload: ChannelRelayPayload, amountIn: MilliSatoshi = 1_100_000 msat, expiryIn: CltvExpiry = CltvExpiry(400_100)): IncomingPaymentPacket.ChannelRelayPacket = { val add_ab = UpdateAddHtlc(channelId = randomBytes32(), id = 123456, amountIn, paymentHash, expiryIn, emptyOnionPacket) ChannelRelayPacket(add_ab, payload, emptyOnionPacket) } - def createLocalUpdate(shortChannelId: ShortChannelId, balance: MilliSatoshi = 10000000 msat, capacity: Satoshi = 500000 sat, enabled: Boolean = true, htlcMinimum: MilliSatoshi = 0 msat, timestamp: TimestampSecond = 0 unixsec, feeBaseMsat: MilliSatoshi = 1000 msat, feeProportionalMillionths: Long = 100): LocalChannelUpdate = { - val channelId = channelIds(shortChannelId) - val update = ChannelUpdate(ByteVector64(randomBytes(64)), Block.RegtestGenesisBlock.hash, shortChannelId, timestamp, ChannelUpdate.ChannelFlags(isNode1 = true, isEnabled = enabled), CltvExpiryDelta(100), htlcMinimum, feeBaseMsat, feeProportionalMillionths, Some(capacity.toMilliSatoshi)) - val commitments = PaymentPacketSpec.makeCommitments(channelId, testAvailableBalanceForSend = balance, testCapacity = capacity) - LocalChannelUpdate(null, channelId, shortChannelId, outgoingNodeId, None, update, commitments) + def createShortIds(channelId: ByteVector32) = { + val realScid = channelIds.collectFirst { case (realScid: RealShortChannelId, cid) if cid == channelId => realScid }.get + val localAlias = channelIds.collectFirst { case (localAlias: Alias, cid) if cid == channelId => localAlias }.get + ShortIds(real = RealScidStatus.Final(realScid), localAlias, remoteAlias_opt = None) + } + + def createLocalUpdate(channelId: ByteVector32, channelUpdateScid_opt: Option[ShortChannelId] = None, balance: MilliSatoshi = 10000000 msat, capacity: Satoshi = 500000 sat, enabled: Boolean = true, htlcMinimum: MilliSatoshi = 0 msat, timestamp: TimestampSecond = 0 unixsec, feeBaseMsat: MilliSatoshi = 1000 msat, feeProportionalMillionths: Long = 100, optionScidAlias: Boolean = false): LocalChannelUpdate = { + val shortIds = createShortIds(channelId) + val channelUpdateScid = channelUpdateScid_opt.getOrElse(shortIds.real.toOption.get) + val update = ChannelUpdate(ByteVector64(randomBytes(64)), Block.RegtestGenesisBlock.hash, channelUpdateScid, timestamp, ChannelUpdate.ChannelFlags(isNode1 = true, isEnabled = enabled), CltvExpiryDelta(100), htlcMinimum, feeBaseMsat, feeProportionalMillionths, Some(capacity.toMilliSatoshi)) + val channelFeatures = if (optionScidAlias) ChannelFeatures(ScidAlias) else ChannelFeatures() + val commitments = PaymentPacketSpec.makeCommitments(channelId, testAvailableBalanceForSend = balance, testCapacity = capacity, channelFeatures = channelFeatures) + LocalChannelUpdate(null, channelId, shortIds, outgoingNodeId, None, update, commitments) } } \ No newline at end of file diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/RelayerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/RelayerSpec.scala index 0df90707cb..89aaef7e17 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/RelayerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/RelayerSpec.scala @@ -31,7 +31,7 @@ import fr.acinq.eclair.payment.PaymentPacketSpec._ import fr.acinq.eclair.payment.relay.Relayer._ import fr.acinq.eclair.payment.{OutgoingPaymentPacket, PaymentPacketSpec} import fr.acinq.eclair.router.BaseRouterSpec.channelHopFromUpdate -import fr.acinq.eclair.router.Router.{ChannelHop, NodeHop} +import fr.acinq.eclair.router.Router.NodeHop import fr.acinq.eclair.wire.protocol._ import fr.acinq.eclair.{NodeParams, TestConstants, randomBytes32, _} import org.scalatest.concurrent.PatienceConfiguration @@ -78,7 +78,8 @@ class RelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("applicat assert(sender.expectMessageType[Relayer.OutgoingChannels].channels.isEmpty) // We publish a channel update, that should be picked up by the channel relayer - system.eventStream ! EventStream.Publish(LocalChannelUpdate(null, channelId_bc, channelUpdate_bc.shortChannelId, c, None, channelUpdate_bc, makeCommitments(channelId_bc))) + val shortIds_bc = ShortIds(RealScidStatus.Final(RealShortChannelId(channelUpdate_bc.shortChannelId.toLong)), ShortChannelId.generateLocalAlias(), remoteAlias_opt = None) + system.eventStream ! EventStream.Publish(LocalChannelUpdate(null, channelId_bc, shortIds_bc, c, None, channelUpdate_bc, makeCommitments(channelId_bc))) eventually(PatienceConfiguration.Timeout(30 seconds), PatienceConfiguration.Interval(1 second)) { childActors.channelRelayer ! ChannelRelayer.GetOutgoingChannels(sender.ref.toClassic, GetOutgoingChannels()) val channels = sender.expectMessageType[Relayer.OutgoingChannels].channels diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/AnnouncementsBatchValidationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/AnnouncementsBatchValidationSpec.scala index b87f78b3f6..b5d7775a4f 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/AnnouncementsBatchValidationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/AnnouncementsBatchValidationSpec.scala @@ -28,7 +28,7 @@ import fr.acinq.eclair.blockchain.bitcoind.rpc.{BasicBitcoinJsonRPCClient, Bitco import fr.acinq.eclair.blockchain.fee.FeeratePerKw import fr.acinq.eclair.transactions.Scripts import fr.acinq.eclair.wire.protocol.{ChannelAnnouncement, ChannelUpdate} -import fr.acinq.eclair.{BlockHeight, CltvExpiryDelta, Features, MilliSatoshiLong, ShortChannelId, randomKey} +import fr.acinq.eclair.{BlockHeight, CltvExpiryDelta, Features, MilliSatoshiLong, RealShortChannelId, ShortChannelId, randomKey} import org.json4s.JsonAST.JString import org.scalatest.funsuite.AnyFunSuite @@ -64,10 +64,10 @@ class AnnouncementsBatchValidationSpec extends AnyFunSuite { bitcoinClient.validate(announcements(0)).pipeTo(sender.ref) sender.expectMsgType[ValidateResult].fundingTx.isRight - bitcoinClient.validate(announcements(1).copy(shortChannelId = ShortChannelId(Long.MaxValue))).pipeTo(sender.ref) // invalid block height + bitcoinClient.validate(announcements(1).copy(shortChannelId = RealShortChannelId(Long.MaxValue))).pipeTo(sender.ref) // invalid block height sender.expectMsgType[ValidateResult].fundingTx.isRight - bitcoinClient.validate(announcements(2).copy(shortChannelId = ShortChannelId(BlockHeight(500), 1000, 0))).pipeTo(sender.ref) // invalid tx index + bitcoinClient.validate(announcements(2).copy(shortChannelId = RealShortChannelId(BlockHeight(500), 1000, 0))).pipeTo(sender.ref) // invalid tx index sender.expectMsgType[ValidateResult].fundingTx.isRight } @@ -101,7 +101,7 @@ object AnnouncementsBatchValidationSpec { def makeChannelAnnouncement(c: SimulatedChannel, bitcoinClient: BitcoinCoreClient)(implicit ec: ExecutionContext): ChannelAnnouncement = { val (blockHeight, txIndex) = Await.result(bitcoinClient.getTransactionShortId(c.fundingTx.txid), 10 seconds) - val shortChannelId = ShortChannelId(blockHeight, txIndex, c.fundingOutputIndex) + val shortChannelId = RealShortChannelId(blockHeight, txIndex, c.fundingOutputIndex) val witness = Announcements.generateChannelAnnouncementWitness(Block.RegtestGenesisBlock.hash, shortChannelId, c.node1Key.publicKey, c.node2Key.publicKey, c.node1FundingKey.publicKey, c.node2FundingKey.publicKey, Features.empty) val channelAnnNodeSig1 = Announcements.signChannelAnnouncement(witness, c.node1Key) val channelAnnBitcoinSig1 = Announcements.signChannelAnnouncement(witness, c.node1FundingKey) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/AnnouncementsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/AnnouncementsSpec.scala index b7e4b52f35..b82360bc0f 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/AnnouncementsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/AnnouncementsSpec.scala @@ -19,6 +19,7 @@ package fr.acinq.eclair.router import fr.acinq.bitcoin.scalacompat.Block import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} import fr.acinq.eclair.TestConstants.Alice +import fr.acinq.eclair.RealShortChannelId import fr.acinq.eclair._ import fr.acinq.eclair.router.Announcements._ import fr.acinq.eclair.wire.protocol.ChannelUpdate.ChannelFlags @@ -43,12 +44,12 @@ class AnnouncementsSpec extends AnyFunSuite { test("create valid signed channel announcement") { val (node_a, node_b, bitcoin_a, bitcoin_b) = (randomKey(), randomKey(), randomKey(), randomKey()) - val witness = Announcements.generateChannelAnnouncementWitness(Block.RegtestGenesisBlock.hash, ShortChannelId(42L), node_a.publicKey, node_b.publicKey, bitcoin_a.publicKey, bitcoin_b.publicKey, Features.empty) + val witness = Announcements.generateChannelAnnouncementWitness(Block.RegtestGenesisBlock.hash, RealShortChannelId(42), node_a.publicKey, node_b.publicKey, bitcoin_a.publicKey, bitcoin_b.publicKey, Features.empty) val node_a_sig = Announcements.signChannelAnnouncement(witness, node_a) val bitcoin_a_sig = Announcements.signChannelAnnouncement(witness, bitcoin_a) val node_b_sig = Announcements.signChannelAnnouncement(witness, node_b) val bitcoin_b_sig = Announcements.signChannelAnnouncement(witness, bitcoin_b) - val ann = makeChannelAnnouncement(Block.RegtestGenesisBlock.hash, ShortChannelId(42L), node_a.publicKey, node_b.publicKey, bitcoin_a.publicKey, bitcoin_b.publicKey, node_a_sig, node_b_sig, bitcoin_a_sig, bitcoin_b_sig) + val ann = makeChannelAnnouncement(Block.RegtestGenesisBlock.hash, RealShortChannelId(42), node_a.publicKey, node_b.publicKey, bitcoin_a.publicKey, bitcoin_b.publicKey, node_a_sig, node_b_sig, bitcoin_a_sig, bitcoin_b_sig) assert(checkSigs(ann)) assert(checkSigs(ann.copy(nodeId1 = randomKey().publicKey)) == false) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/BaseRouterSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/BaseRouterSpec.scala index 625f814d44..6cc08c32b2 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/BaseRouterSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/BaseRouterSpec.scala @@ -24,7 +24,7 @@ import fr.acinq.bitcoin.scalacompat.Script.{pay2wsh, write} import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, SatoshiLong, Transaction, TxOut} import fr.acinq.eclair.TestConstants.Alice import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.{UtxoStatus, ValidateRequest, ValidateResult, WatchExternalChannelSpent} -import fr.acinq.eclair.channel.{CommitmentsSpec, LocalChannelUpdate} +import fr.acinq.eclair.channel.{CommitmentsSpec, LocalChannelUpdate, RealScidStatus, ShortChannelIdAssigned, ShortIds} import fr.acinq.eclair.crypto.TransportHandler import fr.acinq.eclair.crypto.keymanager.{LocalChannelKeyManager, LocalNodeKeyManager} import fr.acinq.eclair.io.Peer.PeerRoutingMessage @@ -33,7 +33,7 @@ import fr.acinq.eclair.router.BaseRouterSpec.channelAnnouncement import fr.acinq.eclair.router.Router._ import fr.acinq.eclair.transactions.Scripts import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{TestKitBaseClass, randomKey, _} +import fr.acinq.eclair._ import org.scalatest.Outcome import org.scalatest.funsuite.FixtureAnyFunSuiteLike import scodec.bits.ByteVector @@ -73,15 +73,21 @@ abstract class BaseRouterSpec extends TestKitBaseClass with FixtureAnyFunSuiteLi val node_g = makeNodeAnnouncement(priv_g, "node-G", Color(30, 10, -50), Nil, Features.empty) val node_h = makeNodeAnnouncement(priv_h, "node-H", Color(30, 10, -50), Nil, Features.empty) - val scid_ab = ShortChannelId(BlockHeight(420000), 1, 0) - val scid_bc = ShortChannelId(BlockHeight(420000), 2, 0) - val scid_cd = ShortChannelId(BlockHeight(420000), 3, 0) - val scid_ef = ShortChannelId(BlockHeight(420000), 4, 0) - val scid_ag_private = ShortChannelId(BlockHeight(420000), 5, 0) - val scid_gh = ShortChannelId(BlockHeight(420000), 6, 0) + val scid_ab = RealShortChannelId(BlockHeight(420000), 1, 0) + val scid_bc = RealShortChannelId(BlockHeight(420000), 2, 0) + val scid_cd = RealShortChannelId(BlockHeight(420000), 3, 0) + val scid_ef = RealShortChannelId(BlockHeight(420000), 4, 0) + val scid_ag_private = RealShortChannelId(BlockHeight(420000), 5, 0) + val scid_gh = RealShortChannelId(BlockHeight(420000), 6, 0) val channelId_ag_private = randomBytes32() + val alias_ab = ShortChannelId.generateLocalAlias() + val alias_ag_private = ShortChannelId.generateLocalAlias() + + val scids_ab = ShortIds(RealScidStatus.Final(scid_ab), alias_ab, None) + val scids_ag_private = ShortIds(RealScidStatus.Final(scid_ag_private), alias_ag_private, None) + val chan_ab = channelAnnouncement(scid_ab, priv_a, priv_b, priv_funding_a, priv_funding_b) val chan_bc = channelAnnouncement(scid_bc, priv_b, priv_c, priv_funding_b, priv_funding_c) val chan_cd = channelAnnouncement(scid_cd, priv_c, priv_d, priv_funding_c, priv_funding_d) @@ -96,8 +102,8 @@ abstract class BaseRouterSpec extends TestKitBaseClass with FixtureAnyFunSuiteLi val update_dc = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_d, c, scid_cd, CltvExpiryDelta(3), htlcMinimumMsat = 0 msat, feeBaseMsat = 10 msat, feeProportionalMillionths = 4, htlcMaximumMsat = htlcMaximum) val update_ef = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_e, f, scid_ef, CltvExpiryDelta(9), htlcMinimumMsat = 0 msat, feeBaseMsat = 10 msat, feeProportionalMillionths = 8, htlcMaximumMsat = htlcMaximum) val update_fe = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_f, e, scid_ef, CltvExpiryDelta(9), htlcMinimumMsat = 0 msat, feeBaseMsat = 10 msat, feeProportionalMillionths = 8, htlcMaximumMsat = htlcMaximum) - val update_ag_private = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_a, g, scid_ag_private, CltvExpiryDelta(7), htlcMinimumMsat = 0 msat, feeBaseMsat = 10 msat, feeProportionalMillionths = 10, htlcMaximumMsat = htlcMaximum) - val update_ga_private = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_g, a, scid_ag_private, CltvExpiryDelta(7), htlcMinimumMsat = 0 msat, feeBaseMsat = 10 msat, feeProportionalMillionths = 10, htlcMaximumMsat = htlcMaximum) + val update_ag_private = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_a, g, scids_ag_private.localAlias, CltvExpiryDelta(7), htlcMinimumMsat = 0 msat, feeBaseMsat = 10 msat, feeProportionalMillionths = 10, htlcMaximumMsat = htlcMaximum) + val update_ga_private = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_g, a, scids_ag_private.localAlias, CltvExpiryDelta(7), htlcMinimumMsat = 0 msat, feeBaseMsat = 10 msat, feeProportionalMillionths = 10, htlcMaximumMsat = htlcMaximum) val update_gh = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_g, h, scid_gh, CltvExpiryDelta(7), htlcMinimumMsat = 0 msat, feeBaseMsat = 10 msat, feeProportionalMillionths = 10, htlcMaximumMsat = htlcMaximum) val update_hg = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_h, g, scid_gh, CltvExpiryDelta(7), htlcMinimumMsat = 0 msat, feeBaseMsat = 10 msat, feeProportionalMillionths = 10, htlcMaximumMsat = htlcMaximum) @@ -112,8 +118,9 @@ abstract class BaseRouterSpec extends TestKitBaseClass with FixtureAnyFunSuiteLi assert(ChannelDesc(update_bc, chan_bc) == ChannelDesc(chan_bc.shortChannelId, b, c)) assert(ChannelDesc(update_cd, chan_cd) == ChannelDesc(chan_cd.shortChannelId, c, d)) assert(ChannelDesc(update_ef, chan_ef) == ChannelDesc(chan_ef.shortChannelId, e, f)) - assert(ChannelDesc(update_ag_private, PrivateChannel(scid_ag_private, channelId_ag_private, a, g, None, None, ChannelMeta(1000 msat, 2000 msat))) == ChannelDesc(scid_ag_private, a, g)) - assert(ChannelDesc(update_ag_private, PrivateChannel(scid_ag_private, channelId_ag_private, g, a, None, None, ChannelMeta(2000 msat, 1000 msat))) == ChannelDesc(scid_ag_private, a, g)) + val privateChannel_ag = PrivateChannel(channelId_ag_private, scids_ag_private, a, g, None, None, ChannelMeta(1000 msat, 1000 msat)) + assert(ChannelDesc(update_ag_private, privateChannel_ag) == ChannelDesc(scids_ag_private.localAlias, a, g)) + assert(ChannelDesc(update_ga_private, privateChannel_ag) == ChannelDesc(scids_ag_private.localAlias, g, a)) assert(ChannelDesc(update_gh, chan_gh) == ChannelDesc(chan_gh.shortChannelId, g, h)) // let's set up the router @@ -151,7 +158,8 @@ abstract class BaseRouterSpec extends TestKitBaseClass with FixtureAnyFunSuiteLi peerConnection.send(router, PeerRoutingMessage(peerConnection.ref, remoteNodeId, update_gh)) peerConnection.send(router, PeerRoutingMessage(peerConnection.ref, remoteNodeId, update_hg)) // then private channels - sender.send(router, LocalChannelUpdate(sender.ref, channelId_ag_private, scid_ag_private, g, None, update_ag_private, CommitmentsSpec.makeCommitments(30000000 msat, 8000000 msat, a, g, announceChannel = false))) + sender.send(router, ShortChannelIdAssigned(sender.ref, channelId_ag_private, scids_ag_private, remoteNodeId = g)) + sender.send(router, LocalChannelUpdate(sender.ref, channelId_ag_private, scids_ag_private, g, None, update_ag_private, CommitmentsSpec.makeCommitments(30000000 msat, 8000000 msat, a, g, announceChannel = false))) // watcher receives the get tx requests assert(watcher.expectMsgType[ValidateRequest].ann == chan_ab) assert(watcher.expectMsgType[ValidateRequest].ann == chan_bc) @@ -214,13 +222,13 @@ abstract class BaseRouterSpec extends TestKitBaseClass with FixtureAnyFunSuiteLi } object BaseRouterSpec { - def channelAnnouncement(channelId: ShortChannelId, node1_priv: PrivateKey, node2_priv: PrivateKey, funding1_priv: PrivateKey, funding2_priv: PrivateKey) = { - val witness = Announcements.generateChannelAnnouncementWitness(Block.RegtestGenesisBlock.hash, channelId, node1_priv.publicKey, node2_priv.publicKey, funding1_priv.publicKey, funding2_priv.publicKey, Features.empty) + def channelAnnouncement(shortChannelId: RealShortChannelId, node1_priv: PrivateKey, node2_priv: PrivateKey, funding1_priv: PrivateKey, funding2_priv: PrivateKey) = { + val witness = Announcements.generateChannelAnnouncementWitness(Block.RegtestGenesisBlock.hash, shortChannelId, node1_priv.publicKey, node2_priv.publicKey, funding1_priv.publicKey, funding2_priv.publicKey, Features.empty) val node1_sig = Announcements.signChannelAnnouncement(witness, node1_priv) val funding1_sig = Announcements.signChannelAnnouncement(witness, funding1_priv) val node2_sig = Announcements.signChannelAnnouncement(witness, node2_priv) val funding2_sig = Announcements.signChannelAnnouncement(witness, funding2_priv) - makeChannelAnnouncement(Block.RegtestGenesisBlock.hash, channelId, node1_priv.publicKey, node2_priv.publicKey, funding1_priv.publicKey, funding2_priv.publicKey, node1_sig, node2_sig, funding1_sig, funding2_sig) + makeChannelAnnouncement(Block.RegtestGenesisBlock.hash, shortChannelId, node1_priv.publicKey, node2_priv.publicKey, funding1_priv.publicKey, funding2_priv.publicKey, node1_sig, node2_sig, funding1_sig, funding2_sig) } def channelHopFromUpdate(nodeId: PublicKey, nextNodeId: PublicKey, channelUpdate: ChannelUpdate): ChannelHop = diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRangeQueriesSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRangeQueriesSpec.scala index 15cbbc832f..2baf31a3a8 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRangeQueriesSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRangeQueriesSpec.scala @@ -17,6 +17,7 @@ package fr.acinq.eclair.router import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, SatoshiLong} +import fr.acinq.eclair.RealShortChannelId import fr.acinq.eclair.router.Router.{ChannelMeta, PublicChannel} import fr.acinq.eclair.router.Sync._ import fr.acinq.eclair.wire.protocol.QueryChannelRangeTlv.QueryFlags @@ -130,8 +131,8 @@ class ChannelRangeQueriesSpec extends AnyFunSuite { assert(computeFlag(channels)(ef.shortChannelId, None, None, includeNodeAnnouncements = true) == (INCLUDE_CHANNEL_ANNOUNCEMENT | INCLUDE_CHANNEL_UPDATE_1 | INCLUDE_CHANNEL_UPDATE_2 | INCLUDE_NODE_ANNOUNCEMENT_1 | INCLUDE_NODE_ANNOUNCEMENT_2)) } - def makeShortChannelIds(height: BlockHeight, count: Int): List[ShortChannelId] = { - val output = ArrayBuffer.empty[ShortChannelId] + def makeShortChannelIds(height: BlockHeight, count: Int): List[RealShortChannelId] = { + val output = ArrayBuffer.empty[RealShortChannelId] var txIndex = 0 var outputIndex = 0 while (output.size < count) { @@ -141,7 +142,7 @@ class ChannelRangeQueriesSpec extends AnyFunSuite { } else { outputIndex = outputIndex + 1 } - output += ShortChannelId(height, txIndex, outputIndex) + output += RealShortChannelId(height, txIndex, outputIndex) } output.toList } @@ -152,7 +153,7 @@ class ChannelRangeQueriesSpec extends AnyFunSuite { // check that chunks contain exactly the ids they were built from are are consistent i.e each chunk covers a range that immediately follows // the previous one even if there are gaps in block heights - def validate(ids: SortedSet[ShortChannelId], firstBlock: BlockHeight, numberOfBlocks: Long, chunks: List[ShortChannelIdsChunk]): Unit = { + def validate(ids: SortedSet[RealShortChannelId], firstBlock: BlockHeight, numberOfBlocks: Long, chunks: List[ShortChannelIdsChunk]): Unit = { @tailrec def noOverlap(chunks: List[ShortChannelIdsChunk]): Boolean = chunks match { @@ -189,14 +190,14 @@ class ChannelRangeQueriesSpec extends AnyFunSuite { test("split short channel ids correctly (basic tests") { - def id(blockHeight: Int, txIndex: Int = 0, outputIndex: Int = 0) = ShortChannelId(BlockHeight(blockHeight), txIndex, outputIndex) + def id(blockHeight: Int, txIndex: Int = 0, outputIndex: Int = 0) = RealShortChannelId(BlockHeight(blockHeight), txIndex, outputIndex) // no ids to split { val ids = Nil val firstBlock = BlockHeight(10) val numberOfBlocks = 100 - val chunks = split(SortedSet.empty[ShortChannelId] ++ ids, firstBlock, numberOfBlocks, ids.size) + val chunks = split(SortedSet.empty[RealShortChannelId] ++ ids, firstBlock, numberOfBlocks, ids.size) assert(chunks == ShortChannelIdsChunk(firstBlock, numberOfBlocks, Nil) :: Nil) } @@ -205,7 +206,7 @@ class ChannelRangeQueriesSpec extends AnyFunSuite { val ids = List(id(1000), id(1001), id(1002), id(1003), id(1004), id(1005)) val firstBlock = BlockHeight(10) val numberOfBlocks = 100 - val chunks = split(SortedSet.empty[ShortChannelId] ++ ids, firstBlock, numberOfBlocks, ids.size) + val chunks = split(SortedSet.empty[RealShortChannelId] ++ ids, firstBlock, numberOfBlocks, ids.size) assert(chunks == ShortChannelIdsChunk(firstBlock, numberOfBlocks, Nil) :: Nil) } @@ -214,7 +215,7 @@ class ChannelRangeQueriesSpec extends AnyFunSuite { val ids = List(id(1000), id(1001), id(1002), id(1003), id(1004), id(1005)) val firstBlock = BlockHeight(1100) val numberOfBlocks = 100 - val chunks = split(SortedSet.empty[ShortChannelId] ++ ids, firstBlock, numberOfBlocks, ids.size) + val chunks = split(SortedSet.empty[RealShortChannelId] ++ ids, firstBlock, numberOfBlocks, ids.size) assert(chunks == ShortChannelIdsChunk(firstBlock, numberOfBlocks, Nil) :: Nil) } @@ -223,7 +224,7 @@ class ChannelRangeQueriesSpec extends AnyFunSuite { val ids = List(id(1000), id(1001), id(1002), id(1003), id(1004), id(1005)) val firstBlock = BlockHeight(900) val numberOfBlocks = 200 - val chunks = split(SortedSet.empty[ShortChannelId] ++ ids, firstBlock, numberOfBlocks, ids.size) + val chunks = split(SortedSet.empty[RealShortChannelId] ++ ids, firstBlock, numberOfBlocks, ids.size) assert(chunks == ShortChannelIdsChunk(firstBlock, numberOfBlocks, ids) :: Nil) } @@ -233,7 +234,7 @@ class ChannelRangeQueriesSpec extends AnyFunSuite { val ids = List(id(1000, 0), id(1000, 1), id(1000, 2), id(1000, 3), id(1000, 4), id(1000, 5)) val firstBlock = BlockHeight(900) val numberOfBlocks = 200 - val chunks = split(SortedSet.empty[ShortChannelId] ++ ids, firstBlock, numberOfBlocks, 2) + val chunks = split(SortedSet.empty[RealShortChannelId] ++ ids, firstBlock, numberOfBlocks, 2) assert(chunks == ShortChannelIdsChunk(firstBlock, numberOfBlocks, ids) :: Nil) } @@ -242,7 +243,7 @@ class ChannelRangeQueriesSpec extends AnyFunSuite { val ids = List(id(1000), id(1005), id(1012), id(1013), id(1040), id(1050)) val firstBlock = BlockHeight(900) val numberOfBlocks = 200 - val chunks = split(SortedSet.empty[ShortChannelId] ++ ids, firstBlock, numberOfBlocks, 2) + val chunks = split(SortedSet.empty[RealShortChannelId] ++ ids, firstBlock, numberOfBlocks, 2) assert(chunks == List( ShortChannelIdsChunk(firstBlock, 100 + 6, List(ids(0), ids(1))), ShortChannelIdsChunk(BlockHeight(1006), 8, List(ids(2), ids(3))), @@ -255,7 +256,7 @@ class ChannelRangeQueriesSpec extends AnyFunSuite { val ids = List(id(1000), id(1005), id(1012), id(1013), id(1040), id(1050)) val firstBlock = BlockHeight(1001) val numberOfBlocks = 200 - val chunks = split(SortedSet.empty[ShortChannelId] ++ ids, firstBlock, numberOfBlocks, 2) + val chunks = split(SortedSet.empty[RealShortChannelId] ++ ids, firstBlock, numberOfBlocks, 2) assert(chunks == List( ShortChannelIdsChunk(firstBlock, 12, List(ids(1), ids(2))), ShortChannelIdsChunk(BlockHeight(1013), 1040 - 1013 + 1, List(ids(3), ids(4))), @@ -268,7 +269,7 @@ class ChannelRangeQueriesSpec extends AnyFunSuite { val ids = List(id(1000), id(1001), id(1002), id(1003), id(1004), id(1005)) val firstBlock = BlockHeight(900) val numberOfBlocks = 105 - val chunks = split(SortedSet.empty[ShortChannelId] ++ ids, firstBlock, numberOfBlocks, 2) + val chunks = split(SortedSet.empty[RealShortChannelId] ++ ids, firstBlock, numberOfBlocks, 2) assert(chunks == List( ShortChannelIdsChunk(firstBlock, 100 + 2, List(ids(0), ids(1))), ShortChannelIdsChunk(BlockHeight(1002), 2, List(ids(2), ids(3))), @@ -281,7 +282,7 @@ class ChannelRangeQueriesSpec extends AnyFunSuite { val ids = List(id(1000), id(1001), id(1002), id(1003), id(1004), id(1005)) val firstBlock = BlockHeight(1001) val numberOfBlocks = 4 - val chunks = split(SortedSet.empty[ShortChannelId] ++ ids, firstBlock, numberOfBlocks, 2) + val chunks = split(SortedSet.empty[RealShortChannelId] ++ ids, firstBlock, numberOfBlocks, 2) assert(chunks == List( ShortChannelIdsChunk(firstBlock, 2, List(ids(1), ids(2))), ShortChannelIdsChunk(BlockHeight(1003), 2, List(ids(3), ids(4))) @@ -293,13 +294,13 @@ class ChannelRangeQueriesSpec extends AnyFunSuite { val ids = makeShortChannelIds(BlockHeight(1000), 100) val firstBlock = BlockHeight(900) val numberOfBlocks = 200 - val chunks = split(SortedSet.empty[ShortChannelId] ++ ids, firstBlock, numberOfBlocks, 10) + val chunks = split(SortedSet.empty[RealShortChannelId] ++ ids, firstBlock, numberOfBlocks, 10) assert(chunks == ShortChannelIdsChunk(firstBlock, numberOfBlocks, ids) :: Nil) } } test("split short channel ids correctly") { - val ids = SortedSet.empty[ShortChannelId] ++ makeShortChannelIds(BlockHeight(42), 100) ++ makeShortChannelIds(BlockHeight(43), 70) ++ makeShortChannelIds(BlockHeight(44), 50) ++ makeShortChannelIds(BlockHeight(45), 30) ++ makeShortChannelIds(BlockHeight(50), 120) + val ids = SortedSet.empty[RealShortChannelId] ++ makeShortChannelIds(BlockHeight(42), 100) ++ makeShortChannelIds(BlockHeight(43), 70) ++ makeShortChannelIds(BlockHeight(44), 50) ++ makeShortChannelIds(BlockHeight(45), 30) ++ makeShortChannelIds(BlockHeight(50), 120) val firstBlock = BlockHeight(0) val numberOfBlocks = 1000 @@ -311,7 +312,7 @@ class ChannelRangeQueriesSpec extends AnyFunSuite { } test("split short channel ids correctly (comprehensive tests)") { - val ids = SortedSet.empty[ShortChannelId] ++ makeShortChannelIds(BlockHeight(42), 100) ++ makeShortChannelIds(BlockHeight(43), 70) ++ makeShortChannelIds(BlockHeight(45), 50) ++ makeShortChannelIds(BlockHeight(47), 30) ++ makeShortChannelIds(BlockHeight(50), 120) + val ids = SortedSet.empty[RealShortChannelId] ++ makeShortChannelIds(BlockHeight(42), 100) ++ makeShortChannelIds(BlockHeight(43), 70) ++ makeShortChannelIds(BlockHeight(45), 50) ++ makeShortChannelIds(BlockHeight(47), 30) ++ makeShortChannelIds(BlockHeight(50), 120) for (firstBlock <- 0 to 60) { for (numberOfBlocks <- 1 to 60) { for (chunkSize <- 1 :: 2 :: 20 :: 50 :: 100 :: 1000 :: Nil) { @@ -357,7 +358,7 @@ class ChannelRangeQueriesSpec extends AnyFunSuite { } test("encode maximum size reply_channel_range") { - val scids = (1 to Sync.MAXIMUM_CHUNK_SIZE).map(i => ShortChannelId(i)).toList + val scids = (1 to Sync.MAXIMUM_CHUNK_SIZE).map(i => RealShortChannelId(i)).toList val timestamps = (1 to Sync.MAXIMUM_CHUNK_SIZE).map(i => Timestamps(i.unixsec, (i + 1).unixsec)).toList val checksums = (1 to Sync.MAXIMUM_CHUNK_SIZE).map(i => Checksums(i, i + 1)).toList val reply = ReplyChannelRange(Block.RegtestGenesisBlock.hash, BlockHeight(0), 100, 0, EncodedShortChannelIds(EncodingType.UNCOMPRESSED, scids), Some(EncodedTimestamps(EncodingType.UNCOMPRESSED, timestamps)), Some(EncodedChecksums(checksums))) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRouterIntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRouterIntegrationSpec.scala index daa0d8ed65..ebe90ec8f5 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRouterIntegrationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRouterIntegrationSpec.scala @@ -2,23 +2,31 @@ package fr.acinq.eclair.router import akka.actor.ActorSystem import akka.testkit.{TestFSMRef, TestProbe} -import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher -import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.WatchFundingDeeplyBuriedTriggered -import fr.acinq.eclair.channel.DATA_NORMAL +import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.{WatchExternalChannelSpent, WatchExternalChannelSpentTriggered, WatchFundingDeeplyBuriedTriggered} import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsTags} +import fr.acinq.eclair.channel.{CMD_CLOSE, DATA_NORMAL} import fr.acinq.eclair.io.Peer.PeerRoutingMessage -import fr.acinq.eclair.router.Router.{GossipOrigin, LocalGossip} -import fr.acinq.eclair.wire.protocol.{AnnouncementSignatures, ChannelUpdate} +import fr.acinq.eclair.router.Graph.GraphStructure.GraphEdge +import fr.acinq.eclair.router.Router._ +import fr.acinq.eclair.wire.protocol.{AnnouncementSignatures, ChannelUpdate, Shutdown} import fr.acinq.eclair.{BlockHeight, TestKitBaseClass} import org.scalatest.funsuite.FixtureAnyFunSuiteLike import org.scalatest.{Outcome, Tag} +import scala.concurrent.duration.DurationInt + /** * This test checks the integration between Channel and Router (events, etc.) */ class ChannelRouterIntegrationSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with ChannelStateTestsBase { - case class FixtureParam(router: TestFSMRef[Router.State, Router.Data, Router], rebroadcastListener: TestProbe, channels: SetupFixture, testTags: Set[String]) + case class FixtureParam(router: TestFSMRef[Router.State, Router.Data, Router], rebroadcastListener: TestProbe, channels: SetupFixture, testTags: Set[String]) { + //@formatter:off + /** there is only one channel here */ + def privateChannel: PrivateChannel = router.stateData.privateChannels.values.head + def publicChannel: PublicChannel = router.stateData.channels.values.head + //@formatter:on + } implicit val log: akka.event.LoggingAdapter = akka.event.NoLogging @@ -34,92 +42,164 @@ class ChannelRouterIntegrationSpec extends TestKitBaseClass with FixtureAnyFunSu withFixture(test.toNoArgTest(FixtureParam(router, rebroadcastListener, channels, test.tags))) } - test("private local channel") { f => + private def internalTest(f: FixtureParam): Unit = { import f._ - reachNormal(channels, testTags) - - awaitAssert(router.stateData.privateChannels.size == 1) - - { - // only the local channel_update is known (bob won't send his before the channel is deeply buried) - val pc = router.stateData.privateChannels.values.head - assert(pc.update_1_opt.isDefined ^ pc.update_2_opt.isDefined) - } - + val (aliceNodeId, bobNodeId) = (channels.alice.underlyingActor.nodeParams.nodeId, channels.bob.underlyingActor.nodeParams.nodeId) + reachNormal(channels, testTags, interceptChannelUpdates = false) + + // the router learns about the local, still unannounced, channel + awaitCond(router.stateData.privateChannels.size == 1) + + // only alice's channel_update is known (NB : due to how node ids are constructed, 1 = alice and 2 = bob) + assert(privateChannel.update_1_opt.isDefined) + assert(privateChannel.update_2_opt.isEmpty) + // alice will only have a real scid if this is not a zeroconf channel + assert(channels.alice.stateData.asInstanceOf[DATA_NORMAL].shortIds.real.toOption.isEmpty == f.testTags.contains(ChannelStateTestsTags.ZeroConf)) + assert(channels.alice.stateData.asInstanceOf[DATA_NORMAL].shortIds.remoteAlias_opt.isDefined) + // alice uses bob's alias for her channel update + assert(privateChannel.update_1_opt.get.shortChannelId != privateChannel.shortIds.localAlias) + assert(privateChannel.update_1_opt.get.shortChannelId == channels.alice.stateData.asInstanceOf[DATA_NORMAL].shortIds.remoteAlias_opt.get) + + // alice and bob send their channel_updates using remote alias when they go to NORMAL state + val aliceChannelUpdate1 = channels.alice2bob.expectMsgType[ChannelUpdate] + val bobChannelUpdate1 = channels.bob2alice.expectMsgType[ChannelUpdate] + // alice's channel_update uses bob's alias, and vice versa + assert(aliceChannelUpdate1.shortChannelId == channels.bob.stateData.asInstanceOf[DATA_NORMAL].shortIds.localAlias) + assert(bobChannelUpdate1.shortChannelId == channels.alice.stateData.asInstanceOf[DATA_NORMAL].shortIds.localAlias) + // channel_updates are handled by the peer connection and sent to the router val peerConnection = TestProbe() - // bob hasn't yet sent his channel_update but we can get it by looking at its internal data - val bobChannelUpdate = channels.bob.stateData.asInstanceOf[DATA_NORMAL].channelUpdate - router ! PeerRoutingMessage(peerConnection.ref, channels.bob.underlyingActor.nodeParams.nodeId, bobChannelUpdate) + router ! PeerRoutingMessage(peerConnection.ref, bobNodeId, bobChannelUpdate1) - awaitAssert { - val pc = router.stateData.privateChannels.values.head - // both channel_updates are known - pc.update_1_opt.isDefined && pc.update_2_opt.isDefined + // router processes bob's channel_update and now knows both channel updates + awaitCond { + privateChannel.update_1_opt.contains(aliceChannelUpdate1) && privateChannel.update_2_opt.contains(bobChannelUpdate1) } - // manual rebroadcast - router ! Router.TickBroadcast - rebroadcastListener.expectNoMessage() - - } - - test("public local channel", Tag(ChannelStateTestsTags.ChannelsPublic)) { f => - import f._ - - val fundingTx = reachNormal(channels, testTags) - - awaitAssert(router.stateData.privateChannels.size == 1) - - { - val pc = router.stateData.privateChannels.values.head - // only the local channel_update is known - assert(pc.update_1_opt.isDefined ^ pc.update_2_opt.isDefined) + // there is nothing for the router to rebroadcast, channel is not announced + assert(router.stateData.rebroadcast == Rebroadcast(Map.empty, Map.empty, Map.empty)) + + // router graph contains a single channel + assert(router.stateData.graphWithBalances.graph.vertexSet() == Set(aliceNodeId, bobNodeId)) + assert(router.stateData.graphWithBalances.graph.edgeSet().toSet == Set(GraphEdge(aliceChannelUpdate1, privateChannel), GraphEdge(bobChannelUpdate1, privateChannel))) + + if (testTags.contains(ChannelStateTestsTags.ChannelsPublic)) { + // this is a public channel + // funding tx reaches 6 blocks, announcements are exchanged + channels.alice ! WatchFundingDeeplyBuriedTriggered(BlockHeight(400000), 42, null) + channels.alice2bob.expectMsgType[AnnouncementSignatures] + channels.alice2bob.forward(channels.bob) + + channels.bob ! WatchFundingDeeplyBuriedTriggered(BlockHeight(400000), 42, null) + channels.bob2alice.expectMsgType[AnnouncementSignatures] + channels.bob2alice.forward(channels.alice) + + // the router learns about the announcement and channel graduates from private to public + awaitCond { + router.stateData.privateChannels.isEmpty && router.stateData.channels.size == 1 + } + + // router has cleaned up the scid mapping + assert(router.stateData.scid2PrivateChannels.isEmpty) + + // alice and bob won't send their channel_update directly to each other because the channel has been announced + // but we can get the update from their data + awaitCond { + channels.alice.stateData.asInstanceOf[DATA_NORMAL].channelAnnouncement.isDefined && + channels.bob.stateData.asInstanceOf[DATA_NORMAL].channelAnnouncement.isDefined + } + val aliceChannelUpdate2 = channels.alice.stateData.asInstanceOf[DATA_NORMAL].channelUpdate + val bobChannelUpdate2 = channels.bob.stateData.asInstanceOf[DATA_NORMAL].channelUpdate + // this time, they use the real scid + val aliceAnn = channels.alice.stateData.asInstanceOf[DATA_NORMAL].channelAnnouncement.get + val bobAnn = channels.bob.stateData.asInstanceOf[DATA_NORMAL].channelAnnouncement.get + assert(aliceAnn == bobAnn) + assert(aliceChannelUpdate2.shortChannelId == aliceAnn.shortChannelId) + assert(bobChannelUpdate2.shortChannelId == bobAnn.shortChannelId) + + // the router has already processed the new local channel update from alice which uses the real scid, and keeps bob's previous channel update + assert(publicChannel.update_1_opt.contains(aliceChannelUpdate2) && publicChannel.update_2_opt.contains(bobChannelUpdate1)) + + // the router prepares to rebroadcast the channel announcement, the local update which uses the real scid, and the first node announcement + assert(router.stateData.rebroadcast == Rebroadcast( + channels = Map(aliceAnn -> Set[GossipOrigin](LocalGossip)), + updates = Map(aliceChannelUpdate2 -> Set[GossipOrigin](LocalGossip)), + nodes = Map(router.stateData.nodes.values.head -> Set[GossipOrigin](LocalGossip))) + ) + + // bob's channel_update reaches the router + router ! PeerRoutingMessage(peerConnection.ref, bobNodeId, bobChannelUpdate2) + + // router processes bob's channel_update and now knows both channel updates with real scids + awaitCond { + publicChannel.update_1_opt.contains(aliceChannelUpdate2) && publicChannel.update_2_opt.contains(bobChannelUpdate2) + } + + // router is now ready to rebroadcast both channel updates + assert(router.stateData.rebroadcast == Rebroadcast( + channels = Map(aliceAnn -> Set[GossipOrigin](LocalGossip)), + updates = Map( + aliceChannelUpdate2 -> Set[GossipOrigin](LocalGossip), + bobChannelUpdate2 -> Set[GossipOrigin](RemoteGossip(peerConnection.ref, bobNodeId)) + ), + nodes = Map(router.stateData.nodes.values.head -> Set[GossipOrigin](LocalGossip))) + ) + + // router graph contains a single channel + assert(router.stateData.graphWithBalances.graph.vertexSet() == Set(aliceNodeId, bobNodeId)) + assert(router.stateData.graphWithBalances.graph.edgeSet().size == 2) + assert(router.stateData.graphWithBalances.graph.edgeSet().toSet == Set(GraphEdge(aliceChannelUpdate2, publicChannel), GraphEdge(bobChannelUpdate2, publicChannel))) + } else { + // this is a private channel + // funding tx reaches 6 blocks, no announcements are exchanged because the channel is private + channels.alice ! WatchFundingDeeplyBuriedTriggered(BlockHeight(400000), 42, null) + channels.bob ! WatchFundingDeeplyBuriedTriggered(BlockHeight(400000), 42, null) + + // alice and bob won't send their channel_update directly to each other because they haven't changed + channels.alice2bob.expectNoMessage(100 millis) + channels.bob2alice.expectNoMessage(100 millis) + + // router graph contains a single channel + assert(router.stateData.graphWithBalances.graph.vertexSet() == Set(aliceNodeId, bobNodeId)) + assert(router.stateData.graphWithBalances.graph.edgeSet().toSet == Set(GraphEdge(aliceChannelUpdate1, privateChannel), GraphEdge(bobChannelUpdate1, privateChannel))) } - - val peerConnection = TestProbe() - // alice and bob haven't yet sent their channel_updates but we can get them by looking at their internal data - val aliceChannelUpdate = channels.alice.stateData.asInstanceOf[DATA_NORMAL].channelUpdate - val bobChannelUpdate = channels.bob.stateData.asInstanceOf[DATA_NORMAL].channelUpdate - router ! PeerRoutingMessage(peerConnection.ref, channels.bob.underlyingActor.nodeParams.nodeId, bobChannelUpdate) - - awaitAssert { - val pc = router.stateData.privateChannels.values.head - // both channel_updates are known - pc.update_1_opt.isDefined && pc.update_2_opt.isDefined - } - - // funding tx reaches 6 blocks, announcements are exchanged - channels.alice ! WatchFundingDeeplyBuriedTriggered(BlockHeight(400000), 42, null) - channels.alice2bob.expectMsgType[AnnouncementSignatures] + // channel closes + channels.alice ! CMD_CLOSE(TestProbe().ref, scriptPubKey = None, feerates = None) + channels.alice2bob.expectMsgType[Shutdown] channels.alice2bob.forward(channels.bob) - - channels.bob ! WatchFundingDeeplyBuriedTriggered(BlockHeight(400000), 42, null) - channels.bob2alice.expectMsgType[AnnouncementSignatures] + channels.bob2alice.expectMsgType[Shutdown] channels.bob2alice.forward(channels.alice) - - // router gets notified and attempts to validate the local channel - val vr = channels.alice2blockchain.expectMsgType[ZmqWatcher.ValidateRequest] - vr.replyTo ! ZmqWatcher.ValidateResult(vr.ann, Right((fundingTx, ZmqWatcher.UtxoStatus.Unspent))) - - awaitAssert { - router.stateData.privateChannels.isEmpty && router.stateData.channels.size == 1 + if (testTags.contains(ChannelStateTestsTags.ChannelsPublic)) { + // if the channel was public, the router asked the watcher to watch the funding tx and will be notified + val watchSpentBasic = channels.alice2blockchain.expectMsgType[WatchExternalChannelSpent] + watchSpentBasic.replyTo ! WatchExternalChannelSpentTriggered(watchSpentBasic.shortChannelId) } - + // router cleans up the channel awaitAssert { - val pc = router.stateData.channels.values.head - // both channel updates are preserved - pc.update_1_opt.isDefined && pc.update_2_opt.isDefined + assert(router.stateData.nodes == Map.empty) + assert(router.stateData.channels == Map.empty) + assert(router.stateData.privateChannels == Map.empty) + assert(router.stateData.scid2PrivateChannels == Map.empty) + assert(router.stateData.graphWithBalances.graph.edgeSet().isEmpty) + // TODO: we're not currently pruning nodes without channels from the graph, but we should! + // assert(router.stateData.graphWithBalances.graph.vertexSet().isEmpty) } + } + + test("private local channel") { f => + internalTest(f) + } - // manual rebroadcast - router ! Router.TickBroadcast - rebroadcastListener.expectMsg(Router.Rebroadcast( - channels = Map(vr.ann -> Set[GossipOrigin](LocalGossip)), - updates = Map(aliceChannelUpdate -> Set[GossipOrigin](LocalGossip), bobChannelUpdate -> Set.empty[GossipOrigin]), // broadcast the channel_updates (they were previously unannounced) - nodes = Map(router.underlyingActor.stateData.nodes.values.head -> Set[GossipOrigin](LocalGossip)), // new node_announcement - )) + test("private local channel (zeroconf)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs), Tag(ChannelStateTestsTags.ZeroConf)) { f => + internalTest(f) + } + + test("public local channel", Tag(ChannelStateTestsTags.ChannelsPublic)) { f => + internalTest(f) + } + test("public local channel (zeroconf)", Tag(ChannelStateTestsTags.ChannelsPublic), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs), Tag(ChannelStateTestsTags.ZeroConf)) { f => + internalTest(f) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/RouteCalculationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/RouteCalculationSpec.scala index a44a9afb14..aca83a802d 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/RouteCalculationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/RouteCalculationSpec.scala @@ -19,6 +19,7 @@ package fr.acinq.eclair.router import com.softwaremill.quicklens.ModifyPimp import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, ByteVector64, Satoshi, SatoshiLong} +import fr.acinq.eclair.RealShortChannelId import fr.acinq.eclair.payment.Bolt11Invoice.ExtraHop import fr.acinq.eclair.payment.relay.Relayer.RelayFees import fr.acinq.eclair.router.BaseRouterSpec.channelHopFromUpdate @@ -29,7 +30,7 @@ import fr.acinq.eclair.router.RouteCalculation._ import fr.acinq.eclair.router.Router._ import fr.acinq.eclair.transactions.Transactions import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{BlockHeight, CltvExpiryDelta, Features, MilliSatoshi, MilliSatoshiLong, ShortChannelId, TimestampSecond, TimestampSecondLong, ToMilliSatoshiConversion, randomKey} +import fr.acinq.eclair.{BlockHeight, CltvExpiryDelta, Features, MilliSatoshi, MilliSatoshiLong, RealShortChannelId, ShortChannelId, TimestampSecond, TimestampSecondLong, ToMilliSatoshiConversion, randomKey} import org.scalatest.funsuite.AnyFunSuite import org.scalatest.{ParallelTestExecution, Tag} import scodec.bits._ @@ -915,7 +916,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { // This test have a channel (542280x2156x0) that according to heuristics is very convenient but actually useless to reach the target, // then if the cost function is not monotonic the path-finding breaks because the result path contains a loop. val updates = SortedMap( - ShortChannelId("565643x1216x0") -> PublicChannel( + RealShortChannelId(BlockHeight(565643), 1216, 0) -> PublicChannel( ann = makeChannel(ShortChannelId("565643x1216x0").toLong, PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f"), PublicKey(hex"024655b768ef40951b20053a5c4b951606d4d86085d51238f2c67c7dec29c792ca")), fundingTxid = ByteVector32.Zeroes, capacity = DEFAULT_CAPACITY, @@ -923,7 +924,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { update_2_opt = Some(ChannelUpdate(ByteVector64.Zeroes, ByteVector32.Zeroes, ShortChannelId("565643x1216x0"), 0 unixsec, ChannelUpdate.ChannelFlags(isEnabled = true, isNode1 = false), CltvExpiryDelta(144), htlcMinimumMsat = 0 msat, feeBaseMsat = 1000 msat, 100, Some(15000000000L msat))), meta_opt = None ), - ShortChannelId("542280x2156x0") -> PublicChannel( + RealShortChannelId(BlockHeight(542280), 2156, 0) -> PublicChannel( ann = makeChannel(ShortChannelId("542280x2156x0").toLong, PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f"), PublicKey(hex"03cb7983dc247f9f81a0fa2dfa3ce1c255365f7279c8dd143e086ca333df10e278")), fundingTxid = ByteVector32.Zeroes, capacity = DEFAULT_CAPACITY, @@ -931,7 +932,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { update_2_opt = Some(ChannelUpdate(ByteVector64.Zeroes, ByteVector32.Zeroes, ShortChannelId("542280x2156x0"), 0 unixsec, ChannelUpdate.ChannelFlags(isEnabled = true, isNode1 = false), CltvExpiryDelta(144), htlcMinimumMsat = 1 msat, feeBaseMsat = 667 msat, 1, Some(16777000000L msat))), meta_opt = None ), - ShortChannelId("565779x2711x0") -> PublicChannel( + RealShortChannelId(BlockHeight(565779), 2711, 0) -> PublicChannel( ann = makeChannel(ShortChannelId("565779x2711x0").toLong, PublicKey(hex"036d65409c41ab7380a43448f257809e7496b52bf92057c09c4f300cbd61c50d96"), PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f")), fundingTxid = ByteVector32.Zeroes, capacity = DEFAULT_CAPACITY, @@ -1948,7 +1949,7 @@ object RouteCalculationSpec { def makeChannel(shortChannelId: Long, nodeIdA: PublicKey, nodeIdB: PublicKey): ChannelAnnouncement = { val (nodeId1, nodeId2) = if (Announcements.isNode1(nodeIdA, nodeIdB)) (nodeIdA, nodeIdB) else (nodeIdB, nodeIdA) - ChannelAnnouncement(DUMMY_SIG, DUMMY_SIG, DUMMY_SIG, DUMMY_SIG, Features.empty, Block.RegtestGenesisBlock.hash, ShortChannelId(shortChannelId), nodeId1, nodeId2, randomKey().publicKey, randomKey().publicKey) + ChannelAnnouncement(DUMMY_SIG, DUMMY_SIG, DUMMY_SIG, DUMMY_SIG, Features.empty, Block.RegtestGenesisBlock.hash, RealShortChannelId(shortChannelId), nodeId1, nodeId2, randomKey().publicKey, randomKey().publicKey) } def makeEdge(shortChannelId: Long, @@ -1962,7 +1963,7 @@ object RouteCalculationSpec { capacity: Satoshi = DEFAULT_CAPACITY, balance_opt: Option[MilliSatoshi] = None): GraphEdge = { val update = makeUpdateShort(ShortChannelId(shortChannelId), nodeId1, nodeId2, feeBase, feeProportionalMillionth, minHtlc, maxHtlc, cltvDelta) - GraphEdge(ChannelDesc(ShortChannelId(shortChannelId), nodeId1, nodeId2), ChannelRelayParams.FromAnnouncement(update), capacity, balance_opt) + GraphEdge(ChannelDesc(RealShortChannelId(shortChannelId), nodeId1, nodeId2), ChannelRelayParams.FromAnnouncement(update), capacity, balance_opt) } def makeUpdateShort(shortChannelId: ShortChannelId, nodeId1: PublicKey, nodeId2: PublicKey, feeBase: MilliSatoshi, feeProportionalMillionth: Int, minHtlc: MilliSatoshi = DEFAULT_AMOUNT_MSAT, maxHtlc: Option[MilliSatoshi] = None, cltvDelta: CltvExpiryDelta = CltvExpiryDelta(0), timestamp: TimestampSecond = 0 unixsec): ChannelUpdate = diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/RouterSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/RouterSpec.scala index 4c5b9d0eb8..fcd54e96cc 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/RouterSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/RouterSpec.scala @@ -22,8 +22,9 @@ import akka.testkit.TestProbe import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.bitcoin.scalacompat.Script.{pay2wsh, write} import fr.acinq.bitcoin.scalacompat.{Block, SatoshiLong, Transaction, TxOut} +import fr.acinq.eclair.RealShortChannelId import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._ -import fr.acinq.eclair.channel.{AvailableBalanceChanged, CommitmentsSpec, LocalChannelUpdate} +import fr.acinq.eclair.channel.{AvailableBalanceChanged, CommitmentsSpec, LocalChannelUpdate, RealScidStatus, ShortIds} import fr.acinq.eclair.crypto.TransportHandler import fr.acinq.eclair.io.Peer.PeerRoutingMessage import fr.acinq.eclair.payment.Bolt11Invoice.ExtraHop @@ -34,7 +35,7 @@ import fr.acinq.eclair.router.RouteCalculationSpec.{DEFAULT_AMOUNT_MSAT, DEFAULT import fr.acinq.eclair.router.Router._ import fr.acinq.eclair.transactions.Scripts import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{BlockHeight, CltvExpiryDelta, Features, MilliSatoshi, MilliSatoshiLong, ShortChannelId, TestConstants, TimestampSecond, nodeFee, randomKey} +import fr.acinq.eclair.{BlockHeight, CltvExpiryDelta, Features, MilliSatoshi, MilliSatoshiLong, RealShortChannelId, ShortChannelId, TestConstants, TimestampSecond, nodeFee, randomKey} import scodec.bits._ import scala.concurrent.duration._ @@ -54,7 +55,7 @@ class RouterSpec extends BaseRouterSpec { { // valid channel announcement, no stashing - val chan_ac = channelAnnouncement(ShortChannelId(BlockHeight(420000), 5, 0), priv_a, priv_c, priv_funding_a, priv_funding_c) + val chan_ac = channelAnnouncement(RealShortChannelId(BlockHeight(420000), 5, 0), priv_a, priv_c, priv_funding_a, priv_funding_c) val update_ac = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_a, c, chan_ac.shortChannelId, CltvExpiryDelta(7), 0 msat, 766000 msat, 10, htlcMaximum) val node_c = makeNodeAnnouncement(priv_c, "node-C", Color(123, 100, -40), Nil, TestConstants.Bob.nodeParams.features.nodeAnnouncementFeatures(), timestamp = TimestampSecond.now() + 1) peerConnection.send(router, PeerRoutingMessage(peerConnection.ref, remoteNodeId, chan_ac)) @@ -84,7 +85,7 @@ class RouterSpec extends BaseRouterSpec { // valid channel announcement, stashing while validating channel announcement val priv_u = randomKey() val priv_funding_u = randomKey() - val chan_uc = channelAnnouncement(ShortChannelId(BlockHeight(420000), 100, 0), priv_u, priv_c, priv_funding_u, priv_funding_c) + val chan_uc = channelAnnouncement(RealShortChannelId(BlockHeight(420000), 100, 0), priv_u, priv_c, priv_funding_u, priv_funding_c) val update_uc = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_u, c, chan_uc.shortChannelId, CltvExpiryDelta(7), 0 msat, 766000 msat, 10, htlcMaximum) val node_u = makeNodeAnnouncement(priv_u, "node-U", Color(-120, -20, 60), Nil, Features.empty) peerConnection.send(router, PeerRoutingMessage(peerConnection.ref, remoteNodeId, chan_uc)) @@ -129,7 +130,7 @@ class RouterSpec extends BaseRouterSpec { { // invalid signatures val invalid_node_b = node_b.copy(timestamp = node_b.timestamp + 10) - val invalid_chan_ac = channelAnnouncement(ShortChannelId(BlockHeight(420000), 101, 1), priv_a, priv_c, priv_funding_a, priv_funding_c).copy(nodeId1 = randomKey().publicKey) + val invalid_chan_ac = channelAnnouncement(RealShortChannelId(BlockHeight(420000), 101, 1), priv_a, priv_c, priv_funding_a, priv_funding_c).copy(nodeId1 = randomKey().publicKey) val invalid_update_ab = update_ab.copy(cltvExpiryDelta = CltvExpiryDelta(21), timestamp = update_ab.timestamp + 1) peerConnection.send(router, PeerRoutingMessage(peerConnection.ref, remoteNodeId, invalid_node_b)) peerConnection.expectMsg(TransportHandler.ReadAck(invalid_node_b)) @@ -149,7 +150,7 @@ class RouterSpec extends BaseRouterSpec { // pruned channel val priv_v = randomKey() val priv_funding_v = randomKey() - val chan_vc = channelAnnouncement(ShortChannelId(BlockHeight(420000), 102, 0), priv_v, priv_c, priv_funding_v, priv_funding_c) + val chan_vc = channelAnnouncement(RealShortChannelId(BlockHeight(420000), 102, 0), priv_v, priv_c, priv_funding_v, priv_funding_c) nodeParams.db.network.addToPruned(chan_vc.shortChannelId :: Nil) peerConnection.send(router, PeerRoutingMessage(peerConnection.ref, remoteNodeId, chan_vc)) peerConnection.expectMsg(TransportHandler.ReadAck(chan_vc)) @@ -190,7 +191,7 @@ class RouterSpec extends BaseRouterSpec { // invalid announcement + reject stashed val priv_y = randomKey() val priv_funding_y = randomKey() // a-y will have an invalid script - val chan_ay = channelAnnouncement(ShortChannelId(42002), priv_a, priv_y, priv_funding_a, priv_funding_y) + val chan_ay = channelAnnouncement(RealShortChannelId(42002), priv_a, priv_y, priv_funding_a, priv_funding_y) val update_ay = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_a, priv_y.publicKey, chan_ay.shortChannelId, CltvExpiryDelta(7), 0 msat, 766000 msat, 10, htlcMaximum) val node_y = makeNodeAnnouncement(priv_y, "node-Y", Color(123, 100, -40), Nil, TestConstants.Bob.nodeParams.features.nodeAnnouncementFeatures()) peerConnection.send(router, PeerRoutingMessage(peerConnection.ref, remoteNodeId, chan_ay)) @@ -212,7 +213,7 @@ class RouterSpec extends BaseRouterSpec { { // validation failure val priv_x = randomKey() - val chan_ax = channelAnnouncement(ShortChannelId(42001), priv_a, priv_x, priv_funding_a, randomKey()) + val chan_ax = channelAnnouncement(RealShortChannelId(42001), priv_a, priv_x, priv_funding_a, randomKey()) peerConnection.send(router, PeerRoutingMessage(peerConnection.ref, remoteNodeId, chan_ax)) assert(watcher.expectMsgType[ValidateRequest].ann == chan_ax) watcher.send(router, ValidateResult(chan_ax, Left(new RuntimeException("funding tx not found")))) @@ -227,7 +228,7 @@ class RouterSpec extends BaseRouterSpec { // funding tx spent (funding tx not confirmed) val priv_z = randomKey() val priv_funding_z = randomKey() - val chan_az = channelAnnouncement(ShortChannelId(42003), priv_a, priv_z, priv_funding_a, priv_funding_z) + val chan_az = channelAnnouncement(RealShortChannelId(42003), priv_a, priv_z, priv_funding_a, priv_funding_z) peerConnection.send(router, PeerRoutingMessage(peerConnection.ref, remoteNodeId, chan_az)) assert(watcher.expectMsgType[ValidateRequest].ann == chan_az) watcher.send(router, ValidateResult(chan_az, Right(Transaction(version = 0, txIn = Nil, txOut = TxOut(1000000 sat, write(pay2wsh(Scripts.multiSig2of2(funding_a, priv_funding_z.publicKey)))) :: Nil, lockTime = 0), UtxoStatus.Spent(spendingTxConfirmed = false)))) @@ -242,7 +243,7 @@ class RouterSpec extends BaseRouterSpec { // funding tx spent (funding tx confirmed) val priv_z = randomKey() val priv_funding_z = randomKey() - val chan_az = channelAnnouncement(ShortChannelId(42003), priv_a, priv_z, priv_funding_a, priv_funding_z) + val chan_az = channelAnnouncement(RealShortChannelId(42003), priv_a, priv_z, priv_funding_a, priv_funding_z) peerConnection.send(router, PeerRoutingMessage(peerConnection.ref, remoteNodeId, chan_az)) assert(watcher.expectMsgType[ValidateRequest].ann == chan_az) watcher.send(router, ValidateResult(chan_az, Right(Transaction(version = 0, txIn = Nil, txOut = TxOut(1000000 sat, write(pay2wsh(Scripts.multiSig2of2(funding_a, priv_funding_z.publicKey)))) :: Nil, lockTime = 0), UtxoStatus.Spent(spendingTxConfirmed = true)))) @@ -285,7 +286,7 @@ class RouterSpec extends BaseRouterSpec { test("handle bad signature for ChannelAnnouncement") { fixture => import fixture._ val peerConnection = TestProbe() - val channelId_ac = ShortChannelId(BlockHeight(420000), 105, 0) + val channelId_ac = RealShortChannelId(BlockHeight(420000), 105, 0) val chan_ac = channelAnnouncement(channelId_ac, priv_a, priv_c, priv_funding_a, priv_funding_c) val buggy_chan_ac = chan_ac.copy(nodeSignature1 = chan_ac.nodeSignature2) peerConnection.send(router, PeerRoutingMessage(peerConnection.ref, remoteNodeId, buggy_chan_ac)) @@ -388,8 +389,8 @@ class RouterSpec extends BaseRouterSpec { assert(res.routes.head.hops.map(_.nodeId).toList == a :: g :: Nil) assert(res.routes.head.hops.last.nextNodeId == h) - val channelUpdate_ag1 = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_a, g, scid_ag_private, CltvExpiryDelta(7), 0 msat, 10 msat, 10, htlcMaximum, enable = false) - sender.send(router, LocalChannelUpdate(sender.ref, null, scid_ag_private, g, None, channelUpdate_ag1, CommitmentsSpec.makeCommitments(10000 msat, 15000 msat, a, g, announceChannel = false))) + val channelUpdate_ag1 = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_a, g, alias_ag_private, CltvExpiryDelta(7), 0 msat, 10 msat, 10, htlcMaximum, enable = false) + sender.send(router, LocalChannelUpdate(sender.ref, channelId_ag_private, scids_ag_private, g, None, channelUpdate_ag1, CommitmentsSpec.makeCommitments(10000 msat, 15000 msat, a, g, announceChannel = false))) sender.send(router, RouteRequest(a, h, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, routeParams = DEFAULT_ROUTE_PARAMS)) sender.expectMsg(Failure(RouteNotFound)) } @@ -410,7 +411,7 @@ class RouterSpec extends BaseRouterSpec { sender.send(router, RouteRequest(a, b, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, routeParams = DEFAULT_ROUTE_PARAMS)) sender.expectMsgType[RouteResponse] val commitments1 = CommitmentsSpec.makeCommitments(10000000 msat, 20000000 msat, a, b, announceChannel = true) - sender.send(router, LocalChannelUpdate(sender.ref, null, scid_ab, b, Some(chan_ab), update_ab, commitments1)) + sender.send(router, LocalChannelUpdate(sender.ref, null, scids_ab, b, Some(chan_ab), update_ab, commitments1)) sender.send(router, RouteRequest(a, b, 12000000 msat, Long.MaxValue.msat, routeParams = DEFAULT_ROUTE_PARAMS)) sender.expectMsg(Failure(BalanceTooLow)) sender.send(router, RouteRequest(a, b, 12000000 msat, Long.MaxValue.msat, allowMultiPart = true, routeParams = DEFAULT_ROUTE_PARAMS)) @@ -484,6 +485,18 @@ class RouterSpec extends BaseRouterSpec { val sender = TestProbe() { + // using the channel alias + val preComputedRoute = PredefinedChannelRoute(g, Seq(alias_ag_private)) + sender.send(router, FinalizeRoute(10000 msat, preComputedRoute)) + val response = sender.expectMsgType[RouteResponse] + assert(response.routes.length == 1) + val route = response.routes.head + assert(route.hops.map(_.params) == Seq(ChannelRelayParams.FromAnnouncement(update_ag_private))) + assert(route.hops.head.nodeId == a) + assert(route.hops.head.nextNodeId == g) + } + { + // using the real scid val preComputedRoute = PredefinedChannelRoute(g, Seq(scid_ag_private)) sender.send(router, FinalizeRoute(10000 msat, preComputedRoute)) val response = sender.expectMsgType[RouteResponse] @@ -511,7 +524,7 @@ class RouterSpec extends BaseRouterSpec { val targetNodeId = randomKey().publicKey { - val invoiceRoutingHint = ExtraHop(b, ShortChannelId(BlockHeight(420000), 516, 1105), 10 msat, 150, CltvExpiryDelta(96)) + val invoiceRoutingHint = ExtraHop(b, RealShortChannelId(BlockHeight(420000), 516, 1105), 10 msat, 150, CltvExpiryDelta(96)) val preComputedRoute = PredefinedChannelRoute(targetNodeId, Seq(scid_ab, invoiceRoutingHint.shortChannelId)) val amount = 10_000.msat // the amount affects the way we estimate the channel capacity of the hinted channel @@ -526,7 +539,7 @@ class RouterSpec extends BaseRouterSpec { assert(route.hops.last.params == ChannelRelayParams.FromHint(invoiceRoutingHint, RoutingHeuristics.CAPACITY_CHANNEL_LOW + nodeFee(invoiceRoutingHint.feeBase, invoiceRoutingHint.feeProportionalMillionths, RoutingHeuristics.CAPACITY_CHANNEL_LOW))) } { - val invoiceRoutingHint = ExtraHop(h, ShortChannelId(BlockHeight(420000), 516, 1105), 10 msat, 150, CltvExpiryDelta(96)) + val invoiceRoutingHint = ExtraHop(h, RealShortChannelId(BlockHeight(420000), 516, 1105), 10 msat, 150, CltvExpiryDelta(96)) val preComputedRoute = PredefinedChannelRoute(targetNodeId, Seq(scid_ag_private, scid_gh, invoiceRoutingHint.shortChannelId)) val amount = RoutingHeuristics.CAPACITY_CHANNEL_LOW * 2 // the amount affects the way we estimate the channel capacity of the hinted channel @@ -571,7 +584,7 @@ class RouterSpec extends BaseRouterSpec { test("ask for channels that we marked as stale for which we receive a new update") { fixture => import fixture._ val blockHeight = BlockHeight(400000) - 2020 - val channelId = ShortChannelId(blockHeight, 5, 0) + val channelId = RealShortChannelId(blockHeight, 5, 0) val announcement = channelAnnouncement(channelId, priv_a, priv_c, priv_funding_a, priv_funding_c) val oldTimestamp = TimestampSecond.now() - 14.days - 1.day val staleUpdate = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_a, c, channelId, CltvExpiryDelta(7), 0 msat, 766000 msat, 10, 5 msat, timestamp = oldTimestamp) @@ -611,7 +624,7 @@ class RouterSpec extends BaseRouterSpec { // When the local channel comes back online, it will send a LocalChannelUpdate to the router. val balances = Set[Option[MilliSatoshi]](Some(10000 msat), Some(15000 msat)) val commitments = CommitmentsSpec.makeCommitments(10000 msat, 15000 msat, a, b, announceChannel = true) - sender.send(router, LocalChannelUpdate(sender.ref, null, scid_ab, b, Some(chan_ab), update_ab, commitments)) + sender.send(router, LocalChannelUpdate(sender.ref, null, scids_ab, b, Some(chan_ab), update_ab, commitments)) sender.send(router, GetRoutingState) val channel_ab = sender.expectMsgType[RoutingState].channels.find(_.ann == chan_ab).get assert(Set(channel_ab.meta_opt.map(_.balance1), channel_ab.meta_opt.map(_.balance2)) == balances) @@ -634,7 +647,7 @@ class RouterSpec extends BaseRouterSpec { // Then we update the balance without changing the contents of the channel update; the graph should still be updated. val balances = Set[Option[MilliSatoshi]](Some(11000 msat), Some(14000 msat)) val commitments = CommitmentsSpec.makeCommitments(11000 msat, 14000 msat, a, b, announceChannel = true) - sender.send(router, LocalChannelUpdate(sender.ref, null, scid_ab, b, Some(chan_ab), update_ab, commitments)) + sender.send(router, LocalChannelUpdate(sender.ref, null, scids_ab, b, Some(chan_ab), update_ab, commitments)) sender.send(router, GetRoutingState) val channel_ab = sender.expectMsgType[RoutingState].channels.find(_.ann == chan_ab).get assert(Set(channel_ab.meta_opt.map(_.balance1), channel_ab.meta_opt.map(_.balance2)) == balances) @@ -652,7 +665,7 @@ class RouterSpec extends BaseRouterSpec { // When HTLCs are relayed through the channel, balance changes are sent to the router. val balances = Set[Option[MilliSatoshi]](Some(12000 msat), Some(13000 msat)) val commitments = CommitmentsSpec.makeCommitments(12000 msat, 13000 msat, a, b, announceChannel = true) - sender.send(router, AvailableBalanceChanged(sender.ref, null, scid_ab, commitments)) + sender.send(router, AvailableBalanceChanged(sender.ref, null, scids_ab, commitments)) sender.send(router, GetRoutingState) val channel_ab = sender.expectMsgType[RoutingState].channels.find(_.ann == chan_ab).get assert(Set(channel_ab.meta_opt.map(_.balance1), channel_ab.meta_opt.map(_.balance2)) == balances) @@ -670,13 +683,13 @@ class RouterSpec extends BaseRouterSpec { // Private channels should also update the graph when HTLCs are relayed through them. val balances = Set(33000000 msat, 5000000 msat) val commitments = CommitmentsSpec.makeCommitments(33000000 msat, 5000000 msat, a, g, announceChannel = false) - sender.send(router, AvailableBalanceChanged(sender.ref, channelId_ag_private, scid_ag_private, commitments)) + sender.send(router, AvailableBalanceChanged(sender.ref, channelId_ag_private, scids_ab, commitments)) sender.send(router, Router.GetRouterData) val data = sender.expectMsgType[Data] val channel_ag = data.privateChannels(channelId_ag_private) assert(Set(channel_ag.meta.balance1, channel_ag.meta.balance2) == balances) // And the graph should be updated too. - val edge_ag = data.graphWithBalances.graph.getEdge(ChannelDesc(scid_ag_private, a, g)).get + val edge_ag = data.graphWithBalances.graph.getEdge(ChannelDesc(alias_ag_private, a, g)).get assert(edge_ag.capacity == channel_ag.capacity) assert(edge_ag.balance_opt == Some(33000000 msat)) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncSpec.scala index 5abee91aba..987c5f22d2 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncSpec.scala @@ -22,6 +22,7 @@ import akka.testkit.{TestFSMRef, TestProbe} import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, Satoshi, Script, Transaction, TxIn, TxOut} import fr.acinq.eclair.TestConstants.{Alice, Bob} +import fr.acinq.eclair.RealShortChannelId import fr.acinq.eclair._ import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.{UtxoStatus, ValidateRequest, ValidateResult} import fr.acinq.eclair.crypto.TransportHandler @@ -35,7 +36,7 @@ import fr.acinq.eclair.wire.protocol._ import org.scalatest.ParallelTestExecution import org.scalatest.funsuite.AnyFunSuiteLike -import scala.collection.immutable.{TreeMap, SortedSet} +import scala.collection.immutable.{SortedSet, TreeMap} import scala.collection.mutable import scala.concurrent.duration._ @@ -45,10 +46,10 @@ class RoutingSyncSpec extends TestKitBaseClass with AnyFunSuiteLike with Paralle // this map will store private keys so that we can sign new announcements at will val pub2priv: mutable.Map[PublicKey, PrivateKey] = mutable.HashMap.empty - val fakeRoutingInfo: TreeMap[ShortChannelId, (PublicChannel, NodeAnnouncement, NodeAnnouncement)] = RoutingSyncSpec + val fakeRoutingInfo: TreeMap[RealShortChannelId, (PublicChannel, NodeAnnouncement, NodeAnnouncement)] = RoutingSyncSpec .shortChannelIds .take(60) - .foldLeft(TreeMap.empty[ShortChannelId, (PublicChannel, NodeAnnouncement, NodeAnnouncement)]) { + .foldLeft(TreeMap.empty[RealShortChannelId, (PublicChannel, NodeAnnouncement, NodeAnnouncement)]) { case (m, shortChannelId) => m + (shortChannelId -> makeFakeRoutingInfo(pub2priv)(shortChannelId)) } @@ -127,7 +128,7 @@ class RoutingSyncSpec extends TestKitBaseClass with AnyFunSuiteLike with Paralle SyncResult(rcrs, queries, channels, updates, nodes) } - def countUpdates(channels: Map[ShortChannelId, PublicChannel]): Int = channels.values.foldLeft(0) { + def countUpdates(channels: Map[RealShortChannelId, PublicChannel]): Int = channels.values.foldLeft(0) { case (count, pc) => count + pc.update_1_opt.map(_ => 1).getOrElse(0) + pc.update_2_opt.map(_ => 1).getOrElse(0) } @@ -310,7 +311,7 @@ class RoutingSyncSpec extends TestKitBaseClass with AnyFunSuiteLike with Paralle test("sync progress") { - def req = QueryShortChannelIds(Block.RegtestGenesisBlock.hash, EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(ShortChannelId(42))), TlvStream.empty) + def req = QueryShortChannelIds(Block.RegtestGenesisBlock.hash, EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(RealShortChannelId(42))), TlvStream.empty) val nodeIdA = randomKey().publicKey val nodeIdB = randomKey().publicKey @@ -331,15 +332,15 @@ class RoutingSyncSpec extends TestKitBaseClass with AnyFunSuiteLike with Paralle object RoutingSyncSpec { - lazy val shortChannelIds: SortedSet[ShortChannelId] = (for { + lazy val shortChannelIds: SortedSet[RealShortChannelId] = (for { block <- 400000 to 420000 txindex <- 0 to 5 outputIndex <- 0 to 1 - } yield ShortChannelId(BlockHeight(block), txindex, outputIndex)).foldLeft(SortedSet.empty[ShortChannelId])(_ + _) + } yield RealShortChannelId(BlockHeight(block), txindex, outputIndex)).foldLeft(SortedSet.empty[RealShortChannelId])(_ + _) val unused: PrivateKey = randomKey() - def makeFakeRoutingInfo(pub2priv: mutable.Map[PublicKey, PrivateKey])(shortChannelId: ShortChannelId): (PublicChannel, NodeAnnouncement, NodeAnnouncement) = { + def makeFakeRoutingInfo(pub2priv: mutable.Map[PublicKey, PrivateKey])(shortChannelId: RealShortChannelId): (PublicChannel, NodeAnnouncement, NodeAnnouncement) = { val timestamp = TimestampSecond.now() val (priv1, priv2) = { val (priv_a, priv_b) = (randomKey(), randomKey()) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/ChannelCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/ChannelCodecsSpec.scala index fe3e7663f7..0d82508f50 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/ChannelCodecsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/ChannelCodecsSpec.scala @@ -18,6 +18,7 @@ package fr.acinq.eclair.wire.internal.channel import fr.acinq.bitcoin.scalacompat.Crypto.PrivateKey import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, ByteVector64, Crypto, DeterministicWallet, Satoshi, SatoshiLong, Transaction, TxIn} +import fr.acinq.eclair.RealShortChannelId import fr.acinq.eclair._ import fr.acinq.eclair.blockchain.fee.FeeratePerKw import fr.acinq.eclair.channel.Helpers.Funding @@ -124,7 +125,7 @@ class ChannelCodecsSpec extends AnyFunSuite { // and we encode with new codec val newbin = channelDataCodec.encode(oldnormal).require.bytes // make sure that encoding used the new codec - assert(newbin.startsWith(hex"030007")) + assert(newbin.startsWith(hex"030009")) // make sure that round-trip yields the same data val newnormal = channelDataCodec.decode(newbin.bits).require.value assert(newnormal == oldnormal) @@ -135,11 +136,11 @@ class ChannelCodecsSpec extends AnyFunSuite { // this test makes sure that we actually produce the same objects than previous versions of eclair val refs = Map( hex"00000303933884AAF1D6B108397E5EFE5C86BCF2D8CA8D2F700EDA99DB9214FC2712B134000456E4167E3C0EB8C856C79CA31C97C0AA0000000000000222000000012A05F2000000000000028F5C000000000000000102D0001E000BD48A2402E80B723C42EE3E42938866EC6686ABB7ABF64380000000C501A7F2974C5074E9E10DBB3F0D9B8C40932EC63ABC610FAD7EB6B21C6D081A459B000000000000011E80000001EEFFFE5C00000000000147AE00000000000001F403F000F18146779F781067ED04B4957E14F1C5623AB653039B2B1D49910240848E4E682DB20131AD64F76FAF90CD7DE26892F1BDAB82FB9E02EF6538D82FF4204B5348F02AE081A5388E9474769D69C4F60A763AE0CCDB5228A06281DE64408871A927297FDFD8818B6383985ABD4F0AC22E73791CF3A4D63C592FA2648242D34B8334B1539E823381BB1F1404C37D9C2318F5FC6B1BF7ECF5E6835B779E3BE09BADCF6DF1F51DCFBC80000000C0808000000000000EFD80000000007F00000000061A0A4880000001EDE5F3C3801203B9C79160B5123CADE575E370480B57B0C816EB35DD7B27372EDA858622EB1E808000000015FFFFFF800000000011001029DFB814F6502A68D6F83B6049E3D2948A2080084083750626532FDB437169C20023A9108146779F781067ED04B4957E14F1C5623AB653039B2B1D49910240848E4E682DB21081B30694071254D8B3B9537320C014B8CB1052E5514F5EFC19CF2EB806308D5CF1A95700AD0100000000008083B9C79160B5123CADE575E370480B57B0C816EB35DD7B27372EDA858622EB1E80800000001961B4C001618F8180000000001100102E648BA30998A28C02C2DFD9DDCD0E0BA064DA199C55186485AFAB296B94E704426FFE00000000000B000A67D9B9FAADB91650E0146B1F742E5C16006708890200239822011026A6925C659D006FEB42D639F1E42DD13224EE49AA34E71B612CF96DB66A8CD4011032C22F653C54CC5E41098227427650644266D80DED45B7387AE0FFC10E529C4680A418228110807CB47D9C1A14CB832FB361C398EA672C9542F34A90BAD4288FA6AC5FC9E9845C01101CF71CAE9252D389135D8C606225DCF1E0333CCDF1FAE84B74FC5D3D440C25F880A3A9108146779F781067ED04B4957E14F1C5623AB653039B2B1D49910240848E4E682DB21081B30694071254D8B3B9537320C014B8CB1052E5514F5EFC19CF2EB806308D5CF1A9573D7C531000000000000000000F3180000000007F00000001EDE5F3C380000000061A0A48D64CA627B243AD5915A2E5D0BAD026762028DDF3304992B83A26D6C11735FC5F01ED56D769BDE7F6A068AF1A4BCFDF950321F3A4744B01B1DDC7498677F112AE1A80000000000000000000000000000000000000658000000000000819800040D37301C10C9419287E9A3B704EB6D7F45CC145DD77DCE8A63B0A47C8AB67467D800901DCE3C8B05A891E56F2BAF1B82405ABD8640B759AEEBD939B976D42C311758F40400000000AFFFFFFC00000000008800814EFDC0A7B2815346B7C1DB024F1E94A451040042041BA83132997EDA1B8B4E10011D48840A33BCFBC0833F6825A4ABF0A78E2B11D5B2981CD958EA4C881204247273416D90840D9834A03892A6C59DCA9B990600A5C65882972A8A7AF7E0CE7975C031846AE78D4AB8002000EC0003FFFFFFFF86801076D98A575A4CDFD0E3F44D1BB3CD3BBAF3BD04C38FED439ED90D88DF932A9296801A80007FFFFFFFF4008136A9D5896669E8724C5120FB6B36C241EF3CEF68AE0316161F04A9EE3EAFF36000FC0003FFFFFFFF86780106E4B5CC4155733A2427082907338051A5DA1E7CA6432840A5528ECAFFA3FB628801B80007FFFFFFFF10020CA4E125E9126107745D4354D4187ABCDE323117857A1DCEB7CCF60B2AAFA80C6003A0000FFFFFFFFE1C0080981575FD981A73A848CC0243CB467BF451F6811DAF4D71CAD8CE8B1E96DB190C01000003FFFFFFFF867400814C747E0FD8290BE8A3B8B3F73015A261479A71780CD3A0A9270234E4B394409C00D80003FFFFFFFF90020E1B9C9B10A97F15F5E1BB27FC8AC670DF8DADEAE4EDFAFB23BDD0AC705FDF51600340000FFFFFFFFF0020AD2581F3494A17B0BE3F63516D53F028A204FD3156D8B21AA4E57A8738D2062080007FFFFFFFF0CE83B9C79160B5123CADE575E370480B57B0C816EB35DD7B27372EDA858622EB1E0B8C1E00000B8000FA46CC2C7E9AB4A37C64216CD65C944E6D73998419D1A1AD2827AB6BC85B32280230764E374064EC82A3751E789607E23BEAE93FB0EDDD5E7FA803767079662E80EAEF384E2AFCB68049D9DC246119E77BD2ED4112330760CAB6CD3671CFCE006C584B9C95E0B554261E00154D40806EA694F44751B328A9291BAD124EFD5664280936EC92D27B242737E7E3E83B4704BA367B7DA5108F2F6EDFB1C38EE721A369E77EED71B12090BAEAAAC322C1457E31AB0C4DE5D9351943F10FD747742616A1AABD09F680B37D4105A8872695EE9B97FAB8985FAA9D747D45046229BF265CEEB300A40FE23040C5F335E0515496C58EE47418B72331FCC6F47A31A9B33B8E000008692FFAFF04D2AE211E9461FB39D875D74F32E4109D21D5A03D46612000000002E307800002E0002069FCA5D3141D3A78436ECFC366E31024CBB18EAF1843EB5FADAC871B42069166C0726710955E3AD621072FCBDFCB90D79E5B1951A5EE01DB533B72429F84E2562680519DE7DE0419FB412D255F853C71588EAD94C0E6CAC7526440902123939A0B6C806CC1A501C495362CEE54DCC830052E32C414B95453D7BF0673CBAE018C23573C69C694A8F88483050257A7366B838489731E5776B6FA0F02573401176D3E7FAEEF11E95A671420586631255F51A0EC2CF4D4D9F69D587712070FE1FB9316B71868692FFAFF04D2AE211E9461FB39D875D74F32E4109D21D5A03D46612000000002E307800002E0002BA11BBBA0202012000000000000007D0000007D0000000C800000007CFFFF83000" - -> """{"type":"DATA_NORMAL","commitments":{"channelId":"07738f22c16a24795bcaebc6e09016af61902dd66bbaf64e6e5db50b0c45d63c","channelConfig":[],"channelFeatures":[],"localParams":{"nodeId":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","fundingKeyPath":{"path":[1457788542,1007597768,1455922339,479707306]},"dustLimit":546,"maxHtlcValueInFlightMsat":5000000000,"requestedChannelReserve_opt":167772,"htlcMinimum":1,"toSelfDelay":720,"maxAcceptedHtlcs":30,"isInitiator":false,"defaultFinalScriptPubKey":"a9144805d016e47885dc7c852710cdd8cd0d576f57ec87","initFeatures":{"activated":{"option_data_loss_protect":"optional","initial_routing_sync":"optional","gossip_queries":"optional"},"unknown":[]}},"remoteParams":{"nodeId":"034fe52e98a0e9d3c21b767e1b371881265d8c7578c21f5afd6d6438da10348b36","dustLimit":573,"maxHtlcValueInFlightMsat":16609443000,"requestedChannelReserve_opt":167772,"htlcMinimum":1000,"toSelfDelay":2016,"maxAcceptedHtlcs":483,"fundingPubKey":"028cef3ef020cfda09692afc29e38ac4756ca60736563a93220481091c9cd05b64","revocationBasepoint":"02635ac9eedf5f219afbc4d125e37b5705f73c05deca71b05fe84096a691e055c1","paymentBasepoint":"034a711d28e8ed3ad389ec14ec75c199b6a45140c503bcc88110e3524e52ffbfb1","delayedPaymentBasepoint":"0316c70730b57a9e15845ce6f239e749ac78b25f44c90485a697066962a73d0467","htlcBasepoint":"03763e280986fb384631ebf8d637efd9ebcd06b6ef3c77c1375b9edbe3ea3b9f79","initFeatures":{"activated":{"option_data_loss_protect":"mandatory","gossip_queries":"optional"},"unknown":[]}},"channelFlags":{"announceChannel":true},"localCommit":{"index":7675,"spec":{"htlcs":[],"commitTxFeerate":254,"toLocal":204739729,"toRemote":16572475271},"commitTxAndRemoteSig":{"commitTx":{"txid":"e25a866b79212015e01e155e530fb547abc8276869f8740a9948e52ca231f1e4","tx":"020000000107738f22c16a24795bcaebc6e09016af61902dd66bbaf64e6e5db50b0c45d63d010000000032c3698002c31f0300000000002200205cc91746133145180585bfb3bb9a1c1740c9b43338aa30c90b5f5652d729ce0884dffc0000000000160014cfb373f55b722ca1c028d63ee85cb82c00ce11127af8a620"},"remoteSig":"4d4d24b8cb3a00dfd685ac73e3c85ba26449dc935469ce36c259f2db6cd519a865845eca78a998bc8213044e84eca0c884cdb01bda8b6e70f5c1ff821ca5388d"},"htlcTxsAndRemoteSigs":[]},"remoteCommit":{"index":7779,"spec":{"htlcs":[],"commitTxFeerate":254,"toLocal":16572475271,"toRemote":204739729},"txid":"ac994c4f64875ab22b45cba175a04cec4051bbe660932570744dad822e6bf8be","remotePerCommitmentPoint":"03daadaed37bcfed40d15e34979fbf2a0643e748e8960363bb8e930cefe2255c35"},"localChanges":{"proposed":[],"signed":[],"acked":[]},"remoteChanges":{"proposed":[],"acked":[],"signed":[]},"localNextHtlcId":203,"remoteNextHtlcId":4147,"originChannels":{},"remoteNextCommitInfo":"034dcc0704325064a1fa68edc13adb5fd173051775df73a298ec291f22ad9d19f6","commitInput":{"outPoint":"3dd6450c0bb55d6e4ef6ba6bd62d9061af1690e0c6ebca5b79246ac1228f7307:1","amountSatoshis":16777215},"remotePerCommitmentSecrets":null},"shortChannelId":"1513532x23x1","buried":true,"channelAnnouncement":{"nodeSignature1":"d2366163f4d5a51be3210b66b2e4a2736b9ccc20ce8d0d69413d5b5e42d991401183b271ba032764151ba8f3c4b03f11df5749fd876eeaf3fd401bb383cb3174","nodeSignature2":"075779c27157e5b4024ecee12308cf3bde976a0891983b0655b669b38e7e700362c25ce4af05aaa130f000aa6a04037534a7a23a8d99454948dd689277eab321","bitcoinSignature1":"4049b7649693d92139bf3f1f41da3825d1b3dbed2884797b76fd8e1c77390d1b4f3bf76b8d890485d7555619160a2bf18d58626f2ec9a8ca1f887eba3ba130b5","bitcoinSignature2":"0d55e84fb4059bea082d443934af74dcbfd5c4c2fd54eba3ea2823114df932e7759805207f1182062f99af028aa4b62c7723a0c5b9198fe637a3d18d4d99dc70","features":{"activated":{},"unknown":[]},"chainHash":"43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000","shortChannelId":"1513532x23x1","nodeId1":"034fe52e98a0e9d3c21b767e1b371881265d8c7578c21f5afd6d6438da10348b36","nodeId2":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","bitcoinKey1":"028cef3ef020cfda09692afc29e38ac4756ca60736563a93220481091c9cd05b64","bitcoinKey2":"03660d280e24a9b16772a6e6418029719620a5caa29ebdf8339e5d700c611ab9e3","tlvStream":{"records":[],"unknown":[]}},"channelUpdate":{"signature":"4e34a547c424182812bd39b35c1c244b98f2bbb5b7d07812b9a008bb69f3fd77788f4ad338a102c331892afa8d076167a6a6cfb4eac3b890387f0fdc98b5b8c3","chainHash":"43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000","shortChannelId":"1513532x23x1","timestamp":{"iso":"2019-06-18T12:49:33Z","unix":1560862173},"channelFlags":{"isEnabled":true,"isNode1":false},"cltvExpiryDelta":144,"htlcMinimumMsat":1000,"feeBaseMsat":1000,"feeProportionalMillionths":100,"htlcMaximumMsat":16777215000,"tlvStream":{"records":[],"unknown":[]}}}""", + -> """{"type":"DATA_NORMAL","commitments":{"channelId":"07738f22c16a24795bcaebc6e09016af61902dd66bbaf64e6e5db50b0c45d63c","channelConfig":[],"channelFeatures":[],"localParams":{"nodeId":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","fundingKeyPath":{"path":[1457788542,1007597768,1455922339,479707306]},"dustLimit":546,"maxHtlcValueInFlightMsat":5000000000,"requestedChannelReserve_opt":167772,"htlcMinimum":1,"toSelfDelay":720,"maxAcceptedHtlcs":30,"isInitiator":false,"defaultFinalScriptPubKey":"a9144805d016e47885dc7c852710cdd8cd0d576f57ec87","initFeatures":{"activated":{"option_data_loss_protect":"optional","initial_routing_sync":"optional","gossip_queries":"optional"},"unknown":[]}},"remoteParams":{"nodeId":"034fe52e98a0e9d3c21b767e1b371881265d8c7578c21f5afd6d6438da10348b36","dustLimit":573,"maxHtlcValueInFlightMsat":16609443000,"requestedChannelReserve_opt":167772,"htlcMinimum":1000,"toSelfDelay":2016,"maxAcceptedHtlcs":483,"fundingPubKey":"028cef3ef020cfda09692afc29e38ac4756ca60736563a93220481091c9cd05b64","revocationBasepoint":"02635ac9eedf5f219afbc4d125e37b5705f73c05deca71b05fe84096a691e055c1","paymentBasepoint":"034a711d28e8ed3ad389ec14ec75c199b6a45140c503bcc88110e3524e52ffbfb1","delayedPaymentBasepoint":"0316c70730b57a9e15845ce6f239e749ac78b25f44c90485a697066962a73d0467","htlcBasepoint":"03763e280986fb384631ebf8d637efd9ebcd06b6ef3c77c1375b9edbe3ea3b9f79","initFeatures":{"activated":{"option_data_loss_protect":"mandatory","gossip_queries":"optional"},"unknown":[]}},"channelFlags":{"announceChannel":true},"localCommit":{"index":7675,"spec":{"htlcs":[],"commitTxFeerate":254,"toLocal":204739729,"toRemote":16572475271},"commitTxAndRemoteSig":{"commitTx":{"txid":"e25a866b79212015e01e155e530fb547abc8276869f8740a9948e52ca231f1e4","tx":"020000000107738f22c16a24795bcaebc6e09016af61902dd66bbaf64e6e5db50b0c45d63d010000000032c3698002c31f0300000000002200205cc91746133145180585bfb3bb9a1c1740c9b43338aa30c90b5f5652d729ce0884dffc0000000000160014cfb373f55b722ca1c028d63ee85cb82c00ce11127af8a620"},"remoteSig":"4d4d24b8cb3a00dfd685ac73e3c85ba26449dc935469ce36c259f2db6cd519a865845eca78a998bc8213044e84eca0c884cdb01bda8b6e70f5c1ff821ca5388d"},"htlcTxsAndRemoteSigs":[]},"remoteCommit":{"index":7779,"spec":{"htlcs":[],"commitTxFeerate":254,"toLocal":16572475271,"toRemote":204739729},"txid":"ac994c4f64875ab22b45cba175a04cec4051bbe660932570744dad822e6bf8be","remotePerCommitmentPoint":"03daadaed37bcfed40d15e34979fbf2a0643e748e8960363bb8e930cefe2255c35"},"localChanges":{"proposed":[],"signed":[],"acked":[]},"remoteChanges":{"proposed":[],"acked":[],"signed":[]},"localNextHtlcId":203,"remoteNextHtlcId":4147,"originChannels":{},"remoteNextCommitInfo":"034dcc0704325064a1fa68edc13adb5fd173051775df73a298ec291f22ad9d19f6","commitInput":{"outPoint":"3dd6450c0bb55d6e4ef6ba6bd62d9061af1690e0c6ebca5b79246ac1228f7307:1","amountSatoshis":16777215},"remotePerCommitmentSecrets":null},"shortIds":{"real":{"status":"final","realScid":"1513532x23x1"},"localAlias":"0x17183c0000170001"},"channelAnnouncement":{"nodeSignature1":"d2366163f4d5a51be3210b66b2e4a2736b9ccc20ce8d0d69413d5b5e42d991401183b271ba032764151ba8f3c4b03f11df5749fd876eeaf3fd401bb383cb3174","nodeSignature2":"075779c27157e5b4024ecee12308cf3bde976a0891983b0655b669b38e7e700362c25ce4af05aaa130f000aa6a04037534a7a23a8d99454948dd689277eab321","bitcoinSignature1":"4049b7649693d92139bf3f1f41da3825d1b3dbed2884797b76fd8e1c77390d1b4f3bf76b8d890485d7555619160a2bf18d58626f2ec9a8ca1f887eba3ba130b5","bitcoinSignature2":"0d55e84fb4059bea082d443934af74dcbfd5c4c2fd54eba3ea2823114df932e7759805207f1182062f99af028aa4b62c7723a0c5b9198fe637a3d18d4d99dc70","features":{"activated":{},"unknown":[]},"chainHash":"43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000","shortChannelId":"1513532x23x1","nodeId1":"034fe52e98a0e9d3c21b767e1b371881265d8c7578c21f5afd6d6438da10348b36","nodeId2":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","bitcoinKey1":"028cef3ef020cfda09692afc29e38ac4756ca60736563a93220481091c9cd05b64","bitcoinKey2":"03660d280e24a9b16772a6e6418029719620a5caa29ebdf8339e5d700c611ab9e3","tlvStream":{"records":[],"unknown":[]}},"channelUpdate":{"signature":"4e34a547c424182812bd39b35c1c244b98f2bbb5b7d07812b9a008bb69f3fd77788f4ad338a102c331892afa8d076167a6a6cfb4eac3b890387f0fdc98b5b8c3","chainHash":"43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000","shortChannelId":"1513532x23x1","timestamp":{"iso":"2019-06-18T12:49:33Z","unix":1560862173},"channelFlags":{"isEnabled":true,"isNode1":false},"cltvExpiryDelta":144,"htlcMinimumMsat":1000,"feeBaseMsat":1000,"feeProportionalMillionths":100,"htlcMaximumMsat":16777215000,"tlvStream":{"records":[],"unknown":[]}}}""", hex"00000303933884AAF1D6B108397E5EFE5C86BCF2D8CA8D2F700EDA99DB9214FC2712B1340004D443ECE9D9C43A11A19B554BAAA6AD150000000000000222000000003B9ACA0000000000000249F000000000000000010090001E800BD48A22F4C80A42CC8BB29A764DBAEFC95674931FBE9A4380000000C50134D4A745996002F219B5FDBA1E045374DF589ECA06ABE23CECAE47343E65EDCF800000000000011E80000001BA90824000000000000124F800000000000001F4038500F1810AE1AF8A1D6F56F80855E26705F191BB07CD4E2434BC5BB1698E7E5880E2266201E8BFEEEEED725775B8116F6F82CF8E87835A5B45B184E56F272AD70D6078118601E06212B8C8F2E25B73EE7974FDCDF007E389B437BBFE238CCC3F3BF7121B6C5E81AA8589D21E9584B24A11F3ABBA5DAD48D121DD63C57A69CD767119C05DA159CB81A649D8CC0E136EB8DFBD2268B69DCA86F8CE4A604235A03D9D37AE7B07FC563F80000000C080800000000000271C000000000177000000002808B14600000001970039BA00123767F0F4F00D5E9FDF24177EF2872343D9F8FAEC65D3048BA575E70E00A0AB08800000000015E070F20000000000110010584241B5FB364208F6E64A80D1166DAD866186B10C015ED0283FF1C308C2105A0023A910810AE1AF8A1D6F56F80855E26705F191BB07CD4E2434BC5BB1698E7E5880E226621081DE8ADFA110DC8A94D8B9E9EF616BAE8598287C8F82AFDF0FC068697D570266FDA95700AD81000000000080B767F0F4F00D5E9FDF24177EF2872343D9F8FAEC65D3048BA575E70E00A0AB0880000000003E7AEDC0011ABE8A00000000001100101A9CE4B6AEF469590BC7BCC51DCEEAE9C86084055A63CC01E443C733FBE400B9B5B16800000000000B000A5E5700106D1A7097E4DE87EBAF1F8F2773842FA482002418228110805E84989A81F51ABD9D11889AE43E68FAD93659DEC019F1B8C0ADBF15A57B118B81101DCC1256F9306439AD3962C043FC47A5179CAAA001CCB23342BE0E8D92E4022780A4182281108074F306DA3751B84EC5FFB155BDCA7B8E02208BBDBC8D4F3327ABA557BF27CD1701102EF4AC8CC92F469DA9642D4D4162BC545F8B34ADE15B7D6F99808AA22B086B0180A3A910810AE1AF8A1D6F56F80855E26705F191BB07CD4E2434BC5BB1698E7E5880E226621081DE8ADFA110DC8A94D8B9E9EF616BAE8598287C8F82AFDF0FC068697D570266FDA9576F8099900000000000000000271C00000000017700000001970039BA000000002808B14648CE00AE97051EE10A3C361263F81A98165CE4AA7BA076933D4266E533585F24815C15DEACF0691332B38ECF23EC39982C5C978C748374A01BA9B30D501EE4F26E8000000000000000000000000000000000001224000000000000004B800040A911C460F1467952E3B99BED072F81BFB4454FF389636DCB399FE6A78113C28580091BB3F87A7806AF4FEF920BBF794391A1ECFC7D7632E98245D2BAF3870050558440000000000AF0387900000000000880082C2120DAFD9B21047B732540688B36D6C330C3588600AF68141FF8E18461082D0011D488408570D7C50EB7AB7C042AF13382F8C8DD83E6A7121A5E2DD8B4C73F2C407113310840EF456FD0886E454A6C5CF4F7B0B5D742CC143E47C157EF87E03434BEAB81337ED4AB8001C00F40003FFFFFFFEC7200403248A1D44DFA3AC9EC237D452C936400CAA86E9517CCCF2A8F77B7493CD70B6A00780001FFFFFFFF63A0041826829646B907A97FBD1455EA8673A12B8E7AA6EA790F7802E955CE3B69DE57E006E0001FFFFFFFF640081E51EB1F91218821E680B50E4B22DF8B094385BD33ACAE36BFC9E8C2F5AD2DA5400EC0003FFFFFFFEC7801047C26AD5435658D063EBCF73A5D0EEFE73ED6B73426246E8DFB3A21D1C4C7465001900007FFFFFFFE0040B115AC58BAAA900195893EA3B2AB408D2AD348AD047E3B6CB15E599625E38608006A0001FFFFFFFF7002033C39A21A38BB61F6FB33623771A9356D8885B7C12C939C770C939EF826286C200360000FFFFFFFFB4008104EF4271064A0973B053727C3E67352D00E25CAEED944F50782449CEAE8F50960001FFFFFFFF6390DD9FC3D3C0357A7F7C905DFBCA1C8D0F67E3EBB1974C122E95D79C380282AC222B21FA0007920001295AA1FB77029F7620A90EF7AE6A6CD31E4588B93264A7ADB76152D535C52E90B9E1B7C2376DABA316A6290F1A9730D4E5E44D0B1CB0EE6A795702E6A6BCDFCDA1A4BFEBFC134AB8847A5187ECE761D75D3CCB904274875680F51984800000000AC87E8001E480002E884D2A8080804800000000000001F4000001F40000003200000001BF08EB000" - -> """{"type":"DATA_NORMAL","commitments":{"channelId":"6ecfe1e9e01abd3fbe482efde50e4687b3f1f5d8cba609174aebce1c01415611","channelConfig":[],"channelFeatures":[],"localParams":{"nodeId":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","fundingKeyPath":{"path":[3561221353,3653515793,2711311691,2863050005]},"dustLimit":546,"maxHtlcValueInFlightMsat":1000000000,"requestedChannelReserve_opt":150000,"htlcMinimum":1,"toSelfDelay":144,"maxAcceptedHtlcs":30,"isInitiator":true,"defaultFinalScriptPubKey":"a91445e990148599176534ec9b75df92ace9263f7d3487","initFeatures":{"activated":{"option_data_loss_protect":"optional","initial_routing_sync":"optional","gossip_queries":"optional"},"unknown":[]}},"remoteParams":{"nodeId":"0269a94e8b32c005e4336bfb743c08a6e9beb13d940d57c479d95c8e687ccbdb9f","dustLimit":573,"maxHtlcValueInFlightMsat":14850000000,"requestedChannelReserve_opt":150000,"htlcMinimum":1000,"toSelfDelay":1802,"maxAcceptedHtlcs":483,"fundingPubKey":"0215c35f143adeadf010abc4ce0be323760f9a9c486978b762d31cfcb101c44cc4","revocationBasepoint":"03d17fdddddae4aeeb7022dedf059f1d0f06b4b68b6309cade4e55ae1ac0f0230c","paymentBasepoint":"03c0c4257191e5c4b6e7dcf2e9fb9be00fc713686f77fc4719987e77ee2436d8bd","delayedPaymentBasepoint":"03550b13a43d2b09649423e75774bb5a91a243bac78af4d39aece23380bb42b397","htlcBasepoint":"034c93b1981c26dd71bf7a44d16d3b950df19c94c0846b407b3a6f5cf60ff8ac7f","initFeatures":{"activated":{"option_data_loss_protect":"mandatory","gossip_queries":"optional"},"unknown":[]}},"channelFlags":{"announceChannel":true},"localCommit":{"index":20024,"spec":{"htlcs":[],"commitTxFeerate":750,"toLocal":1343316620,"toRemote":13656683380},"commitTxAndRemoteSig":{"commitTx":{"txid":"65fe0b1f079fa763448df3ab8d94b1ad7d377c061121376be90b9c0c1bb0cd43","tx":"02000000016ecfe1e9e01abd3fbe482efde50e4687b3f1f5d8cba609174aebce1c0141561100000000007cf5db8002357d1400000000002200203539c96d5de8d2b2178f798a3b9dd5d390c1080ab4c79803c8878e67f7c801736b62d00000000000160014bcae0020da34e12fc9bd0fd75e3f1e4ee7085f49df013320"},"remoteSig":"bd09313503ea357b3a231135c87cd1f5b26cb3bd8033e371815b7e2b4af623173b9824adf260c8735a72c58087f88f4a2f39554003996466857c1d1b25c8044f"},"htlcTxsAndRemoteSigs":[]},"remoteCommit":{"index":20024,"spec":{"htlcs":[],"commitTxFeerate":750,"toLocal":13656683380,"toRemote":1343316620},"txid":"919c015d2e0a3dc214786c24c7f035302cb9c954f740ed267a84cdca66b0be49","remotePerCommitmentPoint":"02b82bbd59e0d22665671d9e47d8733058b92f18e906e9403753661aa03dc9e4dd"},"localChanges":{"proposed":[],"signed":[],"acked":[]},"remoteChanges":{"proposed":[],"acked":[],"signed":[]},"localNextHtlcId":9288,"remoteNextHtlcId":151,"originChannels":{},"remoteNextCommitInfo":"02a4471183c519e54b8ee66fb41cbe06fed1153fce258db72ce67f9a9e044f0a16","commitInput":{"outPoint":"115641011cceeb4a1709a6cbd8f5f1b387460ee5fd2e48be3fbd1ae0e9e1cf6e:0","amountSatoshis":15000000},"remotePerCommitmentSecrets":null},"shortChannelId":"1413373x969x0","buried":true,"channelUpdate":{"signature":"52b543f6ee053eec41521def5cd4d9a63c8b117264c94f5b6ec2a5aa6b8a5d2173c36f846edb57462d4c521e352e61a9cbc89a163961dcd4f2ae05cd4d79bf9b","chainHash":"43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000","shortChannelId":"1413373x969x0","timestamp":{"iso":"2019-06-24T09:39:33Z","unix":1561369173},"channelFlags":{"isEnabled":true,"isNode1":false},"cltvExpiryDelta":144,"htlcMinimumMsat":1000,"feeBaseMsat":1000,"feeProportionalMillionths":100,"htlcMaximumMsat":15000000000,"tlvStream":{"records":[],"unknown":[]}}}""", + -> """{"type":"DATA_NORMAL","commitments":{"channelId":"6ecfe1e9e01abd3fbe482efde50e4687b3f1f5d8cba609174aebce1c01415611","channelConfig":[],"channelFeatures":[],"localParams":{"nodeId":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","fundingKeyPath":{"path":[3561221353,3653515793,2711311691,2863050005]},"dustLimit":546,"maxHtlcValueInFlightMsat":1000000000,"requestedChannelReserve_opt":150000,"htlcMinimum":1,"toSelfDelay":144,"maxAcceptedHtlcs":30,"isInitiator":true,"defaultFinalScriptPubKey":"a91445e990148599176534ec9b75df92ace9263f7d3487","initFeatures":{"activated":{"option_data_loss_protect":"optional","initial_routing_sync":"optional","gossip_queries":"optional"},"unknown":[]}},"remoteParams":{"nodeId":"0269a94e8b32c005e4336bfb743c08a6e9beb13d940d57c479d95c8e687ccbdb9f","dustLimit":573,"maxHtlcValueInFlightMsat":14850000000,"requestedChannelReserve_opt":150000,"htlcMinimum":1000,"toSelfDelay":1802,"maxAcceptedHtlcs":483,"fundingPubKey":"0215c35f143adeadf010abc4ce0be323760f9a9c486978b762d31cfcb101c44cc4","revocationBasepoint":"03d17fdddddae4aeeb7022dedf059f1d0f06b4b68b6309cade4e55ae1ac0f0230c","paymentBasepoint":"03c0c4257191e5c4b6e7dcf2e9fb9be00fc713686f77fc4719987e77ee2436d8bd","delayedPaymentBasepoint":"03550b13a43d2b09649423e75774bb5a91a243bac78af4d39aece23380bb42b397","htlcBasepoint":"034c93b1981c26dd71bf7a44d16d3b950df19c94c0846b407b3a6f5cf60ff8ac7f","initFeatures":{"activated":{"option_data_loss_protect":"mandatory","gossip_queries":"optional"},"unknown":[]}},"channelFlags":{"announceChannel":true},"localCommit":{"index":20024,"spec":{"htlcs":[],"commitTxFeerate":750,"toLocal":1343316620,"toRemote":13656683380},"commitTxAndRemoteSig":{"commitTx":{"txid":"65fe0b1f079fa763448df3ab8d94b1ad7d377c061121376be90b9c0c1bb0cd43","tx":"02000000016ecfe1e9e01abd3fbe482efde50e4687b3f1f5d8cba609174aebce1c0141561100000000007cf5db8002357d1400000000002200203539c96d5de8d2b2178f798a3b9dd5d390c1080ab4c79803c8878e67f7c801736b62d00000000000160014bcae0020da34e12fc9bd0fd75e3f1e4ee7085f49df013320"},"remoteSig":"bd09313503ea357b3a231135c87cd1f5b26cb3bd8033e371815b7e2b4af623173b9824adf260c8735a72c58087f88f4a2f39554003996466857c1d1b25c8044f"},"htlcTxsAndRemoteSigs":[]},"remoteCommit":{"index":20024,"spec":{"htlcs":[],"commitTxFeerate":750,"toLocal":13656683380,"toRemote":1343316620},"txid":"919c015d2e0a3dc214786c24c7f035302cb9c954f740ed267a84cdca66b0be49","remotePerCommitmentPoint":"02b82bbd59e0d22665671d9e47d8733058b92f18e906e9403753661aa03dc9e4dd"},"localChanges":{"proposed":[],"signed":[],"acked":[]},"remoteChanges":{"proposed":[],"acked":[],"signed":[]},"localNextHtlcId":9288,"remoteNextHtlcId":151,"originChannels":{},"remoteNextCommitInfo":"02a4471183c519e54b8ee66fb41cbe06fed1153fce258db72ce67f9a9e044f0a16","commitInput":{"outPoint":"115641011cceeb4a1709a6cbd8f5f1b387460ee5fd2e48be3fbd1ae0e9e1cf6e:0","amountSatoshis":15000000},"remotePerCommitmentSecrets":null},"shortIds":{"real":{"status":"final","realScid":"1413373x969x0"},"localAlias":"0x1590fd0003c90000"},"channelUpdate":{"signature":"52b543f6ee053eec41521def5cd4d9a63c8b117264c94f5b6ec2a5aa6b8a5d2173c36f846edb57462d4c521e352e61a9cbc89a163961dcd4f2ae05cd4d79bf9b","chainHash":"43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000","shortChannelId":"1413373x969x0","timestamp":{"iso":"2019-06-24T09:39:33Z","unix":1561369173},"channelFlags":{"isEnabled":true,"isNode1":false},"cltvExpiryDelta":144,"htlcMinimumMsat":1000,"feeBaseMsat":1000,"feeProportionalMillionths":100,"htlcMaximumMsat":15000000000,"tlvStream":{"records":[],"unknown":[]}}}""", hex"0200020000000303933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b13400098c4b989bbdced820a77a7186c2320e7d176a5c8b5c16d6ac2af3889d6bc8bf8080000001000000000000022200000004a817c80000000000000249f0000000000000000102d0001eff1600148061b7fbd2d84ed1884177ea785faecb2080b10302e56c8eca8d4f00df84ac34c23f49c006d57d316b7ada5c346e9d4211e11604b300000004080aa982027455aef8453d92f4706b560b61527cc217ddf14da41770e8ed6607190a1851b8000000000000023d000000037521048000000000000249f00000000000000001070a01e302eff5309b9368340edc6114d738b3590e6969bec4e95d8a080cf185e8b9ce5e4b0343bf4bfbaea5c100f1f2bf1cdf82a0ef97c9a0069a2aec631e7c3084ba929b7503c54e7d5ccfc13f1a6c7a441ffcfac86248574d1bc0fe9773836f4c724ea7b2bd03765aaac2e8fa6dbce7de5143072e9d9d5e96a1fd451d02fe4ff803f413f303f8022f3b055b0d35cde31dec5263a8ed638433e3424a4e197c06d94053985a364a5700000004808a52a1010000000000000004000000001046000000037e11d6000000000000000000245986f644357956c1674f385e0878fb7bb44ab3d57ea9911fab98af8a71e1ad1b000000002bc0e1e40000000000220020690fb50de412adf9b20a7fc6c8fb86f1bfd4ebc1ef8e2d96a5a196560798d944475221023ced05ed1ab328b67477376d68a69ecd0f371a9d5843c6c3be4d31498d516d8d2102eff5309b9368340edc6114d738b3590e6969bec4e95d8a080cf185e8b9ce5e4b52aefd013b020000000001015986f644357956c1674f385e0878fb7bb44ab3d57ea9911fab98af8a71e1ad1b0000000000c2d6178001f8d5e4000000000022002080f1dfe71a865b605593e169677c952aaa1196fc2f541ef7d21c3b1006527b61040047304402207f8c1936d0a50671c993890f887c78c6019abc2a2e8018899dcdc0e891fd2b090220046b56afa2cb7e9470073c238654ecf584bcf5c00b96b91e38335a70e2739ec901483045022100871afd240e20a171b9cba46f20555f848c5850f94ec7da7b33b9eeaf6af6653c0220119cda8cbf5f80986d6a4f0db2590c734d1de399a7060a477b5d94df0183625b01475221023ced05ed1ab328b67477376d68a69ecd0f371a9d5843c6c3be4d31498d516d8d2102eff5309b9368340edc6114d738b3590e6969bec4e95d8a080cf185e8b9ce5e4b52aed7782c20000000000000000000040000000010460000000000000000000000037e11d600b5f2287b2d5edf4df5602a3c287db3b938c3f1a943e40715886db5bd400f95d802e7e1abac1feb54ee3ac2172c9e2231f77765df57664fb44a6dc2e4aa9e6a9a6a000000000000000000000000000000000000000000000000000000000000ff03fd10fe44564e2d7e1550099785c2c1bad32a5ae0feeef6e27f0c108d18b4931d245986f644357956c1674f385e0878fb7bb44ab3d57ea9911fab98af8a71e1ad1b000000002bc0e1e40000000000220020690fb50de412adf9b20a7fc6c8fb86f1bfd4ebc1ef8e2d96a5a196560798d944475221023ced05ed1ab328b67477376d68a69ecd0f371a9d5843c6c3be4d31498d516d8d2102eff5309b9368340edc6114d738b3590e6969bec4e95d8a080cf185e8b9ce5e4b52ae0001003e0000fffffffffffc0080474b8cf7bb98217dd8dc475cb7c057a3465d466728978bbb909d0a05d4ae7bbe0001fffffffffff85986f644357956c1674f385e0878fb7bb44ab3d57ea9911fab98af8a71e1ad1b1eedce0000010000fffffd01ae98d7a81bc1aa92fcfb74ced2213e85e0d92ae8ac622bf294b3551c7c27f6f84f782f3b318e4d0eb2c67ac719a7c65afcf85bf159f6ceea9427be54920134196992f6ed0e059db72105a13ec0e799bb08896cad8b4feb7e9ec7283c309b5f43123af1bd9e913fc2db018edadde8932d6992408f10c1ad020504361972dfa7fef09bbc2b568cef3c8c006f7860106fd5984bcc271ff06c4829db2a665e59b7c0b22c311a340ff2ab9bcb74a50db10ed85503ad2d248d95af8151aca8ef96248e8f84b3075922385fbaf012f057e7ee84ecbc14c84880520b26d6fd22ab5f107db606a906efdcf0f88ffbe32dc6ecc10131e1ff0dc8d68dad89c98562557f00448b000043497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea3309000000001eedce0000010000027455aef8453d92f4706b560b61527cc217ddf14da41770e8ed6607190a1851b803933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b13402eff5309b9368340edc6114d738b3590e6969bec4e95d8a080cf185e8b9ce5e4b023ced05ed1ab328b67477376d68a69ecd0f371a9d5843c6c3be4d31498d516d8d88710d73875607575f3d84bb507dd87cca5b85f0cdac84f4ccecce7af3a55897525a45070fe26c0ea43e9580d4ea4cfa62ee3273e5546911145cba6bbf56e59d8e43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea3309000000001eedce000001000060e6eb14010100900000000000000001000003e800000064000000037e11d6000000" - -> """{"type":"DATA_NORMAL","commitments":{"channelId":"5986f644357956c1674f385e0878fb7bb44ab3d57ea9911fab98af8a71e1ad1b","channelConfig":["funding_pubkey_based_channel_keypath"],"channelFeatures":["option_static_remotekey"],"localParams":{"nodeId":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","fundingKeyPath":{"path":[2353764507,3184449568,2809819526,3258060413,392846475,1545000620,720603293,1808318336,2147483649]},"dustLimit":546,"maxHtlcValueInFlightMsat":20000000000,"requestedChannelReserve_opt":150000,"htlcMinimum":1,"toSelfDelay":720,"maxAcceptedHtlcs":30,"isInitiator":true,"defaultFinalScriptPubKey":"00148061b7fbd2d84ed1884177ea785faecb2080b103","walletStaticPaymentBasepoint":"02e56c8eca8d4f00df84ac34c23f49c006d57d316b7ada5c346e9d4211e11604b3","initFeatures":{"activated":{"option_support_large_channel":"optional","gossip_queries_ex":"optional","option_data_loss_protect":"optional","var_onion_optin":"mandatory","option_static_remotekey":"optional","payment_secret":"optional","option_shutdown_anysegwit":"optional","basic_mpp":"optional","gossip_queries":"optional"},"unknown":[]}},"remoteParams":{"nodeId":"027455aef8453d92f4706b560b61527cc217ddf14da41770e8ed6607190a1851b8","dustLimit":573,"maxHtlcValueInFlightMsat":14850000000,"requestedChannelReserve_opt":150000,"htlcMinimum":1,"toSelfDelay":1802,"maxAcceptedHtlcs":483,"fundingPubKey":"02eff5309b9368340edc6114d738b3590e6969bec4e95d8a080cf185e8b9ce5e4b","revocationBasepoint":"0343bf4bfbaea5c100f1f2bf1cdf82a0ef97c9a0069a2aec631e7c3084ba929b75","paymentBasepoint":"03c54e7d5ccfc13f1a6c7a441ffcfac86248574d1bc0fe9773836f4c724ea7b2bd","delayedPaymentBasepoint":"03765aaac2e8fa6dbce7de5143072e9d9d5e96a1fd451d02fe4ff803f413f303f8","htlcBasepoint":"022f3b055b0d35cde31dec5263a8ed638433e3424a4e197c06d94053985a364a57","initFeatures":{"activated":{"option_upfront_shutdown_script":"optional","payment_secret":"mandatory","option_data_loss_protect":"mandatory","var_onion_optin":"optional","option_static_remotekey":"mandatory","option_support_large_channel":"optional","option_anchors_zero_fee_htlc_tx":"optional","basic_mpp":"optional","gossip_queries":"optional"},"unknown":[31]}},"channelFlags":{"announceChannel":true},"localCommit":{"index":4,"spec":{"htlcs":[],"commitTxFeerate":4166,"toLocal":15000000000,"toRemote":0},"commitTxAndRemoteSig":{"commitTx":{"txid":"fa747ecb6f718c6831cc7148cf8d65c3468d2bb6c202605e2b82d2277491222f","tx":"02000000015986f644357956c1674f385e0878fb7bb44ab3d57ea9911fab98af8a71e1ad1b0000000000c2d6178001f8d5e4000000000022002080f1dfe71a865b605593e169677c952aaa1196fc2f541ef7d21c3b1006527b61d7782c20"},"remoteSig":"871afd240e20a171b9cba46f20555f848c5850f94ec7da7b33b9eeaf6af6653c119cda8cbf5f80986d6a4f0db2590c734d1de399a7060a477b5d94df0183625b"},"htlcTxsAndRemoteSigs":[]},"remoteCommit":{"index":4,"spec":{"htlcs":[],"commitTxFeerate":4166,"toLocal":0,"toRemote":15000000000},"txid":"b5f2287b2d5edf4df5602a3c287db3b938c3f1a943e40715886db5bd400f95d8","remotePerCommitmentPoint":"02e7e1abac1feb54ee3ac2172c9e2231f77765df57664fb44a6dc2e4aa9e6a9a6a"},"localChanges":{"proposed":[],"signed":[],"acked":[]},"remoteChanges":{"proposed":[],"acked":[],"signed":[]},"localNextHtlcId":0,"remoteNextHtlcId":0,"originChannels":{},"remoteNextCommitInfo":"03fd10fe44564e2d7e1550099785c2c1bad32a5ae0feeef6e27f0c108d18b4931d","commitInput":{"outPoint":"1bade1718aaf98ab1f91a97ed5b34ab47bfb78085e384f67c156793544f68659:0","amountSatoshis":15000000},"remotePerCommitmentSecrets":null},"shortChannelId":"2026958x1x0","buried":true,"channelAnnouncement":{"nodeSignature1":"98d7a81bc1aa92fcfb74ced2213e85e0d92ae8ac622bf294b3551c7c27f6f84f782f3b318e4d0eb2c67ac719a7c65afcf85bf159f6ceea9427be549201341969","nodeSignature2":"92f6ed0e059db72105a13ec0e799bb08896cad8b4feb7e9ec7283c309b5f43123af1bd9e913fc2db018edadde8932d6992408f10c1ad020504361972dfa7fef0","bitcoinSignature1":"9bbc2b568cef3c8c006f7860106fd5984bcc271ff06c4829db2a665e59b7c0b22c311a340ff2ab9bcb74a50db10ed85503ad2d248d95af8151aca8ef96248e8f","bitcoinSignature2":"84b3075922385fbaf012f057e7ee84ecbc14c84880520b26d6fd22ab5f107db606a906efdcf0f88ffbe32dc6ecc10131e1ff0dc8d68dad89c98562557f00448b","features":{"activated":{},"unknown":[]},"chainHash":"43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000","shortChannelId":"2026958x1x0","nodeId1":"027455aef8453d92f4706b560b61527cc217ddf14da41770e8ed6607190a1851b8","nodeId2":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","bitcoinKey1":"02eff5309b9368340edc6114d738b3590e6969bec4e95d8a080cf185e8b9ce5e4b","bitcoinKey2":"023ced05ed1ab328b67477376d68a69ecd0f371a9d5843c6c3be4d31498d516d8d","tlvStream":{"records":[],"unknown":[]}},"channelUpdate":{"signature":"710d73875607575f3d84bb507dd87cca5b85f0cdac84f4ccecce7af3a55897525a45070fe26c0ea43e9580d4ea4cfa62ee3273e5546911145cba6bbf56e59d8e","chainHash":"43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000","shortChannelId":"2026958x1x0","timestamp":{"iso":"2021-07-08T12:09:56Z","unix":1625746196},"channelFlags":{"isEnabled":true,"isNode1":false},"cltvExpiryDelta":144,"htlcMinimumMsat":1,"feeBaseMsat":1000,"feeProportionalMillionths":100,"htlcMaximumMsat":15000000000,"tlvStream":{"records":[],"unknown":[]}}}""" + -> """{"type":"DATA_NORMAL","commitments":{"channelId":"5986f644357956c1674f385e0878fb7bb44ab3d57ea9911fab98af8a71e1ad1b","channelConfig":["funding_pubkey_based_channel_keypath"],"channelFeatures":["option_static_remotekey"],"localParams":{"nodeId":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","fundingKeyPath":{"path":[2353764507,3184449568,2809819526,3258060413,392846475,1545000620,720603293,1808318336,2147483649]},"dustLimit":546,"maxHtlcValueInFlightMsat":20000000000,"requestedChannelReserve_opt":150000,"htlcMinimum":1,"toSelfDelay":720,"maxAcceptedHtlcs":30,"isInitiator":true,"defaultFinalScriptPubKey":"00148061b7fbd2d84ed1884177ea785faecb2080b103","walletStaticPaymentBasepoint":"02e56c8eca8d4f00df84ac34c23f49c006d57d316b7ada5c346e9d4211e11604b3","initFeatures":{"activated":{"option_support_large_channel":"optional","gossip_queries_ex":"optional","option_data_loss_protect":"optional","var_onion_optin":"mandatory","option_static_remotekey":"optional","payment_secret":"optional","option_shutdown_anysegwit":"optional","basic_mpp":"optional","gossip_queries":"optional"},"unknown":[]}},"remoteParams":{"nodeId":"027455aef8453d92f4706b560b61527cc217ddf14da41770e8ed6607190a1851b8","dustLimit":573,"maxHtlcValueInFlightMsat":14850000000,"requestedChannelReserve_opt":150000,"htlcMinimum":1,"toSelfDelay":1802,"maxAcceptedHtlcs":483,"fundingPubKey":"02eff5309b9368340edc6114d738b3590e6969bec4e95d8a080cf185e8b9ce5e4b","revocationBasepoint":"0343bf4bfbaea5c100f1f2bf1cdf82a0ef97c9a0069a2aec631e7c3084ba929b75","paymentBasepoint":"03c54e7d5ccfc13f1a6c7a441ffcfac86248574d1bc0fe9773836f4c724ea7b2bd","delayedPaymentBasepoint":"03765aaac2e8fa6dbce7de5143072e9d9d5e96a1fd451d02fe4ff803f413f303f8","htlcBasepoint":"022f3b055b0d35cde31dec5263a8ed638433e3424a4e197c06d94053985a364a57","initFeatures":{"activated":{"option_upfront_shutdown_script":"optional","payment_secret":"mandatory","option_data_loss_protect":"mandatory","var_onion_optin":"optional","option_static_remotekey":"mandatory","option_support_large_channel":"optional","option_anchors_zero_fee_htlc_tx":"optional","basic_mpp":"optional","gossip_queries":"optional"},"unknown":[31]}},"channelFlags":{"announceChannel":true},"localCommit":{"index":4,"spec":{"htlcs":[],"commitTxFeerate":4166,"toLocal":15000000000,"toRemote":0},"commitTxAndRemoteSig":{"commitTx":{"txid":"fa747ecb6f718c6831cc7148cf8d65c3468d2bb6c202605e2b82d2277491222f","tx":"02000000015986f644357956c1674f385e0878fb7bb44ab3d57ea9911fab98af8a71e1ad1b0000000000c2d6178001f8d5e4000000000022002080f1dfe71a865b605593e169677c952aaa1196fc2f541ef7d21c3b1006527b61d7782c20"},"remoteSig":"871afd240e20a171b9cba46f20555f848c5850f94ec7da7b33b9eeaf6af6653c119cda8cbf5f80986d6a4f0db2590c734d1de399a7060a477b5d94df0183625b"},"htlcTxsAndRemoteSigs":[]},"remoteCommit":{"index":4,"spec":{"htlcs":[],"commitTxFeerate":4166,"toLocal":0,"toRemote":15000000000},"txid":"b5f2287b2d5edf4df5602a3c287db3b938c3f1a943e40715886db5bd400f95d8","remotePerCommitmentPoint":"02e7e1abac1feb54ee3ac2172c9e2231f77765df57664fb44a6dc2e4aa9e6a9a6a"},"localChanges":{"proposed":[],"signed":[],"acked":[]},"remoteChanges":{"proposed":[],"acked":[],"signed":[]},"localNextHtlcId":0,"remoteNextHtlcId":0,"originChannels":{},"remoteNextCommitInfo":"03fd10fe44564e2d7e1550099785c2c1bad32a5ae0feeef6e27f0c108d18b4931d","commitInput":{"outPoint":"1bade1718aaf98ab1f91a97ed5b34ab47bfb78085e384f67c156793544f68659:0","amountSatoshis":15000000},"remotePerCommitmentSecrets":null},"shortIds":{"real":{"status":"final","realScid":"2026958x1x0"},"localAlias":"0x1eedce0000010000"},"channelAnnouncement":{"nodeSignature1":"98d7a81bc1aa92fcfb74ced2213e85e0d92ae8ac622bf294b3551c7c27f6f84f782f3b318e4d0eb2c67ac719a7c65afcf85bf159f6ceea9427be549201341969","nodeSignature2":"92f6ed0e059db72105a13ec0e799bb08896cad8b4feb7e9ec7283c309b5f43123af1bd9e913fc2db018edadde8932d6992408f10c1ad020504361972dfa7fef0","bitcoinSignature1":"9bbc2b568cef3c8c006f7860106fd5984bcc271ff06c4829db2a665e59b7c0b22c311a340ff2ab9bcb74a50db10ed85503ad2d248d95af8151aca8ef96248e8f","bitcoinSignature2":"84b3075922385fbaf012f057e7ee84ecbc14c84880520b26d6fd22ab5f107db606a906efdcf0f88ffbe32dc6ecc10131e1ff0dc8d68dad89c98562557f00448b","features":{"activated":{},"unknown":[]},"chainHash":"43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000","shortChannelId":"2026958x1x0","nodeId1":"027455aef8453d92f4706b560b61527cc217ddf14da41770e8ed6607190a1851b8","nodeId2":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","bitcoinKey1":"02eff5309b9368340edc6114d738b3590e6969bec4e95d8a080cf185e8b9ce5e4b","bitcoinKey2":"023ced05ed1ab328b67477376d68a69ecd0f371a9d5843c6c3be4d31498d516d8d","tlvStream":{"records":[],"unknown":[]}},"channelUpdate":{"signature":"710d73875607575f3d84bb507dd87cca5b85f0cdac84f4ccecce7af3a55897525a45070fe26c0ea43e9580d4ea4cfa62ee3273e5546911145cba6bbf56e59d8e","chainHash":"43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000","shortChannelId":"2026958x1x0","timestamp":{"iso":"2021-07-08T12:09:56Z","unix":1625746196},"channelFlags":{"isEnabled":true,"isNode1":false},"cltvExpiryDelta":144,"htlcMinimumMsat":1,"feeBaseMsat":1000,"feeProportionalMillionths":100,"htlcMaximumMsat":15000000000,"tlvStream":{"records":[],"unknown":[]}}}""" ) refs.foreach { case (oldbin, refjson) => @@ -328,7 +329,7 @@ object ChannelCodecsSpec { commitInput = commitmentInput, remotePerCommitmentSecrets = ShaChain.init) - DATA_NORMAL(commitments, ShortChannelId(42), buried = true, None, channelUpdate, None, None, None) + DATA_NORMAL(commitments, ShortIds(RealScidStatus.Final(RealShortChannelId(42)), ShortChannelId.generateLocalAlias(), None), None, channelUpdate, None, None, None) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3Spec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3Spec.scala index 18f8a86de7..ff76462c35 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3Spec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3Spec.scala @@ -23,7 +23,7 @@ import fr.acinq.eclair.transactions.Transactions._ import fr.acinq.eclair.wire.internal.channel.ChannelCodecsSpec.normal import fr.acinq.eclair.wire.internal.channel.version3.ChannelCodecs3.Codecs._ import fr.acinq.eclair.wire.internal.channel.version3.ChannelCodecs3.channelDataCodec -import fr.acinq.eclair.{BlockHeight, CltvExpiryDelta, Features, MilliSatoshi, UInt64, randomKey} +import fr.acinq.eclair.{BlockHeight, CltvExpiryDelta, Features, MilliSatoshi, RealShortChannelId, ShortChannelId, UInt64, randomKey} import org.scalatest.funsuite.AnyFunSuite import scodec.bits.{ByteVector, HexStringSyntax} @@ -160,7 +160,7 @@ class ChannelCodecs3Spec extends AnyFunSuite { assert(decoded1.asInstanceOf[DATA_NORMAL].closingFeerates == None) val newBin = channelDataCodec.encode(decoded1).require.bytes // make sure that encoding used the new codec - assert(newBin.startsWith(hex"0007")) + assert(newBin.startsWith(hex"0009")) val decoded2 = channelDataCodec.decode(newBin.bits).require.value assert(decoded1 == decoded2) } @@ -250,4 +250,24 @@ class ChannelCodecs3Spec extends AnyFunSuite { } } + test("backwards compatibility with codecs pre-alias") { + { + val bin = hex"0001000000000000000000000000000000000000000000000000000000000000000001010003af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d000010000002a00000000000002220000000002faf0800000000000002710000000000000271000900032ff0000000000022cd2a00cddf20323d320bd14ce0e59b00d62def4d853b88e8bf7dc44c556fc07000000000000022200000000004c4b400000000000002710000000000000138800900032031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f024d4b6cd1361032ca9bd2aeb9d900aa4d45d9ead80ac9423374c451a7254d076602531fe6068134503d2723133227c867ac8fa6c83c537e9a44c3c5bdbdcb1fe33703462779ad4aad39514614751a71085f2f10e1c7a593e4e030efb5b8721ce55b0b03f006a18d5653c4edf5391ff23a61f03ff83d237e880ee61187fa9f379a028e0a00000000000100000000000000000005fffd05aa0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f424066687aadf862bd776c8fc18b8e9f8e20089714856ee233b3902a591d0d5f2925000001f40000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000fd05aa0000000000000000000000000000000000000000000000000000000000000000000000000000001e00000000001e848075877bb41d393b5fb8455ce60ecd8dda001d06316496b14dfa7f895656eeca4a000001f600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000fffd05aa0000000000000000000000000000000000000000000000000000000000000000000000000000000100000000001e848072cd6e8422c407fb6d098690f1130b7ded7ec2f7f5e1d30bd9d521f015363793000001f50000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000fd05aa0000000000000000000000000000000000000000000000000000000000000000000000000000001f00000000002dc6c0648aa5c579fb30f38af744d97d6ec840c7a91277a499a0d780f3e7314eca090b000001f700000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000fffd05aa0000000000000000000000000000000000000000000000000000000000000000000000000000000200000000003d09009f4fb68f3e1dac82202f9aa581ce0bbf1f765df0e9ac3c8c57e20f685abab8ed000001f800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005dc0000000002faf08000000000042c1d8024bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000002b80969800000000002200205e9ed9d4087f82a14496be26b842e968f9ae2e65e331fd93fb97e1f5c6577934475221031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f2103afd229e5da2cc156d1fb929c22bf6878791adad2574614e1c1e5decd65a71a3752aefd010f02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a48848900000000000000000000040047304402202148d2d4aac8c793eb82d31bcf22d4db707b9fd7eee1b89b4b1444c9e19ab71702202bab8c3d997d29163fa0cb255c75afb8ade13617ad1350c1515e9be4a222a04d0147304402206cb12624b253adeb0a41210d63ac6280154923c502202ea16a581bc1839e1e610220178e31542e4a7735d9e243927a5aac00bae1b2889cb9eb785c4d182f3b22a87d01475221031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f2103afd229e5da2cc156d1fb929c22bf6878791adad2574614e1c1e5decd65a71a3752ae000000002148d2d4aac8c793eb82d31bcf22d4db707b9fd7eee1b89b4b1444c9e19ab7172bab8c3d997d29163fa0cb255c75afb8ade13617ad1350c1515e9be4a222a04d00000000000000000000000500fd05aa0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f424066687aadf862bd776c8fc18b8e9f8e20089714856ee233b3902a591d0d5f2925000001f400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000fffd05aa0000000000000000000000000000000000000000000000000000000000000000000000000000001f00000000002dc6c0648aa5c579fb30f38af744d97d6ec840c7a91277a499a0d780f3e7314eca090b000001f700000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000fffd05aa0000000000000000000000000000000000000000000000000000000000000000000000000000001e00000000001e848075877bb41d393b5fb8455ce60ecd8dda001d06316496b14dfa7f895656eeca4a000001f60000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000fd05aa0000000000000000000000000000000000000000000000000000000000000000000000000000000200000000003d09009f4fb68f3e1dac82202f9aa581ce0bbf1f765df0e9ac3c8c57e20f685abab8ed000001f80000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000fd05aa0000000000000000000000000000000000000000000000000000000000000000000000000000000100000000001e848072cd6e8422c407fb6d098690f1130b7ded7ec2f7f5e1d30bd9d521f015363793000001f500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005dc000000000000c35000000000000aae60030303030303030303030303030303030303030303030303030303030303030303462779ad4aad39514614751a71085f2f10e1c7a593e4e030efb5b8721ce55b0b000000000000000000000000000000000000002000000000000000040002000000000000002a00036e9078b299874e92af44d59fcbc9d2190000000000003a9800022a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a000000000000002b0000000000a7d8c00000000000989680ff023730c8e0fc52aff6ba2f618f29e5ebd551c0129e13ce20312df76e4403c5abbc24bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000002b80969800000000002200205e9ed9d4087f82a14496be26b842e968f9ae2e65e331fd93fb97e1f5c6577934475221031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f2103afd229e5da2cc156d1fb929c22bf6878791adad2574614e1c1e5decd65a71a3752ae00000000000000075bcd1541dd93fe76288e183498a7d57065cf472a4413c643730f9c117ca806be6b57f75303a153d48f51439f168ca85ef2fd6f3e0642fc7132f7a530cd50d880ec05cb4c17" + val data = channelDataCodec.decode(bin.bits).require.value.asInstanceOf[DATA_WAIT_FOR_CHANNEL_READY] + assert(data.shortIds.localAlias == ShortChannelId(123456789L)) + assert(data.shortIds.real == RealScidStatus.Temporary(RealShortChannelId(123456789L))) + val binMigrated = channelDataCodec.encode(data).require.toHex + assert(binMigrated.startsWith("000a")) // NB: 01 -> 0a + } + + { + val bin = hex"0007000000000000000000000000000000000000000000000000000000000000000001010003af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d000010000002a00000000000002220000000002faf0800000000000002710000000000000271000900032ff0000000000022cd2a00cddf20323d320bd14ce0e59b00d62def4d853b88e8bf7dc44c556fc07000000000000022200000000004c4b400000000000002710000000000000138800900032031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f024d4b6cd1361032ca9bd2aeb9d900aa4d45d9ead80ac9423374c451a7254d076602531fe6068134503d2723133227c867ac8fa6c83c537e9a44c3c5bdbdcb1fe33703462779ad4aad39514614751a71085f2f10e1c7a593e4e030efb5b8721ce55b0b03f006a18d5653c4edf5391ff23a61f03ff83d237e880ee61187fa9f379a028e0a00000000000100000000000000000005fffd05aa0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f424066687aadf862bd776c8fc18b8e9f8e20089714856ee233b3902a591d0d5f2925000001f40000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000fd05aa0000000000000000000000000000000000000000000000000000000000000000000000000000001e00000000001e848075877bb41d393b5fb8455ce60ecd8dda001d06316496b14dfa7f895656eeca4a000001f600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000fffd05aa0000000000000000000000000000000000000000000000000000000000000000000000000000000100000000001e848072cd6e8422c407fb6d098690f1130b7ded7ec2f7f5e1d30bd9d521f015363793000001f50000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000fd05aa0000000000000000000000000000000000000000000000000000000000000000000000000000001f00000000002dc6c0648aa5c579fb30f38af744d97d6ec840c7a91277a499a0d780f3e7314eca090b000001f700000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000fffd05aa0000000000000000000000000000000000000000000000000000000000000000000000000000000200000000003d09009f4fb68f3e1dac82202f9aa581ce0bbf1f765df0e9ac3c8c57e20f685abab8ed000001f800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005dc0000000002faf08000000000042c1d8024bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000002b80969800000000002200205e9ed9d4087f82a14496be26b842e968f9ae2e65e331fd93fb97e1f5c6577934475221031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f2103afd229e5da2cc156d1fb929c22bf6878791adad2574614e1c1e5decd65a71a3752aefd010f02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a48848900000000000000000000040047304402202148d2d4aac8c793eb82d31bcf22d4db707b9fd7eee1b89b4b1444c9e19ab71702202bab8c3d997d29163fa0cb255c75afb8ade13617ad1350c1515e9be4a222a04d0147304402206cb12624b253adeb0a41210d63ac6280154923c502202ea16a581bc1839e1e610220178e31542e4a7735d9e243927a5aac00bae1b2889cb9eb785c4d182f3b22a87d01475221031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f2103afd229e5da2cc156d1fb929c22bf6878791adad2574614e1c1e5decd65a71a3752ae000000002148d2d4aac8c793eb82d31bcf22d4db707b9fd7eee1b89b4b1444c9e19ab7172bab8c3d997d29163fa0cb255c75afb8ade13617ad1350c1515e9be4a222a04d00000000000000000000000500fd05aa0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f424066687aadf862bd776c8fc18b8e9f8e20089714856ee233b3902a591d0d5f2925000001f400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000fffd05aa0000000000000000000000000000000000000000000000000000000000000000000000000000001f00000000002dc6c0648aa5c579fb30f38af744d97d6ec840c7a91277a499a0d780f3e7314eca090b000001f700000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000fffd05aa0000000000000000000000000000000000000000000000000000000000000000000000000000001e00000000001e848075877bb41d393b5fb8455ce60ecd8dda001d06316496b14dfa7f895656eeca4a000001f60000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000fd05aa0000000000000000000000000000000000000000000000000000000000000000000000000000000200000000003d09009f4fb68f3e1dac82202f9aa581ce0bbf1f765df0e9ac3c8c57e20f685abab8ed000001f80000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000fd05aa0000000000000000000000000000000000000000000000000000000000000000000000000000000100000000001e848072cd6e8422c407fb6d098690f1130b7ded7ec2f7f5e1d30bd9d521f015363793000001f500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005dc000000000000c35000000000000aae60030303030303030303030303030303030303030303030303030303030303030303462779ad4aad39514614751a71085f2f10e1c7a593e4e030efb5b8721ce55b0b000000000000000000000000000000000000002000000000000000040002000000000000002a00036e9078b299874e92af44d59fcbc9d2190000000000003a9800022a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a000000000000002b0000000000a7d8c00000000000989680ff023730c8e0fc52aff6ba2f618f29e5ebd551c0129e13ce20312df76e4403c5abbc24bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000002b80969800000000002200205e9ed9d4087f82a14496be26b842e968f9ae2e65e331fd93fb97e1f5c6577934475221031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f2103afd229e5da2cc156d1fb929c22bf6878791adad2574614e1c1e5decd65a71a3752ae000000000000000000002aff008821c93fbf280b2391826bc70ae858cf815d4afc1816f85445364f188e635d4ae64dfe1c58bedb017dd6f267452444d991b66fcfc638396f72fa6926f69d6125ff01010101010101010101010101010101010101010101010101010101010101010000000000022cd962975eec0101002a000000000000000f0000023f0000003500000003e8000000000000" + val data = channelDataCodec.decode(bin.bits).require.value.asInstanceOf[DATA_NORMAL] + assert(data.shortIds.localAlias == ShortChannelId(42)) + assert(data.shortIds.real == RealScidStatus.Final(RealShortChannelId(42))) + val binMigrated = channelDataCodec.encode(data).require.toHex + assert(binMigrated.startsWith("0009")) + } + } + } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/ExtendedQueriesCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/ExtendedQueriesCodecsSpec.scala index b405869f1e..9d8e643d62 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/ExtendedQueriesCodecsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/ExtendedQueriesCodecsSpec.scala @@ -17,6 +17,8 @@ package fr.acinq.eclair.wire.protocol import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, ByteVector64} +import fr.acinq.eclair.RealShortChannelId +import fr.acinq.eclair.RealShortChannelId import fr.acinq.eclair.router.Sync import fr.acinq.eclair.wire.protocol.LightningMessageCodecs._ import fr.acinq.eclair.wire.protocol.ReplyChannelRangeTlv._ @@ -29,7 +31,7 @@ class ExtendedQueriesCodecsSpec extends AnyFunSuite { test("encode a list of short channel ids") { { // encode/decode with encoding 'uncompressed' - val ids = EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(ShortChannelId(142), ShortChannelId(15465), ShortChannelId(4564676))) + val ids = EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(RealShortChannelId(142), RealShortChannelId(15465), RealShortChannelId(4564676))) val encoded = encodedShortChannelIdsCodec.encode(ids).require val decoded = encodedShortChannelIdsCodec.decode(encoded).require.value assert(decoded == ids) @@ -37,7 +39,7 @@ class ExtendedQueriesCodecsSpec extends AnyFunSuite { { // encode/decode with encoding 'zlib' - val ids = EncodedShortChannelIds(EncodingType.COMPRESSED_ZLIB, List(ShortChannelId(142), ShortChannelId(15465), ShortChannelId(4564676))) + val ids = EncodedShortChannelIds(EncodingType.COMPRESSED_ZLIB, List(RealShortChannelId(142), RealShortChannelId(15465), RealShortChannelId(4564676))) val encoded = encodedShortChannelIdsCodec.encode(ids).require val decoded = encodedShortChannelIdsCodec.decode(encoded).require.value assert(decoded == ids) @@ -65,7 +67,7 @@ class ExtendedQueriesCodecsSpec extends AnyFunSuite { test("encode query_short_channel_ids (no optional data)") { val query_short_channel_id = QueryShortChannelIds( Block.RegtestGenesisBlock.blockId, - EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(ShortChannelId(142), ShortChannelId(15465), ShortChannelId(4564676))), + EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(RealShortChannelId(142), RealShortChannelId(15465), RealShortChannelId(4564676))), TlvStream.empty) val encoded = queryShortChannelIdsCodec.encode(query_short_channel_id).require @@ -76,7 +78,7 @@ class ExtendedQueriesCodecsSpec extends AnyFunSuite { test("encode query_short_channel_ids (with optional data)") { val query_short_channel_id = QueryShortChannelIds( Block.RegtestGenesisBlock.blockId, - EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(ShortChannelId(142), ShortChannelId(15465), ShortChannelId(4564676))), + EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(RealShortChannelId(142), RealShortChannelId(15465), RealShortChannelId(4564676))), TlvStream(QueryShortChannelIdsTlv.EncodedQueryFlags(EncodingType.UNCOMPRESSED, List(1.toByte, 2.toByte, 3.toByte, 4.toByte, 5.toByte)))) val encoded = queryShortChannelIdsCodec.encode(query_short_channel_id).require @@ -87,7 +89,7 @@ class ExtendedQueriesCodecsSpec extends AnyFunSuite { test("encode query_short_channel_ids (with optional data including unknown data)") { val query_short_channel_id = QueryShortChannelIds( Block.RegtestGenesisBlock.blockId, - EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(ShortChannelId(142), ShortChannelId(15465), ShortChannelId(4564676))), + EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(RealShortChannelId(142), RealShortChannelId(15465), RealShortChannelId(4564676))), TlvStream( QueryShortChannelIdsTlv.EncodedQueryFlags(EncodingType.UNCOMPRESSED, List(1.toByte, 2.toByte, 3.toByte, 4.toByte, 5.toByte)) :: Nil, GenericTlv(UInt64(43), ByteVector.fromValidHex("deadbeef")) :: Nil @@ -104,7 +106,7 @@ class ExtendedQueriesCodecsSpec extends AnyFunSuite { Block.RegtestGenesisBlock.blockId, BlockHeight(1), 100, 1.toByte, - EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(ShortChannelId(142), ShortChannelId(15465), ShortChannelId(4564676))), + EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(RealShortChannelId(142), RealShortChannelId(15465), RealShortChannelId(4564676))), None, None) val encoded = replyChannelRangeCodec.encode(replyChannelRange).require @@ -117,7 +119,7 @@ class ExtendedQueriesCodecsSpec extends AnyFunSuite { Block.RegtestGenesisBlock.blockId, BlockHeight(1), 100, 1.toByte, - EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(ShortChannelId(142), ShortChannelId(15465), ShortChannelId(4564676))), + EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(RealShortChannelId(142), RealShortChannelId(15465), RealShortChannelId(4564676))), Some(EncodedTimestamps(EncodingType.COMPRESSED_ZLIB, List(Timestamps(1 unixsec, 1 unixsec), Timestamps(2 unixsec, 2 unixsec), Timestamps(3 unixsec, 3 unixsec)))), None) @@ -131,7 +133,7 @@ class ExtendedQueriesCodecsSpec extends AnyFunSuite { Block.RegtestGenesisBlock.blockId, BlockHeight(1), 100, 1.toByte, - EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(ShortChannelId(142), ShortChannelId(15465), ShortChannelId(4564676))), + EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(RealShortChannelId(142), RealShortChannelId(15465), RealShortChannelId(4564676))), TlvStream( List( EncodedTimestamps(EncodingType.COMPRESSED_ZLIB, List(Timestamps(1 unixsec, 1 unixsec), Timestamps(2 unixsec, 2 unixsec), Timestamps(3 unixsec, 3 unixsec))), diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecsSpec.scala index 9893c6b268..064dfbf758 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecsSpec.scala @@ -221,7 +221,13 @@ class LightningMessageCodecsSpec extends AnyFunSuite { defaultEncoded ++ hex"0000" ++ hex"01021000" -> defaultOpen.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScriptTlv(ByteVector.empty), ChannelTlv.ChannelTypeTlv(ChannelTypes.StaticRemoteKey))), // non-empty upfront_shutdown_script + channel type defaultEncoded ++ hex"0004 01abcdef" ++ hex"0103101000" -> defaultOpen.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScriptTlv(hex"01abcdef"), ChannelTlv.ChannelTypeTlv(ChannelTypes.AnchorOutputs))), - defaultEncoded ++ hex"0002 abcd" ++ hex"0103401000" -> defaultOpen.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScriptTlv(hex"abcd"), ChannelTlv.ChannelTypeTlv(ChannelTypes.AnchorOutputsZeroFeeHtlcTx))), + defaultEncoded ++ hex"0002 abcd" ++ hex"0103401000" -> defaultOpen.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScriptTlv(hex"abcd"), ChannelTlv.ChannelTypeTlv(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)))), + // empty upfront_shutdown_script + channel type (scid-alias) + defaultEncoded ++ hex"0000" ++ hex"0106 400000401000" -> defaultOpen.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScriptTlv(ByteVector.empty), ChannelTlv.ChannelTypeTlv(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = true, zeroConf = false)))), + // empty upfront_shutdown_script + channel type (zeroconf) + defaultEncoded ++ hex"0000" ++ hex"0107 04000000401000" -> defaultOpen.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScriptTlv(ByteVector.empty), ChannelTlv.ChannelTypeTlv(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = true)))), + // empty upfront_shutdown_script + channel type (scid-alias + zeroconf) + defaultEncoded ++ hex"0000" ++ hex"0107 04400000401000" -> defaultOpen.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScriptTlv(ByteVector.empty), ChannelTlv.ChannelTypeTlv(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = true, zeroConf = true)))), ) for ((encoded, expected) <- testCases) { @@ -255,8 +261,8 @@ class LightningMessageCodecsSpec extends AnyFunSuite { val defaultEncoded = hex"0040 0000000000000000000000000000000000000000000000000000000000000000 0100000000000000000000000000000000000000000000000000000000000000 00001388 00000fa0 000000000003d090 00000000000001f4 000000000000c350 000000000000000f 0090 01e3 0009eb10 031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f 024d4b6cd1361032ca9bd2aeb9d900aa4d45d9ead80ac9423374c451a7254d0766 02531fe6068134503d2723133227c867ac8fa6c83c537e9a44c3c5bdbdcb1fe337 03462779ad4aad39514614751a71085f2f10e1c7a593e4e030efb5b8721ce55b0b 0362c0a046dacce86ddd0343c6d3c7c79c2208ba0d9c9cf24a6d046d21d21f90f7 03f006a18d5653c4edf5391ff23a61f03ff83d237e880ee61187fa9f379a028e0a 01" val testCases = Seq( defaultOpen -> defaultEncoded, - defaultOpen.copy(tlvStream = TlvStream(ChannelTypeTlv(ChannelTypes.AnchorOutputsZeroFeeHtlcTx))) -> (defaultEncoded ++ hex"0103401000"), - defaultOpen.copy(tlvStream = TlvStream(UpfrontShutdownScriptTlv(hex"00143adb2d0445c4d491cc7568b10323bd6615a91283"), ChannelTypeTlv(ChannelTypes.AnchorOutputsZeroFeeHtlcTx))) -> (defaultEncoded ++ hex"001600143adb2d0445c4d491cc7568b10323bd6615a91283 0103401000"), + defaultOpen.copy(tlvStream = TlvStream(ChannelTypeTlv(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(false, false)))) -> (defaultEncoded ++ hex"0103401000"), + defaultOpen.copy(tlvStream = TlvStream(UpfrontShutdownScriptTlv(hex"00143adb2d0445c4d491cc7568b10323bd6615a91283"), ChannelTypeTlv(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(false, false)))) -> (defaultEncoded ++ hex"001600143adb2d0445c4d491cc7568b10323bd6615a91283 0103401000"), ) testCases.foreach { case (open, bin) => val decoded = lightningMessageCodec.decode(bin.bits).require.value @@ -347,7 +353,8 @@ class LightningMessageCodecsSpec extends AnyFunSuite { AcceptChannel(randomBytes32(), 3 sat, UInt64(4), 5 sat, 6 msat, 7, CltvExpiryDelta(8), 9, publicKey(1), point(2), point(3), point(4), point(5), point(6)), FundingCreated(randomBytes32(), bin32(0), 3, randomBytes64()), FundingSigned(randomBytes32(), randomBytes64()), - FundingLocked(randomBytes32(), point(2)), + ChannelReady(randomBytes32(), point(2)), + ChannelReady(randomBytes32(), point(2), TlvStream(ChannelReadyTlv.ShortChannelIdTlv(Alias(123456)))), UpdateFee(randomBytes32(), FeeratePerKw(2 sat)), Shutdown(randomBytes32(), bin(47, 0)), ClosingSigned(randomBytes32(), 2 sat, randomBytes64()), @@ -357,19 +364,19 @@ class LightningMessageCodecsSpec extends AnyFunSuite { UpdateFailMalformedHtlc(randomBytes32(), 2, randomBytes32(), 1111), CommitSig(randomBytes32(), randomBytes64(), randomBytes64() :: randomBytes64() :: randomBytes64() :: Nil), RevokeAndAck(randomBytes32(), scalar(0), point(1)), - ChannelAnnouncement(randomBytes64(), randomBytes64(), randomBytes64(), randomBytes64(), Features(bin(7, 9)), Block.RegtestGenesisBlock.hash, ShortChannelId(1), randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey), + ChannelAnnouncement(randomBytes64(), randomBytes64(), randomBytes64(), randomBytes64(), Features(bin(7, 9)), Block.RegtestGenesisBlock.hash, RealShortChannelId(1), randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey), NodeAnnouncement(randomBytes64(), Features(DataLossProtect -> Optional), 1 unixsec, randomKey().publicKey, Color(100.toByte, 200.toByte, 300.toByte), "node-alias", IPv4(InetAddress.getByAddress(Array[Byte](192.toByte, 168.toByte, 1.toByte, 42.toByte)).asInstanceOf[Inet4Address], 42000) :: Nil), ChannelUpdate(randomBytes64(), Block.RegtestGenesisBlock.hash, ShortChannelId(1), 2 unixsec, ChannelUpdate.ChannelFlags.DUMMY, CltvExpiryDelta(3), 4 msat, 5 msat, 6, None), - AnnouncementSignatures(randomBytes32(), ShortChannelId(42), randomBytes64(), randomBytes64()), + AnnouncementSignatures(randomBytes32(), RealShortChannelId(42), randomBytes64(), randomBytes64()), GossipTimestampFilter(Block.RegtestGenesisBlock.blockId, 100000 unixsec, 1500), - QueryShortChannelIds(Block.RegtestGenesisBlock.blockId, EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(ShortChannelId(142), ShortChannelId(15465), ShortChannelId(4564676))), TlvStream.empty), + QueryShortChannelIds(Block.RegtestGenesisBlock.blockId, EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(RealShortChannelId(142), RealShortChannelId(15465), RealShortChannelId(4564676))), TlvStream.empty), QueryChannelRange(Block.RegtestGenesisBlock.blockId, BlockHeight(100000), 1500, TlvStream(QueryChannelRangeTlv.QueryFlags(QueryChannelRangeTlv.QueryFlags.WANT_ALL) :: Nil, unknownTlv :: Nil)), ReplyChannelRange(Block.RegtestGenesisBlock.blockId, BlockHeight(100000), 1500, 1, - EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(ShortChannelId(142), ShortChannelId(15465), ShortChannelId(4564676))), + EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(RealShortChannelId(142), RealShortChannelId(15465), RealShortChannelId(4564676))), TlvStream( EncodedTimestamps(EncodingType.UNCOMPRESSED, List(Timestamps(1 unixsec, 1 unixsec), Timestamps(2 unixsec, 2 unixsec), Timestamps(3 unixsec, 3 unixsec))) :: EncodedChecksums(List(Checksums(1, 1), Checksums(2, 2), Checksums(3, 3))) :: Nil, unknownTlv :: Nil) @@ -402,13 +409,13 @@ class LightningMessageCodecsSpec extends AnyFunSuite { test("non-reg encoding type") { val refs = Map( hex"01050f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206001900000000000000008e0000000000003c69000000000045a6c4" - -> QueryShortChannelIds(Block.RegtestGenesisBlock.blockId, EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(ShortChannelId(142), ShortChannelId(15465), ShortChannelId(4564676))), TlvStream.empty), + -> QueryShortChannelIds(Block.RegtestGenesisBlock.blockId, EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(RealShortChannelId(142), RealShortChannelId(15465), RealShortChannelId(4564676))), TlvStream.empty), hex"01050f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206001601789c636000833e08659309a65c971d0100126e02e3" - -> QueryShortChannelIds(Block.RegtestGenesisBlock.blockId, EncodedShortChannelIds(EncodingType.COMPRESSED_ZLIB, List(ShortChannelId(142), ShortChannelId(15465), ShortChannelId(4564676))), TlvStream.empty), + -> QueryShortChannelIds(Block.RegtestGenesisBlock.blockId, EncodedShortChannelIds(EncodingType.COMPRESSED_ZLIB, List(RealShortChannelId(142), RealShortChannelId(15465), RealShortChannelId(4564676))), TlvStream.empty), hex"01050f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206001900000000000000008e0000000000003c69000000000045a6c4010400010204" - -> QueryShortChannelIds(Block.RegtestGenesisBlock.blockId, EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(ShortChannelId(142), ShortChannelId(15465), ShortChannelId(4564676))), TlvStream(QueryShortChannelIdsTlv.EncodedQueryFlags(EncodingType.UNCOMPRESSED, List(1, 2, 4)))), + -> QueryShortChannelIds(Block.RegtestGenesisBlock.blockId, EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(RealShortChannelId(142), RealShortChannelId(15465), RealShortChannelId(4564676))), TlvStream(QueryShortChannelIdsTlv.EncodedQueryFlags(EncodingType.UNCOMPRESSED, List(1, 2, 4)))), hex"01050f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206001601789c636000833e08659309a65c971d0100126e02e3010c01789c6364620100000e0008" - -> QueryShortChannelIds(Block.RegtestGenesisBlock.blockId, EncodedShortChannelIds(EncodingType.COMPRESSED_ZLIB, List(ShortChannelId(142), ShortChannelId(15465), ShortChannelId(4564676))), TlvStream(QueryShortChannelIdsTlv.EncodedQueryFlags(EncodingType.COMPRESSED_ZLIB, List(1, 2, 4)))) + -> QueryShortChannelIds(Block.RegtestGenesisBlock.blockId, EncodedShortChannelIds(EncodingType.COMPRESSED_ZLIB, List(RealShortChannelId(142), RealShortChannelId(15465), RealShortChannelId(4564676))), TlvStream(QueryShortChannelIdsTlv.EncodedQueryFlags(EncodingType.COMPRESSED_ZLIB, List(1, 2, 4)))) ) refs.forall { @@ -430,28 +437,28 @@ class LightningMessageCodecsSpec extends AnyFunSuite { TlvStream(QueryChannelRangeTlv.QueryFlags(QueryChannelRangeTlv.QueryFlags.WANT_ALL))) -> hex"01070f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206000088b800000064010103", ReplyChannelRange(Block.RegtestGenesisBlock.blockId, BlockHeight(756230), 1500, 1, - EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(ShortChannelId(142), ShortChannelId(15465), ShortChannelId(4564676))), None, None) -> + EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(RealShortChannelId(142), RealShortChannelId(15465), RealShortChannelId(4564676))), None, None) -> hex"01080f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206000b8a06000005dc01001900000000000000008e0000000000003c69000000000045a6c4", ReplyChannelRange(Block.RegtestGenesisBlock.blockId, BlockHeight(1600), 110, 1, - EncodedShortChannelIds(EncodingType.COMPRESSED_ZLIB, List(ShortChannelId(142), ShortChannelId(15465), ShortChannelId(265462))), None, None) -> + EncodedShortChannelIds(EncodingType.COMPRESSED_ZLIB, List(RealShortChannelId(142), RealShortChannelId(15465), RealShortChannelId(265462))), None, None) -> hex"01080f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206000006400000006e01001601789c636000833e08659309a65878be010010a9023a", ReplyChannelRange(Block.RegtestGenesisBlock.blockId, BlockHeight(122334), 1500, 1, - EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(ShortChannelId(12355), ShortChannelId(489686), ShortChannelId(4645313))), + EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(RealShortChannelId(12355), RealShortChannelId(489686), RealShortChannelId(4645313))), Some(EncodedTimestamps(EncodingType.UNCOMPRESSED, List(Timestamps(164545 unixsec, 948165 unixsec), Timestamps(489645 unixsec, 4786864 unixsec), Timestamps(46456 unixsec, 9788415 unixsec)))), Some(EncodedChecksums(List(Checksums(1111, 2222), Checksums(3333, 4444), Checksums(5555, 6666))))) -> hex"01080f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e22060001ddde000005dc01001900000000000000304300000000000778d6000000000046e1c1011900000282c1000e77c5000778ad00490ab00000b57800955bff031800000457000008ae00000d050000115c000015b300001a0a", ReplyChannelRange(Block.RegtestGenesisBlock.blockId, BlockHeight(122334), 1500, 1, - EncodedShortChannelIds(EncodingType.COMPRESSED_ZLIB, List(ShortChannelId(12355), ShortChannelId(489686), ShortChannelId(4645313))), + EncodedShortChannelIds(EncodingType.COMPRESSED_ZLIB, List(RealShortChannelId(12355), RealShortChannelId(489686), RealShortChannelId(4645313))), Some(EncodedTimestamps(EncodingType.COMPRESSED_ZLIB, List(Timestamps(164545 unixsec, 948165 unixsec), Timestamps(489645 unixsec, 4786864 unixsec), Timestamps(46456 unixsec, 9788415 unixsec)))), Some(EncodedChecksums(List(Checksums(1111, 2222), Checksums(3333, 4444), Checksums(5555, 6666))))) -> hex"01080f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e22060001ddde000005dc01001801789c63600001036730c55e710d4cbb3d3c080017c303b1012201789c63606a3ac8c0577e9481bd622d8327d7060686ad150c53a3ff0300554707db031800000457000008ae00000d050000115c000015b300001a0a", - QueryShortChannelIds(Block.RegtestGenesisBlock.blockId, EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(ShortChannelId(142), ShortChannelId(15465), ShortChannelId(4564676))), TlvStream.empty) -> + QueryShortChannelIds(Block.RegtestGenesisBlock.blockId, EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(RealShortChannelId(142), RealShortChannelId(15465), RealShortChannelId(4564676))), TlvStream.empty) -> hex"01050f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206001900000000000000008e0000000000003c69000000000045a6c4", - QueryShortChannelIds(Block.RegtestGenesisBlock.blockId, EncodedShortChannelIds(EncodingType.COMPRESSED_ZLIB, List(ShortChannelId(4564), ShortChannelId(178622), ShortChannelId(4564676))), TlvStream.empty) -> + QueryShortChannelIds(Block.RegtestGenesisBlock.blockId, EncodedShortChannelIds(EncodingType.COMPRESSED_ZLIB, List(RealShortChannelId(4564), RealShortChannelId(178622), RealShortChannelId(4564676))), TlvStream.empty) -> hex"01050f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206001801789c63600001c12b608a69e73e30edbaec0800203b040e", - QueryShortChannelIds(Block.RegtestGenesisBlock.blockId, EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(ShortChannelId(12232), ShortChannelId(15556), ShortChannelId(4564676))), TlvStream(QueryShortChannelIdsTlv.EncodedQueryFlags(EncodingType.COMPRESSED_ZLIB, List(1, 2, 4)))) -> + QueryShortChannelIds(Block.RegtestGenesisBlock.blockId, EncodedShortChannelIds(EncodingType.UNCOMPRESSED, List(RealShortChannelId(12232), RealShortChannelId(15556), RealShortChannelId(4564676))), TlvStream(QueryShortChannelIdsTlv.EncodedQueryFlags(EncodingType.COMPRESSED_ZLIB, List(1, 2, 4)))) -> hex"01050f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e22060019000000000000002fc80000000000003cc4000000000045a6c4010c01789c6364620100000e0008", - QueryShortChannelIds(Block.RegtestGenesisBlock.blockId, EncodedShortChannelIds(EncodingType.COMPRESSED_ZLIB, List(ShortChannelId(14200), ShortChannelId(46645), ShortChannelId(4564676))), TlvStream(QueryShortChannelIdsTlv.EncodedQueryFlags(EncodingType.COMPRESSED_ZLIB, List(1, 2, 4)))) -> + QueryShortChannelIds(Block.RegtestGenesisBlock.blockId, EncodedShortChannelIds(EncodingType.COMPRESSED_ZLIB, List(RealShortChannelId(14200), RealShortChannelId(46645), RealShortChannelId(4564676))), TlvStream(QueryShortChannelIdsTlv.EncodedQueryFlags(EncodingType.COMPRESSED_ZLIB, List(1, 2, 4)))) -> hex"01050f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206001801789c63600001f30a30c5b0cd144cb92e3b020017c6034a010c01789c6364620100000e0008" ) diff --git a/eclair-front/src/main/scala/fr/acinq/eclair/router/FrontRouter.scala b/eclair-front/src/main/scala/fr/acinq/eclair/router/FrontRouter.scala index aed2186f54..ed1f868cb3 100644 --- a/eclair-front/src/main/scala/fr/acinq/eclair/router/FrontRouter.scala +++ b/eclair-front/src/main/scala/fr/acinq/eclair/router/FrontRouter.scala @@ -23,6 +23,7 @@ import akka.event.LoggingAdapter import fr.acinq.bitcoin.scalacompat.ByteVector32 import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.eclair.Logs.LogCategory +import fr.acinq.eclair.RealShortChannelId import fr.acinq.eclair.crypto.TransportHandler import fr.acinq.eclair.io.Peer.PeerRoutingMessage import fr.acinq.eclair.router.Router._ @@ -110,7 +111,7 @@ class FrontRouter(routerConf: RouterConf, remoteRouter: ActorRef, initialized: O origin.peerConnection ! TransportHandler.ReadAck(ann) Metrics.gossipDropped(ann).increment() d - case u: ChannelUpdate if d.channels.contains(u.shortChannelId) && d.channels(u.shortChannelId).getChannelUpdateSameSideAs(u).contains(u) => + case u: ChannelUpdate if d.channels.get(RealShortChannelId(u.shortChannelId.toLong)).exists(_.getChannelUpdateSameSideAs(u).contains(u)) => origin.peerConnection ! TransportHandler.ReadAck(ann) Metrics.gossipDropped(ann).increment() d @@ -213,7 +214,7 @@ object FrontRouter { // @formatter:on case class Data(nodes: Map[PublicKey, NodeAnnouncement], - channels: SortedMap[ShortChannelId, PublicChannel], + channels: SortedMap[RealShortChannelId, PublicChannel], processing: Map[AnnouncementMessage, Set[RemoteGossip]], accepted: Map[AnnouncementMessage, Set[RemoteGossip]], rebroadcast: Rebroadcast) @@ -263,7 +264,7 @@ object FrontRouter { case ChannelsDiscovered(channels) => log.debug("adding {} channels", channels.size) - val channels1 = channels.foldLeft(SortedMap.empty[ShortChannelId, PublicChannel]) { + val channels1 = channels.foldLeft(SortedMap.empty[RealShortChannelId, PublicChannel]) { case (channels, sc) => channels + (sc.ann.shortChannelId -> PublicChannel(sc.ann, ByteVector32.Zeroes, sc.capacity, sc.u1_opt, sc.u2_opt, None)) } val d1 = d.copy(channels = d.channels ++ channels1) @@ -280,7 +281,7 @@ object FrontRouter { case ChannelUpdatesReceived(updates) => log.debug("adding/updating {} channel_updates", updates.size) val channels1 = updates.foldLeft(d.channels) { - case (channels, u) => channels.get(u.shortChannelId) match { + case (channels, u) => channels.get(RealShortChannelId(u.shortChannelId.toLong)) match { case Some(c) => channels + (c.ann.shortChannelId -> c.updateChannelUpdateSameSideAs(u)) case None => channels } @@ -309,7 +310,7 @@ object FrontRouter { case n: NodeAnnouncement => d.rebroadcast.copy(nodes = d.rebroadcast.nodes + (n -> origins)) case c: ChannelAnnouncement => d.rebroadcast.copy(channels = d.rebroadcast.channels + (c -> origins)) case u: ChannelUpdate => - if (d.channels.contains(u.shortChannelId)) { + if (d.channels.contains(RealShortChannelId(u.shortChannelId.toLong))) { d.rebroadcast.copy(updates = d.rebroadcast.updates + (u -> origins)) } else { d.rebroadcast // private channel, we don't rebroadcast the channel_update diff --git a/eclair-front/src/test/scala/fr/acinq/eclair/router/FrontRouterSpec.scala b/eclair-front/src/test/scala/fr/acinq/eclair/router/FrontRouterSpec.scala index a72d82c11b..0501dfcf67 100644 --- a/eclair-front/src/test/scala/fr/acinq/eclair/router/FrontRouterSpec.scala +++ b/eclair-front/src/test/scala/fr/acinq/eclair/router/FrontRouterSpec.scala @@ -340,12 +340,12 @@ object FrontRouterSpec { val ann_e = makeNodeAnnouncement(priv_e, "node-E", Color(-50, 0, 10), Nil, Features.empty) val ann_f = makeNodeAnnouncement(priv_f, "node-F", Color(30, 10, -50), Nil, Features.empty) - val channelId_ab = ShortChannelId(BlockHeight(420000), 1, 0) - val channelId_bc = ShortChannelId(BlockHeight(420000), 2, 0) - val channelId_cd = ShortChannelId(BlockHeight(420000), 3, 0) - val channelId_ef = ShortChannelId(BlockHeight(420000), 4, 0) + val channelId_ab = RealShortChannelId(BlockHeight(420000), 1, 0) + val channelId_bc = RealShortChannelId(BlockHeight(420000), 2, 0) + val channelId_cd = RealShortChannelId(BlockHeight(420000), 3, 0) + val channelId_ef = RealShortChannelId(BlockHeight(420000), 4, 0) - def channelAnnouncement(shortChannelId: ShortChannelId, node1_priv: PrivateKey, node2_priv: PrivateKey, funding1_priv: PrivateKey, funding2_priv: PrivateKey) = { + def channelAnnouncement(shortChannelId: RealShortChannelId, node1_priv: PrivateKey, node2_priv: PrivateKey, funding1_priv: PrivateKey, funding2_priv: PrivateKey) = { val witness = Announcements.generateChannelAnnouncementWitness(Block.RegtestGenesisBlock.hash, shortChannelId, node1_priv.publicKey, node2_priv.publicKey, funding1_priv.publicKey, funding2_priv.publicKey, Features.empty) val node1_sig = Announcements.signChannelAnnouncement(witness, node1_priv) val funding1_sig = Announcements.signChannelAnnouncement(witness, funding1_priv) diff --git a/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Channel.scala b/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Channel.scala index 61fece11a3..f7a04b0263 100644 --- a/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Channel.scala +++ b/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Channel.scala @@ -32,19 +32,28 @@ trait Channel { import fr.acinq.eclair.api.serde.JsonSupport.{formats, marshaller, serialization} + val supportedChannelTypes = Set( + ChannelTypes.Standard, + ChannelTypes.StaticRemoteKey, + ChannelTypes.AnchorOutputs, + ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false), + ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = true), + ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = true, zeroConf = false), + ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = true, zeroConf = true) + ).map(ct => ct.toString -> ct).toMap // we use the toString method as name in the api + val open: Route = postRequest("open") { implicit t => formFields(nodeIdFormParam, "fundingSatoshis".as[Satoshi], "pushMsat".as[MilliSatoshi].?, "channelType".?, "fundingFeerateSatByte".as[FeeratePerByte].?, "announceChannel".as[Boolean].?, "openTimeoutSeconds".as[Timeout].?) { - (nodeId, fundingSatoshis, pushMsat, channelType, fundingFeerateSatByte, announceChannel_opt, openTimeout_opt) => - val (channelTypeOk, channelType_opt) = channelType match { - case Some(str) if str == ChannelTypes.Standard.toString => (true, Some(ChannelTypes.Standard)) - case Some(str) if str == ChannelTypes.StaticRemoteKey.toString => (true, Some(ChannelTypes.StaticRemoteKey)) - case Some(str) if str == ChannelTypes.AnchorOutputs.toString => (true, Some(ChannelTypes.AnchorOutputs)) - case Some(str) if str == ChannelTypes.AnchorOutputsZeroFeeHtlcTx.toString => (true, Some(ChannelTypes.AnchorOutputsZeroFeeHtlcTx)) - case Some(_) => (false, None) + (nodeId, fundingSatoshis, pushMsat, channelTypeName_opt, fundingFeerateSatByte, announceChannel_opt, openTimeout_opt) => + val (channelTypeOk, channelType_opt) = channelTypeName_opt match { + case Some(channelTypeName) => supportedChannelTypes.get(channelTypeName) match { + case Some(channelType) => (true, Some(channelType)) + case None => (false, None) // invalid channel type name + } case None => (true, None) } if (!channelTypeOk) { - reject(MalformedFormFieldRejection("channelType", s"Channel type not supported: must be ${ChannelTypes.Standard.toString}, ${ChannelTypes.StaticRemoteKey.toString}, ${ChannelTypes.AnchorOutputs.toString} or ${ChannelTypes.AnchorOutputsZeroFeeHtlcTx.toString}")) + reject(MalformedFormFieldRejection("channelType", s"Channel type not supported: must be one of ${supportedChannelTypes.keys.mkString(",")}")) } else { complete { eclairApi.open(nodeId, fundingSatoshis, pushMsat, channelType_opt, fundingFeerateSatByte, announceChannel_opt, openTimeout_opt) diff --git a/eclair-node/src/main/scala/fr/acinq/eclair/api/serde/FormParamExtractors.scala b/eclair-node/src/main/scala/fr/acinq/eclair/api/serde/FormParamExtractors.scala index 4843701bd6..5f1bc093d8 100644 --- a/eclair-node/src/main/scala/fr/acinq/eclair/api/serde/FormParamExtractors.scala +++ b/eclair-node/src/main/scala/fr/acinq/eclair/api/serde/FormParamExtractors.scala @@ -27,7 +27,7 @@ import fr.acinq.eclair.crypto.Sphinx import fr.acinq.eclair.io.NodeURI import fr.acinq.eclair.payment.Bolt11Invoice import fr.acinq.eclair.wire.protocol.MessageOnionCodecs.blindedRouteCodec -import fr.acinq.eclair.{MilliSatoshi, ShortChannelId, TimestampSecond} +import fr.acinq.eclair.{UnspecifiedShortChannelId, MilliSatoshi, ShortChannelId, TimestampSecond} import scodec.bits.ByteVector import java.util.UUID diff --git a/eclair-node/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala b/eclair-node/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala index de7394b551..e134c67b7a 100644 --- a/eclair-node/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala +++ b/eclair-node/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala @@ -374,7 +374,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM assert(handled) assert(status == OK) assert(entityAs[String] == "\"created channel 56d7d6eda04d80138270c49709f1eadb5ab4939e5061309ccdacdb98ce637d0e\"") - eclair.open(nodeId, 25000 sat, None, Some(ChannelTypes.AnchorOutputsZeroFeeHtlcTx), None, None, None)(any[Timeout]).wasCalled(once) + eclair.open(nodeId, 25000 sat, None, Some(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)), None, None, None)(any[Timeout]).wasCalled(once) } } @@ -994,7 +994,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM val mockChannelUpdate1 = ChannelUpdate( signature = ByteVector64.fromValidHex("92cf3f12e161391986eb2cd7106ddab41a23c734f8f1ed120fb64f4b91f98f690ecf930388e62965f8aefbf1adafcd25a572669a125396dcfb83615208754679"), chainHash = ByteVector32.fromValidHex("024b7b3626554c44dcc2454ee3812458bfa68d9fced466edfab470844cb7ffe2"), - shortChannelId = ShortChannelId(BlockHeight(1), 2, 3), + shortChannelId = RealShortChannelId(BlockHeight(1), 2, 3), timestamp = 0 unixsec, channelFlags = ChannelUpdate.ChannelFlags.DUMMY, cltvExpiryDelta = CltvExpiryDelta(0), @@ -1003,8 +1003,8 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM feeProportionalMillionths = 1, htlcMaximumMsat = None ) - val mockChannelUpdate2 = mockChannelUpdate1.copy(shortChannelId = ShortChannelId(BlockHeight(1), 2, 4)) - val mockChannelUpdate3 = mockChannelUpdate1.copy(shortChannelId = ShortChannelId(BlockHeight(1), 2, 5)) + val mockChannelUpdate2 = mockChannelUpdate1.copy(shortChannelId = RealShortChannelId(BlockHeight(1), 2, 4)) + val mockChannelUpdate3 = mockChannelUpdate1.copy(shortChannelId = RealShortChannelId (BlockHeight(1), 2, 5)) val mockHop1 = Router.ChannelHop(mockChannelUpdate1.shortChannelId, PublicKey.fromBin(ByteVector.fromValidHex("03007e67dc5a8fd2b2ef21cb310ab6359ddb51f3f86a8b79b8b1e23bc3a6ea150a")), PublicKey.fromBin(ByteVector.fromValidHex("026105f6cb4862810be989385d16f04b0f748f6f2a14040338b1a534d45b4be1c1")), ChannelRelayParams.FromAnnouncement(mockChannelUpdate1)) val mockHop2 = Router.ChannelHop(mockChannelUpdate2.shortChannelId, mockHop1.nextNodeId, PublicKey.fromBin(ByteVector.fromValidHex("038cfa2b5857843ee90cff91b06f692c0d8fe201921ee6387aee901d64f43699f0")), ChannelRelayParams.FromAnnouncement(mockChannelUpdate2)) diff --git a/pom.xml b/pom.xml index 48f3e25e5f..adb6006aea 100644 --- a/pom.xml +++ b/pom.xml @@ -132,7 +132,7 @@ net.alchim31.maven scala-maven-plugin - 4.6.1 + 4.6.2 -feature @@ -228,7 +228,7 @@ org.scalatest scalatest-maven-plugin - 2.0.0 + 2.0.2 true I From f47c7c39faf23d4da3674aba2868829895fc29ca Mon Sep 17 00:00:00 2001 From: Bastien Teinturier <31281497+t-bast@users.noreply.github.com> Date: Wed, 15 Jun 2022 14:25:35 +0200 Subject: [PATCH 093/121] Don't block when generating shutdown script (#2284) We were previously using a blocking call, which can create issues on failures and timeouts. --- .../fr/acinq/eclair/channel/Helpers.scala | 12 --- .../main/scala/fr/acinq/eclair/io/Peer.scala | 75 ++++++++++++------- .../ChannelStateTestsHelperMethods.scala | 5 +- .../scala/fr/acinq/eclair/io/PeerSpec.scala | 2 +- 4 files changed, 54 insertions(+), 40 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala index 238b9f4a9e..8bc231e0d9 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala @@ -266,18 +266,6 @@ object Helpers { result } - /** NB: this is a blocking call, use carefully! */ - def getFinalScriptPubKey(wallet: OnChainAddressGenerator, chainHash: ByteVector32)(implicit ec: ExecutionContext): ByteVector = { - import scala.concurrent.duration._ - val finalAddress = Await.result(wallet.getReceiveAddress(), 40 seconds) - Script.write(addressToPublicKeyScript(finalAddress, chainHash)) - } - - /** NB: this is a blocking call, use carefully! */ - def getWalletPaymentBasepoint(wallet: OnChainAddressGenerator)(implicit ec: ExecutionContext): PublicKey = { - Await.result(wallet.getReceivePubkey(), 40 seconds) - } - def getRelayFees(nodeParams: NodeParams, remoteNodeId: PublicKey, commitments: Commitments): RelayFees = { val defaultFees = nodeParams.relayParams.defaultFees(commitments.announceChannel) nodeParams.db.peers.getRelayFees(remoteNodeId).getOrElse(defaultFees) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala index a37270e758..437767420b 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala @@ -42,7 +42,8 @@ import fr.acinq.eclair.wire.protocol import fr.acinq.eclair.wire.protocol.{Error, HasChannelId, HasTemporaryChannelId, LightningMessage, NodeAddress, OnionMessage, RoutingMessage, UnknownMessage, Warning} import scodec.bits.ByteVector -import scala.concurrent.ExecutionContext +import scala.concurrent.{ExecutionContext, Future} +import scala.util.{Failure, Success} /** * This actor represents a logical peer. There is one [[Peer]] per unique remote node id at all time. @@ -98,6 +99,12 @@ class Peer(val nodeParams: NodeParams, remoteNodeId: PublicKey, wallet: OnChainA log.info(s"channel id switch: previousId=$temporaryChannelId nextId=$channelId") stay() using d.copy(channels = d.channels + (FinalChannelId(channelId) -> channel)) + case Event(e: SpawnChannelInitiator, _) => + e.origin ! Status.Failure(new RuntimeException("channel creation failed: disconnected")) + stay() + + case Event(_: SpawnChannelNonInitiator, _) => stay() // we got disconnected before creating the channel actor + case Event(_: LightningMessage, _) => stay() // we probably just got disconnected and that's the last messages we received } @@ -144,24 +151,33 @@ class Peer(val nodeParams: NodeParams, remoteNodeId: PublicKey, wallet: OnChainA sender() ! Status.Failure(new RuntimeException(s"fundingSatoshis=${c.fundingSatoshis} is too big for the current settings, increase 'eclair.max-funding-satoshis' (see eclair.conf)")) stay() } else { - val channelConfig = ChannelConfig.standard // If a channel type was provided, we directly use it instead of computing it based on local and remote features. val channelFlags = c.channelFlags.getOrElse(nodeParams.channelConf.channelFlags) val channelType = c.channelType_opt.getOrElse(ChannelTypes.defaultFromFeatures(d.localFeatures, d.remoteFeatures, channelFlags.announceChannel)) - val (channel, localParams) = createNewChannel(nodeParams, d.localFeatures, channelType, isInitiator = true, c.fundingSatoshis, origin_opt = Some(sender())) - c.timeout_opt.map(openTimeout => context.system.scheduler.scheduleOnce(openTimeout.duration, channel, Channel.TickChannelOpenTimeout)(context.dispatcher)) - val temporaryChannelId = randomBytes32() - val channelFeeratePerKw = nodeParams.onChainFeeConf.getCommitmentFeerate(remoteNodeId, channelType, c.fundingSatoshis, None) - val fundingTxFeeratePerKw = c.fundingTxFeeratePerKw_opt.getOrElse(nodeParams.onChainFeeConf.feeEstimator.getFeeratePerKw(target = nodeParams.onChainFeeConf.feeTargets.fundingBlockTarget)) - log.info(s"requesting a new channel with type=$channelType fundingSatoshis=${c.fundingSatoshis}, pushMsat=${c.pushMsat} and fundingFeeratePerByte=${c.fundingTxFeeratePerKw_opt} temporaryChannelId=$temporaryChannelId localParams=$localParams") - channel ! INPUT_INIT_FUNDER(temporaryChannelId, c.fundingSatoshis, c.pushMsat, channelFeeratePerKw, fundingTxFeeratePerKw, localParams, d.peerConnection, d.remoteInit, channelFlags, channelConfig, channelType) - stay() using d.copy(channels = d.channels + (TemporaryChannelId(temporaryChannelId) -> channel)) + // NB: we need to capture parameters in a val to use them in andThen + val selfRef = self + val origin = sender() + implicit val ec: ExecutionContext = ExecutionContext.Implicits.global + createLocalParams(nodeParams, d.localFeatures, channelType, isInitiator = true, c.fundingSatoshis).andThen { + case Success(localParams) => selfRef ! SpawnChannelInitiator(c, ChannelConfig.standard, channelType, localParams, origin) + case Failure(t) => origin ! Status.Failure(new RuntimeException("channel creation failed", t)) + } + stay() } + case Event(SpawnChannelInitiator(c, channelConfig, channelType, localParams, origin), d: ConnectedData) => + val channel = spawnChannel(Some(origin)) + c.timeout_opt.map(openTimeout => context.system.scheduler.scheduleOnce(openTimeout.duration, channel, Channel.TickChannelOpenTimeout)(context.dispatcher)) + val temporaryChannelId = randomBytes32() + val channelFeeratePerKw = nodeParams.onChainFeeConf.getCommitmentFeerate(remoteNodeId, channelType, c.fundingSatoshis, None) + val fundingTxFeeratePerKw = c.fundingTxFeeratePerKw_opt.getOrElse(nodeParams.onChainFeeConf.feeEstimator.getFeeratePerKw(target = nodeParams.onChainFeeConf.feeTargets.fundingBlockTarget)) + log.info(s"requesting a new channel with type=$channelType fundingSatoshis=${c.fundingSatoshis}, pushMsat=${c.pushMsat} and fundingFeeratePerByte=${c.fundingTxFeeratePerKw_opt} temporaryChannelId=$temporaryChannelId localParams=$localParams") + channel ! INPUT_INIT_FUNDER(temporaryChannelId, c.fundingSatoshis, c.pushMsat, channelFeeratePerKw, fundingTxFeeratePerKw, localParams, d.peerConnection, d.remoteInit, c.channelFlags.getOrElse(nodeParams.channelConf.channelFlags), channelConfig, channelType) + stay() using d.copy(channels = d.channels + (TemporaryChannelId(temporaryChannelId) -> channel)) + case Event(msg: protocol.OpenChannel, d: ConnectedData) => d.channels.get(TemporaryChannelId(msg.temporaryChannelId)) match { case None => - val channelConfig = ChannelConfig.standard val chosenChannelType: Either[ChannelException, SupportedChannelType] = msg.channelType_opt match { // remote explicitly specifies a channel type: we check whether we want to allow it case Some(remoteChannelType) => ChannelTypes.areCompatible(d.localFeatures, remoteChannelType) match { @@ -175,12 +191,14 @@ class Peer(val nodeParams: NodeParams, remoteNodeId: PublicKey, wallet: OnChainA } chosenChannelType match { case Right(channelType) => - val (channel, localParams) = createNewChannel(nodeParams, d.localFeatures, channelType, isInitiator = false, fundingAmount = msg.fundingSatoshis, origin_opt = None) - val temporaryChannelId = msg.temporaryChannelId - log.info(s"accepting a new channel with type=$channelType temporaryChannelId=$temporaryChannelId localParams=$localParams") - channel ! INPUT_INIT_FUNDEE(temporaryChannelId, localParams, d.peerConnection, d.remoteInit, channelConfig, channelType) - channel ! msg - stay() using d.copy(channels = d.channels + (TemporaryChannelId(temporaryChannelId) -> channel)) + // NB: we need to capture parameters in a val to use them in andThen + val selfRef = self + implicit val ec: ExecutionContext = ExecutionContext.Implicits.global + createLocalParams(nodeParams, d.localFeatures, channelType, isInitiator = false, msg.fundingSatoshis).andThen { + case Success(localParams) => selfRef ! SpawnChannelNonInitiator(msg, ChannelConfig.standard, channelType, localParams) + case Failure(_) => selfRef ! Peer.OutgoingMessage(Error(msg.temporaryChannelId, "channel creation failed"), d.peerConnection) + } + stay() case Left(ex) => log.warning(s"ignoring open_channel: ${ex.getMessage}") val err = Error(msg.temporaryChannelId, ex.getMessage) @@ -192,6 +210,14 @@ class Peer(val nodeParams: NodeParams, remoteNodeId: PublicKey, wallet: OnChainA stay() } + case Event(SpawnChannelNonInitiator(open, channelConfig, channelType, localParams), d: ConnectedData) => + val channel = spawnChannel(None) + val temporaryChannelId = open.temporaryChannelId + log.info(s"accepting a new channel with type=$channelType temporaryChannelId=$temporaryChannelId localParams=$localParams") + channel ! INPUT_INIT_FUNDEE(temporaryChannelId, localParams, d.peerConnection, d.remoteInit, channelConfig, channelType) + channel ! open + stay() using d.copy(channels = d.channels + (TemporaryChannelId(temporaryChannelId) -> channel)) + case Event(msg: HasChannelId, d: ConnectedData) => d.channels.get(FinalChannelId(msg.channelId)) match { case Some(channel) => channel forward msg @@ -357,16 +383,12 @@ class Peer(val nodeParams: NodeParams, remoteNodeId: PublicKey, wallet: OnChainA s(e) } - def createNewChannel(nodeParams: NodeParams, initFeatures: Features[InitFeature], channelType: SupportedChannelType, isInitiator: Boolean, fundingAmount: Satoshi, origin_opt: Option[ActorRef]): (ActorRef, LocalParams) = { - val (finalScript, walletStaticPaymentBasepoint) = if (channelType.paysDirectlyToWallet) { - val walletKey = Helpers.getWalletPaymentBasepoint(wallet)(ExecutionContext.Implicits.global) - (Script.write(Script.pay2wpkh(walletKey)), Some(walletKey)) + def createLocalParams(nodeParams: NodeParams, initFeatures: Features[InitFeature], channelType: SupportedChannelType, isInitiator: Boolean, fundingAmount: Satoshi)(implicit ec: ExecutionContext): Future[LocalParams] = { + if (channelType.paysDirectlyToWallet) { + wallet.getReceivePubkey().map(walletKey => makeChannelParams(nodeParams, initFeatures, Script.write(Script.pay2wpkh(walletKey)), Some(walletKey), isInitiator, fundingAmount)) } else { - (Helpers.getFinalScriptPubKey(wallet, nodeParams.chainHash)(ExecutionContext.Implicits.global), None) + wallet.getReceiveAddress().map(address => makeChannelParams(nodeParams, initFeatures, Script.write(addressToPublicKeyScript(address, nodeParams.chainHash)), None, isInitiator, fundingAmount)) } - val localParams = makeChannelParams(nodeParams, initFeatures, finalScript, walletStaticPaymentBasepoint, isInitiator, fundingAmount) - val channel = spawnChannel(origin_opt) - (channel, localParams) } def spawnChannel(origin_opt: Option[ActorRef]): ActorRef = { @@ -473,6 +495,9 @@ object Peer { fundingTxFeeratePerKw_opt.foreach(feeratePerKw => require(feeratePerKw >= FeeratePerKw.MinimumFeeratePerKw, s"fee rate $feeratePerKw is below minimum ${FeeratePerKw.MinimumFeeratePerKw} rate/kw")) } + private case class SpawnChannelInitiator(cmd: Peer.OpenChannel, channelConfig: ChannelConfig, channelType: SupportedChannelType, localParams: LocalParams, origin: ActorRef) + private case class SpawnChannelNonInitiator(msg: protocol.OpenChannel, channelConfig: ChannelConfig, channelType: SupportedChannelType, localParams: LocalParams) + case class GetPeerInfo(replyTo: Option[typed.ActorRef[PeerInfoResponse]]) sealed trait PeerInfoResponse { def nodeId: PublicKey diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala index b25046e630..5f279ef20a 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala @@ -43,6 +43,7 @@ import org.scalatest.Assertions import org.scalatest.concurrent.Eventually import java.util.UUID +import scala.concurrent.Await import scala.concurrent.duration._ object ChannelStateTestsTags { @@ -185,14 +186,14 @@ trait ChannelStateTestsBase extends Assertions with Eventually { implicit val ec: scala.concurrent.ExecutionContext = scala.concurrent.ExecutionContext.global val aliceParams = Alice.channelParams .modify(_.initFeatures).setTo(aliceInitFeatures) - .modify(_.walletStaticPaymentBasepoint).setToIf(channelType.paysDirectlyToWallet)(Some(Helpers.getWalletPaymentBasepoint(wallet))) + .modify(_.walletStaticPaymentBasepoint).setToIf(channelType.paysDirectlyToWallet)(Some(Await.result(wallet.getReceivePubkey(), 10 seconds))) .modify(_.maxHtlcValueInFlightMsat).setToIf(tags.contains(ChannelStateTestsTags.NoMaxHtlcValueInFlight))(UInt64.MaxValue) .modify(_.maxHtlcValueInFlightMsat).setToIf(tags.contains(ChannelStateTestsTags.AliceLowMaxHtlcValueInFlight))(UInt64(150000000)) .modify(_.dustLimit).setToIf(tags.contains(ChannelStateTestsTags.HighDustLimitDifferenceAliceBob))(5000 sat) .modify(_.dustLimit).setToIf(tags.contains(ChannelStateTestsTags.HighDustLimitDifferenceBobAlice))(1000 sat) val bobParams = Bob.channelParams .modify(_.initFeatures).setTo(bobInitFeatures) - .modify(_.walletStaticPaymentBasepoint).setToIf(channelType.paysDirectlyToWallet)(Some(Helpers.getWalletPaymentBasepoint(wallet))) + .modify(_.walletStaticPaymentBasepoint).setToIf(channelType.paysDirectlyToWallet)(Some(Await.result(wallet.getReceivePubkey(), 10 seconds))) .modify(_.maxHtlcValueInFlightMsat).setToIf(tags.contains(ChannelStateTestsTags.NoMaxHtlcValueInFlight))(UInt64.MaxValue) .modify(_.dustLimit).setToIf(tags.contains(ChannelStateTestsTags.HighDustLimitDifferenceAliceBob))(1000 sat) .modify(_.dustLimit).setToIf(tags.contains(ChannelStateTestsTags.HighDustLimitDifferenceBobAlice))(5000 sat) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala index 77d0623c0e..de1b274936 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala @@ -464,7 +464,7 @@ class PeerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Paralle val probe = TestProbe() val channelFactory = new ChannelFactory { override def spawn(context: ActorContext, remoteNodeId: PublicKey, origin_opt: Option[ActorRef]): ActorRef = { - assert(origin_opt == Some(probe.ref)) + assert(origin_opt.contains(probe.ref)) channel.ref } } From bfba9e41193da5fbe76a7685485e131293f32789 Mon Sep 17 00:00:00 2001 From: Pierre-Marie Padiou Date: Thu, 16 Jun 2022 10:38:58 +0200 Subject: [PATCH 094/121] (Minor) Include the discriminator in all channel codecs (#2317) So we don't need to rename them when a new codec is added. --- .../channel/version0/ChannelCodecs0.scala | 40 ++++++++--------- .../channel/version1/ChannelCodecs1.scala | 28 ++++++------ .../channel/version2/ChannelCodecs2.scala | 28 ++++++------ .../channel/version3/ChannelCodecs3.scala | 44 +++++++++---------- .../channel/version3/ChannelCodecs3Spec.scala | 8 ++-- 5 files changed, 74 insertions(+), 74 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelCodecs0.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelCodecs0.scala index b2c491c0fd..c67fe3573d 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelCodecs0.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelCodecs0.scala @@ -355,21 +355,21 @@ private[channel] object ChannelCodecs0 { ("tlvStream" | provide(TlvStream.empty[ChannelReadyTlv]))).as[ChannelReady] // this is a decode-only codec compatible with versions 997acee and below, with placeholders for new fields - val DATA_WAIT_FOR_FUNDING_CONFIRMED_COMPAT_01_Codec: Codec[DATA_WAIT_FOR_FUNDING_CONFIRMED] = ( + val DATA_WAIT_FOR_FUNDING_CONFIRMED_01_Codec: Codec[DATA_WAIT_FOR_FUNDING_CONFIRMED] = ( ("commitments" | commitmentsCodec) :: ("fundingTx" | provide[Option[Transaction]](None)) :: ("waitingSince" | provide(BlockHeight(TimestampSecond.now().toLong))) :: ("deferred" | optional(bool, channelReadyCodec)) :: ("lastSent" | either(bool, fundingCreatedCodec, fundingSignedCodec))).as[DATA_WAIT_FOR_FUNDING_CONFIRMED].decodeOnly - val DATA_WAIT_FOR_FUNDING_CONFIRMED_Codec: Codec[DATA_WAIT_FOR_FUNDING_CONFIRMED] = ( + val DATA_WAIT_FOR_FUNDING_CONFIRMED_08_Codec: Codec[DATA_WAIT_FOR_FUNDING_CONFIRMED] = ( ("commitments" | commitmentsCodec) :: ("fundingTx" | optional(bool, txCodec)) :: ("waitingSince" | int64.as[BlockHeight]) :: ("deferred" | optional(bool, channelReadyCodec)) :: ("lastSent" | either(bool, fundingCreatedCodec, fundingSignedCodec))).as[DATA_WAIT_FOR_FUNDING_CONFIRMED].decodeOnly - val DATA_WAIT_FOR_CHANNEL_READY_Codec: Codec[DATA_WAIT_FOR_CHANNEL_READY] = ( + val DATA_WAIT_FOR_CHANNEL_READY_02_Codec: Codec[DATA_WAIT_FOR_CHANNEL_READY] = ( ("commitments" | commitmentsCodec) :: ("shortChannelId" | realshortchannelid) :: ("lastSent" | channelReadyCodec)).map { @@ -383,7 +383,7 @@ private[channel] object ChannelCodecs0 { ("tlvStream" | provide(TlvStream.empty[ShutdownTlv]))).as[Shutdown] // this is a decode-only codec compatible with versions 9afb26e and below - val DATA_NORMAL_COMPAT_03_Codec: Codec[DATA_NORMAL] = ( + val DATA_NORMAL_03_Codec: Codec[DATA_NORMAL] = ( ("commitments" | commitmentsCodec) :: ("shortChannelId" | realshortchannelid) :: ("buried" | bool) :: @@ -396,7 +396,7 @@ private[channel] object ChannelCodecs0 { DATA_NORMAL(commitments, shortIds = ShortIds(real = if (buried) RealScidStatus.Final(shortChannelId) else RealScidStatus.Temporary(shortChannelId), localAlias = Alias(shortChannelId.toLong), remoteAlias_opt = None), channelAnnouncement, channelUpdate, localShutdown, remoteShutdown, closingFeerates) }.decodeOnly - val DATA_NORMAL_Codec: Codec[DATA_NORMAL] = ( + val DATA_NORMAL_10_Codec: Codec[DATA_NORMAL] = ( ("commitments" | commitmentsCodec) :: ("shortChannelId" | realshortchannelid) :: ("buried" | bool) :: @@ -409,13 +409,13 @@ private[channel] object ChannelCodecs0 { DATA_NORMAL(commitments, shortIds = ShortIds(real = if (buried) RealScidStatus.Final(shortChannelId) else RealScidStatus.Temporary(shortChannelId), localAlias = Alias(shortChannelId.toLong), remoteAlias_opt = None), channelAnnouncement, channelUpdate, localShutdown, remoteShutdown, closingFeerates) }.decodeOnly - val DATA_SHUTDOWN_Codec: Codec[DATA_SHUTDOWN] = ( + val DATA_SHUTDOWN_04_Codec: Codec[DATA_SHUTDOWN] = ( ("commitments" | commitmentsCodec) :: ("localShutdown" | shutdownCodec) :: ("remoteShutdown" | shutdownCodec) :: ("closingFeerates" | provide(Option.empty[ClosingFeerates]))).as[DATA_SHUTDOWN].decodeOnly - val DATA_NEGOTIATING_Codec: Codec[DATA_NEGOTIATING] = ( + val DATA_NEGOTIATING_05_Codec: Codec[DATA_NEGOTIATING] = ( ("commitments" | commitmentsCodec) :: ("localShutdown" | shutdownCodec) :: ("remoteShutdown" | shutdownCodec) :: @@ -423,7 +423,7 @@ private[channel] object ChannelCodecs0 { ("bestUnpublishedClosingTx_opt" | optional(bool, closingTxCodec))).as[DATA_NEGOTIATING].decodeOnly // this is a decode-only codec compatible with versions 818199e and below, with placeholders for new fields - val DATA_CLOSING_COMPAT_06_Codec: Codec[DATA_CLOSING] = ( + val DATA_CLOSING_06_Codec: Codec[DATA_CLOSING] = ( ("commitments" | commitmentsCodec) :: ("fundingTx" | provide[Option[Transaction]](None)) :: ("waitingSince" | provide(BlockHeight(TimestampSecond.now().toLong))) :: @@ -435,7 +435,7 @@ private[channel] object ChannelCodecs0 { ("futureRemoteCommitPublished" | optional(bool, remoteCommitPublishedCodec)) :: ("revokedCommitPublished" | listOfN(uint16, revokedCommitPublishedCodec))).as[DATA_CLOSING].decodeOnly - val DATA_CLOSING_Codec: Codec[DATA_CLOSING] = ( + val DATA_CLOSING_09_Codec: Codec[DATA_CLOSING] = ( ("commitments" | commitmentsCodec) :: ("fundingTx" | optional(bool, txCodec)) :: ("waitingSince" | int64.as[BlockHeight]) :: @@ -455,22 +455,22 @@ private[channel] object ChannelCodecs0 { ("myCurrentPerCommitmentPoint" | publicKey) :: ("tlvStream" | provide(TlvStream.empty[ChannelReestablishTlv]))).as[ChannelReestablish] - val DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT_Codec: Codec[DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT] = ( + val DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT_07_Codec: Codec[DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT] = ( ("commitments" | commitmentsCodec) :: ("remoteChannelReestablish" | channelReestablishCodec)).as[DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT].decodeOnly } // Order matters! val channelDataCodec: Codec[PersistentChannelData] = discriminated[PersistentChannelData].by(uint16) - .typecase(0x10, Codecs.DATA_NORMAL_Codec) - .typecase(0x09, Codecs.DATA_CLOSING_Codec) - .typecase(0x08, Codecs.DATA_WAIT_FOR_FUNDING_CONFIRMED_Codec) - .typecase(0x01, Codecs.DATA_WAIT_FOR_FUNDING_CONFIRMED_COMPAT_01_Codec) - .typecase(0x02, Codecs.DATA_WAIT_FOR_CHANNEL_READY_Codec) - .typecase(0x03, Codecs.DATA_NORMAL_COMPAT_03_Codec) - .typecase(0x04, Codecs.DATA_SHUTDOWN_Codec) - .typecase(0x05, Codecs.DATA_NEGOTIATING_Codec) - .typecase(0x06, Codecs.DATA_CLOSING_COMPAT_06_Codec) - .typecase(0x07, Codecs.DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT_Codec) + .typecase(0x10, Codecs.DATA_NORMAL_10_Codec) + .typecase(0x09, Codecs.DATA_CLOSING_09_Codec) + .typecase(0x08, Codecs.DATA_WAIT_FOR_FUNDING_CONFIRMED_08_Codec) + .typecase(0x01, Codecs.DATA_WAIT_FOR_FUNDING_CONFIRMED_01_Codec) + .typecase(0x02, Codecs.DATA_WAIT_FOR_CHANNEL_READY_02_Codec) + .typecase(0x03, Codecs.DATA_NORMAL_03_Codec) + .typecase(0x04, Codecs.DATA_SHUTDOWN_04_Codec) + .typecase(0x05, Codecs.DATA_NEGOTIATING_05_Codec) + .typecase(0x06, Codecs.DATA_CLOSING_06_Codec) + .typecase(0x07, Codecs.DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT_07_Codec) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version1/ChannelCodecs1.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version1/ChannelCodecs1.scala index 384947d98f..97be45a049 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version1/ChannelCodecs1.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version1/ChannelCodecs1.scala @@ -232,14 +232,14 @@ private[channel] object ChannelCodecs1 { ("claimHtlcDelayedPenaltyTxs" | listOfN(uint16, txCodec)) :: ("spent" | spentMapCodec)).as[ChannelTypes0.RevokedCommitPublished].decodeOnly.map[RevokedCommitPublished](_.migrate()).decodeOnly - val DATA_WAIT_FOR_FUNDING_CONFIRMED_Codec: Codec[DATA_WAIT_FOR_FUNDING_CONFIRMED] = ( + val DATA_WAIT_FOR_FUNDING_CONFIRMED_20_Codec: Codec[DATA_WAIT_FOR_FUNDING_CONFIRMED] = ( ("commitments" | commitmentsCodec) :: ("fundingTx" | optional(bool8, txCodec)) :: ("waitingSince" | int64.as[BlockHeight]) :: ("deferred" | optional(bool8, lengthDelimited(channelReadyCodec))) :: ("lastSent" | either(bool8, lengthDelimited(fundingCreatedCodec), lengthDelimited(fundingSignedCodec)))).as[DATA_WAIT_FOR_FUNDING_CONFIRMED] - val DATA_WAIT_FOR_CHANNEL_READY_Codec: Codec[DATA_WAIT_FOR_CHANNEL_READY] = ( + val DATA_WAIT_FOR_CHANNEL_READY_21_Codec: Codec[DATA_WAIT_FOR_CHANNEL_READY] = ( ("commitments" | commitmentsCodec) :: ("shortChannelId" | realshortchannelid) :: ("lastSent" | lengthDelimited(channelReadyCodec))).map { @@ -247,7 +247,7 @@ private[channel] object ChannelCodecs1 { DATA_WAIT_FOR_CHANNEL_READY(commitments, shortIds = ShortIds(real = RealScidStatus.Temporary(shortChannelId), localAlias = Alias(shortChannelId.toLong), remoteAlias_opt = None), lastSent = lastSent) }.decodeOnly - val DATA_NORMAL_Codec: Codec[DATA_NORMAL] = ( + val DATA_NORMAL_22_Codec: Codec[DATA_NORMAL] = ( ("commitments" | commitmentsCodec) :: ("shortChannelId" | realshortchannelid) :: ("buried" | bool8) :: @@ -260,20 +260,20 @@ private[channel] object ChannelCodecs1 { DATA_NORMAL(commitments, shortIds = ShortIds(real = if (buried) RealScidStatus.Final(shortChannelId) else RealScidStatus.Temporary(shortChannelId), localAlias = Alias(shortChannelId.toLong), remoteAlias_opt = None), channelAnnouncement, channelUpdate, localShutdown, remoteShutdown, closingFeerates) }.decodeOnly - val DATA_SHUTDOWN_Codec: Codec[DATA_SHUTDOWN] = ( + val DATA_SHUTDOWN_23_Codec: Codec[DATA_SHUTDOWN] = ( ("commitments" | commitmentsCodec) :: ("localShutdown" | lengthDelimited(shutdownCodec)) :: ("remoteShutdown" | lengthDelimited(shutdownCodec)) :: ("closingFeerates" | provide(Option.empty[ClosingFeerates]))).as[DATA_SHUTDOWN] - val DATA_NEGOTIATING_Codec: Codec[DATA_NEGOTIATING] = ( + val DATA_NEGOTIATING_24_Codec: Codec[DATA_NEGOTIATING] = ( ("commitments" | commitmentsCodec) :: ("localShutdown" | lengthDelimited(shutdownCodec)) :: ("remoteShutdown" | lengthDelimited(shutdownCodec)) :: ("closingTxProposed" | listOfN(uint16, listOfN(uint16, lengthDelimited(closingTxProposedCodec)))) :: ("bestUnpublishedClosingTx_opt" | optional(bool8, closingTxCodec))).as[DATA_NEGOTIATING] - val DATA_CLOSING_Codec: Codec[DATA_CLOSING] = ( + val DATA_CLOSING_25_Codec: Codec[DATA_CLOSING] = ( ("commitments" | commitmentsCodec) :: ("fundingTx" | optional(bool8, txCodec)) :: ("waitingSince" | int64.as[BlockHeight]) :: @@ -285,18 +285,18 @@ private[channel] object ChannelCodecs1 { ("futureRemoteCommitPublished" | optional(bool8, remoteCommitPublishedCodec)) :: ("revokedCommitPublished" | listOfN(uint16, revokedCommitPublishedCodec))).as[DATA_CLOSING] - val DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT_Codec: Codec[DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT] = ( + val DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT_26_Codec: Codec[DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT] = ( ("commitments" | commitmentsCodec) :: ("remoteChannelReestablish" | channelReestablishCodec)).as[DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT] } // Order matters! val channelDataCodec: Codec[PersistentChannelData] = discriminated[PersistentChannelData].by(uint16) - .typecase(0x20, Codecs.DATA_WAIT_FOR_FUNDING_CONFIRMED_Codec) - .typecase(0x21, Codecs.DATA_WAIT_FOR_CHANNEL_READY_Codec) - .typecase(0x22, Codecs.DATA_NORMAL_Codec) - .typecase(0x23, Codecs.DATA_SHUTDOWN_Codec) - .typecase(0x24, Codecs.DATA_NEGOTIATING_Codec) - .typecase(0x25, Codecs.DATA_CLOSING_Codec) - .typecase(0x26, Codecs.DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT_Codec) + .typecase(0x20, Codecs.DATA_WAIT_FOR_FUNDING_CONFIRMED_20_Codec) + .typecase(0x21, Codecs.DATA_WAIT_FOR_CHANNEL_READY_21_Codec) + .typecase(0x22, Codecs.DATA_NORMAL_22_Codec) + .typecase(0x23, Codecs.DATA_SHUTDOWN_23_Codec) + .typecase(0x24, Codecs.DATA_NEGOTIATING_24_Codec) + .typecase(0x25, Codecs.DATA_CLOSING_25_Codec) + .typecase(0x26, Codecs.DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT_26_Codec) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version2/ChannelCodecs2.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version2/ChannelCodecs2.scala index 0e1c29770c..fb132d33ba 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version2/ChannelCodecs2.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version2/ChannelCodecs2.scala @@ -267,14 +267,14 @@ private[channel] object ChannelCodecs2 { ("claimHtlcDelayedPenaltyTxs" | listOfN(uint16, claimHtlcDelayedOutputPenaltyTxCodec)) :: ("spent" | spentMapCodec)).as[RevokedCommitPublished] - val DATA_WAIT_FOR_FUNDING_CONFIRMED_Codec: Codec[DATA_WAIT_FOR_FUNDING_CONFIRMED] = ( + val DATA_WAIT_FOR_FUNDING_CONFIRMED_00_Codec: Codec[DATA_WAIT_FOR_FUNDING_CONFIRMED] = ( ("commitments" | commitmentsCodec) :: ("fundingTx" | optional(bool8, txCodec)) :: ("waitingSince" | int64.as[BlockHeight]) :: ("deferred" | optional(bool8, lengthDelimited(channelReadyCodec))) :: ("lastSent" | either(bool8, lengthDelimited(fundingCreatedCodec), lengthDelimited(fundingSignedCodec)))).as[DATA_WAIT_FOR_FUNDING_CONFIRMED] - val DATA_WAIT_FOR_CHANNEL_READY_Codec: Codec[DATA_WAIT_FOR_CHANNEL_READY] = ( + val DATA_WAIT_FOR_CHANNEL_READY_01_Codec: Codec[DATA_WAIT_FOR_CHANNEL_READY] = ( ("commitments" | commitmentsCodec) :: ("shortChannelId" | realshortchannelid) :: ("lastSent" | lengthDelimited(channelReadyCodec))).map { @@ -282,7 +282,7 @@ private[channel] object ChannelCodecs2 { DATA_WAIT_FOR_CHANNEL_READY(commitments, shortIds = ShortIds(real = RealScidStatus.Temporary(shortChannelId), localAlias = Alias(shortChannelId.toLong), remoteAlias_opt = None), lastSent = lastSent) }.decodeOnly - val DATA_NORMAL_Codec: Codec[DATA_NORMAL] = ( + val DATA_NORMAL_02_Codec: Codec[DATA_NORMAL] = ( ("commitments" | commitmentsCodec) :: ("shortChannelId" | realshortchannelid) :: ("buried" | bool8) :: @@ -295,20 +295,20 @@ private[channel] object ChannelCodecs2 { DATA_NORMAL(commitments, shortIds = ShortIds(real = if (buried) RealScidStatus.Final(shortChannelId) else RealScidStatus.Temporary(shortChannelId), localAlias = Alias(shortChannelId.toLong), remoteAlias_opt = None), channelAnnouncement, channelUpdate, localShutdown, remoteShutdown, closingFeerates) }.decodeOnly - val DATA_SHUTDOWN_Codec: Codec[DATA_SHUTDOWN] = ( + val DATA_SHUTDOWN_03_Codec: Codec[DATA_SHUTDOWN] = ( ("commitments" | commitmentsCodec) :: ("localShutdown" | lengthDelimited(shutdownCodec)) :: ("remoteShutdown" | lengthDelimited(shutdownCodec)) :: ("closingFeerates" | provide(Option.empty[ClosingFeerates]))).as[DATA_SHUTDOWN] - val DATA_NEGOTIATING_Codec: Codec[DATA_NEGOTIATING] = ( + val DATA_NEGOTIATING_04_Codec: Codec[DATA_NEGOTIATING] = ( ("commitments" | commitmentsCodec) :: ("localShutdown" | lengthDelimited(shutdownCodec)) :: ("remoteShutdown" | lengthDelimited(shutdownCodec)) :: ("closingTxProposed" | listOfN(uint16, listOfN(uint16, lengthDelimited(closingTxProposedCodec)))) :: ("bestUnpublishedClosingTx_opt" | optional(bool8, closingTxCodec))).as[DATA_NEGOTIATING] - val DATA_CLOSING_Codec: Codec[DATA_CLOSING] = ( + val DATA_CLOSING_05_Codec: Codec[DATA_CLOSING] = ( ("commitments" | commitmentsCodec) :: ("fundingTx" | optional(bool8, txCodec)) :: ("waitingSince" | int64.as[BlockHeight]) :: @@ -320,18 +320,18 @@ private[channel] object ChannelCodecs2 { ("futureRemoteCommitPublished" | optional(bool8, remoteCommitPublishedCodec)) :: ("revokedCommitPublished" | listOfN(uint16, revokedCommitPublishedCodec))).as[DATA_CLOSING] - val DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT_Codec: Codec[DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT] = ( + val DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT_06_Codec: Codec[DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT] = ( ("commitments" | commitmentsCodec) :: ("remoteChannelReestablish" | channelReestablishCodec)).as[DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT] } val channelDataCodec: Codec[PersistentChannelData] = discriminated[PersistentChannelData].by(uint16) - .typecase(0x00, Codecs.DATA_WAIT_FOR_FUNDING_CONFIRMED_Codec) - .typecase(0x01, Codecs.DATA_WAIT_FOR_CHANNEL_READY_Codec) - .typecase(0x02, Codecs.DATA_NORMAL_Codec) - .typecase(0x03, Codecs.DATA_SHUTDOWN_Codec) - .typecase(0x04, Codecs.DATA_NEGOTIATING_Codec) - .typecase(0x05, Codecs.DATA_CLOSING_Codec) - .typecase(0x06, Codecs.DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT_Codec) + .typecase(0x00, Codecs.DATA_WAIT_FOR_FUNDING_CONFIRMED_00_Codec) + .typecase(0x01, Codecs.DATA_WAIT_FOR_CHANNEL_READY_01_Codec) + .typecase(0x02, Codecs.DATA_NORMAL_02_Codec) + .typecase(0x03, Codecs.DATA_SHUTDOWN_03_Codec) + .typecase(0x04, Codecs.DATA_NEGOTIATING_04_Codec) + .typecase(0x05, Codecs.DATA_CLOSING_05_Codec) + .typecase(0x06, Codecs.DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT_06_Codec) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3.scala index 8dfb7410b8..822e224116 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3.scala @@ -315,7 +315,7 @@ private[channel] object ChannelCodecs3 { ("claimHtlcDelayedPenaltyTxs" | listOfN(uint16, claimHtlcDelayedOutputPenaltyTxCodec)) :: ("spent" | spentMapCodec)).as[RevokedCommitPublished] - val DATA_WAIT_FOR_FUNDING_CONFIRMED_Codec: Codec[DATA_WAIT_FOR_FUNDING_CONFIRMED] = ( + val DATA_WAIT_FOR_FUNDING_CONFIRMED_00_Codec: Codec[DATA_WAIT_FOR_FUNDING_CONFIRMED] = ( ("commitments" | commitmentsCodec) :: ("fundingTx" | optional(bool8, txCodec)) :: // TODO: next time we define a new channel codec version, we should use the blockHeight codec here (32 bytes) @@ -323,7 +323,7 @@ private[channel] object ChannelCodecs3 { ("deferred" | optional(bool8, lengthDelimited(channelReadyCodec))) :: ("lastSent" | either(bool8, lengthDelimited(fundingCreatedCodec), lengthDelimited(fundingSignedCodec)))).as[DATA_WAIT_FOR_FUNDING_CONFIRMED] - val DATA_WAIT_FOR_CHANNEL_READY_COMPAT_01_Codec: Codec[DATA_WAIT_FOR_CHANNEL_READY] = ( + val DATA_WAIT_FOR_CHANNEL_READY_01_Codec: Codec[DATA_WAIT_FOR_CHANNEL_READY] = ( ("commitments" | commitmentsCodec) :: ("shortChannelId" | realshortchannelid) :: ("lastSent" | lengthDelimited(channelReadyCodec))).map { @@ -331,12 +331,12 @@ private[channel] object ChannelCodecs3 { DATA_WAIT_FOR_CHANNEL_READY(commitments, shortIds = ShortIds(real = RealScidStatus.Temporary(shortChannelId), localAlias = Alias(shortChannelId.toLong), remoteAlias_opt = None), lastSent = lastSent) }.decodeOnly - val DATA_WAIT_FOR_CHANNEL_READY_Codec: Codec[DATA_WAIT_FOR_CHANNEL_READY] = ( + val DATA_WAIT_FOR_CHANNEL_READY_0a_Codec: Codec[DATA_WAIT_FOR_CHANNEL_READY] = ( ("commitments" | commitmentsCodec) :: ("shortIds" | shortids) :: ("lastSent" | lengthDelimited(channelReadyCodec))).as[DATA_WAIT_FOR_CHANNEL_READY] - val DATA_NORMAL_COMPAT_02_Codec: Codec[DATA_NORMAL] = ( + val DATA_NORMAL_02_Codec: Codec[DATA_NORMAL] = ( ("commitments" | commitmentsCodec) :: ("shortChannelId" | realshortchannelid) :: ("buried" | bool8) :: @@ -349,7 +349,7 @@ private[channel] object ChannelCodecs3 { DATA_NORMAL(commitments, shortIds = ShortIds(real = if (buried) RealScidStatus.Final(shortChannelId) else RealScidStatus.Temporary(shortChannelId), localAlias = Alias(shortChannelId.toLong), remoteAlias_opt = None), channelAnnouncement, channelUpdate, localShutdown, remoteShutdown, closingFeerates) }.decodeOnly - val DATA_NORMAL_COMPAT_07_Codec: Codec[DATA_NORMAL] = ( + val DATA_NORMAL_07_Codec: Codec[DATA_NORMAL] = ( ("commitments" | commitmentsCodec) :: ("shortChannelId" | realshortchannelid) :: ("buried" | bool8) :: @@ -362,7 +362,7 @@ private[channel] object ChannelCodecs3 { DATA_NORMAL(commitments, shortIds = ShortIds(real = if (buried) RealScidStatus.Final(shortChannelId) else RealScidStatus.Temporary(shortChannelId), localAlias = Alias(shortChannelId.toLong), remoteAlias_opt = None), channelAnnouncement, channelUpdate, localShutdown, remoteShutdown, closingFeerates) }.decodeOnly - val DATA_NORMAL_Codec: Codec[DATA_NORMAL] = ( + val DATA_NORMAL_09_Codec: Codec[DATA_NORMAL] = ( ("commitments" | commitmentsCodec) :: ("shortids" | shortids) :: ("channelAnnouncement" | optional(bool8, lengthDelimited(channelAnnouncementCodec))) :: @@ -371,26 +371,26 @@ private[channel] object ChannelCodecs3 { ("remoteShutdown" | optional(bool8, lengthDelimited(shutdownCodec))) :: ("closingFeerates" | optional(bool8, closingFeeratesCodec))).as[DATA_NORMAL] - val DATA_SHUTDOWN_COMPAT_03_Codec: Codec[DATA_SHUTDOWN] = ( + val DATA_SHUTDOWN_03_Codec: Codec[DATA_SHUTDOWN] = ( ("commitments" | commitmentsCodec) :: ("localShutdown" | lengthDelimited(shutdownCodec)) :: ("remoteShutdown" | lengthDelimited(shutdownCodec)) :: ("closingFeerates" | provide(Option.empty[ClosingFeerates]))).as[DATA_SHUTDOWN] - val DATA_SHUTDOWN_Codec: Codec[DATA_SHUTDOWN] = ( + val DATA_SHUTDOWN_08_Codec: Codec[DATA_SHUTDOWN] = ( ("commitments" | commitmentsCodec) :: ("localShutdown" | lengthDelimited(shutdownCodec)) :: ("remoteShutdown" | lengthDelimited(shutdownCodec)) :: ("closingFeerates" | optional(bool8, closingFeeratesCodec))).as[DATA_SHUTDOWN] - val DATA_NEGOTIATING_Codec: Codec[DATA_NEGOTIATING] = ( + val DATA_NEGOTIATING_04_Codec: Codec[DATA_NEGOTIATING] = ( ("commitments" | commitmentsCodec) :: ("localShutdown" | lengthDelimited(shutdownCodec)) :: ("remoteShutdown" | lengthDelimited(shutdownCodec)) :: ("closingTxProposed" | listOfN(uint16, listOfN(uint16, lengthDelimited(closingTxProposedCodec)))) :: ("bestUnpublishedClosingTx_opt" | optional(bool8, closingTxCodec))).as[DATA_NEGOTIATING] - val DATA_CLOSING_Codec: Codec[DATA_CLOSING] = ( + val DATA_CLOSING_05_Codec: Codec[DATA_CLOSING] = ( ("commitments" | commitmentsCodec) :: ("fundingTx" | optional(bool8, txCodec)) :: // TODO: next time we define a new channel codec version, we should use the blockHeight codec here (32 bytes) @@ -403,23 +403,23 @@ private[channel] object ChannelCodecs3 { ("futureRemoteCommitPublished" | optional(bool8, remoteCommitPublishedCodec)) :: ("revokedCommitPublished" | listOfN(uint16, revokedCommitPublishedCodec))).as[DATA_CLOSING] - val DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT_Codec: Codec[DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT] = ( + val DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT_06_Codec: Codec[DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT] = ( ("commitments" | commitmentsCodec) :: ("remoteChannelReestablish" | channelReestablishCodec)).as[DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT] } // Order matters! val channelDataCodec: Codec[PersistentChannelData] = discriminated[PersistentChannelData].by(uint16) - .typecase(0x0a, Codecs.DATA_WAIT_FOR_CHANNEL_READY_Codec) - .typecase(0x09, Codecs.DATA_NORMAL_Codec) - .typecase(0x08, Codecs.DATA_SHUTDOWN_Codec) - .typecase(0x07, Codecs.DATA_NORMAL_COMPAT_07_Codec) - .typecase(0x06, Codecs.DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT_Codec) - .typecase(0x05, Codecs.DATA_CLOSING_Codec) - .typecase(0x04, Codecs.DATA_NEGOTIATING_Codec) - .typecase(0x03, Codecs.DATA_SHUTDOWN_COMPAT_03_Codec) - .typecase(0x02, Codecs.DATA_NORMAL_COMPAT_02_Codec) - .typecase(0x01, Codecs.DATA_WAIT_FOR_CHANNEL_READY_COMPAT_01_Codec) - .typecase(0x00, Codecs.DATA_WAIT_FOR_FUNDING_CONFIRMED_Codec) + .typecase(0x0a, Codecs.DATA_WAIT_FOR_CHANNEL_READY_0a_Codec) + .typecase(0x09, Codecs.DATA_NORMAL_09_Codec) + .typecase(0x08, Codecs.DATA_SHUTDOWN_08_Codec) + .typecase(0x07, Codecs.DATA_NORMAL_07_Codec) + .typecase(0x06, Codecs.DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT_06_Codec) + .typecase(0x05, Codecs.DATA_CLOSING_05_Codec) + .typecase(0x04, Codecs.DATA_NEGOTIATING_04_Codec) + .typecase(0x03, Codecs.DATA_SHUTDOWN_03_Codec) + .typecase(0x02, Codecs.DATA_NORMAL_02_Codec) + .typecase(0x01, Codecs.DATA_WAIT_FOR_CHANNEL_READY_01_Codec) + .typecase(0x00, Codecs.DATA_WAIT_FOR_FUNDING_CONFIRMED_00_Codec) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3Spec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3Spec.scala index ff76462c35..7636977a31 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3Spec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3Spec.scala @@ -33,8 +33,8 @@ class ChannelCodecs3Spec extends AnyFunSuite { test("basic serialization test (NORMAL)") { val data = normal - val bin = DATA_NORMAL_Codec.encode(data).require - val check = DATA_NORMAL_Codec.decodeValue(bin).require + val bin = DATA_NORMAL_09_Codec.encode(data).require + val check = DATA_NORMAL_09_Codec.decodeValue(bin).require assert(data.commitments.localCommit.spec == check.commitments.localCommit.spec) assert(data == check) } @@ -100,10 +100,10 @@ class ChannelCodecs3Spec extends AnyFunSuite { assert(codec.decodeValue(codec.encode(remoteParams1).require).require == remoteParams1) val dataWithoutRemoteShutdownScript = normal.copy(commitments = normal.commitments.copy(remoteParams = remoteParams)) - assert(DATA_NORMAL_Codec.decode(DATA_NORMAL_Codec.encode(dataWithoutRemoteShutdownScript).require).require.value == dataWithoutRemoteShutdownScript) + assert(DATA_NORMAL_09_Codec.decode(DATA_NORMAL_09_Codec.encode(dataWithoutRemoteShutdownScript).require).require.value == dataWithoutRemoteShutdownScript) val dataWithRemoteShutdownScript = normal.copy(commitments = normal.commitments.copy(remoteParams = remoteParams1)) - assert(DATA_NORMAL_Codec.decode(DATA_NORMAL_Codec.encode(dataWithRemoteShutdownScript).require).require.value == dataWithRemoteShutdownScript) + assert(DATA_NORMAL_09_Codec.decode(DATA_NORMAL_09_Codec.encode(dataWithRemoteShutdownScript).require).require.value == dataWithRemoteShutdownScript) } test("encode/decode optional channel reserve") { From 7630c5169c4f0c4c83eb48956ebf90b0769b3ee8 Mon Sep 17 00:00:00 2001 From: Pierre-Marie Padiou Date: Thu, 16 Jun 2022 10:44:23 +0200 Subject: [PATCH 095/121] (Minor) Fix flaky `ZeroConfAliasIntegrationSpec` (#2319) This build failure: https://github.com/ACINQ/eclair/runs/6901214111 was possibly caused by the register not yet having been notified of the real scid. --- .../basic/ThreeNodesIntegrationSpec.scala | 2 +- .../basic/ZeroConfAliasIntegrationSpec.scala | 48 +++++++++---------- 2 files changed, 25 insertions(+), 25 deletions(-) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/ThreeNodesIntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/ThreeNodesIntegrationSpec.scala index a1c8306f80..72da991d06 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/ThreeNodesIntegrationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/ThreeNodesIntegrationSpec.scala @@ -46,7 +46,7 @@ class ThreeNodesIntegrationSpec extends FixtureSpec with IntegrationPatience { // alice now knows about bob-carol eventually { val routerData = getRouterData(alice) - prettyPrint(routerData, alice, bob, carol) + //prettyPrint(routerData, alice, bob, carol) assert(routerData.channels.size == 2) // 2 channels assert(routerData.channels.values.flatMap(c => c.update_1_opt.toSeq ++ c.update_2_opt.toSeq).size == 4) // 2 channel_updates per channel } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/ZeroConfAliasIntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/ZeroConfAliasIntegrationSpec.scala index 71c26c03b0..05af0ee034 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/ZeroConfAliasIntegrationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/ZeroConfAliasIntegrationSpec.scala @@ -98,44 +98,48 @@ class ZeroConfAliasIntegrationSpec extends FixtureSpec with IntegrationPatience } } - if (bcPublic && deepConfirm) { - // if channel bob-carol is public, we wait for alice to learn about it - eventually { + eventually { + if (bcPublic && deepConfirm) { + // if channel bob-carol is public, we wait for alice to learn about it val data = getRouterData(alice) assert(data.channels.size == 2) assert(data.channels.values.forall(pc => pc.update_1_opt.isDefined && pc.update_2_opt.isDefined)) } } - if (paymentWorksWithoutHint) { - sendPaymentAliceToCarol(f) - } else { - intercept[AssertionError] { + eventually { + if (paymentWorksWithoutHint) { sendPaymentAliceToCarol(f) + } else { + intercept[AssertionError] { + sendPaymentAliceToCarol(f) + } } } - paymentWorksWithHint_opt match { - case Some(true) => sendPaymentAliceToCarol(f, useHint = true) - case Some(false) => intercept[AssertionError] { - sendPaymentAliceToCarol(f, useHint = true) + eventually { + paymentWorksWithHint_opt match { + case Some(true) => sendPaymentAliceToCarol(f, useHint = true) + case Some(false) => intercept[AssertionError] { + sendPaymentAliceToCarol(f, useHint = true) + } + case None => // skipped } - case None => // skipped } - paymentWorksWithRealScidHint_opt match { - // if alice uses the real scid instead of the bob-carol alias, it still works - case Some(true) => sendPaymentAliceToCarol(f, useHint = true, overrideHintScid_opt = Some(getChannelData(bob, channelId_bc).asInstanceOf[DATA_NORMAL].shortIds.real.toOption.value)) - case Some(false) => intercept[AssertionError] { - sendPaymentAliceToCarol(f, useHint = true, overrideHintScid_opt = Some(getChannelData(bob, channelId_bc).asInstanceOf[DATA_NORMAL].shortIds.real.toOption.value)) + eventually { + paymentWorksWithRealScidHint_opt match { + // if alice uses the real scid instead of the bob-carol alias, it still works + case Some(true) => sendPaymentAliceToCarol(f, useHint = true, overrideHintScid_opt = Some(getChannelData(bob, channelId_bc).asInstanceOf[DATA_NORMAL].shortIds.real.toOption.value)) + case Some(false) => intercept[AssertionError] { + sendPaymentAliceToCarol(f, useHint = true, overrideHintScid_opt = Some(getChannelData(bob, channelId_bc).asInstanceOf[DATA_NORMAL].shortIds.real.toOption.value)) + } + case None => // skipped } - case None => // skipped } } test("a->b->c (b-c private)") { f => - import f._ - internalTest(f, deepConfirm = true, bcPublic = false, @@ -148,8 +152,6 @@ class ZeroConfAliasIntegrationSpec extends FixtureSpec with IntegrationPatience } test("a->b->c (b-c scid-alias private)", Tag(ScidAliasBobCarol)) { f => - import f._ - internalTest(f, deepConfirm = true, bcPublic = false, @@ -162,8 +164,6 @@ class ZeroConfAliasIntegrationSpec extends FixtureSpec with IntegrationPatience } test("a->b->c (b-c zero-conf unconfirmed private)", Tag(ZeroConfBobCarol)) { f => - import f._ - internalTest(f, deepConfirm = false, bcPublic = false, From 6882bc9b2f9e0c351e59238e958e2f8b573430ea Mon Sep 17 00:00:00 2001 From: Bastien Teinturier <31281497+t-bast@users.noreply.github.com> Date: Thu, 16 Jun 2022 13:04:21 +0200 Subject: [PATCH 096/121] Clean up scid parsing from coordinates string (#2320) We make it explicit that conversion from a string to an scid uses the tx coordinates and can fail. We also clean up compiler warnings in updated test files. --- .../fr/acinq/eclair/ShortChannelId.scala | 14 ++-- .../fr/acinq/eclair/EclairImplSpec.scala | 49 ++++++------- .../fr/acinq/eclair/ShortChannelIdSpec.scala | 20 +++--- .../eclair/payment/Bolt11InvoiceSpec.scala | 69 ++++++++++--------- .../eclair/router/RouteCalculationSpec.scala | 36 +++++----- .../protocol/ExtendedQueriesCodecsSpec.scala | 9 ++- .../api/serde/FormParamExtractors.scala | 6 +- .../fr/acinq/eclair/api/ApiServiceSpec.scala | 52 ++++++++------ 8 files changed, 134 insertions(+), 121 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/ShortChannelId.scala b/eclair-core/src/main/scala/fr/acinq/eclair/ShortChannelId.scala index 2265f220b7..8a4e18490b 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/ShortChannelId.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/ShortChannelId.scala @@ -18,6 +18,8 @@ package fr.acinq.eclair import fr.acinq.eclair.ShortChannelId.toShortId +import scala.util.{Failure, Try} + // @formatter:off sealed trait ShortChannelId extends Ordered[ShortChannelId] { def toLong: Long @@ -52,11 +54,6 @@ case class Alias(private val id: Long) extends ShortChannelId { // @formatter:on object ShortChannelId { - def apply(s: String): ShortChannelId = s.split("x").toList match { - case blockHeight :: txIndex :: outputIndex :: Nil => UnspecifiedShortChannelId(toShortId(blockHeight.toInt, txIndex.toInt, outputIndex.toInt)) - case _ => throw new IllegalArgumentException(s"Invalid short channel id: $s") - } - def apply(l: Long): ShortChannelId = UnspecifiedShortChannelId(l) def toShortId(blockHeight: Int, txIndex: Int, outputIndex: Int): Long = ((blockHeight & 0xFFFFFFL) << 40) | ((txIndex & 0xFFFFFFL) << 16) | (outputIndex & 0xFFFFL) @@ -73,6 +70,13 @@ object ShortChannelId { def outputIndex(shortChannelId: ShortChannelId): Int = (shortChannelId.toLong & 0xFFFF).toInt def coordinates(shortChannelId: ShortChannelId): TxCoordinates = TxCoordinates(blockHeight(shortChannelId), txIndex(shortChannelId), outputIndex(shortChannelId)) + + def fromCoordinates(s: String): Try[ShortChannelId] = s.split("x").toList match { + case blockHeight :: txIndex :: outputIndex :: Nil => Try { + UnspecifiedShortChannelId(toShortId(blockHeight.toInt, txIndex.toInt, outputIndex.toInt)) + } + case _ => Failure(new IllegalArgumentException(s"Invalid short channel id: $s")) + } } /** diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/EclairImplSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/EclairImplSpec.scala index ed46c2e0d8..ba6b7d58e7 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/EclairImplSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/EclairImplSpec.scala @@ -43,6 +43,7 @@ import fr.acinq.eclair.router.{Announcements, Router} import fr.acinq.eclair.wire.internal.channel.ChannelCodecsSpec import fr.acinq.eclair.wire.protocol.{ChannelUpdate, Color, NodeAnnouncement} import org.mockito.scalatest.IdiomaticMockito +import org.scalatest.TryValues.convertTryToSuccessOrFailure import org.scalatest.funsuite.FixtureAnyFunSuiteLike import org.scalatest.{Outcome, ParallelTestExecution} import scodec.bits._ @@ -98,13 +99,13 @@ class EclairImplSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with I // standard conversion eclair.open(nodeId, fundingAmount = 10000000L sat, pushAmount_opt = None, channelType_opt = None, fundingFeeratePerByte_opt = Some(FeeratePerByte(5 sat)), announceChannel_opt = None, openTimeout_opt = None) val open = switchboard.expectMsgType[OpenChannel] - assert(open.fundingTxFeeratePerKw_opt == Some(FeeratePerKw(1250 sat))) + assert(open.fundingTxFeeratePerKw_opt.contains(FeeratePerKw(1250 sat))) // check that minimum fee rate of 253 sat/bw is used eclair.open(nodeId, fundingAmount = 10000000L sat, pushAmount_opt = None, channelType_opt = Some(ChannelTypes.StaticRemoteKey), fundingFeeratePerByte_opt = Some(FeeratePerByte(1 sat)), announceChannel_opt = None, openTimeout_opt = None) val open1 = switchboard.expectMsgType[OpenChannel] - assert(open1.fundingTxFeeratePerKw_opt == Some(FeeratePerKw.MinimumFeeratePerKw)) - assert(open1.channelType_opt == Some(ChannelTypes.StaticRemoteKey)) + assert(open1.fundingTxFeeratePerKw_opt.contains(FeeratePerKw.MinimumFeeratePerKw)) + assert(open1.channelType_opt.contains(ChannelTypes.StaticRemoteKey)) } test("call send with passing correct arguments") { f => @@ -115,7 +116,7 @@ class EclairImplSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with I val invoice0 = Bolt11Invoice(Block.RegtestGenesisBlock.hash, Some(123 msat), ByteVector32.Zeroes, nodePrivKey, Left("description"), CltvExpiryDelta(18)) eclair.send(None, 123 msat, invoice0) val send = paymentInitiator.expectMsgType[SendPaymentToNode] - assert(send.externalId == None) + assert(send.externalId.isEmpty) assert(send.recipientNodeId == nodePrivKey.publicKey) assert(send.recipientAmount == 123.msat) assert(send.paymentHash == ByteVector32.Zeroes) @@ -124,11 +125,11 @@ class EclairImplSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with I // with assisted routes val externalId1 = "030bb6a5e0c6b203c7e2180fb78c7ba4bdce46126761d8201b91ddac089cdecc87" - val hints = List(List(ExtraHop(Bob.nodeParams.nodeId, ShortChannelId("569178x2331x1"), feeBase = 10 msat, feeProportionalMillionths = 1, cltvExpiryDelta = CltvExpiryDelta(12)))) + val hints = List(List(ExtraHop(Bob.nodeParams.nodeId, ShortChannelId.fromCoordinates("569178x2331x1").success.value, feeBase = 10 msat, feeProportionalMillionths = 1, cltvExpiryDelta = CltvExpiryDelta(12)))) val invoice1 = Bolt11Invoice(Block.RegtestGenesisBlock.hash, Some(123 msat), ByteVector32.Zeroes, nodePrivKey, Left("description"), CltvExpiryDelta(18), None, None, hints) eclair.send(Some(externalId1), 123 msat, invoice1) val send1 = paymentInitiator.expectMsgType[SendPaymentToNode] - assert(send1.externalId == Some(externalId1)) + assert(send1.externalId.contains(externalId1)) assert(send1.recipientNodeId == nodePrivKey.publicKey) assert(send1.recipientAmount == 123.msat) assert(send1.paymentHash == ByteVector32.Zeroes) @@ -140,7 +141,7 @@ class EclairImplSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with I val invoice2 = Bolt11Invoice("lntb", Some(123 msat), TimestampSecond.now(), nodePrivKey.publicKey, List(Bolt11Invoice.MinFinalCltvExpiry(96), Bolt11Invoice.PaymentHash(ByteVector32.Zeroes), Bolt11Invoice.Description("description")), ByteVector.empty) eclair.send(Some(externalId2), 123 msat, invoice2) val send2 = paymentInitiator.expectMsgType[SendPaymentToNode] - assert(send2.externalId == Some(externalId2)) + assert(send2.externalId.contains(externalId2)) assert(send2.recipientNodeId == nodePrivKey.publicKey) assert(send2.recipientAmount == 123.msat) assert(send2.paymentHash == ByteVector32.Zeroes) @@ -149,7 +150,7 @@ class EclairImplSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with I // with custom route fees parameters eclair.send(None, 123 msat, invoice0, maxFeeFlat_opt = Some(123 sat), maxFeePct_opt = Some(4.20)) val send3 = paymentInitiator.expectMsgType[SendPaymentToNode] - assert(send3.externalId == None) + assert(send3.externalId.isEmpty) assert(send3.recipientNodeId == nodePrivKey.publicKey) assert(send3.recipientAmount == 123.msat) assert(send3.paymentHash == ByteVector32.Zeroes) @@ -261,13 +262,13 @@ class EclairImplSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with I eclair.forceClose(Left(ByteVector32.Zeroes) :: Nil) register.expectMsg(Register.Forward(ActorRef.noSender, ByteVector32.Zeroes, CMD_FORCECLOSE(ActorRef.noSender))) - eclair.forceClose(Right(ShortChannelId("568749x2597x0")) :: Nil) - register.expectMsg(Register.ForwardShortId(ActorRef.noSender, ShortChannelId("568749x2597x0"), CMD_FORCECLOSE(ActorRef.noSender))) + eclair.forceClose(Right(ShortChannelId.fromCoordinates("568749x2597x0").success.value) :: Nil) + register.expectMsg(Register.ForwardShortId(ActorRef.noSender, ShortChannelId.fromCoordinates("568749x2597x0").success.value, CMD_FORCECLOSE(ActorRef.noSender))) - eclair.forceClose(Left(ByteVector32.Zeroes) :: Right(ShortChannelId("568749x2597x0")) :: Nil) + eclair.forceClose(Left(ByteVector32.Zeroes) :: Right(ShortChannelId.fromCoordinates("568749x2597x0").success.value) :: Nil) register.expectMsgAllOf( Register.Forward(ActorRef.noSender, ByteVector32.Zeroes, CMD_FORCECLOSE(ActorRef.noSender)), - Register.ForwardShortId(ActorRef.noSender, ShortChannelId("568749x2597x0"), CMD_FORCECLOSE(ActorRef.noSender)) + Register.ForwardShortId(ActorRef.noSender, ShortChannelId.fromCoordinates("568749x2597x0").success.value, CMD_FORCECLOSE(ActorRef.noSender)) ) eclair.close(Left(ByteVector32.Zeroes) :: Nil, None, None) @@ -277,17 +278,17 @@ class EclairImplSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with I eclair.close(Left(ByteVector32.Zeroes) :: Nil, None, Some(customClosingFees)) register.expectMsg(Register.Forward(ActorRef.noSender, ByteVector32.Zeroes, CMD_CLOSE(ActorRef.noSender, None, Some(customClosingFees)))) - eclair.close(Right(ShortChannelId("568749x2597x0")) :: Nil, None, None) - register.expectMsg(Register.ForwardShortId(ActorRef.noSender, ShortChannelId("568749x2597x0"), CMD_CLOSE(ActorRef.noSender, None, None))) + eclair.close(Right(ShortChannelId.fromCoordinates("568749x2597x0").success.value) :: Nil, None, None) + register.expectMsg(Register.ForwardShortId(ActorRef.noSender, ShortChannelId.fromCoordinates("568749x2597x0").success.value, CMD_CLOSE(ActorRef.noSender, None, None))) - eclair.close(Right(ShortChannelId("568749x2597x0")) :: Nil, Some(ByteVector.empty), Some(customClosingFees)) - register.expectMsg(Register.ForwardShortId(ActorRef.noSender, ShortChannelId("568749x2597x0"), CMD_CLOSE(ActorRef.noSender, Some(ByteVector.empty), Some(customClosingFees)))) + eclair.close(Right(ShortChannelId.fromCoordinates("568749x2597x0").success.value) :: Nil, Some(ByteVector.empty), Some(customClosingFees)) + register.expectMsg(Register.ForwardShortId(ActorRef.noSender, ShortChannelId.fromCoordinates("568749x2597x0").success.value, CMD_CLOSE(ActorRef.noSender, Some(ByteVector.empty), Some(customClosingFees)))) - eclair.close(Right(ShortChannelId("568749x2597x0")) :: Left(ByteVector32.One) :: Right(ShortChannelId("568749x2597x1")) :: Nil, None, None) + eclair.close(Right(ShortChannelId.fromCoordinates("568749x2597x0").success.value) :: Left(ByteVector32.One) :: Right(ShortChannelId.fromCoordinates("568749x2597x1").success.value) :: Nil, None, None) register.expectMsgAllOf( - Register.ForwardShortId(ActorRef.noSender, ShortChannelId("568749x2597x0"), CMD_CLOSE(ActorRef.noSender, None, None)), + Register.ForwardShortId(ActorRef.noSender, ShortChannelId.fromCoordinates("568749x2597x0").success.value, CMD_CLOSE(ActorRef.noSender, None, None)), Register.Forward(ActorRef.noSender, ByteVector32.One, CMD_CLOSE(ActorRef.noSender, None, None)), - Register.ForwardShortId(ActorRef.noSender, ShortChannelId("568749x2597x1"), CMD_CLOSE(ActorRef.noSender, None, None)) + Register.ForwardShortId(ActorRef.noSender, ShortChannelId.fromCoordinates("568749x2597x1").success.value, CMD_CLOSE(ActorRef.noSender, None, None)) ) } @@ -299,9 +300,9 @@ class EclairImplSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with I eclair.receive(Left("some desc"), Some(123 msat), Some(456), Some(fallBackAddressRaw), None) val receive = paymentHandler.expectMsgType[ReceivePayment] - assert(receive.amount_opt == Some(123 msat)) - assert(receive.expirySeconds_opt == Some(456)) - assert(receive.fallbackAddress_opt == Some(fallBackAddressRaw)) + assert(receive.amount_opt.contains(123 msat)) + assert(receive.expirySeconds_opt.contains(456)) + assert(receive.fallbackAddress_opt.contains(fallBackAddressRaw)) // try with wrong address format assertThrows[IllegalArgumentException](eclair.receive(Left("some desc"), Some(123 msat), Some(456), Some("wassa wassa"), None)) @@ -339,7 +340,7 @@ class EclairImplSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with I eclair.sendWithPreimage(None, nodeId, 12345 msat) val send = paymentInitiator.expectMsgType[SendSpontaneousPayment] - assert(send.externalId == None) + assert(send.externalId.isEmpty) assert(send.recipientNodeId == nodeId) assert(send.recipientAmount == 12345.msat) assert(send.paymentHash == Crypto.sha256(send.paymentPreimage)) @@ -356,7 +357,7 @@ class EclairImplSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with I eclair.sendWithPreimage(None, nodeId, 12345 msat, paymentPreimage = expectedPaymentPreimage) val send = paymentInitiator.expectMsgType[SendSpontaneousPayment] - assert(send.externalId == None) + assert(send.externalId.isEmpty) assert(send.recipientNodeId == nodeId) assert(send.recipientAmount == 12345.msat) assert(send.paymentPreimage == expectedPaymentPreimage) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/ShortChannelIdSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/ShortChannelIdSpec.scala index ae6853e51a..fc472f2054 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/ShortChannelIdSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/ShortChannelIdSpec.scala @@ -18,7 +18,7 @@ package fr.acinq.eclair import org.scalatest.funsuite.AnyFunSuite -import scala.util.Try +import scala.util.Success class ShortChannelIdSpec extends AnyFunSuite { @@ -43,17 +43,17 @@ class ShortChannelIdSpec extends AnyFunSuite { } test("parse a short channel id") { - assert(ShortChannelId("42000x27x3").toLong == 0x0000a41000001b0003L) + assert(ShortChannelId.fromCoordinates("42000x27x3").map(_.toLong) == Success(0x0000a41000001b0003L)) } test("fail parsing a short channel id if not in the required form") { - assert(Try(ShortChannelId("42000x27x3.1")).isFailure) - assert(Try(ShortChannelId("4200aa0x27x3")).isFailure) - assert(Try(ShortChannelId("4200027x3")).isFailure) - assert(Try(ShortChannelId("42000x27ax3")).isFailure) - assert(Try(ShortChannelId("42000x27x")).isFailure) - assert(Try(ShortChannelId("42000x27")).isFailure) - assert(Try(ShortChannelId("42000x")).isFailure) + assert(ShortChannelId.fromCoordinates("42000x27x3.1").isFailure) + assert(ShortChannelId.fromCoordinates("4200aa0x27x3").isFailure) + assert(ShortChannelId.fromCoordinates("4200027x3").isFailure) + assert(ShortChannelId.fromCoordinates("42000x27ax3").isFailure) + assert(ShortChannelId.fromCoordinates("42000x27x").isFailure) + assert(ShortChannelId.fromCoordinates("42000x27").isFailure) + assert(ShortChannelId.fromCoordinates("42000x").isFailure) } test("compare different types of short channel ids") { @@ -70,4 +70,4 @@ class ShortChannelIdSpec extends AnyFunSuite { Seq(-561L, 0xffffffffffffffffL, 0x2affffffffffffffL).foreach(id => assert(Alias(id) == UnspecifiedShortChannelId(id))) } -} \ No newline at end of file +} diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/Bolt11InvoiceSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/Bolt11InvoiceSpec.scala index 3c50b23fe9..657f0c25bf 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/Bolt11InvoiceSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/Bolt11InvoiceSpec.scala @@ -22,6 +22,7 @@ import fr.acinq.eclair.FeatureSupport.{Mandatory, Optional} import fr.acinq.eclair.Features.{PaymentMetadata, PaymentSecret, _} import fr.acinq.eclair.payment.Bolt11Invoice._ import fr.acinq.eclair.{CltvExpiryDelta, Feature, FeatureSupport, Features, MilliSatoshi, MilliSatoshiLong, ShortChannelId, TestConstants, TimestampSecond, TimestampSecondLong, ToMilliSatoshiConversion, UnknownFeature, randomBytes32} +import org.scalatest.TryValues.convertTryToSuccessOrFailure import org.scalatest.funsuite.AnyFunSuite import scodec.DecodeResult import scodec.bits._ @@ -136,12 +137,12 @@ class Bolt11InvoiceSpec extends AnyFunSuite { assert(invoice.prefix == "lnbc") assert(invoice.amount_opt.isEmpty) assert(invoice.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102") - assert(invoice.paymentSecret.map(_.bytes) == Some(hex"1111111111111111111111111111111111111111111111111111111111111111")) + assert(invoice.paymentSecret.map(_.bytes).contains(hex"1111111111111111111111111111111111111111111111111111111111111111")) assert(invoice.features == Features(Features.VariableLengthOnion -> Mandatory, Features.PaymentSecret -> Mandatory)) assert(invoice.createdAt == TimestampSecond(1496314658L)) assert(invoice.nodeId == PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) assert(invoice.description == Left("Please consider supporting this project")) - assert(invoice.fallbackAddress() == None) + assert(invoice.fallbackAddress().isEmpty) assert(invoice.tags.size == 4) assert(invoice.sign(priv).toString == ref) } @@ -150,13 +151,13 @@ class Bolt11InvoiceSpec extends AnyFunSuite { val ref = "lnbc2500u1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpu9qrsgquk0rl77nj30yxdy8j9vdx85fkpmdla2087ne0xh8nhedh8w27kyke0lp53ut353s06fv3qfegext0eh0ymjpf39tuven09sam30g4vgpfna3rh" val Success(invoice) = Bolt11Invoice.fromString(ref) assert(invoice.prefix == "lnbc") - assert(invoice.amount_opt == Some(250000000 msat)) + assert(invoice.amount_opt.contains(250000000 msat)) assert(invoice.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102") assert(invoice.features == Features(Features.VariableLengthOnion -> Mandatory, Features.PaymentSecret -> Mandatory)) assert(invoice.createdAt == TimestampSecond(1496314658L)) assert(invoice.nodeId == PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) assert(invoice.description == Left("1 cup coffee")) - assert(invoice.fallbackAddress() == None) + assert(invoice.fallbackAddress().isEmpty) assert(invoice.tags.size == 5) assert(invoice.sign(priv).toString == ref) } @@ -165,13 +166,13 @@ class Bolt11InvoiceSpec extends AnyFunSuite { val ref = "lnbc2500u1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdpquwpc4curk03c9wlrswe78q4eyqc7d8d0xqzpu9qrsgqhtjpauu9ur7fw2thcl4y9vfvh4m9wlfyz2gem29g5ghe2aak2pm3ps8fdhtceqsaagty2vph7utlgj48u0ged6a337aewvraedendscp573dxr" val Success(invoice) = Bolt11Invoice.fromString(ref) assert(invoice.prefix == "lnbc") - assert(invoice.amount_opt == Some(250000000 msat)) + assert(invoice.amount_opt.contains(250000000 msat)) assert(invoice.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102") assert(invoice.features == Features(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory)) assert(invoice.createdAt == TimestampSecond(1496314658L)) assert(invoice.nodeId == PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) assert(invoice.description == Left("ナンセンス 1杯")) - assert(invoice.fallbackAddress() == None) + assert(invoice.fallbackAddress().isEmpty) assert(invoice.tags.size == 5) assert(invoice.sign(priv).toString == ref) } @@ -180,13 +181,13 @@ class Bolt11InvoiceSpec extends AnyFunSuite { val ref = "lnbc20m1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqs9qrsgq7ea976txfraylvgzuxs8kgcw23ezlrszfnh8r6qtfpr6cxga50aj6txm9rxrydzd06dfeawfk6swupvz4erwnyutnjq7x39ymw6j38gp7ynn44" val Success(invoice) = Bolt11Invoice.fromString(ref) assert(invoice.prefix == "lnbc") - assert(invoice.amount_opt == Some(2000000000 msat)) + assert(invoice.amount_opt.contains(2000000000 msat)) assert(invoice.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102") assert(invoice.features == Features(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory)) assert(invoice.createdAt == TimestampSecond(1496314658L)) assert(invoice.nodeId == PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) assert(invoice.description == Right(Crypto.sha256(ByteVector("One piece of chocolate cake, one icecream cone, one pickle, one slice of swiss cheese, one slice of salami, one lollypop, one piece of cherry pie, one sausage, one cupcake, and one slice of watermelon".getBytes)))) - assert(invoice.fallbackAddress() == None) + assert(invoice.fallbackAddress().isEmpty) assert(invoice.tags.size == 4) assert(invoice.sign(priv).toString == ref) } @@ -195,13 +196,13 @@ class Bolt11InvoiceSpec extends AnyFunSuite { val ref = "lntb20m1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygshp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqfpp3x9et2e20v6pu37c5d9vax37wxq72un989qrsgqdj545axuxtnfemtpwkc45hx9d2ft7x04mt8q7y6t0k2dge9e7h8kpy9p34ytyslj3yu569aalz2xdk8xkd7ltxqld94u8h2esmsmacgpghe9k8" val Success(invoice) = Bolt11Invoice.fromString(ref) assert(invoice.prefix == "lntb") - assert(invoice.amount_opt == Some(2000000000 msat)) + assert(invoice.amount_opt.contains(2000000000 msat)) assert(invoice.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102") assert(invoice.features == Features(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory)) assert(invoice.createdAt == TimestampSecond(1496314658L)) assert(invoice.nodeId == PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) assert(invoice.description == Right(Crypto.sha256(ByteVector.view("One piece of chocolate cake, one icecream cone, one pickle, one slice of swiss cheese, one slice of salami, one lollypop, one piece of cherry pie, one sausage, one cupcake, and one slice of watermelon".getBytes)))) - assert(invoice.fallbackAddress() == Some("mk2QpYatsKicvFVuTAQLBryyccRXMUaGHP")) + assert(invoice.fallbackAddress().contains("mk2QpYatsKicvFVuTAQLBryyccRXMUaGHP")) assert(invoice.tags.size == 5) assert(invoice.sign(priv).toString == ref) } @@ -210,16 +211,16 @@ class Bolt11InvoiceSpec extends AnyFunSuite { val ref = "lnbc20m1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqsfpp3qjmp7lwpagxun9pygexvgpjdc4jdj85fr9yq20q82gphp2nflc7jtzrcazrra7wwgzxqc8u7754cdlpfrmccae92qgzqvzq2ps8pqqqqqqpqqqqq9qqqvpeuqafqxu92d8lr6fvg0r5gv0heeeqgcrqlnm6jhphu9y00rrhy4grqszsvpcgpy9qqqqqqgqqqqq7qqzq9qrsgqdfjcdk6w3ak5pca9hwfwfh63zrrz06wwfya0ydlzpgzxkn5xagsqz7x9j4jwe7yj7vaf2k9lqsdk45kts2fd0fkr28am0u4w95tt2nsq76cqw0" val Success(invoice) = Bolt11Invoice.fromString(ref) assert(invoice.prefix == "lnbc") - assert(invoice.amount_opt == Some(2000000000 msat)) + assert(invoice.amount_opt.contains(2000000000 msat)) assert(invoice.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102") assert(invoice.features == Features(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory)) assert(invoice.createdAt == TimestampSecond(1496314658L)) assert(invoice.nodeId == PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) assert(invoice.description == Right(Crypto.sha256(ByteVector.view("One piece of chocolate cake, one icecream cone, one pickle, one slice of swiss cheese, one slice of salami, one lollypop, one piece of cherry pie, one sausage, one cupcake, and one slice of watermelon".getBytes)))) - assert(invoice.fallbackAddress() == Some("1RustyRX2oai4EYYDpQGWvEL62BBGqN9T")) + assert(invoice.fallbackAddress().contains("1RustyRX2oai4EYYDpQGWvEL62BBGqN9T")) assert(invoice.routingInfo == List(List( - ExtraHop(PublicKey(hex"029e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255"), ShortChannelId("66051x263430x1800"), 1 msat, 20, CltvExpiryDelta(3)), - ExtraHop(PublicKey(hex"039e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255"), ShortChannelId("197637x395016x2314"), 2 msat, 30, CltvExpiryDelta(4)) + ExtraHop(PublicKey(hex"029e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255"), ShortChannelId.fromCoordinates("66051x263430x1800").success.value, 1 msat, 20, CltvExpiryDelta(3)), + ExtraHop(PublicKey(hex"039e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255"), ShortChannelId.fromCoordinates("197637x395016x2314").success.value, 2 msat, 30, CltvExpiryDelta(4)) ))) assert(invoice.tags.size == 6) assert(invoice.sign(priv).toString == ref) @@ -229,13 +230,13 @@ class Bolt11InvoiceSpec extends AnyFunSuite { val ref = "lnbc20m1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygshp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqfppj3a24vwu6r8ejrss3axul8rxldph2q7z99qrsgqz6qsgww34xlatfj6e3sngrwfy3ytkt29d2qttr8qz2mnedfqysuqypgqex4haa2h8fx3wnypranf3pdwyluftwe680jjcfp438u82xqphf75ym" val Success(invoice) = Bolt11Invoice.fromString(ref) assert(invoice.prefix == "lnbc") - assert(invoice.amount_opt == Some(2000000000 msat)) + assert(invoice.amount_opt.contains(2000000000 msat)) assert(invoice.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102") assert(invoice.features == Features(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory)) assert(invoice.createdAt == TimestampSecond(1496314658L)) assert(invoice.nodeId == PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) assert(invoice.description == Right(Crypto.sha256(ByteVector.view("One piece of chocolate cake, one icecream cone, one pickle, one slice of swiss cheese, one slice of salami, one lollypop, one piece of cherry pie, one sausage, one cupcake, and one slice of watermelon".getBytes)))) - assert(invoice.fallbackAddress() == Some("3EktnHQD7RiAE6uzMj2ZifT9YgRrkSgzQX")) + assert(invoice.fallbackAddress().contains("3EktnHQD7RiAE6uzMj2ZifT9YgRrkSgzQX")) assert(invoice.tags.size == 5) assert(invoice.sign(priv).toString == ref) } @@ -244,13 +245,13 @@ class Bolt11InvoiceSpec extends AnyFunSuite { val ref = "lnbc20m1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygshp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqfppqw508d6qejxtdg4y5r3zarvary0c5xw7k9qrsgqt29a0wturnys2hhxpner2e3plp6jyj8qx7548zr2z7ptgjjc7hljm98xhjym0dg52sdrvqamxdezkmqg4gdrvwwnf0kv2jdfnl4xatsqmrnsse" val Success(invoice) = Bolt11Invoice.fromString(ref) assert(invoice.prefix == "lnbc") - assert(invoice.amount_opt == Some(2000000000 msat)) + assert(invoice.amount_opt.contains(2000000000 msat)) assert(invoice.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102") assert(invoice.features == Features(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory)) assert(invoice.createdAt == TimestampSecond(1496314658L)) assert(invoice.nodeId == PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) assert(invoice.description == Right(Crypto.sha256(ByteVector.view("One piece of chocolate cake, one icecream cone, one pickle, one slice of swiss cheese, one slice of salami, one lollypop, one piece of cherry pie, one sausage, one cupcake, and one slice of watermelon".getBytes)))) - assert(invoice.fallbackAddress() == Some("bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4")) + assert(invoice.fallbackAddress().contains("bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4")) assert(invoice.tags.size == 5) assert(invoice.sign(priv).toString == ref) } @@ -259,14 +260,14 @@ class Bolt11InvoiceSpec extends AnyFunSuite { val ref = "lnbc20m1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygshp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqfp4qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q9qrsgq9vlvyj8cqvq6ggvpwd53jncp9nwc47xlrsnenq2zp70fq83qlgesn4u3uyf4tesfkkwwfg3qs54qe426hp3tz7z6sweqdjg05axsrjqp9yrrwc" val Success(invoice) = Bolt11Invoice.fromString(ref) assert(invoice.prefix == "lnbc") - assert(invoice.amount_opt == Some(2000000000 msat)) + assert(invoice.amount_opt.contains(2000000000 msat)) assert(invoice.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102") assert(invoice.features == Features(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory)) assert(!invoice.features.hasFeature(BasicMultiPartPayment)) assert(invoice.createdAt == TimestampSecond(1496314658L)) assert(invoice.nodeId == PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) assert(invoice.description == Right(Crypto.sha256(ByteVector.view("One piece of chocolate cake, one icecream cone, one pickle, one slice of swiss cheese, one slice of salami, one lollypop, one piece of cherry pie, one sausage, one cupcake, and one slice of watermelon".getBytes)))) - assert(invoice.fallbackAddress() == Some("bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3")) + assert(invoice.fallbackAddress().contains("bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3")) assert(invoice.tags.size == 5) assert(invoice.sign(priv).toString == ref) } @@ -275,14 +276,14 @@ class Bolt11InvoiceSpec extends AnyFunSuite { val ref = "lnbc20m1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygscqpvpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqsfp4qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q9qrsgq999fraffdzl6c8j7qd325dfurcq7vl0mfkdpdvve9fy3hy4lw0x9j3zcj2qdh5e5pyrp6cncvmxrhchgey64culwmjtw9wym74xm6xqqevh9r0" val Success(invoice) = Bolt11Invoice.fromString(ref) assert(invoice.prefix == "lnbc") - assert(invoice.amount_opt == Some(2000000000 msat)) + assert(invoice.amount_opt.contains(2000000000 msat)) assert(invoice.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102") assert(invoice.features == Features(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory)) assert(!invoice.features.hasFeature(BasicMultiPartPayment)) assert(invoice.createdAt == TimestampSecond(1496314658L)) assert(invoice.nodeId == PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) assert(invoice.description == Right(Crypto.sha256(ByteVector.view("One piece of chocolate cake, one icecream cone, one pickle, one slice of swiss cheese, one slice of salami, one lollypop, one piece of cherry pie, one sausage, one cupcake, and one slice of watermelon".getBytes)))) - assert(invoice.fallbackAddress() == Some("bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3")) + assert(invoice.fallbackAddress().contains("bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3")) assert(invoice.minFinalCltvExpiryDelta == CltvExpiryDelta(12)) assert(invoice.tags.size == 6) assert(invoice.sign(priv).toString == ref) @@ -300,9 +301,9 @@ class Bolt11InvoiceSpec extends AnyFunSuite { for (ref <- refs) { val Success(invoice) = Bolt11Invoice.fromString(ref) assert(invoice.prefix == "lnbc") - assert(invoice.amount_opt == Some(2500000000L msat)) + assert(invoice.amount_opt.contains(2500000000L msat)) assert(invoice.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102") - assert(invoice.paymentSecret == Some(ByteVector32(hex"1111111111111111111111111111111111111111111111111111111111111111"))) + assert(invoice.paymentSecret.contains(ByteVector32(hex"1111111111111111111111111111111111111111111111111111111111111111"))) assert(invoice.createdAt == TimestampSecond(1496314658L)) assert(invoice.nodeId == PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) assert(invoice.description == Left("coffee beans")) @@ -319,9 +320,9 @@ class Bolt11InvoiceSpec extends AnyFunSuite { val ref = "lnbc25m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5vdhkven9v5sxyetpdeessp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9q4psqqqqqqqqqqqqqqqqsgqtqyx5vggfcsll4wu246hz02kp85x4katwsk9639we5n5yngc3yhqkm35jnjw4len8vrnqnf5ejh0mzj9n3vz2px97evektfm2l6wqccp3y7372" val Success(invoice) = Bolt11Invoice.fromString(ref) assert(invoice.prefix == "lnbc") - assert(invoice.amount_opt == Some(2500000000L msat)) + assert(invoice.amount_opt.contains(2500000000L msat)) assert(invoice.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102") - assert(invoice.paymentSecret == Some(ByteVector32(hex"1111111111111111111111111111111111111111111111111111111111111111"))) + assert(invoice.paymentSecret.contains(ByteVector32(hex"1111111111111111111111111111111111111111111111111111111111111111"))) assert(invoice.createdAt == TimestampSecond(1496314658L)) assert(invoice.nodeId == PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) assert(invoice.description == Left("coffee beans")) @@ -338,7 +339,7 @@ class Bolt11InvoiceSpec extends AnyFunSuite { val ref = "lnbc9678785340p1pwmna7lpp5gc3xfm08u9qy06djf8dfflhugl6p7lgza6dsjxq454gxhj9t7a0sd8dgfkx7cmtwd68yetpd5s9xar0wfjn5gpc8qhrsdfq24f5ggrxdaezqsnvda3kkum5wfjkzmfqf3jkgem9wgsyuctwdus9xgrcyqcjcgpzgfskx6eqf9hzqnteypzxz7fzypfhg6trddjhygrcyqezcgpzfysywmm5ypxxjemgw3hxjmn8yptk7untd9hxwg3q2d6xjcmtv4ezq7pqxgsxzmnyyqcjqmt0wfjjq6t5v4khxsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygsxqyjw5qcqp2rzjq0gxwkzc8w6323m55m4jyxcjwmy7stt9hwkwe2qxmy8zpsgg7jcuwz87fcqqeuqqqyqqqqlgqqqqn3qq9q9qrsgqrvgkpnmps664wgkp43l22qsgdw4ve24aca4nymnxddlnp8vh9v2sdxlu5ywdxefsfvm0fq3sesf08uf6q9a2ke0hc9j6z6wlxg5z5kqpu2v9wz" val Success(invoice) = Bolt11Invoice.fromString(ref) assert(invoice.prefix == "lnbc") - assert(invoice.amount_opt == Some(967878534 msat)) + assert(invoice.amount_opt.contains(967878534 msat)) assert(invoice.paymentHash.bytes == hex"462264ede7e14047e9b249da94fefc47f41f7d02ee9b091815a5506bc8abf75f") assert(invoice.features == Features(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory)) assert(TestConstants.Alice.nodeParams.features.invoiceFeatures().areSupported(invoice.features)) @@ -348,7 +349,7 @@ class Bolt11InvoiceSpec extends AnyFunSuite { assert(invoice.fallbackAddress().isEmpty) assert(invoice.relativeExpiry == 604800.seconds) assert(invoice.minFinalCltvExpiryDelta == CltvExpiryDelta(10)) - assert(invoice.routingInfo == Seq(Seq(ExtraHop(PublicKey(hex"03d06758583bb5154774a6eb221b1276c9e82d65bbaceca806d90e20c108f4b1c7"), ShortChannelId("589390x3312x1"), 1000 msat, 2500, CltvExpiryDelta(40))))) + assert(invoice.routingInfo == Seq(Seq(ExtraHop(PublicKey(hex"03d06758583bb5154774a6eb221b1276c9e82d65bbaceca806d90e20c108f4b1c7"), ShortChannelId.fromCoordinates("589390x3312x1").success.value, 1000 msat, 2500, CltvExpiryDelta(40))))) assert(invoice.sign(priv).toString == ref) } @@ -356,14 +357,14 @@ class Bolt11InvoiceSpec extends AnyFunSuite { val ref = "lnbc10m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdp9wpshjmt9de6zqmt9w3skgct5vysxjmnnd9jx2mq8q8a04uqsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9q2gqqqqqqsgq7hf8he7ecf7n4ffphs6awl9t6676rrclv9ckg3d3ncn7fct63p6s365duk5wrk202cfy3aj5xnnp5gs3vrdvruverwwq7yzhkf5a3xqpd05wjc" val Success(invoice) = Bolt11Invoice.fromString(ref) assert(invoice.prefix == "lnbc") - assert(invoice.amount_opt == Some(1000000000 msat)) + assert(invoice.amount_opt.contains(1000000000 msat)) assert(invoice.paymentHash.bytes == hex"0001020304050607080900010203040506070809000102030405060708090102") assert(invoice.features == Features(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory, PaymentMetadata -> Mandatory)) assert(invoice.createdAt == TimestampSecond(1496314658L)) assert(invoice.nodeId == PublicKey(hex"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad")) - assert(invoice.paymentSecret == Some(ByteVector32(hex"1111111111111111111111111111111111111111111111111111111111111111"))) + assert(invoice.paymentSecret.contains(ByteVector32(hex"1111111111111111111111111111111111111111111111111111111111111111"))) assert(invoice.description == Left("payment metadata inside")) - assert(invoice.paymentMetadata == Some(hex"01fafaf0")) + assert(invoice.paymentMetadata.contains(hex"01fafaf0")) assert(invoice.tags.size == 5) assert(invoice.sign(priv).toString == ref) } @@ -457,7 +458,7 @@ class Bolt11InvoiceSpec extends AnyFunSuite { test("Pay 1 BTC without multiplier") { val ref = "lnbc1000m1pdkmqhusp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygspp5n2ees808r98m0rh4472yyth0c5fptzcxmexcjznrzmq8xald0cgqdqsf4ujqarfwqsxymmccqp2pv37ezvhth477nu0yhhjlcry372eef57qmldhreqnr0kx82jkupp3n7nw42u3kdyyjskdr8jhjy2vugr3skdmy8ersft36969xplkxsp2v7c58" val Success(invoice) = Bolt11Invoice.fromString(ref) - assert(invoice.amount_opt == Some(100000000000L msat)) + assert(invoice.amount_opt.contains(100000000000L msat)) assert(features2bits(invoice.features) == BitVector.empty) } @@ -519,7 +520,7 @@ class Bolt11InvoiceSpec extends AnyFunSuite { val Success(pr2) = Bolt11Invoice.fromString("lnbc40n1pw9qjvwpp5qq3w2ln6krepcslqszkrsfzwy49y0407hvks30ec6pu9s07jur3sdpstfshq5n9v9jzucm0d5s8vmm5v5s8qmmnwssyj3p6yqenwdencqzysxqrrss7ju0s4dwx6w8a95a9p2xc5vudl09gjl0w2n02sjrvffde632nxwh2l4w35nqepj4j5njhh4z65wyfc724yj6dn9wajvajfn5j7em6wsq2elakl") assert(!pr2.features.hasFeature(PaymentSecret, Some(Mandatory))) - assert(pr2.paymentSecret == None) + assert(pr2.paymentSecret.isEmpty) // An invoice that sets the payment secret feature bit must provide a payment secret. assert(Bolt11Invoice.fromString("lnbc1230p1pwljzn3pp5qyqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqdq52dhk6efqd9h8vmmfvdjs9qypqsqylvwhf7xlpy6xpecsnpcjjuuslmzzgeyv90mh7k7vs88k2dkxgrkt75qyfjv5ckygw206re7spga5zfd4agtdvtktxh5pkjzhn9dq2cqz9upw7").isFailure) @@ -627,7 +628,7 @@ class Bolt11InvoiceSpec extends AnyFunSuite { assert(Bolt11Invoice.fromString(invoice.toString).get == invoice) } - test("Invoices can't have high features"){ + test("Invoices can't have high features") { assertThrows[Exception](createInvoiceUnsafe(Block.LivenetGenesisBlock.hash, Some(123 msat), ByteVector32.One, priv, Left("Some invoice"), CltvExpiryDelta(18), features = Features[Feature](Map[Feature, FeatureSupport](VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory), Set(UnknownFeature(424242))))) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/RouteCalculationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/RouteCalculationSpec.scala index aca83a802d..b57d1759f5 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/RouteCalculationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/RouteCalculationSpec.scala @@ -19,7 +19,6 @@ package fr.acinq.eclair.router import com.softwaremill.quicklens.ModifyPimp import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, ByteVector64, Satoshi, SatoshiLong} -import fr.acinq.eclair.RealShortChannelId import fr.acinq.eclair.payment.Bolt11Invoice.ExtraHop import fr.acinq.eclair.payment.relay.Relayer.RelayFees import fr.acinq.eclair.router.BaseRouterSpec.channelHopFromUpdate @@ -30,7 +29,8 @@ import fr.acinq.eclair.router.RouteCalculation._ import fr.acinq.eclair.router.Router._ import fr.acinq.eclair.transactions.Transactions import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{BlockHeight, CltvExpiryDelta, Features, MilliSatoshi, MilliSatoshiLong, RealShortChannelId, ShortChannelId, TimestampSecond, TimestampSecondLong, ToMilliSatoshiConversion, randomKey} +import fr.acinq.eclair.{BlockHeight, CltvExpiryDelta, Features, MilliSatoshi, MilliSatoshiLong, RealShortChannelId, ShortChannelId, ShortChannelIdSpec, TimestampSecond, TimestampSecondLong, ToMilliSatoshiConversion, randomKey} +import org.scalatest.TryValues.convertTryToSuccessOrFailure import org.scalatest.funsuite.AnyFunSuite import org.scalatest.{ParallelTestExecution, Tag} import scodec.bits._ @@ -849,12 +849,12 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { val currentBlockHeight = BlockHeight(554000) val g = DirectedGraph(List( - makeEdge(ShortChannelId(s"${currentBlockHeight.toLong}x0x1").toLong, a, b, feeBase = 1 msat, 0, minHtlc = 0 msat, maxHtlc = None, cltvDelta = CltvExpiryDelta(144)), - makeEdge(ShortChannelId(s"${currentBlockHeight.toLong}x0x4").toLong, a, e, feeBase = 1 msat, 0, minHtlc = 0 msat, maxHtlc = None, cltvDelta = CltvExpiryDelta(144)), - makeEdge(ShortChannelId(s"${currentBlockHeight.toLong - 3000}x0x2").toLong, b, c, feeBase = 1 msat, 0, minHtlc = 0 msat, maxHtlc = None, cltvDelta = CltvExpiryDelta(144)), // younger channel - makeEdge(ShortChannelId(s"${currentBlockHeight.toLong - 3000}x0x3").toLong, c, d, feeBase = 1 msat, 0, minHtlc = 0 msat, maxHtlc = None, cltvDelta = CltvExpiryDelta(144)), - makeEdge(ShortChannelId(s"${currentBlockHeight.toLong}x0x5").toLong, e, f, feeBase = 1 msat, 0, minHtlc = 0 msat, maxHtlc = None, cltvDelta = CltvExpiryDelta(144)), - makeEdge(ShortChannelId(s"${currentBlockHeight.toLong}x0x6").toLong, f, d, feeBase = 1 msat, 0, minHtlc = 0 msat, maxHtlc = None, cltvDelta = CltvExpiryDelta(144)) + makeEdge(ShortChannelId.fromCoordinates(s"${currentBlockHeight.toLong}x0x1").success.value.toLong, a, b, feeBase = 1 msat, 0, minHtlc = 0 msat, maxHtlc = None, cltvDelta = CltvExpiryDelta(144)), + makeEdge(ShortChannelId.fromCoordinates(s"${currentBlockHeight.toLong}x0x4").success.value.toLong, a, e, feeBase = 1 msat, 0, minHtlc = 0 msat, maxHtlc = None, cltvDelta = CltvExpiryDelta(144)), + makeEdge(ShortChannelId.fromCoordinates(s"${currentBlockHeight.toLong - 3000}x0x2").success.value.toLong, b, c, feeBase = 1 msat, 0, minHtlc = 0 msat, maxHtlc = None, cltvDelta = CltvExpiryDelta(144)), // younger channel + makeEdge(ShortChannelId.fromCoordinates(s"${currentBlockHeight.toLong - 3000}x0x3").success.value.toLong, c, d, feeBase = 1 msat, 0, minHtlc = 0 msat, maxHtlc = None, cltvDelta = CltvExpiryDelta(144)), + makeEdge(ShortChannelId.fromCoordinates(s"${currentBlockHeight.toLong}x0x5").success.value.toLong, e, f, feeBase = 1 msat, 0, minHtlc = 0 msat, maxHtlc = None, cltvDelta = CltvExpiryDelta(144)), + makeEdge(ShortChannelId.fromCoordinates(s"${currentBlockHeight.toLong}x0x6").success.value.toLong, f, d, feeBase = 1 msat, 0, minHtlc = 0 msat, maxHtlc = None, cltvDelta = CltvExpiryDelta(144)) )) val Success(routeScoreOptimized :: Nil) = findRoute(g, a, d, DEFAULT_AMOUNT_MSAT / 2, DEFAULT_MAX_FEE, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.copy(heuristics = Left(WeightRatios( @@ -917,27 +917,27 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { // then if the cost function is not monotonic the path-finding breaks because the result path contains a loop. val updates = SortedMap( RealShortChannelId(BlockHeight(565643), 1216, 0) -> PublicChannel( - ann = makeChannel(ShortChannelId("565643x1216x0").toLong, PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f"), PublicKey(hex"024655b768ef40951b20053a5c4b951606d4d86085d51238f2c67c7dec29c792ca")), + ann = makeChannel(ShortChannelId.fromCoordinates("565643x1216x0").success.value.toLong, PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f"), PublicKey(hex"024655b768ef40951b20053a5c4b951606d4d86085d51238f2c67c7dec29c792ca")), fundingTxid = ByteVector32.Zeroes, capacity = DEFAULT_CAPACITY, - update_1_opt = Some(ChannelUpdate(ByteVector64.Zeroes, ByteVector32.Zeroes, ShortChannelId("565643x1216x0"), 0 unixsec, ChannelUpdate.ChannelFlags.DUMMY, CltvExpiryDelta(14), htlcMinimumMsat = 1 msat, feeBaseMsat = 1000 msat, 10, Some(4294967295L msat))), - update_2_opt = Some(ChannelUpdate(ByteVector64.Zeroes, ByteVector32.Zeroes, ShortChannelId("565643x1216x0"), 0 unixsec, ChannelUpdate.ChannelFlags(isEnabled = true, isNode1 = false), CltvExpiryDelta(144), htlcMinimumMsat = 0 msat, feeBaseMsat = 1000 msat, 100, Some(15000000000L msat))), + update_1_opt = Some(ChannelUpdate(ByteVector64.Zeroes, ByteVector32.Zeroes, ShortChannelId.fromCoordinates("565643x1216x0").success.value, 0 unixsec, ChannelUpdate.ChannelFlags.DUMMY, CltvExpiryDelta(14), htlcMinimumMsat = 1 msat, feeBaseMsat = 1000 msat, 10, Some(4294967295L msat))), + update_2_opt = Some(ChannelUpdate(ByteVector64.Zeroes, ByteVector32.Zeroes, ShortChannelId.fromCoordinates("565643x1216x0").success.value, 0 unixsec, ChannelUpdate.ChannelFlags(isEnabled = true, isNode1 = false), CltvExpiryDelta(144), htlcMinimumMsat = 0 msat, feeBaseMsat = 1000 msat, 100, Some(15000000000L msat))), meta_opt = None ), RealShortChannelId(BlockHeight(542280), 2156, 0) -> PublicChannel( - ann = makeChannel(ShortChannelId("542280x2156x0").toLong, PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f"), PublicKey(hex"03cb7983dc247f9f81a0fa2dfa3ce1c255365f7279c8dd143e086ca333df10e278")), + ann = makeChannel(ShortChannelId.fromCoordinates("542280x2156x0").success.value.toLong, PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f"), PublicKey(hex"03cb7983dc247f9f81a0fa2dfa3ce1c255365f7279c8dd143e086ca333df10e278")), fundingTxid = ByteVector32.Zeroes, capacity = DEFAULT_CAPACITY, - update_1_opt = Some(ChannelUpdate(ByteVector64.Zeroes, ByteVector32.Zeroes, ShortChannelId("542280x2156x0"), 0 unixsec, ChannelUpdate.ChannelFlags.DUMMY, CltvExpiryDelta(144), htlcMinimumMsat = 1000 msat, feeBaseMsat = 1000 msat, 100, Some(16777000000L msat))), - update_2_opt = Some(ChannelUpdate(ByteVector64.Zeroes, ByteVector32.Zeroes, ShortChannelId("542280x2156x0"), 0 unixsec, ChannelUpdate.ChannelFlags(isEnabled = true, isNode1 = false), CltvExpiryDelta(144), htlcMinimumMsat = 1 msat, feeBaseMsat = 667 msat, 1, Some(16777000000L msat))), + update_1_opt = Some(ChannelUpdate(ByteVector64.Zeroes, ByteVector32.Zeroes, ShortChannelId.fromCoordinates("542280x2156x0").success.value, 0 unixsec, ChannelUpdate.ChannelFlags.DUMMY, CltvExpiryDelta(144), htlcMinimumMsat = 1000 msat, feeBaseMsat = 1000 msat, 100, Some(16777000000L msat))), + update_2_opt = Some(ChannelUpdate(ByteVector64.Zeroes, ByteVector32.Zeroes, ShortChannelId.fromCoordinates("542280x2156x0").success.value, 0 unixsec, ChannelUpdate.ChannelFlags(isEnabled = true, isNode1 = false), CltvExpiryDelta(144), htlcMinimumMsat = 1 msat, feeBaseMsat = 667 msat, 1, Some(16777000000L msat))), meta_opt = None ), RealShortChannelId(BlockHeight(565779), 2711, 0) -> PublicChannel( - ann = makeChannel(ShortChannelId("565779x2711x0").toLong, PublicKey(hex"036d65409c41ab7380a43448f257809e7496b52bf92057c09c4f300cbd61c50d96"), PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f")), + ann = makeChannel(ShortChannelId.fromCoordinates("565779x2711x0").success.value.toLong, PublicKey(hex"036d65409c41ab7380a43448f257809e7496b52bf92057c09c4f300cbd61c50d96"), PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f")), fundingTxid = ByteVector32.Zeroes, capacity = DEFAULT_CAPACITY, - update_1_opt = Some(ChannelUpdate(ByteVector64.Zeroes, ByteVector32.Zeroes, ShortChannelId("565779x2711x0"), 0 unixsec, ChannelUpdate.ChannelFlags.DUMMY, CltvExpiryDelta(144), htlcMinimumMsat = 1 msat, feeBaseMsat = 1000 msat, 100, Some(230000000L msat))), - update_2_opt = Some(ChannelUpdate(ByteVector64.Zeroes, ByteVector32.Zeroes, ShortChannelId("565779x2711x0"), 0 unixsec, ChannelUpdate.ChannelFlags(isEnabled = false, isNode1 = false), CltvExpiryDelta(144), htlcMinimumMsat = 1 msat, feeBaseMsat = 1000 msat, 100, Some(230000000L msat))), + update_1_opt = Some(ChannelUpdate(ByteVector64.Zeroes, ByteVector32.Zeroes, ShortChannelId.fromCoordinates("565779x2711x0").success.value, 0 unixsec, ChannelUpdate.ChannelFlags.DUMMY, CltvExpiryDelta(144), htlcMinimumMsat = 1 msat, feeBaseMsat = 1000 msat, 100, Some(230000000L msat))), + update_2_opt = Some(ChannelUpdate(ByteVector64.Zeroes, ByteVector32.Zeroes, ShortChannelId.fromCoordinates("565779x2711x0").success.value, 0 unixsec, ChannelUpdate.ChannelFlags(isEnabled = false, isNode1 = false), CltvExpiryDelta(144), htlcMinimumMsat = 1 msat, feeBaseMsat = 1000 msat, 100, Some(230000000L msat))), meta_opt = None ) ) @@ -1871,7 +1871,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { test("use direct channel when available") { // A ===> B ===> C // \___________/ - val recentChannelId = ShortChannelId("399990x1x2").toLong + val recentChannelId = ShortChannelId.fromCoordinates("399990x1x2").success.value.toLong val g = DirectedGraph(List( makeEdge(1L, a, b, 1 msat, 1, capacity = 100_000_000 sat), makeEdge(2L, b, c, 1 msat, 1, capacity = 100_000_000 sat), diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/ExtendedQueriesCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/ExtendedQueriesCodecsSpec.scala index 9d8e643d62..fa3c241427 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/ExtendedQueriesCodecsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/ExtendedQueriesCodecsSpec.scala @@ -17,12 +17,11 @@ package fr.acinq.eclair.wire.protocol import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, ByteVector64} -import fr.acinq.eclair.RealShortChannelId -import fr.acinq.eclair.RealShortChannelId import fr.acinq.eclair.router.Sync import fr.acinq.eclair.wire.protocol.LightningMessageCodecs._ import fr.acinq.eclair.wire.protocol.ReplyChannelRangeTlv._ -import fr.acinq.eclair.{BlockHeight, CltvExpiryDelta, MilliSatoshiLong, ShortChannelId, TimestampSecond, TimestampSecondLong, UInt64} +import fr.acinq.eclair.{BlockHeight, CltvExpiryDelta, MilliSatoshiLong, RealShortChannelId, ShortChannelId, TimestampSecond, TimestampSecondLong, UInt64} +import org.scalatest.TryValues.convertTryToSuccessOrFailure import org.scalatest.funsuite.AnyFunSuite import scodec.bits._ @@ -152,7 +151,7 @@ class ExtendedQueriesCodecsSpec extends AnyFunSuite { val update = ChannelUpdate( chainHash = ByteVector32.fromValidHex("06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f"), signature = ByteVector64.fromValidHex("76df7e70c63cc2b63ef1c062b99c6d934a80ef2fd4dae9e1d86d277f47674af3255a97fa52ade7f129263f591ed784996eba6383135896cc117a438c80293282"), - shortChannelId = ShortChannelId("103x1x0"), + shortChannelId = ShortChannelId.fromCoordinates("103x1x0").success.value, timestamp = TimestampSecond(1565587763L), channelFlags = ChannelUpdate.ChannelFlags.DUMMY, cltvExpiryDelta = CltvExpiryDelta(144), @@ -172,7 +171,7 @@ class ExtendedQueriesCodecsSpec extends AnyFunSuite { val update = ChannelUpdate( chainHash = ByteVector32.fromValidHex("06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f"), signature = ByteVector64.fromValidHex("06737e9e18d3e4d0ab4066ccaecdcc10e648c5f1c5413f1610747e0d463fa7fa39c1b02ea2fd694275ecfefe4fe9631f24afd182ab75b805e16cd550941f858c"), - shortChannelId = ShortChannelId("109x1x0"), + shortChannelId = ShortChannelId.fromCoordinates("109x1x0").success.value, timestamp = TimestampSecond(1565587765L), channelFlags = ChannelUpdate.ChannelFlags.DUMMY, cltvExpiryDelta = CltvExpiryDelta(48), diff --git a/eclair-node/src/main/scala/fr/acinq/eclair/api/serde/FormParamExtractors.scala b/eclair-node/src/main/scala/fr/acinq/eclair/api/serde/FormParamExtractors.scala index 5f1bc093d8..4744dae6df 100644 --- a/eclair-node/src/main/scala/fr/acinq/eclair/api/serde/FormParamExtractors.scala +++ b/eclair-node/src/main/scala/fr/acinq/eclair/api/serde/FormParamExtractors.scala @@ -27,7 +27,7 @@ import fr.acinq.eclair.crypto.Sphinx import fr.acinq.eclair.io.NodeURI import fr.acinq.eclair.payment.Bolt11Invoice import fr.acinq.eclair.wire.protocol.MessageOnionCodecs.blindedRouteCodec -import fr.acinq.eclair.{UnspecifiedShortChannelId, MilliSatoshi, ShortChannelId, TimestampSecond} +import fr.acinq.eclair.{MilliSatoshi, ShortChannelId, TimestampSecond} import scodec.bits.ByteVector import java.util.UUID @@ -46,9 +46,9 @@ object FormParamExtractors { implicit val bolt11Unmarshaller: Unmarshaller[String, Bolt11Invoice] = Unmarshaller.strict { rawRequest => Bolt11Invoice.fromString(rawRequest).get } - implicit val shortChannelIdUnmarshaller: Unmarshaller[String, ShortChannelId] = Unmarshaller.strict { str => ShortChannelId(str) } + implicit val shortChannelIdUnmarshaller: Unmarshaller[String, ShortChannelId] = Unmarshaller.strict { str => ShortChannelId.fromCoordinates(str).get } - implicit val shortChannelIdsUnmarshaller: Unmarshaller[String, List[ShortChannelId]] = listUnmarshaller(str => ShortChannelId(str)) + implicit val shortChannelIdsUnmarshaller: Unmarshaller[String, List[ShortChannelId]] = listUnmarshaller(str => ShortChannelId.fromCoordinates(str).get) implicit val javaUUIDUnmarshaller: Unmarshaller[String, UUID] = Unmarshaller.strict { str => UUID.fromString(str) } diff --git a/eclair-node/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala b/eclair-node/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala index e134c67b7a..36f3d7c37f 100644 --- a/eclair-node/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala +++ b/eclair-node/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala @@ -50,6 +50,7 @@ import fr.acinq.eclair.router.Router.{ChannelRelayParams, PredefinedNodeRoute} import fr.acinq.eclair.wire.protocol._ import org.json4s.{Formats, Serialization} import org.mockito.scalatest.IdiomaticMockito +import org.scalatest.TryValues.convertTryToSuccessOrFailure import org.scalatest.funsuite.AnyFunSuite import org.scalatest.matchers.should.Matchers import scodec.bits._ @@ -380,12 +381,13 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM test("'close' method should accept shortChannelIds") { val shortChannelIdSerialized = "42000x27x3" + val shortChannelId = ShortChannelId.fromCoordinates(shortChannelIdSerialized).success.value val channelId = ByteVector32(hex"56d7d6eda04d80138270c49709f1eadb5ab4939e5061309ccdacdb98ce637d0e") val channelIdSerialized = channelId.toHex val response = Map[ChannelIdentifier, Either[Throwable, CommandResponse[CMD_CLOSE]]]( Left(channelId) -> Right(RES_SUCCESS(CMD_CLOSE(ActorRef.noSender, None, None), channelId)), Left(channelId.reverse) -> Left(new RuntimeException("channel not found")), - Right(ShortChannelId(shortChannelIdSerialized)) -> Right(RES_SUCCESS(CMD_CLOSE(ActorRef.noSender, None, None), ByteVector32.fromValidHex(channelIdSerialized.reverse))) + Right(shortChannelId) -> Right(RES_SUCCESS(CMD_CLOSE(ActorRef.noSender, None, None), ByteVector32.fromValidHex(channelIdSerialized.reverse))) ) val eclair = mock[Eclair] @@ -400,19 +402,20 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM assert(handled) assert(status == OK) val resp = entityAs[String] - eclair.close(Right(ShortChannelId(shortChannelIdSerialized)) :: Nil, None, None)(any[Timeout]).wasCalled(once) + eclair.close(Right(shortChannelId) :: Nil, None, None)(any[Timeout]).wasCalled(once) matchTestJson("close", resp) } } test("'close' method should accept channelIds") { val shortChannelIdSerialized = "42000x27x3" + val shortChannelId = ShortChannelId.fromCoordinates(shortChannelIdSerialized).success.value val channelId = ByteVector32(hex"56d7d6eda04d80138270c49709f1eadb5ab4939e5061309ccdacdb98ce637d0e") val channelIdSerialized = channelId.toHex val response = Map[ChannelIdentifier, Either[Throwable, CommandResponse[CMD_CLOSE]]]( Left(channelId) -> Right(RES_SUCCESS(CMD_CLOSE(ActorRef.noSender, None, None), channelId)), Left(channelId.reverse) -> Left(new RuntimeException("channel not found")), - Right(ShortChannelId(shortChannelIdSerialized)) -> Right(RES_SUCCESS(CMD_CLOSE(ActorRef.noSender, None, None), ByteVector32.fromValidHex(channelIdSerialized.reverse))) + Right(shortChannelId) -> Right(RES_SUCCESS(CMD_CLOSE(ActorRef.noSender, None, None), ByteVector32.fromValidHex(channelIdSerialized.reverse))) ) val eclair = mock[Eclair] @@ -434,12 +437,13 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM test("'close' method should accept channelIds and shortChannelIds") { val shortChannelIdSerialized = "42000x27x3" + val shortChannelId = ShortChannelId.fromCoordinates(shortChannelIdSerialized).success.value val channelId = ByteVector32(hex"56d7d6eda04d80138270c49709f1eadb5ab4939e5061309ccdacdb98ce637d0e") val channelIdSerialized = channelId.toHex val response = Map[ChannelIdentifier, Either[Throwable, CommandResponse[CMD_CLOSE]]]( Left(channelId) -> Right(RES_SUCCESS(CMD_CLOSE(ActorRef.noSender, None, None), channelId)), Left(channelId.reverse) -> Left(new RuntimeException("channel not found")), - Right(ShortChannelId(shortChannelIdSerialized)) -> Right(RES_SUCCESS(CMD_CLOSE(ActorRef.noSender, None, None), ByteVector32.fromValidHex(channelIdSerialized.reverse))) + Right(shortChannelId) -> Right(RES_SUCCESS(CMD_CLOSE(ActorRef.noSender, None, None), ByteVector32.fromValidHex(channelIdSerialized.reverse))) ) val eclair = mock[Eclair] @@ -454,22 +458,23 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM assert(handled) assert(status == OK) val resp = entityAs[String] - eclair.close(Left(channelId) :: Right(ShortChannelId("42000x27x3")) :: Right(ShortChannelId("42000x561x1")) :: Nil, None, None)(any[Timeout]).wasCalled(once) + eclair.close(Left(channelId) :: Right(ShortChannelId.fromCoordinates("42000x27x3").success.value) :: Right(ShortChannelId.fromCoordinates("42000x561x1").success.value) :: Nil, None, None)(any[Timeout]).wasCalled(once) matchTestJson("close", resp) } } test("'close' accepts custom closing feerates 1") { - val shortChannelId = "1701x42x3" + val shortChannelIdSerialized = "1701x42x3" + val shortChannelId = ShortChannelId.fromCoordinates(shortChannelIdSerialized).success.value val response = Map[ChannelIdentifier, Either[Throwable, CommandResponse[CMD_CLOSE]]]( - Right(ShortChannelId(shortChannelId)) -> Right(RES_SUCCESS(CMD_CLOSE(ActorRef.noSender, None, None), randomBytes32())) + Right(shortChannelId) -> Right(RES_SUCCESS(CMD_CLOSE(ActorRef.noSender, None, None), randomBytes32())) ) val eclair = mock[Eclair] eclair.close(any, any, any)(any[Timeout]) returns Future.successful(response) val mockService = new MockService(eclair) - Post("/close", FormData("shortChannelId" -> shortChannelId, "preferredFeerateSatByte" -> "10", "minFeerateSatByte" -> "2", "maxFeerateSatByte" -> "50").toEntity) ~> + Post("/close", FormData("shortChannelId" -> shortChannelIdSerialized, "preferredFeerateSatByte" -> "10", "minFeerateSatByte" -> "2", "maxFeerateSatByte" -> "50").toEntity) ~> addCredentials(BasicHttpCredentials("", mockApi().password)) ~> addHeader("Content-Type", "application/json") ~> Route.seal(mockService.close) ~> @@ -477,21 +482,22 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM assert(handled) assert(status == OK) val expectedFeerates = ClosingFeerates(FeeratePerKw(2500 sat), FeeratePerKw(500 sat), FeeratePerKw(12500 sat)) - eclair.close(Right(ShortChannelId(shortChannelId)) :: Nil, None, Some(expectedFeerates))(any[Timeout]).wasCalled(once) + eclair.close(Right(shortChannelId) :: Nil, None, Some(expectedFeerates))(any[Timeout]).wasCalled(once) } } test("'close' accepts custom closing feerates 2") { - val shortChannelId = "1701x42x3" + val shortChannelIdSerialized = "1701x42x3" + val shortChannelId = ShortChannelId.fromCoordinates(shortChannelIdSerialized).success.value val response = Map[ChannelIdentifier, Either[Throwable, CommandResponse[CMD_CLOSE]]]( - Right(ShortChannelId(shortChannelId)) -> Right(RES_SUCCESS(CMD_CLOSE(ActorRef.noSender, None, None), randomBytes32())) + Right(shortChannelId) -> Right(RES_SUCCESS(CMD_CLOSE(ActorRef.noSender, None, None), randomBytes32())) ) val eclair = mock[Eclair] eclair.close(any, any, any)(any[Timeout]) returns Future.successful(response) val mockService = new MockService(eclair) - Post("/close", FormData("shortChannelId" -> shortChannelId, "preferredFeerateSatByte" -> "10").toEntity) ~> + Post("/close", FormData("shortChannelId" -> shortChannelIdSerialized, "preferredFeerateSatByte" -> "10").toEntity) ~> addCredentials(BasicHttpCredentials("", mockApi().password)) ~> addHeader("Content-Type", "application/json") ~> Route.seal(mockService.close) ~> @@ -499,21 +505,22 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM assert(handled) assert(status == OK) val expectedFeerates = ClosingFeerates(FeeratePerKw(2500 sat), FeeratePerKw(1250 sat), FeeratePerKw(5000 sat)) - eclair.close(Right(ShortChannelId(shortChannelId)) :: Nil, None, Some(expectedFeerates))(any[Timeout]).wasCalled(once) + eclair.close(Right(shortChannelId) :: Nil, None, Some(expectedFeerates))(any[Timeout]).wasCalled(once) } } test("'close' accepts custom closing feerates 3") { - val shortChannelId = "1701x42x3" + val shortChannelIdSerialized = "1701x42x3" + val shortChannelId = ShortChannelId.fromCoordinates(shortChannelIdSerialized).success.value val response = Map[ChannelIdentifier, Either[Throwable, CommandResponse[CMD_CLOSE]]]( - Right(ShortChannelId(shortChannelId)) -> Right(RES_SUCCESS(CMD_CLOSE(ActorRef.noSender, None, None), randomBytes32())) + Right(shortChannelId) -> Right(RES_SUCCESS(CMD_CLOSE(ActorRef.noSender, None, None), randomBytes32())) ) val eclair = mock[Eclair] eclair.close(any, any, any)(any[Timeout]) returns Future.successful(response) val mockService = new MockService(eclair) - Post("/close", FormData("shortChannelId" -> shortChannelId, "preferredFeerateSatByte" -> "10", "minFeerateSatByte" -> "2").toEntity) ~> + Post("/close", FormData("shortChannelId" -> shortChannelIdSerialized, "preferredFeerateSatByte" -> "10", "minFeerateSatByte" -> "2").toEntity) ~> addCredentials(BasicHttpCredentials("", mockApi().password)) ~> addHeader("Content-Type", "application/json") ~> Route.seal(mockService.close) ~> @@ -521,21 +528,22 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM assert(handled) assert(status == OK) val expectedFeerates = ClosingFeerates(FeeratePerKw(2500 sat), FeeratePerKw(500 sat), FeeratePerKw(5000 sat)) - eclair.close(Right(ShortChannelId(shortChannelId)) :: Nil, None, Some(expectedFeerates))(any[Timeout]).wasCalled(once) + eclair.close(Right(shortChannelId) :: Nil, None, Some(expectedFeerates))(any[Timeout]).wasCalled(once) } } test("'close' accepts custom closing feerates 4") { - val shortChannelId = "1701x42x3" + val shortChannelIdSerialized = "1701x42x3" + val shortChannelId = ShortChannelId.fromCoordinates(shortChannelIdSerialized).success.value val response = Map[ChannelIdentifier, Either[Throwable, CommandResponse[CMD_CLOSE]]]( - Right(ShortChannelId(shortChannelId)) -> Right(RES_SUCCESS(CMD_CLOSE(ActorRef.noSender, None, None), randomBytes32())) + Right(shortChannelId) -> Right(RES_SUCCESS(CMD_CLOSE(ActorRef.noSender, None, None), randomBytes32())) ) val eclair = mock[Eclair] eclair.close(any, any, any)(any[Timeout]) returns Future.successful(response) val mockService = new MockService(eclair) - Post("/close", FormData("shortChannelId" -> shortChannelId, "preferredFeerateSatByte" -> "10", "maxFeerateSatByte" -> "50").toEntity) ~> + Post("/close", FormData("shortChannelId" -> shortChannelIdSerialized, "preferredFeerateSatByte" -> "10", "maxFeerateSatByte" -> "50").toEntity) ~> addCredentials(BasicHttpCredentials("", mockApi().password)) ~> addHeader("Content-Type", "application/json") ~> Route.seal(mockService.close) ~> @@ -543,7 +551,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM assert(handled) assert(status == OK) val expectedFeerates = ClosingFeerates(FeeratePerKw(2500 sat), FeeratePerKw(1250 sat), FeeratePerKw(12500 sat)) - eclair.close(Right(ShortChannelId(shortChannelId)) :: Nil, None, Some(expectedFeerates))(any[Timeout]).wasCalled(once) + eclair.close(Right(shortChannelId) :: Nil, None, Some(expectedFeerates))(any[Timeout]).wasCalled(once) } } @@ -1004,7 +1012,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM htlcMaximumMsat = None ) val mockChannelUpdate2 = mockChannelUpdate1.copy(shortChannelId = RealShortChannelId(BlockHeight(1), 2, 4)) - val mockChannelUpdate3 = mockChannelUpdate1.copy(shortChannelId = RealShortChannelId (BlockHeight(1), 2, 5)) + val mockChannelUpdate3 = mockChannelUpdate1.copy(shortChannelId = RealShortChannelId(BlockHeight(1), 2, 5)) val mockHop1 = Router.ChannelHop(mockChannelUpdate1.shortChannelId, PublicKey.fromBin(ByteVector.fromValidHex("03007e67dc5a8fd2b2ef21cb310ab6359ddb51f3f86a8b79b8b1e23bc3a6ea150a")), PublicKey.fromBin(ByteVector.fromValidHex("026105f6cb4862810be989385d16f04b0f748f6f2a14040338b1a534d45b4be1c1")), ChannelRelayParams.FromAnnouncement(mockChannelUpdate1)) val mockHop2 = Router.ChannelHop(mockChannelUpdate2.shortChannelId, mockHop1.nextNodeId, PublicKey.fromBin(ByteVector.fromValidHex("038cfa2b5857843ee90cff91b06f692c0d8fe201921ee6387aee901d64f43699f0")), ChannelRelayParams.FromAnnouncement(mockChannelUpdate2)) From fc30eab0a0bffa796da86458700535a4c635e9e8 Mon Sep 17 00:00:00 2001 From: Bastien Teinturier <31281497+t-bast@users.noreply.github.com> Date: Thu, 16 Jun 2022 17:32:19 +0200 Subject: [PATCH 097/121] Remove `channelId` from `PublicChannel` (#2324) We can currently deduce the `channel_id` of a public channel because it is based on the funding transaction coordinates, but that won't be the case with dual-funding, where the `channel_id` will be private information between the two channel peers. --- .../scala/fr/acinq/eclair/router/Router.scala | 1 - .../scala/fr/acinq/eclair/router/Validation.scala | 15 ++++++--------- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala index 5755151d33..d40e5c1504 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala @@ -359,7 +359,6 @@ object Router { val nodeId1: PublicKey = ann.nodeId1 val nodeId2: PublicKey = ann.nodeId2 def shortChannelId: RealShortChannelId = ann.shortChannelId - def channelId: ByteVector32 = toLongId(fundingTxid.reverse, ann.shortChannelId.outputIndex) def getNodeIdSameSideAs(u: ChannelUpdate): PublicKey = if (u.channelFlags.isNode1) ann.nodeId1 else ann.nodeId2 def getChannelUpdateSameSideAs(u: ChannelUpdate): Option[ChannelUpdate] = if (u.channelFlags.isNode1) update_1_opt else update_2_opt def getBalanceSameSideAs(u: ChannelUpdate): Option[MilliSatoshi] = if (u.channelFlags.isNode1) meta_opt.map(_.balance1) else meta_opt.map(_.balance2) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/Validation.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/Validation.scala index b173ceb2a7..b55356a422 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/Validation.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/Validation.scala @@ -33,7 +33,7 @@ import fr.acinq.eclair.router.Monitoring.Metrics import fr.acinq.eclair.router.Router._ import fr.acinq.eclair.transactions.Scripts import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{Logs, MilliSatoshiLong, NodeParams, RealShortChannelId, ShortChannelId, TxCoordinates, toLongId} +import fr.acinq.eclair.{Logs, MilliSatoshiLong, NodeParams, RealShortChannelId, ShortChannelId, TxCoordinates} object Validation { @@ -113,7 +113,7 @@ object Validation { log.debug("validation successful for shortChannelId={}", c.shortChannelId) remoteOrigins.foreach(o => sendDecision(o.peerConnection, GossipDecision.Accepted(c))) val capacity = tx.txOut(outputIndex).amount - Some(addPublicChannel(d0, nodeParams, watcher, c, fundingTxid = tx.txid, capacity = capacity)) + Some(addPublicChannel(d0, nodeParams, watcher, c, tx.txid, capacity, None)) } case ValidateResult(c, Right((tx, fundingTxStatus: UtxoStatus.Spent))) => if (fundingTxStatus.spendingTxConfirmed) { @@ -156,15 +156,12 @@ object Validation { } } - private def addPublicChannel(d: Data, nodeParams: NodeParams, watcher: typed.ActorRef[ZmqWatcher.Command], ann: ChannelAnnouncement, fundingTxid: ByteVector32, capacity: Satoshi)(implicit ctx: ActorContext, log: DiagnosticLoggingAdapter): Data = { + private def addPublicChannel(d: Data, nodeParams: NodeParams, watcher: typed.ActorRef[ZmqWatcher.Command], ann: ChannelAnnouncement, fundingTxid: ByteVector32, capacity: Satoshi, privChan_opt: Option[PrivateChannel])(implicit ctx: ActorContext, log: DiagnosticLoggingAdapter): Data = { implicit val sender: ActorRef = ctx.self // necessary to preserve origin when sending messages to other actors val fundingOutputIndex = outputIndex(ann.shortChannelId) - val channelId = toLongId(fundingTxid.reverse, fundingOutputIndex) watcher ! WatchExternalChannelSpent(ctx.self, fundingTxid, fundingOutputIndex, ann.shortChannelId) ctx.system.eventStream.publish(ChannelsDiscovered(SingleChannelDiscovered(ann, capacity, None, None) :: Nil)) nodeParams.db.network.addChannel(ann, fundingTxid, capacity) - // if this is a local channel graduating from private to public, we already have data - val privChan_opt = d.privateChannels.get(channelId) val pubChan = PublicChannel( ann = ann, fundingTxid = fundingTxid, @@ -173,7 +170,7 @@ object Validation { update_2_opt = privChan_opt.flatMap(_.update_2_opt), meta_opt = privChan_opt.map(_.meta) ) - log.debug("adding public channel channelId={} realScid={} localChannel={} publicChannel={}", channelId, ann.shortChannelId, privChan_opt.isDefined, pubChan) + log.debug("adding public channel realScid={} localChannel={} publicChannel={}", ann.shortChannelId, privChan_opt.isDefined, pubChan) // if this is a local channel graduating from private to public, we need to update the graph because the edge // identifiers change from alias to real scid, and we can also populate the metadata val graph1 = privChan_opt match { @@ -198,7 +195,7 @@ object Validation { val d1 = d.copy( channels = d.channels + (pubChan.shortChannelId -> pubChan), // we remove the corresponding unannounced channel that we may have until now - privateChannels = d.privateChannels - pubChan.channelId, + privateChannels = d.privateChannels -- privChan_opt.map(_.channelId).toSeq, // we also remove the scid -> channelId mappings scid2PrivateChannels = d.scid2PrivateChannels - pubChan.shortChannelId.toLong -- privChan_opt.map(_.shortIds.localAlias.toLong), // we also add the newly validated channels to the rebroadcast queue @@ -504,7 +501,7 @@ object Validation { case commitments: Commitments => commitments.commitInput.outPoint.txid case _ => ByteVector32.Zeroes } - val d1 = addPublicChannel(d, nodeParams, watcher, ann, fundingTxId, lcu.commitments.capacity) + val d1 = addPublicChannel(d, nodeParams, watcher, ann, fundingTxId, lcu.commitments.capacity, Some(privateChannel)) // maybe the local channel was pruned (can happen if we were disconnected for more than 2 weeks) db.removeFromPruned(ann.shortChannelId) log.debug("processing channel_update") From 26741fabca6550b6becc8bbba563a7bd31f054fb Mon Sep 17 00:00:00 2001 From: Pierre-Marie Padiou Date: Thu, 16 Jun 2022 17:50:16 +0200 Subject: [PATCH 098/121] Reuse postgres instance in tests (#2313) Create new database for each unit test instead of spawning a new db instance. Co-authored-by: Bastien Teinturier <31281497+t-bast@users.noreply.github.com> --- .../scala/fr/acinq/eclair/TestDatabases.scala | 34 ++++++++++++++++--- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/TestDatabases.scala b/eclair-core/src/test/scala/fr/acinq/eclair/TestDatabases.scala index e613e7ab1c..91206d6721 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/TestDatabases.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/TestDatabases.scala @@ -3,8 +3,11 @@ package fr.acinq.eclair import akka.actor.ActorSystem import com.opentable.db.postgres.embedded.EmbeddedPostgres import com.zaxxer.hikari.HikariConfig +import fr.acinq.eclair.TestDatabases.TestPgDatabases.getNewDatabase import fr.acinq.eclair.channel._ +import fr.acinq.eclair.db.Databases.PostgresDatabases import fr.acinq.eclair.db._ +import fr.acinq.eclair.db.jdbc.JdbcUtils import fr.acinq.eclair.db.pg.PgUtils.PgLock.LockFailureHandler import fr.acinq.eclair.db.pg.PgUtils.{PgLock, getVersion, using} import fr.acinq.eclair.db.sqlite.SqliteChannelsDb @@ -95,13 +98,13 @@ object TestDatabases { val dbs = Databases.SqliteDatabases(connection, connection, connection, jdbcUrlFile_opt = Some(jdbcUrlFile)) dbs.copy(channels = new SqliteChannelsDbWithValidation(dbs.channels)) } - override def close(): Unit = () + override def close(): Unit = connection.close() // @formatter:on } case class TestPgDatabases() extends TestDatabases { - private val pg = EmbeddedPostgres.start() - val datasource: DataSource = pg.getPostgresDatabase + + val datasource: DataSource = getNewDatabase() val hikariConfig = new HikariConfig hikariConfig.setDataSource(datasource) val lock: PgLock.LeaseLock = PgLock.LeaseLock(UUID.randomUUID(), 10 minutes, 8 minute, LockFailureHandler.logAndThrow, autoReleaseAtShutdown = false) @@ -112,13 +115,34 @@ object TestDatabases { implicit val system: ActorSystem = ActorSystem() // @formatter:off - override val connection: PgConnection = pg.getPostgresDatabase.getConnection.asInstanceOf[PgConnection] + override val connection: PgConnection = datasource.getConnection.asInstanceOf[PgConnection] // NB: we use a lazy val here: databases won't be initialized until we reference that variable override lazy val db: Databases = Databases.PostgresDatabases(hikariConfig, UUID.randomUUID(), lock, jdbcUrlFile_opt = Some(jdbcUrlFile), readOnlyUser_opt = None, resetJsonColumns = false, safetyChecks_opt = None) - override def close(): Unit = pg.close() + override def close(): Unit = { + db.asInstanceOf[PostgresDatabases].dataSource.close() + connection.close() + system.terminate() + } // @formatter:on } + object TestPgDatabases { + /** single instance */ + private val pg = EmbeddedPostgres.start() + + def getNewDatabase(): DataSource = { + implicit val datasource: DataSource = pg.getPostgresDatabase + val dbName = s"db_${randomBytes(8).toHex}" + JdbcUtils.withConnection { connection => + connection + .createStatement() + .executeUpdate(s"CREATE DATABASE $dbName") + } + pg.getDatabase("postgres", dbName) + } + + } + def forAllDbs(f: TestDatabases => Unit): Unit = { def using(dbs: TestDatabases)(g: TestDatabases => Unit): Unit = try g(dbs) finally dbs.close() // @formatter:off From 6b2e415ecbbd1a602969efb327d9452458129090 Mon Sep 17 00:00:00 2001 From: Goutam Verma <66783850+GoutamVerma@users.noreply.github.com> Date: Thu, 16 Jun 2022 21:22:57 +0530 Subject: [PATCH 099/121] Expose scraping endpoint for prometheus metrics (#2321) Allow eclair to expose a metrics scraping endpoint for Prometheus for users who don't want to rely on Kamon's hosted infrastructure. Co-authored-by: Bastien Teinturier <31281497+t-bast@users.noreply.github.com> --- docs/Monitoring.md | 43 +++++++++++++++++-- eclair-core/pom.xml | 5 +++ .../src/main/resources/application.conf | 9 ++++ 3 files changed, 54 insertions(+), 3 deletions(-) diff --git a/docs/Monitoring.md b/docs/Monitoring.md index b914a3c776..cb56239057 100644 --- a/docs/Monitoring.md +++ b/docs/Monitoring.md @@ -33,6 +33,7 @@ kamon { } ``` + When starting eclair, you should enable the Kanela agent: ```sh @@ -42,13 +43,48 @@ eclair.sh -with-kanela Your eclair node will start exporting monitoring data to Kamon. You can then start creating dashboards, graphs and alerts directly on Kamon's website. -## Enabling monitoring with a different backend +## Enabling monitoring with Prometheus Kamon supports many other monitoring [backends](https://kamon.io/docs/latest/reporters/). This can be useful for nodes that don't want to export any data to third-party servers. -No specific work has been done yet in eclair to support these backends. If you'd like to use them, -don't hesitate to ask around or send a PR. +Eclair currently supports exporting metrics to [Prometheus](https://kamon.io/docs/latest/reporters/prometheus/). +To enable monitoring with Prometheus, add the following to your `eclair.conf`: + +```config +eclair.enable-kamon=true + +// The Kamon APM reporter is enabled by default, but it won't work with Prometheus, so we disable it. +kamon.modules { + apm-reporter { + enabled = false + } +} + +kamon { + prometheus { + start-embedded-http-server = yes + embedded-server { + hostname = 0.0.0.0 + port = + } + } +} +``` + +You should then configure your Prometheus process to scrape metrics from the exposed http server. +* Download Prometheus [here](https://prometheus.io/download/). +* Add the following configuration to the `prometheus.yml` file (see the [Prometheus documentation](https://prometheus.io/docs/prometheus/latest/configuration/configuration/) for more details) + +```prometheus.yml +global: + scrape_interval: 15s # By default, scrape targets every 15 seconds. + +scrape_configs: + - job_name: 'eclair' + static_configs: + - targets: [''] + ``` ## Example metrics @@ -60,3 +96,4 @@ metrics are just a small sample of all the metrics we provide: * Number of connected peers * Bitcoin wallet balance * Various metrics about the public graph (nodes, channels, updates, etc) + diff --git a/eclair-core/pom.xml b/eclair-core/pom.xml index 86185396fa..a511f4a6f2 100644 --- a/eclair-core/pom.xml +++ b/eclair-core/pom.xml @@ -270,6 +270,11 @@ 1.5.0 + + io.kamon + kamon-prometheus_${scala.version.short} + 2.5.4 + io.kamon kamon-core_${scala.version.short} diff --git a/eclair-node/src/main/resources/application.conf b/eclair-node/src/main/resources/application.conf index 930d863bb0..eaa336ffde 100644 --- a/eclair-node/src/main/resources/application.conf +++ b/eclair-node/src/main/resources/application.conf @@ -43,6 +43,15 @@ kamon.instrumentation.akka { } } } +// See documentation here: https://kamon.io/docs/latest/reporters/prometheus/ +kamon.prometheus { + // If you want to expose a scraping endpoint for Prometheus metrics, set this to true. + start-embedded-http-server = no + embedded-server { + hostname = 0.0.0.0 + port = 9095 + } +} kanela.modules { jdbc { From 70de271312d629fd663417bbbe59d05e303b6975 Mon Sep 17 00:00:00 2001 From: Pierre-Marie Padiou Date: Thu, 23 Jun 2022 10:49:13 +0200 Subject: [PATCH 100/121] Make actors signal when they are ready (#2322) Since actors are initialized asynchronously, and the initialization sometimes involves subscribing to an `EventStream`, we don't know when they are ready to process messages, especially in tests, which lead to race conditions. By making actors publish `SubscriptionsComplete ` on the same `EventStream` they are subscribing to, we guarantee that if we receive `SubscriptionsComplete ` the actor has been initialized and its subscriptions have been taken into account. --- .../scala/fr/acinq/eclair/SubscriptionsComplete.scala | 10 ++++++++++ .../main/scala/fr/acinq/eclair/channel/Register.scala | 3 ++- .../main/scala/fr/acinq/eclair/io/Switchboard.scala | 3 ++- .../acinq/eclair/payment/relay/ChannelRelayer.scala | 3 ++- .../main/scala/fr/acinq/eclair/router/Router.scala | 1 + .../basic/fixtures/MinimalNodeFixture.scala | 11 +++++++++-- 6 files changed, 26 insertions(+), 5 deletions(-) create mode 100644 eclair-core/src/main/scala/fr/acinq/eclair/SubscriptionsComplete.scala diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/SubscriptionsComplete.scala b/eclair-core/src/main/scala/fr/acinq/eclair/SubscriptionsComplete.scala new file mode 100644 index 0000000000..23541c5263 --- /dev/null +++ b/eclair-core/src/main/scala/fr/acinq/eclair/SubscriptionsComplete.scala @@ -0,0 +1,10 @@ +package fr.acinq.eclair + +/** + * Since actors are initialized asynchronously, and the initialization sometimes involves subscribing to an + * [[akka.event.EventStream]], we don't know when they are ready to process messages, especially in tests, which + * leads to race conditions. + * By making actors publish [[SubscriptionsComplete]] on the same [[akka.event.EventStream]] they are subscribing to, we guarantee + * that if we receive [[SubscriptionsComplete]] the actor has been initialized and its subscriptions have been taken into account. + */ +case class SubscriptionsComplete(clazz: Class[_]) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Register.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Register.scala index 7622bd13f1..f7d3e6c2cc 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Register.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Register.scala @@ -19,8 +19,8 @@ package fr.acinq.eclair.channel import akka.actor.{Actor, ActorLogging, ActorRef, Terminated} import fr.acinq.bitcoin.scalacompat.ByteVector32 import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey -import fr.acinq.eclair.ShortChannelId import fr.acinq.eclair.channel.Register._ +import fr.acinq.eclair.{SubscriptionsComplete, ShortChannelId} /** * Created by PM on 26/01/2016. @@ -32,6 +32,7 @@ class Register extends Actor with ActorLogging { context.system.eventStream.subscribe(self, classOf[AbstractChannelRestored]) context.system.eventStream.subscribe(self, classOf[ChannelIdAssigned]) context.system.eventStream.subscribe(self, classOf[ShortChannelIdAssigned]) + context.system.eventStream.publish(SubscriptionsComplete(this.getClass)) override def receive: Receive = main(Map.empty, Map.empty, Map.empty) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/Switchboard.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/Switchboard.scala index 640f484177..bfcd514462 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/io/Switchboard.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/Switchboard.scala @@ -21,7 +21,6 @@ import akka.actor.typed.scaladsl.adapter.ClassicActorContextOps import akka.actor.{Actor, ActorContext, ActorLogging, ActorRef, OneForOneStrategy, Props, Status, SupervisorStrategy, typed} import fr.acinq.bitcoin.scalacompat.ByteVector32 import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey -import fr.acinq.eclair.NodeParams import fr.acinq.eclair.blockchain.OnChainAddressGenerator import fr.acinq.eclair.channel.Helpers.Closing import fr.acinq.eclair.channel._ @@ -30,6 +29,7 @@ import fr.acinq.eclair.io.Peer.PeerInfoResponse import fr.acinq.eclair.remote.EclairInternalsSerializer.RemoteTypes import fr.acinq.eclair.router.Router.RouterConf import fr.acinq.eclair.wire.protocol.OnionMessage +import fr.acinq.eclair.{SubscriptionsComplete, NodeParams} /** * Ties network connections to peers. @@ -41,6 +41,7 @@ class Switchboard(nodeParams: NodeParams, peerFactory: Switchboard.PeerFactory) context.system.eventStream.subscribe(self, classOf[ChannelIdAssigned]) context.system.eventStream.subscribe(self, classOf[LastChannelClosed]) + context.system.eventStream.publish(SubscriptionsComplete(this.getClass)) // we load channels from database private def initialPeersWithChannels(): Set[PublicKey] = { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/ChannelRelayer.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/ChannelRelayer.scala index 741f508970..1a9bfc2fc1 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/ChannelRelayer.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/ChannelRelayer.scala @@ -24,7 +24,7 @@ import fr.acinq.bitcoin.scalacompat.ByteVector32 import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.eclair.channel._ import fr.acinq.eclair.payment.IncomingPaymentPacket -import fr.acinq.eclair.{Logs, NodeParams, ShortChannelId} +import fr.acinq.eclair.{SubscriptionsComplete, Logs, NodeParams, ShortChannelId} import java.util.UUID import scala.collection.mutable @@ -65,6 +65,7 @@ object ChannelRelayer { context.system.eventStream ! EventStream.Subscribe(context.messageAdapter[LocalChannelUpdate](WrappedLocalChannelUpdate)) context.system.eventStream ! EventStream.Subscribe(context.messageAdapter[LocalChannelDown](WrappedLocalChannelDown)) context.system.eventStream ! EventStream.Subscribe(context.messageAdapter[AvailableBalanceChanged](WrappedAvailableBalanceChanged)) + context.system.eventStream ! EventStream.Publish(SubscriptionsComplete(this.getClass)) context.messageAdapter[IncomingPaymentPacket.ChannelRelayPacket](Relay) Behaviors.withMdc(Logs.mdc(category_opt = Some(Logs.LogCategory.PAYMENT), nodeAlias_opt = Some(nodeParams.alias)), mdc) { Behaviors.receiveMessage { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala index d40e5c1504..a241536f73 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala @@ -62,6 +62,7 @@ class Router(val nodeParams: NodeParams, watcher: typed.ActorRef[ZmqWatcher.Comm context.system.eventStream.subscribe(self, classOf[LocalChannelUpdate]) context.system.eventStream.subscribe(self, classOf[LocalChannelDown]) context.system.eventStream.subscribe(self, classOf[AvailableBalanceChanged]) + context.system.eventStream.publish(SubscriptionsComplete(this.getClass)) startTimerWithFixedDelay(TickBroadcast.toString, TickBroadcast, nodeParams.routerConf.routerBroadcastInterval) startTimerWithFixedDelay(TickPruneStaleChannels.toString, TickPruneStaleChannels, 1 hour) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/fixtures/MinimalNodeFixture.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/fixtures/MinimalNodeFixture.scala index 491a3ea271..4646f513a2 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/fixtures/MinimalNodeFixture.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/fixtures/MinimalNodeFixture.scala @@ -19,12 +19,12 @@ import fr.acinq.eclair.io.PeerConnection.ConnectionResult import fr.acinq.eclair.io.{Peer, PeerConnection, Switchboard} import fr.acinq.eclair.payment.Bolt11Invoice.ExtraHop import fr.acinq.eclair.payment.receive.{MultiPartHandler, PaymentHandler} -import fr.acinq.eclair.payment.relay.Relayer +import fr.acinq.eclair.payment.relay.{ChannelRelayer, Relayer} import fr.acinq.eclair.payment.send.PaymentInitiator import fr.acinq.eclair.payment.{Bolt11Invoice, PaymentSent} import fr.acinq.eclair.router.Router import fr.acinq.eclair.wire.protocol.IPAddress -import fr.acinq.eclair.{BlockHeight, MilliSatoshi, MilliSatoshiLong, NodeParams, RealShortChannelId, TestBitcoinCoreClient, TestDatabases, TestFeeEstimator} +import fr.acinq.eclair.{BlockHeight, MilliSatoshi, MilliSatoshiLong, NodeParams, RealShortChannelId, SubscriptionsComplete, TestBitcoinCoreClient, TestDatabases, TestFeeEstimator} import org.scalatest.Assertions import org.scalatest.concurrent.Eventually.eventually @@ -71,6 +71,8 @@ object MinimalNodeFixture extends Assertions { def apply(nodeParams: NodeParams): MinimalNodeFixture = { implicit val system: ActorSystem = ActorSystem(s"system-${nodeParams.alias}") + val readyListener = TestProbe("ready-listener") + system.eventStream.subscribe(readyListener.ref, classOf[SubscriptionsComplete]) val bitcoinClient = new TestBitcoinCoreClient() val wallet = new DummyOnChainWallet() val watcher = TestProbe("watcher") @@ -85,6 +87,11 @@ object MinimalNodeFixture extends Assertions { val switchboard = system.actorOf(Switchboard.props(nodeParams, peerFactory), "switchboard") val paymentFactory = PaymentInitiator.SimplePaymentFactory(nodeParams, router, register) val paymentInitiator = system.actorOf(PaymentInitiator.props(nodeParams, paymentFactory), "payment-initiator") + readyListener.expectMsgAllOf( + SubscriptionsComplete(classOf[Router]), + SubscriptionsComplete(classOf[Register]), + SubscriptionsComplete(classOf[Switchboard]), + SubscriptionsComplete(ChannelRelayer.getClass)) MinimalNodeFixture( nodeParams, system, From 0c84063d324403bfdaaafed157325dcdf1e502c9 Mon Sep 17 00:00:00 2001 From: Chris Guida Date: Thu, 23 Jun 2022 07:23:26 -0500 Subject: [PATCH 101/121] add arm64v8 Dockerfile (#2304) Add arm64v8 Dockerfile --- arm64v8.Dockerfile | 65 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 arm64v8.Dockerfile diff --git a/arm64v8.Dockerfile b/arm64v8.Dockerfile new file mode 100644 index 0000000000..c61474481b --- /dev/null +++ b/arm64v8.Dockerfile @@ -0,0 +1,65 @@ +FROM eclipse-temurin:11-jdk-jammy as BUILD + +# Setup maven, we don't use https://hub.docker.com/_/maven/ as it declare .m2 as volume, we loose all mvn cache +# We can alternatively do as proposed by https://github.com/carlossg/docker-maven#packaging-a-local-repository-with-the-image +# this was meant to make the image smaller, but we use multi-stage build so we don't care +RUN apt update && apt install -y curl tar bash + +ARG MAVEN_VERSION=3.6.3 +ARG USER_HOME_DIR="/root" +ARG SHA=c35a1803a6e70a126e80b2b3ae33eed961f83ed74d18fcd16909b2d44d7dada3203f1ffe726c17ef8dcca2dcaa9fca676987befeadc9b9f759967a8cb77181c0 +ARG BASE_URL=https://apache.osuosl.org/maven/maven-3/${MAVEN_VERSION}/binaries + +RUN mkdir -p /usr/share/maven /usr/share/maven/ref \ + && curl -fsSL -o /tmp/apache-maven.tar.gz ${BASE_URL}/apache-maven-${MAVEN_VERSION}-bin.tar.gz \ + && echo "${SHA} /tmp/apache-maven.tar.gz" | sha512sum -c - \ + && tar -xzf /tmp/apache-maven.tar.gz -C /usr/share/maven --strip-components=1 \ + && rm -f /tmp/apache-maven.tar.gz \ + && ln -s /usr/share/maven/bin/mvn /usr/bin/mvn + +ENV MAVEN_HOME /usr/share/maven +ENV MAVEN_CONFIG "$USER_HOME_DIR/.m2" + +# Let's fetch eclair dependencies, so that Docker can cache them +# This way we won't have to fetch dependencies again if only the source code changes +# The easiest way to reliably get dependencies is to build the project with no sources +WORKDIR /usr/src +COPY pom.xml pom.xml +COPY eclair-core/pom.xml eclair-core/pom.xml +COPY eclair-front/pom.xml eclair-front/pom.xml +COPY eclair-node/pom.xml eclair-node/pom.xml +COPY eclair-node/modules/assembly.xml eclair-node/modules/assembly.xml +RUN mkdir -p eclair-core/src/main/scala && touch eclair-core/src/main/scala/empty.scala +# Blank build. We only care about eclair-node, and we use install because eclair-node depends on eclair-core +RUN mvn install -pl eclair-node -am +RUN mvn clean + +# Only then do we copy the sources +COPY . . + +# And this time we can build in offline mode, specifying 'notag' instead of git commit +RUN mvn package -pl eclair-node -am -DskipTests -Dgit.commit.id=notag -Dgit.commit.id.abbrev=notag -o +# It might be good idea to run the tests here, so that the docker build fail if the code is bugged + +# We currently use a debian image for runtime because of some jni-related issue with sqlite +FROM openjdk:11.0.4-jre-slim +WORKDIR /app + +# install jq for eclair-cli +RUN apt-get update && apt-get install -y bash jq curl unzip + +# copy and install eclair-cli executable +COPY --from=BUILD /usr/src/eclair-core/eclair-cli . +RUN chmod +x eclair-cli && mv eclair-cli /sbin/eclair-cli + +# we only need the eclair-node.zip to run +COPY --from=BUILD /usr/src/eclair-node/target/eclair-node-*.zip ./eclair-node.zip +RUN unzip eclair-node.zip && mv eclair-node-* eclair-node && chmod +x eclair-node/bin/eclair-node.sh + +ENV ECLAIR_DATADIR=/data +ENV JAVA_OPTS= + +RUN mkdir -p "$ECLAIR_DATADIR" +VOLUME [ "/data" ] + +ENTRYPOINT JAVA_OPTS="${JAVA_OPTS}" eclair-node/bin/eclair-node.sh "-Declair.datadir=${ECLAIR_DATADIR}" From af79f44051a08ecc31eab5a129d41e6c3100776a Mon Sep 17 00:00:00 2001 From: Fabrice Drouin Date: Thu, 23 Jun 2022 16:11:21 +0200 Subject: [PATCH 102/121] Move arm64 docker file to contrib and udpate README.md (#2327) --- README.md | 3 ++- arm64v8.Dockerfile => contrib/arm64v8.Dockerfile | 0 2 files changed, 2 insertions(+), 1 deletion(-) rename arm64v8.Dockerfile => contrib/arm64v8.Dockerfile (100%) diff --git a/README.md b/README.md index 819509001c..6d5d8cd891 100644 --- a/README.md +++ b/README.md @@ -206,7 +206,8 @@ before copying that file to your final backup location. ## Docker -A [Dockerfile](Dockerfile) image is built on each commit on [docker hub](https://hub.docker.com/r/acinq/eclair) for running a dockerized eclair-node. +A [Dockerfile](Dockerfile) x86_64 image is built on each commit on [docker hub](https://hub.docker.com/r/acinq/eclair) for running a dockerized eclair-node. +For arm64 platforms you can use an [arm64 Dockerfile](contrib/arm64v8.Dockerfile) to build your own arm64 container. You can use the `JAVA_OPTS` environment variable to set arguments to `eclair-node`. diff --git a/arm64v8.Dockerfile b/contrib/arm64v8.Dockerfile similarity index 100% rename from arm64v8.Dockerfile rename to contrib/arm64v8.Dockerfile From 08d2ad4a917aa31719ceb371dcf476a8adf650d7 Mon Sep 17 00:00:00 2001 From: Pierre-Marie Padiou Date: Wed, 29 Jun 2022 13:22:15 +0200 Subject: [PATCH 103/121] Random values for `generateLocalAlias()` (#2337) We generate alias randomly, and use a value space large enough to not have to worry about duplicates. --- .../fr/acinq/eclair/ShortChannelId.scala | 20 ++++++++++++++++++- .../fr/acinq/eclair/channel/Register.scala | 6 ++++++ .../fr/acinq/eclair/ShortChannelIdSpec.scala | 7 +++++++ 3 files changed, 32 insertions(+), 1 deletion(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/ShortChannelId.scala b/eclair-core/src/main/scala/fr/acinq/eclair/ShortChannelId.scala index 8a4e18490b..3e33ff2118 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/ShortChannelId.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/ShortChannelId.scala @@ -58,7 +58,25 @@ object ShortChannelId { def toShortId(blockHeight: Int, txIndex: Int, outputIndex: Int): Long = ((blockHeight & 0xFFFFFFL) << 40) | ((txIndex & 0xFFFFFFL) << 16) | (outputIndex & 0xFFFFL) - def generateLocalAlias(): Alias = Alias(System.nanoTime()) // TODO: fixme (duplicate, etc.) + /** + * At block height 350 000 LN didn't exist, so all real scids less than that will never be used. This results in + * more than `2^58` values. + * + * The expected number of values before we have a collision can be approximated by (*): + * `Q(H) ~= sqrt( Pi * H / 2) = sqrt(Pi * 2^57) = 673 000 000` + * + * We don't expect to have anywhere close to that many channels at any given time on our node, so we don't need to + * check for duplicate values. + * + * (*) https://en.wikipedia.org/wiki/Birthday_attack + */ + private val aliasUpperBound = 2^58 + + def generateLocalAlias(): Alias = { + // modulo won't skew the distribution because 2^64 is a multiple of 2^58 + val l = Math.abs(randomLong() % aliasUpperBound) + Alias(l) + } @inline def blockHeight(shortChannelId: ShortChannelId): BlockHeight = BlockHeight((shortChannelId.toLong >> 40) & 0xFFFFFF) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Register.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Register.scala index f7d3e6c2cc..df053325c1 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Register.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Register.scala @@ -52,6 +52,12 @@ class Register extends Actor with ActorLogging { // We map all known scids (real or alias) to the channel_id. The relayer is in charge of deciding whether a real // scid can be used or not for routing (see option_scid_alias), but the register is neutral. val m = (scidAssigned.shortIds.real.toOption.toSeq :+ scidAssigned.shortIds.localAlias).map(_ -> scidAssigned.channelId).toMap + // duplicate check for aliases (we use a random value in a large enough space that there should never be collisions) + shortIds.get(scidAssigned.shortIds.localAlias) match { + case Some(channelId) if channelId != scidAssigned.channelId => + log.error("duplicate alias={} for channelIds={},{} this should never happen!", scidAssigned.shortIds.localAlias, channelId, scidAssigned.channelId) + case _ => () + } context become main(channels, shortIds ++ m, channelsTo) case Terminated(actor) if channels.values.toSet.contains(actor) => diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/ShortChannelIdSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/ShortChannelIdSpec.scala index fc472f2054..094f4dfa4b 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/ShortChannelIdSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/ShortChannelIdSpec.scala @@ -70,4 +70,11 @@ class ShortChannelIdSpec extends AnyFunSuite { Seq(-561L, 0xffffffffffffffffL, 0x2affffffffffffffL).foreach(id => assert(Alias(id) == UnspecifiedShortChannelId(id))) } + test("basic check on random alias generation") { + for(_ <- 0 until 100) { + val alias = ShortChannelId.generateLocalAlias() + assert(alias.toLong >= 0 && alias.toLong <= 384_829_069_721_665_536L) + } + } + } From c9810d54246648c6177dfb92559e7ff6c9676ad6 Mon Sep 17 00:00:00 2001 From: Pierre-Marie Padiou Date: Wed, 29 Jun 2022 15:12:04 +0200 Subject: [PATCH 104/121] Resume reading after processing unknown messages (#2332) If the current chunk of data read from the TCP connection only contains unknown messages (in particular, could be only one isolated unknown message on an otherwise idle connection), we never resumed reading on the connection. This means all subsequent messages, including pings/pongs, won't be read, which is why the most visible effect is disconnecting due to no response to ping. Related to https://github.com/ElementsProject/lightning/pull/5347. Reported by @wtogami. --- .../eclair/crypto/TransportHandler.scala | 36 ++++---- .../eclair/crypto/TransportHandlerSpec.scala | 88 ++++++++++++++++++- 2 files changed, 101 insertions(+), 23 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/TransportHandler.scala b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/TransportHandler.scala index ac6003d0aa..bb536ada3a 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/TransportHandler.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/TransportHandler.scala @@ -94,7 +94,8 @@ class TransportHandler[T: ClassTag](keyPair: KeyPair, rs: Option[ByteVector], co makeReader(keyPair) } - def sendToListener(listener: ActorRef, plaintextMessages: Seq[ByteVector]): Map[T, Int] = { + def decodeAndSendToListener(listener: ActorRef, plaintextMessages: Seq[ByteVector]): Map[T, Int] = { + log.debug("decoding {} plaintext messages", plaintextMessages.size) var m: Map[T, Int] = Map() plaintextMessages.foreach(plaintext => Try(codec.decode(plaintext.toBitVector)) match { case Success(Attempt.Successful(DecodeResult(message, _))) => @@ -106,6 +107,7 @@ class TransportHandler[T: ClassTag](keyPair: KeyPair, rs: Option[ByteVector], co case Failure(t) => log.error(s"cannot deserialize $plaintext: ${t.getMessage}") }) + log.debug("decoded {} messages", m.values.sum) m } @@ -164,31 +166,28 @@ class TransportHandler[T: ClassTag](keyPair: KeyPair, rs: Option[ByteVector], co case Event(Listener(listener), d@WaitingForListenerData(_, dec)) => context.watch(listener) val (dec1, plaintextMessages) = dec.decrypt() - if (plaintextMessages.isEmpty) { + val unackedReceived1 = decodeAndSendToListener(listener, plaintextMessages) + if (unackedReceived1.isEmpty) { + log.debug("no decoded messages, resuming reading") connection ! Tcp.ResumeReading - goto(Normal) using NormalData(d.encryptor, dec1, listener, sendBuffer = SendBuffer(Queue.empty[T], Queue.empty[T]), unackedReceived = Map.empty[T, Int], unackedSent = None) - } else { - log.debug(s"read ${plaintextMessages.size} messages, waiting for readacks") - val unackedReceived = sendToListener(listener, plaintextMessages) - goto(Normal) using NormalData(d.encryptor, dec1, listener, sendBuffer = SendBuffer(Queue.empty[T], Queue.empty[T]), unackedReceived, unackedSent = None) } + goto(Normal) using NormalData(d.encryptor, dec1, listener, sendBuffer = SendBuffer(Queue.empty[T], Queue.empty[T]), unackedReceived = unackedReceived1, unackedSent = None) } } when(Normal) { handleExceptions { - case Event(Tcp.Received(data), d: NormalData[T @unchecked]) => + case Event(Tcp.Received(data), d: NormalData[T@unchecked]) => + log.debug("received chunk of size={}", data.size) val (dec1, plaintextMessages) = d.decryptor.copy(buffer = d.decryptor.buffer ++ data).decrypt() - if (plaintextMessages.isEmpty) { + val unackedReceived1 = decodeAndSendToListener(d.listener, plaintextMessages) + if (unackedReceived1.isEmpty) { + log.debug("no decoded messages, resuming reading") connection ! Tcp.ResumeReading - stay() using d.copy(decryptor = dec1) - } else { - log.debug("read {} messages, waiting for readacks", plaintextMessages.size) - val unackedReceived = sendToListener(d.listener, plaintextMessages) - stay() using NormalData(d.encryptor, dec1, d.listener, d.sendBuffer, unackedReceived, d.unackedSent) } + stay() using d.copy(decryptor = dec1, unackedReceived = unackedReceived1) - case Event(ReadAck(msg: T), d: NormalData[T @unchecked]) => + case Event(ReadAck(msg: T), d: NormalData[T@unchecked]) => // how many occurences of this message are still unacked? val remaining = d.unackedReceived.getOrElse(msg, 0) - 1 log.debug("acking message {}", msg) @@ -197,13 +196,12 @@ class TransportHandler[T: ClassTag](keyPair: KeyPair, rs: Option[ByteVector], co if (unackedReceived1.isEmpty) { log.debug("last incoming message was acked, resuming reading") connection ! Tcp.ResumeReading - stay() using d.copy(unackedReceived = unackedReceived1) } else { log.debug("still waiting for readacks, unacked={}", unackedReceived1) - stay() using d.copy(unackedReceived = unackedReceived1) } + stay() using d.copy(unackedReceived = unackedReceived1) - case Event(t: T, d: NormalData[T @unchecked]) => + case Event(t: T, d: NormalData[T@unchecked]) => if (d.sendBuffer.normalPriority.size + d.sendBuffer.lowPriority.size >= MAX_BUFFERED) { log.warning("send buffer overrun, closing connection") connection ! PoisonPill @@ -224,7 +222,7 @@ class TransportHandler[T: ClassTag](keyPair: KeyPair, rs: Option[ByteVector], co stay() using d.copy(encryptor = enc1, unackedSent = Some(t)) } - case Event(WriteAck, d: NormalData[T @unchecked]) => + case Event(WriteAck, d: NormalData[T@unchecked]) => def send(t: T) = { diag(t, "OUT") val blob = codec.encode(t).require.toByteVector diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/TransportHandlerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/TransportHandlerSpec.scala index bc7d35797c..0f0b2a89e2 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/TransportHandlerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/TransportHandlerSpec.scala @@ -16,8 +16,6 @@ package fr.acinq.eclair.crypto -import java.nio.charset.Charset - import akka.actor.{Actor, ActorLogging, ActorRef, OneForOneStrategy, Props, Stash, SupervisorStrategy, Terminated} import akka.io.Tcp import akka.testkit.{TestActorRef, TestFSMRef, TestProbe} @@ -31,6 +29,7 @@ import scodec.Codec import scodec.bits._ import scodec.codecs._ +import java.nio.charset.Charset import scala.annotation.tailrec import scala.concurrent.duration._ @@ -46,7 +45,7 @@ class TransportHandlerSpec extends TestKitBaseClass with AnyFunSuiteLike with Be val s = Noise.Secp256k1DHFunctions.generateKeyPair(hex"2121212121212121212121212121212121212121212121212121212121212121") } - test("succesfull handshake") { + test("successful handshake") { val pipe = system.actorOf(Props[MyPipe]()) val probe1 = TestProbe() val probe2 = TestProbe() @@ -76,7 +75,7 @@ class TransportHandlerSpec extends TestKitBaseClass with AnyFunSuiteLike with Be probe1.expectTerminated(pipe) } - test("succesfull handshake with custom serializer") { + test("successful handshake with custom serializer") { case class MyMessage(payload: String) val mycodec: Codec[MyMessage] = ("payload" | scodec.codecs.string32L(Charset.defaultCharset())).as[MyMessage] val pipe = system.actorOf(Props[MyPipe]()) @@ -108,6 +107,52 @@ class TransportHandlerSpec extends TestKitBaseClass with AnyFunSuiteLike with Be probe1.expectTerminated(pipe) } + test("handle unknown messages") { + sealed trait Message + case object Msg1 extends Message + case object Msg2 extends Message + + val codec1: Codec[Message] = discriminated[Message].by(uint8) + .typecase(1, provide(Msg1)) + + val codec12: Codec[Message] = discriminated[Message].by(uint8) + .typecase(1, provide(Msg1)) + .typecase(2, provide(Msg2)) + + val pipe = system.actorOf(Props[MyPipePull]()) + val probe1 = TestProbe() + val probe2 = TestProbe() + val initiator = TestFSMRef(new TransportHandler(Initiator.s, Some(Responder.s.pub), pipe, codec1)) + val responder = TestFSMRef(new TransportHandler(Responder.s, None, pipe, codec12)) + pipe ! (initiator, responder) + + awaitCond(initiator.stateName == TransportHandler.WaitingForListener) + awaitCond(responder.stateName == TransportHandler.WaitingForListener) + + initiator ! Listener(probe1.ref) + responder ! Listener(probe2.ref) + + awaitCond(initiator.stateName == TransportHandler.Normal) + awaitCond(responder.stateName == TransportHandler.Normal) + + responder ! Msg1 + probe1.expectMsg(Msg1) + probe1.reply(TransportHandler.ReadAck(Msg1)) + + responder ! Msg2 + probe1.expectNoMessage(2 seconds) // unknown message + + responder ! Msg1 + probe1.expectMsg(Msg1) + probe1.reply(TransportHandler.ReadAck(Msg1)) + + probe1.watch(pipe) + initiator.stop() + responder.stop() + system.stop(pipe) + probe1.expectTerminated(pipe) + } + test("handle messages split in chunks") { val pipe = system.actorOf(Props[MyPipeSplitter]()) val probe1 = TestProbe() @@ -250,6 +295,41 @@ object TransportHandlerSpec { } } + class MyPipePull extends Actor with Stash { + + def receive = { + case (a: ActorRef, b: ActorRef) => + unstashAll() + context watch a + context watch b + context become ready(a, b, aResume = true, bResume = true) + + case msg => stash() + } + + def ready(a: ActorRef, b: ActorRef, aResume: Boolean, bResume: Boolean): Receive = { + case Tcp.Write(data, ack) if sender().path == a.path => + if (bResume) { + b forward Tcp.Received(data) + if (ack != Tcp.NoAck) sender() ! ack + context become ready(a, b, aResume, bResume = false) + } else stash() + case Tcp.ResumeReading if sender().path == b.path => + unstashAll() + context become ready(a, b, aResume, bResume = true) + case Tcp.Write(data, ack) if sender().path == b.path => + if (aResume) { + a forward Tcp.Received(data) + if (ack != Tcp.NoAck) sender() ! ack + context become ready(a, b, aResume = false, bResume) + } else stash() + case Tcp.ResumeReading if sender().path == a.path => + unstashAll() + context become ready(a, b, aResume = true, bResume) + case Terminated(actor) if actor == a || actor == b => context stop self + } + } + // custom supervisor that will stop an actor if it fails class MySupervisor extends Actor { override def supervisorStrategy: SupervisorStrategy = OneForOneStrategy() { From e880b62c13dbc752e1758386c0ed39aa005728c6 Mon Sep 17 00:00:00 2001 From: Thomas HUET <81159533+thomash-acinq@users.noreply.github.com> Date: Wed, 29 Jun 2022 16:08:37 +0200 Subject: [PATCH 105/121] Refactor routing hints to prepare for payments to blinded routes (#2315) Bolt11 uses routing hints to add extra edges for the path-finding, blinded routes are also extra edges added to the path-finding but they have a different format. The new type `ExtraEdge` is more flexible and can be expanded to cover new kinds of extra edges such as blinded routes. --- .../main/scala/fr/acinq/eclair/Eclair.scala | 14 ++--- .../acinq/eclair/payment/Bolt11Invoice.scala | 17 ++++-- .../acinq/eclair/payment/Bolt12Invoice.scala | 3 +- .../fr/acinq/eclair/payment/Invoice.scala | 29 ++++++++- .../acinq/eclair/payment/PaymentEvents.scala | 21 ++++--- .../acinq/eclair/payment/PaymentPacket.scala | 2 +- .../eclair/payment/relay/NodeRelay.scala | 6 +- .../send/MultiPartPaymentLifecycle.scala | 21 ++++--- .../payment/send/PaymentInitiator.scala | 21 ++++--- .../payment/send/PaymentLifecycle.scala | 59 +++++++++---------- .../scala/fr/acinq/eclair/router/Graph.scala | 23 +++++--- .../eclair/router/RouteCalculation.scala | 40 ++----------- .../scala/fr/acinq/eclair/router/Router.scala | 23 +++----- .../eclair/wire/protocol/PaymentOnion.scala | 3 +- .../fr/acinq/eclair/EclairImplSpec.scala | 15 +++-- .../basic/fixtures/MinimalNodeFixture.scala | 2 +- .../eclair/payment/MultiPartHandlerSpec.scala | 4 +- .../MultiPartPaymentLifecycleSpec.scala | 41 ++++++------- .../eclair/payment/PaymentLifecycleSpec.scala | 36 +++++------ .../payment/relay/NodeRelayerSpec.scala | 29 ++++++--- .../eclair/router/BalanceEstimateSpec.scala | 4 +- .../eclair/router/RouteCalculationSpec.scala | 22 ------- .../fr/acinq/eclair/router/RouterSpec.scala | 16 ++--- .../eclair/api/handlers/PathFinding.scala | 7 +-- 24 files changed, 232 insertions(+), 226 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala index d162fe7890..492c6d1b0d 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala @@ -118,9 +118,9 @@ trait Eclair { def sendOnChain(address: String, amount: Satoshi, confirmationTarget: Long): Future[ByteVector32] - def findRoute(targetNodeId: PublicKey, amount: MilliSatoshi, pathFindingExperimentName_opt: Option[String], assistedRoutes: Seq[Seq[Bolt11Invoice.ExtraHop]] = Seq.empty, includeLocalChannelCost: Boolean = false, ignoreNodeIds: Seq[PublicKey] = Seq.empty, ignoreShortChannelIds: Seq[ShortChannelId] = Seq.empty, maxFee_opt: Option[MilliSatoshi] = None)(implicit timeout: Timeout): Future[RouteResponse] + def findRoute(targetNodeId: PublicKey, amount: MilliSatoshi, pathFindingExperimentName_opt: Option[String], extraEdges: Seq[Invoice.ExtraEdge] = Seq.empty, includeLocalChannelCost: Boolean = false, ignoreNodeIds: Seq[PublicKey] = Seq.empty, ignoreShortChannelIds: Seq[ShortChannelId] = Seq.empty, maxFee_opt: Option[MilliSatoshi] = None)(implicit timeout: Timeout): Future[RouteResponse] - def findRouteBetween(sourceNodeId: PublicKey, targetNodeId: PublicKey, amount: MilliSatoshi, pathFindingExperimentName_opt: Option[String], assistedRoutes: Seq[Seq[Bolt11Invoice.ExtraHop]] = Seq.empty, includeLocalChannelCost: Boolean = false, ignoreNodeIds: Seq[PublicKey] = Seq.empty, ignoreShortChannelIds: Seq[ShortChannelId] = Seq.empty, maxFee_opt: Option[MilliSatoshi] = None)(implicit timeout: Timeout): Future[RouteResponse] + def findRouteBetween(sourceNodeId: PublicKey, targetNodeId: PublicKey, amount: MilliSatoshi, pathFindingExperimentName_opt: Option[String], extraEdges: Seq[Invoice.ExtraEdge] = Seq.empty, includeLocalChannelCost: Boolean = false, ignoreNodeIds: Seq[PublicKey] = Seq.empty, ignoreShortChannelIds: Seq[ShortChannelId] = Seq.empty, maxFee_opt: Option[MilliSatoshi] = None)(implicit timeout: Timeout): Future[RouteResponse] def sendToRoute(amount: MilliSatoshi, recipientAmount_opt: Option[MilliSatoshi], externalId_opt: Option[String], parentId_opt: Option[UUID], invoice: Bolt11Invoice, route: PredefinedRoute, trampolineSecret_opt: Option[ByteVector32] = None, trampolineFees_opt: Option[MilliSatoshi] = None, trampolineExpiryDelta_opt: Option[CltvExpiryDelta] = None, trampolineNodes_opt: Seq[PublicKey] = Nil)(implicit timeout: Timeout): Future[SendPaymentToRouteResponse] @@ -288,8 +288,8 @@ class EclairImpl(appKit: Kit) extends Eclair with Logging { } } - override def findRoute(targetNodeId: PublicKey, amount: MilliSatoshi, pathFindingExperimentName_opt: Option[String], assistedRoutes: Seq[Seq[Bolt11Invoice.ExtraHop]] = Seq.empty, includeLocalChannelCost: Boolean = false, ignoreNodeIds: Seq[PublicKey] = Seq.empty, ignoreShortChannelIds: Seq[ShortChannelId] = Seq.empty, maxFee_opt: Option[MilliSatoshi] = None)(implicit timeout: Timeout): Future[RouteResponse] = - findRouteBetween(appKit.nodeParams.nodeId, targetNodeId, amount, pathFindingExperimentName_opt, assistedRoutes, includeLocalChannelCost, ignoreNodeIds, ignoreShortChannelIds, maxFee_opt) + override def findRoute(targetNodeId: PublicKey, amount: MilliSatoshi, pathFindingExperimentName_opt: Option[String], extraEdges: Seq[Invoice.ExtraEdge] = Seq.empty, includeLocalChannelCost: Boolean = false, ignoreNodeIds: Seq[PublicKey] = Seq.empty, ignoreShortChannelIds: Seq[ShortChannelId] = Seq.empty, maxFee_opt: Option[MilliSatoshi] = None)(implicit timeout: Timeout): Future[RouteResponse] = + findRouteBetween(appKit.nodeParams.nodeId, targetNodeId, amount, pathFindingExperimentName_opt, extraEdges, includeLocalChannelCost, ignoreNodeIds, ignoreShortChannelIds, maxFee_opt) private def getRouteParams(pathFindingExperimentName_opt: Option[String]): Either[IllegalArgumentException, RouteParams] = { pathFindingExperimentName_opt match { @@ -301,14 +301,14 @@ class EclairImpl(appKit: Kit) extends Eclair with Logging { } } - override def findRouteBetween(sourceNodeId: PublicKey, targetNodeId: PublicKey, amount: MilliSatoshi, pathFindingExperimentName_opt: Option[String], assistedRoutes: Seq[Seq[Bolt11Invoice.ExtraHop]] = Seq.empty, includeLocalChannelCost: Boolean = false, ignoreNodeIds: Seq[PublicKey] = Seq.empty, ignoreShortChannelIds: Seq[ShortChannelId] = Seq.empty, maxFee_opt: Option[MilliSatoshi] = None)(implicit timeout: Timeout): Future[RouteResponse] = { + override def findRouteBetween(sourceNodeId: PublicKey, targetNodeId: PublicKey, amount: MilliSatoshi, pathFindingExperimentName_opt: Option[String], extraEdges: Seq[Invoice.ExtraEdge] = Seq.empty, includeLocalChannelCost: Boolean = false, ignoreNodeIds: Seq[PublicKey] = Seq.empty, ignoreShortChannelIds: Seq[ShortChannelId] = Seq.empty, maxFee_opt: Option[MilliSatoshi] = None)(implicit timeout: Timeout): Future[RouteResponse] = { getRouteParams(pathFindingExperimentName_opt) match { case Right(routeParams) => val maxFee = maxFee_opt.getOrElse(routeParams.getMaxFee(amount)) for { ignoredChannels <- getChannelDescs(ignoreShortChannelIds.toSet) ignore = Ignore(ignoreNodeIds.toSet, ignoredChannels) - response <- (appKit.router ? RouteRequest(sourceNodeId, targetNodeId, amount, maxFee, assistedRoutes, ignore = ignore, routeParams = routeParams.copy(includeLocalChannelCost = includeLocalChannelCost))).mapTo[RouteResponse] + response <- (appKit.router ? RouteRequest(sourceNodeId, targetNodeId, amount, maxFee, extraEdges, ignore = ignore, routeParams = routeParams.copy(includeLocalChannelCost = includeLocalChannelCost))).mapTo[RouteResponse] } yield response case Left(t) => Future.failed(t) } @@ -342,7 +342,7 @@ class EclairImpl(appKit: Kit) extends Eclair with Logging { externalId_opt match { case Some(externalId) if externalId.length > externalIdMaxLength => Left(new IllegalArgumentException(s"externalId is too long: cannot exceed $externalIdMaxLength characters")) case _ if invoice.isExpired() => Left(new IllegalArgumentException("invoice has expired")) - case _ => Right(SendPaymentToNode(amount, invoice, maxAttempts, externalId_opt, assistedRoutes = invoice.routingInfo, routeParams = routeParams)) + case _ => Right(SendPaymentToNode(amount, invoice, maxAttempts, externalId_opt, extraEdges = invoice.extraEdges, routeParams = routeParams)) } case Left(t) => Left(t) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Bolt11Invoice.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Bolt11Invoice.scala index 153c2b72d7..558715e006 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Bolt11Invoice.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Bolt11Invoice.scala @@ -19,7 +19,6 @@ package fr.acinq.eclair.payment import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, ByteVector64, Crypto} import fr.acinq.bitcoin.{Base58, Base58Check, Bech32} -import fr.acinq.eclair.payment.relay.Relayer import fr.acinq.eclair.{CltvExpiryDelta, Feature, FeatureSupport, Features, InvoiceFeature, MilliSatoshi, MilliSatoshiLong, ShortChannelId, TimestampSecond, randomBytes32} import scodec.bits.{BitVector, ByteOrdering, ByteVector} import scodec.codecs.{list, ubyte} @@ -86,6 +85,8 @@ case class Bolt11Invoice(prefix: String, amount_opt: Option[MilliSatoshi], creat lazy val routingInfo: Seq[Seq[ExtraHop]] = tags.collect { case t: RoutingInfo => t.path } + lazy val extraEdges: Seq[Invoice.ExtraEdge] = routingInfo.flatMap(path => toExtraEdges(path, nodeId)) + lazy val relativeExpiry: FiniteDuration = FiniteDuration(tags.collectFirst { case expiry: Bolt11Invoice.Expiry => expiry.toLong }.getOrElse(DEFAULT_EXPIRY_SECONDS), TimeUnit.SECONDS) lazy val minFinalCltvExpiryDelta: CltvExpiryDelta = tags.collectFirst { case cltvExpiry: Bolt11Invoice.MinFinalCltvExpiry => cltvExpiry.toCltvExpiryDelta }.getOrElse(DEFAULT_MIN_CLTV_EXPIRY_DELTA) @@ -333,10 +334,7 @@ object Bolt11Invoice { * @param feeProportionalMillionths node proportional fee * @param cltvExpiryDelta node cltv expiry delta */ - case class ExtraHop(nodeId: PublicKey, shortChannelId: ShortChannelId, feeBase: MilliSatoshi, feeProportionalMillionths: Long, cltvExpiryDelta: CltvExpiryDelta) { - def relayFees: Relayer.RelayFees = Relayer.RelayFees(feeBase = feeBase, feeProportionalMillionths = feeProportionalMillionths) - } - + case class ExtraHop(nodeId: PublicKey, shortChannelId: ShortChannelId, feeBase: MilliSatoshi, feeProportionalMillionths: Long, cltvExpiryDelta: CltvExpiryDelta) /** * Routing Info @@ -588,4 +586,13 @@ object Bolt11Invoice { case None => timestamp + DEFAULT_EXPIRY_SECONDS <= TimestampSecond.now() } } + + def toExtraEdges(extraRoute: Seq[ExtraHop], targetNodeId: PublicKey): Seq[Invoice.ExtraEdge] = { + // BOLT 11: "For each entry, the pubkey is the node ID of the start of the channel", and the last node is the destination + val nextNodeIds = extraRoute.map(_.nodeId).drop(1) :+ targetNodeId + extraRoute.zip(nextNodeIds).map { + case (extraHop, nextNodeId) => + Invoice.BasicEdge(extraHop.nodeId, nextNodeId, extraHop.shortChannelId, extraHop.feeBase, extraHop.feeProportionalMillionths, extraHop.cltvExpiryDelta) + } + } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Bolt12Invoice.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Bolt12Invoice.scala index 5d62cf3451..0cd19d3434 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Bolt12Invoice.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Bolt12Invoice.scala @@ -20,7 +20,6 @@ import fr.acinq.bitcoin.Bech32 import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, ByteVector64, Crypto} import fr.acinq.eclair.crypto.Sphinx.RouteBlinding -import fr.acinq.eclair.payment.Bolt11Invoice.ExtraHop import fr.acinq.eclair.wire.protocol.OfferCodecs.{invoiceCodec, invoiceTlvCodec} import fr.acinq.eclair.wire.protocol.Offers._ import fr.acinq.eclair.wire.protocol.{Offers, TlvStream} @@ -60,7 +59,7 @@ case class Bolt12Invoice(records: TlvStream[InvoiceTlv], nodeId_opt: Option[Publ override val description: Either[String, ByteVector32] = Left(records.get[Description].get.description) - override val routingInfo: Seq[Seq[ExtraHop]] = Seq.empty + override val extraEdges: Seq[Invoice.ExtraEdge] = Seq.empty // TODO: the blinded paths need to be converted to graph edges override val createdAt: TimestampSecond = records.get[CreatedAt].get.timestamp diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Invoice.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Invoice.scala index 6060904ae4..545c9cea81 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Invoice.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Invoice.scala @@ -18,7 +18,9 @@ package fr.acinq.eclair.payment import fr.acinq.bitcoin.scalacompat.ByteVector32 import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey -import fr.acinq.eclair.{CltvExpiryDelta, Features, InvoiceFeature, MilliSatoshi, TimestampSecond} +import fr.acinq.eclair.payment.relay.Relayer +import fr.acinq.eclair.wire.protocol.ChannelUpdate +import fr.acinq.eclair.{CltvExpiryDelta, Features, InvoiceFeature, MilliSatoshi, MilliSatoshiLong, ShortChannelId, TimestampSecond} import scodec.bits.ByteVector import scala.concurrent.duration.FiniteDuration @@ -39,7 +41,7 @@ trait Invoice { val description: Either[String, ByteVector32] - val routingInfo: Seq[Seq[Bolt11Invoice.ExtraHop]] + val extraEdges: Seq[Invoice.ExtraEdge] val relativeExpiry: FiniteDuration @@ -53,6 +55,29 @@ trait Invoice { } object Invoice { + sealed trait ExtraEdge { + val sourceNodeId: PublicKey + val feeBase: MilliSatoshi + val feeProportionalMillionths: Long + val cltvExpiryDelta: CltvExpiryDelta + val htlcMinimum: MilliSatoshi + val htlcMaximum_opt: Option[MilliSatoshi] + + def relayFees: Relayer.RelayFees = Relayer.RelayFees(feeBase = feeBase, feeProportionalMillionths = feeProportionalMillionths) + } + + case class BasicEdge(sourceNodeId: PublicKey, + targetNodeId: PublicKey, + shortChannelId: ShortChannelId, + feeBase: MilliSatoshi, + feeProportionalMillionths: Long, + cltvExpiryDelta: CltvExpiryDelta) extends ExtraEdge { + override val htlcMinimum: MilliSatoshi = 0 msat + override val htlcMaximum_opt: Option[MilliSatoshi] = None + + def update(u: ChannelUpdate): BasicEdge = copy(feeBase = u.feeBaseMsat, feeProportionalMillionths = u.feeProportionalMillionths, cltvExpiryDelta = u.cltvExpiryDelta) + } + def fromString(input: String): Try[Invoice] = { if (input.toLowerCase.startsWith("lni")) { Bolt12Invoice.fromString(input) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentEvents.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentEvents.scala index 8d7add9887..621e6b62a5 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentEvents.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentEvents.scala @@ -19,7 +19,7 @@ package fr.acinq.eclair.payment import fr.acinq.bitcoin.scalacompat.ByteVector32 import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.eclair.crypto.Sphinx -import fr.acinq.eclair.payment.Bolt11Invoice.ExtraHop +import fr.acinq.eclair.payment.Invoice.{BasicEdge, ExtraEdge} import fr.acinq.eclair.payment.send.PaymentInitiator.SendPaymentConfig import fr.acinq.eclair.router.Announcements import fr.acinq.eclair.router.Router.{ChannelDesc, ChannelHop, Hop, Ignore} @@ -231,7 +231,7 @@ object PaymentFailure { } /** Update the invoice routing hints based on more recent channel updates received. */ - def updateRoutingHints(failures: Seq[PaymentFailure], routingHints: Seq[Seq[ExtraHop]]): Seq[Seq[ExtraHop]] = { + def updateExtraEdges(failures: Seq[PaymentFailure], extraEdges: Seq[ExtraEdge]): Seq[ExtraEdge] = { // We're only interested in the last channel update received per channel. val updates = failures.foldLeft(Map.empty[ShortChannelId, ChannelUpdate]) { case (current, failure) => failure match { @@ -239,14 +239,13 @@ object PaymentFailure { case _ => current } } - routingHints.map(_.map(extraHop => updates.get(extraHop.shortChannelId) match { - case Some(u) => extraHop.copy( - cltvExpiryDelta = u.cltvExpiryDelta, - feeBase = u.feeBaseMsat, - feeProportionalMillionths = u.feeProportionalMillionths - ) - case None => extraHop - })) + extraEdges.map { + case edge: BasicEdge => updates.get(edge.shortChannelId) match { + case Some(u) => edge.update(u) + case None => edge + } + case edge => edge + } } } @@ -260,4 +259,4 @@ case class PathFindingExperimentMetrics(paymentHash: ByteVector32, isMultiPart: Boolean, experimentName: String, recipientNodeId: PublicKey, - routingHints_opt: Option[Seq[Seq[ExtraHop]]]) + routingHints_opt: Option[Seq[Seq[Bolt11Invoice.ExtraHop]]]) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentPacket.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentPacket.scala index 36ba5769a8..8f42f672e0 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentPacket.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentPacket.scala @@ -212,7 +212,7 @@ object OutgoingPaymentPacket { * - firstExpiry is the cltv expiry for the first trampoline node in the route * - the trampoline onion to include in final payload of a normal onion */ - def buildTrampolineToLegacyPacket(invoice: Invoice, hops: Seq[NodeHop], finalPayload: PaymentOnion.FinalPayload): Try[(MilliSatoshi, CltvExpiry, Sphinx.PacketAndSecrets)] = { + def buildTrampolineToLegacyPacket(invoice: Bolt11Invoice, hops: Seq[NodeHop], finalPayload: PaymentOnion.FinalPayload): Try[(MilliSatoshi, CltvExpiry, Sphinx.PacketAndSecrets)] = { // NB: the final payload will never reach the recipient, since the next-to-last node in the trampoline route will convert that to a non-trampoline payment. // We use the smallest final payload possible, otherwise we may overflow the trampoline onion size. val dummyFinalPayload = PaymentOnion.createSinglePartPayload(finalPayload.amount, finalPayload.expiry, finalPayload.paymentSecret, None) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/NodeRelay.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/NodeRelay.scala index 4937f1b642..3b323700f9 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/NodeRelay.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/NodeRelay.scala @@ -267,18 +267,18 @@ class NodeRelay private(nodeParams: NodeParams, // If invoice features are provided in the onion, the sender is asking us to relay to a non-trampoline recipient. val payFSM = payloadOut.invoiceFeatures match { case Some(features) => - val routingHints = payloadOut.invoiceRoutingInfo.map(_.map(_.toSeq).toSeq).getOrElse(Nil) + val extraEdges = payloadOut.invoiceRoutingInfo.getOrElse(Nil).flatMap(Bolt11Invoice.toExtraEdges(_, payloadOut.outgoingNodeId)) val paymentSecret = payloadOut.paymentSecret.get // NB: we've verified that there was a payment secret in validateRelay if (Features(features).hasFeature(Features.BasicMultiPartPayment)) { context.log.debug("sending the payment to non-trampoline recipient using MPP") - val payment = SendMultiPartPayment(payFsmAdapters, paymentSecret, payloadOut.outgoingNodeId, payloadOut.amountToForward, payloadOut.outgoingCltv, nodeParams.maxPaymentAttempts, payloadOut.paymentMetadata, routingHints, routeParams) + val payment = SendMultiPartPayment(payFsmAdapters, paymentSecret, payloadOut.outgoingNodeId, payloadOut.amountToForward, payloadOut.outgoingCltv, nodeParams.maxPaymentAttempts, payloadOut.paymentMetadata, extraEdges, routeParams) val payFSM = outgoingPaymentFactory.spawnOutgoingPayFSM(context, paymentCfg, multiPart = true) payFSM ! payment payFSM } else { context.log.debug("sending the payment to non-trampoline recipient without MPP") val finalPayload = PaymentOnion.createSinglePartPayload(payloadOut.amountToForward, payloadOut.outgoingCltv, paymentSecret, payloadOut.paymentMetadata) - val payment = SendPaymentToNode(payFsmAdapters, payloadOut.outgoingNodeId, finalPayload, nodeParams.maxPaymentAttempts, routingHints, routeParams) + val payment = SendPaymentToNode(payFsmAdapters, payloadOut.outgoingNodeId, finalPayload, nodeParams.maxPaymentAttempts, extraEdges, routeParams) val payFSM = outgoingPaymentFactory.spawnOutgoingPayFSM(context, paymentCfg, multiPart = false) payFSM ! payment payFSM diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/MultiPartPaymentLifecycle.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/MultiPartPaymentLifecycle.scala index 90537637f9..0cf2f9e155 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/MultiPartPaymentLifecycle.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/MultiPartPaymentLifecycle.scala @@ -22,7 +22,7 @@ import fr.acinq.bitcoin.scalacompat.ByteVector32 import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.eclair.channel.{HtlcOverriddenByLocalCommit, HtlcsTimedoutDownstream, HtlcsWillTimeoutUpstream} import fr.acinq.eclair.db.{OutgoingPayment, OutgoingPaymentStatus, PaymentType} -import fr.acinq.eclair.payment.Bolt11Invoice.ExtraHop +import fr.acinq.eclair.payment.Invoice.ExtraEdge import fr.acinq.eclair.payment.Monitoring.{Metrics, Tags} import fr.acinq.eclair.payment.OutgoingPaymentPacket.Upstream import fr.acinq.eclair.payment.PaymentSent.PartialPayment @@ -117,8 +117,8 @@ class MultiPartPaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, gotoAbortedOrStop(PaymentAborted(d.request, d.failures ++ pf.failures, d.pending.keySet - pf.id)) } else { val ignore1 = PaymentFailure.updateIgnored(pf.failures, d.ignore) - val assistedRoutes1 = PaymentFailure.updateRoutingHints(pf.failures, d.request.assistedRoutes) - stay() using d.copy(pending = d.pending - pf.id, ignore = ignore1, failures = d.failures ++ pf.failures, request = d.request.copy(assistedRoutes = assistedRoutes1)) + val extraEdges1 = PaymentFailure.updateExtraEdges(pf.failures, d.request.extraEdges) + stay() using d.copy(pending = d.pending - pf.id, ignore = ignore1, failures = d.failures ++ pf.failures, request = d.request.copy(extraEdges = extraEdges1)) } // The recipient released the preimage without receiving the full payment amount. @@ -139,12 +139,12 @@ class MultiPartPaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, gotoAbortedOrStop(PaymentAborted(d.request, d.failures ++ pf.failures :+ failure, d.pending.keySet - pf.id)) } else { val ignore1 = PaymentFailure.updateIgnored(pf.failures, d.ignore) - val assistedRoutes1 = PaymentFailure.updateRoutingHints(pf.failures, d.request.assistedRoutes) + val extraEdges1 = PaymentFailure.updateExtraEdges(pf.failures, d.request.extraEdges) val stillPending = d.pending - pf.id val (toSend, maxFee) = remainingToSend(d.request, stillPending.values, d.request.routeParams.includeLocalChannelCost) log.debug("child payment failed, retry sending {} with maximum fee {}", toSend, maxFee) val routeParams = d.request.routeParams.copy(randomize = true) // we randomize route selection when we retry - val d1 = d.copy(pending = stillPending, ignore = ignore1, failures = d.failures ++ pf.failures, request = d.request.copy(assistedRoutes = assistedRoutes1)) + val d1 = d.copy(pending = stillPending, ignore = ignore1, failures = d.failures ++ pf.failures, request = d.request.copy(extraEdges = extraEdges1)) router ! createRouteRequest(nodeParams, toSend, maxFee, routeParams, d1, cfg) goto(WAIT_FOR_ROUTES) using d1 } @@ -264,7 +264,10 @@ class MultiPartPaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, } paymentSent.feesPaid + localFees } - val hints_opt = cfg.invoice.flatMap(invoice => if (invoice.routingInfo.isEmpty) None else Some(invoice.routingInfo)) + val hints_opt = cfg.invoice.flatMap { + case invoice: Bolt11Invoice if invoice.routingInfo.nonEmpty => Some(invoice.routingInfo) + case _ => None + } context.system.eventStream.publish(PathFindingExperimentMetrics(cfg.paymentHash, cfg.recipientAmount, fees, status, duration, now, isMultiPart = true, request.routeParams.experimentName, cfg.recipientNodeId, hints_opt)) } Metrics.SentPaymentDuration @@ -310,7 +313,7 @@ object MultiPartPaymentLifecycle { * @param targetExpiry expiry at the target node (CLTV for the target node's received HTLCs). * @param maxAttempts maximum number of retries. * @param paymentMetadata payment metadata (usually from the Bolt 11 invoice). - * @param assistedRoutes routing hints (usually from a Bolt 11 invoice). + * @param extraEdges routing hints (usually from a Bolt 11 invoice). * @param routeParams parameters to fine-tune the routing algorithm. * @param additionalTlvs when provided, additional tlvs that will be added to the onion sent to the target node. * @param userCustomTlvs when provided, additional user-defined custom tlvs that will be added to the onion sent to the target node. @@ -322,7 +325,7 @@ object MultiPartPaymentLifecycle { targetExpiry: CltvExpiry, maxAttempts: Int, paymentMetadata: Option[ByteVector], - assistedRoutes: Seq[Seq[ExtraHop]] = Nil, + extraEdges: Seq[ExtraEdge] = Nil, routeParams: RouteParams, additionalTlvs: Seq[OnionPaymentPayloadTlv] = Nil, userCustomTlvs: Seq[GenericTlv] = Nil) { @@ -396,7 +399,7 @@ object MultiPartPaymentLifecycle { d.request.targetNodeId, toSend, maxFee, - d.request.assistedRoutes, + d.request.extraEdges, d.ignore, routeParams, allowMultiPart = true, diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentInitiator.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentInitiator.scala index 0fc74bfc26..09fce8898e 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentInitiator.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentInitiator.scala @@ -22,7 +22,7 @@ import fr.acinq.bitcoin.scalacompat.{ByteVector32, Crypto} import fr.acinq.eclair.Features.BasicMultiPartPayment import fr.acinq.eclair.channel.fsm.Channel import fr.acinq.eclair.crypto.Sphinx -import fr.acinq.eclair.payment.Bolt11Invoice.ExtraHop +import fr.acinq.eclair.payment.Invoice.ExtraEdge import fr.acinq.eclair.payment.OutgoingPaymentPacket.Upstream import fr.acinq.eclair.payment._ import fr.acinq.eclair.payment.send.MultiPartPaymentLifecycle.{PreimageReceived, SendMultiPartPayment} @@ -60,12 +60,12 @@ class PaymentInitiator(nodeParams: NodeParams, outgoingPaymentFactory: PaymentIn sender() ! PaymentFailed(paymentId, r.paymentHash, LocalFailure(r.recipientAmount, Nil, PaymentSecretMissing) :: Nil) case Some(paymentSecret) if r.invoice.features.hasFeature(Features.BasicMultiPartPayment) && nodeParams.features.hasFeature(BasicMultiPartPayment) => val fsm = outgoingPaymentFactory.spawnOutgoingMultiPartPayment(context, paymentCfg) - fsm ! SendMultiPartPayment(self, paymentSecret, r.recipientNodeId, r.recipientAmount, finalExpiry, r.maxAttempts, r.invoice.paymentMetadata, r.assistedRoutes, r.routeParams, userCustomTlvs = r.userCustomTlvs) + fsm ! SendMultiPartPayment(self, paymentSecret, r.recipientNodeId, r.recipientAmount, finalExpiry, r.maxAttempts, r.invoice.paymentMetadata, r.extraEdges, r.routeParams, userCustomTlvs = r.userCustomTlvs) context become main(pending + (paymentId -> PendingPaymentToNode(sender(), r))) case Some(paymentSecret) => val finalPayload = PaymentOnion.createSinglePartPayload(r.recipientAmount, finalExpiry, paymentSecret, r.invoice.paymentMetadata, r.userCustomTlvs) val fsm = outgoingPaymentFactory.spawnOutgoingPayment(context, paymentCfg) - fsm ! PaymentLifecycle.SendPaymentToNode(self, r.recipientNodeId, finalPayload, r.maxAttempts, r.assistedRoutes, r.routeParams) + fsm ! PaymentLifecycle.SendPaymentToNode(self, r.recipientNodeId, finalPayload, r.maxAttempts, r.extraEdges, r.routeParams) context become main(pending + (paymentId -> PendingPaymentToNode(sender(), r))) } @@ -115,7 +115,7 @@ class PaymentInitiator(nodeParams: NodeParams, outgoingPaymentFactory: PaymentIn val trampolineSecret = r.trampolineSecret.getOrElse(randomBytes32()) sender() ! SendPaymentToRouteResponse(paymentId, parentPaymentId, Some(trampolineSecret)) val payFsm = outgoingPaymentFactory.spawnOutgoingPayment(context, paymentCfg) - payFsm ! PaymentLifecycle.SendPaymentToRoute(self, Left(r.route), PaymentOnion.createMultiPartPayload(r.amount, trampolineAmount, trampolineExpiry, trampolineSecret, r.invoice.paymentMetadata, Seq(OnionPaymentPayloadTlv.TrampolineOnion(trampolineOnion))), r.invoice.routingInfo) + payFsm ! PaymentLifecycle.SendPaymentToRoute(self, Left(r.route), PaymentOnion.createMultiPartPayload(r.amount, trampolineAmount, trampolineExpiry, trampolineSecret, r.invoice.paymentMetadata, Seq(OnionPaymentPayloadTlv.TrampolineOnion(trampolineOnion))), r.invoice.extraEdges) context become main(pending + (paymentId -> PendingPaymentToRoute(sender(), r))) case Failure(t) => log.warning("cannot send outgoing trampoline payment: {}", t.getMessage) @@ -124,7 +124,7 @@ class PaymentInitiator(nodeParams: NodeParams, outgoingPaymentFactory: PaymentIn case Nil => sender() ! SendPaymentToRouteResponse(paymentId, parentPaymentId, None) val payFsm = outgoingPaymentFactory.spawnOutgoingPayment(context, paymentCfg) - payFsm ! PaymentLifecycle.SendPaymentToRoute(self, Left(r.route), PaymentOnion.createMultiPartPayload(r.amount, r.recipientAmount, finalExpiry, r.invoice.paymentSecret.get, r.invoice.paymentMetadata), r.invoice.routingInfo) + payFsm ! PaymentLifecycle.SendPaymentToRoute(self, Left(r.route), PaymentOnion.createMultiPartPayload(r.amount, r.recipientAmount, finalExpiry, r.invoice.paymentSecret.get, r.invoice.paymentMetadata), r.invoice.extraEdges) context become main(pending + (paymentId -> PendingPaymentToRoute(sender(), r))) case _ => sender() ! PaymentFailed(paymentId, r.paymentHash, LocalFailure(r.recipientAmount, Nil, TrampolineMultiNodeNotSupported) :: Nil) @@ -206,7 +206,10 @@ class PaymentInitiator(nodeParams: NodeParams, outgoingPaymentFactory: PaymentIn val trampolinePacket_opt = if (r.invoice.features.hasFeature(Features.TrampolinePaymentPrototype)) { OutgoingPaymentPacket.buildTrampolinePacket(r.paymentHash, trampolineRoute, finalPayload) } else { - OutgoingPaymentPacket.buildTrampolineToLegacyPacket(r.invoice, trampolineRoute, finalPayload) + r.invoice match { + case invoice: Bolt11Invoice => OutgoingPaymentPacket.buildTrampolineToLegacyPacket(invoice, trampolineRoute, finalPayload) + case _ => Failure(new Exception("Trampoline to legacy is only supported for Bolt11 invoices.")) + } } trampolinePacket_opt.map { case (trampolineAmount, trampolineExpiry, trampolineOnion) => (trampolineAmount, trampolineExpiry, trampolineOnion.packet) @@ -220,7 +223,7 @@ class PaymentInitiator(nodeParams: NodeParams, outgoingPaymentFactory: PaymentIn buildTrampolinePayment(r, r.trampolineNodeId, trampolineFees, trampolineExpiryDelta).map { case (trampolineAmount, trampolineExpiry, trampolineOnion) => val fsm = outgoingPaymentFactory.spawnOutgoingMultiPartPayment(context, paymentCfg) - fsm ! SendMultiPartPayment(self, trampolineSecret, r.trampolineNodeId, trampolineAmount, trampolineExpiry, nodeParams.maxPaymentAttempts, r.invoice.paymentMetadata, r.invoice.routingInfo, r.routeParams, Seq(OnionPaymentPayloadTlv.TrampolineOnion(trampolineOnion))) + fsm ! SendMultiPartPayment(self, trampolineSecret, r.trampolineNodeId, trampolineAmount, trampolineExpiry, nodeParams.maxPaymentAttempts, r.invoice.paymentMetadata, r.invoice.extraEdges, r.routeParams, Seq(OnionPaymentPayloadTlv.TrampolineOnion(trampolineOnion))) } } @@ -302,7 +305,7 @@ object PaymentInitiator { * @param invoice Bolt 11 invoice. * @param maxAttempts maximum number of retries. * @param externalId (optional) externally-controlled identifier (to reconcile between application DB and eclair DB). - * @param assistedRoutes (optional) routing hints (usually from a Bolt 11 invoice). + * @param extraEdges (optional) routing hints (usually from a Bolt 11 invoice). * @param routeParams (optional) parameters to fine-tune the routing algorithm. * @param userCustomTlvs (optional) user-defined custom tlvs that will be added to the onion sent to the target node. * @param blockUntilComplete (optional) if true, wait until the payment completes before returning a result. @@ -311,7 +314,7 @@ object PaymentInitiator { invoice: Invoice, maxAttempts: Int, externalId: Option[String] = None, - assistedRoutes: Seq[Seq[ExtraHop]] = Nil, + extraEdges: Seq[ExtraEdge] = Nil, routeParams: RouteParams, userCustomTlvs: Seq[GenericTlv] = Nil, blockUntilComplete: Boolean = false) extends SendRequestedPayment diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentLifecycle.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentLifecycle.scala index 4a7e80e688..aaa1710ade 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentLifecycle.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentLifecycle.scala @@ -24,7 +24,7 @@ import fr.acinq.eclair._ import fr.acinq.eclair.channel._ import fr.acinq.eclair.crypto.{Sphinx, TransportHandler} import fr.acinq.eclair.db.{OutgoingPayment, OutgoingPaymentStatus, PaymentType} -import fr.acinq.eclair.payment.Bolt11Invoice.ExtraHop +import fr.acinq.eclair.payment.Invoice.{BasicEdge, ExtraEdge} import fr.acinq.eclair.payment.Monitoring.{Metrics, Tags} import fr.acinq.eclair.payment.OutgoingPaymentPacket.Upstream import fr.acinq.eclair.payment.PaymentSent.PartialPayment @@ -56,7 +56,7 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A case Event(c: SendPaymentToRoute, WaitingForRequest) => log.debug("sending {} to route {}", c.finalPayload.amount, c.printRoute()) c.route.fold( - hops => router ! FinalizeRoute(c.finalPayload.amount, hops, c.assistedRoutes, paymentContext = Some(cfg.paymentContext)), + hops => router ! FinalizeRoute(c.finalPayload.amount, hops, c.extraEdges, paymentContext = Some(cfg.paymentContext)), route => self ! RouteResponse(route :: Nil) ) if (cfg.storeInDb) { @@ -66,7 +66,7 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A case Event(c: SendPaymentToNode, WaitingForRequest) => log.debug("sending {} to {}", c.finalPayload.amount, c.targetNodeId) - router ! RouteRequest(nodeParams.nodeId, c.targetNodeId, c.finalPayload.amount, c.maxFee, c.assistedRoutes, routeParams = c.routeParams, paymentContext = Some(cfg.paymentContext)) + router ! RouteRequest(nodeParams.nodeId, c.targetNodeId, c.finalPayload.amount, c.maxFee, c.extraEdges, routeParams = c.routeParams, paymentContext = Some(cfg.paymentContext)) if (cfg.storeInDb) { paymentsDb.addOutgoingPayment(OutgoingPayment(id, cfg.parentId, cfg.externalId, paymentHash, PaymentType.Standard, c.finalPayload.amount, cfg.recipientAmount, cfg.recipientNodeId, TimestampMilli.now(), cfg.invoice, OutgoingPaymentStatus.Pending)) } @@ -138,7 +138,7 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A data.c match { case sendPaymentToNode: SendPaymentToNode => val ignore1 = PaymentFailure.updateIgnored(failure, data.ignore) - router ! RouteRequest(nodeParams.nodeId, data.c.targetNodeId, data.c.finalPayload.amount, sendPaymentToNode.maxFee, data.c.assistedRoutes, ignore1, sendPaymentToNode.routeParams, paymentContext = Some(cfg.paymentContext)) + router ! RouteRequest(nodeParams.nodeId, data.c.targetNodeId, data.c.finalPayload.amount, sendPaymentToNode.maxFee, data.c.extraEdges, ignore1, sendPaymentToNode.routeParams, paymentContext = Some(cfg.paymentContext)) goto(WAITING_FOR_ROUTE) using WaitingForRoute(data.c, data.failures :+ failure, ignore1) case _: SendPaymentToRoute => log.error("unexpected retry during SendPaymentToRoute") @@ -225,7 +225,7 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A log.info(s"received 'Update' type error message from nodeId=$nodeId, retrying payment (failure=$failureMessage)") val failure = RemoteFailure(d.c.finalPayload.amount, cfg.fullRoute(route), e) if (Announcements.checkSig(failureMessage.update, nodeId)) { - val assistedRoutes1 = handleUpdate(nodeId, failureMessage, d) + val extraEdges1 = handleUpdate(nodeId, failureMessage, d) val ignore1 = PaymentFailure.updateIgnored(failure, ignore) // let's try again, router will have updated its state c match { @@ -233,7 +233,7 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A log.error("unexpected retry during SendPaymentToRoute") stop(FSM.Normal) case c: SendPaymentToNode => - router ! RouteRequest(nodeParams.nodeId, c.targetNodeId, c.finalPayload.amount, c.maxFee, assistedRoutes1, ignore1, c.routeParams, paymentContext = Some(cfg.paymentContext)) + router ! RouteRequest(nodeParams.nodeId, c.targetNodeId, c.finalPayload.amount, c.maxFee, extraEdges1, ignore1, c.routeParams, paymentContext = Some(cfg.paymentContext)) goto(WAITING_FOR_ROUTE) using WaitingForRoute(c, failures :+ failure, ignore1) } } else { @@ -244,7 +244,7 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A log.error("unexpected retry during SendPaymentToRoute") stop(FSM.Normal) case c: SendPaymentToNode => - router ! RouteRequest(nodeParams.nodeId, c.targetNodeId, c.finalPayload.amount, c.maxFee, c.assistedRoutes, ignore + nodeId, c.routeParams, paymentContext = Some(cfg.paymentContext)) + router ! RouteRequest(nodeParams.nodeId, c.targetNodeId, c.finalPayload.amount, c.maxFee, c.extraEdges, ignore + nodeId, c.routeParams, paymentContext = Some(cfg.paymentContext)) goto(WAITING_FOR_ROUTE) using WaitingForRoute(c, failures :+ failure, ignore + nodeId) } } @@ -260,7 +260,7 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A * * @return updated routing hints if applicable. */ - private def handleUpdate(nodeId: PublicKey, failure: Update, data: WaitingForComplete): Seq[Seq[ExtraHop]] = { + private def handleUpdate(nodeId: PublicKey, failure: Update, data: WaitingForComplete): Seq[ExtraEdge] = { // TODO: properly handle updates to channels provided as routing hints in the invoice data.route.getChannelUpdateForNode(nodeId) match { case Some(u) if u.shortChannelId != failure.update.shortChannelId => @@ -286,21 +286,17 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A router ! failure.update // we return updated assisted routes: they take precedence over the router's routing table if (failure.update.channelFlags.isEnabled) { - data.c.assistedRoutes.map(_.map { - case extraHop: ExtraHop if extraHop.shortChannelId == failure.update.shortChannelId => extraHop.copy( - cltvExpiryDelta = failure.update.cltvExpiryDelta, - feeBase = failure.update.feeBaseMsat, - feeProportionalMillionths = failure.update.feeProportionalMillionths - ) - case extraHop => extraHop - }) + data.c.extraEdges.map { + case edge: BasicEdge if edge.shortChannelId == failure.update.shortChannelId => edge.update(failure.update) + case edge => edge + } } else { // if the channel is disabled, we temporarily exclude it: this is necessary because the routing hint doesn't contain // channel flags to indicate that it's disabled - data.c.assistedRoutes.flatMap(r => RouteCalculation.toChannelDescs(r, data.c.targetNodeId)) - .find(_.shortChannelId == failure.update.shortChannelId) - .foreach(desc => router ! ExcludeChannel(desc)) // we want the exclusion to be router-wide so that sister payments in the case of MPP are aware the channel is faulty - data.c.assistedRoutes + data.c.extraEdges + .find { case edge: BasicEdge => edge.shortChannelId == failure.update.shortChannelId } + .foreach { case edge: BasicEdge => router ! ExcludeChannel(ChannelDesc(edge.shortChannelId, edge.sourceNodeId, edge.targetNodeId)) } // we want the exclusion to be router-wide so that sister payments in the case of MPP are aware the channel is faulty + data.c.extraEdges } } @@ -353,7 +349,10 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A } request match { case SendPaymentToNode(_, _, _, _, _, routeParams) => - val hints_opt = cfg.invoice.flatMap(invoice => if (invoice.routingInfo.isEmpty) None else Some(invoice.routingInfo)) + val hints_opt = cfg.invoice.flatMap { + case invoice: Bolt11Invoice if invoice.routingInfo.nonEmpty => Some(invoice.routingInfo) + case _ => None + } context.system.eventStream.publish(PathFindingExperimentMetrics(cfg.paymentHash, request.finalPayload.amount, fees, status, duration, now, isMultiPart = false, routeParams.experimentName, cfg.recipientNodeId, hints_opt)) case SendPaymentToRoute(_, _, _, _) => () } @@ -380,7 +379,7 @@ object PaymentLifecycle { // @formatter:off def replyTo: ActorRef def finalPayload: FinalPayload - def assistedRoutes: Seq[Seq[ExtraHop]] + def extraEdges: Seq[ExtraEdge] def targetNodeId: PublicKey def maxAttempts: Int // @formatter:on @@ -395,7 +394,7 @@ object PaymentLifecycle { case class SendPaymentToRoute(replyTo: ActorRef, route: Either[PredefinedRoute, Route], finalPayload: FinalPayload, - assistedRoutes: Seq[Seq[ExtraHop]] = Nil) extends SendPayment { + extraEdges: Seq[ExtraEdge] = Nil) extends SendPayment { require(route.fold(!_.isEmpty, _.hops.nonEmpty), "payment route must not be empty") val targetNodeId: PublicKey = route.fold(_.targetNodeId, _.hops.last.nextNodeId) @@ -412,18 +411,18 @@ object PaymentLifecycle { /** * Send a payment to a given node. A path-finding algorithm will run to find a suitable payment route. * - * @param targetNodeId target node (may be the final recipient when using source-routing, or the first trampoline - * node when using trampoline). - * @param finalPayload onion payload for the target node. - * @param maxAttempts maximum number of retries. - * @param assistedRoutes routing hints (usually from a Bolt 11 invoice). - * @param routeParams parameters to fine-tune the routing algorithm. + * @param targetNodeId target node (may be the final recipient when using source-routing, or the first trampoline + * node when using trampoline). + * @param finalPayload onion payload for the target node. + * @param maxAttempts maximum number of retries. + * @param extraEdges routing hints (usually from a Bolt 11 invoice). + * @param routeParams parameters to fine-tune the routing algorithm. */ case class SendPaymentToNode(replyTo: ActorRef, targetNodeId: PublicKey, finalPayload: FinalPayload, maxAttempts: Int, - assistedRoutes: Seq[Seq[ExtraHop]] = Nil, + extraEdges: Seq[ExtraEdge] = Nil, routeParams: RouteParams) extends SendPayment { require(finalPayload.amount > 0.msat, s"amount must be > 0") diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/Graph.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/Graph.scala index 73c4fcca11..b2dff75d0d 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/Graph.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/Graph.scala @@ -17,8 +17,9 @@ package fr.acinq.eclair.router import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey -import fr.acinq.bitcoin.scalacompat.{Btc, MilliBtc, Satoshi, SatoshiLong} +import fr.acinq.bitcoin.scalacompat.{Btc, BtcDouble, MilliBtc, Satoshi} import fr.acinq.eclair.payment.relay.Relayer.RelayFees +import fr.acinq.eclair.payment.Invoice import fr.acinq.eclair.router.Graph.GraphStructure.{DirectedGraph, GraphEdge} import fr.acinq.eclair.router.Router._ import fr.acinq.eclair.wire.protocol.ChannelUpdate @@ -457,14 +458,18 @@ object Graph { balance_opt = pc.getBalanceSameSideAs(u) ) - def apply(ac: AssistedChannel): GraphEdge = GraphEdge( - desc = ChannelDesc(ac.shortChannelId, ac.nodeId, ac.nextNodeId), - params = ac.params, - // Bolt 11 routing hints don't include the channel's capacity, so we round up the maximum htlc amount - capacity = ac.params.htlcMaximum.truncateToSatoshi + 1.sat, - // we assume channels provided as hints have enough balance to handle the payment - balance_opt = Some(ac.params.htlcMaximum) - ) + def apply(e: Invoice.ExtraEdge): GraphEdge = e match { + case e@Invoice.BasicEdge(sourceNodeId, targetNodeId, shortChannelId, _, _, _) => + val maxBtc = 21e6.btc + GraphEdge( + desc = ChannelDesc(shortChannelId, sourceNodeId, targetNodeId), + params = ChannelRelayParams.FromHint(e), + // Bolt 11 routing hints don't include the channel's capacity, so we assume it's big enough + capacity = maxBtc.toSatoshi, + // we assume channels provided as hints have enough balance to handle the payment + balance_opt = Some(maxBtc.toMilliSatoshi) + ) + } } /** A graph data structure that uses an adjacency list, stores the incoming edges of the neighbors */ diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/RouteCalculation.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/RouteCalculation.scala index 200a81ed2e..b15763634b 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/RouteCalculation.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/RouteCalculation.scala @@ -22,10 +22,9 @@ import com.softwaremill.quicklens.ModifyPimp import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.eclair.Logs.LogCategory import fr.acinq.eclair._ -import fr.acinq.eclair.payment.Bolt11Invoice.ExtraHop import fr.acinq.eclair.router.Graph.GraphStructure.DirectedGraph.graphEdgeToHop import fr.acinq.eclair.router.Graph.GraphStructure.{DirectedGraph, GraphEdge} -import fr.acinq.eclair.router.Graph.{InfiniteLoop, NegativeProbability, RichWeight, RoutingHeuristics} +import fr.acinq.eclair.router.Graph.{InfiniteLoop, NegativeProbability, RichWeight} import fr.acinq.eclair.router.Monitoring.{Metrics, Tags} import fr.acinq.eclair.router.Router._ import kamon.tag.TagSet @@ -44,9 +43,7 @@ object RouteCalculation { paymentHash_opt = fr.paymentContext.map(_.paymentHash))) { implicit val sender: ActorRef = ctx.self // necessary to preserve origin when sending messages to other actors - val assistedChannels: Map[ShortChannelId, AssistedChannel] = fr.assistedRoutes.flatMap(toAssistedChannels(_, fr.route.targetNodeId, fr.amount)).toMap - val extraEdges = assistedChannels.values.map(ac => GraphEdge(ac)).toSet - val g = extraEdges.foldLeft(d.graphWithBalances.graph) { case (g: DirectedGraph, e: GraphEdge) => g.addEdge(e) } + val g = fr.extraEdges.map(GraphEdge(_)).foldLeft(d.graphWithBalances.graph) { case (g: DirectedGraph, e: GraphEdge) => g.addEdge(e) } fr.route match { case PredefinedNodeRoute(hops) => @@ -75,10 +72,7 @@ object RouteCalculation { case c.nodeId2 => Some(ChannelDesc(c.shortIds.localAlias, c.nodeId2, c.nodeId1)) case _ => None } - case None => assistedChannels.get(shortChannelId).flatMap(c => currentNode match { - case c.nodeId => Some(ChannelDesc(shortChannelId, c.nodeId, c.nextNodeId)) - case _ => None - }) + case None => fr.extraEdges.map(GraphEdge(_)).find(e => e.desc.shortChannelId == shortChannelId && e.desc.a == currentNode).map(_.desc) } channelDesc_opt.flatMap(c => g.getEdge(c)) match { case Some(edge) => (edge.desc.b, previousHops :+ ChannelHop(edge.desc.shortChannelId, edge.desc.a, edge.desc.b, edge.params)) @@ -104,17 +98,12 @@ object RouteCalculation { paymentHash_opt = r.paymentContext.map(_.paymentHash))) { implicit val sender: ActorRef = ctx.self // necessary to preserve origin when sending messages to other actors - // we convert extra routing info provided in the invoice to fake channel_update - // it takes precedence over all other channel_updates we know - val assistedChannels: Map[ShortChannelId, AssistedChannel] = r.assistedRoutes.flatMap(toAssistedChannels(_, r.target, r.amount)) - .filterNot { case (_, ac) => ac.nodeId == r.source } // we ignore routing hints for our own channels, we have more accurate information - .toMap - val extraEdges = assistedChannels.values.map(ac => GraphEdge(ac)).toSet + val extraEdges = r.extraEdges.map(GraphEdge(_)).filterNot(_.desc.a == r.source).toSet // we ignore routing hints for our own channels, we have more accurate information val ignoredEdges = r.ignore.channels ++ d.excludedChannels val params = r.routeParams val routesToFind = if (params.randomize) DEFAULT_ROUTES_COUNT else 1 - log.info(s"finding routes ${r.source}->${r.target} with assistedChannels={} ignoreNodes={} ignoreChannels={} excludedChannels={}", assistedChannels.keys.mkString(","), r.ignore.nodes.map(_.value).mkString(","), r.ignore.channels.mkString(","), d.excludedChannels.mkString(",")) + log.info(s"finding routes ${r.source}->${r.target} with assistedChannels={} ignoreNodes={} ignoreChannels={} excludedChannels={}", extraEdges.map(_.desc.shortChannelId).mkString(","), r.ignore.nodes.map(_.value).mkString(","), r.ignore.channels.mkString(","), d.excludedChannels.mkString(",")) log.info("finding routes with params={}, multiPart={}", params, r.allowMultiPart) log.info("local channels to recipient: {}", d.graphWithBalances.graph.getEdgesBetween(r.source, r.target).map(e => s"${e.desc.shortChannelId} (${e.balance_opt}/${e.capacity})").mkString(", ")) val tags = TagSet.Empty.withTag(Tags.MultiPart, r.allowMultiPart).withTag(Tags.Amount, Tags.amountBucket(r.amount)) @@ -147,25 +136,6 @@ object RouteCalculation { } } - def toAssistedChannels(extraRoute: Seq[ExtraHop], targetNodeId: PublicKey, amount: MilliSatoshi): Map[ShortChannelId, AssistedChannel] = { - // BOLT 11: "For each entry, the pubkey is the node ID of the start of the channel", and the last node is the destination - // The invoice doesn't explicitly specify the channel's htlcMaximumMsat, but we can safely assume that the channel - // should be able to route the payment, so we'll compute an htlcMaximumMsat accordingly. - // We also need to make sure the channel isn't excluded by our heuristics. - val lastChannelCapacity = amount.max(RoutingHeuristics.CAPACITY_CHANNEL_LOW) - val nextNodeIds = extraRoute.map(_.nodeId).drop(1) :+ targetNodeId - extraRoute.zip(nextNodeIds).reverse.foldLeft((lastChannelCapacity, Map.empty[ShortChannelId, AssistedChannel])) { - case ((amount, acs), (extraHop: ExtraHop, nextNodeId)) => - val nextAmount = amount + nodeFee(extraHop.relayFees, amount) - (nextAmount, acs + (extraHop.shortChannelId -> AssistedChannel(nextNodeId, Router.ChannelRelayParams.FromHint(extraHop, nextAmount)))) - }._2 - } - - def toChannelDescs(extraRoute: Seq[ExtraHop], targetNodeId: PublicKey): Seq[ChannelDesc] = { - val nextNodeIds = extraRoute.map(_.nodeId).drop(1) :+ targetNodeId - extraRoute.zip(nextNodeIds).map { case (hop, nextNodeId) => ChannelDesc(hop.shortChannelId, hop.nodeId, nextNodeId) } - } - /** This method is used after a payment failed, and we want to exclude some nodes that we know are failing */ def getIgnoredChannelDesc(channels: Map[ShortChannelId, PublicChannel], ignoreNodes: Set[PublicKey]): Iterable[ChannelDesc] = { val desc = if (ignoreNodes.isEmpty) { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala index a241536f73..3327c1856f 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala @@ -31,8 +31,8 @@ import fr.acinq.eclair.channel._ import fr.acinq.eclair.crypto.TransportHandler import fr.acinq.eclair.db.NetworkDb import fr.acinq.eclair.io.Peer.PeerRoutingMessage -import fr.acinq.eclair.payment.Bolt11Invoice -import fr.acinq.eclair.payment.Bolt11Invoice.ExtraHop +import fr.acinq.eclair.payment.Invoice.ExtraEdge +import fr.acinq.eclair.payment.{Bolt11Invoice, Invoice} import fr.acinq.eclair.payment.relay.Relayer import fr.acinq.eclair.remote.EclairInternalsSerializer.RemoteTypes import fr.acinq.eclair.router.Graph.GraphStructure.DirectedGraph @@ -392,7 +392,7 @@ object Router { case Right(rcu) => updateChannelUpdateSameSideAs(rcu.channelUpdate) } /** Create an invoice routing hint from that channel. Note that if the channel is private, the invoice will leak its existence. */ - def toIncomingExtraHop: Option[ExtraHop] = { + def toIncomingExtraHop: Option[Bolt11Invoice.ExtraHop] = { // we want the incoming channel_update val remoteUpdate_opt = if (localNodeId == nodeId1) update_2_opt else update_1_opt // for incoming payments we preferably use the *remote alias*, otherwise the real scid if we have it @@ -400,18 +400,13 @@ object Router { // we override the remote update's scid, because it contains either the real scid or our local alias scid_opt.flatMap { scid => remoteUpdate_opt.map { remoteUpdate => - ExtraHop(remoteNodeId, scid, remoteUpdate.feeBaseMsat, remoteUpdate.feeProportionalMillionths, remoteUpdate.cltvExpiryDelta) + Bolt11Invoice.ExtraHop(remoteNodeId, scid, remoteUpdate.feeBaseMsat, remoteUpdate.feeProportionalMillionths, remoteUpdate.cltvExpiryDelta) } } } } // @formatter:on - case class AssistedChannel(nextNodeId: PublicKey, params: ChannelRelayParams.FromHint) { - val nodeId: PublicKey = params.extraHop.nodeId - val shortChannelId: ShortChannelId = params.extraHop.shortChannelId - } - trait Hop { /** @return the id of the start node. */ def nodeId: PublicKey @@ -448,11 +443,11 @@ object Router { override def htlcMaximum_opt: Option[MilliSatoshi] = channelUpdate.htlcMaximumMsat } /** We learnt about this channel from hints in an invoice */ - case class FromHint(extraHop: Bolt11Invoice.ExtraHop, htlcMaximum: MilliSatoshi) extends ChannelRelayParams { + case class FromHint(extraHop: Invoice.ExtraEdge) extends ChannelRelayParams { override def cltvExpiryDelta: CltvExpiryDelta = extraHop.cltvExpiryDelta override def relayFees: Relayer.RelayFees = extraHop.relayFees - override def htlcMinimum: MilliSatoshi = 0 msat - override def htlcMaximum_opt: Option[MilliSatoshi] = Some(htlcMaximum) + override def htlcMinimum: MilliSatoshi = extraHop.htlcMinimum + override def htlcMaximum_opt: Option[MilliSatoshi] = extraHop.htlcMaximum_opt } def areSame(a: ChannelRelayParams, b: ChannelRelayParams, ignoreHtlcSize: Boolean = false): Boolean = @@ -523,7 +518,7 @@ object Router { target: PublicKey, amount: MilliSatoshi, maxFee: MilliSatoshi, - assistedRoutes: Seq[Seq[ExtraHop]] = Nil, + extraEdges: Seq[ExtraEdge] = Nil, ignore: Ignore = Ignore.empty, routeParams: RouteParams, allowMultiPart: Boolean = false, @@ -532,7 +527,7 @@ object Router { case class FinalizeRoute(amount: MilliSatoshi, route: PredefinedRoute, - assistedRoutes: Seq[Seq[ExtraHop]] = Nil, + extraEdges: Seq[ExtraEdge] = Nil, paymentContext: Option[PaymentContext] = None) /** diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/PaymentOnion.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/PaymentOnion.scala index 73f90b5df7..9691c38820 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/PaymentOnion.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/PaymentOnion.scala @@ -284,7 +284,8 @@ object PaymentOnion { NodeRelayPayload(TlvStream(AmountToForward(amount), OutgoingCltv(expiry), OutgoingNodeId(nextNodeId))) /** Create a trampoline inner payload instructing the trampoline node to relay via a non-trampoline payment. */ - def createNodeRelayToNonTrampolinePayload(amount: MilliSatoshi, totalAmount: MilliSatoshi, expiry: CltvExpiry, targetNodeId: PublicKey, invoice: Invoice): NodeRelayPayload = { + // TODO: Allow sending blinded routes to trampoline nodes instead of routing hints to support BOLT12Invoice + def createNodeRelayToNonTrampolinePayload(amount: MilliSatoshi, totalAmount: MilliSatoshi, expiry: CltvExpiry, targetNodeId: PublicKey, invoice: Bolt11Invoice): NodeRelayPayload = { val tlvs = Seq( Some(AmountToForward(amount)), Some(OutgoingCltv(expiry)), diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/EclairImplSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/EclairImplSpec.scala index ba6b7d58e7..949dcc7014 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/EclairImplSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/EclairImplSpec.scala @@ -32,6 +32,7 @@ import fr.acinq.eclair.db._ import fr.acinq.eclair.io.Peer import fr.acinq.eclair.io.Peer.OpenChannel import fr.acinq.eclair.payment.Bolt11Invoice.ExtraHop +import fr.acinq.eclair.payment.Invoice.BasicEdge import fr.acinq.eclair.payment.receive.MultiPartHandler.ReceivePayment import fr.acinq.eclair.payment.receive.PaymentHandler import fr.acinq.eclair.payment.relay.Relayer.{GetOutgoingChannels, RelayFees} @@ -121,12 +122,12 @@ class EclairImplSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with I assert(send.recipientAmount == 123.msat) assert(send.paymentHash == ByteVector32.Zeroes) assert(send.invoice == invoice0) - assert(send.assistedRoutes == Seq.empty) + assert(send.extraEdges == Seq.empty) // with assisted routes val externalId1 = "030bb6a5e0c6b203c7e2180fb78c7ba4bdce46126761d8201b91ddac089cdecc87" - val hints = List(List(ExtraHop(Bob.nodeParams.nodeId, ShortChannelId.fromCoordinates("569178x2331x1").success.value, feeBase = 10 msat, feeProportionalMillionths = 1, cltvExpiryDelta = CltvExpiryDelta(12)))) - val invoice1 = Bolt11Invoice(Block.RegtestGenesisBlock.hash, Some(123 msat), ByteVector32.Zeroes, nodePrivKey, Left("description"), CltvExpiryDelta(18), None, None, hints) + val hints = List(ExtraHop(Bob.nodeParams.nodeId, ShortChannelId.fromCoordinates("569178x2331x1").success.value, feeBase = 10 msat, feeProportionalMillionths = 1, cltvExpiryDelta = CltvExpiryDelta(12))) + val invoice1 = Bolt11Invoice(Block.RegtestGenesisBlock.hash, Some(123 msat), ByteVector32.Zeroes, nodePrivKey, Left("description"), CltvExpiryDelta(18), None, None, List(hints)) eclair.send(Some(externalId1), 123 msat, invoice1) val send1 = paymentInitiator.expectMsgType[SendPaymentToNode] assert(send1.externalId.contains(externalId1)) @@ -134,7 +135,13 @@ class EclairImplSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with I assert(send1.recipientAmount == 123.msat) assert(send1.paymentHash == ByteVector32.Zeroes) assert(send1.invoice == invoice1) - assert(send1.assistedRoutes == hints) + assert(send1.extraEdges.length == 1) + assert(send1.extraEdges.head.asInstanceOf[BasicEdge].shortChannelId == ShortChannelId.fromCoordinates("569178x2331x1").success.value) + assert(send1.extraEdges.head.asInstanceOf[BasicEdge].sourceNodeId == Bob.nodeParams.nodeId) + assert(send1.extraEdges.head.asInstanceOf[BasicEdge].targetNodeId == nodePrivKey.publicKey) + assert(send1.extraEdges.head.asInstanceOf[BasicEdge].feeBase == 10.msat) + assert(send1.extraEdges.head.asInstanceOf[BasicEdge].feeProportionalMillionths == 1) + assert(send1.extraEdges.head.asInstanceOf[BasicEdge].cltvExpiryDelta == CltvExpiryDelta(12)) // with finalCltvExpiry val externalId2 = "487da196-a4dc-4b1e-92b4-3e5e905e9f3f" diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/fixtures/MinimalNodeFixture.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/fixtures/MinimalNodeFixture.scala index 4646f513a2..e1ca252d93 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/fixtures/MinimalNodeFixture.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/fixtures/MinimalNodeFixture.scala @@ -284,7 +284,7 @@ object MinimalNodeFixture extends Assertions { val invoice = sender.expectMsgType[Bolt11Invoice] val routeParams = node1.nodeParams.routerConf.pathFindingExperimentConf.experiments.values.head.getDefaultRouteParams - sender.send(node1.paymentInitiator, PaymentInitiator.SendPaymentToNode(amount, invoice, maxAttempts = 1, routeParams = routeParams, assistedRoutes = hints, blockUntilComplete = true)) + sender.send(node1.paymentInitiator, PaymentInitiator.SendPaymentToNode(amount, invoice, maxAttempts = 1, routeParams = routeParams, extraEdges = hints.flatMap(Bolt11Invoice.toExtraEdges(_, node2.nodeParams.nodeId)), blockUntilComplete = true)) sender.expectMsgType[PaymentSent] } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartHandlerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartHandlerSpec.scala index a4198eccee..e661906341 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartHandlerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartHandlerSpec.scala @@ -227,10 +227,10 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike val route_x_t = extraHop_x_t :: Nil sender.send(handlerWithMpp, ReceivePayment(Some(42000 msat), Left("1 coffee with additional routing info"), extraHops = List(route_x_z, route_x_t))) - assert(sender.expectMsgType[Invoice].routingInfo == Seq(route_x_z, route_x_t)) + assert(sender.expectMsgType[Bolt11Invoice].routingInfo == Seq(route_x_z, route_x_t)) sender.send(handlerWithMpp, ReceivePayment(Some(42000 msat), Left("1 coffee without routing info"))) - assert(sender.expectMsgType[Invoice].routingInfo == Nil) + assert(sender.expectMsgType[Bolt11Invoice].routingInfo == Nil) } test("PaymentHandler should reject incoming payments if the invoice is expired") { f => diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartPaymentLifecycleSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartPaymentLifecycleSpec.scala index 693d81028e..d3f8bb191f 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartPaymentLifecycleSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartPaymentLifecycleSpec.scala @@ -23,7 +23,7 @@ import fr.acinq.eclair._ import fr.acinq.eclair.channel.{ChannelUnavailable, HtlcsTimedoutDownstream, RemoteCannotAffordFeesForNewHtlc} import fr.acinq.eclair.crypto.Sphinx import fr.acinq.eclair.db.{FailureSummary, FailureType, OutgoingPaymentStatus} -import fr.acinq.eclair.payment.Bolt11Invoice.ExtraHop +import fr.acinq.eclair.payment.Invoice.BasicEdge import fr.acinq.eclair.payment.OutgoingPaymentPacket.Upstream import fr.acinq.eclair.payment.relay.Relayer.RelayFees import fr.acinq.eclair.payment.send.MultiPartPaymentLifecycle._ @@ -318,10 +318,10 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS import f._ // The B -> E channel is private and provided in the invoice routing hints. - val routingHint = ExtraHop(b, hop_be.shortChannelId, hop_be.params.relayFees.feeBase, hop_be.params.relayFees.feeProportionalMillionths, hop_be.params.cltvExpiryDelta) - val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount, expiry, 3, None, routeParams = routeParams, assistedRoutes = List(List(routingHint))) + val extraEdge = Invoice.BasicEdge(b, e, hop_be.shortChannelId, hop_be.params.relayFees.feeBase, hop_be.params.relayFees.feeProportionalMillionths, hop_be.params.cltvExpiryDelta) + val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount, expiry, 3, None, routeParams = routeParams, extraEdges = List(extraEdge)) sender.send(payFsm, payment) - assert(router.expectMsgType[RouteRequest].assistedRoutes.head.head == routingHint) + assert(router.expectMsgType[RouteRequest].extraEdges.head == extraEdge) val route = Route(finalAmount, hop_ab_1 :: hop_be :: Nil) router.send(payFsm, RouteResponse(Seq(route))) childPayFsm.expectMsgType[SendPaymentToRoute] @@ -332,17 +332,18 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS val childId = payFsm.stateData.asInstanceOf[PaymentProgress].pending.keys.head childPayFsm.send(payFsm, PaymentFailed(childId, paymentHash, Seq(RemoteFailure(route.amount, route.hops, Sphinx.DecryptedFailurePacket(b, FeeInsufficient(finalAmount, channelUpdate)))))) // We update the routing hints accordingly before requesting a new route. - assert(router.expectMsgType[RouteRequest].assistedRoutes.head.head == ExtraHop(b, channelUpdate.shortChannelId, 250 msat, 150, CltvExpiryDelta(24))) + val updatedExtraEdge = router.expectMsgType[RouteRequest].extraEdges.head + assert(updatedExtraEdge == BasicEdge(b, e, hop_be.shortChannelId, channelUpdate.feeBaseMsat, channelUpdate.feeProportionalMillionths, channelUpdate.cltvExpiryDelta)) } test("retry with ignored routing hints (temporary channel failure)") { f => import f._ // The B -> E channel is private and provided in the invoice routing hints. - val routingHint = ExtraHop(b, hop_be.shortChannelId, hop_be.params.relayFees.feeBase, hop_be.params.relayFees.feeProportionalMillionths, hop_be.params.cltvExpiryDelta) - val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount, expiry, 3, None, routeParams = routeParams, assistedRoutes = List(List(routingHint))) + val extraEdge = Invoice.BasicEdge(b, e, hop_be.shortChannelId, hop_be.params.relayFees.feeBase, hop_be.params.relayFees.feeProportionalMillionths, hop_be.params.cltvExpiryDelta) + val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount, expiry, 3, None, routeParams = routeParams, extraEdges = List(extraEdge)) sender.send(payFsm, payment) - assert(router.expectMsgType[RouteRequest].assistedRoutes.head.head == routingHint) + assert(router.expectMsgType[RouteRequest].extraEdges.head == extraEdge) val route = Route(finalAmount, hop_ab_1 :: hop_be :: Nil) router.send(payFsm, RouteResponse(Seq(route))) childPayFsm.expectMsgType[SendPaymentToRoute] @@ -356,14 +357,14 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS childPayFsm.send(payFsm, PaymentFailed(childId, paymentHash, Seq(RemoteFailure(route.amount, route.hops, Sphinx.DecryptedFailurePacket(b, TemporaryChannelFailure(channelUpdateBE1)))))) // We update the routing hints accordingly before requesting a new route and ignore the channel. val routeRequest = router.expectMsgType[RouteRequest] - assert(routeRequest.assistedRoutes.head.head == routingHint) + assert(routeRequest.extraEdges.head == extraEdge) assert(routeRequest.ignore.channels.map(_.shortChannelId) == Set(channelUpdateBE1.shortChannelId)) } test("update routing hints") { () => - val routingHints = Seq( - Seq(ExtraHop(a, ShortChannelId(1), 10 msat, 0, CltvExpiryDelta(12)), ExtraHop(b, ShortChannelId(2), 0 msat, 100, CltvExpiryDelta(24))), - Seq(ExtraHop(a, ShortChannelId(3), 1 msat, 10, CltvExpiryDelta(144))) + val extraEdges = Seq( + BasicEdge(a, b, ShortChannelId(1), 10 msat, 0, CltvExpiryDelta(12)), BasicEdge(b, c, ShortChannelId(2), 0 msat, 100, CltvExpiryDelta(24)), + BasicEdge(a, c, ShortChannelId(3), 1 msat, 10, CltvExpiryDelta(144)) ) def makeChannelUpdate(shortChannelId: ShortChannelId, feeBase: MilliSatoshi, feeProportional: Long, cltvExpiryDelta: CltvExpiryDelta): ChannelUpdate = { @@ -376,11 +377,11 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS RemoteFailure(finalAmount, Nil, Sphinx.DecryptedFailurePacket(b, FeeInsufficient(100 msat, makeChannelUpdate(ShortChannelId(2), 15 msat, 150, CltvExpiryDelta(48))))), UnreadableRemoteFailure(finalAmount, Nil) ) - val routingHints1 = Seq( - Seq(ExtraHop(a, ShortChannelId(1), 10 msat, 0, CltvExpiryDelta(12)), ExtraHop(b, ShortChannelId(2), 15 msat, 150, CltvExpiryDelta(48))), - Seq(ExtraHop(a, ShortChannelId(3), 1 msat, 10, CltvExpiryDelta(144))) + val extraEdges1 = Seq( + BasicEdge(a, b, ShortChannelId(1), 10 msat, 0, CltvExpiryDelta(12)), BasicEdge(b, c, ShortChannelId(2), 15 msat, 150, CltvExpiryDelta(48)), + BasicEdge(a, c, ShortChannelId(3), 1 msat, 10, CltvExpiryDelta(144)) ) - assert(routingHints1 == PaymentFailure.updateRoutingHints(failures, routingHints)) + assert(extraEdges1.zip(PaymentFailure.updateExtraEdges(failures, extraEdges)).forall{case (e1, e2) => e1 == e2}) } { val failures = Seq( @@ -389,11 +390,11 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS RemoteFailure(finalAmount, Nil, Sphinx.DecryptedFailurePacket(a, FeeInsufficient(100 msat, makeChannelUpdate(ShortChannelId(3), 22 msat, 22, CltvExpiryDelta(22))))), RemoteFailure(finalAmount, Nil, Sphinx.DecryptedFailurePacket(a, FeeInsufficient(100 msat, makeChannelUpdate(ShortChannelId(1), 23 msat, 23, CltvExpiryDelta(23))))), ) - val routingHints1 = Seq( - Seq(ExtraHop(a, ShortChannelId(1), 23 msat, 23, CltvExpiryDelta(23)), ExtraHop(b, ShortChannelId(2), 21 msat, 21, CltvExpiryDelta(21))), - Seq(ExtraHop(a, ShortChannelId(3), 22 msat, 22, CltvExpiryDelta(22))) + val extraEdges1 = Seq( + BasicEdge(a, b, ShortChannelId(1), 23 msat, 23, CltvExpiryDelta(23)), BasicEdge(b, c, ShortChannelId(2), 21 msat, 21, CltvExpiryDelta(21)), + BasicEdge(a, c, ShortChannelId(3), 22 msat, 22, CltvExpiryDelta(22)) ) - assert(routingHints1 == PaymentFailure.updateRoutingHints(failures, routingHints)) + assert(extraEdges1.zip(PaymentFailure.updateExtraEdges(failures, extraEdges)).forall{case (e1, e2) => e1 == e2}) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala index 13a45c188b..1ad2e17f6a 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala @@ -30,7 +30,7 @@ import fr.acinq.eclair.channel.fsm.Channel import fr.acinq.eclair.crypto.Sphinx import fr.acinq.eclair.db.{OutgoingPayment, OutgoingPaymentStatus, PaymentType} import fr.acinq.eclair.io.Peer.PeerRoutingMessage -import fr.acinq.eclair.payment.Bolt11Invoice.ExtraHop +import fr.acinq.eclair.payment.Invoice.BasicEdge import fr.acinq.eclair.payment.OutgoingPaymentPacket.Upstream import fr.acinq.eclair.payment.PaymentSent.PartialPayment import fr.acinq.eclair.payment.relay.Relayer.RelayFees @@ -195,11 +195,11 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val recipient = randomKey().publicKey val route = PredefinedNodeRoute(Seq(a, b, c, recipient)) - val routingHint = Seq(Seq(ExtraHop(c, ShortChannelId(561), 1 msat, 100, CltvExpiryDelta(144)))) - val request = SendPaymentToRoute(sender.ref, Left(route), PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata), routingHint) + val extraEdges = Seq(BasicEdge(c, recipient, ShortChannelId(561), 1 msat, 100, CltvExpiryDelta(144))) + val request = SendPaymentToRoute(sender.ref, Left(route), PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata), extraEdges) sender.send(paymentFSM, request) - routerForwarder.expectMsg(FinalizeRoute(defaultAmountMsat, route, routingHint, paymentContext = Some(cfg.paymentContext))) + routerForwarder.expectMsg(FinalizeRoute(defaultAmountMsat, route, extraEdges, paymentContext = Some(cfg.paymentContext))) val Transition(_, WAITING_FOR_REQUEST, WAITING_FOR_ROUTE) = monitor.expectMsgClass(classOf[Transition[_]]) routerForwarder.forward(routerFixture.router) @@ -565,17 +565,17 @@ class PaymentLifecycleSpec extends BaseRouterSpec { import cfg._ // we build an assisted route for channel bc and cd - val assistedRoutes = Seq(Seq( - ExtraHop(b, scid_bc, update_bc.feeBaseMsat, update_bc.feeProportionalMillionths, update_bc.cltvExpiryDelta), - ExtraHop(c, scid_cd, update_cd.feeBaseMsat, update_cd.feeProportionalMillionths, update_cd.cltvExpiryDelta) - )) + val extraEdges = Seq( + BasicEdge(b, c, scid_bc, update_bc.feeBaseMsat, update_bc.feeProportionalMillionths, update_bc.cltvExpiryDelta), + BasicEdge(c, d, scid_cd, update_cd.feeBaseMsat, update_cd.feeProportionalMillionths, update_cd.cltvExpiryDelta) + ) - val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata), 5, assistedRoutes = assistedRoutes, routeParams = defaultRouteParams) + val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata), 5, extraEdges = extraEdges, routeParams = defaultRouteParams) sender.send(paymentFSM, request) awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.Pending)) val WaitingForRoute(_, Nil, _) = paymentFSM.stateData - routerForwarder.expectMsg(defaultRouteRequest(nodeParams.nodeId, d, cfg).copy(assistedRoutes = assistedRoutes)) + routerForwarder.expectMsg(defaultRouteRequest(nodeParams.nodeId, d, cfg).copy(extraEdges = extraEdges)) routerForwarder.forward(routerFixture.router) awaitCond(paymentFSM.stateName == WAITING_FOR_PAYMENT_COMPLETE) val WaitingForComplete(_, cmd1, Nil, sharedSecrets1, _, _) = paymentFSM.stateData @@ -590,11 +590,11 @@ class PaymentLifecycleSpec extends BaseRouterSpec { // payment lifecycle forwards the embedded channelUpdate to the router routerForwarder.expectMsg(channelUpdate_bc_modified) awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.Pending)) // 1 failure but not final, the payment is still PENDING - val assistedRoutes1 = Seq(Seq( - ExtraHop(b, scid_bc, update_bc.feeBaseMsat, update_bc.feeProportionalMillionths, channelUpdate_bc_modified.cltvExpiryDelta), - ExtraHop(c, scid_cd, update_cd.feeBaseMsat, update_cd.feeProportionalMillionths, update_cd.cltvExpiryDelta) - )) - routerForwarder.expectMsg(defaultRouteRequest(nodeParams.nodeId, d, cfg).copy(assistedRoutes = assistedRoutes1)) + val extraEdges1 = Seq( + extraEdges(0).asInstanceOf[BasicEdge].update(channelUpdate_bc_modified), + extraEdges(1) + ) + routerForwarder.expectMsg(defaultRouteRequest(nodeParams.nodeId, d, cfg).copy(extraEdges = extraEdges1)) routerForwarder.forward(routerFixture.router) // router answers with a new route, taking into account the new update @@ -610,12 +610,12 @@ class PaymentLifecycleSpec extends BaseRouterSpec { import cfg._ // we build an assisted route for channel cd - val assistedRoutes = Seq(Seq(ExtraHop(c, scid_cd, update_cd.feeBaseMsat, update_cd.feeProportionalMillionths, update_cd.cltvExpiryDelta))) - val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata), 1, assistedRoutes = assistedRoutes, routeParams = defaultRouteParams) + val extraEdges = Seq(BasicEdge(c, d, scid_cd, update_cd.feeBaseMsat, update_cd.feeProportionalMillionths, update_cd.cltvExpiryDelta)) + val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata), 1, extraEdges = extraEdges, routeParams = defaultRouteParams) sender.send(paymentFSM, request) awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.Pending)) - routerForwarder.expectMsg(defaultRouteRequest(nodeParams.nodeId, d, cfg).copy(assistedRoutes = assistedRoutes)) + routerForwarder.expectMsg(defaultRouteRequest(nodeParams.nodeId, d, cfg).copy(extraEdges = extraEdges)) routerForwarder.forward(routerFixture.router) awaitCond(paymentFSM.stateName == WAITING_FOR_PAYMENT_COMPLETE) val WaitingForComplete(_, cmd1, Nil, sharedSecrets1, _, _) = paymentFSM.stateData diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/NodeRelayerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/NodeRelayerSpec.scala index 26a76adf64..a1796ed265 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/NodeRelayerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/NodeRelayerSpec.scala @@ -29,6 +29,7 @@ import fr.acinq.eclair.Features.{BasicMultiPartPayment, PaymentSecret, VariableL import fr.acinq.eclair.channel.{CMD_FAIL_HTLC, CMD_FULFILL_HTLC, Register} import fr.acinq.eclair.crypto.Sphinx import fr.acinq.eclair.payment.Bolt11Invoice.ExtraHop +import fr.acinq.eclair.payment.Invoice.BasicEdge import fr.acinq.eclair.payment.OutgoingPaymentPacket.Upstream import fr.acinq.eclair.payment._ import fr.acinq.eclair.payment.relay.NodeRelayer.PaymentKey @@ -566,9 +567,9 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl import f._ // Receive an upstream multi-part payment. - val hints = List(List(ExtraHop(outgoingNodeId, ShortChannelId(42), feeBase = 10 msat, feeProportionalMillionths = 1, cltvExpiryDelta = CltvExpiryDelta(12)))) + val hints = List(ExtraHop(randomKey().publicKey, ShortChannelId(42), feeBase = 10 msat, feeProportionalMillionths = 1, cltvExpiryDelta = CltvExpiryDelta(12))) val features = Features[InvoiceFeature](VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory, BasicMultiPartPayment -> Optional) - val invoice = Bolt11Invoice(Block.LivenetGenesisBlock.hash, Some(outgoingAmount * 3), paymentHash, randomKey(), Left("Some invoice"), CltvExpiryDelta(18), extraHops = hints, paymentMetadata = Some(hex"123456"), features = features) + val invoice = Bolt11Invoice(Block.LivenetGenesisBlock.hash, Some(outgoingAmount * 3), paymentHash, outgoingNodeKey, Left("Some invoice"), CltvExpiryDelta(18), extraHops = List(hints), paymentMetadata = Some(hex"123456"), features = features) val incomingPayments = incomingMultiPart.map(incoming => incoming.copy(innerPayload = PaymentOnion.createNodeRelayToNonTrampolinePayload( incoming.innerPayload.amountToForward, outgoingAmount * 3, outgoingExpiry, outgoingNodeId, invoice ))) @@ -584,7 +585,12 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl assert(outgoingPayment.targetExpiry == outgoingExpiry) assert(outgoingPayment.targetNodeId == outgoingNodeId) assert(outgoingPayment.additionalTlvs == Nil) - assert(outgoingPayment.assistedRoutes == hints) + assert(outgoingPayment.extraEdges.head.asInstanceOf[BasicEdge].shortChannelId == ShortChannelId(42)) + assert(outgoingPayment.extraEdges.head.asInstanceOf[BasicEdge].sourceNodeId == hints.head.nodeId) + assert(outgoingPayment.extraEdges.head.asInstanceOf[BasicEdge].targetNodeId == outgoingNodeId) + assert(outgoingPayment.extraEdges.head.asInstanceOf[BasicEdge].feeBase == 10.msat) + assert(outgoingPayment.extraEdges.head.asInstanceOf[BasicEdge].feeProportionalMillionths == 1) + assert(outgoingPayment.extraEdges.head.asInstanceOf[BasicEdge].cltvExpiryDelta == CltvExpiryDelta(12)) // those are adapters for pay-fsm messages val nodeRelayerAdapters = outgoingPayment.replyTo @@ -608,8 +614,8 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl import f._ // Receive an upstream multi-part payment. - val hints = List(List(ExtraHop(outgoingNodeId, ShortChannelId(42), feeBase = 10 msat, feeProportionalMillionths = 1, cltvExpiryDelta = CltvExpiryDelta(12)))) - val invoice = Bolt11Invoice(Block.LivenetGenesisBlock.hash, Some(outgoingAmount), paymentHash, randomKey(), Left("Some invoice"), CltvExpiryDelta(18), extraHops = hints, paymentMetadata = Some(hex"123456")) + val hints = List(ExtraHop(randomKey().publicKey, ShortChannelId(42), feeBase = 10 msat, feeProportionalMillionths = 1, cltvExpiryDelta = CltvExpiryDelta(12))) + val invoice = Bolt11Invoice(Block.LivenetGenesisBlock.hash, Some(outgoingAmount), paymentHash, outgoingNodeKey, Left("Some invoice"), CltvExpiryDelta(18), extraHops = List(hints), paymentMetadata = Some(hex"123456")) assert(!invoice.features.hasFeature(BasicMultiPartPayment)) val incomingPayments = incomingMultiPart.map(incoming => incoming.copy(innerPayload = PaymentOnion.createNodeRelayToNonTrampolinePayload( incoming.innerPayload.amountToForward, incoming.innerPayload.amountToForward, outgoingExpiry, outgoingNodeId, invoice @@ -624,7 +630,13 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl assert(outgoingPayment.finalPayload.expiry == outgoingExpiry) assert(outgoingPayment.finalPayload.paymentMetadata == invoice.paymentMetadata) // we should use the provided metadata assert(outgoingPayment.targetNodeId == outgoingNodeId) - assert(outgoingPayment.assistedRoutes == hints) + assert(outgoingPayment.extraEdges.head.asInstanceOf[BasicEdge].shortChannelId == ShortChannelId(42)) + assert(outgoingPayment.extraEdges.head.asInstanceOf[BasicEdge].sourceNodeId == hints.head.nodeId) + assert(outgoingPayment.extraEdges.head.asInstanceOf[BasicEdge].targetNodeId == outgoingNodeId) + assert(outgoingPayment.extraEdges.head.asInstanceOf[BasicEdge].feeBase == 10.msat) + assert(outgoingPayment.extraEdges.head.asInstanceOf[BasicEdge].feeProportionalMillionths == 1) + assert(outgoingPayment.extraEdges.head.asInstanceOf[BasicEdge].cltvExpiryDelta == CltvExpiryDelta(12)) + // those are adapters for pay-fsm messages val nodeRelayerAdapters = outgoingPayment.replyTo @@ -680,7 +692,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl assert(outgoingPayment.targetExpiry == outgoingExpiry) assert(outgoingPayment.targetNodeId == outgoingNodeId) assert(outgoingPayment.additionalTlvs == Seq(OnionPaymentPayloadTlv.TrampolineOnion(nextTrampolinePacket))) - assert(outgoingPayment.assistedRoutes == Nil) + assert(outgoingPayment.extraEdges == Nil) } def validateRelayEvent(e: TrampolinePaymentRelayed): Unit = { @@ -704,7 +716,8 @@ object NodeRelayerSpec { val outgoingAmount = 4000000 msat val outgoingExpiry = CltvExpiry(490000) - val outgoingNodeId = randomKey().publicKey + val outgoingNodeKey = randomKey() + val outgoingNodeId = outgoingNodeKey.publicKey val incomingAmount = 5000000 msat val incomingSecret = randomBytes32() diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/BalanceEstimateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/BalanceEstimateSpec.scala index 4e14e981b6..8c4e6371c4 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/BalanceEstimateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/BalanceEstimateSpec.scala @@ -18,7 +18,7 @@ package fr.acinq.eclair.router import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.bitcoin.scalacompat.{Satoshi, SatoshiLong} -import fr.acinq.eclair.payment.Bolt11Invoice.ExtraHop +import fr.acinq.eclair.payment.Invoice import fr.acinq.eclair.router.Graph.GraphStructure.{DirectedGraph, GraphEdge} import fr.acinq.eclair.router.Router.{ChannelDesc, ChannelRelayParams} import fr.acinq.eclair.{CltvExpiryDelta, MilliSatoshiLong, ShortChannelId, TimestampSecond, randomKey} @@ -38,7 +38,7 @@ class BalanceEstimateSpec extends AnyFunSuite { def makeEdge(nodeId1: PublicKey, nodeId2: PublicKey, channelId: Long, capacity: Satoshi): GraphEdge = GraphEdge( ChannelDesc(ShortChannelId(channelId), nodeId1, nodeId2), - ChannelRelayParams.FromHint(ExtraHop(nodeId1, ShortChannelId(channelId), 0 msat, 0, CltvExpiryDelta(0)), 0 msat), + ChannelRelayParams.FromHint(Invoice.BasicEdge(nodeId1, nodeId2, ShortChannelId(channelId), 0 msat, 0, CltvExpiryDelta(0))), capacity, None) def makeEdge(channelId: Long, capacity: Satoshi): GraphEdge = diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/RouteCalculationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/RouteCalculationSpec.scala index b57d1759f5..338b3f3ea6 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/RouteCalculationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/RouteCalculationSpec.scala @@ -444,28 +444,6 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { assert(route.hops == channelHopFromUpdate(a, b, uab) :: channelHopFromUpdate(b, c, ubc) :: channelHopFromUpdate(c, d, ucd) :: channelHopFromUpdate(d, e, ude) :: Nil) } - test("convert extra hops to assisted channels") { - val a = randomKey().publicKey - val b = randomKey().publicKey - val c = randomKey().publicKey - val d = randomKey().publicKey - val e = randomKey().publicKey - - val extraHop1 = ExtraHop(a, ShortChannelId(1), 12.sat.toMilliSatoshi, 10000, CltvExpiryDelta(12)) - val extraHop2 = ExtraHop(b, ShortChannelId(2), 200.sat.toMilliSatoshi, 0, CltvExpiryDelta(22)) - val extraHop3 = ExtraHop(c, ShortChannelId(3), 150.sat.toMilliSatoshi, 0, CltvExpiryDelta(32)) - val extraHop4 = ExtraHop(d, ShortChannelId(4), 50.sat.toMilliSatoshi, 0, CltvExpiryDelta(42)) - val extraHops = extraHop1 :: extraHop2 :: extraHop3 :: extraHop4 :: Nil - - val amount = 90000 sat // below RoutingHeuristics.CAPACITY_CHANNEL_LOW - val assistedChannels = toAssistedChannels(extraHops, e, amount.toMilliSatoshi) - - assert(assistedChannels(extraHop4.shortChannelId) == AssistedChannel(e, ChannelRelayParams.FromHint(extraHop4, 100050.sat.toMilliSatoshi))) - assert(assistedChannels(extraHop3.shortChannelId) == AssistedChannel(d, ChannelRelayParams.FromHint(extraHop3, 100200.sat.toMilliSatoshi))) - assert(assistedChannels(extraHop2.shortChannelId) == AssistedChannel(c, ChannelRelayParams.FromHint(extraHop2, 100400.sat.toMilliSatoshi))) - assert(assistedChannels(extraHop1.shortChannelId) == AssistedChannel(b, ChannelRelayParams.FromHint(extraHop1, 101416.sat.toMilliSatoshi))) - } - test("blacklist routes") { val g = DirectedGraph(List( makeEdge(1L, a, b, 0 msat, 0), diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/RouterSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/RouterSpec.scala index fcd54e96cc..a69736bdc9 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/RouterSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/RouterSpec.scala @@ -27,9 +27,11 @@ import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._ import fr.acinq.eclair.channel.{AvailableBalanceChanged, CommitmentsSpec, LocalChannelUpdate, RealScidStatus, ShortIds} import fr.acinq.eclair.crypto.TransportHandler import fr.acinq.eclair.io.Peer.PeerRoutingMessage +import fr.acinq.eclair.payment.{Bolt11Invoice, Invoice} import fr.acinq.eclair.payment.Bolt11Invoice.ExtraHop import fr.acinq.eclair.router.Announcements.{makeChannelUpdate, makeNodeAnnouncement} import fr.acinq.eclair.router.BaseRouterSpec.channelAnnouncement +import fr.acinq.eclair.router.Graph.GraphStructure.GraphEdge import fr.acinq.eclair.router.Graph.RoutingHeuristics import fr.acinq.eclair.router.RouteCalculationSpec.{DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, DEFAULT_ROUTE_PARAMS} import fr.acinq.eclair.router.Router._ @@ -359,7 +361,7 @@ class RouterSpec extends BaseRouterSpec { val extraHop_cx = ExtraHop(c, ShortChannelId(1), 10 msat, 11, CltvExpiryDelta(12)) val extraHop_xy = ExtraHop(x, ShortChannelId(2), 10 msat, 11, CltvExpiryDelta(12)) val extraHop_yz = ExtraHop(y, ShortChannelId(3), 20 msat, 21, CltvExpiryDelta(22)) - sender.send(router, RouteRequest(a, z, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, assistedRoutes = Seq(extraHop_cx :: extraHop_xy :: extraHop_yz :: Nil), routeParams = DEFAULT_ROUTE_PARAMS)) + sender.send(router, RouteRequest(a, z, DEFAULT_AMOUNT_MSAT, DEFAULT_MAX_FEE, extraEdges = Bolt11Invoice.toExtraEdges(extraHop_cx :: extraHop_xy :: extraHop_yz :: Nil, z), routeParams = DEFAULT_ROUTE_PARAMS)) val res = sender.expectMsgType[RouteResponse] assert(res.routes.head.hops.map(_.nodeId).toList == a :: b :: c :: x :: y :: Nil) assert(res.routes.head.hops.last.nextNodeId == z) @@ -524,34 +526,34 @@ class RouterSpec extends BaseRouterSpec { val targetNodeId = randomKey().publicKey { - val invoiceRoutingHint = ExtraHop(b, RealShortChannelId(BlockHeight(420000), 516, 1105), 10 msat, 150, CltvExpiryDelta(96)) + val invoiceRoutingHint = Invoice.BasicEdge(b, targetNodeId, RealShortChannelId(BlockHeight(420000), 516, 1105), 10 msat, 150, CltvExpiryDelta(96)) val preComputedRoute = PredefinedChannelRoute(targetNodeId, Seq(scid_ab, invoiceRoutingHint.shortChannelId)) val amount = 10_000.msat // the amount affects the way we estimate the channel capacity of the hinted channel assert(amount < RoutingHeuristics.CAPACITY_CHANNEL_LOW) - sender.send(router, FinalizeRoute(amount, preComputedRoute, assistedRoutes = Seq(Seq(invoiceRoutingHint)))) + sender.send(router, FinalizeRoute(amount, preComputedRoute, extraEdges = Seq(invoiceRoutingHint))) val response = sender.expectMsgType[RouteResponse] assert(response.routes.length == 1) val route = response.routes.head assert(route.hops.map(_.nodeId) == Seq(a, b)) assert(route.hops.map(_.nextNodeId) == Seq(b, targetNodeId)) assert(route.hops.head.params == ChannelRelayParams.FromAnnouncement(update_ab)) - assert(route.hops.last.params == ChannelRelayParams.FromHint(invoiceRoutingHint, RoutingHeuristics.CAPACITY_CHANNEL_LOW + nodeFee(invoiceRoutingHint.feeBase, invoiceRoutingHint.feeProportionalMillionths, RoutingHeuristics.CAPACITY_CHANNEL_LOW))) + assert(route.hops.last.params == ChannelRelayParams.FromHint(invoiceRoutingHint)) } { - val invoiceRoutingHint = ExtraHop(h, RealShortChannelId(BlockHeight(420000), 516, 1105), 10 msat, 150, CltvExpiryDelta(96)) + val invoiceRoutingHint = Invoice.BasicEdge(h, targetNodeId, RealShortChannelId(BlockHeight(420000), 516, 1105), 10 msat, 150, CltvExpiryDelta(96)) val preComputedRoute = PredefinedChannelRoute(targetNodeId, Seq(scid_ag_private, scid_gh, invoiceRoutingHint.shortChannelId)) val amount = RoutingHeuristics.CAPACITY_CHANNEL_LOW * 2 // the amount affects the way we estimate the channel capacity of the hinted channel assert(amount > RoutingHeuristics.CAPACITY_CHANNEL_LOW) - sender.send(router, FinalizeRoute(amount, preComputedRoute, assistedRoutes = Seq(Seq(invoiceRoutingHint)))) + sender.send(router, FinalizeRoute(amount, preComputedRoute, extraEdges = Seq(invoiceRoutingHint))) val response = sender.expectMsgType[RouteResponse] assert(response.routes.length == 1) val route = response.routes.head assert(route.hops.map(_.nodeId) == Seq(a, g, h)) assert(route.hops.map(_.nextNodeId) == Seq(g, h, targetNodeId)) assert(route.hops.map(_.params).dropRight(1) == Seq(ChannelRelayParams.FromAnnouncement(update_ag_private), ChannelRelayParams.FromAnnouncement(update_gh))) - assert(route.hops.last.params == ChannelRelayParams.FromHint(invoiceRoutingHint, amount + nodeFee(invoiceRoutingHint.feeBase, invoiceRoutingHint.feeProportionalMillionths, amount))) + assert(route.hops.last.params == ChannelRelayParams.FromHint(invoiceRoutingHint)) } } diff --git a/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/PathFinding.scala b/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/PathFinding.scala index 163db4fed1..e8241d7372 100644 --- a/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/PathFinding.scala +++ b/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/PathFinding.scala @@ -21,7 +21,6 @@ import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.eclair.api.Service import fr.acinq.eclair.api.directives.{EclairDirectives, RouteFormat} import fr.acinq.eclair.api.serde.FormParamExtractors._ -import fr.acinq.eclair.payment.Bolt11Invoice import scala.concurrent.ExecutionContext @@ -34,10 +33,10 @@ trait PathFinding { val findRoute: Route = postRequest("findroute") { implicit t => formFields(invoiceFormParam, amountMsatFormParam.?, "pathFindingExperimentName".?, routeFormatFormParam.?, "includeLocalChannelCost".as[Boolean].?, ignoreNodeIdsFormParam.?, ignoreShortChannelIdsFormParam.?, maxFeeMsatFormParam.?) { - case (invoice@Bolt11Invoice(_, Some(amount), _, nodeId, _, _), None, pathFindingExperimentName_opt, routeFormat_opt, includeLocalChannelCost_opt, ignoreNodeIds_opt, ignoreChannels_opt, maxFee_opt) => - complete(eclairApi.findRoute(nodeId, amount, pathFindingExperimentName_opt, invoice.routingInfo, includeLocalChannelCost_opt.getOrElse(false), ignoreNodeIds = ignoreNodeIds_opt.getOrElse(Nil), ignoreShortChannelIds = ignoreChannels_opt.getOrElse(Nil), maxFee_opt = maxFee_opt).map(r => RouteFormat.format(r, routeFormat_opt))) + case (invoice, None, pathFindingExperimentName_opt, routeFormat_opt, includeLocalChannelCost_opt, ignoreNodeIds_opt, ignoreChannels_opt, maxFee_opt) if invoice.amount_opt.nonEmpty => + complete(eclairApi.findRoute(invoice.nodeId, invoice.amount_opt.get, pathFindingExperimentName_opt, invoice.extraEdges, includeLocalChannelCost_opt.getOrElse(false), ignoreNodeIds = ignoreNodeIds_opt.getOrElse(Nil), ignoreShortChannelIds = ignoreChannels_opt.getOrElse(Nil), maxFee_opt = maxFee_opt).map(r => RouteFormat.format(r, routeFormat_opt))) case (invoice, Some(overrideAmount), pathFindingExperimentName_opt, routeFormat_opt, includeLocalChannelCost_opt, ignoreNodeIds_opt, ignoreChannels_opt, maxFee_opt) => - complete(eclairApi.findRoute(invoice.nodeId, overrideAmount, pathFindingExperimentName_opt, invoice.routingInfo, includeLocalChannelCost_opt.getOrElse(false), ignoreNodeIds = ignoreNodeIds_opt.getOrElse(Nil), ignoreShortChannelIds = ignoreChannels_opt.getOrElse(Nil), maxFee_opt = maxFee_opt).map(r => RouteFormat.format(r, routeFormat_opt))) + complete(eclairApi.findRoute(invoice.nodeId, overrideAmount, pathFindingExperimentName_opt, invoice.extraEdges, includeLocalChannelCost_opt.getOrElse(false), ignoreNodeIds = ignoreNodeIds_opt.getOrElse(Nil), ignoreShortChannelIds = ignoreChannels_opt.getOrElse(Nil), maxFee_opt = maxFee_opt).map(r => RouteFormat.format(r, routeFormat_opt))) case _ => reject(MalformedFormFieldRejection( "invoice", "The invoice must have an amount or you need to specify one using 'amountMsat'" )) From 8af14ac515f9c01a4b6709720aec1b778aabac90 Mon Sep 17 00:00:00 2001 From: Pierre-Marie Padiou Date: Wed, 29 Jun 2022 16:11:05 +0200 Subject: [PATCH 106/121] Backport from feature branches (#2326) * improve bitcoin client metrics * only use batching bitcoin client for the watcher * use named args for defaultFromFeatures method * use default min_depth in edge case scenario * add OutgoingChannelParams trait * update amounts in ChannelRelayerSpec * make register take typed replyTo * improve codecs non-reg tests --- eclair-core/src/main/resources/reference.conf | 5 +- .../main/scala/fr/acinq/eclair/Eclair.scala | 4 +- .../scala/fr/acinq/eclair/NodeParams.scala | 3 +- .../main/scala/fr/acinq/eclair/Setup.scala | 16 +- .../rpc/BasicBitcoinJsonRPCClient.scala | 3 +- .../fr/acinq/eclair/channel/Helpers.scala | 4 +- .../fr/acinq/eclair/channel/Register.scala | 28 +-- .../fr/acinq/eclair/channel/fsm/Channel.scala | 12 +- .../acinq/eclair/db/PendingCommandsDb.scala | 2 +- .../main/scala/fr/acinq/eclair/io/Peer.scala | 6 +- .../eclair/payment/relay/ChannelRelay.scala | 8 +- .../acinq/eclair/payment/relay/Relayer.scala | 7 +- .../payment/send/PaymentLifecycle.scala | 3 +- .../fundee-announced/data.bin | 1 + .../fundee-announced/data.json | 150 ++++++++++++++++ .../codecs/000003-DATA_NORMAL/funder/data.bin | 1 + .../000003-DATA_NORMAL/funder/data.json | 130 ++++++++++++++ .../funder-announced/data.bin | 1 + .../funder-announced/data.json | 164 ++++++++++++++++++ .../fr/acinq/eclair/EclairImplSpec.scala | 22 +-- .../eclair/channel/ChannelFeaturesSpec.scala | 2 +- .../acinq/eclair/channel/RegisterSpec.scala | 2 +- .../ChannelStateTestsHelperMethods.scala | 2 +- .../integration/ChannelIntegrationSpec.scala | 55 +++--- .../integration/PaymentIntegrationSpec.scala | 8 +- .../basic/fixtures/MinimalNodeFixture.scala | 8 +- .../eclair/payment/MultiPartHandlerSpec.scala | 22 +-- .../eclair/payment/PaymentLifecycleSpec.scala | 29 ++-- .../payment/PostRestartHtlcCleanerSpec.scala | 16 +- .../payment/relay/ChannelRelayerSpec.scala | 30 ++-- .../internal/channel/ChannelCodecsSpec.scala | 76 +++++--- 31 files changed, 663 insertions(+), 157 deletions(-) create mode 100644 eclair-core/src/test/resources/nonreg/codecs/000003-DATA_NORMAL/fundee-announced/data.bin create mode 100644 eclair-core/src/test/resources/nonreg/codecs/000003-DATA_NORMAL/fundee-announced/data.json create mode 100644 eclair-core/src/test/resources/nonreg/codecs/000003-DATA_NORMAL/funder/data.bin create mode 100644 eclair-core/src/test/resources/nonreg/codecs/000003-DATA_NORMAL/funder/data.json create mode 100644 eclair-core/src/test/resources/nonreg/codecs/020002-DATA_NORMAL/funder-announced/data.bin create mode 100644 eclair-core/src/test/resources/nonreg/codecs/020002-DATA_NORMAL/funder-announced/data.json diff --git a/eclair-core/src/main/resources/reference.conf b/eclair-core/src/main/resources/reference.conf index 1ee6a20b28..1350bffb9c 100644 --- a/eclair-core/src/main/resources/reference.conf +++ b/eclair-core/src/main/resources/reference.conf @@ -29,9 +29,10 @@ eclair { wallet = "" zmqblock = "tcp://127.0.0.1:29000" zmqtx = "tcp://127.0.0.1:29000" - // Batching requests saves bandwidth but may slightly degrade latency and reliability. + // Batching requests saves bandwidth but may slightly degrade latency and reliability. It is useful for the watcher, + // which can generate a lot of requests to validate channels, iterate over blocks to find a spending tx, etc. // You may want to disable this when bitcoin is running on a remote machine with an unreliable network. - batch-requests = true + batch-watcher-requests = true } node-alias = "eclair" diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala index 492c6d1b0d..d8926d7d91 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala @@ -449,8 +449,8 @@ class EclairImpl(appKit: Kit) extends Eclair with Logging { * @param channel either a shortChannelId (BOLT encoded) or a channelId (32-byte hex encoded). */ private def sendToChannel[C <: Command, R <: CommandResponse[C]](channel: ApiTypes.ChannelIdentifier, request: C)(implicit timeout: Timeout): Future[R] = (channel match { - case Left(channelId) => appKit.register ? Register.Forward(ActorRef.noSender, channelId, request) - case Right(shortChannelId) => appKit.register ? Register.ForwardShortId(ActorRef.noSender, shortChannelId, request) + case Left(channelId) => appKit.register ? Register.Forward(null, channelId, request) + case Right(shortChannelId) => appKit.register ? Register.ForwardShortId(null, shortChannelId, request) }).map { case t: R@unchecked => t case t: Register.ForwardFailure[C]@unchecked => throw ChannelNotFound(Left(t.fwd.channelId)) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala index 7a7fdbb066..62fb21650c 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala @@ -244,7 +244,8 @@ object NodeParams extends Logging { // v0.7.1 "payment-request-expiry" -> "invoice-expiry", "override-features" -> "override-init-features", - "channel.min-funding-satoshis" -> "channel.min-public-funding-satoshis, channel.min-private-funding-satoshis" + "channel.min-funding-satoshis" -> "channel.min-public-funding-satoshis, channel.min-private-funding-satoshis", + "bitcoind.batch-requests" -> "bitcoind.batch-watcher-requests" ) deprecatedKeyPaths.foreach { case (old, new_) => require(!config.hasPath(old), s"configuration key '$old' has been replaced by '$new_'") diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala index 2b21c48c09..643a93eb29 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala @@ -254,17 +254,17 @@ class Setup(val datadir: File, }) _ <- feeratesRetrieved.future - bitcoinClient = config.getBoolean("bitcoind.batch-requests") match { - case true => - new BitcoinCoreClient(new BatchingBitcoinJsonRPCClient(bitcoin)) - case _ => - new BitcoinCoreClient(bitcoin) - } + bitcoinClient = new BitcoinCoreClient(bitcoin) watcher = { system.actorOf(SimpleSupervisor.props(Props(new ZMQActor(config.getString("bitcoind.zmqblock"), ZMQActor.Topics.HashBlock, Some(zmqBlockConnected))), "zmqblock", SupervisorStrategy.Restart)) system.actorOf(SimpleSupervisor.props(Props(new ZMQActor(config.getString("bitcoind.zmqtx"), ZMQActor.Topics.RawTx, Some(zmqTxConnected))), "zmqtx", SupervisorStrategy.Restart)) - system.spawn(Behaviors.supervise(ZmqWatcher(nodeParams, blockHeight, bitcoinClient)).onFailure(typed.SupervisorStrategy.resume), "watcher") + val watcherBitcoinClient = if (config.getBoolean("bitcoind.batch-watcher-requests")) { + new BitcoinCoreClient(new BatchingBitcoinJsonRPCClient(bitcoin)) + } else { + new BitcoinCoreClient(bitcoin) + } + system.spawn(Behaviors.supervise(ZmqWatcher(nodeParams, blockHeight, watcherBitcoinClient)).onFailure(typed.SupervisorStrategy.resume), "watcher") } router = system.actorOf(SimpleSupervisor.props(Router.props(nodeParams, watcher, Some(routerInitialized)), "router", SupervisorStrategy.Resume)) @@ -293,7 +293,7 @@ class Setup(val datadir: File, system.deadLetters } dbEventHandler = system.actorOf(SimpleSupervisor.props(DbEventHandler.props(nodeParams), "db-event-handler", SupervisorStrategy.Resume)) - register = system.actorOf(SimpleSupervisor.props(Props(new Register), "register", SupervisorStrategy.Resume)) + register = system.actorOf(SimpleSupervisor.props(Register.props(), "register", SupervisorStrategy.Resume)) paymentHandler = system.actorOf(SimpleSupervisor.props(PaymentHandler.props(nodeParams, register), "payment-handler", SupervisorStrategy.Resume)) relayer = system.actorOf(SimpleSupervisor.props(Relayer.props(nodeParams, router, register, paymentHandler, Some(postRestartCleanUpInitialized)), "relayer", SupervisorStrategy.Resume)) // Before initializing the switchboard (which re-connects us to the network) and the user-facing parts of the system, diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/rpc/BasicBitcoinJsonRPCClient.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/rpc/BasicBitcoinJsonRPCClient.scala index 8e3e41fc13..3cb147f2d3 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/rpc/BasicBitcoinJsonRPCClient.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/rpc/BasicBitcoinJsonRPCClient.scala @@ -56,7 +56,8 @@ class BasicBitcoinJsonRPCClient(rpcAuthMethod: BitcoinJsonRPCAuthMethod, host: S requests.groupBy(_.method).foreach { case (method, calls) => Metrics.RpcBasicInvokeCount.withTag(Tags.Method, method).increment(calls.size) } - KamonExt.timeFuture(Metrics.RpcBasicInvokeDuration.withoutTags()) { + // for the duration metric, we use a "mixed" method for batched requests + KamonExt.timeFuture(Metrics.RpcBasicInvokeDuration.withTag(Tags.Method, if (requests.size == 1) requests.head.method else "mixed")) { for { response <- basicRequest .post(serviceUri) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala index 8bc231e0d9..357f418a62 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala @@ -148,10 +148,10 @@ object Helpers { case None if Features.canUseFeature(localFeatures, remoteFeatures, Features.ChannelType) => // Bolt 2: if `option_channel_type` is negotiated: MUST set `channel_type` return Left(MissingChannelType(open.temporaryChannelId)) - case None if channelType != ChannelTypes.defaultFromFeatures(localFeatures, remoteFeatures, open.channelFlags.announceChannel) => + case None if channelType != ChannelTypes.defaultFromFeatures(localFeatures, remoteFeatures, announceChannel = open.channelFlags.announceChannel) => // If we have overridden the default channel type, but they didn't support explicit channel type negotiation, // we need to abort because they expect a different channel type than what we offered. - return Left(InvalidChannelType(open.temporaryChannelId, channelType, ChannelTypes.defaultFromFeatures(localFeatures, remoteFeatures, open.channelFlags.announceChannel))) + return Left(InvalidChannelType(open.temporaryChannelId, channelType, ChannelTypes.defaultFromFeatures(localFeatures, remoteFeatures, announceChannel = open.channelFlags.announceChannel))) case _ => // we agree on channel type } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Register.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Register.scala index df053325c1..5dad716c18 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Register.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Register.scala @@ -16,7 +16,8 @@ package fr.acinq.eclair.channel -import akka.actor.{Actor, ActorLogging, ActorRef, Terminated} +import akka.actor.typed.scaladsl.adapter.TypedActorRefOps +import akka.actor.{Actor, ActorLogging, ActorRef, Props} import fr.acinq.bitcoin.scalacompat.ByteVector32 import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.eclair.channel.Register._ @@ -26,7 +27,7 @@ import fr.acinq.eclair.{SubscriptionsComplete, ShortChannelId} * Created by PM on 26/01/2016. */ -class Register extends Actor with ActorLogging { +class Register() extends Actor with ActorLogging { context.system.eventStream.subscribe(self, classOf[ChannelCreated]) context.system.eventStream.subscribe(self, classOf[AbstractChannelRestored]) @@ -34,18 +35,24 @@ class Register extends Actor with ActorLogging { context.system.eventStream.subscribe(self, classOf[ShortChannelIdAssigned]) context.system.eventStream.publish(SubscriptionsComplete(this.getClass)) + // @formatter:off + private case class ChannelTerminated(channel: ActorRef, channelId: ByteVector32) + // @formatter:on + override def receive: Receive = main(Map.empty, Map.empty, Map.empty) def main(channels: Map[ByteVector32, ActorRef], shortIds: Map[ShortChannelId, ByteVector32], channelsTo: Map[ByteVector32, PublicKey]): Receive = { case ChannelCreated(channel, _, remoteNodeId, _, temporaryChannelId, _, _) => - context.watch(channel) + context.watchWith(channel, ChannelTerminated(channel, temporaryChannelId)) context become main(channels + (temporaryChannelId -> channel), shortIds, channelsTo + (temporaryChannelId -> remoteNodeId)) case event: AbstractChannelRestored => - context.watch(event.channel) + context.watchWith(event.channel, ChannelTerminated(event.channel, event.channelId)) context become main(channels + (event.channelId -> event.channel), shortIds, channelsTo + (event.channelId -> event.remoteNodeId)) case ChannelIdAssigned(channel, remoteNodeId, temporaryChannelId, channelId) => + context.unwatch(channel) + context.watchWith(channel, ChannelTerminated(channel, channelId)) context become main(channels + (channelId -> channel) - temporaryChannelId, shortIds, channelsTo + (channelId -> remoteNodeId) - temporaryChannelId) case scidAssigned: ShortChannelIdAssigned => @@ -60,8 +67,7 @@ class Register extends Actor with ActorLogging { } context become main(channels, shortIds ++ m, channelsTo) - case Terminated(actor) if channels.values.toSet.contains(actor) => - val channelId = channels.find(_._2 == actor).get._1 + case ChannelTerminated(_, channelId) => val shortChannelIds = shortIds.collect { case (key, value) if value == channelId => key } context become main(channels - channelId, shortIds -- shortChannelIds, channelsTo - channelId) @@ -73,7 +79,7 @@ class Register extends Actor with ActorLogging { case fwd@Forward(replyTo, channelId, msg) => // for backward compatibility with legacy ask, we use the replyTo as sender - val compatReplyTo = if (replyTo == ActorRef.noSender) sender() else replyTo + val compatReplyTo = if (replyTo == null) sender() else replyTo.toClassic channels.get(channelId) match { case Some(channel) => channel.tell(msg, compatReplyTo) case None => compatReplyTo ! ForwardFailure(fwd) @@ -81,7 +87,7 @@ class Register extends Actor with ActorLogging { case fwd@ForwardShortId(replyTo, shortChannelId, msg) => // for backward compatibility with legacy ask, we use the replyTo as sender - val compatReplyTo = if (replyTo == ActorRef.noSender) sender() else replyTo + val compatReplyTo = if (replyTo == null) sender() else replyTo.toClassic shortIds.get(shortChannelId).flatMap(channels.get) match { case Some(channel) => channel.tell(msg, compatReplyTo) case None => compatReplyTo ! ForwardShortIdFailure(fwd) @@ -91,9 +97,11 @@ class Register extends Actor with ActorLogging { object Register { + def props(): Props = Props(new Register()) + // @formatter:off - case class Forward[T](replyTo: ActorRef, channelId: ByteVector32, message: T) - case class ForwardShortId[T](replyTo: ActorRef, shortChannelId: ShortChannelId, message: T) + case class Forward[T](replyTo: akka.actor.typed.ActorRef[ForwardFailure[T]], channelId: ByteVector32, message: T) + case class ForwardShortId[T](replyTo: akka.actor.typed.ActorRef[ForwardShortIdFailure[T]], shortChannelId: ShortChannelId, message: T) case class ForwardFailure[T](fwd: Forward[T]) case class ForwardShortIdFailure[T](fwd: ForwardShortId[T]) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala index 1e6fec2f99..bab02b5eae 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala @@ -1319,15 +1319,21 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val when(SYNCING)(handleExceptions { case Event(_: ChannelReestablish, d: DATA_WAIT_FOR_FUNDING_CONFIRMED) => - val minDepth = if (d.commitments.localParams.isInitiator) { + val minDepth_opt = if (d.commitments.localParams.isInitiator) { Helpers.Funding.minDepthFunder(d.commitments.channelFeatures) } else { // when we're not the channel initiator we scale the min_depth confirmations depending on the funding amount Helpers.Funding.minDepthFundee(nodeParams.channelConf, d.commitments.channelFeatures, d.commitments.commitInput.txOut.amount) } + val minDepth = minDepth_opt.getOrElse { + val defaultMinDepth = nodeParams.channelConf.minDepthBlocks + // If we are in state WAIT_FOR_FUNDING_CONFIRMED, then the computed minDepth should be > 0, otherwise we would + // have skipped this state. Maybe the computation method was changed and eclair was restarted? + log.warning("min_depth should be defined since we're waiting for the funding tx to confirm, using default minDepth={}", defaultMinDepth) + defaultMinDepth.toLong + } // we put back the watch (operation is idempotent) because the event may have been fired while we were in OFFLINE - require(minDepth.nonEmpty, "min_depth must be set since we're waiting for the funding tx to confirm") - blockchain ! WatchFundingConfirmed(self, d.commitments.commitInput.outPoint.txid, minDepth.get) + blockchain ! WatchFundingConfirmed(self, d.commitments.commitInput.outPoint.txid, minDepth) goto(WAIT_FOR_FUNDING_CONFIRMED) case Event(_: ChannelReestablish, d: DATA_WAIT_FOR_CHANNEL_READY) => diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/PendingCommandsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/PendingCommandsDb.scala index 62b77e8939..240d209095 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/PendingCommandsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/PendingCommandsDb.scala @@ -54,7 +54,7 @@ object PendingCommandsDb { */ def safeSend(register: ActorRef, db: PendingCommandsDb, channelId: ByteVector32, cmd: HtlcSettlementCommand): Unit = { // htlc settlement commands don't have replyTo - register ! Register.Forward(ActorRef.noSender, channelId, cmd) + register ! Register.Forward(null, channelId, cmd) // we store the command in a db (note that this happens *after* forwarding the command to the channel, so we don't add latency) db.addSettlementCommand(channelId, cmd) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala index 437767420b..4ad26188b5 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala @@ -153,7 +153,7 @@ class Peer(val nodeParams: NodeParams, remoteNodeId: PublicKey, wallet: OnChainA } else { // If a channel type was provided, we directly use it instead of computing it based on local and remote features. val channelFlags = c.channelFlags.getOrElse(nodeParams.channelConf.channelFlags) - val channelType = c.channelType_opt.getOrElse(ChannelTypes.defaultFromFeatures(d.localFeatures, d.remoteFeatures, channelFlags.announceChannel)) + val channelType = c.channelType_opt.getOrElse(ChannelTypes.defaultFromFeatures(d.localFeatures, d.remoteFeatures, announceChannel = channelFlags.announceChannel)) // NB: we need to capture parameters in a val to use them in andThen val selfRef = self val origin = sender() @@ -182,12 +182,12 @@ class Peer(val nodeParams: NodeParams, remoteNodeId: PublicKey, wallet: OnChainA // remote explicitly specifies a channel type: we check whether we want to allow it case Some(remoteChannelType) => ChannelTypes.areCompatible(d.localFeatures, remoteChannelType) match { case Some(acceptedChannelType) => Right(acceptedChannelType) - case None => Left(InvalidChannelType(msg.temporaryChannelId, ChannelTypes.defaultFromFeatures(d.localFeatures, d.remoteFeatures, msg.channelFlags.announceChannel), remoteChannelType)) + case None => Left(InvalidChannelType(msg.temporaryChannelId, ChannelTypes.defaultFromFeatures(d.localFeatures, d.remoteFeatures, announceChannel = msg.channelFlags.announceChannel), remoteChannelType)) } // Bolt 2: if `option_channel_type` is negotiated: MUST set `channel_type` case None if Features.canUseFeature(d.localFeatures, d.remoteFeatures, Features.ChannelType) => Left(MissingChannelType(msg.temporaryChannelId)) // remote doesn't specify a channel type: we use spec-defined defaults - case None => Right(ChannelTypes.defaultFromFeatures(d.localFeatures, d.remoteFeatures, msg.channelFlags.announceChannel)) + case None => Right(ChannelTypes.defaultFromFeatures(d.localFeatures, d.remoteFeatures, announceChannel = msg.channelFlags.announceChannel)) } chosenChannelType match { case Right(channelType) => diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/ChannelRelay.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/ChannelRelay.scala index 3475b97f74..c811d03a84 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/ChannelRelay.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/ChannelRelay.scala @@ -25,7 +25,7 @@ import fr.acinq.bitcoin.scalacompat.ByteVector32 import fr.acinq.eclair.channel._ import fr.acinq.eclair.db.PendingCommandsDb import fr.acinq.eclair.payment.Monitoring.{Metrics, Tags} -import fr.acinq.eclair.payment.relay.Relayer.OutgoingChannel +import fr.acinq.eclair.payment.relay.Relayer.{OutgoingChannel, OutgoingChannelParams} import fr.acinq.eclair.payment.{ChannelPaymentRelayed, IncomingPaymentPacket} import fr.acinq.eclair.wire.protocol._ import fr.acinq.eclair.{Logs, NodeParams, TimestampSecond, channel, nodeFee} @@ -118,7 +118,7 @@ class ChannelRelay private(nodeParams: NodeParams, safeSendAndStop(r.add.channelId, cmdFail) case RelaySuccess(selectedChannelId, cmdAdd) => context.log.info(s"forwarding htlc to channelId=$selectedChannelId") - register ! Register.Forward(forwardFailureAdapter.toClassic, selectedChannelId, cmdAdd) + register ! Register.Forward(forwardFailureAdapter, selectedChannelId, cmdAdd) waitForAddResponse(selectedChannelId, previousFailures) } } @@ -256,7 +256,7 @@ class ChannelRelay private(nodeParams: NodeParams, * channel, because some parameters don't match with our settings for that channel. In that case we directly fail the * htlc. */ - def relayOrFail(outgoingChannel_opt: Option[OutgoingChannel]): RelayResult = { + def relayOrFail(outgoingChannel_opt: Option[OutgoingChannelParams]): RelayResult = { import r._ outgoingChannel_opt match { case None => @@ -272,7 +272,7 @@ class ChannelRelay private(nodeParams: NodeParams, (TimestampSecond.now() - c.channelUpdate.timestamp > nodeParams.relayParams.enforcementDelay || outgoingChannel_opt.flatMap(_.prevChannelUpdate).forall(c => r.relayFeeMsat < nodeFee(c.relayFees, payload.amountToForward))) => RelayFailure(CMD_FAIL_HTLC(add.id, Right(FeeInsufficient(add.amountMsat, c.channelUpdate)), commit = true)) - case Some(c) => + case Some(c: OutgoingChannel) => val origin = Origin.ChannelRelayedHot(addResponseAdapter.toClassic, add, payload.amountToForward) RelaySuccess(c.channelId, CMD_ADD_HTLC(addResponseAdapter.toClassic, payload.amountToForward, add.paymentHash, payload.outgoingCltv, nextPacket, origin, commit = true)) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/Relayer.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/Relayer.scala index 6a95189bb5..1996927e8c 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/Relayer.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/Relayer.scala @@ -135,13 +135,18 @@ object Relayer extends Logging { case class RelayForward(add: UpdateAddHtlc) case class ChannelBalance(remoteNodeId: PublicKey, shortChannelId: ShortChannelId, canSend: MilliSatoshi, canReceive: MilliSatoshi, isPublic: Boolean, isEnabled: Boolean) + sealed trait OutgoingChannelParams { + def channelUpdate: ChannelUpdate + def prevChannelUpdate: Option[ChannelUpdate] + } + /** * Get the list of local outgoing channels. * * @param enabledOnly if true, filter out disabled channels. */ case class GetOutgoingChannels(enabledOnly: Boolean = true) - case class OutgoingChannel(nextNodeId: PublicKey, channelUpdate: ChannelUpdate, prevChannelUpdate: Option[ChannelUpdate], commitments: AbstractCommitments) { + case class OutgoingChannel(nextNodeId: PublicKey, channelUpdate: ChannelUpdate, prevChannelUpdate: Option[ChannelUpdate], commitments: AbstractCommitments) extends OutgoingChannelParams { val channelId: ByteVector32 = commitments.channelId def toChannelBalance: ChannelBalance = ChannelBalance( remoteNodeId = nextNodeId, diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentLifecycle.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentLifecycle.scala index aaa1710ade..e169af20c7 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentLifecycle.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentLifecycle.scala @@ -16,6 +16,7 @@ package fr.acinq.eclair.payment.send +import akka.actor.typed.scaladsl.adapter._ import akka.actor.{ActorRef, FSM, Props, Status} import akka.event.Logging.MDC import fr.acinq.bitcoin.scalacompat.ByteVector32 @@ -78,7 +79,7 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A log.info(s"route found: attempt=${failures.size + 1}/${c.maxAttempts} route=${route.printNodes()} channels=${route.printChannels()}") OutgoingPaymentPacket.buildCommand(self, cfg.upstream, paymentHash, route.hops, c.finalPayload) match { case Success((cmd, sharedSecrets)) => - register ! Register.ForwardShortId(self, route.hops.head.shortChannelId, cmd) + register ! Register.ForwardShortId(self.toTyped[Register.ForwardShortIdFailure[CMD_ADD_HTLC]], route.hops.head.shortChannelId, cmd) goto(WAITING_FOR_PAYMENT_COMPLETE) using WaitingForComplete(c, cmd, failures, sharedSecrets, ignore, route) case Failure(t) => log.warning("cannot send outgoing payment: {}", t.getMessage) diff --git a/eclair-core/src/test/resources/nonreg/codecs/000003-DATA_NORMAL/fundee-announced/data.bin b/eclair-core/src/test/resources/nonreg/codecs/000003-DATA_NORMAL/fundee-announced/data.bin new file mode 100644 index 0000000000..e47310bccf --- /dev/null +++ b/eclair-core/src/test/resources/nonreg/codecs/000003-DATA_NORMAL/fundee-announced/data.bin @@ -0,0 +1 @@ +00000303933884AAF1D6B108397E5EFE5C86BCF2D8CA8D2F700EDA99DB9214FC2712B134000456E4167E3C0EB8C856C79CA31C97C0AA0000000000000222000000012A05F2000000000000028F5C000000000000000102D0001E000BD48A2402E80B723C42EE3E42938866EC6686ABB7ABF64380000000C501A7F2974C5074E9E10DBB3F0D9B8C40932EC63ABC610FAD7EB6B21C6D081A459B000000000000011E80000001EEFFFE5C00000000000147AE00000000000001F403F000F18146779F781067ED04B4957E14F1C5623AB653039B2B1D49910240848E4E682DB20131AD64F76FAF90CD7DE26892F1BDAB82FB9E02EF6538D82FF4204B5348F02AE081A5388E9474769D69C4F60A763AE0CCDB5228A06281DE64408871A927297FDFD8818B6383985ABD4F0AC22E73791CF3A4D63C592FA2648242D34B8334B1539E823381BB1F1404C37D9C2318F5FC6B1BF7ECF5E6835B779E3BE09BADCF6DF1F51DCFBC80000000C0808000000000000EFD80000000007F00000000061A0A4880000001EDE5F3C3801203B9C79160B5123CADE575E370480B57B0C816EB35DD7B27372EDA858622EB1E808000000015FFFFFF800000000011001029DFB814F6502A68D6F83B6049E3D2948A2080084083750626532FDB437169C20023A9108146779F781067ED04B4957E14F1C5623AB653039B2B1D49910240848E4E682DB21081B30694071254D8B3B9537320C014B8CB1052E5514F5EFC19CF2EB806308D5CF1A95700AD0100000000008083B9C79160B5123CADE575E370480B57B0C816EB35DD7B27372EDA858622EB1E80800000001961B4C001618F8180000000001100102E648BA30998A28C02C2DFD9DDCD0E0BA064DA199C55186485AFAB296B94E704426FFE00000000000B000A67D9B9FAADB91650E0146B1F742E5C16006708890200239822011026A6925C659D006FEB42D639F1E42DD13224EE49AA34E71B612CF96DB66A8CD4011032C22F653C54CC5E41098227427650644266D80DED45B7387AE0FFC10E529C4680A418228110807CB47D9C1A14CB832FB361C398EA672C9542F34A90BAD4288FA6AC5FC9E9845C01101CF71CAE9252D389135D8C606225DCF1E0333CCDF1FAE84B74FC5D3D440C25F880A3A9108146779F781067ED04B4957E14F1C5623AB653039B2B1D49910240848E4E682DB21081B30694071254D8B3B9537320C014B8CB1052E5514F5EFC19CF2EB806308D5CF1A9573D7C531000000000000000000F3180000000007F00000001EDE5F3C380000000061A0A48D64CA627B243AD5915A2E5D0BAD026762028DDF3304992B83A26D6C11735FC5F01ED56D769BDE7F6A068AF1A4BCFDF950321F3A4744B01B1DDC7498677F112AE1A80000000000000000000000000000000000000658000000000000819800040D37301C10C9419287E9A3B704EB6D7F45CC145DD77DCE8A63B0A47C8AB67467D800901DCE3C8B05A891E56F2BAF1B82405ABD8640B759AEEBD939B976D42C311758F40400000000AFFFFFFC00000000008800814EFDC0A7B2815346B7C1DB024F1E94A451040042041BA83132997EDA1B8B4E10011D48840A33BCFBC0833F6825A4ABF0A78E2B11D5B2981CD958EA4C881204247273416D90840D9834A03892A6C59DCA9B990600A5C65882972A8A7AF7E0CE7975C031846AE78D4AB8002000EC0003FFFFFFFF86801076D98A575A4CDFD0E3F44D1BB3CD3BBAF3BD04C38FED439ED90D88DF932A9296801A80007FFFFFFFF4008136A9D5896669E8724C5120FB6B36C241EF3CEF68AE0316161F04A9EE3EAFF36000FC0003FFFFFFFF86780106E4B5CC4155733A2427082907338051A5DA1E7CA6432840A5528ECAFFA3FB628801B80007FFFFFFFF10020CA4E125E9126107745D4354D4187ABCDE323117857A1DCEB7CCF60B2AAFA80C6003A0000FFFFFFFFE1C0080981575FD981A73A848CC0243CB467BF451F6811DAF4D71CAD8CE8B1E96DB190C01000003FFFFFFFF867400814C747E0FD8290BE8A3B8B3F73015A261479A71780CD3A0A9270234E4B394409C00D80003FFFFFFFF90020E1B9C9B10A97F15F5E1BB27FC8AC670DF8DADEAE4EDFAFB23BDD0AC705FDF51600340000FFFFFFFFF0020AD2581F3494A17B0BE3F63516D53F028A204FD3156D8B21AA4E57A8738D2062080007FFFFFFFF0CE83B9C79160B5123CADE575E370480B57B0C816EB35DD7B27372EDA858622EB1E0B8C1E00000B8000FA46CC2C7E9AB4A37C64216CD65C944E6D73998419D1A1AD2827AB6BC85B32280230764E374064EC82A3751E789607E23BEAE93FB0EDDD5E7FA803767079662E80EAEF384E2AFCB68049D9DC246119E77BD2ED4112330760CAB6CD3671CFCE006C584B9C95E0B554261E00154D40806EA694F44751B328A9291BAD124EFD5664280936EC92D27B242737E7E3E83B4704BA367B7DA5108F2F6EDFB1C38EE721A369E77EED71B12090BAEAAAC322C1457E31AB0C4DE5D9351943F10FD747742616A1AABD09F680B37D4105A8872695EE9B97FAB8985FAA9D747D45046229BF265CEEB300A40FE23040C5F335E0515496C58EE47418B72331FCC6F47A31A9B33B8E000008692FFAFF04D2AE211E9461FB39D875D74F32E4109D21D5A03D46612000000002E307800002E0002069FCA5D3141D3A78436ECFC366E31024CBB18EAF1843EB5FADAC871B42069166C0726710955E3AD621072FCBDFCB90D79E5B1951A5EE01DB533B72429F84E2562680519DE7DE0419FB412D255F853C71588EAD94C0E6CAC7526440902123939A0B6C806CC1A501C495362CEE54DCC830052E32C414B95453D7BF0673CBAE018C23573C69C694A8F88483050257A7366B838489731E5776B6FA0F02573401176D3E7FAEEF11E95A671420586631255F51A0EC2CF4D4D9F69D587712070FE1FB9316B71868692FFAFF04D2AE211E9461FB39D875D74F32E4109D21D5A03D46612000000002E307800002E0002BA11BBBA0202012000000000000007D0000007D0000000C800000007CFFFF83000 \ No newline at end of file diff --git a/eclair-core/src/test/resources/nonreg/codecs/000003-DATA_NORMAL/fundee-announced/data.json b/eclair-core/src/test/resources/nonreg/codecs/000003-DATA_NORMAL/fundee-announced/data.json new file mode 100644 index 0000000000..d92f759936 --- /dev/null +++ b/eclair-core/src/test/resources/nonreg/codecs/000003-DATA_NORMAL/fundee-announced/data.json @@ -0,0 +1,150 @@ +{ + "type" : "DATA_NORMAL", + "commitments" : { + "channelId" : "07738f22c16a24795bcaebc6e09016af61902dd66bbaf64e6e5db50b0c45d63c", + "channelConfig" : [ ], + "channelFeatures" : [ ], + "localParams" : { + "nodeId" : "03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134", + "fundingKeyPath" : { + "path" : [ 1457788542, 1007597768, 1455922339, 479707306 ] + }, + "dustLimit" : 546, + "maxHtlcValueInFlightMsat" : 5000000000, + "requestedChannelReserve_opt" : 167772, + "htlcMinimum" : 1, + "toSelfDelay" : 720, + "maxAcceptedHtlcs" : 30, + "isInitiator" : false, + "defaultFinalScriptPubKey" : "a9144805d016e47885dc7c852710cdd8cd0d576f57ec87", + "initFeatures" : { + "activated" : { + "option_data_loss_protect" : "optional", + "initial_routing_sync" : "optional", + "gossip_queries" : "optional" + }, + "unknown" : [ ] + } + }, + "remoteParams" : { + "nodeId" : "034fe52e98a0e9d3c21b767e1b371881265d8c7578c21f5afd6d6438da10348b36", + "dustLimit" : 573, + "maxHtlcValueInFlightMsat" : 16609443000, + "requestedChannelReserve_opt" : 167772, + "htlcMinimum" : 1000, + "toSelfDelay" : 2016, + "maxAcceptedHtlcs" : 483, + "fundingPubKey" : "028cef3ef020cfda09692afc29e38ac4756ca60736563a93220481091c9cd05b64", + "revocationBasepoint" : "02635ac9eedf5f219afbc4d125e37b5705f73c05deca71b05fe84096a691e055c1", + "paymentBasepoint" : "034a711d28e8ed3ad389ec14ec75c199b6a45140c503bcc88110e3524e52ffbfb1", + "delayedPaymentBasepoint" : "0316c70730b57a9e15845ce6f239e749ac78b25f44c90485a697066962a73d0467", + "htlcBasepoint" : "03763e280986fb384631ebf8d637efd9ebcd06b6ef3c77c1375b9edbe3ea3b9f79", + "initFeatures" : { + "activated" : { + "option_data_loss_protect" : "mandatory", + "gossip_queries" : "optional" + }, + "unknown" : [ ] + } + }, + "channelFlags" : { + "announceChannel" : true + }, + "localCommit" : { + "index" : 7675, + "spec" : { + "htlcs" : [ ], + "commitTxFeerate" : 254, + "toLocal" : 204739729, + "toRemote" : 16572475271 + }, + "commitTxAndRemoteSig" : { + "commitTx" : { + "txid" : "e25a866b79212015e01e155e530fb547abc8276869f8740a9948e52ca231f1e4", + "tx" : "020000000107738f22c16a24795bcaebc6e09016af61902dd66bbaf64e6e5db50b0c45d63d010000000032c3698002c31f0300000000002200205cc91746133145180585bfb3bb9a1c1740c9b43338aa30c90b5f5652d729ce0884dffc0000000000160014cfb373f55b722ca1c028d63ee85cb82c00ce11127af8a620" + }, + "remoteSig" : "4d4d24b8cb3a00dfd685ac73e3c85ba26449dc935469ce36c259f2db6cd519a865845eca78a998bc8213044e84eca0c884cdb01bda8b6e70f5c1ff821ca5388d" + }, + "htlcTxsAndRemoteSigs" : [ ] + }, + "remoteCommit" : { + "index" : 7779, + "spec" : { + "htlcs" : [ ], + "commitTxFeerate" : 254, + "toLocal" : 16572475271, + "toRemote" : 204739729 + }, + "txid" : "ac994c4f64875ab22b45cba175a04cec4051bbe660932570744dad822e6bf8be", + "remotePerCommitmentPoint" : "03daadaed37bcfed40d15e34979fbf2a0643e748e8960363bb8e930cefe2255c35" + }, + "localChanges" : { + "proposed" : [ ], + "signed" : [ ], + "acked" : [ ] + }, + "remoteChanges" : { + "proposed" : [ ], + "acked" : [ ], + "signed" : [ ] + }, + "localNextHtlcId" : 203, + "remoteNextHtlcId" : 4147, + "originChannels" : { }, + "remoteNextCommitInfo" : "034dcc0704325064a1fa68edc13adb5fd173051775df73a298ec291f22ad9d19f6", + "commitInput" : { + "outPoint" : "3dd6450c0bb55d6e4ef6ba6bd62d9061af1690e0c6ebca5b79246ac1228f7307:1", + "amountSatoshis" : 16777215 + }, + "remotePerCommitmentSecrets" : null + }, + "shortIds" : { + "real" : { + "status" : "final", + "realScid" : "1513532x23x1" + }, + "localAlias" : "0x17183c0000170001" + }, + "channelAnnouncement" : { + "nodeSignature1" : "d2366163f4d5a51be3210b66b2e4a2736b9ccc20ce8d0d69413d5b5e42d991401183b271ba032764151ba8f3c4b03f11df5749fd876eeaf3fd401bb383cb3174", + "nodeSignature2" : "075779c27157e5b4024ecee12308cf3bde976a0891983b0655b669b38e7e700362c25ce4af05aaa130f000aa6a04037534a7a23a8d99454948dd689277eab321", + "bitcoinSignature1" : "4049b7649693d92139bf3f1f41da3825d1b3dbed2884797b76fd8e1c77390d1b4f3bf76b8d890485d7555619160a2bf18d58626f2ec9a8ca1f887eba3ba130b5", + "bitcoinSignature2" : "0d55e84fb4059bea082d443934af74dcbfd5c4c2fd54eba3ea2823114df932e7759805207f1182062f99af028aa4b62c7723a0c5b9198fe637a3d18d4d99dc70", + "features" : { + "activated" : { }, + "unknown" : [ ] + }, + "chainHash" : "43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000", + "shortChannelId" : "1513532x23x1", + "nodeId1" : "034fe52e98a0e9d3c21b767e1b371881265d8c7578c21f5afd6d6438da10348b36", + "nodeId2" : "03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134", + "bitcoinKey1" : "028cef3ef020cfda09692afc29e38ac4756ca60736563a93220481091c9cd05b64", + "bitcoinKey2" : "03660d280e24a9b16772a6e6418029719620a5caa29ebdf8339e5d700c611ab9e3", + "tlvStream" : { + "records" : [ ], + "unknown" : [ ] + } + }, + "channelUpdate" : { + "signature" : "4e34a547c424182812bd39b35c1c244b98f2bbb5b7d07812b9a008bb69f3fd77788f4ad338a102c331892afa8d076167a6a6cfb4eac3b890387f0fdc98b5b8c3", + "chainHash" : "43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000", + "shortChannelId" : "1513532x23x1", + "timestamp" : { + "iso" : "2019-06-18T12:49:33Z", + "unix" : 1560862173 + }, + "channelFlags" : { + "isEnabled" : true, + "isNode1" : false + }, + "cltvExpiryDelta" : 144, + "htlcMinimumMsat" : 1000, + "feeBaseMsat" : 1000, + "feeProportionalMillionths" : 100, + "htlcMaximumMsat" : 16777215000, + "tlvStream" : { + "records" : [ ], + "unknown" : [ ] + } + } +} \ No newline at end of file diff --git a/eclair-core/src/test/resources/nonreg/codecs/000003-DATA_NORMAL/funder/data.bin b/eclair-core/src/test/resources/nonreg/codecs/000003-DATA_NORMAL/funder/data.bin new file mode 100644 index 0000000000..cace3d5deb --- /dev/null +++ b/eclair-core/src/test/resources/nonreg/codecs/000003-DATA_NORMAL/funder/data.bin @@ -0,0 +1 @@ +00000303933884AAF1D6B108397E5EFE5C86BCF2D8CA8D2F700EDA99DB9214FC2712B1340004D443ECE9D9C43A11A19B554BAAA6AD150000000000000222000000003B9ACA0000000000000249F000000000000000010090001E800BD48A22F4C80A42CC8BB29A764DBAEFC95674931FBE9A4380000000C50134D4A745996002F219B5FDBA1E045374DF589ECA06ABE23CECAE47343E65EDCF800000000000011E80000001BA90824000000000000124F800000000000001F4038500F1810AE1AF8A1D6F56F80855E26705F191BB07CD4E2434BC5BB1698E7E5880E2266201E8BFEEEEED725775B8116F6F82CF8E87835A5B45B184E56F272AD70D6078118601E06212B8C8F2E25B73EE7974FDCDF007E389B437BBFE238CCC3F3BF7121B6C5E81AA8589D21E9584B24A11F3ABBA5DAD48D121DD63C57A69CD767119C05DA159CB81A649D8CC0E136EB8DFBD2268B69DCA86F8CE4A604235A03D9D37AE7B07FC563F80000000C080800000000000271C000000000177000000002808B14600000001970039BA00123767F0F4F00D5E9FDF24177EF2872343D9F8FAEC65D3048BA575E70E00A0AB08800000000015E070F20000000000110010584241B5FB364208F6E64A80D1166DAD866186B10C015ED0283FF1C308C2105A0023A910810AE1AF8A1D6F56F80855E26705F191BB07CD4E2434BC5BB1698E7E5880E226621081DE8ADFA110DC8A94D8B9E9EF616BAE8598287C8F82AFDF0FC068697D570266FDA95700AD81000000000080B767F0F4F00D5E9FDF24177EF2872343D9F8FAEC65D3048BA575E70E00A0AB0880000000003E7AEDC0011ABE8A00000000001100101A9CE4B6AEF469590BC7BCC51DCEEAE9C86084055A63CC01E443C733FBE400B9B5B16800000000000B000A5E5700106D1A7097E4DE87EBAF1F8F2773842FA482002418228110805E84989A81F51ABD9D11889AE43E68FAD93659DEC019F1B8C0ADBF15A57B118B81101DCC1256F9306439AD3962C043FC47A5179CAAA001CCB23342BE0E8D92E4022780A4182281108074F306DA3751B84EC5FFB155BDCA7B8E02208BBDBC8D4F3327ABA557BF27CD1701102EF4AC8CC92F469DA9642D4D4162BC545F8B34ADE15B7D6F99808AA22B086B0180A3A910810AE1AF8A1D6F56F80855E26705F191BB07CD4E2434BC5BB1698E7E5880E226621081DE8ADFA110DC8A94D8B9E9EF616BAE8598287C8F82AFDF0FC068697D570266FDA9576F8099900000000000000000271C00000000017700000001970039BA000000002808B14648CE00AE97051EE10A3C361263F81A98165CE4AA7BA076933D4266E533585F24815C15DEACF0691332B38ECF23EC39982C5C978C748374A01BA9B30D501EE4F26E8000000000000000000000000000000000001224000000000000004B800040A911C460F1467952E3B99BED072F81BFB4454FF389636DCB399FE6A78113C28580091BB3F87A7806AF4FEF920BBF794391A1ECFC7D7632E98245D2BAF3870050558440000000000AF0387900000000000880082C2120DAFD9B21047B732540688B36D6C330C3588600AF68141FF8E18461082D0011D488408570D7C50EB7AB7C042AF13382F8C8DD83E6A7121A5E2DD8B4C73F2C407113310840EF456FD0886E454A6C5CF4F7B0B5D742CC143E47C157EF87E03434BEAB81337ED4AB8001C00F40003FFFFFFFEC7200403248A1D44DFA3AC9EC237D452C936400CAA86E9517CCCF2A8F77B7493CD70B6A00780001FFFFFFFF63A0041826829646B907A97FBD1455EA8673A12B8E7AA6EA790F7802E955CE3B69DE57E006E0001FFFFFFFF640081E51EB1F91218821E680B50E4B22DF8B094385BD33ACAE36BFC9E8C2F5AD2DA5400EC0003FFFFFFFEC7801047C26AD5435658D063EBCF73A5D0EEFE73ED6B73426246E8DFB3A21D1C4C7465001900007FFFFFFFE0040B115AC58BAAA900195893EA3B2AB408D2AD348AD047E3B6CB15E599625E38608006A0001FFFFFFFF7002033C39A21A38BB61F6FB33623771A9356D8885B7C12C939C770C939EF826286C200360000FFFFFFFFB4008104EF4271064A0973B053727C3E67352D00E25CAEED944F50782449CEAE8F50960001FFFFFFFF6390DD9FC3D3C0357A7F7C905DFBCA1C8D0F67E3EBB1974C122E95D79C380282AC222B21FA0007920001295AA1FB77029F7620A90EF7AE6A6CD31E4588B93264A7ADB76152D535C52E90B9E1B7C2376DABA316A6290F1A9730D4E5E44D0B1CB0EE6A795702E6A6BCDFCDA1A4BFEBFC134AB8847A5187ECE761D75D3CCB904274875680F51984800000000AC87E8001E480002E884D2A8080804800000000000001F4000001F40000003200000001BF08EB000 \ No newline at end of file diff --git a/eclair-core/src/test/resources/nonreg/codecs/000003-DATA_NORMAL/funder/data.json b/eclair-core/src/test/resources/nonreg/codecs/000003-DATA_NORMAL/funder/data.json new file mode 100644 index 0000000000..79f8c7a9c8 --- /dev/null +++ b/eclair-core/src/test/resources/nonreg/codecs/000003-DATA_NORMAL/funder/data.json @@ -0,0 +1,130 @@ +{ + "type" : "DATA_NORMAL", + "commitments" : { + "channelId" : "6ecfe1e9e01abd3fbe482efde50e4687b3f1f5d8cba609174aebce1c01415611", + "channelConfig" : [ ], + "channelFeatures" : [ ], + "localParams" : { + "nodeId" : "03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134", + "fundingKeyPath" : { + "path" : [ 3561221353, 3653515793, 2711311691, 2863050005 ] + }, + "dustLimit" : 546, + "maxHtlcValueInFlightMsat" : 1000000000, + "requestedChannelReserve_opt" : 150000, + "htlcMinimum" : 1, + "toSelfDelay" : 144, + "maxAcceptedHtlcs" : 30, + "isInitiator" : true, + "defaultFinalScriptPubKey" : "a91445e990148599176534ec9b75df92ace9263f7d3487", + "initFeatures" : { + "activated" : { + "option_data_loss_protect" : "optional", + "initial_routing_sync" : "optional", + "gossip_queries" : "optional" + }, + "unknown" : [ ] + } + }, + "remoteParams" : { + "nodeId" : "0269a94e8b32c005e4336bfb743c08a6e9beb13d940d57c479d95c8e687ccbdb9f", + "dustLimit" : 573, + "maxHtlcValueInFlightMsat" : 14850000000, + "requestedChannelReserve_opt" : 150000, + "htlcMinimum" : 1000, + "toSelfDelay" : 1802, + "maxAcceptedHtlcs" : 483, + "fundingPubKey" : "0215c35f143adeadf010abc4ce0be323760f9a9c486978b762d31cfcb101c44cc4", + "revocationBasepoint" : "03d17fdddddae4aeeb7022dedf059f1d0f06b4b68b6309cade4e55ae1ac0f0230c", + "paymentBasepoint" : "03c0c4257191e5c4b6e7dcf2e9fb9be00fc713686f77fc4719987e77ee2436d8bd", + "delayedPaymentBasepoint" : "03550b13a43d2b09649423e75774bb5a91a243bac78af4d39aece23380bb42b397", + "htlcBasepoint" : "034c93b1981c26dd71bf7a44d16d3b950df19c94c0846b407b3a6f5cf60ff8ac7f", + "initFeatures" : { + "activated" : { + "option_data_loss_protect" : "mandatory", + "gossip_queries" : "optional" + }, + "unknown" : [ ] + } + }, + "channelFlags" : { + "announceChannel" : true + }, + "localCommit" : { + "index" : 20024, + "spec" : { + "htlcs" : [ ], + "commitTxFeerate" : 750, + "toLocal" : 1343316620, + "toRemote" : 13656683380 + }, + "commitTxAndRemoteSig" : { + "commitTx" : { + "txid" : "65fe0b1f079fa763448df3ab8d94b1ad7d377c061121376be90b9c0c1bb0cd43", + "tx" : "02000000016ecfe1e9e01abd3fbe482efde50e4687b3f1f5d8cba609174aebce1c0141561100000000007cf5db8002357d1400000000002200203539c96d5de8d2b2178f798a3b9dd5d390c1080ab4c79803c8878e67f7c801736b62d00000000000160014bcae0020da34e12fc9bd0fd75e3f1e4ee7085f49df013320" + }, + "remoteSig" : "bd09313503ea357b3a231135c87cd1f5b26cb3bd8033e371815b7e2b4af623173b9824adf260c8735a72c58087f88f4a2f39554003996466857c1d1b25c8044f" + }, + "htlcTxsAndRemoteSigs" : [ ] + }, + "remoteCommit" : { + "index" : 20024, + "spec" : { + "htlcs" : [ ], + "commitTxFeerate" : 750, + "toLocal" : 13656683380, + "toRemote" : 1343316620 + }, + "txid" : "919c015d2e0a3dc214786c24c7f035302cb9c954f740ed267a84cdca66b0be49", + "remotePerCommitmentPoint" : "02b82bbd59e0d22665671d9e47d8733058b92f18e906e9403753661aa03dc9e4dd" + }, + "localChanges" : { + "proposed" : [ ], + "signed" : [ ], + "acked" : [ ] + }, + "remoteChanges" : { + "proposed" : [ ], + "acked" : [ ], + "signed" : [ ] + }, + "localNextHtlcId" : 9288, + "remoteNextHtlcId" : 151, + "originChannels" : { }, + "remoteNextCommitInfo" : "02a4471183c519e54b8ee66fb41cbe06fed1153fce258db72ce67f9a9e044f0a16", + "commitInput" : { + "outPoint" : "115641011cceeb4a1709a6cbd8f5f1b387460ee5fd2e48be3fbd1ae0e9e1cf6e:0", + "amountSatoshis" : 15000000 + }, + "remotePerCommitmentSecrets" : null + }, + "shortIds" : { + "real" : { + "status" : "final", + "realScid" : "1413373x969x0" + }, + "localAlias" : "0x1590fd0003c90000" + }, + "channelUpdate" : { + "signature" : "52b543f6ee053eec41521def5cd4d9a63c8b117264c94f5b6ec2a5aa6b8a5d2173c36f846edb57462d4c521e352e61a9cbc89a163961dcd4f2ae05cd4d79bf9b", + "chainHash" : "43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000", + "shortChannelId" : "1413373x969x0", + "timestamp" : { + "iso" : "2019-06-24T09:39:33Z", + "unix" : 1561369173 + }, + "channelFlags" : { + "isEnabled" : true, + "isNode1" : false + }, + "cltvExpiryDelta" : 144, + "htlcMinimumMsat" : 1000, + "feeBaseMsat" : 1000, + "feeProportionalMillionths" : 100, + "htlcMaximumMsat" : 15000000000, + "tlvStream" : { + "records" : [ ], + "unknown" : [ ] + } + } +} \ No newline at end of file diff --git a/eclair-core/src/test/resources/nonreg/codecs/020002-DATA_NORMAL/funder-announced/data.bin b/eclair-core/src/test/resources/nonreg/codecs/020002-DATA_NORMAL/funder-announced/data.bin new file mode 100644 index 0000000000..fbdcec643a --- /dev/null +++ b/eclair-core/src/test/resources/nonreg/codecs/020002-DATA_NORMAL/funder-announced/data.bin @@ -0,0 +1 @@ +0200020000000303933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b13400098c4b989bbdced820a77a7186c2320e7d176a5c8b5c16d6ac2af3889d6bc8bf8080000001000000000000022200000004a817c80000000000000249f0000000000000000102d0001eff1600148061b7fbd2d84ed1884177ea785faecb2080b10302e56c8eca8d4f00df84ac34c23f49c006d57d316b7ada5c346e9d4211e11604b300000004080aa982027455aef8453d92f4706b560b61527cc217ddf14da41770e8ed6607190a1851b8000000000000023d000000037521048000000000000249f00000000000000001070a01e302eff5309b9368340edc6114d738b3590e6969bec4e95d8a080cf185e8b9ce5e4b0343bf4bfbaea5c100f1f2bf1cdf82a0ef97c9a0069a2aec631e7c3084ba929b7503c54e7d5ccfc13f1a6c7a441ffcfac86248574d1bc0fe9773836f4c724ea7b2bd03765aaac2e8fa6dbce7de5143072e9d9d5e96a1fd451d02fe4ff803f413f303f8022f3b055b0d35cde31dec5263a8ed638433e3424a4e197c06d94053985a364a5700000004808a52a1010000000000000004000000001046000000037e11d6000000000000000000245986f644357956c1674f385e0878fb7bb44ab3d57ea9911fab98af8a71e1ad1b000000002bc0e1e40000000000220020690fb50de412adf9b20a7fc6c8fb86f1bfd4ebc1ef8e2d96a5a196560798d944475221023ced05ed1ab328b67477376d68a69ecd0f371a9d5843c6c3be4d31498d516d8d2102eff5309b9368340edc6114d738b3590e6969bec4e95d8a080cf185e8b9ce5e4b52aefd013b020000000001015986f644357956c1674f385e0878fb7bb44ab3d57ea9911fab98af8a71e1ad1b0000000000c2d6178001f8d5e4000000000022002080f1dfe71a865b605593e169677c952aaa1196fc2f541ef7d21c3b1006527b61040047304402207f8c1936d0a50671c993890f887c78c6019abc2a2e8018899dcdc0e891fd2b090220046b56afa2cb7e9470073c238654ecf584bcf5c00b96b91e38335a70e2739ec901483045022100871afd240e20a171b9cba46f20555f848c5850f94ec7da7b33b9eeaf6af6653c0220119cda8cbf5f80986d6a4f0db2590c734d1de399a7060a477b5d94df0183625b01475221023ced05ed1ab328b67477376d68a69ecd0f371a9d5843c6c3be4d31498d516d8d2102eff5309b9368340edc6114d738b3590e6969bec4e95d8a080cf185e8b9ce5e4b52aed7782c20000000000000000000040000000010460000000000000000000000037e11d600b5f2287b2d5edf4df5602a3c287db3b938c3f1a943e40715886db5bd400f95d802e7e1abac1feb54ee3ac2172c9e2231f77765df57664fb44a6dc2e4aa9e6a9a6a000000000000000000000000000000000000000000000000000000000000ff03fd10fe44564e2d7e1550099785c2c1bad32a5ae0feeef6e27f0c108d18b4931d245986f644357956c1674f385e0878fb7bb44ab3d57ea9911fab98af8a71e1ad1b000000002bc0e1e40000000000220020690fb50de412adf9b20a7fc6c8fb86f1bfd4ebc1ef8e2d96a5a196560798d944475221023ced05ed1ab328b67477376d68a69ecd0f371a9d5843c6c3be4d31498d516d8d2102eff5309b9368340edc6114d738b3590e6969bec4e95d8a080cf185e8b9ce5e4b52ae0001003e0000fffffffffffc0080474b8cf7bb98217dd8dc475cb7c057a3465d466728978bbb909d0a05d4ae7bbe0001fffffffffff85986f644357956c1674f385e0878fb7bb44ab3d57ea9911fab98af8a71e1ad1b1eedce0000010000fffffd01ae98d7a81bc1aa92fcfb74ced2213e85e0d92ae8ac622bf294b3551c7c27f6f84f782f3b318e4d0eb2c67ac719a7c65afcf85bf159f6ceea9427be54920134196992f6ed0e059db72105a13ec0e799bb08896cad8b4feb7e9ec7283c309b5f43123af1bd9e913fc2db018edadde8932d6992408f10c1ad020504361972dfa7fef09bbc2b568cef3c8c006f7860106fd5984bcc271ff06c4829db2a665e59b7c0b22c311a340ff2ab9bcb74a50db10ed85503ad2d248d95af8151aca8ef96248e8f84b3075922385fbaf012f057e7ee84ecbc14c84880520b26d6fd22ab5f107db606a906efdcf0f88ffbe32dc6ecc10131e1ff0dc8d68dad89c98562557f00448b000043497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea3309000000001eedce0000010000027455aef8453d92f4706b560b61527cc217ddf14da41770e8ed6607190a1851b803933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b13402eff5309b9368340edc6114d738b3590e6969bec4e95d8a080cf185e8b9ce5e4b023ced05ed1ab328b67477376d68a69ecd0f371a9d5843c6c3be4d31498d516d8d88710d73875607575f3d84bb507dd87cca5b85f0cdac84f4ccecce7af3a55897525a45070fe26c0ea43e9580d4ea4cfa62ee3273e5546911145cba6bbf56e59d8e43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea3309000000001eedce000001000060e6eb14010100900000000000000001000003e800000064000000037e11d6000000 \ No newline at end of file diff --git a/eclair-core/src/test/resources/nonreg/codecs/020002-DATA_NORMAL/funder-announced/data.json b/eclair-core/src/test/resources/nonreg/codecs/020002-DATA_NORMAL/funder-announced/data.json new file mode 100644 index 0000000000..90f79e9c7a --- /dev/null +++ b/eclair-core/src/test/resources/nonreg/codecs/020002-DATA_NORMAL/funder-announced/data.json @@ -0,0 +1,164 @@ +{ + "type" : "DATA_NORMAL", + "commitments" : { + "channelId" : "5986f644357956c1674f385e0878fb7bb44ab3d57ea9911fab98af8a71e1ad1b", + "channelConfig" : [ "funding_pubkey_based_channel_keypath" ], + "channelFeatures" : [ "option_static_remotekey" ], + "localParams" : { + "nodeId" : "03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134", + "fundingKeyPath" : { + "path" : [ 2353764507, 3184449568, 2809819526, 3258060413, 392846475, 1545000620, 720603293, 1808318336, 2147483649 ] + }, + "dustLimit" : 546, + "maxHtlcValueInFlightMsat" : 20000000000, + "requestedChannelReserve_opt" : 150000, + "htlcMinimum" : 1, + "toSelfDelay" : 720, + "maxAcceptedHtlcs" : 30, + "isInitiator" : true, + "defaultFinalScriptPubKey" : "00148061b7fbd2d84ed1884177ea785faecb2080b103", + "walletStaticPaymentBasepoint" : "02e56c8eca8d4f00df84ac34c23f49c006d57d316b7ada5c346e9d4211e11604b3", + "initFeatures" : { + "activated" : { + "option_support_large_channel" : "optional", + "gossip_queries_ex" : "optional", + "option_data_loss_protect" : "optional", + "var_onion_optin" : "mandatory", + "option_static_remotekey" : "optional", + "payment_secret" : "optional", + "option_shutdown_anysegwit" : "optional", + "basic_mpp" : "optional", + "gossip_queries" : "optional" + }, + "unknown" : [ ] + } + }, + "remoteParams" : { + "nodeId" : "027455aef8453d92f4706b560b61527cc217ddf14da41770e8ed6607190a1851b8", + "dustLimit" : 573, + "maxHtlcValueInFlightMsat" : 14850000000, + "requestedChannelReserve_opt" : 150000, + "htlcMinimum" : 1, + "toSelfDelay" : 1802, + "maxAcceptedHtlcs" : 483, + "fundingPubKey" : "02eff5309b9368340edc6114d738b3590e6969bec4e95d8a080cf185e8b9ce5e4b", + "revocationBasepoint" : "0343bf4bfbaea5c100f1f2bf1cdf82a0ef97c9a0069a2aec631e7c3084ba929b75", + "paymentBasepoint" : "03c54e7d5ccfc13f1a6c7a441ffcfac86248574d1bc0fe9773836f4c724ea7b2bd", + "delayedPaymentBasepoint" : "03765aaac2e8fa6dbce7de5143072e9d9d5e96a1fd451d02fe4ff803f413f303f8", + "htlcBasepoint" : "022f3b055b0d35cde31dec5263a8ed638433e3424a4e197c06d94053985a364a57", + "initFeatures" : { + "activated" : { + "option_upfront_shutdown_script" : "optional", + "payment_secret" : "mandatory", + "option_data_loss_protect" : "mandatory", + "var_onion_optin" : "optional", + "option_static_remotekey" : "mandatory", + "option_support_large_channel" : "optional", + "option_anchors_zero_fee_htlc_tx" : "optional", + "basic_mpp" : "optional", + "gossip_queries" : "optional" + }, + "unknown" : [ 31 ] + } + }, + "channelFlags" : { + "announceChannel" : true + }, + "localCommit" : { + "index" : 4, + "spec" : { + "htlcs" : [ ], + "commitTxFeerate" : 4166, + "toLocal" : 15000000000, + "toRemote" : 0 + }, + "commitTxAndRemoteSig" : { + "commitTx" : { + "txid" : "fa747ecb6f718c6831cc7148cf8d65c3468d2bb6c202605e2b82d2277491222f", + "tx" : "02000000015986f644357956c1674f385e0878fb7bb44ab3d57ea9911fab98af8a71e1ad1b0000000000c2d6178001f8d5e4000000000022002080f1dfe71a865b605593e169677c952aaa1196fc2f541ef7d21c3b1006527b61d7782c20" + }, + "remoteSig" : "871afd240e20a171b9cba46f20555f848c5850f94ec7da7b33b9eeaf6af6653c119cda8cbf5f80986d6a4f0db2590c734d1de399a7060a477b5d94df0183625b" + }, + "htlcTxsAndRemoteSigs" : [ ] + }, + "remoteCommit" : { + "index" : 4, + "spec" : { + "htlcs" : [ ], + "commitTxFeerate" : 4166, + "toLocal" : 0, + "toRemote" : 15000000000 + }, + "txid" : "b5f2287b2d5edf4df5602a3c287db3b938c3f1a943e40715886db5bd400f95d8", + "remotePerCommitmentPoint" : "02e7e1abac1feb54ee3ac2172c9e2231f77765df57664fb44a6dc2e4aa9e6a9a6a" + }, + "localChanges" : { + "proposed" : [ ], + "signed" : [ ], + "acked" : [ ] + }, + "remoteChanges" : { + "proposed" : [ ], + "acked" : [ ], + "signed" : [ ] + }, + "localNextHtlcId" : 0, + "remoteNextHtlcId" : 0, + "originChannels" : { }, + "remoteNextCommitInfo" : "03fd10fe44564e2d7e1550099785c2c1bad32a5ae0feeef6e27f0c108d18b4931d", + "commitInput" : { + "outPoint" : "1bade1718aaf98ab1f91a97ed5b34ab47bfb78085e384f67c156793544f68659:0", + "amountSatoshis" : 15000000 + }, + "remotePerCommitmentSecrets" : null + }, + "shortIds" : { + "real" : { + "status" : "final", + "realScid" : "2026958x1x0" + }, + "localAlias" : "0x1eedce0000010000" + }, + "channelAnnouncement" : { + "nodeSignature1" : "98d7a81bc1aa92fcfb74ced2213e85e0d92ae8ac622bf294b3551c7c27f6f84f782f3b318e4d0eb2c67ac719a7c65afcf85bf159f6ceea9427be549201341969", + "nodeSignature2" : "92f6ed0e059db72105a13ec0e799bb08896cad8b4feb7e9ec7283c309b5f43123af1bd9e913fc2db018edadde8932d6992408f10c1ad020504361972dfa7fef0", + "bitcoinSignature1" : "9bbc2b568cef3c8c006f7860106fd5984bcc271ff06c4829db2a665e59b7c0b22c311a340ff2ab9bcb74a50db10ed85503ad2d248d95af8151aca8ef96248e8f", + "bitcoinSignature2" : "84b3075922385fbaf012f057e7ee84ecbc14c84880520b26d6fd22ab5f107db606a906efdcf0f88ffbe32dc6ecc10131e1ff0dc8d68dad89c98562557f00448b", + "features" : { + "activated" : { }, + "unknown" : [ ] + }, + "chainHash" : "43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000", + "shortChannelId" : "2026958x1x0", + "nodeId1" : "027455aef8453d92f4706b560b61527cc217ddf14da41770e8ed6607190a1851b8", + "nodeId2" : "03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134", + "bitcoinKey1" : "02eff5309b9368340edc6114d738b3590e6969bec4e95d8a080cf185e8b9ce5e4b", + "bitcoinKey2" : "023ced05ed1ab328b67477376d68a69ecd0f371a9d5843c6c3be4d31498d516d8d", + "tlvStream" : { + "records" : [ ], + "unknown" : [ ] + } + }, + "channelUpdate" : { + "signature" : "710d73875607575f3d84bb507dd87cca5b85f0cdac84f4ccecce7af3a55897525a45070fe26c0ea43e9580d4ea4cfa62ee3273e5546911145cba6bbf56e59d8e", + "chainHash" : "43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000", + "shortChannelId" : "2026958x1x0", + "timestamp" : { + "iso" : "2021-07-08T12:09:56Z", + "unix" : 1625746196 + }, + "channelFlags" : { + "isEnabled" : true, + "isNode1" : false + }, + "cltvExpiryDelta" : 144, + "htlcMinimumMsat" : 1, + "feeBaseMsat" : 1000, + "feeProportionalMillionths" : 100, + "htlcMaximumMsat" : 15000000000, + "tlvStream" : { + "records" : [ ], + "unknown" : [ ] + } + } +} \ No newline at end of file diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/EclairImplSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/EclairImplSpec.scala index 949dcc7014..9d0ab122a7 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/EclairImplSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/EclairImplSpec.scala @@ -267,35 +267,35 @@ class EclairImplSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with I val eclair = new EclairImpl(kit) eclair.forceClose(Left(ByteVector32.Zeroes) :: Nil) - register.expectMsg(Register.Forward(ActorRef.noSender, ByteVector32.Zeroes, CMD_FORCECLOSE(ActorRef.noSender))) + register.expectMsg(Register.Forward(null, ByteVector32.Zeroes, CMD_FORCECLOSE(ActorRef.noSender))) eclair.forceClose(Right(ShortChannelId.fromCoordinates("568749x2597x0").success.value) :: Nil) - register.expectMsg(Register.ForwardShortId(ActorRef.noSender, ShortChannelId.fromCoordinates("568749x2597x0").success.value, CMD_FORCECLOSE(ActorRef.noSender))) + register.expectMsg(Register.ForwardShortId(null, ShortChannelId.fromCoordinates("568749x2597x0").success.value, CMD_FORCECLOSE(ActorRef.noSender))) eclair.forceClose(Left(ByteVector32.Zeroes) :: Right(ShortChannelId.fromCoordinates("568749x2597x0").success.value) :: Nil) register.expectMsgAllOf( - Register.Forward(ActorRef.noSender, ByteVector32.Zeroes, CMD_FORCECLOSE(ActorRef.noSender)), - Register.ForwardShortId(ActorRef.noSender, ShortChannelId.fromCoordinates("568749x2597x0").success.value, CMD_FORCECLOSE(ActorRef.noSender)) + Register.Forward(null, ByteVector32.Zeroes, CMD_FORCECLOSE(ActorRef.noSender)), + Register.ForwardShortId(null, ShortChannelId.fromCoordinates("568749x2597x0").success.value, CMD_FORCECLOSE(ActorRef.noSender)) ) eclair.close(Left(ByteVector32.Zeroes) :: Nil, None, None) - register.expectMsg(Register.Forward(ActorRef.noSender, ByteVector32.Zeroes, CMD_CLOSE(ActorRef.noSender, None, None))) + register.expectMsg(Register.Forward(null, ByteVector32.Zeroes, CMD_CLOSE(ActorRef.noSender, None, None))) val customClosingFees = ClosingFeerates(FeeratePerKw(500 sat), FeeratePerKw(200 sat), FeeratePerKw(1000 sat)) eclair.close(Left(ByteVector32.Zeroes) :: Nil, None, Some(customClosingFees)) - register.expectMsg(Register.Forward(ActorRef.noSender, ByteVector32.Zeroes, CMD_CLOSE(ActorRef.noSender, None, Some(customClosingFees)))) + register.expectMsg(Register.Forward(null, ByteVector32.Zeroes, CMD_CLOSE(ActorRef.noSender, None, Some(customClosingFees)))) eclair.close(Right(ShortChannelId.fromCoordinates("568749x2597x0").success.value) :: Nil, None, None) - register.expectMsg(Register.ForwardShortId(ActorRef.noSender, ShortChannelId.fromCoordinates("568749x2597x0").success.value, CMD_CLOSE(ActorRef.noSender, None, None))) + register.expectMsg(Register.ForwardShortId(null, ShortChannelId.fromCoordinates("568749x2597x0").success.value, CMD_CLOSE(ActorRef.noSender, None, None))) eclair.close(Right(ShortChannelId.fromCoordinates("568749x2597x0").success.value) :: Nil, Some(ByteVector.empty), Some(customClosingFees)) - register.expectMsg(Register.ForwardShortId(ActorRef.noSender, ShortChannelId.fromCoordinates("568749x2597x0").success.value, CMD_CLOSE(ActorRef.noSender, Some(ByteVector.empty), Some(customClosingFees)))) + register.expectMsg(Register.ForwardShortId(null, ShortChannelId.fromCoordinates("568749x2597x0").success.value, CMD_CLOSE(ActorRef.noSender, Some(ByteVector.empty), Some(customClosingFees)))) eclair.close(Right(ShortChannelId.fromCoordinates("568749x2597x0").success.value) :: Left(ByteVector32.One) :: Right(ShortChannelId.fromCoordinates("568749x2597x1").success.value) :: Nil, None, None) register.expectMsgAllOf( - Register.ForwardShortId(ActorRef.noSender, ShortChannelId.fromCoordinates("568749x2597x0").success.value, CMD_CLOSE(ActorRef.noSender, None, None)), - Register.Forward(ActorRef.noSender, ByteVector32.One, CMD_CLOSE(ActorRef.noSender, None, None)), - Register.ForwardShortId(ActorRef.noSender, ShortChannelId.fromCoordinates("568749x2597x1").success.value, CMD_CLOSE(ActorRef.noSender, None, None)) + Register.ForwardShortId(null, ShortChannelId.fromCoordinates("568749x2597x0").success.value, CMD_CLOSE(ActorRef.noSender, None, None)), + Register.Forward(null, ByteVector32.One, CMD_CLOSE(ActorRef.noSender, None, None)), + Register.ForwardShortId(null, ShortChannelId.fromCoordinates("568749x2597x1").success.value, CMD_CLOSE(ActorRef.noSender, None, None)) ) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/ChannelFeaturesSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/ChannelFeaturesSpec.scala index d992176daa..280ddbf077 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/ChannelFeaturesSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/ChannelFeaturesSpec.scala @@ -82,7 +82,7 @@ class ChannelFeaturesSpec extends TestKitBaseClass with AnyFunSuiteLike with Cha ) for (testCase <- testCases) { - assert(ChannelTypes.defaultFromFeatures(testCase.localFeatures, testCase.remoteFeatures, testCase.announceChannel) == testCase.expectedChannelType, s"localFeatures=${testCase.localFeatures} remoteFeatures=${testCase.remoteFeatures}") + assert(ChannelTypes.defaultFromFeatures(testCase.localFeatures, testCase.remoteFeatures, announceChannel = testCase.announceChannel) == testCase.expectedChannelType, s"localFeatures=${testCase.localFeatures} remoteFeatures=${testCase.remoteFeatures}") } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/RegisterSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/RegisterSpec.scala index c6a9b6aaac..ec4ab0cea6 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/RegisterSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/RegisterSpec.scala @@ -15,7 +15,7 @@ class RegisterSpec extends TestKitBaseClass with AnyFunSuiteLike with ParallelTe test("register processes custom restored events") { val sender = TestProbe() - val registerRef = system.actorOf(Props(new Register)) + val registerRef = system.actorOf(Register.props()) val customRestoredEvent = CustomChannelRestored(TestProbe().ref, randomBytes32(), TestProbe().ref, randomKey().publicKey) registerRef ! customRestoredEvent sender.send(registerRef, Symbol("channels")) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala index 5f279ef20a..48b031fd52 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala @@ -177,7 +177,7 @@ trait ChannelStateTestsBase extends Assertions with Eventually { .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.ScidAlias))(_.updated(Features.ScidAlias, FeatureSupport.Optional)) .initFeatures() - val channelType = ChannelTypes.defaultFromFeatures(aliceInitFeatures, bobInitFeatures, channelFlags.announceChannel) + val channelType = ChannelTypes.defaultFromFeatures(aliceInitFeatures, bobInitFeatures, announceChannel = channelFlags.announceChannel) // those features can only be enabled with AnchorOutputsZeroFeeHtlcTxs, this is to prevent incompatible test configurations if (tags.contains(ChannelStateTestsTags.ZeroConf)) assert(tags.contains(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs), "invalid test configuration") diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/ChannelIntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/ChannelIntegrationSpec.scala index ff22a84393..f757378879 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/ChannelIntegrationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/ChannelIntegrationSpec.scala @@ -17,6 +17,7 @@ package fr.acinq.eclair.integration import akka.actor.ActorRef +import akka.actor.typed.scaladsl.adapter.ClassicActorRefOps import akka.pattern.pipe import akka.testkit.TestProbe import com.typesafe.config.ConfigFactory @@ -152,11 +153,11 @@ abstract class ChannelIntegrationSpec extends IntegrationSpec { // F gets the htlc val htlc = htlcReceiver.expectMsgType[IncomingPaymentPacket.FinalPacket](max = 60 seconds).add // now that we have the channel id, we retrieve channels default final addresses - sender.send(nodes("C").register, Register.Forward(sender.ref, htlc.channelId, CMD_GET_CHANNEL_DATA(ActorRef.noSender))) + sender.send(nodes("C").register, Register.Forward(sender.ref.toTyped[Any], htlc.channelId, CMD_GET_CHANNEL_DATA(ActorRef.noSender))) val dataC = sender.expectMsgType[RES_GET_CHANNEL_DATA[DATA_NORMAL]].data assert(dataC.commitments.commitmentFormat == commitmentFormat) val finalAddressC = scriptPubKeyToAddress(dataC.commitments.localParams.defaultFinalScriptPubKey) - sender.send(nodes("F").register, Register.Forward(sender.ref, htlc.channelId, CMD_GET_CHANNEL_DATA(ActorRef.noSender))) + sender.send(nodes("F").register, Register.Forward(sender.ref.toTyped[Any], htlc.channelId, CMD_GET_CHANNEL_DATA(ActorRef.noSender))) val dataF = sender.expectMsgType[RES_GET_CHANNEL_DATA[DATA_NORMAL]].data assert(dataF.commitments.commitmentFormat == commitmentFormat) val finalAddressF = scriptPubKeyToAddress(dataF.commitments.localParams.defaultFinalScriptPubKey) @@ -172,14 +173,14 @@ abstract class ChannelIntegrationSpec extends IntegrationSpec { // we then kill the connection between C and F disconnectCF(htlc.channelId, sender) // we then have C unilaterally close the channel (which will make F redeem the htlc onchain) - sender.send(nodes("C").register, Register.Forward(sender.ref, htlc.channelId, CMD_FORCECLOSE(sender.ref))) + sender.send(nodes("C").register, Register.Forward(sender.ref.toTyped[Any], htlc.channelId, CMD_FORCECLOSE(sender.ref))) sender.expectMsgType[RES_SUCCESS[CMD_FORCECLOSE]] // we then wait for F to detect the unilateral close and go to CLOSING state awaitCond(stateListenerF.expectMsgType[ChannelStateChanged](max = 60 seconds).currentState == CLOSING, max = 60 seconds) // we generate a few blocks to get the commit tx confirmed generateBlocks(3, Some(minerAddress)) // we then fulfill the htlc, which will make F redeem it on-chain - sender.send(nodes("F").register, Register.Forward(sender.ref, htlc.channelId, CMD_FULFILL_HTLC(htlc.id, preimage))) + sender.send(nodes("F").register, Register.Forward(sender.ref.toTyped[Any], htlc.channelId, CMD_FULFILL_HTLC(htlc.id, preimage))) // we don't need to generate blocks to confirm the htlc-success; C should extract the preimage as soon as it enters // the mempool and fulfill the payment upstream. paymentSender.expectMsgType[PaymentSent](max = 60 seconds) @@ -209,14 +210,14 @@ abstract class ChannelIntegrationSpec extends IntegrationSpec { // we then kill the connection between C and F disconnectCF(htlc.channelId, sender) // then we have F unilaterally close the channel - sender.send(nodes("F").register, Register.Forward(sender.ref, htlc.channelId, CMD_FORCECLOSE(sender.ref))) + sender.send(nodes("F").register, Register.Forward(sender.ref.toTyped[Any], htlc.channelId, CMD_FORCECLOSE(sender.ref))) sender.expectMsgType[RES_SUCCESS[CMD_FORCECLOSE]] awaitCond(stateListenerC.expectMsgType[ChannelStateChanged](max = 60 seconds).currentState == CLOSING, max = 60 seconds) awaitCond(stateListenerF.expectMsgType[ChannelStateChanged](max = 60 seconds).currentState == CLOSING, max = 60 seconds) // we generate a few blocks to get the commit tx confirmed generateBlocks(3, Some(minerAddress)) // we then fulfill the htlc (it won't be sent to C, and will be used to pull funds on-chain) - sender.send(nodes("F").register, Register.Forward(sender.ref, htlc.channelId, CMD_FULFILL_HTLC(htlc.id, preimage))) + sender.send(nodes("F").register, Register.Forward(sender.ref.toTyped[Any], htlc.channelId, CMD_FULFILL_HTLC(htlc.id, preimage))) // we don't need to generate blocks to confirm the htlc-success; C should extract the preimage as soon as it enters // the mempool and fulfill the payment upstream. paymentSender.expectMsgType[PaymentSent](max = 60 seconds) @@ -251,7 +252,7 @@ abstract class ChannelIntegrationSpec extends IntegrationSpec { generateBlocks((htlc.cltvExpiry.blockHeight - getBlockHeight()).toInt, Some(minerAddress)) awaitCond(stateListenerC.expectMsgType[ChannelStateChanged](max = 60 seconds).currentState == CLOSING, max = 60 seconds) awaitCond(stateListenerF.expectMsgType[ChannelStateChanged](max = 60 seconds).currentState == CLOSING, max = 60 seconds) - sender.send(nodes("C").register, Register.Forward(sender.ref, htlc.channelId, CMD_GET_CHANNEL_DATA(ActorRef.noSender))) + sender.send(nodes("C").register, Register.Forward(sender.ref.toTyped[Any], htlc.channelId, CMD_GET_CHANNEL_DATA(ActorRef.noSender))) val Some(localCommit) = sender.expectMsgType[RES_GET_CHANNEL_DATA[DATA_CLOSING]].data.localCommitPublished // we wait until the commit tx has been broadcast val bitcoinClient = new BitcoinCoreClient(bitcoinrpcclient) @@ -295,17 +296,17 @@ abstract class ChannelIntegrationSpec extends IntegrationSpec { // we then kill the connection between C and F to ensure the close can only be detected on-chain disconnectCF(htlc.channelId, sender) // we ask F to unilaterally close the channel - sender.send(nodes("F").register, Register.Forward(sender.ref, htlc.channelId, CMD_FORCECLOSE(sender.ref))) + sender.send(nodes("F").register, Register.Forward(sender.ref.toTyped[Any], htlc.channelId, CMD_FORCECLOSE(sender.ref))) sender.expectMsgType[RES_SUCCESS[CMD_FORCECLOSE]] // we wait for C to detect the unilateral close awaitCond({ - sender.send(nodes("C").register, Register.Forward(sender.ref, htlc.channelId, CMD_GET_CHANNEL_DATA(ActorRef.noSender))) + sender.send(nodes("C").register, Register.Forward(sender.ref.toTyped[Any], htlc.channelId, CMD_GET_CHANNEL_DATA(ActorRef.noSender))) sender.expectMsgType[RES_GET_CHANNEL_DATA[ChannelData]].data match { case d: DATA_CLOSING if d.remoteCommitPublished.nonEmpty => true case _ => false } }, max = 30 seconds, interval = 1 second) - sender.send(nodes("C").register, Register.Forward(sender.ref, htlc.channelId, CMD_GET_CHANNEL_DATA(ActorRef.noSender))) + sender.send(nodes("C").register, Register.Forward(sender.ref.toTyped[Any], htlc.channelId, CMD_GET_CHANNEL_DATA(ActorRef.noSender))) val Some(remoteCommit) = sender.expectMsgType[RES_GET_CHANNEL_DATA[DATA_CLOSING]].data.remoteCommitPublished // we generate enough blocks to make the htlc timeout generateBlocks((htlc.cltvExpiry.blockHeight - getBlockHeight()).toInt, Some(minerAddress)) @@ -422,7 +423,7 @@ abstract class ChannelIntegrationSpec extends IntegrationSpec { // we then generate blocks to make htlcs timeout (nothing will happen in the channel because all of them have already been fulfilled) generateBlocks(40) // we retrieve C's default final address - sender.send(nodes("C").register, Register.Forward(sender.ref, commitmentsF.channelId, CMD_GET_CHANNEL_DATA(ActorRef.noSender))) + sender.send(nodes("C").register, Register.Forward(sender.ref.toTyped[Any], commitmentsF.channelId, CMD_GET_CHANNEL_DATA(ActorRef.noSender))) val finalAddressC = scriptPubKeyToAddress(sender.expectMsgType[RES_GET_CHANNEL_DATA[DATA_NORMAL]].data.commitments.localParams.defaultFinalScriptPubKey) // we prepare the revoked transactions F will publish val keyManagerF = nodes("F").nodeParams.channelKeyManager @@ -494,18 +495,18 @@ class StandardChannelIntegrationSpec extends ChannelIntegrationSpec { sender.send(fundeeChannel, CMD_GET_CHANNEL_DATA(ActorRef.noSender)) val channelId = sender.expectMsgType[RES_GET_CHANNEL_DATA[PersistentChannelData]].data.channelId awaitCond({ - funder.register ! Register.Forward(sender.ref, channelId, CMD_GET_CHANNEL_STATE(ActorRef.noSender)) + funder.register ! Register.Forward(sender.ref.toTyped[Any], channelId, CMD_GET_CHANNEL_STATE(ActorRef.noSender)) sender.expectMsgType[RES_GET_CHANNEL_STATE].state == WAIT_FOR_CHANNEL_READY }) generateBlocks(6) // after 8 blocks the fundee is still waiting for more confirmations - fundee.register ! Register.Forward(sender.ref, channelId, CMD_GET_CHANNEL_STATE(ActorRef.noSender)) + fundee.register ! Register.Forward(sender.ref.toTyped[Any], channelId, CMD_GET_CHANNEL_STATE(ActorRef.noSender)) assert(sender.expectMsgType[RES_GET_CHANNEL_STATE].state == WAIT_FOR_FUNDING_CONFIRMED) // after 8 blocks the funder is still waiting for funding_locked from the fundee - funder.register ! Register.Forward(sender.ref, channelId, CMD_GET_CHANNEL_STATE(ActorRef.noSender)) + funder.register ! Register.Forward(sender.ref.toTyped[Any], channelId, CMD_GET_CHANNEL_STATE(ActorRef.noSender)) assert(sender.expectMsgType[RES_GET_CHANNEL_STATE].state == WAIT_FOR_CHANNEL_READY) // simulate a disconnection @@ -513,9 +514,9 @@ class StandardChannelIntegrationSpec extends ChannelIntegrationSpec { assert(sender.expectMsgType[String] == "disconnecting") awaitCond({ - fundee.register ! Register.Forward(sender.ref, channelId, CMD_GET_CHANNEL_STATE(ActorRef.noSender)) + fundee.register ! Register.Forward(sender.ref.toTyped[Any], channelId, CMD_GET_CHANNEL_STATE(ActorRef.noSender)) val fundeeState = sender.expectMsgType[RES_GET_CHANNEL_STATE].state - funder.register ! Register.Forward(sender.ref, channelId, CMD_GET_CHANNEL_STATE(ActorRef.noSender)) + funder.register ! Register.Forward(sender.ref.toTyped[Any], channelId, CMD_GET_CHANNEL_STATE(ActorRef.noSender)) val funderState = sender.expectMsgType[RES_GET_CHANNEL_STATE].state fundeeState == OFFLINE && funderState == OFFLINE }) @@ -531,9 +532,9 @@ class StandardChannelIntegrationSpec extends ChannelIntegrationSpec { )) sender.expectMsgType[PeerConnection.ConnectionResult.HasConnection](30 seconds) - fundee.register ! Register.Forward(sender.ref, channelId, CMD_GET_CHANNEL_STATE(ActorRef.noSender)) + fundee.register ! Register.Forward(sender.ref.toTyped[Any], channelId, CMD_GET_CHANNEL_STATE(ActorRef.noSender)) val fundeeState = sender.expectMsgType[RES_GET_CHANNEL_STATE].state - funder.register ! Register.Forward(sender.ref, channelId, CMD_GET_CHANNEL_STATE(ActorRef.noSender)) + funder.register ! Register.Forward(sender.ref.toTyped[Any], channelId, CMD_GET_CHANNEL_STATE(ActorRef.noSender)) val funderState = sender.expectMsgType[RES_GET_CHANNEL_STATE].state fundeeState == WAIT_FOR_FUNDING_CONFIRMED && funderState == WAIT_FOR_CHANNEL_READY }, max = 30 seconds, interval = 10 seconds) @@ -542,9 +543,9 @@ class StandardChannelIntegrationSpec extends ChannelIntegrationSpec { generateBlocks(5) awaitCond({ - fundee.register ! Register.Forward(sender.ref, channelId, CMD_GET_CHANNEL_STATE(ActorRef.noSender)) + fundee.register ! Register.Forward(sender.ref.toTyped[Any], channelId, CMD_GET_CHANNEL_STATE(ActorRef.noSender)) val fundeeState = sender.expectMsgType[RES_GET_CHANNEL_STATE].state - funder.register ! Register.Forward(sender.ref, channelId, CMD_GET_CHANNEL_STATE(ActorRef.noSender)) + funder.register ! Register.Forward(sender.ref.toTyped[Any], channelId, CMD_GET_CHANNEL_STATE(ActorRef.noSender)) val funderState = sender.expectMsgType[RES_GET_CHANNEL_STATE].state fundeeState == NORMAL && funderState == NORMAL }) @@ -555,14 +556,14 @@ class StandardChannelIntegrationSpec extends ChannelIntegrationSpec { funder.system.eventStream.subscribe(stateListener.ref, classOf[ChannelStateChanged]) // close that wumbo channel - sender.send(funder.register, Register.Forward(sender.ref, channelId, CMD_GET_CHANNEL_DATA(ActorRef.noSender))) + sender.send(funder.register, Register.Forward(sender.ref.toTyped[Any], channelId, CMD_GET_CHANNEL_DATA(ActorRef.noSender))) val commitmentsC = sender.expectMsgType[RES_GET_CHANNEL_DATA[DATA_NORMAL]].data.commitments val finalPubKeyScriptC = commitmentsC.localParams.defaultFinalScriptPubKey val fundingOutpoint = commitmentsC.commitInput.outPoint - sender.send(fundee.register, Register.Forward(sender.ref, channelId, CMD_GET_CHANNEL_DATA(ActorRef.noSender))) + sender.send(fundee.register, Register.Forward(sender.ref.toTyped[Any], channelId, CMD_GET_CHANNEL_DATA(ActorRef.noSender))) val finalPubKeyScriptF = sender.expectMsgType[RES_GET_CHANNEL_DATA[DATA_NORMAL]].data.commitments.localParams.defaultFinalScriptPubKey - fundee.register ! Register.Forward(sender.ref, channelId, CMD_CLOSE(sender.ref, Some(finalPubKeyScriptF), None)) + fundee.register ! Register.Forward(sender.ref.toTyped[Any], channelId, CMD_CLOSE(sender.ref, Some(finalPubKeyScriptF), None)) sender.expectMsgType[RES_SUCCESS[CMD_CLOSE]] // we then wait for C and F to negotiate the closing fee awaitCond(stateListener.expectMsgType[ChannelStateChanged](max = 60 seconds).currentState == CLOSING, max = 60 seconds) @@ -667,7 +668,7 @@ abstract class AnchorChannelIntegrationSpec extends ChannelIntegrationSpec { // retrieve the channelId of C <--> F val Some(channelId) = sender.expectMsgType[Map[ByteVector32, PublicKey]].find(_._2 == nodes("C").nodeParams.nodeId).map(_._1) - sender.send(nodes("F").register, Register.Forward(sender.ref, channelId, CMD_GET_CHANNEL_DATA(ActorRef.noSender))) + sender.send(nodes("F").register, Register.Forward(sender.ref.toTyped[Any], channelId, CMD_GET_CHANNEL_DATA(ActorRef.noSender))) val initialStateDataF = sender.expectMsgType[RES_GET_CHANNEL_DATA[DATA_NORMAL]].data assert(initialStateDataF.commitments.channelType == expectedChannelType) val initialCommitmentIndex = initialStateDataF.commitments.localCommit.index @@ -692,12 +693,12 @@ abstract class AnchorChannelIntegrationSpec extends ChannelIntegrationSpec { // we make sure the htlc has been removed from F's commitment before we force-close awaitCond({ - sender.send(nodes("F").register, Register.Forward(sender.ref, channelId, CMD_GET_CHANNEL_DATA(ActorRef.noSender))) + sender.send(nodes("F").register, Register.Forward(sender.ref.toTyped[Any], channelId, CMD_GET_CHANNEL_DATA(ActorRef.noSender))) val stateDataF = sender.expectMsgType[RES_GET_CHANNEL_DATA[DATA_NORMAL]].data stateDataF.commitments.localCommit.spec.htlcs.isEmpty }, max = 20 seconds, interval = 1 second) - sender.send(nodes("F").register, Register.Forward(sender.ref, channelId, CMD_GET_CHANNEL_DATA(ActorRef.noSender))) + sender.send(nodes("F").register, Register.Forward(sender.ref.toTyped[Any], channelId, CMD_GET_CHANNEL_DATA(ActorRef.noSender))) val stateDataF = sender.expectMsgType[RES_GET_CHANNEL_DATA[DATA_NORMAL]].data val commitmentIndex = stateDataF.commitments.localCommit.index val commitTx = stateDataF.commitments.localCommit.commitTxAndRemoteSig.commitTx.tx @@ -716,7 +717,7 @@ abstract class AnchorChannelIntegrationSpec extends ChannelIntegrationSpec { // we kill the connection between C and F to ensure the close can only be detected on-chain disconnectCF(channelId, sender) // now let's force close the channel and check the toRemote is what we had at the beginning - sender.send(nodes("F").register, Register.Forward(sender.ref, channelId, CMD_FORCECLOSE(sender.ref))) + sender.send(nodes("F").register, Register.Forward(sender.ref.toTyped[Any], channelId, CMD_FORCECLOSE(sender.ref))) sender.expectMsgType[RES_SUCCESS[CMD_FORCECLOSE]] // we then wait for C to detect the unilateral close and go to CLOSING state awaitCond(stateListener.expectMsgType[ChannelStateChanged](max = 60 seconds).currentState == CLOSING, max = 60 seconds) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/PaymentIntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/PaymentIntegrationSpec.scala index e0d7bcca0b..635a17013c 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/PaymentIntegrationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/PaymentIntegrationSpec.scala @@ -17,7 +17,7 @@ package fr.acinq.eclair.integration import akka.actor.ActorRef -import akka.actor.typed.scaladsl.adapter.actorRefAdapter +import akka.actor.typed.scaladsl.adapter.{ClassicActorRefOps, actorRefAdapter} import akka.testkit.TestProbe import com.typesafe.config.ConfigFactory import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} @@ -178,7 +178,7 @@ class PaymentIntegrationSpec extends IntegrationSpec { sender.send(nodes("B").router, Router.GetChannels) val shortIdBC = sender.expectMsgType[Iterable[ChannelAnnouncement]].find(c => Set(c.nodeId1, c.nodeId2) == Set(nodes("B").nodeParams.nodeId, nodes("C").nodeParams.nodeId)).get.shortChannelId // we also need the full commitment - nodes("B").register ! Register.ForwardShortId(sender.ref, shortIdBC, CMD_GET_CHANNEL_INFO(ActorRef.noSender)) + nodes("B").register ! Register.ForwardShortId(sender.ref.toTyped[Any], shortIdBC, CMD_GET_CHANNEL_INFO(ActorRef.noSender)) val normalBC = sender.expectMsgType[RES_GET_CHANNEL_INFO].data.asInstanceOf[DATA_NORMAL] // we then forge a new channel_update for B-C... val channelUpdateBC = Announcements.makeChannelUpdate(Block.RegtestGenesisBlock.hash, nodes("B").nodeParams.privateKey, nodes("C").nodeParams.nodeId, shortIdBC, nodes("B").nodeParams.channelConf.expiryDelta + 1, nodes("C").nodeParams.channelConf.htlcMinimum, nodes("B").nodeParams.relayParams.publicChannelFees.feeBase, nodes("B").nodeParams.relayParams.publicChannelFees.feeProportionalMillionths, 500000000 msat) @@ -209,8 +209,8 @@ class PaymentIntegrationSpec extends IntegrationSpec { // first let's wait 3 seconds to make sure the timestamp of the new channel_update will be strictly greater than the former sender.expectNoMessage(3 seconds) - nodes("B").register ! Register.ForwardShortId(sender.ref, shortIdBC, BroadcastChannelUpdate(PeriodicRefresh)) - nodes("B").register ! Register.ForwardShortId(sender.ref, shortIdBC, CMD_GET_CHANNEL_INFO(ActorRef.noSender)) + nodes("B").register ! Register.ForwardShortId(sender.ref.toTyped[Any], shortIdBC, BroadcastChannelUpdate(PeriodicRefresh)) + nodes("B").register ! Register.ForwardShortId(sender.ref.toTyped[Any], shortIdBC, CMD_GET_CHANNEL_INFO(ActorRef.noSender)) val channelUpdateBC_new = sender.expectMsgType[RES_GET_CHANNEL_INFO].data.asInstanceOf[DATA_NORMAL].channelUpdate logger.info(s"channelUpdateBC=$channelUpdateBC") logger.info(s"channelUpdateBC_new=$channelUpdateBC_new") diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/fixtures/MinimalNodeFixture.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/fixtures/MinimalNodeFixture.scala index e1ca252d93..cd6ba696e9 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/fixtures/MinimalNodeFixture.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/fixtures/MinimalNodeFixture.scala @@ -1,7 +1,7 @@ package fr.acinq.eclair.integration.basic.fixtures import akka.actor.typed.scaladsl.adapter.ClassicActorRefOps -import akka.actor.{ActorRef, ActorSystem, Props} +import akka.actor.{ActorRef, ActorSystem} import akka.testkit.{TestActor, TestProbe} import com.softwaremill.quicklens.ModifyPimp import com.typesafe.config.ConfigFactory @@ -77,7 +77,7 @@ object MinimalNodeFixture extends Assertions { val wallet = new DummyOnChainWallet() val watcher = TestProbe("watcher") val watcherTyped = watcher.ref.toTyped[ZmqWatcher.Command] - val register = system.actorOf(Props(new Register), "register") + val register = system.actorOf(Register.props(), "register") val router = system.actorOf(Router.props(nodeParams, watcherTyped), "router") val paymentHandler = system.actorOf(PaymentHandler.props(nodeParams, register), "payment-handler") val relayer = system.actorOf(Relayer.props(nodeParams, router, register, paymentHandler), "relayer") @@ -221,13 +221,13 @@ object MinimalNodeFixture extends Assertions { def getChannelState(node: MinimalNodeFixture, channelId: ByteVector32)(implicit system: ActorSystem): ChannelState = { val sender = TestProbe("sender") - node.register ! Register.Forward(sender.ref, channelId, CMD_GET_CHANNEL_STATE(sender.ref)) + node.register ! Register.Forward(sender.ref.toTyped, channelId, CMD_GET_CHANNEL_STATE(sender.ref)) sender.expectMsgType[RES_GET_CHANNEL_STATE].state } def getChannelData(node: MinimalNodeFixture, channelId: ByteVector32)(implicit system: ActorSystem): ChannelData = { val sender = TestProbe("sender") - node.register ! Register.Forward(sender.ref, channelId, CMD_GET_CHANNEL_DATA(sender.ref)) + node.register ! Register.Forward(sender.ref.toTyped, channelId, CMD_GET_CHANNEL_DATA(sender.ref)) sender.expectMsgType[RES_GET_CHANNEL_DATA[ChannelData]].data } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartHandlerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartHandlerSpec.scala index e661906341..bec51d6705 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartHandlerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartHandlerSpec.scala @@ -373,8 +373,8 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike val commands = f.register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]] :: f.register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]] :: Nil assert(commands.toSet == Set( - Register.Forward(ActorRef.noSender, ByteVector32.One, CMD_FAIL_HTLC(0, Right(PaymentTimeout), commit = true)), - Register.Forward(ActorRef.noSender, ByteVector32.One, CMD_FAIL_HTLC(1, Right(PaymentTimeout), commit = true)) + Register.Forward(null, ByteVector32.One, CMD_FAIL_HTLC(0, Right(PaymentTimeout), commit = true)), + Register.Forward(null, ByteVector32.One, CMD_FAIL_HTLC(1, Right(PaymentTimeout), commit = true)) )) awaitCond({ f.sender.send(handler, GetPendingPayments) @@ -383,7 +383,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike // Extraneous HTLCs should be failed. f.sender.send(handler, MultiPartPaymentFSM.ExtraPaymentReceived(pr1.paymentHash, HtlcPart(1000 msat, UpdateAddHtlc(ByteVector32.One, 42, 200 msat, pr1.paymentHash, add1.cltvExpiry, add1.onionRoutingPacket)), Some(PaymentTimeout))) - f.register.expectMsg(Register.Forward(ActorRef.noSender, ByteVector32.One, CMD_FAIL_HTLC(42, Right(PaymentTimeout), commit = true))) + f.register.expectMsg(Register.Forward(null, ByteVector32.One, CMD_FAIL_HTLC(42, Right(PaymentTimeout), commit = true))) // The payment should still be pending in DB. val Some(incomingPayment) = nodeParams.db.payments.getIncomingPayment(pr1.paymentHash) @@ -407,9 +407,9 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add3, PaymentOnion.createMultiPartPayload(add3.amountMsat, 1000 msat, add3.cltvExpiry, invoice.paymentSecret.get, invoice.paymentMetadata))) f.register.expectMsgAllOf( - Register.Forward(ActorRef.noSender, add2.channelId, CMD_FAIL_HTLC(add2.id, Right(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight)), commit = true)), - Register.Forward(ActorRef.noSender, add1.channelId, CMD_FULFILL_HTLC(add1.id, preimage, commit = true)), - Register.Forward(ActorRef.noSender, add3.channelId, CMD_FULFILL_HTLC(add3.id, preimage, commit = true)) + Register.Forward(null, add2.channelId, CMD_FAIL_HTLC(add2.id, Right(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight)), commit = true)), + Register.Forward(null, add1.channelId, CMD_FULFILL_HTLC(add1.id, preimage, commit = true)), + Register.Forward(null, add3.channelId, CMD_FULFILL_HTLC(add3.id, preimage, commit = true)) ) val paymentReceived = f.eventListener.expectMsgType[PaymentReceived] @@ -424,7 +424,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike // Extraneous HTLCs should be fulfilled. f.sender.send(handler, MultiPartPaymentFSM.ExtraPaymentReceived(invoice.paymentHash, HtlcPart(1000 msat, UpdateAddHtlc(ByteVector32.One, 44, 200 msat, invoice.paymentHash, add1.cltvExpiry, add1.onionRoutingPacket)), None)) - f.register.expectMsg(Register.Forward(ActorRef.noSender, ByteVector32.One, CMD_FULFILL_HTLC(44, preimage, commit = true))) + f.register.expectMsg(Register.Forward(null, ByteVector32.One, CMD_FULFILL_HTLC(44, preimage, commit = true))) assert(f.eventListener.expectMsgType[PaymentReceived].amount == 200.msat) val received2 = nodeParams.db.payments.getIncomingPayment(invoice.paymentHash) assert(received2.get.status.asInstanceOf[IncomingPaymentStatus.Received].amount == 1200.msat) @@ -445,7 +445,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike val add1 = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, invoice.paymentHash, f.defaultExpiry, TestConstants.emptyOnionPacket) f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add1, PaymentOnion.createMultiPartPayload(add1.amountMsat, 1000 msat, add1.cltvExpiry, invoice.paymentSecret.get, invoice.paymentMetadata))) - f.register.expectMsg(Register.Forward(ActorRef.noSender, ByteVector32.One, CMD_FAIL_HTLC(0, Right(PaymentTimeout), commit = true))) + f.register.expectMsg(Register.Forward(null, ByteVector32.One, CMD_FAIL_HTLC(0, Right(PaymentTimeout), commit = true))) awaitCond({ f.sender.send(handler, GetPendingPayments) f.sender.expectMsgType[PendingPayments].paymentHashes.isEmpty @@ -458,8 +458,8 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike // the fulfill are not necessarily in the same order as the commands f.register.expectMsgAllOf( - Register.Forward(ActorRef.noSender, add2.channelId, CMD_FULFILL_HTLC(2, preimage, commit = true)), - Register.Forward(ActorRef.noSender, add3.channelId, CMD_FULFILL_HTLC(5, preimage, commit = true)) + Register.Forward(null, add2.channelId, CMD_FULFILL_HTLC(2, preimage, commit = true)), + Register.Forward(null, add3.channelId, CMD_FULFILL_HTLC(5, preimage, commit = true)) ) val paymentReceived = f.eventListener.expectMsgType[PaymentReceived] @@ -510,7 +510,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike val add = UpdateAddHtlc(ByteVector32.One, 0, amountMsat, paymentHash, defaultExpiry, TestConstants.emptyOnionPacket) sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, payload)) - f.register.expectMsg(Register.Forward(ActorRef.noSender, add.channelId, CMD_FAIL_HTLC(add.id, Right(IncorrectOrUnknownPaymentDetails(42000 msat, nodeParams.currentBlockHeight)), commit = true))) + f.register.expectMsg(Register.Forward(null, add.channelId, CMD_FAIL_HTLC(add.id, Right(IncorrectOrUnknownPaymentDetails(42000 msat, nodeParams.currentBlockHeight)), commit = true))) assert(nodeParams.db.payments.getIncomingPayment(paymentHash) == None) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala index 1ad2e17f6a..36443b7bfe 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala @@ -18,6 +18,7 @@ package fr.acinq.eclair.payment import akka.actor.ActorRef import akka.actor.FSM.{CurrentState, SubscribeTransitionCallBack, Transition} +import akka.actor.typed.scaladsl.adapter._ import akka.testkit.{TestFSMRef, TestProbe} import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.bitcoin.scalacompat.Script.{pay2wsh, write} @@ -310,7 +311,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val WaitingForComplete(_, cmd1, Nil, _, ignore1, route) = paymentFSM.stateData assert(ignore1.nodes.isEmpty) - register.expectMsg(ForwardShortId(paymentFSM, scid_ab, cmd1)) + register.expectMsg(ForwardShortId(paymentFSM.toTyped, scid_ab, cmd1)) sender.send(paymentFSM, addCompleted(HtlcResult.RemoteFail(UpdateFailHtlc(ByteVector32.Zeroes, 0, randomBytes32())))) // unparsable message // then the payment lifecycle will ask for a new route excluding all intermediate nodes @@ -322,7 +323,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val WaitingForComplete(_, cmd2, _, _, ignore2, _) = paymentFSM.stateData assert(ignore2.nodes == Set(c)) // and reply a 2nd time with an unparsable failure - register.expectMsg(ForwardShortId(paymentFSM, scid_ab, cmd2)) + register.expectMsg(ForwardShortId(paymentFSM.toTyped, scid_ab, cmd2)) sender.send(paymentFSM, addCompleted(HtlcResult.RemoteFail(UpdateFailHtlc(ByteVector32.Zeroes, 0, defaultPaymentHash)))) // unparsable message // we allow 2 tries, so we send a 2nd request to the router @@ -352,7 +353,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { awaitCond(paymentFSM.stateName == WAITING_FOR_PAYMENT_COMPLETE) val WaitingForComplete(_, cmd1, Nil, _, _, _) = paymentFSM.stateData - register.expectMsg(ForwardShortId(paymentFSM, scid_ab, cmd1)) + register.expectMsg(ForwardShortId(paymentFSM.toTyped, scid_ab, cmd1)) sender.send(paymentFSM, RES_ADD_FAILED(cmd1, ChannelUnavailable(ByteVector32.Zeroes), None)) // then the payment lifecycle will ask for a new route excluding the channel @@ -395,7 +396,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { awaitCond(paymentFSM.stateName == WAITING_FOR_PAYMENT_COMPLETE) val WaitingForComplete(_, cmd1, Nil, _, _, _) = paymentFSM.stateData - register.expectMsg(ForwardShortId(paymentFSM, scid_ab, cmd1)) + register.expectMsg(ForwardShortId(paymentFSM.toTyped, scid_ab, cmd1)) sender.send(paymentFSM, addCompleted(HtlcResult.RemoteFailMalformed(UpdateFailMalformedHtlc(ByteVector32.Zeroes, 0, randomBytes32(), FailureMessageCodecs.BADONION)))) // then the payment lifecycle will ask for a new route excluding the channel @@ -418,7 +419,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { awaitCond(paymentFSM.stateName == WAITING_FOR_PAYMENT_COMPLETE) val WaitingForComplete(_, cmd1, Nil, _, _, _) = paymentFSM.stateData - register.expectMsg(ForwardShortId(paymentFSM, scid_ab, cmd1)) + register.expectMsg(ForwardShortId(paymentFSM.toTyped, scid_ab, cmd1)) sender.send(paymentFSM, addCompleted(HtlcResult.OnChainFail(HtlcsTimedoutDownstream(randomBytes32(), Set.empty)))) // this error is fatal @@ -441,7 +442,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { awaitCond(paymentFSM.stateName == WAITING_FOR_PAYMENT_COMPLETE) val WaitingForComplete(_, cmd1, Nil, _, _, _) = paymentFSM.stateData - register.expectMsg(ForwardShortId(paymentFSM, scid_ab, cmd1)) + register.expectMsg(ForwardShortId(paymentFSM.toTyped, scid_ab, cmd1)) val update_bc_disabled = update_bc.copy(channelFlags = ChannelUpdate.ChannelFlags(isNode1 = true, isEnabled = false)) sender.send(paymentFSM, addCompleted(HtlcResult.DisconnectedBeforeSigned(update_bc_disabled))) @@ -463,7 +464,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { awaitCond(paymentFSM.stateName == WAITING_FOR_PAYMENT_COMPLETE) val WaitingForComplete(_, cmd1, Nil, sharedSecrets1, _, route) = paymentFSM.stateData - register.expectMsg(ForwardShortId(paymentFSM, scid_ab, cmd1)) + register.expectMsg(ForwardShortId(paymentFSM.toTyped, scid_ab, cmd1)) val failure = TemporaryChannelFailure(update_bc) sender.send(paymentFSM, addCompleted(HtlcResult.RemoteFail(UpdateFailHtlc(ByteVector32.Zeroes, 0, Sphinx.FailurePacket.create(sharedSecrets1.head._1, failure))))) @@ -494,7 +495,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { routerForwarder.forward(routerFixture.router) awaitCond(paymentFSM.stateName == WAITING_FOR_PAYMENT_COMPLETE) val WaitingForComplete(_, cmd1, Nil, sharedSecrets1, _, route1) = paymentFSM.stateData - register.expectMsg(ForwardShortId(paymentFSM, scid_ab, cmd1)) + register.expectMsg(ForwardShortId(paymentFSM.toTyped, scid_ab, cmd1)) // we change the cltv expiry val channelUpdate_bc_modified = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_b, c, scid_bc, CltvExpiryDelta(42), htlcMinimumMsat = update_bc.htlcMinimumMsat, feeBaseMsat = update_bc.feeBaseMsat, feeProportionalMillionths = update_bc.feeProportionalMillionths, htlcMaximumMsat = update_bc.htlcMaximumMsat.get) @@ -511,7 +512,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { // router answers with a new route, taking into account the new update awaitCond(paymentFSM.stateName == WAITING_FOR_PAYMENT_COMPLETE) val WaitingForComplete(_, cmd2, _, sharedSecrets2, _, route2) = paymentFSM.stateData - register.expectMsg(ForwardShortId(paymentFSM, scid_ab, cmd2)) + register.expectMsg(ForwardShortId(paymentFSM.toTyped, scid_ab, cmd2)) // we change the cltv expiry one more time val channelUpdate_bc_modified_2 = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_b, c, scid_bc, CltvExpiryDelta(43), htlcMinimumMsat = update_bc.htlcMinimumMsat, feeBaseMsat = update_bc.feeBaseMsat, feeProportionalMillionths = update_bc.feeProportionalMillionths, htlcMaximumMsat = update_bc.htlcMaximumMsat.get) @@ -545,7 +546,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { routerForwarder.forward(routerFixture.router) awaitCond(paymentFSM.stateName == WAITING_FOR_PAYMENT_COMPLETE) val WaitingForComplete(_, cmd1, Nil, sharedSecrets1, _, _) = paymentFSM.stateData - register.expectMsg(ForwardShortId(paymentFSM, scid_ab, cmd1)) + register.expectMsg(ForwardShortId(paymentFSM.toTyped, scid_ab, cmd1)) // the node replies with a temporary failure containing the same update as the one we already have (likely a balance issue) val failure = TemporaryChannelFailure(update_bc) @@ -579,7 +580,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { routerForwarder.forward(routerFixture.router) awaitCond(paymentFSM.stateName == WAITING_FOR_PAYMENT_COMPLETE) val WaitingForComplete(_, cmd1, Nil, sharedSecrets1, _, _) = paymentFSM.stateData - register.expectMsg(ForwardShortId(paymentFSM, scid_ab, cmd1)) + register.expectMsg(ForwardShortId(paymentFSM.toTyped, scid_ab, cmd1)) // we change the cltv expiry val channelUpdate_bc_modified = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_b, c, scid_bc, CltvExpiryDelta(42), htlcMinimumMsat = update_bc.htlcMinimumMsat, feeBaseMsat = update_bc.feeBaseMsat, feeProportionalMillionths = update_bc.feeProportionalMillionths, htlcMaximumMsat = update_bc.htlcMaximumMsat.get) @@ -600,7 +601,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { // router answers with a new route, taking into account the new update awaitCond(paymentFSM.stateName == WAITING_FOR_PAYMENT_COMPLETE) val WaitingForComplete(_, cmd2, _, _, _, _) = paymentFSM.stateData - register.expectMsg(ForwardShortId(paymentFSM, scid_ab, cmd2)) + register.expectMsg(ForwardShortId(paymentFSM.toTyped, scid_ab, cmd2)) assert(cmd2.cltvExpiry > cmd1.cltvExpiry) } @@ -619,7 +620,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { routerForwarder.forward(routerFixture.router) awaitCond(paymentFSM.stateName == WAITING_FOR_PAYMENT_COMPLETE) val WaitingForComplete(_, cmd1, Nil, sharedSecrets1, _, _) = paymentFSM.stateData - register.expectMsg(ForwardShortId(paymentFSM, scid_ab, cmd1)) + register.expectMsg(ForwardShortId(paymentFSM.toTyped, scid_ab, cmd1)) // we disable the channel val channelUpdate_cd_disabled = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_c, d, scid_cd, CltvExpiryDelta(42), update_cd.htlcMinimumMsat, update_cd.feeBaseMsat, update_cd.feeProportionalMillionths, update_cd.htlcMaximumMsat.get, enable = false) @@ -647,7 +648,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { awaitCond(paymentFSM.stateName == WAITING_FOR_PAYMENT_COMPLETE) val WaitingForComplete(_, cmd1, Nil, sharedSecrets1, _, route1) = paymentFSM.stateData - register.expectMsg(ForwardShortId(paymentFSM, scid_ab, cmd1)) + register.expectMsg(ForwardShortId(paymentFSM.toTyped, scid_ab, cmd1)) sender.send(paymentFSM, addCompleted(HtlcResult.RemoteFail(UpdateFailHtlc(ByteVector32.Zeroes, 0, Sphinx.FailurePacket.create(sharedSecrets1.head._1, failure))))) // payment lifecycle forwards the embedded channelUpdate to the router diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PostRestartHtlcCleanerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PostRestartHtlcCleanerSpec.scala index 9dfcf00f73..2a912e74f1 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PostRestartHtlcCleanerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PostRestartHtlcCleanerSpec.scala @@ -415,8 +415,8 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit val origin_2 = Origin.TrampolineRelayedCold(upstream_2.adds.map(u => (u.channelId, u.id)).toList) sender.send(relayer, RES_ADD_SETTLED(origin_2, htlc_2_2, HtlcResult.OnChainFulfill(preimage2))) register.expectMsgAllOf( - Register.Forward(replyTo = ActorRef.noSender, channelId_ab_1, CMD_FULFILL_HTLC(5, preimage2, commit = true)), - Register.Forward(replyTo = ActorRef.noSender, channelId_ab_2, CMD_FULFILL_HTLC(9, preimage2, commit = true)) + Register.Forward(replyTo = null, channelId_ab_1, CMD_FULFILL_HTLC(5, preimage2, commit = true)), + Register.Forward(replyTo = null, channelId_ab_2, CMD_FULFILL_HTLC(9, preimage2, commit = true)) ) // Payment 3 should not be failed: we are still waiting for on-chain confirmation. @@ -511,7 +511,7 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit register.expectNoMessage(100 millis) sender.send(relayer, buildForwardFulfill(testCase.downstream, testCase.origin, preimage1)) - register.expectMsg(Register.Forward(ActorRef.noSender, testCase.origin.originChannelId, CMD_FULFILL_HTLC(testCase.origin.originHtlcId, preimage1, commit = true))) + register.expectMsg(Register.Forward(null, testCase.origin.originChannelId, CMD_FULFILL_HTLC(testCase.origin.originHtlcId, preimage1, commit = true))) eventListener.expectMsgType[ChannelPaymentRelayed] sender.send(relayer, buildForwardFulfill(testCase.downstream, testCase.origin, preimage1)) @@ -530,7 +530,7 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit sender.send(relayer, buildForwardFail(testCase.downstream_1_1, testCase.origin_1)) val fails = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]] :: register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]] :: Nil assert(fails.toSet == testCase.origin_1.htlcs.map { - case (channelId, htlcId) => Register.Forward(ActorRef.noSender, channelId, CMD_FAIL_HTLC(htlcId, Right(TemporaryNodeFailure), commit = true)) + case (channelId, htlcId) => Register.Forward(null, channelId, CMD_FAIL_HTLC(htlcId, Right(TemporaryNodeFailure), commit = true)) }.toSet) sender.send(relayer, buildForwardFail(testCase.downstream_1_1, testCase.origin_1)) @@ -542,7 +542,7 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit sender.send(relayer, buildForwardFail(testCase.downstream_2_3, testCase.origin_2)) register.expectMsg(testCase.origin_2.htlcs.map { - case (channelId, htlcId) => Register.Forward(ActorRef.noSender, channelId, CMD_FAIL_HTLC(htlcId, Right(TemporaryNodeFailure), commit = true)) + case (channelId, htlcId) => Register.Forward(null, channelId, CMD_FAIL_HTLC(htlcId, Right(TemporaryNodeFailure), commit = true)) }.head) register.expectNoMessage(100 millis) @@ -560,7 +560,7 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit sender.send(relayer, buildForwardFulfill(testCase.downstream_1_1, testCase.origin_1, preimage1)) val fulfills = register.expectMsgType[Register.Forward[CMD_FULFILL_HTLC]] :: register.expectMsgType[Register.Forward[CMD_FULFILL_HTLC]] :: Nil assert(fulfills.toSet == testCase.origin_1.htlcs.map { - case (channelId, htlcId) => Register.Forward(ActorRef.noSender, channelId, CMD_FULFILL_HTLC(htlcId, preimage1, commit = true)) + case (channelId, htlcId) => Register.Forward(null, channelId, CMD_FULFILL_HTLC(htlcId, preimage1, commit = true)) }.toSet) sender.send(relayer, buildForwardFulfill(testCase.downstream_1_1, testCase.origin_1, preimage1)) @@ -569,7 +569,7 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit // This payment has 3 downstream HTLCs, but we should fulfill upstream as soon as we receive the preimage. sender.send(relayer, buildForwardFulfill(testCase.downstream_2_1, testCase.origin_2, preimage2)) register.expectMsg(testCase.origin_2.htlcs.map { - case (channelId, htlcId) => Register.Forward(ActorRef.noSender, channelId, CMD_FULFILL_HTLC(htlcId, preimage2, commit = true)) + case (channelId, htlcId) => Register.Forward(null, channelId, CMD_FULFILL_HTLC(htlcId, preimage2, commit = true)) }.head) sender.send(relayer, buildForwardFulfill(testCase.downstream_2_2, testCase.origin_2, preimage2)) @@ -588,7 +588,7 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit sender.send(relayer, buildForwardFail(testCase.downstream_2_1, testCase.origin_2)) sender.send(relayer, buildForwardFulfill(testCase.downstream_2_2, testCase.origin_2, preimage2)) register.expectMsg(testCase.origin_2.htlcs.map { - case (channelId, htlcId) => Register.Forward(ActorRef.noSender, channelId, CMD_FULFILL_HTLC(htlcId, preimage2, commit = true)) + case (channelId, htlcId) => Register.Forward(null, channelId, CMD_FULFILL_HTLC(htlcId, preimage2, commit = true)) }.head) sender.send(relayer, buildForwardFail(testCase.downstream_2_3, testCase.origin_2)) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/ChannelRelayerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/ChannelRelayerSpec.scala index b123d35c51..912569ce3c 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/ChannelRelayerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/ChannelRelayerSpec.scala @@ -26,7 +26,6 @@ import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, ByteVector64, Crypto, Satoshi, SatoshiLong} import fr.acinq.eclair.Features.ScidAlias import fr.acinq.eclair.TestConstants.emptyOnionPacket -import fr.acinq.eclair.RealShortChannelId import fr.acinq.eclair.blockchain.fee.FeeratePerKw import fr.acinq.eclair.channel._ import fr.acinq.eclair.payment.IncomingPaymentPacket.ChannelRelayPacket @@ -36,6 +35,7 @@ import fr.acinq.eclair.router.Announcements import fr.acinq.eclair.wire.protocol.PaymentOnion.{ChannelRelayPayload, ChannelRelayTlvPayload, RelayLegacyPayload} import fr.acinq.eclair.wire.protocol._ import fr.acinq.eclair.{CltvExpiry, NodeParams, RealShortChannelId, TestConstants, randomBytes32, _} +import org.scalatest.Inside.inside import org.scalatest.Outcome import org.scalatest.funsuite.FixtureAnyFunSuiteLike import scodec.bits.HexStringSyntax @@ -67,13 +67,14 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a def expectFwdAdd(register: TestProbe[Any], channelId: ByteVector32, outAmount: MilliSatoshi, outExpiry: CltvExpiry): Register.Forward[CMD_ADD_HTLC] = { val fwd = register.expectMessageType[Register.Forward[CMD_ADD_HTLC]] - assert(fwd.message.isInstanceOf[CMD_ADD_HTLC]) // the line above doesn't check the type due to type erasure + inside(fwd.message) { case add: CMD_ADD_HTLC => + assert(add.amount == outAmount) + assert(add.cltvExpiry == outExpiry) + inside(add.origin) { case o: Origin.ChannelRelayedHot => + assert(o.amountOut == outAmount) + } + } assert(fwd.channelId == channelId) - assert(fwd.message.amount == outAmount) - assert(fwd.message.cltvExpiry == outExpiry) - assert(fwd.message.origin.isInstanceOf[Origin.ChannelRelayedHot]) - val o = fwd.message.origin.asInstanceOf[Origin.ChannelRelayedHot] - assert(o.amountOut == outAmount) fwd } @@ -179,7 +180,7 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a channelRelayer ! WrappedLocalChannelUpdate(u1) // this is another channel, with less balance (it will be preferred) - val u2 = createLocalUpdate(channelId2, balance = 8000000 msat) + val u2 = createLocalUpdate(channelId2, balance = 80_000_000 msat) channelRelayer ! WrappedLocalChannelUpdate(u2) channelRelayer ! Relay(r) @@ -450,7 +451,7 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a val channelId1 = channelIds(realScid1) val payload = RelayLegacyPayload(realScid1, outgoingAmount, outgoingExpiry) - val r = createValidIncomingPacket(payload, 1100000 msat, CltvExpiry(400100)) + val r = createValidIncomingPacket(payload) val u = createLocalUpdate(channelId1) val u_disabled = createLocalUpdate(channelId1, enabled = false) val downstream_htlc = UpdateAddHtlc(channelId1, 7, outgoingAmount, paymentHash, outgoingExpiry, emptyOnionPacket) @@ -564,7 +565,7 @@ object ChannelRelayerSpec { val paymentPreimage: ByteVector32 = randomBytes32() val paymentHash: ByteVector32 = Crypto.sha256(paymentPreimage) - val outgoingAmount: MilliSatoshi = 1000000 msat + val outgoingAmount: MilliSatoshi = 10_000_000 msat val outgoingExpiry: CltvExpiry = CltvExpiry(400000) val outgoingNodeId: PublicKey = randomKey().publicKey @@ -586,7 +587,7 @@ object ChannelRelayerSpec { localAlias2 -> channelId2, ) - def createValidIncomingPacket(payload: ChannelRelayPayload, amountIn: MilliSatoshi = 1_100_000 msat, expiryIn: CltvExpiry = CltvExpiry(400_100)): IncomingPaymentPacket.ChannelRelayPacket = { + def createValidIncomingPacket(payload: ChannelRelayPayload, amountIn: MilliSatoshi = 11_000_000 msat, expiryIn: CltvExpiry = CltvExpiry(400_100)): IncomingPaymentPacket.ChannelRelayPacket = { val add_ab = UpdateAddHtlc(channelId = randomBytes32(), id = 123456, amountIn, paymentHash, expiryIn, emptyOnionPacket) ChannelRelayPacket(add_ab, payload, emptyOnionPacket) } @@ -597,11 +598,14 @@ object ChannelRelayerSpec { ShortIds(real = RealScidStatus.Final(realScid), localAlias, remoteAlias_opt = None) } - def createLocalUpdate(channelId: ByteVector32, channelUpdateScid_opt: Option[ShortChannelId] = None, balance: MilliSatoshi = 10000000 msat, capacity: Satoshi = 500000 sat, enabled: Boolean = true, htlcMinimum: MilliSatoshi = 0 msat, timestamp: TimestampSecond = 0 unixsec, feeBaseMsat: MilliSatoshi = 1000 msat, feeProportionalMillionths: Long = 100, optionScidAlias: Boolean = false): LocalChannelUpdate = { + def createLocalUpdate(channelId: ByteVector32, channelUpdateScid_opt: Option[ShortChannelId] = None, balance: MilliSatoshi = 100_000_000 msat, capacity: Satoshi = 5_000_000 sat, enabled: Boolean = true, htlcMinimum: MilliSatoshi = 0 msat, timestamp: TimestampSecond = 0 unixsec, feeBaseMsat: MilliSatoshi = 1000 msat, feeProportionalMillionths: Long = 100, optionScidAlias: Boolean = false): LocalChannelUpdate = { val shortIds = createShortIds(channelId) val channelUpdateScid = channelUpdateScid_opt.getOrElse(shortIds.real.toOption.get) val update = ChannelUpdate(ByteVector64(randomBytes(64)), Block.RegtestGenesisBlock.hash, channelUpdateScid, timestamp, ChannelUpdate.ChannelFlags(isNode1 = true, isEnabled = enabled), CltvExpiryDelta(100), htlcMinimum, feeBaseMsat, feeProportionalMillionths, Some(capacity.toMilliSatoshi)) - val channelFeatures = if (optionScidAlias) ChannelFeatures(ScidAlias) else ChannelFeatures() + val features: Set[PermanentChannelFeature] = Set( + if (optionScidAlias) Some(ScidAlias) else None, + ).flatten + val channelFeatures = ChannelFeatures(features) val commitments = PaymentPacketSpec.makeCommitments(channelId, testAvailableBalanceForSend = balance, testCapacity = capacity, channelFeatures = channelFeatures) LocalChannelUpdate(null, channelId, shortIds, outgoingNodeId, None, update, commitments) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/ChannelCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/ChannelCodecsSpec.scala index 0d82508f50..c50a3fb7bb 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/ChannelCodecsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/ChannelCodecsSpec.scala @@ -18,8 +18,6 @@ package fr.acinq.eclair.wire.internal.channel import fr.acinq.bitcoin.scalacompat.Crypto.PrivateKey import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, ByteVector64, Crypto, DeterministicWallet, Satoshi, SatoshiLong, Transaction, TxIn} -import fr.acinq.eclair.RealShortChannelId -import fr.acinq.eclair._ import fr.acinq.eclair.blockchain.fee.FeeratePerKw import fr.acinq.eclair.channel.Helpers.Funding import fr.acinq.eclair.channel._ @@ -32,12 +30,14 @@ import fr.acinq.eclair.transactions.Transactions.{AnchorOutputsCommitmentFormat, import fr.acinq.eclair.transactions._ import fr.acinq.eclair.wire.internal.channel.ChannelCodecs._ import fr.acinq.eclair.wire.protocol.{CommonCodecs, UpdateAddHtlc} +import fr.acinq.eclair.{RealShortChannelId, _} import org.json4s.jackson.Serialization import org.scalatest.funsuite.AnyFunSuite import scodec.DecodeResult import scodec.bits._ import scodec.codecs.byte +import java.io.{File, FileWriter} import java.util.UUID import scala.io.Source @@ -133,28 +133,58 @@ class ChannelCodecsSpec extends AnyFunSuite { } test("backward compatibility older codecs (integrity)") { - // this test makes sure that we actually produce the same objects than previous versions of eclair - val refs = Map( - hex"00000303933884AAF1D6B108397E5EFE5C86BCF2D8CA8D2F700EDA99DB9214FC2712B134000456E4167E3C0EB8C856C79CA31C97C0AA0000000000000222000000012A05F2000000000000028F5C000000000000000102D0001E000BD48A2402E80B723C42EE3E42938866EC6686ABB7ABF64380000000C501A7F2974C5074E9E10DBB3F0D9B8C40932EC63ABC610FAD7EB6B21C6D081A459B000000000000011E80000001EEFFFE5C00000000000147AE00000000000001F403F000F18146779F781067ED04B4957E14F1C5623AB653039B2B1D49910240848E4E682DB20131AD64F76FAF90CD7DE26892F1BDAB82FB9E02EF6538D82FF4204B5348F02AE081A5388E9474769D69C4F60A763AE0CCDB5228A06281DE64408871A927297FDFD8818B6383985ABD4F0AC22E73791CF3A4D63C592FA2648242D34B8334B1539E823381BB1F1404C37D9C2318F5FC6B1BF7ECF5E6835B779E3BE09BADCF6DF1F51DCFBC80000000C0808000000000000EFD80000000007F00000000061A0A4880000001EDE5F3C3801203B9C79160B5123CADE575E370480B57B0C816EB35DD7B27372EDA858622EB1E808000000015FFFFFF800000000011001029DFB814F6502A68D6F83B6049E3D2948A2080084083750626532FDB437169C20023A9108146779F781067ED04B4957E14F1C5623AB653039B2B1D49910240848E4E682DB21081B30694071254D8B3B9537320C014B8CB1052E5514F5EFC19CF2EB806308D5CF1A95700AD0100000000008083B9C79160B5123CADE575E370480B57B0C816EB35DD7B27372EDA858622EB1E80800000001961B4C001618F8180000000001100102E648BA30998A28C02C2DFD9DDCD0E0BA064DA199C55186485AFAB296B94E704426FFE00000000000B000A67D9B9FAADB91650E0146B1F742E5C16006708890200239822011026A6925C659D006FEB42D639F1E42DD13224EE49AA34E71B612CF96DB66A8CD4011032C22F653C54CC5E41098227427650644266D80DED45B7387AE0FFC10E529C4680A418228110807CB47D9C1A14CB832FB361C398EA672C9542F34A90BAD4288FA6AC5FC9E9845C01101CF71CAE9252D389135D8C606225DCF1E0333CCDF1FAE84B74FC5D3D440C25F880A3A9108146779F781067ED04B4957E14F1C5623AB653039B2B1D49910240848E4E682DB21081B30694071254D8B3B9537320C014B8CB1052E5514F5EFC19CF2EB806308D5CF1A9573D7C531000000000000000000F3180000000007F00000001EDE5F3C380000000061A0A48D64CA627B243AD5915A2E5D0BAD026762028DDF3304992B83A26D6C11735FC5F01ED56D769BDE7F6A068AF1A4BCFDF950321F3A4744B01B1DDC7498677F112AE1A80000000000000000000000000000000000000658000000000000819800040D37301C10C9419287E9A3B704EB6D7F45CC145DD77DCE8A63B0A47C8AB67467D800901DCE3C8B05A891E56F2BAF1B82405ABD8640B759AEEBD939B976D42C311758F40400000000AFFFFFFC00000000008800814EFDC0A7B2815346B7C1DB024F1E94A451040042041BA83132997EDA1B8B4E10011D48840A33BCFBC0833F6825A4ABF0A78E2B11D5B2981CD958EA4C881204247273416D90840D9834A03892A6C59DCA9B990600A5C65882972A8A7AF7E0CE7975C031846AE78D4AB8002000EC0003FFFFFFFF86801076D98A575A4CDFD0E3F44D1BB3CD3BBAF3BD04C38FED439ED90D88DF932A9296801A80007FFFFFFFF4008136A9D5896669E8724C5120FB6B36C241EF3CEF68AE0316161F04A9EE3EAFF36000FC0003FFFFFFFF86780106E4B5CC4155733A2427082907338051A5DA1E7CA6432840A5528ECAFFA3FB628801B80007FFFFFFFF10020CA4E125E9126107745D4354D4187ABCDE323117857A1DCEB7CCF60B2AAFA80C6003A0000FFFFFFFFE1C0080981575FD981A73A848CC0243CB467BF451F6811DAF4D71CAD8CE8B1E96DB190C01000003FFFFFFFF867400814C747E0FD8290BE8A3B8B3F73015A261479A71780CD3A0A9270234E4B394409C00D80003FFFFFFFF90020E1B9C9B10A97F15F5E1BB27FC8AC670DF8DADEAE4EDFAFB23BDD0AC705FDF51600340000FFFFFFFFF0020AD2581F3494A17B0BE3F63516D53F028A204FD3156D8B21AA4E57A8738D2062080007FFFFFFFF0CE83B9C79160B5123CADE575E370480B57B0C816EB35DD7B27372EDA858622EB1E0B8C1E00000B8000FA46CC2C7E9AB4A37C64216CD65C944E6D73998419D1A1AD2827AB6BC85B32280230764E374064EC82A3751E789607E23BEAE93FB0EDDD5E7FA803767079662E80EAEF384E2AFCB68049D9DC246119E77BD2ED4112330760CAB6CD3671CFCE006C584B9C95E0B554261E00154D40806EA694F44751B328A9291BAD124EFD5664280936EC92D27B242737E7E3E83B4704BA367B7DA5108F2F6EDFB1C38EE721A369E77EED71B12090BAEAAAC322C1457E31AB0C4DE5D9351943F10FD747742616A1AABD09F680B37D4105A8872695EE9B97FAB8985FAA9D747D45046229BF265CEEB300A40FE23040C5F335E0515496C58EE47418B72331FCC6F47A31A9B33B8E000008692FFAFF04D2AE211E9461FB39D875D74F32E4109D21D5A03D46612000000002E307800002E0002069FCA5D3141D3A78436ECFC366E31024CBB18EAF1843EB5FADAC871B42069166C0726710955E3AD621072FCBDFCB90D79E5B1951A5EE01DB533B72429F84E2562680519DE7DE0419FB412D255F853C71588EAD94C0E6CAC7526440902123939A0B6C806CC1A501C495362CEE54DCC830052E32C414B95453D7BF0673CBAE018C23573C69C694A8F88483050257A7366B838489731E5776B6FA0F02573401176D3E7FAEEF11E95A671420586631255F51A0EC2CF4D4D9F69D587712070FE1FB9316B71868692FFAFF04D2AE211E9461FB39D875D74F32E4109D21D5A03D46612000000002E307800002E0002BA11BBBA0202012000000000000007D0000007D0000000C800000007CFFFF83000" - -> """{"type":"DATA_NORMAL","commitments":{"channelId":"07738f22c16a24795bcaebc6e09016af61902dd66bbaf64e6e5db50b0c45d63c","channelConfig":[],"channelFeatures":[],"localParams":{"nodeId":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","fundingKeyPath":{"path":[1457788542,1007597768,1455922339,479707306]},"dustLimit":546,"maxHtlcValueInFlightMsat":5000000000,"requestedChannelReserve_opt":167772,"htlcMinimum":1,"toSelfDelay":720,"maxAcceptedHtlcs":30,"isInitiator":false,"defaultFinalScriptPubKey":"a9144805d016e47885dc7c852710cdd8cd0d576f57ec87","initFeatures":{"activated":{"option_data_loss_protect":"optional","initial_routing_sync":"optional","gossip_queries":"optional"},"unknown":[]}},"remoteParams":{"nodeId":"034fe52e98a0e9d3c21b767e1b371881265d8c7578c21f5afd6d6438da10348b36","dustLimit":573,"maxHtlcValueInFlightMsat":16609443000,"requestedChannelReserve_opt":167772,"htlcMinimum":1000,"toSelfDelay":2016,"maxAcceptedHtlcs":483,"fundingPubKey":"028cef3ef020cfda09692afc29e38ac4756ca60736563a93220481091c9cd05b64","revocationBasepoint":"02635ac9eedf5f219afbc4d125e37b5705f73c05deca71b05fe84096a691e055c1","paymentBasepoint":"034a711d28e8ed3ad389ec14ec75c199b6a45140c503bcc88110e3524e52ffbfb1","delayedPaymentBasepoint":"0316c70730b57a9e15845ce6f239e749ac78b25f44c90485a697066962a73d0467","htlcBasepoint":"03763e280986fb384631ebf8d637efd9ebcd06b6ef3c77c1375b9edbe3ea3b9f79","initFeatures":{"activated":{"option_data_loss_protect":"mandatory","gossip_queries":"optional"},"unknown":[]}},"channelFlags":{"announceChannel":true},"localCommit":{"index":7675,"spec":{"htlcs":[],"commitTxFeerate":254,"toLocal":204739729,"toRemote":16572475271},"commitTxAndRemoteSig":{"commitTx":{"txid":"e25a866b79212015e01e155e530fb547abc8276869f8740a9948e52ca231f1e4","tx":"020000000107738f22c16a24795bcaebc6e09016af61902dd66bbaf64e6e5db50b0c45d63d010000000032c3698002c31f0300000000002200205cc91746133145180585bfb3bb9a1c1740c9b43338aa30c90b5f5652d729ce0884dffc0000000000160014cfb373f55b722ca1c028d63ee85cb82c00ce11127af8a620"},"remoteSig":"4d4d24b8cb3a00dfd685ac73e3c85ba26449dc935469ce36c259f2db6cd519a865845eca78a998bc8213044e84eca0c884cdb01bda8b6e70f5c1ff821ca5388d"},"htlcTxsAndRemoteSigs":[]},"remoteCommit":{"index":7779,"spec":{"htlcs":[],"commitTxFeerate":254,"toLocal":16572475271,"toRemote":204739729},"txid":"ac994c4f64875ab22b45cba175a04cec4051bbe660932570744dad822e6bf8be","remotePerCommitmentPoint":"03daadaed37bcfed40d15e34979fbf2a0643e748e8960363bb8e930cefe2255c35"},"localChanges":{"proposed":[],"signed":[],"acked":[]},"remoteChanges":{"proposed":[],"acked":[],"signed":[]},"localNextHtlcId":203,"remoteNextHtlcId":4147,"originChannels":{},"remoteNextCommitInfo":"034dcc0704325064a1fa68edc13adb5fd173051775df73a298ec291f22ad9d19f6","commitInput":{"outPoint":"3dd6450c0bb55d6e4ef6ba6bd62d9061af1690e0c6ebca5b79246ac1228f7307:1","amountSatoshis":16777215},"remotePerCommitmentSecrets":null},"shortIds":{"real":{"status":"final","realScid":"1513532x23x1"},"localAlias":"0x17183c0000170001"},"channelAnnouncement":{"nodeSignature1":"d2366163f4d5a51be3210b66b2e4a2736b9ccc20ce8d0d69413d5b5e42d991401183b271ba032764151ba8f3c4b03f11df5749fd876eeaf3fd401bb383cb3174","nodeSignature2":"075779c27157e5b4024ecee12308cf3bde976a0891983b0655b669b38e7e700362c25ce4af05aaa130f000aa6a04037534a7a23a8d99454948dd689277eab321","bitcoinSignature1":"4049b7649693d92139bf3f1f41da3825d1b3dbed2884797b76fd8e1c77390d1b4f3bf76b8d890485d7555619160a2bf18d58626f2ec9a8ca1f887eba3ba130b5","bitcoinSignature2":"0d55e84fb4059bea082d443934af74dcbfd5c4c2fd54eba3ea2823114df932e7759805207f1182062f99af028aa4b62c7723a0c5b9198fe637a3d18d4d99dc70","features":{"activated":{},"unknown":[]},"chainHash":"43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000","shortChannelId":"1513532x23x1","nodeId1":"034fe52e98a0e9d3c21b767e1b371881265d8c7578c21f5afd6d6438da10348b36","nodeId2":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","bitcoinKey1":"028cef3ef020cfda09692afc29e38ac4756ca60736563a93220481091c9cd05b64","bitcoinKey2":"03660d280e24a9b16772a6e6418029719620a5caa29ebdf8339e5d700c611ab9e3","tlvStream":{"records":[],"unknown":[]}},"channelUpdate":{"signature":"4e34a547c424182812bd39b35c1c244b98f2bbb5b7d07812b9a008bb69f3fd77788f4ad338a102c331892afa8d076167a6a6cfb4eac3b890387f0fdc98b5b8c3","chainHash":"43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000","shortChannelId":"1513532x23x1","timestamp":{"iso":"2019-06-18T12:49:33Z","unix":1560862173},"channelFlags":{"isEnabled":true,"isNode1":false},"cltvExpiryDelta":144,"htlcMinimumMsat":1000,"feeBaseMsat":1000,"feeProportionalMillionths":100,"htlcMaximumMsat":16777215000,"tlvStream":{"records":[],"unknown":[]}}}""", - hex"00000303933884AAF1D6B108397E5EFE5C86BCF2D8CA8D2F700EDA99DB9214FC2712B1340004D443ECE9D9C43A11A19B554BAAA6AD150000000000000222000000003B9ACA0000000000000249F000000000000000010090001E800BD48A22F4C80A42CC8BB29A764DBAEFC95674931FBE9A4380000000C50134D4A745996002F219B5FDBA1E045374DF589ECA06ABE23CECAE47343E65EDCF800000000000011E80000001BA90824000000000000124F800000000000001F4038500F1810AE1AF8A1D6F56F80855E26705F191BB07CD4E2434BC5BB1698E7E5880E2266201E8BFEEEEED725775B8116F6F82CF8E87835A5B45B184E56F272AD70D6078118601E06212B8C8F2E25B73EE7974FDCDF007E389B437BBFE238CCC3F3BF7121B6C5E81AA8589D21E9584B24A11F3ABBA5DAD48D121DD63C57A69CD767119C05DA159CB81A649D8CC0E136EB8DFBD2268B69DCA86F8CE4A604235A03D9D37AE7B07FC563F80000000C080800000000000271C000000000177000000002808B14600000001970039BA00123767F0F4F00D5E9FDF24177EF2872343D9F8FAEC65D3048BA575E70E00A0AB08800000000015E070F20000000000110010584241B5FB364208F6E64A80D1166DAD866186B10C015ED0283FF1C308C2105A0023A910810AE1AF8A1D6F56F80855E26705F191BB07CD4E2434BC5BB1698E7E5880E226621081DE8ADFA110DC8A94D8B9E9EF616BAE8598287C8F82AFDF0FC068697D570266FDA95700AD81000000000080B767F0F4F00D5E9FDF24177EF2872343D9F8FAEC65D3048BA575E70E00A0AB0880000000003E7AEDC0011ABE8A00000000001100101A9CE4B6AEF469590BC7BCC51DCEEAE9C86084055A63CC01E443C733FBE400B9B5B16800000000000B000A5E5700106D1A7097E4DE87EBAF1F8F2773842FA482002418228110805E84989A81F51ABD9D11889AE43E68FAD93659DEC019F1B8C0ADBF15A57B118B81101DCC1256F9306439AD3962C043FC47A5179CAAA001CCB23342BE0E8D92E4022780A4182281108074F306DA3751B84EC5FFB155BDCA7B8E02208BBDBC8D4F3327ABA557BF27CD1701102EF4AC8CC92F469DA9642D4D4162BC545F8B34ADE15B7D6F99808AA22B086B0180A3A910810AE1AF8A1D6F56F80855E26705F191BB07CD4E2434BC5BB1698E7E5880E226621081DE8ADFA110DC8A94D8B9E9EF616BAE8598287C8F82AFDF0FC068697D570266FDA9576F8099900000000000000000271C00000000017700000001970039BA000000002808B14648CE00AE97051EE10A3C361263F81A98165CE4AA7BA076933D4266E533585F24815C15DEACF0691332B38ECF23EC39982C5C978C748374A01BA9B30D501EE4F26E8000000000000000000000000000000000001224000000000000004B800040A911C460F1467952E3B99BED072F81BFB4454FF389636DCB399FE6A78113C28580091BB3F87A7806AF4FEF920BBF794391A1ECFC7D7632E98245D2BAF3870050558440000000000AF0387900000000000880082C2120DAFD9B21047B732540688B36D6C330C3588600AF68141FF8E18461082D0011D488408570D7C50EB7AB7C042AF13382F8C8DD83E6A7121A5E2DD8B4C73F2C407113310840EF456FD0886E454A6C5CF4F7B0B5D742CC143E47C157EF87E03434BEAB81337ED4AB8001C00F40003FFFFFFFEC7200403248A1D44DFA3AC9EC237D452C936400CAA86E9517CCCF2A8F77B7493CD70B6A00780001FFFFFFFF63A0041826829646B907A97FBD1455EA8673A12B8E7AA6EA790F7802E955CE3B69DE57E006E0001FFFFFFFF640081E51EB1F91218821E680B50E4B22DF8B094385BD33ACAE36BFC9E8C2F5AD2DA5400EC0003FFFFFFFEC7801047C26AD5435658D063EBCF73A5D0EEFE73ED6B73426246E8DFB3A21D1C4C7465001900007FFFFFFFE0040B115AC58BAAA900195893EA3B2AB408D2AD348AD047E3B6CB15E599625E38608006A0001FFFFFFFF7002033C39A21A38BB61F6FB33623771A9356D8885B7C12C939C770C939EF826286C200360000FFFFFFFFB4008104EF4271064A0973B053727C3E67352D00E25CAEED944F50782449CEAE8F50960001FFFFFFFF6390DD9FC3D3C0357A7F7C905DFBCA1C8D0F67E3EBB1974C122E95D79C380282AC222B21FA0007920001295AA1FB77029F7620A90EF7AE6A6CD31E4588B93264A7ADB76152D535C52E90B9E1B7C2376DABA316A6290F1A9730D4E5E44D0B1CB0EE6A795702E6A6BCDFCDA1A4BFEBFC134AB8847A5187ECE761D75D3CCB904274875680F51984800000000AC87E8001E480002E884D2A8080804800000000000001F4000001F40000003200000001BF08EB000" - -> """{"type":"DATA_NORMAL","commitments":{"channelId":"6ecfe1e9e01abd3fbe482efde50e4687b3f1f5d8cba609174aebce1c01415611","channelConfig":[],"channelFeatures":[],"localParams":{"nodeId":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","fundingKeyPath":{"path":[3561221353,3653515793,2711311691,2863050005]},"dustLimit":546,"maxHtlcValueInFlightMsat":1000000000,"requestedChannelReserve_opt":150000,"htlcMinimum":1,"toSelfDelay":144,"maxAcceptedHtlcs":30,"isInitiator":true,"defaultFinalScriptPubKey":"a91445e990148599176534ec9b75df92ace9263f7d3487","initFeatures":{"activated":{"option_data_loss_protect":"optional","initial_routing_sync":"optional","gossip_queries":"optional"},"unknown":[]}},"remoteParams":{"nodeId":"0269a94e8b32c005e4336bfb743c08a6e9beb13d940d57c479d95c8e687ccbdb9f","dustLimit":573,"maxHtlcValueInFlightMsat":14850000000,"requestedChannelReserve_opt":150000,"htlcMinimum":1000,"toSelfDelay":1802,"maxAcceptedHtlcs":483,"fundingPubKey":"0215c35f143adeadf010abc4ce0be323760f9a9c486978b762d31cfcb101c44cc4","revocationBasepoint":"03d17fdddddae4aeeb7022dedf059f1d0f06b4b68b6309cade4e55ae1ac0f0230c","paymentBasepoint":"03c0c4257191e5c4b6e7dcf2e9fb9be00fc713686f77fc4719987e77ee2436d8bd","delayedPaymentBasepoint":"03550b13a43d2b09649423e75774bb5a91a243bac78af4d39aece23380bb42b397","htlcBasepoint":"034c93b1981c26dd71bf7a44d16d3b950df19c94c0846b407b3a6f5cf60ff8ac7f","initFeatures":{"activated":{"option_data_loss_protect":"mandatory","gossip_queries":"optional"},"unknown":[]}},"channelFlags":{"announceChannel":true},"localCommit":{"index":20024,"spec":{"htlcs":[],"commitTxFeerate":750,"toLocal":1343316620,"toRemote":13656683380},"commitTxAndRemoteSig":{"commitTx":{"txid":"65fe0b1f079fa763448df3ab8d94b1ad7d377c061121376be90b9c0c1bb0cd43","tx":"02000000016ecfe1e9e01abd3fbe482efde50e4687b3f1f5d8cba609174aebce1c0141561100000000007cf5db8002357d1400000000002200203539c96d5de8d2b2178f798a3b9dd5d390c1080ab4c79803c8878e67f7c801736b62d00000000000160014bcae0020da34e12fc9bd0fd75e3f1e4ee7085f49df013320"},"remoteSig":"bd09313503ea357b3a231135c87cd1f5b26cb3bd8033e371815b7e2b4af623173b9824adf260c8735a72c58087f88f4a2f39554003996466857c1d1b25c8044f"},"htlcTxsAndRemoteSigs":[]},"remoteCommit":{"index":20024,"spec":{"htlcs":[],"commitTxFeerate":750,"toLocal":13656683380,"toRemote":1343316620},"txid":"919c015d2e0a3dc214786c24c7f035302cb9c954f740ed267a84cdca66b0be49","remotePerCommitmentPoint":"02b82bbd59e0d22665671d9e47d8733058b92f18e906e9403753661aa03dc9e4dd"},"localChanges":{"proposed":[],"signed":[],"acked":[]},"remoteChanges":{"proposed":[],"acked":[],"signed":[]},"localNextHtlcId":9288,"remoteNextHtlcId":151,"originChannels":{},"remoteNextCommitInfo":"02a4471183c519e54b8ee66fb41cbe06fed1153fce258db72ce67f9a9e044f0a16","commitInput":{"outPoint":"115641011cceeb4a1709a6cbd8f5f1b387460ee5fd2e48be3fbd1ae0e9e1cf6e:0","amountSatoshis":15000000},"remotePerCommitmentSecrets":null},"shortIds":{"real":{"status":"final","realScid":"1413373x969x0"},"localAlias":"0x1590fd0003c90000"},"channelUpdate":{"signature":"52b543f6ee053eec41521def5cd4d9a63c8b117264c94f5b6ec2a5aa6b8a5d2173c36f846edb57462d4c521e352e61a9cbc89a163961dcd4f2ae05cd4d79bf9b","chainHash":"43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000","shortChannelId":"1413373x969x0","timestamp":{"iso":"2019-06-24T09:39:33Z","unix":1561369173},"channelFlags":{"isEnabled":true,"isNode1":false},"cltvExpiryDelta":144,"htlcMinimumMsat":1000,"feeBaseMsat":1000,"feeProportionalMillionths":100,"htlcMaximumMsat":15000000000,"tlvStream":{"records":[],"unknown":[]}}}""", - hex"0200020000000303933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b13400098c4b989bbdced820a77a7186c2320e7d176a5c8b5c16d6ac2af3889d6bc8bf8080000001000000000000022200000004a817c80000000000000249f0000000000000000102d0001eff1600148061b7fbd2d84ed1884177ea785faecb2080b10302e56c8eca8d4f00df84ac34c23f49c006d57d316b7ada5c346e9d4211e11604b300000004080aa982027455aef8453d92f4706b560b61527cc217ddf14da41770e8ed6607190a1851b8000000000000023d000000037521048000000000000249f00000000000000001070a01e302eff5309b9368340edc6114d738b3590e6969bec4e95d8a080cf185e8b9ce5e4b0343bf4bfbaea5c100f1f2bf1cdf82a0ef97c9a0069a2aec631e7c3084ba929b7503c54e7d5ccfc13f1a6c7a441ffcfac86248574d1bc0fe9773836f4c724ea7b2bd03765aaac2e8fa6dbce7de5143072e9d9d5e96a1fd451d02fe4ff803f413f303f8022f3b055b0d35cde31dec5263a8ed638433e3424a4e197c06d94053985a364a5700000004808a52a1010000000000000004000000001046000000037e11d6000000000000000000245986f644357956c1674f385e0878fb7bb44ab3d57ea9911fab98af8a71e1ad1b000000002bc0e1e40000000000220020690fb50de412adf9b20a7fc6c8fb86f1bfd4ebc1ef8e2d96a5a196560798d944475221023ced05ed1ab328b67477376d68a69ecd0f371a9d5843c6c3be4d31498d516d8d2102eff5309b9368340edc6114d738b3590e6969bec4e95d8a080cf185e8b9ce5e4b52aefd013b020000000001015986f644357956c1674f385e0878fb7bb44ab3d57ea9911fab98af8a71e1ad1b0000000000c2d6178001f8d5e4000000000022002080f1dfe71a865b605593e169677c952aaa1196fc2f541ef7d21c3b1006527b61040047304402207f8c1936d0a50671c993890f887c78c6019abc2a2e8018899dcdc0e891fd2b090220046b56afa2cb7e9470073c238654ecf584bcf5c00b96b91e38335a70e2739ec901483045022100871afd240e20a171b9cba46f20555f848c5850f94ec7da7b33b9eeaf6af6653c0220119cda8cbf5f80986d6a4f0db2590c734d1de399a7060a477b5d94df0183625b01475221023ced05ed1ab328b67477376d68a69ecd0f371a9d5843c6c3be4d31498d516d8d2102eff5309b9368340edc6114d738b3590e6969bec4e95d8a080cf185e8b9ce5e4b52aed7782c20000000000000000000040000000010460000000000000000000000037e11d600b5f2287b2d5edf4df5602a3c287db3b938c3f1a943e40715886db5bd400f95d802e7e1abac1feb54ee3ac2172c9e2231f77765df57664fb44a6dc2e4aa9e6a9a6a000000000000000000000000000000000000000000000000000000000000ff03fd10fe44564e2d7e1550099785c2c1bad32a5ae0feeef6e27f0c108d18b4931d245986f644357956c1674f385e0878fb7bb44ab3d57ea9911fab98af8a71e1ad1b000000002bc0e1e40000000000220020690fb50de412adf9b20a7fc6c8fb86f1bfd4ebc1ef8e2d96a5a196560798d944475221023ced05ed1ab328b67477376d68a69ecd0f371a9d5843c6c3be4d31498d516d8d2102eff5309b9368340edc6114d738b3590e6969bec4e95d8a080cf185e8b9ce5e4b52ae0001003e0000fffffffffffc0080474b8cf7bb98217dd8dc475cb7c057a3465d466728978bbb909d0a05d4ae7bbe0001fffffffffff85986f644357956c1674f385e0878fb7bb44ab3d57ea9911fab98af8a71e1ad1b1eedce0000010000fffffd01ae98d7a81bc1aa92fcfb74ced2213e85e0d92ae8ac622bf294b3551c7c27f6f84f782f3b318e4d0eb2c67ac719a7c65afcf85bf159f6ceea9427be54920134196992f6ed0e059db72105a13ec0e799bb08896cad8b4feb7e9ec7283c309b5f43123af1bd9e913fc2db018edadde8932d6992408f10c1ad020504361972dfa7fef09bbc2b568cef3c8c006f7860106fd5984bcc271ff06c4829db2a665e59b7c0b22c311a340ff2ab9bcb74a50db10ed85503ad2d248d95af8151aca8ef96248e8f84b3075922385fbaf012f057e7ee84ecbc14c84880520b26d6fd22ab5f107db606a906efdcf0f88ffbe32dc6ecc10131e1ff0dc8d68dad89c98562557f00448b000043497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea3309000000001eedce0000010000027455aef8453d92f4706b560b61527cc217ddf14da41770e8ed6607190a1851b803933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b13402eff5309b9368340edc6114d738b3590e6969bec4e95d8a080cf185e8b9ce5e4b023ced05ed1ab328b67477376d68a69ecd0f371a9d5843c6c3be4d31498d516d8d88710d73875607575f3d84bb507dd87cca5b85f0cdac84f4ccecce7af3a55897525a45070fe26c0ea43e9580d4ea4cfa62ee3273e5546911145cba6bbf56e59d8e43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea3309000000001eedce000001000060e6eb14010100900000000000000001000003e800000064000000037e11d6000000" - -> """{"type":"DATA_NORMAL","commitments":{"channelId":"5986f644357956c1674f385e0878fb7bb44ab3d57ea9911fab98af8a71e1ad1b","channelConfig":["funding_pubkey_based_channel_keypath"],"channelFeatures":["option_static_remotekey"],"localParams":{"nodeId":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","fundingKeyPath":{"path":[2353764507,3184449568,2809819526,3258060413,392846475,1545000620,720603293,1808318336,2147483649]},"dustLimit":546,"maxHtlcValueInFlightMsat":20000000000,"requestedChannelReserve_opt":150000,"htlcMinimum":1,"toSelfDelay":720,"maxAcceptedHtlcs":30,"isInitiator":true,"defaultFinalScriptPubKey":"00148061b7fbd2d84ed1884177ea785faecb2080b103","walletStaticPaymentBasepoint":"02e56c8eca8d4f00df84ac34c23f49c006d57d316b7ada5c346e9d4211e11604b3","initFeatures":{"activated":{"option_support_large_channel":"optional","gossip_queries_ex":"optional","option_data_loss_protect":"optional","var_onion_optin":"mandatory","option_static_remotekey":"optional","payment_secret":"optional","option_shutdown_anysegwit":"optional","basic_mpp":"optional","gossip_queries":"optional"},"unknown":[]}},"remoteParams":{"nodeId":"027455aef8453d92f4706b560b61527cc217ddf14da41770e8ed6607190a1851b8","dustLimit":573,"maxHtlcValueInFlightMsat":14850000000,"requestedChannelReserve_opt":150000,"htlcMinimum":1,"toSelfDelay":1802,"maxAcceptedHtlcs":483,"fundingPubKey":"02eff5309b9368340edc6114d738b3590e6969bec4e95d8a080cf185e8b9ce5e4b","revocationBasepoint":"0343bf4bfbaea5c100f1f2bf1cdf82a0ef97c9a0069a2aec631e7c3084ba929b75","paymentBasepoint":"03c54e7d5ccfc13f1a6c7a441ffcfac86248574d1bc0fe9773836f4c724ea7b2bd","delayedPaymentBasepoint":"03765aaac2e8fa6dbce7de5143072e9d9d5e96a1fd451d02fe4ff803f413f303f8","htlcBasepoint":"022f3b055b0d35cde31dec5263a8ed638433e3424a4e197c06d94053985a364a57","initFeatures":{"activated":{"option_upfront_shutdown_script":"optional","payment_secret":"mandatory","option_data_loss_protect":"mandatory","var_onion_optin":"optional","option_static_remotekey":"mandatory","option_support_large_channel":"optional","option_anchors_zero_fee_htlc_tx":"optional","basic_mpp":"optional","gossip_queries":"optional"},"unknown":[31]}},"channelFlags":{"announceChannel":true},"localCommit":{"index":4,"spec":{"htlcs":[],"commitTxFeerate":4166,"toLocal":15000000000,"toRemote":0},"commitTxAndRemoteSig":{"commitTx":{"txid":"fa747ecb6f718c6831cc7148cf8d65c3468d2bb6c202605e2b82d2277491222f","tx":"02000000015986f644357956c1674f385e0878fb7bb44ab3d57ea9911fab98af8a71e1ad1b0000000000c2d6178001f8d5e4000000000022002080f1dfe71a865b605593e169677c952aaa1196fc2f541ef7d21c3b1006527b61d7782c20"},"remoteSig":"871afd240e20a171b9cba46f20555f848c5850f94ec7da7b33b9eeaf6af6653c119cda8cbf5f80986d6a4f0db2590c734d1de399a7060a477b5d94df0183625b"},"htlcTxsAndRemoteSigs":[]},"remoteCommit":{"index":4,"spec":{"htlcs":[],"commitTxFeerate":4166,"toLocal":0,"toRemote":15000000000},"txid":"b5f2287b2d5edf4df5602a3c287db3b938c3f1a943e40715886db5bd400f95d8","remotePerCommitmentPoint":"02e7e1abac1feb54ee3ac2172c9e2231f77765df57664fb44a6dc2e4aa9e6a9a6a"},"localChanges":{"proposed":[],"signed":[],"acked":[]},"remoteChanges":{"proposed":[],"acked":[],"signed":[]},"localNextHtlcId":0,"remoteNextHtlcId":0,"originChannels":{},"remoteNextCommitInfo":"03fd10fe44564e2d7e1550099785c2c1bad32a5ae0feeef6e27f0c108d18b4931d","commitInput":{"outPoint":"1bade1718aaf98ab1f91a97ed5b34ab47bfb78085e384f67c156793544f68659:0","amountSatoshis":15000000},"remotePerCommitmentSecrets":null},"shortIds":{"real":{"status":"final","realScid":"2026958x1x0"},"localAlias":"0x1eedce0000010000"},"channelAnnouncement":{"nodeSignature1":"98d7a81bc1aa92fcfb74ced2213e85e0d92ae8ac622bf294b3551c7c27f6f84f782f3b318e4d0eb2c67ac719a7c65afcf85bf159f6ceea9427be549201341969","nodeSignature2":"92f6ed0e059db72105a13ec0e799bb08896cad8b4feb7e9ec7283c309b5f43123af1bd9e913fc2db018edadde8932d6992408f10c1ad020504361972dfa7fef0","bitcoinSignature1":"9bbc2b568cef3c8c006f7860106fd5984bcc271ff06c4829db2a665e59b7c0b22c311a340ff2ab9bcb74a50db10ed85503ad2d248d95af8151aca8ef96248e8f","bitcoinSignature2":"84b3075922385fbaf012f057e7ee84ecbc14c84880520b26d6fd22ab5f107db606a906efdcf0f88ffbe32dc6ecc10131e1ff0dc8d68dad89c98562557f00448b","features":{"activated":{},"unknown":[]},"chainHash":"43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000","shortChannelId":"2026958x1x0","nodeId1":"027455aef8453d92f4706b560b61527cc217ddf14da41770e8ed6607190a1851b8","nodeId2":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","bitcoinKey1":"02eff5309b9368340edc6114d738b3590e6969bec4e95d8a080cf185e8b9ce5e4b","bitcoinKey2":"023ced05ed1ab328b67477376d68a69ecd0f371a9d5843c6c3be4d31498d516d8d","tlvStream":{"records":[],"unknown":[]}},"channelUpdate":{"signature":"710d73875607575f3d84bb507dd87cca5b85f0cdac84f4ccecce7af3a55897525a45070fe26c0ea43e9580d4ea4cfa62ee3273e5546911145cba6bbf56e59d8e","chainHash":"43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000","shortChannelId":"2026958x1x0","timestamp":{"iso":"2021-07-08T12:09:56Z","unix":1625746196},"channelFlags":{"isEnabled":true,"isNode1":false},"cltvExpiryDelta":144,"htlcMinimumMsat":1,"feeBaseMsat":1000,"feeProportionalMillionths":100,"htlcMaximumMsat":15000000000,"tlvStream":{"records":[],"unknown":[]}}}""" - ) + // It's not enough to just verify that codecs migrate without errors, we also need to make sure that the decoded + // data is correct. To do that, we compare json-serialized representations of the data. + + // NB: If this test fails (it will fail if there was a change in the model), follow this method which will save you + // a lot of time: + // 1) set 'debug = true' + // 2) run the test + // 3) under test/resources/nonreg/codecs/// you will find a file 'data.tmp.json' + // 4) use intellij to compare 'data.json' and 'data.tmp.json' and fix data.json + // 5) repeat until all test cases are fixed + // 6) make sure all 'data.tmp.json' have been cleaned up (they will be if the test passes) + // 7) set 'debug = false' + + val debug = false + + case class TestCase(dir: File, bin: ByteVector, json: String) + + val testCases = for { + // there are two level of directories for organization purposes + parentDir <- new File(getClass.getResource(s"/nonreg/codecs").getFile).listFiles() + dir <- parentDir.listFiles() + } yield { + val srcBin = Source.fromFile(new File(dir, "data.bin")) + val srcJson = Source.fromFile(new File(dir, "data.json")) + try { + TestCase(dir, ByteVector.fromValidHex(srcBin.mkString), srcJson.mkString) + } finally { + srcBin.close() + srcJson.close() + } + } - refs.foreach { case (oldbin, refjson) => - // we decode with compat codec - val oldnormal = channelDataCodec.decode(oldbin.bits).require.value - // we then encode with new codec - val newbin = channelDataCodec.encode(oldnormal).require.bytes - // and we decode with the new codec - val newnormal = channelDataCodec.decode(newbin.bits).require.value - // finally we check that the actual data is the same as before (we just remove the new json field) - val oldjson = Serialization.write(oldnormal)(JsonSerializers.formats) - val newjson = Serialization.write(newnormal)(JsonSerializers.formats) - assert(oldjson == refjson) - assert(newjson == refjson) + testCases.foreach { testCase => + withClue(testCase.dir.getParentFile.getName + "/" + testCase.dir.getName) { + // we decode with compat codec + val olddecoded = channelDataCodec.decode(testCase.bin.bits).require.value + // we check that the decoded data is the same as before + val oldjson = Serialization.writePretty(olddecoded)(JsonSerializers.formats) + val tmpjsonfile_opt = if (debug) { + val tmpjsonfile = new File(testCase.dir.getPath.replace("target\\test-classes", "src\\test\\resources"), "data.tmp.json") + Serialization.writePretty(olddecoded, new FileWriter(tmpjsonfile))(JsonSerializers.formats) + Some(tmpjsonfile) + } else None + assert(oldjson == testCase.json) + tmpjsonfile_opt.foreach(_.delete()) + // we then encode with new codec + val newencoded = channelDataCodec.encode(olddecoded).require.bytes + // and we decode with the new codec + val newdecoded = channelDataCodec.decode(newencoded.bits).require.value + // and we make sure that we obtain the same data + assert(olddecoded == newdecoded) + } } } From 6011c867cf54ba4d29297d4a51efd577004d085d Mon Sep 17 00:00:00 2001 From: Bastien Teinturier <31281497+t-bast@users.noreply.github.com> Date: Fri, 1 Jul 2022 14:33:14 +0200 Subject: [PATCH 107/121] Additive per-node feature override (#2328) The `override-init-features` field in `eclair.conf` was not previously applied on top of the `features` field, so node operators were usually copy-pasting their `features` overrides in every `override-init-features`. The overrides are now applied on top of the base `features` configuration, which makes it easier and more intuitive to configure per-node features. --- docs/Configure.md | 38 +++++++- docs/release-notes/eclair-vnext.md | 10 +- eclair-core/src/main/resources/reference.conf | 4 +- .../main/scala/fr/acinq/eclair/Features.scala | 27 +++--- .../scala/fr/acinq/eclair/NodeParams.scala | 2 +- .../scala/fr/acinq/eclair/StartupSpec.scala | 91 ++++++++++++++----- 6 files changed, 123 insertions(+), 49 deletions(-) diff --git a/docs/Configure.md b/docs/Configure.md index a92000ce53..754abfd404 100644 --- a/docs/Configure.md +++ b/docs/Configure.md @@ -102,24 +102,52 @@ When experimenting with non-default features, we recommend using this to scope t This is done with the `override-init-features` configuration parameter in your `eclair.conf`: ```conf +eclair.features { + var_onion_optin = mandatory + payment_secret = mandatory + option_support_large_channel = optional + option_static_remotekey = optional + option_channel_type = optional +} eclair.override-init-features = [ { - nodeId = "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f" + nodeId = "" features { option_support_large_channel = disabled - option_static_remotekey = optional + option_static_remotekey = mandatory + option_anchors_zero_fee_htlc_tx = optional } }, { - nodeId = "" + nodeId = "" features { - option_static_remotekey = optional - option_support_large_channel = optional + option_support_large_channel = mandatory + option_static_remotekey = disabled + option_channel_type = mandatory } }, ] ``` +In this example, the features that will be activated with Alice are: + +```conf +var_onion_optin = mandatory +payment_secret = mandatory +option_static_remotekey = mandatory +option_anchors_zero_fee_htlc_tx = optional +option_channel_type = optional +``` + +The features that will be activated with Bob are: + +```conf +var_onion_optin = mandatory +payment_secret = mandatory +option_support_large_channel = mandatory +option_channel_type = mandatory +``` + ## Customize feerate tolerance In order to secure your channels' funds against attacks, your eclair node keeps an up-to-date estimate of on-chain feerates (based on your Bitcoin node's estimations). diff --git a/docs/release-notes/eclair-vnext.md b/docs/release-notes/eclair-vnext.md index 33cdb33d0b..57a8f2a607 100644 --- a/docs/release-notes/eclair-vnext.md +++ b/docs/release-notes/eclair-vnext.md @@ -34,10 +34,6 @@ override-init-features = [ { nodeid = "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f", features = { - // these features need to be enabled - var_onion_optin = mandatory - payment_secret = mandatory - option_channel_type = optional // dependencies of zeroconf option_static_remotekey = optional option_anchors_zero_fee_htlc_tx = optional @@ -77,6 +73,12 @@ $ ./eclair-cli open --nodeId=03864e... --fundingSatoshis=100000 --channelType=an $ ./eclair-cli open --nodeId=03864e... --fundingSatoshis=100000 --channelType=anchor_outputs_zero_fee_htlc_tx+scid_alias+zeroconf --announceChannel=false ``` +### Changes to features override + +Eclair supports overriding features on a per-peer basis, using the `eclair.override-init-features` field in `eclair.conf`. +These overrides will now be applied on top of the default features, whereas the previous behavior was to completely ignore default features. +We provide detailed examples in the [documentation](../Configure.md#customize-features). + ### Remove support for Tor v2 Dropped support for version 2 of Tor protocol. That means: diff --git a/eclair-core/src/main/resources/reference.conf b/eclair-core/src/main/resources/reference.conf index 1350bffb9c..d850ffbc4f 100644 --- a/eclair-core/src/main/resources/reference.conf +++ b/eclair-core/src/main/resources/reference.conf @@ -69,9 +69,11 @@ eclair { keysend = disabled trampoline_payment_prototype = disabled } + // The following section lets you customize features for specific nodes. + // The overrides will be applied on top of the default features settings. override-init-features = [ // optional per-node features # { - # nodeid = "02aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + # nodeid = "02aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" # features { } # } ] diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala index f6816f4cbe..e6e260b0b5 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala @@ -145,19 +145,20 @@ object Features { ) } - def fromConfiguration[T <: Feature](config: Config, validFeatures: Set[T]): Features[T] = Features[T]( - config.root().entrySet().asScala.flatMap { entry => - val featureName = entry.getKey - val feature: T = validFeatures.find(_.rfcName == featureName).getOrElse(throw new IllegalArgumentException(s"Invalid feature name ($featureName)")) - config.getString(featureName) match { - case support if support == Mandatory.toString => Some(feature -> Mandatory) - case support if support == Optional.toString => Some(feature -> Optional) - case support if support == "disabled" => None - case wrongSupport => throw new IllegalArgumentException(s"Wrong support specified ($wrongSupport)") - } - }.toMap) - - def fromConfiguration(config: Config): Features[Feature] = fromConfiguration[Feature](config, knownFeatures) + def fromConfiguration[T <: Feature](config: Config, validFeatures: Set[T], baseFeatures: Features[T]): Features[T] = Features[T]( + config.root().entrySet().asScala.foldLeft(baseFeatures.activated) { + case (current, entry) => + val featureName = entry.getKey + val feature: T = validFeatures.find(_.rfcName == featureName).getOrElse(throw new IllegalArgumentException(s"Invalid feature name ($featureName)")) + config.getString(featureName) match { + case support if support == Mandatory.toString => current + (feature -> Mandatory) + case support if support == Optional.toString => current + (feature -> Optional) + case support if support == "disabled" => current - feature + case wrongSupport => throw new IllegalArgumentException(s"Wrong support specified ($wrongSupport)") + } + }) + + def fromConfiguration(config: Config): Features[Feature] = fromConfiguration[Feature](config, knownFeatures, Features.empty) case object DataLossProtect extends Feature with InitFeature with NodeFeature { val rfcName = "option_data_loss_protect" diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala index 62fb21650c..4eea0c7141 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala @@ -314,7 +314,7 @@ object NodeParams extends Logging { val overrideInitFeatures: Map[PublicKey, Features[InitFeature]] = config.getConfigList("override-init-features").asScala.map { e => val p = PublicKey(ByteVector.fromValidHex(e.getString("nodeid"))) - val f = Features.fromConfiguration[InitFeature](e.getConfig("features"), Features.knownFeatures.collect { case f: InitFeature => f }) + val f = Features.fromConfiguration[InitFeature](e.getConfig("features"), Features.knownFeatures.collect { case f: InitFeature => f }, features.initFeatures()) validateFeatures(f.unscoped()) p -> (f.copy(unknown = f.unknown ++ pluginMessageParams.map(_.pluginFeature)): Features[InitFeature]) }.toMap diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/StartupSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/StartupSpec.scala index e4efaa0cec..4092099c6e 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/StartupSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/StartupSpec.scala @@ -166,23 +166,64 @@ class StartupSpec extends AnyFunSuite { test("parse human readable override features") { val perNodeConf = ConfigFactory.parseString( """ + | features { + | var_onion_optin = mandatory + | payment_secret = mandatory + | option_channel_type = optional + | } | override-init-features = [ // optional per-node features | { - | nodeid = "031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f", - | features { - | var_onion_optin = mandatory - | payment_secret = mandatory - | basic_mpp = mandatory - | option_channel_type = optional - | } + | nodeid = "031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f" + | features { + | basic_mpp = mandatory + | gossip_queries = optional + | } | } | ] """.stripMargin ) - val nodeParams = makeNodeParamsWithDefaults(perNodeConf.withFallback(defaultConf)) + val nodeParams = makeNodeParamsWithDefaults(perNodeConf.withFallback(defaultConf.withoutPath("features"))) val perNodeFeatures = nodeParams.initFeaturesFor(PublicKey(ByteVector.fromValidHex("031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f"))) - assert(perNodeFeatures == Features(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory, BasicMultiPartPayment -> Mandatory, ChannelType -> Optional)) + assert(perNodeFeatures == Features(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory, BasicMultiPartPayment -> Mandatory, ChannelRangeQueries -> Optional, ChannelType -> Optional)) + } + + test("combine node override features with default features") { + val perNodeConf = ConfigFactory.parseString( + """ + | features { + | var_onion_optin = mandatory + | payment_secret = mandatory + | basic_mpp = mandatory + | option_static_remotekey = optional + | option_channel_type = optional + | } + | override-init-features = [ // optional per-node features + | { + | nodeid = "031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f" + | features { + | option_static_remotekey = mandatory + | option_anchors_zero_fee_htlc_tx = optional + | } + | }, + | { + | nodeid = "024d4b6cd1361032ca9bd2aeb9d900aa4d45d9ead80ac9423374c451a7254d0766" + | features { + | basic_mpp = optional + | option_static_remotekey = disabled + | } + | } + | ] + """.stripMargin + ) + + val nodeParams = makeNodeParamsWithDefaults(perNodeConf.withFallback(defaultConf.withoutPath("features"))) + val defaultFeatures = nodeParams.features + assert(defaultFeatures == Features(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory, BasicMultiPartPayment -> Mandatory, StaticRemoteKey -> Optional, ChannelType -> Optional)) + val perNodeFeatures1 = nodeParams.initFeaturesFor(PublicKey(ByteVector.fromValidHex("031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f"))) + assert(perNodeFeatures1 == Features(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory, BasicMultiPartPayment -> Mandatory, StaticRemoteKey -> Mandatory, AnchorOutputsZeroFeeHtlcTx -> Optional, ChannelType -> Optional)) + val perNodeFeatures2 = nodeParams.initFeaturesFor(PublicKey(ByteVector.fromValidHex("024d4b6cd1361032ca9bd2aeb9d900aa4d45d9ead80ac9423374c451a7254d0766"))) + assert(perNodeFeatures2 == Features(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory, BasicMultiPartPayment -> Optional, ChannelType -> Optional)) } test("reject non-init features in node override") { @@ -190,22 +231,22 @@ class StartupSpec extends AnyFunSuite { """ | override-init-features = [ // optional per-node features | { - | nodeid = "031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f", - | features { - | var_onion_optin = mandatory - | payment_secret = mandatory - | option_channel_type = optional - | option_payment_metadata = disabled - | } + | nodeid = "031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f" + | features { + | var_onion_optin = mandatory + | payment_secret = mandatory + | option_channel_type = optional + | option_payment_metadata = disabled + | } | }, | { - | nodeid = "024d4b6cd1361032ca9bd2aeb9d900aa4d45d9ead80ac9423374c451a7254d0766", - | features { - | var_onion_optin = mandatory - | payment_secret = mandatory - | option_channel_type = optional - | option_payment_metadata = mandatory - | } + | nodeid = "024d4b6cd1361032ca9bd2aeb9d900aa4d45d9ead80ac9423374c451a7254d0766" + | features { + | var_onion_optin = mandatory + | payment_secret = mandatory + | option_channel_type = optional + | option_payment_metadata = mandatory + | } | } | ] """.stripMargin @@ -219,7 +260,7 @@ class StartupSpec extends AnyFunSuite { """ | on-chain-fees.override-feerate-tolerance = [ | { - | nodeid = "031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f", + | nodeid = "031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f" | feerate-tolerance { | ratio-low = 0.1 | ratio-high = 15.0 @@ -231,7 +272,7 @@ class StartupSpec extends AnyFunSuite { | } | }, | { - | nodeid = "03462779ad4aad39514614751a71085f2f10e1c7a593e4e030efb5b8721ce55b0b", + | nodeid = "03462779ad4aad39514614751a71085f2f10e1c7a593e4e030efb5b8721ce55b0b" | feerate-tolerance { | ratio-low = 0.75 | ratio-high = 5.0 From a1f7c1e74f993c1c3f1cbb7df479e873e80576e0 Mon Sep 17 00:00:00 2001 From: Bastien Teinturier <31281497+t-bast@users.noreply.github.com> Date: Fri, 1 Jul 2022 15:13:26 +0200 Subject: [PATCH 108/121] Return local channel alias in payment failures (#2323) We now use either our local alias or the real scid in the channel update that we store internally. When we send that channel update directly to our peer, we override it to use the remote alias when it makes sense. --- docs/release-notes/eclair-vnext.md | 13 +- .../fr/acinq/eclair/channel/Helpers.scala | 41 +++--- .../fr/acinq/eclair/channel/fsm/Channel.scala | 6 +- .../channel/fsm/ChannelOpenSingleFunder.scala | 2 +- .../acinq/eclair/payment/PaymentEvents.scala | 2 + .../eclair/payment/relay/ChannelRelay.scala | 5 +- .../eclair/payment/relay/ChannelRelayer.scala | 2 +- .../acinq/eclair/payment/relay/Relayer.scala | 11 +- .../acinq/eclair/router/Announcements.scala | 12 +- .../ChannelStateTestsHelperMethods.scala | 2 +- .../c/WaitForChannelReadyStateSpec.scala | 110 +++++++++----- .../channel/states/e/NormalStateSpec.scala | 55 ++++--- .../basic/ThreeNodesIntegrationSpec.scala | 2 +- .../basic/TwoNodesIntegrationSpec.scala | 2 +- .../basic/ZeroConfAliasIntegrationSpec.scala | 135 +++++++++++------- .../basic/fixtures/MinimalNodeFixture.scala | 22 ++- .../payment/relay/ChannelRelayerSpec.scala | 35 ++--- .../router/ChannelRouterIntegrationSpec.scala | 13 +- .../src/test/resources/api/channelbalances | 2 +- .../src/test/resources/api/usablebalances | 2 +- .../fr/acinq/eclair/api/ApiServiceSpec.scala | 8 +- 21 files changed, 286 insertions(+), 196 deletions(-) diff --git a/docs/release-notes/eclair-vnext.md b/docs/release-notes/eclair-vnext.md index 57a8f2a607..cf6427c795 100644 --- a/docs/release-notes/eclair-vnext.md +++ b/docs/release-notes/eclair-vnext.md @@ -91,8 +91,9 @@ upgrading to this release. ### API changes -- `channelbalances` Retrieves information about the balances of all local channels. (#2196) -- `stop` Stops eclair. Please note that the recommended way of stopping eclair is simply to kill its process (#2233) +- `channelbalances`: retrieves information about the balances of all local channels (#2196) +- `stop`: stops eclair. Please note that the recommended way of stopping eclair is simply to kill its process (#2233) +- `channelbalances` and `usablebalances` return a `shortIds` object instead of a single `shortChannelId` (#2323) ### Miscellaneous improvements and bug fixes @@ -114,8 +115,8 @@ to your node. The `eclair.channel.min-funding-satoshis` setting has been deprecated and replaced with the following two new settings and defaults: -* `eclair.channel.min-public-funding-satoshis = 100000` -* `eclair.channel.min-private-funding-satoshis = 100000` +- `eclair.channel.min-public-funding-satoshis = 100000` +- `eclair.channel.min-private-funding-satoshis = 100000` If your configuration file changes `eclair.channel.min-funding-satoshis` then you should replace it with both of these new settings. @@ -126,8 +127,8 @@ Expired incoming invoices that are unpaid will be searched for and purged from t Thereafter searches for expired unpaid invoices to purge will run once every 24 hours. You can disable this feature, or change the search interval with two new settings: -* `eclair.purge-expired-invoices.enabled = true -* `eclair.purge-expired-invoices.interval = 24 hours` +- `eclair.purge-expired-invoices.enabled = true +- `eclair.purge-expired-invoices.interval = 24 hours` ## Verifying signatures diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala index 357f418a62..8cdb82327a 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala @@ -22,7 +22,6 @@ import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey, sha256} import fr.acinq.bitcoin.scalacompat.Script._ import fr.acinq.bitcoin.scalacompat._ import fr.acinq.eclair._ -import fr.acinq.eclair.blockchain.OnChainAddressGenerator import fr.acinq.eclair.blockchain.fee.{FeeEstimator, FeeTargets, FeeratePerKw} import fr.acinq.eclair.channel.fsm.Channel import fr.acinq.eclair.channel.fsm.Channel.{ChannelConf, REFRESH_CHANNEL_UPDATE_INTERVAL} @@ -39,7 +38,6 @@ import fr.acinq.eclair.wire.protocol._ import scodec.bits.ByteVector import scala.concurrent.duration._ -import scala.concurrent.{Await, ExecutionContext} import scala.util.{Failure, Success, Try} /** @@ -186,27 +184,30 @@ object Helpers { } /** - * The general rule is that we use remote_alias for our channel_update until the channel is publicly announced, and - * then we use the real scid. - * - * Private channels are handled like public channels that have not yet been announced, there is no special case. - * - * Decision tree: - * - received remote_alias from peer - * - before channel announcement: use remote_alias - * - after channel announcement: use real scid - * - no remote_alias from peer - * - min_depth > 0: use real scid (may change if reorg between min_depth and 6 conf) - * - min_depth = 0 (zero-conf): spec violation, our peer MUST send an alias when using zero-conf + * We use the real scid if the channel has been announced, otherwise we use our local alias. */ - def scidForChannelUpdate(channelAnnouncement_opt: Option[ChannelAnnouncement], shortIds: ShortIds): ShortChannelId = { - channelAnnouncement_opt.map(_.shortChannelId) // we use the real "final" scid when it is publicly announced - .orElse(shortIds.remoteAlias_opt) // otherwise the remote alias - .orElse(shortIds.real.toOption) // if we don't have a remote alias, we use the real scid (which could change because the funding tx possibly has less than 6 confs here) - .getOrElse(throw new RuntimeException("this is a zero-conf channel and no alias was provided in channel_ready")) // if we don't have a real scid, it means this is a zero-conf channel and our peer must have sent an alias + def scidForChannelUpdate(channelAnnouncement_opt: Option[ChannelAnnouncement], localAlias: Alias): ShortChannelId = { + channelAnnouncement_opt.map(_.shortChannelId).getOrElse(localAlias) } - def scidForChannelUpdate(d: DATA_NORMAL): ShortChannelId = scidForChannelUpdate(d.channelAnnouncement, d.shortIds) + def scidForChannelUpdate(d: DATA_NORMAL): ShortChannelId = scidForChannelUpdate(d.channelAnnouncement, d.shortIds.localAlias) + + /** + * If our peer sent us an alias, that's what we must use in the channel_update we send them to ensure they're able to + * match this update with the corresponding local channel. If they didn't send us an alias, it means we're not using + * 0-conf and we'll use the real scid. + */ + def channelUpdateForDirectPeer(nodeParams: NodeParams, channelUpdate: ChannelUpdate, shortIds: ShortIds): ChannelUpdate = { + shortIds.remoteAlias_opt match { + case Some(remoteAlias) => Announcements.updateScid(nodeParams.privateKey, channelUpdate, remoteAlias) + case None => shortIds.real.toOption match { + case Some(realScid) => Announcements.updateScid(nodeParams.privateKey, channelUpdate, realScid) + // This case is a spec violation: this is a 0-conf channel, so our peer MUST send their alias. + // They won't be able to match our channel_update with their local channel, too bad for them. + case None => channelUpdate + } + } + } /** * Compute the delay until we need to refresh the channel_update for our channel not to be considered stale by diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala index bab02b5eae..9decc58b0d 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala @@ -622,7 +622,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val // this is a zero-conf channel and it is the first time we know for sure that the funding tx has been confirmed context.system.eventStream.publish(TransactionConfirmed(d.channelId, remoteNodeId, fundingTx)) } - val scidForChannelUpdate = Helpers.scidForChannelUpdate(d.channelAnnouncement, shortIds1) + val scidForChannelUpdate = Helpers.scidForChannelUpdate(d.channelAnnouncement, shortIds1.localAlias) // if the shortChannelId is different from the one we had before, we need to re-announce it val channelUpdate1 = if (d.channelUpdate.shortChannelId != scidForChannelUpdate) { log.info(s"using new scid in channel_update: old=${d.channelUpdate.shortChannelId} new=$scidForChannelUpdate") @@ -659,7 +659,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val handleLocalError(InvalidAnnouncementSignatures(d.channelId, remoteAnnSigs), d, Some(remoteAnnSigs)) } else { // we generate a new channel_update because the scid used may change if we were previously using an alias - val scidForChannelUpdate = Helpers.scidForChannelUpdate(Some(channelAnn), d.shortIds) + val scidForChannelUpdate = Helpers.scidForChannelUpdate(Some(channelAnn), d.shortIds.localAlias) val channelUpdate = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, scidForChannelUpdate, d.channelUpdate.cltvExpiryDelta, d.channelUpdate.htlcMinimumMsat, d.channelUpdate.feeBaseMsat, d.channelUpdate.feeProportionalMillionths, d.commitments.capacity.toMilliSatoshi, enable = Helpers.aboveReserve(d.commitments)) // we use goto() instead of stay() because we want to fire transitions goto(NORMAL) using d.copy(channelAnnouncement = Some(channelAnn), channelUpdate = channelUpdate) storing() @@ -1640,7 +1640,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val val lcu = LocalChannelUpdate(self, d.channelId, d.shortIds, d.commitments.remoteParams.nodeId, d.channelAnnouncement, d.channelUpdate, d.commitments) context.system.eventStream.publish(lcu) if (sendToPeer) { - send(d.channelUpdate) + send(Helpers.channelUpdateForDirectPeer(nodeParams, d.channelUpdate, d.shortIds)) } case EmitLocalChannelDown(d) => log.debug(s"emitting channel down event") diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenSingleFunder.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenSingleFunder.scala index 8966b25364..14722ddfe6 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenSingleFunder.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenSingleFunder.scala @@ -421,7 +421,7 @@ trait ChannelOpenSingleFunder extends FundingHandlers with ErrorHandlers { } log.info("shortIds: real={} localAlias={} remoteAlias={}", shortIds1.real.toOption.getOrElse("none"), shortIds1.localAlias, shortIds1.remoteAlias_opt.getOrElse("none")) // we create a channel_update early so that we can use it to send payments through this channel, but it won't be propagated to other nodes since the channel is not yet announced - val scidForChannelUpdate = Helpers.scidForChannelUpdate(channelAnnouncement_opt = None, shortIds1) + val scidForChannelUpdate = Helpers.scidForChannelUpdate(channelAnnouncement_opt = None, shortIds1.localAlias) log.info("using shortChannelId={} for initial channel_update", scidForChannelUpdate) val relayFees = getRelayFees(nodeParams, remoteNodeId, d.commitments) val initialChannelUpdate = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, scidForChannelUpdate, nodeParams.channelConf.expiryDelta, d.commitments.remoteParams.htlcMinimum, relayFees.feeBase, relayFees.feeProportionalMillionths, d.commitments.capacity.toMilliSatoshi, enable = Helpers.aboveReserve(d.commitments)) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentEvents.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentEvents.scala index 621e6b62a5..226ad8544c 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentEvents.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentEvents.scala @@ -20,6 +20,7 @@ import fr.acinq.bitcoin.scalacompat.ByteVector32 import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.eclair.crypto.Sphinx import fr.acinq.eclair.payment.Invoice.{BasicEdge, ExtraEdge} +import fr.acinq.eclair.payment.send.PaymentError.RetryExhausted import fr.acinq.eclair.payment.send.PaymentInitiator.SendPaymentConfig import fr.acinq.eclair.router.Announcements import fr.acinq.eclair.router.Router.{ChannelDesc, ChannelHop, Hop, Ignore} @@ -154,6 +155,7 @@ object PaymentFailure { def transformForUser(failures: Seq[PaymentFailure]): Seq[PaymentFailure] = { failures match { case previousFailures :+ LocalFailure(_, _, RouteNotFound) if previousFailures.nonEmpty => previousFailures + case previousFailures :+ LocalFailure(_, _, RetryExhausted) if previousFailures.nonEmpty => previousFailures case other => other } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/ChannelRelay.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/ChannelRelay.scala index c811d03a84..3ba8c507f4 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/ChannelRelay.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/ChannelRelay.scala @@ -191,7 +191,10 @@ class ChannelRelay private(nodeParams: NodeParams, private val nextNodeId_opt = channels.headOption.map(_._2.nextNodeId) /** channel id explicitly requested in the onion payload */ - private val requestedChannelId_opt = channels.find(_._2.channelUpdate.shortChannelId == r.payload.outgoingChannelId).map(_._1) + private val requestedChannelId_opt = channels.collectFirst { + case (channelId, channel) if channel.shortIds.localAlias == r.payload.outgoingChannelId => channelId + case (channelId, channel) if channel.shortIds.real.toOption.contains(r.payload.outgoingChannelId) => channelId + } /** * Select a channel to the same node to relay the payment to, that has the lowest capacity and balance and is diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/ChannelRelayer.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/ChannelRelayer.scala index 1a9bfc2fc1..59eb7b58b0 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/ChannelRelayer.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/ChannelRelayer.scala @@ -95,7 +95,7 @@ object ChannelRelayer { case WrappedLocalChannelUpdate(lcu@LocalChannelUpdate(_, channelId, shortIds, remoteNodeId, _, channelUpdate, commitments)) => context.log.debug(s"updating local channel info for channelId=$channelId realScid=${shortIds.real} localAlias=${shortIds.localAlias} remoteNodeId=$remoteNodeId channelUpdate={} commitments={}", channelUpdate, commitments) val prevChannelUpdate = channels.get(channelId).map(_.channelUpdate) - val channel = Relayer.OutgoingChannel(remoteNodeId, channelUpdate, prevChannelUpdate, commitments) + val channel = Relayer.OutgoingChannel(shortIds, remoteNodeId, channelUpdate, prevChannelUpdate, commitments) val channels1 = channels + (channelId -> channel) val mappings = lcu.scidsForRouting.map(_ -> channelId).toMap context.log.debug("adding mappings={} to channelId={}", mappings.keys.mkString(","), channelId) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/Relayer.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/Relayer.scala index 1996927e8c..c7341e7fab 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/Relayer.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/Relayer.scala @@ -29,7 +29,7 @@ import fr.acinq.eclair.channel._ import fr.acinq.eclair.db.PendingCommandsDb import fr.acinq.eclair.payment._ import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{Logs, MilliSatoshi, NodeParams, ShortChannelId} +import fr.acinq.eclair.{Logs, MilliSatoshi, NodeParams} import grizzled.slf4j.Logging import scala.concurrent.Promise @@ -133,9 +133,10 @@ object Relayer extends Logging { } case class RelayForward(add: UpdateAddHtlc) - case class ChannelBalance(remoteNodeId: PublicKey, shortChannelId: ShortChannelId, canSend: MilliSatoshi, canReceive: MilliSatoshi, isPublic: Boolean, isEnabled: Boolean) + case class ChannelBalance(remoteNodeId: PublicKey, shortIds: ShortIds, canSend: MilliSatoshi, canReceive: MilliSatoshi, isPublic: Boolean, isEnabled: Boolean) sealed trait OutgoingChannelParams { + def channelId: ByteVector32 def channelUpdate: ChannelUpdate def prevChannelUpdate: Option[ChannelUpdate] } @@ -146,11 +147,11 @@ object Relayer extends Logging { * @param enabledOnly if true, filter out disabled channels. */ case class GetOutgoingChannels(enabledOnly: Boolean = true) - case class OutgoingChannel(nextNodeId: PublicKey, channelUpdate: ChannelUpdate, prevChannelUpdate: Option[ChannelUpdate], commitments: AbstractCommitments) extends OutgoingChannelParams { - val channelId: ByteVector32 = commitments.channelId + case class OutgoingChannel(shortIds: ShortIds, nextNodeId: PublicKey, channelUpdate: ChannelUpdate, prevChannelUpdate: Option[ChannelUpdate], commitments: AbstractCommitments) extends OutgoingChannelParams { + override val channelId: ByteVector32 = commitments.channelId def toChannelBalance: ChannelBalance = ChannelBalance( remoteNodeId = nextNodeId, - shortChannelId = channelUpdate.shortChannelId, + shortIds = shortIds, canSend = commitments.availableBalanceForSend, canReceive = commitments.availableBalanceForReceive, isPublic = commitments.announceChannel, diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/Announcements.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/Announcements.scala index da8381840c..5441a3407f 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/Announcements.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/Announcements.scala @@ -18,9 +18,8 @@ package fr.acinq.eclair.router import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey, sha256, verifySignature} import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, Crypto, LexicographicalOrdering} -import fr.acinq.eclair.RealShortChannelId import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{CltvExpiryDelta, Feature, Features, MilliSatoshi, NodeFeature, ShortChannelId, TimestampSecond, TimestampSecondLong, serializationResult} +import fr.acinq.eclair.{CltvExpiryDelta, Feature, Features, MilliSatoshi, NodeFeature, RealShortChannelId, ShortChannelId, TimestampSecond, TimestampSecondLong, serializationResult} import scodec.bits.ByteVector import shapeless.HNil @@ -118,7 +117,6 @@ object Announcements { def makeChannelUpdate(chainHash: ByteVector32, nodeSecret: PrivateKey, remoteNodeId: PublicKey, shortChannelId: ShortChannelId, cltvExpiryDelta: CltvExpiryDelta, htlcMinimumMsat: MilliSatoshi, feeBaseMsat: MilliSatoshi, feeProportionalMillionths: Long, htlcMaximumMsat: MilliSatoshi, enable: Boolean = true, timestamp: TimestampSecond = TimestampSecond.now()): ChannelUpdate = { val channelFlags = ChannelUpdate.ChannelFlags(isNode1 = isNode1(nodeSecret.publicKey, remoteNodeId), isEnabled = enable) val htlcMaximumMsatOpt = Some(htlcMaximumMsat) - val witness = channelUpdateWitnessEncode(chainHash, shortChannelId, timestamp, channelFlags, cltvExpiryDelta, htlcMinimumMsat, feeBaseMsat, feeProportionalMillionths, htlcMaximumMsatOpt, TlvStream.empty) val sig = Crypto.sign(witness, nodeSecret) ChannelUpdate( @@ -135,6 +133,14 @@ object Announcements { ) } + def updateScid(nodeSecret: PrivateKey, u: ChannelUpdate, scid: ShortChannelId): ChannelUpdate = { + // NB: we don't update the timestamp as we're not changing any parameter. + val u1 = u.copy(shortChannelId = scid) + val witness = channelUpdateWitnessEncode(u.chainHash, scid, u.timestamp, u.channelFlags, u.cltvExpiryDelta, u.htlcMinimumMsat, u.feeBaseMsat, u.feeProportionalMillionths, u.htlcMaximumMsat, u.tlvStream) + val sig = Crypto.sign(witness, nodeSecret) + u1.copy(signature = sig) + } + def checkSigs(ann: ChannelAnnouncement): Boolean = { val witness = channelAnnouncementWitnessEncode(ann.chainHash, ann.shortChannelId, ann.nodeId1, ann.nodeId2, ann.bitcoinKey1, ann.bitcoinKey2, ann.features, ann.tlvStream) verifySignature(witness, ann.nodeSignature1, ann.nodeId1) && diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala index 48b031fd52..6386271f94 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala @@ -181,7 +181,7 @@ trait ChannelStateTestsBase extends Assertions with Eventually { // those features can only be enabled with AnchorOutputsZeroFeeHtlcTxs, this is to prevent incompatible test configurations if (tags.contains(ChannelStateTestsTags.ZeroConf)) assert(tags.contains(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs), "invalid test configuration") - if (tags.contains(ChannelStateTestsTags.ScidAlias)) assert(channelType.features.contains(Features.ScidAlias), "invalid test configuration") + if (tags.contains(ChannelStateTestsTags.ScidAlias)) assert(tags.contains(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs), "invalid test configuration") implicit val ec: scala.concurrent.ExecutionContext = scala.concurrent.ExecutionContext.global val aliceParams = Alice.channelParams diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForChannelReadyStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForChannelReadyStateSpec.scala index 167c75563d..e08f1c3879 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForChannelReadyStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForChannelReadyStateSpec.scala @@ -25,6 +25,7 @@ import fr.acinq.eclair.channel.fsm.Channel import fr.acinq.eclair.channel.publish.TxPublisher import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsTags} import fr.acinq.eclair.payment.relay.Relayer.RelayFees +import fr.acinq.eclair.router.Announcements import fr.acinq.eclair.wire.protocol._ import fr.acinq.eclair.{BlockHeight, MilliSatoshiLong, TestConstants, TestKitBaseClass} import org.scalatest.funsuite.FixtureAnyFunSuiteLike @@ -91,16 +92,22 @@ class WaitForChannelReadyStateSpec extends TestKitBaseClass with FixtureAnyFunSu test("recv ChannelReady") { f => import f._ // we have a real scid at this stage, because this isn't a zero-conf channel - assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_CHANNEL_READY].shortIds.real.isInstanceOf[RealScidStatus.Temporary]) - assert(bob.stateData.asInstanceOf[DATA_WAIT_FOR_CHANNEL_READY].shortIds.real.isInstanceOf[RealScidStatus.Temporary]) + val aliceIds = alice.stateData.asInstanceOf[DATA_WAIT_FOR_CHANNEL_READY].shortIds + assert(aliceIds.real.isInstanceOf[RealScidStatus.Temporary]) + val bobIds = bob.stateData.asInstanceOf[DATA_WAIT_FOR_CHANNEL_READY].shortIds + assert(bobIds.real.isInstanceOf[RealScidStatus.Temporary]) val channelReady = bob2alice.expectMsgType[ChannelReady] + assert(channelReady.alias_opt.contains(bobIds.localAlias)) bob2alice.forward(alice) - val initialChannelUpdate = alice2bob.expectMsgType[ChannelUpdate] - assert(initialChannelUpdate == alice.stateData.asInstanceOf[DATA_NORMAL].channelUpdate) - // we have a real scid, but the channel is not announced so alice uses bob's alias - assert(initialChannelUpdate.shortChannelId == channelReady.alias_opt.get) + val initialChannelUpdate = alice.stateData.asInstanceOf[DATA_NORMAL].channelUpdate + assert(initialChannelUpdate.shortChannelId == aliceIds.localAlias) assert(initialChannelUpdate.feeBaseMsat == relayFees.feeBase) assert(initialChannelUpdate.feeProportionalMillionths == relayFees.feeProportionalMillionths) + // we have a real scid, but the channel is not announced so alice uses bob's alias + val channelUpdateSentToPeer = alice2bob.expectMsgType[ChannelUpdate] + assert(channelUpdateSentToPeer.shortChannelId == bobIds.localAlias) + assert(Announcements.areSameIgnoreFlags(initialChannelUpdate, channelUpdateSentToPeer)) + assert(Announcements.checkSig(channelUpdateSentToPeer, alice.underlyingActor.nodeParams.nodeId)) alice2blockchain.expectMsgType[WatchFundingDeeplyBuried] bob2alice.expectNoMessage(100 millis) awaitCond(alice.stateName == NORMAL) @@ -109,16 +116,20 @@ class WaitForChannelReadyStateSpec extends TestKitBaseClass with FixtureAnyFunSu test("recv ChannelReady (no alias)") { f => import f._ // we have a real scid at this stage, because this isn't a zero-conf channel - val realScid = alice.stateData.asInstanceOf[DATA_WAIT_FOR_CHANNEL_READY].shortIds.real.asInstanceOf[RealScidStatus.Temporary].realScid + val aliceIds = alice.stateData.asInstanceOf[DATA_WAIT_FOR_CHANNEL_READY].shortIds + val realScid = aliceIds.real.asInstanceOf[RealScidStatus.Temporary].realScid val channelReady = bob2alice.expectMsgType[ChannelReady] val channelReadyNoAlias = channelReady.modify(_.tlvStream.records).using(_.filterNot(_.isInstanceOf[ChannelReadyTlv.ShortChannelIdTlv])) bob2alice.forward(alice, channelReadyNoAlias) - val initialChannelUpdate = alice2bob.expectMsgType[ChannelUpdate] - assert(initialChannelUpdate == alice.stateData.asInstanceOf[DATA_NORMAL].channelUpdate) - // the channel is not announced but bob didn't send an alias so we use the real scid - assert(initialChannelUpdate.shortChannelId == realScid) + val initialChannelUpdate = alice.stateData.asInstanceOf[DATA_NORMAL].channelUpdate + assert(initialChannelUpdate.shortChannelId == aliceIds.localAlias) assert(initialChannelUpdate.feeBaseMsat == relayFees.feeBase) assert(initialChannelUpdate.feeProportionalMillionths == relayFees.feeProportionalMillionths) + // the channel is not announced but bob didn't send an alias so we use the real scid + val channelUpdateSentToPeer = alice2bob.expectMsgType[ChannelUpdate] + assert(channelUpdateSentToPeer.shortChannelId == realScid) + assert(Announcements.areSameIgnoreFlags(initialChannelUpdate, channelUpdateSentToPeer)) + assert(Announcements.checkSig(channelUpdateSentToPeer, alice.underlyingActor.nodeParams.nodeId)) alice2blockchain.expectMsgType[WatchFundingDeeplyBuried] bob2alice.expectNoMessage(100 millis) awaitCond(alice.stateName == NORMAL) @@ -127,16 +138,22 @@ class WaitForChannelReadyStateSpec extends TestKitBaseClass with FixtureAnyFunSu test("recv ChannelReady (zero-conf)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs), Tag(ChannelStateTestsTags.ZeroConf)) { f => import f._ // zero-conf channel: we don't have a real scid - assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_CHANNEL_READY].shortIds.real == RealScidStatus.Unknown) - assert(bob.stateData.asInstanceOf[DATA_WAIT_FOR_CHANNEL_READY].shortIds.real == RealScidStatus.Unknown) + val aliceIds = alice.stateData.asInstanceOf[DATA_WAIT_FOR_CHANNEL_READY].shortIds + assert(aliceIds.real == RealScidStatus.Unknown) + val bobIds = bob.stateData.asInstanceOf[DATA_WAIT_FOR_CHANNEL_READY].shortIds + assert(bobIds.real == RealScidStatus.Unknown) val channelReady = bob2alice.expectMsgType[ChannelReady] + assert(channelReady.alias_opt.contains(bobIds.localAlias)) bob2alice.forward(alice) - val initialChannelUpdate = alice2bob.expectMsgType[ChannelUpdate] - assert(initialChannelUpdate == alice.stateData.asInstanceOf[DATA_NORMAL].channelUpdate) - // the channel is not announced so alice uses bob's alias (we have a no real scid anyway) - assert(initialChannelUpdate.shortChannelId == channelReady.alias_opt.get) + val initialChannelUpdate = alice.stateData.asInstanceOf[DATA_NORMAL].channelUpdate + assert(initialChannelUpdate.shortChannelId == aliceIds.localAlias) assert(initialChannelUpdate.feeBaseMsat == relayFees.feeBase) assert(initialChannelUpdate.feeProportionalMillionths == relayFees.feeProportionalMillionths) + val channelUpdateSentToPeer = alice2bob.expectMsgType[ChannelUpdate] + // the channel is not announced so alice uses bob's alias (we have a no real scid anyway) + assert(channelUpdateSentToPeer.shortChannelId == bobIds.localAlias) + assert(Announcements.areSameIgnoreFlags(initialChannelUpdate, channelUpdateSentToPeer)) + assert(Announcements.checkSig(channelUpdateSentToPeer, alice.underlyingActor.nodeParams.nodeId)) alice2blockchain.expectMsgType[WatchFundingDeeplyBuried] bob2alice.expectNoMessage(100 millis) awaitCond(alice.stateName == NORMAL) @@ -145,33 +162,48 @@ class WaitForChannelReadyStateSpec extends TestKitBaseClass with FixtureAnyFunSu test("recv ChannelReady (zero-conf, no alias)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs), Tag(ChannelStateTestsTags.ZeroConf)) { f => import f._ // zero-conf channel: we don't have a real scid - assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_CHANNEL_READY].shortIds.real == RealScidStatus.Unknown) - assert(bob.stateData.asInstanceOf[DATA_WAIT_FOR_CHANNEL_READY].shortIds.real == RealScidStatus.Unknown) + val aliceIds = alice.stateData.asInstanceOf[DATA_WAIT_FOR_CHANNEL_READY].shortIds + assert(aliceIds.real == RealScidStatus.Unknown) + val bobIds = bob.stateData.asInstanceOf[DATA_WAIT_FOR_CHANNEL_READY].shortIds + assert(bobIds.real == RealScidStatus.Unknown) val channelReady = bob2alice.expectMsgType[ChannelReady] val channelReadyNoAlias = channelReady.modify(_.tlvStream.records).using(_.filterNot(_.isInstanceOf[ChannelReadyTlv.ShortChannelIdTlv])) bob2alice.forward(alice, channelReadyNoAlias) - awaitCond(alice.stateName == CLOSING) - assert(alice2blockchain.expectMsgType[TxPublisher.PublishFinalTx].desc == "commit-tx") - assert(alice2blockchain.expectMsgType[TxPublisher.PublishTx].desc == "local-anchor") - assert(alice2blockchain.expectMsgType[TxPublisher.PublishFinalTx].desc == "local-main-delayed") - alice2blockchain.expectMsgType[WatchTxConfirmed] + val initialChannelUpdate = alice.stateData.asInstanceOf[DATA_NORMAL].channelUpdate + assert(initialChannelUpdate.shortChannelId == aliceIds.localAlias) + assert(initialChannelUpdate.feeBaseMsat == relayFees.feeBase) + assert(initialChannelUpdate.feeProportionalMillionths == relayFees.feeProportionalMillionths) + val channelUpdateSentToPeer = alice2bob.expectMsgType[ChannelUpdate] + // the channel is 0-conf but bob didn't provide an alias: it's a spec violation, so we use our local alias and if + // they can't understand it, too bad for them + assert(channelUpdateSentToPeer.shortChannelId == aliceIds.localAlias) + assert(Announcements.areSameIgnoreFlags(initialChannelUpdate, channelUpdateSentToPeer)) + alice2blockchain.expectMsgType[WatchFundingDeeplyBuried] + bob2alice.expectNoMessage(100 millis) + awaitCond(alice.stateName == NORMAL) } test("recv ChannelReady (public)", Tag(ChannelStateTestsTags.ChannelsPublic)) { f => import f._ // we have a real scid at this stage, because this isn't a zero-conf channel - assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_CHANNEL_READY].shortIds.real.isInstanceOf[RealScidStatus.Temporary]) + val aliceIds = alice.stateData.asInstanceOf[DATA_WAIT_FOR_CHANNEL_READY].shortIds + assert(aliceIds.real.isInstanceOf[RealScidStatus.Temporary]) assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_CHANNEL_READY].commitments.channelFlags.announceChannel) - assert(bob.stateData.asInstanceOf[DATA_WAIT_FOR_CHANNEL_READY].shortIds.real.isInstanceOf[RealScidStatus.Temporary]) + val bobIds = bob.stateData.asInstanceOf[DATA_WAIT_FOR_CHANNEL_READY].shortIds + assert(bobIds.real.isInstanceOf[RealScidStatus.Temporary]) assert(bob.stateData.asInstanceOf[DATA_WAIT_FOR_CHANNEL_READY].commitments.channelFlags.announceChannel) val channelReady = bob2alice.expectMsgType[ChannelReady] + assert(channelReady.alias_opt.contains(bobIds.localAlias)) bob2alice.forward(alice) - val initialChannelUpdate = alice2bob.expectMsgType[ChannelUpdate] - assert(initialChannelUpdate == alice.stateData.asInstanceOf[DATA_NORMAL].channelUpdate) - // we have a real scid, but it is not the final one (less than 6 confirmations) so alice uses bob's alias - assert(initialChannelUpdate.shortChannelId == channelReady.alias_opt.get) + val initialChannelUpdate = alice.stateData.asInstanceOf[DATA_NORMAL].channelUpdate + assert(initialChannelUpdate.shortChannelId == aliceIds.localAlias) assert(initialChannelUpdate.feeBaseMsat == relayFees.feeBase) assert(initialChannelUpdate.feeProportionalMillionths == relayFees.feeProportionalMillionths) + val channelUpdateSentToPeer = alice2bob.expectMsgType[ChannelUpdate] + // we have a real scid, but it is not the final one (less than 6 confirmations) so alice uses bob's alias + assert(channelUpdateSentToPeer.shortChannelId == bobIds.localAlias) + assert(Announcements.areSameIgnoreFlags(initialChannelUpdate, channelUpdateSentToPeer)) + assert(Announcements.checkSig(channelUpdateSentToPeer, alice.underlyingActor.nodeParams.nodeId)) alice2blockchain.expectMsgType[WatchFundingDeeplyBuried] bob2alice.expectNoMessage(100 millis) awaitCond(alice.stateName == NORMAL) @@ -180,16 +212,22 @@ class WaitForChannelReadyStateSpec extends TestKitBaseClass with FixtureAnyFunSu test("recv ChannelReady (public, zero-conf)", Tag(ChannelStateTestsTags.ChannelsPublic), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs), Tag(ChannelStateTestsTags.ZeroConf)) { f => import f._ // zero-conf channel: we don't have a real scid - assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_CHANNEL_READY].shortIds.real == RealScidStatus.Unknown) - assert(bob.stateData.asInstanceOf[DATA_WAIT_FOR_CHANNEL_READY].shortIds.real == RealScidStatus.Unknown) + val aliceIds = alice.stateData.asInstanceOf[DATA_WAIT_FOR_CHANNEL_READY].shortIds + assert(aliceIds.real == RealScidStatus.Unknown) + val bobIds = bob.stateData.asInstanceOf[DATA_WAIT_FOR_CHANNEL_READY].shortIds + assert(bobIds.real == RealScidStatus.Unknown) val channelReady = bob2alice.expectMsgType[ChannelReady] + assert(channelReady.alias_opt.contains(bobIds.localAlias)) bob2alice.forward(alice) - val initialChannelUpdate = alice2bob.expectMsgType[ChannelUpdate] - assert(initialChannelUpdate == alice.stateData.asInstanceOf[DATA_NORMAL].channelUpdate) - // the channel is not announced, so alice uses bob's alias (we have a no real scid anyway) - assert(initialChannelUpdate.shortChannelId == channelReady.alias_opt.get) + val initialChannelUpdate = alice.stateData.asInstanceOf[DATA_NORMAL].channelUpdate + assert(initialChannelUpdate.shortChannelId == aliceIds.localAlias) assert(initialChannelUpdate.feeBaseMsat == relayFees.feeBase) assert(initialChannelUpdate.feeProportionalMillionths == relayFees.feeProportionalMillionths) + val channelUpdateSentToPeer = alice2bob.expectMsgType[ChannelUpdate] + // the channel is not announced, so alice uses bob's alias (we have a no real scid anyway) + assert(channelUpdateSentToPeer.shortChannelId == bobIds.localAlias) + assert(Announcements.areSameIgnoreFlags(initialChannelUpdate, channelUpdateSentToPeer)) + assert(Announcements.checkSig(channelUpdateSentToPeer, alice.underlyingActor.nodeParams.nodeId)) alice2blockchain.expectMsgType[WatchFundingDeeplyBuried] bob2alice.expectNoMessage(100 millis) awaitCond(alice.stateName == NORMAL) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala index 8ff235ff85..5ec60cd941 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala @@ -844,7 +844,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice2bob.expectMsgType[CommitSig] awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.remoteNextCommitInfo.isLeft) val waitForRevocation = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.remoteNextCommitInfo.left.toOption.get - assert(waitForRevocation.reSignAsap == false) + assert(!waitForRevocation.reSignAsap) // actual test starts here alice ! CMD_SIGN() @@ -861,7 +861,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice2bob.expectMsgType[CommitSig] awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.remoteNextCommitInfo.isLeft) val waitForRevocation = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.remoteNextCommitInfo.left.toOption.get - assert(waitForRevocation.reSignAsap == false) + assert(!waitForRevocation.reSignAsap) // actual test starts here addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) @@ -1221,7 +1221,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) alice ! CMD_SIGN() sender.expectNoMessage(300 millis) - assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.remoteNextCommitInfo.left.toOption.get.reSignAsap == true) + assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.remoteNextCommitInfo.left.toOption.get.reSignAsap) // actual test starts here bob2alice.expectMsgType[RevokeAndAck] @@ -1364,7 +1364,6 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with test("recv RevokeAndAck (over max dust htlc exposure in local commit only with pending local changes)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs), Tag(ChannelStateTestsTags.HighDustLimitDifferenceAliceBob)) { f => import f._ - val sender = TestProbe() assert(alice.underlyingActor.nodeParams.channelConf.dustLimit == 5000.sat) assert(bob.underlyingActor.nodeParams.channelConf.dustLimit == 1000.sat) testRevokeAndAckDustOverflowSingleCommit(f) @@ -1372,7 +1371,6 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with test("recv RevokeAndAck (over max dust htlc exposure in remote commit only with pending local changes)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs), Tag(ChannelStateTestsTags.HighDustLimitDifferenceBobAlice)) { f => import f._ - val sender = TestProbe() assert(alice.underlyingActor.nodeParams.channelConf.dustLimit == 1000.sat) assert(bob.underlyingActor.nodeParams.channelConf.dustLimit == 5000.sat) testRevokeAndAckDustOverflowSingleCommit(f) @@ -3402,8 +3400,8 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with test("recv WatchFundingDeeplyBuriedTriggered (public channel)", Tag(ChannelStateTestsTags.ChannelsPublic)) { f => import f._ - val realShortChannelId = alice.stateData.asInstanceOf[DATA_NORMAL].shortIds.real.asInstanceOf[RealScidStatus.Temporary].realScid - val bobAlias = bob.stateData.asInstanceOf[DATA_NORMAL].shortIds.localAlias + val aliceIds = alice.stateData.asInstanceOf[DATA_NORMAL].shortIds + val realShortChannelId = aliceIds.real.asInstanceOf[RealScidStatus.Temporary].realScid // existing funding tx coordinates val TxCoordinates(blockHeight, txIndex, _) = ShortChannelId.coordinates(realShortChannelId) alice ! WatchFundingDeeplyBuriedTriggered(blockHeight, txIndex, null) @@ -3411,17 +3409,16 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(annSigs.shortChannelId == realShortChannelId) // alice updates her internal state awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].shortIds.real == RealScidStatus.Final(realShortChannelId)) - // public channel: alice will update the channel update to use the real scid when she receives her peer's announcement_signatures alice2bob.expectNoMessage(100 millis) channelUpdateListener.expectNoMessage(100 millis) - assert(alice.stateData.asInstanceOf[DATA_NORMAL].channelUpdate.shortChannelId == bobAlias) + assert(alice.stateData.asInstanceOf[DATA_NORMAL].channelUpdate.shortChannelId == aliceIds.localAlias) } test("recv WatchFundingDeeplyBuriedTriggered (public channel, zero-conf)", Tag(ChannelStateTestsTags.ChannelsPublic), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs), Tag(ChannelStateTestsTags.ZeroConf)) { f => import f._ // in zero-conf channel we don't have a real short channel id when going to NORMAL state - assert(alice.stateData.asInstanceOf[DATA_NORMAL].shortIds.real == RealScidStatus.Unknown) - val bobAlias = bob.stateData.asInstanceOf[DATA_NORMAL].shortIds.localAlias + val aliceIds = alice.stateData.asInstanceOf[DATA_NORMAL].shortIds + assert(aliceIds.real == RealScidStatus.Unknown) // funding tx coordinates (unknown before) val (blockHeight, txIndex) = (BlockHeight(400000), 42) alice ! WatchFundingDeeplyBuriedTriggered(blockHeight, txIndex, null) @@ -3430,16 +3427,15 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(annSigs.shortChannelId == realShortChannelId) // alice updates her internal state awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].shortIds.real == RealScidStatus.Final(realShortChannelId)) - // public channel: alice will update the channel update to use the real scid when she receives her peer's announcement_signatures alice2bob.expectNoMessage(100 millis) channelUpdateListener.expectNoMessage(100 millis) - assert(alice.stateData.asInstanceOf[DATA_NORMAL].channelUpdate.shortChannelId == bobAlias) + assert(alice.stateData.asInstanceOf[DATA_NORMAL].channelUpdate.shortChannelId == aliceIds.localAlias) } test("recv WatchFundingDeeplyBuriedTriggered (public channel, short channel id changed)", Tag(ChannelStateTestsTags.ChannelsPublic)) { f => import f._ - val realShortChannelId = alice.stateData.asInstanceOf[DATA_NORMAL].shortIds.real.asInstanceOf[RealScidStatus.Temporary].realScid - val bobAlias = bob.stateData.asInstanceOf[DATA_NORMAL].shortIds.localAlias + val aliceIds = alice.stateData.asInstanceOf[DATA_NORMAL].shortIds + val realShortChannelId = aliceIds.real.asInstanceOf[RealScidStatus.Temporary].realScid // existing funding tx coordinates val TxCoordinates(blockHeight, txIndex, _) = ShortChannelId.coordinates(realShortChannelId) // new funding tx coordinates (there was a reorg) @@ -3450,25 +3446,24 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(annSigs.shortChannelId == newRealShortChannelId) // update data with real short channel id awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].shortIds.real == RealScidStatus.Final(newRealShortChannelId)) - // public channel: alice will update the channel update to use the real scid when she receives her peer's announcement_signatures alice2bob.expectNoMessage(100 millis) channelUpdateListener.expectNoMessage(100 millis) - assert(alice.stateData.asInstanceOf[DATA_NORMAL].channelUpdate.shortChannelId == bobAlias) + assert(alice.stateData.asInstanceOf[DATA_NORMAL].channelUpdate.shortChannelId == aliceIds.localAlias) } test("recv WatchFundingDeeplyBuriedTriggered (private channel)") { f => import f._ + val aliceIds = alice.stateData.asInstanceOf[DATA_NORMAL].shortIds val realShortChannelId = alice.stateData.asInstanceOf[DATA_NORMAL].shortIds.real.asInstanceOf[RealScidStatus.Temporary].realScid - val bobAlias = bob.stateData.asInstanceOf[DATA_NORMAL].shortIds.localAlias // existing funding tx coordinates val TxCoordinates(blockHeight, txIndex, _) = ShortChannelId.coordinates(realShortChannelId) alice ! WatchFundingDeeplyBuriedTriggered(blockHeight, txIndex, null) // update data with real short channel id awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].shortIds.real == RealScidStatus.Final(realShortChannelId)) - // private channel: we prefer the remote alias, so there is no change in the channel_update, and we don't send a new one + // private channel: we'll use the remote alias in the channel_update we sent to our peer, so there is no change and we don't send a new one alice2bob.expectNoMessage(100 millis) channelUpdateListener.expectNoMessage(100 millis) - assert(alice.stateData.asInstanceOf[DATA_NORMAL].channelUpdate.shortChannelId == bobAlias) + assert(alice.stateData.asInstanceOf[DATA_NORMAL].channelUpdate.shortChannelId == aliceIds.localAlias) } test("recv WatchFundingDeeplyBuriedTriggered (private channel, zero-conf)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs), Tag(ChannelStateTestsTags.ZeroConf)) { f => @@ -3477,24 +3472,24 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val listener = TestProbe() alice.underlying.system.eventStream.subscribe(listener.ref, classOf[TransactionConfirmed]) // zero-conf channel: the funding tx isn't confirmed - assert(alice.stateData.asInstanceOf[DATA_NORMAL].shortIds.real == RealScidStatus.Unknown) - val bobAlias = bob.stateData.asInstanceOf[DATA_NORMAL].shortIds.localAlias + val aliceIds = alice.stateData.asInstanceOf[DATA_NORMAL].shortIds + assert(aliceIds.real == RealScidStatus.Unknown) alice ! WatchFundingDeeplyBuriedTriggered(BlockHeight(42000), 42, null) val realShortChannelId = RealShortChannelId(BlockHeight(42000), 42, 0) // update data with real short channel id awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].shortIds.real == RealScidStatus.Final(realShortChannelId)) - // private channel: we prefer the remote alias, so there is no change in the channel_update, and we don't send a new one + // private channel: we'll use the remote alias in the channel_update we sent to our peer, so there is no change and we don't send a new one alice2bob.expectNoMessage(100 millis) channelUpdateListener.expectNoMessage(100 millis) - assert(alice.stateData.asInstanceOf[DATA_NORMAL].channelUpdate.shortChannelId == bobAlias) + assert(alice.stateData.asInstanceOf[DATA_NORMAL].channelUpdate.shortChannelId == aliceIds.localAlias) // this is the first time we know the funding tx has been confirmed listener.expectMsgType[TransactionConfirmed] } test("recv WatchFundingDeeplyBuriedTriggered (private channel, short channel id changed)") { f => import f._ - val realShortChannelId = alice.stateData.asInstanceOf[DATA_NORMAL].shortIds.real.asInstanceOf[RealScidStatus.Temporary].realScid - val bobAlias = bob.stateData.asInstanceOf[DATA_NORMAL].shortIds.localAlias + val aliceIds = alice.stateData.asInstanceOf[DATA_NORMAL].shortIds + val realShortChannelId = aliceIds.real.asInstanceOf[RealScidStatus.Temporary].realScid // existing funding tx coordinates val TxCoordinates(blockHeight, txIndex, _) = ShortChannelId.coordinates(realShortChannelId) // new funding tx coordinates (there was a reorg) @@ -3503,10 +3498,10 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val newRealShortChannelId = RealShortChannelId(blockHeight1, txIndex1, alice.stateData.asInstanceOf[DATA_NORMAL].commitments.commitInput.outPoint.index.toInt) // update data with real short channel id awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].shortIds.real == RealScidStatus.Final(newRealShortChannelId)) - // private channel: we prefer the remote alias, so there is no change in the channel_update, and we don't send a new one + // private channel: we'll use the remote alias in the channel_update we sent to our peer, so there is no change and we don't send a new one alice2bob.expectNoMessage(100 millis) channelUpdateListener.expectNoMessage(100 millis) - assert(alice.stateData.asInstanceOf[DATA_NORMAL].channelUpdate.shortChannelId == bobAlias) + assert(alice.stateData.asInstanceOf[DATA_NORMAL].channelUpdate.shortChannelId == aliceIds.localAlias) } test("recv AnnouncementSignatures", Tag(ChannelStateTestsTags.ChannelsPublic)) { f => @@ -3579,14 +3574,16 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with import f._ alice ! WatchFundingDeeplyBuriedTriggered(BlockHeight(400000), 42, null) bob ! WatchFundingDeeplyBuriedTriggered(BlockHeight(400000), 42, null) - bob2alice.expectMsgType[AnnouncementSignatures] + val realScid = bob2alice.expectMsgType[AnnouncementSignatures].shortChannelId bob2alice.forward(alice) val update1 = channelUpdateListener.expectMsgType[LocalChannelUpdate] + assert(update1.channelUpdate.shortChannelId == realScid) // actual test starts here Thread.sleep(1100) alice ! BroadcastChannelUpdate(PeriodicRefresh) val update2 = channelUpdateListener.expectMsgType[LocalChannelUpdate] + assert(update2.channelUpdate.shortChannelId == realScid) assert(update1.channelUpdate.timestamp < update2.channelUpdate.timestamp) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/ThreeNodesIntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/ThreeNodesIntegrationSpec.scala index 72da991d06..c0cc5482f8 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/ThreeNodesIntegrationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/ThreeNodesIntegrationSpec.scala @@ -51,7 +51,7 @@ class ThreeNodesIntegrationSpec extends FixtureSpec with IntegrationPatience { assert(routerData.channels.values.flatMap(c => c.update_1_opt.toSeq ++ c.update_2_opt.toSeq).size == 4) // 2 channel_updates per channel } - sendPayment(alice, carol, 100_000 msat) + sendSuccessfulPayment(alice, carol, 100_000 msat) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/TwoNodesIntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/TwoNodesIntegrationSpec.scala index 89598db9e5..1e8a87c32f 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/TwoNodesIntegrationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/TwoNodesIntegrationSpec.scala @@ -87,7 +87,7 @@ class TwoNodesIntegrationSpec extends FixtureSpec with IntegrationPatience { getRouterData(alice).privateChannels.size == 1 } - sendPayment(alice, bob, 10_000 msat) + sendSuccessfulPayment(alice, bob, 10_000 msat) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/ZeroConfAliasIntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/ZeroConfAliasIntegrationSpec.scala index 05af0ee034..33bdf0d4c9 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/ZeroConfAliasIntegrationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/ZeroConfAliasIntegrationSpec.scala @@ -5,9 +5,13 @@ import fr.acinq.bitcoin.scalacompat.{ByteVector32, SatoshiLong} import fr.acinq.eclair.FeatureSupport.{Mandatory, Optional} import fr.acinq.eclair.Features.{ScidAlias, ZeroConf} import fr.acinq.eclair.channel.{DATA_NORMAL, RealScidStatus} +import fr.acinq.eclair.crypto.Sphinx import fr.acinq.eclair.integration.basic.fixtures.ThreeNodesFixture -import fr.acinq.eclair.payment.PaymentSent +import fr.acinq.eclair.payment.Bolt11Invoice.ExtraHop +import fr.acinq.eclair.payment._ +import fr.acinq.eclair.router.RouteNotFound import fr.acinq.eclair.testutils.FixtureSpec +import fr.acinq.eclair.wire.protocol.{FailureMessage, UnknownNextPeer, Update} import fr.acinq.eclair.{MilliSatoshiLong, RealShortChannelId} import org.scalatest.OptionValues.convertOptionToValuable import org.scalatest.concurrent.IntegrationPatience @@ -60,17 +64,35 @@ class ZeroConfAliasIntegrationSpec extends FixtureSpec with IntegrationPatience (channelId_ab, channelId_bc) } - private def sendPaymentAliceToCarol(f: FixtureParam, useHint: Boolean = false, overrideHintScid_opt: Option[RealShortChannelId] = None): PaymentSent = { + private def createBobToCarolTestHint(f: FixtureParam, useHint: Boolean, overrideHintScid_opt: Option[RealShortChannelId]): Seq[ExtraHop] = { import f._ - val hint = if (useHint) { + if (useHint) { val Some(carolHint) = getRouterData(carol).privateChannels.values.head.toIncomingExtraHop // due to how node ids are built, bob < carol so carol is always the node 2 val bobAlias = getRouterData(bob).privateChannels.values.find(_.nodeId2 == carol.nodeParams.nodeId).value.shortIds.localAlias // the hint is always using the alias assert(carolHint.shortChannelId == bobAlias) Seq(carolHint.modify(_.shortChannelId).setToIfDefined(overrideHintScid_opt)) - } else Seq.empty - sendPayment(alice, carol, 100_000 msat, hints = Seq(hint)) + } else { + Seq.empty + } + } + + case object Ok + + private def sendPaymentAliceToCarol(f: FixtureParam, expected: Either[Either[Throwable, FailureMessage], Ok.type], useHint: Boolean = false, overrideHintScid_opt: Option[RealShortChannelId] = None): Unit = { + import f._ + val result = sendPayment(alice, carol, 100_000 msat, hints = Seq(createBobToCarolTestHint(f, useHint, overrideHintScid_opt))) match { + case Left(paymentFailed) => + Left(PaymentFailure.transformForUser(paymentFailed.failures).last match { + case LocalFailure(_, _, t) => Left(t) + case RemoteFailure(_, _, e) => Right(e.failureMessage) + case _: UnreadableRemoteFailure => fail("received unreadable remote failure") + }) + case Right(_) => Right(Ok) + } + + assert(result == expected) } private def internalTest(f: FixtureParam, @@ -78,9 +100,9 @@ class ZeroConfAliasIntegrationSpec extends FixtureSpec with IntegrationPatience bcPublic: Boolean, bcZeroConf: Boolean, bcScidAlias: Boolean, - paymentWorksWithoutHint: Boolean, - paymentWorksWithHint_opt: Option[Boolean], - paymentWorksWithRealScidHint_opt: Option[Boolean]): Unit = { + paymentWithoutHint: Either[Either[Throwable, FailureMessage], Ok.type], + paymentWithHint_opt: Option[Either[Either[Throwable, FailureMessage], Ok.type]], + paymentWithRealScidHint_opt: Option[Either[Either[Throwable, FailureMessage], Ok.type]]): Unit = { import f._ val (_, channelId_bc) = createChannels(f)(deepConfirm = deepConfirm) @@ -108,33 +130,18 @@ class ZeroConfAliasIntegrationSpec extends FixtureSpec with IntegrationPatience } eventually { - if (paymentWorksWithoutHint) { - sendPaymentAliceToCarol(f) - } else { - intercept[AssertionError] { - sendPaymentAliceToCarol(f) - } - } + sendPaymentAliceToCarol(f, paymentWithoutHint) } - eventually { - paymentWorksWithHint_opt match { - case Some(true) => sendPaymentAliceToCarol(f, useHint = true) - case Some(false) => intercept[AssertionError] { - sendPaymentAliceToCarol(f, useHint = true) - } - case None => // skipped + paymentWithHint_opt.foreach { paymentWithHint => + eventually { + sendPaymentAliceToCarol(f, paymentWithHint, useHint = true) } } - eventually { - paymentWorksWithRealScidHint_opt match { - // if alice uses the real scid instead of the bob-carol alias, it still works - case Some(true) => sendPaymentAliceToCarol(f, useHint = true, overrideHintScid_opt = Some(getChannelData(bob, channelId_bc).asInstanceOf[DATA_NORMAL].shortIds.real.toOption.value)) - case Some(false) => intercept[AssertionError] { - sendPaymentAliceToCarol(f, useHint = true, overrideHintScid_opt = Some(getChannelData(bob, channelId_bc).asInstanceOf[DATA_NORMAL].shortIds.real.toOption.value)) - } - case None => // skipped + paymentWithRealScidHint_opt.foreach { paymentWithRealScidHint => + eventually { + sendPaymentAliceToCarol(f, paymentWithRealScidHint, useHint = true, overrideHintScid_opt = Some(getChannelData(bob, channelId_bc).asInstanceOf[DATA_NORMAL].shortIds.real.toOption.value)) } } } @@ -145,9 +152,9 @@ class ZeroConfAliasIntegrationSpec extends FixtureSpec with IntegrationPatience bcPublic = false, bcZeroConf = false, bcScidAlias = false, - paymentWorksWithoutHint = false, // alice can't find a route to carol because bob-carol isn't announced - paymentWorksWithHint_opt = Some(true), // with a routing hint the payment works (and it will use the alias, even if the feature isn't enabled) - paymentWorksWithRealScidHint_opt = Some(true) // if alice uses the real scid instead of the bob-carol alias, it still works + paymentWithoutHint = Left(Left(RouteNotFound)), // alice can't find a route to carol because bob-carol isn't announced + paymentWithHint_opt = Some(Right(Ok)), // with a routing hint the payment works (and it will use the alias, even if the feature isn't enabled) + paymentWithRealScidHint_opt = Some(Right(Ok)) // if alice uses the real scid instead of the bob-carol alias, it still works ) } @@ -157,9 +164,9 @@ class ZeroConfAliasIntegrationSpec extends FixtureSpec with IntegrationPatience bcPublic = false, bcZeroConf = false, bcScidAlias = true, - paymentWorksWithoutHint = false, // alice can't find a route to carol because bob-carol isn't announced - paymentWorksWithHint_opt = Some(true), // with a routing hint the payment works - paymentWorksWithRealScidHint_opt = Some(false) // if alice uses the real scid instead of the bob-carol alias, it doesn't work due to option_scid_alias + paymentWithoutHint = Left(Left(RouteNotFound)), // alice can't find a route to carol because bob-carol isn't announced + paymentWithHint_opt = Some(Right(Ok)), // with a routing hint the payment works + paymentWithRealScidHint_opt = Some(Left(Right(UnknownNextPeer))) // if alice uses the real scid instead of the bob-carol alias, it doesn't work due to option_scid_alias ) } @@ -169,9 +176,9 @@ class ZeroConfAliasIntegrationSpec extends FixtureSpec with IntegrationPatience bcPublic = false, bcZeroConf = true, bcScidAlias = false, - paymentWorksWithoutHint = false, // alice can't find a route to carol because bob-carol isn't announced - paymentWorksWithHint_opt = Some(true), // with a routing hint the payment works - paymentWorksWithRealScidHint_opt = None // there is no real scid for bob-carol yet + paymentWithoutHint = Left(Left(RouteNotFound)), // alice can't find a route to carol because bob-carol isn't announced + paymentWithHint_opt = Some(Right(Ok)), // with a routing hint the payment works + paymentWithRealScidHint_opt = None // there is no real scid for bob-carol yet ) } @@ -181,14 +188,14 @@ class ZeroConfAliasIntegrationSpec extends FixtureSpec with IntegrationPatience bcPublic = false, bcZeroConf = true, bcScidAlias = false, - paymentWorksWithoutHint = false, // alice can't find a route to carol because bob-carol isn't announced - paymentWorksWithHint_opt = Some(true), // with a routing hint the payment works + paymentWithoutHint = Left(Left(RouteNotFound)), // alice can't find a route to carol because bob-carol isn't announced + paymentWithHint_opt = Some(Right(Ok)), // with a routing hint the payment works // TODO: we should be able to send payments with the real scid in the routing hint, but this currently doesn't work, // because the ChannelRelayer relies on the the LocalChannelUpdate event to maintain its scid resolution map, and // the channel doesn't emit a new one when a real scid is assigned, because we use the remote alias for the // channel_update, not the real scid. So the channel_update remains the same. We used to have the ChannelRelayer // also listen to ShortChannelIdAssigned event, but it's doesn't seem worth it here. - paymentWorksWithRealScidHint_opt = None + paymentWithRealScidHint_opt = None ) } @@ -198,9 +205,9 @@ class ZeroConfAliasIntegrationSpec extends FixtureSpec with IntegrationPatience bcPublic = false, bcZeroConf = true, bcScidAlias = true, - paymentWorksWithoutHint = false, // alice can't find a route to carol because bob-carol isn't announced - paymentWorksWithHint_opt = Some(true), // with a routing hint the payment works - paymentWorksWithRealScidHint_opt = Some(false) // if alice uses the real scid instead of the b-c alias, it doesn't work due to option_scid_alias + paymentWithoutHint = Left(Left(RouteNotFound)), // alice can't find a route to carol because bob-carol isn't announced + paymentWithHint_opt = Some(Right(Ok)), // with a routing hint the payment works + paymentWithRealScidHint_opt = Some(Left(Right(UnknownNextPeer))) // if alice uses the real scid instead of the b-c alias, it doesn't work due to option_scid_alias ) } @@ -210,9 +217,9 @@ class ZeroConfAliasIntegrationSpec extends FixtureSpec with IntegrationPatience bcPublic = true, bcZeroConf = true, bcScidAlias = false, - paymentWorksWithoutHint = false, // alice can't find a route to carol because bob-carol isn't announced yet - paymentWorksWithHint_opt = Some(true), // with a routing hint the payment works - paymentWorksWithRealScidHint_opt = None // there is no real scid for bob-carol yet + paymentWithoutHint = Left(Left(RouteNotFound)), // alice can't find a route to carol because bob-carol isn't announced yet + paymentWithHint_opt = Some(Right(Ok)), // with a routing hint the payment works + paymentWithRealScidHint_opt = None // there is no real scid for bob-carol yet ) } @@ -222,10 +229,36 @@ class ZeroConfAliasIntegrationSpec extends FixtureSpec with IntegrationPatience bcPublic = true, bcZeroConf = true, bcScidAlias = false, - paymentWorksWithoutHint = true, - paymentWorksWithHint_opt = None, // there is no routing hints for public channels - paymentWorksWithRealScidHint_opt = None // there is no routing hints for public channels + paymentWithoutHint = Right(Ok), + paymentWithHint_opt = None, // there is no routing hints for public channels + paymentWithRealScidHint_opt = None // there is no routing hints for public channels ) } + test("temporary channel failures don't leak the real scid", Tag(ScidAliasBobCarol), Tag(ZeroConfBobCarol)) { f => + import f._ + + val (_, channelId_bc) = createChannels(f)(deepConfirm = false) + + eventually { + assert(getChannelData(bob, channelId_bc).asInstanceOf[DATA_NORMAL].commitments.channelFeatures.features.contains(ZeroConf)) + assert(getChannelData(bob, channelId_bc).asInstanceOf[DATA_NORMAL].commitments.channelFeatures.features.contains(ScidAlias)) + assert(getChannelData(bob, channelId_bc).asInstanceOf[DATA_NORMAL].shortIds.real == RealScidStatus.Unknown) + assert(getRouterData(bob).privateChannels.values.exists(_.nodeId2 == carol.nodeParams.nodeId)) + } + + val Some(carolHint) = getRouterData(carol).privateChannels.values.head.toIncomingExtraHop + val bobAlias = getRouterData(bob).privateChannels.values.find(_.nodeId2 == carol.nodeParams.nodeId).value.shortIds.localAlias + assert(carolHint.shortChannelId == bobAlias) + + // We make sure Bob won't have enough liquidity to relay another payment. + sendSuccessfulPayment(bob, carol, 60_000_000 msat) + + // The channel update returned in failures doesn't leak the real scid. + val failure = sendFailingPayment(alice, carol, 50_000_000 msat, hints = Seq(Seq(carolHint))) + val failureWithChannelUpdate = failure.failures.collect { case RemoteFailure(_, _, Sphinx.DecryptedFailurePacket(_, f: Update)) => f } + assert(failureWithChannelUpdate.length == 1) + assert(failureWithChannelUpdate.head.update.shortChannelId == bobAlias) + } + } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/fixtures/MinimalNodeFixture.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/fixtures/MinimalNodeFixture.scala index cd6ba696e9..0667d515e1 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/fixtures/MinimalNodeFixture.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/fixtures/MinimalNodeFixture.scala @@ -21,12 +21,12 @@ import fr.acinq.eclair.payment.Bolt11Invoice.ExtraHop import fr.acinq.eclair.payment.receive.{MultiPartHandler, PaymentHandler} import fr.acinq.eclair.payment.relay.{ChannelRelayer, Relayer} import fr.acinq.eclair.payment.send.PaymentInitiator -import fr.acinq.eclair.payment.{Bolt11Invoice, PaymentSent} +import fr.acinq.eclair.payment.{Bolt11Invoice, PaymentEvent, PaymentFailed, PaymentSent} import fr.acinq.eclair.router.Router import fr.acinq.eclair.wire.protocol.IPAddress import fr.acinq.eclair.{BlockHeight, MilliSatoshi, MilliSatoshiLong, NodeParams, RealShortChannelId, SubscriptionsComplete, TestBitcoinCoreClient, TestDatabases, TestFeeEstimator} -import org.scalatest.Assertions import org.scalatest.concurrent.Eventually.eventually +import org.scalatest.{Assertions, EitherValues} import java.net.InetAddress import java.util.UUID @@ -51,7 +51,7 @@ case class MinimalNodeFixture private(nodeParams: NodeParams, watcher: TestProbe, wallet: DummyOnChainWallet) -object MinimalNodeFixture extends Assertions { +object MinimalNodeFixture extends Assertions with EitherValues { def nodeParamsFor(alias: String, seed: ByteVector32): NodeParams = { NodeParams.makeNodeParams( @@ -278,14 +278,26 @@ object MinimalNodeFixture extends Assertions { case _ => TestActor.KeepRunning } - def sendPayment(node1: MinimalNodeFixture, node2: MinimalNodeFixture, amount: MilliSatoshi, hints: Seq[Seq[ExtraHop]] = Seq.empty)(implicit system: ActorSystem): PaymentSent = { + def sendPayment(node1: MinimalNodeFixture, node2: MinimalNodeFixture, amount: MilliSatoshi, hints: Seq[Seq[ExtraHop]] = Seq.empty)(implicit system: ActorSystem): Either[PaymentFailed, PaymentSent] = { val sender = TestProbe("sender") sender.send(node2.paymentHandler, MultiPartHandler.ReceivePayment(Some(amount), Left("test payment"))) val invoice = sender.expectMsgType[Bolt11Invoice] val routeParams = node1.nodeParams.routerConf.pathFindingExperimentConf.experiments.values.head.getDefaultRouteParams sender.send(node1.paymentInitiator, PaymentInitiator.SendPaymentToNode(amount, invoice, maxAttempts = 1, routeParams = routeParams, extraEdges = hints.flatMap(Bolt11Invoice.toExtraEdges(_, node2.nodeParams.nodeId)), blockUntilComplete = true)) - sender.expectMsgType[PaymentSent] + sender.expectMsgType[PaymentEvent] match { + case e: PaymentSent => Right(e) + case e: PaymentFailed => Left(e) + case e => fail(s"unexpected event $e") + } + } + + def sendSuccessfulPayment(node1: MinimalNodeFixture, node2: MinimalNodeFixture, amount: MilliSatoshi, hints: Seq[Seq[ExtraHop]] = Seq.empty)(implicit system: ActorSystem): PaymentSent = { + sendPayment(node1, node2, amount, hints).value + } + + def sendFailingPayment(node1: MinimalNodeFixture, node2: MinimalNodeFixture, amount: MilliSatoshi, hints: Seq[Seq[ExtraHop]] = Seq.empty)(implicit system: ActorSystem): PaymentFailed = { + sendPayment(node1, node2, amount, hints).left.value } def prettyPrint(routerData: Router.Data, nodes: MinimalNodeFixture*): Unit = { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/ChannelRelayerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/ChannelRelayerSpec.scala index 912569ce3c..695a9cf96b 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/ChannelRelayerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/ChannelRelayerSpec.scala @@ -78,7 +78,7 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a fwd } - def basicRelaytest(f: FixtureParam)(relayPayloadScid: ShortChannelId, lcu: LocalChannelUpdate, success: Boolean): Unit = { + def basicRelayTest(f: FixtureParam)(relayPayloadScid: ShortChannelId, lcu: LocalChannelUpdate, success: Boolean): Unit = { import f._ val payload = RelayLegacyPayload(relayPayloadScid, outgoingAmount, outgoingExpiry) @@ -95,37 +95,35 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a } test("relay with real scid (channel update uses real scid)") { f => - basicRelaytest(f)(relayPayloadScid = realScid1, lcu = createLocalUpdate(channelId1), success = true) + basicRelayTest(f)(relayPayloadScid = realScid1, lcu = createLocalUpdate(channelId1), success = true) } - test("relay with real scid (channel update uses remote alias)") { f => - basicRelaytest(f)(relayPayloadScid = realScid1, lcu = createLocalUpdate(channelId1, channelUpdateScid_opt = Some(remoteAlias1)), success = true) + test("relay with real scid (channel update uses local alias)") { f => + basicRelayTest(f)(relayPayloadScid = realScid1, lcu = createLocalUpdate(channelId1, channelUpdateScid_opt = Some(localAlias1)), success = true) } test("relay with local alias (channel update uses real scid)") { f => - basicRelaytest(f)(relayPayloadScid = localAlias1, lcu = createLocalUpdate(channelId1), success = true) + basicRelayTest(f)(relayPayloadScid = localAlias1, lcu = createLocalUpdate(channelId1), success = true) } - test("relay with local alias (channel update uses remote alias)") { f => - // we use a random value to simulate a remote alias - basicRelaytest(f)(relayPayloadScid = localAlias1, lcu = createLocalUpdate(channelId1, channelUpdateScid_opt = Some(remoteAlias1)), success = true) + test("relay with local alias (channel update uses local alias)") { f => + basicRelayTest(f)(relayPayloadScid = localAlias1, lcu = createLocalUpdate(channelId1, channelUpdateScid_opt = Some(localAlias1)), success = true) } test("fail to relay with real scid when option_scid_alias is enabled (channel update uses real scid)") { f => - basicRelaytest(f)(relayPayloadScid = realScid1, lcu = createLocalUpdate(channelId1, optionScidAlias = true), success = false) + basicRelayTest(f)(relayPayloadScid = realScid1, lcu = createLocalUpdate(channelId1, optionScidAlias = true), success = false) } - test("fail to relay with real scid when option_scid_alias is enabled (channel update uses remote alias)") { f => - basicRelaytest(f)(relayPayloadScid = realScid1, lcu = createLocalUpdate(channelId1, optionScidAlias = true, channelUpdateScid_opt = Some(remoteAlias1)), success = false) + test("fail to relay with real scid when option_scid_alias is enabled (channel update uses local alias)") { f => + basicRelayTest(f)(relayPayloadScid = realScid1, lcu = createLocalUpdate(channelId1, optionScidAlias = true, channelUpdateScid_opt = Some(localAlias1)), success = false) } test("relay with local alias when option_scid_alias is enabled (channel update uses real scid)") { f => - basicRelaytest(f)(relayPayloadScid = localAlias1, lcu = createLocalUpdate(channelId1, optionScidAlias = true), success = true) + basicRelayTest(f)(relayPayloadScid = localAlias1, lcu = createLocalUpdate(channelId1, optionScidAlias = true), success = true) } - test("relay with local alias when option_scid_alias is enabled (channel update uses remote alias)") { f => - // we use a random value to simulate a remote alias - basicRelaytest(f)(relayPayloadScid = localAlias1, lcu = createLocalUpdate(channelId1, optionScidAlias = true, channelUpdateScid_opt = Some(remoteAlias1)), success = true) + test("relay with local alias when option_scid_alias is enabled (channel update uses local alias)") { f => + basicRelayTest(f)(relayPayloadScid = localAlias1, lcu = createLocalUpdate(channelId1, optionScidAlias = true, channelUpdateScid_opt = Some(localAlias1)), success = true) } test("relay with new real scid after reorg") { f => @@ -154,7 +152,6 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a expectFwdAdd(register, lcu2.channelId, outgoingAmount, outgoingExpiry) } - test("relay with onion tlv payload") { f => import f._ import fr.acinq.eclair.wire.protocol.OnionPaymentPayloadTlv._ @@ -535,9 +532,9 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a val channels1 = getOutgoingChannels(true) assert(channels1.size == 2) assert(channels1.head.channelUpdate == channelUpdate_ab) - assert(channels1.head.toChannelBalance == Relayer.ChannelBalance(a, channelUpdate_ab.shortChannelId, 0 msat, 300000 msat, isPublic = false, isEnabled = true)) + assert(channels1.head.toChannelBalance == Relayer.ChannelBalance(a, shortIds_ab, 0 msat, 300000 msat, isPublic = false, isEnabled = true)) assert(channels1.last.channelUpdate == channelUpdate_bc) - assert(channels1.last.toChannelBalance == Relayer.ChannelBalance(c, channelUpdate_bc.shortChannelId, 400000 msat, 0 msat, isPublic = false, isEnabled = true)) + assert(channels1.last.toChannelBalance == Relayer.ChannelBalance(c, shortIds_bc, 400000 msat, 0 msat, isPublic = false, isEnabled = true)) channelRelayer ! WrappedAvailableBalanceChanged(AvailableBalanceChanged(null, channelId_bc, shortIds_ab, makeCommitments(channelId_bc, 200000 msat, 500000 msat))) val channels2 = getOutgoingChannels(true) @@ -575,8 +572,6 @@ object ChannelRelayerSpec { val localAlias1: Alias = Alias(111000) val localAlias2: Alias = Alias(222000) - val remoteAlias1: ShortChannelId = ShortChannelId(111999) - val channelId1: ByteVector32 = randomBytes32() val channelId2: ByteVector32 = randomBytes32() diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRouterIntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRouterIntegrationSpec.scala index ebe90ec8f5..643e682d5c 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRouterIntegrationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRouterIntegrationSpec.scala @@ -10,6 +10,7 @@ import fr.acinq.eclair.router.Graph.GraphStructure.GraphEdge import fr.acinq.eclair.router.Router._ import fr.acinq.eclair.wire.protocol.{AnnouncementSignatures, ChannelUpdate, Shutdown} import fr.acinq.eclair.{BlockHeight, TestKitBaseClass} +import org.scalatest.OptionValues.convertOptionToValuable import org.scalatest.funsuite.FixtureAnyFunSuiteLike import org.scalatest.{Outcome, Tag} @@ -57,9 +58,9 @@ class ChannelRouterIntegrationSpec extends TestKitBaseClass with FixtureAnyFunSu // alice will only have a real scid if this is not a zeroconf channel assert(channels.alice.stateData.asInstanceOf[DATA_NORMAL].shortIds.real.toOption.isEmpty == f.testTags.contains(ChannelStateTestsTags.ZeroConf)) assert(channels.alice.stateData.asInstanceOf[DATA_NORMAL].shortIds.remoteAlias_opt.isDefined) - // alice uses bob's alias for her channel update - assert(privateChannel.update_1_opt.get.shortChannelId != privateChannel.shortIds.localAlias) - assert(privateChannel.update_1_opt.get.shortChannelId == channels.alice.stateData.asInstanceOf[DATA_NORMAL].shortIds.remoteAlias_opt.get) + // alice uses her alias for her internal channel update + val aliceInitialChannelUpdate = privateChannel.update_1_opt.value + assert(aliceInitialChannelUpdate.shortChannelId == privateChannel.shortIds.localAlias) // alice and bob send their channel_updates using remote alias when they go to NORMAL state val aliceChannelUpdate1 = channels.alice2bob.expectMsgType[ChannelUpdate] @@ -73,7 +74,7 @@ class ChannelRouterIntegrationSpec extends TestKitBaseClass with FixtureAnyFunSu // router processes bob's channel_update and now knows both channel updates awaitCond { - privateChannel.update_1_opt.contains(aliceChannelUpdate1) && privateChannel.update_2_opt.contains(bobChannelUpdate1) + privateChannel.update_1_opt.contains(aliceInitialChannelUpdate) && privateChannel.update_2_opt.contains(bobChannelUpdate1) } // there is nothing for the router to rebroadcast, channel is not announced @@ -81,7 +82,7 @@ class ChannelRouterIntegrationSpec extends TestKitBaseClass with FixtureAnyFunSu // router graph contains a single channel assert(router.stateData.graphWithBalances.graph.vertexSet() == Set(aliceNodeId, bobNodeId)) - assert(router.stateData.graphWithBalances.graph.edgeSet().toSet == Set(GraphEdge(aliceChannelUpdate1, privateChannel), GraphEdge(bobChannelUpdate1, privateChannel))) + assert(router.stateData.graphWithBalances.graph.edgeSet().toSet == Set(GraphEdge(aliceInitialChannelUpdate, privateChannel), GraphEdge(bobChannelUpdate1, privateChannel))) if (testTags.contains(ChannelStateTestsTags.ChannelsPublic)) { // this is a public channel @@ -161,7 +162,7 @@ class ChannelRouterIntegrationSpec extends TestKitBaseClass with FixtureAnyFunSu // router graph contains a single channel assert(router.stateData.graphWithBalances.graph.vertexSet() == Set(aliceNodeId, bobNodeId)) - assert(router.stateData.graphWithBalances.graph.edgeSet().toSet == Set(GraphEdge(aliceChannelUpdate1, privateChannel), GraphEdge(bobChannelUpdate1, privateChannel))) + assert(router.stateData.graphWithBalances.graph.edgeSet().toSet == Set(GraphEdge(aliceInitialChannelUpdate, privateChannel), GraphEdge(bobChannelUpdate1, privateChannel))) } // channel closes channels.alice ! CMD_CLOSE(TestProbe().ref, scriptPubKey = None, feerates = None) diff --git a/eclair-node/src/test/resources/api/channelbalances b/eclair-node/src/test/resources/api/channelbalances index 962b4c89aa..bbfc7619b3 100644 --- a/eclair-node/src/test/resources/api/channelbalances +++ b/eclair-node/src/test/resources/api/channelbalances @@ -1 +1 @@ -[{"remoteNodeId":"03af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d0","shortChannelId":"0x0x1","canSend":100000000,"canReceive":20000000,"isPublic":true,"isEnabled":true},{"remoteNodeId":"03af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d0","shortChannelId":"0x0x2","canSend":0,"canReceive":30000000,"isPublic":false,"isEnabled":false}] \ No newline at end of file +[{"remoteNodeId":"03af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d0","shortIds":{"real":{"status":"temporary","realScid":"0x0x1"},"localAlias":"0x2"},"canSend":100000000,"canReceive":20000000,"isPublic":true,"isEnabled":true},{"remoteNodeId":"03af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d0","shortIds":{"real":{"status":"final","realScid":"0x0x3"},"localAlias":"0x3","remoteAlias":"0x3"},"canSend":0,"canReceive":30000000,"isPublic":false,"isEnabled":false}] \ No newline at end of file diff --git a/eclair-node/src/test/resources/api/usablebalances b/eclair-node/src/test/resources/api/usablebalances index 5613aaf151..2876388234 100644 --- a/eclair-node/src/test/resources/api/usablebalances +++ b/eclair-node/src/test/resources/api/usablebalances @@ -1 +1 @@ -[{"remoteNodeId":"03af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d0","shortChannelId":"0x0x1","canSend":100000000,"canReceive":20000000,"isPublic":true,"isEnabled":true},{"remoteNodeId":"03af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d0","shortChannelId":"0x0x2","canSend":400000000,"canReceive":30000000,"isPublic":false,"isEnabled":true}] \ No newline at end of file +[{"remoteNodeId":"03af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d0","shortIds":{"real":{"status":"unknown"},"localAlias":"0x1"},"canSend":100000000,"canReceive":20000000,"isPublic":true,"isEnabled":true},{"remoteNodeId":"03af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d0","shortIds":{"real":{"status":"final","realScid":"0x0x2"},"localAlias":"0x3","remoteAlias":"0x4"},"canSend":400000000,"canReceive":30000000,"isPublic":false,"isEnabled":true}] \ No newline at end of file diff --git a/eclair-node/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala b/eclair-node/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala index 36f3d7c37f..dba2e0834a 100644 --- a/eclair-node/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala +++ b/eclair-node/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala @@ -205,8 +205,8 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM test("'usablebalances' returns expected balance json only for enabled channels") { val eclair = mock[Eclair] eclair.usableBalances()(any[Timeout]) returns Future.successful(List( - ChannelBalance(aliceNodeId, ShortChannelId(1), 100000000 msat, 20000000 msat, isPublic = true, isEnabled = true), - ChannelBalance(aliceNodeId, ShortChannelId(2), 400000000 msat, 30000000 msat, isPublic = false, isEnabled = true) + ChannelBalance(aliceNodeId, ShortIds(RealScidStatus.Unknown, Alias(1), None), 100000000 msat, 20000000 msat, isPublic = true, isEnabled = true), + ChannelBalance(aliceNodeId, ShortIds(RealScidStatus.Final(RealShortChannelId(2)), Alias(3), Some(Alias(4))), 400000000 msat, 30000000 msat, isPublic = false, isEnabled = true) )) val mockService = mockApi(eclair) @@ -225,8 +225,8 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM test("'channelbalances' returns expected balance json for all channels") { val eclair = mock[Eclair] eclair.channelBalances()(any[Timeout]) returns Future.successful(List( - ChannelBalance(aliceNodeId, ShortChannelId(1), 100000000 msat, 20000000 msat, isPublic = true, isEnabled = true), - ChannelBalance(aliceNodeId, ShortChannelId(2), 0 msat, 30000000 msat, isPublic = false, isEnabled = false) + ChannelBalance(aliceNodeId, ShortIds(RealScidStatus.Temporary(RealShortChannelId(1)), Alias(2), None), 100000000 msat, 20000000 msat, isPublic = true, isEnabled = true), + ChannelBalance(aliceNodeId, ShortIds(RealScidStatus.Final(RealShortChannelId(3)), Alias(3), Some(Alias(3))), 0 msat, 30000000 msat, isPublic = false, isEnabled = false) )) val mockService = mockApi(eclair) From 2790b2ff6c792bdc8d06cd427068b0f4de1beb35 Mon Sep 17 00:00:00 2001 From: Bastien Teinturier <31281497+t-bast@users.noreply.github.com> Date: Fri, 1 Jul 2022 15:13:57 +0200 Subject: [PATCH 109/121] Activate 0-conf based on per-peer feature override (#2329) We restrict 0-conf activation on a per-peer basis. When 0-conf is activated by both peers, we use it even if it wasn't part of the `channel_type`. --- docs/release-notes/eclair-vnext.md | 2 +- eclair-core/src/main/resources/reference.conf | 5 ++-- .../scala/fr/acinq/eclair/NodeParams.scala | 1 + .../eclair/channel/ChannelFeatures.scala | 14 +++++---- .../fr/acinq/eclair/channel/Helpers.scala | 4 +-- .../scala/fr/acinq/eclair/StartupSpec.scala | 29 +++++++++++++++++++ .../eclair/channel/ChannelFeaturesSpec.scala | 28 ++++++++++-------- .../a/WaitForAcceptChannelStateSpec.scala | 24 +++++++-------- .../a/WaitForOpenChannelStateSpec.scala | 14 ++++----- .../integration/ChannelIntegrationSpec.scala | 18 +++++++++--- .../basic/ZeroConfActivationSpec.scala | 18 ++++++++++++ 11 files changed, 112 insertions(+), 45 deletions(-) diff --git a/docs/release-notes/eclair-vnext.md b/docs/release-notes/eclair-vnext.md index cf6427c795..61ad309eec 100644 --- a/docs/release-notes/eclair-vnext.md +++ b/docs/release-notes/eclair-vnext.md @@ -21,7 +21,7 @@ This feature is enabled by default, but your peer has to support it too, and it Zeroconf channels make it possible to use a newly created channel before the funding tx is confirmed on the blockchain. -:warning: Zeroconf requires the fundee to trust the funder. For this reason it is disabled by default, and you should +:warning: Zeroconf requires the fundee to trust the funder. For this reason it is disabled by default, and you can only enable it on a peer-by-peer basis. ##### Enabling through features diff --git a/eclair-core/src/main/resources/reference.conf b/eclair-core/src/main/resources/reference.conf index d850ffbc4f..a8eb0af543 100644 --- a/eclair-core/src/main/resources/reference.conf +++ b/eclair-core/src/main/resources/reference.conf @@ -63,8 +63,9 @@ eclair { option_channel_type = optional option_scid_alias = optional option_payment_metadata = optional - // By enabling option_zeroconf, you will be trusting your peers as fundee. You will lose funds if they double spend - // their funding tx. + // By enabling option_zeroconf, you will be trusting your peer as fundee. You will lose funds if they double spend + // their funding tx. Eclair does not let you activate this feature by default, you have to activate it for every + // node that you trust using override-init-features (see below). option_zeroconf = disabled keysend = disabled trampoline_payment_prototype = disabled diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala index 4eea0c7141..9eb32a91b5 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala @@ -302,6 +302,7 @@ object NodeParams extends Logging { val pluginMessageParams = pluginParams.collect { case p: CustomFeaturePlugin => p } val features = Features.fromConfiguration(config.getConfig("features")) validateFeatures(features) + require(!features.hasFeature(Features.ZeroConf), s"${Features.ZeroConf.rfcName} cannot be enabled for all peers: you have to use override-init-features to enable it on a per-peer basis") require(pluginMessageParams.forall(_.feature.mandatory > 128), "Plugin mandatory feature bit is too low, must be > 128") require(pluginMessageParams.forall(_.feature.mandatory % 2 == 0), "Plugin mandatory feature bit is odd, must be even") diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelFeatures.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelFeatures.scala index 7bd79bb98e..7eacd3e5ab 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelFeatures.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelFeatures.scala @@ -17,7 +17,6 @@ package fr.acinq.eclair.channel import fr.acinq.eclair.transactions.Transactions.{CommitmentFormat, DefaultCommitmentFormat, UnsafeLegacyAnchorOutputsCommitmentFormat, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat} -import fr.acinq.eclair.{Feature, FeatureSupport, Features, InitFeature} import fr.acinq.eclair.{ChannelTypeFeature, FeatureSupport, Features, InitFeature, PermanentChannelFeature} /** @@ -57,10 +56,15 @@ object ChannelFeatures { def apply(features: PermanentChannelFeature*): ChannelFeatures = ChannelFeatures(Set.from(features)) /** Enrich the channel type with other permanent features that will be applied to the channel. */ - def apply(channelType: SupportedChannelType, localFeatures: Features[InitFeature], remoteFeatures: Features[InitFeature]): ChannelFeatures = { + def apply(channelType: SupportedChannelType, localFeatures: Features[InitFeature], remoteFeatures: Features[InitFeature], announceChannel: Boolean): ChannelFeatures = { val additionalPermanentFeatures = Features.knownFeatures.collect { - case _: ChannelTypeFeature => None // channel-type features are negotiated in the channel-type, we ignore them in the init - case f: PermanentChannelFeature if Features.canUseFeature(localFeatures, remoteFeatures, f) => Some(f) // we only consider permanent channel features + // If we both support 0-conf or scid_alias, we use it even if it wasn't in the channel-type. + case Features.ScidAlias if Features.canUseFeature(localFeatures, remoteFeatures, Features.ScidAlias) && !announceChannel => Some(Features.ScidAlias) + case Features.ZeroConf if Features.canUseFeature(localFeatures, remoteFeatures, Features.ZeroConf) => Some(Features.ZeroConf) + // Other channel-type features are negotiated in the channel-type, we ignore their value from the init message. + case _: ChannelTypeFeature => None + // We add all other permanent channel features that aren't negotiated as part of the channel-type. + case f: PermanentChannelFeature if Features.canUseFeature(localFeatures, remoteFeatures, f) => Some(f) }.flatten val allPermanentFeatures = channelType.features.toSeq ++ additionalPermanentFeatures ChannelFeatures(allPermanentFeatures: _*) @@ -139,7 +143,7 @@ object ChannelTypes { /** Pick the channel type based on local and remote feature bits, as defined by the spec. */ def defaultFromFeatures(localFeatures: Features[InitFeature], remoteFeatures: Features[InitFeature], announceChannel: Boolean): SupportedChannelType = { - def canUse(feature: InitFeature) = Features.canUseFeature(localFeatures, remoteFeatures, feature) + def canUse(feature: InitFeature): Boolean = Features.canUseFeature(localFeatures, remoteFeatures, feature) if (canUse(Features.AnchorOutputsZeroFeeHtlcTx)) { AnchorOutputsZeroFeeHtlcTx(scidAlias = canUse(Features.ScidAlias) && !announceChannel, zeroConf = canUse(Features.ZeroConf)) // alias feature is incompatible with public channel diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala index 8cdb82327a..450ca19ff2 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala @@ -131,7 +131,7 @@ object Helpers { val reserveToFundingRatio = open.channelReserveSatoshis.toLong.toDouble / Math.max(open.fundingSatoshis.toLong, 1) if (reserveToFundingRatio > nodeParams.channelConf.maxReserveToFundingRatio) return Left(ChannelReserveTooHigh(open.temporaryChannelId, open.channelReserveSatoshis, reserveToFundingRatio, nodeParams.channelConf.maxReserveToFundingRatio)) - val channelFeatures = ChannelFeatures(channelType, localFeatures, remoteFeatures) + val channelFeatures = ChannelFeatures(channelType, localFeatures, remoteFeatures, open.channelFlags.announceChannel) extractShutdownScript(open.temporaryChannelId, localFeatures, remoteFeatures, open.upfrontShutdownScript_opt).map(script_opt => (channelFeatures, script_opt)) } @@ -179,7 +179,7 @@ object Helpers { val reserveToFundingRatio = accept.channelReserveSatoshis.toLong.toDouble / Math.max(open.fundingSatoshis.toLong, 1) if (reserveToFundingRatio > nodeParams.channelConf.maxReserveToFundingRatio) return Left(ChannelReserveTooHigh(open.temporaryChannelId, accept.channelReserveSatoshis, reserveToFundingRatio, nodeParams.channelConf.maxReserveToFundingRatio)) - val channelFeatures = ChannelFeatures(channelType, localFeatures, remoteFeatures) + val channelFeatures = ChannelFeatures(channelType, localFeatures, remoteFeatures, open.channelFlags.announceChannel) extractShutdownScript(accept.temporaryChannelId, localFeatures, remoteFeatures, accept.upfrontShutdownScript_opt).map(script_opt => (channelFeatures, script_opt)) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/StartupSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/StartupSpec.scala index 4092099c6e..c0779a845e 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/StartupSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/StartupSpec.scala @@ -255,6 +255,35 @@ class StartupSpec extends AnyFunSuite { assertThrows[IllegalArgumentException](makeNodeParamsWithDefaults(perNodeConf.withFallback(defaultConf))) } + test("disallow enabling zero-conf for every peer") { + val invalidConf = ConfigFactory.parseString( + """ + | features { + | option_zeroconf = optional + | } + """.stripMargin + ) + assertThrows[IllegalArgumentException](makeNodeParamsWithDefaults(invalidConf.withFallback(defaultConf))) + + val perNodeConf = ConfigFactory.parseString( + """ + | override-init-features = [ + | { + | nodeid = "031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f" + | features { + | option_zeroconf = optional + | } + | } + | ] + """.stripMargin + ) + + val nodeParams = makeNodeParamsWithDefaults(perNodeConf.withFallback(defaultConf)) + assert(!nodeParams.features.hasFeature(Features.ZeroConf)) + val perNodeFeatures = nodeParams.initFeaturesFor(PublicKey(ByteVector.fromValidHex("031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f"))) + assert(perNodeFeatures.hasFeature(Features.ZeroConf)) + } + test("override feerate mismatch tolerance") { val perNodeConf = ConfigFactory.parseString( """ diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/ChannelFeaturesSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/ChannelFeaturesSpec.scala index 280ddbf077..cf82814d26 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/ChannelFeaturesSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/ChannelFeaturesSpec.scala @@ -122,20 +122,24 @@ class ChannelFeaturesSpec extends TestKitBaseClass with AnyFunSuiteLike with Cha } test("enrich channel type with optional permanent channel features") { - case class TestCase(channelType: SupportedChannelType, localFeatures: Features[InitFeature], remoteFeatures: Features[InitFeature], expected: Set[Feature]) + case class TestCase(channelType: SupportedChannelType, localFeatures: Features[InitFeature], remoteFeatures: Features[InitFeature], announceChannel: Boolean, expected: Set[Feature]) val testCases = Seq( - TestCase(ChannelTypes.Standard, Features(Wumbo -> Optional), Features.empty, Set.empty), - TestCase(ChannelTypes.Standard, Features(Wumbo -> Optional), Features(Wumbo -> Optional), Set(Wumbo)), - TestCase(ChannelTypes.Standard, Features(Wumbo -> Mandatory), Features(Wumbo -> Optional), Set(Wumbo)), - TestCase(ChannelTypes.StaticRemoteKey, Features(Wumbo -> Optional), Features.empty, Set(StaticRemoteKey)), - TestCase(ChannelTypes.StaticRemoteKey, Features(Wumbo -> Optional), Features(Wumbo -> Optional), Set(StaticRemoteKey, Wumbo)), - TestCase(ChannelTypes.AnchorOutputs, Features.empty, Features(Wumbo -> Optional), Set(StaticRemoteKey, AnchorOutputs)), - TestCase(ChannelTypes.AnchorOutputs, Features(Wumbo -> Optional), Features(Wumbo -> Mandatory), Set(StaticRemoteKey, AnchorOutputs, Wumbo)), - TestCase(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false), Features.empty, Features(Wumbo -> Optional), Set(StaticRemoteKey, AnchorOutputsZeroFeeHtlcTx)), - TestCase(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false), Features(Wumbo -> Optional), Features(Wumbo -> Mandatory), Set(StaticRemoteKey, AnchorOutputsZeroFeeHtlcTx, Wumbo)), - TestCase(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false), Features(DualFunding -> Optional, Wumbo -> Optional), Features(DualFunding -> Optional, Wumbo -> Optional), Set(StaticRemoteKey, AnchorOutputsZeroFeeHtlcTx, Wumbo, DualFunding)), + TestCase(ChannelTypes.Standard, Features(Wumbo -> Optional), Features.empty, announceChannel = true, Set.empty), + TestCase(ChannelTypes.Standard, Features(Wumbo -> Optional), Features(Wumbo -> Optional), announceChannel = true, Set(Wumbo)), + TestCase(ChannelTypes.Standard, Features(Wumbo -> Mandatory), Features(Wumbo -> Optional), announceChannel = true, Set(Wumbo)), + TestCase(ChannelTypes.StaticRemoteKey, Features(Wumbo -> Optional), Features.empty, announceChannel = true, Set(StaticRemoteKey)), + TestCase(ChannelTypes.StaticRemoteKey, Features(Wumbo -> Optional), Features(Wumbo -> Optional), announceChannel = true, Set(StaticRemoteKey, Wumbo)), + TestCase(ChannelTypes.AnchorOutputs, Features.empty, Features(Wumbo -> Optional), announceChannel = true, Set(StaticRemoteKey, AnchorOutputs)), + TestCase(ChannelTypes.AnchorOutputs, Features(Wumbo -> Optional), Features(Wumbo -> Mandatory), announceChannel = true, Set(StaticRemoteKey, AnchorOutputs, Wumbo)), + TestCase(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false), Features.empty, Features(Wumbo -> Optional), announceChannel = true, Set(StaticRemoteKey, AnchorOutputsZeroFeeHtlcTx)), + TestCase(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false), Features(ScidAlias -> Optional, ZeroConf -> Optional), Features(ScidAlias -> Optional, ZeroConf -> Optional), announceChannel = true, Set(StaticRemoteKey, AnchorOutputsZeroFeeHtlcTx, ZeroConf)), + TestCase(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false), Features(ScidAlias -> Optional, ZeroConf -> Optional), Features(ScidAlias -> Optional, ZeroConf -> Optional), announceChannel = false, Set(StaticRemoteKey, AnchorOutputsZeroFeeHtlcTx, ScidAlias, ZeroConf)), + TestCase(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = true, zeroConf = false), Features.empty, Features(Wumbo -> Optional), announceChannel = false, Set(StaticRemoteKey, AnchorOutputsZeroFeeHtlcTx, ScidAlias)), + TestCase(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = true, zeroConf = true), Features.empty, Features(Wumbo -> Optional), announceChannel = false, Set(StaticRemoteKey, AnchorOutputsZeroFeeHtlcTx, ScidAlias, ZeroConf)), + TestCase(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false), Features(Wumbo -> Optional), Features(Wumbo -> Mandatory), announceChannel = true, Set(StaticRemoteKey, AnchorOutputsZeroFeeHtlcTx, Wumbo)), + TestCase(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false), Features(DualFunding -> Optional, Wumbo -> Optional), Features(DualFunding -> Optional, Wumbo -> Optional), announceChannel = true, Set(StaticRemoteKey, AnchorOutputsZeroFeeHtlcTx, Wumbo, DualFunding)), ) - testCases.foreach(t => assert(ChannelFeatures(t.channelType, t.localFeatures, t.remoteFeatures).features == t.expected, s"channelType=${t.channelType} localFeatures=${t.localFeatures} remoteFeatures=${t.remoteFeatures}")) + testCases.foreach(t => assert(ChannelFeatures(t.channelType, t.localFeatures, t.remoteFeatures, t.announceChannel).features == t.expected, s"channelType=${t.channelType} localFeatures=${t.localFeatures} remoteFeatures=${t.remoteFeatures}")) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForAcceptChannelStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForAcceptChannelStateSpec.scala index 3ed6f1429b..aaac7e188a 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForAcceptChannelStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForAcceptChannelStateSpec.scala @@ -77,8 +77,8 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS import f._ val accept = bob2alice.expectMsgType[AcceptChannel] // Since https://github.com/lightningnetwork/lightning-rfc/pull/714 we must include an empty upfront_shutdown_script. - assert(accept.upfrontShutdownScript_opt == Some(ByteVector.empty)) - assert(accept.channelType_opt == Some(ChannelTypes.Standard)) + assert(accept.upfrontShutdownScript_opt.contains(ByteVector.empty)) + assert(accept.channelType_opt.contains(ChannelTypes.Standard)) bob2alice.forward(alice) awaitCond(alice.stateName == WAIT_FOR_FUNDING_INTERNAL) aliceOrigin.expectNoMessage() @@ -87,7 +87,7 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS test("recv AcceptChannel (anchor outputs)", Tag(ChannelStateTestsTags.AnchorOutputs)) { f => import f._ val accept = bob2alice.expectMsgType[AcceptChannel] - assert(accept.channelType_opt == Some(ChannelTypes.AnchorOutputs)) + assert(accept.channelType_opt.contains(ChannelTypes.AnchorOutputs)) bob2alice.forward(alice) awaitCond(alice.stateName == WAIT_FOR_FUNDING_INTERNAL) assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_INTERNAL].channelFeatures.channelType == ChannelTypes.AnchorOutputs) @@ -97,7 +97,7 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS test("recv AcceptChannel (anchor outputs zero fee htlc txs)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => import f._ val accept = bob2alice.expectMsgType[AcceptChannel] - assert(accept.channelType_opt == Some(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false))) + assert(accept.channelType_opt.contains(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false))) bob2alice.forward(alice) awaitCond(alice.stateName == WAIT_FOR_FUNDING_INTERNAL) assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_INTERNAL].channelFeatures.channelType == ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)) @@ -107,7 +107,7 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS test("recv AcceptChannel (anchor outputs zero fee htlc txs and scid alias)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs), Tag(ChannelStateTestsTags.ScidAlias)) { f => import f._ val accept = bob2alice.expectMsgType[AcceptChannel] - assert(accept.channelType_opt == Some(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = true, zeroConf = false))) + assert(accept.channelType_opt.contains(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = true, zeroConf = false))) bob2alice.forward(alice) awaitCond(alice.stateName == WAIT_FOR_FUNDING_INTERNAL) assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_INTERNAL].channelFeatures.channelType == ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = true, zeroConf = false)) @@ -117,7 +117,7 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS test("recv AcceptChannel (channel type not set)", Tag(ChannelStateTestsTags.AnchorOutputs)) { f => import f._ val accept = bob2alice.expectMsgType[AcceptChannel] - assert(accept.channelType_opt == Some(ChannelTypes.AnchorOutputs)) + assert(accept.channelType_opt.contains(ChannelTypes.AnchorOutputs)) // Alice explicitly asked for an anchor output channel. Bob doesn't support explicit channel type negotiation but // they both activated anchor outputs so it is the default choice anyway. bob2alice.forward(alice, accept.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScriptTlv(ByteVector.empty)))) @@ -129,7 +129,7 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS test("recv AcceptChannel (channel type not set but feature bit set)", Tag(ChannelStateTestsTags.ChannelType), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => import f._ val accept = bob2alice.expectMsgType[AcceptChannel] - assert(accept.channelType_opt == Some(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false))) + assert(accept.channelType_opt.contains(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false))) bob2alice.forward(alice, accept.copy(tlvStream = TlvStream.empty)) alice2bob.expectMsg(Error(accept.temporaryChannelId, "option_channel_type was negotiated but channel_type is missing")) awaitCond(alice.stateName == CLOSED) @@ -140,7 +140,7 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS import f._ val accept = bob2alice.expectMsgType[AcceptChannel] // Alice asked for a standard channel whereas they both support anchor outputs. - assert(accept.channelType_opt == Some(ChannelTypes.Standard)) + assert(accept.channelType_opt.contains(ChannelTypes.Standard)) bob2alice.forward(alice, accept) awaitCond(alice.stateName == WAIT_FOR_FUNDING_INTERNAL) assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_INTERNAL].channelFeatures.channelType == ChannelTypes.Standard) @@ -150,7 +150,7 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS test("recv AcceptChannel (non-default channel type not set)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs), Tag("standard-channel-type")) { f => import f._ val accept = bob2alice.expectMsgType[AcceptChannel] - assert(accept.channelType_opt == Some(ChannelTypes.Standard)) + assert(accept.channelType_opt.contains(ChannelTypes.Standard)) // Alice asked for a standard channel whereas they both support anchor outputs. Bob doesn't support explicit channel // type negotiation so Alice needs to abort because the channel types won't match. bob2alice.forward(alice, accept.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScriptTlv(ByteVector.empty)))) @@ -170,10 +170,10 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.anchorOutputsFeeratePerKw, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, Init(bobParams.initFeatures), ChannelFlags.Private, channelConfig, ChannelTypes.AnchorOutputs) bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, bobParams, bob2alice.ref, Init(bobParams.initFeatures), channelConfig, ChannelTypes.AnchorOutputs) val open = alice2bob.expectMsgType[OpenChannel] - assert(open.channelType_opt == Some(ChannelTypes.AnchorOutputs)) + assert(open.channelType_opt.contains(ChannelTypes.AnchorOutputs)) alice2bob.forward(bob, open) val accept = bob2alice.expectMsgType[AcceptChannel] - assert(accept.channelType_opt == Some(ChannelTypes.AnchorOutputs)) + assert(accept.channelType_opt.contains(ChannelTypes.AnchorOutputs)) bob2alice.forward(alice, accept) awaitCond(alice.stateName == WAIT_FOR_FUNDING_INTERNAL) assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_INTERNAL].channelFeatures.channelType == ChannelTypes.AnchorOutputs) @@ -183,7 +183,7 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS test("recv AcceptChannel (invalid channel type)") { f => import f._ val accept = bob2alice.expectMsgType[AcceptChannel] - assert(accept.channelType_opt == Some(ChannelTypes.Standard)) + assert(accept.channelType_opt.contains(ChannelTypes.Standard)) val invalidAccept = accept.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScriptTlv(ByteVector.empty), ChannelTlv.ChannelTypeTlv(ChannelTypes.AnchorOutputs))) bob2alice.forward(alice, invalidAccept) alice2bob.expectMsg(Error(accept.temporaryChannelId, "invalid channel_type=anchor_outputs, expected channel_type=standard")) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForOpenChannelStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForOpenChannelStateSpec.scala index 13ac83a34c..f9692f40f2 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForOpenChannelStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForOpenChannelStateSpec.scala @@ -24,7 +24,7 @@ import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel.fsm.Channel import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsTags} import fr.acinq.eclair.wire.protocol.{AcceptChannel, ChannelTlv, Error, Init, OpenChannel, TlvStream} -import fr.acinq.eclair.{CltvExpiryDelta, Features, MilliSatoshiLong, TestConstants, TestKitBaseClass, ToMilliSatoshiConversion} +import fr.acinq.eclair.{CltvExpiryDelta, MilliSatoshiLong, TestConstants, TestKitBaseClass, ToMilliSatoshiConversion} import org.scalatest.funsuite.FixtureAnyFunSuiteLike import org.scalatest.{Outcome, Tag} import scodec.bits.ByteVector @@ -68,9 +68,9 @@ class WaitForOpenChannelStateSpec extends TestKitBaseClass with FixtureAnyFunSui import f._ val open = alice2bob.expectMsgType[OpenChannel] // Since https://github.com/lightningnetwork/lightning-rfc/pull/714 we must include an empty upfront_shutdown_script. - assert(open.upfrontShutdownScript_opt == Some(ByteVector.empty)) + assert(open.upfrontShutdownScript_opt.contains(ByteVector.empty)) // We always send a channel type, even for standard channels. - assert(open.channelType_opt == Some(ChannelTypes.Standard)) + assert(open.channelType_opt.contains(ChannelTypes.Standard)) alice2bob.forward(bob) awaitCond(bob.stateName == WAIT_FOR_FUNDING_CREATED) assert(bob.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CREATED].channelFeatures.channelType == ChannelTypes.Standard) @@ -79,7 +79,7 @@ class WaitForOpenChannelStateSpec extends TestKitBaseClass with FixtureAnyFunSui test("recv OpenChannel (anchor outputs)", Tag(ChannelStateTestsTags.AnchorOutputs)) { f => import f._ val open = alice2bob.expectMsgType[OpenChannel] - assert(open.channelType_opt == Some(ChannelTypes.AnchorOutputs)) + assert(open.channelType_opt.contains(ChannelTypes.AnchorOutputs)) alice2bob.forward(bob) awaitCond(bob.stateName == WAIT_FOR_FUNDING_CREATED) assert(bob.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CREATED].channelFeatures.channelType == ChannelTypes.AnchorOutputs) @@ -88,7 +88,7 @@ class WaitForOpenChannelStateSpec extends TestKitBaseClass with FixtureAnyFunSui test("recv OpenChannel (anchor outputs zero fee htlc txs)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => import f._ val open = alice2bob.expectMsgType[OpenChannel] - assert(open.channelType_opt == Some(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false))) + assert(open.channelType_opt.contains(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false))) alice2bob.forward(bob) awaitCond(bob.stateName == WAIT_FOR_FUNDING_CREATED) assert(bob.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CREATED].channelFeatures.channelType == ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false)) @@ -97,7 +97,7 @@ class WaitForOpenChannelStateSpec extends TestKitBaseClass with FixtureAnyFunSui test("recv OpenChannel (anchor outputs zero fee htlc txs and scid alias)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs), Tag(ChannelStateTestsTags.ScidAlias)) { f => import f._ val open = alice2bob.expectMsgType[OpenChannel] - assert(open.channelType_opt == Some(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = true, zeroConf = false))) + assert(open.channelType_opt.contains(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = true, zeroConf = false))) alice2bob.forward(bob) awaitCond(bob.stateName == WAIT_FOR_FUNDING_CREATED) assert(bob.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CREATED].channelFeatures.channelType == ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = true, zeroConf = false)) @@ -106,7 +106,7 @@ class WaitForOpenChannelStateSpec extends TestKitBaseClass with FixtureAnyFunSui test("recv OpenChannel (non-default channel type)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs), Tag("standard-channel-type")) { f => import f._ val open = alice2bob.expectMsgType[OpenChannel] - assert(open.channelType_opt == Some(ChannelTypes.Standard)) + assert(open.channelType_opt.contains(ChannelTypes.Standard)) alice2bob.forward(bob) awaitCond(bob.stateName == WAIT_FOR_FUNDING_CREATED) assert(bob.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CREATED].channelFeatures.channelType == ChannelTypes.Standard) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/ChannelIntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/ChannelIntegrationSpec.scala index f757378879..7f81b3ff9a 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/ChannelIntegrationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/ChannelIntegrationSpec.scala @@ -128,15 +128,25 @@ abstract class ChannelIntegrationSpec extends IntegrationSpec { val sender = TestProbe() sender.send(bitcoincli, BitcoinReq("getnewaddress")) val JString(minerAddress) = sender.expectMsgType[JValue] - // we create and announce a channel between C and F; we use push_msat to ensure both nodes have an output in the commit tx - connect(nodes("C"), nodes("F"), 5000000 sat, 500000000 msat) - generateBlocks(6, Some(minerAddress)) - awaitAnnouncements(2) // we subscribe to channel state transitions val stateListenerC = TestProbe() val stateListenerF = TestProbe() nodes("C").system.eventStream.subscribe(stateListenerC.ref, classOf[ChannelStateChanged]) nodes("F").system.eventStream.subscribe(stateListenerF.ref, classOf[ChannelStateChanged]) + // we create and announce a channel between C and F; we use push_msat to ensure both nodes have an output in the commit tx + connect(nodes("C"), nodes("F"), 5000000 sat, 500000000 msat) + awaitCond(stateListenerC.expectMsgType[ChannelStateChanged](max = 60 seconds).currentState == WAIT_FOR_FUNDING_CONFIRMED, max = 30 seconds) + awaitCond(stateListenerF.expectMsgType[ChannelStateChanged](max = 60 seconds).currentState == WAIT_FOR_FUNDING_CONFIRMED, max = 30 seconds) + generateBlocks(1, Some(minerAddress)) + // the funder sends its channel_ready after only one block + awaitCond(stateListenerC.expectMsgType[ChannelStateChanged](max = 60 seconds).currentState == WAIT_FOR_CHANNEL_READY, max = 30 seconds) + generateBlocks(2, Some(minerAddress)) + // the fundee sends its channel_ready after 3 blocks + awaitCond(stateListenerF.expectMsgType[ChannelStateChanged](max = 60 seconds).currentState == NORMAL, max = 30 seconds) + awaitCond(stateListenerC.expectMsgType[ChannelStateChanged](max = 60 seconds).currentState == NORMAL, max = 30 seconds) + // we generate more blocks for the funding tx to be deeply buried and the channel to be announced + generateBlocks(3, Some(minerAddress)) + awaitAnnouncements(2) // first we make sure we are in sync with current blockchain height val currentBlockHeight = getBlockHeight() awaitCond(getBlockHeight() == currentBlockHeight, max = 20 seconds, interval = 1 second) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/ZeroConfActivationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/ZeroConfActivationSpec.scala index 23313dffe9..3d304e4941 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/ZeroConfActivationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/ZeroConfActivationSpec.scala @@ -19,6 +19,7 @@ class ZeroConfActivationSpec extends FixtureSpec with IntegrationPatience { type FixtureParam = TwoNodesFixture + val ZeroConfAlice = "zero_conf_alice" val ZeroConfBob = "zero_conf_bob" import fr.acinq.eclair.integration.basic.fixtures.MinimalNodeFixture._ @@ -26,6 +27,8 @@ class ZeroConfActivationSpec 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")) + .modify(_.features.activated).using(_ - ZeroConf) // we will enable those features on demand + .modify(_.features.activated).usingIf(testData.tags.contains(ZeroConfAlice))(_ + (ZeroConf -> Optional)) val bobParams = nodeParamsFor("bob", ByteVector32(hex"7620226fec887b0b2ebe76492e5a3fd3eb0e47cd3773263f6a81b59a704dc492")) .modify(_.features.activated).using(_ - ZeroConf) // we will enable those features on demand .modify(_.features.activated).usingIf(testData.tags.contains(ZeroConfBob))(_ + (ZeroConf -> Optional)) @@ -76,4 +79,19 @@ class ZeroConfActivationSpec extends FixtureSpec with IntegrationPatience { assert(getChannelData(alice, channelId).asInstanceOf[PersistentChannelData].commitments.channelFeatures.hasFeature(ZeroConf)) assert(getChannelData(bob, channelId).asInstanceOf[PersistentChannelData].commitments.channelFeatures.hasFeature(ZeroConf)) } + + test("open a channel alice-bob (zero-conf enabled on alice and bob, but not requested via channel type by alice)", Tag(ZeroConfAlice), Tag(ZeroConfBob)) { f => + import f._ + + assert(alice.nodeParams.features.activated.contains(ZeroConf)) + assert(bob.nodeParams.features.activated.contains(ZeroConf)) + + connect(alice, bob) + val channelType = AnchorOutputsZeroFeeHtlcTx(scidAlias = false, zeroConf = false) + val channelId = openChannel(alice, bob, 100_000 sat, channelType_opt = Some(channelType)).channelId + + assert(getChannelData(alice, channelId).asInstanceOf[PersistentChannelData].commitments.channelFeatures.hasFeature(ZeroConf)) + assert(getChannelData(bob, channelId).asInstanceOf[PersistentChannelData].commitments.channelFeatures.hasFeature(ZeroConf)) + } + } \ No newline at end of file From 2461ef08cb7a688e1e48f9e37682b704f99f8ecb Mon Sep 17 00:00:00 2001 From: Pierre-Marie Padiou Date: Fri, 1 Jul 2022 15:36:30 +0200 Subject: [PATCH 110/121] Call `resolve()` on configuration (#2339) From lightbend's config doc: > A given Config must be resolved before using it to retrieve config values, but ideally should be resolved one time for your entire stack of fallbacks (see withFallback) https://github.com/lightbend/config/blob/4458ea947a7a2a668bb811a122455f1f05975172/config/src/main/java/com/typesafe/config/Config.java#L204-L206 --- eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala | 1 + 1 file changed, 1 insertion(+) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala index 9eb32a91b5..0907a9f46b 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala @@ -114,6 +114,7 @@ object NodeParams extends Logging { ConfigFactory.systemProperties() .withFallback(ConfigFactory.parseFile(new File(datadir, "eclair.conf"))) .withFallback(ConfigFactory.load()) + .resolve() private def readSeedFromFile(seedPath: File): ByteVector = { logger.info(s"use seed file: ${seedPath.getCanonicalPath}") From 3b97e446aaeb50d56409a1fd5a09df7c8ed51117 Mon Sep 17 00:00:00 2001 From: Bastien Teinturier <31281497+t-bast@users.noreply.github.com> Date: Fri, 1 Jul 2022 16:04:41 +0200 Subject: [PATCH 111/121] Allow disabling no-htlc commitment fee-bump (#2246) When a channel force-close without any pending htlcs, funds are not at risk. We want to eventually get our main output back, but if we are not in a rush we can save on fees by never spending the anchors. This is disabled by default as there is a potential risk: if the commit tx doesn't confirm and the feerate rises, the commit tx may eventually be below the network's min-relay-fee and won't confirm (at least until package relay is available). --- docs/release-notes/eclair-vnext.md | 13 ++++ eclair-core/src/main/resources/reference.conf | 22 +++++-- .../scala/fr/acinq/eclair/NodeParams.scala | 1 + .../eclair/blockchain/fee/FeeEstimator.scala | 2 +- .../fr/acinq/eclair/channel/Helpers.scala | 66 +++++++++++-------- .../eclair/channel/fsm/ErrorHandlers.scala | 8 +-- .../scala/fr/acinq/eclair/TestConstants.scala | 2 + .../blockchain/fee/FeeEstimatorSpec.scala | 6 +- .../eclair/channel/CommitmentsSpec.scala | 1 + .../ChannelStateTestsHelperMethods.scala | 4 ++ .../channel/states/e/NormalStateSpec.scala | 44 ++++++++++--- 11 files changed, 117 insertions(+), 52 deletions(-) diff --git a/docs/release-notes/eclair-vnext.md b/docs/release-notes/eclair-vnext.md index 61ad309eec..36e9456b3e 100644 --- a/docs/release-notes/eclair-vnext.md +++ b/docs/release-notes/eclair-vnext.md @@ -130,6 +130,19 @@ change the search interval with two new settings: - `eclair.purge-expired-invoices.enabled = true - `eclair.purge-expired-invoices.interval = 24 hours` +#### Skip anchor CPFP for empty commitment + +When using anchor outputs and a channel force-closes without HTLCs in the commitment transaction, funds cannot be stolen by your counterparty. +In that case eclair can skip spending the anchor output to save on-chain fees, even if the transaction doesn't confirm. +This can be activated by setting the following value in your `eclair.conf`: + +```conf +eclair.on-chain-fees.spend-anchor-without-htlcs = false +``` + +This is disabled by default, because there is still a risk of losing funds until bitcoin adds support for package relay. +If the mempool becomes congested and the feerate is too low, the commitment transaction may never reach miners' mempools because it's below the minimum relay feerate. + ## Verifying signatures You will need `gpg` and our release signing key 7A73FE77DE2C4027. Note that you can get it: diff --git a/eclair-core/src/main/resources/reference.conf b/eclair-core/src/main/resources/reference.conf index a8eb0af543..add6c41f59 100644 --- a/eclair-core/src/main/resources/reference.conf +++ b/eclair-core/src/main/resources/reference.conf @@ -167,12 +167,18 @@ eclair { // number of blocks to target when computing fees for each transaction type target-blocks { - funding = 6 // target for the funding transaction - commitment = 2 // target for the commitment transaction (used in force-close scenario) *do not change this unless you know what you are doing* - commitment-without-htlcs = 12 // target for the commitment transaction when we have no htlcs to claim (used in force-close scenario) *do not change this unless you know what you are doing* - mutual-close = 12 // target for the mutual close transaction - claim-main = 12 // target for the claim main transaction (tx that spends main channel output back to wallet) - safe-utxos-threshold = 10 // when our utxos count is below this threshold, we will use more aggressive confirmation targets in force-close scenarios + // target for the funding transaction + funding = 6 + // target for the commitment transaction (used in force-close scenario) *do not change this unless you know what you are doing* + commitment = 2 + // target for the commitment transaction when we have no htlcs to claim (used in force-close scenario) *do not change this unless you know what you are doing* + commitment-without-htlcs = 12 + // target for the mutual close transaction + mutual-close = 12 + // target for the claim main transaction (tx that spends main channel output back to wallet) + claim-main = 12 + // when our utxos count is below this threshold, we will use more aggressive confirmation targets in force-close scenarios + safe-utxos-threshold = 10 } feerate-tolerance { @@ -207,6 +213,10 @@ eclair { # } ] + // if false, the commitment transaction will not be fee-bumped when we have no htlcs to claim (used in force-close scenario) + // *do not change this unless you know what you are doing* + spend-anchor-without-htlcs = true + close-on-offline-feerate-mismatch = true // do not change this unless you know what you are doing // the channel initiator will send an UpdateFee message if the difference between current commitment fee and actual diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala index 0907a9f46b..a3e7791e53 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala @@ -440,6 +440,7 @@ object NodeParams extends Logging { onChainFeeConf = OnChainFeeConf( feeTargets = feeTargets, feeEstimator = feeEstimator, + spendAnchorWithoutHtlcs = config.getBoolean("on-chain-fees.spend-anchor-without-htlcs"), closeOnOfflineMismatch = config.getBoolean("on-chain-fees.close-on-offline-feerate-mismatch"), updateFeeMinDiffRatio = config.getDouble("on-chain-fees.update-fee-min-diff-ratio"), defaultFeerateTolerance = FeerateTolerance( diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/FeeEstimator.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/FeeEstimator.scala index a01b2fcf75..ec1d33dbfd 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/FeeEstimator.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/FeeEstimator.scala @@ -56,7 +56,7 @@ case class FeerateTolerance(ratioLow: Double, ratioHigh: Double, anchorOutputMax } } -case class OnChainFeeConf(feeTargets: FeeTargets, feeEstimator: FeeEstimator, closeOnOfflineMismatch: Boolean, updateFeeMinDiffRatio: Double, private val defaultFeerateTolerance: FeerateTolerance, private val perNodeFeerateTolerance: Map[PublicKey, FeerateTolerance]) { +case class OnChainFeeConf(feeTargets: FeeTargets, feeEstimator: FeeEstimator, spendAnchorWithoutHtlcs: Boolean, closeOnOfflineMismatch: Boolean, updateFeeMinDiffRatio: Double, private val defaultFeerateTolerance: FeerateTolerance, private val perNodeFeerateTolerance: Map[PublicKey, FeerateTolerance]) { def feerateToleranceFor(nodeId: PublicKey): FeerateTolerance = perNodeFeerateTolerance.getOrElse(nodeId, defaultFeerateTolerance) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala index 450ca19ff2..a6f0b3b2a7 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala @@ -22,7 +22,7 @@ import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey, sha256} import fr.acinq.bitcoin.scalacompat.Script._ import fr.acinq.bitcoin.scalacompat._ import fr.acinq.eclair._ -import fr.acinq.eclair.blockchain.fee.{FeeEstimator, FeeTargets, FeeratePerKw} +import fr.acinq.eclair.blockchain.fee.{FeeEstimator, FeeTargets, FeeratePerKw, OnChainFeeConf} import fr.acinq.eclair.channel.fsm.Channel import fr.acinq.eclair.channel.fsm.Channel.{ChannelConf, REFRESH_CHANNEL_UPDATE_INTERVAL} import fr.acinq.eclair.crypto.Generators @@ -668,7 +668,7 @@ object Helpers { * @param commitments our commitment data, which include payment preimages * @return a list of transactions (one per output of the commit tx that we can claim) */ - def claimCommitTxOutputs(keyManager: ChannelKeyManager, commitments: Commitments, tx: Transaction, currentBlockHeight: BlockHeight, feeEstimator: FeeEstimator, feeTargets: FeeTargets)(implicit log: LoggingAdapter): LocalCommitPublished = { + def claimCommitTxOutputs(keyManager: ChannelKeyManager, commitments: Commitments, tx: Transaction, currentBlockHeight: BlockHeight, onChainFeeConf: OnChainFeeConf)(implicit log: LoggingAdapter): LocalCommitPublished = { import commitments._ require(localCommit.commitTxAndRemoteSig.commitTx.tx.txid == tx.txid, "txid mismatch, provided tx is not the current local commit tx") val channelKeyPath = keyManager.keyPath(localParams, channelConfig) @@ -676,7 +676,7 @@ object Helpers { val localRevocationPubkey = Generators.revocationPubKey(remoteParams.revocationBasepoint, localPerCommitmentPoint) val localDelayedPubkey = Generators.derivePubKey(keyManager.delayedPaymentPoint(channelKeyPath).publicKey, localPerCommitmentPoint) val localFundingPubKey = keyManager.fundingPublicKey(commitments.localParams.fundingKeyPath).publicKey - val feeratePerKwDelayed = feeEstimator.getFeeratePerKw(feeTargets.claimMainBlockTarget) + val feeratePerKwDelayed = onChainFeeConf.feeEstimator.getFeeratePerKw(onChainFeeConf.feeTargets.claimMainBlockTarget) // first we will claim our main output as soon as the delay is over val mainDelayedTx = withTxGenerationLog("local-main-delayed") { @@ -688,16 +688,21 @@ object Helpers { val htlcTxs: Map[OutPoint, Option[HtlcTx]] = claimHtlcOutputs(keyManager, commitments) - // If we don't have pending HTLCs, we don't have funds at risk, so we can aim for a slower confirmation. - val confirmCommitBefore = htlcTxs.values.flatten.map(htlcTx => htlcTx.confirmBefore).minOption.getOrElse(currentBlockHeight + feeTargets.commitmentWithoutHtlcsBlockTarget) - val claimAnchorTxs: List[ClaimAnchorOutputTx] = List( - withTxGenerationLog("local-anchor") { - Transactions.makeClaimLocalAnchorOutputTx(tx, localFundingPubKey, confirmCommitBefore) - }, - withTxGenerationLog("remote-anchor") { - Transactions.makeClaimRemoteAnchorOutputTx(tx, commitments.remoteParams.fundingPubKey) - } - ).flatten + val spendAnchors = htlcTxs.nonEmpty || onChainFeeConf.spendAnchorWithoutHtlcs + val claimAnchorTxs: List[ClaimAnchorOutputTx] = if (spendAnchors) { + // If we don't have pending HTLCs, we don't have funds at risk, so we can aim for a slower confirmation. + val confirmCommitBefore = htlcTxs.values.flatten.map(htlcTx => htlcTx.confirmBefore).minOption.getOrElse(currentBlockHeight + onChainFeeConf.feeTargets.commitmentWithoutHtlcsBlockTarget) + List( + withTxGenerationLog("local-anchor") { + Transactions.makeClaimLocalAnchorOutputTx(tx, localFundingPubKey, confirmCommitBefore) + }, + withTxGenerationLog("remote-anchor") { + Transactions.makeClaimRemoteAnchorOutputTx(tx, commitments.remoteParams.fundingPubKey) + } + ).flatten + } else { + Nil + } LocalCommitPublished( commitTx = tx, @@ -787,26 +792,31 @@ object Helpers { * @param tx the remote commitment transaction that has just been published * @return a list of transactions (one per output of the commit tx that we can claim) */ - def claimCommitTxOutputs(keyManager: ChannelKeyManager, commitments: Commitments, remoteCommit: RemoteCommit, tx: Transaction, currentBlockHeight: BlockHeight, feeEstimator: FeeEstimator, feeTargets: FeeTargets)(implicit log: LoggingAdapter): RemoteCommitPublished = { + def claimCommitTxOutputs(keyManager: ChannelKeyManager, commitments: Commitments, remoteCommit: RemoteCommit, tx: Transaction, currentBlockHeight: BlockHeight, onChainFeeConf: OnChainFeeConf)(implicit log: LoggingAdapter): RemoteCommitPublished = { require(remoteCommit.txid == tx.txid, "txid mismatch, provided tx is not the current remote commit tx") - val htlcTxs: Map[OutPoint, Option[ClaimHtlcTx]] = claimHtlcOutputs(keyManager, commitments, remoteCommit, feeEstimator) - - // If we don't have pending HTLCs, we don't have funds at risk, so we can aim for a slower confirmation. - val confirmCommitBefore = htlcTxs.values.flatten.map(htlcTx => htlcTx.confirmBefore).minOption.getOrElse(currentBlockHeight + feeTargets.commitmentWithoutHtlcsBlockTarget) - val localFundingPubkey = keyManager.fundingPublicKey(commitments.localParams.fundingKeyPath).publicKey - val claimAnchorTxs: List[ClaimAnchorOutputTx] = List( - withTxGenerationLog("local-anchor") { - Transactions.makeClaimLocalAnchorOutputTx(tx, localFundingPubkey, confirmCommitBefore) - }, - withTxGenerationLog("remote-anchor") { - Transactions.makeClaimRemoteAnchorOutputTx(tx, commitments.remoteParams.fundingPubKey) - } - ).flatten + val htlcTxs: Map[OutPoint, Option[ClaimHtlcTx]] = claimHtlcOutputs(keyManager, commitments, remoteCommit, onChainFeeConf.feeEstimator) + + val spendAnchors = htlcTxs.nonEmpty || onChainFeeConf.spendAnchorWithoutHtlcs + val claimAnchorTxs: List[ClaimAnchorOutputTx] = if (spendAnchors) { + // If we don't have pending HTLCs, we don't have funds at risk, so we can aim for a slower confirmation. + val confirmCommitBefore = htlcTxs.values.flatten.map(htlcTx => htlcTx.confirmBefore).minOption.getOrElse(currentBlockHeight + onChainFeeConf.feeTargets.commitmentWithoutHtlcsBlockTarget) + val localFundingPubkey = keyManager.fundingPublicKey(commitments.localParams.fundingKeyPath).publicKey + List( + withTxGenerationLog("local-anchor") { + Transactions.makeClaimLocalAnchorOutputTx(tx, localFundingPubkey, confirmCommitBefore) + }, + withTxGenerationLog("remote-anchor") { + Transactions.makeClaimRemoteAnchorOutputTx(tx, commitments.remoteParams.fundingPubKey) + } + ).flatten + } else { + Nil + } RemoteCommitPublished( commitTx = tx, - claimMainOutputTx = claimMainOutput(keyManager, commitments, remoteCommit.remotePerCommitmentPoint, tx, feeEstimator, feeTargets), + claimMainOutputTx = claimMainOutput(keyManager, commitments, remoteCommit.remotePerCommitmentPoint, tx, onChainFeeConf.feeEstimator, onChainFeeConf.feeTargets), claimHtlcTxs = htlcTxs, claimAnchorTxs = claimAnchorTxs, irrevocablySpent = Map.empty diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ErrorHandlers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ErrorHandlers.scala index 096f3bd610..c027bc106e 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ErrorHandlers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ErrorHandlers.scala @@ -177,7 +177,7 @@ trait ErrorHandlers extends CommonHandlers { stay() } else { val commitTx = d.commitments.fullySignedLocalCommitTx(keyManager).tx - val localCommitPublished = Closing.LocalClose.claimCommitTxOutputs(keyManager, d.commitments, commitTx, nodeParams.currentBlockHeight, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) + val localCommitPublished = Closing.LocalClose.claimCommitTxOutputs(keyManager, d.commitments, commitTx, nodeParams.currentBlockHeight, nodeParams.onChainFeeConf) val nextData = d match { case closing: DATA_CLOSING => closing.copy(localCommitPublished = Some(localCommitPublished)) case negotiating: DATA_NEGOTIATING => DATA_CLOSING(d.commitments, fundingTx = None, waitingSince = nodeParams.currentBlockHeight, negotiating.closingTxProposed.flatten.map(_.unsignedTx), localCommitPublished = Some(localCommitPublished)) @@ -222,7 +222,7 @@ trait ErrorHandlers extends CommonHandlers { require(commitTx.txid == d.commitments.remoteCommit.txid, "txid mismatch") context.system.eventStream.publish(TransactionPublished(d.channelId, remoteNodeId, commitTx, Closing.commitTxFee(d.commitments.commitInput, commitTx, d.commitments.localParams.isInitiator), "remote-commit")) - val remoteCommitPublished = Closing.RemoteClose.claimCommitTxOutputs(keyManager, d.commitments, d.commitments.remoteCommit, commitTx, nodeParams.currentBlockHeight, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) + val remoteCommitPublished = Closing.RemoteClose.claimCommitTxOutputs(keyManager, d.commitments, d.commitments.remoteCommit, commitTx, nodeParams.currentBlockHeight, nodeParams.onChainFeeConf) val nextData = d match { case closing: DATA_CLOSING => closing.copy(remoteCommitPublished = Some(remoteCommitPublished)) case negotiating: DATA_NEGOTIATING => DATA_CLOSING(d.commitments, fundingTx = None, waitingSince = nodeParams.currentBlockHeight, negotiating.closingTxProposed.flatten.map(_.unsignedTx), remoteCommitPublished = Some(remoteCommitPublished)) @@ -254,7 +254,7 @@ trait ErrorHandlers extends CommonHandlers { require(commitTx.txid == remoteCommit.txid, "txid mismatch") context.system.eventStream.publish(TransactionPublished(d.channelId, remoteNodeId, commitTx, Closing.commitTxFee(d.commitments.commitInput, commitTx, d.commitments.localParams.isInitiator), "next-remote-commit")) - val remoteCommitPublished = Closing.RemoteClose.claimCommitTxOutputs(keyManager, d.commitments, remoteCommit, commitTx, nodeParams.currentBlockHeight, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) + val remoteCommitPublished = Closing.RemoteClose.claimCommitTxOutputs(keyManager, d.commitments, remoteCommit, commitTx, nodeParams.currentBlockHeight, nodeParams.onChainFeeConf) val nextData = d match { case closing: DATA_CLOSING => closing.copy(nextRemoteCommitPublished = Some(remoteCommitPublished)) case negotiating: DATA_NEGOTIATING => DATA_CLOSING(d.commitments, fundingTx = None, waitingSince = nodeParams.currentBlockHeight, negotiating.closingTxProposed.flatten.map(_.unsignedTx), nextRemoteCommitPublished = Some(remoteCommitPublished)) @@ -332,7 +332,7 @@ trait ErrorHandlers extends CommonHandlers { // let's try to spend our current local tx val commitTx = d.commitments.fullySignedLocalCommitTx(keyManager).tx - val localCommitPublished = Closing.LocalClose.claimCommitTxOutputs(keyManager, d.commitments, commitTx, nodeParams.currentBlockHeight, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) + val localCommitPublished = Closing.LocalClose.claimCommitTxOutputs(keyManager, d.commitments, commitTx, nodeParams.currentBlockHeight, nodeParams.onChainFeeConf) goto(ERR_INFORMATION_LEAK) calling doPublish(localCommitPublished, d.commitments) sending error } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala b/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala index a0429479a7..e7366e416e 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala @@ -125,6 +125,7 @@ object TestConstants { onChainFeeConf = OnChainFeeConf( feeTargets = FeeTargets(6, 2, 36, 12, 18, 0), feeEstimator = new TestFeeEstimator, + spendAnchorWithoutHtlcs = true, closeOnOfflineMismatch = true, updateFeeMinDiffRatio = 0.1, defaultFeerateTolerance = FeerateTolerance(0.5, 8.0, anchorOutputsFeeratePerKw, DustTolerance(25_000 sat, closeOnUpdateFeeOverflow = true)), @@ -266,6 +267,7 @@ object TestConstants { onChainFeeConf = OnChainFeeConf( feeTargets = FeeTargets(6, 2, 36, 12, 18, 0), feeEstimator = new TestFeeEstimator, + spendAnchorWithoutHtlcs = true, closeOnOfflineMismatch = true, updateFeeMinDiffRatio = 0.1, defaultFeerateTolerance = FeerateTolerance(0.75, 1.5, anchorOutputsFeeratePerKw, DustTolerance(30_000 sat, closeOnUpdateFeeOverflow = true)), diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/FeeEstimatorSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/FeeEstimatorSpec.scala index 4211416a7b..7543f199ba 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/FeeEstimatorSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/FeeEstimatorSpec.scala @@ -27,7 +27,7 @@ class FeeEstimatorSpec extends AnyFunSuite { val defaultFeerateTolerance = FeerateTolerance(0.5, 2.0, FeeratePerKw(2500 sat), DustTolerance(15000 sat, closeOnUpdateFeeOverflow = false)) test("should update fee when diff ratio exceeded") { - val feeConf = OnChainFeeConf(FeeTargets(1, 1, 1, 1, 1, 1), new TestFeeEstimator(), closeOnOfflineMismatch = true, updateFeeMinDiffRatio = 0.1, defaultFeerateTolerance, Map.empty) + val feeConf = OnChainFeeConf(FeeTargets(1, 1, 1, 1, 1, 1), new TestFeeEstimator(), spendAnchorWithoutHtlcs = true, closeOnOfflineMismatch = true, updateFeeMinDiffRatio = 0.1, defaultFeerateTolerance, Map.empty) assert(!feeConf.shouldUpdateFee(FeeratePerKw(1000 sat), FeeratePerKw(1000 sat))) assert(!feeConf.shouldUpdateFee(FeeratePerKw(1000 sat), FeeratePerKw(900 sat))) assert(!feeConf.shouldUpdateFee(FeeratePerKw(1000 sat), FeeratePerKw(1100 sat))) @@ -38,7 +38,7 @@ class FeeEstimatorSpec extends AnyFunSuite { test("get commitment feerate") { val feeEstimator = new TestFeeEstimator() val channelType = ChannelTypes.Standard - val feeConf = OnChainFeeConf(FeeTargets(1, 2, 6, 1, 1, 1), feeEstimator, closeOnOfflineMismatch = true, updateFeeMinDiffRatio = 0.1, defaultFeerateTolerance, Map.empty) + val feeConf = OnChainFeeConf(FeeTargets(1, 2, 6, 1, 1, 1), feeEstimator, spendAnchorWithoutHtlcs = true, closeOnOfflineMismatch = true, updateFeeMinDiffRatio = 0.1, defaultFeerateTolerance, Map.empty) feeEstimator.setFeerate(FeeratesPerKw.single(FeeratePerKw(10000 sat)).copy(blocks_2 = FeeratePerKw(5000 sat))) assert(feeConf.getCommitmentFeerate(randomKey().publicKey, channelType, 100000 sat, None) == FeeratePerKw(5000 sat)) @@ -53,7 +53,7 @@ class FeeEstimatorSpec extends AnyFunSuite { val defaultMaxCommitFeerate = defaultFeerateTolerance.anchorOutputMaxCommitFeerate val overrideNodeId = randomKey().publicKey val overrideMaxCommitFeerate = defaultMaxCommitFeerate * 2 - val feeConf = OnChainFeeConf(FeeTargets(1, 2, 6, 1, 1, 1), feeEstimator, closeOnOfflineMismatch = true, updateFeeMinDiffRatio = 0.1, defaultFeerateTolerance, Map(overrideNodeId -> defaultFeerateTolerance.copy(anchorOutputMaxCommitFeerate = overrideMaxCommitFeerate))) + val feeConf = OnChainFeeConf(FeeTargets(1, 2, 6, 1, 1, 1), feeEstimator, spendAnchorWithoutHtlcs = true, closeOnOfflineMismatch = true, updateFeeMinDiffRatio = 0.1, defaultFeerateTolerance, Map(overrideNodeId -> defaultFeerateTolerance.copy(anchorOutputMaxCommitFeerate = overrideMaxCommitFeerate))) feeEstimator.setFeerate(FeeratesPerKw.single(FeeratePerKw(10000 sat)).copy(blocks_2 = defaultMaxCommitFeerate / 2, mempoolMinFee = FeeratePerKw(250 sat))) assert(feeConf.getCommitmentFeerate(defaultNodeId, ChannelTypes.AnchorOutputs, 100000 sat, None) == defaultMaxCommitFeerate / 2) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/CommitmentsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/CommitmentsSpec.scala index d18261043f..50b09aad80 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/CommitmentsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/CommitmentsSpec.scala @@ -44,6 +44,7 @@ class CommitmentsSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val feeConfNoMismatch = OnChainFeeConf( FeeTargets(6, 2, 12, 2, 6, 1), new TestFeeEstimator(), + spendAnchorWithoutHtlcs = true, closeOnOfflineMismatch = false, 1.0, FeerateTolerance(0.00001, 100000.0, TestConstants.anchorOutputsFeeratePerKw, DustTolerance(100000 sat, closeOnUpdateFeeOverflow = false)), diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala index 6386271f94..6a195691b0 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala @@ -77,6 +77,8 @@ object ChannelStateTestsTags { val ZeroConf = "zeroconf" /** If set, channels will use option_scid_alias. */ val ScidAlias = "scid_alias" + /** If set, we won't spend anchors to fee-bump commitments without htlcs (no funds at risk). */ + val DontSpendAnchorWithoutHtlcs = "dont-spend-anchor-without-htlcs" } trait ChannelStateTestsBase extends Assertions with Eventually { @@ -135,11 +137,13 @@ trait ChannelStateTestsBase extends Assertions with Eventually { .modify(_.channelConf.dustLimit).setToIf(tags.contains(ChannelStateTestsTags.HighDustLimitDifferenceBobAlice))(1000 sat) .modify(_.channelConf.maxRemoteDustLimit).setToIf(tags.contains(ChannelStateTestsTags.HighDustLimitDifferenceAliceBob))(10000 sat) .modify(_.channelConf.maxRemoteDustLimit).setToIf(tags.contains(ChannelStateTestsTags.HighDustLimitDifferenceBobAlice))(10000 sat) + .modify(_.onChainFeeConf.spendAnchorWithoutHtlcs).setToIf(tags.contains(ChannelStateTestsTags.DontSpendAnchorWithoutHtlcs))(false) val finalNodeParamsB = nodeParamsB .modify(_.channelConf.dustLimit).setToIf(tags.contains(ChannelStateTestsTags.HighDustLimitDifferenceAliceBob))(1000 sat) .modify(_.channelConf.dustLimit).setToIf(tags.contains(ChannelStateTestsTags.HighDustLimitDifferenceBobAlice))(5000 sat) .modify(_.channelConf.maxRemoteDustLimit).setToIf(tags.contains(ChannelStateTestsTags.HighDustLimitDifferenceAliceBob))(10000 sat) .modify(_.channelConf.maxRemoteDustLimit).setToIf(tags.contains(ChannelStateTestsTags.HighDustLimitDifferenceBobAlice))(10000 sat) + .modify(_.onChainFeeConf.spendAnchorWithoutHtlcs).setToIf(tags.contains(ChannelStateTestsTags.DontSpendAnchorWithoutHtlcs))(false) val alice: TestFSMRef[ChannelState, ChannelData, Channel] = { implicit val system: ActorSystem = systemA TestFSMRef(new Channel(finalNodeParamsA, wallet, finalNodeParamsB.nodeId, alice2blockchain.ref, alice2relayer.ref, FakeTxPublisherFactory(alice2blockchain), origin_opt = Some(aliceOrigin.ref)), alicePeer.ref) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala index 5ec60cd941..45e66ded52 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala @@ -3300,7 +3300,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice2blockchain.expectNoMessage(1 second) } - test("recv Error (anchor outputs zero fee htlc txs)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + def testErrorAnchorOutputsWithHtlcs(f: FixtureParam): Unit = { import f._ val (ra1, htlca1) = addHtlc(250000000 msat, CltvExpiryDelta(20), alice, bob, alice2bob, bob2alice) @@ -3343,7 +3343,16 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice2blockchain.expectNoMessage(1 second) } - test("recv Error (anchor outputs zero fee htlc txs without htlcs)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + test("recv Error (anchor outputs zero fee htlc txs)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + testErrorAnchorOutputsWithHtlcs(f) + } + + test("recv Error (anchor outputs zero fee htlc txs, fee-bumping for commit txs without htlcs disabled)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs), Tag(ChannelStateTestsTags.DontSpendAnchorWithoutHtlcs)) { f => + // We should ignore the disable flag since there are htlcs in the commitment (funds at risk). + testErrorAnchorOutputsWithHtlcs(f) + } + + def testErrorAnchorOutputsWithoutHtlcs(f: FixtureParam, commitFeeBumpDisabled: Boolean): Unit = { import f._ // an error occurs and alice publishes her commit tx @@ -3355,14 +3364,29 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val currentBlockHeight = alice.underlyingActor.nodeParams.currentBlockHeight val blockTargets = alice.underlyingActor.nodeParams.onChainFeeConf.feeTargets - val localAnchor = alice2blockchain.expectMsgType[PublishReplaceableTx] - // When there are no pending HTLCs, there is no rush to get the commit tx confirmed - assert(localAnchor.txInfo.confirmBefore == currentBlockHeight + blockTargets.commitmentWithoutHtlcsBlockTarget) - val claimMain = alice2blockchain.expectMsgType[PublishFinalTx] - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == aliceCommitTx.txid) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == claimMain.tx.txid) - assert(alice2blockchain.expectMsgType[WatchOutputSpent].outputIndex == localAnchor.input.index) - alice2blockchain.expectNoMessage(1 second) + if (commitFeeBumpDisabled) { + val claimMain = alice2blockchain.expectMsgType[PublishFinalTx] + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === aliceCommitTx.txid) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === claimMain.tx.txid) + alice2blockchain.expectNoMessage(1 second) + } else { + val localAnchor = alice2blockchain.expectMsgType[PublishReplaceableTx] + // When there are no pending HTLCs, there is no rush to get the commit tx confirmed + assert(localAnchor.txInfo.confirmBefore === currentBlockHeight + blockTargets.commitmentWithoutHtlcsBlockTarget) + val claimMain = alice2blockchain.expectMsgType[PublishFinalTx] + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === aliceCommitTx.txid) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === claimMain.tx.txid) + assert(alice2blockchain.expectMsgType[WatchOutputSpent].outputIndex === localAnchor.input.index) + alice2blockchain.expectNoMessage(1 second) + } + } + + test("recv Error (anchor outputs zero fee htlc txs without htlcs)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + testErrorAnchorOutputsWithoutHtlcs(f, commitFeeBumpDisabled = false) + } + + test("recv Error (anchor outputs zero fee htlc txs without htlcs, fee-bumping for commit txs without htlcs disabled)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs), Tag(ChannelStateTestsTags.DontSpendAnchorWithoutHtlcs)) { f => + testErrorAnchorOutputsWithoutHtlcs(f, commitFeeBumpDisabled = true) } test("recv Error (nothing at stake)", Tag(ChannelStateTestsTags.NoPushMsat)) { f => From 276340d1b3722c01b5aab196ac8e45ff09e90e07 Mon Sep 17 00:00:00 2001 From: Goutam Verma <66783850+GoutamVerma@users.noreply.github.com> Date: Fri, 1 Jul 2022 21:02:46 +0530 Subject: [PATCH 112/121] Add host metrics grafana dashboard (#2334) Add a grafana dashboard to monitor host metrics (CPU, memory, network usage, etc). --- docs/Monitoring.md | 5 + .../grafana-dashboard/host-metrics.json | 993 ++++++++++++++++++ 2 files changed, 998 insertions(+) create mode 100644 monitoring/grafana-dashboard/host-metrics.json diff --git a/docs/Monitoring.md b/docs/Monitoring.md index cb56239057..086cc5c6de 100644 --- a/docs/Monitoring.md +++ b/docs/Monitoring.md @@ -86,6 +86,11 @@ scrape_configs: - targets: [''] ``` +Eclair provides many [Grafana](https://grafana.com/) dashboards to help you monitor your lightning node, which you can find in the `monitoring` folder of this repository. Follow the [Grafana documentation](https://grafana.com/docs/grafana/latest/dashboards/export-import/#import-dashboard) to import these dashboards and create new ones if necessary. + +Note: do not forget to add `Prometheus` as a data source in grafana (see [grafana documentation](https://prometheus.io/docs/visualization/grafana/#creating-a-prometheus-data-source) for more details) + + ## Example metrics Apart from akka and system metrics, eclair generates a lot of lightning metrics. The following diff --git a/monitoring/grafana-dashboard/host-metrics.json b/monitoring/grafana-dashboard/host-metrics.json new file mode 100644 index 0000000000..d48379d44b --- /dev/null +++ b/monitoring/grafana-dashboard/host-metrics.json @@ -0,0 +1,993 @@ +{ + "__inputs": [ + { + "name": "DS_PROMETHEUS", + "label": "Prometheus", + "description": "", + "type": "datasource", + "pluginId": "prometheus", + "pluginName": "Prometheus" + } + ], + "__elements": [], + "__requires": [ + { + "type": "panel", + "id": "gauge", + "name": "Gauge", + "version": "" + }, + { + "type": "grafana", + "id": "grafana", + "name": "Grafana", + "version": "9.0.1" + }, + { + "type": "panel", + "id": "heatmap", + "name": "Heatmap", + "version": "" + }, + { + "type": "panel", + "id": "piechart", + "name": "Pie chart", + "version": "" + }, + { + "type": "datasource", + "id": "prometheus", + "name": "Prometheus", + "version": "1.0.0" + }, + { + "type": "panel", + "id": "timeseries", + "name": "Time series", + "version": "" + } + ], + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "target": { + "limit": 100, + "matchAny": false, + "tags": [], + "type": "dashboard" + }, + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": null, + "links": [], + "liveNow": false, + "panels": [ + { + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 36, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 19, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineStyle": { + "fill": "solid" + }, + "lineWidth": 2, + "pointSize": 2, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 90 + } + ] + }, + "unit": "none" + }, + "overrides": [ { + "matcher": { + "id": "byFrameRefID", + "options": "A" + }, + "properties": [ + { + "id": "displayName", + "value": "host_load_average" + } + ] + } + ] + }, + "gridPos": { + "h": 9, + "w": 12, + "x": 0, + "y": 1 + }, + "id": 34, + "options": { + "legend": { + "calcs": [ + "firstNotNull", + "lastNotNull", + "min", + "max" + ], + "displayMode": "table", + "placement": "bottom" + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "builder", + "expr": "max(rate(host_load_average[1m]))", + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "Load Average", + "type": "timeseries" + } + ], + "title": "Load Average", + "type": "row" + }, + { + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 1 + }, + "id": 32, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 19, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineStyle": { + "fill": "solid" + }, + "lineWidth": 2, + "pointSize": 2, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 90 + } + ] + }, + "unit": "percent" + }, + "overrides": [ { + "matcher": { + "id": "byName", + "options": "Value" + }, + "properties": [ + { + "id": "displayName", + "value": "host_swap_usage" + } + ] + } + ] + }, + "gridPos": { + "h": 9, + "w": 12, + "x": 0, + "y": 2 + }, + "id": 30, + "options": { + "legend": { + "calcs": [ + "firstNotNull", + "lastNotNull", + "min", + "max" + ], + "displayMode": "table", + "placement": "bottom" + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "builder", + "expr": "rate(host_swap_usage[1m])", + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "Swap Usage", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 19, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineStyle": { + "fill": "solid" + }, + "lineWidth": 2, + "pointSize": 2, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 90 + } + ] + }, + "unit": "percent" + }, + "overrides": [ { + "matcher": { + "id": "byName", + "options": "Value" + }, + "properties": [ + { + "id": "displayName", + "value": "host_memory_usage" + } + ] + } + ] + }, + "gridPos": { + "h": 9, + "w": 12, + "x": 12, + "y": 2 + }, + "id": 24, + "options": { + "legend": { + "calcs": [ + "firstNotNull", + "lastNotNull", + "min", + "max" + ], + "displayMode": "table", + "placement": "bottom" + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "builder", + "expr": "rate(host_memory_usage[1m])", + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "Memory Usage", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + } + }, + "mappings": [], + "unit": "percent" + }, + "overrides": [ + { + "__systemRef": "hideSeriesFrom", + "matcher": { + "id": "byNames", + "options": { + "mode": "exclude", + "names": [ + "{__name__=\"host_storage_mount_space_total_bytes\", component=\"host\", job=\"eclair\", mount=\"/\"}", + "{__name__=\"host_storage_mount_space_free_bytes\", component=\"host\", job=\"eclair\", mount=\"/\"}" + ], + "prefix": "All except:", + "readOnly": true + } + }, + "properties": [ + { + "id": "custom.hideFrom", + "value": { + "legend": false, + "tooltip": false, + "viz": true + } + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 11 + }, + "id": 26, + "options": { + "displayLabels": [ + "percent" + ], + "legend": { + "displayMode": "table", + "placement": "bottom", + "values": [ + "percent" + ] + }, + "pieType": "donut", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "builder", + "expr": "host_storage_mount_space_total_bytes", + "legendFormat": "__auto", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "builder", + "expr": "host_storage_mount_space_free_bytes", + "hide": false, + "legendFormat": "__auto", + "range": true, + "refId": "B" + } + ], + "title": "Host Storage", + "type": "piechart" + } + ], + "title": "Memory and File System Usage", + "type": "row" + }, + { + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 2 + }, + "id": 22, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": -1, + "drawStyle": "line", + "fillOpacity": 19, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineStyle": { + "fill": "solid" + }, + "lineWidth": 2, + "pointSize": 2, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 90 + } + ] + }, + "unit": "none" + }, + "overrides": [ + { + "matcher": { + "id": "byFrameRefID", + "options": "A" + }, + "properties": [ + { + "id": "displayName", + "value": "host_network_data_read_bytes" + } + ] + }, + { + "matcher": { + "id": "byFrameRefID", + "options": "B" + }, + "properties": [ + { + "id": "displayName", + "value": "host_network_data_write_bytes" + } + ] + }] + }, + "gridPos": { + "h": 9, + "w": 12, + "x": 0, + "y": 3 + }, + "id": 20, + "options": { + "legend": { + "calcs": [ + "firstNotNull", + "lastNotNull", + "min", + "max" + ], + "displayMode": "table", + "placement": "bottom" + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "builder", + "expr": "rate(host_network_data_read_bytes_total[1m])", + "legendFormat": "__auto", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "builder", + "expr": "rate(host_network_data_write_bytes_total[1m])", + "hide": false, + "legendFormat": "__auto", + "range": true, + "refId": "B" + } + ], + "title": "Host network data read/write bytes", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": -1, + "drawStyle": "line", + "fillOpacity": 19, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineStyle": { + "fill": "solid" + }, + "lineWidth": 2, + "pointSize": 2, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 90 + } + ] + }, + "unit": "none" + }, + "overrides": [ + { + "matcher": { + "id": "byFrameRefID", + "options": "A" + }, + "properties": [ + { + "id": "displayName", + "value": "host_network_packets_read" + } + ] + }, + { + "matcher": { + "id": "byFrameRefID", + "options": "B" + }, + "properties": [ + { + "id": "displayName", + "value": "host_network_packets_write" + } + ] + }] + }, + "gridPos": { + "h": 9, + "w": 12, + "x": 12, + "y": 3 + }, + "id": 18, + "options": { + "legend": { + "calcs": [ + "firstNotNull", + "lastNotNull", + "min", + "max" + ], + "displayMode": "table", + "placement": "bottom" + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "builder", + "expr": "rate(host_network_packets_read_total[1m])", + "legendFormat": "__auto", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "builder", + "expr": "rate(host_network_packets_write_total[1m])", + "hide": false, + "legendFormat": "__auto", + "range": true, + "refId": "B" + } + ], + "title": "Host network packet read/write", + "type": "timeseries" + } + ], + "title": "Network Usage", + "type": "row" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 12 + }, + "id": 16, + "panels": [], + "title": "CPU Usage", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 11, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "smooth", + "lineStyle": { + "fill": "solid" + }, + "lineWidth": 1, + "pointSize": 4, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "percent" + }, + "overrides": [ + { + "matcher": { + "id": "byFrameRefID", + "options": "A" + }, + "properties": [ + { + "id": "displayName", + "value": "host_cpu_system_usage" + } + ] + }, + { + "matcher": { + "id": "byFrameRefID", + "options": "B" + }, + "properties": [ + { + "id": "displayName", + "value": "host_cpu_user_usage" + } + ] + }] + }, + "gridPos": { + "h": 9, + "w": 12, + "x": 0, + "y": 13 + }, + "id": 7, + "options": { + "legend": { + "calcs": [ + "max", + "min", + "firstNotNull", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom" + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "builder", + "expr": "rate(host_cpu_usage_sum{mode=\"system\"}[1m])", + "legendFormat": "__auto", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "builder", + "expr": "rate(host_cpu_usage_sum{mode=\"user\"}[1m])", + "hide": false, + "legendFormat": "__auto", + "range": true, + "refId": "B" + } + ], + "title": "Overall", + "type": "timeseries" + } + ], + "refresh": false, + "schemaVersion": 36, + "style": "dark", + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-12h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "Eclair Host Metrics", + "uid": "FDdGZp37k", + "version": 3, + "weekStart": "" +} \ No newline at end of file From 31964620bdcb519f2f4c9f8ebd2aeb381815f38f Mon Sep 17 00:00:00 2001 From: Pierre-Marie Padiou Date: Fri, 1 Jul 2022 17:46:54 +0200 Subject: [PATCH 113/121] Proper json serializer for `BlockHeight` (#2340) It currently serializes to `{}`. --- .../acinq/eclair/json/JsonSerializers.scala | 7 +- .../funder/data.bin | 1 + .../funder/data.json | 123 ++++++++++++++++++ .../eclair/json/JsonSerializersSpec.scala | 4 + 4 files changed, 134 insertions(+), 1 deletion(-) create mode 100644 eclair-core/src/test/resources/nonreg/codecs/030000-DATA_WAIT_FOR_FUNDING_CONFIRMED/funder/data.bin create mode 100644 eclair-core/src/test/resources/nonreg/codecs/030000-DATA_WAIT_FOR_FUNDING_CONFIRMED/funder/data.json diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/json/JsonSerializers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/json/JsonSerializers.scala index a03abfb410..2bb7bc22a4 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/json/JsonSerializers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/json/JsonSerializers.scala @@ -36,7 +36,7 @@ import fr.acinq.eclair.transactions.DirectedHtlc import fr.acinq.eclair.transactions.Transactions._ import fr.acinq.eclair.wire.protocol.MessageOnionCodecs.blindedRouteCodec import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, Feature, FeatureSupport, Alias, MilliSatoshi, ShortChannelId, TimestampMilli, TimestampSecond, UInt64, UnknownFeature, channel} +import fr.acinq.eclair.{Alias, BlockHeight, CltvExpiry, CltvExpiryDelta, Feature, FeatureSupport, MilliSatoshi, ShortChannelId, TimestampMilli, TimestampSecond, UInt64, UnknownFeature, channel} import org.json4s import org.json4s.JsonAST._ import org.json4s.jackson.Serialization @@ -151,6 +151,10 @@ object CltvExpiryDeltaSerializer extends MinimalSerializer({ case x: CltvExpiryDelta => JInt(x.toInt) }) +object BlockHeightSerializer extends MinimalSerializer({ + case x: BlockHeight => JInt(x.toInt) +}) + object FeeratePerKwSerializer extends MinimalSerializer({ case x: FeeratePerKw => JLong(x.toLong) }) @@ -539,6 +543,7 @@ object JsonSerializers { MilliSatoshiSerializer + CltvExpirySerializer + CltvExpiryDeltaSerializer + + BlockHeightSerializer + FeeratePerKwSerializer + ShortChannelIdSerializer + ChannelIdentifierSerializer + diff --git a/eclair-core/src/test/resources/nonreg/codecs/030000-DATA_WAIT_FOR_FUNDING_CONFIRMED/funder/data.bin b/eclair-core/src/test/resources/nonreg/codecs/030000-DATA_WAIT_FOR_FUNDING_CONFIRMED/funder/data.bin new file mode 100644 index 0000000000..0f0ba269a5 --- /dev/null +++ b/eclair-core/src/test/resources/nonreg/codecs/030000-DATA_WAIT_FOR_FUNDING_CONFIRMED/funder/data.bin @@ -0,0 +1 @@ +030000e917adc681383fe00f779c6144a1bd91135ba2c9862ad1bc5aa8a14d37bae3f401010002aaaa00ce2f18a967dc4f25f414e671ba6585f8ef0b8c5fb812c21064f55a2eaa0009f3ef2134fbf50054ec0414b18905a8d87d8b0931410173e35923fa5d4c5a3f5280000001000000000000044c000000001dcd65000000000000002710000000000000000000900064ff160014fec406ef7a0258cb503fe1f1803787d971eeb4d10000186b020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002498202bbbb671d15145722fb8c28d732cddb249bcc6652ed2b297ff1f77a18371b1e6300000000000003e8ffffffffffffffff0000000000004e2000000000000003e80090001e02e3048a4918587b33fb380e3061b7ac38ef4038551c0f098850d43e45ab1cb28302f7c3bf47cdc640304eda4c761a26dfebfee561de15ba106f3d9982d3ef3fbe1002be361b7bf1bdfb283cff3f83bf16f6f8fb67d3f480b541e76518939f667ab83403fcaec5443dd423f160c9b77a48b2585b186f2d850147f57210d3f8a8c8d754a7021eecb7915d4a3f2b391b9b1fbccbaad2006a1e67a0a03aa041721a817e9a08740000000302498200000000000000000000000000002710000000002faf0800000000000bebc20024e917adc681383fe00f779c6144a1bd91135ba2c9862ad1bc5aa8a14d37bae3f4000000002b40420f0000000000220020a5fb8f636bb73759b4c7e5edd55fd8e9e8e5467c7079709cd22fb519c79ab8b347522102e3048a4918587b33fb380e3061b7ac38ef4038551c0f098850d43e45ab1cb2832103e89d5ecf56f19b5080f2c71629a9da8f6bd027ee282b94d615b82d9f7be14cf952ae7d0200000001e917adc681383fe00f779c6144a1bd91135ba2c9862ad1bc5aa8a14d37bae3f40000000000fd11418002400d0300000000001600142276cff9d96f4696d6e504568db62088428706e0b8180c0000000000220020ad1b593fc0780225407ba65c612b974b4b66610e129f21064cb8ada0ffe4c8d2b72621201ea9cffd2af82f6c14251bd59ffa3e876178c7b263f16d12790c33fc093eaed053dd9e0d49f79558aeb3c2fe7fe84f95a91eecbea2679fb65916ad9632df07b400000000000000000000000000002710000000000bebc200000000002faf080010e4205393672b61bbde4261441a2cf8d08ae50fdb813df8efaac6c6090ef290032a992c123095216f7937a8b0baf442211eeb57942d586854a61a0dc6b01ca6ee000000000000000000000000000000000000000000000000000000000000ff030af74aa1e98668a504d50fe6f664aff3fbdb5c8681f0667c34cdb80024fb950f24e917adc681383fe00f779c6144a1bd91135ba2c9862ad1bc5aa8a14d37bae3f4000000002b40420f0000000000220020a5fb8f636bb73759b4c7e5edd55fd8e9e8e5467c7079709cd22fb519c79ab8b347522102e3048a4918587b33fb380e3061b7ac38ef4038551c0f098850d43e45ab1cb2832103e89d5ecf56f19b5080f2c71629a9da8f6bd027ee282b94d615b82d9f7be14cf952ae000000ff5e020000000101010101010101010101010101010101010101010101010101010101010101012a00000000ffffffff0140420f0000000000220020a5fb8f636bb73759b4c7e5edd55fd8e9e8e5467c7079709cd22fb519c79ab8b3000000000000000000061a800000820000000000000000000000000000000000000000000000000000000000000000e917adc681383fe00f779c6144a1bd91135ba2c9862ad1bc5aa8a14d37bae3f40000bd55d8660f54a1be6e123e2ed9cd6669d90b830d99dc6f9addd9ae65c447ea6b657e2fe39841f66c1d6a2fc80ad59e6f3d9bfaf177f4e95a579f0683bf3e9790 \ No newline at end of file diff --git a/eclair-core/src/test/resources/nonreg/codecs/030000-DATA_WAIT_FOR_FUNDING_CONFIRMED/funder/data.json b/eclair-core/src/test/resources/nonreg/codecs/030000-DATA_WAIT_FOR_FUNDING_CONFIRMED/funder/data.json new file mode 100644 index 0000000000..548e30349a --- /dev/null +++ b/eclair-core/src/test/resources/nonreg/codecs/030000-DATA_WAIT_FOR_FUNDING_CONFIRMED/funder/data.json @@ -0,0 +1,123 @@ +{ + "type" : "DATA_WAIT_FOR_FUNDING_CONFIRMED", + "commitments" : { + "channelId" : "e917adc681383fe00f779c6144a1bd91135ba2c9862ad1bc5aa8a14d37bae3f4", + "channelConfig" : [ "funding_pubkey_based_channel_keypath" ], + "channelFeatures" : [ ], + "localParams" : { + "nodeId" : "02aaaa00ce2f18a967dc4f25f414e671ba6585f8ef0b8c5fb812c21064f55a2eaa", + "fundingKeyPath" : { + "path" : [ 4092535092, 4227137620, 3959690417, 2298849496, 2106263857, 1090614243, 1495530077, 1280982866, 2147483649 ] + }, + "dustLimit" : 1100, + "maxHtlcValueInFlightMsat" : 500000000, + "requestedChannelReserve_opt" : 10000, + "htlcMinimum" : 0, + "toSelfDelay" : 144, + "maxAcceptedHtlcs" : 100, + "isInitiator" : true, + "defaultFinalScriptPubKey" : "0014fec406ef7a0258cb503fe1f1803787d971eeb4d1", + "initFeatures" : { + "activated" : { + "payment_secret" : "mandatory", + "gossip_queries_ex" : "optional", + "option_data_loss_protect" : "optional", + "var_onion_optin" : "mandatory", + "basic_mpp" : "optional", + "gossip_queries" : "optional" + }, + "unknown" : [ 50001 ] + } + }, + "remoteParams" : { + "nodeId" : "02bbbb671d15145722fb8c28d732cddb249bcc6652ed2b297ff1f77a18371b1e63", + "dustLimit" : 1000, + "maxHtlcValueInFlightMsat" : 18446744073709551615, + "requestedChannelReserve_opt" : 20000, + "htlcMinimum" : 1000, + "toSelfDelay" : 144, + "maxAcceptedHtlcs" : 30, + "fundingPubKey" : "02e3048a4918587b33fb380e3061b7ac38ef4038551c0f098850d43e45ab1cb283", + "revocationBasepoint" : "02f7c3bf47cdc640304eda4c761a26dfebfee561de15ba106f3d9982d3ef3fbe10", + "paymentBasepoint" : "02be361b7bf1bdfb283cff3f83bf16f6f8fb67d3f480b541e76518939f667ab834", + "delayedPaymentBasepoint" : "03fcaec5443dd423f160c9b77a48b2585b186f2d850147f57210d3f8a8c8d754a7", + "htlcBasepoint" : "021eecb7915d4a3f2b391b9b1fbccbaad2006a1e67a0a03aa041721a817e9a0874", + "initFeatures" : { + "activated" : { + "payment_secret" : "mandatory", + "gossip_queries_ex" : "optional", + "option_data_loss_protect" : "optional", + "var_onion_optin" : "mandatory", + "basic_mpp" : "optional", + "gossip_queries" : "optional" + }, + "unknown" : [ ] + } + }, + "channelFlags" : { + "announceChannel" : false + }, + "localCommit" : { + "index" : 0, + "spec" : { + "htlcs" : [ ], + "commitTxFeerate" : 10000, + "toLocal" : 800000000, + "toRemote" : 200000000 + }, + "commitTxAndRemoteSig" : { + "commitTx" : { + "txid" : "c6fe6bc0a5a9c149a03a907d2351714aa27fc98a485e981343cea08a1904ee26", + "tx" : "0200000001e917adc681383fe00f779c6144a1bd91135ba2c9862ad1bc5aa8a14d37bae3f40000000000fd11418002400d0300000000001600142276cff9d96f4696d6e504568db62088428706e0b8180c0000000000220020ad1b593fc0780225407ba65c612b974b4b66610e129f21064cb8ada0ffe4c8d2b7262120" + }, + "remoteSig" : "1ea9cffd2af82f6c14251bd59ffa3e876178c7b263f16d12790c33fc093eaed053dd9e0d49f79558aeb3c2fe7fe84f95a91eecbea2679fb65916ad9632df07b4" + }, + "htlcTxsAndRemoteSigs" : [ ] + }, + "remoteCommit" : { + "index" : 0, + "spec" : { + "htlcs" : [ ], + "commitTxFeerate" : 10000, + "toLocal" : 200000000, + "toRemote" : 800000000 + }, + "txid" : "10e4205393672b61bbde4261441a2cf8d08ae50fdb813df8efaac6c6090ef290", + "remotePerCommitmentPoint" : "032a992c123095216f7937a8b0baf442211eeb57942d586854a61a0dc6b01ca6ee" + }, + "localChanges" : { + "proposed" : [ ], + "signed" : [ ], + "acked" : [ ] + }, + "remoteChanges" : { + "proposed" : [ ], + "acked" : [ ], + "signed" : [ ] + }, + "localNextHtlcId" : 0, + "remoteNextHtlcId" : 0, + "originChannels" : { }, + "remoteNextCommitInfo" : "030af74aa1e98668a504d50fe6f664aff3fbdb5c8681f0667c34cdb80024fb950f", + "commitInput" : { + "outPoint" : "f4e3ba374da1a85abcd12a86c9a25b1391bda144619c770fe03f3881c6ad17e9:0", + "amountSatoshis" : 1000000 + }, + "remotePerCommitmentSecrets" : null + }, + "fundingTx" : { + "txid" : "f4e3ba374da1a85abcd12a86c9a25b1391bda144619c770fe03f3881c6ad17e9", + "tx" : "020000000101010101010101010101010101010101010101010101010101010101010101012a00000000ffffffff0140420f0000000000220020a5fb8f636bb73759b4c7e5edd55fd8e9e8e5467c7079709cd22fb519c79ab8b300000000" + }, + "waitingSince" : 400000, + "lastSent" : { + "temporaryChannelId" : "0000000000000000000000000000000000000000000000000000000000000000", + "fundingTxid" : "e917adc681383fe00f779c6144a1bd91135ba2c9862ad1bc5aa8a14d37bae3f4", + "fundingOutputIndex" : 0, + "signature" : "bd55d8660f54a1be6e123e2ed9cd6669d90b830d99dc6f9addd9ae65c447ea6b657e2fe39841f66c1d6a2fc80ad59e6f3d9bfaf177f4e95a579f0683bf3e9790", + "tlvStream" : { + "records" : [ ], + "unknown" : [ ] + } + } +} \ No newline at end of file diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/json/JsonSerializersSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/json/JsonSerializersSpec.scala index 0249b9a53f..59a05f2d0d 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/json/JsonSerializersSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/json/JsonSerializersSpec.scala @@ -320,6 +320,10 @@ class JsonSerializersSpec extends AnyFunSuite with Matchers { } } + test("serialize block height") { + JsonSerializers.serialization.write(BlockHeight(123456))(JsonSerializers.formats) shouldBe "123456" + } + /** utility method that strips line breaks in the expected json */ def assertJsonEquals(actual: String, expected: String) = { val cleanedExpected = expected From e1dc358c7957da1723860c2a450eb194b56d0d4e Mon Sep 17 00:00:00 2001 From: Barry G Becker Date: Mon, 4 Jul 2022 05:05:12 -0700 Subject: [PATCH 114/121] Fix RoutingHeuristics.normalize (#2331) `RoutingHeuristics.normalize` could return 1, it now returns a value in (0, 1) --- .../scala/fr/acinq/eclair/router/Graph.scala | 7 +++--- .../fr/acinq/eclair/router/GraphSpec.scala | 24 +++++++++++++++++++ 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/Graph.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/Graph.scala index b2dff75d0d..554a25dbea 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/Graph.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/Graph.scala @@ -415,9 +415,10 @@ object Graph { * extremes but always bigger than zero so it's guaranteed to never return zero */ def normalize(value: Double, min: Double, max: Double): Double = { - if (value <= min) 0.00001D - else if (value > max) 0.99999D - else (value - min) / (max - min) + require(max > min, "The max must be larger than the min") + val clampedValue = Math.min(Math.max(min, value), max) + val extent = max - min + 0.00001D + 0.99998D * (clampedValue - min) / extent } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/GraphSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/GraphSpec.scala index c2e832905b..a62561bb47 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/GraphSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/GraphSpec.scala @@ -24,6 +24,7 @@ import fr.acinq.eclair.router.Graph.{HeuristicsConstants, WeightRatios, yenKshor import fr.acinq.eclair.router.RouteCalculationSpec._ import fr.acinq.eclair.router.Router.ChannelDesc import fr.acinq.eclair.{BlockHeight, MilliSatoshiLong, ShortChannelId} +import org.scalactic.Tolerance.convertNumericToPlusOrMinusWrapper import org.scalatest.funsuite.AnyFunSuite import scodec.bits._ @@ -348,4 +349,27 @@ class GraphSpec extends AnyFunSuite { assert(paths(1).path == Seq(edgeCE, edgeEG, edgeGH)) assert(paths(2).path == Seq(edgeCD, edgeDF, edgeFH)) } + + test("RoutingHeuristics.normalize") { + // value inside the range + assert(Graph.RoutingHeuristics.normalize(value = 10, min = 0, max = 100) === (10.0 / 100.0) +- 0.001) + assert(Graph.RoutingHeuristics.normalize(value = 20, min = 10, max = 200) === (10.0 / 190.0) +- 0.001) + assert(Graph.RoutingHeuristics.normalize(value = -11, min = -100, max = -10) === (89.0 / 90.0) +- 0.001) + + // value on the bounds + assert(Graph.RoutingHeuristics.normalize(value = 0, min = 0, max = 100) > 0) + assert(Graph.RoutingHeuristics.normalize(value = 10, min = 10, max = 200) > 0) + assert(Graph.RoutingHeuristics.normalize(value = -100, min = -100, max = -10) > 0) + assert(Graph.RoutingHeuristics.normalize(value = 9.1, min = 10, max = 200) > 0) + assert(Graph.RoutingHeuristics.normalize(value = 100, min = 0, max = 100) < 1) + assert(Graph.RoutingHeuristics.normalize(value = 200, min = 10, max = 200) < 1) + + // value outside the range + assert(Graph.RoutingHeuristics.normalize(value = 105.2, min = 0, max = 100) < 1) + + // Should throw exception if min > max + assertThrows[IllegalArgumentException]( + Graph.RoutingHeuristics.normalize(value = 9, min = 10, max = 1) + ) + } } From b5aaddb586e6f7a981e7e99c18f6837fc7cfcb2b Mon Sep 17 00:00:00 2001 From: Thomas HUET Date: Tue, 23 Nov 2021 10:39:52 +0100 Subject: [PATCH 115/121] Add support for paying offers --- eclair-core/src/main/resources/reference.conf | 5 +- .../main/scala/fr/acinq/eclair/Eclair.scala | 56 ++++- .../scala/fr/acinq/eclair/NodeParams.scala | 1 + .../acinq/eclair/message/OnionMessages.scala | 7 +- .../fr/acinq/eclair/message/Postman.scala | 41 +++- .../acinq/eclair/payment/Bolt12Invoice.scala | 13 +- .../acinq/eclair/payment/send/Autoprobe.scala | 2 +- .../eclair/payment/send/OfferPayment.scala | 207 ++++++++++++++++++ .../payment/send/PaymentInitiator.scala | 14 +- .../eclair/wire/protocol/MessageOnion.scala | 9 +- .../eclair/wire/protocol/OfferCodecs.scala | 6 +- .../acinq/eclair/wire/protocol/Offers.scala | 26 +-- .../eclair/wire/protocol/TlvCodecs.scala | 2 +- .../scala/fr/acinq/eclair/TestConstants.scala | 6 +- .../integration/ChannelIntegrationSpec.scala | 6 +- .../integration/OffersIntegrationSpec.scala | 103 +++++++++ .../integration/PaymentIntegrationSpec.scala | 28 +-- .../PerformanceIntegrationSpec.scala | 2 +- .../basic/fixtures/MinimalNodeFixture.scala | 2 +- .../fr/acinq/eclair/message/PostmanSpec.scala | 2 +- .../eclair/payment/Bolt12InvoiceSpec.scala | 38 ++-- .../eclair/payment/PaymentInitiatorSpec.scala | 10 +- .../eclair/wire/protocol/OffersSpec.scala | 26 +-- .../api/directives/ExtraDirectives.scala | 2 + .../acinq/eclair/api/handlers/Payment.scala | 9 +- .../api/serde/FormParamExtractors.scala | 3 + 26 files changed, 522 insertions(+), 104 deletions(-) create mode 100644 eclair-core/src/main/scala/fr/acinq/eclair/payment/send/OfferPayment.scala create mode 100644 eclair-core/src/test/scala/fr/acinq/eclair/integration/OffersIntegrationSpec.scala diff --git a/eclair-core/src/main/resources/reference.conf b/eclair-core/src/main/resources/reference.conf index add6c41f59..f612c769a8 100644 --- a/eclair-core/src/main/resources/reference.conf +++ b/eclair-core/src/main/resources/reference.conf @@ -429,7 +429,10 @@ eclair { max-per-peer-per-second = 10 # Consider a message to be lost if we haven't received a reply after that amount of time - reply-timeout = 20 seconds + reply-timeout = 5 seconds + + # If we expect a reply but do not get one, retry until we reach this number of attempts + max-attempts = 1 } purge-expired-invoices { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala index d8926d7d91..5cdca7dc67 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala @@ -19,6 +19,9 @@ package fr.acinq.eclair import akka.actor.ActorRef import akka.actor.typed.scaladsl.AskPattern.Askable import akka.actor.typed.scaladsl.adapter.ClassicSchedulerOps +import akka.actor.typed +import akka.actor.typed.scaladsl.AskPattern.schedulerFromActorSystem +import akka.actor.typed.scaladsl.adapter.ClassicActorSystemOps import akka.pattern._ import akka.util.Timeout import com.softwaremill.quicklens.ModifyPimp @@ -42,14 +45,16 @@ import fr.acinq.eclair.payment._ import fr.acinq.eclair.payment.receive.MultiPartHandler.ReceivePayment import fr.acinq.eclair.payment.relay.Relayer.{ChannelBalance, GetOutgoingChannels, OutgoingChannels, RelayFees} import fr.acinq.eclair.payment.send.MultiPartPaymentLifecycle.PreimageReceived +import fr.acinq.eclair.payment.send.OfferPayment import fr.acinq.eclair.payment.send.PaymentInitiator._ import fr.acinq.eclair.router.Router import fr.acinq.eclair.router.Router._ import fr.acinq.eclair.wire.protocol.MessageOnionCodecs.blindedRouteCodec +import fr.acinq.eclair.wire.protocol.Offers.Offer import fr.acinq.eclair.wire.protocol._ import grizzled.slf4j.Logging import scodec.bits.ByteVector -import scodec.{Attempt, DecodeResult, codecs} +import scodec.{Attempt, DecodeResult} import java.nio.charset.StandardCharsets import java.util.UUID @@ -67,6 +72,8 @@ case class VerifiedMessage(valid: Boolean, publicKey: PublicKey) case class SendOnionMessageResponsePayload(encodedReplyPath: Option[String], replyPath: Option[Sphinx.RouteBlinding.BlindedRoute], unknownTlvs: Map[String, ByteVector]) case class SendOnionMessageResponse(sent: Boolean, failureMessage: Option[String], response: Option[SendOnionMessageResponsePayload]) + +case class PayOfferResponse(invoice: Option[String], invoiceRequest: Option[String], payerKey: Option[String], preimage: Option[String], failureMessage: Option[String]) // @formatter:on object SignedMessage { @@ -160,6 +167,8 @@ trait Eclair { def sendOnionMessage(intermediateNodes: Seq[PublicKey], destination: Either[PublicKey, Sphinx.RouteBlinding.BlindedRoute], replyPath: Option[Seq[PublicKey]], userCustomContent: ByteVector)(implicit timeout: Timeout): Future[SendOnionMessageResponse] + def payOffer(offer: Offer, amount: MilliSatoshi, quantity: Long, externalId_opt: Option[String] = None, maxAttempts_opt: Option[Int] = None, maxFeeFlat_opt: Option[Satoshi] = None, maxFeePct_opt: Option[Double] = None, pathFindingExperimentName_opt: Option[String] = None)(implicit timeout: Timeout): Future[PayOfferResponse] + def stop(): Future[Unit] } @@ -167,6 +176,8 @@ class EclairImpl(appKit: Kit) extends Eclair with Logging { implicit val ec: ExecutionContext = appKit.system.dispatcher + implicit val typedSystem: typed.ActorSystem[Nothing] = appKit.system.toTyped + // We constrain external identifiers. This allows uuid, long and pubkey to be used. private val externalIdMaxLength = 66 @@ -342,7 +353,7 @@ class EclairImpl(appKit: Kit) extends Eclair with Logging { externalId_opt match { case Some(externalId) if externalId.length > externalIdMaxLength => Left(new IllegalArgumentException(s"externalId is too long: cannot exceed $externalIdMaxLength characters")) case _ if invoice.isExpired() => Left(new IllegalArgumentException("invoice has expired")) - case _ => Right(SendPaymentToNode(amount, invoice, maxAttempts, externalId_opt, extraEdges = invoice.extraEdges, routeParams = routeParams)) + case _ => Right(SendPaymentToNode(ActorRef.noSender ,amount, invoice, maxAttempts, externalId_opt, extraEdges = invoice.extraEdges, routeParams = routeParams)) } case Left(t) => Left(t) } @@ -543,7 +554,7 @@ class EclairImpl(appKit: Kit) extends Eclair with Logging { destination: Either[PublicKey, Sphinx.RouteBlinding.BlindedRoute], replyPath: Option[Seq[PublicKey]], userCustomContent: ByteVector)(implicit timeout: Timeout): Future[SendOnionMessageResponse] = { - codecs.list(TlvCodecs.genericTlv).decode(userCustomContent.bits) match { + MessageOnionCodecs.perHopPayloadCodec.decode(userCustomContent.bits) match { case Attempt.Successful(DecodeResult(userCustomTlvs, _)) => val replyPathId = randomBytes32() val replyRoute = replyPath.map(hops => OnionMessages.buildRoute(randomKey(), hops.map(OnionMessages.IntermediateNode(_)), OnionMessages.Recipient(appKit.nodeParams.nodeId, Some(replyPathId)))) @@ -555,7 +566,7 @@ class EclairImpl(appKit: Kit) extends Eclair with Logging { replyRoute.map(OnionMessagePayloadTlv.ReplyPath(_) :: Nil).getOrElse(Nil), userCustomTlvs) appKit.postman.ask(ref => Postman.SendMessage(nextNodeId, message, replyPath.map(_ => replyPathId), ref, appKit.nodeParams.onionMessageConfig.timeout))(timeout, appKit.system.scheduler.toTyped).mapTo[Postman.OnionMessageResponse].map { - case Postman.Response(payload) => + case Postman.Response(payload, _) => val encodedReplyPath = payload.replyPath.map(route => blindedRouteCodec.encode(route.blindedRoute).require.bytes.toHex) SendOnionMessageResponse(sent = true, None, Some(SendOnionMessageResponsePayload(encodedReplyPath, payload.replyPath.map(_.blindedRoute), payload.records.unknown.map(tlv => tlv.tag.toString -> tlv.value).toMap))) case Postman.NoReply => SendOnionMessageResponse(sent = true, Some("No response"), None) @@ -566,6 +577,43 @@ class EclairImpl(appKit: Kit) extends Eclair with Logging { } } + override def payOffer(offer: Offer, + amount: MilliSatoshi, + quantity: Long, + externalId_opt: Option[String], + maxAttempts_opt: Option[Int], + maxFeeFlat_opt: Option[Satoshi], + maxFeePct_opt: Option[Double], + pathFindingExperimentName_opt: Option[String])(implicit timeout: Timeout): Future[PayOfferResponse] = { + if(externalId_opt.exists(_.length > externalIdMaxLength)) { + return Future.failed(new IllegalArgumentException(s"externalId is too long: cannot exceed $externalIdMaxLength characters")) + } + val routeParams = getRouteParams(pathFindingExperimentName_opt) match { + case Right(defaultRouteParams) => + defaultRouteParams + .modify(_.boundaries.maxFeeProportional).setToIfDefined(maxFeePct_opt.map(_ / 100)) + .modify(_.boundaries.maxFeeFlat).setToIfDefined(maxFeeFlat_opt.map(_.toMilliSatoshi)) + case Left(t) => return Future.failed(t) + } + val sendPaymentConfig = OfferPayment.SendPaymentConfig(externalId_opt, maxAttempts_opt.getOrElse(appKit.nodeParams.maxPaymentAttempts), routeParams) + val offerPayment = appKit.system.spawnAnonymous(OfferPayment(appKit.nodeParams, appKit.postman, appKit.paymentInitiator, offer, amount, quantity, sendPaymentConfig)) + offerPayment.ask((ref: typed.ActorRef[OfferPayment.Result]) => OfferPayment.PayOffer(ref)).mapTo[OfferPayment.Result].map { + case OfferPayment.Success(invoice, payerKey, paymentPreimage) => PayOfferResponse(Some(invoice.toString), None, Some(payerKey.value.toHex), Some(paymentPreimage.toHex), None) + case OfferPayment.UnsupportedFeatures(features) => PayOfferResponse(None, None, None, None, Some(s"Unsupported features: $features")) + case OfferPayment.UnsupportedChains(chains) => PayOfferResponse(None, None, None, None, Some(s"Unsupported chains: ${chains.mkString(",")}")) + case OfferPayment.UnsupportedCurrency(iso4217) => PayOfferResponse(None, None, None, None, Some(s"Unsupported currency: $iso4217")) + case OfferPayment.ExpiredOffer(expiryDate) => PayOfferResponse(None, None, None, None, Some(s"Offer expired since $expiryDate")) + case OfferPayment.QuantityTooLow(quantityMin) => PayOfferResponse(None, None, None, None, Some(s"Minimum quantity is $quantityMin")) + case OfferPayment.QuantityTooHigh(quantityMax) => PayOfferResponse(None, None, None, None, Some(s"Maximum quantity is $quantityMax")) + case OfferPayment.IsSendInvoice => PayOfferResponse(None, None, None, None, Some("This offer is not payable")) + case OfferPayment.AmountInsufficient(amountNeeded) => PayOfferResponse(None, None, None, None, Some(s"Paying this offer requires at least $amountNeeded")) + case OfferPayment.InvalidSignature(signature) => PayOfferResponse(None, None, None, None, Some(s"Invalid signature: ${signature.toHex}")) + case OfferPayment.InvalidInvoice(invoice, request, payerKey) => PayOfferResponse(Some(invoice.toString), Some(request.toString), Some(payerKey.value.toHex), None, Some("Invalid invoice")) + case OfferPayment.NoInvoice => PayOfferResponse(None, None, None, None, Some("Couldn't retrieve invoice")) + case OfferPayment.FailedPayment(invoice, payerKey, paymentFailed) => PayOfferResponse(Some(invoice.toString), None, Some(payerKey.value.toHex), None, Some(paymentFailed.toString)) + } + } + override def stop(): Future[Unit] = { // README: do not make this smarter or more complex ! // eclair can simply and cleanly be stopped by killing its process without fear of losing data, payments, ... and it should remain this way. diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala index a3e7791e53..1d65b7a8c5 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala @@ -510,6 +510,7 @@ object NodeParams extends Logging { onionMessageConfig = OnionMessageConfig( relayPolicy = onionMessageRelayPolicy, timeout = FiniteDuration(config.getDuration("onion-messages.reply-timeout").getSeconds, TimeUnit.SECONDS), + maxAttempts = config.getInt("onion-messages.max-attempts"), ), purgeInvoicesInterval = purgeInvoicesInterval ) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/message/OnionMessages.scala b/eclair-core/src/main/scala/fr/acinq/eclair/message/OnionMessages.scala index 72e6ac298f..20f525a188 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/message/OnionMessages.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/message/OnionMessages.scala @@ -34,7 +34,8 @@ import scala.util.{Failure, Success} object OnionMessages { case class OnionMessageConfig(relayPolicy: RelayPolicy, - timeout: FiniteDuration) + timeout: FiniteDuration, + maxAttempts: Int) case class IntermediateNode(nodeId: PublicKey, padding: Option[ByteVector] = None) @@ -88,9 +89,9 @@ object OnionMessages { intermediateNodes: Seq[IntermediateNode], destination: Destination, content: Seq[OnionMessagePayloadTlv], - userCustomTlvs: Seq[GenericTlv] = Nil): (PublicKey, OnionMessage) = { + userCustomTlvs: TlvStream[OnionMessagePayloadTlv] = TlvStream.empty): (PublicKey, OnionMessage) = { val route = buildRoute(blindingSecret, intermediateNodes, destination) - val lastPayload = MessageOnionCodecs.finalPerHopPayloadCodec.encode(FinalPayload(TlvStream(EncryptedData(route.encryptedPayloads.last) +: content, userCustomTlvs))).require.bytes + val lastPayload = MessageOnionCodecs.finalPerHopPayloadCodec.encode(FinalPayload(TlvStream[OnionMessagePayloadTlv](EncryptedData(route.encryptedPayloads.last) +: (content ++ userCustomTlvs.records), userCustomTlvs.unknown))).require.bytes val payloads = route.encryptedPayloads.dropRight(1).map(encTlv => MessageOnionCodecs.relayPerHopPayloadCodec.encode(RelayPayload(TlvStream(EncryptedData(encTlv)))).require.bytes) :+ lastPayload val payloadSize = payloads.map(_.length + Sphinx.MacLength).sum val packetSize = if (payloadSize <= 1300) { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/message/Postman.scala b/eclair-core/src/main/scala/fr/acinq/eclair/message/Postman.scala index 5a5cff13ba..c3a71f18fc 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/message/Postman.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/message/Postman.scala @@ -39,11 +39,19 @@ object Postman { replyPathId: Option[ByteVector32], replyTo: ActorRef[OnionMessageResponse], timeout: FiniteDuration) extends Command + case class SendMessageToBoth(nextNodeId1: PublicKey, + message1: OnionMessage, + replyPathId1: ByteVector32, + nextNodeId2: PublicKey, + message2: OnionMessage, + replyPathId2: ByteVector32, + replyTo: ActorRef[OnionMessageResponse], + timeout: FiniteDuration) extends Command private case class Unsubscribe(pathId: ByteVector32) extends Command private case class WrappedMessage(finalPayload: FinalPayload, pathId: Option[ByteVector]) extends Command sealed trait OnionMessageResponse case object NoReply extends OnionMessageResponse - case class Response(payload: FinalPayload) extends OnionMessageResponse + case class Response(payload: FinalPayload, pathId: ByteVector32) extends OnionMessageResponse case class SendingStatus(status: MessageRelay.Status) extends OnionMessageResponse with Command // @formatter:on @@ -59,13 +67,21 @@ object Postman { // For messages not expecting a reply, send success or failure to send val sendStatusTo = new mutable.HashMap[ByteVector32, ActorRef[OnionMessageResponse]]() - Behaviors.receiveMessagePartial { + // For pairs or messages expecting one reply, send first reply or second failure + val messagePair = new mutable.HashMap[ByteVector32, (ByteVector32, ActorRef[OnionMessageResponse])]() + + Behaviors.receiveMessage { case WrappedMessage(finalPayload, Some(pathId)) if pathId.length == 32 => val id = ByteVector32(pathId) subscribed.get(id).foreach(ref => { subscribed -= id - ref ! Response(finalPayload) + ref ! Response(finalPayload, id) }) + messagePair.get(id).foreach { case (otherId, ref) => + messagePair -= id + messagePair -= otherId + ref ! Response(finalPayload, id) + } Behaviors.same case WrappedMessage(_, _) => // ignoring message with invalid or missing pathId @@ -80,11 +96,24 @@ object Postman { context.scheduleOnce(timeout, context.self, Unsubscribe(pathId)) switchboard ! Switchboard.RelayMessage(pathId, None, nextNodeId, message, MessageRelay.RelayAll, Some(relayMessageStatusAdapter)) Behaviors.same + case SendMessageToBoth(nextNodeId1, message1, pathId1, nextNodeId2, message2, pathId2, ref, timeout) => // two messages expecting one reply + messagePair += (pathId1 -> (pathId2, ref)) + messagePair += (pathId2 -> (pathId1, ref)) + context.scheduleOnce(timeout, context.self, Unsubscribe(pathId1)) + context.scheduleOnce(timeout, context.self, Unsubscribe(pathId2)) + switchboard ! Switchboard.RelayMessage(pathId1, None, nextNodeId1, message1, MessageRelay.RelayAll, Some(relayMessageStatusAdapter)) + switchboard ! Switchboard.RelayMessage(pathId2, None, nextNodeId2, message2, MessageRelay.RelayAll, Some(relayMessageStatusAdapter)) + Behaviors.same case Unsubscribe(pathId) => subscribed.get(pathId).foreach(ref => { subscribed -= pathId ref ! NoReply }) + messagePair.get(pathId).foreach { case (otherId, ref) => + messagePair -= pathId + messagePair -= otherId + ref ! NoReply + } Behaviors.same case status@SendingStatus(MessageRelay.Sent(messageId)) => sendStatusTo.get(messageId).foreach(ref => { @@ -101,6 +130,12 @@ object Postman { subscribed -= status.messageId ref ! SendingStatus(status) }) + messagePair.get(status.messageId).foreach { case (otherId, ref) => + messagePair -= status.messageId + if (!messagePair.contains(otherId)) { + ref ! NoReply + } + } Behaviors.same } }) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Bolt12Invoice.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Bolt12Invoice.scala index 0cd19d3434..ca204beeb2 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Bolt12Invoice.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Bolt12Invoice.scala @@ -39,7 +39,7 @@ case class Bolt12Invoice(records: TlvStream[InvoiceTlv], nodeId_opt: Option[Publ import Bolt12Invoice._ require(records.get[Amount].nonEmpty, "bolt 12 invoices must provide an amount") - require(records.get[NodeId].nonEmpty, "bolt 12 invoices must provide a node id") + require(records.get[NodeIdXOnly].nonEmpty, "bolt 12 invoices must provide a node id") require(records.get[PaymentHash].nonEmpty, "bolt 12 invoices must provide a payment hash") require(records.get[Description].nonEmpty, "bolt 12 invoices must provide a description") require(records.get[CreatedAt].nonEmpty, "bolt 12 invoices must provide a creation timestamp") @@ -49,7 +49,7 @@ case class Bolt12Invoice(records: TlvStream[InvoiceTlv], nodeId_opt: Option[Publ override val amount_opt: Option[MilliSatoshi] = Some(amount) - override val nodeId: Crypto.PublicKey = nodeId_opt.getOrElse(records.get[NodeId].get.nodeId1) + override val nodeId: Crypto.PublicKey = nodeId_opt.getOrElse(records.get[NodeIdXOnly].get.nodeId1) override val paymentHash: ByteVector32 = records.get[PaymentHash].get.hash @@ -102,7 +102,7 @@ case class Bolt12Invoice(records: TlvStream[InvoiceTlv], nodeId_opt: Option[Publ // It is assumed that the request is valid for this offer. def isValidFor(offer: Offer, request: InvoiceRequest): Boolean = { - Offers.xOnlyPublicKey(nodeId) == offer.nodeIdXOnly && + Offers.xOnlyPublicKey(nodeId) == offer.nodeIdXOnly.xOnly && checkSignature() && offerId.contains(request.offerId) && request.chain == chain && @@ -146,14 +146,14 @@ object Bolt12Invoice { * @param nodeKey the key that was used to generate the offer, may be different from our public nodeId if we're hiding behind a blinded route * @param features invoice features */ - def apply(offer: Offer, request: InvoiceRequest, preimage: ByteVector32, nodeKey: PrivateKey, features: Features[InvoiceFeature]): Bolt12Invoice = { + def apply(offer: Offer, request: InvoiceRequest, preimage: ByteVector32, nodeKey: PrivateKey, minFinalCltvExpiryDelta: CltvExpiryDelta, features: Features[InvoiceFeature]): Bolt12Invoice = { require(request.amount.nonEmpty || offer.amount.nonEmpty) val tlvs: Seq[InvoiceTlv] = Seq( Some(Chain(request.chain)), Some(CreatedAt(TimestampSecond.now())), Some(PaymentHash(Crypto.sha256(preimage))), Some(OfferId(offer.offerId)), - Some(NodeId(nodeKey.publicKey)), + Some(NodeIdXOnly(xOnlyPublicKey(nodeKey.publicKey))), Some(Amount(request.amount.orElse(offer.amount.map(_ * request.quantity)).get)), Some(Description(offer.description)), request.quantity_opt.map(Quantity), @@ -162,7 +162,8 @@ object Bolt12Invoice { request.payerNote.map(PayerNote), request.replaceInvoice.map(ReplaceInvoice), offer.issuer.map(Issuer), - Some(FeaturesTlv(features.unscoped())) + Some(Cltv(minFinalCltvExpiryDelta)), + Some(FeaturesTlv(features.unscoped())), ).flatten val signature = signSchnorr(signatureTag("signature"), rootHash(TlvStream(tlvs), invoiceTlvCodec), nodeKey) Bolt12Invoice(TlvStream(tlvs :+ Signature(signature)), Some(nodeKey.publicKey)) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/Autoprobe.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/Autoprobe.scala index 6250534eb9..fdab6ee2a3 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/Autoprobe.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/Autoprobe.scala @@ -65,7 +65,7 @@ class Autoprobe(nodeParams: NodeParams, router: ActorRef, paymentInitiator: Acto ByteVector.empty) log.info(s"sending payment probe to node=$targetNodeId payment_hash=${fakeInvoice.paymentHash}") val routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams - paymentInitiator ! PaymentInitiator.SendPaymentToNode(PAYMENT_AMOUNT_MSAT, fakeInvoice, maxAttempts = 1, routeParams = routeParams) + paymentInitiator ! PaymentInitiator.SendPaymentToNode(self, PAYMENT_AMOUNT_MSAT, fakeInvoice, maxAttempts = 1, routeParams = routeParams) case None => log.info(s"could not find a destination, re-scheduling") scheduleProbe() diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/OfferPayment.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/OfferPayment.scala new file mode 100644 index 0000000000..4b93448a23 --- /dev/null +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/OfferPayment.scala @@ -0,0 +1,207 @@ +/* + * 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.payment.send + +import akka.actor.typed.Behavior +import akka.actor.typed.scaladsl.adapter.TypedActorRefOps +import akka.actor.typed.scaladsl.{ActorContext, Behaviors} +import akka.actor.{ActorRef, typed} +import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64} +import fr.acinq.eclair.message.OnionMessages.Recipient +import fr.acinq.eclair.message.Postman.{OnionMessageResponse, SendMessage, SendMessageToBoth} +import fr.acinq.eclair.message.{OnionMessages, Postman} +import fr.acinq.eclair.payment.send.MultiPartPaymentLifecycle.PreimageReceived +import fr.acinq.eclair.payment.send.PaymentInitiator.SendPaymentToNode +import fr.acinq.eclair.payment.{Bolt12Invoice, PaymentFailed, PaymentSent} +import fr.acinq.eclair.wire.protocol.Offers.{InvoiceRequest, Offer} +import fr.acinq.eclair.wire.protocol.OnionMessagePayloadTlv.ReplyPath +import fr.acinq.eclair.wire.protocol.{OnionMessage, OnionMessagePayloadTlv} +import fr.acinq.eclair.{Features, InvoiceFeature, MilliSatoshi, NodeParams, TimestampSecond, randomBytes32, randomKey} +import fr.acinq.eclair.router.Router.RouteParams + +import java.util.UUID +import scala.util.Random + +object OfferPayment { + sealed trait Result + case class Success(invoice: Bolt12Invoice, payerKey: PrivateKey, paymentPreimage: ByteVector32) extends Result + case class UnsupportedFeatures(features: Features[InvoiceFeature]) extends Result + case class UnsupportedChains(chains: Seq[ByteVector32]) extends Result + case class UnsupportedCurrency(iso4217: String) extends Result + case class ExpiredOffer(expiryDate: TimestampSecond) extends Result + case class QuantityTooLow(quantityMin: Long) extends Result + case class QuantityTooHigh(quantityMax: Long) extends Result + case object IsSendInvoice extends Result + case class AmountInsufficient(amountNeeded: MilliSatoshi) extends Result + case class InvalidSignature(signature: ByteVector64) extends Result + case class InvalidInvoice(invoice: Bolt12Invoice, request: InvoiceRequest, payerKey: PrivateKey) extends Result + case object NoInvoice extends Result + case class FailedPayment(invoice: Bolt12Invoice, payerKey: PrivateKey, paymentFailed: PaymentFailed) extends Result + + sealed trait Command + case class PayOffer(replyTo: typed.ActorRef[Result]) extends Command + case class WrappedMessageResponse(response: OnionMessageResponse) extends Command + case class WrappedPaymentId(paymentId: UUID) extends Command + case class WrappedPreimageReceived(preimageReceived: PreimageReceived) extends Command + case class WrappedPaymentSent(paymentSent: PaymentSent) extends Command + case class WrappedPaymentFailed(paymentFailed: PaymentFailed) extends Command + + case class SendPaymentConfig(externalId_opt: Option[String], + maxAttempts: Int, + routeParams: RouteParams) + + def paymentResultWrapper(x: Any): Command = { + x match { + case paymentId: UUID => WrappedPaymentId(paymentId) + case preimageReceived: PreimageReceived => WrappedPreimageReceived(preimageReceived) + case paymentSent: PaymentSent => WrappedPaymentSent(paymentSent) + case paymentFailed: PaymentFailed => WrappedPaymentFailed(paymentFailed) + } + } + + def buildRequest(nodeParams: NodeParams, request: InvoiceRequest, destination: OnionMessages.Destination): (ByteVector32, PublicKey, OnionMessage) = { + val pathId = randomBytes32() + // TODO: randomize intermediate nodes + val intermediateNodes = Seq.empty + val replyPath = ReplyPath(OnionMessages.buildRoute(randomKey(), intermediateNodes.reverse, Recipient(nodeParams.nodeId, Some(pathId.bytes)))) + val (nextNodeId, message) = OnionMessages.buildMessage(randomKey(), randomKey(), intermediateNodes, destination, Seq(replyPath, OnionMessagePayloadTlv.InvoiceRequest(request))) + (pathId, nextNodeId, message) + } + + def apply(nodeParams: NodeParams, + postman: typed.ActorRef[Postman.Command], + paymentInitiator: ActorRef, + offer: Offer, + amount: MilliSatoshi, + quantity: Long, + sendPaymentConfig: SendPaymentConfig): Behavior[Command] = { + Behaviors.receivePartial { + case (context, PayOffer(replyTo)) => + if (!nodeParams.features.invoiceFeatures().areSupported(offer.features)) { + replyTo ! UnsupportedFeatures(offer.features) + Behaviors.stopped + } else if (!offer.chains.contains(nodeParams.chainHash)) { + replyTo ! UnsupportedChains(offer.chains) + Behaviors.stopped + } else if (offer.currency.nonEmpty) { + replyTo ! UnsupportedCurrency(offer.currency.get) + Behaviors.stopped + } else if (offer.expiry.exists(_ < TimestampSecond.now())) { + replyTo ! ExpiredOffer(offer.expiry.get) + Behaviors.stopped + } else if (offer.quantityMin.exists(_ > quantity)) { + replyTo ! QuantityTooLow(offer.quantityMin.get) + Behaviors.stopped + } else if (offer.quantityMax.getOrElse(1L) < quantity) { + replyTo ! QuantityTooHigh(offer.quantityMax.getOrElse(1)) + Behaviors.stopped + } else if (offer.sendInvoice) { + replyTo ! IsSendInvoice + Behaviors.stopped + } else if (offer.amount.exists(_ * quantity > amount)) { + replyTo ! AmountInsufficient(offer.amount.get * quantity) + Behaviors.stopped + } else if (offer.signature.nonEmpty && !offer.checkSignature()) { + replyTo ! InvalidSignature(offer.signature.get) + Behaviors.stopped + } else { + val payerKey = randomKey() + val request = InvoiceRequest(offer, amount, quantity, nodeParams.features.invoiceFeatures(), payerKey, nodeParams.chainHash) + sendInvoiceRequest(nodeParams, postman, paymentInitiator, context, offer, request, payerKey, replyTo, nodeParams.onionMessageConfig.maxAttempts, sendPaymentConfig) + } + } + } + + val rand = new Random + + def sendInvoiceRequest(nodeParams: NodeParams, + postman: typed.ActorRef[Postman.Command], + paymentInitiator: ActorRef, + context: ActorContext[Command], + offer: Offer, + request: InvoiceRequest, + payerKey: PrivateKey, + replyTo: typed.ActorRef[Result], + remainingAttempts: Int, + sendPaymentConfig: SendPaymentConfig): Behavior[Command] = { + offer.contact match { + case Left(blindedRoutes) => + val blindedRoute = blindedRoutes(rand.nextInt(blindedRoutes.length)) + val (pathId, nextNodeId, message) = buildRequest(nodeParams, request, OnionMessages.BlindedPath(blindedRoute)) + postman ! SendMessage(nextNodeId, message, Some(pathId), context.messageAdapter(WrappedMessageResponse), nodeParams.onionMessageConfig.timeout) + waitForInvoice(nodeParams, postman, paymentInitiator, offer, Map(pathId -> offer.nodeIdXOnly.nodeId1), request, payerKey, replyTo, remainingAttempts - 1, sendPaymentConfig) + case Right(nodeIdXOnly) => + // The node id from the offer is missing the first byte so we try both the odd and even versions, we'll + // pay the first one that answers (the other one should not exist but if it does, it is controlled by the + // same person). + val (pathId1, nextNodeId1, message1) = buildRequest(nodeParams, request, Recipient(nodeIdXOnly.nodeId1, None, None)) + val (pathId2, nextNodeId2, message2) = buildRequest(nodeParams, request, Recipient(nodeIdXOnly.nodeId2, None, None)) + postman ! SendMessageToBoth(nextNodeId1, message1, pathId1, nextNodeId2, message2, pathId2, context.messageAdapter(WrappedMessageResponse), nodeParams.onionMessageConfig.timeout) + waitForInvoice(nodeParams, postman, paymentInitiator, offer, Map(pathId1 -> nodeIdXOnly.nodeId1, pathId2 -> nodeIdXOnly.nodeId2),request, payerKey, replyTo, remainingAttempts - 1, sendPaymentConfig) + + } + } + + def waitForInvoice(nodeParams: NodeParams, + postman: typed.ActorRef[Postman.Command], + paymentInitiator: ActorRef, + offer: Offer, + nodeId: Map[ByteVector32, PublicKey], + request: InvoiceRequest, + payerKey: PrivateKey, + replyTo: typed.ActorRef[Result], + remainingAttempts: Int, + sendPaymentConfig: SendPaymentConfig): Behavior[Command] = { + Behaviors.receivePartial { + case (context, WrappedMessageResponse(Postman.Response(payload, pathId))) if payload.invoice.nonEmpty => + val invoice = payload.invoice.get.invoice.withNodeId(nodeId(pathId)) + if (invoice.isValidFor(offer, request)) { + val recipientAmount = invoice.amount + paymentInitiator ! SendPaymentToNode(context.messageAdapter(paymentResultWrapper).toClassic, recipientAmount, invoice, maxAttempts = sendPaymentConfig.maxAttempts, externalId = sendPaymentConfig.externalId_opt, routeParams = sendPaymentConfig.routeParams) + waitForPayment(invoice, payerKey, replyTo) + } else { + replyTo ! InvalidInvoice(invoice, request, payerKey) + Behaviors.stopped + } + case (context, WrappedMessageResponse(_)) if remainingAttempts > 0 => + // We didn't get an invoice, let's retry. + sendInvoiceRequest(nodeParams, postman, paymentInitiator, context, offer, request, payerKey, replyTo, remainingAttempts, sendPaymentConfig) + case (_, WrappedMessageResponse(_)) => + replyTo ! NoInvoice + Behaviors.stopped + } + } + + def waitForPayment(invoice: Bolt12Invoice, payerKey: PrivateKey, replyTo: typed.ActorRef[Result]): Behavior[Command] = { + Behaviors.receiveMessagePartial { + case WrappedPaymentId(_) => + Behaviors.same + case WrappedPreimageReceived(preimageReceived) => + replyTo ! Success(invoice, payerKey, preimageReceived.paymentPreimage) + Behaviors.stopped + case WrappedPaymentSent(paymentSent) => + replyTo ! Success(invoice, payerKey, paymentSent.paymentPreimage) + Behaviors.stopped + case WrappedPaymentFailed(failed) => + replyTo ! FailedPayment(invoice, payerKey, failed) + Behaviors.stopped + } + } +} + + diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentInitiator.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentInitiator.scala index 09fce8898e..2bdd4ff683 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentInitiator.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentInitiator.scala @@ -46,27 +46,28 @@ class PaymentInitiator(nodeParams: NodeParams, outgoingPaymentFactory: PaymentIn def main(pending: Map[UUID, PendingPayment]): Receive = { case r: SendPaymentToNode => + val replyTo = if (r.replyTo == ActorRef.noSender) sender() else r.replyTo val paymentId = UUID.randomUUID() if (!r.blockUntilComplete) { // Immediately return the paymentId - sender() ! paymentId + replyTo ! paymentId } val paymentCfg = SendPaymentConfig(paymentId, paymentId, r.externalId, r.paymentHash, r.recipientAmount, r.recipientNodeId, Upstream.Local(paymentId), Some(r.invoice), storeInDb = true, publishEvent = true, recordPathFindingMetrics = true, Nil) val finalExpiry = r.finalExpiry(nodeParams.currentBlockHeight) r.invoice.paymentSecret match { case _ if !nodeParams.features.invoiceFeatures().areSupported(r.invoice.features) => - sender() ! PaymentFailed(paymentId, r.paymentHash, LocalFailure(r.recipientAmount, Nil, UnsupportedFeatures(r.invoice.features)) :: Nil) + replyTo ! PaymentFailed(paymentId, r.paymentHash, LocalFailure(r.recipientAmount, Nil, UnsupportedFeatures(r.invoice.features)) :: Nil) case None => - sender() ! PaymentFailed(paymentId, r.paymentHash, LocalFailure(r.recipientAmount, Nil, PaymentSecretMissing) :: Nil) + replyTo ! PaymentFailed(paymentId, r.paymentHash, LocalFailure(r.recipientAmount, Nil, PaymentSecretMissing) :: Nil) case Some(paymentSecret) if r.invoice.features.hasFeature(Features.BasicMultiPartPayment) && nodeParams.features.hasFeature(BasicMultiPartPayment) => val fsm = outgoingPaymentFactory.spawnOutgoingMultiPartPayment(context, paymentCfg) fsm ! SendMultiPartPayment(self, paymentSecret, r.recipientNodeId, r.recipientAmount, finalExpiry, r.maxAttempts, r.invoice.paymentMetadata, r.extraEdges, r.routeParams, userCustomTlvs = r.userCustomTlvs) - context become main(pending + (paymentId -> PendingPaymentToNode(sender(), r))) + context become main(pending + (paymentId -> PendingPaymentToNode(replyTo, r))) case Some(paymentSecret) => val finalPayload = PaymentOnion.createSinglePartPayload(r.recipientAmount, finalExpiry, paymentSecret, r.invoice.paymentMetadata, r.userCustomTlvs) val fsm = outgoingPaymentFactory.spawnOutgoingPayment(context, paymentCfg) fsm ! PaymentLifecycle.SendPaymentToNode(self, r.recipientNodeId, finalPayload, r.maxAttempts, r.extraEdges, r.routeParams) - context become main(pending + (paymentId -> PendingPaymentToNode(sender(), r))) + context become main(pending + (paymentId -> PendingPaymentToNode(replyTo, r))) } case r: SendSpontaneousPayment => @@ -310,7 +311,8 @@ object PaymentInitiator { * @param userCustomTlvs (optional) user-defined custom tlvs that will be added to the onion sent to the target node. * @param blockUntilComplete (optional) if true, wait until the payment completes before returning a result. */ - case class SendPaymentToNode(recipientAmount: MilliSatoshi, + case class SendPaymentToNode(replyTo: ActorRef, + recipientAmount: MilliSatoshi, invoice: Invoice, maxAttempts: Int, externalId: Option[String] = None, diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/MessageOnion.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/MessageOnion.scala index 037e42446c..5db9056fd9 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/MessageOnion.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/MessageOnion.scala @@ -22,6 +22,7 @@ import fr.acinq.eclair.crypto.Sphinx.RouteBlinding.{BlindedNode, BlindedRoute} import fr.acinq.eclair.payment.Bolt12Invoice import fr.acinq.eclair.wire.protocol.OfferCodecs.{invoiceCodec, invoiceErrorCodec, invoiceRequestCodec} import fr.acinq.eclair.wire.protocol.OnionRoutingCodecs.{ForbiddenTlv, MissingRequiredTlv} +import fr.acinq.eclair.wire.protocol.TlvCodecs.tlvStream import scodec.bits.ByteVector /** Tlv types used inside the onion of an [[OnionMessage]]. */ @@ -118,9 +119,11 @@ object MessageOnionCodecs { .typecase(UInt64(68), variableSizeBytesLong(varintoverflow, invoiceErrorCodec.as[InvoiceError])) - val perHopPayloadCodec: Codec[TlvStream[OnionMessagePayloadTlv]] = TlvCodecs.lengthPrefixedTlvStream[OnionMessagePayloadTlv](onionTlvCodec).complete + val perHopPayloadCodec: Codec[TlvStream[OnionMessagePayloadTlv]] = TlvCodecs.tlvStream[OnionMessagePayloadTlv](onionTlvCodec).complete - val relayPerHopPayloadCodec: Codec[RelayPayload] = perHopPayloadCodec.narrow({ + val prefixedPerHopPayloadCodec: Codec[TlvStream[OnionMessagePayloadTlv]] = variableSizeBytesLong(CommonCodecs.varintoverflow, perHopPayloadCodec) + + val relayPerHopPayloadCodec: Codec[RelayPayload] = prefixedPerHopPayloadCodec.narrow({ case tlvs if tlvs.get[EncryptedData].isEmpty => Attempt.failure(MissingRequiredTlv(UInt64(4))) case tlvs if tlvs.get[ReplyPath].nonEmpty => Attempt.failure(ForbiddenTlv(UInt64(2))) case tlvs => Attempt.successful(RelayPayload(tlvs)) @@ -128,7 +131,7 @@ object MessageOnionCodecs { case RelayPayload(tlvs) => tlvs }) - val finalPerHopPayloadCodec: Codec[FinalPayload] = perHopPayloadCodec.narrow({ + val finalPerHopPayloadCodec: Codec[FinalPayload] = prefixedPerHopPayloadCodec.narrow({ case tlvs if tlvs.get[EncryptedData].isEmpty => Attempt.failure(MissingRequiredTlv(UInt64(4))) case tlvs => Attempt.successful(FinalPayload(tlvs)) }, { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/OfferCodecs.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/OfferCodecs.scala index 81bb9d5c58..948ed8bd16 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/OfferCodecs.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/OfferCodecs.scala @@ -53,7 +53,7 @@ object OfferCodecs { private val quantityMax: Codec[QuantityMax] = variableSizeBytesLong(varintoverflow, tu64overflow).as[QuantityMax] - private val nodeId: Codec[NodeId] = variableSizeBytesLong(varintoverflow, bytes32).as[NodeId] + private val nodeId: Codec[NodeIdXOnly] = variableSizeBytesLong(varintoverflow, bytes32).as[NodeIdXOnly] private val sendInvoice: Codec[SendInvoice] = variableSizeBytesLong(varintoverflow, provide(SendInvoice())) @@ -81,7 +81,7 @@ object OfferCodecs { if (tlvs.get[Description].isEmpty) { Attempt.failure(MissingRequiredTlv(UInt64(10))) } - if (tlvs.get[NodeId].isEmpty) { + if (tlvs.get[NodeIdXOnly].isEmpty && tlvs.get[Paths].forall(_.paths.isEmpty)) { Attempt.failure(MissingRequiredTlv(UInt64(30))) } Attempt.successful(Offer(tlvs)) @@ -189,7 +189,7 @@ object OfferCodecs { Attempt.failure(MissingRequiredTlv(UInt64(8))) } else if (tlvs.get[Description].isEmpty) { Attempt.failure(MissingRequiredTlv(UInt64(10))) - } else if (tlvs.get[NodeId].isEmpty) { + } else if (tlvs.get[NodeIdXOnly].isEmpty) { Attempt.failure(MissingRequiredTlv(UInt64(30))) } else if (tlvs.get[CreatedAt].isEmpty) { Attempt.failure(MissingRequiredTlv(UInt64(40))) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/Offers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/Offers.scala index 99e8e79e25..e1c3580a74 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/Offers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/Offers.scala @@ -20,7 +20,6 @@ import fr.acinq.bitcoin.Bech32 import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, ByteVector64, Crypto, LexicographicalOrdering} import fr.acinq.eclair.crypto.Sphinx.RouteBlinding.BlindedRoute -import fr.acinq.eclair.message.OnionMessages import fr.acinq.eclair.wire.protocol.OfferCodecs._ import fr.acinq.eclair.wire.protocol.TlvCodecs.genericTlv import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, Feature, Features, InvoiceFeature, MilliSatoshi, TimestampSecond} @@ -65,6 +64,7 @@ object Offers { case class PaymentPathsInfo(paymentInfo: Seq[PaymentInfo]) extends InvoiceTlv case class PaymentConstraints(maxCltvExpiry: CltvExpiry, minHtlc: MilliSatoshi, allowedFeatures: Features[Feature]) + case class PaymentPathsConstraints(paymentConstraints: Seq[PaymentConstraints]) extends InvoiceTlv case class Issuer(issuer: String) extends OfferTlv with InvoiceTlv @@ -73,13 +73,13 @@ object Offers { case class QuantityMax(max: Long) extends OfferTlv - case class NodeId(xonly: ByteVector32) extends OfferTlv with InvoiceTlv { - val nodeId1: PublicKey = PublicKey(2 +: xonly) - val nodeId2: PublicKey = PublicKey(3 +: xonly) + case class NodeIdXOnly(xOnly: ByteVector32) extends OfferTlv with InvoiceTlv { + val nodeId1: PublicKey = PublicKey(2 +: xOnly) + val nodeId2: PublicKey = PublicKey(3 +: xOnly) } - object NodeId { - def apply(publicKey: PublicKey): NodeId = NodeId(xOnlyPublicKey(publicKey)) + object NodeIdXOnly { + def apply(publicKey: PublicKey): NodeIdXOnly = NodeIdXOnly(xOnlyPublicKey(publicKey)) } case class SendInvoice() extends OfferTlv with InvoiceTlv @@ -128,7 +128,7 @@ object Offers { case class Offer(records: TlvStream[OfferTlv]) { - require(records.get[NodeId].nonEmpty, "bolt 12 offers must provide a node id") + require(records.get[NodeIdXOnly].nonEmpty || records.get[Paths].exists(_.paths.nonEmpty), "bolt 12 offers must provide a node id or a blinded path") require(records.get[Description].nonEmpty, "bolt 12 offers must provide a description") val offerId: ByteVector32 = rootHash(removeSignature(records), offerTlvCodec) @@ -153,7 +153,8 @@ object Offers { val quantityMin: Option[Long] = records.get[QuantityMin].map(_.min) val quantityMax: Option[Long] = records.get[QuantityMax].map(_.max) - val nodeIdXOnly: ByteVector32 = records.get[NodeId].get.xonly + val nodeIdXOnly: NodeIdXOnly = + records.get[NodeIdXOnly].getOrElse(NodeIdXOnly(records.get[Paths].get.paths.head.blindedNodes.last.blindedPublicKey)) val sendInvoice: Boolean = records.get[SendInvoice].nonEmpty @@ -161,9 +162,8 @@ object Offers { val signature: Option[ByteVector64] = records.get[Signature].map(_.signature) - val contact: Seq[OnionMessages.Destination] = - records.get[Paths].flatMap(_.paths.headOption).map(OnionMessages.BlindedPath).map(Seq(_)) - .getOrElse(Seq(PublicKey(2.toByte +: nodeIdXOnly), PublicKey(3.toByte +: nodeIdXOnly)).map(nodeId => OnionMessages.Recipient(nodeId, None, None))) + val contact: Either[Seq[BlindedRoute], NodeIdXOnly] = + records.get[Paths].map(_.paths).map(Left(_)).getOrElse(Right(records.get[NodeIdXOnly].get)) def sign(key: PrivateKey): Offer = { val sig = signSchnorr(Offer.signatureTag, rootHash(records, offerTlvCodec), key) @@ -172,7 +172,7 @@ object Offers { def checkSignature(): Boolean = { signature match { - case Some(sig) => verifySchnorr(Offer.signatureTag, rootHash(removeSignature(records), offerTlvCodec), sig, nodeIdXOnly) + case Some(sig) => verifySchnorr(Offer.signatureTag, rootHash(removeSignature(records), offerTlvCodec), sig, nodeIdXOnly.xOnly) case None => false } } @@ -201,7 +201,7 @@ object Offers { if (chain != Block.LivenetGenesisBlock.hash) Some(Chains(Seq(chain))) else None, amount_opt.map(Amount), Some(Description(description)), - Some(NodeId(nodeId)), + Some(NodeIdXOnly(xOnlyPublicKey(nodeId))), if (!features.isEmpty) Some(FeaturesTlv(features.unscoped())) else None, ).flatten Offer(TlvStream(tlvs)) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/TlvCodecs.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/TlvCodecs.scala index 42798b4ad0..67d7836029 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/TlvCodecs.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/TlvCodecs.scala @@ -118,7 +118,7 @@ object TlvCodecs { val genericTlv: Codec[GenericTlv] = (("tag" | varint) :: variableSizeBytesLong(varintoverflow, bytes)).as[GenericTlv] - private val unknownTlv = genericTlv.exmap(validateUnknownTlv, validateUnknownTlv) + private val unknownTlv = genericTlv.exmap[GenericTlv](validateUnknownTlv, validateUnknownTlv) private def tag[T <: Tlv](codec: DiscriminatorCodec[T, UInt64], record: Either[GenericTlv, T]): UInt64 = record match { case Left(generic) => generic.tag diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala b/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala index e7366e416e..7a374fa10b 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala @@ -199,7 +199,8 @@ object TestConstants { blockchainWatchdogSources = blockchainWatchdogSources, onionMessageConfig = OnionMessageConfig( relayPolicy = RelayAll, - timeout = 1 minute + timeout = 1 minute, + maxAttempts = 2, ), purgeInvoicesInterval = None, ) @@ -341,7 +342,8 @@ object TestConstants { blockchainWatchdogSources = blockchainWatchdogSources, onionMessageConfig = OnionMessageConfig( relayPolicy = RelayAll, - timeout = 1 minute + timeout = 1 minute, + maxAttempts = 2, ), purgeInvoicesInterval = None ) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/ChannelIntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/ChannelIntegrationSpec.scala index 7f81b3ff9a..390f752b85 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/ChannelIntegrationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/ChannelIntegrationSpec.scala @@ -156,8 +156,8 @@ abstract class ChannelIntegrationSpec extends IntegrationSpec { val preimage = randomBytes32() val paymentHash = Crypto.sha256(preimage) // A sends a payment to F - val paymentReq = SendPaymentToNode(100000000 msat, Bolt11Invoice(Block.RegtestGenesisBlock.hash, None, paymentHash, nodes("F").nodeParams.privateKey, Left("test"), finalCltvExpiryDelta), maxAttempts = 1, routeParams = integrationTestRouteParams) val paymentSender = TestProbe() + val paymentReq = SendPaymentToNode(paymentSender.ref, 100000000 msat, Bolt11Invoice(Block.RegtestGenesisBlock.hash, None, paymentHash, nodes("F").nodeParams.privateKey, Left("test"), finalCltvExpiryDelta), maxAttempts = 1, routeParams = integrationTestRouteParams) paymentSender.send(nodes("A").paymentInitiator, paymentReq) val paymentId = paymentSender.expectMsgType[UUID] // F gets the htlc @@ -379,7 +379,7 @@ abstract class ChannelIntegrationSpec extends IntegrationSpec { def send(amountMsat: MilliSatoshi, paymentHandler: ActorRef, paymentInitiator: ActorRef): UUID = { sender.send(paymentHandler, ReceivePayment(Some(amountMsat), Left("1 coffee"))) val invoice = sender.expectMsgType[Invoice] - val sendReq = SendPaymentToNode(amountMsat, invoice, maxAttempts = 1, routeParams = integrationTestRouteParams) + val sendReq = SendPaymentToNode(sender.ref, amountMsat, invoice, maxAttempts = 1, routeParams = integrationTestRouteParams) sender.send(paymentInitiator, sendReq) sender.expectMsgType[UUID] } @@ -695,7 +695,7 @@ abstract class AnchorChannelIntegrationSpec extends ChannelIntegrationSpec { val invoice = sender.expectMsgType[Invoice] // then we make the actual payment - sender.send(nodes("C").paymentInitiator, SendPaymentToNode(amountMsat, invoice, maxAttempts = 1, routeParams = integrationTestRouteParams)) + sender.send(nodes("C").paymentInitiator, SendPaymentToNode(sender.ref, amountMsat, invoice, maxAttempts = 1, routeParams = integrationTestRouteParams)) val paymentId = sender.expectMsgType[UUID] val ps = sender.expectMsgType[PaymentSent](60 seconds) assert(ps.id == paymentId) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/OffersIntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/OffersIntegrationSpec.scala new file mode 100644 index 0000000000..a302ee786a --- /dev/null +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/OffersIntegrationSpec.scala @@ -0,0 +1,103 @@ +/* + * 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.integration + +import akka.pattern.pipe +import akka.testkit.TestProbe +import akka.util.Timeout +import com.typesafe.config.ConfigFactory +import akka.actor.typed.scaladsl.adapter.actorRefAdapter +import fr.acinq.bitcoin.scalacompat.SatoshiLong +import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher +import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.{Watch, WatchFundingConfirmed} +import fr.acinq.eclair.channel.{ChannelStateChanged, NORMAL} +import fr.acinq.eclair.message.OnionMessages +import fr.acinq.eclair.payment.Bolt12Invoice +import fr.acinq.eclair.wire.protocol.MessageOnionCodecs.perHopPayloadCodec +import fr.acinq.eclair.wire.protocol.Offers.Offer +import fr.acinq.eclair.wire.protocol.OnionMessagePayloadTlv.Invoice +import fr.acinq.eclair.wire.protocol.TlvStream +import fr.acinq.eclair.{EclairImpl, Features, MilliSatoshiLong, PayOfferResponse, randomBytes32} + +import scala.concurrent.Await +import scala.concurrent.ExecutionContext.Implicits.global +import scala.concurrent.duration._ +import scala.jdk.CollectionConverters._ + +class OffersIntegrationSpec extends IntegrationSpec { + implicit val timeout: Timeout = FiniteDuration(30, SECONDS) + + test("setup eclair nodes") { + instantiateEclairNode("A", ConfigFactory.parseMap(Map("eclair.node-alias" -> "A", "eclair.server.port" -> 30700, "eclair.api.port" -> 30780, s"eclair.features.${Features.OnionMessages.rfcName}" -> "optional", "eclair.onion-messages.relay-policy" -> "relay-all").asJava).withFallback(commonConfig)) + instantiateEclairNode("B", ConfigFactory.parseMap(Map("eclair.node-alias" -> "B", "eclair.server.port" -> 30701, "eclair.api.port" -> 30781, s"eclair.features.${Features.OnionMessages.rfcName}" -> "optional", "eclair.onion-messages.relay-policy" -> "relay-all").asJava).withFallback(commonConfig)) + + val sender = TestProbe() + val eventListener = TestProbe() + nodes.values.foreach(_.system.eventStream.subscribe(eventListener.ref, classOf[ChannelStateChanged])) + + connect(nodes("B"), nodes("A"), 1000000 sat, 0 msat) + + val numberOfChannels = 1 + val channelEndpointsCount = 2 * numberOfChannels + + // we make sure all channels have set up their WatchConfirmed for the funding tx + awaitCond({ + val watches = nodes.values.foldLeft(Set.empty[Watch[_]]) { + case (watches, setup) => + setup.watcher ! ZmqWatcher.ListWatches(sender.ref) + watches ++ sender.expectMsgType[Set[Watch[_]]] + } + watches.count(_.isInstanceOf[WatchFundingConfirmed]) == channelEndpointsCount + }, max = 20 seconds, interval = 1 second) + + // confirming the funding tx + generateBlocks(2) + + within(60 seconds) { + var count = 0 + while (count < channelEndpointsCount) { + if (eventListener.expectMsgType[ChannelStateChanged](60 seconds).currentState == NORMAL) count = count + 1 + } + } + } + + test("simple offer") { + val alice = new EclairImpl(nodes("A")) + val bob = new EclairImpl(nodes("B")) + + val eventListener = TestProbe() + nodes("A").system.eventStream.subscribe(eventListener.ref, classOf[OnionMessages.ReceiveMessage]) + + val offer = Offer(Some(100000 msat), "test offer", nodes("A").nodeParams.nodeId, Features.empty, nodes("A").nodeParams.chainHash) + + val probe = TestProbe() + bob.payOffer(offer, 100000 msat, 1).pipeTo(probe.ref) + + val preimage = randomBytes32() + val dummyInvoice = Await.result(alice.receive(Left("dummy bolt11 invoice"), Some(100000 msat), None, None, Some(preimage)), 10 seconds) + + val invoiceRequestPayload = eventListener.expectMsgType[OnionMessages.ReceiveMessage].finalPayload + val invoiceRequest = invoiceRequestPayload.invoiceRequest.get.request + val invoiceRequestReplyPath = invoiceRequestPayload.replyPath.get.blindedRoute + val invoice = Bolt12Invoice(offer, invoiceRequest, preimage, nodes("A").nodeParams.privateKey, dummyInvoice.minFinalCltvExpiryDelta, Features.empty) + val encodedInvoice = perHopPayloadCodec.encode(TlvStream(Invoice(invoice))).require.bytes + alice.sendOnionMessage(Nil, Right(invoiceRequestReplyPath), None, encodedInvoice) + + val response = probe.expectMsgType[PayOfferResponse](1 minute) + assert(response.preimage contains preimage.toHex) + } +} diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/PaymentIntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/PaymentIntegrationSpec.scala index 635a17013c..017a2c7bd9 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/PaymentIntegrationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/PaymentIntegrationSpec.scala @@ -163,7 +163,7 @@ class PaymentIntegrationSpec extends IntegrationSpec { assert(invoice.paymentMetadata.nonEmpty) // then we make the actual payment - sender.send(nodes("A").paymentInitiator, SendPaymentToNode(amountMsat, invoice, routeParams = integrationTestRouteParams, maxAttempts = 1)) + sender.send(nodes("A").paymentInitiator, SendPaymentToNode(sender.ref, amountMsat, invoice, routeParams = integrationTestRouteParams, maxAttempts = 1)) val paymentId = sender.expectMsgType[UUID] val ps = sender.expectMsgType[PaymentSent] assert(ps.id == paymentId) @@ -189,7 +189,7 @@ class PaymentIntegrationSpec extends IntegrationSpec { sender.send(nodes("D").paymentHandler, ReceivePayment(Some(amountMsat), Left("1 coffee"))) val invoice = sender.expectMsgType[Invoice] // then we make the actual payment, do not randomize the route to make sure we route through node B - val sendReq = SendPaymentToNode(amountMsat, invoice, routeParams = integrationTestRouteParams, maxAttempts = 5) + val sendReq = SendPaymentToNode(sender.ref, amountMsat, invoice, routeParams = integrationTestRouteParams, maxAttempts = 5) sender.send(nodes("A").paymentInitiator, sendReq) // A will receive an error from B that include the updated channel update, then will retry the payment val paymentId = sender.expectMsgType[UUID] @@ -230,7 +230,7 @@ class PaymentIntegrationSpec extends IntegrationSpec { sender.send(nodes("D").paymentHandler, ReceivePayment(Some(amountMsat), Left("1 coffee"))) val invoice = sender.expectMsgType[Invoice] // then we make the payment (B-C has a smaller capacity than A-B and C-D) - val sendReq = SendPaymentToNode(amountMsat, invoice, routeParams = integrationTestRouteParams, maxAttempts = 5) + val sendReq = SendPaymentToNode(sender.ref, amountMsat, invoice, routeParams = integrationTestRouteParams, maxAttempts = 5) sender.send(nodes("A").paymentInitiator, sendReq) // A will first receive an error from C, then retry and route around C: A->B->E->C->D sender.expectMsgType[UUID] @@ -241,7 +241,7 @@ class PaymentIntegrationSpec extends IntegrationSpec { val sender = TestProbe() val amount = 100000000 msat val unknownInvoice = Bolt11Invoice(Block.RegtestGenesisBlock.hash, Some(amount), randomBytes32(), nodes("D").nodeParams.privateKey, Left("test"), finalCltvExpiryDelta) - val invoice = SendPaymentToNode(amount, unknownInvoice, routeParams = integrationTestRouteParams, maxAttempts = 5) + val invoice = SendPaymentToNode(sender.ref, amount, unknownInvoice, routeParams = integrationTestRouteParams, maxAttempts = 5) sender.send(nodes("A").paymentInitiator, invoice) // A will receive an error from D and won't retry @@ -261,7 +261,7 @@ class PaymentIntegrationSpec extends IntegrationSpec { val invoice = sender.expectMsgType[Invoice] // A send payment of only 1 mBTC - val sendReq = SendPaymentToNode(100000000 msat, invoice, routeParams = integrationTestRouteParams, maxAttempts = 5) + val sendReq = SendPaymentToNode(sender.ref, 100000000 msat, invoice, routeParams = integrationTestRouteParams, maxAttempts = 5) sender.send(nodes("A").paymentInitiator, sendReq) // A will first receive an IncorrectPaymentAmount error from D @@ -281,7 +281,7 @@ class PaymentIntegrationSpec extends IntegrationSpec { val invoice = sender.expectMsgType[Invoice] // A send payment of 6 mBTC - val sendReq = SendPaymentToNode(600000000 msat, invoice, routeParams = integrationTestRouteParams, maxAttempts = 5) + val sendReq = SendPaymentToNode(sender.ref, 600000000 msat, invoice, routeParams = integrationTestRouteParams, maxAttempts = 5) sender.send(nodes("A").paymentInitiator, sendReq) // A will first receive an IncorrectPaymentAmount error from D @@ -301,7 +301,7 @@ class PaymentIntegrationSpec extends IntegrationSpec { val invoice = sender.expectMsgType[Invoice] // A send payment of 3 mBTC, more than asked but it should still be accepted - val sendReq = SendPaymentToNode(300000000 msat, invoice, routeParams = integrationTestRouteParams, maxAttempts = 5) + val sendReq = SendPaymentToNode(sender.ref, 300000000 msat, invoice, routeParams = integrationTestRouteParams, maxAttempts = 5) sender.send(nodes("A").paymentInitiator, sendReq) sender.expectMsgType[UUID] } @@ -314,7 +314,7 @@ class PaymentIntegrationSpec extends IntegrationSpec { sender.send(nodes("D").paymentHandler, ReceivePayment(Some(amountMsat), Left("1 payment"))) val invoice = sender.expectMsgType[Invoice] - val sendReq = SendPaymentToNode(amountMsat, invoice, routeParams = integrationTestRouteParams, maxAttempts = 5) + val sendReq = SendPaymentToNode(sender.ref, amountMsat, invoice, routeParams = integrationTestRouteParams, maxAttempts = 5) sender.send(nodes("A").paymentInitiator, sendReq) sender.expectMsgType[UUID] sender.expectMsgType[PaymentSent] // the payment FSM will also reply to the sender after the payment is completed @@ -329,7 +329,7 @@ class PaymentIntegrationSpec extends IntegrationSpec { val invoice = sender.expectMsgType[Invoice] // the payment is requesting to use a capacity-optimized route which will select node G even though it's a bit more expensive - sender.send(nodes("A").paymentInitiator, SendPaymentToNode(amountMsat, invoice, maxAttempts = 1, routeParams = integrationTestRouteParams.copy(heuristics = Left(WeightRatios(0, 0, 0, 1, RelayFees(0 msat, 0)))))) + sender.send(nodes("A").paymentInitiator, SendPaymentToNode(sender.ref, amountMsat, invoice, maxAttempts = 1, routeParams = integrationTestRouteParams.copy(heuristics = Left(WeightRatios(0, 0, 0, 1, RelayFees(0 msat, 0)))))) sender.expectMsgType[UUID] val ps = sender.expectMsgType[PaymentSent] ps.parts.foreach(part => assert(part.route.getOrElse(Nil).exists(_.nodeId == nodes("G").nodeParams.nodeId))) @@ -343,7 +343,7 @@ class PaymentIntegrationSpec extends IntegrationSpec { val invoice = sender.expectMsgType[Invoice] assert(invoice.features.hasFeature(Features.BasicMultiPartPayment)) - sender.send(nodes("B").paymentInitiator, SendPaymentToNode(amount, invoice, maxAttempts = 5, routeParams = integrationTestRouteParams)) + sender.send(nodes("B").paymentInitiator, SendPaymentToNode(sender.ref, amount, invoice, maxAttempts = 5, routeParams = integrationTestRouteParams)) val paymentId = sender.expectMsgType[UUID] val paymentSent = sender.expectMsgType[PaymentSent](max = 30 seconds) assert(paymentSent.id == paymentId, paymentSent) @@ -386,7 +386,7 @@ class PaymentIntegrationSpec extends IntegrationSpec { val canSend = sender.expectMsgType[Relayer.OutgoingChannels].channels.map(_.commitments.availableBalanceForSend).sum assert(canSend > amount) - sender.send(nodes("B").paymentInitiator, SendPaymentToNode(amount, invoice, maxAttempts = 1, routeParams = integrationTestRouteParams)) + sender.send(nodes("B").paymentInitiator, SendPaymentToNode(sender.ref, amount, invoice, maxAttempts = 1, routeParams = integrationTestRouteParams)) val paymentId = sender.expectMsgType[UUID] val paymentFailed = sender.expectMsgType[PaymentFailed](max = 30 seconds) assert(paymentFailed.id == paymentId, paymentFailed) @@ -409,7 +409,7 @@ class PaymentIntegrationSpec extends IntegrationSpec { val invoice = sender.expectMsgType[Invoice] assert(invoice.features.hasFeature(Features.BasicMultiPartPayment)) - sender.send(nodes("D").paymentInitiator, SendPaymentToNode(amount, invoice, maxAttempts = 3, routeParams = integrationTestRouteParams)) + sender.send(nodes("D").paymentInitiator, SendPaymentToNode(sender.ref, amount, invoice, maxAttempts = 3, routeParams = integrationTestRouteParams)) val paymentId = sender.expectMsgType[UUID] val paymentSent = sender.expectMsgType[PaymentSent](max = 30 seconds) assert(paymentSent.id == paymentId, paymentSent) @@ -441,7 +441,7 @@ class PaymentIntegrationSpec extends IntegrationSpec { val canSend = sender.expectMsgType[Relayer.OutgoingChannels].channels.map(_.commitments.availableBalanceForSend).sum assert(canSend < amount) - sender.send(nodes("D").paymentInitiator, SendPaymentToNode(amount, invoice, maxAttempts = 1, routeParams = integrationTestRouteParams)) + sender.send(nodes("D").paymentInitiator, SendPaymentToNode(sender.ref, amount, invoice, maxAttempts = 1, routeParams = integrationTestRouteParams)) val paymentId = sender.expectMsgType[UUID] val paymentFailed = sender.expectMsgType[PaymentFailed](max = 30 seconds) assert(paymentFailed.id == paymentId, paymentFailed) @@ -603,7 +603,7 @@ class PaymentIntegrationSpec extends IntegrationSpec { // We put most of the capacity C <-> D on D's side. sender.send(nodes("D").paymentHandler, ReceivePayment(Some(8000000000L msat), Left("plz send everything"))) val pr1 = sender.expectMsgType[Invoice] - sender.send(nodes("C").paymentInitiator, SendPaymentToNode(8000000000L msat, pr1, maxAttempts = 3, routeParams = integrationTestRouteParams)) + sender.send(nodes("C").paymentInitiator, SendPaymentToNode(sender.ref, 8000000000L msat, pr1, maxAttempts = 3, routeParams = integrationTestRouteParams)) sender.expectMsgType[UUID] sender.expectMsgType[PaymentSent](max = 30 seconds) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/PerformanceIntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/PerformanceIntegrationSpec.scala index 0072313230..c85340b549 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/PerformanceIntegrationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/PerformanceIntegrationSpec.scala @@ -86,7 +86,7 @@ class PerformanceIntegrationSpec extends IntegrationSpec { sender.send(nodes("B").paymentHandler, ReceivePayment(Some(amountMsat), Left("1 coffee"))) val pr = sender.expectMsgType[Invoice] // then we make the actual payment - sender.send(nodes("A").paymentInitiator, PaymentInitiator.SendPaymentToNode(amountMsat, pr, routeParams = integrationTestRouteParams, maxAttempts = 1)) + sender.send(nodes("A").paymentInitiator, PaymentInitiator.SendPaymentToNode(sender.ref, amountMsat, pr, routeParams = integrationTestRouteParams, maxAttempts = 1)) val paymentId = sender.expectMsgType[UUID] sender.expectMsgType[PreimageReceived] val ps = sender.expectMsgType[PaymentSent] diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/fixtures/MinimalNodeFixture.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/fixtures/MinimalNodeFixture.scala index 0667d515e1..5acfd6e32c 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/fixtures/MinimalNodeFixture.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/fixtures/MinimalNodeFixture.scala @@ -284,7 +284,7 @@ object MinimalNodeFixture extends Assertions with EitherValues { val invoice = sender.expectMsgType[Bolt11Invoice] val routeParams = node1.nodeParams.routerConf.pathFindingExperimentConf.experiments.values.head.getDefaultRouteParams - sender.send(node1.paymentInitiator, PaymentInitiator.SendPaymentToNode(amount, invoice, maxAttempts = 1, routeParams = routeParams, extraEdges = hints.flatMap(Bolt11Invoice.toExtraEdges(_, node2.nodeParams.nodeId)), blockUntilComplete = true)) + sender.send(node1.paymentInitiator, PaymentInitiator.SendPaymentToNode(sender.ref, amount, invoice, maxAttempts = 1, routeParams = routeParams, extraEdges = hints.flatMap(Bolt11Invoice.toExtraEdges(_, node2.nodeParams.nodeId)), blockUntilComplete = true)) sender.expectMsgType[PaymentEvent] match { case e: PaymentSent => Right(e) case e: PaymentFailed => Left(e) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/message/PostmanSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/message/PostmanSpec.scala index 66297b5535..5067290b59 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/message/PostmanSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/message/PostmanSpec.scala @@ -68,7 +68,7 @@ class PostmanSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("applicat testKit.system.eventStream ! EventStream.Publish(ReceiveMessage(payload, Some(pathId))) testKit.system.eventStream ! EventStream.Publish(ReceiveMessage(payload, Some(pathId))) - messageRecipient.expectMessage(Response(payload)) + messageRecipient.expectMessage(Response(payload, pathId)) messageRecipient.expectNoMessage() } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/Bolt12InvoiceSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/Bolt12InvoiceSpec.scala index 5cfddb4c22..af8471f066 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/Bolt12InvoiceSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/Bolt12InvoiceSpec.scala @@ -45,7 +45,7 @@ class Bolt12InvoiceSpec extends AnyFunSuite { val (nodeKey, payerKey, chain) = (randomKey(), randomKey(), randomBytes32()) val offer = Offer(Some(10000 msat), "test offer", nodeKey.publicKey, Features.empty, chain) val request = InvoiceRequest(offer, 11000 msat, 1, Features.empty, payerKey, chain) - val invoice = Bolt12Invoice(offer, request, randomBytes32(), nodeKey, Features.empty) + val invoice = Bolt12Invoice(offer, request, randomBytes32(), nodeKey, CltvExpiryDelta(20), Features.empty) assert(invoice.isValidFor(offer, request)) assert(invoice.checkSignature()) assert(!invoice.checkRefundSignature()) @@ -70,7 +70,7 @@ class Bolt12InvoiceSpec extends AnyFunSuite { val (nodeKey, payerKey, chain) = (randomKey(), randomKey(), randomBytes32()) val offer = Offer(Some(10000 msat), "test offer", nodeKey.publicKey, Features.empty, chain) val request = InvoiceRequest(offer, 11000 msat, 1, Features.empty, payerKey, chain) - val invoice = Bolt12Invoice(offer, request, randomBytes32(), nodeKey, Features.empty) + val invoice = Bolt12Invoice(offer, request, randomBytes32(), nodeKey, CltvExpiryDelta(20), Features.empty) assert(invoice.isValidFor(offer, request)) assert(!invoice.isValidFor(Offer(None, "test offer", randomKey().publicKey, Features.empty, chain), request)) // amount must match the offer @@ -83,7 +83,7 @@ class Bolt12InvoiceSpec extends AnyFunSuite { assert(withExtendedDescription.isValidFor(offer, request)) // nodeId must match the offer val otherNodeKey = randomKey() - val withOtherNodeId = signInvoice(Bolt12Invoice(TlvStream(invoice.records.records.map { case NodeId(_) => NodeId(otherNodeKey.publicKey) case x => x }.toSeq), None), otherNodeKey) + val withOtherNodeId = signInvoice(Bolt12Invoice(TlvStream(invoice.records.records.map { case NodeIdXOnly(_) => NodeIdXOnly(otherNodeKey.publicKey) case x => x }.toSeq), None), otherNodeKey) assert(!withOtherNodeId.isValidFor(offer, request)) // offerId must match the offer val withOtherOfferId = signInvoice(Bolt12Invoice(TlvStream(invoice.records.records.map { case OfferId(_) => OfferId(randomBytes32()) case x => x }.toSeq), None), nodeKey) @@ -97,8 +97,8 @@ class Bolt12InvoiceSpec extends AnyFunSuite { val (nodeKey, payerKey, chain) = (randomKey(), randomKey(), randomBytes32()) val offer = Offer(Some(15000 msat), "test offer", nodeKey.publicKey, Features(VariableLengthOnion -> Mandatory), chain) val request = InvoiceRequest(offer, 15000 msat, 1, Features(VariableLengthOnion -> Mandatory), payerKey, chain) - assert(request.quantity_opt == None) // when paying for a single item, the quantity field must not be present - val invoice = Bolt12Invoice(offer, request, randomBytes32(), nodeKey, Features(VariableLengthOnion -> Mandatory, BasicMultiPartPayment -> Optional)) + assert(request.quantity_opt.isEmpty) // when paying for a single item, the quantity field must not be present + val invoice = Bolt12Invoice(offer, request, randomBytes32(), nodeKey, CltvExpiryDelta(20), Features(VariableLengthOnion -> Mandatory, BasicMultiPartPayment -> Optional)) assert(invoice.isValidFor(offer, request)) val withInvalidFeatures = signInvoice(Bolt12Invoice(TlvStream(invoice.records.records.map { case FeaturesTlv(_) => FeaturesTlv(Features(VariableLengthOnion -> Mandatory, BasicMultiPartPayment -> Mandatory)) case x => x }.toSeq), None), nodeKey) assert(!withInvalidFeatures.isValidFor(offer, request)) @@ -125,7 +125,7 @@ class Bolt12InvoiceSpec extends AnyFunSuite { val signature = signSchnorr(InvoiceRequest.signatureTag, rootHash(TlvStream(tlvs), invoiceRequestTlvCodec), payerKey) InvoiceRequest(TlvStream(tlvs :+ Signature(signature))) } - val withPayerDetails = Bolt12Invoice(offer, requestWithPayerDetails, randomBytes32(), nodeKey, Features.empty) + val withPayerDetails = Bolt12Invoice(offer, requestWithPayerDetails, randomBytes32(), nodeKey, CltvExpiryDelta(20), Features.empty) assert(withPayerDetails.isValidFor(offer, requestWithPayerDetails)) assert(!withPayerDetails.isValidFor(offer, request)) val withOtherPayerInfo = signInvoice(Bolt12Invoice(TlvStream(withPayerDetails.records.records.map { case PayerInfo(_) => PayerInfo(hex"deadbeef") case x => x }.toSeq), None), nodeKey) @@ -140,7 +140,7 @@ class Bolt12InvoiceSpec extends AnyFunSuite { val (nodeKey, payerKey, chain) = (randomKey(), randomKey(), randomBytes32()) val offer = Offer(Some(5000 msat), "test offer", nodeKey.publicKey, Features.empty, chain) val request = InvoiceRequest(offer, 5000 msat, 1, Features.empty, payerKey, chain) - val invoice = Bolt12Invoice(offer, request, randomBytes32(), nodeKey, Features.empty) + val invoice = Bolt12Invoice(offer, request, randomBytes32(), nodeKey, CltvExpiryDelta(20), Features.empty) assert(!invoice.isExpired()) assert(invoice.isValidFor(offer, request)) val expiredInvoice1 = signInvoice(Bolt12Invoice(TlvStream(invoice.records.records.map { case CreatedAt(_) => CreatedAt(0 unixsec) case x => x }), None), nodeKey) @@ -162,7 +162,7 @@ class Bolt12InvoiceSpec extends AnyFunSuite { CreatedAt(TimestampSecond.now()), PaymentHash(Crypto.sha256(randomBytes32())), OfferId(offerBtc.offerId), - NodeId(nodeKey.publicKey), + NodeIdXOnly(nodeKey.publicKey), Amount(amount), Description(offerBtc.description), PayerKey(payerKey.publicKey) @@ -177,7 +177,7 @@ class Bolt12InvoiceSpec extends AnyFunSuite { CreatedAt(TimestampSecond.now()), PaymentHash(Crypto.sha256(randomBytes32())), OfferId(offerBtc.offerId), - NodeId(nodeKey.publicKey), + NodeIdXOnly(nodeKey.publicKey), Amount(amount), Description(offerBtc.description), PayerKey(payerKey.publicKey) @@ -192,7 +192,7 @@ class Bolt12InvoiceSpec extends AnyFunSuite { CreatedAt(TimestampSecond.now()), PaymentHash(Crypto.sha256(randomBytes32())), OfferId(offerBtc.offerId), - NodeId(nodeKey.publicKey), + NodeIdXOnly(nodeKey.publicKey), Amount(amount), Description(offerBtc.description), PayerKey(payerKey.publicKey) @@ -201,7 +201,7 @@ class Bolt12InvoiceSpec extends AnyFunSuite { Bolt12Invoice(TlvStream(tlvs :+ Signature(signature)), None) } assert(!invoiceOtherChain.isValidFor(offerBtc, requestBtc)) - val offerOtherChains = Offer(TlvStream(Seq(Chains(Seq(chain1, chain2)), Amount(amount), Description("testnets offer"), NodeId(nodeKey.publicKey)))) + val offerOtherChains = Offer(TlvStream(Seq(Chains(Seq(chain1, chain2)), Amount(amount), Description("testnets offer"), NodeIdXOnly(nodeKey.publicKey)))) val requestOtherChains = InvoiceRequest(offerOtherChains, amount, 1, Features.empty, payerKey, chain1) val invoiceOtherChains = { val tlvs: Seq[InvoiceTlv] = Seq( @@ -209,7 +209,7 @@ class Bolt12InvoiceSpec extends AnyFunSuite { CreatedAt(TimestampSecond.now()), PaymentHash(Crypto.sha256(randomBytes32())), OfferId(offerOtherChains.offerId), - NodeId(nodeKey.publicKey), + NodeIdXOnly(nodeKey.publicKey), Amount(amount), Description(offerOtherChains.description), PayerKey(payerKey.publicKey) @@ -224,7 +224,7 @@ class Bolt12InvoiceSpec extends AnyFunSuite { CreatedAt(TimestampSecond.now()), PaymentHash(Crypto.sha256(randomBytes32())), OfferId(offerOtherChains.offerId), - NodeId(nodeKey.publicKey), + NodeIdXOnly(nodeKey.publicKey), Amount(amount), Description(offerOtherChains.description), PayerKey(payerKey.publicKey) @@ -241,8 +241,8 @@ class Bolt12InvoiceSpec extends AnyFunSuite { val nodeKey = PrivateKey(hex"c6a75116a91dc5ff741b079c32c8ce7544656b98f047fb0c0fa011bfb2bb3c05") val payerKey = PrivateKey(hex"7dd30ec116470c5f7f00af2c7e84968e28cdb43083b33ee832decbe73ec07f1a") val Success(offer) = Offer.decode("lno1qgsyxjtl6luzd9t3pr62xr7eemp6awnejusgf6gw45q75vcfqqqqqqqgqvqcdgq2pd3xzumfvvsx7enxv4epug9kku8f4e9nuef5lv59yrkdc24t5mtrym62cg085w5wtqkp0rsuly") - assert(offer.amount == Some(100_000 msat)) - assert(offer.nodeIdXOnly == xOnlyPublicKey(nodeKey.publicKey)) + assert(offer.amount.contains(100_000 msat)) + assert(offer.nodeIdXOnly.xOnly == xOnlyPublicKey(nodeKey.publicKey)) assert(offer.chains == Seq(Block.TestnetGenesisBlock.hash)) val request = InvoiceRequest(offer, 100_000 msat, 1, Features.empty, payerKey, Block.TestnetGenesisBlock.hash) val Success(invoice) = Bolt12Invoice.fromString("lni1qvsyxjtl6luzd9t3pr62xr7eemp6awnejusgf6gw45q75vcfqqqqqqqyyp53zuupqkwxpmdq0tjg58ntat5ujpejlvyn92r0l5xzh4wru8e5zzqrqxr2qzstvfshx6tryphkven9wgxqq83qk6msaxhyk0n9xnajs5swehp24wndvvn0ftppu7363evzc9uwrnujvg95tuyy05nqkcdetsaljgq4u6789jllc54qrpjrzzn3c38dj3tscu5qgcs2y4lj5gqlvq50uu7sce478j3j0l599nxfs6svx2cfefgn4a0675893wtzuckqfwlcrcq0qspa9zynlpdk9zzechehkemgaksklylxhr7yfjfx6h696th327nm4nsf52xzq0ukchx69g00c4vvk6kzc5jyklneyy05l9tef7a5jcjn5") @@ -254,14 +254,14 @@ class Bolt12InvoiceSpec extends AnyFunSuite { val nodeKey = PrivateKey(hex"c6a75116a91dc5ff741b079c32c8ce7544656b98f047fb0c0fa011bfb2bb3c05") val payerKey = PrivateKey(hex"94c7a21a11efa16c5f73b093dc136d9525e2ff40ea7a958c43c1f6004bf6a676") val Success(offer) = Offer.decode("lno1pqpzwyq2pf382mrtyphkven9wgtqzqgcqy9pug9kku8f4e9nuef5lv59yrkdc24t5mtrym62cg085w5wtqkp0rsuly") - assert(offer.amount == Some(10_000 msat)) - assert(offer.nodeIdXOnly == xOnlyPublicKey(nodeKey.publicKey)) + assert(offer.amount.contains(10_000 msat)) + assert(offer.nodeIdXOnly.xOnly == xOnlyPublicKey(nodeKey.publicKey)) assert(offer.chains == Seq(Block.LivenetGenesisBlock.hash)) val request = InvoiceRequest(offer, 50_000 msat, 5, Features.empty, payerKey, Block.LivenetGenesisBlock.hash) val Success(invoice) = Bolt12Invoice.fromString("lni1qss8u47nw2lsgml7fy4jaqwph9f8cl83zfrrhxccvh6076avqzzzv4qgqtp4qzs2vf6kc6eqdanxvetjpsqpug9kku8f4e9nuef5lv59yrkdc24t5mtrym62cg085w5wtqkp0rsulysqzpfxyrat02l8wtgtwuc4h5hw6dxhn0hcpdrtu3dpejfjdlw9h4j3nppxc2qyvg9z0lf2yq7wl9ygd6td4cj7whp3ye4cfxrtu7zq4r2mc0mcdspk3duzv7d0stqyh0upuq8sgr44r7aaluwqfw8pkd9f3cgk7ae2l8rkexznhegr0p7w4mlhvfkvlnr5k2lnw0hhsf6ckys3sst7kng5p7m2pxlvdxl3tan809vkk75j") assert(!invoice.isExpired()) assert(invoice.amount == 50_000.msat) - assert(invoice.quantity == Some(5)) + assert(invoice.quantity.contains(5)) assert(invoice.isValidFor(offer, request)) } @@ -310,7 +310,7 @@ class Bolt12InvoiceSpec extends AnyFunSuite { Description(description), FeaturesTlv(features), Issuer(issuer), - NodeId(nodeKey.publicKey), + NodeIdXOnly(nodeKey.publicKey), Quantity(quantity), PayerKey(payerKey), PayerNote(payerNote), diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentInitiatorSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentInitiatorSpec.scala index a72817e183..e49af3c8ef 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentInitiatorSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentInitiatorSpec.scala @@ -98,7 +98,7 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike import f._ val customRecords = Seq(GenericTlv(500L, hex"01020304"), GenericTlv(501L, hex"d34db33f")) val invoice = Bolt11Invoice(Block.LivenetGenesisBlock.hash, None, paymentHash, priv_c.privateKey, Left("test"), Channel.MIN_CLTV_EXPIRY_DELTA) - val req = SendPaymentToNode(finalAmount, invoice, 1, userCustomTlvs = customRecords, routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams) + val req = SendPaymentToNode(sender.ref, finalAmount, invoice, 1, userCustomTlvs = customRecords, routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams) sender.send(initiator, req) sender.expectMsgType[UUID] payFsm.expectMsgType[SendPaymentConfig] @@ -131,7 +131,7 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike Bolt11Invoice.InvoiceFeatures(Features(Map(VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory), unknown = Set(UnknownFeature(42)))) ) val invoice = Bolt11Invoice("lnbc", Some(finalAmount), TimestampSecond.now(), randomKey().publicKey, taggedFields, ByteVector.empty) - val req = SendPaymentToNode(finalAmount + 100.msat, invoice, 1, routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams) + val req = SendPaymentToNode(sender.ref, finalAmount + 100.msat, invoice, 1, routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams) sender.send(initiator, req) val id = sender.expectMsgType[UUID] val fail = sender.expectMsgType[PaymentFailed] @@ -169,8 +169,8 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike import f._ val finalExpiryDelta = CltvExpiryDelta(24) val invoice = Bolt11Invoice(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, priv_c.privateKey, Left("Some MPP invoice"), finalExpiryDelta, features = featuresWithMpp) - val req = SendPaymentToNode(finalAmount, invoice, 1, routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams) - assert(req.finalExpiry(nodeParams.currentBlockHeight) == (finalExpiryDelta + 1).toCltvExpiry(nodeParams.currentBlockHeight)) + val req = SendPaymentToNode(sender.ref, finalAmount, invoice, 1, routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams) + assert(req.finalExpiry(nodeParams.currentBlockHeight) === (finalExpiryDelta + 1).toCltvExpiry(nodeParams.currentBlockHeight)) sender.send(initiator, req) val id = sender.expectMsgType[UUID] payFsm.expectMsg(SendPaymentConfig(id, id, None, paymentHash, finalAmount, c, Upstream.Local(id), Some(invoice), storeInDb = true, publishEvent = true, recordPathFindingMetrics = true, Nil)) @@ -193,7 +193,7 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike test("forward multi-part payment") { f => import f._ val invoice = Bolt11Invoice(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, priv_c.privateKey, Left("Some invoice"), CltvExpiryDelta(18), features = featuresWithMpp) - val req = SendPaymentToNode(finalAmount + 100.msat, invoice, 1, routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams) + val req = SendPaymentToNode(sender.ref, finalAmount + 100.msat, invoice, 1, routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams) sender.send(initiator, req) val id = sender.expectMsgType[UUID] multiPartPayFsm.expectMsg(SendPaymentConfig(id, id, None, paymentHash, finalAmount + 100.msat, c, Upstream.Local(id), Some(invoice), storeInDb = true, publishEvent = true, recordPathFindingMetrics = true, Nil)) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/OffersSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/OffersSpec.scala index 22e7636dc2..87fc065d86 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/OffersSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/OffersSpec.scala @@ -53,7 +53,7 @@ class OffersSpec extends AnyFunSuite { assert(offer.amount.isEmpty) assert(offer.signature.isEmpty) assert(offer.description == "Offer by rusty's node") - assert(offer.nodeIdXOnly == nodeId) + assert(offer.nodeIdXOnly.xOnly == nodeId) } test("basic signed offer") { @@ -62,29 +62,29 @@ class OffersSpec extends AnyFunSuite { assert(signedOffer.checkSignature()) assert(signedOffer.amount.isEmpty) assert(signedOffer.description == "Offer by rusty's node") - assert(signedOffer.nodeIdXOnly == nodeId) + assert(signedOffer.nodeIdXOnly.xOnly == nodeId) } test("offer with amount and quantity") { val encoded = "lno1pqqnyzsmx5cx6umpwssx6atvw35j6ut4v9h8g6t50ysx7enxv4epgrmjw4ehgcm0wfczucm0d5hxzagkqyq3ugztng063cqx783exlm97ekyprnd4rsu5u5w5sez9fecrhcuc3ykq5" val Success(offer) = Offer.decode(encoded) - assert(offer.amount == Some(50 msat)) + assert(offer.amount.contains(50 msat)) assert(offer.signature.isEmpty) assert(offer.description == "50msat multi-quantity offer") - assert(offer.nodeIdXOnly == nodeId) - assert(offer.issuer == Some("rustcorp.com.au")) - assert(offer.quantityMin == Some(1)) + assert(offer.nodeIdXOnly.xOnly == nodeId) + assert(offer.issuer.contains("rustcorp.com.au")) + assert(offer.quantityMin.contains(1)) } test("signed offer with amount and quantity") { val encodedSigned = "lno1pqqnyzsmx5cx6umpwssx6atvw35j6ut4v9h8g6t50ysx7enxv4epgrmjw4ehgcm0wfczucm0d5hxzagkqyq3ugztng063cqx783exlm97ekyprnd4rsu5u5w5sez9fecrhcuc3ykqhcypjju7unu05vav8yvhn27lztf46k9gqlga8uvu4uq62kpuywnu6me8srgh2q7puczukr8arectaapfl5d4rd6uc7st7tnqf0ttx39n40s" val Success(signedOffer) = Offer.decode(encodedSigned) assert(signedOffer.checkSignature()) - assert(signedOffer.amount == Some(50 msat)) + assert(signedOffer.amount.contains(50 msat)) assert(signedOffer.description == "50msat multi-quantity offer") - assert(signedOffer.nodeIdXOnly == nodeId) - assert(signedOffer.issuer == Some("rustcorp.com.au")) - assert(signedOffer.quantityMin == Some(1)) + assert(signedOffer.nodeIdXOnly.xOnly == nodeId) + assert(signedOffer.issuer.contains("rustcorp.com.au")) + assert(signedOffer.quantityMin.contains(1)) } test("decode invalid offer") { @@ -142,7 +142,7 @@ class OffersSpec extends AnyFunSuite { test("check that invoice request matches offer (chain compatibility)") { { - val offer = Offer(TlvStream(Seq(Amount(100 msat), Description("offer without chains"), NodeId(randomKey().publicKey)))) + val offer = Offer(TlvStream(Seq(Amount(100 msat), Description("offer without chains"), NodeIdXOnly(randomKey().publicKey)))) val payerKey = randomKey() val request = { val tlvs: Seq[InvoiceRequestTlv] = Seq( @@ -162,7 +162,7 @@ class OffersSpec extends AnyFunSuite { } { val (chain1, chain2) = (randomBytes32(), randomBytes32()) - val offer = Offer(TlvStream(Seq(Chains(Seq(chain1, chain2)), Amount(100 msat), Description("offer with chains"), NodeId(randomKey().publicKey)))) + val offer = Offer(TlvStream(Seq(Chains(Seq(chain1, chain2)), Amount(100 msat), Description("offer with chains"), NodeIdXOnly(randomKey().publicKey)))) val payerKey = randomKey() val request1 = InvoiceRequest(offer, 100 msat, 1, Features.empty, payerKey, chain1) assert(request1.isValidFor(offer)) @@ -179,7 +179,7 @@ class OffersSpec extends AnyFunSuite { val offer = Offer(TlvStream( Amount(500 msat), Description("offer for multiple items"), - NodeId(randomKey().publicKey), + NodeIdXOnly(randomKey().publicKey), QuantityMin(3), QuantityMax(10), )) diff --git a/eclair-node/src/main/scala/fr/acinq/eclair/api/directives/ExtraDirectives.scala b/eclair-node/src/main/scala/fr/acinq/eclair/api/directives/ExtraDirectives.scala index e7889d02ac..d9ce249b10 100644 --- a/eclair-node/src/main/scala/fr/acinq/eclair/api/directives/ExtraDirectives.scala +++ b/eclair-node/src/main/scala/fr/acinq/eclair/api/directives/ExtraDirectives.scala @@ -27,6 +27,7 @@ import fr.acinq.eclair.ApiTypes.ChannelIdentifier import fr.acinq.eclair.api.serde.FormParamExtractors._ import fr.acinq.eclair.api.serde.JsonSupport._ import fr.acinq.eclair.payment.Bolt11Invoice +import fr.acinq.eclair.wire.protocol.Offers.Offer import fr.acinq.eclair.{MilliSatoshi, ShortChannelId, TimestampSecond} import scala.concurrent.Future @@ -50,6 +51,7 @@ trait ExtraDirectives extends Directives { val ignoreNodeIdsFormParam: NameUnmarshallerReceptacle[List[PublicKey]] = "ignoreNodeIds".as[List[PublicKey]](pubkeyListUnmarshaller) val ignoreShortChannelIdsFormParam: NameUnmarshallerReceptacle[List[ShortChannelId]] = "ignoreShortChannelIds".as[List[ShortChannelId]](shortChannelIdsUnmarshaller) val maxFeeMsatFormParam: NameReceptacle[MilliSatoshi] = "maxFeeMsat".as[MilliSatoshi] + val offerFormParam: NameUnmarshallerReceptacle[Offer] = "offer".as[Offer](offerUnmarshaller) // custom directive to fail with HTTP 404 (and JSON response) if the element was not found def completeOrNotFound[T](fut: Future[Option[T]])(implicit marshaller: ToResponseMarshaller[T]): Route = onComplete(fut) { diff --git a/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Payment.scala b/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Payment.scala index f6b68fbded..74b5cd4233 100644 --- a/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Payment.scala +++ b/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Payment.scala @@ -94,6 +94,13 @@ trait Payment { } } - val paymentRoutes: Route = usableBalances ~ payInvoice ~ sendToNode ~ sendToRoute ~ getSentInfo ~ getReceivedInfo + val payOffer: Route = postRequest("payoffer") { implicit t => + formFields(offerFormParam, amountMsatFormParam, "quantity".as[Long].?, "maxAttempts".as[Int].?, "maxFeeFlatSat".as[Satoshi].?, "maxFeePct".as[Double].?, "externalId".?, "pathFindingExperimentName".?) { + case (offer, amountMsat, quantity_opt, maxAttempts_opt, maxFeeFlat_opt, maxFeePct_opt, externalId_opt, pathFindingExperimentName_opt) => + complete(eclairApi.payOffer(offer, amountMsat, quantity_opt.getOrElse(1), externalId_opt, maxAttempts_opt, maxFeeFlat_opt, maxFeePct_opt, pathFindingExperimentName_opt)) + } + } + + val paymentRoutes: Route = usableBalances ~ payInvoice ~ sendToNode ~ sendToRoute ~ getSentInfo ~ getReceivedInfo ~ payOffer } diff --git a/eclair-node/src/main/scala/fr/acinq/eclair/api/serde/FormParamExtractors.scala b/eclair-node/src/main/scala/fr/acinq/eclair/api/serde/FormParamExtractors.scala index 4744dae6df..bb79daf2e4 100644 --- a/eclair-node/src/main/scala/fr/acinq/eclair/api/serde/FormParamExtractors.scala +++ b/eclair-node/src/main/scala/fr/acinq/eclair/api/serde/FormParamExtractors.scala @@ -27,6 +27,7 @@ import fr.acinq.eclair.crypto.Sphinx import fr.acinq.eclair.io.NodeURI import fr.acinq.eclair.payment.Bolt11Invoice import fr.acinq.eclair.wire.protocol.MessageOnionCodecs.blindedRouteCodec +import fr.acinq.eclair.wire.protocol.Offers.Offer import fr.acinq.eclair.{MilliSatoshi, ShortChannelId, TimestampSecond} import scodec.bits.ByteVector @@ -74,6 +75,8 @@ object FormParamExtractors { blindedRouteCodec.decode(ByteVector.fromValidHex(str).bits).require.value } + val offerUnmarshaller: Unmarshaller[String, Offer] = Unmarshaller.strict { Offer.decode(_).get } + private def listUnmarshaller[T](unmarshal: String => T): Unmarshaller[String, List[T]] = Unmarshaller.strict { str => Try(serialization.read[List[String]](str).map(unmarshal)) .recoverWith(_ => Try(str.split(",").toList.map(unmarshal))) From 8ef5dfe1f2ae2c1ba5b596f6583ffd04ede31df3 Mon Sep 17 00:00:00 2001 From: Thomas HUET Date: Fri, 15 Apr 2022 17:34:52 +0200 Subject: [PATCH 116/121] Make payment secret optional again --- .../main/scala/fr/acinq/eclair/Eclair.scala | 12 +-- .../fr/acinq/eclair/db/DualDatabases.scala | 2 +- .../scala/fr/acinq/eclair/db/PaymentsDb.scala | 6 +- .../fr/acinq/eclair/db/pg/PgPaymentsDb.scala | 2 +- .../eclair/db/sqlite/SqlitePaymentsDb.scala | 2 +- .../fr/acinq/eclair/payment/Invoice.scala | 12 +++ .../payment/receive/MultiPartHandler.scala | 13 +-- .../eclair/payment/relay/NodeRelay.scala | 8 +- .../eclair/payment/relay/NodeRelayer.scala | 4 +- .../send/MultiPartPaymentLifecycle.scala | 2 +- .../eclair/payment/send/PaymentError.scala | 2 - .../payment/send/PaymentInitiator.scala | 93 +++++++++---------- .../eclair/wire/protocol/PaymentOnion.scala | 12 +-- .../fr/acinq/eclair/channel/FuzzySpec.scala | 2 +- .../ChannelStateTestsHelperMethods.scala | 2 +- .../channel/states/f/ShutdownStateSpec.scala | 4 +- .../integration/OffersIntegrationSpec.scala | 1 + .../eclair/payment/MultiPartHandlerSpec.scala | 42 ++++----- .../MultiPartPaymentLifecycleSpec.scala | 34 +++---- .../eclair/payment/PaymentInitiatorSpec.scala | 14 +-- .../eclair/payment/PaymentLifecycleSpec.scala | 48 +++++----- .../eclair/payment/PaymentPacketSpec.scala | 34 +++---- .../payment/PostRestartHtlcCleanerSpec.scala | 4 +- .../payment/relay/NodeRelayerSpec.scala | 36 +++---- .../eclair/payment/relay/RelayerSpec.scala | 14 +-- .../wire/protocol/PaymentOnionSpec.scala | 4 +- 26 files changed, 208 insertions(+), 201 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala index 5cdca7dc67..ed26babfd0 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala @@ -137,11 +137,11 @@ trait Eclair { def channelStats(from: TimestampSecond, to: TimestampSecond)(implicit timeout: Timeout): Future[Seq[Stats]] - def getInvoice(paymentHash: ByteVector32)(implicit timeout: Timeout): Future[Option[Bolt11Invoice]] + def getInvoice(paymentHash: ByteVector32)(implicit timeout: Timeout): Future[Option[Invoice]] - def pendingInvoices(from: TimestampSecond, to: TimestampSecond)(implicit timeout: Timeout): Future[Seq[Bolt11Invoice]] + def pendingInvoices(from: TimestampSecond, to: TimestampSecond)(implicit timeout: Timeout): Future[Seq[Invoice]] - def allInvoices(from: TimestampSecond, to: TimestampSecond)(implicit timeout: Timeout): Future[Seq[Bolt11Invoice]] + def allInvoices(from: TimestampSecond, to: TimestampSecond)(implicit timeout: Timeout): Future[Seq[Invoice]] def deleteInvoice(paymentHash: ByteVector32): Future[String] @@ -438,15 +438,15 @@ class EclairImpl(appKit: Kit) extends Eclair with Logging { Future(appKit.nodeParams.db.audit.stats(from.toTimestampMilli, to.toTimestampMilli)) } - override def allInvoices(from: TimestampSecond, to: TimestampSecond)(implicit timeout: Timeout): Future[Seq[Bolt11Invoice]] = Future { + override def allInvoices(from: TimestampSecond, to: TimestampSecond)(implicit timeout: Timeout): Future[Seq[Invoice]] = Future { appKit.nodeParams.db.payments.listIncomingPayments(from.toTimestampMilli, to.toTimestampMilli).map(_.invoice) } - override def pendingInvoices(from: TimestampSecond, to: TimestampSecond)(implicit timeout: Timeout): Future[Seq[Bolt11Invoice]] = Future { + override def pendingInvoices(from: TimestampSecond, to: TimestampSecond)(implicit timeout: Timeout): Future[Seq[Invoice]] = Future { appKit.nodeParams.db.payments.listPendingIncomingPayments(from.toTimestampMilli, to.toTimestampMilli).map(_.invoice) } - override def getInvoice(paymentHash: ByteVector32)(implicit timeout: Timeout): Future[Option[Bolt11Invoice]] = Future { + override def getInvoice(paymentHash: ByteVector32)(implicit timeout: Timeout): Future[Option[Invoice]] = Future { appKit.nodeParams.db.payments.getIncomingPayment(paymentHash).map(_.invoice) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/DualDatabases.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/DualDatabases.scala index 1fa137dcf8..41b226f8a9 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/DualDatabases.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/DualDatabases.scala @@ -294,7 +294,7 @@ case class DualPaymentsDb(primary: PaymentsDb, secondary: PaymentsDb) extends Pa primary.listPaymentsOverview(limit) } - override def addIncomingPayment(pr: Bolt11Invoice, preimage: ByteVector32, paymentType: String): Unit = { + override def addIncomingPayment(pr: Invoice, preimage: ByteVector32, paymentType: String): Unit = { runAsync(secondary.addIncomingPayment(pr, preimage, paymentType)) primary.addIncomingPayment(pr, preimage, paymentType) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala index 2e60ea707a..615da166c5 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala @@ -30,7 +30,7 @@ trait PaymentsDb extends IncomingPaymentsDb with OutgoingPaymentsDb with Payment trait IncomingPaymentsDb { /** Add a new expected incoming payment (not yet received). */ - def addIncomingPayment(pr: Bolt11Invoice, preimage: ByteVector32, paymentType: String = PaymentType.Standard): Unit + def addIncomingPayment(pr: Invoice, preimage: ByteVector32, paymentType: String = PaymentType.Standard): Unit /** * Mark an incoming payment as received (paid). The received amount may exceed the invoice amount. @@ -98,13 +98,13 @@ case object PaymentType { * At first it is in a pending state once the invoice has been generated, then will become either a success (if * we receive a valid HTLC) or a failure (if the invoice expires). * - * @param invoice Bolt 11 invoice. + * @param invoice invoice. * @param paymentPreimage pre-image associated with the invoice's payment_hash. * @param paymentType distinguish different payment types (standard, swaps, etc). * @param createdAt absolute time in milli-seconds since UNIX epoch when the invoice was generated. * @param status current status of the payment. */ -case class IncomingPayment(invoice: Bolt11Invoice, +case class IncomingPayment(invoice: Invoice, paymentPreimage: ByteVector32, paymentType: String, createdAt: TimestampMilli, diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgPaymentsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgPaymentsDb.scala index e82c755a34..2f0a80ee01 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgPaymentsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgPaymentsDb.scala @@ -219,7 +219,7 @@ class PgPaymentsDb(implicit ds: DataSource, lock: PgLock) extends PaymentsDb wit } } - override def addIncomingPayment(invoice: Bolt11Invoice, preimage: ByteVector32, paymentType: String): Unit = withMetrics("payments/add-incoming", DbBackends.Postgres) { + override def addIncomingPayment(invoice: Invoice, preimage: ByteVector32, paymentType: String): Unit = withMetrics("payments/add-incoming", DbBackends.Postgres) { withLock { pg => using(pg.prepareStatement("INSERT INTO payments.received (payment_hash, payment_preimage, payment_type, payment_request, created_at, expire_at) VALUES (?, ?, ?, ?, ?, ?)")) { statement => statement.setString(1, invoice.paymentHash.toHex) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala index 7a61795bae..98a321fc51 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala @@ -231,7 +231,7 @@ class SqlitePaymentsDb(val sqlite: Connection) extends PaymentsDb with Logging { } } - override def addIncomingPayment(invoice: Bolt11Invoice, preimage: ByteVector32, paymentType: String): Unit = withMetrics("payments/add-incoming", DbBackends.Sqlite) { + override def addIncomingPayment(invoice: Invoice, preimage: ByteVector32, paymentType: String): Unit = withMetrics("payments/add-incoming", DbBackends.Sqlite) { using(sqlite.prepareStatement("INSERT INTO received_payments (payment_hash, payment_preimage, payment_type, payment_request, created_at, expire_at) VALUES (?, ?, ?, ?, ?, ?)")) { statement => statement.setBytes(1, invoice.paymentHash.toArray) statement.setBytes(2, preimage.toArray) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Invoice.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Invoice.scala index 545c9cea81..e36e427045 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Invoice.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Invoice.scala @@ -86,3 +86,15 @@ object Invoice { } } } + +case class DummyInvoice(amount_opt: Option[MilliSatoshi], + createdAt: TimestampSecond, + nodeId: PublicKey, + paymentHash: ByteVector32, + paymentSecret: Option[ByteVector32], + paymentMetadata: Option[ByteVector], + description: Either[String, ByteVector32], + extraEdges: Seq[Invoice.ExtraEdge], + relativeExpiry: FiniteDuration, + minFinalCltvExpiryDelta: CltvExpiryDelta, + features: Features[InvoiceFeature]) extends Invoice diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/MultiPartHandler.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/MultiPartHandler.scala index f2bc05cf55..bd6c196d30 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/MultiPartHandler.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/MultiPartHandler.scala @@ -26,13 +26,14 @@ import fr.acinq.bitcoin.scalacompat.{ByteVector32, Crypto} import fr.acinq.eclair.channel.{CMD_FAIL_HTLC, CMD_FULFILL_HTLC, RES_SUCCESS} import fr.acinq.eclair.db._ import fr.acinq.eclair.payment.Monitoring.{Metrics, Tags} -import fr.acinq.eclair.payment.{IncomingPaymentPacket, PaymentMetadataReceived, PaymentReceived, Invoice} +import fr.acinq.eclair.payment.{Bolt11Invoice, DummyInvoice, IncomingPaymentPacket, Invoice, PaymentMetadataReceived, PaymentReceived} import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{FeatureSupport, Features, InvoiceFeature, Logs, MilliSatoshi, NodeParams, randomBytes32} +import fr.acinq.eclair.{FeatureSupport, Features, InvoiceFeature, Logs, MilliSatoshi, NodeParams, TimestampSecond, randomBytes32} import scodec.bits.HexStringSyntax import fr.acinq.eclair.payment.Bolt11Invoice.ExtraHop -import fr.acinq.eclair.payment.Bolt11Invoice +import java.util.concurrent.TimeUnit +import scala.concurrent.duration.FiniteDuration import scala.util.{Failure, Success, Try} /** @@ -99,7 +100,7 @@ class MultiPartHandler(nodeParams: NodeParams, register: ActorRef, db: IncomingP Features[InvoiceFeature](Features.PaymentSecret -> FeatureSupport.Mandatory, Features.VariableLengthOnion -> FeatureSupport.Mandatory) } // Insert a fake invoice and then restart the incoming payment handler - val invoice = Bolt11Invoice(nodeParams.chainHash, amount, paymentHash, nodeParams.privateKey, desc, nodeParams.channelConf.minFinalExpiryDelta, paymentSecret = p.payload.paymentSecret, features = features) + val invoice = DummyInvoice(amount, TimestampSecond.now(), nodeParams.privateKey.publicKey, paymentHash, p.payload.paymentSecret, None, desc, Nil, FiniteDuration(Bolt11Invoice.DEFAULT_EXPIRY_SECONDS, TimeUnit.SECONDS), nodeParams.channelConf.minFinalExpiryDelta, features) log.debug("generated fake invoice={} from amount={} (KeySend)", invoice.toString, amount) db.addIncomingPayment(invoice, paymentPreimage, paymentType = PaymentType.KeySend) ctx.self ! p @@ -319,10 +320,10 @@ object MultiPartHandler { if (payment.payload.amount < payment.payload.totalAmount && !invoice.features.hasFeature(Features.BasicMultiPartPayment)) { log.warning("received multi-part payment but invoice doesn't support it for amount={} totalAmount={}", payment.add.amountMsat, payment.payload.totalAmount) false - } else if (payment.payload.amount < payment.payload.totalAmount && !invoice.paymentSecret.contains(payment.payload.paymentSecret)) { + } else if (payment.payload.amount < payment.payload.totalAmount && invoice.paymentSecret != payment.payload.paymentSecret) { log.warning("received multi-part payment with invalid secret={} for amount={} totalAmount={}", payment.payload.paymentSecret, payment.add.amountMsat, payment.payload.totalAmount) false - } else if (!invoice.paymentSecret.contains(payment.payload.paymentSecret)) { + } else if (invoice.paymentSecret != payment.payload.paymentSecret) { log.warning("received payment with invalid secret={} for amount={} totalAmount={}", payment.payload.paymentSecret, payment.add.amountMsat, payment.payload.totalAmount) false } else { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/NodeRelay.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/NodeRelay.scala index 3b323700f9..05abf32558 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/NodeRelay.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/NodeRelay.scala @@ -163,7 +163,7 @@ class NodeRelay private(nodeParams: NodeParams, register: ActorRef, relayId: UUID, paymentHash: ByteVector32, - paymentSecret: ByteVector32, + paymentSecret: Option[ByteVector32], context: ActorContext[NodeRelay.Command], outgoingPaymentFactory: NodeRelay.OutgoingPaymentFactory) { @@ -271,13 +271,13 @@ class NodeRelay private(nodeParams: NodeParams, val paymentSecret = payloadOut.paymentSecret.get // NB: we've verified that there was a payment secret in validateRelay if (Features(features).hasFeature(Features.BasicMultiPartPayment)) { context.log.debug("sending the payment to non-trampoline recipient using MPP") - val payment = SendMultiPartPayment(payFsmAdapters, paymentSecret, payloadOut.outgoingNodeId, payloadOut.amountToForward, payloadOut.outgoingCltv, nodeParams.maxPaymentAttempts, payloadOut.paymentMetadata, extraEdges, routeParams) + val payment = SendMultiPartPayment(payFsmAdapters, Some(paymentSecret), payloadOut.outgoingNodeId, payloadOut.amountToForward, payloadOut.outgoingCltv, nodeParams.maxPaymentAttempts, payloadOut.paymentMetadata, extraEdges, routeParams) val payFSM = outgoingPaymentFactory.spawnOutgoingPayFSM(context, paymentCfg, multiPart = true) payFSM ! payment payFSM } else { context.log.debug("sending the payment to non-trampoline recipient without MPP") - val finalPayload = PaymentOnion.createSinglePartPayload(payloadOut.amountToForward, payloadOut.outgoingCltv, paymentSecret, payloadOut.paymentMetadata) + val finalPayload = PaymentOnion.createSinglePartPayload(payloadOut.amountToForward, payloadOut.outgoingCltv, Some(paymentSecret), payloadOut.paymentMetadata) val payment = SendPaymentToNode(payFsmAdapters, payloadOut.outgoingNodeId, finalPayload, nodeParams.maxPaymentAttempts, extraEdges, routeParams) val payFSM = outgoingPaymentFactory.spawnOutgoingPayFSM(context, paymentCfg, multiPart = false) payFSM ! payment @@ -287,7 +287,7 @@ class NodeRelay private(nodeParams: NodeParams, context.log.debug("sending the payment to the next trampoline node") val payFSM = outgoingPaymentFactory.spawnOutgoingPayFSM(context, paymentCfg, multiPart = true) val paymentSecret = randomBytes32() // we generate a new secret to protect against probing attacks - val payment = SendMultiPartPayment(payFsmAdapters, paymentSecret, payloadOut.outgoingNodeId, payloadOut.amountToForward, payloadOut.outgoingCltv, nodeParams.maxPaymentAttempts, None, routeParams = routeParams, additionalTlvs = Seq(OnionPaymentPayloadTlv.TrampolineOnion(packetOut))) + val payment = SendMultiPartPayment(payFsmAdapters, Some(paymentSecret), payloadOut.outgoingNodeId, payloadOut.amountToForward, payloadOut.outgoingCltv, nodeParams.maxPaymentAttempts, None, routeParams = routeParams, additionalTlvs = Seq(OnionPaymentPayloadTlv.TrampolineOnion(packetOut))) payFSM ! payment payFSM } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/NodeRelayer.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/NodeRelayer.scala index 20ae577674..71c6723396 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/NodeRelayer.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/NodeRelayer.scala @@ -38,7 +38,7 @@ object NodeRelayer { // @formatter:off sealed trait Command case class Relay(nodeRelayPacket: IncomingPaymentPacket.NodeRelayPacket) extends Command - case class RelayComplete(childHandler: ActorRef[NodeRelay.Command], paymentHash: ByteVector32, paymentSecret: ByteVector32) extends Command + case class RelayComplete(childHandler: ActorRef[NodeRelay.Command], paymentHash: ByteVector32, paymentSecret: Option[ByteVector32]) extends Command private[relay] case class GetPendingPayments(replyTo: akka.actor.ActorRef) extends Command // @formatter:on @@ -48,7 +48,7 @@ object NodeRelayer { case _: GetPendingPayments => Logs.mdc() } - case class PaymentKey(paymentHash: ByteVector32, paymentSecret: ByteVector32) + case class PaymentKey(paymentHash: ByteVector32, paymentSecret: Option[ByteVector32]) /** * @param children a map of pending payments. We must index by both payment hash and payment secret because we may diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/MultiPartPaymentLifecycle.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/MultiPartPaymentLifecycle.scala index 0cf2f9e155..42d0e93e33 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/MultiPartPaymentLifecycle.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/MultiPartPaymentLifecycle.scala @@ -319,7 +319,7 @@ object MultiPartPaymentLifecycle { * @param userCustomTlvs when provided, additional user-defined custom tlvs that will be added to the onion sent to the target node. */ case class SendMultiPartPayment(replyTo: ActorRef, - paymentSecret: ByteVector32, + paymentSecret: Option[ByteVector32], targetNodeId: PublicKey, totalAmount: MilliSatoshi, targetExpiry: CltvExpiry, diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentError.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentError.scala index 7248019cd3..8ac3ed8bb8 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentError.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentError.scala @@ -26,8 +26,6 @@ object PaymentError { sealed trait InvalidInvoice extends PaymentError /** The invoice contains a feature we don't support. */ case class UnsupportedFeatures(features: Features[InvoiceFeature]) extends InvalidInvoice { override def getMessage: String = s"unsupported invoice features: ${features.toByteVector.toHex}" } - /** The invoice is missing a payment secret. */ - case object PaymentSecretMissing extends InvalidInvoice { override def getMessage: String = "invalid invoice: payment secret is missing" } // @formatter:on // @formatter:off diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentInitiator.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentInitiator.scala index 2bdd4ff683..5ae91771a6 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentInitiator.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentInitiator.scala @@ -54,20 +54,17 @@ class PaymentInitiator(nodeParams: NodeParams, outgoingPaymentFactory: PaymentIn } val paymentCfg = SendPaymentConfig(paymentId, paymentId, r.externalId, r.paymentHash, r.recipientAmount, r.recipientNodeId, Upstream.Local(paymentId), Some(r.invoice), storeInDb = true, publishEvent = true, recordPathFindingMetrics = true, Nil) val finalExpiry = r.finalExpiry(nodeParams.currentBlockHeight) - r.invoice.paymentSecret match { - case _ if !nodeParams.features.invoiceFeatures().areSupported(r.invoice.features) => - replyTo ! PaymentFailed(paymentId, r.paymentHash, LocalFailure(r.recipientAmount, Nil, UnsupportedFeatures(r.invoice.features)) :: Nil) - case None => - replyTo ! PaymentFailed(paymentId, r.paymentHash, LocalFailure(r.recipientAmount, Nil, PaymentSecretMissing) :: Nil) - case Some(paymentSecret) if r.invoice.features.hasFeature(Features.BasicMultiPartPayment) && nodeParams.features.hasFeature(BasicMultiPartPayment) => - val fsm = outgoingPaymentFactory.spawnOutgoingMultiPartPayment(context, paymentCfg) - fsm ! SendMultiPartPayment(self, paymentSecret, r.recipientNodeId, r.recipientAmount, finalExpiry, r.maxAttempts, r.invoice.paymentMetadata, r.extraEdges, r.routeParams, userCustomTlvs = r.userCustomTlvs) - context become main(pending + (paymentId -> PendingPaymentToNode(replyTo, r))) - case Some(paymentSecret) => - val finalPayload = PaymentOnion.createSinglePartPayload(r.recipientAmount, finalExpiry, paymentSecret, r.invoice.paymentMetadata, r.userCustomTlvs) - val fsm = outgoingPaymentFactory.spawnOutgoingPayment(context, paymentCfg) - fsm ! PaymentLifecycle.SendPaymentToNode(self, r.recipientNodeId, finalPayload, r.maxAttempts, r.extraEdges, r.routeParams) - context become main(pending + (paymentId -> PendingPaymentToNode(replyTo, r))) + if (!nodeParams.features.invoiceFeatures().areSupported(r.invoice.features)) { + replyTo ! PaymentFailed(paymentId, r.paymentHash, LocalFailure(r.recipientAmount, Nil, UnsupportedFeatures(r.invoice.features)) :: Nil) + } else if (r.invoice.features.hasFeature(Features.BasicMultiPartPayment) && nodeParams.features.hasFeature(BasicMultiPartPayment)) { + val fsm = outgoingPaymentFactory.spawnOutgoingMultiPartPayment(context, paymentCfg) + fsm ! SendMultiPartPayment(self, r.invoice.paymentSecret, r.recipientNodeId, r.recipientAmount, finalExpiry, r.maxAttempts, r.invoice.paymentMetadata, r.extraEdges, r.routeParams, userCustomTlvs = r.userCustomTlvs) + context become main(pending + (paymentId -> PendingPaymentToNode(replyTo, r))) + } else { + val finalPayload = PaymentOnion.createSinglePartPayload(r.recipientAmount, finalExpiry, r.invoice.paymentSecret, r.invoice.paymentMetadata, r.userCustomTlvs) + val fsm = outgoingPaymentFactory.spawnOutgoingPayment(context, paymentCfg) + fsm ! PaymentLifecycle.SendPaymentToNode(self, r.recipientNodeId, finalPayload, r.maxAttempts, r.extraEdges, r.routeParams) + context become main(pending + (paymentId -> PendingPaymentToNode(replyTo, r))) } case r: SendSpontaneousPayment => @@ -106,8 +103,6 @@ class PaymentInitiator(nodeParams: NodeParams, outgoingPaymentFactory: PaymentIn val additionalHops = r.trampolineNodes.sliding(2).map(hop => NodeHop(hop.head, hop(1), CltvExpiryDelta(0), 0 msat)).toSeq val paymentCfg = SendPaymentConfig(paymentId, parentPaymentId, r.externalId, r.paymentHash, r.recipientAmount, r.recipientNodeId, Upstream.Local(paymentId), Some(r.invoice), storeInDb = true, publishEvent = true, recordPathFindingMetrics = false, additionalHops) r.trampolineNodes match { - case _ if r.invoice.paymentSecret.isEmpty => - sender() ! PaymentFailed(paymentId, r.paymentHash, LocalFailure(r.recipientAmount, Nil, PaymentSecretMissing) :: Nil) case trampoline :: recipient :: Nil => log.info(s"sending trampoline payment to $recipient with trampoline=$trampoline, trampoline fees=${r.trampolineFees}, expiry delta=${r.trampolineExpiryDelta}") buildTrampolinePayment(r, trampoline, r.trampolineFees, r.trampolineExpiryDelta) match { @@ -116,7 +111,7 @@ class PaymentInitiator(nodeParams: NodeParams, outgoingPaymentFactory: PaymentIn val trampolineSecret = r.trampolineSecret.getOrElse(randomBytes32()) sender() ! SendPaymentToRouteResponse(paymentId, parentPaymentId, Some(trampolineSecret)) val payFsm = outgoingPaymentFactory.spawnOutgoingPayment(context, paymentCfg) - payFsm ! PaymentLifecycle.SendPaymentToRoute(self, Left(r.route), PaymentOnion.createMultiPartPayload(r.amount, trampolineAmount, trampolineExpiry, trampolineSecret, r.invoice.paymentMetadata, Seq(OnionPaymentPayloadTlv.TrampolineOnion(trampolineOnion))), r.invoice.extraEdges) + payFsm ! PaymentLifecycle.SendPaymentToRoute(self, Left(r.route), PaymentOnion.createMultiPartPayload(r.amount, trampolineAmount, trampolineExpiry, Some(trampolineSecret), r.invoice.paymentMetadata, Seq(OnionPaymentPayloadTlv.TrampolineOnion(trampolineOnion))), r.invoice.extraEdges) context become main(pending + (paymentId -> PendingPaymentToRoute(sender(), r))) case Failure(t) => log.warning("cannot send outgoing trampoline payment: {}", t.getMessage) @@ -125,7 +120,7 @@ class PaymentInitiator(nodeParams: NodeParams, outgoingPaymentFactory: PaymentIn case Nil => sender() ! SendPaymentToRouteResponse(paymentId, parentPaymentId, None) val payFsm = outgoingPaymentFactory.spawnOutgoingPayment(context, paymentCfg) - payFsm ! PaymentLifecycle.SendPaymentToRoute(self, Left(r.route), PaymentOnion.createMultiPartPayload(r.amount, r.recipientAmount, finalExpiry, r.invoice.paymentSecret.get, r.invoice.paymentMetadata), r.invoice.extraEdges) + payFsm ! PaymentLifecycle.SendPaymentToRoute(self, Left(r.route), PaymentOnion.createMultiPartPayload(r.amount, r.recipientAmount, finalExpiry, r.invoice.paymentSecret, r.invoice.paymentMetadata), r.invoice.extraEdges) context become main(pending + (paymentId -> PendingPaymentToRoute(sender(), r))) case _ => sender() ! PaymentFailed(paymentId, r.paymentHash, LocalFailure(r.recipientAmount, Nil, TrampolineMultiNodeNotSupported) :: Nil) @@ -199,9 +194,9 @@ class PaymentInitiator(nodeParams: NodeParams, outgoingPaymentFactory: PaymentIn NodeHop(trampolineNodeId, r.recipientNodeId, trampolineExpiryDelta, trampolineFees) // for now we only use a single trampoline hop ) val finalPayload = if (r.invoice.features.hasFeature(Features.BasicMultiPartPayment)) { - PaymentOnion.createMultiPartPayload(r.recipientAmount, r.recipientAmount, r.finalExpiry(nodeParams.currentBlockHeight), r.invoice.paymentSecret.get, r.invoice.paymentMetadata) + PaymentOnion.createMultiPartPayload(r.recipientAmount, r.recipientAmount, r.finalExpiry(nodeParams.currentBlockHeight), r.invoice.paymentSecret, r.invoice.paymentMetadata) } else { - PaymentOnion.createSinglePartPayload(r.recipientAmount, r.finalExpiry(nodeParams.currentBlockHeight), r.invoice.paymentSecret.get, r.invoice.paymentMetadata) + PaymentOnion.createSinglePartPayload(r.recipientAmount, r.finalExpiry(nodeParams.currentBlockHeight), r.invoice.paymentSecret, r.invoice.paymentMetadata) } // We assume that the trampoline node supports multi-part payments (it should). val trampolinePacket_opt = if (r.invoice.features.hasFeature(Features.TrampolinePaymentPrototype)) { @@ -224,7 +219,7 @@ class PaymentInitiator(nodeParams: NodeParams, outgoingPaymentFactory: PaymentIn buildTrampolinePayment(r, r.trampolineNodeId, trampolineFees, trampolineExpiryDelta).map { case (trampolineAmount, trampolineExpiry, trampolineOnion) => val fsm = outgoingPaymentFactory.spawnOutgoingMultiPartPayment(context, paymentCfg) - fsm ! SendMultiPartPayment(self, trampolineSecret, r.trampolineNodeId, trampolineAmount, trampolineExpiry, nodeParams.maxPaymentAttempts, r.invoice.paymentMetadata, r.invoice.extraEdges, r.routeParams, Seq(OnionPaymentPayloadTlv.TrampolineOnion(trampolineOnion))) + fsm ! SendMultiPartPayment(self, Some(trampolineSecret), r.trampolineNodeId, trampolineAmount, trampolineExpiry, nodeParams.maxPaymentAttempts, r.invoice.paymentMetadata, r.invoice.extraEdges, r.routeParams, Seq(OnionPaymentPayloadTlv.TrampolineOnion(trampolineOnion))) } } @@ -286,14 +281,14 @@ object PaymentInitiator { * Once we have trampoline fee estimation built into the router, the decision to use Trampoline or not should be done * automatically by the router instead of the caller. * - * @param recipientAmount amount that should be received by the final recipient (usually from a Bolt 11 invoice). - * @param invoice Bolt 11 invoice. - * @param trampolineNodeId id of the trampoline node. - * @param trampolineAttempts fees and expiry delta for the trampoline node. If this list contains multiple entries, - * the payment will automatically be retried in case of TrampolineFeeInsufficient errors. - * For example, [(10 msat, 144), (15 msat, 288)] will first send a payment with a fee of 10 - * msat and cltv of 144, and retry with 15 msat and 288 in case an error occurs. - * @param routeParams (optional) parameters to fine-tune the routing algorithm. + * @param recipientAmount amount that should be received by the final recipient (usually from a Bolt 11 invoice). + * @param invoice Bolt 11 invoice. + * @param trampolineNodeId id of the trampoline node. + * @param trampolineAttempts fees and expiry delta for the trampoline node. If this list contains multiple entries, + * the payment will automatically be retried in case of TrampolineFeeInsufficient errors. + * For example, [(10 msat, 144), (15 msat, 288)] will first send a payment with a fee of 10 + * msat and cltv of 144, and retry with 15 msat and 288 in case an error occurs. + * @param routeParams (optional) parameters to fine-tune the routing algorithm. */ case class SendTrampolinePayment(recipientAmount: MilliSatoshi, invoice: Invoice, @@ -303,7 +298,7 @@ object PaymentInitiator { /** * @param recipientAmount amount that should be received by the final recipient (usually from a Bolt 11 invoice). - * @param invoice Bolt 11 invoice. + * @param invoice invoice. * @param maxAttempts maximum number of retries. * @param externalId (optional) externally-controlled identifier (to reconcile between application DB and eclair DB). * @param extraEdges (optional) routing hints (usually from a Bolt 11 invoice). @@ -355,25 +350,25 @@ object PaymentInitiator { * SendPaymentToRouteRequest(250 msat, 600 msat, None, parentId, invoice, CltvExpiryDelta(9), Seq(alice, bob, dave), secret, 100 msat, CltvExpiryDelta(144), Seq(dave, peter)) * SendPaymentToRouteRequest(450 msat, 600 msat, None, parentId, invoice, CltvExpiryDelta(9), Seq(alice, carol, dave), secret, 100 msat, CltvExpiryDelta(144), Seq(dave, peter)) * - * @param amount amount that should be received by the last node in the route (should take trampoline - * fees into account). - * @param recipientAmount amount that should be received by the final recipient (usually from a Bolt 11 invoice). - * This amount may be split between multiple requests if using MPP. - * @param invoice Bolt 11 invoice. - * @param route route to use to reach either the final recipient or the first trampoline node. - * @param externalId (optional) externally-controlled identifier (to reconcile between application DB and eclair DB). - * @param parentId id of the whole payment. When manually sending a multi-part payment, you need to make - * sure all partial payments use the same parentId. If not provided, a random parentId will - * be generated that can be used for the remaining partial payments. - * @param trampolineSecret if trampoline is used, this is a secret to protect the payment to the first trampoline - * node against probing. When manually sending a multi-part payment, you need to make sure - * all partial payments use the same trampolineSecret. - * @param trampolineFees if trampoline is used, fees for the first trampoline node. This value must be the same - * for all partial payments in the set. - * @param trampolineExpiryDelta if trampoline is used, expiry delta for the first trampoline node. This value must be - * the same for all partial payments in the set. - * @param trampolineNodes if trampoline is used, list of trampoline nodes to use (we currently support only a - * single trampoline node). + * @param amount amount that should be received by the last node in the route (should take trampoline + * fees into account). + * @param recipientAmount amount that should be received by the final recipient (usually from a Bolt 11 invoice). + * This amount may be split between multiple requests if using MPP. + * @param invoice Bolt 11 invoice. + * @param route route to use to reach either the final recipient or the first trampoline node. + * @param externalId (optional) externally-controlled identifier (to reconcile between application DB and eclair DB). + * @param parentId id of the whole payment. When manually sending a multi-part payment, you need to make + * sure all partial payments use the same parentId. If not provided, a random parentId will + * be generated that can be used for the remaining partial payments. + * @param trampolineSecret if trampoline is used, this is a secret to protect the payment to the first trampoline + * node against probing. When manually sending a multi-part payment, you need to make sure + * all partial payments use the same trampolineSecret. + * @param trampolineFees if trampoline is used, fees for the first trampoline node. This value must be the same + * for all partial payments in the set. + * @param trampolineExpiryDelta if trampoline is used, expiry delta for the first trampoline node. This value must be + * the same for all partial payments in the set. + * @param trampolineNodes if trampoline is used, list of trampoline nodes to use (we currently support only a + * single trampoline node). */ case class SendPaymentToRoute(amount: MilliSatoshi, recipientAmount: MilliSatoshi, diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/PaymentOnion.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/PaymentOnion.scala index 9691c38820..ab18778971 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/PaymentOnion.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/PaymentOnion.scala @@ -234,7 +234,7 @@ object PaymentOnion { sealed trait FinalPayload extends PerHopPayload with TrampolinePacket with PaymentPacket { val amount: MilliSatoshi val expiry: CltvExpiry - val paymentSecret: ByteVector32 + val paymentSecret: Option[ByteVector32] val totalAmount: MilliSatoshi val paymentPreimage: Option[ByteVector32] val paymentMetadata: Option[ByteVector] @@ -271,7 +271,7 @@ object PaymentOnion { case class FinalTlvPayload(records: TlvStream[OnionPaymentPayloadTlv]) extends FinalPayload { override val amount = records.get[AmountToForward].get.amount override val expiry = records.get[OutgoingCltv].get.cltv - override val paymentSecret = records.get[PaymentData].get.secret + override val paymentSecret = records.get[PaymentData].map(_.secret) override val totalAmount = records.get[PaymentData].map(_.totalAmount match { case MilliSatoshi(0) => amount case totalAmount => totalAmount @@ -298,21 +298,21 @@ object PaymentOnion { NodeRelayPayload(TlvStream(tlvs)) } - def createSinglePartPayload(amount: MilliSatoshi, expiry: CltvExpiry, paymentSecret: ByteVector32, paymentMetadata: Option[ByteVector], userCustomTlvs: Seq[GenericTlv] = Nil): FinalPayload = { + def createSinglePartPayload(amount: MilliSatoshi, expiry: CltvExpiry, paymentSecret: Option[ByteVector32], paymentMetadata: Option[ByteVector], userCustomTlvs: Seq[GenericTlv] = Nil): FinalPayload = { val tlvs = Seq( Some(AmountToForward(amount)), Some(OutgoingCltv(expiry)), - Some(PaymentData(paymentSecret, amount)), + paymentSecret.map(PaymentData(_, amount)), paymentMetadata.map(m => PaymentMetadata(m)) ).flatten FinalTlvPayload(TlvStream(tlvs, userCustomTlvs)) } - def createMultiPartPayload(amount: MilliSatoshi, totalAmount: MilliSatoshi, expiry: CltvExpiry, paymentSecret: ByteVector32, paymentMetadata: Option[ByteVector], additionalTlvs: Seq[OnionPaymentPayloadTlv] = Nil, userCustomTlvs: Seq[GenericTlv] = Nil): FinalPayload = { + def createMultiPartPayload(amount: MilliSatoshi, totalAmount: MilliSatoshi, expiry: CltvExpiry, paymentSecret: Option[ByteVector32], paymentMetadata: Option[ByteVector], additionalTlvs: Seq[OnionPaymentPayloadTlv] = Nil, userCustomTlvs: Seq[GenericTlv] = Nil): FinalPayload = { val tlvs = Seq( Some(AmountToForward(amount)), Some(OutgoingCltv(expiry)), - Some(PaymentData(paymentSecret, totalAmount)), + paymentSecret.map(PaymentData(_, totalAmount)), paymentMetadata.map(m => PaymentMetadata(m)) ).flatten FinalTlvPayload(TlvStream(tlvs ++ additionalTlvs, userCustomTlvs)) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/FuzzySpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/FuzzySpec.scala index 987b686a7c..9d3af27cb5 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/FuzzySpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/FuzzySpec.scala @@ -123,7 +123,7 @@ class FuzzySpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Channe // allow overpaying (no more than 2 times the required amount) val amount = requiredAmount + Random.nextInt(requiredAmount.toLong.toInt).msat val expiry = (Channel.MIN_CLTV_EXPIRY_DELTA + 1).toCltvExpiry(currentBlockHeight = BlockHeight(400000)) - OutgoingPaymentPacket.buildCommand(self, Upstream.Local(UUID.randomUUID()), paymentHash, ChannelHop(null, null, dest, null) :: Nil, PaymentOnion.createSinglePartPayload(amount, expiry, paymentSecret, None)).get._1 + OutgoingPaymentPacket.buildCommand(self, Upstream.Local(UUID.randomUUID()), paymentHash, ChannelHop(null, null, dest, null) :: Nil, PaymentOnion.createSinglePartPayload(amount, expiry, Some(paymentSecret), None)).get._1 } def initiatePaymentOrStop(remaining: Int): Unit = diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala index 6a195691b0..82616501fe 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala @@ -289,7 +289,7 @@ trait ChannelStateTestsBase extends Assertions with Eventually { def makeCmdAdd(amount: MilliSatoshi, cltvExpiryDelta: CltvExpiryDelta, destination: PublicKey, paymentPreimage: ByteVector32, currentBlockHeight: BlockHeight, upstream: Upstream, replyTo: ActorRef = TestProbe().ref): (ByteVector32, CMD_ADD_HTLC) = { val paymentHash: ByteVector32 = Crypto.sha256(paymentPreimage) val expiry = cltvExpiryDelta.toCltvExpiry(currentBlockHeight) - val cmd = OutgoingPaymentPacket.buildCommand(replyTo, upstream, paymentHash, ChannelHop(null, null, destination, null) :: Nil, PaymentOnion.createSinglePartPayload(amount, expiry, randomBytes32(), None)).get._1.copy(commit = false) + val cmd = OutgoingPaymentPacket.buildCommand(replyTo, upstream, paymentHash, ChannelHop(null, null, destination, null) :: Nil, PaymentOnion.createSinglePartPayload(amount, expiry, Some(randomBytes32()), None)).get._1.copy(commit = false) (paymentPreimage, cmd) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/f/ShutdownStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/f/ShutdownStateSpec.scala index a7e5bb68a8..0668a17a0d 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/f/ShutdownStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/f/ShutdownStateSpec.scala @@ -60,7 +60,7 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit val h1 = Crypto.sha256(r1) val amount1 = 300000000 msat val expiry1 = CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight) - val cmd1 = OutgoingPaymentPacket.buildCommand(sender.ref, Upstream.Local(UUID.randomUUID), h1, ChannelHop(null, null, TestConstants.Bob.nodeParams.nodeId, null) :: Nil, PaymentOnion.createSinglePartPayload(amount1, expiry1, randomBytes32(), None)).get._1.copy(commit = false) + val cmd1 = OutgoingPaymentPacket.buildCommand(sender.ref, Upstream.Local(UUID.randomUUID), h1, ChannelHop(null, null, TestConstants.Bob.nodeParams.nodeId, null) :: Nil, PaymentOnion.createSinglePartPayload(amount1, expiry1, Some(randomBytes32()), None)).get._1.copy(commit = false) alice ! cmd1 sender.expectMsgType[RES_SUCCESS[CMD_ADD_HTLC]] val htlc1 = alice2bob.expectMsgType[UpdateAddHtlc] @@ -70,7 +70,7 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit val h2 = Crypto.sha256(r2) val amount2 = 200000000 msat val expiry2 = CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight) - val cmd2 = OutgoingPaymentPacket.buildCommand(sender.ref, Upstream.Local(UUID.randomUUID), h2, ChannelHop(null, null, TestConstants.Bob.nodeParams.nodeId, null) :: Nil, PaymentOnion.createSinglePartPayload(amount2, expiry2, randomBytes32(), None)).get._1.copy(commit = false) + val cmd2 = OutgoingPaymentPacket.buildCommand(sender.ref, Upstream.Local(UUID.randomUUID), h2, ChannelHop(null, null, TestConstants.Bob.nodeParams.nodeId, null) :: Nil, PaymentOnion.createSinglePartPayload(amount2, expiry2, Some(randomBytes32()), None)).get._1.copy(commit = false) alice ! cmd2 sender.expectMsgType[RES_SUCCESS[CMD_ADD_HTLC]] val htlc2 = alice2bob.expectMsgType[UpdateAddHtlc] diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/OffersIntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/OffersIntegrationSpec.scala index a302ee786a..fd52d36b20 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/OffersIntegrationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/OffersIntegrationSpec.scala @@ -98,6 +98,7 @@ class OffersIntegrationSpec extends IntegrationSpec { alice.sendOnionMessage(Nil, Right(invoiceRequestReplyPath), None, encodedInvoice) val response = probe.expectMsgType[PayOfferResponse](1 minute) + println(response) assert(response.preimage contains preimage.toHex) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartHandlerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartHandlerSpec.scala index bec51d6705..70006e388a 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartHandlerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartHandlerSpec.scala @@ -94,7 +94,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike assert(Crypto.sha256(incoming.get.paymentPreimage) == invoice.paymentHash) val add = UpdateAddHtlc(ByteVector32.One, 0, amountMsat, invoice.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket) - sender.send(handlerWithoutMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createSinglePartPayload(add.amountMsat, add.cltvExpiry, invoice.paymentSecret.get, invoice.paymentMetadata))) + sender.send(handlerWithoutMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createSinglePartPayload(add.amountMsat, add.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata))) register.expectMsgType[Register.Forward[CMD_FULFILL_HTLC]] val paymentReceived = eventListener.expectMsgType[PaymentReceived] @@ -113,7 +113,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike assert(nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).get.status == IncomingPaymentStatus.Pending) val add = UpdateAddHtlc(ByteVector32.One, 0, amountMsat, invoice.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket) - sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createSinglePartPayload(add.amountMsat, add.cltvExpiry, invoice.paymentSecret.get, invoice.paymentMetadata))) + sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createSinglePartPayload(add.amountMsat, add.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata))) register.expectMsgType[Register.Forward[CMD_FULFILL_HTLC]] val paymentReceived = eventListener.expectMsgType[PaymentReceived] @@ -131,7 +131,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike assert(nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).get.status == IncomingPaymentStatus.Pending) val add = UpdateAddHtlc(ByteVector32.One, 0, amountMsat, invoice.paymentHash, CltvExpiryDelta(3).toCltvExpiry(nodeParams.currentBlockHeight), TestConstants.emptyOnionPacket) - sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createSinglePartPayload(add.amountMsat, add.cltvExpiry, invoice.paymentSecret.get, invoice.paymentMetadata))) + sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createSinglePartPayload(add.amountMsat, add.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata))) val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message assert(cmd.reason == Right(IncorrectOrUnknownPaymentDetails(amountMsat, nodeParams.currentBlockHeight))) assert(nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).get.status == IncomingPaymentStatus.Pending) @@ -242,7 +242,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike assert(invoice.isExpired()) val add = UpdateAddHtlc(ByteVector32.One, 0, 1000 msat, invoice.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket) - sender.send(handlerWithoutMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createSinglePartPayload(add.amountMsat, add.cltvExpiry, invoice.paymentSecret.get, invoice.paymentMetadata))) + sender.send(handlerWithoutMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createSinglePartPayload(add.amountMsat, add.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata))) register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]] val Some(incoming) = nodeParams.db.payments.getIncomingPayment(invoice.paymentHash) assert(incoming.invoice.isExpired() && incoming.status == IncomingPaymentStatus.Expired) @@ -257,7 +257,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike assert(invoice.isExpired()) val add = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, invoice.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket) - sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createMultiPartPayload(add.amountMsat, 1000 msat, add.cltvExpiry, invoice.paymentSecret.get, invoice.paymentMetadata))) + sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createMultiPartPayload(add.amountMsat, 1000 msat, add.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata))) val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message assert(cmd.reason == Right(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight))) val Some(incoming) = nodeParams.db.payments.getIncomingPayment(invoice.paymentHash) @@ -272,7 +272,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike assert(!invoice.features.hasFeature(Features.BasicMultiPartPayment)) val add = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, invoice.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket) - sender.send(handlerWithoutMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createMultiPartPayload(add.amountMsat, 1000 msat, add.cltvExpiry, invoice.paymentSecret.get, invoice.paymentMetadata))) + sender.send(handlerWithoutMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createMultiPartPayload(add.amountMsat, 1000 msat, add.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata))) val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message assert(cmd.reason == Right(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight))) assert(nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).get.status == IncomingPaymentStatus.Pending) @@ -287,7 +287,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike val lowCltvExpiry = nodeParams.channelConf.fulfillSafetyBeforeTimeout.toCltvExpiry(nodeParams.currentBlockHeight) val add = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, invoice.paymentHash, lowCltvExpiry, TestConstants.emptyOnionPacket) - sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createMultiPartPayload(add.amountMsat, 1000 msat, add.cltvExpiry, invoice.paymentSecret.get, invoice.paymentMetadata))) + sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createMultiPartPayload(add.amountMsat, 1000 msat, add.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata))) val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message assert(cmd.reason == Right(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight))) assert(nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).get.status == IncomingPaymentStatus.Pending) @@ -301,7 +301,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike assert(invoice.features.hasFeature(Features.BasicMultiPartPayment)) val add = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, invoice.paymentHash.reverse, defaultExpiry, TestConstants.emptyOnionPacket) - sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createMultiPartPayload(add.amountMsat, 1000 msat, add.cltvExpiry, invoice.paymentSecret.get, invoice.paymentMetadata))) + sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createMultiPartPayload(add.amountMsat, 1000 msat, add.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata))) val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message assert(cmd.reason == Right(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight))) assert(nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).get.status == IncomingPaymentStatus.Pending) @@ -315,7 +315,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike assert(invoice.features.hasFeature(Features.BasicMultiPartPayment)) val add = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, invoice.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket) - sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createMultiPartPayload(add.amountMsat, 999 msat, add.cltvExpiry, invoice.paymentSecret.get, invoice.paymentMetadata))) + sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createMultiPartPayload(add.amountMsat, 999 msat, add.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata))) val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message assert(cmd.reason == Right(IncorrectOrUnknownPaymentDetails(999 msat, nodeParams.currentBlockHeight))) assert(nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).get.status == IncomingPaymentStatus.Pending) @@ -329,7 +329,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike assert(invoice.features.hasFeature(Features.BasicMultiPartPayment)) val add = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, invoice.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket) - sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createMultiPartPayload(add.amountMsat, 2001 msat, add.cltvExpiry, invoice.paymentSecret.get, invoice.paymentMetadata))) + sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createMultiPartPayload(add.amountMsat, 2001 msat, add.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata))) val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message assert(cmd.reason == Right(IncorrectOrUnknownPaymentDetails(2001 msat, nodeParams.currentBlockHeight))) assert(nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).get.status == IncomingPaymentStatus.Pending) @@ -344,7 +344,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike // Invalid payment secret. val add = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, invoice.paymentHash, defaultExpiry, TestConstants.emptyOnionPacket) - sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createMultiPartPayload(add.amountMsat, 1000 msat, add.cltvExpiry, invoice.paymentSecret.get.reverse, invoice.paymentMetadata))) + sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createMultiPartPayload(add.amountMsat, 1000 msat, add.cltvExpiry, invoice.paymentSecret.map(_.reverse), invoice.paymentMetadata))) val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message assert(cmd.reason == Right(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight))) assert(nodeParams.db.payments.getIncomingPayment(invoice.paymentHash).get.status == IncomingPaymentStatus.Pending) @@ -358,13 +358,13 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike f.sender.send(handler, ReceivePayment(Some(1000 msat), Left("1 slow coffee"))) val pr1 = f.sender.expectMsgType[Invoice] val add1 = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, pr1.paymentHash, f.defaultExpiry, TestConstants.emptyOnionPacket) - f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add1, PaymentOnion.createMultiPartPayload(add1.amountMsat, 1000 msat, add1.cltvExpiry, pr1.paymentSecret.get, pr1.paymentMetadata))) + f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add1, PaymentOnion.createMultiPartPayload(add1.amountMsat, 1000 msat, add1.cltvExpiry, pr1.paymentSecret, pr1.paymentMetadata))) // Partial payment exceeding the invoice amount, but incomplete because it promises to overpay. f.sender.send(handler, ReceivePayment(Some(1500 msat), Left("1 slow latte"))) val pr2 = f.sender.expectMsgType[Invoice] val add2 = UpdateAddHtlc(ByteVector32.One, 1, 1600 msat, pr2.paymentHash, f.defaultExpiry, TestConstants.emptyOnionPacket) - f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add2, PaymentOnion.createMultiPartPayload(add2.amountMsat, 2000 msat, add2.cltvExpiry, pr2.paymentSecret.get, pr2.paymentMetadata))) + f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add2, PaymentOnion.createMultiPartPayload(add2.amountMsat, 2000 msat, add2.cltvExpiry, pr2.paymentSecret, pr2.paymentMetadata))) awaitCond { f.sender.send(handler, GetPendingPayments) @@ -399,12 +399,12 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike val invoice = f.sender.expectMsgType[Invoice] val add1 = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, invoice.paymentHash, f.defaultExpiry, TestConstants.emptyOnionPacket) - f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add1, PaymentOnion.createMultiPartPayload(add1.amountMsat, 1000 msat, add1.cltvExpiry, invoice.paymentSecret.get, invoice.paymentMetadata))) + f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add1, PaymentOnion.createMultiPartPayload(add1.amountMsat, 1000 msat, add1.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata))) // Invalid payment secret -> should be rejected. val add2 = UpdateAddHtlc(ByteVector32.Zeroes, 42, 200 msat, invoice.paymentHash, f.defaultExpiry, TestConstants.emptyOnionPacket) - f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add2, PaymentOnion.createMultiPartPayload(add2.amountMsat, 1000 msat, add2.cltvExpiry, invoice.paymentSecret.get.reverse, invoice.paymentMetadata))) + f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add2, PaymentOnion.createMultiPartPayload(add2.amountMsat, 1000 msat, add2.cltvExpiry, invoice.paymentSecret.map(_.reverse), invoice.paymentMetadata))) val add3 = add2.copy(id = 43) - f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add3, PaymentOnion.createMultiPartPayload(add3.amountMsat, 1000 msat, add3.cltvExpiry, invoice.paymentSecret.get, invoice.paymentMetadata))) + f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add3, PaymentOnion.createMultiPartPayload(add3.amountMsat, 1000 msat, add3.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata))) f.register.expectMsgAllOf( Register.Forward(null, add2.channelId, CMD_FAIL_HTLC(add2.id, Right(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight)), commit = true)), @@ -444,7 +444,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike assert(invoice.paymentHash == Crypto.sha256(preimage)) val add1 = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, invoice.paymentHash, f.defaultExpiry, TestConstants.emptyOnionPacket) - f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add1, PaymentOnion.createMultiPartPayload(add1.amountMsat, 1000 msat, add1.cltvExpiry, invoice.paymentSecret.get, invoice.paymentMetadata))) + f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add1, PaymentOnion.createMultiPartPayload(add1.amountMsat, 1000 msat, add1.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata))) f.register.expectMsg(Register.Forward(null, ByteVector32.One, CMD_FAIL_HTLC(0, Right(PaymentTimeout), commit = true))) awaitCond({ f.sender.send(handler, GetPendingPayments) @@ -452,9 +452,9 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike }) val add2 = UpdateAddHtlc(ByteVector32.One, 2, 300 msat, invoice.paymentHash, f.defaultExpiry, TestConstants.emptyOnionPacket) - f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add2, PaymentOnion.createMultiPartPayload(add2.amountMsat, 1000 msat, add2.cltvExpiry, invoice.paymentSecret.get, invoice.paymentMetadata))) + f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add2, PaymentOnion.createMultiPartPayload(add2.amountMsat, 1000 msat, add2.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata))) val add3 = UpdateAddHtlc(ByteVector32.Zeroes, 5, 700 msat, invoice.paymentHash, f.defaultExpiry, TestConstants.emptyOnionPacket) - f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add3, PaymentOnion.createMultiPartPayload(add3.amountMsat, 1000 msat, add3.cltvExpiry, invoice.paymentSecret.get, invoice.paymentMetadata))) + f.sender.send(handler, IncomingPaymentPacket.FinalPacket(add3, PaymentOnion.createMultiPartPayload(add3.amountMsat, 1000 msat, add3.cltvExpiry, invoice.paymentSecret, invoice.paymentMetadata))) // the fulfill are not necessarily in the same order as the commands f.register.expectMsgAllOf( @@ -522,7 +522,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike assert(nodeParams.db.payments.getIncomingPayment(paymentHash) == None) val add = UpdateAddHtlc(ByteVector32.One, 0, 1000 msat, paymentHash, defaultExpiry, TestConstants.emptyOnionPacket) - sender.send(handlerWithoutMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createSinglePartPayload(add.amountMsat, add.cltvExpiry, paymentSecret, None))) + sender.send(handlerWithoutMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createSinglePartPayload(add.amountMsat, add.cltvExpiry, Some(paymentSecret), None))) val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message assert(cmd.id == add.id) assert(cmd.reason == Right(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight))) @@ -536,7 +536,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike assert(nodeParams.db.payments.getIncomingPayment(paymentHash) == None) val add = UpdateAddHtlc(ByteVector32.One, 0, 800 msat, paymentHash, defaultExpiry, TestConstants.emptyOnionPacket) - sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createMultiPartPayload(add.amountMsat, 1000 msat, add.cltvExpiry, paymentSecret, Some(hex"012345")))) + sender.send(handlerWithMpp, IncomingPaymentPacket.FinalPacket(add, PaymentOnion.createMultiPartPayload(add.amountMsat, 1000 msat, add.cltvExpiry, Some(paymentSecret), Some(hex"012345")))) val cmd = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message assert(cmd.id == add.id) assert(cmd.reason == Right(IncorrectOrUnknownPaymentDetails(1000 msat, nodeParams.currentBlockHeight))) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartPaymentLifecycleSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartPaymentLifecycleSpec.scala index d3f8bb191f..df2e721385 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartPaymentLifecycleSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartPaymentLifecycleSpec.scala @@ -79,7 +79,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS import f._ assert(payFsm.stateName == WAIT_FOR_PAYMENT_REQUEST) - val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount, expiry, 1, None, routeParams = routeParams.copy(randomize = true)) + val payment = SendMultiPartPayment(sender.ref, Some(randomBytes32()), e, finalAmount, expiry, 1, None, routeParams = routeParams.copy(randomize = true)) sender.send(payFsm, payment) router.expectMsg(RouteRequest(nodeParams.nodeId, e, finalAmount, maxFee, routeParams = routeParams.copy(randomize = false), allowMultiPart = true, paymentContext = Some(cfg.paymentContext))) @@ -112,7 +112,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS import f._ assert(payFsm.stateName == WAIT_FOR_PAYMENT_REQUEST) - val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, 1200000 msat, expiry, 1, Some(hex"012345"), routeParams = routeParams.copy(randomize = false)) + val payment = SendMultiPartPayment(sender.ref, Some(randomBytes32()), e, 1200000 msat, expiry, 1, Some(hex"012345"), routeParams = routeParams.copy(randomize = false)) sender.send(payFsm, payment) router.expectMsg(RouteRequest(nodeParams.nodeId, e, 1200000 msat, maxFee, routeParams = routeParams.copy(randomize = false), allowMultiPart = true, paymentContext = Some(cfg.paymentContext))) @@ -151,7 +151,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS // We include a bunch of additional tlv records. val trampolineTlv = OnionPaymentPayloadTlv.TrampolineOnion(OnionRoutingPacket(0, ByteVector.fill(33)(0), ByteVector.fill(400)(0), randomBytes32())) val userCustomTlv = GenericTlv(UInt64(561), hex"deadbeef") - val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount + 1000.msat, expiry, 1, None, routeParams = routeParams, additionalTlvs = Seq(trampolineTlv), userCustomTlvs = Seq(userCustomTlv)) + val payment = SendMultiPartPayment(sender.ref, Some(randomBytes32()), e, finalAmount + 1000.msat, expiry, 1, None, routeParams = routeParams, additionalTlvs = Seq(trampolineTlv), userCustomTlvs = Seq(userCustomTlv)) sender.send(payFsm, payment) router.expectMsgType[RouteRequest] router.send(payFsm, RouteResponse(Seq(Route(500000 msat, hop_ab_1 :: hop_be :: Nil), Route(501000 msat, hop_ac_1 :: hop_ce :: Nil)))) @@ -175,7 +175,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS test("successful retry") { f => import f._ - val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount, expiry, 3, None, routeParams = routeParams) + val payment = SendMultiPartPayment(sender.ref, Some(randomBytes32()), e, finalAmount, expiry, 3, None, routeParams = routeParams) sender.send(payFsm, payment) router.expectMsgType[RouteRequest] val failingRoute = Route(finalAmount, hop_ab_1 :: hop_be :: Nil) @@ -208,7 +208,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS test("retry failures while waiting for routes") { f => import f._ - val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount, expiry, 3, None, routeParams = routeParams) + val payment = SendMultiPartPayment(sender.ref, Some(randomBytes32()), e, finalAmount, expiry, 3, None, routeParams = routeParams) sender.send(payFsm, payment) router.expectMsgType[RouteRequest] router.send(payFsm, RouteResponse(Seq(Route(400000 msat, hop_ab_1 :: hop_be :: Nil), Route(600000 msat, hop_ab_2 :: hop_be :: Nil)))) @@ -250,7 +250,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS test("retry local channel failures") { f => import f._ - val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount, expiry, 3, None, routeParams = routeParams) + val payment = SendMultiPartPayment(sender.ref, Some(randomBytes32()), e, finalAmount, expiry, 3, None, routeParams = routeParams) sender.send(payFsm, payment) router.expectMsgType[RouteRequest] router.send(payFsm, RouteResponse(Seq(Route(finalAmount, hop_ab_1 :: hop_be :: Nil)))) @@ -275,7 +275,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS test("retry without ignoring channels") { f => import f._ - val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount, expiry, 3, None, routeParams = routeParams) + val payment = SendMultiPartPayment(sender.ref, Some(randomBytes32()), e, finalAmount, expiry, 3, None, routeParams = routeParams) sender.send(payFsm, payment) router.expectMsgType[RouteRequest] router.send(payFsm, RouteResponse(Seq(Route(500000 msat, hop_ab_1 :: hop_be :: Nil), Route(500000 msat, hop_ab_1 :: hop_be :: Nil)))) @@ -319,7 +319,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS // The B -> E channel is private and provided in the invoice routing hints. val extraEdge = Invoice.BasicEdge(b, e, hop_be.shortChannelId, hop_be.params.relayFees.feeBase, hop_be.params.relayFees.feeProportionalMillionths, hop_be.params.cltvExpiryDelta) - val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount, expiry, 3, None, routeParams = routeParams, extraEdges = List(extraEdge)) + val payment = SendMultiPartPayment(sender.ref, Some(randomBytes32()), e, finalAmount, expiry, 3, None, routeParams = routeParams, extraEdges = List(extraEdge)) sender.send(payFsm, payment) assert(router.expectMsgType[RouteRequest].extraEdges.head == extraEdge) val route = Route(finalAmount, hop_ab_1 :: hop_be :: Nil) @@ -341,7 +341,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS // The B -> E channel is private and provided in the invoice routing hints. val extraEdge = Invoice.BasicEdge(b, e, hop_be.shortChannelId, hop_be.params.relayFees.feeBase, hop_be.params.relayFees.feeProportionalMillionths, hop_be.params.cltvExpiryDelta) - val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount, expiry, 3, None, routeParams = routeParams, extraEdges = List(extraEdge)) + val payment = SendMultiPartPayment(sender.ref, Some(randomBytes32()), e, finalAmount, expiry, 3, None, routeParams = routeParams, extraEdges = List(extraEdge)) sender.send(payFsm, payment) assert(router.expectMsgType[RouteRequest].extraEdges.head == extraEdge) val route = Route(finalAmount, hop_ab_1 :: hop_be :: Nil) @@ -401,7 +401,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS test("abort after too many failed attempts") { f => import f._ - val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount, expiry, 2, None, routeParams = routeParams) + val payment = SendMultiPartPayment(sender.ref, Some(randomBytes32()), e, finalAmount, expiry, 2, None, routeParams = routeParams) sender.send(payFsm, payment) router.expectMsgType[RouteRequest] router.send(payFsm, RouteResponse(Seq(Route(500000 msat, hop_ab_1 :: hop_be :: Nil), Route(500000 msat, hop_ac_1 :: hop_ce :: Nil)))) @@ -432,7 +432,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS import f._ sender.watch(payFsm) - val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount, expiry, 5, None, routeParams = routeParams) + val payment = SendMultiPartPayment(sender.ref, Some(randomBytes32()), e, finalAmount, expiry, 5, None, routeParams = routeParams) sender.send(payFsm, payment) router.expectMsgType[RouteRequest] router.send(payFsm, Status.Failure(RouteNotFound)) @@ -462,7 +462,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS test("abort if recipient sends error") { f => import f._ - val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount, expiry, 5, None, routeParams = routeParams) + val payment = SendMultiPartPayment(sender.ref, Some(randomBytes32()), e, finalAmount, expiry, 5, None, routeParams = routeParams) sender.send(payFsm, payment) router.expectMsgType[RouteRequest] router.send(payFsm, RouteResponse(Seq(Route(finalAmount, hop_ab_1 :: hop_be :: Nil)))) @@ -483,7 +483,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS test("abort if payment gets settled on chain") { f => import f._ - val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount, expiry, 5, None, routeParams = routeParams) + val payment = SendMultiPartPayment(sender.ref, Some(randomBytes32()), e, finalAmount, expiry, 5, None, routeParams = routeParams) sender.send(payFsm, payment) router.expectMsgType[RouteRequest] router.send(payFsm, RouteResponse(Seq(Route(finalAmount, hop_ab_1 :: hop_be :: Nil)))) @@ -497,7 +497,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS test("abort if recipient sends error during retry") { f => import f._ - val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount, expiry, 5, None, routeParams = routeParams) + val payment = SendMultiPartPayment(sender.ref, Some(randomBytes32()), e, finalAmount, expiry, 5, None, routeParams = routeParams) sender.send(payFsm, payment) router.expectMsgType[RouteRequest] router.send(payFsm, RouteResponse(Seq(Route(400000 msat, hop_ab_1 :: hop_be :: Nil), Route(600000 msat, hop_ac_1 :: hop_ce :: Nil)))) @@ -515,7 +515,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS test("receive partial success after retriable failure (recipient spec violation)") { f => import f._ - val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount, expiry, 5, None, routeParams = routeParams) + val payment = SendMultiPartPayment(sender.ref, Some(randomBytes32()), e, finalAmount, expiry, 5, None, routeParams = routeParams) sender.send(payFsm, payment) router.expectMsgType[RouteRequest] router.send(payFsm, RouteResponse(Seq(Route(400000 msat, hop_ab_1 :: hop_be :: Nil), Route(600000 msat, hop_ac_1 :: hop_ce :: Nil)))) @@ -535,7 +535,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS test("receive partial success after abort (recipient spec violation)") { f => import f._ - val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount, expiry, 5, None, routeParams = routeParams) + val payment = SendMultiPartPayment(sender.ref, Some(randomBytes32()), e, finalAmount, expiry, 5, None, routeParams = routeParams) sender.send(payFsm, payment) router.expectMsgType[RouteRequest] router.send(payFsm, RouteResponse(Seq(Route(400000 msat, hop_ab_1 :: hop_be :: Nil), Route(600000 msat, hop_ac_1 :: hop_ce :: Nil)))) @@ -568,7 +568,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS test("receive partial failure after success (recipient spec violation)") { f => import f._ - val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount, expiry, 5, None, routeParams = routeParams) + val payment = SendMultiPartPayment(sender.ref, Some(randomBytes32()), e, finalAmount, expiry, 5, None, routeParams = routeParams) sender.send(payFsm, payment) router.expectMsgType[RouteRequest] router.send(payFsm, RouteResponse(Seq(Route(400000 msat, hop_ab_1 :: hop_be :: Nil), Route(600000 msat, hop_ac_1 :: hop_ce :: Nil)))) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentInitiatorSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentInitiatorSpec.scala index e49af3c8ef..e534d6ce61 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentInitiatorSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentInitiatorSpec.scala @@ -149,7 +149,7 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike sender.send(initiator, request) val payment = sender.expectMsgType[SendPaymentToRouteResponse] payFsm.expectMsg(SendPaymentConfig(payment.paymentId, payment.parentId, None, paymentHash, finalAmount, c, Upstream.Local(payment.paymentId), Some(invoice), storeInDb = true, publishEvent = true, recordPathFindingMetrics = false, Nil)) - payFsm.expectMsg(PaymentLifecycle.SendPaymentToRoute(initiator, Left(route), PaymentOnion.createSinglePartPayload(finalAmount, finalExpiryDelta.toCltvExpiry(nodeParams.currentBlockHeight + 1), invoice.paymentSecret.get, invoice.paymentMetadata))) + payFsm.expectMsg(PaymentLifecycle.SendPaymentToRoute(initiator, Left(route), PaymentOnion.createSinglePartPayload(finalAmount, finalExpiryDelta.toCltvExpiry(nodeParams.currentBlockHeight + 1), invoice.paymentSecret, invoice.paymentMetadata))) sender.send(initiator, GetPayment(Left(payment.paymentId))) sender.expectMsg(PaymentIsPending(payment.paymentId, invoice.paymentHash, PendingPaymentToRoute(sender.ref, request))) @@ -197,7 +197,7 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike sender.send(initiator, req) val id = sender.expectMsgType[UUID] multiPartPayFsm.expectMsg(SendPaymentConfig(id, id, None, paymentHash, finalAmount + 100.msat, c, Upstream.Local(id), Some(invoice), storeInDb = true, publishEvent = true, recordPathFindingMetrics = true, Nil)) - multiPartPayFsm.expectMsg(SendMultiPartPayment(initiator, invoice.paymentSecret.get, c, finalAmount + 100.msat, req.finalExpiry(nodeParams.currentBlockHeight), 1, invoice.paymentMetadata, routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams)) + multiPartPayFsm.expectMsg(SendMultiPartPayment(initiator, invoice.paymentSecret, c, finalAmount + 100.msat, req.finalExpiry(nodeParams.currentBlockHeight), 1, invoice.paymentMetadata, routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams)) sender.send(initiator, GetPayment(Left(id))) sender.expectMsg(PaymentIsPending(id, invoice.paymentHash, PendingPaymentToNode(sender.ref, req))) @@ -226,7 +226,7 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike assert(msg.route == Left(route)) assert(msg.finalPayload.amount == finalAmount / 2) assert(msg.finalPayload.expiry == req.finalExpiry(nodeParams.currentBlockHeight)) - assert(msg.finalPayload.paymentSecret == invoice.paymentSecret.get) + assert(msg.finalPayload.paymentSecret == invoice.paymentSecret) assert(msg.finalPayload.totalAmount == finalAmount) sender.send(initiator, GetPayment(Left(payment.paymentId))) @@ -259,7 +259,7 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike sender.expectMsg(PaymentIsPending(id, invoice.paymentHash, PendingTrampolinePayment(sender.ref, Nil, req))) val msg = multiPartPayFsm.expectMsgType[SendMultiPartPayment] - assert(msg.paymentSecret !== invoice.paymentSecret.get) // we should not leak the invoice secret to the trampoline node + assert(msg.paymentSecret !== invoice.paymentSecret) // we should not leak the invoice secret to the trampoline node assert(msg.targetNodeId == b) assert(msg.targetExpiry.toLong == currentBlockCount + 9 + 12 + 1) assert(msg.totalAmount == finalAmount + trampolineFees) @@ -286,7 +286,7 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike assert(finalPayload.amount == finalAmount) assert(finalPayload.totalAmount == finalAmount) assert(finalPayload.expiry.toLong == currentBlockCount + 9 + 1) - assert(finalPayload.paymentSecret == invoice.paymentSecret.get) + assert(finalPayload.paymentSecret == invoice.paymentSecret) } test("forward trampoline to legacy payment") { f => @@ -299,7 +299,7 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike multiPartPayFsm.expectMsgType[SendPaymentConfig] val msg = multiPartPayFsm.expectMsgType[SendMultiPartPayment] - assert(msg.paymentSecret !== invoice.paymentSecret.get) // we should not leak the invoice secret to the trampoline node + assert(msg.paymentSecret !== invoice.paymentSecret) // we should not leak the invoice secret to the trampoline node assert(msg.targetNodeId == b) assert(msg.targetExpiry.toLong == currentBlockCount + 9 + 12 + 1) assert(msg.totalAmount == finalAmount + trampolineFees) @@ -453,7 +453,7 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike val msg = payFsm.expectMsgType[PaymentLifecycle.SendPaymentToRoute] assert(msg.route == Left(route)) assert(msg.finalPayload.amount == finalAmount + trampolineFees) - assert(msg.finalPayload.paymentSecret == payment.trampolineSecret.get) + assert(msg.finalPayload.paymentSecret == payment.trampolineSecret) assert(msg.finalPayload.totalAmount == finalAmount + trampolineFees) assert(msg.finalPayload.isInstanceOf[PaymentOnion.FinalTlvPayload]) val trampolineOnion = msg.finalPayload.asInstanceOf[PaymentOnion.FinalTlvPayload].records.get[OnionPaymentPayloadTlv.TrampolineOnion] diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala index 36443b7bfe..5a8bdc3654 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala @@ -105,7 +105,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { // pre-computed route going from A to D val route = Route(defaultAmountMsat, channelHopFromUpdate(a, b, update_ab) :: channelHopFromUpdate(b, c, update_bc) :: channelHopFromUpdate(c, d, update_cd) :: Nil) - val request = SendPaymentToRoute(sender.ref, Right(route), PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata)) + val request = SendPaymentToRoute(sender.ref, Right(route), PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret, defaultInvoice.paymentMetadata)) sender.send(paymentFSM, request) routerForwarder.expectNoMessage(100 millis) // we don't need the router, we have the pre-computed route val Transition(_, WAITING_FOR_REQUEST, WAITING_FOR_ROUTE) = monitor.expectMsgClass(classOf[Transition[_]]) @@ -133,7 +133,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { // pre-computed route going from A to D val route = PredefinedNodeRoute(Seq(a, b, c, d)) - val request = SendPaymentToRoute(sender.ref, Left(route), PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata)) + val request = SendPaymentToRoute(sender.ref, Left(route), PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret, defaultInvoice.paymentMetadata)) sender.send(paymentFSM, request) routerForwarder.expectMsg(FinalizeRoute(defaultAmountMsat, route, paymentContext = Some(cfg.paymentContext))) @@ -159,7 +159,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val payFixture = createPaymentLifecycle(recordMetrics = false) import payFixture._ - val brokenRoute = SendPaymentToRoute(sender.ref, Left(PredefinedNodeRoute(Seq(randomKey().publicKey, randomKey().publicKey, randomKey().publicKey))), PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata)) + val brokenRoute = SendPaymentToRoute(sender.ref, Left(PredefinedNodeRoute(Seq(randomKey().publicKey, randomKey().publicKey, randomKey().publicKey))), PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret, defaultInvoice.paymentMetadata)) sender.send(paymentFSM, brokenRoute) routerForwarder.expectMsgType[FinalizeRoute] routerForwarder.forward(routerFixture.router) @@ -176,7 +176,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val payFixture = createPaymentLifecycle(recordMetrics = false) import payFixture._ - val brokenRoute = SendPaymentToRoute(sender.ref, Left(PredefinedChannelRoute(randomKey().publicKey, Seq(ShortChannelId(1), ShortChannelId(2)))), PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata)) + val brokenRoute = SendPaymentToRoute(sender.ref, Left(PredefinedChannelRoute(randomKey().publicKey, Seq(ShortChannelId(1), ShortChannelId(2)))), PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret, defaultInvoice.paymentMetadata)) sender.send(paymentFSM, brokenRoute) routerForwarder.expectMsgType[FinalizeRoute] routerForwarder.forward(routerFixture.router) @@ -197,7 +197,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val recipient = randomKey().publicKey val route = PredefinedNodeRoute(Seq(a, b, c, recipient)) val extraEdges = Seq(BasicEdge(c, recipient, ShortChannelId(561), 1 msat, 100, CltvExpiryDelta(144))) - val request = SendPaymentToRoute(sender.ref, Left(route), PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata), extraEdges) + val request = SendPaymentToRoute(sender.ref, Left(route), PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret, defaultInvoice.paymentMetadata), extraEdges) sender.send(paymentFSM, request) routerForwarder.expectMsg(FinalizeRoute(defaultAmountMsat, route, extraEdges, paymentContext = Some(cfg.paymentContext))) @@ -223,7 +223,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { import payFixture._ import cfg._ - val request = SendPaymentToNode(sender.ref, f, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata), 5, routeParams = defaultRouteParams) + val request = SendPaymentToNode(sender.ref, f, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret, defaultInvoice.paymentMetadata), 5, routeParams = defaultRouteParams) sender.send(paymentFSM, request) val Transition(_, WAITING_FOR_REQUEST, WAITING_FOR_ROUTE) = monitor.expectMsgClass(classOf[Transition[_]]) val routeRequest = routerForwarder.expectMsgType[RouteRequest] @@ -256,7 +256,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { "my-test-experiment", experimentPercentage = 100 ).getDefaultRouteParams - val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata), 5, routeParams = routeParams) + val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret, defaultInvoice.paymentMetadata), 5, routeParams = routeParams) sender.send(paymentFSM, request) val routeRequest = routerForwarder.expectMsgType[RouteRequest] val Transition(_, WAITING_FOR_REQUEST, WAITING_FOR_ROUTE) = monitor.expectMsgClass(classOf[Transition[_]]) @@ -281,7 +281,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { import cfg._ val paymentMetadataTooBig = ByteVector.fromValidHex("01" * 1300) - val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, Some(paymentMetadataTooBig)), 5, routeParams = defaultRouteParams) + val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret, Some(paymentMetadataTooBig)), 5, routeParams = defaultRouteParams) sender.send(paymentFSM, request) val Transition(_, WAITING_FOR_REQUEST, WAITING_FOR_ROUTE) = monitor.expectMsgClass(classOf[Transition[_]]) val routeRequest = routerForwarder.expectMsgType[RouteRequest] @@ -300,7 +300,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { import payFixture._ import cfg._ - val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata), 2, routeParams = defaultRouteParams) + val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret, defaultInvoice.paymentMetadata), 2, routeParams = defaultRouteParams) sender.send(paymentFSM, request) routerForwarder.expectMsg(defaultRouteRequest(a, d, cfg)) awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.Pending)) @@ -345,7 +345,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { import payFixture._ import cfg._ - val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata), 2, routeParams = defaultRouteParams) + val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret, defaultInvoice.paymentMetadata), 2, routeParams = defaultRouteParams) sender.send(paymentFSM, request) awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.Pending)) routerForwarder.expectMsgType[RouteRequest] @@ -366,7 +366,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { import payFixture._ import cfg._ - val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata), 2, routeParams = defaultRouteParams) + val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret, defaultInvoice.paymentMetadata), 2, routeParams = defaultRouteParams) sender.send(paymentFSM, request) awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.Pending)) routerForwarder.expectMsgType[RouteRequest] @@ -386,7 +386,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { import payFixture._ import cfg._ - val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata), 2, routeParams = defaultRouteParams) + val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret, defaultInvoice.paymentMetadata), 2, routeParams = defaultRouteParams) sender.send(paymentFSM, request) awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.Pending)) @@ -409,7 +409,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { import payFixture._ import cfg._ - val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata), 2, routeParams = defaultRouteParams) + val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret, defaultInvoice.paymentMetadata), 2, routeParams = defaultRouteParams) sender.send(paymentFSM, request) awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.Pending)) @@ -432,7 +432,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { import payFixture._ import cfg._ - val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata), 2, routeParams = defaultRouteParams) + val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret, defaultInvoice.paymentMetadata), 2, routeParams = defaultRouteParams) sender.send(paymentFSM, request) awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.Pending)) @@ -455,7 +455,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val payFixture = createPaymentLifecycle() import payFixture._ - val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata), 2, routeParams = defaultRouteParams) + val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret, defaultInvoice.paymentMetadata), 2, routeParams = defaultRouteParams) sender.send(paymentFSM, request) awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE) val WaitingForRoute(_, Nil, _) = paymentFSM.stateData @@ -486,7 +486,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { import payFixture._ import cfg._ - val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata), 5, routeParams = defaultRouteParams) + val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret, defaultInvoice.paymentMetadata), 5, routeParams = defaultRouteParams) sender.send(paymentFSM, request) awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.Pending)) @@ -540,7 +540,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val payFixture = createPaymentLifecycle() import payFixture._ - val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata), 1, routeParams = defaultRouteParams) + val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret, defaultInvoice.paymentMetadata), 1, routeParams = defaultRouteParams) sender.send(paymentFSM, request) routerForwarder.expectMsg(defaultRouteRequest(nodeParams.nodeId, d, cfg)) routerForwarder.forward(routerFixture.router) @@ -571,7 +571,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { BasicEdge(c, d, scid_cd, update_cd.feeBaseMsat, update_cd.feeProportionalMillionths, update_cd.cltvExpiryDelta) ) - val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata), 5, extraEdges = extraEdges, routeParams = defaultRouteParams) + val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret, defaultInvoice.paymentMetadata), 5, extraEdges = extraEdges, routeParams = defaultRouteParams) sender.send(paymentFSM, request) awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.Pending)) @@ -612,7 +612,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { // we build an assisted route for channel cd val extraEdges = Seq(BasicEdge(c, d, scid_cd, update_cd.feeBaseMsat, update_cd.feeProportionalMillionths, update_cd.cltvExpiryDelta)) - val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata), 1, extraEdges = extraEdges, routeParams = defaultRouteParams) + val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret, defaultInvoice.paymentMetadata), 1, extraEdges = extraEdges, routeParams = defaultRouteParams) sender.send(paymentFSM, request) awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.Pending)) @@ -638,7 +638,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { import payFixture._ import cfg._ - val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata), 2, routeParams = defaultRouteParams) + val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret, defaultInvoice.paymentMetadata), 2, routeParams = defaultRouteParams) sender.send(paymentFSM, request) awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.Pending)) @@ -676,7 +676,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { import payFixture._ import cfg._ - val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata), 5, routeParams = defaultRouteParams) + val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret, defaultInvoice.paymentMetadata), 5, routeParams = defaultRouteParams) sender.send(paymentFSM, request) routerForwarder.expectMsgType[RouteRequest] val Transition(_, WAITING_FOR_REQUEST, WAITING_FOR_ROUTE) = monitor.expectMsgClass(classOf[Transition[_]]) @@ -732,7 +732,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { import payFixture._ // we send a payment to H - val request = SendPaymentToNode(sender.ref, h, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata), 5, routeParams = defaultRouteParams) + val request = SendPaymentToNode(sender.ref, h, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret, defaultInvoice.paymentMetadata), 5, routeParams = defaultRouteParams) sender.send(paymentFSM, request) routerForwarder.expectMsgType[RouteRequest] @@ -818,7 +818,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { import payFixture._ import cfg._ - val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata), 3, routeParams = defaultRouteParams) + val request = SendPaymentToNode(sender.ref, d, PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret, defaultInvoice.paymentMetadata), 3, routeParams = defaultRouteParams) sender.send(paymentFSM, request) routerForwarder.expectMsgType[RouteRequest] val Transition(_, WAITING_FOR_REQUEST, WAITING_FOR_ROUTE) = monitor.expectMsgClass(classOf[Transition[_]]) @@ -841,7 +841,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { // pre-computed route going from A to D val route = Route(defaultAmountMsat, channelHopFromUpdate(a, b, update_ab) :: channelHopFromUpdate(b, c, update_bc) :: channelHopFromUpdate(c, d, update_cd) :: Nil) - val request = SendPaymentToRoute(sender.ref, Right(route), PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret.get, defaultInvoice.paymentMetadata)) + val request = SendPaymentToRoute(sender.ref, Right(route), PaymentOnion.createSinglePartPayload(defaultAmountMsat, defaultExpiry, defaultInvoice.paymentSecret, defaultInvoice.paymentMetadata)) sender.send(paymentFSM, request) routerForwarder.expectNoMessage(100 millis) // we don't need the router, we have the pre-computed route val Transition(_, WAITING_FOR_REQUEST, WAITING_FOR_ROUTE) = monitor.expectMsgClass(classOf[Transition[_]]) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentPacketSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentPacketSpec.scala index 1a3edce695..c528512e4e 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentPacketSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentPacketSpec.scala @@ -110,7 +110,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { assert(payload_e.amount == finalAmount) assert(payload_e.totalAmount == finalAmount) assert(payload_e.expiry == finalExpiry) - assert(payload_e.paymentSecret == paymentSecret) + assert(payload_e.paymentSecret contains paymentSecret) } test("build onion with final payload") { @@ -118,7 +118,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { } test("build a command including the onion") { - val Success((add, _)) = buildCommand(ActorRef.noSender, Upstream.Local(UUID.randomUUID), paymentHash, hops, PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, paymentSecret, None)) + val Success((add, _)) = buildCommand(ActorRef.noSender, Upstream.Local(UUID.randomUUID), paymentHash, hops, PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, Some(paymentSecret), None)) assert(add.amount > finalAmount) assert(add.cltvExpiry == finalExpiry + channelUpdate_de.cltvExpiryDelta + channelUpdate_cd.cltvExpiryDelta + channelUpdate_bc.cltvExpiryDelta) assert(add.paymentHash == paymentHash) @@ -129,7 +129,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { } test("build a command with no hops") { - val Success((add, _)) = buildCommand(ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, hops.take(1), PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, paymentSecret, Some(paymentMetadata))) + val Success((add, _)) = buildCommand(ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, hops.take(1), PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, Some(paymentSecret), Some(paymentMetadata))) assert(add.amount == finalAmount) assert(add.cltvExpiry == finalExpiry) assert(add.paymentHash == paymentHash) @@ -142,8 +142,8 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { assert(payload_b.amount == finalAmount) assert(payload_b.totalAmount == finalAmount) assert(payload_b.expiry == finalExpiry) - assert(payload_b.paymentSecret == paymentSecret) - assert(payload_b.paymentMetadata == Some(paymentMetadata)) + assert(payload_b.paymentSecret contains paymentSecret) + assert(payload_b.paymentMetadata contains paymentMetadata) } test("build a trampoline payment") { @@ -152,7 +152,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { // / \ / \ // a -> b -> c d e - val Success((amount_ac, expiry_ac, trampolineOnion)) = buildTrampolinePacket(paymentHash, trampolineHops, PaymentOnion.createMultiPartPayload(finalAmount, finalAmount * 3, finalExpiry, paymentSecret, Some(hex"010203"))) + val Success((amount_ac, expiry_ac, trampolineOnion)) = buildTrampolinePacket(paymentHash, trampolineHops, PaymentOnion.createMultiPartPayload(finalAmount, finalAmount * 3, finalExpiry, Some(paymentSecret), Some(hex"010203"))) assert(amount_ac == amount_bc) assert(expiry_ac == expiry_bc) @@ -216,7 +216,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { val routingHints = List(List(Bolt11Invoice.ExtraHop(randomKey().publicKey, ShortChannelId(42), 10 msat, 100, CltvExpiryDelta(144)))) val invoiceFeatures = Features[InvoiceFeature](VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory, BasicMultiPartPayment -> Optional) val invoice = Bolt11Invoice(Block.RegtestGenesisBlock.hash, Some(finalAmount), paymentHash, priv_a.privateKey, Left("#reckless"), CltvExpiryDelta(18), None, None, routingHints, features = invoiceFeatures, paymentMetadata = Some(hex"010203")) - val Success((amount_ac, expiry_ac, trampolineOnion)) = buildTrampolineToLegacyPacket(invoice, trampolineHops, PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, invoice.paymentSecret.get, None)) + val Success((amount_ac, expiry_ac, trampolineOnion)) = buildTrampolineToLegacyPacket(invoice, trampolineHops, PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, invoice.paymentSecret, None)) assert(amount_ac == amount_bc) assert(expiry_ac == expiry_bc) @@ -263,18 +263,18 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { test("fail to build a trampoline payment when too much invoice data is provided") { val routingHintOverflow = List(List.fill(7)(Bolt11Invoice.ExtraHop(randomKey().publicKey, ShortChannelId(1), 10 msat, 100, CltvExpiryDelta(12)))) val invoice = Bolt11Invoice(Block.RegtestGenesisBlock.hash, Some(finalAmount), paymentHash, priv_a.privateKey, Left("#reckless"), CltvExpiryDelta(18), None, None, routingHintOverflow) - assert(buildTrampolineToLegacyPacket(invoice, trampolineHops, PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, invoice.paymentSecret.get, invoice.paymentMetadata)).isFailure) + assert(buildTrampolineToLegacyPacket(invoice, trampolineHops, PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, invoice.paymentSecret, invoice.paymentMetadata)).isFailure) } test("fail to decrypt when the onion is invalid") { - val Success((firstAmount, firstExpiry, onion)) = buildPaymentPacket(paymentHash, hops, PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, paymentSecret, None)) + val Success((firstAmount, firstExpiry, onion)) = buildPaymentPacket(paymentHash, hops, PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, Some(paymentSecret), None)) val add = UpdateAddHtlc(randomBytes32(), 1, firstAmount, paymentHash, firstExpiry, onion.packet.copy(payload = onion.packet.payload.reverse)) val Left(failure) = decrypt(add, priv_b.privateKey) assert(failure.isInstanceOf[InvalidOnionHmac]) } test("fail to decrypt when the trampoline onion is invalid") { - val Success((amount_ac, expiry_ac, trampolineOnion)) = buildTrampolinePacket(paymentHash, trampolineHops, PaymentOnion.createMultiPartPayload(finalAmount, finalAmount * 2, finalExpiry, paymentSecret, None)) + val Success((amount_ac, expiry_ac, trampolineOnion)) = buildTrampolinePacket(paymentHash, trampolineHops, PaymentOnion.createMultiPartPayload(finalAmount, finalAmount * 2, finalExpiry, Some(paymentSecret), None)) val Success((firstAmount, firstExpiry, onion)) = buildPaymentPacket(paymentHash, trampolineChannelHops, PaymentOnion.createTrampolinePayload(amount_ac, amount_ac, expiry_ac, randomBytes32(), trampolineOnion.packet.copy(payload = trampolineOnion.packet.payload.reverse))) val add_b = UpdateAddHtlc(randomBytes32(), 1, firstAmount, paymentHash, firstExpiry, onion.packet) val Right(ChannelRelayPacket(_, _, packet_c)) = decrypt(add_b, priv_b.privateKey) @@ -284,28 +284,28 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { } test("fail to decrypt when payment hash doesn't match associated data") { - val Success((firstAmount, firstExpiry, onion)) = buildPaymentPacket(paymentHash.reverse, hops, PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, paymentSecret, None)) + val Success((firstAmount, firstExpiry, onion)) = buildPaymentPacket(paymentHash.reverse, hops, PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, Some(paymentSecret), None)) val add = UpdateAddHtlc(randomBytes32(), 1, firstAmount, paymentHash, firstExpiry, onion.packet) val Left(failure) = decrypt(add, priv_b.privateKey) assert(failure.isInstanceOf[InvalidOnionHmac]) } test("fail to decrypt at the final node when amount has been modified by next-to-last node") { - val Success((firstAmount, firstExpiry, onion)) = buildPaymentPacket(paymentHash, hops.take(1), PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, paymentSecret, None)) + val Success((firstAmount, firstExpiry, onion)) = buildPaymentPacket(paymentHash, hops.take(1), PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, Some(paymentSecret), None)) val add = UpdateAddHtlc(randomBytes32(), 1, firstAmount - 100.msat, paymentHash, firstExpiry, onion.packet) val Left(failure) = decrypt(add, priv_b.privateKey) assert(failure == FinalIncorrectHtlcAmount(firstAmount - 100.msat)) } test("fail to decrypt at the final node when expiry has been modified by next-to-last node") { - val Success((firstAmount, firstExpiry, onion)) = buildPaymentPacket(paymentHash, hops.take(1), PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, paymentSecret, None)) + val Success((firstAmount, firstExpiry, onion)) = buildPaymentPacket(paymentHash, hops.take(1), PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, Some(paymentSecret), None)) val add = UpdateAddHtlc(randomBytes32(), 1, firstAmount, paymentHash, firstExpiry - CltvExpiryDelta(12), onion.packet) val Left(failure) = decrypt(add, priv_b.privateKey) assert(failure == FinalIncorrectCltvExpiry(firstExpiry - CltvExpiryDelta(12))) } test("fail to decrypt at the final trampoline node when amount has been modified by next-to-last trampoline") { - val Success((amount_ac, expiry_ac, trampolineOnion)) = buildTrampolinePacket(paymentHash, trampolineHops, PaymentOnion.createMultiPartPayload(finalAmount, finalAmount, finalExpiry, paymentSecret, None)) + val Success((amount_ac, expiry_ac, trampolineOnion)) = buildTrampolinePacket(paymentHash, trampolineHops, PaymentOnion.createMultiPartPayload(finalAmount, finalAmount, finalExpiry, Some(paymentSecret), None)) val Success((firstAmount, firstExpiry, onion)) = buildPaymentPacket(paymentHash, trampolineChannelHops, PaymentOnion.createTrampolinePayload(amount_ac, amount_ac, expiry_ac, randomBytes32(), trampolineOnion.packet)) val Right(ChannelRelayPacket(_, _, packet_c)) = decrypt(UpdateAddHtlc(randomBytes32(), 1, firstAmount, paymentHash, firstExpiry, onion.packet), priv_b.privateKey) val Right(NodeRelayPacket(_, _, _, packet_d)) = decrypt(UpdateAddHtlc(randomBytes32(), 2, amount_bc, paymentHash, expiry_bc, packet_c), priv_c.privateKey) @@ -320,7 +320,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { } test("fail to decrypt at the final trampoline node when expiry has been modified by next-to-last trampoline") { - val Success((amount_ac, expiry_ac, trampolineOnion)) = buildTrampolinePacket(paymentHash, trampolineHops, PaymentOnion.createMultiPartPayload(finalAmount, finalAmount, finalExpiry, paymentSecret, None)) + val Success((amount_ac, expiry_ac, trampolineOnion)) = buildTrampolinePacket(paymentHash, trampolineHops, PaymentOnion.createMultiPartPayload(finalAmount, finalAmount, finalExpiry, Some(paymentSecret), None)) val Success((firstAmount, firstExpiry, onion)) = buildPaymentPacket(paymentHash, trampolineChannelHops, PaymentOnion.createTrampolinePayload(amount_ac, amount_ac, expiry_ac, randomBytes32(), trampolineOnion.packet)) val Right(ChannelRelayPacket(_, _, packet_c)) = decrypt(UpdateAddHtlc(randomBytes32(), 1, firstAmount, paymentHash, firstExpiry, onion.packet), priv_b.privateKey) val Right(NodeRelayPacket(_, _, _, packet_d)) = decrypt(UpdateAddHtlc(randomBytes32(), 2, amount_bc, paymentHash, expiry_bc, packet_c), priv_c.privateKey) @@ -335,7 +335,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { } test("fail to decrypt at intermediate trampoline node when amount is invalid") { - val Success((amount_ac, expiry_ac, trampolineOnion)) = buildTrampolinePacket(paymentHash, trampolineHops, PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, paymentSecret, None)) + val Success((amount_ac, expiry_ac, trampolineOnion)) = buildTrampolinePacket(paymentHash, trampolineHops, PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, Some(paymentSecret), None)) val Success((firstAmount, firstExpiry, onion)) = buildPaymentPacket(paymentHash, trampolineChannelHops, PaymentOnion.createTrampolinePayload(amount_ac, amount_ac, expiry_ac, randomBytes32(), trampolineOnion.packet)) val Right(ChannelRelayPacket(_, _, packet_c)) = decrypt(UpdateAddHtlc(randomBytes32(), 1, firstAmount, paymentHash, firstExpiry, onion.packet), priv_b.privateKey) // A trampoline relay is very similar to a final node: it can validate that the HTLC amount matches the onion outer amount. @@ -344,7 +344,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { } test("fail to decrypt at intermediate trampoline node when expiry is invalid") { - val Success((amount_ac, expiry_ac, trampolineOnion)) = buildTrampolinePacket(paymentHash, trampolineHops, PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, paymentSecret, None)) + val Success((amount_ac, expiry_ac, trampolineOnion)) = buildTrampolinePacket(paymentHash, trampolineHops, PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, Some(paymentSecret), None)) val Success((firstAmount, firstExpiry, onion)) = buildPaymentPacket(paymentHash, trampolineChannelHops, PaymentOnion.createTrampolinePayload(amount_ac, amount_ac, expiry_ac, randomBytes32(), trampolineOnion.packet)) val Right(ChannelRelayPacket(_, _, packet_c)) = decrypt(UpdateAddHtlc(randomBytes32(), 1, firstAmount, paymentHash, firstExpiry, onion.packet), priv_b.privateKey) // A trampoline relay is very similar to a final node: it can validate that the HTLC expiry matches the onion outer expiry. diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PostRestartHtlcCleanerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PostRestartHtlcCleanerSpec.scala index 2a912e74f1..2ccc0ca29f 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PostRestartHtlcCleanerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PostRestartHtlcCleanerSpec.scala @@ -721,7 +721,7 @@ object PostRestartHtlcCleanerSpec { val (paymentHash1, paymentHash2, paymentHash3) = (Crypto.sha256(preimage1), Crypto.sha256(preimage2), Crypto.sha256(preimage3)) def buildHtlc(htlcId: Long, channelId: ByteVector32, paymentHash: ByteVector32): UpdateAddHtlc = { - val Success((cmd, _)) = buildCommand(ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, hops, PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, randomBytes32(), None)) + val Success((cmd, _)) = buildCommand(ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, hops, PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, Some(randomBytes32()), None)) UpdateAddHtlc(channelId, htlcId, cmd.amount, cmd.paymentHash, cmd.cltvExpiry, cmd.onion) } @@ -730,7 +730,7 @@ object PostRestartHtlcCleanerSpec { def buildHtlcOut(htlcId: Long, channelId: ByteVector32, paymentHash: ByteVector32): DirectedHtlc = OutgoingHtlc(buildHtlc(htlcId, channelId, paymentHash)) def buildFinalHtlc(htlcId: Long, channelId: ByteVector32, paymentHash: ByteVector32): DirectedHtlc = { - val Success((cmd, _)) = buildCommand(ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, channelHopFromUpdate(a, TestConstants.Bob.nodeParams.nodeId, channelUpdate_ab) :: Nil, PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, randomBytes32(), None)) + val Success((cmd, _)) = buildCommand(ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, channelHopFromUpdate(a, TestConstants.Bob.nodeParams.nodeId, channelUpdate_ab) :: Nil, PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, Some(randomBytes32()), None)) IncomingHtlc(UpdateAddHtlc(channelId, htlcId, cmd.amount, cmd.paymentHash, cmd.cltvExpiry, cmd.onion)) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/NodeRelayerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/NodeRelayerSpec.scala index a1796ed265..a89702934d 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/NodeRelayerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/NodeRelayerSpec.scala @@ -102,26 +102,26 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl parentRelayer ! NodeRelayer.Relay(payment1) parentRelayer ! NodeRelayer.GetPendingPayments(probe.ref.toClassic) val pending1 = probe.expectMessageType[Map[PaymentKey, ActorRef[NodeRelay.Command]]] - assert(pending1.keySet == Set(PaymentKey(paymentHash1, paymentSecret1))) + assert(pending1.keySet == Set(PaymentKey(paymentHash1, Some(paymentSecret1)))) val (paymentHash2, paymentSecret2) = (randomBytes32(), randomBytes32()) val payment2 = createPartialIncomingPacket(paymentHash2, paymentSecret2) parentRelayer ! NodeRelayer.Relay(payment2) parentRelayer ! NodeRelayer.GetPendingPayments(probe.ref.toClassic) val pending2 = probe.expectMessageType[Map[PaymentKey, ActorRef[NodeRelay.Command]]] - assert(pending2.keySet == Set(PaymentKey(paymentHash1, paymentSecret1), PaymentKey(paymentHash2, paymentSecret2))) + assert(pending2.keySet == Set(PaymentKey(paymentHash1, Some(paymentSecret1)), PaymentKey(paymentHash2, Some(paymentSecret2)))) val payment3a = createPartialIncomingPacket(paymentHash1, paymentSecret2) parentRelayer ! NodeRelayer.Relay(payment3a) parentRelayer ! NodeRelayer.GetPendingPayments(probe.ref.toClassic) val pending3 = probe.expectMessageType[Map[PaymentKey, ActorRef[NodeRelay.Command]]] - assert(pending3.keySet == Set(PaymentKey(paymentHash1, paymentSecret1), PaymentKey(paymentHash2, paymentSecret2), PaymentKey(paymentHash1, paymentSecret2))) + assert(pending3.keySet == Set(PaymentKey(paymentHash1, Some(paymentSecret1)), PaymentKey(paymentHash2, Some(paymentSecret2)), PaymentKey(paymentHash1, Some(paymentSecret2)))) val payment3b = createPartialIncomingPacket(paymentHash1, paymentSecret2) parentRelayer ! NodeRelayer.Relay(payment3b) parentRelayer ! NodeRelayer.GetPendingPayments(probe.ref.toClassic) val pending4 = probe.expectMessageType[Map[PaymentKey, ActorRef[NodeRelay.Command]]] - assert(pending4.keySet == Set(PaymentKey(paymentHash1, paymentSecret1), PaymentKey(paymentHash2, paymentSecret2), PaymentKey(paymentHash1, paymentSecret2))) + assert(pending4.keySet == Set(PaymentKey(paymentHash1, Some(paymentSecret1)), PaymentKey(paymentHash2, Some(paymentSecret2)), PaymentKey(paymentHash1, Some(paymentSecret2)))) register.expectNoMessage(100 millis) } @@ -137,8 +137,8 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl probe.expectMessage(Map.empty) } { - val (paymentHash1, paymentSecret1, child1) = (randomBytes32(), randomBytes32(), TestProbe[NodeRelay.Command]()) - val (paymentHash2, paymentSecret2, child2) = (randomBytes32(), randomBytes32(), TestProbe[NodeRelay.Command]()) + val (paymentHash1, paymentSecret1, child1) = (randomBytes32(), Some(randomBytes32()), TestProbe[NodeRelay.Command]()) + val (paymentHash2, paymentSecret2, child2) = (randomBytes32(), Some(randomBytes32()), TestProbe[NodeRelay.Command]()) val children = Map(PaymentKey(paymentHash1, paymentSecret1) -> child1.ref, PaymentKey(paymentHash2, paymentSecret2) -> child2.ref) val parentRelayer = testKit.spawn(NodeRelayer(nodeParams, register.ref.toClassic, outgoingPaymentFactory, children)) parentRelayer ! NodeRelayer.GetPendingPayments(probe.ref.toClassic) @@ -153,8 +153,8 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl } { val paymentHash = randomBytes32() - val (paymentSecret1, child1) = (randomBytes32(), TestProbe[NodeRelay.Command]()) - val (paymentSecret2, child2) = (randomBytes32(), TestProbe[NodeRelay.Command]()) + val (paymentSecret1, child1) = (Some(randomBytes32()), TestProbe[NodeRelay.Command]()) + val (paymentSecret2, child2) = (Some(randomBytes32()), TestProbe[NodeRelay.Command]()) val children = Map(PaymentKey(paymentHash, paymentSecret1) -> child1.ref, PaymentKey(paymentHash, paymentSecret2) -> child2.ref) val parentRelayer = testKit.spawn(NodeRelayer(nodeParams, register.ref.toClassic, outgoingPaymentFactory, children)) parentRelayer ! NodeRelayer.GetPendingPayments(probe.ref.toClassic) @@ -171,9 +171,9 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl parentRelayer ! NodeRelayer.GetPendingPayments(probe.ref.toClassic) val pending1 = probe.expectMessageType[Map[PaymentKey, ActorRef[NodeRelay.Command]]] assert(pending1.size == 1) - assert(pending1.head._1 == PaymentKey(paymentHash, incomingSecret)) + assert(pending1.head._1 == PaymentKey(paymentHash, Some(incomingSecret))) - parentRelayer ! NodeRelayer.RelayComplete(pending1.head._2, paymentHash, incomingSecret) + parentRelayer ! NodeRelayer.RelayComplete(pending1.head._2, paymentHash, Some(incomingSecret)) parentRelayer ! NodeRelayer.GetPendingPayments(probe.ref.toClassic) probe.expectMessage(Map.empty) @@ -213,7 +213,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl // and then one extra val extra = IncomingPaymentPacket.NodeRelayPacket( UpdateAddHtlc(randomBytes32(), Random.nextInt(100), 1000 msat, paymentHash, CltvExpiry(499990), TestConstants.emptyOnionPacket), - PaymentOnion.createMultiPartPayload(1000 msat, incomingAmount, CltvExpiry(499990), incomingSecret, None), + PaymentOnion.createMultiPartPayload(1000 msat, incomingAmount, CltvExpiry(499990), Some(incomingSecret), None), PaymentOnion.createNodeRelayPayload(outgoingAmount, outgoingExpiry, outgoingNodeId), nextTrampolinePacket) nodeRelayer ! NodeRelay.Relay(extra) @@ -242,7 +242,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl // Receive new extraneous multi-part HTLC. val i1 = IncomingPaymentPacket.NodeRelayPacket( UpdateAddHtlc(randomBytes32(), Random.nextInt(100), 1000 msat, paymentHash, CltvExpiry(499990), TestConstants.emptyOnionPacket), - PaymentOnion.createMultiPartPayload(1000 msat, incomingAmount, CltvExpiry(499990), incomingSecret, None), + PaymentOnion.createMultiPartPayload(1000 msat, incomingAmount, CltvExpiry(499990), Some(incomingSecret), None), PaymentOnion.createNodeRelayPayload(outgoingAmount, outgoingExpiry, outgoingNodeId), nextTrampolinePacket) nodeRelayer ! NodeRelay.Relay(i1) @@ -255,7 +255,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl // Receive new HTLC with different details, but for the same payment hash. val i2 = IncomingPaymentPacket.NodeRelayPacket( UpdateAddHtlc(randomBytes32(), Random.nextInt(100), 1500 msat, paymentHash, CltvExpiry(499990), TestConstants.emptyOnionPacket), - PaymentOnion.createSinglePartPayload(1500 msat, CltvExpiry(499990), incomingSecret, None), + PaymentOnion.createSinglePartPayload(1500 msat, CltvExpiry(499990), Some(incomingSecret), None), PaymentOnion.createNodeRelayPayload(1250 msat, outgoingExpiry, outgoingNodeId), nextTrampolinePacket) nodeRelayer ! NodeRelay.Relay(i2) @@ -579,7 +579,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl val outgoingCfg = mockPayFSM.expectMessageType[SendPaymentConfig] validateOutgoingCfg(outgoingCfg, Upstream.Trampoline(incomingMultiPart.map(_.add))) val outgoingPayment = mockPayFSM.expectMessageType[SendMultiPartPayment] - assert(outgoingPayment.paymentSecret == invoice.paymentSecret.get) // we should use the provided secret + assert(outgoingPayment.paymentSecret == invoice.paymentSecret) // we should use the provided secret assert(outgoingPayment.paymentMetadata == invoice.paymentMetadata) // we should use the provided metadata assert(outgoingPayment.totalAmount == outgoingAmount) assert(outgoingPayment.targetExpiry == outgoingExpiry) @@ -687,7 +687,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl } def validateOutgoingPayment(outgoingPayment: SendMultiPartPayment): Unit = { - assert(outgoingPayment.paymentSecret !== incomingSecret) // we should generate a new outgoing secret + assert(!outgoingPayment.paymentSecret.contains(incomingSecret)) // we should generate a new outgoing secret assert(outgoingPayment.totalAmount == outgoingAmount) assert(outgoingPayment.targetExpiry == outgoingExpiry) assert(outgoingPayment.targetNodeId == outgoingNodeId) @@ -734,9 +734,9 @@ object NodeRelayerSpec { def createValidIncomingPacket(amountIn: MilliSatoshi, totalAmountIn: MilliSatoshi, expiryIn: CltvExpiry, amountOut: MilliSatoshi, expiryOut: CltvExpiry): IncomingPaymentPacket.NodeRelayPacket = { val outerPayload = if (amountIn == totalAmountIn) { - PaymentOnion.createSinglePartPayload(amountIn, expiryIn, incomingSecret, None) + PaymentOnion.createSinglePartPayload(amountIn, expiryIn, Some(incomingSecret), None) } else { - PaymentOnion.createMultiPartPayload(amountIn, totalAmountIn, expiryIn, incomingSecret, None) + PaymentOnion.createMultiPartPayload(amountIn, totalAmountIn, expiryIn, Some(incomingSecret), None) } IncomingPaymentPacket.NodeRelayPacket( UpdateAddHtlc(randomBytes32(), Random.nextInt(100), amountIn, paymentHash, expiryIn, TestConstants.emptyOnionPacket), @@ -750,7 +750,7 @@ object NodeRelayerSpec { val amountIn = incomingAmount / 2 IncomingPaymentPacket.NodeRelayPacket( UpdateAddHtlc(randomBytes32(), Random.nextInt(100), amountIn, paymentHash, expiryIn, TestConstants.emptyOnionPacket), - PaymentOnion.createMultiPartPayload(amountIn, incomingAmount, expiryIn, paymentSecret, None), + PaymentOnion.createMultiPartPayload(amountIn, incomingAmount, expiryIn, Some(paymentSecret), None), PaymentOnion.createNodeRelayPayload(outgoingAmount, expiryOut, outgoingNodeId), nextTrampolinePacket) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/RelayerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/RelayerSpec.scala index 89aaef7e17..1e041e8674 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/RelayerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/RelayerSpec.scala @@ -87,7 +87,7 @@ class RelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("applicat } // we use this to build a valid onion - val Success((cmd, _)) = buildCommand(ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, hops, PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, paymentSecret, None)) + val Success((cmd, _)) = buildCommand(ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, hops, PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, Some(paymentSecret), None)) // and then manually build an htlc val add_ab = UpdateAddHtlc(channelId = randomBytes32(), id = 123456, cmd.amount, cmd.paymentHash, cmd.cltvExpiry, cmd.onion) relayer ! RelayForward(add_ab) @@ -97,14 +97,14 @@ class RelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("applicat test("relay an htlc-add at the final node to the payment handler") { f => import f._ - val Success((cmd, _)) = buildCommand(ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, hops.take(1), PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, paymentSecret, None)) + val Success((cmd, _)) = buildCommand(ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, hops.take(1), PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, Some(paymentSecret), None)) val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amount, cmd.paymentHash, cmd.cltvExpiry, cmd.onion) relayer ! RelayForward(add_ab) val fp = paymentHandler.expectMessageType[FinalPacket] assert(fp.add == add_ab) - assert(fp.payload == PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, paymentSecret, None)) + assert(fp.payload == PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, Some(paymentSecret), None)) register.expectNoMessage(50 millis) } @@ -117,7 +117,7 @@ class RelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("applicat // We simulate a payment split between multiple trampoline routes. val totalAmount = finalAmount * 3 val trampolineHops = NodeHop(a, b, channelUpdate_ab.cltvExpiryDelta, 0 msat) :: Nil - val Success((trampolineAmount, trampolineExpiry, trampolineOnion)) = OutgoingPaymentPacket.buildTrampolinePacket(paymentHash, trampolineHops, PaymentOnion.createMultiPartPayload(finalAmount, totalAmount, finalExpiry, paymentSecret, None)) + val Success((trampolineAmount, trampolineExpiry, trampolineOnion)) = OutgoingPaymentPacket.buildTrampolinePacket(paymentHash, trampolineHops, PaymentOnion.createMultiPartPayload(finalAmount, totalAmount, finalExpiry, Some(paymentSecret), None)) assert(trampolineAmount == finalAmount) assert(trampolineExpiry == finalExpiry) val Success((cmd, _)) = buildCommand(ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, channelHopFromUpdate(a, b, channelUpdate_ab) :: Nil, PaymentOnion.createTrampolinePayload(trampolineAmount, trampolineAmount, trampolineExpiry, randomBytes32(), trampolineOnion.packet)) @@ -132,7 +132,7 @@ class RelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("applicat assert(fp.payload.amount == finalAmount) assert(fp.payload.totalAmount == totalAmount) assert(fp.payload.expiry == finalExpiry) - assert(fp.payload.paymentSecret == paymentSecret) + assert(fp.payload.paymentSecret contains paymentSecret) register.expectNoMessage(50 millis) } @@ -141,7 +141,7 @@ class RelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("applicat import f._ // we use this to build a valid onion - val Success((cmd, _)) = buildCommand(ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, hops, PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, paymentSecret, None)) + val Success((cmd, _)) = buildCommand(ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, hops, PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, Some(paymentSecret), None)) // and then manually build an htlc with an invalid onion (hmac) val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amount, cmd.paymentHash, cmd.cltvExpiry, cmd.onion.copy(hmac = cmd.onion.hmac.reverse)) @@ -162,7 +162,7 @@ class RelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("applicat // we use this to build a valid trampoline onion inside a normal onion val trampolineHops = NodeHop(a, b, channelUpdate_ab.cltvExpiryDelta, 0 msat) :: NodeHop(b, c, channelUpdate_bc.cltvExpiryDelta, fee_b) :: Nil - val Success((trampolineAmount, trampolineExpiry, trampolineOnion)) = OutgoingPaymentPacket.buildTrampolinePacket(paymentHash, trampolineHops, PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, paymentSecret, None)) + val Success((trampolineAmount, trampolineExpiry, trampolineOnion)) = OutgoingPaymentPacket.buildTrampolinePacket(paymentHash, trampolineHops, PaymentOnion.createSinglePartPayload(finalAmount, finalExpiry, Some(paymentSecret), None)) val Success((cmd, _)) = buildCommand(ActorRef.noSender, Upstream.Local(UUID.randomUUID()), paymentHash, channelHopFromUpdate(a, b, channelUpdate_ab) :: Nil, PaymentOnion.createTrampolinePayload(trampolineAmount, trampolineAmount, trampolineExpiry, randomBytes32(), trampolineOnion.packet)) // and then manually build an htlc diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/PaymentOnionSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/PaymentOnionSpec.scala index 0e597c8a75..499ff7e057 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/PaymentOnionSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/PaymentOnionSpec.scala @@ -180,13 +180,13 @@ class PaymentOnionSpec extends AnyFunSuite { assert(multiPart.amount == 561.msat) assert(multiPart.expiry == CltvExpiry(42)) assert(multiPart.totalAmount == 1105.msat) - assert(multiPart.paymentSecret == ByteVector32(hex"eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619")) + assert(multiPart.paymentSecret contains ByteVector32(hex"eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619")) val multiPartNoTotalAmount = finalPerHopPayloadCodec.decode(hex"29 02020231 04012a 0820eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619".bits).require.value assert(multiPartNoTotalAmount.amount == 561.msat) assert(multiPartNoTotalAmount.expiry == CltvExpiry(42)) assert(multiPartNoTotalAmount.totalAmount == 561.msat) - assert(multiPartNoTotalAmount.paymentSecret == ByteVector32(hex"eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619")) + assert(multiPartNoTotalAmount.paymentSecret contains ByteVector32(hex"eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619")) } test("decode variable-length (tlv) relay per-hop payload missing information") { From 0b323f5c970737cb512c7d7096ff49310388516f Mon Sep 17 00:00:00 2001 From: Thomas HUET Date: Tue, 14 Jun 2022 11:13:16 +0200 Subject: [PATCH 117/121] Dummy invoice --- .../main/scala/fr/acinq/eclair/Eclair.scala | 11 ++--- .../fr/acinq/eclair/db/pg/PgPaymentsDb.scala | 12 ++--- .../eclair/db/sqlite/SqlitePaymentsDb.scala | 12 ++--- .../acinq/eclair/json/JsonSerializers.scala | 17 +++++++ .../acinq/eclair/payment/Bolt11Invoice.scala | 41 ----------------- .../acinq/eclair/payment/Bolt12Invoice.scala | 8 ++-- .../fr/acinq/eclair/payment/Invoice.scala | 44 ++++++++++++++++--- .../payment/receive/MultiPartHandler.scala | 5 +-- .../eclair/payment/send/OfferPayment.scala | 2 +- .../eclair/wire/protocol/MessageOnion.scala | 4 +- .../eclair/wire/protocol/OfferCodecs.scala | 5 ++- .../{Offers.scala => OfferTypes.scala} | 2 +- .../integration/OffersIntegrationSpec.scala | 26 ++++++++++- .../eclair/payment/Bolt12InvoiceSpec.scala | 6 +-- ...{OffersSpec.scala => OfferTypesSpec.scala} | 6 +-- .../api/directives/ExtraDirectives.scala | 2 +- .../api/serde/FormParamExtractors.scala | 2 +- 17 files changed, 116 insertions(+), 89 deletions(-) rename eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/{Offers.scala => OfferTypes.scala} (99%) rename eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/{OffersSpec.scala => OfferTypesSpec.scala} (99%) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala index ed26babfd0..9b2bd00346 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala @@ -16,12 +16,9 @@ package fr.acinq.eclair -import akka.actor.ActorRef -import akka.actor.typed.scaladsl.AskPattern.Askable -import akka.actor.typed.scaladsl.adapter.ClassicSchedulerOps -import akka.actor.typed -import akka.actor.typed.scaladsl.AskPattern.schedulerFromActorSystem -import akka.actor.typed.scaladsl.adapter.ClassicActorSystemOps +import akka.actor.{ActorRef, typed} +import akka.actor.typed.scaladsl.AskPattern.{Askable, schedulerFromActorSystem} +import akka.actor.typed.scaladsl.adapter.{ClassicActorSystemOps, ClassicSchedulerOps} import akka.pattern._ import akka.util.Timeout import com.softwaremill.quicklens.ModifyPimp @@ -50,7 +47,7 @@ import fr.acinq.eclair.payment.send.PaymentInitiator._ import fr.acinq.eclair.router.Router import fr.acinq.eclair.router.Router._ import fr.acinq.eclair.wire.protocol.MessageOnionCodecs.blindedRouteCodec -import fr.acinq.eclair.wire.protocol.Offers.Offer +import fr.acinq.eclair.wire.protocol.OfferTypes.Offer import fr.acinq.eclair.wire.protocol._ import grizzled.slf4j.Logging import scodec.bits.ByteVector diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgPaymentsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgPaymentsDb.scala index 2f0a80ee01..9afcac83bc 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgPaymentsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgPaymentsDb.scala @@ -23,7 +23,7 @@ import fr.acinq.eclair.db.Monitoring.Tags.DbBackends import fr.acinq.eclair.db.PaymentsDb.{decodeFailures, decodeRoute, encodeFailures, encodeRoute} import fr.acinq.eclair.db._ import fr.acinq.eclair.db.pg.PgUtils.PgLock -import fr.acinq.eclair.payment.{Bolt11Invoice, Invoice, PaymentFailed, PaymentSent} +import fr.acinq.eclair.payment.{Invoice, PaymentFailed, PaymentSent} import fr.acinq.eclair.{MilliSatoshi, TimestampMilli, TimestampMilliLong} import grizzled.slf4j.Logging import scodec.bits.BitVector @@ -246,19 +246,19 @@ class PgPaymentsDb(implicit ds: DataSource, lock: PgLock) extends PaymentsDb wit } private def parseIncomingPayment(rs: ResultSet): IncomingPayment = { - val invoice = rs.getString("payment_request") + val invoice = Invoice.fromString(rs.getString("payment_request")).get IncomingPayment( - Bolt11Invoice.fromString(invoice).get, + invoice, rs.getByteVector32FromHex("payment_preimage"), rs.getString("payment_type"), TimestampMilli.fromSqlTimestamp(rs.getTimestamp("created_at")), buildIncomingPaymentStatus(rs.getMilliSatoshiNullable("received_msat"), Some(invoice), rs.getTimestampNullable("received_at").map(TimestampMilli.fromSqlTimestamp))) } - private def buildIncomingPaymentStatus(amount_opt: Option[MilliSatoshi], serializedInvoice_opt: Option[String], receivedAt_opt: Option[TimestampMilli]): IncomingPaymentStatus = { + private def buildIncomingPaymentStatus(amount_opt: Option[MilliSatoshi], invoice_opt: Option[Invoice], receivedAt_opt: Option[TimestampMilli]): IncomingPaymentStatus = { amount_opt match { case Some(amount) => IncomingPaymentStatus.Received(amount, receivedAt_opt.getOrElse(0 unixms)) - case None if serializedInvoice_opt.exists(Bolt11Invoice.fastHasExpired) => IncomingPaymentStatus.Expired + case None if invoice_opt.exists(_.isExpired()) => IncomingPaymentStatus.Expired case None => IncomingPaymentStatus.Pending } } @@ -394,7 +394,7 @@ class PgPaymentsDb(implicit ds: DataSource, lock: PgLock) extends PaymentsDb wit val expireAt_opt = rs.getTimestampNullable("expire_at").map(TimestampMilli.fromSqlTimestamp) if (rs.getString("type") == "received") { - val status: IncomingPaymentStatus = buildIncomingPaymentStatus(amount_opt, invoice_opt, completedAt_opt) + val status: IncomingPaymentStatus = buildIncomingPaymentStatus(amount_opt, invoice_opt.map(Invoice.fromString(_).get), completedAt_opt) PlainIncomingPayment(paymentHash, paymentType, amount_opt, invoice_opt, status, createdAt, completedAt_opt, expireAt_opt) } else { val preimage_opt = rs.getByteVector32Nullable("payment_preimage") diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala index 98a321fc51..603298b646 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePaymentsDb.scala @@ -237,7 +237,7 @@ class SqlitePaymentsDb(val sqlite: Connection) extends PaymentsDb with Logging { statement.setBytes(2, preimage.toArray) statement.setString(3, paymentType) statement.setString(4, invoice.toString) - statement.setLong(5, invoice.createdAt.toTimestampMilli.toLong) // BOLT11 timestamp is in seconds + statement.setLong(5, invoice.createdAt.toTimestampMilli.toLong) statement.setLong(6, (invoice.createdAt + invoice.relativeExpiry).toLong.seconds.toMillis) statement.executeUpdate() } @@ -254,19 +254,19 @@ class SqlitePaymentsDb(val sqlite: Connection) extends PaymentsDb with Logging { } private def parseIncomingPayment(rs: ResultSet): IncomingPayment = { - val invoice = rs.getString("payment_request") + val invoice = Invoice.fromString(rs.getString("payment_request")).get IncomingPayment( - Bolt11Invoice.fromString(invoice).get, + invoice, rs.getByteVector32("payment_preimage"), rs.getString("payment_type"), TimestampMilli(rs.getLong("created_at")), buildIncomingPaymentStatus(rs.getMilliSatoshiNullable("received_msat"), Some(invoice), rs.getLongNullable("received_at").map(TimestampMilli(_)))) } - private def buildIncomingPaymentStatus(amount_opt: Option[MilliSatoshi], serializedInvoice_opt: Option[String], receivedAt_opt: Option[TimestampMilli]): IncomingPaymentStatus = { + private def buildIncomingPaymentStatus(amount_opt: Option[MilliSatoshi], invoice_opt: Option[Invoice], receivedAt_opt: Option[TimestampMilli]): IncomingPaymentStatus = { amount_opt match { case Some(amount) => IncomingPaymentStatus.Received(amount, receivedAt_opt.getOrElse(0 unixms)) - case None if serializedInvoice_opt.exists(Bolt11Invoice.fastHasExpired) => IncomingPaymentStatus.Expired + case None if invoice_opt.exists(_.isExpired()) => IncomingPaymentStatus.Expired case None => IncomingPaymentStatus.Pending } } @@ -385,7 +385,7 @@ class SqlitePaymentsDb(val sqlite: Connection) extends PaymentsDb with Logging { val expireAt_opt = rs.getLongNullable("expire_at").map(TimestampMilli(_)) if (rs.getString("type") == "received") { - val status: IncomingPaymentStatus = buildIncomingPaymentStatus(amount_opt, invoice_opt, completedAt_opt) + val status: IncomingPaymentStatus = buildIncomingPaymentStatus(amount_opt, invoice_opt.map(Invoice.fromString(_).get), completedAt_opt) PlainIncomingPayment(paymentHash, paymentType, amount_opt, invoice_opt, status, createdAt, completedAt_opt, expireAt_opt) } else { val preimage_opt = rs.getByteVector32Nullable("payment_preimage") diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/json/JsonSerializers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/json/JsonSerializers.scala index 2bb7bc22a4..fedb546410 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/json/JsonSerializers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/json/JsonSerializers.scala @@ -393,6 +393,23 @@ object InvoiceSerializer extends MinimalSerializer({ routingInfo JObject(fieldList) + case invoice: Invoice => + val features = JField("features", Extraction.decompose(invoice.features)( + DefaultFormats + + FeatureKeySerializer + + FeatureSupportSerializer + + UnknownFeatureSerializer + )) + JObject( + JField("serialized", JString(invoice.toString)), + JField("amount", JLong(invoice.amount_opt.get.toLong)), + JField("nodeId", JString(invoice.nodeId.toString())), + invoice.description.fold(string => JField("description", JString(string)), hash => JField("descriptionHash", JString(hash.toHex))), + JField("paymentHash", JString(invoice.paymentHash.toString())), + JField("timestamp", JLong(invoice.createdAt.toLong)), + JField("relativeExpiry", JLong(invoice.relativeExpiry.toSeconds)), + JField("minFinalCltvExpiry", JInt(invoice.minFinalCltvExpiryDelta.toInt)), + features) }) object JavaUUIDSerializer extends MinimalSerializer({ diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Bolt11Invoice.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Bolt11Invoice.scala index 558715e006..03857806dc 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Bolt11Invoice.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Bolt11Invoice.scala @@ -546,47 +546,6 @@ object Bolt11Invoice { signature = bolt11Data.signature) } - private def readBoltData(input: String): Bolt11Data = { - val lowercaseInput = input.toLowerCase - val separatorIndex = lowercaseInput.lastIndexOf('1') - val hrp = lowercaseInput.take(separatorIndex) - if (!prefixes.values.exists(prefix => hrp.startsWith(prefix))) throw new RuntimeException("unknown prefix") - val data = string2Bits(lowercaseInput.slice(separatorIndex + 1, lowercaseInput.length - 6)) // 6 == checksum size - Codecs.bolt11DataCodec.decode(data).require.value - } - - /** - * Extracts the description from a serialized invoice that is **expected to be valid**. - * Throws an error if the invoice is not valid. - * - * @param input valid serialized invoice - * @return description as a String. If the description is a hash, returns the hash value as a String. - */ - def fastReadDescription(input: String): String = { - readBoltData(input).taggedFields.collectFirst { - case Bolt11Invoice.Description(d) => d - case Bolt11Invoice.DescriptionHash(h) => h.toString() - }.get - } - - /** - * Checks if a serialized invoice is expired. Timestamp is compared to the System's current time. - * - * @param input valid serialized invoice - * @return true if the invoice has expired, false otherwise. - */ - def fastHasExpired(input: String): Boolean = { - val bolt11Data = readBoltData(input) - val expiry_opt = bolt11Data.taggedFields.collectFirst { - case p: Bolt11Invoice.Expiry => p - } - val timestamp = bolt11Data.timestamp - expiry_opt match { - case Some(expiry) => timestamp + expiry.toLong <= TimestampSecond.now() - case None => timestamp + DEFAULT_EXPIRY_SECONDS <= TimestampSecond.now() - } - } - def toExtraEdges(extraRoute: Seq[ExtraHop], targetNodeId: PublicKey): Seq[Invoice.ExtraEdge] = { // BOLT 11: "For each entry, the pubkey is the node ID of the start of the channel", and the last node is the destination val nextNodeIds = extraRoute.map(_.nodeId).drop(1) :+ targetNodeId diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Bolt12Invoice.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Bolt12Invoice.scala index ca204beeb2..2038100bfa 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Bolt12Invoice.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Bolt12Invoice.scala @@ -21,8 +21,8 @@ import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, ByteVector64, Crypto} import fr.acinq.eclair.crypto.Sphinx.RouteBlinding import fr.acinq.eclair.wire.protocol.OfferCodecs.{invoiceCodec, invoiceTlvCodec} -import fr.acinq.eclair.wire.protocol.Offers._ -import fr.acinq.eclair.wire.protocol.{Offers, TlvStream} +import fr.acinq.eclair.wire.protocol.OfferTypes._ +import fr.acinq.eclair.wire.protocol.{OfferTypes, TlvStream} import fr.acinq.eclair.{CltvExpiryDelta, Features, InvoiceFeature, MilliSatoshi, TimestampSecond} import scodec.bits.ByteVector @@ -102,7 +102,7 @@ case class Bolt12Invoice(records: TlvStream[InvoiceTlv], nodeId_opt: Option[Publ // It is assumed that the request is valid for this offer. def isValidFor(offer: Offer, request: InvoiceRequest): Boolean = { - Offers.xOnlyPublicKey(nodeId) == offer.nodeIdXOnly.xOnly && + OfferTypes.xOnlyPublicKey(nodeId) == offer.nodeIdXOnly.xOnly && checkSignature() && offerId.contains(request.offerId) && request.chain == chain && @@ -126,7 +126,7 @@ case class Bolt12Invoice(records: TlvStream[InvoiceTlv], nodeId_opt: Option[Publ } def checkSignature(): Boolean = { - verifySchnorr(signatureTag("signature"), rootHash(Offers.removeSignature(records), invoiceTlvCodec), signature, Offers.xOnlyPublicKey(nodeId)) + verifySchnorr(signatureTag("signature"), rootHash(OfferTypes.removeSignature(records), invoiceTlvCodec), signature, OfferTypes.xOnlyPublicKey(nodeId)) } def withNodeId(nodeId: PublicKey): Bolt12Invoice = Bolt12Invoice(records, Some(nodeId)) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Invoice.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Invoice.scala index e36e427045..ccb9741c27 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Invoice.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Invoice.scala @@ -18,11 +18,14 @@ package fr.acinq.eclair.payment import fr.acinq.bitcoin.scalacompat.ByteVector32 import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey +import fr.acinq.eclair.payment.DummyInvoice.dummyInvoiceCodec import fr.acinq.eclair.payment.relay.Relayer import fr.acinq.eclair.wire.protocol.ChannelUpdate import fr.acinq.eclair.{CltvExpiryDelta, Features, InvoiceFeature, MilliSatoshi, MilliSatoshiLong, ShortChannelId, TimestampSecond} +import scodec.Codec import scodec.bits.ByteVector +import java.util.concurrent.TimeUnit import scala.concurrent.duration.FiniteDuration import scala.util.Try @@ -81,20 +84,49 @@ object Invoice { def fromString(input: String): Try[Invoice] = { if (input.toLowerCase.startsWith("lni")) { Bolt12Invoice.fromString(input) + } else if (input.startsWith("lnd")) { + DummyInvoice.fromString(input) } else { Bolt11Invoice.fromString(input) } } } -case class DummyInvoice(amount_opt: Option[MilliSatoshi], +/** Dummy invoice used for unsolicited payments + */ +case class DummyInvoice(amount: MilliSatoshi, createdAt: TimestampSecond, nodeId: PublicKey, paymentHash: ByteVector32, paymentSecret: Option[ByteVector32], - paymentMetadata: Option[ByteVector], - description: Either[String, ByteVector32], - extraEdges: Seq[Invoice.ExtraEdge], - relativeExpiry: FiniteDuration, minFinalCltvExpiryDelta: CltvExpiryDelta, - features: Features[InvoiceFeature]) extends Invoice + features: Features[InvoiceFeature]) extends Invoice { + override val amount_opt: Option[MilliSatoshi] = Some(amount) + override val paymentMetadata: Option[ByteVector] = None + override val description: Either[String, ByteVector32] = Left("Donation") + override val extraEdges: Seq[Invoice.ExtraEdge] = Seq.empty + override val relativeExpiry: FiniteDuration = FiniteDuration(Bolt11Invoice.DEFAULT_EXPIRY_SECONDS, TimeUnit.SECONDS) + + override def toString: String = { + "lnd" + dummyInvoiceCodec.encode(this).require.bytes.toBase64 + } +} + +object DummyInvoice { + + import fr.acinq.eclair.wire.protocol.CommonCodecs._ + import scodec.codecs._ + + private val dummyInvoiceCodec: Codec[DummyInvoice] = ( + ("amount" | millisatoshi) :: + ("createdAt" | timestampSecond) :: + ("nodeId" | publicKey) :: + ("paymentHash" | bytes32) :: + ("paymentSecret" | optional(bool8, bytes32)) :: + ("minFinalCltvExpiryDelta" | cltvExpiryDelta) :: + ("features" | bytes.xmap[Features[InvoiceFeature]](Features(_).invoiceFeatures(), _.toByteVector))).as[DummyInvoice] + + def fromString(input: String): Try[DummyInvoice] = Try { + dummyInvoiceCodec.decodeValue(ByteVector.fromValidBase64(input.stripPrefix("lnd")).bits).require + } +} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/MultiPartHandler.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/MultiPartHandler.scala index bd6c196d30..45dabd7001 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/MultiPartHandler.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/MultiPartHandler.scala @@ -91,16 +91,15 @@ class MultiPartHandler(nodeParams: NodeParams, register: ActorRef, db: IncomingP } case None => p.payload.paymentPreimage match { case Some(paymentPreimage) if nodeParams.features.hasFeature(Features.KeySend) => - val amount = Some(p.payload.totalAmount) + val amount = p.payload.totalAmount val paymentHash = Crypto.sha256(paymentPreimage) - val desc = Left("Donation") val features = if (nodeParams.features.hasFeature(Features.BasicMultiPartPayment)) { Features[InvoiceFeature](Features.BasicMultiPartPayment -> FeatureSupport.Optional, Features.PaymentSecret -> FeatureSupport.Mandatory, Features.VariableLengthOnion -> FeatureSupport.Mandatory) } else { Features[InvoiceFeature](Features.PaymentSecret -> FeatureSupport.Mandatory, Features.VariableLengthOnion -> FeatureSupport.Mandatory) } // Insert a fake invoice and then restart the incoming payment handler - val invoice = DummyInvoice(amount, TimestampSecond.now(), nodeParams.privateKey.publicKey, paymentHash, p.payload.paymentSecret, None, desc, Nil, FiniteDuration(Bolt11Invoice.DEFAULT_EXPIRY_SECONDS, TimeUnit.SECONDS), nodeParams.channelConf.minFinalExpiryDelta, features) + val invoice = DummyInvoice(amount, TimestampSecond.now(), nodeParams.privateKey.publicKey, paymentHash, p.payload.paymentSecret, nodeParams.channelConf.minFinalExpiryDelta, features) log.debug("generated fake invoice={} from amount={} (KeySend)", invoice.toString, amount) db.addIncomingPayment(invoice, paymentPreimage, paymentType = PaymentType.KeySend) ctx.self ! p diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/OfferPayment.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/OfferPayment.scala index 4b93448a23..8061534d32 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/OfferPayment.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/OfferPayment.scala @@ -28,7 +28,7 @@ import fr.acinq.eclair.message.{OnionMessages, Postman} import fr.acinq.eclair.payment.send.MultiPartPaymentLifecycle.PreimageReceived import fr.acinq.eclair.payment.send.PaymentInitiator.SendPaymentToNode import fr.acinq.eclair.payment.{Bolt12Invoice, PaymentFailed, PaymentSent} -import fr.acinq.eclair.wire.protocol.Offers.{InvoiceRequest, Offer} +import fr.acinq.eclair.wire.protocol.OfferTypes.{InvoiceRequest, Offer} import fr.acinq.eclair.wire.protocol.OnionMessagePayloadTlv.ReplyPath import fr.acinq.eclair.wire.protocol.{OnionMessage, OnionMessagePayloadTlv} import fr.acinq.eclair.{Features, InvoiceFeature, MilliSatoshi, NodeParams, TimestampSecond, randomBytes32, randomKey} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/MessageOnion.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/MessageOnion.scala index 5db9056fd9..226c48c70a 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/MessageOnion.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/MessageOnion.scala @@ -47,7 +47,7 @@ object OnionMessagePayloadTlv { * In order to pay a Bolt 12 offer, we must send an onion message to request an invoice corresponding to that offer. * The creator of the offer will send us an invoice back through our blinded reply path. */ - case class InvoiceRequest(request: Offers.InvoiceRequest) extends OnionMessagePayloadTlv + case class InvoiceRequest(request: OfferTypes.InvoiceRequest) extends OnionMessagePayloadTlv /** * When receiving an invoice request, we must send an onion message back containing an invoice corresponding to the @@ -59,7 +59,7 @@ object OnionMessagePayloadTlv { * This message may be used when we receive an invalid invoice or invoice request. * It contains information helping senders figure out why their message was invalid. */ - case class InvoiceError(error: Offers.InvoiceError) extends OnionMessagePayloadTlv + case class InvoiceError(error: OfferTypes.InvoiceError) extends OnionMessagePayloadTlv } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/OfferCodecs.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/OfferCodecs.scala index 948ed8bd16..df5a06f22d 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/OfferCodecs.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/OfferCodecs.scala @@ -17,11 +17,10 @@ package fr.acinq.eclair.wire.protocol import fr.acinq.bitcoin.scalacompat.ByteVector32 -import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.eclair.crypto.Sphinx.RouteBlinding.{BlindedNode, BlindedRoute} import fr.acinq.eclair.payment.Bolt12Invoice import fr.acinq.eclair.wire.protocol.CommonCodecs._ -import fr.acinq.eclair.wire.protocol.Offers._ +import fr.acinq.eclair.wire.protocol.OfferTypes._ import fr.acinq.eclair.wire.protocol.OnionRoutingCodecs.MissingRequiredTlv import fr.acinq.eclair.wire.protocol.TlvCodecs.{tmillisatoshi, tu32, tu64overflow} import fr.acinq.eclair.{CltvExpiryDelta, Feature, Features, TimestampSecond, UInt64} @@ -189,6 +188,8 @@ object OfferCodecs { Attempt.failure(MissingRequiredTlv(UInt64(8))) } else if (tlvs.get[Description].isEmpty) { Attempt.failure(MissingRequiredTlv(UInt64(10))) + } else if (tlvs.get[Paths].isEmpty) { + Attempt.failure(MissingRequiredTlv(UInt64(16))) } else if (tlvs.get[NodeIdXOnly].isEmpty) { Attempt.failure(MissingRequiredTlv(UInt64(30))) } else if (tlvs.get[CreatedAt].isEmpty) { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/Offers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/OfferTypes.scala similarity index 99% rename from eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/Offers.scala rename to eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/OfferTypes.scala index e1c3580a74..1c762a2882 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/Offers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/OfferTypes.scala @@ -34,7 +34,7 @@ import scala.util.Try * Lightning Bolt 12 offers * see https://github.com/lightning/bolts/blob/master/12-offer-encoding.md */ -object Offers { +object OfferTypes { sealed trait Bolt12Tlv extends Tlv diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/OffersIntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/OffersIntegrationSpec.scala index fd52d36b20..b41ef6625f 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/OffersIntegrationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/OffersIntegrationSpec.scala @@ -25,10 +25,13 @@ import fr.acinq.bitcoin.scalacompat.SatoshiLong import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.{Watch, WatchFundingConfirmed} import fr.acinq.eclair.channel.{ChannelStateChanged, NORMAL} +import fr.acinq.eclair.db.{OutgoingPayment, OutgoingPaymentStatus} +import fr.acinq.eclair.db.OutgoingPaymentStatus.Succeeded import fr.acinq.eclair.message.OnionMessages -import fr.acinq.eclair.payment.Bolt12Invoice +import fr.acinq.eclair.payment.{Bolt12Invoice, PaymentEvent} +import fr.acinq.eclair.payment.send.MultiPartPaymentLifecycle.PreimageReceived import fr.acinq.eclair.wire.protocol.MessageOnionCodecs.perHopPayloadCodec -import fr.acinq.eclair.wire.protocol.Offers.Offer +import fr.acinq.eclair.wire.protocol.OfferTypes.Offer import fr.acinq.eclair.wire.protocol.OnionMessagePayloadTlv.Invoice import fr.acinq.eclair.wire.protocol.TlvStream import fr.acinq.eclair.{EclairImpl, Features, MilliSatoshiLong, PayOfferResponse, randomBytes32} @@ -75,6 +78,25 @@ class OffersIntegrationSpec extends IntegrationSpec { } } + test("simple bolt11 payment") { + val alice = new EclairImpl(nodes("A")) + val bob = new EclairImpl(nodes("B")) + + val preimage = randomBytes32() + val invoice = Await.result(alice.receive(Left("simple bolt11 invoice"), Some(100000 msat), None, None, Some(preimage)), 10 seconds) + + bob.send(None,100000 msat, invoice) + + Thread.sleep(2000) + + val probe = TestProbe() + bob.sentInfo(Right(invoice.paymentHash)).pipeTo(probe.ref) + + val response = probe.expectMsgType[Seq[OutgoingPayment]](1 minute) + val OutgoingPaymentStatus.Succeeded(preimageReceived, _, _, _) = response.head.status + assert(preimageReceived === preimage) + } + test("simple offer") { val alice = new EclairImpl(nodes("A")) val bob = new EclairImpl(nodes("B")) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/Bolt12InvoiceSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/Bolt12InvoiceSpec.scala index af8471f066..028ba5c7bb 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/Bolt12InvoiceSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/Bolt12InvoiceSpec.scala @@ -22,8 +22,8 @@ import fr.acinq.eclair.FeatureSupport.{Mandatory, Optional} import fr.acinq.eclair.Features.{BasicMultiPartPayment, VariableLengthOnion} import fr.acinq.eclair.payment.Bolt12Invoice.signatureTag import fr.acinq.eclair.wire.protocol.OfferCodecs.{invoiceRequestTlvCodec, invoiceTlvCodec} -import fr.acinq.eclair.wire.protocol.Offers._ -import fr.acinq.eclair.wire.protocol.{GenericTlv, Offers, TlvStream} +import fr.acinq.eclair.wire.protocol.OfferTypes._ +import fr.acinq.eclair.wire.protocol.{GenericTlv, OfferTypes, TlvStream} import fr.acinq.eclair.{CltvExpiryDelta, Feature, FeatureSupport, Features, MilliSatoshiLong, TimestampSecond, TimestampSecondLong, UInt64, randomBytes32, randomBytes64, randomKey} import org.scalatest.funsuite.AnyFunSuite import scodec.bits._ @@ -34,7 +34,7 @@ import scala.util.Success class Bolt12InvoiceSpec extends AnyFunSuite { def signInvoice(invoice: Bolt12Invoice, key: PrivateKey): Bolt12Invoice = { - val tlvs = Offers.removeSignature(invoice.records) + val tlvs = OfferTypes.removeSignature(invoice.records) val signature = signSchnorr(Bolt12Invoice.signatureTag("signature"), rootHash(tlvs, invoiceTlvCodec), key) val signedInvoice = Bolt12Invoice(tlvs.copy(records = tlvs.records ++ Seq(Signature(signature))), None) assert(signedInvoice.checkSignature()) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/OffersSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/OfferTypesSpec.scala similarity index 99% rename from eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/OffersSpec.scala rename to eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/OfferTypesSpec.scala index 87fc065d86..819b516bae 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/OffersSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/OfferTypesSpec.scala @@ -21,14 +21,14 @@ import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32} import fr.acinq.eclair.FeatureSupport.{Mandatory, Optional} import fr.acinq.eclair.Features.{BasicMultiPartPayment, VariableLengthOnion} import fr.acinq.eclair.wire.protocol.OfferCodecs.invoiceRequestTlvCodec -import fr.acinq.eclair.wire.protocol.Offers._ +import fr.acinq.eclair.wire.protocol.OfferTypes._ import fr.acinq.eclair.{Features, MilliSatoshiLong, randomBytes32, randomKey} import org.scalatest.funsuite.AnyFunSuite import scodec.bits.{ByteVector, HexStringSyntax} import scala.util.Success -class OffersSpec extends AnyFunSuite { +class OfferTypesSpec extends AnyFunSuite { val nodeId = ByteVector32(hex"4b9a1fa8e006f1e3937f65f66c408e6da8e1ca728ea43222a7381df1cc449605") test("sign and check offer") { @@ -275,7 +275,7 @@ class OffersSpec extends AnyFunSuite { val genericTlvStream: Codec[TlvStream[GenericTlv]] = list(TlvCodecs.genericTlv).xmap(tlvs => TlvStream(tlvs), tlvs => tlvs.records.toList) val tlvs = genericTlvStream.decode(tlvStream.bits).require.value assert(tlvs.records.size == tlvCount) - val root = Offers.rootHash(tlvs, genericTlvStream) + val root = OfferTypes.rootHash(tlvs, genericTlvStream) assert(root == expectedRoot) } } diff --git a/eclair-node/src/main/scala/fr/acinq/eclair/api/directives/ExtraDirectives.scala b/eclair-node/src/main/scala/fr/acinq/eclair/api/directives/ExtraDirectives.scala index d9ce249b10..37062f2eb0 100644 --- a/eclair-node/src/main/scala/fr/acinq/eclair/api/directives/ExtraDirectives.scala +++ b/eclair-node/src/main/scala/fr/acinq/eclair/api/directives/ExtraDirectives.scala @@ -27,7 +27,7 @@ import fr.acinq.eclair.ApiTypes.ChannelIdentifier import fr.acinq.eclair.api.serde.FormParamExtractors._ import fr.acinq.eclair.api.serde.JsonSupport._ import fr.acinq.eclair.payment.Bolt11Invoice -import fr.acinq.eclair.wire.protocol.Offers.Offer +import fr.acinq.eclair.wire.protocol.OfferTypes.Offer import fr.acinq.eclair.{MilliSatoshi, ShortChannelId, TimestampSecond} import scala.concurrent.Future diff --git a/eclair-node/src/main/scala/fr/acinq/eclair/api/serde/FormParamExtractors.scala b/eclair-node/src/main/scala/fr/acinq/eclair/api/serde/FormParamExtractors.scala index bb79daf2e4..40c8b98156 100644 --- a/eclair-node/src/main/scala/fr/acinq/eclair/api/serde/FormParamExtractors.scala +++ b/eclair-node/src/main/scala/fr/acinq/eclair/api/serde/FormParamExtractors.scala @@ -27,7 +27,7 @@ import fr.acinq.eclair.crypto.Sphinx import fr.acinq.eclair.io.NodeURI import fr.acinq.eclair.payment.Bolt11Invoice import fr.acinq.eclair.wire.protocol.MessageOnionCodecs.blindedRouteCodec -import fr.acinq.eclair.wire.protocol.Offers.Offer +import fr.acinq.eclair.wire.protocol.OfferTypes.Offer import fr.acinq.eclair.{MilliSatoshi, ShortChannelId, TimestampSecond} import scodec.bits.ByteVector From 87c98db4c0ab7f68565074a7c302232e81874a0a Mon Sep 17 00:00:00 2001 From: Thomas HUET Date: Tue, 19 Jul 2022 16:33:02 +0200 Subject: [PATCH 118/121] Mandatory blinded paths --- .../scala/fr/acinq/eclair/crypto/Sphinx.scala | 14 +++ .../acinq/eclair/payment/Bolt12Invoice.scala | 16 +++- .../integration/OffersIntegrationSpec.scala | 3 +- .../eclair/payment/Bolt12InvoiceSpec.scala | 92 +++++++++++-------- 4 files changed, 84 insertions(+), 41 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Sphinx.scala b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Sphinx.scala index ed55f2071b..df485abcd5 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Sphinx.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Sphinx.scala @@ -19,6 +19,7 @@ package fr.acinq.eclair.crypto import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} import fr.acinq.bitcoin.scalacompat.{ByteVector32, Crypto} import fr.acinq.eclair.crypto.Monitoring.{Metrics, Tags} +import fr.acinq.eclair.wire.protocol.RouteBlindingEncryptedDataCodecs.encryptedDataCodec import fr.acinq.eclair.wire.protocol._ import grizzled.slf4j.Logging import scodec.Attempt @@ -410,6 +411,19 @@ object Sphinx extends Logging { BlindedRoute(publicKeys.head, blindingKeys.head, blindedHops) } + /** + * Creates a direct blinded route with a single node + * + * @param sessionKey this node's session key. + * @param nodeId public key of the target node, which is also the introduction point. + * @param selfId payloads that should be encrypted for each node on the route. + * @return a blinded route. + */ + def createDirect(sessionKey: PrivateKey, nodeId: PublicKey, selfId: ByteVector): BlindedRoute = { + val selfPayload = encryptedDataCodec.encode(TlvStream(Seq(RouteBlindingEncryptedDataTlv.PathId(selfId)))).require.bytes + Sphinx.RouteBlinding.create(sessionKey, Seq(nodeId), Seq(selfPayload)) + } + /** * Compute the blinded private key that must be used to decrypt an incoming blinded onion. * diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Bolt12Invoice.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Bolt12Invoice.scala index 2038100bfa..0cc9648138 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Bolt12Invoice.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Bolt12Invoice.scala @@ -19,11 +19,12 @@ package fr.acinq.eclair.payment import fr.acinq.bitcoin.Bech32 import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, ByteVector64, Crypto} +import fr.acinq.eclair.crypto.Sphinx import fr.acinq.eclair.crypto.Sphinx.RouteBlinding import fr.acinq.eclair.wire.protocol.OfferCodecs.{invoiceCodec, invoiceTlvCodec} import fr.acinq.eclair.wire.protocol.OfferTypes._ import fr.acinq.eclair.wire.protocol.{OfferTypes, TlvStream} -import fr.acinq.eclair.{CltvExpiryDelta, Features, InvoiceFeature, MilliSatoshi, TimestampSecond} +import fr.acinq.eclair.{CltvExpiryDelta, Features, InvoiceFeature, MilliSatoshi, TimestampSecond, randomKey} import scodec.bits.ByteVector import java.util.concurrent.TimeUnit @@ -40,6 +41,7 @@ case class Bolt12Invoice(records: TlvStream[InvoiceTlv], nodeId_opt: Option[Publ require(records.get[Amount].nonEmpty, "bolt 12 invoices must provide an amount") require(records.get[NodeIdXOnly].nonEmpty, "bolt 12 invoices must provide a node id") + require(records.get[Paths].exists(_.paths.nonEmpty), "bolt 12 invoices must provide a blinded path") require(records.get[PaymentHash].nonEmpty, "bolt 12 invoices must provide a payment hash") require(records.get[Description].nonEmpty, "bolt 12 invoices must provide a description") require(records.get[CreatedAt].nonEmpty, "bolt 12 invoices must provide a creation timestamp") @@ -78,7 +80,7 @@ case class Bolt12Invoice(records: TlvStream[InvoiceTlv], nodeId_opt: Option[Publ val offerId: Option[ByteVector32] = records.get[OfferId].map(_.offerId) - val blindedPaths: Option[Seq[RouteBlinding.BlindedRoute]] = records.get[Paths].map(_.paths) + val blindedPaths: Seq[RouteBlinding.BlindedRoute] = records.get[Paths].get.paths val issuer: Option[String] = records.get[Issuer].map(_.issuer) @@ -145,8 +147,15 @@ object Bolt12Invoice { * @param preimage the preimage to use for the payment * @param nodeKey the key that was used to generate the offer, may be different from our public nodeId if we're hiding behind a blinded route * @param features invoice features + * @param selfId data to identify the payment when receiving it, can contain anything and is not readable by the payer */ - def apply(offer: Offer, request: InvoiceRequest, preimage: ByteVector32, nodeKey: PrivateKey, minFinalCltvExpiryDelta: CltvExpiryDelta, features: Features[InvoiceFeature]): Bolt12Invoice = { + def apply(offer: Offer, + request: InvoiceRequest, + preimage: ByteVector32, + nodeKey: PrivateKey, + minFinalCltvExpiryDelta: CltvExpiryDelta, + features: Features[InvoiceFeature], + selfId: ByteVector): Bolt12Invoice = { require(request.amount.nonEmpty || offer.amount.nonEmpty) val tlvs: Seq[InvoiceTlv] = Seq( Some(Chain(request.chain)), @@ -154,6 +163,7 @@ object Bolt12Invoice { Some(PaymentHash(Crypto.sha256(preimage))), Some(OfferId(offer.offerId)), Some(NodeIdXOnly(xOnlyPublicKey(nodeKey.publicKey))), + Some(Paths(Seq(Sphinx.RouteBlinding.createDirect(randomKey(), nodeKey.publicKey, selfId)))), Some(Amount(request.amount.orElse(offer.amount.map(_ * request.quantity)).get)), Some(Description(offer.description)), request.quantity_opt.map(Quantity), diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/OffersIntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/OffersIntegrationSpec.scala index b41ef6625f..76a76aad40 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/OffersIntegrationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/OffersIntegrationSpec.scala @@ -35,6 +35,7 @@ import fr.acinq.eclair.wire.protocol.OfferTypes.Offer import fr.acinq.eclair.wire.protocol.OnionMessagePayloadTlv.Invoice import fr.acinq.eclair.wire.protocol.TlvStream import fr.acinq.eclair.{EclairImpl, Features, MilliSatoshiLong, PayOfferResponse, randomBytes32} +import scodec.bits.ByteVector import scala.concurrent.Await import scala.concurrent.ExecutionContext.Implicits.global @@ -115,7 +116,7 @@ class OffersIntegrationSpec extends IntegrationSpec { val invoiceRequestPayload = eventListener.expectMsgType[OnionMessages.ReceiveMessage].finalPayload val invoiceRequest = invoiceRequestPayload.invoiceRequest.get.request val invoiceRequestReplyPath = invoiceRequestPayload.replyPath.get.blindedRoute - val invoice = Bolt12Invoice(offer, invoiceRequest, preimage, nodes("A").nodeParams.privateKey, dummyInvoice.minFinalCltvExpiryDelta, Features.empty) + val invoice = Bolt12Invoice(offer, invoiceRequest, preimage, nodes("A").nodeParams.privateKey, dummyInvoice.minFinalCltvExpiryDelta, Features.empty, randomBytes32()) val encodedInvoice = perHopPayloadCodec.encode(TlvStream(Invoice(invoice))).require.bytes alice.sendOnionMessage(Nil, Right(invoiceRequestReplyPath), None, encodedInvoice) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/Bolt12InvoiceSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/Bolt12InvoiceSpec.scala index 028ba5c7bb..b91a833205 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/Bolt12InvoiceSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/Bolt12InvoiceSpec.scala @@ -16,11 +16,13 @@ package fr.acinq.eclair.payment +import fr.acinq.bitcoin.Bech32 import fr.acinq.bitcoin.scalacompat.Crypto.PrivateKey import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, Crypto} import fr.acinq.eclair.FeatureSupport.{Mandatory, Optional} import fr.acinq.eclair.Features.{BasicMultiPartPayment, VariableLengthOnion} -import fr.acinq.eclair.payment.Bolt12Invoice.signatureTag +import fr.acinq.eclair.crypto.Sphinx +import fr.acinq.eclair.payment.Bolt12Invoice.{hrp, signatureTag} import fr.acinq.eclair.wire.protocol.OfferCodecs.{invoiceRequestTlvCodec, invoiceTlvCodec} import fr.acinq.eclair.wire.protocol.OfferTypes._ import fr.acinq.eclair.wire.protocol.{GenericTlv, OfferTypes, TlvStream} @@ -33,10 +35,14 @@ import scala.util.Success class Bolt12InvoiceSpec extends AnyFunSuite { + def signInvoiceTlvs(tlvs: TlvStream[InvoiceTlv], key: PrivateKey): TlvStream[InvoiceTlv] = { + val signature = signSchnorr(Bolt12Invoice.signatureTag("signature"), rootHash(tlvs, invoiceTlvCodec), key) + tlvs.copy(records = tlvs.records ++ Seq(Signature(signature))) + } + def signInvoice(invoice: Bolt12Invoice, key: PrivateKey): Bolt12Invoice = { val tlvs = OfferTypes.removeSignature(invoice.records) - val signature = signSchnorr(Bolt12Invoice.signatureTag("signature"), rootHash(tlvs, invoiceTlvCodec), key) - val signedInvoice = Bolt12Invoice(tlvs.copy(records = tlvs.records ++ Seq(Signature(signature))), None) + val signedInvoice = Bolt12Invoice(signInvoiceTlvs(tlvs, key), None) assert(signedInvoice.checkSignature()) signedInvoice } @@ -45,7 +51,7 @@ class Bolt12InvoiceSpec extends AnyFunSuite { val (nodeKey, payerKey, chain) = (randomKey(), randomKey(), randomBytes32()) val offer = Offer(Some(10000 msat), "test offer", nodeKey.publicKey, Features.empty, chain) val request = InvoiceRequest(offer, 11000 msat, 1, Features.empty, payerKey, chain) - val invoice = Bolt12Invoice(offer, request, randomBytes32(), nodeKey, CltvExpiryDelta(20), Features.empty) + val invoice = Bolt12Invoice(offer, request, randomBytes32(), nodeKey, CltvExpiryDelta(20), Features.empty, randomBytes32()) assert(invoice.isValidFor(offer, request)) assert(invoice.checkSignature()) assert(!invoice.checkRefundSignature()) @@ -70,7 +76,7 @@ class Bolt12InvoiceSpec extends AnyFunSuite { val (nodeKey, payerKey, chain) = (randomKey(), randomKey(), randomBytes32()) val offer = Offer(Some(10000 msat), "test offer", nodeKey.publicKey, Features.empty, chain) val request = InvoiceRequest(offer, 11000 msat, 1, Features.empty, payerKey, chain) - val invoice = Bolt12Invoice(offer, request, randomBytes32(), nodeKey, CltvExpiryDelta(20), Features.empty) + val invoice = Bolt12Invoice(offer, request, randomBytes32(), nodeKey, CltvExpiryDelta(20), Features.empty, randomBytes32()) assert(invoice.isValidFor(offer, request)) assert(!invoice.isValidFor(Offer(None, "test offer", randomKey().publicKey, Features.empty, chain), request)) // amount must match the offer @@ -98,7 +104,7 @@ class Bolt12InvoiceSpec extends AnyFunSuite { val offer = Offer(Some(15000 msat), "test offer", nodeKey.publicKey, Features(VariableLengthOnion -> Mandatory), chain) val request = InvoiceRequest(offer, 15000 msat, 1, Features(VariableLengthOnion -> Mandatory), payerKey, chain) assert(request.quantity_opt.isEmpty) // when paying for a single item, the quantity field must not be present - val invoice = Bolt12Invoice(offer, request, randomBytes32(), nodeKey, CltvExpiryDelta(20), Features(VariableLengthOnion -> Mandatory, BasicMultiPartPayment -> Optional)) + val invoice = Bolt12Invoice(offer, request, randomBytes32(), nodeKey, CltvExpiryDelta(20), Features(VariableLengthOnion -> Mandatory, BasicMultiPartPayment -> Optional), randomBytes32()) assert(invoice.isValidFor(offer, request)) val withInvalidFeatures = signInvoice(Bolt12Invoice(TlvStream(invoice.records.records.map { case FeaturesTlv(_) => FeaturesTlv(Features(VariableLengthOnion -> Mandatory, BasicMultiPartPayment -> Mandatory)) case x => x }.toSeq), None), nodeKey) assert(!withInvalidFeatures.isValidFor(offer, request)) @@ -125,7 +131,7 @@ class Bolt12InvoiceSpec extends AnyFunSuite { val signature = signSchnorr(InvoiceRequest.signatureTag, rootHash(TlvStream(tlvs), invoiceRequestTlvCodec), payerKey) InvoiceRequest(TlvStream(tlvs :+ Signature(signature))) } - val withPayerDetails = Bolt12Invoice(offer, requestWithPayerDetails, randomBytes32(), nodeKey, CltvExpiryDelta(20), Features.empty) + val withPayerDetails = Bolt12Invoice(offer, requestWithPayerDetails, randomBytes32(), nodeKey, CltvExpiryDelta(20), Features.empty, randomBytes32()) assert(withPayerDetails.isValidFor(offer, requestWithPayerDetails)) assert(!withPayerDetails.isValidFor(offer, request)) val withOtherPayerInfo = signInvoice(Bolt12Invoice(TlvStream(withPayerDetails.records.records.map { case PayerInfo(_) => PayerInfo(hex"deadbeef") case x => x }.toSeq), None), nodeKey) @@ -140,7 +146,7 @@ class Bolt12InvoiceSpec extends AnyFunSuite { val (nodeKey, payerKey, chain) = (randomKey(), randomKey(), randomBytes32()) val offer = Offer(Some(5000 msat), "test offer", nodeKey.publicKey, Features.empty, chain) val request = InvoiceRequest(offer, 5000 msat, 1, Features.empty, payerKey, chain) - val invoice = Bolt12Invoice(offer, request, randomBytes32(), nodeKey, CltvExpiryDelta(20), Features.empty) + val invoice = Bolt12Invoice(offer, request, randomBytes32(), nodeKey, CltvExpiryDelta(20), Features.empty, randomBytes32()) assert(!invoice.isExpired()) assert(invoice.isValidFor(offer, request)) val expiredInvoice1 = signInvoice(Bolt12Invoice(TlvStream(invoice.records.records.map { case CreatedAt(_) => CreatedAt(0 unixsec) case x => x }), None), nodeKey) @@ -163,6 +169,7 @@ class Bolt12InvoiceSpec extends AnyFunSuite { PaymentHash(Crypto.sha256(randomBytes32())), OfferId(offerBtc.offerId), NodeIdXOnly(nodeKey.publicKey), + Paths(Seq(Sphinx.RouteBlinding.createDirect(randomKey(), nodeKey.publicKey, randomBytes32()))), Amount(amount), Description(offerBtc.description), PayerKey(payerKey.publicKey) @@ -178,6 +185,7 @@ class Bolt12InvoiceSpec extends AnyFunSuite { PaymentHash(Crypto.sha256(randomBytes32())), OfferId(offerBtc.offerId), NodeIdXOnly(nodeKey.publicKey), + Paths(Seq(Sphinx.RouteBlinding.createDirect(randomKey(), nodeKey.publicKey, randomBytes32()))), Amount(amount), Description(offerBtc.description), PayerKey(payerKey.publicKey) @@ -193,6 +201,7 @@ class Bolt12InvoiceSpec extends AnyFunSuite { PaymentHash(Crypto.sha256(randomBytes32())), OfferId(offerBtc.offerId), NodeIdXOnly(nodeKey.publicKey), + Paths(Seq(Sphinx.RouteBlinding.createDirect(randomKey(), nodeKey.publicKey, randomBytes32()))), Amount(amount), Description(offerBtc.description), PayerKey(payerKey.publicKey) @@ -210,6 +219,7 @@ class Bolt12InvoiceSpec extends AnyFunSuite { PaymentHash(Crypto.sha256(randomBytes32())), OfferId(offerOtherChains.offerId), NodeIdXOnly(nodeKey.publicKey), + Paths(Seq(Sphinx.RouteBlinding.createDirect(randomKey(), nodeKey.publicKey, randomBytes32()))), Amount(amount), Description(offerOtherChains.description), PayerKey(payerKey.publicKey) @@ -225,6 +235,7 @@ class Bolt12InvoiceSpec extends AnyFunSuite { PaymentHash(Crypto.sha256(randomBytes32())), OfferId(offerOtherChains.offerId), NodeIdXOnly(nodeKey.publicKey), + Paths(Seq(Sphinx.RouteBlinding.createDirect(randomKey(), nodeKey.publicKey, randomBytes32()))), Amount(amount), Description(offerOtherChains.description), PayerKey(payerKey.publicKey) @@ -237,7 +248,7 @@ class Bolt12InvoiceSpec extends AnyFunSuite { assert(!invoiceMissingChain.isValidFor(offerOtherChains, requestOtherChains)) } - test("decode invoice") { + test("decode simple invoice") { val nodeKey = PrivateKey(hex"c6a75116a91dc5ff741b079c32c8ce7544656b98f047fb0c0fa011bfb2bb3c05") val payerKey = PrivateKey(hex"7dd30ec116470c5f7f00af2c7e84968e28cdb43083b33ee832decbe73ec07f1a") val Success(offer) = Offer.decode("lno1qgsyxjtl6luzd9t3pr62xr7eemp6awnejusgf6gw45q75vcfqqqqqqqgqvqcdgq2pd3xzumfvvsx7enxv4epug9kku8f4e9nuef5lv59yrkdc24t5mtrym62cg085w5wtqkp0rsuly") @@ -245,8 +256,7 @@ class Bolt12InvoiceSpec extends AnyFunSuite { assert(offer.nodeIdXOnly.xOnly == xOnlyPublicKey(nodeKey.publicKey)) assert(offer.chains == Seq(Block.TestnetGenesisBlock.hash)) val request = InvoiceRequest(offer, 100_000 msat, 1, Features.empty, payerKey, Block.TestnetGenesisBlock.hash) - val Success(invoice) = Bolt12Invoice.fromString("lni1qvsyxjtl6luzd9t3pr62xr7eemp6awnejusgf6gw45q75vcfqqqqqqqyyp53zuupqkwxpmdq0tjg58ntat5ujpejlvyn92r0l5xzh4wru8e5zzqrqxr2qzstvfshx6tryphkven9wgxqq83qk6msaxhyk0n9xnajs5swehp24wndvvn0ftppu7363evzc9uwrnujvg95tuyy05nqkcdetsaljgq4u6789jllc54qrpjrzzn3c38dj3tscu5qgcs2y4lj5gqlvq50uu7sce478j3j0l599nxfs6svx2cfefgn4a0675893wtzuckqfwlcrcq0qspa9zynlpdk9zzechehkemgaksklylxhr7yfjfx6h696th327nm4nsf52xzq0ukchx69g00c4vvk6kzc5jyklneyy05l9tef7a5jcjn5") - assert(!invoice.isExpired()) + val Success(invoice) = Bolt12Invoice.fromString("lni1qvsyxjtl6luzd9t3pr62xr7eemp6awnejusgf6gw45q75vcfqqqqqqqyyp53zuupqkwxpmdq0tjg58ntat5ujpejlvyn92r0l5xzh4wru8e5zzqrqxr2qzstvfshx6tryphkven9wgxqqyycq2mtwr56uje7v560k2zjpmxu9246d43jda9vy8n68289stqh3cw0jqls9hxx3tpkddkun48gvlnefhdkkyhrkycr6dqm77z7qffrvyew8qqsy6tpz4gc080qqrcj59pmfz2n7as9j532qhygpatrw6a4k4u0ctc3qqez8vunpqq4jt2gmtwwgjhzvqs9f80yjvws6g98nsyh64h6wjf84ns48pfkzytgrqz6ld07tl7szug7khd3ug9kku8f4e9nuef5lv59yrkdc24t5mtrym62cg085w5wtqkp0rsulynzpdzlpprayc9krw2u80ujq90xh3evhl799gqcvscs5uwyfmv52ux89qzx94kurg4zpdh4kz4j7wvmx3jln9pqmrdryd9kcp98cv99pleghgmqe7ae6scq9cpqq98sgr0pwptagg7dutnx8mfwe7d84vp36f2afkycfqqum5lx3rz3p9heqx52w7zfytsa09mrq293apty6y330jgv322389cu6548twpxpeay") assert(invoice.isValidFor(offer, request)) } @@ -258,31 +268,36 @@ class Bolt12InvoiceSpec extends AnyFunSuite { assert(offer.nodeIdXOnly.xOnly == xOnlyPublicKey(nodeKey.publicKey)) assert(offer.chains == Seq(Block.LivenetGenesisBlock.hash)) val request = InvoiceRequest(offer, 50_000 msat, 5, Features.empty, payerKey, Block.LivenetGenesisBlock.hash) - val Success(invoice) = Bolt12Invoice.fromString("lni1qss8u47nw2lsgml7fy4jaqwph9f8cl83zfrrhxccvh6076avqzzzv4qgqtp4qzs2vf6kc6eqdanxvetjpsqpug9kku8f4e9nuef5lv59yrkdc24t5mtrym62cg085w5wtqkp0rsulysqzpfxyrat02l8wtgtwuc4h5hw6dxhn0hcpdrtu3dpejfjdlw9h4j3nppxc2qyvg9z0lf2yq7wl9ygd6td4cj7whp3ye4cfxrtu7zq4r2mc0mcdspk3duzv7d0stqyh0upuq8sgr44r7aaluwqfw8pkd9f3cgk7ae2l8rkexznhegr0p7w4mlhvfkvlnr5k2lnw0hhsf6ckys3sst7kng5p7m2pxlvdxl3tan809vkk75j") - assert(!invoice.isExpired()) + val Success(invoice) = Bolt12Invoice.fromString("lni1qvsxlc5vp2m0rvmjcxn2y34wv0m5lyc7sdj7zksgn35dvxgqqqqqqqqyypl905mjhuzxlljf9vhgrsde2f78eugjgcaekxr97nlkhtqqssn9gzqzcdgq5znzw4kxkgr0venx2usvqqgfsq4kku8f4e9nuef5lv59yrkdc24t5mtrym62cg085w5wtqkp0rsulyp8jup0gy33yx0q4qzvkq82v2u2ms9fsr44m40rtg8a73v5qyrg7vgpqw9pvycg3sf6ppq0nlxj43k36ugcz28nu53wxvg08uummjgr3vgcqqpjdec35dl86njwq5a6lp0vd6tn2nyq095lvy60vl0v57jcq0xu5c7j4qtssfn2uuc27662gq7yyyumtecwrcstddcwntjt8ejnf7eg2g8vms42hfkkxfh54ss70gagukpvz78pe7fqqyzjvg86k747wukskae3t0fwa56d0xl0sz6xhez6rnynym7ut0t9rxzzds5qgckkm3mj5gytaax4mqrj9n37m85fs847yyc7kngwryk8387ggrgd07j5fexershqyqq57pqgjumr63gmrdl2v36ldad2e4s7ae9k4e792alaks3umm0np25jntxxz7sq84udgz7ph3ru4r3xac89mjz6sxyqjq7g9f8p27et76dm25") assert(invoice.amount == 50_000.msat) assert(invoice.quantity.contains(5)) assert(invoice.isValidFor(offer, request)) } test("decode invalid invoice") { - val testCases = Seq( - // Missing amount. - "lni1qssqkquyqnwldm8mjekcm7ztejf0dzhwyvh95l5fz06ztnpfm0sgc3c2pf6x2um5yphkven9wgxqq83q2hfdphr3r8x07ej0z0swprnll58z4jlw36wye7kw63ssm95ru8qjvgxj40x4favsm7uue24lhleg7gng2r69g2plgwxm8xmpuw7wmnyh455qgcs29g3j5g8vpnxqvnzwu0sgqc20hdllxzr4qnpu0zge6drn8p3galht8f62uncyqyrhddpcla8pprdxwdkmjmutya0kvnrpeqjqa75sr02wff0le52ydckr8ww09gg4jyvxyma903fhh6v8t4edftg7vw6qz7h0tz20p5wq", - // Missing node id. - "lni1qssqkquyqnwldm8mjekcm7ztejf0dzhwyvh95l5fz06ztnpfm0sgc3cgqgfcszs2w3jhxapqdanxvetjpsqzvgxj40x4favsm7uue24lhleg7gng2r69g2plgwxm8xmpuw7wmnyh455qgcs29g3j5g8vpnxqvnzwu0sgqc20hdllxzr4qnpu0zge6drn8p3galht8f62uncypc7vg95clf4z8fklzua72nhavtjp5qp0u7pemgpecypn6q809qr5733c9y0thf6fdnegxleeupddgzgsyszrktay6cjv9cld3wzlw5pq", - // Missing payment hash. - "lni1qssqkquyqnwldm8mjekcm7ztejf0dzhwyvh95l5fz06ztnpfm0sgc3cgqgfcszs2w3jhxapqdanxvetjpsqpugz46tgdcugeenlkvncnursgullapc4vhm5wn3x04nk5vyxedqlpcynzp54te420tyxlh8x240al728jy6zs732zs06r3keekc0rhnkue9ad9qzxyz32y0cyqhfql7g2dp8w0s5xc0ccgelq4hrnkgmxxltvdq95rqzpf4e68j60h6dysm3evhnu4rwtrqp3dnekmk9sxklw267axtj6zangxtnfx2pq", - // Missing description. - "lni1qssqkquyqnwldm8mjekcm7ztejf0dzhwyvh95l5fz06ztnpfm0sgc3cgqgfcsrqqrcs9t5ksm3c3nn8lve838c8q3ell6r32e0hga8zvlt8dgcgdj6p7rsfxyrf2hn257kgdlwwv42lmlu50yf59paz59ql58rdnnds7808dejt662qyvg9z5ge2yrkqenqxf38w8cyqv98mkllnpp6sfs783yvax3ensc5wlm4n5a9wfuzqjraxg7gnskte8m9lzn2j5r55a4n3nhhfnflzd5953tau0h2auztf9und4psz6p34wx4vsjxwyvc33lezqvm208ntdczqneylzznt0cg", - // Missing creation date. - "lni1qssqkquyqnwldm8mjekcm7ztejf0dzhwyvh95l5fz06ztnpfm0sgc3cgqgfcszs2w3jhxapqdanxvetjpsqpugz46tgdcugeenlkvncnursgullapc4vhm5wn3x04nk5vyxedqlpcynzp54te420tyxlh8x240al728jy6zs732zs06r3keekc0rhnkue9ad9gswcrxvqexyaclqsps5lwml7vy82pxrc7y3n568xwrz3mlwkwn54e8sgp22vcpqylq4zcxep5faeysey8ucu6wrfgffphlt457rd9f7tlpnyltlqz0yd9eqjcehyu3zwvear2e4ksx32t3qtek9gucrephdmsk0", - // Missing signature. - "lni1qssqkquyqnwldm8mjekcm7ztejf0dzhwyvh95l5fz06ztnpfm0sgc3cgqgfcszs2w3jhxapqdanxvetjpsqpugz46tgdcugeenlkvncnursgullapc4vhm5wn3x04nk5vyxedqlpcynzp54te420tyxlh8x240al728jy6zs732zs06r3keekc0rhnkue9ad9qzxyz32yv4zpmqvesrycnhruzqxznam0lessagyc0rcjxwnguecv280a6e6wjhy", + val nodeKey = randomKey() + val tlvs = Seq[InvoiceTlv]( + Amount(765432 msat), + Description("minimal invoice"), + NodeIdXOnly(nodeKey.publicKey), + Paths(Seq(Sphinx.RouteBlinding.createDirect(randomKey(), randomKey().publicKey, randomBytes32()))), + CreatedAt(TimestampSecond(123456789L)), + PaymentHash(randomBytes32()), ) - for (testCase <- testCases) { - assert(Bolt12Invoice.fromString(testCase).isFailure, testCase) + // This minimal invoice is valid. + val signed = signInvoiceTlvs(TlvStream[InvoiceTlv](tlvs), nodeKey) + val signedEncoded = Bech32.encodeBytes(hrp, invoiceTlvCodec.encode(signed).require.bytes.toArray, Bech32.Encoding.Beck32WithoutChecksum) + assert(Bolt12Invoice.fromString(signedEncoded).isSuccess) + // But removing any TLV makes it invalid. + for (tlv <- tlvs){ + val incomplete = tlvs.filterNot(_ == tlv) + val incompleteSigned = signInvoiceTlvs(TlvStream[InvoiceTlv](incomplete), nodeKey) + val incompleteSignedEncoded = Bech32.encodeBytes(hrp, invoiceTlvCodec.encode(incompleteSigned).require.bytes.toArray, Bech32.Encoding.Beck32WithoutChecksum) + assert(Bolt12Invoice.fromString(incompleteSignedEncoded).isFailure) } + // Missing signature is also invalid. + val unsignedEncoded = Bech32.encodeBytes(hrp, invoiceTlvCodec.encode(TlvStream[InvoiceTlv](tlvs)).require.bytes.toArray, Bech32.Encoding.Beck32WithoutChecksum) + assert(Bolt12Invoice.fromString(unsignedEncoded).isFailure) } test("encode/decode invoice with many fields") { @@ -293,6 +308,7 @@ class Bolt12InvoiceSpec extends AnyFunSuite { val features = Features[Feature](Features.VariableLengthOnion -> FeatureSupport.Mandatory) val issuer = "acinq.co" val nodeKey = PrivateKey(hex"998cf8ecab46f949bb960813b79d3317cabf4193452a211795cd8af1b9a25d90") + val path = Sphinx.RouteBlinding.createDirect(PrivateKey(hex"f0442c17bdd2cefe4a4ede210f163b068bb3fea6113ffacea4f322de7aa9737b"), nodeKey.publicKey, hex"76030536ba732cdc4e7bb0a883750bab2e88cb3dddd042b1952c44b4849c86bb") val quantity = 57 val payerKey = ByteVector32.fromValidHex("8faadd71b1f78b16265e5b061b9d2b88891012dc7ad38626eeaaa2a271615a65") val payerNote = "I'm the king" @@ -311,6 +327,7 @@ class Bolt12InvoiceSpec extends AnyFunSuite { FeaturesTlv(features), Issuer(issuer), NodeIdXOnly(nodeKey.publicKey), + Paths(Seq(path)), Quantity(quantity), PayerKey(payerKey), PayerNote(payerNote), @@ -324,25 +341,26 @@ class Bolt12InvoiceSpec extends AnyFunSuite { ), Seq(GenericTlv(UInt64(311), hex"010203"), GenericTlv(UInt64(313), hex""))) val signature = signSchnorr(Bolt12Invoice.signatureTag("signature"), rootHash(tlvs, invoiceTlvCodec), nodeKey) val invoice = Bolt12Invoice(tlvs.copy(records = tlvs.records ++ Seq(Signature(signature))), None) - assert(invoice.toString == "lni1qvsyxjtl6luzd9t3pr62xr7eemp6awnejusgf6gw45q75vcfqqqqqqqyyz9ut9uduhtztjgpxm06394g5qkw7v79g4czw6zxsl3lnrsvljj0qzqrq83yqzscd9h8vmmfvdjjqamfw35zqmtpdeujqenfv4kxgucvqgqsq9qgv93kjmn39e3k783qc3nnudswp67znrydjtv7ta56c9cpc0nmjmv7rszs568gqdz3w77zqqfeycsgl2kawxcl0zckye09kpsmn54c3zgsztw845uxymh24g4zw9s45ef8p3yjwmfqw35x2grtd9hxw2qyv2sqd032ypge282v20ysgq6lpv5nmjwlrs88jeepxsc2upa970snfnfnxff5ztqzpcgzuqsq0vcpgpcyqqzpy02klq9svqqgavadc6y5tmmqzvsv484ku5nw43vumxuflvsrsgr345pnuh6zq6pz2cy8wra8vujs23y5yhd4gwslns3m7qm9023hc8cyq6knwqxzve9r5kpufq3szuhn9f437cj05az5kqnsl9wefhfnwzenf5z68qh5jj48rmku97u0gzdm2wlkuwrylpvqfttdtw972cwdteal6qfhqvqsyqlaqyusq") + assert(invoice.toString == "lni1qvsyxjtl6luzd9t3pr62xr7eemp6awnejusgf6gw45q75vcfqqqqqqqyyz9ut9uduhtztjgpxm06394g5qkw7v79g4czw6zxsl3lnrsvljj0qzqrq83yqzscd9h8vmmfvdjjqamfw35zqmtpdeujqenfv4kxgucvqgqsqyycq0zxw03kpc8tc2vv3kfdne0kntqhq8p70wtdncwq2zngaqp529mmcq5ecw92k3597h7kdndc64mg2xt709acf2gmxnnag5kq9a6wslznscqsyu5p4eckl7m69k0qpcppkpz3lq4chus9szjkgw9w7mgeknz7m7fpqqe02qmqdj08z62mz0jws0gxt45fyq8udel9jg5gd6xlgdrkdt5qywp0jsc93kcksk4x4yvk7s3dej984yh3gzrpvd5kuufwvdh3ugxyvulrvrswhs5cervjm8jldxkpwqwru7ukm8suq59x36qrg5thhssqzwfxyz864ht3k8mck93xtedsvxua9wygjyqjm3ad8p3xa6429gn3v9dx2fcvfynk6gr5dpjjq6mfdenjsprz5qrtu23q2x236nzneyzqxhct9y7unhcupeukwgf5xzhq0f0nuy6v6vej2dqjcqswzqhqyqrmxq2qwpqqqsfr64hcpvrqqz8t8twx39z77cqnyr9fadh9ym4vt8xehz0myquzquddqvl97ssxsgjkppmslfm8y5z5f9p9md2r58uuywlsxet65d7p7pq94u2jf9d7uz8xq064860j5578f26l3jv2thhh7q6knxnsk33djhyqwwhm4xktsrc69sjfegtsaxq4c0pdhw5kfr55z4q8vkutrgwskt7szdcrqypq8lgp8yqq") val Success(codedDecoded) = Bolt12Invoice.fromString(invoice.toString) assert(codedDecoded.chain == chain) - assert(codedDecoded.offerId == Some(offerId)) + assert(codedDecoded.offerId.contains(offerId)) assert(codedDecoded.amount == amount) assert(codedDecoded.description == Left(description)) assert(codedDecoded.features == features) - assert(codedDecoded.issuer == Some(issuer)) + assert(codedDecoded.issuer.contains(issuer)) assert(codedDecoded.nodeId.value.drop(1) == nodeKey.publicKey.value.drop(1)) - assert(codedDecoded.quantity == Some(quantity)) - assert(codedDecoded.payerKey == Some(payerKey)) - assert(codedDecoded.payerNote == Some(payerNote)) - assert(codedDecoded.payerInfo == Some(payerInfo)) + assert(codedDecoded.blindedPaths == Seq(path)) + assert(codedDecoded.quantity.contains(quantity)) + assert(codedDecoded.payerKey.contains(payerKey)) + assert(codedDecoded.payerNote.contains(payerNote)) + assert(codedDecoded.payerInfo.contains(payerInfo)) assert(codedDecoded.createdAt == createdAt) assert(codedDecoded.paymentHash == paymentHash) assert(codedDecoded.relativeExpiry == relativeExpiry.seconds) assert(codedDecoded.minFinalCltvExpiryDelta == cltv) - assert(codedDecoded.fallbacks == Some(fallbacks)) - assert(codedDecoded.replaceInvoice == Some(replaceInvoice)) + assert(codedDecoded.fallbacks.contains(fallbacks)) + assert(codedDecoded.replaceInvoice.contains(replaceInvoice)) assert(codedDecoded.records.unknown.toSet == Set(GenericTlv(UInt64(311), hex"010203"), GenericTlv(UInt64(313), hex""))) } From 518a5f34fe48d0496b5b05e14b54a5c98be4d792 Mon Sep 17 00:00:00 2001 From: Thomas HUET Date: Wed, 20 Jul 2022 17:25:41 +0200 Subject: [PATCH 119/121] Full keys and more tests --- .../fr/acinq/eclair/message/Postman.scala | 16 -- .../acinq/eclair/payment/Bolt12Invoice.scala | 35 +-- .../eclair/payment/send/OfferPayment.scala | 34 ++- .../eclair/wire/protocol/OfferCodecs.scala | 27 ++- .../eclair/wire/protocol/OfferTypes.scala | 36 ++- .../eclair/payment/Bolt12InvoiceSpec.scala | 205 ++++++++++++------ .../eclair/wire/protocol/OfferTypesSpec.scala | 18 +- 7 files changed, 209 insertions(+), 162 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/message/Postman.scala b/eclair-core/src/main/scala/fr/acinq/eclair/message/Postman.scala index c3a71f18fc..0a04506421 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/message/Postman.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/message/Postman.scala @@ -39,14 +39,6 @@ object Postman { replyPathId: Option[ByteVector32], replyTo: ActorRef[OnionMessageResponse], timeout: FiniteDuration) extends Command - case class SendMessageToBoth(nextNodeId1: PublicKey, - message1: OnionMessage, - replyPathId1: ByteVector32, - nextNodeId2: PublicKey, - message2: OnionMessage, - replyPathId2: ByteVector32, - replyTo: ActorRef[OnionMessageResponse], - timeout: FiniteDuration) extends Command private case class Unsubscribe(pathId: ByteVector32) extends Command private case class WrappedMessage(finalPayload: FinalPayload, pathId: Option[ByteVector]) extends Command sealed trait OnionMessageResponse @@ -96,14 +88,6 @@ object Postman { context.scheduleOnce(timeout, context.self, Unsubscribe(pathId)) switchboard ! Switchboard.RelayMessage(pathId, None, nextNodeId, message, MessageRelay.RelayAll, Some(relayMessageStatusAdapter)) Behaviors.same - case SendMessageToBoth(nextNodeId1, message1, pathId1, nextNodeId2, message2, pathId2, ref, timeout) => // two messages expecting one reply - messagePair += (pathId1 -> (pathId2, ref)) - messagePair += (pathId2 -> (pathId1, ref)) - context.scheduleOnce(timeout, context.self, Unsubscribe(pathId1)) - context.scheduleOnce(timeout, context.self, Unsubscribe(pathId2)) - switchboard ! Switchboard.RelayMessage(pathId1, None, nextNodeId1, message1, MessageRelay.RelayAll, Some(relayMessageStatusAdapter)) - switchboard ! Switchboard.RelayMessage(pathId2, None, nextNodeId2, message2, MessageRelay.RelayAll, Some(relayMessageStatusAdapter)) - Behaviors.same case Unsubscribe(pathId) => subscribed.get(pathId).foreach(ref => { subscribed -= pathId diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Bolt12Invoice.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Bolt12Invoice.scala index 0cc9648138..d62bde5b63 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Bolt12Invoice.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Bolt12Invoice.scala @@ -24,7 +24,7 @@ import fr.acinq.eclair.crypto.Sphinx.RouteBlinding import fr.acinq.eclair.wire.protocol.OfferCodecs.{invoiceCodec, invoiceTlvCodec} import fr.acinq.eclair.wire.protocol.OfferTypes._ import fr.acinq.eclair.wire.protocol.{OfferTypes, TlvStream} -import fr.acinq.eclair.{CltvExpiryDelta, Features, InvoiceFeature, MilliSatoshi, TimestampSecond, randomKey} +import fr.acinq.eclair.{CltvExpiryDelta, Features, InvoiceFeature, MilliSatoshi, TimestampSecond, randomKey, MilliSatoshiLong} import scodec.bits.ByteVector import java.util.concurrent.TimeUnit @@ -35,13 +35,14 @@ import scala.util.Try * Lightning Bolt 12 invoice * see https://github.com/lightning/bolts/blob/master/12-offer-encoding.md */ -case class Bolt12Invoice(records: TlvStream[InvoiceTlv], nodeId_opt: Option[PublicKey]) extends Invoice { +case class Bolt12Invoice(records: TlvStream[InvoiceTlv]) extends Invoice { import Bolt12Invoice._ require(records.get[Amount].nonEmpty, "bolt 12 invoices must provide an amount") - require(records.get[NodeIdXOnly].nonEmpty, "bolt 12 invoices must provide a node id") + require(records.get[NodeId].nonEmpty, "bolt 12 invoices must provide a node id") require(records.get[Paths].exists(_.paths.nonEmpty), "bolt 12 invoices must provide a blinded path") + require(records.get[PaymentPathsInfo].exists(_.paymentInfo.length == records.get[Paths].get.paths.length), "bolt 12 invoices must provide a blinded_payinfo for each path") require(records.get[PaymentHash].nonEmpty, "bolt 12 invoices must provide a payment hash") require(records.get[Description].nonEmpty, "bolt 12 invoices must provide a description") require(records.get[CreatedAt].nonEmpty, "bolt 12 invoices must provide a creation timestamp") @@ -51,7 +52,7 @@ case class Bolt12Invoice(records: TlvStream[InvoiceTlv], nodeId_opt: Option[Publ override val amount_opt: Option[MilliSatoshi] = Some(amount) - override val nodeId: Crypto.PublicKey = nodeId_opt.getOrElse(records.get[NodeIdXOnly].get.nodeId1) + override val nodeId: Crypto.PublicKey = records.get[NodeId].get.publicKey override val paymentHash: ByteVector32 = records.get[PaymentHash].get.hash @@ -104,7 +105,7 @@ case class Bolt12Invoice(records: TlvStream[InvoiceTlv], nodeId_opt: Option[Publ // It is assumed that the request is valid for this offer. def isValidFor(offer: Offer, request: InvoiceRequest): Boolean = { - OfferTypes.xOnlyPublicKey(nodeId) == offer.nodeIdXOnly.xOnly && + nodeId == offer.nodeId && checkSignature() && offerId.contains(request.offerId) && request.chain == chain && @@ -130,8 +131,6 @@ case class Bolt12Invoice(records: TlvStream[InvoiceTlv], nodeId_opt: Option[Publ def checkSignature(): Boolean = { verifySchnorr(signatureTag("signature"), rootHash(OfferTypes.removeSignature(records), invoiceTlvCodec), signature, OfferTypes.xOnlyPublicKey(nodeId)) } - - def withNodeId(nodeId: PublicKey): Bolt12Invoice = Bolt12Invoice(records, Some(nodeId)) } object Bolt12Invoice { @@ -157,26 +156,28 @@ object Bolt12Invoice { features: Features[InvoiceFeature], selfId: ByteVector): Bolt12Invoice = { require(request.amount.nonEmpty || offer.amount.nonEmpty) + val amount = request.amount.orElse(offer.amount.map(_ * request.quantity)).get val tlvs: Seq[InvoiceTlv] = Seq( Some(Chain(request.chain)), - Some(CreatedAt(TimestampSecond.now())), - Some(PaymentHash(Crypto.sha256(preimage))), Some(OfferId(offer.offerId)), - Some(NodeIdXOnly(xOnlyPublicKey(nodeKey.publicKey))), - Some(Paths(Seq(Sphinx.RouteBlinding.createDirect(randomKey(), nodeKey.publicKey, selfId)))), - Some(Amount(request.amount.orElse(offer.amount.map(_ * request.quantity)).get)), + Some(Amount(amount)), Some(Description(offer.description)), + if (!features.isEmpty) Some(FeaturesTlv(features.unscoped())) else None, + Some(Paths(Seq(Sphinx.RouteBlinding.createDirect(randomKey(), nodeKey.publicKey, selfId)))), + Some(PaymentPathsInfo(Seq(PaymentInfo(0 msat, 0, CltvExpiryDelta(0), 0 msat, amount, Features.empty)))), + offer.issuer.map(Issuer), + Some(NodeId(nodeKey.publicKey)), request.quantity_opt.map(Quantity), Some(PayerKey(request.payerKey)), - request.payerInfo.map(PayerInfo), request.payerNote.map(PayerNote), - request.replaceInvoice.map(ReplaceInvoice), - offer.issuer.map(Issuer), + Some(CreatedAt(TimestampSecond.now())), + Some(PaymentHash(Crypto.sha256(preimage))), Some(Cltv(minFinalCltvExpiryDelta)), - Some(FeaturesTlv(features.unscoped())), + request.payerInfo.map(PayerInfo), + request.replaceInvoice.map(ReplaceInvoice), ).flatten val signature = signSchnorr(signatureTag("signature"), rootHash(TlvStream(tlvs), invoiceTlvCodec), nodeKey) - Bolt12Invoice(TlvStream(tlvs :+ Signature(signature)), Some(nodeKey.publicKey)) + Bolt12Invoice(TlvStream(tlvs :+ Signature(signature))) } def signatureTag(fieldName: String): String = "lightning" + "invoice" + fieldName diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/OfferPayment.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/OfferPayment.scala index 8061534d32..61992b06bb 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/OfferPayment.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/OfferPayment.scala @@ -22,17 +22,16 @@ import akka.actor.typed.scaladsl.{ActorContext, Behaviors} import akka.actor.{ActorRef, typed} import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64} -import fr.acinq.eclair.message.OnionMessages.Recipient -import fr.acinq.eclair.message.Postman.{OnionMessageResponse, SendMessage, SendMessageToBoth} +import fr.acinq.eclair.message.Postman.{OnionMessageResponse, SendMessage} import fr.acinq.eclair.message.{OnionMessages, Postman} import fr.acinq.eclair.payment.send.MultiPartPaymentLifecycle.PreimageReceived import fr.acinq.eclair.payment.send.PaymentInitiator.SendPaymentToNode import fr.acinq.eclair.payment.{Bolt12Invoice, PaymentFailed, PaymentSent} +import fr.acinq.eclair.router.Router.RouteParams import fr.acinq.eclair.wire.protocol.OfferTypes.{InvoiceRequest, Offer} import fr.acinq.eclair.wire.protocol.OnionMessagePayloadTlv.ReplyPath import fr.acinq.eclair.wire.protocol.{OnionMessage, OnionMessagePayloadTlv} import fr.acinq.eclair.{Features, InvoiceFeature, MilliSatoshi, NodeParams, TimestampSecond, randomBytes32, randomKey} -import fr.acinq.eclair.router.Router.RouteParams import java.util.UUID import scala.util.Random @@ -78,7 +77,7 @@ object OfferPayment { val pathId = randomBytes32() // TODO: randomize intermediate nodes val intermediateNodes = Seq.empty - val replyPath = ReplyPath(OnionMessages.buildRoute(randomKey(), intermediateNodes.reverse, Recipient(nodeParams.nodeId, Some(pathId.bytes)))) + val replyPath = ReplyPath(OnionMessages.buildRoute(randomKey(), intermediateNodes.reverse, OnionMessages.Recipient(nodeParams.nodeId, Some(pathId.bytes)))) val (nextNodeId, message) = OnionMessages.buildMessage(randomKey(), randomKey(), intermediateNodes, destination, Seq(replyPath, OnionMessagePayloadTlv.InvoiceRequest(request))) (pathId, nextNodeId, message) } @@ -139,29 +138,22 @@ object OfferPayment { replyTo: typed.ActorRef[Result], remainingAttempts: Int, sendPaymentConfig: SendPaymentConfig): Behavior[Command] = { - offer.contact match { - case Left(blindedRoutes) => - val blindedRoute = blindedRoutes(rand.nextInt(blindedRoutes.length)) - val (pathId, nextNodeId, message) = buildRequest(nodeParams, request, OnionMessages.BlindedPath(blindedRoute)) - postman ! SendMessage(nextNodeId, message, Some(pathId), context.messageAdapter(WrappedMessageResponse), nodeParams.onionMessageConfig.timeout) - waitForInvoice(nodeParams, postman, paymentInitiator, offer, Map(pathId -> offer.nodeIdXOnly.nodeId1), request, payerKey, replyTo, remainingAttempts - 1, sendPaymentConfig) - case Right(nodeIdXOnly) => - // The node id from the offer is missing the first byte so we try both the odd and even versions, we'll - // pay the first one that answers (the other one should not exist but if it does, it is controlled by the - // same person). - val (pathId1, nextNodeId1, message1) = buildRequest(nodeParams, request, Recipient(nodeIdXOnly.nodeId1, None, None)) - val (pathId2, nextNodeId2, message2) = buildRequest(nodeParams, request, Recipient(nodeIdXOnly.nodeId2, None, None)) - postman ! SendMessageToBoth(nextNodeId1, message1, pathId1, nextNodeId2, message2, pathId2, context.messageAdapter(WrappedMessageResponse), nodeParams.onionMessageConfig.timeout) - waitForInvoice(nodeParams, postman, paymentInitiator, offer, Map(pathId1 -> nodeIdXOnly.nodeId1, pathId2 -> nodeIdXOnly.nodeId2),request, payerKey, replyTo, remainingAttempts - 1, sendPaymentConfig) - + val destination = offer.contact match { + case Left(blindedRoutes) => + val blindedRoute = blindedRoutes(rand.nextInt(blindedRoutes.length)) + OnionMessages.BlindedPath(blindedRoute) + case Right(nodeId) => + OnionMessages.Recipient(nodeId, None, None) } + val (pathId, nextNodeId, message) = buildRequest(nodeParams, request, destination) + postman ! SendMessage(nextNodeId, message, Some(pathId), context.messageAdapter(WrappedMessageResponse), nodeParams.onionMessageConfig.timeout) + waitForInvoice(nodeParams, postman, paymentInitiator, offer, request, payerKey, replyTo, remainingAttempts - 1, sendPaymentConfig) } def waitForInvoice(nodeParams: NodeParams, postman: typed.ActorRef[Postman.Command], paymentInitiator: ActorRef, offer: Offer, - nodeId: Map[ByteVector32, PublicKey], request: InvoiceRequest, payerKey: PrivateKey, replyTo: typed.ActorRef[Result], @@ -169,7 +161,7 @@ object OfferPayment { sendPaymentConfig: SendPaymentConfig): Behavior[Command] = { Behaviors.receivePartial { case (context, WrappedMessageResponse(Postman.Response(payload, pathId))) if payload.invoice.nonEmpty => - val invoice = payload.invoice.get.invoice.withNodeId(nodeId(pathId)) + val invoice = payload.invoice.get.invoice if (invoice.isValidFor(offer, request)) { val recipientAmount = invoice.amount paymentInitiator ! SendPaymentToNode(context.messageAdapter(paymentResultWrapper).toClassic, recipientAmount, invoice, maxAttempts = sendPaymentConfig.maxAttempts, externalId = sendPaymentConfig.externalId_opt, routeParams = sendPaymentConfig.routeParams) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/OfferCodecs.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/OfferCodecs.scala index df5a06f22d..6652c917ae 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/OfferCodecs.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/OfferCodecs.scala @@ -23,7 +23,7 @@ import fr.acinq.eclair.wire.protocol.CommonCodecs._ import fr.acinq.eclair.wire.protocol.OfferTypes._ import fr.acinq.eclair.wire.protocol.OnionRoutingCodecs.MissingRequiredTlv import fr.acinq.eclair.wire.protocol.TlvCodecs.{tmillisatoshi, tu32, tu64overflow} -import fr.acinq.eclair.{CltvExpiryDelta, Feature, Features, TimestampSecond, UInt64} +import fr.acinq.eclair.{CltvExpiryDelta, Feature, Features, MilliSatoshi, TimestampSecond, UInt64} import scodec.codecs._ import scodec.{Attempt, Codec} @@ -52,7 +52,7 @@ object OfferCodecs { private val quantityMax: Codec[QuantityMax] = variableSizeBytesLong(varintoverflow, tu64overflow).as[QuantityMax] - private val nodeId: Codec[NodeIdXOnly] = variableSizeBytesLong(varintoverflow, bytes32).as[NodeIdXOnly] + private val nodeId: Codec[NodeId] = variableSizeBytesLong(varintoverflow, publicKey).as[NodeId] private val sendInvoice: Codec[SendInvoice] = variableSizeBytesLong(varintoverflow, provide(SendInvoice())) @@ -80,7 +80,7 @@ object OfferCodecs { if (tlvs.get[Description].isEmpty) { Attempt.failure(MissingRequiredTlv(UInt64(10))) } - if (tlvs.get[NodeIdXOnly].isEmpty && tlvs.get[Paths].forall(_.paths.isEmpty)) { + if (tlvs.get[NodeId].isEmpty && tlvs.get[Paths].forall(_.paths.isEmpty)) { Attempt.failure(MissingRequiredTlv(UInt64(30))) } Attempt.successful(Offer(tlvs)) @@ -129,16 +129,15 @@ object OfferCodecs { }) private val paymentInfo: Codec[PaymentInfo] = (("fee_base_msat" | millisatoshi32) :: - ("fee_proportional_millionths" | tu32) :: - ("cltv_expiry_delta" | cltvExpiryDelta)).as[PaymentInfo] + ("fee_proportional_millionths" | uint32) :: + ("cltv_expiry_delta" | cltvExpiryDelta) :: + ("htlc_minimum_msat" | millisatoshi) :: + ("htlc_maximum_msat" | millisatoshi) :: + ("features" | variableSizeBytesLong(uint32, bytes).xmap[Features[Feature]](Features(_), _.toByteVector))).as[PaymentInfo] private val paymentPathsInfo: Codec[PaymentPathsInfo] = variableSizeBytesLong(varintoverflow, list(paymentInfo)).xmap[Seq[PaymentInfo]](_.toSeq, _.toList).as[PaymentPathsInfo] - private val paymentConstraints: Codec[PaymentConstraints] = (("max_cltv_expiry" | cltvExpiry) :: - ("htlc_minimum_msat" | millisatoshi) :: - ("allowed_features" | bytes.xmap[Features[Feature]](Features(_), _.toByteVector))).as[PaymentConstraints] - - private val paymentPathsConstraints: Codec[PaymentPathsConstraints] = variableSizeBytesLong(varintoverflow, list(paymentConstraints)).xmap[Seq[PaymentConstraints]](_.toSeq, _.toList).as[PaymentPathsConstraints] + private val paymentPathsCapacities: Codec[PaymentPathsCapacities] = variableSizeBytesLong(varintoverflow, list(millisatoshi)).xmap[Seq[MilliSatoshi]](_.toSeq, _.toList).as[PaymentPathsCapacities] private val createdAt: Codec[CreatedAt] = variableSizeBytesLong(varintoverflow, tu64overflow).as[TimestampSecond].as[CreatedAt] @@ -165,7 +164,7 @@ object OfferCodecs { // TODO: the spec for payment paths is not final, adjust codecs if changes are made to he spec. .typecase(UInt64(16), paths) .typecase(UInt64(18), paymentPathsInfo) - .typecase(UInt64(19), paymentPathsConstraints) + .typecase(UInt64(19), paymentPathsCapacities) .typecase(UInt64(20), issuer) .typecase(UInt64(30), nodeId) .typecase(UInt64(32), quantity) @@ -190,7 +189,7 @@ object OfferCodecs { Attempt.failure(MissingRequiredTlv(UInt64(10))) } else if (tlvs.get[Paths].isEmpty) { Attempt.failure(MissingRequiredTlv(UInt64(16))) - } else if (tlvs.get[NodeIdXOnly].isEmpty) { + } else if (tlvs.get[NodeId].isEmpty) { Attempt.failure(MissingRequiredTlv(UInt64(30))) } else if (tlvs.get[CreatedAt].isEmpty) { Attempt.failure(MissingRequiredTlv(UInt64(40))) @@ -199,10 +198,10 @@ object OfferCodecs { } else if (tlvs.get[Signature].isEmpty) { Attempt.failure(MissingRequiredTlv(UInt64(240))) } else { - Attempt.successful(Bolt12Invoice(tlvs, None)) + Attempt.successful(Bolt12Invoice(tlvs)) } }, { - case Bolt12Invoice(tlvs, _) => tlvs + case Bolt12Invoice(tlvs) => tlvs }) val invoiceErrorTlvCodec: Codec[TlvStream[InvoiceErrorTlv]] = TlvCodecs.tlvStream[InvoiceErrorTlv](discriminated[InvoiceErrorTlv].by(varint) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/OfferTypes.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/OfferTypes.scala index 1c762a2882..a9ab3671f4 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/OfferTypes.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/OfferTypes.scala @@ -60,12 +60,15 @@ object OfferTypes { case class Paths(paths: Seq[BlindedRoute]) extends OfferTlv with InvoiceTlv - case class PaymentInfo(feeBase: MilliSatoshi, feeProportionalMillionths: Long, cltvExpiryDelta: CltvExpiryDelta) + case class PaymentInfo(feeBase: MilliSatoshi, + feeProportionalMillionths: Long, + cltvExpiryDelta: CltvExpiryDelta, + minHtlc: MilliSatoshi, + maxHtlc: MilliSatoshi, + allowedFeatures: Features[Feature]) case class PaymentPathsInfo(paymentInfo: Seq[PaymentInfo]) extends InvoiceTlv - case class PaymentConstraints(maxCltvExpiry: CltvExpiry, minHtlc: MilliSatoshi, allowedFeatures: Features[Feature]) - - case class PaymentPathsConstraints(paymentConstraints: Seq[PaymentConstraints]) extends InvoiceTlv + case class PaymentPathsCapacities(capacities: Seq[MilliSatoshi]) extends InvoiceTlv case class Issuer(issuer: String) extends OfferTlv with InvoiceTlv @@ -73,14 +76,7 @@ object OfferTypes { case class QuantityMax(max: Long) extends OfferTlv - case class NodeIdXOnly(xOnly: ByteVector32) extends OfferTlv with InvoiceTlv { - val nodeId1: PublicKey = PublicKey(2 +: xOnly) - val nodeId2: PublicKey = PublicKey(3 +: xOnly) - } - - object NodeIdXOnly { - def apply(publicKey: PublicKey): NodeIdXOnly = NodeIdXOnly(xOnlyPublicKey(publicKey)) - } + case class NodeId(publicKey: PublicKey) extends OfferTlv with InvoiceTlv case class SendInvoice() extends OfferTlv with InvoiceTlv @@ -128,7 +124,7 @@ object OfferTypes { case class Offer(records: TlvStream[OfferTlv]) { - require(records.get[NodeIdXOnly].nonEmpty || records.get[Paths].exists(_.paths.nonEmpty), "bolt 12 offers must provide a node id or a blinded path") + require(records.get[NodeId].nonEmpty || records.get[Paths].exists(_.paths.nonEmpty), "bolt 12 offers must provide a node id or a blinded path") require(records.get[Description].nonEmpty, "bolt 12 offers must provide a description") val offerId: ByteVector32 = rootHash(removeSignature(records), offerTlvCodec) @@ -153,8 +149,8 @@ object OfferTypes { val quantityMin: Option[Long] = records.get[QuantityMin].map(_.min) val quantityMax: Option[Long] = records.get[QuantityMax].map(_.max) - val nodeIdXOnly: NodeIdXOnly = - records.get[NodeIdXOnly].getOrElse(NodeIdXOnly(records.get[Paths].get.paths.head.blindedNodes.last.blindedPublicKey)) + val nodeId: PublicKey = + records.get[NodeId].map(_.publicKey).getOrElse(records.get[Paths].get.paths.head.blindedNodes.last.blindedPublicKey) val sendInvoice: Boolean = records.get[SendInvoice].nonEmpty @@ -162,8 +158,8 @@ object OfferTypes { val signature: Option[ByteVector64] = records.get[Signature].map(_.signature) - val contact: Either[Seq[BlindedRoute], NodeIdXOnly] = - records.get[Paths].map(_.paths).map(Left(_)).getOrElse(Right(records.get[NodeIdXOnly].get)) + val contact: Either[Seq[BlindedRoute], PublicKey] = + records.get[Paths].map(_.paths).map(Left(_)).getOrElse(Right(records.get[NodeId].get.publicKey)) def sign(key: PrivateKey): Offer = { val sig = signSchnorr(Offer.signatureTag, rootHash(records, offerTlvCodec), key) @@ -172,7 +168,7 @@ object OfferTypes { def checkSignature(): Boolean = { signature match { - case Some(sig) => verifySchnorr(Offer.signatureTag, rootHash(removeSignature(records), offerTlvCodec), sig, nodeIdXOnly.xOnly) + case Some(sig) => verifySchnorr(Offer.signatureTag, rootHash(removeSignature(records), offerTlvCodec), sig, xOnlyPublicKey(nodeId)) case None => false } } @@ -201,7 +197,7 @@ object OfferTypes { if (chain != Block.LivenetGenesisBlock.hash) Some(Chains(Seq(chain))) else None, amount_opt.map(Amount), Some(Description(description)), - Some(NodeIdXOnly(xOnlyPublicKey(nodeId))), + Some(NodeId(nodeId)), if (!features.isEmpty) Some(FeaturesTlv(features.unscoped())) else None, ).flatten Offer(TlvStream(tlvs)) @@ -298,7 +294,7 @@ object OfferTypes { Some(Amount(amount)), if (offer.quantityMin.nonEmpty || offer.quantityMax.nonEmpty) Some(Quantity(quantity)) else None, Some(PayerKey(payerKey.publicKey)), - Some(FeaturesTlv(features.unscoped())) + if (!features.isEmpty) Some(FeaturesTlv(features.unscoped())) else None, ).flatten val signature = signSchnorr(InvoiceRequest.signatureTag, rootHash(TlvStream(tlvs), invoiceRequestTlvCodec), payerKey) InvoiceRequest(TlvStream(tlvs :+ Signature(signature))) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/Bolt12InvoiceSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/Bolt12InvoiceSpec.scala index b91a833205..a8a4829bd3 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/Bolt12InvoiceSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/Bolt12InvoiceSpec.scala @@ -17,7 +17,7 @@ package fr.acinq.eclair.payment import fr.acinq.bitcoin.Bech32 -import fr.acinq.bitcoin.scalacompat.Crypto.PrivateKey +import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, Crypto} import fr.acinq.eclair.FeatureSupport.{Mandatory, Optional} import fr.acinq.eclair.Features.{BasicMultiPartPayment, VariableLengthOnion} @@ -42,7 +42,7 @@ class Bolt12InvoiceSpec extends AnyFunSuite { def signInvoice(invoice: Bolt12Invoice, key: PrivateKey): Bolt12Invoice = { val tlvs = OfferTypes.removeSignature(invoice.records) - val signedInvoice = Bolt12Invoice(signInvoiceTlvs(tlvs, key), None) + val signedInvoice = Bolt12Invoice(signInvoiceTlvs(tlvs, key)) assert(signedInvoice.checkSignature()) signedInvoice } @@ -57,16 +57,16 @@ class Bolt12InvoiceSpec extends AnyFunSuite { assert(!invoice.checkRefundSignature()) assert(Bolt12Invoice.fromString(invoice.toString).get.toString == invoice.toString) // changing signature makes check fail - val withInvalidSignature = Bolt12Invoice(TlvStream(invoice.records.records.map { case Signature(_) => Signature(randomBytes64()) case x => x }, invoice.records.unknown), None) + val withInvalidSignature = Bolt12Invoice(TlvStream(invoice.records.records.map { case Signature(_) => Signature(randomBytes64()) case x => x }, invoice.records.unknown)) assert(!withInvalidSignature.checkSignature()) assert(!withInvalidSignature.isValidFor(offer, request)) assert(!withInvalidSignature.checkRefundSignature()) // changing fields makes the signature invalid - val withModifiedUnknownTlv = Bolt12Invoice(invoice.records.copy(unknown = Seq(GenericTlv(UInt64(7), hex"ade4"))), None) + val withModifiedUnknownTlv = Bolt12Invoice(invoice.records.copy(unknown = Seq(GenericTlv(UInt64(7), hex"ade4")))) assert(!withModifiedUnknownTlv.checkSignature()) assert(!withModifiedUnknownTlv.isValidFor(offer, request)) assert(!withModifiedUnknownTlv.checkRefundSignature()) - val withModifiedAmount = Bolt12Invoice(TlvStream(invoice.records.records.map { case Amount(amount) => Amount(amount + 100.msat) case x => x }, invoice.records.unknown), None) + val withModifiedAmount = Bolt12Invoice(TlvStream(invoice.records.records.map { case Amount(amount) => Amount(amount + 100.msat) case x => x }, invoice.records.unknown)) assert(!withModifiedAmount.checkSignature()) assert(!withModifiedAmount.isValidFor(offer, request)) assert(!withModifiedAmount.checkRefundSignature()) @@ -80,22 +80,22 @@ class Bolt12InvoiceSpec extends AnyFunSuite { assert(invoice.isValidFor(offer, request)) assert(!invoice.isValidFor(Offer(None, "test offer", randomKey().publicKey, Features.empty, chain), request)) // amount must match the offer - val withOtherAmount = signInvoice(Bolt12Invoice(TlvStream(invoice.records.records.map { case Amount(_) => Amount(9000 msat) case x => x }.toSeq), None), nodeKey) + val withOtherAmount = signInvoice(Bolt12Invoice(TlvStream(invoice.records.records.map { case Amount(_) => Amount(9000 msat) case x => x }.toSeq)), nodeKey) assert(!withOtherAmount.isValidFor(offer, request)) // description must match the offer, may have appended info - val withOtherDescription = signInvoice(Bolt12Invoice(TlvStream(invoice.records.records.map { case Description(_) => Description("other description") case x => x }.toSeq), None), nodeKey) + val withOtherDescription = signInvoice(Bolt12Invoice(TlvStream(invoice.records.records.map { case Description(_) => Description("other description") case x => x }.toSeq)), nodeKey) assert(!withOtherDescription.isValidFor(offer, request)) - val withExtendedDescription = signInvoice(Bolt12Invoice(TlvStream(invoice.records.records.map { case Description(_) => Description("test offer + more") case x => x }.toSeq), None), nodeKey) + val withExtendedDescription = signInvoice(Bolt12Invoice(TlvStream(invoice.records.records.map { case Description(_) => Description("test offer + more") case x => x }.toSeq)), nodeKey) assert(withExtendedDescription.isValidFor(offer, request)) // nodeId must match the offer val otherNodeKey = randomKey() - val withOtherNodeId = signInvoice(Bolt12Invoice(TlvStream(invoice.records.records.map { case NodeIdXOnly(_) => NodeIdXOnly(otherNodeKey.publicKey) case x => x }.toSeq), None), otherNodeKey) + val withOtherNodeId = signInvoice(Bolt12Invoice(TlvStream(invoice.records.records.map { case NodeId(_) => NodeId(otherNodeKey.publicKey) case x => x }.toSeq)), otherNodeKey) assert(!withOtherNodeId.isValidFor(offer, request)) // offerId must match the offer - val withOtherOfferId = signInvoice(Bolt12Invoice(TlvStream(invoice.records.records.map { case OfferId(_) => OfferId(randomBytes32()) case x => x }.toSeq), None), nodeKey) + val withOtherOfferId = signInvoice(Bolt12Invoice(TlvStream(invoice.records.records.map { case OfferId(_) => OfferId(randomBytes32()) case x => x }.toSeq)), nodeKey) assert(!withOtherOfferId.isValidFor(offer, request)) // issuer must match the offer - val withOtherIssuer = signInvoice(Bolt12Invoice(TlvStream(invoice.records.records ++ Seq(Issuer("spongebob"))), None), nodeKey) + val withOtherIssuer = signInvoice(Bolt12Invoice(TlvStream(invoice.records.records ++ Seq(Issuer("spongebob")))), nodeKey) assert(!withOtherIssuer.isValidFor(offer, request)) } @@ -106,17 +106,17 @@ class Bolt12InvoiceSpec extends AnyFunSuite { assert(request.quantity_opt.isEmpty) // when paying for a single item, the quantity field must not be present val invoice = Bolt12Invoice(offer, request, randomBytes32(), nodeKey, CltvExpiryDelta(20), Features(VariableLengthOnion -> Mandatory, BasicMultiPartPayment -> Optional), randomBytes32()) assert(invoice.isValidFor(offer, request)) - val withInvalidFeatures = signInvoice(Bolt12Invoice(TlvStream(invoice.records.records.map { case FeaturesTlv(_) => FeaturesTlv(Features(VariableLengthOnion -> Mandatory, BasicMultiPartPayment -> Mandatory)) case x => x }.toSeq), None), nodeKey) + val withInvalidFeatures = signInvoice(Bolt12Invoice(TlvStream(invoice.records.records.map { case FeaturesTlv(_) => FeaturesTlv(Features(VariableLengthOnion -> Mandatory, BasicMultiPartPayment -> Mandatory)) case x => x }.toSeq)), nodeKey) assert(!withInvalidFeatures.isValidFor(offer, request)) - val withAmountTooBig = signInvoice(Bolt12Invoice(TlvStream(invoice.records.records.map { case Amount(_) => Amount(20000 msat) case x => x }.toSeq), None), nodeKey) + val withAmountTooBig = signInvoice(Bolt12Invoice(TlvStream(invoice.records.records.map { case Amount(_) => Amount(20000 msat) case x => x }.toSeq)), nodeKey) assert(!withAmountTooBig.isValidFor(offer, request)) - val withQuantity = signInvoice(Bolt12Invoice(TlvStream(invoice.records.records.toSeq :+ Quantity(2)), None), nodeKey) + val withQuantity = signInvoice(Bolt12Invoice(TlvStream(invoice.records.records.toSeq :+ Quantity(2))), nodeKey) assert(!withQuantity.isValidFor(offer, request)) - val withOtherPayerKey = signInvoice(Bolt12Invoice(TlvStream(invoice.records.records.map { case PayerKey(_) => PayerKey(randomBytes32()) case x => x }.toSeq), None), nodeKey) + val withOtherPayerKey = signInvoice(Bolt12Invoice(TlvStream(invoice.records.records.map { case PayerKey(_) => PayerKey(randomBytes32()) case x => x }.toSeq)), nodeKey) assert(!withOtherPayerKey.isValidFor(offer, request)) - val withPayerNote = signInvoice(Bolt12Invoice(TlvStream(invoice.records.records.toSeq :+ PayerNote("I am Batman")), None), nodeKey) + val withPayerNote = signInvoice(Bolt12Invoice(TlvStream(invoice.records.records.toSeq :+ PayerNote("I am Batman"))), nodeKey) assert(!withPayerNote.isValidFor(offer, request)) - val withPayerInfo = signInvoice(Bolt12Invoice(TlvStream(invoice.records.records.toSeq :+ PayerInfo(hex"010203040506")), None), nodeKey) + val withPayerInfo = signInvoice(Bolt12Invoice(TlvStream(invoice.records.records.toSeq :+ PayerInfo(hex"010203040506"))), nodeKey) assert(!withPayerInfo.isValidFor(offer, request)) // Invoice request with more details about the payer. val requestWithPayerDetails = { @@ -134,10 +134,10 @@ class Bolt12InvoiceSpec extends AnyFunSuite { val withPayerDetails = Bolt12Invoice(offer, requestWithPayerDetails, randomBytes32(), nodeKey, CltvExpiryDelta(20), Features.empty, randomBytes32()) assert(withPayerDetails.isValidFor(offer, requestWithPayerDetails)) assert(!withPayerDetails.isValidFor(offer, request)) - val withOtherPayerInfo = signInvoice(Bolt12Invoice(TlvStream(withPayerDetails.records.records.map { case PayerInfo(_) => PayerInfo(hex"deadbeef") case x => x }.toSeq), None), nodeKey) + val withOtherPayerInfo = signInvoice(Bolt12Invoice(TlvStream(withPayerDetails.records.records.map { case PayerInfo(_) => PayerInfo(hex"deadbeef") case x => x }.toSeq)), nodeKey) assert(!withOtherPayerInfo.isValidFor(offer, requestWithPayerDetails)) assert(!withOtherPayerInfo.isValidFor(offer, request)) - val withOtherPayerNote = signInvoice(Bolt12Invoice(TlvStream(withPayerDetails.records.records.map { case PayerNote(_) => PayerNote("Or am I Bruce Wayne?") case x => x }.toSeq), None), nodeKey) + val withOtherPayerNote = signInvoice(Bolt12Invoice(TlvStream(withPayerDetails.records.records.map { case PayerNote(_) => PayerNote("Or am I Bruce Wayne?") case x => x }.toSeq)), nodeKey) assert(!withOtherPayerNote.isValidFor(offer, requestWithPayerDetails)) assert(!withOtherPayerNote.isValidFor(offer, request)) } @@ -149,10 +149,10 @@ class Bolt12InvoiceSpec extends AnyFunSuite { val invoice = Bolt12Invoice(offer, request, randomBytes32(), nodeKey, CltvExpiryDelta(20), Features.empty, randomBytes32()) assert(!invoice.isExpired()) assert(invoice.isValidFor(offer, request)) - val expiredInvoice1 = signInvoice(Bolt12Invoice(TlvStream(invoice.records.records.map { case CreatedAt(_) => CreatedAt(0 unixsec) case x => x }), None), nodeKey) + val expiredInvoice1 = signInvoice(Bolt12Invoice(TlvStream(invoice.records.records.map { case CreatedAt(_) => CreatedAt(0 unixsec) case x => x })), nodeKey) assert(expiredInvoice1.isExpired()) assert(!expiredInvoice1.isValidFor(offer, request)) // when an invoice is expired, we mark it as invalid as well - val expiredInvoice2 = signInvoice(Bolt12Invoice(TlvStream(invoice.records.records.map { case CreatedAt(_) => CreatedAt(TimestampSecond.now() - 2000) case x => x } ++ Seq(RelativeExpiry(1800))), None), nodeKey) + val expiredInvoice2 = signInvoice(Bolt12Invoice(TlvStream(invoice.records.records.map { case CreatedAt(_) => CreatedAt(TimestampSecond.now() - 2000) case x => x } ++ Seq(RelativeExpiry(1800)))), nodeKey) assert(expiredInvoice2.isExpired()) assert(!expiredInvoice2.isValidFor(offer, request)) // when an invoice is expired, we mark it as invalid as well } @@ -168,14 +168,15 @@ class Bolt12InvoiceSpec extends AnyFunSuite { CreatedAt(TimestampSecond.now()), PaymentHash(Crypto.sha256(randomBytes32())), OfferId(offerBtc.offerId), - NodeIdXOnly(nodeKey.publicKey), + NodeId(nodeKey.publicKey), Paths(Seq(Sphinx.RouteBlinding.createDirect(randomKey(), nodeKey.publicKey, randomBytes32()))), + PaymentPathsInfo(Seq(PaymentInfo(0 msat, 0, CltvExpiryDelta(0), 0 msat, amount, Features.empty))), Amount(amount), Description(offerBtc.description), PayerKey(payerKey.publicKey) ) val signature = signSchnorr(signatureTag("signature"), rootHash(TlvStream(tlvs), invoiceTlvCodec), nodeKey) - Bolt12Invoice(TlvStream(tlvs :+ Signature(signature)), None) + Bolt12Invoice(TlvStream(tlvs :+ Signature(signature))) } assert(invoiceImplicitBtc.isValidFor(offerBtc, requestBtc)) val invoiceExplicitBtc = { @@ -184,14 +185,15 @@ class Bolt12InvoiceSpec extends AnyFunSuite { CreatedAt(TimestampSecond.now()), PaymentHash(Crypto.sha256(randomBytes32())), OfferId(offerBtc.offerId), - NodeIdXOnly(nodeKey.publicKey), + NodeId(nodeKey.publicKey), Paths(Seq(Sphinx.RouteBlinding.createDirect(randomKey(), nodeKey.publicKey, randomBytes32()))), + PaymentPathsInfo(Seq(PaymentInfo(0 msat, 0, CltvExpiryDelta(0), 0 msat, amount, Features.empty))), Amount(amount), Description(offerBtc.description), PayerKey(payerKey.publicKey) ) val signature = signSchnorr(signatureTag("signature"), rootHash(TlvStream(tlvs), invoiceTlvCodec), nodeKey) - Bolt12Invoice(TlvStream(tlvs :+ Signature(signature)), None) + Bolt12Invoice(TlvStream(tlvs :+ Signature(signature))) } assert(invoiceExplicitBtc.isValidFor(offerBtc, requestBtc)) val invoiceOtherChain = { @@ -200,17 +202,18 @@ class Bolt12InvoiceSpec extends AnyFunSuite { CreatedAt(TimestampSecond.now()), PaymentHash(Crypto.sha256(randomBytes32())), OfferId(offerBtc.offerId), - NodeIdXOnly(nodeKey.publicKey), + NodeId(nodeKey.publicKey), Paths(Seq(Sphinx.RouteBlinding.createDirect(randomKey(), nodeKey.publicKey, randomBytes32()))), + PaymentPathsInfo(Seq(PaymentInfo(0 msat, 0, CltvExpiryDelta(0), 0 msat, amount, Features.empty))), Amount(amount), Description(offerBtc.description), PayerKey(payerKey.publicKey) ) val signature = signSchnorr(signatureTag("signature"), rootHash(TlvStream(tlvs), invoiceTlvCodec), nodeKey) - Bolt12Invoice(TlvStream(tlvs :+ Signature(signature)), None) + Bolt12Invoice(TlvStream(tlvs :+ Signature(signature))) } assert(!invoiceOtherChain.isValidFor(offerBtc, requestBtc)) - val offerOtherChains = Offer(TlvStream(Seq(Chains(Seq(chain1, chain2)), Amount(amount), Description("testnets offer"), NodeIdXOnly(nodeKey.publicKey)))) + val offerOtherChains = Offer(TlvStream(Seq(Chains(Seq(chain1, chain2)), Amount(amount), Description("testnets offer"), NodeId(nodeKey.publicKey)))) val requestOtherChains = InvoiceRequest(offerOtherChains, amount, 1, Features.empty, payerKey, chain1) val invoiceOtherChains = { val tlvs: Seq[InvoiceTlv] = Seq( @@ -218,14 +221,15 @@ class Bolt12InvoiceSpec extends AnyFunSuite { CreatedAt(TimestampSecond.now()), PaymentHash(Crypto.sha256(randomBytes32())), OfferId(offerOtherChains.offerId), - NodeIdXOnly(nodeKey.publicKey), + NodeId(nodeKey.publicKey), Paths(Seq(Sphinx.RouteBlinding.createDirect(randomKey(), nodeKey.publicKey, randomBytes32()))), + PaymentPathsInfo(Seq(PaymentInfo(0 msat, 0, CltvExpiryDelta(0), 0 msat, amount, Features.empty))), Amount(amount), Description(offerOtherChains.description), PayerKey(payerKey.publicKey) ) val signature = signSchnorr(signatureTag("signature"), rootHash(TlvStream(tlvs), invoiceTlvCodec), nodeKey) - Bolt12Invoice(TlvStream(tlvs :+ Signature(signature)), None) + Bolt12Invoice(TlvStream(tlvs :+ Signature(signature))) } assert(invoiceOtherChains.isValidFor(offerOtherChains, requestOtherChains)) val invoiceInvalidOtherChain = { @@ -234,53 +238,29 @@ class Bolt12InvoiceSpec extends AnyFunSuite { CreatedAt(TimestampSecond.now()), PaymentHash(Crypto.sha256(randomBytes32())), OfferId(offerOtherChains.offerId), - NodeIdXOnly(nodeKey.publicKey), + NodeId(nodeKey.publicKey), Paths(Seq(Sphinx.RouteBlinding.createDirect(randomKey(), nodeKey.publicKey, randomBytes32()))), + PaymentPathsInfo(Seq(PaymentInfo(0 msat, 0, CltvExpiryDelta(0), 0 msat, amount, Features.empty))), Amount(amount), Description(offerOtherChains.description), PayerKey(payerKey.publicKey) ) val signature = signSchnorr(signatureTag("signature"), rootHash(TlvStream(tlvs), invoiceTlvCodec), nodeKey) - Bolt12Invoice(TlvStream(tlvs :+ Signature(signature)), None) + Bolt12Invoice(TlvStream(tlvs :+ Signature(signature))) } assert(!invoiceInvalidOtherChain.isValidFor(offerOtherChains, requestOtherChains)) - val invoiceMissingChain = signInvoice(Bolt12Invoice(TlvStream(invoiceOtherChains.records.records.filter { case Chain(_) => false case _ => true }), None), nodeKey) + val invoiceMissingChain = signInvoice(Bolt12Invoice(TlvStream(invoiceOtherChains.records.records.filter { case Chain(_) => false case _ => true })), nodeKey) assert(!invoiceMissingChain.isValidFor(offerOtherChains, requestOtherChains)) } - test("decode simple invoice") { - val nodeKey = PrivateKey(hex"c6a75116a91dc5ff741b079c32c8ce7544656b98f047fb0c0fa011bfb2bb3c05") - val payerKey = PrivateKey(hex"7dd30ec116470c5f7f00af2c7e84968e28cdb43083b33ee832decbe73ec07f1a") - val Success(offer) = Offer.decode("lno1qgsyxjtl6luzd9t3pr62xr7eemp6awnejusgf6gw45q75vcfqqqqqqqgqvqcdgq2pd3xzumfvvsx7enxv4epug9kku8f4e9nuef5lv59yrkdc24t5mtrym62cg085w5wtqkp0rsuly") - assert(offer.amount.contains(100_000 msat)) - assert(offer.nodeIdXOnly.xOnly == xOnlyPublicKey(nodeKey.publicKey)) - assert(offer.chains == Seq(Block.TestnetGenesisBlock.hash)) - val request = InvoiceRequest(offer, 100_000 msat, 1, Features.empty, payerKey, Block.TestnetGenesisBlock.hash) - val Success(invoice) = Bolt12Invoice.fromString("lni1qvsyxjtl6luzd9t3pr62xr7eemp6awnejusgf6gw45q75vcfqqqqqqqyyp53zuupqkwxpmdq0tjg58ntat5ujpejlvyn92r0l5xzh4wru8e5zzqrqxr2qzstvfshx6tryphkven9wgxqqyycq2mtwr56uje7v560k2zjpmxu9246d43jda9vy8n68289stqh3cw0jqls9hxx3tpkddkun48gvlnefhdkkyhrkycr6dqm77z7qffrvyew8qqsy6tpz4gc080qqrcj59pmfz2n7as9j532qhygpatrw6a4k4u0ctc3qqez8vunpqq4jt2gmtwwgjhzvqs9f80yjvws6g98nsyh64h6wjf84ns48pfkzytgrqz6ld07tl7szug7khd3ug9kku8f4e9nuef5lv59yrkdc24t5mtrym62cg085w5wtqkp0rsulynzpdzlpprayc9krw2u80ujq90xh3evhl799gqcvscs5uwyfmv52ux89qzx94kurg4zpdh4kz4j7wvmx3jln9pqmrdryd9kcp98cv99pleghgmqe7ae6scq9cpqq98sgr0pwptagg7dutnx8mfwe7d84vp36f2afkycfqqum5lx3rz3p9heqx52w7zfytsa09mrq293apty6y330jgv322389cu6548twpxpeay") - assert(invoice.isValidFor(offer, request)) - } - - test("decode invoice with quantity") { - val nodeKey = PrivateKey(hex"c6a75116a91dc5ff741b079c32c8ce7544656b98f047fb0c0fa011bfb2bb3c05") - val payerKey = PrivateKey(hex"94c7a21a11efa16c5f73b093dc136d9525e2ff40ea7a958c43c1f6004bf6a676") - val Success(offer) = Offer.decode("lno1pqpzwyq2pf382mrtyphkven9wgtqzqgcqy9pug9kku8f4e9nuef5lv59yrkdc24t5mtrym62cg085w5wtqkp0rsuly") - assert(offer.amount.contains(10_000 msat)) - assert(offer.nodeIdXOnly.xOnly == xOnlyPublicKey(nodeKey.publicKey)) - assert(offer.chains == Seq(Block.LivenetGenesisBlock.hash)) - val request = InvoiceRequest(offer, 50_000 msat, 5, Features.empty, payerKey, Block.LivenetGenesisBlock.hash) - val Success(invoice) = Bolt12Invoice.fromString("lni1qvsxlc5vp2m0rvmjcxn2y34wv0m5lyc7sdj7zksgn35dvxgqqqqqqqqyypl905mjhuzxlljf9vhgrsde2f78eugjgcaekxr97nlkhtqqssn9gzqzcdgq5znzw4kxkgr0venx2usvqqgfsq4kku8f4e9nuef5lv59yrkdc24t5mtrym62cg085w5wtqkp0rsulyp8jup0gy33yx0q4qzvkq82v2u2ms9fsr44m40rtg8a73v5qyrg7vgpqw9pvycg3sf6ppq0nlxj43k36ugcz28nu53wxvg08uummjgr3vgcqqpjdec35dl86njwq5a6lp0vd6tn2nyq095lvy60vl0v57jcq0xu5c7j4qtssfn2uuc27662gq7yyyumtecwrcstddcwntjt8ejnf7eg2g8vms42hfkkxfh54ss70gagukpvz78pe7fqqyzjvg86k747wukskae3t0fwa56d0xl0sz6xhez6rnynym7ut0t9rxzzds5qgckkm3mj5gytaax4mqrj9n37m85fs847yyc7kngwryk8387ggrgd07j5fexershqyqq57pqgjumr63gmrdl2v36ldad2e4s7ae9k4e792alaks3umm0np25jntxxz7sq84udgz7ph3ru4r3xac89mjz6sxyqjq7g9f8p27et76dm25") - assert(invoice.amount == 50_000.msat) - assert(invoice.quantity.contains(5)) - assert(invoice.isValidFor(offer, request)) - } - test("decode invalid invoice") { val nodeKey = randomKey() val tlvs = Seq[InvoiceTlv]( Amount(765432 msat), Description("minimal invoice"), - NodeIdXOnly(nodeKey.publicKey), + NodeId(nodeKey.publicKey), Paths(Seq(Sphinx.RouteBlinding.createDirect(randomKey(), randomKey().publicKey, randomBytes32()))), + PaymentPathsInfo(Seq(PaymentInfo(0 msat, 0, CltvExpiryDelta(0), 0 msat, 765432 msat, Features.empty))), CreatedAt(TimestampSecond(123456789L)), PaymentHash(randomBytes32()), ) @@ -306,12 +286,13 @@ class Bolt12InvoiceSpec extends AnyFunSuite { val amount = 123456 msat val description = "invoice with many fields" val features = Features[Feature](Features.VariableLengthOnion -> FeatureSupport.Mandatory) - val issuer = "acinq.co" + val issuer = "alice" val nodeKey = PrivateKey(hex"998cf8ecab46f949bb960813b79d3317cabf4193452a211795cd8af1b9a25d90") val path = Sphinx.RouteBlinding.createDirect(PrivateKey(hex"f0442c17bdd2cefe4a4ede210f163b068bb3fea6113ffacea4f322de7aa9737b"), nodeKey.publicKey, hex"76030536ba732cdc4e7bb0a883750bab2e88cb3dddd042b1952c44b4849c86bb") + val payInfo = PaymentInfo(2345 msat, 765, CltvExpiryDelta(324), 1000 msat, amount, Features.empty) val quantity = 57 val payerKey = ByteVector32.fromValidHex("8faadd71b1f78b16265e5b061b9d2b88891012dc7ad38626eeaaa2a271615a65") - val payerNote = "I'm the king" + val payerNote = "I'm Bob" val payerInfo = hex"a9eb6e526eac59cd9b89fb20" val createdAt = TimestampSecond(1654654654L) val paymentHash = ByteVector32.fromValidHex("51951d4c53c904035f0b293dc9df1c0e7967213430ae07a5f3e134cd33325341") @@ -326,8 +307,9 @@ class Bolt12InvoiceSpec extends AnyFunSuite { Description(description), FeaturesTlv(features), Issuer(issuer), - NodeIdXOnly(nodeKey.publicKey), + NodeId(nodeKey.publicKey), Paths(Seq(path)), + PaymentPathsInfo(Seq(payInfo)), Quantity(quantity), PayerKey(payerKey), PayerNote(payerNote), @@ -340,8 +322,8 @@ class Bolt12InvoiceSpec extends AnyFunSuite { ReplaceInvoice(replaceInvoice) ), Seq(GenericTlv(UInt64(311), hex"010203"), GenericTlv(UInt64(313), hex""))) val signature = signSchnorr(Bolt12Invoice.signatureTag("signature"), rootHash(tlvs, invoiceTlvCodec), nodeKey) - val invoice = Bolt12Invoice(tlvs.copy(records = tlvs.records ++ Seq(Signature(signature))), None) - assert(invoice.toString == "lni1qvsyxjtl6luzd9t3pr62xr7eemp6awnejusgf6gw45q75vcfqqqqqqqyyz9ut9uduhtztjgpxm06394g5qkw7v79g4czw6zxsl3lnrsvljj0qzqrq83yqzscd9h8vmmfvdjjqamfw35zqmtpdeujqenfv4kxgucvqgqsqyycq0zxw03kpc8tc2vv3kfdne0kntqhq8p70wtdncwq2zngaqp529mmcq5ecw92k3597h7kdndc64mg2xt709acf2gmxnnag5kq9a6wslznscqsyu5p4eckl7m69k0qpcppkpz3lq4chus9szjkgw9w7mgeknz7m7fpqqe02qmqdj08z62mz0jws0gxt45fyq8udel9jg5gd6xlgdrkdt5qywp0jsc93kcksk4x4yvk7s3dej984yh3gzrpvd5kuufwvdh3ugxyvulrvrswhs5cervjm8jldxkpwqwru7ukm8suq59x36qrg5thhssqzwfxyz864ht3k8mck93xtedsvxua9wygjyqjm3ad8p3xa6429gn3v9dx2fcvfynk6gr5dpjjq6mfdenjsprz5qrtu23q2x236nzneyzqxhct9y7unhcupeukwgf5xzhq0f0nuy6v6vej2dqjcqswzqhqyqrmxq2qwpqqqsfr64hcpvrqqz8t8twx39z77cqnyr9fadh9ym4vt8xehz0myquzquddqvl97ssxsgjkppmslfm8y5z5f9p9md2r58uuywlsxet65d7p7pq94u2jf9d7uz8xq064860j5578f26l3jv2thhh7q6knxnsk33djhyqwwhm4xktsrc69sjfegtsaxq4c0pdhw5kfr55z4q8vkutrgwskt7szdcrqypq8lgp8yqq") + val invoice = Bolt12Invoice(tlvs.copy(records = tlvs.records ++ Seq(Signature(signature)))) + assert(invoice.toString == "lni1qvsyxjtl6luzd9t3pr62xr7eemp6awnejusgf6gw45q75vcfqqqqqqqyyz9ut9uduhtztjgpxm06394g5qkw7v79g4czw6zxsl3lnrsvljj0qzqrq83yqzscd9h8vmmfvdjjqamfw35zqmtpdeujqenfv4kxgucvqgqsqyycq0zxw03kpc8tc2vv3kfdne0kntqhq8p70wtdncwq2zngaqp529mmcq5ecw92k3597h7kdndc64mg2xt709acf2gmxnnag5kq9a6wslznscqsyu5p4eckl7m69k0qpcppkpz3lq4chus9szjkgw9w7mgeknz7m7fpqqe02qmqdj08z62mz0jws0gxt45fyq8udel9jg5gd6xlgdrkdt5qywp0jsc93kcksk4x4yvk7s3dej984yh3y8sqqqyjjqqqqt7sz3qqqqqqqqqqq05qqqqqqqqqrcjqqqqqqqq5q4skc6trv50zzq7yvulrvrswhs5cervjm8jldxkpwqwru7ukm8suq59x36qrg5thhssqzwfxyz864ht3k8mck93xtedsvxua9wygjyqjm3ad8p3xa6429gn3v9dx2fc8fynk6gzzda3zsprz5qrtu23q2x236nzneyzqxhct9y7unhcupeukwgf5xzhq0f0nuy6v6vej2dqjcqswzqhqyqrmxq2qwpqqqsfr64hcpvrqqz8t8twx39z77cqnyr9fadh9ym4vt8xehz0myquzquddqvl97ssxsgjkppmslfm8y5z5f9p9md2r58uuywlsxet65d7p7pqgrnx80xlmayz6d2kfupl65rk4mh07a6qllgs6cn0qgdqtpzyesh52h62dpegw6a6v2d38lr3jx07yasxr943wq6kuwa9gkuwq6279tl7szdcrqypq8lgp8yqq") val Success(codedDecoded) = Bolt12Invoice.fromString(invoice.toString) assert(codedDecoded.chain == chain) assert(codedDecoded.offerId.contains(offerId)) @@ -364,4 +346,97 @@ class Bolt12InvoiceSpec extends AnyFunSuite { assert(codedDecoded.records.unknown.toSet == Set(GenericTlv(UInt64(311), hex"010203"), GenericTlv(UInt64(313), hex""))) } + test("minimal tip") { + val nodeKey = PrivateKey(hex"48c6e5fcf499f50436f54c3b3edecdb0cb5961ca29d74bea5ab764828f08bf47") + assert(nodeKey.publicKey == PublicKey(hex"024ff5317f051c7f6eac0266c5cceaeb6c5775a940fab9854e47bfebf6bc7a0407")) + val payerKey = PrivateKey(hex"d817e8896c67d0bcabfdb93da7eb7fc698c829a181f994dd0ad866a8eda745e8") + assert(payerKey.publicKey == PublicKey(hex"031ef4439f638914de79220483dda32dfb7a431e799a5ce5a7643fbd70b2118e4e")) + val preimage = ByteVector32(hex"317d1fd8fec5f3ea23044983c2ba2a8043395b2a0790a815c9b12719aa5f1516") + val offer = Offer(None, "minimal tip", nodeKey.publicKey, Features.empty, Block.LivenetGenesisBlock.hash) + val encodedOffer = "lno1pg9k66twd9kkzmpqw35hq83pqf8l2vtlq5w87m4vqfnvtn82adk9wadfgratnp2wg7l7ha4u0gzqw" + assert(offer.toString == encodedOffer) + assert(Offer.decode(encodedOffer).get == offer) + val request = InvoiceRequest(offer, 12000000 msat, 1, Features.empty, payerKey, Block.LivenetGenesisBlock.hash) + val encodedRequest = "lnr1qvsxlc5vp2m0rvmjcxn2y34wv0m5lyc7sdj7zksgn35dvxgqqqqqqqqyyrfgrkke8dp3jww26jz8zgvhxhdhzgj062ejxecv9uqsdhh2x9lnjzqrkudsqf3qrm6y88mr3y2du7fzqjpamgedldayx8nenfwwtfmy877hpvs33e80qszhudm9rdk99qpnzktv6emdwq3gda2l77c6av7nn542sl3uhzq5yau26508s7n0mf3ztpnwr6f8vlxhjrlhc34w6sehs9jwydpxhxnws" + assert(request.toString == encodedRequest) + assert(InvoiceRequest.decode(encodedRequest).get == request) + assert(request.isValidFor(offer)) + val invoice = Bolt12Invoice(offer, request, preimage, nodeKey, CltvExpiryDelta(22), Features.empty, hex"") + assert(Bolt12Invoice.fromString(invoice.toString).get.records == invoice.records) + assert(invoice.isValidFor(offer, request)) + // Invoice generation is not reproducible as the timestamp and blinding point will change but all other fields should be the same. + val encodedInvoice = "lni1qvsxlc5vp2m0rvmjcxn2y34wv0m5lyc7sdj7zksgn35dvxgqqqqqqqqyyrfgrkke8dp3jww26jz8zgvhxhdhzgj062ejxecv9uqsdhh2x9lnjzqrkudsqzstd45ku6tdv9kzqarfwqg8sqj075ch7pgu0ah2cqnxchxw46mv2a66js86hxz5u3ala0mtc7syqupyrp8cxgf355un0gqnfhgqtnde9xqaazd4fcsrd5khlm4rns5v4aqpqd7aj6ja7pmk34s4llfjeju5y2lkuegn2dvjvnktkqvkxz05qvdjgqqjhfax8gmt6fgfunnhj99m3xk60auwvys7qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqpdcmqqqqqqqqrcssynl4x9ls28rld6kqyek9en4wkmzhwk55p74es48y00lt76785pq8ycspaazrna3cj9x70y3qfq7a5vklk7jrreue5h895ajrl0tskggcun3gq33ds9tg9gsgtlj8vuulq6aca20u59mx5xzdg84pksgxgnahfamrsnnup8ck4lpwqgqpduzqrvfdar6jwu6c66w93stjn3qute7tgser2geyyadl0p5u0t4p08523c2tk9jlj9l2j5q47qdpta0rqtndwwdsqdfajpskxkrfrcqk4jc" + val decodedInvoice = Bolt12Invoice.fromString(encodedInvoice).get + assert(decodedInvoice.amount == invoice.amount) + assert(decodedInvoice.nodeId == invoice.nodeId) + assert(decodedInvoice.paymentHash == invoice.paymentHash) + assert(decodedInvoice.description == invoice.description) + assert(decodedInvoice.payerKey == invoice.payerKey) + assert(decodedInvoice.chain == invoice.chain) + } + + test("minimal offer") { + val nodeKey = PrivateKey(hex"3b7a19e8320bb86431cf92cd7c69cc1dc0181c37d5a09875e4603c4e37d3705d") + assert(nodeKey.publicKey == PublicKey(hex"03c48ac97e09f3cbbaeb35b02aaa6d072b57726841a34d25952157caca60a1caf5")) + val payerKey = PrivateKey(hex"0e00a9ef505292f90a0e8a7aa99d31750e885c42a3ef8866dd2bf97919aa3891") + assert(payerKey.publicKey == PublicKey(hex"033e94f2afd568d128f02ece844ad4a0a1ddf2a4e3a08beb2dba11b3f1134b0517")) + val preimage = ByteVector32(hex"09ad5e952ec39d45461ebdeceac206fb45574ae9054b5a454dd02c65f5ba1b7c") + val offer = Offer(Some(456000000 msat), "minimal offer", nodeKey.publicKey, Features.empty, Block.LivenetGenesisBlock.hash) + val encodedOffer = "lno1pqzpktszqq9q6mtfde5k6ctvyphkven9wg0zzq7y3tyhuz0newawkdds924x6pet2aexssdrf5je2g2het9xpgw275" + assert(offer.toString == encodedOffer) + assert(Offer.decode(encodedOffer).get == offer) + val request = InvoiceRequest(offer, 456001234 msat, 1, Features.empty, payerKey, Block.LivenetGenesisBlock.hash) + val encodedRequest = "lnr1qvsxlc5vp2m0rvmjcxn2y34wv0m5lyc7sdj7zksgn35dvxgqqqqqqqqyypmpsc7ww3cxguwl27ela95ykset7t8tlvyfy7a200eujcnhczws6zqyrvhqd53xyqlffu40645dz28s9m8ggjk55zsamu4yuwsgh6edhggm8ugnfvz30uzqjq9tsnv60r570yqfypx3jghrff92qlcjwff0azwatsuehd0vkxxvz2wx07qlurz42ca0r96x6a4xh5h9gpz39w4em3687k6n3w9349g" + assert(request.toString == encodedRequest) + assert(InvoiceRequest.decode(encodedRequest).get == request) + assert(request.isValidFor(offer)) + val invoice = Bolt12Invoice(offer, request, preimage, nodeKey, CltvExpiryDelta(22), Features.empty, hex"747e01a7152169b058a1fbc0024c254077db7e399308483e0c30e2352ba1d6cc") + assert(Bolt12Invoice.fromString(invoice.toString).get.records == invoice.records) + assert(invoice.isValidFor(offer, request)) + // Invoice generation is not reproducible as the timestamp and blinding point will change but all other fields should be the same. + val encodedInvoice = "lni1qvsxlc5vp2m0rvmjcxn2y34wv0m5lyc7sdj7zksgn35dvxgqqqqqqqqyypmpsc7ww3cxguwl27ela95ykset7t8tlvyfy7a200eujcnhczws6zqyrvhqd5s2p4kkjmnfd4skcgr0venx2ussnqpufzkf0cyl8ja6av6mq242d5rjk4mjdpq6xnf9j5s40jk2vzsu4agze6zapx6yyjyjxsahmqvurucz66plh2wsvum6el7ff0ugmghn4f7szqlfaudyqmqfvwk3q092y0n7n5zmajxp59t8dl6wpal5wwykj8fkrcqr9xe2umxlml4myefpzj9q207yn4zp23nfxkhmnu8mspu5lscnvgysc3rjnh8yzalyaw5nn6tqnn3emnu6wys7qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqpktsx6gqqqqqqrcss83y2e9lqnu7tht4ntvp24fksw26hwf5yrg6dyk2jz472efs2rjh4ycsra98j4l2k35fg7qhvapz26js2rh0j5n36pzlt9kaprvl3zd9s29egq33dsxf29gszev88kpfrveu8g5xr8khk6tev8jmpxg3pxfhpcx6f4jtlm4ltwgpwqgqpduzqpkmntgyk3ms0p5fe0ps22ed9v7zvzgwtvge2kl6ey7qhvz0s7u694fl75ushaga2syfvw46kuz7tjstwq4jgz52n599ph5nt0854hrq" + val decodedInvoice = Bolt12Invoice.fromString(encodedInvoice).get + assert(decodedInvoice.amount == invoice.amount) + assert(decodedInvoice.nodeId == invoice.nodeId) + assert(decodedInvoice.paymentHash == invoice.paymentHash) + assert(decodedInvoice.description == invoice.description) + assert(decodedInvoice.payerKey == invoice.payerKey) + assert(decodedInvoice.chain == invoice.chain) + } + + test("offer with quantity") { + val nodeKey = PrivateKey(hex"334a488858f260a2bb262493f6edcd35470f110bba62c7a5f90c78a047b364df") + assert(nodeKey.publicKey == PublicKey(hex"0327afd599da3226f4608b96ab042fe558bf558211d3c5e67ecc8be9963220434f")) + val payerKey = PrivateKey(hex"4b4129a801ea631e25903cd59dd7f7a6820c19d73aa0b095496e21027934becf") + assert(payerKey.publicKey == PublicKey(hex"027c6d03fa8f366e2ef8017cdfaf5d3cf1a3b0123db1318263b662c0aa9ec9c959")) + val preimage = ByteVector32(hex"99221825b86576e94391b179902be8b22c7cfa7c3d14aec6ae86657dfd9bd2a8") + val offer = Offer(TlvStream[OfferTlv]( + Chains(Seq(Block.TestnetGenesisBlock.hash)), + Amount(100000 msat), + Description("offer with quantity"), + Issuer("alice@bigshop.com"), + QuantityMin(50), + QuantityMax(1000), + NodeId(nodeKey.publicKey))) + val encodedOffer = "lno1qgsyxjtl6luzd9t3pr62xr7eemp6awnejusgf6gw45q75vcfqqqqqqqgqvqcdgq2zdhkven9wgs8w6t5dqs8zatpde6xjarezsgkzmrfvdj5qcnfvaeksmms9e3k7mgkqyepsqsraq0zzqe84l2enk3jym6xpzuk4vzzle2cha2cyywnchn8anytaxtrygzrfu" + assert(offer.toString == encodedOffer) + assert(Offer.decode(encodedOffer).get == offer) + val request = InvoiceRequest(offer, 7200000 msat, 72, Features.empty, payerKey, Block.TestnetGenesisBlock.hash) + val encodedRequest = "lnr1qvsyxjtl6luzd9t3pr62xr7eemp6awnejusgf6gw45q75vcfqqqqqqqyyqcnw8ucesh0ttrka67a62qyf04tsprv4ul6uyrpctdm596q7av2zzqrdhwsqgqpfqnzqlrdq0ag7dnw9muqzlxl4awneudrkqfrmvf3sf3mvckq420vnj2e7pq998np5gs3khqdqpgztenk5k5wqhzlxjg0ed4q9439yh8dayzz7q24kay7qsrhxg8tf303g223fknj8d79d3dvj78nlkg9s8c5hyqgz5" + assert(request.toString == encodedRequest) + assert(InvoiceRequest.decode(encodedRequest).get == request) + assert(request.isValidFor(offer)) + val invoice = Bolt12Invoice(offer, request, preimage, nodeKey, CltvExpiryDelta(34), Features.empty, hex"9134d86e269a13203bd85bb3fd05bf396b72fcb9fd5206e3a392f6a0ab94011d") + assert(Bolt12Invoice.fromString(invoice.toString).get.records == invoice.records) + assert(invoice.isValidFor(offer, request)) + // Invoice generation is not reproducible as the timestamp and blinding point will change but all other fields should be the same. + val encodedInvoice = "lni1qvsyxjtl6luzd9t3pr62xr7eemp6awnejusgf6gw45q75vcfqqqqqqqyyqcnw8ucesh0ttrka67a62qyf04tsprv4ul6uyrpctdm596q7av2zzqrdhwsqzsndanxvetjypmkjargypch2ctww35hg7gsnqpj0t74n8dryfh5vz9ed2cy9lj43064sgga830x0mxgh6vkxgsyxncrjczxkxg06n3zvk280qntncsgx4kfducc57dy3cj0qcprf9sgnqfqzq3pntdcen5g5nuy84ny8e9geerst688u82xmyenwhuy5dju9er3rsqryttw2zsv5xn6sht2lvakdved7thlw4ggk9g3ga9qpc6rlh2xhwhjkt3rnlcz03szw6ww2khdx3rv4m0x2ys7qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqmwaqqqqqqqqzsgkzmrfvdj5qcnfvaeksmms9e3k7mg7yypj0t74n8dryfh5vz9ed2cy9lj43064sgga830x0mxgh6vkxgsyxneqq9yzvgrud5pl4rekdch0sqtum7h460835wcpy0d3xxpx8dnzcz4fajwfty5qgckcypdj5gr7q6pnms32xvy3rx08aw5zjpkua90zymcv947urlhz72wg5g7xkqhqyqpz7pqrs69fh5xv6gvfxahya334fc6ycjly4lzqzap5w543qrpx42pjn5ez8mtx73k0vlvhjjskspmjyukdqugwy54zf9qxxss3hh7q2uxlcg" + val decodedInvoice = Bolt12Invoice.fromString(encodedInvoice).get + assert(decodedInvoice.amount == invoice.amount) + assert(decodedInvoice.nodeId == invoice.nodeId) + assert(decodedInvoice.paymentHash == invoice.paymentHash) + assert(decodedInvoice.description == invoice.description) + assert(decodedInvoice.payerKey == invoice.payerKey) + assert(decodedInvoice.chain == invoice.chain) + } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/OfferTypesSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/OfferTypesSpec.scala index 819b516bae..7f8c2d9074 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/OfferTypesSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/OfferTypesSpec.scala @@ -16,7 +16,7 @@ package fr.acinq.eclair.wire.protocol -import fr.acinq.bitcoin.scalacompat.Crypto.PrivateKey +import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32} import fr.acinq.eclair.FeatureSupport.{Mandatory, Optional} import fr.acinq.eclair.Features.{BasicMultiPartPayment, VariableLengthOnion} @@ -29,7 +29,7 @@ import scodec.bits.{ByteVector, HexStringSyntax} import scala.util.Success class OfferTypesSpec extends AnyFunSuite { - val nodeId = ByteVector32(hex"4b9a1fa8e006f1e3937f65f66c408e6da8e1ca728ea43222a7381df1cc449605") + val nodeId = PublicKey(hex"024b9a1fa8e006f1e3937f65f66c408e6da8e1ca728ea43222a7381df1cc449605") test("sign and check offer") { val key = randomKey() @@ -53,7 +53,7 @@ class OfferTypesSpec extends AnyFunSuite { assert(offer.amount.isEmpty) assert(offer.signature.isEmpty) assert(offer.description == "Offer by rusty's node") - assert(offer.nodeIdXOnly.xOnly == nodeId) + assert(offer.nodeId == nodeId) } test("basic signed offer") { @@ -62,7 +62,7 @@ class OfferTypesSpec extends AnyFunSuite { assert(signedOffer.checkSignature()) assert(signedOffer.amount.isEmpty) assert(signedOffer.description == "Offer by rusty's node") - assert(signedOffer.nodeIdXOnly.xOnly == nodeId) + assert(signedOffer.nodeId == nodeId) } test("offer with amount and quantity") { @@ -71,7 +71,7 @@ class OfferTypesSpec extends AnyFunSuite { assert(offer.amount.contains(50 msat)) assert(offer.signature.isEmpty) assert(offer.description == "50msat multi-quantity offer") - assert(offer.nodeIdXOnly.xOnly == nodeId) + assert(offer.nodeId == nodeId) assert(offer.issuer.contains("rustcorp.com.au")) assert(offer.quantityMin.contains(1)) } @@ -82,7 +82,7 @@ class OfferTypesSpec extends AnyFunSuite { assert(signedOffer.checkSignature()) assert(signedOffer.amount.contains(50 msat)) assert(signedOffer.description == "50msat multi-quantity offer") - assert(signedOffer.nodeIdXOnly.xOnly == nodeId) + assert(signedOffer.nodeId == nodeId) assert(signedOffer.issuer.contains("rustcorp.com.au")) assert(signedOffer.quantityMin.contains(1)) } @@ -142,7 +142,7 @@ class OfferTypesSpec extends AnyFunSuite { test("check that invoice request matches offer (chain compatibility)") { { - val offer = Offer(TlvStream(Seq(Amount(100 msat), Description("offer without chains"), NodeIdXOnly(randomKey().publicKey)))) + val offer = Offer(TlvStream(Seq(Amount(100 msat), Description("offer without chains"), NodeId(randomKey().publicKey)))) val payerKey = randomKey() val request = { val tlvs: Seq[InvoiceRequestTlv] = Seq( @@ -162,7 +162,7 @@ class OfferTypesSpec extends AnyFunSuite { } { val (chain1, chain2) = (randomBytes32(), randomBytes32()) - val offer = Offer(TlvStream(Seq(Chains(Seq(chain1, chain2)), Amount(100 msat), Description("offer with chains"), NodeIdXOnly(randomKey().publicKey)))) + val offer = Offer(TlvStream(Seq(Chains(Seq(chain1, chain2)), Amount(100 msat), Description("offer with chains"), NodeId(randomKey().publicKey)))) val payerKey = randomKey() val request1 = InvoiceRequest(offer, 100 msat, 1, Features.empty, payerKey, chain1) assert(request1.isValidFor(offer)) @@ -179,7 +179,7 @@ class OfferTypesSpec extends AnyFunSuite { val offer = Offer(TlvStream( Amount(500 msat), Description("offer for multiple items"), - NodeIdXOnly(randomKey().publicKey), + NodeId(randomKey().publicKey), QuantityMin(3), QuantityMax(10), )) From 906ba586f471ae8d8b54a4c5fcf08268016bf71a Mon Sep 17 00:00:00 2001 From: Thomas HUET Date: Thu, 21 Jul 2022 13:46:26 +0200 Subject: [PATCH 120/121] Fix offer tests --- .../eclair/wire/protocol/OfferTypesSpec.scala | 51 +++++++++++++------ 1 file changed, 36 insertions(+), 15 deletions(-) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/OfferTypesSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/OfferTypesSpec.scala index 7f8c2d9074..11916c64c0 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/OfferTypesSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/OfferTypesSpec.scala @@ -29,7 +29,8 @@ import scodec.bits.{ByteVector, HexStringSyntax} import scala.util.Success class OfferTypesSpec extends AnyFunSuite { - val nodeId = PublicKey(hex"024b9a1fa8e006f1e3937f65f66c408e6da8e1ca728ea43222a7381df1cc449605") + val nodeKey = PrivateKey(hex"85d08273493e489b9330c85a3e54123874c8cd67c1bf531f4b926c9c555f8e1d") + val nodeId = nodeKey.publicKey test("sign and check offer") { val key = randomKey() @@ -48,42 +49,62 @@ class OfferTypesSpec extends AnyFunSuite { } test("basic offer") { - val encoded = "lno1pg257enxv4ezqcneype82um50ynhxgrwdajx283qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczs" - val offer = Offer.decode(encoded).get + val offer = Offer(TlvStream[OfferTlv]( + Description("basic offer"), + NodeId(nodeId))) + val encoded = "lno1pg9kyctnd93jqmmxvejhy83pqvxl9c6mjgkeaxa6a0vtxqteql688v0ywa8qqwx4j05cyskn8ncrj" + assert(Offer.decode(encoded).get == offer) assert(offer.amount.isEmpty) assert(offer.signature.isEmpty) - assert(offer.description == "Offer by rusty's node") + assert(offer.description == "basic offer") assert(offer.nodeId == nodeId) } test("basic signed offer") { - val encodedSigned = "lno1pg257enxv4ezqcneype82um50ynhxgrwdajx283qfwdpl28qqmc78ymlvhmxcsywdk5wrjnj36jryg488qwlrnzyjczlqs85ck65ycmkdk92smwt9zuewdzfe7v4aavvaz5kgv9mkk63v3s0ge0f099kssh3yc95qztx504hu92hnx8ctzhtt08pgk0texz0509tk" - val Success(signedOffer) = Offer.decode(encodedSigned) + val signedOffer = Offer(TlvStream[OfferTlv]( + Description("basic signed offer"), + NodeId(nodeId))).sign(nodeKey) + val encoded = "lno1pgfxyctnd93jqumfvahx2epqdanxvetjrcssxr0juddeytv7nwawhk9nq9us0arnk8j8wnsq8r2e86vzgtfneupe7pqr8k27dajwvrehuprj08ggamld6pgnj9ydp3whx6kz5hjaavh8rhfjzkhyxsjwakelepqxmx26aqhlaslxn8ljn4mtm2cx76xz72kxkc" + assert(Offer.decode(encoded).get == signedOffer) assert(signedOffer.checkSignature()) assert(signedOffer.amount.isEmpty) - assert(signedOffer.description == "Offer by rusty's node") + assert(signedOffer.description == "basic signed offer") assert(signedOffer.nodeId == nodeId) } test("offer with amount and quantity") { - val encoded = "lno1pqqnyzsmx5cx6umpwssx6atvw35j6ut4v9h8g6t50ysx7enxv4epgrmjw4ehgcm0wfczucm0d5hxzagkqyq3ugztng063cqx783exlm97ekyprnd4rsu5u5w5sez9fecrhcuc3ykq5" - val Success(offer) = Offer.decode(encoded) + val offer = Offer(TlvStream[OfferTlv]( + Chains(Seq(Block.TestnetGenesisBlock.hash)), + Amount(50 msat), + Description("offer with quantity"), + Issuer("alice@bigshop.com"), + QuantityMin(1), + NodeId(nodeKey.publicKey))) + val encoded = "lno1qgsyxjtl6luzd9t3pr62xr7eemp6awnejusgf6gw45q75vcfqqqqqqqgqyeq5ym0venx2u3qwa5hg6pqw96kzmn5d968j9q3v9kxjcm9gp3xjemndphhqtnrdak3vqgprcssxr0juddeytv7nwawhk9nq9us0arnk8j8wnsq8r2e86vzgtfneupe" + assert(Offer.decode(encoded).get == offer) assert(offer.amount.contains(50 msat)) assert(offer.signature.isEmpty) - assert(offer.description == "50msat multi-quantity offer") + assert(offer.description == "offer with quantity") assert(offer.nodeId == nodeId) - assert(offer.issuer.contains("rustcorp.com.au")) + assert(offer.issuer.contains("alice@bigshop.com")) assert(offer.quantityMin.contains(1)) } test("signed offer with amount and quantity") { - val encodedSigned = "lno1pqqnyzsmx5cx6umpwssx6atvw35j6ut4v9h8g6t50ysx7enxv4epgrmjw4ehgcm0wfczucm0d5hxzagkqyq3ugztng063cqx783exlm97ekyprnd4rsu5u5w5sez9fecrhcuc3ykqhcypjju7unu05vav8yvhn27lztf46k9gqlga8uvu4uq62kpuywnu6me8srgh2q7puczukr8arectaapfl5d4rd6uc7st7tnqf0ttx39n40s" - val Success(signedOffer) = Offer.decode(encodedSigned) + val signedOffer = Offer(TlvStream[OfferTlv]( + Chains(Seq(Block.TestnetGenesisBlock.hash)), + Amount(50 msat), + Description("offer with quantity"), + Issuer("alice@bigshop.com"), + QuantityMin(1), + NodeId(nodeKey.publicKey))).sign(nodeKey) + val encoded = "lno1qgsyxjtl6luzd9t3pr62xr7eemp6awnejusgf6gw45q75vcfqqqqqqqgqyeq5ym0venx2u3qwa5hg6pqw96kzmn5d968j9q3v9kxjcm9gp3xjemndphhqtnrdak3vqgprcssxr0juddeytv7nwawhk9nq9us0arnk8j8wnsq8r2e86vzgtfneupe7pqte5yn8m6mtk5racz9c0hgw6smxhp5t0ns77huttmy4632f6t2ns3jdwkxh9qy9f2eun2329gswcz38dn5f2us4us2r76zxvhkj9uecv" + assert(Offer.decode(encoded).get == signedOffer) assert(signedOffer.checkSignature()) assert(signedOffer.amount.contains(50 msat)) - assert(signedOffer.description == "50msat multi-quantity offer") + assert(signedOffer.description == "offer with quantity") assert(signedOffer.nodeId == nodeId) - assert(signedOffer.issuer.contains("rustcorp.com.au")) + assert(signedOffer.issuer.contains("alice@bigshop.com")) assert(signedOffer.quantityMin.contains(1)) } From d2810ffc0c01f7cc14fc82c13f4c461b45d3122c Mon Sep 17 00:00:00 2001 From: Thomas HUET Date: Fri, 22 Jul 2022 10:20:48 +0200 Subject: [PATCH 121/121] Add offers DB --- .../scala/fr/acinq/eclair/db/Databases.scala | 1 + .../scala/fr/acinq/eclair/db/OffersDb.scala | 49 +++++++++++++++++++ 2 files changed, 50 insertions(+) create mode 100644 eclair-core/src/main/scala/fr/acinq/eclair/db/OffersDb.scala diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/Databases.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/Databases.scala index 9713cfbf1b..96e30d5c30 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/Databases.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/Databases.scala @@ -43,6 +43,7 @@ trait Databases { def channels: ChannelsDb def peers: PeersDb def payments: PaymentsDb + def offers: OffersDb def pendingCommands: PendingCommandsDb //@formatter:on } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/OffersDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/OffersDb.scala new file mode 100644 index 0000000000..727bf875d3 --- /dev/null +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/OffersDb.scala @@ -0,0 +1,49 @@ +/* + * 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.db + +import fr.acinq.bitcoin.scalacompat.ByteVector32 +import fr.acinq.bitcoin.scalacompat.Crypto.PrivateKey +import fr.acinq.eclair.MilliSatoshi +import fr.acinq.eclair.payment.Bolt12Invoice +import fr.acinq.eclair.wire.protocol.OfferTypes.{InvoiceRequest, Offer} + +import java.util.UUID + +trait OffersDb extends OffersToSellDb with OffersToBuyDb + +trait OffersToSellDb { + + def addOfferToSell(offer: Offer): Unit + + def getOfferToSell(offerId: ByteVector32, pathId_opt: Option[ByteVector32]): Option[Offer] + + def listOffersToSell(): Seq[(Offer, Option[ByteVector32])] + + def addAttemptToSellOffer(offerId: ByteVector32, invoiceRequest: InvoiceRequest, invoice: Bolt12Invoice, preimage: ByteVector32, pathId: ByteVector32): Unit + +} + +trait OffersToBuyDb { + + def addAttemptToBuyOffer(offer: Offer, amount: MilliSatoshi, quantity: Long, payerKey: PrivateKey, attemptId: UUID): Unit + + def getAttemptToBuyOffer(attemptId: UUID): Option[Any] + + def addInvoiceReceived(attemptId: UUID, invoice: Bolt12Invoice, paymentId_opt: Option[UUID]): Unit + +}