Skip to content

Commit a1ccf3a

Browse files
committed
Include block header and height in init
We include the current block height and block header in our `init` message. This lets our peers detect that they may be behind and need to check their bitcoin node.
1 parent 791edf7 commit a1ccf3a

30 files changed

+191
-111
lines changed

eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package fr.acinq.eclair
1818

1919
import com.typesafe.config.{Config, ConfigFactory, ConfigValueType}
20+
import fr.acinq.bitcoin.BlockHeader
2021
import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey
2122
import fr.acinq.bitcoin.scalacompat.{Block, BlockHash, Crypto, Satoshi}
2223
import fr.acinq.eclair.Setup.Seeds
@@ -57,6 +58,7 @@ case class NodeParams(nodeKeyManager: NodeKeyManager,
5758
onChainKeyManager_opt: Option[OnChainKeyManager],
5859
instanceId: UUID, // a unique instance ID regenerated after each restart
5960
private val blockHeight: AtomicLong,
61+
private val blockHeader: AtomicReference[BlockHeader],
6062
private val feerates: AtomicReference[FeeratesPerKw],
6163
alias: String,
6264
color: Color,
@@ -100,6 +102,8 @@ case class NodeParams(nodeKeyManager: NodeKeyManager,
100102

101103
def currentBlockHeight: BlockHeight = BlockHeight(blockHeight.get)
102104

105+
def currentBlockHeader: BlockHeader = blockHeader.get()
106+
103107
def currentFeerates: FeeratesPerKw = feerates.get()
104108

105109
/** Only to be used in tests. */
@@ -215,7 +219,7 @@ object NodeParams extends Logging {
215219

216220
def makeNodeParams(config: Config, instanceId: UUID,
217221
nodeKeyManager: NodeKeyManager, channelKeyManager: ChannelKeyManager, onChainKeyManager_opt: Option[OnChainKeyManager],
218-
torAddress_opt: Option[NodeAddress], database: Databases, blockHeight: AtomicLong, feerates: AtomicReference[FeeratesPerKw],
222+
torAddress_opt: Option[NodeAddress], database: Databases, blockHeight: AtomicLong, blockHeader: AtomicReference[BlockHeader], feerates: AtomicReference[FeeratesPerKw],
219223
pluginParams: Seq[PluginParams] = Nil): NodeParams = {
220224
// check configuration for keys that have been renamed
221225
val deprecatedKeyPaths = Map(
@@ -482,6 +486,7 @@ object NodeParams extends Logging {
482486
onChainKeyManager_opt = onChainKeyManager_opt,
483487
instanceId = instanceId,
484488
blockHeight = blockHeight,
489+
blockHeader = blockHeader,
485490
feerates = feerates,
486491
alias = nodeAlias,
487492
color = Color(color(0), color(1), color(2)),

eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import akka.actor.typed.scaladsl.adapter.{ClassicActorRefOps, ClassicActorSystem
2222
import akka.actor.{ActorRef, ActorSystem, Props, SupervisorStrategy, typed}
2323
import akka.pattern.after
2424
import akka.util.Timeout
25+
import fr.acinq.bitcoin.BlockHeader
2526
import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey
2627
import fr.acinq.bitcoin.scalacompat.{Block, BlockHash, BlockId, ByteVector32, Satoshi, Script, addressToPublicKeyScript}
2728
import fr.acinq.eclair.Setup.Seeds
@@ -110,6 +111,11 @@ class Setup(val datadir: File,
110111
*/
111112
val blockHeight = new AtomicLong(0)
112113

114+
/**
115+
* This holds the latest block header we've received.
116+
*/
117+
val blockHeader = new AtomicReference[BlockHeader](null)
118+
113119
/**
114120
* This holds the current feerates, in satoshi-per-kilobytes.
115121
* The value is read by all actors, hence it needs to be thread-safe.
@@ -134,7 +140,7 @@ class Setup(val datadir: File,
134140
case "password" => BitcoinJsonRPCAuthMethod.UserPassword(config.getString("bitcoind.rpcuser"), config.getString("bitcoind.rpcpassword"))
135141
}
136142

137-
case class BitcoinStatus(version: Int, chainHash: BlockHash, initialBlockDownload: Boolean, verificationProgress: Double, blockCount: Long, headerCount: Long, unspentAddresses: List[String])
143+
case class BitcoinStatus(version: Int, chainHash: BlockHash, initialBlockDownload: Boolean, verificationProgress: Double, blockCount: Long, headerCount: Long, latestHeader: BlockHeader, unspentAddresses: List[String])
138144

139145
def getBitcoinStatus(bitcoinClient: BasicBitcoinJsonRPCClient): Future[BitcoinStatus] = for {
140146
json <- bitcoinClient.invoke("getblockchaininfo").recover { case e => throw BitcoinRPCConnectionException(e) }
@@ -150,6 +156,8 @@ class Setup(val datadir: File,
150156
// NB: bitcoind confusingly returns the blockId instead of the blockHash.
151157
chainHash <- bitcoinClient.invoke("getblockhash", 0).map(_.extract[String]).map(s => BlockId(ByteVector32.fromValidHex(s))).map(BlockHash(_))
152158
bitcoinVersion <- bitcoinClient.invoke("getnetworkinfo").map(json => json \ "version").map(_.extract[Int])
159+
blockHash <- bitcoinClient.invoke("getblockhash", blocks).map(_.extract[String])
160+
latestHeader <- bitcoinClient.invoke("getblockheader", blockHash, /* verbose */ false).map(_.extract[String]).map(BlockHeader.read)
153161
unspentAddresses <- bitcoinClient.invoke("listunspent").recover { _ => if (wallet.isEmpty && wallets.length > 1) throw BitcoinDefaultWalletException(wallets) else throw BitcoinWalletNotLoadedException(wallet.getOrElse(""), wallets) }
154162
.collect { case JArray(values) =>
155163
values
@@ -162,7 +170,7 @@ class Setup(val datadir: File,
162170
case "signet" => bitcoinClient.invoke("getrawtransaction", "ff1027486b628b2d160859205a3401fb2ee379b43527153b0b50a92c17ee7955") // coinbase of #5000
163171
case "regtest" => Future.successful(())
164172
}
165-
} yield BitcoinStatus(bitcoinVersion, chainHash, ibd, progress, blocks, headers, unspentAddresses)
173+
} yield BitcoinStatus(bitcoinVersion, chainHash, ibd, progress, blocks, headers, latestHeader, unspentAddresses)
166174

167175
def pollBitcoinStatus(bitcoinClient: BasicBitcoinJsonRPCClient): Future[BitcoinStatus] = {
168176
getBitcoinStatus(bitcoinClient).transformWith {
@@ -197,16 +205,17 @@ class Setup(val datadir: File,
197205
assert(bitcoinStatus.verificationProgress > 0.999, s"bitcoind should be synchronized (progress=${bitcoinStatus.verificationProgress})")
198206
assert(bitcoinStatus.headerCount - bitcoinStatus.blockCount <= 1, s"bitcoind should be synchronized (headers=${bitcoinStatus.headerCount} blocks=${bitcoinStatus.blockCount})")
199207
}
200-
logger.info(s"current blockchain height=${bitcoinStatus.blockCount}")
208+
logger.info(s"current blockchain height=${bitcoinStatus.blockCount} header=${ByteVector(BlockHeader.write(bitcoinStatus.latestHeader)).toHex}")
201209
blockHeight.set(bitcoinStatus.blockCount)
210+
blockHeader.set(bitcoinStatus.latestHeader)
202211
(bitcoinClient, bitcoinStatus.chainHash)
203212
}
204213

205214
val instanceId = UUID.randomUUID()
206215
logger.info(s"connecting to database with instanceId=$instanceId")
207216
val databases = Databases.init(config.getConfig("db"), instanceId, chaindir, db)
208217

209-
val nodeParams = NodeParams.makeNodeParams(config, instanceId, nodeKeyManager, channelKeyManager, onChainKeyManager_opt, initTor(), databases, blockHeight, feeratesPerKw, pluginParams)
218+
val nodeParams = NodeParams.makeNodeParams(config, instanceId, nodeKeyManager, channelKeyManager, onChainKeyManager_opt, initTor(), databases, blockHeight, blockHeader, feeratesPerKw, pluginParams)
210219

211220
logger.info(s"nodeid=${nodeParams.nodeId} alias=${nodeParams.alias}")
212221
assert(bitcoinChainHash == nodeParams.chainHash, s"chainHash mismatch (conf=${nodeParams.chainHash} != bitcoind=$bitcoinChainHash)")

eclair-core/src/main/scala/fr/acinq/eclair/blockchain/BlockchainEvents.scala

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
package fr.acinq.eclair.blockchain
1818

19+
import fr.acinq.bitcoin.BlockHeader
1920
import fr.acinq.bitcoin.scalacompat.{BlockId, Transaction}
2021
import fr.acinq.eclair.BlockHeight
2122
import fr.acinq.eclair.blockchain.fee.FeeratesPerKw
@@ -30,6 +31,6 @@ case class NewBlock(blockId: BlockId) extends BlockchainEvent
3031

3132
case class NewTransaction(tx: Transaction) extends BlockchainEvent
3233

33-
case class CurrentBlockHeight(blockHeight: BlockHeight) extends BlockchainEvent
34+
case class CurrentBlockHeight(blockHeight: BlockHeight, blockHeader_opt: Option[BlockHeader]) extends BlockchainEvent
3435

3536
case class CurrentFeerates(feeratesPerKw: FeeratesPerKw) extends BlockchainEvent

eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/ZmqWatcher.scala

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package fr.acinq.eclair.blockchain.bitcoind
1919
import akka.actor.typed.eventstream.EventStream
2020
import akka.actor.typed.scaladsl.{ActorContext, Behaviors, TimerScheduler}
2121
import akka.actor.typed.{ActorRef, Behavior, SupervisorStrategy}
22+
import fr.acinq.bitcoin.BlockHeader
2223
import fr.acinq.bitcoin.scalacompat._
2324
import fr.acinq.eclair.blockchain.Monitoring.Metrics
2425
import fr.acinq.eclair.blockchain._
@@ -60,7 +61,7 @@ object ZmqWatcher {
6061
private case object TickBlockTimeout extends Command
6162
private case class GetBlockCountFailed(t: Throwable) extends Command
6263
private case class CheckBlockHeight(current: BlockHeight) extends Command
63-
private case class PublishBlockHeight(current: BlockHeight) extends Command
64+
private case class PublishBlockHeight(current: BlockHeight, header_opt: Option[BlockHeader]) extends Command
6465
private case class ProcessNewBlock(blockId: BlockId) extends Command
6566
private case class ProcessNewTransaction(tx: Transaction) extends Command
6667

@@ -275,9 +276,12 @@ private class ZmqWatcher(nodeParams: NodeParams, blockHeight: AtomicLong, client
275276
Behaviors.same
276277

277278
case TickNewBlock =>
278-
context.pipeToSelf(client.getBlockHeight()) {
279+
context.pipeToSelf(for {
280+
height <- client.getBlockHeight()
281+
header_opt <- client.getBlockHeader(height)
282+
} yield (height, header_opt)) {
279283
case Failure(t) => GetBlockCountFailed(t)
280-
case Success(currentHeight) => PublishBlockHeight(currentHeight)
284+
case Success((currentHeight, header_opt)) => PublishBlockHeight(currentHeight, header_opt)
281285
}
282286
// TODO: beware of the herd effect
283287
KamonExt.timeFuture(Metrics.NewBlockCheckConfirmedDuration.withoutTags()) {
@@ -288,10 +292,10 @@ private class ZmqWatcher(nodeParams: NodeParams, blockHeight: AtomicLong, client
288292
}
289293
Behaviors.same
290294

291-
case PublishBlockHeight(currentHeight) =>
295+
case PublishBlockHeight(currentHeight, header_opt) =>
292296
log.debug("setting blockHeight={}", currentHeight)
293297
blockHeight.set(currentHeight.toLong)
294-
context.system.eventStream ! EventStream.Publish(CurrentBlockHeight(currentHeight))
298+
context.system.eventStream ! EventStream.Publish(CurrentBlockHeight(currentHeight, header_opt))
295299
Behaviors.same
296300

297301
case TriggerEvent(replyTo, watch, event) =>

eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/rpc/BitcoinCoreClient.scala

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,13 @@ package fr.acinq.eclair.blockchain.bitcoind.rpc
1919
import fr.acinq.bitcoin.psbt.Psbt
2020
import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey
2121
import fr.acinq.bitcoin.scalacompat._
22-
import fr.acinq.bitcoin.{Bech32, Block, SigHash}
22+
import fr.acinq.bitcoin.{Bech32, Block, BlockHeader, SigHash}
2323
import fr.acinq.eclair.ShortChannelId.coordinates
2424
import fr.acinq.eclair.blockchain.OnChainWallet
2525
import fr.acinq.eclair.blockchain.OnChainWallet.{FundTransactionResponse, MakeFundingTxResponse, OnChainBalance, ProcessPsbtResponse}
2626
import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.{GetTxWithMetaResponse, UtxoStatus, ValidateResult}
2727
import fr.acinq.eclair.blockchain.fee.{FeeratePerKB, FeeratePerKw}
2828
import fr.acinq.eclair.crypto.keymanager.OnChainKeyManager
29-
import fr.acinq.eclair.json.SatoshiSerializer
3029
import fr.acinq.eclair.transactions.Transactions
3130
import fr.acinq.eclair.wire.protocol.ChannelAnnouncement
3231
import fr.acinq.eclair.{BlockHeight, TimestampSecond, TxCoordinates}
@@ -655,6 +654,13 @@ class BitcoinCoreClient(val rpcClient: BitcoinJsonRPCClient, val onChainKeyManag
655654
case JInt(count) => BlockHeight(count.toLong)
656655
}
657656

657+
def getBlockHeader(height: BlockHeight)(implicit ec: ExecutionContext): Future[Option[BlockHeader]] =
658+
rpcClient.invoke("getblockhash", height.toLong)
659+
.collect { case JString(blockHash) => blockHash }
660+
.flatMap(blockHash => rpcClient.invoke("getblockheader", blockHash, /* verbose */ false))
661+
.collect { case JString(blockHeader) => Some(BlockHeader.read(blockHeader)) }
662+
.recover { case _ => None }
663+
658664
def validate(c: ChannelAnnouncement)(implicit ec: ExecutionContext): Future[ValidateResult] = {
659665
val TxCoordinates(blockHeight, txIndex, outputIndex) = coordinates(c.shortChannelId)
660666
for {

eclair-core/src/main/scala/fr/acinq/eclair/io/PeerConnection.scala

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ package fr.acinq.eclair.io
1818

1919
import akka.actor.{ActorRef, FSM, OneForOneStrategy, PoisonPill, Props, Stash, SupervisorStrategy, Terminated}
2020
import akka.event.Logging.MDC
21-
import fr.acinq.bitcoin.scalacompat.{BlockHash, ByteVector32}
21+
import fr.acinq.bitcoin.BlockHeader
22+
import fr.acinq.bitcoin.scalacompat.BlockHash
2223
import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey
2324
import fr.acinq.eclair.Logs.LogCategory
2425
import fr.acinq.eclair.crypto.Noise.KeyPair
@@ -28,7 +29,7 @@ import fr.acinq.eclair.remote.EclairInternalsSerializer.RemoteTypes
2829
import fr.acinq.eclair.router.Router._
2930
import fr.acinq.eclair.wire.protocol
3031
import fr.acinq.eclair.wire.protocol._
31-
import fr.acinq.eclair.{FSMDiagnosticActorLogging, FeatureCompatibilityResult, Features, InitFeature, Logs, TimestampMilli, TimestampSecond}
32+
import fr.acinq.eclair.{BlockHeight, FSMDiagnosticActorLogging, Features, InitFeature, Logs, TimestampMilli, TimestampSecond}
3233
import scodec.Attempt
3334
import scodec.bits.ByteVector
3435

@@ -103,14 +104,16 @@ class PeerConnection(keyPair: KeyPair, conf: PeerConnection.Conf, switchboard: A
103104
}
104105

105106
when(BEFORE_INIT) {
106-
case Event(InitializeConnection(peer, chainHash, localFeatures, doSync), d: BeforeInitData) =>
107+
case Event(InitializeConnection(peer, chainHash, currentBlockHeight, currentBlockHeader, localFeatures, doSync), d: BeforeInitData) =>
107108
d.transport ! TransportHandler.Listener(self)
108109
Metrics.PeerConnectionsConnecting.withTag(Tags.ConnectionState, Tags.ConnectionStates.Initializing).increment()
109110
log.debug(s"using features=$localFeatures")
110-
val localInit = d.pendingAuth.address match {
111-
case remoteAddress if !d.pendingAuth.outgoing && conf.sendRemoteAddressInit && NodeAddress.isPublicIPAddress(remoteAddress) => protocol.Init(localFeatures, TlvStream(InitTlv.Networks(chainHash :: Nil), InitTlv.RemoteAddress(remoteAddress)))
112-
case _ => protocol.Init(localFeatures, TlvStream(InitTlv.Networks(chainHash :: Nil)))
113-
}
111+
val tlvs = TlvStream(Set(
112+
Some(InitTlv.Networks(chainHash :: Nil)),
113+
if (!d.pendingAuth.outgoing && conf.sendRemoteAddressInit && NodeAddress.isPublicIPAddress(d.pendingAuth.address)) Some(InitTlv.RemoteAddress(d.pendingAuth.address)) else None,
114+
Some(InitTlv.LatestBlockHeader(currentBlockHeight, currentBlockHeader))
115+
).flatten[InitTlv])
116+
val localInit = protocol.Init(localFeatures, tlvs)
114117
d.transport ! localInit
115118
startSingleTimer(INIT_TIMER, InitTimeout, conf.initTimeout)
116119
unstashAll() // unstash remote init if it already arrived
@@ -130,6 +133,8 @@ class PeerConnection(keyPair: KeyPair, conf: PeerConnection.Conf, switchboard: A
130133

131134
log.info(s"peer is using features=${remoteInit.features}, networks=${remoteInit.networks.mkString(",")}")
132135
remoteInit.remoteAddress_opt.foreach(address => log.info("peer reports that our IP address is {} (public={})", address.toString, NodeAddress.isPublicIPAddress(address)))
136+
remoteInit.currentBlockHeight_opt.foreach(height => log.info("peer reports that the current block height is {}", height.toLong))
137+
remoteInit.currentBlockHeader_opt.foreach(header => log.info("peer reports that the current block header is {}", ByteVector(BlockHeader.write(header)).toHex))
133138

134139
val featureGraphErr_opt = Features.validateFeatureGraph(remoteInit.features)
135140
val featuresCompatibilityResult = Features.testCompatible(d.localInit.features, remoteInit.features)
@@ -574,7 +579,7 @@ object PeerConnection {
574579
def outgoing: Boolean = remoteNodeId_opt.isDefined // if this is an outgoing connection, we know the node id in advance
575580
}
576581
case class Authenticated(peerConnection: ActorRef, remoteNodeId: PublicKey, outgoing: Boolean) extends RemoteTypes
577-
case class InitializeConnection(peer: ActorRef, chainHash: BlockHash, features: Features[InitFeature], doSync: Boolean) extends RemoteTypes
582+
case class InitializeConnection(peer: ActorRef, chainHash: BlockHash, currentBlockHeight: BlockHeight, currentBlockHeader: BlockHeader, features: Features[InitFeature], doSync: Boolean) extends RemoteTypes
578583
case class ConnectionReady(peerConnection: ActorRef, remoteNodeId: PublicKey, address: NodeAddress, outgoing: Boolean, localInit: protocol.Init, remoteInit: protocol.Init) extends RemoteTypes
579584

580585
sealed trait ConnectionResult extends RemoteTypes

eclair-core/src/main/scala/fr/acinq/eclair/io/Switchboard.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ class Switchboard(nodeParams: NodeParams, peerFactory: Switchboard.PeerFactory)
102102
val hasChannels = peersWithChannels.contains(authenticated.remoteNodeId)
103103
// if the peer is whitelisted, we sync with them, otherwise we only sync with peers with whom we have at least one channel
104104
val doSync = nodeParams.syncWhitelist.contains(authenticated.remoteNodeId) || (nodeParams.syncWhitelist.isEmpty && hasChannels)
105-
authenticated.peerConnection ! PeerConnection.InitializeConnection(peer, nodeParams.chainHash, features, doSync)
105+
authenticated.peerConnection ! PeerConnection.InitializeConnection(peer, nodeParams.chainHash, nodeParams.currentBlockHeight, nodeParams.currentBlockHeader, features, doSync)
106106
if (!hasChannels && !authenticated.outgoing) {
107107
incomingConnectionsTracker ! TrackIncomingConnection(authenticated.remoteNodeId)
108108
}

eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/AsyncPaymentTriggerer.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ private class AsyncPaymentTriggerer(context: ActorContext[Command]) {
113113
case (remoteNodeId, peer) => peer.cancel(paymentHash).map(peer1 => remoteNodeId -> peer1)
114114
}
115115
watching(peers1)
116-
case WrappedCurrentBlockHeight(CurrentBlockHeight(currentBlockHeight)) =>
116+
case WrappedCurrentBlockHeight(CurrentBlockHeight(currentBlockHeight, _)) =>
117117
val peers1 = peers.flatMap {
118118
case (remoteNodeId, peer) => peer.update(currentBlockHeight).map(peer1 => remoteNodeId -> peer1)
119119
}

eclair-core/src/main/scala/fr/acinq/eclair/remote/EclairInternalsSerializer.scala

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,8 +89,8 @@ object EclairInternalsSerializer {
8989
val messageRouteParamsCodec: Codec[MessageRouteParams] = (
9090
("maxRouteLength" | int32) ::
9191
(("baseFactor" | double) ::
92-
("ageFactor" | double) ::
93-
("capacityFactor" | double)).as[Graph.MessagePath.WeightRatios]).as[MessageRouteParams]
92+
("ageFactor" | double) ::
93+
("capacityFactor" | double)).as[Graph.MessagePath.WeightRatios]).as[MessageRouteParams]
9494

9595
val routerConfCodec: Codec[RouterConf] = (
9696
("watchSpentWindow" | finiteDurationCodec) ::
@@ -150,6 +150,8 @@ object EclairInternalsSerializer {
150150
def initializeConnectionCodec(system: ExtendedActorSystem): Codec[PeerConnection.InitializeConnection] = (
151151
("peer" | actorRefCodec(system)) ::
152152
("chainHash" | blockHash) ::
153+
("currentBlockHeight" | blockHeight) ::
154+
("currentBlockHeader" | blockHeader) ::
153155
("features" | variableSizeBytes(uint16, initFeaturesCodec)) ::
154156
("doSync" | bool(8))).as[PeerConnection.InitializeConnection]
155157

eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/CommonCodecs.scala

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
package fr.acinq.eclair.wire.protocol
1818

19+
import fr.acinq.bitcoin.BlockHeader
1920
import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey}
2021
import fr.acinq.bitcoin.scalacompat.{BlockHash, ByteVector32, ByteVector64, Satoshi, Transaction, TxHash, TxId}
2122
import fr.acinq.eclair.blockchain.fee.FeeratePerKw
@@ -170,6 +171,8 @@ object CommonCodecs {
170171

171172
val txCodec: Codec[Transaction] = bytes.xmap(d => Transaction.read(d.toArray), d => Transaction.write(d))
172173

174+
val blockHeader: Codec[BlockHeader] = bytes(80).xmap(b => BlockHeader.read(b.toArray), h => ByteVector(BlockHeader.write(h)))
175+
173176
def zeropaddedstring(size: Int): Codec[String] = fixedSizeBytes(size, utf8).xmap(s => s.takeWhile(_ != '\u0000'), s => s)
174177

175178
/**

0 commit comments

Comments
 (0)