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
25 changes: 25 additions & 0 deletions OPERATOR.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ Routine knobs are **CLI / conf**, not required env vars. Clean smoke:
|------|----------------|---------|
| `--datadir PATH` | same | `./datadir` |
| `--network NET` | `--chain` | `mainnet` |
| `--signetchallenge HEX` | `--signet-challenge` | default global Signet challenge |
| `--signetblocktime SECONDS` | `--signet-block-time` | 600; requires a custom challenge |
| `--listen ADDR` | | bind later default port |
| `--connect ADDR` | (repeatable) | seeds |
| `--milestone HEIGHT` | `--assumevalid-height` | network default (mainnet 840000) |
Expand Down Expand Up @@ -304,6 +306,29 @@ mkdir -p ./datadir-signet
--log-level info
```

### Custom Signet

A custom Signet derives its P2P message magic from the challenge. Default
Signet seeds are not used, so provide at least one peer with `--connect`.
Use a dedicated datadir for each challenge.

```bash
mkdir -p ./datadir-custom-signet
./target/release/rbitcoin-node \
--datadir ./datadir-custom-signet \
--network signet \
--signetchallenge 51 \
--signetblocktime 60 \
--connect 192.0.2.1:38333 \
--listen 0.0.0.0:38333 \
--milestone 0 \
--log-level info
```

The equivalent conf-file keys are `signetchallenge` and `signetblocktime`.
Replace the illustrative `OP_TRUE` challenge and documentation-only peer with
the parameters supplied by the custom Signet operator.

### Resume / clean stop

Same `--datadir` resumes tip from the relational archive.
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,9 @@ install -m 755 result/bin/rbitcoin-node result/bin/rbitcoin-cli target/release/
--listen 127.0.0.1:38333 --milestone 200000 --max-run-secs 120
```

Custom Signets are supported with `--signetchallenge` and
`--signetblocktime`; see the [custom Signet example](./OPERATOR.md#custom-signet).

## Build

### Portable static release (recommended)
Expand Down
2 changes: 1 addition & 1 deletion crates/rbitcoin-consensus/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ pub use header::{expected_next_bits, median_time_past, validate_header};
pub use milestone::Milestone;
pub use params::{default_milestone_height, genesis_block, ChainParams, Checkpoint};
pub use policy::{check_tx_standard, is_push_only, is_standard_script_pubkey, PolicyResult};
pub use signet::{default_signet_challenge, validate_signet_block_solution};
pub use signet::{default_signet_challenge, signet_magic, validate_signet_block_solution};

pub fn crate_name() -> &'static str {
"rbitcoin-consensus"
Expand Down
34 changes: 30 additions & 4 deletions crates/rbitcoin-consensus/src/params.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,15 +71,29 @@ impl ChainParams {
}

pub fn signet() -> Self {
Self::custom_signet(crate::signet::default_signet_challenge(), 10 * 60)
.expect("default signet block time is nonzero")
}

/// Build custom BIP325 signet parameters.
///
/// `block_time` changes PoW target spacing while retaining Signet's two-week
/// target timespan, matching Bitcoin Core's `signetblocktime` behavior.
pub fn custom_signet(challenge: ScriptBuf, block_time: u64) -> Result<Self, &'static str> {
if block_time == 0 {
return Err("signet block time must be greater than zero");
}
let genesis = constants::genesis_block(Network::Signet);
Self {
let mut btc = BtcParams::new(Network::Signet);
btc.pow_target_spacing = block_time;
Ok(Self {
network: Network::Signet,
genesis_hash: genesis.block_hash(),
pow_limit: Target::MAX_ATTAINABLE_SIGNET,
checkpoints: vec![],
btc: BtcParams::new(Network::Signet),
signet_challenge: Some(crate::signet::default_signet_challenge()),
}
btc,
signet_challenge: Some(challenge),
})
}

pub fn checkpoint_at(&self, height: Height) -> Option<BlockHash> {
Expand Down Expand Up @@ -313,6 +327,18 @@ mod tests {
assert!(p.signet_challenge.is_some());
}

#[test]
fn custom_signet_uses_challenge_and_block_time() {
let challenge = ScriptBuf::from_bytes(vec![0x51]);
let p = ChainParams::custom_signet(challenge.clone(), 60).unwrap();

assert_eq!(p.signet_challenge.as_ref(), Some(&challenge));
assert_eq!(p.btc.pow_target_spacing, 60);
assert_eq!(p.btc.pow_target_timespan, 14 * 24 * 60 * 60);
assert_eq!(p.difficulty_adjustment_interval(), 20_160);
assert!(ChainParams::custom_signet(challenge, 0).is_err());
}

/// Mainnet buried heights (Core + Inquisition).
#[test]
fn mainnet_buried_deployments_match_core() {
Expand Down
26 changes: 25 additions & 1 deletion crates/rbitcoin-consensus/src/signet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
//! expensive for the IBD prep path).

use bitcoin::absolute::LockTime;
use bitcoin::consensus::Encodable;
use bitcoin::consensus::{serialize, Encodable};
use bitcoin::hashes::{sha256d, Hash};
use bitcoin::script::{Script, ScriptBuf};
use bitcoin::{Amount, Block, OutPoint, Sequence, Transaction, TxIn, TxOut, Witness};
Expand All @@ -26,6 +26,17 @@ pub fn default_signet_challenge() -> ScriptBuf {
))
}

/// Derive the four P2P message-start bytes for a BIP325 challenge.
///
/// Bitcoin Core hashes the consensus-serialized challenge byte vector, including
/// its CompactSize length prefix, and uses the first four digest bytes.
pub fn signet_magic(challenge: &Script) -> [u8; 4] {
let encoded = serialize(&challenge.as_bytes().to_vec());
sha256d::Hash::hash(&encoded).to_byte_array()[..4]
.try_into()
.expect("four-byte digest prefix")
}

fn hex_decode(s: &str) -> Vec<u8> {
(0..s.len())
.step_by(2)
Expand Down Expand Up @@ -436,6 +447,19 @@ mod tests {
.expect("BIP325 solution for real signet height 1");
}

#[test]
fn custom_challenge_derives_expected_wire_magic() {
let challenge = ScriptBuf::from_bytes(vec![0x51]);
assert_eq!(
signet_magic(challenge.as_script()),
[0x54, 0xd2, 0x6f, 0xbd]
);
assert_eq!(
signet_magic(default_signet_challenge().as_script()),
[0x0a, 0x03, 0xcf, 0x40]
);
}

#[test]
fn signet_block_1_rejects_mutated_solution() {
let raw = include_bytes!("../tests/fixtures/signet_block_1.bin");
Expand Down
2 changes: 1 addition & 1 deletion crates/rbitcoin-net/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ pub use seeds::{
default_port, dns_seeds, fixed_seed_hosts, resolve_all_seeds, resolve_dns_seeds,
resolve_fixed_seeds, AddrMan, PeerEntry, PeerFlags,
};
pub use service::{magic_for, NetConfig, P2PHandle, P2PNode};
pub use service::{magic_for, magic_for_params, NetConfig, P2PHandle, P2PNode};
pub use tx_relay::{
decode_len_prefixed_package, ElectrumMempoolItem, MempoolHub, QueryUtxoProvider,
};
Expand Down
24 changes: 22 additions & 2 deletions crates/rbitcoin-net/src/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use crate::peer_dos::{inbound_semaphore, max_inbound_from_env};
use bitcoin::p2p::Magic;
use bitcoin::Block;
use bitcoin::BlockHash;
use rbitcoin_consensus::{ChainParams, Milestone};
use rbitcoin_consensus::{signet_magic, ChainParams, Milestone};
use rbitcoin_primitives::Network as RNetwork;
use rbitcoin_query::Query;
use std::net::SocketAddr;
Expand Down Expand Up @@ -65,7 +65,7 @@ impl P2PNode {
params: ChainParams,
milestone: Milestone,
) -> Result<Self, NetError> {
let magic = Magic::from(params.network);
let magic = magic_for_params(&params);
let hub = Arc::new(ChainHub::new(query, params, milestone));
hub.ensure_genesis()?;
let cache = hub.cache.clone();
Expand Down Expand Up @@ -279,6 +279,14 @@ pub fn magic_for(network: RNetwork) -> Magic {
})
}

/// Resolve P2P message magic, including BIP325 custom-Signet derivation.
pub fn magic_for_params(params: &ChainParams) -> Magic {
match params.signet_challenge.as_ref() {
Some(challenge) => Magic::from_bytes(signet_magic(challenge.as_script())),
None => Magic::from(params.network),
}
}

#[cfg(test)]
mod tests {
use super::*;
Expand All @@ -303,4 +311,16 @@ mod tests {
assert!(cfg.listen.is_none());
assert_eq!(cfg.user_agent, "/rbitcoin:0.1.0/");
}

#[test]
fn custom_signet_uses_challenge_derived_magic() {
use bitcoin::ScriptBuf;

let challenge = ScriptBuf::from_bytes(vec![0x51]);
let params = ChainParams::custom_signet(challenge, 60).unwrap();
assert_eq!(
magic_for_params(&params),
Magic::from_bytes([0x54, 0xd2, 0x6f, 0xbd])
);
}
}
67 changes: 67 additions & 0 deletions crates/rbitcoin-node/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ where
let mut datadir_set = false;
let mut network = Network::Mainnet;
let mut network_set = false;
let mut signet_challenge = None;
let mut signet_block_time = None;
let mut smoke = false;
let mut listen: Option<SocketAddr> = None;
let mut electrum_listen: Option<SocketAddr> = None;
Expand Down Expand Up @@ -55,6 +57,7 @@ where
[--mempool-size-mb|--maxmempool N] [--archive-queue-mb N] \\\n\
[--max-run-secs N] [--log-level LEVEL] [--no-seeds] [--smoke] [--inhibit-suspend]\n\n\
Networks: mainnet|testnet|signet|regtest\n\
Custom Signet: --signetchallenge HEX [--signetblocktime SECONDS].\n\
Log level: error|warn|info|debug|trace|off (CLI > conf log_level > RBITCOIN_LOG / RUST_LOG).\n\
Milestone / assumevalid-height: skip script/sig checks at/below HEIGHT.\n\
Defaults: mainnet 840000, signet 2000000, testnet 2500000, regtest 0. Use 0 for full scripts.\n\
Expand Down Expand Up @@ -122,6 +125,40 @@ IBD: up to 1024 concurrent getdata, max 16 in transit per peer.",
}
i += 1;
}
"--signetchallenge" | "--signet-challenge" => {
i += 1;
if i >= args.len() {
eprintln!("error: --signetchallenge requires hexadecimal script bytes");
return ExitCode::from(2);
}
match crate::config::parse_signet_challenge(&args[i].to_string_lossy()) {
Ok(challenge) => signet_challenge = Some(challenge),
Err(e) => {
eprintln!("error: bad --signetchallenge: {e}");
return ExitCode::from(2);
}
}
i += 1;
}
"--signetblocktime" | "--signet-block-time" => {
i += 1;
if i >= args.len() {
eprintln!("error: --signetblocktime requires seconds");
return ExitCode::from(2);
}
match args[i].to_string_lossy().parse::<u64>() {
Ok(n) if n > 0 => signet_block_time = Some(n),
Ok(_) => {
eprintln!("error: --signetblocktime must be greater than zero");
return ExitCode::from(2);
}
Err(e) => {
eprintln!("error: bad --signetblocktime: {e}");
return ExitCode::from(2);
}
}
i += 1;
}
"--listen" => {
i += 1;
if i >= args.len() {
Expand Down Expand Up @@ -358,6 +395,12 @@ IBD: up to 1024 concurrent getdata, max 16 in transit per peer.",
if network_set {
config.network = network;
}
if let Some(challenge) = signet_challenge {
config.signet_challenge = Some(challenge);
}
if signet_block_time.is_some() {
config.signet_block_time = signet_block_time;
}
if let Some(a) = listen {
config.p2p_listen = Some(a);
}
Expand Down Expand Up @@ -609,6 +652,30 @@ mod tests {
let _ = std::fs::remove_dir_all(&dir);
}

#[test]
fn custom_signet_cli_smoke() {
let dir = tmp_datadir();
let code = cli_main([
"rbitcoin-node",
"--smoke",
"--network",
"signet",
"--datadir",
dir.to_str().unwrap(),
"--signetchallenge",
"51",
"--signetblocktime",
"60",
"--no-seeds",
"--log-level",
"error",
"--milestone",
"0",
]);
assert_exit(code, ExitCode::SUCCESS);
let _ = std::fs::remove_dir_all(&dir);
}

#[test]
fn help_lists_coreish_flags_not_only_env() {
let _g = OPERATOR_ENV_TEST_LOCK
Expand Down
Loading