diff --git a/integration/bip0009_test.go b/integration/bip0009_test.go index 7de6137c0..9e7c8d2b9 100644 --- a/integration/bip0009_test.go +++ b/integration/bip0009_test.go @@ -8,14 +8,27 @@ package integration import ( + "fmt" "runtime" "testing" + "time" + "github.com/btcsuite/btcd/blockchain" "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/rpctest" ) +const ( + // vbLegacyBlockVersion is the highest legacy block version before the + // version bits scheme became active. + vbLegacyBlockVersion = 4 + + // vbTopBits defines the bits to set in the version to signal that the + // version bits scheme is being used. + vbTopBits = 0x20000000 +) + // assertVersionBit gets the passed block hash from the given test harness and // ensures its version either has the provided bit set or unset per the set // flag. @@ -37,6 +50,257 @@ func assertVersionBit(r *rpctest.Harness, t *testing.T, hash *chainhash.Hash, bi } } +// assertChainHeight retrieves the current chain height from the given test +// harness and ensures it matches the provided expected height. +func assertChainHeight(r *rpctest.Harness, t *testing.T, expectedHeight uint32) { + height, err := r.Node.GetBlockCount() + if err != nil { + t.Fatalf("failed to retrieve block height: %v", err) + } + if uint32(height) != expectedHeight { + _, _, line, _ := runtime.Caller(1) + t.Fatalf("assertion failed at line %d: block height of %d "+ + "is not the expected %d", line, height, expectedHeight) + } +} + +// thresholdStateToStatus converts the passed threshold state to the equivalent +// status string returned in the getblockchaininfo RPC. +func thresholdStateToStatus(state blockchain.ThresholdState) (string, error) { + switch state { + case blockchain.ThresholdDefined: + return "defined", nil + case blockchain.ThresholdStarted: + return "started", nil + case blockchain.ThresholdLockedIn: + return "lockedin", nil + case blockchain.ThresholdActive: + return "active", nil + case blockchain.ThresholdFailed: + return "failed", nil + } + + return "", fmt.Errorf("unrecognized threshold state: %v", state) +} + +// assertSoftForkStatus retrieves the current blockchain info from the given +// test harness and ensures the provided soft fork key is both available and its +// status is the equivalent of the passed state. +func assertSoftForkStatus(r *rpctest.Harness, t *testing.T, forkKey string, state blockchain.ThresholdState) { + // Convert the expected threshold state into the equivalent + // getblockchaininfo RPC status string. + status, err := thresholdStateToStatus(state) + if err != nil { + _, _, line, _ := runtime.Caller(1) + t.Fatalf("assertion failed at line %d: unable to convert "+ + "threshold state %v to string", line, state) + } + + info, err := r.Node.GetBlockChainInfo() + if err != nil { + t.Fatalf("failed to retrieve chain info: %v", err) + } + + // Ensure the key is available. + desc, ok := info.Bip9SoftForks[forkKey] + if !ok { + _, _, line, _ := runtime.Caller(1) + t.Fatalf("assertion failed at line %d: softfork status for %q "+ + "is not in getblockchaininfo results", line, forkKey) + } + + // Ensure the status it the expected value. + if desc.Status != status { + _, _, line, _ := runtime.Caller(1) + t.Fatalf("assertion failed at line %d: softfork status for %q "+ + "is %v instead of expected %v", line, forkKey, + desc.Status, status) + } +} + +// testBIP0009 ensures the BIP0009 soft fork mechanism follows the state +// transition rules set forth by the BIP for the provided soft fork key. It +// uses the regression test network to signal support and advance through the +// various threshold states including failure to achieve locked in status. +// +// See TestBIP0009 for an overview of what is tested. +// +// NOTE: This only differs from the exported version in that it accepts the +// specific soft fork deployment to test. +func testBIP0009(t *testing.T, forkKey string, deploymentID uint32) { + // Initialize the primary mining node with only the genesis block. + r, err := rpctest.New(&chaincfg.RegressionNetParams, nil, nil) + if err != nil { + t.Fatalf("unable to create primary harness: %v", err) + } + if err := r.SetUp(false, 0); err != nil { + t.Fatalf("unable to setup test chain: %v", err) + } + defer r.TearDown() + + // *** ThresholdDefined *** + // + // Assert the chain height is the expected value and the soft fork + // status starts out as defined. + assertChainHeight(r, t, 0) + assertSoftForkStatus(r, t, forkKey, blockchain.ThresholdDefined) + + // *** ThresholdDefined part 2 - 1 block prior to ThresholdStarted *** + // + // Generate enough blocks to reach the height just before the first + // state transition without signalling support since the state should + // move to started once the start time has been reached regardless of + // support signalling. + // + // NOTE: This is two blocks before the confirmation window because the + // getblockchaininfo RPC reports the status for the block AFTER the + // current one. All of the heights below are thus offset by one to + // compensate. + // + // Assert the chain height is the expected value and soft fork status is + // still defined and did NOT move to started. + confirmationWindow := r.ActiveNet.MinerConfirmationWindow + for i := uint32(0); i < confirmationWindow-2; i++ { + _, err := r.GenerateAndSubmitBlock(nil, vbLegacyBlockVersion, + time.Time{}) + if err != nil { + t.Fatalf("failed to generated block %d: %v", i, err) + } + } + assertChainHeight(r, t, confirmationWindow-2) + assertSoftForkStatus(r, t, forkKey, blockchain.ThresholdDefined) + + // *** ThresholdStarted *** + // + // Generate another block to reach the next window. + // + // Assert the chain height is the expected value and the soft fork + // status is started. + _, err = r.GenerateAndSubmitBlock(nil, vbLegacyBlockVersion, time.Time{}) + if err != nil { + t.Fatalf("failed to generated block: %v", err) + } + assertChainHeight(r, t, confirmationWindow-1) + assertSoftForkStatus(r, t, forkKey, blockchain.ThresholdStarted) + + // *** ThresholdStarted part 2 - Fail to achieve ThresholdLockedIn *** + // + // Generate enough blocks to reach the next window in such a way that + // the number blocks with the version bit set to signal support is 1 + // less than required to achieve locked in status. + // + // Assert the chain height is the expected value and the soft fork + // status is still started and did NOT move to locked in. + if deploymentID > uint32(len(r.ActiveNet.Deployments)) { + t.Fatalf("deployment ID %d does not exist", deploymentID) + } + deployment := &r.ActiveNet.Deployments[deploymentID] + activationThreshold := r.ActiveNet.RuleChangeActivationThreshold + signalForkVersion := int32(1<