Repository: ZcashFoundation/zebra
Pull request #1278 (2020-11-10 19:50:11)

Update RFC template
I found the RFC template a bit confusing so i made some minor mods here that i think will make it look better.


Patch:
diff --git a/.github/PULL_REQUEST_TEMPLATE/rfc.md b/.github/PULL_REQUEST_TEMPLATE/rfc.md
index bdb39cf85..e8a363a1e 100644
--- a/.github/PULL_REQUEST_TEMPLATE/rfc.md
+++ b/.github/PULL_REQUEST_TEMPLATE/rfc.md
@@ -15,10 +15,22 @@ The RFC process is documented in CONTRIBUTING.md.
 Please see the checklist there: https://zebra.zfnd.org/CONTRIBUTING.html
 -->
 
+
+### Summary
+
+Please copy the RFC summary over here. 
+
+### More information
+
 Feature Name: `my_feature`
+
 Start Date: YYYY-MM-DD
+
 Design PR: [ZcashFoundation/zebra#0000](https://github.com/ZcashFoundation/zebra/pull/0000)
-Zebra Issue: [ZcashFoundation/zebra#0000](https://github.com/ZcashFoundation/zebra/pull/0000)
+
+Zebra Issue: [ZcashFoundation/zebra#0000](https://github.com/ZcashFoundation/zebra/issues/0000)
+
+### Document
 
 <!--
 Fill this in, replacing:
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 208833304..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]]
@@ -1457,11 +1468,11 @@ 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",
 ]
 
@@ -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 = "b78a366903f506d2ad52ca8dc552102ffdd3e937ba8a227f024dc1d1eae28575"
+dependencies = [
+ "tinyvec_macros",
+]
+
+[[package]]
+name = "tinyvec_macros"
+version = "0.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "238ce071d267c5710f9d31451efec16c5ee22de34df17cc05e56cbc92e967117"
+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 0192f0b52..3d634ab33 100644
--- a/book/src/dev/rfcs/0005-state-updates.md
+++ b/book/src/dev/rfcs/0005-state-updates.md
@@ -404,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`
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/lib.rs b/zebra-chain/src/lib.rs
index 3c504a26f..c35a8afde 100644
--- a/zebra-chain/src/lib.rs
+++ b/zebra-chain/src/lib.rs
@@ -8,6 +8,8 @@
 #![doc(html_root_url = "https://doc.zebra.zfnd.org/zebra_chain")]
 // #![deny(missing_docs)]
 #![allow(clippy::try_err)]
+#![allow(clippy::unknown_clippy_lints)]
+#![allow(clippy::from_iter_instead_of_collect)]
 
 #[macro_use]
 extern crate serde;
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/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/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/protocol/external/message.rs b/zebra-network/src/protocol/external/message.rs
index 1bf051a4b..e0e035460 100644
--- a/zebra-network/src/protocol/external/message.rs
+++ b/zebra-network/src/protocol/external/message.rs
@@ -121,20 +121,15 @@ pub enum Message {
         data: Option<[u8; 32]>,
     },
 
-    /// An `addr` message.
-    ///
-    /// [Bitcoin reference](https://en.bitcoin.it/wiki/Protocol_documentation#addr)
-    Addr(Vec<MetaAddr>),
-
     /// A `getaddr` message.
     ///
     /// [Bitcoin reference](https://en.bitcoin.it/wiki/Protocol_documentation#getaddr)
     GetAddr,
 
-    /// A `block` message.
+    /// An `addr` message.
     ///
-    /// [Bitcoin reference](https://en.bitcoin.it/wiki/Protocol_documentation#block)
-    Block(Arc<Block>),
+    /// [Bitcoin reference](https://en.bitcoin.it/wiki/Protocol_documentation#addr)
+    Addr(Vec<MetaAddr>),
 
     /// A `getblocks` message.
     ///
@@ -155,16 +150,14 @@ pub enum Message {
         stop: Option<block::Hash>,
     },
 
-    /// A `headers` message.
+    /// An `inv` message.
     ///
-    /// Returns block headers in response to a getheaders packet.
+    /// Allows a node to advertise its knowledge of one or more
+    /// objects. It can be received unsolicited, or in reply to
+    /// `getblocks`.
     ///
-    /// [Bitcoin reference](https://en.bitcoin.it/wiki/Protocol_documentation#headers)
-    // Note that the block headers in this packet include a
-    // transaction count (a var_int, so there can be more than 81
-    // bytes per header) as opposed to the block headers that are
-    // hashed by miners.
-    Headers(Vec<block::Header>),
+    /// [Bitcoin reference](https://en.bitcoin.it/wiki/Protocol_documentation#inv)
+    Inv(Vec<InventoryHash>),
 
     /// A `getheaders` message.
     ///
@@ -173,7 +166,7 @@ pub enum Message {
     /// of its best chain and determine the blocks following the intersection
     /// point.
     ///
-    /// The peer responds with an `headers` packet with the hashes of subsequent blocks.
+    /// The peer responds with an `headers` packet with the headers of subsequent blocks.
     /// If supplied, the `stop` parameter specifies the last header to request.
     /// Otherwise, the maximum number of block headers (160) are sent.
     ///
@@ -185,14 +178,16 @@ pub enum Message {
         stop: Option<block::Hash>,
     },
 
-    /// An `inv` message.
+    /// A `headers` message.
     ///
-    /// Allows a node to advertise its knowledge of one or more
-    /// objects. It can be received unsolicited, or in reply to
-    /// `getblocks`.
+    /// Returns block headers in response to a getheaders packet.
     ///
-    /// [Bitcoin reference](https://en.bitcoin.it/wiki/Protocol_documentation#inv)
-    Inv(Vec<InventoryHash>),
+    /// [Bitcoin reference](https://en.bitcoin.it/wiki/Protocol_documentation#headers)
+    // Note that the block headers in this packet include a
+    // transaction count (a var_int, so there can be more than 81
+    // bytes per header) as opposed to the block headers that are
+    // hashed by miners.
+    Headers(Vec<block::Header>),
 
     /// A `getdata` message.
     ///
@@ -203,17 +198,22 @@ pub enum Message {
     /// [Bitcoin reference](https://en.bitcoin.it/wiki/Protocol_documentation#getdata)
     GetData(Vec<InventoryHash>),
 
-    /// A `notfound` message.
+    /// A `block` message.
     ///
-    /// [Bitcoin reference](https://en.bitcoin.it/wiki/Protocol_documentation#notfound)
-    // See note above on `Inventory`.
-    NotFound(Vec<InventoryHash>),
+    /// [Bitcoin reference](https://en.bitcoin.it/wiki/Protocol_documentation#block)
+    Block(Arc<Block>),
 
     /// A `tx` message.
     ///
     /// [Bitcoin reference](https://en.bitcoin.it/wiki/Protocol_documentation#tx)
     Tx(Arc<Transaction>),
 
+    /// A `notfound` message.
+    ///
+    /// [Bitcoin reference](https://en.bitcoin.it/wiki/Protocol_documentation#notfound)
+    // See note above on `Inventory`.
+    NotFound(Vec<InventoryHash>),
+
     /// A `mempool` message.
     ///
     /// This was defined in [BIP35], which is included in Zcash.
diff --git a/zebra-state/Cargo.toml b/zebra-state/Cargo.toml
index a2134cd21..ab5bf2295 100644
--- a/zebra-state/Cargo.toml
+++ b/zebra-state/Cargo.toml
@@ -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/lib.rs b/zebra-state/src/lib.rs
index 5e716772e..42fd4cea2 100644
--- a/zebra-state/src/lib.rs
+++ b/zebra-state/src/lib.rs
@@ -5,6 +5,8 @@
 #![doc(html_root_url = "https://doc.zebra.zfnd.org/zebra_state")]
 #![warn(missing_docs)]
 #![allow(clippy::try_err)]
+#![allow(clippy::unknown_clippy_lints)]
+#![allow(clippy::field_reassign_with_default)]
 
 mod config;
 mod constants;
diff --git a/zebra-state/src/request.rs b/zebra-state/src/request.rs
index 974210617..6a0da75c8 100644
--- a/zebra-state/src/request.rs
+++ b/zebra-state/src/request.rs
@@ -83,45 +83,49 @@ pub enum Request {
         // sapling_anchor: sapling::tree::Root,
     },
 
-    /// Computes the depth in the best chain of the block identified by the given hash.
+    /// Computes the depth in the current best chain of the block identified by the given hash.
     ///
     /// Returns
     ///
-    /// * [`Response::Depth(Some(depth))`](Response::Depth) if the block is in the main chain;
+    /// * [`Response::Depth(Some(depth))`](Response::Depth) if the block is in the best chain;
     /// * [`Response::Depth(None)`](Response::Depth) otherwise.
     Depth(block::Hash),
 
     /// Returns [`Response::Tip`] with the current best chain tip.
     Tip,
 
-    /// Computes a block locator object based on the current chain state.
+    /// Computes a block locator object based on the current best chain.
     ///
     /// Returns [`Response::BlockLocator`] with hashes starting
-    /// from the current chain tip and reaching backwards towards the genesis
-    /// block. The first hash is the current chain tip. The last hash is the tip
-    /// of the finalized portion of the state. If the state is empty, the block
-    /// locator is also empty.
+    /// from the best chain tip, and following the chain of previous
+    /// hashes. The first hash is the best chain tip. The last hash is
+    /// the tip of the finalized portion of the state. Block locators
+    /// are not continuous - some intermediate hashes might be skipped.
+    ///
+    /// If the state is empty, the block locator is also empty.
     BlockLocator,
 
-    /// Looks up a transaction by hash.
+    /// Looks up a transaction by hash in the current best chain.
     ///
     /// Returns
     ///
-    /// * [`Response::Transaction(Some(Arc<Transaction>))`](Response::Transaction) if the transaction is known;
+    /// * [`Response::Transaction(Some(Arc<Transaction>))`](Response::Transaction) if the transaction is in the best chain;
     /// * [`Response::Transaction(None)`](Response::Transaction) otherwise.
     Transaction(transaction::Hash),
 
-    /// Looks up a block by hash or height.
+    /// Looks up a block by hash or height in the current best chain.
     ///
     /// Returns
     ///
-    /// * [`Response::Block(Some(Arc<Block>))`](Response::Block) if the block is known;
-    /// * [`Response::Block(None)`](Response::Transaction) otherwise.
+    /// * [`Response::Block(Some(Arc<Block>))`](Response::Block) if the block is in the best chain;
+    /// * [`Response::Block(None)`](Response::Block) otherwise.
     ///
     /// Note: the [`HashOrHeight`] can be constructed from a [`block::Hash`] or
     /// [`block::Height`] using `.into()`.
     Block(HashOrHeight),
 
-    /// Request a UTXO identified by the given Outpoint
+    /// Request a UTXO identified by the given Outpoint in any chain.
+    ///
+    /// Returns UTXOs fron any chain, including side-chains.
     AwaitUtxo(transparent::OutPoint),
 }
diff --git a/zebra-state/src/service.rs b/zebra-state/src/service.rs
index f2c6b0e7c..fd0faa2d8 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
@@ -47,6 +50,8 @@ struct StateService {
     queued_blocks: QueuedBlocks,
     /// The set of outpoints with pending requests for their associated transparent::Output
     pending_utxos: utxo::PendingUtxos,
+    /// The configured Zcash network
+    network: Network,
     /// Instant tracking the last time `pending_utxos` was pruned
     last_prune: Instant,
 }
@@ -65,6 +70,7 @@ impl StateService {
             mem,
             queued_blocks,
             pending_utxos,
+            network,
             last_prune: Instant::now(),
         }
     }
@@ -182,19 +188,34 @@ 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 height = 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("semantically valid blocks have a coinbase height");
+        let hash = block.hash();
+
+        let span = tracing::info_span!("StateService::check_contextual_validity",
+                                             ?height, network = ?self.network, ?hash);
+        let _entered = span.enter();
+
+        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)?;
 
-        // TODO: contextual validation design and implementation
+        let mut relevant_chain = self.chain(block.header.previous_block_hash);
+        let parent_block = relevant_chain
+            .next()
+            .expect("state must contain parent block to do contextual validation");
+        let parent_height = parent_block
+            .coinbase_height()
+            .expect("valid blocks have a coinbase height");
+        let parent_hash = parent_block.hash();
+        check::height_one_more_than_parent_height(parent_height, block)?;
+        // should be impossible by design, so no handleable error is thrown
+        assert_eq!(parent_hash, block.header.previous_block_hash);
+
+        // TODO: validate difficulty adjustment
+        // TODO: other contextual validation design and implelentation
         Ok(())
     }
 
@@ -248,10 +269,121 @@ 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.
+    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 5df57399a..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 {
@@ -140,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()?;
@@ -170,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()?;
@@ -217,3 +246,160 @@ impl NonFinalizedState {
         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 dc7721857..ec16d807c 100644
--- a/zebra-state/src/service/memory_state/queued_blocks.rs
+++ b/zebra-state/src/service/memory_state/queued_blocks.rs
@@ -96,10 +96,27 @@ impl QueuedBlocks {
         for hash in by_height.into_iter().flat_map(|(_, hashes)| hashes) {
             let expired = self.blocks.remove(&hash).expect("block is present");
             let parent_hash = &expired.block.header.previous_block_hash;
-            self.by_parent
+
+            let parent_list = self
+                .by_parent
                 .get_mut(parent_hash)
-                .expect("parent is present")
-                .remove(&hash);
+                .expect("parent is present");
+
+            if parent_list.len() == 1 {
+                let removed = self
+                    .by_parent
+                    .remove(parent_hash)
+                    .expect("parent is present");
+                assert!(
+                    removed.contains(&hash),
+                    "hash must be present in parent hash list"
+                );
+            } else {
+                assert!(
+                    parent_list.remove(&hash),
+                    "hash must be present in parent hash list"
+                );
+            }
         }
 
         tracing::trace!(num_blocks = %self.blocks.len(), "Finished pruning blocks at or beneath the finalized tip height",);
@@ -122,3 +139,136 @@ impl QueuedBlocks {
         metrics::gauge!("state.memory.queued.block.count", self.blocks.len() as _);
     }
 }
+
+#[cfg(test)]
+mod tests {
+    use std::sync::Arc;
+
+    use tokio::sync::oneshot;
+    use zebra_chain::{block::Block, serialization::ZcashDeserializeInto};
+    use zebra_test::prelude::*;
+
+    use crate::tests::FakeChainHelper;
+
+    use self::assert_eq;
+    use super::*;
+
+    // Quick helper trait for making queued blocks with throw away channels
+    trait IntoQueued {
+        fn into_queued(self) -> QueuedBlock;
+    }
+
+    impl IntoQueued for Arc<Block> {
+        fn into_queued(self) -> QueuedBlock {
+            let (rsp_tx, _) = oneshot::channel();
+
+            QueuedBlock {
+                block: self,
+                rsp_tx,
+            }
+        }
+    }
+
+    #[test]
+    fn dequeue_gives_right_children() -> Result<()> {
+        zebra_test::init();
+
+        let block1: Arc<Block> =
+            zebra_test::vectors::BLOCK_MAINNET_419200_BYTES.zcash_deserialize_into()?;
+        let child1: Arc<Block> =
+            zebra_test::vectors::BLOCK_MAINNET_419201_BYTES.zcash_deserialize_into()?;
+        let child2 = block1.make_fake_child();
+
+        let parent = block1.header.previous_block_hash;
+
+        let mut queue = QueuedBlocks::default();
+        // Empty to start
+        assert_eq!(0, queue.blocks.len());
+        assert_eq!(0, queue.by_parent.len());
+        assert_eq!(0, queue.by_height.len());
+
+        // Inserting the first block gives us 1 in each table
+        queue.queue(block1.clone().into_queued());
+        assert_eq!(1, queue.blocks.len());
+        assert_eq!(1, queue.by_parent.len());
+        assert_eq!(1, queue.by_height.len());
+
+        // The second gives us one in each table because its a child of the first
+        queue.queue(child1.clone().into_queued());
+        assert_eq!(2, queue.blocks.len());
+        assert_eq!(2, queue.by_parent.len());
+        assert_eq!(2, queue.by_height.len());
+
+        // The 3rd only increments blocks, because it is also a child of the
+        // first block, so for the second and third tables it gets added to the
+        // existing HashSet value
+        queue.queue(child2.clone().into_queued());
+        assert_eq!(3, queue.blocks.len());
+        assert_eq!(2, queue.by_parent.len());
+        assert_eq!(2, queue.by_height.len());
+
+        // Dequeueing the first block removes 1 block from each list
+        let children = queue.dequeue_children(parent);
+        assert_eq!(1, children.len());
+        assert_eq!(block1, children[0].block);
+        assert_eq!(2, queue.blocks.len());
+        assert_eq!(1, queue.by_parent.len());
+        assert_eq!(1, queue.by_height.len());
+
+        // Dequeueing the children of the first block removes both of the other
+        // blocks, and empties all lists
+        let parent = children[0].block.hash();
+        let children = queue.dequeue_children(parent);
+        assert_eq!(2, children.len());
+        assert!(children
+            .iter()
+            .any(|QueuedBlock { block, .. }| block == &child1));
+        assert!(children
+            .iter()
+            .any(|QueuedBlock { block, .. }| block == &child2));
+        assert_eq!(0, queue.blocks.len());
+        assert_eq!(0, queue.by_parent.len());
+        assert_eq!(0, queue.by_height.len());
+
+        Ok(())
+    }
+
+    #[test]
+    fn prune_removes_right_children() -> Result<()> {
+        zebra_test::init();
+
+        let block1: Arc<Block> =
+            zebra_test::vectors::BLOCK_MAINNET_419200_BYTES.zcash_deserialize_into()?;
+        let child1: Arc<Block> =
+            zebra_test::vectors::BLOCK_MAINNET_419201_BYTES.zcash_deserialize_into()?;
+        let child2 = block1.make_fake_child();
+
+        let mut queue = QueuedBlocks::default();
+        queue.queue(block1.clone().into_queued());
+        queue.queue(child1.clone().into_queued());
+        queue.queue(child2.clone().into_queued());
+        assert_eq!(3, queue.blocks.len());
+        assert_eq!(2, queue.by_parent.len());
+        assert_eq!(2, queue.by_height.len());
+
+        // Pruning the first height removes only block1
+        queue.prune_by_height(block1.coinbase_height().unwrap());
+        assert_eq!(2, queue.blocks.len());
+        assert_eq!(1, queue.by_parent.len());
+        assert_eq!(1, queue.by_height.len());
+        assert!(queue.get_mut(&block1.hash()).is_none());
+        assert!(queue.get_mut(&child1.hash()).is_some());
+        assert!(queue.get_mut(&child2.hash()).is_some());
+
+        // Dequeueing the children of the first block removes both of the other
+        // blocks, and empties all lists
+        queue.prune_by_height(child1.coinbase_height().unwrap());
+        assert_eq!(0, queue.blocks.len());
+        assert_eq!(0, queue.by_parent.len());
+        assert_eq!(0, queue.by_height.len());
+        assert!(queue.get_mut(&child1.hash()).is_none());
+        assert!(queue.get_mut(&child2.hash()).is_none());
+
+        Ok(())
+    }
+}
diff --git a/zebra-state/src/sled_state.rs b/zebra-state/src/sled_state.rs
index 4b3ce9e88..62a3a2b0a 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");
-            }
-        }
-
-        // 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"
-                );
-            }
+            return false;
         }
 
-        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,20 +145,64 @@ 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()) {
+        loop {
+            let finalized_tip_hash = self.finalized_tip_hash();
+            let queued_block =
+                if let Some(queued_block) = self.queued_by_prev_hash.remove(&finalized_tip_hash) {
+                    queued_block
+                } else {
+                    break;
+                };
+
             let height = queued_block
                 .block
                 .coinbase_height()
                 .expect("valid blocks must have a height");
+
+            if self.block_by_height.is_empty() {
+                assert_eq!(
+                    block::Hash([0; 32]),
+                    prev_hash,
+                    "the first block added to an empty state must be a genesis block"
+                );
+                assert_eq!(
+                    block::Height(0),
+                    height,
+                    "cannot commit genesis: invalid height"
+                );
+            } else {
+                assert_eq!(
+                    self.finalized_tip_height()
+                        .expect("state must have a genesis block committed")
+                        + 1,
+                    Some(height)
+                );
+
+                assert_eq!(
+                    finalized_tip_hash,
+                    queued_block.block.header.previous_block_hash
+                );
+            }
+
             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 _
         );
     }
@@ -234,8 +250,6 @@ impl FinalizedState {
                     sprout_nullifiers,
                     sapling_nullifiers,
                 )| {
-                    // TODO: check highest entry of hash_by_height as in RFC
-
                     // Index the block
                     hash_by_height.zs_insert(height, hash)?;
                     height_by_hash.zs_insert(hash, height)?;
@@ -295,8 +309,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 50aa13aca..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,
         );


Test patch:
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-consensus/src/block/tests.rs b/zebra-consensus/src/block/tests.rs
index 174d73190..82c8c0d14 100644
--- a/zebra-consensus/src/block/tests.rs
+++ b/zebra-consensus/src/block/tests.rs
@@ -234,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(())
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-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 fce5aacec..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,6 +78,20 @@ 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() {
diff --git a/zebra-test/src/lib.rs b/zebra-test/src/lib.rs
index 276369f20..e45eb4a81 100644
--- a/zebra-test/src/lib.rs
+++ b/zebra-test/src/lib.rs
@@ -1,4 +1,10 @@
 //! Miscellaneous test code for Zebra.
+
+#![allow(clippy::unknown_clippy_lints)]
+#![allow(clippy::from_iter_instead_of_collect)]
+// 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 bfa075b71..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");
diff --git a/zebrad/tests/acceptance.rs b/zebrad/tests/acceptance.rs
index 3aedb972d..742fb1ebd 100644
--- a/zebrad/tests/acceptance.rs
+++ b/zebrad/tests/acceptance.rs
@@ -14,6 +14,8 @@
 #![warn(warnings, missing_docs, trivial_casts, unused_qualifications)]
 #![forbid(unsafe_code)]
 #![allow(clippy::try_err)]
+#![allow(clippy::unknown_clippy_lints)]
+#![allow(clippy::field_reassign_with_default)]
 
 use color_eyre::eyre::Result;
 use eyre::WrapErr;
@@ -539,7 +541,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),
     )?;