mirror of
https://github.com/btcsuite/btcd.git
synced 2025-03-13 11:35:52 +01:00
blockchain: add property-based tests for assertNoTimeWarp
Add robust property-based tests for the assertNoTimeWarp function using the rapid testing library. The tests verify the following scenarios: - Basic property tests: - Only retarget blocks (block height divisible by blocksPerRetarget) are checked - Valid timestamps (within maxTimeWarp of previous block) pass validation - Invalid timestamps (too early) fail with appropriate ErrTimewarpAttack - Correct boundary behavior (exactly at maxTimeWarp limit) - Invariant tests: - Function never panics with valid inputs - Non-retarget blocks always return nil regardless of timestamps - Security tests: - All retarget blocks are protected from timewarp attacks - Non-retarget blocks are not affected by the timewarp check
This commit is contained in:
parent
ce2e6af72e
commit
1732501dd9
1 changed files with 344 additions and 0 deletions
344
blockchain/validate_rapid_test.go
Normal file
344
blockchain/validate_rapid_test.go
Normal file
|
@ -0,0 +1,344 @@
|
|||
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)
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
Loading…
Add table
Reference in a new issue