Skip to content

Commit 86055d9

Browse files
committed
Merge rust-bitcoin/rust-bitcoin#672: New Witness struct to improve ser/de perfomance
106acdc Add fuzzing for Witness struct (Riccardo Casatta) 2fd0125 Introduce Witness struct mainly to improve ser/de performance while keeping most usability. (Riccardo Casatta) Pull request description: At the moment the Witness struct is `Vec<Vec<u8>>`, the vec inside a vec cause a lot of allocations, specifically: - empty witness -> 1 allocation, while an empty vec doesn't allocate, the outer vec is not empty - witness with n elements -> n+1 allocations The proposed Witness struct contains the serialized format of the witness. This reduces the allocations to: - empty witness -> 0 allocations - witness with n elements -> 1 allocation for most common cases (you don't know how many bytes is long the entire witness beforehand, thus you need to estimate a good value, not too big to avoid wasting space and not too low to avoid vector reallocation, I used 128 since it covers about 80% of cases on mainnet) The inconvenience is having slightly less comfortable access to the witness, but the iterator is efficient (no allocations) and you can always collect the iteration to have a Vec of slices. If you collect the iteration you end up doing allocation anyway, but the rationale is that it is an operation you need to do rarely while ser/de is done much more often. I had to add a bigger block to better see the improvement (ae860247e191e2136d7c87382f78c96e0908d700), these are the results of the benches on my machine: ``` RCasatta/master_with_block test blockdata::block::benches::bench_block_deserialize ... bench: 5,496,821 ns/iter (+/- 298,859) test blockdata::block::benches::bench_block_serialize ... bench: 437,389 ns/iter (+/- 31,576) test blockdata::block::benches::bench_block_serialize_logic ... bench: 108,759 ns/iter (+/- 5,807) test blockdata::transaction::benches::bench_transaction_deserialize ... bench: 670 ns/iter (+/- 49) test blockdata::transaction::benches::bench_transaction_get_size ... bench: 7 ns/iter (+/- 0) test blockdata::transaction::benches::bench_transaction_serialize ... bench: 51 ns/iter (+/- 5) test blockdata::transaction::benches::bench_transaction_serialize_logic ... bench: 13 ns/iter (+/- 0) branch witness_with_block (this one) test blockdata::block::benches::bench_block_deserialize ... bench: 4,302,788 ns/iter (+/- 424,806) test blockdata::block::benches::bench_block_serialize ... bench: 366,493 ns/iter (+/- 42,216) test blockdata::block::benches::bench_block_serialize_logic ... bench: 84,646 ns/iter (+/- 7,366) test blockdata::transaction::benches::bench_transaction_deserialize ... bench: 648 ns/iter (+/- 77) test blockdata::transaction::benches::bench_transaction_get_size ... bench: 7 ns/iter (+/- 0) test blockdata::transaction::benches::bench_transaction_serialize ... bench: 50 ns/iter (+/- 5) test blockdata::transaction::benches::bench_transaction_serialize_logic ... bench: 14 ns/iter (+/- 0) ``` With an increased performance to deserialize a block of about 21% and to serialize a block of about 16% (seems even higher than expected, need to do more tests to confirm, I'll appreciate tests results from reviewers) ACKs for top commit: apoelstra: ACK 106acdc sanket1729: ACK 106acdc dr-orlovsky: utACK 106acdc Tree-SHA512: e4f23bdd55075c7ea788bc55846fd9e30f9cb76d5847cb259bddbf72523857715b0d4dbac505be3dfb9d4b1bcae289384ab39885b4887e188f8f1c06caf4049a
2 parents e511670 + 106acdc commit 86055d9

File tree

12 files changed

+501
-38
lines changed

12 files changed

+501
-38
lines changed

.github/workflows/fuzz.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ jobs:
1111
strategy:
1212
fail-fast: false
1313
matrix:
14-
fuzz_target: [deser_net_msg, deserialize_address, deserialize_amount, deserialize_block, deserialize_psbt, deserialize_script, deserialize_transaction, outpoint_string, uint128_fuzz, script_bytes_to_asm_fmt]
14+
fuzz_target: [deser_net_msg, deserialize_address, deserialize_amount, deserialize_block, deserialize_psbt, deserialize_script, deserialize_transaction, deserialize_witness, outpoint_string, uint128_fuzz, script_bytes_to_asm_fmt]
1515
steps:
1616
- name: Install test dependencies
1717
run: sudo apt-get update -y && sudo apt-get install -y binutils-dev libunwind8-dev libcurl4-openssl-dev libelf-dev libdw-dev cmake gcc libiberty-dev

fuzz/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,3 +59,7 @@ path = "fuzz_targets/uint128_fuzz.rs"
5959
[[bin]]
6060
name = "script_bytes_to_asm_fmt"
6161
path = "fuzz_targets/script_bytes_to_asm_fmt.rs"
62+
63+
[[bin]]
64+
name = "deserialize_witness"
65+
path = "fuzz_targets/deserialize_witness.rs"

fuzz/fuzz_targets/deserialize_transaction.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ fn do_test(data: &[u8]) {
1010
let len = ser.len();
1111
let calculated_weight = tx.get_weight();
1212
for input in &mut tx.input {
13-
input.witness = vec![];
13+
input.witness = bitcoin::blockdata::witness::Witness::default();
1414
}
1515
let no_witness_len = bitcoin::consensus::encode::serialize(&tx).len();
1616
// For 0-input transactions, `no_witness_len` will be incorrect because
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
extern crate bitcoin;
2+
3+
use bitcoin::consensus::{serialize, deserialize};
4+
use bitcoin::blockdata::witness::Witness;
5+
6+
fn do_test(data: &[u8]) {
7+
let w: Result<Witness, _> = deserialize(data);
8+
if let Ok(witness) = w {
9+
let serialized = serialize(&witness);
10+
assert_eq!(data, serialized);
11+
}
12+
}
13+
14+
#[cfg(feature = "afl")]
15+
#[macro_use] extern crate afl;
16+
#[cfg(feature = "afl")]
17+
fn main() {
18+
fuzz!(|data| {
19+
do_test(&data);
20+
});
21+
}
22+
23+
#[cfg(feature = "honggfuzz")]
24+
#[macro_use] extern crate honggfuzz;
25+
#[cfg(feature = "honggfuzz")]
26+
fn main() {
27+
loop {
28+
fuzz!(|data| {
29+
do_test(data);
30+
});
31+
}
32+
}
33+
34+
#[cfg(test)]
35+
mod tests {
36+
fn extend_vec_from_hex(hex: &str, out: &mut Vec<u8>) {
37+
let mut b = 0;
38+
for (idx, c) in hex.as_bytes().iter().enumerate() {
39+
b <<= 4;
40+
match *c {
41+
b'A'..=b'F' => b |= c - b'A' + 10,
42+
b'a'..=b'f' => b |= c - b'a' + 10,
43+
b'0'..=b'9' => b |= c - b'0',
44+
_ => panic!("Bad hex"),
45+
}
46+
if (idx & 1) == 1 {
47+
out.push(b);
48+
b = 0;
49+
}
50+
}
51+
}
52+
53+
#[test]
54+
fn duplicate_crash() {
55+
let mut a = Vec::new();
56+
extend_vec_from_hex("00", &mut a);
57+
super::do_test(&a);
58+
}
59+
}

src/blockdata/block.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -199,9 +199,10 @@ impl Block {
199199
o.script_pubkey[0..6] == [0x6a, 0x24, 0xaa, 0x21, 0xa9, 0xed] }) {
200200
let commitment = WitnessCommitment::from_slice(&coinbase.output[pos].script_pubkey.as_bytes()[6..38]).unwrap();
201201
// witness reserved value is in coinbase input witness
202-
if coinbase.input[0].witness.len() == 1 && coinbase.input[0].witness[0].len() == 32 {
202+
let witness_vec: Vec<_> = coinbase.input[0].witness.iter().collect();
203+
if witness_vec.len() == 1 && witness_vec[0].len() == 32 {
203204
match self.witness_root() {
204-
Some(witness_root) => return commitment == Self::compute_witness_commitment(&witness_root, coinbase.input[0].witness[0].as_slice()),
205+
Some(witness_root) => return commitment == Self::compute_witness_commitment(&witness_root, witness_vec[0]),
205206
None => return false,
206207
}
207208
}

src/blockdata/constants.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ use blockdata::opcodes;
2929
use blockdata::script;
3030
use blockdata::transaction::{OutPoint, Transaction, TxOut, TxIn};
3131
use blockdata::block::{Block, BlockHeader};
32+
use blockdata::witness::Witness;
3233
use network::constants::Network;
3334
use util::uint::Uint256;
3435

@@ -93,7 +94,7 @@ fn bitcoin_genesis_tx() -> Transaction {
9394
previous_output: OutPoint::null(),
9495
script_sig: in_script,
9596
sequence: MAX_SEQUENCE,
96-
witness: vec![],
97+
witness: Witness::default(),
9798
});
9899

99100
// Outputs

src/blockdata/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,5 @@ pub mod opcodes;
2323
pub mod script;
2424
pub mod transaction;
2525
pub mod block;
26+
pub mod witness;
2627

src/blockdata/transaction.rs

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ use util::endian;
3636
use blockdata::constants::WITNESS_SCALE_FACTOR;
3737
#[cfg(feature="bitcoinconsensus")] use blockdata::script;
3838
use blockdata::script::Script;
39+
use blockdata::witness::Witness;
3940
use consensus::{encode, Decodable, Encodable};
4041
use consensus::encode::MAX_VEC_SIZE;
4142
use hash_types::{SigHash, Txid, Wtxid};
@@ -197,7 +198,7 @@ pub struct TxIn {
197198
/// Encodable/Decodable, as it is (de)serialized at the end of the full
198199
/// Transaction. It *is* (de)serialized with the rest of the TxIn in other
199200
/// (de)serialization routines.
200-
pub witness: Vec<Vec<u8>>
201+
pub witness: Witness
201202
}
202203

203204
impl Default for TxIn {
@@ -206,7 +207,7 @@ impl Default for TxIn {
206207
previous_output: OutPoint::default(),
207208
script_sig: Script::new(),
208209
sequence: u32::max_value(),
209-
witness: Vec::new(),
210+
witness: Witness::default(),
210211
}
211212
}
212213
}
@@ -280,7 +281,7 @@ impl Transaction {
280281
let cloned_tx = Transaction {
281282
version: self.version,
282283
lock_time: self.lock_time,
283-
input: self.input.iter().map(|txin| TxIn { script_sig: Script::new(), witness: vec![], .. *txin }).collect(),
284+
input: self.input.iter().map(|txin| TxIn { script_sig: Script::new(), witness: Witness::default(), .. *txin }).collect(),
284285
output: self.output.clone(),
285286
};
286287
cloned_tx.txid().into()
@@ -357,7 +358,7 @@ impl Transaction {
357358
previous_output: self.input[input_index].previous_output,
358359
script_sig: script_pubkey.clone(),
359360
sequence: self.input[input_index].sequence,
360-
witness: vec![],
361+
witness: Witness::default(),
361362
}];
362363
} else {
363364
tx.input = Vec::with_capacity(self.input.len());
@@ -366,7 +367,7 @@ impl Transaction {
366367
previous_output: input.previous_output,
367368
script_sig: if n == input_index { script_pubkey.clone() } else { Script::new() },
368369
sequence: if n != input_index && (sighash == EcdsaSigHashType::Single || sighash == EcdsaSigHashType::None) { 0 } else { input.sequence },
369-
witness: vec![],
370+
witness: Witness::default(),
370371
});
371372
}
372373
}
@@ -480,10 +481,7 @@ impl Transaction {
480481
input.script_sig.len());
481482
if !input.witness.is_empty() {
482483
inputs_with_witnesses += 1;
483-
input_weight += VarInt(input.witness.len() as u64).len();
484-
for elem in &input.witness {
485-
input_weight += VarInt(elem.len() as u64).len() + elem.len();
486-
}
484+
input_weight += input.witness.serialized_len();
487485
}
488486
}
489487
let mut output_size = 0;
@@ -585,7 +583,7 @@ impl Decodable for TxIn {
585583
previous_output: Decodable::consensus_decode(&mut d)?,
586584
script_sig: Decodable::consensus_decode(&mut d)?,
587585
sequence: Decodable::consensus_decode(d)?,
588-
witness: vec![],
586+
witness: Witness::default(),
589587
})
590588
}
591589
}
@@ -1458,6 +1456,7 @@ mod tests {
14581456
use hashes::hex::FromHex;
14591457
use std::collections::HashMap;
14601458
use blockdata::script;
1459+
use blockdata::witness::Witness;
14611460

14621461
// a random recent segwit transaction from blockchain using both old and segwit inputs
14631462
let mut spending: Transaction = deserialize(Vec::from_hex("020000000001031cfbc8f54fbfa4a33a30068841371f80dbfe166211242213188428f437445c91000000006a47304402206fbcec8d2d2e740d824d3d36cc345b37d9f65d665a99f5bd5c9e8d42270a03a8022013959632492332200c2908459547bf8dbf97c65ab1a28dec377d6f1d41d3d63e012103d7279dfb90ce17fe139ba60a7c41ddf605b25e1c07a4ddcb9dfef4e7d6710f48feffffff476222484f5e35b3f0e43f65fc76e21d8be7818dd6a989c160b1e5039b7835fc00000000171600140914414d3c94af70ac7e25407b0689e0baa10c77feffffffa83d954a62568bbc99cc644c62eb7383d7c2a2563041a0aeb891a6a4055895570000000017160014795d04cc2d4f31480d9a3710993fbd80d04301dffeffffff06fef72f000000000017a91476fd7035cd26f1a32a5ab979e056713aac25796887a5000f00000000001976a914b8332d502a529571c6af4be66399cd33379071c588ac3fda0500000000001976a914fc1d692f8de10ae33295f090bea5fe49527d975c88ac522e1b00000000001976a914808406b54d1044c429ac54c0e189b0d8061667e088ac6eb68501000000001976a914dfab6085f3a8fb3e6710206a5a959313c5618f4d88acbba20000000000001976a914eb3026552d7e3f3073457d0bee5d4757de48160d88ac0002483045022100bee24b63212939d33d513e767bc79300051f7a0d433c3fcf1e0e3bf03b9eb1d70220588dc45a9ce3a939103b4459ce47500b64e23ab118dfc03c9caa7d6bfc32b9c601210354fd80328da0f9ae6eef2b3a81f74f9a6f66761fadf96f1d1d22b1fd6845876402483045022100e29c7e3a5efc10da6269e5fc20b6a1cb8beb92130cc52c67e46ef40aaa5cac5f0220644dd1b049727d991aece98a105563416e10a5ac4221abac7d16931842d5c322012103960b87412d6e169f30e12106bdf70122aabb9eb61f455518322a18b920a4dfa887d30700")
@@ -1496,7 +1495,9 @@ mod tests {
14961495
}).is_err());
14971496

14981497
// test that we get a failure if we corrupt a signature
1499-
spending.input[1].witness[0][10] = 42;
1498+
let mut witness: Vec<_> = spending.input[1].witness.to_vec();
1499+
witness[0][10] = 42;
1500+
spending.input[1].witness = Witness::from_vec(witness);
15001501
match spending.verify(|point: &OutPoint| {
15011502
if let Some(tx) = spent3.remove(&point.txid) {
15021503
return tx.output.get(point.vout as usize).cloned();

0 commit comments

Comments
 (0)