diff --git a/configure.ac b/configure.ac
index 0116a662c..83593dbaa 100644
--- a/configure.ac
+++ b/configure.ac
@@ -8,7 +8,7 @@ define(_CLIENT_VERSION_IS_RELEASE, true)
 define(_COPYRIGHT_YEAR, 2020)
 define(_COPYRIGHT_HOLDERS,[The %s developers])
 define(_COPYRIGHT_HOLDERS_SUBSTITUTION,[[Bitcoin Core]])
-AC_INIT([Bitcoin Core],m4_join([.], _CLIENT_VERSION_MAJOR, _CLIENT_VERSION_MINOR, _CLIENT_VERSION_REVISION, m4_if(_CLIENT_VERSION_BUILD, [0], [], _CLIENT_VERSION_BUILD))m4_if(_CLIENT_VERSION_RC, [0], [], [rc]_CLIENT_VERSION_RC),[https://github.com/bitcoin/bitcoin/issues],[bitcoin],[https://bitcoincore.org/])
+AC_INIT([Bitcoin Core],m4_join([.], _CLIENT_VERSION_MAJOR, _CLIENT_VERSION_MINOR, _CLIENT_VERSION_REVISION, m4_if(_CLIENT_VERSION_BUILD, [0], [], _CLIENT_VERSION_BUILD))m4_if(_CLIENT_VERSION_RC, [0], [], [rc]_CLIENT_VERSION_RC),[https://github.com/BitcoinActivation/bitcoin/issues],[bitcoin],[https://bitcointaproot.org/])
 AC_CONFIG_SRCDIR([src/validation.cpp])
 AC_CONFIG_HEADERS([src/config/bitcoin-config.h])
 AC_CONFIG_AUX_DIR([build-aux])
diff --git a/doc/bips.md b/doc/bips.md
index 8c20533c9..faec8184e 100644
--- a/doc/bips.md
+++ b/doc/bips.md
@@ -1,6 +1,7 @@
 BIPs that are implemented by Bitcoin Core (up-to-date up to **v0.21.0**):
 
-* [`BIP 9`](https://github.com/bitcoin/bips/blob/master/bip-0009.mediawiki): The changes allowing multiple soft-forks to be deployed in parallel have been implemented since **v0.12.1**  ([PR #7575](https://github.com/bitcoin/bitcoin/pull/7575))
+* [`BIP 8`](https://github.com/bitcoin/bips/blob/master/bip-0008.mediawiki): The changes for parallel, rapid deployment based on block height miner activation with UASF failover have been implemented since **v0.21.1**  ([PR #19573](https://github.com/bitcoin/bitcoin/pull/19573)).
+* [`BIP 9`](https://github.com/bitcoin/bips/blob/master/bip-0009.mediawiki): The changes allowing multiple soft-forks to be deployed in parallel have been implemented since **v0.12.1**  ([PR #7575](https://github.com/bitcoin/bitcoin/pull/7575)) Support was removed in **v0.21.1** ([PR #21392](https://github.com/bitcoin/bitcoin/pull/21392)).
 * [`BIP 11`](https://github.com/bitcoin/bips/blob/master/bip-0011.mediawiki): Multisig outputs are standard since **v0.6.0** ([PR #669](https://github.com/bitcoin/bitcoin/pull/669)).
 * [`BIP 13`](https://github.com/bitcoin/bips/blob/master/bip-0013.mediawiki): The address format for P2SH addresses has been implemented since **v0.6.0** ([PR #669](https://github.com/bitcoin/bitcoin/pull/669)).
 * [`BIP 14`](https://github.com/bitcoin/bips/blob/master/bip-0014.mediawiki): The subversion string is being used as User Agent since **v0.6.0** ([PR #669](https://github.com/bitcoin/bitcoin/pull/669)).
diff --git a/doc/release-notes-21392.md b/doc/release-notes-21392.md
new file mode 100644
index 000000000..fa02e599c
--- /dev/null
+++ b/doc/release-notes-21392.md
@@ -0,0 +1,7 @@
+Low-level changes
+=================
+
+RPC
+---
+
+* BIP 9 has been replaced with BIP 8. This change is reflected in `getblockchaininfo` where references to BIP 9 have been replaced with references to BIP 8.
diff --git a/doc/release-notes.md b/doc/release-notes.md
index 66aee7764..3cf758f22 100644
--- a/doc/release-notes.md
+++ b/doc/release-notes.md
@@ -1,3 +1,85 @@
+0.21.0+taproot0.1 Release Notes
+===============================
+
+Bitcoin Core 0.21.0-based Taproot Client 0.1 is now available from:
+
+  <https://github.com/BitcoinActivation/bitcoin/releases/tag/v0.21.0-taproot0.1>
+
+This release includes Taproot activation parameters.
+
+How to Upgrade
+==============
+
+If you are running an older version, shut it down. Wait until it has completely
+shut down (which might take a few minutes in some cases), then run the
+installer (on Windows) or just copy over `/Applications/Bitcoin-Qt` (on Mac)
+or `bitcoind`/`bitcoin-qt` (on Linux).
+
+Upgrading directly from a version of Bitcoin Core that has reached its EOL is
+possible, but it might take some time if the data directory needs to be migrated. Old
+wallet versions of Bitcoin Core are generally supported.
+
+Compatibility
+==============
+
+Bitcoin Core is supported and extensively tested on operating systems
+using the Linux kernel, macOS 10.12+, and Windows 7 and newer.  Bitcoin
+Core should also work on most other Unix-like systems but is not as
+frequently tested on them.  It is not recommended to use Bitcoin Core on
+unsupported systems.
+
+From Bitcoin Core 0.20.0 onwards, macOS versions earlier than 10.12 are no
+longer supported. Additionally, Bitcoin Core does not yet change appearance
+when macOS "dark mode" is activated.
+
+
+Notable changes
+===============
+
+Taproot soft fork
+
+When activated, the Taproot softfork will enable much more scalable and private construction of smart contracts as well as the use of Schnorr signatures. This comes with immediate benefits to many users, as well as lays the groundwork for more improvements to come building on top of the new functionality it will make available. Here is a non-comprehensive list of some of that potential new functionality.
+
+- Node validation costs: Schnorr signatures are able to be validated in batches. This will bring down the validation costs of node operators the more schnorr is adopted by transacting users.
+
+- Multisig: Schnorr allows the construction of multi-signature addresses that are indistinguishable from a normal single signature address. This will allow Lightning Network developers to enable more private and efficient use of the Bitcoin blockchain for channel openings and closures. It will also improve the on chain privacy of multisig users after the implementation of safe threshold multi-signatures using schnorr. As well, multisig users will wind up saving a lot on fees due to smaller scripts and producing only a single signature to validate.
+
+- MAST(Merkelized Abstract Syntax Tree): It is currently possible to create a Bitcoin UTXO that can be spent in multiple different ways; the downside to this however is that makes the script larger and the UTXO more expensive to spend. The more ways to spend the UTXO, the larger the script, the larger the fee. MAST is an old proposal to make this more cost effective and private. It works by creating a merkle tree of the different spending conditions, with each condition in it's own leaf at the edge, and requires only providing the spending condition that is used with its associated merkle proof. It allows a UTXO to be locked to multiple spending conditions without requiring all of them to be revealed at the time of spending, which saves fees for blockchain space and comes with privacy benefits. Taproot implements MAST by taking advantage of some of the mathematical benefits of Schnorr, i.e. "tweaking" a schnorr public key with the merkle root of a MAST tree, to produce a new public key. Because of how schnorr works, "tweaking" the matching private key would create a new private key corresponding to the tweaked public key. With this new change to MAST when the UTXO is spent with the tweaked private key no one will even be able to tell a MAST tree existed by watching the blockchain. 
+
+- Privacy: The above functionalities, especially how they can be utilized by different wallet and protocol developers, lay a lot of groundwork for a general improvement in how privately users interact with the Bitcoin network. Some of these improvements will happen passively just through the use of schnorr addresses, some of them will require the use of special tools to benefit from, but overall this soft fork will expand the possibilities for private use of the Bitcoin blockchain. 
+
+
+P2P and network changes
+------------------------
+
+- Taproot activation parameters: At blockheight 681408 (2021 Apr 29) a BIP 8 signaling period will begin with signal bit 2 and an activation threshold of 90% (1815/2016 blocks in a difficulty period) with LOT=true. The minimum activation height is set to block 709632 (2021 Nov 11),  with a timeout height of 760032 (2022 Oct 27). The latest activation height is block 762048 (2022 Nov 10). 
+
+Updated RPCs
+------------
+
+- getblockchaininfo has replaced all references in returned data to BIP9 with BIP8.
+
+0.21.0+taproot0.1 change log
+============================
+
+https://github.com/BitcoinActivation/bitcoin/pulls?q=is%3Apr+is%3Amerged
+
+### Consensus
+- BA#6 Implement BIP 8 based Speedy Trial activation (achow101, ajtowns, luke-jr, Sjors)
+- BA#7 Add BIP 8 lockinontimeout flag and MUST_SIGNAL phase for versionbits (ajtowns, luke-jr)
+- BA#9 chainparams: Set taproot activation parameters
+
+### Tests and QA
+- BA#2 test: Additional (refactored) BIP9 tests (ajtowns, Sjors)
+- BA#4 tests: Add fuzzing harness for versionbits (ajtowns, MarcoFalke)
+- BA#5 fuzz: cleanups for versionbits fuzzer (ajtowns) 
+
+### Miscellaneous
+- BA#8 Branding for Bitcoin Core with Taproot (BitcoinMechanic, luke-jr)
+
+This client was based on Bitcoin Core 0.21.0. The above release notes cover only the changes from that Core release. All major changes in Bitcoin Core 0.21.0 also present in this client are detailed below in the official 0.21.0 Release Notes.
+
+
 0.21.0 Release Notes
 ====================
 
diff --git a/src/Makefile.test.include b/src/Makefile.test.include
index 9cc383c24..45f2a04cd 100644
--- a/src/Makefile.test.include
+++ b/src/Makefile.test.include
@@ -157,7 +157,8 @@ FUZZ_TARGETS = \
   test/fuzz/txrequest \
   test/fuzz/txundo_deserialize \
   test/fuzz/uint160_deserialize \
-  test/fuzz/uint256_deserialize
+  test/fuzz/uint256_deserialize \
+  test/fuzz/versionbits
 
 if ENABLE_FUZZ
 noinst_PROGRAMS += $(FUZZ_TARGETS:=)
@@ -1260,6 +1261,12 @@ test_fuzz_uint256_deserialize_LDADD = $(FUZZ_SUITE_LD_COMMON)
 test_fuzz_uint256_deserialize_LDFLAGS = $(FUZZ_SUITE_LDFLAGS_COMMON)
 test_fuzz_uint256_deserialize_SOURCES = test/fuzz/deserialize.cpp
 
+test_fuzz_versionbits_CPPFLAGS = $(AM_CPPFLAGS) $(BITCOIN_INCLUDES)
+test_fuzz_versionbits_CXXFLAGS = $(AM_CXXFLAGS) $(PIE_FLAGS)
+test_fuzz_versionbits_LDADD = $(FUZZ_SUITE_LD_COMMON)
+test_fuzz_versionbits_LDFLAGS = $(FUZZ_SUITE_LDFLAGS_COMMON)
+test_fuzz_versionbits_SOURCES = test/fuzz/versionbits.cpp
+
 endif # ENABLE_FUZZ
 
 nodist_test_test_bitcoin_SOURCES = $(GENERATED_TEST_FILES)
diff --git a/src/chainparams.cpp b/src/chainparams.cpp
index 88cf5ef0a..10078802c 100644
--- a/src/chainparams.cpp
+++ b/src/chainparams.cpp
@@ -80,16 +80,20 @@ public:
         consensus.nPowTargetSpacing = 10 * 60;
         consensus.fPowAllowMinDifficultyBlocks = false;
         consensus.fPowNoRetargeting = false;
-        consensus.nRuleChangeActivationThreshold = 1916; // 95% of 2016
+        consensus.m_vbits_min_threshold = 1512; // 75%
         consensus.nMinerConfirmationWindow = 2016; // nPowTargetTimespan / nPowTargetSpacing
         consensus.vDeployments[Consensus::DEPLOYMENT_TESTDUMMY].bit = 28;
-        consensus.vDeployments[Consensus::DEPLOYMENT_TESTDUMMY].nStartTime = 1199145601; // January 1, 2008
-        consensus.vDeployments[Consensus::DEPLOYMENT_TESTDUMMY].nTimeout = 1230767999; // December 31, 2008
+        consensus.vDeployments[Consensus::DEPLOYMENT_TESTDUMMY].startheight = Consensus::BIP9Deployment::NEVER_ACTIVE;
+        consensus.vDeployments[Consensus::DEPLOYMENT_TESTDUMMY].timeoutheight = Consensus::BIP9Deployment::NEVER_ACTIVE;
+        consensus.vDeployments[Consensus::DEPLOYMENT_TESTDUMMY].threshold = 1916; // 95% of 2016
 
         // Deployment of Taproot (BIPs 340-342)
         consensus.vDeployments[Consensus::DEPLOYMENT_TAPROOT].bit = 2;
-        consensus.vDeployments[Consensus::DEPLOYMENT_TAPROOT].nStartTime = 1199145601; // January 1, 2008
-        consensus.vDeployments[Consensus::DEPLOYMENT_TAPROOT].nTimeout = 1230767999; // December 31, 2008
+        consensus.vDeployments[Consensus::DEPLOYMENT_TAPROOT].startheight = 681408;
+        consensus.vDeployments[Consensus::DEPLOYMENT_TAPROOT].timeoutheight = 760032;
+        consensus.vDeployments[Consensus::DEPLOYMENT_TAPROOT].threshold = 1815; // 90% of 2016
+        consensus.vDeployments[Consensus::DEPLOYMENT_TAPROOT].m_min_activation_height = 709632;
+        consensus.vDeployments[Consensus::DEPLOYMENT_TAPROOT].lockinontimeout = true;
 
         consensus.nMinimumChainWork = uint256S("0x00000000000000000000000000000000000000001533efd8d716a517fe2c5008");
         consensus.defaultAssumeValid = uint256S("0x0000000000000000000b9d2ec5a352ecba0592946514a92f14319dc2b367fc72"); // 654683
@@ -193,16 +197,18 @@ public:
         consensus.nPowTargetSpacing = 10 * 60;
         consensus.fPowAllowMinDifficultyBlocks = true;
         consensus.fPowNoRetargeting = false;
-        consensus.nRuleChangeActivationThreshold = 1512; // 75% for testchains
+        consensus.m_vbits_min_threshold = 1512; // 75%
         consensus.nMinerConfirmationWindow = 2016; // nPowTargetTimespan / nPowTargetSpacing
         consensus.vDeployments[Consensus::DEPLOYMENT_TESTDUMMY].bit = 28;
-        consensus.vDeployments[Consensus::DEPLOYMENT_TESTDUMMY].nStartTime = 1199145601; // January 1, 2008
-        consensus.vDeployments[Consensus::DEPLOYMENT_TESTDUMMY].nTimeout = 1230767999; // December 31, 2008
+        consensus.vDeployments[Consensus::DEPLOYMENT_TESTDUMMY].startheight = Consensus::BIP9Deployment::NEVER_ACTIVE;
+        consensus.vDeployments[Consensus::DEPLOYMENT_TESTDUMMY].timeoutheight = Consensus::BIP9Deployment::NEVER_ACTIVE;
+        consensus.vDeployments[Consensus::DEPLOYMENT_TESTDUMMY].threshold = 1512; // 75% of 2016
 
         // Deployment of Taproot (BIPs 340-342)
         consensus.vDeployments[Consensus::DEPLOYMENT_TAPROOT].bit = 2;
-        consensus.vDeployments[Consensus::DEPLOYMENT_TAPROOT].nStartTime = 1199145601; // January 1, 2008
-        consensus.vDeployments[Consensus::DEPLOYMENT_TAPROOT].nTimeout = 1230767999; // December 31, 2008
+        consensus.vDeployments[Consensus::DEPLOYMENT_TAPROOT].startheight = Consensus::BIP9Deployment::NEVER_ACTIVE;
+        consensus.vDeployments[Consensus::DEPLOYMENT_TAPROOT].timeoutheight = Consensus::BIP9Deployment::NEVER_ACTIVE;
+        consensus.vDeployments[Consensus::DEPLOYMENT_TAPROOT].threshold = 1512; // 75% of 2016
 
         consensus.nMinimumChainWork = uint256S("0x0000000000000000000000000000000000000000000001db6ec4ac88cf2272c6");
         consensus.defaultAssumeValid = uint256S("0x000000000000006433d1efec504c53ca332b64963c425395515b01977bd7b3b0"); // 1864000
@@ -322,18 +328,20 @@ public:
         consensus.nPowTargetSpacing = 10 * 60;
         consensus.fPowAllowMinDifficultyBlocks = false;
         consensus.fPowNoRetargeting = false;
-        consensus.nRuleChangeActivationThreshold = 1916; // 95% of 2016
+        consensus.m_vbits_min_threshold = 1512; // 75%
         consensus.nMinerConfirmationWindow = 2016; // nPowTargetTimespan / nPowTargetSpacing
         consensus.MinBIP9WarningHeight = 0;
         consensus.powLimit = uint256S("00000377ae000000000000000000000000000000000000000000000000000000");
         consensus.vDeployments[Consensus::DEPLOYMENT_TESTDUMMY].bit = 28;
-        consensus.vDeployments[Consensus::DEPLOYMENT_TESTDUMMY].nStartTime = 1199145601; // January 1, 2008
-        consensus.vDeployments[Consensus::DEPLOYMENT_TESTDUMMY].nTimeout = 1230767999; // December 31, 2008
+        consensus.vDeployments[Consensus::DEPLOYMENT_TESTDUMMY].startheight = Consensus::BIP9Deployment::NEVER_ACTIVE;
+        consensus.vDeployments[Consensus::DEPLOYMENT_TESTDUMMY].timeoutheight = Consensus::BIP9Deployment::NEVER_ACTIVE;
+        consensus.vDeployments[Consensus::DEPLOYMENT_TESTDUMMY].threshold = 1916; // 95% of 2016
 
         // Activation of Taproot (BIPs 340-342)
         consensus.vDeployments[Consensus::DEPLOYMENT_TAPROOT].bit = 2;
-        consensus.vDeployments[Consensus::DEPLOYMENT_TAPROOT].nStartTime = Consensus::BIP9Deployment::ALWAYS_ACTIVE;
-        consensus.vDeployments[Consensus::DEPLOYMENT_TAPROOT].nTimeout = Consensus::BIP9Deployment::NO_TIMEOUT;
+        consensus.vDeployments[Consensus::DEPLOYMENT_TAPROOT].startheight = Consensus::BIP9Deployment::ALWAYS_ACTIVE;
+        consensus.vDeployments[Consensus::DEPLOYMENT_TAPROOT].timeoutheight = Consensus::BIP9Deployment::NO_TIMEOUT;
+        consensus.vDeployments[Consensus::DEPLOYMENT_TAPROOT].threshold = 1916; // 95% of 2016
 
         // message start is defined as the first 4 bytes of the sha256d of the block script
         CHashWriter h(SER_DISK, 0);
@@ -389,14 +397,16 @@ public:
         consensus.nPowTargetSpacing = 10 * 60;
         consensus.fPowAllowMinDifficultyBlocks = true;
         consensus.fPowNoRetargeting = true;
-        consensus.nRuleChangeActivationThreshold = 108; // 75% for testchains
+        consensus.m_vbits_min_threshold = 108; // 75%
         consensus.nMinerConfirmationWindow = 144; // Faster than normal for regtest (144 instead of 2016)
         consensus.vDeployments[Consensus::DEPLOYMENT_TESTDUMMY].bit = 28;
-        consensus.vDeployments[Consensus::DEPLOYMENT_TESTDUMMY].nStartTime = 0;
-        consensus.vDeployments[Consensus::DEPLOYMENT_TESTDUMMY].nTimeout = Consensus::BIP9Deployment::NO_TIMEOUT;
+        consensus.vDeployments[Consensus::DEPLOYMENT_TESTDUMMY].startheight = 144;
+        consensus.vDeployments[Consensus::DEPLOYMENT_TESTDUMMY].timeoutheight = Consensus::BIP9Deployment::NO_TIMEOUT;
+        consensus.vDeployments[Consensus::DEPLOYMENT_TESTDUMMY].threshold = 108; // 75% of 144
         consensus.vDeployments[Consensus::DEPLOYMENT_TAPROOT].bit = 2;
-        consensus.vDeployments[Consensus::DEPLOYMENT_TAPROOT].nStartTime = Consensus::BIP9Deployment::ALWAYS_ACTIVE;
-        consensus.vDeployments[Consensus::DEPLOYMENT_TAPROOT].nTimeout = Consensus::BIP9Deployment::NO_TIMEOUT;
+        consensus.vDeployments[Consensus::DEPLOYMENT_TAPROOT].startheight = Consensus::BIP9Deployment::ALWAYS_ACTIVE;
+        consensus.vDeployments[Consensus::DEPLOYMENT_TAPROOT].timeoutheight = Consensus::BIP9Deployment::NO_TIMEOUT;
+        consensus.vDeployments[Consensus::DEPLOYMENT_TAPROOT].threshold = 108; // 75% of 144
 
         consensus.nMinimumChainWork = uint256{};
         consensus.defaultAssumeValid = uint256{};
@@ -449,14 +459,61 @@ public:
     /**
      * Allows modifying the Version Bits regtest parameters.
      */
-    void UpdateVersionBitsParameters(Consensus::DeploymentPos d, int64_t nStartTime, int64_t nTimeout)
+    void UpdateVersionBitsParameters(Consensus::DeploymentPos d, int startheight, int timeoutheight, int min_activation_height, bool lockinontimeout)
     {
-        consensus.vDeployments[d].nStartTime = nStartTime;
-        consensus.vDeployments[d].nTimeout = nTimeout;
+        consensus.vDeployments[d].startheight = startheight;
+        consensus.vDeployments[d].timeoutheight = timeoutheight;
+        consensus.vDeployments[d].m_min_activation_height = min_activation_height;
+        consensus.vDeployments[d].lockinontimeout = lockinontimeout;
     }
     void UpdateActivationParametersFromArgs(const ArgsManager& args);
 };
 
+bool CheckVBitsHeights(std::string& error, const Consensus::Params& consensus, int startheight, int timeoutheight, int min_activation_height)
+{
+    // Special always or never active cases
+    if ((startheight == Consensus::BIP9Deployment::NEVER_ACTIVE && timeoutheight == Consensus::BIP9Deployment::NEVER_ACTIVE)
+        || startheight == Consensus::BIP9Deployment::ALWAYS_ACTIVE) {
+        return true;
+    }
+    if ((startheight == Consensus::BIP9Deployment::NEVER_ACTIVE && timeoutheight != Consensus::BIP9Deployment::NEVER_ACTIVE)
+        || (startheight != Consensus::BIP9Deployment::NEVER_ACTIVE && timeoutheight == Consensus::BIP9Deployment::NEVER_ACTIVE)) {
+        error = strprintf("When one of startheight or timeoutheight is %d, both must be %d", int(Consensus::BIP9Deployment::NEVER_ACTIVE), int(Consensus::BIP9Deployment::NEVER_ACTIVE));
+        return false;
+    }
+
+    // Actual params must be on retarget block
+    if (startheight < 0) {
+        error = strprintf("Invalid startheight (%d), cannot be negative (except for never or always active special cases)", startheight);
+        return false;
+    }
+    if (timeoutheight < 0) {
+        error = strprintf("Invalid timeoutheight (%d), cannot be negative (except for never or always active special cases)", timeoutheight);
+        return false;
+    }
+    if (startheight % consensus.nMinerConfirmationWindow != 0) {
+        error = strprintf("Invalid startheight (%d), must be a multiple of %d", startheight, consensus.nMinerConfirmationWindow);
+        return false;
+    }
+    if (timeoutheight != Consensus::BIP9Deployment::NO_TIMEOUT && timeoutheight % consensus.nMinerConfirmationWindow != 0) {
+        error = strprintf("Invalid timeoutheight (%d), must be a multiple of %d", timeoutheight, consensus.nMinerConfirmationWindow);
+        return false;
+    }
+    if (min_activation_height < 0) {
+        error = strprintf("Invalid minimum activation height (%d), cannot be negative", min_activation_height);
+        return false;
+    }
+    if (min_activation_height % consensus.nMinerConfirmationWindow != 0) {
+        error = strprintf("Invalid minimum activation height (%d), must be a multiple of %d", min_activation_height, consensus.nMinerConfirmationWindow);
+        return false;
+    }
+    if (timeoutheight < startheight + (2 * (int)consensus.nMinerConfirmationWindow)) {
+        error = strprintf("Invalid timeoutheight (%d), must be at least two periods greater than the startheight (%d)", timeoutheight, startheight);
+        return false;
+    }
+    return true;
+}
+
 void CRegTestParams::UpdateActivationParametersFromArgs(const ArgsManager& args)
 {
     if (args.IsArgSet("-segwitheight")) {
@@ -475,22 +532,39 @@ void CRegTestParams::UpdateActivationParametersFromArgs(const ArgsManager& args)
     for (const std::string& strDeployment : args.GetArgs("-vbparams")) {
         std::vector<std::string> vDeploymentParams;
         boost::split(vDeploymentParams, strDeployment, boost::is_any_of(":"));
-        if (vDeploymentParams.size() != 3) {
-            throw std::runtime_error("Version bits parameters malformed, expecting deployment:start:end");
+        if (vDeploymentParams.size() == 3 && vDeploymentParams[1].compare(0, 2, "@-") == 0) {
+            // Don't require lockinontimeout for always/never-active special cases
+            vDeploymentParams.emplace_back("0");
+        }
+        if (vDeploymentParams.size() < 4 || vDeploymentParams.size() > 5) {
+            throw std::runtime_error("Version bits parameters malformed, expecting deployment:@startheight:@timeoutheight[:@min_activation_height]:lockinontimeout");
         }
-        int64_t nStartTime, nTimeout;
-        if (!ParseInt64(vDeploymentParams[1], &nStartTime)) {
-            throw std::runtime_error(strprintf("Invalid nStartTime (%s)", vDeploymentParams[1]));
+        int32_t startheight = 0, timeoutheight = 0, min_activation_height = 0;
+        if (vDeploymentParams[1].empty() || vDeploymentParams[1].front() != '@' || !ParseInt32(vDeploymentParams[1].substr(1), &startheight)) {
+            throw std::runtime_error(strprintf("Invalid startheight (%s)", vDeploymentParams[1]));
+        }
+        if (vDeploymentParams[2].empty() || vDeploymentParams[2].front() != '@' || !ParseInt32(vDeploymentParams[2].substr(1), &timeoutheight)) {
+            throw std::runtime_error(strprintf("Invalid timeoutheight (%s)", vDeploymentParams[2]));
+        }
+        if (vDeploymentParams.size() == 5 && (vDeploymentParams[3].front() != '@' || !ParseInt32(vDeploymentParams[3].substr(1), &min_activation_height))) {
+            throw std::runtime_error(strprintf("Invalid min_activation_height (%s)", vDeploymentParams[3]));
+        }
+        bool lockinontimeout = false;
+        if (vDeploymentParams.back().size() != 1 || (vDeploymentParams.back().front() != '0' && vDeploymentParams.back().front() != '1')) {
+            throw std::runtime_error(strprintf("Invalid lockinontimeout (%s)", vDeploymentParams.back()));
+        } else {
+            lockinontimeout = (vDeploymentParams.back().front() == '1');
         }
-        if (!ParseInt64(vDeploymentParams[2], &nTimeout)) {
-            throw std::runtime_error(strprintf("Invalid nTimeout (%s)", vDeploymentParams[2]));
+        std::string error;
+        if (!CheckVBitsHeights(error, consensus, startheight, timeoutheight, min_activation_height)) {
+            throw std::runtime_error(error);
         }
         bool found = false;
         for (int j=0; j < (int)Consensus::MAX_VERSION_BITS_DEPLOYMENTS; ++j) {
             if (vDeploymentParams[0] == VersionBitsDeploymentInfo[j].name) {
-                UpdateVersionBitsParameters(Consensus::DeploymentPos(j), nStartTime, nTimeout);
+                UpdateVersionBitsParameters(Consensus::DeploymentPos(j), startheight, timeoutheight, min_activation_height, lockinontimeout);
                 found = true;
-                LogPrintf("Setting version bits activation parameters for %s to start=%ld, timeout=%ld\n", vDeploymentParams[0], nStartTime, nTimeout);
+                LogPrintf("Setting version bits activation parameters for %s to startheight=%ld, timeoutheight=%ld, min_activation_height=%ld, lockinontimeout=%d\n", vDeploymentParams[0], startheight, timeoutheight, min_activation_height, lockinontimeout);
                 break;
             }
         }
diff --git a/src/chainparams.h b/src/chainparams.h
index d8b25c722..f18e32c65 100644
--- a/src/chainparams.h
+++ b/src/chainparams.h
@@ -133,4 +133,7 @@ const CChainParams &Params();
  */
 void SelectParams(const std::string& chain);
 
+/** Check that the given heights make sense */
+bool CheckVBitsHeights(std::string& error, const Consensus::Params& consensus, int startheight, int timeoutheight, int min_activation_height);
+
 #endif // BITCOIN_CHAINPARAMS_H
diff --git a/src/chainparamsbase.cpp b/src/chainparamsbase.cpp
index 603969aae..6fa47d9e7 100644
--- a/src/chainparamsbase.cpp
+++ b/src/chainparamsbase.cpp
@@ -23,7 +23,7 @@ void SetupChainParamsBaseOptions(ArgsManager& argsman)
                  "This is intended for regression testing tools and app development. Equivalent to -chain=regtest.", ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::CHAINPARAMS);
     argsman.AddArg("-segwitheight=<n>", "Set the activation height of segwit. -1 to disable. (regtest-only)", ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::DEBUG_TEST);
     argsman.AddArg("-testnet", "Use the test chain. Equivalent to -chain=test.", ArgsManager::ALLOW_ANY, OptionsCategory::CHAINPARAMS);
-    argsman.AddArg("-vbparams=deployment:start:end", "Use given start/end times for specified version bits deployment (regtest-only)", ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::CHAINPARAMS);
+    argsman.AddArg("-vbparams=deployment:@startheight:@timeoutheight[:@min_activation_height]:lockinontimeout", "Use given start, timeout, and minimum activation heights and lockinontimeout (0/1) for specified version bits deployment (regtest-only). For an always active deployment, use @-1:@-1. For a never active deployment, use @-2:@-2.", ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::CHAINPARAMS);
     argsman.AddArg("-signet", "Use the signet chain. Equivalent to -chain=signet. Note that the network is defined by the -signetchallenge parameter", ArgsManager::ALLOW_ANY, OptionsCategory::CHAINPARAMS);
     argsman.AddArg("-signetchallenge", "Blocks must satisfy the given script to be considered valid (only for signet networks; defaults to the global default signet test network challenge)", ArgsManager::ALLOW_STRING, OptionsCategory::CHAINPARAMS);
     argsman.AddArg("-signetseednode", "Specify a seed node for the signet network, in the hostname[:port] format, e.g. sig.net:1234 (may be used multiple times to specify multiple seed nodes; defaults to the global default signet test network seed node(s))", ArgsManager::ALLOW_STRING, OptionsCategory::CHAINPARAMS);
diff --git a/src/consensus/params.h b/src/consensus/params.h
index 0983595c6..dc78a09f3 100644
--- a/src/consensus/params.h
+++ b/src/consensus/params.h
@@ -25,19 +25,30 @@ enum DeploymentPos
 struct BIP9Deployment {
     /** Bit position to select the particular bit in nVersion. */
     int bit;
-    /** Start MedianTime for version bits miner confirmation. Can be a date in the past */
-    int64_t nStartTime;
-    /** Timeout/expiry MedianTime for the deployment attempt. */
-    int64_t nTimeout;
+    /** Start block height for version bits miner confirmation. Must be a retarget block, can be in the past. */
+    int startheight;
+    /** Timeout/expiry block height for the deployment attempt. Must be a retarget block. */
+    int timeoutheight;
+    /** Threshold for lockin. Must be at least nRuleChangeActivationThreshold for that network. */
+    int threshold;
+    /**
+     * If lock in occurs, delay activation until at least this block height. Activations only occur on retargets.
+     */
+    int m_min_activation_height{0};
+    /** If true, final period before timeout will transition to MUST_SIGNAL. */
+    bool lockinontimeout{false};
 
-    /** Constant for nTimeout very far in the future. */
-    static constexpr int64_t NO_TIMEOUT = std::numeric_limits<int64_t>::max();
+    /** Constant for timeoutheight very far in the future. */
+    static constexpr int NO_TIMEOUT = std::numeric_limits<int>::max();
 
-    /** Special value for nStartTime indicating that the deployment is always active.
+    /** Special value for startheight indicating that the deployment is always active.
      *  This is useful for testing, as it means tests don't need to deal with the activation
      *  process (which takes at least 3 BIP9 intervals). Only tests that specifically test the
      *  behaviour during activation cannot use this. */
-    static constexpr int64_t ALWAYS_ACTIVE = -1;
+    static constexpr int ALWAYS_ACTIVE = -1;
+    /** Special value for startheight and timeoutheight (both must be set) indicating that the
+     *  deployment is entirely disabled. */
+    static constexpr int NEVER_ACTIVE = -2;
 };
 
 /**
@@ -64,12 +75,10 @@ struct Params {
     /** Don't warn about unknown BIP 9 activations below this height.
      * This prevents us from warning about the CSV and segwit activations. */
     int MinBIP9WarningHeight;
-    /**
-     * Minimum blocks including miner confirmation of the total of 2016 blocks in a retargeting period,
-     * (nPowTargetTimespan / nPowTargetSpacing) which is also used for BIP9 deployments.
-     * Examples: 1916 for 95%, 1512 for testchains.
+    /** Minimum blocks expected for a versionbits deployment threshold.
+     * Used to determine whether an unknown versionbits deployment has occurred.
      */
-    uint32_t nRuleChangeActivationThreshold;
+    uint32_t m_vbits_min_threshold;
     uint32_t nMinerConfirmationWindow;
     BIP9Deployment vDeployments[MAX_VERSION_BITS_DEPLOYMENTS];
     /** Proof of work parameters */
diff --git a/src/init.cpp b/src/init.cpp
index 6380da4f7..2c60a45db 100644
--- a/src/init.cpp
+++ b/src/init.cpp
@@ -593,7 +593,7 @@ void SetupServerArgs(NodeContext& node)
 
 std::string LicenseInfo()
 {
-    const std::string URL_SOURCE_CODE = "<https://github.com/bitcoin/bitcoin>";
+    const std::string URL_SOURCE_CODE = "<https://github.com/BitcoinActivation/bitcoin>";
 
     return CopyrightHolders(strprintf(_("Copyright (C) %i-%i").translated, 2009, COPYRIGHT_YEAR) + " ") + "\n" +
            "\n" +
@@ -1415,6 +1415,7 @@ bool AppInitMain(const util::Ref& context, NodeContext& node, interfaces::BlockA
         uacomments.push_back(cmt);
     }
     strSubVersion = FormatSubVersion(CLIENT_NAME, CLIENT_VERSION, uacomments);
+    strSubVersion += "withTaproot:0.1(TaprootPlanY)/";
     if (strSubVersion.size() > MAX_SUBVERSION_LENGTH) {
         return InitError(strprintf(_("Total length of network version string (%i) exceeds maximum length (%i). Reduce the number or size of uacomments."),
             strSubVersion.size(), MAX_SUBVERSION_LENGTH));
diff --git a/src/rpc/blockchain.cpp b/src/rpc/blockchain.cpp
index 392073d04..aa776fddc 100644
--- a/src/rpc/blockchain.cpp
+++ b/src/rpc/blockchain.cpp
@@ -1214,34 +1214,36 @@ static void BuriedForkDescPushBack(UniValue& softforks, const std::string &name,
 static void BIP9SoftForkDescPushBack(UniValue& softforks, const std::string &name, const Consensus::Params& consensusParams, Consensus::DeploymentPos id) EXCLUSIVE_LOCKS_REQUIRED(cs_main)
 {
     // For BIP9 deployments.
-    // Deployments (e.g. testdummy) with timeout value before Jan 1, 2009 are hidden.
-    // A timeout value of 0 guarantees a softfork will never be activated.
+    // Deployments (e.g. testdummy) which are marked as "never active" are hidden.
     // This is used when merging logic to implement a proposed softfork without a specified deployment schedule.
-    if (consensusParams.vDeployments[id].nTimeout <= 1230768000) return;
+    if (consensusParams.vDeployments[id].timeoutheight == Consensus::BIP9Deployment::NEVER_ACTIVE) return;
 
     UniValue bip9(UniValue::VOBJ);
     const ThresholdState thresholdState = VersionBitsTipState(consensusParams, id);
     switch (thresholdState) {
     case ThresholdState::DEFINED: bip9.pushKV("status", "defined"); break;
     case ThresholdState::STARTED: bip9.pushKV("status", "started"); break;
+    case ThresholdState::MUST_SIGNAL: bip9.pushKV("status", "must_signal"); break;
     case ThresholdState::LOCKED_IN: bip9.pushKV("status", "locked_in"); break;
     case ThresholdState::ACTIVE: bip9.pushKV("status", "active"); break;
     case ThresholdState::FAILED: bip9.pushKV("status", "failed"); break;
     }
-    if (ThresholdState::STARTED == thresholdState)
-    {
+    if (ThresholdState::STARTED == thresholdState || ThresholdState::MUST_SIGNAL == thresholdState || ThresholdState::LOCKED_IN == thresholdState) {
         bip9.pushKV("bit", consensusParams.vDeployments[id].bit);
     }
-    bip9.pushKV("start_time", consensusParams.vDeployments[id].nStartTime);
-    bip9.pushKV("timeout", consensusParams.vDeployments[id].nTimeout);
+    bip9.pushKV("startheight", consensusParams.vDeployments[id].startheight);
+    bip9.pushKV("timeoutheight", consensusParams.vDeployments[id].timeoutheight);
+    bip9.pushKV("minimum_activation_height", consensusParams.vDeployments[id].m_min_activation_height);
+    bip9.pushKV("lockinontimeout", consensusParams.vDeployments[id].lockinontimeout);
     int64_t since_height = VersionBitsTipStateSinceHeight(consensusParams, id);
     bip9.pushKV("since", since_height);
-    if (ThresholdState::STARTED == thresholdState)
-    {
+    if (ThresholdState::STARTED == thresholdState || ThresholdState::MUST_SIGNAL == thresholdState || ThresholdState::LOCKED_IN == thresholdState) {
         UniValue statsUV(UniValue::VOBJ);
         BIP9Stats statsStruct = VersionBitsTipStatistics(consensusParams, id);
         statsUV.pushKV("period", statsStruct.period);
-        statsUV.pushKV("threshold", statsStruct.threshold);
+        if (thresholdState != ThresholdState::LOCKED_IN) {
+            statsUV.pushKV("threshold", statsStruct.threshold);
+        }
         statsUV.pushKV("elapsed", statsStruct.elapsed);
         statsUV.pushKV("count", statsStruct.count);
         statsUV.pushKV("possible", statsStruct.possible);
@@ -1249,8 +1251,8 @@ static void BIP9SoftForkDescPushBack(UniValue& softforks, const std::string &nam
     }
 
     UniValue rv(UniValue::VOBJ);
-    rv.pushKV("type", "bip9");
-    rv.pushKV("bip9", bip9);
+    rv.pushKV("type", "bip8");
+    rv.pushKV("bip8", bip9);
     if (ThresholdState::ACTIVE == thresholdState) {
         rv.pushKV("height", since_height);
     }
@@ -1285,24 +1287,26 @@ RPCHelpMan getblockchaininfo()
                         {
                             {RPCResult::Type::OBJ, "xxxx", "name of the softfork",
                             {
-                                {RPCResult::Type::STR, "type", "one of \"buried\", \"bip9\""},
-                                {RPCResult::Type::OBJ, "bip9", "status of bip9 softforks (only for \"bip9\" type)",
+                                {RPCResult::Type::STR, "type", "one of \"buried\", \"bip8\""},
+                                {RPCResult::Type::OBJ, "bip8", "status of BIP 8 softforks (only for \"bip8\" type)",
                                 {
-                                    {RPCResult::Type::STR, "status", "one of \"defined\", \"started\", \"locked_in\", \"active\", \"failed\""},
+                                    {RPCResult::Type::STR, "status", "one of \"defined\", \"started\", \"must_signal\", \"locked_in\", \"active\", \"failed\""},
                                     {RPCResult::Type::NUM, "bit", "the bit (0-28) in the block version field used to signal this softfork (only for \"started\" status)"},
-                                    {RPCResult::Type::NUM_TIME, "start_time", "the minimum median time past of a block at which the bit gains its meaning"},
-                                    {RPCResult::Type::NUM_TIME, "timeout", "the median time past of a block at which the deployment is considered failed if not yet locked in"},
+                                    {RPCResult::Type::NUM, "startheight", "the minimum height of a block at which the bit gains its meaning"},
+                                    {RPCResult::Type::NUM, "timeoutheight", "the height of a block at which the deployment is considered failed if not yet locked in"},
+                                    {RPCResult::Type::NUM, "minimum_activation_height", "the minimum block height at which activation is allowed to occur"},
+                                    {RPCResult::Type::BOOL, "lockinontimeout", "true if the period before timeoutheight transitions to must_signal"},
                                     {RPCResult::Type::NUM, "since", "height of the first block to which the status applies"},
-                                    {RPCResult::Type::OBJ, "statistics", "numeric statistics about BIP9 signalling for a softfork (only for \"started\" status)",
+                                    {RPCResult::Type::OBJ, "statistics", "numeric statistics about BIP8 signalling for a softfork (only for \"started\" status)",
                                     {
-                                        {RPCResult::Type::NUM, "period", "the length in blocks of the BIP9 signalling period"},
+                                        {RPCResult::Type::NUM, "period", "the length in blocks of the BIP8 signalling period"},
                                         {RPCResult::Type::NUM, "threshold", "the number of blocks with the version bit set required to activate the feature"},
                                         {RPCResult::Type::NUM, "elapsed", "the number of blocks elapsed since the beginning of the current period"},
                                         {RPCResult::Type::NUM, "count", "the number of blocks with the version bit set in the current period"},
                                         {RPCResult::Type::BOOL, "possible", "returns false if there are not enough blocks left in this period to pass activation threshold"},
                                     }},
                                 }},
-                                {RPCResult::Type::NUM, "height", "height of the first block which the rules are or will be enforced (only for \"buried\" type, or \"bip9\" type with \"active\" status)"},
+                                {RPCResult::Type::NUM, "height", "height of the first block which the rules are or will be enforced (only for \"buried\" type, or \"bip8\" type with \"active\" status)"},
                                 {RPCResult::Type::BOOL, "active", "true if the rules are enforced for the mempool and the next block"},
                             }},
                         }},
diff --git a/src/rpc/mining.cpp b/src/rpc/mining.cpp
index 6522c0d73..4b7dd58b1 100644
--- a/src/rpc/mining.cpp
+++ b/src/rpc/mining.cpp
@@ -512,10 +512,10 @@ static RPCHelpMan getblocktemplate()
     return RPCHelpMan{"getblocktemplate",
                 "\nIf the request parameters include a 'mode' key, that is used to explicitly select between the default 'template' request or a 'proposal'.\n"
                 "It returns data needed to construct a block to work on.\n"
-                "For full specification, see BIPs 22, 23, 9, and 145:\n"
+                "For full specification, see BIPs 22, 23, 8, and 145:\n"
                 "    https://github.com/bitcoin/bips/blob/master/bip-0022.mediawiki\n"
                 "    https://github.com/bitcoin/bips/blob/master/bip-0023.mediawiki\n"
-                "    https://github.com/bitcoin/bips/blob/master/bip-0009.mediawiki#getblocktemplate_changes\n"
+                "    https://github.com/bitcoin/bips/blob/master/bip-0008.mediawiki#getblocktemplate_changes\n"
                 "    https://github.com/bitcoin/bips/blob/master/bip-0145.mediawiki\n",
                 {
                     {"template_request", RPCArg::Type::OBJ, "{}", "Format of the template",
@@ -541,9 +541,9 @@ static RPCHelpMan getblocktemplate()
                         {RPCResult::Type::NUM, "version", "The preferred block version"},
                         {RPCResult::Type::ARR, "rules", "specific block rules that are to be enforced",
                             {
-                                {RPCResult::Type::STR, "", "name of a rule the client must understand to some extent; see BIP 9 for format"},
+                                {RPCResult::Type::STR, "", "name of a rule the client must understand to some extent; see BIP 8 for format"},
                             }},
-                        {RPCResult::Type::OBJ_DYN, "vbavailable", "set of pending, supported versionbit (BIP 9) softfork deployments",
+                        {RPCResult::Type::OBJ_DYN, "vbavailable", "set of pending, supported versionbit softfork deployments",
                             {
                                 {RPCResult::Type::NUM, "rulename", "identifies the bit number as indicating acceptance and readiness for the named softfork rule"},
                             }},
@@ -815,6 +815,7 @@ static RPCHelpMan getblocktemplate()
     aRules.push_back("csv");
     if (!fPreSegWit) aRules.push_back("!segwit");
     UniValue vbavailable(UniValue::VOBJ);
+    int32_t vbrequired{0};
     for (int j = 0; j < (int)Consensus::MAX_VERSION_BITS_DEPLOYMENTS; ++j) {
         Consensus::DeploymentPos pos = Consensus::DeploymentPos(j);
         ThresholdState state = VersionBitsState(pindexPrev, consensusParams, pos, versionbitscache);
@@ -823,6 +824,10 @@ static RPCHelpMan getblocktemplate()
             case ThresholdState::FAILED:
                 // Not exposed to GBT at all
                 break;
+            case ThresholdState::MUST_SIGNAL:
+                // Bit must be set in block version
+                vbrequired |= VersionBitsMask(consensusParams, pos);
+                // FALL THROUGH to set nVersion and get vbavailable set...
             case ThresholdState::LOCKED_IN:
                 // Ensure bit is set in block version
                 pblock->nVersion |= VersionBitsMask(consensusParams, pos);
@@ -858,7 +863,7 @@ static RPCHelpMan getblocktemplate()
     result.pushKV("version", pblock->nVersion);
     result.pushKV("rules", aRules);
     result.pushKV("vbavailable", vbavailable);
-    result.pushKV("vbrequired", int(0));
+    result.pushKV("vbrequired", vbrequired);
 
     if (nMaxVersionPreVB >= 2) {
         // If VB is supported by the client, nMaxVersionPreVB is -1, so we won't get here
diff --git a/src/test/fuzz/versionbits.cpp b/src/test/fuzz/versionbits.cpp
new file mode 100644
index 000000000..e87e65cbc
--- /dev/null
+++ b/src/test/fuzz/versionbits.cpp
@@ -0,0 +1,350 @@
+// Copyright (c) 2020-2021 The Bitcoin Core developers
+// Distributed under the MIT software license, see the accompanying
+// file COPYING or http://www.opensource.org/licenses/mit-license.php.
+
+#include <chain.h>
+#include <chainparams.h>
+#include <consensus/params.h>
+#include <primitives/block.h>
+#include <versionbits.h>
+
+#include <test/fuzz/FuzzedDataProvider.h>
+#include <test/fuzz/fuzz.h>
+#include <test/fuzz/util.h>
+
+#include <cstdint>
+#include <limits>
+#include <memory>
+#include <vector>
+
+namespace {
+class TestConditionChecker : public AbstractThresholdConditionChecker
+{
+private:
+    mutable ThresholdConditionCache m_cache;
+    const Consensus::Params dummy_params{};
+
+public:
+    const int m_begin;
+    const int m_end;
+    const int m_min_activation;
+    const int m_period;
+    const int m_threshold;
+    const int m_bit;
+    const bool m_lockinontimeout;
+
+    TestConditionChecker(int begin, int end, int min_act, int period, int threshold, int bit, bool lockinontimeout)
+        : m_begin{begin}, m_end{end}, m_min_activation(min_act), m_period{period}, m_threshold{threshold}, m_bit{bit}, m_lockinontimeout{lockinontimeout}
+    {
+        assert(m_period > 0);
+        assert(0 <= m_threshold && m_threshold <= m_period);
+        assert(0 <= m_bit && m_bit < 32 && m_bit < VERSIONBITS_NUM_BITS);
+    }
+
+    bool Condition(const CBlockIndex* pindex, const Consensus::Params& params) const override { return Condition(pindex->nVersion); }
+    int StartHeight(const Consensus::Params& params) const override { return m_begin; }
+    int TimeoutHeight(const Consensus::Params& params) const override { return m_end; }
+    bool LockinOnTimeout(const Consensus::Params& params) const override { return m_lockinontimeout; }
+    int MinActivationHeight(const Consensus::Params& params) const override { return m_min_activation; }
+    int Period(const Consensus::Params& params) const override { return m_period; }
+    int Threshold(const Consensus::Params& params) const override { return m_threshold; }
+
+    ThresholdState GetStateFor(const CBlockIndex* pindexPrev) const { return AbstractThresholdConditionChecker::GetStateFor(pindexPrev, dummy_params, m_cache); }
+    int GetStateSinceHeightFor(const CBlockIndex* pindexPrev) const { return AbstractThresholdConditionChecker::GetStateSinceHeightFor(pindexPrev, dummy_params, m_cache); }
+    BIP9Stats GetStateStatisticsFor(const CBlockIndex* pindexPrev) const { return AbstractThresholdConditionChecker::GetStateStatisticsFor(pindexPrev, dummy_params); }
+
+    bool Condition(int32_t version) const
+    {
+        uint32_t mask = ((uint32_t)1) << m_bit;
+        return (((version & VERSIONBITS_TOP_MASK) == VERSIONBITS_TOP_BITS) && (version & mask) != 0);
+    }
+
+    bool Condition(const CBlockIndex* pindex) const { return Condition(pindex->nVersion); }
+};
+
+/** Track blocks mined for test */
+class Blocks
+{
+private:
+    std::vector<std::unique_ptr<CBlockIndex>> m_blocks;
+    const int32_t m_signal;
+    const int32_t m_no_signal;
+
+public:
+    Blocks(int32_t signal, int32_t no_signal)
+        : m_signal{signal}, m_no_signal{no_signal} {}
+
+    size_t size() const { return m_blocks.size(); }
+
+    CBlockIndex* tip() const
+    {
+        return m_blocks.empty() ? nullptr : m_blocks.back().get();
+    }
+
+    CBlockIndex* mine_block(bool signal)
+    {
+        CBlockHeader header;
+        header.nVersion = signal ? m_signal : m_no_signal;
+        header.nBits = 0x1d00ffff;
+
+        auto current_block = std::make_unique<CBlockIndex>(header);
+        current_block->pprev = tip();
+        current_block->nHeight = m_blocks.size();
+        current_block->BuildSkip();
+
+        return m_blocks.emplace_back(std::move(current_block)).get();
+    }
+};
+} // namespace
+
+std::unique_ptr<const CChainParams> g_params;
+
+void initialize()
+{
+    // this is actually comparatively slow, so only do it once
+    g_params = CreateChainParams(ArgsManager{}, CBaseChainParams::MAIN);
+    assert(g_params != nullptr);
+}
+
+void test_one_input(const std::vector<uint8_t>& buffer)
+{
+    const CChainParams& params = *g_params;
+    const int64_t interval = params.GetConsensus().nPowTargetSpacing;
+    assert(interval > 1); // need to be able to halve it
+    assert(interval < std::numeric_limits<int32_t>::max());
+
+    FuzzedDataProvider fuzzed_data_provider(buffer.data(), buffer.size());
+
+    // making period/max_periods larger slows these tests down significantly
+    const int period = 32;
+    const size_t max_periods = 16;
+    const size_t max_blocks = 2 * period * max_periods;
+
+    const int threshold = fuzzed_data_provider.ConsumeIntegralInRange(1, period);
+    assert(0 < threshold && threshold <= period); // must be able to both pass and fail threshold!
+
+    // what values for version will we use to signal / not signal?
+    const int32_t ver_signal = fuzzed_data_provider.ConsumeIntegral<int32_t>();
+    const int32_t ver_nosignal = fuzzed_data_provider.ConsumeIntegral<int32_t>();
+
+    // select deployment parameters: bit, start time, timeout
+    const int bit = fuzzed_data_provider.ConsumeIntegralInRange<int>(0, VERSIONBITS_NUM_BITS - 1);
+
+    bool always_active_test = false;
+    bool never_active_test = false;
+    int startheight;
+    int timeoutheight;
+    int min_activation = 0;
+    bool lockinontimeout = false;
+    if (fuzzed_data_provider.ConsumeBool()) {
+        // pick the timestamp to switch based on a block
+        startheight = fuzzed_data_provider.ConsumeIntegralInRange<int>(0, period * (max_periods - 2));
+        timeoutheight = fuzzed_data_provider.ConsumeIntegralInRange<int>(0, period * (max_periods - 2));
+        min_activation = fuzzed_data_provider.ConsumeIntegralInRange<int>(0, period * (max_periods - 1));
+        if (startheight < int(period * (max_periods - 3)) && threshold < period) {
+            // LOT=True requires 3 periods (STARTED->MUST_SIGNAL->LOCKED_IN), pushing it past the deadline
+            // Furthermore, this fuzzer doesn't let us easily guarantee the signal of the first block in a period, so skip LOT=True when threshold is 100%
+            lockinontimeout = fuzzed_data_provider.ConsumeBool();
+        }
+    } else {
+        if (fuzzed_data_provider.ConsumeBool()) {
+            startheight = Consensus::BIP9Deployment::ALWAYS_ACTIVE;
+            timeoutheight = Consensus::BIP9Deployment::NO_TIMEOUT;
+            always_active_test = true;
+        } else {
+            startheight = Consensus::BIP9Deployment::NEVER_ACTIVE;
+            timeoutheight = Consensus::BIP9Deployment::NEVER_ACTIVE;
+            never_active_test = true;
+        }
+    }
+
+    TestConditionChecker checker(startheight, timeoutheight, min_activation, period, threshold, bit, lockinontimeout);
+
+    // Early exit if the versions don't signal sensibly for the deployment
+    if (!checker.Condition(ver_signal)) return;
+    if (checker.Condition(ver_nosignal)) return;
+    if (ver_nosignal < 0) return;
+
+    // TOP_BITS should ensure version will be positive and meet min
+    // version requirement
+    assert(ver_signal > 0);
+    assert(ver_signal >= VERSIONBITS_LAST_OLD_BLOCK_VERSION);
+
+    // Now that we have chosen time and versions, setup to mine blocks
+    Blocks blocks(ver_signal, ver_nosignal);
+
+    /* Strategy:
+     *  * we will mine a final period worth of blocks, with
+     *    randomised signalling according to a mask
+     *  * but before we mine those blocks, we will mine some
+     *    randomised number of prior periods; with either all
+     *    or no blocks in the period signalling
+     *
+     * We establish the mask first, then consume "bools" until
+     * we run out of fuzz data to work out how many prior periods
+     * there are and which ones will signal.
+     */
+
+    // establish the mask
+    const uint32_t signalling_mask = fuzzed_data_provider.ConsumeIntegral<uint32_t>();
+
+    // mine prior periods
+    while (fuzzed_data_provider.remaining_bytes() > 0) {
+        // all blocks in these periods either do or don't signal
+        bool signal = fuzzed_data_provider.ConsumeBool();
+        for (int b = 0; b < period; ++b) {
+            blocks.mine_block(signal);
+        }
+
+        // don't risk exceeding max_blocks or times may wrap around
+        if (blocks.size() + 2 * period > max_blocks) break;
+    }
+    // NOTE: fuzzed_data_provider may be fully consumed at this point and should not be used further
+
+    // now we mine the final period and check that everything looks sane
+
+    // count the number of signalling blocks
+    int blocks_sig = 0;
+
+    // get the info for the first block of the period
+    CBlockIndex* prev = blocks.tip();
+    const int exp_since = checker.GetStateSinceHeightFor(prev);
+    const ThresholdState exp_state = checker.GetStateFor(prev);
+    BIP9Stats last_stats = checker.GetStateStatisticsFor(prev);
+
+    int prev_next_height = (prev == nullptr ? 0 : prev->nHeight + 1);
+    assert(exp_since <= prev_next_height);
+
+    // mine (period-1) blocks and check state
+    for (int b = 1; b < period; ++b) {
+        bool signal = (signalling_mask >> (b % 32)) & 1;
+        if (exp_state == ThresholdState::MUST_SIGNAL && threshold - blocks_sig >= period - b) {
+            // Further blocks need to signal to be valid
+            signal = true;
+        }
+        if (signal) ++blocks_sig;
+
+        CBlockIndex* current_block = blocks.mine_block(signal);
+
+        // verify that signalling attempt was interpreted correctly
+        assert(checker.Condition(current_block) == signal);
+
+        // state and since don't change within the period
+        const ThresholdState state = checker.GetStateFor(current_block);
+        const int since = checker.GetStateSinceHeightFor(current_block);
+        assert(state == exp_state);
+        assert(since == exp_since);
+
+        // GetStateStatistics may crash when state is not STARTED or MUST_SIGNAL
+        if (state != ThresholdState::STARTED && state != ThresholdState::MUST_SIGNAL) continue;
+
+        // check that after mining this block stats change as expected
+        const BIP9Stats stats = checker.GetStateStatisticsFor(current_block);
+        assert(stats.period == period);
+        assert(stats.threshold == threshold);
+        assert(stats.elapsed == b);
+        assert(stats.count == last_stats.count + (signal ? 1 : 0));
+        assert(stats.possible == (stats.count + period >= stats.elapsed + threshold));
+        last_stats = stats;
+    }
+
+    if (exp_state == ThresholdState::STARTED || exp_state == ThresholdState::MUST_SIGNAL) {
+        // double check that stats.possible is sane
+        if (blocks_sig >= threshold - 1) assert(last_stats.possible);
+    }
+
+    // mine the final block
+    bool signal = (signalling_mask >> (period % 32)) & 1;
+    if (signal) ++blocks_sig;
+    CBlockIndex* current_block = blocks.mine_block(signal);
+    assert(checker.Condition(current_block) == signal);
+
+    // All states are for the next block
+    int height = current_block->nHeight + 1;
+
+    // GetStateStatistics is safe on a period boundary
+    // and has progressed to a new period
+    const BIP9Stats stats = checker.GetStateStatisticsFor(current_block);
+    assert(stats.period == period);
+    assert(stats.threshold == threshold);
+    assert(stats.elapsed == 0);
+    assert(stats.count == 0);
+    assert(stats.possible == true);
+
+    // More interesting is whether the state changed.
+    const ThresholdState state = checker.GetStateFor(current_block);
+    const int since = checker.GetStateSinceHeightFor(current_block);
+
+    // since is straightforward:
+    assert(since % period == 0);
+    assert(0 <= since && since <= height);
+    if (state == exp_state) {
+        assert(since == exp_since);
+    } else {
+        assert(since == height);
+    }
+
+    // state is where everything interesting is
+    switch (state) {
+    case ThresholdState::DEFINED:
+        assert(since == 0);
+        assert(exp_state == ThresholdState::DEFINED);
+        assert(never_active_test || height < checker.m_begin);
+        break;
+    case ThresholdState::STARTED:
+        assert(height >= checker.m_begin);
+        assert(!never_active_test);
+        if (exp_state == ThresholdState::STARTED) {
+            assert(height < checker.m_end);
+            assert(blocks_sig < threshold);
+        } else {
+            assert(exp_state == ThresholdState::DEFINED);
+        }
+        break;
+    case ThresholdState::MUST_SIGNAL:
+        assert(height >= checker.m_end - period);
+        assert(exp_state == ThresholdState::STARTED);
+        break;
+    case ThresholdState::LOCKED_IN:
+        if (exp_state == ThresholdState::LOCKED_IN) {
+            assert(height < checker.m_min_activation);
+        } else {
+            assert(exp_state == ThresholdState::STARTED || exp_state == ThresholdState::MUST_SIGNAL);
+            assert(blocks_sig >= threshold);
+        }
+        break;
+    case ThresholdState::ACTIVE:
+        assert(always_active_test || height >= checker.m_min_activation);
+        assert(exp_state == ThresholdState::ACTIVE || exp_state == ThresholdState::LOCKED_IN);
+        break;
+    case ThresholdState::FAILED:
+        assert(height >= checker.m_begin);
+        assert(height >= checker.m_end);
+        assert(exp_state == ThresholdState::FAILED || (exp_state == ThresholdState::STARTED && blocks_sig < threshold));
+        break;
+    default:
+        assert(false);
+    }
+
+    if (!never_active_test && blocks.size() >= period * max_periods) {
+        // we chose the timeout (and block times) so that by the time we have this many blocks it's all over
+        assert(state == ThresholdState::ACTIVE || state == ThresholdState::FAILED);
+    }
+
+    // "always active" has additional restrictions
+    if (always_active_test) {
+        assert(state == ThresholdState::ACTIVE);
+        assert(exp_state == ThresholdState::ACTIVE);
+        assert(since == 0);
+    } else {
+        // except for always active, the initial state is always DEFINED
+        assert(since > 0 || state == ThresholdState::DEFINED);
+        assert(exp_since > 0 || exp_state == ThresholdState::DEFINED);
+    }
+
+    // "never active" does too
+    if (never_active_test) {
+        assert(state == ThresholdState::DEFINED);
+        assert(since == 0);
+    }
+}
diff --git a/src/test/versionbits_tests.cpp b/src/test/versionbits_tests.cpp
index 50444f7bb..9d3f4aae1 100644
--- a/src/test/versionbits_tests.cpp
+++ b/src/test/versionbits_tests.cpp
@@ -14,6 +14,19 @@
 /* Define a virtual block time, one block per 10 minutes after Nov 14 2014, 0:55:36am */
 static int32_t TestTime(int nHeight) { return 1415926536 + 600 * nHeight; }
 
+static const std::string StateName(ThresholdState state)
+{
+    switch (state) {
+    case ThresholdState::DEFINED:   return "DEFINED";
+    case ThresholdState::STARTED:   return "STARTED";
+    case ThresholdState::MUST_SIGNAL: return "MUST_SIGNAL";
+    case ThresholdState::LOCKED_IN: return "LOCKED_IN";
+    case ThresholdState::ACTIVE:    return "ACTIVE";
+    case ThresholdState::FAILED:    return "FAILED";
+    } // no default case, so the compiler can warn about missing cases
+    return "";
+}
+
 static const Consensus::Params paramsDummy = Consensus::Params();
 
 class TestConditionChecker : public AbstractThresholdConditionChecker
@@ -22,20 +35,37 @@ private:
     mutable ThresholdConditionCache cache;
 
 public:
-    int64_t BeginTime(const Consensus::Params& params) const override { return TestTime(10000); }
-    int64_t EndTime(const Consensus::Params& params) const override { return TestTime(20000); }
-    int Period(const Consensus::Params& params) const override { return 1000; }
-    int Threshold(const Consensus::Params& params) const override { return 900; }
+    bool m_lockinontimeout{false};
+
+    int StartHeight(const Consensus::Params& params) const override { return 100; }
+    int TimeoutHeight(const Consensus::Params& params) const override { return 200; }
+    bool LockinOnTimeout(const Consensus::Params& params) const override { return m_lockinontimeout; }
+    int Period(const Consensus::Params& params) const override { return 10; }
+    int Threshold(const Consensus::Params& params) const override { return 9; }
+    int MinActivationHeight(const Consensus::Params& params) const override { return 0; }
     bool Condition(const CBlockIndex* pindex, const Consensus::Params& params) const override { return (pindex->nVersion & 0x100); }
 
     ThresholdState GetStateFor(const CBlockIndex* pindexPrev) const { return AbstractThresholdConditionChecker::GetStateFor(pindexPrev, paramsDummy, cache); }
     int GetStateSinceHeightFor(const CBlockIndex* pindexPrev) const { return AbstractThresholdConditionChecker::GetStateSinceHeightFor(pindexPrev, paramsDummy, cache); }
 };
 
+class TestDelayedActivationConditionChecker : public TestConditionChecker
+{
+public:
+    int MinActivationHeight(const Consensus::Params& params) const override { return 250; }
+};
+
 class TestAlwaysActiveConditionChecker : public TestConditionChecker
 {
 public:
-    int64_t BeginTime(const Consensus::Params& params) const override { return Consensus::BIP9Deployment::ALWAYS_ACTIVE; }
+    int StartHeight(const Consensus::Params& params) const override { return Consensus::BIP9Deployment::ALWAYS_ACTIVE; }
+};
+
+class TestNeverActiveConditionChecker : public TestConditionChecker
+{
+public:
+    int StartHeight(const Consensus::Params& params) const override { return Consensus::BIP9Deployment::NEVER_ACTIVE; }
+    int TimeoutHeight(const Consensus::Params& params) const override { return Consensus::BIP9Deployment::NEVER_ACTIVE; }
 };
 
 #define CHECKERS 6
@@ -49,22 +79,29 @@ class VersionBitsTester
     // The first one performs all checks, the second only 50%, the third only 25%, etc...
     // This is to test whether lack of cached information leads to the same results.
     TestConditionChecker checker[CHECKERS];
+    // Another 6 that assume delayed activation
+    TestDelayedActivationConditionChecker checker_delayed[CHECKERS];
     // Another 6 that assume always active activation
     TestAlwaysActiveConditionChecker checker_always[CHECKERS];
+    // Another 6 that assume never active activation
+    TestNeverActiveConditionChecker checker_never[CHECKERS];
 
     // Test counter (to identify failures)
     int num;
 
 public:
-    VersionBitsTester() : num(0) {}
+    VersionBitsTester() : num(1000) {}
 
     VersionBitsTester& Reset() {
+        num = num - (num % 1000) + 1000;
         for (unsigned int i = 0; i < vpblock.size(); i++) {
             delete vpblock[i];
         }
         for (unsigned int  i = 0; i < CHECKERS; i++) {
             checker[i] = TestConditionChecker();
+            checker_delayed[i] = TestDelayedActivationConditionChecker();
             checker_always[i] = TestAlwaysActiveConditionChecker();
+            checker_never[i] = TestNeverActiveConditionChecker();
         }
         vpblock.clear();
         return *this;
@@ -74,6 +111,16 @@ public:
          Reset();
     }
 
+    VersionBitsTester& SetLockinOnTimeout(const bool lockinontimeout) {
+        for (unsigned int  i = 0; i < CHECKERS; i++) {
+            checker[i].m_lockinontimeout = lockinontimeout;
+            checker_delayed[i].m_lockinontimeout = lockinontimeout;
+            checker_always[i].m_lockinontimeout = lockinontimeout;
+            checker_never[i].m_lockinontimeout = lockinontimeout;
+        }
+        return *this;
+    }
+
     VersionBitsTester& Mine(unsigned int height, int32_t nTime, int32_t nVersion) {
         while (vpblock.size() < height) {
             CBlockIndex* pindex = new CBlockIndex();
@@ -87,72 +134,71 @@ public:
         return *this;
     }
 
-    VersionBitsTester& TestStateSinceHeight(int height) {
-        for (int i = 0; i < CHECKERS; i++) {
-            if (InsecureRandBits(i) == 0) {
-                BOOST_CHECK_MESSAGE(checker[i].GetStateSinceHeightFor(vpblock.empty() ? nullptr : vpblock.back()) == height, strprintf("Test %i for StateSinceHeight", num));
-                BOOST_CHECK_MESSAGE(checker_always[i].GetStateSinceHeightFor(vpblock.empty() ? nullptr : vpblock.back()) == 0, strprintf("Test %i for StateSinceHeight (always active)", num));
-            }
-        }
-        num++;
-        return *this;
+    VersionBitsTester& TestStateSinceHeight(int height)
+    {
+        return TestStateSinceHeight(height, height);
     }
 
-    VersionBitsTester& TestDefined() {
+    VersionBitsTester& TestStateSinceHeight(int height, int height_delayed)
+    {
+        const CBlockIndex* pindex = vpblock.empty() ? nullptr : vpblock.back();
         for (int i = 0; i < CHECKERS; i++) {
             if (InsecureRandBits(i) == 0) {
-                BOOST_CHECK_MESSAGE(checker[i].GetStateFor(vpblock.empty() ? nullptr : vpblock.back()) == ThresholdState::DEFINED, strprintf("Test %i for DEFINED", num));
-                BOOST_CHECK_MESSAGE(checker_always[i].GetStateFor(vpblock.empty() ? nullptr : vpblock.back()) == ThresholdState::ACTIVE, strprintf("Test %i for ACTIVE (always active)", num));
-            }
-        }
-        num++;
-        return *this;
-    }
+                BOOST_CHECK_MESSAGE(checker[i].GetStateSinceHeightFor(pindex) == height, strprintf("Test %i for StateSinceHeight", num));
+                BOOST_CHECK_MESSAGE(checker_delayed[i].GetStateSinceHeightFor(pindex) == height_delayed, strprintf("Test %i for StateSinceHeight (delayed)", num));
+                BOOST_CHECK_MESSAGE(checker_always[i].GetStateSinceHeightFor(pindex) == 0, strprintf("Test %i for StateSinceHeight (always active)", num));
 
-    VersionBitsTester& TestStarted() {
-        for (int i = 0; i < CHECKERS; i++) {
-            if (InsecureRandBits(i) == 0) {
-                BOOST_CHECK_MESSAGE(checker[i].GetStateFor(vpblock.empty() ? nullptr : vpblock.back()) == ThresholdState::STARTED, strprintf("Test %i for STARTED", num));
-                BOOST_CHECK_MESSAGE(checker_always[i].GetStateFor(vpblock.empty() ? nullptr : vpblock.back()) == ThresholdState::ACTIVE, strprintf("Test %i for ACTIVE (always active)", num));
+                // never active may go from DEFINED -> FAILED at the first period
+                const auto never_height = checker_never[i].GetStateSinceHeightFor(vpblock.empty() ? nullptr : vpblock.back());
+                BOOST_CHECK_MESSAGE(never_height == 0 || never_height == checker_never[i].Period(paramsDummy), strprintf("Test %i for StateSinceHeight (never active)", num));
             }
         }
         num++;
         return *this;
     }
 
-    VersionBitsTester& TestLockedIn() {
-        for (int i = 0; i < CHECKERS; i++) {
-            if (InsecureRandBits(i) == 0) {
-                BOOST_CHECK_MESSAGE(checker[i].GetStateFor(vpblock.empty() ? nullptr : vpblock.back()) == ThresholdState::LOCKED_IN, strprintf("Test %i for LOCKED_IN", num));
-                BOOST_CHECK_MESSAGE(checker_always[i].GetStateFor(vpblock.empty() ? nullptr : vpblock.back()) == ThresholdState::ACTIVE, strprintf("Test %i for ACTIVE (always active)", num));
-            }
-        }
-        num++;
-        return *this;
+    VersionBitsTester& TestState(ThresholdState exp)
+    {
+        return TestState(exp, exp);
     }
 
-    VersionBitsTester& TestActive() {
-        for (int i = 0; i < CHECKERS; i++) {
-            if (InsecureRandBits(i) == 0) {
-                BOOST_CHECK_MESSAGE(checker[i].GetStateFor(vpblock.empty() ? nullptr : vpblock.back()) == ThresholdState::ACTIVE, strprintf("Test %i for ACTIVE", num));
-                BOOST_CHECK_MESSAGE(checker_always[i].GetStateFor(vpblock.empty() ? nullptr : vpblock.back()) == ThresholdState::ACTIVE, strprintf("Test %i for ACTIVE (always active)", num));
-            }
+    VersionBitsTester& TestState(ThresholdState exp, ThresholdState exp_delayed)
+    {
+        if (exp != exp_delayed) {
+            // only expected differences are that delayed stays in locked_in longer
+            BOOST_CHECK_EQUAL(exp, ThresholdState::ACTIVE);
+            BOOST_CHECK_EQUAL(exp_delayed, ThresholdState::LOCKED_IN);
         }
-        num++;
-        return *this;
-    }
-
-    VersionBitsTester& TestFailed() {
         for (int i = 0; i < CHECKERS; i++) {
             if (InsecureRandBits(i) == 0) {
-                BOOST_CHECK_MESSAGE(checker[i].GetStateFor(vpblock.empty() ? nullptr : vpblock.back()) == ThresholdState::FAILED, strprintf("Test %i for FAILED", num));
-                BOOST_CHECK_MESSAGE(checker_always[i].GetStateFor(vpblock.empty() ? nullptr : vpblock.back()) == ThresholdState::ACTIVE, strprintf("Test %i for ACTIVE (always active)", num));
+                const CBlockIndex* pindex = vpblock.empty() ? nullptr : vpblock.back();
+                ThresholdState got = checker[i].GetStateFor(pindex);
+                ThresholdState got_delayed = checker_delayed[i].GetStateFor(pindex);
+                ThresholdState got_always = checker_always[i].GetStateFor(pindex);
+                ThresholdState got_never = checker_never[i].GetStateFor(pindex);
+                // nHeight of the next block. If vpblock is empty, the next (ie first)
+                // block should be the genesis block with nHeight == 0.
+                int height = pindex == nullptr ? 0 : pindex->nHeight + 1;
+                BOOST_CHECK_MESSAGE(got == exp, strprintf("Test %i for %s height %d (got %s)", num, StateName(exp), height, StateName(got)));
+                BOOST_CHECK_MESSAGE(got_delayed == exp_delayed, strprintf("Test %i for %s height %d (got %s; delayed case)", num, StateName(exp_delayed), height, StateName(got_delayed)));
+                BOOST_CHECK_MESSAGE(got_always == ThresholdState::ACTIVE, strprintf("Test %i for ACTIVE height %d (got %s; always active case)", num, height, StateName(got_always)));
+                BOOST_CHECK_MESSAGE(got_never == ThresholdState::DEFINED|| got_never == ThresholdState::FAILED, strprintf("Test %i for DEFINED/FAILED height %d (got %s; never active case)", num, height, StateName(got_never)));
             }
         }
         num++;
         return *this;
     }
 
+    VersionBitsTester& TestDefined() { return TestState(ThresholdState::DEFINED); }
+    VersionBitsTester& TestStarted() { return TestState(ThresholdState::STARTED); }
+    VersionBitsTester& TestMustSignal() { return TestState(ThresholdState::MUST_SIGNAL); }
+    VersionBitsTester& TestLockedIn() { return TestState(ThresholdState::LOCKED_IN); }
+    VersionBitsTester& TestActive() { return TestState(ThresholdState::ACTIVE); }
+    VersionBitsTester& TestFailed() { return TestState(ThresholdState::FAILED); }
+
+    // non-delayed should be active; delayed should still be locked in
+    VersionBitsTester& TestActiveDelayed() { return TestState(ThresholdState::ACTIVE, ThresholdState::LOCKED_IN); }
+
     CBlockIndex * Tip() { return vpblock.size() ? vpblock.back() : nullptr; }
 };
 
@@ -161,75 +207,79 @@ BOOST_FIXTURE_TEST_SUITE(versionbits_tests, TestingSetup)
 BOOST_AUTO_TEST_CASE(versionbits_test)
 {
     for (int i = 0; i < 64; i++) {
-        // DEFINED -> FAILED
-        VersionBitsTester().TestDefined().TestStateSinceHeight(0)
-                           .Mine(1, TestTime(1), 0x100).TestDefined().TestStateSinceHeight(0)
-                           .Mine(11, TestTime(11), 0x100).TestDefined().TestStateSinceHeight(0)
-                           .Mine(989, TestTime(989), 0x100).TestDefined().TestStateSinceHeight(0)
-                           .Mine(999, TestTime(20000), 0x100).TestDefined().TestStateSinceHeight(0)
-                           .Mine(1000, TestTime(20000), 0x100).TestFailed().TestStateSinceHeight(1000)
-                           .Mine(1999, TestTime(30001), 0x100).TestFailed().TestStateSinceHeight(1000)
-                           .Mine(2000, TestTime(30002), 0x100).TestFailed().TestStateSinceHeight(1000)
-                           .Mine(2001, TestTime(30003), 0x100).TestFailed().TestStateSinceHeight(1000)
-                           .Mine(2999, TestTime(30004), 0x100).TestFailed().TestStateSinceHeight(1000)
-                           .Mine(3000, TestTime(30005), 0x100).TestFailed().TestStateSinceHeight(1000)
-
         // DEFINED -> STARTED -> FAILED
-                           .Reset().TestDefined().TestStateSinceHeight(0)
-                           .Mine(1, TestTime(1), 0).TestDefined().TestStateSinceHeight(0)
-                           .Mine(1000, TestTime(10000) - 1, 0x100).TestDefined().TestStateSinceHeight(0) // One second more and it would be defined
-                           .Mine(2000, TestTime(10000), 0x100).TestStarted().TestStateSinceHeight(2000) // So that's what happens the next period
-                           .Mine(2051, TestTime(10010), 0).TestStarted().TestStateSinceHeight(2000) // 51 old blocks
-                           .Mine(2950, TestTime(10020), 0x100).TestStarted().TestStateSinceHeight(2000) // 899 new blocks
-                           .Mine(3000, TestTime(20000), 0).TestFailed().TestStateSinceHeight(3000) // 50 old blocks (so 899 out of the past 1000)
-                           .Mine(4000, TestTime(20010), 0x100).TestFailed().TestStateSinceHeight(3000)
-
-        // DEFINED -> STARTED -> FAILED while threshold reached
-                           .Reset().TestDefined().TestStateSinceHeight(0)
+        VersionBitsTester().TestDefined().TestStateSinceHeight(0)
                            .Mine(1, TestTime(1), 0).TestDefined().TestStateSinceHeight(0)
-                           .Mine(1000, TestTime(10000) - 1, 0x101).TestDefined().TestStateSinceHeight(0) // One second more and it would be defined
-                           .Mine(2000, TestTime(10000), 0x101).TestStarted().TestStateSinceHeight(2000) // So that's what happens the next period
-                           .Mine(2999, TestTime(30000), 0x100).TestStarted().TestStateSinceHeight(2000) // 999 new blocks
-                           .Mine(3000, TestTime(30000), 0x100).TestFailed().TestStateSinceHeight(3000) // 1 new block (so 1000 out of the past 1000 are new)
-                           .Mine(3999, TestTime(30001), 0).TestFailed().TestStateSinceHeight(3000)
-                           .Mine(4000, TestTime(30002), 0).TestFailed().TestStateSinceHeight(3000)
-                           .Mine(14333, TestTime(30003), 0).TestFailed().TestStateSinceHeight(3000)
-                           .Mine(24000, TestTime(40000), 0).TestFailed().TestStateSinceHeight(3000)
+                           .Mine(99, TestTime(10000) - 1, 0x100).TestDefined().TestStateSinceHeight(0) // One block more and it would be defined
+                           .Mine(100, TestTime(10000), 0x100).TestStarted().TestStateSinceHeight(100) // So that's what happens the next period
+                           .Mine(101, TestTime(10010), 0).TestStarted().TestStateSinceHeight(100) // 1 old block
+                           .Mine(109, TestTime(10020), 0x100).TestStarted().TestStateSinceHeight(100) // 8 new blocks
+                           .Mine(110, TestTime(10020), 0).TestStarted().TestStateSinceHeight(100) // 1 old block (so 8 out of the past 10 are new)
+                           .Mine(151, TestTime(10020), 0).TestStarted().TestStateSinceHeight(100)
+                           .Mine(200, TestTime(20000), 0).TestFailed().TestStateSinceHeight(200)
+                           .Mine(300, TestTime(20010), 0x100).TestFailed().TestStateSinceHeight(200)
 
         // DEFINED -> STARTED -> LOCKEDIN at the last minute -> ACTIVE
                            .Reset().TestDefined()
                            .Mine(1, TestTime(1), 0).TestDefined().TestStateSinceHeight(0)
-                           .Mine(1000, TestTime(10000) - 1, 0x101).TestDefined().TestStateSinceHeight(0) // One second more and it would be defined
-                           .Mine(2000, TestTime(10000), 0x101).TestStarted().TestStateSinceHeight(2000) // So that's what happens the next period
-                           .Mine(2050, TestTime(10010), 0x200).TestStarted().TestStateSinceHeight(2000) // 50 old blocks
-                           .Mine(2950, TestTime(10020), 0x100).TestStarted().TestStateSinceHeight(2000) // 900 new blocks
-                           .Mine(2999, TestTime(19999), 0x200).TestStarted().TestStateSinceHeight(2000) // 49 old blocks
-                           .Mine(3000, TestTime(29999), 0x200).TestLockedIn().TestStateSinceHeight(3000) // 1 old block (so 900 out of the past 1000)
-                           .Mine(3999, TestTime(30001), 0).TestLockedIn().TestStateSinceHeight(3000)
-                           .Mine(4000, TestTime(30002), 0).TestActive().TestStateSinceHeight(4000)
-                           .Mine(14333, TestTime(30003), 0).TestActive().TestStateSinceHeight(4000)
-                           .Mine(24000, TestTime(40000), 0).TestActive().TestStateSinceHeight(4000)
+                           .Mine(99, TestTime(10000) - 1, 0x101).TestDefined().TestStateSinceHeight(0) // One block more and it would be started
+                           .Mine(100, TestTime(10000), 0x101).TestStarted().TestStateSinceHeight(100) // So that's what happens the next period
+                           .Mine(109, TestTime(10020), 0x100).TestStarted().TestStateSinceHeight(100) // 9 new blocks
+                           .Mine(110, TestTime(29999), 0x200).TestLockedIn().TestStateSinceHeight(110) // 1 old block (so 9 out of the past 10)
+                           .Mine(119, TestTime(30001), 0).TestLockedIn().TestStateSinceHeight(110)
+                           .Mine(120, TestTime(30002), 0).TestActiveDelayed().TestStateSinceHeight(120, 110) // Delayed will not become active until height 250
+                           .Mine(200, TestTime(30003), 0).TestActiveDelayed().TestStateSinceHeight(120, 110)
+                           .Mine(250, TestTime(30004), 0).TestActive().TestStateSinceHeight(120, 250)
+                           .Mine(300, TestTime(40000), 0).TestActive().TestStateSinceHeight(120, 250)
+
+        // DEFINED -> STARTED -> MUST_SIGNAL -> LOCKEDIN -> ACTIVE
+                           .Reset().TestDefined()
+                           .SetLockinOnTimeout(true)
+                           .Mine(1, TestTime(1), 0).TestDefined().TestStateSinceHeight(0)
+                           .Mine(99, TestTime(10000) - 1, 0x101).TestDefined().TestStateSinceHeight(0) // One second more and it would be started
+                           .Mine(100, TestTime(10000), 0x101).TestStarted().TestStateSinceHeight(100) // So that's what happens the next period
+                           .Mine(189, TestTime(10010), 0).TestStarted().TestStateSinceHeight(100)
+                           .Mine(190, TestTime(10020), 0).TestMustSignal().TestStateSinceHeight(190)
+                           .Mine(199, TestTime(10030), 0).TestMustSignal().TestStateSinceHeight(190) // 9 new blocks
+                           .Mine(200, TestTime(29999), 0x200).TestLockedIn().TestStateSinceHeight(200) // 1 old block (so 9 out of the past 10)
+                           .Mine(209, TestTime(30001), 0).TestLockedIn().TestStateSinceHeight(200)
+                           .Mine(210, TestTime(30002), 0).TestActiveDelayed().TestStateSinceHeight(210, 200)
+                           .Mine(290, TestTime(30003), 0).TestActive().TestStateSinceHeight(210, 250)
+                           .Mine(390, TestTime(40000), 0).TestActive().TestStateSinceHeight(210, 250)
+                           .SetLockinOnTimeout(false)
 
         // DEFINED multiple periods -> STARTED multiple periods -> FAILED
                            .Reset().TestDefined().TestStateSinceHeight(0)
-                           .Mine(999, TestTime(999), 0).TestDefined().TestStateSinceHeight(0)
-                           .Mine(1000, TestTime(1000), 0).TestDefined().TestStateSinceHeight(0)
-                           .Mine(2000, TestTime(2000), 0).TestDefined().TestStateSinceHeight(0)
-                           .Mine(3000, TestTime(10000), 0).TestStarted().TestStateSinceHeight(3000)
-                           .Mine(4000, TestTime(10000), 0).TestStarted().TestStateSinceHeight(3000)
-                           .Mine(5000, TestTime(10000), 0).TestStarted().TestStateSinceHeight(3000)
-                           .Mine(6000, TestTime(20000), 0).TestFailed().TestStateSinceHeight(6000)
-                           .Mine(7000, TestTime(20000), 0x100).TestFailed().TestStateSinceHeight(6000);
+                           .Mine(9, TestTime(999), 0).TestDefined().TestStateSinceHeight(0)
+                           .Mine(10, TestTime(1000), 0).TestDefined().TestStateSinceHeight(0)
+                           .Mine(20, TestTime(2000), 0).TestDefined().TestStateSinceHeight(0)
+                           .Mine(100, TestTime(10000), 0).TestStarted().TestStateSinceHeight(100)
+                           .Mine(103, TestTime(10000), 0).TestStarted().TestStateSinceHeight(100)
+                           .Mine(105, TestTime(10000), 0).TestStarted().TestStateSinceHeight(100)
+                           .Mine(200, TestTime(20000), 0).TestFailed().TestStateSinceHeight(200)
+                           .Mine(300, TestTime(20000), 0x100).TestFailed().TestStateSinceHeight(200);
     }
+}
 
+void sanity_check_params(const Consensus::Params& params)
+{
     // Sanity checks of version bit deployments
-    const auto chainParams = CreateChainParams(*m_node.args, CBaseChainParams::MAIN);
-    const Consensus::Params &mainnetParams = chainParams->GetConsensus();
     for (int i=0; i<(int) Consensus::MAX_VERSION_BITS_DEPLOYMENTS; i++) {
-        uint32_t bitmask = VersionBitsMask(mainnetParams, static_cast<Consensus::DeploymentPos>(i));
+
+        // Verify the threshold is sane and isn't lower than the threshold
+        // used for warning for unknown activations
+        int threshold = params.vDeployments[i].threshold;
+        BOOST_CHECK(threshold > 0);
+        BOOST_CHECK((uint32_t)threshold >= params.m_vbits_min_threshold);
+        BOOST_CHECK((uint32_t)threshold <= params.nMinerConfirmationWindow);
+
+        uint32_t bitmask = VersionBitsMask(params, static_cast<Consensus::DeploymentPos>(i));
         // Make sure that no deployment tries to set an invalid bit.
         BOOST_CHECK_EQUAL(bitmask & ~(uint32_t)VERSIONBITS_TOP_MASK, bitmask);
 
+        std::string error;
+        BOOST_CHECK_MESSAGE(CheckVBitsHeights(error, params, params.vDeployments[i].startheight, params.vDeployments[i].timeoutheight, params.vDeployments[i].m_min_activation_height), error);
+
         // Verify that the deployment windows of different deployment using the
         // same bit are disjoint.
         // This test may need modification at such time as a new deployment
@@ -238,116 +288,92 @@ BOOST_AUTO_TEST_CASE(versionbits_test)
         // activated soft fork could be later changed to be earlier to avoid
         // overlap.)
         for (int j=i+1; j<(int) Consensus::MAX_VERSION_BITS_DEPLOYMENTS; j++) {
-            if (VersionBitsMask(mainnetParams, static_cast<Consensus::DeploymentPos>(j)) == bitmask) {
-                BOOST_CHECK(mainnetParams.vDeployments[j].nStartTime > mainnetParams.vDeployments[i].nTimeout ||
-                        mainnetParams.vDeployments[i].nStartTime > mainnetParams.vDeployments[j].nTimeout);
+            if (VersionBitsMask(params, static_cast<Consensus::DeploymentPos>(j)) == bitmask) {
+                BOOST_CHECK(params.vDeployments[j].startheight > params.vDeployments[i].timeoutheight ||
+                        params.vDeployments[i].startheight > params.vDeployments[j].timeoutheight);
             }
         }
     }
 }
 
+BOOST_AUTO_TEST_CASE(versionbits_params)
+{
+    for (const auto& chain : {CBaseChainParams::MAIN, CBaseChainParams::TESTNET, CBaseChainParams::SIGNET, CBaseChainParams::REGTEST}) {
+        const auto chainParams = CreateChainParams(*m_node.args, chain);
+        const Consensus::Params &params = chainParams->GetConsensus();
+        sanity_check_params(params);
+    }
+}
+
 BOOST_AUTO_TEST_CASE(versionbits_computeblockversion)
 {
     // Check that ComputeBlockVersion will set the appropriate bit correctly
-    // on mainnet.
-    const auto chainParams = CreateChainParams(*m_node.args, CBaseChainParams::MAIN);
-    const Consensus::Params &mainnetParams = chainParams->GetConsensus();
+    const auto period = CreateChainParams(*m_node.args, CBaseChainParams::REGTEST)->GetConsensus().nMinerConfirmationWindow;
+    gArgs.ForceSetArg("-vbparams", strprintf("testdummy:@%s:@%s:0", period, period * 3));
+    const auto chainParams = CreateChainParams(*m_node.args, CBaseChainParams::REGTEST);
+    const Consensus::Params &params = chainParams->GetConsensus();
 
     // Use the TESTDUMMY deployment for testing purposes.
-    int64_t bit = mainnetParams.vDeployments[Consensus::DEPLOYMENT_TESTDUMMY].bit;
-    int64_t nStartTime = mainnetParams.vDeployments[Consensus::DEPLOYMENT_TESTDUMMY].nStartTime;
-    int64_t nTimeout = mainnetParams.vDeployments[Consensus::DEPLOYMENT_TESTDUMMY].nTimeout;
+    int64_t bit = params.vDeployments[Consensus::DEPLOYMENT_TESTDUMMY].bit;
+    int64_t startheight = params.vDeployments[Consensus::DEPLOYMENT_TESTDUMMY].startheight;
+    int64_t timeoutheight = params.vDeployments[Consensus::DEPLOYMENT_TESTDUMMY].timeoutheight;
+    const int64_t nTime = TestTime(startheight);
 
-    assert(nStartTime < nTimeout);
+    assert(startheight < timeoutheight);
 
     // In the first chain, test that the bit is set by CBV until it has failed.
     // In the second chain, test the bit is set by CBV while STARTED and
     // LOCKED-IN, and then no longer set while ACTIVE.
     VersionBitsTester firstChain, secondChain;
 
-    // Start generating blocks before nStartTime
-    int64_t nTime = nStartTime - 1;
-
-    // Before MedianTimePast of the chain has crossed nStartTime, the bit
-    // should not be set.
+    // Start generating blocks before startheight
+    // Before the chain has reached startheight-1, the bit should not be set.
     CBlockIndex *lastBlock = nullptr;
-    lastBlock = firstChain.Mine(mainnetParams.nMinerConfirmationWindow, nTime, VERSIONBITS_LAST_OLD_BLOCK_VERSION).Tip();
-    BOOST_CHECK_EQUAL(ComputeBlockVersion(lastBlock, mainnetParams) & (1<<bit), 0);
-
-    // Mine more blocks (4 less than the adjustment period) at the old time, and check that CBV isn't setting the bit yet.
-    for (uint32_t i = 1; i < mainnetParams.nMinerConfirmationWindow - 4; i++) {
-        lastBlock = firstChain.Mine(mainnetParams.nMinerConfirmationWindow + i, nTime, VERSIONBITS_LAST_OLD_BLOCK_VERSION).Tip();
-        // This works because VERSIONBITS_LAST_OLD_BLOCK_VERSION happens
-        // to be 4, and the bit we're testing happens to be bit 28.
-        BOOST_CHECK_EQUAL(ComputeBlockVersion(lastBlock, mainnetParams) & (1<<bit), 0);
-    }
-    // Now mine 5 more blocks at the start time -- MTP should not have passed yet, so
-    // CBV should still not yet set the bit.
-    nTime = nStartTime;
-    for (uint32_t i = mainnetParams.nMinerConfirmationWindow - 4; i <= mainnetParams.nMinerConfirmationWindow; i++) {
-        lastBlock = firstChain.Mine(mainnetParams.nMinerConfirmationWindow + i, nTime, VERSIONBITS_LAST_OLD_BLOCK_VERSION).Tip();
-        BOOST_CHECK_EQUAL(ComputeBlockVersion(lastBlock, mainnetParams) & (1<<bit), 0);
-    }
+    lastBlock = firstChain.Mine(startheight - 1, nTime, VERSIONBITS_LAST_OLD_BLOCK_VERSION).Tip();
+    BOOST_CHECK_EQUAL(ComputeBlockVersion(lastBlock, params) & (1<<bit), 0);
 
-    // Advance to the next period and transition to STARTED,
-    lastBlock = firstChain.Mine(mainnetParams.nMinerConfirmationWindow * 3, nTime, VERSIONBITS_LAST_OLD_BLOCK_VERSION).Tip();
+    // Advance to the next block and transition to STARTED,
+    lastBlock = firstChain.Mine(startheight, nTime, VERSIONBITS_LAST_OLD_BLOCK_VERSION).Tip();
     // so ComputeBlockVersion should now set the bit,
-    BOOST_CHECK((ComputeBlockVersion(lastBlock, mainnetParams) & (1<<bit)) != 0);
+    BOOST_CHECK((ComputeBlockVersion(lastBlock, params) & (1<<bit)) != 0);
     // and should also be using the VERSIONBITS_TOP_BITS.
-    BOOST_CHECK_EQUAL(ComputeBlockVersion(lastBlock, mainnetParams) & VERSIONBITS_TOP_MASK, VERSIONBITS_TOP_BITS);
-
-    // Check that ComputeBlockVersion will set the bit until nTimeout
-    nTime += 600;
-    uint32_t blocksToMine = mainnetParams.nMinerConfirmationWindow * 2; // test blocks for up to 2 time periods
-    uint32_t nHeight = mainnetParams.nMinerConfirmationWindow * 3;
-    // These blocks are all before nTimeout is reached.
-    while (nTime < nTimeout && blocksToMine > 0) {
-        lastBlock = firstChain.Mine(nHeight+1, nTime, VERSIONBITS_LAST_OLD_BLOCK_VERSION).Tip();
-        BOOST_CHECK((ComputeBlockVersion(lastBlock, mainnetParams) & (1<<bit)) != 0);
-        BOOST_CHECK_EQUAL(ComputeBlockVersion(lastBlock, mainnetParams) & VERSIONBITS_TOP_MASK, VERSIONBITS_TOP_BITS);
-        blocksToMine--;
-        nTime += 600;
-        nHeight += 1;
-    }
+    BOOST_CHECK_EQUAL(ComputeBlockVersion(lastBlock, params) & VERSIONBITS_TOP_MASK, VERSIONBITS_TOP_BITS);
+
+    // Check that ComputeBlockVersion will set the bit until timeoutheight
+    // These blocks are all before timeoutheight is reached.
+    lastBlock = firstChain.Mine(timeoutheight - 1, nTime, VERSIONBITS_LAST_OLD_BLOCK_VERSION).Tip();
+    BOOST_CHECK((ComputeBlockVersion(lastBlock, params) & (1<<bit)) != 0);
+    BOOST_CHECK_EQUAL(ComputeBlockVersion(lastBlock, params) & VERSIONBITS_TOP_MASK, VERSIONBITS_TOP_BITS);
 
-    nTime = nTimeout;
-    // FAILED is only triggered at the end of a period, so CBV should be setting
-    // the bit until the period transition.
-    for (uint32_t i = 0; i < mainnetParams.nMinerConfirmationWindow - 1; i++) {
-        lastBlock = firstChain.Mine(nHeight+1, nTime, VERSIONBITS_LAST_OLD_BLOCK_VERSION).Tip();
-        BOOST_CHECK((ComputeBlockVersion(lastBlock, mainnetParams) & (1<<bit)) != 0);
-        nHeight += 1;
-    }
     // The next block should trigger no longer setting the bit.
-    lastBlock = firstChain.Mine(nHeight+1, nTime, VERSIONBITS_LAST_OLD_BLOCK_VERSION).Tip();
-    BOOST_CHECK_EQUAL(ComputeBlockVersion(lastBlock, mainnetParams) & (1<<bit), 0);
+    lastBlock = firstChain.Mine(timeoutheight, nTime, VERSIONBITS_LAST_OLD_BLOCK_VERSION).Tip();
+    BOOST_CHECK_EQUAL(ComputeBlockVersion(lastBlock, params) & (1<<bit), 0);
 
     // On a new chain:
     // verify that the bit will be set after lock-in, and then stop being set
     // after activation.
-    nTime = nStartTime;
 
-    // Mine one period worth of blocks, and check that the bit will be on for the
+    // Mine up until startheight-1, and check that the bit will be on for the
     // next period.
-    lastBlock = secondChain.Mine(mainnetParams.nMinerConfirmationWindow, nTime, VERSIONBITS_LAST_OLD_BLOCK_VERSION).Tip();
-    BOOST_CHECK((ComputeBlockVersion(lastBlock, mainnetParams) & (1<<bit)) != 0);
+    lastBlock = secondChain.Mine(startheight, nTime, VERSIONBITS_LAST_OLD_BLOCK_VERSION).Tip();
+    BOOST_CHECK((ComputeBlockVersion(lastBlock, params) & (1<<bit)) != 0);
 
-    // Mine another period worth of blocks, signaling the new bit.
-    lastBlock = secondChain.Mine(mainnetParams.nMinerConfirmationWindow * 2, nTime, VERSIONBITS_TOP_BITS | (1<<bit)).Tip();
+    // Mine another block, signaling the new bit.
+    lastBlock = secondChain.Mine(startheight + params.nMinerConfirmationWindow, nTime, VERSIONBITS_TOP_BITS | (1<<bit)).Tip();
     // After one period of setting the bit on each block, it should have locked in.
     // We keep setting the bit for one more period though, until activation.
-    BOOST_CHECK((ComputeBlockVersion(lastBlock, mainnetParams) & (1<<bit)) != 0);
+    BOOST_CHECK((ComputeBlockVersion(lastBlock, params) & (1<<bit)) != 0);
 
     // Now check that we keep mining the block until the end of this period, and
     // then stop at the beginning of the next period.
-    lastBlock = secondChain.Mine((mainnetParams.nMinerConfirmationWindow * 3) - 1, nTime, VERSIONBITS_LAST_OLD_BLOCK_VERSION).Tip();
-    BOOST_CHECK((ComputeBlockVersion(lastBlock, mainnetParams) & (1 << bit)) != 0);
-    lastBlock = secondChain.Mine(mainnetParams.nMinerConfirmationWindow * 3, nTime, VERSIONBITS_LAST_OLD_BLOCK_VERSION).Tip();
-    BOOST_CHECK_EQUAL(ComputeBlockVersion(lastBlock, mainnetParams) & (1<<bit), 0);
+    lastBlock = secondChain.Mine(startheight + (params.nMinerConfirmationWindow * 2) - 1, nTime, VERSIONBITS_LAST_OLD_BLOCK_VERSION).Tip();
+    BOOST_CHECK((ComputeBlockVersion(lastBlock, params) & (1 << bit)) != 0);
+    lastBlock = secondChain.Mine(startheight + (params.nMinerConfirmationWindow * 2), nTime, VERSIONBITS_LAST_OLD_BLOCK_VERSION).Tip();
+    BOOST_CHECK_EQUAL(ComputeBlockVersion(lastBlock, params) & (1<<bit), 0);
 
     // Finally, verify that after a soft fork has activated, CBV no longer uses
     // VERSIONBITS_LAST_OLD_BLOCK_VERSION.
-    //BOOST_CHECK_EQUAL(ComputeBlockVersion(lastBlock, mainnetParams) & VERSIONBITS_TOP_MASK, VERSIONBITS_TOP_BITS);
+    //BOOST_CHECK_EQUAL(ComputeBlockVersion(lastBlock, params) & VERSIONBITS_TOP_MASK, VERSIONBITS_TOP_BITS);
 }
 
 
diff --git a/src/validation.cpp b/src/validation.cpp
index feb7502a0..4d8dc7f64 100644
--- a/src/validation.cpp
+++ b/src/validation.cpp
@@ -47,6 +47,7 @@
 #include <util/system.h>
 #include <util/translation.h>
 #include <validationinterface.h>
+#include <versionbitsinfo.h>
 #include <warnings.h>
 
 #include <string>
@@ -1834,7 +1835,7 @@ int32_t ComputeBlockVersion(const CBlockIndex* pindexPrev, const Consensus::Para
 
     for (int i = 0; i < (int)Consensus::MAX_VERSION_BITS_DEPLOYMENTS; i++) {
         ThresholdState state = VersionBitsState(pindexPrev, params, static_cast<Consensus::DeploymentPos>(i), versionbitscache);
-        if (state == ThresholdState::LOCKED_IN || state == ThresholdState::STARTED) {
+        if (state == ThresholdState::LOCKED_IN || state == ThresholdState::MUST_SIGNAL || state == ThresholdState::STARTED) {
             nVersion |= VersionBitsMask(params, static_cast<Consensus::DeploymentPos>(i));
         }
     }
@@ -1853,10 +1854,12 @@ private:
 public:
     explicit WarningBitsConditionChecker(int bitIn) : bit(bitIn) {}
 
-    int64_t BeginTime(const Consensus::Params& params) const override { return 0; }
-    int64_t EndTime(const Consensus::Params& params) const override { return std::numeric_limits<int64_t>::max(); }
+    int StartHeight(const Consensus::Params& params) const override { return 0; }
+    int TimeoutHeight(const Consensus::Params& params) const override { return std::numeric_limits<int>::max(); }
+    bool LockinOnTimeout(const Consensus::Params& params) const override { return false; }
     int Period(const Consensus::Params& params) const override { return params.nMinerConfirmationWindow; }
-    int Threshold(const Consensus::Params& params) const override { return params.nRuleChangeActivationThreshold; }
+    int Threshold(const Consensus::Params& params) const override { return params.m_vbits_min_threshold; }
+    int MinActivationHeight(const Consensus::Params& params) const override { return 0; }
 
     bool Condition(const CBlockIndex* pindex, const Consensus::Params& params) const override
     {
@@ -1940,6 +1943,8 @@ static int64_t nTimeCallbacks = 0;
 static int64_t nTimeTotal = 0;
 static int64_t nBlocksTotal = 0;
 
+static bool ContextualCheckBlockHeaderVolatile(const CBlockHeader&, BlockValidationState&, const CChainParams&, const CBlockIndex* pindexPrev) EXCLUSIVE_LOCKS_REQUIRED(cs_main);
+
 /** Apply the effects of this block (with given index) on the UTXO set represented by coins.
  *  Validity checks that depend on the UTXO set are also done; ConnectBlock()
  *  can fail if those validity checks fail (among other reasons). */
@@ -1974,6 +1979,10 @@ bool CChainState::ConnectBlock(const CBlock& block, BlockValidationState& state,
         return error("%s: Consensus::CheckBlock: %s", __func__, state.ToString());
     }
 
+    if (!ContextualCheckBlockHeaderVolatile(block, state, chainparams, pindex->pprev)) {
+        return error("%s: Consensus::ContextualCheckBlockHeaderVolatile: %s", __func__, state.ToString());
+    }
+
     // verify that the view's current state corresponds to the previous block
     uint256 hashPrevBlock = pindex->pprev == nullptr ? uint256() : pindex->pprev->GetBlockHash();
     assert(hashPrevBlock == view.GetBestBlock());
@@ -3531,6 +3540,38 @@ static bool ContextualCheckBlockHeader(const CBlockHeader& block, BlockValidatio
             return state.Invalid(BlockValidationResult::BLOCK_INVALID_HEADER, strprintf("bad-version(0x%08x)", block.nVersion),
                                  strprintf("rejected nVersion=0x%08x block", block.nVersion));
 
+    if (!ContextualCheckBlockHeaderVolatile(block, state, params, pindexPrev)) return false;
+
+    return true;
+}
+
+/** Context-dependent validity checks, but rechecked in ConnectBlock().
+ *  Note that -reindex-chainstate skips the validation that happens here!
+ */
+static bool ContextualCheckBlockHeaderVolatile(const CBlockHeader& block, BlockValidationState& state, const CChainParams& params, const CBlockIndex* pindexPrev) EXCLUSIVE_LOCKS_REQUIRED(cs_main)
+{
+    const Consensus::Params& consensusParams = params.GetConsensus();
+
+    // Enforce MUST_SIGNAL status of deployments
+    for (int j = 0; j < (int)Consensus::MAX_VERSION_BITS_DEPLOYMENTS; ++j) {
+        Consensus::DeploymentPos deployment_pos = Consensus::DeploymentPos(j);
+        ThresholdState deployment_state = VersionBitsState(pindexPrev, consensusParams, deployment_pos, versionbitscache);
+        if (deployment_state == ThresholdState::MUST_SIGNAL) {
+            if ((block.nVersion & VersionBitsMask(consensusParams, deployment_pos)) == 0 || (block.nVersion & VERSIONBITS_TOP_MASK) != VERSIONBITS_TOP_BITS) {
+                BIP9Stats stats = VersionBitsStatistics(pindexPrev, consensusParams, deployment_pos);
+                if (stats.elapsed == stats.period) {
+                    // first block in new period
+                    stats.count = stats.elapsed = 0;
+                }
+                ++stats.elapsed;
+                if (stats.count + (stats.period - stats.elapsed) < stats.threshold) {
+                    const auto& deployment_name = VersionBitsDeploymentInfo[deployment_pos].name;
+                    return state.Invalid(BlockValidationResult::BLOCK_RECENT_CONSENSUS_CHANGE, std::string{"bad-vbit-unset-"} + deployment_name, std::string{deployment_name} + " must be signalled");
+                }
+            }
+        }
+    }
+
     return true;
 }
 
diff --git a/src/versionbits.cpp b/src/versionbits.cpp
index af07c67cc..25535b5aa 100644
--- a/src/versionbits.cpp
+++ b/src/versionbits.cpp
@@ -9,11 +9,18 @@ ThresholdState AbstractThresholdConditionChecker::GetStateFor(const CBlockIndex*
 {
     int nPeriod = Period(params);
     int nThreshold = Threshold(params);
-    int64_t nTimeStart = BeginTime(params);
-    int64_t nTimeTimeout = EndTime(params);
+    int height_start = StartHeight(params);
+    int height_timeout = TimeoutHeight(params);
+    int height_active_min = MinActivationHeight(params);
+    const bool lockinontimeout = LockinOnTimeout(params);
+
+    // Check if this deployment is never active.
+    if (height_start == Consensus::BIP9Deployment::NEVER_ACTIVE && height_timeout == Consensus::BIP9Deployment::NEVER_ACTIVE) {
+        return ThresholdState::DEFINED;
+    }
 
     // Check if this deployment is always active.
-    if (nTimeStart == Consensus::BIP9Deployment::ALWAYS_ACTIVE) {
+    if (height_start == Consensus::BIP9Deployment::ALWAYS_ACTIVE) {
         return ThresholdState::ACTIVE;
     }
 
@@ -30,8 +37,10 @@ ThresholdState AbstractThresholdConditionChecker::GetStateFor(const CBlockIndex*
             cache[pindexPrev] = ThresholdState::DEFINED;
             break;
         }
-        if (pindexPrev->GetMedianTimePast() < nTimeStart) {
-            // Optimization: don't recompute down further, as we know every earlier block will be before the start time
+
+        // We track state by previous-block, so the height we should be comparing is +1
+        if (pindexPrev->nHeight + 1 < height_start) {
+            // Optimization: don't recompute down further, as we know every earlier block will be before the start height
             cache[pindexPrev] = ThresholdState::DEFINED;
             break;
         }
@@ -49,20 +58,17 @@ ThresholdState AbstractThresholdConditionChecker::GetStateFor(const CBlockIndex*
         pindexPrev = vToCompute.back();
         vToCompute.pop_back();
 
+        // We track state by previous-block, so the height we should be comparing is +1
+        const int64_t height = pindexPrev->nHeight + 1;
+
         switch (state) {
             case ThresholdState::DEFINED: {
-                if (pindexPrev->GetMedianTimePast() >= nTimeTimeout) {
-                    stateNext = ThresholdState::FAILED;
-                } else if (pindexPrev->GetMedianTimePast() >= nTimeStart) {
+                if (height >= height_start) {
                     stateNext = ThresholdState::STARTED;
                 }
                 break;
             }
             case ThresholdState::STARTED: {
-                if (pindexPrev->GetMedianTimePast() >= nTimeTimeout) {
-                    stateNext = ThresholdState::FAILED;
-                    break;
-                }
                 // We need to count
                 const CBlockIndex* pindexCount = pindexPrev;
                 int count = 0;
@@ -74,12 +80,23 @@ ThresholdState AbstractThresholdConditionChecker::GetStateFor(const CBlockIndex*
                 }
                 if (count >= nThreshold) {
                     stateNext = ThresholdState::LOCKED_IN;
+                } else if (lockinontimeout && height + nPeriod >= height_timeout) {
+                    stateNext = ThresholdState::MUST_SIGNAL;
+                } else if (height >= height_timeout) {
+                    stateNext = ThresholdState::FAILED;
                 }
                 break;
             }
+            case ThresholdState::MUST_SIGNAL: {
+                // Always progresses into LOCKED_IN.
+                stateNext = ThresholdState::LOCKED_IN;
+                break;
+            }
             case ThresholdState::LOCKED_IN: {
-                // Always progresses into ACTIVE.
-                stateNext = ThresholdState::ACTIVE;
+                // Only progress into ACTIVE if minimum activation height has been reached
+                if (height >= height_active_min) {
+                    stateNext = ThresholdState::ACTIVE;
+                }
                 break;
             }
             case ThresholdState::FAILED:
@@ -125,8 +142,8 @@ BIP9Stats AbstractThresholdConditionChecker::GetStateStatisticsFor(const CBlockI
 
 int AbstractThresholdConditionChecker::GetStateSinceHeightFor(const CBlockIndex* pindexPrev, const Consensus::Params& params, ThresholdConditionCache& cache) const
 {
-    int64_t start_time = BeginTime(params);
-    if (start_time == Consensus::BIP9Deployment::ALWAYS_ACTIVE) {
+    int height_start = StartHeight(params);
+    if (height_start == Consensus::BIP9Deployment::ALWAYS_ACTIVE) {
         return 0;
     }
 
@@ -168,10 +185,12 @@ private:
     const Consensus::DeploymentPos id;
 
 protected:
-    int64_t BeginTime(const Consensus::Params& params) const override { return params.vDeployments[id].nStartTime; }
-    int64_t EndTime(const Consensus::Params& params) const override { return params.vDeployments[id].nTimeout; }
+    int StartHeight(const Consensus::Params& params) const override { return params.vDeployments[id].startheight; }
+    int TimeoutHeight(const Consensus::Params& params) const override { return params.vDeployments[id].timeoutheight; }
+    bool LockinOnTimeout(const Consensus::Params& params) const override { return params.vDeployments[id].lockinontimeout; }
     int Period(const Consensus::Params& params) const override { return params.nMinerConfirmationWindow; }
-    int Threshold(const Consensus::Params& params) const override { return params.nRuleChangeActivationThreshold; }
+    int Threshold(const Consensus::Params& params) const override { return params.vDeployments[id].threshold; }
+    int MinActivationHeight(const Consensus::Params& params) const override { return params.vDeployments[id].m_min_activation_height; }
 
     bool Condition(const CBlockIndex* pindex, const Consensus::Params& params) const override
     {
diff --git a/src/versionbits.h b/src/versionbits.h
index b02f848b6..0d8b97371 100644
--- a/src/versionbits.h
+++ b/src/versionbits.h
@@ -24,10 +24,11 @@ static const int32_t VERSIONBITS_NUM_BITS = 29;
  */
 enum class ThresholdState {
     DEFINED,   // First state that each softfork starts out as. The genesis block is by definition in this state for each deployment.
-    STARTED,   // For blocks past the starttime.
+    STARTED,   // For blocks past the startheight.
+    MUST_SIGNAL, // If lockinontimeout is true, the period immediately before timeoutheight unless LOCKED_IN is reached first
     LOCKED_IN, // For one retarget period after the first retarget period with STARTED blocks of which at least threshold have the associated bit set in nVersion.
-    ACTIVE,    // For all blocks after the LOCKED_IN retarget period (final state)
-    FAILED,    // For all blocks once the first retarget period after the timeout time is hit, if LOCKED_IN wasn't already reached (final state)
+    ACTIVE,    // For all blocks after the LOCKED_IN retarget period (final state) if the minimum activation height has been reached.
+    FAILED,    // For all blocks once the first retarget period after the timeout height is hit, if LOCKED_IN wasn't already reached (final state)
 };
 
 // A map that gives the state for blocks whose height is a multiple of Period().
@@ -55,10 +56,12 @@ struct BIP9Stats {
 class AbstractThresholdConditionChecker {
 protected:
     virtual bool Condition(const CBlockIndex* pindex, const Consensus::Params& params) const =0;
-    virtual int64_t BeginTime(const Consensus::Params& params) const =0;
-    virtual int64_t EndTime(const Consensus::Params& params) const =0;
+    virtual int StartHeight(const Consensus::Params& params) const =0;
+    virtual int TimeoutHeight(const Consensus::Params& params) const =0;
+    virtual bool LockinOnTimeout(const Consensus::Params& params) const =0;
     virtual int Period(const Consensus::Params& params) const =0;
     virtual int Threshold(const Consensus::Params& params) const =0;
+    virtual int MinActivationHeight(const Consensus::Params& params) const =0;
 
 public:
     /** Returns the numerical statistics of an in-progress BIP9 softfork in the current period */
diff --git a/test/functional/feature_bip8.py b/test/functional/feature_bip8.py
new file mode 100755
index 000000000..57d56c26a
--- /dev/null
+++ b/test/functional/feature_bip8.py
@@ -0,0 +1,125 @@
+#!/usr/bin/env python3
+# Copyright (c) 2021 The Bitcoin Core developers
+# Distributed under the MIT software license, see the accompanying
+# file COPYING or http://www.opensource.org/licenses/mit-license.php.
+# Test BIP 8 softforks
+from collections import namedtuple
+from test_framework.test_framework import BitcoinTestFramework
+from test_framework.util import assert_equal
+
+RestartedNodeInfo = namedtuple("RestartedNodeInfo", ("node", "extra_args", "status"))
+
+class Bip8Test(BitcoinTestFramework):
+    def set_test_params(self):
+        self.num_nodes = 4
+        self.setup_clean_chain = True
+        self.extra_args = [
+            ['-vbparams=testdummy:@-2:@-2'], # Node 0 has TestDummy inactive
+            ['-vbparams=testdummy:@144:@{}:0'.format(144 * 3)], # Node 1 has regular activation window
+            ['-vbparams=testdummy:@144:@{}:@{}:0'.format(144 * 3, 144 * 5)], # Node 2 has minimum activation height
+            ['-vbparams=testdummy:@-2:@-2'], # Node 3 has TestDummy inactive, but will be restarted with different params
+        ]
+
+    def setup_network(self):
+        self.setup_nodes()
+
+    def test_height(self, height, status, mine_from, restart):
+        if height > self.height:
+            self.log.info("Test status at height {}...".format(height))
+            while self.height < height:
+                blockhash = self.nodes[mine_from].generate(1)[0]
+                block = self.nodes[mine_from].getblock(blockhash, 0)
+                for node in self.nodes:
+                    node.submitblock(block)
+                    assert_equal(node.getbestblockhash(), blockhash)
+                self.height += 1
+        elif height < self.height:
+            assert mine_from is None
+            self.log.info("Roll back to height {}...".format(height))
+            old_block = self.nodes[0].getblockhash(height + 1)
+            for node in self.nodes:
+                node.invalidateblock(old_block)
+        else:
+            assert mine_from is None
+            assert height == 0 and self.height == 0
+            self.log.info("Test status at genesis...")
+
+        self.height = height
+
+        for (i, node), st in zip(enumerate(self.nodes), status):
+            self.log.debug("Node #{}...".format(i))
+            info = node.getblockchaininfo()
+            assert_equal(info["blocks"], height)
+            if st is None:
+                assert "testdummy" not in info["softforks"]
+            else:
+                assert_equal(info["softforks"]["testdummy"]["bip8"]["status"], st)
+
+        if restart:
+            # Restart this node and check that the status is what we expect
+            restart.extra_args[0] += ':0'  # LockinOnTimeout
+            self.restart_node(restart.node, restart.extra_args)
+            info = self.nodes[restart.node].getblockchaininfo()
+            assert_equal(info["blocks"], height)
+            if restart.status is None:
+                assert "testdummy" not in info["softforks"]
+            else:
+                assert_equal(info["softforks"]["testdummy"]["bip8"]["status"], restart.status)
+
+            # Now restart again to go back to the original settings
+            self.restart_node(restart.node, self.extra_args[restart.node])
+            info = self.nodes[restart.node].getblockchaininfo()
+            assert_equal(info["blocks"], height)
+            if status[restart.node] is None:
+                assert "testdummy" not in info["softforks"]
+            else:
+                assert_equal(info["softforks"]["testdummy"]["bip8"]["status"], status[restart.node])
+
+    def run_test(self):
+        self.log.info("Checking -vbparams")
+        self.stop_node(3)
+        self.nodes[3].assert_start_raises_init_error(extra_args=["-vbparams=testdummy:@-2:@1"], expected_msg="Error: When one of startheight or timeoutheight is -2, both must be -2")
+        self.nodes[3].assert_start_raises_init_error(extra_args=["-vbparams=testdummy:@1:@-2:0"], expected_msg="Error: When one of startheight or timeoutheight is -2, both must be -2")
+        self.nodes[3].assert_start_raises_init_error(extra_args=["-vbparams=testdummy:@1:@1:0"], expected_msg="Error: Invalid startheight (1), must be a multiple of 144")
+        self.nodes[3].assert_start_raises_init_error(extra_args=["-vbparams=testdummy:@144:@1:0"], expected_msg="Error: Invalid timeoutheight (1), must be a multiple of 144")
+        self.nodes[3].assert_start_raises_init_error(extra_args=["-vbparams=testdummy:@144:@144:@-1:0"], expected_msg="Error: Invalid minimum activation height (-1), cannot be negative")
+        self.nodes[3].assert_start_raises_init_error(extra_args=["-vbparams=testdummy:@144:@144:@1:0"], expected_msg="Error: Invalid minimum activation height (1), must be a multiple of 144")
+        self.nodes[3].assert_start_raises_init_error(extra_args=["-vbparams=testdummy:@288:@144:0"], expected_msg="Error: Invalid timeoutheight (144), must be at least two periods greater than the startheight (288)")
+        self.nodes[3].assert_start_raises_init_error(extra_args=["-vbparams=testdummy:@-3:@144"], expected_msg="Error: Invalid startheight (-3), cannot be negative (except for never or always active special cases)")
+        self.nodes[3].assert_start_raises_init_error(extra_args=["-vbparams=testdummy:@144:@-1:0"], expected_msg="Error: Invalid timeoutheight (-1), cannot be negative (except for never or always active special cases)")
+        self.nodes[3].assert_start_raises_init_error(extra_args=["-vbparams=testdummy:@{}:@144:0".format(0x7fffffff + 1)], expected_msg="Error: Invalid startheight (@{})".format(0x7fffffff + 1))
+        self.nodes[3].assert_start_raises_init_error(extra_args=["-vbparams=testdummy:@144:@{}:0".format(0x7fffffff + 1)], expected_msg="Error: Invalid timeoutheight (@{})".format(0x7fffffff + 1))
+        self.nodes[3].assert_start_raises_init_error(extra_args=["-vbparams=testdummy:@{}:@144:0".format(-(0x7fffffff + 2))], expected_msg="Error: Invalid startheight (@{})".format(-(0x7fffffff + 2)))
+        self.nodes[3].assert_start_raises_init_error(extra_args=["-vbparams=testdummy:@144:@{}:0".format(-(0x7fffffff + 2))], expected_msg="Error: Invalid timeoutheight (@{})".format(-(0x7fffffff + 2)))
+        self.start_node(3, self.extra_args[3])
+
+        self.height = 0
+        self.test_height(0, [None, "defined", "defined", None], None, RestartedNodeInfo(3, ["-vbparams=testdummy:@144:@{}".format(144*3)], "defined"))
+
+        # BIP 8 state transitions from "defined" to "started" or "failed" after
+        # the last block of the retargeting period has been mined. This means
+        # any new rules apply to transactions currently in the mempool, which
+        # might be mined in the next block.
+        #
+        # The next retargeting period starts at block 144, so nothing should
+        # happen at 142 and the state should change at 143.
+        self.test_height(144-2, [None, "defined", "defined", None], 0, RestartedNodeInfo(3, ["-vbparams=testdummy:@144:@{}".format(144*3)], "defined"))
+        self.test_height(144-1, [None, "started", "started", None], 0, RestartedNodeInfo(3, ["-vbparams=testdummy:@144:@{}".format(144*3)], "started"))
+
+        self.log.info("Test status when not signaling...")
+        self.test_height(144*2-1, [None, "started", "started", None], 0, RestartedNodeInfo(3, ["-vbparams=testdummy:@144:@{}".format(144*3)], "started"))
+        self.test_height(144*3-1, [None, "failed", "failed", None], 0, RestartedNodeInfo(3, ["-vbparams=testdummy:@144:@{}".format(144*3)], "failed"))
+
+        # The new branch has unique block hashes, because of the signalling and
+        # because generate uses a deterministic address that depends on the node
+        # index.
+        # Also tests that if the last period meets the threshold, the fork activates.
+        self.log.info("Test status when signaling...")
+        self.test_height(144-1, [None, "started", "started", None], None, RestartedNodeInfo(3, ["-vbparams=testdummy:@144:@{}".format(144*3)], "started"))
+        self.test_height(144*2-1, [None, "started", "started", None], 3, RestartedNodeInfo(3, ["-vbparams=testdummy:@144:@{}".format(144*3)], "started"))
+        self.test_height(144*3-1, [None, "locked_in", "locked_in", None], 1, RestartedNodeInfo(3, ["-vbparams=testdummy:@144:@{}".format(144*3)], "locked_in"))
+        self.test_height(144*4-1, [None, "active", "locked_in", None], 1, RestartedNodeInfo(3, ["-vbparams=testdummy:@144:@{}".format(144*3)], "active"))
+        self.test_height(144*5-1, [None, "active", "active", None], 1, RestartedNodeInfo(3, ["-vbparams=testdummy:@144:@{}".format(144*3)], "active"))
+
+if __name__ == '__main__':
+    Bip8Test().main()
diff --git a/test/functional/feature_bip8b.py b/test/functional/feature_bip8b.py
new file mode 100755
index 000000000..9f4113c24
--- /dev/null
+++ b/test/functional/feature_bip8b.py
@@ -0,0 +1,277 @@
+#!/usr/bin/env python3
+# Copyright (c) 2021 The Bitcoin Core developers
+# Distributed under the MIT software license, see the accompanying
+# file COPYING or http://www.opensource.org/licenses/mit-license.php.
+"""Test bip8 activation
+"""
+
+import random
+import time
+
+from test_framework.test_framework import BitcoinTestFramework
+from test_framework.util import assert_equal, assert_greater_than_or_equal
+from test_framework.blocktools import create_block, create_coinbase
+from test_framework.p2p import P2PDataStore, p2p_lock
+from test_framework.messages import CBlockHeader, msg_headers
+
+SEED = random.randrange(2**128)
+
+VB_SIGNAL = 0x20000000 | (0x01 << 28)
+VB_NOSIGNAL = 0x20000000
+
+MAX_HEADERS = 2000
+
+BASE_TIME = int(time.time()) - 12*60*60
+
+class P2PBlockCheck(P2PDataStore):
+    def __init__(self, node, blocks):
+        super().__init__()
+        self.node = node
+        with p2p_lock:
+            for block in blocks:
+                self.block_store[block.sha256] = block
+                self.last_block_hash = block.sha256
+
+    def refresh_headers(self):
+        headers = []
+        with p2p_lock:
+            for _, block in self.block_store.items():
+                headers.append(CBlockHeader(block))
+        while headers:
+            self.send_message(msg_headers(headers[:MAX_HEADERS]))
+            headers = headers[MAX_HEADERS:]
+
+    def send_blocks(self, blocks, *, reject_reason=None, timeout=20):
+        tiphash = blocks[-1].hash
+
+        with p2p_lock:
+            for block in blocks:
+                self.block_store[block.sha256] = block
+                self.last_block_hash = block.sha256
+
+        reject_reason = [reject_reason] if reject_reason else []
+        success = reject_reason == []
+
+        with self.node.assert_debug_log(expected_msgs=reject_reason, timeout=timeout):
+            self.send_message(msg_headers([CBlockHeader(block) for block in blocks]))
+            self.sync_with_ping(timeout)
+            if success:
+                self.wait_until(lambda: self.node.getbestblockhash() == tiphash, timeout=timeout)
+
+        if not success:
+            assert self.node.getbestblockhash() != tiphash
+
+class BIP8Test(BitcoinTestFramework):
+    def set_test_params(self):
+        self.setup_clean_chain = True
+        self.num_nodes = 4
+
+    def setup_network(self):
+        # don't want to connect the bitcoinds to each other
+        self.setup_nodes()
+
+    def generate_blocks(self, versioniter, prevblock, tipheight):
+        test_blocks = []
+
+        for version in versioniter:
+            blocktime = BASE_TIME + (tipheight * 6)  # 6 seconds between blocks
+            block = create_block(prevblock, create_coinbase(tipheight), blocktime)
+            block.nVersion = version
+            block.rehash()
+            block.solve()
+            test_blocks.append(block)
+            prevblock = block.sha256
+            tipheight = tipheight + 1
+
+        return test_blocks
+
+    def apply_blocks_and_check(self, versions, base_height, state, height=None):
+        start = base_height + 288
+        stop = base_height + 720
+
+        if height is None:
+            bci = self.nodes[0].getblockchaininfo()
+            prevblock = int("0x" + bci["bestblockhash"], 0)
+            tipheight = bci["blocks"] + 1
+        else:
+            prevblock = int("0x" + self.nodes[0].getblockhash(height), 0)
+            tipheight = height + 1
+
+        blocks = self.generate_blocks(versions, prevblock, tipheight)
+
+        self.helper[0].send_blocks(blocks)
+        self.helper[1].send_blocks(blocks)
+        self.helper[2].send_blocks(blocks)
+        if state[0] == "stopped":
+            ok_cnt = state[1]-tipheight
+            if ok_cnt > 0:
+                self.helper[3].send_blocks(blocks[:ok_cnt])
+            if ok_cnt >= 0:
+                self.helper[3].send_blocks(blocks[ok_cnt:], reject_reason="bad-vbit-unset-testdummy")
+        else:
+            self.helper[3].send_blocks(blocks)
+
+        heights, status = self.get_softfork_status()
+
+        # compare results
+        assert_equal(heights[0], heights[1], heights[2])
+        assert_greater_than_or_equal(heights[0], heights[3])
+        if state[0] != "stopped":
+            assert_equal(heights[0], heights[3])
+
+        if state[0] != "stopped":
+            assert_equal(status[2]["bip8"].get("statistics", None), status[3]["bip8"].get("statistics", None))
+            assert_equal(status[2]["active"], status[3]["active"])
+
+        # never active
+        assert_equal(status[0], None)
+
+        # always active
+        assert_equal(status[1], {'type': 'bip8', 'bip8': {'status': 'active', 'startheight': -1, 'timeoutheight': 0, 'minimum_activation_height': 0, 'lockinontimeout': True, 'since': 0}, 'height': 0, 'active': True})
+
+        # lockinontimeout=false
+        if "statistics" in status[2]["bip8"]:
+            status[2]["bip8"]["statistics"] = None
+
+        if state[0] == "defined":
+            assert_equal(status[2], {'type': 'bip8', 'bip8': {'status': 'defined', 'startheight': start, 'timeoutheight': stop, 'minimum_activation_height': 0, 'lockinontimeout': False, 'since': state[1]}, 'active': False})
+        elif state[0] == "started" or state[0] == "must_signal":
+            since = state[1] if state[0] == "started" else (state[1] - 288)
+            assert_equal(status[2], {'type': 'bip8', 'bip8': {'status': 'started', 'bit': 28, 'startheight': start, 'timeoutheight': stop, 'minimum_activation_height': 0, 'lockinontimeout': False, 'since': since, 'statistics': None}, 'active': False})
+        elif state[0] == "locked_in":
+            assert_equal(status[2], {'type': 'bip8', 'bip8': {'status': 'locked_in', 'bit': 28, 'startheight': start, 'timeoutheight': stop, 'minimum_activation_height': 0, 'lockinontimeout': False, 'since': state[1], 'statistics': None}, 'active': False})
+        elif state[0] == "active":
+            assert_equal(status[2], {'type': 'bip8', 'bip8': {'status': 'active', 'startheight': start, 'timeoutheight': stop, 'minimum_activation_height': 0, 'lockinontimeout': False, 'since': state[1]}, 'height': state[1], 'active': True})
+        elif state[0] == "stopped":
+            failat = state[1] - (state[1] % 144) + 144
+            assert_equal(status[2], {'type': 'bip8', 'bip8': {'status': 'failed', 'startheight': start, 'timeoutheight': stop, 'minimum_activation_height': 0, 'lockinontimeout': False, 'since': failat}, 'active': False})
+        else:
+            assert False, ("bad state %r" % (state))
+
+        # lockinontimeout=true
+        if "statistics" in status[3]["bip8"]:
+            assert_equal(status[3]["bip8"]["statistics"]["possible"], True)
+            status[3]["bip8"]["statistics"] = None
+
+        if state[0] == "defined":
+            assert_equal(status[3], {'type': 'bip8', 'bip8': {'status': 'defined', 'startheight': start, 'timeoutheight': stop, 'minimum_activation_height': 0, 'lockinontimeout': True, 'since': state[1]}, 'active': False})
+        elif state[0] == "started":
+            assert_equal(status[3], {'type': 'bip8', 'bip8': {'status': 'started', 'bit': 28, 'startheight': start, 'timeoutheight': stop, 'minimum_activation_height': 0, 'lockinontimeout': True, 'since': state[1], 'statistics': None}, 'active': False})
+        elif state[0] == "must_signal" or state[0] == "stopped":
+            since = state[1] - state[1] % 144
+            assert_equal(status[3], {'type': 'bip8', 'bip8': {'status': 'must_signal', 'bit': 28, 'startheight': start, 'timeoutheight': stop, 'minimum_activation_height': 0, 'lockinontimeout': True, 'since': since, 'statistics': None}, 'active': False})
+        elif state[0] == "locked_in":
+            assert_equal(status[3], {'type': 'bip8', 'bip8': {'status': 'locked_in', 'bit': 28, 'startheight': start, 'timeoutheight': stop, 'minimum_activation_height': 0, 'lockinontimeout': True, 'since': state[1], 'statistics': None}, 'active': False})
+        elif state[0] == "active":
+            assert_equal(status[3], {'type': 'bip8', 'bip8': {'status': 'active', 'startheight': start, 'timeoutheight': stop, 'minimum_activation_height': 0, 'lockinontimeout': True, 'since': state[1]}, 'height': state[1], 'active': True})
+        else:
+            assert False, ("bad state %r" % (state))
+
+        return blocks
+
+
+    def get_softfork_status(self):
+        info = [node.getblockchaininfo() for node in self.nodes]
+        return zip( *[(i["blocks"], i['softforks'].get('testdummy', None))
+                    for i in info] )
+
+    def setup_vbparams(self):
+        base_height = self.nodes[0].getblockcount()
+        if base_height:
+            base_height += 1
+        assert base_height % 144 == 0
+        start = base_height + 288
+        stop = base_height + 720
+
+        for node in self.nodes:
+            node.disconnect_p2ps()
+        self.helper = []
+        self.stop_nodes()
+
+        self.start_nodes(extra_args = [
+            # never active
+            ["-vbparams=testdummy:@-2:@-2:0"],
+            # always active
+            ["-vbparams=testdummy:@-1:@0:1"],
+            # abandon softfork if not locked in by timeout
+            ["-vbparams=testdummy:@%s:@%s:0" % (start, stop)],
+            # reject blocks if not locked in by timeout
+            ["-vbparams=testdummy:@%s:@%s:1" % (start, stop)],
+        ])
+        for n in range(1, 4):
+            self.connect_nodes(0, n)
+        self.sync_all()
+        for n in range(1, 4):
+            self.disconnect_nodes(0, n)
+        self.helper = [n.add_p2p_connection(P2PBlockCheck(n, self.all_blocks)) for n in self.nodes]
+        return base_height
+
+    def do_test(self, signalling, expstate, expheight):
+        base_height = self.setup_vbparams()
+
+        # track the expected state/height for rejecting-node
+        state = "defined", 0
+
+        period = 0
+        for cnt in signalling:
+            nblocks = 144
+            if base_height == 0 and period == 0:
+                nblocks -= 1
+            cnt = max(0, min(cnt, nblocks))
+            bits = [VB_SIGNAL]*cnt + [VB_NOSIGNAL]*(nblocks - cnt)
+
+            random.shuffle(bits)
+
+            # what will the state be after these blocks are mined for a lockinontimeout=true node?
+            period += 1
+            if state[0] == "defined" and period == 2:
+                state = "started", base_height + (period*144)
+            elif state[0] == "started":
+                if cnt >= 108:
+                    state = "locked_in", base_height + (period*144)
+                elif period == 4:
+                    state = "must_signal", base_height + (period*144)
+            elif state[0] == "must_signal":
+                if cnt >= 108:
+                    state = "locked_in", base_height + (period*144)
+                else:
+                    oknosig = 144-108
+                    howmany = None
+                    for i, x in enumerate(bits):
+                        if x != VB_SIGNAL:
+                            if oknosig == 0:
+                                howmany = i
+                                break
+                            oknosig -= 1
+                    state = "stopped", base_height + (period*144) - 144 + howmany
+            elif state[0] == "locked_in":
+                state = "active", base_height + (period*144)
+
+            self.all_blocks.extend(self.apply_blocks_and_check(bits, base_height, state))
+
+        assert_equal(state[0], expstate)
+        assert_greater_than_or_equal(state[1] - base_height, expheight)
+        if expstate != "stopped":
+            assert_equal(state[1] - base_height, expheight)
+        else:
+            last_possible = expheight - expheight%144 + 143
+            assert_greater_than_or_equal(last_possible, state[1] - base_height)
+        self.log.info("Completed test %s %s" % (base_height, state))
+
+    def run_test(self):
+        self.all_blocks = []
+
+        random.seed(SEED)
+        N = random.randrange(0,108)
+        Y = random.randrange(108,145)
+        self.log.info("seed %s, signal vals %s/%s" % (SEED, Y, N))
+
+        self.do_test([N,N,N,N,107,144], "stopped", 612)
+        self.do_test([144,144,Y,0,0,0], "active", 576)
+        self.do_test([Y,Y,0,0,0,Y], "stopped", 612)
+        self.do_test([N,N,N,N,N,144], "stopped", 612)
+        self.do_test([0,0,0,Y,0,0], "active", 720)
+        self.do_test([0,144,N,N,Y,0], "active", 864)
+
+if __name__ == '__main__':
+    BIP8Test().main()
diff --git a/test/functional/feature_includeconf.py b/test/functional/feature_includeconf.py
index 6f1a0cd34..eda9b64f8 100755
--- a/test/functional/feature_includeconf.py
+++ b/test/functional/feature_includeconf.py
@@ -39,7 +39,7 @@ class IncludeConfTest(BitcoinTestFramework):
         self.log.info("-includeconf works from config file. subversion should end with 'main; relative)/'")
 
         subversion = self.nodes[0].getnetworkinfo()["subversion"]
-        assert subversion.endswith("main; relative)/")
+        assert "main; relative)" in subversion
 
         self.log.info("-includeconf cannot be used as command-line arg")
         self.stop_node(0)
@@ -51,7 +51,7 @@ class IncludeConfTest(BitcoinTestFramework):
         self.start_node(0)
 
         subversion = self.nodes[0].getnetworkinfo()["subversion"]
-        assert subversion.endswith("main; relative)/")
+        assert "main; relative)" in subversion
         self.stop_node(0, expected_stderr="warning: -includeconf cannot be used from included files; ignoring -includeconf=relative2.conf")
 
         self.log.info("-includeconf cannot contain invalid arg")
@@ -76,7 +76,7 @@ class IncludeConfTest(BitcoinTestFramework):
         self.start_node(0)
 
         subversion = self.nodes[0].getnetworkinfo()["subversion"]
-        assert subversion.endswith("main; relative; relative2)/")
+        assert "main; relative; relative2)" in subversion
 
 if __name__ == '__main__':
     IncludeConfTest().main()
diff --git a/test/functional/feature_taproot.py b/test/functional/feature_taproot.py
index 0f0fe8a34..1c33a3a4c 100755
--- a/test/functional/feature_taproot.py
+++ b/test/functional/feature_taproot.py
@@ -1199,8 +1199,10 @@ class TaprootTest(BitcoinTestFramework):
     def set_test_params(self):
         self.num_nodes = 2
         self.setup_clean_chain = True
-        # Node 0 has Taproot inactive, Node 1 active.
-        self.extra_args = [["-par=1", "-vbparams=taproot:1:1"], ["-par=1"]]
+        self.extra_args = [
+            ["-par=1", "-vbparams=taproot:@-2:@-2"], # Node 0 has Taproot never active
+            ["-par=1"] # Node 1 has taproot always active
+        ]
 
     def block_submit(self, node, txs, msg, err_msg, cb_pubkey=None, fees=0, sigops_weight=0, witness=False, accept=False):
 
diff --git a/test/functional/feature_uacomment.py b/test/functional/feature_uacomment.py
index 4720f6dea..7e59f7705 100755
--- a/test/functional/feature_uacomment.py
+++ b/test/functional/feature_uacomment.py
@@ -8,7 +8,6 @@ import re
 
 from test_framework.test_framework import BitcoinTestFramework
 from test_framework.test_node import ErrorMatch
-from test_framework.util import assert_equal
 
 
 class UacommentTest(BitcoinTestFramework):
@@ -18,12 +17,12 @@ class UacommentTest(BitcoinTestFramework):
 
     def run_test(self):
         self.log.info("test multiple -uacomment")
-        test_uacomment = self.nodes[0].getnetworkinfo()["subversion"][-12:-1]
-        assert_equal(test_uacomment, "(testnode0)")
+        test_uacomment = self.nodes[0].getnetworkinfo()["subversion"]
+        assert "(testnode0)" in test_uacomment
 
         self.restart_node(0, ["-uacomment=foo"])
-        foo_uacomment = self.nodes[0].getnetworkinfo()["subversion"][-17:-1]
-        assert_equal(foo_uacomment, "(testnode0; foo)")
+        foo_uacomment = self.nodes[0].getnetworkinfo()["subversion"]
+        assert "(testnode0; foo)" in foo_uacomment
 
         self.log.info("test -uacomment max length")
         self.stop_node(0)
diff --git a/test/functional/rpc_blockchain.py b/test/functional/rpc_blockchain.py
index f96567740..e0b926666 100755
--- a/test/functional/rpc_blockchain.py
+++ b/test/functional/rpc_blockchain.py
@@ -131,12 +131,14 @@ class BlockchainTest(BitcoinTestFramework):
             'csv': {'type': 'buried', 'active': False, 'height': 432},
             'segwit': {'type': 'buried', 'active': True, 'height': 0},
             'testdummy': {
-                'type': 'bip9',
-                'bip9': {
+                'type': 'bip8',
+                'bip8': {
                     'status': 'started',
                     'bit': 28,
-                    'start_time': 0,
-                    'timeout': 0x7fffffffffffffff,  # testdummy does not have a timeout so is set to the max int64 value
+                    'startheight': 144,
+                    'timeoutheight': 0x7fffffff,  # testdummy does not have a timeout so is set to the max int value
+                    'minimum_activation_height': 0,
+                    'lockinontimeout': False,
                     'since': 144,
                     'statistics': {
                         'period': 144,
@@ -149,11 +151,13 @@ class BlockchainTest(BitcoinTestFramework):
                 'active': False
             },
             'taproot': {
-                'type': 'bip9',
-                'bip9': {
+                'type': 'bip8',
+                'bip8': {
                     'status': 'active',
-                    'start_time': -1,
-                    'timeout': 9223372036854775807,
+                    'startheight': -1,
+                    'timeoutheight': 0x7fffffff,
+                    'minimum_activation_height': 0,
+                    'lockinontimeout': False,
                     'since': 0
                 },
                 'height': 0,
diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py
index 5b3db282e..22e0a674e 100755
--- a/test/functional/test_runner.py
+++ b/test/functional/test_runner.py
@@ -90,6 +90,7 @@ BASE_SCRIPTS = [
     'wallet_hd.py --descriptors',
     'wallet_backup.py',
     'wallet_backup.py --descriptors',
+    'feature_bip8b.py',
     # vv Tests less than 5m vv
     'mining_getblocktemplate_longpoll.py',
     'feature_maxuploadtarget.py',
@@ -129,6 +130,7 @@ BASE_SCRIPTS = [
     'p2p_feefilter.py',
     'feature_reindex.py',
     'feature_abortnode.py',
+    'feature_bip8.py',
     # vv Tests less than 30s vv
     'wallet_keypool_topup.py',
     'wallet_keypool_topup.py --descriptors',
