Voltage for Developers - Bitcoin Core Mutinynet

Voltage for Developers - Bitcoin Core Mutinynet

August 8, 2024

Developing on Bitcoin can be hard. Tools for the reference implementation, Bitcoin Core, are often unintuitive and difficult to use. Documentation and examples are often scattered across online forums like StackOverflow or buried deep in wikis instead of being readily available in tutorial websites and walkthroughs online.

To further complicate things, developers have to choose between the multiple networks available for building and testing. It's not a straightforward decision. Most of the choices are suboptimal, and client-facing applications rarely support networks other than Mainnet (real bitcoin), so testing how your app will work in the wild can be difficult unless you're willing to risk some sats.

Bitcoin Development Networks

Each development network has different tradeoffs. There are essentially four choices to choose from. Often, some combination of these will be used at various stages of the development cycle:

  • Mainnet: Real bitcoin. Obviously, as the real thing, it's not meant as a development network, but often gets used as such anyway, since it's the easiest way to know how your app will work in the real world, and every bitcoin application supports it. But, for obvious reasons, this can be dangerous if you're not careful!
  • Testnet: Essentially the same as real bitcoin in almost every way, except it's supposed to have no value. In practice, Testnet needs to be continually reset because it keeps accruing value, it's irregularly mined, and it's difficult to get from faucets. There are ongoing efforts now to reset it, from Testnet3 to Testnet4, due to these and other issues.
  • Regtest: A Bitcoin network running locally on your machine. Great for rapid prototyping, but very difficult for simulating realistic situations, and you can't share your network state with other developers or users.
  • Signet: Signet is essentially a centralized Testnet where blocks must be signed by the creator(s) of the network instead of proof-of-work. This allows for a controlled network where coins can be shared and experimented with beyond just a local environment, allowing for more flexible development and testing scenarios. There is a primary Signet, but many signets can be created and modified to suit the developer's purposes.

Here's a table to summarize the differences:

Network Mainnet Testnet Signet Regtest
Global State (Shareable)
No Cost
Fast/Instant Blocks

After experimenting the limitations of each of the above methods, the team at Mutiny decided to create Mutinynet. It's a fork of Bitcoin Core that changes the block time average from the usual 10 minutes to around 30 seconds, along with some other bells and whistles. The Mutiny team found that they were building things that couldn't be easily tested in a local Regtest environment, and given all the issues with Testnet, they decided to make their own network.

30-second block times shrink the feedback loop and reduce context-switching for developers, especially for applications that require multiple transactions like opening lightning channels. And importantly, this way they still have a globally-available means for themselves and others to test their projects.

As I've personally witnessed at hackathons, the current test networks are so hard to use and riddled with issues that devs normally opt to use small amounts of real mainnet bitcoin to test their apps, despite having to pay fees.

This comment on the fork PR does a good job of summarizing the need for a network like this.

Here at Voltage, we build a lot of Bitcoin and Lightning FOSS, and we're big Mutinynet fans and users. We knew firsthand that a network optimized for development was sorely needed, and so we decided to integrate it into our platform, both for Lightning and Bitcoin Core nodes.

Best of all, Mutinynet nodes are free on Voltage, and don't contribute to your billing plan.

See our Mutinynet announcement here, and be sure to checkout our Mutinynet lightning node tutorial by our very own bitcoinplebdev.

In this article, we're going to see how to create a Mutinynet Bitcoin Core node on Voltage and interact with it via RPC commands.

Create a Mutinynet Bitcoin Core Node

First, let's create the node. If you're new, head on over to voltage.cloud and create an account. On your first login, you'll see a screen showing your newly created team. Once past that, you'll see the infrastructure page. Click on the "New Instance" button:

Select Bitcoin Core:

Choose a Mutinynet Full Node. Don't worry, it doesn't contribute to your team's bill:

NOTE: If you are a new user or on the Essentials Plan, you must select at minimum a Starter Plan to be able to create a Mutinynet Node. See how to select a team plan here.

Then, click Create and give it a name in the modal that pops up. Then click Create again:

You should see a loading screen for a bit while the node is being provisioned:

When it finishes, you'll see a screen like this:

Let's walk through each of these cards.

Header shows the node's name, status, whether it's a full or pruned node, and the network it runs on. In the upper right-hand corner you can stop or restart the node.

Blockclock shows RPC information about the node in a compact and easy to understand way. At first, it must download the blockchain (which takes ~20-30 minutes on Mutinynet).

When it's finished, it'll look like this:

We built this web component at Voltage as an open source project using the design from the Bitcoin Design team's bitcoincore.app project. Read more about the Blockclock here.

Next we have the Node Details and Network cards. The Node Details card displays high-level information pertaining to the node, such as the version and cloud it's running on. Of particular interest to us now is the API Endpoint. The Network card shows RPC info coming directly from the node itself, such as it's verification progress, the block height, and the difficulty.

Finally, we have the network cards for monitoring traffic into and out of the node. This can be a useful "health check" for the node.

Interacting With Your Node

Great! You now have a node up and running. Let's take a look at the sidebar and click on "Connect". This is where you can create RPC users who can interact with the node.

Click "New RPC User" and type in a username/password combination, then click "Add":

You'll notice your node restart once you do this:

Now, copy the sample cURL command and replace it with your username/password credentials. Notice that the API Endpoint it uses is the same as the Node Details card earlier.

Paste the command into the Terminal and press Enter:

curl --user alice:mypassword --data-binary '{"method": "getblockchaininfo", "params": []}' https://chwhbpwcqj.b.staging.voltageapp.io:38332

You should get a result like this:

{
  "result": {
    "chain": "signet",
    "blocks": 1308464,
    "headers": 1308464,
    "bestblockhash": "0000022217b32aa44e4683c59abdc849c30d1612a394bb3826f72bd1768c12f1",
    "difficulty": 0.001126515290698186,
    "time": 1722519489,
    "mediantime": 1722519337,
    "verificationprogress": 1,
    "initialblockdownload": false,
    "chainwork": "000000000000000000000000000000000000000000000000000005c2073ce3d4",
    "size_on_disk": 853141585,
    "pruned": false,
    "warnings": "This is a pre-release test build - use at your own risk - do not use for mining or merchant applications"
  },
  "error": null,
  "id": null
}

If you do, great! Now you can run any Bitcoin Core RPC command against your node.

If you don't, check that your RPC credentials were entered correctly. You can always delete and create a new one if you forgot. Note: you can add a -v flag to the curl command if you want to dig into why it failed.

Free (valueless) Coins!

Let's do something a bit more interesting than getblockchaininfo. How about receiving some coins?

Let's generate an address to receive to. First, we need to create a wallet (named alice here):

curl --user alice:mypassword --data-binary '{"method": "createwallet", "params": ["alice"]}' https://chwhbpwcqj.b.staging.voltageapp.io:38332

Then, let's get a new address:

curl --user alice:mypassword --data-binary '{"method": "getnewaddress", "params": []}' https://chwhbpwcqj.b.staging.voltageapp.io:38332

You should see something like:

{
  "result": "tb1qpytqda8jsu7vhexy7wa5ef6nwss4y2yp0gfu4z",
  "error": null,
  "id": null
}

Copy the address from the command. Now go to faucet.mutinynet.com and choose how many coins you'd like to send to it, then make it rain!

Now, after about 30 seconds, you can check if the funds have been received by checking getbalance:

curl --user alice:mypassword --data-binary '{"method": "getbalance", "params": []}' https://chwhbpwcqj.b.staging.voltageapp.io:38332
{"result":0.00100000,"error":null,"id":null}

Or, you can inspect the transaction itself with listtransactions

curl --user alice:mypassword --data-binary '{"method": "listtransactions", "params": []}' https://chwhbpwcqj.b.staging.voltageapp.io:38332
{
  "result": [
    {
      "address": "tb1qpytqda8jsu7vhexy7wa5ef6nwss4y2yp0gfu4z",
      "parent_descs": [
        "wpkh(tpubD6NzVbkrYhZ4WQg15sC72aA7taQitS5rAt7rQ7GmTArhRcqAQB2TW4GXqHJgFmGNxQFaLQ85WjusF3i1NQ4ZVb4EQE2SmPGkqKh1yVCwgWk/84h/1h/0h/0/*)#na46vkme"
      ],
      "category": "receive",
      "amount": 0.001,
      "label": "",
      "vout": 0,
      "abandoned": false,
      "confirmations": 1,
      "blockhash": "0000015f719f06b5edbc47760f028b2e0d0a0a3ef6b76f6ea8a0374857732ebf",
      "blockheight": 1308493,
      "blockindex": 1,
      "blocktime": 1722520384,
      "txid": "b1eb199d8cc8553f3199cb0d56ab06d67c5b597f4c89ef21820bf22725c81af2",
      "wtxid": "acf5d39f1a558d5392f3d0ab8cf6a99af7958104f0bbb4d98d3108bacfadad57",
      "walletconflicts": [],
      "time": 1722520356,
      "timereceived": 1722520356,
      "bip125-replaceable": "no"
    }
  ],
  "error": null,
  "id": null
}

And just for fun, let's do a send back to the return address listed at https://faucet.mutinynet.com: tb1qd28npep0s8frcm3y7dxqajkcy2m40eysplyr9v

curl --user alice:mypassword --data-binary '{"method": "sendtoaddress", "params": ["tb1qd28npep0s8frcm3y7dxqajkcy2m40eysplyr9v", "0.00090000"]}' https://chwhbpwcqj.b.staging.voltageapp.io:38332
{
  "result": "27938906a1003234035649782ad052998325a0ed14aaff07c67a12b5b02e3973",
  "error": null,
  "id": null
}

The Mutiny team also spun up their own mempool.space instance at mutinynet.com. Let's check out the transaction on there:

Perfect! Now that we've got a handle at running the RPC commands from the terminal, let's kick things up a notch. How about a super simple HTML page that displays information

RPC Calls from Code

You'll likely want to be able to RPC from an application you're building. Let's look at examples of how to do this with in a few languages.

Javascript (Node.JS)

Let's create an example in the universal language of the web, Javascript. We'll just do a command-line Node.JS example, but this can be easily adapted for browser environments. First, install Node.JS. Then, create a directory called mutinynet and cd into it:

mkdir mutinynet
cd mutinynet

And install node-fetch in it:

npm i node-fetch

Now, create a file called node-mutinynet.mjs. We're using the .mjs extension here to explicitly define that we want it to parse as an ES Module. Let's write a script just to do a simple getblockchaininfo. We'll pass in the environment variables RPC_ENDPOINT, RPC_USERNAME, and RPC_PASSWORD so that we don't have to hardcode the values in the script:

import fetch from "node-fetch";

const { RPC_ENDPOINT, RPC_USERNAME, RPC_PASSWORD } = process.env;

const headers = {
  Authorization:
    "Basic " +
    Buffer.from(RPC_USERNAME + ":" + RPC_PASSWORD).toString("base64"),
  "Content-Type": "text/plain",
};

const body = JSON.stringify({
  jsonrpc: "1.0",
  method: "getblockchaininfo",
  params: [],
});

fetch(RPC_ENDPOINT, {
  method: "POST",
  headers: headers,
  body: body,
})
.then(async (response) => {
    const json = await response.json();
    try {
      console.log(json);
    } catch (error) {
      console.error("Error parsing JSON:", error);
    }
})
.catch((error) => console.error("Fetch error:", error));

Now run the script, passing in the environment variables:

RPC_ENDPOINT="https://chwhbpwcqj.b.staging.voltageapp.io:38332" RPC_USERNAME="alice" RPC_PASSWORD="mypassword" node node-mutinynet.mjs

And you should get:

{
  result: {
    chain: 'signet',
    blocks: 1314657,
    headers: 1314657,
    bestblockhash: '0000011a5909072b36696f7091d30791bdb41131981f835793f0c398cec2c99c',
    difficulty: 0.001126515290698186,
    time: 1722711598,
    mediantime: 1722711442,
    verificationprogress: 1,
    initialblockdownload: false,
    chainwork: '000000000000000000000000000000000000000000000000000005c901405ba8',
    size_on_disk: 862088526,
    pruned: false,
    warnings: 'This is a pre-release test build - use at your own risk - do not use for mining or merchant applications'
  },
  error: null,
  id: null
}

Nice! Now you have an example call you can easily port to frontend web applications!

Python

Let's create one in Python:

import os
import json
import base64
import requests

# Get environment variables
RPC_ENDPOINT = os.environ.get('RPC_ENDPOINT')
RPC_USERNAME = os.environ.get('RPC_USERNAME')
RPC_PASSWORD = os.environ.get('RPC_PASSWORD')

# Create headers
auth = base64.b64encode(f"{RPC_USERNAME}:{RPC_PASSWORD}".encode()).decode()
headers = {
    'Authorization': f"Basic {auth}",
    'Content-Type': 'text/plain'
}

# Create body
body = json.dumps({
    "jsonrpc": "1.0",
    "method": "getblockchaininfo",
    "params": []
})

try:
    # Make the POST request
    response = requests.post(RPC_ENDPOINT, headers=headers, data=body)
    
    # Raise an exception for bad status codes
    response.raise_for_status()
    
    # Parse JSON response
    json_response = response.json()
    
    # Pretty print the JSON response
    pretty_json = json.dumps(json_response, indent=2, sort_keys=True)
    print(pretty_json)

except json.JSONDecodeError as e:
    print(f"Error parsing JSON: {e}")
except requests.RequestException as e:
    print(f"Request error: {e}")

Run the script:

RPC_ENDPOINT="https://chwhbpwcqj.b.staging.voltageapp.io:38332" RPC_USERNAME="alice" RPC_PASSWORD="mypassword" python3 python-mutinynet.py

You should see the same getblockchaininfo response from earlier.

Rust

Finally, let's do an example in Rust, a popular language in the Bitcoin developer community.

First, install Rust. Then, using the cargo package manager, create a new project:

cargo new rust-mutinynet
cd rust-mutinynet

You should see the following directory structure:

/rust-mutinynet
|-- src
|   |-- main.rs
|
|-- target
|
|-- Cargo.toml
|-- Cargo.lock

Replace Cargo.toml with the following minimal dependencies that will allow us to make requests (tokio and reqwest), convert our responses to JSON (serde_json), and encode our Authorization header to base64 (base64):

[package]
name = "rust-mutinynet"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
reqwest = { version = "0.11", features = ["json"] }
tokio = { version = "1", features = ["full"] }
serde_json = "1.0"
base64 = "0.21"

Then, add the following code to src/main.rs:

use base64::{engine::general_purpose, Engine as _};
use reqwest::header::{HeaderMap, HeaderValue, AUTHORIZATION, CONTENT_TYPE};
use serde_json::{json, Value};
use std::env;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Get environment variables
    let rpc_endpoint = env::var("RPC_ENDPOINT").expect("RPC_ENDPOINT not set");
    let rpc_username = env::var("RPC_USERNAME").expect("RPC_USERNAME not set");
    let rpc_password = env::var("RPC_PASSWORD").expect("RPC_PASSWORD not set");

    // Create headers
    let auth = general_purpose::STANDARD.encode(format!("{}:{}", rpc_username, rpc_password));
    let mut headers = HeaderMap::new();
    headers.insert(
        AUTHORIZATION,
        HeaderValue::from_str(&format!("Basic {}", auth))?,
    );
    headers.insert(CONTENT_TYPE, HeaderValue::from_static("text/plain"));

    // Create body
    let body = json!({
        "jsonrpc": "1.0",
        "method": "getblockchaininfo",
        "params": []
    });

    // Create client and send request
    let client = reqwest::Client::new();
    let response = client
        .post(&rpc_endpoint)
        .headers(headers)
        .json(&body)
        .send()
        .await?;

    // Check status and parse response
    if response.status().is_success() {
        let json: Value = response.json().await?;
        println!("{:#?}", json);
    } else {
        println!("Error: {}", response.status());
        println!("Response: {}", response.text().await?);
    }

    Ok(())
}

Now run with cargo run:

RPC_ENDPOINT="https://chwhbpwcqj.b.staging.voltageapp.io:38332" RPC_USERNAME="alice" RPC_PASSWORD="mypassword" cargo run

Hopefully we get our nicely pretty-printed getblockchaininfo response again.

Testing Apps With the Signet Mutiny Wallet

The Mutiny team released a version of their wallet that uses Mutinynet instead of mainnet bitcoin. If you want to test how your app interacts with a wallet, by making bitcoin payments via a QR code, for example, you can use signet-app.mutinywallet.com.

Conclusion

Mutinynet is a great network to develop Bitcoin apps on, due to its 30-second block times and active maintenance. And with Mutinynet Bitcoin Core nodes on Voltage, it's easier than ever. It's also a great way to learn about and experiment with Bitcoin Core in an easy and consequence-free environment.

We at Voltage are doing what we can to tear down the barriers to a great Bitcoin developer experience. So get out there and build. We can't wait to see what you create.

When developing on Mutinynet, it'll be helpful to have these handy:

Acknowledgements

Thank you to @bitcoinplebdev, @StephenDeLorme, and @tee8z for their suggestions on improving this article.

blu