diff --git a/.github/workflows/cd-deploy-nodes-gcp.yml b/.github/workflows/cd-deploy-nodes-gcp.yml index cccdd4af542..315a7dc4464 100644 --- a/.github/workflows/cd-deploy-nodes-gcp.yml +++ b/.github/workflows/cd-deploy-nodes-gcp.yml @@ -269,13 +269,13 @@ jobs: # Setup gcloud CLI - name: Authenticate to Google Cloud id: auth - uses: google-github-actions/auth@v2.1.7 + uses: google-github-actions/auth@v2.1.8 with: workload_identity_provider: '${{ vars.GCP_WIF }}' service_account: '${{ vars.GCP_DEPLOYMENTS_SA }}' - name: Set up Cloud SDK - uses: google-github-actions/setup-gcloud@v2.1.2 + uses: google-github-actions/setup-gcloud@v2.1.4 - name: Create instance template for ${{ matrix.network }} run: | @@ -384,13 +384,13 @@ jobs: # Setup gcloud CLI - name: Authenticate to Google Cloud id: auth - uses: google-github-actions/auth@v2.1.7 + uses: google-github-actions/auth@v2.1.8 with: workload_identity_provider: '${{ vars.GCP_WIF }}' service_account: '${{ vars.GCP_DEPLOYMENTS_SA }}' - name: Set up Cloud SDK - uses: google-github-actions/setup-gcloud@v2.1.2 + uses: google-github-actions/setup-gcloud@v2.1.4 # Create instance template from container image - name: Manual deploy of a single ${{ inputs.network }} instance running zebrad diff --git a/.github/workflows/chore-delete-gcp-resources.yml b/.github/workflows/chore-delete-gcp-resources.yml index 962442fc8d8..661c8c05093 100644 --- a/.github/workflows/chore-delete-gcp-resources.yml +++ b/.github/workflows/chore-delete-gcp-resources.yml @@ -50,13 +50,13 @@ jobs: # Setup gcloud CLI - name: Authenticate to Google Cloud id: auth - uses: google-github-actions/auth@v2.1.7 + uses: google-github-actions/auth@v2.1.8 with: workload_identity_provider: '${{ vars.GCP_WIF }}' service_account: '${{ vars.GCP_DEPLOYMENTS_SA }}' - name: Set up Cloud SDK - uses: google-github-actions/setup-gcloud@v2.1.2 + uses: google-github-actions/setup-gcloud@v2.1.4 # Deletes all mainnet and testnet instances older than $DELETE_INSTANCE_DAYS days. # @@ -121,7 +121,7 @@ jobs: # Setup gcloud CLI - name: Authenticate to Google Cloud id: auth - uses: google-github-actions/auth@v2.1.7 + uses: google-github-actions/auth@v2.1.8 with: workload_identity_provider: '${{ vars.GCP_WIF }}' service_account: '${{ vars.GCP_DEPLOYMENTS_SA }}' diff --git a/.github/workflows/ci-lint.yml b/.github/workflows/ci-lint.yml index 0e40daa7c7e..df13ae1b1f7 100644 --- a/.github/workflows/ci-lint.yml +++ b/.github/workflows/ci-lint.yml @@ -44,7 +44,7 @@ jobs: - name: Rust files id: changed-files-rust - uses: tj-actions/changed-files@v45.0.6 + uses: tj-actions/changed-files@v45.0.7 with: files: | **/*.rs @@ -56,7 +56,7 @@ jobs: - name: Workflow files id: changed-files-workflows - uses: tj-actions/changed-files@v45.0.6 + uses: tj-actions/changed-files@v45.0.7 with: files: | .github/workflows/*.yml diff --git a/.github/workflows/docs-deploy-firebase.yml b/.github/workflows/docs-deploy-firebase.yml index 0154ffe1bd7..eca70c4d98b 100644 --- a/.github/workflows/docs-deploy-firebase.yml +++ b/.github/workflows/docs-deploy-firebase.yml @@ -92,7 +92,7 @@ jobs: - uses: r7kamura/rust-problem-matchers@v1.5.0 - name: Setup mdBook - uses: jontze/action-mdbook@v3.0.1 + uses: jontze/action-mdbook@v4.0.0 with: token: ${{ secrets.GITHUB_TOKEN }} mdbook-version: '~0.4' @@ -106,7 +106,7 @@ jobs: # Setup gcloud CLI - name: Authenticate to Google Cloud id: auth - uses: google-github-actions/auth@v2.1.7 + uses: google-github-actions/auth@v2.1.8 with: workload_identity_provider: '${{ vars.GCP_WIF }}' service_account: '${{ vars.GCP_FIREBASE_SA }}' @@ -164,7 +164,7 @@ jobs: # Setup gcloud CLI - name: Authenticate to Google Cloud id: auth - uses: google-github-actions/auth@v2.1.7 + uses: google-github-actions/auth@v2.1.8 with: workload_identity_provider: '${{ vars.GCP_WIF }}' service_account: '${{ vars.GCP_FIREBASE_SA }}' diff --git a/.github/workflows/manual-zcashd-deploy.yml b/.github/workflows/manual-zcashd-deploy.yml index 8fc5951d142..8d6541ff370 100644 --- a/.github/workflows/manual-zcashd-deploy.yml +++ b/.github/workflows/manual-zcashd-deploy.yml @@ -52,13 +52,13 @@ jobs: # Setup gcloud CLI - name: Authenticate to Google Cloud id: auth - uses: google-github-actions/auth@v2.1.7 + uses: google-github-actions/auth@v2.1.8 with: workload_identity_provider: '${{ vars.GCP_WIF }}' service_account: '${{ vars.GCP_DEPLOYMENTS_SA }}' - name: Set up Cloud SDK - uses: google-github-actions/setup-gcloud@v2.1.2 + uses: google-github-actions/setup-gcloud@v2.1.4 # Create instance template from container image - name: Create instance template diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml index 6b1e21364d3..b5025a4b463 100644 --- a/.github/workflows/release-drafter.yml +++ b/.github/workflows/release-drafter.yml @@ -38,7 +38,7 @@ jobs: runs-on: ubuntu-latest steps: # Drafts your next Release notes - - uses: release-drafter/release-drafter@v6.0.0 + - uses: release-drafter/release-drafter@v6.1.0 with: config-name: release-drafter.yml commitish: main diff --git a/.github/workflows/sub-build-docker-image.yml b/.github/workflows/sub-build-docker-image.yml index 743b3e1565c..c5142babe31 100644 --- a/.github/workflows/sub-build-docker-image.yml +++ b/.github/workflows/sub-build-docker-image.yml @@ -127,7 +127,7 @@ jobs: - name: Authenticate to Google Cloud id: auth - uses: google-github-actions/auth@v2.1.7 + uses: google-github-actions/auth@v2.1.8 with: workload_identity_provider: '${{ vars.GCP_WIF }}' service_account: '${{ vars.GCP_ARTIFACTS_SA }}' diff --git a/.github/workflows/sub-deploy-integration-tests-gcp.yml b/.github/workflows/sub-deploy-integration-tests-gcp.yml index bd23da1f31b..1a8854febd0 100644 --- a/.github/workflows/sub-deploy-integration-tests-gcp.yml +++ b/.github/workflows/sub-deploy-integration-tests-gcp.yml @@ -172,13 +172,13 @@ jobs: # Setup gcloud CLI - name: Authenticate to Google Cloud id: auth - uses: google-github-actions/auth@v2.1.7 + uses: google-github-actions/auth@v2.1.8 with: workload_identity_provider: '${{ vars.GCP_WIF }}' service_account: '${{ vars.GCP_DEPLOYMENTS_SA }}' - name: Set up Cloud SDK - uses: google-github-actions/setup-gcloud@v2.1.2 + uses: google-github-actions/setup-gcloud@v2.1.4 # Create a Compute Engine virtual machine and attach a cached state disk using the # $CACHED_DISK_NAME env as the source image to populate the disk cached state @@ -429,13 +429,13 @@ jobs: # Setup gcloud CLI - name: Authenticate to Google Cloud id: auth - uses: google-github-actions/auth@v2.1.7 + uses: google-github-actions/auth@v2.1.8 with: workload_identity_provider: '${{ vars.GCP_WIF }}' service_account: '${{ vars.GCP_DEPLOYMENTS_SA }}' - name: Set up Cloud SDK - uses: google-github-actions/setup-gcloud@v2.1.2 + uses: google-github-actions/setup-gcloud@v2.1.4 # Sets the $UPDATE_SUFFIX env var to "-u" if updating a previous cached state, # and the empty string otherwise. @@ -695,13 +695,13 @@ jobs: # Setup gcloud CLI - name: Authenticate to Google Cloud id: auth - uses: google-github-actions/auth@v2.1.7 + uses: google-github-actions/auth@v2.1.8 with: workload_identity_provider: '${{ vars.GCP_WIF }}' service_account: '${{ vars.GCP_DEPLOYMENTS_SA }}' - name: Set up Cloud SDK - uses: google-github-actions/setup-gcloud@v2.1.2 + uses: google-github-actions/setup-gcloud@v2.1.4 # Deletes the instances that has been recently deployed in the actual commit after all # previous jobs have run, no matter the outcome of the job. diff --git a/.github/workflows/sub-find-cached-disks.yml b/.github/workflows/sub-find-cached-disks.yml index a45e3f731fa..d0dd52d6c1e 100644 --- a/.github/workflows/sub-find-cached-disks.yml +++ b/.github/workflows/sub-find-cached-disks.yml @@ -67,13 +67,13 @@ jobs: # Setup gcloud CLI - name: Authenticate to Google Cloud id: auth - uses: google-github-actions/auth@v2.1.7 + uses: google-github-actions/auth@v2.1.8 with: workload_identity_provider: '${{ vars.GCP_WIF }}' service_account: '${{ vars.GCP_DEPLOYMENTS_SA }}' - name: Set up Cloud SDK - uses: google-github-actions/setup-gcloud@v2.1.2 + uses: google-github-actions/setup-gcloud@v2.1.4 # Performs formatting on disk name components. # diff --git a/zebra-state/src/service/non_finalized_state.rs b/zebra-state/src/service/non_finalized_state.rs index ebcbb2cfd35..91ae30ae23d 100644 --- a/zebra-state/src/service/non_finalized_state.rs +++ b/zebra-state/src/service/non_finalized_state.rs @@ -9,7 +9,7 @@ use std::{ }; use zebra_chain::{ - block::{self, Block}, + block::{self, Block, Hash}, parameters::Network, sprout, transparent, }; @@ -45,6 +45,10 @@ pub struct NonFinalizedState { /// callers should migrate to `chain_iter().next()`. chain_set: BTreeSet>, + /// Blocks that have been invalidated in, and removed from, the non finalized + /// state. + invalidated_blocks: HashMap>>, + // Configuration // /// The configured Zcash network. @@ -92,6 +96,7 @@ impl Clone for NonFinalizedState { Self { chain_set: self.chain_set.clone(), network: self.network.clone(), + invalidated_blocks: self.invalidated_blocks.clone(), #[cfg(feature = "getblocktemplate-rpcs")] should_count_metrics: self.should_count_metrics, @@ -112,6 +117,7 @@ impl NonFinalizedState { NonFinalizedState { chain_set: Default::default(), network: network.clone(), + invalidated_blocks: Default::default(), #[cfg(feature = "getblocktemplate-rpcs")] should_count_metrics: true, #[cfg(feature = "progress-bar")] @@ -264,6 +270,37 @@ impl NonFinalizedState { Ok(()) } + /// Invalidate block with hash `block_hash` and all descendants from the non-finalized state. Insert + /// the new chain into the chain_set and discard the previous. + pub fn invalidate_block(&mut self, block_hash: Hash) { + let Some(chain) = self.find_chain(|chain| chain.contains_block_hash(block_hash)) else { + return; + }; + + let invalidated_blocks = if chain.non_finalized_root_hash() == block_hash { + self.chain_set.remove(&chain); + chain.blocks.values().cloned().collect() + } else { + let (new_chain, invalidated_blocks) = chain + .invalidate_block(block_hash) + .expect("already checked that chain contains hash"); + + // Add the new chain fork or updated chain to the set of recent chains, and + // remove the chain containing the hash of the block from chain set + self.insert_with(Arc::new(new_chain.clone()), |chain_set| { + chain_set.retain(|c| !c.contains_block_hash(block_hash)) + }); + + invalidated_blocks + }; + + self.invalidated_blocks + .insert(block_hash, Arc::new(invalidated_blocks)); + + self.update_metrics_for_chains(); + self.update_metrics_bars(); + } + /// Commit block to the non-finalized state as a new chain where its parent /// is the finalized tip. #[tracing::instrument(level = "debug", skip(self, finalized_state, prepared))] @@ -586,6 +623,11 @@ impl NonFinalizedState { self.chain_set.len() } + /// Return the invalidated blocks. + pub fn invalidated_blocks(&self) -> HashMap>> { + self.invalidated_blocks.clone() + } + /// Return the chain whose tip block hash is `parent_hash`. /// /// The chain can be an existing chain in the non-finalized state, or a freshly diff --git a/zebra-state/src/service/non_finalized_state/chain.rs b/zebra-state/src/service/non_finalized_state/chain.rs index 6ad284a23f5..c7d0d2877c6 100644 --- a/zebra-state/src/service/non_finalized_state/chain.rs +++ b/zebra-state/src/service/non_finalized_state/chain.rs @@ -359,6 +359,26 @@ impl Chain { (block, treestate) } + // Returns the block at the provided height and all of its descendant blocks. + pub fn child_blocks(&self, block_height: &block::Height) -> Vec { + self.blocks + .range(block_height..) + .map(|(_h, b)| b.clone()) + .collect() + } + + // Returns a new chain without the invalidated block or its descendants. + pub fn invalidate_block( + &self, + block_hash: block::Hash, + ) -> Option<(Self, Vec)> { + let block_height = self.height_by_hash(block_hash)?; + let mut new_chain = self.fork(block_hash)?; + new_chain.pop_tip(); + new_chain.last_fork_height = None; + Some((new_chain, self.child_blocks(&block_height))) + } + /// Returns the height of the chain root. pub fn non_finalized_root_height(&self) -> block::Height { self.blocks @@ -1600,7 +1620,7 @@ impl DerefMut for Chain { /// The revert position being performed on a chain. #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] -enum RevertPosition { +pub(crate) enum RevertPosition { /// The chain root is being reverted via [`Chain::pop_root`], when a block /// is finalized. Root, @@ -1619,7 +1639,7 @@ enum RevertPosition { /// and [`Chain::pop_tip`] functions, and fear that it would be easy to /// introduce bugs when updating them, unless the code was reorganized to keep /// related operations adjacent to each other. -trait UpdateWith { +pub(crate) trait UpdateWith { /// When `T` is added to the chain tip, /// update [`Chain`] cumulative data members to add data that are derived from `T`. fn update_chain_tip_with(&mut self, _: &T) -> Result<(), ValidateContextError>; diff --git a/zebra-state/src/service/non_finalized_state/tests/vectors.rs b/zebra-state/src/service/non_finalized_state/tests/vectors.rs index b489d6f94f0..5b392e4a0b9 100644 --- a/zebra-state/src/service/non_finalized_state/tests/vectors.rs +++ b/zebra-state/src/service/non_finalized_state/tests/vectors.rs @@ -4,7 +4,7 @@ use std::sync::Arc; use zebra_chain::{ amount::NonNegative, - block::{Block, Height}, + block::{self, Block, Height}, history_tree::NonEmptyHistoryTree, parameters::{Network, NetworkUpgrade}, serialization::ZcashDeserializeInto, @@ -216,6 +216,94 @@ fn finalize_pops_from_best_chain_for_network(network: Network) -> Result<()> { Ok(()) } +fn invalidate_block_removes_block_and_descendants_from_chain_for_network( + network: Network, +) -> Result<()> { + let block1: Arc = Arc::new(network.test_block(653599, 583999).unwrap()); + let block2 = block1.make_fake_child().set_work(10); + let block3 = block2.make_fake_child().set_work(1); + + let mut state = NonFinalizedState::new(&network); + let finalized_state = FinalizedState::new( + &Config::ephemeral(), + &network, + #[cfg(feature = "elasticsearch")] + false, + ); + + let fake_value_pool = ValueBalance::::fake_populated_pool(); + finalized_state.set_finalized_value_pool(fake_value_pool); + + state.commit_new_chain(block1.clone().prepare(), &finalized_state)?; + state.commit_block(block2.clone().prepare(), &finalized_state)?; + state.commit_block(block3.clone().prepare(), &finalized_state)?; + + assert_eq!( + state + .best_chain() + .unwrap_or(&Arc::new(Chain::default())) + .blocks + .len(), + 3 + ); + + state.invalidate_block(block2.hash()); + + let post_invalidated_chain = state.best_chain().unwrap(); + + assert_eq!(post_invalidated_chain.blocks.len(), 1); + assert!( + post_invalidated_chain.contains_block_hash(block1.hash()), + "the new modified chain should contain block1" + ); + + assert!( + !post_invalidated_chain.contains_block_hash(block2.hash()), + "the new modified chain should not contain block2" + ); + assert!( + !post_invalidated_chain.contains_block_hash(block3.hash()), + "the new modified chain should not contain block3" + ); + + let invalidated_blocks_state = &state.invalidated_blocks; + assert!( + invalidated_blocks_state.contains_key(&block2.hash()), + "invalidated blocks map should reference the hash of block2" + ); + + let invalidated_blocks_state_descendants = + invalidated_blocks_state.get(&block2.hash()).unwrap(); + + match network { + Network::Mainnet => assert!( + invalidated_blocks_state_descendants + .iter() + .any(|block| block.height == block::Height(653601)), + "invalidated descendants vec should contain block3" + ), + Network::Testnet(_parameters) => assert!( + invalidated_blocks_state_descendants + .iter() + .any(|block| block.height == block::Height(584001)), + "invalidated descendants vec should contain block3" + ), + } + + Ok(()) +} + +#[test] +fn invalidate_block_removes_block_and_descendants_from_chain() -> Result<()> { + let _init_guard = zebra_test::init(); + + for network in Network::iter() { + invalidate_block_removes_block_and_descendants_from_chain_for_network(network)?; + } + + Ok(()) +} + #[test] // This test gives full coverage for `take_chain_if` fn commit_block_extending_best_chain_doesnt_drop_worst_chains() -> Result<()> {