mirror of
https://github.com/mimblewimble/grin.git
synced 2025-02-01 17:01:09 +03:00
client invalidateheader and resetchainhead (#3618)
* wip - "reset_head" via owner api functionality * jsonrpc pass hash in as a string * sort of works * not a reorg if we simply accept several blocks at once * remember to reset header MMR separately as it is readonly when interacting with txhashset extension * basic client integration needs error handling etc. * reset sync status when reset chain head * track "denylist" (todo) and validate headers against this via the ctx * track denylist (header hashes) in chain itself * header denylist in play * expose invalidateheader as client cmd * rework reset_chain_head - rewind txhashset then header MMR
This commit is contained in:
parent
9e27e6f9d3
commit
89c06ddab7
10 changed files with 288 additions and 46 deletions
|
@ -14,7 +14,7 @@
|
||||||
|
|
||||||
use super::utils::{get_output, get_output_v2, w};
|
use super::utils::{get_output, get_output_v2, w};
|
||||||
use crate::chain;
|
use crate::chain;
|
||||||
use crate::core::core::hash::Hashed;
|
use crate::core::core::hash::{Hash, Hashed};
|
||||||
use crate::rest::*;
|
use crate::rest::*;
|
||||||
use crate::router::{Handler, ResponseFuture};
|
use crate::router::{Handler, ResponseFuture};
|
||||||
use crate::types::*;
|
use crate::types::*;
|
||||||
|
@ -72,6 +72,29 @@ impl Handler for ChainValidationHandler {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct ChainResetHandler {
|
||||||
|
pub chain: Weak<chain::Chain>,
|
||||||
|
pub sync_state: Weak<chain::SyncState>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ChainResetHandler {
|
||||||
|
pub fn reset_chain_head(&self, hash: Hash) -> Result<(), Error> {
|
||||||
|
let chain = w(&self.chain)?;
|
||||||
|
let header = chain.get_block_header(&hash)?;
|
||||||
|
chain.reset_chain_head(&header)?;
|
||||||
|
|
||||||
|
// Reset the sync status and clear out any sync error.
|
||||||
|
w(&self.sync_state)?.reset();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn invalidate_header(&self, hash: Hash) -> Result<(), Error> {
|
||||||
|
let chain = w(&self.chain)?;
|
||||||
|
chain.invalidate_header(hash)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Chain compaction handler. Trigger a compaction of the chain state to regain
|
/// Chain compaction handler. Trigger a compaction of the chain state to regain
|
||||||
/// storage space.
|
/// storage space.
|
||||||
/// POST /v1/chain/compact
|
/// POST /v1/chain/compact
|
||||||
|
@ -81,9 +104,9 @@ pub struct ChainCompactHandler {
|
||||||
|
|
||||||
impl ChainCompactHandler {
|
impl ChainCompactHandler {
|
||||||
pub fn compact_chain(&self) -> Result<(), Error> {
|
pub fn compact_chain(&self) -> Result<(), Error> {
|
||||||
w(&self.chain)?
|
let chain = w(&self.chain)?;
|
||||||
.compact()
|
chain.compact()?;
|
||||||
.map_err(|_| ErrorKind::Internal("chain error".to_owned()).into())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -15,7 +15,8 @@
|
||||||
//! Owner API External Definition
|
//! Owner API External Definition
|
||||||
|
|
||||||
use crate::chain::{Chain, SyncState};
|
use crate::chain::{Chain, SyncState};
|
||||||
use crate::handlers::chain_api::{ChainCompactHandler, ChainValidationHandler};
|
use crate::core::core::hash::Hash;
|
||||||
|
use crate::handlers::chain_api::{ChainCompactHandler, ChainResetHandler, ChainValidationHandler};
|
||||||
use crate::handlers::peers_api::{PeerHandler, PeersConnectedHandler};
|
use crate::handlers::peers_api::{PeerHandler, PeersConnectedHandler};
|
||||||
use crate::handlers::server_api::StatusHandler;
|
use crate::handlers::server_api::StatusHandler;
|
||||||
use crate::p2p::types::PeerInfoDisplay;
|
use crate::p2p::types::PeerInfoDisplay;
|
||||||
|
@ -107,6 +108,26 @@ impl Owner {
|
||||||
chain_compact_handler.compact_chain()
|
chain_compact_handler.compact_chain()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn reset_chain_head(&self, hash: String) -> Result<(), Error> {
|
||||||
|
let hash = Hash::from_hex(&hash)
|
||||||
|
.map_err(|_| ErrorKind::RequestError("invalid header hash".into()))?;
|
||||||
|
let handler = ChainResetHandler {
|
||||||
|
chain: self.chain.clone(),
|
||||||
|
sync_state: self.sync_state.clone(),
|
||||||
|
};
|
||||||
|
handler.reset_chain_head(hash)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn invalidate_header(&self, hash: String) -> Result<(), Error> {
|
||||||
|
let hash = Hash::from_hex(&hash)
|
||||||
|
.map_err(|_| ErrorKind::RequestError("invalid header hash".into()))?;
|
||||||
|
let handler = ChainResetHandler {
|
||||||
|
chain: self.chain.clone(),
|
||||||
|
sync_state: self.sync_state.clone(),
|
||||||
|
};
|
||||||
|
handler.invalidate_header(hash)
|
||||||
|
}
|
||||||
|
|
||||||
/// Retrieves information about stored peers.
|
/// Retrieves information about stored peers.
|
||||||
/// If `None` is provided, will list all stored peers.
|
/// If `None` is provided, will list all stored peers.
|
||||||
///
|
///
|
||||||
|
|
|
@ -132,6 +132,10 @@ pub trait OwnerRpc: Sync + Send {
|
||||||
*/
|
*/
|
||||||
fn compact_chain(&self) -> Result<(), ErrorKind>;
|
fn compact_chain(&self) -> Result<(), ErrorKind>;
|
||||||
|
|
||||||
|
fn reset_chain_head(&self, hash: String) -> Result<(), ErrorKind>;
|
||||||
|
|
||||||
|
fn invalidate_header(&self, hash: String) -> Result<(), ErrorKind>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Networked version of [Owner::get_peers](struct.Owner.html#method.get_peers).
|
Networked version of [Owner::get_peers](struct.Owner.html#method.get_peers).
|
||||||
|
|
||||||
|
@ -363,6 +367,14 @@ impl OwnerRpc for Owner {
|
||||||
Owner::validate_chain(self).map_err(|e| e.kind().clone())
|
Owner::validate_chain(self).map_err(|e| e.kind().clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn reset_chain_head(&self, hash: String) -> Result<(), ErrorKind> {
|
||||||
|
Owner::reset_chain_head(self, hash).map_err(|e| e.kind().clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn invalidate_header(&self, hash: String) -> Result<(), ErrorKind> {
|
||||||
|
Owner::invalidate_header(self, hash).map_err(|e| e.kind().clone())
|
||||||
|
}
|
||||||
|
|
||||||
fn compact_chain(&self) -> Result<(), ErrorKind> {
|
fn compact_chain(&self) -> Result<(), ErrorKind> {
|
||||||
Owner::compact_chain(self).map_err(|e| e.kind().clone())
|
Owner::compact_chain(self).map_err(|e| e.kind().clone())
|
||||||
}
|
}
|
||||||
|
@ -391,7 +403,7 @@ macro_rules! doctest_helper_json_rpc_owner_assert_response {
|
||||||
// create temporary grin server, run jsonrpc request on node api, delete server, return
|
// create temporary grin server, run jsonrpc request on node api, delete server, return
|
||||||
// json response.
|
// json response.
|
||||||
|
|
||||||
{
|
{
|
||||||
/*use grin_servers::test_framework::framework::run_doctest;
|
/*use grin_servers::test_framework::framework::run_doctest;
|
||||||
use grin_util as util;
|
use grin_util as util;
|
||||||
use serde_json;
|
use serde_json;
|
||||||
|
@ -425,6 +437,6 @@ macro_rules! doctest_helper_json_rpc_owner_assert_response {
|
||||||
serde_json::to_string_pretty(&expected_response).unwrap()
|
serde_json::to_string_pretty(&expected_response).unwrap()
|
||||||
);
|
);
|
||||||
}*/
|
}*/
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,7 +15,6 @@
|
||||||
//! Facade and handler for the rest of the blockchain implementation
|
//! Facade and handler for the rest of the blockchain implementation
|
||||||
//! and mostly the chain pipeline.
|
//! and mostly the chain pipeline.
|
||||||
|
|
||||||
use crate::core::core::hash::{Hash, Hashed};
|
|
||||||
use crate::core::core::merkle_proof::MerkleProof;
|
use crate::core::core::merkle_proof::MerkleProof;
|
||||||
use crate::core::core::{
|
use crate::core::core::{
|
||||||
Block, BlockHeader, BlockSums, Committed, Inputs, KernelFeatures, Output, OutputIdentifier,
|
Block, BlockHeader, BlockSums, Committed, Inputs, KernelFeatures, Output, OutputIdentifier,
|
||||||
|
@ -34,6 +33,11 @@ use crate::types::{
|
||||||
};
|
};
|
||||||
use crate::util::secp::pedersen::{Commitment, RangeProof};
|
use crate::util::secp::pedersen::{Commitment, RangeProof};
|
||||||
use crate::util::RwLock;
|
use crate::util::RwLock;
|
||||||
|
use crate::{
|
||||||
|
core::core::hash::{Hash, Hashed},
|
||||||
|
store::Batch,
|
||||||
|
txhashset::{ExtensionPair, HeaderExtension},
|
||||||
|
};
|
||||||
use grin_store::Error::NotFoundErr;
|
use grin_store::Error::NotFoundErr;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::fs::{self, File};
|
use std::fs::{self, File};
|
||||||
|
@ -151,6 +155,7 @@ pub struct Chain {
|
||||||
pibd_segmenter: Arc<RwLock<Option<Segmenter>>>,
|
pibd_segmenter: Arc<RwLock<Option<Segmenter>>>,
|
||||||
// POW verification function
|
// POW verification function
|
||||||
pow_verifier: fn(&BlockHeader) -> Result<(), pow::Error>,
|
pow_verifier: fn(&BlockHeader) -> Result<(), pow::Error>,
|
||||||
|
denylist: Arc<RwLock<Vec<Hash>>>,
|
||||||
archive_mode: bool,
|
archive_mode: bool,
|
||||||
genesis: BlockHeader,
|
genesis: BlockHeader,
|
||||||
}
|
}
|
||||||
|
@ -198,6 +203,7 @@ impl Chain {
|
||||||
header_pmmr: Arc::new(RwLock::new(header_pmmr)),
|
header_pmmr: Arc::new(RwLock::new(header_pmmr)),
|
||||||
pibd_segmenter: Arc::new(RwLock::new(None)),
|
pibd_segmenter: Arc::new(RwLock::new(None)),
|
||||||
pow_verifier,
|
pow_verifier,
|
||||||
|
denylist: Arc::new(RwLock::new(vec![])),
|
||||||
archive_mode,
|
archive_mode,
|
||||||
genesis: genesis.header,
|
genesis: genesis.header,
|
||||||
};
|
};
|
||||||
|
@ -222,6 +228,51 @@ impl Chain {
|
||||||
Ok(chain)
|
Ok(chain)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Add provided header hash to our "denylist".
|
||||||
|
/// The header corresponding to any "denied" hash will be rejected
|
||||||
|
/// and the peer subsequently banned.
|
||||||
|
pub fn invalidate_header(&self, hash: Hash) -> Result<(), Error> {
|
||||||
|
self.denylist.write().push(hash);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reset both head and header_head to the provided header.
|
||||||
|
/// Handles simple rewind and more complex fork scenarios.
|
||||||
|
/// Used by the reset_chain_head owner api endpoint.
|
||||||
|
pub fn reset_chain_head<T: Into<Tip>>(&self, head: T) -> Result<(), Error> {
|
||||||
|
let head = head.into();
|
||||||
|
|
||||||
|
let mut header_pmmr = self.header_pmmr.write();
|
||||||
|
let mut txhashset = self.txhashset.write();
|
||||||
|
let mut batch = self.store.batch()?;
|
||||||
|
|
||||||
|
let header = batch.get_block_header(&head.hash())?;
|
||||||
|
|
||||||
|
// Rewind and reapply blocks to reset the output/rangeproof/kernel MMR.
|
||||||
|
txhashset::extending(
|
||||||
|
&mut header_pmmr,
|
||||||
|
&mut txhashset,
|
||||||
|
&mut batch,
|
||||||
|
|ext, batch| {
|
||||||
|
self.rewind_and_apply_fork(&header, ext, batch)?;
|
||||||
|
batch.save_body_head(&head)?;
|
||||||
|
Ok(())
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
|
||||||
|
// If the rewind of full blocks was successful then we can rewind the header MMR.
|
||||||
|
// Rewind and reapply headers to reset the header MMR.
|
||||||
|
txhashset::header_extending(&mut header_pmmr, &mut batch, |ext, batch| {
|
||||||
|
self.rewind_and_apply_header_fork(&header, ext, batch)?;
|
||||||
|
batch.save_header_head(&head)?;
|
||||||
|
Ok(())
|
||||||
|
})?;
|
||||||
|
|
||||||
|
batch.commit()?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Are we running with archive_mode enabled?
|
/// Are we running with archive_mode enabled?
|
||||||
pub fn archive_mode(&self) -> bool {
|
pub fn archive_mode(&self) -> bool {
|
||||||
self.archive_mode
|
self.archive_mode
|
||||||
|
@ -278,7 +329,7 @@ impl Chain {
|
||||||
// If head is updated then we are either "next" block or we just experienced a "reorg" to new head.
|
// If head is updated then we are either "next" block or we just experienced a "reorg" to new head.
|
||||||
// Otherwise this is a "fork" off the main chain.
|
// Otherwise this is a "fork" off the main chain.
|
||||||
if let Some(head) = head {
|
if let Some(head) = head {
|
||||||
if head.prev_block_h == prev_head.last_block_h {
|
if self.is_on_current_chain(prev_head, head).is_ok() {
|
||||||
BlockStatus::Next { prev }
|
BlockStatus::Next { prev }
|
||||||
} else {
|
} else {
|
||||||
BlockStatus::Reorg {
|
BlockStatus::Reorg {
|
||||||
|
@ -297,7 +348,8 @@ impl Chain {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Quick check for "known" duplicate block up to and including current chain head.
|
/// Quick check for "known" duplicate block up to and including current chain head.
|
||||||
fn is_known(&self, header: &BlockHeader) -> Result<(), Error> {
|
/// Returns an error if this block is "known".
|
||||||
|
pub fn is_known(&self, header: &BlockHeader) -> Result<(), Error> {
|
||||||
let head = self.head()?;
|
let head = self.head()?;
|
||||||
if head.hash() == header.hash() {
|
if head.hash() == header.hash() {
|
||||||
return Err(ErrorKind::Unfit("duplicate block".into()).into());
|
return Err(ErrorKind::Unfit("duplicate block".into()).into());
|
||||||
|
@ -347,14 +399,14 @@ impl Chain {
|
||||||
/// Returns true if it has been added to the longest chain
|
/// Returns true if it has been added to the longest chain
|
||||||
/// or false if it has added to a fork (or orphan?).
|
/// or false if it has added to a fork (or orphan?).
|
||||||
fn process_block_single(&self, b: Block, opts: Options) -> Result<Option<Tip>, Error> {
|
fn process_block_single(&self, b: Block, opts: Options) -> Result<Option<Tip>, Error> {
|
||||||
// Check if we already know about this block.
|
|
||||||
self.is_known(&b.header)?;
|
|
||||||
|
|
||||||
// Process the header first.
|
// Process the header first.
|
||||||
// If invalid then fail early.
|
// If invalid then fail early.
|
||||||
// If valid then continue with block processing with header_head committed to db etc.
|
// If valid then continue with block processing with header_head committed to db etc.
|
||||||
self.process_block_header(&b.header, opts)?;
|
self.process_block_header(&b.header, opts)?;
|
||||||
|
|
||||||
|
// Check if we already know about this full block.
|
||||||
|
self.is_known(&b.header)?;
|
||||||
|
|
||||||
// Check if this block is an orphan.
|
// Check if this block is an orphan.
|
||||||
// Only do this once we know the header PoW is valid.
|
// Only do this once we know the header PoW is valid.
|
||||||
self.check_orphan(&b, opts)?;
|
self.check_orphan(&b, opts)?;
|
||||||
|
@ -431,9 +483,13 @@ impl Chain {
|
||||||
header_pmmr: &'a mut txhashset::PMMRHandle<BlockHeader>,
|
header_pmmr: &'a mut txhashset::PMMRHandle<BlockHeader>,
|
||||||
txhashset: &'a mut txhashset::TxHashSet,
|
txhashset: &'a mut txhashset::TxHashSet,
|
||||||
) -> Result<pipe::BlockContext<'a>, Error> {
|
) -> Result<pipe::BlockContext<'a>, Error> {
|
||||||
|
let denylist = self.denylist.read().clone();
|
||||||
Ok(pipe::BlockContext {
|
Ok(pipe::BlockContext {
|
||||||
opts,
|
opts,
|
||||||
pow_verifier: self.pow_verifier,
|
pow_verifier: self.pow_verifier,
|
||||||
|
header_allowed: Box::new(move |header| {
|
||||||
|
pipe::validate_header_denylist(header, &denylist)
|
||||||
|
}),
|
||||||
header_pmmr,
|
header_pmmr,
|
||||||
txhashset,
|
txhashset,
|
||||||
batch,
|
batch,
|
||||||
|
@ -621,7 +677,7 @@ impl Chain {
|
||||||
// latest block header. Rewind the extension to the specified header to
|
// latest block header. Rewind the extension to the specified header to
|
||||||
// ensure the view is consistent.
|
// ensure the view is consistent.
|
||||||
txhashset::extending_readonly(&mut header_pmmr, &mut txhashset, |ext, batch| {
|
txhashset::extending_readonly(&mut header_pmmr, &mut txhashset, |ext, batch| {
|
||||||
pipe::rewind_and_apply_fork(&header, ext, batch)?;
|
self.rewind_and_apply_fork(&header, ext, batch)?;
|
||||||
ext.extension
|
ext.extension
|
||||||
.validate(&self.genesis, fast_validation, &NoStatus, &header)?;
|
.validate(&self.genesis, fast_validation, &NoStatus, &header)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -634,7 +690,7 @@ impl Chain {
|
||||||
let prev_root =
|
let prev_root =
|
||||||
txhashset::header_extending_readonly(&mut header_pmmr, &self.store(), |ext, batch| {
|
txhashset::header_extending_readonly(&mut header_pmmr, &self.store(), |ext, batch| {
|
||||||
let prev_header = batch.get_previous_header(header)?;
|
let prev_header = batch.get_previous_header(header)?;
|
||||||
pipe::rewind_and_apply_header_fork(&prev_header, ext, batch)?;
|
self.rewind_and_apply_header_fork(&prev_header, ext, batch)?;
|
||||||
ext.root()
|
ext.root()
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
|
@ -653,7 +709,7 @@ impl Chain {
|
||||||
let (prev_root, roots, sizes) =
|
let (prev_root, roots, sizes) =
|
||||||
txhashset::extending_readonly(&mut header_pmmr, &mut txhashset, |ext, batch| {
|
txhashset::extending_readonly(&mut header_pmmr, &mut txhashset, |ext, batch| {
|
||||||
let previous_header = batch.get_previous_header(&b.header)?;
|
let previous_header = batch.get_previous_header(&b.header)?;
|
||||||
pipe::rewind_and_apply_fork(&previous_header, ext, batch)?;
|
self.rewind_and_apply_fork(&previous_header, ext, batch)?;
|
||||||
|
|
||||||
let extension = &mut ext.extension;
|
let extension = &mut ext.extension;
|
||||||
let header_extension = &mut ext.header_extension;
|
let header_extension = &mut ext.header_extension;
|
||||||
|
@ -698,7 +754,7 @@ impl Chain {
|
||||||
let mut txhashset = self.txhashset.write();
|
let mut txhashset = self.txhashset.write();
|
||||||
let merkle_proof =
|
let merkle_proof =
|
||||||
txhashset::extending_readonly(&mut header_pmmr, &mut txhashset, |ext, batch| {
|
txhashset::extending_readonly(&mut header_pmmr, &mut txhashset, |ext, batch| {
|
||||||
pipe::rewind_and_apply_fork(&header, ext, batch)?;
|
self.rewind_and_apply_fork(&header, ext, batch)?;
|
||||||
ext.extension.merkle_proof(out_id, batch)
|
ext.extension.merkle_proof(out_id, batch)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
|
@ -712,6 +768,34 @@ impl Chain {
|
||||||
txhashset.merkle_proof(commit)
|
txhashset.merkle_proof(commit)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Rewind and apply fork with the chain specific header validation (denylist) rules.
|
||||||
|
/// If we rewind and re-apply a "denied" block then validation will fail.
|
||||||
|
fn rewind_and_apply_fork(
|
||||||
|
&self,
|
||||||
|
header: &BlockHeader,
|
||||||
|
ext: &mut ExtensionPair,
|
||||||
|
batch: &Batch,
|
||||||
|
) -> Result<BlockHeader, Error> {
|
||||||
|
let denylist = self.denylist.read().clone();
|
||||||
|
pipe::rewind_and_apply_fork(header, ext, batch, &|header| {
|
||||||
|
pipe::validate_header_denylist(header, &denylist)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Rewind and apply fork with the chain specific header validation (denylist) rules.
|
||||||
|
/// If we rewind and re-apply a "denied" header then validation will fail.
|
||||||
|
fn rewind_and_apply_header_fork(
|
||||||
|
&self,
|
||||||
|
header: &BlockHeader,
|
||||||
|
ext: &mut HeaderExtension,
|
||||||
|
batch: &Batch,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
let denylist = self.denylist.read().clone();
|
||||||
|
pipe::rewind_and_apply_header_fork(header, ext, batch, &|header| {
|
||||||
|
pipe::validate_header_denylist(header, &denylist)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/// Provides a reading view into the current txhashset state as well as
|
/// Provides a reading view into the current txhashset state as well as
|
||||||
/// the required indexes for a consumer to rewind to a consistent state
|
/// the required indexes for a consumer to rewind to a consistent state
|
||||||
/// at the provided block hash.
|
/// at the provided block hash.
|
||||||
|
@ -725,8 +809,9 @@ impl Chain {
|
||||||
|
|
||||||
let mut header_pmmr = self.header_pmmr.write();
|
let mut header_pmmr = self.header_pmmr.write();
|
||||||
let mut txhashset = self.txhashset.write();
|
let mut txhashset = self.txhashset.write();
|
||||||
|
|
||||||
txhashset::extending_readonly(&mut header_pmmr, &mut txhashset, |ext, batch| {
|
txhashset::extending_readonly(&mut header_pmmr, &mut txhashset, |ext, batch| {
|
||||||
pipe::rewind_and_apply_fork(&header, ext, batch)?;
|
self.rewind_and_apply_fork(&header, ext, batch)?;
|
||||||
ext.extension.snapshot(batch)?;
|
ext.extension.snapshot(batch)?;
|
||||||
|
|
||||||
// prepare the zip
|
// prepare the zip
|
||||||
|
@ -853,7 +938,7 @@ impl Chain {
|
||||||
pub fn fork_point(&self) -> Result<BlockHeader, Error> {
|
pub fn fork_point(&self) -> Result<BlockHeader, Error> {
|
||||||
let body_head = self.head()?;
|
let body_head = self.head()?;
|
||||||
let mut current = self.get_block_header(&body_head.hash())?;
|
let mut current = self.get_block_header(&body_head.hash())?;
|
||||||
while !self.is_on_current_chain(¤t).is_ok() {
|
while !self.is_on_current_chain(¤t, body_head).is_ok() {
|
||||||
current = self.get_previous_header(¤t)?;
|
current = self.get_previous_header(¤t)?;
|
||||||
}
|
}
|
||||||
Ok(current)
|
Ok(current)
|
||||||
|
@ -1404,9 +1489,13 @@ impl Chain {
|
||||||
/// Verifies the given block header is actually on the current chain.
|
/// Verifies the given block header is actually on the current chain.
|
||||||
/// Checks the header_by_height index to verify the header is where we say
|
/// Checks the header_by_height index to verify the header is where we say
|
||||||
/// it is
|
/// it is
|
||||||
pub fn is_on_current_chain(&self, header: &BlockHeader) -> Result<(), Error> {
|
fn is_on_current_chain<T: Into<Tip>>(&self, x: T, head: Tip) -> Result<(), Error> {
|
||||||
let chain_header = self.get_header_by_height(header.height)?;
|
let x: Tip = x.into();
|
||||||
if chain_header.hash() == header.hash() {
|
if x.height > head.height {
|
||||||
|
return Err(ErrorKind::Other("not on current chain".to_string()).into());
|
||||||
|
}
|
||||||
|
|
||||||
|
if x.hash() == self.get_header_hash_by_height(x.height)? {
|
||||||
Ok(())
|
Ok(())
|
||||||
} else {
|
} else {
|
||||||
Err(ErrorKind::Other("not on current chain".to_string()).into())
|
Err(ErrorKind::Other("not on current chain".to_string()).into())
|
||||||
|
@ -1419,7 +1508,7 @@ impl Chain {
|
||||||
let mut header_pmmr = self.header_pmmr.write();
|
let mut header_pmmr = self.header_pmmr.write();
|
||||||
txhashset::header_extending_readonly(&mut header_pmmr, &self.store(), |ext, batch| {
|
txhashset::header_extending_readonly(&mut header_pmmr, &self.store(), |ext, batch| {
|
||||||
let header = batch.get_block_header(&sync_head.hash())?;
|
let header = batch.get_block_header(&sync_head.hash())?;
|
||||||
pipe::rewind_and_apply_header_fork(&header, ext, batch)?;
|
self.rewind_and_apply_header_fork(&header, ext, batch)?;
|
||||||
|
|
||||||
let hashes = heights
|
let hashes = heights
|
||||||
.iter()
|
.iter()
|
||||||
|
@ -1496,7 +1585,7 @@ fn setup_head(
|
||||||
let header = batch.get_block_header(&head.last_block_h)?;
|
let header = batch.get_block_header(&head.last_block_h)?;
|
||||||
|
|
||||||
let res = txhashset::extending(header_pmmr, txhashset, &mut batch, |ext, batch| {
|
let res = txhashset::extending(header_pmmr, txhashset, &mut batch, |ext, batch| {
|
||||||
pipe::rewind_and_apply_fork(&header, ext, batch)?;
|
pipe::rewind_and_apply_fork(&header, ext, batch, &|_| Ok(()))?;
|
||||||
|
|
||||||
let extension = &mut ext.extension;
|
let extension = &mut ext.extension;
|
||||||
|
|
||||||
|
@ -1544,7 +1633,7 @@ fn setup_head(
|
||||||
let prev_header = batch.get_block_header(&head.prev_block_h)?;
|
let prev_header = batch.get_block_header(&head.prev_block_h)?;
|
||||||
|
|
||||||
txhashset::extending(header_pmmr, txhashset, &mut batch, |ext, batch| {
|
txhashset::extending(header_pmmr, txhashset, &mut batch, |ext, batch| {
|
||||||
pipe::rewind_and_apply_fork(&prev_header, ext, batch)
|
pipe::rewind_and_apply_fork(&prev_header, ext, batch, &|_| Ok(()))
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
// Now "undo" the latest block and forget it ever existed.
|
// Now "undo" the latest block and forget it ever existed.
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
//! Implementation of the chain block acceptance (or refusal) pipeline.
|
//! Implementation of the chain block acceptance (or refusal) pipeline.
|
||||||
|
|
||||||
use crate::core::consensus;
|
use crate::core::consensus;
|
||||||
use crate::core::core::hash::Hashed;
|
use crate::core::core::hash::{Hash, Hashed};
|
||||||
use crate::core::core::Committed;
|
use crate::core::core::Committed;
|
||||||
use crate::core::core::{
|
use crate::core::core::{
|
||||||
block, Block, BlockHeader, BlockSums, HeaderVersion, OutputIdentifier, TransactionBody,
|
block, Block, BlockHeader, BlockSums, HeaderVersion, OutputIdentifier, TransactionBody,
|
||||||
|
@ -34,6 +34,8 @@ pub struct BlockContext<'a> {
|
||||||
pub opts: Options,
|
pub opts: Options,
|
||||||
/// The pow verifier to use when processing a block.
|
/// The pow verifier to use when processing a block.
|
||||||
pub pow_verifier: fn(&BlockHeader) -> Result<(), pow::Error>,
|
pub pow_verifier: fn(&BlockHeader) -> Result<(), pow::Error>,
|
||||||
|
/// Custom fn allowing arbitrary header validation rules (denylist) to be applied.
|
||||||
|
pub header_allowed: Box<dyn Fn(&BlockHeader) -> Result<(), Error>>,
|
||||||
/// The active txhashset (rewindable MMRs) to use for block processing.
|
/// The active txhashset (rewindable MMRs) to use for block processing.
|
||||||
pub txhashset: &'a mut txhashset::TxHashSet,
|
pub txhashset: &'a mut txhashset::TxHashSet,
|
||||||
/// The active header MMR handle.
|
/// The active header MMR handle.
|
||||||
|
@ -119,8 +121,9 @@ pub fn process_block(
|
||||||
let header_pmmr = &mut ctx.header_pmmr;
|
let header_pmmr = &mut ctx.header_pmmr;
|
||||||
let txhashset = &mut ctx.txhashset;
|
let txhashset = &mut ctx.txhashset;
|
||||||
let batch = &mut ctx.batch;
|
let batch = &mut ctx.batch;
|
||||||
|
let ctx_specific_validation = &ctx.header_allowed;
|
||||||
let fork_point = txhashset::extending(header_pmmr, txhashset, batch, |ext, batch| {
|
let fork_point = txhashset::extending(header_pmmr, txhashset, batch, |ext, batch| {
|
||||||
let fork_point = rewind_and_apply_fork(&prev, ext, batch)?;
|
let fork_point = rewind_and_apply_fork(&prev, ext, batch, ctx_specific_validation)?;
|
||||||
|
|
||||||
// Check any coinbase being spent have matured sufficiently.
|
// Check any coinbase being spent have matured sufficiently.
|
||||||
// This needs to be done within the context of a potentially
|
// This needs to be done within the context of a potentially
|
||||||
|
@ -198,9 +201,11 @@ pub fn process_block_headers(
|
||||||
add_block_header(header, &ctx.batch)?;
|
add_block_header(header, &ctx.batch)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let ctx_specific_validation = &ctx.header_allowed;
|
||||||
|
|
||||||
// Now apply this entire chunk of headers to the header MMR.
|
// Now apply this entire chunk of headers to the header MMR.
|
||||||
txhashset::header_extending(&mut ctx.header_pmmr, &mut ctx.batch, |ext, batch| {
|
txhashset::header_extending(&mut ctx.header_pmmr, &mut ctx.batch, |ext, batch| {
|
||||||
rewind_and_apply_header_fork(&last_header, ext, batch)?;
|
rewind_and_apply_header_fork(&last_header, ext, batch, ctx_specific_validation)?;
|
||||||
|
|
||||||
// If previous sync_head is not on the "current" chain then
|
// If previous sync_head is not on the "current" chain then
|
||||||
// these headers are on an alternative fork to sync_head.
|
// these headers are on an alternative fork to sync_head.
|
||||||
|
@ -255,10 +260,12 @@ pub fn process_block_header(header: &BlockHeader, ctx: &mut BlockContext<'_>) ->
|
||||||
// We want to validate this individual header before applying it to our header PMMR.
|
// We want to validate this individual header before applying it to our header PMMR.
|
||||||
validate_header(header, ctx)?;
|
validate_header(header, ctx)?;
|
||||||
|
|
||||||
|
let ctx_specific_validation = &ctx.header_allowed;
|
||||||
|
|
||||||
// Apply the header to the header PMMR, making sure we put the extension in the correct state
|
// Apply the header to the header PMMR, making sure we put the extension in the correct state
|
||||||
// based on previous header first.
|
// based on previous header first.
|
||||||
txhashset::header_extending(&mut ctx.header_pmmr, &mut ctx.batch, |ext, batch| {
|
txhashset::header_extending(&mut ctx.header_pmmr, &mut ctx.batch, |ext, batch| {
|
||||||
rewind_and_apply_header_fork(&prev_header, ext, batch)?;
|
rewind_and_apply_header_fork(&prev_header, ext, batch, ctx_specific_validation)?;
|
||||||
ext.validate_root(header)?;
|
ext.validate_root(header)?;
|
||||||
ext.apply_header(header)?;
|
ext.apply_header(header)?;
|
||||||
if !has_more_work(&header, &header_head) {
|
if !has_more_work(&header, &header_head) {
|
||||||
|
@ -322,10 +329,42 @@ fn prev_header_store(
|
||||||
Ok(prev)
|
Ok(prev)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Apply any "header_invalidated" (aka denylist) rules provided as part of the context.
|
||||||
|
fn validate_header_ctx(header: &BlockHeader, ctx: &mut BlockContext<'_>) -> Result<(), Error> {
|
||||||
|
// Apply any custom header validation rules via the context.
|
||||||
|
(ctx.header_allowed)(header)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validate header against an explicit "denylist" of header hashes.
|
||||||
|
/// Returns a "Block" error which is "bad_data" and will result in peer being banned.
|
||||||
|
pub fn validate_header_denylist(header: &BlockHeader, denylist: &[Hash]) -> Result<(), Error> {
|
||||||
|
if denylist.is_empty() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assume our denylist is a manageable size for now.
|
||||||
|
// Log it here to occasionally remind us.
|
||||||
|
debug!(
|
||||||
|
"validate_header_denylist: {} at {}, denylist: {:?}",
|
||||||
|
header.hash(),
|
||||||
|
header.height,
|
||||||
|
denylist
|
||||||
|
);
|
||||||
|
|
||||||
|
if denylist.contains(&header.hash()) {
|
||||||
|
return Err(ErrorKind::Block(block::Error::Other("header hash denied".into())).into());
|
||||||
|
} else {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// First level of block validation that only needs to act on the block header
|
/// First level of block validation that only needs to act on the block header
|
||||||
/// to make it as cheap as possible. The different validations are also
|
/// to make it as cheap as possible. The different validations are also
|
||||||
/// arranged by order of cost to have as little DoS surface as possible.
|
/// arranged by order of cost to have as little DoS surface as possible.
|
||||||
fn validate_header(header: &BlockHeader, ctx: &mut BlockContext<'_>) -> Result<(), Error> {
|
fn validate_header(header: &BlockHeader, ctx: &mut BlockContext<'_>) -> Result<(), Error> {
|
||||||
|
// Apply any ctx specific header validation (denylist) rules.
|
||||||
|
validate_header_ctx(header, ctx)?;
|
||||||
|
|
||||||
// First I/O cost, delayed as late as possible.
|
// First I/O cost, delayed as late as possible.
|
||||||
let prev = prev_header_store(header, &mut ctx.batch)?;
|
let prev = prev_header_store(header, &mut ctx.batch)?;
|
||||||
|
|
||||||
|
@ -537,6 +576,7 @@ pub fn rewind_and_apply_header_fork(
|
||||||
header: &BlockHeader,
|
header: &BlockHeader,
|
||||||
ext: &mut txhashset::HeaderExtension<'_>,
|
ext: &mut txhashset::HeaderExtension<'_>,
|
||||||
batch: &store::Batch<'_>,
|
batch: &store::Batch<'_>,
|
||||||
|
ctx_specific_validation: &dyn Fn(&BlockHeader) -> Result<(), Error>,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
let mut fork_hashes = vec![];
|
let mut fork_hashes = vec![];
|
||||||
let mut current = header.clone();
|
let mut current = header.clone();
|
||||||
|
@ -556,6 +596,11 @@ pub fn rewind_and_apply_header_fork(
|
||||||
let header = batch
|
let header = batch
|
||||||
.get_block_header(&h)
|
.get_block_header(&h)
|
||||||
.map_err(|e| ErrorKind::StoreErr(e, "getting forked headers".to_string()))?;
|
.map_err(|e| ErrorKind::StoreErr(e, "getting forked headers".to_string()))?;
|
||||||
|
|
||||||
|
// Re-validate every header being re-applied.
|
||||||
|
// This makes it possible to check all header hashes against the ctx specific "denylist".
|
||||||
|
(ctx_specific_validation)(&header)?;
|
||||||
|
|
||||||
ext.validate_root(&header)?;
|
ext.validate_root(&header)?;
|
||||||
ext.apply_header(&header)?;
|
ext.apply_header(&header)?;
|
||||||
}
|
}
|
||||||
|
@ -572,12 +617,13 @@ pub fn rewind_and_apply_fork(
|
||||||
header: &BlockHeader,
|
header: &BlockHeader,
|
||||||
ext: &mut txhashset::ExtensionPair<'_>,
|
ext: &mut txhashset::ExtensionPair<'_>,
|
||||||
batch: &store::Batch<'_>,
|
batch: &store::Batch<'_>,
|
||||||
|
ctx_specific_validation: &dyn Fn(&BlockHeader) -> Result<(), Error>,
|
||||||
) -> Result<BlockHeader, Error> {
|
) -> Result<BlockHeader, Error> {
|
||||||
let extension = &mut ext.extension;
|
let extension = &mut ext.extension;
|
||||||
let header_extension = &mut ext.header_extension;
|
let header_extension = &mut ext.header_extension;
|
||||||
|
|
||||||
// Prepare the header MMR.
|
// Prepare the header MMR.
|
||||||
rewind_and_apply_header_fork(header, header_extension, batch)?;
|
rewind_and_apply_header_fork(header, header_extension, batch, ctx_specific_validation)?;
|
||||||
|
|
||||||
// Rewind the txhashset extension back to common ancestor based on header MMR.
|
// Rewind the txhashset extension back to common ancestor based on header MMR.
|
||||||
let mut current = batch.head_header()?;
|
let mut current = batch.head_header()?;
|
||||||
|
|
|
@ -807,8 +807,6 @@ where
|
||||||
{
|
{
|
||||||
let batch = store.batch()?;
|
let batch = store.batch()?;
|
||||||
|
|
||||||
// Note: Extending either the sync_head or header_head MMR here.
|
|
||||||
// Use underlying MMR to determine the "head".
|
|
||||||
let head = match handle.head_hash() {
|
let head = match handle.head_hash() {
|
||||||
Ok(hash) => {
|
Ok(hash) => {
|
||||||
let header = batch.get_block_header(&hash)?;
|
let header = batch.get_block_header(&hash)?;
|
||||||
|
@ -845,8 +843,6 @@ where
|
||||||
// index saving can be undone
|
// index saving can be undone
|
||||||
let child_batch = batch.child()?;
|
let child_batch = batch.child()?;
|
||||||
|
|
||||||
// Note: Extending either the sync_head or header_head MMR here.
|
|
||||||
// Use underlying MMR to determine the "head".
|
|
||||||
let head = match handle.head_hash() {
|
let head = match handle.head_hash() {
|
||||||
Ok(hash) => {
|
Ok(hash) => {
|
||||||
let header = child_batch.get_block_header(&hash)?;
|
let header = child_batch.get_block_header(&hash)?;
|
||||||
|
|
|
@ -134,6 +134,12 @@ impl SyncState {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Reset sync status to NoSync.
|
||||||
|
pub fn reset(&self) {
|
||||||
|
self.clear_sync_error();
|
||||||
|
self.update(SyncStatus::NoSync);
|
||||||
|
}
|
||||||
|
|
||||||
/// Whether the current state matches any active syncing operation.
|
/// Whether the current state matches any active syncing operation.
|
||||||
/// Note: This includes our "initial" state.
|
/// Note: This includes our "initial" state.
|
||||||
pub fn is_syncing(&self) -> bool {
|
pub fn is_syncing(&self) -> bool {
|
||||||
|
@ -363,6 +369,23 @@ impl Tip {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<BlockHeader> for Tip {
|
||||||
|
fn from(header: BlockHeader) -> Self {
|
||||||
|
Self::from(&header)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&BlockHeader> for Tip {
|
||||||
|
fn from(header: &BlockHeader) -> Self {
|
||||||
|
Tip {
|
||||||
|
height: header.height,
|
||||||
|
last_block_h: header.hash(),
|
||||||
|
prev_block_h: header.prev_hash,
|
||||||
|
total_difficulty: header.total_difficulty(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Hashed for Tip {
|
impl Hashed for Tip {
|
||||||
/// The hash of the underlying block.
|
/// The hash of the underlying block.
|
||||||
fn hash(&self) -> Hash {
|
fn hash(&self) -> Hash {
|
||||||
|
@ -380,16 +403,6 @@ impl Default for Tip {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
impl From<&BlockHeader> for Tip {
|
|
||||||
fn from(header: &BlockHeader) -> Tip {
|
|
||||||
Tip {
|
|
||||||
height: header.height,
|
|
||||||
last_block_h: header.hash(),
|
|
||||||
prev_block_h: header.prev_hash,
|
|
||||||
total_difficulty: header.total_difficulty(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Serialization of a tip, required to save to datastore.
|
/// Serialization of a tip, required to save to datastore.
|
||||||
impl ser::Writeable for Tip {
|
impl ser::Writeable for Tip {
|
||||||
|
|
|
@ -139,9 +139,10 @@ where
|
||||||
peer_info: &PeerInfo,
|
peer_info: &PeerInfo,
|
||||||
opts: chain::Options,
|
opts: chain::Options,
|
||||||
) -> Result<bool, chain::Error> {
|
) -> Result<bool, chain::Error> {
|
||||||
if self.chain().block_exists(b.hash())? {
|
if self.chain().is_known(&b.header).is_err() {
|
||||||
return Ok(true);
|
return Ok(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
debug!(
|
debug!(
|
||||||
"Received block {} at {} from {} [in/out/kern: {}/{}/{}] going to process.",
|
"Received block {} at {} from {} [in/out/kern: {}/{}/{}] going to process.",
|
||||||
b.hash(),
|
b.hash(),
|
||||||
|
@ -160,9 +161,10 @@ where
|
||||||
peer_info: &PeerInfo,
|
peer_info: &PeerInfo,
|
||||||
) -> Result<bool, chain::Error> {
|
) -> Result<bool, chain::Error> {
|
||||||
// No need to process this compact block if we have previously accepted the _full block_.
|
// No need to process this compact block if we have previously accepted the _full block_.
|
||||||
if self.chain().block_exists(cb.hash())? {
|
if self.chain().is_known(&cb.header).is_err() {
|
||||||
return Ok(true);
|
return Ok(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
let bhash = cb.hash();
|
let bhash = cb.hash();
|
||||||
debug!(
|
debug!(
|
||||||
"Received compact_block {} at {} from {} [out/kern/kern_ids: {}/{}/{}] going to process.",
|
"Received compact_block {} at {} from {} [out/kern/kern_ids: {}/{}/{}] going to process.",
|
||||||
|
|
|
@ -129,6 +129,26 @@ impl HTTPNodeClient {
|
||||||
e.reset().unwrap();
|
e.reset().unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn reset_chain_head(&self, hash: String) {
|
||||||
|
let mut e = term::stdout().unwrap();
|
||||||
|
let params = json!([hash]);
|
||||||
|
match self.send_json_request::<()>("reset_chain_head", ¶ms) {
|
||||||
|
Ok(_) => writeln!(e, "Successfully reset chain head {}", hash).unwrap(),
|
||||||
|
Err(_) => writeln!(e, "Failed to reset chain head {}", hash).unwrap(),
|
||||||
|
}
|
||||||
|
e.reset().unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn invalidate_header(&self, hash: String) {
|
||||||
|
let mut e = term::stdout().unwrap();
|
||||||
|
let params = json!([hash]);
|
||||||
|
match self.send_json_request::<()>("invalidate_header", ¶ms) {
|
||||||
|
Ok(_) => writeln!(e, "Successfully invalidated header: {}", hash).unwrap(),
|
||||||
|
Err(_) => writeln!(e, "Failed to invalidate header: {}", hash).unwrap(),
|
||||||
|
}
|
||||||
|
e.reset().unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
pub fn ban_peer(&self, peer_addr: &SocketAddr) {
|
pub fn ban_peer(&self, peer_addr: &SocketAddr) {
|
||||||
let mut e = term::stdout().unwrap();
|
let mut e = term::stdout().unwrap();
|
||||||
let params = json!([peer_addr]);
|
let params = json!([peer_addr]);
|
||||||
|
@ -163,6 +183,14 @@ pub fn client_command(client_args: &ArgMatches<'_>, global_config: GlobalConfig)
|
||||||
("listconnectedpeers", Some(_)) => {
|
("listconnectedpeers", Some(_)) => {
|
||||||
node_client.list_connected_peers();
|
node_client.list_connected_peers();
|
||||||
}
|
}
|
||||||
|
("resetchainhead", Some(args)) => {
|
||||||
|
let hash = args.value_of("hash").unwrap();
|
||||||
|
node_client.reset_chain_head(hash.to_string());
|
||||||
|
}
|
||||||
|
("invalidateheader", Some(args)) => {
|
||||||
|
let hash = args.value_of("hash").unwrap();
|
||||||
|
node_client.invalidate_header(hash.to_string());
|
||||||
|
}
|
||||||
("ban", Some(peer_args)) => {
|
("ban", Some(peer_args)) => {
|
||||||
let peer = peer_args.value_of("peer").unwrap();
|
let peer = peer_args.value_of("peer").unwrap();
|
||||||
|
|
||||||
|
|
|
@ -72,3 +72,15 @@ subcommands:
|
||||||
long: peer
|
long: peer
|
||||||
required: true
|
required: true
|
||||||
takes_value: true
|
takes_value: true
|
||||||
|
- resetchainhead:
|
||||||
|
about: Resets the local chain head
|
||||||
|
args:
|
||||||
|
- hash:
|
||||||
|
help: The header hash to reset to
|
||||||
|
required: true
|
||||||
|
- invalidateheader:
|
||||||
|
about: Adds header hash to denylist
|
||||||
|
args:
|
||||||
|
- hash:
|
||||||
|
help: The header hash to invalidate
|
||||||
|
required: true
|
Loading…
Reference in a new issue