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.
Useful Links
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.
Comments ()