From ac60f8858e7850fc34e2cbd2691b2ddfaa9af482 Mon Sep 17 00:00:00 2001 From: Jude Nelson Date: Mon, 18 Feb 2019 22:00:01 -0500 Subject: [PATCH] the check() method for operations returns a CheckResult enum now to encode specific kinds of errors (instead of a catch-all bool). Add tests for each check() method and make sure they all fail in the right way. --- .../burn/operations/leader_block_commit.rs | 367 +++++++++++++++++- .../burn/operations/leader_key_register.rs | 152 +++++++- src/chainstate/burn/operations/mod.rs | 46 ++- .../burn/operations/user_burn_support.rs | 181 ++++++++- 4 files changed, 714 insertions(+), 32 deletions(-) diff --git a/src/chainstate/burn/operations/leader_block_commit.rs b/src/chainstate/burn/operations/leader_block_commit.rs index fbe9fa232..289cd880b 100644 --- a/src/chainstate/burn/operations/leader_block_commit.rs +++ b/src/chainstate/burn/operations/leader_block_commit.rs @@ -21,6 +21,7 @@ use std::marker::PhantomData; use chainstate::burn::operations::BlockstackOperation; use chainstate::burn::operations::Error as op_error; +use chainstate::burn::operations::CheckResult; use chainstate::burn::{BlockHeaderHash, VRFSeed}; use chainstate::burn::db::DBConn; @@ -30,6 +31,7 @@ use burnchains::{BurnchainTransaction, BurnchainTxInput, PublicKey}; use burnchains::Txid; use burnchains::Address; use burnchains::BurnchainHeaderHash; +use burnchains::Burnchain; use util::log; use util::hash::to_hex; @@ -179,11 +181,21 @@ where let data = parse_data_opt.unwrap(); // basic sanity checks + if data.parent_block_backptr == 0 { + warn!("Invalid tx: parent block back-pointer must be positive"); + return Err(op_error::ParseError); + } + if data.parent_block_backptr as u64 >= block_height { warn!("Invalid tx: parent block back-pointer {} exceeds block height {}", data.parent_block_backptr, block_height); return Err(op_error::ParseError); } + if data.key_block_backptr == 0 { + warn!("Invalid tx: key block back-pointer must be positive"); + return Err(op_error::ParseError); + } + if data.key_block_backptr as u64 >= block_height { warn!("Invalid tx: key block back-pointer {} exceeds block height {}", data.key_block_backptr, block_height); return Err(op_error::ParseError); @@ -227,7 +239,7 @@ where LeaderBlockCommitOp::::parse_from_tx(block_height, block_hash, tx) } - fn check(&self, conn: &DBConn) -> Result { + fn check(&self, burnchain: &Burnchain, conn: &DBConn) -> Result { let leader_key_block_height = self.block_number - (self.key_block_backptr as u64); let parent_block_height = self.block_number - (self.parent_block_backptr as u64); @@ -240,47 +252,47 @@ where if self.block_number < first_block_snapshot.block_height { warn!("Invalid block commit: predates genesis height {}", first_block_snapshot.block_height); - return Ok(false); + return Ok(CheckResult::BlockCommitPredatesGenesis); } let target_epoch = self.block_number - first_block_snapshot.block_height; if (self.epoch_num as u64) != target_epoch { warn!("Invalid block commit: current epoch is {}; got {}", target_epoch, self.epoch_num); - return Ok(false); + return Ok(CheckResult::BlockCommitBadEpoch); } ///////////////////////////////////////////////////////////////////////////////////// // There must exist a previously-accepted *unused* key from a LeaderKeyRegister ///////////////////////////////////////////////////////////////////////////////////// - let register_key_opt = BurnDB::::get_leader_key(conn, leader_key_block_height, self.key_vtxindex.into()) + let register_key_opt = BurnDB::::get_leader_key_at(conn, leader_key_block_height, self.key_vtxindex.into()) .map_err(op_error::DBError)?; if register_key_opt.is_none() { warn!("Invalid block commit: no corresponding leader key"); - return Ok(false); + return Ok(CheckResult::BlockCommitNoLeaderKey); } let register_key = register_key_opt.unwrap(); - let is_key_available = BurnDB::::is_leader_key_available(conn, ®ister_key) + let is_key_consumed = BurnDB::::is_leader_key_consumed(conn, ®ister_key) .map_err(op_error::DBError)?; - if !is_key_available { - warn!("Invalid block commit: leader key at {},{} is already used", register_key.block_number, register_key.vtxindex); - return Ok(false); + if is_key_consumed { + warn!("Invalid block commit: leader key at ({},{}) is already used", register_key.block_number, register_key.vtxindex); + return Ok(CheckResult::BlockCommitLeaderKeyAlreadyUsed); } ///////////////////////////////////////////////////////////////////////////////////// // There must exist a previously-accepted block from a LeaderBlockCommit ///////////////////////////////////////////////////////////////////////////////////// - let parent_block_opt = BurnDB::::get_block_commit(conn, parent_block_height, self.parent_vtxindex.into()) + let parent_block_opt = BurnDB::::get_block_commit_at(conn, parent_block_height, self.parent_vtxindex.into()) .map_err(op_error::DBError)?; if parent_block_opt.is_none() { warn!("Invalid block commit: no corresponding parent block"); - return Ok(false); + return Ok(CheckResult::BlockCommitNoParent); } ///////////////////////////////////////////////////////////////////////////////////// @@ -299,12 +311,12 @@ where let addr_bytes = register_key.address.to_bytes(); if input_address_bytes != addr_bytes { - warn!("Invalid block commit: leader key at {},{} has address bytes {}, but this tx input has address bytes {}", + warn!("Invalid block commit: leader key at ({},{}) has address bytes {}, but this tx input has address bytes {}", register_key.block_number, register_key.vtxindex, &to_hex(&input_address_bytes[..]), &to_hex(&addr_bytes[..])); - return Ok(false); + return Ok(CheckResult::BlockCommitBadInput); } - Ok(true) + Ok(CheckResult::BlockCommitOk) } } @@ -317,22 +329,34 @@ mod tests { use burnchains::bitcoin::blocks::BitcoinBlockParser; use burnchains::Txid; use burnchains::BLOCKSTACK_MAGIC_MAINNET; + use burnchains::burnchain::get_burn_quota_config; use burnchains::bitcoin::BitcoinNetworkType; use bitcoin::network::serialize::deserialize; use bitcoin::blockdata::transaction::Transaction; - use chainstate::burn::{BlockHeaderHash, VRFSeed}; + use chainstate::burn::{BlockHeaderHash, ConsensusHash, VRFSeed}; + use chainstate::burn::operations::leader_key_register::LeaderKeyRegisterOp; + use chainstate::burn::operations::leader_key_register::OPCODE as LeaderKeyRegisterOpcode; + + use ed25519_dalek::PublicKey as VRFPublicKey; use util::hash::hex_bytes; use util::log; + use super::OPCODE as LeaderBlockCommitOpcode; + struct OpFixture { txstr: String, result: Option> } + struct CheckFixture { + op: LeaderBlockCommitOp, + res: CheckResult + } + fn make_tx(hex_str: &str) -> Result { let tx_bin = hex_bytes(hex_str)?; let tx = deserialize(&tx_bin.to_vec()) @@ -418,5 +442,318 @@ mod tests { }; } } + + #[test] + fn test_check() { + let first_block_height = 120; + let first_burn_hash = BurnchainHeaderHash::from_hex("0000000000000000000000000000000000000000000000000000000000000123").unwrap(); + + let block_122_hash = BurnchainHeaderHash::from_hex("0000000000000000000000000000000000000000000000000000000000001220").unwrap(); + let block_123_hash = BurnchainHeaderHash::from_hex("0000000000000000000000000000000000000000000000000000000000001230").unwrap(); + let block_124_hash = BurnchainHeaderHash::from_hex("0000000000000000000000000000000000000000000000000000000000001240").unwrap(); + let block_125_hash = BurnchainHeaderHash::from_hex("0000000000000000000000000000000000000000000000000000000000001250").unwrap(); + + let burnchain = Burnchain { + chain_name: "bitcoin".to_string(), + network_name: "testnet".to_string(), + working_dir: "/nope".to_string(), + burn_quota: get_burn_quota_config(&"bitcoin".to_string()).unwrap(), + consensus_hash_lifetime: 24 + }; + + let mut db : BurnDB = BurnDB::connect_memory(first_block_height, &first_burn_hash).unwrap(); + + let leader_key_1 : LeaderKeyRegisterOp = LeaderKeyRegisterOp { + consensus_hash: ConsensusHash::from_bytes(&hex_bytes("2222222222222222222222222222222222222222").unwrap()).unwrap(), + public_key: VRFPublicKey::from_bytes(&hex_bytes("a366b51292bef4edd64063d9145c617fec373bceb0758e98cd72becd84d54c7a").unwrap()).unwrap(), + memo: vec![01, 02, 03, 04, 05], + address: BitcoinAddress::from_scriptpubkey(BitcoinNetworkType::Testnet, &hex_bytes("76a914306231b2782b5f80d944bf69f9d46a1453a0a0eb88ac").unwrap()).unwrap(), + + op: LeaderKeyRegisterOpcode, + txid: Txid::from_bytes_be(&hex_bytes("1bfa831b5fc56c858198acb8e77e5863c1e9d8ac26d49ddb914e24d8d4083562").unwrap()).unwrap(), + vtxindex: 456, + block_number: 123, + burn_header_hash: block_123_hash.clone(), + + _phantom: PhantomData + }; + + let leader_key_2 : LeaderKeyRegisterOp = LeaderKeyRegisterOp { + consensus_hash: ConsensusHash::from_bytes(&hex_bytes("3333333333333333333333333333333333333333").unwrap()).unwrap(), + public_key: VRFPublicKey::from_bytes(&hex_bytes("bb519494643f79f1dea0350e6fb9a1da88dfdb6137117fc2523824a8aa44fe1c").unwrap()).unwrap(), + memo: vec![01, 02, 03, 04, 05], + address: BitcoinAddress::from_scriptpubkey(BitcoinNetworkType::Testnet, &hex_bytes("76a914306231b2782b5f80d944bf69f9d46a1453a0a0eb88ac").unwrap()).unwrap(), + + op: LeaderKeyRegisterOpcode, + txid: Txid::from_bytes_be(&hex_bytes("9410df84e2b440055c33acb075a0687752df63fe8fe84aeec61abe469f0448c7").unwrap()).unwrap(), + vtxindex: 457, + block_number: 122, + burn_header_hash: block_122_hash.clone(), + + _phantom: PhantomData + }; + + let block_commit_1 : LeaderBlockCommitOp = LeaderBlockCommitOp { + block_header_hash: BlockHeaderHash::from_bytes(&hex_bytes("2222222222222222222222222222222222222222222222222222222222222222").unwrap()).unwrap(), + new_seed: VRFSeed::from_bytes(&hex_bytes("3333333333333333333333333333333333333333333333333333333333333333").unwrap()).unwrap(), + parent_block_backptr: 1, + parent_vtxindex: 1, + key_block_backptr: 1, + key_vtxindex: 456, + epoch_num: 50, + memo: vec![0x80], + + burn_fee: 12345, + input: BurnchainTxInput { + keys: vec![ + BitcoinPublicKey::from_hex("02d8015134d9db8178ac93acbc43170a2f20febba5087a5b0437058765ad5133d0").unwrap(), + ], + num_required: 1, + in_type: BurnchainInputType::BitcoinInput, + }, + + op: LeaderBlockCommitOpcode, + txid: Txid::from_bytes_be(&hex_bytes("3c07a0a93360bc85047bbaadd49e30c8af770f73a37e10fec400174d2e5f27cf").unwrap()).unwrap(), + vtxindex: 444, + block_number: 124, + burn_header_hash: block_124_hash.clone(), + + _phantom: PhantomData + }; + + { + let mut tx = db.tx_begin().unwrap(); + BurnDB::::insert_leader_key(&mut tx, &leader_key_1); + BurnDB::::insert_leader_key(&mut tx, &leader_key_2); + BurnDB::::insert_block_commit(&mut tx, &block_commit_1); + tx.commit().unwrap(); + } + + let block_height = 124; + + let fixtures = vec![ + CheckFixture { + // reject -- predates start block + op: LeaderBlockCommitOp { + block_header_hash: BlockHeaderHash::from_bytes(&hex_bytes("2222222222222222222222222222222222222222222222222222222222222222").unwrap()).unwrap(), + new_seed: VRFSeed::from_bytes(&hex_bytes("3333333333333333333333333333333333333333333333333333333333333333").unwrap()).unwrap(), + parent_block_backptr: 50, + parent_vtxindex: 456, + key_block_backptr: 1, + key_vtxindex: 456, + epoch_num: 50, + memo: vec![0x80], + + burn_fee: 12345, + input: BurnchainTxInput { + keys: vec![ + BitcoinPublicKey::from_hex("02d8015134d9db8178ac93acbc43170a2f20febba5087a5b0437058765ad5133d0").unwrap(), + ], + num_required: 1, + in_type: BurnchainInputType::BitcoinInput, + }, + + op: LeaderBlockCommitOpcode, + txid: Txid::from_bytes_be(&hex_bytes("3c07a0a93360bc85047bbaadd49e30c8af770f73a37e10fec400174d2e5f27cf").unwrap()).unwrap(), + vtxindex: 444, + block_number: 80, + burn_header_hash: block_124_hash.clone(), + + _phantom: PhantomData + }, + res: CheckResult::BlockCommitPredatesGenesis, + }, + CheckFixture { + // reject -- epoch does not match block height + op: LeaderBlockCommitOp { + block_header_hash: BlockHeaderHash::from_bytes(&hex_bytes("2222222222222222222222222222222222222222222222222222222222222222").unwrap()).unwrap(), + new_seed: VRFSeed::from_bytes(&hex_bytes("3333333333333333333333333333333333333333333333333333333333333333").unwrap()).unwrap(), + parent_block_backptr: 50, + parent_vtxindex: 456, + key_block_backptr: 1, + key_vtxindex: 456, + epoch_num: 50, + memo: vec![0x80], + + burn_fee: 12345, + input: BurnchainTxInput { + keys: vec![ + BitcoinPublicKey::from_hex("02d8015134d9db8178ac93acbc43170a2f20febba5087a5b0437058765ad5133d0").unwrap(), + ], + num_required: 1, + in_type: BurnchainInputType::BitcoinInput, + }, + + op: LeaderBlockCommitOpcode, + txid: Txid::from_bytes_be(&hex_bytes("3c07a0a93360bc85047bbaadd49e30c8af770f73a37e10fec400174d2e5f27cf").unwrap()).unwrap(), + vtxindex: 444, + block_number: 124, + burn_header_hash: block_124_hash.clone(), + + _phantom: PhantomData + }, + res: CheckResult::BlockCommitBadEpoch, + }, + CheckFixture { + // reject -- no such leader key + op: LeaderBlockCommitOp { + block_header_hash: BlockHeaderHash::from_bytes(&hex_bytes("2222222222222222222222222222222222222222222222222222222222222222").unwrap()).unwrap(), + new_seed: VRFSeed::from_bytes(&hex_bytes("3333333333333333333333333333333333333333333333333333333333333333").unwrap()).unwrap(), + parent_block_backptr: 50, + parent_vtxindex: 456, + key_block_backptr: 2, + key_vtxindex: 456, + epoch_num: (124 - first_block_height) as u32, + memo: vec![0x80], + + burn_fee: 12345, + input: BurnchainTxInput { + keys: vec![ + BitcoinPublicKey::from_hex("02d8015134d9db8178ac93acbc43170a2f20febba5087a5b0437058765ad5133d0").unwrap(), + ], + num_required: 1, + in_type: BurnchainInputType::BitcoinInput, + }, + + op: LeaderBlockCommitOpcode, + txid: Txid::from_bytes_be(&hex_bytes("3c07a0a93360bc85047bbaadd49e30c8af770f73a37e10fec400174d2e5f27cf").unwrap()).unwrap(), + vtxindex: 444, + block_number: 124, + burn_header_hash: block_124_hash.clone(), + + _phantom: PhantomData + }, + res: CheckResult::BlockCommitNoLeaderKey, + }, + CheckFixture { + // reject -- leader key consumed + op: LeaderBlockCommitOp { + block_header_hash: BlockHeaderHash::from_bytes(&hex_bytes("2222222222222222222222222222222222222222222222222222222222222222").unwrap()).unwrap(), + new_seed: VRFSeed::from_bytes(&hex_bytes("3333333333333333333333333333333333333333333333333333333333333333").unwrap()).unwrap(), + parent_block_backptr: 50, + parent_vtxindex: 456, + key_block_backptr: 1, + key_vtxindex: 456, + epoch_num: (124 - first_block_height) as u32, + memo: vec![0x80], + + burn_fee: 12345, + input: BurnchainTxInput { + keys: vec![ + BitcoinPublicKey::from_hex("02d8015134d9db8178ac93acbc43170a2f20febba5087a5b0437058765ad5133d0").unwrap(), + ], + num_required: 1, + in_type: BurnchainInputType::BitcoinInput, + }, + + op: LeaderBlockCommitOpcode, + txid: Txid::from_bytes_be(&hex_bytes("3c07a0a93360bc85047bbaadd49e30c8af770f73a37e10fec400174d2e5f27cf").unwrap()).unwrap(), + vtxindex: 445, + block_number: 124, + burn_header_hash: block_124_hash.clone(), + + _phantom: PhantomData + }, + res: CheckResult::BlockCommitLeaderKeyAlreadyUsed, + }, + CheckFixture { + // reject -- previous block must exist + op: LeaderBlockCommitOp { + block_header_hash: BlockHeaderHash::from_bytes(&hex_bytes("2222222222222222222222222222222222222222222222222222222222222222").unwrap()).unwrap(), + new_seed: VRFSeed::from_bytes(&hex_bytes("3333333333333333333333333333333333333333333333333333333333333333").unwrap()).unwrap(), + parent_block_backptr: 50, + parent_vtxindex: 456, + key_block_backptr: 2, + key_vtxindex: 457, + epoch_num: (124 - first_block_height) as u32, + memo: vec![0x80], + + burn_fee: 12345, + input: BurnchainTxInput { + keys: vec![ + BitcoinPublicKey::from_hex("02d8015134d9db8178ac93acbc43170a2f20febba5087a5b0437058765ad5133d0").unwrap(), + ], + num_required: 1, + in_type: BurnchainInputType::BitcoinInput, + }, + + op: LeaderBlockCommitOpcode, + txid: Txid::from_bytes_be(&hex_bytes("3c07a0a93360bc85047bbaadd49e30c8af770f73a37e10fec400174d2e5f27cf").unwrap()).unwrap(), + vtxindex: 445, + block_number: 124, + burn_header_hash: block_124_hash.clone(), + + _phantom: PhantomData + }, + res: CheckResult::BlockCommitNoParent, + }, + CheckFixture { + // reject -- bad tx input + op: LeaderBlockCommitOp { + block_header_hash: BlockHeaderHash::from_bytes(&hex_bytes("2222222222222222222222222222222222222222222222222222222222222222").unwrap()).unwrap(), + new_seed: VRFSeed::from_bytes(&hex_bytes("3333333333333333333333333333333333333333333333333333333333333333").unwrap()).unwrap(), + parent_block_backptr: 1, + parent_vtxindex: 444, + key_block_backptr: 3, + key_vtxindex: 457, + epoch_num: (125 - first_block_height) as u32, + memo: vec![0x80], + + burn_fee: 12345, + input: BurnchainTxInput { + keys: vec![ + BitcoinPublicKey::from_hex("02b3a05c80babc78e1566ebd41d43ff10030a3e19a9cc660e5c0e18b5b392a1d16").unwrap(), + ], + num_required: 1, + in_type: BurnchainInputType::BitcoinInput, + }, + + op: LeaderBlockCommitOpcode, + txid: Txid::from_bytes_be(&hex_bytes("3c07a0a93360bc85047bbaadd49e30c8af770f73a37e10fec400174d2e5f27cf").unwrap()).unwrap(), + vtxindex: 445, + block_number: 125, + burn_header_hash: block_125_hash.clone(), + + _phantom: PhantomData + }, + res: CheckResult::BlockCommitBadInput + }, + CheckFixture { + // accept + op: LeaderBlockCommitOp { + block_header_hash: BlockHeaderHash::from_bytes(&hex_bytes("2222222222222222222222222222222222222222222222222222222222222222").unwrap()).unwrap(), + new_seed: VRFSeed::from_bytes(&hex_bytes("3333333333333333333333333333333333333333333333333333333333333333").unwrap()).unwrap(), + parent_block_backptr: 1, + parent_vtxindex: 444, + key_block_backptr: 3, + key_vtxindex: 457, + epoch_num: (125 - first_block_height) as u32, + memo: vec![0x80], + + burn_fee: 12345, + input: BurnchainTxInput { + keys: vec![ + BitcoinPublicKey::from_hex("02d8015134d9db8178ac93acbc43170a2f20febba5087a5b0437058765ad5133d0").unwrap(), + ], + num_required: 1, + in_type: BurnchainInputType::BitcoinInput, + }, + + op: LeaderBlockCommitOpcode, + txid: Txid::from_bytes_be(&hex_bytes("3c07a0a93360bc85047bbaadd49e30c8af770f73a37e10fec400174d2e5f27cf").unwrap()).unwrap(), + vtxindex: 445, + block_number: 125, + burn_header_hash: block_125_hash.clone(), + + _phantom: PhantomData + }, + res: CheckResult::BlockCommitOk + } + ]; + + for fixture in fixtures { + assert_eq!(fixture.res, fixture.op.check(&burnchain, &db.conn()).unwrap()); + } + } } diff --git a/src/chainstate/burn/operations/leader_key_register.rs b/src/chainstate/burn/operations/leader_key_register.rs index 0720d74a5..d41bed32e 100644 --- a/src/chainstate/burn/operations/leader_key_register.rs +++ b/src/chainstate/burn/operations/leader_key_register.rs @@ -21,6 +21,7 @@ use std::marker::PhantomData; use chainstate::burn::operations::BlockstackOperation; use chainstate::burn::operations::Error as op_error; +use chainstate::burn::operations::CheckResult; use chainstate::burn::ConsensusHash; use chainstate::burn::db::burndb::BurnDB; @@ -31,6 +32,7 @@ use burnchains::Txid; use burnchains::Address; use burnchains::PublicKey; use burnchains::BurnchainHeaderHash; +use burnchains::Burnchain; use util::vrf::{ECVRF_check_public_key, ECVRF_public_key_to_hex}; @@ -161,7 +163,7 @@ where LeaderKeyRegisterOp::::parse_from_tx(block_height, block_hash, tx) } - fn check(&self, conn: &DBConn) -> Result { + fn check(&self, burnchain: &Burnchain, conn: &DBConn) -> Result { ///////////////////////////////////////////////////////////////// // Keys must be unique -- no one can register the same key twice ///////////////////////////////////////////////////////////////// @@ -172,22 +174,22 @@ where if has_key_already { warn!("Invalid leader key registration: public key {} previously used", ECVRF_public_key_to_hex(&self.public_key)); - return Ok(false); + return Ok(CheckResult::LeaderKeyAlreadyRegistered); } ///////////////////////////////////////////////////////////////// // Consensus hash must be recent and valid ///////////////////////////////////////////////////////////////// - let consensus_hash_recent = BurnDB::::is_fresh_consensus_hash(conn, self.block_number, &self.consensus_hash) + let consensus_hash_recent = BurnDB::::is_fresh_consensus_hash(conn, self.block_number, burnchain.consensus_hash_lifetime, &self.consensus_hash) .map_err(op_error::DBError)?; if !consensus_hash_recent { - warn!("Invalid consensus hash {}", &self.consensus_hash.to_hex()); - return Ok(false); + warn!("Invalid leader key registration: invalid consensus hash {}", &self.consensus_hash.to_hex()); + return Ok(CheckResult::LeaderKeyBadConsensusHash); } - return Ok(true); + return Ok(CheckResult::LeaderKeyOk); } } @@ -200,20 +202,29 @@ mod tests { use burnchains::bitcoin::BitcoinNetworkType; use burnchains::Txid; use burnchains::BLOCKSTACK_MAGIC_MAINNET; + use burnchains::burnchain::get_burn_quota_config; use bitcoin::network::serialize::deserialize; use bitcoin::blockdata::transaction::Transaction; - use chainstate::burn::ConsensusHash; + use chainstate::burn::{ConsensusHash, OpsHash, SortitionHash, BlockSnapshot}; + use chainstate::burn::operations::CheckResult; use util::hash::hex_bytes; use util::log; + use super::OPCODE as LeaderKeyRegisterOpcode; + struct OpFixture { txstr: String, result: Option> } + struct CheckFixture { + op: LeaderKeyRegisterOp, + res: CheckResult + } + fn make_tx(hex_str: &str) -> Result { let tx_bin = hex_bytes(hex_str)?; let tx = deserialize(&tx_bin.to_vec()) @@ -307,5 +318,132 @@ mod tests { }; } } + + #[test] + fn test_check() { + + let first_block_height = 120; + let first_burn_hash = BurnchainHeaderHash::from_hex("0000000000000000000000000000000000000000000000000000000000000123").unwrap(); + + let block_122_hash = BurnchainHeaderHash::from_hex("0000000000000000000000000000000000000000000000000000000000000002").unwrap(); + let block_123_hash = BurnchainHeaderHash::from_hex("0000000000000000000000000000000000000000000000000000000000000003").unwrap(); + let block_124_hash = BurnchainHeaderHash::from_hex("0000000000000000000000000000000000000000000000000000000000000004").unwrap(); + let block_125_hash = BurnchainHeaderHash::from_hex("0000000000000000000000000000000000000000000000000000000000000005").unwrap(); + + let burnchain = Burnchain { + chain_name: "bitcoin".to_string(), + network_name: "testnet".to_string(), + working_dir: "/nope".to_string(), + burn_quota: get_burn_quota_config(&"bitcoin".to_string()).unwrap(), + consensus_hash_lifetime: 24 + }; + + let mut db : BurnDB = BurnDB::connect_memory(first_block_height, &first_burn_hash).unwrap(); + + let leader_key_1 : LeaderKeyRegisterOp = LeaderKeyRegisterOp { + consensus_hash: ConsensusHash::from_bytes(&hex_bytes("0000000000000000000000000000000000000000").unwrap()).unwrap(), + public_key: VRFPublicKey::from_bytes(&hex_bytes("a366b51292bef4edd64063d9145c617fec373bceb0758e98cd72becd84d54c7a").unwrap()).unwrap(), + memo: vec![01, 02, 03, 04, 05], + address: BitcoinAddress::from_scriptpubkey(BitcoinNetworkType::Testnet, &hex_bytes("76a9140be3e286a15ea85882761618e366586b5574100d88ac").unwrap()).unwrap(), + + op: LeaderKeyRegisterOpcode, + txid: Txid::from_bytes_be(&hex_bytes("1bfa831b5fc56c858198acb8e77e5863c1e9d8ac26d49ddb914e24d8d4083562").unwrap()).unwrap(), + vtxindex: 456, + block_number: 123, + burn_header_hash: block_123_hash.clone(), + + _phantom: PhantomData + }; + + // populate consensus hashes + { + let mut tx = db.tx_begin().unwrap(); + for i in 0..10 { + let snapshot_row = BlockSnapshot { + block_height: i + first_block_height, + burn_header_hash: BurnchainHeaderHash::from_bytes(&[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,i as u8]).unwrap(), + consensus_hash: ConsensusHash::from_bytes(&[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,i as u8]).unwrap(), + ops_hash: OpsHash::from_bytes(&[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,i as u8]).unwrap(), + total_burn: i, + burn_quota: 0, + sortition: true, + sortition_hash: SortitionHash::initial(), + winning_block_txid: Txid::from_hex("0000000000000000000000000000000000000000000000000000000000000000").unwrap(), + winning_block_burn_hash: BurnchainHeaderHash::from_hex("0000000000000000000000000000000000000000000000000000000000000000").unwrap(), + canonical: true + }; + BurnDB::::insert_block_snapshot(&mut tx, &snapshot_row).unwrap(); + } + + tx.commit(); + } + + { + let mut tx = db.tx_begin().unwrap(); + BurnDB::::insert_leader_key(&mut tx, &leader_key_1).unwrap(); + tx.commit().unwrap(); + } + + let check_fixtures = vec![ + CheckFixture { + // reject -- key already registered + op: LeaderKeyRegisterOp { + consensus_hash: ConsensusHash::from_bytes(&hex_bytes("0000000000000000000000000000000000000000").unwrap()).unwrap(), + public_key: VRFPublicKey::from_bytes(&hex_bytes("a366b51292bef4edd64063d9145c617fec373bceb0758e98cd72becd84d54c7a").unwrap()).unwrap(), + memo: vec![01, 02, 03, 04, 05], + address: BitcoinAddress::from_scriptpubkey(BitcoinNetworkType::Testnet, &hex_bytes("76a9140be3e286a15ea85882761618e366586b5574100d88ac").unwrap()).unwrap(), + + op: LeaderKeyRegisterOpcode, + txid: Txid::from_bytes_be(&hex_bytes("1bfa831b5fc56c858198acb8e77e5863c1e9d8ac26d49ddb914e24d8d4083562").unwrap()).unwrap(), + vtxindex: 455, + block_number: 122, + burn_header_hash: block_123_hash.clone(), + + _phantom: PhantomData + }, + res: CheckResult::LeaderKeyAlreadyRegistered + }, + CheckFixture { + // reject -- invalid consensus hash + op: LeaderKeyRegisterOp { + consensus_hash: ConsensusHash::from_bytes(&hex_bytes("1000000000000000000000000000000000000000").unwrap()).unwrap(), + public_key: VRFPublicKey::from_bytes(&hex_bytes("bb519494643f79f1dea0350e6fb9a1da88dfdb6137117fc2523824a8aa44fe1c").unwrap()).unwrap(), + memo: vec![01, 02, 03, 04, 05], + address: BitcoinAddress::from_scriptpubkey(BitcoinNetworkType::Testnet, &hex_bytes("76a9140be3e286a15ea85882761618e366586b5574100d88ac").unwrap()).unwrap(), + + op: LeaderKeyRegisterOpcode, + txid: Txid::from_bytes_be(&hex_bytes("1bfa831b5fc56c858198acb8e77e5863c1e9d8ac26d49ddb914e24d8d4083562").unwrap()).unwrap(), + vtxindex: 456, + block_number: 123, + burn_header_hash: block_123_hash.clone(), + + _phantom: PhantomData + }, + res: CheckResult::LeaderKeyBadConsensusHash, + }, + CheckFixture { + // accept + op: LeaderKeyRegisterOp { + consensus_hash: ConsensusHash::from_bytes(&hex_bytes("0000000000000000000000000000000000000000").unwrap()).unwrap(), + public_key: VRFPublicKey::from_bytes(&hex_bytes("bb519494643f79f1dea0350e6fb9a1da88dfdb6137117fc2523824a8aa44fe1c").unwrap()).unwrap(), + memo: vec![01, 02, 03, 04, 05], + address: BitcoinAddress::from_scriptpubkey(BitcoinNetworkType::Testnet, &hex_bytes("76a9140be3e286a15ea85882761618e366586b5574100d88ac").unwrap()).unwrap(), + + op: LeaderKeyRegisterOpcode, + txid: Txid::from_bytes_be(&hex_bytes("1bfa831b5fc56c858198acb8e77e5863c1e9d8ac26d49ddb914e24d8d4083562").unwrap()).unwrap(), + vtxindex: 456, + block_number: 123, + burn_header_hash: block_123_hash.clone(), + + _phantom: PhantomData + }, + res: CheckResult::LeaderKeyOk + } + ]; + + for fixture in check_fixtures { + assert_eq!(fixture.res, fixture.op.check(&burnchain, db.conn()).unwrap()); + } + } } diff --git a/src/chainstate/burn/operations/mod.rs b/src/chainstate/burn/operations/mod.rs index c97501bd6..f70cc0a00 100644 --- a/src/chainstate/burn/operations/mod.rs +++ b/src/chainstate/burn/operations/mod.rs @@ -34,6 +34,7 @@ use chainstate::burn::db::Error as db_error; use chainstate::burn::db::DBConn; use burnchains::{Address, PublicKey, BurnchainHeaderHash, BurnchainTransaction}; +use burnchains::Burnchain; #[derive(Debug)] pub enum Error { @@ -73,17 +74,58 @@ impl error::Error for Error { } } +#[derive(Debug, Clone, PartialEq)] +pub enum CheckResult { + BlockCommitOk, + BlockCommitPredatesGenesis, + BlockCommitBadEpoch, + BlockCommitNoLeaderKey, + BlockCommitLeaderKeyAlreadyUsed, + BlockCommitNoParent, + BlockCommitBadInput, + + LeaderKeyOk, + LeaderKeyAlreadyRegistered, + LeaderKeyBadConsensusHash, + + UserBurnSupportOk, + UserBurnSupportBadConsensusHash, + UserBurnSupportNoLeaderKey, +} + +impl fmt::Display for CheckResult { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + CheckResult::BlockCommitOk => f.write_str("Block commit OK"), + CheckResult::BlockCommitPredatesGenesis => f.write_str("Block commit predates genesis block"), + CheckResult::BlockCommitBadEpoch => f.write_str("Block commit has a bad epoch value"), + CheckResult::BlockCommitNoLeaderKey => f.write_str("Block commit has no matching register key"), + CheckResult::BlockCommitLeaderKeyAlreadyUsed => f.write_str("Block commit register key already used"), + CheckResult::BlockCommitNoParent => f.write_str("Block commit parent does not exist"), + CheckResult::BlockCommitBadInput => f.write_str("Block commit tx input does not match register key tx output"), + + CheckResult::LeaderKeyOk => f.write_str("Leader key OK"), + CheckResult::LeaderKeyAlreadyRegistered => f.write_str("Leader key has already been registered"), + CheckResult::LeaderKeyBadConsensusHash => f.write_str("Leader key has an invalid consensus hash"), + + CheckResult::UserBurnSupportOk => f.write_str("User burn support OK"), + CheckResult::UserBurnSupportBadConsensusHash => f.write_str("User burn support has an invalid consensus hash"), + CheckResult::UserBurnSupportNoLeaderKey => f.write_str("User burn support does not match a registered leader key"), + } + } +} + pub trait BlockstackOperation where A: Address, K: PublicKey { - fn check(&self, conn: &DBConn) -> Result; + fn check(&self, burnchain: &Burnchain, conn: &DBConn) -> Result; fn from_tx(block_height: u64, block_hash: &BurnchainHeaderHash, tx: &BurnchainTransaction) -> Result where Self: Sized; } -#[derive(Debug)] +#[derive(Debug, Clone)] pub enum BlockstackOperationType where A: Address, diff --git a/src/chainstate/burn/operations/user_burn_support.rs b/src/chainstate/burn/operations/user_burn_support.rs index c8aa9015c..27c5d65c8 100644 --- a/src/chainstate/burn/operations/user_burn_support.rs +++ b/src/chainstate/burn/operations/user_burn_support.rs @@ -20,6 +20,7 @@ use std::marker::PhantomData; use chainstate::burn::operations::BlockstackOperation; use chainstate::burn::operations::Error as op_error; +use chainstate::burn::operations::CheckResult; use chainstate::burn::ConsensusHash; use chainstate::burn::db::DBConn; @@ -30,6 +31,7 @@ use burnchains::Txid; use burnchains::Address; use burnchains::PublicKey; use burnchains::BurnchainHeaderHash; +use burnchains::Burnchain; use util::hash::Hash160; use util::vrf::{ECVRF_check_public_key, ECVRF_public_key_to_hex}; @@ -168,11 +170,23 @@ where UserBurnSupportOp::::parse_from_tx(block_height, block_hash, tx) } - fn check(&self, conn: &DBConn) -> Result { + fn check(&self, burnchain: &Burnchain, conn: &DBConn) -> Result { + + ///////////////////////////////////////////////////////////////// + // Consensus hash must be recent and valid + ///////////////////////////////////////////////////////////////// + + let consensus_hash_recent = BurnDB::::is_fresh_consensus_hash(conn, self.block_number, burnchain.consensus_hash_lifetime, &self.consensus_hash) + .map_err(op_error::DBError)?; + + if !consensus_hash_recent { + warn!("Invalid user burn: invalid consensus hash {}", &self.consensus_hash.to_hex()); + return Ok(CheckResult::UserBurnSupportBadConsensusHash); + } + ///////////////////////////////////////////////////////////////////////////////////// - // There must exist a previously-accepted LeaderKeyRegisterOp. It may already be - // consumed by a LeaderBlockCommitOp by the time this transaction is processed, - // so we only check for the key's existence. + // There must exist a previously-accepted LeaderKeyRegisterOp that matches this + // user support burn's VRF public key. ///////////////////////////////////////////////////////////////////////////////////// let register_key_opt = BurnDB::::get_leader_key_by_VRF_key(conn, &self.public_key) @@ -180,10 +194,17 @@ where if register_key_opt.is_none() { warn!("Invalid user burn: no such leader VRF key {}", &ECVRF_public_key_to_hex(&self.public_key)); - return Ok(false); + return Ok(CheckResult::UserBurnSupportNoLeaderKey); } + + ///////////////////////////////////////////////////////////////////////////////////// + // The block hash can't be checked here -- the corresponding LeaderBlockCommitOp may + // not have been checked yet, so we don't know yet if it exists. The sortition + // algorithm will carry out this check, and only consider user burns if they match + // a block commit and the commit's corresponding leader key. + ///////////////////////////////////////////////////////////////////////////////////// - Ok(true) + Ok(CheckResult::UserBurnSupportOk) } } @@ -197,20 +218,32 @@ mod tests { use burnchains::bitcoin::keys::BitcoinPublicKey; use burnchains::bitcoin::address::BitcoinAddress; + use burnchains::burnchain::get_burn_quota_config; + use chainstate::burn::operations::leader_key_register::LeaderKeyRegisterOp; + use chainstate::burn::operations::leader_key_register::OPCODE as LeaderKeyRegisterOpcode; + + use chainstate::burn::{ConsensusHash, OpsHash, SortitionHash, BlockSnapshot}; + use chainstate::burn::operations::CheckResult; + use bitcoin::network::serialize::deserialize; use bitcoin::blockdata::transaction::Transaction; - use chainstate::burn::ConsensusHash; - use util::hash::{hex_bytes, Hash160}; use util::log; + use super::OPCODE as UserBurnSupportOpcode; + struct OpFixture { txstr: String, result: Option> } + struct CheckFixture { + op: UserBurnSupportOp, + res: CheckResult + } + fn make_tx(hex_str: &str) -> Result { let tx_bin = hex_bytes(hex_str)?; let tx = deserialize(&tx_bin.to_vec()) @@ -289,5 +322,137 @@ mod tests { }; } } + + #[test] + fn test_check() { + let first_block_height = 120; + let first_burn_hash = BurnchainHeaderHash::from_hex("0000000000000000000000000000000000000000000000000000000000000123").unwrap(); + + let block_122_hash = BurnchainHeaderHash::from_hex("0000000000000000000000000000000000000000000000000000000000000002").unwrap(); + let block_123_hash = BurnchainHeaderHash::from_hex("0000000000000000000000000000000000000000000000000000000000000003").unwrap(); + let block_124_hash = BurnchainHeaderHash::from_hex("0000000000000000000000000000000000000000000000000000000000000004").unwrap(); + let block_125_hash = BurnchainHeaderHash::from_hex("0000000000000000000000000000000000000000000000000000000000000005").unwrap(); + + let burnchain = Burnchain { + chain_name: "bitcoin".to_string(), + network_name: "testnet".to_string(), + working_dir: "/nope".to_string(), + burn_quota: get_burn_quota_config(&"bitcoin".to_string()).unwrap(), + consensus_hash_lifetime: 24 + }; + + let mut db : BurnDB = BurnDB::connect_memory(first_block_height, &first_burn_hash).unwrap(); + + let leader_key_1 : LeaderKeyRegisterOp = LeaderKeyRegisterOp { + consensus_hash: ConsensusHash::from_bytes(&hex_bytes("0000000000000000000000000000000000000000").unwrap()).unwrap(), + public_key: VRFPublicKey::from_bytes(&hex_bytes("a366b51292bef4edd64063d9145c617fec373bceb0758e98cd72becd84d54c7a").unwrap()).unwrap(), + memo: vec![01, 02, 03, 04, 05], + address: BitcoinAddress::from_scriptpubkey(BitcoinNetworkType::Testnet, &hex_bytes("76a9140be3e286a15ea85882761618e366586b5574100d88ac").unwrap()).unwrap(), + + op: LeaderKeyRegisterOpcode, + txid: Txid::from_bytes_be(&hex_bytes("1bfa831b5fc56c858198acb8e77e5863c1e9d8ac26d49ddb914e24d8d4083562").unwrap()).unwrap(), + vtxindex: 456, + block_number: 123, + burn_header_hash: block_123_hash.clone(), + + _phantom: PhantomData + }; + + // populate consensus hashes + { + let mut tx = db.tx_begin().unwrap(); + for i in 0..10 { + let snapshot_row = BlockSnapshot { + block_height: i + first_block_height, + burn_header_hash: BurnchainHeaderHash::from_bytes(&[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,i as u8]).unwrap(), + consensus_hash: ConsensusHash::from_bytes(&[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,i as u8]).unwrap(), + ops_hash: OpsHash::from_bytes(&[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,i as u8]).unwrap(), + total_burn: i, + burn_quota: 0, + sortition: true, + sortition_hash: SortitionHash::initial(), + winning_block_txid: Txid::from_hex("0000000000000000000000000000000000000000000000000000000000000000").unwrap(), + winning_block_burn_hash: BurnchainHeaderHash::from_hex("0000000000000000000000000000000000000000000000000000000000000000").unwrap(), + canonical: true + }; + BurnDB::::insert_block_snapshot(&mut tx, &snapshot_row).unwrap(); + } + + tx.commit(); + } + + { + let mut tx = db.tx_begin().unwrap(); + BurnDB::::insert_leader_key(&mut tx, &leader_key_1).unwrap(); + tx.commit().unwrap(); + } + + let check_fixtures = vec![ + CheckFixture { + // reject -- bad consensus hash + op: UserBurnSupportOp { + consensus_hash: ConsensusHash::from_bytes(&hex_bytes("1000000000000000000000000000000000000000").unwrap()).unwrap(), + public_key: VRFPublicKey::from_bytes(&hex_bytes("a366b51292bef4edd64063d9145c617fec373bceb0758e98cd72becd84d54c7a").unwrap()).unwrap(), + block_header_hash_160: Hash160::from_bytes(&hex_bytes("7150f635054b87df566a970b21e07030d6444bf2").unwrap()).unwrap(), // 22222....2222 + memo: vec![0x01, 0x02, 0x03, 0x04, 0x05], + burn_fee: 10000, + + op: UserBurnSupportOpcode, + txid: Txid::from_bytes_be(&hex_bytes("1d5cbdd276495b07f0e0bf0181fa57c175b217bc35531b078d62fc20986c716b").unwrap()).unwrap(), + vtxindex: 13, + block_number: 124, + burn_header_hash: block_124_hash.clone(), + + _phantom_a: PhantomData, + _phantom_k: PhantomData + }, + res: CheckResult::UserBurnSupportBadConsensusHash, + }, + CheckFixture { + // reject -- no leader key + op: UserBurnSupportOp { + consensus_hash: ConsensusHash::from_bytes(&hex_bytes("0000000000000000000000000000000000000000").unwrap()).unwrap(), + public_key: VRFPublicKey::from_bytes(&hex_bytes("bb519494643f79f1dea0350e6fb9a1da88dfdb6137117fc2523824a8aa44fe1c").unwrap()).unwrap(), + block_header_hash_160: Hash160::from_bytes(&hex_bytes("7150f635054b87df566a970b21e07030d6444bf2").unwrap()).unwrap(), // 22222....2222 + memo: vec![0x01, 0x02, 0x03, 0x04, 0x05], + burn_fee: 10000, + + op: UserBurnSupportOpcode, + txid: Txid::from_bytes_be(&hex_bytes("1d5cbdd276495b07f0e0bf0181fa57c175b217bc35531b078d62fc20986c716b").unwrap()).unwrap(), + vtxindex: 13, + block_number: 124, + burn_header_hash: block_124_hash.clone(), + + _phantom_a: PhantomData, + _phantom_k: PhantomData + }, + res: CheckResult::UserBurnSupportNoLeaderKey, + }, + CheckFixture { + // accept + op: UserBurnSupportOp { + consensus_hash: ConsensusHash::from_bytes(&hex_bytes("0000000000000000000000000000000000000000").unwrap()).unwrap(), + public_key: VRFPublicKey::from_bytes(&hex_bytes("a366b51292bef4edd64063d9145c617fec373bceb0758e98cd72becd84d54c7a").unwrap()).unwrap(), + block_header_hash_160: Hash160::from_bytes(&hex_bytes("7150f635054b87df566a970b21e07030d6444bf2").unwrap()).unwrap(), // 22222....2222 + memo: vec![0x01, 0x02, 0x03, 0x04, 0x05], + burn_fee: 10000, + + op: UserBurnSupportOpcode, + txid: Txid::from_bytes_be(&hex_bytes("1d5cbdd276495b07f0e0bf0181fa57c175b217bc35531b078d62fc20986c716b").unwrap()).unwrap(), + vtxindex: 13, + block_number: 124, + burn_header_hash: block_124_hash.clone(), + + _phantom_a: PhantomData, + _phantom_k: PhantomData + }, + res: CheckResult::UserBurnSupportOk, + } + ]; + + for fixture in check_fixtures { + assert_eq!(fixture.res, fixture.op.check(&burnchain, db.conn()).unwrap()); + } + } }