Skip to main content

zkVM Taproot Address Ownership Proofs

This demo uses zkVM to prove ownership of a Bitcoin taproot address. A user submits a Bitcoin private key to zkVM, which will derive the public key and submit it to the BOB testnet. Because of the zero-knowledge (zk) part of zkVM, no information regarding the private key gets published. The proof is generated by Bonsai and verified on-chain.

Example Code

The code of the demo can be found in this repository.

Limitations, Notes and Warnings

This demo is a demonstration of the technical possibilities rather than something to use in production. In production, other approaches are more suitable, such as signing a message with the private key, and having zkVM check that signed message using the public key.


It is important to note that while zkVM does not leak information about the private key, the private key is received by the Bonsai API, which means that you have to trust this service to not steal your keys. As such, it is not recommended to use this demo on production accounts and generate a new throwaway key for trying this demo.

Finally, taproot addresses are pretty flexible - the spending condition can be either pubkey-based (like P2PK) or script-based (like P2SH). This demo only works for taproot addresses of the former type. Specifically is written to work only with Bitcoin-core wallets, where wallets and addresses are created using the ord tool.



This demo has the following prerequisites:

  • bitcoin-core version 23 or higher. Tests were done with version 25.
  • ord for address and wallet generation.
  • risc0 dependencies for the zkVM program compilation.
  • a Bonsai API key. The commands below will use $API_KEY - substitute this for your key.

Running the Demo

To execute the demo, perform the following steps:

1. Account Creation and Funding

Create a new Ethereum account using e.g. MetaMask, and fund the account using the l2 faucet on this page. The private key of this account will be used in the commands below (substitute the DEPLOYER_PRIVATE_KEY variable).

2. Deploy the Contracts

Deploy the necessary contracts (the Bonsai relay, verifier, and the TaprootRegister):

forge script script/Deploy.s.sol --rpc-url --broadcast --verify --verifier blockscout --verifier-url ''

After running the command above, look for output like this:

== Logs ==
Deployed RiscZeroGroth16Verifier to [...]
Deployed BonsaiRelay to [...]
Image ID for taproot-derive is [...]
Deployed TaprootRegister to [...]

The commands below will use the $BONSAI_RELAY and $TAPROOT_REGISTER values - substitute those for the addresses logged above.

3. Start the Relayer

Start a relayer client (and leave it running in the background):

BONSAI_API_URL= BONSAI_API_KEY=$API_KEY cargo run --bin bonsai-ethereum-relay-cli -- run --relay-address $BONSAI_RELAY --eth-node wss:// --eth-chain-id 901 --private-key $DEPLOYER_PRIVATE_KEY

4. Start Bitcoin Core

Start a regtest Bitcoin daemon:

bitcoind -regtest -server -rpcuser=rpcuser -rpcpassword=rpcpassword -fallbackfee=0.0002 -blockfilterindex -txindex=1 -prune=0 -blockversion=4

5. Create an Ordinals Wallet

Create an ordinals wallet:

ord --regtest --bitcoin-rpc-pass rpcpassword --bitcoin-rpc-user rpcuser wallet create

6. Create a Taproot Address

Create a new taproot address:

ord --regtest --bitcoin-rpc-pass rpcpassword --bitcoin-rpc-user rpcuser wallet receive

7. Prove Ownership of the Address

Now finally, initiate the proving of an address:

cargo run --bin taproot-prover -- --address 0000000000000000000000000000000000000001 --taproot-address $TAPROOT_ADDRESS_FROM_PREVIOUS_STEP --bonsai-api-key=$API_KEY

The command above, if it runs successfully, will initiate the generation of a zk-proof on the Bonsai server, and after completion (which can take a couple of minutes), it will submit it to the BOB testnet for verification. After waiting a couple of minutes, you will be able to see the result in the explorer. Go to the explorer and search for the previously logged $TAPROOT_REGISTER address. Go to the "Internal Transactions", click the latest transaction, and click "Logs". You should see an OwnershipProven event, showing your Ethereum and taproot address.

Diving into the Code

The guest program running inside the zkVM can be found in methods/guest/src/ and methods/guest/src/ This code takes the private key of the taproot address and a public EVM address (both encoded using the Ethereum ABI), and outputs the derived the bech32m address. The EVM address is passed through unprocessed. Note that in production you wouldn't want to do this, since you'd assume that the host is an untrusted component.

The code for extracting the private key associated with an address can be found in taproot-prover/src/ Note that this is not very straightforward, since with descriptor wallets the RPC commands for directly dumping private addresses do not work.