Repository: ZcashFoundation/zebra
Pull request #1271 (2020-11-09 22:22:39)

Add internal iterator API for accessing relevant chain blocks
## Motivation

The difficulty calculation logic needs the ability to access the last 28 blocks of a block's relevant chain. Before this PR the state internals have had APIs for getting individual blocks but no easy way to abstract over "the last n blocks of a chain starting at this block".

## Solution

This PR adds a `chain` method to `StateService` in `zebra-state`. This function provides an iterator API that abstracts over the `StateService`. It will first attempt to find the given block from the non-finalized state, and if it doesn't exist there it falls back to the finalized state.

The code in this pull request has:
  - [x] Documentation Comments
  - [ ] Unit Tests and Property Tests

## Review

@teor2345

## Related Issues

## Follow Up Work




Patch:
diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml
index f36799bcf..e37bd8140 100644
--- a/.github/workflows/cd.yml
+++ b/.github/workflows/cd.yml
@@ -10,26 +10,25 @@ env:
   PROJECT_ID: zealous-zebra
 
 jobs:
+
   build:
-    name: Google Cloud / Test, Build, and Deploy
+    name: Build images
     timeout-minutes: 30
     runs-on: ubuntu-latest
     steps:
-
-    - name: Checkout
-      uses: actions/checkout@master
+    - uses: actions/checkout@v2
 
     - name: Set project and image names
       run: |
         BRANCH_NAME=$(expr $GITHUB_REF : '.*/\(.*\)') && \
         BRANCH_NAME=${BRANCH_NAME,,} && \
         REPOSITORY=${GITHUB_REPOSITORY,,} && \
-        echo "::set-env name=BRANCH_NAME::$BRANCH_NAME" && \
-        echo "::set-env name=SHORT_SHA::$(git rev-parse --short=7 $GITHUB_SHA)" && \
-        echo "::set-env name=REPOSITORY::$REPOSITORY"
+        echo "BRANCH_NAME=$BRANCH_NAME" >> $GITHUB_ENV && \
+        echo "SHORT_SHA=$(git rev-parse --short=7 $GITHUB_SHA)" >> $GITHUB_ENV && \
+        echo "REPOSITORY=$REPOSITORY" >> $GITHUB_ENV
 
     # Setup gcloud CLI
-    - name: Set up gcloud Cloud SDK environment
+    - name: Set up gcloud SDK environment
       uses: GoogleCloudPlatform/github-actions/setup-gcloud@master
       with:
         version: '295.0.0'
@@ -44,46 +43,135 @@ jobs:
           --config cloudbuild.yaml \
           --substitutions SHORT_SHA="$SHORT_SHA",BRANCH_NAME="$BRANCH_NAME"
 
-    # Create instance template from container image
-    - name: Create instance template
-      run: |
-        gcloud compute instance-templates create-with-container "zebrad-$BRANCH_NAME-$SHORT_SHA" \
+  test:
+    name: Run integration tests
+    needs: build
+    runs-on: ubuntu-latest
+    timeout-minutes: 30
+    steps:
+      - uses: actions/checkout@v2
+
+      - name: Set project and image names
+        run: |
+          BRANCH_NAME=$(expr $GITHUB_REF : '.*/\(.*\)') && \
+          BRANCH_NAME=${BRANCH_NAME,,} && \
+          REPOSITORY=${GITHUB_REPOSITORY,,} && \
+          echo "BRANCH_NAME=$BRANCH_NAME" >> $GITHUB_ENV && \
+          echo "SHORT_SHA=$(git rev-parse --short=7 $GITHUB_SHA)" >> $GITHUB_ENV && \
+          echo "REPOSITORY=$REPOSITORY" >> $GITHUB_ENV
+
+      # Setup gcloud CLI
+      - name: Set up gcloud SDK environment
+        uses: GoogleCloudPlatform/github-actions/setup-gcloud@master
+        with:
+          version: '295.0.0'
+          project_id: ${{ env.PROJECT_ID }}
+          service_account_key: ${{ secrets.GCLOUD_AUTH }}
+
+      # Run once: create firewall rule to allow incoming traffic to the nodes
+      # - name: Create Zcash incoming traffic firewall rule
+      #   run: |
+      #     gcloud compute firewall-rules create "allow-zcash" \
+      #     --target-tags zebrad \
+      #     --allow tcp:8233,tcp:18233 \
+      #     --source-ranges 0.0.0.0/0 \
+      #     --description="Allow incoming Zcash traffic from anywhere" \
+
+      # Creates Compute Engine virtual machine instance w/ test container and disks
+      - name: Create instance with test container image
+        run: |
+          gcloud compute instances create-with-container "zebra-tests-$BRANCH_NAME-$SHORT_SHA" \
+          --container-command='/zebra/target/debug/deps/acceptance' \
+          --container-arg='-Z' --container-arg='unstable-options' --container-arg='--include-ignored' \
+          --container-image "gcr.io/$PROJECT_ID/$REPOSITORY/tests/$BRANCH_NAME:$SHORT_SHA" \
+          --container-mount-disk mount-path='/zebrad-cache',name=zebrad-cache-$SHORT_SHA \
+          --container-restart-policy='never' \
+          --create-disk name=zebrad-cache-$SHORT_SHA,image=zebrad-cache-mainnet-height-1018849 \
+          --service-account cos-vm@zealous-zebra.iam.gserviceaccount.com \
+          --scopes cloud-platform \
+          --tags zebrad \
+          --zone us-central1-a
+
+      # Wait for acceptance test container is up and stream its results
+      - name: Run acceptance tests
+        run: |
+          gcloud compute ssh "zebra-tests-$BRANCH_NAME-$SHORT_SHA" --zone us-central1-a \
+          --command "echo 'Pulling container image...'; until [[ -n \$(docker ps --all -q --filter name=zebra-test) ]]; do sleep 0.1; done; docker attach \$(docker ps -q -a --filter name=zebra-test)"
+
+      # Clean up
+      - name: Delete test instance
+        # Always run even if the earlier step fails
+        if: ${{ always() }}
+        run: |
+          gcloud compute instances delete "zebra-tests-$BRANCH_NAME-$SHORT_SHA" --delete-disks all --zone us-central1-a
+
+  deploy:
+    name: Deploy mainnet nodes
+    needs: build
+    runs-on: ubuntu-latest
+    timeout-minutes: 30
+    steps:
+      - uses: actions/checkout@v2
+
+      - name: Set project and image names
+        run: |
+          BRANCH_NAME=$(expr $GITHUB_REF : '.*/\(.*\)') && \
+          BRANCH_NAME=${BRANCH_NAME,,} && \
+          REPOSITORY=${GITHUB_REPOSITORY,,} && \
+          echo "BRANCH_NAME=$BRANCH_NAME" >> $GITHUB_ENV && \
+          echo "SHORT_SHA=$(git rev-parse --short=7 $GITHUB_SHA)" >> $GITHUB_ENV && \
+          echo "REPOSITORY=$REPOSITORY" >> $GITHUB_ENV
+
+      # Setup gcloud CLI
+      - name: Set up gcloud SDK environment
+        uses: GoogleCloudPlatform/github-actions/setup-gcloud@master
+        with:
+          version: '295.0.0'
+          project_id: ${{ env.PROJECT_ID }}
+          service_account_key: ${{ secrets.GCLOUD_AUTH }}
+
+      # Run once: create firewall rule to allow incoming traffic to the nodes
+      # - name: Create Zcash incoming traffic firewall rule
+      #   run: |
+      #     gcloud compute firewall-rules create "allow-zcash" \
+      #     --target-tags zebrad \
+      #     --allow tcp:8233,tcp:18233 \
+      #     --source-ranges 0.0.0.0/0 \
+      #     --description="Allow incoming Zcash traffic from anywhere" \
+
+      # Create instance template from container image
+      - name: Create instance template
+        run: |
+          gcloud compute instance-templates create-with-container "zebrad-$BRANCH_NAME-$SHORT_SHA" \
           --container-image "gcr.io/$PROJECT_ID/$REPOSITORY/$BRANCH_NAME:$SHORT_SHA" \
-          --machine-type n1-highcpu-2 \
+          --create-disk name=zebrad-cache-$SHORT_SHA,size=100GB,type=pd-balanced,auto-delete=no \
+          --container-mount-disk mount-path="/zebrad-cache",name=zebrad-cache-$SHORT_SHA \
+          --machine-type n2-standard-4 \
           --service-account cos-vm@zealous-zebra.iam.gserviceaccount.com \
           --scopes cloud-platform \
           --tags zebrad \
 
-    # Run once: create firewall rule to allow healthchecks
-    # - name: Create healthcheck firewall rule
-    #   run: |
-    #     gcloud compute firewall-rules create "allow-tracing-health-check" \
-    #     --target-tags zebrad \
-    #     --allow tcp:3000 \
-    #     --source-ranges 130.211.0.0/22,35.191.0.0/16 \
-    #     --description="Allow HTTP requests to our tracing endpoint from Google's probes" \
-
-    # Run once: create firewall rule to allow incoming traffic to the nodes
-    # - name: Create Zcash incoming traffic firewall rule
-    #   run: |
-    #     gcloud compute firewall-rules create "allow-zcash" \
-    #     --target-tags zebrad \
-    #     --allow tcp:8233,tcp:18233 \
-    #     --source-ranges 0.0.0.0/0 \
-    #     --description="Allow incoming Zcash traffic from anywhere" \
-
-    # Check if our destination instance group exists already
-    - name: Check if instance group exists
-      id: does-group-exist
-      continue-on-error: true
-      run: |
-        gcloud compute instance-groups list | grep "zebrad-$BRANCH_NAME"
+      # Run once: create firewall rule to allow healthchecks
+      # - name: Create healthcheck firewall rule
+      #   run: |
+      #     gcloud compute firewall-rules create "allow-tracing-health-check" \
+      #     --target-tags zebrad \
+      #     --allow tcp:3000 \
+      #     --source-ranges 130.211.0.0/22,35.191.0.0/16 \
+      #     --description="Allow HTTP requests to our tracing endpoint from Google's probes" \
 
-    # Deploy new managed instance group using the new instance template
-    - name: Create managed instance group
-      if: steps.does-group-exist.outcome == 'failure'
-      run: |
-        gcloud compute instance-groups managed create \
+      # Check if our destination instance group exists already
+      - name: Check if instance group exists
+        id: does-group-exist
+        continue-on-error: true
+        run: |
+          gcloud compute instance-groups list | grep "zebrad-$BRANCH_NAME"
+
+      # Deploy new managed instance group using the new instance template
+      - name: Create managed instance group
+        if: steps.does-group-exist.outcome == 'failure'
+        run: |
+          gcloud compute instance-groups managed create \
           "zebrad-$BRANCH_NAME" \
           --template "zebrad-$BRANCH_NAME-$SHORT_SHA" \
           --health-check zebrad-tracing-filter \
@@ -91,11 +179,11 @@ jobs:
           --region us-central1 \
           --size 2
 
-    # Rolls out update to existing group using the new instance template
-    - name: Update managed instance group
-      if: steps.does-group-exist.outcome == 'success'
-      run: |
-        gcloud compute instance-groups managed rolling-action start-update \
+      # Rolls out update to existing group using the new instance template
+      - name: Update managed instance group
+        if: steps.does-group-exist.outcome == 'success'
+        run: |
+          gcloud compute instance-groups managed rolling-action start-update \
           "zebrad-$BRANCH_NAME" \
           --version template="zebrad-$BRANCH_NAME-$SHORT_SHA" \
           --region us-central1 \
diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml
index f31066b79..021831bd4 100644
--- a/.github/workflows/coverage.yml
+++ b/.github/workflows/coverage.yml
@@ -26,7 +26,7 @@ jobs:
       - name: Run cargo-tarpaulin
         uses: actions-rs/tarpaulin@v0.1
         with:
-          version: 'latest'
+          version: '0.16.0'
           timeout: 600
       - name: Upload coverage report to Codecov
         uses: codecov/codecov-action@v1.0.14
diff --git a/.github/workflows/manual-deploy.yml b/.github/workflows/manual-deploy.yml
new file mode 100644
index 000000000..fa33d64de
--- /dev/null
+++ b/.github/workflows/manual-deploy.yml
@@ -0,0 +1,65 @@
+name: Manual Deploy
+
+on:
+  workflow_dispatch:
+    inputs:
+      network:
+        default: 'Mainnet'
+      checkpoint_sync:
+        default: true
+
+env:
+  PROJECT_ID: zealous-zebra
+
+jobs:
+
+  deploy:
+    name: Deploy one zebrad node
+    runs-on: ubuntu-latest
+    steps:
+    - uses: actions/checkout@v2
+
+    - name: Set project and image names
+      run: |
+        BRANCH_NAME=$(expr $GITHUB_REF : '.*/\(.*\)') && \
+        BRANCH_NAME=${BRANCH_NAME,,} && \
+        REPOSITORY=${GITHUB_REPOSITORY,,} && \
+        echo "BRANCH_NAME=$BRANCH_NAME" >> $GITHUB_ENV && \
+        echo "SHORT_SHA=$(git rev-parse --short=7 $GITHUB_SHA)" >> $GITHUB_ENV && \
+        echo "REPOSITORY=$REPOSITORY" >> $GITHUB_ENV
+
+    - name: Set up gcloud
+      uses: GoogleCloudPlatform/github-actions/setup-gcloud@master
+      with:
+        version: '295.0.0'
+        project_id: ${{ env.PROJECT_ID }}
+        service_account_key: ${{ secrets.GCLOUD_AUTH }}
+
+    # Build and push image to Google Container Registry
+    - name: Build
+      # Tagging w/ the commit SHA blocks the :latest tag on GCR
+      run: |
+        gcloud builds submit \
+          --config cloudbuild.yaml \
+          --substitutions SHORT_SHA="$SHORT_SHA",BRANCH_NAME="$BRANCH_NAME",_CHECKPOINT_SYNC="${{ github.event.inputs.checkpoint_sync }}",_NETWORK="${{ github.event.inputs.network }}"
+
+    # Run once: create firewall rule to allow incoming traffic to the node
+    # - name: Create Zcash incoming traffic firewall rule
+    #   run: |
+    #     gcloud compute firewall-rules create "allow-zcash" \
+    #     --target-tags zebrad \
+    #     --allow tcp:8233,tcp:18233 \
+    #     --source-ranges 0.0.0.0/0 \
+    #     --description="Allow incoming Zcash traffic from anywhere" \
+
+    # Creates Compute Engine virtual machine instance w/ zebrad container and disks
+    - name: Create instance running zebrad container image
+      run: |
+        gcloud compute instances create-with-container "zebrad-$BRANCH_NAME-$SHORT_SHA" \
+        --container-image "gcr.io/$PROJECT_ID/$REPOSITORY/$BRANCH_NAME:$SHORT_SHA" \
+        --container-mount-disk mount-path='/zebrad-cache',name=zebrad-cache-$SHORT_SHA \
+        --create-disk name=zebrad-cache-$SHORT_SHA,size=100GB,type=pd-balanced,auto-delete=no \
+        --machine-type n2-standard-4 \
+        --service-account cos-vm@zealous-zebra.iam.gserviceaccount.com \
+        --tags zebrad \
+        --zone us-central1-a
diff --git a/Cargo.lock b/Cargo.lock
index 8a23efc4b..268023d64 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -38,15 +38,15 @@ dependencies = [
  "ident_case",
  "proc-macro2 1.0.24",
  "quote 1.0.7",
- "syn 1.0.46",
+ "syn 1.0.48",
  "synstructure",
 ]
 
 [[package]]
 name = "addr2line"
-version = "0.13.0"
+version = "0.14.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1b6a2d3371669ab3ca9797670853d61402b03d0b4b9ebf33d677dfa720203072"
+checksum = "7c0929d69e78dd9bf5408269919fcbcaeb2e35e5d43e5815517cdc6a8e11a423"
 dependencies = [
  "gimli",
 ]
@@ -68,9 +68,9 @@ dependencies = [
 
 [[package]]
 name = "aho-corasick"
-version = "0.7.14"
+version = "0.7.15"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b476ce7103678b0c6d3d395dbbae31d48ff910bd28be979ba5d48c6351131d0d"
+checksum = "7404febffaa47dac81aa44dba71523c9d069b1bdc50a77db41195149e17f68e5"
 dependencies = [
  "memchr",
 ]
@@ -116,9 +116,9 @@ dependencies = [
 
 [[package]]
 name = "arrayvec"
-version = "0.5.1"
+version = "0.5.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cff77d8686867eceff3105329d4698d96c2391c176d5d03adc90c7389162b5b8"
+checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b"
 
 [[package]]
 name = "atomic-shim"
@@ -148,9 +148,9 @@ checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a"
 
 [[package]]
 name = "backtrace"
-version = "0.3.53"
+version = "0.3.54"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "707b586e0e2f247cbde68cdd2c3ce69ea7b7be43e1c5b426e37c9319c4b9838e"
+checksum = "2baad346b2d4e94a24347adeee9c7a93f412ee94b9cc26e5b59dea23848e9f28"
 dependencies = [
  "addr2line",
  "cfg-if 1.0.0",
@@ -273,7 +273,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "afa748e348ad3be8263be728124b24a24f268266f6f5d58af9d75f6a40b5c587"
 dependencies = [
  "arrayref",
- "arrayvec 0.5.1",
+ "arrayvec 0.5.2",
  "constant_time_eq",
 ]
 
@@ -284,7 +284,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9e461a7034e85b211a4acb57ee2e6730b32912b06c08cc242243c39fc21ae6a2"
 dependencies = [
  "arrayref",
- "arrayvec 0.5.1",
+ "arrayvec 0.5.2",
  "constant_time_eq",
 ]
 
@@ -532,9 +532,9 @@ dependencies = [
 
 [[package]]
 name = "const_fn"
-version = "0.4.2"
+version = "0.4.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ce90df4c658c62f12d78f7508cf92f9173e5184a539c10bfe54a3107b3ffd0f2"
+checksum = "c478836e029dcef17fb47c89023448c64f781a046e0300e257ad8225ae59afab"
 
 [[package]]
 name = "constant_time_eq"
@@ -708,7 +708,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "7fbaabec2c953050352311293be5c6aba8e141ba19d6811862b232d6fd020484"
 dependencies = [
  "quote 1.0.7",
- "syn 1.0.46",
+ "syn 1.0.48",
 ]
 
 [[package]]
@@ -746,7 +746,7 @@ dependencies = [
  "proc-macro2 1.0.24",
  "quote 1.0.7",
  "strsim 0.9.3",
- "syn 1.0.46",
+ "syn 1.0.48",
 ]
 
 [[package]]
@@ -757,7 +757,7 @@ checksum = "d9b5a2f4ac4969822c62224815d069952656cadc7084fdca9751e6d959189b72"
 dependencies = [
  "darling_core",
  "quote 1.0.7",
- "syn 1.0.46",
+ "syn 1.0.48",
 ]
 
 [[package]]
@@ -812,7 +812,7 @@ checksum = "adc2ab4d5a16117f9029e9a6b5e4e79f4c67f6519bc134210d4d4a04ba31f41b"
 dependencies = [
  "proc-macro2 1.0.24",
  "quote 1.0.7",
- "syn 1.0.46",
+ "syn 1.0.48",
 ]
 
 [[package]]
@@ -880,9 +880,9 @@ dependencies = [
 
 [[package]]
 name = "eyre"
-version = "0.6.1"
+version = "0.6.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f4c5cb4dc433c59f09df4b4450f649cbfed61e8a3505abe32e4154066439157e"
+checksum = "5f29abf4740a4778632fe27a4f681ef5b7a6f659aeba3330ac66f48e20cfa3b7"
 dependencies = [
  "indenter",
  "once_cell",
@@ -913,11 +913,11 @@ dependencies = [
 
 [[package]]
 name = "flate2"
-version = "1.0.18"
+version = "1.0.19"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "da80be589a72651dcda34d8b35bcdc9b7254ad06325611074d9cc0fbb19f60ee"
+checksum = "7411863d55df97a419aa64cb4d2f167103ea9d767e2c54a1868b7ac3f6b47129"
 dependencies = [
- "cfg-if 0.1.10",
+ "cfg-if 1.0.0",
  "crc32fast",
  "libc",
  "miniz_oxide",
@@ -929,6 +929,16 @@ version = "1.0.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
 
+[[package]]
+name = "form_urlencoded"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ece68d15c92e84fa4f19d3780f1294e5ca82a78a6d515f1efaabcc144688be00"
+dependencies = [
+ "matches",
+ "percent-encoding",
+]
+
 [[package]]
 name = "fs2"
 version = "0.4.3"
@@ -969,9 +979,9 @@ checksum = "0ba62103ce691c2fd80fbae2213dfdda9ce60804973ac6b6e97de818ea7f52c8"
 
 [[package]]
 name = "futures"
-version = "0.3.7"
+version = "0.3.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "95314d38584ffbfda215621d723e0a3906f032e03ae5551e650058dac83d4797"
+checksum = "9b3b0c040a1fe6529d30b3c5944b280c7f0dcb2930d2c3062bca967b602583d0"
 dependencies = [
  "futures-channel",
  "futures-core",
@@ -984,9 +994,9 @@ dependencies = [
 
 [[package]]
 name = "futures-channel"
-version = "0.3.7"
+version = "0.3.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0448174b01148032eed37ac4aed28963aaaa8cfa93569a08e5b479bbc6c2c151"
+checksum = "4b7109687aa4e177ef6fe84553af6280ef2778bdb7783ba44c9dc3399110fe64"
 dependencies = [
  "futures-core",
  "futures-sink",
@@ -994,15 +1004,15 @@ dependencies = [
 
 [[package]]
 name = "futures-core"
-version = "0.3.7"
+version = "0.3.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "18eaa56102984bed2c88ea39026cff3ce3b4c7f508ca970cedf2450ea10d4e46"
+checksum = "847ce131b72ffb13b6109a221da9ad97a64cbe48feb1028356b836b47b8f1748"
 
 [[package]]
 name = "futures-executor"
-version = "0.3.7"
+version = "0.3.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f5f8e0c9258abaea85e78ebdda17ef9666d390e987f006be6080dfe354b708cb"
+checksum = "4caa2b2b68b880003057c1dd49f1ed937e38f22fcf6c212188a121f08cf40a65"
 dependencies = [
  "futures-core",
  "futures-task",
@@ -1011,42 +1021,42 @@ dependencies = [
 
 [[package]]
 name = "futures-io"
-version = "0.3.7"
+version = "0.3.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6e1798854a4727ff944a7b12aa999f58ce7aa81db80d2dfaaf2ba06f065ddd2b"
+checksum = "611834ce18aaa1bd13c4b374f5d653e1027cf99b6b502584ff8c9a64413b30bb"
 
 [[package]]
 name = "futures-macro"
-version = "0.3.7"
+version = "0.3.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e36fccf3fc58563b4a14d265027c627c3b665d7fed489427e88e7cc929559efe"
+checksum = "77408a692f1f97bcc61dc001d752e00643408fbc922e4d634c655df50d595556"
 dependencies = [
  "proc-macro-hack",
  "proc-macro2 1.0.24",
  "quote 1.0.7",
- "syn 1.0.46",
+ "syn 1.0.48",
 ]
 
 [[package]]
 name = "futures-sink"
-version = "0.3.7"
+version = "0.3.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0e3ca3f17d6e8804ae5d3df7a7d35b2b3a6fe89dac84b31872720fc3060a0b11"
+checksum = "f878195a49cee50e006b02b93cf7e0a95a38ac7b776b4c4d9cc1207cd20fcb3d"
 
 [[package]]
 name = "futures-task"
-version = "0.3.7"
+version = "0.3.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "96d502af37186c4fef99453df03e374683f8a1eec9dcc1e66b3b82dc8278ce3c"
+checksum = "7c554eb5bf48b2426c4771ab68c6b14468b6e76cc90996f528c3338d761a4d0d"
 dependencies = [
  "once_cell",
 ]
 
 [[package]]
 name = "futures-util"
-version = "0.3.7"
+version = "0.3.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "abcb44342f62e6f3e8ac427b8aa815f724fd705dfad060b18ac7866c15bb8e34"
+checksum = "d304cff4a7b99cfb7986f7d43fbe93d175e72e704a8860787cc95e9ffd85cbd2"
 dependencies = [
  "futures-channel",
  "futures-core",
@@ -1136,9 +1146,9 @@ dependencies = [
 
 [[package]]
 name = "gimli"
-version = "0.22.0"
+version = "0.23.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "aaf91faf136cb47367fa430cd46e37a788775e7fa104f8b4bcb3861dc389b724"
+checksum = "f6503fe142514ca4799d4c26297c4248239fe8838d827db6bd6065c6ed29a6ce"
 
 [[package]]
 name = "glob"
@@ -1175,14 +1185,14 @@ checksum = "90454ce4de40b7ca6a8968b5ef367bdab48413962588d0d2b1638d60090c35d7"
 dependencies = [
  "proc-macro2 1.0.24",
  "quote 1.0.7",
- "syn 1.0.46",
+ "syn 1.0.48",
 ]
 
 [[package]]
 name = "h2"
-version = "0.2.6"
+version = "0.2.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "993f9e0baeed60001cf565546b0d3dbe6a6ad23f2bd31644a133c641eccf6d53"
+checksum = "5e4728fd124914ad25e99e3d15a9361a879f6620f63cb56bbb08f95abb97a535"
 dependencies = [
  "bytes",
  "fnv",
@@ -1195,6 +1205,7 @@ dependencies = [
  "tokio",
  "tokio-util 0.3.1",
  "tracing",
+ "tracing-futures",
 ]
 
 [[package]]
@@ -1381,11 +1392,11 @@ dependencies = [
 
 [[package]]
 name = "instant"
-version = "0.1.7"
+version = "0.1.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "63312a18f7ea8760cdd0a7c5aac1a619752a246b833545e3e36d1f81f7cd9e66"
+checksum = "cb1fc4429a33e1f80d41dc9fea4d108a88bec1de8053878898ae448a0b52f613"
 dependencies = [
- "cfg-if 0.1.10",
+ "cfg-if 1.0.0",
 ]
 
 [[package]]
@@ -1451,17 +1462,17 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
 
 [[package]]
 name = "libc"
-version = "0.2.79"
+version = "0.2.80"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2448f6066e80e3bfc792e9c98bf705b4b0fc6e8ef5b43e5889aff0eaa9c58743"
+checksum = "4d58d1b70b004888f764dfbf6a26a3b0342a1632d33968e4a179d8011c760614"
 
 [[package]]
 name = "libloading"
-version = "0.6.4"
+version = "0.6.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3557c9384f7f757f6d139cd3a4c62ef4e850696c16bf27924a5538c8a09717a1"
+checksum = "1090080fe06ec2648d0da3881d9453d97e71a45f00eb179af7fdd7e3f686fdb0"
 dependencies = [
- "cfg-if 0.1.10",
+ "cfg-if 1.0.0",
  "winapi 0.3.9",
 ]
 
@@ -1534,9 +1545,9 @@ checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00"
 
 [[package]]
 name = "memchr"
-version = "2.3.3"
+version = "2.3.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3728d817d99e5ac407411fa471ff9800a778d88a24685968b36824eaf4bee400"
+checksum = "0ee1c47aaa256ecabcaea351eae4a9b01ef39ed810004e298d2511ed284b1525"
 
 [[package]]
 name = "memoffset"
@@ -1774,9 +1785,9 @@ dependencies = [
 
 [[package]]
 name = "num-integer"
-version = "0.1.43"
+version = "0.1.44"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8d59457e662d541ba17869cf51cf177c0b5f0cbf476c66bdc90bf1edac4f875b"
+checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db"
 dependencies = [
  "autocfg",
  "num-traits",
@@ -1784,9 +1795,9 @@ dependencies = [
 
 [[package]]
 name = "num-traits"
-version = "0.2.12"
+version = "0.2.14"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ac267bcc07f48ee5f8935ab0d24f316fb722d7a1292e2913f0cc196b29ffd611"
+checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290"
 dependencies = [
  "autocfg",
 ]
@@ -1803,15 +1814,15 @@ dependencies = [
 
 [[package]]
 name = "object"
-version = "0.21.1"
+version = "0.22.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "37fd5004feb2ce328a52b0b3d01dbf4ffff72583493900ed15f22d4111c51693"
+checksum = "8d3b63360ec3cb337817c2dbd47ab4a0f170d285d8e5a2064600f3def1402397"
 
 [[package]]
 name = "once_cell"
-version = "1.4.1"
+version = "1.5.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "260e51e7efe62b592207e9e13a68e43692a7a279171d6ba57abd208bf23645ad"
+checksum = "13bd41f508810a131401606d54ac32a467c97172d74ba7662562ebba5ad07fa0"
 
 [[package]]
 name = "opaque-debug"
@@ -1855,7 +1866,7 @@ version = "1.3.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "7c740e5fbcb6847058b40ac7e5574766c6388f585e184d769910fe0d3a2ca861"
 dependencies = [
- "arrayvec 0.5.1",
+ "arrayvec 0.5.2",
  "bitvec 0.17.4",
  "byte-slice-cast",
  "serde",
@@ -1949,7 +1960,7 @@ checksum = "65ad2ae56b6abe3a1ee25f15ee605bacadb9a764edaba9c2bf4103800d4a1895"
 dependencies = [
  "proc-macro2 1.0.24",
  "quote 1.0.7",
- "syn 1.0.46",
+ "syn 1.0.48",
 ]
 
 [[package]]
@@ -1960,7 +1971,7 @@ checksum = "81a4ffa594b66bff340084d4081df649a7dc049ac8d7fc458d8e628bfbbb2f86"
 dependencies = [
  "proc-macro2 1.0.24",
  "quote 1.0.7",
- "syn 1.0.46",
+ "syn 1.0.48",
 ]
 
 [[package]]
@@ -1977,9 +1988,9 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
 
 [[package]]
 name = "ppv-lite86"
-version = "0.2.9"
+version = "0.2.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c36fa947111f5c62a733b652544dd0016a43ce89619538a8ef92724a6f501a20"
+checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857"
 
 [[package]]
 name = "pretty_assertions"
@@ -1995,9 +2006,9 @@ dependencies = [
 
 [[package]]
 name = "primitive-types"
-version = "0.7.2"
+version = "0.7.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c55c21c64d0eaa4d7ed885d959ef2d62d9e488c27c0e02d9aa5ce6c877b7d5f8"
+checksum = "7dd39dcacf71411ba488570da7bbc89b717225e46478b30ba99b92db6b149809"
 dependencies = [
  "fixed-hash",
  "impl-codec",
@@ -2013,7 +2024,7 @@ dependencies = [
  "proc-macro-error-attr",
  "proc-macro2 1.0.24",
  "quote 1.0.7",
- "syn 1.0.46",
+ "syn 1.0.48",
  "version_check 0.9.2",
 ]
 
@@ -2030,9 +2041,9 @@ dependencies = [
 
 [[package]]
 name = "proc-macro-hack"
-version = "0.5.18"
+version = "0.5.19"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "99c605b9a0adc77b7211c6b1f722dcb613d68d66859a44f3d485a6da332b0598"
+checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5"
 
 [[package]]
 name = "proc-macro-nested"
@@ -2497,7 +2508,7 @@ checksum = "cbd1ae72adb44aab48f325a02444a5fc079349a8d804c1fc922aed3f7454c74e"
 dependencies = [
  "proc-macro2 1.0.24",
  "quote 1.0.7",
- "syn 1.0.46",
+ "syn 1.0.48",
 ]
 
 [[package]]
@@ -2513,9 +2524,9 @@ dependencies = [
 
 [[package]]
 name = "serde_yaml"
-version = "0.8.13"
+version = "0.8.14"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ae3e2dd40a7cdc18ca80db804b7f461a39bb721160a85c9a1fa30134bf3c02a5"
+checksum = "f7baae0a99f1a324984bcdc5f0718384c1f69775f1c7eec8b859b71b443e3fd7"
 dependencies = [
  "dtoa",
  "linked-hash-map",
@@ -2564,11 +2575,10 @@ dependencies = [
 
 [[package]]
 name = "signal-hook-registry"
-version = "1.2.1"
+version = "1.2.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a3e12110bc539e657a646068aaf5eb5b63af9d0c1f7b29c97113fad80e15f035"
+checksum = "ce32ea0c6c56d5eacaeb814fbed9960547021d3edd010ded1425f180536b20ab"
 dependencies = [
- "arc-swap",
  "libc",
 ]
 
@@ -2590,13 +2600,13 @@ checksum = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8"
 
 [[package]]
 name = "sled"
-version = "0.34.4"
+version = "0.34.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f72c064e63fbca3138ad07f3588c58093f1684f3a99f60dcfa6d46b87e60fde7"
+checksum = "34a9981af2a13b88f562777db36ed6e8695eef9be8bbbfb1c8b1c83762b8c15c"
 dependencies = [
  "crc32fast",
- "crossbeam-epoch 0.8.2",
- "crossbeam-utils 0.7.2",
+ "crossbeam-epoch 0.9.0",
+ "crossbeam-utils 0.8.0",
  "fs2",
  "fxhash",
  "libc",
@@ -2621,9 +2631,9 @@ checksum = "fbee7696b84bbf3d89a1c2eccff0850e3047ed46bfcd2e92c29a2d074d57e252"
 
 [[package]]
 name = "socket2"
-version = "0.3.15"
+version = "0.3.16"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b1fa70dc5c8104ec096f4fe7ede7a221d35ae13dcd19ba1ad9a81d2cab9a1c44"
+checksum = "7fd8b795c389288baa5f355489c65e71fd48a02104600d15c4cfbc561e9e429d"
 dependencies = [
  "cfg-if 0.1.10",
  "libc",
@@ -2633,9 +2643,9 @@ dependencies = [
 
 [[package]]
 name = "spandoc"
-version = "0.2.0"
+version = "0.2.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2c287699b5fa467d5286a2be14bff499f684133c223cf79c46c6251d5920badb"
+checksum = "8c026ab1a725f8e3ee7ca4480ce5c88a67778815e0cdf5d190021565a2426b5b"
 dependencies = [
  "spandoc-attribute",
  "tracing",
@@ -2644,13 +2654,13 @@ dependencies = [
 
 [[package]]
 name = "spandoc-attribute"
-version = "0.1.0"
+version = "0.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5254766110c377a921c002ca0775d4e384ba69af951fc4329d9dd77af2c25763"
+checksum = "5bdfb59103e43a0f99a346b57860d50f2138a7008d08acd964e9ac0fef3ae9a5"
 dependencies = [
  "proc-macro2 1.0.24",
  "quote 1.0.7",
- "syn 1.0.46",
+ "syn 1.0.48",
 ]
 
 [[package]]
@@ -2704,7 +2714,7 @@ dependencies = [
  "proc-macro-error",
  "proc-macro2 1.0.24",
  "quote 1.0.7",
- "syn 1.0.46",
+ "syn 1.0.48",
 ]
 
 [[package]]
@@ -2726,9 +2736,9 @@ dependencies = [
 
 [[package]]
 name = "syn"
-version = "1.0.46"
+version = "1.0.48"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5ad5de3220ea04da322618ded2c42233d02baca219d6f160a3e9c87cda16c942"
+checksum = "cc371affeffc477f42a221a1e4297aedcea33d47d19b61455588bd9d8f6b19ac"
 dependencies = [
  "proc-macro2 1.0.24",
  "quote 1.0.7",
@@ -2743,7 +2753,7 @@ checksum = "b834f2d66f734cb897113e34aaff2f1ab4719ca946f9a7358dba8f8064148701"
 dependencies = [
  "proc-macro2 1.0.24",
  "quote 1.0.7",
- "syn 1.0.46",
+ "syn 1.0.48",
  "unicode-xid 0.2.1",
 ]
 
@@ -2806,7 +2816,7 @@ checksum = "9ba20f23e85b10754cd195504aebf6a27e2e6cbe28c17778a0c930724628dd56"
 dependencies = [
  "proc-macro2 1.0.24",
  "quote 1.0.7",
- "syn 1.0.46",
+ "syn 1.0.48",
 ]
 
 [[package]]
@@ -2831,9 +2841,18 @@ dependencies = [
 
 [[package]]
 name = "tinyvec"
-version = "0.3.4"
+version = "1.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "238ce071d267c5710f9d31451efec16c5ee22de34df17cc05e56cbc92e967117"
+checksum = "b78a366903f506d2ad52ca8dc552102ffdd3e937ba8a227f024dc1d1eae28575"
+dependencies = [
+ "tinyvec_macros",
+]
+
+[[package]]
+name = "tinyvec_macros"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
 
 [[package]]
 name = "tokio"
@@ -2862,13 +2881,13 @@ dependencies = [
 
 [[package]]
 name = "tokio-macros"
-version = "0.2.5"
+version = "0.2.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f0c3acc6aa564495a0f2e1d59fab677cd7f81a19994cfc7f3ad0e64301560389"
+checksum = "e44da00bfc73a25f814cd8d7e57a68a5c31b74b3152a0a1d1f590c97ed06265a"
 dependencies = [
  "proc-macro2 1.0.24",
  "quote 1.0.7",
- "syn 1.0.46",
+ "syn 1.0.48",
 ]
 
 [[package]]
@@ -2997,7 +3016,7 @@ checksum = "80e0ccfc3378da0cce270c946b676a376943f5cd16aeba64568e7939806f4ada"
 dependencies = [
  "proc-macro2 1.0.24",
  "quote 1.0.7",
- "syn 1.0.46",
+ "syn 1.0.48",
 ]
 
 [[package]]
@@ -3135,9 +3154,9 @@ dependencies = [
 
 [[package]]
 name = "unicode-normalization"
-version = "0.1.13"
+version = "0.1.14"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6fb19cf769fa8c6a80a162df694621ebeb4dafb606470b2b2fce0be40a98a977"
+checksum = "b7f98e67a4d84f730d343392f9bfff7d21e3fca562b9cb7a43b768350beeddc6"
 dependencies = [
  "tinyvec",
 ]
@@ -3168,10 +3187,11 @@ checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564"
 
 [[package]]
 name = "url"
-version = "2.1.1"
+version = "2.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "829d4a8476c35c9bf0bbce5a3b23f4106f79728039b726d292bb93bc106787cb"
+checksum = "5909f2b0817350449ed73e8bcd81c8c3c8d9a7a5d8acba4b27db277f1868976e"
 dependencies = [
+ "form_urlencoded",
  "idna",
  "matches",
  "percent-encoding",
@@ -3458,6 +3478,7 @@ dependencies = [
  "lazy_static",
  "metrics",
  "once_cell",
+ "primitive-types",
  "proptest",
  "serde",
  "sled",
@@ -3561,6 +3582,6 @@ checksum = "c3f369ddb18862aba61aa49bf31e74d29f0f162dec753063200e1dc084345d16"
 dependencies = [
  "proc-macro2 1.0.24",
  "quote 1.0.7",
- "syn 1.0.46",
+ "syn 1.0.48",
  "synstructure",
 ]
diff --git a/Dockerfile b/Dockerfile
index 102c692e0..12946c086 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,3 +1,4 @@
+# Builder image
 FROM rust:buster as builder
 
 RUN apt-get update && \
@@ -16,10 +17,37 @@ COPY . .
 RUN cargo fetch --verbose
 COPY . .
 RUN rustc -V; cargo -V; rustup -V; cargo test --all && cargo build --release
+RUN find /zebra/target/debug/deps -type f -perm 755 ! -name '*.dylib' ! -name '*.so' | sed -e 'p;s/-.*//' | xargs -n2 mv
 
 
-FROM debian:buster-slim
+# Test binaries image
+FROM debian:buster AS zebra-tests
+
+RUN mkdir /zebra
+WORKDIR /zebra
+COPY --from=builder /zebra/target/debug/zebrad /zebra/target/debug/zebrad
+COPY --from=builder /zebra/target/debug/deps/[a-z0-9_]* /zebra/target/debug/deps/
+EXPOSE 8233 18233
+
+
+# Runner image
+FROM debian:buster-slim AS zebrad-release
+
 COPY --from=builder /zebra/target/release/zebrad /
-RUN echo "[tracing]\nendpoint_addr = '0.0.0.0:3000'" > /zebrad.toml
+
+ARG CHECKPOINT_SYNC=true
+ARG NETWORK=Mainnet
+
+RUN printf "[consensus]\n" >> /zebrad.toml
+RUN printf "checkpoint_sync = ${CHECKPOINT_SYNC}\n" >> /zebrad.toml
+RUN printf "[network]\n" >> /zebrad.toml
+RUN printf "network = '${NETWORK}'\n" >> /zebrad.toml
+RUN printf "[state]\n" >> /zebrad.toml
+RUN printf "cache_dir = '/zebrad-cache'\n" >> /zebrad.toml
+RUN printf "memory_cache_bytes = 52428800\n" >> /zebrad.toml
+RUN printf "[tracing]\n" >> /zebrad.toml
+RUN printf "endpoint_addr = '0.0.0.0:3000'\n" >> /zebrad.toml
+
 EXPOSE 3000 8233 18233
+
 CMD [ "/zebrad", "-c", "/zebrad.toml", "start" ]
diff --git a/book/src/dev/rfcs/0000-template.md b/book/src/dev/rfcs/0000-template.md
index b24ffe3bd..686dbe949 100644
--- a/book/src/dev/rfcs/0000-template.md
+++ b/book/src/dev/rfcs/0000-template.md
@@ -43,10 +43,12 @@ This is the technical portion of the RFC. Explain the design in sufficient detai
 The section should return to the examples given in the previous section, and explain more fully how the detailed proposal makes those examples work.
 
 ## Module Structure
+[module-structure]: #module-structure
 
 Describe the crate and modules that will implement the feature.
 
 ## Test Plan
+[test-plan]: #test-plan
 
 Explain how the feature will be tested, including:
 * tests for consensus-critical functionality
diff --git a/book/src/dev/rfcs/0005-state-updates.md b/book/src/dev/rfcs/0005-state-updates.md
index e2842f116..3d634ab33 100644
--- a/book/src/dev/rfcs/0005-state-updates.md
+++ b/book/src/dev/rfcs/0005-state-updates.md
@@ -124,11 +124,12 @@ pub enum Request {
 ```
 
 `zebra-state` breaks down its requests into two categories and provides
-different guarantees for each category: requests that modify the state, and requests that
-do not. Requests that update the state are guaranteed to run sequentially and
-will never race against each other. Requests that read state are done
-asynchronously and are guaranteed to read at least the state present at the
-time the request was processed by the service, or a later state present at the time the request future is executed. The state service avoids
+different guarantees for each category: requests that modify the state, and
+requests that do not. Requests that update the state are guaranteed to run
+sequentially and will never race against each other. Requests that read state
+are done asynchronously and are guaranteed to read at least the state present
+at the time the request was processed by the service, or a later state
+present at the time the request future is executed. The state service avoids
 race conditions between the read state and the written state by doing all
 contextual verification internally.
 
@@ -231,7 +232,7 @@ time the request was processed, or a later state.
 At a high level, the in-memory data structures store a collection of chains,
 each rooted at the highest finalized block. Each chain consists of a map from
 heights to blocks. Chains are stored using an ordered map from cumulative work to
-chains, so that the map ordering is the ordering of best to worst chains.
+chains, so that the map ordering is the ordering of worst to best chains.
 
 ### The `Chain` type
 [chain-type]: #chain-type
@@ -343,10 +344,10 @@ Remove the highest height block of the non-finalized portion of a chain.
 
 #### `Ord`
 
-The `Chain` type implements `Ord` for reorganizing chains. First chains
-are compared by their `partial_cumulative_work`. Ties are then broken by
-comparing `block::Hash`es of the tips of each chain. (This tie-breaker
-means that all `Chain`s in the `ChainSet` must have at least one block.)
+The `Chain` type implements `Ord` for reorganizing chains. First chains are
+compared by their `partial_cumulative_work`. Ties are then broken by
+comparing `block::Hash`es of the tips of each chain. (This tie-breaker means
+that all `Chain`s in the `NonFinalizedState` must have at least one block.)
 
 **Note**: Unlike `zcashd`, Zebra does not use block arrival times as a
 tie-breaker for the best tip. Since Zebra downloads blocks in parallel,
@@ -403,11 +404,11 @@ chain and updates all side chains to match.
 3. Remove the lowest height block from the best chain with
    `let finalized_block = best_chain.pop_root();`
 
-4. Add `best_chain` back to `self.chain_set`
+4. Add `best_chain` back to `self.chain_set` if `best_chain` is not empty
 
 5. For each remaining `chain` in `side_chains`
     - remove the lowest height block from `chain`
-    - If that block is equal to `finalized_block` add `chain` back to `self.chain_set`
+    - If that block is equal to `finalized_block` and `chain` is not empty add `chain` back to `self.chain_set`
     - Else, drop `chain`
 
 6. Return `finalized_block`
@@ -617,7 +618,7 @@ Zcash structures are encoded using `ZcashSerialize`/`ZcashDeserialize`.
 - The `hash_by_height` and `height_by_hash` trees provide a bijection between
   block heights and block hashes.  (Since the Sled state only stores finalized
   state, they are actually a bijection).
-  
+
 - The `block_by_height` tree provides a bijection between block heights and block
   data. There is no corresponding `height_by_block` tree: instead, hash the block,
   and use `height_by_hash`. (Since the Sled state only stores finalized state,
@@ -668,7 +669,7 @@ check that `block`'s parent hash is `null` (all zeroes) and its height is `0`.
 
     (Due to a [bug in zcashd](https://github.com/ZcashFoundation/zebra/issues/559),
     genesis block anchors and transactions are ignored during validation.)
-    
+
 4.  Update the `sprout_anchors` and `sapling_anchors` trees with the Sprout and
     Sapling anchors.
 
diff --git a/cloudbuild.yaml b/cloudbuild.yaml
index e67548872..b96b0c95e 100644
--- a/cloudbuild.yaml
+++ b/cloudbuild.yaml
@@ -1,10 +1,29 @@
 steps:
 - name: 'gcr.io/cloud-builders/docker'
-  args: ['build', '-t', 'gcr.io/$PROJECT_ID/zcashfoundation/zebra/$BRANCH_NAME:$SHORT_SHA', '.']
+  args: ['build',
+         '--target',
+         'zebra-tests',
+         '-t',
+         'gcr.io/$PROJECT_ID/zcashfoundation/zebra/tests/$BRANCH_NAME:$SHORT_SHA',
+         '.']
+- name: 'gcr.io/cloud-builders/docker'
+  args: ['build',
+         '--build-arg',
+         'checkpoint_sync=${_CHECKPOINT_SYNC}',
+         '--build-arg',
+         'network=${_NETWORK}',
+         '--target',
+         'zebrad-release',
+         '-t',
+         'gcr.io/$PROJECT_ID/zcashfoundation/zebra/$BRANCH_NAME:$SHORT_SHA',
+         '.']
 
-images: ['gcr.io/$PROJECT_ID/zcashfoundation/zebra/$BRANCH_NAME:$SHORT_SHA']
+images:
+- 'gcr.io/$PROJECT_ID/zcashfoundation/zebra/tests/$BRANCH_NAME:$SHORT_SHA'
+- 'gcr.io/$PROJECT_ID/zcashfoundation/zebra/$BRANCH_NAME:$SHORT_SHA'
 
 options:
     machineType: 'N1_HIGHCPU_32'
+    substitution_option: 'ALLOW_LOOSE'
 
 timeout: 1800s # 30 mins
diff --git a/zebra-chain/Cargo.toml b/zebra-chain/Cargo.toml
index 93efc7bc9..549538583 100644
--- a/zebra-chain/Cargo.toml
+++ b/zebra-chain/Cargo.toml
@@ -24,7 +24,7 @@ futures = "0.3"
 hex = "0.4"
 jubjub = "0.5.1"
 lazy_static = "1.4.0"
-primitive-types = "0.7.2"
+primitive-types = "0.7.3"
 rand_core = "0.5.1"
 ripemd160 = "0.8.0"
 secp256k1 = { version = "0.19.0", features = ["serde"] }
diff --git a/zebra-chain/src/amount.rs b/zebra-chain/src/amount.rs
index de644a46a..81a30b6fe 100644
--- a/zebra-chain/src/amount.rs
+++ b/zebra-chain/src/amount.rs
@@ -403,6 +403,7 @@ mod test {
     #[test]
     fn test_add_bare() -> Result<()> {
         zebra_test::init();
+
         let one: Amount = 1.try_into()?;
         let neg_one: Amount = (-1).try_into()?;
 
@@ -417,6 +418,7 @@ mod test {
     #[test]
     fn test_add_opt_lhs() -> Result<()> {
         zebra_test::init();
+
         let one: Amount = 1.try_into()?;
         let one = Ok(one);
         let neg_one: Amount = (-1).try_into()?;
@@ -432,6 +434,7 @@ mod test {
     #[test]
     fn test_add_opt_rhs() -> Result<()> {
         zebra_test::init();
+
         let one: Amount = 1.try_into()?;
         let neg_one: Amount = (-1).try_into()?;
         let neg_one = Ok(neg_one);
@@ -447,6 +450,7 @@ mod test {
     #[test]
     fn test_add_opt_both() -> Result<()> {
         zebra_test::init();
+
         let one: Amount = 1.try_into()?;
         let one = Ok(one);
         let neg_one: Amount = (-1).try_into()?;
@@ -463,6 +467,7 @@ mod test {
     #[test]
     fn test_add_assign() -> Result<()> {
         zebra_test::init();
+
         let one: Amount = 1.try_into()?;
         let neg_one: Amount = (-1).try_into()?;
         let mut neg_one = Ok(neg_one);
@@ -479,6 +484,7 @@ mod test {
     #[test]
     fn test_sub_bare() -> Result<()> {
         zebra_test::init();
+
         let one: Amount = 1.try_into()?;
         let zero: Amount = 0.try_into()?;
 
@@ -493,6 +499,7 @@ mod test {
     #[test]
     fn test_sub_opt_lhs() -> Result<()> {
         zebra_test::init();
+
         let one: Amount = 1.try_into()?;
         let one = Ok(one);
         let zero: Amount = 0.try_into()?;
@@ -508,6 +515,7 @@ mod test {
     #[test]
     fn test_sub_opt_rhs() -> Result<()> {
         zebra_test::init();
+
         let one: Amount = 1.try_into()?;
         let zero: Amount = 0.try_into()?;
         let zero = Ok(zero);
@@ -523,6 +531,7 @@ mod test {
     #[test]
     fn test_sub_assign() -> Result<()> {
         zebra_test::init();
+
         let one: Amount = 1.try_into()?;
         let zero: Amount = 0.try_into()?;
         let mut zero = Ok(zero);
@@ -538,6 +547,8 @@ mod test {
 
     #[test]
     fn add_with_diff_constraints() -> Result<()> {
+        zebra_test::init();
+
         let one = Amount::<NonNegative>::try_from(1)?;
         let zero = Amount::<NegativeAllowed>::try_from(0)?;
 
@@ -549,6 +560,8 @@ mod test {
 
     #[test]
     fn deserialize_checks_bounds() -> Result<()> {
+        zebra_test::init();
+
         let big = MAX_MONEY * 2;
         let neg = -10;
 
@@ -572,6 +585,8 @@ mod test {
 
     #[test]
     fn hash() -> Result<()> {
+        zebra_test::init();
+
         let one = Amount::<NonNegative>::try_from(1)?;
         let another_one = Amount::<NonNegative>::try_from(1)?;
         let zero = Amount::<NonNegative>::try_from(0)?;
@@ -601,6 +616,8 @@ mod test {
 
     #[test]
     fn ordering_constraints() -> Result<()> {
+        zebra_test::init();
+
         ordering::<NonNegative, NonNegative>()?;
         ordering::<NonNegative, NegativeAllowed>()?;
         ordering::<NegativeAllowed, NonNegative>()?;
diff --git a/zebra-chain/src/block/height.rs b/zebra-chain/src/block/height.rs
index a9465db5f..0c11b2d74 100644
--- a/zebra-chain/src/block/height.rs
+++ b/zebra-chain/src/block/height.rs
@@ -132,6 +132,8 @@ impl Arbitrary for Height {
 
 #[test]
 fn operator_tests() {
+    zebra_test::init();
+
     assert_eq!(Some(Height(2)), Height(1) + Height(1));
     assert_eq!(None, Height::MAX + Height(1));
     // Bad heights aren't caught at compile-time or runtime, until we add or subtract
diff --git a/zebra-chain/src/parameters/network_upgrade.rs b/zebra-chain/src/parameters/network_upgrade.rs
index 3fc8e0916..d3db8f740 100644
--- a/zebra-chain/src/parameters/network_upgrade.rs
+++ b/zebra-chain/src/parameters/network_upgrade.rs
@@ -8,6 +8,8 @@ use crate::parameters::{Network, Network::*};
 use std::collections::{BTreeMap, HashMap};
 use std::ops::Bound::*;
 
+use chrono::Duration;
+
 /// A Zcash network upgrade.
 ///
 /// Network upgrades can change the Zcash network protocol or consensus rules in
@@ -96,6 +98,23 @@ pub(crate) const CONSENSUS_BRANCH_IDS: &[(NetworkUpgrade, ConsensusBranchId)] =
     (Canopy, ConsensusBranchId(0xe9ff75a6)),
 ];
 
+/// The target block spacing before Blossom.
+const PRE_BLOSSOM_POW_TARGET_SPACING: i64 = 150;
+
+/// The target block spacing after Blossom activation.
+const POST_BLOSSOM_POW_TARGET_SPACING: i64 = 75;
+
+/// The multiplier used to derive the testnet minimum difficulty block time gap
+/// threshold.
+///
+/// Based on https://zips.z.cash/zip-0208#minimum-difficulty-blocks-on-the-test-network
+const TESTNET_MINIMUM_DIFFICULTY_GAP_MULTIPLIER: i32 = 6;
+
+/// The start height for the testnet minimum difficulty consensus rule.
+///
+/// Based on https://zips.z.cash/zip-0208#minimum-difficulty-blocks-on-the-test-network
+const TESTNET_MINIMUM_DIFFICULTY_START_HEIGHT: block::Height = block::Height(299_188);
+
 impl NetworkUpgrade {
     /// Returns a BTreeMap of activation heights and network upgrades for
     /// `network`.
@@ -163,6 +182,47 @@ impl NetworkUpgrade {
     pub fn branch_id(&self) -> Option<ConsensusBranchId> {
         NetworkUpgrade::branch_id_list().get(&self).cloned()
     }
+
+    /// Returns the target block spacing for the network upgrade.
+    ///
+    /// Based on `PRE_BLOSSOM_POW_TARGET_SPACING` and
+    /// `POST_BLOSSOM_POW_TARGET_SPACING` from the Zcash specification.
+    pub fn target_spacing(&self) -> Duration {
+        let spacing_seconds = match self {
+            Genesis | BeforeOverwinter | Overwinter | Sapling => PRE_BLOSSOM_POW_TARGET_SPACING,
+            Blossom | Heartwood | Canopy => POST_BLOSSOM_POW_TARGET_SPACING,
+        };
+
+        Duration::seconds(spacing_seconds)
+    }
+
+    /// Returns the target block spacing for `network` and `height`.
+    ///
+    /// See `target_spacing` for details.
+    pub fn target_spacing_for_height(network: Network, height: block::Height) -> Duration {
+        NetworkUpgrade::current(network, height).target_spacing()
+    }
+
+    /// Returns the minimum difficulty block spacing for `network` and `height`.
+    /// Returns `None` if the testnet minimum difficulty consensus rule is not active.
+    ///
+    /// Based on https://zips.z.cash/zip-0208#minimum-difficulty-blocks-on-the-test-network
+    ///
+    /// `zcashd` requires a gap that's strictly greater than 6 times the target
+    /// threshold, but ZIP-205 and ZIP-208 are ambiguous. See bug #1276.
+    pub fn minimum_difficulty_spacing_for_height(
+        network: Network,
+        height: block::Height,
+    ) -> Option<Duration> {
+        match (network, height) {
+            (Network::Testnet, height) if height < TESTNET_MINIMUM_DIFFICULTY_START_HEIGHT => None,
+            (Network::Mainnet, _) => None,
+            (Network::Testnet, _) => {
+                let network_upgrade = NetworkUpgrade::current(network, height);
+                Some(network_upgrade.target_spacing() * TESTNET_MINIMUM_DIFFICULTY_GAP_MULTIPLIER)
+            }
+        }
+    }
 }
 
 impl ConsensusBranchId {
diff --git a/zebra-chain/src/sapling/address.rs b/zebra-chain/src/sapling/address.rs
index 841f97c7a..2ee9e6ae5 100644
--- a/zebra-chain/src/sapling/address.rs
+++ b/zebra-chain/src/sapling/address.rs
@@ -119,6 +119,8 @@ mod tests {
 
     #[test]
     fn from_str_display() {
+        zebra_test::init();
+
         let zs_addr: Address =
             "zs1qqqqqqqqqqqqqqqqqrjq05nyfku05msvu49mawhg6kr0wwljahypwyk2h88z6975u563j8nfaxd"
                 .parse()
@@ -132,6 +134,8 @@ mod tests {
 
     #[test]
     fn derive_keys_and_addresses() {
+        zebra_test::init();
+
         let spending_key = keys::SpendingKey::new(&mut OsRng);
 
         let spend_authorizing_key = keys::SpendAuthorizingKey::from(spending_key);
@@ -158,6 +162,7 @@ proptest! {
 
     #[test]
     fn sapling_address_roundtrip(zaddr in any::<Address>()) {
+        zebra_test::init();
 
         let string = zaddr.to_string();
 
diff --git a/zebra-chain/src/sapling/commitment.rs b/zebra-chain/src/sapling/commitment.rs
index cd702d32c..562b83b1d 100644
--- a/zebra-chain/src/sapling/commitment.rs
+++ b/zebra-chain/src/sapling/commitment.rs
@@ -288,6 +288,8 @@ mod tests {
 
     #[test]
     fn pedersen_hash_to_point_test_vectors() {
+        zebra_test::init();
+
         const D: [u8; 8] = *b"Zcash_PH";
 
         for test_vector in test_vectors::TEST_VECTORS.iter() {
@@ -302,6 +304,8 @@ mod tests {
 
     #[test]
     fn add() {
+        zebra_test::init();
+
         let identity = ValueCommitment(jubjub::AffinePoint::identity());
 
         let g = ValueCommitment(jubjub::AffinePoint::from_raw_unchecked(
@@ -324,6 +328,8 @@ mod tests {
 
     #[test]
     fn add_assign() {
+        zebra_test::init();
+
         let mut identity = ValueCommitment(jubjub::AffinePoint::identity());
 
         let g = ValueCommitment(jubjub::AffinePoint::from_raw_unchecked(
@@ -349,6 +355,8 @@ mod tests {
 
     #[test]
     fn sub() {
+        zebra_test::init();
+
         let g_point = jubjub::AffinePoint::from_raw_unchecked(
             jubjub::Fq::from_raw([
                 0xe4b3_d35d_f1a7_adfe,
@@ -373,6 +381,8 @@ mod tests {
 
     #[test]
     fn sub_assign() {
+        zebra_test::init();
+
         let g_point = jubjub::AffinePoint::from_raw_unchecked(
             jubjub::Fq::from_raw([
                 0xe4b3_d35d_f1a7_adfe,
@@ -400,6 +410,8 @@ mod tests {
 
     #[test]
     fn sum() {
+        zebra_test::init();
+
         let g_point = jubjub::AffinePoint::from_raw_unchecked(
             jubjub::Fq::from_raw([
                 0xe4b3_d35d_f1a7_adfe,
diff --git a/zebra-chain/src/sapling/note/ciphertexts.rs b/zebra-chain/src/sapling/note/ciphertexts.rs
index f5215d184..5f7b97586 100644
--- a/zebra-chain/src/sapling/note/ciphertexts.rs
+++ b/zebra-chain/src/sapling/note/ciphertexts.rs
@@ -107,6 +107,7 @@ proptest! {
 
     #[test]
     fn encrypted_ciphertext_roundtrip(ec in any::<EncryptedNote>()) {
+        zebra_test::init();
 
         let mut data = Vec::new();
 
@@ -119,6 +120,7 @@ proptest! {
 
     #[test]
     fn out_ciphertext_roundtrip(oc in any::<WrappedNoteKey>()) {
+        zebra_test::init();
 
         let mut data = Vec::new();
 
diff --git a/zebra-chain/src/sapling/tree.rs b/zebra-chain/src/sapling/tree.rs
index 15e06f543..ad45b40fa 100644
--- a/zebra-chain/src/sapling/tree.rs
+++ b/zebra-chain/src/sapling/tree.rs
@@ -161,6 +161,8 @@ mod tests {
 
     #[test]
     fn empty_roots() {
+        zebra_test::init();
+
         // From https://github.com/zcash/librustzcash/blob/master/zcash_primitives/src/merkle_tree.rs#L512
         const HEX_EMPTY_ROOTS: [&str; 33] = [
             "0100000000000000000000000000000000000000000000000000000000000000",
@@ -205,6 +207,7 @@ mod tests {
 
     #[test]
     fn incremental_roots() {
+        zebra_test::init();
         // From https://github.com/zcash/zcash/blob/master/src/test/data/merkle_commitments_sapling.json
         // Byte-reversed from those ones because the original test vectors are loaded using uint256S()
         let commitments = [
diff --git a/zebra-chain/src/serialization/sha256d.rs b/zebra-chain/src/serialization/sha256d.rs
index 4b6acbfec..b861704da 100644
--- a/zebra-chain/src/serialization/sha256d.rs
+++ b/zebra-chain/src/serialization/sha256d.rs
@@ -60,6 +60,8 @@ mod tests {
 
     #[test]
     fn sha256d_checksum() {
+        zebra_test::init();
+
         // https://en.bitcoin.it/wiki/Protocol_documentation#Hashes
         let input = b"hello";
         let checksum = Checksum::from(&input[..]);
@@ -69,6 +71,8 @@ mod tests {
 
     #[test]
     fn sha256d_checksum_debug() {
+        zebra_test::init();
+
         let input = b"hello";
         let checksum = Checksum::from(&input[..]);
 
diff --git a/zebra-chain/src/sprout/address.rs b/zebra-chain/src/sprout/address.rs
index 73c074cbb..bbe9f1c49 100644
--- a/zebra-chain/src/sprout/address.rs
+++ b/zebra-chain/src/sprout/address.rs
@@ -138,6 +138,8 @@ mod tests {
 
     #[test]
     fn from_string_debug() {
+        zebra_test::init();
+
         let string = "zcU1Cd6zYyZCd2VJF8yKgmzjxdiiU1rgTTjEwoN1CGUWCziPkUTXUjXmX7TMqdMNsTfuiGN1jQoVN4kGxUR4sAPN4XZ7pxb";
         let zc_addr = string.parse::<SproutShieldedAddress>().unwrap();
 
@@ -150,6 +152,7 @@ proptest! {
 
     #[test]
     fn zcash_de_serialize_roundtrip(zaddr in any::<SproutShieldedAddress>()) {
+        zebra_test::init();
 
         let mut data = Vec::new();
 
@@ -163,6 +166,7 @@ proptest! {
 
     #[test]
     fn zcash_base58check_roundtrip(zaddr in any::<SproutShieldedAddress>()) {
+        zebra_test::init();
 
         let string = zaddr.to_string();
 
diff --git a/zebra-chain/src/sprout/keys.rs b/zebra-chain/src/sprout/keys.rs
index e9c562a0c..59c5ed52e 100644
--- a/zebra-chain/src/sprout/keys.rs
+++ b/zebra-chain/src/sprout/keys.rs
@@ -344,6 +344,8 @@ mod tests {
     #[test]
     // TODO: test vectors, not just random data
     fn derive_keys() {
+        zebra_test::init();
+
         let spending_key = SpendingKey::new(&mut OsRng);
 
         let receiving_key = ReceivingKey::from(spending_key);
@@ -357,6 +359,7 @@ proptest! {
 
     #[test]
     fn spending_key_roundtrip(sk in any::<SpendingKey>()) {
+        zebra_test::init();
 
         let mut data = Vec::new();
 
@@ -370,6 +373,7 @@ proptest! {
 
     #[test]
     fn spending_key_string_roundtrip(sk in any::<SpendingKey>()) {
+        zebra_test::init();
 
         let string = sk.to_string();
 
@@ -381,6 +385,7 @@ proptest! {
 
     #[test]
     fn incoming_viewing_key_roundtrip(ivk in any::<IncomingViewingKey>()) {
+        zebra_test::init();
 
         let mut data = Vec::new();
 
@@ -394,6 +399,7 @@ proptest! {
 
     #[test]
     fn incoming_viewing_key_string_roundtrip(ivk in any::<IncomingViewingKey>()) {
+        zebra_test::init();
 
         let string = ivk.to_string();
 
diff --git a/zebra-chain/src/sprout/note/ciphertexts.rs b/zebra-chain/src/sprout/note/ciphertexts.rs
index 515355106..818ca0ef7 100644
--- a/zebra-chain/src/sprout/note/ciphertexts.rs
+++ b/zebra-chain/src/sprout/note/ciphertexts.rs
@@ -60,6 +60,7 @@ proptest! {
 
     #[test]
     fn encrypted_ciphertext_roundtrip(ec in any::<EncryptedNote>()) {
+        zebra_test::init();
 
         let mut data = Vec::new();
 
diff --git a/zebra-chain/src/sprout/tree.rs b/zebra-chain/src/sprout/tree.rs
index e020c2310..10f9ec859 100644
--- a/zebra-chain/src/sprout/tree.rs
+++ b/zebra-chain/src/sprout/tree.rs
@@ -176,6 +176,8 @@ mod tests {
 
     #[test]
     fn empty_roots() {
+        zebra_test::init();
+
         // From https://github.com/zcash/zcash/blob/master/src/zcash/IncrementalMerkleTree.cpp#L439
         let hex_empty_roots = [
             "0000000000000000000000000000000000000000000000000000000000000000",
@@ -217,6 +219,8 @@ mod tests {
 
     #[test]
     fn incremental_roots() {
+        zebra_test::init();
+
         // From https://github.com/zcash/zcash/blob/master/src/test/data/merkle_commitments.json
         //
         // Byte-reversed from those ones because the original test vectors are
diff --git a/zebra-chain/src/transaction/hash.rs b/zebra-chain/src/transaction/hash.rs
index 9b7511aaa..ae1e9ea8c 100644
--- a/zebra-chain/src/transaction/hash.rs
+++ b/zebra-chain/src/transaction/hash.rs
@@ -65,6 +65,8 @@ mod tests {
 
     #[test]
     fn transactionhash_from_str() {
+        zebra_test::init();
+
         let hash: Hash = "3166411bd5343e0b284a108f39a929fbbb62619784f8c6dafe520703b5b446bf"
             .parse()
             .unwrap();
diff --git a/zebra-chain/src/transaction/memo.rs b/zebra-chain/src/transaction/memo.rs
index e5d0ad52e..6b09f7cb8 100644
--- a/zebra-chain/src/transaction/memo.rs
+++ b/zebra-chain/src/transaction/memo.rs
@@ -50,6 +50,8 @@ impl fmt::Debug for Memo {
 
 #[test]
 fn memo_fmt() {
+    zebra_test::init();
+
     let memo = Memo(Box::new(
         *b"thiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiis \
            iiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiis \
@@ -74,6 +76,8 @@ fn memo_fmt() {
 
 #[test]
 fn memo_from_string() {
+    zebra_test::init();
+
     let memo = Memo::try_from("foo bar baz".as_ref()).unwrap();
 
     let mut bytes = [0; 512];
diff --git a/zebra-chain/src/transparent/address.rs b/zebra-chain/src/transparent/address.rs
index 923007357..b52eb2686 100644
--- a/zebra-chain/src/transparent/address.rs
+++ b/zebra-chain/src/transparent/address.rs
@@ -257,6 +257,8 @@ mod tests {
 
     #[test]
     fn pubkey_mainnet() {
+        zebra_test::init();
+
         let pub_key = PublicKey::from_slice(&[
             3, 23, 183, 225, 206, 31, 159, 148, 195, 42, 67, 115, 146, 41, 248, 140, 11, 3, 51, 41,
             111, 180, 110, 143, 114, 134, 88, 73, 198, 174, 52, 184, 78,
@@ -270,6 +272,8 @@ mod tests {
 
     #[test]
     fn pubkey_testnet() {
+        zebra_test::init();
+
         let pub_key = PublicKey::from_slice(&[
             3, 23, 183, 225, 206, 31, 159, 148, 195, 42, 67, 115, 146, 41, 248, 140, 11, 3, 51, 41,
             111, 180, 110, 143, 114, 134, 88, 73, 198, 174, 52, 184, 78,
@@ -283,6 +287,8 @@ mod tests {
 
     #[test]
     fn empty_script_mainnet() {
+        zebra_test::init();
+
         let script = Script(vec![0; 20]);
 
         let t_addr = script.to_address(Network::Mainnet);
@@ -292,6 +298,8 @@ mod tests {
 
     #[test]
     fn empty_script_testnet() {
+        zebra_test::init();
+
         let script = Script(vec![0; 20]);
 
         let t_addr = script.to_address(Network::Testnet);
@@ -301,6 +309,8 @@ mod tests {
 
     #[test]
     fn from_string() {
+        zebra_test::init();
+
         let t_addr: Address = "t3Vz22vK5z2LcKEdg16Yv4FFneEL1zg9ojd".parse().unwrap();
 
         assert_eq!(format!("{}", t_addr), "t3Vz22vK5z2LcKEdg16Yv4FFneEL1zg9ojd");
@@ -308,6 +318,8 @@ mod tests {
 
     #[test]
     fn debug() {
+        zebra_test::init();
+
         let t_addr: Address = "t3Vz22vK5z2LcKEdg16Yv4FFneEL1zg9ojd".parse().unwrap();
 
         assert_eq!(
@@ -322,6 +334,7 @@ proptest! {
 
     #[test]
     fn transparent_address_roundtrip(taddr in any::<Address>()) {
+        zebra_test::init();
 
         let mut data = Vec::new();
 
diff --git a/zebra-chain/src/transparent/script.rs b/zebra-chain/src/transparent/script.rs
index 1e92bda7f..d99914acd 100644
--- a/zebra-chain/src/transparent/script.rs
+++ b/zebra-chain/src/transparent/script.rs
@@ -53,6 +53,8 @@ mod proptests {
     proptest! {
         #[test]
         fn script_roundtrip(script in any::<Script>()) {
+            zebra_test::init();
+
             let mut bytes = Cursor::new(Vec::new());
             script.zcash_serialize(&mut bytes)?;
 
diff --git a/zebra-chain/src/work/difficulty.rs b/zebra-chain/src/work/difficulty.rs
index 6443e2b27..45e654a19 100644
--- a/zebra-chain/src/work/difficulty.rs
+++ b/zebra-chain/src/work/difficulty.rs
@@ -98,7 +98,7 @@ impl fmt::Debug for ExpandedDifficulty {
     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
         let mut buf = [0; 32];
         // Use the same byte order as block::Hash
-        self.0.to_little_endian(&mut buf);
+        self.0.to_big_endian(&mut buf);
         f.debug_tuple("ExpandedDifficulty")
             .field(&hex::encode(&buf))
             .finish()
@@ -123,6 +123,12 @@ impl fmt::Debug for ExpandedDifficulty {
 #[derive(Clone, Copy, Default, Eq, PartialEq, Ord, PartialOrd)]
 pub struct Work(u128);
 
+impl Work {
+    pub fn as_u128(self) -> u128 {
+        self.0
+    }
+}
+
 impl fmt::Debug for Work {
     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
         // There isn't a standard way to represent alternate formats for the
@@ -239,20 +245,33 @@ impl CompactDifficulty {
     /// [Zcash Specification]: https://zips.z.cash/protocol/protocol.pdf#workdef
     pub fn to_work(&self) -> Option<Work> {
         let expanded = self.to_expanded()?;
+        Work::try_from(expanded).ok()
+    }
+}
+
+impl TryFrom<ExpandedDifficulty> for Work {
+    type Error = ();
 
+    fn try_from(expanded: ExpandedDifficulty) -> Result<Self, Self::Error> {
         // We need to compute `2^256 / (expanded + 1)`, but we can't represent
         // 2^256, as it's too large for a u256. However, as 2^256 is at least as
         // large as `expanded + 1`, it is equal to
         // `((2^256 - expanded - 1) / (expanded + 1)) + 1`, or
         let result = (!expanded.0 / (expanded.0 + 1)) + 1;
         if result <= u128::MAX.into() {
-            Some(Work(result.as_u128()))
+            Ok(Work(result.as_u128()))
         } else {
-            None
+            Err(())
         }
     }
 }
 
+impl From<ExpandedDifficulty> for CompactDifficulty {
+    fn from(value: ExpandedDifficulty) -> Self {
+        value.to_compact()
+    }
+}
+
 impl ExpandedDifficulty {
     /// Returns the difficulty of the hash.
     ///
@@ -280,7 +299,16 @@ impl ExpandedDifficulty {
             Network::Testnet => (U256::one() << 251) - 1,
         };
 
-        limit.into()
+        // `zcashd` converts the PoWLimit into a compact representation before
+        // using it to perform difficulty filter checks.
+        //
+        // The Zcash specification converts to compact for the default difficulty
+        // filter, but not for testnet minimum difficulty blocks. (ZIP 205 and
+        // ZIP 208 don't specify this conversion either.) See #1277 for details.
+        ExpandedDifficulty(limit)
+            .to_compact()
+            .to_expanded()
+            .expect("difficulty limits are valid expanded values")
     }
 
     /// Calculate the CompactDifficulty for an expanded difficulty.
diff --git a/zebra-chain/src/work/difficulty/arbitrary.rs b/zebra-chain/src/work/difficulty/arbitrary.rs
index 586ad83ea..1e6c93032 100644
--- a/zebra-chain/src/work/difficulty/arbitrary.rs
+++ b/zebra-chain/src/work/difficulty/arbitrary.rs
@@ -51,15 +51,7 @@ impl Arbitrary for Work {
         // In the Zcash protocol, a Work is converted from an ExpandedDifficulty.
         // But some randomised difficulties are impractically large, and will
         // never appear in any real-world block. So we just use a random Work value.
-        (any::<u128>())
-            .prop_filter_map("zero Work values are invalid", |w| {
-                if w == 0 {
-                    None
-                } else {
-                    Some(Work(w))
-                }
-            })
-            .boxed()
+        (1..std::u128::MAX).prop_map(Work).boxed()
     }
 
     type Strategy = BoxedStrategy<Self>;
diff --git a/zebra-consensus/Cargo.toml b/zebra-consensus/Cargo.toml
index eb84e8b45..eeae83d0c 100644
--- a/zebra-consensus/Cargo.toml
+++ b/zebra-consensus/Cargo.toml
@@ -10,7 +10,7 @@ chrono = "0.4.19"
 color-eyre = "0.5"
 displaydoc = "0.1.7"
 jubjub = "0.5.1"
-once_cell = "1.4"
+once_cell = "1.5"
 rand = "0.7"
 redjubjub = "0.2"
 serde = { version = "1", features = ["serde_derive"] }
diff --git a/zebra-consensus/src/block/check.rs b/zebra-consensus/src/block/check.rs
index eb1ef1bb3..b522d0f36 100644
--- a/zebra-consensus/src/block/check.rs
+++ b/zebra-consensus/src/block/check.rs
@@ -70,12 +70,19 @@ pub fn difficulty_is_valid(
         ))?;
     }
 
-    // Difficulty filter
+    // The difficulty filter is also context-free.
+    //
+    // ZIP 205 and ZIP 208 incorrectly describe testnet minimum difficulty blocks
+    // as a change to the difficulty filter. But in `zcashd`, it is implemented
+    // as a change to the difficulty adjustment algorithm. So we don't need to
+    // do anything special for testnet here.
+    // For details, see https://github.com/zcash/zips/issues/416
     if hash > &difficulty_threshold {
         Err(BlockError::DifficultyFilter(
             *height,
             *hash,
             difficulty_threshold,
+            network,
         ))?;
     }
 
diff --git a/zebra-consensus/src/block/subsidy/founders_reward.rs b/zebra-consensus/src/block/subsidy/founders_reward.rs
index 953968487..b4b08a3bd 100644
--- a/zebra-consensus/src/block/subsidy/founders_reward.rs
+++ b/zebra-consensus/src/block/subsidy/founders_reward.rs
@@ -33,6 +33,8 @@ mod test {
     use zebra_chain::parameters::NetworkUpgrade::*;
     #[test]
     fn test_founders_reward() -> Result<(), Report> {
+        zebra_test::init();
+
         let network = Network::Mainnet;
         let blossom_height = Blossom.activation_height(network).unwrap();
         let canopy_height = Canopy.activation_height(network).unwrap();
diff --git a/zebra-consensus/src/block/subsidy/general.rs b/zebra-consensus/src/block/subsidy/general.rs
index f8970db38..4da95a34a 100644
--- a/zebra-consensus/src/block/subsidy/general.rs
+++ b/zebra-consensus/src/block/subsidy/general.rs
@@ -109,6 +109,8 @@ mod test {
 
     #[test]
     fn halving_test() -> Result<(), Report> {
+        zebra_test::init();
+
         halving_for_network(Network::Mainnet)?;
         halving_for_network(Network::Testnet)?;
 
@@ -194,6 +196,8 @@ mod test {
 
     #[test]
     fn block_subsidy_test() -> Result<(), Report> {
+        zebra_test::init();
+
         block_subsidy_for_network(Network::Mainnet)?;
         block_subsidy_for_network(Network::Testnet)?;
 
@@ -283,6 +287,8 @@ mod test {
 
     #[test]
     fn miner_subsidy_test() -> Result<(), Report> {
+        zebra_test::init();
+
         miner_subsidy_for_network(Network::Mainnet)?;
         miner_subsidy_for_network(Network::Testnet)?;
 
diff --git a/zebra-consensus/src/checkpoint.rs b/zebra-consensus/src/checkpoint.rs
index d4018c434..c9a407eff 100644
--- a/zebra-consensus/src/checkpoint.rs
+++ b/zebra-consensus/src/checkpoint.rs
@@ -44,7 +44,7 @@ mod tests;
 
 pub(crate) use list::CheckpointList;
 use types::{Progress, Progress::*};
-use types::{Target, Target::*};
+use types::{TargetHeight, TargetHeight::*};
 
 /// An unverified block, which is in the queue for checkpoint verification.
 #[derive(Debug)]
@@ -262,7 +262,7 @@ where
     /// `height` increases as checkpoints are verified.
     ///
     /// If verification has finished, returns `FinishedVerifying`.
-    fn target_checkpoint_height(&self) -> Target<block::Height> {
+    fn target_checkpoint_height(&self) -> TargetHeight {
         // Find the height we want to start searching at
         let start_height = match self.previous_checkpoint_height() {
             // Check if we have the genesis block as a special case, to simplify the loop
@@ -400,6 +400,14 @@ where
 
     /// Increase the current checkpoint height to `verified_height`,
     fn update_progress(&mut self, verified_height: block::Height) {
+        if let Some(max_height) = self.queued.keys().next_back() {
+            metrics::gauge!("checkpoint.queued.max.height", max_height.0 as i64);
+        } else {
+            // use -1 as a sentinel value for "None", because 0 is a valid height
+            metrics::gauge!("checkpoint.queued.max.height", -1);
+        }
+        metrics::gauge!("checkpoint.queued_slots", self.queued.len() as i64);
+
         // Ignore blocks that are below the previous checkpoint, or otherwise
         // have invalid heights.
         //
@@ -837,8 +845,6 @@ where
         // should be rare.
         self.process_checkpoint_range();
 
-        metrics::gauge!("checkpoint.queued_slots", self.queued.len() as i64);
-
         let mut state_service = self.state_service.clone();
         async move {
             let hash = rx
diff --git a/zebra-consensus/src/checkpoint/types.rs b/zebra-consensus/src/checkpoint/types.rs
index 6ca0dcac8..db295d926 100644
--- a/zebra-consensus/src/checkpoint/types.rs
+++ b/zebra-consensus/src/checkpoint/types.rs
@@ -5,7 +5,7 @@ use std::cmp::Ordering;
 use zebra_chain::block;
 
 use Progress::*;
-use Target::*;
+use TargetHeight::*;
 
 /// A `CheckpointVerifier`'s current progress verifying the chain.
 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
@@ -62,16 +62,17 @@ impl PartialOrd for Progress<block::Height> {
     }
 }
 
-/// A `CheckpointVerifier`'s target checkpoint, based on the current queue.
+/// A `CheckpointVerifier`'s target checkpoint height, based on the current
+/// queue.
 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
-pub enum Target<HeightOrHash> {
+pub enum TargetHeight {
     /// We need more blocks before we can choose a target checkpoint.
     WaitingForBlocks,
     /// We want to verify this checkpoint.
     ///
     /// The target checkpoint can be multiple checkpoints ahead of the previous
     /// checkpoint.
-    Checkpoint(HeightOrHash),
+    Checkpoint(block::Height),
     /// We have finished verifying, there will be no more targets.
     FinishedVerifying,
 }
@@ -79,7 +80,7 @@ pub enum Target<HeightOrHash> {
 /// Block height target, in chain order.
 ///
 /// `WaitingForBlocks` is incomparable with itself and `Checkpoint(_)`.
-impl PartialOrd for Target<block::Height> {
+impl PartialOrd for TargetHeight {
     fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
         match (self, other) {
             // FinishedVerifying is the final state
diff --git a/zebra-consensus/src/error.rs b/zebra-consensus/src/error.rs
index bee2f1878..70c002c32 100644
--- a/zebra-consensus/src/error.rs
+++ b/zebra-consensus/src/error.rs
@@ -108,10 +108,13 @@ pub enum BlockError {
         zebra_chain::work::difficulty::ExpandedDifficulty,
     ),
 
-    #[error("block {0:?} has a hash {1:?} that is easier than the difficulty threshold {2:?}")]
+    #[error(
+        "block {0:?} on {3:?} has a hash {1:?} that is easier than its difficulty threshold {2:?}"
+    )]
     DifficultyFilter(
         zebra_chain::block::Height,
         zebra_chain::block::Hash,
         zebra_chain::work::difficulty::ExpandedDifficulty,
+        zebra_chain::parameters::Network,
     ),
 }
diff --git a/zebra-consensus/src/parameters/subsidy.rs b/zebra-consensus/src/parameters/subsidy.rs
index 03894b311..fc5d00a01 100644
--- a/zebra-consensus/src/parameters/subsidy.rs
+++ b/zebra-consensus/src/parameters/subsidy.rs
@@ -1,7 +1,5 @@
 //! Constants for Block Subsidy, Funding Streams, and Founders’ Reward
 
-use std::time::Duration;
-
 use zebra_chain::{amount::COIN, block::Height};
 
 /// An initial period from Genesis to this Height where the block subsidy is gradually incremented. [What is slow-start mining][slow-mining]
@@ -22,15 +20,11 @@ pub const SLOW_START_SHIFT: Height = Height(SLOW_START_INTERVAL.0 / 2);
 /// This calculation is exact, because COIN is divisible by 2, and the division is done last.
 pub const MAX_BLOCK_SUBSIDY: u64 = ((25 * COIN) / 2) as u64;
 
-/// The blocktime before Blossom, used to calculate ratio.
-pub const PRE_BLOSSOM_POW_TARGET_SPACING: Duration = Duration::from_secs(150);
-
-/// The blocktime after Blossom, used to calculate ratio.
-pub const POST_BLOSSOM_POW_TARGET_SPACING: Duration = Duration::from_secs(75);
-
 /// Used as a multiplier to get the new halving interval after Blossom.
-pub const BLOSSOM_POW_TARGET_SPACING_RATIO: u64 =
-    PRE_BLOSSOM_POW_TARGET_SPACING.as_secs() / POST_BLOSSOM_POW_TARGET_SPACING.as_secs();
+///
+/// Calculated as `PRE_BLOSSOM_POW_TARGET_SPACING / POST_BLOSSOM_POW_TARGET_SPACING`
+/// in the Zcash specification.
+pub const BLOSSOM_POW_TARGET_SPACING_RATIO: u64 = 2;
 
 /// Halving is at about every 4 years, before Blossom block time is 150 seconds.
 ///
diff --git a/zebra-network/src/constants.rs b/zebra-network/src/constants.rs
index 37a4fcfd4..f4b8d59de 100644
--- a/zebra-network/src/constants.rs
+++ b/zebra-network/src/constants.rs
@@ -113,6 +113,8 @@ mod tests {
     /// relies on.
     #[test]
     fn ensure_live_peer_duration_value_matches_others() {
+        zebra_test::init();
+
         let constructed_live_peer_duration =
             HEARTBEAT_INTERVAL + REQUEST_TIMEOUT + REQUEST_TIMEOUT + REQUEST_TIMEOUT;
 
@@ -122,6 +124,8 @@ mod tests {
     /// Make sure that the timeout values are consistent with each other.
     #[test]
     fn ensure_timeouts_consistent() {
+        zebra_test::init();
+
         assert!(HANDSHAKE_TIMEOUT <= REQUEST_TIMEOUT,
                 "Handshakes are requests, so the handshake timeout can't be longer than the timeout for all requests.");
         // This check is particularly important on testnet, which has a small
diff --git a/zebra-network/src/meta_addr.rs b/zebra-network/src/meta_addr.rs
index 792e6d1c7..a122a10f4 100644
--- a/zebra-network/src/meta_addr.rs
+++ b/zebra-network/src/meta_addr.rs
@@ -92,6 +92,8 @@ mod tests {
     // XXX remove this test and replace it with a proptest instance.
     #[test]
     fn sanitize_truncates_timestamps() {
+        zebra_test::init();
+
         let entry = MetaAddr {
             services: PeerServices::default(),
             addr: "127.0.0.1:8233".parse().unwrap(),
diff --git a/zebra-network/src/protocol/external/codec.rs b/zebra-network/src/protocol/external/codec.rs
index baa89832e..ac3c56adc 100644
--- a/zebra-network/src/protocol/external/codec.rs
+++ b/zebra-network/src/protocol/external/codec.rs
@@ -582,6 +582,7 @@ mod tests {
 
     #[test]
     fn version_message_round_trip() {
+        zebra_test::init();
         use std::net::{IpAddr, Ipv4Addr, SocketAddr};
         let services = PeerServices::NODE_NETWORK;
         let timestamp = Utc.timestamp(1_568_000_000, 0);
@@ -631,6 +632,8 @@ mod tests {
 
     #[test]
     fn filterload_message_round_trip() {
+        zebra_test::init();
+
         let mut rt = Runtime::new().unwrap();
 
         let v = Message::FilterLoad {
@@ -665,6 +668,8 @@ mod tests {
 
     #[test]
     fn filterload_message_too_large_round_trip() {
+        zebra_test::init();
+
         let mut rt = Runtime::new().unwrap();
 
         let v = Message::FilterLoad {
diff --git a/zebra-network/src/protocol/external/types.rs b/zebra-network/src/protocol/external/types.rs
index c0d699005..a277343d0 100644
--- a/zebra-network/src/protocol/external/types.rs
+++ b/zebra-network/src/protocol/external/types.rs
@@ -122,6 +122,8 @@ mod proptest {
 
     #[test]
     fn magic_debug() {
+        zebra_test::init();
+
         assert_eq!(format!("{:?}", magics::MAINNET), "Magic(\"24e92764\")");
         assert_eq!(format!("{:?}", magics::TESTNET), "Magic(\"fa1af9bf\")");
     }
@@ -130,6 +132,8 @@ mod proptest {
 
         #[test]
         fn proptest_magic_from_array(data in any::<[u8; 4]>()) {
+            zebra_test::init();
+
             assert_eq!(format!("{:?}", Magic(data)), format!("Magic({:x?})", hex::encode(data)));
         }
     }
@@ -152,6 +156,8 @@ mod test {
     /// Test the min_for_upgrade and current_min functions for `network` with
     /// extreme values.
     fn version_extremes(network: Network) {
+        zebra_test::init();
+
         assert_eq!(
             Version::current_min(network, block::Height(0)),
             Version::min_for_upgrade(network, BeforeOverwinter),
@@ -178,6 +184,8 @@ mod test {
     /// Check that the min_for_upgrade and current_min functions
     /// are consistent for `network`.
     fn version_consistent(network: Network) {
+        zebra_test::init();
+
         let highest_network_upgrade = NetworkUpgrade::current(network, block::Height::MAX);
         assert!(highest_network_upgrade == Canopy || highest_network_upgrade == Heartwood,
                 "expected coverage of all network upgrades: add the new network upgrade to the list in this test");
diff --git a/zebra-state/Cargo.toml b/zebra-state/Cargo.toml
index 97621c506..ab5bf2295 100644
--- a/zebra-state/Cargo.toml
+++ b/zebra-state/Cargo.toml
@@ -15,7 +15,7 @@ dirs = "3.0.1"
 hex = "0.4.2"
 lazy_static = "1.4.0"
 serde = { version = "1", features = ["serde_derive"] }
-sled = "0.34.4"
+sled = "0.34.5"
 
 futures = "0.3.7"
 metrics = "0.12"
@@ -30,8 +30,9 @@ displaydoc = "0.1.7"
 zebra-chain = { path = "../zebra-chain", features = ["proptest-impl"] }
 zebra-test = { path = "../zebra-test/" }
 
-once_cell = "1.4"
+once_cell = "1.5"
 spandoc = "0.2"
 tempdir = "0.3.7"
 tokio = { version = "0.2.22", features = ["full"] }
 proptest = "0.10.1"
+primitive-types = "0.7.3"
diff --git a/zebra-state/src/error.rs b/zebra-state/src/error.rs
index 8bdf83769..2b2bdb794 100644
--- a/zebra-state/src/error.rs
+++ b/zebra-state/src/error.rs
@@ -38,4 +38,8 @@ pub enum ValidateContextError {
     /// block.height is lower than the current finalized height
     #[non_exhaustive]
     OrphanedBlock,
+
+    /// block.height is not one greater than its parent block's height
+    #[non_exhaustive]
+    NonSequentialBlock,
 }
diff --git a/zebra-state/src/service.rs b/zebra-state/src/service.rs
index f2c6b0e7c..6b446d990 100644
--- a/zebra-state/src/service.rs
+++ b/zebra-state/src/service.rs
@@ -25,7 +25,10 @@ use crate::{
     ValidateContextError,
 };
 
+mod check;
 mod memory_state;
+#[cfg(test)]
+mod tests;
 mod utxo;
 
 // todo: put this somewhere
@@ -182,17 +185,18 @@ impl StateService {
     /// Check that `block` is contextually valid based on the committed finalized
     /// and non-finalized state.
     fn check_contextual_validity(&mut self, block: &Block) -> Result<(), ValidateContextError> {
-        use ValidateContextError::*;
-
-        if block
+        let finalized_tip_height = self.sled.finalized_tip_height().expect(
+            "finalized state must contain at least one block to use the non-finalized state",
+        );
+        check::block_is_not_orphaned(finalized_tip_height, block)?;
+
+        let parent_block = self
+            .block(block.hash().into())
+            .expect("the parent's presence has already been checked");
+        let parent_height = parent_block
             .coinbase_height()
-            .expect("valid blocks have a coinbase height")
-            <= self.sled.finalized_tip_height().expect(
-                "finalized state must contain at least one block to use the non-finalized state",
-            )
-        {
-            Err(OrphanedBlock)?;
-        }
+            .expect("valid blocks have a coinbase height");
+        check::height_one_more_than_parent_height(parent_height, block)?;
 
         // TODO: contextual validation design and implementation
         Ok(())
@@ -248,10 +252,122 @@ impl StateService {
         self.mem.hash(height).or_else(|| self.sled.hash(height))
     }
 
+    /// Return the height for the block at `hash` in any chain.
+    pub fn height_by_hash(&self, hash: block::Hash) -> Option<block::Height> {
+        self.mem
+            .height_by_hash(hash)
+            .or_else(|| self.sled.height(hash))
+    }
+
     /// Return the utxo pointed to by `outpoint` if it exists in any chain.
     pub fn utxo(&self, outpoint: &transparent::OutPoint) -> Option<transparent::Output> {
         self.mem.utxo(outpoint).or_else(|| self.sled.utxo(outpoint))
     }
+
+    /// Return an iterator over the relevant chain of the block identified by
+    /// `hash`.
+    ///
+    /// The block identified by `hash` is included in the chain of blocks yielded
+    /// by the iterator.
+    #[allow(dead_code)]
+    pub fn chain(&self, hash: block::Hash) -> Iter<'_> {
+        Iter {
+            service: self,
+            state: IterState::NonFinalized(hash),
+        }
+    }
+}
+
+struct Iter<'a> {
+    service: &'a StateService,
+    state: IterState,
+}
+
+enum IterState {
+    NonFinalized(block::Hash),
+    Finalized(block::Height),
+    Finished,
+}
+
+impl Iter<'_> {
+    fn next_non_finalized_block(&mut self) -> Option<Arc<Block>> {
+        let Iter { service, state } = self;
+
+        let hash = match state {
+            IterState::NonFinalized(hash) => *hash,
+            IterState::Finalized(_) | IterState::Finished => unreachable!(),
+        };
+
+        if let Some(block) = service.mem.block_by_hash(hash) {
+            let hash = block.header.previous_block_hash;
+            self.state = IterState::NonFinalized(hash);
+            Some(block)
+        } else {
+            None
+        }
+    }
+
+    fn next_finalized_block(&mut self) -> Option<Arc<Block>> {
+        let Iter { service, state } = self;
+
+        let hash_or_height: HashOrHeight = match *state {
+            IterState::Finalized(height) => height.into(),
+            IterState::NonFinalized(hash) => hash.into(),
+            IterState::Finished => unreachable!(),
+        };
+
+        if let Some(block) = service.sled.block(hash_or_height) {
+            let height = block
+                .coinbase_height()
+                .expect("valid blocks have a coinbase height");
+
+            if let Some(next_height) = height - 1 {
+                self.state = IterState::Finalized(next_height);
+            } else {
+                self.state = IterState::Finished;
+            }
+
+            Some(block)
+        } else {
+            self.state = IterState::Finished;
+            None
+        }
+    }
+}
+
+impl Iterator for Iter<'_> {
+    type Item = Arc<Block>;
+
+    fn next(&mut self) -> Option<Self::Item> {
+        match self.state {
+            IterState::NonFinalized(_) => self
+                .next_non_finalized_block()
+                .or_else(|| self.next_finalized_block()),
+            IterState::Finalized(_) => self.next_finalized_block(),
+            IterState::Finished => None,
+        }
+    }
+
+    fn size_hint(&self) -> (usize, Option<usize>) {
+        let len = self.len();
+        (len, Some(len))
+    }
+}
+
+impl std::iter::FusedIterator for Iter<'_> {}
+
+impl ExactSizeIterator for Iter<'_> {
+    fn len(&self) -> usize {
+        match self.state {
+            IterState::NonFinalized(hash) => self
+                .service
+                .height_by_hash(hash)
+                .map(|height| (height.0 + 1) as _)
+                .unwrap_or(0),
+            IterState::Finalized(height) => (height.0 + 1) as _,
+            IterState::Finished => 0,
+        }
+    }
 }
 
 impl Service<Request> for StateService {
diff --git a/zebra-state/src/service/check.rs b/zebra-state/src/service/check.rs
new file mode 100644
index 000000000..b62cb61da
--- /dev/null
+++ b/zebra-state/src/service/check.rs
@@ -0,0 +1,87 @@
+//! Consensus critical contextual checks
+
+use zebra_chain::block::{self, Block};
+
+use crate::ValidateContextError;
+
+/// Returns `ValidateContextError::OrphanedBlock` if the height of the given
+/// block is less than or equal to the finalized tip height.
+pub(super) fn block_is_not_orphaned(
+    finalized_tip_height: block::Height,
+    block: &Block,
+) -> Result<(), ValidateContextError> {
+    if block
+        .coinbase_height()
+        .expect("valid blocks have a coinbase height")
+        <= finalized_tip_height
+    {
+        Err(ValidateContextError::OrphanedBlock)
+    } else {
+        Ok(())
+    }
+}
+
+/// Returns `ValidateContextError::NonSequentialBlock` if the block height isn't
+/// equal to the parent_height+1.
+pub(super) fn height_one_more_than_parent_height(
+    parent_height: block::Height,
+    block: &Block,
+) -> Result<(), ValidateContextError> {
+    let height = block
+        .coinbase_height()
+        .expect("valid blocks have a coinbase height");
+
+    if parent_height + 1 != Some(height) {
+        Err(ValidateContextError::NonSequentialBlock)
+    } else {
+        Ok(())
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use std::sync::Arc;
+
+    use zebra_chain::serialization::ZcashDeserializeInto;
+
+    use super::*;
+
+    #[test]
+    fn test_orphan_consensus_check() {
+        zebra_test::init();
+
+        let block = zebra_test::vectors::BLOCK_MAINNET_347499_BYTES
+            .zcash_deserialize_into::<Arc<Block>>()
+            .unwrap();
+
+        block_is_not_orphaned(block::Height(0), &block).expect("tip is lower so it should be fine");
+        block_is_not_orphaned(block::Height(347498), &block)
+            .expect("tip is lower so it should be fine");
+        block_is_not_orphaned(block::Height(347499), &block)
+            .expect_err("tip is equal so it should error");
+        block_is_not_orphaned(block::Height(500000), &block)
+            .expect_err("tip is higher so it should error");
+    }
+
+    #[test]
+    fn test_sequential_height_check() {
+        zebra_test::init();
+
+        let block = zebra_test::vectors::BLOCK_MAINNET_347499_BYTES
+            .zcash_deserialize_into::<Arc<Block>>()
+            .unwrap();
+
+        height_one_more_than_parent_height(block::Height(0), &block)
+            .expect_err("block is much lower, should panic");
+        height_one_more_than_parent_height(block::Height(347497), &block)
+            .expect_err("parent height is 2 less, should panic");
+        height_one_more_than_parent_height(block::Height(347498), &block)
+            .expect("parent height is 1 less, should be good");
+        height_one_more_than_parent_height(block::Height(347499), &block)
+            .expect_err("parent height is equal, should panic");
+        height_one_more_than_parent_height(block::Height(347500), &block)
+            .expect_err("parent height is way more, should panic");
+        height_one_more_than_parent_height(block::Height(500000), &block)
+            .expect_err("parent height is way more, should panic");
+    }
+}
diff --git a/zebra-state/src/service/memory_state/chain.rs b/zebra-state/src/service/memory_state/chain.rs
index 50f521a09..82b85bff6 100644
--- a/zebra-state/src/service/memory_state/chain.rs
+++ b/zebra-state/src/service/memory_state/chain.rs
@@ -117,6 +117,10 @@ impl Chain {
             .next_back()
             .expect("only called while blocks is populated")
     }
+
+    pub fn is_empty(&self) -> bool {
+        self.blocks.is_empty()
+    }
 }
 
 /// Helper trait to organize inverse operations done on the `Chain` type. Used to
@@ -414,9 +418,7 @@ impl Ord for Chain {
 
 #[cfg(test)]
 mod tests {
-    use transaction::Transaction;
-
-    use std::{env, fmt, mem};
+    use std::{env, fmt};
 
     use zebra_chain::serialization::ZcashDeserializeInto;
     use zebra_chain::{
@@ -425,6 +427,8 @@ mod tests {
     };
     use zebra_test::prelude::*;
 
+    use crate::tests::FakeChainHelper;
+
     use self::assert_eq;
     use super::*;
 
@@ -436,37 +440,6 @@ mod tests {
         }
     }
 
-    /// Helper trait for constructing "valid" looking chains of blocks
-    trait FakeChainHelper {
-        fn make_fake_child(&self) -> Arc<Block>;
-    }
-
-    impl FakeChainHelper for Block {
-        fn make_fake_child(&self) -> Arc<Block> {
-            let parent_hash = self.hash();
-            let mut child = Block::clone(self);
-            let mut transactions = mem::take(&mut child.transactions);
-            let mut tx = transactions.remove(0);
-
-            let input = match Arc::make_mut(&mut tx) {
-                Transaction::V1 { inputs, .. } => &mut inputs[0],
-                Transaction::V2 { inputs, .. } => &mut inputs[0],
-                Transaction::V3 { inputs, .. } => &mut inputs[0],
-                Transaction::V4 { inputs, .. } => &mut inputs[0],
-            };
-
-            match input {
-                transparent::Input::Coinbase { height, .. } => height.0 += 1,
-                _ => panic!("block must have a coinbase height to create a child"),
-            }
-
-            child.transactions.push(tx);
-            child.header.previous_block_hash = parent_hash;
-
-            Arc::new(child)
-        }
-    }
-
     #[test]
     fn construct_empty() {
         zebra_test::init();
@@ -511,6 +484,25 @@ mod tests {
         Ok(())
     }
 
+    #[test]
+    fn ord_matches_work() -> Result<()> {
+        zebra_test::init();
+        let less_block = zebra_test::vectors::BLOCK_MAINNET_434873_BYTES
+            .zcash_deserialize_into::<Arc<Block>>()?
+            .set_work(1);
+        let more_block = less_block.clone().set_work(10);
+
+        let mut lesser_chain = Chain::default();
+        lesser_chain.push(less_block);
+
+        let mut bigger_chain = Chain::default();
+        bigger_chain.push(more_block);
+
+        assert!(bigger_chain > lesser_chain);
+
+        Ok(())
+    }
+
     fn arbitrary_chain(tip_height: block::Height) -> BoxedStrategy<Vec<Arc<Block>>> {
         Block::partial_chain_strategy(LedgerState::new(tip_height, Network::Mainnet), 100)
     }
diff --git a/zebra-state/src/service/memory_state/non_finalized_state.rs b/zebra-state/src/service/memory_state/non_finalized_state.rs
index 5b6965f06..f4bcbe1e5 100644
--- a/zebra-state/src/service/memory_state/non_finalized_state.rs
+++ b/zebra-state/src/service/memory_state/non_finalized_state.rs
@@ -33,15 +33,18 @@ impl NonFinalizedState {
 
         // remove the lowest height block from the best_chain as finalized_block
         let finalized_block = best_chain.pop_root();
+
         // add best_chain back to `self.chain_set`
-        self.chain_set.insert(best_chain);
+        if !best_chain.is_empty() {
+            self.chain_set.insert(best_chain);
+        }
 
         // for each remaining chain in side_chains
         for mut chain in side_chains {
             // remove the first block from `chain`
             let chain_start = chain.pop_root();
             // if block equals finalized_block
-            if chain_start == finalized_block {
+            if !chain.is_empty() && chain_start == finalized_block {
                 // add the chain back to `self.chain_set`
                 self.chain_set.insert(chain);
             } else {
@@ -50,11 +53,13 @@ impl NonFinalizedState {
             }
         }
 
+        self.update_metrics_for_chains();
+
         // return the finalized block
         finalized_block
     }
 
-    /// Commit block to the non-finalize state.
+    /// Commit block to the non-finalized state.
     pub fn commit_block(&mut self, block: Arc<Block>) {
         let parent_hash = block.header.previous_block_hash;
 
@@ -68,16 +73,18 @@ impl NonFinalizedState {
             })
             .expect("commit_block is only called with blocks that are ready to be commited");
 
-        parent_chain.push(block);
+        parent_chain.push(block.clone());
         self.chain_set.insert(parent_chain);
+        self.update_metrics_for_committed_block(block);
     }
 
     /// Commit block to the non-finalized state as a new chain where its parent
     /// is the finalized tip.
     pub fn commit_new_chain(&mut self, block: Arc<Block>) {
         let mut chain = Chain::default();
-        chain.push(block);
+        chain.push(block.clone());
         self.chain_set.insert(Box::new(chain));
+        self.update_metrics_for_committed_block(block);
     }
 
     /// Returns the length of the non-finalized portion of the current best chain.
@@ -93,6 +100,7 @@ impl NonFinalizedState {
     pub fn any_chain_contains(&self, hash: &block::Hash) -> bool {
         self.chain_set
             .iter()
+            .rev()
             .any(|chain| chain.height_by_hash.contains_key(hash))
     }
 
@@ -135,6 +143,21 @@ impl NonFinalizedState {
         None
     }
 
+    /// Returns the `block` with the given hash in the any chain.
+    pub fn block_by_hash(&self, hash: block::Hash) -> Option<Arc<Block>> {
+        for chain in self.chain_set.iter().rev() {
+            if let Some(block) = chain
+                .height_by_hash
+                .get(&hash)
+                .and_then(|height| chain.blocks.get(height))
+            {
+                return Some(block.clone());
+            }
+        }
+
+        None
+    }
+
     /// Returns the `block` at a given height or hash in the best chain.
     pub fn block(&self, hash_or_height: HashOrHeight) -> Option<Arc<Block>> {
         let best_chain = self.best_chain()?;
@@ -165,6 +188,17 @@ impl NonFinalizedState {
         Some(height)
     }
 
+    /// Returns the height of `hash` in any chain.
+    pub fn height_by_hash(&self, hash: block::Hash) -> Option<block::Height> {
+        for chain in self.chain_set.iter().rev() {
+            if let Some(height) = chain.height_by_hash.get(&hash) {
+                return Some(*height);
+            }
+        }
+
+        None
+    }
+
     /// Returns the given transaction if it exists in the best chain.
     pub fn transaction(&self, hash: transaction::Hash) -> Option<Arc<Transaction>> {
         let best_chain = self.best_chain()?;
@@ -181,4 +215,191 @@ impl NonFinalizedState {
             .next_back()
             .map(|box_chain| box_chain.deref())
     }
+
+    /// Update the metrics after `block` is committed
+    fn update_metrics_for_committed_block(&self, block: Arc<Block>) {
+        let height = block.coinbase_height().unwrap();
+
+        metrics::counter!("state.memory.committed.block.count", 1);
+        metrics::gauge!("state.memory.committed.block.height", height.0 as _);
+
+        if self
+            .best_chain()
+            .unwrap()
+            .blocks
+            .iter()
+            .next_back()
+            .unwrap()
+            .1
+            == &block
+        {
+            metrics::counter!("state.memory.best.committed.block.count", 1);
+            metrics::gauge!("state.memory.best.committed.block.height", height.0 as _);
+        }
+
+        self.update_metrics_for_chains();
+    }
+
+    /// Update the metrics after `self.chain_set` is modified
+    fn update_metrics_for_chains(&self) {
+        metrics::gauge!("state.memory.chain.count", self.chain_set.len() as _);
+        metrics::gauge!("state.memory.best.chain.length", self.best_chain_len() as _);
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use zebra_chain::serialization::ZcashDeserializeInto;
+    use zebra_test::prelude::*;
+
+    use crate::tests::FakeChainHelper;
+
+    use self::assert_eq;
+    use super::*;
+
+    #[test]
+    fn best_chain_wins() -> Result<()> {
+        zebra_test::init();
+
+        let block1: Arc<Block> =
+            zebra_test::vectors::BLOCK_MAINNET_419200_BYTES.zcash_deserialize_into()?;
+        let block2 = block1.make_fake_child().set_work(10);
+        let child = block1.make_fake_child().set_work(1);
+
+        let expected_hash = block2.hash();
+
+        let mut state = NonFinalizedState::default();
+        state.commit_new_chain(block2);
+        state.commit_new_chain(child);
+
+        let best_chain = state.best_chain().unwrap();
+        assert!(best_chain.height_by_hash.contains_key(&expected_hash));
+
+        Ok(())
+    }
+
+    #[test]
+    fn finalize_pops_from_best_chain() -> Result<()> {
+        zebra_test::init();
+
+        let block1: Arc<Block> =
+            zebra_test::vectors::BLOCK_MAINNET_419200_BYTES.zcash_deserialize_into()?;
+        let block2 = block1.make_fake_child().set_work(10);
+        let child = block1.make_fake_child().set_work(1);
+
+        let mut state = NonFinalizedState::default();
+        state.commit_new_chain(block1.clone());
+        state.commit_block(block2.clone());
+        state.commit_block(child);
+
+        let finalized = state.finalize();
+        assert_eq!(block1, finalized);
+
+        let finalized = state.finalize();
+        assert_eq!(block2, finalized);
+
+        assert!(state.best_chain().is_none());
+
+        Ok(())
+    }
+
+    #[test]
+    // This test gives full coverage for `take_chain_if`
+    fn commit_block_extending_best_chain_doesnt_drop_worst_chains() -> Result<()> {
+        zebra_test::init();
+
+        let block1: Arc<Block> =
+            zebra_test::vectors::BLOCK_MAINNET_419200_BYTES.zcash_deserialize_into()?;
+        let block2 = block1.make_fake_child().set_work(10);
+        let child1 = block1.make_fake_child().set_work(1);
+        let child2 = block2.make_fake_child().set_work(1);
+
+        let mut state = NonFinalizedState::default();
+        assert_eq!(0, state.chain_set.len());
+        state.commit_new_chain(block1);
+        assert_eq!(1, state.chain_set.len());
+        state.commit_block(block2);
+        assert_eq!(1, state.chain_set.len());
+        state.commit_block(child1);
+        assert_eq!(2, state.chain_set.len());
+        state.commit_block(child2);
+        assert_eq!(2, state.chain_set.len());
+
+        Ok(())
+    }
+
+    #[test]
+    fn shorter_chain_can_be_best_chain() -> Result<()> {
+        zebra_test::init();
+
+        let block1: Arc<Block> =
+            zebra_test::vectors::BLOCK_MAINNET_419200_BYTES.zcash_deserialize_into()?;
+
+        let long_chain_block1 = block1.make_fake_child().set_work(1);
+        let long_chain_block2 = long_chain_block1.make_fake_child().set_work(1);
+
+        let short_chain_block = block1.make_fake_child().set_work(3);
+
+        let mut state = NonFinalizedState::default();
+        state.commit_new_chain(block1);
+        state.commit_block(long_chain_block1);
+        state.commit_block(long_chain_block2);
+        state.commit_block(short_chain_block);
+        assert_eq!(2, state.chain_set.len());
+
+        assert_eq!(2, state.best_chain_len());
+
+        Ok(())
+    }
+
+    #[test]
+    fn longer_chain_with_more_work_wins() -> Result<()> {
+        zebra_test::init();
+
+        let block1: Arc<Block> =
+            zebra_test::vectors::BLOCK_MAINNET_419200_BYTES.zcash_deserialize_into()?;
+
+        let long_chain_block1 = block1.make_fake_child().set_work(1);
+        let long_chain_block2 = long_chain_block1.make_fake_child().set_work(1);
+        let long_chain_block3 = long_chain_block2.make_fake_child().set_work(1);
+        let long_chain_block4 = long_chain_block3.make_fake_child().set_work(1);
+
+        let short_chain_block = block1.make_fake_child().set_work(3);
+
+        let mut state = NonFinalizedState::default();
+        state.commit_new_chain(block1);
+        state.commit_block(long_chain_block1);
+        state.commit_block(long_chain_block2);
+        state.commit_block(long_chain_block3);
+        state.commit_block(long_chain_block4);
+        state.commit_block(short_chain_block);
+        assert_eq!(2, state.chain_set.len());
+
+        assert_eq!(5, state.best_chain_len());
+
+        Ok(())
+    }
+
+    #[test]
+    fn equal_length_goes_to_more_work() -> Result<()> {
+        zebra_test::init();
+
+        let block1: Arc<Block> =
+            zebra_test::vectors::BLOCK_MAINNET_419200_BYTES.zcash_deserialize_into()?;
+
+        let less_work_child = block1.make_fake_child().set_work(1);
+        let more_work_child = block1.make_fake_child().set_work(3);
+        let expected_hash = more_work_child.hash();
+
+        let mut state = NonFinalizedState::default();
+        state.commit_new_chain(block1);
+        state.commit_block(less_work_child);
+        state.commit_block(more_work_child);
+        assert_eq!(2, state.chain_set.len());
+
+        let tip_hash = state.tip().unwrap().1;
+        assert_eq!(expected_hash, tip_hash);
+
+        Ok(())
+    }
 }
diff --git a/zebra-state/src/service/memory_state/queued_blocks.rs b/zebra-state/src/service/memory_state/queued_blocks.rs
index d8e74839b..dc7721857 100644
--- a/zebra-state/src/service/memory_state/queued_blocks.rs
+++ b/zebra-state/src/service/memory_state/queued_blocks.rs
@@ -49,6 +49,7 @@ impl QueuedBlocks {
         assert!(inserted, "hashes must be unique");
 
         tracing::trace!(num_blocks = %self.blocks.len(), %parent_hash, ?new_height,  "Finished queueing a new block");
+        self.update_metrics();
     }
 
     /// Dequeue and return all blocks that were waiting for the arrival of
@@ -73,6 +74,7 @@ impl QueuedBlocks {
         }
 
         tracing::trace!(num_blocks = %self.blocks.len(), "Finished dequeuing blocks waiting for parent hash",);
+        self.update_metrics();
 
         queued_children
     }
@@ -99,10 +101,24 @@ impl QueuedBlocks {
                 .expect("parent is present")
                 .remove(&hash);
         }
+
+        tracing::trace!(num_blocks = %self.blocks.len(), "Finished pruning blocks at or beneath the finalized tip height",);
+        self.update_metrics();
     }
 
     /// Return the queued block if it has already been registered
     pub fn get_mut(&mut self, hash: &block::Hash) -> Option<&mut QueuedBlock> {
         self.blocks.get_mut(&hash)
     }
+
+    /// Update metrics after the queue is modified
+    fn update_metrics(&self) {
+        if let Some(max_height) = self.by_height.keys().next_back() {
+            metrics::gauge!("state.memory.queued.max.height", max_height.0 as i64);
+        } else {
+            // use -1 as a sentinel value for "None", because 0 is a valid height
+            metrics::gauge!("state.memory.queued.max.height", -1);
+        }
+        metrics::gauge!("state.memory.queued.block.count", self.blocks.len() as _);
+    }
 }
diff --git a/zebra-state/src/sled_state.rs b/zebra-state/src/sled_state.rs
index 4b3ce9e88..002f2b80e 100644
--- a/zebra-state/src/sled_state.rs
+++ b/zebra-state/src/sled_state.rs
@@ -38,6 +38,7 @@ use self::sled_format::TransactionLocation;
 pub struct FinalizedState {
     /// Queued blocks that arrived out of order, indexed by their parent block hash.
     queued_by_prev_hash: HashMap<block::Hash, QueuedBlock>,
+    max_queued_height: i64,
 
     hash_by_height: sled::Tree,
     height_by_hash: sled::Tree,
@@ -52,21 +53,13 @@ pub struct FinalizedState {
     debug_stop_at_height: Option<block::Height>,
 }
 
-/// Where is the stop check being performed?
-#[derive(Debug, Copy, Clone, Eq, PartialEq)]
-enum StopCheckContext {
-    /// Checking when the state is loaded
-    OnLoad,
-    /// Checking when a block is committed
-    OnCommit,
-}
-
 impl FinalizedState {
     pub fn new(config: &Config, network: Network) -> Self {
         let db = config.sled_config(network).open().unwrap();
 
         let new_state = Self {
             queued_by_prev_hash: HashMap::new(),
+            max_queued_height: -1,
             hash_by_height: db.open_tree(b"hash_by_height").unwrap(),
             height_by_hash: db.open_tree(b"height_by_hash").unwrap(),
             block_by_height: db.open_tree(b"block_by_height").unwrap(),
@@ -78,11 +71,32 @@ impl FinalizedState {
         };
 
         if let Some(tip_height) = new_state.finalized_tip_height() {
-            new_state.stop_if_at_height_limit(
-                StopCheckContext::OnLoad,
-                tip_height,
-                new_state.finalized_tip_hash(),
-            );
+            if new_state.is_at_stop_height(tip_height) {
+                let debug_stop_at_height = new_state
+                    .debug_stop_at_height
+                    .expect("true from `is_at_stop_height` implies `debug_stop_at_height` is Some");
+
+                let tip_hash = new_state.finalized_tip_hash();
+
+                if tip_height > debug_stop_at_height {
+                    tracing::error!(
+                        ?debug_stop_at_height,
+                        ?tip_height,
+                        ?tip_hash,
+                        "previous state height is greater than the stop height",
+                    );
+                }
+
+                tracing::info!(
+                    ?debug_stop_at_height,
+                    ?tip_height,
+                    ?tip_hash,
+                    "state is already at the configured height"
+                );
+
+                // There's no need to sync before exit, because the trees have just been opened
+                std::process::exit(0);
+            }
         }
 
         new_state
@@ -112,59 +126,17 @@ impl FinalizedState {
     /// Flushes sled trees before exiting.
     ///
     /// `called_from` and `block_hash` are used for assertions and logging.
-    fn stop_if_at_height_limit(
-        &self,
-        called_from: StopCheckContext,
-        block_height: block::Height,
-        block_hash: block::Hash,
-    ) {
+    fn is_at_stop_height(&self, block_height: block::Height) -> bool {
         let debug_stop_at_height = match self.debug_stop_at_height {
             Some(debug_stop_at_height) => debug_stop_at_height,
-            None => return,
+            None => return false,
         };
 
         if block_height < debug_stop_at_height {
-            return;
-        }
-
-        // this error is expected on load, but unexpected on commit
-        if block_height > debug_stop_at_height {
-            if called_from == StopCheckContext::OnLoad {
-                tracing::error!(
-                    ?debug_stop_at_height,
-                    ?called_from,
-                    ?block_height,
-                    ?block_hash,
-                    "previous state height is greater than the stop height",
-                );
-            } else {
-                unreachable!("committed blocks must be committed in order");
-            }
+            return false;
         }
 
-        // Don't sync when the trees have just been opened
-        if called_from == StopCheckContext::OnCommit {
-            if let Err(e) = self.flush() {
-                tracing::error!(
-                    ?e,
-                    ?debug_stop_at_height,
-                    ?called_from,
-                    ?block_height,
-                    ?block_hash,
-                    "error flushing sled state before stopping"
-                );
-            }
-        }
-
-        tracing::info!(
-            ?debug_stop_at_height,
-            ?called_from,
-            ?block_height,
-            ?block_hash,
-            "stopping at configured height"
-        );
-
-        std::process::exit(0);
+        true
     }
 
     /// Queue a finalized block to be committed to the state.
@@ -173,6 +145,7 @@ impl FinalizedState {
     /// queued block (and any of its descendants) can be committed to the state.
     pub fn queue_and_commit_finalized_blocks(&mut self, queued_block: QueuedBlock) {
         let prev_hash = queued_block.block.header.previous_block_hash;
+        let height = queued_block.block.coinbase_height().unwrap();
         self.queued_by_prev_hash.insert(prev_hash, queued_block);
 
         while let Some(queued_block) = self.queued_by_prev_hash.remove(&self.finalized_tip_hash()) {
@@ -181,12 +154,21 @@ impl FinalizedState {
                 .coinbase_height()
                 .expect("valid blocks must have a height");
             self.commit_finalized(queued_block);
-            metrics::counter!("state.committed.block.count", 1);
-            metrics::gauge!("state.committed.block.height", height.0 as _);
+            metrics::counter!("state.finalized.committed.block.count", 1);
+            metrics::gauge!("state.finalized.committed.block.height", height.0 as _);
+        }
+
+        if self.queued_by_prev_hash.is_empty() {
+            // use -1 as a sentinel value for "None", because 0 is a valid height
+            self.max_queued_height = -1;
+        } else {
+            self.max_queued_height = std::cmp::max(self.max_queued_height, height.0 as _);
         }
 
+        metrics::gauge!("state.finalized.queued.max.height", self.max_queued_height);
+
         metrics::gauge!(
-            "state.queued.block.count",
+            "state.finalized.queued.block.count",
             self.queued_by_prev_hash.len() as _
         );
     }
@@ -295,8 +277,19 @@ impl FinalizedState {
                 },
             );
 
-        if result.is_ok() {
-            self.stop_if_at_height_limit(StopCheckContext::OnCommit, height, hash);
+        if result.is_ok() && self.is_at_stop_height(height) {
+            if let Err(e) = self.flush() {
+                tracing::error!(
+                    ?e,
+                    ?height,
+                    ?hash,
+                    "error flushing sled state before stopping"
+                );
+            }
+
+            tracing::info!(?height, ?hash, "stopping at configured height");
+
+            std::process::exit(0);
         }
 
         result.map_err(Into::into)
diff --git a/zebrad/Cargo.toml b/zebrad/Cargo.toml
index cb5431780..d88d7d29a 100644
--- a/zebrad/Cargo.toml
+++ b/zebrad/Cargo.toml
@@ -41,6 +41,6 @@ inferno = { version = "0.10.1", default-features = false }
 
 [dev-dependencies]
 abscissa_core = { version = "0.5", features = ["testing"] }
-once_cell = "1.4"
+once_cell = "1.5"
 tempdir = "0.3.7"
 zebra-test = { path = "../zebra-test" }
diff --git a/zebrad/src/components/sync.rs b/zebrad/src/components/sync.rs
index 02f678e8f..07b3a459d 100644
--- a/zebrad/src/components/sync.rs
+++ b/zebrad/src/components/sync.rs
@@ -142,9 +142,6 @@ where
         // making a less-fallible network service, and the Hedge layer
         // tries to reduce latency of that less-fallible service.
         //
-        // We hedge every request. If we don't have any recent timings,
-        // Hedge issues a second request with no delay.
-        //
         // XXX add ServiceBuilder::hedge() so this becomes
         // ServiceBuilder::new().hedge(...).retry(...)...
         let block_network = Hedge::new(
@@ -154,7 +151,7 @@ where
                 .timeout(BLOCK_DOWNLOAD_TIMEOUT)
                 .service(peers),
             AlwaysHedge,
-            0,
+            20,
             0.95,
             2 * SYNC_RESTART_TIMEOUT,
         );
@@ -588,6 +585,8 @@ mod test {
     /// Make sure the timeout values are consistent with each other.
     #[test]
     fn ensure_timeouts_consistent() {
+        zebra_test::init();
+
         let max_download_retry_time =
             BLOCK_DOWNLOAD_TIMEOUT.as_secs() * (BLOCK_DOWNLOAD_RETRY_LIMIT as u64);
         assert!(


Test patch:
diff --git a/zebra-chain/src/block/tests/prop.rs b/zebra-chain/src/block/tests/prop.rs
index f202306fd..7cfee309e 100644
--- a/zebra-chain/src/block/tests/prop.rs
+++ b/zebra-chain/src/block/tests/prop.rs
@@ -12,6 +12,8 @@ use super::super::{serialize::MAX_BLOCK_BYTES, *};
 proptest! {
     #[test]
     fn block_hash_roundtrip(hash in any::<Hash>()) {
+        zebra_test::init();
+
         let bytes = hash.zcash_serialize_to_vec()?;
         let other_hash: Hash = bytes.zcash_deserialize_into()?;
 
@@ -20,6 +22,8 @@ proptest! {
 
     #[test]
     fn block_hash_display_fromstr_roundtrip(hash in any::<Hash>()) {
+        zebra_test::init();
+
         let display = format!("{}", hash);
         let parsed = display.parse::<Hash>().expect("hash should parse");
         prop_assert_eq!(hash, parsed);
@@ -27,6 +31,8 @@ proptest! {
 
     #[test]
     fn blockheader_roundtrip(header in any::<Header>()) {
+        zebra_test::init();
+
         let bytes = header.zcash_serialize_to_vec()?;
         let other_header = bytes.zcash_deserialize_into()?;
 
@@ -39,6 +45,8 @@ proptest! {
         network in any::<Network>(),
         block_height in any::<Height>()
     ) {
+        zebra_test::init();
+
         let root_hash = RootHash::from_bytes(bytes, network, block_height);
         let other_bytes = root_hash.to_bytes();
 
@@ -56,6 +64,8 @@ proptest! {
 
     #[test]
     fn block_roundtrip(block in any::<Block>(), network in any::<Network>()) {
+        zebra_test::init();
+
         let bytes = block.zcash_serialize_to_vec()?;
         let bytes = &mut bytes.as_slice();
 
diff --git a/zebra-chain/src/block/tests/vectors.rs b/zebra-chain/src/block/tests/vectors.rs
index 1dcfd9a8a..15a47e771 100644
--- a/zebra-chain/src/block/tests/vectors.rs
+++ b/zebra-chain/src/block/tests/vectors.rs
@@ -11,6 +11,8 @@ use super::generate; // XXX this should be rewritten as strategies
 
 #[test]
 fn blockheaderhash_debug() {
+    zebra_test::init();
+
     let preimage = b"foo bar baz";
     let mut sha_writer = sha256d::Writer::default();
     let _ = sha_writer.write_all(preimage);
@@ -25,6 +27,8 @@ fn blockheaderhash_debug() {
 
 #[test]
 fn blockheaderhash_from_blockheader() {
+    zebra_test::init();
+
     let blockheader = generate::block_header();
 
     let hash = Hash::from(&blockheader);
@@ -69,6 +73,8 @@ fn deserialize_blockheader() {
 
 #[test]
 fn deserialize_block() {
+    zebra_test::init();
+
     // this one has a bad version field
     zebra_test::vectors::BLOCK_MAINNET_434873_BYTES
         .zcash_deserialize_into::<Block>()
@@ -83,6 +89,8 @@ fn deserialize_block() {
 
 #[test]
 fn block_test_vectors_unique() {
+    zebra_test::init();
+
     let block_count = zebra_test::vectors::BLOCKS.len();
     let block_hashes: HashSet<_> = zebra_test::vectors::BLOCKS
         .iter()
@@ -103,11 +111,15 @@ fn block_test_vectors_unique() {
 
 #[test]
 fn block_test_vectors_height_mainnet() {
+    zebra_test::init();
+
     block_test_vectors_height(Network::Mainnet);
 }
 
 #[test]
 fn block_test_vectors_height_testnet() {
+    zebra_test::init();
+
     block_test_vectors_height(Network::Testnet);
 }
 
@@ -131,6 +143,8 @@ fn block_test_vectors_height(network: Network) {
 
 #[test]
 fn block_limits_multi_tx() {
+    zebra_test::init();
+
     // Test multiple small transactions to fill a block max size
 
     // Create a block just below the limit
@@ -166,6 +180,8 @@ fn block_limits_multi_tx() {
 
 #[test]
 fn block_limits_single_tx() {
+    zebra_test::init();
+
     // Test block limit with a big single transaction
 
     // Create a block just below the limit
@@ -213,6 +229,8 @@ fn node_time_check(
 
 #[test]
 fn time_check_now() {
+    zebra_test::init();
+
     // These checks are deteministic, because all the times are offset
     // from the current time.
     let now = Utc::now();
@@ -273,6 +291,8 @@ static BLOCK_HEADER_INVALID_TIMESTAMPS: &[i64] = &[
 
 #[test]
 fn time_check_fixed() {
+    zebra_test::init();
+
     // These checks are non-deterministic, but the times are all in the
     // distant past or far future. So it's unlikely that the test
     // machine will have a clock that makes these tests fail.
diff --git a/zebra-chain/src/parameters/tests.rs b/zebra-chain/src/parameters/tests.rs
index ad7345d60..33e5b6831 100644
--- a/zebra-chain/src/parameters/tests.rs
+++ b/zebra-chain/src/parameters/tests.rs
@@ -12,6 +12,8 @@ use NetworkUpgrade::*;
 /// Check that the activation heights and network upgrades are unique.
 #[test]
 fn activation_bijective() {
+    zebra_test::init();
+
     let mainnet_activations = NetworkUpgrade::activation_list(Mainnet);
     let mainnet_heights: HashSet<&block::Height> = mainnet_activations.keys().collect();
     assert_eq!(MAINNET_ACTIVATION_HEIGHTS.len(), mainnet_heights.len());
@@ -29,11 +31,13 @@ fn activation_bijective() {
 
 #[test]
 fn activation_extremes_mainnet() {
+    zebra_test::init();
     activation_extremes(Mainnet)
 }
 
 #[test]
 fn activation_extremes_testnet() {
+    zebra_test::init();
     activation_extremes(Testnet)
 }
 
@@ -84,11 +88,13 @@ fn activation_extremes(network: Network) {
 
 #[test]
 fn activation_consistent_mainnet() {
+    zebra_test::init();
     activation_consistent(Mainnet)
 }
 
 #[test]
 fn activation_consistent_testnet() {
+    zebra_test::init();
     activation_consistent(Testnet)
 }
 
@@ -119,6 +125,8 @@ fn activation_consistent(network: Network) {
 /// Check that the network upgrades and branch ids are unique.
 #[test]
 fn branch_id_bijective() {
+    zebra_test::init();
+
     let branch_id_list = NetworkUpgrade::branch_id_list();
     let nus: HashSet<&NetworkUpgrade> = branch_id_list.keys().collect();
     assert_eq!(CONSENSUS_BRANCH_IDS.len(), nus.len());
@@ -129,11 +137,13 @@ fn branch_id_bijective() {
 
 #[test]
 fn branch_id_extremes_mainnet() {
+    zebra_test::init();
     branch_id_extremes(Mainnet)
 }
 
 #[test]
 fn branch_id_extremes_testnet() {
+    zebra_test::init();
     branch_id_extremes(Testnet)
 }
 
@@ -165,11 +175,13 @@ fn branch_id_extremes(network: Network) {
 
 #[test]
 fn branch_id_consistent_mainnet() {
+    zebra_test::init();
     branch_id_consistent(Mainnet)
 }
 
 #[test]
 fn branch_id_consistent_testnet() {
+    zebra_test::init();
     branch_id_consistent(Testnet)
 }
 
diff --git a/zebra-chain/src/sapling/keys/tests.rs b/zebra-chain/src/sapling/keys/tests.rs
index 252eae0df..8b8f43228 100644
--- a/zebra-chain/src/sapling/keys/tests.rs
+++ b/zebra-chain/src/sapling/keys/tests.rs
@@ -37,6 +37,8 @@ mod tests {
 
     #[test]
     fn derive_for_each_test_vector() {
+        zebra_test::init();
+
         for test_vector in test_vectors::TEST_VECTORS.iter() {
             let spending_key = SpendingKey::from(test_vector.sk);
 
@@ -76,6 +78,8 @@ proptest! {
 
     #[test]
     fn string_roundtrips(spending_key in any::<SpendingKey>()) {
+        zebra_test::init();
+
         let sk_string = spending_key.to_string();
         let spending_key_2: SpendingKey = sk_string.parse().unwrap();
         prop_assert_eq![spending_key, spending_key_2];
diff --git a/zebra-chain/src/serialization/proptests.rs b/zebra-chain/src/serialization/proptests.rs
index dda4b7637..99f2ec166 100644
--- a/zebra-chain/src/serialization/proptests.rs
+++ b/zebra-chain/src/serialization/proptests.rs
@@ -11,6 +11,8 @@ proptest! {
 
     #[test]
     fn compactsize_write_then_read_round_trip(s in 0u64..0x2_0000u64) {
+        zebra_test::init();
+
         // Maximum encoding size of a compactsize is 9 bytes.
         let mut buf = [0u8; 8+1];
         Cursor::new(&mut buf[..]).write_compactsize(s).unwrap();
@@ -20,6 +22,8 @@ proptest! {
 
     #[test]
     fn compactsize_read_then_write_round_trip(bytes in prop::array::uniform9(0u8..)) {
+        zebra_test::init();
+
         // Only do the test if the bytes were valid.
         if let Ok(s) = Cursor::new(&bytes[..]).read_compactsize() {
             // The compactsize encoding is variable-length, so we may not even
diff --git a/zebra-chain/src/transaction/tests/prop.rs b/zebra-chain/src/transaction/tests/prop.rs
index 29548bbbf..97cf6fcf3 100644
--- a/zebra-chain/src/transaction/tests/prop.rs
+++ b/zebra-chain/src/transaction/tests/prop.rs
@@ -8,6 +8,8 @@ use crate::serialization::{ZcashDeserialize, ZcashDeserializeInto, ZcashSerializ
 proptest! {
     #[test]
     fn transaction_roundtrip(tx in any::<Transaction>()) {
+        zebra_test::init();
+
         let data = tx.zcash_serialize_to_vec().expect("tx should serialize");
         let tx2 = data.zcash_deserialize_into().expect("randomized tx should deserialize");
 
@@ -16,6 +18,8 @@ proptest! {
 
     #[test]
     fn transaction_hash_display_fromstr_roundtrip(hash in any::<Hash>()) {
+        zebra_test::init();
+
         let display = format!("{}", hash);
         let parsed = display.parse::<Hash>().expect("hash should parse");
         prop_assert_eq!(hash, parsed);
@@ -23,6 +27,8 @@ proptest! {
 
     #[test]
     fn locktime_roundtrip(locktime in any::<LockTime>()) {
+        zebra_test::init();
+
         let mut bytes = Cursor::new(Vec::new());
         locktime.zcash_serialize(&mut bytes)?;
 
diff --git a/zebra-chain/src/transaction/tests/vectors.rs b/zebra-chain/src/transaction/tests/vectors.rs
index 50265362b..62c585813 100644
--- a/zebra-chain/src/transaction/tests/vectors.rs
+++ b/zebra-chain/src/transaction/tests/vectors.rs
@@ -4,6 +4,8 @@ use crate::serialization::{ZcashDeserialize, ZcashSerialize};
 
 #[test]
 fn librustzcash_tx_deserialize_and_round_trip() {
+    zebra_test::init();
+
     let tx = Transaction::zcash_deserialize(&zebra_test::vectors::GENERIC_TESTNET_TX[..])
         .expect("transaction test vector from librustzcash should deserialize");
 
@@ -15,6 +17,8 @@ fn librustzcash_tx_deserialize_and_round_trip() {
 
 #[test]
 fn librustzcash_tx_hash() {
+    zebra_test::init();
+
     let tx = Transaction::zcash_deserialize(&zebra_test::vectors::GENERIC_TESTNET_TX[..])
         .expect("transaction test vector from librustzcash should deserialize");
 
@@ -29,6 +33,8 @@ fn librustzcash_tx_hash() {
 
 #[test]
 fn zip143_deserialize_and_round_trip() {
+    zebra_test::init();
+
     let tx1 = Transaction::zcash_deserialize(&zebra_test::vectors::ZIP143_1[..])
         .expect("transaction test vector from ZIP143 should deserialize");
 
@@ -50,6 +56,8 @@ fn zip143_deserialize_and_round_trip() {
 
 #[test]
 fn zip243_deserialize_and_round_trip() {
+    zebra_test::init();
+
     let tx1 = Transaction::zcash_deserialize(&zebra_test::vectors::ZIP243_1[..])
         .expect("transaction test vector from ZIP243 should deserialize");
 
diff --git a/zebra-chain/src/work/difficulty/tests/vectors.rs b/zebra-chain/src/work/difficulty/tests/vectors.rs
index a906b8e33..b1637f261 100644
--- a/zebra-chain/src/work/difficulty/tests/vectors.rs
+++ b/zebra-chain/src/work/difficulty/tests/vectors.rs
@@ -1,8 +1,8 @@
+use color_eyre::eyre::eyre;
 use color_eyre::eyre::Report;
-use std::sync::Arc;
 
-use crate::block::Block;
 use crate::serialization::ZcashDeserialize;
+use crate::{block::Block, parameters::NetworkUpgrade};
 
 use super::super::*;
 
@@ -36,7 +36,7 @@ fn debug_format() {
     );
     assert_eq!(
         format!("{:?}", ExpandedDifficulty(U256::one())),
-        "ExpandedDifficulty(\"0100000000000000000000000000000000000000000000000000000000000000\")"
+        "ExpandedDifficulty(\"0000000000000000000000000000000000000000000000000000000000000001\")"
     );
     assert_eq!(
         format!("{:?}", ExpandedDifficulty(U256::MAX)),
@@ -44,6 +44,7 @@ fn debug_format() {
     );
 
     assert_eq!(format!("{:?}", Work(0)), "Work(0x0, 0, -inf)");
+    assert_eq!(format!("{:?}", Work(1)), "Work(0x1, 1, 0.00000)");
     assert_eq!(
         format!("{:?}", Work(u8::MAX as u128)),
         "Work(0xff, 255, 7.99435)"
@@ -247,16 +248,21 @@ fn compact_bitcoin_test_vectors() {
 
 /// Test blocks using CompactDifficulty.
 #[test]
-#[spandoc::spandoc]
 fn block_difficulty() -> Result<(), Report> {
+    block_difficulty_for_network(Network::Mainnet)?;
+    block_difficulty_for_network(Network::Testnet)?;
+
+    Ok(())
+}
+
+#[spandoc::spandoc]
+fn block_difficulty_for_network(network: Network) -> Result<(), Report> {
     zebra_test::init();
 
-    let mut blockchain = Vec::new();
-    for b in zebra_test::vectors::BLOCKS.iter() {
-        let block = Arc::<Block>::zcash_deserialize(*b)?;
-        let hash = block.hash();
-        blockchain.push((block.clone(), block.coinbase_height().unwrap(), hash));
-    }
+    let block_iter = match network {
+        Network::Mainnet => zebra_test::vectors::MAINNET_BLOCKS.iter(),
+        Network::Testnet => zebra_test::vectors::TESTNET_BLOCKS.iter(),
+    };
 
     let diff_zero = ExpandedDifficulty(U256::zero());
     let diff_one = ExpandedDifficulty(U256::one());
@@ -267,15 +273,20 @@ fn block_difficulty() -> Result<(), Report> {
 
     let mut cumulative_work = PartialCumulativeWork::default();
     let mut previous_cumulative_work = PartialCumulativeWork::default();
-    for (block, height, hash) in blockchain {
-        /// SPANDOC: Calculate the threshold for block {?height}
+
+    for (&height, block) in block_iter {
+        let block =
+            Block::zcash_deserialize(&block[..]).expect("block test vector should deserialize");
+        let hash = block.hash();
+
+        /// SPANDOC: Calculate the threshold for block {?height, ?network}
         let threshold = block
             .header
             .difficulty_threshold
             .to_expanded()
             .expect("Chain blocks have valid difficulty thresholds.");
 
-        /// SPANDOC: Check the difficulty for block {?height, ?threshold, ?hash}
+        /// SPANDOC: Check the difficulty for block {?height, ?network, ?threshold, ?hash}
         {
             assert!(hash <= threshold);
             // also check the comparison operators work
@@ -284,7 +295,15 @@ fn block_difficulty() -> Result<(), Report> {
             assert!(hash < diff_max);
         }
 
-        /// SPANDOC: Check compact round-trip for block {?height}
+        /// SPANDOC: Check the PoWLimit for block {?height, ?network, ?threshold, ?hash}
+        {
+            // the consensus rule
+            assert!(threshold <= ExpandedDifficulty::target_difficulty_limit(network));
+            // check that ordering is transitive, we checked `hash <= threshold` above
+            assert!(hash <= ExpandedDifficulty::target_difficulty_limit(network));
+        }
+
+        /// SPANDOC: Check compact round-trip for block {?height, ?network}
         {
             let canonical_compact = threshold.to_compact();
 
@@ -292,7 +311,7 @@ fn block_difficulty() -> Result<(), Report> {
                        canonical_compact);
         }
 
-        /// SPANDOC: Check the work for block {?height}
+        /// SPANDOC: Check the work for block {?height, ?network}
         {
             let work = block
                 .header
@@ -317,6 +336,148 @@ fn block_difficulty() -> Result<(), Report> {
     Ok(())
 }
 
+/// Test that the genesis block threshold is PowLimit
+#[test]
+fn genesis_block_difficulty() -> Result<(), Report> {
+    genesis_block_difficulty_for_network(Network::Mainnet)?;
+    genesis_block_difficulty_for_network(Network::Testnet)?;
+
+    Ok(())
+}
+
+#[spandoc::spandoc]
+fn genesis_block_difficulty_for_network(network: Network) -> Result<(), Report> {
+    zebra_test::init();
+
+    let block = match network {
+        Network::Mainnet => zebra_test::vectors::MAINNET_BLOCKS.get(&0),
+        Network::Testnet => zebra_test::vectors::TESTNET_BLOCKS.get(&0),
+    };
+
+    let block = block.expect("test vectors contain the genesis block");
+    let block = Block::zcash_deserialize(&block[..]).expect("block test vector should deserialize");
+    let hash = block.hash();
+
+    /// SPANDOC: Calculate the threshold for the genesis block {?network}
+    let threshold = block
+        .header
+        .difficulty_threshold
+        .to_expanded()
+        .expect("Chain blocks have valid difficulty thresholds.");
+
+    /// SPANDOC: Check the genesis PoWLimit {?network, ?threshold, ?hash}
+    {
+        assert_eq!(threshold, ExpandedDifficulty::target_difficulty_limit(network),
+                   "genesis block difficulty thresholds must be equal to the PoWLimit");
+    }
+
+    Ok(())
+}
+
+/// Test that testnet minimum-difficulty blocks are valid
+#[test]
+#[spandoc::spandoc]
+fn testnet_minimum_difficulty() -> Result<(), Report> {
+    const MINIMUM_DIFFICULTY_HEIGHTS: &[block::Height] = &[
+        // block time gaps greater than 15 minutes (pre-Blossom)
+        block::Height(299_188),
+        block::Height(299_189),
+        block::Height(299_202),
+        // block time gaps greater than 7.5 minutes (Blossom and later)
+        block::Height(584_000),
+        // these 3 blocks have gaps greater than 7.5 minutes and less than 15 minutes
+        block::Height(903_800),
+        block::Height(903_801),
+        block::Height(1_028_500),
+    ];
+
+    for (&height, _block) in zebra_test::vectors::TESTNET_BLOCKS.iter() {
+        let height = block::Height(height);
+
+        /// SPANDOC: Do minimum difficulty checks for testnet block {?height}
+        if MINIMUM_DIFFICULTY_HEIGHTS.contains(&height) {
+            check_testnet_minimum_difficulty_block(height)?;
+        } else {
+            assert!(check_testnet_minimum_difficulty_block(height).is_err(),
+                   "all testnet minimum difficulty block test vectors must be tested by the unit tests. Hint: add the failing block to MINIMUM_DIFFICULTY_HEIGHTS");
+        }
+    }
+
+    Ok(())
+}
+
+/// Check that the testnet block at `height` is a testnet minimum difficulty
+/// block.
+#[spandoc::spandoc]
+fn check_testnet_minimum_difficulty_block(height: block::Height) -> Result<(), Report> {
+    let block = zebra_test::vectors::TESTNET_BLOCKS
+        .get(&height.0)
+        .expect("test vectors contain the specified minimum difficulty block height");
+    let block = Block::zcash_deserialize(&block[..]).expect("block test vector should deserialize");
+    let hash = block.hash();
+
+    /// SPANDOC: Check the testnet minimum difficulty start height {?height, ?hash}
+    if height < block::Height(299_188) {
+        Err(eyre!(
+            "the testnet minimum difficulty rule starts at block 299188"
+        ))?;
+    }
+
+    /// SPANDOC: Make sure testnet minimum difficulty blocks have large time gaps {?height, ?hash}
+    {
+        let previous_block = zebra_test::vectors::TESTNET_BLOCKS.get(&(height.0 - 1));
+        if previous_block.is_none() {
+            Err(eyre!(
+                "test vectors should contain the previous block for each minimum difficulty block"
+            ))?;
+        }
+
+        let previous_block = previous_block.unwrap();
+        let previous_block = Block::zcash_deserialize(&previous_block[..])
+            .expect("block test vector should deserialize");
+        let time_gap = block
+            .header
+            .time
+            .signed_duration_since(previous_block.header.time);
+
+        // zcashd requires a gap that's strictly greater than 6 times the target
+        // threshold, but ZIP-205 and ZIP-208 are ambiguous. See bug #1276.
+        match NetworkUpgrade::minimum_difficulty_spacing_for_height(Network::Testnet, height) {
+            None => Err(eyre!("the minimum difficulty rule is not active"))?,
+            Some(spacing) if (time_gap <= spacing) => Err(eyre!(
+                "minimum difficulty block times must be more than 6 target spacing intervals apart"
+            ))?,
+            _ => {}
+        };
+    }
+
+    // At this point, the current block has passed all the consensus rules that allow
+    // minimum-difficulty blocks. So it is *allowed* to be a minimum-difficulty block, but not
+    // *required* to be one. But at the moment, all test vectors with large gaps are minimum-difficulty
+    // blocks.
+
+    /// SPANDOC: Calculate the threshold for testnet block {?height, ?hash}
+    let threshold = block
+        .header
+        .difficulty_threshold
+        .to_expanded()
+        .expect("Chain blocks have valid difficulty thresholds.");
+
+    /// SPANDOC: Check that the testnet minimum difficulty is the PoWLimit {?height, ?threshold, ?hash}
+    {
+        assert_eq!(threshold, ExpandedDifficulty::target_difficulty_limit(Network::Testnet),
+                   "testnet minimum difficulty thresholds should be equal to the PoWLimit. Hint: Blocks with large gaps are allowed to have the minimum difficulty, but it's not required.");
+        // all blocks pass the minimum difficulty threshold, even if they aren't minimum
+        // difficulty blocks, because it's the lowest permitted difficulty
+        assert!(
+            hash <= ExpandedDifficulty::target_difficulty_limit(Network::Testnet),
+            "testnet minimum difficulty hashes must be less than the PoWLimit"
+        );
+    }
+
+    Ok(())
+}
+
 /// Test ExpandedDifficulty ordering
 #[test]
 #[spandoc::spandoc]
diff --git a/zebra-chain/src/work/tests/prop.rs b/zebra-chain/src/work/tests/prop.rs
index 3e285c2e3..6436b2335 100644
--- a/zebra-chain/src/work/tests/prop.rs
+++ b/zebra-chain/src/work/tests/prop.rs
@@ -9,6 +9,8 @@ use super::super::*;
 
 #[test]
 fn equihash_solution_roundtrip() {
+    zebra_test::init();
+
     proptest!(|(solution in any::<equihash::Solution>())| {
             let data = solution
                 .zcash_serialize_to_vec()
diff --git a/zebra-consensus/src/block/tests.rs b/zebra-consensus/src/block/tests.rs
index 49e5d9fad..82c8c0d14 100644
--- a/zebra-consensus/src/block/tests.rs
+++ b/zebra-consensus/src/block/tests.rs
@@ -135,6 +135,8 @@ async fn check_transcripts() -> Result<(), Report> {
 
 #[test]
 fn coinbase_is_first_for_historical_blocks() -> Result<(), Report> {
+    zebra_test::init();
+
     let block_iter = zebra_test::vectors::BLOCKS.iter();
 
     for block in block_iter {
@@ -151,6 +153,8 @@ fn coinbase_is_first_for_historical_blocks() -> Result<(), Report> {
 
 #[test]
 fn difficulty_is_valid_for_historical_blocks() -> Result<(), Report> {
+    zebra_test::init();
+
     difficulty_is_valid_for_network(Network::Mainnet)?;
     difficulty_is_valid_for_network(Network::Testnet)?;
 
@@ -177,6 +181,7 @@ fn difficulty_is_valid_for_network(network: Network) -> Result<(), Report> {
 
 #[test]
 fn difficulty_validation_failure() -> Result<(), Report> {
+    zebra_test::init();
     use crate::error::*;
 
     // Get a block in the mainnet, and mangle its difficulty field
@@ -229,7 +234,8 @@ fn difficulty_validation_failure() -> Result<(), Report> {
     // Validate the block
     let result = check::difficulty_is_valid(&block.header, Network::Mainnet, &height, &bad_hash)
         .unwrap_err();
-    let expected = BlockError::DifficultyFilter(height, bad_hash, difficulty_threshold);
+    let expected =
+        BlockError::DifficultyFilter(height, bad_hash, difficulty_threshold, Network::Mainnet);
     assert_eq!(expected, result);
 
     Ok(())
@@ -237,6 +243,8 @@ fn difficulty_validation_failure() -> Result<(), Report> {
 
 #[test]
 fn equihash_is_valid_for_historical_blocks() -> Result<(), Report> {
+    zebra_test::init();
+
     let block_iter = zebra_test::vectors::BLOCKS.iter();
 
     for block in block_iter {
@@ -253,6 +261,8 @@ fn equihash_is_valid_for_historical_blocks() -> Result<(), Report> {
 
 #[test]
 fn subsidy_is_valid_for_historical_blocks() -> Result<(), Report> {
+    zebra_test::init();
+
     subsidy_is_valid_for_network(Network::Mainnet)?;
     subsidy_is_valid_for_network(Network::Testnet)?;
 
@@ -283,6 +293,7 @@ fn subsidy_is_valid_for_network(network: Network) -> Result<(), Report> {
 
 #[test]
 fn coinbase_validation_failure() -> Result<(), Report> {
+    zebra_test::init();
     use crate::error::*;
 
     let network = Network::Mainnet;
@@ -355,6 +366,7 @@ fn coinbase_validation_failure() -> Result<(), Report> {
 
 #[test]
 fn founders_reward_validation_failure() -> Result<(), Report> {
+    zebra_test::init();
     use crate::error::*;
     use zebra_chain::transaction::Transaction;
 
@@ -398,6 +410,8 @@ fn founders_reward_validation_failure() -> Result<(), Report> {
 
 #[test]
 fn time_is_valid_for_historical_blocks() -> Result<(), Report> {
+    zebra_test::init();
+
     let block_iter = zebra_test::vectors::BLOCKS.iter();
     let now = Utc::now();
 
diff --git a/zebra-consensus/src/checkpoint/tests.rs b/zebra-consensus/src/checkpoint/tests.rs
index 6f2c99d8d..4b8eb8039 100644
--- a/zebra-consensus/src/checkpoint/tests.rs
+++ b/zebra-consensus/src/checkpoint/tests.rs
@@ -3,7 +3,7 @@
 use super::*;
 
 use super::types::Progress::*;
-use super::types::Target::*;
+use super::types::TargetHeight::*;
 
 use color_eyre::eyre::{eyre, Report};
 use futures::{future::TryFutureExt, stream::FuturesUnordered};
diff --git a/zebra-consensus/src/parameters/tests.rs b/zebra-consensus/src/parameters/tests.rs
index 9c5fc1eaa..ad10ad706 100644
--- a/zebra-consensus/src/parameters/tests.rs
+++ b/zebra-consensus/src/parameters/tests.rs
@@ -19,6 +19,7 @@ fn minimum_difficulty_testnet() {
 
 /// Test MinimumDifficulty
 fn minimum_difficulty(network: Network) {
+    zebra_test::init();
     use block::Height;
     use MinimumDifficulty::*;
 
diff --git a/zebra-state/src/service/tests.rs b/zebra-state/src/service/tests.rs
new file mode 100644
index 000000000..1acf15907
--- /dev/null
+++ b/zebra-state/src/service/tests.rs
@@ -0,0 +1,182 @@
+use std::sync::Arc;
+
+use futures::stream::FuturesUnordered;
+use tower::{util::BoxService, Service, ServiceExt};
+use zebra_chain::{
+    block::Block, parameters::Network, serialization::ZcashDeserializeInto, transaction,
+    transparent,
+};
+use zebra_test::{prelude::*, transcript::Transcript};
+
+use crate::{init, BoxError, Config, Request, Response};
+
+const LAST_BLOCK_HEIGHT: u32 = 10;
+
+async fn populated_state(
+    blocks: impl IntoIterator<Item = Arc<Block>>,
+) -> BoxService<Request, Response, BoxError> {
+    let requests = blocks
+        .into_iter()
+        .map(|block| Request::CommitFinalizedBlock { block });
+
+    let config = Config::ephemeral();
+    let network = Network::Mainnet;
+    let mut state = init(config, network);
+
+    let mut responses = FuturesUnordered::new();
+
+    for request in requests {
+        let rsp = state.ready_and().await.unwrap().call(request);
+        responses.push(rsp);
+    }
+
+    use futures::StreamExt;
+    while let Some(rsp) = responses.next().await {
+        rsp.expect("blocks should commit just fine");
+    }
+
+    state
+}
+
+async fn test_populated_state_responds_correctly(
+    mut state: BoxService<Request, Response, BoxError>,
+) -> Result<()> {
+    let blocks = zebra_test::vectors::MAINNET_BLOCKS
+        .range(0..=LAST_BLOCK_HEIGHT)
+        .map(|(_, block_bytes)| block_bytes.zcash_deserialize_into::<Arc<Block>>().unwrap());
+
+    for (ind, block) in blocks.into_iter().enumerate() {
+        let mut transcript = vec![];
+        let height = block.coinbase_height().unwrap();
+        let hash = block.hash();
+
+        transcript.push((
+            Request::Block(hash.into()),
+            Ok(Response::Block(Some(block.clone()))),
+        ));
+
+        transcript.push((
+            Request::Block(height.into()),
+            Ok(Response::Block(Some(block.clone()))),
+        ));
+
+        transcript.push((
+            Request::Depth(block.hash()),
+            Ok(Response::Depth(Some(LAST_BLOCK_HEIGHT - height.0))),
+        ));
+
+        if ind == LAST_BLOCK_HEIGHT as usize {
+            transcript.push((Request::Tip, Ok(Response::Tip(Some((height, hash))))));
+        }
+
+        // Consensus-critical bug in zcashd: transactions in the genesis block
+        // are ignored.
+        if height.0 != 0 {
+            for transaction in &block.transactions {
+                let transaction_hash = transaction.hash();
+
+                transcript.push((
+                    Request::Transaction(transaction_hash),
+                    Ok(Response::Transaction(Some(transaction.clone()))),
+                ));
+
+                for (index, output) in transaction.outputs().iter().enumerate() {
+                    let outpoint = transparent::OutPoint {
+                        hash: transaction_hash,
+                        index: index as _,
+                    };
+
+                    transcript.push((
+                        Request::AwaitUtxo(outpoint),
+                        Ok(Response::Utxo(output.clone())),
+                    ));
+                }
+            }
+        }
+
+        let transcript = Transcript::from(transcript);
+        transcript.check(&mut state).await?;
+    }
+
+    Ok(())
+}
+
+#[tokio::main]
+async fn populate_and_check(blocks: Vec<Arc<Block>>) -> Result<()> {
+    let state = populated_state(blocks).await;
+    test_populated_state_responds_correctly(state).await?;
+    Ok(())
+}
+
+fn out_of_order_committing_strategy() -> BoxedStrategy<Vec<Arc<Block>>> {
+    let blocks = zebra_test::vectors::MAINNET_BLOCKS
+        .range(0..=LAST_BLOCK_HEIGHT)
+        .map(|(_, block_bytes)| block_bytes.zcash_deserialize_into::<Arc<Block>>().unwrap())
+        .collect::<Vec<_>>();
+
+    Just(blocks).prop_shuffle().boxed()
+}
+
+#[tokio::test]
+async fn empty_state_still_responds_to_requests() -> Result<()> {
+    zebra_test::init();
+
+    let block =
+        zebra_test::vectors::BLOCK_MAINNET_419200_BYTES.zcash_deserialize_into::<Arc<Block>>()?;
+
+    let iter = vec![
+        // No checks for CommitBlock or CommitFinalizedBlock because empty state
+        // precondition doesn't matter to them
+        (Request::Depth(block.hash()), Ok(Response::Depth(None))),
+        (Request::Tip, Ok(Response::Tip(None))),
+        (Request::BlockLocator, Ok(Response::BlockLocator(vec![]))),
+        (
+            Request::Transaction(transaction::Hash([0; 32])),
+            Ok(Response::Transaction(None)),
+        ),
+        (
+            Request::Block(block.hash().into()),
+            Ok(Response::Block(None)),
+        ),
+        (
+            Request::Block(block.coinbase_height().unwrap().into()),
+            Ok(Response::Block(None)),
+        ),
+        // No check for AwaitUTXO because it will wait if the UTXO isn't present
+    ]
+    .into_iter();
+    let transcript = Transcript::from(iter);
+
+    let config = Config::ephemeral();
+    let network = Network::Mainnet;
+    let state = init(config, network);
+
+    transcript.check(state).await?;
+
+    Ok(())
+}
+
+#[test]
+fn state_behaves_when_blocks_are_committed_in_order() -> Result<()> {
+    zebra_test::init();
+
+    let blocks = zebra_test::vectors::MAINNET_BLOCKS
+        .range(0..=LAST_BLOCK_HEIGHT)
+        .map(|(_, block_bytes)| block_bytes.zcash_deserialize_into::<Arc<Block>>().unwrap())
+        .collect();
+
+    populate_and_check(blocks)?;
+
+    Ok(())
+}
+
+#[test]
+fn state_behaves_when_blocks_are_committed_out_of_order() -> Result<()> {
+    zebra_test::init();
+
+    proptest!(|(blocks in out_of_order_committing_strategy())| {
+        populate_and_check(blocks).unwrap();
+    });
+
+    Ok(())
+}
diff --git a/zebra-state/src/tests.rs b/zebra-state/src/tests.rs
index 26af6df2f..86ea33237 100644
--- a/zebra-state/src/tests.rs
+++ b/zebra-state/src/tests.rs
@@ -1,7 +1,70 @@
-use zebra_chain::block;
+use std::{convert::TryFrom, mem, sync::Arc};
+
+use primitive_types::U256;
+use zebra_chain::{
+    block::{self, Block},
+    transaction::Transaction,
+    transparent,
+    work::difficulty::ExpandedDifficulty,
+    work::difficulty::Work,
+};
 
 use super::*;
 
+/// Helper trait for constructing "valid" looking chains of blocks
+pub trait FakeChainHelper {
+    fn make_fake_child(&self) -> Arc<Block>;
+
+    fn set_work(self, work: u128) -> Arc<Block>;
+}
+
+impl FakeChainHelper for Arc<Block> {
+    fn make_fake_child(&self) -> Arc<Block> {
+        let parent_hash = self.hash();
+        let mut child = Block::clone(self);
+        let mut transactions = mem::take(&mut child.transactions);
+        let mut tx = transactions.remove(0);
+
+        let input = match Arc::make_mut(&mut tx) {
+            Transaction::V1 { inputs, .. } => &mut inputs[0],
+            Transaction::V2 { inputs, .. } => &mut inputs[0],
+            Transaction::V3 { inputs, .. } => &mut inputs[0],
+            Transaction::V4 { inputs, .. } => &mut inputs[0],
+        };
+
+        match input {
+            transparent::Input::Coinbase { height, .. } => height.0 += 1,
+            _ => panic!("block must have a coinbase height to create a child"),
+        }
+
+        child.transactions.push(tx);
+        child.header.previous_block_hash = parent_hash;
+
+        Arc::new(child)
+    }
+
+    fn set_work(mut self, work: u128) -> Arc<Block> {
+        let work: U256 = work.into();
+        let expanded = work_to_expanded(work);
+
+        let block = Arc::make_mut(&mut self);
+        block.header.difficulty_threshold = expanded.into();
+        self
+    }
+}
+
+fn work_to_expanded(work: U256) -> ExpandedDifficulty {
+    // Work is calculated from expanded difficulty with the equation `work = 2^256 / (expanded + 1)`
+    // By balancing the equation we get `expanded = (2^256 / work) - 1`
+    // `2^256` is too large to represent, so we instead use the following equivalent equations
+    // `expanded = (2^256 / work) - (work / work)`
+    // `expanded = (2^256 - work) / work`
+    // `expanded = ((2^256 - 1) - work + 1) / work`
+    // `(2^256 - 1 - work)` is equivalent to `!work` when work is a U256
+    let expanded = (!work + 1) / work;
+    ExpandedDifficulty::from(expanded)
+}
+
 /// Block heights, and the expected minimum block locator height
 static BLOCK_LOCATOR_CASES: &[(u32, u32)] = &[
     (0, 0),
@@ -15,9 +78,25 @@ static BLOCK_LOCATOR_CASES: &[(u32, u32)] = &[
     (10000, 9901),
 ];
 
+use proptest::prelude::*;
+
+#[test]
+fn round_trip_work_expanded() {
+    zebra_test::init();
+
+    proptest!(|(work_before in any::<Work>())| {
+        let work: U256 = work_before.as_u128().into();
+        let expanded = work_to_expanded(work);
+        let work_after = Work::try_from(expanded).unwrap();
+        prop_assert_eq!(work_before, work_after);
+    });
+}
+
 /// Check that the block locator heights are sensible.
 #[test]
 fn test_block_locator_heights() {
+    zebra_test::init();
+
     for (height, min_height) in BLOCK_LOCATOR_CASES.iter().cloned() {
         let locator = util::block_locator_heights(block::Height(height));
 
diff --git a/zebra-test/src/lib.rs b/zebra-test/src/lib.rs
index 276369f20..46919611a 100644
--- a/zebra-test/src/lib.rs
+++ b/zebra-test/src/lib.rs
@@ -1,4 +1,8 @@
 //! Miscellaneous test code for Zebra.
+
+// Each lazy_static variable uses additional recursion
+#![recursion_limit = "256"]
+
 use color_eyre::section::PanicMessage;
 use owo_colors::OwoColorize;
 use tracing_error::ErrorLayer;
diff --git a/zebra-test/src/transcript.rs b/zebra-test/src/transcript.rs
index 8c9a2d447..feea1b0c8 100644
--- a/zebra-test/src/transcript.rs
+++ b/zebra-test/src/transcript.rs
@@ -51,6 +51,7 @@ impl TransError {
 #[error("ErrorChecker Error: {0}")]
 struct ErrorCheckerError(Error);
 
+#[must_use]
 pub struct Transcript<R, S, I>
 where
     I: Iterator<Item = (R, Result<S, TransError>)>,
@@ -58,12 +59,14 @@ where
     messages: I,
 }
 
-impl<R, S, I> From<I> for Transcript<R, S, I>
+impl<R, S, I> From<I> for Transcript<R, S, I::IntoIter>
 where
-    I: Iterator<Item = (R, Result<S, TransError>)>,
+    I: IntoIterator<Item = (R, Result<S, TransError>)>,
 {
     fn from(messages: I) -> Self {
-        Self { messages }
+        Self {
+            messages: messages.into_iter(),
+        }
     }
 }
 
diff --git a/zebra-test/src/vectors/block-test-0-299-187.txt b/zebra-test/src/vectors/block-test-0-299-187.txt
new file mode 100644
index 000000000..4441bb5e3
--- /dev/null
+++ b/zebra-test/src/vectors/block-test-0-299-187.txt
@@ -0,0 +1,1 @@
+040000001e8745663d7183b438bd0e37a9d350f2a5d586e8f3dc2fbe98315f1fd8010000198843867743542ebd4d76f413b706d110c4d8945646ad53d1b9fe114f6c33b62155aa92b3ea1ce41bc92a1c10a7db4893ff92e9a4b3a08694ecf7f999df1568c100b65bd5b4021edf027a1d050f0000000000000000000000000000000000000000000000000003fd4005019a418d043a8857e2cf73f71c364f7e7778fcbd4a088a6c7eca146a06b77c713f4f26fbcd876af67529057c04756ddf1f5b7a11453a033d17decd181eeb911dcecf423665667bcfe5766aa041c379a36bdad14608cb1529cbc6708c5899535335ec29ceaf8b5da1ea2bde01630ef23d23bf479398a0f4674a2252f43dc0148a2b028a93d008f828244eada4fd65afd7f73f27368b2f145093c459048f0cb470ee120781b13f5c8301e55ca0bdd20935032342c7ee50d59f85987ded5f0ca644f46d96b5abd49ce4f4165d4a1a6d3b3a324004b3125f4f36f64ff6c8f2f6fdd2780947531348bf0e33aab7e3679463da53832735377f88ff5e743c4c10719b0e5fd6e3d37073ea10a5fe0da3aaf1de843a74d81c780aa2c58fc98319620f6ce8b679f974167510aae242339137a4c4df13f907c39691065d11ec8f68dcb56e3d1e913dee9196c872c4faf344a0befd29025e653b10af53eb8900bb7c196581ef4c4ede8bea1fbc27d0836679fdb20bd43e4d50b93128174aaaf204c5fe8b176a262b62af57f4a8e62cb5ff90d36b370880c15e1ddfa629370ee1324bb097785c5a3286b10dd884b3204bd6ef13036276aff1fdc4f03a9b12d0278015348df0c5b99235b2d4cd3898163adc3a314a20b7c56dd612ccc1b805b92cd4db6def54a75ee6552caeae4690d724e5e3ab5355983e64a6313b5bff39042412fdb6e3b6a7394f466f566e8fa2dedc1c3fdd1d0153653c2a7bf7a17dc333cab478116f7d58f18909dc420981cfc829ee80a3bb4bcaa04b420fdb86fd1d014e57cd9731a595ed28e2a9ec8f225fe7fef12b0df9bfaa614d2e5fd08066c5e5e873a67bd8bca26255e74ac00d562d833f9cf6aca8c999163ee5df347a132d964f0916b7853019421b139339e58653d25cf82b1e9c94bc749813f063546553a900c27530d661ec019e005f5928a147bd6ae9a279d554626c667951701926da3262743b29e186031baaa3ab5a0ea514c801044cc25bf851a2e4c5d8c0a3d35aeafd52ccb2a4351308f33a224ae532873173fc15c7a8765f6bde0c4701c4d484edc8e2cf36a3567ab2c4554da69a8f87ab0fa12f6f2bce791edbb86162db8d3006fc921854270ee3f945560e6fcd47ad22fa89631131831ed5fd2a14618c380448ea138aa0f5260e3dbb176a563bbe330452a3a8b523dc5da993c502236d5506c9e457ecb1070261b72e2704d9820b10b2cb463a771886bf5c171bd186c79d19eb0919ed95d1d069b066e29d1b554e2a90e34d948bfd6b68d526453869484a4ebcd3f2de0499d7db67028f75546e76d22dcacb32992a3f134f0a5e4badb61174075a0b30f4f13a2c94b806cfca790c6000e90221a68b60b6e26698a224ef70377ed76519097dc8fa661e959f6155384363f66dc0d71a7c0101b265e75f45fd8c92a7809ef4d98d1ca2fb6c68740a6783d1a40558d65ad615aa70d8bc122799ff3b721e668424308e21edbaa962640b77532db840b5e16d33622299fce38a6fc90a7420e7c1c2717f67f76aef068c2c12f4656deb3769122103bf509ddd835ef6d42b98d6e433188cad06dda82ede47a022c468ddf6680c35aca592448a4927c4b340bea53bcd1307ab3f9b54fbfe11ad72e409d614d6d5174adb51c221d6dc8f048c6540214ce6aaef45d414c6596cc58032afaf5357862c18c4dfeb0b6f90982f3dc6e85254e557bd1e0bb8fc0d8f97c9fbee80f1e3a5c4ee44db6e8faa79129228a2748bc713f6b9e6981ee9930ec687176c36068829715cf14ecda56f0265b63efce5575faf037723955bb039a632b3e8f796826cbfcaa2b99618e9fc2bb1c1818fe1b24f20ec35b0e27bb6e9828e6f649649c1242a969f6a3dfa314556bdf4ee9963f6ee1b33010400008085202f89010000000000000000000000000000000000000000000000000000000000000000ffffffff0503b3900400ffffffff0200ca9a3b000000001976a9144bb9f3f3de8da40922a1cdb0775a2956289e285188ac80b2e60e0000000017a914fd79e0c84acac223142cf503dc6afde24acac50e8700000000000000000000000000000000000000
diff --git a/zebra-test/src/vectors/block-test-0-299-188.txt b/zebra-test/src/vectors/block-test-0-299-188.txt
new file mode 100644
index 000000000..251b7db91
--- /dev/null
+++ b/zebra-test/src/vectors/block-test-0-299-188.txt
@@ -0,0 +1,1 @@
+040000004ff18b3a2ea1ba2caa9c6b310ec8bfa1e7106aca4a0a176878f4618d190100007404a23449657413177db10158ff2fbeb9b31c5134cdf12f63b47bcf551b297c2155aa92b3ea1ce41bc92a1c10a7db4893ff92e9a4b3a08694ecf7f999df1568d80db65bffff0720020095e608a506f5b7f58f4c990cd8a86a806df95237b950e2a58917033e0000fd400500416392a1daa1676380a275031c2221e11731877913037f57cb8d2af9cc5cb57d4870764580093a5489071e8418e5c22ed7a626a1066310087e7a36979d500fb4f7325dcaeba2960a4627d4d764ae3d5494235301b58f1a8ad6a5c4d8c143d8db5113ab01e45d8bf212dca36bf461f8312b4429bdaff5605ee74cbbd8a2037075bde0a5142942af2708e8dcdd1247db17c01a206251b22c1bbfd1ea50b8d2805d87f325e7dfb6600366a0512092615cb429b57974e174b5ef1254da590cafba687f0796fe7c1771d88c1d38208e86ca19f709afb3729528de41dc1ae63942b9d47240ee3e19ef26bad54223d881152e4c04e4bc3a9ead5fb32ec895065d906759d2b343b93a1a45296299971c1ffb4620198e4de52148f24a5c791b2b7b65a2f749b0fcc7e80819ab0ce2ce9af3e7728148ed1063c10ccbab82ee417c54ba6451f4f4e7dbe975ad5216179c031d0b6003de9c34c38242929d5eb16cf81f27ecc0ae8caeba1fc61a230a0e7a70ab394203c05be239acdf3c9def0b7fc7cb450d8228f5b19242bea4370da46e7f70d71216dd297dd9d8cbec7ed20a6fb2c8b1b5c493c54b06777ad327cf6ce746866173d75db6f47b6bd3d5610be0e947ebca12709791612242b37028734a18a85214f18e7b0c0a9f4e8625a24439b642bcf95937aa481ebc9100e7b11ff9f83aa3a52db30c7603685f9ff905826f1e09c250789a43911ac0cd358ad9bddd682222de5c8a35244201445082ab4e414baa22b97824170f24cc255f116c30997fd1520ec2fb4a357bb7f0c92f99c9a3bb2d233b7c82dc2a216885439049bcd3aa08c861357686be94a93a130b4cfb8dc928ba1401960b4b2b5a48777173d765c3fb9dcd2465fa043525c31b3b316e5e87847b7fab2a3a667d63029461b6f68923d37edd481d7ceff4b454e5eb383c8dc348f8690c008355bc60cde307a9936097deeea7bdf975f3c41e4cafb27a289cc3b4fc13c83653f80d8afb1bdb5b540d05569363d19c10ab3d021c07ef6b757bdb37d9d4284cdc35f1e9b79fdf2e52cf30d37d41571515d5b20fcff97f24449d1e49ec72712b7d9f14d41d517b9120e20419ed912c00f9feb52d8a2fe3cabda2bf17111136e221e856866f0cba71f0e97caf41c1049e366024e543d74a619561759447e5e6799da6f71b5c56810978925aae5b12acdf9612e308272bae838f55864610ae1c716e4e53dd191f83f3752c4e9d758959a1eb0a2fc71f66a0bf65dc78c4423dbd477650cf7a50c626682ace0c9c84498586a30db6a466ce553315a31b09cd27a64f107d25a2a866526b5924f6207f765c730c1ecb3bfa2c288976f022259b241166698495d071312beda08ccdc39b68eb2471d171c5721a4ffa040a3f811fc9856ddc9d7ca4d4a6b3dffe393a80f81879023bf3b3450a96dac495e0d881a2042dd0d1bc84df1041b661918df82a9f05a24fa26fc1dd58aa1246e711bfa0c7302c360d913844bd005979ea0f77bad27e1ac8ccf86c2035af12054355a9bd5a9dcb029d503e0cc68f9047a29025ea16f6e37374e39e25d37763ba1ab4b4af73c99f92b108d46ad37cbe15f76ddae8b10fa49c7f56a2959bd607d3bcaefce5ed0f779651784d8175c74ffc569bfeab77de7fc6f70356173bd0f1027405231f24695d9c8e9605c74683daf2483ab2b4369cc2f4284f09b6f799875d9ccf6f333a1fde9b5906659f1a4bc586beffbaf1863dbf5976774ddcc0d20f0fb29ff36719bbbc9c96f57c75e94ece305c10f1047ad4232f6612a1345e4125c69bffa689a7570b03244024efa79fd4bda566936db67d7471028bcc01f718374260441123bbc010d185daed23190da9aadbde1e4ce5b2532f8c15b568043aaff82665afc21a6b7e010400008085202f89010000000000000000000000000000000000000000000000000000000000000000ffffffff0603b490040101ffffffff0200ca9a3b000000001976a9144fcef3cd1d27436e43c2ca7e6ab3317e08a57f9c88ac80b2e60e0000000017a914fd79e0c84acac223142cf503dc6afde24acac50e8700000000000000000000000000000000000000
diff --git a/zebra-test/src/vectors/block-test-0-299-189.txt b/zebra-test/src/vectors/block-test-0-299-189.txt
new file mode 100644
index 000000000..1e1f53652
--- /dev/null
+++ b/zebra-test/src/vectors/block-test-0-299-189.txt
@@ -0,0 +1,1 @@
+04000000c338ba3aaea3919c3f1077aa4d636553ac6fee63f6e212e083a043af8c87ed003972d7e8c6d0a7ca7999bcc9851fc88bef0ade38849d62553224699d9f9b09502155aa92b3ea1ce41bc92a1c10a7db4893ff92e9a4b3a08694ecf7f999df15682812b65bffff072001003d67059d9f40d0786370f13b4be590f7fd908f26e496b9f79fc1c0d20000fd4005002965c0648a4c808db486b8be3aa075c6e1d347ed3f749bfb83357687f29a94bbb1c6c969b1e3191b630b13531f9af7a7d1e352820705cd08fd92767570372449073899980fefd91ad4cf193ccae57179eddb1001087356cb4f2a7351f30434227a412b260ddec5c6280cd61d4b8d6a72f175955a3671cc4605ddf592320e53c6e459a461efadc6041367ad539e0a4315e0a01f3a2761500998db93200590bbe2ea5d828a51b9e405225cdb188f216ece785061d14aea958a6f9564251a56710b0669ef25c8c5b887d85dd08b0dee7e9f94055ded81120866dd5a96b2b226cfa44a942595bf2c2577c9d40bf75753da5e55b82855d8cf02fff90d5a13ba6eaff21a5dc340c4133c4bb0f0c598183a7b366dd0950408ac5a71afa6cb79cbfd0dbae80abf6b65243a8eb2bba10b2d950572a259f6ffa58dd31c4d1b3730255c86d826636a22247e846a58aa8b0e7fd2e702e654e733816b9d0484b0def90e65f22122db66cb2ebb5fd7f6fdfbe9fc8bf94d7bd97c26c6253c0bd516da337a7598e41fdec6b4fc58f1e16a312656389b1b2279c4f06210bf8af104ff974afecaeb01ba765304c9def7610e2e619c09a3b8154214c124aa7f3b742795164f4069359f5aeb66a41c48ed41f193b766140d318b3d99d51e6759882559a858b3ad5a9fb7f1480da13313845f3edba6c2b3c9f371bc39d5c0b89bcc07a8a1e0c3d536db745055d1e5476bf9d1eebdc8d40aa4a3bb2e8c0c3282f41408a9c1a20e323c7a574f0a177bdf2e57d019d44b12063fac806671a67a9f3724854f5b950a990483d2e4effbb4ba7d467a50ae710c86117ec21bb1694b15f27ccc5d7ce9721bf485601c7211d9b6ded419de28a470673be607045e3f289028aa61bf554af8635cdd929c1ef333c692c31e8cf03026b76984e68acf8c38e49a495f2b85418cdb3ad400faf27e6f32d2fbeb402501d3caf0b14c594edd4a0132bad7dc5c979962b832c6313c6bf54b3a9293cb13390abc08114c2b6e0e630064c49831a9437ccc7728b08ceabf4ded5fb290b4b388c8e04ed5a0fefad907a4cac56cc5dddf98780746a85cf0da74a9d9567f1964730cba8f2c8d1a9227a2dac080369b2e1fe5f50de477b21cd5315938e7e1c84d2a812a7075168cee1bab20e099e361f7b4fea6306bfa85fe178c592efe0131ed941bdfc0b13ada93514ee1e6d10e019925b92723cc0c100e24e1d0299444d34a72213dcc3ba91b02da7aec4556df235e14d2cb4c6f11294eb51e9bd61ff03f25321f703f3118c84ae4d198fa667355544a079415de89145de90ae7f4459b45e90259131321d20ce386e35d52ae2d1daaa388f24b10226e20ff14af09cd64a07de13aad45a2f1f0b6b82c7a574fb4e4a628dbed9431afebef9fc184598a597d52806b14b6570114dc994a848b7a8f2a1291b974f0bcdb4c913b213205caca902e4477ff59f41672eca90da671386d6d0e662f0c8b8a28e30f0fb3a2af5258a5ebb29b6be03bbf17cb68e7cb217d2d18a18df91b8e57575d4c9302c4d2e76151c441ae66c37e852a84266ba1da594b65beacdc1721cf37f8fd866c26704246587d3880570382a8978d0b20dd493d10e5dad9b9634b761f5b7112a4b622214c4b5b6b27b20076dca1e6252e17f77702cac763e011507ed51a12779feb5b5b0909ff80990e3ad42d8badcdef7a5e46f9287b786e0fe31dff61181855a7f40cee81189db2702a53d9a0bcb50bcf4225af3fd8b0cc1460ebe4c6609646227dbda5784d050b14759d44f5636fbe61a446b5ca0cd5c2605dfe4c20fedf440169b29996a9b4189fdc116937dc73f9a315b3153d0c17bfa9ea0b2293cb9c29655c986e19fd1f61a4ddde31ebb3c6778270a4aca1617476ad7f3b010400008085202f89010000000000000000000000000000000000000000000000000000000000000000ffffffff0603b590040101ffffffff0200ca9a3b000000001976a9149b4e02e6cd875f8adfa2167018531c4b4e0e0a6188ac80b2e60e0000000017a914fd79e0c84acac223142cf503dc6afde24acac50e8700000000000000000000000000000000000000
diff --git a/zebra-test/src/vectors/block-test-0-299-201.txt b/zebra-test/src/vectors/block-test-0-299-201.txt
new file mode 100644
index 000000000..20ff934e2
--- /dev/null
+++ b/zebra-test/src/vectors/block-test-0-299-201.txt
@@ -0,0 +1,1 @@
+04000000489c76c5bef8b4c9cefd35e4af11438d5dca5c10ed5a7a44bfbc343e164b600094f9fd3a15efdedad8664f543ccb1641205d457ad43573221792870c6c6beba82155aa92b3ea1ce41bc92a1c10a7db4893ff92e9a4b3a08694ecf7f999df15683014b65bfdd30220160033627b85fd00348671bea53e5f13279e9461e077f3b0510e737397b60000fd4005018a2752c205a73b16b0a513e5f8b8ca0d48d40c4638ae02c97f386111f854d57bb1640336c948578324120fe60fd7b3de49e29b2afe86eb6b07ed6a3fd7ca30773491880c268ce3996493987ef9da588a3e477d16c67da902976ca12909c350f19bc089c3db5ca4921e779f41a4c85e7ba459a560a4364c4adbe97cdf0b178a61c7df511053cce26610826da5860841956ff057a77324cbd9f46b3e97169a7ffb1f465a5dff635b0b212515280812235e98a495497b5a25ca57d76c812718f9d0725ae331fccb95efa35fe7c21e7ede78af1a4502dbeba20adfff76066eb4d4724f32867998352235a9c60f1c7fb7f60612d03e54f8c0b53fa94eb318bfbd548a064f154237477849d9866e6d8b73928f5ecaf7d466e820f7e377298d26daeb4e8da2950e7b2c39e9aee14f2f7de92b74c3917e522d9c6033e90a3230541ee59163689ba5755d6cc6674b002a78064002b7a3b92daa687993d2f11aaa3162af25051dc0af228e7f94d057b0f9af6ec66d52d9b4f9f3a736125d100825d30a1fe65dca0bf6d4c4708f6ec80d99f12d10e702e5651db309c30e555a023c88b63dafb5c505064cf9b7e6f2cd31a6602348543253f95a1f969fb2146a7fb55ad5252f117721f2f5dd3ce8c3a0db38e112885b3b8c8bc815a642828dfd7829f9869bd2ff6c227b97e5bddc7975c8c5780b6dc5fb82d16e9ece0809deb50f84465cb04337d5bd5374ea3276971f217231ba92e0eb6b6b8568f0054296d72d16b5687929e31be15d4af80c38b5552b4214aea2167d3393b37bb03e6133ceb760c2abc4f243f71c554d753ccbae1aa10a0f77fb9a446481c42634ecb4796ea33c24bb2b8f0c7ef9a25fa014911b03618ea36d623eadcadf6ff81b0e7b9195543889a881924921217064c56f4c39ec28ca43d2194baff4ce8e639673237552dff8fa538d02194dddd5dc9903e5e0923678976e7167cf9c73d41fc9137fba88f02af9457292cc9be3c51382f35a7905838c149ea897fbefbb787340d9ecaf4d87bbe2920853a59f418a23b3759a66c7a6fa7cd5cf7571cfe80843c0549f4ba10cd32b823870fe4f1d064551b11224c2bd5669d36127eee1ebfb0d7f501b24139cd9fc15399992bb08303a581577f7736cb9ee8dd11b0bce6e228724951dea0f2388c7529febed3e3c10b56c2305c23e9dedd01041b12fc69aba3783f23d57dfcd1c1beb346f9d12d55bcf9b4377f4d454d2a16ed88cb51d5e5bf01b72a4e7a975d1f535a05a12902e9ff03253389aa8fd28c839af85656bd1b464ce61d5f434420c9e4876df237dbf6962a24f14b7bedb084ed9ee8b1ed07751fd8b05d45d70e24da15b792d65083d8ded1136e74ec5059ab96e86d17949abc925a4d7790ae03ac74e8a605a4287feee861bb0caf073288efa58b40648c2fb46981505f2b42319b6ad6352bccbb69aa85a7cd6d6db338ecbb8741716ac7cf692737d3c39cc118e0da3066982d3ae83470329cc18b5e79e50d2d65e341f5b42aa2b99fe1027085cc2152de9abba09f60b2a4d4e76a1485ded4a466d8cc6d391c218bf6d240babb0d6a38417bbbb1b9270a76c893edd509b67511ff855e331dc8713b18aa3f2fe5f242f9ef23ec59332696fe7f1ef410deb89da244af5231ea22ddd5fe10aefdc356cefe1ffb8b9d476d149cd7ac2ffdbe32410916fea9a0588899cefe69a1d6d6206aaea9906c10c01f983d6f3d19fd886d51c10cd17ce59f99fb49a1137f2ab3eaf48b5b3f2e27cad53a40cbd408cccbb0e614dfa37b9838fcf7d814492946370684066bbd93181bc7dcf213da56ab233d34875e83d1a37762a9325ff33eaf55de4c4f722d483bc52db1e7a3835ceda49cbefd2c8349d6fc475c7a1495a6766212ad3d928010400008085202f89010000000000000000000000000000000000000000000000000000000000000000ffffffff0603c190040101ffffffff0200ca9a3b000000001976a91478f228ff2278032ad67a2a91d03494dd724c8e0088ac80b2e60e0000000017a914fd79e0c84acac223142cf503dc6afde24acac50e8700000000000000000000000000000000000000
diff --git a/zebra-test/src/vectors/block-test-0-299-202.txt b/zebra-test/src/vectors/block-test-0-299-202.txt
new file mode 100644
index 000000000..bbc5556f1
--- /dev/null
+++ b/zebra-test/src/vectors/block-test-0-299-202.txt
@@ -0,0 +1,1 @@
+040000002bc8fe5fbcc33f28637c187713cc66a9d0282e9523b961e6bd98a6584dc2aa00543d074920d46dccfdab357e2f8dfbcb3b5278473bfba0f48b7eea3209fcd5882155aa92b3ea1ce41bc92a1c10a7db4893ff92e9a4b3a08694ecf7f999df1568254db65bffff072000006350564dd6bde56965e94caa700a314a94c0bc822b25ec27989dd8fe0000fd400500ab13ad00b4716bc82cd604203d1445883235463c1742f1bfbb8666e26ac8c497a5657aaddd0a0eeaa0059333c4fbc4976e267f53c97d56cab1dfa2b9e48012d36de271d141919db1d1ee00cce85f285f1fa6ff014318d96993b98902fba19d98cde590af609be18814fcf3a80d87f493b02ffb2ce8f0c552fb30ba62031cd3d1e703de45eb3576a25291d94559c3d3f370872db996db1faa53bf6e7915b1ab53414dab53d583080449a263a4157e1984cf02c308684d11889cda33700d7e064932d4c6f74debc4be9c76d957bcff5e75cd0669f766b56ed90583e3e20ad0c7c844b4e346a0eb30972f831f8eafc2f1752994d7767e731336ffc85e0af12413c88a35b36cbbd2af9724271a6e08155d497bbe2f47a5ed86eb78bf17f5ffe0a97e4d76f58bf50dbb1164654b736b32efa0ebc3355aa4538afd0bea1ab2c366b8cb37945d6361afe0364f75120b92c3f7017eef42278b9a91910646413750caae18821c38ed1db98be17c0babb3dbce92b3502fe5a58f316d5ff711e3fa6b31c4b4d8d341e80cd75323aa403e576e9b4f5a0da135abbc15dd3677b1e1f7f2f29abc198b7e063b7ee2540ba2c4c0d7f2740cc29950eb63ec58be0647b863f101bcbde694a46f6f32766de2303307732faac22cfc659081d9bf74d130b4547a854d3dc805370f93088a4ea67db73d8a2a0fe618d3abcbfee12e04c621723821357f11e2440558f66445772fed92de33f8c5a48621563b2e6c55891ec065e295a0b80eca092830b81c4d9ff3e4e514b9fe2e43f715107dca05155544945fc91f03ace833cacfc341f746517eae0a05ed066b568a8f3258b1d12ba4b2c4909ad8347969264b8bc83f122604917ad7f9136390c64efc74b6d8153d2fe95947dfd0b7bd01f130101112b02b18b14e30006b210ed19b4ec8541395edb9f21a7827573d6c0112b61c57265477f1402213149fbad8ab150d12b320380f4fa80e1261ba17e3075fdadcc18be16de31f1f7ca4e72bd9342b1a1307ecee55558f093edbe5c62d3ada0baa35ef95b10e84d15ec3f131a7f55ba47b0b3897f29e55c078f733e372127aa3ee24f997d6672057bea944775991cb9ea3e10cd0629d77114cffb819f5bc46c616d97b051cb56547d2cca29cdcde1b60311e1eb24e5e4795fe80856d2db25e8a43c8bf0fd7059b49d760d0e495469ed0e28ec4c738e54af38ae9077eedb617905a70fd5ce3fd79583dea8a8f5dac56266107d95cdd6939ea7337283677009723b63c1c4b5992fcabd22c96837c008788cc52bb2e925e75af29085842a6aa0da0138af0417ca53191f91a655c344e094021e64787dfd84142a856227a47a62095dc3e8f0c56e14e7de66437c78cf344e0b082c9f86418e3931288f59c7e3a9d35de5c23ec0c27a9625c875fa3fa03f4d0ad0ac633bedc0f327d4515dc5da4451d6dcb08979e31368d7a6b2c5ea1145675e5d996083afec704bb42e0821f3dfdb0ed63ac6c67e1013b449121141e372ef7a84e46eb021483347425f5370e7afbad11081977575eed9b89a6a6a403c55d9f49f5b339028d1515ae3b46177ac8ff2f31688d4c2b6d97f153431811747a9a15dfd301e216993ff7d7673ec5687c2f671840c4353cdf074169eeb24172b0ab6dddc30f5c7504c75ecf9ba88c3f4f1e51871c8de6e068b3f50b9e1fa0c1d33b5de68d1ca4047fb05c6947277ff9d31316965c5f0ede9b0107a742b4b663b19a3e733e50a919d932d1b26b92a36151368d03c0f415f6acd11f8209ef3ecab9874b6c729960a7409009a031c2d50bee33ade42534d9069712e744a09c6a53919d11daaf3f3390ab3504b30757f1d8273882427c43a3441d286e563f1bdf935625bf33665a3b18610532ae345ffc66010400008085202f89010000000000000000000000000000000000000000000000000000000000000000ffffffff0603c290040101ffffffff0200ca9a3b000000001976a914dd2f390663eefced093d78dcfce557f6858a657a88ac80b2e60e0000000017a914fd79e0c84acac223142cf503dc6afde24acac50e8700000000000000000000000000000000000000
diff --git a/zebra-test/src/vectors/block.rs b/zebra-test/src/vectors/block.rs
index 93ad816e5..a95cdee9c 100644
--- a/zebra-test/src/vectors/block.rs
+++ b/zebra-test/src/vectors/block.rs
@@ -46,6 +46,7 @@ lazy_static! {
             // Sapling
             (419_200, BLOCK_MAINNET_419200_BYTES.as_ref()),
             (419_201, BLOCK_MAINNET_419201_BYTES.as_ref()),
+            // A bad version field
             (434_873, BLOCK_MAINNET_434873_BYTES.as_ref()),
             (653_599, BLOCK_MAINNET_653599_BYTES.as_ref()),
             // Blossom
@@ -55,6 +56,7 @@ lazy_static! {
             // Heartwood
             (903_000, BLOCK_MAINNET_903000_BYTES.as_ref()),
             (903_001, BLOCK_MAINNET_903001_BYTES.as_ref()),
+            // Shielded coinbase x3
             (949_496, BLOCK_MAINNET_949496_BYTES.as_ref()),
             (975_066, BLOCK_MAINNET_975066_BYTES.as_ref()),
             (982_681, BLOCK_MAINNET_982681_BYTES.as_ref()),
@@ -80,6 +82,7 @@ lazy_static! {
             (8, BLOCK_TESTNET_8_BYTES.as_ref()),
             (9, BLOCK_TESTNET_9_BYTES.as_ref()),
             (10, BLOCK_TESTNET_10_BYTES.as_ref()),
+            // A large block
             (141_042, BLOCK_TESTNET_141042_BYTES.as_ref()),
             (207_499, BLOCK_TESTNET_207499_BYTES.as_ref()),
             // Overwinter
@@ -89,6 +92,14 @@ lazy_static! {
             // Sapling
             (280_000, BLOCK_TESTNET_280000_BYTES.as_ref()),
             (280_001, BLOCK_TESTNET_280001_BYTES.as_ref()),
+            (299_187, BLOCK_TESTNET_299187_BYTES.as_ref()),
+            // Minimum-difficulty blocks x2
+            // See zebra_chain's MINIMUM_DIFFICULTY_HEIGHTS for a full list
+            (299_188, BLOCK_TESTNET_299188_BYTES.as_ref()),
+            (299_189, BLOCK_TESTNET_299189_BYTES.as_ref()),
+            (299_201, BLOCK_TESTNET_299201_BYTES.as_ref()),
+            // Minimum-difficulty block
+            (299_202, BLOCK_TESTNET_299202_BYTES.as_ref()),
             (583_999, BLOCK_TESTNET_583999_BYTES.as_ref()),
             // Blossom
             (584_000, BLOCK_TESTNET_584000_BYTES.as_ref()),
@@ -97,6 +108,7 @@ lazy_static! {
             // Heartwood
             (903_800, BLOCK_TESTNET_903800_BYTES.as_ref()),
             (903_801, BLOCK_TESTNET_903801_BYTES.as_ref()),
+            // Shielded coinbase x2
             (914_678, BLOCK_TESTNET_914678_BYTES.as_ref()),
             (925_483, BLOCK_TESTNET_925483_BYTES.as_ref()),
             (1_028_499, BLOCK_TESTNET_1028499_BYTES.as_ref()),
@@ -104,6 +116,7 @@ lazy_static! {
             (1_028_500, BLOCK_TESTNET_1028500_BYTES.as_ref()),
             (1_028_501, BLOCK_TESTNET_1028501_BYTES.as_ref()),
             (1_095_000, BLOCK_TESTNET_1095000_BYTES.as_ref()),
+            // Shielded coinbase
             (1_101_629, BLOCK_TESTNET_1101629_BYTES.as_ref()),
             // TODO: First Halving, see #1104
         ].iter().cloned()
@@ -327,6 +340,28 @@ lazy_static! {
         <Vec<u8>>::from_hex(include_str!("block-test-0-280-001.txt").trim())
         .expect("Block bytes are in valid hex representation");
 
+    // The first minimum difficulty blocks 299188, 299189, 299202 and their previous blocks for context
+    // (pre-Blossom minimum difficulty)
+    //
+    // for i in 299187 299188 299189 299201 299202; do
+    //     zcash-cli -testnet getblock $i 0 > block-test-$[i/1000000]-$[i/1000%1000]-$[i%1000].txt
+    // done
+    pub static ref BLOCK_TESTNET_299187_BYTES: Vec<u8> =
+        <Vec<u8>>::from_hex(include_str!("block-test-0-299-187.txt").trim())
+        .expect("Block bytes are in valid hex representation");
+    pub static ref BLOCK_TESTNET_299188_BYTES: Vec<u8> =
+        <Vec<u8>>::from_hex(include_str!("block-test-0-299-188.txt").trim())
+        .expect("Block bytes are in valid hex representation");
+    pub static ref BLOCK_TESTNET_299189_BYTES: Vec<u8> =
+        <Vec<u8>>::from_hex(include_str!("block-test-0-299-189.txt").trim())
+        .expect("Block bytes are in valid hex representation");
+    pub static ref BLOCK_TESTNET_299201_BYTES: Vec<u8> =
+        <Vec<u8>>::from_hex(include_str!("block-test-0-299-201.txt").trim())
+        .expect("Block bytes are in valid hex representation");
+    pub static ref BLOCK_TESTNET_299202_BYTES: Vec<u8> =
+        <Vec<u8>>::from_hex(include_str!("block-test-0-299-202.txt").trim())
+        .expect("Block bytes are in valid hex representation");
+
     // Blossom transition
     // i=583999
     // zcash-cli -testnet getblock $i 0 > block-test-$[i/1000000]-$[i/1000%1000]-$[i%1000].txt
@@ -336,6 +371,7 @@ lazy_static! {
     pub static ref BLOCK_TESTNET_583999_BYTES: Vec<u8> =
         <Vec<u8>>::from_hex(include_str!("block-test-0-583-999.txt").trim())
         .expect("Block bytes are in valid hex representation");
+    // The first post-Blossom minimum difficulty block is the activation block
     pub static ref BLOCK_TESTNET_584000_BYTES: Vec<u8> =
         <Vec<u8>>::from_hex(include_str!("block-test-0-584-000.txt").trim())
         .expect("Block bytes are in valid hex representation");
@@ -404,8 +440,12 @@ mod test {
 
     use std::collections::HashSet;
 
+    use crate::init;
+
     #[test]
     fn block_test_vectors_unique() {
+        init();
+
         let block_count = BLOCKS.len();
         let block_set: HashSet<_> = BLOCKS.iter().collect();
 
@@ -422,6 +462,8 @@ mod test {
     /// We're using lazy_static! and combinators, so it would be easy to make this mistake.
     #[test]
     fn block_test_vectors_count() {
+        init();
+
         assert!(
             BLOCKS.len() > 50,
             "there should be a reasonable number of block test vectors"
diff --git a/zebrad/tests/acceptance.rs b/zebrad/tests/acceptance.rs
index 8b9d023f8..227d94354 100644
--- a/zebrad/tests/acceptance.rs
+++ b/zebrad/tests/acceptance.rs
@@ -134,6 +134,7 @@ where
 #[test]
 fn generate_no_args() -> Result<()> {
     zebra_test::init();
+
     let child = testdir()?
         .with_config(default_test_config()?)?
         .spawn_child(&["generate"])?;
@@ -165,6 +166,7 @@ macro_rules! assert_with_context {
 #[test]
 fn generate_args() -> Result<()> {
     zebra_test::init();
+
     let testdir = testdir()?;
     let testdir = &testdir;
 
@@ -205,6 +207,7 @@ fn generate_args() -> Result<()> {
 #[test]
 fn help_no_args() -> Result<()> {
     zebra_test::init();
+
     let testdir = testdir()?.with_config(default_test_config()?)?;
 
     let child = testdir.spawn_child(&["help"])?;
@@ -223,6 +226,7 @@ fn help_no_args() -> Result<()> {
 #[test]
 fn help_args() -> Result<()> {
     zebra_test::init();
+
     let testdir = testdir()?;
     let testdir = &testdir;
 
@@ -242,6 +246,7 @@ fn help_args() -> Result<()> {
 #[test]
 fn start_no_args() -> Result<()> {
     zebra_test::init();
+
     // start caches state, so run one of the start tests with persistent state
     let testdir = testdir()?.with_config(persistent_test_config()?)?;
 
@@ -265,6 +270,7 @@ fn start_no_args() -> Result<()> {
 #[test]
 fn start_args() -> Result<()> {
     zebra_test::init();
+
     let testdir = testdir()?.with_config(default_test_config()?)?;
     let testdir = &testdir;
 
@@ -291,6 +297,7 @@ fn start_args() -> Result<()> {
 #[test]
 fn persistent_mode() -> Result<()> {
     zebra_test::init();
+
     let testdir = testdir()?.with_config(persistent_test_config()?)?;
     let testdir = &testdir;
 
@@ -314,6 +321,7 @@ fn persistent_mode() -> Result<()> {
 #[test]
 fn ephemeral_mode() -> Result<()> {
     zebra_test::init();
+
     let testdir = testdir()?.with_config(default_test_config()?)?;
     let testdir = &testdir;
 
@@ -378,6 +386,7 @@ fn misconfigured_ephemeral_mode() -> Result<()> {
 #[test]
 fn app_no_args() -> Result<()> {
     zebra_test::init();
+
     let testdir = testdir()?.with_config(default_test_config()?)?;
 
     let child = testdir.spawn_child(&[])?;
@@ -392,6 +401,7 @@ fn app_no_args() -> Result<()> {
 #[test]
 fn version_no_args() -> Result<()> {
     zebra_test::init();
+
     let testdir = testdir()?.with_config(default_test_config()?)?;
 
     let child = testdir.spawn_child(&["version"])?;
@@ -406,6 +416,7 @@ fn version_no_args() -> Result<()> {
 #[test]
 fn version_args() -> Result<()> {
     zebra_test::init();
+
     let testdir = testdir()?.with_config(default_test_config()?)?;
     let testdir = &testdir;
 
@@ -434,6 +445,7 @@ fn valid_generated_config_test() -> Result<()> {
 
 fn valid_generated_config(command: &str, expected_output: &str) -> Result<()> {
     zebra_test::init();
+
     let testdir = testdir()?;
     let testdir = &testdir;
 
@@ -527,7 +539,7 @@ fn restart_stop_at_height() -> Result<()> {
     sync_until(
         Height(0),
         Mainnet,
-        "called_from=OnLoad",
+        "state is already at the configured height",
         STOP_ON_LOAD_TIMEOUT,
         Some(reuse_tempdir),
     )?;