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:
parent
8381fc4d2b
commit
39e2842261
17 changed files with 582 additions and 100 deletions
|
@ -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")
|
||||
}
|
||||
|
||||
|
|
|
@ -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.
|
||||
//
|
||||
|
|
|
@ -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. */
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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"))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue