Skip to content
Merged
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
6 changes: 6 additions & 0 deletions changelog.d/submitblock-witness-reserved-value.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Fixed

- **`submitblock` adds the coinbase witness reserved value like Core.**
A block with a witness commitment and no coinbase witness was rejected
as `bad-witness-nonce-size`. Core fills in 32 zero bytes once segwit is
active, and mining software may leave it out.
4 changes: 3 additions & 1 deletion crates/rbitcoin-net/src/chain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1557,7 +1557,9 @@ impl ChainHub {
self.stored_header_height(hash)
}

fn stored_header_height(&self, hash: &BlockHash) -> Option<u32> {
/// Height of a stored header, connected or not. Walks back to a known
/// height (at most 10_000 headers).
pub fn stored_header_height(&self, hash: &BlockHash) -> Option<u32> {
if let Some(h) = self.header_height(hash) {
return Some(h);
}
Expand Down
24 changes: 23 additions & 1 deletion crates/rbitcoin-rpc/src/methods/mine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -897,8 +897,29 @@ pub(crate) fn header_time(ctx: &RpcContext, h: u32) -> Option<u32> {
Some(rec.timestamp)
}

/// Core `UpdateUncommittedBlockStructures`: a coinbase that commits to
/// witnesses but carries no witness gets the 32 zero bytes, once segwit is
/// active for this block. Mining software may leave it out.
fn fill_witness_reserved_value(hub: &rbitcoin_net::ChainHub, block: &mut Block) {
let segwit = hub
.stored_header_height(&block.header.prev_blockhash)
.is_some_and(|h| hub.params.segwit_active_at(h.saturating_add(1)));
Comment thread
niteshbalusu11 marked this conversation as resolved.
let Some(coinbase) = block.txdata.first_mut() else {
return;
};
if !segwit
|| rbitcoin_consensus::witness_commitment_vout_index(coinbase).is_none()
|| coinbase.input.iter().any(|i| !i.witness.is_empty())
{
return;
}
if let Some(input) = coinbase.input.first_mut() {
input.witness = bitcoin::Witness::from_slice(&[[0u8; 32]]);
}
}

/// Same receive path as P2P `block` (all networks).
pub fn submit_received_block(hub: &rbitcoin_net::ChainHub, block: Block) -> SubmitBlockOutcome {
pub fn submit_received_block(hub: &rbitcoin_net::ChainHub, mut block: Block) -> SubmitBlockOutcome {
use bitcoin::Target;
use rbitcoin_net::AcceptOutcome;
let hash = block.block_hash();
Expand All @@ -922,6 +943,7 @@ pub fn submit_received_block(hub: &rbitcoin_net::ChainHub, block: Block) -> Subm
if !known {
return SubmitBlockOutcome::Rejected("prev-blk-not-found".into());
}
fill_witness_reserved_value(hub, &mut block);
match cheap_submit_tx_reject(
hub.query.as_ref(),
&block,
Expand Down
73 changes: 73 additions & 0 deletions crates/rbitcoin-rpc/src/methods_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4014,6 +4014,79 @@ fn submitblock_vout_past_n_out_is_missingorspent() {
let _ = std::fs::remove_dir_all(&dir);
}

/// Core `UpdateUncommittedBlockStructures`: a coinbase with a witness
/// commitment and no witness gets the 32 zero bytes once segwit is active.
/// Below segwit the commitment is plain data and the coinbase stays bare.
/// A reserved value the miner already set is kept, a block with no
/// commitment is left alone, and a parent known only as a header counts.
#[test]
fn submitblock_adds_the_witness_reserved_value_once_segwit_is_active() {
let mut params = rbitcoin_consensus::ChainParams::regtest();
params.apply_test_activation_height("segwit", 2).unwrap();
let (ctx, dir, hub) = ctx_regtest_hub_on(params, 300_000_000);
let mk = |prev: BlockHash, time: u32, height: u32, commit: Option<[u8; 32]>, own: bool| {
let mut b = rbitcoin_consensus::mine_regtest_paying(
prev,
time,
height,
ScriptBuf::from_bytes(vec![0x51]),
vec![],
);
if let Some(reserved) = commit {
if own {
b.txdata[0].input[0].witness = bitcoin::Witness::from_slice(&[reserved]);
}
let commitment = rbitcoin_consensus::witness_commitment_script([], &reserved);
b.txdata[0].output.push(bitcoin::TxOut {
value: Amount::ZERO,
script_pubkey: ScriptBuf::from_bytes(commitment),
});
b.header.merkle_root = b.compute_merkle_root().unwrap();
regrind(&mut b);
}
b
};
let submit = |b: &Block| dispatch(&ctx, "submitblock", vec![json!(block_hex(b))]).unwrap();
let mut prev = (hub.tip_hash().unwrap(), hub.tip_header().unwrap().time);
let zero = Some([0u8; 32]);
for (height, commit, own) in [
(1u32, zero, false),
(2, zero, false),
(3, Some([7u8; 32]), true),
(4, None, false),
] {
let b = mk(prev.0, prev.1 + 1, height, commit, own);
let r = submit(&b);
assert!(r.is_null(), "height {height}: {r}");
assert_eq!(hub.tip_hash(), Some(b.block_hash()), "height {height}");
prev = (b.block_hash(), b.header.time);
}
// Headers first: once its child's header arrives, h5 is a stored header
// with no body and no longer a header tip.
let b5 = mk(prev.0, prev.1 + 1, 5, None, false);
let b6 = mk(b5.block_hash(), prev.1 + 2, 6, zero, false);
for b in [&b5, &b6] {
dispatch(&ctx, "submitheader", vec![json!(serialize_hex(&b.header))]).unwrap();
}
assert_eq!(submit(&b6), "inconclusive", "held until h5's body");
assert!(submit(&b5).is_null());
assert_eq!(hub.tip_hash(), Some(b6.block_hash()));

let coinbase_witness = |h: u32| {
let b = hub.query.reconstruct_block_at_height(Height(h)).unwrap();
b.txdata[0].input[0].witness.to_vec()
};
assert!(coinbase_witness(1).is_empty(), "no fill below segwit");
assert_eq!(coinbase_witness(2), vec![vec![0u8; 32]]);
assert_eq!(coinbase_witness(3), vec![vec![7u8; 32]]);
assert!(
coinbase_witness(4).is_empty(),
"no fill without a commitment"
);
assert_eq!(coinbase_witness(6), vec![vec![0u8; 32]]);
let _ = std::fs::remove_dir_all(&dir);
}

/// A store fault reading the parent header is not `prev-blk-not-found`,
/// and on the tip-extend path it is not a reject reason that gets cached.
#[test]
Expand Down
2 changes: 1 addition & 1 deletion docs/rpc.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ still wait for durable SH when shindex is on.
| `getmempoolfeeratediagram` | All networks. Mining chunks as `{weight, fee}` points (decreasing feerate). |
| `submitpackage` | All networks. `MempoolHub::submit_package_rpc` admits each tx with trim deferred, then package-evaluates a remainder of two or more that failed static min relay, the dynamic mempool min fee, or missing inputs. The package path is a child-with-parents tree; an in-mempool parent outside the remainder still supplies prevouts. When that remainder's combined fee meets static min relay, those members skip the dynamic floor. `fees.effective-feerate` / `effective-includes` for an individually admitted tx are that tx's modified fee and its own wtxid; for a package-eval tx they are the remainder's summed modified fees and vsizes, in package order. One unprotected trim runs after the package; a tx that was admitted and then dropped is `mempool full`. Package-admitted txs use the same relay-age clock as an individual admit. RPC `maxfeerate` / `maxburnamount` / `"version"` pre-checks. A member whose own feerate exceeds `maxfeerate`, including a parent that exists only in the package, is rejected (`max feerate exceeded`) and is not used to rescue the rest. An individual admit still lists the transactions it replaced. A 3-gen chain admits when fees/policy allow. A later member that failed missing-inputs and is not in a successful remainder stays `bad-txns-inputs-missingorspent`. `IsChildWithParents` `-25` applies only when `RBITCOIN_RPC_PACKAGE_DIALECT` is on ([`env-knobs.md`](./env-knobs.md)). `package_msg` / `tx-results` / `replaced-transactions`. Esplora `POST /txs/package` and Electrum `broadcast_package` still use atomic `accept_package` (all-or-nothing; child-fail rollback). |
| `gettxspendingprevout` | All networks. Live mempool spender of each `{txid,vout}`. |
| `submitblock` | All networks. Same `ChainHub::accept_received_block` as a P2P `block` message: tip-extend, or hold by hash + most-work `accept_branch`. A consensus reject is a BIP22 reason string. A local store fault, including one reading the parent or tip header or a spent output, is RPC error `-25` (Core `state.IsError()`). A cancelled connect is `inconclusive`. Neither caches the block as invalid. |
| `submitblock` | All networks. Like Core, a coinbase with a witness commitment and no witness first gets the 32-byte zero reserved value once segwit is active. The parent's height comes from the connected chain, a header tip, or a walk of at most 10,000 stored headers. A parent past that walk is treated as before segwit, so the block is left as sent. Then the same `ChainHub::accept_received_block` as a P2P `block` message: tip-extend, or hold by hash + most-work `accept_branch`. A consensus reject is a BIP22 reason string. A local store fault, including one reading the parent or tip header or a spent output, is RPC error `-25` (Core `state.IsError()`). A cancelled connect is `inconclusive`. Neither caches the block as invalid. |
| `getblockfilter` | All networks. Requires `--block-filter-index`. `filtertype` `basic` only (anything else is `-5`). Returns `filter` and `header` hex. A sealed best-chain height uses the index. One block whose parent filter header is already sealed (genesis uses the zero prev-header) is built from the stored body. A stale branch is rebuilt back to that sealed fork point. An unsealed best-chain gap is “Filter not found. Block filters are still in the process of being indexed.” Unknown hash is `-5` “Block not found”. Flag off is “Index is not enabled”. REST `/rest/blockfilter/` stays watermark-only (“still in the process of being indexed” past the watermark). |
| `gettxout` | All networks. Connected Class A + mempool. Default `include_mempool=true` returns `null` for a confirmed out spent by a live mempool tx. `include_mempool=false` still returns the confirmed coin. A leftover still live in the hub (IBD / `-blocksonly`) uses the connected path, not `confirmations: 0`. A disconnected archive row is `null` (not tip+1 confirmations). A vout at or past the output count is `null`. The genesis coinbase output is `null` (Core never adds it to the coins view); REST `getutxos` follows. |
| `getindexinfo` | All networks. Reports `txindex` synced at tip — we reconstruct by txid from Class A (no separate index flag). |
Expand Down
Loading