Skip to content

Commit d4a498c

Browse files
authored
Use bitcoinheaders.net v2 format (#2787)
The format used by bitcoinheaders.net is changing to use whole bytes instead of nibles, which is easier to parse. We start using the v2 format exclusively, which will allow deprecating the previous format. Fixes #2786
1 parent f0cb58a commit d4a498c

File tree

1 file changed

+36
-30
lines changed

1 file changed

+36
-30
lines changed

eclair-core/src/main/scala/fr/acinq/eclair/blockchain/watchdogs/HeadersOverDns.scala

Lines changed: 36 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import fr.acinq.eclair.BlockHeight
2828
import fr.acinq.eclair.blockchain.watchdogs.BlockchainWatchdog.BlockHeaderAt
2929
import fr.acinq.eclair.blockchain.watchdogs.Monitoring.{Metrics, Tags}
3030
import org.slf4j.Logger
31-
import scodec.bits.BitVector
31+
import scodec.bits.ByteVector
3232

3333
/**
3434
* Created by t-bast on 29/09/2020.
@@ -57,7 +57,7 @@ object HeadersOverDns {
5757
case Block.LivenetGenesisBlock.hash =>
5858
// We try to get the next 10 blocks; if we're late by more than 10 blocks, this is bad, no need to even look further.
5959
(currentBlockHeight.toLong until currentBlockHeight.toLong + 10).foreach(blockHeight => {
60-
val hostname = s"$blockHeight.${blockHeight / 10000}.bitcoinheaders.net"
60+
val hostname = s"v2.$blockHeight.${blockHeight / 10_000}.bitcoinheaders.net"
6161
IO(Dns)(context.system.classicSystem).tell(DnsProtocol.resolve(hostname, DnsProtocol.Ip(ipv4 = false, ipv6 = true)), dnsAdapters)
6262
})
6363
collect(replyTo, currentBlockHeight, Set.empty, 10)
@@ -75,7 +75,7 @@ object HeadersOverDns {
7575
Behaviors.receiveMessage {
7676
case WrappedDnsResolved(response) =>
7777
val blockHeader_opt = for {
78-
blockHeight <- parseBlockCount(response)(context.log)
78+
blockHeight <- parseBlockHeight(response)(context.log)
7979
blockHeader <- parseBlockHeader(response)(context.log)
8080
} yield BlockHeaderAt(blockHeight, blockHeader)
8181
val received1 = blockHeader_opt match {
@@ -103,42 +103,48 @@ object HeadersOverDns {
103103
collect(replyTo, currentBlockHeight, received, remaining)
104104
}
105105

106-
private def parseBlockCount(response: DnsProtocol.Resolved)(implicit log: Logger): Option[BlockHeight] = {
107-
response.name.split('.').headOption match {
108-
case Some(blockHeight) => blockHeight.toLongOption.map(l => BlockHeight(l))
109-
case None =>
110-
log.error("bitcoinheaders.net response did not contain block count: {}", response)
111-
None
106+
private def parseBlockHeight(response: DnsProtocol.Resolved)(implicit log: Logger): Option[BlockHeight] = {
107+
// v2.height.(height / 10000).bitcoinheaders.net
108+
val parts = response.name.split('.')
109+
if (parts.length < 2) {
110+
log.error("bitcoinheaders.net response did not contain block height: {}", response)
111+
None
112+
} else {
113+
parts(1).toLongOption.map(l => BlockHeight(l))
112114
}
113115
}
114116

115117
private def parseBlockHeader(response: DnsProtocol.Resolved)(implicit log: Logger): Option[BlockHeader] = {
116118
val addresses = response.records.collect { case record: AAAARecord => record.ip.getAddress }
117119
if (addresses.nonEmpty) {
118-
val countOk = addresses.length == 6
119-
// addresses must be prefixed with 0x2001
120-
val prefixOk = addresses.forall(_.startsWith(Array(0x20.toByte, 0x01.toByte)))
121-
// the first nibble after the prefix encodes the order since nameservers often reorder responses
122-
val orderOk = addresses.map(a => a(2) & 0xf0).toSet == Set(0x00, 0x10, 0x20, 0x30, 0x40, 0x50)
123-
if (countOk && prefixOk && orderOk) {
124-
val header = addresses.sortBy(a => a(2)).foldLeft(BitVector.empty) {
125-
case (current, address) =>
126-
// The first address contains an additional 0x00 prefix
127-
val toDrop = if (current.isEmpty) 28 else 20
128-
current ++ BitVector(address).drop(toDrop)
129-
}.bytes
130-
header.length match {
131-
case 80 => Some(BlockHeader.read(header.toArray))
132-
case _ =>
133-
log.error("bitcoinheaders.net response did not contain block header (invalid length): {}", response)
134-
None
135-
}
136-
} else {
137-
log.error("invalid response from bitcoinheaders.net: {}", response)
120+
// From https://bitcoinheaders.net/:
121+
// All headers are encoded with an arbitrary one byte prefix (which you must ignore, as it may change in the
122+
// future), followed by a 0-indexed order byte (as nameservers often reorder responses). Entries are then prefixed
123+
// by a single version byte (currently version 1) and placed into the remaining bytes of the IPv6 addresses.
124+
// For example with the genesis block:
125+
// v2.0.0.bitcoinheaders.net. 604800 IN AAAA 2603:7b12:b27a:c72c:3e67:768f:617f:c81b
126+
// v2.0.0.bitcoinheaders.net. 604800 IN AAAA 2600:101::
127+
// v2.0.0.bitcoinheaders.net. 604800 IN AAAA 2601::
128+
// v2.0.0.bitcoinheaders.net. 604800 IN AAAA 2602::3b:a3ed:fd7a
129+
// v2.0.0.bitcoinheaders.net. 604800 IN AAAA 2605:ab5f:49ff:ff00:1d1d:ac2b:7c00:0
130+
// v2.0.0.bitcoinheaders.net. 604800 IN AAAA 2604:c388:8a51:323a:9fb8:aa4b:1e5e:4a29
131+
// Which decodes to 0100000000000000000000000000000000000000000000000000000000000000000000003ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4a29ab5f49ffff001d1dac2b7c.
132+
val data = addresses
133+
.filter(_.length >= 2)
134+
.map(_.tail) // the first byte is a prefix that we must ignore
135+
.sortBy(_.head) // the second byte is a 0-indexed order byte
136+
.flatMap(_.tail) // the remaining bytes contain the header chunks
137+
if (data.length < 81) {
138+
log.error("bitcoinheaders.net response did not contain a 1-byte version followed by a block header: {}", ByteVector(data).toHex)
138139
None
140+
} else if (data.head != 0x01) {
141+
log.error("bitcoinheaders.net response is not using version 1: version={}", data.head)
142+
None
143+
} else {
144+
Some(BlockHeader.read(data.tail.take(80).toArray))
139145
}
140146
} else {
141-
// Instead of not resolving the DNS request when block height is unknown, bitcoinheaders sometimes returns an empty response.
147+
// When the block height is unknown, bitcoinheaders returns an empty response.
142148
None
143149
}
144150
}

0 commit comments

Comments
 (0)