btcd/blockchain/validate_rapid_test.go

345 lines
11 KiB
Go
Raw Normal View History

package blockchain
import (
"testing"
"time"
"pgregory.net/rapid"
)
// TestAssertNoTimeWarpProperties uses property-based testing to verify that
// the assertNoTimeWarp function correctly implements the BIP-94 rule. This
// helps catch edge cases that might be missed with regular unit tests.
func TestAssertNoTimeWarpProperties(t *testing.T) {
t.Parallel()
// Define constant for blocks per retarget (similar to Bitcoin's 2016).
const blocksPerRetarget = 2016
// Rapid test that only the retarget blocks are checked.
t.Run("only_checks_retarget_blocks", rapid.MakeCheck(func(t *rapid.T) {
// Generate block height that is not a retarget block.
height := rapid.Int32Range(
1, 1000000,
).Filter(func(h int32) bool {
return h%blocksPerRetarget != 0
}).Draw(t, "height")
// Even with an "extreme" time warp, the function should return
// nil because it only applies the check to retarget blocks.
headerTime := time.Unix(0, 0) // Unix epoch start
prevBlockTime := time.Now() // Current time, creating extreme gap
err := assertNoTimeWarp(
height, blocksPerRetarget,
headerTime, prevBlockTime,
)
if err != nil {
t.Fatalf("expected nil error for non-retarget block "+
"but got: %v", err)
}
}))
// Rapid test that retarget blocks with acceptable timestamps pass
// validation.
t.Run("valid_timestamps_pass", rapid.MakeCheck(func(t *rapid.T) {
// Generate block height that is a retarget block
height := rapid.Int32Range(blocksPerRetarget, 1000000).
Filter(func(h int32) bool {
return h%blocksPerRetarget == 0
}).Draw(t, "height")
// Generate a previous block timestamp.
prevTimeUnix := rapid.Int64Range(
1000000, 2000000000,
).Draw(t, "prev_time")
prevBlockTime := time.Unix(prevTimeUnix, 0)
// Generate a header timestamp that is not more than
// maxTimeWarp earlier than the previous block timestamp.
minValidHeaderTime := prevBlockTime.Add(
-maxTimeWarp,
).Add(time.Second)
// Generate any valid header time between the minimum valid
// time and prevBlockTime to ensure it passes the time warp
// check.
minTimeUnix := minValidHeaderTime.Unix()
maxTimeUnix := prevBlockTime.Unix()
// Ensure min is always less than max.
if minTimeUnix >= maxTimeUnix {
// If we can't generate a valid range, just use the
// previous block time which is guaranteed to pass the
// test.
headerTime := prevBlockTime
err := assertNoTimeWarp(
height, blocksPerRetarget, headerTime,
prevBlockTime,
)
if err != nil {
t.Fatalf("expected valid timestamps to pass, "+
"but got: %v", err)
}
return
}
headerTimeUnix := rapid.Int64Range(
minTimeUnix, maxTimeUnix,
).Draw(t, "header_time_unix")
headerTime := time.Unix(headerTimeUnix, 0)
err := assertNoTimeWarp(
height, blocksPerRetarget, headerTime, prevBlockTime,
)
if err != nil {
t.Fatalf("expected valid timestamps to pass, "+
"but got: %v", err)
}
}))
// Rapid test that retarget blocks with invalid timestamps fail
// validation.
t.Run("invalid_timestamps_fail", rapid.MakeCheck(func(t *rapid.T) {
// Generate block height that is a retarget block.
height := rapid.Int32Range(blocksPerRetarget, 1000000).
Filter(func(h int32) bool {
return h%blocksPerRetarget == 0
}).Draw(t, "height")
// Generate a previous block timestamp.
prevTimeUnix := rapid.Int64Range(
1000000, 2000000000,
).Draw(t, "prev_time")
prevBlockTime := time.Unix(prevTimeUnix, 0)
// Invalid header timestamp: more than maxTimeWarp earlier than
// prevBlockTime Ensure we generate a time that is definitely
// beyond the maxTimeWarp (which is 600 seconds) by using at
// least 601 seconds.
invalidDelta := time.Duration(
-rapid.Int64Range(601, 86400).Draw(t, "invalid_delta"),
) * time.Second
headerTime := prevBlockTime.Add(invalidDelta)
err := assertNoTimeWarp(
height, blocksPerRetarget, headerTime, prevBlockTime,
)
if err == nil {
t.Fatalf("expected error for time-warped " +
"header but got nil")
}
// Verify the correct error type is returned
if _, ok := err.(RuleError); !ok {
t.Fatalf("expected Ruleerror but got: %T", err)
}
// Verify it's the expected ErrTimewarpAttack error
if ruleErr, ok := err.(RuleError); ok {
if ruleErr.ErrorCode != ErrTimewarpAttack {
t.Fatalf("expected ErrTimewarpAttack but "+
"got: %v", ruleErr.ErrorCode)
}
}
}))
// Test the edge case right at the boundary of maxTimeWarp.
t.Run("boundary_timestamps", rapid.MakeCheck(func(t *rapid.T) {
// Generate block height that is a retarget block.
height := rapid.Int32Range(blocksPerRetarget, 1000000).
Filter(func(h int32) bool {
return h%blocksPerRetarget == 0
}).Draw(t, "height")
// Generate a previous block timestamp with enough padding
// to avoid time.Time precision issues.
prevTimeUnix := rapid.Int64Range(
1000000, 2000000000,
).Draw(t, "prev_time")
prevBlockTime := time.Unix(prevTimeUnix, 0)
// Test exact boundary: headerTime is exactly maxTimeWarp earlier
headerTime := prevBlockTime.Add(-maxTimeWarp)
// Check the actual implementation (looking at
// validate.go:797-798) The comparison is
// "headerTimestamp.Before(prevBlockTimestamp.Add(-maxTimeWarp))"
// This means at exact boundary (headerTime ==
// prevBlockTime.Add(-maxTimeWarp)) it should NOT fail, since
// Before() is strict < not <=.
err := assertNoTimeWarp(
height, blocksPerRetarget, headerTime, prevBlockTime,
)
if err != nil {
t.Fatalf("expected no error at exact boundary "+
"but got: %v", err)
}
// Test 1 nanosecond BEYOND the boundary (which should fail).
headerTime = prevBlockTime.Add(-maxTimeWarp).Add(-time.Nanosecond)
// This should fail as it's just beyond the maxTimeWarp limit.
err = assertNoTimeWarp(
height, blocksPerRetarget, headerTime, prevBlockTime,
)
if err == nil {
t.Fatalf("expected error just beyond boundary " +
"but got nil")
}
}))
}
// TestAssertNoTimeWarpInvariants uses property-based testing to verify the
// invariants of the assertNoTimeWarp function regardless of inputs.
func TestAssertNoTimeWarpInvariants(t *testing.T) {
t.Parallel()
// Invariant: The function should never panic regardless of input.
t.Run("never_panics", rapid.MakeCheck(func(t *rapid.T) {
// Generate any possible inputs
height := rapid.Int32().Draw(t, "height")
blocksPerRetarget := rapid.Int32Range(
1, 10000,
).Draw(t, "blocks_per_retarget")
headerTimeUnix := rapid.Int64().Draw(t, "header_time")
prevTimeUnix := rapid.Int64().Draw(t, "prev_time")
headerTime := time.Unix(headerTimeUnix, 0)
prevBlockTime := time.Unix(prevTimeUnix, 0)
// The function should never panic regardless of input
_ = assertNoTimeWarp(
height, blocksPerRetarget, headerTime, prevBlockTime,
)
}))
// Invariant: For non-retarget blocks, the function always returns nil.
t.Run("non_retarget_blocks_return_nil", rapid.MakeCheck(func(t *rapid.T) { //nolint:lll
// Generate height and blocksPerRetarget such that height is
// not a multiple of blocksPerRetarget.
blocksPerRetarget := rapid.Int32Range(
2, 10000,
).Draw(t, "blocks_per_retarget")
// Ensure height is not a multiple of blocksPerRetarget.
remainders := rapid.Int32Range(
1, blocksPerRetarget-1,
).Draw(t, "remainder")
height := rapid.Int32Range(
0, 1000000,
).Draw(t, "base")*blocksPerRetarget + remainders
// Generate any timestamps, even invalid ones.
headerTime := time.Unix(rapid.Int64().Draw(
t, "header_time"), 0,
)
prevBlockTime := time.Unix(rapid.Int64().Draw(
t, "prev_time"), 0,
)
// For non-retarget blocks, should always return nil.
err := assertNoTimeWarp(
height, blocksPerRetarget, headerTime, prevBlockTime,
)
if err != nil {
t.Fatalf("expected nil for non-retarget block "+
"(height=%d, blocks_per_retarget=%d) but "+
"got: %v",
height, blocksPerRetarget, err)
}
}))
}
// TestAssertNoTimeWarpSecurity tests the security properties of the
// assertNoTimeWarp function. This verifies that the function properly prevents
// "time warp" attacks where miners might attempt to manipulate timestamps for
// difficulty adjustment blocks.
func TestAssertNoTimeWarpSecurity(t *testing.T) {
t.Parallel()
const blocksPerRetarget = 2016
// Test that all difficulty adjustment blocks are protected from timewarp.
t.Run("all_retarget_blocks_protected", rapid.MakeCheck(func(t *rapid.T) { //nolint:lll
// Generate any retarget block height (multiples of
// blocksPerRetarget).
multiplier := rapid.Int32Range(1, 1000).Draw(t, "multiplier")
height := multiplier * blocksPerRetarget
// Generate a reasonable previous block timestamp.
prevTimeUnix := rapid.Int64Range(
1000000, 2000000000,
).Draw(t, "prev_time")
prevBlockTime := time.Unix(prevTimeUnix, 0)
// Generate a test header timestamp that's significantly before
// the previous timestamp This should always be rejected for
// retarget blocks.
timeDiff := rapid.Int64Range(
int64(maxTimeWarp+time.Second),
int64(maxTimeWarp+time.Hour*24*7),
).Draw(t, "warp_amount")
invalidDelta := time.Duration(-timeDiff)
headerTime := prevBlockTime.Add(invalidDelta)
// This should always fail with ErrTimewarpAttack for any retarget block.
err := assertNoTimeWarp(
height, blocksPerRetarget, headerTime, prevBlockTime,
)
if err == nil {
t.Fatalf("security vulnerability: Time warp attack "+
"not detected for height %d", height)
}
// Verify it's the expected error type>
if ruleErr, ok := err.(RuleError); ok {
if ruleErr.ErrorCode != ErrTimewarpAttack {
t.Fatalf("expected ErrTimewarpAttack but "+
"got: %v", ruleErr.ErrorCode)
}
} else {
t.Fatalf("expected RuleError but got: %T", err)
}
}))
// Test that non-adjustment blocks are not subject to the same check.
t.Run("non_retarget_blocks_not_affected", rapid.MakeCheck(func(t *rapid.T) { //nolint:lll
// Generate any non-retarget block height>
baseHeight := rapid.Int32Range(
0, 1000,
).Draw(t, "base_height") * blocksPerRetarget
offset := rapid.Int32Range(
1, blocksPerRetarget-1,
).Draw(t, "offset")
height := baseHeight + offset
// Generate a reasonable previous block timestamp.
prevTimeUnix := rapid.Int64Range(
1000000, 2000000000,
).Draw(t, "prev_time")
prevBlockTime := time.Unix(prevTimeUnix, 0)
// Generate a test header timestamp that's significantly before
// the previous timestamp Even though this would be rejected
// for retarget blocks, it shouldn't matter here.
timeDiff := rapid.Int64Range(
int64(maxTimeWarp+time.Second),
int64(maxTimeWarp+time.Hour*24*7),
).Draw(t, "warp_amount")
invalidDelta := time.Duration(-timeDiff)
headerTime := prevBlockTime.Add(invalidDelta)
// This should NOT fail for non-retarget blocks, even with
// extreme timewarp.
err := assertNoTimeWarp(
height, blocksPerRetarget, headerTime, prevBlockTime,
)
if err != nil {
t.Fatalf("non-retarget blocks should not be "+
"affected by time warp check, but got: %v", err)
}
}))
}