Run Chainhook as a service

Learn how to run Chainhook as a service to evaluate your "if this, then that" predicates against the Bitcoin and Stacks blockchains.

In order to build out a more robust and secure web app, you can run Chainhook as a service to stream your events continously to a server you designate.

In this guide, you will learn how to:

  1. Configure an existing Bitcoin node to work with Chainhook.
  2. Generate a Chainhook predicate to target specific transactions.
  3. Scan the Bitcoin blockchain for transactions that match your predicate.
  4. Initiate a Chainhook service to watch for matching transactions.
  5. Dynamically register your predicate with Chainhook.

Configure Chainhook

In this section, you will configure Chainhook to match the network configurations with the bitcoin config file. First, install the latest version of Chainhook.

Next, you will generate a Chainhook.toml file to connect Chainhook with your bitcoind node. Navigate to the directory where you want to generate the Chainhook.toml file and use the following command in your terminal:

Terminal
chainhook config generate --mainnet

Several network parameters in the generated Chainhook.toml configuration file need to match those in the bitcoin.conf file created earlier in the setting up a Bitcoin node section. Update the following parameters accordingly:

  1. Update bitcoind_rpc_username with the username set for rpcuser in bitcoin.conf.
  2. Update bitcoind_rpc_password with the password set for rpcpassword in bitcoin.conf.
  3. Update bitcoind_rpc_url with the same host and port used for rpcport in bitcoin.conf.

Additionally, if you want to receive events from the configured Bitcoin node, substitute stacks_node_rpc_url with bitcoind_zmq_url, as follows:

[storage]
working_dir = "cache"

# The http API allows you to register / deregister predicates dynamically.
# This is disabled by default.

# [http_api]
# http_port = 20456
# database_uri = "redis://localhost:6379/"

[network]
mode = "mainnet"
bitcoind_rpc_url = "http://localhost:8332"
bitcoind_rpc_username = "devnet"
bitcoind_rpc_password = "devnet"
# Bitcoin block events can be received by Chainhook
# either through a Bitcoin node's ZeroMQ interface,
# or through the Stacks node. The Stacks node is
# used by default:
# stacks_node_rpc_url = "http://localhost:20443"
# but zmq can be used instead:
bitcoind_zmq_url = "tcp://0.0.0.0:18543"

[limits]
max_number_of_bitcoin_predicates = 100
max_number_of_concurrent_bitcoin_scans = 100
max_number_of_stacks_predicates = 10
max_number_of_concurrent_stacks_scans = 10
max_number_of_processing_threads = 16
max_number_of_networking_threads = 16
max_caching_memory_size_mb = 32000

[[event_source]]
tsv_file_url = "https://archive.hiro.so/mainnet/stacks-blockchain-api/mainnet-stacks-blockchain-api-latest"

Here is a table of the relevant parameters this guide changes in our configuration files.

bitcoin.confChainhook.toml
rpcuserbitcoind_rpc_username
rpcpasswordbitcoind_rpc_password
rpcportbitcoind_rpc_url
zmqpubhashblockbitcoind_zmq_url

Scan the blockchain based on predicates

Now that your bitcoind and Chainhook configurations are complete, you can define the Chainhook predicate scope you would like to scan for.

These predicates are where you specify the if_this / then_that pattern to trigger Chainhook to deliver a result (either a file appendation or an HTTP POST request).

Appending events to a file

To generate a sample JSON file with predicates, execute the following command in your terminal:

Terminal
chainhook predicates new stacking-pool.json --bitcoin

Replace the contents of the stacking-pool.json file with the following:

{
  "chain": "bitcoin",
  "uuid": "1",
  "name": "Stacking Pool",
  "version": 1,
  "networks": {
    "mainnet": {
      "start_block": 801500,
      "end_block": 802000,
      "if_this": {
        "scope": "outputs",
        "p2wpkh": {
          "equals": "bc1qs0kkdpsrzh3ngqgth7mkavlwlzr7lms2zv3wxe"
        }
      },
      "then_that": {
        "file_append": {
          "path": "bitcoin-transactions.txt"
        }
      }
    }
  }
}

This example demonstrates scanning a portion of the Bitcoin blockchain to capture specific outputs from a Bitcoin address associated with a Stacking pool, in this case Friedgar Pool. Notice the then_that predicate specifying a file_append.

You can get blockchain height and current block by referring to the Stacks Explorer.

Now, use the following command to scan the blocks based on the predicates defined in the stacking-pool.json file.

Terminal
chainhook predicates scan stacking-pool.json --config-path=./Chainhook.toml

The output of the above command will be a text file bitcoin-transactions.txt generated based on the predicate definition.

Sending events to an API endpoint

Now you will generate another sample predicate, but this time you will send the payload to an API endpoint:

Terminal
chainhook predicates new stacking-pool-api.json --bitcoin

Replace the contents of the stacking-pool-api.json file with the following:

{
  "chain": "bitcoin",
  "uuid": "2",
  "name": "Stacking Pool (API)",
  "version": 1,
  "networks": {
    "mainnet": {
      "start_block": 801500,
      "if_this": {
        "scope": "outputs",
        "p2wpkh": {
          "equals": "bc1qs0kkdpsrzh3ngqgth7mkavlwlzr7lms2zv3wxe"
        }
      },
      "then_that": {
        "http_post": {
          "url": "http://localhost:3000/events",
          "authorization_header": "12345"
        }
      }
    }
  }
}

The start_block is a required field when using the http_post then_that predicate.

Once you are finished setting up your endpoint, use the following command to scan the blocks based on the predicates defined in the stacking-pool-api.json file.

Terminal
chainhook predicates scan stacking-pool-api.json --config-path=./Chainhook.toml

The above command posts events to the URL http://localhost:3000/events, mentioned in the JSON file.

Initiate Chainhook service

In the examples above, your Chainhook scanned historical blockchain data against predicates and delivered results. In this next section, you will learn how to set up a Chainhook that acts as an ongoing observer and event-streaming service.

You can start a Chainhook service with an existing predicate. You can also dynamically register new predicates by making an API call to your chainhook. In both of these instances, your predicates will be delivering their results to a server set up to receive results.

Initiate the chainhook service by passing the predicate path to the command as shown below:

Terminal
chainhook service start --predicate-path=stacking-pool-api.json --config-path=Chainhook.toml

The above command registers the predicate based on the predicate definition in the stacking-pool-api.json file.

Dynamically register predicates

You can also dynamically register new predicates with your Chainhook service. This means you can start a long-running process that exposes HTTP endpoints to register, deregister, and report on new predicates.

This section requires that you have Redis running locally. To install, refer to the Redis documentation.

First, ensure that the following lines of code are uncommented in the Chainhook.toml file to enable the predicate registration server:

[http_api]
http_port = 20456
database_uri = "redis://localhost:6379/"

If you have an instance of Redis running locally, you can now start the Chainhook service by running the following command:

Terminal
chainhook service start --config-path=Chainhook.toml

To dynamically register a new predicate, send a POST request to the running predicate registration server at localhost:20456/v1/chainhooks.

Terminal
curl -X POST \
  -H "Content-Type: application/json" \
  -d '{
    "chain": "bitcoin",
    "uuid": "3",
    "name": "Ordinals",
    "version": 1,
    "networks": {
      "mainnet": {
        "start_block": 777534,
        "if_this": {
          "scope": "ordinals_protocol",
          "operation": "inscription_feed"
        },
        "then_that": {
          "http_post": {
            "url": "http://localhost:3000/events",
            "authorization_header": "12345"
          }
        }
      }
    }
  }' \
  http://localhost:20456/v1/chainhooks

The sample response should look like this:

{
  "chainhook": {
    "predicate": {
      "operation": "inscription_feed",
      "scope": "ordinals_protocol",
    },
    "uuid": "1",
  },
  "apply": [
    {
      "block_identifier": {
        "hash": "0x00000000000000000003e3e2ffd3baaff2cddda7d12e84ed0ffe6f7778e988d4",
        "index": 777534,
      },
      "metadata": {},
      "parent_block_identifier": {
        "hash": "0x0000000000000000000463a1034c59e6dc94c7e52855582af11882743b86e2a7",
        "index": 777533,
      },
      "timestamp": 1676923039,
      "transactions": [
        {
          "transaction_identifier": {
            "hash": "0xca20efe5e4d71c16cd9b8dfe4d969efdd225ef0a26136a6a4409cb3afb2e013e",
          },
          "metadata": {
            "ordinal_operations": [
              {
                "inscription_revealed": {
                  "content_bytes": "<INSCRIPTION_BYTES>",
                  "content_length": 12293,
                  "content_type": "image/jpeg",
                  "inscriber_address": "bc1punnjva5ayg84kf5tmvx265uwvp8py3ux24skz43aycj5rzdgzjfq0jxsuc",
                  "inscription_fee": 64520,
                  "inscription_id": "ca20efe5e4d71c16cd9b8dfe4d969efdd225ef0a26136a6a4409cb3afb2e013ei0",
                  "inscription_number": 0,
                  "inscription_output_value": 10000,
                  "ordinal_block_height": 543164,
                  "ordinal_number": 1728956147664701,
                  "ordinal_offset": 1147664701,
                  "satpoint_post_inscription": "ca20efe5e4d71c16cd9b8dfe4d969efdd225ef0a26136a6a4409cb3afb2e013e:0:0",
                  "transfers_pre_inscription": 0,
                },
              },
            ],
            "proof": null,
          },
          "operations": [],
          // Other transactions
        },
      ],
    },
  ],
  "rollback": [],
}

To better understand the output of the above JSON file, let's break down some of the details:

  • The apply payload includes the block header and the transactions that triggered the predicate.
  • The rollback payload includes the block header and the transactions that triggered the predicate for a past block that is no longer part of the canonical chain and must be reverted. (Note: this is a chief component of Chainhook's reorg-aware functionality. Chainhook maintains rollback data for blocks near the chaintip.)

You can also run chainhook service by passing multiple predicates, ie chainhook service start --predicate-path=predicate_1.json --predicate-path=predicate_2.json --config-path=Chainhook.toml


Next steps