Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
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
84 changes: 74 additions & 10 deletions crates/rpc/src/handlers/mining.rs
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,7 @@ pub(crate) fn generateblock(ctx: &Arc<Context>, params: &Value) -> Result<Value,
.ok_or(RpcError::MethodDisabled("mining is unavailable"))?;
let output = required_str(params, 0, "output is required")?;
let payout = generateblock_payout_script(output, convert::bitcoin_network(ctx.chain_network))?;
let transactions = parse_generateblock_transactions(params)?;
let transactions = parse_generateblock_transactions(ctx, params)?;
let submit = optional_bool(params, 2, true)?;
let generated = control
.generate(GenerateRequest {
Expand Down Expand Up @@ -362,7 +362,10 @@ fn optional_u64(params: &Value, index: usize, default: u64) -> Result<u64, RpcEr
.ok_or(RpcError::InvalidType("parameter must be an integer"))
}

fn parse_generateblock_transactions(params: &Value) -> Result<Vec<GenerateTx>, RpcError> {
fn parse_generateblock_transactions(
ctx: &Context,
params: &Value,
) -> Result<Vec<GenerateTx>, RpcError> {
let array = params_array(params)?;
let Some(value) = array.get(1) else {
return Err(RpcError::InvalidParams("transactions is required"));
Expand All @@ -380,19 +383,31 @@ fn parse_generateblock_transactions(params: &Value) -> Result<Vec<GenerateTx>, R
"transactions must be an array of hex strings",
));
};
// CONTRACT: docs/contracts/external-api.md#API-24
if let Ok(txid) = Txid::from_str(text) {
if ctx.mempool.read().entry_by_txid(&txid).is_none() {
return Err(generateblock_unknown_txid(text));
}
transactions.push(GenerateTx::Mempool(txid));
Comment thread
qodo-code-review[bot] marked this conversation as resolved.
Outdated
continue;
}
let bytes = from_hex(text)
.map_err(|()| RpcError::InvalidParams("transaction hex is not valid hexadecimal"))?;
let tx: Tx = deserialize(&bytes)
.map_err(|_| RpcError::InvalidParams("transaction hex could not be decoded"))?;
let bytes = from_hex(text).map_err(|()| generateblock_tx_decode_failed(text))?;
let tx: Tx = deserialize(&bytes).map_err(|_| generateblock_tx_decode_failed(text))?;
transactions.push(GenerateTx::Raw(tx));
}
Ok(transactions)
}

fn generateblock_unknown_txid(text: &str) -> RpcError {
RpcError::InvalidAddressOrKey(format!("Transaction {text} not in mempool."))
}

fn generateblock_tx_decode_failed(text: &str) -> RpcError {
RpcError::Deserialization(format!(
"Transaction decode failed for {text}. Make sure the tx has at least one input."
))
}

fn parse_block_template_request(params: &Value) -> Result<BlockTemplateRequest, RpcError> {
if params.is_null() {
return Ok(BlockTemplateRequest {
Expand Down Expand Up @@ -2073,11 +2088,20 @@ mod tests {
/// API-05: 64-character hex is a mempool txid; longer hex is a raw transaction.
#[test]
fn generateblock_keeps_raw_transactions() {
use bitcoin_rs_mempool::MempoolEntry;

let control = FakeMiningControl::with_template(sample_template());
let ctx = ctx_with_control(control.clone());
let tx = sample_raw_tx();
let raw_hex = to_lower_hex(&consensus_bytes(&tx));
let txid = Txid::from(Hash256::from_le_bytes(&[0xcd; 32]));
let pooled = sample_raw_tx();
let txid = pooled.txid();
{
let mut pool = ctx.mempool.pool().write();
pool.insert_entry(MempoolEntry::new(Arc::new(pooled), 100, 1_000, 1, 7))
.unwrap_or_else(|err| panic!("insert failed: {err}"));
}
let mut raw = sample_raw_tx();
raw.lock_time = 1;
let raw_hex = to_lower_hex(&consensus_bytes(&raw));
generateblock(&ctx, &json!([REGTEST_ADDRESS, [txid.to_string(), raw_hex]]))
.unwrap_or_else(|err| panic!("generateblock failed: {err}"));
let request = control
Expand All @@ -2087,10 +2111,50 @@ mod tests {
.unwrap_or_else(|| panic!("generateblock must call generate"));
assert_eq!(
request.selection,
GenerateSelection::Ordered(vec![GenerateTx::Mempool(txid), GenerateTx::Raw(tx)])
GenerateSelection::Ordered(vec![GenerateTx::Mempool(txid), GenerateTx::Raw(raw)])
);
}

// CONTRACT: docs/contracts/external-api.md#API-24
#[test]
fn generateblock_rejects_unknown_mempool_txid_like_core() {
let control = FakeMiningControl::with_template(sample_template());
let ctx = ctx_with_control(control);
let missing = "CD".repeat(32);
let error = generateblock(&ctx, &json!([REGTEST_ADDRESS, [missing.as_str()]]))
.err()
.unwrap_or_else(|| panic!("unknown mempool txid must fail at parse"));
assert_eq!(error.code(), RpcError::CORE_NOT_FOUND);
assert_eq!(
error.to_string(),
format!("Transaction {missing} not in mempool.")
);
}

// CONTRACT: docs/contracts/external-api.md#API-24
#[test]
fn generateblock_rejects_undecodable_raw_tx_like_core() {
let control = FakeMiningControl::with_template(sample_template());
let ctx = ctx_with_control(control);
for payload in ["00", "zz", "abcd"] {
let error = generateblock(&ctx, &json!([REGTEST_ADDRESS, [payload]]))
.err()
.unwrap_or_else(|| panic!("`{payload}` must fail decode"));
assert_eq!(
error.code(),
RpcError::CORE_DESERIALIZATION_ERROR,
"for `{payload}`"
);
assert_eq!(
error.to_string(),
format!(
"Transaction decode failed for {payload}. Make sure the tx has at least one input."
),
"for `{payload}`"
);
}
}

/// API-05: extra generateblock positionals are rejected, matching Core arity.
#[test]
fn generateblock_rejects_trailing_parameters() {
Expand Down
2 changes: 1 addition & 1 deletion crates/rpc/src/registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ declare_rows! {
"submitheader", SurfaceKind::Rpc, Status::Implemented, "", CORE_VERSION, "Header-only admission through the same tree path as inbound P2P headers. Decode failures are -22; missing previous or invalid headers are -25.", "0.4.0", Some(mining::submitheader);
"prioritisetransaction", SurfaceKind::Rpc, Status::Implemented, "", CORE_VERSION, "Dummy (params[1]) must be 0 or null; fee_delta is params[2]. Non-zero dummy is Core -8. Pooled dust outputs are -8 except on regtest.", "0.4.0", Some(mining::prioritisetransaction);
"generatetoaddress", SurfaceKind::Rpc, Status::Implemented, "", CORE_VERSION, "Assembles, solves, and submits n blocks paying the given address through the mining coordinator.", "0.4.0", Some(mining::generatetoaddress);
"generateblock", SurfaceKind::Rpc, Status::Implemented, "", CORE_VERSION, "Assembles and solves one block paying an address or descriptor from the listed mempool txids or raw txs in that order; third param is Core's submit flag.", "0.4.0", Some(mining::generateblock);
"generateblock", SurfaceKind::Rpc, Status::Implemented, "", CORE_VERSION, "Assembles and solves one block paying an address or descriptor from the listed mempool txids or raw txs in that order; third param is Core's submit flag. Unknown 64-hex txids are -5; raw-tx decode failures are -22.", "0.4.0", Some(mining::generateblock);
Comment thread
qodo-code-review[bot] marked this conversation as resolved.
Outdated
"getnetworkhashps", SurfaceKind::Rpc, Status::Implemented, "", CORE_VERSION, "Estimated hashes/s over a caller-chosen lookback ending at a caller-chosen height; default lookback 120, height the applied tip.", "0.4.0", Some(mining::getnetworkhashps);
"getprioritisedtransactions", SurfaceKind::Rpc, Status::Implemented, "", CORE_VERSION, "Projects the mempool's signed fee-delta overlay, including txids not currently pooled.", "0.4.0", Some(mining::getprioritisedtransactions);

Expand Down
2 changes: 1 addition & 1 deletion docs/contracts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ match a regression.
| [chain-events.md](chain-events.md) | `EVT-01`–`EVT-05` | `ChainSnapshot`, `ChainEventHint`, `ChainEventPublisher`, `ConsumerCursor`, `UndoStore`/`DisconnectMarker`, `ChainChangeProof`: the seam between the apply path and reconciliation consumers | `crates/node/src/txindex_worker.rs` (first consumer); any index mirroring the applied chain | `crates/node/src/state.rs` tests `record_publishes_snapshot_and_hints_in_commit_order`, `record_drops_hints_when_channel_full`, `active_chain_snapshot_anchors_at_restored_tip_after_restart`; `crates/node/src/txindex_worker_recovery_tests.rs` tests `shallow_reorg_rewinds_to_common_ancestor_then_replays`, `tip_change_during_rebuild_converges_on_new_tip`; `crates/node/src/apply.rs` tests `a_clean_disconnect_leaves_no_in_flight_marker`, `chain_change_proof_finish_restores_even_generation` |
| [mempool-mutations.md](mempool-mutations.md) | `MPL-01`–`MPL-04` | Gateway ordering invariant, `MutationEnvelope`/`MutationResult` semantics, ZMQ `A`/`R` payload bytes, generation-validated admission and chain-change fencing | apply path (`crates/node/src/apply.rs`), `sendrawtransaction` (`crates/rpc/src/handlers/tx.rs`), ZMQ `sequence` subscribers (enforcer `--enable-mempool`) | `crates/mempool/src/gateway.rs` test `accepted_and_block_inclusion_events_arrive_in_commit_order`; `crates/node/src/zmq_publisher.rs` tests `block_inclusion_suppresses_r_frames` and `mempool_event_payloads_carry_reversed_txid_label_and_le_sequence`; `crates/node/src/apply.rs` test `stable_generation_is_even_before_and_after_connect` |
| [mempool-policy.md](mempool-policy.md) | `POL-01` | Pointer: relay policy contract pinned to Core 31.1 | `sendrawtransaction`/`testmempoolaccept` (`crates/rpc/src/handlers/tx.rs`), P2P relay admission | `crates/mempool/tests/policy_contract.rs` and `crates/rpc/tests/policy_contract.rs` (`cargo test -p bitcoin-rs-mempool --test policy_contract` / `-p bitcoin-rs-rpc --test policy_contract`) |
| [external-api.md](external-api.md) | `API-01`–`API-23` | Pointer: JSON-RPC/REST/ZMQ manifest, generated reference, error code mappings, query budgeting, solo-mining generate, `getnetworkhashps` snapshot behavior, BIP22/BIP23 template extras, mainnet GBT operational gates, `submitheader`, GBT client-rule negotiation, `submitblock` decode, GBT proposal request parsing, `submitblock` uncommitted witness fill, Core v31 submit/proposal duplicate vocabulary, BIP22 reject-reason mapping, GBT `vbrequired` always 0, Core `CheckWitnessMalleation` reject reasons, GBT `coinbaseaux.flags` empty hex, `prioritisetransaction` dummy/`fee_delta` arity, pooled-dust refusal, reorged scripts-valid duplicate, getmininginfo omitted optional fields, and estimatesmartfee Core conf_target/estimate_mode gates | RPC/REST/ZMQ clients; `tools/bip300301-enforcer` | `crates/rpc/tests/manifest_coverage.rs` tests `rpc_rows_and_the_live_registry_agree_both_ways`, `generated_reference_matches_checked_in`; `crates/rpc/src/handlers/mining.rs` generate, GBT extra, GBT gate, `submitheader`, GBT rule-negotiation, `submitblock` decode, GBT proposal-parse, `coinbaseaux`, `prioritisetransaction` dummy/dust, and `getmininginfo` omit-null tests; `crates/node/tests/mining.rs` generate, GBT extra, `submitheader`, uncommitted-witness submit, submit/proposal duplicate, BIP22 reject-reason, `vbrequired`, witness-malleation proposal, and reorged scripts-valid duplicate tests; `crates/mining/src/coinbase.rs` uncommitted-witness tests; `crates/consensus/src/verify_block.rs` CheckWitnessMalleation tests; `crates/node/src/mining.rs` test `hash_ps_at_rejects_a_height_the_tip_cannot_resolve`; `crates/node/tests/mining.rs` test `network_hash_ps_rejects_core_invalid_windows` |
| [external-api.md](external-api.md) | `API-01`–`API-24` | Pointer: JSON-RPC/REST/ZMQ manifest, generated reference, error code mappings, query budgeting, solo-mining generate, `getnetworkhashps` snapshot behavior, BIP22/BIP23 template extras, mainnet GBT operational gates, `submitheader`, GBT client-rule negotiation, `submitblock` decode, GBT proposal request parsing, `submitblock` uncommitted witness fill, Core v31 submit/proposal duplicate vocabulary, BIP22 reject-reason mapping, GBT `vbrequired` always 0, Core `CheckWitnessMalleation` reject reasons, GBT `coinbaseaux.flags` empty hex, `prioritisetransaction` dummy/`fee_delta` arity, pooled-dust refusal, reorged scripts-valid duplicate, getmininginfo omitted optional fields, and estimatesmartfee Core conf_target/estimate_mode gates, and generateblock Core txid/raw-tx parse errors | RPC/REST/ZMQ clients; `tools/bip300301-enforcer` | `crates/rpc/tests/manifest_coverage.rs` tests `rpc_rows_and_the_live_registry_agree_both_ways`, `generated_reference_matches_checked_in`; `crates/rpc/src/handlers/mining.rs` generate, GBT extra, GBT gate, `submitheader`, GBT rule-negotiation, `submitblock` decode, GBT proposal-parse, `coinbaseaux`, `prioritisetransaction` dummy/dust, and `getmininginfo` omit-null tests; `crates/node/tests/mining.rs` generate, GBT extra, `submitheader`, uncommitted-witness submit, submit/proposal duplicate, BIP22 reject-reason, `vbrequired`, witness-malleation proposal, and reorged scripts-valid duplicate tests; `crates/mining/src/coinbase.rs` uncommitted-witness tests; `crates/consensus/src/verify_block.rs` CheckWitnessMalleation tests; `crates/node/src/mining.rs` test `hash_ps_at_rejects_a_height_the_tip_cannot_resolve`; `crates/node/tests/mining.rs` test `network_hash_ps_rejects_core_invalid_windows` |
| [wallet-facing.md](wallet-facing.md) | `WF-01`–`WF-03` | Public Esplora/JSON-RPC surface an external wallet may use; no `NodeState` / `UtxoSet` / index types | [bitcoin-wallet](https://github.com/gosuda/bitcoin-wallet) (`btcw`); any Esplora HTTP client | `bin/bitcoin-rs/tests/wallet_facing.rs` tests `external_wallet_can_scan_estimate_and_broadcast`, `source_does_not_import_node_internals`; `crates/rpc/src/esplora.rs` tests `esplora_lives_only_under_the_api_prefix`, `api_is_the_public_electrs_directory`, `esplora_is_the_mempool_backend_superset` |
| [p2p-wire.md](p2p-wire.md) | `P2P-01`–`P2P-02` | Pointer: command inventory in `crates/p2p/src/compat.rs`, handshake/reject/deviations pinned to Core 31.1 | `crates/p2p` peers; `crates/p2p/src/chain_query.rs` active-chain serving | `crates/p2p/tests/core_compat.rs` (`cargo test -p bitcoin-rs-p2p --test core_compat`); live lane `scripts/run-p2p-core-interop.sh` |
| [qa-corpus.md](qa-corpus.md) | `QAC-01` | Pointer: fuzz seed provenance and refresh rules | `fuzz/fuzz_targets/{p2p_message,block_decode,tx_decode,script_eval}.rs`; CI fuzz lanes | `fuzz/CORPUS_PROVENANCE.md` mapping table; targets run under `cargo fuzz run <target> -- -runs=10000` |
Expand Down
29 changes: 25 additions & 4 deletions docs/contracts/external-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ reject reasons. `API-18` is GBT `coinbaseaux.flags`. `API-19` is
`prioritisetransaction` dust-output refusal. `API-21` is GBT proposal /
`submitblock` duplicate for reorged scripts-valid bodies. `API-22` is
`getmininginfo` omitting unset optional fields. `API-23` is
`estimatesmartfee` Core `conf_target` and `estimate_mode` gates.
`estimatesmartfee` Core `conf_target` and `estimate_mode` gates. `API-24`
is `generateblock` Core txid/raw-tx parse errors.

## Clauses

Expand Down Expand Up @@ -81,7 +82,8 @@ reject reasons. `API-18` is GBT `coinbaseaux.flags`. `API-19` is
The transactions array is required (an explicit `[]` is coinbase-only).
Listed order is kept, those fees are not added to the coinbase, 64-character
hex is a mempool txid, and decoded raw transactions are included without
mempool admission. Extra positional arguments are rejected.
mempool admission. Extra positional arguments are rejected. Transaction
parse errors are `API-24`.

### `API-06`: `getnetworkhashps` snapshot and invalid-height behavior

Expand Down Expand Up @@ -298,6 +300,18 @@ reject reasons. `API-18` is GBT `coinbaseaux.flags`. `API-19` is
this node's estimator has one horizon.
- Trailing parameters are refused.

### `API-24`: `generateblock` txid and raw-tx parse errors

- **Owner**: `parse_generateblock_transactions` in
`crates/rpc/src/handlers/mining.rs`.
- 64-character hex is Core `Txid::FromHex`. A txid missing from the
mempool is `-5` `Transaction {str} not in mempool.` using the caller's
string.
- Anything else is Core `DecodeHexTx`. Invalid hex or a payload that is
not a complete transaction is `-22`
`Transaction decode failed for {str}. Make sure the tx has at least one
input.`

The wallet-facing subset of this surface — tip, fees, address/script
queries, and broadcast over Esplora, plus the key-free node RPCs — is
owned by [wallet-facing.md](wallet-facing.md).
Expand All @@ -320,8 +334,10 @@ owned by [wallet-facing.md](wallet-facing.md).
`generateblock_projects_hash_object`, `generateblock_accepts_addr_descriptor`,
`generateblock_without_submit_includes_hex`,
`generateblock_requires_transactions_array`, `generateblock_keeps_raw_transactions`,
`generateblock_rejects_trailing_parameters`,
`generateblock_rejects_invalid_supplied_checksums`
`generateblock_rejects_trailing_parameters`,
`generateblock_rejects_invalid_supplied_checksums`,
`generateblock_rejects_unknown_mempool_txid_like_core`,
`generateblock_rejects_undecodable_raw_tx_like_core`
- `crates/node/tests/mining.rs` tests `generate_mines_coinbase_only_blocks_to_the_tip`,
`generateblock_rejects_unknown_mempool_txid`,
`generateblock_raw_tx_does_not_require_mempool_admission`,
Expand Down Expand Up @@ -425,3 +441,8 @@ owned by [wallet-facing.md](wallet-facing.md).
`estimatesmartfee_rejects_conf_target_outside_core_range`,
`estimatesmartfee_rejects_unknown_estimate_mode`,
`estimatesmartfee_accepts_core_estimate_modes_and_rejects_trailing`
- `API-24`:
- `crates/rpc/src/handlers/mining.rs` tests
`generateblock_rejects_unknown_mempool_txid_like_core`,
`generateblock_rejects_undecodable_raw_tx_like_core`,
`generateblock_keeps_raw_transactions`
2 changes: 1 addition & 1 deletion docs/rpc-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ Unimplemented-set derivation: audited against the Bitcoin Core v31.0 source comm
| `submitheader` | 0.4.0 | Header-only admission through the same tree path as inbound P2P headers. Decode failures are -22; missing previous or invalid headers are -25. |
| `prioritisetransaction` | 0.4.0 | Dummy (params[1]) must be 0 or null; fee_delta is params[2]. Non-zero dummy is Core -8. Pooled dust outputs are -8 except on regtest. |
| `generatetoaddress` | 0.4.0 | Assembles, solves, and submits n blocks paying the given address through the mining coordinator. |
| `generateblock` | 0.4.0 | Assembles and solves one block paying an address or descriptor from the listed mempool txids or raw txs in that order; third param is Core's submit flag. |
| `generateblock` | 0.4.0 | Assembles and solves one block paying an address or descriptor from the listed mempool txids or raw txs in that order; third param is Core's submit flag. Unknown 64-hex txids are -5; raw-tx decode failures are -22. |
| `getnetworkhashps` | 0.4.0 | Estimated hashes/s over a caller-chosen lookback ending at a caller-chosen height; default lookback 120, height the applied tip. |
| `getprioritisedtransactions` | 0.4.0 | Projects the mempool's signed fee-delta overlay, including txids not currently pooled. |

Expand Down
Loading