mirror of
https://github.com/btcsuite/btcd.git
synced 2025-03-13 19:37:39 +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