diff --git a/pkg/ccl/pgcryptoccl/pgcryptocipherccl/BUILD.bazel b/pkg/ccl/pgcryptoccl/pgcryptocipherccl/BUILD.bazel index 2b4db1d16959..97c104b1cda8 100644 --- a/pkg/ccl/pgcryptoccl/pgcryptocipherccl/BUILD.bazel +++ b/pkg/ccl/pgcryptoccl/pgcryptocipherccl/BUILD.bazel @@ -4,9 +4,11 @@ go_library( name = "pgcryptocipherccl", srcs = [ "cipher.go", + "cipher_fuzz.go", "cipher_method.go", "doc.go", "padding.go", + "test_helper.go", ], importpath = "github.com/cockroachdb/cockroach/pkg/ccl/pgcryptoccl/pgcryptocipherccl", visibility = ["//visibility:public"], @@ -15,6 +17,7 @@ go_library( "//pkg/sql/pgwire/pgerror", "//pkg/util/errorutil/unimplemented", "@com_github_cockroachdb_errors//:errors", + "@com_github_stretchr_testify//require", ], ) diff --git a/pkg/ccl/pgcryptoccl/pgcryptocipherccl/cipher_fuzz.go b/pkg/ccl/pgcryptoccl/pgcryptocipherccl/cipher_fuzz.go new file mode 100644 index 000000000000..cbb8958262c8 --- /dev/null +++ b/pkg/ccl/pgcryptoccl/pgcryptocipherccl/cipher_fuzz.go @@ -0,0 +1,46 @@ +// Copyright 2024 The Cockroach Authors. +// +// Licensed as a CockroachDB Enterprise file under the Cockroach Community +// License (the "License"); you may not use this file except in compliance with +// the License. You may obtain a copy of the License at +// +// https://github.com/cockroachdb/cockroach/blob/master/licenses/CCL.txt + +package pgcryptocipherccl + +import ( + "crypto/aes" + "testing" + + "github.com/stretchr/testify/require" +) + +func FuzzEncryptDecryptAES(f *testing.F) { + for _, tc := range CipherTestCases { + f.Add(tc.Plaintext, tc.Key, tc.Iv) + } + f.Fuzz(func(t *testing.T, plaintext []byte, key []byte, iv []byte) { + ciphertext, err := Encrypt(plaintext, key, iv, "aes") + require.NoError(t, err) + decryptedCiphertext, err := Decrypt(ciphertext, key, iv, "aes") + require.NoError(t, err) + require.Equal(t, plaintext, decryptedCiphertext) + }) +} + +func FuzzNoPaddingEncryptDecryptAES(f *testing.F) { + for _, tc := range CipherTestCases { + f.Add(tc.Plaintext, tc.Key, tc.Iv) + } + f.Fuzz(func(t *testing.T, plaintext []byte, key []byte, iv []byte) { + ciphertext, err := Encrypt(plaintext, key, iv, "aes/pad:none") + if plaintextLength := len(plaintext); plaintextLength%aes.BlockSize != 0 { + require.ErrorIs(t, err, ErrInvalidDataLength) + return + } + require.NoError(t, err) + decryptedCiphertext, err := Decrypt(ciphertext, key, iv, "aes/pad:none") + require.NoError(t, err) + require.Equal(t, plaintext, decryptedCiphertext) + }) +} diff --git a/pkg/ccl/pgcryptoccl/pgcryptocipherccl/cipher_test.go b/pkg/ccl/pgcryptoccl/pgcryptocipherccl/cipher_test.go index bca9e5a9c8fc..768c0dd6c611 100644 --- a/pkg/ccl/pgcryptoccl/pgcryptocipherccl/cipher_test.go +++ b/pkg/ccl/pgcryptoccl/pgcryptocipherccl/cipher_test.go @@ -9,7 +9,6 @@ package pgcryptocipherccl_test import ( - "crypto/aes" "testing" "github.com/cockroachdb/cockroach/pkg/ccl/pgcryptoccl/pgcryptocipherccl" @@ -17,67 +16,37 @@ import ( ) func TestEncrypt(t *testing.T) { - for name, tc := range cipherTestCases { + for name, tc := range pgcryptocipherccl.CipherTestCases { t.Run(name, func(t *testing.T) { - res, err := pgcryptocipherccl.Encrypt(tc.plaintext, tc.key, tc.iv, tc.cipherType) + res, err := pgcryptocipherccl.Encrypt(tc.Plaintext, tc.Key, tc.Iv, tc.CipherType) require.NoError(t, err) - require.Equal(t, tc.ciphertext, res) + require.Equal(t, tc.Ciphertext, res) }) } } func TestDecrypt(t *testing.T) { - for name, tc := range cipherTestCases { + for name, tc := range pgcryptocipherccl.CipherTestCases { t.Run(name, func(t *testing.T) { - res, err := pgcryptocipherccl.Decrypt(tc.ciphertext, tc.key, tc.iv, tc.cipherType) + res, err := pgcryptocipherccl.Decrypt(tc.Ciphertext, tc.Key, tc.Iv, tc.CipherType) require.NoError(t, err) - require.Equal(t, tc.plaintext, res) + require.Equal(t, tc.Plaintext, res) }) } } -func FuzzEncryptDecryptAES(f *testing.F) { - for _, tc := range cipherTestCases { - f.Add(tc.plaintext, tc.key, tc.iv) - } - f.Fuzz(func(t *testing.T, plaintext []byte, key []byte, iv []byte) { - ciphertext, err := pgcryptocipherccl.Encrypt(plaintext, key, iv, "aes") - require.NoError(t, err) - decryptedCiphertext, err := pgcryptocipherccl.Decrypt(ciphertext, key, iv, "aes") - require.NoError(t, err) - require.Equal(t, plaintext, decryptedCiphertext) - }) -} - -func FuzzEncryptDecryptAESNoPadding(f *testing.F) { - for _, tc := range cipherTestCases { - f.Add(tc.plaintext, tc.key, tc.iv) - } - f.Fuzz(func(t *testing.T, plaintext []byte, key []byte, iv []byte) { - ciphertext, err := pgcryptocipherccl.Encrypt(plaintext, key, iv, "aes/pad:none") - if plaintextLength := len(plaintext); plaintextLength%aes.BlockSize != 0 { - require.ErrorIs(t, err, pgcryptocipherccl.ErrInvalidDataLength) - return - } - require.NoError(t, err) - decryptedCiphertext, err := pgcryptocipherccl.Decrypt(ciphertext, key, iv, "aes/pad:none") - require.NoError(t, err) - require.Equal(t, plaintext, decryptedCiphertext) - }) -} - func BenchmarkEncrypt(b *testing.B) { - for name, tc := range cipherTestCases { + for name, tc := range pgcryptocipherccl.CipherTestCases { b.Run(name, func(b *testing.B) { - benchmarkEncrypt(b, tc.plaintext, tc.key, tc.iv, tc.cipherType) + benchmarkEncrypt(b, tc.Plaintext, tc.Key, tc.Iv, tc.CipherType) }) } } func BenchmarkDecrypt(b *testing.B) { - for name, tc := range cipherTestCases { + for name, tc := range pgcryptocipherccl.CipherTestCases { b.Run(name, func(*testing.B) { - benchmarkDecrypt(b, tc.ciphertext, tc.key, tc.iv, tc.cipherType) + benchmarkDecrypt(b, tc.Ciphertext, tc.Key, tc.Iv, tc.CipherType) }) } } @@ -95,53 +64,3 @@ func benchmarkDecrypt(b *testing.B, data []byte, key []byte, iv []byte, cipherTy require.NoError(b, err) } } - -// Note: The encrypted values were manually checked against the results of -// the equivalent function on Postgres 15.3. -var cipherTestCases = map[string]struct { - plaintext []byte - key []byte - iv []byte - cipherType string - ciphertext []byte -}{ - "AES-128": { - plaintext: []byte("abc"), - key: []byte("16_byte_long_key"), - cipherType: "aes", - ciphertext: []byte{0x0, 0x26, 0xcd, 0x62, 0x6, 0xcf, 0xd9, 0x21, 0x40, 0x88, 0x3b, 0x75, 0xc0, 0x98, 0xd6, 0x13}, - }, - "AES-128 with IV": { - plaintext: []byte("abc"), - key: []byte("16_byte_long_key"), - iv: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20}, - cipherType: "aes/pad:pkcs", - ciphertext: []byte{0xf8, 0x3e, 0xf0, 0x88, 0x83, 0x9b, 0x2f, 0x19, 0x1c, 0x42, 0x5, 0xa2, 0xe, 0x62, 0x69, 0xd3}, - }, - "AES-192": { - plaintext: []byte("UCVBgQEhwVxXrABksapmqglhTGNWRRnGnUUQHtNanYuyIDrQgHvQanwkfXRvTnJLsMauOfdlLBuJRJkzaGpnVRyENKGwFfauXLJD"), - key: []byte("24_byte_looooooooong_key"), - cipherType: "aes", - ciphertext: []byte{0x44, 0x68, 0x34, 0xe4, 0x3c, 0xb1, 0x76, 0x3, 0xa2, 0xb2, 0x3d, 0x25, 0x13, 0xa7, 0x84, 0xa3, 0x6b, 0xae, 0x79, 0x3, 0x72, 0x74, 0x57, 0x1c, 0x32, 0xcd, 0x29, 0x4a, 0xd, 0x6e, 0xe, 0xf9, 0x27, 0xee, 0x0, 0x76, 0xdc, 0xa4, 0x60, 0xb0, 0x9, 0x0, 0xbc, 0x11, 0x75, 0x4f, 0xee, 0x61, 0x22, 0x52, 0xa6, 0x7, 0x25, 0xa, 0x4f, 0x86, 0xd6, 0x20, 0x56, 0xb6, 0xae, 0xee, 0xc, 0xcd, 0x66, 0x4, 0x88, 0x9e, 0x7f, 0x39, 0xaf, 0x64, 0xce, 0x8b, 0x59, 0x60, 0xc1, 0x74, 0x4d, 0xb2, 0x16, 0xb0, 0x10, 0xea, 0x9c, 0x1, 0x49, 0x6c, 0x42, 0x9f, 0xd5, 0xf5, 0x81, 0x6, 0xf6, 0x4f, 0x21, 0x12, 0xf4, 0xdb, 0xc2, 0x90, 0x74, 0x2a, 0xa3, 0xd1, 0x66, 0x91, 0x5d, 0x97, 0x60, 0x64}, - }, - "AES-192 with IV": { - plaintext: []byte("UCVBgQEhwVxXrABksapmqglhTGNWRRnGnUUQHtNanYuyIDrQgHvQanwkfXRvTnJLsMauOfdlLBuJRJkzaGpnVRyENKGwFfauXLJD"), - key: []byte("24_byte_looooooooong_key"), - iv: []byte{152, 0, 68, 172}, - cipherType: "aes", - ciphertext: []byte{0xd2, 0x84, 0x24, 0x69, 0x11, 0x17, 0xec, 0x5c, 0x1c, 0xbe, 0x2d, 0x34, 0x67, 0xb7, 0xf, 0xa9, 0x94, 0xeb, 0x11, 0x97, 0xe4, 0x60, 0x72, 0xe7, 0xf1, 0x49, 0x9b, 0xc7, 0xc8, 0xc6, 0x7e, 0xad, 0x2b, 0xe2, 0x36, 0x6a, 0xd1, 0x20, 0x0, 0x4a, 0x28, 0x7a, 0x25, 0x86, 0x9a, 0x23, 0x9, 0xe9, 0xf1, 0x2c, 0x7e, 0xe, 0x3a, 0xec, 0xb9, 0x10, 0xf5, 0x35, 0xb6, 0xc2, 0xf6, 0x93, 0x16, 0x2, 0xd, 0x3b, 0x65, 0x63, 0x27, 0x29, 0xd2, 0xeb, 0xc8, 0xc0, 0xa0, 0x2e, 0x19, 0x22, 0xc4, 0x1e, 0x38, 0x73, 0xb0, 0x34, 0x69, 0xda, 0x52, 0x63, 0x9e, 0xaa, 0xa5, 0x93, 0x1, 0x37, 0xff, 0x9d, 0x4e, 0xf1, 0x7f, 0x72, 0x79, 0xe6, 0xad, 0x4b, 0x1f, 0x67, 0x4f, 0x2, 0xab, 0x17, 0x13, 0xcc}, - }, - "AES-256": { - plaintext: []byte("ghJxetyYQiLwdAtibf52bECQbA6QP0FsC0wDURcrR9DRZs7WChql2cJSunTh6rr6b5MM5YYgzWgXHvTxHaEIMiAuEXHsfcyInlxIyaHe2wS03PV6HZ1GKNbhksUx6NjKEoW5SmvmSngdlXSAOWwTYalUP6mKZm9BYHe57LQiHhiX76dKkqqVBvt16t6Hki3hRbqxdUH9JTB3sNAjoH6EjZKnH9h04M02IYncyJAhEpfDINkaerYMQ1Hbavpo0UHu"), - key: []byte("32_byte_looooooooooooooooong_key"), - cipherType: "aes/pad:none", - ciphertext: []byte{0xb1, 0x7e, 0x2f, 0xfd, 0x8d, 0xe2, 0x96, 0x5, 0xdc, 0xb1, 0x74, 0x5f, 0x59, 0x67, 0x2e, 0x30, 0x78, 0x6c, 0xdf, 0x2e, 0xd6, 0xbd, 0x23, 0xcd, 0xf2, 0x9b, 0x31, 0xd0, 0xa1, 0xa3, 0xd5, 0xd0, 0x4e, 0x25, 0x81, 0x25, 0x19, 0xdd, 0x4a, 0x67, 0xb4, 0xba, 0x71, 0x28, 0x2f, 0xa6, 0x31, 0x71, 0xf, 0xfc, 0xd6, 0x9a, 0xe3, 0xa6, 0x7e, 0xc7, 0x5d, 0xe7, 0xe5, 0xef, 0x8c, 0x90, 0xeb, 0x15, 0xfb, 0xa0, 0xd7, 0x30, 0x87, 0xf6, 0x5, 0xa9, 0x16, 0x86, 0xcc, 0xc0, 0x9f, 0x19, 0x14, 0xb8, 0x43, 0x72, 0xcb, 0x35, 0x9d, 0x18, 0xdc, 0xd2, 0xd6, 0xe4, 0x30, 0xc5, 0xc4, 0x8f, 0x4b, 0x98, 0xdd, 0x29, 0x53, 0xd9, 0x6, 0x1b, 0x48, 0xe9, 0x4a, 0x76, 0x36, 0xf5, 0x5a, 0x55, 0x5e, 0xd, 0x18, 0x57, 0xfe, 0xc2, 0x90, 0x5b, 0x82, 0x50, 0x7e, 0x14, 0xe1, 0xe0, 0xb0, 0x5e, 0x26, 0x44, 0x61, 0xe4, 0x22, 0x72, 0x7, 0x19, 0x60, 0xef, 0x6c, 0xb9, 0xfb, 0xa5, 0x9e, 0x5c, 0x7, 0xf6, 0x13, 0xc7, 0xa1, 0x97, 0x6b, 0xc, 0x1c, 0xaa, 0xaa, 0xcf, 0xd8, 0x6d, 0x39, 0x50, 0x57, 0x94, 0xce, 0x5d, 0xd, 0xf2, 0xd4, 0x2b, 0xbd, 0x71, 0xb4, 0xc6, 0x3b, 0x92, 0xb0, 0x5f, 0x3d, 0x56, 0xf5, 0x23, 0x6d, 0xaf, 0xb0, 0xbc, 0x62, 0xd5, 0xf0, 0x1e, 0x75, 0xad, 0xc2, 0x64, 0xbf, 0x66, 0xe7, 0xad, 0xab, 0xfd, 0xbf, 0xde, 0x72, 0xa4, 0x6, 0x39, 0xd2, 0xf4, 0x3, 0x5e, 0x27, 0x6, 0x98, 0x92, 0x7d, 0x2c, 0xe4, 0xac, 0x9e, 0xff, 0x41, 0xc1, 0xe7, 0xc1, 0x69, 0xbd, 0x8f, 0x5b, 0xc4, 0x1e, 0x36, 0x59, 0xa9, 0xb5, 0xbc, 0x64, 0x49, 0xfd, 0x78, 0x7f, 0xc, 0xad, 0x25, 0xae, 0x4b, 0xec, 0xb5, 0x4f, 0x97, 0x9e, 0x8c, 0xa6, 0x7b, 0x9, 0x2a, 0x76, 0xcd, 0x2d, 0x2e, 0x41}, - }, - "AES-256 with IV": { - plaintext: []byte("ghJxetyYQiLwdAtibf52bECQbA6QP0FsC0wDURcrR9DRZs7WChql2cJSunTh6rr6b5MM5YYgzWgXHvTxHaEIMiAuEXHsfcyInlxIyaHe2wS03PV6HZ1GKNbhksUx6NjKEoW5SmvmSngdlXSAOWwTYalUP6mKZm9BYHe57LQiHhiX76dKkqqVBvt16t6Hki3hRbqxdUH9JTB3sNAjoH6EjZKnH9h04M02IYncyJAhEpfDINkaerYMQ1Hbavpo0UHu"), - key: []byte("32_byte_looooooooooooooooong_key"), - iv: []byte{154, 201, 158, 144, 3, 25, 184, 208, 70, 90, 123, 45, 168, 27, 236, 25}, - cipherType: "aes", - ciphertext: []byte{0x8b, 0xb2, 0xd7, 0x52, 0xdd, 0xff, 0x46, 0xa, 0xb9, 0x67, 0x12, 0x57, 0xe0, 0xff, 0x61, 0x23, 0x7, 0x60, 0x2, 0xe2, 0x28, 0x53, 0x53, 0x7c, 0x3e, 0xa, 0xdf, 0x8, 0x6e, 0xbd, 0x54, 0x48, 0xb3, 0x82, 0xf7, 0x6a, 0xce, 0xc6, 0xc8, 0xb3, 0xda, 0x19, 0xc5, 0x81, 0x91, 0x11, 0xd8, 0xd0, 0x87, 0x7d, 0xc0, 0x29, 0xb4, 0xe4, 0x8d, 0x74, 0x93, 0x0, 0xeb, 0x34, 0xc9, 0xd2, 0xb1, 0xc9, 0x30, 0xbc, 0xd9, 0x67, 0x7b, 0xa4, 0xeb, 0x81, 0x32, 0x8c, 0xa9, 0x95, 0xb9, 0x7c, 0xfb, 0xc0, 0x5f, 0x87, 0x2c, 0x32, 0x43, 0x15, 0xb4, 0xd1, 0x9b, 0x25, 0x3c, 0xe, 0xaf, 0xd1, 0x56, 0x17, 0xac, 0x26, 0x51, 0x17, 0x36, 0xda, 0xf6, 0x13, 0x5d, 0xf8, 0x12, 0xf6, 0x1f, 0x4, 0x17, 0x5b, 0xa8, 0xf1, 0xb1, 0xda, 0xc, 0x38, 0x13, 0x10, 0xe7, 0xe0, 0xd7, 0xa8, 0x4a, 0x69, 0x83, 0x5e, 0xe7, 0xc0, 0xf7, 0x21, 0xdb, 0xe3, 0x9d, 0x84, 0xce, 0x8e, 0x50, 0x7, 0x44, 0xe4, 0xe, 0xdf, 0xde, 0x88, 0xa5, 0xee, 0xd7, 0xf4, 0x32, 0xa0, 0x60, 0x24, 0x9d, 0x8b, 0x5a, 0xdf, 0xbc, 0xca, 0x6c, 0x2c, 0x80, 0x40, 0x47, 0xdf, 0x3f, 0x9b, 0xbe, 0x86, 0x34, 0x2b, 0x79, 0xc1, 0x27, 0xcf, 0x4c, 0xda, 0x89, 0x6b, 0xd8, 0xcb, 0xf8, 0xcd, 0x57, 0x46, 0xbc, 0x54, 0x55, 0x21, 0xc3, 0x34, 0x5e, 0x4c, 0xc8, 0xc3, 0x21, 0x51, 0xce, 0x8b, 0x3e, 0xcb, 0x36, 0xdc, 0x32, 0x31, 0xdc, 0x27, 0xf8, 0x12, 0x77, 0x64, 0xa8, 0xb8, 0x6d, 0x3c, 0x42, 0x6a, 0xd9, 0x7c, 0xbf, 0x14, 0xc5, 0x58, 0xdb, 0x1d, 0x24, 0x74, 0xb5, 0x47, 0xce, 0x54, 0x50, 0x32, 0xfc, 0xb9, 0x9f, 0xd3, 0x45, 0xa2, 0x5, 0x5d, 0xa4, 0x8a, 0x57, 0x58, 0x10, 0xc7, 0x5e, 0x83, 0x9f, 0x2b, 0x64, 0xe3, 0xd4, 0x98, 0x15, 0xf7, 0x8a, 0xdf, 0xe7, 0xc7, 0x2, 0x27, 0xa4, 0xff, 0xa7, 0x98, 0x24, 0xa, 0xf0, 0x2d}, - }, -} diff --git a/pkg/ccl/pgcryptoccl/pgcryptocipherccl/test_helper.go b/pkg/ccl/pgcryptoccl/pgcryptocipherccl/test_helper.go new file mode 100644 index 000000000000..296a9b5266ee --- /dev/null +++ b/pkg/ccl/pgcryptoccl/pgcryptocipherccl/test_helper.go @@ -0,0 +1,59 @@ +// Copyright 2024 The Cockroach Authors. +// +// Licensed as a CockroachDB Enterprise file under the Cockroach Community +// License (the "License"); you may not use this file except in compliance with +// the License. You may obtain a copy of the License at +// +// https://github.com/cockroachdb/cockroach/blob/master/licenses/CCL.txt + +package pgcryptocipherccl + +// Note: The encrypted values were manually checked against the results of +// the equivalent function on Postgres 15.3. +var CipherTestCases = map[string]struct { + Plaintext []byte + Key []byte + Iv []byte + CipherType string + Ciphertext []byte +}{ + "AES-128": { + Plaintext: []byte("abc"), + Key: []byte("16_byte_long_key"), + CipherType: "aes", + Ciphertext: []byte{0x0, 0x26, 0xcd, 0x62, 0x6, 0xcf, 0xd9, 0x21, 0x40, 0x88, 0x3b, 0x75, 0xc0, 0x98, 0xd6, 0x13}, + }, + "AES-128 with IV": { + Plaintext: []byte("abc"), + Key: []byte("16_byte_long_key"), + Iv: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20}, + CipherType: "aes/pad:pkcs", + Ciphertext: []byte{0xf8, 0x3e, 0xf0, 0x88, 0x83, 0x9b, 0x2f, 0x19, 0x1c, 0x42, 0x5, 0xa2, 0xe, 0x62, 0x69, 0xd3}, + }, + "AES-192": { + Plaintext: []byte("UCVBgQEhwVxXrABksapmqglhTGNWRRnGnUUQHtNanYuyIDrQgHvQanwkfXRvTnJLsMauOfdlLBuJRJkzaGpnVRyENKGwFfauXLJD"), + Key: []byte("24_byte_looooooooong_key"), + CipherType: "aes", + Ciphertext: []byte{0x44, 0x68, 0x34, 0xe4, 0x3c, 0xb1, 0x76, 0x3, 0xa2, 0xb2, 0x3d, 0x25, 0x13, 0xa7, 0x84, 0xa3, 0x6b, 0xae, 0x79, 0x3, 0x72, 0x74, 0x57, 0x1c, 0x32, 0xcd, 0x29, 0x4a, 0xd, 0x6e, 0xe, 0xf9, 0x27, 0xee, 0x0, 0x76, 0xdc, 0xa4, 0x60, 0xb0, 0x9, 0x0, 0xbc, 0x11, 0x75, 0x4f, 0xee, 0x61, 0x22, 0x52, 0xa6, 0x7, 0x25, 0xa, 0x4f, 0x86, 0xd6, 0x20, 0x56, 0xb6, 0xae, 0xee, 0xc, 0xcd, 0x66, 0x4, 0x88, 0x9e, 0x7f, 0x39, 0xaf, 0x64, 0xce, 0x8b, 0x59, 0x60, 0xc1, 0x74, 0x4d, 0xb2, 0x16, 0xb0, 0x10, 0xea, 0x9c, 0x1, 0x49, 0x6c, 0x42, 0x9f, 0xd5, 0xf5, 0x81, 0x6, 0xf6, 0x4f, 0x21, 0x12, 0xf4, 0xdb, 0xc2, 0x90, 0x74, 0x2a, 0xa3, 0xd1, 0x66, 0x91, 0x5d, 0x97, 0x60, 0x64}, + }, + "AES-192 with IV": { + Plaintext: []byte("UCVBgQEhwVxXrABksapmqglhTGNWRRnGnUUQHtNanYuyIDrQgHvQanwkfXRvTnJLsMauOfdlLBuJRJkzaGpnVRyENKGwFfauXLJD"), + Key: []byte("24_byte_looooooooong_key"), + Iv: []byte{152, 0, 68, 172}, + CipherType: "aes", + Ciphertext: []byte{0xd2, 0x84, 0x24, 0x69, 0x11, 0x17, 0xec, 0x5c, 0x1c, 0xbe, 0x2d, 0x34, 0x67, 0xb7, 0xf, 0xa9, 0x94, 0xeb, 0x11, 0x97, 0xe4, 0x60, 0x72, 0xe7, 0xf1, 0x49, 0x9b, 0xc7, 0xc8, 0xc6, 0x7e, 0xad, 0x2b, 0xe2, 0x36, 0x6a, 0xd1, 0x20, 0x0, 0x4a, 0x28, 0x7a, 0x25, 0x86, 0x9a, 0x23, 0x9, 0xe9, 0xf1, 0x2c, 0x7e, 0xe, 0x3a, 0xec, 0xb9, 0x10, 0xf5, 0x35, 0xb6, 0xc2, 0xf6, 0x93, 0x16, 0x2, 0xd, 0x3b, 0x65, 0x63, 0x27, 0x29, 0xd2, 0xeb, 0xc8, 0xc0, 0xa0, 0x2e, 0x19, 0x22, 0xc4, 0x1e, 0x38, 0x73, 0xb0, 0x34, 0x69, 0xda, 0x52, 0x63, 0x9e, 0xaa, 0xa5, 0x93, 0x1, 0x37, 0xff, 0x9d, 0x4e, 0xf1, 0x7f, 0x72, 0x79, 0xe6, 0xad, 0x4b, 0x1f, 0x67, 0x4f, 0x2, 0xab, 0x17, 0x13, 0xcc}, + }, + "AES-256": { + Plaintext: []byte("ghJxetyYQiLwdAtibf52bECQbA6QP0FsC0wDURcrR9DRZs7WChql2cJSunTh6rr6b5MM5YYgzWgXHvTxHaEIMiAuEXHsfcyInlxIyaHe2wS03PV6HZ1GKNbhksUx6NjKEoW5SmvmSngdlXSAOWwTYalUP6mKZm9BYHe57LQiHhiX76dKkqqVBvt16t6Hki3hRbqxdUH9JTB3sNAjoH6EjZKnH9h04M02IYncyJAhEpfDINkaerYMQ1Hbavpo0UHu"), + Key: []byte("32_byte_looooooooooooooooong_key"), + CipherType: "aes/pad:none", + Ciphertext: []byte{0xb1, 0x7e, 0x2f, 0xfd, 0x8d, 0xe2, 0x96, 0x5, 0xdc, 0xb1, 0x74, 0x5f, 0x59, 0x67, 0x2e, 0x30, 0x78, 0x6c, 0xdf, 0x2e, 0xd6, 0xbd, 0x23, 0xcd, 0xf2, 0x9b, 0x31, 0xd0, 0xa1, 0xa3, 0xd5, 0xd0, 0x4e, 0x25, 0x81, 0x25, 0x19, 0xdd, 0x4a, 0x67, 0xb4, 0xba, 0x71, 0x28, 0x2f, 0xa6, 0x31, 0x71, 0xf, 0xfc, 0xd6, 0x9a, 0xe3, 0xa6, 0x7e, 0xc7, 0x5d, 0xe7, 0xe5, 0xef, 0x8c, 0x90, 0xeb, 0x15, 0xfb, 0xa0, 0xd7, 0x30, 0x87, 0xf6, 0x5, 0xa9, 0x16, 0x86, 0xcc, 0xc0, 0x9f, 0x19, 0x14, 0xb8, 0x43, 0x72, 0xcb, 0x35, 0x9d, 0x18, 0xdc, 0xd2, 0xd6, 0xe4, 0x30, 0xc5, 0xc4, 0x8f, 0x4b, 0x98, 0xdd, 0x29, 0x53, 0xd9, 0x6, 0x1b, 0x48, 0xe9, 0x4a, 0x76, 0x36, 0xf5, 0x5a, 0x55, 0x5e, 0xd, 0x18, 0x57, 0xfe, 0xc2, 0x90, 0x5b, 0x82, 0x50, 0x7e, 0x14, 0xe1, 0xe0, 0xb0, 0x5e, 0x26, 0x44, 0x61, 0xe4, 0x22, 0x72, 0x7, 0x19, 0x60, 0xef, 0x6c, 0xb9, 0xfb, 0xa5, 0x9e, 0x5c, 0x7, 0xf6, 0x13, 0xc7, 0xa1, 0x97, 0x6b, 0xc, 0x1c, 0xaa, 0xaa, 0xcf, 0xd8, 0x6d, 0x39, 0x50, 0x57, 0x94, 0xce, 0x5d, 0xd, 0xf2, 0xd4, 0x2b, 0xbd, 0x71, 0xb4, 0xc6, 0x3b, 0x92, 0xb0, 0x5f, 0x3d, 0x56, 0xf5, 0x23, 0x6d, 0xaf, 0xb0, 0xbc, 0x62, 0xd5, 0xf0, 0x1e, 0x75, 0xad, 0xc2, 0x64, 0xbf, 0x66, 0xe7, 0xad, 0xab, 0xfd, 0xbf, 0xde, 0x72, 0xa4, 0x6, 0x39, 0xd2, 0xf4, 0x3, 0x5e, 0x27, 0x6, 0x98, 0x92, 0x7d, 0x2c, 0xe4, 0xac, 0x9e, 0xff, 0x41, 0xc1, 0xe7, 0xc1, 0x69, 0xbd, 0x8f, 0x5b, 0xc4, 0x1e, 0x36, 0x59, 0xa9, 0xb5, 0xbc, 0x64, 0x49, 0xfd, 0x78, 0x7f, 0xc, 0xad, 0x25, 0xae, 0x4b, 0xec, 0xb5, 0x4f, 0x97, 0x9e, 0x8c, 0xa6, 0x7b, 0x9, 0x2a, 0x76, 0xcd, 0x2d, 0x2e, 0x41}, + }, + "AES-256 with IV": { + Plaintext: []byte("ghJxetyYQiLwdAtibf52bECQbA6QP0FsC0wDURcrR9DRZs7WChql2cJSunTh6rr6b5MM5YYgzWgXHvTxHaEIMiAuEXHsfcyInlxIyaHe2wS03PV6HZ1GKNbhksUx6NjKEoW5SmvmSngdlXSAOWwTYalUP6mKZm9BYHe57LQiHhiX76dKkqqVBvt16t6Hki3hRbqxdUH9JTB3sNAjoH6EjZKnH9h04M02IYncyJAhEpfDINkaerYMQ1Hbavpo0UHu"), + Key: []byte("32_byte_looooooooooooooooong_key"), + Iv: []byte{154, 201, 158, 144, 3, 25, 184, 208, 70, 90, 123, 45, 168, 27, 236, 25}, + CipherType: "aes", + Ciphertext: []byte{0x8b, 0xb2, 0xd7, 0x52, 0xdd, 0xff, 0x46, 0xa, 0xb9, 0x67, 0x12, 0x57, 0xe0, 0xff, 0x61, 0x23, 0x7, 0x60, 0x2, 0xe2, 0x28, 0x53, 0x53, 0x7c, 0x3e, 0xa, 0xdf, 0x8, 0x6e, 0xbd, 0x54, 0x48, 0xb3, 0x82, 0xf7, 0x6a, 0xce, 0xc6, 0xc8, 0xb3, 0xda, 0x19, 0xc5, 0x81, 0x91, 0x11, 0xd8, 0xd0, 0x87, 0x7d, 0xc0, 0x29, 0xb4, 0xe4, 0x8d, 0x74, 0x93, 0x0, 0xeb, 0x34, 0xc9, 0xd2, 0xb1, 0xc9, 0x30, 0xbc, 0xd9, 0x67, 0x7b, 0xa4, 0xeb, 0x81, 0x32, 0x8c, 0xa9, 0x95, 0xb9, 0x7c, 0xfb, 0xc0, 0x5f, 0x87, 0x2c, 0x32, 0x43, 0x15, 0xb4, 0xd1, 0x9b, 0x25, 0x3c, 0xe, 0xaf, 0xd1, 0x56, 0x17, 0xac, 0x26, 0x51, 0x17, 0x36, 0xda, 0xf6, 0x13, 0x5d, 0xf8, 0x12, 0xf6, 0x1f, 0x4, 0x17, 0x5b, 0xa8, 0xf1, 0xb1, 0xda, 0xc, 0x38, 0x13, 0x10, 0xe7, 0xe0, 0xd7, 0xa8, 0x4a, 0x69, 0x83, 0x5e, 0xe7, 0xc0, 0xf7, 0x21, 0xdb, 0xe3, 0x9d, 0x84, 0xce, 0x8e, 0x50, 0x7, 0x44, 0xe4, 0xe, 0xdf, 0xde, 0x88, 0xa5, 0xee, 0xd7, 0xf4, 0x32, 0xa0, 0x60, 0x24, 0x9d, 0x8b, 0x5a, 0xdf, 0xbc, 0xca, 0x6c, 0x2c, 0x80, 0x40, 0x47, 0xdf, 0x3f, 0x9b, 0xbe, 0x86, 0x34, 0x2b, 0x79, 0xc1, 0x27, 0xcf, 0x4c, 0xda, 0x89, 0x6b, 0xd8, 0xcb, 0xf8, 0xcd, 0x57, 0x46, 0xbc, 0x54, 0x55, 0x21, 0xc3, 0x34, 0x5e, 0x4c, 0xc8, 0xc3, 0x21, 0x51, 0xce, 0x8b, 0x3e, 0xcb, 0x36, 0xdc, 0x32, 0x31, 0xdc, 0x27, 0xf8, 0x12, 0x77, 0x64, 0xa8, 0xb8, 0x6d, 0x3c, 0x42, 0x6a, 0xd9, 0x7c, 0xbf, 0x14, 0xc5, 0x58, 0xdb, 0x1d, 0x24, 0x74, 0xb5, 0x47, 0xce, 0x54, 0x50, 0x32, 0xfc, 0xb9, 0x9f, 0xd3, 0x45, 0xa2, 0x5, 0x5d, 0xa4, 0x8a, 0x57, 0x58, 0x10, 0xc7, 0x5e, 0x83, 0x9f, 0x2b, 0x64, 0xe3, 0xd4, 0x98, 0x15, 0xf7, 0x8a, 0xdf, 0xe7, 0xc7, 0x2, 0x27, 0xa4, 0xff, 0xa7, 0x98, 0x24, 0xa, 0xf0, 0x2d}, + }, +} diff --git a/pkg/keys/BUILD.bazel b/pkg/keys/BUILD.bazel index dcfbc1cd6841..fd1d1a7e9843 100644 --- a/pkg/keys/BUILD.bazel +++ b/pkg/keys/BUILD.bazel @@ -8,6 +8,7 @@ go_library( "errors.go", "keys.go", "printer.go", + "printer_fuzz.go", "spans.go", "sql.go", ], diff --git a/pkg/keys/printer_fuzz.go b/pkg/keys/printer_fuzz.go new file mode 100644 index 000000000000..66ca59027398 --- /dev/null +++ b/pkg/keys/printer_fuzz.go @@ -0,0 +1,42 @@ +// Copyright 2024 The Cockroach Authors. +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package keys + +import ( + "testing" + + "github.com/cockroachdb/cockroach/pkg/util/encoding" +) + +func FuzzPrettyPrint(f *testing.F) { + f.Add([]byte(nil), []byte(nil)) + + byteSliceToValDirs := func(s []byte) (dir []encoding.Direction) { + for _, b := range s { + dir = append(dir, encoding.Direction(b)) + if len(dir) > 5 { // Arbitrary limit on values direction as fuzzer can generate huge arrays. + break + } + } + return dir + } + + f.Fuzz(func(t *testing.T, valsDirs, key []byte) { + s := PrettyPrint(byteSliceToValDirs(valsDirs), key) + // Arbitrary limit. Some inputs generate bit arrays (and those tend to be long + // in string form), but we want to make + // sure that pretty printer doesn't do something super silly and allocate + // excessive amount of memory when the key is small. + if len(s) > 1<<19 { + t.Fatalf("pretty string is %d bytes, while key is only %d with valDirs len %d", len(s), len(key), len(valsDirs)) + } + }) +} diff --git a/pkg/keys/printer_test.go b/pkg/keys/printer_test.go index 1a51519d6365..0ff7b0ffe079 100644 --- a/pkg/keys/printer_test.go +++ b/pkg/keys/printer_test.go @@ -639,28 +639,3 @@ func TestFormatHexKey(t *testing.T) { func makeKey(keys ...[]byte) []byte { return bytes.Join(keys, nil) } - -func FuzzPrettyPrint(f *testing.F) { - f.Add([]byte(nil), []byte(nil)) - - byteSliceToValDirs := func(s []byte) (dir []encoding.Direction) { - for _, b := range s { - dir = append(dir, encoding.Direction(b)) - if len(dir) > 5 { // Arbitrary limit on values direction as fuzzer can generate huge arrays. - break - } - } - return dir - } - - f.Fuzz(func(t *testing.T, valsDirs, key []byte) { - s := keys.PrettyPrint(byteSliceToValDirs(valsDirs), key) - // Arbitrary limit. Some inputs generate bit arrays (and those tend to be long - // in string form), but we want to make - // sure that pretty printer doesn't do something super silly and allocate - // excessive amount of memory when the key is small. - if len(s) > 1<<19 { - t.Fatalf("pretty string is %d bytes, while key is only %d with valDirs len %d", len(s), len(key), len(valsDirs)) - } - }) -} diff --git a/pkg/sql/sqlliveness/slstorage/BUILD.bazel b/pkg/sql/sqlliveness/slstorage/BUILD.bazel index 883d4b68d652..5122e74e8a8c 100644 --- a/pkg/sql/sqlliveness/slstorage/BUILD.bazel +++ b/pkg/sql/sqlliveness/slstorage/BUILD.bazel @@ -6,6 +6,7 @@ go_library( "key_encoder.go", "metrics.go", "sessionid.go", + "sessionid_fuzz.go", "slstorage.go", "test_helpers.go", ], @@ -22,6 +23,7 @@ go_library( "//pkg/settings/cluster", "//pkg/sql/catalog", "//pkg/sql/catalog/systemschema", + "//pkg/sql/enum", "//pkg/sql/regionliveness", "//pkg/sql/sem/eval", "//pkg/sql/sqlliveness", @@ -29,6 +31,7 @@ go_library( "//pkg/util/cache", "//pkg/util/encoding", "//pkg/util/hlc", + "//pkg/util/leaktest", "//pkg/util/log", "//pkg/util/metric", "//pkg/util/stop", @@ -39,6 +42,7 @@ go_library( "@com_github_cockroachdb_errors//:errors", "@com_github_cockroachdb_redact//:redact", "@com_github_prometheus_client_model//go", + "@com_github_stretchr_testify//require", ], ) diff --git a/pkg/sql/sqlliveness/slstorage/sessionid_fuzz.go b/pkg/sql/sqlliveness/slstorage/sessionid_fuzz.go new file mode 100644 index 000000000000..9eb86b2ee8d1 --- /dev/null +++ b/pkg/sql/sqlliveness/slstorage/sessionid_fuzz.go @@ -0,0 +1,52 @@ +// Copyright 2024 The Cockroach Authors. +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package slstorage + +import ( + "testing" + + "github.com/cockroachdb/cockroach/pkg/sql/enum" + "github.com/cockroachdb/cockroach/pkg/sql/sqlliveness" + "github.com/cockroachdb/cockroach/pkg/util/leaktest" + "github.com/cockroachdb/cockroach/pkg/util/log" + "github.com/cockroachdb/cockroach/pkg/util/uuid" + "github.com/stretchr/testify/require" +) + +func FuzzSessionIDEncoding(f *testing.F) { + defer leaktest.AfterTest(f)() + defer log.Scope(f).Close(f) + + f.Add(string("")) + f.Add(string(uuid.MakeV4().GetBytes())) + + session, err := MakeSessionID(enum.One, uuid.MakeV4()) + require.NoError(f, err) + f.Add(string(session)) + + f.Fuzz(func(t *testing.T, randomSession string) { + session := sqlliveness.SessionID(randomSession) + region, id, err := UnsafeDecodeSessionID(session) + if err == nil { + if len([]byte(randomSession)) == 16 { + // A 16 bytes session is always valid, because it is the legacy uuid encoding. + require.Equal(t, []byte(randomSession), id) + } else { + // If the session is a valid encoding, then re-encoding the + // decoded pieces should produce an identical session. + require.Len(t, id, 16) + reEncoded, err := MakeSessionID(region, uuid.FromBytesOrNil(id)) + require.NoError(t, err) + require.Equal(t, session, reEncoded) + } + } + }) +} diff --git a/pkg/sql/sqlliveness/slstorage/sessionid_test.go b/pkg/sql/sqlliveness/slstorage/sessionid_test.go index 0b4ed821eaab..6a1b69c11e5c 100644 --- a/pkg/sql/sqlliveness/slstorage/sessionid_test.go +++ b/pkg/sql/sqlliveness/slstorage/sessionid_test.go @@ -22,36 +22,6 @@ import ( "github.com/stretchr/testify/require" ) -func FuzzSessionIDEncoding(f *testing.F) { - defer leaktest.AfterTest(f)() - defer log.Scope(f).Close(f) - - f.Add(string("")) - f.Add(string(uuid.MakeV4().GetBytes())) - - session, err := slstorage.MakeSessionID(enum.One, uuid.MakeV4()) - require.NoError(f, err) - f.Add(string(session)) - - f.Fuzz(func(t *testing.T, randomSession string) { - session := sqlliveness.SessionID(randomSession) - region, id, err := slstorage.UnsafeDecodeSessionID(session) - if err == nil { - if len([]byte(randomSession)) == 16 { - // A 16 bytes session is always valid, because it is the legacy uuid encoding. - require.Equal(t, []byte(randomSession), id) - } else { - // If the session is a valid encoding, then re-encoding the - // decoded pieces should produce an identical session. - require.Len(t, id, 16) - reEncoded, err := slstorage.MakeSessionID(region, uuid.FromBytesOrNil(id)) - require.NoError(t, err) - require.Equal(t, session, reEncoded) - } - } - }) -} - func TestMakeSessionIDValidation(t *testing.T) { defer leaktest.AfterTest(t)() defer log.Scope(t).Close(t) diff --git a/pkg/storage/BUILD.bazel b/pkg/storage/BUILD.bazel index c71e34dcc77d..542e5f5a77d0 100644 --- a/pkg/storage/BUILD.bazel +++ b/pkg/storage/BUILD.bazel @@ -12,6 +12,7 @@ go_library( "doc.go", "engine.go", "engine_key.go", + "engine_key_fuzz.go", "fingerprint_writer.go", "in_mem.go", "intent_interleaving_iter.go", diff --git a/pkg/storage/engine_key_fuzz.go b/pkg/storage/engine_key_fuzz.go new file mode 100644 index 000000000000..b1c7aacc5c26 --- /dev/null +++ b/pkg/storage/engine_key_fuzz.go @@ -0,0 +1,153 @@ +// Copyright 2024 The Cockroach Authors. +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package storage + +import ( + "bytes" + "math" + "testing" + + "github.com/cockroachdb/cockroach/pkg/keys" + "github.com/cockroachdb/cockroach/pkg/kv/kvserver/concurrency/lock" + "github.com/cockroachdb/cockroach/pkg/roachpb" + "github.com/cockroachdb/cockroach/pkg/util/hlc" + "github.com/cockroachdb/cockroach/pkg/util/uuid" +) + +func encodeLockTableKey(ltk LockTableKey) []byte { + ek, _ := ltk.ToEngineKey(nil) + return ek.Encode() +} + +// interestingEngineKeys is a slice of byte slices that may be used in tests as +// engine keys. Not all the keys are valid keys. +var interestingEngineKeys = [][]byte{ + {0x00, 0x00}, + {0x01, 0x11, 0x01}, + EncodeMVCCKey(MVCCKey{Key: roachpb.Key("a"), Timestamp: hlc.Timestamp{WallTime: math.MaxInt64}}), + EncodeMVCCKey(MVCCKey{Key: roachpb.Key("foo"), Timestamp: hlc.Timestamp{WallTime: 1691183078362053000}}), + EncodeMVCCKey(MVCCKey{Key: roachpb.Key("bar")}), + EncodeMVCCKey(MVCCKey{Key: roachpb.Key("bar"), Timestamp: hlc.Timestamp{WallTime: 1643550788737652545}}), + EncodeMVCCKey(MVCCKey{Key: roachpb.Key("bar"), Timestamp: hlc.Timestamp{WallTime: 1643550788737652545, Logical: 1}}), + encodeLockTableKey(LockTableKey{ + Key: roachpb.Key("foo"), + Strength: lock.Exclusive, + TxnUUID: uuid.Must(uuid.FromString("6ba7b810-9dad-11d1-80b4-00c04fd430c8")), + }), + encodeLockTableKey(LockTableKey{ + Key: keys.RangeDescriptorKey(roachpb.RKey("baz")), + Strength: lock.Exclusive, + TxnUUID: uuid.Must(uuid.FromString("6ba7b810-9dad-11d1-80b4-00c04fd430c8")), + }), +} + +// FuzzEngineKeysInvariants fuzz tests various functions over engine keys, +// ensuring that invariants over engine keys hold. +func FuzzEngineKeysInvariants(f *testing.F) { + for i := 0; i < len(interestingEngineKeys); i++ { + for j := 0; j < len(interestingEngineKeys); j++ { + f.Add(interestingEngineKeys[i], interestingEngineKeys[j]) + } + } + + compareEngineKeys := func(t *testing.T, a, b []byte) int { + cmp := EngineKeyCompare(a, b) + eq := EngineKeyEqual(a, b) + // Invariant: Iff EngineKeyCompare(a, b) == 0, EngineKeyEqual(a, b) + if eq != (cmp == 0) { + t.Errorf("EngineKeyEqual(0x%x, 0x%x) = %t; EngineKeyCompare(0x%x, 0x%x) = %d", + a, b, eq, a, b, cmp) + } + return cmp + } + computeImmediateSuccessor := func(t *testing.T, a []byte) []byte { + succ := EngineComparer.ImmediateSuccessor(nil, a) + // Invariant: ImmediateSuccessor(a) > a + if cmp := compareEngineKeys(t, a, succ); cmp >= 0 { + t.Errorf("ImmediateSuccessor(0x%x) = 0x%x, but EngineKeyCompare(0x%x, 0x%x) = %d", + a, succ, a, succ, cmp) + } + return succ + } + decodeEngineKey := func(t *testing.T, a []byte) (EngineKey, bool) { + // Invariant: DecodeEngineKey(a) ok iff GetKeyPartFromEngineKey(a) ok + // Invariant: DecodeEngineKey(a).Key == GetKeyPartFromEngineKey(a) + ek, ok1 := DecodeEngineKey(a) + kp, ok2 := GetKeyPartFromEngineKey(a) + if ok1 != ok2 || ok1 && !bytes.Equal(ek.Key, kp) { + t.Errorf("DecodeEngineKey(0x%x) = (%s, %t); but GetKeyPartFromEngineKey(0x%x) = (0x%x, %t)", + a, ek, ok1, a, kp, ok2) + } + + return ek, ok1 + } + + f.Fuzz(func(t *testing.T, a, b []byte) { + t.Logf("a = 0x%x; b = 0x%x", a, b) + cmp := compareEngineKeys(t, a, b) + if cmp == 0 { + return + } + if len(a) == 0 || len(b) == 0 { + return + } + + // Make a < b. + if cmp > 0 { + a, b = b, a + t.Logf("Swapped: a = 0x%x; b = 0x%x", a, b) + } + // Invariant: Separator(a, b) >= a + // Invariant: Separator(a, b) < b + sep := EngineComparer.Separator(nil, a, b) + if cmp = compareEngineKeys(t, a, b); cmp > 0 { + t.Errorf("Separator(0x%x, 0x%x) = 0x%x; but EngineKeyCompare(0x%x, 0x%x) = %d", + a, b, sep, a, sep, cmp) + } + if cmp = compareEngineKeys(t, sep, b); cmp >= 0 { + t.Errorf("Separator(0x%x, 0x%x) = 0x%x; but EngineKeyCompare(0x%x, 0x%x) = %d", + a, b, sep, sep, b, cmp) + } + ekA, okA := decodeEngineKey(t, a) + ekB, okB := decodeEngineKey(t, b) + if !okA || !okB { + return + } + errA := ekA.Validate() + errB := ekB.Validate() + // The below invariants only apply for valid keys. + if errA != nil || errB != nil { + return + } + t.Logf("ekA = %s (Key: 0x%x, Version: 0x%x); ekB = %s (Key: 0x%x, Version: 0x%x)", + ekA, ekA.Key, ekA.Version, ekB, ekB.Key, ekB.Version) + + splitA := EngineComparer.Split(a) + splitB := EngineComparer.Split(b) + aIsSuffixless := splitA == len(a) + bIsSuffixless := splitB == len(b) + // ImmediateSuccessor is only defined on prefix keys. + var immediateSuccessorA, immediateSuccessorB []byte + if aIsSuffixless { + immediateSuccessorA = computeImmediateSuccessor(t, a) + } + if bIsSuffixless { + immediateSuccessorB = computeImmediateSuccessor(t, b) + } + if aIsSuffixless && bIsSuffixless { + // Invariant: ImmediateSuccessor(a) < ImmediateSuccessor(b) + if cmp = compareEngineKeys(t, immediateSuccessorA, immediateSuccessorB); cmp >= 0 { + t.Errorf("ImmediateSuccessor(0x%x) = 0x%x, ImmediateSuccessor(0x%x) = 0x%x; but EngineKeyCompare(0x%x, 0x%x) = %d", + a, immediateSuccessorA, b, immediateSuccessorB, immediateSuccessorA, immediateSuccessorB, cmp) + } + } + }) +} diff --git a/pkg/storage/engine_key_test.go b/pkg/storage/engine_key_test.go index 672dbf014d5b..e22cde7246d5 100644 --- a/pkg/storage/engine_key_test.go +++ b/pkg/storage/engine_key_test.go @@ -11,7 +11,6 @@ package storage import ( - "bytes" "encoding/hex" "fmt" "math" @@ -311,28 +310,6 @@ func TestEngineKeyValidate(t *testing.T) { }) } -// interestingEngineKeys is a slice of byte slices that may be used in tests as -// engine keys. Not all the keys are valid keys. -var interestingEngineKeys = [][]byte{ - {0x00, 0x00}, - {0x01, 0x11, 0x01}, - EncodeMVCCKey(MVCCKey{Key: roachpb.Key("a"), Timestamp: hlc.Timestamp{WallTime: math.MaxInt64}}), - EncodeMVCCKey(MVCCKey{Key: roachpb.Key("foo"), Timestamp: hlc.Timestamp{WallTime: 1691183078362053000}}), - EncodeMVCCKey(MVCCKey{Key: roachpb.Key("bar")}), - EncodeMVCCKey(MVCCKey{Key: roachpb.Key("bar"), Timestamp: hlc.Timestamp{WallTime: 1643550788737652545}}), - EncodeMVCCKey(MVCCKey{Key: roachpb.Key("bar"), Timestamp: hlc.Timestamp{WallTime: 1643550788737652545, Logical: 1}}), - encodeLockTableKey(LockTableKey{ - Key: roachpb.Key("foo"), - Strength: lock.Exclusive, - TxnUUID: uuid.Must(uuid.FromString("6ba7b810-9dad-11d1-80b4-00c04fd430c8")), - }), - encodeLockTableKey(LockTableKey{ - Key: keys.RangeDescriptorKey(roachpb.RKey("baz")), - Strength: lock.Exclusive, - TxnUUID: uuid.Must(uuid.FromString("6ba7b810-9dad-11d1-80b4-00c04fd430c8")), - }), -} - func TestEngineKeyVerifyMVCC(t *testing.T) { defer leaktest.AfterTest(t)() @@ -489,114 +466,6 @@ func BenchmarkEngineKeyVerify(b *testing.B) { } } -// FuzzEngineKeysInvariants fuzz tests various functions over engine keys, -// ensuring that invariants over engine keys hold. -func FuzzEngineKeysInvariants(f *testing.F) { - for i := 0; i < len(interestingEngineKeys); i++ { - for j := 0; j < len(interestingEngineKeys); j++ { - f.Add(interestingEngineKeys[i], interestingEngineKeys[j]) - } - } - - compareEngineKeys := func(t *testing.T, a, b []byte) int { - cmp := EngineKeyCompare(a, b) - eq := EngineKeyEqual(a, b) - // Invariant: Iff EngineKeyCompare(a, b) == 0, EngineKeyEqual(a, b) - if eq != (cmp == 0) { - t.Errorf("EngineKeyEqual(0x%x, 0x%x) = %t; EngineKeyCompare(0x%x, 0x%x) = %d", - a, b, eq, a, b, cmp) - } - return cmp - } - computeImmediateSuccessor := func(t *testing.T, a []byte) []byte { - succ := EngineComparer.ImmediateSuccessor(nil, a) - // Invariant: ImmediateSuccessor(a) > a - if cmp := compareEngineKeys(t, a, succ); cmp >= 0 { - t.Errorf("ImmediateSuccessor(0x%x) = 0x%x, but EngineKeyCompare(0x%x, 0x%x) = %d", - a, succ, a, succ, cmp) - } - return succ - } - decodeEngineKey := func(t *testing.T, a []byte) (EngineKey, bool) { - // Invariant: DecodeEngineKey(a) ok iff GetKeyPartFromEngineKey(a) ok - // Invariant: DecodeEngineKey(a).Key == GetKeyPartFromEngineKey(a) - ek, ok1 := DecodeEngineKey(a) - kp, ok2 := GetKeyPartFromEngineKey(a) - if ok1 != ok2 || ok1 && !bytes.Equal(ek.Key, kp) { - t.Errorf("DecodeEngineKey(0x%x) = (%s, %t); but GetKeyPartFromEngineKey(0x%x) = (0x%x, %t)", - a, ek, ok1, a, kp, ok2) - } - - return ek, ok1 - } - - f.Fuzz(func(t *testing.T, a, b []byte) { - t.Logf("a = 0x%x; b = 0x%x", a, b) - cmp := compareEngineKeys(t, a, b) - if cmp == 0 { - return - } - if len(a) == 0 || len(b) == 0 { - return - } - - // Make a < b. - if cmp > 0 { - a, b = b, a - t.Logf("Swapped: a = 0x%x; b = 0x%x", a, b) - } - // Invariant: Separator(a, b) >= a - // Invariant: Separator(a, b) < b - sep := EngineComparer.Separator(nil, a, b) - if cmp = compareEngineKeys(t, a, b); cmp > 0 { - t.Errorf("Separator(0x%x, 0x%x) = 0x%x; but EngineKeyCompare(0x%x, 0x%x) = %d", - a, b, sep, a, sep, cmp) - } - if cmp = compareEngineKeys(t, sep, b); cmp >= 0 { - t.Errorf("Separator(0x%x, 0x%x) = 0x%x; but EngineKeyCompare(0x%x, 0x%x) = %d", - a, b, sep, sep, b, cmp) - } - ekA, okA := decodeEngineKey(t, a) - ekB, okB := decodeEngineKey(t, b) - if !okA || !okB { - return - } - errA := ekA.Validate() - errB := ekB.Validate() - // The below invariants only apply for valid keys. - if errA != nil || errB != nil { - return - } - t.Logf("ekA = %s (Key: 0x%x, Version: 0x%x); ekB = %s (Key: 0x%x, Version: 0x%x)", - ekA, ekA.Key, ekA.Version, ekB, ekB.Key, ekB.Version) - - splitA := EngineComparer.Split(a) - splitB := EngineComparer.Split(b) - aIsSuffixless := splitA == len(a) - bIsSuffixless := splitB == len(b) - // ImmediateSuccessor is only defined on prefix keys. - var immediateSuccessorA, immediateSuccessorB []byte - if aIsSuffixless { - immediateSuccessorA = computeImmediateSuccessor(t, a) - } - if bIsSuffixless { - immediateSuccessorB = computeImmediateSuccessor(t, b) - } - if aIsSuffixless && bIsSuffixless { - // Invariant: ImmediateSuccessor(a) < ImmediateSuccessor(b) - if cmp = compareEngineKeys(t, immediateSuccessorA, immediateSuccessorB); cmp >= 0 { - t.Errorf("ImmediateSuccessor(0x%x) = 0x%x, ImmediateSuccessor(0x%x) = 0x%x; but EngineKeyCompare(0x%x, 0x%x) = %d", - a, immediateSuccessorA, b, immediateSuccessorB, immediateSuccessorA, immediateSuccessorB, cmp) - } - } - }) -} - -func encodeLockTableKey(ltk LockTableKey) []byte { - ek, _ := ltk.ToEngineKey(nil) - return ek.Encode() -} - func randomMVCCKey(r *rand.Rand) MVCCKey { k := MVCCKey{ Key: randutil.RandBytes(r, randutil.RandIntInRange(r, 1, 12)), diff --git a/pkg/util/span/BUILD.bazel b/pkg/util/span/BUILD.bazel index e548c43d19fe..cbe6a811aa19 100644 --- a/pkg/util/span/BUILD.bazel +++ b/pkg/util/span/BUILD.bazel @@ -6,7 +6,9 @@ go_library( srcs = [ "doc.go", "frontier.go", + "frontier_fuzz.go", "llrb_frontier.go", + "test_helper.go", ":btreefrontierentry_interval_btree.go", #keep ], importpath = "github.com/cockroachdb/cockroach/pkg/util/span", @@ -19,6 +21,7 @@ go_library( "//pkg/util/hlc", "//pkg/util/interval", "//pkg/util/metamorphic", + "//pkg/util/randutil", "//pkg/util/syncutil", "@com_github_cockroachdb_errors//:errors", ], @@ -39,12 +42,10 @@ go_test( "//pkg/testutils/skip", "//pkg/util/encoding", "//pkg/util/hlc", - "//pkg/util/interval", "//pkg/util/leaktest", "//pkg/util/log", "//pkg/util/randutil", "//pkg/util/timeutil", #keep - "@com_github_cockroachdb_errors//:errors", "@com_github_stretchr_testify//require", ], ) diff --git a/pkg/util/span/frontier_fuzz.go b/pkg/util/span/frontier_fuzz.go new file mode 100644 index 000000000000..28b59c40573d --- /dev/null +++ b/pkg/util/span/frontier_fuzz.go @@ -0,0 +1,73 @@ +// Copyright 2024 The Cockroach Authors. +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package span + +import ( + "math/rand" + "testing" + + "github.com/cockroachdb/cockroach/pkg/roachpb" + "github.com/cockroachdb/cockroach/pkg/util/randutil" +) + +func fuzzFrontier(f *testing.F) { + seed := randutil.NewPseudoSeed() + rnd := rand.New(rand.NewSource(seed)) + + spanMaker, initialSpan := newSpanMaker(6, rnd) + const corpusSize = 2 << 10 + for i := 0; i < corpusSize; i++ { + s := spanMaker.rndSpan() + // Add fuzz corpus. Note: timestamps added could be negative, which + // of course is not a valid timestamp, but makes it so much fun to test. + f.Add([]byte(s.Key), []byte(s.EndKey), rnd.Intn(corpusSize)-rnd.Intn(corpusSize)) + } + + mkFrontier := func() Frontier { + sf, err := MakeFrontier(initialSpan) + if err != nil { + f.Fatal(err) + } + return sf + } + + sf := &captureHistoryFrontier{SpanFrontier: mkFrontier()} + + f.Fuzz(func(t *testing.T, startKey, endKey []byte, walltime int) { + // NB: copy start and end keys: fuzzer mutates inputs. + var sp roachpb.Span + sp.Key = append(sp.Key, startKey...) + sp.EndKey = append(sp.EndKey, endKey...) + + if err := forwardWithErrorCheck(sf, sp, int64(walltime)); err != nil { + t.Fatalf("err=%+v f=%s History:\n%s", err, sf, sf.History()) + } + + startKey, endKey, err := checkContiguousFrontier(sf) + if err != nil { + t.Fatalf("err=%s\nHistory:\n%s", err, sf.History()) + } + // At the end of iteration, we should have record start/end key equal to the initial span. + if !initialSpan.Key.Equal(startKey) || !initialSpan.EndKey.Equal(endKey) { + t.Fatalf("expected to see entire %s sf, saw [%s-%s)", initialSpan, startKey, endKey) + } + }) +} + +func FuzzBtreeFrontier(f *testing.F) { + defer enableBtreeFrontier(true)() + fuzzFrontier(f) +} + +func FuzzLLRBFrontier(f *testing.F) { + defer enableBtreeFrontier(false)() + fuzzFrontier(f) +} diff --git a/pkg/util/span/frontier_test.go b/pkg/util/span/frontier_test.go index 40b86bd9af83..2057fb96a583 100644 --- a/pkg/util/span/frontier_test.go +++ b/pkg/util/span/frontier_test.go @@ -23,11 +23,9 @@ import ( "github.com/cockroachdb/cockroach/pkg/testutils/skip" "github.com/cockroachdb/cockroach/pkg/util/encoding" "github.com/cockroachdb/cockroach/pkg/util/hlc" - "github.com/cockroachdb/cockroach/pkg/util/interval" "github.com/cockroachdb/cockroach/pkg/util/leaktest" "github.com/cockroachdb/cockroach/pkg/util/log" "github.com/cockroachdb/cockroach/pkg/util/randutil" - "github.com/cockroachdb/errors" "github.com/stretchr/testify/require" ) @@ -398,81 +396,6 @@ func TestSpanEntries(t *testing.T) { }) } -func checkContiguousFrontier(f Frontier) (startKey, endKey []byte, retErr error) { - // Iterate frontier to make sure it is sane. - prev := struct { - s roachpb.Span - ts hlc.Timestamp - }{} - - frontierSpan := f.PeekFrontierSpan() - frontierTS := f.Frontier() - sawFrontierSpan := false - - f.Entries(func(s roachpb.Span, ts hlc.Timestamp) (done OpResult) { - if s.Equal(frontierSpan) && ts.Equal(frontierTS) { - sawFrontierSpan = true - } - - if prev.s.Key == nil && prev.s.EndKey == nil { - prev.s = s - prev.ts = ts - startKey = s.Key - endKey = s.EndKey - return ContinueMatch - } - - if s.Key.Equal(prev.s.EndKey) { - // Contiguous spans with the same timestamps are expected to be merged. - // However, LLRB based frontier has some gaps in its merge logic, so just - // let it be. - if useBtreeFrontier && ts.Equal(prev.ts) { - retErr = errors.Newf("expected ranges with equal timestamp to be merged, found %s and %s: %s", prev.s, s, f) - return StopMatch - } - } else { - // We expect frontier entries to be contiguous. - retErr = errors.Newf("expected contiguous entries, found gap between %s and %s: %s", prev.s, s, f) - return StopMatch - } - - endKey = s.EndKey - prev.s = s - prev.ts = ts - return ContinueMatch - }) - - if !sawFrontierSpan { - return startKey, endKey, errors.Newf("expected to find frontier span %s@%s: %s", frontierSpan, frontierTS, f) - } - - return startKey, endKey, retErr -} - -// forwardWithErrorCheck forwards span timestamp. -// It verifies if the returned error is consistent with the input span. -func forwardWithErrorCheck(f Frontier, s roachpb.Span, wall int64) error { - if _, err := f.Forward(s, hlc.Timestamp{WallTime: wall}); err != nil { - switch s.Key.Compare(s.EndKey) { - case 1: - if !errors.Is(err, interval.ErrInvertedRange) { - return errors.Wrapf(err, "expected inverted span error for span %s", s) - } - case 0: - if len(s.Key) == 0 && len(s.EndKey) == 0 { - if !errors.Is(err, interval.ErrNilRange) { - return errors.Wrapf(err, "expected nil range error for span %s", s) - } - } else if !errors.Is(err, interval.ErrEmptyRange) { - return errors.Wrapf(err, "expected empty span error for span %s", s) - } - default: - return errors.Wrapf(err, "f=%s", f) - } - } - return nil -} - func advanceFrontier(t *testing.T, f Frontier, s roachpb.Span, wall int64) { t.Helper() if log.V(1) { @@ -651,148 +574,6 @@ func TestForwardDeepNestedFrontierEntry(t *testing.T) { }) } -// symbols that can make up spans. -var spanSymbols = []byte("@$0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") - -type spanMaker struct { - rnd *rand.Rand - numSymbols int - starts []interval.Comparable -} - -func newSpanMaker(numSymbols int, rnd *rand.Rand) (*spanMaker, roachpb.Span) { - m := &spanMaker{ - rnd: rnd, - numSymbols: numSymbols, - } - span := roachpb.Span{ - Key: roachpb.Key{'A'}, - EndKey: roachpb.Key{'z'}, - } - return m, span -} - -func (m *spanMaker) rndKey() interval.Comparable { - var key []byte - for n := 1 + m.rnd.Intn(m.numSymbols); n > 0; n-- { - key = append(key, spanSymbols[m.rnd.Intn(len(spanSymbols))]) - } - return key -} - -func (m *spanMaker) rndSpan() roachpb.Span { - var startKey interval.Comparable - - if len(m.starts) > 0 && m.rnd.Int()%37 == 0 { - // With some probability use previous starting point. - startKey = append(startKey, m.starts[m.rnd.Intn(len(m.starts))]...) - // Just for fun, nudge start a bit forward or back. - if dice := m.rnd.Intn(3) - 1; dice != 0 { - startKey[len(startKey)-1] += byte(dice) - } - } else { - // Generate a new start. - startKey = m.rndKey() - m.starts = append(m.starts, startKey) - } - - endKey := m.rndKey() - // With some probability, make startKey prefix of endKey. - if m.rnd.Int()%97 == 0 { - endKey = append(startKey, endKey...) - } - - if startKey.Equal(endKey) { - endKey = append(endKey, spanSymbols[m.rnd.Intn(len(spanSymbols))]) - } - - if endKey.Compare(startKey) < 0 { - startKey, endKey = endKey, startKey - } - if endKey.Equal(startKey) { - panic(roachpb.Span{Key: roachpb.Key(startKey), EndKey: roachpb.Key(endKey)}.String()) - } - return roachpb.Span{Key: roachpb.Key(startKey), EndKey: roachpb.Key(endKey)} -} - -const maxHistory = 64 - -// captureHistoryFrontier is a Frontier that captures history -// of forward calls in order to make it easier to reproduce fuzz test failures. -// See TestForwardInvertedSpan. -type SpanFrontier = Frontier -type captureHistoryFrontier struct { - SpanFrontier - history []string -} - -func (f *captureHistoryFrontier) Forward(span roachpb.Span, ts hlc.Timestamp) (bool, error) { - f.history = append(f.history, - fmt.Sprintf(`advanceFrontier(t, f, makeSpan(%q, %q), %d)`, span.Key, span.EndKey, ts.WallTime)) - if len(f.history) > maxHistory { - f.history = append([]string{}, f.history[1:]...) - } - return f.SpanFrontier.Forward(span, ts) -} - -func (f *captureHistoryFrontier) History() string { - return strings.Join(f.history, "\n") -} - -func fuzzFrontier(f *testing.F) { - seed := randutil.NewPseudoSeed() - rnd := rand.New(rand.NewSource(seed)) - - spanMaker, initialSpan := newSpanMaker(6, rnd) - const corpusSize = 2 << 10 - for i := 0; i < corpusSize; i++ { - s := spanMaker.rndSpan() - // Add fuzz corpus. Note: timestamps added could be negative, which - // of course is not a valid timestamp, but makes it so much fun to test. - f.Add([]byte(s.Key), []byte(s.EndKey), rnd.Intn(corpusSize)-rnd.Intn(corpusSize)) - } - - mkFrontier := func() Frontier { - sf, err := MakeFrontier(initialSpan) - if err != nil { - f.Fatal(err) - } - return sf - } - - sf := &captureHistoryFrontier{SpanFrontier: mkFrontier()} - - f.Fuzz(func(t *testing.T, startKey, endKey []byte, walltime int) { - // NB: copy start and end keys: fuzzer mutates inputs. - var sp roachpb.Span - sp.Key = append(sp.Key, startKey...) - sp.EndKey = append(sp.EndKey, endKey...) - - if err := forwardWithErrorCheck(sf, sp, int64(walltime)); err != nil { - t.Fatalf("err=%+v f=%s History:\n%s", err, sf, sf.History()) - } - - startKey, endKey, err := checkContiguousFrontier(sf) - if err != nil { - t.Fatalf("err=%s\nHistory:\n%s", err, sf.History()) - } - // At the end of iteration, we should have record start/end key equal to the initial span. - if !initialSpan.Key.Equal(startKey) || !initialSpan.EndKey.Equal(endKey) { - t.Fatalf("expected to see entire %s sf, saw [%s-%s)", initialSpan, startKey, endKey) - } - }) -} - -func FuzzBtreeFrontier(f *testing.F) { - defer enableBtreeFrontier(true)() - fuzzFrontier(f) -} - -func FuzzLLRBFrontier(f *testing.F) { - defer enableBtreeFrontier(false)() - fuzzFrontier(f) -} - func BenchmarkFrontier(b *testing.B) { disableSanityChecksForBenchmark = true defer func() { diff --git a/pkg/util/span/test_helper.go b/pkg/util/span/test_helper.go new file mode 100644 index 000000000000..305ba7387220 --- /dev/null +++ b/pkg/util/span/test_helper.go @@ -0,0 +1,185 @@ +// Copyright 2024 The Cockroach Authors. +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package span + +import ( + "fmt" + "math/rand" + "strings" + + "github.com/cockroachdb/cockroach/pkg/roachpb" + "github.com/cockroachdb/cockroach/pkg/util/hlc" + "github.com/cockroachdb/cockroach/pkg/util/interval" + "github.com/cockroachdb/errors" +) + +func checkContiguousFrontier(f Frontier) (startKey, endKey []byte, retErr error) { + // Iterate frontier to make sure it is sane. + prev := struct { + s roachpb.Span + ts hlc.Timestamp + }{} + + frontierSpan := f.PeekFrontierSpan() + frontierTS := f.Frontier() + sawFrontierSpan := false + + f.Entries(func(s roachpb.Span, ts hlc.Timestamp) (done OpResult) { + if s.Equal(frontierSpan) && ts.Equal(frontierTS) { + sawFrontierSpan = true + } + + if prev.s.Key == nil && prev.s.EndKey == nil { + prev.s = s + prev.ts = ts + startKey = s.Key + endKey = s.EndKey + return ContinueMatch + } + + if s.Key.Equal(prev.s.EndKey) { + // Contiguous spans with the same timestamps are expected to be merged. + // However, LLRB based frontier has some gaps in its merge logic, so just + // let it be. + if useBtreeFrontier && ts.Equal(prev.ts) { + retErr = errors.Newf("expected ranges with equal timestamp to be merged, found %s and %s: %s", prev.s, s, f) + return StopMatch + } + } else { + // We expect frontier entries to be contiguous. + retErr = errors.Newf("expected contiguous entries, found gap between %s and %s: %s", prev.s, s, f) + return StopMatch + } + + endKey = s.EndKey + prev.s = s + prev.ts = ts + return ContinueMatch + }) + + if !sawFrontierSpan { + return startKey, endKey, errors.Newf("expected to find frontier span %s@%s: %s", frontierSpan, frontierTS, f) + } + + return startKey, endKey, retErr +} + +// forwardWithErrorCheck forwards span timestamp. +// It verifies if the returned error is consistent with the input span. +func forwardWithErrorCheck(f Frontier, s roachpb.Span, wall int64) error { + if _, err := f.Forward(s, hlc.Timestamp{WallTime: wall}); err != nil { + switch s.Key.Compare(s.EndKey) { + case 1: + if !errors.Is(err, interval.ErrInvertedRange) { + return errors.Wrapf(err, "expected inverted span error for span %s", s) + } + case 0: + if len(s.Key) == 0 && len(s.EndKey) == 0 { + if !errors.Is(err, interval.ErrNilRange) { + return errors.Wrapf(err, "expected nil range error for span %s", s) + } + } else if !errors.Is(err, interval.ErrEmptyRange) { + return errors.Wrapf(err, "expected empty span error for span %s", s) + } + default: + return errors.Wrapf(err, "f=%s", f) + } + } + return nil +} + +// symbols that can make up spans. +var spanSymbols = []byte("@$0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") + +type spanMaker struct { + rnd *rand.Rand + numSymbols int + starts []interval.Comparable +} + +func newSpanMaker(numSymbols int, rnd *rand.Rand) (*spanMaker, roachpb.Span) { + m := &spanMaker{ + rnd: rnd, + numSymbols: numSymbols, + } + span := roachpb.Span{ + Key: roachpb.Key{'A'}, + EndKey: roachpb.Key{'z'}, + } + return m, span +} + +func (m *spanMaker) rndKey() interval.Comparable { + var key []byte + for n := 1 + m.rnd.Intn(m.numSymbols); n > 0; n-- { + key = append(key, spanSymbols[m.rnd.Intn(len(spanSymbols))]) + } + return key +} + +func (m *spanMaker) rndSpan() roachpb.Span { + var startKey interval.Comparable + + if len(m.starts) > 0 && m.rnd.Int()%37 == 0 { + // With some probability use previous starting point. + startKey = append(startKey, m.starts[m.rnd.Intn(len(m.starts))]...) + // Just for fun, nudge start a bit forward or back. + if dice := m.rnd.Intn(3) - 1; dice != 0 { + startKey[len(startKey)-1] += byte(dice) + } + } else { + // Generate a new start. + startKey = m.rndKey() + m.starts = append(m.starts, startKey) + } + + endKey := m.rndKey() + // With some probability, make startKey prefix of endKey. + if m.rnd.Int()%97 == 0 { + endKey = append(startKey, endKey...) + } + + if startKey.Equal(endKey) { + endKey = append(endKey, spanSymbols[m.rnd.Intn(len(spanSymbols))]) + } + + if endKey.Compare(startKey) < 0 { + startKey, endKey = endKey, startKey + } + if endKey.Equal(startKey) { + panic(roachpb.Span{Key: roachpb.Key(startKey), EndKey: roachpb.Key(endKey)}.String()) + } + return roachpb.Span{Key: roachpb.Key(startKey), EndKey: roachpb.Key(endKey)} +} + +const maxHistory = 64 + +// captureHistoryFrontier is a Frontier that captures history +// of forward calls in order to make it easier to reproduce fuzz test failures. +// See TestForwardInvertedSpan. +type SpanFrontier = Frontier +type captureHistoryFrontier struct { + SpanFrontier + history []string +} + +func (f *captureHistoryFrontier) Forward(span roachpb.Span, ts hlc.Timestamp) (bool, error) { + f.history = append(f.history, + fmt.Sprintf(`advanceFrontier(t, f, makeSpan(%q, %q), %d)`, span.Key, span.EndKey, ts.WallTime)) + if len(f.history) > maxHistory { + f.history = append([]string{}, f.history[1:]...) + } + return f.SpanFrontier.Forward(span, ts) +} + +func (f *captureHistoryFrontier) History() string { + return strings.Join(f.history, "\n") +}