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) } })) }