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
16 changes: 16 additions & 0 deletions doc/src/design/ir.md
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,10 @@ containing the correctly serialized transactions `v15` and `v30`.
| `BeginBuildBlockTxn` | Begins building a blocktxn message after sending a compact block. |
| `AddTxToBlockTxn` | Adds a transaction to the blocktxn message. |
| `EndBuildBlockTxn` | Finishes building a blocktxn message. |
| **Getblocktxn building** | **Construct a BIP152 getblocktxn request (compact block reconstruction side).**|
| `BeginBuildGetBlockTxn` | Begins building a getblocktxn request for a block the node announced via `cmpctblock`. |
| `AddIndexToGetBlockTxn` | Adds a block-level transaction index to the request. |
| `EndBuildGetBlockTxn` | Finishes building a getblocktxn request. |
| **Filter building** | **Construct a BIP37 filter.** |
| `BeginBuildFilterLoad` | Begins building a filter. |
| `AddTxToFilter` | Adds a transaction to a filter. |
Expand Down Expand Up @@ -257,6 +261,7 @@ containing the correctly serialized transactions `v15` and `v30`.
| `SendGetCFCheckpt`| Sends a `getcfcheckpt` message. |
| `SendCompactBlock` | Sends a `cmpctblock` message. |
| `SendBlockTxn` | Sends a `blocktxn` message. |
| `SendGetBlockTxn` | Sends a `getblocktxn` message (requesting transactions for a compact block the node announced to us). |
| **Other** | |
| `Nop` | No operation. Used during minimization. |
| `Probe` | Tells the scenario to probe state for the fuzzer (e.g. received messages, tip hash, ...). |
Expand Down Expand Up @@ -294,6 +299,17 @@ fuzzing campaign. The following generators are available:
block
- `CompactBlockGenerator`: Generates instructions to build and send a compact
block for an existing block, with a randomly chosen prefill transaction list
- `BlockTxnGenerator`: Generates instructions to build and send a `blocktxn`
message in response to a `getblocktxn` the node sent us (recorded by the probe)
- `GetCompactBlockGenerator`: Generates a `getdata(MSG_CMPCT_BLOCK)` to actively
fetch a compact block for a block the node announced via `headers`/`inv`
(BIP152 low bandwidth path)
- `GetBlockTxnGenerator`: Generates a `getblocktxn` request in response to a
`cmpctblock` the node announced to us (recorded by the probe), simulating the
compact block reconstruction side. Requested indexes are either *faithful*
(the transactions a peer with an empty mempool could not reconstruct, i.e. the
non-prefilled positions) or *adversarial* (an arbitrary, possibly out-of-range
set to exercise the node's validation and `blocktxn` response path)
- `OneParentOneChildGenerator`: Generates instructions for building two new
transactions (a 1-parent 1-child package) and sending them to a node
- ... see
Expand Down
26 changes: 25 additions & 1 deletion doc/src/design/scenarios.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,28 @@ crate. For example:
* [`IrScenario`](https://github.com/dergoegge/fuzzamoto/tree/master/fuzzamoto-scenarios/bin/ir.rs):
generic scenario for testing Bitcoin full nodes through the p2p interface.
Primarily meant to be fuzzed using `fuzzamoto-libafl` (custom fuzzer for
[Fuzzamoto IR](./ir.md)).
[Fuzzamoto IR](./ir.md)). Built as `scenario-ir`. In addition to `getblocktxn`
requests, it records compact block messages the node emits (`cmpctblock`,
plus `headers`/`inv` block announcements) and drains each connection with
recording enabled at the end of a run so that unsolicited high-bandwidth
`cmpctblock` announcements are observed. This feeds the `GetBlockTxnGenerator`
and `GetCompactBlockGenerator`, which simulate the BIP152 compact block
reconstruction side.

### Probe-driven feedback loop

Some p2p flows can only be exercised in response to a message the node sends
*us*. For example, a `getblocktxn` is only meaningful after the node announces a
`cmpctblock`, and that announcement is itself triggered by a new block arriving
on a *different* connection — a single source cannot send a `cmpctblock` and a
`getblocktxn` about it.

To handle this, `IrScenario` supports a `Probe` operation. When present, the
scenario records selected messages received from the node during execution,
decodes them, and reports them back to the fuzzer (the
[probing stage](https://github.com/oss-garage/fuzzamoto/tree/master/fuzzamoto-libafl/src/stages/probe.rs)
prepends `Probe` to a testcase and runs it once). The decoded observations
(e.g. "the node sent a `getblocktxn`/`cmpctblock`/block announcement, triggered
by instruction N, on connection C") are stored as per-testcase metadata.
Generators then use that metadata to insert the appropriate response right after
the instruction that triggered it.
7 changes: 5 additions & 2 deletions fuzzamoto-cli/src/commands/ir.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ use fuzzamoto_ir::compiler::Compiler;
use fuzzamoto_ir::{
AddTxToBlockGenerator, AddrRelayGenerator, AddrRelayV2Generator, AdvanceTimeGenerator,
BlockGenerator, BloomFilterAddGenerator, BloomFilterClearGenerator, BloomFilterLoadGenerator,
CompactFilterQueryGenerator, FullProgramContext, Generator, GetAddrGenerator, GetDataGenerator,
HeaderGenerator, InstructionContext, InventoryGenerator, LargeTxGenerator, LongChainGenerator,
CompactFilterQueryGenerator, FullProgramContext, Generator, GetAddrGenerator,
GetBlockTxnGenerator, GetCompactBlockGenerator, GetDataGenerator, HeaderGenerator,
InstructionContext, InventoryGenerator, LargeTxGenerator, LongChainGenerator,
OneParentOneChildGenerator, Program, ProgramBuilder, SendBlockGenerator, SendMessageGenerator,
SingleTxGenerator, TxoGenerator, WitnessGenerator,
};
Expand Down Expand Up @@ -216,6 +217,8 @@ fn all_generators(context: &FullProgramContext) -> Vec<Box<dyn Generator<ThreadR
Box::new(AddrRelayGenerator::default()),
Box::new(AddrRelayV2Generator::default()),
Box::new(GetAddrGenerator),
Box::new(GetBlockTxnGenerator),
Box::new(GetCompactBlockGenerator),
]
}

Expand Down
90 changes: 89 additions & 1 deletion fuzzamoto-ir/src/compiler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -469,6 +469,12 @@ impl Compiler {
self.handle_bip152_blocktxn_operations(instruction)?;
}

Operation::BeginBuildGetBlockTxn
| Operation::EndBuildGetBlockTxn
| Operation::AddIndexToGetBlockTxn => {
self.handle_bip152_getblocktxn_operations(instruction)?;
}

Operation::AddConnection | Operation::AddConnectionWithHandshake { .. } => {
self.handle_new_connection_operations(instruction)?;
}
Expand Down Expand Up @@ -498,7 +504,8 @@ impl Compiler {
| Operation::SendFilterAdd
| Operation::SendFilterClear
| Operation::SendCompactBlock
| Operation::SendBlockTxn => {
| Operation::SendBlockTxn
| Operation::SendGetBlockTxn => {
self.handle_message_sending_operations(instruction)?;
}

Expand Down Expand Up @@ -1385,6 +1392,17 @@ impl Compiler {
.clone();
self.emit_send_message(*connection_var, "blocktxn", &blocktxn);
}
Operation::SendGetBlockTxn => {
let connection_var = *self.get_input::<usize>(&instruction.inputs, 0)?;
let request = self
.get_input::<bitcoin::bip152::BlockTransactionsRequest>(&instruction.inputs, 1)?
.clone();
let sanitized = bitcoin::bip152::BlockTransactionsRequest {
block_hash: request.block_hash,
indexes: sanitize_getblocktxn_indexes(request.indexes),
};
self.emit_send_message(connection_var, "getblocktxn", &sanitized);
}
Operation::SendRawMessage => {
let connection_var = self.get_input::<usize>(&instruction.inputs, 0)?;
let message_type_var = self.get_input::<[char; 12]>(&instruction.inputs, 1)?;
Expand Down Expand Up @@ -1592,6 +1610,40 @@ impl Compiler {
Ok(())
}

fn handle_bip152_getblocktxn_operations(
&mut self,
instruction: &Instruction,
) -> Result<(), CompilerError> {
match &instruction.operation {
Operation::BeginBuildGetBlockTxn => {
let block = self.get_input::<Block>(&instruction.inputs, 0)?;
let request = bitcoin::bip152::BlockTransactionsRequest {
block_hash: block.block_hash(),
indexes: Vec::new(),
};
self.append_variable(request);
}
Operation::AddIndexToGetBlockTxn => {
let index = *self.get_input::<usize>(&instruction.inputs, 1)?;
let request = self.get_input_mut::<bitcoin::bip152::BlockTransactionsRequest>(
&instruction.inputs,
0,
)?;
request.indexes.push(index as u64);
}
Operation::EndBuildGetBlockTxn => {
let request = self
.get_input::<bitcoin::bip152::BlockTransactionsRequest>(&instruction.inputs, 0)?
.clone();
self.append_variable(request);
}
_ => unreachable!(
"Non-getblocktxn operation passed to handle_bip152_getblocktxn_operations"
),
}
Ok(())
}

fn handle_load_operations(&mut self, instruction: &Instruction) {
match &instruction.operation {
Operation::Nop {
Expand Down Expand Up @@ -2327,6 +2379,19 @@ impl Compiler {
}
}

/// Sanitize the index list of a `getblocktxn` request before encoding.
///
/// `BlockTransactionsRequest` is differentially `VarInt` encoded, which panics on non-increasing
/// indexes or on `u64::MAX`. The IR (and its mutators) may produce arbitrary index values, so we
/// drop `u64::MAX`, sort and dedup to guarantee a panic-free, well-formed encoding. The semantic
/// fuzzing value (e.g. out-of-range indexes past the end of the block) is preserved.
fn sanitize_getblocktxn_indexes(mut indexes: Vec<u64>) -> Vec<u64> {
indexes.retain(|&i| i != u64::MAX);
indexes.sort_unstable();
indexes.dedup();
indexes
}

#[cfg(test)]
mod tests {
use super::*;
Expand All @@ -2337,6 +2402,29 @@ mod tests {
Transaction, consensus::Decodable, opcodes::all::OP_PUSHNUM_1, taproot::LeafVersion,
};

#[test]
fn sanitize_getblocktxn_indexes_yields_encodable_request() {
use bitcoin::bip152::BlockTransactionsRequest;
use bitcoin::hashes::Hash;

// Hostile index list: out of order, duplicates, and u64::MAX (which would otherwise panic
// the differential VarInt encoder).
let raw = vec![5u64, 1, 1, 0, 9, u64::MAX, 3, 9];
let sanitized = sanitize_getblocktxn_indexes(raw);
assert_eq!(sanitized, vec![0, 1, 3, 5, 9]);

let request = BlockTransactionsRequest {
block_hash: bitcoin::BlockHash::all_zeros(),
indexes: sanitized.clone(),
};

// Must encode without panicking and decode back to the same indexes.
let bytes = bitcoin::consensus::encode::serialize(&request);
let decoded = BlockTransactionsRequest::consensus_decode(&mut bytes.as_slice())
.expect("sanitized getblocktxn request should decode");
assert_eq!(decoded.indexes, sanitized);
}

#[test]
fn compile_send_getaddr_emits_getaddr_message() {
let context = ProgramContext {
Expand Down
26 changes: 16 additions & 10 deletions fuzzamoto-ir/src/generators/block_txn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -138,17 +138,23 @@ impl<R: RngCore> Generator<R> for BlockTxnGenerator {
rng: &mut R,
meta: Option<&PerTestcaseMetadata>,
) -> Option<usize> {
if let Some(meta) = meta
&& !meta.block_txn_request().is_empty()
{
let blocktxn_req = meta.block_txn_request();
let choice = rng.gen_range(0..blocktxn_req.len());
let insertion_point = blocktxn_req[choice].triggering_instruction_index + 1;
Some(insertion_point)
} else {
program
.get_random_instruction_index(rng, &<Self as Generator<R>>::requested_context(self))
if let Some(meta) = meta {
// A `triggering_instruction_index` recorded against a different (e.g. since
// minimized/replaced) version of this corpus entry's program can point past its
// current end; `index <= program.instructions.len()` is exactly what makes the
// `instructions[..index]` slice in `IrGenerator::mutate` safe.
let valid: Vec<usize> = meta
.block_txn_request()
.iter()
.map(|r| r.triggering_instruction_index + 1)
.filter(|&index| index <= program.instructions.len())
.collect();
if !valid.is_empty() {
return Some(valid[rng.gen_range(0..valid.len())]);
}
}

program.get_random_instruction_index(rng, &<Self as Generator<R>>::requested_context(self))
}
}

Expand Down
Loading
Loading