Skip to content

Commit dc36ada

Browse files
committed
Add extra tests for added wallet inputs
1 parent 5dcf2b2 commit dc36ada

File tree

2 files changed

+64
-40
lines changed

2 files changed

+64
-40
lines changed

eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/ReplaceableTxFunder.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -274,7 +274,7 @@ private class ReplaceableTxFunder(nodeParams: NodeParams,
274274
Behaviors.stopped
275275
case AdjustPreviousTxOutputResult.TxOutputAdjusted(updatedTx) =>
276276
log.debug("bumping {} fees without adding new inputs: txid={}", cmd.desc, updatedTx.txInfo.tx.txid)
277-
sign(updatedTx, targetFeerate, previousTx.totalAmountIn, Map.empty)
277+
sign(updatedTx, targetFeerate, previousTx.totalAmountIn, previousTx.walletInputs)
278278
case AdjustPreviousTxOutputResult.AddWalletInputs(tx) =>
279279
log.debug("bumping {} fees requires adding new inputs (feerate={})", cmd.desc, targetFeerate)
280280
// We restore the original transaction (remove previous attempt's wallet inputs).

eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPublisherSpec.scala

Lines changed: 63 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -407,35 +407,18 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w
407407
}
408408
}
409409

410+
def getTransaction(probe: TestProbe, txid: TxId): Transaction = {
411+
bitcoinrpcclient.invoke("getrawtransaction", txid).pipeTo(probe.ref)
412+
Transaction.read(probe.expectMsgType[JString].values)
413+
}
414+
415+
def getExtraInputsMap(probe: TestProbe, tx: Transaction): Map[OutPoint, TxOut] = {
416+
tx.txIn.tail.map(input => input.outPoint -> getTransaction(probe, input.outPoint.txid).txOut(input.outPoint.index.toInt)).toMap
417+
}
418+
410419
test("commit tx feerate too low, spending anchor output (local commit)") {
411420
//we create a new channel key manager that delegates all call to Alice's key manager but track extra utxos passed to the sign() methods
412-
val walletInputs = new AtomicReference[Map[OutPoint, TxOut]](Map.empty)
413-
val channelKeyManagerA = TestConstants.Alice.nodeParams.channelKeyManager
414-
// @formatter:off
415-
val channelKeyManager = new ChannelKeyManager {
416-
override def fundingPublicKey(fundingKeyPath: DeterministicWallet.KeyPath, fundingTxIndex: Long): DeterministicWallet.ExtendedPublicKey = channelKeyManagerA.fundingPublicKey(fundingKeyPath, fundingTxIndex)
417-
override def revocationPoint(channelKeyPath: DeterministicWallet.KeyPath): DeterministicWallet.ExtendedPublicKey = channelKeyManagerA.revocationPoint(channelKeyPath)
418-
override def paymentPoint(channelKeyPath: DeterministicWallet.KeyPath): DeterministicWallet.ExtendedPublicKey = channelKeyManagerA.paymentPoint(channelKeyPath)
419-
override def delayedPaymentPoint(channelKeyPath: DeterministicWallet.KeyPath): DeterministicWallet.ExtendedPublicKey = channelKeyManagerA.delayedPaymentPoint(channelKeyPath)
420-
override def htlcPoint(channelKeyPath: DeterministicWallet.KeyPath): DeterministicWallet.ExtendedPublicKey = channelKeyManagerA.htlcPoint(channelKeyPath)
421-
override def commitmentSecret(channelKeyPath: DeterministicWallet.KeyPath, index: Long): Crypto.PrivateKey = channelKeyManagerA.commitmentSecret(channelKeyPath, index)
422-
override def commitmentPoint(channelKeyPath: DeterministicWallet.KeyPath, index: Long): PublicKey = channelKeyManagerA.commitmentPoint(channelKeyPath, index)
423-
override def newFundingKeyPath(isChannelOpener: Boolean): DeterministicWallet.KeyPath = channelKeyManagerA.newFundingKeyPath(isChannelOpener)
424-
override def sign(tx: TransactionWithInputInfo, publicKey: DeterministicWallet.ExtendedPublicKey, txOwner: TxOwner, commitmentFormat: CommitmentFormat, extraUtxos: Map[OutPoint, TxOut]): ByteVector64 = {
425-
walletInputs.set(extraUtxos)
426-
channelKeyManagerA.sign(tx, publicKey, txOwner, commitmentFormat, extraUtxos)
427-
}
428-
override def sign(tx: TransactionWithInputInfo, publicKey: DeterministicWallet.ExtendedPublicKey, remotePoint: PublicKey, txOwner: TxOwner, commitmentFormat: CommitmentFormat, extraUtxos: Map[OutPoint, TxOut]): ByteVector64 = {
429-
walletInputs.set(extraUtxos)
430-
channelKeyManagerA.sign(tx, publicKey, remotePoint, txOwner, commitmentFormat, extraUtxos)
431-
}
432-
override def sign(tx: TransactionWithInputInfo, publicKey: DeterministicWallet.ExtendedPublicKey, remoteSecret: Crypto.PrivateKey, txOwner: TxOwner, commitmentFormat: CommitmentFormat, extraUtxos: Map[OutPoint, TxOut]): ByteVector64 = {
433-
walletInputs.set(extraUtxos)
434-
channelKeyManagerA.sign(tx, publicKey, remoteSecret, txOwner, commitmentFormat, extraUtxos)
435-
}
436-
override def signChannelAnnouncement(witness: ByteVector, fundingKeyPath: DeterministicWallet.KeyPath): ByteVector64 = channelKeyManagerA.signChannelAnnouncement(witness, fundingKeyPath)
437-
}
438-
// @formatter:on
421+
val channelKeyManager = new DummyChannelKeyManager(TestConstants.Alice.nodeParams.channelKeyManager)
439422

440423
withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx(), Some(channelKeyManager)) { f =>
441424
import f._
@@ -455,16 +438,10 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w
455438
// check that the wallet input added by the tx funder was passed to the key manager
456439
assert(mempoolTxs.map(_.txid).contains(commitTx.tx.txid))
457440

458-
def getTransaction(txid: TxId): Transaction = {
459-
bitcoinrpcclient.invoke("getrawtransaction", txid).pipeTo(probe.ref)
460-
Transaction.read(probe.expectMsgType[JString].values)
461-
}
462-
463441
// there are 2 transactions in the mempool, the one that is not the commit tx has to be the anchor tx
464-
val publishedAnchorTx = getTransaction(mempoolTxs.filterNot(_.txid == commitTx.tx.txid).head.txid)
465-
val extraInputs = publishedAnchorTx.txIn.tail.map(input => input.outPoint -> getTransaction(input.outPoint.txid).txOut(input.outPoint.index.toInt)).toMap
442+
val publishedAnchorTx = getTransaction(probe, mempoolTxs.filterNot(_.txid == commitTx.tx.txid).head.txid)
466443
// check that inputs added to the anchor tx match what was passed to our key manager's sign() method
467-
assert(walletInputs.get() == extraInputs)
444+
assert(channelKeyManager.walletInputs.get() == getExtraInputsMap(probe, publishedAnchorTx))
468445

469446
val targetFee = Transactions.weight2fee(targetFeerate, mempoolTxs.map(_.weight).sum.toInt)
470447
val actualFee = mempoolTxs.map(_.fees).sum
@@ -1293,7 +1270,8 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w
12931270
}
12941271

12951272
test("htlc success tx not confirming, lowering output amount") {
1296-
withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx()) { f =>
1273+
val channelKeyManager = new DummyChannelKeyManager(TestConstants.Alice.nodeParams.channelKeyManager)
1274+
withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx(), channelKeyManager_opt = Some(channelKeyManager)) { f =>
12971275
import f._
12981276

12991277
val initialFeerate = FeeratePerKw(15_000 sat)
@@ -1321,13 +1299,19 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w
13211299
assert(htlcSuccessTx2.txid == htlcSuccessTxId2)
13221300
assert(htlcSuccessTx1.fees < htlcSuccessTx2.fees)
13231301
assert(htlcSuccessInputs1 == htlcSuccessInputs2)
1302+
1303+
// check that wallet inputs were passed to the sign() methods
1304+
val publishedHtlcSuccessTx = getTransaction(probe, htlcSuccessTxId2)
1305+
assert(channelKeyManager.walletInputs.get() == getExtraInputsMap(probe, publishedHtlcSuccessTx))
1306+
13241307
val htlcSuccessTargetFee = Transactions.weight2fee(targetFeerate, htlcSuccessTx2.weight.toInt)
13251308
assert(htlcSuccessTargetFee * 0.9 <= htlcSuccessTx2.fees && htlcSuccessTx2.fees <= htlcSuccessTargetFee * 1.1, s"actualFee=${htlcSuccessTx2.fees} targetFee=$htlcSuccessTargetFee")
13261309
}
13271310
}
13281311

13291312
test("htlc success tx not confirming, adding other wallet inputs") {
1330-
withFixture(Seq(1_010_000 sat, 10_000 sat), ChannelTypes.AnchorOutputsZeroFeeHtlcTx()) { f =>
1313+
val channelKeyManager = new DummyChannelKeyManager(TestConstants.Alice.nodeParams.channelKeyManager)
1314+
withFixture(Seq(1_010_000 sat, 10_000 sat), ChannelTypes.AnchorOutputsZeroFeeHtlcTx(), channelKeyManager_opt = Some(channelKeyManager)) { f =>
13311315
import f._
13321316

13331317
val initialFeerate = FeeratePerKw(3_000 sat)
@@ -1355,6 +1339,11 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w
13551339
assert(htlcSuccessTx2.txid == htlcSuccessTxId2)
13561340
assert(htlcSuccessTx1.fees < htlcSuccessTx2.fees)
13571341
assert(htlcSuccessInputs1 != htlcSuccessInputs2)
1342+
1343+
// check that wallet inputs were passed to the sign() methods
1344+
val publishedHtlcSuccessTx = getTransaction(probe, htlcSuccessTxId2)
1345+
assert(channelKeyManager.walletInputs.get() == getExtraInputsMap(probe, publishedHtlcSuccessTx))
1346+
13581347
val htlcSuccessTargetFee = Transactions.weight2fee(targetFeerate, htlcSuccessTx2.weight.toInt)
13591348
assert(htlcSuccessTargetFee * 0.9 <= htlcSuccessTx2.fees && htlcSuccessTx2.fees <= htlcSuccessTargetFee * 1.1, s"actualFee=${htlcSuccessTx2.fees} targetFee=$htlcSuccessTargetFee")
13601349
}
@@ -1422,7 +1411,9 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w
14221411
}
14231412

14241413
test("htlc timeout tx not confirming, increasing fees") {
1425-
withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx()) { f =>
1414+
val channelKeyManager = new DummyChannelKeyManager(TestConstants.Alice.channelKeyManager)
1415+
1416+
withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx(), channelKeyManager_opt = Some(channelKeyManager)) { f =>
14261417
import f._
14271418

14281419
val feerate = FeeratePerKw(15_000 sat)
@@ -1452,6 +1443,10 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w
14521443
assert(htlcTimeoutTx2.txid == htlcTimeoutTxId2)
14531444
assert(htlcTimeoutTx1.fees < htlcTimeoutTx2.fees)
14541445
assert(htlcTimeoutInputs1 == htlcTimeoutInputs2)
1446+
1447+
val publishedHtlcTimeoutTx = getTransaction(probe, htlcTimeoutTxId2)
1448+
assert(channelKeyManager.walletInputs.get() == getExtraInputsMap(probe, publishedHtlcTimeoutTx))
1449+
14551450
// Once the confirmation target is reach, we should raise the feerate by at least 20% at every block.
14561451
val htlcTimeoutTargetFee = Transactions.weight2fee(feerate * 1.2, htlcTimeoutTx2.weight.toInt)
14571452
assert(htlcTimeoutTargetFee * 0.9 <= htlcTimeoutTx2.fees && htlcTimeoutTx2.fees <= htlcTimeoutTargetFee * 1.1, s"actualFee=${htlcTimeoutTx2.fees} targetFee=$htlcTimeoutTargetFee")
@@ -1922,4 +1917,33 @@ class ReplaceableTxPublisherWithEclairSignerSpec extends ReplaceableTxPublisherS
19221917

19231918
(walletRpcClient, walletClient)
19241919
}
1925-
}
1920+
}
1921+
1922+
// wrapper that delegates operations to an actual ChannelKeyManager and extra inputs passed to the sign() methods
1923+
class DummyChannelKeyManager(channelKeyManager: ChannelKeyManager) extends ChannelKeyManager {
1924+
val walletInputs = new AtomicReference[Map[OutPoint, TxOut]](Map.empty)
1925+
1926+
// @formatter:off
1927+
override def fundingPublicKey(fundingKeyPath: DeterministicWallet.KeyPath, fundingTxIndex: Long): DeterministicWallet.ExtendedPublicKey = channelKeyManager.fundingPublicKey(fundingKeyPath, fundingTxIndex)
1928+
override def revocationPoint(channelKeyPath: DeterministicWallet.KeyPath): DeterministicWallet.ExtendedPublicKey = channelKeyManager.revocationPoint(channelKeyPath)
1929+
override def paymentPoint(channelKeyPath: DeterministicWallet.KeyPath): DeterministicWallet.ExtendedPublicKey = channelKeyManager.paymentPoint(channelKeyPath)
1930+
override def delayedPaymentPoint(channelKeyPath: DeterministicWallet.KeyPath): DeterministicWallet.ExtendedPublicKey = channelKeyManager.delayedPaymentPoint(channelKeyPath)
1931+
override def htlcPoint(channelKeyPath: DeterministicWallet.KeyPath): DeterministicWallet.ExtendedPublicKey = channelKeyManager.htlcPoint(channelKeyPath)
1932+
override def commitmentSecret(channelKeyPath: DeterministicWallet.KeyPath, index: Long): Crypto.PrivateKey = channelKeyManager.commitmentSecret(channelKeyPath, index)
1933+
override def commitmentPoint(channelKeyPath: DeterministicWallet.KeyPath, index: Long): PublicKey = channelKeyManager.commitmentPoint(channelKeyPath, index)
1934+
override def newFundingKeyPath(isChannelOpener: Boolean): DeterministicWallet.KeyPath = channelKeyManager.newFundingKeyPath(isChannelOpener)
1935+
override def sign(tx: TransactionWithInputInfo, publicKey: DeterministicWallet.ExtendedPublicKey, txOwner: TxOwner, commitmentFormat: CommitmentFormat, extraUtxos: Map[OutPoint, TxOut]): ByteVector64 = {
1936+
walletInputs.set(extraUtxos)
1937+
channelKeyManager.sign(tx, publicKey, txOwner, commitmentFormat, extraUtxos)
1938+
}
1939+
override def sign(tx: TransactionWithInputInfo, publicKey: DeterministicWallet.ExtendedPublicKey, remotePoint: PublicKey, txOwner: TxOwner, commitmentFormat: CommitmentFormat, extraUtxos: Map[OutPoint, TxOut]): ByteVector64 = {
1940+
walletInputs.set(extraUtxos)
1941+
channelKeyManager.sign(tx, publicKey, remotePoint, txOwner, commitmentFormat, extraUtxos)
1942+
}
1943+
override def sign(tx: TransactionWithInputInfo, publicKey: DeterministicWallet.ExtendedPublicKey, remoteSecret: Crypto.PrivateKey, txOwner: TxOwner, commitmentFormat: CommitmentFormat, extraUtxos: Map[OutPoint, TxOut]): ByteVector64 = {
1944+
walletInputs.set(extraUtxos)
1945+
channelKeyManager.sign(tx, publicKey, remoteSecret, txOwner, commitmentFormat, extraUtxos)
1946+
}
1947+
override def signChannelAnnouncement(witness: ByteVector, fundingKeyPath: DeterministicWallet.KeyPath): ByteVector64 = channelKeyManager.signChannelAnnouncement(witness, fundingKeyPath)
1948+
// @formatter:on
1949+
}

0 commit comments

Comments
 (0)