mirror of
https://github.com/mimblewimble/grin.git
synced 2025-02-01 08:51:08 +03:00
Add TransactionPool fuzz driver (#3396)
Add fuzz driver for fuzzing adding transactions to the main and stem TransactionPool Co-authored-by: Nym Seddon <unseddd@shh.xyz>
This commit is contained in:
parent
a038687d20
commit
6a012d7e5b
5 changed files with 2114 additions and 0 deletions
4
pool/fuzz/.gitignore
vendored
Normal file
4
pool/fuzz/.gitignore
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
|
||||
target
|
||||
corpus
|
||||
artifacts
|
1640
pool/fuzz/Cargo.lock
generated
Normal file
1640
pool/fuzz/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
28
pool/fuzz/Cargo.toml
Normal file
28
pool/fuzz/Cargo.toml
Normal file
|
@ -0,0 +1,28 @@
|
|||
[package]
|
||||
name = "grin_pool_fuzz"
|
||||
version = "0.0.0"
|
||||
authors = ["Automatically generated"]
|
||||
publish = false
|
||||
edition = "2018"
|
||||
|
||||
[package.metadata]
|
||||
cargo-fuzz = true
|
||||
|
||||
[dependencies]
|
||||
chrono = "0.4.11"
|
||||
libfuzzer-sys = "0.3"
|
||||
grin_chain = { path = "../../chain" }
|
||||
grin_core = { path = "../../core" }
|
||||
grin_keychain = { path = "../../keychain" }
|
||||
grin_pool = { path = ".." }
|
||||
grin_util = { path = "../../util" }
|
||||
|
||||
# Prevent this from interfering with workspaces
|
||||
[workspace]
|
||||
members = ["."]
|
||||
|
||||
[[bin]]
|
||||
name = "transaction_pool"
|
||||
path = "fuzz_targets/transaction_pool.rs"
|
||||
test = false
|
||||
doc = false
|
316
pool/fuzz/fuzz_targets/common.rs
Normal file
316
pool/fuzz/fuzz_targets/common.rs
Normal file
|
@ -0,0 +1,316 @@
|
|||
// Copyright 2020 The Grin Developers
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! Common test functions
|
||||
|
||||
use self::chain::types::{NoopAdapter, Options};
|
||||
use self::chain::Chain;
|
||||
use self::core::consensus;
|
||||
use self::core::core::hash::Hash;
|
||||
use self::core::core::verifier_cache::{LruVerifierCache, VerifierCache};
|
||||
use self::core::core::{Block, BlockHeader, BlockSums, KernelFeatures, Transaction};
|
||||
use self::core::genesis;
|
||||
use self::core::global;
|
||||
use self::core::libtx::{build, reward, ProofBuilder};
|
||||
use self::core::pow;
|
||||
use self::keychain::{ExtKeychain, ExtKeychainPath, Keychain};
|
||||
use self::pool::types::*;
|
||||
use self::pool::TransactionPool;
|
||||
use self::util::RwLock;
|
||||
use chrono::Duration;
|
||||
use grin_chain as chain;
|
||||
use grin_core as core;
|
||||
use grin_keychain as keychain;
|
||||
use grin_pool as pool;
|
||||
use grin_util as util;
|
||||
use std::fs;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Build genesis block with reward (non-empty, like we have in mainnet).
|
||||
// Same as from pool/tests/common.rs
|
||||
pub fn genesis_block<K>(keychain: &K) -> Block
|
||||
where
|
||||
K: Keychain,
|
||||
{
|
||||
let key_id = keychain::ExtKeychain::derive_key_id(1, 0, 0, 0, 0);
|
||||
let reward = reward::output(keychain, &ProofBuilder::new(keychain), &key_id, 0, false).unwrap();
|
||||
|
||||
genesis::genesis_dev().with_reward(reward.0, reward.1)
|
||||
}
|
||||
|
||||
pub fn fuzz_tx_source(nonce: u8) -> TxSource {
|
||||
if nonce < 51 {
|
||||
TxSource::PushApi
|
||||
} else if nonce < 102 {
|
||||
TxSource::Broadcast
|
||||
} else if nonce < 153 {
|
||||
TxSource::Fluff
|
||||
} else if nonce < 204 {
|
||||
TxSource::EmbargoExpired
|
||||
} else {
|
||||
TxSource::Deaggregate
|
||||
}
|
||||
}
|
||||
|
||||
// Same as from pool/tests/common.rs
|
||||
#[derive(Clone)]
|
||||
pub struct ChainAdapter {
|
||||
pub chain: Arc<Chain>,
|
||||
}
|
||||
|
||||
// Same as from pool/tests/common.rs
|
||||
impl BlockChain for ChainAdapter {
|
||||
fn chain_head(&self) -> Result<BlockHeader, PoolError> {
|
||||
self.chain
|
||||
.head_header()
|
||||
.map_err(|_| PoolError::Other("failed to get chain head".into()))
|
||||
}
|
||||
|
||||
fn get_block_header(&self, hash: &Hash) -> Result<BlockHeader, PoolError> {
|
||||
self.chain
|
||||
.get_block_header(hash)
|
||||
.map_err(|_| PoolError::Other("failed to get block header".into()))
|
||||
}
|
||||
|
||||
fn get_block_sums(&self, hash: &Hash) -> Result<BlockSums, PoolError> {
|
||||
self.chain
|
||||
.get_block_sums(hash)
|
||||
.map_err(|_| PoolError::Other("failed to get block sums".into()))
|
||||
}
|
||||
|
||||
fn validate_tx(&self, tx: &Transaction) -> Result<(), pool::PoolError> {
|
||||
self.chain.validate_tx(tx).map_err(|e| match e.kind() {
|
||||
chain::ErrorKind::Transaction(txe) => txe.into(),
|
||||
chain::ErrorKind::NRDRelativeHeight => PoolError::NRDKernelRelativeHeight,
|
||||
_ => PoolError::Other("failed to validate tx".into()),
|
||||
})
|
||||
}
|
||||
|
||||
fn verify_coinbase_maturity(&self, tx: &Transaction) -> Result<(), PoolError> {
|
||||
self.chain
|
||||
.verify_coinbase_maturity(tx)
|
||||
.map_err(|_| PoolError::ImmatureCoinbase)
|
||||
}
|
||||
|
||||
fn verify_tx_lock_height(&self, tx: &Transaction) -> Result<(), PoolError> {
|
||||
self.chain
|
||||
.verify_tx_lock_height(tx)
|
||||
.map_err(|_| PoolError::ImmatureTransaction)
|
||||
}
|
||||
}
|
||||
|
||||
// Same as from pool/tests/common.rs
|
||||
pub fn clean_output_dir(db_root: String) {
|
||||
if let Err(e) = fs::remove_dir_all(db_root) {
|
||||
println!("cleaning output dir failed - {:?}", e)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PoolFuzzer {
|
||||
pub chain: Arc<Chain>,
|
||||
pub keychain: ExtKeychain,
|
||||
pub pool: TransactionPool<ChainAdapter, NoopPoolAdapter, LruVerifierCache>,
|
||||
}
|
||||
|
||||
impl PoolFuzzer {
|
||||
pub fn new(db_root: &str) -> Self {
|
||||
let keychain: ExtKeychain = Keychain::from_random_seed(false).unwrap();
|
||||
|
||||
clean_output_dir(db_root.into());
|
||||
|
||||
let genesis = genesis_block(&keychain);
|
||||
let chain = Arc::new(Self::init_chain(db_root, genesis));
|
||||
let verifier_cache = Arc::new(RwLock::new(LruVerifierCache::new()));
|
||||
|
||||
// Initialize a new pool with our chain adapter.
|
||||
let pool = Self::init_transaction_pool(
|
||||
Arc::new(ChainAdapter {
|
||||
chain: chain.clone(),
|
||||
}),
|
||||
verifier_cache.clone(),
|
||||
);
|
||||
|
||||
let ret = Self {
|
||||
chain,
|
||||
keychain,
|
||||
pool,
|
||||
};
|
||||
|
||||
ret.add_some_blocks(3);
|
||||
|
||||
ret
|
||||
}
|
||||
|
||||
// Same as from pool/tests/common.rs, with interface change
|
||||
pub fn test_transaction_spending_coinbase(&self, output_values: Vec<u64>) -> Transaction {
|
||||
let header = self.chain.get_header_by_height(1).unwrap();
|
||||
|
||||
let mut output_sum = 0u64;
|
||||
for &s in output_values.iter() {
|
||||
output_sum = output_sum.overflowing_add(s).0;
|
||||
}
|
||||
|
||||
let coinbase_reward: u64 = 60_000_000_000;
|
||||
|
||||
let fees = coinbase_reward.overflowing_sub(output_sum).0;
|
||||
|
||||
let mut tx_elements = Vec::new();
|
||||
|
||||
// single input spending a single coinbase (deterministic key_id aka height)
|
||||
{
|
||||
let key_id = ExtKeychain::derive_key_id(1, header.height as u32, 0, 0, 0);
|
||||
tx_elements.push(build::coinbase_input(coinbase_reward, key_id));
|
||||
}
|
||||
|
||||
for output_value in output_values {
|
||||
let key_id = ExtKeychain::derive_key_id(1, output_value as u32, 0, 0, 0);
|
||||
tx_elements.push(build::output(output_value, key_id));
|
||||
}
|
||||
|
||||
build::transaction(
|
||||
KernelFeatures::Plain { fee: fees },
|
||||
tx_elements,
|
||||
&self.keychain,
|
||||
&ProofBuilder::new(&self.keychain),
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
// Same as from pool/tests/common.rs,
|
||||
// with changes for summing inputs and outputs
|
||||
pub fn test_transaction(&self, input_values: Vec<u64>, output_values: Vec<u64>) -> Transaction {
|
||||
let mut input_sum = 0u64;
|
||||
for &s in input_values.iter() {
|
||||
input_sum = input_sum.overflowing_add(s).0;
|
||||
}
|
||||
|
||||
let mut output_sum = 0u64;
|
||||
for &s in output_values.iter() {
|
||||
output_sum = output_sum.overflowing_add(s).0;
|
||||
}
|
||||
|
||||
let fees = input_sum.overflowing_sub(output_sum).0;
|
||||
|
||||
self.test_transaction_with_kernel_features(
|
||||
input_values,
|
||||
output_values,
|
||||
KernelFeatures::Plain { fee: fees },
|
||||
)
|
||||
}
|
||||
|
||||
pub fn test_transaction_with_kernel_features(
|
||||
&self,
|
||||
input_values: Vec<u64>,
|
||||
output_values: Vec<u64>,
|
||||
kernel_features: KernelFeatures,
|
||||
) -> Transaction {
|
||||
let mut tx_elements = Vec::new();
|
||||
|
||||
for input_value in input_values {
|
||||
let key_id = ExtKeychain::derive_key_id(1, input_value as u32, 0, 0, 0);
|
||||
tx_elements.push(build::input(input_value, key_id));
|
||||
}
|
||||
|
||||
for output_value in output_values {
|
||||
let key_id = ExtKeychain::derive_key_id(1, output_value as u32, 0, 0, 0);
|
||||
tx_elements.push(build::output(output_value, key_id));
|
||||
}
|
||||
|
||||
let keychain = &self.keychain;
|
||||
|
||||
build::transaction(
|
||||
kernel_features,
|
||||
tx_elements,
|
||||
keychain,
|
||||
&ProofBuilder::new(keychain),
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn init_chain(dir_name: &str, genesis: Block) -> Chain {
|
||||
let verifier_cache = Arc::new(RwLock::new(LruVerifierCache::new()));
|
||||
Chain::init(
|
||||
dir_name.to_string(),
|
||||
Arc::new(NoopAdapter {}),
|
||||
genesis,
|
||||
pow::verify_size,
|
||||
verifier_cache,
|
||||
false,
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
// Same as from pool/tests/common.rs
|
||||
fn init_transaction_pool<B, V>(
|
||||
chain: Arc<B>,
|
||||
verifier_cache: Arc<RwLock<V>>,
|
||||
) -> TransactionPool<B, NoopPoolAdapter, V>
|
||||
where
|
||||
B: BlockChain,
|
||||
V: VerifierCache + 'static,
|
||||
{
|
||||
TransactionPool::new(
|
||||
PoolConfig {
|
||||
accept_fee_base: 0,
|
||||
max_pool_size: 50,
|
||||
max_stempool_size: 50,
|
||||
mineable_max_weight: 10_000,
|
||||
},
|
||||
chain.clone(),
|
||||
verifier_cache.clone(),
|
||||
Arc::new(NoopPoolAdapter {}),
|
||||
)
|
||||
}
|
||||
|
||||
// Same as from pool/tests/common.rs, with interface change
|
||||
pub fn add_some_blocks(&self, count: u64) {
|
||||
for _ in 0..count {
|
||||
self.add_block(vec![]);
|
||||
}
|
||||
}
|
||||
|
||||
// Same as from pool/tests/common.rs, with interface change
|
||||
pub fn add_block(&self, txs: Vec<Transaction>) {
|
||||
let chain = &self.chain;
|
||||
let keychain = &self.keychain;
|
||||
|
||||
let prev = chain.head_header().unwrap();
|
||||
let height = prev.height + 1;
|
||||
let next_header_info = consensus::next_difficulty(height, chain.difficulty_iter().unwrap());
|
||||
let fee = txs.iter().map(|x| x.fee()).sum();
|
||||
let key_id = ExtKeychainPath::new(1, height as u32, 0, 0, 0).to_identifier();
|
||||
let reward =
|
||||
reward::output(keychain, &ProofBuilder::new(keychain), &key_id, fee, false).unwrap();
|
||||
|
||||
let mut block =
|
||||
Block::new(&prev, txs, next_header_info.clone().difficulty, reward).unwrap();
|
||||
|
||||
block.header.timestamp = prev.timestamp + Duration::seconds(60);
|
||||
block.header.pow.secondary_scaling = next_header_info.secondary_scaling;
|
||||
|
||||
chain.set_txhashset_roots(&mut block).unwrap();
|
||||
|
||||
let edge_bits = global::min_edge_bits();
|
||||
block.header.pow.proof.edge_bits = edge_bits;
|
||||
pow::pow_size(
|
||||
&mut block.header,
|
||||
next_header_info.difficulty,
|
||||
global::proofsize(),
|
||||
edge_bits,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
chain.process_block(block, Options::NONE).unwrap();
|
||||
}
|
||||
}
|
126
pool/fuzz/fuzz_targets/transaction_pool.rs
Normal file
126
pool/fuzz/fuzz_targets/transaction_pool.rs
Normal file
|
@ -0,0 +1,126 @@
|
|||
#![no_main]
|
||||
use libfuzzer_sys::fuzz_target;
|
||||
|
||||
use std::fs::{self, File};
|
||||
use std::io::BufWriter;
|
||||
|
||||
use grin_core::{
|
||||
core::{KernelFeatures, NRDRelativeHeight, Transaction},
|
||||
global, ser,
|
||||
};
|
||||
|
||||
mod common;
|
||||
|
||||
use common::*;
|
||||
|
||||
#[derive(Debug)]
|
||||
enum Error {
|
||||
IoErr(std::io::Error),
|
||||
SerErr(ser::Error),
|
||||
}
|
||||
|
||||
struct FuzzTx {
|
||||
version: u32,
|
||||
name: String,
|
||||
tx: Transaction,
|
||||
}
|
||||
|
||||
fn gen_tx_corpus() -> Result<(), Error> {
|
||||
let fuzzer = PoolFuzzer::new("fuzz/target/.transaction_pool_corpus");
|
||||
|
||||
// create arbitrary inputs and outputs
|
||||
let inputs: Vec<u64> = vec![10, 100, 1000, 10000, 100000, 200000, 400000, 800000];
|
||||
let outputs: Vec<u64> = vec![5, 50, 500, 5000, 50000, 100000, 200000, 400000];
|
||||
let mut txes: Vec<FuzzTx> = vec![];
|
||||
|
||||
// create valid txes of all supported types
|
||||
txes.push(FuzzTx {
|
||||
version: 1u32,
|
||||
name: "coinbase".into(),
|
||||
tx: fuzzer.test_transaction_spending_coinbase(outputs.clone()),
|
||||
});
|
||||
txes.push(FuzzTx {
|
||||
version: 1u32,
|
||||
name: "plain".into(),
|
||||
tx: fuzzer.test_transaction(inputs.clone(), outputs.clone()),
|
||||
});
|
||||
txes.push(FuzzTx {
|
||||
version: 2u32,
|
||||
name: "height-locked".into(),
|
||||
tx: fuzzer.test_transaction_with_kernel_features(
|
||||
inputs.clone(),
|
||||
outputs.clone(),
|
||||
KernelFeatures::HeightLocked {
|
||||
fee: 100u64,
|
||||
lock_height: 42u64,
|
||||
},
|
||||
),
|
||||
});
|
||||
txes.push(FuzzTx {
|
||||
version: 2u32,
|
||||
name: "no-recent-duplicate".into(),
|
||||
tx: fuzzer.test_transaction_with_kernel_features(
|
||||
inputs.clone(),
|
||||
outputs.clone(),
|
||||
KernelFeatures::NoRecentDuplicate {
|
||||
fee: 100u64,
|
||||
relative_height: NRDRelativeHeight::new(42u64).unwrap(),
|
||||
},
|
||||
),
|
||||
});
|
||||
|
||||
fs::create_dir_all("fuzz/corpus/transaction_pool").map_err(|e| Error::IoErr(e))?;
|
||||
|
||||
// write txes to corpus files
|
||||
for tx in txes {
|
||||
let dict = File::create(format!("fuzz/corpus/transaction_pool/{}", tx.name))
|
||||
.map_err(|e| Error::IoErr(e))?;
|
||||
let mut writer = BufWriter::new(dict);
|
||||
ser::serialize(&mut writer, ser::ProtocolVersion(tx.version), &tx.tx)
|
||||
.map_err(|e| Error::SerErr(e))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fuzz_target!(|data: &[u8]| {
|
||||
// skip if input is too short
|
||||
if data.len() < 80 {
|
||||
return ();
|
||||
}
|
||||
|
||||
grin_util::init_test_logger();
|
||||
global::set_local_chain_type(global::ChainTypes::AutomatedTesting);
|
||||
|
||||
// check for corpus generation arguments
|
||||
// only generate corpus once, skipping on every other run
|
||||
if let Ok(gen_corpus) = std::env::var("GRIN_POOL_GEN_CORPUS") {
|
||||
if gen_corpus == "0" {
|
||||
gen_tx_corpus().unwrap();
|
||||
std::env::set_var("GRIN_POOL_GEN_CORPUS", "1");
|
||||
}
|
||||
}
|
||||
|
||||
let mut fuzzer = PoolFuzzer::new("fuzz/target/.transaction_pool");
|
||||
|
||||
let header = fuzzer.chain.head_header().unwrap();
|
||||
|
||||
for &i in [true, false].iter() {
|
||||
// deserialize tx from fuzzer data
|
||||
let tx: Result<Transaction, ser::Error> =
|
||||
ser::deserialize(&mut data.clone(), ser::ProtocolVersion(2));
|
||||
// we only care about inputs that pass
|
||||
if tx.is_ok() {
|
||||
// attempt to add fuzzed tx to the transaction pool
|
||||
// fuzz tx source on random first byte of fuzzer input
|
||||
// add to tx pool, then stem pool
|
||||
match fuzzer
|
||||
.pool
|
||||
.add_to_pool(fuzz_tx_source(data[0]), tx.unwrap(), i, &header)
|
||||
{
|
||||
Ok(_) => assert!(fuzzer.pool.total_size() >= 1),
|
||||
Err(_) => continue,
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
Loading…
Reference in a new issue