A Bitcoin Node for Identity Infrastructure

Sep 11, 2026

Introduction

Each post in this series about resolution mentions the same step: the resolver "discovers Beacon Signals on Bitcoin." That phrase hides one of the most important operational decisions in a did:btcr2 deployment.

To discover Beacon Signals, a resolver finds the transactions that spend from the Beacon Addresses of a DID. That is a query of address history, and Bitcoin Core intentionally keeps no index for this query. Your solution to this gap sets what you trust. The answer is not obvious, so people often do not think about it until they already built on top of it.

This post describes the options and the hardware and operational cost of each option. It also shows which items the spec decides and which items it leaves to you.

Why Bitcoin Core Keeps No Address Index

Bitcoin Core keeps a UTXO set, not an address index. The UTXO set records which outputs are unspent now. Core has no map from an address to each transaction that used that address. This is a design choice, not an omission. An address index is large and costs effort to maintain, and chain validation does not need it. Validation of the chain is the main job of the software.

Bitcoin Core contributors discussed an address index in PR #14053, and its author closed it without a merge in January 2021. One objection in that discussion was that it is a bad idea to build infrastructure that depends on fully indexed chain data. Other comments noted that external tools such as electrs already do this work.

txindex=1 does not solve this problem. It maps transaction IDs to transactions, so you can look up a transaction that you already know. A search by address for transactions that you do not know is a different query.

But Core has some tools for this query, and it is important to be precise about the gap. Since v25.0, Core has the scanblocks RPC. This RPC takes descriptors and scans the built-in compact block filter index (-blockfilterindex=1). It returns the hashes of the blocks that possibly contain a match. BIP158 basic filters include "the previous output script (the script being spent) for each input." Thus a spend from a Beacon Address can match, and that is the shape of Beacon Signal discovery.

The filters are probabilistic, so some candidate blocks do not contain a match. The filter_false_positives option of scanblocks removes these blocks, but the scan becomes slower. You can also get the candidate blocks and check them yourself. Since v29.0, the getdescriptoractivity RPC does that check: it returns the spend and receive activity for your descriptors in the blocks that you name. The Core documentation warns that each of these calls can take several minutes.

Thus Core can answer the question on a node that you validated yourself, with no third party. But Core has no index that answers at request latency. For this reason, scanblocks is a tool for one-time setup work, not a replacement for an Electrum-protocol server.

The resolve algorithm of the btcr2 specification makes the same distinction in two short sentences, and it does not give a reason. It RECOMMENDS an indexed Bitcoin RPC service and names electrs and Esplora as examples. The reason that the spec does not state is the reason above: Core keeps no address index. The spec also states that implementations MAY traverse blocks from the genesis block instead.

The second path is easy to overlook, so we examine it here. Genesis traversal is not an emergency fallback. It is an explicitly permitted resolution strategy, and it gives full self-sovereignty. Because the spec permits it, the next section describes a real choice, not a theoretical one.

The Trust Decision

Suppose that you connect your resolver to a public Esplora instance. Resolution works, it is fast, and you operate no infrastructure. But you also accept a condition that is easy to miss.

You accept the view of the chain that the instance gives you. Specifically, when you ask "which transactions spend from this Beacon Address," you accept that the list you get back is complete.

You cannot verify that completeness from the answer. Many people miss this point, because btcr2 is otherwise thoroughly cryptographic, and it is natural to assume that the cryptography also covers this. It does not. The resolver verifies each update payload that you receive. The Data Integrity proof must be valid, the sourceHash must match the document that you hold, and the patched result must hash to targetHash. These checks confirm that the updates you received are genuine.

None of these checks detects an update that you did not receive.

An indexer that omits one Beacon Signal from its response causes a resolution that looks internally consistent but is wrong. The wrong result can also look exactly like the correct answer. Suppose that the withheld signal is the most recent one. Then you resolve to a stale document with a key set that the controller already replaced, and nothing in the response shows a problem. You can then accept a signature from a key that the controller retired last month.

Compare this with the gap case from the sidecar post. An absent intermediate update leaves a hole in the version sequence. The hole causes LATE_PUBLISHING, or MISSING_UPDATE_DATA if the resolver cannot get the payload at all. In both cases, the resolver fails closed. A withheld latest update causes silence, and silence looks the same as "no further updates." The first failure is visible, and the second failure is not.

Thus the question "who operates your indexer" is really the question "who decides which Beacon Signals you see." A third-party indexer can censor your view of the chain, by accident or on purpose. Your own index closes that gap, because you build it from blocks that your own node validated.

The specification does not require that you operate your own index. But it does require completeness: "Resolvers MUST enforce strict ordering of the BTCR2 Updates and process all relevant Beacon Signals" (Security Considerations). The spec does not say where those signals come from.

Many deployments will decide, for good reasons, that a reputable indexer is sufficient for their threat model. But one of the stated features of btcr2 is non-repudiation: "every BTCR2 DID is anchored on a single, canonical, immutable history for the lifetime of the DID."

The spec also names anti-censorship as its primary motivation. Both properties are only as strong as your confidence that you see the whole history. If you give your view of the chain to a third party, you also give these properties to that party.

Three Architectures

Indexed RPC from a third party

Connect to a public Esplora or Electrum server. This option needs no infrastructure, it is available immediately, and it is free.

This option is appropriate for development, tests, low-stakes resolution, and each case where you also accept the word of a third party for other facts. It is not appropriate as the sole source of truth for a function that your business depends on, for the reason above. It also has a second cost: each query tells the operator which Beacon Addresses you watch and when.

For a Singleton Beacon, that address maps to a single DID document, so you give the operator your resolution pattern. If you use a public server, use several servers from independent operators and compare the results. A disagreement between two indexers is a signal that you do not get otherwise. But each additional operator increases the disclosure, so this is a trade-off, not a free improvement.

Indexed RPC on your own node

Operate Bitcoin Core, and operate electrs or Esplora next to it. Connect the resolver to your own endpoint. Your indexer builds the index from blocks that your own node validated, so a remote party cannot control your view.

This is a common production solution, and it is not complex. It is one more service in your infrastructure. It has the same operational needs as other stateful services: monitors and alerts, free disk space, and tests before each upgrade.

Many people miss one requirement: you cannot use a pruned node. The indexer reads each historical block to build an address index. A pruned node deletes those blocks, so the indexer has nothing to read. Plan for the full chain, which was about 770 GB in September 2026 and grows by about 80 GB each year. Bitcoin Core 31.1 uses 856 GiB as the minimum free space for the data directory of a mainnet node that keeps all blocks. The Core release process sets this value to the size of a synced data directory plus 5 to 10% overhead.

Add the index to that total. The index is also large, and its size changes by more than a factor of ten between indexers. Check the current numbers before you choose the volume size. Do not trust a number from a blog post, and that includes this post.

The initial sync takes the most time. The node must validate the full chain, and then the indexer must build the index. On modest hardware, the node sync takes days: the RaspiBolt FAQ gives 3 to 5 days on a Pi 5 with a fast SSD. The index build is shorter: the electrs usage guide gives about 2 hours in July 2026 on a 6-core machine with an NVMe drive. The initial sync is a one-time cost. Provision the node well before you need it.

Genesis traversal

Read each block, look for spends from the Beacon Addresses that you need, and keep no index.

The advantage is that you trust nothing except your own node. You also do not need an address index, with its storage and its maintenance. The disadvantage is that each resolution of a DID that the resolver did not see before needs a scan of the full chain. That scan is too slow for a per-request operation.

The compact block filters from the earlier section decrease this cost, and they need only one small index. With -blockfilterindex=1, a full traversal becomes a filter scan plus a small number of block fetches. The trust model stays the same, and the I/O is much less. The filter index is also much smaller than an address index: the Bitcoin Core 0.19 release notes gave about 4 GiB for it in 2019. But each scan still takes minutes, not milliseconds. Thus the filters decrease the cost of this architecture, but they do not change how it works.

This architecture is a good fit for a resolver that handles a small, known, and stable set of DIDs. Scan once, cache the results, and then follow the chain tip one block at a time. Examples are the DIDs of your own organization, the member DIDs of a consortium, or a supply chain with a fixed set of participants. For such a set, the scan is a setup step, not a part of each request. You also remove a trusted component from your architecture at no extra cost.

The Option That Is Not in the Spec

A fourth approach fits an important case, and it needs no index: watch-only descriptor wallets in Bitcoin Core.

A request for the history of an arbitrary address costs Core a scan, from genesis or with filters. But it costs Core almost nothing to track addresses that you give it in advance.

Create a wallet with private keys disabled (createwallet). Import the Beacon Addresses into it as a watch-only descriptor (importdescriptors). The import rescans the chain from the timestamp that you set. You can also run rescanblockchain once to find the earlier history. After that, Core tracks the addresses as a normal wallet does.

This works because of an asymmetry. A resolver for arbitrary DIDs learns the Beacon Addresses from the DID document at resolution time. Thus each new DID needs its own historical scan, and that is genesis traversal in a different form. A controller that monitors its own DIDs knows the addresses in advance, and it knows that the set is small and stable. The controller pays for the scan once at setup, and after that it only watches the chain tip. Both use the same Core function, but only the controller can spread the cost over time.

Thus this approach is a low-cost way to follow a recommendation from the rotation post: monitor your own Beacon Addresses. An alarm on unexpected signals at these addresses is one of the most valuable alarms in a btcr2 deployment. An unexpected signal means that someone publishes updates that you did not write. You do not need electrs for this alarm. You need a Core node, a descriptor import, and a process that detects new wallet transactions.

The rescan is a one-time cost, and it increases with the number of blocks that you must scan. It is the only expensive part. The rescan is much faster if the block filter index is available (rescanblockchain). On a pruned node, you can rescan only the blocks that the node kept.

Hardware

Many people think that Bitcoin node hardware must be powerful and expensive. People repeat this claim, but the data does not support it. The actual requirements are as follows.

A Raspberry Pi 5 with 8 GB of RAM and a 2 TB SSD can operate a full node. A small mini PC with 8 to 16 GB of RAM gives better performance than the Pi at a moderate price. As of September 2026, the list price of a Raspberry Pi 5 with 8 GB is $175. This price comes after three price increases that Raspberry Pi linked to memory costs (December 2025, February 2026, April 2026). With a 2 TB SSD, a complete node costs a few hundred US dollars, not thousands.

Storage is a large part of the cost, and the size that you need depends on the indexer. romanz/electrs states a low index storage overhead of about 10% of the chain, so a 2 TB NVMe drive gives space for years of growth. Blockstream/electrs, the backend of Esplora, states that its indexes needed 610 GB in June 2020. The chain was then about 280 GB. The README also warns that compaction needs about double that amount as free space. Its --lightmode option decreases the storage by about 50%, but lookups become slower.

Thus, for Esplora, calculate the storage as a multiple of the chain size, not as a fraction of it. 2 TB is sufficient for romanz/electrs. It is not sufficient for Esplora.

These items are more important than the CPU:

  • Disk type. Use an SSD. The RaspiBolt FAQ gives 3 to 5 days for the initial sync on a Pi 5 with a fast SSD. It also states that a hard disk drive can make the sync take more than a week.
  • Free disk space. The chain only grows. Provision for years, not months. If you must expand the volume later, you need a maintenance window that you can avoid now.
  • RAM for the indexer. electrs uses more memory for the first index build than for normal operation. The romanz/electrs README states "Low CPU & memory usage (after initial indexing)." If you choose the RAM size for normal operation only, the first index build can run out of memory at a bad time.

Two low-cost nodes in different locations give you redundancy and a cross-check. For infrastructure that decides if identity assertions are valid, this is a reasonable cost.

Connect the Resolver to the Node

The reference implementation has clients for both interfaces in @did-btcr2/bitcoin: Bitcoin Core RPC and Esplora REST. The library builds each request, but it does not send the request. It gives the request to an injectable executor, so the same code runs against a real node, a mock, or a recorded fixture.

We recommend that you keep this structure in your own integration. It gives you these benefits:

  • Test without a node. Recorded responses test the resolution logic, and you do not need a synced chain in CI.
  • Change the backend. If you did not hard-code the client, a move from a public indexer to your own indexer is a configuration change. If you hard-coded the client, the move needs a refactor.
  • Measure at the boundary. A wrapper executor gives you latency and error metrics for each Bitcoin call, and you do not change the resolution code. If resolution is slow, these metrics are the first data that you will need.

The @did-btcr2/api package also has a signalDiscovery option with two modes (source). These modes match two of the architectures above:

  • indexer is the default. It reads the transaction list for each Beacon Address from an Esplora-compatible REST backend.
  • fullnode scans each block from genesis over Bitcoin Core RPC, and it needs no third-party index. It needs -txindex=1 and Bitcoin Core 25 or later. The source comment states that this scan is "only practical on regtest."

The library packages get connection parameters as explicit arguments, and they do not read environment variables. Only the CLI reads environment variables. The CLI resolves BTCR2_* variables in a documented order: flags first, then environment variables, then the config file.

But @did-btcr2/api has default endpoints for each network. On mainnet, the default is a public mempool.space Esplora endpoint, and the API merges your overrides on top of it. This default is a good way to start. But it is also the third-party indexer architecture from earlier in this post. Make it a conscious decision, and do not simply accept the default.

Also, the network name and the endpoint are separate fields. Nothing in the transport verifies that the host serves the chain that you named. For example, a regtest endpoint with the label mainnet causes no error. Validate that pair in your own configuration checks, because the library does not do it.

Operations

These items become important over time:

Monitor the sync height, not only liveness. A node that answers RPC calls but is twenty blocks behind is worse than a node that is down. The stalled node gives wrong answers, and it does not show that they are wrong. Set an alert on the age of the chain tip.

Monitor the index lag separately. electrs can fall behind the node that it indexes. If the index is behind the tip, a resolver that queries it gets a stale answer with no sign that the answer is stale.

Test upgrades on a copy. Index formats change between indexer versions, and a rebuild can take a long time. For example, one electrs release changed the database schema and caused a reindex after upgrade. Find such changes on a copy, not in a production upgrade window.

Monitor free disk space. This task is simple, but it is important: Bitcoin Core stops with a fatal error if disk space is too low.

Plan the resync. Assume that you will need one at some time, after data corruption or a version migration. Know how long it takes and which system serves requests in that time. With a second node, a resync causes no outage.

Conclusion

In a btcr2 deployment, the infrastructure question is not the cost of a node. Node hardware costs a few hundred dollars, and the software is mature. The real question is whose view of the chain you accept. Make that decision on purpose. Do not take it from the endpoint in a quick-start guide.

A third-party indexer is a reasonable choice for many deployments, if it is a conscious choice. But do not believe that you have cryptographic assurance of a complete history when a remote service decides which Beacon Signals you see. The cryptography is real, but it does not cover that risk.

Next week: the other half of correct resolution on a live chain. Beacon Signals arrive in blocks, and a reorg can replace blocks. A resolver must decide how many confirmations make an update final.

Jintek LLC