1
0
Fork 0
mirror of https://github.com/ACINQ/eclair.git synced 2025-03-13 11:35:47 +01:00

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).
This commit is contained in:
sstone 2024-06-20 15:25:57 +02:00
parent 8381fc4d2b
commit 39e2842261
No known key found for this signature in database
GPG key ID: E04E48E72C205463
17 changed files with 582 additions and 100 deletions

View file

@ -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}
@ -784,7 +785,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")
}

View file

@ -266,6 +266,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.
@ -275,18 +276,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.
//

View file

@ -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. */

View file

@ -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,40 +14,59 @@ 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) =>
@ -54,10 +75,17 @@ private class OnchainPubkeyRefresher(generator: OnChainAddressGenerator, finalPu
}
}
def waiting(script: PublicKey): Behavior[Command] = Behaviors.receiveMessagePartial {
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: ByteVector): Behavior[Command] = Behaviors.receiveMessagePartial {
case Done =>
context.log.info(s"setting final onchain script to $script")
finalPubkey.set(script)
finalPubkeyScript.set(script)
idle()
}
}

View file

@ -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")

View file

@ -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
}

View file

@ -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 {
private val rootPathBIP84 = chainHash match {
case Block.RegtestGenesisBlock.hash | Block.Testnet3GenesisBlock.hash | Block.Testnet4GenesisBlock.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.Testnet3GenesisBlock.hash | Block.Testnet4GenesisBlock.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.Testnet3GenesisBlock.hash | Block.Testnet4GenesisBlock.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.Testnet3GenesisBlock.hash | Block.Testnet4GenesisBlock.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.Testnet3GenesisBlock.hash | Block.Testnet4GenesisBlock.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
@ -222,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")
}
}
}

View file

@ -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

View file

@ -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

View file

@ -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()
}
@ -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

View file

@ -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) {

View file

@ -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)
}
}

View file

@ -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()
}
@ -1329,7 +1332,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 =>
@ -1442,7 +1445,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)
@ -1758,7 +1761,9 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit
val targetFeerate = FeeratePerKw(10_000 sat)
val fundingA = 100_000 sat
val utxosA = Seq(150_000 sat)
val fundingB = 92_000 sat
// values are chose so that we don't have a change output but with bech32m addresses the tx is smaller and the fee (8000 sats) becomes
// too expensive, so we use a slightly larger funding amount
val fundingB = if (this.defaultAddressType_opt.contains("bech32m")) 93_000 sat else 92_000 sat
val utxosB = Seq(50_000 sat, 50_000 sat, 50_000 sat, 50_000 sat)
withFixture(fundingA, utxosA, fundingB, utxosB, targetFeerate, 660 sat, 0, RequireConfirmedInputs(forLocal = false, forRemote = false)) { f =>
import f._
@ -2856,4 +2861,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")
}

View file

@ -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)
@ -1808,8 +1816,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)

View file

@ -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
@ -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.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
@ -114,4 +115,72 @@ class LocalOnChainKeyManagerSpec extends AnyFunSuite {
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"))
}
}
}

View file

@ -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
)

View file

@ -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()
}