From 9dfaf3d561c337ff33d6d924a964ef6549f757cd Mon Sep 17 00:00:00 2001 From: "Mr. Z" Date: Wed, 5 Nov 2025 09:09:22 -0500 Subject: [PATCH 01/13] docs(README): correct project name in installation section --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index d6dc81a..3592cdb 100644 --- a/README.md +++ b/README.md @@ -90,7 +90,7 @@ ## 📦 Installation ### Run from source -**alert-system** requires a [supported release of Go](https://golang.org/doc/devel/release.html#policy). +**go-alert-system** requires a [supported release of Go](https://golang.org/doc/devel/release.html#policy). To run the application, clone this repository locally and run: ```shell script @@ -322,4 +322,4 @@ The most basic way to show your support is to star :star2: the project, or to ra ## 📝 License -[![License](https://img.shields.io/github/license/bsv-blockchain/go-alert-system.svg?style=flat&v=1)](LICENSE) +[![License](https://img.shields.io/badge/license-OpenBSV-blue?style=flat&logo=springsecurity&logoColor=white)](LICENSE) From 1ace2ff981712a6f2ba3184c8348934f1c1bdbbf Mon Sep 17 00:00:00 2001 From: "Mr. Z" Date: Wed, 5 Nov 2025 09:09:27 -0500 Subject: [PATCH 02/13] chore(deps): remove repository reference from dependabot config --- .github/dependabot.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 1e45575..3cd5c6c 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,6 +1,5 @@ # ──────────────────────────────────────────────────────────────── # Dependabot Configuration -# Repo: mrz1836/ # # Purpose: # • Keep Go modules, GitHub Actions, DevContainer images/features, and Docker From d2a36204d4f8eb9337d9063010d9d46b843674f9 Mon Sep 17 00:00:00 2001 From: "Mr. Z" Date: Wed, 5 Nov 2025 09:28:09 -0500 Subject: [PATCH 03/13] build(Dockerfile): pin image versions for reproducibility --- Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 6d74322..b17e28f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM galtbv/builder:ubi9 AS builder +FROM galtbv/builder:ubi9@sha256:a4d5adae0cb776574255bf1c326583fcbd95b0275c81572b0283fee90e33bec4 AS builder # Copy in the go src WORKDIR $APP_ROOT/src/github.com/bsv-blockchain/go-alert-system @@ -10,7 +10,7 @@ COPY go.sum go.sum RUN CGO_ENABLED=0 go build -a -o $APP_ROOT/src/go-alert-system github.com/bsv-blockchain/go-alert-system/cmd/go-alert-system # Copy the controller-manager into a thin image -FROM registry.access.redhat.com/ubi9-minimal:9.6 +FROM registry.access.redhat.com/ubi9-minimal:9.6@sha256:a2c5a85865a585c3bc8b10f6c269358fdf89fe32be4232885166889d85c76421 WORKDIR / RUN mkdir /.bitcoin RUN touch /.bitcoin/alert_system_private_key From 3d1f7141d73c34f7a8104a7b4afa3c8cdfce0a11 Mon Sep 17 00:00:00 2001 From: "Mr. Z" Date: Wed, 5 Nov 2025 09:45:45 -0500 Subject: [PATCH 04/13] test(fuzz): add fuzz tests for P2P sync message parsing Introduces fuzz tests for the P2P sync message parsing and serialization to ensure robustness against various input scenarios and edge cases. --- app/p2p/sync_fuzz_test.go | 190 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 190 insertions(+) create mode 100644 app/p2p/sync_fuzz_test.go diff --git a/app/p2p/sync_fuzz_test.go b/app/p2p/sync_fuzz_test.go new file mode 100644 index 0000000..da58ad4 --- /dev/null +++ b/app/p2p/sync_fuzz_test.go @@ -0,0 +1,190 @@ +package p2p + +import ( + "encoding/binary" + "testing" + + "github.com/stretchr/testify/require" +) + +// FuzzNewSyncMessageFromBytes tests P2P sync message parsing +func FuzzNewSyncMessageFromBytes(f *testing.F) { + // Seed with valid IWantLatest message (type only, no sequence) + f.Add([]byte{IWantLatest}) + + // Seed with valid IWantSequenceNumber message (type + sequence + optional data) + wantSeqMsg := []byte{IWantSequenceNumber} + wantSeqMsg = binary.LittleEndian.AppendUint32(wantSeqMsg, 12345) + f.Add(wantSeqMsg) + + // Seed with valid IGotSequenceNumber message + gotSeqMsg := []byte{IGotSequenceNumber} + gotSeqMsg = binary.LittleEndian.AppendUint32(gotSeqMsg, 67890) + gotSeqMsg = append(gotSeqMsg, []byte("alert data here")...) + f.Add(gotSeqMsg) + + // Seed with valid IGotLatest message + gotLatestMsg := []byte{IGotLatest} + gotLatestMsg = binary.LittleEndian.AppendUint32(gotLatestMsg, 99999) + gotLatestMsg = append(gotLatestMsg, []byte("latest alert data")...) + f.Add(gotLatestMsg) + + // Seed with edge cases + f.Add([]byte{}) // empty + f.Add([]byte{0x00}) // unknown type, no sequence + f.Add([]byte{0xFF}) // invalid type + f.Add([]byte{IWantSequenceNumber}) // type without required sequence + f.Add([]byte{IWantSequenceNumber, 0x01}) // type with partial sequence + f.Add([]byte{IWantSequenceNumber, 0x01, 0x02}) // type with partial sequence + f.Add([]byte{IWantSequenceNumber, 0x01, 0x02, 0x03}) // type with partial sequence + + // Seed with minimum valid non-IWantLatest (5 bytes: type + 4 byte sequence) + minMsg := make([]byte, 5) + minMsg[0] = IGotSequenceNumber + binary.LittleEndian.PutUint32(minMsg[1:5], 0) + f.Add(minMsg) + + // Seed with maximum uint32 sequence number + maxSeqMsg := []byte{IGotSequenceNumber} + maxSeqMsg = binary.LittleEndian.AppendUint32(maxSeqMsg, ^uint32(0)) + f.Add(maxSeqMsg) + + // Seed with large data payload + largeMsg := []byte{IGotLatest} + largeMsg = binary.LittleEndian.AppendUint32(largeMsg, 12345) + largeMsg = append(largeMsg, make([]byte, 1000)...) // 1KB data + f.Add(largeMsg) + + f.Fuzz(func(t *testing.T, data []byte) { + // Should never panic + msg, err := NewSyncMessageFromBytes(data) + if err != nil { + // Error is acceptable for invalid input + require.Nil(t, msg, "message should be nil when error is returned") + return + } + + // If no error, validate the parsed message + require.NotNil(t, msg, "message should not be nil when no error") + + // Type should be set + require.GreaterOrEqual(t, len(data), 1, "data should have at least 1 byte for type") + require.Equal(t, data[0], msg.Type, "type should match first byte") + + // If type is IWantLatest, no sequence number is required + if msg.Type == IWantLatest { + require.Equal(t, uint32(0), msg.SequenceNumber, "sequence should be 0 for IWantLatest") + require.Nil(t, msg.Data, "data should be nil for IWantLatest") + return + } + + // For other types, sequence number should be present + require.GreaterOrEqual(t, len(data), 5, "data should have at least 5 bytes for non-IWantLatest types") + + // Validate sequence number is correctly parsed + expectedSeq := binary.LittleEndian.Uint32(data[1:5]) + require.Equal(t, expectedSeq, msg.SequenceNumber, "sequence number should match bytes 1-4") + + // Validate data field + if len(data) > 5 { + require.Equal(t, data[5:], msg.Data, "data should match remaining bytes") + } else { + require.Empty(t, msg.Data, "data should be empty when no extra bytes") + } + }) +} + +// FuzzSyncMessageSerialize tests round-trip serialization consistency +func FuzzSyncMessageSerialize(f *testing.F) { + // Seed with various message types + f.Add(byte(IWantLatest), uint32(0), []byte{}) + f.Add(byte(IWantSequenceNumber), uint32(12345), []byte{}) + f.Add(byte(IGotSequenceNumber), uint32(67890), []byte("alert data")) + f.Add(byte(IGotLatest), uint32(99999), []byte("latest alert")) + f.Add(byte(0x00), uint32(0), []byte{}) + f.Add(byte(0xFF), ^uint32(0), make([]byte, 100)) + + f.Fuzz(func(t *testing.T, msgType byte, seqNum uint32, data []byte) { + // Create sync message + msg := &SyncMessage{ + Type: msgType, + SequenceNumber: seqNum, + Data: data, + } + + // Serialize should never panic + serialized := msg.Serialize() + + // Validate serialization + require.NotNil(t, serialized, "serialization should produce output") + require.GreaterOrEqual(t, len(serialized), 5, "serialized message should have at least 5 bytes") + + // Validate format: type(1) + sequence(4) + data + require.Equal(t, msgType, serialized[0], "first byte should be type") + parsedSeq := binary.LittleEndian.Uint32(serialized[1:5]) + require.Equal(t, seqNum, parsedSeq, "bytes 1-4 should be sequence number") + + if len(data) > 0 { + require.Equal(t, data, serialized[5:], "remaining bytes should be data") + } + + // Test round-trip consistency (serialize → deserialize) + // Only test if the message type is IWantLatest or has enough data + if msgType == IWantLatest { + // For IWantLatest, we can deserialize from just the type byte + deserialized, err := NewSyncMessageFromBytes([]byte{msgType}) + if err == nil { + require.Equal(t, msgType, deserialized.Type, "deserialized type should match") + } + } else { + // For other types, deserialize from full serialized data + deserialized, err := NewSyncMessageFromBytes(serialized) + if err == nil { + require.Equal(t, msgType, deserialized.Type, "deserialized type should match") + require.Equal(t, seqNum, deserialized.SequenceNumber, "deserialized sequence should match") + require.Equal(t, data, deserialized.Data, "deserialized data should match") + } + } + }) +} + +// FuzzSyncMessageTypes tests handling of different message type values +func FuzzSyncMessageTypes(f *testing.F) { + // Seed with known message types + f.Add(byte(IWantLatest)) + f.Add(byte(IWantSequenceNumber)) + f.Add(byte(IGotSequenceNumber)) + f.Add(byte(IGotLatest)) + + // Seed with boundary values + f.Add(byte(0x00)) + f.Add(byte(0xFF)) + f.Add(byte(0x7F)) + f.Add(byte(0x80)) + + f.Fuzz(func(t *testing.T, msgType byte) { + // Test with just the type byte + msg1, err1 := NewSyncMessageFromBytes([]byte{msgType}) + + // IWantLatest (0x01) should succeed with just 1 byte + if msgType == IWantLatest { + require.NoError(t, err1, "IWantLatest should parse with just 1 byte") + require.NotNil(t, msg1, "message should not be nil for IWantLatest") + require.Equal(t, msgType, msg1.Type, "type should match") + return + } + + // All other types should fail with just 1 byte (need 5 bytes minimum) + require.Error(t, err1, "non-IWantLatest types should fail with just 1 byte") + require.Nil(t, msg1, "message should be nil on error") + + // Test with full 5 byte message + fullMsg := []byte{msgType, 0x01, 0x02, 0x03, 0x04} + msg2, err2 := NewSyncMessageFromBytes(fullMsg) + + // Should succeed with 5 bytes regardless of type + require.NoError(t, err2, "should parse with 5 bytes") + require.NotNil(t, msg2, "message should not be nil with 5 bytes") + require.Equal(t, msgType, msg2.Type, "type should match") + }) +} From 83be8a93dac412ebd8e8c638d2fe2d042cb52d0b Mon Sep 17 00:00:00 2001 From: "Mr. Z" Date: Wed, 5 Nov 2025 09:45:51 -0500 Subject: [PATCH 05/13] test(fuzz): add fuzz tests for bitcoin.conf parsing --- app/config/load_fuzz_test.go | 244 +++++++++++++++++++++++++++++++++++ 1 file changed, 244 insertions(+) create mode 100644 app/config/load_fuzz_test.go diff --git a/app/config/load_fuzz_test.go b/app/config/load_fuzz_test.go new file mode 100644 index 0000000..ba8cb12 --- /dev/null +++ b/app/config/load_fuzz_test.go @@ -0,0 +1,244 @@ +package config + +import ( + "bufio" + "bytes" + "strings" + "testing" + + "github.com/stretchr/testify/require" +) + +// FuzzSplitFunc tests the bitcoin.conf line splitting function +func FuzzSplitFunc(f *testing.F) { + // Seed with valid config lines + f.Add([]byte("rpcuser=bitcoin\nrpcpassword=secret\n"), false) + f.Add([]byte("rpcconnect=127.0.0.1\n"), false) + f.Add([]byte("rpcport=8332\n"), false) + + // Seed with edge cases + f.Add([]byte(""), false) // empty + f.Add([]byte(""), true) // empty at EOF + f.Add([]byte("key=value"), true) // no newline at EOF + f.Add([]byte("key=value\n"), false) // with newline + f.Add([]byte("\n"), false) // just newline + f.Add([]byte("===\n"), false) // multiple delimiters + f.Add([]byte("nodelimiter"), false) // no '=' or '\n' + f.Add([]byte("key=value\nkey2=value2\n"), false) // multiple lines + + // Seed with special characters + f.Add([]byte("key=value with spaces\n"), false) + f.Add([]byte("key=\n"), false) // empty value + f.Add([]byte("=value\n"), false) // empty key + f.Add([]byte("key==value\n"), false) // double delimiter + + f.Fuzz(func(t *testing.T, data []byte, atEOF bool) { + // Should never panic + advance, token, err := splitFunc(data, atEOF) + + // Validate return values are consistent + require.GreaterOrEqual(t, advance, 0, "advance should be non-negative") + require.LessOrEqual(t, advance, len(data), "advance should not exceed data length") + + if err != nil { + // Error is acceptable + return + } + + // If token is returned, it should not exceed data length + if token != nil { + require.LessOrEqual(t, len(token), len(data), "token should not exceed data length") + } + + // If atEOF is true and data is empty, should return 0, nil, nil + if atEOF && len(data) == 0 { + require.Equal(t, 0, advance, "advance should be 0 for empty data at EOF") + require.Nil(t, token, "token should be nil for empty data at EOF") + require.NoError(t, err, "error should be nil for empty data at EOF") + } + + // If atEOF is true and data is not empty, should return all data + if atEOF && len(data) > 0 { + require.Equal(t, len(data), advance, "should advance by data length at EOF") + require.Equal(t, data, token, "should return all data at EOF") + } + }) +} + +// FuzzBitcoinConfParsing tests bitcoin.conf parsing logic +func FuzzBitcoinConfParsing(f *testing.F) { + // Seed with valid bitcoin.conf content + validConf := `rpcuser=bitcoin +rpcpassword=secretpassword123 +rpcconnect=127.0.0.1 +rpcport=8332 +` + f.Add([]byte(validConf)) + + // Seed with various formats + f.Add([]byte("rpcuser=test\nrpcpassword=pass\n")) + f.Add([]byte("key=value\n")) + f.Add([]byte("key=\n")) + f.Add([]byte("=value\n")) + f.Add([]byte("key\n")) + f.Add([]byte("")) + f.Add([]byte("\n")) + f.Add([]byte("===\n")) + + // Seed with malformed content + f.Add([]byte("rpcuser=test\nnodelimiter\nrpcpassword=pass\n")) + f.Add([]byte("key==value\n")) + f.Add([]byte("key=value=extra\n")) + + // Seed with special characters + f.Add([]byte("rpcuser=user@domain\nrpcpassword=p@ss!#$%\n")) + f.Add([]byte("rpcuser=user with spaces\n")) + f.Add([]byte("# comment line\nrpcuser=test\n")) + + // Seed with different line endings + f.Add([]byte("key=value\r\n")) + f.Add([]byte("key=value\r")) + + f.Fuzz(func(t *testing.T, data []byte) { + // Create a scanner with the custom split function + scanner := bufio.NewScanner(bytes.NewReader(data)) + scanner.Split(splitFunc) + + // Parse the config content + confValues := map[string]string{} + lineCount := 0 + + // Should never panic during scanning + for scanner.Scan() { + lineCount++ + kv := scanner.Text() + keyValue := strings.Split(kv, "=") + + // Skip lines that don't have exactly 2 parts + if len(keyValue) != 2 { + continue + } + + confValues[keyValue[0]] = keyValue[1] + } + + // Validate scanner completed without panic + err := scanner.Err() + if err != nil { + // Scanner errors are acceptable for invalid input + return + } + + // Validate keys and values don't contain newlines + for key, value := range confValues { + require.NotContains(t, key, "\n", "keys should not contain newlines") + require.NotContains(t, value, "\n", "values should not contain newlines") + } + }) +} + +// FuzzConfigKeyValueParsing tests key=value parsing logic +func FuzzConfigKeyValueParsing(f *testing.F) { + // Seed with valid key=value pairs + f.Add("rpcuser=bitcoin") + f.Add("rpcpassword=secret123") + f.Add("rpcconnect=127.0.0.1") + f.Add("rpcport=8332") + + // Seed with edge cases + f.Add("") + f.Add("=") + f.Add("key=") + f.Add("=value") + f.Add("key") + f.Add("key=value=extra") + f.Add("===") + f.Add("key==value") + + // Seed with special characters + f.Add("key=value with spaces") + f.Add("key with spaces=value") + f.Add("key=@#$%^&*()") + f.Add("user@domain=pass!#$") + + f.Fuzz(func(t *testing.T, kv string) { + // Parse key=value pair + keyValue := strings.Split(kv, "=") + + // Validate split result + require.NotNil(t, keyValue, "split should always return a slice") + require.GreaterOrEqual(t, len(keyValue), 1, "split should return at least one element") + + // If exactly 2 parts, validate they form a valid key-value pair + if len(keyValue) == 2 { + key := keyValue[0] + value := keyValue[1] + + // Keys and values can be empty, but shouldn't cause issues + _ = key + _ = value + + // Test that we can store in a map + testMap := map[string]string{} + testMap[key] = value + + require.Equal(t, value, testMap[key], "value should be retrievable from map") + } + + // If not exactly 2 parts, it should be skipped (as per the actual code logic) + if len(keyValue) != 2 { + // This is valid behavior - malformed lines are skipped + require.NotEqual(t, 2, len(keyValue), "should skip lines that don't have exactly 2 parts") + } + }) +} + +// FuzzHostPortParsing tests host:port parsing logic +func FuzzHostPortParsing(f *testing.F) { + // Seed with valid host:port combinations + f.Add("http://127.0.0.1:8332") + f.Add("https://localhost:8332") + f.Add("http://192.168.1.1:18332") + f.Add("https://node.example.com:8332") + + // Seed with edge cases + f.Add("") + f.Add(":") + f.Add("127.0.0.1") + f.Add(":8332") + f.Add("http://") + f.Add("https://") + f.Add("http://127.0.0.1") + f.Add("127.0.0.1:8332") + + // Seed with malformed input + f.Add("not-a-url") + f.Add("http://localhost:not-a-port") + f.Add("http://[::1]:8332") + f.Add("http://localhost:8332:extra") + + f.Fuzz(func(t *testing.T, hostPort string) { + // Simulate the trimming logic from loadBitcoinConfiguration + trimmed := strings.TrimPrefix(hostPort, "http://") + trimmed = strings.TrimPrefix(trimmed, "https://") + + // Should never panic + parts := strings.Split(trimmed, ":") + + // Validate split result + require.NotNil(t, parts, "split should always return a slice") + require.GreaterOrEqual(t, len(parts), 1, "split should return at least one element") + + // If we have at least one part, it could be a valid host + if len(parts) >= 1 { + host := parts[0] + _ = host // host can be any string + } + + // If we have at least two parts, second part could be a port + if len(parts) >= 2 { + port := parts[1] + _ = port // port can be any string (validation happens elsewhere) + } + }) +} From 2b21e53cc1242a67ea6e289e48694019e25bfdda Mon Sep 17 00:00:00 2001 From: "Mr. Z" Date: Wed, 5 Nov 2025 09:45:56 -0500 Subject: [PATCH 06/13] test(fuzz): add fuzz tests for alert message handling --- app/models/alert_message_fuzz_test.go | 181 ++++++++++++++++++++++++++ 1 file changed, 181 insertions(+) create mode 100644 app/models/alert_message_fuzz_test.go diff --git a/app/models/alert_message_fuzz_test.go b/app/models/alert_message_fuzz_test.go new file mode 100644 index 0000000..1cb9706 --- /dev/null +++ b/app/models/alert_message_fuzz_test.go @@ -0,0 +1,181 @@ +package models + +import ( + "encoding/binary" + "encoding/hex" + "testing" + + "github.com/stretchr/testify/require" +) + +// FuzzNewAlertFromBytes tests NewAlertFromBytes with arbitrary byte inputs +func FuzzNewAlertFromBytes(f *testing.F) { + // Seed with valid alert message structure + // Format: version(4) + sequence(4) + timestamp(8) + alertType(4) + message + signatures(195) + validAlert := make([]byte, 0) + validAlert = binary.LittleEndian.AppendUint32(validAlert, 1) // version + validAlert = binary.LittleEndian.AppendUint32(validAlert, 1) // sequence + validAlert = binary.LittleEndian.AppendUint64(validAlert, 1234567890) // timestamp + validAlert = binary.LittleEndian.AppendUint32(validAlert, uint32(AlertTypeInformational)) // alert type + validAlert = append(validAlert, []byte("test message")...) // message + validAlert = append(validAlert, make([]byte, 195)...) // 3 signatures (65 bytes each) + + f.Add(validAlert) + + // Seed with minimal valid length (16 bytes header + 2 bytes message + 195 signatures) + minimalAlert := make([]byte, 0) + minimalAlert = binary.LittleEndian.AppendUint32(minimalAlert, 1) + minimalAlert = binary.LittleEndian.AppendUint32(minimalAlert, 1) + minimalAlert = binary.LittleEndian.AppendUint64(minimalAlert, 0) + minimalAlert = binary.LittleEndian.AppendUint32(minimalAlert, uint32(AlertTypeInformational)) + minimalAlert = append(minimalAlert, []byte("ab")...) // 2 byte message + minimalAlert = append(minimalAlert, make([]byte, 195)...) // signatures + + f.Add(minimalAlert) + + // Seed with different alert types + for _, alertType := range []AlertType{ + AlertTypeInformational, + AlertTypeFreezeUtxo, + AlertTypeUnfreezeUtxo, + AlertTypeConfiscateUtxo, + AlertTypeBanPeer, + AlertTypeUnbanPeer, + AlertTypeInvalidateBlock, + AlertTypeSetKeys, + } { + typeAlert := make([]byte, 0) + typeAlert = binary.LittleEndian.AppendUint32(typeAlert, 1) + typeAlert = binary.LittleEndian.AppendUint32(typeAlert, 1) + typeAlert = binary.LittleEndian.AppendUint64(typeAlert, 0) + typeAlert = binary.LittleEndian.AppendUint32(typeAlert, uint32(alertType)) + typeAlert = append(typeAlert, []byte("test")...) + typeAlert = append(typeAlert, make([]byte, 195)...) + f.Add(typeAlert) + } + + // Seed with special alert type 99 (uses 128 byte signature) + specialAlert := make([]byte, 0) + specialAlert = binary.LittleEndian.AppendUint32(specialAlert, 1) + specialAlert = binary.LittleEndian.AppendUint32(specialAlert, 1) + specialAlert = binary.LittleEndian.AppendUint64(specialAlert, 0) + specialAlert = binary.LittleEndian.AppendUint32(specialAlert, 99) + specialAlert = append(specialAlert, []byte("test")...) + specialAlert = append(specialAlert, make([]byte, 128)...) + f.Add(specialAlert) + + // Seed with edge cases + f.Add([]byte{}) // empty + f.Add([]byte{0}) // single byte + f.Add(make([]byte, 19)) // just under minimum header (20 bytes) + f.Add(make([]byte, 20)) // minimum header only (no message/signatures) + f.Add(make([]byte, 217)) // minimum valid total (20 header + 2 message + 195 sig) + f.Add(make([]byte, 1000)) // large message + + f.Fuzz(func(t *testing.T, data []byte) { + // The function should never panic, regardless of input + alert, err := NewAlertFromBytes(data) + if err != nil { + // Error is acceptable - just ensure it's one of the expected errors + require.Nil(t, alert, "alert should be nil when error is returned") + return + } + + // If no error, validate the alert was created properly + require.NotNil(t, alert, "alert should not be nil when no error") + require.NotNil(t, alert.GetRawMessage(), "raw message should be set") + // The raw message length is validated in ReadRaw(), so if we got here it's valid + }) +} + +// FuzzAlertMessageReadRaw tests the ReadRaw method with arbitrary inputs +func FuzzAlertMessageReadRaw(f *testing.F) { + // Seed with valid hex-encoded alert + validAlert := make([]byte, 0) + validAlert = binary.LittleEndian.AppendUint32(validAlert, 1) + validAlert = binary.LittleEndian.AppendUint32(validAlert, 1) + validAlert = binary.LittleEndian.AppendUint64(validAlert, 1234567890) + validAlert = binary.LittleEndian.AppendUint32(validAlert, uint32(AlertTypeInformational)) + validAlert = append(validAlert, []byte("test message")...) + validAlert = append(validAlert, make([]byte, 195)...) + f.Add(hex.EncodeToString(validAlert)) + + // Seed with edge cases + f.Add("") // empty string + f.Add("00") // single byte hex + f.Add("not-valid-hex") // invalid hex + f.Add(hex.EncodeToString(make([]byte, 15))) // under minimum + f.Add(hex.EncodeToString(make([]byte, 16))) // minimum header + f.Add(hex.EncodeToString(make([]byte, 217))) // minimum valid + + f.Fuzz(func(t *testing.T, rawHex string) { + // Create alert with raw hex string + alert := NewAlertMessage() + alert.Raw = rawHex + + // The function should never panic + err := alert.ReadRaw() + if err != nil { + // Error is acceptable - validate the alert state remains consistent + require.NotNil(t, alert, "alert should not be nil even on error") + return + } + + // If no error, validate parsing succeeded + require.NotNil(t, alert.GetRawMessage(), "raw message should be set after successful parse") + // Sequence number can be any uint32 value including 0 + require.NotEmpty(t, alert.Hash, "hash should be computed") + }) +} + +// FuzzAlertMessageSerialize tests round-trip serialization consistency +func FuzzAlertMessageSerialize(f *testing.F) { + // Seed with valid alert components + f.Add(uint32(1), uint32(1), uint64(1234567890), uint32(AlertTypeInformational), []byte("test")) + + f.Fuzz(func(t *testing.T, version, sequence uint32, timestamp uint64, alertType uint32, message []byte) { + // Create alert + alert := NewAlertMessage() + alert.SetVersion(version) + alert.SequenceNumber = sequence + alert.SetTimestamp(timestamp) + + // Only test valid alert types to avoid nil pointer in ProcessAlertMessage + validAlertTypes := []AlertType{ + AlertTypeInformational, + AlertTypeFreezeUtxo, + AlertTypeUnfreezeUtxo, + AlertTypeConfiscateUtxo, + AlertTypeBanPeer, + AlertTypeUnbanPeer, + AlertTypeInvalidateBlock, + AlertTypeSetKeys, + } + + // Map the fuzzed uint32 to a valid alert type + // Safe conversion: len always returns non-negative int, and we have a fixed small array + numTypes := len(validAlertTypes) + if numTypes > 0 { + alert.SetAlertType(validAlertTypes[int(alertType)%numTypes]) + } + alert.SetRawMessage(message) + + // Add dummy signatures (3 x 65 bytes) + alert.SetSignatures([][]byte{ + make([]byte, 65), + make([]byte, 65), + make([]byte, 65), + }) + + // Serialize should never panic + serialized := alert.Serialize() + + // Validate serialization produced output + require.NotNil(t, serialized, "serialization should produce output") + require.NotEmpty(t, alert.Raw, "Raw field should be set after serialization") + require.NotEmpty(t, alert.Hash, "Hash should be computed after serialization") + + // Validate the serialized data structure + require.GreaterOrEqual(t, len(serialized), 20, "serialized data should include header") + }) +} From 9204bf6743c20a10ed81f3b0e4c169d7ff573f9a Mon Sep 17 00:00:00 2001 From: "Mr. Z" Date: Wed, 5 Nov 2025 09:46:01 -0500 Subject: [PATCH 07/13] fix(alert): validate alert length before processing --- app/models/alert_message_invalidate_block.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/models/alert_message_invalidate_block.go b/app/models/alert_message_invalidate_block.go index 9c77515..cbfe70e 100644 --- a/app/models/alert_message_invalidate_block.go +++ b/app/models/alert_message_invalidate_block.go @@ -21,6 +21,10 @@ type AlertMessageInvalidateBlock struct { // Read reads the alert func (a *AlertMessageInvalidateBlock) Read(alert []byte) error { + if len(alert) < 32 { + return fmt.Errorf("%w: need at least 32 bytes for block hash, got %d", ErrAlertTooShort, len(alert)) + } + blockHash, err := chainhash.NewHash(alert[:32]) if err != nil { return err From 6c252a5d4eb8f0f7c257a61e1d373792660825d1 Mon Sep 17 00:00:00 2001 From: "Mr. Z" Date: Wed, 5 Nov 2025 09:46:06 -0500 Subject: [PATCH 08/13] test(fuzz): add fuzz tests for alert message parsing --- app/models/alert_message_types_fuzz_test.go | 387 ++++++++++++++++++++ 1 file changed, 387 insertions(+) create mode 100644 app/models/alert_message_types_fuzz_test.go diff --git a/app/models/alert_message_types_fuzz_test.go b/app/models/alert_message_types_fuzz_test.go new file mode 100644 index 0000000..039e011 --- /dev/null +++ b/app/models/alert_message_types_fuzz_test.go @@ -0,0 +1,387 @@ +package models + +import ( + "encoding/binary" + "testing" + + "github.com/bsv-blockchain/go-sdk/util" + "github.com/stretchr/testify/require" +) + +// FuzzAlertMessageBanPeerRead tests ban peer alert parsing +func FuzzAlertMessageBanPeerRead(f *testing.F) { + // Seed with valid ban peer message: VarInt(peerLen) + peer + VarInt(reasonLen) + reason + validMsg := make([]byte, 0) + peerData := []byte("192.168.1.1:8333") + reasonData := []byte("malicious behavior") + + // Add peer length and data + w := util.NewWriter() + w.WriteVarInt(uint64(len(peerData))) + validMsg = append(validMsg, w.Buf...) + validMsg = append(validMsg, peerData...) + + // Add reason length and data + w = util.NewWriter() + w.WriteVarInt(uint64(len(reasonData))) + validMsg = append(validMsg, w.Buf...) + validMsg = append(validMsg, reasonData...) + + f.Add(validMsg) + + // Seed with edge cases + f.Add([]byte{}) // empty + f.Add([]byte{0x00}) // zero length peer + f.Add([]byte{0x01, 0x41}) // 1 byte peer 'A' + f.Add([]byte{0x01, 0x41, 0x00}) // peer + zero reason + + // Seed with large values + largeMsg := make([]byte, 0) + w = util.NewWriter() + w.WriteVarInt(100) + largeMsg = append(largeMsg, w.Buf...) + largeMsg = append(largeMsg, make([]byte, 100)...) // 100 byte peer + w = util.NewWriter() + w.WriteVarInt(100) + largeMsg = append(largeMsg, w.Buf...) + largeMsg = append(largeMsg, make([]byte, 100)...) // 100 byte reason + f.Add(largeMsg) + + f.Fuzz(func(t *testing.T, data []byte) { + alert := &AlertMessageBanPeer{} + + // Should never panic + err := alert.Read(data) + if err != nil { + // Error is acceptable + return + } + + // If successful, validate the parsed data + require.LessOrEqual(t, alert.PeerLength, uint64(len(data)), "peer length should not exceed data length") + require.LessOrEqual(t, alert.ReasonLength, uint64(len(data)), "reason length should not exceed data length") + require.Equal(t, alert.PeerLength, uint64(len(alert.Peer)), "peer length should match peer data") + require.Equal(t, alert.ReasonLength, uint64(len(alert.Reason)), "reason length should match reason data") + }) +} + +// FuzzAlertMessageUnbanPeerRead tests unban peer alert parsing +func FuzzAlertMessageUnbanPeerRead(f *testing.F) { + // Seed with valid unban peer message (same structure as ban) + validMsg := make([]byte, 0) + peerData := []byte("192.168.1.1:8333") + reasonData := []byte("false positive") + + w := util.NewWriter() + w.WriteVarInt(uint64(len(peerData))) + validMsg = append(validMsg, w.Buf...) + validMsg = append(validMsg, peerData...) + + w = util.NewWriter() + w.WriteVarInt(uint64(len(reasonData))) + validMsg = append(validMsg, w.Buf...) + validMsg = append(validMsg, reasonData...) + + f.Add(validMsg) + + // Edge cases + f.Add([]byte{}) + f.Add([]byte{0x00}) + f.Add([]byte{0x01, 0x41}) + f.Add([]byte{0x01, 0x41, 0x00}) + + f.Fuzz(func(t *testing.T, data []byte) { + alert := &AlertMessageUnbanPeer{} + + // Should never panic + err := alert.Read(data) + if err != nil { + return + } + + // Validate parsed data + require.LessOrEqual(t, alert.PeerLength, uint64(len(data)), "peer length should not exceed data length") + require.LessOrEqual(t, alert.ReasonLength, uint64(len(data)), "reason length should not exceed data length") + }) +} + +// FuzzAlertMessageInformationalRead tests informational alert parsing +func FuzzAlertMessageInformationalRead(f *testing.F) { + // Seed with valid informational message + validMsg := make([]byte, 0) + message := []byte("System maintenance scheduled") + + w := util.NewWriter() + w.WriteVarInt(uint64(len(message))) + validMsg = append(validMsg, w.Buf...) + validMsg = append(validMsg, message...) + + f.Add(validMsg) + + // Edge cases + f.Add([]byte{}) // empty + f.Add([]byte{0x00}) // zero length message + f.Add([]byte{0x01, 0x41}) // single character 'A' + + // Invalid: length exceeds data + invalidLen := make([]byte, 0) + w = util.NewWriter() + w.WriteVarInt(100) // claim 100 bytes + invalidLen = append(invalidLen, w.Buf...) + invalidLen = append(invalidLen, []byte{0x01}...) // but only 1 byte + f.Add(invalidLen) + + // Valid with extra bytes (should trigger IsComplete check) + extraBytes := make([]byte, 0) + w = util.NewWriter() + w.WriteVarInt(5) + extraBytes = append(extraBytes, w.Buf...) + extraBytes = append(extraBytes, []byte("hello")...) + extraBytes = append(extraBytes, []byte("extra")...) // extra bytes + f.Add(extraBytes) + + f.Fuzz(func(t *testing.T, data []byte) { + alert := &AlertMessageInformational{} + + // Should never panic + err := alert.Read(data) + if err != nil { + return + } + + // Validate successful parse + require.Equal(t, alert.MessageLength, uint64(len(alert.Message)), "message length should match message data") + require.LessOrEqual(t, alert.MessageLength, uint64(len(data)), "message length should not exceed input data") + }) +} + +// FuzzAlertMessageFreezeUtxoRead tests freeze UTXO alert parsing +func FuzzAlertMessageFreezeUtxoRead(f *testing.F) { + // Seed with valid freeze message (57 bytes per fund) + validMsg := make([]byte, 57) + // txid (32 bytes) + vout (8) + start height (8) + end height (8) + expire flag (1) = 57 + copy(validMsg[0:32], make([]byte, 32)) // txid + binary.LittleEndian.PutUint64(validMsg[32:40], 0) // vout + binary.LittleEndian.PutUint64(validMsg[40:48], 100000) // start height + binary.LittleEndian.PutUint64(validMsg[48:56], 200000) // end height + validMsg[56] = 1 // expire flag + + f.Add(validMsg) + + // Multiple funds (114 bytes = 2 funds) + multipleMsg := make([]byte, 114) + copy(multipleMsg[0:57], validMsg) + copy(multipleMsg[57:114], validMsg) + f.Add(multipleMsg) + + // Edge cases + f.Add([]byte{}) // empty + f.Add(make([]byte, 56)) // one byte short + f.Add(make([]byte, 58)) // one byte over (not divisible by 57) + f.Add(make([]byte, 57)) // minimum valid (all zeros) + f.Add(make([]byte, 171)) // 3 funds + + // Test with max int values to trigger overflow check + overflowMsg := make([]byte, 57) + copy(overflowMsg[0:32], make([]byte, 32)) + binary.LittleEndian.PutUint64(overflowMsg[32:40], ^uint64(0)) // max uint64 for vout + binary.LittleEndian.PutUint64(overflowMsg[40:48], 100000) + binary.LittleEndian.PutUint64(overflowMsg[48:56], 200000) + overflowMsg[56] = 0 + f.Add(overflowMsg) + + f.Fuzz(func(t *testing.T, data []byte) { + alert := &AlertMessageFreezeUtxo{} + + // Should never panic + err := alert.Read(data) + if err != nil { + return + } + + // Validate successful parse + expectedFunds := len(data) / 57 + require.Len(t, alert.Funds, expectedFunds, "number of funds should match data length / 57") + + // Validate no overflow occurred + for _, fund := range alert.Funds { + require.GreaterOrEqual(t, fund.TxOut.Vout, 0, "vout should be non-negative") + require.LessOrEqual(t, fund.EnforceAtHeight[0].Start, int(^uint(0)>>1), "start height should not overflow int") + require.LessOrEqual(t, fund.EnforceAtHeight[0].Stop, int(^uint(0)>>1), "end height should not overflow int") + } + }) +} + +// FuzzAlertMessageUnfreezeUtxoRead tests unfreeze UTXO alert parsing +func FuzzAlertMessageUnfreezeUtxoRead(f *testing.F) { + // Same structure as freeze UTXO (57 bytes per fund) + validMsg := make([]byte, 57) + copy(validMsg[0:32], make([]byte, 32)) + binary.LittleEndian.PutUint64(validMsg[32:40], 0) + binary.LittleEndian.PutUint64(validMsg[40:48], 100000) + binary.LittleEndian.PutUint64(validMsg[48:56], 200000) + validMsg[56] = 0 + + f.Add(validMsg) + f.Add([]byte{}) + f.Add(make([]byte, 56)) + f.Add(make([]byte, 58)) + f.Add(make([]byte, 114)) + + f.Fuzz(func(t *testing.T, data []byte) { + alert := &AlertMessageUnfreezeUtxo{} + + // Should never panic + err := alert.Read(data) + if err != nil { + return + } + + // Validate successful parse + expectedFunds := len(data) / 57 + require.Len(t, alert.Funds, expectedFunds, "number of funds should match data length / 57") + }) +} + +// FuzzAlertMessageConfiscateTransactionRead tests confiscate transaction alert parsing +func FuzzAlertMessageConfiscateTransactionRead(f *testing.F) { + // Seed with valid confiscation message: height(8) + VarInt(hexLen) + hex + validMsg := make([]byte, 0) + heightBytes := make([]byte, 8) + binary.LittleEndian.PutUint64(heightBytes[0:8], 100000) // enforce at height + validMsg = append(validMsg, heightBytes...) + + txHex := []byte("0100000001...") + w := util.NewWriter() + w.WriteVarInt(uint64(len(txHex))) + validMsg = append(validMsg, w.Buf...) + validMsg = append(validMsg, txHex...) + + f.Add(validMsg) + + // Edge cases + f.Add([]byte{}) // empty + f.Add(make([]byte, 8)) // only height, no tx + f.Add(make([]byte, 9)) // height + varint start + + // Minimum valid: height + zero-length tx + minMsgHeight := make([]byte, 8) + binary.LittleEndian.PutUint64(minMsgHeight[0:8], 0) + minMsg := append([]byte(nil), minMsgHeight...) + minMsg = append(minMsg, 0x00) // zero length varint + f.Add(minMsg) + + // Test max int64 overflow + overflowMsgHeight := make([]byte, 8) + binary.LittleEndian.PutUint64(overflowMsgHeight[0:8], ^uint64(0)) // max uint64 + overflowMsg := append([]byte(nil), overflowMsgHeight...) + overflowMsg = append(overflowMsg, 0x00) + f.Add(overflowMsg) + + // Length exceeds data + badLenMsgHeight := make([]byte, 8) + binary.LittleEndian.PutUint64(badLenMsgHeight[0:8], 100000) + w = util.NewWriter() + w.WriteVarInt(100) // claim 100 bytes + badLenMsg := append([]byte(nil), badLenMsgHeight...) + badLenMsg = append(badLenMsg, w.Buf...) + badLenMsg = append(badLenMsg, []byte{0x01}...) // but only 1 byte + f.Add(badLenMsg) + + f.Fuzz(func(t *testing.T, data []byte) { + alert := &AlertMessageConfiscateTransaction{} + + // Should never panic + err := alert.Read(data) + if err != nil { + return + } + + // Validate successful parse + require.Len(t, alert.Transactions, 1, "should parse exactly one transaction") + require.GreaterOrEqual(t, alert.Transactions[0].ConfiscationTransaction.EnforceAtHeight, int64(0), "height should be non-negative") + // Hex can be empty (zero-length transaction is valid in the parser) + }) +} + +// FuzzAlertMessageInvalidateBlockRead tests invalidate block alert parsing +func FuzzAlertMessageInvalidateBlockRead(f *testing.F) { + // Seed with valid invalidate block message: blockHash(32) + VarInt(reasonLen) + reason + blockHashBytes := make([]byte, 32) + // Fill with a sample block hash + copy(blockHashBytes[0:32], []byte("blockhash123456789012345678901")) + validMsg := append([]byte(nil), blockHashBytes...) + + reason := []byte("invalid proof of work") + w := util.NewWriter() + w.WriteVarInt(uint64(len(reason))) + validMsg = append(validMsg, w.Buf...) + validMsg = append(validMsg, reason...) + + f.Add(validMsg) + + // Edge cases + f.Add([]byte{}) // empty + f.Add(make([]byte, 31)) // hash too short + f.Add(make([]byte, 32)) // only hash + f.Add(make([]byte, 33)) // hash + varint start + + // Minimum valid: hash + zero reason + minMsgHash := make([]byte, 32) + minMsgInvalidate := append([]byte(nil), minMsgHash...) + minMsgInvalidate = append(minMsgInvalidate, 0x00) // zero length reason + f.Add(minMsgInvalidate) + + f.Fuzz(func(t *testing.T, data []byte) { + alert := &AlertMessageInvalidateBlock{} + + // Should never panic + err := alert.Read(data) + if err != nil { + return + } + + // Validate successful parse + require.Len(t, alert.BlockHash, 32, "block hash should be 32 bytes") + require.Equal(t, alert.ReasonLength, uint64(len(alert.Reason)), "reason length should match reason data") + }) +} + +// FuzzAlertMessageSetKeysRead tests set keys alert parsing +func FuzzAlertMessageSetKeysRead(f *testing.F) { + // Seed with valid set keys message: exactly 165 bytes (5 keys × 33 bytes) + validMsg := make([]byte, 165) + // Fill with sample public keys (33 bytes each) + for i := 0; i < 5; i++ { + validMsg[i*33] = 0x02 // compressed public key prefix + for j := 1; j < 33; j++ { + validMsg[i*33+j] = byte(i + j) + } + } + + f.Add(validMsg) + + // Edge cases + f.Add([]byte{}) // empty + f.Add(make([]byte, 164)) // one byte short + f.Add(make([]byte, 165)) // exact length (all zeros) + f.Add(make([]byte, 166)) // one byte over + f.Add(make([]byte, 33)) // single key + f.Add(make([]byte, 100)) // arbitrary length + + f.Fuzz(func(t *testing.T, data []byte) { + alert := &AlertMessageSetKeys{} + + // Should never panic + err := alert.Read(data) + if err != nil { + return + } + + // Validate successful parse + require.Len(t, alert.Keys, 5, "should parse exactly 5 keys") + for _, key := range alert.Keys { + require.Len(t, key, 33, "each key should be 33 bytes") + } + }) +} From 9b49b4292c0d047108f0b8487950b313125580d5 Mon Sep 17 00:00:00 2001 From: "Mr. Z" Date: Wed, 5 Nov 2025 09:46:26 -0500 Subject: [PATCH 09/13] fix(load): return advance, token, and err in splitFunc Ensure proper return values from splitFunc for correct functionality. --- app/config/load.go | 2 +- app/models/model/model_internals.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/config/load.go b/app/config/load.go index d1ec82e..d3eda38 100644 --- a/app/config/load.go +++ b/app/config/load.go @@ -327,7 +327,7 @@ func splitFunc(data []byte, atEOF bool) (advance int, token []byte, err error) { // skip the delimiter in advancing to the next pair return i + 1, data[0:i], nil } - return + return advance, token, err } // CloseAll will close all connections to all services diff --git a/app/models/model/model_internals.go b/app/models/model/model_internals.go index f5ec5a4..891f8b9 100644 --- a/app/models/model/model_internals.go +++ b/app/models/model/model_internals.go @@ -114,7 +114,7 @@ func (m *Model) GetOptions(isNewRecord bool) (opts []Options) { opts = append(opts, New()) } - return + return opts } // IsNew returns true if the model is (or was) a new record From 1bc273c6d9b700e7a73780fa910dcfb43ee60013 Mon Sep 17 00:00:00 2001 From: "Mr. Z" Date: Wed, 5 Nov 2025 09:46:35 -0500 Subject: [PATCH 10/13] fix(alert): update minimum raw message length check --- app/models/alert_message.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/models/alert_message.go b/app/models/alert_message.go index 2100dee..2ad778a 100644 --- a/app/models/alert_message.go +++ b/app/models/alert_message.go @@ -272,8 +272,8 @@ func (m *AlertMessage) ReadRaw() error { m.SetRawMessage(ak) } - if len(m.GetRawMessage()) < 16 { - // todo DETERMINE ACTUAL PROPER LENGTH + if len(m.GetRawMessage()) < 20 { + // Minimum length: version(4) + sequence(4) + timestamp(8) + alertType(4) = 20 bytes return ErrAlertTooShort } ak := m.GetRawMessage() From 3152c96781785fb2bfb279723f0162d1aceedd3f Mon Sep 17 00:00:00 2001 From: "Mr. Z" Date: Wed, 5 Nov 2025 09:46:43 -0500 Subject: [PATCH 11/13] fix(model): return model instance in NewBaseModel function --- app/models/model/model.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/model/model.go b/app/models/model/model.go index ab0342f..47af436 100644 --- a/app/models/model/model.go +++ b/app/models/model/model.go @@ -75,7 +75,7 @@ func NewBaseModel(name Name, opts ...Options) (m *Model) { } } - return + return m } /* From 511162c9d306d1a7468dbb90a865dee194169b12 Mon Sep 17 00:00:00 2001 From: "Mr. Z" Date: Wed, 5 Nov 2025 09:59:28 -0500 Subject: [PATCH 12/13] test(fuzz): enhance fuzz tests for alert message parsing Refactored fuzz tests to utilize helper functions for building messages. Added common edge cases and improved validation checks for length fields. --- app/models/alert_message_types_fuzz_test.go | 222 ++++++++------------ 1 file changed, 90 insertions(+), 132 deletions(-) diff --git a/app/models/alert_message_types_fuzz_test.go b/app/models/alert_message_types_fuzz_test.go index 039e011..a1f2981 100644 --- a/app/models/alert_message_types_fuzz_test.go +++ b/app/models/alert_message_types_fuzz_test.go @@ -8,43 +8,81 @@ import ( "github.com/stretchr/testify/require" ) +// Helper functions for fuzz tests + +// buildVarIntMessage builds a message from multiple parts, each prefixed with its length as a VarInt +func buildVarIntMessage(parts ...[]byte) []byte { + msg := make([]byte, 0) + for _, part := range parts { + w := util.NewWriter() + w.WriteVarInt(uint64(len(part))) + msg = append(msg, w.Buf...) + msg = append(msg, part...) + } + return msg +} + +// addCommonEdgeCases adds standard edge case seeds to the fuzz corpus +func addCommonEdgeCases(f *testing.F) { + f.Add([]byte{}) // empty + f.Add([]byte{0x00}) // zero length + f.Add([]byte{0x01, 0x41}) // 1 byte 'A' + f.Add([]byte{0x01, 0x41, 0x00}) // with zero terminator +} + +// assertLengthFieldValid validates that a length field matches actual data and doesn't exceed input +func assertLengthFieldValid(t *testing.T, lengthField uint64, actualData, inputData []byte, fieldName string) { + require.LessOrEqual(t, lengthField, uint64(len(inputData)), "%s length should not exceed data length", fieldName) + require.Equal(t, lengthField, uint64(len(actualData)), "%s length should match %s data", fieldName, fieldName) +} + +// buildUtxoAlertMessage builds a 57-byte UTXO freeze/unfreeze alert message +func buildUtxoAlertMessage(vout, startHeight, endHeight uint64, expireFlag byte) []byte { + msg := make([]byte, 57) + copy(msg[0:32], make([]byte, 32)) // txid (32 bytes zero-filled) + binary.LittleEndian.PutUint64(msg[32:40], vout) + binary.LittleEndian.PutUint64(msg[40:48], startHeight) + binary.LittleEndian.PutUint64(msg[48:56], endHeight) + msg[56] = expireFlag + return msg +} + +// buildHeightPlusVarIntMessage builds a message with an 8-byte height followed by VarInt-prefixed data +func buildHeightPlusVarIntMessage(height uint64, data []byte) []byte { + heightBytes := make([]byte, 8) + binary.LittleEndian.PutUint64(heightBytes[0:8], height) + w := util.NewWriter() + w.WriteVarInt(uint64(len(data))) + msg := make([]byte, 0, 8+len(w.Buf)+len(data)) + msg = append(msg, heightBytes...) + msg = append(msg, w.Buf...) + msg = append(msg, data...) + return msg +} + +// buildFixedPrefixVarIntMessage builds a message with a fixed prefix followed by VarInt-prefixed data +func buildFixedPrefixVarIntMessage(prefix, data []byte) []byte { + msg := append([]byte(nil), prefix...) + w := util.NewWriter() + w.WriteVarInt(uint64(len(data))) + msg = append(msg, w.Buf...) + msg = append(msg, data...) + return msg +} + // FuzzAlertMessageBanPeerRead tests ban peer alert parsing func FuzzAlertMessageBanPeerRead(f *testing.F) { // Seed with valid ban peer message: VarInt(peerLen) + peer + VarInt(reasonLen) + reason - validMsg := make([]byte, 0) peerData := []byte("192.168.1.1:8333") reasonData := []byte("malicious behavior") - - // Add peer length and data - w := util.NewWriter() - w.WriteVarInt(uint64(len(peerData))) - validMsg = append(validMsg, w.Buf...) - validMsg = append(validMsg, peerData...) - - // Add reason length and data - w = util.NewWriter() - w.WriteVarInt(uint64(len(reasonData))) - validMsg = append(validMsg, w.Buf...) - validMsg = append(validMsg, reasonData...) - + validMsg := buildVarIntMessage(peerData, reasonData) f.Add(validMsg) // Seed with edge cases - f.Add([]byte{}) // empty - f.Add([]byte{0x00}) // zero length peer - f.Add([]byte{0x01, 0x41}) // 1 byte peer 'A' - f.Add([]byte{0x01, 0x41, 0x00}) // peer + zero reason + addCommonEdgeCases(f) // Seed with large values - largeMsg := make([]byte, 0) - w = util.NewWriter() - w.WriteVarInt(100) - largeMsg = append(largeMsg, w.Buf...) - largeMsg = append(largeMsg, make([]byte, 100)...) // 100 byte peer - w = util.NewWriter() - w.WriteVarInt(100) - largeMsg = append(largeMsg, w.Buf...) - largeMsg = append(largeMsg, make([]byte, 100)...) // 100 byte reason + largeMsg := buildVarIntMessage(make([]byte, 100), make([]byte, 100)) f.Add(largeMsg) f.Fuzz(func(t *testing.T, data []byte) { @@ -58,37 +96,21 @@ func FuzzAlertMessageBanPeerRead(f *testing.F) { } // If successful, validate the parsed data - require.LessOrEqual(t, alert.PeerLength, uint64(len(data)), "peer length should not exceed data length") - require.LessOrEqual(t, alert.ReasonLength, uint64(len(data)), "reason length should not exceed data length") - require.Equal(t, alert.PeerLength, uint64(len(alert.Peer)), "peer length should match peer data") - require.Equal(t, alert.ReasonLength, uint64(len(alert.Reason)), "reason length should match reason data") + assertLengthFieldValid(t, alert.PeerLength, alert.Peer, data, "peer") + assertLengthFieldValid(t, alert.ReasonLength, alert.Reason, data, "reason") }) } // FuzzAlertMessageUnbanPeerRead tests unban peer alert parsing func FuzzAlertMessageUnbanPeerRead(f *testing.F) { // Seed with valid unban peer message (same structure as ban) - validMsg := make([]byte, 0) peerData := []byte("192.168.1.1:8333") reasonData := []byte("false positive") - - w := util.NewWriter() - w.WriteVarInt(uint64(len(peerData))) - validMsg = append(validMsg, w.Buf...) - validMsg = append(validMsg, peerData...) - - w = util.NewWriter() - w.WriteVarInt(uint64(len(reasonData))) - validMsg = append(validMsg, w.Buf...) - validMsg = append(validMsg, reasonData...) - + validMsg := buildVarIntMessage(peerData, reasonData) f.Add(validMsg) // Edge cases - f.Add([]byte{}) - f.Add([]byte{0x00}) - f.Add([]byte{0x01, 0x41}) - f.Add([]byte{0x01, 0x41, 0x00}) + addCommonEdgeCases(f) f.Fuzz(func(t *testing.T, data []byte) { alert := &AlertMessageUnbanPeer{} @@ -100,43 +122,28 @@ func FuzzAlertMessageUnbanPeerRead(f *testing.F) { } // Validate parsed data - require.LessOrEqual(t, alert.PeerLength, uint64(len(data)), "peer length should not exceed data length") - require.LessOrEqual(t, alert.ReasonLength, uint64(len(data)), "reason length should not exceed data length") + assertLengthFieldValid(t, alert.PeerLength, alert.Peer, data, "peer") + assertLengthFieldValid(t, alert.ReasonLength, alert.Reason, data, "reason") }) } // FuzzAlertMessageInformationalRead tests informational alert parsing func FuzzAlertMessageInformationalRead(f *testing.F) { // Seed with valid informational message - validMsg := make([]byte, 0) message := []byte("System maintenance scheduled") - - w := util.NewWriter() - w.WriteVarInt(uint64(len(message))) - validMsg = append(validMsg, w.Buf...) - validMsg = append(validMsg, message...) - + validMsg := buildVarIntMessage(message) f.Add(validMsg) // Edge cases - f.Add([]byte{}) // empty - f.Add([]byte{0x00}) // zero length message - f.Add([]byte{0x01, 0x41}) // single character 'A' + addCommonEdgeCases(f) // Invalid: length exceeds data - invalidLen := make([]byte, 0) - w = util.NewWriter() - w.WriteVarInt(100) // claim 100 bytes - invalidLen = append(invalidLen, w.Buf...) - invalidLen = append(invalidLen, []byte{0x01}...) // but only 1 byte + invalidLen := buildVarIntMessage(make([]byte, 100)) + invalidLen = append(invalidLen[:len(invalidLen)-99], 0x01) // claim 100 bytes but only 1 byte f.Add(invalidLen) // Valid with extra bytes (should trigger IsComplete check) - extraBytes := make([]byte, 0) - w = util.NewWriter() - w.WriteVarInt(5) - extraBytes = append(extraBytes, w.Buf...) - extraBytes = append(extraBytes, []byte("hello")...) + extraBytes := buildVarIntMessage([]byte("hello")) extraBytes = append(extraBytes, []byte("extra")...) // extra bytes f.Add(extraBytes) @@ -150,28 +157,19 @@ func FuzzAlertMessageInformationalRead(f *testing.F) { } // Validate successful parse - require.Equal(t, alert.MessageLength, uint64(len(alert.Message)), "message length should match message data") - require.LessOrEqual(t, alert.MessageLength, uint64(len(data)), "message length should not exceed input data") + assertLengthFieldValid(t, alert.MessageLength, alert.Message, data, "message") }) } // FuzzAlertMessageFreezeUtxoRead tests freeze UTXO alert parsing func FuzzAlertMessageFreezeUtxoRead(f *testing.F) { // Seed with valid freeze message (57 bytes per fund) - validMsg := make([]byte, 57) // txid (32 bytes) + vout (8) + start height (8) + end height (8) + expire flag (1) = 57 - copy(validMsg[0:32], make([]byte, 32)) // txid - binary.LittleEndian.PutUint64(validMsg[32:40], 0) // vout - binary.LittleEndian.PutUint64(validMsg[40:48], 100000) // start height - binary.LittleEndian.PutUint64(validMsg[48:56], 200000) // end height - validMsg[56] = 1 // expire flag - + validMsg := buildUtxoAlertMessage(0, 100000, 200000, 1) f.Add(validMsg) // Multiple funds (114 bytes = 2 funds) - multipleMsg := make([]byte, 114) - copy(multipleMsg[0:57], validMsg) - copy(multipleMsg[57:114], validMsg) + multipleMsg := append(validMsg, validMsg...) f.Add(multipleMsg) // Edge cases @@ -182,12 +180,7 @@ func FuzzAlertMessageFreezeUtxoRead(f *testing.F) { f.Add(make([]byte, 171)) // 3 funds // Test with max int values to trigger overflow check - overflowMsg := make([]byte, 57) - copy(overflowMsg[0:32], make([]byte, 32)) - binary.LittleEndian.PutUint64(overflowMsg[32:40], ^uint64(0)) // max uint64 for vout - binary.LittleEndian.PutUint64(overflowMsg[40:48], 100000) - binary.LittleEndian.PutUint64(overflowMsg[48:56], 200000) - overflowMsg[56] = 0 + overflowMsg := buildUtxoAlertMessage(^uint64(0), 100000, 200000, 0) f.Add(overflowMsg) f.Fuzz(func(t *testing.T, data []byte) { @@ -215,13 +208,7 @@ func FuzzAlertMessageFreezeUtxoRead(f *testing.F) { // FuzzAlertMessageUnfreezeUtxoRead tests unfreeze UTXO alert parsing func FuzzAlertMessageUnfreezeUtxoRead(f *testing.F) { // Same structure as freeze UTXO (57 bytes per fund) - validMsg := make([]byte, 57) - copy(validMsg[0:32], make([]byte, 32)) - binary.LittleEndian.PutUint64(validMsg[32:40], 0) - binary.LittleEndian.PutUint64(validMsg[40:48], 100000) - binary.LittleEndian.PutUint64(validMsg[48:56], 200000) - validMsg[56] = 0 - + validMsg := buildUtxoAlertMessage(0, 100000, 200000, 0) f.Add(validMsg) f.Add([]byte{}) f.Add(make([]byte, 56)) @@ -246,17 +233,8 @@ func FuzzAlertMessageUnfreezeUtxoRead(f *testing.F) { // FuzzAlertMessageConfiscateTransactionRead tests confiscate transaction alert parsing func FuzzAlertMessageConfiscateTransactionRead(f *testing.F) { // Seed with valid confiscation message: height(8) + VarInt(hexLen) + hex - validMsg := make([]byte, 0) - heightBytes := make([]byte, 8) - binary.LittleEndian.PutUint64(heightBytes[0:8], 100000) // enforce at height - validMsg = append(validMsg, heightBytes...) - txHex := []byte("0100000001...") - w := util.NewWriter() - w.WriteVarInt(uint64(len(txHex))) - validMsg = append(validMsg, w.Buf...) - validMsg = append(validMsg, txHex...) - + validMsg := buildHeightPlusVarIntMessage(100000, txHex) f.Add(validMsg) // Edge cases @@ -265,27 +243,16 @@ func FuzzAlertMessageConfiscateTransactionRead(f *testing.F) { f.Add(make([]byte, 9)) // height + varint start // Minimum valid: height + zero-length tx - minMsgHeight := make([]byte, 8) - binary.LittleEndian.PutUint64(minMsgHeight[0:8], 0) - minMsg := append([]byte(nil), minMsgHeight...) - minMsg = append(minMsg, 0x00) // zero length varint + minMsg := buildHeightPlusVarIntMessage(0, []byte{}) f.Add(minMsg) // Test max int64 overflow - overflowMsgHeight := make([]byte, 8) - binary.LittleEndian.PutUint64(overflowMsgHeight[0:8], ^uint64(0)) // max uint64 - overflowMsg := append([]byte(nil), overflowMsgHeight...) - overflowMsg = append(overflowMsg, 0x00) + overflowMsg := buildHeightPlusVarIntMessage(^uint64(0), []byte{}) f.Add(overflowMsg) // Length exceeds data - badLenMsgHeight := make([]byte, 8) - binary.LittleEndian.PutUint64(badLenMsgHeight[0:8], 100000) - w = util.NewWriter() - w.WriteVarInt(100) // claim 100 bytes - badLenMsg := append([]byte(nil), badLenMsgHeight...) - badLenMsg = append(badLenMsg, w.Buf...) - badLenMsg = append(badLenMsg, []byte{0x01}...) // but only 1 byte + badLenMsg := buildHeightPlusVarIntMessage(100000, make([]byte, 100)) + badLenMsg = append(badLenMsg[:len(badLenMsg)-99], 0x01) // claim 100 bytes but only 1 byte f.Add(badLenMsg) f.Fuzz(func(t *testing.T, data []byte) { @@ -308,16 +275,9 @@ func FuzzAlertMessageConfiscateTransactionRead(f *testing.F) { func FuzzAlertMessageInvalidateBlockRead(f *testing.F) { // Seed with valid invalidate block message: blockHash(32) + VarInt(reasonLen) + reason blockHashBytes := make([]byte, 32) - // Fill with a sample block hash copy(blockHashBytes[0:32], []byte("blockhash123456789012345678901")) - validMsg := append([]byte(nil), blockHashBytes...) - reason := []byte("invalid proof of work") - w := util.NewWriter() - w.WriteVarInt(uint64(len(reason))) - validMsg = append(validMsg, w.Buf...) - validMsg = append(validMsg, reason...) - + validMsg := buildFixedPrefixVarIntMessage(blockHashBytes, reason) f.Add(validMsg) // Edge cases @@ -327,10 +287,8 @@ func FuzzAlertMessageInvalidateBlockRead(f *testing.F) { f.Add(make([]byte, 33)) // hash + varint start // Minimum valid: hash + zero reason - minMsgHash := make([]byte, 32) - minMsgInvalidate := append([]byte(nil), minMsgHash...) - minMsgInvalidate = append(minMsgInvalidate, 0x00) // zero length reason - f.Add(minMsgInvalidate) + minMsg := buildFixedPrefixVarIntMessage(make([]byte, 32), []byte{}) + f.Add(minMsg) f.Fuzz(func(t *testing.T, data []byte) { alert := &AlertMessageInvalidateBlock{} @@ -343,7 +301,7 @@ func FuzzAlertMessageInvalidateBlockRead(f *testing.F) { // Validate successful parse require.Len(t, alert.BlockHash, 32, "block hash should be 32 bytes") - require.Equal(t, alert.ReasonLength, uint64(len(alert.Reason)), "reason length should match reason data") + assertLengthFieldValid(t, alert.ReasonLength, alert.Reason, data, "reason") }) } From a0a075e29bd020f9cd2f821e886e39e9dc76ccfe Mon Sep 17 00:00:00 2001 From: "Mr. Z" Date: Wed, 5 Nov 2025 10:01:44 -0500 Subject: [PATCH 13/13] fix(alert): ensure EnforceAtHeight is not empty in freeze UTXO test --- app/models/alert_message_types_fuzz_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/app/models/alert_message_types_fuzz_test.go b/app/models/alert_message_types_fuzz_test.go index a1f2981..39a11fa 100644 --- a/app/models/alert_message_types_fuzz_test.go +++ b/app/models/alert_message_types_fuzz_test.go @@ -199,6 +199,7 @@ func FuzzAlertMessageFreezeUtxoRead(f *testing.F) { // Validate no overflow occurred for _, fund := range alert.Funds { require.GreaterOrEqual(t, fund.TxOut.Vout, 0, "vout should be non-negative") + require.NotEmpty(t, fund.EnforceAtHeight, "EnforceAtHeight should not be empty") require.LessOrEqual(t, fund.EnforceAtHeight[0].Start, int(^uint(0)>>1), "start height should not overflow int") require.LessOrEqual(t, fund.EnforceAtHeight[0].Stop, int(^uint(0)>>1), "end height should not overflow int") }