Skip to content

Commit

Permalink
Publish txs with min-relay-fee met (#1687)
Browse files Browse the repository at this point in the history
When our mempool is full, its min-relay-fee may be constantly changing.
To ensure our txs can be published, we need to check the min-relay-fee when
we fund the transaction, and raise it if necessary.
  • Loading branch information
t-bast authored Feb 15, 2021
1 parent 36e8c05 commit 2a359c6
Show file tree
Hide file tree
Showing 2 changed files with 39 additions and 12 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ package fr.acinq.eclair.blockchain.bitcoind

import fr.acinq.bitcoin.Crypto.PublicKey
import fr.acinq.bitcoin._
import fr.acinq.eclair._
import fr.acinq.eclair.blockchain._
import fr.acinq.eclair.blockchain.bitcoind.rpc.{BitcoinJsonRPCClient, Error, ExtendedBitcoinClient, JsonRPCError}
import fr.acinq.eclair.blockchain.fee.{FeeratePerKB, FeeratePerKw}
Expand All @@ -28,6 +27,7 @@ import org.json4s.JsonAST._
import scodec.bits.ByteVector

import scala.concurrent.{ExecutionContext, Future}
import scala.math.BigDecimal.long2bigDecimal
import scala.util.{Failure, Success}

/**
Expand All @@ -42,12 +42,20 @@ class BitcoinCoreWallet(rpcClient: BitcoinJsonRPCClient)(implicit ec: ExecutionC
def fundTransaction(tx: Transaction, lockUnspents: Boolean, feeRatePerKw: FeeratePerKw): Future[FundTransactionResponse] = fundTransaction(Transaction.write(tx).toHex, lockUnspents, feeRatePerKw)

private def fundTransaction(hex: String, lockUnspents: Boolean, feeRatePerKw: FeeratePerKw): Future[FundTransactionResponse] = {
val feeRatePerKB = BigDecimal(FeeratePerKB(feeRatePerKw).toLong)
rpcClient.invoke("fundrawtransaction", hex, Options(lockUnspents, feeRatePerKB.bigDecimal.scaleByPowerOfTen(-8))).map(json => {
val JString(hex) = json \ "hex"
val JInt(changepos) = json \ "changepos"
val JDecimal(fee) = json \ "fee"
FundTransactionResponse(Transaction.read(hex), changepos.intValue, toSatoshi(fee))
val requestedFeeRatePerKB = FeeratePerKB(feeRatePerKw)
rpcClient.invoke("getmempoolinfo").map(json => json \ "mempoolminfee" match {
case JDecimal(feerate) => FeeratePerKB(Btc(feerate).toSatoshi).max(requestedFeeRatePerKB)
case JInt(feerate) => FeeratePerKB(Btc(feerate.toLong).toSatoshi).max(requestedFeeRatePerKB)
case other =>
logger.warn(s"cannot retrieve mempool minimum fee: $other")
requestedFeeRatePerKB
}).flatMap(feeRatePerKB => {
rpcClient.invoke("fundrawtransaction", hex, Options(lockUnspents, feeRatePerKB.toLong.bigDecimal.scaleByPowerOfTen(-8))).map(json => {
val JString(hex) = json \ "hex"
val JInt(changepos) = json \ "changepos"
val JDecimal(fee) = json \ "fee"
FundTransactionResponse(Transaction.read(hex), changepos.intValue, toSatoshi(fee))
})
})
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ class BitcoinCoreWalletSpec extends TestKitBaseClass with BitcoindService with A
port = config.getInt("bitcoind.rpcport")) {
override def invoke(method: String, params: Any*)(implicit ec: ExecutionContext): Future[JValue] = method match {
case "getbalances" => Future(JObject("mine" -> JObject("trusted" -> apiAmount, "untrusted_pending" -> apiAmount)))(ec)
case "getmempoolinfo" => Future(JObject("mempoolminfee" -> JDecimal(0.0002)))(ec)
case "fundrawtransaction" => Future(JObject(List("hex" -> JString(hexOut), "changepos" -> JInt(1), "fee" -> apiAmount)))(ec)
case _ => Future.failed(new RuntimeException(s"Test BasicBitcoinJsonRPCClient: method $method is not supported"))
}
Expand Down Expand Up @@ -244,11 +245,11 @@ class BitcoinCoreWalletSpec extends TestKitBaseClass with BitcoindService with A

val fundingTxes = for (_ <- 0 to 3) yield {
val pubkeyScript = Script.write(Script.pay2wsh(Scripts.multiSig2of2(randomKey.publicKey, randomKey.publicKey)))
wallet.makeFundingTx(pubkeyScript, MilliBtc(50), FeeratePerKw(200 sat)).pipeTo(sender.ref) // create a tx with an invalid feerate (too little)
val belowFeeFundingTx = sender.expectMsgType[MakeFundingTxResponse].fundingTx
extendedClient.publishTransaction(belowFeeFundingTx).pipeTo(sender.ref) // try publishing the tx
assert(sender.expectMsgType[Failure].cause.asInstanceOf[JsonRPCError].error.message.contains("min relay fee not met"))
wallet.rollback(belowFeeFundingTx).pipeTo(sender.ref) // rollback the locked outputs
wallet.makeFundingTx(pubkeyScript, Satoshi(500), FeeratePerKw(250 sat)).pipeTo(sender.ref)
val fundingTx = sender.expectMsgType[MakeFundingTxResponse].fundingTx
extendedClient.publishTransaction(fundingTx.copy(txIn = Nil)).pipeTo(sender.ref) // try publishing an invalid version of the tx
sender.expectMsgType[Failure]
wallet.rollback(fundingTx).pipeTo(sender.ref) // rollback the locked outputs
assert(sender.expectMsgType[Boolean])

// now fund a tx with correct feerate
Expand Down Expand Up @@ -326,6 +327,24 @@ class BitcoinCoreWalletSpec extends TestKitBaseClass with BitcoindService with A
assert(sender.expectMsgType[OnChainBalance].confirmed > 0.sat)
}

test("ensure feerate is always above min-relay-fee") {
val bitcoinClient = new BasicBitcoinJsonRPCClient(
user = config.getString("bitcoind.rpcuser"),
password = config.getString("bitcoind.rpcpassword"),
host = config.getString("bitcoind.host"),
port = config.getInt("bitcoind.rpcport"))
val wallet = new BitcoinCoreWallet(bitcoinClient)
val sender = TestProbe()

val pubkeyScript = Script.write(Script.pay2wsh(Scripts.multiSig2of2(randomKey.publicKey, randomKey.publicKey)))
// 200 sat/kw is below the min-relay-fee
wallet.makeFundingTx(pubkeyScript, MilliBtc(5), FeeratePerKw(200 sat)).pipeTo(sender.ref)
val MakeFundingTxResponse(fundingTx, _, _) = sender.expectMsgType[MakeFundingTxResponse]

wallet.commit(fundingTx).pipeTo(sender.ref)
sender.expectMsg(true)
}

test("getReceivePubkey should return the raw pubkey for the receive address") {
val bitcoinClient = new BasicBitcoinJsonRPCClient(
user = config.getString("bitcoind.rpcuser"),
Expand Down

0 comments on commit 2a359c6

Please sign in to comment.