From 2353b24eb953e03a19e459b84bbbd867925e8439 Mon Sep 17 00:00:00 2001 From: sstone Date: Thu, 20 Jun 2024 15:25:57 +0200 Subject: [PATCH] Add support for bech32m bitcoin wallets These changes allow eclair to be used with a bitcoin core wallet configured to generate bech32m (p2tr) addresses and change addresses. The wallet still needs to be able to generate bech32 (p2wpkh) addresses in some cases (support for static_remote_key for non anchor channels for example). --- eclair-core/pom.xml | 15 ++ .../main/scala/fr/acinq/eclair/Eclair.scala | 3 +- .../scala/fr/acinq/eclair/NodeParams.scala | 4 +- .../main/scala/fr/acinq/eclair/Setup.scala | 16 +- .../eclair/blockchain/OnChainWallet.scala | 18 +- .../bitcoind/OnchainPubkeyRefresher.scala | 52 +++-- .../bitcoind/rpc/BitcoinCoreClient.scala | 30 ++- .../blockchain/watchdogs/ExplorerApi.scala | 10 +- .../eclair/channel/fsm/CommonHandlers.scala | 18 +- .../channel/publish/ReplaceableTxFunder.scala | 13 +- .../keymanager/LocalChannelKeyManager.scala | 2 +- .../keymanager/LocalNodeKeyManager.scala | 2 +- .../keymanager/LocalOnChainKeyManager.scala | 183 ++++++++++++++---- .../crypto/keymanager/OnChainKeyManager.scala | 3 +- .../acinq/eclair/payment/Bolt11Invoice.scala | 5 +- .../scala/fr/acinq/eclair/PackageSpec.scala | 20 +- .../scala/fr/acinq/eclair/StartupSpec.scala | 4 +- .../blockchain/DummyOnChainWallet.scala | 22 ++- .../bitcoind/BitcoinCoreClientSpec.scala | 58 +++++- .../blockchain/bitcoind/BitcoindService.scala | 3 +- .../bitcoind/OnchainPubkeyRefresherSpec.scala | 25 ++- .../watchdogs/BlockchainWatchdogSpec.scala | 4 +- .../watchdogs/HeadersOverDnsSpec.scala | 2 +- .../channel/InteractiveTxBuilderSpec.scala | 17 +- .../publish/ReplaceableTxPublisherSpec.scala | 20 +- .../LocalChannelKeyManagerSpec.scala | 8 +- .../keymanager/LocalNodeKeyManagerSpec.scala | 6 +- .../LocalOnChainKeyManagerSpec.scala | 105 ++++++++-- .../fr/acinq/eclair/db/PaymentsDbSpec.scala | 44 ++--- .../integration/ChannelIntegrationSpec.scala | 164 +++++++++++++++- .../eclair/integration/IntegrationSpec.scala | 4 +- .../eclair/payment/Bolt12InvoiceSpec.scala | 6 +- .../eclair/payment/MultiPartHandlerSpec.scala | 2 +- .../payment/PostRestartHtlcCleanerSpec.scala | 2 +- .../payment/receive/InvoicePurgerSpec.scala | 14 +- .../eclair/wire/protocol/OfferTypesSpec.scala | 4 +- pom.xml | 2 +- 37 files changed, 717 insertions(+), 193 deletions(-) diff --git a/eclair-core/pom.xml b/eclair-core/pom.xml index 20f3814e34..5f629f58a4 100644 --- a/eclair-core/pom.xml +++ b/eclair-core/pom.xml @@ -188,6 +188,21 @@ 4.1.94.Final + + fr.acinq.bitcoin + bitcoin-kmp-jvm + 0.20.0 + + + fr.acinq.secp256k1 + secp256k1-kmp-jvm + 0.15.0 + + + fr.acinq.secp256k1 + secp256k1-kmp-jni-jvm + 0.15.0 + fr.acinq bitcoin-lib_${scala.version.short} 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 9aeec55496..7ecd68c7ee 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala @@ -28,6 +28,7 @@ import fr.acinq.bitcoin.scalacompat.{BlockHash, ByteVector32, ByteVector64, Cryp import fr.acinq.eclair.ApiTypes.ChannelNotFound import fr.acinq.eclair.balance.CheckBalance.GlobalBalance import fr.acinq.eclair.balance.{BalanceActor, ChannelsListener} +import fr.acinq.eclair.blockchain.AddressType import fr.acinq.eclair.blockchain.OnChainWallet.OnChainBalance import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinCoreClient import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinCoreClient.{Descriptors, WalletTx} @@ -760,7 +761,7 @@ class EclairImpl(appKit: Kit) extends Eclair with Logging { } override def getOnChainMasterPubKey(account: Long): String = appKit.nodeParams.onChainKeyManager_opt match { - case Some(keyManager) => keyManager.masterPubKey(account) + case Some(keyManager) => keyManager.masterPubKey(account, AddressType.Bech32) case _ => throw new RuntimeException("on-chain seed is not configured") } 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 1c66625b53..ed4df41cd3 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala @@ -187,7 +187,9 @@ object NodeParams extends Logging { private val chain2Hash: Map[String, BlockHash] = Map( "regtest" -> Block.RegtestGenesisBlock.hash, - "testnet" -> Block.TestnetGenesisBlock.hash, + "testnet" -> Block.Testnet3GenesisBlock.hash, + "testnet3" -> Block.Testnet3GenesisBlock.hash, + "testnet4" -> Block.Testnet4GenesisBlock.hash, "signet" -> Block.SignetGenesisBlock.hash, "mainnet" -> Block.LivenetGenesisBlock.hash ) 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 b6ca12c5e5..37a9ba7d41 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala @@ -265,6 +265,7 @@ class Setup(val datadir: File, _ <- feeratesRetrieved.future finalPubkey = new AtomicReference[PublicKey](null) + finalPubkeyScript = new AtomicReference[ByteVector](null) pubkeyRefreshDelay = FiniteDuration(config.getDuration("bitcoind.final-pubkey-refresh-delay").getSeconds, TimeUnit.SECONDS) // there are 3 possibilities regarding onchain key management: // 1) there is no `eclair-signer.conf` file in Eclair's data directory, Eclair will not manage Bitcoin core keys, and Eclair's API will not return bitcoin core descriptors. This is the default mode. @@ -274,18 +275,29 @@ class Setup(val datadir: File, // 3) there is an `eclair-signer.conf` file in Eclair's data directory, and the name of the wallet set in `eclair-signer.conf` matches the `eclair.bitcoind.wallet` setting in `eclair.conf`. // Eclair will assume that this is a watch-only bitcoin wallet that has been created from descriptors generated by Eclair, and will manage its private keys, and here we pass the onchain key manager to our bitcoin client. bitcoinClient = new BitcoinCoreClient(bitcoin, if (bitcoin.wallet == onChainKeyManager_opt.map(_.walletName)) onChainKeyManager_opt else None) with OnchainPubkeyCache { - val refresher: typed.ActorRef[OnchainPubkeyRefresher.Command] = system.spawn(Behaviors.supervise(OnchainPubkeyRefresher(this, finalPubkey, pubkeyRefreshDelay)).onFailure(typed.SupervisorStrategy.restart), name = "onchain-address-manager") + + val refresher: typed.ActorRef[OnchainPubkeyRefresher.Command] = system.spawn(Behaviors.supervise(OnchainPubkeyRefresher(bitcoinChainHash, this, finalPubkey, finalPubkeyScript, pubkeyRefreshDelay)).onFailure(typed.SupervisorStrategy.restart), name = "onchain-address-manager") override def getP2wpkhPubkey(renew: Boolean): PublicKey = { val key = finalPubkey.get() - if (renew) refresher ! OnchainPubkeyRefresher.Renew + if (renew) refresher ! OnchainPubkeyRefresher.RenewPubkey key } + + override def getPubkeyScript(renew: Boolean): ByteVector = { + val script = finalPubkeyScript.get() + if (renew) refresher ! OnchainPubkeyRefresher.RenewPubkeyScript + script + } } _ = if (bitcoinClient.useEclairSigner) logger.info("using eclair to sign bitcoin core transactions") initialPubkey <- bitcoinClient.getP2wpkhPubkey() _ = finalPubkey.set(initialPubkey) + initialAddress <- bitcoinClient.getReceiveAddress() + Right(initialPubkeyScript) = addressToPublicKeyScript(bitcoinChainHash, initialAddress) + _ = finalPubkeyScript.set(Script.write(initialPubkeyScript)) + // If we started funding a transaction and restarted before signing it, we may have utxos that stay locked forever. // We want to do something about it: we can unlock them automatically, or let the node operator decide what to do. // 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 3544636678..fb03d4f741 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 @@ -18,7 +18,7 @@ package fr.acinq.eclair.blockchain import fr.acinq.bitcoin.psbt.Psbt import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey -import fr.acinq.bitcoin.scalacompat.{OutPoint, Satoshi, Transaction, TxId} +import fr.acinq.bitcoin.scalacompat.{BlockHash, OutPoint, Satoshi, Script, Transaction, TxId, addressToPublicKeyScript} import fr.acinq.eclair.blockchain.fee.FeeratePerKw import scodec.bits.ByteVector @@ -28,6 +28,18 @@ import scala.concurrent.{ExecutionContext, Future} * Created by PM on 06/07/2017. */ +sealed trait AddressType + +object AddressType { + case object Bech32 extends AddressType { + override def toString: String = "bech32" + } + + case object Bech32m extends AddressType { + override def toString: String = "bech32m" + } +} + /** This trait lets users fund lightning channels. */ trait OnChainChannelFunder { @@ -119,7 +131,7 @@ trait OnChainAddressGenerator { /** * @param label used if implemented with bitcoin core, can be ignored by implementation */ - def getReceiveAddress(label: String = "")(implicit ec: ExecutionContext): Future[String] + def getReceiveAddress(label: String = "", addressType_opt: Option[AddressType] = None)(implicit ec: ExecutionContext): Future[String] /** Generate a p2wpkh wallet address and return the corresponding public key. */ def getP2wpkhPubkey()(implicit ec: ExecutionContext): Future[PublicKey] @@ -132,6 +144,8 @@ trait OnchainPubkeyCache { * @param renew applies after requesting the current pubkey, and is asynchronous */ def getP2wpkhPubkey(renew: Boolean = true): PublicKey + + def getPubkeyScript(renew: Boolean = true): ByteVector } /** This trait lets users check the wallet's on-chain balance. */ diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/OnchainPubkeyRefresher.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/OnchainPubkeyRefresher.scala index 391e8212bc..9564d27df4 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/OnchainPubkeyRefresher.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/OnchainPubkeyRefresher.scala @@ -3,8 +3,10 @@ package fr.acinq.eclair.blockchain.bitcoind import akka.actor.typed.Behavior import akka.actor.typed.scaladsl.{ActorContext, Behaviors, TimerScheduler} +import fr.acinq.bitcoin.scalacompat.{BlockHash, Script, addressToPublicKeyScript} import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.eclair.blockchain.OnChainAddressGenerator +import scodec.bits.ByteVector import java.util.concurrent.atomic.AtomicReference import scala.concurrent.ExecutionContext.Implicits.global @@ -12,52 +14,78 @@ import scala.concurrent.duration.FiniteDuration import scala.util.{Failure, Success} /** - * Handles the renewal of public keys generated by bitcoin core and used to send onchain funds to when channels get closed + * Handles the renewal of public keys and public key scripts generated by bitcoin core and used to send onchain funds to when channels get closed */ object OnchainPubkeyRefresher { // @formatter:off sealed trait Command - case object Renew extends Command - private case class Set(pubkey: PublicKey) extends Command + case object RenewPubkey extends Command + private case class SetPubkey(pubkey: PublicKey) extends Command + case object RenewPubkeyScript extends Command + private case class SetPubkeyScript(pubkeyScript: ByteVector) extends Command private case class Error(reason: Throwable) extends Command private case object Done extends Command // @formatter:on - def apply(generator: OnChainAddressGenerator, finalPubkey: AtomicReference[PublicKey], delay: FiniteDuration): Behavior[Command] = { + def apply(chainHash: BlockHash, generator: OnChainAddressGenerator, finalPubkey: AtomicReference[PublicKey], finalPubkeyScript: AtomicReference[ByteVector], delay: FiniteDuration): Behavior[Command] = { Behaviors.setup { context => Behaviors.withTimers { timers => - new OnchainPubkeyRefresher(generator, finalPubkey, context, timers, delay).idle() + new OnchainPubkeyRefresher(chainHash, generator, finalPubkey, finalPubkeyScript, context, timers, delay).idle() } } } } -private class OnchainPubkeyRefresher(generator: OnChainAddressGenerator, finalPubkey: AtomicReference[PublicKey], context: ActorContext[OnchainPubkeyRefresher.Command], timers: TimerScheduler[OnchainPubkeyRefresher.Command], delay: FiniteDuration) { +private class OnchainPubkeyRefresher(chainHash: BlockHash, generator: OnChainAddressGenerator, finalPubkey: AtomicReference[PublicKey], finalPubkeyScript: AtomicReference[ByteVector], context: ActorContext[OnchainPubkeyRefresher.Command], timers: TimerScheduler[OnchainPubkeyRefresher.Command], delay: FiniteDuration) { import OnchainPubkeyRefresher._ def idle(): Behavior[Command] = Behaviors.receiveMessagePartial { - case Renew => - context.log.debug(s"received Renew current script is ${finalPubkey.get()}") + case RenewPubkey => + context.log.debug(s"received RenewPubkey current pubkey is ${finalPubkey.get()}") context.pipeToSelf(generator.getP2wpkhPubkey()) { - case Success(pubkey) => Set(pubkey) + case Success(pubkey) => SetPubkey(pubkey) case Failure(reason) => Error(reason) } Behaviors.receiveMessagePartial { - case Set(script) => + case SetPubkey(script) => timers.startSingleTimer(Done, delay) // wait a bit to avoid generating too many addresses in case of mass channel force-close waiting(script) case Error(reason) => context.log.error("cannot generate new onchain address", reason) Behaviors.same } + case RenewPubkeyScript => + context.log.debug(s"received Renew current script is ${finalPubkeyScript.get()}") + context.pipeToSelf(generator.getReceiveAddress("")) { + case Success(address) => addressToPublicKeyScript(chainHash, address) match { + case Right(script) => SetPubkeyScript(Script.write(script)) + case Left(error) => Error(error.getCause) + } + case Failure(reason) => Error(reason) + } + Behaviors.receiveMessagePartial { + case SetPubkeyScript(script) => + timers.startSingleTimer(Done, delay) // wait a bit to avoid generating too many addresses in case of mass channel force-close + waiting(script) + case Error(reason) => + context.log.error("cannot generate new onchain address", reason) + Behaviors.same + } + } + + def waiting(pubkey: PublicKey): Behavior[Command] = Behaviors.receiveMessagePartial { + case Done => + context.log.info(s"setting final onchain public key to $pubkey") + finalPubkey.set(pubkey) + idle() } - def waiting(script: PublicKey): Behavior[Command] = Behaviors.receiveMessagePartial { + def waiting(script: ByteVector): Behavior[Command] = Behaviors.receiveMessagePartial { case Done => context.log.info(s"setting final onchain script to $script") - finalPubkey.set(script) + finalPubkeyScript.set(script) idle() } } 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 f6a8daa965..e3e6defdfc 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 @@ -21,12 +21,11 @@ import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.bitcoin.scalacompat._ import fr.acinq.bitcoin.{Bech32, Block, SigHash} import fr.acinq.eclair.ShortChannelId.coordinates -import fr.acinq.eclair.blockchain.OnChainWallet import fr.acinq.eclair.blockchain.OnChainWallet.{FundTransactionResponse, MakeFundingTxResponse, OnChainBalance, ProcessPsbtResponse} import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.{GetTxWithMetaResponse, UtxoStatus, ValidateResult} import fr.acinq.eclair.blockchain.fee.{FeeratePerKB, FeeratePerKw} +import fr.acinq.eclair.blockchain.{AddressType, OnChainWallet} import fr.acinq.eclair.crypto.keymanager.OnChainKeyManager -import fr.acinq.eclair.json.SatoshiSerializer import fr.acinq.eclair.transactions.Transactions import fr.acinq.eclair.wire.protocol.ChannelAnnouncement import fr.acinq.eclair.{BlockHeight, TimestampSecond, TxCoordinates} @@ -550,10 +549,29 @@ class BitcoinCoreClient(val rpcClient: BitcoinJsonRPCClient, val onChainKeyManag } } - def getReceiveAddress(label: String)(implicit ec: ExecutionContext): Future[String] = for { - JString(address) <- rpcClient.invoke("getnewaddress", label) - _ <- extractPublicKey(address) - } yield address + private def verifyAddress(address: String)(implicit ec: ExecutionContext): Future[String] = { + for { + addressInfo <- rpcClient.invoke("getaddressinfo", address) + JString(keyPath) = addressInfo \ "hdkeypath" + } yield { + // check that when we manage private keys we can re-compute the address we got from bitcoin core + onChainKeyManager_opt match { + case Some(keyManager) => + val (_, computed) = keyManager.derivePublicKey(DeterministicWallet.KeyPath(keyPath)) + if (computed != address) return Future.failed(new RuntimeException("cannot recompute address generated by bitcoin core")) + case None => () + } + address + } + } + + def getReceiveAddress(label: String, addressType_opt: Option[AddressType] = None)(implicit ec: ExecutionContext): Future[String] = { + val params = List(label) ++ addressType_opt.map(_.toString).toList + for { + JString(address) <- rpcClient.invoke("getnewaddress", params: _*) + _ <- verifyAddress(address) + } yield address + } def getP2wpkhPubkey()(implicit ec: ExecutionContext): Future[Crypto.PublicKey] = for { JString(address) <- rpcClient.invoke("getnewaddress", "", "bech32") 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 e9971d620a..8c76a3f4c0 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 @@ -118,7 +118,7 @@ object ExplorerApi { 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", + Block.Testnet3GenesisBlock.hash -> uri"https://api.blockcypher.com/v1/btc/test3", Block.LivenetGenesisBlock.hash -> uri"https://api.blockcypher.com/v1/btc/main" ) @@ -208,12 +208,12 @@ object ExplorerApi { override val name = "blockstream.info" override val baseUris = if (useTorEndpoints) { Map( - Block.TestnetGenesisBlock.hash -> uri"http://explorerzydxu5ecjrkwceayqybizmpjjznk5izmitf2modhcusuqlid.onion/testnet/api", + Block.Testnet3GenesisBlock.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.Testnet3GenesisBlock.hash -> uri"https://blockstream.info/testnet/api", Block.LivenetGenesisBlock.hash -> uri"https://blockstream.info/api" ) } @@ -224,13 +224,13 @@ object ExplorerApi { override val name = "mempool.space" override val baseUris = if (useTorEndpoints) { Map( - Block.TestnetGenesisBlock.hash -> uri"http://mempoolhqx4isw62xs7abwphsq7ldayuidyx2v2oethdhhj6mlo2r6ad.onion/testnet/api", + Block.Testnet3GenesisBlock.hash -> uri"http://mempoolhqx4isw62xs7abwphsq7ldayuidyx2v2oethdhhj6mlo2r6ad.onion/testnet/api", Block.LivenetGenesisBlock.hash -> uri"http://mempoolhqx4isw62xs7abwphsq7ldayuidyx2v2oethdhhj6mlo2r6ad.onion/api", Block.SignetGenesisBlock.hash -> uri"http://mempoolhqx4isw62xs7abwphsq7ldayuidyx2v2oethdhhj6mlo2r6ad.onion/signet/api" ) } else { Map( - Block.TestnetGenesisBlock.hash -> uri"https://mempool.space/testnet/api", + Block.Testnet3GenesisBlock.hash -> uri"https://mempool.space/testnet/api", Block.LivenetGenesisBlock.hash -> uri"https://mempool.space/api", Block.SignetGenesisBlock.hash -> uri"https://mempool.space/signet/api" ) 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 a5efa07cb6..9c9ef0152c 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 @@ -16,13 +16,13 @@ package fr.acinq.eclair.channel.fsm -import akka.actor.{ActorRef, FSM, Status} +import akka.actor.FSM import fr.acinq.bitcoin.scalacompat.{ByteVector32, Script} -import fr.acinq.eclair.Features 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 fr.acinq.eclair.{Features, InitFeature} import scodec.bits.ByteVector import scala.concurrent.duration.DurationInt @@ -115,17 +115,21 @@ trait CommonHandlers { upfrontShutdownScript } else { log.info("ignoring pre-generated shutdown script, because option_upfront_shutdown_script is disabled") - generateFinalScriptPubKey() + generateFinalScriptPubKey(data.commitments.params.localParams.initFeatures, data.commitments.params.remoteParams.initFeatures) } case None => // normal case: we don't pre-generate shutdown scripts - generateFinalScriptPubKey() + generateFinalScriptPubKey(data.commitments.params.localParams.initFeatures, data.commitments.params.remoteParams.initFeatures) } } - private def generateFinalScriptPubKey(): ByteVector = { - val finalPubKey = wallet.getP2wpkhPubkey() - val finalScriptPubKey = Script.write(Script.pay2wpkh(finalPubKey)) + private def generateFinalScriptPubKey(localFeatures: Features[InitFeature], remoteFeatures: Features[InitFeature]): ByteVector = { + val allowAnySegwit = Features.canUseFeature(localFeatures, remoteFeatures, Features.ShutdownAnySegwit) + val finalScriptPubKey = if (allowAnySegwit) { + wallet.getPubkeyScript() + } else { + Script.write(Script.pay2wpkh(wallet.getP2wpkhPubkey())) + } log.info(s"using finalScriptPubkey=$finalScriptPubKey") finalScriptPubKey } 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 4c3d1777aa..6826df4393 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 @@ -359,8 +359,17 @@ private class ReplaceableTxFunder(nodeParams: NodeParams, // We create a PSBT with the non-wallet input already signed: val psbt = new Psbt(locallySignedTx.txInfo.tx) - .updateWitnessInput(locallySignedTx.txInfo.input.outPoint, locallySignedTx.txInfo.input.txOut, null, fr.acinq.bitcoin.Script.parse(locallySignedTx.txInfo.input.redeemScript), fr.acinq.bitcoin.SigHash.SIGHASH_ALL, java.util.Map.of()) - .flatMap(_.finalizeWitnessInput(0, locallySignedTx.txInfo.tx.txIn.head.witness)) + .updateWitnessInput( + locallySignedTx.txInfo.input.outPoint, + locallySignedTx.txInfo.input.txOut, + null, + fr.acinq.bitcoin.Script.parse(locallySignedTx.txInfo.input.redeemScript), + fr.acinq.bitcoin.SigHash.SIGHASH_ALL, + java.util.Map.of(), + null, + null, + java.util.Map.of() + ).flatMap(_.finalizeWitnessInput(0, locallySignedTx.txInfo.tx.txIn.head.witness)) psbt match { case Left(failure) => log.error(s"cannot sign ${cmd.desc}: $failure") 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 ab04e54b48..75ab2736ac 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 @@ -32,7 +32,7 @@ import scodec.bits.ByteVector object LocalChannelKeyManager { def keyBasePath(chainHash: BlockHash): List[Long] = (chainHash: @unchecked) match { - case Block.RegtestGenesisBlock.hash | Block.TestnetGenesisBlock.hash | Block.SignetGenesisBlock.hash => DeterministicWallet.hardened(46) :: DeterministicWallet.hardened(1) :: Nil + case Block.RegtestGenesisBlock.hash | Block.Testnet4GenesisBlock.hash | Block.Testnet3GenesisBlock.hash | Block.SignetGenesisBlock.hash => DeterministicWallet.hardened(46) :: DeterministicWallet.hardened(1) :: Nil case Block.LivenetGenesisBlock.hash => DeterministicWallet.hardened(47) :: DeterministicWallet.hardened(1) :: Nil } } 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 a0cfc018ca..007e7a8e3c 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 @@ -28,7 +28,7 @@ object LocalNodeKeyManager { // Note that the node path and the above channel path are on different branches so even if the // node key is compromised there is no way to retrieve the wallet keys def keyBasePath(chainHash: BlockHash): List[Long] = (chainHash: @unchecked) match { - case Block.RegtestGenesisBlock.hash | Block.TestnetGenesisBlock.hash | Block.SignetGenesisBlock.hash => DeterministicWallet.hardened(46) :: DeterministicWallet.hardened(0) :: Nil + case Block.RegtestGenesisBlock.hash | Block.Testnet4GenesisBlock.hash | Block.Testnet3GenesisBlock.hash | Block.SignetGenesisBlock.hash => DeterministicWallet.hardened(46) :: DeterministicWallet.hardened(0) :: Nil case Block.LivenetGenesisBlock.hash => DeterministicWallet.hardened(47) :: DeterministicWallet.hardened(0) :: Nil } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/LocalOnChainKeyManager.scala b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/LocalOnChainKeyManager.scala index 0aa38d8194..8c1e6bb6ea 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/LocalOnChainKeyManager.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/LocalOnChainKeyManager.scala @@ -17,16 +17,18 @@ package fr.acinq.eclair.crypto.keymanager import com.typesafe.config.ConfigFactory +import fr.acinq.bitcoin.ScriptTree import fr.acinq.bitcoin.psbt.{Psbt, UpdateFailure} import fr.acinq.bitcoin.scalacompat.DeterministicWallet._ -import fr.acinq.bitcoin.scalacompat.{Block, BlockHash, Crypto, DeterministicWallet, MnemonicCode, Satoshi, Script, computeBIP84Address} +import fr.acinq.bitcoin.scalacompat.{Block, BlockHash, Crypto, DeterministicWallet, KotlinUtils, MnemonicCode, Satoshi, Script, ScriptWitness, computeBIP84Address} import fr.acinq.eclair.TimestampSecond +import fr.acinq.eclair.blockchain.AddressType import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinCoreClient.{Descriptor, Descriptors} import grizzled.slf4j.Logging import scodec.bits.ByteVector import java.io.File -import scala.jdk.CollectionConverters.{CollectionHasAsScala, MapHasAsScala} +import scala.jdk.CollectionConverters.MapHasAsScala import scala.util.{Failure, Success, Try} object LocalOnChainKeyManager extends Logging { @@ -49,7 +51,7 @@ object LocalOnChainKeyManager extends Logging { val passphrase = config.getString("eclair.signer.passphrase") val timestamp = config.getLong("eclair.signer.timestamp") val keyManager = new LocalOnChainKeyManager(wallet, MnemonicCode.toSeed(mnemonics, passphrase), TimestampSecond(timestamp), chainHash) - logger.info(s"using on-chain key manager wallet=$wallet xpub=${keyManager.masterPubKey(0)}") + logger.info(s"using on-chain key manager wallet=$wallet xpub bech32=${keyManager.masterPubKey(0, AddressType.Bech32)} xpub bech32m=${keyManager.masterPubKey(0, AddressType.Bech32m)}") Some(keyManager) } else { None @@ -73,46 +75,90 @@ class LocalOnChainKeyManager(override val walletName: String, seed: ByteVector, private val fingerprint = DeterministicWallet.fingerprint(master) & 0xFFFFFFFFL private val fingerPrintHex = String.format("%8s", fingerprint.toHexString).replace(' ', '0') // Root BIP32 on-chain path: we use BIP84 (p2wpkh) paths: m/84h/{0h/1h} - private val rootPath = chainHash match { - case Block.RegtestGenesisBlock.hash | Block.TestnetGenesisBlock.hash | Block.SignetGenesisBlock.hash => "84h/1h" + private val rootPathBIP84 = chainHash match { + case Block.RegtestGenesisBlock.hash | Block.Testnet4GenesisBlock.hash | Block.Testnet3GenesisBlock.hash | Block.SignetGenesisBlock.hash => "84h/1h" case Block.LivenetGenesisBlock.hash => "84h/0h" case _ => throw new IllegalArgumentException(s"invalid chain hash $chainHash") } - private val rootKey = DeterministicWallet.derivePrivateKey(master, KeyPath(rootPath)) + private val rootPathBIP86 = chainHash match { + case Block.RegtestGenesisBlock.hash | Block.Testnet4GenesisBlock.hash | Block.Testnet3GenesisBlock.hash | Block.SignetGenesisBlock.hash => "86h/1h" + case Block.LivenetGenesisBlock.hash => "86h/0h" + case _ => throw new IllegalArgumentException(s"invalid chain hash $chainHash") + } + private val rootKeyBIP84 = DeterministicWallet.derivePrivateKey(master, KeyPath(rootPathBIP84)) + private val rootKeyBIP86 = DeterministicWallet.derivePrivateKey(master, KeyPath(rootPathBIP86)) - override def masterPubKey(account: Long): String = { - val prefix = chainHash match { - case Block.RegtestGenesisBlock.hash | Block.TestnetGenesisBlock.hash | Block.SignetGenesisBlock.hash => vpub - case Block.LivenetGenesisBlock.hash => zpub - case _ => throw new IllegalArgumentException(s"invalid chain hash $chainHash") + private def addressType(keyPath: KeyPath): AddressType = { + if (keyPath.path.nonEmpty && keyPath.path.head == hardened(86)) { + AddressType.Bech32m + } else { + AddressType.Bech32 } - // master pubkey for account 0 is m/84h/{0h/1h}/0h - val accountPub = DeterministicWallet.publicKey(DeterministicWallet.derivePrivateKey(rootKey, hardened(account))) - DeterministicWallet.encode(accountPub, prefix) + } + + override def masterPubKey(account: Long, addressType: AddressType): String = addressType match { + case AddressType.Bech32 => + val prefix = chainHash match { + case Block.RegtestGenesisBlock.hash | Block.Testnet4GenesisBlock.hash | Block.Testnet3GenesisBlock.hash | Block.SignetGenesisBlock.hash => vpub + case Block.LivenetGenesisBlock.hash => zpub + case _ => throw new IllegalArgumentException(s"invalid chain hash $chainHash") + } + // master pubkey for account 0 is m/84h/{0h/1h}/0h + val accountPub = DeterministicWallet.publicKey(DeterministicWallet.derivePrivateKey(rootKeyBIP84, hardened(account))) + DeterministicWallet.encode(accountPub, prefix) + case AddressType.Bech32m => + val prefix = chainHash match { + case Block.RegtestGenesisBlock.hash | Block.Testnet4GenesisBlock.hash | Block.Testnet3GenesisBlock.hash | Block.SignetGenesisBlock.hash => tpub + case Block.LivenetGenesisBlock.hash => xpub + case _ => throw new IllegalArgumentException(s"invalid chain hash $chainHash") + } + // master pubkey for account 0 is m/86h/{0h/1h}/0h + val accountPub = DeterministicWallet.publicKey(DeterministicWallet.derivePrivateKey(rootKeyBIP86, hardened(account))) + DeterministicWallet.encode(accountPub, prefix) } override def derivePublicKey(keyPath: KeyPath): (Crypto.PublicKey, String) = { + import KotlinUtils._ val pub = DeterministicWallet.derivePrivateKey(master, keyPath).publicKey - val address = computeBIP84Address(pub, chainHash) + val address = addressType(keyPath) match { + case AddressType.Bech32m => fr.acinq.bitcoin.Bitcoin.computeBIP86Address(pub, chainHash) + case AddressType.Bech32 => computeBIP84Address(pub, chainHash) + } (pub, address) } override def descriptors(account: Long): Descriptors = { - val keyPath = s"$rootPath/${account}h" val prefix = chainHash match { case Block.LivenetGenesisBlock.hash => xpub case _ => tpub } - val accountPub = DeterministicWallet.publicKey(DeterministicWallet.derivePrivateKey(rootKey, hardened(account))) - // descriptors for account 0 are: - // 84h/{0h/1h}/0h/0/* for main addresses - // 84h/{0h/1h}/0h/1/* for change addresses - val receiveDesc = s"wpkh([$fingerPrintHex/$keyPath]${encode(accountPub, prefix)}/0/*)" - val changeDesc = s"wpkh([$fingerPrintHex/$keyPath]${encode(accountPub, prefix)}/1/*)" - Descriptors(wallet_name = walletName, descriptors = List( - Descriptor(desc = s"$receiveDesc#${fr.acinq.bitcoin.Descriptor.checksum(receiveDesc)}", internal = false, active = true, timestamp = walletTimestamp.toLong), - Descriptor(desc = s"$changeDesc#${fr.acinq.bitcoin.Descriptor.checksum(changeDesc)}", internal = true, active = true, timestamp = walletTimestamp.toLong), - )) + val descriptorsBIP84 = { + val keyPath = s"$rootPathBIP84/${account}h" + val accountPub = DeterministicWallet.publicKey(DeterministicWallet.derivePrivateKey(rootKeyBIP84, hardened(account))) + // descriptors for account 0 are: + // 84h/{0h/1h}/0h/0/* for main addresses + // 84h/{0h/1h}/0h/1/* for change addresses + val receiveDesc = s"wpkh([$fingerPrintHex/$keyPath]${encode(accountPub, prefix)}/0/*)" + val changeDesc = s"wpkh([$fingerPrintHex/$keyPath]${encode(accountPub, prefix)}/1/*)" + List( + Descriptor(desc = s"$receiveDesc#${fr.acinq.bitcoin.Descriptor.checksum(receiveDesc)}", internal = false, active = true, timestamp = walletTimestamp.toLong), + Descriptor(desc = s"$changeDesc#${fr.acinq.bitcoin.Descriptor.checksum(changeDesc)}", internal = true, active = true, timestamp = walletTimestamp.toLong), + ) + } + val descriptorsBIP86 = { + val keyPath = s"$rootPathBIP86/${account}h" + val accountPub = DeterministicWallet.publicKey(DeterministicWallet.derivePrivateKey(rootKeyBIP86, hardened(account))) + // descriptors for account 0 are: + // 86h/{0h/1h}/0h/0/* for main addresses + // 86h/{0h/1h}/0h/1/* for change addresses + val receiveDesc = s"tr([$fingerPrintHex/$keyPath]${encode(accountPub, prefix)}/0/*)" + val changeDesc = s"tr([$fingerPrintHex/$keyPath]${encode(accountPub, prefix)}/1/*)" + List( + Descriptor(desc = s"$receiveDesc#${fr.acinq.bitcoin.Descriptor.checksum(receiveDesc)}", internal = false, active = true, timestamp = walletTimestamp.toLong), + Descriptor(desc = s"$changeDesc#${fr.acinq.bitcoin.Descriptor.checksum(changeDesc)}", internal = true, active = true, timestamp = walletTimestamp.toLong), + ) + } + Descriptors(wallet_name = walletName, descriptors = descriptorsBIP84 ++ descriptorsBIP86) } override def sign(psbt: Psbt, ourInputs: Seq[Int], ourOutputs: Seq[Int]): Try[Psbt] = { @@ -149,37 +195,59 @@ class LocalOnChainKeyManager(override val walletName: String, seed: ByteVector, /** Check that an output belongs to us (i.e. we can recompute its public key from its bip32 path). */ private def isOurOutput(psbt: Psbt, outputIndex: Int): Boolean = { import fr.acinq.bitcoin.scalacompat.KotlinUtils._ + if (psbt.outputs.size() <= outputIndex || psbt.global.tx.txOut.size() <= outputIndex) { return false } val output = psbt.outputs.get(outputIndex) val txOut = psbt.global.tx.txOut.get(outputIndex) + + def expectedPubKey(keyPath: KeyPath): fr.acinq.bitcoin.PublicKey = derivePublicKey(keyPath)._1 + + def expectedBIP84Script(keyPath: KeyPath): fr.acinq.bitcoin.ByteVector = Script.write(Script.pay2wpkh(expectedPubKey(keyPath))) + + def expectedBIP86Script(keyPath: KeyPath): fr.acinq.bitcoin.ByteVector = Script.write(Script.pay2tr(expectedPubKey(keyPath).xOnly, None)) + output.getDerivationPaths.asScala.headOption match { - case Some((pub, keypath)) => - val (expectedPubKey, _) = derivePublicKey(KeyPath(keypath.keyPath.path.asScala.toSeq.map(_.longValue()))) - val expectedScript = Script.write(Script.pay2wpkh(expectedPubKey)) - if (expectedPubKey != kmp2scala(pub)) { - logger.warn(s"public key mismatch (expected=$expectedPubKey, actual=$pub): bitcoin core may be malicious") - false - } else if (kmp2scala(txOut.publicKeyScript) != expectedScript) { - logger.warn(s"script mismatch (expected=$expectedScript, actual=${txOut.publicKeyScript}): bitcoin core may be malicious") - false - } else { - true - } - case None => - logger.warn("derivation path is missing: bitcoin core may be malicious") + case Some((pub, keypath)) if pub != expectedPubKey(keypath.keyPath) => + logger.warn(s"public key mismatch (expected=${expectedPubKey(keypath.keyPath)}, actual=$pub): bitcoin core may be malicious") + false + case Some((_, keypath)) if txOut.publicKeyScript != expectedBIP84Script(keypath.keyPath) => + logger.warn(s"script mismatch (expected=${expectedBIP84Script(keypath.keyPath)}, actual=${txOut.publicKeyScript}): bitcoin core may be malicious") false + case Some((_, _)) => + true + case None => + output.getTaprootDerivationPaths.asScala.headOption match { + case Some((pub, _)) if pub != output.getTaprootInternalKey => + logger.warn("internal key mismatch: bitcoin core may be malicious") + false + case Some((_, keyPath)) if txOut.publicKeyScript != expectedBIP86Script(keyPath.keyPath) => + logger.warn(s"script mismatch (expected=${expectedBIP86Script(keyPath.keyPath)}, actual=${txOut.publicKeyScript}): bitcoin core may be malicious") + false + case Some((_, _)) => + true + case None => + logger.warn("derivation path is missing: bitcoin core may be malicious") + false + } } } private def signPsbtInput(psbt: Psbt, pos: Int): Try[Psbt] = Try { + val input = psbt.getInput(pos) + require(input != null, s"input $pos is missing from psbt: bitcoin core may be malicious") + if (input.getTaprootInternalKey != null) signPsbtInput86(psbt, pos) else signPsbtInput84(psbt, pos) + } + + private def signPsbtInput84(psbt: Psbt, pos: Int): Psbt = { import fr.acinq.bitcoin.scalacompat.KotlinUtils._ import fr.acinq.bitcoin.{Script, SigHash} val input = psbt.getInput(pos) require(input != null, s"input $pos is missing from psbt: bitcoin core may be malicious") + // For each wallet input, Bitcoin Core will provide: // - the output that was spent, in the PSBT's witness utxo field // - the actual transaction that was spent, in the PSBT's non-witness utxo field @@ -202,7 +270,16 @@ class LocalOnChainKeyManager(override val walletName: String, seed: ByteVector, require(kmp2scala(input.getWitnessUtxo.publicKeyScript) == expectedScript, s"script mismatch (expected=$expectedScript, actual=${input.getWitnessUtxo.publicKeyScript}): bitcoin core may be malicious") // Update the input with the right script for a p2wpkh input, which is a *p2pkh* script, then sign and finalize. - val updated: Either[UpdateFailure, Psbt] = psbt.updateWitnessInput(psbt.global.tx.txIn.get(pos).outPoint, input.getWitnessUtxo, null, Script.pay2pkh(pub), SigHash.SIGHASH_ALL, input.getDerivationPaths) + val updated: Either[UpdateFailure, Psbt] = psbt.updateWitnessInput( + psbt.global.tx.txIn.get(pos).outPoint, + input.getWitnessUtxo, + null, + Script.pay2pkh(pub), + SigHash.SIGHASH_ALL, + input.getDerivationPaths, + null, + null, + java.util.Map.of()) val signed = updated.flatMap(_.sign(priv, pos)) val finalized = signed.flatMap(s => { require(s.getSig.last.toInt == SigHash.SIGHASH_ALL, "signature must end with SIGHASH_ALL") @@ -213,4 +290,28 @@ class LocalOnChainKeyManager(override val walletName: String, seed: ByteVector, case Left(failure) => throw new RuntimeException(s"cannot sign psbt input, error = $failure") } } + + private def signPsbtInput86(psbt: Psbt, pos: Int): Psbt = { + import fr.acinq.bitcoin.scalacompat.KotlinUtils._ + import fr.acinq.bitcoin.{Script, SigHash} + + val input = psbt.getInput(pos) + require(input != null, s"input $pos is missing from psbt: bitcoin core may be malicious") + require(Option(input.getSighashType).forall(_ == SigHash.SIGHASH_DEFAULT), s"input sighash must be SIGHASH_DEFAULT (got=${input.getSighashType}): bitcoin core may be malicious") + + // Check that we're signing a p2tr input and that the keypath is provided and correct. + require(input.getTaprootDerivationPaths.size() == 1, "bip32 derivation path is missing: bitcoin core may be malicious") + val (pub, keypath) = input.getTaprootDerivationPaths.asScala.toSeq.head + val priv = fr.acinq.bitcoin.DeterministicWallet.derivePrivateKey(master.priv, keypath.keyPath).getPrivateKey + require(priv.publicKey().xOnly() == pub, s"derived public key doesn't match (expected=$pub actual=${priv.publicKey().xOnly()}): bitcoin core may be malicious") + val expectedScript = ByteVector(Script.write(Script.pay2tr(pub, null.asInstanceOf[ScriptTree]))) + require(kmp2scala(input.getWitnessUtxo.publicKeyScript) == expectedScript, s"script mismatch (expected=$expectedScript, actual=${input.getWitnessUtxo.publicKeyScript}): bitcoin core may be malicious") + + val signed = psbt.sign(priv, pos) + val finalized = signed.flatMap(s => s.getPsbt.finalizeWitnessInput(pos, ScriptWitness(List(s.getSig)))) + finalized match { + case Right(psbt) => psbt + case Left(failure) => throw new RuntimeException(s"cannot sign psbt input, error = $failure") + } + } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/OnChainKeyManager.scala b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/OnChainKeyManager.scala index 9e555f1092..6bc36f8507 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/OnChainKeyManager.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/OnChainKeyManager.scala @@ -19,6 +19,7 @@ package fr.acinq.eclair.crypto.keymanager import fr.acinq.bitcoin.psbt.Psbt import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.bitcoin.scalacompat.DeterministicWallet.KeyPath +import fr.acinq.eclair.blockchain.AddressType import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinCoreClient.Descriptors import scala.util.Try @@ -30,7 +31,7 @@ trait OnChainKeyManager { * @param account account number (0 is used by most wallets) * @return the on-chain pubkey for this account, which can then be imported into a BIP39-compatible wallet such as Electrum */ - def masterPubKey(account: Long): String + def masterPubKey(account: Long, addressType: AddressType): String /** * @param keyPath BIP32 path 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 76cf3703dc..8370de25e4 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 @@ -138,7 +138,8 @@ object Bolt11Invoice { val prefixes = Map( Block.RegtestGenesisBlock.hash -> "lnbcrt", Block.SignetGenesisBlock.hash -> "lntbs", - Block.TestnetGenesisBlock.hash -> "lntb", + Block.Testnet3GenesisBlock.hash -> "lntb", + Block.Testnet4GenesisBlock.hash -> "lntb", Block.LivenetGenesisBlock.hash -> "lnbc" ) @@ -531,7 +532,7 @@ object Bolt11Invoice { val lowercaseInput = input.toLowerCase val separatorIndex = lowercaseInput.lastIndexOf('1') val hrp = lowercaseInput.take(separatorIndex) - val prefix: String = prefixes.values.find(prefix => hrp.startsWith(prefix)).getOrElse(throw new RuntimeException("unknown prefix")) + val prefix: String = prefixes.values.toSeq.sortBy(_.length).findLast(prefix => hrp.startsWith(prefix)).getOrElse(throw new RuntimeException("unknown prefix")) val data = string2Bits(lowercaseInput.slice(separatorIndex + 1, lowercaseInput.length - 6)) // 6 == checksum size val bolt11Data = Codecs.bolt11DataCodec.decode(data).require.value val signature = ByteVector64(bolt11Data.signature.take(64)) 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 fb990193ee..593ad580ed 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/PackageSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/PackageSpec.scala @@ -50,16 +50,16 @@ class PackageSpec extends AnyFunSuite { // p2pkh // valid chain - assert(addressToPublicKeyScript(Block.TestnetGenesisBlock.hash, Base58Check.encode(Base58.Prefix.PubkeyAddressTestnet, pub.hash160)) == Right(Script.pay2pkh(pub))) - assert(addressToPublicKeyScript(Block.TestnetGenesisBlock.hash, Base58Check.encode(Base58.Prefix.PubkeyAddressTestnet, pub.hash160)) == Right(Script.pay2pkh(pub))) - assert(addressToPublicKeyScript(Block.TestnetGenesisBlock.hash, Base58Check.encode(Base58.Prefix.PubkeyAddressTestnet, pub.hash160)) == Right(Script.pay2pkh(pub))) + assert(addressToPublicKeyScript(Block.Testnet3GenesisBlock.hash, Base58Check.encode(Base58.Prefix.PubkeyAddressTestnet, pub.hash160)) == Right(Script.pay2pkh(pub))) + assert(addressToPublicKeyScript(Block.Testnet3GenesisBlock.hash, Base58Check.encode(Base58.Prefix.PubkeyAddressTestnet, pub.hash160)) == Right(Script.pay2pkh(pub))) + assert(addressToPublicKeyScript(Block.Testnet3GenesisBlock.hash, Base58Check.encode(Base58.Prefix.PubkeyAddressTestnet, pub.hash160)) == Right(Script.pay2pkh(pub))) assert(addressToPublicKeyScript(Block.LivenetGenesisBlock.hash, Base58Check.encode(Base58.Prefix.PubkeyAddress, pub.hash160)) == Right(Script.pay2pkh(pub))) // wrong chain - val Left(failure) = addressToPublicKeyScript(Block.TestnetGenesisBlock.hash, Base58Check.encode(Base58.Prefix.PubkeyAddress, pub.hash160)) + val Left(failure) = addressToPublicKeyScript(Block.Testnet3GenesisBlock.hash, Base58Check.encode(Base58.Prefix.PubkeyAddress, pub.hash160)) assert(failure.isInstanceOf[ChainHashMismatch]) - assert(addressToPublicKeyScript(Block.TestnetGenesisBlock.hash, Base58Check.encode(Base58.Prefix.PubkeyAddress, pub.hash160)).isLeft) + assert(addressToPublicKeyScript(Block.Testnet3GenesisBlock.hash, Base58Check.encode(Base58.Prefix.PubkeyAddress, pub.hash160)).isLeft) assert(addressToPublicKeyScript(Block.RegtestGenesisBlock.hash, Base58Check.encode(Base58.Prefix.PubkeyAddress, pub.hash160)).isLeft) assert(addressToPublicKeyScript(Block.SignetGenesisBlock.hash, Base58Check.encode(Base58.Prefix.PubkeyAddress, pub.hash160)).isLeft) @@ -67,14 +67,14 @@ class PackageSpec extends AnyFunSuite { val script = Script.write(Script.pay2wpkh(pub)) // valid chain - assert(addressToPublicKeyScript(Block.TestnetGenesisBlock.hash, Base58Check.encode(Base58.Prefix.ScriptAddressTestnet, Crypto.hash160(script))) == Right(Script.pay2sh(script))) + assert(addressToPublicKeyScript(Block.Testnet3GenesisBlock.hash, Base58Check.encode(Base58.Prefix.ScriptAddressTestnet, Crypto.hash160(script))) == Right(Script.pay2sh(script))) assert(addressToPublicKeyScript(Block.RegtestGenesisBlock.hash, Base58Check.encode(Base58.Prefix.ScriptAddressTestnet, Crypto.hash160(script))) == Right(Script.pay2sh(script))) assert(addressToPublicKeyScript(Block.SignetGenesisBlock.hash, Base58Check.encode(Base58.Prefix.ScriptAddressTestnet, Crypto.hash160(script))) == Right(Script.pay2sh(script))) assert(addressToPublicKeyScript(Block.LivenetGenesisBlock.hash, Base58Check.encode(Base58.Prefix.ScriptAddress, Crypto.hash160(script))) == Right(Script.pay2sh(script))) // wrong chain assert(addressToPublicKeyScript(Block.LivenetGenesisBlock.hash, Base58Check.encode(Base58.Prefix.ScriptAddressTestnet, Crypto.hash160(script))).isLeft) - assert(addressToPublicKeyScript(Block.TestnetGenesisBlock.hash, Base58Check.encode(Base58.Prefix.ScriptAddress, Crypto.hash160(script))).isLeft) + assert(addressToPublicKeyScript(Block.Testnet3GenesisBlock.hash, Base58Check.encode(Base58.Prefix.ScriptAddress, Crypto.hash160(script))).isLeft) assert(addressToPublicKeyScript(Block.RegtestGenesisBlock.hash, Base58Check.encode(Base58.Prefix.ScriptAddress, Crypto.hash160(script))).isLeft) assert(addressToPublicKeyScript(Block.SignetGenesisBlock.hash, Base58Check.encode(Base58.Prefix.ScriptAddress, Crypto.hash160(script))).isLeft) } @@ -85,19 +85,19 @@ class PackageSpec extends AnyFunSuite { // p2wpkh assert(addressToPublicKeyScript(Block.LivenetGenesisBlock.hash, Bech32.encodeWitnessAddress("bc", 0, pub.hash160)) == Right(Script.pay2wpkh(pub))) - assert(addressToPublicKeyScript(Block.TestnetGenesisBlock.hash, Bech32.encodeWitnessAddress("tb", 0, pub.hash160)) == Right(Script.pay2wpkh(pub))) + assert(addressToPublicKeyScript(Block.Testnet3GenesisBlock.hash, Bech32.encodeWitnessAddress("tb", 0, pub.hash160)) == Right(Script.pay2wpkh(pub))) assert(addressToPublicKeyScript(Block.RegtestGenesisBlock.hash, Bech32.encodeWitnessAddress("bcrt", 0, pub.hash160)) == Right(Script.pay2wpkh(pub))) assert(addressToPublicKeyScript(Block.SignetGenesisBlock.hash, Bech32.encodeWitnessAddress("tb", 0, pub.hash160)) == Right(Script.pay2wpkh(pub))) // wrong chain - assert(addressToPublicKeyScript(Block.TestnetGenesisBlock.hash, Bech32.encodeWitnessAddress("bc", 0, pub.hash160)).isLeft) + assert(addressToPublicKeyScript(Block.Testnet3GenesisBlock.hash, Bech32.encodeWitnessAddress("bc", 0, pub.hash160)).isLeft) assert(addressToPublicKeyScript(Block.LivenetGenesisBlock.hash, Bech32.encodeWitnessAddress("tb", 0, pub.hash160)).isLeft) assert(addressToPublicKeyScript(Block.LivenetGenesisBlock.hash, Bech32.encodeWitnessAddress("bcrt", 0, pub.hash160)).isLeft) assert(addressToPublicKeyScript(Block.LivenetGenesisBlock.hash, Bech32.encodeWitnessAddress("tb", 0, pub.hash160)).isLeft) val script = Script.write(Script.pay2wpkh(pub)) assert(addressToPublicKeyScript(Block.LivenetGenesisBlock.hash, Bech32.encodeWitnessAddress("bc", 0, Crypto.sha256(script))) == Right(Script.pay2wsh(script))) - assert(addressToPublicKeyScript(Block.TestnetGenesisBlock.hash, Bech32.encodeWitnessAddress("tb", 0, Crypto.sha256(script))) == Right(Script.pay2wsh(script))) + assert(addressToPublicKeyScript(Block.Testnet3GenesisBlock.hash, Bech32.encodeWitnessAddress("tb", 0, Crypto.sha256(script))) == Right(Script.pay2wsh(script))) assert(addressToPublicKeyScript(Block.RegtestGenesisBlock.hash, Bech32.encodeWitnessAddress("bcrt", 0, Crypto.sha256(script))) == Right(Script.pay2wsh(script))) assert(addressToPublicKeyScript(Block.SignetGenesisBlock.hash, Bech32.encodeWitnessAddress("tb", 0, Crypto.sha256(script))) == Right(Script.pay2wsh(script))) } 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 1bd052c264..acf2157cd8 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/StartupSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/StartupSpec.scala @@ -39,8 +39,8 @@ class StartupSpec extends AnyFunSuite { def makeNodeParamsWithDefaults(conf: Config): NodeParams = { val blockCount = new AtomicLong(0) val feerates = new AtomicReference(FeeratesPerKw.single(feeratePerKw)) - val nodeKeyManager = new LocalNodeKeyManager(randomBytes32(), chainHash = Block.TestnetGenesisBlock.hash) - val channelKeyManager = new LocalChannelKeyManager(randomBytes32(), chainHash = Block.TestnetGenesisBlock.hash) + val nodeKeyManager = new LocalNodeKeyManager(randomBytes32(), chainHash = Block.Testnet3GenesisBlock.hash) + val channelKeyManager = new LocalChannelKeyManager(randomBytes32(), chainHash = Block.Testnet3GenesisBlock.hash) val db = TestDatabases.inMemoryDb() NodeParams.makeNodeParams(conf, UUID.fromString("01234567-0123-4567-89ab-0123456789ab"), nodeKeyManager, channelKeyManager, None, None, db, blockCount, feerates) } 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 a3c6c04863..340e4dca15 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 @@ -46,10 +46,12 @@ class DummyOnChainWallet extends OnChainWallet with OnchainPubkeyCache { override def onChainBalance()(implicit ec: ExecutionContext): Future[OnChainBalance] = Future.successful(OnChainBalance(1105 sat, 561 sat)) - override def getReceiveAddress(label: String)(implicit ec: ExecutionContext): Future[String] = Future.successful(dummyReceiveAddress) + override def getReceiveAddress(label: String, addressType_opt: Option[AddressType] = None)(implicit ec: ExecutionContext): Future[String] = Future.successful(dummyReceiveAddress) override def getP2wpkhPubkey()(implicit ec: ExecutionContext): Future[Crypto.PublicKey] = Future.successful(dummyReceivePubkey) + override def getPubkeyScript(renew: Boolean): ByteVector = Script.write(Script.pay2wpkh(dummyReceivePubkey)) + override def fundTransaction(tx: Transaction, feeRate: FeeratePerKw, replaceable: Boolean, externalInputsWeight: Map[OutPoint, Long], feeBudget_opt: Option[Satoshi])(implicit ec: ExecutionContext): Future[FundTransactionResponse] = { funded += (tx.txid -> tx) Future.successful(FundTransactionResponse(tx, 0 sat, None)) @@ -101,10 +103,12 @@ class NoOpOnChainWallet extends OnChainWallet with OnchainPubkeyCache { override def onChainBalance()(implicit ec: ExecutionContext): Future[OnChainBalance] = Future.successful(OnChainBalance(1105 sat, 561 sat)) - override def getReceiveAddress(label: String)(implicit ec: ExecutionContext): Future[String] = Future.successful(dummyReceiveAddress) + override def getReceiveAddress(label: String, addressType_opt: Option[AddressType] = None)(implicit ec: ExecutionContext): Future[String] = Future.successful(dummyReceiveAddress) override def getP2wpkhPubkey()(implicit ec: ExecutionContext): Future[Crypto.PublicKey] = Future.successful(dummyReceivePubkey) + override def getPubkeyScript(renew: Boolean): ByteVector = Script.write(Script.pay2wpkh(dummyReceivePubkey)) + override def fundTransaction(tx: Transaction, feeRate: FeeratePerKw, replaceable: Boolean, externalInputsWeight: Map[OutPoint, Long], feeBudget_opt: Option[Satoshi])(implicit ec: ExecutionContext): Future[FundTransactionResponse] = Promise().future // will never be completed override def signPsbt(psbt: Psbt, ourInputs: Seq[Int], ourOutputs: Seq[Int])(implicit ec: ExecutionContext): Future[ProcessPsbtResponse] = Promise().future // will never be completed @@ -148,10 +152,12 @@ class SingleKeyOnChainWallet extends OnChainWallet with OnchainPubkeyCache { override def onChainBalance()(implicit ec: ExecutionContext): Future[OnChainBalance] = Future.successful(OnChainBalance(1105 sat, 561 sat)) - override def getReceiveAddress(label: String)(implicit ec: ExecutionContext): Future[String] = Future.successful(Bech32.encodeWitnessAddress("bcrt", 0, pubkey.hash160.toArray)) + override def getReceiveAddress(label: String, addressType_opt: Option[AddressType] = None)(implicit ec: ExecutionContext): Future[String] = Future.successful(Bech32.encodeWitnessAddress("bcrt", 0, pubkey.hash160.toArray)) override def getP2wpkhPubkey()(implicit ec: ExecutionContext): Future[Crypto.PublicKey] = Future.successful(pubkey) + override def getPubkeyScript(renew: Boolean): ByteVector = Script.write(Script.pay2wpkh(pubkey)) + override def fundTransaction(tx: Transaction, feeRate: FeeratePerKw, replaceable: Boolean, externalInputsWeight: Map[OutPoint, Long], feeBudget_opt: Option[Satoshi])(implicit ec: ExecutionContext): Future[FundTransactionResponse] = synchronized { val currentAmountIn = tx.txIn.flatMap(txIn => inputs.find(_.txid == txIn.outPoint.txid).flatMap(_.txOut.lift(txIn.outPoint.index.toInt))).map(_.amount).sum val amountOut = tx.txOut.map(_.amount).sum @@ -204,7 +210,15 @@ class SingleKeyOnChainWallet extends OnChainWallet with OnchainPubkeyCache { case (currentPsbt, (txIn, index)) => inputs.find(_.txid == txIn.outPoint.txid) match { case Some(inputTx) => val sig = Transaction.signInput(tx, index, Script.pay2pkh(pubkey), SigHash.SIGHASH_ALL, inputTx.txOut.head.amount, SigVersion.SIGVERSION_WITNESS_V0, privkey) - val updated = currentPsbt.updateWitnessInput(txIn.outPoint, inputTx.txOut(txIn.outPoint.index.toInt), null, Script.pay2pkh(pubkey).map(scala2kmp).asJava, null, java.util.Map.of()).getRight + val updated = currentPsbt.updateWitnessInput( + txIn.outPoint, + inputTx.txOut(txIn.outPoint.index.toInt), + null, + Script.pay2pkh(pubkey).map(scala2kmp).asJava, + null, java.util.Map.of(), + null, + null, + java.util.Map.of()).getRight updated.finalizeWitnessInput(txIn.outPoint, Script.witnessPay2wpkh(pubkey, sig)).getRight case None => currentPsbt } 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 fd091a0803..2eb6a85509 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 @@ -25,6 +25,7 @@ import fr.acinq.bitcoin.scalacompat.Crypto.{PublicKey, der2compact} import fr.acinq.bitcoin.scalacompat.{Block, Btc, BtcDouble, Crypto, DeterministicWallet, MilliBtcDouble, MnemonicCode, OP_DROP, OP_PUSHDATA, OutPoint, Satoshi, SatoshiLong, Script, ScriptWitness, Transaction, TxId, TxIn, TxOut, addressFromPublicKeyScript, addressToPublicKeyScript, computeBIP84Address, computeP2PkhAddress, computeP2WpkhAddress} import fr.acinq.bitcoin.{Bech32, SigHash, SigVersion} import fr.acinq.eclair.TestUtils.randomTxId +import fr.acinq.eclair.blockchain.AddressType import fr.acinq.eclair.blockchain.OnChainWallet.{FundTransactionResponse, MakeFundingTxResponse, OnChainBalance, ProcessPsbtResponse} import fr.acinq.eclair.blockchain.WatcherSpec.{createSpendManyP2WPKH, createSpendP2WPKH} import fr.acinq.eclair.blockchain.bitcoind.BitcoindService.{BitcoinReq, SignTransactionResponse} @@ -52,7 +53,7 @@ class BitcoinCoreClientSpec extends TestKitBaseClass with BitcoindService with A implicit val formats: Formats = DefaultFormats override def beforeAll(): Unit = { - startBitcoind(defaultAddressType_opt = Some("bech32"), mempoolSize_opt = Some(5 /* MB */), mempoolMinFeerate_opt = Some(FeeratePerByte(2 sat))) + startBitcoind(defaultAddressType_opt = Some("bech32"), defaultChangeType_opt = Some("bech32"), mempoolSize_opt = Some(5 /* MB */), mempoolMinFeerate_opt = Some(FeeratePerByte(2 sat))) waitForBitcoindReady() } @@ -254,7 +255,7 @@ class BitcoinCoreClientSpec extends TestKitBaseClass with BitcoindService with A // We sign our external input. val externalSig = Transaction.signInput(fundedTx2.tx, 0, inputScript1, SigHash.SIGHASH_ALL, 250_000 sat, SigVersion.SIGVERSION_WITNESS_V0, alicePriv) val psbt = new Psbt(fundedTx2.tx) - .updateWitnessInput(outpoint1, txOut1, null, null, null, java.util.Map.of()).getRight + .updateWitnessInput(outpoint1, txOut1, null, null, null, java.util.Map.of(), null, null, java.util.Map.of()).getRight .finalizeWitnessInput(0, Script.witnessMultiSigMofN(Seq(alicePriv, bobPriv).map(_.publicKey), Seq(externalSig))).getRight // And let bitcoind sign the wallet input. walletExternalFunds.signPsbt(psbt, fundedTx2.tx.txIn.indices, Nil).pipeTo(sender.ref) @@ -284,7 +285,7 @@ class BitcoinCoreClientSpec extends TestKitBaseClass with BitcoindService with A // We sign our external input. val externalSig = Transaction.signInput(fundedTx.tx, 0, inputScript2, SigHash.SIGHASH_ALL, 300_000 sat, SigVersion.SIGVERSION_WITNESS_V0, alicePriv) val psbt = new Psbt(fundedTx.tx) - .updateWitnessInput(externalOutpoint, tx2.txOut(0), null, null, null, java.util.Map.of()).getRight + .updateWitnessInput(externalOutpoint, tx2.txOut(0), null, null, null, java.util.Map.of(), null, null, java.util.Map.of()).getRight .finalizeWitnessInput(0, Script.witnessMultiSigMofN(Seq(alicePriv, carolPriv).map(_.publicKey), Seq(externalSig))).getRight // bitcoind signs the wallet input. walletExternalFunds.signPsbt(psbt, fundedTx.tx.txIn.indices, Nil).pipeTo(sender.ref) @@ -320,7 +321,7 @@ class BitcoinCoreClientSpec extends TestKitBaseClass with BitcoindService with A // We sign our external input. val externalSig = Transaction.signInput(fundedTx.tx, 0, inputScript2, SigHash.SIGHASH_ALL, 300_000 sat, SigVersion.SIGVERSION_WITNESS_V0, alicePriv) val psbt = new Psbt(fundedTx.tx) - .updateWitnessInput(OutPoint(tx2, 0), tx2.txOut(0), null, null, null, java.util.Map.of()).getRight + .updateWitnessInput(OutPoint(tx2, 0), tx2.txOut(0), null, null, null, java.util.Map.of(), null, null, java.util.Map.of()).getRight .finalizeWitnessInput(0, Script.witnessMultiSigMofN(Seq(alicePriv, carolPriv).map(_.publicKey), Seq(externalSig))).getRight // bitcoind signs the wallet input. walletExternalFunds.signPsbt(psbt, fundedTx.tx.txIn.indices, Nil).pipeTo(sender.ref) @@ -816,7 +817,7 @@ class BitcoinCoreClientSpec extends TestKitBaseClass with BitcoindService with A val nonWalletWitness = ScriptWitness(Seq(nonWalletSig, nonWalletKey.publicKey.value)) val txWithSignedNonWalletInput = txWithNonWalletInput.updateWitness(0, nonWalletWitness) val psbt = new Psbt(txWithSignedNonWalletInput) - val updated: Either[UpdateFailure, Psbt] = psbt.updateWitnessInput(psbt.global.tx.txIn.get(0).outPoint, txToRemote.txOut(0), null, fr.acinq.bitcoin.Script.pay2pkh(nonWalletKey.publicKey), SigHash.SIGHASH_ALL, psbt.getInput(0).getDerivationPaths) + val updated: Either[UpdateFailure, Psbt] = psbt.updateWitnessInput(psbt.global.tx.txIn.get(0).outPoint, txToRemote.txOut(0), null, fr.acinq.bitcoin.Script.pay2pkh(nonWalletKey.publicKey), SigHash.SIGHASH_ALL, psbt.getInput(0).getDerivationPaths, null, null, java.util.Map.of()) val Right(psbt1) = updated.flatMap(_.finalizeWitnessInput(0, nonWalletWitness)) bitcoinClient.signPsbt(psbt1, txWithSignedNonWalletInput.txIn.indices.tail, Nil).pipeTo(sender.ref) val signTxResponse2 = sender.expectMsgType[ProcessPsbtResponse] @@ -848,7 +849,7 @@ class BitcoinCoreClientSpec extends TestKitBaseClass with BitcoindService with A val nonWalletWitness = ScriptWitness(Seq(nonWalletSig, nonWalletKey.publicKey.value)) val txWithSignedUnconfirmedInput = txWithUnconfirmedInput.updateWitness(0, nonWalletWitness) val psbt = new Psbt(txWithSignedUnconfirmedInput) - val Right(psbt1) = psbt.updateWitnessInput(psbt.global.tx.txIn.get(0).outPoint, unconfirmedTx.txOut(0), null, fr.acinq.bitcoin.Script.pay2pkh(nonWalletKey.publicKey), SigHash.SIGHASH_ALL, psbt.getInput(0).getDerivationPaths) + val Right(psbt1) = psbt.updateWitnessInput(psbt.global.tx.txIn.get(0).outPoint, unconfirmedTx.txOut(0), null, fr.acinq.bitcoin.Script.pay2pkh(nonWalletKey.publicKey), SigHash.SIGHASH_ALL, psbt.getInput(0).getDerivationPaths, null, null, java.util.Map.of()) .flatMap(_.finalizeWitnessInput(0, nonWalletWitness)) bitcoinClient.signPsbt(psbt1, txWithSignedUnconfirmedInput.txIn.indices.tail, Nil).pipeTo(sender.ref) assert(sender.expectMsgType[ProcessPsbtResponse].complete) @@ -1521,7 +1522,7 @@ class BitcoinCoreClientSpec extends TestKitBaseClass with BitcoindService with A val txOut = Seq(TxOut(outputAmount, Script.pay2wpkh(randomKey().publicKey))) var psbt = new Psbt(Transaction(2, txIn, txOut, 0)) (fromInput until toInput).foreach { i => - psbt = psbt.updateWitnessInput(OutPoint(parentTx, i), parentTx.txOut(i), null, null, null, psbt.getInput(i - fromInput).getDerivationPaths).getRight + psbt = psbt.updateWitnessInput(OutPoint(parentTx, i), parentTx.txOut(i), null, null, null, psbt.getInput(i - fromInput).getDerivationPaths, null, null, java.util.Map.of()).getRight psbt = psbt.finalizeWitnessInput(i - fromInput, ScriptWitness(Seq(ByteVector(1), bigInputScript))).getRight } val signedTx: Transaction = psbt.extract().getRight @@ -1732,7 +1733,7 @@ class BitcoinCoreClientWithEclairSignerSpec extends BitcoinCoreClientSpec { val accountXPub = DeterministicWallet.encode( DeterministicWallet.publicKey(DeterministicWallet.derivePrivateKey(master, DeterministicWallet.KeyPath("m/84'/1'/0'"))), DeterministicWallet.vpub) - assert(wallet.onChainKeyManager_opt.get.masterPubKey(0) == accountXPub) + assert(wallet.onChainKeyManager_opt.get.masterPubKey(0, AddressType.Bech32) == accountXPub) def getBip32Path(address: String): DeterministicWallet.KeyPath = { wallet.rpcClient.invoke("getaddressinfo", address).pipeTo(sender.ref) @@ -1755,13 +1756,50 @@ class BitcoinCoreClientWithEclairSignerSpec extends BitcoinCoreClientSpec { } } + test("wallets managed by eclair implement BIP86") { + import fr.acinq.bitcoin.scalacompat.KotlinUtils._ + val sender = TestProbe() + val entropy = randomBytes32() + val seed = MnemonicCode.toSeed(MnemonicCode.toMnemonics(entropy), "") + val master = DeterministicWallet.generate(seed) + val (wallet, keyManager) = createWallet(seed) + createEclairBackedWallet(wallet.rpcClient, keyManager) + + // this account xpub can be used to create a watch-only wallet + val accountXPub = DeterministicWallet.encode( + DeterministicWallet.publicKey(DeterministicWallet.derivePrivateKey(master, DeterministicWallet.KeyPath("m/86'/1'/0'"))), + DeterministicWallet.tpub) + assert(wallet.onChainKeyManager_opt.get.masterPubKey(0, AddressType.Bech32m) == accountXPub) + + def getBip32Path(address: String): DeterministicWallet.KeyPath = { + wallet.rpcClient.invoke("getaddressinfo", address).pipeTo(sender.ref) + val JString(bip32path) = sender.expectMsgType[JValue] \ "hdkeypath" + DeterministicWallet.KeyPath(bip32path) + } + + (0 to 10).foreach { _ => + wallet.getReceiveAddress(addressType_opt = Some(AddressType.Bech32m)).pipeTo(sender.ref) + val address = sender.expectMsgType[String] + val bip32path = getBip32Path(address) + assert(bip32path.path.length == 5 && bip32path.toString().startsWith("m/86'/1'/0'/0")) + assert(fr.acinq.bitcoin.Bitcoin.computeBIP86Address(DeterministicWallet.derivePrivateKey(master, bip32path).publicKey, Block.RegtestGenesisBlock.hash) == address) + + wallet.getP2wpkhPubkeyHashForChange().pipeTo(sender.ref) + val Right(changeAddress) = addressFromPublicKeyScript(Block.RegtestGenesisBlock.hash, Script.pay2wpkh(sender.expectMsgType[ByteVector])) + val bip32ChangePath = getBip32Path(changeAddress) + assert(bip32ChangePath.path.length == 5 && bip32ChangePath.toString().startsWith("m/84'/1'/0'/1")) + assert(computeBIP84Address(DeterministicWallet.derivePrivateKey(master, bip32ChangePath).publicKey, Block.RegtestGenesisBlock.hash) == changeAddress) + } + } + test("use eclair to manage on-chain keys") { val sender = TestProbe() - (1 to 10).foreach { _ => + (1 to 10).foreach { i => val (wallet, keyManager) = createWallet(randomBytes32()) createEclairBackedWallet(wallet.rpcClient, keyManager) - wallet.getReceiveAddress().pipeTo(sender.ref) + val addressType = if (i % 2 == 0) AddressType.Bech32 else AddressType.Bech32m + wallet.getReceiveAddress(addressType_opt = Some(addressType)).pipeTo(sender.ref) val address = sender.expectMsgType[String] // we can send to an on-chain address if eclair signs the transactions 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 0320822268..f8d9085523 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 @@ -90,6 +90,7 @@ trait BitcoindService extends Logging { def startBitcoind(useCookie: Boolean = false, defaultAddressType_opt: Option[String] = None, + defaultChangeType_opt: Option[String] = None, mempoolSize_opt: Option[Int] = None, // mempool size in MB mempoolMinFeerate_opt: Option[FeeratePerByte] = None, // transactions below this feerate won't be accepted in the mempool startupFlags: String = ""): Unit = { @@ -103,7 +104,7 @@ trait BitcoindService extends Logging { .replace("28334", bitcoindZmqBlockPort.toString) .replace("28335", bitcoindZmqTxPort.toString) .appendedAll(defaultAddressType_opt.map(addressType => s"addresstype=$addressType\n").getOrElse("")) - .appendedAll(defaultAddressType_opt.map(addressType => s"changetype=$addressType\n").getOrElse("")) + .appendedAll(defaultChangeType_opt.map(addressType => s"changetype=$addressType\n").getOrElse("")) .appendedAll(mempoolSize_opt.map(mempoolSize => s"maxmempool=$mempoolSize\n").getOrElse("")) .appendedAll(mempoolMinFeerate_opt.map(mempoolMinFeerate => s"minrelaytxfee=${FeeratePerKB(mempoolMinFeerate).feerate.toBtc.toBigDecimal}\n").getOrElse("")) if (useCookie) { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/OnchainPubkeyRefresherSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/OnchainPubkeyRefresherSpec.scala index 92218a109e..f0bb0534a9 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/OnchainPubkeyRefresherSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/OnchainPubkeyRefresherSpec.scala @@ -2,10 +2,11 @@ package fr.acinq.eclair.blockchain.bitcoind import akka.actor.typed.scaladsl.adapter.ClassicActorSystemOps import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey -import fr.acinq.bitcoin.scalacompat.{Block, Crypto, computeBIP84Address} -import fr.acinq.eclair.blockchain.OnChainAddressGenerator +import fr.acinq.bitcoin.scalacompat.{Block, Crypto, Script, computeBIP84Address} +import fr.acinq.eclair.blockchain.{AddressType, OnChainAddressGenerator} import fr.acinq.eclair.{TestKitBaseClass, randomKey} import org.scalatest.funsuite.AnyFunSuiteLike +import scodec.bits.ByteVector import java.util.concurrent.atomic.AtomicReference import scala.concurrent.duration.DurationInt @@ -13,17 +14,29 @@ import scala.concurrent.{ExecutionContext, Future} class OnchainPubkeyRefresherSpec extends TestKitBaseClass with AnyFunSuiteLike { test("renew onchain scripts") { + import fr.acinq.bitcoin.scalacompat.KotlinUtils._ val finalPubkey = new AtomicReference[PublicKey](randomKey().publicKey) + val finalPubkeyScript = new AtomicReference[ByteVector](Script.write(Script.pay2wpkh(randomKey().publicKey))) val generator = new OnChainAddressGenerator { - override def getReceiveAddress(label: String)(implicit ec: ExecutionContext): Future[String] = Future.successful(computeBIP84Address(randomKey().publicKey, Block.RegtestGenesisBlock.hash)) + override def getReceiveAddress(label: String, addressType_opt: Option[AddressType] = None)(implicit ec: ExecutionContext): Future[String] = Future.successful( + addressType_opt match { + case Some(AddressType.Bech32m) => fr.acinq.bitcoin.Bitcoin.computeBIP86Address(randomKey().publicKey, Block.RegtestGenesisBlock.hash) + case _ => computeBIP84Address(randomKey().publicKey, Block.RegtestGenesisBlock.hash) + } + ) override def getP2wpkhPubkey()(implicit ec: ExecutionContext): Future[Crypto.PublicKey] = Future.successful(randomKey().publicKey) } - val manager = system.spawnAnonymous(OnchainPubkeyRefresher(generator, finalPubkey, 3 seconds)) + val manager = system.spawnAnonymous(OnchainPubkeyRefresher(Block.RegtestGenesisBlock.hash, generator, finalPubkey, finalPubkeyScript, 3 seconds)) - // renew script explicitly + // renew pubkey explicitly val currentPubkey = finalPubkey.get() - manager ! OnchainPubkeyRefresher.Renew + manager ! OnchainPubkeyRefresher.RenewPubkey awaitCond(finalPubkey.get() != currentPubkey) + + // renew pubkey script explicitly + val currentPubkeyScript = finalPubkeyScript.get() + manager ! OnchainPubkeyRefresher.RenewPubkeyScript + awaitCond(finalPubkeyScript.get() != currentPubkeyScript) } } 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 cd21387563..6c1615ca65 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 @@ -34,10 +34,10 @@ class BlockchainWatchdogSpec extends ScalaTestWithActorTestKit(ConfigFactory.loa // blockcypher.com is very flaky - it either imposes rate limits or requires captcha // but sometimes it works. If want to check whether you're lucky uncomment these lines: // val nodeParamsLivenet = TestConstants.Alice.nodeParams.copy(chainHash = Block.LivenetGenesisBlock.hash) - // val nodeParamsTestnet = TestConstants.Alice.nodeParams.copy(chainHash = Block.TestnetGenesisBlock.hash) + // val nodeParamsTestnet = TestConstants.Alice.nodeParams.copy(chainHash = Block.Testnet3GenesisBlock.hash) // and comment these: val nodeParamsLivenet = removeBlockcypher(TestConstants.Alice.nodeParams.copy(chainHash = Block.LivenetGenesisBlock.hash)) - val nodeParamsTestnet = removeBlockcypher(TestConstants.Alice.nodeParams.copy(chainHash = Block.TestnetGenesisBlock.hash)) + val nodeParamsTestnet = removeBlockcypher(TestConstants.Alice.nodeParams.copy(chainHash = Block.Testnet3GenesisBlock.hash)) val nodeParamsSignet = removeBlockcypher(TestConstants.Alice.nodeParams.copy(chainHash = Block.SignetGenesisBlock.hash)) 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 27cd7f45df..69d4f22ffa 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 @@ -57,7 +57,7 @@ class HeadersOverDnsSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a } test("ignore testnet requests", TestTags.ExternalApi) { - val headersOverDns = testKit.spawn(HeadersOverDns(Block.TestnetGenesisBlock.hash, BlockHeight(500000))) + val headersOverDns = testKit.spawn(HeadersOverDns(Block.Testnet3GenesisBlock.hash, BlockHeight(500000))) val sender = testKit.createTestProbe[LatestHeaders]() headersOverDns ! CheckLatestHeaders(sender.ref) sender.expectNoMessage(1 second) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/InteractiveTxBuilderSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/InteractiveTxBuilderSpec.scala index 4a146d0094..c961dca7ab 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/InteractiveTxBuilderSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/InteractiveTxBuilderSpec.scala @@ -49,8 +49,11 @@ import scala.reflect.ClassTag class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike with BitcoindService with BeforeAndAfterAll { + val defaultAddressType_opt: Option[String] = None + val defaultChangeType_opt: Option[String] = None + override def beforeAll(): Unit = { - startBitcoind() + startBitcoind(defaultAddressType_opt = defaultAddressType_opt, defaultChangeType_opt = defaultChangeType_opt) waitForBitcoindReady() } @@ -1191,7 +1194,7 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit } test("fund transaction with previous inputs (with new inputs)") { - val targetFeerate = FeeratePerKw(10_000 sat) + val targetFeerate = FeeratePerKw(11_000 sat) val fundingA = 100_000 sat val utxosA = Seq(55_000 sat, 55_000 sat, 55_000 sat) withFixture(fundingA, utxosA, 0 sat, Nil, targetFeerate, 660 sat, 0, RequireConfirmedInputs(forLocal = false, forRemote = false)) { f => @@ -1304,7 +1307,7 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit val successA1 = alice2bob.expectMsgType[Succeeded] val successB1 = bob2alice.expectMsgType[Succeeded] val (txA1, commitmentA1, txB1, commitmentB1) = fixtureParams.exchangeSigsBobFirst(bobParams, successA1, successB1) - assert(initialFeerate * 0.9 <= txA1.feerate && txA1.feerate <= initialFeerate * 1.25) + assert(initialFeerate * 0.9 <= txA1.feerate && txA1.feerate <= initialFeerate * 1.3) val probe = TestProbe() walletA.publishTransaction(txA1.signedTx).pipeTo(probe.ref) probe.expectMsg(txA1.txId) @@ -2595,4 +2598,12 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit class InteractiveTxBuilderWithEclairSignerSpec extends InteractiveTxBuilderSpec { override def useEclairSigner = true +} + +class InteractiveTxBuilderWithEclairSignerBech32mSpec extends InteractiveTxBuilderSpec { + override def useEclairSigner = true + + override val defaultAddressType_opt: Option[String] = Some("bech32m") + + override val defaultChangeType_opt: Option[String] = Some("bech32m") } \ No newline at end of file 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 580efbd02b..01ad78a00f 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 @@ -22,7 +22,7 @@ import akka.pattern.pipe import akka.testkit.{TestFSMRef, TestProbe} import com.softwaremill.quicklens.ModifyPimp import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey -import fr.acinq.bitcoin.scalacompat.{Block, BtcAmount, MilliBtcDouble, MnemonicCode, OutPoint, SatoshiLong, Transaction, TxId} +import fr.acinq.bitcoin.scalacompat.{Block, BtcAmount, MilliBtcDouble, MnemonicCode, OutPoint, SatoshiLong, Script, Transaction, TxId, addressToPublicKeyScript} import fr.acinq.eclair.NotificationsLogger.NotifyNodeOperator import fr.acinq.eclair.blockchain.bitcoind.BitcoindService import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._ @@ -128,8 +128,16 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w getP2wpkhPubkey().pipeTo(probe.ref) probe.expectMsgType[PublicKey] } + val pubkeyScript = { + getReceiveAddress().pipeTo(probe.ref) + val address = probe.expectMsgType[String] + val Right(script) = addressToPublicKeyScript(Block.RegtestGenesisBlock.hash, address) + Script.write(script) + } override def getP2wpkhPubkey(renew: Boolean): PublicKey = pubkey + + override def getPubkeyScript(renew: Boolean): ByteVector = pubkeyScript } (walletRpcClient, walletClient) @@ -1809,8 +1817,18 @@ class ReplaceableTxPublisherWithEclairSignerSpec extends ReplaceableTxPublisherS probe.expectMsgType[PublicKey] } + lazy val pubkeyScript = { + getReceiveAddress().pipeTo(probe.ref) + val address = probe.expectMsgType[String] + val Right(script) = addressToPublicKeyScript(Block.RegtestGenesisBlock.hash, address) + Script.write(script) + } + override def getP2wpkhPubkey(renew: Boolean): PublicKey = pubkey + + override def getPubkeyScript(renew: Boolean): ByteVector = pubkeyScript } + createEclairBackedWallet(walletRpcClient, keyManager) (walletRpcClient, walletClient) 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 3aebdf2783..c0375b7990 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 @@ -34,8 +34,8 @@ class LocalChannelKeyManagerSpec extends AnyFunSuite { test("generate the same secrets from the same seed") { // data was generated with eclair 0.3 val seed = hex"17b086b228025fa8f4416324b6ba2ec36e68570ae2fc3d392520969f2a9d0c1501" - val nodeKeyManager = new LocalNodeKeyManager(seed, Block.TestnetGenesisBlock.hash) - val channelKeyManager = new LocalChannelKeyManager(seed, Block.TestnetGenesisBlock.hash) + val nodeKeyManager = new LocalNodeKeyManager(seed, Block.Testnet3GenesisBlock.hash) + val channelKeyManager = new LocalChannelKeyManager(seed, Block.Testnet3GenesisBlock.hash) assert(nodeKeyManager.nodeId == PublicKey(hex"02a051267759c3a149e3e72372f4e0c4054ba597ebfd0eda78a2273023667205ee")) val keyPath = KeyPath("m/1'/2'/3'/4'") assert(channelKeyManager.commitmentSecret(keyPath, 0L).value == ByteVector32.fromValidHex("fa7a8c2fc62642f7a9a19ea0bfad14d39a430f3c9899c185dcecc61c8077891e")) @@ -64,7 +64,7 @@ class LocalChannelKeyManagerSpec extends AnyFunSuite { test("test vectors (testnet, funder)") { val seed = ByteVector.fromValidHex("17b086b228025fa8f4416324b6ba2ec36e68570ae2fc3d392520969f2a9d0c1501") - val channelKeyManager = new LocalChannelKeyManager(seed, Block.TestnetGenesisBlock.hash) + val channelKeyManager = new LocalChannelKeyManager(seed, Block.Testnet3GenesisBlock.hash) val fundingKeyPath = makefundingKeyPath(hex"be4fa97c62b9f88437a3be577b31eb48f2165c7bc252194a15ff92d995778cfb", isInitiator = true) val fundingPub = channelKeyManager.fundingPublicKey(fundingKeyPath, fundingTxIndex = 0) @@ -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 channelKeyManager = new LocalChannelKeyManager(seed, Block.Testnet3GenesisBlock.hash) val fundingKeyPath = makefundingKeyPath(hex"06535806c1aa73971ec4877a5e2e684fa636136c073810f190b63eefc58ca488", isInitiator = false) val fundingPub = channelKeyManager.fundingPublicKey(fundingKeyPath, fundingTxIndex = 0) 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 d6384f4a6e..c2aa787ac7 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 @@ -33,15 +33,15 @@ class LocalNodeKeyManagerSpec extends AnyFunSuite { // if this test breaks it means that we will generate a different node id from // the same seed, which could be a problem during an upgrade val seed = hex"17b086b228025fa8f4416324b6ba2ec36e68570ae2fc3d392520969f2a9d0c1501" - val nodeKeyManager = new LocalNodeKeyManager(seed, Block.TestnetGenesisBlock.hash) + val nodeKeyManager = new LocalNodeKeyManager(seed, Block.Testnet3GenesisBlock.hash) assert(nodeKeyManager.nodeId == PublicKey(hex"02a051267759c3a149e3e72372f4e0c4054ba597ebfd0eda78a2273023667205ee")) } test("generate different node ids from the same seed on different chains") { val seed = hex"17b086b228025fa8f4416324b6ba2ec36e68570ae2fc3d392520969f2a9d0c1501" - val nodeKeyManager1 = new LocalNodeKeyManager(seed, Block.TestnetGenesisBlock.hash) + val nodeKeyManager1 = new LocalNodeKeyManager(seed, Block.Testnet3GenesisBlock.hash) val nodeKeyManager2 = new LocalNodeKeyManager(seed, Block.LivenetGenesisBlock.hash) - val channelKeyManager1 = new LocalChannelKeyManager(seed, Block.TestnetGenesisBlock.hash) + val channelKeyManager1 = new LocalChannelKeyManager(seed, Block.Testnet3GenesisBlock.hash) val channelKeyManager2 = new LocalChannelKeyManager(seed, Block.LivenetGenesisBlock.hash) assert(nodeKeyManager1.nodeId != nodeKeyManager2.nodeId) val keyPath = KeyPath(1L :: Nil) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/keymanager/LocalOnChainKeyManagerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/keymanager/LocalOnChainKeyManagerSpec.scala index 9a4d4303f6..1902ea59c8 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/keymanager/LocalOnChainKeyManagerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/keymanager/LocalOnChainKeyManagerSpec.scala @@ -1,9 +1,10 @@ package fr.acinq.eclair.crypto.keymanager -import fr.acinq.bitcoin.psbt.{KeyPathWithMaster, Psbt} +import fr.acinq.bitcoin.psbt.{KeyPathWithMaster, Psbt, TaprootBip32DerivationPath} import fr.acinq.bitcoin.scalacompat.{Block, DeterministicWallet, MnemonicCode, OutPoint, Satoshi, Script, Transaction, TxIn, TxOut} -import fr.acinq.bitcoin.{ScriptFlags, SigHash} +import fr.acinq.bitcoin.{KeyPath, ScriptFlags, SigHash} import fr.acinq.eclair.TimestampSecond +import fr.acinq.eclair.blockchain.AddressType import org.scalatest.funsuite.AnyFunSuite import scodec.bits.ByteVector @@ -15,7 +16,7 @@ class LocalOnChainKeyManagerSpec extends AnyFunSuite { test("sign psbt (non-reg test)") { val entropy = ByteVector.fromValidHex("01" * 32) val seed = MnemonicCode.toSeed(MnemonicCode.toMnemonics(entropy), "") - val onChainKeyManager = new LocalOnChainKeyManager("eclair", seed, TimestampSecond.now(), Block.TestnetGenesisBlock.hash) + val onChainKeyManager = new LocalOnChainKeyManager("eclair", seed, TimestampSecond.now(), Block.Testnet3GenesisBlock.hash) // data generated by bitcoin core on regtest val psbt = Psbt.read( Base64.getDecoder.decode("cHNidP8BAHECAAAAAfZo4nGIyTg77MFmEBkQH1Au3Jl8vzB2WWQGGz/MbyssAAAAAAD9////ArAHPgUAAAAAFgAU6j9yVvLg66Zu3GM/xHbmXT0yvyiAlpgAAAAAABYAFODscQh3N7lmDYyV5yrHpGL2Zd4JAAAAAAABAH0CAAAAAaNdmqUNlziIjSaif3JUcvJWdyF0U5bYq13NMe+LbaBZAAAAAAD9////AjSp1gUAAAAAFgAUjfFMfBg8ulo/874n3+0ode7ka0BAQg8AAAAAACIAIPUn/XU17DfnvDkj8gn2twG3jtr2Z7sthy9K2MPTdYkaAAAAAAEBHzSp1gUAAAAAFgAUjfFMfBg8ulo/874n3+0ode7ka0AiBgM+PDdyxsVisa66SyBxiUvhEam8lEP64yujvVsEcGaqIxgPCfOBVAAAgAEAAIAAAACAAQAAAAMAAAAAIgIDWmAhb/sCV9+HjwFpPuy2TyEBi/Y11wrEHZUihe3N80EYDwnzgVQAAIABAACAAAAAgAEAAAAFAAAAAAA=") @@ -26,14 +27,14 @@ class LocalOnChainKeyManagerSpec extends AnyFunSuite { assert(tx.isRight) } - test("sign psbt") { + test("sign psbt (BIP84") { import fr.acinq.bitcoin.scalacompat.KotlinUtils._ val seed = ByteVector.fromValidHex("01" * 32) - val onChainKeyManager = new LocalOnChainKeyManager("eclair", seed, TimestampSecond.now(), Block.TestnetGenesisBlock.hash) + val onChainKeyManager = new LocalOnChainKeyManager("eclair", seed, TimestampSecond.now(), Block.Testnet3GenesisBlock.hash) // create a watch-only BIP84 wallet from our key manager xpub - val (_, accountPub) = DeterministicWallet.ExtendedPublicKey.decode(onChainKeyManager.masterPubKey(0)) + val (_, accountPub) = DeterministicWallet.ExtendedPublicKey.decode(onChainKeyManager.masterPubKey(0, AddressType.Bech32)) val mainPub = DeterministicWallet.derivePublicKey(accountPub, 0) def getPublicKey(index: Long) = DeterministicWallet.derivePublicKey(mainPub, index).publicKey @@ -54,13 +55,13 @@ class LocalOnChainKeyManagerSpec extends AnyFunSuite { txOut = TxOut(Satoshi(1000_000), Script.pay2wpkh(getPublicKey(0))) :: Nil, lockTime = 0) val Right(psbt) = for { - p0 <- new Psbt(tx).updateWitnessInput(OutPoint(utxos(0), 0), utxos(0).txOut(0), null, Script.pay2pkh(getPublicKey(0)).map(scala2kmp).asJava, null, java.util.Map.of(getPublicKey(0), bip32paths(0))) - p1 <- p0.updateWitnessInput(OutPoint(utxos(1), 0), utxos(1).txOut(0), null, Script.pay2pkh(getPublicKey(1)).map(scala2kmp).asJava, null, java.util.Map.of(getPublicKey(1), bip32paths(1))) - p2 <- p1.updateWitnessInput(OutPoint(utxos(2), 0), utxos(2).txOut(0), null, Script.pay2pkh(getPublicKey(2)).map(scala2kmp).asJava, null, java.util.Map.of(getPublicKey(2), bip32paths(2))) + p0 <- new Psbt(tx).updateWitnessInput(OutPoint(utxos(0), 0), utxos(0).txOut(0), null, Script.pay2pkh(getPublicKey(0)).map(scala2kmp).asJava, null, java.util.Map.of(getPublicKey(0), bip32paths(0)), null, null, java.util.Map.of()) + p1 <- p0.updateWitnessInput(OutPoint(utxos(1), 0), utxos(1).txOut(0), null, Script.pay2pkh(getPublicKey(1)).map(scala2kmp).asJava, null, java.util.Map.of(getPublicKey(1), bip32paths(1)), null, null, java.util.Map.of()) + p2 <- p1.updateWitnessInput(OutPoint(utxos(2), 0), utxos(2).txOut(0), null, Script.pay2pkh(getPublicKey(2)).map(scala2kmp).asJava, null, java.util.Map.of(getPublicKey(2), bip32paths(2)), null, null, java.util.Map.of()) p3 <- p2.updateNonWitnessInput(utxos(0), 0, null, null, java.util.Map.of()) p4 <- p3.updateNonWitnessInput(utxos(1), 0, null, null, java.util.Map.of()) p5 <- p4.updateNonWitnessInput(utxos(2), 0, null, null, java.util.Map.of()) - p6 <- p5.updateWitnessOutput(0, null, null, java.util.Map.of(getPublicKey(0), bip32paths(0))) + p6 <- p5.updateWitnessOutput(0, null, null, java.util.Map.of(getPublicKey(0), bip32paths(0)), null, java.util.Map.of()) } yield p6 { @@ -78,40 +79,108 @@ class LocalOnChainKeyManagerSpec extends AnyFunSuite { } { // provide a wrong derivation path for the first input - val updated = psbt.updateWitnessInput(OutPoint(utxos(0), 0), utxos(0).txOut(0), null, Script.pay2pkh(getPublicKey(0)).map(scala2kmp).asJava, null, java.util.Map.of(getPublicKey(0), bip32paths(2))).getRight // wrong bip32 path + val updated = psbt.updateWitnessInput(OutPoint(utxos(0), 0), utxos(0).txOut(0), null, Script.pay2pkh(getPublicKey(0)).map(scala2kmp).asJava, null, java.util.Map.of(getPublicKey(0), bip32paths(2)), null, null, java.util.Map.of()).getRight // wrong bip32 path val Failure(error) = onChainKeyManager.sign(updated, Seq(0, 1, 2), Seq(0)) assert(error.getMessage.contains("derived public key doesn't match")) } { // provide a wrong derivation path for the first output - val updated = psbt.updateWitnessOutput(0, null, null, java.util.Map.of(getPublicKey(0), bip32paths(1))).getRight // wrong path + val updated = psbt.updateWitnessOutput(0, null, null, java.util.Map.of(getPublicKey(0), bip32paths(1)), null, java.util.Map.of()).getRight // wrong path val Failure(error) = onChainKeyManager.sign(updated, Seq(0, 1, 2), Seq(0)) assert(error.getMessage.contains("could not verify output 0")) } { // lie about the amount being spent - val updated = psbt.updateWitnessInput(OutPoint(utxos(0), 0), utxos(0).txOut(0).copy(amount = Satoshi(10)), null, Script.pay2pkh(getPublicKey(0)).map(scala2kmp).asJava, null, java.util.Map.of(getPublicKey(0), bip32paths(0))).getRight + val updated = psbt.updateWitnessInput(OutPoint(utxos(0), 0), utxos(0).txOut(0).copy(amount = Satoshi(10)), null, Script.pay2pkh(getPublicKey(0)).map(scala2kmp).asJava, null, java.util.Map.of(getPublicKey(0), bip32paths(0)), null, null, java.util.Map.of()).getRight val Failure(error) = onChainKeyManager.sign(updated, Seq(0, 1, 2), Seq(0)) assert(error.getMessage.contains("utxo mismatch")) } { // do not provide non-witness utxo for utxo #2 val Right(psbt) = for { - p0 <- new Psbt(tx).updateWitnessInput(OutPoint(utxos(0), 0), utxos(0).txOut(0), null, Script.pay2pkh(getPublicKey(0)).map(scala2kmp).asJava, null, java.util.Map.of(getPublicKey(0), bip32paths(0))) - p1 <- p0.updateWitnessInput(OutPoint(utxos(1), 0), utxos(1).txOut(0), null, Script.pay2pkh(getPublicKey(1)).map(scala2kmp).asJava, null, java.util.Map.of(getPublicKey(1), bip32paths(1))) - p2 <- p1.updateWitnessInput(OutPoint(utxos(2), 0), utxos(2).txOut(0), null, Script.pay2pkh(getPublicKey(2)).map(scala2kmp).asJava, null, java.util.Map.of(getPublicKey(2), bip32paths(2))) + p0 <- new Psbt(tx).updateWitnessInput(OutPoint(utxos(0), 0), utxos(0).txOut(0), null, Script.pay2pkh(getPublicKey(0)).map(scala2kmp).asJava, null, java.util.Map.of(getPublicKey(0), bip32paths(0)), null, null, java.util.Map.of()) + p1 <- p0.updateWitnessInput(OutPoint(utxos(1), 0), utxos(1).txOut(0), null, Script.pay2pkh(getPublicKey(1)).map(scala2kmp).asJava, null, java.util.Map.of(getPublicKey(1), bip32paths(1)), null, null, java.util.Map.of()) + p2 <- p1.updateWitnessInput(OutPoint(utxos(2), 0), utxos(2).txOut(0), null, Script.pay2pkh(getPublicKey(2)).map(scala2kmp).asJava, null, java.util.Map.of(getPublicKey(2), bip32paths(2)), null, null, java.util.Map.of()) p3 <- p2.updateNonWitnessInput(utxos(0), 0, null, null, java.util.Map.of()) p4 <- p3.updateNonWitnessInput(utxos(1), 0, null, null, java.util.Map.of()) - p5 <- p4.updateWitnessOutput(0, null, null, java.util.Map.of(getPublicKey(0), bip32paths(0))) + p5 <- p4.updateWitnessOutput(0, null, null, java.util.Map.of(getPublicKey(0), bip32paths(0)), null, java.util.Map.of()) } yield p5 val Failure(error) = onChainKeyManager.sign(psbt, Seq(0, 1, 2), Seq(0)) assert(error.getMessage.contains("non-witness utxo is missing")) } { // use sighash type != SIGHASH_ALL - val updated = psbt.updateWitnessInput(OutPoint(utxos(0), 0), utxos(0).txOut(0), null, Script.pay2pkh(getPublicKey(0)).map(scala2kmp).asJava, SigHash.SIGHASH_SINGLE, java.util.Map.of(getPublicKey(0), bip32paths(0))).getRight + val updated = psbt.updateWitnessInput(OutPoint(utxos(0), 0), utxos(0).txOut(0), null, Script.pay2pkh(getPublicKey(0)).map(scala2kmp).asJava, SigHash.SIGHASH_SINGLE, java.util.Map.of(getPublicKey(0), bip32paths(0)), null, null, java.util.Map.of()).getRight val Failure(error) = onChainKeyManager.sign(updated, Seq(0, 1, 2), Seq(0)) assert(error.getMessage.contains("input sighash must be SIGHASH_ALL")) } } + + test("sign psbt (BIP86") { + import fr.acinq.bitcoin.scalacompat.KotlinUtils._ + + val seed = ByteVector.fromValidHex("01" * 32) + val onChainKeyManager = new LocalOnChainKeyManager("eclair", seed, TimestampSecond.now(), Block.Testnet3GenesisBlock.hash) + + // create a watch-only BIP84 wallet from our key manager xpub + val (_, accountPub) = DeterministicWallet.ExtendedPublicKey.decode(onChainKeyManager.masterPubKey(0, AddressType.Bech32m)) + val mainPub = DeterministicWallet.derivePublicKey(accountPub, 0) + + def getPublicKey(index: Long) = DeterministicWallet.derivePublicKey(mainPub, index).publicKey.xOnly + + val utxos = Seq( + Transaction(version = 2, txIn = Nil, txOut = TxOut(Satoshi(1_000_000), Script.pay2tr(getPublicKey(0), None)) :: Nil, lockTime = 0), + Transaction(version = 2, txIn = Nil, txOut = TxOut(Satoshi(1_100_000), Script.pay2tr(getPublicKey(1), None)) :: Nil, lockTime = 0), + Transaction(version = 2, txIn = Nil, txOut = TxOut(Satoshi(1_200_000), Script.pay2tr(getPublicKey(2), None)) :: Nil, lockTime = 0), + ) + val bip32paths = Seq( + new TaprootBip32DerivationPath(java.util.List.of(), 0, new KeyPath("m/86'/1'/0'/0/0")), + new TaprootBip32DerivationPath(java.util.List.of(), 0, new fr.acinq.bitcoin.KeyPath("m/86'/1'/0'/0/1")), + new TaprootBip32DerivationPath(java.util.List.of(), 0, new fr.acinq.bitcoin.KeyPath("m/86'/1'/0'/0/2")), + ) + + val tx = Transaction(version = 2, + txIn = utxos.map(tx => TxIn(OutPoint(tx, 0), Nil, fr.acinq.bitcoin.TxIn.SEQUENCE_FINAL)), + txOut = TxOut(Satoshi(1000_000), Script.pay2tr(getPublicKey(0), None)) :: Nil, lockTime = 0) + + val Right(psbt) = for { + p0 <- new Psbt(tx).updateWitnessInput(OutPoint(utxos(0), 0), utxos(0).txOut(0), null, null, null, java.util.Map.of(), null, getPublicKey(0), java.util.Map.of(getPublicKey(0), bip32paths(0))) + p1 <- p0.updateWitnessInput(OutPoint(utxos(1), 0), utxos(1).txOut(0), null, null, null, java.util.Map.of(), null, getPublicKey(1), java.util.Map.of(getPublicKey(1), bip32paths(1))) + p2 <- p1.updateWitnessInput(OutPoint(utxos(2), 0), utxos(2).txOut(0), null, null, null, java.util.Map.of(), null, getPublicKey(2), java.util.Map.of(getPublicKey(2), bip32paths(2))) + p3 <- p2.updateWitnessOutput(0, null, null, java.util.Map.of(), getPublicKey(0), java.util.Map.of(getPublicKey(0), bip32paths(0))) + } yield p3 + + { + // sign all inputs and outputs + val Success(psbt1) = onChainKeyManager.sign(psbt, Seq(0, 1, 2), Seq(0)) + val signedTx = psbt1.extract().getRight + Transaction.correctlySpends(signedTx, utxos, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + } + { + // sign the first 2 inputs only + val Success(psbt1) = onChainKeyManager.sign(psbt, Seq(0, 1), Seq(0)) + // extracting the final tx fails because no all inputs as signed + assert(psbt1.extract().isLeft) + assert(psbt1.getInput(2).getScriptWitness == null) + } + { + // provide a wrong derivation path for the first input + val updated = psbt.updateWitnessInput(OutPoint(utxos(0), 0), utxos(0).txOut(0), null, null, null, java.util.Map.of(), null, null, java.util.Map.of(getPublicKey(0), bip32paths(2))).getRight // wrong bip32 path + val Failure(error) = onChainKeyManager.sign(updated, Seq(0, 1, 2), Seq(0)) + assert(error.getMessage.contains("derived public key doesn't match")) + } + { + // provide a wrong derivation path for the first output + val updated = psbt.updateWitnessOutput(0, null, null, java.util.Map.of(), null, java.util.Map.of(getPublicKey(0), bip32paths(1))).getRight // wrong path + val Failure(error) = onChainKeyManager.sign(updated, Seq(0, 1, 2), Seq(0)) + assert(error.getMessage.contains("could not verify output 0")) + } + { + // use sighash type != SIGHASH_ALL + val updated = psbt.updateWitnessInput(OutPoint(utxos(0), 0), utxos(0).txOut(0), null, null, SigHash.SIGHASH_SINGLE, java.util.Map.of(), null, null, java.util.Map.of(getPublicKey(0), bip32paths(0))).getRight + val Failure(error) = onChainKeyManager.sign(updated, Seq(0, 1, 2), Seq(0)) + assert(error.getMessage.contains("input sighash must be SIGHASH_DEFAULT")) + } + } + } 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 e8967b52fa..829b521612 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 @@ -81,7 +81,7 @@ 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, None, OutgoingPaymentStatus.Pending) - val i1 = Bolt11Invoice(Block.TestnetGenesisBlock.hash, Some(500 msat), paymentHash1, davePriv, Left("Some invoice"), CltvExpiryDelta(18), expirySeconds = None, timestamp = 1 unixsec) + val i1 = Bolt11Invoice(Block.Testnet3GenesisBlock.hash, Some(500 msat), paymentHash1, davePriv, Left("Some invoice"), CltvExpiryDelta(18), expirySeconds = None, timestamp = 1 unixsec) val pr1 = IncomingStandardPayment(i1, preimage1, PaymentType.Standard, i1.createdAt.toTimestampMilli, IncomingPaymentStatus.Received(550 msat, 1100 unixms)) db.addOutgoingPayment(ps1) @@ -104,9 +104,9 @@ class PaymentsDbSpec extends AnyFunSuite { val ps1 = OutgoingPayment(id1, id1, None, randomBytes32(), PaymentType.Standard, 561 msat, 561 msat, PrivateKey(ByteVector32.One).publicKey, 1000 unixms, None, None, OutgoingPaymentStatus.Pending) val ps2 = OutgoingPayment(id2, id2, None, randomBytes32(), PaymentType.Standard, 1105 msat, 1105 msat, PrivateKey(ByteVector32.One).publicKey, 1010 unixms, None, 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, None, OutgoingPaymentStatus.Succeeded(preimage1, 0 msat, Nil, 1060 unixms)) - val i1 = Bolt11Invoice(Block.TestnetGenesisBlock.hash, Some(12345678 msat), paymentHash1, davePriv, Left("Some invoice"), CltvExpiryDelta(18), expirySeconds = None, timestamp = 1 unixsec) + val i1 = Bolt11Invoice(Block.Testnet3GenesisBlock.hash, Some(12345678 msat), paymentHash1, davePriv, Left("Some invoice"), CltvExpiryDelta(18), expirySeconds = None, timestamp = 1 unixsec) val pr1 = IncomingStandardPayment(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 i2 = Bolt11Invoice(Block.Testnet3GenesisBlock.hash, Some(12345678 msat), paymentHash2, carolPriv, Left("Another invoice"), CltvExpiryDelta(18), expirySeconds = Some(30), timestamp = 1 unixsec) val pr2 = IncomingStandardPayment(i2, preimage2, PaymentType.Standard, i2.createdAt.toTimestampMilli, IncomingPaymentStatus.Expired) migrationCheck( @@ -190,7 +190,7 @@ class PaymentsDbSpec extends AnyFunSuite { assert(db.getIncomingPayment(i2.paymentHash).contains(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 i3 = Bolt11Invoice(Block.Testnet3GenesisBlock.hash, Some(561 msat), paymentHash3, alicePriv, Left("invoice #3"), CltvExpiryDelta(18), expirySeconds = Some(30)) val pr3 = IncomingStandardPayment(i3, preimage3, PaymentType.Standard, i3.createdAt.toTimestampMilli, IncomingPaymentStatus.Pending) db.addIncomingPayment(i3, pr3.paymentPreimage) @@ -215,7 +215,7 @@ class PaymentsDbSpec extends AnyFunSuite { // Test data val (id1, id2, id3) = (UUID.randomUUID(), UUID.randomUUID(), UUID.randomUUID()) val parentId = UUID.randomUUID() - val invoice1 = Bolt11Invoice(Block.TestnetGenesisBlock.hash, Some(2834 msat), paymentHash1, bobPriv, Left("invoice #1"), CltvExpiryDelta(18), expirySeconds = Some(30)) + val invoice1 = Bolt11Invoice(Block.Testnet3GenesisBlock.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, 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), None, OutgoingPaymentStatus.Pending) val ps3 = OutgoingPayment(id3, parentId, None, paymentHash1, PaymentType.Standard, 1729 msat, 1729 msat, bob, 1040 unixms, None, None, OutgoingPaymentStatus.Succeeded(preimage1, 10 msat, Seq(HopSummary(hop_ab), HopSummary(hop_bc)), 1060 unixms)) @@ -295,9 +295,9 @@ class PaymentsDbSpec extends AnyFunSuite { test("migrate sqlite payments db v4 -> current") { val dbs = TestSqliteDatabases() val now = TimestampSecond.now() - val pendingInvoice = Bolt11Invoice(Block.TestnetGenesisBlock.hash, Some(2500 msat), paymentHash1, bobPriv, Left("invoice #1"), CltvExpiryDelta(18), timestamp = now, expirySeconds = Some(30)) + val pendingInvoice = Bolt11Invoice(Block.Testnet3GenesisBlock.hash, Some(2500 msat), paymentHash1, bobPriv, Left("invoice #1"), CltvExpiryDelta(18), timestamp = now, expirySeconds = Some(30)) val pending = IncomingStandardPayment(pendingInvoice, preimage1, PaymentType.Standard, pendingInvoice.createdAt.toTimestampMilli, IncomingPaymentStatus.Pending) - val paidInvoice = Bolt11Invoice(Block.TestnetGenesisBlock.hash, Some(10_000 msat), paymentHash2, bobPriv, Left("invoice #2"), CltvExpiryDelta(12), timestamp = 250 unixsec, expirySeconds = Some(60)) + val paidInvoice = Bolt11Invoice(Block.Testnet3GenesisBlock.hash, Some(10_000 msat), paymentHash2, bobPriv, Left("invoice #2"), CltvExpiryDelta(12), timestamp = 250 unixsec, expirySeconds = Some(60)) val paid = IncomingStandardPayment(paidInvoice, preimage2, PaymentType.Standard, paidInvoice.createdAt.toTimestampMilli, IncomingPaymentStatus.Received(11_000 msat, 300.unixsec.toTimestampMilli)) migrationCheck( @@ -427,9 +427,9 @@ 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, 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, 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, None, OutgoingPaymentStatus.Succeeded(preimage1, 0 msat, Nil, TimestampMilli.now())) - val i1 = Bolt11Invoice(Block.TestnetGenesisBlock.hash, Some(12345678 msat), paymentHash1, davePriv, Left("Some invoice"), CltvExpiryDelta(18), expirySeconds = None, timestamp = TimestampSecond.now()) + val i1 = Bolt11Invoice(Block.Testnet3GenesisBlock.hash, Some(12345678 msat), paymentHash1, davePriv, Left("Some invoice"), CltvExpiryDelta(18), expirySeconds = None, timestamp = TimestampSecond.now()) val pr1 = IncomingStandardPayment(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 i2 = Bolt11Invoice(Block.Testnet3GenesisBlock.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 = IncomingStandardPayment(i2, preimage2, PaymentType.Standard, i2.createdAt.toTimestampMilli, IncomingPaymentStatus.Expired) migrationCheck( @@ -523,9 +523,9 @@ class PaymentsDbSpec extends AnyFunSuite { test("migrate postgres payments db v6 -> current") { val dbs = TestPgDatabases() val now = TimestampSecond.now() - val pendingInvoice = Bolt11Invoice(Block.TestnetGenesisBlock.hash, Some(2500 msat), paymentHash1, bobPriv, Left("invoice #1"), CltvExpiryDelta(18), timestamp = now, expirySeconds = Some(30)) + val pendingInvoice = Bolt11Invoice(Block.Testnet3GenesisBlock.hash, Some(2500 msat), paymentHash1, bobPriv, Left("invoice #1"), CltvExpiryDelta(18), timestamp = now, expirySeconds = Some(30)) val pending = IncomingStandardPayment(pendingInvoice, preimage1, PaymentType.Standard, pendingInvoice.createdAt.toTimestampMilli, IncomingPaymentStatus.Pending) - val paidInvoice = Bolt11Invoice(Block.TestnetGenesisBlock.hash, Some(10_000 msat), paymentHash2, bobPriv, Left("invoice #2"), CltvExpiryDelta(12), timestamp = 250 unixsec, expirySeconds = Some(60)) + val paidInvoice = Bolt11Invoice(Block.Testnet3GenesisBlock.hash, Some(10_000 msat), paymentHash2, bobPriv, Left("invoice #2"), CltvExpiryDelta(12), timestamp = 250 unixsec, expirySeconds = Some(60)) val paid = IncomingStandardPayment(paidInvoice, preimage2, PaymentType.Standard, paidInvoice.createdAt.toTimestampMilli, IncomingPaymentStatus.Received(11_000 msat, 300.unixsec.toTimestampMilli)) migrationCheck( @@ -653,21 +653,21 @@ class PaymentsDbSpec extends AnyFunSuite { assert(!db.receiveIncomingPayment(unknownPaymentHash, 12345678 msat)) assert(db.getIncomingPayment(unknownPaymentHash).isEmpty) - 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 expiredInvoice1 = Bolt11Invoice(Block.Testnet3GenesisBlock.hash, Some(561 msat), randomBytes32(), alicePriv, Left("invoice #1"), CltvExpiryDelta(18), timestamp = 1 unixsec) + val expiredInvoice2 = Bolt11Invoice(Block.Testnet3GenesisBlock.hash, Some(1105 msat), randomBytes32(), bobPriv, Left("invoice #2"), CltvExpiryDelta(18), timestamp = 2 unixsec, expirySeconds = Some(30)) val expiredPayment1 = IncomingStandardPayment(expiredInvoice1, randomBytes32(), PaymentType.Standard, expiredInvoice1.createdAt.toTimestampMilli, IncomingPaymentStatus.Expired) val expiredPayment2 = IncomingStandardPayment(expiredInvoice2, randomBytes32(), PaymentType.Standard, expiredInvoice2.createdAt.toTimestampMilli, IncomingPaymentStatus.Expired) - val pendingInvoice1 = Bolt11Invoice(Block.TestnetGenesisBlock.hash, Some(561 msat), randomBytes32(), alicePriv, Left("invoice #4"), CltvExpiryDelta(18), timestamp = TimestampSecond.now() - 10.seconds) - val pendingInvoice2 = Bolt11Invoice(Block.TestnetGenesisBlock.hash, Some(1105 msat), randomBytes32(), bobPriv, Left("invoice #5"), CltvExpiryDelta(18), expirySeconds = Some(30), timestamp = TimestampSecond.now() - 9.seconds) + val pendingInvoice1 = Bolt11Invoice(Block.Testnet3GenesisBlock.hash, Some(561 msat), randomBytes32(), alicePriv, Left("invoice #4"), CltvExpiryDelta(18), timestamp = TimestampSecond.now() - 10.seconds) + val pendingInvoice2 = Bolt11Invoice(Block.Testnet3GenesisBlock.hash, Some(1105 msat), randomBytes32(), bobPriv, Left("invoice #5"), CltvExpiryDelta(18), expirySeconds = Some(30), timestamp = TimestampSecond.now() - 9.seconds) val pendingPayment1 = IncomingStandardPayment(pendingInvoice1, randomBytes32(), PaymentType.Standard, pendingInvoice1.createdAt.toTimestampMilli, IncomingPaymentStatus.Pending) val pendingPayment2 = IncomingStandardPayment(pendingInvoice2, randomBytes32(), PaymentType.SwapIn, pendingInvoice2.createdAt.toTimestampMilli, IncomingPaymentStatus.Pending) - val paidInvoice1 = Bolt11Invoice(Block.TestnetGenesisBlock.hash, Some(561 msat), randomBytes32(), alicePriv, Left("invoice #7"), CltvExpiryDelta(18), timestamp = TimestampSecond.now() - 5.seconds) - val paidInvoice2 = Bolt11Invoice(Block.TestnetGenesisBlock.hash, Some(1105 msat), randomBytes32(), bobPriv, Left("invoice #8"), CltvExpiryDelta(18), expirySeconds = Some(60), timestamp = TimestampSecond.now() - 4.seconds) + val paidInvoice1 = Bolt11Invoice(Block.Testnet3GenesisBlock.hash, Some(561 msat), randomBytes32(), alicePriv, Left("invoice #7"), CltvExpiryDelta(18), timestamp = TimestampSecond.now() - 5.seconds) + val paidInvoice2 = Bolt11Invoice(Block.Testnet3GenesisBlock.hash, Some(1105 msat), randomBytes32(), bobPriv, Left("invoice #8"), CltvExpiryDelta(18), expirySeconds = Some(60), timestamp = TimestampSecond.now() - 4.seconds) val nodeId = randomKey().publicKey - val offer = Offer(None, Some("offer"), nodeId, Features.empty, Block.TestnetGenesisBlock.hash) - val paidInvoice3 = MinimalBolt12Invoice(offer, Block.TestnetGenesisBlock.hash, 1729 msat, 1, randomBytes32(), randomKey().publicKey, TimestampSecond.now() - 3.seconds) + val offer = Offer(None, Some("offer"), nodeId, Features.empty, Block.Testnet3GenesisBlock.hash) + val paidInvoice3 = MinimalBolt12Invoice(offer, Block.Testnet3GenesisBlock.hash, 1729 msat, 1, randomBytes32(), randomKey().publicKey, TimestampSecond.now() - 3.seconds) val receivedAt1 = TimestampMilli.now() + 1.milli val receivedAt2 = TimestampMilli.now() + 2.milli val receivedAt3 = TimestampMilli.now() + 3.milli @@ -731,7 +731,7 @@ class PaymentsDbSpec extends AnyFunSuite { val db = dbs.payments val parentId = UUID.randomUUID() - val i1 = Bolt11Invoice(Block.TestnetGenesisBlock.hash, Some(123 msat), paymentHash1, davePriv, Left("Some invoice"), CltvExpiryDelta(18), expirySeconds = None, timestamp = 0 unixsec) + val i1 = Bolt11Invoice(Block.Testnet3GenesisBlock.hash, Some(123 msat), paymentHash1, davePriv, Left("Some invoice"), CltvExpiryDelta(18), expirySeconds = None, timestamp = 0 unixsec) val payerKey = randomKey() val i2 = createBolt12Invoice(789 msat, payerKey, carolPriv, randomBytes32()) val s1 = OutgoingPayment(UUID.randomUUID(), parentId, None, paymentHash1, PaymentType.Standard, 123 msat, 600 msat, dave, 100 unixms, Some(i1), None, OutgoingPaymentStatus.Pending) @@ -800,8 +800,8 @@ object PaymentsDbSpec { val (paymentHash1, paymentHash2, paymentHash3, paymentHash4) = (Crypto.sha256(preimage1), Crypto.sha256(preimage2), Crypto.sha256(preimage3), Crypto.sha256(preimage4)) def createBolt12Invoice(amount: MilliSatoshi, payerKey: PrivateKey, recipientKey: PrivateKey, preimage: ByteVector32): Bolt12Invoice = { - val offer = Offer(Some(amount), Some("some offer"), recipientKey.publicKey, Features.empty, Block.TestnetGenesisBlock.hash) - val invoiceRequest = InvoiceRequest(offer, 789 msat, 1, Features.empty, payerKey, Block.TestnetGenesisBlock.hash) + val offer = Offer(Some(amount), Some("some offer"), recipientKey.publicKey, Features.empty, Block.Testnet3GenesisBlock.hash) + val invoiceRequest = InvoiceRequest(offer, 789 msat, 1, Features.empty, payerKey, Block.Testnet3GenesisBlock.hash) val dummyRoute = PaymentBlindedRoute(RouteBlinding.create(randomKey(), Seq(randomKey().publicKey), Seq(randomBytes(100))).route, PaymentInfo(0 msat, 0, CltvExpiryDelta(0), 0 msat, 0 msat, Features.empty)) Bolt12Invoice(invoiceRequest, preimage, recipientKey, 1 hour, Features.empty, Seq(dummyRoute)) } 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 5a959adeb4..c4a05f7481 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 @@ -24,7 +24,7 @@ import akka.testkit.TestProbe import com.typesafe.config.ConfigFactory import fr.acinq.bitcoin.ScriptFlags import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey -import fr.acinq.bitcoin.scalacompat.{Block, BtcDouble, ByteVector32, Crypto, OutPoint, SatoshiLong, Script, Transaction, TxId, computeBIP84Address} +import fr.acinq.bitcoin.scalacompat.{Block, BtcDouble, ByteVector32, Crypto, OutPoint, SatoshiLong, Script, Transaction, TxId, addressFromPublicKeyScript} import fr.acinq.eclair.blockchain.bitcoind.BitcoindService.BitcoinReq import fr.acinq.eclair.blockchain.bitcoind.rpc.{BitcoinCoreClient, JsonRPCError} import fr.acinq.eclair.channel._ @@ -40,6 +40,7 @@ import fr.acinq.eclair.transactions.{OutgoingHtlc, Scripts, Transactions} import fr.acinq.eclair.wire.protocol._ import fr.acinq.eclair.{MilliSatoshi, MilliSatoshiLong, randomBytes32} import org.json4s.JsonAST.{JString, JValue} +import org.scalatest.{DoNotDiscover, Sequential} import java.util.UUID import scala.concurrent.ExecutionContext.Implicits.global @@ -153,11 +154,13 @@ abstract class ChannelIntegrationSpec extends IntegrationSpec { 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.params.commitmentFormat == commitmentFormat) - val finalAddressC = computeBIP84Address(nodes("C").wallet.getP2wpkhPubkey(false), Block.RegtestGenesisBlock.hash) + val pubkeyScriptC = nodes("C").wallet.getPubkeyScript(false) + val Right(finalAddressC) = addressFromPublicKeyScript(Block.RegtestGenesisBlock.hash, Script.parse(pubkeyScriptC)) 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.params.commitmentFormat == commitmentFormat) - val finalAddressF = computeBIP84Address(nodes("F").wallet.getP2wpkhPubkey(false), Block.RegtestGenesisBlock.hash) + val pubkeyScriptF = nodes("F").wallet.getPubkeyScript(false) + val Right(finalAddressF) = addressFromPublicKeyScript(Block.RegtestGenesisBlock.hash, Script.parse(pubkeyScriptF)) ForceCloseFixture(sender, paymentSender, stateListenerC, stateListenerF, paymentId, htlc, preimage, minerAddress, finalAddressC, finalAddressF) } @@ -439,7 +442,7 @@ abstract class ChannelIntegrationSpec extends IntegrationSpec { // we retrieve C's default final address sender.send(nodes("C").register, Register.Forward(sender.ref.toTyped[Any], commitmentsF.channelId, CMD_GET_CHANNEL_DATA(ActorRef.noSender))) sender.expectMsgType[RES_GET_CHANNEL_DATA[DATA_NORMAL]] - val finalAddressC = computeBIP84Address(nodes("C").wallet.getP2wpkhPubkey(false), Block.RegtestGenesisBlock.hash) + val Right(finalAddressC) = addressFromPublicKeyScript(Block.RegtestGenesisBlock.hash, Script.parse(nodes("C").wallet.getPubkeyScript(false))) // we prepare the revoked transactions F will publish val keyManagerF = nodes("F").nodeParams.channelKeyManager val channelKeyPathF = keyManagerF.keyPath(commitmentsF.params.localParams, commitmentsF.params.channelConfig) @@ -465,6 +468,7 @@ abstract class ChannelIntegrationSpec extends IntegrationSpec { } +@DoNotDiscover class StandardChannelIntegrationSpec extends ChannelIntegrationSpec { test("start eclair nodes") { @@ -574,8 +578,8 @@ class StandardChannelIntegrationSpec extends ChannelIntegrationSpec { 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 fundingOutpoint = commitmentsC.latest.commitInput.outPoint - val finalPubKeyScriptC = Script.write(Script.pay2wpkh(nodes("C").wallet.getP2wpkhPubkey(false))) - val finalPubKeyScriptF = Script.write(Script.pay2wpkh(nodes("F").wallet.getP2wpkhPubkey(false))) + val finalPubKeyScriptC = nodes("C").wallet.getPubkeyScript(false) + val finalPubKeyScriptF = nodes("F").wallet.getPubkeyScript(false) fundee.register ! Register.Forward(sender.ref.toTyped[Any], channelId, CMD_CLOSE(sender.ref, None, None)) sender.expectMsgType[RES_SUCCESS[CMD_CLOSE]] @@ -650,10 +654,18 @@ class StandardChannelIntegrationSpec extends ChannelIntegrationSpec { } +@DoNotDiscover class StandardChannelIntegrationWithEclairSignerSpec extends StandardChannelIntegrationSpec { override def useEclairSigner: Boolean = true } +@DoNotDiscover +class StandardChannelIntegrationWithEclairSignerBech32mSpec extends StandardChannelIntegrationSpec { + override def useEclairSigner: Boolean = true + + override val defaultAddressType_opt: Option[String] = Some("bech32m") +} + abstract class AnchorChannelIntegrationSpec extends ChannelIntegrationSpec { val commitmentFormat: AnchorOutputsCommitmentFormat @@ -802,6 +814,7 @@ abstract class AnchorChannelIntegrationSpec extends ChannelIntegrationSpec { } +@DoNotDiscover class AnchorOutputChannelIntegrationSpec extends AnchorChannelIntegrationSpec { override val commitmentFormat = Transactions.UnsafeLegacyAnchorOutputsCommitmentFormat @@ -842,6 +855,7 @@ class AnchorOutputChannelIntegrationSpec extends AnchorChannelIntegrationSpec { } +@DoNotDiscover class AnchorOutputZeroFeeHtlcTxsChannelIntegrationSpec extends AnchorChannelIntegrationSpec { override val commitmentFormat = Transactions.ZeroFeeHtlcTxAnchorOutputsCommitmentFormat @@ -881,3 +895,141 @@ class AnchorOutputZeroFeeHtlcTxsChannelIntegrationSpec extends AnchorChannelInte } } + +@DoNotDiscover +class AnchorOutputZeroFeeHtlcTxsChannelIntegrationWithEclairSignerSpec extends AnchorChannelIntegrationSpec { + override def useEclairSigner: Boolean = true + + override val commitmentFormat = Transactions.ZeroFeeHtlcTxAnchorOutputsCommitmentFormat + + test("start eclair nodes") { + 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") { + connectNodes(DefaultCommitmentFormat) + } + + test("open channel C <-> F, send payments and close (anchor outputs zero fee htlc txs)") { + testOpenPayClose(commitmentFormat) + } + + test("propagate a fulfill upstream when a downstream htlc is redeemed on-chain (local commit, anchor outputs zero fee htlc txs)") { + testDownstreamFulfillLocalCommit(commitmentFormat) + } + + test("propagate a fulfill upstream when a downstream htlc is redeemed on-chain (remote commit, anchor outputs zero fee htlc txs)") { + testDownstreamFulfillRemoteCommit(commitmentFormat) + } + + test("propagate a failure upstream when a downstream htlc times out (local commit, anchor outputs zero fee htlc txs)") { + testDownstreamTimeoutLocalCommit(commitmentFormat) + } + + test("propagate a failure upstream when a downstream htlc times out (remote commit, anchor outputs zero fee htlc txs)") { + testDownstreamTimeoutRemoteCommit(commitmentFormat) + } + + test("punish a node that has published a revoked commit tx (anchor outputs)") { + testPunishRevokedCommit() + } + +} + +@DoNotDiscover +class AnchorOutputZeroFeeHtlcTxsChannelIntegrationWithBech32mWalletSpec extends AnchorChannelIntegrationSpec { + + override val commitmentFormat = Transactions.ZeroFeeHtlcTxAnchorOutputsCommitmentFormat + + override val defaultAddressType_opt: Option[String] = Some("bech32m") + + test("start eclair nodes") { + 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") { + connectNodes(DefaultCommitmentFormat) + } + + test("open channel C <-> F, send payments and close (anchor outputs zero fee htlc txs)") { + testOpenPayClose(commitmentFormat) + } + + test("propagate a fulfill upstream when a downstream htlc is redeemed on-chain (local commit, anchor outputs zero fee htlc txs)") { + testDownstreamFulfillLocalCommit(commitmentFormat) + } + + test("propagate a fulfill upstream when a downstream htlc is redeemed on-chain (remote commit, anchor outputs zero fee htlc txs)") { + testDownstreamFulfillRemoteCommit(commitmentFormat) + } + + test("propagate a failure upstream when a downstream htlc times out (local commit, anchor outputs zero fee htlc txs)") { + testDownstreamTimeoutLocalCommit(commitmentFormat) + } + + test("propagate a failure upstream when a downstream htlc times out (remote commit, anchor outputs zero fee htlc txs)") { + testDownstreamTimeoutRemoteCommit(commitmentFormat) + } + + test("punish a node that has published a revoked commit tx (anchor outputs)") { + testPunishRevokedCommit() + } + +} + +@DoNotDiscover +class AnchorOutputZeroFeeHtlcTxsChannelIntegrationWithBech32mWalletWithEclairSignerSpec extends AnchorChannelIntegrationSpec { + override def useEclairSigner: Boolean = true + + override val commitmentFormat = Transactions.ZeroFeeHtlcTxAnchorOutputsCommitmentFormat + + override val defaultAddressType_opt: Option[String] = Some("bech32m") + + test("start eclair nodes") { + 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") { + connectNodes(DefaultCommitmentFormat) + } + + test("open channel C <-> F, send payments and close (anchor outputs zero fee htlc txs)") { + testOpenPayClose(commitmentFormat) + } + + test("propagate a fulfill upstream when a downstream htlc is redeemed on-chain (local commit, anchor outputs zero fee htlc txs)") { + testDownstreamFulfillLocalCommit(commitmentFormat) + } + + test("propagate a fulfill upstream when a downstream htlc is redeemed on-chain (remote commit, anchor outputs zero fee htlc txs)") { + testDownstreamFulfillRemoteCommit(commitmentFormat) + } + + test("propagate a failure upstream when a downstream htlc times out (local commit, anchor outputs zero fee htlc txs)") { + testDownstreamTimeoutLocalCommit(commitmentFormat) + } + + test("propagate a failure upstream when a downstream htlc times out (remote commit, anchor outputs zero fee htlc txs)") { + testDownstreamTimeoutRemoteCommit(commitmentFormat) + } + + test("punish a node that has published a revoked commit tx (anchor outputs)") { + testPunishRevokedCommit() + } + +} + +class ChannelIntegrationSuite extends Sequential( + new StandardChannelIntegrationSpec, + new AnchorOutputChannelIntegrationSpec, + new AnchorOutputZeroFeeHtlcTxsChannelIntegrationSpec, + new AnchorOutputZeroFeeHtlcTxsChannelIntegrationWithEclairSignerSpec, + new AnchorOutputZeroFeeHtlcTxsChannelIntegrationWithBech32mWalletSpec, + new AnchorOutputZeroFeeHtlcTxsChannelIntegrationWithBech32mWalletWithEclairSignerSpec +) 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 f4fdd37510..28238dee01 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 @@ -128,8 +128,10 @@ abstract class IntegrationSpec extends TestKitBaseClass with BitcoindService wit implicit val formats: Formats = DefaultFormats + val defaultAddressType_opt: Option[String] = None + override def beforeAll(): Unit = { - startBitcoind() + startBitcoind(defaultAddressType_opt = defaultAddressType_opt) waitForBitcoindReady() } 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 b9bb4417c5..f8cd6e884e 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 @@ -210,7 +210,7 @@ class Bolt12InvoiceSpec extends AnyFunSuite { } test("encode/decode invoice with many fields") { - val chain = Block.TestnetGenesisBlock.hash + val chain = Block.Testnet3GenesisBlock.hash val amount = 123456 msat val description = "invoice with many fields" val features = Features.empty @@ -337,7 +337,7 @@ class Bolt12InvoiceSpec extends AnyFunSuite { assert(payerKey.publicKey == PublicKey(hex"027c6d03fa8f366e2ef8017cdfaf5d3cf1a3b0123db1318263b662c0aa9ec9c959")) val preimage = ByteVector32(hex"99221825b86576e94391b179902be8b22c7cfa7c3d14aec6ae86657dfd9bd2a8") val offer = Offer(TlvStream[OfferTlv]( - OfferChains(Seq(Block.TestnetGenesisBlock.hash)), + OfferChains(Seq(Block.Testnet3GenesisBlock.hash)), OfferAmount(100000 msat), OfferDescription("offer with quantity"), OfferIssuer("alice@bigshop.com"), @@ -346,7 +346,7 @@ class Bolt12InvoiceSpec extends AnyFunSuite { val encodedOffer = "lno1qgsyxjtl6luzd9t3pr62xr7eemp6awnejusgf6gw45q75vcfqqqqqqqgqvqcdgq2zdhkven9wgs8w6t5dqs8zatpde6xjarezggkzmrfvdj5qcnfvaeksmms9e3k7mg5qgp7s93pqvn6l4vemgezdarq3wt2kpp0u4vt74vzz8futen7ej97n93jypp57" assert(offer.toString == encodedOffer) assert(Offer.decode(encodedOffer).get == offer) - val request = InvoiceRequest(offer, 7200000 msat, 72, Features.empty, payerKey, Block.TestnetGenesisBlock.hash) + val request = InvoiceRequest(offer, 7200000 msat, 72, Features.empty, payerKey, Block.Testnet3GenesisBlock.hash) // Invoice request generation is not reproducible because we add randomness in the first TLV. val encodedRequest = "lnr1qqs8lqvnh3kg9uj003lxlxyj8hthymgq4p9ms0ag0ryx5uw8gsuus4gzypp5jl7hlqnf2ugg7j3slkwwcwht57vhyzzwjr4dq84rxzgqqqqqqzqrqxr2qzsndanxvetjypmkjargypch2ctww35hg7gjz9skc6trv4qxy6t8wd5x7upwvdhk69qzq05pvggry7hatxw6xgn0gcytj64sgtl9tzl4tqs360z7vlkv305evv3qgd84qgzrf9la07pxj4cs3a9rplvuasawhfuewgyyay826q02xvysqqqqqpfqxmwaqptqzjzcyyp8cmgrl28nvm3wlqqheha0t570rgaszg7mzvvzvwmx9s92nmyujk0sgpef8dt57nygu3dnfhglymt6mnle6j8s28rler8wv3zygen07v4ddfplc9qs7nkdzwcelm2rs552slkpv45xxng65ne6y4dlq2764gqv" val decodedRequest = InvoiceRequest.decode(encodedRequest).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 37fbfa34d7..359616764e 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 @@ -810,7 +810,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike assert(nodeParams.db.payments.getIncomingPayment(paymentHash).isEmpty) val add = UpdateAddHtlc(ByteVector32.One, 0, 1000 msat, paymentHash, defaultExpiry, TestConstants.emptyOnionPacket, None, 1.0) - val invoice = Bolt11Invoice(Block.TestnetGenesisBlock.hash, None, paymentHash, randomKey(), Left("dummy"), CltvExpiryDelta(12)) + val invoice = Bolt11Invoice(Block.Testnet3GenesisBlock.hash, None, paymentHash, randomKey(), Left("dummy"), CltvExpiryDelta(12)) val incomingPayment = IncomingStandardPayment(invoice, paymentPreimage, PaymentType.Standard, invoice.createdAt.toTimestampMilli, IncomingPaymentStatus.Pending) val fulfill = DoFulfill(incomingPayment, MultiPartPaymentFSM.MultiPartPaymentSucceeded(paymentHash, Queue(HtlcPart(1000 msat, add)))) sender.send(handlerWithoutMpp, fulfill) 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 cd45aa1c7e..41b421d091 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 @@ -158,7 +158,7 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit val preimage = randomBytes32() val paymentHash = Crypto.sha256(preimage) - val invoice = Bolt11Invoice(Block.TestnetGenesisBlock.hash, Some(500 msat), paymentHash, TestConstants.Bob.nodeKeyManager.nodeKey.privateKey, Left("Some invoice"), CltvExpiryDelta(18)) + val invoice = Bolt11Invoice(Block.Testnet3GenesisBlock.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/receive/InvoicePurgerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/receive/InvoicePurgerSpec.scala index e019b0bb01..866cca0210 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 @@ -39,19 +39,19 @@ class InvoicePurgerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("ap 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), + val expiredInvoices = Seq.fill(count)(Bolt11Invoice(Block.Testnet3GenesisBlock.hash, Some(100 msat), randomBytes32(), alicePriv, Left("expired invoice"), CltvExpiryDelta(18), timestamp = 1 unixsec)) val expiredPayments = expiredInvoices.map(invoice => IncomingStandardPayment(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 pendingInvoices = Seq.fill(count)(Bolt11Invoice(Block.Testnet3GenesisBlock.hash, Some(100 msat), randomBytes32(), alicePriv, Left("pending invoice"), CltvExpiryDelta(18))) val pendingPayments = pendingInvoices.map(invoice => IncomingStandardPayment(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 paidInvoices = Seq.fill(count)(Bolt11Invoice(Block.Testnet3GenesisBlock.hash, Some(100 msat), randomBytes32(), alicePriv, Left("paid invoice"), CltvExpiryDelta(18))) val paidPayments = paidInvoices.map(invoice => IncomingStandardPayment(invoice, randomBytes32(), PaymentType.Standard, invoice.createdAt.toTimestampMilli, IncomingPaymentStatus.Received(100 msat, receivedAt))) paidPayments.foreach(payment => { db.addIncomingPayment(payment.invoice, payment.paymentPreimage) @@ -86,13 +86,13 @@ class InvoicePurgerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("ap 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), + val expiredInvoice1 = Bolt11Invoice(Block.Testnet3GenesisBlock.hash, Some(100 msat), randomBytes32(), alicePriv, Left("expired invoice2"), CltvExpiryDelta(18), timestamp = 5 unixsec) val expiredPayment1 = IncomingStandardPayment(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), + val expiredInvoice2 = Bolt11Invoice(Block.Testnet3GenesisBlock.hash, Some(100 msat), randomBytes32(), alicePriv, Left("expired invoice2"), CltvExpiryDelta(18), timestamp = TimestampSecond.now() - 10.days) val expiredPayment2 = IncomingStandardPayment(expiredInvoice2, randomBytes32(), PaymentType.Standard, expiredInvoice2.createdAt.toTimestampMilli, IncomingPaymentStatus.Expired) db.addIncomingPayment(expiredPayment2.invoice, expiredPayment2.paymentPreimage) @@ -108,13 +108,13 @@ class InvoicePurgerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("ap assert(db.listExpiredIncomingPayments(0 unixms, TimestampMilli.now(), None).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), + val expiredInvoice3 = Bolt11Invoice(Block.Testnet3GenesisBlock.hash, Some(100 msat), randomBytes32(), alicePriv, Left("expired invoice3"), CltvExpiryDelta(18), timestamp = 5 unixsec) val expiredPayment3 = IncomingStandardPayment(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), + val expiredInvoice4 = Bolt11Invoice(Block.Testnet3GenesisBlock.hash, Some(100 msat), randomBytes32(), alicePriv, Left("expired invoice4"), CltvExpiryDelta(18), timestamp = TimestampSecond.now() - 10.days) val expiredPayment4 = IncomingStandardPayment(expiredInvoice4, randomBytes32(), PaymentType.Standard, expiredInvoice4.createdAt.toTimestampMilli, IncomingPaymentStatus.Expired) db.addIncomingPayment(expiredPayment4.invoice, expiredPayment4.paymentPreimage) 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 f4bbc0ad4c..e7cd0007c5 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 @@ -64,7 +64,7 @@ class OfferTypesSpec extends AnyFunSuite { test("offer with amount and quantity") { val offer = Offer(TlvStream[OfferTlv]( - OfferChains(Seq(Block.TestnetGenesisBlock.hash)), + OfferChains(Seq(Block.Testnet3GenesisBlock.hash)), OfferAmount(50 msat), OfferDescription("offer with quantity"), OfferIssuer("alice@bigshop.com"), @@ -144,7 +144,7 @@ class OfferTypesSpec extends AnyFunSuite { val withDefaultChain = signInvoiceRequest(request.copy(records = TlvStream(request.records.records ++ Seq(InvoiceRequestChain(Block.LivenetGenesisBlock.hash)))), payerKey) assert(withDefaultChain.isValid) assert(withDefaultChain.offer == offer) - val otherChain = signInvoiceRequest(request.copy(records = TlvStream(request.records.records ++ Seq(InvoiceRequestChain(Block.TestnetGenesisBlock.hash)))), payerKey) + val otherChain = signInvoiceRequest(request.copy(records = TlvStream(request.records.records ++ Seq(InvoiceRequestChain(Block.Testnet3GenesisBlock.hash)))), payerKey) assert(!otherChain.isValid) } { diff --git a/pom.xml b/pom.xml index e7751f7ee5..49963a6d68 100644 --- a/pom.xml +++ b/pom.xml @@ -72,7 +72,7 @@ 2.6.20 10.2.7 3.8.16 - 0.33 + 0.34-SNAPSHOT 32.1.1-jre 2.7.3