Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,7 @@ version = "0.1.0"
bitcoin = "0.32"
clap = { version = "4.3.14", features = ["derive"] }
corepc-node = { package = "corepc-node", version = "0.8.0", features = ["29_0"] }
hex = "0.4.3"
rand = { version = "0.9", features = ["thread_rng"] }
serde = "1.0.219"
tokio = { version = "1.46.1", features = ["full"] }
62 changes: 55 additions & 7 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
use bitcoin::address::Address;
use clap::Parser;
use corepc_node as node;
use corepc_node::{self as node};
use node::{Conf, Node, P2P};
use rand::prelude::*;
use std::net::SocketAddrV4;
use std::num::NonZeroU32;
use std::ops::{Deref, DerefMut};
use std::path::PathBuf;
use std::str::FromStr;
use std::time::Duration;

use crate::utils::mutated_block::MutatedBlockError;
use crate::utils::wallet_funds::add_wallet_funds;

mod utils;

/// Simple regtest traffic generator for bitcoin test and development
#[derive(Parser, Debug)]
#[command(version, about, long_about = None)]
Expand Down Expand Up @@ -85,7 +89,11 @@ impl DerefMut for Network {

impl Network {
fn new(cli: &Cli) -> Network {
let bitcoind_path = node::exe_path().expect("Can't find bitcoind executable");
let bitcoind_path = cli
.bitcoind_path
.as_ref()
.map(|p| p.to_string_lossy().to_string())
.unwrap_or_else(|| node::exe_path().expect("Can't find bitcoind executable"));
let n = cli.nodes.get();
let mut network = Vec::with_capacity(n as usize);

Expand All @@ -106,14 +114,27 @@ impl Network {
Network(network)
}

fn mine(self: &Self) {
fn mine(self: &Self, nblocks: Option<usize>) {
let nblocks = nblocks.unwrap_or(1);
let size = self.len();
let n = rand::random_range(0..size);
println!("Mining with node {}", n);
println!("Mining with node {}, {} blocks", n, nblocks);

let addr = &self[n].mine_addr;
let block = self[n].node.client.generate_to_address(1, addr).unwrap();
let block = self[n]
.node
.client
.generate_to_address(nblocks, addr)
.unwrap();
println!("{:?}", block);

// wait all nodes sync
while self.iter().any(|i| {
let height = i.node.client.get_block_count().unwrap().0;
height < self[n].node.client.get_block_count().unwrap().0
}) {
std::thread::sleep(Duration::from_millis(100));
}
}
}

Expand All @@ -125,8 +146,35 @@ async fn main() {
let network = Network::new(&cli);
println!("{:?}", network);

// network maturity to make above coinbase transaction valid
// TODO: refactor it to make a global balance so we avoid this solution
network.mine(Some(105));

let peer = &network[0].node;
let wallet_funds = add_wallet_funds(&peer.client, None).await.unwrap();

let _ = MutatedBlockError::BadTxnMrklRoot
.print_mutated_block_raw_hash(&peer.client, &wallet_funds.address)
.await;

let _ = MutatedBlockError::BadTxnsDuplicate
.print_mutated_block_raw_hash(&peer.client, &wallet_funds.address)
.await;

let _ = MutatedBlockError::BadWitnessNonceSize
.print_mutated_block_raw_hash(&peer.client, &wallet_funds.address)
.await;

let _ = MutatedBlockError::BadWitnessMerkleMatch
.print_mutated_block_raw_hash(&peer.client, &wallet_funds.address)
.await;

let _ = MutatedBlockError::UnexpectedWitness
.print_mutated_block_raw_hash(&peer.client, &wallet_funds.address)
.await;

loop {
network.mine();
network.mine(None);
tokio::time::sleep(Duration::from_secs(cli.mine_interval)).await;
}
}
108 changes: 108 additions & 0 deletions src/utils/create_block.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
use bitcoin::block::Version;
use bitcoin::blockdata::block::Block;
use bitcoin::blockdata::transaction::Transaction;
use bitcoin::hash_types::BlockHash;
use bitcoin::hashes::{sha256d, Hash};
use bitcoin::opcodes::all::OP_RETURN;
use bitcoin::script::{Builder, PushBytesBuf};
use bitcoin::{CompactTarget, TxMerkleNode};
use corepc_node::{Client, Error};
use std::time::{SystemTime, UNIX_EPOCH};

use crate::utils::create_coinbase::create_coinbase;

// Constants from blocktools.py
// https://github.com/bitcoin/bitcoin/blob/dadf15f88cbad37538d85415ae5da12d4f0f1721/test/functional/test_framework/blocktools.py
const VERSIONBITS_LAST_OLD_BLOCK_VERSION: i32 = 4;
const REGTEST_DIFFICULTY: u32 = 0x207fffff;
const COMMITMENT_HEADER: [u8; 4] = [0xaa, 0x21, 0xa9, 0xed];

pub fn create_block(
client: &Client,
txlist: Option<Vec<Transaction>>,
) -> Result<Block, Error> {
// Create coinbase transaction if not provided
let height = (get_block_height(client) + 1) as u32;
let coinbase = create_coinbase(height);

// Collect transactions
let mut transactions = vec![coinbase];
if let Some(txlist) = txlist {
transactions.extend(txlist);
}

// Initialize block header
let header = bitcoin::block::Header {
version: Version::from_consensus(VERSIONBITS_LAST_OLD_BLOCK_VERSION),
prev_blockhash: get_prev_hash(client),
merkle_root: TxMerkleNode::all_zeros(), // fill in later
time: get_min_timestamp(client),
bits: CompactTarget::from_consensus(REGTEST_DIFFICULTY),
nonce: 0, // Set to 0 for now; adjust if mining is needed
};

// Create and return the block
let mut block = Block {
header: header,
txdata: transactions,
};

prepare_commitment(&mut block);
update_merkle_root(&mut block);
mine_block(&mut block);

Ok(block)
}

fn prepare_commitment(block: &mut Block) -> () {
let commitment = {
let witness_root = block.witness_root().unwrap();
let mut data = Vec::from(witness_root.to_byte_array());
data.extend_from_slice(&[0u8; 32]); // reserved value
sha256d::Hash::hash(&data).to_byte_array()
};

let mut witness_bytes = Vec::with_capacity(4 + commitment.len());
witness_bytes.extend(COMMITMENT_HEADER);
witness_bytes.extend(commitment);

let coinbase = block.txdata.first_mut().unwrap();
coinbase.output[0].script_pubkey = Builder::new()
.push_opcode(OP_RETURN)
.push_slice(PushBytesBuf::try_from(witness_bytes).unwrap())
.into_script();
}

fn get_min_timestamp(client: &Client) -> u32 {
let blockchain_info = client.get_blockchain_info().unwrap();
let min_timestamp = blockchain_info.median_time as u32 + 1;

let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs() as u32;

if now > min_timestamp {
return now;
}
min_timestamp
}

fn get_prev_hash(client: &Client) -> BlockHash {
client.get_best_block_hash().unwrap().block_hash().unwrap()
}

fn get_block_height(client: &Client) -> u64 {
client.get_block_count().unwrap().0
}

pub fn mine_block(block: &mut Block) {
let target = block.header.target();
while !block.header.validate_pow(target).is_ok() {
block.header.nonce += 1;
}
}

pub fn update_merkle_root(block: &mut Block) -> () {
block.header.merkle_root = block.compute_merkle_root().unwrap();
}
54 changes: 54 additions & 0 deletions src/utils/create_coinbase.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
use bitcoin::absolute::Height;
use bitcoin::blockdata::script::Builder;
use bitcoin::blockdata::transaction::{OutPoint, Transaction, TxIn, TxOut};
use bitcoin::hashes::Hash;
use bitcoin::opcodes::all::OP_RETURN;
use bitcoin::opcodes::OP_TRUE;
use bitcoin::{Amount, Sequence, Witness, Wtxid};

const INITIAL_SUBSIDY: u64 = 50_0000_0000; // 50 BTC in satoshis
const HALVING_INTERVAL: u32 = 150;

pub fn calculate_subsidy(height: u32) -> Amount {
let halvings = height / HALVING_INTERVAL;
let subsidy_sat = INITIAL_SUBSIDY >> halvings;
Amount::from_sat(subsidy_sat)
}

pub fn create_coinbase(height: u32) -> Transaction {
// Create coinbase input with height script
let script_sig = if height <= 16 {
Builder::new()
.push_int(height as i64)
.push_opcode(OP_TRUE)
.into_script()
} else {
Builder::new().push_int(height as i64).into_script()
};

// anyone can spend
let script_pubkey = Builder::new()
.push_opcode(OP_RETURN)
.into_script();

let value = calculate_subsidy(height);

let mut tx = Transaction {
version: bitcoin::transaction::Version::ONE,
lock_time: bitcoin::absolute::LockTime::Blocks(Height::from_consensus(height).unwrap()),
input: vec![TxIn {
previous_output: OutPoint::null(),
script_sig,
sequence: Sequence::MAX,
witness: Witness::new(),
}],
output: vec![TxOut {
value,
script_pubkey,
}],
};

tx.input[0].witness.push(Wtxid::all_zeros().to_raw_hash());

tx
}
92 changes: 92 additions & 0 deletions src/utils/create_transaction.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
use bitcoin::{Address, Amount, Sequence, Transaction};
use corepc_node::{Client, Input, Output};
use serde::Deserialize;
use std::error::Error;

const DEFAULT_FEE: u64 = 1000; // in satoshis

// TODO: Remove this when corepc-types is updated
#[derive(Debug, Deserialize)]
pub struct UnspentOutput {
pub txid: String,
pub vout: i64,
pub address: String,
pub label: String,
// pub account: String, // this throws an error in original code
#[serde(rename = "scriptPubKey")]
pub script_pubkey: String,
pub amount: f64,
pub confirmations: i64,
#[serde(rename = "redeemScript")]
pub redeem_script: Option<String>,
pub spendable: bool,
pub solvable: bool,
pub safe: bool,
}

pub async fn create_self_transactions(
client: &Client,
to_address: &Address,
) -> Result<Transaction, Box<dyn Error>> {
let transactions = create_many_self_transactions(client, to_address, 1).await.unwrap();
Ok(transactions[0].clone())
}

pub async fn create_many_self_transactions(
client: &Client,
to_address: &Address,
count: usize,
) -> Result<Vec<Transaction>, Box<dyn Error>> {
let mut transactions = Vec::with_capacity(count);

let unspent: Vec<UnspentOutput> = client.call("listunspent", &[])?;
for (i, utxo) in unspent.iter().enumerate() {
if i >= count {
break;
}

let amount = Amount::from_btc(utxo.amount).unwrap() - Amount::from_sat(DEFAULT_FEE);
println!("Creating transfer of {} to {}", amount, to_address);
let tx = create_transaction(client, to_address, utxo, amount.clone())?;

transactions.push(tx);
}

Ok(transactions)
}

pub fn create_transaction(
client: &Client,
to_address: &Address,
utxo: &UnspentOutput,
amount: Amount,
) -> Result<Transaction, Box<dyn Error>> {
let inputs = {
vec![Input {
txid: utxo.txid.parse()?,
vout: u64::try_from(utxo.vout)?,
sequence: Some(Sequence::MAX),
}]
};
let outputs = [Output::new(to_address.clone(), amount)];

let raw_tx = client.create_raw_transaction(&inputs, &outputs)?;

let signed_tx = client.sign_raw_transaction_with_wallet(&raw_tx.transaction()?)?;
if !signed_tx.complete {
return Err("Failed to sign transaction".into());
}

if signed_tx.hex.is_empty() {
return Err("Signed transaction hex is empty".into());
}
let tx_bytes = hex::decode(&signed_tx.hex).map_err(|e| format!("Hex decode error: {}", e))?;
if tx_bytes.is_empty() {
return Err("Decoded transaction bytes are empty".into());
}

let final_tx: Transaction = bitcoin::consensus::deserialize(&tx_bytes)
.map_err(|e| format!("Deserialization error: {}", e))?;

Ok(final_tx)
}
5 changes: 5 additions & 0 deletions src/utils/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
pub mod wallet_funds;
pub mod create_block;
pub mod create_coinbase;
pub mod create_transaction;
pub mod mutated_block;
Loading