Add bitcoin rpc call to check if an address belongs to our wallet (#3267)
What changed, and why it matters
This commit adds a new helper function that lets Eclair ask Bitcoin Core whether a given bitcoin address belongs to the user's own wallet. It is a feature addition with no visible bug fix or security patch. There is no indication in the commit that it fixes a vulnerability.
No security action required. Treat as a normal feature commit. If this method is intended for use in a later security-sensitive flow, ensure the caller validates the address format and handles RPC failures safely.
Security signals we found
No security-relevant keywords in commit title or message
Pure feature addition (new RPC wrapper and tests)
No bug fix, bounds check, input validation, or authorization change
No vendor disclosure or CVE reference present
Evidence from the diff
The change introduces an isMine(address: String) method on the OnChainChannelFunder trait and implements it in BitcoinCoreClient by calling Bitcoin Core’s getaddressinfo RPC and returning the ismine boolean. Dummy and single-key test wallets are updated to provide matching stub implementations, and unit tests are added. The diff is purely additive and does not modify existing logic or close any apparent security hole.
Changed components
eclair-core/src/main/scala/fr/acinq/eclair/blockchain/OnChainWallet.scalaeclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/rpc/BitcoinCoreClient.scalaeclair-core/src/test/scala/fr/acinq/eclair/blockchain/DummyOnChainWallet.scalaeclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreClientSpec.scalaInspect captured patch +64 / −1
diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/OnChainWallet.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/OnChainWallet.scala
index 0d2a217..37e1481 100644
--- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/OnChainWallet.scala
+++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/OnChainWallet.scala
@@ -111,6 +111,12 @@ trait OnChainChannelFunder {
*/
def doubleSpent(tx: Transaction)(implicit ec: ExecutionContext): Future[Boolean]
+ /**
+ * @param address bitcoin address to check
+ * @return true if address belongs to our wallet
+ */
+ def isMine(address: String)(implicit ec: ExecutionContext): Future[Boolean]
+
}
/** This trait lets users generate on-chain addresses and public keys. */
diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/rpc/BitcoinCoreClient.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/rpc/BitcoinCoreClient.scala
index d48600e..0a93000 100644
--- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/rpc/BitcoinCoreClient.scala
+++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/rpc/BitcoinCoreClient.scala
@@ -684,6 +684,13 @@ class BitcoinCoreClient(val rpcClient: BitcoinJsonRPCClient, val lockUtxos: Bool
}
}
+ def isMine(address: String)(implicit ec: ExecutionContext): Future[Boolean] = {
+ for {
+ addressInfo <- rpcClient.invoke("getaddressinfo", address)
+ JBool(isMine) = addressInfo \ "ismine"
+ } yield isMine
+ }
+
//------------------------- MEMPOOL -------------------------//
def getMempool()(implicit ec: ExecutionContext): Future[Seq[Transaction]] =
diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/DummyOnChainWallet.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/DummyOnChainWallet.scala
index 1ee99db..c6312f1 100644
--- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/DummyOnChainWallet.scala
+++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/DummyOnChainWallet.scala
@@ -21,7 +21,7 @@ import fr.acinq.bitcoin.psbt.{KeyPathWithMaster, Psbt, TaprootBip32DerivationPat
import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey
import fr.acinq.bitcoin.scalacompat.Crypto.TaprootTweak.KeyPathTweak
import fr.acinq.bitcoin.scalacompat.DeterministicWallet.KeyPath
-import fr.acinq.bitcoin.scalacompat.{Block, ByteVector64, KotlinUtils, OutPoint, Satoshi, SatoshiLong, Script, ScriptElt, ScriptWitness, Transaction, TxId, TxIn, TxOut}
+import fr.acinq.bitcoin.scalacompat.{Block, ByteVector64, KotlinUtils, OutPoint, Satoshi, SatoshiLong, Script, ScriptElt, ScriptWitness, Transaction, TxId, TxIn, TxOut, addressFromPublicKeyScript}
import fr.acinq.eclair.TestUtils.randomTxId
import fr.acinq.eclair.blockchain.OnChainWallet.{FundTransactionResponse, MakeFundingTxResponse, OnChainBalance, ProcessPsbtResponse}
import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinCoreClient.AddressType
@@ -92,6 +92,12 @@ class DummyOnChainWallet extends OnChainWallet with OnChainAddressCache {
override def doubleSpent(tx: Transaction)(implicit ec: ExecutionContext): Future[Boolean] = Future.successful(false)
override def getReceivePublicKeyScript(renew: Boolean): Seq[ScriptElt] = Script.pay2tr(dummyReceivePubkey.xOnly, KeyPathTweak)
+
+ override def isMine(address: String)(implicit ec: ExecutionContext): Future[Boolean] = Future.successful {
+ if (Right(address) == addressFromPublicKeyScript(Block.RegtestGenesisBlock.hash, Script.pay2wpkh(dummyReceivePubkey))) true
+ else if (Right(address) == addressFromPublicKeyScript(Block.RegtestGenesisBlock.hash, Script.pay2tr(dummyReceivePubkey.xOnly, KeyPathTweak))) true
+ else false
+ }
}
class SingleKeyOnChainWallet extends OnChainWallet with OnChainAddressCache {
@@ -227,6 +233,12 @@ class SingleKeyOnChainWallet extends OnChainWallet with OnChainAddressCache {
override def doubleSpent(tx: Transaction)(implicit ec: ExecutionContext): Future[Boolean] = Future.successful(doubleSpent.contains(tx.txid))
override def getReceivePublicKeyScript(renew: Boolean): Seq[ScriptElt] = p2trScript
+
+ override def isMine(address: String)(implicit ec: ExecutionContext): Future[Boolean] = Future.successful {
+ if (Right(address) == addressFromPublicKeyScript(Block.RegtestGenesisBlock.hash, p2wpkhScript)) true
+ else if (Right(address) == addressFromPublicKeyScript(Block.RegtestGenesisBlock.hash, p2trScript)) true
+ else false
+ }
}
/** A wallet that blocks when called to fund transactions (useful to test events happening while funding). */
diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreClientSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreClientSpec.scala
index d0ba649..24afad7 100644
--- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreClientSpec.scala
+++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreClientSpec.scala
@@ -123,6 +123,25 @@ class BitcoinCoreClientSpec extends TestKitBaseClass with BitcoindService with A
assert(addressToPublicKeyScript(Block.RegtestGenesisBlock.hash, address2).map(Script.isPay2tr).contains(true))
}
+ test("check whether an address belongs to our wallet") {
+ val sender = TestProbe()
+ val bitcoinClient = makeBitcoinCoreClient()
+
+ bitcoinClient.getReceiveAddress(None).pipeTo(sender.ref)
+ val address = sender.expectMsgType[String]
+ bitcoinClient.isMine(address).pipeTo(sender.ref)
+ sender.expectMsg(true)
+
+ bitcoinClient.getChangeAddress(None).pipeTo(sender.ref)
+ val changeAddress = sender.expectMsgType[String]
+ bitcoinClient.isMine(changeAddress).pipeTo(sender.ref)
+ sender.expectMsg(true)
+
+ val randomAddress = computeBIP84Address(randomKey().publicKey, Block.RegtestGenesisBlock.hash)
+ bitcoinClient.isMine(randomAddress).pipeTo(sender.ref)
+ sender.expectMsg(false)
+ }
+
test("fund transactions") {
val sender = TestProbe()
val bitcoinClient = makeBitcoinCoreClient()
@@ -2124,4 +2143,23 @@ class BitcoinCoreClientWithEclairSignerSpec extends BitcoinCoreClientSpec {
wallet.publishTransaction(signedTx).pipeTo(sender.ref)
sender.expectMsg(signedTx.txid)
}
+
+ test("check whether an address belongs to our wallet (private keys managed by eclair)") {
+ val sender = TestProbe()
+ val bitcoinClient = makeBitcoinCoreClient()
+
+ bitcoinClient.getReceiveAddress(None).pipeTo(sender.ref)
+ val address = sender.expectMsgType[String]
+ bitcoinClient.isMine(address).pipeTo(sender.ref)
+ sender.expectMsg(true)
+
+ bitcoinClient.getChangeAddress(None).pipeTo(sender.ref)
+ val changeAddress = sender.expectMsgType[String]
+ bitcoinClient.isMine(changeAddress).pipeTo(sender.ref)
+ sender.expectMsg(true)
+
+ val randomAddress = computeBIP84Address(randomKey().publicKey, Block.RegtestGenesisBlock.hash)
+ bitcoinClient.isMine(randomAddress).pipeTo(sender.ref)
+ sender.expectMsg(false)
+ }
}
Why this scored 17/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.